深度学习基础:从反向传播到残差
系列精华 · 六篇正文每篇一页,按 ↓ 看实验与原论文图
arganzheng · 2026-09-29
这个系列回答一个问题
深网络训练里的每个现象,能不能推到公式、代到数字、用几十行代码复现,然后在 LLM 里认出它的形态?
- 主线是一条连乘链:前向的方差每层乘一个因子、反向的梯度是一串 Jacobian 的乘积——因子偏离 1 就指数放大或消失
- 01 写出这串乘积 → 02 它怎么坏、三样东西各修哪一环 → 03 步长怎么定 → 04 挑出的解为什么泛化 → 05 / 06 在卷积与循环上再看一遍
- 六篇用同一份几百行的 NumPy 小框架,全部实验笔记本 CPU 几分钟跑完
全景:一条连乘链
%%{init: {"flowchart": {"wrappingWidth": 190}}}%%
flowchart TB
D1["01 反向传播<br/>写出那串 Jacobian 的乘积"] --> D2["02 训练为什么不稳定<br/>初始化 / 归一化 / 残差各修哪一环"]
D2 --> D3["03 优化器<br/>拿到稳定的梯度后步长怎么定"]
D3 --> D4["04 正则化与泛化<br/>挑出的解为什么泛化、何时不再"]
D2 --> D5["05 CNN<br/>ResNet 的退化问题 = 连乘的历史形态一"]
D2 --> D6["06 RNN<br/>BPTT 衰减 = 历史形态二;LSTM 遗忘门 ≈ 残差"]
D6 -. "seq2seq 瓶颈 → attention 诞生" .-> T["→ L4 Transformer"]
D5 -. "patch → token(ViT)" .-> T
01 · 反向传播:手推一个两层网络
结论:反向传播 = 沿计算图反向拓扑序对每个算子算一次 VJP,从不构造 Jacobian;每个 Linear 反向做两个 GEMM,所以反向 = 2 × 前向、训练 \(6ND\);\(\partial L/\partial W = X^\top G\) 需要本层输入,所以激活要存。

三个公式与两个数字
| 量 |
公式 |
形状检查 |
| 输出层梯度 |
\(G = (P - Y)/m\) |
与 logits 同形 |
| 权重梯度 |
\(\partial L/\partial W = X^\top G\) |
\([n_{in}, m] \times [m, n_{out}]\)——需要本层输入 \(X\) |
| 传给下一层 |
\(\partial L/\partial X = G\,W^\top\) |
\([m, n_{out}] \times [n_{out}, n_{in}]\) |
- 一个 20 万参数网络的一层 Jacobian 就有 13 GB——所以只算 \(J^\top v\)
- 激活 552 KiB 对权重 795 KiB,batch × 32 → 17.3 MiB:激活显存与 batch × 序列 × 层数 × 宽度成正比,与参数量无关;重算用 33% 算力换掉(\(8ND\))
02 · 训练为什么不稳定:初始化、归一化与残差
结论:深网络的稳定性是连乘问题;初始化只管 \(t = 0\),归一化切断前向的连乘但修不了反向,残差把 Jacobian 变成 \(I + J\) 给梯度一条恒等通路;三样必须同用,Pre-Norm 是当前摆法。

七种接法只有 Pre-Norm 两个学习率都能训

- Kaiming 初始化的 64 层 plain 网络:梯度范数在层间 1.7 到 13,lr 0.05 三步 NaN——初始化只管 \(t = 0\)
- 加了 LN 不加残差:300 步 37%;Post-Norm 顶层梯度是底层 4 倍,lr 0.05 时 8.8% 对 Pre-Norm 91.6%
Post-LN vs Pre-LN(Xiong 等 2020)

- 残差流无归一化每层翻倍(\(2^{64}\)),Pre-Norm 下线性增长;GPT-2 把残差分支缩放 \(1/\sqrt{2L}\);LLM 用 Pre-Norm 加 final norm
03 · 优化器:从 SGD 到 AdamW 与学习率调度
结论:Adam 让每个参数的步长 \(\approx \eta\)、与梯度大小无关——所以第一步是满步长 \(\eta \cdot \text{sign}(g)\)、必须 warmup;\(L_2\) 被 \(1/\sqrt v\) 缩放而 AdamW 不被;线性 scaling 只在临界 batch 内成立。

warmup 为什么不能省

- 偏差修正后第一步是 \(\eta \cdot \text{sign}(g)\),\(v\) 还没学到尺度
- batch 翻倍 lr 翻倍:512 成立、2048 NaN——只在「\(k\) 步内梯度基本不变」时成立;裁剪 1.0 限制步长上界
Adam + L2 不是 AdamW(Kingma & Ba 2014 Algorithm 1)

- \(\lambda\theta\) 混进 \(g\) 被 \(1/\sqrt v\) 缩放,梯度小的参数被过度衰减;同一 \(\lambda = 0.1\):\(\lVert W_1\rVert\) 2.33 对 23.59,准确率 86.2% 对 95.6%
- \(\beta_2 = 0.95\) 记 20 步;Momentum 有效学习率 \(\times 1/(1-\beta)\)
04 · 正则化与泛化:为什么参数比样本多却不过拟合
结论:先算参数 / 数据比定体制;过参数化时优化器挑最小范数、平坦、先学简单的解——隐式正则化;double descent 越过插值阈值再变好;预训练在数据体制里不用 dropout、一个 epoch。

过拟合最常见的形态:loss 涨而准确率不动

- 测试 loss 0.40 → 0.55 而准确率不动——是过度自信;dropout 0.5 到 300 epoch 反而最差(0.62)
- 重复数据 4 epoch 以内几乎无损;2 万字符第 16 epoch 拐点、记忆率 10%,200 万字符未到
05 · CNN:从 LeNet 到 ResNet,再到 ViT
结论:卷积 = 全连接 + 局部性 + 参数共享两条约束;深度是为了感受野,深了就训不动——BN 修前向、残差修反向;数据够多时约束成负担,ViT 把图切成 token,只留下 patch embedding 这一个 stride 卷积。


- plain 56 层 loss 0.72、首末层梯度比 2849——退化问题就是连乘;bottleneck 69K 参数对 1.18M
- ResNet-50 25.6M 参数、8.2 GFLOPs(4.1 数的是 MACs)、每参数用 320 次
- ViT:196 / 576 / 5476 个 token;patch embedding = kernel = stride = p 的卷积,两种写法差 \(10^{-6}\)、参数同为 590,592
案例:复现 LeNet-5

- 61,706 个参数、5 个 epoch、0.82%——MLP 的 1/3 参数、1/3 错误率;第一层学出边缘检测器
06 · RNN:从 LSTM 到 attention 的诞生
结论:BPTT 是同一个 \(W\) 的连乘,训练信号传不到远处;LSTM 的 \(c_t = f_t \odot c_{t-1} + i_t \odot \tilde c_t\) 是时间上的残差,遗忘门偏置要初始化为 1;attention 为绕过 seq2seq 的固定向量瓶颈而生,然后人们发现循环可以不要。

从固定向量瓶颈到对齐(Bahdanau 等 2015)

- seq2seq 把整句压进一个向量(Sutskever 2014);倒序 16 token 任务整句正确率 0% → 76%
- attention 算力 4.5 倍、\(O(T^2 d)\) 对 \(O(Td^2)\)——换来的是可并行与不衰减的通路
案例:字符级 LSTM 写莎士比亚,与 nanoGPT 同预算

- 这个规模看不出 Transformer 的优势——差别在顺序 vs 并行;RNN 记不住远处不是隐状态太小(64 维装得下 69 bit),是训练信号传不到
%%{init: {"flowchart": {"wrappingWidth": 210}}}%%
flowchart TB
CHAIN["梯度 = Jacobian 连乘(01、02)"] --> VAN["因子偏离 1 → 指数消失 / 爆炸(02、05、06)"]
VAN --> INIT["初始化:t = 0 时因子期望为 1"]
VAN --> NORM["归一化:前向方差拉回 1"]
VAN --> RES["残差:I + J 的恒等通路"]
RES --> GROW["残差流方差增长 → 归一化 + 1/√(2L)"]
RES --> LSTM["LSTM 遗忘门 = 时间上的残差(06)"]
RES --> PRE["Pre-Norm / ResNet-v2(02、05)"]
NORM --> SCALE["尺度不变性 → 有效学习率由范数决定"]
SCALE --> WD["AdamW weight decay = 范数平衡点(03、04)"]
ADAM["Adam 步长 ≈ η(03)"] --> WARM["warmup、β₂ = 0.95、η ∝ 1/n_in"]
常见误区
- 反向传播要算出每层的 Jacobian——一层就 13 GB,只算 VJP
- 激活显存与参数量成正比——与 batch × 序列 × 层数 × 宽度成正比
- 初始化对了深网络就能训——初始化只管 \(t = 0\),64 层 plain 三步 NaN
- Adam + L2 就是 AdamW——\(\lambda\theta\) 被 \(1/\sqrt v\) 缩放,86.2% 对 95.6%
- 参数比样本多就会过拟合——407 倍的网络测试 loss 最好;别停在插值阈值
- 验证准确率没掉就没过拟合——loss 涨而准确率不变是过度自信
- ViT 完全不用卷积——patch embedding 就是 stride 卷积
- RNN 记不住远处是隐状态太小——是训练信号传不到