詞向量word2vec(圖學習參考資料)
介紹詞向量word2evc概念,及CBOW和Skip-gram的演算法實現。
項目鏈接: //aistudio.baidu.com/aistudio/projectdetail/5009409
在自然語言處理任務中,詞向量(Word Embedding)是表示自然語言里單詞的一種方法,即把每個詞都表示為一個N維空間內的點,即一個高維空間內的向量。通過這種方法,實現把自然語言計算轉換為向量計算。
如 圖1 所示的詞向量計算任務中,先把每個詞(如queen,king等)轉換成一個高維空間的向量,這些向量在一定意義上可以代表這個詞的語義資訊。再通過計算這些向量之間的距離,就可以計算出詞語之間的關聯關係,從而達到讓電腦像計算數值一樣去計算自然語言的目的。
因此,大部分詞向量模型都需要回答兩個問題:
- 如何把詞轉換為向量?
自然語言單詞是離散訊號,比如「香蕉」,「橘子」,「水果」在我們看來就是3個離散的詞。
如何把每個離散的單詞轉換為一個向量?
- 如何讓向量具有語義資訊?
比如,我們知道在很多情況下,「香蕉」和「橘子」更加相似,而「香蕉」和「句子」就沒有那麼相似,同時「香蕉」和「食物」、「水果」的相似程度可能介於「橘子」和「句子」之間。
那麼,我們該如何讓詞向量具備這樣的語義資訊?
1.如何把詞轉換為向量
自然語言單詞是離散訊號,比如「我」、「 愛」、「人工智慧」。如何把每個離散的單詞轉換為一個向量?通常情況下,我們可以維護一個如 圖2 所示的查詢表。表中每一行都存儲了一個特定詞語的向量值,每一列的第一個元素都代表著這個詞本身,以便於我們進行詞和向量的映射(如「我」對應的向量值為 [0.3,0.5,0.7,0.9,-0.2,0.03] )。給定任何一個或者一組單詞,我們都可以通過查詢這個excel,實現把單詞轉換為向量的目的,這個查詢和替換過程稱之為Embedding Lookup。
上述過程也可以使用一個字典數據結構實現。事實上如果不考慮計算效率,使用字典實現上述功能是個不錯的選擇。然而在進行神經網路計算的過程中,需要大量的算力,常常要藉助特定硬體(如GPU)滿足訓練速度的需求。GPU上所支援的計算都是以張量(Tensor)為單位展開的,因此在實際場景中,我們需要把Embedding Lookup的過程轉換為張量計算,如 圖3 所示。
假設對於句子”我,愛,人工,智慧”,把Embedding Lookup的過程轉換為張量計算的流程如下:
-
通過查詢字典,先把句子中的單詞轉換成一個ID(通常是一個大於等於0的整數),這個單詞到ID的映射關係可以根據需求自定義(如圖3中,我=>1, 人工=>2,愛=>3,…)。
-
得到ID後,再把每個ID轉換成一個固定長度的向量。假設字典的詞表中有5000個詞,那麼,對於單詞「我」,就可以用一個5000維的向量來表示。由於「我」的ID是1,因此這個向量的第一個元素是1,其他元素都是0([1,0,0,…,0]);同樣對於單詞「人工」,第二個元素是1,其他元素都是0。用這種方式就實現了用一個向量表示一個單詞。由於每個單詞的向量表示都只有一個元素為1,而其他元素為0,因此我們稱上述過程為One-Hot Encoding。
-
經過One-Hot Encoding後,句子「我,愛,人工,智慧」就被轉換成為了一個形狀為 4×5000的張量,記為$V$。在這個張量里共有4行、5000列,從上到下,每一行分別代表了「我」、「愛」、「人工」、「智慧」四個單詞的One-Hot Encoding。最後,我們把這個張量$V$和另外一個稠密張量$W$相乘,其中$W$張量的形狀為5000 × 128(5000表示詞表大小,128表示每個詞的向量大小)。經過張量乘法,我們就得到了一個4×128的張量,從而完成了把單詞表示成向量的目的。
2.如何讓向量具有語義資訊
得到每個單詞的向量表示後,我們需要思考下一個問題:比如在多數情況下,「香蕉」和「橘子」更加相似,而「香蕉」和「句子」就沒有那麼相似;同時,「香蕉」和「食物」、「水果」的相似程度可能介於「橘子」和「句子」之間。那麼如何讓存儲的詞向量具備這樣的語義資訊呢?
我們先學習自然語言處理領域的一個小技巧。在自然語言處理研究中,科研人員通常有一個共識:使用一個單詞的上下文來了解這個單詞的語義,比如:
「蘋果手機品質不錯,就是價格有點貴。」
「這個蘋果很好吃,非常脆。」
「菠蘿品質也還行,但是不如蘋果支援的APP多。」
在上面的句子中,我們通過上下文可以推斷出第一個「蘋果」指的是蘋果手機,第二個「蘋果」指的是水果蘋果,而第三個「菠蘿」指的應該也是一個手機。事實上,在自然語言處理領域,使用上下文描述一個詞語或者元素的語義是一個常見且有效的做法。我們可以使用同樣的方式訓練詞向量,讓這些詞向量具備表示語義資訊的能力。
2013年,Mikolov提出的經典word2vec演算法就是通過上下文來學習語義資訊。word2vec包含兩個經典模型:CBOW(Continuous Bag-of-Words)和Skip-gram,如 圖4 所示。
- CBOW:通過上下文的詞向量推理中心詞。
- Skip-gram:根據中心詞推理上下文。
假設有一個句子「Pineapples are spiked and yellow」,兩個模型的推理方式如下:
-
在CBOW中,先在句子中選定一個中心詞,並把其它詞作為這個中心詞的上下文。如 圖4 CBOW所示,把「spiked」作為中心詞,把「Pineapples、are、and、yellow」作為中心詞的上下文。在學習過程中,使用上下文的詞向量推理中心詞,這樣中心詞的語義就被傳遞到上下文的詞向量中,如「spiked → pineapple」,從而達到學習語義資訊的目的。
-
在Skip-gram中,同樣先選定一個中心詞,並把其他詞作為這個中心詞的上下文。如 圖4 Skip-gram所示,把「spiked」作為中心詞,把「Pineapples、are、and、yellow」作為中心詞的上下文。不同的是,在學習過程中,使用中心詞的詞向量去推理上下文,這樣上下文定義的語義被傳入中心詞的表示中,如「pineapple → spiked」,
從而達到學習語義資訊的目的。
說明:
一般來說,CBOW比Skip-gram訓練速度快,訓練過程更加穩定,原因是CBOW使用上下文average的方式進行訓練,每個訓練step會見到更多樣本。而在生僻字(出現頻率低的字)處理上,skip-gram比CBOW效果更好,原因是skip-gram不會刻意迴避生僻字。
2.1 CBOW和Skip-gram的演算法實現
我們以這句話:「Pineapples are spiked and yellow」為例分別介紹CBOW和Skip-gram的演算法實現。
如 圖5 所示,CBOW是一個具有3層結構的神經網路,分別是:
- 輸入層: 一個形狀為C×V的one-hot張量,其中C代表上線文中詞的個數,通常是一個偶數,我們假設為4;V表示詞表大小,我們假設為5000,該張量的每一行都是一個上下文詞的one-hot向量表示,比如「Pineapples, are, and, yellow」。
- 隱藏層: 一個形狀為V×N的參數張量W1,一般稱為word-embedding,N表示每個詞的詞向量長度,我們假設為128。輸入張量和word embedding W1進行矩陣乘法,就會得到一個形狀為C×N的張量。綜合考慮上下文中所有詞的資訊去推理中心詞,因此將上下文中C個詞相加得一個1×N的向量,是整個上下文的一個隱含表示。
- 輸出層: 創建另一個形狀為N×V的參數張量,將隱藏層得到的1×N的向量乘以該N×V的參數張量,得到了一個形狀為1×V的向量。最終,1×V的向量代表了使用上下文去推理中心詞,每個候選詞的打分,再經過softmax函數的歸一化,即得到了對中心詞的推理概率:
$$𝑠𝑜𝑓𝑡𝑚𝑎𝑥({O_i})= \frac{exp({O_i})}{\sum_jexp({O_j})}$$
如 圖6 所示,Skip-gram是一個具有3層結構的神經網路,分別是:
- Input Layer(輸入層):接收一個one-hot張量 $V \in R^{1 \times \text{vocab_size}}$ 作為網路的輸入,裡面存儲著當前句子中心詞的one-hot表示。
- Hidden Layer(隱藏層):將張量$V$乘以一個word embedding張量$W_1 \in R^{\text{vocab_size} \times \text{embed_size}}$,並把結果作為隱藏層的輸出,得到一個形狀為$R^{1 \times \text{embed_size}}$的張量,裡面存儲著當前句子中心詞的詞向量。
- Output Layer(輸出層):將隱藏層的結果乘以另一個word embedding張量$W_2 \in R^{\text{embed_size} \times \text{vocab_size}}$,得到一個形狀為$R^{1 \times \text{vocab_size}}$的張量。這個張量經過softmax變換後,就得到了使用當前中心詞對上下文的預測結果。根據這個softmax的結果,我們就可以去訓練詞向量模型。
在實際操作中,使用一個滑動窗口(一般情況下,長度是奇數),從左到右開始掃描當前句子。每個掃描出來的片段被當成一個小句子,每個小句子中間的詞被認為是中心詞,其餘的詞被認為是這個中心詞的上下文。
2.1.1 Skip-gram的理想實現
使用神經網路實現Skip-gram中,模型接收的輸入應該有2個不同的tensor:
-
代表中心詞的tensor:假設我們稱之為center_words $V$,一般來說,這個tensor是一個形狀為[batch_size, vocab_size]的one-hot tensor,表示在一個mini-batch中,每個中心詞的ID,對應位置為1,其餘為0。
-
代表目標詞的tensor:目標詞是指需要推理出來的上下文詞,假設我們稱之為target_words $T$,一般來說,這個tensor是一個形狀為[batch_size, 1]的整型tensor,這個tensor中的每個元素是一個[0, vocab_size-1]的值,代表目標詞的ID。
在理想情況下,我們可以使用一個簡單的方式實現skip-gram。即把需要推理的每個目標詞都當成一個標籤,把skip-gram當成一個大規模分類任務進行網路構建,過程如下:
- 聲明一個形狀為[vocab_size, embedding_size]的張量,作為需要學習的詞向量,記為$W_0$。對於給定的輸入$V$,使用向量乘法,將$V$乘以$W_0$,這樣就得到了一個形狀為[batch_size, embedding_size]的張量,記為$H=V×W_0$。這個張量$H$就可以看成是經過詞向量查表後的結果。
- 聲明另外一個需要學習的參數$W_1$,這個參數的形狀為[embedding_size, vocab_size]。將上一步得到的$H$去乘以$W_1$,得到一個新的tensor $O=H×W_1$,此時的$O$是一個形狀為[batch_size, vocab_size]的tensor,表示當前這個mini-batch中的每個中心詞預測出的目標詞的概率。
- 使用softmax函數對mini-batch中每個中心詞的預測結果做歸一化,即可完成網路構建。
2.1.2 Skip-gram的實際實現
然而在實際情況中,vocab_size通常很大(幾十萬甚至幾百萬),導致$W_0$和$W_1$也會非常大。對於$W_0$而言,所參與的矩陣運算並不是通過一個矩陣乘法實現,而是通過指定ID,對參數$W_0$進行訪存的方式獲取。然而對$W_1$而言,仍要處理一個非常大的矩陣運算(計算過程非常緩慢,需要消耗大量的記憶體/顯示記憶體)。為了緩解這個問題,通常採取負取樣(negative_sampling)的方式來近似模擬多分類任務。此時新定義的$W_0$和$W_1$均為形狀為[vocab_size, embedding_size]的張量。
假設有一個中心詞$c$和一個上下文詞正樣本$t_p$。在Skip-gram的理想實現里,需要最大化使用$c$推理$t_p$的概率。在使用softmax學習時,需要最大化$t_p$的推理概率,同時最小化其他詞表中詞的推理概率。之所以計算緩慢,是因為需要對詞表中的所有詞都計算一遍。然而我們還可以使用另一種方法,就是隨機從詞表中選擇幾個代表詞,通過最小化這幾個代表詞的概率,去近似最小化整體的預測概率。比如,先指定一個中心詞(如「人工」)和一個目標詞正樣本(如「智慧」),再隨機在詞表中取樣幾個目標詞負樣本(如「日本」,「喝茶」等)。有了這些內容,我們的skip-gram模型就變成了一個二分類任務。對於目標詞正樣本,我們需要最大化它的預測概率;對於目標詞負樣本,我們需要最小化它的預測概率。通過這種方式,我們就可以完成計算加速。上述做法,我們稱之為負取樣。
在實現的過程中,通常會讓模型接收3個tensor輸入:
-
代表中心詞的tensor:假設我們稱之為center_words $V$,一般來說,這個tensor是一個形狀為[batch_size, vocab_size]的one-hot tensor,表示在一個mini-batch中每個中心詞具體的ID。
-
代表目標詞的tensor:假設我們稱之為target_words $T$,一般來說,這個tensor同樣是一個形狀為[batch_size, vocab_size]的one-hot tensor,表示在一個mini-batch中每個目標詞具體的ID。
-
代表目標詞標籤的tensor:假設我們稱之為labels $L$,一般來說,這個tensor是一個形狀為[batch_size, 1]的tensor,每個元素不是0就是1(0:負樣本,1:正樣本)。
模型訓練過程如下:
- 用$V$去查詢$W_0$,用$T$去查詢$W_1$,分別得到兩個形狀為[batch_size, embedding_size]的tensor,記為$H_1$和$H_2$。
- 點乘這兩個tensor,最終得到一個形狀為[batch_size]的tensor $O = [O_i = \sum_j H_0[i,j] × H_1[i,j]]_{i=1}^{batch_size}$。
- 使用sigmoid函數作用在$O$上,將上述點乘的結果歸一化為一個0-1的概率值,作為預測概率,根據標籤資訊$L$訓練這個模型即可。
在結束模型訓練之後,一般使用$W_0$作為最終要使用的詞向量,可以用$W_0$提供的向量表示。通過向量點乘的方式,計算兩個不同詞之間的相似度。
3. 實現Skip-gram
接下來我們將學習使用飛槳實現Skip-gram模型的方法。在飛槳中,不同深度學習模型的訓練過程基本一致,流程如下:
-
數據處理:選擇需要使用的數據,並做好必要的預處理工作。
-
網路定義:使用飛槳定義好網路結構,包括輸入層,中間層,輸出層,損失函數和優化演算法。
-
網路訓練:將準備好的數據送入神經網路進行學習,並觀察學習的過程是否正常,如損失函數值是否在降低,也可以列印一些中間步驟的結果出來等。
-
網路評估:使用測試集合測試訓練好的神經網路,看看訓練效果如何。
在數據處理前,需要先載入飛槳平台(如果用戶在本地使用,請確保已經安裝飛槳)。
import os
import sys
import requests
from collections import OrderedDict
import math
import random
import numpy as np
import paddle
import paddle.fluid as fluid
from paddle.fluid.dygraph.nn import Embedding
3.1數據處理
首先,找到一個合適的語料用於訓練word2vec模型。使用text8數據集,這個數據集里包含了大量從維基百科收集到的英文語料,我們可以通過如下程式碼下載數據集,下載後的文件被保存在當前目錄的「text8.txt」文件內。
def download():
#可以從百度雲伺服器下載一些開源數據集(dataset.bj.bcebos.com)
corpus_url = "//dataset.bj.bcebos.com/word2vec/text8.txt"
#使用python的requests包下載數據集到本地
web_request = requests.get(corpus_url)
corpus = web_request.content
#把下載後的文件存儲在當前目錄的text8.txt文件內
with open("./text8.txt", "wb") as f:
f.write(corpus)
f.close()
download()
接下來,把下載的語料讀取到程式里,並列印前500個字元查看語料的格式,程式碼如下:
def load_text8():
with open("./text8.txt", "r") as f:
corpus = f.read().strip("\n")
f.close()
return corpus
corpus = load_text8()
#列印前500個字元,簡要看一下這個語料的樣子
print(corpus[:500])
def data_preprocess(corpus):
#由於英文單詞出現在句首的時候經常要大寫,所以我們把所有英文字元都轉換為小寫,
#以便對語料進行歸一化處理(Apple vs apple等)
corpus = corpus.strip().lower()
corpus = corpus.split(" ")
return corpus
corpus = data_preprocess(corpus)
print(corpus[:50])
在經過切詞後,需要對語料進行統計,為每個詞構造ID。一般來說,可以根據每個詞在語料中出現的頻次構造ID,頻次越高,ID越小,便於對詞典進行管理。程式碼如下:
def build_dict(corpus):
#首先統計每個不同詞的頻率(出現的次數),使用一個詞典記錄
word_freq_dict = dict()
for word in corpus:
if word not in word_freq_dict:
word_freq_dict[word] = 0
word_freq_dict[word] += 1
#將這個詞典中的詞,按照出現次數排序,出現次數越高,排序越靠前
#一般來說,出現頻率高的高頻詞往往是:I,the,you這種代詞,而出現頻率低的詞,往往是一些名詞,如:nlp
word_freq_dict = sorted(word_freq_dict.items(), key = lambda x:x[1], reverse = True)
#構造3個不同的詞典,分別存儲,
#每個詞到id的映射關係:word2id_dict
#每個id出現的頻率:word2id_freq
#每個id到詞的映射關係:id2word_dict
word2id_dict = dict()
word2id_freq = dict()
id2word_dict = dict()
#按照頻率,從高到低,開始遍歷每個單詞,並為這個單詞構造一個獨一無二的id
for word, freq in word_freq_dict:
curr_id = len(word2id_dict)
word2id_dict[word] = curr_id
word2id_freq[word2id_dict[word]] = freq
id2word_dict[curr_id] = word
return word2id_freq, word2id_dict, id2word_dict
word2id_freq, word2id_dict, id2word_dict = build_dict(corpus)
vocab_size = len(word2id_freq)
print("there are totoally %d different words in the corpus" % vocab_size)
for _, (word, word_id) in zip(range(50), word2id_dict.items()):
print("word %s, its id %d, its word freq %d" % (word, word_id, word2id_freq[word_id]))
得到word2id詞典後,還需要進一步處理原始語料,把每個詞替換成對應的ID,便於神經網路進行處理,程式碼如下:
def convert_corpus_to_id(corpus, word2id_dict):
#使用一個循環,將語料中的每個詞替換成對應的id,以便於神經網路進行處理
corpus = [word2id_dict[word] for word in corpus]
return corpus
corpus = convert_corpus_to_id(corpus, word2id_dict)
print("%d tokens in the corpus" % len(corpus))
print(corpus[:50])
接下來,需要使用二次取樣法處理原始文本。二次取樣法的主要思想是降低高頻詞在語料中出現的頻次。方法是將隨機將高頻的詞拋棄,頻率越高,被拋棄的概率就越大;頻率越低,被拋棄的概率就越小。標點符號或冠詞這樣的高頻詞就會被拋棄,從而優化整個詞表的詞向量訓練效果,程式碼如下:
def subsampling(corpus, word2id_freq):
#這個discard函數決定了一個詞會不會被替換,這個函數是具有隨機性的,每次調用結果不同
#如果一個詞的頻率很大,那麼它被遺棄的概率就很大
def discard(word_id):
return random.uniform(0, 1) < 1 - math.sqrt(
1e-4 / word2id_freq[word_id] * len(corpus))
corpus = [word for word in corpus if not discard(word)]
return corpus
corpus = subsampling(corpus, word2id_freq)
print("%d tokens in the corpus" % len(corpus))
print(corpus[:50])
在完成語料數據預處理之後,需要構造訓練數據。根據上面的描述,我們需要使用一個滑動窗口對語料從左到右掃描,在每個窗口內,中心詞需要預測它的上下文,並形成訓練數據。
在實際操作中,由於詞表往往很大(50000,100000等),對大詞表的一些矩陣運算(如softmax)需要消耗巨大的資源,因此可以通過負取樣的方式模擬softmax的結果。
- 給定一個中心詞和一個需要預測的上下文詞,把這個上下文詞作為正樣本。
- 通過詞表隨機取樣的方式,選擇若干個負樣本。
- 把一個大規模分類問題轉化為一個2分類問題,通過這種方式優化計算速度。
#max_window_size代表了最大的window_size的大小,程式會根據max_window_size從左到右掃描整個語料
#negative_sample_num代表了對於每個正樣本,我們需要隨機取樣多少負樣本用於訓練,
#一般來說,negative_sample_num的值越大,訓練效果越穩定,但是訓練速度越慢。
def build_data(corpus, word2id_dict, word2id_freq, max_window_size = 3, negative_sample_num = 4):
#使用一個list存儲處理好的數據
dataset = []
#從左到右,開始枚舉每個中心點的位置
for center_word_idx in range(len(corpus)):
#以max_window_size為上限,隨機取樣一個window_size,這樣會使得訓練更加穩定
window_size = random.randint(1, max_window_size)
#當前的中心詞就是center_word_idx所指向的詞
center_word = corpus[center_word_idx]
#以當前中心詞為中心,左右兩側在window_size內的詞都可以看成是正樣本
positive_word_range = (max(0, center_word_idx - window_size), min(len(corpus) - 1, center_word_idx + window_size))
positive_word_candidates = [corpus[idx] for idx in range(positive_word_range[0], positive_word_range[1]+1) if idx != center_word_idx]
#對於每個正樣本來說,隨機取樣negative_sample_num個負樣本,用於訓練
for positive_word in positive_word_candidates:
#首先把(中心詞,正樣本,label=1)的三元組數據放入dataset中,
#這裡label=1表示這個樣本是個正樣本
dataset.append((center_word, positive_word, 1))
#開始負取樣
i = 0
while i < negative_sample_num:
negative_word_candidate = random.randint(0, vocab_size-1)
if negative_word_candidate not in positive_word_candidates:
#把(中心詞,正樣本,label=0)的三元組數據放入dataset中,
#這裡label=0表示這個樣本是個負樣本
dataset.append((center_word, negative_word_candidate, 0))
i += 1
return dataset
dataset = build_data(corpus, word2id_dict, word2id_freq)
for _, (center_word, target_word, label) in zip(range(50), dataset):
print("center_word %s, target %s, label %d" % (id2word_dict[center_word],
id2word_dict[target_word], label))
訓練數據準備好後,把訓練數據都組裝成mini-batch,並準備輸入到網路中進行訓練,程式碼如下:
#我們將不同類型的數據放到不同的tensor里,便於神經網路進行處理
#並通過numpy的array函數,構造出不同的tensor來,並把這些tensor送入神經網路中進行訓練
def build_batch(dataset, batch_size, epoch_num):
#center_word_batch快取batch_size個中心詞
center_word_batch = []
#target_word_batch快取batch_size個目標詞(可以是正樣本或者負樣本)
target_word_batch = []
#label_batch快取了batch_size個0或1的標籤,用於模型訓練
label_batch = []
for epoch in range(epoch_num):
#每次開啟一個新epoch之前,都對數據進行一次隨機打亂,提高訓練效果
random.shuffle(dataset)
for center_word, target_word, label in dataset:
#遍歷dataset中的每個樣本,並將這些數據送到不同的tensor里
center_word_batch.append([center_word])
target_word_batch.append([target_word])
label_batch.append(label)
#當樣本積攢到一個batch_size後,我們把數據都返回回來
#在這裡我們使用numpy的array函數把list封裝成tensor
#並使用python的迭代器機制,將數據yield出來
#使用迭代器的好處是可以節省記憶體
if len(center_word_batch) == batch_size:
yield np.array(center_word_batch).astype("int64"), \
np.array(target_word_batch).astype("int64"), \
np.array(label_batch).astype("float32")
center_word_batch = []
target_word_batch = []
label_batch = []
if len(center_word_batch) > 0:
yield np.array(center_word_batch).astype("int64"), \
np.array(target_word_batch).astype("int64"), \
np.array(label_batch).astype("float32")
for _, batch in zip(range(10), build_batch(dataset, 128, 3)):
print(batch)
3.2網路定義
定義skip-gram的網路結構,用於模型訓練。在飛槳動態圖中,對於任意網路,都需要定義一個繼承自fluid.dygraph.Layer
的類來搭建網路結構、參數等數據的聲明。同時需要在forward
函數中定義網路的計算邏輯。值得注意的是,我們僅需要定義網路的前向計算邏輯,飛槳會自動完成神經網路的後向計算,程式碼如下:
#這裡我們使用的是paddlepaddle的1.8.0版本
#一般來說,在使用fluid訓練的時候,我們需要通過一個類來定義網路結構,這個類繼承了fluid.dygraph.Layer
class SkipGram(fluid.dygraph.Layer):
def __init__(self, vocab_size, embedding_size, init_scale=0.1):
#vocab_size定義了這個skipgram這個模型的詞表大小
#embedding_size定義了詞向量的維度是多少
#init_scale定義了詞向量初始化的範圍,一般來說,比較小的初始化範圍有助於模型訓練
super(SkipGram, self).__init__()
self.vocab_size = vocab_size
self.embedding_size = embedding_size
#使用paddle.fluid.dygraph提供的Embedding函數,構造一個詞向量參數
#這個參數的大小為:[self.vocab_size, self.embedding_size]
#數據類型為:float32
#這個參數的名稱為:embedding_para
#這個參數的初始化方式為在[-init_scale, init_scale]區間進行均勻取樣
self.embedding = Embedding(
size=[self.vocab_size, self.embedding_size],
dtype='float32',
param_attr=fluid.ParamAttr(
name='embedding_para',
initializer=fluid.initializer.UniformInitializer(
low=-0.5/embedding_size, high=0.5/embedding_size)))
#使用paddle.fluid.dygraph提供的Embedding函數,構造另外一個詞向量參數
#這個參數的大小為:[self.vocab_size, self.embedding_size]
#數據類型為:float32
#這個參數的名稱為:embedding_para_out
#這個參數的初始化方式為在[-init_scale, init_scale]區間進行均勻取樣
#跟上面不同的是,這個參數的名稱跟上面不同,因此,
#embedding_para_out和embedding_para雖然有相同的shape,但是權重不共享
self.embedding_out = Embedding(
size=[self.vocab_size, self.embedding_size],
dtype='float32',
param_attr=fluid.ParamAttr(
name='embedding_out_para',
initializer=fluid.initializer.UniformInitializer(
low=-0.5/embedding_size, high=0.5/embedding_size)))
#定義網路的前向計算邏輯
#center_words是一個tensor(mini-batch),表示中心詞
#target_words是一個tensor(mini-batch),表示目標詞
#label是一個tensor(mini-batch),表示這個詞是正樣本還是負樣本(用0或1表示)
#用於在訓練中計算這個tensor中對應詞的同義詞,用於觀察模型的訓練效果
def forward(self, center_words, target_words, label):
#首先,通過embedding_para(self.embedding)參數,將mini-batch中的詞轉換為詞向量
#這裡center_words和eval_words_emb查詢的是一個相同的參數
#而target_words_emb查詢的是另一個參數
center_words_emb = self.embedding(center_words)
target_words_emb = self.embedding_out(target_words)
#center_words_emb = [batch_size, embedding_size]
#target_words_emb = [batch_size, embedding_size]
#我們通過點乘的方式計算中心詞到目標詞的輸出概率,並通過sigmoid函數估計這個詞是正樣本還是負樣本的概率。
word_sim = fluid.layers.elementwise_mul(center_words_emb, target_words_emb)
word_sim = fluid.layers.reduce_sum(word_sim, dim = -1)
word_sim = fluid.layers.reshape(word_sim, shape=[-1])
pred = fluid.layers.sigmoid(word_sim)
#通過估計的輸出概率定義損失函數,注意我們使用的是sigmoid_cross_entropy_with_logits函數
#將sigmoid計算和cross entropy合併成一步計算可以更好的優化,所以輸入的是word_sim,而不是pred
loss = fluid.layers.sigmoid_cross_entropy_with_logits(word_sim, label)
loss = fluid.layers.reduce_mean(loss)
#返回前向計算的結果,飛槳會通過backward函數自動計算出反向結果。
return pred, loss
3.3網路訓練
完成網路定義後,就可以啟動模型訓練。我們定義每隔100步列印一次Loss,以確保當前的網路是正常收斂的。同時,我們每隔10000步觀察一下skip-gram計算出來的同義詞(使用 embedding的乘積),可視化網路訓練效果,程式碼如下:
batch_size = 512
epoch_num = 3
embedding_size = 200
step = 0
learning_rate = 0.001
#定義一個使用word-embedding查詢同義詞的函數
#這個函數query_token是要查詢的詞,k表示要返回多少個最相似的詞,embed是我們學習到的word-embedding參數
#我們通過計算不同詞之間的cosine距離,來衡量詞和詞的相似度
#具體實現如下,x代表要查詢詞的Embedding,Embedding參數矩陣W代表所有詞的Embedding
#兩者計算Cos得出所有詞對查詢詞的相似度得分向量,排序取top_k放入indices列表
def get_similar_tokens(query_token, k, embed):
W = embed.numpy()
x = W[word2id_dict[query_token]]
cos = np.dot(W, x) / np.sqrt(np.sum(W * W, axis=1) * np.sum(x * x) + 1e-9)
flat = cos.flatten()
indices = np.argpartition(flat, -k)[-k:]
indices = indices[np.argsort(-flat[indices])]
for i in indices:
print('for word %s, the similar word is %s' % (query_token, str(id2word_dict[i])))
#將模型放到GPU上訓練(fluid.CUDAPlace(0)),如果需要指定CPU,則需要改為fluid.CPUPlace()
with fluid.dygraph.guard(fluid.CUDAPlace(0)):
#通過我們定義的SkipGram類,來構造一個Skip-gram模型網路
skip_gram_model = SkipGram(vocab_size, embedding_size)
#構造訓練這個網路的優化器
adam = fluid.optimizer.AdamOptimizer(learning_rate=learning_rate, parameter_list = skip_gram_model.parameters())
#使用build_batch函數,以mini-batch為單位,遍歷訓練數據,並訓練網路
for center_words, target_words, label in build_batch(
dataset, batch_size, epoch_num):
#使用fluid.dygraph.to_variable函數,將一個numpy的tensor,轉換為飛槳可計算的tensor
center_words_var = fluid.dygraph.to_variable(center_words)
target_words_var = fluid.dygraph.to_variable(target_words)
label_var = fluid.dygraph.to_variable(label)
#將轉換後的tensor送入飛槳中,進行一次前向計算,並得到計算結果
pred, loss = skip_gram_model(
center_words_var, target_words_var, label_var)
#通過backward函數,讓程式自動完成反向計算
loss.backward()
#通過minimize函數,讓程式根據loss,完成一步對參數的優化更新
adam.minimize(loss)
#使用clear_gradients函數清空模型中的梯度,以便於下一個mini-batch進行更新
skip_gram_model.clear_gradients()
#每經過100個mini-batch,列印一次當前的loss,看看loss是否在穩定下降
step += 1
if step % 100 == 0:
print("step %d, loss %.3f" % (step, loss.numpy()[0]))
#經過10000個mini-batch,列印一次模型對eval_words中的10個詞計算的同義詞
#這裡我們使用詞和詞之間的向量點積作為衡量相似度的方法
#我們只列印了5個最相似的詞
if step % 10000 == 0:
get_similar_tokens('one', 5, skip_gram_model.embedding.weight)
get_similar_tokens('she', 5, skip_gram_model.embedding.weight)
get_similar_tokens('chip', 5, skip_gram_model.embedding.weight)
step 200, loss 0.693
step 300, loss 0.693
step 400, loss 0.693
step 500, loss 0.691
step 600, loss 0.688
step 700, loss 0.682
step 800, loss 0.665
step 900, loss 0.658
step 1000, loss 0.646
step 1100, loss 0.631
step 1200, loss 0.612
step 1300, loss 0.592
step 1400, loss 0.568
從列印結果可以看到,經過一定步驟的訓練,Loss逐漸下降並趨於穩定。同時也可以發現skip-gram模型可以學習到一些有趣的語言現象,比如:跟who比較接近的詞是"who, he, she, him, himself"。
3.4詞向量的有趣應用
在使用word2vec模型的過程中,研究人員發現了一些有趣的現象。比如得到整個詞表的word embedding之後,對任意詞都可以基於向量乘法計算出跟這個詞最接近的詞。我們會發現,word2vec模型可以自動學習出一些同義詞關係,如:
Top 5 words closest to "beijing" are:
1. newyork
2. paris
3. tokyo
4. berlin
5. seoul
...
Top 5 words closest to "apple" are:
1. banana
2. pineapple
3. huawei
4. peach
5. orange
除此以外,研究人員還發現可以使用加減法完成一些基於語言的邏輯推理,如:
Top 1 words closest to "king - man + woman" are
1. queen
...
Top 1 words closest to "captial - china + america" are
1. Washington