第 3 章 第一部分:基础篇

Tokenizer 设计哲学

BPE、Unigram、WordPiece 的原理与取舍,多语言 Tokenizer 的设计艺术

Tokenizer BPE SentencePiece 多语言

为什么 Tokenizer 如此重要

Tokenizer 是 LLM 的”眼睛”——模型看到的不是原始文本,而是 Tokenizer 切分后的 token 序列。一个设计不当的 Tokenizer 会导致:

  1. 效率低下:相同的文本被切成更多的 token,消耗更多的计算和内存
  2. 多语言不公平:某些语言(如中文、日文)需要比英文多 2-4 倍的 token 来表达同样的信息
  3. 推理质量下降:过度碎片化的切分可能破坏词义
  4. 成本增加:API 按 token 计费,低效的 tokenizer 意味着更高的成本

一个直观的例子:用 GPT-4 的 tokenizer,英文 “Hello World” 是 2 个 token,但中文”你好世界”可能是 3-4 个 token。这意味着处理同等语义量的中文内容,成本可能是英文的 2 倍。

三种主流 Tokenization 算法

BPE(Byte Pair Encoding)

BPE(Sennrich et al., 2016)是目前最流行的 tokenization 方法,被 GPT 系列、LLaMA 等使用。

训练过程(自底向上的合并策略):

  1. 初始化词表为所有单字符(或字节)
  2. 统计相邻 token 对的出现频率
  3. 合并出现频率最高的 token 对,生成新 token
  4. 重复步骤 2-3,直到词表达到目标大小

例子:假设训练语料中 “low” 出现 5 次,“lowest” 3 次,“newer” 6 次,“wider” 3 次

初始: l o w (5), l o w e s t (3), n e w e r (6), w i d e r (3)

第一轮: 最高频对 "e r" → 合并为 "er"
       l o w (5), l o w e s t (3), n e w er (6), w i d er (3)

第二轮: "n e" 频率最高 → 合并为 "ne"
       l o w (5), l o w e s t (3), ne w er (6), w i d er (3)

第三轮: "ne w" → "new"
       l o w (5), l o w e s t (3), new er (6), w i d er (3)

...如此继续直到词表大小达标

BPE 的优点:

  • 通过数据驱动的方式平衡词表大小和覆盖率
  • 永远不会遇到 OOV(Out-Of-Vocabulary)问题(最坏情况回退到字节级)
  • 训练高效,实现简单

BPE 的缺点:

  • 贪心合并策略不一定是全局最优
  • 同一个词在不同上下文可能有不同的切分(虽然 deterministic BPE 保证一致性)
  • 对低资源语言不友好(语料少 → 学到的合并规则少 → 切分碎片化)

Unigram Language Model

与 BPE 的自底向上策略相反,Unigram(Kudo, 2018)使用自顶向下的策略。

训练过程:

  1. 初始化一个很大的候选词表(如所有子串 + 单字符)
  2. 用 EM 算法估计每个子词的出现概率
  3. 删除使训练语料 likelihood 下降最小的子词
  4. 重复步骤 2-3,直到词表达到目标大小

推理时的切分:对于输入文本,找到使序列概率最大的切分方式:

x∗=arg⁡max⁡x∈S(X)∏i=1∣x∣P(xi)x^* = \arg\max_{x \in S(X)} \prod_{i=1}^{|x|} P(x_i)

其中 S(X)S(X) 是所有可能的切分方式集合。这可以用 Viterbi 算法(动态规划)高效求解。

Unigram 的优势:

  • 基于概率模型,理论上更优雅
  • 支持概率采样——同一个词可以有多种切分方式,训练时随机采样可以作为一种正则化(Subword Regularization)
  • 对训练数据中的噪声更鲁棒

WordPiece

WordPiece 是 BERT 使用的方法,与 BPE 类似但合并策略不同:

  • BPE 选择出现频率最高的 token 对合并
  • WordPiece 选择使训练语料 likelihood 提升最大的 token 对合并

具体来说,合并 a,ba, b 为 abab 的收益是:

score(a,b)=freq(ab)freq(a)×freq(b)\text{score}(a, b) = \frac{\text{freq}(ab)}{\text{freq}(a) \times \text{freq}(b)}

这个指标实际上是 互信息(PMI) 的一个变体,倾向于合并经常共现的 token 对。

三种方法对比

特性BPEUnigramWordPiece
策略自底向上(合并)自顶向下(删减)自底向上(合并)
合并依据频率最高likelihood 最大PMI 最大
切分方式确定性概率性(可采样)确定性
OOV 处理回退到字节回退到字符标记为 [UNK]
代表模型GPT, LLaMAT5, ALBERTBERT

注:上述三种方法都基于固定词表和固定粒度。2024 年后出现了第四种范式——基于动态粒度的字节级架构(如 BLT),完全不需要传统 tokenizer。详见本章”超越 Tokenizer:字节级架构”一节。

Byte-Level BPE

现代 LLM 几乎都使用 Byte-Level BPE(也称 BBPE),即以字节(byte,0-255)而非字符作为最小单元。

好处:

  • 零 OOV:任何文本(甚至二进制数据)都可以被编码
  • 语言无关:不需要为每种语言做特殊处理
  • Unicode 友好:UTF-8 编码的文本自然地被分解为字节序列

GPT-4 的 tiktoken 就是一个高性能的 Byte-Level BPE 实现,词表大小约 100K。

LLaMA 使用 SentencePiece 的 BPE 模式,词表 32K,后来扩展到 128K。

超词分词:突破子词边界

传统 BPE 有一个硬约束:token 不能跨越空格(由预分词步骤强制)。当词表扩展时,新添加的子词越来越稀有,边际收益递减。SuperBPE 的核心创新是打破这一约束——采用预分词课程:先在标准子词约束下学习基础合并,然后 lift 约束学习超词(superword),即跨空格的多词表达(如 “by the way”、“fish oil”)。

SuperBPE 128K 正式版的实验结果:

维度标准 BPESuperBPE
编码效率基准少 33% token
8B 模型 MMLU基准+8.2%
推理计算基准减少 27%
token 难度均匀性稀有 token 难度高更均匀

BoundlessBPE 进一步解决了超词分词器的训练速度问题:通过按频率聚合超合并候选避免内存爆炸,两阶段流程(常规合并 → 超合并学习)产生与原始相同的结果,训练速度提升 600x(从 4.7 CPU 日降至约 10 分钟)。

反向思路:不改边界规则,改冗余与词表分配

超词分词的思路是放宽”token 不跨词边界”的约束。但 2026 年还出现了两条方向相反、成本更低的路线——它们根本不动边界规则,只在 BPE 框架内做后处理与表示修正:

1. 显式词边界标记(arXiv:2608.08847, 2026-08-09)

标准 BPE 有一个长期存在但少被讨论的冗余:同一个词,带前导空格和不带前导空格,会被编码成两条完全不同的 token 序列。” the” 和 “the” 是两个词表条目,各自占用预算、各自需要训练样本才能学好。

该工作的做法很直接——在分词中加入显式的词边界标记,把这个冗余消解掉。报告的收益是 bits/byte 更低(压缩效率更好),但与基线的压缩率差异在 1% 以内。

2. Pruned BPE(arXiv:2608.00837, 2026-08-01)

训练完的 BPE 词表里存在大量”死权重”——合并规则里出现过、但在最终语料上曝光度极低的 token。Pruned BPE 的做法是按 token 的最终曝光度(final exposure)剪枝,并把剪掉的预算重新分配给实际有用的可见词表。这是一个纯后处理步骤,不需要改动训练流程,报告编码长度缩短 0.27%–0.36%。

怎么理解这两条路线的价值:单项收益都在 1% 以内,看起来微不足道。但把它们和 SuperBPE 的”少 33% token”放在一起比较是不公平的——后者需要改变分词范式和重新训练模型,前者只需要改几行后处理代码。判断标准不是”收益多大”,而是”收益 / 改动成本 / 风险”:收益小但可叠加、风险极低、且不需要重新预训练,这正是工业环境里最容易落地的一类改进。

⚠️ 引用边界:上述数字均为原论文自报,来自 2026-08 的预印本,未见第三方复现。0.27%–0.36% 这样的幅度在实际系统中很容易被数据分布差异淹没,不宜作为选型依据;把它们当作”存在这个优化方向”的提示即可。

当前定位(2026-10 更新):超词分词已经走完了第一格(学术验证),现在处在中间格——已进入前沿机构的流水线,被当作对比基线/候选手段使用,但尚未成为主流生产模型的默认选择。与 BLT 的”完全去除 tokenizer”路线不同,超词分词是”保留 BPE 但扩大粒度”的渐进改良路线。

支撑这一格上移的证据:Nature(2026-10-07)那篇改造字节模型的工作,在想办法提高每个 patch 的平均字节数时,直接把”通过 tokenizer transfer 把子词 LLM 迁移到 SuperBPE 分词器(在 OLMo 分词器上训练到 200k/400k 词表),并用 FOCUS 初始化超词 token 的 embedding”当作基线来对比,而不是当作自己新提出的方法。换句话说,SuperBPE + FOCUS 在这条流水线里已经是”拿来就用的既有手段”,配套初始化方法也有了明确的工程位置。

⚠️ 距离仍在,别一次挪到位。 “被前沿工作当作既有手段使用”不等于”已有主流生产模型默认采用 SuperBPE”。后者至今没有任何公开证据。这三格的边界是:学术验证 → 前沿流水线的对比基线/候选手段 → 主流生产默认。现在在第二格。

多语言 Tokenizer 的挑战

设计一个公平高效的多语言 Tokenizer 是 LLM 领域的一个重要问题。

效率不公平问题

以 LLaMA 2 的 tokenizer 为例:

语言原始文本Token 数每 token 平均字符数
英文”The quick brown fox jumps”55.2
中文”敏捷的棕色狐狸跳跃”9-110.8-1.0
日文”素早い茶色の狐がジャンプ”12-150.8-1.1
阿拉伯文”الثعلب البني السريع يقفز”15-201.0-1.5

中文需要的 token 数是英文的 2-3 倍!这意味着:

  • 处理同样语义量的中文,上下文窗口的利用率只有英文的 1/2 到 1/3
  • API 按 token 收费时,中文用户要多付 2-3 倍的费用

2026 年系统基准研究(arXiv:2606.15044) 在 11 种东南亚语言上从 1.5B 模型验证了多种公平性方案的实际效果:MYTE 使缅甸语编码长度减少近 70%;PA-BPE 将 token 成本 Gini 系数从 0.064 降至 0.011(83% 降幅);BLT 在翻译任务上 BLEU+2 且完全消除词表偏向。“选择更好的 tokenizer”已从学术讨论变为可量化的工程决策。

不公平到底由哪一步造成?——答案是”预分词”,不是”词表”

前面讲了不公平存在、以及怎么缓解,但缺一层:这些多出来的 token 到底是哪一步造成的?知道这一点才能判断哪些手段有效、哪些是白费力气。两项研究给出了答案。

其一,控制变量之后,词表规模与预分词才是解释变量。 Explaining and Mitigating Crosslingual Tokenizer Inequities 训练了 97 种语言、约 7,000 个可比较的单语 tokenizer,系统操纵分词算法、词表规模与数据集规模后发现:

  • 训练语料与评测语料的相似度不影响 token premium——这是个反直觉的阴性结果,说明”多喂点该语言的数据”不是解药;
  • 词表规模与预分词会影响,但单纯加大词表并不能降低 premium;
  • 真正有用的是为每种语言找它自己的”最优词表规模”——不存在跨语言通用的最优解;
  • 超词分词(允许跨空格合并,即本章前面讲的 SuperBPE)既降低 premium,也提升整体压缩率。

其二,预分词甚至可以构成一个”天花板”:词表再大也修不好。 Vowel Signs Are Not Letters(2026-08)给出了一个近乎教科书式的例子:HuggingFace 的 ByteLevel 预分词器继承 GPT-2 的词正则,把”词”定义为 \p{L}+(一个或多个 Unicode 字母)。但在元音附标文字(abugida,如泰语、藏语、尼泊尔语)里,元音是写成组合记号的,不属于 \p{L},于是每个词会在每一个元音处被切开。

由于 BPE 的合并只在预分词内部进行,这些切分点会一路保留到训练结束,词表多大、语料怎么配比都救不回来。实测覆盖 26 种语言:17 种 abugida 全部中招,倍率从 1.47×(藏语)到 9.02×(泰语);而拉丁、西里尔、谚文、汉字恰好是 1.00×。把尼泊尔语在训练语料中的占比从 5% 扫到 95%,坏掉的 tokenizer 几乎不动(变化 1.7%),修好的那个变化 33.9%——这个对比把”结构性天花板”和”数据不够”干净地分开了;修好的版本在同等算力下尼泊尔语 held-out BPB 低 4.43%。

这一层为什么重要:它把”多语言不公平”从”词表偏向”这种听起来只能靠堆数据缓解的问题,变成了一个定位明确、改动很小的工程问题——改一行预分词正则,比重建 100K 词表便宜几个数量级。

一个非 CJK 的对照点:问题不止于中文

上表的案例以中文/日文/阿拉伯文为主,容易让人以为这是”CJK 或 RTL 文字的专属问题”。西里尔字母的实测说明并非如此:Beyond Two Bytes per Letter 在 9 个生产 tokenizer、5 种语言、837 万词形上量化了西里尔文字的开销:乌克兰语在现代 tokenizer 上的 token 开销为 68%–121%,在较老的 cl100k 上达 220%。缓解手段也验证有效:一个带 200K 上限的均衡字节级 BPE 词表(实际收敛到 158,184 条)把乌/英 token 比从 2.22× 降到 1.30×。论文最后的结论与上面两条研究一致:跨语言的分词效率偏向的是网络数据里更常见的那种文字。

⚠️ 提案里”俄语 8.6ד未能回源,故本处不写该数字,改用上述已核实编号的实测值。若后续找到 MDPI 原文,可再补。

解决方案

1. 扩大词表 + 均衡训练数据

Qwen 的 tokenizer 词表 151K,专门为中文优化,中文效率接近英文。

2. 混合粒度策略

对高频语言(英文、中文)使用较粗的粒度(词/字级别),对低频语言使用较细的粒度(字节级别)。

3. 语言自适应 tokenizer

根据检测到的输入语言,动态切换 tokenization 策略。但这增加了系统复杂度。

4. SCRIPT-BPE——基于 Unicode 脚本属性的预分词编码

绕过 UTF-8 字节转换,使用 Unicode 脚本和类别属性生成初始 token。优势:(1) 从编码层面消除非拉丁文字的字节惩罚;(2) 避免跨字符边界的无效合并(GPT-4o tokenizer 有 874 个此类 token);(3) 简单规则驱动预分词,无需复杂正则。限制:需要修改 tokenizer 训练流程,且尚在学术验证阶段(ICML 2025 提交)。[arXiv:2505.24689]

5. MorphBPE——形态感知分词

在 BPE 合并过程中考虑语素边界(morpheme boundary),使分词结果更贴合语言的自然结构。对形态丰富语言(如土耳其语、芬兰语、匈牙利语等黏着语)有显著改善——这些语言通过词缀堆叠形成复杂词形,标准 BPE 的纯频率驱动合并容易切断有意义的语素边界。

与 SCRIPT-BPE(脚本感知)互补:SCRIPT-BPE 解决脚本异构问题,MorphBPE 解决形态复杂性问题。二者可叠加使用。限制:形态分析依赖语言特定工具,尚无大规模工业验证。

6. Parity-Aware BPE——公平性驱动的合并策略(ACL 2026 长文):传统 BPE 每次合并频率最高的 token 对,资源丰富语言主导合并过程。PA-BPE(Foroutan et al., ACL 2026)每次选择能使”压缩率最差语言”改善最大的合并,将 token 成本不平等(Gini 系数)降低 89%(0.064→0.011),对全局压缩率影响可忽略。与 SCRIPT-BPE(改编码基础)和 MorphBPE(改边界识别)互补——PA-BPE 改的是 合并策略,三者形成”公平性三件套”。已获 ACL 2026 正式发表,但��无大规模工业部署。

7. SuTRA——面向形态丰富语言的形态边界设计(Interspeech 2026):SuTRA 以 akshara(梵文音节单位)与形态边界为核心设计分词单元,专门面向形态丰富语言(以印度语系为主)。其报告的三层收益为:边界识别 Boundary F1 峰值 +14.7%、印地语语义可恢复性峰值 +34%、机器翻译 chrF2 平均 +8.08。

⚠️ 读数字时请注意口径:三项收益中,只有 chrF2 的 +8.08 是平均值;Boundary F1 的 +14.7% 与语义可恢复性的 +34% 都是峰值,代表该方法表现最好的语言/设置,而非跨语言平均。引用时若把峰值当均值,会显著高估其普适收益。

与前面几条的关系:SCRIPT-BPE 改编码基础、MorphBPE 改边界识别、PA-BPE 改合并策略,SuTRA 则是围绕特定书写系统重新定义切分单元——它是四者中语言针对性最强的,量化收益也给出得最完整(同时覆盖边界质量、语义保真与下游任务三层)。代价是通用性最弱:其设计紧扣印度语系的形态特征,向其他语系推广需要重新设计切分单元,不宜直接套用。

词表大小的权衡

词表大小 VV 是一个关键的设计参数:

  • 太小(如 8K):分词粒度过细,序列变长,效率低
  • 太大(如 500K):Embedding 矩阵参数量 V×dmodelV \times d_{model} 过大,低频 token 训练不充分
  • 最佳实践:32K-128K 是目前的主流范围
模型词表大小说明
GPT-250,257英文为主
GPT-4 (cl100k)~100K改善多语言
LLaMA32,000偏英文
LLaMA 3128,256大幅扩展,改善多语言
Qwen151,936中英优化
DeepSeek V3128,256多语言 + 代码

特殊 Token 的设计

现代 LLM 的 tokenizer 包含多种特殊 token:

<|begin_of_text|>     -- 文本开始
<|end_of_text|>       -- 文本结束
<|start_header_id|>   -- 角色标识开始(system/user/assistant)
<|end_header_id|>     -- 角色标识结束
<|eot_id|>            -- 轮次结束
<|python_tag|>        -- 代码执行标记(工具调用)

这些特殊 token 在对话模板(chat template)中扮演关键角色,它们的设计直接影响模型的指令遵循和工具调用能力。

Tokenizer 对模型的深层影响

对 Scaling Laws 的影响

Tokenizer 的效率直接影响”有效数据量”。假设 Tokenizer A 将 1TB 文本编码为 250B tokens,Tokenizer B 编码为 500B tokens,那么在同样的 token 数目下,Tokenizer A 的模型实际”看到”了更多的信息。

CoTok(Compute Optimal Tokenization, 2026) 首次将这一直觉量化为理论框架:最优压缩率(字节/token)是模型大小和计算预算的函数,而非固定值。例如 102010^{20} FLOPs 预算下,BLT 的熵驱动动态压缩在不同模型规模上均优于固定 BPE 词表。这意味 tokenizer 设计的”最佳词表大小”问题应升级为”最佳压缩率函数”问题。详见第五章 Scaling Laws。

⚠️ 评估纪律:压缩率类指标不能跨分词方案比较。

上面这段话(以及本章后面用 bytes/token、BPB 衡量 tokenizer 好坏的做法)有一个隐含前提:比较对象用的是同一套分词方案。这个前提一旦打破,指标就会误导。

Breaking the Token Ceiling(2026-09)给出了明确的负面结论:同一个验证 BPB 在不同分词方案下对应的下游准确率可以差很多。原因是 BPB 只约束目标 token 的概率值,对残余概率质量如何排序完全不敏感——两个模型 BPB 相同,一个把剩余概率堆在合理候选上、另一个堆在垃圾候选上,下游表现可以天差地别。论文据此指出:低 BPB 的模型可能下游更差,按验证 loss 选学生模型不再安全(原文 §6.6 标题即 “When Lower BPB Does Not (Always) Imply Better Models”)。

工程含义很直接:跨分词方案选模型时,不要只看验证 loss / BPB,要配上一条下游错误率的 scaling law 再做决定。

这条与上面的 CoTok 是互补而非重复:CoTok 讲的是”最优压缩率是模型规模与算力预算的函数”,本条讲的是”压缩率这个指标本身不可跨方案比较”。两句话合起来才是完整的评估纪律。

⚠️ 评估纪律(续):pass@k 这类多样性指标同样不可跨分词方案比较。

上面那条只覆盖了 BPB/压缩率这一族指标。第二族陷阱落在生成多样性上,而且更隐蔽——因为它可以让你得出方向相反的结论。

Nature(2026-10-07)那篇字节化论文的 Table 1 里,byteified 模型在 Code 类别上 pass@16 更高、pass@1 更低。按 pass@16 读,字节模型”生成的多样性更好”;按 pass@1 读,字节模型”变差了”。作者明确拒绝据此下结论,原文是:要断言字节级模型更适合生成多样化输出,需要先画出质量—多样性 Pareto 前沿,因为观察到的多样性提升也可能来自采样设置或推理引擎在字节模型上的不同效果——实验中 temperature = 0.6、top_p = 0.6 对所有模型一致,但”一致”不等于”等效”。

工程含义很直接:跨分词方案比较生成多样性时,必须同时报 pass@1 与 pass@k。只报 pass@k 会得出与只报 pass@1 相反的结论,两者都只讲了一半。

这两族陷阱有一个共同的结构:被比较的两个模型在”概率质量如何组织”上不同,而所有单一标量指标都只看到了组织方式的一面。BPB 看的是目标 token 的概率值,pass@k 看的是分布尾部的厚度——各自都漏掉另一面。

对代码生成的影响

代码的 tokenization 需要特殊关注:

  • 缩进(空格/制表符)的处理方式影响生成质量
  • 编程关键字是否作为整体 token 很重要
  • 变量名的驼峰/下划线风格可能导致不同的切分

对数学推理的影响

数字的 tokenization 是一个已知的痛点。例如:

  • “1234567” 可能被切为 “123” + “45” + “67”
  • 这种切分不尊重数字的位值系统
  • 一些研究尝试用特殊的数字 tokenization 方案(如逐位编码)来改善数学推理

对推理延迟的影响

随着上下文窗口从 8K 扩展到百万级,tokenization 本身的计算开销正在成为新的系统瓶颈。传统 CPU tokenizer 采用串行处理方式,在长序列上的延迟不再可忽略。

GPUTOK(2026)构建了 GPU 常驻的字节级 BPE tokenizer,通过 cuCollections 静态映射和 BlockBPE 内核实现大规模并行化,在长序列上获得显著加速,且精确复现 GPT-2 的合并语义。

这意味着在设计面向长上下文的推理系统时,tokenization 时间需要纳入端到端延迟预算——尤其是当输入达到数十万 token 级别时。

动手实现:从零构建 BPE Tokenizer

理解 BPE 最好的方式是自己实现一个。下面是一个 ~60 行的简化 BPE 训练器:

from collections import Counter, defaultdict
from typing import List, Dict, Tuple

class SimpleBPE:
    """简化版 BPE Tokenizer — 用于理解核心算法"""

    def __init__(self, vocab_size: int = 300):
        self.vocab_size = vocab_size
        self.merges: List[Tuple[str, str]] = []  # 合并规则(有序)
        self.vocab: Dict[str, int] = {}

    def train(self, text: str):
        """BPE 训练: 迭代合并最高频的 token 对"""
        # Step 1: 初始词表 = 所有唯一字节 + 特殊标记
        words = text.split()
        # 将每个词拆为字符序列,末尾加 </w> 标记词边界
        word_freqs = Counter(words)
        splits = {word: list(word) + ["</w>"] for word in word_freqs}

        # 初始词表
        self.vocab = {ch: i for i, ch in enumerate(
            sorted(set(ch for word in splits for ch in splits[word]))
        )}

        # Step 2: 迭代合并
        while len(self.vocab) < self.vocab_size:
            # 统计所有相邻 token 对的频率
            pair_freqs = Counter()
            for word, freq in word_freqs.items():
                tokens = splits[word]
                for i in range(len(tokens) - 1):
                    pair_freqs[(tokens[i], tokens[i + 1])] += freq

            if not pair_freqs:
                break

            # 找到频率最高的 pair
            best_pair = pair_freqs.most_common(1)[0][0]
            self.merges.append(best_pair)

            # 执行合并
            new_token = best_pair[0] + best_pair[1]
            self.vocab[new_token] = len(self.vocab)
            for word in splits:
                tokens = splits[word]
                new_tokens = []
                i = 0
                while i < len(tokens):
                    if (i < len(tokens) - 1 and
                        tokens[i] == best_pair[0] and
                        tokens[i + 1] == best_pair[1]):
                        new_tokens.append(new_token)
                        i += 2
                    else:
                        new_tokens.append(tokens[i])
                        i += 1
                splits[word] = new_tokens

        print(f"训练完成: 词表大小 {len(self.vocab)}, 合并规则 {len(self.merges)} 条")
        return self

    def encode(self, text: str) -> List[str]:
        """编码: 应用学习到的合并规则"""
        tokens = list(text) + ["</w>"]
        for pair in self.merges:
            new_tokens = []
            i = 0
            while i < len(tokens):
                if (i < len(tokens) - 1 and
                    tokens[i] == pair[0] and tokens[i + 1] == pair[1]):
                    new_tokens.append(pair[0] + pair[1])
                    i += 2
                else:
                    new_tokens.append(tokens[i])
                    i += 1
            tokens = new_tokens
        return tokens

# 示例用法
bpe = SimpleBPE(vocab_size=50)
bpe.train("low low low low low lowest lowest newest newest newest newest newest newest widest widest widest")
print("合并规则:", bpe.merges[:5])
print("编码 'lowest':", bpe.encode("lowest"))
print("编码 'newest':", bpe.encode("newest"))

用 HuggingFace tokenizers 库训练 BPE

实际项目中,我们使用高性能的 tokenizers 库(Rust 实现,速度极快):

from tokenizers import Tokenizer, models, trainers, pre_tokenizers

# 创建 Byte-Level BPE tokenizer
tokenizer = Tokenizer(models.BPE())
tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)

# 配置训练器
trainer = trainers.BpeTrainer(
    vocab_size=32000,
    special_tokens=["<|begin_of_text|>", "<|end_of_text|>", "<|pad|>"],
    min_frequency=2,
    show_progress=True,
)

# 训练(传入文件路径列表)
tokenizer.train(files=["corpus_zh.txt", "corpus_en.txt", "corpus_code.txt"],
                trainer=trainer)
tokenizer.save("my_tokenizer.json")

# 测试
enc = tokenizer.encode("你好世界 Hello World")
print(f"Tokens: {enc.tokens}")
print(f"IDs: {enc.ids}")
print(f"Token 数: {len(enc.ids)}")

Tokenizer 效率实测对比

⚠️ 本节的比较方法只在”同一套分词方案内部”有效。 跨方案(例如拿 BPE 模型去和字节级模型比 BPB)时,低 BPB 不意味着下游更好——详见「对 Scaling Laws 的影响」一节中的评估纪律提醒。本节下面的脚本用于同一族 tokenizer 之间的效率对比,用途正确。

选择 tokenizer 前一定要用实际数据测量效率:

from transformers import AutoTokenizer

def measure_efficiency(text: str, model_names: list):
    """对比不同 tokenizer 的编码效率"""
    print(f"原文: {text[:60]}... ({len(text)} 字符)")
    print("-" * 60)
    for name in model_names:
        tok = AutoTokenizer.from_pretrained(name, trust_remote_code=True)
        ids = tok.encode(text)
        n_tokens = len(ids)
        chars_per_token = len(text) / n_tokens
        print(f"{name:30s} | {n_tokens:5d} tokens | "
              f"{chars_per_token:.2f} 字符/token")
    print()

# 中文文本效率对比
zh_text = "大语言模型的预训练数据工程是一个复杂的系统工程,涉及数据采集、清洗、去重、质量过滤等多个环节。" * 5

# 英文文本效率对比
en_text = "Large language model pretraining data engineering is a complex systems problem involving data collection, cleaning, deduplication, and quality filtering." * 5

# 代码效率对比
code_text = """
def transformer_block(x, attn, ffn, norm1, norm2):
    x = x + attn(norm1(x))
    x = x + ffn(norm2(x))
    return x
""" * 5

models = [
    "meta-llama/Llama-2-7b-hf",   # 32K 词表
    "meta-llama/Meta-Llama-3-8B",  # 128K 词表
    "Qwen/Qwen2.5-7B",            # 151K 词表,中文优化
    "deepseek-ai/DeepSeek-V3",    # 128K 词表
]

for text, label in [(zh_text, "中文"), (en_text, "English"), (code_text, "Code")]:
    print(f"=== {label} ===")
    measure_efficiency(text, models)

运行结果通常会显示:Qwen 在中文上效率最高(约 1.5-2 字符/token),LLaMA 2 最低(约 0.8 字符/token)。

超越 Tokenizer:字节级架构

上述所有方法——BPE、Unigram、WordPiece——都有一个共同前提:需要预先构建一个固定词表,将文本离散化为 token 序列。但如果这个前提本身可以被挑战呢?

BLT(Byte Latent Transformer)

Meta 提出的 Byte Latent Transformer(BLT)(ACL 2025)直接在原始字节序列上工作,完全不需要 tokenizer。

核心机制——基于信息熵的动态 Patch 分组:

BLT 用信息熵检测字节序列中的边界——信息密度高的区域(如罕见词、复杂语法)被分为更细的 patch,信息密度低的区域(如常见短语)合并为更大的 patch。模型在这些动态长度的 patch(而非固定词表的 token)的潜在表示上运算。

与传统 tokenizer 的本质区别:

维度BPE/Unigram/WordPieceBLT
最小单元子词(固定词表)字节(无词表)
分词粒度固定动态(按信息熵自适应)
多语言公平性受词表偏向影响字节级天然公平
OOV 问题通过回退缓解不存在
训练复杂度低较高

多语言公平性是 BLT 的一个天然优势。传统 BPE 的多语言不公平问题(中文需要英文 2-3 倍的 token 数)根源在于词表构建时的语料偏向。字节级处理从根本上消除了这一问题——所有语言都在同一个 256 维度的字节空间中被公平处理。

⚠️ 先划定本节的作用域:下面讨论的是”从零训练”的字节模型。 另有一条改造路线——把已训好的子词模型改成字节级(见后面的「采用现状」一节)——不适用这条曲线。两者的成本结构完全不同(改造不需要重新预训练,约 1% 预训练算力),代价也不同(改造换来的是字符级结构保真,通用能力略有退化)。两条路线回答的不是同一个问题,别把一方的结论套到另一方上。

当前局限(2026-09-24 改写):BLT 尚未成为主流生产方案。至于训练效率,更准确的说法不是”字节级更差”,而是”存在一道算力门槛”——

Breaking the Token Ceiling(UW + Meta FAIR,2026-09-11)在层参数匹配的约 1B 模型上(byte 变体 1.28B 层参数,token 变体 1.81B)、沿”分词方案 × 训练目标”两个轴、最高扫到 1T bytes,得到一条与”字节级训练效率低”完全不同的曲线:

Token-1B 在低算力区间领先,但很快进入平台期;byte 模型起步更差,却以更陡的斜率持续改善,算力继续增长后反超 token 模型,下游性能上限更高。

也就是说,低算力区看到”字节级落后”是真实的,但把它外推成”字节级训练效率差”就错了——两者是同一条曲线的两段。论文还给出:蒸馏的 End-Of-Token-1B 渐近超过蒸馏 Token-1B 最多 4%,且只需 1/6 的训练数据即可追平。

⚠️ 这些是外推值,不是实测分数。 上述 “+4%” 以及论文对比开源小模型的 “+6.5% / +8.1% / +2.1%“(分别对应 Llama-3.2-1B / Gemma-3-1B-pt / Gemma 2B)全部来自拟合 scaling law 后的渐近外推,不是某个当前 checkpoint 上跑出来的成绩。

但作为 ACL 2025 长文并已被集成到 HuggingFace Transformers,它标志着”文本离散化策略”正从”选择哪种 tokenizer”扩展为”是否需要 tokenizer”这一更根本的设计选择。

另一条路线:不加层级的扁平字节 Transformer

BLT 与 Hierarchical BPE 虽然一激进一温和,却共享一个假设:字节模型需要显式的层级结构(局部 encoder → 全局模型 → 局部 decoder,或两级 BPE)。2026-10 的一篇工作直接检验了这个假设,结论是层级不是必需的。

Byte Language Models: Scaling, Emergent Abstractions, and Information Allocation(2026-10-05,Wang Jie 等 4 人)的做法是:保留标准扁平 Transformer,不引入任何分词相关的专用架构,只加两个配方——token-superposition 训练(TST) 与 hash embeddings。在此配置下,字节 Transformer 在参数量 scaling 时持续优于子词 Transformer。

三个可直接引用的结论:

结论数字出处
固定 FLOPs 预算下最优”数据量/参数量”比例约为子词模型的 1.5–3 倍正文 §2(Figure 1c)
细粒度感知任务的相对提升CUTE 约 +40%、OCRBench 约 +20%正文 §2.3
用学到的局部结构做 speculative decoding接受 token 数比子词 Transformer 多 3.4 倍摘要

还有一条机制层面的观察比数字更有价值:字节 Transformer 会自发学会分词器显式提供的那种局部抽象——一组类似切分的位置被用来收集局部上下文表示;把最多 25% 的中间层限制为只访问这些局部表示,下游性能仍然保住。也就是说”tokenization 提供的抽象”可以在模型内部涌现,不必从架构外部塞进去。

⚠️ “约 40%“这个数字要按规模读。 论文正文给出的是 CUTE 上的相对提升约 40%,但其 Table 1 显示该增益随参数量迅速收窄:200M 档 69.89 → 99.19(相对 +41.9%),到了 3B 档只剩 80.83 → 99.93(相对 +23.6%)。把 40% 当作通用结论会高估大模型档位的收益。另一个边界:这条路线验证的规模是 200M–3B,且子词基线由同一批作者按同一套配置训练,不是拿 Qwen/Llama 这类生产模型直接对比。

和 BLT 放在一起读:BLT 赢在熵驱动的动态 patching(把算力分配到信息密度高的地方),这条路线赢在更长序列本身提供的额外计算——论文的原话是,与其把更长的序列当作缺陷,不如把它当作额外的算力预算。两条路线都成立,但机制不同,选型时问的问题也不同:BLT 问”怎么压缩”,扁平路线问”多出来的序列长度还能干什么”。

Hierarchical BPE——保留 BPE 的”温和改良”

与 BLT 的”激进革命”不同,Hierarchical BPE(ACL ARR 2025 May)不需要额外的熵检测模型,直接利用 BPE 的合并结构:在 BPE token 添加 end-of-patch 标记,然后进行第二级 BPE 压缩来控制 patch 粒度。优势:语言无关(不依赖空格分词,适用于中文等语言),实现简单,中英文均有效。定位:BLT 是”完全去除 tokenizer”,Hierarchical BPE 是”保留 BPE 但增加层级”。

第三条路线:字节级蒸馏——teacher 是 token 模型,student 是 byte 模型

前面两条路线都要回答”要不要从头训一个字节模型”。但工程上最常见、成本最低的场景其实是另一个:你手上已经有一个很强的 token 级 teacher,只想蒸馏一个小模型。 这条路径之前有个技术障碍——teacher 输出的是 token logits,student 要的是 byte logits,两套词表对不上。

Breaking the Token Ceiling 给出了两个转换方法,都能在单次 teacher 前向内完成:

方法做法性质代价
Marginalize-Ittoken 结束后不再向后追踪,把剩余未结束候选的概率重新归一化近似效率最高,但丢信息
End-Of-Token向词表加一个 <eot> 标记,用它吸收残余概率质量精确不丢信息

为什么这条路线在成本上有压倒性优势:student 若用 token,词表约 100K,保存 teacher logits 时必须做 top-k 截断,丢掉长尾分布信息;student 若用 byte,词表只有约 256,可以完整保存整个分布,直接免掉 top-k 截断,logit 存储成本降到约 1/5。

三项可立刻落地的收益:

  1. 数据效率:只用 1/6 的训练数据即可追平蒸馏 Token-1B(在算力充足时);
  2. 存储:logit 存储成本约 1/5;
  3. 免去 top-k 截断:不再因为词表太大而被迫丢弃 teacher 的长尾信息。

趋势结论比任何单项数字都更有决策价值:低算力区 token 学生领先,算力增长后 byte 学生反超——选谁取决于你的训练预算落在曲线的哪一段。如果你的蒸馏预算很小,字节学生不会给你惊喜;如果你的预算足够大,它的上限更高。

⚠️ 本小节所有带百分号的数字(+4% / +6.5% / +8.1% / +2.1%)均为拟合外推的渐近预测值,不是实测分数,见上一节的限定说明。

采用现状:截至 2026 年 10 月,仍无前沿生产部署

在结束这一节之前,需要给读者一个明确的时间定位:截至 2026 年 10 月,尚未见任何前沿模型在生产环境中大规模采用字节级 / 无 tokenizer 架构。BLT 一类方案仍停留在研究与小规模验证阶段,Hierarchical BPE 同样未见工业部署。子词分词仍是所有主流前沿模型的实际选择。

但这条判定现在有了明确的”被推翻方向”——而且已经不止一条。 写于 2026-09 的那次判断是”最可能的路径是先出现在小模型蒸馏场景”。一个月后出现了第二条、且已在三个不同模型族上跑通的路径:

  • 路径 A:字节级蒸馏(原预测)。 绕开”从零预训练一个前沿字节模型”这条成本极高的路径,只需要把现成的强 token 模型当 teacher,1/6 数据、1/5 logit 存储、算力足够时反超——这些收益在 1B 规模上已经验证。
  • 路径 B:字节化改造(2026-10 新出现)。 与”蒸馏出一个学生”不同,这条路径改造模型本身:把已训好的子词 LLM 直接改造成字节级模型,不需要重新预训练。

路径 B 的成本与做法(Retrofitting language models to operate over bytes,Nature,2026-10-07 发表;Minixhofer 等,Ai2/剑桥语言技术实验室/华盛顿大学/帝国理工/LMU 慕尼黑(另含慕尼黑机器学习中心)):两阶段转换,合计 49.1B token,不到典型预训练预算的 1%。

阶段训练对象数据量
阶段一冻结全局模型,只训局部 encoder / decoder / 边界预测器 / LM 头9.8B token(约 43B 字节)
阶段二端到端联合微调整个模型39.3B token(约 173B 字节)

产出的四个模型是 Bolmo 7B(改自 Olmo 3 7B)、Bolmo 1B(改自 OLMo 2 1B)、Bwen 8B(改自 Qwen3 8B Base)、Blama 8B(改自 Llama 3 8B)。参数量变化并不统一:Bolmo 7B 增加约 3.3 亿(+4.5%),Blama 8B 增加约 2.2 亿(+2.7%),Bwen 8B 增加约 1.2 亿(+1.5%),Bolmo 1B 反而减少约 1000 万(−0.7%)。

效果上,Bolmo 7B 在 STEM 任务上比从零训练的 BLT 7B 高 16.5 个百分点;Bwen 8B 的表现接近、有时超过其源模型 Qwen3 8B。

⚠️ “逼近源模型”不等于”超过源模型”,更不等于”通用能力上涨”。 Nature 正文明确记载:字节化在 MC / GenQA / Math 三类上有轻微退化,在 Code(pass@1)上有明显退化;大幅改善集中在 Code(pass@16)与字符级理解。理由值得抄进选型文档——字节化换来的是结构保真(字符级理解、无 OOV、跨文字一致),不是通用能力上涨。读到这里若写成”字节模型已经追平子词模型”,就已经越界了。

这个判断属于本书基于新证据的推演,不是已发生的事实,请按”最可能路径”而非断言来读。

📌 原预测已被部分证伪,但方式出乎意料。 2026-09 判断”突破口最可能是蒸馏”,实际发生的第二条路径不是蒸馏而是改造,且在 三个不同模型族(Olmo/Qwen3/Llama 3)上同时跑通,成本约 1% 预训练算力。这条预测错在路径类型,而不是”字节级会找到低成本入口”这个大方向——原推理链(绕开从零预训练)被证明是对的。保留原文是因为它记录了当时的推理链,而不是因为它仍是最新判断。

这个判断的作用不是否定这些方向,而是帮读者给本节内容定位:

  • 它们是有前景的研究方向,值得跟踪——尤其是 BLT 已被 ACL 2025 收录并集成进 HuggingFace Transformers,说明其工程可行性已获认可;
  • 但它们不是近期的可替换选项。如果你正在为一个将要上线训练的模型选分词方案,2026 年 8 月的现实答案仍然是 BPE 系。

换句话说,本章前面所有关于 BPE、词表大小、多语言公平性的讨论,在可预见的未来仍然完全适用。字节级架构目前改变的主要仍是”我们如何思考分词”这个问题的边界,而不是”我们现在该怎么做”的答案——不过有一个例外:字节级蒸馏的三项收益(1/6 数据、1/5 存储、免 top-k 截断)是当下就能兑现的,如果你的场景正好是”从大模型蒸馏小模型”,这条已经值得实测,而不只是跟踪。

⚠️ 时效提醒:这是一条随时间易失效的判定。它以”未发现反例”为依据,一旦有前沿模型宣布字节级架构的生产部署,该结论即告失效。引用时请务必带上时间限定,并重新核对最新情况。

实践建议

  1. 选择 tokenizer 时,先测量目标语言的 token 效率:用实际数据计算”每 token 平均字符数”
  2. 如果你的应用场景以中文为主,优先考虑 Qwen 或 DeepSeek 的 tokenizer
  3. 训练自定义 tokenizer 时,确保训练数据的语言分布与模型训练数据一致
  4. 词表大小推荐 64K-128K,除非有特殊需求
  5. 不要低估 tokenizer 对下游任务的影响——换一个 tokenizer 可能需要重新训练整个模型

📖 延伸阅读路线

  • 下一章:第四章 预训练数据工程 — 预训练数据是 LLM 的”燃料”。从数据采集到清洗、去重、质量过滤,构建完整的数据处理管线
  • 效率影响链条:第五章 Scaling Laws — Tokenizer 效率直接影响”有效数据量”,进而影响 Scaling Laws 的计算
  • 架构依赖:第二章 Transformer 架构 — Tokenizer 的输出维度决定了 Embedding 矩阵的行数(V×dmodelV \times d_{model})
  • 代码生成影响:第十一章 评估与 Benchmark — Tokenizer 对代码和数学 benchmark 结果有直接影响