第 19 章 深入分词:BPE、WordPiece、SentencePiece 与字节级方法
第 3 章给文本片段手工安排了 ID,足以解释查表,却没有回答怎样从语料中挑出有用的片段。整词词表可能已经很大,仍会遇到没见过的词;字符词表能共用更多片段,但有些序列会变长。我们需要一套可复现的规则,在这两端之间选择单位。
这不只是把 token 边界显示得不同。它决定 ID 选中哪一行嵌入、上下文有多长,以及输出层究竟预测什么。模型训练之后再换 tokenizer,就改变了模型接口。本章先手工学习四次合并,再为第 20—21 章固定另一套有明确版本的字节分词器。第 8 章对原文与规范化检索键的区分仍然重要:生成模型不必沿用知识助手的检索规范化。
19.1 数相邻符号以前,先说明边界和并列规则
Section titled “19.1 数相邻符号以前,先说明边界和并列规则”例 19.1:学习四次 BPE 合并。 本例的语料只有四个词:low、low、lower、new,按词频保存为 2、1、1。每个词先拆为单个字符,再追加一个特殊词尾符号 </w>。这个标记是一个符号,不是四个普通字符,用来保留词在哪结束。相邻对不能跨越词边界。本例从已经分好的词出发,不承诺保存原词之间的空白形式。
初始符号表按比较顺序列为 [</w>,e,l,n,o,r,w]。每一步都统计 当前符号 的相邻对,并乘上所在词的频次。选择出现最多的一对;如果并列,按符号字符串的字典序选最小的一对,先比第一个字符串,再比第二个。然后从左到右,把这对符号的非重叠出现合成一个新符号,重新计数。做满四次合并就停止。并列规则和停止规则是本例规定,不是所有 BPE 的默认规则。
表 19.1:第一次合并前的相邻对计数。
| 相邻对 | 次数 | 出现在哪里 |
|---|---|---|
l + o | 3 | low 两次,lower 一次 |
o + w | 3 | low 两次,lower 一次 |
w + </w> | 3 | low 两次,new 一次 |
w + e | 1 | lower |
e + r | 1 | lower |
r + </w> | 1 | lower |
n + e | 1 | new |
e + w | 1 | new |
有三对都出现 3 次。l 排在 o、w 前面,所以先合并 l + o,得到 lo。更新后的切分中,lo + w 与 w + </w> 都出现 3 次;lo 在 w 前面,因此第二次合并得到 low。此后,low + </w> 出现两次,其余相邻对都只有一次,所以它成为第三次合并。到第四步,剩下的相邻对次数全是 1;e + r 排在 e + w 以及其他首符号的选择之前,因此胜出。
表 19.2:每次合并,以及每个词的完整中间状态。 三个词的频次始终为 2、1、1。
| 步骤 | 选中的相邻对;次数 | low × 2 | lower × 1 | new × 1 |
|---|---|---|---|---|
| 0 | 初始符号 | l o w </w> | l o w e r </w> | n e w </w> |
| 1 | l + o;3 | lo w </w> | lo w e r </w> | n e w </w> |
| 2 | lo + w;3 | low </w> | low e r </w> | n e w </w> |
| 3 | low + </w>;2 | low</w> | low e r </w> | n e w </w> |
| 4 | e + r;1 | low</w> | low er </w> | n e w </w> |
这就是 字节对编码(byte-pair encoding,简称 BPE) 在字符符号上的子词切分用法。名称来自早期压缩方法,不表示现代所有 BPE 都从原始字节开始。本例每次合并增加一个片段,初始有七个符号,四次之后共有十一个;旧片段仍保留,可以用于别的词。字符子词 BPE,第 3.2 节。
- 一组数字(l,o):3;(o,w):3;(w,</w>):3
- 处理步骤按字典序决胜 → 合并 l+o
l+o 中,low 贡献两次,lower 贡献一次。
- 处理步骤(lo,w):3 → low
- 处理步骤(low,</w>):2 → low</w>
当前 lo 已是一个符号,不能再数其内部的 l+o。
- 处理步骤(e,r) 排在 (e,w) 前 → er
- 文本片段low</w> ×2;low er </w>;n e w </w>
编码新词之前,固定这四条有顺序的规则。
图 19.1 说明了为什么合并后必须重新计数。统计对象是相邻的 符号,不是合成符号内部藏着的所有字符子串。low 即使变成一个符号,词频也仍是 2。
19.2 用学到的规则处理新文本
Section titled “19.2 用学到的规则处理新文本”学习词表与编码另一条输入,是两件事。先冻结带顺序的合并列表。编码 lower 时,从 l o w e r </w> 开始,先合并 l+o,再合并 lo+w。由于 low 后面是 e,low+</w> 这一步不适用;再合并 e+r,最终为 low er </w>。不能在新输入上重新数相邻对、临时创造另一套顺序。合并排名也是分词器资产的一部分。
边界同样影响结果。low 使用的是带词尾的 low</w>,lower 开头用的则是不带词尾的 low。拼接片段内容、移除规定的词尾标记,可以恢复词本身;但恢复不了原来两个词之间是制表符还是三个空格,因为 BPE 开始以前,那份信息就已经丢了。
没见过的词如果完全由已知初始字符组成,仍可能表示出来。但没见过的字符是另一件事:这个小符号表没有雪人 ☃。若规定用未知 token 回退,就会得到 [UNK];只凭 [UNK],无法知道原来是哪个未知字符。“使用子词”本身不保证覆盖全部内容,也不保证精确还原。
19.3 WordPiece 与 SentencePiece 并不是同一层级的选择
Section titled “19.3 WordPiece 与 SentencePiece 并不是同一层级的选择”例 19.2:给定一份 WordPiece 词表。 词表只有 [UNK]、low、##er、new。## 标记这是词中间的续接片段,不是原词里的井号。BERT 官方编码过程会从当前偏移处寻找词表中最长的匹配。lower 没有整词匹配,但开头能匹配 low,剩下的 er 匹配 ##er,所以得到 [low,##er]。对 lowerz,末尾 z 无法覆盖,这个实现会把 整个词 变成 [UNK],不是保留 [low,##er,UNK]。
刚才说的是已知词表以后的编码规则。WordPiece 的词表学习与改善语言模型似然的目标有关,也就是提高模型给已观察到的训练文本分配的概率;本例 BPE 则选择相邻对计数最大者。BERT 官方分词文件没有包含词表训练器,因此不能把流行的某种相邻对打分简式称为唯一的原始 WordPiece 训练算法。在子词编码之前,它的基础分词还可能根据配置处理大小写、重音、空白或中日韩字符边界。本例只隔离出词片段阶段,不是完整 BERT 文本流水线。GNMT,第 4.1 节、BERT 官方分词实现。
SentencePiece 则是一套提供规范化、词表训练、编码和解码的软件框架。它可以训练 BPE,也可以训练一元(unigram)子词模型等其他模型。一元模型为片段赋予概率,并对可能的切分评分,不是刚才按排名执行的合并过程。因此,“BPE 与 SentencePiece 二选一”混淆了算法与工具:一个分词器完全可以使用 BPE 算法,并由 SentencePiece 训练。
SentencePiece 可以从原始句子出发,也会明确表示空白。论文中的还原关系针对的是 规范化后的 输入。核验版本的默认规范化基于 NFKC,实际配置和覆盖范围仍然重要;这不等于在所有设置下都能恢复每个原始码点、处理所有输入。具体实现说明和核验版本放在研究记录中,本章不声称测过 SentencePiece 的性能。SentencePiece,第 2—3 节、官方版本文档。
19.4 需要精确保留文本时,从字节开始
Section titled “19.4 需要精确保留文本时,从字节开始”第 8 章区分了 Unicode 码点与存储它的字节。UTF-8 中,中 占三个字节 [228,184,173],🙂 占四个 [240,159,153,130]。如果 256 种字节值各有一个基础 token,那么一次合并还没学,每一份有效 UTF-8 输入就已经有了表示方式。罕见字符可能占好几个 token;能表示,不等于懂得含义。
例 19.3:固定 Mini GPT 的字节 BPE。 共享分词器 book-byte-bpe-v1 把 ID 0—255 分别对应到这些字节值。256、257、258 预留给 BOS、EOS、PAD。它只从 24 条训练记录 学习 32 次合并,既不跨记录,也不跨特殊 token。每个新片段依次分配新 ID,从 259 开始,最终词表有 291 项。不做规范化、大小写转换、空白切词,也不预先按标点切分。原文中真的写了 BOS,仍按字节编码;特殊 BOS ID 由构造记录的程序另行加入。
这里的并列规则是:次数最高者优先,其次选择最小的 整数 ID 有序对,而非例 19.1 的符号字符串顺序。这是另一套单独命名的分词器,没有悄悄替换前面词边界例子的规则。第一次合并是字节 58 与 32,即冒号加空格,共出现 25 次,新 ID 259 表示 : 。之后,ID 268 表示 key,270 表示 red,282 表示 750。完整 32 对合并的排名与次数直接保存在 data/mini-gpt/tokenizer.json,编码时按顺序重放这些规则。
表 19.3:同一份冻结分词器的实际编码。 表中都没有自动添加特殊 token,因此计数也不包含它们。
| 精确输入 | 码点数 | UTF-8 字节数/基础字节 token 数 | BPE token 数 | 是否还原原输入? |
|---|---|---|---|---|
red key . | 9 | 9 | 5 | 是 |
法国首都是巴黎。 | 8 | 24 | 24 | 是 |
中 | 1 | 3 | 3 | 是 |
🙂 | 1 | 4 | 4 | 是 |
| e 后接 U+0301 | 2 | 3 | 3 | 是 |
é(U+00E9) | 1 | 2 | 2 | 是 |
全角 A、空格、A:A A | 3 | 5 | 5 | 是 |
第一条变为 [270,32,268,32,46],依次表示 red、空格、key、空格、句号。中 仍是 [228,184,173],表情仍是 [240,159,153,130]。语料中的中文很少,这份 32 次合并预算没有选中中文的字节合并。这里只比较固定语料下,同一字符串的两种编码长度,不能据此推定普遍的中英文 token 比例。另一条固定往返测试还保留了两个开头空格、内部连续空格和末尾换行。
- 文本片段red key .
- 行编号[270, 32, 268, 32, 46]
- 处理步骤red + 空格 + key + 空格 + .
五个 token,原空格精确保留。
- 文本片段中 → 字节 E4 B8 AD
- 行编号[228, 184, 173]
- 处理步骤拼接字节 → 严格 UTF-8 → 中
一个码点占三个 token,不能逐 token 独立解码。
- 行编号生成 ID 255 → 字节 FF
- 处理步骤严格 UTF-8 → UnicodeDecodeError
覆盖所有输入,不保证任意生成字节都能构成有效文本。
图 19.2 沿着实际字节和 ID 还原文本。一个 token 的字节内容,可能在多字节字符的中间结束。解码完整输入时,要先把全部普通 token 的字节拼接起来,再把整体按 UTF-8 解码。若试图把每个字节 token 单独当作一个字符解码,中 就无法完成。
文本前缀也未必是 token 前缀。这个字节分词器把末尾单独的冒号编码为 ID 58,冒号后跟一个空格则合为 ID 259。提示恰好停在空格以前,相比完整编码的训练记录,最后一个输入 token 就变了。精确往返不会消除这种边界效应。第 21 章会保留原失败提示,再补充边界对齐的比较;生成程序本身只追加已选 ID,不会把不断增长的整段文本重新编码。
19.5 序列更短,不代表接口一定更好
Section titled “19.5 序列更短,不代表接口一定更好”例 19.4:两种往返失败,原因不同。 外观相近的 e 加 U+0301 与预组合字符 é,原始码点序列不同。无规范化的字节分词器能分别精确保留。若先做 NFC,两者都变成 é,只凭规范化结果便无法恢复第一种写法。这是专门演示的规范化信息损失,不是 SentencePiece 的实测失败。相比之下,把 ☃ 变成 [UNK],是覆盖不足导致的信息丢失。把原文另存一份,可以保留原始记录,但不会让 token 序列本身变得可逆。
覆盖全部字节,也不保证生成结果一定是有效文本。输入编码器产生的,是某个有效字符串的字节;随机选出来的输出 token 则未必遵守 UTF-8。单独的基础 token 255 对应字节 FF,在 UTF-8 中不能独立构成合法文本。严格解码器会抛出 UnicodeDecodeError。生成记录必须说明遇到这种情况是停止、返回原始字节,还是显示替代字符。如果默默打印一个替代符号,就掩盖了精确解码与显示回退的区别。
增加合并次数,可能缩短常见字符串,但更大的词表会扩展嵌入表和词表输出矩阵。设模型宽度为 C,词表有 V 项,不共享权重的输入表与输出矩阵合计有 个标量权重,未计偏置。C=16 时,从 259 项增加到 291 项,就增加 个权重。注意力分数存储量随序列长度的平方变化,序列缩短可能节省那部分计算,但词表变大会增加别处的成本。罕见合成片段能获得的训练观察也更少。token 更少、编码完全可逆,都不等于任务准确率更高。
因此,版本记录是模型接口的要求。需要保存初始符号与 ID 顺序、合并排名、特殊 ID、规范化、边界规则、训练器版本和训练数据身份。只保留词表大小远远不够:交换 red 与 key 的 ID,所有 shape 仍正确,但会选错嵌入行,也会把输出分数标到错误的候选上。检查点必须匹配分词器的精确身份;若要迁移,就要有意重排相应模型行列和受影响的数据。
19.6 先复现接口,再训练模型
Section titled “19.6 先复现接口,再训练模型”共享 CPU 程序 code/part-iv/tokenizer_example.py 会计算手工合并过程,与独立确定的相邻对、次数和切分结果逐项核对;随后检查给定 WordPiece 词表,并使用 code/mini-gpt/tokenizer.py 做严格往返测试。后者负责训练小型字节分词器,保存完整资产。data/part-iv/tokenizer-run.json 留下实际输出、码点/字节/token 数,以及规范化反例。两份程序都不下载词表或模型。
这份冻结分词器的规范序列化 SHA-256 标识是 9bb146ce375bda3aa518a48a60d9f69611b8d0afde9df9a8298eae9dfa9dd354。长字符串用于复现时确认身份,不需要背诵。数据说明规定了规范序列化方式,它与直接对排版后的 JSON 文件求哈希不同。第 20 章会把这个身份与数据集、嵌入表和语言模型输出层绑定。
19.7 分词器已经建立了什么
Section titled “19.7 分词器已经建立了什么”我们现在能够解释,相邻对规则怎样形成词表,冻结规则怎样编码另一条输入,以及工具、算法、初始符号和规范化为什么是几类不同的选择。Mini GPT 的接口能覆盖全部字节,也已经通过精确还原输入的测试。它还没有学会语言,只是根据训练文本选好了表示单位。
下一步要把每条记录变成输入序列和错开一位的目标,再沿着这些 ID 走过完整的块,得到 logits 与损失。这就是第 20 章的 MG-1。
19.8 习题
Section titled “19.8 习题”- 把例 19.1 中 new 的词频从 1 改为 2。重算可能胜出的初始相邻对次数,按原并列规则选出第一次合并。为什么不能直接复用原来的第一步?
- 原来的四次合并完成以后,分别编码
low和lower。新遇到一次 lower 会改变合并排名吗?这套按词处理的方法恢复不了哪些原始空白信息? - 用给定 WordPiece 词表编码
lowerz。为什么 BERT 官方实现的规则不同于“保留成功前缀,再加未知后缀”? - 精确往返与 token 更少,能否证明中文处理得好?用字节例子区分覆盖能力、序列长度和学到的任务行为。
- 新分词器仍有 291 项,但交换 ID 268 与 270。哪些模型或数据接口受影响?为什么 shape 检查会通过?
- 生成器输出 ID 255。它在这里表示什么字节?如果界面显示了替代字符,忠实的结果记录应该怎样说明?
19.9 参考解答
Section titled “19.9 参考解答”w + </w>在 low 中出现两次,在 new 中也出现两次,共 4 次;l + o、o + w仍各 3 次。因此先合并w + </w>。原来的第一步依据是三个计数都为 3,改变词频就改变了这个前提。- Low 为
[low</w>],lower 为[low,er,</w>]。编码重放已经固定排名的规则,不会用这次新出现再训练。原词之间是制表符、多个空格还是其他分隔方式,在构造按词记录以前就已经丢了。 - 因为 z 无法继续切分,整个词返回
[UNK]。保留[low,##er,UNK]是另一套回退约定,会得到不同 ID 和还原行为。本例不表示所有 WordPiece 实现的预处理与回退规则都必须一致。 - 覆盖能力说明输入有表示方式;往返说明在规定的规范化条件下能够还原;序列长度衡量这条具体字符串用了多少单位。任务行为还需要训练好的模型和受控评测。本例中文用 24 个字节 token 往返成功,频繁的英文钥匙短语则被压缩,两者都不足以比较理解能力。
- 已保存的数据集和提示需要一致地重新编码。若要保留旧函数,还须交换对应输入嵌入行、输出词表列及输出偏置。特殊 ID、合并引用和资产身份也必须相互一致。维度仍为 291,所以 shape 断言察觉不到含义交换。
- 它表示字节 FF,单独存在时不是合法 UTF-8 字符串,严格解码会失败。显示回退可以给出替代字符,但记录应保留原始字节与 token ID,并明确使用了替代解码,不能声称模型原本生成了那个有效字符。
19.10 参考文献
Section titled “19.10 参考文献”- Sennrich、Haddow、Birch(2016),Neural Machine Translation of Rare Words with Subword Units。第 3.2 节说明带词边界、按频次计数的字符 BPE。本章语料和并列规则均为明确给定的原创例子。
- Wu 等(2016),Google’s Neural Machine Translation System,第 4.1 节;Google Research,BERT 分词实现。前者讨论 WordPiece 词表学习,后者实现给定词表上的最长匹配与整词未知回退。
- Kudo、Richardson(2018),SentencePiece,第 2—3 节;SentencePiece v0.2.1 文档。解释软件组件、可选算法、空白与规范化约定。
- Kudo(2018),Subword Regularization,第 3 节。介绍 unigram 切分模型,有助于区分另一种算法与容纳它的 SentencePiece 工具。