时长 8 小时:§1 FFN(1h)· §2 块的组装(1h)· §3 位置编码(2h)· §4 编码器-解码器与三种架构(1h)· §5 参数与 FLOPs 的整块账(1h)· §6 分词三家(1h)· §A–§C(1h)
对应正课 Lecture 1 的 Transformer 架构部分,以及开头的分词。Lecture 2 的位置编码(固定、可学习)也在这里打基础,RoPE 留到 Day 5。
配套代码
w1day2_checks.py(纯 numpy):FFN 是键值记忆、SwiGLU 的 2/3 规则与 11008、GELU 近似误差、残差流随深度增长与 、正弦编码的旋转性质与点积、投影破坏相对性、三个模型的精确参数量、6N 规则、词表占比、BPE vs WordPiece、Unigram 的 Viterbi§B 参考实现(
code/目录):w1d2_block.py、w1d2_tokenizers.py。今天的目标:能画出一个完整的 Transformer 块并说出每个零件的动机;能从正弦位置编码推出”相对位移是一个旋转”;能精确算出 GPT-2 small 的参数量。
§0 今天的主线
0.1 跳跃二:扔掉 RNN 之后要补什么
Day 1 做完了跳跃一(自注意力)和跳跃三(多头)。现在执行跳跃二:只用注意力,不要循环。 可是 Day 1 已经暴露了自注意力单独工作时的四个缺口:
| 缺口 | Day 1 的证据 | 今天的解法 |
|---|---|---|
| 只会平均,不会逐位置加工 | §2.2 凸包性质 | FFN(§1) |
| 堆深了会坍缩、不好训 | §2.2 秩坍缩 | 残差 + LayerNorm(§2) |
| 不知道顺序 | §1.5 置换等变 | 位置编码(§3) |
| 翻译这类任务要”读一句、写一句” | Week 0 Day 4 §4 | 编码器-解码器 + 交叉注意力(§4) |
Transformer 块 = 注意力 + 四个补丁。 每个补丁都对应一个能定量说清的缺口。今天结束时,你手里会有一张完整的图,并且知道图上每个方框为什么在那里。
0.2 今天要用到的旧结论
- Week 0 Day 2 §4.3–§4.5:LayerNorm 的雅可比是投影;Pre-LN 的残差流按 增长,梯度按 缩小
- Week 0 Day 2 §5.3:残差分支输出按 缩放
- Week 0 Day 3 §2.2:PMI;Week 0 Day 3 §7:BPE 就是压缩
- Week 0 Day 1 §9:显存账;Week 0 Day 5 §3.3:权重绑定
- Day 1 §2.2 凸包与秩坍缩;Day 1 §1.5 置换等变;Day 1 §4.5 QK / OV 电路;Day 1 §5.1 注意力的 FLOPs
§1 前馈网络(FFN):每个位置自己的加工车间
1.1 动机
Day 1 §2.2 证明了注意力的输出是 value 的凸组合:它能把信息从一个位置搬到另一个位置,但不能对单个位置做非线性加工。 仅有注意力的模型,本质上是在反复”平均”。
我们需要一个组件:对每个位置独立地做一个表达力强的非线性变换。
1.2 形式
一个两层 MLP,对序列里每个位置用同一套参数、各自独立地算(所以叫 position-wise,等价于核宽为 1 的卷积)。原始论文 、。
分工一句话:注意力负责位置之间的通信,FFN 负责位置内部的计算。 一个块先通信、再计算,堆 次。
1.3 直觉:FFN 是一张键-值记忆表
把矩阵乘拆开写( 是 的第 行, 是 的第 列):
这和注意力的形式几乎一样:
| 注意力 | FFN | |
|---|---|---|
| 键 | ,来自上下文,每句话不同 | ,训练学到,与上下文无关 |
| 匹配 | ||
| 权重 | softmax,归一化,和为 1 | ,不归一化,多数为 0 |
| 值 | ,来自上下文 | ,训练学到 |
| 槽数 | (序列长度) |
Geva et al.(2021)据此提出:FFN 是一张存储在参数里的键-值记忆。 检测输入里的某种模式(比如”这个词在一个关于首都的句子里”),命中时把 加到输出上(比如提升”Paris”的 logit)。这是后来”知识存在 FFN 里”、知识编辑(ROME 等)一系列研究的起点。
w1day2_checks.py 第 1 组验证了两种写法完全相等(差 );随机输入下每个 token 大约激活一半的槽(0.503,ReLU 对称输入时的期望值)。训练后激活会变得稀疏得多,这是 MoE(Lecture 3)“只激活一部分专家”思路的一个动机。
1.4 为什么是 4 倍,以及激活函数的演化
为什么 ? 没有理论推导,是经验值。但可以说清它的预算含义:FFN 参数 ,注意力 (Day 1 §4.4),FFN 占一层参数的 2/3(第 8 组:GPT-2 small 每层注意力 2.36M、FFN 4.72M)。它也是计算的大头:每 token 前向 对注意力投影的 。所以谈 Transformer 的计算量,大部分其实是在谈 FFN。
激活函数:ReLU → GELU → SwiGLU。
- ReLU(原始 Transformer):负半轴梯度为 0,神经元可能”死掉”。
- GELU(BERT、GPT-2):, 是标准正态 CDF。直觉是”按输入有多大可能是正的来决定保留多少”,一个平滑版的 ReLU。负半轴有小的非零输出(第 3 组:),梯度不会完全消失。GPT-2 用的是 tanh 近似版,与精确版最大相差 (第 3 组),见 §A.2。
- SwiGLU(PaLM、Llama 等现代 LLM):Shazeer(2020)提出的门控变体
一路做非线性、另一路做线性,逐元素相乘当门,和 LSTM 的门(Week 0 Day 4 §3.3)是同一种思想:让网络按输入决定”放多少信息过去”。
2/3 规则。 SwiGLU 有三个矩阵,要保持参数量不变, 要从 缩到 : ✓。Llama 还会向上取整到 256 的倍数以便硬件对齐: 时 11008(第 2 组)。这就是 Llama 7B 那个奇怪的 11008 的来历。
为什么 SwiGLU 更好? 老实说,Shazeer 论文的原话是”我们无法解释为什么这些架构有效,把它们的成功归功于神的仁慈”。经验上它在同样的参数和计算下困惑度更低,于是被广泛采用。这是一个”先有实验、后找解释”的设计,面试时这么答比编一个理由更诚实。
1.5 收益与代价
收益:逐位置的非线性加工能力;参数里的”记忆”。代价:参数和计算的大头;它和注意力一样不看顺序;推理时它是计算密集的而注意力的 KV 读取是访存密集的,这个差别决定了 Day 4 的推理优化方向。
§2 块的组装:残差与 LayerNorm
2.1 一个现代 decoder 块
x ──────────────────────┐
│ │
LN₁ │
│ │
Masked MHA(Day 1) │
│ │
Dropout │
└──────────(+)──────────┘
│
x' ─────────┼───────────┐
LN₂ │
│ │
FFN(§1) │
│ │
Dropout │
└───(+)─────┘
│
x''
个块堆完,最后再加一个 LN,然后接输出头。这是 Pre-LN。原始 Transformer 用的是 Post-LN:。
2.2 为什么是 Pre-LN:复习与补充
Week 0 Day 2 §4.5 已经推过:Pre-LN 有一条一次 LayerNorm 都不经过的残差直通路径,梯度畅通;Post-LN 每层都重新归一化,深的时候靠近输出的参数梯度大 倍,离不开 warmup。原始 Transformer(Post-LN,6 层)用了 4000 步 warmup,这不是巧合。
残差流视角。 Pre-LN 下, 像一条贯穿全网的”总线”,每个子层读它(先 LN 再算)、再把结果加回去。结合 Day 1 §4.5 的 OV 电路:每个注意力头往总线上写一个低秩更新,每个 FFN 往总线上写一个”记忆读出”。模型的最终表示就是所有子层写入之和(加上初始嵌入)。这是阅读可解释性论文最有用的心智模型。
2.3 残差流的增长与
Pre-LN 的代价是残差流不断累加。w1day2_checks.py 第 4 组用完整的”注意力 + FFN”随机块实测每个 token 的 RMS(8 个种子):
| 层数 | 2 | 8 | 32 | 128 |
|---|---|---|---|---|
| 不缩放 | 1.85 | 3.44 | 7.61 | 15.28 |
| 输出投影 | 1.25 | 1.25 | 1.25 | 1.25 |
不缩放时 每乘 4,RMS 约翻倍(: 个近似独立的 分支相加)。把每个残差分支的最后一个线性层(注意力的 、FFN 的 )初始化时乘 ,残差流就与深度无关了。这就是 GPT-2 的做法(Week 0 Day 2 §5.3),Day 3 手写 mini-GPT 时要加上。
2.4 Dropout 放在哪
原始论文在三处用 dropout():注意力权重上、每个子层输出加回残差之前、嵌入 + 位置编码之后。现代 LLM 预训练通常完全关掉 dropout:数据量远大于参数能记住的量,过拟合不是主要矛盾,dropout 只会拖慢训练。小数据(比如 Day 3 的 Tiny Shakespeare)上它仍然有用。
2.5 LayerNorm → RMSNorm
现代模型把 LN 换成 RMSNorm(Week 0 Day 2 §4.4):去掉减均值和 ,只按均方根缩放。省一点计算,效果相当。Llama 系列、Qwen 系列都是 Pre-RMSNorm + SwiGLU + RoPE,这三件套分别来自今天 §2.5、§1.4 和 Day 5。
§3 位置编码
3.1 动机
Day 1 §1.5 证明了自注意力是置换等变的:“dog bites man”和”man bites dog”对它来说是同一袋词。要让模型知道顺序,必须把位置信息注入进去。
3.2 设计空间:从最笨的方案开始
我们要给每个位置 一个向量 ,加到词嵌入上。依次考察几个候选,看每个撞上什么墙:
| 方案 | 问题 |
|---|---|
| (直接用整数) | 数值无界,位置 1000 的量级压倒词嵌入 |
| (归一化到 [0,1]) | 同一个相对距离在长短句里对应不同的数值;推理时 未知 |
| one-hot | 维度随最大长度增长,且任意两个位置”一样远” |
| 可学习向量 | 能用,BERT、GPT-2 都这么做;但训练时没见过的位置没有向量,无法外推(§3.7) |
我们想要的性质:(1) 每个位置唯一;(2) 数值有界;(3) 任意长度都能算(不依赖训练见过);(4) 相对位置容易表达:模型最常需要的是”前一个词""三个词之前”,而不是”第 137 个位置”。
3.3 正弦位置编码:直觉
从二进制计数器出发。 把位置写成二进制:
0: 0 0 0 0 最低位每 1 步翻转一次
1: 0 0 0 1 第二位每 2 步翻转一次
2: 0 0 1 0 第三位每 4 步翻转一次
3: 0 0 1 1 ……
每一位是一个频率不同的方波,高位慢、低位快,组合起来唯一地编码位置。但方波是离散的、不光滑。把方波换成正弦波,就得到了 Vaswani 等人的方案:
每对维度 是一个频率为 的”时钟指针”。 转得最快,波长 ; 转得最慢,波长 ( 时约 47117,第 5 组)。频率按几何级数排列,于是短距离和长距离都有对应的”刻度”。满足性质 (1)(2)(3)。
3.4 关键性质:相对位移是一个旋转
这是正弦编码真正的设计意图,也是 RoPE 的种子。 看第 对维度,用和角公式:
写成矩阵:
把所有 对拼起来,:
向后挪 个位置 = 乘一个固定的旋转矩阵。 w1day2_checks.py 第 5 组: 时,同一个 对不同的 都成立,误差 量级。
意义。 “三个词之前”这个相对关系对应一个线性变换,模型原则上可以用线性层学会它。Vaswani 等人的原文是:“We chose this function because we hypothesized it would allow the model to easily learn to attend by relative positions.” 注意是 “hypothesized”:他们并没有证明模型真的这么用。
3.5 点积只依赖相对距离
由 §3.4 马上得到:
只依赖 。 第 6 组():
| 距离 | 0 | 1 | 4 | 16 | 64 | 256 | 1024 |
|---|---|---|---|---|---|---|---|
| 64.0 | 62.1 | 48.6 | 39.7 | 30.5 | 20.5 | 13.1 |
整体随距离减小,但不单调:第一次回升出现在距离 12,0–1024 之间有 492 步是上升的,远处还会变成负数(4096 以内最小 −8.36)。所以它是”相对位置的一个可用信号”,不是一把干净的距离尺子。
3.6 可惜:投影之后,相对性就没了
§3.4–3.5 的漂亮性质是关于 本身的。但进入注意力的是 和 ,打分展开成四项:
即使只看最后一项, 也只有在 与所有 可交换时才只依赖 ,随便一个学到的 不满足。第 7 组:三对都相距 3 的位置, 时打分完全相同(25.587), 随机时变成 4.935、0.302、2.819。加法式的位置编码经过投影后,相对性被破坏了;中间两项还把内容和位置搅在一起。
这正是 RoPE(Day 5)的出发点:既然”旋转”是我们想要的性质,为什么不在投影之后、直接对 和 做旋转?那样 就严格只依赖 。
3.7 可学习的绝对位置编码
BERT、GPT-2 用一张 nn.Embedding(max_len, d) 直接学每个位置的向量。收益:简单,让数据决定位置表示,效果不比正弦差。代价:硬上限,BERT 是 512、GPT-2 是 1024。超过就没有向量可查(IndexError),就算扩大表格,新位置的向量也没训练过。这是”长度外推”问题的最原始形态(Day 5 §3)。
3.8 两个工程细节
为什么是”加”而不是”拼接”? 拼接 再乘 得到 ;相加再乘得到 ,相当于强制 。加法省了维度,代价是内容和位置共享同一组投影。高维空间里两个随机向量近似正交,模型可以学着把位置信息放在与词义大致正交的子空间里,所以实践中加法够用。
原始论文把词嵌入乘了 (“we multiply those weights by ”)。原因是它的嵌入与输出层权重绑定,初始化尺度小,而正弦编码的每个分量在 ,不放大的话位置信号会压过词义。
§4 编码器、解码器与三种架构
4.1 原始 Transformer
源句 ──► Embedding + PE ──► [ 编码器块 ×6 ] ──► 记忆 (T_src × d) ──┐
双向自注意力 │
+ FFN │ K, V
▼
目标句(右移一位)──► Embedding + PE ──► [ 解码器块 ×6 ] ──► Linear ──► softmax
因果自注意力
+ 交叉注意力 ◄── Q 来自解码器
+ FFN
base 配置:6 + 6 层,,,,dropout 0.1,label smoothing 0.1,共约 65M 参数;Adam(),学习率 ,即 4000 步线性 warmup 后按 衰减。
编码器块 = 双向自注意力(只有 padding 掩码)+ FFN。解码器块多一层交叉注意力: 来自解码器的当前表示, 来自编码器的最终输出。
4.2 交叉注意力就是 Bahdanau 注意力
回看 Week 0 Day 4 §5.2:解码器状态当查询,编码器的所有隐状态当键和值。交叉注意力就是它,只是换成了缩放点积、多头、K/V 各自投影。 所以 Transformer 里其实有两种注意力:自注意力(跳跃一)和交叉注意力(2014 年的原版)。
掩码上:交叉注意力只需要源句的 padding 掩码,不需要因果掩码(源句完整已知)。
推理流程:编码器跑一次得到记忆;解码器自回归地一个 token 一个 token 生成,每一步都去查同一份记忆。这份记忆的 K、V 可以一次算好缓存起来,Day 4 的 KV Cache 对交叉注意力来说是免费的。
4.3 三种架构
| 架构 | 注意力 | 训练目标 | 代表 | 擅长 |
|---|---|---|---|---|
| Encoder-only | 双向 | 掩码语言模型(完形填空) | BERT、RoBERTa | 理解:分类、抽取、句向量(第 4 周 RAG 的检索器) |
| Decoder-only | 因果 | 下一个 token | GPT 系列、Llama、Qwen | 生成;通过提示做一切任务 |
| Encoder-decoder | 编码器双向 + 解码器因果 + 交叉 | 序列到序列 | 原始 Transformer、T5、BART | 输入输出结构清晰的任务:翻译、摘要 |
BERT 为什么不能直接生成? 它的每个位置都能看到两边,训练时从没学过”只根据左边预测右边”。Day 6 §4 展开。
为什么今天的 LLM 几乎全是 decoder-only? 这是 Lecture 3 的第一个问题,先记住几条经常被引用的理由:(1) 目标统一,每个 token 都是一个监督信号(Day 1 §3.1),BERT 只在被遮住的 15% 上有损失;(2) 结构简单,一种块、一种掩码,工程上好扩展;(3) 生成即接口,任何任务都能写成”给定前文续写”,零样本和少样本能力随规模涌现;(4) 推理友好,KV Cache 只有一份。这些理由没有一条是决定性的证明,encoder-decoder 在一些任务上仍有优势,但行业已经收敛。
§5 参数与 FLOPs:整块的账
5.1 参数量:数到每一个
一层(带 bias 和 LN 的 GPT-2 式块,):
| 组件 | 参数 |
|---|---|
| Q/K/V 投影 | |
| 输出投影 | |
| FFN 第一层 | |
| FFN 第二层 | |
| 两个 LayerNorm | |
| 一层合计 |
整个模型 (词嵌入,与输出头绑定)(位置嵌入)(末端 LN)。
w1day2_checks.py 第 8 组,三个模型都精确到个位:
| 模型 | 配置 | 算出来 | 官方 |
|---|---|---|---|
| GPT-2 small | 124,439,808 | 124,439,808 ✓ | |
| BERT-base | ,+ 段嵌入、pooler | 109,482,240 | 109,482,240 ✓ |
| Llama 2 7B | ,SwiGLU,无 bias,不绑定 | 6,738,415,616 | 6.74B ✓ |
要记住的近似:非嵌入参数 。GPT-2 small 是 84.9M,嵌入另有 39.4M,小模型里词表占了近三分之一(§6.4)。
5.2 FLOPs:为什么训练是
一个线性层,,,一个 token:前向 (每个参数一次乘一次加)。反向要算两样东西:
所以反向 = 2 × 前向(第 9 组:4,718,592 对 9,437,184)。每个参数:前向 2 次运算,反向 4 次,训练 6 次。一个 参数的模型在 个 token 上训练:
这就是 Kaplan / Chinchilla 缩放律的计算量口径(Lecture 4)。推理只有前向: 每 token。
注意力的 相关项不在 里。每 token 每层: 和 各 ,共 (不计因果掩码省下的一半)。它追上每层线性部分(,其中注意力投影 、FFN )要到 : 时 4608, 时 24576。第 9 组给出 GPT-2 small 的实例: 时注意力项占每 token 前向的 10%, 时 31%(这里用 Kaplan 的口径 )。
结论:对大模型、常见上下文长度, 是很好的近似;Day 1 §5.1 说的”短序列上瓶颈不在 “,放到整块(加上 FFN)更是如此。
§6 分词:Lecture 1 的另一半
Week 0 Day 3 §7 讲了 BPE 的原理(分词 = 压缩)。Lecture 1 开头还会提到 WordPiece、Unigram 和 SentencePiece,这里补齐。
6.1 BPE(复习)
从字符开始,反复合并出现频次最高的相邻对。GPT 系列用它的字节级版本:基础词表是 256 个字节,任何输入都能编码,永远没有 [UNK]。GPT-2 词表 50257 = 256 字节 + 50000 次合并 + 1 个 <|endoftext|>。
6.2 WordPiece:合并”意外地常在一起”的对
BERT 用 WordPiece。合并规则换了:
直觉:BPE 问”哪对最常见”;WordPiece 问”哪对在一起的次数,远超它们各自的频率所预期的”。这就是 PMI(Week 0 Day 3 §2.2)去掉 log 和常数的版本,本质上是在最大化训练语料在 unigram 模型下的似然增益。
w1day2_checks.py 第 11 组(语料 hug×10、pug×5、pun×12、bun×4、hugs×5):BPE 先合并 u+g(频次 20);WordPiece 先合并 g+s(得分 0.050,而 u+g 只有 0.028)。u 出现 36 次,含 u 的对不稀奇;s 只出现 5 次,但每次都跟在 g 后面。
WordPiece 用 ## 标记词中片段(playing → play, ##ing),编码时从左到右贪心取最长匹配,与 BPE 按合并顺序重放不同。
6.3 Unigram:从大到小剪枝
Unigram(Kudo 2018)方向相反:从一个很大的候选词表出发,给每个子词一个概率,一个词的切分概率是各片段概率之积。训练时反复 (1) 用 EM 估计子词概率,(2) 删掉”删了之后语料似然损失最小”的那批子词,直到词表缩到目标大小。
同一个词有多种切法,推理时用 Viterbi 找概率最大的切分。第 12 组:hugs 的 7 种切法里,hugs 整体()优于 hug + s(),Viterbi 与穷举一致。
独有的好处:subword regularization。 训练时按概率采样不同的切分,让模型见到同一个词的多种拆法,相当于数据增强,对低资源翻译有帮助。
6.4 SentencePiece 与词表大小
SentencePiece(Kudo & Richardson 2018)不是一种新算法,而是一个框架:它把输入当作原始字符流,连空格也当普通字符(记作 ▁),不需要按空格预分词。所以对中文、日文这类没有空格的语言天然友好,而且空白信息不会丢,decode 能还原空格和换行(对照 Week 0 Day 3 §A.9:按空白预分词的实现做不到这一点)。严格说它默认还会做 NFKC 规范化,想要逐字节还原要设 normalization_rule_name="identity"。它实现了 BPE 和 Unigram 两种算法。T5、ALBERT 用 SentencePiece-Unigram;Llama 1/2 用 SentencePiece-BPE 并开启字节回退(未知字符拆成字节)。
词表大小的取舍:词表越大,同样文本的 token 越少(序列短、注意力便宜、上下文装得多),但嵌入和输出层越大。第 10 组:
| 模型 | 嵌入相关参数 | 占总参数 | |
|---|---|---|---|
| GPT-2 small | 50,257 | 38.6M(绑定) | 31.0% |
| Llama 2 7B | 32,000 | 262.1M(不绑定) | 3.9% |
| Llama 3 8B | 128,256 | 1050.7M(不绑定) | 13.1% |
小模型里词表是参数大户;大模型里占比下降,于是有余地用 128k 甚至更大的词表,换更短的序列和更好的多语言覆盖(Week 0 Day 3 §7.4 的”多语言不公平”)。
数字怎么切也是个设计选择:Llama 1/2 把所有数字拆成单个数字;OpenAI 的 cl100k 词表在预分词时把数字按最多 3 位一组切开。这会影响模型做算术的难度。
§A 架构与分词:API 用法与边界
A.1 nn.TransformerEncoderLayer / nn.TransformerDecoderLayer
layer = nn.TransformerEncoderLayer(
d_model=512, nhead=8,
dim_feedforward=2048, # 默认 2048,不是 4·d_model
dropout=0.1, # 默认 0.1
activation="relu", # 默认 relu;可传 "gelu" 或可调用对象
batch_first=False, # ★ 默认 False
norm_first=False, # ★ 默认 False = Post-LN(2017 原版)
)
enc = nn.TransformerEncoder(layer, num_layers=6, norm=nn.LayerNorm(512)) # Pre-LN 时要自己传末端 norm
norm_first=False是 Post-LN。 想要现代的 Pre-LN 必须显式写norm_first=True,并且给TransformerEncoder传norm=作为末端归一化,否则最后一层输出没有归一化。batch_first=False,和nn.MultiheadAttention一样的坑(Day 1 §A.2)。dim_feedforward默认 2048 与d_model无关。d_model=768时不改它,FFN 就只有 2.67 倍而不是 4 倍。nn.TransformerEncoder在norm_first=True时会警告 nested tensor 快速路径被禁用,可以忽略或传enable_nested_tensor=False。- 它不含嵌入、位置编码和输出头,只是块的堆叠。
本周的建议:手写(Day 3)。这些类适合快速搭 baseline,但默认值是 2017 年的配方。
A.2 GELU 的两个版本
F.gelu(x) # 精确版:x·Φ(x)
F.gelu(x, approximate="tanh") # tanh 近似:GPT-2、BERT 原实现用的
nn.GELU(approximate="tanh")
最大差 (第 3 组)。从头训练时无所谓;加载别人训好的权重做推理时要用对版本,否则 logits 有小偏差,生成结果可能不同,对拍时会困惑很久。HuggingFace 的 GPT-2 配置里 activation_function="gelu_new" 就是 tanh 版。
A.3 位置编码的实现
class SinusoidalPE(nn.Module):
def __init__(self, d, max_len=4096, base=10000.0):
super().__init__()
pos = torch.arange(max_len).unsqueeze(1)
w = torch.exp(-math.log(base) * torch.arange(0, d, 2) / d) # 1/base^(2i/d)
pe = torch.zeros(max_len, d)
pe[:, 0::2], pe[:, 1::2] = torch.sin(pos * w), torch.cos(pos * w)
self.register_buffer("pe", pe, persistent=False) # 可重建,不进 state_dict
def forward(self, x): # x: (B, T, d)
return x + self.pe[: x.size(1)]
- 用
exp(-log(base)·2i/d)而不是base ** (2i/d)再取倒数,数值上更稳,写法也更常见。 - 可学习位置编码:
nn.Embedding(max_len, d),前向传torch.arange(T, device=x.device)。忘了指定 device 是高频 bug:索引在 CPU、嵌入表在 GPU,直接报错。max_len时报IndexError(CUDA 上是难读的 device-side assert,Week 0 Day 4 §A.5)。 - 必须是偶数。
A.4 用 HuggingFace tokenizers 训练三种分词器
from tokenizers import Tokenizer, models, pre_tokenizers, trainers
tok = Tokenizer(models.BPE(unk_token="[UNK]")) # 或 models.WordPiece(unk_token=...)、models.Unigram()
tok.pre_tokenizer = pre_tokenizers.Whitespace()
trainer = trainers.BpeTrainer(vocab_size=8000, special_tokens=["[UNK]", "[PAD]"])
# WordPieceTrainer / UnigramTrainer 参数类似;UnigramTrainer 要额外给 unk_token="[UNK]"
tok.train(["input.txt"], trainer)
print(tok.encode("unbelievable").tokens)
special_tokens要在训练时给,它们会占用最前面的 id。训练完再加会导致 id 布局与预期不同。Unigram模型的unk_token要在 trainer 里指定,否则遇到未知字符会报错。- 字节级 BPE 用
pre_tokenizers.ByteLevel()配decoders.ByteLevel(),不需要[UNK]。
A.5 sentencepiece
import sentencepiece as spm
spm.SentencePieceTrainer.train(input="input.txt", model_prefix="sp", vocab_size=8000,
model_type="unigram", # 默认 unigram;也可 "bpe"
character_coverage=0.9995, # 中日文等大字符集用 0.9995,纯英文可用 1.0
byte_fallback=True) # 未覆盖的字符拆成字节,不产生 <unk>
sp = spm.SentencePieceProcessor(model_file="sp.model")
sp.encode("Hello world", out_type=str) # ['▁Hello', '▁world']
character_coverage小于 1 时,最罕见的字符不进词表。不开byte_fallback的话它们会变成<unk>,而且不报错。vocab_size太大而语料太小时训练会失败并提示最大可行值。
§B 实操
- 跑通
w1day2_checks.py,12 组结论都看懂。第 5、7 组放在一起看:正弦编码本身有完美的相对性,投影之后就没了,这是 Day 5 学 RoPE 的全部动机。 - 推导(1h,纸笔):(a) 用和角公式推出 ;(b) 推出 ;(c) 按 §5.1 的表逐项数出 GPT-2 small 的 124,439,808。
- 手写一个 Pre-LN decoder 块(1.5h):MHA 用 Day 1 你自己写的;FFN 支持
relu/gelu/swiglu;带残差和 dropout;输出投影按 初始化。与nn.TransformerEncoderLayer(norm_first=True, batch_first=True, activation="gelu")对拍(复制权重后输出应一致,传因果掩码)。 - 位置编码实验(1h):实现正弦编码,画出
pe的热力图(位置 × 维度),再画 随 的曲线,对照第 6 组。 - 训练三种分词器(1h):在 Tiny Shakespeare 上用
tokenizers分别训 BPE、WordPiece、Unigram(同样的vocab_size),比较同一段文本的 token 数、以及几个词的切法。
参考实现:code/w1d2_block.py(第 3、4 题,含参数量核对)、code/w1d2_tokenizers.py(第 5 题)。
§C 自测
动机与直觉层
- Transformer 块里的四个”补丁”各自对应自注意力的哪个缺口?
- 为什么说”注意力负责通信、FFN 负责计算”?FFN 为什么能被看成键-值记忆?和注意力对比,键、值、权重各有什么不同?
- 为什么需要位置编码?列出四个朴素方案各自的问题,以及理想方案应满足的四条性质。
- 正弦位置编码的”二进制计数器”直觉是什么?频率为什么按几何级数排列?
- 交叉注意力和 Bahdanau 注意力是什么关系?它需要什么掩码?
- 三种架构各适合什么任务?BERT 为什么不能直接生成?
推导层
- 推出 ,写出 。为什么说这是 RoPE 的种子?
- 推出 只依赖 。它随距离单调衰减吗?
- 为什么加法式位置编码经过 之后不再只依赖相对位置?写出打分展开的四项。
- SwiGLU 为什么 取 ?Llama 7B 的 11008 怎么来的?
- 逐项数出 GPT-2 small 的参数量。非嵌入参数的近似公式是什么?
- 为什么反向传播的计算量是前向的 2 倍?由此推出 。
- 每 token 的 相关注意力计算量追上线性部分要多长的序列?
- WordPiece 的合并得分是什么?它和 PMI 有什么关系?举一个 BPE 和 WordPiece 选择不同的例子。
工程与 API 层
nn.TransformerEncoderLayer默认是 Pre-LN 还是 Post-LN?dim_feedforward默认是多少?- 加载 GPT-2 权重时 GELU 要用哪个版本?用错了会怎样?
- 可学习位置编码最常见的两个 bug 是什么?
- SentencePiece 和 BPE 是什么关系?
byte_fallback解决什么问题? - 为什么大模型敢用 128k 的词表而小模型不太敢?
出关标准
- 能画出 Pre-LN decoder 块和原始 encoder-decoder 架构,说出每个零件的动机
- 能讲清 FFN 的键-值记忆视角,知道 FFN 占一层参数的 2/3
- 能说出 ReLU → GELU → SwiGLU 的演化和 2/3 规则
- 能用实测数字说明 缩放的作用
- 推出 ,并说明它为什么经过投影后失效
- 能讲清三种架构的区别和 decoder-only 胜出的常见理由
- 精确算出 GPT-2 small 的参数量,推出
- 能讲清 BPE / WordPiece / Unigram / SentencePiece 的区别
- 手写 Pre-LN 块,与
nn.TransformerEncoderLayer对拍通过