第 1 章用字符级分词,65 个字符;第 6 章起一直直接调 tiktoken.get_encoding("gpt2")。
这一关从零写一个同类的
字节级 BPE:
从 256 个字节出发,数相邻对、合并出现最多的一对,重复 16,128 次。配套代码 phase5-fullstack/16_bpe.py,
在第 6 章那份 FineWeb-Edu 的 8 MB 原文上训练,
再在另外 1 MB 上和 GPT-2 的分词器对照。Phase 5 补齐全链路,这是第一关。
第 1 章的字符级词表只认莎士比亚里出现过的 65 个字符,换一段中文就编不出来。 字节级的做法是先把文本按 UTF-8 编成字节:英文字母 1 个字节,中文 3 个,emoji 4 个。 一个字节只有 256 种取值,所以初始词表固定是 256,任何文本都编得出来。改下面的文字试试:
train_bpe 开头 list(w.encode("utf-8"))<e5><85>),
拼上后一个就完整了。第 5 章 05_sample.py 的 TokenStreamer 边生成边打印,就是凑齐一个完整字符再输出,避免吐出 �。
下面是一小段玩具语料,已经按空格切成词(每个词前面的空格记成 ␣,算词的一部分)。 左边是现在所有相邻对的出现次数,橙色那一对最多。点「合并一次」,它就变成一个新 token,记进 合并表:
train_bpe(真实版会记下每一对出现在哪些段里,合并时只改这些段)16_bpe.py 用 Python max 在计数字典上取最大值,
次数相同时取字典里排在前面的,同样是确定的。不同实现在这里的选择不同,所以用同一份语料、同样的词表大小训出来的合并表可能不完全相同。
16_bpe.py 的 make_encoder,和 tiktoken 的规则相同。解码更简单:每个 token 查回它的字节,拼起来。
合并之前先把文本切成小段,合并不能跨段。GPT-2 的 预切分 正则把字母串、数字串、其他符号、空白分成不同的段;只按空格切时,标点跟着前面的词。切换看同一句话怎么分段:
同样 8 MB 语料、同样 16,128 次合并,只换预切分。点一边看词表里字母和标点粘在一起的 token:
GPT2_SPLIT / SPACE_SPLIT · 对照跑法 --split spaceinformation, 这样一个 token 能顶两个,测试语料上每 token 多装了 –。
代价在词表:s, s. ing, ation. 这类变体占掉 2,279 个位置,约 14%,
同一个词带不同标点的几种写法各有各的嵌入向量,各自只拿到一部分训练信号。
GPT-2 论文 §2.2 描述了这个现象:BPE 会学出 dog. dog! dog? 这类变体,词表和模型容量分配得不理想,所以禁止合并跨越字符类别。
dog's 切成 dog 和 's,don't 切成 don 和 't。
用 GPT-2 正则训出的词表里,字母和标点粘在一起的 7 个 token 全是这几个缩写。完整正则见 16_bpe.py 的 GPT2_SPLIT,取自 openai/gpt-2 仓库 src/encoder.py。
GPT-4 的 cl100k 改过这条正则:缩写不分大小写,数字最多 3 位一段。
同一个分词器,只用合并表的前一部分,就是一个更小的词表。下面是 1 MB 测试语料在不同词表下的 每 token 字节数, 灰点是 GPT-2 的 50,257 词表。拖滑块:
make_encoder(merges, limit):只用前 limit 条合并词表 × n_embd 个参数;第 6 章 124M 的 50,304 × 768 = 38.6M,占全部参数的约 31%。
输出层每一步都要对整个词表算 softmax,词表大,这一步也更贵。另一头,排在后面的合并对应的 token 出现次数少,嵌入学得差。
本页曲线越往右越平:从 256 到 512 每 token 多 1.02 字节,从 8,192 到 16,384 只多 0.38 字节。
<|endoftext|> 特殊 token。第 6 章代码里向上取整到 50,304(128 的倍数),多出来的 47 行永远不会被用到。
它是在 GPT-2 自己的 WebText 上训的,不是 FineWeb-Edu;本页对照用的是同一段测试语料,两者训练语料不同,压缩率差距里也含有这部分影响。
选一个句子,再选合并次数,看它被切成哪些 token(相邻 token 交替着色,<e5> 这类是还没拼成完整字符的字节):
\p{N}+ 是一段,合并就在这段里按频率进行,哪几位合在一起取决于训练语料里哪些数字组合常见。
本分词器切成 8 , 0 45 …,GPT-2 切成 045 311 447,都不按位值对齐。
第 21 章做加法时就遇到这个问题:37、85 在 GPT-2 词表里各是一个整体 token,模型看不到个位和十位,所以那一章把数字逐位写开。
python 16_bpe.py --json runs/ch18_bpe.json(纯 CPU)· 对照 --split space