BPE
Neural Machine Translation of Rare Words with Subword Units
在自然语言处理(NLP)任务中,稀有词汇和开放词汇表问题是影响机器翻译、语言模型性能的关键挑战之一,而机器翻译是一个开放词汇的问题。为了解决这一问题,Byte Pair Encoding (BPE) 被引入到神经机器翻译(NMT)领域,用于将单词分解为子词单元,减少词汇表外(Out-Of-Vocabulary, OOV)问题的发生。BPE 通过逐步合并频繁的字符对或子词对,构建一个动态的子词词汇表,并能够灵活地处理开放词汇表。
背景
Byte Pair Encoding (BPE) 最初是一种数据压缩算法,由 Gage 于 1994 年提出。其核心思想是通过迭代地合并文本中的常见字符对,将整个文本表示为更紧凑的字节序列。在自然语言处理中,BPE 被改进为一种子词级别的编码方法,能够根据语料中的频率信息将单词拆分或合并为子词。
在自然语言处理任务中,BPE 的主要优势是可以动态调整词汇表的大小,平衡了模型复杂性和词汇覆盖率,特别是在处理形态复杂的语言(如德语、土耳其语)时表现出色。
BPE 工作原理
初始化词汇表
将符号词汇表初始化为基本字符词汇表。则每个单词被分解为一个由字符组成的序列,同时在每个单词的末尾添加一个特殊的结束符号(‘·’
,或类似符号)。这个结束符号的作用是在翻译或编码完成后,能够准确地还原单词的原始分词形式。
每个单词在被输入 BPE 算法时都会被拆分为单个字符。例如,单词 “hello” 会被表示为:h e l l o ·
。这里每个字母都是一个独立的符号,结尾的 ‘·’
表示这是单词的结束。
这个特殊的结束符号 ‘·’
(或类似的符号 </w>
)是为了确保在后续的子词合并过程中,我们可以正确地处理单词边界。这是因为 BPE 会合并相邻的字符对,而我们不希望错误地跨越单词边界进行合并。例如,如果没有这个结束符号,可能会错误地将两个不同单词的末尾和开头字符合并。结束符号帮助确保当我们合并字符时,不会影响单词的完整性。在翻译结束时,结束符号还可以帮助我们准确地还原每个单词的原始形式。
统计字符对频率
接下来,BPE 会统计词汇表中所有相邻字符对的出现频率。例如,假设初始词汇表如下:
1
2
3
4
5
6
vocab = {
'l o w </w>': 5,
'l o w e r </w>': 2,
'n e w e s t </w>': 6,
'w i d e s t </w>': 3
}
此时,BPE 会统计所有相邻字符对的出现频率,比如:
1
2
3
4
('l', 'o'): 7
('o', 'w'): 7
('w', '</w>'): 5
...
合并最频繁的字符对
BPE 将统计的字符对按频率排序,选择最频繁的字符对进行合并。每次合并后,会更新词汇表中所有出现该字符对的地方。例如,假设最频繁的字符对是 ('l', 'o')
,则将其合并为一个新的符号 "lo"
,词汇表更新为:
1
2
3
4
5
6
vocab = {
'lo w </w>': 5,
'lo w e r </w>': 2,
'n e w e s t </w>': 6,
'w i d e s t </w>': 3
}
重复合并操作
BPE 算法会不断重复合并最频繁的字符对,直到达到指定的合并次数或无法进一步合并为止。每次合并后,词汇表都会变得更加紧凑,但依然保留单词的原始语义信息。
最终词汇表
最终词汇表等于初始词汇表的大小,加上合并操作的数量。而合并操作数是 BPE 的唯一超参数。
原始代码
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
import re, collections
def get_stats(vocab):
pairs = collections.defaultdict(int)
for word, freq in vocab.items():
symbols = word.split()
for i in range(len(symbols) - 1):
pairs[symbols[i], symbols[i + 1]] += freq
return pairs
def merge_vocab(pair, v_in):
v_out = {}
bigram = re.escape(' '.join(pair))
p = re.compile(r'(?<!\S)' + bigram + r'(?!\S)')
for word in v_in:
w_out = p.sub(''.join(pair), word)
v_out[w_out] = v_in[word]
return v_out
vocab = {'l o w </w>': 5, 'l o w e r </w>': 2,
'n e w e r </w>': 6, 'w i d e r </w>': 3}
num_merges = 10
for i in range(num_merges):
pairs = get_stats(vocab)
best = max(pairs, key=pairs.get)
vocab = merge_vocab(best, vocab)
print(best)
那么,示例中的合并规则为
\[\begin{aligned} r \ \cdot &\ \rightarrow \ r\cdot \\ l \ o &\ \rightarrow \ lo \\ lo \ w &\ \rightarrow \ low \\ e \ r \ \cdot &\ \rightarrow \ er\cdot \\ \end{aligned}\]应用
独立编码
独立编码方法分别对源语言和目标语言的词汇表进行子词编码,其主要优势在于能够更紧凑地处理文本和词汇表,同时保证每个子词单元都在对应语言的训练数据中被看到过。然而,这种方法可能会导致同一个词(例如人名)在不同语言中的分割方式不一致,进而使得神经模型更难学习子词单元之间的映射。
联合编码
联合编码(也成为 Joint BPE
)在源语言和目标语言的词汇表并集中进行编码,从而提高了源语言和目标语言在子词分割上的一致性。这对于跨语言的子词映射尤为重要。特别是在字符集不同的语言中(如英语和俄语)。