词表式分词器有一个结构性软肋:遇到词表外的字符串就得输出 <unk>,信息直接丢失。早期 NMT 系统里 <unk> 满天飞是常态,BERT 的 WordPiece 也没根本解决——生僻汉字、新造的 emoji、用户随手打的符号,都可能落进词表外。GPT-2 的改造把这个问题的根拔了:分词器不面对字符,只面对字节。本文讲这个设计怎么工作、聪明在哪、以及字节化之后仍然漏风的地方。
设计:初始词表就是 256 个字节
UTF-8 是满射:任何字符串都唯一对应一个字节序列,包括非法字符、半个 emoji、控制符。初始词表固定为 256 个单字节 token,BPE 合并全部在字节序列上进行。于是"可编码性"不再是问题——任何输入都能编码,最坏情况退化成字节序列,这就是 byte-level 设计的全部秘诀,一句话能说完,但它消灭的是整类故障。
编码结果的 token 直接就是字节串的合并:词表里的每个条目都是一段字节。解码端只需把 token 序列拼接还原成字节流、按 UTF-8 解码即可。合法的 UTF-8 输入必然合法解码;非法输入(比如被错误转码的老文件)也能原样编码原样解出字节,不会在分词器这里丢信息。
bytes_to_unicode:一个障眼法
GPT-2 的实现里有个一眼看不懂的细节 bytes_to_unicode()。直接把字节序列喂给 BPE 训练当然可行,但会引入一堆麻烦:字节 0–32 是控制字符(换行、Tab),在语料文件里会造成格式混乱;字节 128–255 不是合法的独立 UTF-8,直接写进文本文件会炸。GPT-2 的解法是做一个可逆映射,把 256 个字节值一一映射到 256 个可见、合法的 Unicode 字符上:
- 可打印字节(33–126、161–172、174–255)映射到自己;
- 其余字节(空白、控制符、UTF-8 高位字节)映射到第 129–256 号 Unicode 辅助区字符,如字节 32(空格)→
Ġ(U+0120),字节 195 →Ã(U+00C3)。
训练语料和词表文件看起来全是正常文本,BPE 在"字符"上照常工作;编码时再逆映射回字节。这个障眼法不产生任何语义影响,纯粹是工程卫生——分词器的数据文件可以安全地当文本读写。
一个具体例子串起整条链。字符串 héllo 的 UTF-8 编码是 68 C3 A9 6C 6C 6F:字母 h、l、o 各占一字节,é 占两字节(C3 A9)。映射后变成字符序列 h à © l l o(其中 à © 是 C3、A9 的替身)。BPE 学习时按频率合并,à © 稳定相邻会被合并成一个 token,词表里就出现" é 的 UTF-8 双字节形式"这个条目——解码时逆映射回 C3 A9,按 UTF-8 解出 é。整个过程对字节之外的语义一无所知,但效果恰好正确。
byte fallback:另一条到达同终点的路
SentencePiece 生态的 byte fallback 是同一个思想的另一实现:模型主体在 Unicode 字符 + ▁ 上工作,遇到无法编码的字符时,逐字节拆成 <0xNN> 特殊 piece(XLM-R 的标配)。效果等价——无 OOV、可逆——差别在默认行为:不开 fallback 的 SentencePiece 模型对词表外字符只能给 <unk>,鲁棒性要靠训练时显式打开开关。Llama 3 的词表走的是纯字节级 BPE 路线,格式兼容 SentencePiece 生态,鲁棒性上限和 GPT-2 系相同。
字节化之后,还漏什么风
字节级设计消灭的是"编码失败",不是"编码合理"。剩下的风险面至少有三处。
多语言膨胀。 CJK 字符 UTF-8 占 3 字节,词表没学到的字要 3 个 token 起步;韩文音节占 3 字节,泰文占 3 字节。字节级保证永远能编码,但稠密的 token 序列也是真金白银的成本——这正是大词表多语言模型存在的理由(3050 的账本)。
尾部合并条目的不可预测行为。 字节级词表尾部有大量来自低频语料的怪异合并(整个 URL、乱码串、罕见组合),它们的嵌入几乎没被训练过。这类 glitch token 平时沉睡,一旦被精确触发,模型的输出可能完全失控——这是字节级设计引入的新风险面,词表越大越严重,后面讲 tokenizer 安全时专门展开。
规范化真空。 字节级对 Unicode 规范化(NFC/NFKC)完全无感:视觉上相同的文本可能有不同字节序列,编码成不同 token。同形异义字符(用西里尔字母伪装拉丁字母的钓鱼文本)在字节层面天经地义地不同 token——这不算 bug,但它意味着"防混淆"必须在上游(归一化或安全过滤)做,分词器帮不上忙。
小结
byte-level 的核心不是性能优化,而是一次失败模式的删除:把"输入无法编码"从可能状态里划掉。256 字节起步 + 频率合并 + 可逆映射,让鲁棒性成为构造性质而非测试出来的性质。剩余的风险——膨胀、glitch token、规范化——都不在编码层,提醒我们:tokenizer 的鲁棒性有边界,边界之外要靠系统的其他层兜住。