Appearance
子词嵌入
一、为什么需要子词嵌入
在自然语言处理里,我们经常会遇到这样的情况:
- 很多词是基础词的变形,比如 "helps" 是 "help" 的第三人称单数形式,"helped" 是 "help" 的过去式,"helping" 是 "help" 的现在分词;"dogs" 是 "dog" 的复数形式,"cats" 是 "cat" 的复数形式。
- 还有一些词是由基础词组合而来的,比如 "boyfriend" 是 "boy" 和 "friend" 的组合,"girlfriend" 是 "girl" 和 "friend" 的组合。
但是之前学的 word2vec 和 GloVe 这些词嵌入方法,都是把每个词当成一个独立的整体,不会考虑词的内部结构。比如 "help" 和 "helps" 在 word2vec 里是两个完全不同的词,它们的向量没有任何关联,这样就浪费了词的内部结构信息,而且对于那些很少出现的稀有词,或者词表里没有的未登录词,word2vec 和 GloVe 很难生成好的向量表示。
子词嵌入就是为了解决这个问题出现的,它会把每个词拆分成更小的 "小词块"(也就是子词),比如把 "helps" 拆分成 "hel"、"elp"、"lps" 这些子词,然后用这些子词的向量来表示整个词的向量。这样相似结构的词可以共享子词的参数,稀有词和未登录词也能通过子词生成更好的向量表示。
二、fastText 模型:用子词表示词
fastText 是一种子词嵌入方法,它是在 word2vec 的跳元模型基础上改进的,思路很简单:
- 拆分词为子词:把每个词拆分成多个子词,比如对于单词 "where",我们先在词的开头和结尾加上特殊字符 "<" 和 ">",变成 "",然后提取长度为 3 到 6 的字符片段(也就是字符 n-gram),比如 n=3 时,会得到"<wh"、"whe"、"her"、"ere"、"re>",还有特殊子词""。
- 词向量是子词向量的和:每个词的向量等于它所有子词的向量之和,比如 "where" 的向量就是 "<wh"、"whe"、"her"、"ere"、"re>"、"" 这些子词的向量加起来。
这样做的好处是:
- 相似结构的词可以共享子词的参数,比如 "help" 和 "helps" 有很多相同的子词,它们的向量会比较接近,模型能更好地理解它们的语义关系。
- 对于稀有词或者未登录词,即使这个词在训练数据里很少出现,甚至没出现过,模型也可以通过它的子词生成向量表示,比如 "supercalifragilisticexpialidocious" 这个很长的词,模型可以把它拆分成子词,然后用子词的向量之和来表示它。
不过 fastText 也有缺点:
- 词表会变大,因为每个词都有很多子词,模型的参数也会变多。
- 计算词向量的时候需要把所有子词的向量加起来,计算复杂度会更高。
三、字节对编码(BPE):生成可变长度的子词
fastText 的子词长度是固定的(比如 3 到 6),这样词表大小没法预先定义。字节对编码(Byte Pair Encoding,BPE)是一种压缩算法,它可以根据训练数据的统计信息,生成可变长度的子词,适合固定大小的词表,现在很多预训练模型(比如 GPT-2、RoBERTa)都用 BPE 来处理输入。
1. BPE 的基本思路
BPE 的思路很简单,就是从单个字符开始,不断合并最频繁出现的连续字符对,生成新的子词,直到达到我们想要的词表大小。
2. BPE 的步骤
我们用一个简单的例子来看看 BPE 是怎么工作的:
(1)初始化符号词表
首先我们初始化符号词表,包含所有英文小写字母、特殊的词尾符号 "_"(用来区分词的结尾)和未知符号 "[UNK]"(用来表示词表里没有的符号)。
(2)统计词频
我们有一个训练数据集,里面的词和它们的出现频率是:"fast*" 出现 4 次,"faster*" 出现 3 次,"tall*" 出现 5 次,"taller*" 出现 4 次。
然后我们把每个词拆分成单个字符,字符之间用空格分隔,比如 "fast*" 变成 "f a s t ","faster"变成"f a s t e r *",这样我们得到一个新的词频字典:
Plain
{'f a s t _': 4, 'f a s t e r _': 3, 't a l l _': 5, 't a l l e r _': 4}(3)迭代合并最频繁的符号对
接下来我们迭代地合并最频繁出现的连续符号对:
- 第一次迭代:我们统计所有连续符号对的出现频率,发现 "t" 和 "a" 出现的频率最高("t a" 在 "t a l l *" 里出现 5 次,在 "t a l l e r *" 里出现 4 次,总共 9 次),所以我们把 "t" 和 "a" 合并成新的符号 "ta",词频字典变成:
Plain
{'f a s t _': 4, 'f a s t e r _': 3, 'ta l l _': 5, 'ta l l e r _': 4}- 第二次迭代:我们继续统计连续符号对的频率,发现 "ta" 和 "l" 出现的频率最高("ta l" 在 "ta l l *" 里出现 5 次,在 "ta l l e r *" 里出现 4 次,总共 9 次),所以我们把 "ta" 和 "l" 合并成新的符号 "tal",词频字典变成:
Plain
{'f a s t _': 4, 'f a s t e r _': 3, 'tal l _': 5, 'tal l e r _': 4}- 第三次迭代:我们统计连续符号对的频率,发现 "tal" 和 "l" 出现的频率最高("tal l" 在 "tal l *" 里出现 5 次,在 "tal l e r *" 里出现 4 次,总共 9 次),所以我们把 "tal" 和 "l" 合并成新的符号 "tall",词频字典变成:
Plain
{'f a s t _': 4, 'f a s t e r _': 3, 'tall _': 5, 'tall e r _': 4}- 第四次迭代:我们统计连续符号对的频率,发现 "f" 和 "a" 出现的频率最高("f a" 在 "f a s t *" 里出现 4 次,在 "f a s t e r *" 里出现 3 次,总共 7 次),所以我们把 "f" 和 "a" 合并成新的符号 "fa",词频字典变成:
Plain
{'fa s t _': 4, 'fa s t e r _': 3, 'tall _': 5, 'tall e r _': 4}- 第五次迭代:我们统计连续符号对的频率,发现 "fa" 和 "s" 出现的频率最高("fa s" 在 "fa s t *" 里出现 4 次,在 "fa s t e r *" 里出现 3 次,总共 7 次),所以我们把 "fa" 和 "s" 合并成新的符号 "fas",词频字典变成:
Plain
{'fas t _': 4, 'fas t e r _': 3, 'tall _': 5, 'tall e r _': 4}- 第六次迭代:我们统计连续符号对的频率,发现 "fas" 和 "t" 出现的频率最高("fas t" 在 "fas t *" 里出现 4 次,在 "fas t e r *" 里出现 3 次,总共 7 次),所以我们把 "fas" 和 "t" 合并成新的符号 "fast",词频字典变成:
Plain
{'fast _': 4, 'fast e r _': 3, 'tall _': 5, 'tall e r _': 4}- 第七次迭代:我们统计连续符号对的频率,发现 "e" 和 "r" 出现的频率最高("e r" 在 "fast e r *" 里出现 3 次,在 "tall e r *" 里出现 4 次,总共 7 次),所以我们把 "e" 和 "r" 合并成新的符号 "er",词频字典变成:
Plain
{'fast _': 4, 'fast er _': 3, 'tall _': 5, 'tall er _': 4}- 第八次迭代:我们统计连续符号对的频率,发现 "er" 和 "*" 出现的频率最高("er *" 在 "fast er *"里出现 3 次,在"tall er *"里出现 4 次,总共 7 次),所以我们把"er"和"*"合并成新的符号"er*",词频字典变成:
Plain
{'fast _': 4, 'fast er_': 3, 'tall _': 5, 'tall er_': 4}- 第九次迭代:我们统计连续符号对的频率,发现 "tall" 和 ""出现的频率最高("tall "出现 5 次),所以我们把"tall"和""合并成新的符号"tall",词频字典变成:
Plain
{'fast _': 4, 'fast er_': 3, 'tall_': 5, 'tall er_': 4}- 第十次迭代:我们统计连续符号对的频率,发现 "fast" 和 ""出现的频率最高("fast "出现 4 次),所以我们把"fast"和""合并成新的符号"fast",词频字典变成:
Plain
{'fast_': 4, 'fast er_': 3, 'tall_': 5, 'tall er_': 4}经过 10 次迭代,我们生成了 10 个新的子词:"ta"、"tal"、"tall"、"fa"、"fas"、"fast"、"er"、"er*"、"tall*"、"fast_"。
(4)用 BPE 切分单词
现在我们可以用这些子词来切分新的单词,比如 "tallest*" 和 "fatter*":
对于 "tallest*",我们从最长的子词开始匹配,首先匹配 "tall",剩下的部分是 "est*",然后匹配 "e"、"s"、"t"、"*",所以切分结果是 "tall e s t *"。
对于 "fatter*",我们从最长的子词开始匹配,首先匹配 "fa",剩下的部分是 "tter*",然后匹配 "t"、"t"、"er*",所以切分结果是 "fa t t er*"。
3. BPE 的优点
- 可以生成可变长度的子词,能更好地适应不同的词结构。
- 可以根据训练数据生成适合的子词,提升模型对词的表示能力。
- 可以用训练好的子词来切分未见过的单词,处理未登录词。
四、小结
子词嵌入是为了利用词的内部结构,提升模型对稀有词和未登录词的表示能力。
fastText 把词拆分成固定长度的子词,每个词的向量是子词向量的和,相似结构的词可以共享子词参数。
字节对编码(BPE)是一种压缩算法,通过迭代合并最频繁的符号对来生成可变长度的子词,适合固定大小的词表,现在很多预训练模型都用 BPE 来处理输入。
子词嵌入可以让模型更好地理解词的语义关系,提升模型的性能。
(注:文档部分内容可能由 AI 生成) 源地址