Current: Week 4

0%

Week 4

Week 4 - Backpropagation:让网络里的所有参数一起学习

Key questionLoss 怎样把责任传回每个 weight?

Learning objectives

  • 理解并应用 Week 4 - Backpropagation:让网络里的所有参数一起学习

55 min estimated reading time

Week progress: 0 of 21 sections (0%)Course progress: 0 of 321 sections (0%)

Week 3 已经能算一个网络的预测;本周的问题是:最终 Loss 只有一个数,怎样分别知道每个参数该往哪边调?不靠每个参数轮流试很多次,而是复用 forward 的中间结果计算局部影响。

Scroll horizontally to view all columns.

Course data table
分次安排本次产物
一:保存依赖固定 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(反向传播)就是答案。

本周只需要真正理解四件事:

  1. Computational Graph;
  2. Local Derivative;
  3. Chain Rule;
  4. 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

假设一个很小的网络:

z=w1x+b1z = w_1x+b_1
h=ReLU(z)h = \operatorname{ReLU}(z)
y^=w2h+b2\hat y=w_2h+b_2
L=(y^y)2L=(\hat y-y)^2

依赖关系是:

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 需要的上下文。例如乘法:

c=abc=ab

Backward 时需要知道forward 当时的a 和b,因为:

ca=b,cb=a\frac{\partial c}{\partial a}=b,\qquad \frac{\partial c}{\partial b}=a

因此,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=w1x+b1=1×2+0=2z=w_1x+b_1=1\times2+0=2
h=ReLU(2)=2h=\operatorname{ReLU}(2)=2
y^=w2h+b2=1×2+0=2\hat y=w_2h+b_2=1\times2+0=2
L=(25)2=9L=(2-5)^2=9

需要保存的中间值包括z、h 和ŷ,因为backward 时计算局部导数会再次使用它们。

Forward 不只是在“做预测”

Training 时的forward 同时完成两件事:

  1. 计算prediction 和loss;
  2. 为之后的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。

几个常见局部导数:

加法

c=a+bc=a+b
ca=1,cb=1\frac{\partial c}{\partial a}=1,\qquad \frac{\partial c}{\partial b}=1

为什么都是1?假设c = a + b,固定b:a 增加0.01,c 也正好增加0.01。因此output 对这个input 的变化倍率是1。加法节点不会放大或缩小gradient,只会把收到的gradient 原样分给两个inputs。

乘法

c=abc=ab
ca=b,cb=a\frac{\partial c}{\partial a}=b,\qquad \frac{\partial c}{\partial b}=a

例如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,不是需要死记的神秘规则。

平方误差

L=(y^y)2L=(\hat y-y)^2
Ly^=2(y^y)\frac{\partial L}{\partial \hat y}=2(\hat y-y)

这里的导数同时编码了方向和严重程度:

  • 如果prediction 高于target,ŷ-y 为正,gradient 为正;
  • 如果prediction 低于target,ŷ-y 为负,gradient 为负;
  • 错得越远,gradient 的绝对值越大。

因此平方误差不仅告诉模型“错了多少”,它的导数还告诉模型“prediction 应该往哪边移动”。

ReLU

ReLU(z)=max(0,z)\operatorname{ReLU}(z)=\max(0,z)
ReLU(z)={1,z>00,z<0\operatorname{ReLU}'(z)=\begin{cases}1,&z>0\\0,&z<0\end{cases}

在z = 0 的特殊点,框架会选择一个约定值;当前不影响我们的核心理解。

ReLU 的local derivative 像一个gradient gate:

Concept sequence
  1. forward 时z > 0
  2. gate 打开
  3. gradient 乘1,继续通过
Concept sequence
  1. forward 时z < 0
  2. gate 关闭
  3. gradient 乘0,这条路径停止

注意,ReLU 并不是凭空“生成” gradient。它只是根据forward 时的输入,决定收到的上游gradient 有多少可以继续传回去。

Local Derivative 怎样真正参与Backward

一个节点在backward 时执行的核心规则是:

input gradient=upstream gradient×local derivative\mathit{input\ gradient}=\mathit{upstream\ gradient}\times\mathit{local\ derivative}

例如乘法节点c = ab 收到:

upstream gradient = ∂L/∂c = -6

forward 时a = 3

forward 时b = 4

那么它传给两个inputs 的gradients 是:

La=Lcca=(6)(4)=24\frac{\partial L}{\partial a}=\frac{\partial L}{\partial c}\frac{\partial c}{\partial a}=(-6)(4)=-24
Lb=Lccb=(6)(3)=18\frac{\partial L}{\partial b}=\frac{\partial L}{\partial c}\frac{\partial c}{\partial b}=(-6)(3)=-18

这就是截图中几条local derivative 公式真正的用途:它们不是孤立的求导表,而是每个节点把“Loss 的责任”继续向前传递时使用的倍率。

5. Chain Rule:把局部影响串起来

本节为了单独展示“沿路径相乘”,可以临时更换某个局部系数;这不表示第 3 节保存的主实验参数已经更新。回到第 6 节时继续使用主实验旧状态。链式法则先解决怎样计算影响,optimizer 才决定什么时候改参数。

Lw1=Ly^y^hhzzw1\frac{\partial L}{\partial w_1}=\frac{\partial L}{\partial \hat y}\frac{\partial \hat y}{\partial h}\frac{\partial h}{\partial z}\frac{\partial z}{\partial w_1}

每一项只是相邻两个节点之间的局部影响。

为什么相乘?因为:

w₁ 改一点

↓影响z

z 再影响h

h 再影响prediction

prediction 再影响Loss

最终影响等于每一段影响的乘积。

为什么是相乘,而不是相加

假设一个很小的变化经过三段:

w₁ 增加0.01

z 大约增加2 × 0.01

h 大约增加1 × z 的变化

ŷ 大约增加3 × h 的变化

最终:

Δy^0.01×2×1×3=0.06\Delta\hat y\approx0.01\times2\times1\times3=0.06

每一段都是上一段变化的倍率,所以倍率自然连续相乘。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 的逻辑起点其实是:

LL=1\frac{\partial L}{\partial L}=1

一个值对自己的变化率当然是1。可以理解成在Loss 节点放入一个初始信号1,然后让它通过每个节点的local derivative 向前传播。

为什么最终通常要求Loss 是scalar?因为scalar 只有一个明确的起点。如果output 是一个vector,就需要先说明我们想对这个vector 做怎样的加权组合,才能决定backward 的初始方向。训练中把一个batch的各项损失求和或求平均,正是为了得到一个scalar objective。

接下来第一步是计算Loss 对prediction 的影响。

第一步:

Ly^=2(y^y)=2(25)=6\frac{\partial L}{\partial \hat y}=2(\hat y-y)=2(2-5)=-6

这个-6 是从Loss 向前一个节点传出的gradient。

它表示:在当前位置稍微增加prediction,会让Loss 下降。

7. Output Layer 的gradients

Output:

y^=w2h+b2\hat y=w_2h+b_2

对w₂:

y^w2=h=2\frac{\partial \hat y}{\partial w_2}=h=2

Chain Rule:

Lw2=Ly^y^w2=(6)(2)=12\frac{\partial L}{\partial w_2}=\frac{\partial L}{\partial \hat y}\frac{\partial \hat y}{\partial w_2}=(-6)(2)=-12

对b₂:

y^b2=1\frac{\partial \hat y}{\partial b_2}=1

所以:

Lb2=(6)(1)=6\frac{\partial L}{\partial b_2}=(-6)(1)=-6

现在Output Layer 的两个parameters 都知道应该怎样改变。

8. 把Gradient 继续传给Hidden Layer

为了继续向前传,需要先计算Loss 对h 的影响:

y^h=w2=1\frac{\partial \hat y}{\partial h}=w_2=1

所以:

Lh=Ly^y^h=(6)(1)=6\frac{\partial L}{\partial h}=\frac{\partial L}{\partial \hat y}\frac{\partial \hat y}{\partial h}=(-6)(1)=-6

这个gradient 不是parameter gradient。它是一条继续向前传播的“责任信号”。

9. Gradient 经过ReLU

Forward 时:

z = 2 > 0

因此:

hz=1\frac{\partial h}{\partial z}=1

所以:

Lz=Lhhz=(6)(1)=6\frac{\partial L}{\partial z}=\frac{\partial L}{\partial h}\frac{\partial h}{\partial z}=(-6)(1)=-6

如果Forward 时z < 0,ReLU 的局部导数为0,这条路径的gradient 就会被挡住。

10. Hidden Layer 的gradients

Hidden linear calculation:

z=w1x+b1z=w_1x+b_1

对w₁:

zw1=x=2\frac{\partial z}{\partial w_1}=x=2

所以:

Lw1=Lzzw1=(6)(2)=12\frac{\partial L}{\partial w_1}=\frac{\partial L}{\partial z}\frac{\partial z}{\partial w_1}=(-6)(2)=-12

对b₁:

zb1=1\frac{\partial z}{\partial b_1}=1
Lb1=(6)(1)=6\frac{\partial L}{\partial b_1}=(-6)(1)=-6

最终:

gradient_w₁ = -12

gradient_b₁ = -6

gradient_w₂ = -12

gradient_b₂ = -6

所有parameters 都收到了来自同一个Loss 的反馈。

11. 全部梯度算完,才一起更新参数

到这里我们还没有改动任何参数。所有梯度都描述同一次旧 forward 的 Loss。学习率设为 0.01,现在才执行“旧参数减去学习率乘梯度”。

Scroll horizontally to view all columns.

Course data table
参数旧值本次梯度更新后
w11−121.12
b10−60.06
w21−121.12
b20−60.06

为什么不能算完 dw2 就先更新 w2?旧 forward 中 dL/dprediction=−6,向隐藏值传递时应乘旧 w2=1,所以 dL/dh=−6。若偷用新 w2=1.12,会得到 −6.72,再乘输入 2 得到错误 dw1=−13.44;正确是 −12。你混合了两个时刻的计算。

θnew=θoldαL(θold)\theta_{\mathrm{new}}=\theta_{\mathrm{old}}-\alpha\nabla L(\theta_{\mathrm{old}})

θ 只是四个参数的统称;∇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

新参数:

z=(1.12)(2)+0.06=2.30z=(1.12)(2)+0.06=2.30
h=ReLU(2.30)=2.30h=\operatorname{ReLU}(2.30)=2.30
y^=(1.12)(2.30)+0.06=2.636\hat y=(1.12)(2.30)+0.06=2.636
L=(2.6365)25.59L=(2.636-5)^2\approx5.59

对比:

Before: Loss = 9.00

After: Loss ≈5.59

一次backward + update 后,模型已经向正确答案移动。

13. 用代码对应完整计算

python
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 要相加。

例如:

L=L1+L2L=L_1+L_2

那么:

Lw=L1w+L2w\frac{\partial L}{\partial w}=\frac{\partial L_1}{\partial w}+\frac{\partial L_2}{\partial w}

这和一个shared service 被多个调用路径影响的情况很像:总影响必须汇总。

为什么这里是相加?因为每条路径都造成了一部分真实影响。忽略一条就可能算错总影响;各路径可以同向,也可以因正负号不同而抵消。

可以记住graph 的两条规则:

沿同一条连续路径:local derivatives 相乘

多条路径汇合到同一变量:path gradients 相加

“路径上相乘,分支处相加”几乎概括了Backpropagation 的计算结构。

Knowledge check

两个分支分别给同一参数梯度 -3 和 +1,总梯度是多少?

15. Batch 中为什么也要累加Gradient

一个batch 有多个examples:

L=1ni=1nLiL=\frac{1}{n}\sum_{i=1}^{n}L_i

所以parameter gradient 是每个example 对它的影响的平均:

Lw=1ni=1nLiw\frac{\partial L}{\partial w}=\frac{1}{n}\sum_{i=1}^{n}\frac{\partial L_i}{\partial w}

这解释了为什么训练代码经常对batch loss 求mean。

16. Autograd 做了什么

PyTorch 的Autograd 自动完成三件事:

  1. Forward 时记录运算和必要的中间值;
  2. 从scalar loss 开始反向遍历graph;
  3. 用每个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,总倍率是:

0.5200.000000950.5^{20}\approx0.00000095

信号几乎消失。若连续经过20 个2,总倍率则是:

220=1,048,5762^{20}=1{,}048{,}576

这说明问题不是“层数多”本身,而是很多局部倍率连续相乘后可能变得极小或极大。后续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 件事

  1. Forward 计算prediction 和loss,并保存中间值。
  2. Backward 从Loss 开始,沿graph 反向移动。
  3. 每个operation 只提供自己的local derivative。
  4. Chain Rule 把局部影响相乘成最终影响。
  5. 多条路径的gradients 要相加。
  6. Backprop 算gradient;optimizer 使用gradient 更新parameter。

19. 理解测试

Knowledge check

如果: Loss → prediction 的gradient = -4 prediction = w × h h = 3 那么:

Review the relevant lesson
predictionw=h=3\frac{\partial\mathit{prediction}}{\partial w}=h=3

因此:

Lossw=(4)(3)=12\frac{\partial\mathit{Loss}}{\partial w}=(-4)(3)=-12

如果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