0%

机器学习-PyTorch

PyTorch 入门。

自动求导 (1)

计算图 (1.1)

PyTorch 会记录下来 tensor 的计算过程,也就是保留计算图。

假如:

  • 模型函数:$f_{\boldsymbol{\theta}}(\boldsymbol{x})$;$\boldsymbol{\theta} = \begin{bmatrix} \theta_1, \theta_2, \theta_3, \dots \theta_m \end{bmatrix}^T$,$m$ 个参数。
  • 损失:MSE (Mean Square Error);
  • 一个 Batch 包含 $B$ 条训练数据:$(\boldsymbol{x}^i, y^i)$,$i = 1, 2, \dots, B$;

那么,损失函数就是:

$$
L_{\boldsymbol{\theta}} = \frac{1}{B}\sum\limits_{i=1}^{B} (f_{\boldsymbol{\theta}}(\boldsymbol{x}^i) - y^i)^2
$$

注意:$L_{\boldsymbol{\theta}}$ 中只有 ${\boldsymbol{\theta}}$ 是未知数;$\boldsymbol{x}^i$ 和 $y^i$ 都是训练数据;

函数 $f$ 非常复杂,包含很多激活函数。而且对于深度学习,激活函数还会发生嵌套(展开的话,下一层嵌套上一层)。所以,想要把 $L_{\boldsymbol{\theta}}$ 展开成关于 $\theta_1, \theta_2, \dots, \theta_m$ 的独立项是不可能的,PyTorch 也根本不会做这样的尝试。那么 PyTorch 是怎么做的呢?

假设初始参数是 $\boldsymbol{\theta}^0$,PyTorch 会根据 $L_{\boldsymbol{\theta}}$ 一步一步地进行前向计算(forward pass),就像执行程序一样。每一步都是一个运算节点(记为 $p_i$,如乘法、加法、sigmoid、平方、求均值等)。计算完成之后,PyTorch 就记录了:

$$
L^0 = p_1(p_2(p_3(… p_N(\boldsymbol{\theta}^0))))
$$

这就是计算图

链式法则 (1.2)

嵌套函数导数,使用链式法则。

如果:

$$
y = f_1(f_2(f_3(x)))
$$

那么:

$$
\frac{dy}{dx}
=
f_1’(f_2(f_3(x)))
\cdot
f_2’(f_3(x))
\cdot
f_3’(x)
$$

每一层只需要知道自己的局部导数,再乘起来即可。

把这个规则套用到计算图 $L^0 = p_1(p_2(p_3(\dots p_N(\boldsymbol{\theta}^0))))$ 上,对于参数 $\theta_k$,有:

$$
\frac{\partial L}{\partial \theta_k}
=
\frac{\partial p_1}{\partial p_2}
\cdot
\frac{\partial p_2}{\partial p_3}
\cdot
\cdots
\cdot
\frac{\partial p_{N-1}}{\partial p_N}
\cdot
\frac{\partial p_N}{\partial \theta_k}
$$

其中每个因子 $\frac{\partial p_i}{\partial p_{i+1}}$ 都是局部导数,只需知道节点 $p_i$ 是什么运算就能直接算出来(例如:加法节点导数为 1;乘法节点导数为另一个因子;sigmoid 导数为 $p_i(1-p_i)$;平方节点导数为 $2x$)。

PyTorch 就是把这些局部导数逐层累乘,从而得到 $\frac{\partial L}{\partial \theta_k}$。

PyTorch 从 $L^0$ 出发,沿计算图反向逐层计算局部导数并累乘,最终得到 $L$ 对 $\theta_k$ 的偏导数 $\frac{\partial L}{\partial \theta_k}$,存入 $\theta_k$ 的 .grad

对 $\boldsymbol{\theta}$ 的每个分量求偏导数,就得到 Error Surface 上 $\boldsymbol{\theta}^0$ 处的梯度。

举例 (1.3)

网络结构 (1.3.1)

figure1

网络结构

输入层:$x_1$, $x_2$(2 个 feature)
第一层:3 个 sigmoid 神经元
第二层:2 个 sigmoid 神经元
输出层:1 个线性神经元,输出值为 $\hat{y}$

参数定义 (1.3.2)

第一层(3 个神经元,每个接收 2 个输入):

参数 含义
$w_{11}, w_{12}, b_1$ 第 1 个神经元的 2 个权重 + 1 个 bias
$w_{21}, w_{22}, b_2$ 第 2 个神经元的 2 个权重 + 1 个 bias
$w_{31}, w_{32}, b_3$ 第 3 个神经元的 2 个权重 + 1 个 bias

小计:$3 \times 3 = 9$ 个参数。

第二层(2 个神经元,每个接收 3 个输入):

参数 含义
$u_{11}, u_{12}, u_{13}, d_1$ 第 1 个神经元的 3 个权重 + 1 个 bias
$u_{21}, u_{22}, u_{23}, d_2$ 第 2 个神经元的 3 个权重 + 1 个 bias

小计:$2 \times 4 = 8$ 个参数。

输出层(1 个神经元,接收 2 个输入):

参数 含义
$v_1, v_2, b$ 2 个权重 + 1 个 bias

小计:$3$ 个参数。

参数总数:

$$
9 + 8 + 3 = 20
$$

模型函数 (1.3.3)

第一层:

$$
\begin{aligned}
z_1 &= w_{11}x_1 + w_{12}x_2 + b_1 \\
z_2 &= w_{21}x_1 + w_{22}x_2 + b_2 \\
z_3 &= w_{31}x_1 + w_{32}x_2 + b_3
\end{aligned}
$$

$$
\begin{aligned}
h_1 &= \sigma(z_1) \\
h_2 &= \sigma(z_2) \\
h_3 &= \sigma(z_3)
\end{aligned}
$$

其中 $\sigma(z) = \frac{1}{1+e^{-z}}$。

第二层:

$$
\begin{aligned}
t_1 &= u_{11}h_1 + u_{12}h_2 + u_{13}h_3 + d_1 \\
t_2 &= u_{21}h_1 + u_{22}h_2 + u_{23}h_3 + d_2
\end{aligned}
$$

$$
\begin{aligned}
g_1 &= \sigma(t_1) \\
g_2 &= \sigma(t_2)
\end{aligned}
$$

输出层:

$$
\hat{y} = v_1 g_1 + v_2 g_2 + b
$$

损失函数 (1.3.4)

假如一个 Batch 包含 $B$ 条训练数据:$(\boldsymbol{x}^i, y^i)$,$i = 1, 2, \dots, B$;损失用 MSE(均方误差):

$$
L_{\boldsymbol{\theta}} = \frac{1}{B}\sum\limits_{i=1}^{B} (\hat{y}^i - y^i)^2
$$

损失函数 $L$ 是一个关于 $\boldsymbol{\theta}$ 的复杂嵌套函数,其中:

  • ${\boldsymbol{\theta}} = \begin{bmatrix} w_{11},w_{12},\dots,w_{32},b_1,b_2,b_3,u_{11},u_{12},\dots,u_{23},d_1,d_2,v_1,v_2,b \end{bmatrix}^T$

  • $\hat{y}^i$ 是第 $i$ 条样本的预测值。对任意一条样本,把 $\hat{y}$ 完全展开:

$$
\begin{aligned}
\hat{y} = & b + \\
& v_1 \sigma(u_{11}\sigma(w_{11}x_1 + w_{12}x_2 + b_1) + u_{12}\sigma(w_{21}x_1 + w_{22}x_2 + b_2) + u_{13}\sigma(w_{31}x_1 + w_{32}x_2 + b_3) + d_1) + \\
& v_2 \sigma(u_{21}\sigma(w_{11}x_1 + w_{12}x_2 + b_1) + u_{22}\sigma(w_{21}x_1 + w_{22}x_2 + b_2) + u_{23}\sigma(w_{31}x_1 + w_{32}x_2 + b_3) + d_2)
\end{aligned}
$$

前向计算(forward pass)(1.3.5)

假设一个 batch 有 2 条训练数据:

样本 $x_1$ $x_2$ $y$
1 3.0 2.0 1.5
2 0.5 1.2 0.9

假设初始参数$\boldsymbol{\theta}$:

参数 初始值 参数 初始值 参数 初始值
$w_{11}$ 0.3 $u_{11}$ 0.7 $v_1$ 0.6
$w_{12}$ 0.6 $u_{12}$ 0.2 $v_2$ 0.8
$b_1$ 0.2 $u_{13}$ 0.8 $b$ 0.3
$w_{21}$ 0.4 $d_1$ 0.2
$w_{22}$ 0.7 $u_{21}$ 0.5
$b_2$ 0.5 $u_{22}$ 0.4
$w_{31}$ 0.8 $u_{23}$ 0.9
$w_{32}$ 0.1 $d_2$ 0.5
$b_3$ 0.9

样本1

第一层:

$$
\begin{aligned}
z_1 &= w_{11}x_1 + w_{12}x_2 + b_1 = 0.3 \cdot 3.0 + 0.6 \cdot 2.0 + 0.2 = 0.9 + 1.2 + 0.2 = 2.3 \\
z_2 &= w_{21}x_1 + w_{22}x_2 + b_2 = 0.4 \cdot 3.0 + 0.7 \cdot 2.0 + 0.5 = 1.2 + 1.4 + 0.5 = 3.1 \\
z_3 &= w_{31}x_1 + w_{32}x_2 + b_3 = 0.8 \cdot 3.0 + 0.1 \cdot 2.0 + 0.9 = 2.4 + 0.2 + 0.9 = 3.5
\end{aligned}
$$

$$
\begin{aligned}
h_1 &= \sigma(z_1) = \frac{1}{1+e^{-2.3}} = 0.9089 \\
h_2 &= \sigma(z_2) = \frac{1}{1+e^{-3.1}} = 0.9569 \\
h_3 &= \sigma(z_3) = \frac{1}{1+e^{-3.5}} = 0.9707
\end{aligned}
$$

第二层:

$$
\begin{aligned}
t_1 &= u_{11}h_1 + u_{12}h_2 + u_{13}h_3 + d_1 = 0.7 \cdot 0.9089 + 0.2 \cdot 0.9569 + 0.8 \cdot 0.9707 + 0.2 = 1.8042 \\
t_2 &= u_{21}h_1 + u_{22}h_2 + u_{23}h_3 + d_2 = 0.5 \cdot 0.9089 + 0.4 \cdot 0.9569 + 0.9 \cdot 0.9707 + 0.5 = 2.2108
\end{aligned}
$$

$$
\begin{aligned}
g_1 &= \sigma(t_1) = \frac{1}{1+e^{-1.8042}} = 0.8587 \\
g_2 &= \sigma(t_2) = \frac{1}{1+e^{-2.2108}} = 0.9012
\end{aligned}
$$

输出层:

$$
\hat{y}^1 = v_1 g_1 + v_2 g_2 + b = 0.6 \cdot 0.8587 + 0.8 \cdot 0.9012 + 0.3 = 1.5362
$$

样本2

第一层:

$$
\begin{aligned}
z_1 &= w_{11}x_1 + w_{12}x_2 + b_1 = 0.3 \cdot 0.5 + 0.6 \cdot 1.2 + 0.2 = 0.15 + 0.72 + 0.2 = 1.07 \\
z_2 &= w_{21}x_1 + w_{22}x_2 + b_2 = 0.4 \cdot 0.5 + 0.7 \cdot 1.2 + 0.5 = 0.20 + 0.84 + 0.5 = 1.54 \\
z_3 &= w_{31}x_1 + w_{32}x_2 + b_3 = 0.8 \cdot 0.5 + 0.1 \cdot 1.2 + 0.9 = 0.40 + 0.12 + 0.9 = 1.42
\end{aligned}
$$

$$
\begin{aligned}
h_1 &= \sigma(z_1) = \frac{1}{1+e^{-1.07}} = 0.7446 \\
h_2 &= \sigma(z_2) = \frac{1}{1+e^{-1.54}} = 0.8235 \\
h_3 &= \sigma(z_3) = \frac{1}{1+e^{-1.42}} = 0.8053
\end{aligned}
$$

第二层:

$$
\begin{aligned}
t_1 &= u_{11}h_1 + u_{12}h_2 + u_{13}h_3 + d_1 = 0.7 \cdot 0.7446 + 0.2 \cdot 0.8235 + 0.8 \cdot 0.8053 + 0.2 = 1.5302 \\
t_2 &= u_{21}h_1 + u_{22}h_2 + u_{23}h_3 + d_2 = 0.5 \cdot 0.7446 + 0.4 \cdot 0.8235 + 0.9 \cdot 0.8053 + 0.5 = 1.9265
\end{aligned}
$$

$$
\begin{aligned}
g_1 &= \sigma(t_1) = \frac{1}{1+e^{-1.5302}} = 0.8220 \\
g_2 &= \sigma(t_2) = \frac{1}{1+e^{-1.9265}} = 0.8729
\end{aligned}
$$

输出层:

$$
\hat{y}^2 = v_1 g_1 + v_2 g_2 + b = 0.6 \cdot 0.8220 + 0.8 \cdot 0.8729 + 0.3 = 1.4915
$$

损失

$$
\begin{aligned}
L & = \frac{1}{2} \left[(\hat{y}^1 - y^1)^2 + (\hat{y}^2 - y^2)^2\right] \\
& = \frac{1}{2} \left[(1.5362 - 1.5)^2 + (1.4915 - 0.9)^2\right] \\
& = \frac{1}{2}(0.001310 + 0.349872) \\
& = 0.1756
\end{aligned}
$$

计算图 (1.3.6)

以 $w_{11}$ 为例构建计算图,其它参数类似。

  • 样本1:第一层

$$
\begin{aligned}
z_1 &= w_{11}x_1 + w_{12}x_2 + b_1 = 3.0 \cdot w_{11} + 1.4 \\
z_2 &= w_{21}x_1 + w_{22}x_2 + b_2 = 3.1 \\
z_3 &= w_{31}x_1 + w_{32}x_2 + b_3 = 3.5
\end{aligned}
$$

把 $z_1$ 记为 $p_1(w_{11}) = 3.0 \cdot w_{11} + 1.4$

$$
\begin{aligned}
h_1 &= \sigma(z_1)\\
h_2 &= \sigma(z_2) = 0.9569 \\
h_3 &= \sigma(z_3) = 0.9707
\end{aligned}
$$

把 $h_1$ 记为 $p_2(w_{11}) = \sigma(p_1(w_{11}))$

  • 样本1:第二层

$$
\begin{aligned}
t_1 &= u_{11}h_1 + u_{12}h_2 + u_{13}h_3 + d_1 = 0.7 \cdot h_1 + 1.1679 \\
t_2 &= u_{21}h_1 + u_{22}h_2 + u_{23}h_3 + d_2 = 0.5 \cdot h_1 + 1.7564
\end{aligned}
$$

把 $t_1$ 记为 $p_3(w_{11}) = 0.7 \cdot p_2(w_{11}) + 1.1679$
把 $t_2$ 记为 $p_4(w_{11}) = 0.5 \cdot p_2(w_{11}) + 1.7564$

$$
\begin{aligned}
g_1 &= \sigma(t_1) \\
g_2 &= \sigma(t_2)
\end{aligned}
$$

把 $g_1$ 记为 $p_5(w_{11}) = \sigma(p_3(w_{11}))$
把 $g_2$ 记为 $p_6(w_{11}) = \sigma(p_4(w_{11}))$

  • 样本1:输出层

$$
\hat{y}^1 = v_1 g_1 + v_2 g_2 + b = 0.6 \cdot g_1 + 0.8 \cdot g_2 + 0.3
$$

把 $\hat{y}^1$ 记为 $p_7(w_{11}) = 0.6 \cdot p_5(w_{11}) + 0.8 \cdot p_6(w_{11}) + 0.3$

  • 样本2:第一层

$$
\begin{aligned}
z_1 &= w_{11}x_1 + w_{12}x_2 + b_1 = 0.5 \cdot w_{11} + 0.92 \\
z_2 &= w_{21}x_1 + w_{22}x_2 + b_2 = 1.54 \\
z_3 &= w_{31}x_1 + w_{32}x_2 + b_3 = 1.42
\end{aligned}
$$

把 $z_1$ 记为 $p_8(w_{11}) = 0.5 \cdot w_{11} + 0.92$

$$
\begin{aligned}
h_1 &= \sigma(z_1) \\
h_2 &= \sigma(z_2) = 0.8235 \\
h_3 &= \sigma(z_3) = 0.8053
\end{aligned}
$$

把 $h_1$ 记为 $p_9(w_{11}) = \sigma(p_8(w_{11}))$

  • 样本2:第二层

$$
\begin{aligned}
t_1 &= u_{11}h_1 + u_{12}h_2 + u_{13}h_3 + d_1 = 0.7 \cdot h_1 + 1.0089 \\
t_2 &= u_{21}h_1 + u_{22}h_2 + u_{23}h_3 + d_2 = 0.5 \cdot h_1 + 1.5542
\end{aligned}
$$

把 $t_1$ 记为 $p_{10}(w_{11}) = 0.7 \cdot p_9(w_{11}) + 1.0089$
把 $t_2$ 记为 $p_{11}(w_{11}) = 0.5 \cdot p_9(w_{11}) + 1.5542$

$$
\begin{aligned}
g_1 &= \sigma(t_1) \\
g_2 &= \sigma(t_2)
\end{aligned}
$$

把 $g_1$ 记为 $p_{12}(w_{11}) = \sigma(p_{10}(w_{11}))$
把 $g_2$ 记为 $p_{13}(w_{11}) = \sigma(p_{11}(w_{11}))$

  • 样本2:输出层

$$
\hat{y}^2 = v_1 g_1 + v_2 g_2 + b = 0.6 \cdot g_1 + 0.8 \cdot g_2 + 0.3
$$

把 $\hat{y}^2$ 记为 $p_{14}(w_{11}) = 0.6 \cdot p_{12}(w_{11}) + 0.8 \cdot p_{13}(w_{11}) + 0.3$

  • 损失

$$
L = \frac{1}{2}\left[(\hat{y}^1 - y^1)^2 + (\hat{y}^2 - y^2)^2\right]
$$

把 $(\hat{y}^1 - y^1)^2$ 记为 $p_{15}(w_{11}) = (p_7(w_{11}) - 1.5)^2$
把 $(\hat{y}^2 - y^2)^2$ 记为 $p_{16}(w_{11}) = (p_{14}(w_{11}) - 0.9)^2$
把 $L$ 记为 $p_{17}(w_{11}) = \frac{1}{2}(p_{15}(w_{11}) + p_{16}(w_{11}))$

至此,$L = p_{17}(w_{11})$ 是一个关于 $w_{11}$ 的嵌套函数,即 1.1 中 $L^0 = p_1(p_2(\dots p_N(\boldsymbol{\theta}^0)))$ 的具体形态(沿 $w_{11}$ 的路径共 17 个运算节点)。代入 $w_{11}=0.3$ 验证:$p_7(0.3)=1.5362$、$p_{14}(0.3)=1.4915$、$p_{17}(0.3)=0.1756$,与 1.3.5 一致。

机器视角

重要说明:上文是盯着一个参数 ($w_{11}$) 从“人脑视角”看到的计算图。实际上:

  • PyTorch 存的是所有参数共享的一张运算 DAG,不是每个参数一条链。
  • 一次 forward 建一张图,节点是原子运算(mul、add、sigmoid、平方、求均值…),边是 tensor。而本小节为了突出展示 $w_{11}$ 相关的链条,把其它参数的运算都省略了,直接替换成了它们的结果。20 个参数共用这张图。
  • 对 $w_{11}$ 的链是 backward 时沿图反向走出来的,不是事先存好的。
  • PyTorch 不做符号代入/合并。前面展示的 $p_1(w_{11}) = 3.0 \cdot w_{11} + 1.4$ 是把其它参数和子表达式折叠成常数了,这是为了突出 $w_{11}$ 相关的链条。PyTorch 里这会保持为多个独立节点:$mul(w_{11}, 3.0)$、$mul(0.6, 2.0)$、$add$、$add$;常数 $1.4$、$1.1679$ 等在图里是各自子图的计算结果,不会被合并成一个字面量。
  • 每个节点只存:运算类型 + 输入/输出 tensor 的引用(部分节点额外保存 backward 需要的中间值,如 sigmoid 存自己的输出、乘法存另一个因子)。局部导数是 backward 时用它现算的。
  • backward 的走法:从 $L$ 出发,反向拓扑遍历这张共享 DAG,每个节点把自己的局部导数乘进上游传来的梯度;在分叉点(如 $h_1$ 同时喂给 $t_1$ 和 $t_2$)把多条路径的贡献相加——这正好对应 $p_2$ 出现在 $p_3$ 和 $p_4$ 两处。

总之,PyTorch 的“机器视角”是存原始运算 DAG + 数值 tensor,反向时逐节点累乘累加,从不生成 $p_i(w_{11})$ 这种符号式单变量函数。

figure2

计算图 DAG

反向传播(backward)(1.3.7)

人类视角:从外向内手算 $\partial L/\partial w_{11}$

forward 是“由内向外算每个节点的值”,backward 是“由外向内传每个节点的导数”。记 $\bar{p}_i \triangleq \partial L/\partial p_i$,即“流进节点 $p_i$ 的梯度”。每个运算节点的局部导数规则见 1.2;若某节点的输出同时喂给多个下游节点,则把各条贡献相加

  • 起点:$\bar{p}_{17} = 1$($L$ 对自身求导)。
  • $p_{17}=\frac{1}{2}(p_{15}+p_{16})$,加法节点系数为 $\frac{1}{2}$:

$$\bar{p}{15} = \tfrac{1}{2}, \qquad \bar{p}{16} = \tfrac{1}{2}$$

  • 平方节点 $p_{15}=(p_7-1.5)^2$,局部导数 $2(p_7-1.5)$,与上游的 $\tfrac{1}{2}$ 相乘正好是残差 $(\hat{y}^1-y^1)$:

$$\bar{p}7 = 1.5362-1.5 = 0.0362, \qquad \bar{p}{14} = 1.4915-0.9 = 0.5915$$

  • 输出层加法节点 $p_7 = 0.6,p_5+0.8,p_6+0.3$,系数即“另一个因子”:

$$\bar{p}_5 = 0.0362\cdot0.6 = 0.02172, \qquad \bar{p}_6 = 0.0362\cdot0.8 = 0.02896$$

$$\bar{p}{12} = 0.5915\cdot0.6 = 0.3549, \qquad \bar{p}{13} = 0.5915\cdot0.8 = 0.4732$$

  • sigmoid 节点,局部导数 = 输出·(1−输出)(即 1.2 的 $p_i(1-p_i)$):

$$\bar{p}_3 = 0.02172\cdot(0.8587\cdot0.1413) = 0.02172\cdot0.1213 = 0.002635$$

$$\bar{p}_4 = 0.02896\cdot(0.9012\cdot0.0988) = 0.02896\cdot0.0890 = 0.002579$$

$$\bar{p}_{10} = 0.3549\cdot(0.8220\cdot0.1780) = 0.3549\cdot0.1463 = 0.051928$$

$$\bar{p}_{11} = 0.4732\cdot(0.8729\cdot0.1271) = 0.4732\cdot0.1109 = 0.052499$$

  • 第二层加法节点。注意 $h_1$ 同时喂给 $t_1$ 和 $t_2$,所以 $p_2$/$p_9$ 处要把两条贡献相加(分叉点链式法则的具体形态):

$$\bar{p}_2 = 0.002635\cdot0.7 + 0.002579\cdot0.5 = 0.001845+0.001289 = 0.003134$$

$$\bar{p}_9 = 0.051928\cdot0.7 + 0.052499\cdot0.5 = 0.036350+0.026250 = 0.062599$$

  • 第一层 sigmoid 节点:

$$\bar{p}_1 = 0.003134\cdot(0.9089\cdot0.0911) = 0.003134\cdot0.0828 = 0.000260$$

$$\bar{p}_8 = 0.062599\cdot(0.7446\cdot0.2554) = 0.062599\cdot0.1902 = 0.011905$$

  • 叶子 $w_{11}$:它同时进入 $p_1$(系数 $x_1=3.0$)与 $p_8$(系数 $x_1=0.5$),两条路径相加:

$$\frac{\partial L}{\partial w_{11}} = 0.000260\cdot3.0 + 0.011905\cdot0.5 = 0.000779+0.005952 = 0.006731 \approx 0.0067$$

脚本验证:数值差分与全精度反向均得 $0.006731$,与手算一致。

两个观察:

  • 两条样本的贡献为 $0.000779$ 对 $0.005952$:样本 2 的残差($0.5915$)是样本 1($0.0362$)的十几倍,梯度主要由误差大的样本驱动——“误差越大、修正越狠”。
  • 梯度沿链从 $0.5915$ 一路缩到 $0.0067$:每层 sigmoid 的局部导数不超过 $0.25$,层数越深梯度越小——梯度消失的原型。

机器视角

上面是“盯住一个参数写出符号链”。PyTorch 的 backward() 做的是同一份计算,只是记账方式换成在共享 DAG 上走一遍:

  • 从 $L$ 带梯度 $1.0$ 出发,按反向拓扑序遍历;每个节点用 forward 时保存的中间值(sigmoid 存自己的输出、乘法存另一个因子、平方存输入)现算局部导数,乘进上游传来的梯度再发出去——数值正是上小节那些 $\bar{p}_i$。
  • 分叉点处相加;叶子参数处把梯度累加.grad 属性。注意是累加(+=)而不是覆盖,所以每轮 backward 前要 optimizer.zero_grad() 清掉旧梯度,否则多步梯度会叠在一起。
  • 走一遍同时得到全部 20 个参数的梯度。人类视角手算的只是 $w_{11}$ 一个,机器视角一次全给(脚本验证值,保留 4 位小数):
参数 $\partial L/\partial$ 参数 $\partial L/\partial$
$w_{11}$ 0.0067 $u_{11}$ 0.0411
$w_{12}$ 0.0148 $u_{12}$ 0.0453
$b_1$ 0.0122 $u_{13}$ 0.0444
$w_{21}$ 0.0025 $d_1$ 0.0546
$w_{22}$ 0.0056 $u_{21}$ 0.0414
$b_2$ 0.0046 $u_{22}$ 0.0457
$w_{31}$ 0.0073 $u_{23}$ 0.0448
$w_{32}$ 0.0170 $d_2$ 0.0551
$b_3$ 0.0140 $v_1$ 0.5173
$v_2$ 0.5489
$b$ 0.6277
  • 两种视角对照:数学是同一份链式法则,数值是同一份局部导数;人类视角适合理解,机器视角适合执行。梯度到手,下一步就是梯度下降 $\boldsymbol{\theta} \leftarrow \boldsymbol{\theta} - \eta,\partial L/\partial\boldsymbol{\theta}$(见下一节)。
写的不错,有赏!