自动微分与反向传播 —— 神经网络是怎么"学会"东西的?
用生活比喻和可视化图解,从零讲解自动微分引擎、基础数学运算和反向传播的工作原理,让没有高数基础的人也能理解深度学习最核心的机制
你可能听过这样的说法:”神经网络通过训练来学习。”但它到底是怎么学的?这背后有三个核心概念——自动微分引擎、基础数学运算和反向传播。听起来很吓人,但它们的本质非常直观。这篇文章不需要你有高等数学基础,我们从生活经验出发,一步步搞懂。
一、先从一个生活场景说起
1.1 烤蛋糕的故事
假设你在学烤蛋糕,有三个可以调整的”参数”:
1
2
3
4
5
6
参数:
├── 温度:180°C
├── 时间:30 分钟
└── 糖量:50 克
结果:蛋糕评分 = 6 分(满分 10 分)
你想让蛋糕更好吃(评分更高),但不知道该调哪个参数、往哪个方向调。
最笨的方法: 一个个试。把温度加 10°C 试试?把时间多 5 分钟试试?每次只改一个,烤几十次蛋糕,慢慢找到最佳组合。
聪明的方法: 如果有人能告诉你——
1
2
3
"温度每升高 1°C,评分大约 +0.1" ← 温度的"梯度"
"时间每增加 1 分钟,评分大约 -0.2" ← 时间的"梯度"(负的说明该减少)
"糖量每增加 1 克,评分大约 +0.05" ← 糖量的"梯度"
有了这些信息,你就知道:温度稍微升一点,时间减少一点,糖量略加一点——下一次烤出来的蛋糕一定更好。
这就是神经网络学习的全部秘密:
- “参数”就是神经网络里的权重(几百万甚至几十亿个)
- “评分”就是损失函数(Loss,越低越好)
- “梯度”就是每个参数对 Loss 的影响方向和大小
- 自动微分引擎就是那个能告诉你所有梯度的”智能助手”
1.2 如果没有自动微分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
一个简单的神经网络可能有 1,000,000 个参数
手动求梯度:
对每个参数,你需要推导一个数学公式
1,000,000 个参数 = 1,000,000 个公式
任何一个写错,整个训练就跑偏了
→ 这不是人能做的事
自动微分引擎:
你只需要定义"怎么算结果"(前向计算)
引擎自动帮你算出所有 1,000,000 个梯度
精确无误,一行代码搞定
→ 这就是为什么深度学习能爆发
二、什么是”导数”和”梯度”?
不用怕,我们不需要高数课本上的定义。
2.1 导数 = “如果我稍微动一下,结果会怎么变?”
1
2
3
4
5
6
7
8
9
10
11
12
13
你在山坡上站着,想知道往前迈一步会怎样:
.
/|\ ← 你在这里
/ | \
/ | \
/ | \
站在上坡处 站在平地 站在下坡处
导数 > 0 导数 = 0 导数 < 0
(往前走会升高) (往前走不变)(往前走会降低)
导数的大小 = 坡度有多陡
导数的正负 = 往上走还是往下走
2.2 梯度 = 多个方向的导数打包在一起
当你有多个参数时,每个参数都有一个导数,把它们打包在一起就叫”梯度”:
1
2
3
4
5
梯度 = [温度的导数, 时间的导数, 糖量的导数]
= [+0.1, -0.2, +0.05]
它告诉你在"参数空间"里,
往哪个方向走一步,结果变化最大。
2.3 梯度下降 = “往低处走”
训练神经网络的目标是让 Loss(损失)越小越好。有了梯度,我们就知道 Loss 在哪个方向会变大——然后往反方向走就行了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
当前参数位置
\
\ ← 梯度指向"上坡方向"
\
* 你在这里,Loss = 5.0
/
/ ← 往反方向走(梯度下降)
/
* 新位置,Loss = 4.2(更好了!)
/
/
* 继续走,Loss = 3.1
...
* 最终,Loss ≈ 0.01(学会了!)
1
2
3
4
5
6
7
参数更新公式(非常简单):
新参数 = 旧参数 - 学习率 × 梯度
学习率:步子迈多大(通常是个很小的数,比如 0.001)
梯度: 方向和坡度
减号: 往梯度的反方向走(下坡)
三、自动微分引擎是怎么工作的?
3.1 核心思想:记录每一步,然后倒放
自动微分的原理出奇地简单——你做计算时,我在旁边默默记笔记;你算完了,我把笔记倒着读一遍,就能算出所有梯度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
类比:录像倒放
前向计算(你做的事):
"拿了 2 个鸡蛋" → "加了 50g 糖" → "搅拌" → "烤 30 分钟" → "成品"
自动微分引擎(在旁边记录):
📝 步骤 1: 鸡蛋 = 2
📝 步骤 2: 加糖 50g
📝 步骤 3: 搅拌(混合操作)
📝 步骤 4: 烤 30 分钟
📝 步骤 5: 成品评分 = 6 分
反向传播(倒着读笔记算梯度):
📝 步骤 5: 评分对烤制时间的影响是...
📝 步骤 4: 烤制时间对搅拌结果的影响是...
📝 步骤 3: 搅拌对加糖量的影响是...
...一直推回到每个原始参数
3.2 计算图:自动微分的”笔记本”
引擎记录的这些笔记,形成一个叫计算图的结构。来看一个真实的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
任务:计算 y = (x + w) × w
其中 x = 2,w = 3
前向计算,一步步拆开:
步骤 1: a = x + w = 2 + 3 = 5 (加法)
步骤 2: y = a × w = 5 × 3 = 15 (乘法)
引擎记录的计算图:
x=2 ──┐
├── [ + ] ── a=5 ──┐
w=3 ──┤ ├── [ × ] ── y=15
│ │
└───────────────────┘
w 参与了两步运算!
这张图记录了”谁和谁做了什么运算,得到了什么结果”。有了这张图,就能倒推梯度。
四、基础数学运算:计算图的”积木块”
4.1 为什么要拆成基础运算?
再复杂的数学公式,都可以拆成几种简单运算的组合。就像乐高积木——不管拼出来的是城堡还是飞机,积木块就那么几种形状。
自动微分引擎只需要知道每种”积木块”的导数规则,就能处理任意复杂的计算。
4.2 最常用的”积木块”
加法:z = a + b
1
2
3
4
5
6
7
8
9
10
a ──┐
├── [+] ── z
b ──┘
导数规则:
∂z/∂a = 1(a 变 1,z 也变 1)
∂z/∂b = 1(b 变 1,z 也变 1)
直觉:3 + 5 = 8,如果 3 变成 4,结果变成 9
变化量完全传递,所以导数是 1
乘法:z = a × b
1
2
3
4
5
6
7
8
9
10
11
a ──┐
├── [×] ── z
b ──┘
导数规则:
∂z/∂a = b(a 变化时,影响程度取决于 b 有多大)
∂z/∂b = a(反过来也一样)
直觉:3 × 5 = 15
a 从 3 变到 4:4 × 5 = 20,变化了 5(= b 的值)
所以 ∂z/∂a = b = 5
ReLU 激活函数:z = max(0, x)
1
2
3
4
5
6
7
8
9
10
11
┌── z = x (如果 x > 0)
x ──┤
└── z = 0 (如果 x ≤ 0)
导数规则:
∂z/∂x = 1(x > 0 时,变化完全传递)
∂z/∂x = 0(x ≤ 0 时,输出恒为 0,变化被"截断")
直觉:ReLU 就像一个单向阀门
正数:畅通无阻(导数 = 1)
负数:完全堵死(导数 = 0)
矩阵乘法:Z = X × W
1
2
3
4
5
6
这是神经网络最核心的运算。
一个神经网络层本质上就是:
输出 = 激活函数(输入 × 权重矩阵 + 偏置)
↑
矩阵乘法(大量的乘法和加法打包在一起)
4.3 积木搭出大模型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
一个神经网络的前向计算拆解:
输入 x
│
├── [矩阵乘法] × W₁ ── [加法] + b₁ ── [ReLU] ── h₁(第一层输出)
│
├── [矩阵乘法] × W₂ ── [加法] + b₂ ── [ReLU] ── h₂(第二层输出)
│
├── [矩阵乘法] × W₃ ── [加法] + b₃ ── [Softmax] ── ŷ(预测结果)
│
└── [交叉熵] (ŷ, y) ── Loss(损失值)
每一步都是基础运算!
引擎知道每个基础运算的导数规则
所以它能自动算出 Loss 对 W₁、W₂、W₃、b₁、b₂、b₃ 的所有梯度
五、反向传播:从终点倒推起点
5.1 链式法则:反向传播的数学基础
别被名字吓到。链式法则说的是一个非常直觉的事情:
1
2
3
4
5
6
7
8
9
10
11
12
场景:
你是公司的实习生
你的表现影响你的组长评分
你组长的评分影响部门评分
部门评分影响公司业绩
问题:你的表现变好一点点,公司业绩会变多少?
答案(链式法则):
= 你对组长的影响 × 组长对部门的影响 × 部门对公司的影响
就是把每一层的影响"链式"地乘起来!
用数学符号写:
1
2
3
4
5
如果 y = f(g(h(x))),那么
dy/dx = dy/dg × dg/dh × dh/dx
就是沿着"链条"把每一环的导数乘起来
5.2 手把手走一遍反向传播
我们用一个具体例子,完整走一遍:
题目: y = (x + w) × w,其中 x = 2, w = 3,求 ∂y/∂w
第一步:前向计算(从左到右,算出结果)
1
2
3
4
5
6
7
8
x=2 ──┐
├── [+] ── a=5 ──┐
w=3 ──┤ ├── [×] ── y=15
│ │
└─────────────────┘
a = x + w = 2 + 3 = 5
y = a × w = 5 × 3 = 15
第二步:反向传播(从右到左,算出梯度)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
起点:∂y/∂y = 1(自己对自己的导数永远是 1)
第 1 站:y = a × w(乘法节点)
┌──────────────────────────────────────┐
│ 乘法规则:∂z/∂a = b,∂z/∂b = a │
│ │
│ ∂y/∂a = w = 3 │
│ ∂y/∂w₍直接₎ = a = 5 │
└──────────────────────────────────────┘
第 2 站:a = x + w(加法节点)
┌──────────────────────────────────────┐
│ 加法规则:∂a/∂x = 1,∂a/∂w = 1 │
│ │
│ ∂y/∂x = ∂y/∂a × ∂a/∂x = 3 × 1 = 3 │ ← 链式法则!
│ ∂y/∂w₍间接₎ = ∂y/∂a × ∂a/∂w = 3 × 1 = 3 │
└──────────────────────────────────────┘
w 的总梯度(两条路径加起来):
∂y/∂w = ∂y/∂w₍直接₎ + ∂y/∂w₍间接₎ = 5 + 3 = 8
为什么 w 有两条路径? 因为 w 在计算图里参与了两次运算(一次加法、一次乘法),所以梯度要从两条路径累加。
验证: y = (x+w)×w = xw + w²,∂y/∂w = x + 2w = 2 + 6 = 8 ✓
5.3 反向传播的完整图解
把上面的过程画成一张完整的图:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
═══════ 前向传播(从左到右,算结果)═══════>
x=2 ──┐
├── [ + ] ── a=5 ──┐
w=3 ──┤ ├── [ × ] ── y=15
│ │
└───────────────────┘
<══════ 反向传播(从右到左,算梯度)═══════
∂y/∂x=3 ◄──┐
├◄── [ + ] ◄── ∂y/∂a=3 ◄──┐
∂y/∂w=8 ◄──┤ (×1传递) ├◄── [ × ] ◄── ∂y/∂y=1
│ │ (∂y/∂a=w=3)
└◄──────── ∂y/∂w₍直接₎=5 ◄──┘ (∂y/∂w=a=5)
w 的梯度 = 5(直接路径) + 3(间接路径) = 8
5.4 梯度怎么”流动”的?一个更直观的比喻
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
把计算图想象成一个水管网络:
前向传播 = 水从左边流到右边(数据流动)
反向传播 = 颜料从右边倒灌回左边(梯度流动)
颜料从这里倒入
↓
x ──[管道A]──┐
├──[接头]── a ──[管道C]──┐
w ──[管道B]──┤ ├──[接头]── y ← 倒入 1 份颜料
│ │
└──[管道D]───────────────┘
每个"接头"(运算节点)决定颜料怎么分配:
├── 加法接头:颜料均匀分配到两个上游管道
└── 乘法接头:颜料按"对方的值"分配
最终每个入口(x, w)收到的颜料量 = 它的梯度
六、PyTorch 中的自动微分:四行代码的魔法
6.1 最简单的例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import torch
# 创建一个需要求梯度的参数
w = torch.tensor(3.0, requires_grad=True) # ← "嘿,引擎,请记录 w 的操作"
x = torch.tensor(2.0)
# 前向计算(引擎在背后自动构建计算图)
y = (x + w) * w # y = 15
# 反向传播(一行代码,引擎自动沿图反向计算所有梯度)
y.backward()
# 查看梯度
print(w.grad) # tensor(8.) ← 自动算出 ∂y/∂w = 8
你只写了”怎么算 y”,引擎自动帮你算出了”w 变一点点,y 会变多少”。
6.2 一个真实的神经网络训练循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import torch
import torch.nn as nn
# 定义一个简单的神经网络
model = nn.Sequential(
nn.Linear(10, 64), # 第一层:10 个输入 → 64 个神经元(640 个参数)
nn.ReLU(), # 激活函数
nn.Linear(64, 1) # 第二层:64 → 1 个输出(64 个参数)
)
# 损失函数和优化器
loss_fn = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
# 训练循环
for epoch in range(100):
# ① 前向传播:算出预测结果
prediction = model(input_data)
# ② 计算损失:预测和真实值的差距
loss = loss_fn(prediction, target)
# ③ 反向传播:自动算出所有 704 个参数的梯度
loss.backward() # ← 就这一行!引擎自动遍历计算图
# ④ 更新参数:每个参数沿梯度反方向走一小步
optimizer.step() # 参数 = 参数 - 学习率 × 梯度
# ⑤ 清零梯度:为下一轮做准备
optimizer.zero_grad()
6.3 背后发生了什么?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
loss.backward() 这一行代码背后的完整过程:
1. 从 loss 节点出发,梯度 = 1
2. 反向经过 MSELoss 节点
→ 算出 ∂loss/∂prediction
3. 反向经过第二层 Linear(64, 1)
→ 算出 ∂loss/∂W₂(64 个梯度)
→ 算出 ∂loss/∂b₂(1 个梯度)
→ 继续往回传递
4. 反向经过 ReLU
→ 正数位置:梯度原样传递
→ 负数位置:梯度变为 0(被"截断")
5. 反向经过第一层 Linear(10, 64)
→ 算出 ∂loss/∂W₁(640 个梯度)
→ 算出 ∂loss/∂b₁(64 个梯度)
总共自动算出了 704 + 1 = 705 个梯度
耗时:毫秒级
七、三种求导方式的对比
自动微分不是唯一的求导方式,但它是最适合深度学习的:
7.1 手动求导
1
2
3
4
5
6
7
8
9
10
11
12
方法:用纸笔推导数学公式
优点:精确
缺点:参数多了根本不可能
示例:
y = w₁x₁ + w₂x₂ + b
∂y/∂w₁ = x₁ ← 简单函数还行
但如果是一个 100 层的神经网络...
你需要推导 100 层链式法则的展开式
→ 不现实
7.2 数值微分
1
2
3
4
5
6
7
8
9
10
方法:用极小的变化量去"试探"
公式:∂y/∂w ≈ [f(w + 0.0001) - f(w)] / 0.0001
优点:简单,什么函数都能用
缺点:
├── 不精确(只是近似值)
├── 极慢(每个参数都要算两次前向传播)
│ 100 万个参数 → 200 万次前向计算
└── 数值不稳定(步长太大不准,太小有舍入误差)
7.3 自动微分
1
2
3
4
5
6
7
8
9
10
11
方法:记录计算图,用链式法则反向精确求导
优点:
├── 精确(不是近似,是数学上精确的值)
├── 快(不管多少参数,只需一次前向 + 一次反向)
└── 自动(你只需定义前向计算)
缺点:
└── 需要额外内存来存储计算图
这就是为什么所有深度学习框架都用自动微分。
1
2
3
4
5
6
速度对比(100 万个参数):
数值微分: ████████████████████████████████ 200 万次前向计算
自动微分: █ 1 次前向 + 1 次反向
差距:约 100 万倍!
八、为什么叫”反向”传播?
因为信息的流动方向和计算方向相反:
1
2
3
4
5
6
7
8
9
前向传播(Forward Pass):
数据流方向:输入 → 第一层 → 第二层 → ... → 输出 → Loss
做的事情: 算出预测结果和损失值
类比: 工厂流水线,原料进去,成品出来
反向传播(Backward Pass):
梯度流方向:输入 ← 第一层 ← 第二层 ← ... ← 输出 ← Loss
做的事情: 算出每个参数的梯度
类比: 质量追溯,从不合格的成品倒查是哪道工序出了问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌──────────────────────────────────────────────────┐
│ │
│ 输入 Loss │
│ │ │ │
│ ▼ 前向传播(算结果) ▼ │
│ ┌───┐ ────────────────────────────────> ┌───┐ │
│ │ x │ 层1 → 层2 → 层3 → ... → 输出 │ L │ │
│ └───┘ <──────────────────────────────── └───┘ │
│ ▲ 反向传播(算梯度) ▲ │
│ │ │ │
│ 梯度 起点=1 │
│ │
│ 两个方向,一个完整的训练步骤 │
└──────────────────────────────────────────────────┘
九、常见问题
9.1 梯度消失和梯度爆炸
反向传播需要沿路把梯度一层层乘回去。如果每一层的导数都很小或很大,问题就来了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
梯度消失(Vanishing Gradient):
层1 层2 层3 层4 层5
0.1 × 0.1 × 0.1 × 0.1 × 0.1 = 0.00001
梯度小到几乎为零,前面的层"学不动"了
▼ 解决方案:
├── 用 ReLU 激活函数(导数要么 0 要么 1,不会缩小)
├── 残差连接(ResNet,给梯度开一条"高速公路"直达前面的层)
└── Batch Normalization(标准化每层的输出)
梯度爆炸(Exploding Gradient):
层1 层2 层3 层4 层5
10 × 10 × 10 × 10 × 10 = 100,000
梯度大到参数剧烈震荡,训练发散
▼ 解决方案:
├── 梯度裁剪(Gradient Clipping,设置梯度上限)
└── 合适的参数初始化
9.2 为什么要”清零梯度”?
1
optimizer.zero_grad() # 为什么每次训练都要清零?
因为 PyTorch 的梯度是累加的。如果不清零,新一轮的梯度会加到旧梯度上,结果就乱了。这个设计是为了支持某些需要累加梯度的高级用法(如梯度累积),但大多数情况下你需要手动清零。
9.3 计算图用完就销毁?
1
2
3
loss.backward() # 反向传播完毕后,计算图默认被销毁
# 释放内存
# 下一次前向计算会构建新的计算图
这就是 PyTorch “动态计算图”的特点——每次前向计算都构建新图,反向传播后销毁。好处是你可以在代码里用 if/else、for 循环等动态逻辑,图会自动适应。
十、总结:把三个概念串起来
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
┌────────────────────────────────────────────────────────────┐
│ 神经网络训练全景 │
│ │
│ ┌─────────┐ 前向传播 ┌─────────┐ ┌──────┐ │
│ │ 输入 │ ─────────────> │ 神经网络 │ ──>│ Loss │ │
│ │ 数据 │ (基础运算 │ (参数 W) │ │ │ │
│ └─────────┘ 的组合) └─────────┘ └──┬───┘ │
│ │ │
│ 自动微分引擎在前向传播时 │ │
│ 默默记录了计算图 📝 │ │
│ │ │
│ ▼ │
│ 反向传播 │
│ (沿计算图反向 │
│ 用链式法则 │
│ 算出所有梯度) │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ 梯度 │ │
│ │ ∂L/∂W₁ │ │
│ │ ∂L/∂W₂ │ │
│ │ ... │ │
│ └────┬─────┘ │
│ │ │
│ ▼ │
│ 参数更新 │
│ W = W - lr × 梯度 │
│ │ │
│ ┌──────────┘ │
│ │ 回到开头,重复训练 │
│ └──────────> │
└────────────────────────────────────────────────────────────┘
| 概念 | 角色 | 生活比喻 |
|---|---|---|
| 基础数学运算 | 积木块 | 烹饪的基本操作(切、炒、煮、蒸) |
| 自动微分引擎 | 记录员 + 计算器 | 站在旁边记录你每一步操作的助手 |
| 反向传播 | 倒推过程 | 产品不合格时从终点倒查每道工序的影响 |
一句话总结: 自动微分引擎在你做计算(前向传播)时,用基础运算搭建计算图并记录每一步;然后通过反向传播,沿着计算图从输出倒推回输入,用链式法则精确算出每个参数的梯度——这就是神经网络”学会”东西的全部秘密。
本文为科普性质的技术入门文章。为了便于理解,部分概念做了简化。如需深入了解,推荐阅读 Andrej Karpathy 的 micrograd 项目——一个仅用 100 行 Python 代码实现的完整自动微分引擎,是理解这些概念的最佳实践材料。