1. 先看整体关系
训练一个神经网络,一次完整迭代通常是:
输入 x
→ 正向传播(使用当前参数计算预测值)
→ 根据预测值和真实值计算损失 L
→ 反向传播(计算 L 对每个参数的梯度)
→ 优化器根据梯度更新参数
最重要的区别是:
- 正向传播(forward propagation):回答“当前参数会算出什么结果”。
- 反向传播(backpropagation):回答“每个参数应该往哪个方向调整,损失才可能减小”。
- 参数更新(optimizer step):真正修改参数。反向传播本身只计算梯度。
在 PyTorch 中对应:
optimizer.zero_grad() # 清除上一轮梯度
y_pred = model(x) # 正向传播
loss = loss_fn(y_pred, y) # 计算损失
loss.backward() # 反向传播:计算梯度
optimizer.step() # 根据梯度更新参数
2. 正向传播是什么
假设有一个两层神经网络:
输入 x → 线性层 1 → 激活函数 → 线性层 2 → 预测值 ŷ → 损失 L
用数学公式表示:
z(1)=W(1)x+b(1)
a(1)=f(z(1))
z(2)=W(2)a(1)+b(2)
如果是回归任务,可以直接令:
y^=z(2)
再用均方误差衡量预测值与真实值的差异:
L=21(y^−y)2
这里的 1/2 不是必须的,只是为了求导后把平方项产生的 2 抵消掉。
正向传播做的事情,就是按照这些公式从左到右依次计算:
- 输入乘第一层权重并加偏置,得到
z₁。
- 经过激活函数,得到隐藏层输出
a₁。
- 进入第二层,得到预测值
ŷ。
- 用
ŷ 和真实答案 y 计算损失 L。
正向传播不会修改参数,它只是使用当前的 W 和 b 完成一次计算。
3. 用一个具体数字完成正向传播
为了先看清主线,暂时省略偏置,使用只有一个神经元的两层网络:
h=ReLU(w1x)
y^=w2h
L=21(y^−y)2
给定:
x=2,y=1,w1=0.5,w2=2
第一层计算:
z1=w1x=0.5×2=1
因为 z₁ > 0,所以:
h=ReLU(1)=1
第二层计算:
y^=w2h=2×1=2
计算损失:
L=21(2−1)2=0.5
到这里,正向传播结束。当前网络预测为 2,真实答案为 1,损失为 0.5。
4. 反向传播是什么
反向传播从损失 L 出发,沿着正向计算路径反向使用链式法则,计算损失对每个可训练参数的偏导数:
∂w1∂L,∂w2∂L
这些偏导数就是梯度。
梯度的含义是:参数发生一个很小的变化时,损失会怎样变化。
∂L/∂w > 0:稍微增大 w,损失倾向于增大。
∂L/∂w < 0:稍微增大 w,损失倾向于减小。
|∂L/∂w| 越大:损失对该参数越敏感。
反向传播不是“把输入倒着计算一遍”,而是在正向传播建立的计算关系上,反向计算每个节点的局部导数,并通过链式法则把它们相乘或累加。
5. 链式法则为什么是反向传播的核心
如果:
u=g(w),v=f(u),L=q(v)
那么:
∂w∂L=∂v∂L∂u∂v∂w∂u
神经网络虽然层数很多,但本质也是一串复合函数。因此,某个较早参数对最终损失的影响,需要把整条路径上的导数连续相乘。
对前面的两层网络:
w1→z1→h→y^→L
所以:
∂w1∂L=∂y^∂L∂h∂y^∂z1∂h∂w1∂z1
这就是“误差信号逐层向前面传递”的数学含义。
6. 把具体数字反向算一遍
前面已经算出:
z1=1,h=1,y^=2,y=1,L=0.5
6.1 从损失传播到预测值
L=21(y^−y)2
因此:
∂y^∂L=y^−y=2−1=1
6.2 计算第二层权重的梯度
因为:
y^=w2h
所以:
∂w2∂y^=h=1
根据链式法则:
∂w2∂L=∂y^∂L∂w2∂y^=1×1=1
6.3 把梯度继续传回隐藏层
∂h∂y^=w2=2
因此:
∂h∂L=∂y^∂L∂h∂y^=1×2=2
6.4 经过 ReLU
ReLU 为:
ReLU(z)=max(0,z)
它的导数为:
ReLU′(z)={1,0,z>0z<0
当前 z₁ = 1 > 0,所以:
∂z1∂h=1
于是:
∂z1∂L=∂h∂L∂z1∂h=2×1=2
6.5 计算第一层权重的梯度
因为:
z1=w1x
所以:
∂w1∂z1=x=2
最终:
∂w1∂L=∂z1∂L∂w1∂z1=2×2=4
因此,本轮反向传播得到:
∂w1∂L=4
∂w2∂L=1
这两个数会分别保存到参数的梯度位置中。在 PyTorch 中,可以理解为:
w1.grad = 4
w2.grad = 1
此时参数还没有被修改。
7. 参数为什么要沿负梯度方向更新
设所有参数组成向量 θ。在当前位置附近,损失函数可以用一阶泰勒展开近似:
L(θ+Δθ)≈L(θ)+∇θL(θ)TΔθ
如果选择:
Δθ=−η∇θL(θ)
其中 η > 0 是学习率,那么:
L(θ+Δθ)≈L(θ)−η∥∇θL(θ)∥2
因为梯度范数的平方不会小于零,所以在学习率足够小时,沿负梯度方向移动通常会让损失下降。
最基本的梯度下降更新公式是:
wnew=wold−η∂w∂L
继续使用前面的例子,令学习率:
η=0.01
更新参数:
w1new=0.5−0.01×4=0.46
w2new=2−0.01×1=1.99
重新正向传播:
h=ReLU(0.46×2)=0.92
y^=1.99×0.92=1.8308
新损失约为:
Lnew=21(1.8308−1)2≈0.3451
损失从 0.5 降到了约 0.3451。这说明本次小步更新方向是有效的,但不代表每一次更新都必然下降;学习率过大、随机批次波动等因素都可能让单次损失上升。
8. 推广到任意多层神经网络
令第 l 层的计算为:
z(l)=W(l)a(l−1)+b(l)
a(l)=f(l)(z(l))
并令:
a(0)=x
定义第 l 层的误差信号:
δ(l)=∂z(l)∂L
输出层的误差信号通常为:
δ(L)=∂a(L)∂L⊙f′(L)(z(L))
其中 ⊙ 表示逐元素相乘。
隐藏层的误差信号递推公式为:
δ(l)=(W(l+1))Tδ(l+1)⊙f′(l)(z(l))
得到 δ(l) 后,该层参数的梯度为:
∂W(l)∂L=δ(l)(a(l−1))T
∂b(l)∂L=δ(l)
这四个公式就是全连接神经网络反向传播的核心。
9. 批次计算的矩阵公式与形状
下面采用 PyTorch 常见的批次优先形式:
X∈RB×D
其中:
B:batch size,批次中的样本数。
D:每条样本的输入特征数。
H:隐藏层神经元数。
C:输出维度或类别数。
PyTorch 的 Linear(D, H) 中,权重实际保存为:
W1∈RH×D
因此正向传播为:
Z1=XW1T+b1
形状变化:
[B,D]×[D,H]→[B,H]
隐藏层输出:
A1=f(Z1),A1∈RB×H
第二层:
Z2=A1W2T+b2
其中:
W2∈RC×H,Z2∈RB×C
设:
G2=∂Z2∂L∈RB×C
那么第二层参数梯度为:
∂W2∂L=G2TA1
形状为:
[C,B]×[B,H]→[C,H]
偏置梯度需要把 batch 维度上的贡献相加:
∂b2∂L=i=1∑BG2,i
传播回隐藏层:
GA1=G2W2
经过激活函数:
G1=GA1⊙f′(Z1)
第一层梯度:
∂W1∂L=G1TX
∂b1∂L=i=1∑BG1,i
如果损失采用 batch 平均值,梯度中还会带有 1/B。具体放在哪一步取决于损失函数的定义,但整体含义不变。
10. Softmax 与交叉熵为什么经常一起出现
多分类模型输出的通常是 logits:
z=[z1,z2,…,zC]
Softmax 把 logits 转换为概率:
pk=∑j=1Cezjezk
如果真实标签使用 one-hot 向量 y,交叉熵为:
L=−k=1∑Cyklogpk
Softmax 与交叉熵组合后,对第 k 个 logit 的导数可以化简为:
∂zk∂L=pk−yk
这个结果非常重要:
- 对真实类别,
yₖ = 1,梯度是 pₖ - 1,推动真实类别概率增大。
- 对其他类别,
yₖ = 0,梯度是 pₖ,推动其他类别概率减小。
对于 batch 平均损失:
∂Z∂L=BP−Y
在 PyTorch 中,nn.CrossEntropyLoss()直接接收原始 logits,内部已经组合了对数 Softmax 与负对数似然。因此训练时通常不要在模型末尾先手动调用 Softmax。
11. 常见激活函数的导数
反向传播必须知道激活函数的局部导数。
11.1 Sigmoid
σ(z)=1+e−z1
σ′(z)=σ(z)(1−σ(z))
当 |z| 很大时,导数接近 0,梯度经过多层连乘后容易变小。
11.2 Tanh
tanh′(z)=1−tanh2(z)
它的输出以 0 为中心,但在饱和区同样可能产生很小的梯度。
11.3 ReLU
ReLU(z)=max(0,z)
ReLU′(z)={1,0,z>0z<0
当输入落在负半轴时,梯度为 0,这也是“死亡 ReLU”问题的来源之一。工程中通常把 z = 0 处的导数按框架约定处理。
12. 自动微分与手算反向传播的关系
实际使用 PyTorch 时,不需要手写上述全部偏导数。正向传播过程中,框架会记录张量之间的运算关系,形成动态计算图。
例如:
y_pred = model(x)
loss = loss_fn(y_pred, y)
loss.backward()
loss.backward()会从标量 loss 出发,沿计算图反向执行每个算子的局部求导规则,把最终梯度累积到叶子参数的 .grad 中。
但是,自动微分没有改变数学原理:底层仍然是链式法则,只是框架自动保存了正向计算所需的中间结果,并自动组织了反向计算顺序。
13. 为什么梯度会累积
如果一个参数通过多条路径影响损失,总梯度是各条路径贡献之和。
例如:
L=L1(w)+L2(w)
那么:
∂w∂L=∂w∂L1+∂w∂L2
这解释了两个现象:
- 一个共享参数被多次使用时,各条计算路径都会向同一个梯度贡献数值。
- PyTorch 默认把多次
backward() 的结果累积到 .grad,所以每轮训练前通常需要 optimizer.zero_grad()。
RNN 的时间反向传播也是同一原理:同一套循环权重在多个时间步重复使用,各时间步产生的梯度贡献最终会累加到同一个参数上。
14. 梯度消失与梯度爆炸的数学来源
深层网络的梯度包含很多雅可比矩阵或局部导数的乘积。简化表示为:
∂a(l)∂L=∂a(L)∂Lk=l+1∏L∂a(k−1)∂a(k)
如果连续乘积中的因子大多小于 1,梯度可能指数级变小,这就是梯度消失。
如果连续乘积中的因子大多大于 1,梯度可能快速增大,这就是梯度爆炸。
因此,激活函数、权重初始化、归一化、残差连接、门控结构和梯度裁剪都会影响反向传播的稳定性。
15. 训练、验证和推理的区别
训练阶段需要:
forward → loss → backward → step
验证和推理阶段通常只需要:
forward → 指标或预测结果
因为验证和推理不修改模型参数,所以通常不需要构建反向传播所需的计算图:
model.eval()
with torch.no_grad():
y_pred = model(x)
model.eval()负责切换 Dropout、BatchNorm 等模块的行为;torch.no_grad()负责关闭梯度记录。两者作用不同。
16. 常见误区
误区 1:反向传播会直接修改权重
错误。反向传播只计算:
∂W∂L,∂b∂L
真正修改参数的是优化器。
误区 2:梯度就是损失
损失 L 是一个数,用来衡量预测误差;梯度 ∇L 描述损失对各参数的变化率。
误区 3:正向传播只是预测阶段才有
错误。训练、验证和推理都需要正向传播;只有训练阶段通常还需要反向传播和参数更新。
误区 4:用了 argmax 以后还能反向传播训练
argmax只返回最大值的位置,操作是离散的,不能提供适合训练的连续梯度。多分类训练应使用 logits 与交叉熵计算损失;argmax主要用于得到最终预测类别。
误区 5:zero_grad() 应该放在任何位置都一样
只要保证一次更新使用的是预期梯度即可,但最清晰、最常见的写法是在每轮正向传播前清空上一轮梯度:
optimizer.zero_grad()
logits = model(x)
loss = criterion(logits, y)
loss.backward()
optimizer.step()
17. 最终记忆版
正向传播
x→z(1)→a(1)→⋯→y^→L
使用当前参数,从输入计算出预测值和损失。
反向传播
∂y^∂L→∂W(L)∂L→⋯→∂W(1)∂L
从损失出发,利用链式法则计算每个参数的梯度。
参数更新
θt+1=θt−η∇θL
优化器读取梯度并真正修改参数。
一句话总结:
正向传播负责“算答案和损失”,反向传播负责“算每个参数对损失的责任”,优化器负责“根据责任修改参数”。