Post

BPE

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)在源语言和目标语言的词汇表并集中进行编码,从而提高了源语言和目标语言在子词分割上的一致性。这对于跨语言的子词映射尤为重要。特别是在字符集不同的语言中(如英语和俄语)。

This post is licensed under CC BY 4.0 by the author.