同一段话交给三个真分词器,token 数差多少?
本课一问:同一段话交给三个真分词器,token 数差多少?
这是本章的实验课,不引入新概念。这一课先不碰:分词器的训练代码(交给 AI 写,见本章项目推进)。只动手验证前面几课的结论。
你手里已经有的工具
前面几课的结论:切法由词表决定(第 1 课),子词取中间(第 2 课),词表从语料里统计出来(第 3 课),字节级 BPE 永不缺字但会把没学过的语言切碎(岔路 B2),词表大小是一笔交易(第 4 课)。下面把它们一次性放到真分词器上验证。组件打开时默认就是贯穿例句「小明把苹果给了小王」:Qwen3.8 5 个 token、GPT-2 21 个;我们在繁体古白话上训练的分词器,切简体的「苹」「给」只能退回字节——三行放在一起比。
朴素的想法:分词器都差不多,随便挑一个
- ① 朴素方案
- ② 它在哪失败
- ③ 改进方案
两个分词器都是字节级 BPE,都能把任何文字编码、再无损解码回来。看上去随便挑一个都行。
| 中文段落(209 字) | 同义英文段落(817 字符) | |
|---|---|---|
| GPT-2 | 461 个 token | 149 个 token |
| Qwen3.8 | 106 个 token | 149 个 token |
「都能编码」不等于「一样好」。token 数直接决定训练要处理多少个位置、API 花多少钱、上下文窗口装多少内容。所以选分词器之前先在自己的语料上测;现成的都不合适,就像我们的项目一样自己训一个。反过来也成立:我们在三国演义上训的分词器,切简体现代文会很吃力——下面亲手看。
三个真分词器,同一段话
这节实验覆盖演进图里的两代:
- 解决了不用训练,任何文字都能编号。卡在码点从 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」。
看什么: ① 同一句中文,三行的 token 数与压缩率差多少——这就是「词表对这种语言的友好度」;② 橙色虚线框:一个字被拆成了几个字节 token,框里的编号合起来才是一个完整的字(单独解码会显示 �,不是 bug);③ 点「strawberry 问句」:句中的「 strawberry」在两个大词表里都是 1 个 token;④ 点「价格与数字」:Qwen3.8 把 100 切成 1、0、0 三块,GPT-2 整块切;⑤ 点「三国演义原句」,再点「今天天气真好」,比较我们的分词器在「见过的文体」与「没见过的简体字」上的差别。
实验记录建议(给本章项目用): 选一段你的项目将来要处理的文字,把三行的 token 数、压缩率、fertility 抄下来。第 4 课的问题「词表该多大」,最终要用你自己语料上的这类数字回答。
压缩率与 fertility 的定义见第 2 课与第 4 课:压缩率 = 字符数 ÷ token 数;fertility 在文本以汉字为主时是「token 数 ÷ 汉字数」,以英文为主时是「token 数 ÷ 英文单词数」,混合文本不给 fertility。组件里所有数字都是对当前输入实时算的。
常见误解:「压缩率越高越好,没有代价」。 压缩率靠更大的词表换来:输入输出两张 V × d 的表跟着变大,罕见 token 得到的训练也更少(第 4 课)。Qwen3.8 能用 24.8 万的词表,是因为它有 5,120 维、约 278 亿参数来承担;我们的小模型用同样的词表,embedding 会吃掉 95% 以上的参数。
实时算,离线核对
组件里的每个数字都对当前输入现算:token 数来自分词器本身,字符数与字节数由浏览器数,压缩率与 fertility 由它们相除。Qwen3.8 与 GPT-2 的分词器在页面里实时加载(第一次要从 Hugging Face 下载约 13 MB 的 Qwen3.8 词表文件);加载成功后,预置例句会与离线预计算的结果逐个 ID 核对,一致时显示 ✓。网络不通时,两者自动改用离线预计算结果,只覆盖预置例句,并在状态栏写明。我们的分词器直接在浏览器里用 JS 跑 BPE,启动时先与 Python 端的编码结果逐 ID 核对。
这些数字能信到什么程度
- 它解决什么问题:把「分词器对某种语言友不友好」从感觉变成可以比较的数字。
- 数学上为什么可能有效:三个分词器都是字节级 BPE,编码可逆,token 数是确定的 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条同一版本的分词器对同一输入永远给出同一串 ID 出处待补 。
- 实验上为什么最后选了它:token 数是对某个具体版本分词器的测量 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条Qwen3.8 与 GPT-2 的离线结果固定在 tokenizer 文件的某个 revision 上;实时加载的是当前最新版本,不一致时组件会标出 出处c1-tokenize.json · _meta ↗需联网 ;我们的分词器的数字只代表三国演义这份语料。
一句话、下一个缺口、你的提问
一句话: 同一段话交给不同的分词器,token 数可以差好几倍;分词器要和你的语料、你的语言匹配——先测,再选或自己训。
下一个缺口
分词、查表都做完了:一句话已经是一个 [T × 5120] 的矩阵。但查表只看 ID,「小明打了小王」和「小王打了小明」得到的是同一组向量。C2:位置——模型怎么知道顺序
你的提问
C1 还没有真实提问记录。实验里遇到看不懂的切法,直接在对话里问,会归档到 05-问答记录/。回到本章导读看项目推进:用这些数字给我们自己的分词器定词表。