§1.1.4

矩阵求导的分子/分母布局约定?链式法则在反向传播中的体现?

手写练习
  • 不用 autograd 手写 y = x W^T 的反向梯度

核心概念

矩阵求导(Matrix Calculus)是多元微积分在向量和矩阵上的推广,是理解深度学习优化算法(如反向传播)的数学基础。布局约定(Layout Convention)定义了导数结果的形状。

  • 分母布局 (Denominator Layout):导数 yx\frac{\partial \mathbf{y}}{\partial \mathbf{x}} 的形状与分母 xT\mathbf{x}^T 和分子 y\mathbf{y} 的形状相匹配。例如,若 y\mathbf{y}m×1m \times 1 向量,x\mathbf{x}n×1n \times 1 向量,则导数是 n×mn \times m 矩阵。
  • 分子布局 (Numerator Layout):导数 yx\frac{\partial \mathbf{y}}{\partial \mathbf{x}} 的形状与分子 y\mathbf{y} 和分母 xT\mathbf{x}^T 的形状相匹配。在上述例子中,导数是 m×nm \times n 矩阵。

深度学习框架(如 PyTorch, TensorFlow)普遍采用分子布局。链式法则是计算复合函数导数的基本规则,在反向传播中,它表现为一个从输出到输入、逐层计算梯度的过程,其核心是雅可比矩阵的连乘。

原理与推导

1. 布局约定详解

假设标量为 yy,列向量为 xRn×1\mathbf{x} \in \mathbb{R}^{n \times 1},列向量为 yRm×1\mathbf{y} \in \mathbb{R}^{m \times 1}

| 导数 | 分母布局 (Denominator Layout) | 分子布局 (Numerator Layout) | | :--- | :--- | :--- | | yx\frac{\partial y}{\partial \mathbf{x}} | (yx1yxn)\begin{pmatrix} \frac{\partial y}{\partial x_1} \\ \vdots \\ \frac{\partial y}{\partial x_n} \end{pmatrix} (列向量, n×1n \times 1) | (yx1yxn)\begin{pmatrix} \frac{\partial y}{\partial x_1} & \cdots & \frac{\partial y}{\partial x_n} \end{pmatrix} (行向量, 1×n1 \times n) | | yx\frac{\partial \mathbf{y}}{\partial \mathbf{x}} | (yx)ij=yjxi\left(\frac{\partial \mathbf{y}}{\partial \mathbf{x}}\right)_{ij} = \frac{\partial y_j}{\partial x_i} (雅可比矩阵, n×mn \times m) | (yx)ij=yixj\left(\frac{\partial \mathbf{y}}{\partial \mathbf{x}}\right)_{ij} = \frac{\partial y_i}{\partial x_j} (雅可比矩阵, m×nm \times n) |

关键记忆点:在分子布局中,yx\frac{\partial \mathbf{y}}{\partial \mathbf{x}} 的第 ii 行是由 yiy_ix\mathbf{x} 中所有元素求导得到的行向量。这种约定使得链式法则的表达非常自然。

2. 链式法则与反向传播

链式法则是反向传播算法的数学核心。考虑一个简单的计算图:xfygz\mathbf{x} \xrightarrow{f} \mathbf{y} \xrightarrow{g} z。其中 zz 是最终的标量损失(Loss)。我们的目标是计算损失 zz 对输入 x\mathbf{x} 的梯度 zx\frac{\partial z}{\partial \mathbf{x}}

根据链式法则(采用分子布局):

zx=zyyx\frac{\partial z}{\partial \mathbf{x}} = \frac{\partial z}{\partial \mathbf{y}} \frac{\partial \mathbf{y}}{\partial \mathbf{x}}
  • zy\frac{\partial z}{\partial \mathbf{y}}:是一个 1×m1 \times m 的行向量,表示损失对中间变量 y\mathbf{y} 的梯度。这通常是从更深层网络反向传播过来的“上游梯度”。
  • yx\frac{\partial \mathbf{y}}{\partial \mathbf{x}}:是一个 m×nm \times n 的雅可比矩阵,表示函数 ff 的局部导数。
  • zx\frac{\partial z}{\partial \mathbf{x}}:是一个 1×n1 \times n 的行向量,是我们要计算的“下游梯度”。

反向传播的本质:反向传播是一种在计算图上高效应用链式法则的算法。它从最终的损失节点开始,将梯度(一个标量 1)乘以当前节点的局部雅可比矩阵,然后将结果传递给其父节点。这个过程一直持续到所有需要梯度的节点(如模型参数、输入)都被访问。通过动态规划的思想,它避免了对共享子图的重复计算。

3. 线性层 y = x W^T 的梯度推导

这是面试中非常经典的问题。我们使用“形状推断”法,这在工程实践中最为直观和常用。

设定:

  • 输入: xRN×Din\mathbf{x} \in \mathbb{R}^{N \times D_{in}} (N个样本, 每个样本DinD_{in}维)
  • 权重: WRDout×Din\mathbf{W} \in \mathbb{R}^{D_{out} \times D_{in}}
  • 输出: y=xWTRN×Dout\mathbf{y} = \mathbf{x} \mathbf{W}^T \in \mathbb{R}^{N \times D_{out}}
  • 上游梯度 (已知): LyRN×Dout\frac{\partial L}{\partial \mathbf{y}} \in \mathbb{R}^{N \times D_{out}} (通常表示为 grad_y)

目标:

  1. 计算 LxRN×Din\frac{\partial L}{\partial \mathbf{x}} \in \mathbb{R}^{N \times D_{in}} (传播给前一层的梯度)
  2. 计算 LWRDout×Din\frac{\partial L}{\partial \mathbf{W}} \in \mathbb{R}^{D_{out} \times D_{in}} (用于更新权重的梯度)

推导过程:

  1. 计算 Lx\frac{\partial L}{\partial \mathbf{x}} (grad_x)

    • 前向传播是 y=xWT\mathbf{y} = \mathbf{x} \mathbf{W}^T
    • 我们可以将 WT\mathbf{W}^T 看作是作用于 x\mathbf{x} 的一个线性变换。
    • 根据链式法则的矩阵形式,为了得到 Lx\frac{\partial L}{\partial \mathbf{x}},我们需要将上游梯度 Ly\frac{\partial L}{\partial \mathbf{y}} 乘以这个变换的“另一部分”。
    • 形式上,Lx=Lyyx\frac{\partial L}{\partial \mathbf{x}} = \frac{\partial L}{\partial \mathbf{y}} \frac{\partial \mathbf{y}}{\partial \mathbf{x}}。这里的 yx\frac{\partial \mathbf{y}}{\partial \mathbf{x}} 可以直观地理解为 W\mathbf{W}
    • 梯度流动的方向与前向计算相反,所以我们用上游梯度 grad_y 左乘 W\mathbf{W}
    • 公式: Lx=LyW\frac{\partial L}{\partial \mathbf{x}} = \frac{\partial L}{\partial \mathbf{y}} \mathbf{W}
    • 形状检查: (N×Dout)(Dout×Din)(N×Din)(N \times D_{out}) \cdot (D_{out} \times D_{in}) \rightarrow (N \times D_{in})。形状与 x\mathbf{x} 一致,正确。
  2. 计算 LW\frac{\partial L}{\partial \mathbf{W}} (grad_W)

    • 这一步稍微复杂,因为 W\mathbf{W} 在乘法中被转置了。我们先求 LWT\frac{\partial L}{\partial \mathbf{W}^T}
    • 前向传播是 y=xWT\mathbf{y} = \mathbf{x} \mathbf{W}^T。现在我们将 x\mathbf{x} 看作是作用于 WT\mathbf{W}^T 的线性变换。
    • 梯度流动的方向与前向计算相反,所以我们用 x\mathbf{x} 的转置左乘上游梯度 grad_y
    • 公式 for WT\mathbf{W}^T: LWT=xTLy\frac{\partial L}{\partial \mathbf{W}^T} = \mathbf{x}^T \frac{\partial L}{\partial \mathbf{y}}
    • 形状检查: (Din×N)(N×Dout)(Din×Dout)(D_{in} \times N) \cdot (N \times D_{out}) \rightarrow (D_{in} \times D_{out})。形状与 WT\mathbf{W}^T 一致,正确。
    • 最后,我们知道 (LA)T=LAT(\frac{\partial L}{\partial \mathbf{A}})^T = \frac{\partial L}{\partial \mathbf{A}^T}
    • 因此,LW=(LWT)T=(xTLy)T=(Ly)Tx\frac{\partial L}{\partial \mathbf{W}} = \left(\frac{\partial L}{\partial \mathbf{W}^T}\right)^T = \left(\mathbf{x}^T \frac{\partial L}{\partial \mathbf{y}}\right)^T = \left(\frac{\partial L}{\partial \mathbf{y}}\right)^T \mathbf{x}
    • 最终公式: LW=(Ly)Tx\frac{\partial L}{\partial \mathbf{W}} = \left(\frac{\partial L}{\partial \mathbf{y}}\right)^T \mathbf{x}
    • 形状检查: (Dout×N)(N×Din)(Dout×Din)(D_{out} \times N) \cdot (N \times D_{in}) \rightarrow (D_{out} \times D_{in})。形状与 W\mathbf{W} 一致,正确。

总结:

  • Lx=LyW\frac{\partial L}{\partial \mathbf{x}} = \frac{\partial L}{\partial \mathbf{y}} \mathbf{W}
  • LW=(Ly)Tx\frac{\partial L}{\partial \mathbf{W}} = \left(\frac{\partial L}{\partial \mathbf{y}}\right)^T \mathbf{x}

代码实现

以下代码手动实现了 y = x @ W.T 的反向梯度计算,并与 PyTorch 的 autograd 结果进行对比验证。

python
1import torch
2
3# ---- 1. 设置参数和输入 ----
4# 定义维度
5N = 4 # batch_size, 样本数量
6D_in = 10 # 输入维度
7D_out = 5 # 输出维度
8
9# 创建输入张量 x 和权重矩阵 W
10# 使用 float64 以获得更高的精度,便于精确比较
11x = torch.randn(N, D_in, dtype=torch.float64)
12W = torch.randn(D_out, D_in, dtype=torch.float64)
13
14# ---- 2. 手动实现前向和反向传播 ----
15print("--- 手动计算 ---")
16
17# 前向传播
18# y = x @ W.T
19y_manual = x @ W.T
20
21# 假设我们从后续层得到了 y 的梯度 grad_y
22# grad_y 的形状必须和 y 相同
23grad_y = torch.randn(N, D_out, dtype=torch.float64)
24
25# 手动计算反向梯度
26# 根据我们推导的公式进行计算
27# grad_x = grad_y @ W
28# grad_W = grad_y.T @ x
29grad_x_manual = grad_y @ W
30# 为什么是 grad_y.T @ x?
31# 这是公式 (∂L/∂y)^T @ x 的直接实现。
32# (∂L/∂y) 的形状是 (N, D_out),转置后是 (D_out, N)
33# x 的形状是 (N, D_in)
34# 矩阵乘法 (D_out, N) @ (N, D_in) -> (D_out, D_in),这正是 W 的形状。
35grad_W_manual = grad_y.T @ x
36
37print("手动计算的 grad_x shape:", grad_x_manual.shape)
38print("手动计算的 grad_W shape:", grad_W_manual.shape)
39
40
41# ---- 3. 使用 PyTorch autograd 进行验证 ----
42print("\n--- PyTorch Autograd 验证 ---")
43
44# 复制 x 和 W,并设置 requires_grad=True 以追踪计算图
45x_auto = x.clone().detach().requires_grad_(True)
46W_auto = W.clone().detach().requires_grad_(True)
47
48# PyTorch 的前向传播
49y_auto = x_auto @ W_auto.T
50
51# 调用 backward() 方法进行自动求导
52# backward() 需要一个与输出张量形状相同的上游梯度
53# 这里我们传入与手动计算中相同的 grad_y
54# 这等价于计算 L = sum(y_auto * grad_y),然后对 L 求导
55y_auto.backward(gradient=grad_y)
56
57print("Autograd 计算的 x.grad shape:", x_auto.grad.shape)
58print("Autograd 计算的 W.grad shape:", W_auto.grad.shape)
59
60
61# ---- 4. 比较结果 ----
62print("\n--- 结果比较 ---")
63
64# 使用 torch.allclose 比较两个浮点张量是否足够接近
65# atol (absolute tolerance) 设置一个很小的容忍误差
66x_grad_match = torch.allclose(grad_x_manual, x_auto.grad, atol=1e-10)
67W_grad_match = torch.allclose(grad_W_manual, W_auto.grad, atol=1e-10)
68
69print(f"手动计算的 grad_x 与 autograd 结果是否一致: {x_grad_match}")
70print(f"手动计算的 grad_W 与 autograd 结果是否一致: {W_grad_match}")
71
72assert x_grad_match
73assert W_grad_match

工程实践

  • 使用场景: 反向传播是训练所有现代深度神经网络(DNN, CNN, RNN, Transformer等)的核心算法。虽然框架会自动处理,但理解其原理对于设计新的网络层(custom layer)、实现复杂的模型或进行算法优化至关重要。
  • 超参数选择: 反向传播本身没有超参数。但它计算出的梯度被优化器(如 SGD, Adam)使用,而优化器的超参数(如学习率、动量)对训练至关重要。梯度的尺度会影响学习率的选择。
  • 性能 / 显存 / 吞吐:
    • 性能: 现代深度学习框架(PyTorch, TensorFlow)的 backward() 调用的是高度优化的 C++/CUDA 后端代码。矩阵乘法(@torch.matmul)是其中最核心的计算,在 GPU 上通过 cuBLAS 等库实现并行加速。
    • 显存: 反向传播需要存储前向传播过程中的中间激活值,以便在反向计算局部梯度时使用。例如,在计算 grad_W 时,我们需要 xgrad_y。这导致训练时的显存占用远大于推理时。技术如“梯度检查点(Gradient Checkpointing)”通过牺牲计算时间来换取显存。
    • 吞吐: 梯度累积(Gradient Accumulation)是一种常见技巧。通过多次前向/反向传播但不执行优化器步骤(optimizer.step()),将梯度累积起来,等效于使用一个更大的 batch size 进行训练,这在显存有限无法容纳大 batch 时非常有用。
  • 常见坑和调试技巧:
    • 维度不匹配: 这是最常见的错误。手动实现反向传播时,一定要时刻检查矩阵乘法的维度是否正确。使用“形状推断”法是最好的防错手段。
    • 梯度检查 (Gradient Checking): 对于自定义的复杂层,一个强大的调试工具是梯度检查。通过有限差分法数值估算梯度(f(x)f(x+h)f(xh)2hf'(x) \approx \frac{f(x+h) - f(x-h)}{2h}),并与反向传播计算出的解析梯度进行比较。如果两者差异很大,说明反向传播的实现有误。由于数值法计算成本高,这通常只用于测试阶段。

常见误区与边界情况

  • 误区一:混淆布局约定

    • 初学者可能会在网上看到不同布局的公式,导致混淆。关键是记住:你所使用的框架(PyTorch/TF)内部是统一的(分子布局)。在面试中,可以声明你将使用分子布局,然后在此约定下进行所有推导。
  • 误区二:认为反向传播就是链式法则

    • 反向传播是应用链式法则的一种高效算法。它不仅仅是数学规则,还包含了计算图的拓扑排序、动态规划/记忆化(避免重复计算)等算法思想。直接暴力应用链式法则会导致指数级的计算复杂度,而反向传播的复杂度与前向传播在同一数量级。
  • 边界情况与面试追问:

    • 问:如果网络中某个参数 W 不影响最终的 Loss L,它的梯度会是什么?
      • 答:梯度为 0 或 None。因为在计算图中,从 L 到 W 不存在任何路径,所以 LW=0\frac{\partial L}{\partial W} = 0。框架通常会返回 None 或一个零张量。
    • 问:链式法则的连乘形式会带来什么问题?
      • 答:梯度消失/爆炸 (Vanishing/Exploding Gradients)。在一个很深的网络中,梯度是大量雅可比矩阵的连乘。如果这些矩阵的范数(或奇异值)持续小于1,梯度会指数级衰减至0(梯度消失),导致浅层网络无法更新。反之,如果持续大于1,梯度会指数级增长(梯度爆炸),导致训练不稳定。ReLU激活函数、残差连接(ResNet)、归一化层(BatchNorm)等技术在很大程度上缓解了这些问题。
    • 问:在低精度训练(如FP16)中,反向传播有什么需要注意的?
      • 答:FP16 表示的数值范围更小,精度更低。小的梯度值可能在FP16中下溢(underflow)变成0,导致模型参数停止更新。混合精度训练(Mixed Precision Training)通过“损失缩放(Loss Scaling)”技术解决此问题:在反向传播前,将损失值乘以一个大的缩放因子 S,所有梯度也相应放大 S 倍,从而避免下溢。在更新权重前,再将梯度除以 S 恢复其原始值。
相关题目