BPE 怎么学会「该怎么切」?
本课一问:BPE 怎么学会「该怎么切」?
这一课先不碰:为什么初始单元是字节而不是字(岔路课「为什么中文更贵」)、词表该定多大(第 4 课)。只看 BPE 的训练规则,以及它在真语料上一步步做了什么。
子词切分:哪些块算「常见」?
第 2 课的结论是子词切分:常见的字串整块进词表,罕见的拆成碎片。贯穿例句「小明把苹果给了小王」在 Qwen3.8 里是 5 块:小明 / 把 / 苹果 / 给了 / 小王。剩下一个问题——谁来决定「小明」「苹果」「给了」是整块?
朴素的想法:人来规定怎么切
最直接的办法是人来定:写一套切分规则,或者编一本词典,词典里有的就整块切。看看我们自己那份不大的训练语料(三国演义),要为多少种「块」做决定:
- ① 朴素方案
- ② 它在哪失败
- ③ 改进方案
不让人判断,让频次判断:从最小的单元出发,反复找「语料里最常挨在一起的一对」,把它粘成一个新单元。哪些字串最后变成整块,完全由它在这份语料里出现得多不多决定。这就是 BPE(Byte Pair Encoding)。
BPE 的四步规则
本课在演进图里的位置:第一代子词切分。
- 解决了不用训练,任何文字都能编号。卡在码点从 0 排到 1,114,111,embedding 表得开 1,114,112 行,是 Qwen3.8 实际 248,320 行的 4.5 倍;而且一个字一个 ID,序列一点不缩短。
- 解决了字符集封闭,词表小。卡在序列最长:209 字的段落按字切要做 43,681 次注意力打分,是 Qwen3.8 子词切分(106 个 token)的 3.9 倍。
- 解决了一个词一块,序列短。卡在词是开放集合:我们的语料按空白和标点切出 70,310 种块,94.8% 只出现过一次;验证集里连训练集没见过的字都有 83 种——含它们的词只能记成 UNK。
- 解决了按频次反复合并相邻对:常见的整块、罕见的拆碎,词表大小由合并次数控制。卡在初始单元是字符:要覆盖全部 Unicode,基础词表就超过 13 万(GPT-2 论文 §2.2);只收见过的字,我们的验证集照样缺 83 种字。
- 解决了初始词表就是 256 个字节,任何文字都能编码,永远不出 UNK。卡在词表没学过的语言会被切碎:同义的中文段落,GPT-2 切 461 个 token,Qwen3.8 切 106 个,差 4.3 倍;同义英文段落两者一样多。
- 解决了中文常用字串整块进词表:「我喜欢机器学习」只要 2 个 token(GPT-2 要 17 个)。各家词表:DeepSeek-V3 129,280 · Qwen3 151,936 · GLM-5.3 154,880 · Kimi K3 163,840 · Qwen3.8 248,320。卡在两头各一张 V × d 的表:2 × 248,320 × 5,120 = 2,542,796,800 个参数,占 Qwen3.8 的 9.2%;同样的词表搬到我们 d = 256 的小模型上,embedding 占 95.3%。
- 解决了ID 只是行号,取出一行 5,120 维向量;one-hot 乘矩阵与查表等价,但查表只拷现成的数。卡在表里的 1,271,398,400 个数(输入端一张表)从哪来?人填不完,也不知道每一维该是什么意思。
- 解决了用「预测下一个 token」的 loss 端到端训练,用法相近的 token 被推到相近的位置。卡在只在出现时才被推:我们的项目里一个 batch 8,192 个位置,平均只出现 1,453 个不同 token;罕见 token 学得少——MiniMax 的「嘉祺」在后训练里被推偏,输出层那一行与原来的余弦只剩 0.329。
- 解决了维度越高,每个 token 能装的信息越多。卡在参数按 d² 涨:我们的模型 d 从 128 到 256,总参数 1,066,112 → 3,688,704,val loss 4.776 → 4.641(d = 256 这一档是参照 run ref_baseline,与项目基线同配置同种子;基线记录的 4.626 与它之差是重跑波动,见 C14);没有公式能给出「最优的 d」。
BPE 的训练只有一条规则,重复执行:
1. 把语料全部拆成最小单元,作为初始词表(字节级 BPE 用 256 个字节);
2. 统计所有相邻单元对的出现次数;
3. 把出现最多的那一对合并成一个新单元,加进词表,并把语料里所有这一对都替换成新单元;
4. 回到第 2 步,直到词表达到设定的大小——这是你来定的超参数。
🔍 BPE 的来历
BPE 不是为 NLP 发明的。它是 Philip Gage 1994 年发表的一个数据压缩算法(《A New Algorithm for Data Compression》,C Users Journal 1994 年 2 月,网页存档):反复把最常见的一对字节替换成一个没用过的字节。Sennrich、Haddow、Birch 2015 年把它改造成子词切分(本地 PDF · arXiv:1508.07909,ACL 2016),要解决的是机器翻译里的罕见词:词表装不下所有词,没见过的词只能输出 UNK。论文 §3.2 的原话要点:最终词表大小 = 初始词表大小 + 合并次数,合并次数是这个算法唯一的超参数。注意归属:BPE 算法不是 Sennrich 发明的,他们的贡献是把它用来切词。
训练完得到两样东西:词表(每个单元一个 ID)和合并规则表(按学到的先后排好)。之后切任何新文本,都是按同样的先后顺序套用合并规则——哪怕这个词训练时从没见过,也能被切成词表里已有的块。
先在 4 个英文词的小语料上亲手跑一遍真 BPE,每点一次就执行一步:
语料只有 4 个词(右上角是出现次数)。每点一次「执行合并」,就执行一步真正的 BPE 训练:先看下面的相邻对统计表,最高频的一对(紫色行)将被合并。
| 相邻对 | 出现次数 |
|---|
lowest →
小语料上看清了规则,再看真语料。下面是我们项目的分词器在三国演义上的前 30 步真实合并,每一步合并了哪一对、这一对出现了多少次、合并后语料还剩多少 token,全部来自训练日志:
| 步 | 合并哪一对 | 新 token | 这一对出现次数 | 合并后语料 token 数 |
|---|
注意看两件小语料上看不到的事:① 前几步合并的多是半个字——汉字和全角标点的 UTF-8 编码是 3 个字节,BPE 先把最常见的前两个字节粘起来,再和第三个字节拼成完整的字(为什么是字节,岔路课「为什么中文更贵」讲);② 压轴的输入框里,语料里没有的简体现代句会被切得很碎,拼不出的字停在字节层面——分词器只认识它统计过的东西。
常见误解:「词表是人编的一本字典」。 词表里的每一项,都是某一步合并的产物;哪一步合并谁,由那一刻语料里的频次决定。人只定了两件事:在什么语料上训练、合并到多大为止。所以同一套算法、不同的语料,会长出完全不同的词表——GPT-2 与 Qwen3.8 切中文差几倍,根源就在这里。
🔍 为什么先切块、再在块里合并
GPT-2 论文 §2.2 观察到:直接在整段字节上跑 BPE,贪心的频次合并会造出 dog.、dog!、dog? 这类「词 + 标点」的 token,白白占掉词表名额。所以它规定不跨字符类别合并(空格除外)——实现上就是先用一条正则把文本切成「字母串 / 数字串 / 标点串 / 空白」这样的块,合并只在块内发生。我们的项目分词器用的是同一条规则(minillm/tokenizer.py 的 GPT2_SPLIT_PATTERN);Qwen3.8 的正则还把数字一位一位切开。(GPT-2 论文本地 PDF)
🔍 我们踩过的坑:正则漏了下划线
项目分词器最初把 GPT-2 的正则改写成 Python 标准库能用的写法时,字母类写成 [^\W\d_]、标点类写成 [^\s\w]——两类都没有包含下划线。结果下划线不属于任何一类,预切分时被静默跳过:decode(encode("snake_case")) 得到的是 snakecase。项目的往返测试照样通过,因为测试语料三国演义里一个下划线都没有。修法是把标点类改成 (?:[^\s\w]|_)+,并在测试里补一条含下划线的样例(2026-09-26 修复)。教训:第 1 课说字节级 BPE 的往返可逆「可以证明」,证明的前提是预切分不丢字符;测试只能覆盖测试数据里出现过的字符类别。
一步合并,拆开看
用模拟器里的小语料(low ×5、lower ×2、newest ×6、widest ×3)算第一步。点公式里每一项:
试三件事: ① 模拟器里点 4 次合并,再点「切分 lowest」,看它被切成哪两块;② 真实日志里一键跑完 30 步,数一数「半个字」「一个完整字符」「多个字符」各占几步;③ 压轴输入框里切「小明把苹果给了小王。」,再切一句三国原文,比较虚线框(半个字)的数量——这就是训练语料决定词表的现场证据。
BPE 凭什么成了标准做法
- 它解决什么问题:不靠人工规则,从数据里自动得到一个大小可控、能覆盖任意文本的子词表。
- 数学上为什么可能有效:每一步合并都让训练语料的 token 数减少「这一对被合并的次数」,所以它是一个每步压缩最多的贪心过程 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条由算法定义直接推出;贪心每步最优不等于全局最优 出处待补 ;初始单元是全部 256 个字节时,词表覆盖任意输入 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条任何 UTF-8 字节串都能切成单字节 token 出处GPT-2 论文 §2.2 ↗ 。
- 实验上为什么最后选了它:Sennrich 等在翻译实验里显示子词切分能处理罕见词和没见过的词 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条WMT 15 英→德、英→俄;BPE 本身来自 Gage 1994 的压缩算法 出处Sennrich 2015 ↗ ;GPT 系、LLaMA、Qwen、DeepSeek 的分词器都是 BPE 或它的变体。它成为标准是工程上的反复选择,同类的 Unigram、WordPiece 也在用——BPE 不是被证明唯一正确的方法。
一句话、下一个缺口、你的提问
一句话: BPE 从单个字节出发,每一步把语料里最常挨在一起的一对粘成新 token——粘多少次,词表就长多少;切法是统计出来的,不是人规定的。
下一个缺口
「合并到多大为止」是你要拍板的超参数。那词表该多大?越大越好吗?第 4 课:词表该多大?
你的提问
C1 还没有真实提问记录。你可能会问:
- 「为什么前几步合并的是半个字?中文为什么比英文贵?」→ 岔路课:为什么中文更「贵」?