Word2Vec
基本思想:通過訓(xùn)練將每一個(gè)詞映射成一個(gè)固定長度的向量,所有向量構(gòu)成一個(gè)詞向量空間,每一個(gè)向量(單詞)可以看作是向量空間中的一個(gè)點(diǎn),意思越相近的單詞距離越近。
如何把詞轉(zhuǎn)換為向量?
通常情況下,我們可以維護(hù)一個(gè)查詢表。表中每一行都存儲(chǔ)了一個(gè)特定詞語的向量值,每一列的第一個(gè)元素都代表著這個(gè)詞本身,以便于我們進(jìn)行詞和向量的映射(如“我”對(duì)應(yīng)的向量值為 [0.3,0.5,0.7,0.9,-0.2,0.03] )。給定任何一個(gè)或者一組單詞,我們都可以通過查詢這個(gè)excel,實(shí)現(xiàn)把單詞轉(zhuǎn)換為向量的目的,這個(gè)查詢和替換過程稱之為Embedding Lookup。
然而在進(jìn)行神經(jīng)網(wǎng)絡(luò)計(jì)算的過程中,需要大量的算力,常常要借助特定硬件(如GPU)滿足訓(xùn)練速度的需求。GPU上所支持的計(jì)算都是以張量(Tensor)為單位展開的,因此在實(shí)際場景中,我們需要把Embedding Lookup的過程轉(zhuǎn)換為張量計(jì)算:
如何讓向量具有語義信息?
在自然語言處理研究中,科研人員通常有一個(gè)共識(shí):使用一個(gè)單詞的上下文來了解這個(gè)單詞的語義,比如:
“蘋果手機(jī)質(zhì)量不錯(cuò),就是價(jià)格有點(diǎn)貴。”
“這個(gè)蘋果很好吃,非常脆?!?“菠蘿質(zhì)量也還行,但是不如蘋果支持的APP多?!?
在上面的句子中,我們通過上下文可以推斷出第一個(gè)“蘋果”指的是蘋果手機(jī),第二個(gè)“蘋果”指的是水果蘋果,而第三個(gè)“菠蘿”指的應(yīng)該也是一個(gè)手機(jī)。在自然語言處理領(lǐng)域,使用上下文描述一個(gè)詞語或者元素的語義是一個(gè)常見且有效的做法。我們可以使用同樣的方式訓(xùn)練詞向量,讓這些詞向量具備表示語義信息的能力。
2013年,Mikolov提出的經(jīng)典word2vec算法就是通過上下文來學(xué)習(xí)語義信息。word2vec包含兩個(gè)經(jīng)典模型:CBOW(Continuous Bag-of-Words)和Skip-gram。
CBOW:通過上下文的詞向量推理中心詞。
Skip-gram:根據(jù)中心詞推理上下文。
CBOW模型
輸入層: 一個(gè)形狀為C×V的one-hot張量,其中C代表上下文中詞的個(gè)數(shù),通常是一個(gè)偶數(shù),我們假設(shè)為4;V表示詞表大小,我們假設(shè)為5000,該張量的每一行都是一個(gè)上下文詞的one-hot向量表示。
隱藏層: 一個(gè)形狀為V×N的參數(shù)張量W1,一般稱為word-embedding,N表示每個(gè)詞的詞向量長度,我們假設(shè)為128。輸入張量和word embedding W1進(jìn)行矩陣乘法,就會(huì)得到一個(gè)形狀為C×N的張量。綜合考慮上下文中所有詞的信息去推理中心詞,因此將上下文中C個(gè)詞相加得一個(gè)1×N的向量,是整個(gè)上下文的一個(gè)隱含表示。
輸出層: 創(chuàng)建另一個(gè)形狀為N×V的參數(shù)張量,將隱藏層得到的1×N的向量乘以該N×V的參數(shù)張量,得到了一個(gè)形狀為1×V的向量。最終,1×V的向量代表了使用上下文去推理中心詞,每個(gè)候選詞的打分,再經(jīng)過softmax函數(shù)的歸一化,即得到了對(duì)中心詞的推理概率:
Skip-gram模型
Input Layer(輸入層): 接收一個(gè)one-hot張量 V∈R1×vocab_size作為網(wǎng)絡(luò)的輸入,假設(shè)vocab_size為5000。
Hidden Layer(隱藏層): 將張量V乘以一個(gè)word embedding張量W1∈Rvocab_size×embed_size ,假設(shè)embed_size為128,并把結(jié)果作為隱藏層的輸出,得到一個(gè)形狀為R1×embed_size的張量,里面存儲(chǔ)著當(dāng)前句子中心詞的詞向量。
Output Layer(輸出層): 將隱藏層的結(jié)果乘以另一個(gè)word embedding張量W2∈Rembed_size×vocab_size,得到一個(gè)形狀為R1×vocab_size的張量。這個(gè)張量經(jīng)過softmax變換后,就得到了使用當(dāng)前中心詞對(duì)上下文的預(yù)測結(jié)果。根據(jù)這個(gè)softmax的結(jié)果,我們就可以去訓(xùn)練詞向量模型。
Skip-gram在實(shí)際操作中,使用一個(gè)滑動(dòng)窗口(一般情況下,長度是奇數(shù)),從左到右開始掃描當(dāng)前句子。每個(gè)掃描出來的片段被當(dāng)成一個(gè)小句子,每個(gè)小句子中間的詞被認(rèn)為是中心詞,其余的詞被認(rèn)為是這個(gè)中心詞的上下文。
Skip-gram實(shí)現(xiàn)
1.數(shù)據(jù)處理:
首先下載數(shù)據(jù)集處理語料,
# 下載語料用來訓(xùn)練word2vec
download()
# 讀取text8數(shù)據(jù)
corpus = load_text8()
# 對(duì)語料進(jìn)行預(yù)處理(分詞)并把所有英文字符都轉(zhuǎn)換為小寫
corpus = data_preprocess(corpus)
# 構(gòu)造詞典,統(tǒng)計(jì)每個(gè)詞的頻率,并根據(jù)頻率將每個(gè)詞轉(zhuǎn)換為一個(gè)整數(shù)id
word2id_freq, word2id_dict, id2word_dict = build_dict(corpus)
vocab_size = len(word2id_freq)
#保存word2id_freq, word2id_dict, id2word_dict到本地
f_save = open('word2id_freq.pkl', 'wb')
pickle.dump(word2id_freq, f_save)
f_save.close()
f_save = open('word2id_dict.pkl', 'wb')
pickle.dump(word2id_dict, f_save)
f_save.close()
f_save = open('id2word_dict.pkl', 'wb')
pickle.dump(id2word_dict, f_save)
f_save.close()
# 把語料轉(zhuǎn)換為id序列
corpus = convert_corpus_to_id(corpus, word2id_dict)
# 使用二次采樣算法(subsampling)處理語料,強(qiáng)化訓(xùn)練效果,遺棄頻率高的詞
corpus = subsampling(corpus, word2id_freq)
構(gòu)造數(shù)據(jù),假設(shè)有一個(gè)中心詞c和一個(gè)上下文詞正樣本tp。在Skip-gram的理想實(shí)現(xiàn)里,需要最大化使用c推理tp的概率。在使用softmax學(xué)習(xí)時(shí),需要最大化tp的推理概率,同時(shí)最小化其他詞表中詞的推理概率。之所以計(jì)算緩慢,是因?yàn)樾枰獙?duì)詞表中的所有詞都計(jì)算一遍。然而我們還可以使用另一種方法,就是隨機(jī)從詞表中選擇幾個(gè)代表詞,通過最小化這幾個(gè)代表詞的概率,去近似最小化整體的預(yù)測概率。比如,先指定一個(gè)中心詞(如“人工”)和一個(gè)目標(biāo)詞正樣本(如“智能”),再隨機(jī)在詞表中采樣幾個(gè)目標(biāo)詞負(fù)樣本(如“日本”,“喝茶”等)。對(duì)于目標(biāo)詞正樣本,我們需要最大化它的預(yù)測概率;對(duì)于目標(biāo)詞負(fù)樣本,我們需要最小化它的預(yù)測概率。通過這種方式,我們就可以完成計(jì)算加速。上述做法,我們稱之為負(fù)采樣。實(shí)現(xiàn)如下:
# 構(gòu)造數(shù)據(jù),準(zhǔn)備模型訓(xùn)練
# max_window_size代表了最大的window_size的大小,程序會(huì)根據(jù)max_window_size從左到右掃描整個(gè)語料
# negative_sample_num代表了對(duì)于每個(gè)正樣本,我們需要隨機(jī)采樣多少負(fù)樣本用于訓(xùn)練,
# 一般來說,negative_sample_num的值越大,訓(xùn)練效果越穩(wěn)定,但是訓(xùn)練速度越慢。
def build_data(corpus, word2id_dict, word2id_freq, max_window_size=3, negative_sample_num=4):
# 使用一個(gè)list存儲(chǔ)處理好的數(shù)據(jù)
dataset = []
# 從左到右,開始枚舉每個(gè)中心點(diǎn)的位置
for center_word_idx in range(len(corpus)):
# 以max_window_size為上限,隨機(jī)采樣一個(gè)window_size,這樣會(huì)使得訓(xùn)練更加穩(wěn)定
window_size = random.randint(1, max_window_size)
# 當(dāng)前的中心詞就是center_word_idx所指向的詞
center_word = corpus[center_word_idx]
# 以當(dāng)前中心詞為中心,左右兩側(cè)在window_size內(nèi)的詞都可以看成是正樣本
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]
# 對(duì)于每個(gè)正樣本來說,隨機(jī)采樣negative_sample_num個(gè)負(fù)樣本,用于訓(xùn)練
for positive_word in positive_word_candidates:
# 首先把(中心詞,正樣本,label=1)的三元組數(shù)據(jù)放入dataset中,
# 這里label=1表示這個(gè)樣本是個(gè)正樣本
dataset.append((center_word, positive_word, 1))
# 開始負(fù)采樣
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)的三元組數(shù)據(jù)放入dataset中,
# 這里label=0表示這個(gè)樣本是個(gè)負(fù)樣本
dataset.append((center_word, negative_word_candidate, 0))
i += 1
return dataset
2.網(wǎng)絡(luò)定義
import paddle
from paddle.nn import Embedding
import paddle.nn.functional as F
import paddle.nn as nn
#定義skip-gram訓(xùn)練網(wǎng)絡(luò)結(jié)構(gòu)
#使用paddlepaddle的2.0.0版本
#一般來說,在使用paddle訓(xùn)練的時(shí)候,我們需要通過一個(gè)類來定義網(wǎng)絡(luò)結(jié)構(gòu),這個(gè)類繼承了paddle.nn.layer
class SkipGram(nn.Layer):
def __init__(self, vocab_size, embedding_size, init_scale=0.1):
# vocab_size定義了這個(gè)skipgram這個(gè)模型的詞表大小
# embedding_size定義了詞向量的維度是多少
# init_scale定義了詞向量初始化的范圍,一般來說,比較小的初始化范圍有助于模型訓(xùn)練
super(SkipGram, self).__init__()
self.vocab_size = vocab_size
self.embedding_size = embedding_size
# 使用Embedding函數(shù)構(gòu)造一個(gè)詞向量參數(shù)
# 這個(gè)參數(shù)的大小為:[self.vocab_size, self.embedding_size]
# 數(shù)據(jù)類型為:float32
# 這個(gè)參數(shù)的初始化方式為在[-init_scale, init_scale]區(qū)間進(jìn)行均勻采樣
self.embedding = Embedding(
num_embeddings = self.vocab_size,
embedding_dim = self.embedding_size,
weight_attr=paddle.ParamAttr(
initializer=paddle.nn.initializer.Uniform(
low=-init_scale, high=init_scale)))
# 使用Embedding函數(shù)構(gòu)造另外一個(gè)詞向量參數(shù)
# 這個(gè)參數(shù)的大小為:[self.vocab_size, self.embedding_size]
# 這個(gè)參數(shù)的初始化方式為在[-init_scale, init_scale]區(qū)間進(jìn)行均勻采樣
self.embedding_out = Embedding(
num_embeddings = self.vocab_size,
embedding_dim = self.embedding_size,
weight_attr=paddle.ParamAttr(
initializer=paddle.nn.initializer.Uniform(
low=-init_scale, high=init_scale)))
# 定義網(wǎng)絡(luò)的前向計(jì)算邏輯
# center_words是一個(gè)tensor(mini-batch),表示中心詞
# target_words是一個(gè)tensor(mini-batch),表示目標(biāo)詞
# label是一個(gè)tensor(mini-batch),表示這個(gè)詞是正樣本還是負(fù)樣本(用0或1表示)
# 用于在訓(xùn)練中計(jì)算這個(gè)tensor中對(duì)應(yīng)詞的同義詞,用于觀察模型的訓(xùn)練效果
def forward(self, center_words, target_words, label):
# 首先,通過self.embedding參數(shù),將mini-batch中的詞轉(zhuǎn)換為詞向量
# 這里center_words和eval_words_emb查詢的是一個(gè)相同的參數(shù)
# 而target_words_emb查詢的是另一個(gè)參數(shù)
center_words_emb = self.embedding(center_words)
target_words_emb = self.embedding_out(target_words)
# 我們通過點(diǎn)乘的方式計(jì)算中心詞到目標(biāo)詞的輸出概率,并通過sigmoid函數(shù)估計(jì)這個(gè)詞是正樣本還是負(fù)樣本的概率。
word_sim = paddle.multiply(center_words_emb, target_words_emb)
word_sim = paddle.sum(word_sim, axis=-1)
word_sim = paddle.reshape(word_sim, shape=[-1])
pred = F.sigmoid(word_sim)
# 通過估計(jì)的輸出概率定義損失函數(shù),注意我們使用的是binary_cross_entropy_with_logits函數(shù)
# 將sigmoid計(jì)算和cross entropy合并成一步計(jì)算可以更好的優(yōu)化,所以輸入的是word_sim,而不是pred
loss = F.binary_cross_entropy_with_logits(word_sim, label)
loss = paddle.mean(loss)
# 返回前向計(jì)算的結(jié)果,飛槳會(huì)通過backward函數(shù)自動(dòng)計(jì)算出反向結(jié)果。
return pred, loss
3.網(wǎng)絡(luò)訓(xùn)練:
# 開始訓(xùn)練,定義一些訓(xùn)練過程中需要使用的超參數(shù)
batch_size = 512
epoch_num = 3
embedding_size = 200
step = 0
learning_rate = 0.001
# 定義一個(gè)使用word-embedding查詢同義詞的函數(shù)
# 這個(gè)函數(shù)query_token是要查詢的詞,k表示要返回多少個(gè)最相似的詞,embed是我們學(xué)習(xí)到的word-embedding參數(shù)
# 我們通過計(jì)算不同詞之間的cosine距離,來衡量詞和詞的相似度
# 具體實(shí)現(xiàn)如下,x代表要查詢?cè)~的Embedding,Embedding參數(shù)矩陣W代表所有詞的Embedding
# 兩者計(jì)算Cos得出所有詞對(duì)查詢?cè)~的相似度得分向量,排序取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])))
# 通過我們定義的SkipGram類,來構(gòu)造一個(gè)Skip-gram模型網(wǎng)絡(luò)
skip_gram_model = SkipGram(vocab_size, embedding_size)
# 構(gòu)造訓(xùn)練這個(gè)網(wǎng)絡(luò)的優(yōu)化器
adam = paddle.optimizer.Adam(learning_rate=learning_rate, parameters=skip_gram_model.parameters())
# 使用build_batch函數(shù),以mini-batch為單位,遍歷訓(xùn)練數(shù)據(jù),并訓(xùn)練網(wǎng)絡(luò)
for center_words, target_words, label in build_batch(
dataset, batch_size, epoch_num):
# 使用paddle.to_tensor,將一個(gè)numpy的tensor,轉(zhuǎn)換為飛槳可計(jì)算的tensor
center_words_var = paddle.to_tensor(center_words)
target_words_var = paddle.to_tensor(target_words)
label_var = paddle.to_tensor(label)
# 將轉(zhuǎn)換后的tensor送入飛槳中,進(jìn)行一次前向計(jì)算,并得到計(jì)算結(jié)果
pred, loss = skip_gram_model(
center_words_var, target_words_var, label_var)
# 程序自動(dòng)完成反向計(jì)算
loss.backward()
# 程序根據(jù)loss,完成一步對(duì)參數(shù)的優(yōu)化更新
adam.step()
# 清空模型中的梯度,以便于下一個(gè)mini-batch進(jìn)行更新
adam.clear_grad()
# 每經(jīng)過100個(gè)mini-batch,打印一次當(dāng)前的loss,看看loss是否在穩(wěn)定下降
step += 1
if step % 1000 == 0:
print("step %d, loss %.3f" % (step, loss.numpy()[0]))
# 每隔10000步,打印一次模型對(duì)以下查詢?cè)~的相似詞,這里我們使用詞和詞之間的向量點(diǎn)積作為衡量相似度的方法,只打印了5個(gè)最相似的詞并保存網(wǎng)絡(luò)模型參數(shù)和優(yōu)化器模型參數(shù)
if step % 10000 == 0:
get_similar_tokens('movie', 5, skip_gram_model.embedding.weight)
get_similar_tokens('one', 5, skip_gram_model.embedding.weight)
get_similar_tokens('chip', 5, skip_gram_model.embedding.weight)
paddle.save(skip_gram_model.state_dict(), "text8.pdparams")
paddle.save(adam.state_dict(), "adam.pdopt")
這里step到300000我就結(jié)束訓(xùn)練了,可以看到詞性已經(jīng)很接近了。
4.測試訓(xùn)練模型
訓(xùn)練完成后,對(duì)任意詞都可以基于我們訓(xùn)練出模型計(jì)算出跟這個(gè)詞最接近的詞。
# 定義一些訓(xùn)練過程中需要使用的超參數(shù)
embedding_size = 200
learning_rate = 0.001
# 通過我們定義的SkipGram類,來構(gòu)造一個(gè)Skip-gram模型網(wǎng)絡(luò)
skip_gram_model = SkipGram(vocab_size, embedding_size)
# 構(gòu)造訓(xùn)練這個(gè)網(wǎng)絡(luò)的優(yōu)化器
adam = paddle.optimizer.Adam(learning_rate=learning_rate, parameters=skip_gram_model.parameters())
# 加載網(wǎng)絡(luò)模型和優(yōu)化器模型
layer_state_dict = paddle.load("./my_model/text8.pdparams")
opt_state_dict = paddle.load("./my_model/adam.pdopt")
skip_gram_model.set_state_dict(layer_state_dict)
adam.set_state_dict(opt_state_dict)
# get_similar_tokens('movie', 5, skip_gram_model.embedding.weight)
# get_similar_tokens('one', 5, skip_gram_model.embedding.weight)
# get_similar_tokens('chip', 5, skip_gram_model.embedding.weight)
get_similar_tokens('she', 5, skip_gram_model.embedding.weight)
get_similar_tokens('dog', 5, skip_gram_model.embedding.weight)
get_similar_tokens('apple', 5, skip_gram_model.embedding.weight)
get_similar_tokens('beijing', 5, skip_gram_model.embedding.weight)
結(jié)果如下,我沒有完全訓(xùn)練完,可以看到效果還可以。
文章來源:http://www.zghlxwxcb.cn/news/detail-642628.html
源碼已經(jīng)上傳到GitHub上,github鏈接:https://github.com/fakerst/NLP/tree/main/Word2Vec-SkipGram文章來源地址http://www.zghlxwxcb.cn/news/detail-642628.html
到了這里,關(guān)于Word2Vec詳解的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!