云镜收藏

稍后阅读

清单保存在当前浏览器,方便下次回来继续阅读。

清单还是空的

在笔记卡片或正文页点击“加入稍后阅读”即可收藏。

卷1 · 深度学习与 CV 基础

1.3 Transformer 逐层拆解:从 Block 到小 GPT

约 7 分钟阅读
全书目录

展开本章目录与同卷章节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-onlyBERT双向理解类任务(分类/检索)沦为嵌入模型
Decoder-onlyGPT因果 mask生成 + 通用任务大模型时代的主宰
Encoder-decoderT5双向+交叉翻译等 seq2seq特化场景

decoder-only 胜出的原因(面试高频):训练目标最简单(每个位置都预测下一个 token,1 个 token 的数据产出 1 个监督信号)、生成和理解是同一件事(都是”给定前文续写”)、架构最简(没有 encoder-decoder 的交互设计)、天然支持 in-context learning。BERT 式”完形填空”目标学到的表示很好,但生成弱、扩展性叙事输给了 GPT。

2. Block 结构:Pre-Norm 为什么取代了 Post-Norm

原版 Transformer(Post-Norm):

x=LN(x+Attn(x))→x=LN(x+MLP(x))x = \text{LN}(x + \text{Attn}(x)) \quad\text{→}\quad x = \text{LN}(x + \text{MLP}(x))

现代(Pre-Norm,GPT-2 之后所有主流模型):

x=x+Attn(LN(x))→x=x+MLP(LN(x))x = x + \text{Attn}(\text{LN}(x)) \quad\text{→}\quad x = x + \text{MLP}(\text{LN}(x))

区别在 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 内积前按位置旋转一个角度——旋转角随位置线性增长,于是 qm⋅knq_m \cdot k_n 只依赖相对距离 m−nm-n,天然相对位置、天然可外推(配合 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:注意力 4d24d^2(QKV 投影 3d23d^2 + 输出投影 d2d^2)+ MLP 8d28d^2 = 12d212d^2。

验算 GPT-2 small:L=12,d=768L=12, d=768 → 12×12×7682≈85M12 \times 12 \times 768^2 \approx 85M;词表嵌入 50257×768≈38.6M50257 \times 768 \approx 38.6M;位置 1024×768≈0.8M1024 \times 768 \approx 0.8M。合计 ≈124M,和论文对上了。会算这笔账,你就能自己估算任何模型的显存(0.2 章 §7)和 LoRA 目标模块的选择(LoRA 常挂在哪些 12d212d^2 上)。

6. 自测题

  1. Pre-Norm 相比 Post-Norm 的核心优势?(残差主干无变换,梯度直通,百层可训;final norm 补表达力)
  2. 为什么必须有位置编码?RoPE 一句话思想?(注意力置换等变;按位置旋转 q/k 使内积只依赖相对距离)
  3. MLP 占每层参数的比例?它和注意力分工是什么?(约 2/3;位置内加工 vs 位置间搬运)
  4. decoder-only 赢在哪些点?(目标简单数据利用率高、生成理解统一、架构简洁、支持 in-context learning)
  5. 权重 tying 是什么,省多少?(embedding 和输出投影共享,省 V×dV \times d,小词表模型占比可观)
  6. 采样时 temperature=0.1 和 5 分别是什么行为?(接近贪心;接近均匀随机、胡言乱语)
  7. 为什么 generate 时要裁剪 idx 到最近 n_ctx?(位置编码表长度有限——绝对位置的硬伤,RoPE 缓解此问题)

7. 动手作业

  1. 跑通 TinyGPT:用莎士比亚字符级数据(Karpathy input.txt,词表 65)训到 loss ≈ 1.5,generate 出一段”伪莎翁”。对照 Karpathy《Let’s build GPT》逐行验证你的实现。
  2. 算一笔账:你的 TinyGPT(d=256, L=4)参数量是多少?用 sum(p.numel()) 验证。
  3. 把 LayerNorm 换成 RMSNorm、把 top-k 换成 top-p,各写一版。
  4. 挑战:加 dropout 和权重衰减后把 L 加到 6,观察过拟合边界——体会”小数据喂不大模型”。

案例 · 手算:TinyGPT 一次前向的形状流

案例:张量形状从头流到尾(B=4, N=128, D=256, H=4, 词表=65)

  1. 输入 idx:(4, 128) 整数;
  2. tok+pos embedding:(4, 128, 256);
  3. 每个 Block 内:q/k/v 各 (4, 4, 128, 64);注意力分数 (4, 4, 128, 128)——这 26 万个数就是”平方成本”的本体;
  4. 加权求和回 (4, 128, 256),MLP 内部先膨胀到 (4, 128, 1024) 再压回;
  5. 最终 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 的流动