詞嵌入進階
在“Word2Vec的實現”一節中,我們在小規模數據集上訓練了一個 Word2Vec 詞嵌入模型,並通過詞向量的餘弦相似度搜索近義詞。雖然 Word2Vec 已經能夠成功地將離散的單詞轉換爲連續的詞向量,並能一定程度上地保存詞與詞之間的近似關係,但 Word2Vec 模型仍不是完美的,它還可以被進一步地改進:
- 子詞嵌入(subword embedding):FastText 以固定大小的 n-gram 形式將單詞更細緻地表示爲了子詞的集合,而 BPE (byte pair encoding) 算法則能根據語料庫的統計信息,自動且動態地生成高頻子詞的集合;
- GloVe 全局向量的詞嵌入: 通過等價轉換 Word2Vec 模型的條件概率公式,我們可以得到一個全局的損失函數表達,並在此基礎上進一步優化模型。
實際中,我們常常在大規模的語料上訓練這些詞嵌入模型,並將預訓練得到的詞向量應用到下游的自然語言處理任務中。本節就將以 GloVe 模型爲例,演示如何用預訓練好的詞向量來求近義詞和類比詞。
GloVe 全局向量的詞嵌入
GloVe 模型
先簡單回顧以下 Word2Vec 的損失函數(以 Skip-Gram 模型爲例,不考慮負採樣近似):
其中
是 爲中心詞, 爲背景詞時 Skip-Gram 模型所假設的條件概率計算公式,我們將其簡寫爲 。
注意到此時我們的損失函數中包含兩個求和符號,它們分別枚舉了語料庫中的每個中心詞和其對應的每個背景詞。實際上我們還可以採用另一種計數方式,那就是直接枚舉每個詞分別作爲中心詞和背景詞的情況:
其中 表示整個數據集中 作爲 的背景詞的次數總和。
我們還可以將該式進一步地改寫爲交叉熵 (cross-entropy) 的形式如下:
其中 是 的背景詞窗大小總和, 是 在 的背景詞窗中所佔的比例。
從這裏可以看出,我們的詞嵌入方法實際上就是想讓模型學出 有多大概率是 的背景詞,而真實的標籤則是語料庫上的統計數據。同時,語料庫中的每個詞根據 的不同,在損失函數中所佔的比重也不同。
注意到目前爲止,我們只是改寫了 Skip-Gram 模型損失函數的表面形式,還沒有對模型做任何實質上的改動。而在 Word2Vec 之後提出的 GloVe 模型,則是在之前的基礎上做出了以下幾點改動:
- 使用非概率分佈的變量 和 ,並對它們取對數;
- 爲每個詞 增加兩個標量模型參數:中心詞偏差項 和背景詞偏差項 ,鬆弛了概率定義中的規範性;
- 將每個損失項的權重 替換成函數 ,權重函數 是值域在 上的單調遞增函數,鬆弛了中心詞重要性與 線性相關的隱含假設;
- 用平方損失函數替代了交叉熵損失函數。
綜上,我們獲得了 GloVe 模型的損失函數表達式:
由於這些非零 是預先基於整個數據集計算得到的,包含了數據集的全局統計信息,因此 GloVe 模型的命名取“全局向量”(Global Vectors)之意。
載入預訓練的 GloVe 向量
GloVe 官方 提供了多種規格的預訓練詞向量,語料庫分別採用了維基百科、CommonCrawl和推特等,語料庫中詞語總數也涵蓋了從60億到8,400億的不同規模,同時還提供了多種詞向量維度供下游模型使用。
torchtext.vocab
中已經支持了 GloVe, FastText, CharNGram 等常用的預訓練詞向量,我們可以通過聲明 torchtext.vocab.GloVe
類的實例來加載預訓練好的 GloVe 詞向量。
import torch
import torchtext.vocab as vocab
print(vocab.pretrained_aliases.keys())
print([key for key in vocab.pretrained_aliases.keys() if "glove" in key])
# 查看所支持的所有詞向量
cache_dir = "/home/kesci/input/GloVe6B5429"
glove = vocab.GloVe(name='6B', dim=50, cache=cache_dir)
# name:多大規模,dim多大維度
print("一共包含%d個詞。" % len(glove.stoi))
print(glove.stoi['beautiful'], glove.itos[3366])
# stoi –指向向量輸入參數中相關向量索引的字符串字典
0%| | 0/400000 [00:00<?, ?it/s]
dict_keys(['charngram.100d', 'fasttext.en.300d', 'fasttext.simple.300d', 'glove.42B.300d', 'glove.840B.300d', 'glove.twitter.27B.25d', 'glove.twitter.27B.50d', 'glove.twitter.27B.100d', 'glove.twitter.27B.200d', 'glove.6B.50d', 'glove.6B.100d', 'glove.6B.200d', 'glove.6B.300d'])
['glove.42B.300d', 'glove.840B.300d', 'glove.twitter.27B.25d', 'glove.twitter.27B.50d', 'glove.twitter.27B.100d', 'glove.twitter.27B.200d', 'glove.6B.50d', 'glove.6B.100d', 'glove.6B.200d', 'glove.6B.300d']
99%|█████████▉| 397720/400000 [00:09<00:00, 44795.30it/s]
一共包含400000個詞。
3366 beautiful
求近義詞和類比詞
求近義詞
由於詞向量空間中的餘弦相似性可以衡量詞語含義的相似性(爲什麼?),我們可以通過尋找空間中的 k 近鄰,來查詢單詞的近義詞。
- 我們可以把它們想象成空間中的兩條線段,都是從原點([0, 0, …])出發,指向不同的方向。兩條線段之間形成一個夾角,如果夾角爲0度,意味着方向相同、線段重合,這是表示兩個向量代表的文本完全相等;如果夾角爲90度,意味着形成直角,方向完全不相似;如果夾角爲180度,意味着方向正好相反。因此,我們可以通過夾角的大小,來判斷向量的相似程度。夾角越小,就代表越相似。
def knn(W, x, k):
'''
@params:
W: 所有向量的集合
x: 給定向量
k: 查詢的數量
@outputs:
topk: 餘弦相似性最大k個的下標
[...]: 餘弦相似度
'''
cos = torch.matmul(W, x.view((-1,))) / (
(torch.sum(W * W, dim=1) + 1e-9).sqrt() * torch.sum(x * x).sqrt())
_, topk = torch.topk(cos, k=k)
topk = topk.cpu().numpy()
return topk, [cos[i].item() for i in topk]
# torch.topk(input, k, dim=None, largest=True, sorted=True, out=None) -> (Tensor, LongTensor)
# 沿給定dim維度返回輸入張量input中 k 個最大值。
# 如果不指定dim,則默認爲input的最後一維。
# 如果爲largest爲 False ,則返回最小的 k 個值。
# 返回一個元組 (values,indices),其中indices是原始輸入張量input中測元素下標。
# 如果設定布爾值sorted 爲_True_,將會確保返回的 k 個值被排序。
def get_similar_tokens(query_token, k, embed):
'''
@params:
query_token: 給定的單詞
k: 所需近義詞的個數
embed: 預訓練詞向量
'''
topk, cos = knn(embed.vectors,
embed.vectors[embed.stoi[query_token]], k+1)
for i, c in zip(topk[0:], cos[0:]): # 第一個詞語就是它本身
print('cosine sim=%.3f: %s' % (c, (embed.itos[i])))
get_similar_tokens('chip', 3, glove)#包含GloVe,CharNGram或Vectors類的實例化的一個或列表。或者,可用預訓練向量之一或列表
cosine sim=1.000: chip
cosine sim=0.856: chips
cosine sim=0.749: intel
cosine sim=0.749: electronics
get_similar_tokens('baby', 3, glove)
cosine sim=1.000: baby
cosine sim=0.839: babies
cosine sim=0.800: boy
cosine sim=0.792: girl
get_similar_tokens('beautiful', 3, glove)
cosine sim=1.000: beautiful
cosine sim=0.921: lovely
cosine sim=0.893: gorgeous
cosine sim=0.830: wonderful
求類比詞
除了求近義詞以外,我們還可以使用預訓練詞向量求詞與詞之間的類比關係,例如“man”之於“woman”相當於“son”之於“daughter”。求類比詞問題可以定義爲:對於類比關係中的4個詞“ 之於 相當於 之於 ”,給定前3個詞 求 。求類比詞的思路是,搜索與 的結果向量最相似的詞向量,其中 爲 的詞向量。
def get_analogy(token_a, token_b, token_c, embed):
'''
@params:
token_a: 詞a
token_b: 詞b
token_c: 詞c
embed: 預訓練詞向量
@outputs:
res: 類比詞d
'''
vecs = [embed.vectors[embed.stoi[t]]
for t in [token_a, token_b, token_c]]
x = vecs[1] - vecs[0] + vecs[2]
topk, cos = knn(embed.vectors, x, 1)
res = embed.itos[topk[0]]
return res
get_analogy('man', 'woman', 'son', glove)
'daughter'
get_analogy('beijing', 'china', 'tokyo', glove)
'japan'
get_analogy('bad', 'worst', 'big', glove)
'biggest'
get_analogy('do', 'did', 'go', glove)
'went'
總結
- 由於他人訓練詞向量時用到的語料庫和當前任務上的語料庫通常都不相同,所以詞典中包含的詞語以及詞語的順序都可能有很大差別,此時應當根據當前數據集上詞典的順序,來依次讀入詞向量,同時,爲了避免訓練好的詞向量在訓練的最初被破壞,還可以適當調整嵌入層的學習速率甚至設定其不參與梯度下降
- 在進行預訓練詞向量的載入時,我們需要根據任務的特性來選定語料庫的大小和詞向量的維度,以均衡模型的表達能力和泛化能力,同時還要兼顧計算的時間複雜度