跳到主要内容

切词器 — BPE,把文字变成 ID

这一章讲三件事: 为什么文字必须先切成小块并编号; 字节对编码(BPE)怎么从「数据压缩」的老手艺变成大模型的标配; 以及本书真的从零写了一个 BPE 切词器——训练、编码、解码、存盘、载入全生命周期。 读完你能手推一个 200 行切词器的每一步。这是全书第一件「动手造」的零件。

1. 先看现象:模型不认识字

模型内部只有矩阵和加法(第 02 章的损失、第 04 章的参数全是数)。 「今天天气」这四个字对它是天书。切词器(tokenizer)就是文字世界和数字世界之间的桥: 把文字切成小单元(token),再按一张固定的对照表把每个单元换成一个编号1

原书对「好切词器」的要求有五条,归并成三类2:

要求一句话
规矩统一同样的标点、大小写、格式,每次都切出同样的结果
词表合身词表(对照表)太大费显存,太小切得太碎、一串变得太长
不怕生词没见过的词也能拆成熟悉的碎片——语义不丢

第三条是大词表的死穴:靠整词建表,新词、错拼、外语全都进不来。 解法是子词(比词小、比字符大的单元):把「不高兴」拆成「不+高兴」,把 unhappiness 拆成 un+happi+ness3

2. 三类切法,一个光谱

原书给了四类,按「切多碎」排成一条光谱4:

切法例:「The cat」优点死穴
按词[The] [cat]直观词表爆炸;生词抓瞎;中文没空格
按字符[T][h][e][空格][c][a][t]词表极小、什么都能拼串变得极长,计算贵;每个小块没什么含义
按子词[The] [cat]前两者的折中:常见词保持完整,生词自动拆要先在语料上「学」出词表
按手写规则在标点处切可定制、可解释规则靠人写,换语言就重写

子词这一档有三个出名的算法,BPE、WordPiece、SentencePiece;原书选 BPE 来实现, 理由是它简单、够用、是 GPT 系的标准5

3. BPE 的来历:一个压缩算法的转行

原书用两段讲完历史,这两段值得原样带走6:

  • 1994 年,Philip Gage 发明 BPE 时,它是个数据压缩算法:反复把文本里最高频的相邻字节对 换成一个没被用过的新字节——文本变短,还能无损还原;
  • 2015 年,Sennrich 等人把它搬进机器翻译,拿来对付稀有词:同样的「高频对合并」, 合出来的不再是压缩编码,而是子词词表

一句话点破两者的关系:学 BPE 词表就是在压缩语料——合并次数越多,语料被切得越短, 词表越大。这个「压缩」视角后面解释词数选择时还要用。

4. 主走查:算法一个来回一个来回怎么转

BPE 训练的全部逻辑三个来回就走完。语料(这些划分和频次按原书例子照录7):「the cat in the hat」。 先把每个词拆成字符,词尾加一个专门的「词尾记号」,防止跨词乱并:

初始 t h e / c a t / i n / t h e / h a t

第 1 回 数相邻对:「t h」出现 2 次、「h e」2 次,其余各 1 次
→ 挑「t h」,给它新编号 256,全文替换:t h e → [256] e
→ 词表多一条:256 = "th"

第 2 回 再数:[256] e 现在出现 2 次
→ 合成 257 = "the",替换后 the cat in the hat 里的两个 the 都变成了一个数

第 3 回 「c a」等各只出现 1 次……继续挑最高频的对,直到词表达到目标大小

图说:每一个来回 = 数对 → 合最高频 → 全文替换 → 记住这条规则。
新编号从 256 起跳,因为 0-255 要留给单字节字符[^8]。

编码新词时反过来用:把新词拆成字符,按「学的顺序」逐条套用合并规则, 套到没有规则可套为止8解码再反着来:查表把编号换回字符对,最后拆掉词尾记号、把字符拼回文字—— 原书强调这个过程是无损的,原文一个字都不会丢9

原书还给了第二个例子,展示「合并怎么长出词形」10:语料 low / lower / lowest / widest—— 「l o」并成 lo,「lo w」并成 low,「e s」→ es、「es t」→ est; 于是新词 lowest 被编码成 [low, est] 两个单元——常见词根自动成了词表条目

5. 真的写一个:本书的 BPETokenizer

第 4 节是算法,这一节是工程。本书的实现是 GPT-2 风格,四个设计决定各有用处11:

① 空格的前缀约定 Ġ。 GPT-2 不把空格当独立 token,而是把它粘到下一个词的开头, 用一个专门的字符 Ġ 表示。「Hello world」在它眼里是 Hello + Ġworld 两块。 原书给的转换例:"Hello world\n New line""HelloĠworld\nĠNewĠline"12。 为什么这么干:词在句首和在句中是两种分布,粘上空格记号后词表不用两套。

② 特殊 token 占据词表最前排。 <pad>(补齐用)、<unk>(不认识的字符)、 <s>(句首)、</s>(句尾)、<mask>(遮词)五个,编号 0-4 固定13。 固定的意义:训练时和推理(拿训好的模型来用)时,永远知道去哪找它们。

③ 词表留余量。 建词表时预留「至少 32 个、或词表数的 10%」给未来的合并, 避免基础字符把词表名额吃光14

④ 合并规则按「学的顺序」排成名次。 编码时遇到多条规则都能套,永远套名次最靠前的—— 保证同一个词无论在谁的机器上都切出同样的结果15

训练主循环就是第 4 节那三个来回的代码版:数最高频对(Counter 统计相邻编号对)→ 替换(从左到右扫描,遇到这对就换新编号)→ 登记 → 重复,直到词表满或没有对可并16

6. 生命周期的完整演示——包括一次「没考够」

原书在玩具语料上跑了一遍全流程,输出里有两个细节比成功更有教学价值17:

请求词表 160,实际只得 111 —— 语料太小,没有对可以并了,BPE 提前收工[^19]

学到的合并(节选): rank 0: 'o'+'r' → 'or'
rank 1: 'l'+'o' → 'lo'
rank 2: 'w'+'or' → 'wor'
rank 8: 'Hel'+'lo' → 'Hello' ← 整词这样长出来[^20]

编码 "Please, <mask> only this token."
→ 大写 P 从没在语料里出现过 → 变成 <unk>
→ 解码时 <unk> 被丢掉 → 输出成了 "lease,only this token."[^21]

「lease」这个事故把两件事一次讲透:词表是语料的函数(语料里没有的,词表里就没有); 在解码时静默消失(所以真实系统都尽量让基础字符覆盖完整,而不是靠 unk 兜底)。

最后是存/载测试:训练好的切词器存成两个文件——词表一份、合并规则一份,都用 JSON(一种通用的文本存档格式)—— 载入新实例后对同一段文字编码,前后得到的编号串完全一致18—— 这一致性是训练和推理必须吃同一套切法的前提。

7. 作者的判断与证据

说法书里的证据我们的标注
BPE 是现代大模型的标配切法点名 BERT/GPT/T5 采用;给了 5 万级词表的行业口径19领域共识,属实
词表开多大是「表达力 vs 效率」的折中GPT-2 的 50,257 是锚点;小词表把词切得碎、串变长,大词表嵌入贵20作者判断,与第 04 章的 vocab_size 一节互相印证
玩具语料足以演示 BPE 全部行为111/160 的输出与 "lease" 事故作者用实测输出兑现

判断(我们的,不是书里的): 原书这个切词器最大的教学价值在「防复制」的伏笔—— 它产出的编号串,在第 11 章被拿去建「禁止照抄训练文本」的对照表。 切词器不是 preprocessing 杂务,它是整条流水线的字典:训练、推理、防抄袭全都在查它。 如果错,会错在: 如果后续章节根本不回来用这个切词器,这句伏笔就不成立—— 但第 10 章(用它编码语料)和第 11 章(用它的词表建反抄袭对照表)都真实引用了它。

8. 边界与局限

原书自己列了 BPE 的四条局限,全部属实、全部有后果21:

  • 词表被语料锁死:换领域(医疗→法律)就要重训切词器;
  • 切点不合语言学:可能把 playing 切成 pla+ying 这类人类看不懂的碎法;
  • 超参敏感:词表开多大、合并多少次,都要调;
  • 可能切得太碎:形态复杂的语言里串变长,计算变贵。

两条出门会撞上的补充:其一,补充(不在书里,来自通用知识):今天各家模型的词表普遍到 5 万-20 万, 中文一个字常常只占一个 token,但生僻字会碎成多字节;其二,数字与代码的切法直接拖累算术与编程能力 (原书在数据工程一章点了一句22,第 10 章复述)。

9. 可带走的

  1. 模型只吃数字;切词器 = 切小单元 + 查表编号,是文字与数字世界之间的桥;
  2. 三档切法的光谱:整词(词表爆炸)↔ 字符(串太长爆炸),子词是折中;
  3. BPE = 从字符起步,反复合并最高频相邻对;它在 1994 年是压缩算法,2015 年转行进 NLP(自然语言处理——教机器对付人类语言的学问);
  4. 学词表 = 压缩语料:词表越大,语料被压得越短;
  5. 新编号从 256 起跳、词尾记号防跨词乱并、特殊 token 占最前排、合并规则按名次套——四个工程决定缺一不可;
  6. Ġ 约定:空格粘在词头,句首句中的同一个词才不会劈成两个条目;
  7. 词表是语料的函数:语料里没有的字符,只能变成 ,解码时还会静默消失;
  8. 存/载后编码必须逐位一致——这是训练与推理同源的硬前提。

10. 原文地图

主题原书章原文位置
切词的定义与例子What Is Tokenization?text/23-fm-what-is-tokenization.txt:3(搜「I love to code」) · text/23-fm-what-is-tokenization.txt:5(搜「unique identifiers」)
切词重要的五条理由Why Is Tokenization Important?text/24-fm-why-is-tokenization-important.txt:5(搜「Standardization」) · text/24-fm-why-is-tokenization-important.txt:21(搜「unhappiness」)
四类切法Types of Tokenizerstext/25-fm-types-of-tokenizers.txt:7(搜「Word-Based Tokenizers」) · text/25-fm-types-of-tokenizers.txt:21(搜「Character-Based」) · text/25-fm-types-of-tokenizers.txt:37(搜「Subword-Based」)
BPE 的两段历史Byte-Pair Encoding (BPE): A Deep Divetext/26-fm-byte-pair-encoding-bpe-a-deep-dive.txt:7(搜「Philip Gage」) · text/26-fm-byte-pair-encoding-bpe-a-deep-dive.txt:9(搜「Sennrich」)
算法三步与新 ID 从 256 起BPE Algorithm Outline and Mathematical Formulationtext/27-fm-bpe-algorithm-outline-and-mathematical-formulati.txt:11(搜「256」) · text/27-fm-bpe-algorithm-outline-and-mathematical-formulati.txt:15(搜「50,257」)
the cat in the hat 演示BPE Algorithm Exampletext/28-fm-bpe-algorithm-example.txt:27(搜「t h」) · text/28-fm-bpe-algorithm-example.txt:35(搜「256」)
解码无损还原BPE Algorithm Exampletext/28-fm-bpe-algorithm-example.txt:99(搜「Substitute」) · text/28-fm-bpe-algorithm-example.txt:107(搜「losslessly」)
low/lowest 词形生长Step-by-Step Process of Building a BPE Tokenizertext/29-fm-step-by-step-process-of-building-a-bpe-tokenizer.txt:57(搜「lowest」)
BPE 与 WordPiece/SentencePiece 之比Comparison with Other Tokenization Methodstext/33-fm-comparison-with-other-tokenization-methods.txt:5(搜「WordPiece」)
词表 3 万-5 万口径Practical Considerationstext/34-fm-practical-considerations.txt:3(搜「50,257」)
Ġ 约定与特殊 tokenBPE Implementation Walkthroughtext/35-fm-bpe-implementation-walkthrough.txt:5(搜「Ġ」) · text/35-fm-bpe-implementation-walkthrough.txt:53(搜「」) · text/35-fm-bpe-implementation-walkthrough.txt:167(搜「headroom」)
合并名次BPE Implementation Walkthroughtext/35-fm-bpe-implementation-walkthrough.txt:215(搜「rank view」)
GPT-2 预处理的转换例Helping Functionstext/36-fm-helping-functions.txt:331(搜「HelloĠworld」)
训练循环的两个静态方法Helping Functionstext/36-fm-helping-functions.txt:371(搜「_find_freq_pair」) · text/36-fm-helping-functions.txt:381(搜「_replace_pair」)
111/160 与词形生长Analysis of the Outputtext/38-fm-analysis-of-the-output.txt:11(搜「111」) · text/38-fm-analysis-of-the-output.txt:47(搜「rank 0」) · text/38-fm-analysis-of-the-output.txt:59(搜「Hel」)
lease 事故Analysis of the Outputtext/38-fm-analysis-of-the-output.txt:97(搜「」) · text/38-fm-analysis-of-the-output.txt:99(搜「lease」)
存载一致Analysis of the Outputtext/38-fm-analysis-of-the-output.txt:157(搜「Match? True」)
BPE 局限Limitations of BPEtext/32-fm-limitations-of-bpe.txt:3(搜「playing」)

Footnotes

  1. 出处:「What Is Tokenization?」第 3 段(text/23-fm-what-is-tokenization.txt:3,搜「I love to code」)与第 5 段(text/23-fm-what-is-tokenization.txt:5,搜「unique identifiers」)。原文:切词把文字拆成 token(词、子词、字符或标点),并映射到词表中的唯一 ID。

  2. 出处:「Why Is Tokenization Important?」第 5-21 段(text/24-fm-why-is-tokenization-important.txt:5,搜「Standardization」;:9,搜「Vocabulary Creation」;:17,搜「Efficiency」;:21,搜「unhappiness」)。原文列五条:标准化、词表创建、语言多样性、效率、语义保留。

  3. 出处:「Why Is Tokenization Important?」第 21 段(text/24-fm-why-is-tokenization-important.txt:21,搜「unhappiness」)。原文:好切词器把 unhappiness 拆成 [「un」, 「happi」, 「ness」],让模型学到它与 happy 的关系。

  4. 出处:「Types of Tokenizers」第 7-63 段(text/25-fm-types-of-tokenizers.txt:7,搜「Word-Based Tokenizers」;:21,搜「Character-Based Tokenizers」;:37,搜「Subword-Based Tokenizers」;:55,搜「Rule-Based Tokenizers」)。

  5. 出处:「Subword-Based Tokenizers」第 37 段(text/25-fm-types-of-tokenizers.txt:37,搜「byte-pair encoding (BPE), WordPiece, and SentencePiece」)。

  6. 出处:「Byte-Pair Encoding (BPE): A Deep Dive」第 7、9 段(text/26-fm-byte-pair-encoding-bpe-a-deep-dive.txt:7,搜「Philip Gage」;text/26-fm-byte-pair-encoding-bpe-a-deep-dive.txt:9,搜「Sennrich」)。原文:1994 年 Gage 的压缩算法——反复用未占用字节替换最高频相邻字节对;2015 年 Sennrich/Haddow/Birch 在《Neural Machine Translation of Rare Words with Subword Units》中移植到 NLP。

  7. 出处:「BPE Algorithm Example」第 27-37 段(text/28-fm-bpe-algorithm-example.txt:27,搜「t h」;text/28-fm-bpe-algorithm-example.txt:35,搜「256」)。原文:「t h」与「h e」各出现两次;挑「t h」赋新 ID 256;词表更新为 {…, 256: "th"}。

  8. 出处:「BPE Algorithm Example」第 87-91 段(text/28-fm-bpe-algorithm-example.txt:83,搜「Encoding New Text」)。原文:编码新词时初始化为字符序列,按顺序应用每条合并规则。

  9. 出处:「BPE Algorithm Example」第 107 段(text/28-fm-bpe-algorithm-example.txt:107,搜「losslessly」)。原文:按词表逆序替换即可无损还原原文。

  10. 出处:「Step-by-Step Process of Building a BPE Tokenizer」第 57-65 段(text/29-fm-step-by-step-process-of-building-a-bpe-tokenizer.txt:57,搜「lowest」)。原文:对 lowest 依次套合并,最终 token 是 [「low」, 「est」],再映射到 ID。

  11. 出处:「BPE Implementation Walkthrough」第 5 段(text/35-fm-bpe-implementation-walkthrough.txt:5,搜「Ġ」)。原文:该实现采用 GPT-2 风格,含 Ġ 空格前缀约定、特殊 token、从零训练或加载预置词表/合并。

  12. 出处:「Helping Functions」(_preprocess_gpt2 一节)(text/36-fm-helping-functions.txt:331,搜「HelloĠworld」)。原文给的转换例:"Hello world\n New line" → "HelloĠworld\nĠNewĠline";行首不加 Ġ。

  13. 出处:「BPE Implementation Walkthrough」第 53 段(text/35-fm-bpe-implementation-walkthrough.txt:53,搜「」)。原文:特殊 token 列表为

  14. 出处:「BPE Implementation Walkthrough」第 167 段(text/35-fm-bpe-implementation-walkthrough.txt:167,搜「headroom」)。原文:预留 max(32, 词表数的 10%) 的余量给合并。

  15. 出处:「BPE Implementation Walkthrough」第 109 段(text/35-fm-bpe-implementation-walkthrough.txt:109,搜「bpe_ranks」)。原文:bpe_ranks 存 (字符串对) → 合并名次,保证 GPT-2 式的稳定、确定性查找。

  16. 出处:「Helping Functions」(_find_freq_pair 与 _replace_pair 两节)(text/36-fm-helping-functions.txt:371,搜「_find_freq_pair」;text/36-fm-helping-functions.txt:381,搜「_replace_pair」)。前者用 Counter(zip(ids, ids[1:])) 取最高频相邻对;后者用 deque 从左到右扫描替换。

  17. 出处:「Analysis of the Output」第 11 段(text/38-fm-analysis-of-the-output.txt:11,搜「111」)。原文:请求 160 个词表,实际只得 111——语料太小,没有足够的高频对可并。

  18. 出处:「Analysis of the Output」第 157 段(text/38-fm-analysis-of-the-output.txt:157,搜「Match? True」)。原文:重载后对同一段文字编码得到完全相同的 ID 序列。

  19. 出处:「Practical Considerations」第 3 段(text/34-fm-practical-considerations.txt:3,搜「50,257」)。原文:3 万-5 万 token(如 GPT-2 的 50,257)是大型模型的常用词表区间。

  20. 出处:「Vocabulary Size and Hyperparameters」第 123 段(text/28-fm-bpe-algorithm-example.txt:123,搜「Expressiveness」)。原文:大词表表达力强,小词表省内存与算力。

  21. 出处:「Limitations of BPE」第 5-13 段(text/32-fm-limitations-of-bpe.txt:3,搜「playing」)。原文:依赖语料、可能切出 pla+ying 这类不直觉碎片、词表固定难迁移、对超参敏感、可能过度切分。

  22. 出处:「Dataset Preparation for LLM Training」(Tokenization Strategy 一节)(text/69-fm-dataset-preparation-for-llm-training.txt:73,搜「Poor tokenization」)。原文:糟糕的数字切法伤害数学推理,糟糕的代码切法伤害编程能力。