反向传播完整推导:从链式法则到通用自动微分

引言

反向传播(Backpropagation, BP)是深度学习的核心算法,由 Rumelhart, Hinton & Williams 在 1986 年的开创性论文中系统化。其本质是链式法则在计算图上的高效实现

本文将建立从基础数学到通用自动微分的完整推导链:

  1. 单变量/多变量链式法则
  2. 计算图视角(节点、边、拓扑排序)
  3. Jacobian 矩阵(多元导数的紧凑表示)
  4. Hessian 矩阵(二阶导数)
  5. 自动微分(前向模式、反向模式、混合模式)
  6. 特殊结构的反向传播(卷积、循环、注意力)
  7. 数值稳定性(log-sum-exp、梯度检查)

最终,我们将在纯 NumPy 中实现一个通用反向传播引擎,可以处理任意计算图。


一、链式法则基础

1.1 单变量链式法则

对于复合函数

多重复合

1.2 多元链式法则

情形1,其中

情形2,其中

Jacobian 链式法则

其中 是 Jacobian 矩阵:


二、计算图视角

2.1 计算图定义

计算图是一个有向无环图(DAG),其中:

  • 节点:表示变量(输入、中间值、参数、输出)
  • :表示函数依赖关系
# 简单计算图示例
# f(x, y, z) = (x + y) * z
# 计算图:
#   x, y, z (输入)
#    ↓   ↓
#    add  →  a = x + y
#     ↓
#    mul  →  b = a * z
#     ↓
#    output

2.2 前向传播与反向传播

前向传播(Forward Pass):从输入到输出,按拓扑序计算每个节点。

反向传播(Backward Pass):从输出到输入,按反向拓扑序计算梯度。

关键观察

  • 前向传播中,每个节点保存其值(用于反向传播)
  • 反向传播中,每个节点计算其对输出损失的梯度
  • 反向传播的时间复杂度 = 前向传播的常数倍

2.3 计算图的分解

任意复杂函数都可以分解为基本操作的组合:

  • 算术, , ,
  • 激活函数, ,
  • 矩阵运算:matmul, transpose
  • 归约:sum, mean, max
  • 广播:broadcasting
class Node:
    """计算图节点"""
    def __init__(self, value, parents=(), op=''):
        self.value = value          # 前向值
        self.parents = parents      # 父节点
        self.op = op                # 生成此节点的操作
        self.grad = None            # 反向梯度(∂L/∂self)
        self._backward = lambda: None  # 反向传播函数
    
    def __repr__(self):
        return f"Node({self.value}, op={self.op})"

三、Jacobian 矩阵

3.1 定义与几何意义

,其 Jacobian 矩阵是:

几何意义

  • Jacobian 是线性变换 处的局部近似
  • 矩阵的是局部维度

3.2 雅可比链式法则

定理:若 ,则

矩阵乘法自然地实现了链式法则。

3.3 反向传播的 Jacobian 视角

反向传播的每一步实际上是:

即:上游梯度 通过 Jacobian 的转置流向下游。

示例

  • (常数)

3.4 神经网络层的 Jacobian

线性层

(实际上是用

ReLU

Softmax

即 Jacobian =


四、Hessian 矩阵

4.1 定义

是标量函数,Hessian 矩阵是:

关键性质

  • 对称( 二阶连续可微时)
  • 特征值是局部曲率

4.2 Hessian 与优化

牛顿法

二次型

Hessian 条件数

大 → 优化难(病态)。

4.3 Hessian-vector product (HVP)

Hessian 完整计算太昂贵( 空间),但 Hessian-vector product 是高效的( 时间)。

关键观察

实现:用 reverse-on-forward 自动微分。

def hessian_vector_product(loss_fn, params, v):
    """
    Hessian-vector product: Hv
    loss_fn: 计算损失的函数
    params: 参数列表
    v: 向量
    """
    # 1. 前向计算损失
    loss = loss_fn()
    
    # 2. 计算梯度 ∇L
    grads = torch.autograd.grad(loss, params, create_graph=True)
    
    # 3. 计算 ∇L · v
    grad_v = sum((g * v_i).sum() for g, v_i in zip(grads, v))
    
    # 4. 计算 ∇(∇L · v) = Hv
    Hv = torch.autograd.grad(grad_v, params)
    return Hv

五、自动微分

5.1 三种自动微分

模式适用场景计算复杂度
数值微分简单 调用,每次
符号微分复杂表达式表达式膨胀
前向模式 AD(输入少) 次前向
反向模式 AD(输出少) 次反向

反向模式 AD = 反向传播(用于深度学习)。

5.2 前向模式 AD

核心思想:对每个变量同时跟踪梯度(种子)。

class Dual:
    """前向模式 AD 的对偶数"""
    def __init__(self, real, dual=0):
        self.real = real  # 实际值
        self.dual = dual  # 导数(种子)
    
    def __add__(self, other):
        other = other if isinstance(other, Dual) else Dual(other)
        return Dual(self.real + other.real, self.dual + other.dual)
    
    def __mul__(self, other):
        other = other if isinstance(other, Dual) else Dual(other)
        return Dual(self.real * other.real, 
                    self.real * other.dual + self.dual * other.real)
    
    def sin(self):
        return Dual(np.sin(self.real), np.cos(self.real) * self.dual)
 
def f(x):
    return x * x + Dual(3) * x + Dual(5)
 
# 计算 df/dx 在 x=2 的值
x = Dual(2.0, dual=1.0)  # 种子 = 1
y = f(x)
print(f"f(2) = {y.real}, f'(2) = {y.dual}")

复杂度:每个操作做 2 个浮点运算,因此计算梯度是 步(前向传播的常数倍)。

5.3 反向模式 AD

核心思想:先前向计算所有中间值,然后反向计算梯度。

操作(以乘法为例):

  • 前向:
  • 反向:
def backward_mode_example():
    """反向模式 AD 示例:f(x, y) = x*y + sin(x)"""
    # 前向
    x = 2.0
    y = 3.0
    a = x * y         # 6
    b = np.sin(x)     # sin(2)
    z = a + b         # 6 + sin(2)
    
    # 反向
    grad_z = 1.0
    grad_a = grad_z  # ∂z/∂a = 1
    grad_b = grad_z  # ∂z/∂b = 1
    grad_x = grad_a * y + grad_b * np.cos(x)  # ∂z/∂x = y + cos(x)
    grad_y = grad_a * x  # ∂z/∂y = x
    
    print(f"z = {z:.4f}")
    print(f"∂z/∂x = {grad_x:.4f} (expected: {y + np.cos(x):.4f})")
    print(f"∂z/∂y = {grad_y:.4f} (expected: {x:.4f})")

复杂度:对每个中间节点访问常数次,总复杂度 = 次反向(与输入维度无关)。

这就是为什么反向模式 AD适合深度学习:输入维度高(百万参数),输出维度低(标量损失)。

5.4 混合模式 AD

前向-反向混合:对部分变量用前向模式,对其他用反向模式。

实现:嵌套的 AD 调用。

应用:Hessian-vector product、计算特定参数的梯度。


六、特殊结构的反向传播

6.1 卷积层的反向传播

卷积操作,其中

反向传播推导

是损失, 已知。

的梯度

的梯度(转置卷积 / 反卷积):

其中 表示转置卷积(用翻转的核做卷积)。

关键洞察:卷积的 Jacobian 是 Toeplitz 矩阵(每个对角线元素相同)。

def conv2d_backward(X, W, dY):
    """
    简化的 2D 卷积反向传播
    X: (B, C_in, H, W) 输入
    W: (C_out, C_in, kH, kW) 卷积核
    dY: (B, C_out, H_out, W_out) 上游梯度
    """
    B, C_in, H, W_ = X.shape
    C_out, _, kH, kW = W.shape
    
    # 1. 对 W 的梯度:dL/dW = X * dL/dY
    dW = np.zeros_like(W)
    for b in range(B):
        for co in range(C_out):
            for ci in range(C_in):
                dW[co, ci] += conv2d(X[b, ci], dY[b, co])
    
    # 2. 对 X 的梯度:dL/dX = 转置卷积 dL/dY with W
    dX = np.zeros_like(X)
    for b in range(B):
        for ci in range(C_in):
            for co in range(C_out):
                dX[b, ci] += conv_transpose2d(dY[b, co], W[co, ci])
    
    # 3. 对 bias 的梯度
    db = dY.sum(axis=(0, 2, 3))
    return dX, dW, db

6.2 RNN 的 BPTT

循环神经网络(RNN):

BPTT (Backpropagation Through Time):将 RNN “展开”为前向网络。

梯度推导

梯度消失/爆炸

如果 ,梯度爆炸;如果 ,梯度消失。

LSTM 的解决方案:用加性更新 ,使梯度沿时间稳定传播。

def bptt(rnn, X, Y, T):
    """
    BPTT 实现
    X: (B, T, d_in) 输入序列
    Y: (B, T, d_out) 目标序列
    """
    B, T, d_in = X.shape
    h = rnn.initial_hidden(B)
    
    # 前向
    hs, ys = [], []
    for t in range(T):
        h, y = rnn.step(X[:, t], h)
        hs.append(h)
        ys.append(y)
    
    # 计算损失
    loss = ((torch.stack(ys) - Y) ** 2).mean()
    
    # 反向(PyTorch 自动)
    loss.backward()
    
    return loss

6.3 注意力层的反向传播

注意力

反向传播

给定

Softmax 反向(关键步骤):

即:

def softmax_backward(dA, A):
    """softmax 反向传播"""
    # dA: 上游梯度
    # A: softmax 输出
    # 返回:dS(softmax 输入的梯度)
    sum_dA = dA.sum(dim=-1, keepdim=True)  # (..., 1)
    dS = A * (dA - sum_dA)
    return dS

最后:


七、数值稳定性

7.1 梯度检查

梯度检查(Gradient Check):用数值微分验证解析梯度。

def gradient_check(loss_fn, params, epsilon=1e-5, tol=1e-5):
    """梯度检查"""
    loss = loss_fn()
    loss.backward()
    
    for i, p in enumerate(params):
        analytical_grad = p.grad.clone()
        
        # 数值梯度
        p.data[i] += epsilon
        loss_plus = loss_fn().item()
        p.data[i] -= 2 * epsilon
        loss_minus = loss_fn().item()
        p.data[i] += epsilon
        numerical_grad = (loss_plus - loss_minus) / (2 * epsilon)
        
        # 比较
        diff = abs(analytical_grad[i] - numerical_grad)
        rel = diff / (abs(analytical_grad[i]) + abs(numerical_grad) + 1e-8)
        if rel > tol:
            print(f"参数 {i}: 解析={analytical_grad[i]:.6e}, "
                  f"数值={numerical_grad:.6e}, 相对误差={rel:.6e}")
        else:
            print(f"参数 {i}: OK")

7.2 数值问题

1. 梯度消失/爆炸

  • 解决:归一化(BatchNorm、LayerNorm)、残差连接、梯度裁剪

2. log(0) = -∞

  • 解决:log-sum-exp 技巧

3. Softmax 溢出

  • 解决:减去最大值(数值等价):

4. 除以零

  • 解决: 平滑,例如
def log_softmax(x):
    """数值稳定的 log-softmax"""
    m = x.max(dim=-1, keepdim=True).values
    return x - m - torch.log(torch.exp(x - m).sum(dim=-1, keepdim=True))
 
def log_sum_exp(x):
    """数值稳定的 log-sum-exp"""
    m = x.max()
    return m + torch.log(torch.exp(x - m).sum())

八、通用反向传播引擎(NumPy 实现)

import numpy as np
from collections import defaultdict
 
class Tensor:
    """支持自动微分的张量"""
    def __init__(self, data, requires_grad=False, _children=(), _op=''):
        self.data = np.asarray(data, dtype=np.float32)
        self.requires_grad = requires_grad
        self.grad = None
        self._backward = lambda: None
        self._children = set(_children)
        self._op = _op
    
    def __repr__(self):
        return f"Tensor(shape={self.data.shape}, op={self._op})"
    
    # === 基本运算 ===
    def __add__(self, other):
        other = other if isinstance(other, Tensor) else Tensor(other)
        out = Tensor(self.data + other.data, 
                     requires_grad=self.requires_grad or other.requires_grad,
                     _children=(self, other), _op='+')
        
        def _backward():
            if self.requires_grad:
                self.grad = self._accumulate_grad(self.grad, out.grad)
            if other.requires_grad:
                other.grad = other._accumulate_grad(other.grad, out.grad)
        out._backward = _backward
        return out
    
    def __mul__(self, other):
        other = other if isinstance(other, Tensor) else Tensor(other)
        out = Tensor(self.data * other.data,
                     requires_grad=self.requires_grad or other.requires_grad,
                     _children=(self, other), _op='*')
        
        def _backward():
            if self.requires_grad:
                self.grad = self._accumulate_grad(self.grad, other.data * out.grad)
            if other.requires_grad:
                other.grad = other._accumulate_grad(other.grad, self.data * out.grad)
        out._backward = _backward
        return out
    
    def __matmul__(self, other):
        other = other if isinstance(other, Tensor) else Tensor(other)
        out = Tensor(self.data @ other.data,
                     requires_grad=self.requires_grad or other.requires_grad,
                     _children=(self, other), _op='@')
        
        def _backward():
            if self.requires_grad:
                self.grad = self._accumulate_grad(self.grad, out.grad @ other.data.T)
            if other.requires_grad:
                other.grad = other._accumulate_grad(other.grad, self.data.T @ out.grad)
        out._backward = _backward
        return out
    
    def relu(self):
        out = Tensor(np.maximum(0, self.data),
                     requires_grad=self.requires_grad,
                     _children=(self,), _op='relu')
        def _backward():
            if self.requires_grad:
                mask = (self.data > 0).astype(np.float32)
                self.grad = self._accumulate_grad(self.grad, mask * out.grad)
        out._backward = _backward
        return out
    
    def sum(self):
        out = Tensor(self.data.sum(),
                     requires_grad=self.requires_grad,
                     _children=(self,), _op='sum')
        def _backward():
            if self.requires_grad:
                self.grad = self._accumulate_grad(self.grad, 
                    np.broadcast_to(out.grad, self.data.shape))
        out._backward = _backward
        return out
    
    def _accumulate_grad(self, existing, new):
        if existing is None:
            return new
        return existing + new
    
    # === 反向传播 ===
    def backward(self, grad=None):
        if grad is None:
            grad = np.ones_like(self.data)
        self.grad = grad
        
        # 拓扑排序
        topo = []
        visited = set()
        def build_topo(node):
            if node not in visited:
                visited.add(node)
                for child in node._children:
                    build_topo(child)
                topo.append(node)
        build_topo(self)
        
        # 反向传播
        for node in reversed(topo):
            node._backward()
 
 
# === 测试 ===
def test_simple_function():
    """测试简单函数:f(x, y) = (x + y) * (x - y)"""
    x = Tensor([2.0], requires_grad=True)
    y = Tensor([3.0], requires_grad=True)
    
    a = x + y       # 5
    b = x - y       # -1
    f = a * b       # -5
    
    f.backward()
    
    print(f"x.grad = {x.grad} (expected: 2x = 4)")
    print(f"y.grad = {y.grad} (expected: -2y = -6)")
 
 
def test_mlp():
    """测试两层 MLP"""
    np.random.seed(42)
    X = Tensor(np.random.randn(4, 3), requires_grad=False)
    W1 = Tensor(np.random.randn(3, 5) * 0.1, requires_grad=True)
    b1 = Tensor(np.zeros(5), requires_grad=True)
    W2 = Tensor(np.random.randn(5, 2) * 0.1, requires_grad=True)
    b2 = Tensor(np.zeros(2), requires_grad=True)
    
    # 前向
    h = (X @ W1 + b1).relu()
    out = h @ W2 + b2
    loss = (out * out).sum()  # 平方损失(伪目标为 0)
    
    # 反向
    loss.backward()
    
    print(f"Loss = {loss.data[0]:.4f}")
    print(f"W1.grad shape = {W1.grad.shape}")
    print(f"W2.grad shape = {W2.grad.shape}")

九、关键洞察总结

9.1 三大数学对象

  1. 链式法则:反向传播的理论基础
  2. Jacobian 矩阵:多元导数的紧凑表示
  3. Hessian 矩阵:二阶优化、曲率分析

9.2 三大计算范式

  1. 前向模式 AD:对输出求导高效
  2. 反向模式 AD:对输入求导高效(深度学习用这个)
  3. 混合模式 AD:结合两者

9.3 三大数值挑战

  1. 梯度消失/爆炸:深层网络的根本问题
  2. 数值溢出:softmax、log 等函数
  3. 病态 Hessian:二阶优化困难

十、与其他专题的连接

  • 自动微分库:PyTorch autograd、JAX grad、TensorFlow GradientTape
  • 神经 ODE:连续时间 BP
  • Transformer 数学:注意力矩阵的反向传播
  • 优化器:SGD、Adam、Muon 都依赖 BP

参考资料

  • Rumelhart, D.E. et al. (1986). Learning representations by back-propagating errors. Nature 323:533-536.
  • Baydin, A.G. et al. (2017). Automatic Differentiation in Machine Learning: a Survey. JMLR 18:1-43. arXiv:1502.05767
  • Griewank, A. & Walther, A. (2008). Evaluating Derivatives: Principles and Techniques of Algorithmic Differentiation. SIAM.
  • Goodfellow, I. et al. (2016). Deep Learning. MIT Press. Chapter 6.
  • Paszke, A. et al. (2017). Automatic Differentiation in PyTorch. NeurIPS Autodiff Workshop.
  • Chen, T.Q. et al. (2018). Neural Ordinary Differential Equations. NeurIPS 2018.
  • Werbos, P.J. (1974). Beyond Regression: New Tools for Prediction and Analysis in the Behavioral Sciences. PhD thesis, Harvard.
  • Linnainmaa, S. (1970). The representation of the cumulative rounding error of an algorithm as a Taylor expansion of the local rounding errors. MSc thesis (in Finnish), University of Helsinki.

最后更新:2026-06-22