在英语中,“helps”、“helped”和“helping”等单词都是同⼀个词“help”的变形形式。“dog”和“dogs”之间的关系与“cat”和“cats”之间的关系相同,“boy”和“boyfriend”之间的关系与“girl”和“girlfriend”之间的关系相同。在法语和西班⽛语等其他语⾔中,许多动词有40多种变形形式,⽽在芬兰语中,名词最多可能有15种变形。在语⾔学中,形态学研究单词形成和词汇关系。但是,word2vec和GloVe都没有对词的内部结构进⾏探讨
在fastText中,所有提取的子词都必须指定长度,例如3到6,因此词表大小不能预定义。为了在固定大小的词表中允许可变长度的子词,我们可以应用一种称为字节对编码(Byte Pair Encoding,BPE)的压缩算法来提取子词
字节对编码执行训练数据集的统计分析,以发现单词内的公共符合,诸如任意长度的连续字符。从长度为1的符号开始,字节对编码迭代地合并最频繁的连续符号对以产生新的更长的符号。请注意,为提高效率,不考虑跨越单词边界的对。最后,我们可以使用像子词这样的符号来切分单词。字节对编码及其变体已经用于诸如:GPT-2、RoBERTa等自然语言处理预训练模型中的输入表示。在下面,我们将说明字节对编码时如何工作的
首先,我们将符号词表初始化为所有英文小写字符、特殊的词尾符合’ _ ‘和特殊的未知符号’ [UNK] ’
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_aller_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]
print(token_freqs)
{'f a s t _': 4}
{'f a s t _': 4, 'f a s t e r _': 3}
{'f a s t _': 4, 'f a s t e r _': 3, 't a l l _': 5}
{'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', 'ta', 'ta', 'ta', 'ta', 'ta', 'ta', 'ta', 'ta', 'ta', 'ta', '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_']