字符级词表太大、字节级压缩比最差、词级有 UNK 问题——三种极端各有死穴。BPE 从字节出发,迭代合并最高频 pair,正好落在没有短板的那个点。
Tokenizer 是文本和模型之间的桥梁:把字符串 encode 成 token 序列,再能 decode 回来(必须可逆)。它做的好不好,看两个维度——词汇表多大、压缩比多高。
词汇表大小:模型要为每个 token 学一个 embedding。太大 → embedding 矩阵稀疏、罕见 token 学不好;太小 → 每种方案有自己的麻烦。
压缩比 = UTF-8 字节数 ÷ token 数。越高 → 序列越短 → 越好,因为 attention 的计算量是序列长度的平方,序列减半就是 4 倍省。
把四种方案画到「词汇表大小 × 压缩比」的二维平面上,谁有死穴一目了然。
用 "café matcha 🐱" 这句话(含多字节字符)看四种方案的切分差异——token 数和痛点都不同。
| 方案 | 词汇表大小 | 压缩比 | UNK 问题 | 死穴 |
|---|---|---|---|---|
| 字符级 | ~150K(太大) | 低 | 无 | 词表大 + 压缩比低,两头差 |
| 字节级 | 256(太小) | 1.0(最差) | 无 | 序列太长,attention 爆炸 |
| 词级 | 无上限 | 高 | 有 | 罕见词、形态变化、新词全 UNK |
| BPE | 可配置 | 高 | 无 | 无明显缺陷 |
BPE 的核心:从字节出发,每轮统计相邻 token 对的频率,合并出现最多的那一对,重复 num_merges 次。常见序列逐渐被压成单个 token,罕见序列保持多 token。
# train_bpe 伪代码(CS336 Assignment 1)
indices = utf8_bytes(文本) # 初始化:每个字节一个 token
vocab = {0..255 → 对应字节}
merges = {}
重复 num_merges 次:
pairs = 统计 indices 中所有相邻对的频率
(A, B) = 频率最高的 pair
merges[(A,B)] = 256 + i # 记录合并规则
把 indices 里所有 (A,B) 替换为新 index
BPE 之所以没有明显短板,是因为它站在两个失败模式中间——词汇表大小由 num_merges 这一个超参控制,数据驱动地找到平衡点。
真实的 BPE(GPT-2 起)多了一步 pre-tokenization:先用正则把文本切成块(词、数字、空格绑在词上),再在每个块独立做 BPE。这避免跨词边界的奇怪合并,并加入 <|endoftext|> 等特殊 token 标记文档边界。
BPE = 词级的高效率 × 字节级的通用性。它从字节出发,把高频序列压成单 token、低频序列留给多 token,于是永远不会 UNK,同时把序列压到很短。这正是它成为 GPT-2 以来语言模型标准选择的原因。
当前 tokenization 是个独立步骤,未来也许能端到端从字节学习(ByT5、MEGABYTE、BLT、H-Net、T-FREE 等)。但任何替代方案都必须满足两个条件:模型应操作在序列的抽象块上,且块应是可变长的——把更多容量分配给更有信息量的块。目前这些方案尚未被扩展到前沿规模。
来源:CS336 Lecture 1: Language Modeling From Scratch — Percy Liang & Tatsunori Hashimoto, Stanford, 2026(Tokenization 单元,受 Karpathy 的 tokenization 视频启发)
参考文献:Sennrich et al. 2015《Neural Machine Translation of Rare Words with Subword Units》(BPE 原始来源)