Skip to content

分词与词表

本页速览 分词把连续文本切成模型处理的最小单元 token,是语言建模真正的"输入语言"。本文系统对比字符/词/BPE/WordPiece/Unigram/SentencePiece 方案,手把手讲清 BPE 算法,并说明词表大小、特殊 token、多语言/代码/数字影响及 token 与成本换算。

分词与词表

分词(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-250,257byte-level BPE50k 合并 + 256 字节 + 1 特殊 token
LLaMA / Llama 232,000SentencePiece(BPE)早期 Llama 系
Llama 3128,256tiktoken 风格 BPE大幅扩表,多语言提升
GPT-4(cl100k_base)约 100,256byte-level BPE约 10 万级
Qwen2/Qwen2.5约 152,000BPE中文优先的扩表(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) —— 开源实现