查看全课大纲(第 77 / 82 节)
  1. 1.函数与数据泛化
  2. 2.向量:从几何直觉到高维空间
  3. 3.矩阵与张量
  4. 4.特征值与特征向量:矩阵变换的不动方向
  5. 5.凸函数:从向量求导的矩阵记号到好优化的函数形状
  6. 6.概率与统计:机器学习的数学基础
  7. 7.独立性与随机变量:贝叶斯公式与二项分布
  8. 8.期望、方差与协方差:随机变量的数字画像
  9. 9.联合概率:当一条轴不够用的时候
  10. 10.概率分布(一):从伯努利到伽马函数
  11. 11.概率分布(二):贝塔、泊松、正态与两个贝叶斯故事
  12. 12.机器学习的模型:找输入与输出的关系
  13. 13.机器学习的应用
  14. 14.过拟合
  15. 15.机器学习的分类
  16. 16.有监督学习:从房价预测说起
  17. 17.损失函数
  18. 18.梯度下降(上):更新规则与学习速率
  19. 19.梯度下降(2)
  20. 20.牛顿方法
  21. 21.KNN简介
  22. 22.距离测算
  23. 23.朴素贝叶斯
  24. 24.朴素贝叶斯的实际应用
  25. 25.信息熵:决策树构建的度量基础
  26. 26.信息增益与决策树的构建
  27. 27.ID3算法
  28. 28.决策树节点的划分
  29. 29.CART树
  30. 30.Bagging
  31. 31.Boosting:自适应提升算法
  32. 32.Gradient Boosting
  33. 33.线性模型:回归与分类的同一副骨架
  34. 34.最小二乘:把最优参数直接解出来
  35. 35.最小二乘法的几何与概率视角
  36. 36.极大似然:给最小二乘一个概率出身
  37. 37.逻辑回归(1):把线性模型接到分类上
  38. 38.逻辑回归(2):损失函数与多分类
  39. 39.---- 手造数据: 5 个样本, 2 个特征; x2 几乎 = 2*x1 (偏差手工指定, 无随机) ----
  40. 40.讲师例: 向量 "23014" -- 三把范数尺子量同一个向量
  41. 41.优化方法(1):坐标下降与前向选择
  42. 42.优化方法(2):前向策略与最小角回归
  43. 43.拉格朗日乘子法(1):从无约束到等式约束
  44. 44.拉格朗日乘子法(2):等式约束下的平行条件与拉格朗日函数
  45. 45.拉格朗日乘子法(3):不等式约束与 KKT 条件
  46. 46.最大间隔:把分类问题变成优化问题
  47. 47.拉格朗日对偶问题:把 w 和 b 全部换成 α
  48. 48.支持向量:边界上的少数派
  49. 49.SVM 计算过程:一笔到底算完,再走进核方法
  50. 50.贝叶斯公式:概率即信念
  51. 51.隐含变量和概率模型:给看不见的原因建模
  52. 52.EM 算法:期望与最大化的迭代
  53. 53.琴生不等式:EM 的数学钥匙
  54. 54.三个硬币问题:EM 的复盘与衔接
  55. 55.高斯混合模型:用隐变量解释双峰数据
  56. 56.隐马尔可夫模型
  57. 57.隐马尔可夫模型(2):前向算法与动态混合模型
  58. 58.隐马尔可夫模型在 NLP 中的运用
  59. 59.EM 算法:把「为什么收敛」推到底
  60. 60.HMM 在 NLP 中的运用:从分词到机器翻译
  61. 61.HMM 参数估计:Baum-Welch 的三块拉格朗日
  62. 62.高维下的高斯模型
  63. 63.主题模型:从词袋到隐含主题
  64. 64.Dirichlet 分布(1)
  65. 65.Dirichlet 分布(2)
  66. 66.统计学习内容回顾
  67. 67.K-means 算法
  68. 68.PCA 降维
  69. 69.SVD 降维
  70. 70.从聚类到流形
  71. 71.局部线性嵌入
  72. 72.随机邻域嵌入:SNE 与 t-SNE
  73. 73.无参模型:让数据自己说话
  74. 74.高斯过程:函数空间里的正态分布
  75. 75.高斯过程(2):噪声、正则化与核的线性组合
  76. 76.神经网络简介:从感知器到自编码器
  77. 77.反向传播:把误差一层层传回去
  78. 78.卷积神经网络(CNN):让特征自己学出来
  79. 79.循环神经网络(RNN):给网络装上记忆
  80. 80.深度学习回顾:静态一类,动态一类
  81. 81.增强学习(1):蒙特卡洛、树搜索与 Q-learning
  82. 82.强化学习(2):从乌鸦吃核桃到井字棋的 value 回传

反向传播:把误差一层层传回去

约 23 分钟

📺 正在播放小象官方高清录播(支持倍速与清晰度调节)

小象实战讲义 · 机器学习算法精讲

上一节结尾我们停在门口:构造损失函数时要「连续」,这样误差才能不断地传递回来。本节就讲这件事的正片——反向传播(Back Propagation):神经网络里最重要的算法,是「得到训练神经网络」最重要的一块。很多人对它有不同解释,讲师开场就说:我原先也有特别长的公式给大家解释这件事,直到后来发现,其实有更好的例子——所以本节的路线不是推公式,而是两个算例:一个只有加法和乘法,一个带 sigmoid;一步一步把梯度乘回去,每一层的数都手算可验。

💡 核心导读

  • 要调的是谁:损失 EE 最小 → 每个参数 ww 的梯度 → 梯度下降(4.3 节老朋友);ww 实际是大矩阵,更新的是其中每个 wijw_{ij}——「统一方式表述」而已。
  • 链式法则是全部:dfdx=dfdg⋅dgdx\frac{df}{dx} = \frac{df}{dg} \cdot \frac{dg}{dx},一层层乘回去(4.4 节「先外后内」的老规矩)。
  • 算例一 f=(x+y)⋅zf = (x+y) \cdot z:加法和乘法——恰好就是神经网络前半部分(线性组合)的全部构成;绿色正向算值,红色反向算导数。
  • 算例二 sigmoid 神经元:本节主戏——从 f/f=1f/f = 1 出发,−0.53→−0.2→0.2→-0.53 \to -0.2 \to 0.2 \to 每个权重一个梯度,每个数字手算可验。
  • sigmoid 是一个「门」:σ′=σ(1−σ)\sigma' = \sigma(1-\sigma)——整块一起算与一步一步算,一模一样。
  • 大图景:10 个参数就是 10 维空间——每个方向都要有梯度,迭代循环找 loss 的最低点。

要调整谁:从 E 到每一个 w

先把问题摆正。我们整个神经网络是线性、非线性叠加的形式(上一节),给定输入、得到输出 y^\hat{y},我们期望的是:yy 与 y^\hat{y} 之间误差的平方和最小——这就是要构造的损失函数 EE(4.2 节的老朋友)。

要使 EE 最小,需要调整谁?这么多参数里,比如我们要求关于 w1w_1 的——调整 w1w_1;对 EE,我们要调整 w2w_2……其实任何参数的调整,都可以用以前学过的梯度下降(4.3 节):给 w1w_1 随机一个值,它下一个值就等于当前值,加上学习速率 α\alpha 乘上 w1w_1 的梯度——这个梯度是关于 EE 的梯度。(一个记号提醒:4.3 节的最小化更新是「往负梯度方向走」的减号 w←w−α⋅∂E/∂ww \leftarrow w - \alpha \cdot \partial E / \partial w;本节的算例里 ff 还只是网络输出、没挂上要最小化的 EE,讲师板书随手用了加号演示「梯度进更新式」的机械形态,数值以板书为准。)

这里有个几乎必被问到的问题,讲师自己替大家问了:「w1w_1 实际上是一个大的矩阵,难道矩阵同时更新吗?」——不是这样。你真正更新的时候,更新的是 w1w_1 里边的每一个 wijw_{ij}:每个分量各自加上 α\alpha 乘上自己的梯度。每次更新,只是我们用统一的方式表述而已。

所以最关键的问题在于:求出来误差和前边需要调整的参数之间的梯度关系。这该怎么做?

链式法则:先复习,再用

先给大家求一个最简单的。如果大家还记得 f(g(x))f(g(x))——想求它关于 xx 的导数,等于多少?

ddxf(g(x))=f′(g(x))⋅g′(x)\frac{d}{dx} f(g(x)) = f'(g(x)) \cdot g'(x)

也可以换一种描述方式:先求 ff 对 gg 的导数,乘上 gg 对 xx 的导数——这就是链式法则,复合函数求导的重要部分(4.4 节讲梯度下降时用过,先外后内)。讲师的原话很实在:「如果大家还记得的话——不记得也得没办法,也得要记住我们已经讲过这些事情。」

层数再多也一样:如果有一个 kk 是 ff 的函数,那就 kk 对 ff 求导、ff 对 gg 求导、gg 再对 xx 求导,这么连成一条链。而我们这个神经网络要做的,就是这样一个反向传导的过程。

算例一:f=(x+y)⋅zf = (x+y) \cdot z

最简单的复合函数构成就是加法和乘法——当然,这也是我们神经网络前一部分(线性组合)的全部内容。设:

f=(x+y)⋅z,x=−2, y=5, z=−4f = (x + y) \cdot z, \qquad x = -2,\ y = 5,\ z = -4

先正向算。引入一个中间变量 qq,把「x+yx+y」变成单独一个变量:q=x+y=3q = x + y = 3;再 f=q⋅z=3×(−4)=−12f = q \cdot z = 3 \times (-4) = -12,非常容易计算。为什么要引入 qq?因为这样一拆,每一步都容易求导:qq 对 xx 求导、对 yy 求导,每个都等于 1;ff 变成 qq 和 zz 的乘积,对 qq 求导等于 zz、对 zz 求导等于 qq。

再反向算。想求 ff 对 xx 的偏导,用链式原则:

∂f∂x=∂f∂q⋅∂q∂x=z⋅1=−4\frac{\partial f}{\partial x} = \frac{\partial f}{\partial q} \cdot \frac{\partial q}{\partial x} = z \cdot 1 = -4

ff 对 qq 求导等于 zz,zz 是 −4-4,所以第一部分是 −4-4;qq 对 xx 求导等于 1——乘上 1,最后等于 −4-4。把这个 −4-4 用红色写在 xx 旁边:ff 对 xx 的传递导数是 −4-4。当然,ff 对 yy 的偏导也是 −4-4(qq 对 yy 的导数同样是 1)。但 ff 对 zz 的导数就不一样了:等于 qq,而 qq 的输入值是 3——所以它的导数是 3。

# 例 1: f = (x + y) * z —— 前向(绿)与反向(红)
x, y, z = -2, 5, -4

# --- 前向: 绿色, 正面计算 ---
q = x + y            # 引入中间变量 q, 把 x+y 变成单独一个变量
f = q * z
print(f"[前向/绿] q = x + y = {q}")
print(f"[前向/绿] f = q * z = {f}")

# --- 反向: 红色, 从输出往回乘 ---
df_dq = z            # f = q*z, 对 q 求导得 z
df_dz = q            # f = q*z, 对 z 求导得 q
dq_dx = 1            # q = x+y, 对 x 求导得 1
dq_dy = 1
print(f"[反向/红] df/dx = df/dq * dq/dx = {df_dq} * {dq_dx} = {df_dq * dq_dx}")
print(f"[反向/红] df/dy = df/dq * dq/dy = {df_dq} * {dq_dy} = {df_dq * dq_dy}")
print(f"[反向/红] df/dz = {df_dz}")
[前向/绿] q = x + y = 3
[前向/绿] f = q * z = -12
[反向/红] df/dx = df/dq * dq/dx = -4 * 1 = -4
[反向/红] df/dy = df/dq * dq/dy = -4 * 1 = -4
[反向/红] df/dz = 3

讲师给这套双色记法定了名:绿色是我们输入值正面的计算,红色是最后输出的结果对于每一个变量的导数。那这些导数有什么用?——以后会看到:导数是用来调整所有这些数的。这个例子特别简单,我猜大家没有什么问题。

顺路:激活函数的动物园

往下走之前,讲师顺路把激活函数盘了一遍。整个神经网络,就是先通过刚才描述的线性组合(加和乘的关系),之后通过一个非线性的激活函数——这个激活函数:

  • 有的时候用 sigmoid。它最大的问题是:输入跑到很远的两端时,梯度就非常小了;
  • 有的时候用 tanh,会好一些;
  • 但最好是 rectifier(修正线性,即现在常说的 ReLU):保持大于 0 的时候,它的导数就是一个 1——一直是这样一个特别好的值,不会减小;
  • 还有一种 softplus,是去逼近 rectifier 的另一个比较连续的激活函数,这个方向都是 OK 的。

所以整个过程就是两个方向:绿色叫 activation——把这些值 xx 输进去,激活出 zz;红色就是求它的 gradient——对 zz 求导、zz 对 yy、yy 对 xx,一路乘回去。整个反向传播,走的就是红色这条路。

算例二:sigmoid 神经元,一步一步乘回去(主戏)

现在看经过 sigmoid 这个激活函数时,整个运算该怎么做。这个例子跟我们以前的记号稍稍有点区别——讲师原话:「这个也没办法,是别人写的。」线性组合是 w0x0+w1x1+w2w_0 x_0 + w_1 x_1 + w_2,其中 w2w_2 就当作偏置 bb。讲师还预防了一问:「大家不用纠结说我们这整个 presentation 怎么又变了?一样的道理,没什么区别。」——它描述的正是上一节那个 sigmoid 函数、也就是逻辑回归(8.5 节):给定的输入配上系数 w0,w1,w2w_0, w_1, w_2,做成线性组合,经过 ee 的负的线性组合,构造出 sigmoid,它的输出就是我们最终激活出来的输出。

值从哪来:初始化

假设给定的值是:w0=2, x0=−1, w1=−3, x1=−2, w2=−3w_0 = 2,\ x_0 = -1,\ w_1 = -3,\ x_1 = -2,\ w_2 = -3。这些值哪来的?——xx 肯定知道,xx 就是我们正常的输入;ww 呢,随机先取一些值,就像在最开始做一个初始化一样。

前向(绿):从 1 到 0.73

w0x0=2×(−1)=−2w_0 x_0 = 2 \times (-1) = -2,w1x1=(−3)×(−2)=6w_1 x_1 = (-3) \times (-2) = 6,它俩一加为正 4;正 4 再加上 w2=−3w_2 = -3,合起来为 1——所以括号里头得出的就是一个正 1。前面还有一个负号,乘上 −1-1 变成 −1-1;ee 的负一次幂是 0.37(讲师先顺口说了个 0.73、当场纠正——0.73 是最终输出,别混);0.37 再加 1,整个分母是 1.37;分子是 1 的时候,最后得出来的结果——最后一个绿色的 0.73。讲师对这个前向很有底气:「大家不用纠结这个数对不对,一定是对的,你不信大家可以算一下。」

反向(红)之前:四个求导基本功

在做整个 back propagation 之前,需要对每一个子函数求导数。比较难的有这么几个(其实都是基本功):

  • exe^x 求导,它的导数是它自己;
  • aa 倍乘上 xx,导数是 aa——相当于常数乘 xx;
  • xx 分之一就是 x−1x^{-1},再求导,变成 −1x2-\frac{1}{x^2};
  • c+xc + x,一个常数对 xx 求导,最后等于 1。

反向(红):每一步都是「本段导数 × 上游传来的导数」

第一块,永远是一:这是 ff 对 ff 自己求导,返回的第一部分永远是 1。

过 1x\frac{1}{x}:它的导数是 −1x2-\frac{1}{x^2},而 1x\frac{1}{x} 之前输入的是 1.37——所以这一段的导数是 −11.372≈−0.53-\frac{1}{1.37^2} \approx -0.53,乘上上游的 1,从这步返回去的就是 −0.53。

过 +1+1:加一个常数,导数还是 1——问大家 −0.53 跟加一比应该有多少?不用问,1 乘上 −0.53,还是 −0.53。

过 exp:exp 这边输入的是 −1-1,导数是它自己 e−1=0.37e^{-1} = 0.37;乘上边传过来的 −0.53,一乘是 0.37×(−0.53)0.37 \times (-0.53),最后得到 −0.2。到这里讲师把原则钉死了一句话:「每次都是求这一阶段的导数,乘上上一部分传来的导数。」——链式法则的双层版,kk 再多几层就再多乘几段,完全一样。

过 ×(−1)\times(-1):−0.2 乘上 −1-1 的导数(就是 −1-1 本身),负负得正,变成 0.2。

加法扇出:0.2 遇到加法节点,导数都是 1,所以两条分支各得 0.2——还记得刚才 x+yx + y 那个形式吗?它传导的是 −4-4,只要一加和,两支就都是 −4-4。同一个道理。

到权重了:我们关心 w0w_0 这一块。w0x0w_0 x_0 这一段,对 w0w_0 求导得到的是 x0=−1x_0 = -1;−1-1 乘上面传来的 0.2,等于 −0.2。(讲师在这里把前向的绿色值和反向的红色梯度看混了一眼——「这边呢是 6——啊,对,不是 6,是 0.2,Sorry」——前向是 6,反向是 0.2,两套数字别串色。)同样道理:

  • 对 x0x_0 求导,得到的是 w0=2w_0 = 2,2×0.2=2 \times 0.2 = 0.4;
  • 对 w1w_1 求导,得到的是 x1=−2x_1 = -2,−0.4;
  • 对 x1x_1 求导,得到的是 w1=−3w_1 = -3,−0.6;
  • 偏置那一路(w2w_2 乘的输入是常数 1),梯度就是 0.2。

这样的结果都算出来了。把整个前向与反向跑成代码——每一步对应板书的一个数字:

import math

# 例 2: sigmoid 神经元(权重为随机初始化后给定的值)
w0, x0 = 2, -1
w1, x1 = -3, -2
w2 = -3            # 偏置

# --- 前向: 绿色, 一步一步 ---
s = w0 * x0 + w1 * x1 + w2     # 线性组合 = 1
neg = -s                       # = -1
e_neg = math.exp(neg)          # = 0.37
denom = 1 + e_neg              # = 1.37
f = 1 / denom                  # = 0.73
print("[前向/绿]")
print(f"  线性组合 s = 2*(-1) + (-3)*(-2) + (-3) = {s}")
print(f"  -s = {neg},  e^(-s) = {e_neg:.2f},  1+e^(-s) = {denom:.2f}")
print(f"  f = 1/(1+e^(-s)) = {f:.2f}")
print()

# --- 反向: 红色, 每步 = 本段导数 * 上游传来的导数 ---
print("[反向/红]")
g = 1.0                                   # f 对 f 自己求导, 起点永远是 1
print(f"  起点 f/f = {g:.0f}")
g = g * (-1.0 / denom ** 2)               # 1/x 的导数 -1/x^2, 此处 x=1.37
print(f"  过 1/x    乘 -1/{denom:.2f}^2 = {-1.0 / denom ** 2:.2f} -> {g:.2f}")
g = g * 1.0                               # 加常数 1, 导数是 1
print(f"  过 +1     乘 1 -> {g:.2f}")
g = g * e_neg                             # exp 的导数是它自己, 此处 = 0.37
print(f"  过 exp    乘 e^(-s) = {e_neg:.2f} -> {g:.2f}")
g = g * (-1.0)                            # 乘 (-1)
print(f"  过 *(-1)  乘 -1 -> {g:.2f}")
# 加法节点: 梯度原样分给每个加数, 再各自乘本地梯度
print(f"  加法扇出: 梯度 {g:.1f} 原样分给 w0*x0 与 w1*x1 两支")
dw0, dx0 = g * x0, g * w0
dw1, dx1 = g * x1, g * w1
dw2 = g * 1.0
print(f"    df/dw0 = {g:.1f} * x0 = {dw0:.1f}    df/dx0 = {g:.1f} * w0 = {dx0:.1f}")
print(f"    df/dw1 = {g:.1f} * x1 = {dw1:.1f}    df/dx1 = {g:.1f} * w1 = {dx1:.1f}")
print(f"    df/dw2 = {g:.1f} * 1  = {dw2:.1f}   (偏置那一路乘的输入是常数 1)")
print()

# --- gate 捷径: 把 sigmoid 整块当一个门, sigma' = f*(1-f) ---
gate = f * (1 - f)
print(f"[gate 捷径] sigma'(s) = f*(1-f) = {f:.2f} * {1 - f:.2f} = {gate:.2f}")
print(f"[对照] 逐步链乘到 s 处 = {g:.2f}, 与 gate 捷径 {gate:.2f} 一模一样")
[前向/绿]
  线性组合 s = 2*(-1) + (-3)*(-2) + (-3) = 1
  -s = -1,  e^(-s) = 0.37,  1+e^(-s) = 1.37
  f = 1/(1+e^(-s)) = 0.73

[反向/红]
  起点 f/f = 1
  过 1/x    乘 -1/1.37^2 = -0.53 -> -0.53
  过 +1     乘 1 -> -0.53
  过 exp    乘 e^(-s) = 0.37 -> -0.20
  过 *(-1)  乘 -1 -> 0.20
  加法扇出: 梯度 0.2 原样分给 w0*x0 与 w1*x1 两支
    df/dw0 = 0.2 * x0 = -0.2    df/dx0 = 0.2 * w0 = 0.4
    df/dw1 = 0.2 * x1 = -0.4    df/dx1 = 0.2 * w1 = -0.6
    df/dw2 = 0.2 * 1  = 0.2   (偏置那一路乘的输入是常数 1)

[gate 捷径] sigma'(s) = f*(1-f) = 0.73 * 0.27 = 0.20
[对照] 逐步链乘到 s 处 = 0.20, 与 gate 捷径 0.20 一模一样

拿梯度去更新

我们举一个例子,关心 w0w_0,最后得出一个 −0.2-0.2,这是什么意思?最开始给定的 w0w_0 等于 2。下一次需要调整的时候,下一阶段的 w0w_0 就等于这一时刻的 w0w_0,加上学习速率 α\alpha 乘上 w0w_0 的梯度。假设 α=0.1\alpha = 0.1(随便举的例子):

w0new=2+0.1×(−0.2)=1.98w_0^{\text{new}} = 2 + 0.1 \times (-0.2) = 1.98

所以下一次更新的时候,w0w_0 就会变成 1.98(讲师口播说成了「0.98」,是口误——2−0.022 - 0.02 是 1.98)。同样的道理,你可以不断地更新 w1w_1、w2w_2,所有的这些。

那 E 在哪里

当然,我们其实最后真正更新优化的,不是 ff 这个函数本身。f(x)f(x) 这个函数,需要跟它得到的训练数据期望的 yy 之间做一个差,才是我们的 EE。我只是举了个特别简单的例子:输入第一个数据 x0,x1x_0, x_1,它可能有一个期望的 yy,但我们这没有一个大家期望的 yy——就算有的话也没什么。有一个期望的 yy 的话,yy 是一个常数:EE 对 w0w_0 求导,就等于 EE 对 ff 求导、乘上 ff 对 w0w_0 求导——而 EE 对 ff 求导是一个常数,没有变化。所以链上多乘一层而已,还是刚才的梯度在传。这就是通过一个具体算例告诉大家:back propagation 是怎么去做的。而且刚才只传导了一层——下面看两层怎么传导。

把 sigmoid 当成一个「门」

在这之前,还有一个重点:也许有的时候,我们需要把整个一块变成单独的一个区域。比如说,sigmoid 这一整块可以当成是一个大的 gate(门)——它里边是一个 sigmoid 函数。sigmoid 函数求导自己有特别多的优点:

σ′(z)=σ(z)(1−σ(z))\sigma'(z) = \sigma(z)\left(1 - \sigma(z)\right)

求导之后,等于「一减去它本身,乘上它本身」。所以我们可以把整个这一大块看成一个复合函数:它输出的结果是 0.73,那么返回梯度直接就 0.73 乘上 1−0.73=0.271 - 0.73 = 0.27,直接算出 0.2——不需要一步一步地回去算(上面代码块最后一行的对照:逐步链 0.20,gate 捷径 0.20,一模一样)。一步一步回去算和整个一起算,结果是完全相同的——只是这里边有个特殊的性质而已。这个「门」的思想,就是工程上把一整块运算打包、只留进出两个接口的做法。

两层怎么传导

再往后走:两层的时候,这件事情该怎么算?跟着思路走——

ai=f(zi)(sigmoid),zi=∑j=13wijxj+b(线性组合),S=u⋅a(汇聚)a_i = f(z_i) \quad (\text{sigmoid}), \qquad z_i = \sum_{j=1}^{3} w_{ij} x_j + b \quad (\text{线性组合}), \qquad S = u \cdot a \quad (\text{汇聚})

AA 是输出,f(zi)f(z_i) 是一个 sigmoid 函数;ziz_i 等于所有的线性组合——每个节点接一个三维的系数;SS 是这一块:a1,a2a_1, a_2 乘上 uu 那边的线性组合。回过头来做的是 SS 对 ww 求导。一层层展开:

∂S∂wij=ui⋅f′(zi)⏟sigmoid 的导数⋅xj⏟线性部分\frac{\partial S}{\partial w_{ij}} = u_i \cdot \underbrace{f'(z_i)}_{\text{sigmoid 的导数}} \cdot \underbrace{x_j}_{\text{线性部分}}

拆开看每一步。对 wijw_{ij} 这个具体某一个权重求导的时候,我们只关心它的 ii——uiu_i 乘上 aia_i,其他节点的部分并不关心。把 uiu_i 提出去,剩下对 aia_i 求 wijw_{ij} 的导数:又是一个复合函数求导——aia_i 对 ziz_i 求导(这就简单了,是 sigmoid 的导数,一减它本身),ziz_i 再对 wijw_{ij} 求导(线性的,把 ziz_i 解析成 w⋅x+bw \cdot x + b 的形式,得出 xjx_j)。所以最后:xjx_j 乘上一个 sigmoid 的导数,前面再乘 uiu_i——你会发现这个也并不特别难算。

那我们想再多算一层呢?比如输出这部分叫 YY:Y=f(S)Y = f(S)——就像 ai=f(zi)a_i = f(z_i) 一样,我们就多了一层而已。把所有前面的结果,都再乘上一个 YY 对 ff 的导数(又一个 sigmoid 的门),就可以把 yy 这一层的误差,直接导到 wijw_{ij} 里边。所以就是这么一步一步、一层一层地往前推算。

把这套公式跑成数字(讲义设定的小网络,正好就是「2 个节点、各接 3 维输入、汇聚成一个 SS、再过一层 sigmoid」),并用数值梯度对账——把 wijw_{ij} 扰动一点点,看 yy 实际变多少,与公式比一比:

import math

def sig(z):
    return 1.0 / (1.0 + math.exp(-z))

def dsig(v):
    """sigmoid 的门: 已知输出 v, 导数就是 v*(1-v)"""
    return v * (1 - v)

# 两层小网络(讲义设定): 第一层 2 个节点各接 3 维输入, 汇聚成 S, 再过一层 sigmoid 输出 y
x = [1.0, 2.0, -1.0]                        # 3 维输入
W = [[1.0, 0.0, -1.0],                      # 节点 1 的三个权重
     [0.0, 1.0,  1.0]]                      # 节点 2 的三个权重
b = [0.0, 0.0]
u = [1.0, 2.0]                              # 汇聚权重: S = u1*a1 + u2*a2

def forward(W, b, u):
    a = []
    for i in range(2):
        z = sum(W[i][j] * x[j] for j in range(3)) + b[i]
        a.append(sig(z))
    S = u[0] * a[0] + u[1] * a[1]
    y = sig(S)
    return a, S, y

a, S, y = forward(W, b, u)
print(f"前向: a = [{a[0]:.4f}, {a[1]:.4f}], S = {S:.4f}, y = {y:.4f}")
print()

# 公式: dy/dw_ij = sigma'(S) * u_i * sigma'(z_i) * x_j —— 一层层乘回去
print("公式 dy/dw_ij = sigma'(S) * u_i * sigma'(z_i) * x_j:")
for i in range(2):
    for j in range(3):
        g = dsig(y) * u[i] * dsig(a[i]) * x[j]
        print(f"  dy/dw_{i+1}{j+1} = {dsig(y):.4f} * {u[i]:.1f} * {dsig(a[i]):.4f} * {x[j]:+.1f} = {g:+.4f}")
print()

# 对账: 数值梯度(把 w_ij 扰动一点点, 看 y 实际变多少)与公式一致
eps = 1e-6
print("数值对账(扰动 eps=1e-6, 中心差分):")
for i in range(2):
    for j in range(3):
        Wp = [row[:] for row in W]
        Wp[i][j] += eps
        Wm = [row[:] for row in W]
        Wm[i][j] -= eps
        num = (forward(Wp, b, u)[2] - forward(Wm, b, u)[2]) / (2 * eps)
        g = dsig(y) * u[i] * dsig(a[i]) * x[j]
        print(f"  w_{i+1}{j+1}: 公式 {g:+.6f}  数值 {num:+.6f}")
前向: a = [0.8808, 0.7311], S = 2.3429, y = 0.9124

公式 dy/dw_ij = sigma'(S) * u_i * sigma'(z_i) * x_j:
  dy/dw_11 = 0.0800 * 1.0 * 0.1050 * +1.0 = +0.0084
  dy/dw_12 = 0.0800 * 1.0 * 0.1050 * +2.0 = +0.0168
  dy/dw_13 = 0.0800 * 1.0 * 0.1050 * -1.0 = -0.0084
  dy/dw_21 = 0.0800 * 2.0 * 0.1966 * +1.0 = +0.0314
  dy/dw_22 = 0.0800 * 2.0 * 0.1966 * +2.0 = +0.0629
  dy/dw_23 = 0.0800 * 2.0 * 0.1966 * -1.0 = -0.0314

数值对账(扰动 eps=1e-6, 中心差分):
  w_11: 公式 +0.008394  数值 +0.008394
  w_12: 公式 +0.016789  数值 +0.016789
  w_13: 公式 -0.008394  数值 -0.008394
  w_21: 公式 +0.031439  数值 +0.031439
  w_22: 公式 +0.062878  数值 +0.062878
  w_23: 公式 -0.031439  数值 -0.031439

读这张表的两层结构。第一层(公式那六行):dydw11\frac{dy}{dw_{11}} 与 dydw13\frac{dy}{dw_{13}} 只差一个符号——因为节点 1 的两个权重一个接 x1=+1x_1 = +1、一个接 x3=−1x_3 = -1,同一个节点、不同的输入,梯度只在最后乘 xjx_j 那一步分道;而 dydw22\frac{dy}{dw_{22}} 比 dydw21\frac{dy}{dw_{21}} 大一倍——输入翻倍(+1+1 对 +2+2)。第二层(对账那六行):公式的梯度与「真把权重拨动一丁点、看输出实际变多少」到小数点后六位完全一致——一步步乘回去的链,就是真导数。这就是反向传播的全部底气。

大图景:10 维空间里找最低点

总之一句话:就是一个复合函数求导。这个导数代表的是:神经网络有向量的输入、有特别多复杂的系数(权重),给它构造了一个特别高维空间的、要被优化的函数。这个函数的参数特别多——大家想想上面这个小网络里有几个参数?第一层 3×2=63 \times 2 = 6 个权重,再加 2 个偏置是 8 个;再把汇聚的 2 个 uu 算上,一共 10 个参数——就是 10 维空间里边构造的一个需要被优化的函数。你要在 10 个维度上不断地变化,就很难找,而且也不知道怎么搜索——我们需要知道每一个方向上的梯度。这 10 维空间的每一个轴都往某一个方向驱动、往某一个方向移动,大家不断地迭代和循环,找到这 10 维空间里函数的最优点,或者说它的最低点——loss function 最低的情况。所以这个呢,就是我们在做的:反向传播。

📝 动手练一练

  1. 乘法节点的对称性:算例二里,∂f∂w0=−0.2\frac{\partial f}{\partial w_0} = -0.2 而 ∂f∂x0=0.4\frac{\partial f}{\partial x_0} = 0.4;∂f∂w1=−0.4\frac{\partial f}{\partial w_1} = -0.4 而 ∂f∂x1=−0.6\frac{\partial f}{\partial x_1} = -0.6。观察每一对 ww 与 xx 的梯度,说说它们之间「互为对方的什么」?再回答:训练的时候我们更新哪一个、不更新哪一个,为什么?
👉 点击查看参考答案

w0x0w_0 x_0 这个乘法节点上,对 w0w_0 求导得 x0x_0、对 x0x_0 求导得 w0w_0——两侧的梯度互为对方的输入值:∂f∂w0=0.2×x0\frac{\partial f}{\partial w_0} = 0.2 \times x_0、∂f∂x0=0.2×w0\frac{\partial f}{\partial x_0} = 0.2 \times w_0。训练时只更新 ww(参数):ww 是模型的旋钮,梯度告诉每个旋钮往哪边拧;xx 是给定的数据,不是旋钮——数据不动,模型动。

  1. gate 捷径与「两端变小」:用 σ′(z)=σ(z)(1−σ(z))\sigma'(z) = \sigma(z)(1-\sigma(z)) 手算三个导数:σ′(0)\sigma'(0)、σ′(2)\sigma'(2)、σ′(−4)\sigma'(-4)(提示:σ(0)=0.5\sigma(0) = 0.5,σ(2)≈0.88\sigma(2) \approx 0.88,σ(−4)≈0.018\sigma(-4) \approx 0.018)。观察输出越极端、导数越小的规律,然后回答:为什么 rectifier 没有这个烦恼?
👉 点击查看参考答案

σ′(0)=0.5×0.5=0.25\sigma'(0) = 0.5 \times 0.5 = 0.25;σ′(2)≈0.88×0.12=0.105\sigma'(2) \approx 0.88 \times 0.12 = 0.105;σ′(−4)≈0.018×0.982≈0.018\sigma'(-4) \approx 0.018 \times 0.982 \approx 0.018。输出越靠近 0 或 1(输入跑向两端),导数越小——梯度在两端「熄火」,这就是讲师说的 sigmoid 最大的问题(层数一多,小梯度连乘会越乘越小)。rectifier 在大于 0 的区域导数恒等于 1——不管跑多远都不减小,所以没有这个烦恼。

本章小结

反向传播 = 复合函数求导的链式法则在神经网络上的落地。本节走了三步。第一步,摆正问题:损失 EE 最小 → 要每个参数 wijw_{ij} 的梯度 → 梯度下降;ww 是大矩阵但更新的是每个分量,「统一表述」而已。第二步,两个算例:f=(x+y)⋅zf = (x+y) \cdot z 建立直觉(绿色正向算值、红色反向算导数,中间变量把复合拆成每步可导的小段);sigmoid 神经元是主戏——从 f/f=1f/f = 1 出发,−0.53-0.53(过 1/x1/x)、−0.53-0.53(过 +1+1)、−0.2-0.2(过 exp)、0.20.2(过 ×(−1)\times(-1))、加法扇出,最后每个权重拿到自己的梯度(w0w_0: −0.2-0.2、x0x_0: 0.40.4、w1w_1: −0.4-0.4、x1x_1: −0.6-0.6)——每一步都是「本段导数 × 上游传来的导数」;拿 α=0.1\alpha = 0.1 一乘一加,w0w_0 从 2 更新到 1.98。第三步,工程化与图景:sigmoid 整块是一个「门」,σ′=σ(1−σ)\sigma' = \sigma(1-\sigma),一步出导数、与逐步链一模一样;两层网络 ∂y∂wij=σ′(S)⋅ui⋅σ′(zi)⋅xj\frac{\partial y}{\partial w_{ij}} = \sigma'(S) \cdot u_i \cdot \sigma'(z_i) \cdot x_j,数值对账六位小数一致;而真正的 EE 只是在链上多乘一层(yy 是常数)。收尾的大图景:10 个参数就是 10 维空间——每个方向都要有梯度,迭代循环,找 loss function 的最低点。

📋 行动清单

  • 口述原则:对着算例二的七个数字(1 → −0.53 → −0.53 → −0.2 → 0.2 → 各权重梯度),不看笔记把「每一步 = 本段导数 × 上游传来的导数」讲一遍。
  • 手推两层公式:在纸上推出 ∂y∂wij=σ′(S)⋅ui⋅σ′(zi)⋅xj\frac{\partial y}{\partial w_{ij}} = \sigma'(S) \cdot u_i \cdot \sigma'(z_i) \cdot x_j,再与代码块的六行数字对账。
  • 数参数:给自己画一个「3 维输入、2 节点、1 汇聚输出」的小网络,数一数一共几个参数(10 个)——那就是几维的优化空间。
  • 对照激活函数:用练一练第 2 题的三个数(0.25、0.105、0.018)说清「sigmoid 两端梯度熄火」,再讲 rectifier 为什么不怕(导数恒 1)。

—— 小象教研组

配套资料 · 直接下载

课件、代码、数据集等配套资料全部免费,点击即可直接下载,无需分享或解锁。

  • 第19章课件:神经网络(BP/CNN/RNN)
    下载