跳到主要内容

数据截至 (上游 commit 1acefe89412b)

minbpe — 架构与原理

30 秒导读: minbpe 是 Andrej Karpathy 写的教学用 LLM 分词器库:三个Tokenizer 类(Basic → Regex → GPT4)层层递进,用约 500 行带密集注释的纯 Python,把「文本怎么变成 token id」这件事从最裸的字节对合并,一直讲到逐 token 精确复刻 GPT-4 的 tiktoken 分词结果。它存在的意义不是拿来跑生产,而是让你一个下午读完、从此真正理解 BPE 分词器是怎么回事。


1. 这是什么(零基础也能懂)

一句话定义

minbpe 是一个 byte-level BPE(Byte Pair Encoding,字节对编码)分词器的最小可读实现:先教会它一份语料(训练出合并表),它就能把任意字符串切成整数序列(编码),也能把整数序列原样还原成字符串(解码)。

它要解决谁的什么问题

设想你在学 LLM,看过无数遍「模型吃的是 token 不是字符」,但始终有几个疑问:

  • 「token」到底怎么从文本里切出来的?为什么「hello」是一个 token、「hello123」会被切成几段?
  • 为什么模型常常在数字、空格、非英文文本上表现怪异?
  • GPT-4 官方分词器(tiktoken)的内部规则长什么样?

这些问题的答案藏在 tiktoken 的 Rust 实现里,不好读。minbpe 把答案压缩到一个人能通读的尺寸:四个 Python 文件,每行都有注释。

它能做什么

  • 训练自己的分词器:给一段文本和一个词表大小,学出一份合并规则表(merges)。
  • 编码 / 解码:字符串 ↔ token id 序列,严格往返(round-trip)。
  • 复刻 GPT-4 分词:GPT4Tokenizer 加载 tiktoken 的 cl100k_base 参数后,编码结果与 tiktoken 逐 token 相等(有对拍测试为证,tests/test_tokenizer.py:62-69)。
  • 存取模型:把训练好的分词器存成纯文本 .model 文件,随时加载。

不能做的事同样重要,见 §5 边界与局限。

用起来什么样

最小示例:复现 Wikipedia 上 BPE 词条的经典例子(摘自 README.md:22-33,已精简):

from minbpe import BasicTokenizer
tokenizer = BasicTokenizer()
text = "aaabdaaabac"
tokenizer.train(text, 256 + 3) # 256 个字节 token + 学 3 条合并规则
print(tokenizer.encode(text)) # [258, 100, 258, 97, 99]
print(tokenizer.decode([258, 100, 258, 97, 99])) # aaabdaaabac
tokenizer.save("toy") # 写出 toy.model(给机器)和 toy.vocab(给人看)

训练出的三条规则是:(a,a)→256(a,b)→257(256,257)→258。于是原串被压成 [258, d, 258, a, c]——11 个字节变 5 个 token。

一句话直觉

BPE 就是数据压缩。 把文本看成字节流,反复做一件事:找当前最常相邻出现的两个字节(或片段),把它们「焊」成一个新符号。焊得越多,常见单词/词根就变成单个 token,文本序列越短——而 LLM 的上下文窗口按 token 计费,序列短就是一切。


2. 顶层全景(它大概怎么转)

2.1 部件与依赖

整个库是四个 Python 文件,呈一条继承链:

base.py Tokenizer 基类
┌─────────────────────────────────┐
│ get_stats / merge (原子操作) │
│ _build_vocab / save / load │
└───────────────┬─────────────────┘
│ 继承
┌──────────┴──────────┐
▼ ▼
basic.py regex.py
BasicTokenizer RegexTokenizer
整段文本直接跑 BPE 先按 regex 切块,块内独立 BPE
+ special tokens 支持
│ 继承

gpt4.py
GPT4Tokenizer
灌入 tiktoken 的参数,精确复刻 GPT-4

怎么读这张图: 上下是继承方向(箭头指向子类);所有真实算法都在 base.py 的两个自由函数和 basic.py 里,后两个子类每层只加一个概念:regex 层加「切块」,gpt4 层加「别人的参数」。

2.2 部件职责

部件干什么在哪个文件
get_stats / mergeBPE 的两个原子操作:数相邻对频次 / 把某对全部替换成新 idminbpe/base.py:13minbpe/base.py:25
Tokenizer基类:由 merges 推导 vocab、模型的 save/loadminbpe/base.py:66
BasicTokenizer最简 BPE:train / encode / decode 三件套,直接跑在整段文本上minbpe/basic.py:15
RegexTokenizer加两件套:regex 预切分(默认 GPT-4 模式)+ special tokensminbpe/regex.py:22
GPT4Tokenizer不训练,灌入 tiktoken cl100k_base 的合并表与 special tokens,复刻 GPT-4minbpe/gpt4.py:57
recover_merges / bpe从 tiktoken 只存「token→rank」的表里,反推出「(对)→新 id」的合并表minbpe/gpt4.py:29minbpe/gpt4.py:11
对拍测试与 tiktoken 逐 token 断言相等 + 编码解码往返tests/test_tokenizer.py:52-77
train.py演示脚本:在 Taylor Swift 维基文本上训两个 512 词表的分词器并存盘train.py:17-24

2.3 主线走一遍(从语料到可用分词器)

BasicTokenizer 为例,分词器的完整生命周期四步:

① train:文本 → UTF-8 字节 → [0..255] 整数流
→ 循环「数相邻对 → 合并最高频对 → 记录规则」vocab_size-256 次


② 产物:merges 合并表 —— 分词器学到的全部知识
(vocab 不用学,由 merges 确定性推导出来)


③ encode:新文本 → 字节流 → 按 merges 里的优先级反复合并 → id 序列


④ decode:id 序列 → 逐 id 查 vocab 拼回字节 → UTF-8 解码 → 原字符串

这条线最值得记住的一点:训练得到的资产只有一张 merges。编码不是「查词典」,而是拿这张表在新文本上按优先级重演合并过程——所以编码和训练是同一个机制的两个方向,这是理解整个库的钥匙(详见 01 章)。


3. 阅读地图(建议顺序)

minbpe 极小,三章读完连同源码不超过两小时。01 是心脏,必读;02、03 各半小时。

顺序章节讲什么适合谁
101 · BPE 算法本体get_stats/merge 两个原子操作;训练、编码、解码三个循环;vocab 推导与模型存取所有人必读
202 · regex 预切分与 GPT-4 模式为什么要按类别切块;GPT4_SPLIT_PATTERN 逐项拆解;块内独立 BPE想懂「GPT 系分词怪癖从哪来」的人
303 · special tokens 与 GPT-4 精确复刻allowed_special 四种语义;recover_merges 反推合并表;byte_shuffle 历史包袱想把教学算法接到 tiktoken 真实参数上的人

上手路径建议:先跑 README.md:22-33 的 Wikipedia 例子建立体感,再按章节读;想动手重写一遍,仓库自带分步练习 exercise.md


4. 巧妙之处(可借鉴的技术)

每条先白话点出妙在哪,再给锚点;细节论证见各章节。

  1. 训练与编码是同一枚硬币的两面。 训练「自底向上」造合并表(最高频的对优先变成 token);编码「按优先级重演」合并表(合并表里 rank 最小的对先合并)。编码端的优先级查找只用一行:min(stats, key=lambda p: self.merges.get(p, float("inf")))(minbpe/basic.py:64)——查不到的对得无穷大,自然排到最后。
  2. vocab 不需要存储。 词表(id→字节串)由合并表确定性推导:256 个字节打底,每条合并规则把两个子 token 的字节拼起来(minbpe/base.py:88-95 _build_vocab)。所以 .model 文件里只存 merges、pattern、special tokens 三样(minbpe/base.py:104-116)。
  3. merge 是单趟线性替换。 一个 while 循环扫过序列,命中对就写新 id 并跳两格,否则照抄(minbpe/base.py:25-41)。算法教学里最干净的一版。
  4. regex 预切分把「跨类别合并」问题转化为「块内独立 BPE」。 训练和编码共用同一把刀(re.findall),保证两边切法永远一致;模式串本身被写进 .model 文件随模型走(minbpe/base.py:109)。
  5. 从「只存结果」的格式里反推规则。 tiktoken 不存合并对,只存每个 token 的字节串和 rank;recover_merges 对每个 token 做一次「限高 BPE」(只许用 rank 比它小的部件),最后必然剩两个部件——那就是它的父母(minbpe/gpt4.py:29-46)。
  6. 把历史包袱隔离成兼容层。 GPT-4 的 256 个字节 id 被打乱过;GPT4Tokenizer 不动主算法,只在编码入口 shuffle、解码出口 unshuffle(minbpe/gpt4.py:81-92)。
  7. 用对手做测试。 不手写期望 id,直接拿 tiktoken 当裁判,断言两边输出逐一相等(tests/test_tokenizer.py:62-69)——与 micrograd 拿 PyTorch 对拍是同一招。

5. 边界与局限

minbpe 是刻意做小的教学件,以下每一条都是设计取舍,不是缺陷:

边界具体含义依据
性能不可用每轮合并都全量重数相邻对、纯 Python 循环;train.py 在约 18 万字节文本、512 词表上就要约 25 秒(train.py:3 注释)。生产级训练要用 Rust 实现README.md:143-144 todos 自述
只有 byte-level BPE 一种算法不支持 sentencepiece 的 codepoint 级 BPE(Llama/Mistral 系),也没有 Unigram / WordPieceREADME.md:146 todos;exercise.md Step 5
GPT4Tokenizer 不可训练、不可存盘train / save / load 直接 raise NotImplementedError——它是预训练分词器的只读复刻minbpe/gpt4.py:95-107
special tokens 不参与训练只能事后 register_special_tokens 挂上去,id 要手工规划(接在 merges 之后,vocab_size 起)README.md:107-115
编码对无效输入不报错decodeerrors="replace",非法 UTF-8 序列静默变成 ;BasicTokenizer.decode 对未知 id 则直接 KeyErrorminbpe/basic.py:54
项目事实冻结最后一个 commit 停在 2024-04-22;代码主体随 2024-02 的教学视频定型git log(53 个 commit,头部 1acefe8)

想继续往前走的人:作者在 exercise.md Step 5 指了下一个副本——把 BPE 搬到 Unicode code point 上,去复刻 Llama 2 的 sentencepiece 分词器。


6. 横向对比

同书架上与 minbpe 关系最自然的对比对象:

项目与 minbpe 的关系取舍差异
tiktoken(非书架项目,作参照)GPT4Tokenizer 的复刻对象与对拍裁判tiktoken 用 Rust 核心换性能,且刻意不暴露训练与合并表;minbpe 用纯 Python 换「每一行都能读」
tokenizersHuggingFace 的生产级分词库,同问题的工业答案多算法(BPE/Unigram/WordPiece)、Rust、流式与并行;minbpe 只有一种算法的教学骨架
micrograd同作者、同风格、同尺寸的教学件一个讲透反向传播,一个讲透分词;都按「一个下午读完」设计
nanogpt直接消费 tiktoken 的 GPT-2 BPE 做数据准备(data/openwebtext/prepare.py)nanogpt 把分词器当黑盒用;minbpe 正是把这个黑盒拆开的那一份
nanochat全栈仓库里的分词器章节:rustbpe 训练 + tiktoken 推理的双拼nanochat 是 minbpe 思路的「工程提速版」——同一套概念,换成能训 2B 字符的实现

结论:minbpe 在书架上的定位是「LLM 分词的最小可读样本」——理解它之后,再去看 tiktoken、tokenizers 或 sentencepiece,都是在看同一件事的工程化版本。


7. 代码地图(导航索引)

读源码顺序:先 base.py(165 行),再 basic.py(74 行),然后 regex.py(164 行),最后 gpt4.py(130 行)。全程约 540 行,注释占一半。

主题文件路径符号名
相邻对统计(原子操作一)minbpe/base.pyget_stats
对替换(原子操作二)minbpe/base.pymerge
基类三状态(merges/pattern/special)minbpe/base.pyTokenizer.__init__
vocab 确定性推导minbpe/base.pyTokenizer._build_vocab
模型存取(纯文本格式)minbpe/base.pyTokenizer.saveTokenizer.load
token 打印转义minbpe/base.pyrender_tokenreplace_control_characters
最简训练循环minbpe/basic.pyBasicTokenizer.train
最简编码(按优先级重演合并)minbpe/basic.pyBasicTokenizer.encode
最简解码minbpe/basic.pyBasicTokenizer.decode
GPT-2 / GPT-4 切分模式常量minbpe/regex.pyGPT2_SPLIT_PATTERNGPT4_SPLIT_PATTERN
带切分的训练minbpe/regex.pyRegexTokenizer.train
块内编码与整句编码minbpe/regex.pyRegexTokenizer._encode_chunkRegexTokenizer.encode_ordinary
special tokens 注册与编解码minbpe/regex.pyRegexTokenizer.register_special_tokensRegexTokenizer.encodeRegexTokenizer.decode
从 tiktoken 反推合并表minbpe/gpt4.pyrecover_mergesbpe
字节重排兼容层minbpe/gpt4.pyGPT4Tokenizer.byte_shuffleGPT4Tokenizer._encode_chunkGPT4Tokenizer.decode
GPT-4 special tokens 常量minbpe/gpt4.pyGPT4_SPECIAL_TOKENS
演示训练脚本train.py(模块级脚本)
往返 / 对拍 / 存取测试tests/test_tokenizer.pytest_encode_decode_identitytest_gpt4_tiktoken_equalitytest_save_loadtest_wikipedia_example