系统讲解分词器:编码与解码两个阶段中分词、映射、反映射与文本重组的四步流程,对比 GPT 系列常用的 BPE 与 BERT 系列常用的 WordPiece,并给出使用 GPT-2 分词器做编码解码的代码示例。
常见的子词分割方法:
- BPE(Byte-Pair Encoding):用于 GPT、GPT-2、RoBERTa、BART 和 DeBERTa 等模型。
- WordPiece:用于 DistilBERT、MobileBERT、Funnel Transformers 和 MPNET 等模型。
什么是 Tokenizer?
Tokenizer(分词器)可以将原始文本(raw text)转换为模型能够理解的数字序列,在模型输入和输出的两个主要阶段中发挥重要作用:
编码Encode阶段
在进一步讲解之前,我们先通过 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)
参考文献:
BPE 每次的迭代目标是找到频率最高的相邻字符对,定义 Score 以与 WordPiece 作对比:
$$
Score_{BPE}(x,y)=freq(x,y)
$$
其中, $freq(x,y)$ 表示字符对$ (x,y) $在语料库中的出现频次。
步骤
- 初始化词汇表 V:
- 统计字符对的频次:
- 找到频次(Score)最高的字符对并合并:
- 更新词汇表并重复步骤 2 到 4:
示例
步骤 1:初始化词汇表
- 将单词拆分为字符序列:
- 词汇表 V:
步骤 2:统计字符对的频次
编写一个函数,根据给定的单词和其频次,自动统计字符对的频次。
1 | from collections import defaultdict |
输出:
1 | 字符对频次统计结果: |
步骤 3:找到频次最高的字符对并合并
- 选择频次最高的字符对:
- 合并 **
("e", "s")** 为新符号 **es**。 - 记录合并操作:
步骤 4:更新词汇表并重复
参考文献:
与 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:
- 统计字符对的频次及 Score:
- 找到 Score 最高的字符对并合并:
- 更新词汇表并重复步骤 2 到 4:
示例
使用与 BPE 示例相同的语料库。
步骤 1:初始化词汇表
- 将单词拆分为字符序列:
- 词汇表 V:
步骤 2:统计字符和字符对的频次,计算 Score
可以设计一个函数完成这个步骤(直接运行查看输出):
1 | from collections import defaultdict |
输出:
1 | 字符对频次统计结果: |
- 选择频次最高的字符对:
- 合并 **
('w', '##i')** 为新符号 **wi** - 记录合并操作:
步骤 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)。
](/images/loading.gif)
运行代码:
1 | from transformers import AutoTokenizer |
输出
1 | 原始文本: Hello how are U tday |

