Skip to content

Latest commit

 

History

History
2118 lines (1547 loc) · 74.5 KB

File metadata and controls

2118 lines (1547 loc) · 74.5 KB

算子前向与反向传播原理详解

本文档详细说明 OriginDL 框架中各个算子的前向传播与反向传播数学原理,并通过具体例子展示计算过程,帮助快速理解算子实现。

目录


一、数学运算算子

1.1 加法算子(Add)

1.1.1 前向传播原理

加法算子的前向传播执行元素级加法运算:y = x0 + x1

数学描述:

  • 对于相同位置的元素,输出等于两个输入对应元素的和:y[i] = x0[i] + x1[i]
  • 当两个输入张量的形状不同时,支持广播机制:较小的张量会在缺失维度上扩展,然后进行元素级加法
  • 广播规则:从最右边的维度开始,如果维度大小相同或其中一个为 1,则可以广播

1.1.2 反向传播原理

对于加法运算 y = x0 + x1,根据链式法则和偏导数:

  • ∂y/∂x0 = 1∂y/∂x1 = 1

因此,如果输出梯度为 gy,则:

  • gx0 = gy × ∂y/∂x0 = gy × 1 = gy
  • gx1 = gy × ∂y/∂x1 = gy × 1 = gy

数学原理:

  • 加法对每个输入的偏导数都是 1,因此梯度直接传递
  • 当发生广播时,被广播的输入在多个位置被使用,其梯度需要累加所有使用位置的梯度
  • 具体地,如果 x1 被广播到形状 S,而原始形状是 S0,则 gx1 需要在广播维度上求和:gx1 = sum(gy, dims=广播维度)

1.1.3 计算示例

示例 1.1.3.1:相同形状的加法

前向传播:

  • 输入:x0 = [[1, 2], [3, 4]],形状 (2, 2)
  • 输入:x1 = [[5, 6], [7, 8]],形状 (2, 2)
  • 输出:y = x0 + x1 = [[6, 8], [10, 12]],形状 (2, 2)

反向传播:

  • 输出梯度:gy = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)
  • 输入梯度:
    • gx0 = gy = [[0.1, 0.2], [0.3, 0.4]]
    • gx1 = gy = [[0.1, 0.2], [0.3, 0.4]]
示例 1.1.3.2:广播加法

前向传播:

  • 输入:x0 = [[1, 2], [3, 4]],形状 (2, 2)
  • 输入:x1 = [10, 20],形状 (2,)
  • 广播后:x1 广播为 [[10, 20], [10, 20]],形状 (2, 2)
  • 输出:y = [[11, 22], [13, 24]],形状 (2, 2)

反向传播:

  • 输出梯度:gy = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)
  • 初始梯度:
    • gx0 = gy = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)(匹配原始形状)
    • gx1 = gy = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)(不匹配原始形状 (2,)
  • 形状恢复:
    • gx0 形状已匹配,无需调整
    • gx1 需要压缩:gx1 = sum_to([[0.1, 0.2], [0.3, 0.4]], (2,)) = [0.1+0.3, 0.2+0.4] = [0.4, 0.6]
  • 最终梯度:
    • gx0 = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)
    • gx1 = [0.4, 0.6],形状 (2,)

数学解释: 在广播加法中,x1 的每个元素被加到 x0 的对应行上。反向传播时,x1 的梯度需要累加所有使用该元素的位置的梯度:

  • x1[0] = 10 被用于 y[0,0]y[1,0],所以 gx1[0] = gy[0,0] + gy[1,0] = 0.1 + 0.3 = 0.4
  • x1[1] = 20 被用于 y[0,1]y[1,1],所以 gx1[1] = gy[0,1] + gy[1,1] = 0.2 + 0.4 = 0.6

1.2 减法算子(Sub)

1.2.1 前向传播原理

减法算子的前向传播执行元素级减法运算:y = x0 - x1

数学描述:

  • 对于相同位置的元素,输出等于两个输入对应元素的差:y[i] = x0[i] - x1[i]
  • 当两个输入张量的形状不同时,支持广播机制:较小的张量会在缺失维度上扩展,然后进行元素级减法
  • 广播规则:从最右边的维度开始,如果维度大小相同或其中一个为 1,则可以广播

1.2.2 反向传播原理

对于减法运算 y = x0 - x1,根据链式法则和偏导数:

  • ∂y/∂x0 = 1∂y/∂x1 = -1

因此,如果输出梯度为 gy,则:

  • gx0 = gy × ∂y/∂x0 = gy × 1 = gy
  • gx1 = gy × ∂y/∂x1 = gy × (-1) = -gy

数学原理:

  • 减法对第一个输入的偏导数是 1,对第二个输入的偏导数是 -1
  • 当发生广播时,被广播的输入在多个位置被使用,其梯度需要累加所有使用位置的梯度(第二个输入需要取负号后累加)

1.2.3 计算示例

示例 1.2.3.1:相同形状的减法

前向传播:

  • 输入:x0 = [[5, 6], [7, 8]],形状 (2, 2)
  • 输入:x1 = [[1, 2], [3, 4]],形状 (2, 2)
  • 输出:y = x0 - x1 = [[4, 4], [4, 4]],形状 (2, 2)

反向传播:

  • 输出梯度:gy = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)
  • 输入梯度:
    • gx0 = gy = [[0.1, 0.2], [0.3, 0.4]]
    • gx1 = -gy = [[-0.1, -0.2], [-0.3, -0.4]]
示例 1.2.3.2:广播减法

前向传播:

  • 输入:x0 = [[5, 6], [7, 8]],形状 (2, 2)
  • 输入:x1 = [1, 2],形状 (2,)
  • 广播后:x1 广播为 [[1, 2], [1, 2]],形状 (2, 2)
  • 输出:y = [[4, 4], [6, 6]],形状 (2, 2)

反向传播:

  • 输出梯度:gy = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)
  • 初始梯度:
    • gx0 = gy = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)(匹配原始形状)
    • gx1 = -gy = [[-0.1, -0.2], [-0.3, -0.4]],形状 (2, 2)(不匹配原始形状 (2,)
  • 形状恢复:
    • gx0 形状已匹配,无需调整
    • gx1 需要压缩:gx1 = sum_to([[-0.1, -0.2], [-0.3, -0.4]], (2,)) = [-0.1+(-0.3), -0.2+(-0.4)] = [-0.4, -0.6]
  • 最终梯度:
    • gx0 = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)
    • gx1 = [-0.4, -0.6],形状 (2,)

数学解释: 在广播减法中,x1 的每个元素被从 x0 的对应行中减去。反向传播时,x1 的梯度需要累加所有使用该元素的位置的梯度(注意是负号):

  • x1[0] = 1 被用于 y[0,0]y[1,0],所以 gx1[0] = -gy[0,0] + (-gy[1,0]) = -0.1 + (-0.3) = -0.4
  • x1[1] = 2 被用于 y[0,1]y[1,1],所以 gx1[1] = -gy[0,1] + (-gy[1,1]) = -0.2 + (-0.4) = -0.6

1.3 乘法算子(Mul)

1.3.1 前向传播原理

乘法算子的前向传播执行元素级乘法运算:y = x0 × x1

数学描述:

  • 对于相同位置的元素,输出等于两个输入对应元素的乘积:y[i] = x0[i] × x1[i]
  • 当两个输入张量的形状不同时,支持广播机制:较小的张量会在缺失维度上扩展,然后进行元素级乘法
  • 广播规则:从最右边的维度开始,如果维度大小相同或其中一个为 1,则可以广播

1.3.2 反向传播原理

对于乘法运算 y = x0 × x1,根据链式法则和偏导数:

  • ∂y/∂x0 = x1∂y/∂x1 = x0

因此,如果输出梯度为 gy,则:

  • gx0 = gy × ∂y/∂x0 = gy × x1
  • gx1 = gy × ∂y/∂x1 = gy × x0

数学原理:

  • 乘法对第一个输入的偏导数是第二个输入的值,对第二个输入的偏导数是第一个输入的值
  • 当发生广播时,被广播的输入在多个位置被使用,其梯度需要累加所有使用位置的梯度

1.3.3 计算示例

示例 1.3.3.1:相同形状的乘法

前向传播:

  • 输入:x0 = [[1, 2], [3, 4]],形状 (2, 2)
  • 输入:x1 = [[5, 6], [7, 8]],形状 (2, 2)
  • 输出:y = x0 × x1 = [[5, 12], [21, 32]],形状 (2, 2)

反向传播:

  • 输出梯度:gy = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)
  • 输入梯度:
    • gx0 = gy × x1 = [[0.1, 0.2], [0.3, 0.4]] × [[5, 6], [7, 8]] = [[0.5, 1.2], [2.1, 3.2]]
    • gx1 = gy × x0 = [[0.1, 0.2], [0.3, 0.4]] × [[1, 2], [3, 4]] = [[0.1, 0.4], [0.9, 1.6]]
示例 1.3.3.2:广播乘法

前向传播:

  • 输入:x0 = [[1, 2], [3, 4]],形状 (2, 2)
  • 输入:x1 = [5, 6],形状 (2,)
  • 广播后:x1 广播为 [[5, 6], [5, 6]],形状 (2, 2)
  • 输出:y = [[1×5, 2×6], [3×5, 4×6]] = [[5, 12], [15, 24]],形状 (2, 2)

反向传播:

  • 输出梯度:gy = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)
  • 初始梯度:
    • gx0 = gy × x1_broadcast = [[0.1, 0.2], [0.3, 0.4]] × [[5, 6], [5, 6]] = [[0.5, 1.2], [1.5, 2.4]],形状 (2, 2)(匹配 x0 原始形状)
    • gx1 = gy × x0 = [[0.1, 0.2], [0.3, 0.4]] × [[1, 2], [3, 4]] = [[0.1, 0.4], [0.9, 1.6]],形状 (2, 2)(不匹配 x1 原始形状 (2,)
  • 形状恢复(对 gx1 执行 sum_to):
    • gx1 = sum_to([[0.1, 0.4], [0.9, 1.6]], (2,)) = [0.1+0.9, 0.4+1.6] = [1.0, 2.0],形状 (2,)

数学解释:

  • x1[0] = 5 被用于 y[0,0] = 1×5y[1,0] = 3×5,所以 gx1[0] = gy[0,0]×1 + gy[1,0]×3 = 0.1×1 + 0.3×3 = 1.0
  • x1[1] = 6 被用于 y[0,1] = 2×6y[1,1] = 4×6,所以 gx1[1] = gy[0,1]×2 + gy[1,1]×4 = 0.2×2 + 0.4×4 = 2.0

1.4 除法算子(Div)

1.4.1 前向传播原理

除法算子的前向传播执行元素级除法运算:y = x0 / x1

数学描述:

  • 对于相同位置的元素,输出等于两个输入对应元素的商:y[i] = x0[i] / x1[i]
  • 当两个输入张量的形状不同时,支持广播机制:较小的张量会在缺失维度上扩展,然后进行元素级除法
  • 广播规则:从最右边的维度开始,如果维度大小相同或其中一个为 1,则可以广播
  • 注意:当 x1[i] = 0 时,除法未定义,通常会产生错误或特殊值(如 inf)

1.4.2 反向传播原理

对于除法运算 y = x0 / x1,根据链式法则和偏导数:

  • ∂y/∂x0 = 1 / x1
  • ∂y/∂x1 = -x0 / x1²

因此,如果输出梯度为 gy,则:

  • gx0 = gy × ∂y/∂x0 = gy / x1
  • gx1 = gy × ∂y/∂x1 = -gy × x0 / x1²

数学原理:

  • 除法对第一个输入的偏导数是 1/x1,对第二个输入的偏导数是 -x0/x1²
  • 当发生广播时,被广播的输入在多个位置被使用,其梯度需要累加所有使用位置的梯度

1.4.3 计算示例

示例 1.4.3.1:相同形状的除法

前向传播:

  • 输入:x0 = [[6, 8], [10, 12]],形状 (2, 2)
  • 输入:x1 = [[2, 2], [2, 3]],形状 (2, 2)
  • 输出:y = x0 / x1 = [[3, 4], [5, 4]],形状 (2, 2)

反向传播:

  • 输出梯度:gy = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)
  • 输入梯度:
    • gx0 = gy / x1 = [[0.1, 0.2], [0.3, 0.4]] / [[2, 2], [2, 3]] = [[0.05, 0.1], [0.15, 0.133...]]
    • gx1 = -gy × x0 / x1² = -[[0.1, 0.2], [0.3, 0.4]] × [[6, 8], [10, 12]] / [[4, 4], [4, 9]] = [[-0.15, -0.4], [-0.75, -0.533...]]
示例 1.4.3.2:广播除法

前向传播:

  • 输入:x0 = [[6, 8], [10, 12]],形状 (2, 2)
  • 输入:x1 = [2, 4],形状 (2,)
  • 广播后:x1 广播为 [[2, 4], [2, 4]],形状 (2, 2)
  • 输出:y = x0 / x1_broadcast = [[6/2, 8/4], [10/2, 12/4]] = [[3, 2], [5, 3]],形状 (2, 2)

反向传播:

  • 输出梯度:gy = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)
  • 初始梯度(使用广播后的 x1_broadcast):
    • gx0 = gy / x1_broadcast = [[0.1, 0.2], [0.3, 0.4]] / [[2, 4], [2, 4]] = [[0.05, 0.05], [0.15, 0.1]],形状 (2, 2)(匹配 x0 原始形状)
    • gx1 = -gy × x0 / x1_broadcast²
      • x1_broadcast² = [[4, 16], [4, 16]]
      • -gy × x0 / x1_broadcast² = -[[0.1×6/4, 0.2×8/16], [0.3×10/4, 0.4×12/16]] = [[-0.15, -0.1], [-0.75, -0.3]],形状 (2, 2)(不匹配 x1 原始形状 (2,)
  • 形状恢复(对 gx1 执行 sum_to):
    • gx1 = sum_to([[-0.15, -0.1], [-0.75, -0.3]], (2,)) = [-0.15+(-0.75), -0.1+(-0.3)] = [-0.9, -0.4],形状 (2,)

数学解释:

  • x1[0] = 2 被用于 y[0,0] = 6/2y[1,0] = 10/2,所以 gx1[0] = -gy[0,0]×6/2² + -gy[1,0]×10/2² = -0.15 + -0.75 = -0.9
  • x1[1] = 4 被用于 y[0,1] = 8/4y[1,1] = 12/4,所以 gx1[1] = -gy[0,1]×8/4² + -gy[1,1]×12/4² = -0.1 + -0.3 = -0.4

1.5 矩阵乘法算子(MatMul)

1.5.1 前向传播原理

矩阵乘法算子的前向传播执行矩阵乘法运算:y = x0 @ x1

数学描述:

  • 输入:两个矩阵 x0 形状 (m, k)x1 形状 (k, n)
  • 输出:矩阵 y 形状 (m, n),其中 y[i, j] = sum(x0[i, :] × x1[:, j])
  • 维度要求:x0 的列数必须等于 x1 的行数
  • 支持批量矩阵乘法:x0 形状 (batch, m, k)x1 形状 (k, n),输出形状 (batch, m, n)

数学表示:

$$ y = x_0 \times x_1 $$

$$ y_{i j} = \sum_{k} x_{0, i k}, x_{1, k j} $$

1.5.2 反向传播原理

对于矩阵乘法 y = x0 @ x1,根据链式法则和偏导数:

  • ∂y/∂x0 = gy @ transpose(x1)
  • ∂y/∂x1 = transpose(x0) @ gy

因此,如果输出梯度为 gy,则:

  • gx0 = gy @ transpose(x1)
  • gx1 = transpose(x0) @ gy

数学原理:

  • 矩阵乘法对第一个输入的梯度是输出梯度与第二个输入转置的矩阵乘法
  • 矩阵乘法对第二个输入的梯度是第一个输入转置与输出梯度的矩阵乘法
  • 转置操作确保了维度匹配

1.5.3 计算示例

示例 1.5.3.1:矩阵乘法前向与反向传播

前向传播:

  • 输入:x0 = [[1, 2], [3, 4]],形状 (2, 2)
  • 输入:x1 = [[5, 6], [7, 8]],形状 (2, 2)
  • 输出:y = x0 @ x1 = [[1×5+2×7, 1×6+2×8], [3×5+4×7, 3×6+4×8]] = [[19, 22], [43, 50]],形状 (2, 2)

反向传播:

  • 输出梯度:gy = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)
  • 输入梯度:
    • gx0 = gy @ transpose(x1) = [[0.1, 0.2], [0.3, 0.4]] @ [[5, 7], [6, 8]] = [[1.7, 2.3], [3.9, 5.3]]
    • gx1 = transpose(x0) @ gy = [[1, 3], [2, 4]] @ [[0.1, 0.2], [0.3, 0.4]] = [[1.0, 1.4], [1.4, 2.0]]

1.6 幂运算算子(Pow)

1.6.1 前向传播原理

幂运算算子的前向传播执行:$y = x^{\text{exponent}}$

数学描述:

  • 输入:张量 $x$ 和标量指数 $\text{exponent}$

  • 输出:$y_i = x_i^{\text{exponent}}$

  • 数学表达式:

    $$ y = \operatorname{pow}(x, \text{exponent}) = x^{\text{exponent}} $$

  • 支持任意实数指数,包括分数指数(开方)

1.6.2 反向传播原理

对于幂运算 $y = x^{\text{exponent}}$,根据链式法则和偏导数:

$$ \frac{\partial y}{\partial x} = \text{exponent} \cdot x^{\text{exponent}-1} $$

因此,如果输出梯度为 $g_y$,则:

$$ g_x = g_y \times \frac{\partial y}{\partial x} = g_y \times \text{exponent} \cdot x^{\text{exponent}-1} $$

数学原理:

  • 幂函数的导数是 $\text{exponent} \cdot x^{\text{exponent}-1}$
  • $\text{exponent} = 0$ 时,$y = 1$,梯度为 0
  • $\text{exponent} = 1$ 时,$y = x$,梯度为 $g_y$
  • $\text{exponent} = 2$ 时,$y = x^2$,梯度为 $g_y \times 2x$

1.6.3 计算示例

示例 1.6.3.1:平方运算(exponent = 2)

前向传播:

  • 输入:x = [[1, 2], [3, 4]],形状 (2, 2)
  • 指数:exponent = 2
  • 输出:y = x² = [[1, 4], [9, 16]],形状 (2, 2)

反向传播:

  • 输出梯度:gy = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)
  • 输入梯度:gx = gy × 2 × x = [[0.1, 0.2], [0.3, 0.4]] × 2 × [[1, 2], [3, 4]] = [[0.2, 0.8], [1.8, 3.2]]

1.7 指数算子(Exp)

1.7.1 前向传播原理

在 OriginDL 中,指数函数特指以自然常数 e 为底的指数函数,即 exp(x) = e^x,而不是一般形式的 a^x(其中 a 为任意底数)。

指数算子的前向传播执行:$y = \exp(x)$

数学描述:

  • 输入:张量 $x$(可以是任意实数)

  • 输出:$y_i = \exp(x_i) = e^{x_i}$

  • 数学表达式:

    $$ y = \exp(x) $$

  • 指数函数的输出范围是 $(0, +\infty)$,无论输入是正数、负数还是零,输出总是大于 0

1.7.2 反向传播原理

对于指数运算 $y = \exp(x)$,根据链式法则和偏导数:

$$ \frac{\partial y}{\partial x} = \exp(x) = y $$

因此,如果输出梯度为 $g_y$,则:

$$ g_x = g_y \times \frac{\partial y}{\partial x} = g_y \times \exp(x) = g_y \times y $$

数学原理:

  • 指数函数的导数等于其自身:$\frac{\mathrm{d}}{\mathrm{d}x}\exp(x) = \exp(x)$
  • 反向传播时,梯度等于输出梯度乘以输出值本身
  • 这使得指数函数的梯度计算非常高效

1.7.3 计算示例

示例 1.7.3.1:指数运算前向与反向传播

前向传播:

  • 输入:x = [[0, 1], [2, -1]],形状 (2, 2)
  • 输出:y = exp(x) = [[1.0, 2.718...], [7.389..., 0.368...]],形状 (2, 2)

反向传播:

  • 输出梯度:gy = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)
  • 输入梯度:gx = gy × y = [[0.1, 0.2], [0.3, 0.4]] × [[1.0, 2.718...], [7.389..., 0.368...]] = [[0.1, 0.544...], [2.217..., 0.147...]]

1.8 对数算子(Log)

1.8.1 前向传播原理

在 OriginDL 中,对数函数特指以自然常数 e 为底的对数函数,即 log(x) = ln(x),等于数学上的自然对数。

对数算子的前向传播执行:$y = \log(x)$

数学描述:

  • 输入:张量 $x$(要求 $x > 0$

  • 输出:$y_i = \log(x_i) = \ln(x_i)$

  • 数学表达式(自然对数,以 $e$ 为底):

    $$ y = \log(x) $$

  • 对数函数将正数映射到实数范围

1.8.2 反向传播原理

对于对数运算 $y = \log(x)$,根据链式法则和偏导数:

$$ \frac{\partial y}{\partial x} = \frac{1}{x} $$

因此,如果输出梯度为 $g_y$,则:

$$ g_x = g_y \times \frac{\partial y}{\partial x} = \frac{g_y}{x} $$

数学原理:

  • 对数函数的导数是 $1/x$:$\frac{\mathrm{d}}{\mathrm{d}x}\log(x) = 1/x$
  • 反向传播时,梯度等于输出梯度除以输入值
  • 注意:当 $x \le 0$ 时,对数未定义,通常会产生错误或特殊值

1.8.3 计算示例

示例 1.8.3.1:对数运算前向与反向传播

前向传播:

  • 输入:x = [[1, 2.718...], [7.389..., 0.5]],形状 (2, 2)
  • 输出:y = log(x) = [[0, 1], [2, -0.693...]],形状 (2, 2)

反向传播:

  • 输出梯度:gy = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)
  • 输入梯度:gx = gy / x = [[0.1, 0.2], [0.3, 0.4]] / [[1, 2.718...], [7.389..., 0.5]] = [[0.1, 0.074...], [0.041..., 0.8]]

1.9 取负算子(Neg)

1.9.1 前向传播原理

取负算子的前向传播执行:$y = -x$

数学描述:

  • 输入:张量 $x$

  • 输出:$y_i = -x_i$

  • 数学表达式:

    $$ y = \operatorname{neg}(x) = -x $$

  • 取负操作将每个元素的值取相反数

1.9.2 反向传播原理

对于取负运算 $y = -x$,根据链式法则和偏导数:

$$ \frac{\partial y}{\partial x} = -1 $$

因此,如果输出梯度为 $g_y$,则:

$$ g_x = g_y \times \frac{\partial y}{\partial x} = -g_y $$

数学原理:

  • 取负函数的导数是 $-1$
  • 反向传播时,梯度等于输出梯度的相反数
  • 这是最简单的线性变换之一

1.9.3 计算示例

示例 1.9.3.1:取负运算前向与反向传播

前向传播:

  • 输入:x = [[1, 2], [3, 4]],形状 (2, 2)
  • 输出:y = -x = [[-1, -2], [-3, -4]],形状 (2, 2)

反向传播:

  • 输出梯度:gy = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)
  • 输入梯度:gx = -gy = [[-0.1, -0.2], [-0.3, -0.4]],形状 (2, 2)

1.10 平方算子(Square)

1.10.1 前向传播原理

平方算子的前向传播执行:$y = x^2$

数学描述:

  • 输入:张量 $x$

  • 输出:$y_i = x_i^2 = x_i \cdot x_i$

  • 数学表达式:

    $$ y = \operatorname{square}(x) = x^2 $$

  • 平方操作是幂运算的特例($\text{exponent} = 2$)

1.10.2 反向传播原理

对于平方运算 $y = x^2$,根据链式法则和偏导数:

$$ \frac{\partial y}{\partial x} = 2x $$

因此,如果输出梯度为 $g_y$,则:

$$ g_x = g_y \times \frac{\partial y}{\partial x} = g_y \times 2x $$

数学原理:

  • 平方函数的导数是 $2x$
  • 反向传播时,梯度等于输出梯度乘以 $2x$
  • 这是幂运算在 $\text{exponent} = 2$ 时的特例

1.10.3 计算示例

示例 1.10.3.1:平方运算前向与反向传播

前向传播:

  • 输入:x = [[1, 2], [3, 4]],形状 (2, 2)
  • 输出:y = x² = [[1, 4], [9, 16]],形状 (2, 2)

反向传播:

  • 输出梯度:gy = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)
  • 输入梯度:gx = gy × 2 × x = [[0.1, 0.2], [0.3, 0.4]] × 2 × [[1, 2], [3, 4]] = [[0.2, 0.8], [1.8, 3.2]]

1.11 求和算子(Sum)

1.11.1 前向传播原理

求和算子的前向传播执行:y = sum(x, axis=axis, keepdim=keepdim)

数学描述:

  • 输入:张量 x、指定的轴 axiskeepdim 参数
  • 输出:沿指定轴求和后的张量
  • 如果 axis = -1 或未指定,则对所有元素求和
  • 如果指定了 axis,则沿该轴求和
  • keepdim 参数控制是否保持维度:
    • keepdim=false(默认):输出维度减少 1(移除求和轴)
    • keepdim=true:输出维度保持不变,但求和轴的大小变为 1

数学表示:

  • 沿第 $k$ 维求和,移除该维度($\text{keepdim} = \mathrm{false}$):

    $$ y = \operatorname{sum}(x, \mathrm{axis}=k, \mathrm{keepdim}=\mathrm{false}) $$

    $$ y_{i_0, \dots, i_{k-1}, i_{k+1}, \dots, i_n} = \sum_{i_k} x_{i_0, \dots, i_k, \dots, i_n} $$

  • 沿第 $k$ 维求和,保持维度($\text{keepdim} = \mathrm{true}$):

    $$ y = \operatorname{sum}(x, \mathrm{axis}=k, \mathrm{keepdim}=\mathrm{true}) $$

    $$ y_{i_0, \dots, i_{k-1}, 1, i_{k+1}, \dots, i_n} = \sum_{i_k} x_{i_0, \dots, i_k, \dots, i_n} $$

1.11.2 反向传播原理

对于求和运算 y = sum(x, axis=k, keepdim=keepdim),根据链式法则和偏导数:

  • ∂y/∂x = 1(在求和维度上)

因此,如果输出梯度为 gy,则:

  • gx = broadcast(gy, x.shape)

数学原理:

  • 求和的反向传播是广播:输出梯度被广播回原始形状
  • 因为每个输入元素对输出的贡献是独立的,所以梯度直接传递
  • 广播操作确保梯度形状与输入形状匹配
  • keepdim 参数不影响反向传播逻辑,因为 broadcast_to 可以自动处理包含维度 1 的梯度形状

1.11.3 计算示例

示例 1.11.3.1:沿轴求和(keepdim=false)

前向传播:

  • 输入:x = [[1, 2, 3], [4, 5, 6]],形状 (2, 3)
  • 沿轴 0 求和:y = sum(x, axis=0, keepdim=false) = [1+4, 2+5, 3+6] = [5, 7, 9],形状 (3,)
    • 沿第 0 维(行)压缩,保留第 1 维(列)

反向传播:

  • 输出梯度:gy = [0.1, 0.2, 0.3],形状 (3,)
  • 输入梯度:gx = broadcast(gy, (2, 3)) = [[0.1, 0.2, 0.3], [0.1, 0.2, 0.3]],形状 (2, 3)
    • 梯度广播回原始形状,每行的梯度相同
示例 1.11.3.2:沿轴求和(keepdim=true)

前向传播:

  • 输入:x = [[1, 2, 3], [4, 5, 6]],形状 (2, 3)
  • 沿轴 0 求和,keepdim=true:y = sum(x, axis=0, keepdim=true) = [[5, 7, 9]],形状 (1, 3)
    • 沿第 0 维(行)压缩,但保持维度,第 0 维变为 1

反向传播:

  • 输出梯度:gy = [[0.1, 0.2, 0.3]],形状 (1, 3)
  • 输入梯度:gx = broadcast(gy, (2, 3)) = [[0.1, 0.2, 0.3], [0.1, 0.2, 0.3]],形状 (2, 3)
    • 梯度从 (1, 3) 广播回 (2, 3),每行的梯度相同
示例 1.11.3.3:全局求和(keepdim=false)

前向传播:

  • 输入:x = [[1, 2, 3], [4, 5, 6]],形状 (2, 3)
  • 全局求和:y = sum(x, axis=-1, keepdim=false) = 21,形状 (1,)
    • 所有维度都被压缩,输出标量(形状为 (1,)

反向传播:

  • 输出梯度:gy = 0.5(标量,形状 (1,)
  • 输入梯度:gx = broadcast(gy, (2, 3)) = [[0.5, 0.5, 0.5], [0.5, 0.5, 0.5]],形状 (2, 3)
    • 标量梯度广播到所有位置
示例 1.11.3.4:全局求和(keepdim=true)

前向传播:

  • 输入:x = [[1, 2, 3], [4, 5, 6]],形状 (2, 3)
  • 全局求和,keepdim=true:y = sum(x, axis=-1, keepdim=true) = 21,形状 (1, 1)
    • 所有维度都被压缩为 1,但保持维度数

反向传播:

  • 输出梯度:gy = 0.5(形状 (1, 1)
  • 输入梯度:gx = broadcast(gy, (2, 3)) = [[0.5, 0.5, 0.5], [0.5, 0.5, 0.5]],形状 (2, 3)
    • 梯度从 (1, 1) 广播回 (2, 3)

1.12 广播算子(BroadcastTo)

1.12.1 前向传播原理

广播算子的前向传播执行:y = broadcast_to(x, shape)

数学描述:

  • 输入:张量 $x$ 和目标形状 $\mathrm{shape}$
  • 输出:将 $x$ 广播到目标形状 $\mathrm{shape}$
  • 广播规则:从最右边的维度开始,如果维度大小相同或其中一个为 1,则可以广播
  • 广播时,大小为 1 的维度会被复制到目标大小

数学表示:

$$ y = \operatorname{broadcast_to}(x, \mathrm{shape}) $$

输出 $y$ 的形状为 $\mathrm{shape}$,但数据来自 $x$(在广播维度上重复)

1.12.2 反向传播原理

对于广播运算 y = broadcast_to(x, shape),根据链式法则:

  • 反向传播是压缩:gx = sum_to(gy, x.shape)

数学原理:

  • 广播的反向传播是压缩:输出梯度需要在广播维度上求和
  • 因为前向传播时,一个输入元素被复制到多个输出位置
  • 反向传播时,这些位置的梯度需要累加回对应的输入位置

1.12.3 计算示例

示例 1.12.3.1:广播前向与反向传播

前向传播:

  • 输入:x = [1, 2],形状 (2,)
  • 目标形状:(2, 2)
  • 输出:y = broadcast_to(x, (2, 2)) = [[1, 2], [1, 2]],形状 (2, 2)

反向传播:

  • 输出梯度:gy = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)
  • 输入梯度:gx = sum_to(gy, (2,)) = [0.1+0.3, 0.2+0.4] = [0.4, 0.6],形状 (2,)

数学解释:

  • x[0] = 1 被广播到 y[0,0]y[1,0],所以 gx[0] = gy[0,0] + gy[1,0] = 0.1 + 0.3 = 0.4
  • x[1] = 2 被广播到 y[0,1]y[1,1],所以 gx[1] = gy[0,1] + gy[1,1] = 0.2 + 0.4 = 0.6

1.13 压缩到指定形状算子(SumTo)

1.13.1 前向传播原理

压缩到指定形状算子的前向传播将输入张量压缩到目标形状:y = sum_to(x, target_shape)

数学描述:

  • 输入:张量 x,形状为 (d0, d1, ..., dm)
  • 目标形状:target_shape = (t0, t1, ..., tk),其中 k ≤ m
  • 输出:y,形状为 target_shape
  • 压缩规则:在需要压缩的维度上对元素求和,将维度大小从 di 压缩到 ti

压缩过程:

  • 如果 di > ti:在第 i 维上对元素求和,将维度大小从 di 压缩到 ti
  • 如果 di == ti:保持该维度不变
  • 如果 di < ti:这种情况不应该发生,会抛出错误

1.13.2 反向传播原理

对于压缩运算 y = sum_to(x, target_shape),根据链式法则和偏导数:

  • ∂y/∂x 在压缩维度上的偏导数为 1(因为求和操作对每个元素的偏导数为 1)

因此,如果输出梯度为 gy,则:

  • gx = broadcast_to(gy, x.shape)

数学原理:

  • 压缩操作的反向传播是广播操作
  • 输出梯度 gy 的形状是 target_shape,需要广播回输入形状 x.shape
  • 在压缩的维度上,梯度会被广播到所有原始元素位置

1.13.3 计算示例

示例 1.13.3.1:压缩到指定形状

前向传播:

  • 输入:x = [[1, 2, 3], [4, 5, 6]],形状 (2, 3)
  • 目标形状:target_shape = (2,)
  • 压缩:在第 1 维上求和,y = sum_to(x, (2,)) = [1+2+3, 4+5+6] = [6, 15],形状 (2,)

反向传播:

  • 输出梯度:gy = [0.1, 0.2],形状 (2,)
  • 广播:gx = broadcast_to(gy, (2, 3)) = [[0.1, 0.1, 0.1], [0.2, 0.2, 0.2]],形状 (2, 3)

1.14 比较算子(Compare)

比较算子包括:>(Greater)、>=(GreaterEqual)、<(Less)、<=(LessEqual)、==(Equal)、!=(NotEqual)。

本节以**大于算子(Greater)**为例详细介绍比较算子的原理,其他比较运算符的实现方式与大于算子类似,只是比较条件不同。

1.14.1 前向传播原理

大于算子的前向传播执行元素级比较运算:y = x > threshold

数学描述:

  • 输入:张量 x,形状为 (d0, d1, ..., dm)
  • 阈值:threshold,可以是标量(元素数量为 1)或与 x 相同形状的张量
  • 输出:y[i] = 1 如果 x[i] > threshold[i],否则 y[i] = 0
  • 输出形状:与输入 x 相同
  • 输出数据类型:与输入 x 相同,用0或1表示布尔值)

支持的模式:

  1. 相同形状比较:当 thresholdx 形状相同时,执行逐元素比较
  2. 标量广播比较:当 threshold 是标量(元素数量为 1)时,将标量广播到 x 的形状,然后执行逐元素比较

数学表达式:

$$ y_i = \begin{cases} 1, & x_i > \mathrm{threshold}_i \\ 0, & x_i \le \mathrm{threshold}_i \end{cases} $$

$\mathrm{threshold}$ 是标量时:

$$ y_i = \begin{cases} 1, & x_i > \mathrm{threshold} \\ 0, & x_i \le \mathrm{threshold} \end{cases} $$

1.14.2 反向传播原理

比较运算符是不可微的离散操作,在反向传播中对输入的梯度为零。

数学原理:

  • 比较操作 y = (x > threshold) 在几乎所有点处的偏导数 ∂y/∂x = 0
  • 这是因为比较操作只在 x = threshold 处发生跳变,而这是一个测度为零的集合
  • 参考 PyTorch 的实现,比较运算符的反向传播返回全零梯度

因此,如果输出梯度为 gy,则:

  • gx = zeros_like(x)(全零梯度,形状与 x 相同)
  • gthreshold = zeros_like(threshold)(全零梯度,形状与 threshold 相同)

数学原理:

  • 比较运算符本质上是不可微的,因为它们是离散的跳跃操作
  • 在深度学习中,比较运算符通常用于生成掩码(mask),而不是直接参与梯度计算
  • 如果需要梯度信息,通常会将比较结果转换为浮点数后再参与后续计算

1.14.3 计算示例

示例 1.14.3.1:标量阈值比较

前向传播:

  • 输入:x = [[1.0, 2.0], [3.0, 4.0]],形状 (2, 2)
  • 阈值:threshold = 2.0(标量)
  • 输出:y = [[0.0, 0.0], [1.0, 1.0]],形状 (2, 2)

反向传播:

  • 输出梯度:gy = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)
  • 输入梯度:
    • gx = [[0.0, 0.0], [0.0, 0.0]],形状 (2, 2)(全零)
    • gthreshold = 0.0,形状 ()(全零)
示例 1.14.3.2:相同形状张量比较

前向传播:

  • 输入:x = [[1.0, 2.0], [3.0, 4.0]],形状 (2, 2)
  • 阈值:threshold = [[2.0, 1.0], [3.0, 5.0]],形状 (2, 2)
  • 输出:y = [[0.0, 1.0], [0.0, 0.0]],形状 (2, 2)

反向传播:

  • 输出梯度:gy = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)
  • 输入梯度:
    • gx = [[0.0, 0.0], [0.0, 0.0]],形状 (2, 2)(全零)
    • gthreshold = [[0.0, 0.0], [0.0, 0.0]],形状 (2, 2)(全零)

1.14.4 其他比较运算符

其他比较运算符(>=<<===!=)的实现方式与大于算子类似:

  • 大于等于(GreaterEqual)y = (x >= threshold) ? 1 : 0
  • 小于(Less)y = (x < threshold) ? 1 : 0
  • 小于等于(LessEqual)y = (x <= threshold) ? 1 : 0
  • 等于(Equal)y = (x == threshold) ? 1 : 0
  • 不等于(NotEqual)y = (x != threshold) ? 1 : 0

所有比较运算符的反向传播都返回全零梯度,因为它们都是不可微的离散操作。


二、激活函数算子

2.1 ReLU 算子

2.1.1 前向传播原理

ReLU(Rectified Linear Unit)算子的前向传播执行:$y = \max(0, x)$

数学描述:

  • 对于输入 $x$ 的每个元素,如果 $x_i &gt; 0$,则 $y_i = x_i$;否则 $y_i = 0$

  • 数学表达式:

    $$ y = \operatorname{ReLU}(x) = \max(0, x) $$

  • ReLU 是一个非线性激活函数,用于引入非线性特性

2.1.2 反向传播原理

对于 ReLU 运算 $y = \operatorname{ReLU}(x)$,其导数(梯度)为:

$$ \frac{\partial y}{\partial x}

\begin{cases} 1, & x > 0 \ 0, & x \le 0 \end{cases} $$

因此,如果输出梯度为 $g_y$,则:

$$ g_x = g_y \times \frac{\partial y}{\partial x} $$

或者写成“掩码”形式:$g_x = g_y \times \mathbf{1}_{x > 0}$

数学原理:

  • ReLU 的梯度是分段函数:正数位置梯度为 1,非正数位置梯度为 0
  • 反向传播时,需要根据前向传播时的输入值 x 来判断哪些位置的梯度需要传递
  • 具体地,gx[i] = gy[i] 如果 x[i] > 0,否则 gx[i] = 0

2.1.3 计算示例

示例 2.1.3.1:ReLU 前向与反向传播

前向传播:

  • 输入:x = [[-1, 2], [-3, 4]],形状 (2, 2)
  • 输出:y = relu(x) = [[0, 2], [0, 4]],形状 (2, 2)
    • x[0,0] = -1 < 0,所以 y[0,0] = 0
    • x[0,1] = 2 > 0,所以 y[0,1] = 2
    • x[1,0] = -3 < 0,所以 y[1,0] = 0
    • x[1,1] = 4 > 0,所以 y[1,1] = 4

反向传播:

  • 输出梯度:gy = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)
  • 计算 mask(基于前向输入 x):
    • mask = [[0, 1], [0, 1]]x > 0 的位置为 1,否则为 0)
  • 输入梯度:gx = gy × mask
    gx = [[0.1, 0.2],    ×    [[0, 1],    =    [[0, 0.2],
          [0.3, 0.4]]          [0, 1]]          [0, 0.4]]
    
  • 最终梯度:gx = [[0, 0.2], [0, 0.4]],形状 (2, 2)

数学解释:

  • x[0,0] = -1x[1,0] = -3 在前向传播时输出为 0,因此这些位置的梯度不传播(梯度为 0)
  • x[0,1] = 2x[1,1] = 4 在前向传播时输出等于输入,因此梯度直接传递

2.2 Sigmoid 算子

2.2.1 前向传播原理

Sigmoid 算子的前向传播执行:$y = \sigma(x) = \frac{1}{1 + e^{-x}}$

数学描述:

  • 输入:张量 $x$

  • 对于每个元素,有 $y_i = \sigma(x_i) = \frac{1}{1 + e^{-x_i}}$

  • 数学表达式:

    $$ y = \sigma(x) = \frac{1}{1 + e^{-x}} $$

  • Sigmoid 函数将输入映射到 $(0, 1)$ 区间,常用于二分类问题的输出层

2.2.2 反向传播原理

对于 Sigmoid 运算 $y = \sigma(x)$,其导数(梯度)为:

$$ \frac{\partial y}{\partial x} = \sigma(x)\bigl(1 - \sigma(x)\bigr) = y(1 - y) $$

因此,如果输出梯度为 $g_y$,则:

$$ g_x = g_y \times \frac{\partial y}{\partial x} = g_y \times y \times (1 - y) $$

数学原理:

  • Sigmoid 函数的导数可以用其自身表示:$\sigma'(x) = \sigma(x)\bigl(1 - \sigma(x)\bigr)$
  • 反向传播时,梯度等于输出梯度乘以 $y \times (1 - y)$
  • $y$ 接近 0 或 1 时,梯度接近 0,可能导致梯度消失问题

2.2.3 计算示例

示例 2.2.3.1:Sigmoid 前向与反向传播

前向传播:

  • 输入:x = [[0, 1], [-1, 2]],形状 (2, 2)
  • 输出:y = sigmoid(x) = [[0.5, 0.731...], [0.269..., 0.881...]],形状 (2, 2)

反向传播:

  • 输出梯度:gy = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)
  • 计算 y × (1 - y)
    • y × (1 - y) = [[0.5×0.5, 0.731×0.269...], [0.269×0.731..., 0.881×0.119...]] = [[0.25, 0.197...], [0.197..., 0.105...]]
  • 输入梯度:gx = gy × y × (1 - y) = [[0.1, 0.2], [0.3, 0.4]] × [[0.25, 0.197...], [0.197..., 0.105...]] = [[0.025, 0.039...], [0.059..., 0.042...]]

2.3 Softmax 算子

2.3.1 前向传播原理

Softmax 算子的前向传播执行:$y = \operatorname{softmax}(x, \mathrm{axis})$

数学定义(向量形式):

  • 对任意实数向量 $z = (z_1, z_2, \dots, z_n)$,第 $i$ 个输出定义为:

$$ \operatorname{softmax}(z)_i = \frac{e^{z_i}}{\sum_{j=1}^n e^{z_j}} $$

  • 对于向量中的每个元素 $z_i$,有 $\operatorname{softmax}(z)i \in (0, 1)$,且 $\sum{i=1}^n \operatorname{softmax}(z)_i = 1$

张量 / axis 形式描述:

  • 输入:张量 $x$ 和指定的轴 $\mathrm{axis}$,在该轴上的每个切片视作一条向量 $\mathbf{z}$
  • 输出:$y = \operatorname{softmax}(x, \mathrm{axis})$,即对每条向量 $\mathbf{z}$ 按上述定义做 Softmax,使得:
    • $\mathrm{axis}$ 轴上有:$0 < y[i] < 1$
    • $\mathrm{axis}$ 轴上有:$\operatorname{sum}(y, \mathrm{axis}=\mathrm{axis}) = 1$

数学特性:

  • 归一化:输出在指定轴上的和为 1,适合作为概率分布
  • 非线性:通过指数函数引入非线性变换
  • 单调性:在同一向量内部保持输入元素的相对大小关系
  • 平滑性:函数处处可导,提供平滑梯度,有利于优化

计算稳定性(数值稳定 Softmax):

  • 直接按 $e^{z_i}$ 计算时,当 $z_i$ 绝对值较大时容易出现数值溢出
  • 常用的数值稳定技巧是减去一个常数 $C$(通常取该向量的最大值),不改变结果:

$$ \operatorname{softmax}(z)_i = \frac{e^{z_i - C}}{\sum_{j=1}^n e^{z_j - C}}, \quad C = \max(z_1, z_2, \dots, z_n) $$

2.3.2 反向传播原理

对于 Softmax 运算 $y = \operatorname{softmax}(x, \mathrm{axis})$,其梯度为: $$ \frac{\partial y}{\partial x} = y \times \bigl(g_y - \operatorname{sum}(g_y \times y, \mathrm{axis})\bigr) $$

因此,如果输出梯度为 $g_y$,则: $$ g_x = y \times \bigl(g_y - \operatorname{sum}(g_y \times y, \mathrm{axis})\bigr) $$

数学原理:

  • Softmax 的梯度计算需要考虑所有输出之间的相关性
  • 梯度等于输出值乘以(输出梯度减去加权平均)
  • 这确保了梯度在概率分布上的正确传播

2.3.3 计算示例

示例 2.3.3.1:Softmax 前向与反向传播

前向传播:

  • 输入:x = [[1, 2], [3, 1]],形状 (2, 2)axis = 1
  • 计算:max(x, axis=1) = [2, 3]exp(x - max) = [[exp(-1), 1], [1, exp(-2)]]
  • 输出:y = softmax(x, axis=1) = [[0.269..., 0.731...], [0.881..., 0.119...]],形状 (2, 2)

反向传播:

  • 输出梯度:gy = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)
  • 计算 sum(gy × y, axis=1) = [0.1×0.269+0.2×0.731, 0.3×0.881+0.4×0.119] = [0.173..., 0.300...]
  • 输入梯度:gx = y × (gy - sum(gy × y)) = [[0.269..., 0.731...], [0.881..., 0.119...]] × [[0.1-0.173..., 0.2-0.173...], [0.3-0.300..., 0.4-0.300...]]

2.4 SiLU 算子

2.4.1 前向传播原理

SiLU(Sigmoid Linear Unit)算子的前向传播执行:$y = \operatorname{SiLU}(x) = x \cdot \sigma(x)$

数学描述:

  • 输入:张量 $x$

  • 输出:$y_i = x_i \cdot \sigma(x_i)$

  • 数学表达式:

    $$ y = \operatorname{SiLU}(x) = x \cdot \sigma(x) $$

  • SiLU 是 Swish 激活函数的变体,结合了线性和非线性特性

2.4.2 反向传播原理

对于 SiLU 运算 $y = \operatorname{SiLU}(x) = x \cdot \sigma(x)$,其梯度为:

$$ \frac{\partial y}{\partial x} = \sigma(x) + x \cdot \sigma(x)\bigl(1 - \sigma(x)\bigr) = \sigma(x)\bigl(1 + x(1 - \sigma(x))\bigr) $$

因此,如果输出梯度为 $g_y$,则:

$$ g_x = g_y \times \sigma(x)\bigl(1 + x(1 - \sigma(x))\bigr) $$

数学原理:

  • SiLU 的导数可以用 sigmoid 函数表示
  • 反向传播时,需要计算 $\sigma(x)$ 及其导数相关项

2.4.3 计算示例

示例 2.4.3.1:SiLU 前向与反向传播

前向传播:

  • 输入:x = [[0, 1], [-1, 2]],形状 (2, 2)
  • 计算:sigmoid(x) = [[0.5, 0.731...], [0.269..., 0.881...]]
  • 输出:y = x × sigmoid(x) = [[0, 0.731...], [-0.269..., 1.762...]],形状 (2, 2)

反向传播:

  • 输出梯度:gy = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)
  • 计算导数项:sigmoid(x) × (1 + x × (1 - sigmoid(x)))
  • 输入梯度:gx = gy × sigmoid(x) × (1 + x × (1 - sigmoid(x)))

三、卷积运算算子

3.1 二维卷积算子(Conv2d)

3.1.1 前向传播原理

二维卷积算子的前向传播执行:y = conv2d(x, W, b, stride, pad)

数学描述:

  • 输入:x 形状 (N, C, H, W)(N 个样本,C 个通道,高度 H,宽度 W)
  • 卷积核:W 形状 (OC, C, KH, KW)(OC 个输出通道,C 个输入通道,卷积核高度 KH,宽度 KW)
  • 偏置:b 形状 (OC,)(可选)
  • 参数:步长 stride、填充 pad
  • 输出:y 形状 (N, OC, H_out, W_out),其中:
    • H_out = (H + 2×pad_h - KH) / stride_h + 1
    • W_out = (W + 2×pad_w - KW) / stride_w + 1

数学表示:

$$ y_{n,,\mathrm{oc},,h,,w} = \sum_{c} \sum_{k_h} \sum_{k_w} x_{n,,c,,h \cdot \mathrm{stride}_h + k_h - \mathrm{pad}_h,, w \cdot \mathrm{stride}_w + k_w - \mathrm{pad}_w} ; W_{\mathrm{oc},,c,,k_h,,k_w} + b_{\mathrm{oc}} $$

卷积操作通过滑动窗口在输入特征图上提取特征

3.1.2 反向传播原理

对于二维卷积 y = conv2d(x, W, b, stride, pad),其梯度为:

  • gx = conv2d_transpose(gy, W, stride, pad):输入梯度通过转置卷积计算
  • gW = conv2d(x, gy, stride, pad):权重梯度通过卷积计算
  • gb = sum(gy, axis=(0,2,3)):偏置梯度是输出梯度在空间维度上的求和

数学原理:

  • 卷积的反向传播涉及转置卷积(输入梯度)和卷积(权重梯度)
  • 转置卷积可以理解为卷积的逆操作
  • 偏置的梯度是所有输出位置的梯度之和

3.1.3 计算示例

示例 3.1.3.1:二维卷积前向与反向传播

前向传播:

  • 输入:x,形状 (1, 1, 4, 4)(1 个样本,1 个通道,4×4 特征图)
  • 卷积核:W,形状 (1, 1, 3, 3)(1 个输出通道,1 个输入通道,3×3 卷积核)
  • 参数:stride = (1, 1)pad = (0, 0)
  • 输出:y,形状 (1, 1, 2, 2)(输出特征图大小:(4-3+1) × (4-3+1) = 2×2

反向传播:

  • 输出梯度:gy,形状 (1, 1, 2, 2)
  • 输入梯度:gx,形状 (1, 1, 4, 4)(通过转置卷积计算)
  • 权重梯度:gW,形状 (1, 1, 3, 3)(通过卷积计算)
  • 偏置梯度:gb,形状 (1,)(输出梯度在空间维度上的求和)

四、池化运算算子

4.1 最大池化算子(MaxPool2d)

4.1.1 前向传播原理

最大池化算子的前向传播执行:y = max_pool2d(x, kernel_size, stride, pad)

数学描述:

  • 输入:4D 张量 x,形状为 (N, C, H, W)
  • 参数:卷积核大小 kernel_size、步长 stride、填充 pad
  • 输出:y,形状为 (N, C, H_out, W_out),其中:
    • H_out = (H + 2×pad_h - kernel_h) / stride_h + 1
    • W_out = (W + 2×pad_w - kernel_w) / stride_w + 1
  • 操作:在每个 kernel_size × kernel_size 的窗口内取最大值
  • 保存索引:前向传播时保存最大值的位置索引,用于反向传播

数学表示:

$$ y_{i, c, h, w} = \max\bigl( x_{i, c,, h \cdot \mathrm{stride}_h : h \cdot \mathrm{stride}_h + \mathrm{kernel}_h,, w \cdot \mathrm{stride}_w : w \cdot \mathrm{stride}_w + \mathrm{kernel}_w} \bigr) $$

4.1.2 反向传播原理

对于最大池化运算 y = max_pool2d(x, kernel_size, stride, pad),其梯度为:

  • 只有最大值位置传递梯度:gx[argmax位置] = gy,其他位置梯度为 0

数学原理:

  • 前向传播中,输出是输入窗口内所有元素的最大值:
    • out = max(x₁, x₂, ..., xₙ)
  • 对于窗口内的每个元素 xᵢ,其对输出的偏导数是:
    • ∂out/∂xᵢ = 1 (如果 xᵢ 是最大值)
    • ∂out/∂xᵢ = 0 (如果 xᵢ 不是最大值)
  • 根据链式法则,输入梯度为:
    • dx = dout * (1 或 0)
  • 因此,上游梯度 dout 只传递给最大值位置,其他位置的梯度为 0
  • 使用前向传播时保存的索引来确定梯度传递位置

4.1.3 计算示例

示例 4.1.3.1:最大池化前向与反向传播

前向传播:

  • 输入:x,形状 (1, 1, 4, 4)
    x = [[[[1, 2, 3, 4],
           [5, 6, 7, 8],
           [9, 10, 11, 12],
           [13, 14, 15, 16]]]]
    
  • 参数:kernel_size = (2, 2)stride = (2, 2)pad = (0, 0)
  • 输出:y,形状 (1, 1, 2, 2)
    y = [[[[6, 8],
            [14, 16]]]]
    
    • y[0,0] = max(x[0:2, 0:2]) = max(1,2,5,6) = 6
    • y[0,1] = max(x[0:2, 2:4]) = max(3,4,7,8) = 8
    • y[1,0] = max(x[2:4, 0:2]) = max(9,10,13,14) = 14
    • y[1,1] = max(x[2:4, 2:4]) = max(11,12,15,16) = 16

反向传播:

  • 输出梯度:gy = [[[[0.1, 0.2], [0.3, 0.4]]]],形状 (1, 1, 2, 2)
  • 输入梯度:gx,形状 (1, 1, 4, 4)
    gx = [[[[0, 0, 0, 0],
            [0, 0.1, 0, 0.2],
            [0, 0, 0, 0],
            [0, 0.3, 0, 0.4]]]]
    
    • 只有最大值位置(6, 8, 14, 16)传递梯度

4.2 平均池化算子(AvgPool2d)

4.2.1 前向传播原理

平均池化算子的前向传播执行:y = avg_pool2d(x, kernel_size, stride, pad)

数学描述:

  • 输入:4D 张量 x,形状为 (N, C, H, W)
  • 参数:卷积核大小 kernel_size、步长 stride、填充 pad
  • 输出:y,形状为 (N, C, H_out, W_out)
  • 操作:在每个 kernel_size × kernel_size 的窗口内计算平均值

数学表示:

$$ y_{i, c, h, w} = \operatorname{mean}\bigl( x_{i, c,, h \cdot \mathrm{stride}_h : h \cdot \mathrm{stride}_h + \mathrm{kernel}_h,, w \cdot \mathrm{stride}_w : w \cdot \mathrm{stride}_w + \mathrm{kernel}_w} \bigr) $$

4.2.2 反向传播原理

对于平均池化运算 y = avg_pool2d(x, kernel_size, stride, pad),其梯度为:

  • 梯度平均分配:gx[窗口内所有位置] = gy / (kernel_h × kernel_w)

数学原理:

  • 前向传播中,输出是输入窗口内所有元素的平均值:
    • out = (x₁ + x₂ + ... + xₙ) / n
    • 其中 n = kernel_h × kernel_w 是窗口大小
  • 对于窗口内的每个元素 xᵢ,其对输出的偏导数是:
    • ∂out/∂xᵢ = 1/n
  • 根据链式法则,输入梯度为:
    • dx = dout * (1/n)
  • 因此,上游梯度 dout 被均匀地分配到输入窗口的每个位置,每个位置获得 dout/n 的梯度贡献
  • 这确保了梯度在窗口内的均匀分布

4.2.3 计算示例

示例 4.2.3.1:平均池化前向与反向传播

前向传播:

  • 输入:x,形状 (1, 1, 4, 4)
    x = [[[[1, 2, 3, 4],
           [5, 6, 7, 8],
           [9, 10, 11, 12],
           [13, 14, 15, 16]]]]
    
  • 参数:kernel_size = (2, 2)stride = (2, 2)pad = (0, 0)
  • 输出:y,形状 (1, 1, 2, 2)
    y = [[[[3.5, 5.5],
            [11.5, 13.5]]]]
    
    • y[0,0] = mean(x[0:2, 0:2]) = (1+2+5+6)/4 = 3.5

反向传播:

  • 输出梯度:gy = [[[[0.1, 0.2], [0.3, 0.4]]]],形状 (1, 1, 2, 2)
  • 输入梯度:gx,形状 (1, 1, 4, 4)
    gx = [[[[0.025, 0.025, 0.05, 0.05],
            [0.025, 0.025, 0.05, 0.05],
            [0.075, 0.075, 0.1, 0.1],
            [0.075, 0.075, 0.1, 0.1]]]]
    
    • 每个窗口内的梯度平均分配:gy[0,0] = 0.1 分配到 4 个位置,每个位置 0.1/4 = 0.025

4.3 自适应平均池化算子(AdaptiveAvgPool2d)

4.3.1 前向传播原理

自适应平均池化算子的前向传播执行:y = adaptive_avg_pool2d(x, output_size)

数学描述:

  • 输入:4D 张量 x,形状为 (N, C, H, W)
  • 参数:目标输出大小 output_size = (H_out, W_out)
  • 输出:y,形状为 (N, C, H_out, W_out)
  • 特点:无论输入大小如何,输出总是固定大小
  • 计算:自适应地计算每个输出位置对应的输入窗口大小,然后进行平均池化

数学表示:

  • 窗口大小:

    $$ \mathrm{kernel}h = \left\lceil \frac{H}{H{\text{out}}} \right\rceil, \quad \mathrm{kernel}w = \left\lceil \frac{W}{W{\text{out}}} \right\rceil $$

  • 对每个输出位置进行平均池化:

    $$ y_{i, c, h, w} = \operatorname{mean}\bigl( x_{i, c,, h \cdot \mathrm{kernel}_h : (h+1) \cdot \mathrm{kernel}_h,, w \cdot \mathrm{kernel}_w : (w+1) \cdot \mathrm{kernel}_w} \bigr) $$

4.3.2 反向传播原理

对于自适应平均池化 y = adaptive_avg_pool2d(x, output_size),其梯度为:

  • 梯度平均分配:gx[窗口内所有位置] = gy / (kernel_h × kernel_w)

数学原理:

  • 自适应平均池化的反向传播原理与普通平均池化相同,区别在于窗口大小是自适应的
  • 前向传播中,输出是输入窗口内所有元素的平均值:
    • out = (x₁ + x₂ + ... + xₙ) / n
    • 其中窗口大小 n = kernel_h × kernel_w 根据输入和输出大小自适应计算
  • 对于窗口内的每个元素 xᵢ,其对输出的偏导数是:
    • ∂out/∂xᵢ = 1/n
  • 根据链式法则,输入梯度为:
    • dx = dout * (1/n)
  • 因此,上游梯度 dout 被均匀地分配到输入窗口的每个位置,每个位置获得 dout/n 的梯度贡献

4.3.3 计算示例

示例 4.3.3.1:自适应平均池化前向与反向传播

前向传播:

  • 输入:x,形状 (1, 1, 5, 5)(高度 5,宽度 5)
  • 输出大小:output_size = (2, 2)
  • 窗口大小:kernel_h = ceil(5/2) = 3kernel_w = ceil(5/2) = 3
  • 输出:y,形状 (1, 1, 2, 2)
    • y[0,0] = mean(x[0:3, 0:3])
    • y[0,1] = mean(x[0:3, 2:5])
    • y[1,0] = mean(x[2:5, 0:3])
    • y[1,1] = mean(x[2:5, 2:5])

反向传播:

  • 输出梯度:gy,形状 (1, 1, 2, 2)
  • 输入梯度:gx,形状 (1, 1, 5, 5)
    • 每个窗口内的梯度平均分配:gy[0,0] 分配到 x[0:3, 0:3] 的 9 个位置,每个位置 gy[0,0] / 9

五、形状变换算子

5.1 拼接算子(Cat)

5.1.1 前向传播原理

拼接算子将多个张量沿着指定维度拼接在一起。

数学描述:

  • 输入:n 个张量 [x0, x1, ..., xn],每个张量的形状为 (d0, d1, ..., dk, ..., dm)
  • 拼接维度:dim = k,指定在第 k 维上进行拼接
  • 形状要求:除了第 k 维外,所有输入张量的其他维度必须相同
  • 输出形状:(d0, d1, ..., sum(dk_i), ..., dm),其中 sum(dk_i) 是所有输入在第 k 维大小的总和

数学表示:

$$ y = \operatorname{cat}([x_0, x_1, \dots, x_n], \mathrm{dim}=k) $$

输出 $y$ 在第 $k$ 维上包含了所有输入在该维度的数据,按顺序排列

5.1.2 反向传播原理

拼接的反向传播是分割:将输出梯度按照原始输入的形状分割回各个输入。

数学原理:

  • 前向操作:y = cat([x0, x1, ..., xn], dim=k)
  • 反向操作:[gx0, gx1, ..., gxn] = split(gy, [shape0, shape1, ..., shapen], dim=k)
  • 核心思想:输出梯度 gy 在第 k 维上被分割成 n 段,每段对应一个输入的梯度
  • 分割位置:第 i 个输入的梯度 gxi 对应 gy 在第 k 维上的区间 [sum(dk_0...dk_{i-1}), sum(dk_0...dk_i))

5.1.3 计算示例

示例 5.1.3.1:沿第 0 维拼接

前向传播:

  • 输入:
    • x0 = [[1, 2], [3, 4]],形状 (2, 2)
    • x1 = [[5, 6], [7, 8]],形状 (2, 2)
    • x2 = [[9, 10], [11, 12]],形状 (2, 2)
  • 拼接维度:dim = 0
  • 输出:y = cat([x0, x1, x2], dim=0)
    y = [[1,  2 ],
         [3,  4 ],
         [5,  6 ],
         [7,  8 ],
         [9,  10],
         [11, 12]]
    
    形状 (6, 2),其中第 0 维 6 = 2 + 2 + 2

反向传播:

  • 输出梯度:gy,形状 (6, 2)
  • 分割形状:[(2, 2), (2, 2), (2, 2)]
  • 输入梯度:
    • gx0 = gy[0:2, :],形状 (2, 2)
    • gx1 = gy[2:4, :],形状 (2, 2)
    • gx2 = gy[4:6, :],形状 (2, 2)
示例 5.1.3.2:沿第 1 维拼接

前向传播:

  • 输入:
    • x0 = [[1, 2], [3, 4]],形状 (2, 2)
    • x1 = [[5, 6], [7, 8]],形状 (2, 2)
  • 拼接维度:dim = 1
  • 输出:y = cat([x0, x1], dim=1)
    y = [[1, 2, 5, 6],
         [3, 4, 7, 8]]
    
    形状 (2, 4),其中第 1 维 4 = 2 + 2

反向传播:

  • 输出梯度:gy = [[0.1, 0.2, 0.3, 0.4], [0.5, 0.6, 0.7, 0.8]],形状 (2, 4)
  • 分割形状:[(2, 2), (2, 2)]
  • 输入梯度:
    • gx0 = gy[:, 0:2] = [[0.1, 0.2], [0.5, 0.6]],形状 (2, 2)
    • gx1 = gy[:, 2:4] = [[0.3, 0.4], [0.7, 0.8]],形状 (2, 2)

5.2 分割算子(Split)

5.2.1 前向传播原理

分割算子将一个张量沿着指定维度分割成多个张量。

数学描述:

  • 输入:单个张量 x,形状为 (d0, d1, ..., dk, ..., dm)
  • 分割维度:dim = k,指定在第 k 维上进行分割
  • 分割方式:
    • 固定大小分割:按固定大小 s 分割,第 k 维被分成 ⌈dk/s⌉ 段,最后一段可能小于 s
    • 大小列表分割:按指定大小列表 [s0, s1, ..., sn] 分割,要求 sum(si) = dk
  • 输出:n 个张量 [y0, y1, ..., yn],每个 yi 的形状为 (d0, d1, ..., si, ..., dm)

数学表示:

$$ [y_0, y_1, \dots, y_n] = \operatorname{split}(x, \mathrm{sizes}, \mathrm{dim}=k) $$

每个输出 $y_i$ 对应输入 $x$ 在第 $k$ 维上的一个连续区间

5.2.2 反向传播原理

分割的反向传播是拼接:将所有分割后的梯度拼接回原始形状。

数学原理:

  • 前向操作:[y0, y1, ..., yn] = split(x, sizes, dim=k)
  • 反向操作:gx = cat([gy0, gy1, ..., gyn], dim=k)
  • 核心思想:分割是拼接的逆操作,因此反向传播就是将分割后的梯度重新拼接
  • 梯度连续性:由于前向传播时数据在分割维度上是连续切分的,反向传播时梯度也在同一维度上连续拼接

5.2.3 计算示例

示例 5.2.3.1:固定大小分割

前向传播:

  • 输入:x,形状 (10, 3)
  • 分割参数:split_size = 3dim = 0
  • 计算:
    • dim_size = 10
    • num_splits = (10 + 3 - 1) / 3 = 4
    • actual_split_sizes = [3, 3, 3, 1]
  • 输出:
    • y0 = x[0:3, :],形状 (3, 3)
    • y1 = x[3:6, :],形状 (3, 3)
    • y2 = x[6:9, :],形状 (3, 3)
    • y3 = x[9:10, :],形状 (1, 3)

反向传播:

  • 输出梯度:
    • gy0,形状 (3, 3)
    • gy1,形状 (3, 3)
    • gy2,形状 (3, 3)
    • gy3,形状 (1, 3)
  • 输入梯度:gx = cat([gy0, gy1, gy2, gy3], dim=0),形状 (10, 3)
示例 5.2.3.2:大小列表分割

前向传播:

  • 输入:x = [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]],形状 (2, 5)
  • 分割参数:split_sizes = [2, 3]dim = 1
  • 输出:
    • y0 = x[:, 0:2] = [[1, 2], [6, 7]],形状 (2, 2)
    • y1 = x[:, 2:5] = [[3, 4, 5], [8, 9, 10]],形状 (2, 3)

反向传播:

  • 输出梯度:
    • gy0 = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)
    • gy1 = [[0.5, 0.6, 0.7], [0.8, 0.9, 1.0]],形状 (2, 3)
  • 输入梯度:gx = cat([gy0, gy1], dim=1)
    gx = [[0.1, 0.2, 0.5, 0.6, 0.7],
          [0.3, 0.4, 0.8, 0.9, 1.0]]
    
    形状 (2, 5)

5.3 重塑算子(Reshape)

5.3.1 前向传播原理

重塑算子的前向传播执行:$y = \operatorname{reshape}(x, \mathrm{shape})$

数学描述:

  • 输入:张量 $x$ 和目标形状 $\mathrm{shape}$
  • 输出:将 $x$ 重塑为目标形状 $\mathrm{shape}$
  • 要求:x 的元素总数必须等于 shape 的元素总数
  • 重塑操作不改变数据,只改变张量的维度排列

数学表示:

$$ y = \operatorname{reshape}(x, \mathrm{shape}) $$

输出 $y$ 的形状为 $\mathrm{shape}$,但数据与 $x$ 相同(按行优先顺序重新排列)

5.3.2 反向传播原理

对于重塑运算 y = reshape(x, shape),根据链式法则:

  • 反向传播也是重塑:gx = reshape(gy, x.shape)

数学原理:

  • 重塑是形状变换,不涉及数值计算
  • 反向传播时,梯度被重塑回原始形状
  • 这是恒等变换在形状层面的体现

5.3.3 计算示例

示例 5.3.3.1:重塑前向与反向传播

前向传播:

  • 输入:x = [[1, 2], [3, 4]],形状 (2, 2)
  • 目标形状:(4,)
  • 输出:y = reshape(x, (4,)) = [1, 2, 3, 4],形状 (4,)

反向传播:

  • 输出梯度:gy = [0.1, 0.2, 0.3, 0.4],形状 (4,)
  • 输入梯度:gx = reshape(gy, (2, 2)) = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)

5.4 转置算子(Transpose)

5.4.1 前向传播原理

转置算子的前向传播执行:$y = \operatorname{transpose}(x)$

数学描述:

  • 输入:张量 $x$
  • 输出:将 $x$ 转置,交换最后两个维度
  • 对于 2D 矩阵:$y_{i j} = x_{j i}$
  • 对于更高维张量:只交换最后两个维度

数学表示:

$$ y = \operatorname{transpose}(x) $$

输出 $y$ 的形状是 $x$ 的形状在最后两个维度上交换后的结果

5.4.2 反向传播原理

对于转置运算 y = transpose(x),根据链式法则:

  • 反向传播也是转置:gx = transpose(gy)

数学原理:

  • 转置是形状变换,不涉及数值计算
  • 反向传播时,梯度也被转置
  • 转置的转置等于原矩阵

5.4.3 计算示例

示例 5.4.3.1:转置前向与反向传播

前向传播:

  • 输入:x = [[1, 2], [3, 4]],形状 (2, 2)
  • 输出:y = transpose(x) = [[1, 3], [2, 4]],形状 (2, 2)

反向传播:

  • 输出梯度:gy = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)
  • 输入梯度:gx = transpose(gy) = [[0.1, 0.3], [0.2, 0.4]],形状 (2, 2)

5.5 展平算子(Flatten)

5.5.1 前向传播原理

展平算子的前向传播执行:$y = \operatorname{flatten}(x, \mathrm{start_dim}, \mathrm{end_dim})$

数学描述:

  • 输入:张量 $x$ 和维度范围 $[\mathrm{start_dim}, \mathrm{end_dim}]$
  • 输出:将 x 在指定维度范围内展平
  • 展平操作将多个连续维度合并为一个维度
  • 例如:(2, 3, 4)[1, 2] 展平后变为 (2, 12)

数学表示:

$$ y = \operatorname{flatten}(x, \mathrm{start_dim}, \mathrm{end_dim}) $$

输出 $y$ 的形状是 $x$ 的形状在 $[\mathrm{start_dim}, \mathrm{end_dim}]$ 范围内展平

5.5.2 反向传播原理

对于展平运算 y = flatten(x, start_dim, end_dim),根据链式法则:

  • 反向传播是重塑:gx = reshape(gy, x.shape)

数学原理:

  • 展平是重塑的特例,将多个维度合并为一个
  • 反向传播时,梯度被重塑回原始形状
  • 这确保了梯度形状与输入形状匹配

5.5.3 计算示例

示例 5.5.3.1:展平前向与反向传播

前向传播:

  • 输入:x,形状 (2, 3, 4)
  • 展平范围:start_dim = 1end_dim = 2
  • 输出:y = flatten(x, 1, 2),形状 (2, 12)

反向传播:

  • 输出梯度:gy,形状 (2, 12)
  • 输入梯度:gx = reshape(gy, (2, 3, 4)),形状 (2, 3, 4)

六、神经网络层算子

6.1 Dropout 算子

6.1.1 前向传播原理

Dropout 算子的前向传播执行:y = dropout(x, p, training)

数学描述:

  • 输入:张量 x、丢弃概率 p 和训练模式标志 training
  • 训练模式:以概率 p 将元素置为 0,其余元素除以 (1 - p) 进行缩放
  • 推理模式:直接返回输入 x,不做任何变换
  • 目的:防止过拟合,通过随机丢弃部分神经元来增强模型泛化能力

数学表示:

  • 训练模式:

    $$ y_i = \begin{cases} \dfrac{x_i}{1 - p}, & \text{如果 } \mathrm{mask}_i = 1 \ 0, & \text{如果 } \mathrm{mask}_i = 0 \end{cases} $$

    其中 $\mathrm{mask}_i$ 是随机生成的掩码。

  • 推理模式:

    $$ y = x $$

6.1.2 反向传播原理

对于 Dropout 运算 y = dropout(x, p, training),其梯度为:

  • 训练模式:gx = dropout_backward(gy, mask),根据前向传播时的掩码传递梯度
  • 推理模式:gx = gy

数学原理:

  • 训练模式下,只有未被丢弃的位置才传递梯度
  • 梯度需要与前向传播时的缩放因子保持一致
  • 推理模式下,梯度直接传递

6.1.3 计算示例

示例 6.1.3.1:Dropout 前向与反向传播(训练模式)

前向传播:

  • 输入:x = [[1, 2], [3, 4]],形状 (2, 2)
  • 丢弃概率:p = 0.5
  • 缩放因子:scale = 1/(1-p) = 1/(1-0.5) = 2
  • 假设掩码:mask = [[2, 0], [2, 2]](随机生成,保存缩放因子 scale0
    • 位置 (0,0)(1,0)(1,1) 未被丢弃,mask 保存 scale = 2
    • 位置 (0,1) 被丢弃,mask 保存 0
  • 输出:y[i] = x[i] * mask[i]
    • y = [[1*2, 2*0], [3*2, 4*2]] = [[2, 0], [6, 8]],形状 (2, 2)

反向传播:

  • 输出梯度:gy = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)
  • 输入梯度:gx[i] = gy[i] * mask[i]
    • gx = [[0.1*2, 0.2*0], [0.3*2, 0.4*2]] = [[0.2, 0], [0.6, 0.8]],形状 (2, 2)

关键说明:

  • mask 既用于决定哪些位置传递梯度(mask = 0 的位置梯度为 0),也用于保持缩放因子(mask = scale 的位置梯度乘以 scale
  • 前向传播时:y[i] = x[i] * mask[i],其中 mask[i] = scale(未被丢弃)或 mask[i] = 0(被丢弃)
  • 反向传播时:gx[i] = gy[i] * mask[i],梯度传递与缩放因子保持一致

6.2 上采样算子(Upsample)

当前实现约束: 仅支持 4D 输入,形状为 (N, C, H, W);不支持 3D/5D。mode 接口取值 "nearest""bilinear",但当前实现未使用,实际均为最近邻插值。

6.2.1 前向传播原理

上采样算子将输入张量的空间维度(高度和宽度)进行放大。

数学描述:

  • 输入:4D 张量 x,形状为 (N, C, H, W),其中 N 是批次大小,C 是通道数,H 是高度,W 是宽度
  • 上采样方式:可以通过指定目标大小 (H_out, W_out) 或缩放因子 (scale_h, scale_w) 来放大
  • 输出形状:(N, C, H_out, W_out),其中 H_out = H × scale_hW_out = W × scale_w
  • 插值方法:通常使用最近邻插值或双线性插值

数学表示:

$$ y = \operatorname{upsample}\bigl(x, \mathrm{size}=(H_{\text{out}}, W_{\text{out}})\bigr) $$

$$ y = \operatorname{upsample}\bigl(x, \mathrm{scale_factor}=(\mathrm{scale}_h, \mathrm{scale}_w)\bigr) $$

输出 $y$ 的空间维度被放大,通道数和批次大小保持不变

6.2.2 反向传播原理

上采样的反向传播是下采样:将输出梯度按照缩放因子压缩回原始形状。

数学原理:

  • 前向操作:y = upsample(x, scale_factor=(scale_h, scale_w))
  • 反向操作:gx = downsample(gy, scale_factor=(scale_h, scale_w))
  • 核心思想:上采样时,一个输入像素对应多个输出像素;反向传播时,需要将多个输出梯度累加回对应的输入位置
  • 梯度累加:对于输入位置 (i, j),其梯度等于所有对应输出位置的梯度之和

数学表示:

  • 如果 $y[h_{\text{out}}, w_{\text{out}}]$ 对应 $x[h, w]$,其中 $$ h = \frac{h_{\text{out}}}{\mathrm{scale}h}, \quad w = \frac{w{\text{out}}}{\mathrm{scale}_w}, $$ 则

    $$ g_x[h, w] = \sum_{\substack{h_{\text{out}}, w_{\text{out}} \ h_{\text{out}} / \mathrm{scale}h = h \ w{\text{out}} / \mathrm{scale}w = w}} g_y[h{\text{out}}, w_{\text{out}}] $$

6.2.3 计算示例

示例 6.2.3.1:上采样前向与反向传播

前向传播:

  • 输入:x,形状 (1, 1, 2, 2)(批次大小 1,通道数 1,高度 2,宽度 2)
    x = [[[[1, 2],
           [3, 4]]]]
    
  • 缩放因子:scale_h = 2scale_w = 2
  • 输出:y,形状 (1, 1, 4, 4)
    y = [[[[1, 1, 2, 2],
           [1, 1, 2, 2],
           [3, 3, 4, 4],
           [3, 3, 4, 4]]]]
    
    (使用最近邻插值,每个输入像素复制到对应的 2×2 区域)

反向传播:

  • 输出梯度:gy,形状 (1, 1, 4, 4)
    gy = [[[[0.1, 0.2, 0.3, 0.4],
            [0.5, 0.6, 0.7, 0.8],
            [0.9, 1.0, 1.1, 1.2],
            [1.3, 1.4, 1.5, 1.6]]]]
    
  • 输入梯度:gx,形状 (1, 1, 2, 2)
    • gx[0, 0] 对应 gy[0:2, 0:2] 的所有元素,所以 gx[0, 0] = 0.1 + 0.2 + 0.5 + 0.6 = 1.4
    • gx[0, 1] 对应 gy[0:2, 2:4] 的所有元素,所以 gx[0, 1] = 0.3 + 0.4 + 0.7 + 0.8 = 2.2
    • gx[1, 0] 对应 gy[2:4, 0:2] 的所有元素,所以 gx[1, 0] = 0.9 + 1.0 + 1.3 + 1.4 = 4.6
    • gx[1, 1] 对应 gy[2:4, 2:4] 的所有元素,所以 gx[1, 1] = 1.1 + 1.2 + 1.5 + 1.6 = 5.4
  • 最终梯度:
    gx = [[[[1.4, 2.2],
            [4.6, 5.4]]]]
    

数学解释:

  • 上采样时,x[0, 0] = 1 被复制到 y[0:2, 0:2] 的 4 个位置
  • 反向传播时,这 4 个位置的梯度需要累加回 gx[0, 0]
  • 类似地,其他输入位置的梯度也是对应输出区域梯度的累加

6.3 恒等算子(Identity)

6.3.1 前向传播原理

恒等算子的前向传播执行:$y = \operatorname{identity}(x) = x$

数学描述:

  • 输入:张量 $x$

  • 输出:直接返回输入 $x$,不做任何变换

  • 数学表达式:

    $$ y = \operatorname{identity}(x) = x $$

  • 恒等算子主要用于控制计算图结构,不改变数据

6.3.2 反向传播原理

对于恒等运算 $y = \operatorname{identity}(x)$,根据链式法则:

$$ \frac{\partial y}{\partial x} = 1 $$

因此,如果输出梯度为 $g_y$,则:

$$ g_x = g_y $$

数学原理:

  • 恒等函数的导数是 $1$
  • 反向传播时,梯度直接传递,不做任何变换
  • 这是最简单的算子之一

6.3.3 计算示例

示例 6.3.3.1:恒等运算前向与反向传播

前向传播:

  • 输入:x = [[1, 2], [3, 4]],形状 (2, 2)
  • 输出:y = identity(x) = [[1, 2], [3, 4]],形状 (2, 2)

反向传播:

  • 输出梯度:gy = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)
  • 输入梯度:gx = gy = [[0.1, 0.2], [0.3, 0.4]],形状 (2, 2)

七、归一化算子

7.1 批归一化算子(BatchNorm)

接口与 PyTorch 对齐说明:

  • Functional 接口:OriginDL 使用 batch_norm(...),与 PyTorch 的 torch.nn.functional.batch_norm 命名一致;通过参数区分输入维度(见下)。
  • NN 层接口:与 PyTorch 一致,提供 BatchNorm1dBatchNorm2dBatchNorm3d 三种层;当前实现仅支持 BatchNorm1dBatchNorm2dBatchNorm3d 尚未实现(见「当前支持范围」)。

当前支持范围:

  • BatchNorm1d:已实现。输入形状 (N, C),对应 functional 的 num_dims=2
  • BatchNorm2d:已实现。输入形状 (N, C, H, W),对应 functional 的 num_dims=4
  • BatchNorm3d未实现。输入形状应为 (N, C, D, H, W),对应 functional 的 num_dims=5;当前 CPU/CUDA 后端仅处理 num_dims=2num_dims=4,传入 num_dims=5 会报错。

7.1.1 前向传播原理

批归一化算子的前向传播执行:y = batch_norm(x, gamma, beta, running_mean, running_var, training)

数学描述:

  • 输入:x 形状 (N, C, H, W)(N, C)(N 个样本,C 个通道);当前实现仅支持上述两种形状(即 BatchNorm2d 与 BatchNorm1d)
  • 参数:缩放参数 gamma、平移参数 beta、运行均值 running_mean、运行方差 running_var
  • 训练模式:
    1. 计算当前 batch 的均值和方差:mean = mean(x, axis=0)var = var(x, axis=0)
    2. 归一化:x_norm = (x - mean) / sqrt(var + ε)。其中 ε 为小正数(如 1e-5),加在方差上可避免分母为零或过小,保证数值稳定。
    3. 缩放和平移:y = gamma × x_norm + beta
    4. 更新运行统计:running_mean = momentum × running_mean + (1-momentum) × mean
  • 推理模式:使用 running_meanrunning_var 进行归一化

数学表示:

  • 训练模式:

    $$ y = \gamma \cdot \frac{x - \mathrm{mean}}{\sqrt{\mathrm{var} + \varepsilon}} + \beta $$

  • 推理模式:

    $$ y = \gamma \cdot \frac{x - \mathrm{running_mean}}{\sqrt{\mathrm{running_var} + \varepsilon}} + \beta $$

7.1.2 反向传播原理

对于批归一化 y = batch_norm(x, gamma, beta, running_mean, running_var, training),其梯度为:

  • gx:输入梯度,通过归一化操作的链式法则计算
  • dgamma = sum(gy × x_norm, axis=0):缩放参数梯度
  • dbeta = sum(gy, axis=0):平移参数梯度

数学原理:

  • 批归一化的梯度计算需要考虑归一化操作对均值和方差的依赖
  • 输入梯度需要考虑 batch 内所有样本的统计信息
  • 缩放和平移参数的梯度直接计算

7.1.3 计算示例

示例 7.1.3.1:批归一化前向与反向传播(训练模式)

前向传播:

  • 输入:x,形状 (2, 3)(2 个样本,3 个特征)
    x = [[1, 2, 3],
         [4, 5, 6]]
    
  • 计算均值:mean = [2.5, 3.5, 4.5](沿第 0 维求平均)
  • 计算方差:var = [2.25, 2.25, 2.25]
  • 归一化:x_norm = (x - mean) / sqrt(var + ε)
  • 缩放和平移:y = gamma × x_norm + beta

反向传播:

  • 输出梯度:gy,形状 (2, 3)
  • 输入梯度:gx,形状 (2, 3)(考虑归一化操作的梯度)
  • 缩放参数梯度:dgamma = sum(gy × x_norm, axis=0),形状 (3,)
  • 平移参数梯度:dbeta = sum(gy, axis=0),形状 (3,)

八、损失函数算子

8.1 Softmax 交叉熵损失算子(SoftmaxCrossEntropy)

8.1.1 前向传播原理

Softmax 交叉熵损失算子的前向传播执行:loss = softmax_cross_entropy(x, target)

直观理解(为什么需要这个损失):

  • 场景:分类问题。例如 N 张图片,每张图属于 C 个类别之一(如猫/狗/鸟)。网络对每张图输出 C 个“得分”x(logits),我们想知道:预测与真实标签差多少。
  • 两步合一:先把得分变成“概率”p(softmax),再用量化“预测概率与真实标签的差距”的交叉熵作为损失。概率越集中在正确类别上,损失越小;越分散或给错误类别高概率,损失越大。

数学描述:

  • 输入:x 形状 (N, C)(N 个样本,C 个类别),target 形状 (N,)(每个样本的真实类别编号,取值 0 到 C-1)

  • 计算步骤:

    1. 计算 softmax:p = softmax(x, axis=-1),形状 (N, C)
    2. 提取目标类别的概率:p_selected = p[i, target[i]],形状 (N,)
    3. 计算交叉熵:loss = -mean(log(p_selected + ε))
  • 数学表达式:

    $$ \mathrm{loss} = -\frac{1}{N} \sum_{i=1}^N \log\bigl(p_{i,,\mathrm{target}_i} + \varepsilon\bigr) $$

各步含义(便于普通人理解):

  1. Softmax:从“得分”到“概率”

    • 输入 x[i, c] 是第 i 个样本在第 c 类上的得分(logit),可正可负、可大于 1。
    • Softmax 公式:p[i, c] = exp(x[i, c]) / Σ(k) exp(x[i, k])。对每个样本 i,把 C 个得分用指数放大后归一化,得到 C 个非负且和为 1 的数,即“该类别的概率”。
    • 直观:得分越高,对应概率越大;得分相对低时,概率被压小。
  2. 提取目标概率 p_selected

    • 对样本 i,真实类别是 target[i],我们只关心“模型给正确类别多少概率”,即 p[i, target[i]]
    • p_selected[i] = p[i, target[i]],形状 (N,),表示每个样本上正确类别的预测概率。
  3. 交叉熵:用 -log(概率) 衡量“错多少”

    • 单样本:若正确类别概率为 p_selected[i],定义该样本的损失为 -log(p_selected[i] + ε)
    • 直观:概率越接近 1,log(p) 越接近 0,-log(p) 越小(预测越对);概率越小,-log(p) 越大(预测越错)。ε 是为了数值稳定,避免 log(0)。
    • 多样本:对所有样本的 -log(p_selected + ε) 取平均,得到 loss

数学表示(小结):

$$ p = \operatorname{softmax}(x) $$

将 logits 转换为概率分布(每行和为 1)。

$$ \mathrm{loss} = -\operatorname{mean}\bigl(\log(p_{\mathrm{target}})\bigr) $$

交叉熵损失衡量预测分布与“真实类别”的一致性,越小表示预测越准。

8.1.2 反向传播原理

对于 Softmax 交叉熵损失 loss = softmax_cross_entropy(x, target),其梯度为:

  • gx = (softmax(x) - one_hot(target)) / N
  • gtarget = 0(标签不需要梯度)

数学原理:

  • Softmax 交叉熵的梯度等于预测概率减去真实标签的 one-hot 编码
  • 梯度已经包含了 softmax 的梯度,因此计算非常高效
  • 这是 softmax 和交叉熵组合的数学性质

8.1.3 计算示例

示例 8.1.3.1:Softmax 交叉熵前向与反向传播

前向传播:

  • 输入:x = [[1, 2, 3], [2, 1, 3]],形状 (2, 3)(2 个样本,3 个类别)
  • 标签:target = [2, 0],形状 (2,)(第一个样本类别 2,第二个样本类别 0)
  • 计算 softmax:
    • p = softmax(x) = [[0.090..., 0.245..., 0.665...], [0.245..., 0.090..., 0.665...]]
  • 提取目标概率:
    • p_selected = [0.665..., 0.245...]
  • 计算损失:
    • loss = -mean(log(p_selected)) = -mean([-0.407..., -1.407...]) = 0.907...

反向传播:

  • 输出梯度:gy = 1.0(标量)
  • 计算 one-hot 编码:
    • one_hot(target) = [[0, 0, 1], [1, 0, 0]]
  • 输入梯度:
    • gx = (p - one_hot(target)) / N = ([[0.090..., 0.245..., 0.665...], [0.245..., 0.090..., 0.665...]] - [[0, 0, 1], [1, 0, 0]]) / 2
    • gx = [[0.045..., 0.123..., -0.167...], [-0.377..., 0.045..., 0.333...]]

九、总结

9.1 算子分类总结

分类 算子 特点
数学运算 Add, Sub, Mul, Div, MatMul, Pow, Exp, Log, Neg, Square, Sum, BroadcastTo, SumTo, Compare 基础数学运算,支持广播
激活函数 ReLU, Sigmoid, Softmax, SiLU 非线性变换,引入非线性特性
卷积运算 Conv2d 特征提取,参数共享
池化运算 MaxPool2d, AvgPool2d, AdaptiveAvgPool2d 降维,减少计算量
形状变换 Cat, Split, Reshape, Transpose, Flatten 改变张量形状和维度
神经网络层 Dropout, Upsample, Identity 网络层操作
归一化 BatchNorm(BatchNorm1d/2d 已实现,BatchNorm3d 未实现) 加速训练,稳定梯度
损失函数 SoftmaxCrossEntropy 计算损失值

9.2 数学原理总结

前面的每个算子小节已经分别给出了详细的数学公式和梯度推导,这里不再重复展开,只做整体性的归纳:

  • 前向:所有算子都可以看成是对张量元素或张量块应用某种标量运算、线性变换、卷积/池化、归一化或形状重排,并严格遵守形状和广播规则。
  • 反向:统一遵循链式法则,梯度始终可以写成“输出梯度 gy × 局部导数”的形式;局部导数的具体表达式已经在对应算子的数学原理小节中给出。

9.3 对偶关系总结

算子对 前向操作 反向操作 关系
Cat / Split y = cat([x0, x1, ...], dim) [gx0, gx1, ...] = split(gy, shapes, dim) 互为反向操作
Split / Cat [y0, y1, ...] = split(x, sizes, dim) gx = cat([gy0, gy1, ...], dim) 互为反向操作
Upsample / Downsample y = upsample(x, scale) gx = downsample(gy, scale) 互为反向操作
BroadcastTo / SumTo y = broadcast_to(x, shape) gx = sum_to(gy, x.shape) 互为反向操作
SumTo / BroadcastTo y = sum_to(x, shape) gx = broadcast_to(gy, x.shape) 互为反向操作

对偶性说明:

  • Cat 和 Split 互为反向操作,体现了算子的对称性
  • 这种对偶性使得反向传播的实现更加直观和高效
  • 理解对偶关系有助于快速掌握算子的前向和反向传播原理

参考实现

  • src/operators/math/:数学运算算子实现(Add、Sub、Mul、Div、MatMul、Pow、Exp、Log、Neg、Square、Sum、BroadcastTo、SumTo、Compare 等)
  • src/operators/activation/:激活函数算子实现(ReLU、Sigmoid、Softmax、SiLU 等)
  • src/operators/conv/:卷积算子实现(Conv2d 等)
  • src/operators/pooling/:池化算子实现(MaxPool2d、AvgPool2d、AdaptiveAvgPool2d 等)
  • src/operators/shape/:形状变换算子实现(Cat、Split、Reshape、Transpose、Flatten 等)
  • src/operators/nn/:神经网络层相关算子实现(Dropout、Upsample、Identity 等)
  • src/operators/normalization/:归一化算子实现(BatchNorm 等)
  • src/operators/loss/:损失函数算子实现(SoftmaxCrossEntropy 等)