Word2Vec詳解-公式推導以及代碼

1.前記

   這篇Word2Vec介紹,大量參考了word2vec中的數學這份pdf,感謝peghoty大神的教程,我將這份教程的pdf版本放在了github上面,點擊跳出.這裏同時有一份我改寫的Python版本的word2vec的代碼,包含本次講解裏面的所有內容,大家可以參考一下.

  除此之外,我也參考了java版本的和C語言版本的word2vec代碼,最終才寫出來了Python版本的,附上鍊接:

dav/word2vec

liuwei1206/word2vec

linshouyi/Word2VEC_java

word2vec C語言註釋版本

  我不建議大家直接看原作者的論文,因爲原作者的論文寫的太簡練了,以至於很難讀懂,大家直接看代碼,會明白的更多,會對更多的細節有更多的理解.這裏我不建議在你找了很多資料依舊看不懂的情況下再繼續找更多的網上資料來看,因爲網上說的大都是一些個人的理解,而且關於公式的推導偏少,大都淺嘗輒止,我強烈建議大家在看完公式推導之後,直接就看源代碼,這樣你肯定會明白更多word2vec的內部原理.同時因爲本人水平有限,有些地方不對的地方,還請指出.

2.一些背景知識

2.1詞向量簡單介紹

  詞向量,簡單的來說,就是把我們習以爲常的漢字,字母等轉換成數字,因爲對於計算機而言,它只能讀懂二進制數字,但是對於人而言,十進制數字會比二進制數字更加容易理解一些,所以人們先將詞轉換成了十進制的數字.

  對於計算機而言,詞向量的轉換是nlp方向特有的一種數據處理方式,因爲在cv領域,圖像本身就是按照數字存儲在計算機中的,而且這些數字本身就已經包含了某些信息,同時每組不同的數字之間已經包含一些關係了,例如兩張都是大海的圖片,那麼兩張圖片裏面藍色偏多,然後兩張圖片的數字RGB裏面的B的佔比就會比較大,當然還會有別的特徵聯繫,但是因爲人本身對數字的不敏感,所以有些信息人們是直接發現不了.

  詞向量的質量直接影響了之後的nlp的處理,例如機器翻譯,圖片理解等等,沒有一個好質量的詞向量,機器翻譯的質量肯定是沒法很好的提升的.

  當初,人們的做法非常簡單,直接把詞映射爲獨熱編碼,例如I like writing code,那麼轉換成獨熱編碼就是:

單詞 獨熱編碼
I 0001
like 0010
writing 0100
code 1000

  這麼看着感覺還行吧,成功的把單詞轉換成了編碼,這樣是不是就可以了呢?

  答案是肯定不行的,因爲這麼做最明顯的缺點就是,單詞之間的聯繫沒有了,比如說Ilike之間的關係和likewriting之間的關係,通過0001和00100010和0100怎麼表現,通過距離?通過1的位置?你會發現獨熱編碼完全沒法表現單詞之間的任何關係.

  除此之外,當你的詞彙量達到千萬甚至上億級別的時候,你會遇到一個更加嚴重的問題,維度爆炸了.這裏舉例使用的是4個詞,你會發現,我們使用了四個維度,當詞數量達到1千萬的時候,詞向量的大小變成了1千萬維,不說別的,光內存你都受不了這麼大的詞向量,假設你使用一個bit來表示每一維,那麼一個單詞大概需要0.12GB的內存,但是注意這只是一個詞,一共會有上千萬的詞,這樣內存爆炸了.當維度過度增長的時候,你還會發現一個問題,你會發現0特別多,這樣造成的後果就是整個向量中,有用的信息特別少,幾乎就沒法做計算.並且在高維空間中,所有的點幾乎都是均勻分佈的,這樣的話,你根本就沒法對詞進行劃分.

  綜上,獨熱編碼完全沒法用的

  所以我們需要做的是,用一個稠密的向量,來表示單詞,還是上面例子,例如使用下面的方式進行表示(下面的只是舉例隨便寫的向量):

單詞 稠密向量
I [0.112]
like [0.224]
writing [0.512]
code {0.912}

  我們可以看到,以前使用4維才能描述的數據,這裏使用1維就可以描述了,當然這裏只是舉例,實際使用過程中,我在代碼中使用的數據集中的有效詞彙量大概是7萬多,總的詞彙在接近2千萬,使用的維度實際是200維度的,再壓縮一點我感覺也是可以的.

  如何生產稠密的向量,是一個難題,這個時候,Word2vec出來了,層次softmax的word2vec本質上應該更加接近是BP神經網絡,因它的整體運行模式和神經網絡的前向傳播和反向傳播非常類似.

2.2哈弗曼樹簡單介紹

  哈弗曼樹是指給定N個權值作爲N個葉子結點,構造一棵二叉樹,若該樹的帶權路徑長度達到最小,稱這樣的二叉樹爲最優二叉樹,也稱爲哈夫曼樹(Huffman Tree)。哈夫曼樹是帶權路徑長度最短的樹,權值較大的結點離根較近。(來自百度百科)

  下面的示意圖表現了哈弗曼樹的構建過程,實際上這個過程也是原作者在代碼中構建哈弗曼樹的過程,原代碼作者在構建哈弗曼樹的時候並沒有使用指針之類的方式進行構建,而是採用了一種稍微有點抽象的方式,應該說是原作者存儲的是數組的下標的位置,構建的一個比較"抽象"的哈弗曼樹.大家有機會可以去閱讀一下最初的C語言的代碼,原作者寫的是真的好.
下圖中紅色是葉子節點,也即是詞彙,數字代表該單詞出現的頻率.

在這裏插入圖片描述

  爲什麼這裏會使用到哈弗曼樹呢?因爲這裏需要使用層次softmax,所以需要構建一個哈弗曼樹.構建好一個哈弗曼樹之後,我們可以有效的減少計算量,因爲詞頻比較高的詞都比較靠近樹的根部,因爲對詞頻比較高的詞的更新會比較頻繁,所以每次進行計算的時候,可以有效的減少對樹的遍歷深度,也就減少了計算量.

  當然上面說的是一個方面,其次,還有別的好處,但是因爲個人水平有限,這裏就不再繼續探討.

3.基於層次softmax的模型

  基於層次softmax的模型,主要包括輸入層,投影層和輸出層,非常的類似神經網絡結構.CBOW的方式是在知道詞wtw_t的上下文 ...wt2,wt1,wt+1,wt+2...... w_{t-2}, w_{t-1}, w_{t+1}, w_{t+2} ... 的情況下預測當前詞wtw_t.而Skip-gram是在知道了詞wtw_t的情況下,對詞wtw_t的上下文 ...wt2,wt1,wt+1,wt+2...... w_{t-2}, w_{t-1}, w_{t+1}, w_{t+2} ... 進行預測.如下圖:

在這裏插入圖片描述

  而基於層次softmax的CBOW方式,我們需要最終優化的目標函數是
ζ=logp(wContext(w))           (3.1)\zeta=\sum \log p(w|Context(w)) \ \ \ \ \ \ \ \ \ \ \ (3.1)

  簡單的說可以認爲這個是層次softmax的公式,其中Context(w)Context(w)表示的是單詞ww的的上下文單詞,而基於Skip-gram的方式的最終需要優化的目標函數是:

ζ=logp(Context(w)w)\zeta=\sum \log p(Context(w)|w)

  下面的討論計算中,我們主要關注的是如何構造p(wContext(w))p(w|Context(w))p(Context(w)w)p(Context(w)|w)即可,因爲求導啥的主要都在p(wContext(w))p(w|Context(w))p(Context(w)w)p(Context(w)|w)

  看到這裏,估計你看的也是雲裏霧裏,而且網上大部分說的幾乎都和這個差不多,然後網上還有很多說詞向量只不過是這整個模型的副產物,從某些角度來說,說詞向量是這些模型的副產物也對,因爲實際上這些模型的目標是給定一個上下文,然後可以預測一個詞,或者給定一個詞,可以預測上下文.但是在我看來,這個模型實際上想要產生的就是詞向量,只不過是通過預測詞或者預測上下文的方式來構造詞向量,因爲這樣構造出來的詞可以很好的體現詞之間的關係.不過這些其實都不重要,如果你真的想明白word2vec,你需要做的是繼續閱讀,然後儘量把下面的公式自己推導一遍.

3.1COBW 層次softmax

3.1.1整體結構

  下圖給出了基於層次softmax的CBOW的整體結構,首先它包括輸入層,投影層和輸出層:
在這裏插入圖片描述

  其中輸入層是指Context(w)Context(w)中所包含的2c2c個詞向量Context(w)1,Context(w)2,...,Context(w)2c1,Context(w)2cContext(w)_1, Context(w)_2, ..., Context(w)_{2c-1}, Context(w)_{2c},

  然後投影層這裏指的是直接對2c2c個詞向量進行累加,當然了,這裏除了累加,還有另外一種方式,就是將所有的詞首位相連的連接起來,但是那樣做好像有些問題要處理,具體的我也沒有去探討.累加之後得到Xw=i=12cv(Context(w)i)X_w=\sum_{i=1}^{2c}v(Context(w)_i)

  最後是輸出層,輸出層是一個哈弗曼樹,然後其中葉子節點是N個,對應於N個單詞(對應於紅色節點),其中非葉子節點N-1個(對應於綠色節點).word2vec基於層次softmax的方式主要的精華部分都集中在了哈弗曼樹這部分.下面慢慢介紹

3.1.2 前向傳播和反向傳播推導

爲了便於下面的介紹和公式的推導,這裏需要預先定義一些變量:

  1. pwp^w:從根節點出發,然後到達單詞ww對應葉子節點的路徑
  2. lwl^w:路徑pwp^w中包含的節點的個數
  3. p1w,p2w,...,plwwp^w_1, p^w_2, ..., p^w_{l^w}: 路徑pwp^w中對應的各個節點,其中p1wp^w_1代表根節點,而plwwp^w_{l^w}代表的是單詞ww對應的節點
  4. d2w,d3w...,dlww{0,1}d^w_2, d^w_3 ..., d^w_{l^w}\in \left \{0, 1 \right \}: 單詞ww對應的哈夫曼編碼,一個詞的哈夫曼編碼是由lw1l^w-1位構成的,djwd^w_j表示路徑pwp^w中的第j個單詞對應的哈夫曼編碼,因爲根節點不參與對應的編碼
  5. θ1w,θ2w,...,θlw1w{0,1}\theta^w_1, \theta^w_2, ..., \theta^w_{l^w-1}\in\left \{0, 1 \right \}: 路徑pwp^w中非葉子節點對應的向量,θjw\theta^w_j表示路徑pwp^w中第jj個非葉子節點對應的向量.
    這裏之所以給非葉子節點定義詞向量,是因爲這裏的非葉子節點的詞向量會作爲下面的一個輔助變量進行計算,下面的公式推導的時候就會發現它的作用

 &emsp:既然已經引入了那麼多符號,那麼我們通過一個簡單的例子來看一下實際的運行情況,我們考慮單詞w="世界",然後下圖中黃色線路就是我們的單詞走過的路徑,整個路徑上的4個節點就構成了路徑pwp^w,其長度lw=4l^w=4,然後p1w,p2w,p3w,p4wp^w_1, p^w_2,p^w_3,p^w_4就是路徑pwp^w上的四個節點,其中d2w,d3w,d4wd^w_2,d^w_3,d^w_4分別爲1,0,1,即"世界"對應的哈夫曼編碼就是101,最後θ1w,θ2w,θ3w\theta^w_1, \theta^w_2, \theta^w_3就是路徑pwp^w上的4個非葉子節點對應的詞向量

  下面先進行前向傳播的公式推導:

在這裏插入圖片描述

  下面我們需要開始考慮如何構建概率函數p(wContext(w))p(w|Context(w)),以上面的w=""w="世界"爲例,從根節點到"世界"這個單詞,經歷了三次分類,也就是那3條黃色的線,而對於這個哈弗曼樹而言,每次分類,相當於一個二分類.

  既然是二分類,那麼我們可以定義一個爲正類,一個爲父類.我們還有"世界"的哈夫曼編碼,爲101,這個哈夫曼編碼是不包含根節點的,因爲根節點沒法分爲左還是右子樹.那麼根據哈夫曼編碼,我們一般可以把正類就認爲是哈夫曼編碼裏面的1,而負類認爲是哈夫曼編碼裏面的0.不過這個只是一個約定而已,因爲哈夫曼編碼和正類負類之間並沒有什麼明確要求對應的關係.但是原作者看來並不喜歡一般,原作者在寫的時候,將編碼爲1的認定爲負類,而編碼爲0的認定爲正類,也就是說如果分到了左子樹,就是負類,分到了右子樹,就是正類.那麼我們可以定義一個正類和負類的公式:

Label(piw)=1diw,i=2,3,4,...,lw Label(p^w_i)=1-d^w_i, i=2, 3, 4, ..., l^w

  公式中,剛好正類和負類是和編碼相反的.

  在進行二分類的時候,這裏選擇了sigmoid函數.雖然sigmoid函數存在梯度消失的問題,但是源代碼中進行了一些處理,稍微避免了這個問題

在這裏插入圖片描述

  那麼分爲正類的概率就是

σ(xwTθ)=11+exwtθ \sigma (x^T_w\theta)=\frac{1}{1+e^{-x^t_w\theta}}

  那麼分爲負類的概率就是
1σ(xwTθ) 1-\sigma (x^T_w\theta)

上面公式裏面包含的有θ\theta,這個就是非葉子對應的向量
  對於從根節點出發到達“世界”這個葉子節點所經歷的3次二分類,每次分類的概率寫出來就是:

  1. 第一次分類:p(d2wxw,θ1w)=1σ(xwTθ1w)p(d^w_2|x_w,\theta^w_1)=1-\sigma(x^T_w\theta^w_1)
  2. 第二次分類:p(d3wxw,θ2w)=σ(xwTθ2w)p(d^w_3|x_w,\theta^w_2)=\sigma(x^T_w\theta^w_2)
  3. 第三次分類:p(d4wxw,θ3w)=σ(xwTθ3w)p(d^w_4|x_w,\theta^w_3)=\sigma(x^T_w\theta^w_3)

  那麼,我們就可以得到p(wContext(w))p(w|Context(w))爲:

p(""Context())=j=24p(djwxw,θj1w) p("世界"|Context(“世界”))=\prod_{j=2}^{4}p(d^w_j|x_w,\theta^w_{j-1})

這裏應該說是貝葉斯公式的思想,對於詞典中的任意一個單詞ww,哈夫曼樹中肯定存在一個通路,從根節點到單詞ww的路徑pwp^w,而路徑pwp^w這條路並不是一條直線,每經過一個非葉子節點,肯定需要進行一次二分類,每次分類就會產生一個概率,我們將這些所有的概率都乘起來,那麼我們就可以得到我們需要的p(wContext(w))p(w|Context(w))

  條件概率p(wContext(w))p(w|Context(w))一般寫爲:

p(wContext(w))=j=2lwp(djwxw,θj1w)           (3.2) p(w|Context(w))=\prod_{j=2}^{l^w}p(d^w_j|x_w,\theta^w_{j-1}) \ \ \ \ \ \ \ \ \ \ \ (3.2)

其中:

p(djwxw,θj1w)={σ(xwTθj1w),djw=01σ(xwTθjw1),djw=1 p(d^w_j|x_w,\theta^w_{j-1})=\left\{\begin{matrix} \sigma(x^T_w\theta^w_{j-1}), & d^w_j=0 \\ 1 - \sigma(x^T_w\theta^w_j-1), & d^w_j=1 \end{matrix}\right.

將上面的兩個公式合併到一起

p(djwxw,θj1w)=[σ(xwTθj1w)1djw[1σ(xwTθj1w)djw]] p(d^w_j|x_w,\theta^w_{j-1})=[\sigma(x^T_w\theta^w_{j-1})^{1-d^w_j}\cdot [1-\sigma(x^T_w\theta^w_{j-1})^{d^w_j}]]

將(3.2)帶入(3.1)中,得到

ζ=wClogj=2lw{[σ(xwTθj1w)]1djw[1σ(xwTθj1w)]djw}                                         =wCj=2lw{(1djw)log[σ(xwTθj1w)]+djwlog[1σ(xwTθj1w)]}    (3.3) \zeta =\sum_{w \in C} \log \prod_{j=2}^{l^w}{\{[\sigma(x^T_w\theta^w_{j-1})]^{1-d^w_j}\cdot [1-\sigma(x^T_w\theta^w_{j-1})]^{d^w_j}\}} \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \\ \ = \sum_{w \in C} \sum_{j=2}^{l^w}\{(1-d^w_j) \cdot \log [\sigma(x^T_w \theta ^w_{j-1})] + d^w_j \cdot \log [1-\sigma(x^T_w \theta ^w_{j-1})] \} \ \ \ \ (3.3)

爲了推導方便,我們直接把累加里面的部分提取出來:

ζ(w,j)=(1djw)log[σ(xwTθj1w)]+djwlog[1σ(xwTθj1w)] \zeta(w,j)=(1-d^w_j) \cdot \log [\sigma(x^T_w \theta ^w_{j-1})] + d^w_j \cdot \log [1-\sigma(x^T_w \theta ^w_{j-1})]

  至此,前向傳播的公式已經全部推導完畢,下面開始反向傳播的推導

  Word2Vec中採用的是隨機梯度上升法,爲什麼採用隨機梯度上升法呢?在一般的神經網絡中,我們都是採用的隨機梯度下降法,因爲在那些優化的目標裏面,是讓損失值最小,所以採用讓目標沿着梯度降低的方向進行計算。而在這裏,我們想要讓目標函數ζ\zeta最大,因爲只有當ζ\zeta最大的時候,才說明了這個句子(單詞)出現在語料庫中的概率越大,其實就是說在強化一個詞ww和某些詞(例如和ww出現在一個句子中的詞)的關係.

  爲了能夠使用隨機梯度上升法,我們需要先對相應的變量求梯度,觀察公式ζ(w,j)\zeta(w,j),我們可以發現,其中的變量只有xwTx^T_wθj1w\theta^w_{j-1},其中w  C,j=2,...,lww \ \in \ C, j=2, ..., l^w.首先計算函數ζ(w,j)\zeta(w,j)關於θj1w\theta^w_{j-1}的導數:

在進行所有的推導之前,我們先對sigmoidsigmoid函數進行求導,因爲下面會用到:

Δσ(x)Δx=ex(ex+1)2=σ(x)(1σ(x)) \frac{\Delta \sigma(x)}{\Delta x} = \frac{e^x}{(e^x+1)^2}=\sigma(x)(1-\sigma(x))

Δζ(w,j)Δθj1w=(1djw)[1σ(xwTθj1w)]xwdjwσ(xwTθj1w)xw=[1djwσ(xwTθj1w)]xw \begin {aligned} \frac{\Delta \zeta(w,j)}{\Delta \theta ^w_{j-1}} &= (1-d^w_j)[1- \sigma(x^T_w \theta ^w_{j-1})]x_w - d^w_j \sigma (x^T_w \theta^w_{j-1})x_w \\ &= [1-d^w_j- \sigma(x^T_w \theta^w_{j-1})]x_w \end {aligned}

那麼的話,我們可以寫出θ\theta的更新公式:

θj1w=θj1w+η[1djwσ(xwTθj1w)]xw \theta ^ w_{j-1}= \theta^w_{j-1}+ \eta [1-d^w_j- \sigma(x^T_w \theta^w_{j-1})]x_w

其中η\eta是學習率,一般在設置學習率的時候,原作者在CBOW中將學習率設置爲0.05,在Skip-gram中設置爲了0.025.不過在代碼中,學習率會根據學習的進行,不停的進行着衰減,用來滿足自適應性,防止訓練後期的動盪和加快收斂.

  接下來可以考慮關於xx的梯度了,觀察ζ(w,j)\zeta(w,j)可以發現,xxθ\theta其實是對稱的,那麼在計算過程中,其實我們將最終結果的變量的位置進行交換就可以了

Δζ(w,j)Δxw=[1djwσ(xwTθj1w)]θj1w \frac{\Delta \zeta(w,j)}{\Delta x_w} = [1-d^w_j- \sigma(x^T_w \theta^w_{j-1})] \theta^w_{j-1}

  到了這裏,我們已經求出來了xwx_w的梯度,但是我們想要的其實是每次進行運算的每個單詞的梯度,而xwx_wContext(w)Context(w)中所有單詞累加的結果,那麼我們怎麼使用xwx_w來對Context(w)Context(w)中的每個單詞v(u)v(u)進行更新呢?這裏原作者選擇了一個簡單粗暴的方式,直接使用xwx_w的梯度累加對v(u)v(u)進行更新:

v(u)=v(u)+ηj=2lwΔζ(w,j)Δxw,  uContext(w) v(u) = v(u) + \eta \sum^{l^w}_{j=2} \frac{\Delta \zeta(w,j)}{\Delta x_w}, \ \ u \in Context(w)

至於使用別的方式是不是更有效,我沒有進行嘗試,所以這裏也就不在進行深入的探討

  雖然推導已經結束了,但是實際寫代碼和實際的推導還是有點差距的,下面是僞代碼,你可以發現,這個和推導的計算過程還是稍有不同

在這裏插入圖片描述

  這裏需要注意的是,(3.3)和(3.4)不可以電刀,因爲每次進行反向傳播更新v(u)v(u),的時候,我們在進行反向傳播的時候,需要使用的是前向傳播的時候參與計算的θj1w\theta^w_{j-1},而不是更新之後的θj1w\theta^w_{j-1}.

同時,上面的符合和實際代碼中的符號不太一樣,在word2vec最初的代碼中(我寫的代碼也按照了原來的命名方式進行),syn0syn0表示v(u)v(u),而syn1syn1表示θj1w\theta^w_{j-1},neulneul表示xwx_w,neuleneule表示ee

讀到了這裏,你可能對word2vec有了一些瞭解,也可能雲裏霧裏.但是都沒關係,大部分人在沒有接觸代碼的時候,都會感覺到word2vec很神奇,不清楚它的運行方式,看到這裏,我強烈建議你去看代碼,原版代碼中只看cbow相關的層次softmax

3.2 Skip-gram 層次softmax

3.2.1 整體結構

  可以認爲skip-gram模式的層次softmax的結構和3.1 cbow的很類似,可以說它也具有輸入層,"投影層"和輸出層,但是因爲它輸入的就是一個單詞,所以投影層就可以不要了.可以得到類似的下面的結構:

在這裏插入圖片描述

3.2.2 前向傳播和反向傳播推導

  Skip-gram舉例來看的話,可以看到類似下面的這樣的示意圖:

在這裏插入圖片描述

其中藍色的路線是需要走的路線,完整的句子是I like writing code,所以首先是先到I,然後再到like這條路線,最後到code這條路線.每條路線都像上面cbow裏面的類似,都是經過節點的時候類似於經過一個二分類.所以本節的符號和上一節類似,就不再重複列出.

  首先我們先定義每個路線的概率函數爲p(uw), uContext(w)p(u|w), \ u \in Context(w),表示在給定單詞ww的情況下,找到單詞wwContext(w)Context(w)對應的詞的概率(路線),記爲:

p(uw)=j=2lup(djwv(w),θj1u) p(u|w)= \prod^{l^u}_{j=2}p(d^w_j|v(w), \theta^u_{j-1})

  之後,我們知道單詞ww對應的上下文單詞Context(w)Context(w)包含好幾個單詞,那麼我們可以定義:

p(Context(w)w)=uContext(w)p(uw) p(Context(w)|w)= \prod_{u \in Context(w)}p(u|w)
其中p(djuv(w),θj1u)p(d^u_j|v(w), \theta^u_{j-1})和cbow中的定義類似,爲:

p(djuv(w),θj1u)=[σ(v(w)Tθj1u)]1djw[1σ(v(w)Tθj1u)]dju p(d^u_j|v(w), \theta^u_{j-1})=[\sigma(v(w)^T \theta^u_{j-1})]^{1-d^w_j} \cdot [1- \sigma(v(w)^T \theta^u_{j-1})]^{d^u_j}

  那麼現在將上面的式子帶回,然後可以得到:

ζ=wCloguContext(w)j=2lu{[σ(v(w)Tθj1u)]1djw[1σ(v(w)Tθj1u)]dju}=wCuContext(w)j=2lu{(1dju)log[σ(v(w)Tθj1u)]+djulog[1σ(v(w)Tθj1u)]} \begin {aligned} \zeta &= \sum_{w \in C} \log \prod_{u \in Context(w)} \prod_{j=2}^{l^u} \{ [\sigma(v(w)^T \theta^u_{j-1})]^{1-d^w_j} \cdot [1- \sigma(v(w)^T \theta^u_{j-1})]^{d^u_j} \} \\ &= \sum_{w \in C} \sum_{u \in Context(w)} \sum_{j=2}^{l^u}\{ (1-d^u_j) \cdot \log [\sigma(v(w)^T\theta^u_{j-1})] + d^u_j \log [1- \sigma(v(w)^T \theta^u_{j-1})] \} \end {aligned}

  還和上次一樣,爲了推導方便,我們將需要求導的部分直接提取出來:

ζ(w,u,j)=(1dju)log[σ(v(w)Tθj1u)]+djulog[1σ(v(w)Tθj1u)] \zeta (w,u,j)=(1-d^u_j) \cdot \log [\sigma(v(w)^T\theta^u_{j-1})] + d^u_j \log [1- \sigma(v(w)^T \theta^u_{j-1})]

  依舊和上次一樣,我們發現這裏面只有兩個變量,分別是v(w)v(w)θj1u\theta^u_{j-1},那麼我們依舊使用隨機梯度上升法來對其進行優化,首先計算關於θj1u\theta^u_{j-1}的梯度:

Δζ(w,u,j)Δθj1u=(1dju)(1σ(v(w)Tθj1u))v(w)djuσ(v(w)Tθj1u)v(w)=[1djuσ(v(w)Tθj1u]v(w) \begin {aligned} \frac{ \Delta \zeta(w,u,j)}{\Delta \theta^u_{j-1}} &= (1-d^u_j)(1- \sigma(v(w)^T \theta^u_{j-1}))v(w)-d^u_j \sigma(v(w)^T \theta^u_{j-1})v(w) \\ &= [1-d^u_j-\sigma(v(w)^T \theta^u_{j-1}]v(w) \end {aligned}

於是,θj1u\theta^u_{j-1}的更新公式可以寫成:

θj1u=θj1u+η[1djuσ(v(w)Tθj1u]v(w) \theta^u_{j-1}=\theta^u_{j-1} + \eta [1-d^u_j-\sigma(v(w)^T \theta^u_{j-1}]v(w)

同理,根據對稱性,可以很容易得到ζ(w,u,j)\zeta(w,u,j)關於v(w)v(w)的梯度:

Δζ(w,u,j)Δv(w)=[1djuσ(v(w)Tθj1u]θj1u \begin {aligned} \frac{ \Delta \zeta(w,u,j)}{\Delta v(w)} &= [1-d^u_j-\sigma(v(w)^T \theta^u_{j-1}] \theta^u_{j-1} \end {aligned}

我們也可以得到關於v(w)的更新公式:

v(w)=v(w)+ηuContext(w)j=2lwΔζ(w,u,j)Δv(w) v(w)=v(w)+ \eta \sum_{u \in Context(w)} \sum^{l^w}_{j=2} \frac{ \Delta \zeta(w,u,j)}{\Delta v(w)}

  那麼我們可以到Skip-gram使用層次softmax方法的時候的僞代碼:

在這裏插入圖片描述
這裏依舊需要注意的是,(3.3)和(3.4)不能交換位置,原因在上面已經解釋過了

這裏給出和源碼的對應關係:syn0syn0表示v(u)v(u),而syn1syn1表示θj1w\theta^w_{j-1},neulneul表示xwx_w,neuleneule表示ee. 其實看到這裏,你會發現,只要搞懂了一個,剩下的那個就很簡單了

4.基於負採樣的模型

  下面將介紹基於負採樣的CBOW和Skip-gram模型.具體什麼NCE,NGE,我也不是特別清楚他們的關係,大家都說負採樣是NCE的簡化版本,具體什麼樣,我沒有深究,以後有機會了再去研究.使用負採樣的時候,可以明顯感覺到訓練速度快於層次softmax,而且不需要構建複雜的哈弗曼樹.再我實際訓練的過程中,在使用C語言的時候,相對於層次softmax,訓練速度可以獲得好幾倍的增長,即使使用Python,訓練速度也至少增長了兩倍.

4.1 負採樣算法簡單介紹

  什麼是負採樣呢?
  例如在CBOW中,我們是知道了Context(w)Context(w),然後來預測單詞ww,那麼這個時候,相對於Context(w)Context(w),我們提供一組結果,這些結果中包含正確的解ww,剩下的都是錯誤的解,那麼ww就是正樣本,剩下的解就是負樣本.Skip-gram類似,相當於給一組輸入,然後預測正確的輸出Context(w)Context(w),輸入的一組數據裏面,有一個是正確的輸入,爲v(w)v(w),剩下的都是錯誤的輸入,也就是負樣本.
  那麼如何確定怎麼選取負樣本呢?
  這裏採用的是一種帶權採樣的方法,這裏的,在這裏可以使用詞的頻率來表示,也就是說,詞的頻率越高,它的權重越大,被採集到的可能性就越大.例如設詞典中每個單詞ww對應的權值爲len(w)len(w):

len(w)=counter(w)uCcounter(u) len(w)=\frac{counter(w)}{\sum_{u \in C}counter(u)}

這裏counter(w)counter(w)表示單詞ww出現的次數.

在word2vec中,它的做法很簡單,在word2vec中,令

l0=0,...,lk=j=1klen(wj),   k=1,2,...,N l_0=0,..., l_k=\sum^{k}_{j=1}len(w_j), \ \ \ k=1,2,...,N

這裏wjw_j表示詞典中的第jj個單詞,那麼按照集合{li}j=0N\{l_i\}^N_{j=0}中每個元素的大小,可以按照一定的比例將[0,1][0,1]進行劃分,這個劃分是非等距的,並且將[0,1][0,1]劃分成了N份(也就是說有N個單詞).這個時候,再提供一個在[0,1][0,1]上的等距劃分,劃分爲M份,這裏要求M>>NM>>N,如下圖所示:

在這裏插入圖片描述

這樣就可以將非等距劃分的{li}j=1N\{l_i\}^N_{j=1}映射到等距劃分的Table(i)Table(i)上,當然了,lil_i實際上就代表的單詞,那麼在映射的時候,把ljl_j換成wjw_j:

Table(i)=wj,           mi(ljlj1),i=1,2,...,M1,j=1,2...,N Table(i)=w_j, \ \ \ \ \ \ \ \ \ \ \ m_i \in (l_j-l_{j-1}),i=1,2,...,M-1,j=1,2...,N

  之後根據映射關係,每次對單詞wkw^k進行負採樣的時候,在[1,M1][1,M-1]上生成一個隨機數ii,然後Table(i)Table(i)就是那個被採樣到的單詞.如果這個時候不幸採樣到了單詞wkw^k自己,這個時候,word2vec源代碼的處理方式是直接跳過去,忽略這次採樣的結果就行了,畢竟這樣的概率不太高.
  不過在word2vec中,原作者實際上沒有直接使用counter(w)counter(w),而是加上了一個α\alpha次方,在代碼中,實際上是下面這樣的:

len(w)=counter(w)αuC[counter(u)]α=counter(w)0.75uC[counter(u)]0.75 \begin {aligned} len(w) &= \frac{counter(w)^\alpha}{\sum_{u \in C}[counter(u)]^\alpha} \\ \\ &= \frac{counter(w)^{0.75}}{\sum_{u \in C}[counter(u)]^{0.75}} \end {aligned}

  猜測作者這樣寫,是因爲想提高一點低頻詞被採集到的概率.除此之外,作者在代碼中取M=108M=10^8,源代碼中是變量table_size.

這裏我在使用Python實現的時候,採用的是原作者的方式,但是實際在初始化Tabel(i)的時候,還是挺慢的,大概需要十幾秒的時間,原作者使用的C語言,要快的多.我猜想的是numpy自帶的有choice函數,這個函數可以根據所給的數據,從這些數據中隨機抽取一個出來,同時可以設置每個數據被隨機抽取到的概率.然後每次進行負採樣的時候,直接使用這個函數生成負採樣結果,不知道這樣效率會不會提升.或者提前使用這個函數生成一組負採樣結果,計算的時候就直接拿來用.我沒有嘗試,你要是感興趣可以試試.

4.2 CBOW 負採樣

4.2.1 前向傳播

  上面的負採樣已經介紹完了,下面開始進行公式的推導.首先我們先選好一個關於Context(w)Context(w)的負樣本集NEG(w)NEG(w),對於uNEG(w){w}\forall u \in NEG(w) \cup \{w\},我們定義單詞uu的標籤爲:

Lw(u)={1,   u=w0,   uw L^w(u)= \left\{\begin{matrix} 1, & \ \ \ u=w \\ 0, & \ \ \ u \neq w \end{matrix}\right.

其中1表示是正樣本,0表示負樣本.
  對於一個給定的Context(w)Context(w)的正樣本NEG(w)NEG(w),我們希望最大化的目標函數是:

g(w)=u{w}NEG(W)p(uContext(w)) g(w)=\prod_{u \in \{w\} \cup NEG(W)} p(u|Context(w))

其中

p(uContext(w))={σ(xwTθu),   Lw(u)=11σ(xwTθu),   Lw(u)=0=[σ(xwTθu)]Lw(u)[1σ(xwT)θu]1Lw(u) \begin {aligned} p(u|Context(w)) &= \left\{\begin{matrix} \sigma(x^T_w \theta^u), & \ \ \ L^w(u)=1 \\ 1-\sigma(x^T_w \theta^u), & \ \ \ L^w(u)=0 \end{matrix}\right. \\\\ &= [\sigma(x^T_w\theta^u)]^{L^w(u)} \cdot [1-\sigma(x^T_w)\theta^u]^{1-L^w(u)} \end {aligned}

  這裏需要注意的是,這裏的xwx_w依舊還是上面CBOW-hs中定義的Context(w)Context(w)中所有詞的詞向量之和,而θuRm\theta^u \in R^m在這裏作爲一個輔助向量,作爲待訓練的參數.

  爲什麼最大化g(w)g(w)就可以了呢?我們可以改變一下g(w)的表達式:

g(w)=σ(xwTθw)uNEG(w)[1σ(xwTθu)] g(w)=\sigma(x^T_w\theta^w) \prod_{u \in NEG(w)} [1- \sigma(x^T_w\theta^u)]

  我們可以看到,如果我們最大化g(w)g(w)的話,就可在最大化σ(xwTθw)\sigma(x^T_w \theta^w)的同時,最大化1σ(xwTθu), uNEG(w)1- \sigma(x^T_w\theta^u), \ u \in NEG(w),也就是最小化σ(xwTθu), uNEG(w)\sigma(x^T_w\theta^u), \ u \in NEG(w).這樣就相當於最大化了正樣本,最小化了負樣本.既然明白了這個,那麼對於整個語料庫,有:

G=wCg(w) G = \prod_{w \in C}g(w)

作爲最終的優化目標,這裏爲了求導方便,其實就是爲了把\prod轉換成\sum,我們在GG前面加上loglog,得到:

ζ=logG=wClogg(w)=wCuwNEG(w)log{[σ(xwTθu)]Lw(u)[1σ(xwT)θu]1Lw(u)}=wCuwNEG(w){Lw(u)log[σ(xwTθu)+[1Lw(u)]log[1σ(xwTθu)]]} \begin {aligned} \zeta &= \log G \\ &= \sum_{w \in C} \log g(w) \\ &= \sum_{w \in C} \sum_{u \in {w} \cup NEG(w)} \log \{ [\sigma(x^T_w\theta^u)]^{L^w(u)} \cdot [1-\sigma(x^T_w)\theta^u]^{1-L^w(u)} \} \\ &= \sum_{w \in C} \sum_{u \in {w} \cup NEG(w)} \{ L^w(u) \cdot \log[\sigma(x^T_w \theta^u) + [1-L^w(u)] \cdot \log [1-\sigma(x^T_w \theta^u)]] \} \end {aligned}

同樣,爲了求導方便,我們還是取ζ(w,u)\zeta(w,u):

ζ(w,u)=Lw(u)log[σ(xwTθu)+[1Lw(u)]log[1σ(xwTθu)]] \zeta(w,u) = L^w(u) \cdot \log[\sigma(x^T_w \theta^u) + [1-L^w(u)] \cdot \log [1-\sigma(x^T_w \theta^u)]]

4.2.2 反向傳播

  於是乎,現在到了反向傳播的時候了,和以前的都幾乎一樣啦,這還是使用隨機梯度上升法,然後首先求關於θu\theta^u的梯度:

Δζ(w,u)Δθu=Lw(u)[1σ(xwTθu)]xw[1Lw(u)]σ(xwTθu)xw=[Lw(u)σ(xwTθu)]xw \begin {aligned} \frac{\Delta \zeta(w,u)}{\Delta \theta^u} &=L^w(u)[1- \sigma(x^T_w\theta^u)]x_w-[1-L^w(u)] \cdot \sigma(x^T_w \theta^u)x_w \\ &=[L^w(u)-\sigma(x^T_w \theta^u)]x_w \end {aligned}

那麼θu\theta^u的更新公式可以寫成:

θu=θu+η[Lw(u)σ(xwTθu)]xw \theta^u=\theta^u+\eta [L^w(u)-\sigma(x^T_w \theta^u)]x_w

  同時根據對稱性,額可以得到xwx_w的梯度:

Δζ(w,u)Δxw=[Lw(u)σ(xwTθu)]θu \begin {aligned} \frac{\Delta \zeta(w,u)}{\Delta x_w} &=[L^w(u)-\sigma(x^T_w \theta^u)] \theta^u \end {aligned}

那麼v(w)v(w)的更新公式可以寫成:

v(w~)=v(w~)+ηuwNEG(w)Δζ(w,u)Δxw,  w~Context(w) v(\tilde w) =v(\tilde w)+ \eta \sum_{u \in {w} \cup NEG(w)} \frac{\Delta \zeta(w,u)}{\Delta x_w}, \ \ \tilde w \in Context(w)

  最後這裏給出基於負採樣的CBOW的僞代碼:

1. e=02. xw=uContext(w)v(u)3. FOR  u=wNEG(w):    {        3.1 q=σ(xwTθu)        3.2 g=η(Lu(w)q)        3.3 e=e+gθu        3.4 θu=θu+gxw    }4. FOR  uContext(w):    {         v(u)=v(u)+e    } \begin {aligned} & 1. \ e=0 \\ & 2. \ x_w = \sum_{u \in Context(w)}v(u) \\ & 3. \ FOR \ \ u = {w} \cup NEG(w): \\ & \ \ \ \ \{ \\ & \ \ \ \ \ \ \ \ 3.1 \ q = \sigma(x^T_w \theta^u) \\ & \ \ \ \ \ \ \ \ 3.2 \ g = \eta(L^u(w) -q) \\ & \ \ \ \ \ \ \ \ 3.3 \ e = e + g \theta^u \\ & \ \ \ \ \ \ \ \ 3.4 \ \theta^u = \theta^u + g x_w \\ & \ \ \ \ \} \\ & 4. \ FOR \ \ u \in Context(w): \\ & \ \ \ \ \{ \\ & \ \ \ \ \ \ \ \ \ v(u) = v(u) + e \\ & \ \ \ \ \} \\ \end {aligned}
依舊是3.3和3.4的位置不能對調,然後對應於代碼的關係是:syn0syn0對應v(u)v(u), syn1negsyn1neg對應θu\theta^u(不過在Python中這裏依舊使用的是syn1),neulneul對應是xwx_w,neule對應是ee.

4.3 Skip-gram 負採樣

4.3.1 前向傳播

  因爲這裏和前面的幾乎都很類似,所以這裏就不再多敘述,直接給出最終的優化目標

ζ=logGG=wCg(w)g(w)=w~Context(w)u{w}NEUw~(w)p(Contextu)p(Contextu)={σ(v(w~)Tθu),   Lw(u)=11σ(v(w~)Tθu),   Lw(u)=0=[σ(v(w~)T]Lw(u)[1σ(v(w~)T]1Lw(u)Lw(u)={1,   u=w0,   uw \begin {aligned} \zeta&= \log G \\\\ G&=\prod_{w \in C}g(w) \\ \\ g(w)&= \prod_{\tilde w \in Context(w)} \prod_{u \in \{w\} \cup NEU^{\tilde w}(w)}p(Context|u) \\\\ p(Context|u) & = \left\{\begin{matrix} \sigma(v(\tilde w)^T \theta^u), & \ \ \ L^w(u)=1 \\ 1-\sigma(v(\tilde w)^T \theta^u), & \ \ \ L^w(u)=0 \end{matrix}\right. \\ &=[\sigma(v(\tilde w)^T]^{L^w(u)} \cdot [1-\sigma(v(\tilde w)^T]^{1-L^w(u)} \\\\ L^w(u)&= \left\{\begin{matrix} 1, & \ \ \ u=w \\ 0, & \ \ \ u \neq w \end{matrix}\right. \end {aligned}

  化簡之後,可以得到ζ\zeta

ζ=wCw~Context(w)u{w}NEUw~(w)Lw(u)log[σ(v(w~)Tθu)]+[1Lw(u)]log[1σ(v(w~)Tθu)] \begin {aligned} \zeta = & \sum_{w\in C} \sum_{\tilde w \in Context(w)} \sum_{u \in \{w\} \cup NEU^{\tilde w}(w)} \\ &L^w(u)\log[\sigma(v(\tilde w)^T \theta^u)] + [1-L^w(u)]\log[1-\sigma(v(\tilde w)^T \theta^u)] \end {aligned}

爲了推導方便,我們依舊提取出來ζ(w,w~,u)\zeta(w, \tilde w, u)

ζ(w,w~,u)=Lw(u)log[σ(v(w~)Tθu)]+[1Lw(u)]log[1σ(v(w~)Tθu)] \zeta(w, \tilde w, u) = L^w(u)\log[\sigma(v(\tilde w)^T \theta^u)] + [1-L^w(u)]\log[1-\sigma(v(\tilde w)^T \theta^u)]

下面進行梯度的求解.

4.3.2 反向傳播

  這裏依舊首先對θu\theta^u進行求導:

Δζ(w,w~,u)Δθu=Lw(u)[1σ(v(w~)wTθu)]v(w~)[1Lw(u)]σ(v(w~)wθu)v(w~)T=[Lw(u)σ(v(w~)Tθu)]v(w~) \begin {aligned} \frac{\Delta \zeta(w, \tilde w, u)}{\Delta \theta^u} &=L^w(u)[1- \sigma(v(\tilde w)^T_w\theta^u)]v(\tilde w)-[1-L^w(u)] \cdot \sigma(v(\tilde w)_w \theta^u)v(\tilde w)^T \\ &=[L^w(u)-\sigma(v(\tilde w)^T \theta^u)]v(\tilde w) \end {aligned}

然後得到θu\theta^u的更新公式:

θu=θu+η=[Lw(u)σ(v(w~)Tθu)]v(w~) \theta^u = \theta^u + \eta =[L^w(u)-\sigma(v(\tilde w)^T \theta^u)]v(\tilde w)

  同理根據對稱性,得到:

Δζ(w,w~,u)Δv(w~)=[Lw(u)σ(v(w~)Tθu)]θu \begin {aligned} \frac{\Delta \zeta(w, \tilde w, u)}{\Delta v(\tilde w)} &=[L^w(u)-\sigma(v(\tilde w)^T \theta^u)]\theta^u \end {aligned}

然後得到v(w~)v(\tilde w)的更新公式:

v(w~)=v(w~)+u{w}NEUw~(w)Δζ(w,w~,u)Δv(w~),   w~Context(w) v(\tilde w) = v(\tilde w) + \sum_{u \in \{w\} \cup NEU^{\tilde w}(w)} \frac{\Delta \zeta(w, \tilde w, u)}{\Delta v(\tilde w)}, \ \ \ \tilde w \in Context(w)

  最後依舊是僞代碼,同時還是3.3和3.4不能顛倒.同時和代碼對應關係是:syn0syn0對應v(u)v(u),syn1negsyn1neg對應θu\theta^u(python 代碼中依舊是syn1),neuleneule對應ee.

1. FOR  w~Context(w):    {         2. e=0         3. FOR  u=wNEGw~(w):            {                   3.1 q=σ(v(w~)Tθu)                   3.2 g=η(Lw(u)q)                   3.3 e=e+gθu                   3.4 θu=θu+gv(w~)            }        v(w~)=v(w~)+e    } \begin {aligned} & 1. \ FOR \ \ \tilde w \in Context(w): \\ & \ \ \ \ \{ \\ & \ \ \ \ \ \ \ \ \ 2. \ e = 0 \\ & \ \ \ \ \ \ \ \ \ 3. \ FOR \ \ u = {w} \cup NEG^{\tilde w}(w): \\ & \ \ \ \ \ \ \ \ \ \ \ \ \{ \\ & \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 3.1 \ q = \sigma(v(\tilde w)^T \theta^u) \\ & \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 3.2 \ g = \eta(L^w(u) - q) \\ & \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 3.3 \ e = e + g \theta^u \\ & \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 3.4 \ \theta^u = \theta^u + g v(\tilde w) \\ & \ \ \ \ \ \ \ \ \ \ \ \ \} \\ & \ \ \ \ \ \ \ \ v(\tilde w) = v(\tilde w) + e \\ & \ \ \ \ \} \\ \end {aligned}

5. 後記

  斷斷續續使用了4天寫完了這篇博客,這篇博客幾乎都參考了peghoty.雖然大神總結的很好了,根據大神的教程和github的一些代碼,已經使用Python複寫出word2vec的代碼,並且成功訓練出了還行的結果,雖然Python效率很低,而且對多線程的支持不好(使用了多進程),多進程數據交互時間較長,但是也是實現出來了.然後使用這篇博客記錄一些自己的理解.本來認爲理解的已經還不錯了.但是在參考了peghoty大神的總結,然後寫博客的過程中,對於公式的推導,和對於一些模糊的地方有了一個更加清晰的認識,也感覺到了自己學習的不足,日後需要更加努力!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章