word2Vec 概述、算法實現過程
一、word2Vec 是什麼,作用什麼
背景
自然語言處理中,比如翻譯,問答系統,都需要一個基礎:如何用數據表示單個的詞呢?只有很好的表徵單個詞以後,才能後續輸入到模型中去訓練。這樣的表徵能使每個詞不一樣,最好能反映出詞更多的自身特性。
二、有哪些詞向量表示方法
one-hot vector
這種是比較容易想到的表示方法:
每個單詞表示成一個 V*1 維的向量,V 爲整個詞彙表的長度,向量中單詞對應的行爲1,其他爲0;這樣詞彙表中每個詞都不一樣。假設詞彙表中第一個單詞是aardvark,第二個是a,最後一個是zebra,則形式可以表示爲:
缺點:
- 英語中大概有1300萬個符號,這樣詞彙表維度V 將非常大。
- 詞語直接是有關聯的,比如
hotel to motel
,man to woman
,但是這樣表示後,
任意兩向量內積都爲0,每個詞之間沒有任何的關係。
所以one-hot這並不是個很好的表徵方法
基於SVD的方法
1、Word-Document Matrix
針對one-hot編碼不能表達詞與詞之間關係的問題,我們可以統計文檔中單詞的數量,以此來表徵詞之間關係。比如 “banks”, “bonds”, “stocks”, "money"這些詞出現在同一篇文檔中概率很大。
用 來表示詞-文檔矩陣。i 表示詞的索引,維度V;j 表示文檔的索引,維度記爲M。通過訓練大量文檔,最終可以得出詞矩陣,的維度。可以發現,此矩陣維度仍然沒有得到改進。
2、Window based Co-occurrence Matrix(基於窗口的共現矩陣)
假設語料庫只有三句:
- I enjoy flying.
- I like NLP.
- I like deep learning.
共現矩陣試圖表示出詞語之間共同出現的次數,假設窗口爲1,即只考慮相鄰的單詞,那麼共現矩陣可以表示爲:
如果詞彙表長度爲V, 那麼矩陣維度爲。
針對以上兩種方法,可以看出矩陣是稀疏的,維度也過大。可以通過奇異值分解SVD來將得到的矩陣降維。大體思路是仍能保證大部分特徵不丟失的前提下,找到一些維度更低的矩陣,比如,來替代以前的矩陣,這樣來降爲。具體方法可以參考PAC。
3、缺點
針對上述兩種方法即使我們可以降維,可以表示詞之間的關聯性,但是還存在許多其他問題:
- 矩陣維度會經常變動,比如Word-Document Matrix中增加了新的訓練文檔進來。
- 矩陣太稀疏,因爲共現的詞數量相對太少
- SVD 降爲因爲維度過高會很耗時
- 有一些常用單詞不平衡問題,比如"the", “he”, "has"的共現率就很高
基於迭代的方法
不同於上面的基於大量數據集,一次性統計單詞的詞頻。我們希望有種方法來找到一個詞模型可以表徵出當給定上下文單詞後,能給出哪個單詞出現概率最大,而且最好可以通過一次次的迭代來改進模型。比如"The cat jumped over the puddle.",當‘jumped’,缺失時,是否可以根據上下文單詞"The * jumped over the puddle."推斷出’jumped’呢?
如何用數學來表述這種概率呢?
上式表明了句子(每個詞出現在指定序列位置)出現的概率大小。
對於上面的例句,當cat出現時,後面一個詞爲jumped 的概率是不是比其他詞要更大呢。當然這個公式模型只考慮了相鄰的單詞,沒有考慮到整個句子,但是這個基本思想是很重要的,基於這個思想,我們引入了後續幾個模型。
1、Continuous Bag of Words Model (CBOW)
思想
對於"The cat _ over the puddle.",我們想通過上下文詞預測出缺失的是個什麼詞。即通過上下文詞來預測中心詞。
思路
首先需要一個損失函數來衡量單詞訓練樣本結果的好壞,然後使用梯度下降算法來優化參數。
這裏輸入數據爲詞的one-hot表示,維度記爲;輸出數據也爲詞的one-hot表示,維度記爲。對結果使用交叉熵cross-entropy損失函數以此來進行反向傳播進行梯度優化。
因爲輸出結果y也是on-hot形式,只有對應單詞索引位置爲1,其他全爲0,所以上面損失函數可簡化爲
i爲待預測單詞的索引,即’jumped’ one-hot中爲1的位置。
有了損失函數,通過反向傳播,梯度下降就可以得到我們想要的模型。
具體步驟
以前知道大概步驟,但是具體怎麼訓練出來的詞向量模型一直沒有搞清楚,怎麼從look up table(word embedding)算出最後的維度爲的結果。我也是通過學習資料,然後配合代碼看才把具體訓練過程搞明白。對於算法細節本身,通過代碼來了解不失爲一種好方法。
上圖W即爲對應的詞向量,訓練過程中還有個額外的 需要訓練。
- 選定一箇中心詞, 上下文窗口寬度m, 則輸入數據爲。 每個x 爲one-hot編碼維度爲.
- 詞向量(N爲自己設定的數), 計算出上下文的詞向量:。可看出上下文詞向量即爲W中對應單詞所在的一行。輸入層的每個單詞與矩陣W相乘得到的向量的就是我們想要的詞向量(word embedding),這個矩陣(所有單詞的word embedding)也叫做look up table。也就是說,任何一個單詞的onehot乘以這個矩陣都將得到自己的詞向量。
- 將所有上下文詞求均值:
- 根據損失函數:,和反向傳播梯度下降進行求解最優的,即爲詞向量。
其中梯度下降過程需要用鏈式求導,算出softmax,sigmoid等函數的微分,這裏不再細述。
2、Skip-Gram Model
和CBOW類似,不同的是輸入變爲了一個,即步驟3有差異,不用再求平均;預測輸出變爲了多個(上下文詞)。
梯度下降時,針對每個輸出,都需要更新詞向量參數。
3、Negative Sampling
對於上面兩種算法,沒次計算都會涉及到維度爲(詞彙表長度)的softmax運算,比較慢。負採樣思想是採集句子中相關聯的詞(o,c),句子外無關的詞對(k,c), 使其損失函數最小。
負樣本數爲K,正確答案爲o,那麼有o∉{1,…,K}。負採樣損失函數定義如下:
其中
爲logistic 函數。
利用反向傳播,不斷更新。
三、程序實現word2Vec
借用斯坦福大學 cs224n 的作業1 的代碼來說明具體實現細節參考
1、CBOW
def cbow(currentWord, C, contextWords, tokens, inputVectors, outputVectors,
dataset, word2vecCostAndGradient=softmaxCostAndGradient):
cost = 0.0
gradIn = np.zeros(inputVectors.shape)
gradOut = np.zeros(outputVectors.shape)
### YOUR CODE HERE
predicted_indices = [tokens[word] for word in contextWords]
predicted_vectors = inputVectors[predicted_indices]
predicted = np.sum(predicted_vectors, axis=0) # why not divid the 2*C ?, got it. softmax(x)=softmax(x/(2*C))
target = tokens[currentWord]
cost, gradIn_predicted, gradOut = word2vecCostAndGradient(predicted, target, outputVectors, dataset)
for i in predicted_indices:
gradIn[i] += gradIn_predicted
### END YOUR CODE
return cost, gradIn, gradOut
對應 具體步驟 章節來說明。其中入參,outputVectors 即爲cbow 圖中的 , inputVectors 爲對應的詞向量
softmaxCostAndGradient(predicted, target, outputVectors, dataset)
的predicted 對應 具體步驟 章節中的 ,target對應, softmaxCostAndGradient
函數將返回的。
即gradOut爲, gradIn_predicted 爲, 要求 的詞向量的梯度,由步驟2所示,,所以 的梯度更新僅僅影響對應上下文詞所在索引的, 於是會有代碼:
for i in predicted_indices:
gradIn[i] += gradIn_predicted
備足,要看懂代碼,需要知道反向傳播中softmax等函數的中微分,可以參考這裏;反向傳播中的矩陣維度也挺讓人頭大,可以配合代碼debug看。
2、Skip-Gram
代碼類似,只不過梯度更新時不一樣,因爲有多個 中心詞->上下文詞 的映射,所以會多次更新,的梯度
def skipgram(currentWord, C, contextWords, tokens, inputVectors, outputVectors,
dataset, word2vecCostAndGradient=softmaxCostAndGradient):
cost = 0.0
gradIn = np.zeros(inputVectors.shape)
gradOut = np.zeros(outputVectors.shape)
### YOUR CODE HERE
cword_idx = tokens[currentWord]
vhat = inputVectors[cword_idx]
for j in contextWords:
u_idx = tokens[j]
c_cost, c_grad_in, c_grad_out = \
word2vecCostAndGradient(vhat, u_idx, outputVectors, dataset)
cost += c_cost
gradIn[cword_idx] += c_grad_in
gradOut += c_grad_out
### END YOUR CODE
return cost, gradIn, gradOut
3、Negative Sampling
negSamplingCostAndGradient
函數返回(cost、正樣本梯度、負樣本梯度)