fastText模型提出了一种子词嵌入方法:基于word2vec中的跳元模型,它将中心词表示为其子词向量之和。
字节对编码执行训练数据集的统计分析,以发现词内的公共符号。作为一种贪心方法,字节对编码迭代地合并最频繁的连续符号对。
子词嵌入可以提高稀有词和词典外词的表示质量。
在实践中,在大型语料库上预先训练的词向量可以应用于下游的自然语言处理任务。
预训练的词向量可以应用于词的相似性和类比任务。
目录
1.子词嵌入
1.1fastText模型
1.2 字节对编码(Byte Pair Encoding)
2.词的相似性和类比任务
2.1 加载预训练词向量
2.2应用预训练词向量
2.2.1词相似度
2.2.2词类比
1.子词嵌入
在英语中,“helps”“helped”和“helping”等单词都是同一个词“help”的变形形式。“dog”和“dogs”之间的关系与“cat”和“cats”之间的关系相同,“boy”和“boyfriend”之间的关系与“girl”和“girlfriend”之间的关系相同。在法语和西班牙语等其他语言中,许多动词有40多种变形形式,而在芬兰语中,名词最多可能有15种变形。在语言学中,形态学研究单词形成和词汇关系。但是,word2vec和GloVe都没有对词的内部结构进行探讨。
1.1fastText模型
回想一下词在word2vec中是如何表示的。在跳元模型和连续词袋模型中,同一词的不同变形形式直接由不同的向量表示,不需要共享参数。为了使用形态信息,fastText模型提出了一种子词嵌入方法,其中子词是一个字符n-gram (Bojanowski et al., 2017)。fastText可以被认为是子词级跳元模型,而非学习词级向量表示,其中每个中心词由其子词级向量之和表示。
让我们来说明如何以单词“where”为例获得fastText中每个中心词的子词。首先,在词的开头和末尾添加特殊字符“<”和“>”,以将前缀和后缀与其他子词区分开来。 然后,从词中提取字符n-gram。 例如,值n=3时,我们将获得长度为3的所有子词: “<wh”“whe”“her”“ere”“re>”和特殊子词“<where>”。
fastText的其余部分与跳元模型相同。与跳元模型相比,fastText的词量更大,模型参数也更多。此外,为了计算一个词的表示,它的所有子词向量都必须求和,这导致了更高的计算复杂度。然而,由于具有相似结构的词之间共享来自子词的参数,罕见词甚至词表外的词在fastText中可能获得更好的向量表示。
1.2 字节对编码(Byte Pair Encoding)
在fastText中,所有提取的子词都必须是指定的长度,例如3到6,因此词表大小不能预定义。为了在固定大小的词表中允许可变长度的子词,我们可以应用一种称为字节对编码(Byte Pair Encoding,BPE)的压缩算法来提取子词 (Sennrich et al., 2015)。
字节对编码执行训练数据集的统计分析,以发现单词内的公共符号,诸如任意长度的连续字符。从长度为1的符号开始,字节对编码迭代地合并最频繁的连续符号对以产生新的更长的符号。请注意,为提高效率,不考虑跨越单词边界的对。最后,我们可以使用像子词这样的符号来切分单词。字节对编码及其变体已经用于诸如GPT-2 (Radford et al., 2019)和RoBERTa (Liu et al., 2019)等自然语言处理预训练模型中的输入表示。在下面,我们将说明字节对编码是如何工作的。
首先,我们将符号词表初始化为所有英文小写字符、特殊的词尾符号'_'
和特殊的未知符号'[UNK]'
。
pip install mxnet==1.7.0.post1
pip install d2l==0.17.0
import collections
symbols = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'_', '[UNK]']
因为我们不考虑跨越词边界的符号对,所以我们只需要一个字典raw_token_freqs
将词映射到数据集中的频率(出现次数)。注意,特殊符号'_'
被附加到每个词的尾部,以便我们可以容易地从输出符号序列(例如,“a_all er_man”)恢复单词序列(例如,“a_all er_man”)。由于我们仅从单个字符和特殊符号的词开始合并处理,所以在每个词(词典token_freqs
的键)内的每对连续字符之间插入空格。换句话说,空格是词中符号之间的分隔符。
raw_token_freqs = {'fast_': 4, 'faster_': 3, 'tall_': 5, 'taller_': 4}
token_freqs = {}
for token, freq in raw_token_freqs.items():
token_freqs[' '.join(list(token))] = raw_token_freqs[token]
token_freqs
{'f a s t _': 4, 'f a s t e r _': 3, 't a l l _': 5, 't a l l e r _': 4}
我们定义以下get_max_freq_pair
函数,其返回词内最频繁的连续符号对,其中词来自输入词典token_freqs
的键。
def get_max_freq_pair(token_freqs):
pairs = collections.defaultdict(int)
for token, freq in token_freqs.items():
symbols = token.split()
for i in range(len(symbols) - 1):
# “pairs”的键是两个连续符号的元组
pairs[symbols[i], symbols[i + 1]] += freq
return max(pairs, key=pairs.get) # 具有最大值的“pairs”键
作为基于连续符号频率的贪心方法,字节对编码将使用以下merge_symbols
函数来合并最频繁的连续符号对以产生新符号。
def merge_symbols(max_freq_pair, token_freqs, symbols):
symbols.append(''.join(max_freq_pair))
new_token_freqs = dict()
for token, freq in token_freqs.items():
new_token = token.replace(' '.join(max_freq_pair),
''.join(max_freq_pair))
new_token_freqs[new_token] = token_freqs[token]
return new_token_freqs
现在,我们对词典token_freqs
的键迭代地执行字节对编码算法。在第一次迭代中,最频繁的连续符号对是't'
和'a'
,因此字节对编码将它们合并以产生新符号'ta'
。在第二次迭代中,字节对编码继续合并'ta'
和'l'
以产生另一个新符号'tal'
。
num_merges = 10
for i in range(num_merges):
max_freq_pair = get_max_freq_pair(token_freqs)
token_freqs = merge_symbols(max_freq_pair, token_freqs, symbols)
print(f'合并# {i+1}:',max_freq_pair)
合并# 1: ('t', 'a') 合并# 2: ('ta', 'l') 合并# 3: ('tal', 'l') 合并# 4: ('f', 'a') 合并# 5: ('fa', 's') 合并# 6: ('fas', 't') 合并# 7: ('e', 'r') 合并# 8: ('er', '_') 合并# 9: ('tall', '_') 合并# 10: ('fast', '_')
在字节对编码的10次迭代之后,我们可以看到列表symbols
现在又包含10个从其他符号迭代合并而来的符号。
print(symbols)
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '_', '[UNK]', 'ta', 'tal', 'tall', 'fa', 'fas', 'fast', 'er', 'er_', 'tall_', 'fast_']
对于在词典raw_token_freqs
的键中指定的同一数据集,作为字节对编码算法的结果,数据集中的每个词现在被子词“fast_”“fast”“er_”“tall_”和“tall”分割。例如,单词“fast er_”和“tall er_”分别被分割为“fast er_”和“tall er_”。
print(list(token_freqs.keys()))
['fast_', 'fast er_', 'tall_', 'tall er_']
请注意,字节对编码的结果取决于正在使用的数据集。我们还可以使用从一个数据集学习的子词来切分另一个数据集的单词。作为一种贪心方法,下面的segment_BPE
函数尝试将单词从输入参数symbols
分成可能最长的子词。
def segment_BPE(tokens, symbols):
outputs = []
for token in tokens:
start, end = 0, len(token)
cur_output = []
# 具有符号中可能最长子字的词元段
while start < len(token) and start < end:
if token[start: end] in symbols:
cur_output.append(token[start: end])
start = end
end = len(token)
else:
end -= 1
if start < len(token):
cur_output.append('[UNK]')
outputs.append(' '.join(cur_output))
return outputs
我们使用列表symbols
中的子词(从前面提到的数据集学习)来表示另一个数据集的tokens
。
tokens = ['tallest_', 'fatter_']
print(segment_BPE(tokens, symbols))
['tall e s t _', 'fa t t er_']
2.词的相似性和类比任务
在 预训练word2vec,代码_流萤数点的博客-CSDN博客中,我们在一个小的数据集上训练了一个word2vec模型,并使用它为一个输入词寻找语义相似的词。实际上,在大型语料库上预先训练的词向量可以应用于下游的自然语言处理任务。为了直观地演示大型语料库中预训练词向量的语义,让我们将预训练词向量应用到词的相似性和类比任务中。
import os
from mxnet import np, npx
from d2l import mxnet as d2l
npx.set_np()
2.1 加载预训练词向量
以下列出维度为50、100和300的预训练GloVe嵌入,可从GloVe网站下载。预训练的fastText嵌入有多种语言。这里我们使用可以从fastText网站下载300维度的英文版本(“wiki.en”)。
#@save
d2l.DATA_HUB['glove.6b.50d'] = (d2l.DATA_URL + 'glove.6B.50d.zip',
'0b8703943ccdb6eb788e6f091b8946e82231bc4d')
#@save
d2l.DATA_HUB['glove.6b.100d'] = (d2l.DATA_URL + 'glove.6B.100d.zip',
'cd43bfb07e44e6f27cbcc7bc9ae3d80284fdaf5a')
#@save
d2l.DATA_HUB['glove.42b.300d'] = (d2l.DATA_URL + 'glove.42B.300d.zip',
'b5116e234e9eb9076672cfeabf5469f3eec904fa')
#@save
d2l.DATA_HUB['wiki.en'] = (d2l.DATA_URL + 'wiki.en.zip',
'c1816da3821ae9f43899be655002f6c723e91b88')
为了加载这些预训练的GloVe和fastText嵌入,我们定义了以下TokenEmbedding
类。
#@save
class TokenEmbedding:
"""GloVe嵌入"""
def __init__(self, embedding_name):
self.idx_to_token, self.idx_to_vec = self._load_embedding(
embedding_name)
self.unknown_idx = 0
self.token_to_idx = {token: idx for idx, token in
enumerate(self.idx_to_token)}
def _load_embedding(self, embedding_name):
idx_to_token, idx_to_vec = ['<unk>'], []
data_dir = d2l.download_extract(embedding_name)
# GloVe网站:https://nlp.stanford.edu/projects/glove/
# fastText网站:https://fasttext.cc/
with open(os.path.join(data_dir, 'vec.txt'), 'r') as f:
for line in f:
elems = line.rstrip().split(' ')
token, elems = elems[0], [float(elem) for elem in elems[1:]]
# 跳过标题信息,例如fastText中的首行
if len(elems) > 1:
idx_to_token.append(token)
idx_to_vec.append(elems)
idx_to_vec = [[0] * len(idx_to_vec[0])] + idx_to_vec
return idx_to_token, np.array(idx_to_vec)
def __getitem__(self, tokens):
indices = [self.token_to_idx.get(token, self.unknown_idx)
for token in tokens]
vecs = self.idx_to_vec[np.array(indices)]
return vecs
def __len__(self):
return len(self.idx_to_token)
下面我们加载50维GloVe嵌入(在维基百科的子集上预训练)。创建TokenEmbedding
实例时,如果尚未下载指定的嵌入文件,则必须下载该文件。
glove_6b50d = TokenEmbedding('glove.6b.50d')
输出词表大小。词表包含400000个词(词元)和一个特殊的未知词元。
len(glove_6b50d)
我们可以得到词表中一个单词的索引,反之亦然。
glove_6b50d.token_to_idx['beautiful'], glove_6b50d.idx_to_token[3367]
2.2应用预训练词向量
使用加载的GloVe向量,我们将通过下面的词相似性和类比任务中来展示词向量的语义。
2.2.1词相似度
为了根据词向量之间的余弦相似性为输入词查找语义相似的词,我们实现了以下knn
(k近邻)函数。
def knn(W, x, k):
# 增加1e-9以获得数值稳定性
cos = np.dot(W, x.reshape(-1,)) / (
np.sqrt(np.sum(W * W, axis=1) + 1e-9) * np.sqrt((x * x).sum()))
topk = npx.topk(cos, k=k, ret_typ='indices')
return topk, [cos[int(i)] for i in topk]
然后,我们使用TokenEmbedding
的实例embed
中预训练好的词向量来搜索相似的词。
def get_similar_tokens(query_token, k, embed):
topk, cos = knn(embed.idx_to_vec, embed[[query_token]], k + 1)
for i, c in zip(topk[1:], cos[1:]): # 排除输入词
print(f'{embed.idx_to_token[int(i)]}:cosine相似度={float(c):.3f}')
glove_6b50d
中预训练词向量的词表包含400000个词和一个特殊的未知词元。排除输入词和未知词元后,我们在词表中找到与“chip”一词语义最相似的三个词。
get_similar_tokens('chip', 3, glove_6b50d)
下面输出与“baby”和“beautiful”相似的词。
get_similar_tokens('baby', 3, glove_6b50d)
get_similar_tokens('beautiful', 3, glove_6b50d)
2.2.2词类比
除了找到相似的词,我们还可以将词向量应用到词类比任务中。 例如,“man” : “woman” :: “son” : “daughter”是一个词的类比。 “man”是对“woman”的类比,“son”是对“daughter”的类比。 具体来说,词类比任务可以定义为: 对于单词类比a:b::c:d,给出前三个词a、b和c,找到d。 用vec(w)表示词w的向量, 为了完成这个类比,我们将找到一个词, 其向量与vec(c)+vec(b)−vec(a)的结果最相似。
def get_analogy(token_a, token_b, token_c, embed):
vecs = embed[[token_a, token_b, token_c]]
x = vecs[1] - vecs[0] + vecs[2]
topk, cos = knn(embed.idx_to_vec, x, 1)
return embed.idx_to_token[int(topk[0])] # 删除未知词
让我们使用加载的词向量来验证“male-female”类比。
get_analogy('man', 'woman', 'son', glove_6b50d)
下面完成一个“首都-国家”的类比: “beijing” : “china” :: “tokyo” : “japan”。 这说明了预训练词向量中的语义。
get_analogy('beijing', 'china', 'tokyo', glove_6b50d)
另外,对于“bad” : “worst” :: “big” : “biggest”等“形容词-形容词最高级”的比喻,预训练词向量可以捕捉到句法信息。
get_analogy('bad', 'worst', 'big', glove_6b50d)
为了演示在预训练词向量中捕捉到的过去式概念,我们可以使用“现在式-过去式”的类比来测试句法:“do” : “did” :: “go” : “went”。
get_analogy('do', 'did', 'go', glove_6b50d)