对应:Stanford CS224N Winter 2026 Lecture 5(Transformers)与作业 3;Winter 2025 Lecture 8(Self-Attention and Transformers);Manning 主讲的 Winter 2023 版第 8 讲(录像在 YouTube 上)。 配套代码:
code/lecture07/(04、05 号文件导入第 5 讲的代码;transformer.py会被第 9、10 讲复用) 预计阅读时间:5–6 小时(第一遍通读 2.5 小时,第二遍对照代码 3 小时)
0. 本讲导航
0.1 学完本讲,你应该能够
- 说出 RNN 的两个结构性问题(交互距离随长度线性增长、无法沿时间并行),以及注意力是怎样同时解决它们的。
- 用”查询、键、值”写出自注意力的公式和矩阵形式,并能手算一个小例子。
- 解释为什么点积要除以 :不缩放时 softmax 会饱和、梯度消失。
- 说出把自注意力当作积木时的三个缺陷,以及对应的三个补丁:位置编码、前馈网络、因果掩码。
- 比较正弦位置编码、可学习的位置嵌入和 RoPE,并证明 RoPE 的注意力打分只取决于相对位置。
- 解释多头注意力为什么有用、怎样实现,以及它的计算量为什么和单头一样。
- 画出一个 Transformer 块:残差连接、层归一化、前馈网络;说出 Pre-LN 和 Post-LN 的区别;会算参数量(每块 )。
- 区分编码器、解码器、编码器—解码器三种结构,知道它们各自对应哪些著名模型。
- 分析自注意力 的计算量,说出它什么时候成为瓶颈。
0.2 先修知识
| 知识点 | 用在哪里 | 最低要求 |
|---|---|---|
| 第 6 讲 | 全讲 | 注意力的计算步骤(打分、softmax、加权求和);查询—键—值的一般形式;残差连接 |
| 第 5 讲 | 第 1、11 节 | RNN 语言模型;困惑度;主谓一致测试 |
| 第 3 讲 | 第 3、7 节 | 矩阵乘法的形状;softmax 的导数;初始化;批归一化(了解即可) |
| 三角函数 | 第 5 节 | 和角公式;二维旋转矩阵(不熟悉也能看懂结论) |
0.3 本讲地图
第1节 从 RNN 到"只用注意力"
第2节 自注意力:查询、键、值 ← 代码 01
第3节 缩放点积:为什么除以 √d_k ← 代码 01
第4节 自注意力作为积木的三个缺陷与补丁 ← 代码 01
第5节 位置编码:正弦、可学习、RoPE ← 代码 02、05
第6节 多头注意力 ← 代码 03
第7节 Transformer 块:残差、层归一化、前馈网络 ← 代码 03(本讲重点)
第8节 三种结构:编码器、解码器、编码器—解码器
第9节 训练 Transformer 的细节
第10节 计算量与长序列
第11节 实验:迷你 GPT vs RNN ← 代码 04、05
第12节 今天的 Transformer 改了什么
第13–17节 小结、与作业的关系、练习、延伸阅读、下一讲预告
transformer.py 是本讲所有零件的完整实现(约 400 行,大部分是注释)。建议读完第 7 节之后通读一遍——到那时你会发现,GPT 的结构其实没有多少东西。
1. 从 RNN 到”只用注意力”
1.1 RNN 的两个结构性问题
第 5、6 讲的 RNN(包括 LSTM)按顺序一个词一个词地处理句子。这种”顺序处理”带来两个问题,而且它们是结构本身的问题,换更好的门控也解决不了。
问题一:交互距离随长度线性增长。 第 1 个词的信息要影响第 个词,必须穿过中间的 步。每穿过一步,信息都要被压缩进固定大小的隐藏状态、和新的输入混合一次。距离越远,信息越难保持,梯度也越难传回去(第 6 讲第 2 节)。LSTM 缓解了这个问题,但路径长度仍然是 。
而语言里偏偏有大量远距离的依赖:
The chef who ran to the store and bought some fresh vegetables was tired.
决定动词 was 的 chef 和它隔了 10 个词。一个好的模型应该能让 was “直接看到” chef。
问题二:无法沿时间并行。 要算第 步的隐藏状态,必须先算出第 步。一个长度为 的句子,前向和反向传播都有 个必须依次进行的操作。GPU 擅长同时做大量相互独立的计算,RNN 却让它大部分时间在等待。这限制了我们能在多大的数据上训练多大的模型——而第 9 讲会看到,“规模”恰恰是后来一切进展的关键。
1.2 注意力已经给了我们什么
回顾第 6 讲的注意力:解码器的每一步,都直接给编码器的所有隐藏状态打分、加权求和。
- 任意两个位置之间只隔一步:交互距离是 。
- 不同查询的注意力计算互不依赖,可以同时进行。
第 6 讲的注意力是在 RNN 之上”加”的一个模块。一个自然的问题:如果注意力这么好,能不能把 RNN 整个去掉,只用注意力来构建模型?
1.3 Attention Is All You Need
2017 年,Google 的 Vaswani 等人发表了论文 Attention Is All You Need,提出 Transformer:完全没有循环,只用注意力和逐位置的前馈网络。它在机器翻译上超过了当时最好的 RNN 模型,训练速度却快得多。
此后几年,Transformer 几乎”吃掉”了整个 NLP,然后是语音、视觉、蛋白质结构预测等领域。BERT(2018)、GPT 系列(2018 年起)、T5(2019),以及今天所有的大语言模型,主体都是 Transformer。
本讲从最核心的自注意力开始,一块一块地把 Transformer 搭起来。
2. 自注意力:查询、键、值
2.1 注意力是”软的”字典查找
第 6 讲第 9 节说过,注意力的一般形式是:用一个查询(query)去和一组键(key)比较,按相似度对对应的值(value)加权求和。
用 Python 的字典来类比:
- 字典是”硬”查找:
d[query],查询必须和某个键完全相等,才返回那一个值。 - 注意力是”软”查找:查询和每个键都算一个相似度,经过 softmax 变成权重,返回所有值的加权平均。
动手:代码
01_self_attention_numpy.py演示 1。三个键 、、,对应的值是 10、20、30:
查询 权重 输出 0.953、0.000、0.047 10.95 0.474、0.174、0.351 18.77 0.31、0.31、0.379 20.69 方向相同的查询 和 ,长的那个几乎把全部权重给了第 1 个值(接近”硬”查找),短的那个权重分散得多。打分的大小决定了 softmax 有多”尖”——第 3 节会看到这一点为什么重要。
2.2 自注意力的定义
第 6 讲的注意力是”解码器看编码器”:查询来自一个序列,键和值来自另一个序列。自注意力(self-attention)是”一个序列看它自己”:句子里的每个词都当一次查询,去看句子里的所有词(包括它自己)。
设输入是 个词的向量 。用三个矩阵 、,把每个词分别投影成查询、键、值:
然后对每个词 :
翻译成大白话:词 的新表示 ,是所有词的”值”的加权平均;权重由词 的”查询”和每个词的”键”有多匹配决定。(除以 的原因见第 3 节。)
2.3 矩阵形式
把所有词的向量排成矩阵 (每行一个词),上面的计算可以一次完成:
形状:,(第 行第 列是词 对词 的打分),softmax 按行做,结果再乘 ,得到 的输出。
整个计算就是几次矩阵乘法加一次 softmax——这正是 GPU 最擅长的事。
动手:代码 01 演示 2 用 4 个词(the cat sat down)、、 一步一步地算:打印 、打分矩阵、权重矩阵和输出,并验证”对每个词循环计算”和”矩阵一次计算”的结果完全一致。参数是随机的,所以这里的权重没有语言学含义;训练之后,模型会学到”动词关注主语”这样的模式(第 11.4 节)。
2.4 为什么要三个不同的投影?
为什么不直接用 打分、对 加权求和?
- 查询和键扮演不同的角色。 “我在找什么”(查询)和”我能提供什么”(键)不是一回事。比如动词想找它的主语:动词的查询应该和名词的键匹配,而不是和别的动词匹配。如果用同一个向量, 是对称的, 关注 的程度就必然等于 关注 的程度;而且每个词都会最关注它自己( 通常最大)。
- “用来匹配的信息”和”被取走的信息”也不同。 键决定”谁会被关注”,值决定”被关注之后传递什么”。分开投影,模型可以用名词的”名词性”来匹配,却把它的”单复数”或”语义”传递过去。
- 投影到较小的维度 ,还能减少计算量,并为多头注意力(第 6 节)做准备。
2.5 一个直觉例子
The animal didn’t cross the street because it was too tired.
处理 it 时,一个好的自注意力层应该把大量权重分给 animal——这样 it 的新表示就”吸收”了 animal 的信息,后面的层就知道”累的是动物”。如果把句子改成 “…because it was too wide”,it 就应该关注 street。
同一个词 it,在不同的句子里关注不同的词,得到不同的表示——这就是上下文相关的表示(contextual representation),第 9 讲会看到它是预训练模型强大的根本原因。而 RNN 要做到这一点,得把 animal 的信息一步一步地带过 7 个词。
3. 缩放点积:为什么除以
3.1 点积的方差随维度增长
假设查询和键的每一维都是均值 0、方差 1 的独立随机数(训练初期大致如此)。点积 是 个独立随机变量之和,每一项的均值为 0、方差为 ,所以
标准差是 。 时打分的典型大小是 8, 时是 32。
3.2 打分太大,softmax 会饱和
第 2.1 节看到:打分越大,softmax 越”尖”。当打分的量级达到几十时,softmax 几乎变成 argmax——最大的那个位置拿走几乎全部权重。
这为什么是问题?看 softmax 的导数(第 3 讲):
如果某个 、其余 ,那么每一项都接近 0:,。梯度几乎传不回打分,也就传不回 和 ——注意力”学不会”该看哪里。这和 sigmoid 饱和导致梯度消失(第 6 讲)是一回事。
3.3 解决:除以
把打分除以 ,方差就回到 1,与维度无关。
动手:代码 01 演示 3。对每个 做 2000 次实验(1 个查询、10 个键,都是标准正态随机向量)。“梯度范数”是 softmax 雅可比矩阵的范数,衡量梯度能有多少从权重传回打分:
点积的标准差 不缩放:最大权重 不缩放:梯度范数 缩放:最大权重 缩放:梯度范数 4 2.0 0.497 0.335 0.308 0.340 16 4.0 0.733 0.261 0.317 0.346 64 8.1 0.856 0.169 0.317 0.349 256 16.0 0.934 0.087 0.319 0.347 1024 32.2 0.965 0.048 0.320 0.348 不缩放时,维度越高,最大权重越接近 1,梯度范数缩小到原来的约七分之一;缩放之后,两者都几乎不随维度变化。
为什么第 6 讲的乘性注意力没有缩放?那里的维度不大,问题不明显;而且 的初始化可以吸收这个因子。Vaswani 等人在论文里明确指出, 较大时不缩放的点积注意力效果变差,这才加上了 ——所以 Transformer 的注意力叫缩放点积注意力(scaled dot-product attention)。
4. 自注意力作为积木的三个缺陷与补丁
只用自注意力能搭出一个好的语言模型吗?还不行。它有三个缺陷,每个都需要一个”补丁”。
4.1 缺陷一:不知道词序 → 位置编码
看自注意力的公式: 只取决于两个词的向量,和它们在第几个位置无关。如果把输入的词打乱顺序,每个词得到的输出完全一样,只是输出的顺序跟着打乱。数学上叫置换等变(permutation equivariant)。
动手:代码 01 演示 4。“dog bites man” 和 “man bites dog” 中,dog、bites、man 三个词的输出向量完全相同。给每个位置加上不同的位置向量之后,dog 的输出就不同了。
对自注意力来说,一个句子只是一个”词的集合”(就像词袋模型)。可是”狗咬人”和”人咬狗”意思完全不同。RNN 天生按顺序处理,不存在这个问题;自注意力需要我们显式地告诉它每个词的位置——这就是第 5 节的位置编码。
4.2 缺陷二:没有逐元素的非线性 → 前馈网络
自注意力的输出是值向量的加权平均。虽然 softmax 是非线性的,但对值来说,自注意力只是做线性的混合:把几层自注意力叠起来,每个位置得到的仍然只是各个值向量的加权平均(再乘一些矩阵)。
深度学习的威力来自非线性(第 3 讲)。补丁很简单:在每个自注意力层之后,对每个位置单独加一个小的前馈网络:
(GPT、BERT 用 GELU 代替 ReLU。)每个位置用的是同一组参数。一个直觉的分工:自注意力负责在位置之间”搬运”信息,前馈网络负责在每个位置上”加工”信息。
4.3 缺陷三:能看到未来 → 因果掩码
用自注意力做语言模型时,第 个位置的输出要用来预测第 个词。如果它能关注第 个词,就等于偷看了答案:训练损失会降得很低,模型却什么也没学会。
一个笨办法是每一步只把前 个词送进模型——这样就失去了并行性。更好的办法是掩码(masking):把”不允许看”的位置的打分设为 ,softmax 之后权重恰好为 0:
这叫因果掩码(causal mask),掩码矩阵是一个下三角矩阵。
动手:代码 01 演示 5 打印了掩码和加掩码之后的注意力权重(右上角全是 0),并验证:把最后一个词换掉,前面所有位置的输出都不变。
有了因果掩码,一个句子的所有位置可以一次性并行地算完,而每个位置的输出都和”只给模型看前 个词”时完全相同。训练时一个长度为 的句子同时提供了 个预测任务。这是 Transformer 语言模型训练快的关键。
注意:生成文本时仍然要一个词一个词地生成(第 个词要等第 个词生成出来)。并行的只是训练和读入提示的阶段。
4.4 最小的 Transformer
把三个补丁加上,就得到了一个”最小”的 Transformer 层:
输入词向量 + 位置编码
│
自注意力(带因果掩码)
│
前馈网络(逐位置)
│
……重复 N 层……
│
线性层 + softmax → 下一个词的概率
真正的 Transformer 还加了几样让训练更稳定的东西(多头、残差连接、层归一化),第 6、7 节逐一介绍。先看位置编码。
5. 位置编码
5.1 最朴素的想法为什么不行
我们想给每个位置 一个向量 ,把它加到词向量上:。(为什么是加而不是拼接?加法不增加维度;而且在高维空间里,词向量和位置向量可以大致占据不同的”方向”,加起来之后模型仍能把两种信息分开。)
最朴素的做法:(每一维都等于位置编号)。问题是数值没有上界——第 500 个位置的编码是 500,会完全淹没词向量(量级在 1 左右)。归一化成 ?那同一个位置在长短不同的句子里编码就不同了,模型无法学到”往前数 3 个词”这样的规律。
我们希望位置编码:(1) 每个位置都不同;(2) 数值有界;(3) 不同长度的句子里,同一个位置的编码相同;(4) 最好能方便地表达相对位置(“往前 3 个词”)。
5.2 正弦位置编码
原始 Transformer 的方案:
直觉:一组转速不同的时钟指针。 每一对维度 是一根指针,位置每增加 1,它转过角度 。前面的维度转得快(,大约 6 个位置转一圈),后面的维度转得慢(最后一对的周期长达几万个位置)。就像钟表的秒针、分针、时针:每根针单独看会”重复”,合在一起,每个时刻的组合都不同。所有值都在 之间,满足了前三个要求。
动手:代码
02_positional_encoding.py演示 1 打印了 时前几个位置的编码和一张字符热图(安装了 matplotlib 时还会另存figures/sinusoidal_pe.png):左边的维度变化很快,右边的维度在 50 个位置内几乎不变(sin ≈ 0,cos ≈ 1)。
相对位置:点积只取决于距离。 由 :
只和距离 有关,和起点 无关。
动手:代码 02 演示 2():
距离 0 1 2 4 8 16 32 64 32.0 30.9 28.3 23.9 22.4 19.4 19.6 13.9 起点 、20、100 时的结果完全相同;距离越远,点积大体上越小。
相对位置:平移是线性变换。 由和角公式,每一对 往后挪 个位置,相当于乘一个只依赖 的 旋转矩阵:
所以存在矩阵 使得对所有 都有 (代码 02 演示 3 做了数值验证)。原论文的设想是:这样线性层就有可能学会”关注往前 个位置”这样的模式。
但有一个问题:位置编码是加在词向量上的。打分 展开后有四项,其中只有”位置×位置”那一项是相对的,其他项把词和绝对位置搅在了一起。上面那些漂亮的性质,经过投影之后并不能被干净地利用。
5.3 可学习的位置嵌入
另一种做法简单粗暴:位置也当作”词”,每个位置一个可学习的向量,组成一个 的矩阵 ,和词嵌入一起训练。BERT()、GPT-2()都这样做。
- 优点:最灵活,每个位置的向量想学成什么样都可以。
- 缺点:最多只能处理 个位置;更要紧的是,训练时从没出现过的位置,它们的向量也从没被训练过(还是随机初始化的值)。代码 02 演示 5 演示了超出表长度时直接出错。
5.4 旋转位置编码(RoPE)
今天的大多数大模型(LLaMA、Qwen、Mistral 等)使用 RoPE(Rotary Position Embedding,Su 等 2021)。它的想法是:不把位置加到输入上,而是在每一层计算注意力时,把查询和键按各自的位置”旋转”一下。
二维的情形。 设查询和键都是二维向量。把位置 的查询旋转 角,位置 的键旋转 角:
旋转矩阵满足 (先转回 再转 ,等于转 ),所以
打分只取决于相对位置 ,和绝对位置无关。
用复数看更简洁:把二维向量看成复数,旋转就是乘 。打分是 ,同样只依赖 。
高维的情形。 把 维向量的维度两两配对,看成 个二维平面,第 个平面用角速度 (和正弦编码一样的一组频率)。每个平面各自旋转,打分是各个平面的点积之和,仍然只依赖相对位置。
动手:代码 02 演示 4。同一个查询和同一个键放在不同的位置:
(5, 2) (10, 7) (100, 97) (5, 5) (50, 50) (2, 5) (60, 63) 3 3 3 0 0 −3 −3 RoPE 打分 −3.724 −3.724 −3.724 −1.468 −1.468 −3.823 −3.823 加正弦编码后的打分 1.177 2.000 −2.135 3.041 0.331 0.829 0.842 用 RoPE 时,相对位置相同的三组打分完全相同;把正弦编码加到向量上再做点积,结果随绝对位置变化。旋转也不改变向量的长度(位置 0、1、10、100、1000 处都是 1.8627)。
RoPE 的好处:
- 打分天然只依赖相对位置,而相对位置往往才是语言里真正重要的(“前一个词""隔了 3 个词”);
- 不需要任何参数;
- 作用在每一层的 Q、K 上(不作用于 V),位置信息不会在深层被”冲淡”。
transformer.py 的 apply_rope 用的是”前一半维度和后一半维度配对”的写法(GPT-NeoX、LLaMA 的实现),和原论文”相邻两维配对”只是维度顺序不同,效果完全一样。
5.5 位置编码与长度泛化
如果测试时的句子比训练时更长,模型还能工作吗?这叫长度泛化(length generalization)。直觉上:
- 可学习的位置嵌入:没训练过的位置是随机向量,很可能出问题;
- 正弦编码:每个位置都有定义,但模型从没见过那些编码值;
- RoPE:打分只依赖相对位置,如果训练时见过的相对距离足够,可能推广得更好;
- 还有一种激进的做法:不加任何位置信息(NoPE)。对于带因果掩码的解码器,这并非完全没有位置信息——第 个位置只能看到 个词,模型可以从”能看到多少东西”推断出自己的位置。
第 11.3 节用实验检验这些直觉——结果和直觉并不完全一致。
6. 多头注意力
6.1 一个头只有一个注意力分布
在一个句子里,一个词往往同时和好几个词有关系。比如处理动词 chased 时,模型可能既想知道主语是谁(谁在追),又想知道宾语是谁(追谁)。但一个注意力头在每个位置只产生一个概率分布:它要么只看一个地方,要么把权重分给几个地方——得到的是这几个值向量的平均,平均之后就分不清哪个信息来自哪里了。
动手:代码
03_multihead_and_block.py演示 3 设计了一个需要”同时看三个位置”的任务:随机词序列(词表 10 个词,瞎猜 10%),在每个位置 同时预测 、、。模型只有一层注意力,总维度都是 24(头越多,每个头的维度越小),参数量完全相同(3,678 个),每种 3 个随机种子:
头数 最差的目标 1 69.1% 31.3% 61.2% 10.0% 2 98.4% 94.1% 96.7% 91.9% 3 100% 100% 100% 100% 6 100% 100% 100% 100% 1 个头时,每次训练总有一个目标只能瞎猜(“最差的目标”恰好是 10%);2 个头好得多但没有全对;头数够用之后全对。
6.2 多头注意力的定义
多头注意力(multi-head attention)同时运行 个独立的注意力”头”,每个头有自己的投影矩阵,最后把结果拼起来:
其中 ,。每个头在一个 维的”子空间”里计算注意力,可以关注不同的东西; 把各个头的结果混合起来。
计算量和单头一样。 每个头的维度是 , 个头的投影矩阵拼起来正好是 。实现时根本不需要 个独立的矩阵:先用一个 的矩阵算出整个 ,再把最后一维 reshape 成 ,把”头”这一维挪到前面,后面的矩阵乘法就对每个头分别进行:
Q: (B, T, d) → view → (B, T, h, d/h) → transpose → (B, h, T, d/h)
打分: (B, h, T, d/h) @ (B, h, d/h, T) → (B, h, T, T)
输出: (B, h, T, T) @ (B, h, T, d/h) → (B, h, T, d/h) → 拼回 (B, T, d) → 乘 W^O
动手:代码 03 演示 1 把每一步的形状都打印出来;演示 2 把我们的参数复制进 PyTorch 自带的
nn.MultiheadAttention,两者的输出和注意力权重的差别只有 量级(浮点误差)。
6.3 头在学什么?
研究者分析训练好的模型,发现不同的头确实有分工。Clark 等(2019)在 BERT 里找到了专门关注”动词的直接宾语""名词的限定词""介词的宾语”的头,有的头的注意力几乎就是依存句法树的弧(第 4 讲);还有很多头只是关注前一个词、后一个词,或者句子的分隔符。第 11.4 节会在我们的迷你 GPT 里找”盯着主语”的头。
另一个著名的例子是归纳头(induction head,Olsson 等 2022):两个头配合,实现”如果前面出现过 A B,现在又看到 A,就预测 B”。这被认为是大模型”上下文学习”能力的重要来源之一(第 9 讲)。
7. Transformer 块:残差、层归一化、前馈网络
7.1 残差连接
第 6 讲第 4.1 节说过,梯度消失是所有深层网络的问题,残差连接是通用的解决办法:
每一层不是把输入”替换”成新的表示,而是在输入上加一点修改。反向传播时 ,梯度总有一条直达的路径。
对 Transformer 来说,残差连接还有一个特别的作用。自注意力在每个位置都对所有位置做加权平均——如果没有残差,一层层平均下去,不同位置的表示会越来越像,最后所有位置变成同一个向量(Dong 等 2021 证明了纯注意力网络的这种”秩坍缩”以极快的速度发生)。残差连接给每个位置保留了一条”原样传过去”的通路。
动手:代码 03 演示 6 训练了 8 层的网络(任务:预测 ;瞎猜的损失 2.30、准确率 10%;300 步):
初始化时的坍缩程度 前 50 步损失 最后 50 步损失 准确率 Pre-LN(有残差) 0.126 1.062 0.003 100% Post-LN(有残差) 0.235 1.044 0.003 100% Pre-LN 去掉残差 1.000 2.317 2.305 10.1% “坍缩程度”是最后一层不同位置的表示之间的平均余弦相似度。去掉残差后,还没训练,所有位置的表示就已经是同一个向量了(余弦相似度 1.000),网络完全学不动。
7.2 层归一化
层归一化(Layer Normalization,Ba 等 2016)对每个位置的向量单独做标准化,再做可学习的缩放和平移:
为什么需要它? 经过很多层之后,向量的数值可能越来越大或越来越小,后面的层看到的输入范围不断变化,训练不稳定。层归一化让每一层的输入都保持在稳定的范围内。 和 是可学习的,模型需要时可以把标准化”撤销”掉。
为什么不用批归一化? 第 3 讲的批归一化沿着批的维度统计均值和方差。NLP 的批里句子长短不一(还有填充),批里的句子随时在变,按批统计很不稳定;生成文本时批大小可能是 1。层归一化只看单个位置的向量,和批里的其他东西无关。
动手:代码 03 演示 4 手算了层归一化:输入 和 量级差了 25 倍,标准化之后完全相同(都是 ),并验证和
nn.LayerNorm一致。
7.3 前馈网络
第 4.2 节已经介绍过:,隐藏层宽度通常是 。
它看起来不起眼,却占了每个块三分之二的参数( 对比注意力的 )。一种有影响的解读(Geva 等 2021)把前馈层看作”键—值记忆”: 的每一行是一个”键”,检测输入里的某种模式; 的对应列是”值”,在模式出现时往输出里加上相应的内容。模型记住的大量事实性知识,可能主要存储在前馈层里。
7.4 拼起来:Pre-LN 与 Post-LN
一个 Transformer 块由两个子层组成:多头自注意力和前馈网络,每个子层都带残差连接和层归一化。层归一化放在哪里,有两种写法:
Post-LN(原始 Transformer,2017) Pre-LN(GPT-2 及以后的大部分模型)
x = LN(x + Attention(x)) x = x + Attention(LN(x))
x = LN(x + FFN(x)) x = x + FFN(LN(x))
(所有块之后再加一个 LN)
区别在于残差的”主干”上有没有层归一化:
- Post-LN:每次相加之后都归一化,主干上的信号每层都被”重新缩放”。Xiong 等(2020)证明,Post-LN 在初始化时靠近输出的层梯度偏大,所以必须用学习率预热(第 9 节),否则训练一开始就可能发散;层数越多越难训练。
- Pre-LN:主干是一条干净的”加法高速公路”,归一化只作用在每个子层的输入上。梯度更均匀,训练更稳,对学习率不那么敏感。
在代码 03 演示 6 的小任务上两者都能训练;Post-LN 的问题在更深、更大的模型上才明显。今天几乎所有大模型都用 Pre-LN(通常还把 LayerNorm 换成更简单的 RMSNorm,见第 12 节)。transformer.py 默认使用 Pre-LN。
7.5 参数量
一个块的参数(Pre-LN,含偏置):
| 部分 | 参数 |
|---|---|
| (合成一个 的层) | |
| 输出投影 | |
| 前馈网络 | |
| 两个层归一化 | |
| 合计 |
整个解码器模型 = 词嵌入 + 位置嵌入 + 个块 + 最后的层归一化 (输出层和词嵌入共用同一个矩阵,见第 8.2 节)。
动手:代码 03 演示 5 验证了 、128、768 时公式和实际参数量一致。然后用同样的代码搭了一个 GPT-2 small 大小的模型(,,,12 层,12 个头):参数恰好是 124,439,808 个,和 OpenAI 公布的 GPT-2 small 完全相同。其中词嵌入占 31.0%,12 个块占 68.3%。
我们的
transformer.py和 GPT-2 的结构完全一样,只是小得多。
8. 三种结构:编码器、解码器、编码器—解码器
同样的 Transformer 块,按”能看到哪些位置”和”怎样组合”,可以搭出三种结构。
8.1 编码器:双向
不加因果掩码,每个位置能看到整个句子(左边和右边)。输出是每个词的上下文相关表示,适合”理解”类任务:分类、命名实体识别、抽取式问答。
代表:BERT(2018)。因为能看到右边的词,编码器不能直接当语言模型用(“预测下一个词”时答案就在输入里);BERT 用”把一些词遮住、预测被遮住的词”来训练(第 9 讲)。
transformer.py 里 TransformerLM(causal=False) 就是编码器。处理填充过的批时,要传 pad_mask,让真实的词不去关注填充位置。
8.2 解码器:单向(因果)
加因果掩码,每个位置只能看到自己和左边。就是一个语言模型,适合生成文本。
代表:GPT 系列,以及今天几乎所有的大语言模型(LLaMA、Qwen、Claude、Gemini 等的主体)。第 9 讲会讨论为什么”只用解码器”最终成了主流。
一个常用的技巧是权重共享(weight tying):输出层()和输入词嵌入()用同一个矩阵(Press & Wolf 2017)。直觉:输入时”词 → 向量”和输出时”向量 → 词的打分”用的是同一套词的表示,预测某个词的打分就是隐藏状态和这个词的嵌入的点积。这样能省下 个参数(GPT-2 small 里是 3860 万个),通常效果还更好。
8.3 编码器—解码器
原始 Transformer 的结构,用于机器翻译。编码器读源句子(双向);解码器生成目标句子(因果),并且在每个块的自注意力和前馈网络之间,多一个交叉注意力(cross-attention)子层:
查询来自解码器,键和值来自编码器的输出——这正是第 6 讲 seq2seq 里的注意力,只是现在两边都是 Transformer。
代表:原始 Transformer、T5(2019)、BART(2019)、Whisper(语音识别)。
8.4 小结
| 结构 | 掩码 | 擅长 | 代表 |
|---|---|---|---|
| 编码器 | 无(双向) | 理解:分类、标注、抽取 | BERT、RoBERTa |
| 解码器 | 因果 | 生成:语言模型、对话 | GPT 系列、LLaMA |
| 编码器—解码器 | 编码器双向,解码器因果 + 交叉注意力 | 序列到序列:翻译、摘要 | 原始 Transformer、T5、BART |
9. 训练 Transformer 的细节
Transformer 对训练细节比 RNN 更敏感。原论文和后来的实践中,几个常见的做法:
- Adam / AdamW:Transformer 几乎都用 Adam 类的优化器(第 3 讲)。AdamW(Loshchilov & Hutter 2019)把权重衰减从梯度更新中”解耦”出来,是今天的标准选择。
- 学习率预热(warmup)+ 衰减:前几百到几千步,学习率从接近 0 线性增加到最大值,之后再逐渐减小(原论文按 衰减,今天常用余弦衰减)。为什么要预热?训练刚开始时参数是随机的,梯度方向不可靠;Adam 对梯度二阶矩的估计也还没积累起来,有效步长可能很大;Post-LN 的结构更是在初始化时梯度偏大(第 7.4 节)。先用小学习率”热身”,能避免一开始就把模型推到很差的区域。
- Dropout:加在注意力权重、每个子层的输出、嵌入层之后。
- 标签平滑(label smoothing):训练目标不再是 one-hot,而是给正确答案 、其余词平分 (原论文 )。它让模型不要过于自信,在翻译上提高了 BLEU,代价是困惑度变差。
- 梯度裁剪:和 RNN 一样(第 6 讲),防止偶尔出现的巨大梯度。
- 初始化:GPT-2 把线性层和嵌入层初始化为 ,并且把残差路径上最后一个线性层的初始化再缩小 ( 是层数),让残差主干上的数值在初始化时不随层数增长。
transformer.py 的 warmup_cosine 实现了预热 + 余弦衰减;代码 04 的 train_gpt 用了 AdamW、预热、梯度裁剪和 dropout。
10. 计算量与长序列
10.1 自注意力的计算量
处理长度为 、维度为 的序列,一个 Transformer 层的乘加运算:
| 部分 | 计算量 |
|---|---|
| Q、K、V、输出投影(4 个 矩阵乘) | |
| 前馈网络() | |
| 打分 和加权求和 |
前两部分对 是线性的,注意力矩阵的部分是 。两者之比是 :序列长度超过 时,注意力矩阵开始占大头。
动手:代码 01 演示 6():
128 512 1024 4096 16384 65536 注意力矩阵的计算占比 2.7% 10.0% 18.2% 47.1% 78.0% 93.4%
对 GPT-2()来说,注意力矩阵只占不到 20% 的计算;但到了几万、几十万个词元的长上下文,它就成了主要的开销。除了计算, 的注意力矩阵还要占显存(每个头、每一层都有一个)。
和 RNN 比较:RNN 每层的计算是 ,但有 个不能并行的步骤;Transformer 每层的计算是 ,但所有位置可以并行,任意两个位置之间的路径长度是 。在 GPU 上、对通常的句子长度,Transformer 快得多。
10.2 长序列的应对方法(简介)
- FlashAttention(Dao 等 2022):不改变数学结果,通过分块计算避免把完整的 矩阵写入显存,大幅减少显存读写。今天的标准做法。(算法见 CS336 第 5 讲 §8,用 Triton 实现见第 6 讲)
- 稀疏注意力 / 滑动窗口注意力:每个位置只关注附近的一个窗口,或者固定的一些位置(Longformer、Mistral 的滑动窗口)。
- 线性注意力、状态空间模型(Mamba 等):把计算量降到 ,代价是表达能力上的一些折中;它们在某种意义上又回到了”RNN 式”的递推。(CS336 第 4 讲)
- KV 缓存:生成文本时,把之前位置的键和值缓存起来,每生成一个词只需要为新位置计算一次。(CS336 第 10 讲)
11. 实验:迷你 GPT vs RNN
现在把所有零件拼起来。代码 04_mini_gpt.py 在第 5 讲的玩具语言上训练一个解码器结构的 Transformer:,4 个头,2 层,可学习的位置嵌入,Pre-LN,权重共享——和 GPT-2 相同的结构,只是小得多。
11.1 训练与困惑度
动手:代码 04 演示 1、2。迷你 GPT 有 109,248 个参数(第 5 讲的 RNN 有 33,455 个),在 20,000 个句子上训练 8 轮(AdamW,最大学习率 ,预热 200 步,余弦衰减),在编写讲义的电脑上约 40~50 秒:
轮次 1 2 3 4 8 开发集困惑度 4.57 4.36 4.33 4.31 4.28 测试集困惑度:RNN(第 5 讲)4.33,迷你 GPT 4.27。
两者很接近:这门玩具语言的大部分词只依赖近处的上下文,两种模型都已经接近这门语言本身的”不确定性下限”了。差别要看专门的测试。
11.2 主谓一致
动手:代码 04 演示 3。训练语料里最多 3 个修饰语,最长的句子 20 个词元(含
<s>、</s>);4 个及以上修饰语的句子比训练时的任何句子都长:
修饰语个数 0 1 2 3 4 6 8 平均距离 0 3.7 7.5 11.2 14.8 22.3 29.7 最长前缀(含 <s>)4 8 12 16 20 28 36 RNN 100% 100% 100% 100% 100% 100% 100% 迷你 GPT 100% 100% 100% 100% 98.5% 98.8% 99.2%
在训练时见过的长度上,迷你 GPT 和 RNN 一样全对:自注意力可以直接看到任意远的主语。在更长的句子上,迷你 GPT 也几乎全对,只比 RNN 略低一点。
这其实有点出乎意料(编写这一讲时,我本来预期它会明显变差):位置嵌入表第 20 行以后的向量在训练中从没出现过,还是初始化时很小的随机值。模型仍然能找到主语,说明它找主语主要靠内容(“哪个词是名词、是单数还是复数”),而不是精确的位置;而那些没训练过的位置向量很小,几乎相当于”没有位置信息”,没有把模型带偏。
11.3 位置编码与长度泛化
代码 05_length_generalization.py 用完全相同的数据和设置,比较第 5.5 节的四种做法(每种 2 个随机种子,测试到 10 个修饰语)。
动手:代码 05。四种做法在训练分布内的测试集困惑度几乎一样(4.268~4.278 之间,包括完全不加位置信息的模型)。主谓一致准确率(2 个随机种子的平均):
修饰语个数 0~3 4 6 8 10 最长前缀(含 <s>)≤ 16 20 28 36 44 可学习的位置嵌入 100% 95.8% 97.8% 99.0% 97.8% 正弦编码 100% 100% 99.9% 95.4% 83.5% RoPE 100% 84.9% 72.5% 70.9% 66.4% 无位置信息 100% 100% 100% 100% 99.6% 正弦编码在 10 个修饰语时两个种子差别很大(约 68% 和 99%)。
这个结果和第 5.5 节的直觉几乎反过来:
- 无位置信息的模型推广得最好。 在这个任务里,找主语只需要看内容(哪个词是名词、单数还是复数),一个根本不使用位置的模型,自然不存在”没见过的位置”的问题。因果掩码给它的那点位置信息,已经足够让它在训练分布内和其他模型一样好。
- 可学习的位置嵌入只轻微下降。 没训练过的位置向量还是初始化时很小的随机值,在这些位置上模型几乎相当于”无位置信息”。
- RoPE 最差。 它的打分取决于相对距离,而训练时主动词和主语之间的距离最多二十个词左右。更远的距离对应的旋转角度——尤其是转得慢的那些维度——模型从没见过,打分变得不可预测。“相对位置”的好处,只在训练时见过的相对距离之内成立。
这和文献的发现一致:RoPE 本身不能外推到比训练时更长的序列。今天的大模型想支持更长的上下文时,会对 RoPE 做位置插值、NTK 缩放、YaRN 等调整(大意是把没见过的角度”压缩”回见过的范围),再用长文本继续训练一段时间。另一种思路是 ALiBi(Press 等 2022):不用位置编码,而是在打分上直接减去一个和距离成正比的惩罚。Kazemnejad 等(2023)在小规模实验中也发现,不加位置信息的解码器长度泛化最好。
但不要因此得出”位置编码没用”的结论。真实语言里,精确的位置信息对复制、计数、局部词序等都很重要,真实的大模型几乎都使用 RoPE。我们的玩具任务只考察了”找主语”这一件事——这正是玩具实验的【边界】:它能清楚地展示一个现象,却不能告诉你在真实任务上哪种做法更好。
另外请注意:如果只跑一个随机种子,正弦编码在 10 个修饰语时的结果可能是 68%,也可能是 99%,结论会完全不同。第 8 讲会专门讨论这个问题。
11.4 注意力在看哪里?
在预测主动词的那个位置(前缀的最后一个词),模型的每个头把注意力分给了哪些词?
动手:代码 04 演示 4 统计了 1000 个含 2 个修饰语的句子。如果均匀分配,每个位置平均分到 0.09。表中是每个头分给主语名词和分给干扰名词(单复数和主语不同的名词)的平均权重:
头 0 头 1 头 2 头 3 第 1 层 主语 0.36 / 干扰 0.04 0.01 / 0.12 0.28 / 0.05 0.10 / 0.07 第 2 层 0.00 / 0.00 0.31 / 0.00 0.00 / 0.00 0.46 / 0.00 例句 1(两个修饰语都是介词短语):第 1 层头 2 在这类句子里平均把 0.78 的权重给主语。在 “the old girl near the houses near the angry trees ___” 中,它给 girl 的权重是 0.87,给两个干扰名词 houses、trees 的权重都是 0.00。
例句 2(含主语关系从句):第 2 层头 3 在 “the cat that sees the boys that likes the teacher ___” 中,给主语 cat 的权重是 0;它把 0.25 和 0.20 分给了 sees 和 likes,剩下的落在
<s>(0.25)和句首的 the(0.29)上。
几点观察:
- 有的头学会了”找主语”。 没有人告诉模型什么是主语,它只是在训练”预测下一个词”的过程中发现:要选对动词的形式,就得看主语。例句 1 的那个头几乎只看主语,完全忽略中间的干扰名词。
- 模型会利用任何能用的线索。 例句 2 中,主语关系从句里的动词 sees、likes 和主语的单复数一致——它们的形式同样”泄露”了答案。于是这个头去看这些动词,而不是主语本身。
- 注意力可以分几步传递信息。 第 2 层的头看到的不再是原始的词,而是第 1 层输出的表示。第 1 层可能已经把主语的信息搬到了后面的位置上,第 2 层再从那些位置读取。所以”第 2 层的某个头给主语的权重是 0”并不说明它没有用到主语的信息。
- 注意力汇聚(attention sink)。 例句 2 中超过一半的权重落在
<s>和句首的 the 上,而因果掩码下,这两个位置根本”看不到”主语。很多模型里都能看到这种现象:一个头在某个位置”没事可做”时,就把注意力停在序列开头的几个位置上(Xiao 等 2023)。
这些分析很有启发,但要小心:注意力权重大,不等于这个位置”决定了”输出(第 6 讲第 8.5 节)。哪一层、哪个头负责什么,每次训练也都可能不同。第 17 讲会介绍更严谨的分析方法。
11.5 Transformer vs RNN
| RNN / LSTM | Transformer | |
|---|---|---|
| 任意两个位置之间的路径长度 | ||
| 沿序列的并行性 | 无(必须逐步计算) | 训练时完全并行 |
| 每层计算量 | ||
| 词序信息 | 天生按顺序处理 | 需要位置编码 |
| 生成时每步的代价 | 固定(只更新隐藏状态) | 随上下文变长(需要 KV 缓存) |
| 长度泛化 | 天生没有”位置表”的问题 | 取决于位置编码 |
| 规模扩展 | 难 | 好(第 9 讲) |
在我们的玩具实验里,两者的困惑度和主谓一致都差不多。Transformer 真正的优势在规模:并行训练让它能在海量数据上训练巨大的模型,而这正是第 9 讲的主题。
12. 今天的 Transformer 改了什么
从 2017 年到今天,Transformer 的骨架几乎没变(自注意力 + 前馈网络 + 残差 + 归一化),但很多零件被换成了更好的版本。CS336 第 3 讲有详细的讨论,这里列出最常见的几个:
| 零件 | 2017 年原始 Transformer | 今天的典型做法(如 LLaMA) |
|---|---|---|
| 归一化位置 | Post-LN | Pre-LN |
| 归一化方法 | LayerNorm | RMSNorm(不减均值,只除以均方根,更简单) |
| 位置编码 | 正弦(加在输入上) | RoPE(每层旋转 Q、K) |
| 前馈网络 | ReLU, | SwiGLU(门控的前馈网络,宽度约 ) |
| 偏置项 | 有 | 大多去掉 |
| 注意力 | 多头 | 分组查询注意力(GQA:多个查询头共用一组键和值,减小 KV 缓存) |
| 前馈层 | 稠密 | 部分模型用混合专家(MoE:每个词只激活一部分前馈网络,见 CS336 第 4 讲) |
这些改动大多是为了训练更稳定或推理更省,而不是改变模型”能做什么”。理解了本讲的原始结构,读任何新模型的结构图都不会有障碍。
13. 本讲小结
- RNN 的结构性问题:交互距离 、无法沿时间并行。注意力让任意两个位置一步可达、所有位置同时计算——于是 Transformer 完全去掉了循环。
- 自注意力:每个词用查询去匹配所有词的键,对值加权平均:。查询、键、值用不同的投影,分别负责”找什么""被谁找到""传递什么”。
- 缩放:点积的方差随 增长,打分太大会让 softmax 饱和、梯度消失,所以除以 。
- 三个补丁:自注意力不知道词序 → 位置编码;没有逐元素的非线性 → 前馈网络;能看到未来 → 因果掩码(有了它,训练可以完全并行)。
- 位置编码:正弦编码(不同频率的时钟,点积只依赖距离);可学习的位置嵌入(灵活,但受最大长度限制);RoPE(旋转 Q、K,打分只依赖相对位置,今天的主流)。
- 多头注意力:一个头只有一个注意力分布;多个头可以同时关注不同的位置和关系,计算量和单头相同。
- Transformer 块:多头自注意力 + 前馈网络,每个子层都有残差连接(防止梯度消失和表示坍缩)和层归一化(稳定数值)。Pre-LN 比 Post-LN 更好训练。每块 个参数。
- 三种结构:编码器(双向,BERT)、解码器(因果,GPT)、编码器—解码器(交叉注意力,T5)。
- 代价:注意力的计算和显存是 ,长序列时成为瓶颈。
14. 与作业的关系
Winter 2026 的作业 3(自注意力与 Transformer),以及往年专门讲 Transformer 的那次作业(大约从 Winter 2021 起每年都有,题目名称大致是 Self-Attention, Transformers, and Pretraining,在不同年份是第 4 或第 5 个作业),通常包含:
- 书面题:注意力的探索。 例如:(a) 什么条件下注意力的输出几乎等于某一个值向量(“复制”)?——查询和某个键的点积远大于和其他键的点积(第 2.1 节的 )。(b) 怎样让输出是两个值向量的平均?(c) 如果键带有随机噪声,单头注意力会有什么问题?多头注意力怎样缓解?——和第 6.1 节”一个头只有一个分布”的讨论相关。
- 书面题:位置编码。 例如证明 RoPE 的打分只依赖相对位置(第 5.4 节的复数推导),讨论置换等变性(第 4.1 节)。
- 编程题:在 minGPT(Karpathy 的精简 GPT 实现)的基础上实现注意力、RoPE 等组件,先在文本上预训练,再微调去回答”某人出生在哪里”之类的问题,观察预训练带来的差别——这部分和第 9 讲(预训练)直接相关。
本讲的 transformer.py 和 minGPT 的结构几乎一样,读懂它之后再做作业会轻松很多。
作业的具体题目以当年官方发布的版本为准。
15. 练习
练习 1(手算自注意力)★:两个词,、,、,、,。计算缩放后的打分矩阵、注意力权重和两个词的输出。
参考答案
,,,。除以 :打分矩阵 。
第 1 行 softmax:,输出 。 第 2 行:,权重 ,输出 。
练习 2(缩放)★:假设 和 的每一维独立,均值 0、方差 。(a) 的方差是多少?(b) 应该除以什么数才能让打分的方差为 1?
参考答案
(a) 每一项 的方差是 , 项独立相加,方差 。 (b) 除以 。标准的 对应 的情形;实际中层归一化和初始化让 Q、K 的量级大致可控, 是一个简单而有效的选择。
练习 3(置换等变)★:证明:设 是一个置换矩阵(把 的行打乱),不加位置编码、不加掩码的自注意力满足 。加上因果掩码后,这个性质还成立吗?
参考答案
,同理 ,。打分 :行和列都按同样的方式打乱。softmax 按行进行,对每一行内部元素的打乱是等变的,所以 。输出 (因为 )。
加因果掩码后不成立:掩码是固定的下三角矩阵,打乱输入顺序后,原来能看到某个词的位置可能看不到了。这也是为什么第 5.5 节说”带因果掩码的解码器即使不加位置编码,也能得到一些位置信息”。
练习 4(复制与平均)★★:键 两两正交、长度都为 1,值是 。(a) 怎样选查询 ,使输出近似等于 ?(b) 怎样选 ,使输出近似等于 ?(这里不考虑缩放。)
参考答案
(a) , 很大。此时 ,(),softmax 后 的权重 。 (b) , 很大:、 的打分都是 ,其余是 0,权重趋近 。这也说明了单头注意力的局限:它能”平均”两个值,但平均之后,下游无法再把 和 分开(第 6.1 节)。
练习 5(RoPE)★★:在二维情形下,设 、,。(a) 计算位置 的 和位置 的 ,以及它们的点积。(b) 换成 、 呢?(c) 、 呢?
参考答案
(a) ,,点积 。 (b) 相对距离同样是 1,点积同样是 。 (c) 点积是 ——在这个例子里和 (a) 相同,因为 、 方向相同、 是偶函数。一般情况下 和 不相等(代码 02 演示 4 中,距离 3 和 −3 的打分分别是 −3.724 和 −3.823),所以 RoPE 能区分”在前面”和”在后面”。
练习 6(参数量)★:一个解码器模型:,,32 层,最大长度 4096,用 RoPE(没有位置嵌入表),输出层和词嵌入不共享。用 估算总参数量。(这大约是 LLaMA-7B 的规模;LLaMA 实际用 SwiGLU、RMSNorm、无偏置,数字会略有不同。)
参考答案
每块 ,32 层约 。词嵌入和输出层各 ,合计 。总计约 ,即约 67 亿参数——这就是”7B”模型的由来。
练习 7(计算量)★: 时,序列长度多少时注意力矩阵的计算量开始超过线性层?如果上下文是 128K(131072)个词元,注意力矩阵占多少?
参考答案
。 时比值 ,占比 。
练习 8(掩码):一个编码器处理一批句子,句子被右侧填充到相同长度。(a) 为什么需要填充掩码?(b) 填充掩码应该加在注意力矩阵的行上还是列上?(c) 一个解码器处理右侧填充的批时,为什么可以不加填充掩码?
参考答案
(a) 编码器是双向的,真实的词会关注到右边的填充位置,填充向量没有意义,会污染表示,而且结果会依赖于填充了多少。
(b) 加在列上(被关注的键):任何查询都不应该关注填充位置。填充位置自己作为查询(行)时的输出没有意义,但只要计算损失时忽略它们即可。transformer.py 中 pad_mask[:, None, None, :] 就是对列做掩码。
(c) 因果掩码已经让每个位置只能看到左边;右侧填充的位置都在真实的词右边,真实的词本来就看不到它们。
练习 9(Pre-LN vs Post-LN)★★:考虑 层的 Pre-LN 网络,。(a) 中一定包含哪一项?(b) Post-LN 的 中,这一项还存在吗?
参考答案
(a) 包含单位矩阵 (来自从 一路”加”上去的主干)。无论子层的雅可比矩阵多小,梯度都能通过主干原样传到最底层。 (b) 不存在:每一层的主干都要穿过一次层归一化,, 个 连乘,梯度的大小随层的位置变化,层数越多越不均匀——这就是 Post-LN 需要预热、难以训练很深模型的原因。
练习 10(动手):
(a) 在代码 03 演示 3 中,把任务改成同时取回 4 个位置,需要几个头才能全对?
(b) 在代码 04 中,把 n_layers 改成 1,主谓一致的准确率会怎样?第 11.4 节说”注意力可以分几步传递信息”,一层的模型还能找到主语吗?
(c) 在代码 04 中把位置嵌入去掉(pos="none"),看第 11.4 节的注意力模式有什么变化。
(d) 在代码 05 中,只用 0~1 个修饰语的短句训练(generate_corpus(..., modifier_probs=(0.6, 0.4)),和第 6 讲代码 03 的设置相同),四种位置编码的长度泛化差别会变大吗?
练习 11(思考):第 4.3 节说训练时 Transformer 可以并行,生成时却必须一个词一个词地来。(a) 为什么?(b) 一个长度为 的提示,生成 个新词,不用 KV 缓存时总共要做多少次”位置的计算”?用了 KV 缓存呢?
参考答案
(a) 第 个词要以第 个词为输入,而第 个词只有在上一步生成之后才知道。
(b) 不用缓存:第 次生成要重新计算 个位置,总共 ,约 。用 KV 缓存:提示的 个位置算一次(可以并行),之后每生成一个词只算 1 个新位置,总共 个位置(但每个新位置的注意力仍要和之前所有的键做点积)。代码 04 的 sample 函数为了简单没有用 KV 缓存。
16. 延伸阅读
- Vaswani et al.,Attention Is All You Need(2017):Transformer 的原始论文。
- Jay Alammar,The Illustrated Transformer(博客,2018):最有名的图解。
- Harvard NLP,The Annotated Transformer:逐行注释的 PyTorch 实现。
- Andrej Karpathy,minGPT / nanoGPT 代码,以及视频 Let’s build GPT: from scratch, in code, spelled out(2023):和本讲代码 04 的思路一致。
- Su et al.,RoFormer: Enhanced Transformer with Rotary Position Embedding(2021):RoPE。
- Xiong et al.,On Layer Normalization in the Transformer Architecture(2020):Pre-LN 与 Post-LN。
- Dong, Cordonnier & Loukas,Attention is not all you need: pure attention loses rank doubly exponentially with depth(2021):残差连接与秩坍缩。
- Clark et al.,What Does BERT Look At? An Analysis of BERT’s Attention(2019):注意力头在学什么。
- Kazemnejad et al.,The Impact of Positional Encoding on Length Generalization in Transformers(2023):位置编码与长度泛化,包括不加位置信息的解码器。
- CS224N 课程笔记:Self-Attention & Transformers(John Hewitt 编写)。
- CS336 第 3 讲(同目录):现代 Transformer 的结构选择与超参数。
17. 下一讲预告
到这里,我们已经有了今天所有大模型的核心结构。下一讲(深度学习实践方法与期末项目)暂时离开模型本身,讨论”怎样把一个 NLP 项目做好”:
- 怎样选题、找数据、建立基线;
- 训练 / 验证 / 测试集怎样划分,怎样避免数据泄漏;
- 模型不好使时怎样排查:先在一个小批次上过拟合、读学习曲线、监控梯度;
- 超参数搜索:为什么随机搜索常常比网格搜索好;
- 随机种子带来的波动有多大,怎样判断一个改进是真的还是运气。
本讲的实验里已经出现过这些问题的影子:第 11.2 节的结果和我的预期不一致、多个随机种子的结果有差别——下一讲会系统地讨论怎样面对它们。