深度学习:反向传播算法完全解析

1 阅读8分钟

1. 引言

反向传播(Backpropagation)是训练神经网络的核心算法,它解决了如何高效计算深层网络中每个参数的梯度这一难题。本教程将从数学原理到代码实现,完整揭示反向传播的奥秘。


2. 问题的提出

2.1 神经网络的前向传播

考虑一个简单的三层网络:

输入层 → 隐藏层 → 输出层
  x   →   h    →   y

数学表示:

z1 = W1·x + b1
h = σ(z1)              # 激活函数
z2 = W2·h + b2
ŷ = σ(z2)
L = loss(ŷ, y)        # 损失函数

2.2 核心挑战

我们需要计算:

∂L/∂W1, ∂L/∂b1, ∂L/∂W2, ∂L/∂b2

朴素方法: 对每个参数数值微分

∂L/∂w ≈ (L(w+ε) - L(w)) / ε

问题:

  • 需要前向传播 2×参数数量 次
  • 数值不稳定
  • 计算复杂度 O(n²)

反向传播的贡献: 只需一次前向+一次反向,复杂度 O(n)


3. 数学基础:链式法则

3.1 一维链式法则

若 y = f(u) 且 u = g(x),则:

dy/dx = (dy/du) · (du/dx)

3.2 多维链式法则

若 z 依赖多个中间变量:

z = f(u, v)
u = g(x)
v = h(x)

则:
∂z/∂x = (∂z/∂u)·(∂u/∂x) + (∂z/∂v)·(∂v/∂x)

3.3 向量对向量求导

雅可比矩阵(Jacobian Matrix):

若 y = f(x),其中 x ∈ ℝⁿ, y ∈ ℝᵐ,则:

J = [∂y_i/∂x_j]  (m×n 矩阵)

链式法则的矩阵形式:

∂z/∂x = (∂z/∂y) · (∂y/∂x)

4. 计算图与反向传播

4.1 计算图表示

将神经网络表示为有向无环图(DAG):

      x
      ↓
  [W1·x + b1] = z1
      ↓
    [σ] = h
      ↓
  [W2·h + b2] = z2
      ↓
    [σ] = ŷ
      ↓
   [loss] = L

4.2 前向传播

从输入到输出,逐层计算:

# 前向传播伪代码
def forward(x, W1, b1, W2, b2):
    z1 = np.dot(W1, x) + b1
    h = sigmoid(z1)
    z2 = np.dot(W2, h) + b2
    y_pred = sigmoid(z2)
    return y_pred, (z1, h, z2)  # 缓存中间结果

4.3 反向传播核心思想

从输出到输入,逐层计算梯度:

∂L/∂ŷ → ∂L/∂z2 → ∂L/∂W2, ∂L/∂b2, ∂L/∂h
       → ∂L/∂z1 → ∂L/∂W1, ∂L/∂b1

关键: 利用已计算的梯度,避免重复计算。


5. 逐层反向传播推导

场景:二分类任务

网络结构:

x (输入) → W1, b1 → σ → h → W2, b2 → σ → ŷ → BCE Loss

损失函数(Binary Cross-Entropy):

L = -[y·log(ŷ) + (1-y)·log(1-ŷ)]

第一步:输出层梯度

(1) ∂L/∂ŷ

∂L/∂ŷ = -y/ŷ + (1-y)/(1-ŷ)

(2) ∂L/∂z2 (链式法则)

∂L/∂z2 = (∂L/∂ŷ) · (∂ŷ/∂z2)

其中 ŷ = σ(z2), σ'(z) = σ(z)·(1-σ(z))

∂ŷ/∂z2 = ŷ·(1-ŷ)

代入:

∂L/∂z2 = [-y/ŷ + (1-y)/(1-ŷ)] · ŷ·(1-ŷ)
       = -y(1-ŷ) + (1-y)ŷ
       = ŷ - y

惊人的简洁性! 这是 Sigmoid + BCE 组合的优美之处。


第二步:输出层参数梯度

(3) ∂L/∂W2

z2 = W2·h + b2

∂L/∂W2 = (∂L/∂z2) · (∂z2/∂W2)

注意 z2 对 W2 的每个元素 W2_ij 的偏导:

∂z2_i/∂W2_ij = h_j

因此:

∂L/∂W2 = (∂L/∂z2) ⊗ h^T  (外积)

矩阵形式:

∂L/∂W2 = (∂L/∂z2) @ h.T

(4) ∂L/∂b2

∂L/∂b2 = ∂L/∂z2

第三步:隐藏层梯度

(5) ∂L/∂h

h 影响 z2,z2 影响 L:

∂L/∂h = (∂L/∂z2) · (∂z2/∂h) = W2^T · (∂L/∂z2)

(6) ∂L/∂z1

∂L/∂z1 = (∂L/∂h) ⊙ σ'(z1)
       = (∂L/∂h) ⊙ h ⊙ (1-h)

符号说明:

  • ⊙: 逐元素乘法(Hadamard product)
  • @: 矩阵乘法

第四步:输入层参数梯度

(7) ∂L/∂W1

∂L/∂W1 = (∂L/∂z1) @ x.T

(8) ∂L/∂b1

∂L/∂b1 = ∂L/∂z1

6. 完整代码实现

6.1 从零构建神经网络

import numpy as np

class NeuralNetwork:
    def __init__(self, input_size, hidden_size, output_size):
        # He 初始化
        self.W1 = np.random.randn(hidden_size, input_size) * np.sqrt(2/input_size)
        self.b1 = np.zeros((hidden_size, 1))
        self.W2 = np.random.randn(output_size, hidden_size) * np.sqrt(2/hidden_size)
        self.b2 = np.zeros((output_size, 1))

    def sigmoid(self, z):
        return 1 / (1 + np.exp(-np.clip(z, -500, 500)))  # 防止溢出

    def sigmoid_derivative(self, z):
        s = self.sigmoid(z)
        return s * (1 - s)

    def forward(self, x):
        """前向传播"""
        self.z1 = self.W1 @ x + self.b1
        self.h = self.sigmoid(self.z1)
        self.z2 = self.W2 @ self.h + self.b2
        self.y_pred = self.sigmoid(self.z2)
        return self.y_pred

    def backward(self, x, y):
        """反向传播"""
        m = x.shape[1]  # batch size

        # 输出层梯度
        dz2 = self.y_pred - y                      # (output_size, m)
        dW2 = (1/m) * (dz2 @ self.h.T)            # (output_size, hidden_size)
        db2 = (1/m) * np.sum(dz2, axis=1, keepdims=True)

        # 隐藏层梯度
        dh = self.W2.T @ dz2                       # (hidden_size, m)
        dz1 = dh * self.sigmoid_derivative(self.z1)
        dW1 = (1/m) * (dz1 @ x.T)                 # (hidden_size, input_size)
        db1 = (1/m) * np.sum(dz1, axis=1, keepdims=True)

        return dW1, db1, dW2, db2

    def compute_loss(self, y_pred, y):
        """二元交叉熵损失"""
        m = y.shape[1]
        epsilon = 1e-8  # 防止 log(0)
        loss = -(1/m) * np.sum(
            y * np.log(y_pred + epsilon) + (1-y) * np.log(1-y_pred + epsilon)
        )
        return loss

    def train(self, X, y, epochs, learning_rate):
        """训练"""
        losses = []
        for epoch in range(epochs):
            # 前向传播
            y_pred = self.forward(X)

            # 计算损失
            loss = self.compute_loss(y_pred, y)
            losses.append(loss)

            # 反向传播
            dW1, db1, dW2, db2 = self.backward(X, y)

            # 更新参数
            self.W1 -= learning_rate * dW1
            self.b1 -= learning_rate * db1
            self.W2 -= learning_rate * dW2
            self.b2 -= learning_rate * db2

            if epoch % 100 == 0:
                print(f"Epoch {epoch}, Loss: {loss:.4f}")

        return losses

6.2 测试:XOR 问题

# XOR 数据集
X = np.array([[0, 0, 1, 1],
              [0, 1, 0, 1]])
y = np.array([[0, 1, 1, 0]])

# 创建网络
np.random.seed(42)
nn = NeuralNetwork(input_size=2, hidden_size=4, output_size=1)

# 训练
losses = nn.train(X, y, epochs=10000, learning_rate=0.5)

# 测试
print("\n预测结果:")
predictions = nn.forward(X)
for i in range(4):
    print(f"Input: {X[:, i]}, Predicted: {predictions[0, i]:.4f}, Actual: {y[0, i]}")

输出示例:

Epoch 0, Loss: 0.7135
Epoch 100, Loss: 0.6931
...
Epoch 9900, Loss: 0.0023

预测结果:
Input: [0 0], Predicted: 0.0156, Actual: 0
Input: [0 1], Predicted: 0.9841, Actual: 1
Input: [1 0], Predicted: 0.9839, Actual: 1
Input: [1 1], Predicted: 0.0162, Actual: 0

7. 深度网络的反向传播

7.1 通用公式

对于 L 层网络,第 l 层:

(1) 当前层的激活梯度

δ^(l) = (W^(l+1))^T · δ^(l+1) ⊙ σ'(z^(l))

(2) 参数梯度

∂L/∂W^(l) = δ^(l) @ (a^(l-1))^T
∂L/∂b^(l) = δ^(l)

初始化:

δ^(L) = ∂L/∂a^(L) ⊙ σ'(z^(L))

7.2 梯度消失与爆炸

问题根源:

梯度传播:

∂L/∂W^(1) = ∂L/∂z^(L) · ∂z^(L)/∂z^(L-1) · ... · ∂z^(2)/∂z^(1) · ∂z^(1)/∂W^(1)

每一项包含 W^(l) 和 σ'(z^(l)):

∂z^(l)/∂z^(l-1) = W^(l) ⊙ σ'(z^(l-1))

Sigmoid 的问题:

σ'(z) = σ(z)(1-σ(z)) ∈ (0, 0.25]

多层相乘:

(0.25)^n → 0  (梯度消失)

解决方案:

  1. ReLU 激活函数: σ'(z) = 1 (z>0)
  2. 残差连接(ResNet): 梯度直接跳过层
  3. BatchNorm: 归一化激活值
  4. 梯度裁剪: 限制梯度范数

8. 常见激活函数的导数

8.1 Sigmoid

σ(z) = 1/(1+e^(-z))
σ'(z) = σ(z)·(1-σ(z))

8.2 Tanh

tanh(z) = (e^z - e^(-z))/(e^z + e^(-z))
tanh'(z) = 1 - tanh²(z)

8.3 ReLU

ReLU(z) = max(0, z)
ReLU'(z) = {1, z>0; 0, z≤0}

优势: 计算快,缓解梯度消失

问题: Dying ReLU (负区域梯度为0)

8.4 Leaky ReLU

LeakyReLU(z) = max(αz, z)  (α=0.01)
LeakyReLU'(z) = {1, z>0; α, z≤0}

8.5 GELU (现代 Transformer 常用)

GELU(z) = z·Φ(z)  (Φ: 标准正态CDF)
GELU'(z) ≈ Φ(z) + z·φ(z)  (φ: 正态PDF)

9. 矩阵微积分技巧

9.1 常用恒等式

(1) 标量对向量求导

∂(a^T x)/∂x = a
∂(x^T A x)/∂x = (A + A^T)x

(2) 矩阵乘法的导数

∂(AB)/∂A = B^T ⊗ I

9.2 维度检查技巧

规则: 梯度的形状 = 参数的形状

例如:

W: (m, n) → ∂L/∂W: (m, n)
b: (m, 1) → ∂L/∂b: (m, 1)

调试技巧: 在代码中加入 assert

assert dW1.shape == self.W1.shape

10. 数值梯度检验

10.1 为什么需要?

反向传播易出错:

  • 转置错误
  • 广播问题
  • 索引错误

10.2 梯度检验方法

def gradient_check(nn, x, y, epsilon=1e-7):
    """数值梯度 vs 反向传播梯度"""
    # 反向传播梯度
    y_pred = nn.forward(x)
    dW1_bp, db1_bp, dW2_bp, db2_bp = nn.backward(x, y)

    # 数值梯度
    def compute_numerical_gradient(param, name):
        grad = np.zeros_like(param)
        it = np.nditer(param, flags=['multi_index'], op_flags=['readwrite'])
        while not it.finished:
            idx = it.multi_index
            old_value = param[idx]

            param[idx] = old_value + epsilon
            loss_plus = nn.compute_loss(nn.forward(x), y)

            param[idx] = old_value - epsilon
            loss_minus = nn.compute_loss(nn.forward(x), y)

            grad[idx] = (loss_plus - loss_minus) / (2 * epsilon)
            param[idx] = old_value
            it.iternext()
        return grad

    dW1_num = compute_numerical_gradient(nn.W1, 'W1')

    # 计算相对误差
    diff = np.linalg.norm(dW1_bp - dW1_num) / (
        np.linalg.norm(dW1_bp) + np.linalg.norm(dW1_num)
    )
    print(f"W1 梯度相对误差: {diff}")

    if diff < 1e-7:
        print("✓ 梯度检验通过!")
    else:
        print("✗ 梯度计算可能有误")

11. 实战技巧

11.1 批量处理

向量化计算:

# 差: 循环处理每个样本
for i in range(m):
    y_pred = forward(X[:, i])
    backward(X[:, i], y[i])

# 好: 批量处理
Y_pred = forward(X)  # X: (n, m)
backward(X, Y)

11.2 缓存中间结果

前向传播时保存 z, h 等:

cache = {'z1': z1, 'h': h, 'z2': z2}

反向传播时复用,避免重新计算。

11.3 梯度累积

处理大 batch:

for mini_batch in split(data, batch_size):
    gradients += compute_gradients(mini_batch)
gradients /= num_mini_batches
update_parameters(gradients)

12. 自动微分框架

12.1 PyTorch 的自动求导

import torch

x = torch.tensor([[0., 0., 1., 1.],
                  [0., 1., 0., 1.]], requires_grad=True)
y = torch.tensor([[0., 1., 1., 0.]])

W1 = torch.randn(4, 2, requires_grad=True)
b1 = torch.zeros(4, 1, requires_grad=True)
W2 = torch.randn(1, 4, requires_grad=True)
b2 = torch.zeros(1, 1, requires_grad=True)

# 前向传播
z1 = W1 @ x + b1
h = torch.sigmoid(z1)
z2 = W2 @ h + b2
y_pred = torch.sigmoid(z2)

# 损失
loss = torch.nn.functional.binary_cross_entropy(y_pred, y)

# 自动反向传播
loss.backward()

print("W1 梯度:", W1.grad)

12.2 计算图的构建

PyTorch 动态构建计算图:

TensorFunctionTensor → ...
  ↓         ↓          ↓
 grad    grad_fn     grad

调用 .backward() 时,沿图反向传播。


13. 总结

核心要点

  1. 反向传播 = 链式法则的高效实现
  2. 关键技巧:
    • 缓存前向传播结果
    • 从后向前逐层计算梯度
    • 利用矩阵运算向量化
  3. 常见陷阱:
    • 梯度消失/爆炸
    • 维度不匹配
    • 数值稳定性

进阶方向

  • 自动微分理论 (Forward/Reverse Mode)
  • 高阶导数 (Hessian, Hessian-Vector Product)
  • 分布式训练中的梯度同步

实践建议

  • 先在小网络上验证(梯度检验)
  • 使用成熟框架(PyTorch/TensorFlow)
  • 理解原理后再调优

记住: 反向传播不是黑魔法,而是优雅的数学工程!