word2vec
word2vec是Google在2013年推出的一個工具。word2vec通過訓練,可以將所有的詞向量化,這樣就可以定量的去度量詞與詞之間的關系,挖掘詞之間的聯系;同時還可以將詞向量輸入到各種RNN網絡中進一步處理。因此,word2vec 輸出的詞向量可以被用來做很多自然語言處理相關的工作,比如聚類、找同義詞、詞性分析等等、文本分析等,是自然語言處理的重要基礎。
本文希望能帶你快速入門word2vec。
paper link
Efficient Estimation of Word Representations in Vector Space
word2vec Parameter Learning Explained
基本概念掃盲
語料庫(corpus)、詞(word)、詞匯表(vocabulary)
語料庫一般是指一些根據研究需要從自網站、新聞、報紙等采集的大規模文本。而“詞”是語料中的最小單位。例如:
Winners do what losers do not want to do.
在處理這段非常簡單的語料中,我們會把"winners"作為一個完整詞處理,而不會把詞分為字符單獨處理。通常在自然語言處理(NLP)中,詞也是最小單位,所以有“詞向量”,但是沒有“字符向量“。
而統計語料中出現的不重復的詞構成即可構成詞匯表。如上段語料中出現了"winners losers what want do not to"這7個詞,其中do出現了3次。
為什么要進行詞嵌入(word embedding)?

以機器翻譯為例,要把"you"輸入到RNN網絡中,必須把單詞轉化為一個向量(即把詞“嵌入”到高維空間)。
那么最簡單粗暴的詞嵌入方法就是one-hot編碼:
那么one-hot編碼有什么缺點呢?
-
維度災難
一般情況下,常用英語單詞約8000個,如果使用one-hot編碼,每個詞向量就是8000維;對應的如果有100000個詞,那么每個詞向量就是100000維。在實際應用中,詞向量維度太大,會造成網絡參數量大、網絡推理速度慢、網絡運行占用內存高等問題。
-
編碼過於稀疏
在one-hot編碼的詞向量中,數值幾乎全部是0,非常稀疏,很可能導致實際中網絡難以收斂。
-
無法表示詞間的關系
有向量\(A=(a_1,a_2,...,a_n)\)和\(B=(b_1,b_2,...,b_n)\),定義\(A\)和\(B\)之間相似度為
\[similarity=cos(\theta)=\frac{A\cdot B}{\left \| A \right \| \left \| B \right \|}=\frac{\sum^n_{i=1}a_i\cdot b_i}{\sqrt{\sum^n_{i=1}(a_i)^2}\sqrt{\sum^n_{i=1}(b_i)^2}} \]對於one-hot編碼,任意兩個詞間的相似度都為0,這是違背實際情況的。
那么實際情況是什么?舉例說明:詞"cars"是"car"的復數形式,詞"trucks"又是"truck"的復數形式,所以實際中我們希望他們詞向量相似度很大:
由於"car"和"truck"詞義接近,"car"和"china"詞義差別較大,我們也希望有如下關系:

那么不禁要問:有沒有一種神經網絡,輸入每個詞的one-hot編碼,就可以輸出符合上述要求的詞向量?

有,就是word2vec!
word2vec詳解
word2vec是一個典型的3層全連接網絡:INPUT->PROJECTION->OUTPUT,假設:
- INPUT層->PROJECTION層權重為\(W_1\)矩陣
- PROJECTION層->OUTPUT層權重為\(W_2\)矩陣
其中\(W_1,W_2\)通過訓練得到
那么:\(詞向量 = 詞的one-hot編碼向量(轉置)\times W_1\)

所以\(W_1\)就是由字典中所有詞對應的詞向量組成的矩陣:
那么如何訓練網絡獲得\(W_1,W_2\)矩陣?
word2vec提出了CBOW與skip-gram結構。

CBOW結構:根據輸入周圍\(2c\)個詞來預測出這個詞本身(即通過上下文預測詞語):
skip-gram結構:根據輸入詞來預測周圍\(2n\)個詞(即預測詞語的上下文):
huffman樹
huffman樹是一種特殊結構的二叉樹,通過huffman樹編碼的huffman碼,在通信領域有着廣泛的應用。在word2vec模型中構建PROJECTION->OUTPUT的Hierarchical softmax過程中,也使用到了huffman樹。
構建huffman樹流程:
-
根據給定的n個權值{w1, w2, w3 ... wn},構造n棵只有根節點的二叉樹,令起權值為wj
-
在森林中選取兩棵根節點權值最小的樹作為左右子樹,構造一顆新的二叉樹,置新二叉樹根節點權值為其左右子樹根節點權值之和。
注意,左子樹的權值應小於右子樹的權值。
-
從森林中刪除這兩棵樹,同時將新得到的二叉樹加入森林中。
換句話說,之前的2棵最小的根節點已經被合並成一個新的結點了。
-
重復上述兩步,直到只含一棵樹為止,這棵樹即是“哈弗曼樹”
接下來舉例說明如何構造huffman樹:

假設我們從某書籍中收集了一段語料,統計在語料中出現的詞及每個詞出現的次數,生成如下詞匯表(這里只是舉例,現實中的詞匯表一定會非常大)。
vocabulary = {
"are" 32,
"you": 21,
"and": 19,
"very": 10,
"hi": 7,
"guys": 6,
"wise": 2,
"smart": 3,
}
然后通過每個詞在語料中的出現次數建立huffman樹,作為PROJECTION->OUTPUT結構。
另外在word2vec中約定:從huffman樹根節點(root)開始,每次父節點向左子葉遍歷編碼為1,向右子葉遍歷為0。如and編碼為11,smart編碼為01110(出現頻率越高的詞編碼越短)。

在word2vec中使用huffman樹的重要原因就是降低訓練時的計算量。
一般來說,訓練用的語料庫都非常大。而在語料庫中,有一些詞出現頻率很高,還有一些詞出現頻率很低,而且這種頻率差異是非常巨大的。那么采用huffman樹后,出現頻率很高的常用詞路徑短,計算量小,從而降低了整個word2vec模型在訓練時的計算開銷。
CBOW(Continuous Bag of Words)結構

在之前提到過,CBOW根據輸入詞周圍\(2c\)個詞來預測出這個詞本身。如果當前網絡已經充分訓練,那么輸入you、are、 wise、and四個詞,則應該輸出詞very。那么CBOW結構的word2vec網絡是如何訓練的?
-
從INPUT->PROJECTION層
CBOW結構首先會取中心詞的\(2c\)個上下文詞,然后用這些上下文詞的one-hot編碼向量乘以\(W_1\)權重再求和:
\[V_{context(word_i)}=V_c=\frac{1}{2c}(\sum^c_{t=1}V_{onehot(word_{i+t})}\cdot W_1+\sum^c_{t=1}V_{onehot(word_{i-t})}\cdot W_1) \]其中\(V_{onehot(word_i)}\)代表詞\(word_i\)的one-hot編碼向量;\(V_{context(word_i)}\)代表\(word_i\)的上下文詞的詞向量之和(為了便於書寫記為\(V_c\))
-
從PROJECTION->OUTPUT層(Hierarchical Softmax)
以詞"very"為例在huffman樹中需要分類4次(編碼為0100)
-
第一次分類結果\(d_1=0\)概率為:
\[P(d_1|V_c,\theta_1)=1-\sigma(V^T_c\cdot \theta_1) \]其中\(\sigma(x)=\frac{1}{1+e^x}\in(0,1)\)

-
第二次分類為\(d_2=1\)概率為:
\[P(d_2|V_c,\theta_2)=\sigma(V^T_c\cdot \theta_2) \] -
第三次分類為\(d_3=0\)概率為:
\[P(d_3|V_c,\theta_3)=1-\sigma(V^T_c\cdot \theta_3) \] -
第四次分類為\(d_4=0\)概率為:
\[P(d_4|V_c,\theta_4)=1-\sigma(V^T_c\cdot \theta_4) \]
所以詞"very"最終Hierarchical Softmax最終概率為:
\[P(very|context(very))=\prod_{j=1}^{4}P(d_j|V_c,\theta_j) \]
推廣一下,詞\(word_i\)最終Hierarchical Softmax最終概率為:
\[P(word_i|context(word_i))=\prod_{j=1}^{l_i}P(d_j|V_c,\theta_j) \]其中\(l_i\)為\(word_i\)的huffman樹路徑長度(如"very"為4),而\(P(d_j|V_c,\theta_j)\)為:
\[P(d_j|V_c,\theta_j)=\left\{\begin{matrix} \sigma(V_c^T\cdot \theta_j) &d_j=0 \\ 1-\sigma(V^T_c\cdot \theta_j)&d_j=1 \end{matrix}\right.\]簡化一下
\[P(d_j|V_c,\theta_j)=[\sigma(V_c^T\cdot \theta_j) ]^{1-d_j}\cdot[1-\sigma(V^T_c\cdot \theta_j)]^{d_j} \] -
-
從OUTPUT->PROJECTION->INPUT訓練
在訓練時,我們顯然希望輸入是\(word_i\)的\(2c\)個上下文詞時輸出使\(word_i\),即概率\(P(word_i|context(word_i))\)越大越好,那么最終優化目標就是對預料中每個詞\(word_i\)都有\(L\)最大:
\[\begin{matrix}L&=\underset{word_i\in C}{\sum}P(word_i|context(word_i))\\&=\underset{word_i\in C}{\sum}\prod_{j=1}^{l_i}[\sigma(V_c^T\cdot \theta_j) ]^{1-d_j}\cdot[1-\sigma(V^T_c\cdot \theta_j)]^{d_j}\end{matrix} \]其中\(C\)代表訓練使用的語料庫,\(l_i\)代表詞\(word_i\)的huffman樹路徑長度。而優化目標是乘法形式,所以取對數\(log\)將優化目標轉化為加法:
\[\begin{matrix}L'&=\underset{word_i\in C}{\sum}log\prod_{j=1}^{l_i}[\sigma(V_c^T\cdot \theta_j) ]^{1-d_j}\cdot[1-\sigma(V^T_c\cdot \theta_j)]^{d_j}\\&=\underset{word_i\in C}{\sum}\sum^{l_1}_{i=1}\{(1-d_j)log[\sigma(V_c^T\cdot \theta_j)]+g_jlog[1-\sigma(V_c^T\cdot \theta_j)]\}\end{matrix} \]由於優化目標是使\(L'\)最大,那么訓練采用梯度上升算法,即每當獲取新的訓練中心詞\(word_i\)時都會通過梯度更新一次權重。記:
\[L'(i,j)=(1-d_j)log[\sigma(V_c^T\cdot \theta_j)]+g_jlog[1-\sigma(V_c^T\cdot \theta_j)] \]這里\(L'(i,j)\)時中心詞為\(word_i\)時的優化目標,即希望通過調整\(\theta_j,V_c\)使得\(L'(i,j)\)最大,所以計算\(L'(i,j)\)對\(\theta_j\)的偏導數為
\[\frac{\partial L'(i,j)}{\partial V_c}=[1-d_j-\sigma(V_c^T\cdot \theta_j)]\theta_j \]即可更新\(W_1\)中上下文詞的詞向量\(V_{word_n}\)(注意這里的\(word_n\in context(word_i)\))
\[V^{new}_{word_i}=V^{old}_{word_n}+\eta\sum^{l_i}_{j=1}\frac{\partial L'(i,j)}{\partial V_c} \]其中\(\eta\)表示梯度上升學習率
對於上式一個比較通俗且不嚴謹的理解:
誤差傳給了誰,誰就會把梯度返回給傳它誤差的節點,即“原路送回“。在前傳中通過\(V_c\)點將誤差傳遞給了后續網絡,那么在反傳中后續網絡也要把自己所有的梯度\(\sum^{l_i}_{j=1}\frac{\partial L'(i,j)}{\partial V_c}\)還給\(V_c\)節點,然后\(V_c\)又會把梯度返還給\(2c\)個上下文詞的詞向量。

skip-gram結構

在之前提到過,skip-gram根據輸入詞詞來預測出周圍\(2c\)個上下文詞。如果當前網絡已經充分訓練,那么輸入very,則應該輸出you、are、 wise、and四個上下文詞。
從INPUT->PROJECTION計算\(word_i\)的詞向量\(V_{worad_i}\),然后PROJECTION直接向后續分層softmax輸出\(V_{worad_i}\) (CBOW是求和)。skip-gram的優化目標與CBOW稍微不同:
其中多出的\(\underset{u\in context(word_i)}{\prod}\)符號代表skip-gram要通過\(2c\)個huffman樹分別輸出\(2c\)個上下文詞,\(\theta^u_j\)中的\(u\)代表每個輸出詞對應的\(\theta_j\)參數。skip-gram與CBOW非常接近,考慮篇幅這里不再介紹。
word2vec實際測試
Google提供了word2vec的c代碼。使用代碼和text8語料庫訓練200維詞向量,可以看到與詞"google"余弦下降速度最大的詞依次是"yahoo"和"gmail"。Amazing!

word2vec缺點
- OOV(Out of vocabulary)
在word2vec中,詞匯表從開始訓練就已經是確定的。那么在使用時,必然會有詞不在詞匯表中。一般使用<UNKNOW>特殊標志符解決OOV問題,但是當句子中<UNKNOW>過多時必然嚴重影響精度。
后續ELMO使用char cnn、Bert使用word piece,基本解決了OOV問題。
- 無法處理多義詞
很多詞在不同語境是有含義不同,即多義詞。而word2vec中所有詞的embeding向量都是訓練好即固定的,無法在使用時根據上下文調整,導致處理多義詞效果差。
ELMO和Bert使用訓練language model,動態生成embeding向量,解決多義詞問題。
