word2vec原始碼解析(註釋合理版本)
word2vec詞向量學習筆記
一、使用原版word2vec工具訓練
1、英文編譯測試
- git clone https://github.com/hjimce/word2vec.git
(2)編譯:make
(3)下載測試資料http://mattmahoney.NET/dc/text8.zip,並解壓
(4)輸入命令train起來:
-
time ./word2vec -train text8 -output vectors.bin -cbow 1 -size 200 -window 8 -negative 25 -hs 0 -sample 1e-4 -threads 20 -binary
(5)測試距離功能:
- ./distance vectors.bin
2、中文訓練測試
(1)中文詞向量:下載資料msr_training.utf8,這個資料已經做好分詞工作,如果想要直接使用自己的資料,就需要先做好分詞工作
(2)輸入命令train起來:
- time ./word2vec -train msr_training.utf8 -output vectors.bin -cbow 1 -size 200 -window 8 -negative 25 -hs 0 -sample 1e-4 -threads 20 -binary 1 -iter 15
(3)啟動相似度距離測試:
- ./distance vectors.bin
(4)輸入相關中文詞:中國,檢視結果:
- [email protected]:~/workspace/word2vec$ ./distance vectors.bin
- Enter word or sentence (EXIT to break): 中國
- Word: 中國 Position in vocabulary: 35
- Word Cosine distance
-
------------------------------------------------------------------------
- 中國人民 0.502711
- 美國 0.480650
- 中國政府 0.463177
- 我國 0.447327
- 亞太地區 0.444878
- 加勒比 0.418471
- 兩國 0.408678
- 各國 0.392190
- 獨立自主 0.391517
- 世界 0.387604
- 國際 0.382578
- 中美 0.382208
- 歐美 0.379807
- 古巴 0.378412
二、演算法學習階段
因為這個演算法是半年前所學的演算法,最近只是簡單複習一下,所以不打算寫詳細的演算法流程筆記,原理等也不打算囉嗦。word2vec網路結構可以分成兩種:CBOW、Skip-Gram,其實網路結構都非常簡單,不過是一個三層神經網路罷了。本文只講解CBOW網路結構演算法、演算法流程。
CBOW又有兩種方案,一種叫層次softmax,另一種叫:negative sample。這兩種方法如果看不懂也沒關係,你完全可以用原始的softmax替代網路的最後一層,進行訓練,只是訓練速度比較慢。
1、CBOW+層次softmax演算法總體流程
先講解層次softmax的演算法實現過程:
(1)根據訓練語料庫,建立詞典,並對詞典每個單詞進行二叉樹霍夫曼編碼。
如下圖所示,比如經過編碼後,可能漢語詞典中的“自”就被編碼成了:110,“我”對應的編碼就是:001。這個演算法與word2vec的實現過程關係不大,具體霍夫曼編碼過程程式碼怎麼寫,不懂也沒關係。我們只需要記住,字典中的每個單詞都會被編碼,每個單詞對應二叉樹的葉節點,每個單詞的編碼結果對應於:從跟節點到達當前單詞,所經過的節點路徑。編碼中的1、0表示右節點、左節點。
這邊需要知道的是每一位編碼的用處,因為每位的編碼節點剛好可以對應到0、1輸出標籤,而且這顆二叉樹節點就是神經網路的輸出層神經元,具體可以看下面的圖。假如在訓練的時候,最後一層訓練資料的輸出是“自”,那麼其實我們只需要訓練節點root、node1、node3使得這三個節點的啟用值分別為:1、1、0,這樣就可以了。
因此對於層次softmax來說,神經網路的隱藏層其實是連線到二叉樹的每個非葉子節點上(如果是原始的sotfmax,是直接連線到葉子節點上),然後對這些非葉子節點,根據輸出單詞的編碼路徑,對路徑上的每個節點,根據對應的編碼進行訓練。
(2)根據定義視窗大小,截取出一個視窗內的單詞作為一個樣本,進行訓練
這一步在word2vec裡面,其實我們給出的引數值是一個max window size,然後word2vec底層生成一個隨機大小的視窗,只要滿足其範圍在max window size 即可,這個可能是為了資料擴充吧。
(3)輸入層-》隱藏層:對視窗內的左右單詞(不包含自己)對應的詞向量,進行累加,並求取平均向量Vavg。
(4)隱藏層-》非葉子節點:每個二叉樹非葉子節點,連結到Vavg都有一個可學習的引數W,然後通過sigmoid可以得到每個非葉子節點的啟用值(也表示概率值):
也就是說網路的引數除了詞向量之外,還有隱藏層連結到二叉樹結點的引數向量(從word2vec程式碼上看,我沒有看到偏置引數b)。
(5)反向求導:根據輸出文字的節點路徑,更新路徑上的每個非葉子節點連結到隱藏層的引數值w;並更新視窗內各個單詞的詞向量。
具體網路結構圖如下所示:
採用層次softmax的優點在於加快訓練速度,引數個數、預測速度沒啥差別。
2、CBOW+Negative Sample
這個比較簡單,所謂的Negative Sample,就是除了正樣本標籤之外,還需要隨機抽取出詞典中的其它單詞作為負樣本(以前是把整個詞典的其它單詞都當成負樣本),這個還是具體看原始碼實現吧。
三、原始碼閱讀階段
- word = sen[sentence_position];//當前單詞
- if (word == -1) continue;
- for (c = 0; c < layer1_size; c++) neu1[c] = 0;//隱藏層神經元啟用值初始化為0
- for (c = 0; c < layer1_size; c++) neu1e[c] = 0;//隱藏層反向求導的誤差值
- next_random = next_random * (unsigned longlong)25214903917 + 11;
- b = next_random % window;//b是一個介於0~window size之間的隨機數值
- //cbow演算法,網路第一層:直接把左右視窗內的單詞相加起來
- //sentence_position是當前單詞
- if (cbow) {
- cw = 0;
- //演算法第一步:除了當前單詞之外,把上下文視窗單詞的詞向量相加起來
- for (a = b; a < window * 2 + 1 - b; a++)
- if (a != window)
- {
- c = sentence_position - window + a;//遍歷sentence_position視窗內的上下文單詞
- if (c < 0) continue;//邊界越界處理,因為一個文件的長度是0~sentence_length
- if (c >= sentence_length) continue;
- last_word = sen[c];
- if (last_word == -1) continue;
- for (c = 0; c < layer1_size; c++)//把左右視窗內的單詞的詞向量全部相加,其中layer1_size表示詞向量的維度
- {
- neu1[c] += syn0[c + last_word * layer1_size];//syn0是詞向量表
- }
- cw++;
- }
- if (cw)
- {
- //演算法第二步:平均,把上面左右單詞的詞向量加起來後再平均
- for (c = 0; c < layer1_size; c++)
- {
- neu1[c] /= cw;
- }
- if (hs)
- //vocab[word].codelen表示當前單詞的霍夫曼編碼長度,下面也就遍歷當前詞的路徑節點
- for (d = 0; d < vocab[word].codelen; d++)
- {
- //演算法第三步:啟用了層次sorfmax模式,計算每個節點的概率
- //f是二叉樹節點的輸出值,下面是計算f,f是每個節點的邏輯迴歸概率值
- f = 0;
- l2 = vocab[word].point[d] * layer1_size;
- for (c = 0; c < layer1_size; c++) f += neu1[c] * syn1[c + l2];//syn1是從隱藏層到二叉樹節點連線引數矩陣(可以把二叉樹節點看成是神經元)
- //節點的label被定義為:1-code,而不是code,這個可能是為了方便計算吧
- // 那麼節點損失函式為-[(1-code)*log(f)+code*log(1-f)]
- if (f <= -MAX_EXP) continue;
- elseif (f >= MAX_EXP) continue;
- else f = expTable[(int)((f + MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))];
- //演算法第四步:反向求導
- // 'g' is the gradient multiplied by the learning rate
- g = (1 - vocab[word].code[d] - f) * alpha;//梯度×學習率
- // Propagate errors output -> hidden
- for (c = 0; c < layer1_size; c++) neu1e[c] += g * syn1[c + l2];
- // Learn weights hidden -> output
- for (c = 0; c < layer1_size; c++) syn1[c + l2] += g * neu1[c];
- }
- //如果採用了negative sample
- if (negative > 0)
- for (d = 0; d < negative + 1; d++)
- {
- //正樣本
- if (d == 0)
- {
- target = word;
- label = 1;
- }
- //隨機取樣出負樣本
- else
- {
- next_random = next_random * (unsigned longlong)25214903917 + 11;
- target = table[(next_random >> 16) % table_size];
- if (target == 0) target = next_random % (vocab_size - 1) + 1;
- if (target == word) continue;
- label = 0;
- }
- l2 = target * layer1_size;
- f = 0;
- for (c = 0; c < layer1_size; c++) f += neu1[c] * syn1neg[c + l2];//同樣計算輸出單詞,邏輯迴歸概率值
- //似然函式為-[label*log(f)+(1-label)*log(1-f)]
- //更新連結到當前類別(單詞)節點的權值向量
- if (f > MAX_EXP) g = (label - 1) * alpha;
- elseif (f < -MAX_EXP) g = (label - 0) * alpha;
- else g = (label - expTable[(int)((f + MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))]) * alpha;
- for (c = 0; c < layer1_size; c++) neu1e[c] += g * syn1neg[c + l2];
- for (c = 0; c < layer1_size; c++) syn1neg[c + l2] += g * neu1[c];
- }
- //從隱藏層到詞向量層反向傳播,更新詞向量層
- for (a = b; a < window * 2 + 1 - b; a++) if (a != window)
- {
- c = sentence_position - window + a;
- if (c < 0) continue;
- if (c >= sentence_length) continue;
- last_word = sen[c];
- if (last_word == -1) continue;
- for (c = 0; c < layer1_size; c++) syn0[c + last_word * layer1_size] += neu1e[c];
- }
- }
- }
參考文獻:
1、《Efficient Estimation of Word Representations in Vector Space》
2、《word2vec Explained: Deriving Mikolov et al.'s Negative-Sampling Word-Embedding Method》