Tokenization
LLM 基础 · 子词切分

Tokenization:词表大小是一场压缩率与算力的交换

模型看不到文本,只看到整数 ID;tokenizer 决定一句话被切成多少个 ID。 BPE 从字符出发反复合并最高频的相邻对,WordPiece 换成似然增益,Unigram 先建大词表再按损失剪枝,byte-level BPE 则把 byte 值当底座,让任何文本都能表示。 代价写在词表上:embedding 是 V×d,输出层每 token 要 V×d 次乘加,也就是 2×V×d FLOPs。

BPE · SentencePiece · Llama 3 · Qwen2/3 · tiktoken BPE ↗ SentencePiece ↗ Unigram LM ↗ Llama 3 ↗ Qwen2 ↗ Qwen3 ↗ tiktoken @ 4e71bbe ↗
TL;DR · 30 秒抓住核心

Tokenizer 是模型的输入字母表,而且先于预训练冻结。它决定同一句话占多少 token,也就决定了上下文预算、训练语料被读多少遍,以及推理时每 token 的输出层开销。 BPE 的规则最简单:从字符或 byte 出发,反复把加权频次最高的相邻对合并成一个新符号;词表大小等于初始符号数加合并次数(BPE §3.2)。

三条路线只改「合并哪一对」:BPE 看频次,WordPiece 看似然增益,Unigram 先建大词表再按移除后的损失剪枝。 byte-level BPE 把底座换成 256 个 byte 值,任何 UTF-8 文本都能拆,不会出现 UNK;SentencePiece 则把空格转义成 ▁,直接从原始句子训练,不再依赖预分词。

词表不是越大越好。词表越宽,输入 embedding 和输出层也越宽;Llama 3 8B 的输出层在 128,000 词表下,每 token 需要 1,048,576,000 FLOPs,对应 524,288,000 次乘加(按 1 次乘加 = 2 FLOPs 计)。 换来的压缩率是:相比 Llama 2 tokenizer,Llama 3 在英语样本上的压缩率从 3.17 提到 3.94 字符/token,相对提高 24.3%((3.94 − 3.17) / 3.17)。

证据并不整齐:独立 BPE-60k 在德语 newstest2013 上把 UNK 从 1,079 压到 0;8 模型集成下,它的稀有词 F1 比 WDict 高 4.1 个百分点,joint BPE-J90k 高 5.0 个百分点。 但 BPE-60k 的单模型 BLEU 比字典基线低 0.5 分(BPE Table 2)。 SentencePiece 的 8k 共享词表在 KFTT 两个方向都高于 80k/80k 词模型,预分词却只在一个方向帮忙(SentencePiece Table 1)。

3.94 字符/token
Llama 3 vs Llama 2
英语样本压缩率
151,646
Qwen2 词表总量
regular token 与 control token 合计
1.05 GFLOPs/token
Llama 3 8B 输出层
每 token 的 FLOPs,只算 logits 投影
01

词表一冻结,文本长度和模型成本就跟着定死

先建立共同认知:tokenizer 定义模型的输入字母表,词表大小同时决定序列长度、embedding 参数量与输出层算力。

神经网络只接受固定形状的整数张量。文本要先经过一张词表,把每个切分单位映射成一个整数 ID;这张词表在预训练开始前训练好,之后不再变化。 同一个模型、同一段文本,换一套 tokenizer,token 数会变,能塞进上下文的字符数会变,训练时同一批算力读到的文本量也会变。 所以 tokenizer 不是预处理细节,它是模型设计的一部分。

两个极端都已被论文量化。词级模型把每个词当一个 token:BPE 论文的德语训练语料有 1,750,000 个词类型,newstest2013 上有 1,079 个 UNK;词表大,稀有词仍然会掉到词表外。 字符级模型把每个字符当一个 token:词表只有 3,000 个类型、UNK 为 0,但同一份 100m 词的语料膨胀到 550m 个 token,也就是每词 5.50 个 token,序列长度和注意力开销一起上升。

词表 V ↑ → 每词 token 数 ↓,但 embedding 参数 V×d ↑ 与 输出层 FLOPs 2×V×d ↑
词表大小同时出现在压缩率和参数/算力两侧;子词方法是在这条曲线上选一个工作点

子词方法选了中间点。BPE 用 59,500 次合并,把德语词表压到 63,000 个类型,UNK 为 0,token 数 112m,每词 1.12 个 token。 这组数字说明子词不是「更聪明的词表」,而是用更多类型换掉了 UNK,同时把序列长度压在词级附近。

难的地方不在「把词拆小」这件事本身,而在四个约束同时成立: 切分要可逆,解码后能还原原文;要能处理训练时没见过的字符和语言;要给定一个全局词表,不能按任务临时改;还要让合并规则在同一份语料上可复现。 后面的 BPE、WordPiece、Unigram、SentencePiece 和 byte-level BPE,都是在回答「合并哪一对、底座是什么、要不要预分词」。

02

词级和字符级各走极端,子词方法只改合并单位

词级词表大且 UNK 多,字符级零 UNK 但序列长;BPE、WordPiece、Unigram 的差别在于「合并哪一对」。

词级 + 回退字典

保留词边界,代价是 UNK 和巨大词表

BPE 论文的 WDict 基线用 300k 源语言词表、500k 目标语言词表,再加一个回退字典处理词表外的词; newstest2015 的英德翻译上,单模型 BLEU 是 22.0,八模型集成是 24.2(BPE Table 2)。 回退字典能救回一部分稀有词,但词表仍然是开放的:德语训练语料里 1,750,000 个词类型,newstest2013 上仍有 1,079 个 UNK。

词级模型还有一个固定成本:每个词都要在 embedding 里占一行,词表随语料里的词形增长,模型参数也跟着长。 这条路把「词」当成不可再分的最小单位,遇到新词就只能回退。

字符级 / 字符 n-gram

零 UNK,但序列长度翻几倍

字符级把底座缩到字符:德语语料只有 3,000 个类型, newstest2013 的 UNK 为 0;但 100m 词的语料变成 550m 个 token,每词 5.50 个 token。 字符 bigram 把序列压到 306m 个 token、每词 3.06 个 token,但仍有 34 个 UNK,因为测试集里出现了训练时没见过的 bigram。

字符 n-gram 用 n 调压缩率:n 越大,序列越短,词表越大;n 越小,词表越小,序列越长。 这条曲线说明「零 UNK」不是免费的,它把成本从词表转移到了序列长度。

表 1 · 德语训练语料的切分统计(转录自 BPE 论文 Table 1;#UNK 为 newstest2013 上的未知 token 数;tokens/词 为 # tokens ÷ 100m 的近似值;BPE 的 merge 数 59,500 / 89,500 来自 BPE §4.1)

切分方式# tokens# types# UNKtokens/词(≈)
词(none)100m1,750,00010791.00
字符550m300005.50
字符 bigram306m20,000343.06
字符 trigram214m120,000592.14
compound splitting102m1,100,0006431.02
morfessor109m544,0002371.09
hyphenation186m404,0002301.86
BPE(59,500 merges)112m63,00001.12
BPE joint(89,500 merges)111m82,000321.11
字符 bigram(shortlist 50,000)129m69,000341.29

表里最值得看的是 BPE 和 joint BPE 的差别。独立 BPE 的 UNK 是 0;joint BPE 的 UNK 是 32,因为联合词表里有些片段只在英语训练文本出现,德语测试集遇到时没有对应符号。 论文 BPE §4.1 说这类片段占测试 token 的 0.05%。 所以「BPE 没有 UNK」要限定在独立词表;联合词表仍然会漏。

表 2 · 三种子词选择标准的对照(BPE 与 WordPiece 的规则来自 BPE §3.2 与 HF 文档 §WordPiece;Unigram 来自 Kudo §3.2)

方法合并 / 选择标准词表构造典型使用
BPE加权频次最高的相邻对初始符号 + 合并次数Llama、Gemma、Qwen2(HF 文档 §Byte pair encoding)
WordPiece合并后训练数据似然增益最大:freq(ab)/(freq(a)×freq(b))初始符号 + 合并次数BERT 家族(HF 文档 §WordPiece)
Unigram边际似然,EM 估概率,按移除后的损失剪枝大 seed 词表逐步剪到目标大小T5、BigBird、Pegasus(HF 文档 §Unigram)

BPE 和 WordPiece 都是自底向上的合并,区别只在打分:BPE 数频次,WordPiece 用「共现比随机期望高多少」。 Unigram 反过来,先造一个足够大的候选词表,再用 EM 估每个子词的概率,每轮算移除某个子词会让总似然掉多少,保留 top η%, 论文举例 η = 80(Kudo §3.2)。 两种做法的共同目标都是压缩:BPE 用字典编码的思路减少符号数,Unigram 用熵编码的思路缩短码长(Kudo §3.4)。

03

BPE 反复合并最高频相邻对,词表大小 = 初始符号 + 合并次数

记住三件事:按词频加权数对、每次合并所有出现、词表只增不减;编码时只按学到的顺序套用。

count(A,B) = Σw freq(w) · 1[(A,B) ∈ w]
(A,B)* = argmax(A,B) count(A,B)
V = V0 + M
BPE 的合并规则(BPE §3.2, Algorithm 1):数对不跨词边界,词频作为权重;M 是合并次数,也是这套算法最直接的控制量

初始符号是字符(或 byte),每个词末尾加一个词尾符号 </w>,这样解码时能知道词在哪里结束。 每轮统计所有相邻对的加权频次,取最高的那一对,把语料里所有出现都替换成新符号;新符号进入词表,原来的两个符号仍然保留。 因此词表大小从 V0 单调增加到 V0+M,不会先增后减。 编码时不再看频次,只按训练得到的合并顺序依次套用。

初始 95,每次合并减去该对的加权频次
low ×5
—
lower ×2
—
newest ×6
—
widest ×3
—
合并日志
m = 6:95 → 48 token;每词 48/16 = 3.00 个 token。

    语料来自 BPE 论文 Algorithm 1 的代码块({low</w>:5, lower</w>:2, newest</w>:6, widest</w>:3});Figure 1 的图注是另一份字典且没有给词频,页面不模拟。 这个模拟是简化模拟,不是源码还原:只覆盖按词频数对、合并所有出现、词尾符号和从 0 到 10 的合并步数。

    手算对账:同一语料的 m = 6 与 m = 10点开看每一步

    组 1:Algorithm 1 字典,m = 6

    1. 初始 token:low 4×5 = 20;lower 6×2 = 12;newest 7×6 = 42;widest 7×3 = 21;合计 95。词数 5+2+6+3 = 16。
    2. 第 1 次:e+s 出现 6+3 = 9 次,95 − 9 = 86。
    3. 第 2 次:es+t 出现 9 次,86 − 9 = 77。
    4. 第 3 次:est+</w> 出现 9 次,77 − 9 = 68。
    5. 第 4 次:l+o 出现 5+2 = 7 次,68 − 7 = 61。
    6. 第 5 次:lo+w 出现 7 次,61 − 7 = 54。
    7. 第 6 次:n+e 出现 6 次,54 − 6 = 48。
    8. 最终切分:low </w>、low e r </w>、ne w est</w>、w i d est</w>;每词 48/16 = 3.00 token。

    组 2:同一字典,m = 10

    1. 前 6 步同组 1,第 6 步后为 48。
    2. 第 7 次:ne+w 出现 6 次,48 − 6 = 42。
    3. 第 8 次:new+est</w> 出现 6 次,42 − 6 = 36。
    4. 第 9 次:low+</w> 出现 5 次,36 − 5 = 31。
    5. 第 10 次:w+i 出现 3 次,31 − 3 = 28。
    6. 最终切分:low</w>、low e r </w>、newest</w>、wi d est</w>;每词 28/16 = 1.75 token。

    并列时取「先遇到的一对」:按词在语料里的顺序、每词从左到右扫描,只有频次严格更大才替换当前最优。 Algorithm 1 没有规定并列规则,这是页面自定的;JS 与手算都按这一条。两组参数都能在页面上切换后逐位对上。

    这个模拟覆盖了什么、没覆盖什么覆盖分支与差异清单

    覆盖:按词频加权统计相邻对;每轮合并所有出现;词尾符号 </w>; 并列时按先遇到的顺序;合并次数 0–10 的每一步;token 总数与每词 token 数。

    未覆盖:真实语料的预切分与正则;byte-level 底座;增量式数据结构与堆优化; WordPiece 的似然打分;Unigram 的 EM 与剪枝;SentencePiece 的归一化、▁ 与 byte fallback; special token;编码阶段对未见词的合并顺序;词表大小对模型参数的影响。

    因此它是简化模拟,不是源码还原。原文 Algorithm 1 用 Python 的 max(pairs, key=pairs.get) 取最高频对,并列时的行为依赖 dict 顺序;页面把并列规则显式写成「先遇到」, 以便 JS 与手算可复现。Figure 1 的图注用的是另一份字典 {low, lowest, newer, wider},与 Algorithm 1 代码块里的 {low, lower, newest, widest} 不同;Figure 1 的图注没有给词频,页面无法按同一套规则复现, 所以交互与手算只使用 Algorithm 1 的字典,Figure 1 只作为原文示例提及。

    WordPiece:把「频次最高」换成「似然增益最大」

    WordPiece 和 BPE 一样自底向上合并,但打分不是频次,而是合并后训练数据似然的增益。 HF 文档给出的分数是 freq(ab) / (freq(a) × freq(b)): 两个符号一起出现的次数,比它们各自独立出现时的期望高出越多,越值得合并(HF 文档 §WordPiece)。 BERT 家族使用这套规则;它仍然产出确定性的合并序列,编码时按最长匹配切分。

    Unigram:先建大词表,再按损失剪枝

    Unigram 语言模型假设每个子词出现独立,句子概率是所有子词概率的乘积;给定词表,用 EM 最大化边际似然。 词表本身也从数据里学:先造一个足够大的 seed 词表,重复「EM 估概率 → 算每个子词的 loss → 保留 top η%」, 直到词表达到目标大小;单字符子词始终保留,保证任何词都能被切出来(Kudo §3.2)。 编码时用 Viterbi 找概率最高的切分(Kudo §3.2),所以同一句话只有一条最优路径,但训练时可以按概率采样多条(Kudo §3.3)。

    Byte-level BPE:底座换成 byte,UNK 消失

    byte-level BPE 把初始词表换成 byte 值域,任何 UTF-8 文本都能拆成 byte 序列,因此不存在「词表外字符」。 GPT-2 的词表是 256 个 byte token 加 50,000 次合并,再加 1 个 end-of-text 特殊 token,总计 50,257(tiktoken 的 gpt2 条目)。 tiktoken 的 byte_pair_encode 直接接收 &[u8],在 byte 序列上按 merge rank 合并 (src/lib.rs:198)。

    「无 UNK」不等于「无成本」。byte 底座让稀有文字也变成多个 byte token,序列可能变长; 要压回来,就得在词表里给这些语言留位置。Llama 3 的做法是 100K tiktoken token 加 28K 非英语 token(Llama 3 §3.2), Qwen2 与 Qwen3 则直接用十几万级的词表(Qwen2 §2.1 / Qwen3 §2)。

    SentencePiece:不预分词,空格变成 ▁

    SentencePiece 把输入当成 Unicode 字符流,空格也当普通符号,先用 meta symbol ▁(U+2581)转义, 再在原始句子上训练 BPE 或 Unigram,不需要语言相关的预分词(SentencePiece §2、SentencePiece §3.1)。 它的目标是 lossless tokenization:

    Decode(Encode(Normalize(text))) = Normalize(text)
    规范化后的文本必须能从 token 序列完整还原(SentencePiece §3.1);连续空格也能保留,这是 subword-nmt 的 @@ 表示做不到的

    代价是分词器要自己处理归一化。SentencePiece 的 Normalizer 做 Unicode 规范化,Trainer 学子词模型, Encoder 在编码时先归一化再切分,Decoder 反向还原(SentencePiece §2)。 对没有空格分词的语言(日语、中文)来说,这一步把「预分词」这个语言相关的强约束去掉了; 代价是归一化规则也进了模型文件,换规则就要重新训练。推断(由 SentencePiece §3.5 说 Normalizer 编译进模型文件推出)。

    04

    训练是一次性开销,编码和词表宽度是长期账单

    BPE 训练要反复扫描语料,编码要逐块合并;真正的长期成本是 V×d 的 embedding 和 2×V×d 的输出层 FLOPs。

    三笔账

    数对、合并、查表

    • 训练:每轮重新统计相邻对,再合并所有出现。BPE 原文说实现上用索引和增量更新,避免每轮全量重扫(BPE §3.2);SentencePiece 则把分段做成 O(N log N):朴素 BPE 分段要 O(N²),它用二叉堆管理合并(SentencePiece §3.2)。
    • 编码:tiktoken 先按正则把文本切成 piece,再对每个 piece 做 BPE;源码注释写明,n 个部分、m 次合并的朴素实现是 O(mn),piece 通常很小,所以缓存局部性比堆的渐进复杂度更重要(src/lib.rs:174-176)。
    • 查表:每个 token 一次词表查找;special token 先按字符串匹配,命中后直接取固定 ID,不经过 BPE(src/lib.rs:390-431)。
    • 吞吐:SentencePiece §4.2 报告的分段速度约为英语 21k 句/秒、日语 74k 句/秒;tiktoken README 自述比同类开源 tokenizer 快 3-6 倍,页面没有独立复现这个数字。

    词表大小的账更直接。输入 embedding 是一张 V×d 的表;输出层要把 d 维隐状态投到 V 个 logits,每个 token 需要 V×d 次乘加,按 1 次乘加 = 2 FLOPs 计就是 2×V×d。 下面三行用论文里的 V 和 d 直接相乘:

    表 3 · 词表宽度与每 token 输出层成本(V、d 来自 Llama 3 Table 3 与 Qwen2 Table 1;V×d 与 FLOPs 为页面计算)

    模型VdV×d(输入 embedding 元素数)2×V×d(FLOPs/token)
    Llama 3 8B128,0004,096524,288,0001,048,576,000
    Llama 3 405B128,00016,3842,097,152,0004,194,304,000
    Qwen2-7B151,6463,584543,499,2641,086,998,528

    这张表只算输入 embedding 和输出投影,不含 attention 与 FFN。它说明「词表翻倍」不是只改一个配置项: V 翻倍,输出层每 token 的乘加量也翻倍;如果输入输出不共享,embedding 侧还要再加一份同尺寸矩阵。 条件也要一起读:Llama 3 8B 的 d = 4,096,405B 的 d = 16,384, Qwen2-7B 的 d = 3,584;三者的 V 与 d 都不同,不能只拿 V 比大小。

    词表账本怎么乘:两组手算点开看分解

    组 A:Llama 3 8B,V = 128,000,d = 4,096

    1. 128,000 × 4,096:128,000 × 4,000 = 512,000,000;128,000 × 96 = 12,288,000;合计 524,288,000。
    2. 输出层每 token 的 FLOPs:2 × 524,288,000 = 1,048,576,000,即 1.05 GFLOPs/token。

    组 B:Qwen2-7B,V = 151,646,d = 3,584

    1. 151,646 × 3,584:151,646 × 3,500 = 530,761,000;151,646 × 84 = 12,738,264;合计 543,499,264。
    2. 输出层每 token 的 FLOPs:2 × 543,499,264 = 1,086,998,528,即 1.087 GFLOPs/token。

    JS 只做整数乘法和加法,没有浮点误差;页面上显示的分组数字与这两组逐位一致。 1.05 保留两位小数,1.087 保留三位小数,表里保留完整整数。

    压缩率的账要和模型成本一起看。相比 Llama 2 tokenizer,Llama 3 在英语样本上的压缩率从 3.17 字符/token 提到 3.94 字符/token,绝对增加 0.77 字符/token, 相对提高 24.3%((3.94 − 3.17) / 3.17)。 这意味着同样 8K 的上下文窗口(Llama 3 405B 的原始窗口是 8K,后扩到 128K;8K)能放更多字符,也意味着同样的训练 token 预算能读更多文本。 但 char/token 是语料相关的:英语、代码、中文的同一套词表会给出完全不同的数,不能把 3.94 当成所有语言的代表。

    05

    一条文本过六步,special token 从旁路进入

    原始文本、规范化、预切分、子词切分、查表、模板拼接;每一步都可能改变 token 数,special token 不经过 BPE。

    01 原始文本 UTF-8 字节序列 任意 Unicode 文本,空格、换行、emoji 都在内 词表外的字符最终都要有表示 底座是 byte 全部 byte 值覆盖输入 02 规范化 可选步骤 Unicode 归一化;SentencePiece 把空格转义为 ▁ Decode(Encode(Normalize(x))) = Normalize(x) SentencePiece Normalizer 是组件之一 03 预切分 regex 或空格 tiktoken 先按 pat_str 切块,再对每块做 BPE SentencePiece 可以跳过预切分,直接读原始句子 切块改变边界 数字、标点、空白各有规则 04 子词切分 BPE / Unigram BPE 按 merge rank 合并;Unigram 取最高概率切分 WordPiece 按似然增益合并,再用最长匹配编码 输出 token 序列 长度由词表与规则共同决定 05 映射 ID 查词表 每个 token 查词表得到整数 ID special token 精确匹配后直接取 ID,不经过 BPE 旁路 固定字符串,不参与合并 06 进入模型 embedding 查表 ID → embedding 行 → Transformer 词表大小决定 embedding 矩阵与输出层宽度 成本随 V 线性增长 V×d 参数;2×V×d FLOPs
    图 1 · 一条文本进入模型的六个步骤。依据 SentencePiece §2/§3.1、tiktoken encode_ordinary(src/lib.rs:360-372)与 encode 的 special token 分支(src/lib.rs:426-431)整理;图中只画主路径,byte-level 回退和不同子词算法的分支在正文说明。

    图里是六个步骤;下面按切分动作拆成五条——原始文本和进入模型不作为切分动作单列。

    1. 规范化:SentencePiece 的 Normalizer 把语义等价的 Unicode 字符归一化,文本、词表和模型文件放在一起,保证可复现(SentencePiece §2、SentencePiece §3.5)。tiktoken 的 encode_ordinary 直接对输入文本跑正则,没有单独的 Normalizer 调用(src/lib.rs:360-372)。
    2. 预切分:tiktoken 用 pat_str 把文本切成 piece;cl100k 的数字规则是 \p{N}{1,3},也就是每 1–3 位数字切一块(openai_public.py:89)。这一步决定了 BPE 能看到哪些相邻关系,数字、标点和空白的边界都在这里定下。
    3. 子词切分:BPE 按 merge rank 从低到高合并;Unigram 用 Viterbi 取最高概率路径;WordPiece 用最长匹配。三者的输出都是 token 字符串序列。
    4. 查表:每个 token 在词表里查到整数 ID。tiktoken 的 max_token_value 取 mergeable ranks 和 special tokens 的最大值,n_vocab = max_token_value + 1(core.py:47-52、:373-375)。
    5. special token / 模板:special token 是固定字符串,匹配到后直接取 ID,不经过 BPE(src/lib.rs:390-431)。chat template 决定这些 token 放在哪里。

    special token 与 chat template:BPE 之外的第二个词表

    tiktoken 的 encode 先找允许的 special token,找到就把它的固定 ID 直接推进结果,再对两段普通文本做正则切分和 BPE (src/lib.rs:375-431)。所以 <|im_start|> 这类字符串不会被拆成 <、|、im…… 而是整体一个 ID。 encode_ordinary 则完全不碰 special token,只做普通文本编码(src/lib.rs:360-372)。

    Qwen3 的 chat template 是一个具体例子。Qwen3 Table 9 的非思考模式模板是:

    <|im_start|>user
    {query} /no_think<|im_end|>
    <|im_start|>assistant
    <think>
    </think>
    {response}<|im_end|>

    这里 <|im_start|> 和 <|im_end|> 是 special token, /no_think 和空的 <think> 块是模板文本;Qwen3 用它们在同一模型里切换思考和直答模式(Qwen3 §4.3、Qwen3 Table 9)。 模板本身不是 BPE 学出来的,它是推理侧的字符串拼接规则;模板改了,同一段对话的 token 序列也会变。

    06

    独立 BPE 把 UNK 压到 0,单模型 BLEU 却可能低于字典基线

    先看稀有词,再看整体 BLEU;再核对 SentencePiece 的 8k 共享词表与现代模型的大词表。

    BPE 论文的英德翻译实验把基线、BPE-60k 和 joint BPE-J90k 放在同一张表里。 条件先写清楚:newstest2015、英→德、WDict 的词表是 300k/500k,BPE-60k 是 60k/60k,BPE-J90k 是 90k/90k; 单模型取 newstest2013 dev 上表现最好的那个(共 8 个模型,BPE §4.2),集成包含全部 8 个模型。

    表 4 · 英德翻译的 BLEU 与稀有词 F1(转录自 BPE 论文 Table 2;稀有词指不在训练集 top 50,000 的词;原文 Table 2 还有 syntax-based、WUnk 与 C2-50k 三行,未转录)

    系统单模型 BLEU8 模型集成 BLEU稀有词 F1(集成,%)
    WDict(词级 + 回退字典)22.024.236.8
    BPE-60k21.524.540.9
    BPE-J90k(joint)22.824.741.8

    注:tr.best 只在这三行内标出稀有词 F1 最高;全表 NMT 系统中单模型 BLEU 最高是 C2-50k 与 BPE-J90k 并列 22.8,8 模型集成最高是 C2-50k 25.3,原文的 C2-50k 行未转录。未转录的 syntax-based 行是 SMT 系统,单模型 BLEU 24.4,不与这里的 NMT 行混比。

    反直觉的地方在单模型列:BPE-60k 的 21.5 比 WDict 的 22.0 低 0.5 分。 但 8 模型集成下,BPE-60k 的稀有词 F1 从 36.8 提到 40.9,比 WDict 高 4.1 个百分点(40.9 − 36.8); joint BPE-J90k 提到 41.8,比 WDict 高 5.0 个百分点(41.8 − 36.8)。 集成后 BPE-J90k 比 WDict 高 0.5 分(24.7 − 24.2)。 所以「BPE 更好」要分指标说:它主要改善稀有词和 OOV 的可表示性,整体 BLEU 的收益小得多,单模型甚至可能倒退。 论文 BPE §4.2 还提醒,稀有词只占测试集的 9–11%,BLEU 和 chrF3 可能低估它们对翻译质量的影响。

    SentencePiece 的实验换了语言对和模型:KFTT、GNMT、LSTM 512 维 6 层、case-sensitive BLEU。 词模型是 80k/80k,SentencePiece 是 8k 共享词表; 共享词表把源和目标放在同一张表里;按单侧 80k 对共享 8k 算,词表规模是原来的 1/10(80k/8k)。

    表 5 · KFTT 日英/英日翻译的 BLEU(转录自 SentencePiece 论文 Table 1;8k 为共享词表;原表每个方向有 5 种设置,本表只转录 80k/80k、8k shared、8k shared + 预分词三种,另两种 80k/8k 与 8k/80k 未转录)

    方向词模型 80k/80kSentencePiece 8k(shared)SentencePiece + 预分词 8k
    ja→en28.2429.5529.85
    en→ja20.0621.6220.86

    ja→en 上,8k 共享词表比 80k/80k 词模型高 1.31 分(29.55 − 28.24), 相对提高 4.64%;en→ja 上高 1.56 分 (21.62 − 20.06),相对提高 7.78%。 预分词并不是稳定加分:ja→en 从 29.55 提到 29.85,en→ja 从 21.62 掉到 20.86。 论文 SentencePiece §4.1 的解释是,日语是非分词语言,预分词会变成决定词表的强约束;对日语到英语,这个约束的收益很小,对英语到日语反而有害。

    速度方面,SentencePiece 论文 §4.2 正文说,在原始日语上它的分段速度「约 380 倍」于 subword-nmt; 同一节 SentencePiece Table 2 给出的分段耗时是 subword-nmt 216.2 秒对 SentencePiece 5.9 秒,按这张表算是 36.6 倍。 两个说法并列,页面不替原文选一个。可以确定的是条件:KFTT 440k 句、 16k 词表、Xeon 3.5GHz、BPE 模型; 英语无预分词的分段是 36.1 秒对 20.3 秒, 约 1.78 倍;日语无预分词的训练是 528.0 秒对 217.3 秒,约 2.43 倍。 分段和训练的倍数不同,不能混着引用。

    现代模型的词表规模继续往上走。下面这张表把公开的 V 放在一起,注意「V」的定义不同: tiktoken 的 V 是 max token id + 1,包含 special token;Llama 3 的 128,000 是报告 Llama 3 Table 3 的值; Qwen2 的 151,646 是 151,643 个 regular token 加 3 个 control token。

    表 6 · 公开词表的规模与构成(tiktoken 数字来自源码,Llama 3 来自 Llama 3 Table 3 与 Llama 3 §3.2,Qwen2 来自 Qwen2 Table 1 与 Qwen2 §2.1,Qwen3 来自 Qwen3 §2)

    词表 / 模型V构成出处
    GPT-2 / r50k_base50,257256 byte + 50,000 merges + 1 specialtiktoken openai_public.py:26;HF 文档 §Byte-level BPE
    cl100k_base100,277max token id 100,276 + 1tiktoken openai_public.py:85、core.py:373
    o200k_base200,019max token id 200,018 + 1tiktoken openai_public.py:100、core.py:373
    Llama 3128,000100K tiktoken + 28K 非英语Llama 3 Table 3、Llama 3 §3.2
    Qwen2151,646151,643 regular + 3 controlQwen2 §2.1、Qwen2 Table 1
    Qwen3151,669报告 Qwen3 §2 只给总量Qwen3 §2

    这里要核对一个常见说法:151,646 是 Qwen2 的数字,不是 Qwen3 的。 Qwen2 §2.1 写的是 151,643 个 regular token 加 3 个 control token,Qwen2 Table 1 的 Vocabulary Size 也是 151,646;Qwen3 §2 写的是 151,669。 两者相差 23 个 token,不能混用。 Llama 3 的 128,000 则是 100K tiktoken token 加 28K 非英语 token;报告在 Llama 3 §3.2 只列出英语样本上相对 Llama 2 tokenizer 的压缩率 3.17 → 3.94 字符/token,非英语只描述方向性改善。

    反直觉

    词表更小,BLEU 反而更高

    SentencePiece 用 8k 共享词表,词表规模比 80k/80k 词模型小得多,却在 KFTT 两个方向都拿到更高 BLEU(SentencePiece Table 1)。 这不是「小词表一定更好」,而是同一份数据、同一个 NMT 配置下,子词把稀有词拆成可复用的片段,减少了 UNK 和稀疏词表带来的损失。 推断(由 SentencePiece Table 1 的 BLEU 与 SentencePiece §4.1 对预分词的讨论推出;原文没有给出这一机制的直接解释)。 换成英语到日语、或者换一个语料,预分词和词表大小的最优组合会变;表 5 的 20.86 就是预分词拖后腿的样本。

    07

    它不解决语义,也不保证跨语言覆盖,词表还改不动

    把失败模式写清楚:贪心合并、词表冻结、special token 注入、多语言证据缺口,以及原文自身的不一致。

    原文自身的不一致

    两处并列,模拟只用其一

    BPE 的示例字典:BPE Algorithm 1 的代码块用 {low, lower, newest, widest},并给出词频 5/2/6/3;BPE Figure 1 的图注写 {low, lowest, newer, wider},另给出四条 merge,没有词频;Algorithm 1 的代码块只给字典和 num_merges=10,不列合并结果。 页面并列保留两处,交互与手算因 Figure 1 缺词频而只采用 Algorithm 1 的字典,不表示 Figure 1 的示例有误。

    SentencePiece 的加速倍数:SentencePiece §4.2 正文写原始日语上「约 380 倍」; 同节 SentencePiece Table 2 的分段耗时是 216.2 秒对 5.9 秒,按表算是 36.6 倍。 两个说法并列,页面不替原文选一个。

    什么时候别用子词 BPE:需要严格字符级对齐(比如逐字符标注、某些代码编辑任务),或者输入是固定字母表、 词表外字符极少时,字符级更直接;需要多语言覆盖又不想维护语言相关预分词时,byte-level BPE 或 SentencePiece 更合适。反过来,如果词表已经冻结、模型已经训好,换 tokenizer 不会「免费提升」——它会把 embedding 和输出层一起换掉。 推断(由 embedding 与输出层的形状推出)。