大模型 Tokenization 教學:BPE 合併、中文多語言成本與 152K 詞表取捨
模型不识字:BPE 合并、中文多语言税、15 万词表的账,全部手算一遍
這篇大模型課程第 4 課以 6 個可手算例子,拆解 Tokenization、BPE 合併規則,以及中文 token 成本和詞表大小的取捨。文中估算中文文本較英文多用 54%–69% token;Qwen 151,851 詞表較 Llama 2 32,000 詞表多約 491M Embedding 參數,以更少中文 token 換取較短序列,推理加速 30–50% 亦屬估算。
简介: 本课详解大模型“识字”第一步——Tokenization:为何中文需更多token?BPE如何从语料学出分词器?152K词表值不值?重复输出、隐藏计费等现象根源何在?6个可手算小例,讲透分词本质。课程第4课,34讲全开源。
前三篇讲了 Self-Attention、Transformer 的骨架、现代结构改的五处。这一篇往回退一步,讲最靠前、也最容易被跳过的一步:模型不识字,它只吃数字。把「我爱吃苹果」这五个汉字变成一串整数 ID 的过程,叫 Tokenization。一共 6 个问题,每个都用能手算的小例子。内容来自我做的一套 34 课大模型课程的第 4 课。
- 本课在线阅读(免登录、手机可看):https://zhuangzhaoyeye.github.io/llm-course/lesson-4.html
- 全部 34 课:https://zhuangzhaoyeye.github.io/llm-course/
- 源码(MIT 许可):https://github.com/zhuangzhaoyeye/llm_course
问题 1:「我爱吃苹果」五个汉字,模型怎么变成一串数字?
模型内部只有矩阵乘法,输入必须是一组整数。所以中间必须有一层把文本切块、再把每块映射成一个编号——切出来的块叫 token,这套规则叫分词器(tokenizer)。难点在于中文没有空格:「吃」和「苹果」之间没有天然分隔符。
同一个句子,三种切法差得很远:
| 切法 | 中文结果 | token 数 | 英文 "I love apples" | token 数 |
|---|---|---|---|---|
| 按字符 | 我 / 爱 / 吃 / 苹 / 果 | 5 | I / l / o / v / e / a / p / p / l / e / s | 11 |
| 按词 | 我 / 爱 / 吃 / 苹果 | 4 | I / love / apples | 3 |
| 按子词(BPE) | 我 / 爱吃 / 苹果 | 3 | I / love / apple / s | 4 |
两头都会撞墙:
- 切太细(字符/字节):序列变长,而 Attention 的复杂度是 O(n²),长度一涨计算量平方级上升。英文按字母切尤其糟,"love" 被拆成 4 个。
- 切太粗(整词):词表要装下所有词,中文动辄数万词起步(估算),而且永远有没见过的新词,直接变成 [UNK],信息全丢。
子词是唯一同时躲开两头的切法。没见过的 "apples" 也能用 "apple" + "s" 拼出来,永远不会输出 [UNK]。这就是 BPE 这类算法存在的原因。
问题 2:BPE 是怎么从一堆文本里「学」出一个分词器的?
BPE = Byte Pair Encoding,最初是 1994 年用于数据压缩的算法,2016 年被 Sennrich 等人引入机器翻译,之后成了 GPT、Llama、RoBERTa 系列的标配。
它的规则只有一句:反复把「出现次数最多的相邻符号对」合并成一个新符号,直到词表装满。
拿语料 low / lower / lowest 走一遍,目标词表 12 个符号。
第 0 步,全部按字符拆开:
| 文本 | 初始切分 |
|---|---|
| low | l · o · w |
| lower | l · o · w · e · r |
| lowest | l · o · w · e · s · t |
初始词表 7 个:{l, o, w, e, r, s, t},离 12 还差 5 次合并。
第 1 步,数相邻对频率:
| 符号对 | 次数 | 来源 |
|---|---|---|
| (l, o) | 3 | 每个词都以 lo 开头 |
| (o, w) | 3 | low / lower / lowest |
| (w, e) | 2 | lower / lowest |
| (e, r) | 1 | lower |
| (e, s) | 1 | lowest |
| (s, t) | 1 | lowest |
(l, o) 和 (o, w) 都是 3 次。这里假设先选 (l, o),合并成 lo,词表 8/12。
第 2 步,(lo, w) 出现 3 次,合并成 low,词表 9/12。第 3 步,(low, e) 出现 2 次,合并成 lowe,词表 10/12。
第 4 步出现平局:(lowe, r) 和 (lowe, s) 各 1 次。原始论文只规定「取最高频对」,平局怎么破由具体实现决定(比如取字典序最小)。假设 r 先行,合并成 lower,词表 11/12。
第 5 步,(s, t) 出现 1 次,合并成 st,词表达到 12,停止。
最终词表:{l, o, w, e, r, s, t, lo, low, lowe, lower, st}。
新词 "lowest" 怎么切?把学到的合并规则依次作用:先合并出 lowe,剩下 st,得到 2 个 token。语料里没出现过的 "lowers" 也能切成 lower + s——两个都在词表里。这就是 BPE 的核心价值:词表只有 12 个符号,却能拼出没见过的词。
顺带说清三个名字:BPE(GPT/Llama 系)、WordPiece(BERT)、Unigram(T5)是三种不同的子词算法;SentencePiece 是实现了其中 BPE 和 Unigram 的库,本身不是算法。
问题 3:中文「我喜欢吃苹果」和英文「I like eating apples」,中文要多花多少 token?
先看编码层:一个常用汉字在 UTF-8 里占 3 个字节,一个英文字母占 1 个字节。再看统计层:BPE 的合并顺序完全由训练语料决定,而 Llama 2 的预训练语料里约 89.7% 是英文。英文的 "the" 出现了几百万次,早被合并成一个 token;中文的「的」频率也高,但要先合并字节、再合并字、再合并词,层级更多、机会更少。
单看高频词会低估这个问题:
| 文本 | token 切分 | token 数 |
|---|---|---|
| I like eating apples | I / like / eating / apples | 4 |
| 我喜欢吃苹果 | 我 / 喜欢 / 吃 / 苹果 | 4 |
这两句看着一样,是因为「喜欢」「苹果」都是已合并的高频双字词。换成生僻表达或整段文本,差距才显出来(以下为估算,依 tokenizer 而异):
| 内容 | 英文 token | 中文 token | 中文溢价 |
|---|---|---|---|
| 100 词短文 | ~130 | ~200 | +54% |
| 500 词说明 | ~650 | ~1100 | +69% |
后果直接落在上下文窗口上:同样 4096 token,英文能塞约 3000 词,中文只能放约 2200 字,可用信息量少约 40%。这就是常说的「多语言税」。
问题 4:词表从 32K 扩到 152K,Embedding 层大了近 5 倍,这买卖划算吗?
词表变大,最直接的变化落在 Embedding 上:参数量 = 词表大小 × d_model。
| 模型 | 词表 V | d_model | Embedding 参数 | 占 7B 总参数 |
|---|---|---|---|---|
| Llama 2 7B | 32,000 | 4096 | 32,000 × 4096 = 131,072,000 ≈ 131M | 131M / 7B ≈ 1.9% |
| Qwen 7B | 151,851 | 4096 | 151,851 × 4096 ≈ 622M | 622M / 7B ≈ 8.9% |
两者相差约 491M 参数。注意输入侧的 token embedding 和输出侧的 LM Head 通常共享权重,所以算一次 131M,不是两倍。
换来的是每个 token 承载更多文本(以下为估算):
| 文本 | Llama 32K | Qwen 152K | 节省 |
|---|---|---|---|
| 今天天气真好,我们出去走走吧 | ~18 token | ~12 token | -33% |
| 请帮我写一段关于机器学习的介绍 | ~25 token | ~15 token | -40% |
| 1000 字中文文章 | ~1800 token | ~1000 token | -44% |
token 少了,Attention 的 O(n²) 少算、KV Cache 少读写,同等文本的推理延迟能降 30–50%(估算),显存占用同比例下降。
所以对中文为主的场景,这笔买卖是划算的:多出的 491M 参数是一次性加载成本,但每个请求都更快。反过来说,词表不是越大越好——152K 以上再加,新增的几乎都是极低频碎片,实际用不到,还会挤占其他层的参数预算。Qwen 的 152K 更像是一个工程甜点,不是理论上限。
问题 5:为什么模型有时会「卡壳」——不停重复同一个词,或在句尾加无意义标点?
这个现象叫文本退化(text degeneration),Holtzman 等人在 2019 年的 The Curious Case of Neural Text Degeneration 里系统记录过。
先澄清根因:根因是解码策略,不是 tokenization。贪心地每步取最高概率、或用 beam search 最大化整体似然,都会把概率分布的长尾整个排除掉,生成于是陷进高概率循环。tokenization 和 Attention 会放大这个现象,但不是起因。
token 侧的诱因很具体。假设「的」被编码成独立 token:
| 场景 | 文本 | 切分 | token 数 |
|---|---|---|---|
| 正常 | 这本书的内容很有意思 | 这本 / 书的 / 内容 / 很 / 有意思 | 5 |
| 退化 | 这本书的的的的的的 | 这本 / 书的 / 的 / 的 / 的 / 的 / 的 / 的 | 8 |
输出一个「的」之后,下一个 token 看到的上下文和上一个几乎一样,于是又输出「的」——环形轨道。这类高频单字 token,模型在训练时见过无数次「独立出现」的模式,学会了「看到我就输出我」。
更极端的是词表里的「幽灵 token」。2023 年 Rumbelow 和 Watkins 报告过一个例子:Reddit 用户名 "SolidGoldMagikarp" 被分词器单独编码成一个 token,但它在训练数据里只出现过极少几次,模型对这个 token 几乎没学到语义。一旦输入触发它,输出就不可预测。
缓解手段分两侧:token 侧可以扩大词表、增加训练数据多样性,减少诱因;解码侧可以加 repetition_penalty(对已出现 token 降权)、top-p(nucleus sampling,只在累积概率够高的候选集里采样)、temperature。因为根因在解码,解码侧的手段最直接对症。
问题 6:你和模型聊一轮,真正被计费的 token 里,有多少根本不是你自己打的字?
你发的只是「你好」,模型收到的却是一整段带标记的文本。ChatML 用 <|im_start|> / <|im_end|> 这对特殊 token 把「角色名 + 内容」包成一轮;Llama 2 用的是 [INST] … [/INST]。这些标记由对话模板自动插入,你看不见,但照样分词、照样计费、照样占上下文窗口。
一轮 user 消息的骨架:
| 片段 | 内容 | 是你打的字吗 | token | ||
|---|---|---|---|---|---|
| 系统提示 | You are a helpful assistant. | 否,系统注入 | 6 | ||
| `< | im_start | >` | 轮次开始 | 否 | 1 |
| user | 角色名 | 否 | 1 | ||
| 换行 | 格式控制 | 否 | 1 | ||
| 你好,帮我写封邮件 | 用户输入 | 是 | 6 | ||
| `< | im_end | >` | 轮次结束 | 否 | 1 |
把模板开销按 10 轮累加(示意口径):
| 项目 | 每轮 | 10 轮累计 |
|---|---|---|
| user 轮 4 个角色标记 | 4 | 40 |
| assistant 轮 4 个角色标记 | 4 | 40 |
| 角色标记小计 | 8 | 80 |
| 系统提示(每轮都重发) | 500 | 5000 |
| 看不见的开销合计 | — | 5080 |
再设每轮正文 user 20 + assistant 80 = 100 token,10 轮正文 1000 token。总 token = 5000 + 80 + 1000 = 6080。
| 组成 | token | 占比 |
|---|---|---|
| 系统提示 | 5000 | 82.2% |
| 角色标记 | 80 | 1.3% |
| 用户 + 助手正文 | 1000 | 16.4% |
| 合计 | 6080 | 100% |
把系统提示也算进「看不见的开销」,就是 5080 / 6080 ≈ 83.6%。这个数字随系统提示长短变化,但结论稳定:多轮对话里,你自己打的字往往只是账单的一小部分。
这也是为什么 token 有两重身份:一是钱,二是上下文额度。二者同时消耗,而消耗它们的大部分内容你从没打出来过。
小结
| 问题 | 结论 | 一个数字 |
|---|---|---|
| 文本怎么变数字 | 切 token 再查表映射成 ID | 按词切只要 3 个,按字母切要 11 个 |
| BPE 怎么学 | 反复合并最高频相邻对 | 语料 3 个词,5 次合并到 12 个符号 |
| 中文多花多少 | 编码 3 字节 + 语料英文占多数 | 整段文本中文多 +54%~+69%(估算) |
| 词表扩到 152K | 用 Embedding 参数换 token 效率 | 131M → 622M,多 491M 换推理快 30–50%(估算) |
| 为什么重复 | 根因是解码策略排除长尾 | 10 轮模板开销 80 token |
| 多少不是你打的字 | 系统提示 + 角色标记都计费 | 5080 / 6080 ≈ 83.6%(示意) |
参考与说明
- 子词分词与 BPE 引入 NLP:Sennrich, Haddow, Birch, Neural Machine Translation of Rare Words with Subword Units, 2016,https://arxiv.org/abs/1508.07909
- BPE 原始用途:Gage, A New Algorithm for Data Compression, 1994
- Llama 2 的 32K 词表与语料构成:Touvron 等,2023,https://arxiv.org/abs/2307.09288
- Qwen 词表 151,851(151,643 常规 token + 208 控制 token):https://github.com/QwenLM/Qwen/blob/main/tokenization_note.md
- 文本退化与 nucleus sampling:Holtzman 等,2019(ICLR 2020),https://arxiv.org/abs/1904.09751
- SolidGoldMagikarp:Rumbelow & Watkins, LessWrong, 2023
文中可复算的数字(三种切法 token 数、BPE 合并过程、Embedding 参数、多轮模板开销、占比)我都用代码独立复算过。标「估算」的是没有一手来源的经验数字——中文溢价、token 节省比例、推理加速比例——需要按你所用的具体 tokenizer 实测。
说明:这套课程由 AI 在我设计的规则下生成,再经过脚本检查和独立审查;审查发现过不少问题,都写在仓库 README 里。如果你发现错误,欢迎在仓库提 Issue,写清位置,我会逐条核实。
继续学
上一篇是第 3 课「现代 Transformer 的五处改造」,讲 RoPE、GQA、RMSNorm、SwiGLU、Pre-Norm 各自被什么问题逼出来。这一课之后是第 5 课「逐层解剖一个模型」,把分词结果真正送进网络里跑一遍。每张卡片后面还有 AI 助教,可以接着追问(每人每天有次数限制)。
- 课程:https://zhuangzhaoyeye.github.io/llm-course/
- 源码:https://github.com/zhuangzhaoyeye/llm_course ,觉得有帮助的话欢迎点个 Star
來源:阿里雲開發者社區 AI · developer.aliyun.com