分词与 Tokenizer 原理
大模型能聊天、写代码、做翻译,但有一个它从不直接面对的东西:原始文本里的「字」和「词」。在文本进入任何神经网络之前,必须先经过一道称作分词(tokenization)的工序,被切成一个个模型能处理的单元 token。本篇讲清为什么这一步不可避免、三级表示的权衡、主流子词算法、token 与计费的关系,以及中文的特殊处理,最后用可运行代码带你亲手做一遍 encode 与 decode。
为什么神经网络模型需要先分词而不是直接处理字符或词
神经网络的计算对象是连续向量与固定维度的张量,它无法直接「读懂」一段字符串。要让模型理解语言,必须先把离散的符号(字、词、子词)映射成连续向量(即词嵌入),而 token 正是这套映射的最小基本单元。
如果跳过分词、直接把原始字符串塞给模型,会遇到两类根本障碍:
- 不可微分与不可并行:字符串本身无法参与矩阵运算,必须转为数值索引再查表成向量。
- 表示空间不可控:模型的输入层维度是固定的,而自然语言的词汇量在理论上是无限的(新词、专有名词、拼写变体层出不穷)。
相比直接处理字符或词,分词要解决的其实是同一个核心矛盾:词表既要足够小(便于模型学习与计算),又要足够覆盖(尽量不出现未登录词 OOV)。
- 若以「词」为单位:词表动辄几十万,且每天都有新词诞生,稀疏、昂贵、OOV 严重。
- 若以「字符」为单位:序列变得极长,远距离依赖难以学习,训练与推理成本陡增,且单个字符语义信息稀薄。
- 折中方案是「子词」(subword):把常见词保留为整词,罕见词拆成熟悉的片段,兼顾覆盖率与序列长度。
换句话说,分词是连接离散语言符号与连续向量空间的桥梁,是后续一切语义计算的起点。
词级、字符级、子词级三级表示的权衡
| 维度 | 词级(word-level) | 字符级(char-level) | 子词级(subword) |
|---|---|---|---|
| 词表规模 | 大(十万级) | 极小(几十到几百) | 中等(数千到数万) |
| OOV 风险 | 高 | 几乎为零 | 低 |
| 序列长度 | 短 | 很长 | 中等 |
| 语义密度 | 高 | 低 | 中高 |
| 典型问题 | 稀疏、冷启动难 | 长程依赖难学、计算贵 | 需算法决定切分边界 |
直观结论:词级表达力强但覆盖差;字符级覆盖好但学习效率低;子词级在两者之间取得平衡,因此成为现代大模型(BERT、GPT、LLaMA 等)的事实标准。
主流子词算法
BPE(Byte-Pair Encoding)
BPE 由 Sennrich 等人于 2015 年提出(论文《Neural Machine Translation of Rare Words with Subword Units》,arXiv:1508.07909),最初用于机器翻译中处理罕见词。其思想源自数据压缩领域的字节对编码:
- 以字符为初始词表,把每个词拆成字符序列;
- 统计语料中相邻符号对(pair)的出现频率;
- 反复合并频率最高的相邻对,生成新的合并符号加入词表;
- 当词表达到预设规模或迭代次数耗尽时停止。
训练完成后,编码一个新词时按学到的合并规则从左到右贪心合并。罕见词会被拆成更小的、高频出现的子片段甚至单字符,从而把 OOV 降到极低。
WordPiece
WordPiece 是 BERT 采用的子词算法,思路与 BPE 相近,但合并判据不同:BPE 按相邻对的「出现频次」合并,WordPiece 按「合并后语言模型似然的增益」合并,即优先合并能最大程度提升整体语料概率的那一对。这让它学到的子词更偏向语言上有意义的片段。BERT、Electra 等模型均使用 WordPiece。
Unigram 与 SentencePiece
Unigram 语言模型方法由 Kudo 等人于 2018 年提出(论文《Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates》,arXiv:1804.10959),并配套推出 SentencePiece 工具。它的建模思路与前两者相反:
- BPE/WordPiece 是从字符「自底向上」合并;
- Unigram 是先假设一个较大的候选词表,再用期望最大化(EM)训练每个片段的概率,逐步「剪枝」掉贡献小的片段,最终得到精简词表。
Unigram 的一大优势是:同一个句子可以对应多种合理的子词切分,因此训练时能对切分做随机采样(即论文提出的 subword regularization),把切分歧义当作噪声增强模型鲁棒性。SentencePiece 进一步把空格也编码为一个特殊字符(如 ▁),从而无需依赖语言相关的预分词器,原生支持中文、日文等无空格语种,是目前跨语言场景的主流选择。
token 与 token 计费的关系
主流大模型 API 普遍按 token 数量计费,而非按字数或字符数。理解 token 的粒度,就能理解为什么「长上下文」和「中文」更费钱:
- 一次对话的 token 数 = 输入 prompt 的 token 数 + 模型生成内容的 token 数。上下文越长,每次请求的输入 token 越多,成本线性上升。
- 不同语言的 token 效率差异巨大。以 GPT-2 的 BPE 词表为例,英文常见词往往一个词只占不到一个 token(约 0.75 个 token/词),而中文因为没有空格、且被切成子词后每个汉字常被拆成 1 到 2 个 token。换句话说,表达相同意思的中文文本,token 数可能明显多于英文。
- 子词切分越细(越接近字符级),同一段文本的 token 数越多,成本越高,同时模型需要处理的序列也越长,推理更慢、占用显存更多。
因此对中文用户而言,同等语义的信息通常消耗更多 token,这也是为什么中文场景下的长上下文对话与文档处理成本更高。
中文分词的特殊处理
中文与英文最大的区别在于没有明显的词边界(无空格),这给分词带来特殊性:
- 语言学分词:借助词典与统计模型(如 jieba、HanLP)按语言知识切词,输出「词」序列。这种做法对人类友好,但切分边界与模型词表不一定对齐,且难以覆盖新词。
- 字级(char-level):直接把每个汉字当成一个 token。最不容易 OOV,但序列最长、语义最稀薄,模型需自行从字组合出词意。
- 子词自动切分:BPE / Unigram / SentencePiece 不依赖中文词边界知识,而是从语料里自动学到高频片段。对中文而言,学到的子词常常是「单字」或「词根/词缀片段」,在覆盖率与序列长度间取得平衡,因此现代中文大模型几乎都走这条路线。
实践中的经验是:与其先用心跳式的中文分词器预处理,不如让模型自带的 tokenizer(通常是 SentencePiece 或 BPE)直接处理原始中文,由它在子词层面统一切分,避免「分词器与模型词表不一致」导致的表示错位。
实战:用 HuggingFace 演示 encode 与 decode
下面两段代码均可直接运行。第一段用 GPT-2 的 tokenizer 直观展示中英文 token 数量差异;第二段用 HuggingFace tokenizers 库从零训练一个 BPE 分词器。
# 示例一:用 GPT-2 tokenizer 观察 encode / decode 与中英文 token 数差异
from transformers import GPT2Tokenizer
tok = GPT2Tokenizer.from_pretrained("gpt2")
text = "分词让神经网络能处理文本"
ids = tok.encode(text)
print("token ids :", ids)
print("tokens :", tok.convert_ids_to_tokens(ids))
print("解码还原 :", tok.decode(ids))
# 对比同等语义的中英文 token 数量
en = tok.encode("Tokenization lets neural networks process text")
zh = tok.encode("分词让神经网络能处理文本")
print("英文 token 数:", len(en))
print("中文 token 数:", len(zh))
# 示例二:用 HuggingFace tokenizers 从语料训练一个 BPE 分词器
from tokenizers import Tokenizer
from tokenizers.models import BPE
from tokenizers.trainers import BpeTrainer
from tokenizers.pre_tokenizers import Whitespace
tokenizer = Tokenizer(BPE())
tokenizer.pre_tokenizer = Whitespace()
trainer = BpeTrainer(special_tokens=["<unk>", "<pad>"], vocab_size=8000)
# 准备一份语料文件 corpus.txt(每行一段文本),再执行训练
# tokenizer.train(files=["corpus.txt"], trainer=trainer)
sample = "神经网络需要先分词才能理解语言"
output = tokenizer.encode(sample)
print("ids :", output.ids)
print("tokens:", output.tokens)
运行示例一需要先安装 transformers(pip install transformers);示例二需要 tokenizers(pip install tokenizers)。两段代码都不依赖网络下载大模型权重之外的额外资源,适合作为理解 tokenizer 的最小可运行实验。
小结
- 分词是连接离散文本与连续向量的必经工序,token 是词嵌入映射的最小单元。
- 词级覆盖差、字符级序列长,子词级在二者间取得最佳平衡,是现代大模型标配。
- BPE(Sennrich 等 2015)自底向上合并高频相邻对;WordPiece(BERT)按似然增益合并;Unigram / SentencePiece(Kudo 等 2018)从大词表剪枝,并支持多候选与跨语言无空格处理。
- 计费按 token 计;中文因子词切分更细、无空格边界,同等语义通常比英文消耗更多 token,长上下文进一步推高成本。
- 中文推荐直接用模型自带 tokenizer 做子词切分,避免额外中文分词器与词表错位。
参考与延伸阅读
- Sennrich, R., Haddow, B., Birch, A. Neural Machine Translation of Rare Words with Subword Units. arXiv:1508.07909, 2015(2016 年 ACL). https://arxiv.org/abs/1508.07909
- Kudo, T. Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates. arXiv:1804.10959, 2018(ACL 2018 长文). https://arxiv.org/abs/1804.10959
- HuggingFace 官方 tokenizers 文档(含训练与调用示例). https://huggingface.co/docs/tokenizers/
- HuggingFace Transformers 文档(GPT2Tokenizer 等 API 说明). https://huggingface.co/docs/transformers/