前面的两篇facebook的文章都提到了BPE(Byte Pair Encoding,双字节编码)算法,可能大家不太了解,今天通过这篇文章介绍一下BPE的具体原理。这是2016ACL关于NLP分词操作的论文,许多论文方法(例如BERT等)都将该方法应用到分词处理上,相对于word-level和character-level,该方法取得了不错的效果。该文章主要通过BPE算法来解决OOV问题。
论文链接:
Neural Machine Translation of Rare Words with Subword Units
代码链接:
动机和创新点
- 机器翻译中,通常使用固定大小的词表,而在实际翻译场景中,应当是open-vocabulary。这就使得翻译数据集中的稀有词变得困难,不能生成词表中没出现的词。这就是OOV问题。
- 对于word-level 翻译模型,通常使用back-off dictionary(比如将source和target两两对应起来,使用OOV来表示,这样在翻译结果中出现OOV时,就用source所对应的target来代替)来处理OOV词汇。但是这样做是建立在source target中的词总是能一一对应的前提下,因为语言之间的形态合成程度不同,这种假设常常不成立;其次word-level 翻译模型不能生成模型没见过的词(不在词表中),对于这种情况,有论文提出直接从copy unknown 词到target words中,但是这种处理策略也仅仅限于一些实体名称类的词汇;同时为了节省计算时间和资源,词表大小通常被限制在30k-50k之间,所以词表空间是比较昂贵的,如果一些词义类似的词放到词表中,例如like,liked,liking等形态上类似的词均放到词表中,直观上感觉有些浪费。
- 在实际翻译时,并不一定都是以word为基本单位进行翻译,可通过比单词更小的单位(subword)进行翻译,例如名称类的词(通过subword复制或音译)、复合词(形态上类似的词,前缀后缀相同,如run、runer、running等,可通过合成(run与er合成等)翻译)、同源词和外来词(通过subword语音和形态转换),以上称为透明翻译。直观上来看,有时相对于以一个word作为基本单位整体去翻译,这种通过subword来翻译则显得更有效率和意义。论文中提到,从德语数据集中分析,在100个稀有词(不在出现最频繁的5000个词中)中,大多数的词可以通过更小的subword units进行翻译。
- 那么,如何将word切分成合适的subword呢?论文中提出了采用Byte pair encoding(BPE)压缩算法,首先以字符划分,然后再合并。也就是不断的以出现频次最高的2-gram进行合并操做,直到打到词表大小为止。这种以频次合并是比较符合常识的,例如像’er’,'ing,'ed’这样比较有意义的后缀,'e’和’r’同时出现的频次应该比较多。“ing”和“ed”类似道理。
- 将稀有词划分成subword,能比较好的处理oov问题。例如训练集中大量出现“runner”和“thinking”,那按word-level词频来说,word-level词表中应该包含这两个词,此时词“running”只出现一次,则该词很有可能是oov,但是如果以subword切词,则subword词表中应该含有“run”,”er”,”think”,”ing”,那么对于”running”,其subword切词后为“run”,”ing”均在词表中。
- 通过对稀有词进行适当的切分,得到subword units,机器翻译模型能更好的处理透明翻译,并且能生成一些unseen words。
- 机器翻译模型通常都会用到注意力机制,在word-level模型中,模型每次只能计算在word级别上的注意力,我们希望模型可以在每一步学习中将注意力放在不同的subword上,显然这样更有意义和效率。
BPE算法
一个很简单的压缩算法。具体来说,分为以下几步:
- 将原始数据集划分成单词集合,再将每个单词划分成字符集,对于每个单词最后一个字符后面加上’-’(标记单词边界,以后用于字符恢复成单词),这样就得到了一个大的字符集合。
- 统计上面的得到的字符集合,统计每个单词内 2-gram字符出现频次,得到频次最高的2-gram字符,例如(‘A’,‘B’)连续出现的频率最高,则以“AB”替换所有单词内出现的(‘A‘,‘B’),然后将该新词添加到词表中。这里注意:该新词以后可以作为一个整体与单词内其他字符合并;替换或合并不跨单词边界。
- 对步骤2循环进行多次,直到达到就得到我们预设的词表大小。最终的vocab_size = init_size(这里为0)+ merge_operation_count, merge_operation_count是模型唯一的超参数。
上图中,实际上将er这样常见且很有意义的后缀作为一个subword放入到词表中,对模型理解语言和节省词表空间、以及OOV问题很有帮助。
下面通过具体的例子来通俗的解释BPE的实现过程:
比如我们想编码:
aaabdaaabac
我们会发现这里的aa出现的词数最高(我们这里只看两个字符的频率),那么用这里没有的字符Z来替代aa:
ZabdZabac
Z=aa
此时,又发现ab出现的频率最高,那么同样的,Y来代替ab:
ZYdZYac
Y=ab
Z=aa
同样的,ZY出现的频率大,我们用X来替代ZY:
XdXac
X=ZY
Y=ab
Z=aa
最后,连续两个字符的频率都为1了,也就结束了。就是这么简单。
解码的时候,就按照相反的顺序更新替换即可。
BPE算法应用
BPE这么好,我们是不是哪里都能这么用呢?其实在我们的中文中不是很适用。首先我们的中文不像英文或者其他欧洲的语言一样通过空格分开,我们是连续的。其次我们的中文一个字就是一个最小的单元,无法在拆分的更小了。在中文中一般的处理方式是两中,分词和分字。理论上分词要比分字好,因为分词更加细致,语义分的更加开。分字简单,效率高,词表也很小,常用字就3000左右。
BPE改进算法
下面这篇文章提出了BPE的改进算法BPEmb:
BPEmb: Tokenization-free Pre-trained Subword Embeddings
in 275 Languages