1. 程式人生 > >word2vec原始碼解析(註釋合理版本)

word2vec原始碼解析(註釋合理版本)

word2vec詞向量學習筆記

一、使用原版word2vec工具訓練

1、英文編譯測試

  1. git clone https://github.com/hjimce/word2vec.git  

(2)編譯:make

(3)下載測試資料http://mattmahoney.NET/dc/text8.zip,並解壓

(4)輸入命令train起來:

  1. time ./word2vec -train text8 -output vectors.bin -cbow 1 -size 200 -window 8 -negative 25 -hs 0 -sample 1e-4 -threads 20 -binary 
    1 -iter 15

(5)測試距離功能:

  1. ./distance vectors.bin  

 2、中文訓練測試

(1)中文詞向量:下載資料msr_training.utf8,這個資料已經做好分詞工作,如果想要直接使用自己的資料,就需要先做好分詞工作

(2)輸入命令train起來: 

  1. 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)啟動相似度距離測試:

  1. ./distance vectors.bin  

(4)輸入相關中文詞:中國,檢視結果:
  1. [email protected]:~/workspace/word2vec$ ./distance vectors.bin  
  2. Enter word or sentence (EXIT to break): 中國  
  3. Word: 中國  Position in vocabulary: 35
  4.                                               Word       Cosine distance  
  5. ------------------------------------------------------------------------  
  6.                                       中國人民      0.502711
  7.                                             美國      0.480650
  8.                                       中國政府      0.463177
  9.                                             我國      0.447327
  10.                                       亞太地區      0.444878
  11.                                          加勒比        0.418471
  12.                                             兩國      0.408678
  13.                                             各國      0.392190
  14.                                       獨立自主      0.391517
  15.                                             世界      0.387604
  16.                                             國際      0.382578
  17.                                             中美      0.382208
  18.                                             歐美      0.379807
  19.                                             古巴      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,就是除了正樣本標籤之外,還需要隨機抽取出詞典中的其它單詞作為負樣本(以前是把整個詞典的其它單詞都當成負樣本),這個還是具體看原始碼實現吧。

三、原始碼閱讀階段

  1. word = sen[sentence_position];//當前單詞
  2. if (word == -1) continue;  
  3. for (c = 0; c < layer1_size; c++) neu1[c] = 0;//隱藏層神經元啟用值初始化為0
  4. for (c = 0; c < layer1_size; c++) neu1e[c] = 0;//隱藏層反向求導的誤差值
  5. next_random = next_random * (unsigned longlong)25214903917 + 11;  
  6. b = next_random % window;//b是一個介於0~window size之間的隨機數值
  7. //cbow演算法,網路第一層:直接把左右視窗內的單詞相加起來
  8. //sentence_position是當前單詞
  9. if (cbow) {  
  10.   cw = 0;  
  11.   //演算法第一步:除了當前單詞之外,把上下文視窗單詞的詞向量相加起來
  12.   for (a = b; a < window * 2 + 1 - b; a++)  
  13.     if (a != window)  
  14.     {  
  15.       c = sentence_position - window + a;//遍歷sentence_position視窗內的上下文單詞
  16.       if (c < 0) continue;//邊界越界處理,因為一個文件的長度是0~sentence_length
  17.       if (c >= sentence_length) continue;  
  18.       last_word = sen[c];  
  19.       if (last_word == -1) continue;  
  20.       for (c = 0; c < layer1_size; c++)//把左右視窗內的單詞的詞向量全部相加,其中layer1_size表示詞向量的維度
  21.       {  
  22.         neu1[c] += syn0[c + last_word * layer1_size];//syn0是詞向量表
  23.       }  
  24.       cw++;  
  25.     }  
  26.   if (cw)  
  27.   {  
  28.   //演算法第二步:平均,把上面左右單詞的詞向量加起來後再平均
  29.     for (c = 0; c < layer1_size; c++)  
  30.     {  
  31.       neu1[c] /= cw;  
  32.     }  
  33.     if (hs)  
  34.       //vocab[word].codelen表示當前單詞的霍夫曼編碼長度,下面也就遍歷當前詞的路徑節點
  35.       for (d = 0; d < vocab[word].codelen; d++)  
  36.       {  
  37.   //演算法第三步:啟用了層次sorfmax模式,計算每個節點的概率
  38.         //f是二叉樹節點的輸出值,下面是計算f,f是每個節點的邏輯迴歸概率值
  39.         f = 0;  
  40.         l2 = vocab[word].point[d] * layer1_size;  
  41.         for (c = 0; c < layer1_size; c++) f += neu1[c] * syn1[c + l2];//syn1是從隱藏層到二叉樹節點連線引數矩陣(可以把二叉樹節點看成是神經元)
  42.         //節點的label被定義為:1-code,而不是code,這個可能是為了方便計算吧
  43.         // 那麼節點損失函式為-[(1-code)*log(f)+code*log(1-f)]
  44.         if (f <= -MAX_EXP) continue;  
  45.         elseif (f >= MAX_EXP) continue;  
  46.         else f = expTable[(int)((f + MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))];  
  47.   //演算法第四步:反向求導
  48.         // 'g' is the gradient multiplied by the learning rate
  49.         g = (1 - vocab[word].code[d] - f) * alpha;//梯度×學習率
  50.         // Propagate errors output -> hidden
  51.         for (c = 0; c < layer1_size; c++) neu1e[c] += g * syn1[c + l2];  
  52.         // Learn weights hidden -> output
  53.         for (c = 0; c < layer1_size; c++) syn1[c + l2] += g * neu1[c];  
  54.       }  
  55.   //如果採用了negative sample
  56.     if (negative > 0)  
  57.       for (d = 0; d < negative + 1; d++)  
  58.       {  
  59.         //正樣本
  60.         if (d == 0)  
  61.           {  
  62.             target = word;  
  63.             label = 1;  
  64.           }  
  65.         //隨機取樣出負樣本
  66.         else
  67.         {  
  68.           next_random = next_random * (unsigned longlong)25214903917 + 11;  
  69.           target = table[(next_random >> 16) % table_size];  
  70.           if (target == 0) target = next_random % (vocab_size - 1) + 1;  
  71.           if (target == word) continue;  
  72.           label = 0;  
  73.         }  
  74.         l2 = target * layer1_size;  
  75.         f = 0;  
  76.         for (c = 0; c < layer1_size; c++) f += neu1[c] * syn1neg[c + l2];//同樣計算輸出單詞,邏輯迴歸概率值
  77.         //似然函式為-[label*log(f)+(1-label)*log(1-f)]
  78.         //更新連結到當前類別(單詞)節點的權值向量
  79.         if (f > MAX_EXP) g = (label - 1) * alpha;  
  80.         elseif (f < -MAX_EXP) g = (label - 0) * alpha;  
  81.         else g = (label - expTable[(int)((f + MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))]) * alpha;  
  82.         for (c = 0; c < layer1_size; c++) neu1e[c] += g * syn1neg[c + l2];  
  83.         for (c = 0; c < layer1_size; c++) syn1neg[c + l2] += g * neu1[c];  
  84.     }  
  85.     //從隱藏層到詞向量層反向傳播,更新詞向量層
  86.     for (a = b; a < window * 2 + 1 - b; a++) if (a != window)  
  87.       {  
  88.       c = sentence_position - window + a;  
  89.       if (c < 0) continue;  
  90.       if (c >= sentence_length) continue;  
  91.       last_word = sen[c];  
  92.       if (last_word == -1) continue;  
  93.       for (c = 0; c < layer1_size; c++) syn0[c + last_word * layer1_size] += neu1e[c];  
  94.       }  
  95.   }  
  96. }  

參考文獻:

1、《Efficient Estimation of Word Representations in Vector Space

2、《word2vec Explained: Deriving Mikolov et al.'s Negative-Sampling Word-Embedding Method