对应 Maps to:CMU 10-423 / 10-623 / 10-723 Generative AI(Spring 2026;Aran Nayebi、Matt Gormley),Lecture 3 · Wed 21-Jan · Learning LLMs / Decoding。 阅读材料:Graves (2014), Generating Sequences With Recurrent Neural Networks;Mikolov et al. (2010), Recurrent neural network based language model;Radford et al. (2018), Improving Language Understanding by Generative Pre-Training(GPT-1);Radford et al. (2019), Language Models are Unsupervised Multitask Learners(GPT-2)。 依据说明:本讲以原课第 3 讲公开的 slides(lecture3-llms.pdf)为主线:回顾(模块化自动微分、两种画法、RNN-LM、Transformer-LM、采样)、机器学习的配方、反向传播、SGD 与小批量 SGD、用 RNN 做序列标注、深度语言模型的最大似然、批处理(填充与截断)、KV 缓存、分词、贪心解码与祖先采样。slides 在祖先采样处结束;束搜索、温度、top-k、top-p 和 GPT-1/GPT-2 两节(§6.4–6.6、§7)是本讲义根据阅读材料和原始论文补充的。全部实验是本讲义自己设计的,不代表课堂上讲了什么。 配套代码 Code:
code/lecture03/(6 个脚本 + 4 个模块;04只用标准库,其余用 PyTorch CPU 版;每个脚本 1–20 秒) 预计阅读时间:3.5–4.5 小时(含运行代码)
0. 本讲导航 · How to read this lecture
0.1 学习目标 Learning goals
学完这一讲,你应该能够:
- 写出深度语言模型的训练目标(训练集的对数似然),说清它和 n-gram”数数”的关系,以及为什么需要 START/END。
- 解释小批量 SGD:为什么用小批量、批大小怎样影响梯度噪声和每个样本的计算成本。
- 把不等长的句子组成批:截断、填充、损失掩码;知道右填充和左填充的区别、注意力掩码和位置编号的作用;比较填充、分桶和打包。
- 解释 KV 缓存:为什么 K、V 可以缓存而 Q 不需要;它节省了多少计算、占多少内存;为什么它让 Transformer 的”状态”随长度增长。
- 比较以词、字符、子词为单位的分词;会手工执行 BPE 的训练和编码,知道字节级 BPE 为什么没有 OOV。
- 区分搜索(贪心、束搜索)和采样(祖先采样、温度、top-k、top-p),说出它们各自的失败方式,并解释”最大化概率”为什么会导致平淡和重复。
- 概括 GPT-1 的”生成式预训练 + 判别式微调”和 GPT-2 的零样本思路。
0.2 先修与本讲范围 Prerequisites & scope
| 默认你已经会 Assumed | 本讲只给直觉,后面再展开 Previewed only |
|---|---|
| 第 1 讲:n-gram、RNN-LM、困惑度、自动微分 | 预训练与微调的系统讨论(第 4 讲) |
| 第 2 讲:注意力、因果掩码、GPT 的结构 | 推测解码等高效解码(第 18 讲) |
| 概率:条件概率、期望、方差 | 指令微调、RLHF(第 11 讲) |
| 前置讲 P1 §5 的 KV 缓存公式(不读也行) | 长上下文与 KV 缓存的压缩(第 19 讲) |
0.3 本讲地图 Map
§1 回顾:机器学习的配方、反向传播、SGD;用 RNN 做序列标注
§2 深度语言模型的最大似然:数数 vs 梯度;END 那一列;小批量的噪声 ← 代码 02
§3 批处理:截断、填充、损失掩码、左右填充、分桶与打包 ← 代码 01
§4 KV 缓存:存什么、省多少、占多少 ← 代码 03
§5 分词:词、字符、子词;BPE;字节级 BPE ← 代码 04
§6 解码:贪心、束搜索、祖先采样、温度、top-k、top-p;复读 ← 代码 05、06
§7 GPT-1 与 GPT-2:生成式预训练 (补充)
§8–12 小结、误区、练习、延伸阅读、下一讲
0.4 记号 Notation
沿用前两讲。新增: 批大小; 块大小(block size,即一个训练序列的最大长度); 温度; 束宽; 训练集,每个 是一个句子(token 序列)。
0.5 本讲的实验模型
除了 02 第 1 部分、04 和 06,所有实验都直接读取第 2 讲 06 号脚本缓存的农场语言 GPT(2 层、4 头、,测试困惑度 2.5305,真实下界 2.4913)。农场语言的好处这一讲会特别明显:decoding.farm_true_logprob 能精确算出任意一句话在真实分布下的概率(对训练集的 2 万句,它和生成时记录的概率逐位相同),所以解码策略的好坏可以客观地衡量,而不只是”看起来通不通顺”。
1. 回顾:怎样训练一个神经网络 · Recap: learning a neural network
1.1 到目前为止
slides 把前两讲分成两条线:
- 深度学习:自动微分是计算可微函数梯度的工具,核心构件是有
forward()和backward()的模块;计算图是定义函数的另一种方式。我们见过两张很深的计算图:RNN-LM 和 Transformer-LM。 - 语言建模:关键想法是”以前面的词为条件,采样下一个词”。n-gram 用一大堆 5 万面的骰子定义下一个词的分布,RNN-LM 和 Transformer-LM 用神经网络。n-gram 很容易学——数共现次数就行;**但我们还没说过怎样学 RNN-LM 或 Transformer-LM。**这一讲先回答这个问题。
1.2 机器学习的配方
slides 的四步配方:
- 给定训练数据 ;
- 选择决策函数 和损失函数 ;
- 定义目标:;
- 用 SGD 训练:朝梯度的反方向走一小步,。
梯度由反向传播(第 1 讲 §5)计算——slides 特别强调”整个 Backward 那一列现在都由自动微分替我们算了”。
1.3 SGD 与小批量 SGD
slides 并排写了两个算法:
- SGD:每个 epoch 把训练样本打乱,一次取一个样本 ,算它的梯度,更新参数。
- 小批量 SGD:把样本随机分成 个一组的小批量 ,每次用小批量的平均梯度 更新。
为什么要用小批量?两个原因,02 第 3 部分都测了(随机初始化的小 GPT,“全量梯度”用 4096 句计算):
| 批大小 | 和全量梯度的余弦相似度 | ⏱ 每个样本的耗时 | |
|---|---|---|---|
| 1 | 25.018 | 0.584 | 0.58 ms |
| 4 | 5.136 | 0.843 | 0.24 ms |
| 16 | 1.447 | 0.946 | 0.13 ms |
| 64 | 0.362 | 0.986 | 0.11 ms |
| 256 | 0.088 | 0.996 | 0.11 ms |
- 噪声更小:随机抽取的小批量梯度(近似)是全量梯度的无偏估计,误差的平方大约按 下降( 从 1 到 64 缩小到约 1/69)。
- 每个样本更便宜:把很多小的矩阵-向量乘法合成大的矩阵乘法,算术强度更高(前置讲 P3 §3.4)。在 GPU 上这个差别会大得多。
代价是每一步更贵。批大小是”每一步走得多准”和”每一步花多少计算”之间的权衡;前置讲 P4 §5 的数据并行,本质上就是在多块卡上凑一个更大的批。
1.4 用 RNN 做序列标注
在讲语言模型之前,slides 先回顾了一个更简单的有监督任务:序列标注(sequence tagging)。每个训练样本是一对等长的序列 ,要为每个输入位置预测一个标签:
- 词性标注:time flies like an arrow → n v p d n。同一句话可以有不同的正确标注(n n v d n:“时间苍蝇”喜欢一支箭),这正是需要上下文的原因。
- 手写识别:一串字母图像 → 字母序列(u n e x p e c t e d)。
- 音素识别:一段语音的声学特征 → 音素序列(TIMIT 数据集)。
RNN 做序列标注:,,每个位置的损失是交叉熵 ,总损失 。
slides 问:“在真正的实现里(比如 RNN-LM 或 Transformer-LM),这些步骤里哪些可以为了效率跳过?“两个常见的答案:
- 不必显式算出 softmax 的概率向量 再取对数。交叉熵只需要目标那一项的 概率,而 可以直接从 logits 算出来,既省计算又更稳定(前置讲 P1 §3 的稳定 softmax)。PyTorch 的
F.cross_entropy接收的就是 logits。 - 不必把目标写成 one-hot 向量 再做内积,直接用目标的整数编号去”取”那一项。
slides 还问:“这些参数张量是从哪来的?“——从随机初始化开始,然后用梯度下降学出来。这正是下一节的内容。
2. 深度语言模型的最大似然 · MLE for deep neural LMs
2.1 语言建模的数据集
语言建模的数据集只有输入、没有标签:,每个样本是一个句子(time flies like an arrow;the cows eat grass;birds fly fast……)。“标签”就是句子自己:每个位置的目标是下一个词。这就是为什么语言模型的训练叫自监督(self-supervised)——不需要人工标注,互联网上的任何文本都能用。
2.2 n-gram:数数就是最大似然
slides 的例子:要估计 ,就去语料里数”cows eat”后面跟着什么——corn 4 次、grass 3 次、hay 2 次、if 1 次、which 1 次,共 11 次,于是 。第 1 讲 §3.2 推导过:写出句子在 n-gram 模型下的似然、加上”概率和为 1”的约束、令梯度为零,解出来正好是这个计数比例。
2.3 深度语言模型:同样的目标,没有闭式解
slides:“我们也可以用最大似然估计来学习 RNN-LM 或 Transformer-LM 的参数——但不是闭式解,而是按另一个配方:写出句子在深度语言模型下的似然;用自动微分计算(小批量)似然对参数的梯度;沿负梯度方向走(小批量 SGD,或你喜欢的优化器)。”
具体地,一个句子的对数似然按链式法则拆成每个位置的和:
其中 由 算出(RNN 的隐藏状态,或 Transformer 最后一层在第 个位置的输出)。训练就是最大化 ——等价于最小化每个 token 的平均交叉熵。slides 最后说:“训练 Transformer-LM 完全一样,只是把深度语言模型换成另一个。”
同一个模型族时,梯度下降会走到数数的答案吗?02 第 2 部分做了一个最简单的检验:一个”神经 bigram”模型——参数就是一张 的 logits 表,——在农场语言的 232,011 个 bigram 上用 Adam 训练:
| Adam 步数 | 1 | 10 | 100 | 500 | 1500 |
|---|---|---|---|---|---|
| 平均负对数似然 | 4.5067 | 3.1955 | 1.3474 | 1.3330 | 1.3315 |
数数得到的最大似然解是 1.331175。梯度下降从上方逼近它,永远不会更低;1500 步后,出现过的 bigram 上两者的概率最大只差 。差别在没见过的组合上:数数给 0,神经网络给一个越训练越小、但不为 0 的概率(softmax 的输出永远是正的)。更深的模型还会让相似的上下文共享参数——这才是神经语言模型比 n-gram 强的地方(第 1 讲 §4.7)。
2.4 START 和 END:为什么概率加起来是 1
slides 在 RNN-LM 的计算图上加了一列,然后问:“如果去掉这整一列,我们定义的是什么概率分布?“这里说的是预测 END(</s>)的那一步。02 第 1 部分用一个只有 {a, b} 两个词的小 GPT 枚举了所有句子:
| 长度 | 有 END:长度 ≤ 的累计概率 | 无 END:长度恰为 的总概率 | 无 END:长度 ≤ 的累计 |
|---|---|---|---|
| 0 | 0.0110 | 1.0000 | 1 |
| 2 | 0.5588 | 1.0000 | 3 |
| 8 | 0.8736 | 1.0000 | 9 |
| 12 | 0.9250 | 1.0000 | 13 |
- 有 END 时,所有长度的句子的概率加起来趋于 1:模型定义的是一个在”任意长度的有限句子”上的分布,长句和短句可以直接比较。
- 去掉 END 后,每个固定长度的句子概率之和都是 1:模型定义的是一族分布,每个长度一个,全部加起来是 。它不知道句子该在哪里结束,生成时也就不知道何时停。
- 同理,START(
<s>)让第一个词也有条件分布 ;没有它,模型只能定义 。
slides 的另一个提示:state-of-the-art 的语言模型今天都是 Transformer,但最早的神经语言模型大多是 RNN-LM(Mikolov et al., 2010),它们为今天的结构铺了路。
3. 批处理:填充、截断与掩码 · Batching, padding & truncation
3.1 为什么效率这么重要
slides 用 GPT-3 作案例:训练数据约 5000 亿 token(论文里数据集的总量;实际训练了 3000 亿个 token)、1750 亿个参数。slides 写的计算量是”50 petaflop/s-days”;按 GPT-3 论文附录 D 的表,175B 模型的训练计算量是 FLOPs,约 3640 petaflop/s-days(1 petaflop/s-day = FLOPs),用 前置讲 P3 §6.4 的 也能核对。我们按论文的数字。
在这个规模上,任何浪费都要乘以几千个 GPU 天。slides 列出了 Transformer 能高效训练的几个原因:
- 批处理:一次处理 个句子,每个句子的计算完全相同,可以简单地并行;
- 缩放点积注意力:一个时间步的注意力分数不依赖其他时间步,可以并行(第 2 讲 §3.6);
- 多头注意力:每个头独立计算,更多并行;
- 矩阵乘法:注意力的核心是矩阵乘法,GPU 和 TPU 这类专门硬件让它非常快;
- 模型并行:模型太大时,把它切到多个 GPU 或多台机器上(第 17 讲);
- KV 缓存:键和值在许多时间步之间被重复使用,但查询、相似度分数和注意力权重不需要缓存(§4)。
这一节讲第一条:怎样把句子组成批。
3.2 slides 的例子:截断、填充、查词表、查嵌入
slides 取 8 个训练句子,块大小(block size,最大序列长度)设为 10。组成一个批之前要做四件事:
- 把太长的句子截断;
- 把太短的句子填充(pad);
- 用一张查找表(词表)把每个 token 变成整数;
- 把每个整数变成一个固定长度的嵌入向量。
01_batching_padding.py 第 1 部分逐字复现了 slides 的编号矩阵:
| 句子 | 原长度 | 截断 + 填充后的编号 | 被截掉的词 |
|---|---|---|---|
| In the hole in the ground there lived a hobbit | 10 | 2 41 17 19 41 13 42 23 6 16 | — |
| It was the best of times it was the worst of times | 12 | 3 50 41 9 30 46 21 50 41 55 | of times |
| Even miracles take a little time | 6 | 1 25 39 6 22 45 0 0 0 0 | — |
| The important thing is to never stop questioning | 8 | 4 18 43 20 47 27 37 33 0 0 | — |
(只列了 4 句。)词表有 57 个条目:<PAD> 编号 0,其余按 Python 的字符串顺序排,大写字母排在小写前面,所以 ‘Even’、‘In’、‘It’、‘The’ 排在最前。8 句里 3 句被截断、3 句被填充,共 7 个 <PAD>,占 9% 的位置。查嵌入之后得到形状 (8, 10, 4) 的张量;padding_idx=0 让 <PAD> 的嵌入固定为零、不接收梯度。
块大小就是模型的最长上下文:截断掉的词模型永远看不到。填充的位置则必须在两个地方被排除——损失和注意力。
3.3 损失掩码
01 第 2 部分把验证集前 256 句组成一个批(形状 (256, 19),真实 token 2,967 个,填充 1,897 个):
| 损失的算法 | 平均交叉熵 | 计入的位置数 |
|---|---|---|
忽略填充(ignore_index=-100) | 0.9297 | 2,967 |
把填充当成要预测的 <pad> | 5.1045 | 4,864 |
不排除填充,损失就被这些毫无意义的位置主导了;如果训练时也不排除,模型会把力气花在”句子结束后全是 <pad>”这种无用的事上。第 1 讲的 make_batches 已经这样做了:填充位置的目标设为 −100,F.cross_entropy 会跳过它们。
**按 token 平均还是按句子平均?**同一个批,按 token 平均是 0.9297,先在每句内平均、再对句子平均是 0.8876——按 token 平均时长句子权重更大。slides 的目标函数 是对所有 token 求和,按 token 平均只差一个常数因子,所以 LLM 的训练损失和困惑度都按 token 计(练习 2)。
3.4 右填充还是左填充?注意力掩码与位置编号
填充放在哪一边,对因果 Transformer 有很大区别。01 第 3 部分把短句 “the hens eat corn every day .”(8 个 token)和一个 19 个 token 的长句放进一个批,比较短句在批里和单独计算时的 logits(float64):
| 做法 | max|logits 差| |
|---|---|
| 右填充,直接用模型 | |
| 左填充,直接用模型 | 11 |
| 左填充 + 位置从真实 token 起算(不屏蔽填充) | 6.3 |
| 左填充 + 位置从真实 token 起算 + 屏蔽填充的键 |
- 右填充对因果模型是”免费”的:填充在所有真实 token 的右边,因果掩码本来就不让真实 token 看它们。
- 左填充时,真实 token 能看到左边的
<pad>,而且位置编号整体后移了。必须同时做两件事:在注意力里屏蔽填充的键(key padding mask),并让位置编号从第一个真实 token 开始数。
为什么还要用左填充?批量生成时,左填充让所有序列的最后一个 token 对齐在同一列,下一步可以整批一起接上新 token(§4 的 KV 缓存、第 18 讲的批量推理)。
3.5 浪费了多少?分桶与打包
01 第 4 部分统计了农场语言训练集(2 万句、长度 8–19、共 232,011 个真实位置)在不同组批方式下的浪费:
| 做法(批大小 32) | 实际计算的位置数 | 浪费在填充上的比例 |
|---|---|---|
| 随机分批 | 365,792 | 36.6% |
| 按长度排序后分批(分桶) | 232,192 | 0.1% |
| 打包成 64 个 token 的块 | 232,000 | 0.0% |
- 分桶(bucketing)几乎消除了浪费,但同一批里都是长度相近的句子,批与批之间不再是同分布的随机样本。常见的折中是”桶内随机、桶间打乱”。
- 打包(packing)是 GPT 系列预训练的做法:把文档首尾相接、用分隔符(GPT-2 的
<|endoftext|>)隔开,再切成定长的块,几乎没有浪费。代价是一个块里有好几篇不相关的文档——在我们的设置里,100% 的块中间都有句子边界——注意力会跨过边界去看上一篇。Llama 3 在块内再加一个”文档内掩码”,只许看同一篇文档(第 4 讲 slides 的 intra-document masking)。
4. KV 缓存 · The KV cache
4.1 生成时的浪费
训练时一次前向就能算出所有位置的预测(第 2 讲 §4.8)。生成时却不行:第 个 token 要等第 个生成出来才能算。最朴素的做法是每生成一个 token,就把整个前缀重新喂一遍模型——第 2 讲的 GPT.forward 就只能这样用。
可是前面那些位置的计算结果根本不会变。因果掩码保证第 个位置的 、 只取决于前 个 token;后面来了新 token,它们也不会改变。slides 的总结:
生成的每一步,我们要重用所有之前的键和值(所以要把它们缓存起来),但可以丢掉查询、相似度分数和注意力权重。KV 缓存:每一步为新 token 算出新的 和 ,把它们存起来供之后的时间步使用。
4.2 实现
kv_cache.forward_cached(model, ids, cache) 对每一层:
- 只为新的 token 算 ;
- 把新的 接到缓存后面:,;
- 新 token 的查询对全部 做注意力(新 token 之间仍要遵守因果性);
- 前馈层、残差、层归一化都是逐位置的,只对新 token 算。
第一次调用把整个提示一次喂进去,叫预填充(prefill);之后每次只喂 1 个新 token,叫解码(decode)。前置讲 P3 §6 讲过两个阶段的性能特征完全不同:预填充是大矩阵乘法(计算受限),解码是矩阵-向量乘法(访存受限)。
缓存的形状是每层一对 ,各 ——这正是第 2 讲 GPT 接口 logits, state = model(ids) 里那个空着的 state 该装的东西。
正确性。03_kv_cache.py 第 1 部分:对一个 19 个 token 的句子,不管一次喂 1 个、先喂 5 个再一个一个、还是每次 3 个,带缓存的结果都和整段前向逐位一致(float64 下差 量级);float32 下贪心生成 30 个 token,有缓存和无缓存生成的序列完全相同。
4.3 缓存里有什么
农场 GPT 有 2 层、每层 4 个头、每头 。03 第 2 部分打印了缓存的增长:预填充 4 个 token 后,第 1 层 的形状是 (1, 4, 4, 16),缓存共 4,096 字节;之后每解码一步,长度加 1、增加 1,024 字节。公式是
为什么不缓存 ?第 步之后,旧的查询再也不会被用到——注意力只需要”新查询 × 所有旧键”。分数和权重也只为当前这一步服务。
4.4 省了多少计算
不带缓存时,生成第 个 token 要把前 个 token 全部重算一遍,生成 个 token 的总计算量随 增长;带缓存时每一步只算 1 个 token,总量随 线性增长(注意力那一项每步 ,仍随 增长,但远小于重算整个前缀)。03 第 3 部分按公式估算农场 GPT(提示 4 个 token):
| 生成 个 | 无缓存 | 有缓存 | 倍数 |
|---|---|---|---|
| 8 | 12.5M FLOPs | 2.28M | 5.5× |
| 30 | 119.5M | 7.01M | 17.0× |
| 60 | 453.3M | 13.87M | 32.7× |
⏱ 实测(批大小 32、生成 56 个 token):无缓存 113 ms,有缓存 20 ms,快 5.8 倍——比公式小,因为这个模型非常小,每一步的耗时主要是 Python 和 PyTorch 的固定开销(前置讲 P3 §5.3),而不是 FLOPs。模型越大,实测越接近公式。
4.5 缓存有多大:RNN 的状态 vs Transformer 的缓存
03 第 4 部分比较了每个序列需要保存的”状态”:
| 上下文长度 | 第 1 讲 RNN(H=64) | 农场 GPT 的 KV 缓存(按公式外推) | Llama-3 8B 的 KV 缓存(BF16) | 占 Llama-3 8B 权重的比例 |
|---|---|---|---|---|
| 16 | 256 B | 16.0 KiB | 2.0 MiB | 0% |
| 1,024 | 256 B | 1.0 MiB | 128.0 MiB | 1% |
| 8,192 | 256 B | 8.0 MiB | 1.0 GiB | 7% |
| 131,072 | 256 B | 128.0 MiB | 16.0 GiB | 107% |
- RNN 的状态大小固定:它被迫把历史压缩进一个向量(第 2 讲”遗忘”的根源)。
- Transformer 的 KV 缓存把每个 token 的 、 原样保留:不压缩,所以随时能”回头看”任何位置,代价是内存随长度线性增长。Llama-3 8B 在 128K 上下文时,一个序列的缓存已经比权重还大。
这是第 4 讲 GQA(减少 K、V 的头数——Llama-3 8B 已经在用,否则上表的数字还要乘以 4)、第 18 讲推理优化、第 21 讲状态空间模型(回到固定大小的状态)共同面对的问题。
5. 分词 · Tokenization
5.1 token 应该是什么?
到目前为止我们的”token”都是词(农场语言只有 53 个词)。真实文本该怎样切?slides(改编自 Henry Chai 的课件)比较了三种做法。
以词为单位:输入 “Matt is giving a lecture on transformers”,输出 [matt, is, giving, a, lecture, on, transformers]。缺点:
- 很难在词表大小和计算量之间取舍(每种词形都要一个条目);
- 相似的词(transformers 和 transformer)会被映射到完全不同的表示;
- 拼写错误通常是词表外的词(out-of-vocabulary, OOV):“Matt is givin’ a lectrue on transformers” → [matt, is, , a, , on, transformers]。
以字符为单位:词表小得多,但很多语义信息丢失了;序列比以词为单位长得多,可能带来计算问题;对汉字这样的表意文字效果不错。
以子词为单位:把长的或罕见的词切成更小的、有意义的片段(子词);没有 OOV——任何不是子词的 token 都能用其他子词拼出来(所有字符永远都在子词表里)。学习子词切分的算法有 BPE、WordPiece、SentencePiece。
04_tokenization.py 第 1 部分在一个玩具英语语料上验证了前两种做法的问题:以词为单位时,拼写错误的两个词变成 <OOV>;以字符为单位时,序列长度是以词为单位的 4.84 倍。
5.2 一个带”词形变化”的玩具语料
农场语言没有词形变化,用来演示子词不合适。bpe.make_morph_corpus 生成了一门有规则词缀的玩具英语(2 万句、298 种词):动词加 -s、-ed、-ing,名词加 -s,动词加 -er 变成”做这件事的人”(farmer、painter),形容词加 -ly、-er、-est、-ness,前缀 un-、re-。另外留出了 16 个词形(repainting、unkindly、climbers、softest……):它们的词根和词缀在训练语料里都出现过很多次,但这个组合一次都没出现。
5.3 BPE 算法
字节对编码(Byte-Pair Encoding)原本是一种数据压缩算法,Sennrich, Haddow & Birch (2016) 把它用于分词:
- 把每个词拆成字符,末尾加一个词尾标记
</w>(这样词尾的ing</w>和词中间的ing是不同的 token); - 统计语料里所有相邻 token 对出现的次数;
- 把出现最多的那一对合并成一个新 token,记下这条合并规则;
- 重复 2–3,直到做够指定的合并次数。
词表 = 所有字符 + 每次合并产生的新 token。编码一个新词时,从字符开始,按学到的先后顺序反复应用合并规则(练习 5 手工做一遍)。
04 第 2 部分打印了前 24 次合并:先是最常见的字母组合和最常见的词(e</w>、th、the</w>、s</w>、er……),紧接着就是规则的词尾——s</w> 第 4 次、ers</w> 第 15 次、ly</w> 第 18 次、ing</w> 第 19 次、ed</w> 第 22 次、est</w> 第 59 次、ness</w> 第 90 次合并时成为完整的 token。BPE 没有任何语言学知识,只是因为这些词缀出现在很多不同的词后面,字母组合的总频次很高。
合并次数 = 词表大小与序列长度的取舍:
| 合并次数 | 词表大小 | 平均每个词几个 token | 整个词是一个 token 的比例 |
|---|---|---|---|
| 0(字符级) | 26 | 4.978 | 0.0% |
| 50 | 76 | 2.433 | 49.5% |
| 200 | 226 | 1.357 | 72.7% |
| 400 | 426 | 1.036 | 96.6% |
这份语料只有 298 种词,合并 400 次后几乎每个词都成了一个 token——回到了以词为单位。真实模型取中间:GPT-2 合并了 50,000 次(词表 50,257),Llama 3 的词表约 128K。
5.4 没见过的词形:切得对吗?
04 第 3 部分(”_” 表示词尾 </w>):
| 词 | 语素切分 | 合并 100 次 | 合并 200 次 | 合并 400 次 |
|---|---|---|---|---|
| repainting | re|paint|ing | re paint ing_ | re paint ing_ | re painting_ |
| unkindly | un|kind|ly | un k ind ly_ | un kind ly_ | un kindly_ |
| climbers | climb|er|s | c li m b ers_ | climb ers_ | climb ers_ |
| softest | soft|est | s of t est_ | sof t est_ | sof test_ |
| visitors | visit|or|s | v i s i t or s_ | visit or s_ | visit or s_ |
| 合并次数 | 切分位置的精确率 | 召回率 |
|---|---|---|
| 100 | 0.45 | 0.88 |
| 200 | 0.81 | 0.85 |
| 400 | 0.82 | 0.54 |
- 16 个留出词形以词为单位全是 OOV;BPE 下没有一个是 OOV——总能退回到更小的片段,最坏退回到字符。
- 合并 200 次时,很多切分和语素惊人地一致:模型可以从 “paint” 和 “ing” 的已有知识推断 “repainting” 的意思。
- 但 BPE 的依据只是频次:soft 很少单独出现,softest 被切成 “sof t est_“,再多合并就变成 “sof test_“;合并更多次后,常见的整词(painting、kindly)吞掉了语素边界,召回率从 0.85 降到 0.54。
- visitors 被切对了,但只是因为 “or” 这个字母组合在 work、short 里很常见,并不是 BPE 认识 -or 这个词缀。
用合并 400 次的 BPE 切 slides 那句话(它的词大多不在玩具语料里):“matt is giving a lecture on transformers” 变成 28 个 token(m at t_ is_ g i v ing_ ……),而玩具语料本身平均每个词只要 1.036 个 token。和训练语料差别很大的文本会被切得很碎——同一个模型处理不同语言、不同领域时,“每个词几个 token”差别很大。拼写错误的 lectrue 被切成 “l e c true_“(true 来自语料里的 untrue):不再是 OOV,而且解码可以原样还原(BPE 是无损的)。
5.5 字节级 BPE
以字符为基本单位的 BPE 仍然有一个漏洞:训练时没见过的字符(汉字、带重音的字母、表情符号)不在基本词表里。04 第 5 部分:
| 文本 | 字符数 | UTF-8 字节数 | 字节 |
|---|---|---|---|
| naïve | 5 | 6 | 6E 61 C3 AF 76 65 |
| 漢字 | 2 | 6 | E6 BC A2 E5 AD 97 |
| 生成式 | 3 | 9 | E7 94 9F E6 88 90 E5 BC 8F |
| 🐄 | 1 | 4 | F0 9F 90 84 |
GPT-2 起的做法是字节级 BPE:先把文本变成 UTF-8 字节,基本单位只有 256 个,任何文本都能表示,彻底没有 OOV;再在字节序列上学合并。GPT-2 的词表 50,257 = 256 个字节 + 50,000 次合并 + 1 个 <|endoftext|>。代价是:一个汉字占 3 个字节,如果合并规则里很少有中文,同样的意思用中文写可能要多花好几倍的 token——也就多花好几倍的计算和 API 费用。
(GPT-2 的预分词还会把标点、数字、空格单独处理,并把空格并进下一个词的开头;SentencePiece 则直接在原始文本上学,不需要预先按空格切分。细节不影响 BPE 的核心思想。)
6. 解码 · Decoding
6.1 搜索还是采样?
语言模型给出的是每一步”下一个 token”的分布 。生成一段文字时,怎样从这些分布里挑?有两类做法:
- 搜索:找概率最高的那句话 ——贪心、束搜索;
- 采样:按概率随机抽一句话 ——祖先采样,以及改变分布形状的温度、top-k、top-p。
在农场语言上,我们知道真实分布,所以每种方法的结果都能和”真相”比较。
6.2 贪心搜索
slides 先介绍一般的贪心搜索:搜索空间是带权重的节点和边,目标是找从根到叶子的总权重最小的路径;贪心在每个节点选(眼前)权重最小的边。它是一种启发式方法,不一定找到最好的路径,计算时间和最长路径的长度成正比。
用到语言模型上:节点是部分句子,边的权重是负对数概率,目标是找负对数概率最小(概率最大)的完整句子。贪心解码每一步选概率最大的 token。
05_decoding.py 第 1 部分看 “the dog” 之后的第一步:
| 下一个词 | 模型概率 | 真实概率 |
|---|---|---|
| behind | 0.187 | 0.183 |
| beside | 0.180 | 0.183 |
| near | 0.148 | 0.183 |
| eats | 0.141 | 0.113 |
| hears | 0.133 | 0.075 |
| chases | 0.102 | 0.188 |
| makes | 0.098 | 0.075 |
模型选了 behind;按真实分布,第一步最可能的是 chases(0.1875)。但真实分布下最可能的整句话是 “the dog eats bones every day .”,它的第一个词 eats 的真实概率只有 0.1125。原因是概率被”摊薄”了:选介词之后还要选 the、8 种动物、单复数,每一个具体的句子只分到 1/48;选 chases 之后还要在几种猎物和单复数之间分;而 eats 之后,bones 几乎是唯一的选择。结果:
| 方法 | 生成的句子 | 模型 log p | 真实 log p(给定主语) |
|---|---|---|---|
| 贪心 | the dog behind the foxes hears the foxes in the field . | −8.245 | −10.163 |
| 束搜索(W=4) | the dog eats bones every day . | −3.157 | −3.150 |
| 真实分布的众数 | the dog eats bones every day . | — | −3.150 |
这正是 slides 搜索树的例子:每一步选”眼前最好”的边,不保证整条路径最好。即使模型完全准确,贪心也会失败(练习 8:按真实分布贪心,得到的是 “the dog chases the cat in the morning .”,概率只有众数的约三分之一)。
6.3 祖先采样
slides 的另一种做法:从语言模型采样——把每一步的分布当成一颗加权骰子,选对应前缀的那颗骰子,掷一下,把结果接到前缀后面,重复。在搜索树上,就是在每个节点按(由负对数概率换算回来的)概率随机选一条边。slides 指出:
- 这是一种精确的采样方法:假设分布是局部归一化的(每一步的条件分布各自加起来是 1),按这种方式抽出的路径恰好服从它的总概率;
- 计算时间和最长路径的长度成正比。
这叫祖先采样(ancestral sampling),第 1 讲的 n-gram 和 RNN-LM 采样都是这样做的。
6.4 束搜索(补充)
束搜索(beam search)是贪心和穷举之间的折中:每一步保留累计对数概率最高的 条部分序列(“束”),把每条束扩展出 个候选,从全部 个候选里再取最好的 个;以 </s> 结尾的候选放进”已完成”列表。 就是贪心。decoding.beam_search 的停止条件是:已完成的最好句子得分不低于所有未完成束的得分(再扩展只会让得分更低)。
05 第 2 部分在 16 个主语上比较束宽:
| 束宽 | 找到真实众数的主语数 | 最好句子的平均模型 log p |
|---|---|---|
| 1(贪心) | 5 / 16 | −4.620 |
| 2 | 8 / 16 | −4.041 |
| 4 | 12 / 16 | −2.589 |
| 8 | 13 / 16 | −2.547 |
束宽 8 还没找到的 3 个,不是搜索不够:模型给束搜索结果的概率本来就比给真实众数的高(例如 “the bats eat flies at night .” −4.743 vs “the bats make noise at night .” −4.758)。束搜索忠实地找到了模型最可能的句子,错的是模型的估计(差距都很小)。
**从 <s> 开始时更明显。**束宽 1、4、16 都给出 “the foxes chase the hens at night .”,模型 log p −4.633;真实的全局众数 “the bat makes noise at night .” 在模型看来只有 −5.022。模型对 16 种主语的估计不均匀(foxes 0.098、cats 0.041,真实各 0.0625),最大化概率会把模型的这点偏差放大成”永远只生成这一句”。在真实的大模型上,“模型最可能的句子”往往很短、很平淡;Stahlberg & Byrne (2019) 发现机器翻译模型的精确众数经常是空译文。最大化概率不等于生成好文本。
(束搜索还偏爱短句:每多一个 token 就多乘一个小于 1 的概率。机器翻译里常用长度归一化——按平均每个 token 的 log p 排序——来抵消;beam_search 的 length_norm 参数实现了这一点。农场语言里最可能的句子本来就是最短的,所以这里看不出这个问题。)
6.5 温度、top-k 与 top-p(补充)
采样可以通过改变每一步分布的形状来调节:
- 温度 :。 让分布更尖,偏向高概率的词; 让分布更平; 就是贪心(练习 6)。
- top-k(Fan et al., 2018):只保留概率最高的 个 token,其余设为 0,再重新归一化。
- top-p / nucleus 采样(Holtzman et al., 2020):把 token 按概率从大到小排,保留”累积概率刚好达到 “的最小集合。和 top-k 不同,保留的个数随分布形状自动变化(练习 7)。
05 第 3 部分从 <s> 开始各生成 2000 句:
| 策略 | 合法 | 可能 | 不同句子 | 真实 NLL/token | 占比 | 覆盖 |
|---|---|---|---|---|---|---|
| 真实分布(直接抽样) | 1.000 | 1.000 | 0.722 | 0.883 | 0.45 | 0.384 |
| 贪心 / 束搜索 W=4 | 1.000 | 1.000 | 0.001 | 0.620 | 1.00 | 0.004 |
| 祖先采样 τ=1 | 0.990 | 0.965 | 0.683 | 0.860 | 0.49 | 0.362 |
| 温度 τ=0.5 | 1.000 | 0.999 | 0.601 | 0.823 | 0.46 | 0.302 |
| 温度 τ=1.5 | 0.846 | 0.750 | 0.781 | 0.879 | 0.53 | 0.366 |
| 温度 τ=2.5 | 0.144 | 0.097 | 0.980 | 0.870 | 0.68 | 0.139 |
| top-k, k=3 | 1.000 | 0.996 | 0.228 | 0.787 | 0.55 | 0.058 |
| top-k, k=10 | 0.997 | 0.980 | 0.644 | 0.862 | 0.47 | 0.255 |
| top-p, p=0.9 | 1.000 | 1.000 | 0.655 | 0.857 | 0.45 | 0.307 |
| top-p, p=0.5 | 1.000 | 1.000 | 0.316 | 0.795 | 0.46 | 0.049 |
| τ=2.5 + top-p 0.9 | 0.409 | 0.279 | 0.946 | 0.900 | 0.58 | 0.253 |
(合法 = 语法结构正确;可能 = 在真实分布下概率大于 0,还要符合各种动物的习性;真实 NLL/token = 可能的句子在真实分布下每个 token 的负对数概率;覆盖 = 生成过的不同句子在真实分布下的概率之和。)
- 贪心和束搜索每次都生成同一句话。它的真实 NLL/token 只有 0.620,比真实样本的典型值 0.883 低得多:太”典型”反而不像真实文本的样本。搜索找的是”最可能的一句”,而不是”一句可能的话”。
- 祖先采样的各项统计都和真实分布接近(可能率 0.965):模型准,样本就像真的。剩下 3.5% 不可能的句子来自模型分给”长尾”的那一点点概率——每一步只有很小的概率出错,但一句话有十几步。
- 温度:τ=0.5 几乎全是可能的句子,代价是多样性;τ=1.5 时可能率降到 0.750,τ=2.5 时只剩 0.097,开始胡言乱语。
- top-p 0.9 把长尾砍掉,可能率回到 1.000,多样性只略有下降(不同句子 0.655 vs 0.683)。即使在 τ=2.5 时,加上 top-p 0.9 也能把可能率从 0.097 拉回到 0.279。
- 截得太狠也有代价:top-k=3 时不同句子只剩 0.228——k 是固定的,而选主语时分布很平(16 个各约 1/16),k=3 砍掉了大部分合理的选择;top-p 按累积概率截断,能随分布形状自动调整。
没有一种策略在所有指标上都最好:质量和多样性之间总要权衡。实际产品里常见的组合是”温度 0.7–1.0 + top-p 0.9–0.95”;需要确定性答案(比如做选择题、写代码时)就用很低的温度或贪心。
6.6 一直往下写:复读(补充)
05 的模型每次只生成一句话。真实的语言模型是在连续的文本流上训练的(§3.5 的打包),生成时也常常一口气写很多句。06_degeneration.py 在打包的农场语言文本流(块长 128)上训练了一个 GPT,然后从 32 个句子开头出发各写 120 个 token:
| 解码方式 | 每段完整句子数 | 和前文重复的句子比例 | 可能的句子比例 | 每段不同句子数 |
|---|---|---|---|---|
| 贪心 | 12.6 | 0.594 | 1.000 | 5.1 |
| 温度 τ=0.5 | 10.4 | 0.009 | 0.994 | 10.3 |
| 祖先采样 τ=1 | 10.2 | 0.000 | 0.935 | 10.2 |
| top-p 0.9 | 10.0 | 0.003 | 0.991 | 9.9 |
| 真实分布(每段独立抽 8 句) | 8.0 | 0.003 | 1.000 | 8.0 |
把一段里相同的句子用同一个字母表示,贪心的前几段是 ABCDCECFFFFF、ABCBCBCBDEDDD、ABCCCDBCCDBC……;top-p 0.9 的是 ABCDEFGHIJK。贪心生成的每一句单独看都完全合法,合在一起却是典型的”复读机”。
为什么?真实分布里前后两句话是独立的,下一句的主语是 16 选 1 的均匀分布;可模型对下一句的估计总有一点偏好,贪心每次都选最偏好的那个,再接上它最可能的那一句。贪心又是确定性的,同样的前文永远得到同样的下一句,所以一旦前文开始重复,后面就跟着重复。Holtzman et al. (2020) 在 GPT-2 上观察到同样的现象,称之为退化(degeneration),并据此提出了 top-p 采样:人写的文本并不总是选最可能的词,最大化概率的解码会落入重复的”高概率陷阱”。
7. GPT-1 与 GPT-2:生成式预训练(补充) · GPT-1 and GPT-2
本讲的两篇阅读材料把”训练一个语言模型”推向了”用语言模型解决其他任务”。这里只概括要点,第 4 讲会系统讨论预训练和微调。
GPT-1(Radford et al., 2018):“生成式预训练 + 判别式微调”。
- 无监督预训练:在 BooksCorpus(7000 多本未出版的书)上,用本讲的目标函数——最大化 ——训练一个 12 层的 Transformer 解码器(第 2 讲 §4.7 数过它的参数:约 1.17 亿)。
- 有监督微调:对每个下游任务(文本分类、蕴含判断、相似度、多项选择问答),把输入改写成一个 token 序列(例如蕴含任务把前提和假设用一个特殊的分隔符 token 连起来),在最后一个 token 的表示上加一个线性分类层,用少量标注数据微调整个网络。微调时还把语言模型的损失作为辅助目标加进去。
关键在于:模型结构几乎不随任务改变,预训练学到的知识被所有任务共享。GPT-1 在论文测试的 12 个任务里有 9 个超过了当时专门设计的模型。
GPT-2(Radford et al., 2019):“语言模型是无监督的多任务学习者”。
- 在 WebText(约 800 万个网页、40 GB 文本,取自 Reddit 上获得较多赞的外链)上训练,最大的模型 15 亿参数(第 1 节的表)。
- 核心主张:如果语言模型足够大、训练数据足够多样,它在预测下一个词的过程中就会学会很多任务,因为这些任务的”示范”自然地出现在文本里。于是可以不做任何微调(零样本),只用合适的提示让模型”续写”出答案:例如在文章后面加上 “TL;DR:” 让它写摘要,或者用 “英语句子 = 法语句子” 的格式让它翻译。
- 零样本的成绩在很多任务上还远不如专门的模型,但随着模型变大稳定提升——这为 GPT-3 的”上下文学习”(第 11 讲)埋下了伏笔。
GPT-2 的论文也用到了本讲的两个技术:字节级 BPE(§5.5)和 top-k 采样(§6.5,论文生成样例时用的是 k = 40)。
8. 小结 · Summary
- 深度语言模型的训练目标和 n-gram 一样是训练集的对数似然 ,按链式法则拆成每个 token 的交叉熵;没有闭式解,用自动微分 + 小批量 SGD 优化。同一个模型族时,梯度下降会逼近数数的解。START/END 让模型成为任意长度句子上的一个分布。
- 小批量让梯度噪声按约 下降,并让每个样本的计算更便宜;批大小是准确度和每步成本之间的权衡。
- 批处理:截断到块大小、填充到等长、填充位置不计损失;因果模型右填充是免费的,左填充必须同时屏蔽填充的键并修正位置编号;分桶减少填充,打包消除填充但会跨越文档边界。
- KV 缓存: 只依赖前 个 token,算一次存起来;查询、分数和权重用完即丢。生成 个 token 的计算从 降到 (注意力项除外),代价是内存随长度线性增长——长上下文时可以超过模型权重。
- 分词:以词为单位有 OOV、词表大;以字符为单位序列长;子词(BPE)是折中:从字符开始反复合并最常见的相邻对。BPE 能学到常见的词缀,但依据只是频次。字节级 BPE 以 256 个字节为基础,没有 OOV。
- 解码:贪心和束搜索找模型概率最高的句子,平淡、缺乏多样性,并会把模型的小偏差放大;长文本里贪心会复读。祖先采样是精确的采样方法,但会采到长尾里的错误;温度、top-k、top-p 在质量和多样性之间调节。
- GPT-1 证明了”生成式预训练 + 少量微调”的威力;GPT-2 进一步提出,足够大的语言模型可以零样本完成很多任务。
9. 常见误区 · Common pitfalls
- “训练时填充的位置也算损失没关系。” 会严重扭曲损失(本讲的例子从 0.93 变成 5.10),还会让模型去学预测
<pad>。 - “左填充和右填充一样。” 对因果模型,左填充必须配合注意力掩码和位置编号,否则结果是错的(误差达到 11)。
- “KV 缓存也要缓存 Q。” 旧的查询不会再被用到;只有 K 和 V 会被之后的每一步重复使用。
- “KV 缓存只是一个小优化。” 它把生成的计算从平方降到线性,而且它的大小决定了推理时能放多少并发请求、能支持多长的上下文。
- “束搜索总比贪心好,束越宽越好。” 束搜索只是更好地找到模型的众数;模型的众数可能很平淡(甚至是空句子),而且会放大模型的偏差。开放式生成通常用采样,而不是搜索。
- “温度越低越好 / 越高越有创意。” 低温牺牲多样性,高温产生大量不可能的输出;没有一个温度在所有指标上最好。
- “BPE 切出来的就是语素。” BPE 只看频次,经常切出 “sof test” 这样不符合语言学的片段。
- “字符级分词可以处理任何语言。” 只能处理训练时见过的字符;字节级 BPE 才真正没有 OOV。
- “小批量梯度就是真实梯度。” 它只是一个有噪声的估计,噪声随 下降;B=1 时和全量梯度的余弦相似度只有 0.58。
- “去掉 END 也能定义句子的分布。” 那样定义的是每个长度各一个的一族分布,模型不知道何时停止。
10. 练习 · Exercises
练习 1(截断与填充)。 slides 的 8 个句子长度分别是 10、10、12、6、11、9、11、8。(a) 块大小为 10 时,一共截掉多少个词?填充多少个 <PAD>?(b) 块大小改成 8 呢?填充占全部位置的比例各是多少?
参考答案
(a) 截掉 个词(of times、know、play),填充 个,占 。 (b) 截掉 个词,填充只有 2 个(长度 6 的那句),占 。块越小,填充越少,但被截掉、模型永远看不到的词越多。
练习 2(按 token 平均 vs 按句子平均)。 一个批里有两句话:A 有 2 个预测位置,每个位置的交叉熵都是 2.0;B 有 8 个位置,每个都是 1.0。分别求按 token 平均和按句子平均的损失。哪一个和”整个训练集的对数似然”成正比?
参考答案
按 token:。按句子:。训练集的对数似然是对所有 token 的对数概率求和,所以按 token 平均和它成正比(差一个常数因子:总 token 数)。按句子平均相当于给短句子更大的权重。
练习 3(KV 缓存的大小)。 Llama-2 7B:32 层、32 个头、每头 128 维,用普通多头注意力,FP16 存储。(a) 一个 4096 token 的序列,KV 缓存多大?(b) 批大小 16 呢?(c) 如果改用 8 个 KV 头的 GQA(第 4 讲),(a) 变成多少?
参考答案
(a) 字节 = 2 GiB。(b) 32 GiB——比模型权重(约 13 GB)还大得多,这就是推理时并发数受限于显存的原因。(c) KV 头数减为 1/4,缓存也变为 0.5 GiB。
练习 4(缓存省了多少)。 只看线性层(每个 token 每层 FLOPs),从空提示开始生成 个 token。不带缓存时,第 步要处理 个 token;带缓存时每步只处理 1 个。求两者总 FLOPs 之比。 时是多少?
参考答案
无缓存:;有缓存:。比值 , 时约 500 倍。(这是只看线性层的上界估计;注意力项和固定开销会让实测倍数小一些,见 §4.4。)
练习 5(手工 BPE)。 Sennrich et al. (2016) 的例子:语料里 low 出现 5 次、lower 2 次、newest 6 次、widest 3 次。(a) 写出前 5 次合并(并列时取字典序最小的一对)。(b) 用学到的前 5 次合并,编码 lowest。
参考答案
(a) 第 1 次:(e, s) 出现 6 + 3 = 9 次 → es;第 2 次:(es, t) 9 次 → est;第 3 次:(est, </w>) 9 次 → est</w>;第 4 次:(l, o) 5 + 2 = 7 次 → lo;第 5 次:(lo, w) 7 次 → low。(bpe.BPE.train 得到的前 5 次合并完全相同;之后的第 6 次是 (e, w) 和 (n, e) 并列 6 次,按字典序取 (e, w)。)
(b) l o w e s t </w> → 先合并 e s → es,再 es t → est,再 est </w> → est</w>,再 l o → lo,再 lo w → low:得到 [low, est</w>]——虽然 lowest 从没在语料里出现过。
练习 6(温度)。 logits 。求 、、 时的分布。
参考答案
:[0.665, 0.245, 0.090]。(相当于 logits 乘 2):[0.867, 0.117, 0.016],更尖。:[0.506, 0.307, 0.186],更平。 时趋于 [1, 0, 0](贪心), 时趋于均匀分布。
练习 7(top-k 与 top-p)。 某一步的分布是 [0.5, 0.2, 0.15, 0.1, 0.05]。分别求 top-k(k=2)、top-p(p=0.8)、top-p(p=0.5)之后重新归一化的分布。
参考答案
top-k=2:保留 0.5 和 0.2,归一化为 [0.714, 0.286, 0, 0, 0]。top-p=0.8:按概率从大到小累加,0.5、0.7、0.85——加上第三个时首次达到 0.8,保留前三个,归一化为 [0.588, 0.235, 0.176, 0, 0]。top-p=0.5:第一个就达到 0.5,只保留它,等于贪心。(和 decoding.filter_logits 的输出一致。)
练习 8(即使模型完全准确,贪心也会失败)。 用真实分布对 “the dog” 做贪心解码。已知:第一步 chases 0.1875 是最大的(每个介词 0.1833);chase 之后是 “the” 再选猎物,最大的是 cat(4/9 × 1/2 = 0.222,cats 并列);时间短语的第一个词,“in”(in the morning 3/9 + in the field 2/9)比 “every”(every day 4/9)大;“in the” 之后 morning(3/5)比 field 大。求贪心得到的句子及其条件概率,并和真实众数 “the dog eats bones every day .” 的概率比较。
参考答案
贪心得到 “the dog chases the cat in the morning .”,条件概率 。真实众数的概率 ,是贪心结果的约 3.1 倍。贪心在两个地方被”摊薄”骗了:chases 之后要在猎物和单复数之间分;“in” 之后要在 morning 和 field 之间分。(farm_true_logprob 算出的两个概率与此相同。)
练习 9(END 那一列)。 一个只在 {a, b} 上预测、从不预测 END 的模型,所有长度为 3 的句子的概率之和是多少?长度 0 到 3 的所有句子呢?为什么这说明它不是”句子上的分布”?
参考答案
长度为 3 的 8 个句子的概率之和是 1(每一步都在 a、b 之间归一化)。长度 0 到 3 的所有句子加起来是 4。一个概率分布在所有结果上的总和必须是 1;这里每个长度单独加起来就是 1,所以模型实际上为每个长度定义了一个分布,不能比较不同长度的句子,也不能决定何时停止。
练习 10(小批量的噪声)。 §1.3 的表里, 时 。如果误差严格按 下降, 时应该是多少?实测是 0.362,为什么不完全相等?
参考答案
。实测 0.362,接近但不相等:每种批大小只抽了 16 个小批量,估计本身有波动;而且句子长度不同、损失按 token 平均,各个样本对梯度的权重不完全相等,严格的 关系只对”独立同分布、等权重的样本平均”成立。
练习 11(字节与 token)。 “生成式” 三个字,UTF-8 是多少字节?在一个完全没有中文合并规则的字节级 BPE 里,它会被切成几个 token?以字符为单位呢?
参考答案
9 个字节(每个汉字 3 个字节)。没有中文的合并规则时,就是 9 个 token;以字符为单位是 3 个。真实的多语言分词器会学到常见汉字的合并,把很多汉字合成 1–2 个 token,但中文的”每个字几个 token”通常仍高于英文的”每个词几个 token”。
11. 延伸阅读 · Further reading
- 语言模型与生成:Mikolov et al. (2010),RNN 语言模型;Graves (2014),用 LSTM 逐字符生成文本和手写轨迹——“在长序列上训练、一步一步采样”的早期经典。
- GPT 系列:Radford et al. (2018) GPT-1;Radford et al. (2019) GPT-2;Brown et al. (2020) GPT-3(附录 D 有训练计算量的表)。
- 分词:Sennrich, Haddow & Birch (2016), Neural Machine Translation of Rare Words with Subword Units(BPE);Kudo & Richardson (2018), SentencePiece;Wu et al. (2016) 的 Google 神经机器翻译系统(WordPiece)。
- 解码:Holtzman et al. (2020), The Curious Case of Neural Text Degeneration(top-p、退化);Fan, Lewis & Dauphin (2018), Hierarchical Neural Story Generation(top-k);Stahlberg & Byrne (2019), On NMT Search Errors and Model Errors: Cat Got Your Tongue?(精确众数常是空译文);Meister, Cotterell & Vieira (2020), If beam search is the answer, what was the question?。
- KV 缓存与推理系统:Pope et al. (2022), Efficiently Scaling Transformer Inference;Kwon et al. (2023), Efficient Memory Management for Large Language Model Serving with PagedAttention(vLLM)。
- 打包与文档掩码:Llama 3 技术报告(Dubey et al., 2024)中关于文档内注意力掩码的描述。
12. 下一讲预告 · Next lecture
第 4 讲会做两件事:
- 预训练 vs 微调:从 2006 年的逐层无监督预训练、自编码器,到 GPT 式的生成式预训练;“预训练 → 中期训练 → 后训练”的现代流程;我们会在农场语言上做一个”预训练 + 微调 vs 从零训练”的对比实验。
- 现代 Transformer:2017 年之后,开源模型(Llama、Mistral、OLMo、Gemma……)把 Transformer 改了哪些地方——RMSNorm、SwiGLU,以及三个重点:旋转位置编码 RoPE(让注意力分数只依赖相对位置)、分组查询注意力 GQA(缩小本讲的 KV 缓存)、滑动窗口注意力(让每个 token 只看最近的 个 token)。