深度学习

神经网络中的正向传播与反向传播:从数值计算到矩阵公式

从两层神经网络的具体数值出发,推导链式法则、矩阵梯度、Softmax 交叉熵与 PyTorch 自动微分。

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)z^{(1)} = W^{(1)}x + b^{(1)} a(1)=f(z(1))a^{(1)} = f\left(z^{(1)}\right) z(2)=W(2)a(1)+b(2)z^{(2)} = W^{(2)}a^{(1)} + b^{(2)}

如果是回归任务,可以直接令:

y^=z(2)\hat{y} = z^{(2)}

再用均方误差衡量预测值与真实值的差异:

L=12(y^y)2L = \frac{1}{2}(\hat{y}-y)^2

这里的 1/2 不是必须的,只是为了求导后把平方项产生的 2 抵消掉。

正向传播做的事情,就是按照这些公式从左到右依次计算:

  1. 输入乘第一层权重并加偏置,得到 z₁
  2. 经过激活函数,得到隐藏层输出 a₁
  3. 进入第二层,得到预测值 ŷ
  4. ŷ 和真实答案 y 计算损失 L

正向传播不会修改参数,它只是使用当前的 Wb 完成一次计算。


3. 用一个具体数字完成正向传播

为了先看清主线,暂时省略偏置,使用只有一个神经元的两层网络:

h=ReLU(w1x)h = \operatorname{ReLU}(w_1x) y^=w2h\hat{y} = w_2h L=12(y^y)2L = \frac{1}{2}(\hat{y}-y)^2

给定:

x=2,y=1,w1=0.5,w2=2x=2,\quad y=1,\quad w_1=0.5,\quad w_2=2

第一层计算:

z1=w1x=0.5×2=1z_1=w_1x=0.5\times2=1

因为 z₁ > 0,所以:

h=ReLU(1)=1h=\operatorname{ReLU}(1)=1

第二层计算:

y^=w2h=2×1=2\hat{y}=w_2h=2\times1=2

计算损失:

L=12(21)2=0.5L=\frac{1}{2}(2-1)^2=0.5

到这里,正向传播结束。当前网络预测为 2,真实答案为 1,损失为 0.5


4. 反向传播是什么

反向传播从损失 L 出发,沿着正向计算路径反向使用链式法则,计算损失对每个可训练参数的偏导数:

Lw1,Lw2\frac{\partial L}{\partial w_1},\qquad \frac{\partial L}{\partial w_2}

这些偏导数就是梯度。

梯度的含义是:参数发生一个很小的变化时,损失会怎样变化。

  • ∂L/∂w > 0:稍微增大 w,损失倾向于增大。
  • ∂L/∂w < 0:稍微增大 w,损失倾向于减小。
  • |∂L/∂w| 越大:损失对该参数越敏感。

反向传播不是“把输入倒着计算一遍”,而是在正向传播建立的计算关系上,反向计算每个节点的局部导数,并通过链式法则把它们相乘或累加。


5. 链式法则为什么是反向传播的核心

如果:

u=g(w),v=f(u),L=q(v)u=g(w),\qquad v=f(u),\qquad L=q(v)

那么:

Lw=Lvvuuw\frac{\partial L}{\partial w} = \frac{\partial L}{\partial v} \frac{\partial v}{\partial u} \frac{\partial u}{\partial w}

神经网络虽然层数很多,但本质也是一串复合函数。因此,某个较早参数对最终损失的影响,需要把整条路径上的导数连续相乘。

对前面的两层网络:

w1z1hy^Lw_1 \rightarrow z_1 \rightarrow h \rightarrow \hat{y} \rightarrow L

所以:

Lw1=Ly^y^hhz1z1w1\frac{\partial L}{\partial w_1} = \frac{\partial L}{\partial \hat{y}} \frac{\partial \hat{y}}{\partial h} \frac{\partial h}{\partial z_1} \frac{\partial z_1}{\partial w_1}

这就是“误差信号逐层向前面传递”的数学含义。


6. 把具体数字反向算一遍

前面已经算出:

z1=1,h=1,y^=2,y=1,L=0.5z_1=1,\quad h=1,\quad \hat{y}=2,\quad y=1,\quad L=0.5

6.1 从损失传播到预测值

L=12(y^y)2L=\frac{1}{2}(\hat{y}-y)^2

因此:

Ly^=y^y=21=1\frac{\partial L}{\partial \hat{y}} =\hat{y}-y =2-1 =1

6.2 计算第二层权重的梯度

因为:

y^=w2h\hat{y}=w_2h

所以:

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

根据链式法则:

Lw2=Ly^y^w2=1×1=1\frac{\partial L}{\partial w_2} = \frac{\partial L}{\partial \hat{y}} \frac{\partial \hat{y}}{\partial w_2} =1\times1 =1

6.3 把梯度继续传回隐藏层

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

因此:

Lh=Ly^y^h=1×2=2\frac{\partial L}{\partial h} = \frac{\partial L}{\partial \hat{y}} \frac{\partial \hat{y}}{\partial h} =1\times2 =2

6.4 经过 ReLU

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₁ = 1 > 0,所以:

hz1=1\frac{\partial h}{\partial z_1}=1

于是:

Lz1=Lhhz1=2×1=2\frac{\partial L}{\partial z_1} = \frac{\partial L}{\partial h} \frac{\partial h}{\partial z_1} =2\times1 =2

6.5 计算第一层权重的梯度

因为:

z1=w1xz_1=w_1x

所以:

z1w1=x=2\frac{\partial z_1}{\partial w_1}=x=2

最终:

Lw1=Lz1z1w1=2×2=4\frac{\partial L}{\partial w_1} = \frac{\partial L}{\partial z_1} \frac{\partial z_1}{\partial w_1} =2\times2 =4

因此,本轮反向传播得到:

Lw1=4\boxed{\frac{\partial L}{\partial w_1}=4} Lw2=1\boxed{\frac{\partial L}{\partial w_2}=1}

这两个数会分别保存到参数的梯度位置中。在 PyTorch 中,可以理解为:

w1.grad = 4
w2.grad = 1

此时参数还没有被修改。


7. 参数为什么要沿负梯度方向更新

设所有参数组成向量 θ。在当前位置附近,损失函数可以用一阶泰勒展开近似:

L(θ+Δθ)L(θ)+θL(θ)TΔθL(\theta+\Delta\theta) \approx L(\theta)+\nabla_{\theta}L(\theta)^T\Delta\theta

如果选择:

Δθ=ηθL(θ)\Delta\theta=-\eta\nabla_{\theta}L(\theta)

其中 η > 0 是学习率,那么:

L(θ+Δθ)L(θ)ηθL(θ)2L(\theta+\Delta\theta) \approx L(\theta)-\eta\left\|\nabla_{\theta}L(\theta)\right\|^2

因为梯度范数的平方不会小于零,所以在学习率足够小时,沿负梯度方向移动通常会让损失下降。

最基本的梯度下降更新公式是:

wnew=woldηLww_{\text{new}} = w_{\text{old}}-\eta\frac{\partial L}{\partial w}

继续使用前面的例子,令学习率:

η=0.01\eta=0.01

更新参数:

w1new=0.50.01×4=0.46w_1^{\text{new}}=0.5-0.01\times4=0.46 w2new=20.01×1=1.99w_2^{\text{new}}=2-0.01\times1=1.99

重新正向传播:

h=ReLU(0.46×2)=0.92h=\operatorname{ReLU}(0.46\times2)=0.92 y^=1.99×0.92=1.8308\hat{y}=1.99\times0.92=1.8308

新损失约为:

Lnew=12(1.83081)20.3451L_{\text{new}} = \frac{1}{2}(1.8308-1)^2 \approx0.3451

损失从 0.5 降到了约 0.3451。这说明本次小步更新方向是有效的,但不代表每一次更新都必然下降;学习率过大、随机批次波动等因素都可能让单次损失上升。


8. 推广到任意多层神经网络

令第 l 层的计算为:

z(l)=W(l)a(l1)+b(l)z^{(l)}=W^{(l)}a^{(l-1)}+b^{(l)} a(l)=f(l)(z(l))a^{(l)}=f^{(l)}\left(z^{(l)}\right)

并令:

a(0)=xa^{(0)}=x

定义第 l 层的误差信号:

δ(l)=Lz(l)\delta^{(l)} = \frac{\partial L}{\partial z^{(l)}}

输出层的误差信号通常为:

δ(L)=La(L)f(L)(z(L))\delta^{(L)} = \frac{\partial L}{\partial a^{(L)}} \odot f'^{(L)}\left(z^{(L)}\right)

其中 表示逐元素相乘。

隐藏层的误差信号递推公式为:

δ(l)=(W(l+1))Tδ(l+1)f(l)(z(l))\delta^{(l)} = \left(W^{(l+1)}\right)^T\delta^{(l+1)} \odot f'^{(l)}\left(z^{(l)}\right)

得到 δ(l) 后,该层参数的梯度为:

LW(l)=δ(l)(a(l1))T\frac{\partial L}{\partial W^{(l)}} = \delta^{(l)}\left(a^{(l-1)}\right)^T Lb(l)=δ(l)\frac{\partial L}{\partial b^{(l)}} = \delta^{(l)}

这四个公式就是全连接神经网络反向传播的核心。


9. 批次计算的矩阵公式与形状

下面采用 PyTorch 常见的批次优先形式:

XRB×DX\in\mathbb{R}^{B\times D}

其中:

  • B:batch size,批次中的样本数。
  • D:每条样本的输入特征数。
  • H:隐藏层神经元数。
  • C:输出维度或类别数。

PyTorch 的 Linear(D, H) 中,权重实际保存为:

W1RH×DW_1\in\mathbb{R}^{H\times D}

因此正向传播为:

Z1=XW1T+b1Z_1=XW_1^T+b_1

形状变化:

[B,D]×[D,H][B,H][B,D]\times[D,H]\rightarrow[B,H]

隐藏层输出:

A1=f(Z1),A1RB×HA_1=f(Z_1),\qquad A_1\in\mathbb{R}^{B\times H}

第二层:

Z2=A1W2T+b2Z_2=A_1W_2^T+b_2

其中:

W2RC×H,Z2RB×CW_2\in\mathbb{R}^{C\times H},\qquad Z_2\in\mathbb{R}^{B\times C}

设:

G2=LZ2RB×CG_2=\frac{\partial L}{\partial Z_2} \in\mathbb{R}^{B\times C}

那么第二层参数梯度为:

LW2=G2TA1\frac{\partial L}{\partial W_2} = G_2^TA_1

形状为:

[C,B]×[B,H][C,H][C,B]\times[B,H]\rightarrow[C,H]

偏置梯度需要把 batch 维度上的贡献相加:

Lb2=i=1BG2,i\frac{\partial L}{\partial b_2} = \sum_{i=1}^{B}G_{2,i}

传播回隐藏层:

GA1=G2W2G_{A_1}=G_2W_2

经过激活函数:

G1=GA1f(Z1)G_1 = G_{A_1}\odot f'(Z_1)

第一层梯度:

LW1=G1TX\frac{\partial L}{\partial W_1} = G_1^TX Lb1=i=1BG1,i\frac{\partial L}{\partial b_1} = \sum_{i=1}^{B}G_{1,i}

如果损失采用 batch 平均值,梯度中还会带有 1/B。具体放在哪一步取决于损失函数的定义,但整体含义不变。


10. Softmax 与交叉熵为什么经常一起出现

多分类模型输出的通常是 logits:

z=[z1,z2,,zC]z=[z_1,z_2,\ldots,z_C]

Softmax 把 logits 转换为概率:

pk=ezkj=1Cezjp_k = \frac{e^{z_k}}{\sum_{j=1}^{C}e^{z_j}}

如果真实标签使用 one-hot 向量 y,交叉熵为:

L=k=1CyklogpkL=-\sum_{k=1}^{C}y_k\log p_k

Softmax 与交叉熵组合后,对第 k 个 logit 的导数可以化简为:

Lzk=pkyk\boxed{ \frac{\partial L}{\partial z_k}=p_k-y_k }

这个结果非常重要:

  • 对真实类别,yₖ = 1,梯度是 pₖ - 1,推动真实类别概率增大。
  • 对其他类别,yₖ = 0,梯度是 pₖ,推动其他类别概率减小。

对于 batch 平均损失:

LZ=PYB\frac{\partial L}{\partial Z} = \frac{P-Y}{B}

在 PyTorch 中,nn.CrossEntropyLoss()直接接收原始 logits,内部已经组合了对数 Softmax 与负对数似然。因此训练时通常不要在模型末尾先手动调用 Softmax。


11. 常见激活函数的导数

反向传播必须知道激活函数的局部导数。

11.1 Sigmoid

σ(z)=11+ez\sigma(z)=\frac{1}{1+e^{-z}} σ(z)=σ(z)(1σ(z))\sigma'(z)=\sigma(z)\left(1-\sigma(z)\right)

|z| 很大时,导数接近 0,梯度经过多层连乘后容易变小。

11.2 Tanh

tanh(z)=1tanh2(z)\tanh'(z)=1-\tanh^2(z)

它的输出以 0 为中心,但在饱和区同样可能产生很小的梯度。

11.3 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}

当输入落在负半轴时,梯度为 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)L=L_1(w)+L_2(w)

那么:

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

这解释了两个现象:

  1. 一个共享参数被多次使用时,各条计算路径都会向同一个梯度贡献数值。
  2. PyTorch 默认把多次 backward() 的结果累积到 .grad,所以每轮训练前通常需要 optimizer.zero_grad()

RNN 的时间反向传播也是同一原理:同一套循环权重在多个时间步重复使用,各时间步产生的梯度贡献最终会累加到同一个参数上。


14. 梯度消失与梯度爆炸的数学来源

深层网络的梯度包含很多雅可比矩阵或局部导数的乘积。简化表示为:

La(l)=La(L)k=l+1La(k)a(k1)\frac{\partial L}{\partial a^{(l)}} = \frac{\partial L}{\partial a^{(L)}} \prod_{k=l+1}^{L} \frac{\partial a^{(k)}}{\partial a^{(k-1)}}

如果连续乘积中的因子大多小于 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:反向传播会直接修改权重

错误。反向传播只计算:

LW,Lb\frac{\partial L}{\partial W},\qquad \frac{\partial L}{\partial b}

真正修改参数的是优化器。

误区 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. 最终记忆版

正向传播

xz(1)a(1)y^Lx \rightarrow z^{(1)} \rightarrow a^{(1)} \rightarrow \cdots \rightarrow \hat{y} \rightarrow L

使用当前参数,从输入计算出预测值和损失。

反向传播

Ly^LW(L)LW(1)\frac{\partial L}{\partial \hat{y}} \rightarrow \frac{\partial L}{\partial W^{(L)}} \rightarrow \cdots \rightarrow \frac{\partial L}{\partial W^{(1)}}

从损失出发,利用链式法则计算每个参数的梯度。

参数更新

θt+1=θtηθL\theta_{t+1} = \theta_t-\eta\nabla_{\theta}L

优化器读取梯度并真正修改参数。

一句话总结:

正向传播负责“算答案和损失”,反向传播负责“算每个参数对损失的责任”,优化器负责“根据责任修改参数”。