模型看不到文本,只看到整数 ID;tokenizer 决定一句话被切成多少个 ID。
BPE 从字符出发反复合并最高频的相邻对,WordPiece 换成似然增益,Unigram 先建大词表再按损失剪枝,byte-level BPE 则把 byte 值当底座,让任何文本都能表示。
代价写在词表上:embedding 是 V×d,输出层每 token 要 V×d 次乘加,也就是 2×V×d FLOPs。
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)。
先建立共同认知: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,序列长度和注意力开销一起上升。
子词方法选了中间点。BPE 用 59,500 次合并,把德语词表压到 63,000 个类型,UNK 为 0,token 数 112m,每词 1.12 个 token。 这组数字说明子词不是「更聪明的词表」,而是用更多类型换掉了 UNK,同时把序列长度压在词级附近。
难的地方不在「把词拆小」这件事本身,而在四个约束同时成立: 切分要可逆,解码后能还原原文;要能处理训练时没见过的字符和语言;要给定一个全局词表,不能按任务临时改;还要让合并规则在同一份语料上可复现。 后面的 BPE、WordPiece、Unigram、SentencePiece 和 byte-level BPE,都是在回答「合并哪一对、底座是什么、要不要预分词」。
词级词表大且 UNK 多,字符级零 UNK 但序列长;BPE、WordPiece、Unigram 的差别在于「合并哪一对」。
BPE 论文的 WDict 基线用 300k 源语言词表、500k 目标语言词表,再加一个回退字典处理词表外的词; newstest2015 的英德翻译上,单模型 BLEU 是 22.0,八模型集成是 24.2(BPE Table 2)。 回退字典能救回一部分稀有词,但词表仍然是开放的:德语训练语料里 1,750,000 个词类型,newstest2013 上仍有 1,079 个 UNK。
词级模型还有一个固定成本:每个词都要在 embedding 里占一行,词表随语料里的词形增长,模型参数也跟着长。 这条路把「词」当成不可再分的最小单位,遇到新词就只能回退。
字符级把底座缩到字符:德语语料只有 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 | # UNK | tokens/词(≈) |
|---|---|---|---|---|
| 词(none) | 100m | 1,750,000 | 1079 | 1.00 |
| 字符 | 550m | 3000 | 0 | 5.50 |
| 字符 bigram | 306m | 20,000 | 34 | 3.06 |
| 字符 trigram | 214m | 120,000 | 59 | 2.14 |
| compound splitting | 102m | 1,100,000 | 643 | 1.02 |
| morfessor | 109m | 544,000 | 237 | 1.09 |
| hyphenation | 186m | 404,000 | 230 | 1.86 |
| BPE(59,500 merges) | 112m | 63,000 | 0 | 1.12 |
| BPE joint(89,500 merges) | 111m | 82,000 | 32 | 1.11 |
| 字符 bigram(shortlist 50,000) | 129m | 69,000 | 34 | 1.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)。
记住三件事:按词频加权数对、每次合并所有出现、词表只增不减;编码时只按学到的顺序套用。
初始符号是字符(或 byte),每个词末尾加一个词尾符号 </w>,这样解码时能知道词在哪里结束。
每轮统计所有相邻对的加权频次,取最高的那一对,把语料里所有出现都替换成新符号;新符号进入词表,原来的两个符号仍然保留。
因此词表大小从 V0 单调增加到 V0+M,不会先增后减。
编码时不再看频次,只按训练得到的合并顺序依次套用。
语料来自 BPE 论文 Algorithm 1 的代码块({low</w>:5, lower</w>:2, newest</w>:6, widest</w>:3});Figure 1 的图注是另一份字典且没有给词频,页面不模拟。
这个模拟是简化模拟,不是源码还原:只覆盖按词频数对、合并所有出现、词尾符号和从 0 到 10 的合并步数。
组 1:Algorithm 1 字典,m = 6
95。词数 5+2+6+3 = 16。e+s 出现 6+3 = 9 次,95 − 9 = 86。es+t 出现 9 次,86 − 9 = 77。est+</w> 出现 9 次,77 − 9 = 68。l+o 出现 5+2 = 7 次,68 − 7 = 61。lo+w 出现 7 次,61 − 7 = 54。n+e 出现 6 次,54 − 6 = 48。low </w>、low e r </w>、ne w est</w>、w i d est</w>;每词 48/16 = 3.00 token。组 2:同一字典,m = 10
48。ne+w 出现 6 次,48 − 6 = 42。new+est</w> 出现 6 次,42 − 6 = 36。low+</w> 出现 5 次,36 − 5 = 31。w+i 出现 3 次,31 − 3 = 28。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 和 BPE 一样自底向上合并,但打分不是频次,而是合并后训练数据似然的增益。
HF 文档给出的分数是 freq(ab) / (freq(a) × freq(b)):
两个符号一起出现的次数,比它们各自独立出现时的期望高出越多,越值得合并(HF 文档 §WordPiece)。
BERT 家族使用这套规则;它仍然产出确定性的合并序列,编码时按最长匹配切分。
Unigram 语言模型假设每个子词出现独立,句子概率是所有子词概率的乘积;给定词表,用 EM 最大化边际似然。 词表本身也从数据里学:先造一个足够大的 seed 词表,重复「EM 估概率 → 算每个子词的 loss → 保留 top η%」, 直到词表达到目标大小;单字符子词始终保留,保证任何词都能被切出来(Kudo §3.2)。 编码时用 Viterbi 找概率最高的切分(Kudo §3.2),所以同一句话只有一条最优路径,但训练时可以按概率采样多条(Kudo §3.3)。
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 把输入当成 Unicode 字符流,空格也当普通符号,先用 meta symbol ▁(U+2581)转义,
再在原始句子上训练 BPE 或 Unigram,不需要语言相关的预分词(SentencePiece §2、SentencePiece §3.1)。
它的目标是 lossless tokenization:
代价是分词器要自己处理归一化。SentencePiece 的 Normalizer 做 Unicode 规范化,Trainer 学子词模型, Encoder 在编码时先归一化再切分,Decoder 反向还原(SentencePiece §2)。 对没有空格分词的语言(日语、中文)来说,这一步把「预分词」这个语言相关的强约束去掉了; 代价是归一化规则也进了模型文件,换规则就要重新训练。推断(由 SentencePiece §3.5 说 Normalizer 编译进模型文件推出)。
BPE 训练要反复扫描语料,编码要逐块合并;真正的长期成本是 V×d 的 embedding 和 2×V×d 的输出层 FLOPs。
src/lib.rs:174-176)。src/lib.rs:390-431)。
词表大小的账更直接。输入 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 为页面计算)
| 模型 | V | d | V×d(输入 embedding 元素数) | 2×V×d(FLOPs/token) |
|---|---|---|---|---|
| Llama 3 8B | 128,000 | 4,096 | 524,288,000 | 1,048,576,000 |
| Llama 3 405B | 128,000 | 16,384 | 2,097,152,000 | 4,194,304,000 |
| Qwen2-7B | 151,646 | 3,584 | 543,499,264 | 1,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
128,000 × 4,096:128,000 × 4,000 = 512,000,000;128,000 × 96 = 12,288,000;合计 524,288,000。2 × 524,288,000 = 1,048,576,000,即 1.05 GFLOPs/token。组 B:Qwen2-7B,V = 151,646,d = 3,584
151,646 × 3,584:151,646 × 3,500 = 530,761,000;151,646 × 84 = 12,738,264;合计 543,499,264。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 当成所有语言的代表。
原始文本、规范化、预切分、子词切分、查表、模板拼接;每一步都可能改变 token 数,special token 不经过 BPE。
encode_ordinary(src/lib.rs:360-372)与 encode 的 special token 分支(src/lib.rs:426-431)整理;图中只画主路径,byte-level 回退和不同子词算法的分支在正文说明。图里是六个步骤;下面按切分动作拆成五条——原始文本和进入模型不作为切分动作单列。
encode_ordinary 直接对输入文本跑正则,没有单独的 Normalizer 调用(src/lib.rs:360-372)。pat_str 把文本切成 piece;cl100k 的数字规则是 \p{N}{1,3},也就是每 1–3 位数字切一块(openai_public.py:89)。这一步决定了 BPE 能看到哪些相邻关系,数字、标点和空白的边界都在这里定下。max_token_value 取 mergeable ranks 和 special tokens 的最大值,n_vocab = max_token_value + 1(core.py:47-52、:373-375)。src/lib.rs:390-431)。chat template 决定这些 token 放在哪里。
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 序列也会变。
先看稀有词,再看整体 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 三行,未转录)
| 系统 | 单模型 BLEU | 8 模型集成 BLEU | 稀有词 F1(集成,%) |
|---|---|---|---|
| WDict(词级 + 回退字典) | 22.0 | 24.2 | 36.8 |
| BPE-60k | 21.5 | 24.5 | 40.9 |
| BPE-J90k(joint) | 22.8 | 24.7 | 41.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/80k | SentencePiece 8k(shared) | SentencePiece + 预分词 8k |
|---|---|---|---|
| ja→en | 28.24 | 29.55 | 29.85 |
| en→ja | 20.06 | 21.62 | 20.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_base | 50,257 | 256 byte + 50,000 merges + 1 special | tiktoken openai_public.py:26;HF 文档 §Byte-level BPE |
| cl100k_base | 100,277 | max token id 100,276 + 1 | tiktoken openai_public.py:85、core.py:373 |
| o200k_base | 200,019 | max token id 200,018 + 1 | tiktoken openai_public.py:100、core.py:373 |
| Llama 3 | 128,000 | 100K tiktoken + 28K 非英语 | Llama 3 Table 3、Llama 3 §3.2 |
| Qwen2 | 151,646 | 151,643 regular + 3 control | Qwen2 §2.1、Qwen2 Table 1 |
| Qwen3 | 151,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,非英语只描述方向性改善。
SentencePiece 用 8k 共享词表,词表规模比 80k/80k 词模型小得多,却在 KFTT 两个方向都拿到更高 BLEU(SentencePiece Table 1)。 这不是「小词表一定更好」,而是同一份数据、同一个 NMT 配置下,子词把稀有词拆成可复用的片段,减少了 UNK 和稀疏词表带来的损失。 推断(由 SentencePiece Table 1 的 BLEU 与 SentencePiece §4.1 对预分词的讨论推出;原文没有给出这一机制的直接解释)。 换成英语到日语、或者换一个语料,预分词和词表大小的最优组合会变;表 5 的 20.86 就是预分词拖后腿的样本。
把失败模式写清楚:贪心合并、词表冻结、special token 注入、多语言证据缺口,以及原文自身的不一致。
src/lib.rs:390-431)。用户文本里如果出现同名字符串,是否被当成 special token 取决于
allowed_special 的设置(src/lib.rs:378-398);模板拼接也会改变 token 序列,
但模板不是模型学出来的。
\p{N}{1,3} 把连续数字按
1–3 位切块
(openai_public.py:89)。这对算术、版本号、长 ID 不利:同一个数换一种写法,token 序列可能完全不同。
这是预切分规则的选择,不是 BPE 合并本身能修的。
@@ 表示无法编码连续空格(SentencePiece §3.1)。
需要逐字节还原原始输入时,byte-level BPE 直接在 byte 上工作;「先归一化再切分」会先改变文本,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 与输出层的形状推出)。