对应 Maps to:CMU 11-868 / 11-968 Large Language Model Systems(Spring 2026,Lei Li),Lecture 8 · Mon 2/9 · Tokenization and Embedding。 阅读材料:Sennrich, Haddow & Birch (2016) Neural Machine Translation of Rare Words with Subword Units(BPE);Kudo & Richardson (2018) SentencePiece;Xu et al. (2021) Vocabulary Learning via Optimal Transport for Neural Machine Translation(VOLT,ACL 最佳论文);Yuan et al. (2024) How Vocabulary Sharing Facilitates Multilingualism in LLaMA?;Pagnoni et al. (2024) Byte Latent Transformer。 依据说明:本讲以原课第 8 讲公开的 slides(llmsys-08-tokenization.pdf,45 页)为主线:按词切(词表代码、什么是一个词、中文没有空格、优缺点)、按字切、子词与 BPE(训练算法、cat / catch / rat / rattle 的例子、贪心编码、代码走读)、VOLT(压缩率、归一化熵、MUV、最优传输与 Sinkhorn、编码方式)、LLM 分词的实际问题(LLaMA-3 的去重与过滤、BBPE / WordPiece / SentencePiece、代码的预分词、数字与 xVal、LLaMA-3 的 128K 多语言词表及其构造方法)、词表共享与多语言(嵌入微调的四个象限、停滞象限的过度切分、扩词表与缩短)、无分词器的 BLT。三种切法在本课程语料上的对比、BPE 两种编码方式的差异、WordPiece 打分的对比、字节级 BPE 在中文上的行为、预分词 / 数字 / 缩进 / 语料配比的实验、MUV 曲线与代理模型、合成网页上的去重流水线、词表的系统代价与 BLT 式熵分块,都是本讲义自己设计的,不代表课堂上讲了什么。 配套代码 Code:
code/lecture08/(6 个脚本 +bpe.py+ 冻结的语料;都在 1 分钟以内) 预计阅读时间:3–4 小时(含运行代码)
0. 本讲导航 · How to read this lecture
0.1 学习目标 Learning goals
学完这一讲,你应该能够:
- 说出按词、按字、按子词三种切法各自的问题(未登录词、词表大小、序列长度、没有空格的语言),解释子词为什么是折中。
- 手算 BPE 的训练过程,写出它的训练与编码;说清”按合并顺序编码”和”贪心最长前缀”为什么可能不同;区分 BPE、WordPiece、SentencePiece、字节级 BPE。
- 解释 LLM 分词器的实际细节:UTF-8 与字节回退、正则预分词、数字的切法、代码的空白、多语言词表的分配,以及它们对 token 数(= 计算量、上下文长度、价格)的影响。
- 用压缩率、归一化熵、边际效用(MUV)讨论”词表多大最好”,理解它背后”序列长度 vs 统计稀疏”的权衡。
- 说出预训练语料去重与过滤的流水线(URL、整篇哈希、MinHash + LSH、行级去重、启发式过滤),以及 MinHash 为什么能估计 Jaccard。
- 估算词表对模型的系统代价:嵌入和输出层的参数、每字节的 FLOPs、logits 的显存;理解无分词器模型(BLT)的思路。
0.2 先修与本讲范围 Prerequisites & scope
| 默认你已经会 Assumed | 本讲只给直觉,后面再展开 Previewed only |
|---|---|
| 嵌入就是查表、共享嵌入(第 6 讲 §4) | 融合的交叉熵内核(第 10 讲 LightSeq) |
| n-gram 语言模型与平滑、稀疏性(第 1 讲) | 解码时的 token 级操作(第 9 讲) |
| T5 / GPT-3 / LLaMA 的词表大小(第 7 讲) | 多模态的”图像 token”(附加讲 A4) |
0.3 本讲地图 Map
§1 按词、按字、按子词 ← 代码 01
§2 BPE:训练、编码、解码;WordPiece ← 代码 02
§3 字节级 BPE、SentencePiece 与预分词 ← 代码 03
§4 LLM 分词的实际问题:去重与过滤、代码、数字、多语言 ← 代码 03、05
§5 VOLT:词表多大最好 ← 代码 04
§6 词表共享与多语言 ← 代码 03
§7 嵌入与词表的系统代价;无分词器的 BLT ← 代码 06
§8 小结 §9 常见误区 §10 练习 §11 延伸阅读 §12 下一讲预告
1. 按词、按字、按子词 · Word, character, subword
1.1 分词器在做什么
slides 第 4 页:分词器(tokenizer)把文本切成基本单元(token),每个 token 有一个编号,模型拿编号去嵌入表里查向量(第 6 讲)。分词器是模型的”第一层”,却不参与训练——它在预训练之前就定下来了,之后几乎不可能更换。它决定了:
- 序列长度:同样的文本切成多少个 token,直接就是计算量、上下文占用和 API 的价格;
- 词表大小 V:嵌入表和输出层是 V × d 的矩阵,softmax 要在 V 个类上做(§7);
- 模型看到的世界:数字、代码、各种语言被切成什么样,影响模型学起来的难易。
1.2 按词切
最简单的做法是按空格和标点切词,统计词频建词表(slides 第 5–6 页给了 build_word_dict 的代码);低频词换成 [UNK],数字换成 [number]。01 号脚本把它用在三份语料上:
| 语料 | 词表大小 | 测试集未登录词(OOV)比例 |
|---|---|---|
| 城市语言(第 1 讲,封闭的英语式小语言) | 79 | 0.000 |
| 本课程讲义(中英混排) | 7,359 | 0.480 |
| 本课程的 Python 代码 | 6,299 | 0.418 |
封闭的小语言里按词切刚好合适;真实文本就不行了。slides 第 7–8 页的问题:“什么是一个词?“——所有格(Bob’s)、复合词(do-it-yourself)、缩写(isn’t)、多词表达,以及不用空格的语言:讲义里按空白切出来的最长的”词”是一个 118 个字符的整句。slides 第 9 页总结了词级分词的问题:未登录词(新词如 Covid、拼写变化);词表大小与 OOV 的权衡(词表小,参数少、生成容易,但 OOV 多);中文、日文、韩文、高棉文需要专门的分词工具。
1.3 按字切,以及子词
按字符切(slides 第 10 页):词表很小(英文几十个,中文要几千个常用字),没有 OOV;但序列长得多,每个 token 也没有独立的意义。
子词(slides 第 11 页)介于两者之间:常见的词是一个 token,罕见的词拆成几个片段,最坏退回到单个字符或字节——适中的词表、没有 OOV。01 号脚本第 3 部分在代码语料上对比(序列长度 = 每 1000 个字符切出的 token 数):
| 切法 | 词表大小 | 每 1000 字符的 token | 测试集 OOV |
|---|---|---|---|
| 词级,频率 ≥ 1 | 3,878 | 358 | 0.117 |
| 词级,频率 ≥ 20 | 197 | 358 | 0.209 |
| 字符级 | 904 | 1000 | 0.0023 |
| 字节级 BPE,词表 512 | 512 | 821 | 0 |
| 字节级 BPE,词表 8000 | 8,000 | 459 | 0 |
2. BPE · Byte-Pair Encoding
2.1 训练:反复合并最常见的相邻对
BPE 最早是 1994 年的一个数据压缩算法(Gage),2016 年被 Sennrich 等人用于神经机器翻译。训练过程(slides 第 12 页):
- 词表初始化为所有字符(再加一个词尾符号),统计每个词的频率;
- 重复:数一数语料里所有相邻 token 对的出现次数(按词频加权),把最多的一对合并成一个新 token,加入词表;
- 直到词表达到预定的大小。
02 号脚本逐步复现了 slides 第 13 页的例子(cat 90、catch 50、rat 80、rattle 40):
| 步 | 合并 | 次数 | 当时排前几的对 |
|---|---|---|---|
| 1 | (a, t) → at | 260 | a+t 260、c+a 140、r+a 120 |
| 2 | (c, at) → cat | 140 | c+at 140、r+at 120、at+c 50 |
| 3 | (r, at) → rat | 120 | r+at 120、cat+c 50、c+h 50 |
| 4 | (cat, c) → catc | 50 | cat+c 50、c+h 50、rat+t 40 |
第 4 步 (cat, c) 和 (c, h) 并列,slides 选了 (cat, c)——按”扫描时先遇到”打破平局就能复现。最终词表 a、c、e、h、l、r、t、at、cat、rat、catc,任何由这些字母组成的词都能切开。注意”合并的顺序”本身就是规则:GPT-2 发布的 merges.txt 就是这样一个有序列表。
词尾符号有什么用?02 号脚本第 2 部分:有 </w> 时,第 2 次合并就是 at</w>(cat、rat 结尾的 at),词尾的 at 和词中间的 at 成了两个 token。解码时看到 </w> 就知道词结束了、该放一个空格。GPT-2 之后的字节级 BPE 换了一种做法:把空格当作下一个词的开头(” cat”),效果相同。
在代码里的英文单词上训练(02 号脚本第 3 部分,合并 800 次):最先学到的是 s</w>、t</w>、e</w> 这类”词尾字母”和 in、de、re 这类常见组合;测试集里没见过的词被切成见过的片段,例如 transpose → t | rans | po | se</w>、coarse → co | ar | se</w>。这些片段不一定有语言学意义——BPE 只看频率。
2.2 编码:按合并顺序重放,还是贪心最长前缀?
slides 第 14 页给的编码方式是:先按空格切开,再贪心地取词表里能匹配的最长前缀,剩下的部分继续。BPE 原论文(以及 GPT-2、tiktoken)的做法是按训练时的合并顺序重放合并。两者大多数时候一样,但不总是一样(02 号脚本第 4 部分):430 种测试词里有 48 种(11.2%)切得不同,例如
| 词 | 按合并顺序 | 贪心最长前缀 |
|---|---|---|
| smemarray | s | me | m | array</w> | s | me | mar | ra | y</w> |
| pruned | pr | u | ne | d</w> | pr | un | ed</w> |
总 token 数差不多(7,170 vs 7,159),所以不是哪个”更好”,而是训练和推理必须用同一种方式——否则模型在推理时看到的切分和训练时不同。一个 BPE 分词器 = 词表 + 合并规则;WordPiece(BERT)只有词表,编码就用贪心最长前缀,两套规则各自自洽、不能混用。
2.3 WordPiece
slides 第 30 页:WordPiece 和 BPE 一样从字符出发、逐步合并,但挑选的标准不是”次数最多”,而是让 p(b | a) 相对 p(b) 提升最多的那一对,常写成 count(ab) / (count(a) · count(b))——近似于”合并后语料似然增加最多”。02 号脚本第 5 部分:BPE 的前几次合并是 s</w>、t</w>、e</w>(最常见),WordPiece 是 qu、ck、ock、lock、block……(字母本身少见、但几乎总是一起出现)。
3. 字节级 BPE、SentencePiece 与预分词 · Byte-level BPE and pre-tokenization
3.1 字节级 BPE(BBPE)
slides 第 30 页:把文本看作 UTF-8 字节序列,基础词表就是 256 个字节,任何语言、表情、乱码都能编码,永远没有 OOV(GPT-2、GPT-3、LLaMA-3 都是这样)。代价是 UTF-8 的变长(03 号脚本第 1 部分):ASCII 1 个字节,常用汉字 3 个,表情 4 个。没有被合并的字符就要占好几个 token——“的”在中文讲义训练的分词器里是 1 个 token,在只见过英文的分词器里是 3 个;生僻的”饕”两边都没被合并,就是 3 个字节 token(slides 第 41 页 LLaMA 把它切成 3 个 token,原因相同:LLaMA 的 SentencePiece 对没见过的字符退回到字节)。
在中文讲义上训练字节级 BPE(03 号脚本第 2 部分),最先学到的合并是汉字和全角标点的 UTF-8 前缀(两个字节,如 e4 b8、ef bc),紧接着是完整的高频字(的、个、一)和全角标点——先把字节”拼回”字符,再往上拼成词(线程束、共享内存、矩阵乘法……)。词表里有不少”半个字符”的 token:
| 词表大小 | 合并出的 token | 其中不是完整 UTF-8 的 | 比例 |
|---|---|---|---|
| 512 | 256 | 100 | 39.1% |
| 1000 | 744 | 197 | 26.5% |
| 4000 | 3,744 | 438 | 11.7% |
还会出现”享内存”这样的半个词(“共享内存”的后半截)。这些 token 本身没有意义,模型要学会把它们组合起来。
3.2 SentencePiece
slides 第 30 页:SentencePiece(Kudo & Richardson 2018)把空格当作普通字符(替换成 ▁,U+2581),直接在原始句子上切分,不需要先按空格分词,所以对所有语言一视同仁;解码就是把 ▁ 换回空格。它同时支持 BPE 和 Unigram 两种算法(T5 用 Unigram,LLaMA-1/2 用 BPE,并对没见过的字符做”字节回退”)。
3.3 预分词
字节级 BPE 通常先用一个正则表达式把文本切成块(字母串、数字串、标点串、空白),BPE 只在块内合并(slides 第 31 页)。03 号脚本第 3 部分在代码上各训练一个词表 1000 的 BPE:用 GPT-2 式正则时没有任何”跨类别”的 token;不做预分词(每行一整块)时 13.7% 的新 token 跨了类别,例如 self.、····print(、def·——这些 token 只在特定上下文里有用,还让同一个词在不同上下文里切法不同。slides 的例子 .append 要不要算一个 token,正是由正则的设计决定的(Dagan et al. 2024 专门研究了代码分词器的预分词)。
4. LLM 分词的实际问题 · Practical considerations
4.1 语料去重与过滤
slides 第 29 页列了 LLaMA-3 的数据处理:URL 级去重;文档级用 MinHash 去重;行级每 3000 万篇文档用 64 位 SHA-1 哈希去重(去掉导航菜单、cookie 提示、联系方式这类模板);再过滤一行里 n-gram 重复太多的、“脏词”太多的、token 分布与语料整体差太远(KL 散度大)的文档。
05 号脚本在本课程讲义切成的 245 篇合成”网页”上走了一遍(人为加入重复抓取、转载、小改动的复制、模板行、灌水页):
| 步骤 | 剩下 | 删掉的 |
|---|---|---|
| ① URL 去重 | 225 | 同一 URL 重复抓取 20 |
| ② 整篇哈希 | 195 | 换 URL 的整篇复制 30 |
| ③ MinHash(128 个哈希)+ LSH,估计 Jaccard ≥ 0.8 | 159 | 近似复制 36 / 40 |
| ④ 行级去重(出现在 ≥ 6 篇里的行) | 159 | 300 行模板 |
| ⑤ 字符 3-gram 重复比例 ≥ 0.5 | 143 | 灌水 5,误删原文 11 |
每一步抓一种重复,也都有代价:整篇哈希改一个字就认不出;漏掉的 4 篇近似复制真实 Jaccard 都在 0.83–0.85,是 MinHash 的估计(0.76–0.80)落到了阈值以下;按字符算的重复度过滤误删的 11 篇全是代码块和字符画(大量空格和 ─│ 本来就重复)。真实流水线按词算重复度、对代码单独处理,并人工检查被删的样本来调阈值。
MinHash 为什么能估计相似度?把文档表示成”5 个字符一组”的瓦片集合,Jaccard J = |A ∩ B| / |A ∪ B|。取一个随机哈希函数,两个集合的最小哈希值相同的概率恰好等于 J;用 m 个哈希函数取平均,误差的标准差是 √(J(1−J)/m)。05 号脚本第 2 部分实测与理论吻合(m = 16 / 64 / 128 / 256:0.092 / 0.046 / 0.036 / 0.022)。每篇文档只存 m 个整数。LSH 把 m 个值分成 b 带、每带 r 行,任何一带完全相同就成为候选对,概率 1 − (1 − J^r)^b 是一条 S 形曲线(16 带 × 8 行时 J = 0.5 只有 0.061、J = 0.9 是 1.000),于是只需比较落进同一个桶的文档,工作量与文档数成正比。
为什么要这么费劲?重复的文档会被模型反复”背诵”(记忆、隐私泄露),浪费训练 token;模板行对模型毫无信息量。
4.2 代码
Python 靠缩进表示结构。03 号脚本第 5 部分:如果分词器不能把连续的空格合并成一个 token,纯空白的 token 占 20.5%,整段代码的 token 数是能合并时的 1.17 倍。Codex(Chen et al. 2021)就在 GPT-3 的分词器上加了表示不同长度空白的 token。
4.3 数字
slides 第 32 页:数字怎样切,影响模型做算术的难易。03 号脚本第 4 部分比较三种预分词(各训练一个词表 2000 的 BPE),看 1000–9999 这 9000 个四位数:
| 预分词 | 例:2048 | 例:1234567 | 四位数的切分模式数 |
|---|---|---|---|
| GPT-2 式(数字串整块交给 BPE) | 2 | 048 | 12 | 3 | 4 | 5 | 6 | 7 | 8 |
| LLaMA 式(每位一个) | 2 | 0 | 4 | 8 | 1 | 2 | … | 7 | 1 |
| GPT-4 式(最多三位一组) | 204 | 8 | 12 | 3 | 4 | 56 | 7 | 4 |
GPT-2 式下同一位数字可能落在 token 的任何位置,竖式加法要先弄清楚每个 token 里是几位;LLaMA 式位值对齐、模式唯一,代价是序列更长;GPT-4 式从左边开始三位一组(2048 → 204 | 8),和十进制的位值并不对齐(真实的 cl100k 词表包含全部 1000 个三位数,这里的小词表没有)。xVal(Golkar et al. 2023)走得更远:用一个专门的 [NUM] token 乘以一个连续的数值来表示数。
4.4 多语言词表
slides 第 33–34 页:LLaMA-2 的词表 32K,LLaMA-3.1 扩到 128K——其中 100K 来自 OpenAI 的 tiktoken(从原本的 200K 里取),28K 留给多语言。怎样构造多语言词表?把 176 种语言(约占全部文本的 8%)的文档合在一起训练 BPE;或者每种语言训练同样多的 token 再合并;或者按”平均对数概率”(ALP)给每种语言分配容量(Zheng et al. 2021、Liang et al. 2023)。
03 号脚本第 6 部分用”英语(城市语言)+ 中文(讲义)“的不同配比各训练一个 BPE:
| 词表 | 训练语料(英 / 中) | 英语:token / 字符 | 中文:token / 字符 |
|---|---|---|---|
| 3000 | 英语为主(95% / 5%) | 0.205 | 0.872 |
| 3000 | 平衡 | 0.205 | 0.689 |
| 3000 | 中文为主(5% / 95%) | 0.210 | 0.665 |
| 1000 | 英语为主 | 0.205 | 1.012 |
| 1000 | 中文为主 | 0.398 | 0.899 |
词表的容量是固定的,分给谁谁就省 token;词表太小(1000)时常用汉字都装不下,配比的作用也有限。被”过度切分”的语言同样的内容要多几倍 token:更贵、更慢、上下文更短,模型也更难学(§6)。
5. VOLT:词表多大最好 · How large should the vocabulary be?
5.1 怎样度量一个词表
slides 第 17 页:压缩率(平均每个 token 几个 UTF-8 字节)、归一化的序列长度、归一化熵。04 号脚本第 2 部分(讲义上训练一个 8000 的 BPE,取前 V 个 token):
| 词表 V | 256 | 1000 | 2000 | 4000 | 8000 |
|---|---|---|---|---|---|
| 字节 / token | 1.000 | 2.057 | 2.546 | 2.908 | 3.155 |
| 相对序列长度 | 1.000 | 0.486 | 0.393 | 0.344 | 0.317 |
前几百次合并收益最大:256 → 1000 序列缩短一半,4000 → 8000 只再缩短 8%。
5.2 归一化熵与 MUV
VOLT(Xu et al. 2021,slides 第 19–25 页)用信息论选词表。归一化熵
l_v 是词表里 token 的平均长度——它度量”每个字符承载的不确定性”,越小越好(歧义少、容易生成)。04 号脚本第 1 部分:slides 的第一个例子({a, e, c, t, s})算出 1.374,与 slides 的 1.37 一致;第二个例子({a, aes, cat})按同样的定义是 0.427,而 slides 写的是 0.14——我们没找到能同时得到两个数的口径,可能是笔误;结论不变:用更长的子词,H 更小。
但词表越大,H 总会越小,所以 VOLT 看的是它下降的速度——边际效用
“多加 m 个 token 换来多少信息压缩”。VOLT 发现 MUV 最大的词表往往 BLEU 最好(slides 第 21–22 页:在三分之二的任务上相关,是一个”粗粒度”的指标),并把”找 MUV 最大的词表”放松成一个带熵正则的最优传输问题——把字符”运输”到候选 token 上,用 Sinkhorn 算法求解(slides 第 23–25 页),比”每个大小训练一遍模型”(BPE-Search)快得多。
04 号脚本第 3 部分沿 BPE 的合并顺序算 MUV:在我们的语料上 MUV 单调下降,最大值落在第一个区间(256 → 300,头几次合并把字节拼回汉字),“取 MUV 最大”在这里给不出有用的答案。信息论指标给出了”收益递减”的形状,要定一个具体的大小,还得看模型。
5.3 代理模型:存在一个最好的大小
04 号脚本第 4 部分在每个词表上训练一个插值 bigram 语言模型(第 1 讲的 Witten–Bell 平滑),用 bits / byte(与词表无关,可以直接比较)衡量:
| 词表 | 256 | 1000 | 2000 | 3000 | 4000 | 8000 |
|---|---|---|---|---|---|---|
| unigram bits/byte | 6.527 | 4.374 | 3.811 | 3.624 | 3.530 | 3.411 |
| bigram bits/byte | 4.212 | 3.152 | 3.010 | 2.992 | 2.999 | 3.105 |
unigram 单调变好(token 越长,自带的上下文越多);bigram 是 U 形——词表太大时 token 种类太多,22.8 万字节的训练数据里大多数 bigram 只出现一两次,统计不可靠。这就是词表大小的根本权衡:词表大,序列短、每步的上下文多;词表小,每个 token 见得多、学得准。 最优点取决于数据量和模型容量:数据越多、模型越大,最优词表越大——这也是 LLaMA 从 32K 走到 128K 的统计理由(系统理由见 §7.2)。
6. 词表共享与多语言 · Vocabulary sharing
slides 第 37 页:television 在英、西、法、意、荷、葡、瑞典、芬兰语里写法几乎一样——共享的子词让一种语言学到的东西可以迁移到另一种语言。Yuan et al. (2024)(slides 第 38–42 页)用 1 万句双语平行语料对 LLaMA-7B 做指令微调,只微调嵌入层,看两类效果:被监督的那个翻译方向(双语)和其他所有方向(多语)。语言落进四个象限:
| 象限 | 双语 | 多语 | 例子 |
|---|---|---|---|
| 互惠 Reciprocal | ↑ | ↑ | cs、da、fr、de |
| 利他 Altruistic | ↓ | ↑ | ar、vi、zh、ko |
| 停滞 Stagnant | ↓ | ↓ | km、lo、gu、te |
| 自私 Selfish | ↑ | ↓ | hi |
停滞象限的问题是过度切分:字节级 BPE 给这些语言生成的 token 序列比字符数还长(“饕”是 3 个 token)。改进办法有两个:扩词表(为这些语言加 token)或缩短(去掉多个 token 共有的字节前缀,例如所有这类字符共享的那个前缀字节)。slides 第 42 页用柱状图对比了”全量微调 / 扩词表 / 缩短”三种做法在 en→km、lo、gu、te 上的翻译效果。这和 03 号脚本第 6 部分是同一个故事:词表分给谁,谁就省 token、谁就学得好。
7. 嵌入与词表的系统代价;无分词器 · Systems cost and tokenizer-free models
7.1 嵌入和输出层占多少参数
06 号脚本第 1 部分:
| 模型 | 词表 | d | 嵌入 + 输出层 | 占比 |
|---|---|---|---|---|
| GPT-2 small(共享) | 50,257 | 768 | 38.6M | 31.0% |
| LLaMA-2 7B(不共享) | 32,000 | 4096 | 262.1M | 3.9% |
| LLaMA-3 8B(不共享) | 128,256 | 4096 | 1050.7M | 13.1% |
| LLaMA-3.2 1B(共享) | 128,256 | 2048 | 262.7M | 21.3% |
LLaMA-3 8B 比 LLaMA-2 7B 多出的 1.3B 参数里约 0.8B 来自词表。嵌入表每个 token 只用一行(查表,访存很少);输出层每个 token 都要乘整个 d × V 矩阵。
7.2 每字节的 FLOPs
处理同样多的文本,词表大则序列短(Transformer 主体的计算按 token 数下降),但输出层按 V 变贵。06 号脚本第 2 部分用讲义上训练的 BPE 量出”每字节几个 token”,乘以每 token 的 FLOPs:d = 512、8 层的小模型在词表 8000 时每字节 FLOPs 最小;d = 2048、24 层的模型主体更贵,“把序列变短”更值钱,最优词表移到 16000。模型越大,最优词表越大——这是扩词表的系统理由(Meta 报告 LLaMA-3 的分词器比 LLaMA-2 最多少用 15% 的 token)。
7.3 logits 的显存与分块交叉熵
训练时 logits 是一个 (批 × 序列长度) × V 的矩阵。06 号脚本第 3 部分:LLaMA-3 8B、8 × 8192 个 token,fp32 的 logits 就有 31.3 GiB,反向时 softmax 的梯度还要同样一份——比 8B 模型的 bf16 权重(约 16 GB)还大。交叉熵对每个 token 独立,所以可以分块算:每块算 logits、求损失和梯度、立刻丢掉,与一次算完的结果一致(损失差 9.1e-13)。再进一步是把 softmax 和交叉熵融合成一个内核、梯度 q − p 原地写回(第 10 讲)。
7.4 无分词器:Byte Latent Transformer
slides 第 43 页的 BLT(Pagnoni et al. 2024)干脆不要分词器:一个小的字节级模型算”下一个字节有多难猜”(熵),在熵高的地方切开,把字节分成长度可变的 patch;大的”潜在”Transformer 只在 patch 上运行,再由解码器还原成字节。06 号脚本第 4 部分用一个插值的字节 6-gram 模型代替那个小模型,在讲义上演示:
| 阈值 θ(bits) | 1 | 2 | 3 | 4 | 对照:BPE 8000 |
|---|---|---|---|---|---|
| 平均每个 patch 的字节数 | 1.67 | 2.66 | 7.18 | 30.36 | 3.15 |
θ = 2 时一行讲义的切分:2. |把|一个|表|达式画|成|**|计算|图**,|用|**|拓扑排序|**|安排|计算|顺序|,|并|手|算|反|向——熵低的位置(汉字的后两个字节、常见词组的后半部分、成对的 **)并进前一个 patch,熵高的位置(新词、标点之后)开新 patch。计算量按”文本有多难”动态分配;没有词表,也就没有过度切分、数字切法、多语言分配的问题。代价是多了编码器 / 解码器和动态长度,批处理和 KV 缓存的管理更复杂。
8. 小结 · Summary
- 子词是按词(OOV、没有空格的语言)和按字(序列长、无意义)之间的折中:适中的词表、没有 OOV。
- BPE:反复合并最常见的相邻对;词表 + 合并顺序共同定义分词器。按合并顺序重放与贪心最长前缀在约 1/10 的词上结果不同,训练和推理必须一致。WordPiece 按 count(ab)/(count(a)count(b)) 挑合并;SentencePiece 把空格当字符(▁)、支持 BPE / Unigram。
- 字节级 BPE:256 个字节打底,永无 OOV;代价是没被合并的字符占多个 token(汉字 3 个、表情 4 个),词表里有”半个字符”的 token。预分词正则决定哪些东西能被合并成一个 token。
- 实际问题:去重与过滤(URL、整篇哈希、MinHash + LSH、行级、启发式过滤,每一步都有漏检和误删);代码的空白;数字的切法(逐位 / 三位一组 / 整块);多语言的容量分配与过度切分。
- 词表多大:压缩收益递减;VOLT 用归一化熵的边际效用(MUV)挑选,并用最优传输求解;代理模型显示存在一个最优大小,它随数据量和模型规模增大。
- 系统代价:嵌入和输出层的参数(LLaMA-3 8B 占 13%)、每字节 FLOPs 的 U 形曲线、logits 的显存(几十 GB,需要分块或融合);BLT 用熵分块替代词表。
9. 常见误区 · Common pitfalls
- “分词只是预处理,和系统无关。” token 数就是计算量、上下文长度和价格;词表大小决定嵌入、输出层和 logits 的显存。
- “BPE 学到的是有意义的词素。” 它只看频率,会学出”享内存”这样的半个词。
- “有了词表就能编码。” BPE 还需要合并顺序;用贪心最长前缀编码 BPE 词表,会和训练时的切分不一致。
- “字节级 BPE 对所有语言都公平。” 没有 OOV 不等于效率相同;训练语料里少的语言被过度切分。
- “数字整块交给 BPE 最省 token。” 省 token,但切分模式杂乱、位值不对齐,不利于算术;LLaMA 选择逐位切分。
- “词表越大越好。” 序列变短,但每个 token 的统计更稀疏、输出层更贵;存在一个随模型和数据变化的最优点。
- “MinHash 去重是精确的。” 它是概率估计,阈值附近会漏检;启发式过滤会误删正常内容(代码、字符画)。
- “logits 很小,不用管。” 128K 词表、几万 token 的批,logits 比模型权重还大。
- “不要分词器就要处理很长的字节序列。” BLT 按熵把字节动态分成 patch,主干模型的步数可以比 BPE 还少。
10. 练习 · Exercises
- 对词频 {low: 5, lower: 2, newest: 6, widest: 3}(不加词尾符号),手算 BPE 的前 3 次合并(平局取扫描时先遇到的)。
- 字符串 “LLM 系统🙂” 的 UTF-8 编码有多少个字节?在一个只有 256 个字节 token 的分词器里是多少个 token?
- 举一个例子说明”按合并顺序编码”和”贪心最长前缀”会给出不同结果,并解释原因。
- LLaMA-3 8B:V = 128256、d = 4096、输入嵌入和输出层不共享,总参数约 8.03B。嵌入 + 输出层占多少?
- 批 4 × 4096 个 token、V = 128256,bf16 的 logits 占多少显存?
- 两篇文档的真实 Jaccard 为 0.8,用 128 个哈希函数的 MinHash 估计,误差的标准差约是多少?
- LSH 分成 20 带、每带 5 行。J = 0.8 和 J = 0.5 的文档对成为候选对的概率各是多少?
- 一个词表有 3 个 token:x 出现 50 次(长度 1)、yz 出现 30 次(长度 2)、xyz 出现 20 次(长度 3)。算它的归一化熵(自然对数)。
- “123456789” 用 LLaMA 式(逐位)切成几个 token?用 GPT-4 式(假设每个三位数都是一个 token)呢?
- 为什么字节级 BPE 永远没有未登录词,却仍然会”过度切分”某些语言?
- 一个 d = 1024、12 层的模型(每 token 主体 FLOPs 按 2 · 12 · L · d² 计),词表 2000 时每字节 0.40 个 token,词表 4000 时 0.35 个。哪个词表每字节的总 FLOPs 更少(输出层按 2 · d · V 计)?
- BLT 为什么在”熵高”的位置开新 patch,而不是在”熵低”的位置?
参考答案
- 初始相邻对:e+s 与 s+t 都是 6 + 3 = 9,e+s 先被扫描到 → 合并 es;然后 es+t 是 9 → est;然后 l+o 与 o+w 都是 5 + 2 = 7,l+o 先遇到 → lo(02 号脚本的
naive_bpe给出同样的结果,第 4 次是 low)。 - L、L、M、空格各 1 个字节,“系统”各 3 个,🙂 4 个,共 14 个字节,也就是 14 个 token。
- 02 号脚本里的 smemarray:按合并顺序是 s | me | m | array,贪心最长前缀是 s | me | mar | ra | y。贪心法在 “m” 处看到词表里有更长的 “mar” 就吃掉了,但训练时 “array” 这个合并的优先级更高、会先把后面的 array 合起来;两种规则对”谁先合并”的判断不同。
- 2 × 128256 × 4096 ≈ 1.05B,占 1.05 / 8.03 ≈ 13.1%。
- 4 × 4096 × 128256 × 2 字节 ≈ 4.2 GB(3.9 GiB)。
- √(0.8 × 0.2 / 128) ≈ 0.035。
- J = 0.8:1 − (1 − 0.8⁵)²⁰ = 1 − (0.672)²⁰ ≈ 0.9996;J = 0.5:1 − (1 − 0.5⁵)²⁰ = 1 − (0.969)²⁰ ≈ 0.47。
- P = 0.5、0.3、0.2,熵 = −(0.5 ln 0.5 + 0.3 ln 0.3 + 0.2 ln 0.2) ≈ 0.347 + 0.361 + 0.322 = 1.030;l_v = (1 + 2 + 3) / 3 = 2;归一化熵 ≈ 0.515。
- 逐位:9 个;三位一组:123 | 456 | 789,3 个。
- 任何字节都在基础词表里,所以任何文本都能编码(没有 OOV);但一种语言的字符如果在训练语料里少见,就不会被合并成多字节 token,每个字符要用 2–4 个字节 token 表示,同样的内容比英语多几倍 token(过度切分)。
- 主体:2 × 12 × 12 × 1024² ≈ 302.0M FLOPs / token。词表 2000:0.40 × (302.0M + 2 × 1024 × 2000 = 4.1M) ≈ 122.4M / 字节;词表 4000:0.35 × (302.0M + 8.2M) ≈ 108.6M / 字节。4000 更省。
- 熵低的位置说明下一个字节几乎可以确定(例如汉字的后续字节、常见词的后半部分),把它并进当前 patch 几乎不丢信息、也不需要大模型多花一步;熵高的位置是”真正需要思考”的地方(新词、新句子的开头),在这里开新 patch,让大模型把计算花在难的地方。
11. 延伸阅读 · Further reading
- Sennrich, Haddow & Birch (2016) Neural Machine Translation of Rare Words with Subword Units(ACL):BPE 用于 NMT 的原始论文,附有十几行的 Python 参考实现。
- Kudo & Richardson (2018) SentencePiece;Kudo (2018) Subword Regularization(Unigram 语言模型分词)。
- Xu et al. (2021) Vocabulary Learning via Optimal Transport for Neural Machine Translation(VOLT,ACL 最佳论文)。
- Radford et al. (2019) GPT-2 技术报告(字节级 BPE 与正则预分词);OpenAI 的 tiktoken 开源代码。
- Dagan, Synnaeve & Rozière (2024) Getting the Most Out of Your Tokenizer for Pre-training and Domain Adaptation(ICML):代码分词器的预分词与词表大小。
- Golkar et al. (2023) xVal: A Continuous Number Encoding for Large Language Models。
- Yuan et al. (2024) How Vocabulary Sharing Facilitates Multilingualism in LLaMA?(ACL);Liang et al. (2023) XLM-V;Zheng et al. (2021) Allocating Large Vocabulary Capacity for Cross-lingual Language Model Pre-training。
- Broder (1997) On the Resemblance and Containment of Documents(MinHash);Lee et al. (2022) Deduplicating Training Data Makes Language Models Better(ACL)。
- Pagnoni et al. (2024) Byte Latent Transformer: Patches Scale Better Than Tokens。
12. 下一讲预告 · Next lecture
分词器把文本变成 token,模型给出下一个 token 的分布——可是”给出分布”和”生成一段文本”之间还隔着一步:解码。第 9 讲(生成与推测解码)从”怎样从分布里选 token”讲起:贪心、采样(以及几种离散采样算法和 Gumbel-max 技巧)、温度、top-k / top-p、束搜索。然后回到系统问题:自回归解码为什么慢(每一步都要把全部权重读一遍)?推测解码怎样用一个小的草稿模型一次猜好几个 token、再让大模型一次前向验证,并且严格保持输出分布不变?EAGLE 又怎样在特征层面做草稿、用树形注意力一次验证多条候选?我们会用城市语言的真实分布精确检验”分布不变”这一条。