对比 BPE 与 WordPiece 两种经典子词分割算法,解析 Tokenizer 在编码与解码阶段的词元切分、映射及词汇表构建机制,及其在预训练模型中的应用。
常见的子词分割方法:
- BPE(Byte-Pair Encoding):用于 GPT、GPT-2、RoBERTa、BART 和 DeBERTa 等模型。
- WordPiece:用于 DistilBERT、MobileBERT、Funnel Transformers 和 MPNET 等模型。
什么是 Tokenizer?
Tokenizer(分词器)可以将原始文本(raw text)转换为模型能够理解的数字序列,在模型输入和输出的两个主要阶段中发挥重要作用:
编码Encode阶段
分词(Tokenize)
将文本拆分为词元(Token),常见的分词方式包括字级、词级、子词级(如 BPE、WordPiece)、空格分词等。
1
2输入: "你好"
分词: ["你", "好"]
映射(Mapping)
将每个词元映射为词汇表中的唯一 ID,生成的数字序列即为模型的输入。
1
2分词: ["你", "好"]
映射: [1001, 1002]
解码Decode阶段
反映射(De-mapping)
模型输出的数字序列通过词汇表映射回对应的词元,二者是一一对应的关系。
1
2输出: [1001, 1002]
反映射: ["你", "好"]
文本重组
将解码后的词元以某种规则重新拼接为完整文本。
1
2反映射: ["你", "好"]
重组: "你好"
实际使用
在进一步讲解之前,我们先通过 Transformers 库中的 AutoTokenizer 类来使用 Tokenizer。
安装库
1 | pip install transformers |
BPE 分词器示例
1 | from transformers import AutoTokenizer |
输出:
1 | Tokens: ['Hello', ',', 'Ġworld', '!'] |
Note
实际上 GPT-2 用的是 Byte-level BPE,也就是从字符级处理变成了字节级,这样可以直接处理不同语言或者特殊的符号。
WordPiece 分词器示例
1 | from transformers import AutoTokenizer |
输出:
1 | Tokens: ['hello', ',', 'world', '!'] |
encode() 和 decode() 方法
更简洁且常见的使用方式是直接使用 encode() 和 decode() 方法:
1 | from transformers import AutoTokenizer |
输出:
1 | Token IDs: [15496, 11, 995, 0] |
构造词汇表
我们需要将语料库(corpus)的文本拆分为单词,假设当前语料库包含的单词和对应频次如下:
1 | ("low", 5), ("lower", 2), ("newest", 6), ("widest", 3) |
有些论文也用 vocab 来表述,知道后面是频次即可,命名不用纠结。
Byte-Pair Encoding (BPE)
参考文献:
- A new algorithm for data compression. 1994
- Neural Machine Translation of Rare Words with Subword Units. 2015
BPE 是一种基于数据压缩的技术,最早由 Gage 在 1994 年提出,后来被用于 GPT 等模型。它是一种子词分割算法,从字符级别开始,通过迭代合并频率最高的字符对(或字符序列)来构建新的 Token,从而可以处理部分 OOV(Out-Of-Vocabulary)情况。
Q: 什么是 OOV ?
其实就是不在词汇表中的词,也称之为「未登录词」。
BPE 每次的迭代目标是找到频率最高的相邻字符对,定义 Score 以与 WordPiece 作对比:
$$
Score_{BPE}(x,y)=freq(x,y)
$$
其中, $freq(x,y)$ 表示字符对 $(x,y)$ 在语料库中的出现频次。
步骤
- 初始化词汇表 V:
- V 包含语料库中的所有唯一字符,即单词字符的集合。
- 统计字符对的频次:
- 对于每个单词的字符序列,统计相邻字符对的出现频次。
- 找到频次(Score)最高的字符对并合并:
- 选择出现频率最高的字符对$(x,y)$,将其合并为新符号$xy$ 。
- 更新词汇表并重复步骤 2 到 4:
- 将新符号添加到词汇表 $V=V∪xy$。
- 更新语料库中的单词表示,重复统计和合并过程,直到满足停止条件(例如,词汇表达到预定大小)。
示例
步骤 1:初始化词汇表
将单词拆分为字符序列:
1
2
3
4("l", "o", "w"), 5
("l", "o", "w", "e", "r"), 2
("n", "e", "w", "e", "s", "t"), 6
("w", "i", "d", "e", "s", "t"), 3
词汇表 V:
1
{'l', 'o', 'w', 'e', 'r', 'n', 's', 't', 'i', 'd'}
步骤 2:统计字符对的频次
编写一个函数,根据给定的单词和其频次,自动统计字符对的频次。
1 | from collections import defaultdict |
输出:
1 | 字符对频次统计结果: |
步骤 3:找到频次最高的字符对并合并
选择频次最高的字符对:
("e", "s")和("s", "t"),频次均为 9。可以任选其一进行合并,假设选择排序第一的:("e", "s")。
合并
("e", "s")为新符号es。记录合并操作:
1
Merge 1: ("e", "s") -> "es"
步骤 4:更新词汇表并重复
更新单词序列:
1
2
3
4("l", "o", "w"), 5
("l", "o", "w", "e", "r"), 2
("n", "e", "w", "es", "t"), 6
("w", "i", "d", "es", "t"), 3
更新词汇表 V:
1
{'l', 'o', 'w', 'e', 'r', 'n', 's', 't', 'i', 'd', 'es'}
- 重复步骤 2 到 4,直到达到预定的词汇表大小。
WordPiece
参考文献:
- Japanese and Korean voice search. 2012
- Google’s Neural Machine Translation System: Bridging the Gap between Human and Machine Translation. 2016
WordPiece 是一种子词分割算法,最初用于处理日语和韩语的语音搜索,后来在 Google 的神经机器翻译系统中得到应用。
与 BPE 不同,WordPiece 的 Score 由字符对频次与其组成部分频次的比值决定,定义 Score:
$$
\text{Score}_{\text{WordPiece}}(x, y) = \frac{\text{freq}(xy)}{\text{freq}(x) \times \text{freq}(y)}
$$
其中, $freq(x)$, $freq(y)$ 和 $freq(xy)$ 分别表示符号 x, y 和它们合并后的符号 $xy$ 的频次。
步骤
- 初始化词汇表 V:
- 与 BPE 相同, V 包含语料库中的所有唯一字符,但处理方式略有不同:对于每个单词,除了首个字符外,其他字符前都加上
##前缀。
- 与 BPE 相同, V 包含语料库中的所有唯一字符,但处理方式略有不同:对于每个单词,除了首个字符外,其他字符前都加上
- 统计字符对的频次及 Score:
- 对于每个可能的字符对 (x,y),计算 freq(x), freq(y), freq(xy),并计算 Score。
- 找到 Score 最高的字符对并合并:
- 选择 Score 最高的字符对 (x,y),将其合并为新符号 xy,注意:
- 如果第二个符号以
##开头,合并时去掉##前缀再进行连接。 - 新符号是否以
##开头,取决于第一个符号是否以##开头。
- 如果第二个符号以
- 选择 Score 最高的字符对 (x,y),将其合并为新符号 xy,注意:
- 更新词汇表并重复步骤 2 到 4:
- 将新符号添加到词汇表 $V=V∪xy$。
- 更新语料库中的单词表示,重复统计和合并过程,直到满足停止条件。
示例
使用与 BPE 示例相同的语料库。
步骤 1:初始化词汇表
将单词拆分为字符序列:
1
2
3
4('l', '##o', '##w'), 5 # "low"
('l', '##o', '##w', '##e', '##r'), 2 # "lower"
('n', '##e', '##w', '##e', '##s', '##t'), 6 # "newest"
('w', '##i', '##d', '##e', '##s', '##t'), 3 # "widest"
词汇表 V:
1
{'l', '##o', '##w', '##e', '##r', 'n', '##s', '##t', 'w', '##i', '##d'}
步骤 2:统计字符和字符对的频次,计算 Score
可以设计一个函数完成这个步骤(直接运行查看输出):
1 | from collections import defaultdict |
输出:
1 | 字符对频次统计结果: |
选择频次最高的字符对:
('w', '##i')和('##i', '##d'),Score 都为 0.3333。可以任选其一进行合并,假设选择排序第一的:("w", "##i")。
合并
('w', '##i')为新符号wi- 注意:合并时,若第二个符号以
##开头,合并后的新符号为第一个符号加上第二个符号去掉##前缀的部分。
- 注意:合并时,若第二个符号以
记录合并操作:
1
Merge 1: ('w', '##i') -> 'wi'
步骤 4:更新词汇表并重复
更新词汇表 V:
1
{'l', '##o', '##w', '##e', '##r', 'n', '##s', '##t', 'w', '##i', '##d', 'wi'}
更新单词序列:
1
2
3
4('l', '##o', '##w'), 5 # "low"
('l', '##o', '##w', '##e', '##r'), 2 # "lower"
('n', '##e', '##w', '##e', '##s', '##t'), 6 # "newest"
('wi', '##d', '##e', '##s', '##t'), 3 # "widest"
- 重复步骤 2 到 4,直到达到预定的词汇表大小。
标记文本
每次合并时都会记录对应的 merge 规则,但并未详细说明其作用,下面将以 BPE 为例进行解释。
BPE
在之前的示例中,三轮合并后将得到以下合并规则(按合并顺序排列):
- 合并字符对
'e'和's',得到'es'。 - 合并字符对
'es'和't',得到'est'。 - 合并字符对
'l'和'o',得到'lo'。
假设当前词汇表包含所有单个字符,修改官方文档最后提供的 tokenize() 示例代码进行演示:
1 | def tokenize(text): |
输出:
1 | 初始预分词结果: |
不过,在之前的过程中生成的最终词汇表 V 并未包含所有单个字符,而是:
1 | {'e', 'r', 's', 'est', 'w', 'l', 'o', 'lo', 'es', 'i', 'n', 't', 'd'} |
因此,对于输入 "estimate, local",其标记结果为:
1 | ['est', 'i', '[UNK]', 'a', 't', 'e', '[UNK]', 'lo', '[UNK]', '[UNK]', l] |
这里的 '[UNK]'(UNKNOWN)表示该子词不在词汇表中,即属于 OOV(Out-of-Vocabulary) 的情况。
WordPiece
和 BPE 不同,WordPiece 对 OOV 采取的是「宁杀错不放过」策略,即只要有一个字符没见过,整个单词都标记为 '[UNK]'。
修改官方文档最后提供的 tokenize() 示例代码进行演示:
1 | from transformers import AutoTokenizer |
输出:
1 | 初始预分词结果: |
分词(Transformers)
在 Transformers 中,分词(tokenization) 实际上包含以下几个步骤:
- 标准化(Normalization):对文本进行必要的清理操作,例如删除多余空格或重音符号、进行 Unicode 标准化等。
- 预分词(Pre-tokenization):将输入拆分为单词。
- 通过模型处理输入(Running the input through the model):使用预分词后的单词生成一系列词元(tokens)。
- 后处理(Post-processing):添加分词器的特殊标记,生成注意力掩码(attention mask)和词元类型 ID(token type IDs)。
!整体流程图,图源
整体流程图,图源
运行代码:
1 | from transformers import AutoTokenizer |
输出
1 | 原始文本: Hello how are U tday |

