观文听傑

返回

上一篇把 token 序列错开一位,构造了因果语言模型的 next-token loss。但“机器学习”究竟是 1 个 token、4 个汉字 token,还是 12 个 UTF-8 字节 token,并不是损失函数决定的。整词词表会把罕见词和新名字压成 <unk>;纯字符或纯字节虽然总能覆盖输入,又会把序列拉长。

字节对编码(Byte-Pair Encoding,BPE)在两者之间学习合并规则:高频相邻单元逐步合成较长 token,低频字符串仍由短单元拼出。本文只追踪三个问题:合并规则怎样学出、编码时为何必须按规则顺序应用,以及字节级初始字母表怎样用长度换开放词表覆盖。

01 整词与字符为什么都不理想?#

整词:  [relearner] ─► <unk>        细节丢失
字符:  r e l e a r n e r          永不未知,序列很长
BPE:   re learn er                 复用常见片段
text

设词表大小 VV、模型宽度 DD、序列长度 LL。Embedding 至少有 VDVD 个参数;全注意力通常随 L2L^2 增长;输出 softmax 又要产生 [N,L,V]。分词器是在参数、计算、覆盖率和复用之间取舍,不存在孤立的“最佳词表大小”。

02 tokenizer 是流水线,不只是 BPE#

原始文本
  │ normalizer:Unicode、大小写策略

规范化文本
  │ pre-tokenizer:空格、标点或字节边界

预分片 + 原文 offsets
  │ BPE model:按有序 merges 切成子词

token strings ─► vocabulary ─► token ids [L]
  │ post-processor:BOS/EOS

input_ids / attention_mask / offsets
text

normalizer、pre-tokenizer、merge 顺序、特殊 token 及其 id、decoder 都是模型输入契约。

03 BPE 训练到底在做什么?#

把词拆成初始单元,并用 </w> 标记词尾:

"low"   × 2  ->  l o w </w>
"lower" × 1  ->  l o w e r </w>
text
相邻对频次
(l, o)3
(o, w)3
(w, </w>)2
(w, e)1
(e, r)1

(l,o) 后重写语料为 lo w ...,再统计;下一轮 (lo,w) 得到 low。经典 BPE 每轮贪心选择:

(a,b)=argmax(a,b)uCc(u)nab(u)(a^*,b^*)=\arg\max_{(a,b)}\sum_{u\in\mathcal C}c(u)n_{ab}(u)

C\mathcal C 是预分片集合,c(u)c(u) 是频次,nab(u)n_{ab}(u) 是相邻对出现数。它不直接最小化语言模型 loss,也不保证全局最优。

04 编码为何必须遵守 merge rank?#

假设规则依次是 l+o→lolo+w→lowe+r→er

[l,o,w,e,r,</w>]
 └ rank 0 ─► [lo,w,e,r,</w>]
                └ rank 1 ─► [low,e,r,</w>]
                                  └ rank 2 ─► [low,er,</w>]
text

编码依据训练所得 rank,不会重新统计测试文本,也不是任意最长匹配。重叠规则若应用次序不同,结果就可能不同。

05 教学版训练器与输入输出#

输入是“片段到频次”,输出是有序 merges 与最终切分。固定同频 tie-break 才能复现。真实实现用优先队列增量更新计数,但应先用这个小版本核对语义。

06 编码器也要可检查#

def encode_symbols(symbols, merges):
    ranks = {pair: rank for rank, pair in enumerate(merges)}
    pieces = list(symbols)
    while len(pieces) > 1:
        candidates = [(ranks[(a,b)], i, a+b)
                      for i, (a,b) in enumerate(zip(pieces, pieces[1:]))
                      if (a,b) in ranks]
        if not candidates: break
        _, i, merged = min(candidates)
        pieces[i:i+2] = [merged]
    return pieces
python

测试训练中词、新词、空串、标点与非拉丁字符。若 normalizer 会小写或规范化,decode(encode(text)) 只能对规范化文本保证可逆。

07 字符 BPE 为什么仍可能 <unk>#

训练没见过 Ω,字符级初始字母表便没有可分解起点。字节级 BPE(Byte-Level BPE)从 256 个字节开始,任何 UTF-8 文本都可表示:

"学" ─UTF-8─► [0xE5,0xAD,0xA6]
              │ 高频时再逐步 merge

          1 个合并 token,或 3 个基础 byte token
text

它消除普通文本未知字符,却会让罕见脚本或乱码产生更长序列。英文字母常占 1 字节,常用汉字通常占 3 字节,“字节级”不等于“字符级”。

08 用 Tokenizers 0.23.2 落地#

官方当前稳定版文档为 0.23.2。BpeTrainer 的核心参数包括 vocab_sizemin_frequencyspecial_tokensinitial_alphabet

特殊 token 列表顺序影响 id。tokenizer.json 必须与模型 checkpoint 一同版本化;只记录 VV 无法复现输入。

09 normalization、offsets 与长度预算#

Unicode 规范化会减少表面变体,也可能合并本应区分的标识符、破坏代码,或令字符标注错位。高亮和序列标注应使用 offsets 回指原文,不要用 token 字符串猜位置。

指标太小词表太大词表
tokens / byte序列长通常更短
稀有片段共享短单元许多词表行几乎不更新
Embedding 参数 VDVD
logits [N,L,V]VV 小、LLLL 小、每位置分类贵
低资源语言可能被切得很碎取决于语料和词表配额

应按语言统计 tokens/byte 的 50、95、99 分位数,并在固定计算预算下比较验证 loss、吞吐与显存。不同 tokenizer 的每-token perplexity 分母不同,不能直接比较。

10 上线前验证与常见错误#

  1. 冻结覆盖空白、emoji、URL、数字、代码和多语言的 golden strings;
  2. 比较保存前后 tokens、ids、offsets 与 decode;
  3. 核对特殊 id 与模型配置;
  4. 扫描 <unk> 率、长度尾部和被截断的文本侧;
  5. 比较 tokenizer.json 哈希,禁止服务端悄悄替换。
症状常见原因最短检查
线上 id 改变merge/normalizer 版本错配比较 tokenizer 哈希
新文字大量 <unk>初始字母表不覆盖打印码点或启用字节级
中文/代码异常长语料偏科、预分片不当分语言统计 tokens/byte
decode 空格错误ByteLevel decoder 不匹配add_prefix_space
标注位置漂移用 token 文本猜原位检查 offsets 及其单位
loss 忽然“变好”更换 tokenizer 后按 token 比改报 byte-normalized NLL

11 失败场景与相近方法#

BPE 偏向高频表面片段,不保证 token 对应语素;语料失衡会让低资源语言承担更长上下文成本。重复日志和分隔符还会污染词表。

WordPiece 常用似然增益式打分;Unigram 从较大候选集出发删除片段并可保留多种切分;字符 tokenizer 词表小但序列长;词级 tokenizer 序列短却难处理开放词表。它们只产生离散 id,并未赋予连续语义。

12 今天真正需要记住什么?#

  1. BPE 反复合并高频相邻对,得到有顺序的 merge 规则。
  2. 编码按 rank 应用规则;vocab、merges 与规范化流水线缺一不可。
  3. 字节级字母表覆盖任意 UTF-8 输入,但罕见文本可能变长。
  4. tokenizer 改变 LLVV、成本和 loss 分母,是模型版本的一部分。

13 思考题与小练习#

  1. aaab×2aaac×1 手算前两轮 BPE;同频 tie-break 改变会怎样?
  2. 找一个 emoji,比较 Python 字符长度与 UTF-8 字节数。
  3. 训练 V{2k,8k,32k}V\in\{2k,8k,32k\} 三个 tokenizer,分语言比较 tokens/byte 的 50、95、99 分位数。

相关工作#

  1. Gage, A New Algorithm for Data Compression,早期字节对压缩算法。
  2. Sennrich et al., Neural Machine Translation of Rare Words with Subword Units,将 BPE 用于开放词表翻译。
  3. Wu et al., Google’s Neural Machine Translation System,大规模 WordPiece 系统。
  4. Kudo, Subword Regularization,用多种切分增强鲁棒性。
  5. Wang et al., Neural Machine Translation with Byte-Level Subwords,字节级子词研究。

14 下一篇预告#

BPE 已把文本变成 [N,L] 整数 id,但 Transformer 不能直接在“编号 42”上计算语义。下一篇将拆解 token Embedding 的查表与梯度累加,并解释为什么输入 Embedding 和输出 LM Head 常绑定为同一张 [V,D] 权重矩阵。

词表装不下所有单词怎么办?BPE 的高频合并、字节回退与长度代价
https://zwjcode.cn/blog/bpe-subword-tokenization-merge-rules-byte-fallback
作者
发布于 2026年9月10日
版权协议 CC BY-NC-SA 4.0
评论加载似乎遇到了问题,请尝试刷新页面。