在英语中,诸如“helps”、“helped”和“helping”之类的词是同一个词“help”的变形形式。“dog”与“dogs”的关系与“cat”与“cats”的关系相同,“boy”与“boyfriend”的关系与“girl”与“girlfriend”的关系相同。在法语和西班牙语等其他语言中,许多动词有超过 40 种变形形式,而在芬兰语中,一个名词可能有多达 15 种格。在语言学中,形态学研究词的形成和词的关系。然而,word2vec 和 GloVe 都没有探索单词的内部结构。
15.6.1。fastText 模型
回想一下单词在 word2vec 中是如何表示的。在skip-gram模型和连续词袋模型中,同一个词的不同变形形式直接由不同的向量表示,没有共享参数。为了使用形态信息,fastText 模型提出了一种子词嵌入方法,其中一个子词是一个字符n-gram (Bojanowski等人,2017 年)。与学习词级向量表示不同,fastText 可以被视为子词级 skip-gram,其中每个中心词由其子词向量的总和表示。
让我们举例说明如何使用单词“where”为 fastText 中的每个中心词获取子词。首先,在单词的首尾添加特殊字符“<”和“>”,以区别于其他子词的前缀和后缀。然后,提取字符n-克从字。例如,当n=3,我们得到所有长度为 3 的子词:“”,以及特殊子词“”。
在 fastText 中,对于任何单词w, 表示为Gw其所有长度在 3 到 6 之间的子字及其特殊子字的并集。词汇表是所有词的子词的并集。出租zg是子词的向量g在字典中,向量vw为词w作为 skip-gram 模型中的中心词的是其子词向量的总和:
(15.6.1)vw=∑g∈Gwzg.
fastText 的其余部分与 skip-gram 模型相同。与skip-gram模型相比,fastText中的词汇量更大,导致模型参数更多。此外,为了计算一个词的表示,必须将其所有子词向量相加,从而导致更高的计算复杂度。然而,由于具有相似结构的词之间的子词共享参数,稀有词甚至词汇表外的词可能会在 fastText 中获得更好的向量表示。
15.6.2。字节对编码
在 fastText 中,所有提取的子词都必须具有指定的长度,例如3到6,因此无法预定义词汇量大小。为了允许在固定大小的词汇表中使用可变长度的子词,我们可以应用一种称为字节对编码(BPE) 的压缩算法来提取子词 ( Sennrich et al. , 2015 )。
字节对编码对训练数据集进行统计分析,以发现单词中的常见符号,例如任意长度的连续字符。从长度为 1 的符号开始,字节对编码迭代地合并最频繁的一对连续符号以产生新的更长的符号。请注意,为了提高效率,不考虑跨越单词边界的对。最后,我们可以使用子词这样的符号来分词。字节对编码及其变体已用于流行的自然语言处理预训练模型中的输入表示,例如 GPT-2 (Radford等人,2019 年)和 RoBERTa (Liu等人,2019 年). 下面,我们将说明字节对编码的工作原理。
首先,我们将符号词汇表初始化为所有英文小写字符、一个特殊的词尾符号'_'和一个特殊的未知符号'[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]']
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_ tall er_ man”)中恢复单词序列(例如,“a taller 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}
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): # Key of `pairs` is a tuple of two consecutive symbols pairs[symbols[i], symbols[i + 1]] += freq return max(pairs, key=pairs.get) # Key of `pairs` with the max value
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): # Key of `pairs` is a tuple of two consecutive symbols pairs[symbols[i], symbols[i + 1]] += freq return max(pairs, key=pairs.get) # Key of `pairs` with the max value
作为一种基于连续符号频率的贪婪方法,字节对编码将使用以下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
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'merge #{i + 1}:', max_freq_pair)
merge #1: ('t', 'a') merge #2: ('ta', 'l') merge #3: ('tal', 'l') merge #4: ('f', 'a') merge #5: ('fa', 's') merge #6: ('fas', 't') merge #7: ('e', 'r') merge #8: ('er', '_') merge #9: ('tall', '_') merge #10: ('fast', '_')
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'merge #{i + 1}:', max_freq_pair)
merge #1: ('t', 'a') merge #2: ('ta', 'l') merge #3: ('tal', 'l') merge #4: ('f', 'a') merge #5: ('fa', 's') merge #6: ('fas', 't') merge #7: ('e', 'r') merge #8: ('er', '_') merge #9: ('tall', '_') merge #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_']
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”分割为字节对编码的结果算法。例如,单词“faster_”和“taller_”分别被分割为“fast er_”和“tall er_”。
print(list(token_freqs.keys()))
['fast_', 'fast er_', 'tall_', '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 = [] # Segment token with the longest possible subwords from symbols 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
def segment_BPE(tokens, symbols): outputs = [] for token in tokens: start, end = 0, len(token) cur_output = [] # Segment token with the longest possible subwords from symbols 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_']
tokens = ['tallest_', 'fatter_'] print(segment_BPE(tokens, symbols))
['tall e s t _', 'fa t t er_']
15.6.3。概括
fastText 模型提出了一种子词嵌入方法。基于 word2vec 中的 skip-gram 模型,它将中心词表示为其子词向量的总和。
字节对编码对训练数据集进行统计分析,以发现单词中的常见符号。作为一种贪婪的方法,字节对编码迭代地合并最频繁的一对连续符号。
子词嵌入可以提高罕见词和词典外词的表示质量。
15.6.4。练习
例如,大约有3×108可能的 6-英语克。当子词太多时会出现什么问题?如何解决这个问题?提示:参考 fastText 论文(Bojanowski et al. , 2017)第 3.2 节的结尾。
如何设计基于连续词袋模型的子词嵌入模型?
获得尺寸词汇表m, 当初始符号词汇量为n?
如何扩展字节对编码的思想来提取短语?
-
pytorch
+关注
关注
2文章
808浏览量
13216
发布评论请先 登录
相关推荐
评论