文章目錄
- 前言
- NNLM
- word2vec
-
- Skip-Gram
- CBOW
- GloVe
前言
2018年 NLP 領域最大的突破毫無疑問是 BERT 預訓練模型的提出,它重新整理了11項 NLP 任務,本文打算從 Word Embedding 入手,介紹 NLP 當中的預訓練發展過程,可能結合後期項目使用相應的算法,由于涉及實際使用,更新跨度會較大,希望能在學習和使用中有自己的發現和心得。
Embed這個詞,英文的釋義為"fix firmly and deeply in a surrounding mass"。Embedding在數學領域就表征某個數學結構中的一個執行個體被包含在另外一個執行個體中,當我們說某個對象 X 被嵌入到另外一個對象 Y 中,那麼 Embedding 就由一個單射的、結構保持的映射 f : X → Y 來給定的。此處的結構保持的具體含義要依賴于X 和 Y 是哪種數學結構的執行個體而定。Word embedding 是NLP中一組語言模型(language modeling)和特征學習技術(feature learning techniques)的總稱,這些技術會把詞彙表中的單詞或者短語(words or phrases)映射成由實數構成的向量上,比如One-Hot編碼、High-dimensional sparse word vector、Low-dimensional sparse word vector,這些都是 Word Embedding 的做法。說白了詞嵌入就是作為文本當中的輸入資料,類比語音中的聲譜MFCC等或是圖像中的像素值矩陣,但它相較于這兩者的不同之處就是在于它是語言模型訓練得到的副産品。
NNLM
首先要提一下統計語言模型N元文法模型(N-gram Model),它的作用是為一個長度為 m 的字元串确定一個機率分布 P ( W 1 ; W 2 ; . . . ; W m ) P(W_1; W_2; ...; W_m) P(W1;W2;...;Wm) ,表示其存在的可能性,其中 W 1 W_1 W1 到 W m W_m Wm 依次表示這段文本中的各個詞。公式如下:
P ( sentence ) = P ( W 0 ) P ( W 1 ∣ W 0 ) P ( W 2 ∣ W 0 W 1 ) P ( W 3 ∣ W 0 W 1 W 2 ) P(\text {sentence})=P(W 0) P(W 1 | W 0) P(W 2 | W 0 W 1) P\left(W 3 | W_{0} W 1 W 2\right) P(sentence)=P(W0)P(W1∣W0)P(W2∣W0W1)P(W3∣W0W1W2)
由此而來的基本語言模型:
L = ∑ 1 m log P ( w ∣ c o n t e x t ( w ) ) L=\sum_{1}^{m} \log P(w | c o n t e x t(w)) L=1∑mlogP(w∣context(w))
其實就是假設目前詞隻與之前1或者幾個詞有關系,就是隐馬爾可夫的思想嘛,是以它有兩個重要應用場景:
(1)基于一定的語料庫,利用 N-Gram 預計或者評估一個句子是否合理。
(2)評估兩個字元串之間的差異程度。這是模糊比對中常用的一種手段。
NNLM 即 Neural Network based Language Model,由Bengio在2003年提出,它是一個很簡單的模型,由四層組成,輸入層、嵌入層、隐層和輸出層。模型接收的輸入是長度為的詞序列,輸出是下一個詞的類别。首先,輸入是單詞序列的index序列,例如單詞 I 在字典(大小為)中的index是10,單詞 am 的 index 是23, Bengio 的 index 是65,則句子“I am Bengio”的index序列就是 10, 23, 65。嵌入層(Embedding)是一個大小為的矩陣,從中取出第10、23、65行向量拼成的矩陣就是Embedding層的輸出了。隐層接受拼接後的Embedding層輸出作為輸入,以tanh為激活函數,最後送入帶softmax的輸出層,輸出機率。
NNLM最大的缺點就是參數多,訓練慢。另外,NNLM要求輸入是定長,定長輸入這一點本身就很不靈活,同時不能利用完整的曆史資訊。以下就是對句子中某一詞 W t W_t Wt 的前 t − 1 t-1 t−1 項學習網絡,就是最大化: P ( W t ∣ W 1 , W 2 . . . W t − 1 ) P(W_t|W_1,W_2 ... W_{t-1}) P(Wt∣W1,W2...Wt−1)
前面的單詞用Onehot編碼,與右邊的矩陣相乘得到輸入向量 C ( W i ) C(W_i) C(Wi) ,每個單詞的輸入拼起來最後softmax層輸出預測下一個單詞,是以網絡更新的就是右邊這個矩陣的參數,而這個矩陣就是語言模型得到的副産品 Word Embedding, V 代表詞典大小,m就是 embedding 的編碼數。這個網絡用Pytorch實作如下:
class NNLM(nn.Module):
def __init__(self):
super(NNLM, self).__init__()
self.C = nn.Embedding(n_class,embed_size)
self.H = nn.Parameter(torch.randn(n_step*embed_size, n_hidden).type(dtype))
self.W = nn.Parameter(torch.randn(n_step * embed_size, n_class).type(dtype))
self.d = nn.Parameter(torch.randn(n_hidden).type(dtype))
self.U = nn.Parameter(torch.randn(n_hidden, n_class).type(dtype))
self.b = nn.Parameter(torch.randn(n_class).type(dtype))
def forward(self,X):
X = self.C(X)
X = X.view(-1,n_step*embed_size)
tanh = torch.tanh(self.d + torch.mm(X,self.H)) # [bs,n_hidden]
output = self.b + torch.mm(X,self.W) + torch.mm(tanh,self.U) #[bs,n_class】
return output
具體代碼可以檢視我的github源碼
word2vec
Word2Vec是較早提出并使用有效延續至今的NLP任務的簡單模型,其中涉及到了兩種算法,一個是CBOW一個是Skip-Gram,不同于之前的一些Word Embedding方法,它能夠自動實作:單詞語義相似性的度量;詞彙的語義的類比。反應的是類似下面這種關系:
“國王” – “王後” ≈ “男” – “女”
“英國” – “倫敦” ≈ “法國” – “巴黎” ≈ “首都”
Skip-Gram
給定input word來預測上下文。
Skip-Gram模型實際上分為了兩個部分,第一部分為建立模型,第二部分是通過模型擷取嵌入詞向量。它的整個模組化過程實際上與自編碼器(auto-encoder)的思想很相似,即先基于訓練資料建構一個神經網絡,當這個模型訓練好以後,我們并不會用這個訓練好的模型處理新的任務,我們真正需要的是這個模型通過訓練資料所學得的參數,例如隐層的權重矩陣在中實際上就是我們試圖去學習的“word vectors”。基于訓練資料模組化的過程,我們給它一個名字叫“Fake Task”,意味着模組化并不是我們最終的目的。以下是網絡模型:
關于這個網絡的詳細說明,CS224n有官方的筆記可以參考:Skip-Gram
其實就是模型的輸出機率代表着到我們詞典中每個詞有多大可能性跟 input word 同時出現。如下圖,藍色代表 input word ,将skip_num設定為2,則可以得到每個滑框得到的輸入網絡的樣本對,它們同樣都是根據樣本單詞總量來設定Onehot向量。
而我們訓練得到的隐層矩陣就是一個"單詞數" x “embedding特征量” 的權重矩陣,這就是我們所要的 Word Embedding,而其中每一行就是每個單詞訓練得到的向量表示,我們通過索引(圖中的矩陣相乘是便于了解,由于向量的稀疏浪費計算資源一般不會直接矩陣相乘)得到每個單詞的向量。
skip-gramgithub上有源碼可以參考,使用了pytorch訓練了幾句簡單的句子并可視化了最終得到的詞向量的空間分布,因為資料量太少僅僅作為簡單的參考。
CBOW
給定上下文預測中間詞。
跟前一個做法相反,這裡輸入層是由one-hot編碼的輸入上下文組成,其中視窗大小為C,詞彙表大小為V。隐藏層是N維的向量。最後輸出層是也被one-hot編碼的輸出單詞y。被one-hot編碼的輸入向量通過一個V×N維的權重矩陣WW連接配接到隐藏層;隐藏層通過一個N×V的權重矩陣W′連接配接到輸出層。這裡就不詳細說明了。
總結: 兩種Word2Vec算法相比,Skip-Gram精度更高,但CBOW訓練時間更短,一般前者更受重用一些,它有個很大的作用就是同義詞或是相近用法的詞距離更近一些,因為他們的上下文肯定是很相似的,這有助于進行詞幹化工作(stemming),就是把名字的複數形式、動詞的不同時态歸為一類的處理,對于文本分類分詞等工作有一定幫助。但是這種基于簡單網絡的Embedding有一個很大的漏洞就在于一詞多義的問題,一個單詞都是占用一行參數,一個詞的不同上下文會把這個詞編碼到同一個空間去,這就無法在使用時區分多義詞的不同語義,而這個問題下面提到的ELMO就可以很好地解決。
GloVe
GloVe 是 Global Vectors for Word Representation 的縮寫。
對于One-hot詞向量:
I = [ 1 , 0 , 0 ] L i k e = [ 0 , 1 , 0 ] A p p l e = [ 0 , 0 , 1 ] \begin{aligned} I &= [1, 0, 0] \\ Like &= [0, 1, 0] \\ Apple &= [0, 0, 1]\end{aligned} ILikeApple=[1,0,0]=[0,1,0]=[0,0,1]
無法通過兩向量夾角餘弦值計算其相似度,word2vec是一種嵌入模型,通過這種模型訓練出的詞向量可以較好的表示出詞之間的相似度,但是word2vec僅僅考慮了兩個詞在一段上下文的相關度,而GloVe考慮了兩個詞向量在全文中的相關度。這裡首先介紹兩種算法:
- 基于奇異值分解(SVD)的LSA算法,該方法對term-document矩陣(矩陣的每個元素為tf-idf)進行奇異值分解,進而得到term的向量表示和document的向量表示。此處使用的tf-idf主要還是term的全局統計特征。
- 另一個方法是word2vec算法,該算法可以分為skip-gram 和 continuous bag-of-words(CBOW)兩類,但都是基于局部滑動視窗計算的。即,該方法利用了局部的上下文特征(local context)。
LSA和word2vec作為兩大類方法的代表,一個是利用了全局特征的矩陣分解方法,一個是利用局部上下文的方法。GloVe 模型就是将這兩中特征合并到一起的,即使用了語料庫的全局統計(overall statistics)特征,也使用了局部的上下文特征(即滑動視窗)。為了做到這一點GloVe模型引入了Co-occurrence Probabilities Matrix。
共現矩陣(Co-occurrence Probabilities Matrix)
訓練GloVe模型前,首先需要建構一個共現矩陣,設詞表大小為V,共現矩陣将是一個V行V列的方陣,而第 i i i 行第 j j j 列的表示了以第 i i i 個中心詞 v i v_i vi ,第 j j j 個背景詞 v j v_j vj 出現的次數。
假設我們有上下文:
a n a p p l e a d a y k e e p s a n a p p l e a d a y \mathrm an\ apple\ a\ day\ keeps\ an\ apple\ a\ day an apple a day keeps an apple a day
我們設定滑窗大小m等于2,我們将會有如下中心詞-背景詞對:
然後周遊中心詞-背景詞對,更新共現矩陣,以上圖為例,最後共現矩陣的結果将有如下形式:
共現矩陣揭示了某種規律,定義共現矩陣的第i行的和為:
X i = ∑ j = 1 V X i , j X_i = \sum_{j=1}^V X_{i,j} Xi=j=1∑VXi,j
之後我們有條件機率,即第j列對應的詞出現在第i行上下文中的條件機率:
P i , j = X i , j X i P_{i,j} = \frac {X_{i,j}}{X_i} Pi,j=XiXi,j
而對于某個詞 v k v_k vk ,他在第 i i i 行或者第 j j j 行上下文出現的條件機率的比值:
P i , k P j , k \frac {P_{i,k}} {P_{j,k}} Pj,kPi,k
這個值是可以直接觀測并計算到的,并将會有如下規律:
- 如果 v j v_j vj 與 v k v_k vk 相關,且 v i v_i vi 與 v k v_k vk 相關,那麼這個比值将會趨近于1
- 如果 v j v_j vj 與 v k v_k vk 相關,且 v i v_i vi 與 v k v_k vk 不相關,那麼這個比值将會很小
- 如果 v j v_j vj 與 v k v_k vk 不相關,且 v i v_i vi 與 v k v_k vk 相關,那麼這個比值将會很大
- 如果 v j v_j vj 與 v k v_k vk 不相關,且 v i v_i vi 與 v k v_k vk 不相關,那麼這個比值将會趨近于1
損失函數
我們希望設計一個損失函數,希望對詞表内每兩個詞對, v i v_i vi 與 v j v_j vj ,盡可能與 v k v_k vk 在共現矩陣中對于第 i i i , j j j 上下文中,出現的條件機率比值相近:
P i , k P j , k = e x p ( v i T v k ) e x p ( v j T v k \frac {P_{i,k}} {P_{j,k}} = \frac {exp(v_i^Tv_k)} {exp(v_j^Tv_k} Pj,kPi,k=exp(vjTvkexp(viTvk)
兩邊取對數,對于分子分母:
l o g X i , k X i = l o g ( X i , k ) − l o g ( X i ) = v i T v k log\frac {X_{i,k}}{X_i} = log(X_{i,k}) - log(X_i) = v_i^Tv_k logXiXi,k=log(Xi,k)−log(Xi)=viTvk
l o g X j , k X j = l o g ( X j , k ) − l o g ( X j ) = v j T v k log\frac {X_{j,k}}{X_j} = log(X_{j,k}) - log(X_j) = v_j^Tv_k logXjXj,k=log(Xj,k)−log(Xj)=vjTvk
可以看到問題得到了簡化,我們希望左式的分子盡可能等于右式的分子,分母亦然,則問題被簡化為:對于詞表内任意一組詞對 i i i , j j j ,我們希望最小化下式:
J = ∑ i = 1 V ∑ J = 1 V ( v i T v j − l o g ( X i ) − l o g ( X i , j ) ) 2 J = \sum_{i=1}^V \sum_{J=1}^V (v_i^Tv_j-log(X_i)-log(X_{i,j}))^2 J=i=1∑VJ=1∑V(viTvj−log(Xi)−log(Xi,j))2
其中偏置項 b i b_i bi , b j b_j bj 将會替換 log X i \log X_i logXi ;
但是并不是每一個詞對都是平權的,需要考慮詞頻來設定每一個詞對的權重:
f ( X i , j ) = { ( X i , j / C ) 0.75 X > C 1 X < 0 f(X_{i, j}) = \begin{cases} (X_{i, j} \ /\ C)^{0.75}& \text{ X > C } \\ 1& \text{ X < 0} \end{cases} f(Xi,j)={(Xi,j / C)0.751 X > C X < 0
是以我們希望最小化:
∑ i = 1 V ∑ j = 1 V f ( X i , j ) ( v i T v j + b i + b j − l o g ( X i , j ) ) 2 \sum^{V}_{i=1} \sum^{V}_{j=1} f(X_{i, j}) (v^T_i v_j + b_i + b_j-log(X_{i, j}) )^2 i=1∑Vj=1∑Vf(Xi,j)(viTvj+bi+bj−log(Xi,j))2
最後使用中心詞向量 v j v_j vj 與背景詞向量 v i v_i vi 的和作為中心詞向量的表示。
對比word2vec
1、兩個模型在并行化上有一些不同,即GloVe更容易并行化,是以對于較大的訓練資料,GloVe更快,word2vec速度與資料集規模密切相關。
2、在英文上,glove for GloVe 和 gensim for word2vec是常用的訓練詞向量的python package,完全可以使用自己的訓練語料訓練詞向量。當然,他們都提供了google news(英文)上訓練好的詞向量,大家完全可以下載下傳下來,直接使用。對于中文的訓練語料,可以使用sogou中文新聞語料。