【深度学习】神经网络前向传播与反向传播推导

文章目录

摘要 :训练神经网络时,我们每天都在写 loss.backward(),但这行代码背后发生了什么?前向传播负责把输入一步步变成预测,损失函数衡量预测和目标的差距,反向传播用链式法则把误差信号从 loss 传回每个参数,优化器再根据梯度更新参数。本文用一个两层 MLP 作为例子,从矩阵形状、前向公式、MSE loss、ReLU 梯度、参数梯度一步步推导到 PyTorch autograd 验证。目标不是让你手写大模型反传,而是让你真正看懂训练循环里的每一步。

前置知识 :第 3 篇矩阵乘法,第 4 篇导数与链式法则,第 5 篇梯度下降,第 10 篇 Softmax

阅读时间 :约 70 分钟

代码环境:Python 3.10+,numpy >= 1.24,torch >= 2.0

入门导读:先抓住主线

神经网络训练可以压缩成四句话:

text 复制代码
前向传播:用当前参数算预测
损失函数:衡量预测错多少
反向传播:计算每个参数该怎么改
优化器:根据梯度更新参数

对应到 PyTorch:

python 复制代码
logits = model(x)
loss = criterion(logits, y)
loss.backward()
optimizer.step()
optimizer.zero_grad()

很多初学者会把 backward() 当成黑盒。其实它的核心就是链式法则:如果 loss 依赖输出,输出依赖隐藏层,隐藏层依赖参数,那么 loss 对参数的影响可以沿着计算图一层层传回去。

读完先达到这个程度就够了

  • 能解释前向传播、loss、反向传播、参数更新的关系;
  • 能看懂两层 MLP 的矩阵形状;
  • 能手推 MSE loss 下输出层和隐藏层梯度;
  • 能理解 ReLU 的梯度为什么是 0 或 1;
  • 能知道 PyTorch autograd 如何自动做这些事;
  • 能解释为什么每轮训练前后要 zero_grad()

带着这 3 个问题读

  1. loss.backward() 到底在计算什么?
  2. 为什么梯度能从最后一层传回第一层?
  3. 为什么 PyTorch 默认会累积梯度,而不是自动清空?

一、训练循环里发生了什么

先看最小训练循环:

python 复制代码
for x, y in dataloader:
    pred = model(x)
    loss = criterion(pred, y)

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

这几行代码对应一整套数学过程。

代码 数学含义
pred = model(x) 前向传播,计算预测
loss = criterion(pred, y) 计算预测与目标的差距
zero_grad() 清空上一轮梯度
loss.backward() 反向传播,计算参数梯度
optimizer.step() 根据梯度更新参数

训练的目标不是让模型"记住代码",而是让参数沿着能降低 loss 的方向移动。

梯度就是这个方向的局部线索。


二、一个两层 MLP

我们用一个最小神经网络做推导。

输入:

text 复制代码
X: [batch, input_dim]

第一层:

text 复制代码
Z1 = X W1 + b1
A1 = ReLU(Z1)

第二层:

text 复制代码
Y_hat = A1 W2 + b2

损失函数用均方误差 MSE:

text 复制代码
Loss = mean((Y_hat - Y)^2)

形状如下:

符号 形状 含义
X [B, D_in] 输入 batch
W1 [D_in, H] 第一层权重
b1 [H] 第一层偏置
Z1 [B, H] 第一层线性输出
A1 [B, H] ReLU 后隐藏表示
W2 [H, D_out] 第二层权重
b2 [D_out] 第二层偏置
Y_hat [B, D_out] 预测
Y [B, D_out] 目标

用 NumPy 写前向:

python 复制代码
import numpy as np

np.random.seed(0)

B, D_in, H, D_out = 4, 3, 5, 2
X = np.random.randn(B, D_in)
Y = np.random.randn(B, D_out)

W1 = np.random.randn(D_in, H) * 0.1
b1 = np.zeros(H)
W2 = np.random.randn(H, D_out) * 0.1
b2 = np.zeros(D_out)

Z1 = X @ W1 + b1
A1 = np.maximum(0, Z1)
Y_hat = A1 @ W2 + b2
loss = np.mean((Y_hat - Y) ** 2)

print("Y_hat shape:", Y_hat.shape)
print("loss:", loss)

前向传播只是按照计算图从输入算到输出。


三、反向传播从 loss 开始

反向传播的目标是求:

text 复制代码
dLoss/dW1, dLoss/db1, dLoss/dW2, dLoss/db2

也就是每个参数变化一点点,loss 会怎么变。

如果某个参数的梯度是正数,说明它增大可能让 loss 增大,梯度下降会让它减小。

如果某个参数的梯度是负数,说明它增大可能让 loss 减小,梯度下降会让它增大。

参数更新最简单写法是:

θ ← θ − η ∂ L ∂ θ \theta \leftarrow \theta - \eta \frac{\partial L}{\partial \theta} θ←θ−η∂θ∂L

其中 η \eta η 是学习率。

反向传播从最后的 loss 开始,一步步往前传:

text 复制代码
Loss -> Y_hat -> W2, b2, A1 -> Z1 -> W1, b1, X

链式法则负责把这些局部导数组合起来。


四、MSE Loss 的梯度

我们定义:

L = 1 N ∑ ( Y ^ − Y ) 2 L = \frac{1}{N}\sum (\hat{Y} - Y)^2 L=N1∑(Y^−Y)2

其中 N N N 是所有输出元素数量。

对预测值求导:

∂ L ∂ Y ^ = 2 N ( Y ^ − Y ) \frac{\partial L}{\partial \hat{Y}} = \frac{2}{N}(\hat{Y} - Y) ∂Y^∂L=N2(Y^−Y)

代码:

python 复制代码
N = Y_hat.size
dY_hat = 2.0 * (Y_hat - Y) / N
print(dY_hat.shape)

dY_hat 表示 loss 对每个预测元素的梯度。它是反向传播的起点。

如果预测值比目标大,梯度为正,更新会倾向于让预测变小。

如果预测值比目标小,梯度为负,更新会倾向于让预测变大。


五、输出层参数梯度

输出层公式是:

Y ^ = A 1 W 2 + b 2 \hat{Y} = A_1 W_2 + b_2 Y^=A1W2+b2

已知 dY_hat,要求 dW2db2dA1

根据矩阵乘法反向规则:

text 复制代码
dW2 = A1.T @ dY_hat
db2 = sum(dY_hat, axis=0)
dA1 = dY_hat @ W2.T

代码:

python 复制代码
dW2 = A1.T @ dY_hat
db2 = dY_hat.sum(axis=0)
dA1 = dY_hat @ W2.T

print("dW2:", dW2.shape)
print("db2:", db2.shape)
print("dA1:", dA1.shape)

检查形状:

  • dW2W2 一样,是 [H, D_out]
  • db2b2 一样,是 [D_out]
  • dA1A1 一样,是 [B, H]

反向传播时,最重要的调试习惯就是检查梯度形状是否和对应变量一致。


六、ReLU 的梯度

隐藏层激活是:

A 1 = R e L U ( Z 1 ) = m a x ( 0 , Z 1 ) A_1 = ReLU(Z_1) = max(0, Z_1) A1=ReLU(Z1)=max(0,Z1)

ReLU 的导数很简单:

text 复制代码
Z1 > 0  -> 梯度为 1
Z1 <= 0 -> 梯度为 0

所以:

python 复制代码
dZ1 = dA1 * (Z1 > 0)
print(dZ1.shape)

这一步的意思是:只有前向时被 ReLU 放行的位置,反向时梯度才能通过。前向时被压成 0 的位置,反向梯度也会被截断。

这解释了 ReLU 的一个问题:如果大量神经元长期输出负值,它们可能很少得到梯度更新,出现所谓 dead ReLU。后面的激活函数篇会继续讲。


七、第一层参数梯度

第一层公式是:

Z 1 = X W 1 + b 1 Z_1 = XW_1 + b_1 Z1=XW1+b1

已知 dZ1,可以求:

text 复制代码
dW1 = X.T @ dZ1
db1 = sum(dZ1, axis=0)
dX = dZ1 @ W1.T

代码:

python 复制代码
dW1 = X.T @ dZ1
db1 = dZ1.sum(axis=0)
dX = dZ1 @ W1.T

print("dW1:", dW1.shape)
print("db1:", db1.shape)
print("dX:", dX.shape)

至此,所有参数梯度都算出来了:

text 复制代码
dW1, db1, dW2, db2

这就是两层 MLP 的完整反向传播。


八、参数更新:梯度真正发挥作用

有了梯度后,用梯度下降更新参数:

python 复制代码
lr = 0.1

W1 -= lr * dW1
b1 -= lr * db1
W2 -= lr * dW2
b2 -= lr * db2

这个更新的直觉是:沿着让 loss 增长最快的反方向走一小步。

学习率 lr 决定步子多大。

  • 太大:可能越过最低点,甚至发散;
  • 太小:收敛很慢;
  • 合适:loss 稳定下降。

真实训练中,优化器会更复杂,比如 Momentum、Adam、AdamW,但它们本质上仍然依赖梯度。


九、把前向和反向串成一个 NumPy 训练循环

现在写一个完整的手动训练循环。

python 复制代码
import numpy as np

np.random.seed(1)
B, D_in, H, D_out = 16, 4, 8, 2
X = np.random.randn(B, D_in)
Y = np.random.randn(B, D_out)

W1 = np.random.randn(D_in, H) * 0.1
b1 = np.zeros(H)
W2 = np.random.randn(H, D_out) * 0.1
b2 = np.zeros(D_out)

lr = 0.05

for step in range(200):
    # forward
    Z1 = X @ W1 + b1
    A1 = np.maximum(0, Z1)
    Y_hat = A1 @ W2 + b2
    loss = np.mean((Y_hat - Y) ** 2)

    # backward
    dY_hat = 2.0 * (Y_hat - Y) / Y_hat.size
    dW2 = A1.T @ dY_hat
    db2 = dY_hat.sum(axis=0)
    dA1 = dY_hat @ W2.T
    dZ1 = dA1 * (Z1 > 0)
    dW1 = X.T @ dZ1
    db1 = dZ1.sum(axis=0)

    # update
    W1 -= lr * dW1
    b1 -= lr * db1
    W2 -= lr * dW2
    b2 -= lr * db2

    if step % 50 == 0:
        print(step, loss)

这段代码没有 PyTorch,却完成了神经网络训练最核心的逻辑。

PyTorch 的 autograd 做的就是自动构建计算图、记录局部操作,然后按链式法则自动计算梯度。


十、PyTorch autograd 验证

用 PyTorch 写同样的模型,并和 autograd 对齐。

python 复制代码
import torch
import torch.nn as nn

B, D_in, H, D_out = 4, 3, 5, 2
torch.manual_seed(0)

X = torch.randn(B, D_in)
Y = torch.randn(B, D_out)

model = nn.Sequential(
    nn.Linear(D_in, H),
    nn.ReLU(),
    nn.Linear(H, D_out),
)

criterion = nn.MSELoss()
Y_hat = model(X)
loss = criterion(Y_hat, Y)
loss.backward()

for name, param in model.named_parameters():
    print(name, param.shape, param.grad.shape)

你会看到每个可训练参数都有 .grad,形状和参数本身一致。

例如:

text 复制代码
0.weight [H, D_in]
0.bias   [H]
2.weight [D_out, H]
2.bias   [D_out]

PyTorch 里 nn.Linear 的权重形状是 [out_features, in_features],和我们前面 NumPy 里写的 W1: [D_in, H] 是转置关系。这是常见细节,读源码时要注意。


十一、计算图和链式法则

Autograd 的核心是计算图。

前向传播时,每个操作都会记录自己如何由输入得到输出:

text 复制代码
X, W1, b1 -> Z1 -> A1 -> Y_hat -> Loss

反向传播时,从 Loss 开始,每个节点根据自己的局部导数,把梯度传给前面的节点。

这就是链式法则的自动化。

如果你写:

python 复制代码
x = torch.tensor(2.0, requires_grad=True)
y = x * x + 3 * x

y.backward()
print(x.grad)

数学上:

y = x 2 + 3 x y = x^2 + 3x y=x2+3x

d y d x = 2 x + 3 \frac{dy}{dx} = 2x + 3 dxdy=2x+3

x=2,梯度是 7。PyTorch 会自动算出这个结果。


十二、为什么要清空梯度

PyTorch 默认梯度会累积,而不是每次 backward() 自动覆盖。

python 复制代码
x = torch.tensor(1.0, requires_grad=True)

for _ in range(3):
    y = x * 2
    y.backward()
    print(x.grad)

你会看到梯度不断增加。

为什么这样设计?因为有些训练需要梯度累积。比如显存不够时,把一个大 batch 拆成多个小 batch,多次 backward 后再 step。

但普通训练中,如果不清空梯度,上一轮梯度会混进下一轮,更新就错了。

所以训练循环里通常写:

python 复制代码
optimizer.zero_grad(set_to_none=True)
loss.backward()
optimizer.step()

也有人把 zero_grad() 放在 step 后面,只要每次 backward 前梯度是干净的即可。


十三、反向传播在大模型里有什么不同

大模型里的反向传播原理没有变,仍然是链式法则。但工程复杂度高很多。

原因包括:

  • 参数量巨大,梯度和优化器状态占用大量显存;
  • Transformer 层数多,激活值显存很大;
  • 混合精度可能引入数值问题;
  • 分布式训练需要同步或分片梯度;
  • 长序列导致 attention 激活很大;
  • 梯度裁剪、checkpointing、ZeRO/FSDP 都会影响训练流程。

但无论多复杂,核心链条仍然是:

text 复制代码
forward -> loss -> backward -> optimizer step

理解本篇后,你看到大模型训练脚本时,就能把复杂工程放回这条主线。


十四、常见误区

误区 1:反向传播是另一套独立算法,和前向没关系。

反向传播依赖前向计算图和中间值。没有前向记录,就无法按链式法则传梯度。

误区 2:梯度就是参数应该更新后的值。

梯度是 loss 对参数的局部变化率,不是新参数。优化器根据梯度计算更新量。

误区 3:backward() 会自动更新参数。

不会。backward() 只计算 .grad,真正更新发生在 optimizer.step()

误区 4:PyTorch 会自动清空梯度。

不会。梯度默认累积,需要手动 zero_grad()

误区 5:只要 loss 下降,梯度一定没问题。

不一定。可能存在部分层梯度为 0、梯度爆炸、NaN 被掩盖、学习率过小等问题。

误区 6:手推梯度没必要。

真实项目不用手写反传,但理解一次推导能帮助你排查 shape、loss、梯度和训练稳定性问题。


十五、你应该记住的最小心智模型

神经网络训练可以这样记:

text 复制代码
Forward:
X -> Z1 -> A1 -> Y_hat -> Loss

Backward:
Loss -> dY_hat -> dW2/db2 -> dA1 -> dZ1 -> dW1/db1

Update:
W = W - lr * dW

PyTorch 对应:

python 复制代码
pred = model(x)
loss = criterion(pred, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()

其中:

  • 前向决定预测;
  • loss 决定目标;
  • backward 计算梯度;
  • optimizer 使用梯度更新参数。

总结

前向传播把输入经过一层层计算变成预测,损失函数衡量预测与目标的差距,反向传播用链式法则把 loss 的影响传回每个参数,优化器再根据梯度更新参数。两层 MLP 的手推过程展示了神经网络训练最核心的机制:每个局部操作只需要知道自己的导数,整体梯度就能沿计算图自动组合出来。

PyTorch autograd 自动完成反向传播,但它没有改变数学本质。理解 loss.backward() 背后的链式法则、梯度形状和累积机制,是读懂优化器、混合精度、分布式训练和大模型微调脚本的基础。

第一遍记住一句话:反向传播不是魔法,它只是沿着前向计算图反方向应用链式法则。

大模型视角

现代大模型训练脚本看起来复杂:AMP、DDP、FSDP、gradient checkpointing、gradient accumulation、clip grad、scheduler。但它们都围绕同一条主线服务:更稳定、更省显存、更高吞吐地完成 forward、loss、backward 和 optimizer step。

下一篇

激活函数演进:从 Sigmoid 到 SwiGLU ------ 反向传播依赖每个模块的导数。下一篇看激活函数如何影响梯度、非线性表达和现代 Transformer FFN 的设计。

相关推荐
我有满天星辰1 小时前
《从 0 打造我的本地 AI 知识库:Obsidian + Ollama + Milvus + RAG + MCP + Agent》第 01 篇
人工智能·milvus
您^_^1 小时前
DeepSeek-Harness v0.1.5-alpha.1更新后旧会话打不开?5 步抢救 8MB 会话照抄
人工智能·windows·个人开发·deepseek v4 pro·deepseekharness
行业研究员1 小时前
企业智能体安全厂商综合评估
人工智能·智能体安全
智能运维指南1 小时前
AI加持下的可观测平台与传统平台有什么本质区别?智能告警降噪和根因分析是否可靠?
运维·人工智能·可观测平台·嘉为蓝鲸
七牛云行业应用1 小时前
OpenCode 跑本地 Llama:编程 Agent 接入本地大模型的完整思路
人工智能·ai编程·llama
AI技术新视界1 小时前
从“只观不控”到“即时御防”:基于感知治理遥测(GAAT)的多 AI 智能体闭环治理架构
人工智能
Theo_xx1 小时前
声学感知基础:Day2(1).Chirp信号
人工智能·无线感知·声学感知
云雀衔光1 小时前
Java Spring AI MCP:在 Spring 里把 AI 接上你的业务系统
java·开发语言·人工智能·后端·测试工具·spring
小白说大模型1 小时前
去AI味提示词大全:25个实用Prompt帮你降低AI率
大数据·人工智能·pytorch·深度学习·机器学习·prompt