分词算法(Tokenization)¶
面试高频考点¶
- BPE、WordPiece、SentencePiece 的区别?
- 为什么不直接按字/词切分?
- 词表大小如何权衡?
- BBPE vs BPE?中文分词有什么特殊处理?
- Tokenization 对 LLM 性能有什么实际影响?
一、为什么需要子词分词?¶
三种粒度对比¶
句子:"我喜欢吃北京烤鸭"
字符级切分:
["我", "喜", "欢", "吃", "北", "京", "烤", "鸭"]
词表:~5000 (中文字符+标点+英文大小写)
优点:词表最小,无OOV
缺点:序列最长(本句8 tokens),每个token语义信息极少
词级切分:
["我", "喜欢", "吃", "北京烤鸭"]
词表:~50万+(中文词量巨大)
优点:序列最短,每个token语义丰富
缺点:词表爆炸,OOV(未登录词)严重,"武汉热干面"不在词表里就得拆成字
子词切分(Subword):
["我", "喜欢", "吃", "北京", "烤", "鸭"]
词表:~32K-100K
优点:高频词保持完整("喜欢"),低频词拆成子词("北京烤鸭"→"北京"+"烤"+"鸭")
子词分词 = 高频短语保持词级 + 低频词用可复用子词单元组合,兼顾了词表和序列长度。
二、BPE(Byte Pair Encoding)¶
细化理解: BPE 的直觉是从字符或字节开始,反复合并语料中最常见的相邻片段。高频词会逐渐变成较长 token,低频词仍能由较短片段拼出来,因此可以兼顾词表规模和 OOV 问题。它的工程优势是简单、可控、适合大规模训练;缺点是合并规则只看频次,不理解语义边界,所以数字、代码和跨语言文本可能出现不理想切分。
使用模型:GPT-2/3/4、RoBERTa、LLaMA 等。
训练算法¶
输入:语料库,目标词表大小 V
输出:BPE 合并规则列表
1. 初始化:将所有词拆成字符序列,每词末尾加 </w>
例如:"low low lower newest widest" →
l o w </w> l o w </w> l o w e r </w> n e w e s t </w> w i d e s t </w>
2. 统计所有相邻符号对的出现频率
3. 重复(直到词表达到 V):
a. 找出频率最高的符号对 (A, B)
b. 将 (A, B) 合并为 AB,加入词表
c. 更新语料:将所有 A B 替换为 AB
d. 重新统计频率
4. 输出:合并规则列表(用于推理时对新文本做同样合并)
完整示例¶
初始字符序列 + 频率:
l o w </w> (×5)
l o w e s t </w> (×2)
n e w e s t </w> (×6)
w i d e s t </w> (×3)
迭代 1:频率最高 = (e, s) → 合并为 "es",词表 +1
迭代 2:频率最高 = (es, t) → 合并为 "est",词表 +1
迭代 3:频率最高 = (est, </w>) → 合并为 "est</w>",词表 +1
迭代 4:频率最高 = (l, o) → 合并为 "lo",词表 +1
迭代 5:频率最高 = (lo, w) → 合并为 "low",词表 +1
...
最终词表包含:l, o, w, e, s, t, n, i, d, es, est, lo, low, low</w>, est</w>, ...
推理时对 "lowest" 分词:
从字符开始 → l o w e s t
应用合并规则:lo + w → low
e + s → es
es + t → est
结果:["low", "est"]
BBPE(Byte-Level BPE)¶
GPT-2 引入的改进:在字节级操作。256 个可能的字节值作为初始"字母表",任何 Unicode 字符都被编码为 UTF-8 字节序列。
"你好" → UTF-8 字节 → [0xE4, 0xBD, 0xA0, 0xE5, 0xA5, 0xBD]
BPE 合并在这些字节上执行,而非字符。完全消除了 OOV 问题——任何 Unicode 文本都可由 256 个字节表示。
三、WordPiece¶
使用模型:BERT 及变体。
与 BPE 的核心区别¶
| 维度 | BPE | WordPiece |
|---|---|---|
| 合并标准 | 频率最高的符号对 | 最大化语言模型似然增益 |
| 数学上 | 贪心最大频率 | 贪心最大互信息 |
| 子词标记 | 无 | ## 前缀表示非开头子词 |
WordPiece 的合并标准¶
给定两个子词 A 和 B,WordPiece 选择使以下比值最大的对:
score(A, B) = count(A, B) / (count(A) × count(B))
这衡量了 A 和 B 共现的统计显著程度——不是"出现得多不多",而是"在一起出现的程度是否远超随机"。
例子:
"un" 和 "able" 的共现:
count("un", "able") = 10000 ("unable"出现了 10000 次)
count("un") = 50000
count("able") = 20000
score = 10000 / (50000 × 20000) ≈ 10⁻⁵ (低!"un"和"able"各自都很常见)
"un" 和 "believ" 的共现:
count("un", "believ") = 500 ("unbelievable"出现了 500 次)
count("un") = 50000
count("believ") = 800 ("believ"几乎只在"unbelievable"中出现)
score = 500 / (50000 × 800) ≈ 1.25 × 10⁻⁵ (也低,但比上一对略高)
实际中 WordPiece 会选择使整体语料似然提升最大的合并。
WordPiece 的分词过程¶
输入:"unbelievable"
1. 从第一个字符开始,找词表中能匹配的最长子串
词表中有 "un" → 匹配 → 继续
2. 从剩余部分 "believable" 找最长匹配
词表中没有 "believable" → 缩短
词表中没有 "believabl" → 继续缩短
词表中有 "##believ" → 匹配(## 表示非开头子词)
3. 从剩余部分 "able" 找最长匹配
词表中有 "##able" → 匹配
4. 剩余为空 → 完成
结果:["un", "##believ", "##able"]
四、SentencePiece¶
使用模型:T5、LLaMA、Qwen、Mistral 等。
核心创新:不依赖预分词¶
BPE 和 WordPiece 都需要先按空格/标点将文本切成词(pre-tokenization),这在中文、日文等无语空格分隔的语言中是个问题。SentencePiece 直接从原始文本学习,将空格视为普通字符之一。
英文 "Hello world" →
BPE 预分词:["Hello", "world"] → "Hello" → "H" "e" "l" "l" "o"
SentencePiece:直接 "H" "e" "l" "l" "o" "▁" "w" "o" "r" "l" "d"
(▁ 表示空格,是普通字符)
中文 "你好世界" →
BPE 预分词:需要先做中文分词 → 依赖外部分词器
SentencePiece:直接 "你" "好" "世" "界"
(无需预分词!)
Unigram 语言模型(SentencePiece 的主力算法)¶
训练流程:
1. 初始化一个大词表(包含所有可能的子词候选,通常 ~100 万)
2. 用 EM 算法估计每个子词的 unigram 概率 P(x_i)
3. 对每个子词计算:如果把它从词表中删除,语料整体似然损失多少?
损失 = -log P(用剩余子词重新分词) + log P(当前分词)
4. 删除损失最小的 20% 子词(对整体似然影响最小的)
5. 重复步骤 2-4,直到词表缩减到目标大小
Unigram 的优势:输出每个分词的概率,可以在训练时做子词采样(subword regularization)——不总选最优分词,而是按概率随机采样一个分词结果,起到数据增强作用。
BPE vs Unigram(同为 SentencePiece 支持的算法)¶
| 维度 | BPE | Unigram |
|---|---|---|
| 训练方向 | 从底向上(合并) | 从顶向下(删减) |
| 训练速度 | 快 | 慢(需要多轮 EM) |
| 推理速度 | 快(确定性规则) | 快(Viterbi 解码) |
| 子词采样 | 不支持 | 原生支持 |
| 最终质量 | 好 | 略好(理论上更优) |
五、词表大小的权衡¶
细化理解: 词表越大,平均序列长度通常越短,但 embedding 和 LM head 参数会增加,低频 token 也更难训练充分;词表越小,模型输入序列更长,attention 与 KV Cache 成本上升。多语言模型还要考虑不同语言的 token tax:某些语言被切得更碎,同样语义会消耗更多上下文预算,这会影响公平性和实际使用成本。
小词表 (8K-16K):
优点:Embedding 层参数少(16K × 768 ≈ 12M 参数),训练/推理快
缺点:每个 token 表示极短文本片段,序列变长,语义信息稀疏
中词表 (32K-64K):
优点:平衡序列长度和参数开销,高频词完整保留
缺点:低频词和专有名词仍可能被拆得太碎
代表:LLaMA (32K)、Mistral (32K)、Qwen (152K)
大词表 (100K-250K):
优点:序列短,多语言和代码覆盖好
缺点:Embedding 层参数大(200K × 4096 ≈ 819M 参数),训练慢
代表:Qwen (152K)、Gemma (256K)
极端案例:
纯中文 BPE (200K+):容纳更多完整中文词
多语言 BPE (250K+):每种语言的高频词都在词表中
实践建议:多语言模型通常需要 100K+ 的词表来公平覆盖各语言;纯英文模型 32K-50K 即可。
六、分词对 LLM 的实际影响¶
1. "Token Tax"——不同语言的分词效率差异¶
同一句话的 token 数:
英文:"The weather is nice today" → ~6 tokens
中文:"今天天气不错" → ~6 tokens(SentencePiece对中文还算友好)
但某些非英语语言的分词效率显著更低
这就是多语言模型中某些语言"更贵"的原因——同样的语义需要更多 token。
2. 空格和数字的处理¶
数字 "12345" 的常见分词:
GPT-2: ["123", "45"] → 不可预测的拆分
GPT-3.5+: 每个数字独立 → ["1", "2", "3", "4", "5"]
数字被拆成单个数字时,LLM 做数学计算的难度大幅增加
这就是 CoT + calculator 对数学问题有效的原因之一
3. 特殊 Token¶
工程细节: 特殊 token 是模型协议的一部分,例如 BOS/EOS、system/user/assistant 分隔符、工具调用边界、填充符和图像占位符。训练、微调、推理模板必须保持一致,否则模型可能把角色标记当普通文本,或者在多轮对话中混淆指令层级。面试中可以把 chat template 看成 tokenizer 与应用协议之间的桥。
| Token | 含义 | 作用 |
|---|---|---|
<s> / <bos> |
序列开始 | 初始 hidden state 的起点 |
</s> / <eos> |
序列结束 | 生成终止信号 |
<unk> |
未知 token | 回退(现代分词器基本不用了) |
<pad> |
填充 | batch 内补齐到相同长度 |
<|user|> <|assistant|> |
角色标记 | Chat 模板的结构化角色边界 |
七、面试延伸¶
Q:词表大小怎么选?
太小(<16K):序列过长,语义稀疏,尤其对于中文等字符丰富的语言体验差。太大(>250K):Embedding 层参数爆炸,低频子词的 Embedding 训练不充分,容易欠拟合。32K-64K 是单语言模型的 sweet spot,100K-152K 是多语言模型的常见选择。Qwen 使用 152K 词汇表,在中文和多语言能力上表现突出。
Q:中文分词有什么特殊处理?
SentencePiece 不需要特殊处理(直接在字序列上做 BPE/Unigram);BPE 类分词器通常先按字符切分中文,再在字符序列上做合并。但中文"词"的概念模糊("北京大学"是一个词还是"北京"+"大学"?),现代做法一般不过度纠结语言学上的"词",让 BPE 从数据中自己学习合理的切分单元。Qwen 等中文模型的词表专门扩充了中文常用字和子词。
Q:BBPE 和 BPE 的区别?
BBPE(Byte-Level BPE)在字节级操作,256 个字节值作为初始词表。任何 Unicode 字符都被编码为 UTF-8 字节序列后进行 BPE 合并。这完全消除了 OOV(任意 Unicode 都在 256 字节范围内),GPT-2 是第一个大规模应用此方案的工作。代价是:非拉丁字母的文本(如中文)在字节级序列更长,训练 BPE 合并需要更多数据。
Q:为什么不同模型的同段文本 token 数差异很大?
取决于词表大小、训练语料的语言分布和分词器类型。同一个句子在 LLaMA (32K 词表) 和 Qwen (152K 词表) 下的 token 数可能差 30-50%。评估模型的 token 效率时,应该用同一分词器做比较。这也是为什么 benchmark 常比较的是"同等 token 预算下的性能"。
Q:Tokenizer 会导致哪些安全隐患?
① SolidGoldMagikarp 问题:某些从未在训练数据中出现但存在于词表中的 token,可能导致模型输出异常(因为这些 token 的 Embedding 从未被训练更新);② glitch token:某些 token 序列触发不稳定的生成行为;③ 分词不一致:同一实体在不同上下文被切成不同子词,导致模型无法一致地理解。
原始论文¶
| 算法 | 论文 | 链接 |
|---|---|---|
| BPE | Neural Machine Translation of Rare Words with Subword Units (Sennrich et al., ACL 2016) | arxiv.org/abs/1508.07909 |
| WordPiece | Japanese and Korean Voice Search (Schuster & Nakajima, ICASSP 2012) | — |
| SentencePiece | SentencePiece: A simple and language independent subword tokenizer (Kudo & Richardson, EMNLP 2018) | arxiv.org/abs/1808.06226 |
| BBPE | Language Models are Unsupervised Multitask Learners (Radford et al., 2019) — GPT-2 | — |
延伸阅读与视频¶
| 平台 | 标题 | 说明 |
|---|---|---|
| 📺 YouTube | Let's build the GPT Tokenizer | Andrej Karpathy 手把手从零构建 BPE 分词器(英文,~2h) |
| 📖 Hugging Face Course | Byte-Pair Encoding tokenization | 明确讲 BPE 训练与分词过程 |
| 📖 Hugging Face Docs | Tokenizers | Rust tokenizers 工具链,适合理解训练、编码、解码和 special tokens |
| 📖 OpenAI Cookbook | How to count tokens with tiktoken | API 成本估算、上下文长度估算和 tokenizer 差异对比 |
| 📖 SentencePiece GitHub | google/sentencepiece | SentencePiece 官方实现,适合看 Unigram/BPE 的真实训练参数 |
| 📖 Tiktoken GitHub | openai/tiktoken | OpenAI tokenizer 工具,适合工程中做 token 统计与 prompt budget |