Tokenizer 是把连续文本压缩成离散整数序列的编码器,它的词表和切分规则一旦冻结就贯穿模型的整个生命周期——本篇梳理分词的必要性、粒度权衡、完整编解码管线与子词算法谱系,作为整个 Tokenizer 体系的入口。
为什么必须离散化
Transformer 的输入端是一张 embedding 查表,输出端是一个在固定词表上的 softmax。两端都要求一个有限、稠密编号的符号集合,而文本是变长的 Unicode 码点串。Tokenizer 就是这两者之间的编解码层。
理论上直接拿 Unicode 码点(约 15 万)或原始字节(256)当词表就够用,但代价立刻转嫁到序列长度上。真正的约束是一组反向耦合的成本:
- 注意力随序列长度呈 增长,KV cache 随 线性膨胀
- embedding 层与输出投影随词表大小呈 增长,、 时单层就是 6 亿参数
- 输出 softmax 的 logits 计算与采样开销同样正比于
于是分词退化为一个清晰的工程问题:在给定的词表预算 下,把语料编码得尽可能短。这正是数据压缩的目标函数,也解释了为什么主流算法(BPE、Unigram)本质都是压缩算法而非语言学工具。压缩率(每 token 承载的字符数)因此成为评价分词器的第一指标。
三种粒度的权衡
| 粒度 | 词表规模 | OOV | 序列长度 | 形态泛化 | 代表 |
|---|---|---|---|---|---|
| character | – | 几乎无 | 最长 | 强,但需模型自行拼词 | ByT5、早期 char-CNN |
| word | – | 严重,长尾截断 | 最短 | 无,run/running 互不相关 | word2vec 时代 |
| subword | – | 可控制到零 | 折中 | 强,共享词根与词缀 | 全部现代 LLM |
word 级的致命缺陷不只是 OOV,而是长尾塌缩:自然语言词频服从 Zipf 分布,任何有限词表都会把大量低频词映射到 <unk>,而低频词往往正是句子的信息核心。character 级避开了这个问题,却把组词的负担全压给模型,同等算力下有效上下文大幅缩水。
subword 取中间态:高频词保留为完整 token,低频词退化为若干片段。只要词表包含全部单字节,OOV 率在数学上为零——这是 BBPE 字节级分词 相对纯字符级 BPE 的关键改进,也是今天 GPT、LLaMA、Qwen 系列的共同底座。
完整管线
Tokenizer 不是单个函数,而是四级流水线。以 Hugging Face 的抽象为准,训练与推理共用同一套组件配置:
原文
↓ Normalizer NFKC/NFD、大小写、空白折叠、控制符剔除
↓ Pre-tokenizer 粗切分边界:空格/标点/正则、UTF-8 转字节
↓ Model BPE / WordPiece / Unigram,输出 token 串
↓ Post-processor 拼接特殊 token、生成 type_ids 与 attention_mask
token ids解码是它的镜像:id 查表还原 token 字符串 → 按 pre-tokenizer 约定拼接(如 SentencePiece 的 ▁ 还原为空格,BBPE 的字节替代字符还原为原字节)→ 输出文本。
关键事实:解码不保证是编码的严格逆运算。Normalizer 中的小写化、NFKC 归一化都是有损的,Café 编码解码后可能变成 cafe。只有字节级、无有损归一化的配置(如 GPT-2 以来的 BBPE)才满足 decode(encode(x)) == x。
from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained("Qwen/Qwen3-8B")
text = "分词器把文本切成 token。"
# fast 版本才返回 offset_mapping,用它把 token 对回原文字符区间
enc = tok(text, add_special_tokens=False, return_offsets_mapping=True)
for tid, (start, end) in zip(enc["input_ids"], enc["offset_mapping"]):
piece = tok.convert_ids_to_tokens(tid)
# repr 会暴露 BBPE 的字节替代字符(如 'ä¸'),这不是乱码而是字节的可打印映射
print(f"{tid:>7} {piece!r:<16} ← {text[start:end]!r}")
# 字节级管线保证无损回环
assert tok.decode(enc["input_ids"]) == textoffset_mapping 是把 token 空间对齐回字符空间的唯一可靠手段,NER、抽取式 QA、流式输出的高亮都依赖它。流式生成时还有一层坑:单个 UTF-8 字符可能横跨多个 token,必须缓冲到字节序列合法才能吐字。
子词算法谱系
现代子词算法按训练目标分两支,这是理解全部变体的主轴:
频率派——自底向上合并。BPE 算法原理 从字符集出发,迭代统计相邻 pair 频次,把最高频的一对合并进词表,产出一张有序的 merge 表;编码时按同样顺序贪心应用。规则简单、训练快、结果确定,但贪心切分并非全局最优。BBPE 字节级分词把 BPE 的基本单元从字符换成字节,用 256 个字节符号兜住任何 Unicode 输入,代价是生僻文字被拆得更碎。
似然派——以概率为准则。WordPiece 与 Unigram LM 覆盖另一支:WordPiece 仍然自底向上合并,但打分从原始频次换成语言模型似然增益;Unigram LM 反向操作——先构造超大候选集,再用 EM 迭代剪掉对似然贡献最小的片段,编码时用 Viterbi 求全局最优切分,天然支持 subword regularization 采样。
必须分清算法与实现这两件常被混淆的事。SentencePiece 不是算法,而是一个可以直接吞原始文本、把空格显式编码为 ▁ 的语言无关框架,内部同时提供 bpe 与 unigram 两种模式,因此摆脱了对空格分隔语言的假设。Tiktoken 是 OpenAI 的 BBPE 高性能编码器,只负责编解码不负责训练。Hugging Face Tokenizers 实操 则是 Rust 实现的通用四件套流水线,训练与部署都在这里落地。完整索引见 Tokenizer MOC,横向的演进史与实测数据见工业级实测对比综述。
词表冻结之后的连带效应
Tokenizer 在预训练第一步就被固定,之后几乎无法更改,它的每个决策都会向下游传导:
- 对话与控制——
<|im_start|>、<|endoftext|>这类保留 token 既划分角色边界又充当生成终止信号,其注入规则由特殊 Token 与 Chat Template 决定,模板错一个字符就会让模型跑偏 - 语言公平性——词表里中文条目的多少直接决定 token 膨胀倍率,同一段内容中英文 token 数可差 2–3 倍,即等量信息付出数倍的推理成本
- 结构化文本——数字切分方式影响算术能力,缩进与 FIM 标记影响代码补全质量
- 推理鲁棒性——训练中从未出现的孤儿 token 会诱发 glitch 行为,prompt 结尾停在 token 中间会破坏切分,补救手段是 token healing
- 参数预算—— 的取值同时牵动 embedding 显存、输出层算力与压缩率,做领域模型时常需裁剪或扩充
多模态把同一套问题搬到了图像与音频上。视觉 Tokenizer 面临离散化(VQ 码本)与连续注入(投影后的 patch embedding)两条分岔路,而多模态统一词表试图把视觉 token 与文本 token 放进同一个 softmax 空间,以换取原生的跨模态生成能力。