本文講解 skip-gram 模型以及優化和擴展。主要包括層次 Softmax、負采樣、學習短語的表示。
先提一下詞向量:
- 詞向量(也叫詞嵌入,word embedding),簡單地說就是用一個低維向量表示一個詞。由於獨熱編碼(one-hot encoding)存在維度災難,即稀疏性,且無法理解詞與詞之間的內在聯系,詞向量的出現就可解決這些問題,大大簡化了操作。
- 特點:
- 維度大小是固定值
- 維度值是實數,不限於0和1,可表示連續空間,可通過計算距離表示詞之間的相似性,還可類比
- 使用嵌入矩陣表示詞向量
介紹
- Mikolov等人在《Efficient estimation of word representations in vector space》中提出Skip-gram模型,是一種高效的可從大量無結構文本數據中學習高質量詞向量表示的方法。該模型的訓練不涉及密集的矩陣乘法,這使得模型訓練很快。
-
該論文提出了幾點優化擴展,比如對高頻率詞進行重采樣,可以提高訓練速度(大約 2倍 - 10倍),並且提高了低頻詞的向量表示。此外還提出了一種簡化的噪聲對比估計(Noise Contrastive Estimation, NCE),與之前使用層次 Softmax 相比,能夠更快的訓練和更好的表示頻繁單詞。
從基於單詞的模型擴展到基於短語的模型相對簡單,文中使用數據驅動的方法識別大量的短語,然后將短語作為單獨的標記來處理。為了評估短語向量的質量,作者開發了一套包含單詞和短語的類比推理任務測試集,效果如下:
vec(“Montreal Canadiens”) - vec(“Montreal”) + vec(“Toronto”) is vec(“Toronto Maple Leafs”)
- 還發現簡單的向量加法可得出有意思的結果,比如
vec(“Russia”) + vec(“river”) is close to vec(“Volga River”), and vec(“Germany”) + vec(“capital”) is close to vec(“Berlin”)
Skip-gram model
- Skip-gram 的訓練目標是給定中心詞,能夠預測該詞在一定半徑內可能出現的詞。
而Cbow模型是給定中心詞 的一定鄰域半徑(比如半徑為2,通常成為windows,窗口,這里窗口的大小為5)內的單詞
,預測輸出單詞為該中心詞
的概率,由於沒有考慮詞之間的順序,所以稱為詞袋模型。
- 層次softmax
本文使用霍夫曼二叉樹來表示輸出層,W 個詞分別作為葉子節點,每個節點都表示其子節點的相對概率。總詞匯中的每個詞都有一條從二叉樹根部到自身的路徑。用 n(w,j) 來表示從根節點到 w 詞這條路徑上的第 j 個節點,用 ch(n) 來表示 n 的任意一個子節點,設如果 x 為真則[x]=1[x]=1,否則[x]=−1[x]=−1。那么 Hierarchical Softmax 可以表示為:
好處:
1.霍夫曼二叉樹的節點離高頻詞距離更近,從而可以進行快速訓練。之前已經觀察到,通過頻率將單詞分組在一起,對於基於神經網絡的語言模型來說,是一種非常簡單的加速技術,效果很好。
2. 最多計算logW個節點
- 負采樣
Noise Contrastive Estimation (NCE)-噪聲對比估計,NCE表面,一個好的模型應該能夠通過邏輯回歸將數據與噪聲區分開。
因為 skip-gram 更關注於學習高質量的詞向量表達,所以可以在保證詞向量質量的前提下對 NCE 進行簡化。於是定義了 NEG(Negative Sampling):
實驗表明,在5-20范圍內k值對於小的訓練數據集是有用的,而對於大數據集,k可以小到2-5。NCE 和 NEG 的區別在於 NCE 在計算時需要樣本和噪音分布的數值概率,而 NEG 只需要樣本。
- 高頻詞的二次采樣
為了克服稀有詞和頻繁詞之間的不平衡,我們使用了一種簡單的次抽樣方法:訓練集中的每個單詞以公式計算的概率被丟棄。
其中,是單詞
的頻率,
是選擇的閾值,通常在
左右。選擇這個次抽樣公式,是因為它在保持頻率排序的同時,對頻率大於t的詞進行了積極的子采樣。同時發現它在實踐中效果很好。它加速了學習,甚至顯着地提高了r的學習向量的准確性。
- 結果:
通過比較,作者們發現,似乎最好的短語表示是通過一個層次 softmax 和 Subsampling 結合的模型來學習的。
- 學習短語
許多短語的含義並不是由單個單詞的含義組成的。要學習短語的向量表示,首先要找到經常出現在一起的單詞,並且組成一個 Token 作為一個詞處理。通過這種方式,我們可以構成許多合理的短語,而不會大大增加詞匯量;從理論上講,我們可以使用所有n-gram訓練Skip-gram模型,但這會占用大量內存。於是使用了一個基於 unigram, bigram 的數據驅動方法:
其中用作折扣系數,防止由非常不常用的單詞組成的短語過多。然后將分數超過所選閾值的作為短語使用。這是用來評價性能的工具。
代碼:來自https://github.com/graykode/nlp-tutorial/tree/master/1-2.Word2Vec
''' code by Tae Hwan Jung(Jeff Jung) @graykode ''' import numpy as np import torch import torch.nn as nn import torch.optim as optim from torch.autograd import Variable import matplotlib.pyplot as plt %matplotlib inline dtype = torch.FloatTensor # 定義一個多維張量torch # 3 Words Sentence sentences = [ "i like dog", "i like cat", "i like animal", "dog cat animal", "apple cat dog like", "dog fish milk like", "dog cat eyes like", "i like apple", "apple i hate", "apple i movie book music like", "cat dog hate", "cat dog like"] word_sequence = " ".join(sentences).split() word_list = " ".join(sentences).split() word_list = list(set(word_list)) #enumerate是一個枚舉的關鍵詞,i是鍵,w是值,這樣就將所有單詞按照自然數編成字典 word_dict = {w: i for i, w in enumerate(word_list)} # Word2Vec Parameter batch_size = 20 # To show 2 dim embedding graph embedding_size = 2 # To show 2 dim embedding graph voc_size = len(word_list) def random_batch(data, size): random_inputs = [] random_labels = [] # 隨機選取sample random_index = np.random.choice(range(len(data)), size, replace=False) for i in random_index: random_inputs.append(np.eye(voc_size)[data[i][0]]) # target random_labels.append(data[i][1]) # context word return random_inputs, random_labels # Make skip gram of one size window #構建輸入的samples skip_grams = [] for i in range(1, len(word_sequence) - 1): target = word_dict[word_sequence[i]] context = [word_dict[word_sequence[i - 1]], word_dict[word_sequence[i + 1]]] for w in context: skip_grams.append([target, w]) # Model class Word2Vec(nn.Module): def __init__(self): super(Word2Vec, self).__init__() # W and WT is not Traspose relationship # 初始化從輸入到隱藏層的嵌入矩陣,參數隨機初始化在(-1,1] self.W = nn.Parameter(-2 * torch.rand(voc_size, embedding_size) + 1).type(dtype) # voc_size > embedding_size Weight # 隨機初始化從隱藏層到輸出層的系數矩陣,參數隨機初始化在(-1,1] self.WT = nn.Parameter(-2 * torch.rand(embedding_size, voc_size) + 1).type(dtype) # embedding_size > voc_size Weight # 前向傳播 def forward(self, X): # X : [batch_size, voc_size] hidden_layer = torch.matmul(X, self.W) # hidden_layer : [batch_size, embedding_size] output_layer = torch.matmul(hidden_layer, self.WT) # output_layer : [batch_size, voc_size] return output_layer model = Word2Vec() # 定義損失函數和優化 criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=0.001) # Training for epoch in range(5000): input_batch, target_batch = random_batch(skip_grams, batch_size) # 被定義為Varialbe類型的變量可以認為是一種常量,在pytorch反向傳播過程中不對其求導 input_batch = Variable(torch.Tensor(input_batch)) target_batch = Variable(torch.LongTensor(target_batch)) optimizer.zero_grad() output = model(input_batch) # output : [batch_size, voc_size], target_batch : [batch_size] (LongTensor, not one-hot) loss = criterion(output, target_batch) if (epoch + 1)%1000 == 0: print('Epoch:', '%04d' % (epoch + 1), 'cost =', '{:.6f}'.format(loss)) loss.backward() optimizer.step() for i, label in enumerate(word_list): # model.parameters()是獲取模型的參數 W, WT = model.parameters() x, y = float(W[i][0]), float(W[i][1]) plt.scatter(x, y) plt.annotate(label, xy=(x, y), xytext=(5, 2), textcoords='offset points', ha='right', va='bottom') plt.show()
結果: