Subword策略,很有用的论文。BERT也使用到了,有对应的tokenization.py

Neural Machine Translation of Rare Words with Subword Units

背景

摘要

NMT处理的词汇表是定长的,但是实际翻译却是OOV(out of vocabulary)的。以前是把 新词汇加到词典里去。本文是提出一种subword单元的策略,会把稀有和未知词汇以subword units序列来进行编码,更简单更有效。

会介绍不同分词技术的适用性,包括简单的字符n元模型和基于字节对编码压缩算法的分词技术。也会以经验说明subword模型比传统back-off词典的方法好。

简介

NMT模型的词汇一般是30000-5000,但是翻译却是open-vocabulary的问题。很多语言富有信息创造力,比如凝聚组合等等,翻译系统就需要一种低于word-level的机制。

Word-level NMT的缺点

对于word-level的NMT模型,翻译out-of-vocabulary的单词会回退到dictionary里面去查找。有下面几个缺点

  • 种技术在实际上使这种假设并不成立。比如源单词和目标单词并不是一对一的,你怎么找呢
  • 不能够翻译或者产生未见单词
  • 把unknown单词直接copy到目标句子中,对于人名有时候可以。但是有时候却需要改变形态或者直译。

Subword-level NMT

我们的目标是建立open-vocabulary的翻译模型,不用针对稀有词汇去查字典。事实证明,subword模型效果比传统大词汇表方法更好、更精确。Subword神经网络模型可以从subword表达中学习到组合和直译等能力,也可以有效的产生不在训练数据集中的词汇。本文主要有下面两个贡献

  • open-vocabulary的问题可以通过对稀有词汇使用subword units单元来编码解决
  • 采用Byte pair encoding (BPE) 算法来进行分割。BPE通过一个固定大小的词汇表来表示开放词汇,这个词汇表里面的是变长的字符串序列。这是一种对于神经网络模型非常合适的词分割策略。

神经机器翻译

NMT是使用的Bahdanau的Attention模型。Encoder是双向RNN,输入\(X=(x_1, x_2, \cdots, x_m)\),会把两个方向的隐状态串联起来得到annotation向量\(\mathbf x\)。实际上是一个矩阵,对于单个\(x_j\)来说,对应的注释向量是\(\mathbf{x}_j\)

Decoder是一个单向的RNN,预测\(Y=(y_1, y_2, \cdots, y_n)\)。 预测\(y_i\) 时,需要:

  • 当前的隐状态\(s_i\)
  • 上一时刻的输出\(y_{i-1}\)作为当前的输入
  • 语义向量\(\mathbf c_i\) 。语义向量是由所有的注释向量\(x_j\) 加权求和得到的。权就是对齐概率\(\alpha_{ij}\)。 即\(\mathbf c_i = \sum_{j=1} ^ m \alpha_{ij} \mathbf x_j\)

详情请看谷歌论文里面的介绍或者Bahdanau的论文。

Subword 翻译

下面词汇的翻译是透明的(transparent,明显的)

  • 命名实体。如果两个语言的字母表相同,可以直接copy到目标句子中去,也可以抄写音译直译等。
  • 同源词和外来词。有着共同的起源,但是不同的语言表达形式不同,所以character-level翻译规则就可以了。
  • 形态复杂的词语。包含多个语素的单词,可以通过单独翻译语素来翻译。

总之,通过subword单元表示稀有词汇对于NMT来说可以学到transparent翻译,并且可以翻译和产生未见词汇。

相关工作

对于SMT(Statistical Machine Translation)来说,翻译未见单词一直是研究的主题。

很多未见单词都是人名,如果两种语言的字母表一样,那么可以直接复制过去。如果不一样,那么就得音译过去。基于字符(character-based)的翻译是比较成功的。

形态上很复杂的单词往往需要分割,这里有很多的分割算法。基于短语的SMT的分割算法是比较保守的。而我们需要积极的细分,让网络可以处理open-vocabulary,而不是去求助于背字典。 怎么选择subword units要看具体的任务。

提出了很多这样的技术:生成基于字符或者基于语素的定长的连续的词向量。于此同时,word-based的方法并没有重大发现。现在的注意力机制还是基于word-level的。我们希望,注意力机制能从我们变长表达中收益:网络可以把注意力放在不同的subword units中。 这可以突破定长表达的信息传达瓶颈。

NMT减少词汇表可以大大节省时间和增加空间效率。我们也想要对一个句子更紧凑的表达。因为文本长度增加了,会减少效率,也会增加模型传递信息的距离。(hidden size?)

权衡词汇表大小和文本长度,可以用未分割单词列表,subword 单元表达的稀有词汇。作为一个代替,Byte pair encoding就是这样的一种分割算法,可以学到一个词汇表,同时对文本有很好的压缩率。

Byte Pair Encoding

Byte pair encoding是一种简单的数据压缩技术,它把句子中经常出现的字节pairs用一个没有出现的字节去替代。我们使用这种算法去分割单词,但我们合并字符或者字符序列。

算法步骤

算法步骤如下:

  • 初始化符号词表。用所有的字符加入到符号词表中。对所有单词的末尾加入特殊标记,如-。翻译后恢复原始的标记。
  • 迭代对所有符号进行计数,找出次数最多的(A, B),用AB代替。
  • 每次合并,会产生一个新的符号,代表着n-gram字符
  • 常见的n-grams字符(或者whole words),最终会被合并到一个符号
  • 最终符号词表大小=初始大小+合并操作次数。操作次数是算法唯一的超参数。

不用考虑不在训练集里面的pair,为每个word根据出现频率设置权重。

和传统的压缩算法(哈夫曼编码)相比,我们的以subword 单元堆积的符号序列依然是可以解释的网络也可以翻译和产生新的词汇(训练集没有见过的)。

下面是代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def process_raw_words(words, endtag='-'):
'''把单词分割成最小的符号,并且加上结尾符号'''
vocabs = {}
for word, count in words.items():
# 加上空格
word = re.sub(r'([a-zA-Z])', r' \1', word)
word += ' ' + endtag
vocabs[word] = count
return vocabs

def get_symbol_pairs(vocabs):
''' 获得词汇中所有的字符pair,连续长度为2,并统计出现次数
Args:
vocabs: 单词dict,(word, count)单词的出现次数。单词已经分割为最小的字符
Returns:
pairs: ((符号1, 符号2), count)
'''
#pairs = collections.defaultdict(int)
pairs = dict()
for word, freq in vocabs.items():
# 单词里的符号
symbols = word.split()
for i in range(len(symbols) - 1):
p = (symbols[i], symbols[i + 1])
pairs[p] = pairs.get(p, 0) + freq
return pairs

def merge_symbols(symbol_pair, vocabs):
'''把vocabs中的所有单词中的'a b'字符串用'ab'替换
Args:
symbol_pair: (a, b) 两个符号
vocabs: 用subword(symbol)表示的单词,(word, count)。其中word使用subword空格分割
Returns:
vocabs_new: 替换'a b'为'ab'的新词汇表
'''
vocabs_new = {}
raw = ' '.join(symbol_pair)
merged = ''.join(symbol_pair)
# 非字母和数字字符做转义
bigram = re.escape(raw)
p = re.compile(r'(?<!\S)' + bigram + r'(?!\S)')
for word, count in vocabs.items():
word_new = p.sub(merged, word)
vocabs_new[word_new] = count
return vocabs_new

raw_words = {"low":5, "lower":2, "newest":6, "widest":3}
vocabs = process_raw_words(raw_words)

num_merges = 10
print (vocabs)
for i in range(num_merges):
pairs = get_symbol_pairs(vocabs)
# 选择出现频率最高的pair
symbol_pair = max(pairs, key=pairs.get)
vocabs = merge_symbols(symbol_pair, vocabs)
print (vocabs)

单独BPE

为目标语言和原语言分别使用BPE去计算词典。从文本和词汇表大小来说更加紧凑,能保证每个subword单元在各自的训练数据上都有。同样的名字在不同的语言中可能切割的不一样,神经网络很难去学习subword units之间的映射。

Joint BPE

为目标语言和原语言一起使用BPE,即联合两种语言的词典去做BPE。提高了源语言和目标语言的分割一致性。训练中一般concat两种语言。

评估

有两个重要问题

  • subword units表达稀有词汇,是否真的对翻译有效果?
  • 根据词汇表大小,文本长度,翻译质量,怎样分割才是最好的?

我们的使用WMT2015的数据,使用BLEU来评判结果。英语-德语:

  • 420万句子对
  • 1亿个token

英语-俄罗斯语:

  • 260万句子对
  • 5000万个token

minibatch-size是80,每个epoch都会reshuffle训练数据。训练了7天,每12个小时存一次模型,取最后4个模型再单独训练。分别选择clip 梯度是5.0和1.0,1.0效果好一些。最终是融合了8个模型。

Beam search的大小是12,使用双语词典进行快速对齐,类似于对稀有词汇查找词典,也会用词典去加速训练。

Subword统计

我们的目标是通过一个紧凑的固定大小的subword词典去代表一个open-vocabulary,并且能够有效的训练和解码。

一个简单的基准就是把单词分割成字符n-grams 。n的选择很重要,可以在序列长度(tokens)和词汇表大小(size)之间做一个权衡。序列的长度会增加许多,一个比较好得减少长度的方法就是使用k个最常见的未被分割的词列表。只有unigram(n=1,一元模型)表达才能真正实现open-vocabulary,但是实际上效果却并不好。Bigram效果好,但是不能产生测试集中的tokens。

BPE符合open-vocabulary的目标,并且合并操作可以应用于测试集,去发现未知符号的分割。与字符集模型的主要区别在于,BPE更紧凑的表示较短序列,注意力模型可以应对变长的单元。

分割方法 tokens types unk merge次数
BPE 112 m 63000 0 59500
BPE(joint) 111 m 82000 32 89500

实际上,NMT词汇表中,并不会包含不常见的subword单元,因为里面有很多噪声。

name seg shotlist s-v t-v S-BLEU BLEU CHAR-F3 CHAR-F3 F1 F1 F1
BPE-60k BPE 60000 60000 21.5 24.5 52.0 53.9 58.4 40.9 29.3
BPE-J60k BPE(joint) 90000 90000 22.8 24.7 51.7 54.1 58.5 41.8 33.6

翻译评估

所有的subword系统都不会去查字典。使用UNK表示模型词典以外的单词,OOV表示训练集里面没有的单词。