Week 4
Week 4 - Backpropagation:让网络里的所有参数一起学习
Key questionLoss 怎样把责任传回每个 weight?
Learning objectives
- 理解并应用 Week 4 - Backpropagation:让网络里的所有参数一起学习
55 min estimated reading time
Week 3 已经能算一个网络的预测;本周的问题是:最终 Loss 只有一个数,怎样分别知道每个参数该往哪边调?不靠每个参数轮流试很多次,而是复用 forward 的中间结果计算局部影响。
Scroll horizontally to view all columns.
| 分次安排 | 本次产物 |
|---|---|
| 一:保存依赖 | 固定 x=2、y=5、w1=w2=1、b1=b2=0;算出 z=h=prediction=2、Loss=9。 |
| 二:逐条反传 | 得到 dw2=−12、db2=−6,再向前传得 dw1=−12、db1=−6。 |
| 三:分支与更新 | 解释梯度沿路径相乘、在汇合处相加;一起更新后再预测。 |
| 四:独立核对 | 运行 course_examples/week04_gradient_check.py;补做提前更新的错误对照。 |
前置:Week 2 的局部变化率和 Week 3 的 ReLU。本周只使用一个隐藏神经元,减少算术负担;这不是上一周两隐藏单元的参数被悄悄改变,而是同一算法的更小演示。
Week 4 核心目标
Week 2 已经解释了一个参数怎样通过Gradient Descent 学习。
Week 3 把一个参数扩展成大量neurons、layers、weights 和biases,并在最后留下四个问题:
w₁ 应该修改多少?
w₂ 应该增加还是减少?
某个weight 对最终错误负多少责任?
很前面的weight 怎样收到Loss 的反馈?
Backpropagation(反向传播)就是答案。
本周只需要真正理解四件事:
- Computational Graph;
- Local Derivative;
- Chain Rule;
- Gradient 怎样从Loss 一层层传回每个parameter。
核心一句话:
Backpropagation 从Loss 开始,沿计算依赖关系反向移动,用Chain Rule 计算每个parameter 对Loss 的影响。
1. Backpropagation 不是新的Training 方法
完整训练过程仍然是:
Forward Pass
↓
Prediction
↓
Loss
↓
Backpropagation
↓
Gradients
↓
Optimizer / Gradient Descent
↓
Updated Parameters
其中:
- Backpropagation 负责算gradients;
- Gradient Descent 或optimizer 负责用gradients 更新parameters。
不要把二者混成一件事。
2. Computational Graph
假设一个很小的网络:
依赖关系是:
x, w₁, b₁
↓
z
↓ReLU
h
↓with w₂, b₂
ŷ
↓compare with y
L
这就是Computational Graph:把一个大公式拆成一组小操作,并记录谁依赖谁。
对程序员来说,它像运行时建立的一张dependency graph。
为什么要把公式拆成Graph
如果直接把整个网络写成一个巨大公式,理论上也可以一次求导,但工程上几乎不可维护。真实网络里同一个tensor 可能被多个layer 使用,一个parameter 也可能通过多条路径影响Loss。Computational Graph 把复杂问题变成一组统一的小问题:
这个operation 收到哪些inputs?
它产生哪个output?
它的output 被谁继续使用?
它的local derivative 是什么?
这很像编译器的中间表示或服务之间的dependency graph:每个节点只描述自己的行为,图负责保存整体依赖关系。这样无论网络有十个操作还是十亿个操作,Backward 使用的仍然是同一套遍历规则。
Graph 中真正保存的是什么
Graph 不只是保存“调用顺序”,还要保存backward 需要的上下文。例如乘法:
Backward 时需要知道forward 当时的a 和b,因为:
因此,forward 中保存中间结果并不是浪费内存,而是在用内存换取backward 所需的信息。这也是训练通常比纯inference 占用更多显存的重要原因之一。
3. Forward Pass:先保存中间结果
本周另开一个更小的网络:只有一个隐藏神经元,方便沿每条边反传。不要沿用 Week 3 的两个隐藏单元。先在纸上记录本节 x、w₁、b₁、w₂、b₂,以及 z₁、a₁、预测、loss;后面直到第 11 节更新前,都使用这张旧状态记录。
设定:
x = 2
y = 5
w₁ = 1
b₁ = 0
w₂ = 1
b₂ = 0
Forward:
需要保存的中间值包括z、h 和ŷ,因为backward 时计算局部导数会再次使用它们。
Forward 不只是在“做预测”
Training 时的forward 同时完成两件事:
- 计算prediction 和loss;
- 为之后的backward 留下计算路径和必要的中间值。
Inference 只需要第一件事,所以通常可以关闭gradient tracking。Training 不能随意丢掉所有中间结果,否则走到backward 时就不知道ReLU 当时输入是正还是负,也不知道乘法节点应该把哪个值乘到上游gradient 上。
4. Local Derivative:每个小操作只负责自己
Backprop 不会一次求完整大公式。每个节点只回答:
我的某个input 稍微变化时,我的output 会变化多快?
更准确地说,Local Derivative(局部导数)回答的是:
在这个小operation 内,某个input 稍微变化一点,它的output 会变化多快?
“Local” 的意思是它不需要知道整个network,也不需要知道最终Loss 是多少。乘法节点只懂乘法,ReLU 节点只懂ReLU。最终Loss 对某个parameter 的影响,由这些局部关系沿graph 组合出来。
可以把每个节点想成一个实现统一接口的组件:
forward(inputs) → output
backward(upstream_gradient) → input_gradients
forward 产生值;backward 接收后面传来的影响,再结合自己的local derivative,把影响继续传给前面的inputs。
几个常见局部导数:
加法
为什么都是1?假设c = a + b,固定b:a 增加0.01,c 也正好增加0.01。因此output 对这个input 的变化倍率是1。加法节点不会放大或缩小gradient,只会把收到的gradient 原样分给两个inputs。
乘法
例如c = a × b,forward 时a = 3、b = 4。如果固定b,让a 增加0.01:
原来c = 3 × 4
= 12
现在c = 3.01 × 4
= 12.04
a 增加0.01,c 增加0.04,变化倍率是4,正好等于另一个input b。这就是乘法节点的local derivative,不是需要死记的神秘规则。
平方误差
这里的导数同时编码了方向和严重程度:
- 如果prediction 高于target,ŷ-y 为正,gradient 为正;
- 如果prediction 低于target,ŷ-y 为负,gradient 为负;
- 错得越远,gradient 的绝对值越大。
因此平方误差不仅告诉模型“错了多少”,它的导数还告诉模型“prediction 应该往哪边移动”。
ReLU
在z = 0 的特殊点,框架会选择一个约定值;当前不影响我们的核心理解。
ReLU 的local derivative 像一个gradient gate:
- forward 时z > 0
- gate 打开
- gradient 乘1,继续通过
- forward 时z < 0
- gate 关闭
- gradient 乘0,这条路径停止
注意,ReLU 并不是凭空“生成” gradient。它只是根据forward 时的输入,决定收到的上游gradient 有多少可以继续传回去。
Local Derivative 怎样真正参与Backward
一个节点在backward 时执行的核心规则是:
例如乘法节点c = ab 收到:
upstream gradient = ∂L/∂c = -6
forward 时a = 3
forward 时b = 4
那么它传给两个inputs 的gradients 是:
这就是截图中几条local derivative 公式真正的用途:它们不是孤立的求导表,而是每个节点把“Loss 的责任”继续向前传递时使用的倍率。
5. Chain Rule:把局部影响串起来
本节为了单独展示“沿路径相乘”,可以临时更换某个局部系数;这不表示第 3 节保存的主实验参数已经更新。回到第 6 节时继续使用主实验旧状态。链式法则先解决怎样计算影响,optimizer 才决定什么时候改参数。
每一项只是相邻两个节点之间的局部影响。
为什么相乘?因为:
w₁ 改一点
↓影响z
z 再影响h
↓
h 再影响prediction
↓
prediction 再影响Loss
最终影响等于每一段影响的乘积。
为什么是相乘,而不是相加
假设一个很小的变化经过三段:
w₁ 增加0.01
z 大约增加2 × 0.01
h 大约增加1 × z 的变化
ŷ 大约增加3 × h 的变化
最终:
每一段都是上一段变化的倍率,所以倍率自然连续相乘。Chain Rule 只是把这件事写成精确的导数形式。
用程序员熟悉的Dependency Chain 理解
如果:
Tax = Tax(Subtotal(Price(quantity, unitPrice)))
unitPrice 改动对最终Tax 的影响,必须依次经过Price、Subtotal、Tax。任何一段完全不受输入影响,最终影响就会变成0。Neural Network 的backward 做的也是沿依赖链追踪影响,只是graph 更大、分支更多。Chain Rule 计算的是一阶导数
Backpropagation 主要计算Loss 对parameters 的一阶导数。它不需要先计算二阶导数:一阶gradient已经能告诉optimizer 哪个方向会让Loss 上升,因此取反方向就能下降。
二阶导数描述Loss 曲面的弯曲程度,某些高级优化方法会使用,但完整神经网络的二阶导数会形成巨大的Hessian matrix,成本很高。GPT 训练通常使用SGD、AdamW 等一阶优化方法。
6. 从Loss 开始反向计算
Forward 得到:
ŷ = 2
y = 5
L = 9
Backward 的逻辑起点其实是:
一个值对自己的变化率当然是1。可以理解成在Loss 节点放入一个初始信号1,然后让它通过每个节点的local derivative 向前传播。
为什么最终通常要求Loss 是scalar?因为scalar 只有一个明确的起点。如果output 是一个vector,就需要先说明我们想对这个vector 做怎样的加权组合,才能决定backward 的初始方向。训练中把一个batch的各项损失求和或求平均,正是为了得到一个scalar objective。
接下来第一步是计算Loss 对prediction 的影响。
第一步:
这个-6 是从Loss 向前一个节点传出的gradient。
它表示:在当前位置稍微增加prediction,会让Loss 下降。
7. Output Layer 的gradients
Output:
对w₂:
Chain Rule:
对b₂:
所以:
现在Output Layer 的两个parameters 都知道应该怎样改变。
9. Gradient 经过ReLU
Forward 时:
z = 2 > 0
因此:
所以:
如果Forward 时z < 0,ReLU 的局部导数为0,这条路径的gradient 就会被挡住。
11. 全部梯度算完,才一起更新参数
到这里我们还没有改动任何参数。所有梯度都描述同一次旧 forward 的 Loss。学习率设为 0.01,现在才执行“旧参数减去学习率乘梯度”。
Scroll horizontally to view all columns.
| 参数 | 旧值 | 本次梯度 | 更新后 |
|---|---|---|---|
| w1 | 1 | −12 | 1.12 |
| b1 | 0 | −6 | 0.06 |
| w2 | 1 | −12 | 1.12 |
| b2 | 0 | −6 | 0.06 |
为什么不能算完 dw2 就先更新 w2?旧 forward 中 dL/dprediction=−6,向隐藏值传递时应乘旧 w2=1,所以 dL/dh=−6。若偷用新 w2=1.12,会得到 −6.72,再乘输入 2 得到错误 dw1=−13.44;正确是 −12。你混合了两个时刻的计算。
θ 只是四个参数的统称;∇L 表示这四个对应梯度。backward 负责计算它们,optimizer 负责按选定规则改变参数。它们不是同一动作。
下一节重新 forward:z=1.12×2+0.06=2.30,h=2.30,prediction=1.12×2.30+0.06=2.636,Loss=(2.636−5)²=5.588496。Loss=9 的旧变量不会自己刷新,必须重算。
Knowledge check
EX04:如果某次隐藏 z<0,ReLU 让这一条隐藏路径梯度为 0,能否断言输出偏置 b2 的梯度也为 0?
12. 再做一次Forward
新参数:
对比:
Before: Loss = 9.00
After: Loss ≈5.59
一次backward + update 后,模型已经向正确答案移动。
13. 用代码对应完整计算
x = 2.0
y = 5.0
w1, b1 = 1.0, 0.0
w2, b2 = 1.0, 0.0
learning_rate = 0.01
# Forward
z = w1 * x + b1
h = max(0.0, z)
prediction = w2 * h + b2
loss = (prediction - y) ** 2
# Backward: Loss → prediction
d_prediction = 2 * (prediction - y)
# Output layer
dw2 = d_prediction * h
db2 = d_prediction
d_h = d_prediction * w2
# ReLU
d_z = d_h * (1.0 if z > 0 else 0.0)
# Hidden layer
dw1 = d_z * x
db1 = d_z
# Update
w1 -= learning_rate * dw1
b1 -= learning_rate * db1
w2 -= learning_rate * dw2
b2 -= learning_rate * db2每一行都能对应回计算图中的一条边。
可以不用另一套求导规则来验算:只把某个参数增加/减少一个很小的 ε,其余参数固定,各算一次 loss。中央差分 [L(θ+ε)−L(θ−ε)]/(2ε) 应接近手算梯度。它用于检查,不是训练时逐参数执行的高效方法;ReLU 在零点不可导,检查点应避开该折点。完整可运行例子见 course_examples/week04_gradient_check.py。
14. 如果一个Parameter 有多条路径
在真实网络中,一个parameter 或intermediate value 可能通过多条路径影响Loss。
规则是:
同一个变量从不同路径收到的gradients 要相加。
例如:
那么:
这和一个shared service 被多个调用路径影响的情况很像:总影响必须汇总。
为什么这里是相加?因为每条路径都造成了一部分真实影响。忽略一条就可能算错总影响;各路径可以同向,也可以因正负号不同而抵消。
可以记住graph 的两条规则:
沿同一条连续路径:local derivatives 相乘
多条路径汇合到同一变量:path gradients 相加
“路径上相乘,分支处相加”几乎概括了Backpropagation 的计算结构。
Knowledge check
两个分支分别给同一参数梯度 -3 和 +1,总梯度是多少?
15. Batch 中为什么也要累加Gradient
一个batch 有多个examples:
所以parameter gradient 是每个example 对它的影响的平均:
这解释了为什么训练代码经常对batch loss 求mean。
16. Autograd 做了什么
PyTorch 的Autograd 自动完成三件事:
- Forward 时记录运算和必要的中间值;
- 从scalar loss 开始反向遍历graph;
- 用每个operation 的local derivative 和Chain Rule 累积gradients。
它没有改变数学原理,只是替我们执行大量重复计算。
Autograd 也不是保存一张永久的静态流程图。PyTorch 默认在每次forward 时根据实际执行的operations动态建立graph,所以普通Python 的if、loop 和函数调用都可以参与模型计算。loss.backward() 完成后,graph 通常会被释放,下一次training step 再建立新的graph。
这解释了三个常见现象:
- 改变本次input 可能改变本次graph;
- 不需要gradient 的inference 可以用torch.no_grad() 节省内存;
- 如果错误地把旧graph 跨step 保留下来,内存可能持续增长。
17. Vanishing 与Exploding Gradient:知道概念即可
当gradient 穿过很多层时,会连续相乘。
如果沿路径很多局部导数的绝对值都小于 1,gradient 可能越来越小,叫Vanishing Gradient。
如果沿路径很多局部导数的绝对值都大于 1,gradient 可能越来越大,叫Exploding Gradient。
后面会看到ReLU、Residual Connection、LayerNorm 和Gradient Clipping 等设计帮助训练稳定。现在不需要展开证明。
一个最小数字例子:如果gradient 连续经过20 个local derivative 0.5,总倍率是:
信号几乎消失。若连续经过20 个2,总倍率则是:
这说明问题不是“层数多”本身,而是很多局部倍率连续相乘后可能变得极小或极大。后续architecture 和optimizer 中的稳定化设计,很多都在处理这个后果。
Gradient、Derivative 与Parameter Gradient 的关系
这三个词容易在代码里混在一起:
- Derivative 是一般概念:一个输出对一个输入的变化率;
- Local Derivative 是单个operation 内部的变化率;
- Gradient 是scalar Loss 对一组variables 的所有偏导数组成的向量;
- Parameter Gradient 是其中针对可训练parameters 的部分,例如w.grad。
Intermediate tensor 也可能有gradient,但optimizer 通常只更新parameters。Backpropagation 的任务,是从许多local derivatives 组合出每个parameter gradient。
18. Week 4 最应该理解的6 件事
- Forward 计算prediction 和loss,并保存中间值。
- Backward 从Loss 开始,沿graph 反向移动。
- 每个operation 只提供自己的local derivative。
- Chain Rule 把局部影响相乘成最终影响。
- 多条路径的gradients 要相加。
- Backprop 算gradient;optimizer 使用gradient 更新parameter。
19. 理解测试
Knowledge check
如果: Loss → prediction 的gradient = -4 prediction = w × h h = 3 那么:
Review the relevant lesson因此:
如果learning rate 是0.01:
w_new = w - 0.01 × (-12)
= w + 0.12
20. Week 4 → Week 5
我们已经能手算一个小网络的forward、backward 和parameter update。
但真实网络可能有数百万甚至数十亿parameters,人工维护计算图和导数不现实。
Week 5 会把同样的数学交给PyTorch:
Python numbers
↓
Tensor
↓
Autograd
↓
nn.Module
↓
Optimizer