分词与 BPE Tokenization & BPE
所属:01-fundamentals / 01-llm-core
面试权重:★★
来源:通用领域知识;未做一手考证
一句话#
Tokenization 是把文本映射成词表内 token id 序列的有损、不可微的前处理步骤;BPE 是其中最主流的子词(subword)算法,通过反复合并频次最高的相邻字节/字符对来构造词表,让常见词成为一个 token、生僻词被拆成若干子词。
为什么重要(面试角度)#
它本身不难,但踩坑密度极高,是区分"调过模型"和"只跑过 demo"的分水岭。面试官常问的点:
- 为什么是子词而不是词/字符?
- BPE、WordPiece、Unigram 的区别(很多人的答案是错的)
- 中文/多语言/数字上 tokenizer 的具体失效模式
- 换 tokenizer 为什么不能"只换前端"、词表扩展要做什么
- 为什么跨模型的 loss 不能用 per-token 数值比较
你在 HKGAI 做香港本地大模型的数据工程时大概率撞过中文 token 效率问题(主流基座的词表以英文为主),这题可以答出一手体感。
核心内容#
为什么需要子词#
| 粒度 | 问题 |
|---|---|
| 词级(word) | 词表爆炸、OOV 无解、形态变化(run/running)互相独立 |
| 字符级(char) | 序列过长(注意力 直接变贵)、单字符语义太弱 |
| 子词(subword) | 折中:高频词整体成 token,低频词靠子词组合,理论上无 OOV |
BPE(Byte-Pair Encoding)#
训练流程(从零到词表):
- 初始化词表为基础单元(字符,或 byte 级 BPE 里的 256 个字节);
- 统计语料中所有相邻 token 对的频次;
- 把频次最高的一对合并成新 token,加入词表;
- 重复 2–3,直到达到目标词表大小。
产出的核心产物是有序的 merge 列表——顺序有意义,因为它就是编码时应用的优先级。编码阶段按 merge 顺序贪心地依次应用,所以同一段文本在不同 BPE 实现下可能切得不同。
Byte-level BPE(GPT-2 起的主流做法):基础单元是 256 个字节,先把文本按 UTF-8 编码成字节序列再合并。好处是任何 Unicode 字符都能表示,永远不需要 <UNK>。代价是非拉丁文字被"打折":一个中文字符占 3 个 UTF-8 字节,合并不充分时会长到 2–3 个 token。
三种子词算法的区别(高频错点)#
| 算法 | 合并准则 | 编码方式 | 代表 |
|---|---|---|---|
| BPE | 合并频次最高的相邻对(贪心、频次驱动) | 按 merge 顺序贪心应用 | GPT 系、Llama 系 |
| WordPiece | 合并能最大提升语料似然的对(用 pair 频次与各自频次构造分数) | 贪心最长匹配优先 | BERT |
| Unigram LM | 从大词表出发,按删除后语料似然损失最小的原则逐步剪枝 | Viterbi 求全局最优切分(概率模型) | SentencePiece 的 unigram 模式、T5 |
要点:BPE 与 WordPiece 都是自底向上合并,Unigram 是自顶向下剪枝;Unigram 是概率模型,理论上可以做 subword regularization(采样切分做数据增强),BPE 不行。WordPiece 用 ## 前缀标记"非词首"子词。
SentencePiece 不是一种算法,而是一个把文本当原始 Unicode 流处理的工具包(可跑 BPE 或 Unigram)。它把空格也当成字符(记为 ▁,U+2581),因此无需语言相关的前置分词,且编码可逆(detokenize 能还原原文本,包括空格)。这对中文、日文这种无空格语言尤其重要。
词表大小与压缩率的权衡#
- 词表越大:序列越短(同样的上下文窗口能装下更多内容)、推理更快、训练时同样的 token 预算覆盖更多文本;但嵌入矩阵和输出层参数变大( 各一份,常占小模型参数的相当比例),尾部 token 训练不充分。
- 公开配置可作参考(引用具体模型时请核对对应版本):GPT-2 为 50,257;Llama 2 为 32,000;Llama 3 提升到 128,256;Qwen 系列约 15 万级。多语言模型倾向大词表,就是为了提高非英语的压缩率。
常见失效模式#
- 数字:多数 tokenizer 把数字切成不固定长度的块,导致算式两边切法不一致(
1234与1235可能一个整块一个拆成两段),这是 LLM 算术能力弱的诱因之一。数字逐位切分(digit-level)是可选的改进方向。 - 中文/多语言:压缩率显著低于英文,同样的上下文窗口能装的中文内容更少;港式粤语、繁体/简体混排、全角半角差异会进一步恶化。
- 前后导空格:
" hello"与"hello"是不同 token。"答案: 好" 和 "答案:好" 完全可能切法不同,这是 chat template 事故的常见来源。 - 特殊 token:
<|im_start|>、<|eot_id|>等必须逐字符一致;一旦被当普通文本切碎,格式立刻崩。 - 稀有/未登录 token:词表里存在但训练语料几乎没出现过的 token(社区俗称 glitch token)可能触发异常行为,因为其 embedding 基本没被训练。
- 规范化(normalization):NFKC 之类的 Unicode 规范化会改变字符,
₁/²/全角字符可能被折叠。训练与推理必须用同一套规范化规则。
词表变更与继续预训练#
tokenizer 与模型权重是绑死的:embedding 行与 lm_head 行按 token id 索引。因此:
- 换 tokenizer 等价于重init 嵌入层,必须继续预训练才能恢复,绝不可能只换前端。
- 扩展词表(加中文/领域 token):新 token 的 embedding 是随机初始化的,需要继续训练;同时新旧 token 的竞争会改变已有的切分习惯,通常还要一并训练原词表部分。
- 实践顺序是:先定 tokenizer(在目标语料上训练)→ 再训练模型。事后改代价很高。
评测:为什么不能比 per-token loss#
不同 tokenizer 的 token 定义不同,同一个句子的 token 数不同,per-token loss 不可比。跨模型比较要用 bits-per-byte(BPB) 这类与切分无关的指标:把每个 token 的 log 概率按其覆盖的字节数折算,除以总字节数。
常见面试问法#
Q:BPE 的训练过程描述一下。 A:初始化为基础单元(字节级则 256 个字节),反复统计相邻单元对频次、合并最高频的一对、写入词表,直到达到目标词表大小。产物是一份有序的 merge 规则,编码时按顺序贪心应用。它是纯频次驱动的贪心算法,与语言知识无关。
Q:BPE、WordPiece、Unigram 有什么区别? A:BPE 按频次贪心合并;WordPiece 按"合并后对语料似然的提升"打分(等价于用 pair 频次除以各子词频次的构造)并贪心最长匹配编码;Unigram 反过来,从大词表出发按似然损失剪枝,是概率模型,用 Viterbi 求最可能切分,还能采样做 subword regularization。SentencePiece 是工具包,可以跑前两者中的任一种。
Q:为什么大模型都用子词? A:词级会 OOV 且词表爆炸,字符级序列太长(注意力 太贵、单字符语义弱)。子词让高频词整体成 token、低频词可组合,兼顾序列长度、词表规模和 OOV 覆盖。
Q:模型换 tokenizer 需要做什么? A:不能只换前端。embedding 与 lm_head 按 token id 索引,换词表等于重置这两层(以及可能的输出偏置),必须做继续预训练。扩词表同理:新行随机初始化,要训;还要注意新旧 token 竞争会改变原有切分行为。实践上 tokenizer 应当在预训练之前就定好。
Q:为什么中文场景下模型的上下文利用率感觉更差? A:中文的 token 压缩率低——同样字数的中文往往占更多 token,所以同样标称的上下文窗口能装的中文内容更少,成本也更高。缓解手段包括:用中文占比高的语料训练更大的词表、在 SFT/继续预训练阶段扩展词表、以及按字节或按字符而不是按 token 做长度预算。
易错点#
- ❌ 说"BPE 选择概率最高的合并"——BPE 是频次驱动;WordPiece 才是似然准则;Unigram 才是概率模型。
- ❌ 认为 SentencePiece 是第三种算法——它是实现 BPE/Unigram 的工具包。
- ❌ 以为换 tokenizer 只要改前端代码,或以为词表可以随便扩。
- ❌ 拿不同模型的 per-token loss / perplexity 直接比较。
- ❌ 忽略空格与全角/半角差异,把 chat template 不匹配归因于"模型变笨"。
- ❌ 认为 Byte-level BPE 对中文没有代价——一个汉字 3 字节,压缩率是实打实的劣势。
- ❌ 把 tokenization 当成可微的一部分去"训"——它本身不可微,只能通过词表设计和继续预训练间接影响。
自测#
1. BPE 训练好之后,编码一个新句子时是怎么切分的?是重新跑一遍合并统计吗?
答案
不是。训练产出的是一份有序的 merge 列表(合并规则)。编码时把文本先切成基础单元(bytes),然后按 merge 列表的顺序,能应用就应用,最终得到 token 序列。所以同一个句子用不同实现(甚至不同版本的库)可能切法不同——merge 的应用顺序和实现细节都会影响结果。
2. 为什么 Byte-level BPE 能做到没有 <UNK>,代价是什么?
答案
因为它以 256 个字节为基础单元,任何 Unicode 文本都能表示为 UTF-8 字节序列,因此不存在无法表示的字符,也不需要回退到 UNK。代价是非 ASCII 文本的压缩率差:一个汉字/日文汉字占 3 个字节,合并不充分时就要 2–3 个 token 表示,导致序列更长、上下文利用率更低、成本更高。
3. 你发现微调后模型输出格式不稳定、偶尔漏掉 role 标记。最该先检查什么?
答案
先检查 tokenizer 与 chat template 是否训练/推理完全一致:特殊 token(如 <|im_start|>)是否被当成普通文本切碎、前后是否有多余/缺失空格、是否用了 add_special_tokens 重复添加 BOS。其次是特殊 token 本身的 embedding 是否被训练到(新增特殊 token 需要训练对应行)。这类事故的根因几乎总在 tokenizer/模板层,而不是权重。
4. 团队想把一个英文为主的基座适配中文,通过扩词表继续预训练。有哪些坑?
答案
① 新 token 的 embedding 与 lm_head 行是随机初始化的,必须继续训练,否则输出乱码或高频出现新 token;② 新旧 token 存在竞争,扩展后原词表的切分统计被扰动,要同时更新两侧;③ 词表变大后 embedding/lm_head 参数增加,小模型上占比可观;④ 需重新对齐 chat template 与特殊 token 的 id;⑤ 评测要换到 bits-per-byte 之类的可比指标,否则与基线不可比。
5. 为什么说 tokenizer 的粒度会间接影响模型的算术和推理能力?
答案
因为切分决定了模型看到的"最小单位"。数字被切成不定长块时,同一个数在不同上下文里可能切法不同,模型难以学到稳定的位值对齐关系;数字位数变长时切分还会漂移。此外分词边界会影响跨 token 的注意力模式(如多位数加法需要跨 token 对齐),所以 tokenization 是影响算术表现的一个真实但常被忽视的因素。需要说明的是,这只是一个诱因,模型算术差更主要的原因是训练数据与结构,具体归因在学界仍有争议。