跳至内容
3/30第 3 章,共 30 章

下坡:Gradient Descent,以及所有人都跳过的两个步骤

计算学习率的精确上限,再看一次对 3,600 个方向的暴力搜索如何在无人告知的情况下重新发现梯度。

本页内容

上一章以一条山谷结束。

这不是比喻意义上的山谷:它是一条真实的曲线,把损失画在单个参数上,先下降再回升。而曲线下方的那个损失并不是因为整洁才被选中——它是从关于测量噪声的陈述中推导出来的,平方误差是作为结果而不是惯例出现的。

所以我们有了一片带底部的地形,也有理由相信底部就是正确的位置。但我们还没有到达那里的方法。

本章会构建一个方法,而它就是训练本课程其余所有模型的算法——每一个,无一例外,包括那些拥有数千亿参数的模型。它大约二十行就能写完。难点不在这二十行里,而在几乎所有解释都会跳过的两个问题里:

  • 为什么是减号。 更新会减去梯度。每篇教程都会这么写;却很少有人说明为什么梯度是向上的方向,而这正是让减号不只是信仰行为的唯一事实。
  • 步子能迈多大。 “太大会发散,太小会很慢”是真的,也没什么用。有一个精确的数字,它可以从损失中计算出来,本章会算两次——一次针对玩具抛物线,一次针对真实数据。

为了让本章自洽,先重述一下:数据仍是 Chapter 1 传送带上的八个零件,但我们换一个问题。不是接受或拒绝——那个问题稍后会回来——而是根据零件宽度预测重量

belt.pyPYTHON
import numpy as np

WIDTH  = np.array([18.0, 19.5, 20.2, 21.0, 24.0, 25.5, 23.0, 26.0])
WEIGHT = np.array([47.0, 52.0, 49.0, 55.0, 61.0, 66.0, 70.0, 58.0])

x = WIDTH  - WIDTH.mean()      # 22.15 mm
y = WEIGHT - WEIGHT.mean()     # 57.25 g

这些测量值已经居中,和 Chapter 1 完全一样,而且这个原因在本章结束前会连本带利地回来。模型是一条直线,y^=ax+b\hat{y} = a x + b,损失是上一章推导出的均方误差:

L(a,b)=1ni=1n(axi+byi)2L(a, b) = \frac{1}{n} \sum_{i=1}^{n} \left(a x_i + b - y_i\right)^2

两个参数。为什么不直接试很多值?我们真的试一下——从 a=0a = 055,从 b=5b = -555,步长 0.010.01

TEXT
grid 501 x 1001 = 501,501 evaluations in 3.67 s
  best found: a = 2.1000, b = -0.0000, L = 24.592450

半百万次评估,只是把两个数字确定到小数点后两位——而且那一秒是某台机器上的挂钟时间,所以重跑可能在三到六秒之间;可复现的是评估次数和最小值。到本章末尾,Gradient Descent 会在八步内得到四位小数,在三十六步内得到完整 float64 答案。

但速度不是核心论点,接下来这一点决定了整门课程。Grid search 对 PP 个参数、每个参数 kk 个取值,需要 kPk^P 次评估。每个轴一千个取值时:

模型参数grid 评估次数
这条直线210610^{6}
Chapter 5 的 XOR 网络9102710^{27}
一个小型多层网络20,0001060,00010^{60{,}000}

第三行不是一个很大的数字,而是一个失去意义的数字——可观测宇宙中的原子数量大约是 108010^{80}。随着模型变大,搜索不是变慢;它是停止存在。后面的一切,都是因为这张表而存在。

先把 b=0b = 0 固定住,这样就只剩一个参数和一条曲线,也就是上一章留给你的那张图。在曲线上取一点 a=1a = 1,然后问:如果我把 aa 轻推一个小量 hh,损失每单位轻推会移动多少?

L(a+h)L(a)h\frac{L(a + h) - L(a)}{h}

这个比值就是上升量除以前进量——穿过曲线上两点的直线斜率。当 hh 变小,两点滑向彼此,这条线就变成切线。它的斜率就是导数 L(a)L'(a):损失相对于 aa 的单位变化而变化的速率。它不是对某个东西的近似,也不是一个无穷小的量。它是普通比值的极限。

值得跑一下,因为数字会说出定义本身没有说的东西:

slope.pyPYTHON
def loss1(a):
    return np.mean((a * x - y) ** 2)

for h in [1.0, 1e-2, 1e-4, 1e-6, 1e-8, 1e-10, 1e-12, 1e-14]:
    q = (loss1(1.0 + h) - loss1(1.0)) / h
    print(f"h = {h:<8.0e}   slope estimate = {q:.10f}   error = {abs(q + 16.385):.3e}")
TEXT
h = 1e+00      slope estimate = -8.9400000000    error = 7.445e+00
h = 1e-02      slope estimate = -16.3105500000   error = 7.445e-02
h = 1e-04      slope estimate = -16.3842555001   error = 7.445e-04
h = 1e-06      slope estimate = -16.3849925556   error = 7.444e-06
h = 1e-08      slope estimate = -16.3850003787   error = 3.787e-07
h = 1e-10      slope estimate = -16.3850444324   error = 4.443e-05
h = 1e-12      slope estimate = -16.3851154866   error = 1.155e-04
h = 1e-14      slope estimate = -17.0530256582   error = 6.680e-01

这里发生了两件事,而且两件都承重。

误差不是含糊地正比于 hh——它精确地是 7.445h7.445\,hhh 除以一百,误差也除以一百,每次都精确到四位有效数字。这个常数不是装饰:它是损失的二阶导数的一半,也是两节之后一个想法的首次出现——曲线在某点附近看起来像一条直线,再加上一个与 h2h^2 成正比的修正项。

然后模式断裂了。 低于 h=108h = 10^{-8} 时,估计反而变得更差,到了 101410^{-14},第二位数字就错了。数学上什么都没发生;是上一章的浮点盒子在起作用。L(a+h)L(a+h)L(a)L(a) 的前十位数字相同,把它们相减会毁掉这些数字,再除以一个极小的数会放大剩下的残骸。存在一个最佳 hh——这里大约是 10810^{-8},差不多是机器 epsilon 的平方根——再小不是更谨慎,而是更不谨慎。记住这一点;本章末尾有个函数依赖它。

精确斜率来自微积分而不是测量,是 16.385-16.385。所以我们可以停止测量,开始推导。

这是整门课程其余部分建立其上的想法,清楚地说一次。

复合两个函数,就是把一个函数喂给另一个函数:(fg)(x)=f(g(x))(f \circ g)(x) = f(g(x))。仅此而已。

深度网络不是一个复合。它就是一个复合。一层是一个函数;堆叠层就是复合它们;“深度”就是这条链中函数的数量。Chapter 5 构建网络时,它构建的是 f4f3f2f1f_4 \circ f_3 \circ f_2 \circ f_1,除此之外没有别的。这意味着,对我们来说,微积分中最重要的一条规则,就是那条对复合求导的规则:

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

速率会相乘。 如果 gg 的变化速度是 xx 的三倍,而 ff 的变化速度是 gg 的两倍,那么 ff 的变化速度就是 xx 的六倍。这就是全部内容,也解释了为什么一个信号反向穿过十层时会乘上十个数字——这也是 Chapter 6 会花一节讨论当这些数字都略小于一时会发生什么的原因。

把它用到我们的损失上。写出残差 ri=axi+byir_i = a x_i + b - y_i,于是 L=1nri2L = \frac{1}{n}\sum r_i^2。每个 rir_i 都通过内部函数 axia x_i 依赖于 aa,而该内部函数的导数是 xix_i。逐项应用链式法则:

La=1ni2rixi,Lb=1ni2ri1\frac{\partial L}{\partial a} = \frac{1}{n}\sum_i 2 r_i \cdot x_i, \qquad \frac{\partial L}{\partial b} = \frac{1}{n}\sum_i 2 r_i \cdot 1

这些花体 \partial 符号表示偏导数:对一个变量求导,并把其他所有变量都视为常数。这里没有发生新事情——它仍是前面的同一个极限,只是沿着一个轴取。把这些偏导收集成一个向量,你就得到了梯度

L=(La, Lb)\nabla L = \left( \frac{\partial L}{\partial a},\ \frac{\partial L}{\partial b} \right)

在点 (a,b)=(1,4)(a, b) = (1, 4),这个向量是 (16.385, 8.0)(-16.385,\ 8.0)。两个数字。问题是它们意味着什么,而这就是所有人都会跳过的第一步。

梯度是一个由沿坐标轴的斜率组成的向量。我们只证明了这一点。把它们组装成一个向量后,会得到某个特定方向,这并不显然——也不应该显然。

所以先定义我们真正想要的东西。选一个单位向量 u\mathbf{u},也就是一个方向。方向导数就是当你沿那个方向走时,损失变化的速率:

DuL=limh0L(θ+hu)L(θ)hD_{\mathbf{u}} L = \lim_{h \to 0} \frac{L(\boldsymbol{\theta} + h\mathbf{u}) - L(\boldsymbol{\theta})}{h}

链式法则把它变成可计算的东西。沿 u\mathbf{u} 行走时,aau1u_1 的速率变化,bbu2u_2 的速率变化,而这些贡献会相加:

DuL=Lau1+Lbu2=LuD_{\mathbf{u}} L = \frac{\partial L}{\partial a} u_1 + \frac{\partial L}{\partial b} u_2 = \nabla L \cdot \mathbf{u}

任意方向上的变化速率,都是梯度与该方向的 dot product。现在是关键结论,一行几何就够了。用两个向量之间的角 ϕ\phi 写出 dot product,

Lu=Lucosϕ=Lcosϕ\nabla L \cdot \mathbf{u} = \lVert \nabla L \rVert \, \lVert \mathbf{u} \rVert \cos\phi = \lVert \nabla L \rVert \cos\phi

因为 u\mathbf{u} 的长度是 1。你唯一能控制的是 cosϕ\cos\phi,它在 ϕ=0\phi = 0 最大,在转半圈也就是 ϕ=180\phi = 180 度时最小。所以:

  • 最陡上升沿着 L\nabla L 本身,且那里的斜率精确等于 L\lVert \nabla L \rVert
  • 最陡下降沿着 L-\nabla L,且那里的斜率是 L-\lVert \nabla L \rVert
  • 垂直于梯度时,损失完全不变。这就是为什么等高线图中的线会与梯度成直角相交。

这就是减号。它不是惯例,不是某个人选择的符号翻转:最快下降的方向是负梯度,是因为 cosϕ\cos\phi 在转半圈时最小,除此之外没有别的原因。

既然这是关于所有方向的主张,那就拿所有方向来测试。采样 3,600 个方向,每十分之一度一个,并通过轻推来测量每一个:

directions.pyPYTHON
theta = np.array([1.0, 4.0])
g = grad(theta)
print("gradient      ", g)
print("its length    ", np.linalg.norm(g))
print("its angle     ", np.degrees(np.arctan2(g[1], g[0])) % 360, "degrees")

best = max(
    ((loss(theta + 1e-6 * u) - loss(theta - 1e-6 * u)) / 2e-6, np.degrees(ang))
    for ang, u in (
        (a, np.array([np.cos(a), np.sin(a)])) for a in np.arange(3600) * 2 * np.pi / 3600
    )
)
print("steepest slope", best[0], "at", best[1], "degrees")
TEXT
gradient       [-16.385   8.   ]
its length     18.23371122399386
its angle      153.97598928042032 degrees
steepest slope 18.233709624837502 at 154.0 degrees

一个完全不知道梯度的搜索,在 3,600 个方向上找到了 154.0 度处的最陡上升——也就是梯度自己的方向,误差只在搜索的 0.1 度分辨率内。它在那里找到的斜率 18.2337,也与梯度长度在六位数字上相符。这个定理不是关于梯度含义的故事;它是一个可测量的事实,而这就是测量结果。

现在是第二个被跳过的步骤。我们知道哪个方向向下。但这并不意味着沿那个方向走会降低损失,因为“向下”说的是无穷小轻推,而一步并不是无穷小。

桥梁是线性化。在某点附近,一个光滑函数等于它的切线再加上一个修正项:

L(θ+δ)=L(θ)+Lδ+O(δ2)L(\boldsymbol{\theta} + \boldsymbol{\delta}) = L(\boldsymbol{\theta}) + \nabla L \cdot \boldsymbol{\delta} + O(\lVert\boldsymbol{\delta}\rVert^2)

这是一阶 Taylor 展开。被丢弃的 O(δ2)O(\lVert\boldsymbol{\delta}\rVert^2) 是曲率——也就是让斜率表中的估计恰好错 7.445h7.445\,h 的同一个项。放入我们打算迈出的步子,δ=ηL\boldsymbol{\delta} = -\eta \nabla L

L(θηL)L(θ)ηL2L(\boldsymbol{\theta} - \eta \nabla L) \approx L(\boldsymbol{\theta}) - \eta \lVert \nabla L \rVert^2

损失会下降 ηL2\eta \lVert \nabla L \rVert^2。这里的每一部分都是非负的,所以承诺是真实的——只要 η\eta 足够小,因为被忽略的项会像 η2\eta^2 一样增长,并最终吃掉它。这就是全部理论。下面先是承诺被兑现,然后是承诺被打破:

TEXT
eta = 0.2       promised    66.49364500   delivered   -16.01619240   ratio -0.240868
eta = 0.1       promised    33.24682250   delivered    12.61936315   ratio  0.379566
eta = 0.01      promised     3.32468225   delivered     3.11840766   ratio  0.937957
eta = 0.001     promised     0.33246822   delivered     0.33040548   ratio  0.993796
eta = 0.0001    promised     0.03324682   delivered     0.03322620   ratio  0.999380
eta = 1e-05     promised     0.00332468   delivered     0.00332448   ratio  0.999938

从底部往上读。随着 η\eta 变小,实际下降量会收敛到承诺的下降量——比值 0.99938,然后 0.99994——这就是 Taylor 定理正确的样子。从顶部往下读,在 η=0.2\eta = 0.2 时,实际“下降量”是负十六。这一步沿着下坡走,损失却上升了。

所以更新规则是

θθηL(θ)\boldsymbol{\theta} \leftarrow \boldsymbol{\theta} - \eta \nabla L(\boldsymbol{\theta})

而它带着一个没人说明的条件:η\eta 必须足够小。到底相对于什么足够小,就是下一节。

从最简单的山谷开始,f(x)=x2f(x) = x^2,其中 f(x)=2xf'(x) = 2x。Gradient Descent 的一步是

xxη2x=x(12η)x \leftarrow x - \eta \cdot 2x = x\,(1 - 2\eta)

位置在每一步都会乘以 (12η)(1 - 2\eta)。这是一个等比数列,而等比数列只有一条规则:当乘数的绝对值小于 1 时它会收缩,否则会增长。所以 12η<1\lvert 1 - 2\eta \rvert < 1,也就是 0<η<10 < \eta < 1

边界精确地在 η=1\eta = 1 不是“大约 1”,也不是“1 通常太大”。在 η=1\eta = 1 时,乘数是 1-1,点会永远在 xxx-x 之间来回弹跳,既不接近也不逃离。低于它,收敛;高于它,发散。区间在 η=0.5\eta = 0.5 处再次分裂,那里乘数改变符号:低于它时接近是单调的,高于它时点会越过谷底并在两侧交替,而恰好等于 0.50.5 时乘数为 0,一步就落在最小值上。

四种状态,来自四行代数。自己去跨过这些边界:

14 步后结束于 x = -0.0836。

以表格查看数据
步骤xf(x)
0⁨-1.9000⁩⁨3.6100⁩
1⁨-1.5200⁩⁨2.3104⁩
2⁨-1.2160⁩⁨1.4787⁩
3⁨-0.9728⁩⁨0.9463⁩
4⁨-0.7782⁩⁨0.6057⁩
5⁨-0.6226⁩⁨0.3876⁩
6⁨-0.4981⁩⁨0.2481⁩
7⁨-0.3985⁩⁨0.1588⁩
8⁨-0.3188⁩⁨0.1016⁩
9⁨-0.2550⁩⁨0.0650⁩
10⁨-0.2040⁩⁨0.0416⁩
11⁨-0.1632⁩⁨0.0266⁩
12⁨-0.1306⁩⁨0.0170⁩
13⁨-0.1045⁩⁨0.0109⁩
14⁨-0.0836⁩⁨0.0070⁩
梯度下降,交互式

x=1.9x = -1.9 开始,学习率为 0.1,十四步后结束在 0.0836-0.0836。把学习率推到 0.5,第一步就落在谷底。推到 0.9,它会结束在和 0.1 相同的 0.0836-0.0836——距离相同,风格相反,因为两者的 12η\lvert 1 - 2\eta \rvert 都是 0.8——但它是通过在山谷两侧之字形跳动到达那里,而不是沿一侧走下去。

然后是有趣的那个:

14 步后结束于 x = -1.9000。

以表格查看数据
步骤xf(x)
0⁨-1.9000⁩⁨3.6100⁩
1⁨1.9000⁩⁨3.6100⁩
2⁨-1.9000⁩⁨3.6100⁩
3⁨1.9000⁩⁨3.6100⁩
4⁨-1.9000⁩⁨3.6100⁩
5⁨1.9000⁩⁨3.6100⁩
6⁨-1.9000⁩⁨3.6100⁩
7⁨1.9000⁩⁨3.6100⁩
8⁨-1.9000⁩⁨3.6100⁩
9⁨1.9000⁩⁨3.6100⁩
10⁨-1.9000⁩⁨3.6100⁩
11⁨1.9000⁩⁨3.6100⁩
12⁨-1.9000⁩⁨3.6100⁩
13⁨1.9000⁩⁨3.6100⁩
14⁨-1.9000⁩⁨3.6100⁩
梯度下降,交互式

正好在边界上。学习率为 1,十四步后结束在 1.9-1.9:精确回到起点,除了弹跳什么也没做。只要再高一点,弹跳就会增长而不是保持;在 1.2 时,四步后就冲出图表。过大的学习率不是慢慢收敛。它不会收敛。

现在是一般规则,它也从同一个论证中掉出来。乘数 12η1 - 2\eta 实际上是 1ηf1 - \eta f'',而在最小值附近,多参数损失的每个方向都有一个这样的数——也就是二阶导数矩阵的特征值。每个方向都必须同时稳定,所以上限由最大的那个决定:

η<2λmax\eta < \frac{2}{\lambda_{\max}}

对于 f(x)=x2f(x) = x^2f=2f'' = 2,上限是 1,也就是我们刚刚推导出的结果。对于我们的传送带,二阶导数矩阵是 2nAA\frac{2}{n} A^{\top} A,其中 AA 是输入的两列矩阵,它的特征值是 2 和 14.89,所以上限是 2/14.89=0.134322 / 14.89 = 0.13432。这是一个带有五位有效数字的预测。测试一下:

TEXT
lr=0.1343    -> L =      24.5924
lr=0.13431   -> L =      24.5924
lr=0.13432   -> L =       4707.8       BLEW UP
lr=0.13433   -> L =  4.00452e+16       BLEW UP
lr=0.1344    -> L = 1.18229e+107       BLEW UP

一行线性代数和十万次 for 循环迭代,在五位小数上吻合。

这就是 Chapter 1 回来的地方。 上面的一切都使用了居中后的测量值。在原始毫米和克上运行完全相同的代码,特征值会从 2 和 14.89 变成 0.0298 和 998.1。上限从 0.134 崩塌到 0.002004——同样精确,在 lr=0.002003 收敛,在 lr=0.002004 爆炸。

比上限更糟的是特征值之间的比值。条件数衡量山谷离圆形有多远:一条又长又窄的沟会迫使学习率小到能应付陡峭的墙壁,然后沟底也只能以同样的爬行速度前进。我们的条件数从居中后的 7.44 变成原始数据的 33,452。在每个版本能采用的最佳学习率下:

特征条件数最佳学习率达到距离最优值 1% 以内所需步数
居中7.440.118410
原始毫米和克33,4520.002003779,513

同样的数据,同样的代码,最后得到同样的答案——却要多做八千倍的工作,只因为没人减去均值。在 Chapter 1 中,同样的遗漏让感知机的 epoch 数多了六千倍,而那里的诊断是几何的:数据漂浮在离原点很远的位置。这里是同一种几何,只是披上了优化的外衣,也因此输入归一化不是卫生建议,而是算术。1

上面没有任何东西需要库。下面就是整个优化器。

descent.pyPYTHON
def loss(theta):
    a, b = theta
    return np.mean((a * x + b - y) ** 2)


def grad(theta):
    a, b = theta
    residual = a * x + b - y
    return np.array([np.mean(2 * residual * x), np.mean(2 * residual)])


def descend(theta, lr, steps):
    theta = np.array(theta, dtype=float)
    for _ in range(steps):
        theta = theta - lr * grad(theta)   
    return theta


theta = descend([0.0, 0.0], lr=0.05, steps=60)
print(theta, loss(theta))
TEXT
[ 2.10040296e+00 -2.76445533e-15] 24.592448791134984

这八个点的闭式最小二乘答案是 a=2.100403a = 2.100403b=0b = 0,损失为 24.59244924.592449。这个循环在不知道闭式解存在的情况下找到了八位有效数字——这很重要,因为从 Chapter 5 开始就不会再有闭式解了。

轨迹如下,因为看它移动才是重点:

TEXT
   0 a=0.000000 b=0.000000 L=57.437500
   1 a=1.563750 b=0.000000 L=26.736582
   2 a=1.963288 b=-0.000000 L=24.732418
   5 a=2.098116 b=-0.000000 L=24.592488
  10 a=2.100400 b=-0.000000 L=24.592449
  60 a=2.100403 b=-0.000000 L=24.592449

大部分距离在前两步就走完了,因为当你离谷底最远时梯度最大,靠近时梯度会变小。Gradient Descent 会在最小值附近自动减速。这是一个特性,而在 Chapter 6 中,它也会成为一个问题。

到目前为止的论证有一个漏洞。当 L=0\nabla L = \mathbf{0} 时,步骤会停止,而我们一直把那里叫作“最小值”。梯度为零的点是临界点,而成为最小值只是成为临界点的方式之一:

  • 局部最小值:每个方向都是上坡,但它可能不是所有这类点中最低的;
  • 局部最大值:每个方向都是下坡;
  • 鞍点:某些方向上坡,另一些方向下坡。曲面 f(x,y)=x2y2f(x,y) = x^2 - y^2f=(2x,2y)\nabla f = (2x, -2y),它在原点为零,而原点处函数沿 xx 轴是最小值,同时沿 yy 轴是最大值。

Gradient Descent 无法区分这些情况,因为它永远只看梯度,而三者的梯度都为零。

我们的直线只有一个临界点,而且它就是答案——线性模型上的平方误差损失是凸的,是一个单一的碗,在它上面做下降不可能找不到全局最小值。这个性质不会在本课程后面继续保留。神经网络的损失不是凸的,从 Chapter 5 开始,“那个最小值”不再是存在的东西:会有很多个,深浅不同,而你得到哪一个取决于从哪里开始。这句话到此为止,因为理论很大,而实践后果很小。

你可以在一条曲线上看到完整后果。取 f(x)=x44x22+x10f(x) = \tfrac{x^4}{4} - \tfrac{x^2}{2} + \tfrac{x}{10},它有两个深度不同的山谷:

TEXT
   x =  -1.046681   f(x) =  -0.352386   minimum
   x =   0.101031   f(x) =   0.005026   maximum
   x =   0.945649   f(x) =  -0.152639   minimum

40 步后结束于 x = 0.9456。

以表格查看数据
步骤xf(x)
0⁨0.1100⁩⁨0.0050⁩
1⁨0.1122⁩⁨0.0050⁩
2⁨0.1149⁩⁨0.0049⁩
3⁨0.1182⁩⁨0.0049⁩
4⁨0.1223⁩⁨0.0048⁩
5⁨0.1275⁩⁨0.0047⁩
6⁨0.1338⁩⁨0.0045⁩
7⁨0.1416⁩⁨0.0042⁩
8⁨0.1513⁩⁨0.0038⁩
9⁨0.1633⁩⁨0.0032⁩
10⁨0.1781⁩⁨0.0022⁩
11⁨0.1962⁩⁨0.0007⁩
12⁨0.2183⁩⁨-0.0014⁩
13⁨0.2453⁩⁨-0.0046⁩
14⁨0.2779⁩⁨-0.0093⁩
15⁨0.3170⁩⁨-0.0160⁩
16⁨0.3633⁩⁨-0.0253⁩
17⁨0.4172⁩⁨-0.0377⁩
18⁨0.4783⁩⁨-0.0535⁩
19⁨0.5455⁩⁨-0.0721⁩
20⁨0.6163⁩⁨-0.0922⁩
21⁨0.6869⁩⁨-0.1116⁩
22⁨0.7526⁩⁨-0.1277⁩
23⁨0.8092⁩⁨-0.1393⁩
24⁨0.8540⁩⁨-0.1463⁩
25⁨0.8868⁩⁨-0.1499⁩
26⁨0.9091⁩⁨-0.1516⁩
27⁨0.9236⁩⁨-0.1522⁩
28⁨0.9325⁩⁨-0.1525⁩
29⁨0.9379⁩⁨-0.1526⁩
30⁨0.9411⁩⁨-0.1526⁩
31⁨0.9430⁩⁨-0.1526⁩
32⁨0.9441⁩⁨-0.1526⁩
33⁨0.9448⁩⁨-0.1526⁩
34⁨0.9451⁩⁨-0.1526⁩
35⁨0.9454⁩⁨-0.1526⁩
36⁨0.9455⁩⁨-0.1526⁩
37⁨0.9455⁩⁨-0.1526⁩
38⁨0.9456⁩⁨-0.1526⁩
39⁨0.9456⁩⁨-0.1526⁩
40⁨0.9456⁩⁨-0.1526⁩
梯度下降,交互式

x=0.11x = 0.11 出发走四十步,稳定在 0.94560.9456——两个山谷中较浅的那个。现在把起点向左挪一个刻度,到 0.100.10。同样的学习率,同样的四十步,它改为稳定在 1.0461-1.0461,那里的损失低 0.199747。分水岭是 0.1010310.101031 处的隆起,而两个答案之间的全部差异,只是你碰巧从它的哪一侧开始。

落入浅谷会让损失差 56.7%,而算法无从得知,因为在山谷内部,每个方向都是上坡。Gradient Descent 没有对此的修复,也不会有。实践中存在的,是一个发现:它远没有这张图暗示的那么重要——在真实网络的极高维空间里,大多数临界点最终是鞍点而不是陷阱,2 Chapter 5 会测量一个小网络实际被卡住的频率。

更便宜的步子:随机、小批量、momentum

链接到此部分:更便宜的步子:随机、小批量、momentum

上面的 grad 有一件事应该让你不舒服:它每一步都对整个数据集求和。八个零件不算什么。一百万个样本,就意味着要计算一百万个梯度,才能移动一次参数。

逃脱方式是:梯度是一个平均值,而平均值可以从样本中估计。在随机抽出的一小把样本——一个 minibatch——上计算它,然后据此迈步。估计是有噪声的;它也是无偏的,而数百个便宜的带噪步骤胜过一个昂贵的精确步骤。在十万个合成零件上,如果统计的是每个样本的梯度而不是步骤数:

方法达到距离最优值 0.1% 以内所需步数每样本梯度数
full batch7700,000
minibatch of 321003,200
one example at a time17,58017,580

到达同一位置所需算术少了二百一十九倍。而极端情况——一次一个样本,也就是 Robbins 和 Monro3 最初的随机近似——并不是赢家:它比 32 个样本的 batch 差五倍,因为在会做矩阵乘法的硬件上,32 个样本几乎不比 1 个样本更贵,而噪声会随 batch size 的平方根下降。这个取舍就是为什么你以后读到的每个训练脚本里都会有一个 batch_size

Momentum 是另一个便宜修复,它正对着那条沟。在条件很差的山谷里,步子会在狭窄方向上来回之字形振荡,同时沿长方向缓慢爬行。Momentum 会保留过去梯度的运行平均,让振荡分量相互抵消,而一致分量累积起来:4

vβv+L(θ),θθηv\mathbf{v} \leftarrow \beta \mathbf{v} + \nabla L(\boldsymbol{\theta}), \qquad \boldsymbol{\theta} \leftarrow \boldsymbol{\theta} - \eta \mathbf{v}

多两行。在原始未居中的传送带上——条件数 33,452,是我们遇到的最坏情况——使用普通下降法能承受的最佳学习率:

TEXT
momentum beta=0.0   ->    79,513 steps to 1%
momentum beta=0.9   ->     1,609 steps to 1%
momentum beta=0.99  ->       461 steps to 1%

两行代码带来 172 倍提升。Chapter 6 会把它变成 Adam;机制已经在这里了。

本章中的每个梯度都是手工推导的,因此都可能是错的。修复方法就是开头的斜率表:用数值方法测量导数并比较。使用中心差分,L(θ+h)L(θh)2h\frac{L(\theta+h) - L(\theta-h)}{2h},它会抵消主要误差项,并且在相同 hh 下准确得多。

gradcheck.pyPYTHON
def numeric_grad(f, theta, h=1e-5):
    theta = np.asarray(theta, dtype=float)
    out = np.zeros_like(theta)
    for i in range(theta.size):
        bump = np.zeros_like(theta)
        bump[i] = h
        out[i] = (f(theta + bump) - f(theta - bump)) / (2 * h)     
    return out


def gradcheck(f, df, theta, h=1e-5):
    analytic = np.asarray(df(theta), dtype=float)
    numeric = numeric_grad(f, theta, h)
    return np.max(np.abs(analytic - numeric) / np.maximum(1e-8, np.abs(analytic) + np.abs(numeric)))

比较的相对形式很重要:10410^{-4} 的绝对差,对大小为 10310^{-3} 的梯度是灾难,对大小为 10610^{6} 的梯度却无关紧要。

TEXT
relative error: 1.8929136036763527e-11
with 2 dropped: 0.33333333331650744

第一行是上面手工推导的梯度。第二行是同一个函数,但某个分量漏掉了因子 2——一个单字符 typo——检查会立刻抓住它。低于大约 10710^{-7} 就是一致;高于 10410^{-4} 就是 bug。保留这个函数:Chapter 5 会用它调试一个自动微分引擎,而它也是让错误梯度能被找到的唯一原因。

本章中的一切都建立在一个从未明说的假设上:你能把 L/θ\partial L / \partial \theta 写下来。

对于两个参数的一条直线,那只是一行代数。它几乎立刻就不再是一行。让符号代数系统求一个网络的损失对单个第一层权重、在单个样本上的导数,然后数一数答案里的算术量:

网络一个偏导数中的运算数
四个 hidden units,一层40
四个 hidden units,两层301
四个 hidden units,三层1,717

第三行是一个有 57 个参数的网络——小到在 Chapter 6 里只能算脚注——而手写它在一个训练样本上的梯度,大约意味着 97,869 次运算。没有任何记号能拯救这一点。真正拯救它的是一个观察:应用在复合上的链式法则有巨大的结构,相同的中间量会反复出现,而按正确顺序计算它们,就能以大约一次前向传播的代价得到所有导数。这就是 Chapter 5。

但在那之前还有一个更小的问题,而且它马上就在等着。

我们现在有了一台机器,可以在任何可微损失上向下滚。把它指向传送带最初的问题——接受还是拒绝,目标是 1 或 0——在输出上放一个 sigmoid,让它预测概率,并最小化平方误差。它会运行。但当它错得最厉害时,它几乎不会移动,而梯度说明了原因:

输出 zz预测真值平方误差下的梯度cross-entropy 下的梯度
000.500012.5×1012.5 \times 10^{-1}5.0×1015.0 \times 10^{-1}
2-20.119211.850×1011.850 \times 10^{-1}8.808×1018.808 \times 10^{-1}
6-60.002514.921×1034.921 \times 10^{-3}9.975×1019.975 \times 10^{-1}
10-104.54×1054.54 \times 10^{-5}19.079×1059.079 \times 10^{-5}1.0001.000

一个自信而灾难性地错误的模型——在答案为 1 时预测 0.0000454——产生的平方误差梯度是 9×1059 \times 10^{-5}。它完全不知道自己有麻烦。另一列来自一个我们还没有推导的损失,报告的是 1.0:最大紧迫性,且正好出现在应得的位置。

这就引出了下一章开头的问题。上一章说,损失是关于噪声的假设,而平方误差假设的是 Gaussian noise。一个是或否的答案拥有什么噪声模型——当你对它做同样的推导时,会得到什么损失?


这个方法比这些文献都更古老:Cauchy 在 1847 年给 Académie des Sciences 的一则笔记中描述了它,作为一种通过沿平方残差之和下坡行走来求解方程组的方法。也值得和本章一起阅读的有:Sebastian Ruder 的 An overview of gradient descent optimization algorithmsarXiv:1609.04747),它用十四页易读文字覆盖了从 momentum 到 Adam;Nocedal 和 Wright 的 Numerical Optimization(第 2 版,Springer, 2006)第 3 章,其中定理 3.3 用条件数给出了二次函数上最陡下降的收敛率——这就是为什么条件决定步数的理论背景,虽然它处理的是 line search,而不是上面测量的固定步长 2/λmax2/\lambda_{\max} 上限;或者 Deisenroth, Faisal 和 Ong 的 Mathematics for Machine Learning §5.8 和 §7.1,用更少的工具讲同一片领域;Prince 的 Understanding Deep Learning §6.1 和 Goodfellow, Bengio 与 Courville 的 Deep Learning §4.3;Dive into Deep Learning §12.1–12.3,其中的 minibatch 分析有比这里篇幅允许更多的测量;以及 Géron 的 Hands-On Machine Learning(第 3 版)第 4 章,它最实用地把学习率当作需要调的东西,而不是要推导的东西。MIT 6.390 notes 把 Gradient Descent 放在分类之前,和本课程一样,理由也相同。

  1. LeCun, Y., Bottou, L., Orr, G. B. and Müller, K.-R. Efficient BackProp, in Neural Networks: Tricks of the Trade (Springer, 1998), pp. 9–50. 第 4.3 节给出了建议,第 5.1 节给出了上面细节框所用的论证:对输入进行居中和缩放会改变二阶导数矩阵的特征值,因此改变步骤数,而不仅仅是数值上的舒适度。

  2. Dauphin, Y. N., Pascanu, R., Gulcehre, C., Cho, K., Ganguli, S. and Bengio, Y. Identifying and attacking the saddle point problem in high-dimensional non-convex optimization, arXiv:1406.2572 (2014). 其论点是,在高维中,临界点压倒性地更可能是鞍点而不是局部最小值,因为一个最小值要求成千上万个方向中的每一个都同时向上弯曲。

  3. Robbins, H. and Monro, S. A Stochastic Approximation Method. Annals of Mathematical Statistics 22(3), pp. 400–407 (1951). 这篇论文确立了这样一个事实:只要步长以正确方式缩小,一个带噪的梯度估计就足够。

  4. Polyak, B. T. Some methods of speeding up the convergence of iteration methods. USSR Computational Mathematics and Mathematical Physics 4(5), pp. 1–17 (1964). 重球法,也就是上面的 momentum 更新,比 backpropagation 进入这个领域早了二十二年。

准备好让 LIA 替你选模型了吗?

所有 AI 模型都在一处——今天就免费开始。