系列所有帖子
自己動手寫word2vec (一):主要概念和流程
自己動手寫word2vec (二):統計詞頻
自己動手寫word2vec (三):構建Huffman樹
自己動手寫word2vec (四):CBOW和skip-gram模型
CBOW和skip-gram應該可以說算是word2vec的核心概念之一了。這一節我們就來仔細的闡述這兩個模型。其實這兩個模型有很多的相通之處,所以這裏就以闡述CBOW模型爲主,然後再闡述skip-gram與CBOW的不同之處。這一部分的代碼放在pyword2vec.py文件中
1.CBOW模型
之前已經解釋過,無論是CBOW模型還是skip-gram模型,都是以Huffman樹作爲基礎的。而Huffman樹的構建在前一節已經講過咯,這裏就不再重複。值得注意的是,Huffman樹中非葉節點存儲的中間向量的初始化值是零向量,而葉節點對應的單詞的詞向量是隨機初始化的。
1.1 訓練的流程
那麼現在假設我們已經有了一個已經構造好的Huffman樹,以及初始化完畢的各個向量,可以開始輸入文本來進行訓練了。
訓練的過程如下圖所示,主要有輸入層(input),映射層(projection)和輸出層(output)三個階段。
輸入層即爲某個單詞A周圍的n-1個單詞的詞向量。如果n取5,則詞A(可記爲w(t))前兩個和後兩個的單詞爲w(t-2),w(t-1),w(t+1),w(t+2)。相對應的,那4個單詞的詞向量記爲v(w(t-2)),v(w(t-1)),v(w(t+1)),v(w(t+2))。從輸入層到映射層比較簡單,將那n-1個詞向量相加即可。而從映射層到到輸出層則比較繁瑣,下面單獨講
1.2 從映射層到輸出層
要完成這一步驟,需要藉助之前構造的Huffman樹。從根節點開始,映射層的值需要沿着Huffman樹不斷的進行logistic分類,並且不斷的修正各中間向量和詞向量。
舉個例子, 比如說有下圖所示的Huffman樹
此時中間的單詞爲w(t),而映射層輸入爲
pro(t)=v(w(t-2))+v(w(t-1))+v(w(t+1))+v(w(t+2))
假設此時的單詞爲“足球”,即w(t)=“足球”,則其Huffman碼可知爲d(t)=”1001”(具體可見上一節),那麼根據Huffman碼可知,從根節點到葉節點的路徑爲“左右右左”,即從根節點開始,先往左拐,再往右拐2次,最後再左拐。
既然知道了路徑,那麼就按照路徑從上往下依次修正路徑上各節點的中間向量。在第一個節點,根據節點的中間向量Θ(t,1)和pro(t)進行Logistic分類。如果分類結果顯示爲0,則表示分類錯誤(應該向左拐,即分類到1),則要對Θ(t,1)進行修正,並記錄誤差量。
接下來,處理完第一個節點之後,開始處理第二個節點。方法類似,修正Θ(t,2),並累加誤差量。接下來的節點都以此類推。
在處理完所有節點,達到葉節點之後,根據之前累計的誤差來修正詞向量v(w(t))。
這樣,一個詞w(t)的處理流程就結束了。如果一個文本中有N個詞,則需要將上述過程在重複N遍,從w(0)~w(N-1)。
1.3 CBOW模型的僞代碼描述
將模型形象化的描述過以後,還需要以更精確的方式將模型的流程確定下來。
首先,我們需要先引入一些符號以便於更清晰的表達。
那麼根據word2vec中的數學,流程可以表述爲
其中σ表示sigmoid函數,η表示學習率。學習率越大,則判斷錯誤的懲罰也越大,對中間向量的修正跨度也越大。
1.4 CBOW模型的代碼描述
爲了提高複用性,代碼主要由兩部分組成,分別是__Deal_Gram_CBOW和__GoAlong_Huffman。後者負責最核心部分,也就是與huffman相關的部分,前者負責剩下的功能,包括修正詞向量等
def __Deal_Gram_CBOW(self,word,gram_word_list):
if not self.word_dict.__contains__(word):
return
word_huffman = self.word_dict[word]['Huffman']
gram_vector_sum = np.zeros([1,self.vec_len])
for i in range(gram_word_list.__len__())[::-1]:
item = gram_word_list[i]
if self.word_dict.__contains__(item):
gram_vector_sum += self.word_dict[item]['vector'] #將周圍單詞的詞向量相加
else:
gram_word_list.pop(i)
if gram_word_list.__len__()==0:
return
e = self.__GoAlong_Huffman(word_huffman,gram_vector_sum,self.huffman.root) #與Huffman相關方法
for item in gram_word_list:
self.word_dict[item]['vector'] += e
self.word_dict[item]['vector'] = preprocessing.normalize(self.word_dict[item]['vector']) #修正詞向量
def __GoAlong_Huffman(self,word_huffman,input_vector,root):
node = root #從root開始 自頂向下
e = np.zeros([1,self.vec_len]) #將誤差初始化爲零向量
for level in range(word_huffman.__len__()): # 一層層處理
huffman_charat = word_huffman[level] # 根據Huffman碼獲知當前節點應該將輸入分到哪一邊
q = self.__Sigmoid(input_vector.dot(node.value.T))
grad = self.learn_rate * (1-int(huffman_charat)-q) # 計算當前節點的誤差
e += grad * node.value # 累加誤差
node.value += grad * input_vector #修正當前節點的中間向量
node.value = preprocessing.normalize(node.value) # 歸一化
if huffman_charat=='0': #將當前節點切換到路徑上的下一節點
node = node.right
else:
node = node.left
return e
2. skip-gram模型
skip-gram與CBOW相比,只有細微的不同。skip-gram的輸入是當前詞的詞向量,而輸出是周圍詞的詞向量。也就是說,通過當前詞來預測周圍的詞。如下圖所示
由於輸出有n-1個詞,所以要對於一個詞來講,上述沿着huffman樹從頂到底的過程要循環n-1遍。。。其僞碼描述如下
其代碼描述如下,與huffman有關的代碼上面已經貼過了,就不再重複
def __Deal_Gram_SkipGram(self,word,gram_word_list):
if not self.word_dict.__contains__(word):
return
word_vector = self.word_dict[word]['vector']
for i in range(gram_word_list.__len__())[::-1]:
if not self.word_dict.__contains__(gram_word_list[i]):
gram_word_list.pop(i)
if gram_word_list.__len__()==0:
return
for u in gram_word_list:
u_huffman = self.word_dict[u]['Huffman']
e = self.__GoAlong_Huffman(u_huffman,word_vector,self.huffman.root)
self.word_dict[word]['vector'] += e
self.word_dict[word]['vector'] = preprocessing.normalize(self.word_dict[word]['vector'])