文章

自动微分与反向传播 —— 神经网络是怎么"学会"东西的?

用生活比喻和可视化图解,从零讲解自动微分引擎、基础数学运算和反向传播的工作原理,让没有高数基础的人也能理解深度学习最核心的机制

自动微分与反向传播 —— 神经网络是怎么"学会"东西的?

你可能听过这样的说法:”神经网络通过训练来学习。”但它到底是怎么学的?这背后有三个核心概念——自动微分引擎基础数学运算反向传播。听起来很吓人,但它们的本质非常直观。这篇文章不需要你有高等数学基础,我们从生活经验出发,一步步搞懂。


一、先从一个生活场景说起

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/elsefor 循环等动态逻辑,图会自动适应。


十、总结:把三个概念串起来

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 代码实现的完整自动微分引擎,是理解这些概念的最佳实践材料。

本文由作者按照 CC BY 4.0 进行授权