Word Embedding教程
本文介紹Word Embedding的基本概念以及常見的無監督訓練方法,主要Word2Vec。
目錄
詞的表示方法
不同於更底層的影象和聲音訊號,語言是高度抽象的離散符號系統。為了能夠使用神經網路來解決NLP任務,幾乎所有的深度學習模型都要在第一步把離散的符號變成向量。我們希望把一個詞對映到”語義”空間中的一個點,使得相似的詞的距離較近而不相似的較遠。我們可以用向量來表示一個點,因此我們通常把這個向量叫做詞向量。
one-hot向量
最簡單的方法就是one-hot表示。假設我們的詞典大小是4(當然實際通常是很多,至少幾萬),如所示,每個詞對應一個下標。每個詞都用長度為4的向量表示,只有對應的下標為1,其餘都是零。比如上面的例子,第一個詞是[1, 0, 0, 0],而第三個詞是[0, 0, 1, 0]。
one-hot的問題是不滿足我們前面的期望——相似的詞的距離較近而不相似的較遠。對於one-hot向量來說,相同的詞距離是0,而不同的詞距離是1。這顯然是有問題的,因為cat和dog的距離肯定要比cat和apple要遠。但是在one-hot的表示裡,cat和其它任何詞的距離都是1。
圖:one-hot表示法
one-hot的問題在於它是一個高維(通常幾萬甚至幾十萬)的稀疏(只有一個1)向量。我們希望用一個低維的稠密的向量來表示一個詞,我們期望每一維都是表示某種語義。比如第一維代表動物(當然這只是假設),那麼cat和dog在這一維的值都比較大,而apple在這一維的值比較小。這樣cat和dog的距離就比cat和apple要近。
神經網路語言模型
那麼我們怎麼學習到比較好的詞向量呢?最早的詞向量其實可以追溯到神經網路語言模型。但是首先我們來了解一下語言模型的概念和傳統的基於統計的N-Gram語言模型。
給定詞序列$w_1,…,w_K$,語言模型會計算這個序列的概率,根據條件概率的定義,我們可以把聯合概率分解為如下的條件概率:
實際的語言模型很難考慮特別長的歷史,通常我們會限定當前詞的概率值依賴與之前的N-1個詞,這就是所謂的N-Gram語言模型:
在實際的應用中N的取值通常是2-5。我們通常用困惑度(Perplexity)來衡量語言模型的好壞:N-Gram語言模型可以通過最大似然方法來估計引數,假設$C(w_{k−2} w_{k−1} w_k)$表示3個詞$(w_{k−2} w_{k−1} w_k$連續出現在一起的次數,類似的$C(w_{k−2} w_{k−1}$表示兩個詞$w_{k−2} w_{k−1}$連續出現在一起的次數。那麼:
最大似然估計的最大問題是資料的稀疏性,如果3個詞沒有在訓練資料中一起出現過,那麼概率就是0,但不在訓練資料裡出現不代表它不是合理的句子。實際一般會使用打折(Discount)和回退(Backoff)等平滑方法來改進最大似然估計。打折的意思就是把一些高頻N-Gram的概率分配給從沒有出現過的N-Gram,回退就是如果N-Gram沒有出現過,我們就用(N-1)-Gram來估計。比如Katz平滑方法的公式如下:
圖:Katz平滑
上式中C’是一個閾值,頻次高於它的概率和最大似然估計一樣,但是對於低於它(但是至少出現一次)的概率做一些打折,然後把這些概率分配給沒有出現的3-Gram,怎麼分配呢?通過回退到2-Gram的概率$P(w_k|w_{k-1})$來按比例分配。
N-Gram語言模型有兩個比較大的問題。第一個就是N不能太大,否則需要儲存的N-gram太多,因此它無法考慮長距離的依賴。比如”I grew up in France… I speak fluent _.”,我們想猜測fluent後面哪個詞的可能性大。如果只看”speak fluent”,那麼French、English和Chinese的概率都是一樣大,但是通過前面的”I grew up in France”,我們可以知道French的概率要大的多。這個問題可以通過後面介紹的RNN/LSTM/GRU等模型來一定程度的解決,我們這裡暫時忽略。
另外一個問題就是它的泛化能力差,因為它完全基於詞的共現。比如訓練資料中有”我 在 北京”,但是沒有”我 在 上海”,那麼$p(上海|在)$的概率就會比$p(北京|在)$小很多。但是我們人能知道”上海”和”北京”有很多相似的地方,作為一個地名,都可以出現在”在”的後面。這個其實和前面的one-hot問題是一樣的,原因是我們把北京和上海當成完全兩個不同的東西,但是我們希望它能知道北京和上海是兩個很類似的東西。
通過把一個詞表示成一個低維稠密的向量就能解決這個問題,通過上下文,模型能夠知道北京和上海經常出現在相似的上下文裡,因此模型能用相似的向量來表示這兩個不同的詞。
神經網路如所示。
圖:神經網路語言模型
這個模型的輸入是當前要預測的詞,比如用前兩個詞預測當前詞。模型首先用lookup table把一個詞變成一個向量,然後把這兩個詞的向量拼接成一個大的向量,輸入神經網路,最後使用softmax輸出預測每個詞的概率。
Lookup table等價於one-hot向量乘以Embedding矩陣。假設我們有3個詞,詞向量的維度是5維,那麼Embedding矩陣就是(3, 5)的矩陣,比如:
這個矩陣的每一行表示一個詞的詞向量,那麼我們要獲得第二個詞的詞向量,就可以用如下的向量矩陣乘法來提取:
但是這樣的實現並不高效,我們只需要”複製”第二行就可以了,因此大部分深度學習框架都提供了Lookup table的操作,用於從一個矩陣中提取某一行或者某一列。
這個Embedding矩陣不是固定的,它也是神經網路的引數之一。通過語言模型的學習,我們就可以得到這個Embedding矩陣,從而得到詞向量。
Word2Vec
我們可以使用語言模型(甚至其它的任務比如機器翻譯)來獲得詞向量,但是語言模型的訓練非常慢(機器翻譯就更慢了,而且還需要監督的標註資料)。可以說詞向量是這些任務的一個副產品,而Mikolov等人提出Word2Vec直接就是用於訓練詞向量,這個模型的速度更快。
Word2Vec的基本思想就是Distributional假設(hypothesis):如果兩個詞的上下文相似,那麼這兩個詞的語義就相似。上下文有很多粒度,比如文件的粒度,也就是一個詞的上下文是所有與它出現在同一個文件中的詞。也可以是較細的粒度,比如當前詞前後固定大小的視窗。比如所示,written的上下文是前後個兩個詞,也就是”Portter is by J.K.”這4個詞。
圖:詞的上下文
除了我們即將介紹的Word2Vec,還有很多其它方法也可以利用上述假設學習詞向量。所有通過Distributional假設學習到的(向量)表示都叫做Distributional表示(Representation)。
注意,還有一個很像的術語叫Distributed表示(Representation)。它其實就是指的是用稠密的低維向量來表示一個詞的語義,也就是把語義”分散”到不同的維度上。與之相對的通常是one-hot表示,它的語義集中在高維的稀疏的某一維上。
我們再來回顧一下word2vec的基本思想是:一個詞的語義可以由它的上下文確定。word2vec有兩個模型:CBOW(Continuous Bag-of-Word)和SG(Skip-Gram)模型。我們首先來介紹CBOW模型,它的基本思想就是用一個詞的上下文來預測這個詞。這有點像英語的完形填空題——一個完整的句子,我們從中“摳掉”一個單詞,然後讓我們從4個選項中選擇一個最合適的詞。和真正完形填空不同的是,這裡我們不是做四選一的選擇題,而是從所有的詞中選擇。有很多詞是合適的,我們可以計算每一個詞的可能性(概率)。
它的思路其實是比較簡單的,我們下面詳細分析它的實現,這裡首先介紹上下文是一個詞的情況,之後我們再把上下文推廣的多個詞的情況。讀者可能會問上下文只有一個詞是什麼意思,其實我們把它理解成bi-gram的語言模型就好了,根據前一個詞來預測當前詞。
上下文(context)是一個詞
上下文是一個詞的CBOW模型如所示。這裡,詞典(Vocabulary)的大小是V(詞的個數),隱層的隱單元個數是N。輸入層-隱層以及隱層-輸出層都是全連線的網路層。輸入向量是one-hot的表示,也就是$x_1, x_2, …, x_V$裡只有一個1,其餘全是0。輸入層和隱層的引數是一個$V \times N$的矩陣$W$,$W$的每一行是一個D維的向量$v_w$,這個向量對應輸入的詞w。更形式化一點,假設輸入的詞(上下文)是$w$,它對應的下標是k,那麼它的one-hot表示x只有第k維是1,其餘都是0。因此有:
圖:上下文只有一個詞的CBOW模型
因此隱層的輸出h其實就是輸入詞$w_I$對應的第k行這個向量,由於x的one-hot特性,我們計算h的時候並不需要真的進行矩陣向量乘法,只需要找到x對應的那一行就好了。在word2vec的隱層,我們一般不使用啟用函式。
隱層到輸出層的引數矩陣是一個$N \times V$的矩陣$W’$(注意這裡W’不是轉置的意思,只是表示和W不同第一個矩陣而已),使用它我們可以計算輸出第j個詞的得分$u_j$為:
上式中$v’_{w_j}$是$W’$的第j列。為了輸出概率,我們對所有的$u_j$進行softmax:
把公式1和2代入公式3得到:
在上式中,$v_w$和$v’_w$是詞w的兩種向量表示,其中$v_w$來自矩陣$W$的某一行,我們把它叫作輸入向量;而$v’_w$來自矩陣$W’$的某一列,我們把它叫作輸出向量。我們可以發現:如果輸入詞$w_I$和輸出詞$w_j$的詞向量的內積比較大,說明它們比較相似,則$p(w_j|w_I)$就較大。
前面介紹了一個詞的上下文的CBOW模型的forward計算過程,接下來介紹怎麼反向計算梯度。讀者可能會有疑惑,這其實是一個很簡單的三層全連線網路,梯度的推導前面已經介紹過了,為什麼還要再來一次呢?原因有兩個:首先是因為這個模型的輸入x是one-hot的表示,引數W的梯度有更加簡潔的形式;其次是我們需要分析引數W’的梯度涉及到的softmax計算,分析計算量最大的地方在哪裡,從而瞭解為什麼要用後面的hierachical softmax或者negative sampling了加速。
首先我們來計算隱層到輸出層引數W的梯度。我們的損失函式就是交叉熵損失函式,給定輸入$w_I$,輸出是$w_O$,假設$w_O$對應的下標是$j^*$,那麼損失E的計算公式如下:
E是$u_j$的函式,我們求E對它的偏導數如下:
上式的推導中,
對$u_j$求偏導數時,如果 ,那麼偏導數是1,否則是0,因此最終的結果可以寫出 ,我們把它記作$t_j$。而第二項的偏導數log的導數是先把它放到分母裡,然後再對它求導,而求和的V項中只有當$j’=j$時才有$u_j$,而指數的導數是它本身,因此分子最後就剩下$exp(u_j)$,最後我們對比一下就能發現,第二項就是$y_j$。最後我們用一個記號$e_j$來表示這個值,e的意思是error,可以發現,$\frac{\partial E}{\partial u_j}$等於模型預測的值$y_j$和實際值(下標為$w_O$對應的的時候$t_j$值是1,否則是0)的差值。如果這個差值很大,說明我們的模型預測的不好,錯誤就越大,引數需要做更大的調整;反之說明錯誤小,不需要調整引數。根據鏈式法則,我們可以求E對引數$w’_{ij}$的導數:
求出之後我們可以用梯度下降演算法更新引數$w’_{ij}$:
其中$\eta$是學習率,$e_j=y_j-t_j$。因為詞$w_j$對應的$W’$的第j列,因此我們把它寫出向量形式:
上面兩式說明,對於每一個訓練資料(輸入$w_I$和輸出$w_O$),我們都要更新所有V個詞對應的輸出詞向量$w’_{w_j}$,這個計算量是非常大的。接下來我們求E對隱層的輸出$h_i$的梯度:
因為$h_i$會輸入給所有的$u_j$,所以根據鏈式法則對$h_i$的導數是從$h_i$到E的所有路徑的求和。E對$h_i$的導數求出來後,我們就可以求E對$w_{ij}$的導數了。在這之前,我們把$h_i$和$w_{ij}$的關係寫出來:
因此我們可以求出:
我們可以把它寫成向量的形式:
這是一個$V \times N$的矩陣,但是x只有一個元素是非零的,因此$\frac{\partial E}{\partial W}$只有那一行是非零的。因此我們的梯度下降演算法對於W只需要更新輸入$w_I$對應的那一個(行)向量:
多個詞的上下文
接下來我們把上下文擴充套件到多個詞的情況。模型如所示,輸入是多個詞,我們用一個詞周圍的多個詞來預測這個詞。
圖:CBOW模型
和前面類似,我們用one-hot的方式來表示每一個詞,那怎麼把多個向量輸入到CBOW模型中呢?這裡使用了最簡單的平均:
我們發現一旦h計算出來之後,後面的計算和一個詞的完全相同,因此我們可以計算損失:
這和是完全一樣的,唯一不同的是h的計算方法不同。因此輸出向量的梯度更新公式是完全一樣的:
輸入向量的梯度更新稍微有點區別,之前值更新$w_I$輸入向量,這裡需要更新輸入的所有上下文$w_{I,c}$的輸入向量,當然還要乘以一個$\frac{1}{C}$:
在上式中,$v_{w_{I,c}}$是要預測的詞的第c個上下文詞。比如句子是”it is a good day”,假設context視窗是2,單詞”a”的context是”it”、”is”、”good”、”day”。而$EH_i=\frac{\partial E}{\partial h_i}$,計算公式為。
Skip-Gram模型
Skip-Gram模型如下圖所示,它用一個詞來預測它的上下文。比如前面”it is a good day”的例子,比如當前詞是”a”,我們會預測它周圍4個詞的概率。
圖:Skip-Gram模型
這個模型看起來比較複雜,但是實際上它非常簡單,和前面介紹過的一個詞的CBOW很類似。讀者可能會奇怪怎麼用一個詞預測多(C)個詞呢?其實我們可以簡化一下,一次預測一個詞,然後預測C次。雖然我們預測了C次,但是預測的公式都是一個:
上式中,$w_I$是輸入詞,$w_{O,c}$是需要預測的第c個輸出。而$u_{c,j}$是預測第c個詞的為j的概率,它的公式為:
從上式可以看出,$u_{c,j}$的計算其實與下標c是無關的。接著我們可以計算損失:
上式中
是要預測的第c個詞的下標。到這裡我們可以發現Skip-Gram和前面一個詞的CBOW非常類似,只不過E是多個詞的損失的求和而已。具體的偏導數求導我們就不贅述了。在實際計算中,我們可以把一次預測C個詞分解成一次預測一個詞然後預測C詞。這兩者有一點細微的區別——前者的C個詞的forward是一次計算出來的,然後用C個詞的損失去計算梯度;而後者會計算C次forward,然後backward也會計算C詞,而且在這C詞的過程中引數W和W’已經發生了細微的變化。當然後者的計算效率較低,但是可以讓我們更好的一個詞的CBOW對比。
計算的效率
以一個上下文的CBOW為例,對於每一個訓練資料,我們計算損失的時候需要對所有V個詞都計算它的輸出向量和h的內積。而反向更新引數時,我們需要更新輸入詞對應的輸入向量一次,參考。而對於輸出向量,我們需要更新所有V個詞的輸出向量,如所示,這個計算量是很大的,在實際的資料中,V通常都是幾萬甚至幾十萬。下面我們介紹加速計算的一些技術。
Hierarchical Softmax
Hierarchical Softmax用一棵二叉樹(為了效率通常使用Huffman樹)來表示詞典裡的所有詞。這棵樹有V個葉子節點,分別對應V個詞,它是二叉樹,因此有V-1箇中間節點。每一個葉子節點都有從樹根到它的唯一路徑,而輸出的概率可以根據這條路徑計算出來。我們以下圖為例來解釋一些術語。比如詞$w_2$,$L(w_2)=4$表示這個詞對應的葉子節點的路徑上的節點的個數。$n(w,j)$表示這條路徑第j個節點,比如圖中$n(w_2,1)$是根節點,$n(w_2,2)$是第二層最左邊的節點,…。
圖:Hierarchical Softmax示例
在Hierarchical Softmax裡,只有V-1箇中間節點對應一個詞向量$v’_{n(w,j)}$(普通的Softmax是V個詞)。葉子節點(真正的詞)根據這個路徑來計算概率,計算公式為:
上面這個式子有一些複雜,我們仔細來閱讀一下。首先$ch(n(w,j))$是節點$n(w,j)$的左孩子,因此$n(w,j+1)=ch(n(w,j))$表示路徑上的第j+1個節點是第j個節點的左孩子。$[![ x ]!]$定義如下:
我們用上圖所示的$w_2$為例來展開上面的公式:
用自然語言來描述上面的公式其實比較簡單:從路徑的第二層開始,如果這個節點是父親的左孩子,那麼把它父親節點的向量乘以h然後用$\sigma$啟用($\sigma(v_{parent}^T \cdot h)$);否則如果是父親的右孩子,則把它父親節點的向量乘以h後再乘以-1再啟用($\sigma(-v_{parent}^T \cdot h)$)。然後把這些值全部乘起來就是概率。
為了使得公式看起來簡單,在沒有歧義的地方我們用$[![ ]!]\text{表示}[![ n(w,j+1)=ch(n(w,j)) ]!]$,用
表示。
這裡我們不證明,讀者可以驗證一下所有葉子節點的概率加起來是1。根據這個概率我們可以計算損失:
我們可以來分析一下計算E的複雜度的變化。根據計算E需要遍歷V個詞,它的複雜度是O(V),而現在我們只需要變數L(w)個詞,L(w)通常是O(logV)的複雜度。接下來我們簡單的推導一下梯度,驗證梯度的更新的複雜度也是下降到O(logV)的。
接下來我們求E對n(w,j)對應的向量的梯度:
我們可以用梯度下降來更新引數:
從上面的引數更新公式可以看出,對於一個訓練資料,我們只需要更新路徑上的節點對應的v,因此時間複雜度變為O(logV)。最後我們計算E對h的梯度:
計算EH的複雜度也是O(logV),有了EH之後,輸入向量的引數更新就和之前一樣了,它只涉及輸入詞對應的那個向量。
Negative Sampling
在實際應用中,我們通常使用這個演算法。它的思路比Hierarchical Softmax更加簡單直接——既然計算所有詞的softmax太慢,那麼我們就只採樣一部分來計算!
很顯然,需要預測的(Positive)詞肯定需要計算,我們還需要取樣一些Negative的詞(也就是錯誤預測的詞),這就是Negative Sampling的名字由來。這個取樣的概率分佈我們把它叫作噪音分佈$P_n(w)$,在Mikolov的word2vec實現裡使用的$P_n(w)=(p(w))^{3/4}$,其中$p(w)$是unigram的概率(基本上就是詞頻)。有了取樣的Negative詞,我們就可以和之前一樣計算softmax了(認為其它的詞的softmax很小,趨近於0)。不過在word2vec的實現裡,作者使用了一種更加簡單的損失函式。這雖然和softmax不完全等價,但是也能學到不錯的word embedding,這個新的損失函式定義為:
其中,$w_O$是輸出的詞,$v’_{w_O}$是它對於的輸出詞向量。h是隱層的輸出。
是K個negative樣本。
我們來簡單分析一下這個損失函式:對於正樣本$w_O$,$(v’_{w_O})^Th$越大,則E越小;而對於負樣本正好相反。這和之前的softmax是一致的。接下來我們推導一下Negative Sampling時的梯度。
在上式中,如果$w_j$是輸出的詞,那麼$t_j$就是1,否則就是0。接著我們計算E對$v’_{w_j}$的梯度:
接著我們就可以用梯度下降更新引數:
上式我們只需要更新取樣的詞對應的v就可以了,因此比原來的softmax的複雜度要低得多。同樣我們可以計算E對h的梯度:
計算EH也同樣只需要計算${w_O} \cup \mathcal{W}_{neg}$裡的詞對應的向量。而有了EH之後,引數W的更新和之前完全一樣。
程式碼
原始的實現
Mikolov最早的實現在 google code ,不過google code已經死掉了,讀者可以在 這裡 下載到匯出的版本。
安裝:
$cd src $make
安裝後的二進位制程式在bin目錄下。
訓練
我們需要準備資料,作者使用的是自己抓取的百科網頁,由於版權原因不能提供,讀者可以 這裡 下載他人提供的資料。我們需要對文字進行預處理,主要是分詞,詞之間用空格分開。
./bin/word2vec -train baike.txt-output baike.bin -size 200 -window 3 -negative 10 -sample 1e-4 -threads 20 -binary 1 -iter 30
我們需要提供檔案baike.txt。訓練的模型存放在baike.bin裡。讀者可以不帶引數的執行,預設會打出所有的選項,這裡介紹經常需要修改的選項。
- size 詞向量的維度,預設100,這裡設定為200。
- window 視窗的大小,預設是5,這裡設定為3。
- negative 10 使用Negative Sample演算法,設定負樣本的個數為10
- sample 對高頻詞進行下采樣。這裡是1e-4
- threads 執行緒數,根據機器進行設定
- binary 輸出二進位制格式的模型
- iter 迭代次數
測試
訓練好了我們來測試一下類比實驗:
$ ./bin/word-analogy baike.bin Enter three words (EXIT to break): 湖南 長沙 河北 Word: 湖南Position in vocabulary: 2720 Word: 長沙Position in vocabulary: 2394 Word: 河北Position in vocabulary: 2859 WordDistance ------------------------------------------------------------------------ 石家莊0.900409 保定0.888418 邯鄲0.857933 廊坊0.851938 邢臺0.851816 唐山0.843865
我們看到確實它學到了長沙和湖南的關係等於石家莊與河北的關係,用向量來說就是:
湖南-長沙=河北-石家莊
接下來找一個詞最近的詞:
$ ./bin/distance baike.bin Enter word or sentence (EXIT to break): 北京 Word: 北京Position in vocabulary: 373 WordCosine distance ------------------------------------------------------------------------ 上海0.827013 天津0.781236 廣州0.737348 瀋陽0.721798 成都0.711291 南京0.706751 深圳0.706011
ngram2vec
除了作者最原始的實現,網上也有許多其它實現。如果讀者想使用現成的詞向量,可以參考 Chinese-Word-Vectors 。 裡面預訓練好了很多詞向量(包括N-Gram的向量),訓練工具在 這裡 。