外观
分词与词表
分词(tokenization)是把连续文本切成离散单元——token——的过程,token 是语言模型真正"看见"和处理的最小单位。 模型并不直接理解文字,它只认识词表(vocabulary)里的整数 id;语言建模里说的"预测下一个词",精确地说是"预测下一个 token"。分词的质量决定模型能学到什么、学得多好,也决定算力和成本,因此常被称作"被低估的最重要组件"。
一句话定位:tokenizer 定义了模型的输入字母表——它决定了"模型能表达什么"(词表覆盖)与"每句话花多少钱"(token 数)。
一、为什么需要分词
理论上模型可以逐字符建模,但纯字符粒度有三个问题:
| 问题 | 说明 |
|---|---|
| 序列太长 | 一个词平均 4~5 个字符,序列长度翻数倍,自注意力 O(n²) 成本爆炸 |
| 语义太碎 | "apple" 的 5 个字符各自独立,模型要自己学会拼接,学习负担重 |
| 无法跨任务复用 | 每个子词级的知识(如 "ing"、"tion" 的词法)无法被字符级模型直接复用 |
分词要在粒度与覆盖之间找平衡:单元越小越能覆盖任意文本(包括生僻词),但序列越长、语义越碎;单元越大语义越完整,但词表越稀疏、越难覆盖新词。现代主流方案是子词(subword)分词:常用词作为整体、生僻词拆成可组合的子词片段,两全其美。
二、分词方案对比
| 方案 | 基本单元 | 词表来源 | 优点 | 缺点 | 典型使用 |
|---|---|---|---|---|---|
| 字符(char) | 单个字符 | 固定 | 词表极小、覆盖一切 | 序列太长、无语义 | 生僻语言兜底 |
| 词(word) | 完整词 | 语料统计 | 语义完整 | 词表巨大、OOV 多、形态变化爆炸 | 传统 NLP |
| BPE | 子词(字节合并) | 语料统计合并 | 覆盖好、可还原、实现简单 | 无语言先验、对罕见形态不稳定 | GPT、Llama 系 |
| WordPiece | 子词(概率合并) | 语料统计合并 | 合并准则更优(似然增益) | 依赖空格预切分 | BERT、T5 系 |
| Unigram | 子词(概率删减) | 从大词表反推 | 词表大小可控、输出可按概率采样 | 实现复杂 | SentencePiece、LLaMA(后来) |
| SentencePiece | 子词 + 整句编码 | 上述算法的统一框架 | 免空格预切分、支持字节级、跨语言 | 概念多、需配置 | LLaMA、Mistral、Qwen 系 |
两类核心算法——BPE(字节对编码)与WordPiece——的合并准则不同:BPE 每次合并出现频率最高的相邻对;WordPiece 每次合并使语言模型似然增益最大的相邻对(类似"最佳单步分词")。Unigram 反其道而行:先建一个超大候选词表,再按 EM 计算的损失贡献逐个删词,删到目标大小。三者都基于"子词 = 权衡词表覆盖与序列长度的解"。值得注意的是,词级分词并非被淘汰——在形态简单的语言(如英语规则词形)上它仍然直观,但对形态丰富语言(德语复合词、土耳其语黏着)、无空格语言(中/日/泰)与 OOV 高发场景,子词方案的系统性优势决定了它才是现代 LLM 的事实标准。
三、BPE 算法:逐步拆解
BPE 由 Sennrich et al. 2016 从数据压缩领域引入神经机器翻译,是 GPT 系列(字节级变体 byte-level BPE)与 Llama 系采用的主流算法。核心思想:反复合并语料中频率最高的相邻 token 对,直到词表达到目标大小。
text
输入:训练语料、目标词表大小 V
第 0 步:把每个词拆成字符序列并计数,例如
"low"×5 "lower"×2 "newest"×6 "widest"×3
第 1 步:统计所有相邻字符对的频次:
("l","o")=7 ("o","w")=7 ("n","e")=6 ("w","e")=8 ...
第 2 步:合并频率最高的对 ("w","e") → 新增词表项 "we"
语料变成:lo-w·er → "lo" "we" "r" 的形式(示意)
第 3 步:重复:统计新的相邻对频次,继续合并最高频对
("lo","w")=7 → "low";("n","e")=6 → "ne";("ne","w")=6 → "new"...
第 4 步:直到词表项数达到 V,训练完成
编码新文本:
"lowest" → 贪心匹配最长词表项:
字符级开始,反复应用已学到的合并规则:
l-o-w-e-s-t → "low" "est"(若 "est" 也在词表中)关键特性:
- 字节级 BPE(byte-level BPE):GPT-2 起把输入降到 UTF-8 字节再做合并,词表只依赖 256 个字节,任何 Unicode 文本都不会出现未登录词(OOV)——这是多语言鲁棒性的根基。
- 确定性贪心:编码时按最长匹配/学习到的规则贪心合并,速度快、可复现。
- 缺陷:规则独立于语言结构,"st" 与 "tion" 这类词法片段能不能形成、以什么顺序形成,完全由频率决定。
一个完整的例子能看清 byte-level BPE 的"成长":假设语料里大量出现 "low" 与 "lower",训练会先合并 "lo"(如果它最高频),再把 "low" 形成,最后可能形成 "lower";而 "newest" 与 "widest" 中高频出现的 "est" 会被单独合并且复用于所有以 -est 结尾的词。训练完成后,"lowest" 会被贪心地切成 "low" + "est" 两个子词——这正是子词方案"常用词整体、生僻词拆块"的实际运作。
python
# byte-level BPE 的合并逻辑(伪代码,示意训练循环)
def train_bpe(corpus: dict[str, int], vocab_size: int):
vocab = set() # 词表
while len(vocab) < vocab_size:
pairs = count_adjacent_pairs(corpus) # 统计相邻 token 对频次
best = max(pairs, key=pairs.get) # 选最高频对
vocab.add(best) # 并入词表
corpus = merge_pair(corpus, best) # 在语料中替换该对
return vocab实践中的实现
生产环境不必自己实现——SentencePiece(Google,开源)统一实现 BPE/Unigram/WordPiece 并免去空格预切分;Hugging Face tokenizers 库提供高性能 Rust 实现。自己写 BPE 的意义在于理解原理,而不是重新造轮子。
四、特殊 token 与词表设计
词表不是纯内容 token,还包含模型协议用的特殊 token(special tokens):
| 特殊 token | 作用 | 常见写法 |
|---|---|---|
<bos> | 序列开始(begin of sequence) | GPT-2 不用;Llama 有 |
<eos> | 序列结束 / 对话轮次边界 | GPT 系为 <endoftext> |
<pad> | 批内对齐填充 | 预训练可不用,微调/推理批次常用 |
<unk> | 未知 token(兜底) | 字节级 BPE 几乎用不上 |
<sep> / <cls> | 句子边界 / 分类聚合位 | BERT 系 |
| `< | im_start | >` 等 |
特殊 token 的语义由训练时的使用方式定义:例如 <eos> 出现的位置、对话角色标记如何组织,都必须在预训练:数据与目标与微调:SFT 与参数高效微调中保持一致,否则推理时模型会困惑——这是"tokenizer 与训练数据协议必须对齐"的经典坑。
一个直观的例子是 ChatML 风格的对话模板:<|im_start|>system\n你是助手<|im_end|>\n<|im_start|>user\n你好<|im_end|>\n<|im_start|>assistant\n。这些 <|im_start|> 在预训练语料中就该以同样的方式出现,模型才能学到"看到这个标记意味着角色切换";若微调时才引入全新的特殊 token,模型对它们的语义几乎为零,需要额外的适配期。
五、词表大小:怎么选
词表大小是分词器的头号超参数,主流模型给出了一组经验值:
| 模型 | 词表大小 | 分词方案 | 备注 |
|---|---|---|---|
| GPT-2 | 50,257 | byte-level BPE | 50k 合并 + 256 字节 + 1 特殊 token |
| LLaMA / Llama 2 | 32,000 | SentencePiece(BPE) | 早期 Llama 系 |
| Llama 3 | 128,256 | tiktoken 风格 BPE | 大幅扩表,多语言提升 |
| GPT-4(cl100k_base) | 约 100,256 | byte-level BPE | 约 10 万级 |
| Qwen2/Qwen2.5 | 约 152,000 | BPE | 中文优先的扩表(dataAsOf 2025,以官方发布为准) |
选择逻辑是三角权衡:
另一个常被忽略的效应是词表大小与"每 token 信息量"的绑定:词表翻倍不一定让文本的 token 数减半——它只减少最常用部分的切分粒度,长尾词汇仍按旧方式拆。因此实际工作中更常见的做法是:按目标语言的字符覆盖需求确定词表规模,再用一个小型代理实验(在同一份开发集上对比不同词表的平均 token 数)做最终确认。
- 词表大 → 序列短(省注意力)、信息密度高,但 embedding/输出层参数暴涨(参数量 ≈ 词表 × 隐藏维度),且需要更多数据把稀疏项训充分;
- 词表小 → 参数省,但序列变长、罕见词被拆得七零八落;
- 语言结构 → 中文、日文等"字密集"语言需要更大的词表或更细的子词切分,否则一个汉字可能被拆成两三个 token,成本和语义都吃亏。
词表大小影响的是"每个 token 的参数",不是"一个 token 的参数"
输出层与 embedding 在部分实现中共享权重(tied embeddings),词表从 32k 扩到 128k,仅输出头就多出约 96k × 隐藏维度 个参数。扩表的同时通常要重新训练 tokenizer 并保持词表"对齐",不能直接拼接。
六、分词对多语言、代码与数字的影响
分词的隐性影响常在工程中被低估:
| 场景 | 问题 | 影响 |
|---|---|---|
| 中文/日文 | 无空格分词,BPE 天然按字拆 | 同义字词分散在多个 token,语义建模更难;词表需专门扩 |
| 代码 | 缩进/符号/标识符形态复杂 | 未在代码上训练的 tokenizer 会把常见代码序列拆得很碎 |
| 数字 | "1234567" 可被拆成任意片段 | 模型对数字的算术与排序能力受 token 切分影响(对位错位) |
| 罕见专名 | 人名/地名/生僻字 | 被拆成碎片后语义丢失,多语言名字尤其明显 |
| 噪声文本 | 社交媒体的拼写变体 | 拼写变体各自独立成 token,稀释统计 |
举个例子:中文 "机器学习" 在词表覆盖良好的 BPE 里可能是 机器/学习(两个 token),在覆盖不佳的 tokenizer 里则可能变成 机/器/学/习(四个 token)甚至含字节碎片;而同样 4 个字在英文为主的模型里往往只占 2~3 个 token 的当量。token 数翻倍的直接后果是:同样 128K 的上下文窗口,能装下的中文内容只有英文的一半左右,成本与有效长度同时缩水。
一个值得记住的经验
tokenizer 的训练语料应与预训练语料同源:如果预训练语料有 40% 中文,tokenizer 的训练语料也应大致如此配比,否则中文会被切成低效的长序列,白白增加成本。多数开源模型仓库会公布 tokenizer 训练语料的构成,值得阅读。
七、token 数与成本、上下文长度的换算
token 是计费与显存的"货币":
- 换算经验:英文约 1 个 token ≈ 0.75 个词(1 个词 ≈ 1.3 个 token);中文约 1 个汉字 ≈ 1~2 个 token(视词表);代码符号密集场景 token 数更高。
- 成本公式:调用一次的总 token = 输入 token + 输出 token,主流 API 按此计费(输出往往单价更高)。
- 上下文占用:模型能处理的上下文长度以 token 计,见上下文与长文本;KV Cache 显存也随输入 token 数线性增长(见推理基础:自回归与采样)。
- 速度影响:单 token 延迟基本恒定,序列越长,生成前需要处理的前缀越长,首 token 延迟越高。
text
成本估算示例(假设输入 1 美元/百万 token、输出 3 美元/百万 token):
一段 2000 字中文文档 ≈ 2000~4000 token
回答约 500 token
单次调用 ≈ (3000×1 + 500×3) / 10^6 ≈ 0.0045 美元
→ 批量对话前先估 token,别让"隐形按量付费"惊到账单用代码亲手看一下(以 OpenAI 的 tiktoken 为例):
python
import tiktoken
enc = tiktoken.get_encoding("cl100k_base") # GPT-4 系使用的编码
ids = enc.encode("机器学习与大模型")
print(len(ids), ids) # 观察同一段中文被切成了几个 token多试几种语言、代码片段和数字串,会对"token 数随语言与格式剧烈变化"建立直观感受。
| 输入 | 大致 token 数(cl100k_base 量级) | 说明 |
|---|---|---|
| "Hello, world!" | 约 4 | 英文短句接近按词切分 |
| "你好,世界!" | 约 6~8 | 汉字通常 1~2 token/字 |
| "Machine learning is great" | 约 6 | 英文词级为主 |
| 一段 Python 缩进代码 | 高于等长英文 | 缩进/符号/标识符被拆碎 |
(以上为量级示例,实际以具体 tokenizer 为准)
## 八、权衡与边界
- **分词是一个"不可见却昂贵"的组件**:改 tokenizer = 重新训练整个模型。词表、方案、特殊 token 都要在[预训练:数据与目标](/concepts/pretraining)启动前一次定死。
- **跨模型不可移植**:不同模型的 tokenizer 完全不同,同一段文本的 token 数与计费不可通用。
- **分词方案仍在演进**:字节级 BPE、多语言扩表、对代码/数学的专项优化(如给数字加"数字 token")都是活跃方向。更激进的思路(如 MegaByte、字节级语言模型)主张**绕过分词**直接建模字节序列,把"分词器与模型必须一起重训"的强耦合拆掉——目前因序列过长尚不经济,但它提醒我们:分词不是天经地义的,只是当前算力约束下的最优妥协。
- **调试入口**:遇到"模型对某语言/某领域表现异常",第一步永远是看 tokenizer 把输入切成了什么——这常常直接暴露问题根源。
::: tip 实操建议
调试任何模型行为异常,先问三个问题:这段文本被切成了什么 token?每个 token 的 id 是什么?有没有意外地拆碎关键片段?分词工具(Hugging Face tokenizers、tiktoken)几分钟就能给出答案。
:::
### tokenizer 设计决策清单
| 决策 | 主要考量 | 常见默认 |
|---|---|---|
| 算法 | 覆盖 vs 可还原 vs 实现成本 | byte-level BPE / SentencePiece-Unigram |
| 词表大小 | 参数量、序列长度、语言覆盖 | 32k~128k(多语言偏大) |
| 是否字节级 | 全 Unicode 覆盖 vs 语义粒度 | 是(无 OOV) |
| 特殊 token 设计 | 与训练/对话协议对齐 | ChatML 或按需定义 |
| 训练语料配比 | 与预训练语料同源同比例 | 与主语料一致 |
| 评测口径 | PPL 与成本都依赖 token 数 | 统一分词器再比较 |
### 分词质量怎么评估
除了"平均 token 数"这类效率指标,成熟团队还会检查四件事:**往返一致性**(encode→decode 能否无损还原原文)、**多语言覆盖**(目标语言的平均切分长度与未登录率)、**特殊 token 完整性**(是否与模板/协议一一对应)、**长尾稳定性**(同一词在不同上下文中的切分是否一致)。这些检查可以脚本化,放进 tokenizer 变更的 CI 流程,防止"改了词表、模型悄悄变差"。
## 延伸阅读
- [语言建模:下一词预测范式](/concepts/language-modeling)——token 是"下一个词"的真正单位
- [预训练:数据与目标](/concepts/pretraining)——分词器随语料一起决定预训练质量
- [上下文与长文本](/concepts/context-window)——上下文长度与成本都以 token 计
- [推理基础:自回归与采样](/concepts/inference-fundamentals)——token 流如何被逐步生成
- [什么是大语言模型](/guide/what-is-llm)——模型的整体输入输出链路
## 参考资料
- [Sennrich, Haddow, Birch. *Neural Machine Translation of Rare Words with Subword Units*(2016)](https://arxiv.org/abs/1508.07909) —— BPE 分词原始论文
- [Wu et al. *Google's Neural Machine Translation System*(2016,WordPiece)](https://arxiv.org/abs/1609.08144) —— WordPiece 的来源
- [Kudo. *Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates*(Unigram, 2018)](https://arxiv.org/abs/1804.10959) —— Unigram 分词算法
- [Kudo & Richardson. *SentencePiece: A simple and language independent subword tokenizer*(2018)](https://arxiv.org/abs/1808.06226) —— SentencePiece 框架论文
- [Radford et al. *Language Models are Unsupervised Multitask Learners*(GPT-2, 2019)](https://openai.com/research/language-unsupervised) —— byte-level BPE 的引入
- [Hugging Face Tokenizers 文档](https://huggingface.co/docs/tokenizers/index) —— 高性能分词实现与教程
- [SentencePiece GitHub](https://github.com/google/sentencepiece) —— 开源实现