Tokenizer 详解
目录
- 本质问题:为什么需要 Tokenizer?
- Tokenizer 的三种"切法"——按粒度分
- 2.1 Word-level(单词级)
- 2.2 Char-level(字符级)
- 2.3 Subword(子词级)⭐ 业界标准
- 三种主流 Subword 算法——按"怎么切"分
- 3.1 BPE(Byte Pair Encoding)—— 最主流、GPT 系列在用
1. 本质问题:为什么需要 Tokenizer?
一句话:模型只认识数字,不认识文字。Token 是文本和数字之间的"翻译单位"。
"我今天学习了反向传播"
↓ Tokenizer
[12875, 392, 561, 18903, 6521, 98] ← token IDs(一串整数)
↓ Embedding 层
[[0.12, -0.34, ...], [...], ...] ← 向量(模型真正能处理的东西)
没有 Tokenizer,模型连一个汉字都读不懂。它是所有 NLP 模型的入口。
核心的 “翻译” 过程
输入文本 → Tokenizer → Token IDs → Embedding → 模型计算
↑
词典(vocabulary / vocab)
Tokenizer 做的事情本质上就是查词典:
- 编码(Encode):文本 → Token IDs
- 解码(Decode):Token IDs → 文本
- 可逆性要求:理想情况下,编码再解码应该完全还原原文
Tokenizer 做决策的关键问题
任何一个 Tokenizer 都要回答三个问题:
- 切多细?——按词切?按字切?还是按中间粒度切?
- 词表多大?——几万个还是几十万个?
- 没见过的新词怎么办?——是抛异常?还是能"拼"出来?
这三个问题的不同回答,就衍生出下面三种切分方法。
2. Tokenizer 的三种"切法"——按粒度分
三种方式对应三个不同层级的切分单位。
───────────────────────────────────────────────────────────
方法 | 粒度 | 例子:"我爱机器学习"
───────────────────────────────────────────────────────────
Word-level | 单词级 | ["我", "爱", "机器", "学习"] ← 按词切
Char-level | 字符级 | ["我", "爱", "机", "器", "学", "习"] ← 按单字切
Subword | 子词级 | ["我", "爱", "机器", "学习"] ← 按"最常用的片段"切
───────────────────────────────────────────────────────────
2.1 Word-level(单词级)
核心思路:按自然语言的"单词"为单位切分。英文以空格和标点为界,中文需要额外分词器。
英文:
"I love machine learning"
→ ["I", "love", "machine", "learning"]
中文(需分词工具,如 jieba):
"我爱机器学习"
→ ["我", "爱", "机器", "学习"]
词表规模:
英文:约 50 万+ 单词(包含所有变形)
run, runs, running, ran — 4 个独立的词
unbelievable, believable, believe — 3 个独立的词
中文:理论上无限(组合词太多)
学习、学会、学好、学完、学成、学以致用...
| 优点 | 缺点 |
|---|---|
| 直觉最自然,每个 token 语义完整 | 词表极大(英文 50 万+) |
| 实现简单 | 遇到未登录词(OOV)直接认不出 |
| 人类可理解性最强 | “run”/“running”/“ran” 被当成三个完全不同的词 |
| 中文需要额外分词工具,且边界划分有歧义 |
OOV(Out-of-Vocabulary)(超出词表外的字词)问题:
词表里只有:word-level 级别的常见词
实际遇到:
"tokenizationalization" ← 不在词表中
→ OOV!模型不认识
→ 只能用 UNK(Unknown)特殊 token 替代
→ 信息丢失了!
结论:Word-level 在开放场景下不可行。
现实:GPT / LLaMA / Qwen 都不用 Word-level。词表太大 + OOV 问题无解。
2.2 Char-level(字符级)
核心思路:以单个字符(字母/汉字)为单位切分。词表极小。
英文:
"unbelievable"
→ ["u", "n", "b", "e", "l", "i", "e", "v", "a", "b", "l", "e"]
"I love ML"
→ ["I", " ", "l", "o", "v", "e", " ", "M", "L"]
中文(天然适合,因为汉字本身就是字符):
"我爱机器学习"
→ ["我", "爱", "机", "器", "学", "习"]
词表规模:
英文:26 字母 + 大小写 + 数字 + 标点 + 空格 ≈ 200 个 token
中文:常用汉字约 6,763 个(GB2312 一级 + 二级)
全部 CJK 统一汉字约 80,000+
中文用字符级的话词表要比英文大得多
| 优点 | 缺点 |
|---|---|
| 词表极小(英文 ~200) | 序列极长(1 个单词变 12 个 token) |
| 没有 OOV 问题(任何字符都能表示) | 注意力计算量是序列长度的平方增长 |
| 任何语言、任何特殊符号都能处理 | 单个字符几乎没有语义信息 |
| 跨语言一致性(统一处理) | 模型难以学到"单词"级别的模式 |
序列长度问题:
同样是 100 个英文单词的段落:
Word-level: ≈ 100 tokens(含标点)
Char-level: ≈ 500-600 tokens(平均每个单词 5-6 个字母)
Transformer 自注意力是 O(n²) 复杂度:
Word-level: 100² = 10,000 次计算
Char-level: 600² = 360,000 次计算 → 36 倍!
这意味着:同样的计算资源下,Char-level 只能处理 Word-level 1/6 的文本长度。
现实:极少直接使用。Transformer 的序列长度是瓶颈,用太多 token 表示一段文本太浪费。
2.3 Subword(子词级)⭐ 业界标准
核心思想:常用词不分(保留语义),生僻词切成已知片段(解决 OOV)。平衡了 Word-level 和 Char-level 的优缺点。
"unbelievable" → ["un", "believe", "able"] ← 3 个已知子词
"believable" → ["believe", "able"] ← 2 个已知子词
"特朗普" → ["特朗", "普"] ← 2 个高频汉字片段(BPE 训练后合并)
处理未登录词(这是 Subword 最强大的能力):
"tokenizationalization"
情况 A:这个词不在词表中(Word-level → OOV ❌)
情况 B:每个字母拆开(Char-level → 13 个 token,太长 ❌)
情况 C:Subword 切分
→ ["token", "ization", "al", "ization"]
→ 4 个 token,每个都在词表里 ✓
→ 意思保留:"token" + "化" + "的" + "化"(大致可理解)
| 优点 | 缺点 |
|---|---|
| 词表可控(典型 8k-128k 大小) | 切分结果有时违反直觉 |
| 无 OOV 问题(生僻词也能"拼"出来) | 相同词在不同上下文中可能被不同切法 |
| 序列长度适中(比 Word-level 略长,远短于 Char-level) | 多语言场景下低频语言编码效率低 |
| 平衡了语义完整性和词表大小 | 训练 Tokenizer 需要大量语料 |
为什么 Subword 能成为业界标准?
三者的量化对比(同一段英文 100 词):
词表大小 序列长度 OOV处理 语义密度
Word-level 50k+ ~100 ❌ ⭐⭐⭐
Char-level ~200 ~500 ✓ ⭐
Subword 32k ~120 ✓ ⭐⭐⭐
↑ ↑ ↑
合理大小 几乎不膨 无死角覆盖
Subword 在三者中找到了最佳平衡点。
Subword 的"反向"理解:
如果你学过"词根词缀"背单词,你就已经理解 Subword 了:
"unlockable" = "un" + "lock" + "able"
→ 不认识这个词?没关系,三个子词你都认识
→ 组合意思:"不能" + "锁" + "能够" → "可以解锁的"
Transformer 的 Subword Tokenizer 做的也是类似的事:
把词汇拆成"语素"级别的片段,组合使用。
3. 三种主流 Subword 算法——按"怎么切"分
上文我们讲了 Subword “是什么”——它是介于单词和字符之间的切分粒度。接下来讲 Subword “具体怎么切”——有三种算法,分别被不同模型家族采用。
Subword 算法三大家族:
BPE(Byte Pair Encoding) ← GPT / LLaMA / Qwen / StableLM
Unigram ← T5 / Gemma / SentencePiece 默认
WordPiece ← BERT / ELECTRA
3.1 BPE(Byte Pair Encoding)—— 最主流
提出:Neural Machine Translation of Rare Words with Subword Units (Sennrich et al., 2016)
使用模型:GPT 全系列(GPT-2/GPT-3/GPT-4)、LLaMA 1/2/3、Qwen、DeepSeek、StableLM 等——绝大多数当前主流模型。
3.1.1 原理:自底向上的合并
一句话:从字母/单字开始,逐步合并出现频率最高的相邻字节对。
训练过程图解:
Step 0:初始状态
词表 = 所有单字符(26 字母 + 标点 + 空格等)
语料 = "low low low low low lower lowest"
Step 1:拆成字符
l o w _ l o w _ l o w _ l o w _ l o w _ l o w e r _ l o w e s t
("_" 表示空格)
Step 2:统计相邻 pair 频率
(l, o): 出现 8 次 ← 假设数字
(o, w): 出现 8 次
(w, _): 出现 5 次
(w, e): 出现 2 次
...
最高频的 pair: (l, o) 或 (o, w)
Step 3:合并最高频 pair
假设 (l, o) 是最高频 → 合并为 "lo"
词表新增 "lo"
语料变成:lo w _ lo w _ lo w _ lo w _ lo w _ lo w e r _ lo w e s t
Step 4:继续统计、合并
现在 (lo, w) 出现 8 次 → 合并为 "low"
词表新增 "low"
语料变成:low _ low _ low _ low _ low _ low e r _ low e s t
Step 5:继续
(low, _) 出现 5 次 → 合并 "low_"
(e, r) → 合并 "er"
(e, s) → 合并 "es"
(es, t) → 合并 "est"
...
Step 6:停止
当词表大小达到预设值(如 32,000)时停止训练
最终词表 = {..., "low", "low_", "er", "est", ...}
核心要点:
训练完成后,BPE 的"贪婪"解码规则:
→ 对输入文本,从左到右,优先匹配词表中"最长"的子词
→ 匹配不上就逐字符回退
例子(假设词表里有 "low" 但没 "lower"):
"lower" → 从左往右匹配
"l" → 不是最长,继续
"lo" → 有 "low"?→ 匹配 "low" ✓
剩下 "er" → 匹配 "er" ✓
→ 结果:["low", "er"]
3.1.2 实际案例:GPT 系列的编码表现
英文编码效率:
GPT-4(≈100k 词表):
"hello world" → [15339, 1917] ← 2 个 token
"Hello World" → [15496, 21592] ← 2 个不同 token!大小写敏感
"the quick brown fox" → [9, 9, 9, ...] ← 高频词各占 1 个 token
"antidisestablishment" → 未知词 → 切成子词 ← 不会 OOV
英文平均效率:约 3.5-4.5 字符/token
中文编码效率(在 GPT 这种"英文优化"的 Tokenizer 下):
GPT-4(中文编码效率较差——这是 GPT 的已知弱点):
"我在学习反向传播"
→ 每个中文字几乎各占 1-2 个 token
→ 约 8-12 个 token
对比 Qwen 2.5(中文优化 Tokenizer):
"我在学习反向传播"
→ "我" + "在" + "学习" + "反向传播"
→ 约 4-5 个 token
结论:GPT 的中文效率约为 1.5-2 chars/token
而 Qwen 可以达到 4-5 chars/token
→ 同样的中文文本,GPT 要多花 2-3 倍的价格!
GPT-4o 的 Tokenizer 更新(2024):
旧版 GPT-3.5 的 p50k_base:
"gpt" → ["g", "pt"] ← 被切开了!
"ChatGPT" → ["Chat", "GPT"] ← 还可以
"api" → ["api"] ← 完整
新版 GPT-4o 的 o200k_base:
"gpt" → ["gpt"] ← 作为一个完整 token ✓
"ChatGPT" → ["Chat", "GPT"] ← "GPT" 作为整体 ✓
"api" → ["api"] ← 完整
词表从 ~100k 扩展到 ~200k
常见 AI 相关术语被保留为完整 token
编码效率整体提升 10-20%
3.1.3 代码场景的 BPE 表现
BPE 在代码 + 自然语言混合场景下有一些特殊现象:
# Python 代码的 tokenization
"def fibonacci(n):" → 可能保持完整,因为 "def" 是高频词
"softmax_attention" → ["softmax", "_", "attention"] 或 ["soft", "max", "_attention"]
"__init__" → ["_", "_", "init", "_", "_"] ← "dunder" 方法常被切成碎片
代码和自然语言混合的挑战:
挑战 1:空格和缩进
Python 用缩进表示代码块
如果 Tokenizer 把连续空格/缩进合并了,代码语义就变了
解决方案:保留空格作为单独 token,或在 Pre-tokenizer 阶段特殊处理
挑战 2:命名约定
snake_case: "tokenizer_config" → 下划线作为分隔符
camelCase: "tokenizerConfig" → 大写字母作为隐式分隔
PascalCase: "TokenizerConfig" → 大写开头作为隐式分隔
BPE 对这些命名约定的处理方式不同,影响代码理解效率
挑战 3:数字
"1234567890" → BPE 可能切成 ["123", "456", "7890"]
→ 模型"看到"的是碎片化的数字,而不是完整数字
→ 影响数学计算能力
→ 解决方案:split_digits=True — 每个数字独立成 token
3.1.4 GPT-4o 的"被切开的单词"特征表
以下是一些实际测试的常见词汇的 token 分解情况(基于 o200k_base):
完整 token(1 个 token):
GPT, AI, chat, api, token, model, data, train, test, python, java
hello, world, transformer, attention
被切开的(2+ 个 token):
tokenization → ["token", "ization"]
finetuning → ["fine", "tuning"]
reinforcement → ["reinforce", "ment"]
bidirectional → ["bi", "direction", "al"]
parameter → ["para", "meter"] 或完整
initialization → ["initial", "ization"]
这些切分方式有些符合语言学直觉(如 "token" + "ization"),
有些则完全出于统计频率(如 "bi" + "direction" + "al")。
3.1.5 BPE 的优缺点总结
| 优点 | 缺点 |
|---|---|
| 原理简单、训练高效 | 贪心匹配,不是全局最优切分 |
| 词表大小可控 | 相同词在不同上下文可能被不同切法 |
| 无 OOV 问题 | 对中文等非空格语言不友好(需预处理) |
| 经验证明效果最好(GPT/LLaMA 都用) | 低频 token 可能出现"浪费" |
| 实现成熟(tiktoken / HuggingFace 都支持) | 训练语料分布偏差会影响合并结果 |
为什么 BPE 是事实标准?
不是因为它理论最优(Unigram 理论上更优雅),而是因为经验最优——OpenAI 在 GPT-2/GPT-3 上大规模验证了它的有效性,后来 LLaMA/Qwen/DeepSeek 跟进,形成了生态效应。工具链(tiktoken、HuggingFace tokenizers)也围绕 BPE 最成熟。
3.1.6 与 Unigram / WordPiece 的前瞻对比(概要)
| 维度 | BPE | Unigram | WordPiece |
|---|---|---|---|
| 合并方向 | 自底向上(合并) | 自顶向下(裁剪) | 自底向上(合并) |
| 合并/保留标准 | 出现频率 | 损失函数减少量 | 互信息(PMI) |
| 是否概率模型 | 否 | 是 | 否 |
| 训练速度 | 快 | 中 | 中 |
| 代表模型 | GPT / LLaMA / Qwen | T5 / Gemma | BERT / ELECTRA |
| 中文支持 | 需配合 Pre-tokenizer | 原生支持更好(SentencePiece) | 需配合预处理 |
附录:关键概念快速索引
| 概念 | 一句话解释 |
|---|---|
| Token | 文本切分后的最小单位(可能是词、字符或子词) |
| Token ID | 每个 Token 对应的整数编号(词典中的索引) |
| 词表 | Token 到 ID 的映射字典(Vocabulary) |
| OOV | 词表中不存在的词(Out-of-Vocabulary) |
| UNK | 代替 OOV 的特殊 token(Unknown) |
| BPE | 自底向上合并高频子词的算法 |
| Pre-tokenizer | 正式训练前的粗切分(如空格 + 标点分割) |
| Encoder | 文本 → Token IDs 的过程 |
| Decoder | Token IDs → 文本的过程 |
| 编码效率 | 每个 token 平均能表示多少字符(chars/token) |
165



被折叠的 条评论
为什么被折叠?



