Skip to content

第5章 · UTF-8、byte BPE 与数据管线

词表与数据许可需按实际来源单独记录。

先修:第4章的 token、概率和数据切分。

本章目标

  • 区分 Unicode 字符、UTF-8 bytes、token id 和模型上下文长度。
  • 手写 BPE 的 pair 统计、merge、训练、编码、解码和词表序列化。
  • 对中英混合、emoji、未知 bytes 和特殊 token 做可逆性与边界测试。

公式与 shape

给定 token 序列 s,每轮选择频次最高的相邻 pair:

(a,b)=argmax(a,b)counts(a,b),smerge(s,(a,b),v).

频次相同的 pair 固定选择字典序最小者;special token 是硬边界,不参与普通 byte merge。

编码是 bytes → token ids,解码是 token ids → bytes → UTF-8 文本;合法输入要求 decode(encode(x))=x

对象shape说明
UTF-8 bytes(N,)每个元素为 0..255
token ids(T,)T 取决于 merge 词表
pair countsMap[(id,id), count]每轮重新统计
vocabMap[id, bytes]序列化时固定版本和 special-token 策略

数学桥接:第2章 → 第5章

BPE 的词表大小选择是一个 率失真 问题:词表越大,每个 token 的平均信息量(自信息)越小;词表越小,每个 token 需要编码更多信息。第2章 §6 的 entropy(probs) 可以衡量一个语料在给定词表下的平均不确定性——同个语料用不同 merge 次数的词表编码,熵会变化。

直觉:英文文本的 byte 级熵约 4-5 bits/byte(因为有大量冗余),BPE 通过 merge 高频 pair 来压缩常见模式,相当于用更短的 code 编码高频事件——这正是 Huffman 编码的思想。merge 次数越多,词表越大,平均 token 长度越短,但 single-token 信息量也越分散。

前端类比:BPE merge 就像 CSS 的 font-feature-settings——把常见字母组合(如 "th")合成一个"连字"(ligature),减少渲染开销。但过度合并会导致罕见组合无法表达(类似 ligature 太多反而看不清原文)。

交互观察

交互:BPE 合并与编解码

raw bytes: 6 token ids: [258, 256, 257] (3 toks) decode: lowest round-trip: OK

合并步骤
merge 1: ("w" + "e") → id 256 ×8
merge 2: ("s" + "t") → id 257 ×8
merge 3: ("l" + "o") → id 258 ×7
merge 4: ("st" + " ") → id 259 ×7
merge 5: (" " + "lo") → id 260 ×6
merge 6: ("st " + "n") → id 261 ×6
merge 7: ("st n" + "e") → id 262 ×6
merge 8: ("st ne" + "we") → id 263 ×6
merge 9: ("w" + " lo") → id 264 ×5
merge 10: ("st newe" + "st newe") → id 265 ×5

改变语料和 merge 次数,检查 token 数、merge 顺序以及 decode 是否仍与输入完全一致。浏览器 round-trip 不是许可证或随机 UTF-8 覆盖的替代品。

从零实践

  1. 先实现 get_statsmerge,用手算 fixture 验证 pair 计数和 tie-break。

  2. 用 byte 序列训练小词表,明确边界 token、未知 bytes 和特殊 token 不可被普通 merge 吞并。

  3. 实现 encode/decode round-trip,分别测试 ASCII、中英文、emoji 和随机合法 UTF-8。

  4. 当前入口与基础测试:

    bash
    PYTHONPATH=python python -m pytest python/tests/test_bpe.py -q

当前 python/llm_core/bpe.py 已提供 UTF-8 byte BPE、词频相同 pair 的确定性 tie-break 和 special-token 硬边界;本地混合文本 fixture 已记录在 evidence/05-runtime-v1.json。随机 property 覆盖、生产 tokenizer 兼容和数据许可清单仍是本章 gate,不得用 fixture 通过冒充完整覆盖。

故障注入与预期信号

本表是本章唯一的故障注入权威清单;「动手实验」一节不再另列第二份。

注入预期失败信号修复后证据
vocab 与 merge 顺序未绑定同一文本在不同运行中 token ids 改变词表/配置 hash 和重放一致
str 而非 bytes 切分输入中文或 emoji 的 decode(encode(x)) != x最底层统一按 UTF-8 bytes 处理,str 仅在最外层包装
合并 pair 时频次并列未稳定排序同一语料两次训练得到不同词表,结果不可复现按(频次降序、pair 字典序)稳定排序,两次训练词表一致
特殊 token 参与普通 byte merge特殊 token 被拆成多个 byte id,is_special 判断失效切分阶段先隔离特殊 token,merge 只在非特殊片段上跑
vocab_size 小于 256byte 基表被合并跳过或 id 缺失,round-trip 失败保证 vocab_size >= 256 + 特殊 token 数,并在训练入口断言

论文与延伸

前端/Agent 迁移

tokenizer 是输入协议而不只是“分词工具”:版本、特殊 token、最大长度和 decode 规则都属于接口契约。Agent 的 tool schema、事件格式和持久化状态也要有版本与可逆解析,否则上游模型升级会变成隐性协议破坏。

口述与自测(不看资料,5–10 分钟)

  • 用一个中英混合字符串说明 Unicode 字符、UTF-8 byte 和 token id 的区别,并口述一轮 BPE pair 统计、tie-break 与 merge 后序列如何变化。
  • 解释特殊 token 为什么不能只靠普通文本替换;给出一种注入失败路径,以及 encode/decode 如何保持边界可审计。
  • 用第2章 entropy 的思路解释:为什么同一个语料用不同 merge 次数的词表,平均每个 token 携带的信息量会不同?
  • 说明 BPE 和 Huffman 编码的共同目标(高频短码、低频长码),以及 BPE 为什么不能保证最优压缩。

实验与参考

动手实验

把 tokenizer 的工作从“黑盒调用”变成“看得见的统计过程”:train_bpe 按频次稳定合并字节对,词表大小受 vocab_size 控制;对任意字符串(包括中文与 emoji)调用 encodedecode 后输出与输入完全一致;合并顺序是确定的,相同输入与相同 vocab_size 多次训练应得到相同的 token id 序列。

环境准备

bash
cd <仓库>
export PYTHONPATH="$PWD/python"

命令与预期输出

bash
# 交互验证:训练 + 中英混合 round-trip(test_bpe.py 已在「从零实践」跑过)
python -m labs.run_all
text
bpe [104, 101, 108, 108, 111] hello

# 判定条件:
# - decode(encode(x)) == x 对纯 ASCII、中文、emoji 都成立
# - 同一语料两次训练,encode('hello') 输出的 id 序列一致
# - vocab_size >= 256,确保 byte 基表完整

概念图

图:第5章 BPE 分词训练与编解码流程 — 第2章的信息论直觉(高频子串合并 = 压缩)驱动 BPE 迭代合并:从 Base-256 字节开始,反复合并最高频相邻 pair,直到达到目标 vocab_size。训练产出 merge 表,推理时用 encode/decode 做文本↔id 双向转换。后续第6章把 token id 查表为 embedding 向量,第7章把 token 序列送入 attention,第11章用子词边界策略处理长文本 chunking。

故障注入清单

故障注入以正文「故障注入与预期信号」一节为唯一权威清单(5 项,覆盖 vocab 绑定、str/bytes 切分、tie-break、special-token 边界和 vocab_size 下限),此处不再另列第二份;做故障题时逐项对照该表的预期信号与修复后证据。

资源 / 成本 / 隐私

本地 Python/NumPy 即可,合成中英/emoji fixture 不产生云费用。只有许可证明确的数据才能进入词表;原始履历、飞书文档和私人语料禁止写入仓库。

Evidence

仓库当前机器证据(只读快照)

evidence/module-manifest-v1.json05.evidence 指向当前文件:evidence/05-runtime-v1.json。这是当前 checkout 的脱敏机器运行记录,只覆盖该 JSON 记录的命令、指标、产物和已知失败;它不是学习者提交,也不能推出学习者已完成本章。

学习者提交模板(待填写,不是当前机器证据)

复制下面模板并填写自己的真实运行结果。所有 <...> 都是未填写状态;actualartifacts 尤其不能被当作已运行或已通过。artifacts 必须替换为本次提交中真实存在的仓库相对路径。

yaml
schema: learn-llm.evidence.v1
module: 05-tokenizer
commit: <learner-commit-sha>
verified_at: <iso-date>
environment: <sanitized-python-device>
seed: 3
commands:
  - PYTHONPATH=python python -m pytest python/tests/test_bpe.py -q
  - PYTHONPATH=python python <learner-mixed-utf8-roundtrip>
metrics:
  - name: roundtrip_exact_rate
    expected: 1.0
    actual: <recorded-value>
  - name: vocab_hash
    expected: <frozen-hash>
    actual: <recorded-hash>
artifacts:
  - <learner-repo-relative-artifact-path>
cost:
  gross_usd: 0
  credit_usd: 0
licenses:
  - source: <source>
    version: <version>
    license: <license>
    attribution: <attribution>
    redistribution: <redistribution>
known_failures:
  - <sanitized-failure-or-none>

混合 UTF-8、special-token boundary、词表 hash 或来源记录任一缺失时,本章保持 gate

私有学习站 · 原理从零构建 · 勿提交个人隐私或密钥