卷1 · 深度学习与 CV 基础
1.3 Transformer 逐层拆解:从 Block 到小 GPT
展开本章目录与同卷章节
1. 宏观地图:三种架构,为什么 decoder-only 赢了2. Block 结构:Pre-Norm 为什么取代了 Post-Norm3. 逐组件拆解3.1 输入端:token embedding + 位置信息3.2 注意力层3.3 MLP:不比注意力次要3.4 残差流视角(建立这个心智模型)4. 手写小 GPT(完整可运行)5. 参数量的账(必须会算)6. 自测题7. 动手作业案例 · 手算:TinyGPT 一次前向的形状流8. 延伸资源同卷章节1.1 从 CNN 到 ResNet:卷积时代的遗产1.2 注意力机制:从加权检索到万物的基础操作1.3 Transformer 逐层拆解:从 Block 到小 GPT1.4 ViT 与视觉 Transformer 谱系:图像成为 token1.5 自监督三巨头:MAE、CLIP、DINO1.6 CLIP 精读与 zero-shot 评测实操(第一个完整项目)1.7 SAM 与分割基础模型:提示式视觉1.8 检测与分割极简史:知道即可的一章1.9 图像形成与相机模型:给 3D/具身留的几何底座本章目标:把 1.2 的注意力装进完整的 Transformer,逐个组件搞清”它解决什么问题、现代版本是什么”,最后手写一个能跑的小 GPT。这一章是卷2(LLM 内功)的地基——学完它,nanoGPT 对你只剩工程细节。前置:1.2(注意力)、0.3(RMSNorm)。
1. 宏观地图:三种架构,为什么 decoder-only 赢了
| 架构 | 代表 | 注意力 | 擅长 | 命运 |
|---|---|---|---|---|
| Encoder-only | BERT | 双向 | 理解类任务(分类/检索) | 沦为嵌入模型 |
| Decoder-only | GPT | 因果 mask | 生成 + 通用任务 | 大模型时代的主宰 |
| Encoder-decoder | T5 | 双向+交叉 | 翻译等 seq2seq | 特化场景 |
decoder-only 胜出的原因(面试高频):训练目标最简单(每个位置都预测下一个 token,1 个 token 的数据产出 1 个监督信号)、生成和理解是同一件事(都是”给定前文续写”)、架构最简(没有 encoder-decoder 的交互设计)、天然支持 in-context learning。BERT 式”完形填空”目标学到的表示很好,但生成弱、扩展性叙事输给了 GPT。
2. Block 结构:Pre-Norm 为什么取代了 Post-Norm
原版 Transformer(Post-Norm):
现代(Pre-Norm,GPT-2 之后所有主流模型):
区别在 Norm 包住的是子层输入还是子层输出。Pre-Norm 的残差主干上没有任何变换,梯度从顶层直通底层不经过任何 LN/非线性——深层网络(百层)训练稳定的关键。代价是表达力略降,所以现代模型在最后加一个 final norm(你读 Qwen/LLaMA 代码时看到的 ln_f 就是它)。0.3 章讲过 Norm 本身,现在你有了它为什么”在这里”的完整答案。
3. 逐组件拆解
3.1 输入端:token embedding + 位置信息
注意力本身是置换等变的——打乱输入顺序,输出跟着同样打乱,“位置”完全丢失。序列任务必须显式注入位置:
- 正弦编码(原版):固定三角函数,相对位置可线性表达;
- 可学习绝对位置(GPT-2):一张
nn.Embedding(n_ctx, d)表,简单有效,但不能外推到训练长度之外; - RoPE(旋转位置编码):现代主流(LLaMA/Qwen/GLM 全家)。直觉版:把 q、k 的维度两两配对,在 attention 内积前按位置旋转一个角度——旋转角随位置线性增长,于是 只依赖相对距离 ,天然相对位置、天然可外推(配合 NTK/YaRN 等缩放技巧做长上下文)。不需要会推导,需要会一句话讲清思想和好处。
3.2 注意力层
1.2 章已手写。现代补充细节:qkv 用一个矩阵一次投影(省两次 GEMM);对 q、k 做 norm(QK-Norm,稳定大模型训练的新趋势,Qwen3 等在用)。
3.3 MLP:不比注意力次要
原版:Linear(d → 4d) → GELU → Linear(4d → d),占每层约 2/3 参数(见第 5 节的账)。现代升级为 SwiGLU:三个矩阵(gate/up/down),down(silu(gate(x)) * up(x))——门控让网络自己决定”哪些特征放行”。LLaMA/Qwen 全在用;为保持参数量与 4d 版本对齐,中间维度取约 8d/3。注意力的作用是”位置间搬运信息”,MLP 的作用是”位置内加工信息”——可解释性研究(残差流视角)的基本分工认知。
3.4 残差流视角(建立这个心智模型)
把 Block 看成:残差主干是一条”信息高速公路”,注意力层和 MLP 层以加法方式向主干读写信息。训练后期大量层接近恒等映射。这个视角来自可解释性研究,但工程上同样有用:调 bug 时想”信息怎么流的”比想”第几层算错了”更有效。
4. 手写小 GPT(完整可运行)
import torch, torch.nn as nn, torch.nn.functional as F, math
class CausalSelfAttention(nn.Module):
def __init__(self, d, h, n_ctx):
super().__init__()
self.h, self.dh = h, d // h
self.qkv = nn.Linear(d, 3 * d, bias=False)
self.proj = nn.Linear(d, d, bias=False)
mask = torch.tril(torch.ones(n_ctx, n_ctx)).view(1, 1, n_ctx, n_ctx)
self.register_buffer("mask", mask) # 随模型保存/迁移,不参与训练
def forward(self, x):
B, N, D = x.shape
q, k, v = self.qkv(x).split(D, dim=2)
q, k, v = (t.view(B, N, self.h, self.dh).transpose(1, 2) for t in (q, k, v))
att = (q @ k.transpose(-2, -1)) / math.sqrt(self.dh)
att = att.masked_fill(self.mask[:, :, :N, :N] == 0, float("-inf"))
y = F.softmax(att, dim=-1) @ v
y = y.transpose(1, 2).contiguous().view(B, N, D)
return self.proj(y)
class Block(nn.Module):
def __init__(self, d, h, n_ctx):
super().__init__()
self.ln1, self.ln2 = nn.LayerNorm(d), nn.LayerNorm(d)
self.attn = CausalSelfAttention(d, h, n_ctx)
self.mlp = nn.Sequential(nn.Linear(d, 4 * d), nn.GELU(), nn.Linear(4 * d, d))
def forward(self, x): # Pre-Norm
x = x + self.attn(self.ln1(x))
x = x + self.mlp(self.ln2(x))
return x
class TinyGPT(nn.Module):
def __init__(self, vocab, d=256, h=4, L=4, n_ctx=128):
super().__init__()
self.tok = nn.Embedding(vocab, d)
self.pos = nn.Embedding(n_ctx, d)
self.blocks = nn.ModuleList([Block(d, h, n_ctx) for _ in range(L)])
self.ln_f = nn.LayerNorm(d)
self.head = nn.Linear(d, vocab, bias=False) # 可与 tok.weight 绑定(权重 tying)
self.n_ctx = n_ctx
def forward(self, idx):
x = self.tok(idx) + self.pos(torch.arange(idx.size(1), device=idx.device))
for blk in self.blocks: x = blk(x)
return self.head(self.ln_f(x))
@torch.no_grad()
def generate(model, idx, n_new=60, temperature=1.0, top_k=20):
for _ in range(n_new):
logits = model(idx[:, -model.n_ctx:])[:, -1] / temperature # 只看最近 n_ctx
if top_k:
kth = logits.topk(top_k, dim=-1).values[..., -1, None]
logits = logits.masked_fill(logits < kth, float("-inf"))
probs = F.softmax(logits, dim=-1)
idx = torch.cat([idx, torch.multinomial(probs, 1)], dim=1)
return idx
训练循环就是 0.2 章那套:logits = model(x); loss = F.cross_entropy(logits[:, :-1].reshape(-1, V), y[:, 1:].reshape(-1))——注意输入右移一位(预测下一个 token)。加上 0.4 章的四件套,你就有了一个完整的 nanoGPT。
5. 参数量的账(必须会算)
每个 Block:注意力 (QKV 投影 + 输出投影 )+ MLP = 。
验算 GPT-2 small: → ;词表嵌入 ;位置 。合计 ≈124M,和论文对上了。会算这笔账,你就能自己估算任何模型的显存(0.2 章 §7)和 LoRA 目标模块的选择(LoRA 常挂在哪些 上)。
6. 自测题
- Pre-Norm 相比 Post-Norm 的核心优势?(残差主干无变换,梯度直通,百层可训;final norm 补表达力)
- 为什么必须有位置编码?RoPE 一句话思想?(注意力置换等变;按位置旋转 q/k 使内积只依赖相对距离)
- MLP 占每层参数的比例?它和注意力分工是什么?(约 2/3;位置内加工 vs 位置间搬运)
- decoder-only 赢在哪些点?(目标简单数据利用率高、生成理解统一、架构简洁、支持 in-context learning)
- 权重 tying 是什么,省多少?(embedding 和输出投影共享,省 ,小词表模型占比可观)
- 采样时 temperature=0.1 和 5 分别是什么行为?(接近贪心;接近均匀随机、胡言乱语)
- 为什么 generate 时要裁剪 idx 到最近 n_ctx?(位置编码表长度有限——绝对位置的硬伤,RoPE 缓解此问题)
7. 动手作业
- 跑通 TinyGPT:用莎士比亚字符级数据(Karpathy
input.txt,词表 65)训到 loss ≈ 1.5,generate出一段”伪莎翁”。对照 Karpathy《Let’s build GPT》逐行验证你的实现。 - 算一笔账:你的 TinyGPT(d=256, L=4)参数量是多少?用
sum(p.numel())验证。 - 把 LayerNorm 换成 RMSNorm、把 top-k 换成 top-p,各写一版。
- 挑战:加 dropout 和权重衰减后把 L 加到 6,观察过拟合边界——体会”小数据喂不大模型”。
案例 · 手算:TinyGPT 一次前向的形状流
案例:张量形状从头流到尾(B=4, N=128, D=256, H=4, 词表=65)
- 输入 idx:(4, 128) 整数;
- tok+pos embedding:(4, 128, 256);
- 每个 Block 内:q/k/v 各 (4, 4, 128, 64);注意力分数 (4, 4, 128, 128)——这 26 万个数就是”平方成本”的本体;
- 加权求和回 (4, 128, 256),MLP 内部先膨胀到 (4, 128, 1024) 再压回;
- 最终 head:(4, 128, 65) logits → 对 127 个位置算 next-token loss。
参数量(§5 公式):每 Block 12×256²=786K,4 个 Block=3.15M;加词表嵌入 65×256≈17K——
sum(p.numel())应约 3.2M。形状对不上 = 90% 的 reshape/transpose bug(0.2 §2.2 的三兄弟再查一遍)。
8. 延伸资源
- Karpathy《Let’s build GPT: from scratch, in code, spelled out》——与本章一一对应,必跟
- nanoGPT repo——工业级微缩版(DDP、配置系统、都在)
- Jay Alammar《The Illustrated GPT-2》——图解推理时 KV 的流动
