文章目錄
- 0引言
- 1 CBOW模型的重構
- 1.1模型初始化
- 1.2模型的前向計算
- 1.3模型的反向傳播
- 2總結
0引言
- 前面講述了對word2vec高速化的改進:
- 改進輸入側的計算,變成Embedding,即從權重矩陣中選取特定的行;
- 改進輸出側的計算,包含兩點
- 改進輸出側矩陣乘法,改為Embedding_dot層,Embedding部分其實與輸入側一樣;dot部分就是將中間層的結果與Embedding部分的結果做內積得到一個值;
- 化多分類為二分類,將softmax改進為sigmoid,并引入負采樣方法;損失函數依然使用交叉熵損失,只不過是二分類的;
- 接下來,將這兩塊的改進應用到CBOW模型上,重新構建CBOW模型以及學習代碼。
1 CBOW模型的重構
代碼位于:
improved_CBOW/CBOW.py
;代碼文件鏈接:https://1drv.ms/u/s!AvF6gzVaw0cNjqNRnWXdF3J6J0scCA?e=3mfDlx;
1.1模型初始化
-
截止模型初始化,程序入口的代碼如下:
if __name__ == "__main__":text = "you say goodbye and I say hello."# 構建單詞與編號之間的映射并將句子向量化corpus, word_to_id, id_to_word = preprocess(text)# contexts是一個維度為[6,2]的numpy數組contexts = np.array([[0, 2], [1, 3], [2, 4], [3, 1], [4, 5], [1, 6]]) # (6,2)target = np.array([1, 2, 3, 4, 1, 5]) # (6,)vocab_size = len(word_to_id)hidden_size = 3window_size = 1CBOW_model = CBOW(vocab_size, hidden_size, window_size, corpus)
-
改進之后CBOW模型的初始化代碼如下:
class CBOW:def __init__(self, vocab_size, hidden_size, window_size, corpus):V, H = vocab_size, hidden_size# 初始化權重W_in = 0.01 * np.random.randn(V, H).astype('f') # (7,3)# 因為W_out這里將使用embedding層,計算時需要轉置,# 所以這里索性初始化就直接是轉置后的W_out = 0.01 * np.random.randn(V, H).astype('f') # (7,3)# 生成層self.in_layers = []for i in range(2 * window_size):layer = Embedding(W_in) # 使用Embedding層self.in_layers.append(layer)self.ns_loss = NegativeSamplingLoss(W_out, corpus, power=0.75, sample_size=3)# 將所有的權重和梯度整理到列表中layers = self.in_layers + [self.ns_loss]self.params, self.grads = [], []for layer in layers:self.params += layer.paramsself.grads += layer.grads# 將單詞的分布式表示設置為成員變量self.word_vecs = W_in
-
關于初始化的代碼,做如下解釋:
- 因為
W_out
這里將使用Embedding層,前面的筆記中說過,計算時需要轉置,所以這里索性初始化就直接是轉置后的;因此從代碼上來看,輸入側的權重和輸出側的權重維度相同,在學習的過程中分別去優化; - 和之前一樣,根據上下文窗口的大小,生成相應數量的輸入層;只是這里改進之后,創建的是相應數量的Embedding層
- 根據負采樣的
sample_size
,為每個負例創建相應的sigmoid層以及交叉熵損失計算層;為正例創建一個sigmoid層以及交叉熵損失計算層
- 因為
-
再來看一下初始化的結果:
-
如下圖:創建的
CBOW_model
包含輸入層in_layers
、輸出側的Embedding_dot層ns_loss.embed_dot_layers
、sigmoid&交叉熵損失計算層ns_loss.loss_layers
; -
每一個Embedding_dot層都包含一個Embedding層,其中的參數維度都是
(vocab_size,hidden_size)
;如下圖所示: -
經過整理,所有的參數和梯度都被整理到一塊,如下圖所示;前兩個是輸入側的兩個權重矩陣的參數(因為上下文窗口大小為
1
),后面四個是輸出側一個正例和三個負例的權重矩陣的參數;梯度跟參數對應,這里就不列了;
-
1.2模型的前向計算
-
為了進行前向計算,程序入口增加的代碼如下:
loss = CBOW_model.forward(contexts, target) # contexts:(6,2);target:(6,)
-
前向計算的代碼如下:
def forward(self, contexts, target):'''@param contexts: 目標詞的上下文;(batch_size, 2*window_size);e.g. (6,2)@param target: 目標詞;(batch_size,);e.g. (6,)'''h = 0for i, layer in enumerate(self.in_layers):h += layer.forward(contexts[:, i]) # h:(6,3)h *= 1 / len(self.in_layers) # 對h進行平均;window_size不一定是1,所以取決于self.in_layersloss = self.ns_loss.forward(h, target)return loss
-
關于輸入側的計算:
- 每次計算一個mini-batch的上下文的某一個單詞的前向計算結果,因此每次傳入的是
contexts[:, i]
,維度是(6,)
;這是一個mini-batch的單詞ID,forward
方法會從該layer
的權重矩陣中抽取對應的行,返回的結果就是(6,3)
的h
; - 由于我們只改變了輸入側的計算方法,輸入側的計算結果仍然像之前一樣,求平均;因此需要對所有輸入層的中間結果求平均得到總的
h
;
- 每次計算一個mini-batch的上下文的某一個單詞的前向計算結果,因此每次傳入的是
-
接著,在
self.ns_loss.forward
中,首先進行負例采樣;根據傳入的target
,為其中每一個樣本抽取sample_size
個負例樣本對應的單詞ID,得到negative_sample
,維度為(batch_size,sample_size)
; -
接著,在
self.ns_loss.forward
中,進行正例的前向計算;將這一個mini-batch的正例從輸出側的權重矩陣中抽取對應的行,并于對應的中間結果做內積,得到這個mini-batch的得分,維度為(batch_size,)
;例如(6,)
;然后將這個得分和真實標簽一起送入sigmoid&損失計算層,計算交叉熵損失得到損失值;這個損失值是一個標量,是一個mini-batch損失的平均值; -
接著,在
self.ns_loss.forward
中,進行負例的前向計算;計算過程與正例一樣;但因為每個樣本的有sample_size
個負例,因此一次同時處理一個mini-batch的某一個負例;然后將所有負例的損失累加的正例的損失中,作為最終的前向計算的損失值; -
輸出以及損失側的計算步驟較多,這里再貼出來
loss = self.ns_loss.forward(h, target)
的具體過程:def forward(self, h, target):'''@param h: 中間層的結果,維度為(batch_size,hidden_dim); e.g. (6,3)@param target: 正確解標簽;維度為(batch_size,); e.g. (6,)'''batch_size = target.shape[0]# 獲取self.sample_size個負例解標簽negative_sample = self.sampler.get_negative_sample(target) # (batch_size,sample_size); e.g. (6,3)# 正例的正向傳播score = self.embed_dot_layers[0].forward(h, target) # (batch_size,) e.g. (6,)correct_label = np.ones(batch_size, dtype=np.int32) # 正例的真實標簽自然是1;維度為(batch_size,) e.g. (6,)loss = self.loss_layers[0].forward(score, correct_label) # 損失標量# 負例的正向傳播negative_label = np.zeros(batch_size, dtype=np.int32) # 負例的真實標簽自然是0;維度為(batch_size,) e.g. (6,)for i in range(self.sample_size):# 對一個mini-batch的每一個負例樣本,依次計算損失并累加到正例的損失上去negative_target = negative_sample[:, i] # (batch_size,) e.g. (6,)score = self.embed_dot_layers[1 + i].forward(h, negative_target) # (batch_size,)loss += self.loss_layers[1 + i].forward(score, negative_label)return loss
1.3模型的反向傳播
-
為了進行反向傳播,程序入口增加的代碼如下:
CBOW_model.backward()
-
先進行輸出側的反向傳播:
-
輸出側由一個正例+
sample_size
個負例組成,根據計算圖,它們求得的輸出層的輸入側的梯度需要進行累加; -
因此遍歷所有的
loss_layers
和embed_dot_layers
,然后先進行loss_layer
的反向傳播,再進行embed_dot_layer
的反向傳播;- 因為前向計算時每個損失是累加起來作為最終損失的,因此反向傳播時傳到每個損失里面的
dout=1
;于是就根據之前推導的結果,sigmoid+交叉熵損失的梯度是y-t
,計算傳遞至loss_layer
輸入側的梯度; - 然后計算
embed_dot_layer
的反向傳播;計算對dtarget_w
的梯度以更新這個Embedding_dot層的權重參數;計算dh
以將梯度傳遞至下游;過程在前面的筆記中講解過;
- 因為前向計算時每個損失是累加起來作為最終損失的,因此反向傳播時傳到每個損失里面的
-
由于過程較多,因此這里貼出來代碼供查看;另外注釋也更新了;
# 輸出側損失反向傳播入口 dout = self.ns_loss.backward(dout) # 輸出側損失反向傳播入口對應的反向傳播函數 def backward(self, dout=1):dh = 0# 中間層結果h到輸出側是進入了多個分支,因此反向傳播時梯度需要累加for l0, l1 in zip(self.loss_layers, self.embed_dot_layers):# 依次對正例和每個負例所在的網絡結構進行反向傳播dscore = l0.backward(dout) # 損失函數(sigmoid和交叉熵損失)的反向傳播,即y-t的結果;維度為(batch_size,); e.g. (6,)dh += l1.backward(dscore) # Embedding_dot的反向傳播,包含保存各自權重矩陣對應的行的梯度return dh# sigmoid函數的反向傳播 def backward(self, dout=1):'''本質上是對sigmoid函數的輸入求梯度'''batch_size = self.t.shape[0]dx = (self.y - self.t) * dout / batch_size # 這里將梯度平均了;維度為(batch_size,); e.g. (6,)return dx# Embedding_dot層的反向傳播 def backward(self,dout):'''@param dout: 上游損失函數的梯度;形狀為(batch_size,);e.g. (6,)'''h,target_w=self.cachedout=dout.reshape(dout.shape[0],1) # 這里是為了保證dout的形狀與h的形狀一致;形狀為(batch_size,1);e.g. (6,1)dtarget_w=dout*h # 對應元素相乘;dout:[batch_size,1];h:[batch_size,hid_dim];所以會進行廣播;形狀為(batch_size,hidden_dim);e.g. (6,3)self.embed.backward(dtarget_w) # 把梯度更新到權重矩陣的梯度矩陣的對應行;先前在執行self.embed.forward(idx)時已經保存了使用的idxdh=dout*target_w # 對應元素相乘;會進行廣播;形狀為(batch_size,hidden_dim);e.g. (6,3)return dh
-
-
然后是中間層的梯度,因為前向計算時,是對window_size個輸入層的輸出結果平均了,才得到的h;所以執行如下語句計算中間層的梯度;
dout *= 1 / len(self.in_layers)
-
最后,計算window_size個輸入層的梯度,即Embedding層;由于只是從輸入側權重矩陣中選取了特定行,因此梯度的傳播僅僅是將上游傳遞來的梯度值放到對應的梯度矩陣中;代碼如下:
for layer in self.in_layers:layer.backward(dout)
2總結
- 幾點注意
- 關于這里使用的batch_size的含義:個人理解,這里的批處理大小并不是指通常意義的樣本數(or句子數),CBOW模型每次的輸入就是目標詞的上下文單詞;一個目標詞對應的上下文單詞構成mini-batch里面的一條數據;
- 改進之前,輸入和輸出都是使用的獨熱編碼,改進之后,不再使用;