- 人工智能
- 深度学习
- 机器学习
- 教程
【免费下载链接】d2l-zh
《动手学深度学习》:面向中文读者、能运行、可讨论。中英文版被70多个国家的500多所大学用于教学。
导读:本文对应《动手学深度学习》(d2l-zh)优化算法章节中的梯度下降(gradient descent)指南,讲解梯度下降的基本原理、一维与多元实现、学习率的影响、局部最小值问题,以及牛顿法、收敛性分析、预处理与线搜索等自适应方法。读完本文,你将掌握梯度下降的数学推导与可运行代码(支持 MXNet / PyTorch / TensorFlow / Paddle 四种框架),并理解学习率过大导致发散、预处理按坐标缩放等关键现象,为后续学习随机梯度下降(SGD)与动量、Adam 等高级优化算法打下基础。
一维梯度下降
尽管梯度下降很少被直接用于深度学习,但理解它是理解下一节随机梯度下降算法的关键。例如,由于学习率过大,优化问题可能会发散,这种现象早在梯度下降中就已经出现;同样地,预处理(preconditioning)是梯度下降中的一种常用技术,还会被沿用到更高级的算法中。因此我们从最简单的一维梯度下降开始。
考虑一类连续可微实值函数 $f: \mathbb{R} \rightarrow \mathbb{R}$,利用泰勒展开可以得到:
$$f(x + \epsilon) = f(x) + \epsilon f'(x) + \mathcal{O}(\epsilon^2).$$
即在一阶近似中,$f(x+\epsilon)$ 可通过 $x$ 处的函数值 $f(x)$ 和一阶导数 $f'(x)$ 得出。我们可以假设在负梯度方向上移动的 $\epsilon$ 会减少 $f$。为了简单起见,选择固定步长 $\eta > 0$,然后取 $\epsilon = -\eta f'(x)$,将其代入泰勒展开式得到:
$$f(x - \eta f'(x)) = f(x) - \eta f'^2(x) + \mathcal{O}(\eta^2 f'^2(x)).$$
如果其导数 $f'(x) \neq 0$ 没有消失,就能继续推进,这是因为 $\eta f'^2(x)>0$。此外,我们总是可以令 $\eta$ 小到足以使高阶项变得不相关,因此有:
$$f(x - \eta f'(x)) \lessapprox f(x).$$
这意味着,如果我们使用
$$x \leftarrow x - \eta f'(x)$$
来迭代 $x$,函数 $f(x)$ 的值可能会下降。因此,在梯度下降中,我们首先选择初始值 $x$ 和常数 $\eta > 0$,然后使用它们连续迭代 $x$,直到停止条件达成。例如,当梯度 $|f'(x)|$ 的幅度足够小,或迭代次数达到某个值时停止。
代码实现:以 $f(x)=x^2$ 为例
为了演示实现,选用目标函数 $f(x)=x^2$。尽管已知 $x=0$ 时 $f(x)$ 取得最小值,我们仍使用这个简单的函数来观察 $x$ 的变化。定义目标函数与其梯度:
def f(x): # 目标函数 return x ** 2 def f_grad(x): # 目标函数的梯度(导数) return 2 * x本仓库的d2l包为每种深度学习框架提供了同名接口,上述代码在 MXNet、PyTorch、TensorFlow、Paddle 四个后端下均可运行,导入方式分别为:
from d2l import mxnet as d2l # 或 torch / tensorflow / paddle接下来使用 $x=10$ 作为初始值,并假设 $\eta=0.2$,用梯度下降迭代 $x$ 共 10 次。可以看到 $x$ 的值最终将接近最优解 $x=0$:
def gd(eta, f_grad): x = 10.0 results = [x] for i in range(10): x -= eta * f_grad(x) results.append(float(x)) print(f'epoch 10, x: {x:f}') return results results = gd(0.2, f_grad)对 $x$ 优化的过程可以绘制如下。show_trace函数以results的最大绝对值为界生成函数曲线,并把每次迭代后的点用-o折线标记出来:
def show_trace(results, f): n = max(abs(min(results)), abs(max(results))) f_line = d2l.arange(-n, n, 0.01) d2l.set_figsize() d2l.plot([f_line, results], [[f(x) for x in f_line], [ f(x) for x in results]], 'x', 'f(x)', fmts=['-', '-o']) show_trace(results, f)其中d2l.set_figsize、d2l.plot等绘图工具在仓库中均有统一实现,例如 d2l/torch.py 中set_figsize默认设置figure.figsize = (3.5, 2.5)并切换到 SVG 输出,plot负责归一化输入、设置坐标轴并逐条绘制曲线。d2l.arange、d2l.meshgrid、d2l.cos、d2l.sin等符号则在 d2l/torch.py 中直接映射到torch的对应函数,从而保证四个后端代码风格一致。
学习率:过大发散,过小停滞
学习率 $\eta$ 由算法设计者设置,它决定目标函数能否收敛到局部最小值,以及何时收敛到最小值。
- 学习率太小:将导致 $x$ 更新非常缓慢,需要更多迭代。例如同一优化问题中取 $\eta = 0.05$,即使经过 10 步,仍离最优解很远:
show_trace(gd(0.05, f_grad), f)- 学习率过大:$\left|\eta f'(x)\right|$ 对一阶泰勒展开式可能太大,即展开式中的高阶项 $\mathcal{O}(\eta^2 f'^2(x))$ 变得显著,此时 $x$ 的迭代不能保证降低 $f(x)$ 的值。例如 $\eta=1.1$ 时,$x$ 超出最优解 $x=0$ 并逐渐发散:
show_trace(gd(1.1, f_grad), f)这正是原文档强调的"学习率调节"难题的雏形:过大则发散、过小则停滞。后面会看到,二阶方法、预处理与线搜索都是为了自动或半自动地解决这一难题。
局部最小值:非凸函数的陷阱
为了演示非凸函数上的梯度下降,考虑函数 $f(x) = x \cdot \cos(cx)$,其中 $c$ 为某常数。这个函数有无穷多个局部最小值,根据选择的学习率与问题的条件状况,最终可能只得到众多解中的一个。下面的例子说明(不切实际的)高学习率如何导致较差的局部最小值:
c = d2l.tensor(0.15 * np.pi) def f(x): # 目标函数 return x * d2l.cos(c * x) def f_grad(x): # 目标函数的梯度 return d2l.cos(c * x) - c * x * d2l.sin(c * x) show_trace(gd(2, f_grad), f)当 $\eta = 2$ 时,由于该函数存在无数波峰波谷,一阶迭代很容易越过极小值点,最终收敛到一个"较差的"局部最小值而非全局最优点。这提示我们:梯度下降只能保证下降到某个局部极小,无法保证全局最优,这也是深度学习中几乎所有优化问题都是非凸的背景下,需要更精巧算法的原因之一。
多元梯度下降
现在考虑 $\mathbf{x} = [x_1, x_2, \ldots, x_d]^\top$ 的情形,即目标函数 $f: \mathbb{R}^d \to \mathbb{R}$ 将向量映射成标量。相应地,梯度也是多元的,它是一个由 $d$ 个偏导数组成的向量:
$$\nabla f(\mathbf{x}) = \bigg[\frac{\partial f(\mathbf{x})}{\partial x_1}, \frac{\partial f(\mathbf{x})}{\partial x_2}, \ldots, \frac{\partial f(\mathbf{x})}{\partial x_d}\bigg]^\top.$$
梯度中的每个偏导数元素 $\partial f(\mathbf{x})/\partial x_i$ 代表了 $f$ 在 $\mathbf{x}$ 处关于输入 $x_i$ 的变化率。对多元函数使用相应的泰勒近似:
$$f(\mathbf{x} + \boldsymbol{\epsilon}) = f(\mathbf{x}) + \mathbf{\boldsymbol{\epsilon}}^\top \nabla f(\mathbf{x}) + \mathcal{O}(|\boldsymbol{\epsilon}|^2).$$
换句话说,在 $\boldsymbol{\epsilon}$ 的二阶项内,最陡下降的方向由负梯度 $-\nabla f(\mathbf{x})$ 给出。选择合适的学习率 $\eta > 0$,便得到典型的梯度下降算法:
$$\mathbf{x} \leftarrow \mathbf{x} - \eta \nabla f(\mathbf{x}).$$
二维示例与轨迹可视化
构造目标函数 $f(\mathbf{x})=x_1^2+2x_2^2$,梯度为 $\nabla f(\mathbf{x}) = [2x_1, 4x_2]^\top$,从初始位置 $[-5, -2]$ 出发观察 $\mathbf{x}$ 的轨迹。原文档提供了两个辅助函数:
train_2d:接收自定义 trainer,从x1=-5, x2=-2, s1=0, s2=0出发迭代 20 步(s1、s2是预留的内部状态变量,供后续动量、AdaGrad 等算法使用),打印每一步结果并返回轨迹;show_trace_2d:在 $x_1 \in [-5.5, 1.0]$、$x_2 \in [-3.0, 1.0]$ 的网格上绘制等值线(contour),并用橙色折线标出迭代轨迹。
这两个函数在仓库 d2l/torch.py 中有正式实现(标注为#@save,会随d2l包导出),并在 d2l/mxnet.py、d2l/tensorflow.py、d2l/paddle.py 中保持一致。完整演示代码如下:
def train_2d(trainer, steps=20, f_grad=None): # 定制训练器优化 2D 目标函数 x1, x2, s1, s2 = -5, -2, 0, 0 results = [(x1, x2)] for i in range(steps): if f_grad: x1, x2, s1, s2 = trainer(x1, x2, s1, s2, f_grad) else: x1, x2, s1, s2 = trainer(x1, x2, s1, s2) results.append((x1, x2)) print(f'epoch {i + 1}, x1: {float(x1):f}, x2: {float(x2):f}') return results def show_trace_2d(f, results): # 显示优化过程中 2D 变量的轨迹 d2l.set_figsize() d2l.plt.plot(*zip(*results), '-o', color='#ff7f0e') x1, x2 = d2l.meshgrid(d2l.arange(-5.5, 1.0, 0.1), d2l.arange(-3.0, 1.0, 0.1), indexing='ij') d2l.plt.contour(x1, x2, f(x1, x2), colors='#1f77b4') d2l.plt.xlabel('x1') d2l.plt.ylabel('x2') def f_2d(x1, x2): # 目标函数 return x1 ** 2 + 2 * x2 ** 2 def f_2d_grad(x1, x2): # 目标函数的梯度 return (2 * x1, 4 * x2) def gd_2d(x1, x2, s1, s2, f_grad): g1, g2 = f_grad(x1, x2) return (x1 - eta * g1, x2 - eta * g2, 0, 0) eta = 0.1 show_trace_2d(f_2d, train_2d(gd_2d, f_grad=f_2d_grad))学习率 $\eta = 0.1$ 时,经过 20 步,$\mathbf{x}$ 的值接近其位于 $[0, 0]$ 的最小值。注意 $x_2$ 方向因系数 2 梯度更强,收敛更快;整体进展相当顺利,但相当缓慢——这正是"每个坐标共享同一个学习率"带来的局限。
自适应方法:二阶信息与曲率
正如上文所见,选择"恰到好处"的学习率 $\eta$ 是很棘手的:选得太小没有进展,选得太大解会振荡甚至发散。那么能否自动确定 $\eta$,或者完全不必选择学习率?
除了考虑目标函数的值和梯度,还考虑其曲率的二阶方法可以帮我们解决这个问题。虽然由于计算代价的原因,这些方法不能直接应用于深度学习,但它们为设计高级优化算法提供了有用的直觉——后续的 AdaGrad、RMSProp、Adam 等算法正是模拟了其中许多理想特性。
牛顿法
回顾 $f: \mathbb{R}^d \rightarrow \mathbb{R}$ 的泰勒展开式,可以写出二阶项:
$$f(\mathbf{x} + \boldsymbol{\epsilon}) = f(\mathbf{x}) + \boldsymbol{\epsilon}^\top \nabla f(\mathbf{x}) + \frac{1}{2} \boldsymbol{\epsilon}^\top \nabla^2 f(\mathbf{x}) \boldsymbol{\epsilon} + \mathcal{O}(|\boldsymbol{\epsilon}|^3).$$
定义 $\mathbf{H} \stackrel{\mathrm{def}}{=} \nabla^2 f(\mathbf{x})$ 为 $f$ 的Hessian 矩阵,是一个 $d \times d$ 矩阵。当 $d$ 很小且问题简单时 $\mathbf{H}$ 很容易计算;但对于深度神经网络,$\mathcal{O}(d^2)$ 个条目的存储代价很高,通过反向传播计算也代价昂贵。先忽略这些考量,看看能得到什么算法。
由于 $f$ 的最小值满足 $\nabla f = 0$,对二阶泰勒展开关于 $\boldsymbol{\epsilon}$ 求导并忽略高阶项:
$$\nabla f(\mathbf{x}) + \mathbf{H} \boldsymbol{\epsilon} = 0 \text{ and hence } \boldsymbol{\epsilon} = -\mathbf{H}^{-1} \nabla f(\mathbf{x}).$$
也就是说,作为优化问题的一部分,需要对 Hessian 矩阵 $\mathbf{H}$ 求逆,更新规则为:
$$\mathbf{x} \leftarrow \mathbf{x} - \eta \mathbf{H}^{-1} \nabla f(\mathbf{x}).$$
理想情况:对于 $f(x) = \frac{1}{2} x^2$,有 $\nabla f(x) = x$ 和 $\mathbf{H} = 1$,因此对任何 $x$ 可得 $\epsilon = -x$——单步即完美收敛,无须任何调整。这里比较幸运:泰勒展开式是精确的,因为 $f(x+\epsilon)= \frac{1}{2} x^2 + \epsilon x + \frac{1}{2} \epsilon^2$。
凸函数验证:对凸双曲余弦函数 $f(x) = \cosh(cx)$($c$ 为常数,这里取 $c=0.5$),经过几次迭代即可到达 $x=0$ 处的全局最小值:
c = d2l.tensor(0.5) def f(x): # 目标函数 return d2l.cosh(c * x) def f_grad(x): # 目标函数的梯度 return c * d2l.sinh(c * x) def f_hess(x): # 目标函数的 Hessian return c**2 * d2l.cosh(c * x) def newton(eta=1): x = 10.0 results = [x] for i in range(10): x -= eta * f_grad(x) / f_hess(x) results.append(float(x)) print('epoch 10, x:', x) return results show_trace(newton(), f)非凸函数的致命缺陷:考虑 $f(x) = x \cos(c x)$。在牛顿法中我们最终要除以 Hessian,这意味着如果二阶导数是负数,更新方向可能指向 $f$ 值增大的方向:
c = d2l.tensor(0.15 * np.pi) def f(x): # 目标函数 return x * d2l.cos(c * x) def f_grad(x): # 目标函数的梯度 return d2l.cos(c * x) - c * x * d2l.sin(c * x) def f_hess(x): # 目标函数的 Hessian return -2 * c * d2l.sin(c * x) - x * c**2 * d2l.cos(c * x) show_trace(newton(), f) # eta=1,结果"惊人地错误"如何修正?一种方法是取 Hessian 的绝对值来修正符号问题;另一种策略是重新引入学习率。这看似违背初衷,但并不完全如此——拥有二阶信息可以使我们在曲率较大时保持谨慎(缩小步长),而在目标函数较平坦时采用较大的步长。例如将学习率降为 $\eta = 0.5$:
show_trace(newton(0.5), f)此时算法变得相当高效。这直观地解释了为什么实践中会发展出"带阻尼的牛顿法"以及 AdaGrad / RMSProp 等按历史梯度规模自适应缩放步长的算法。
收敛性分析
对牛顿法的收敛速度,原文档给出了严格的一维证明。设目标函数 $f$ 凸、三次可微且二阶导数不为零($f'' > 0$);多元证明是一维论证的直接推广,因此省略。记 $x^{(k)}$ 为第 $k$ 次迭代的值,$e^{(k)} \stackrel{\mathrm{def}}{=} x^{(k)} - x^$ 为第 $k$ 次迭代与最优解的距离。由泰勒展开,条件 $f'(x^) = 0$ 可写为:
$$0 = f'(x^{(k)} - e^{(k)}) = f'(x^{(k)}) - e^{(k)} f''(x^{(k)}) + \frac{1}{2} (e^{(k)})^2 f'''(\xi^{(k)}),$$
对某个 $\xi^{(k)} \in [x^{(k)} - e^{(k)}, x^{(k)}]$ 成立。除以 $f''(x^{(k)})$ 得到:
$$e^{(k)} - \frac{f'(x^{(k)})}{f''(x^{(k)})} = \frac{1}{2} (e^{(k)})^2 \frac{f'''(\xi^{(k)})}{f''(x^{(k)})}.$$
代入更新方程 $x^{(k+1)} = x^{(k)} - f'(x^{(k)}) / f''(x^{(k)})$,两边取绝对值:
$$\left|e^{(k+1)}\right| = \frac{1}{2}(e^{(k)})^2 \frac{\left|f'''(\xi^{(k)})\right|}{f''(x^{(k)})}.$$
因此,只要处于 $\left|f'''(\xi^{(k)})\right| / (2f''(x^{(k)})) \leq c$ 的有界区域,就有二次递减误差(即误差每步按平方收缩):
$$\left|e^{(k+1)}\right| \leq c (e^{(k)})^2.$$
优化研究者称这种收敛为"线性"收敛,而把 $\left|e^{(k+1)}\right| \leq \alpha \left|e^{(k)}\right|$($\alpha<1$ 常数收缩)称为"恒定"收敛速度。需要指出该分析的两点局限:其一,我们无法估计何时进入快速收敛区域,只知道一旦接近极小值,收敛将变得非常快;其二,分析要求 $f$ 在高阶导数上表现良好,即确保 $f$ 在取值变化上没有"超常"特性。
预处理(Preconditioning)
计算和存储完整 Hessian 非常昂贵,改善的方法之一是预处理:只计算 Hessian 的"对角线"项,得到更新:
$$\mathbf{x} \leftarrow \mathbf{x} - \eta \mathrm{diag}(\mathbf{H})^{-1} \nabla f(\mathbf{x}).$$
虽然这不如完整的牛顿法精确,但远比不使用要好。为什么有效?假设一个变量以毫米表示高度、另一个变量以公里表示高度,若二者的自然尺度都以米为单位,参数化就出现严重的不匹配,共享一个学习率的梯度下降会在不同坐标上表现悬殊。预处理可以消除这种尺度不匹配——梯度下降的有效预处理,相当于为 $\mathbf{x}$ 的每个坐标选择各自不同的学习率。这一思想直接推动了后续随机梯度下降优化算法的创新,例如 AdaGrad 与 RMSProp 按各坐标的历史梯度平方根缩放学习率,本质上就是对坐标尺度差异的自适应预处理。
梯度下降与线搜索
梯度下降的关键问题之一是会"冲过头"(overshoot)或"进展不足"。简单的修复办法是结合线搜索:使用 $\nabla f(\mathbf{x})$ 给出的方向,然后做二分搜索,确定使 $f(\mathbf{x} - \eta \nabla f(\mathbf{x}))$ 取最小值的学习率 $\eta$。该算法收敛迅速(分析与证明可参考凸优化经典教材,对应参考文献收录于仓库 d2l.bib 的Boyd.Vandenberghe.2004条目)。
然而对深度学习而言这不太可行:线搜索的每一步都需要在整个数据集上评估目标函数,代价过高。这解释了为什么深度学习实践中更常采用启发式的学习率调度(见 学习率调度)而非精确线搜索。
小结
- 学习率的大小很重要:学习率太大会使模型发散,学习率太小会没有进展。
- 梯度下降可能陷入局部极小值,而得不到全局最小值。
- 在高维模型中,调整学习率是很复杂的。
- 预处理有助于调节各坐标的比例(相当于逐坐标的学习率)。
- 牛顿法在凸问题中一旦开始正常工作,速度就会快得多(误差按平方收缩)。
- 对于非凸问题,不要不作任何调整就使用牛顿法(二阶导数为负时可能反向更新)。
练习与动手实验
- 用不同的学习率和目标函数进行梯度下降实验,观察收敛、振荡与发散的分界。
- 在区间 $[a, b]$ 中实现线搜索以最小化凸函数:
- 二分搜索决定选择 $[a, (a+b)/2]$ 还是 $[(a+b)/2, b]$ 时,是否需要导数?
- 该算法的收敛速度有多快?
- 实现该算法,并应用于最小化 $\log (\exp(x) + \exp(-2x -3))$。
- 设计一个定义在 $\mathbb{R}^2$ 上的目标函数,使梯度下降收敛极其缓慢。提示:不同坐标采用不同的缩放方式。
- 使用预处理实现牛顿法的轻量版本:
- 用对角 Hessian 作为预条件子;
- 使用它的绝对值而非实际(可能有符号的)值;
- 将上述方法应用于练习 3 中的问题。
- 将上述算法应用于多个目标函数(凸或非凸),观察如果把坐标旋转 45 度,收敛轨迹会发生什么变化。
延伸阅读:在本仓库中继续深入
- 本文对应的章节源码与后续内容见 chapter_optimization/index.md,其中梯度下降(
gd)位于凸优化基础(convexity)之后、随机梯度下降 sgd.md 之前——SGD 正是以本文的梯度下降原理为基石展开的。 - 微积分基础(泰勒展开、梯度、Hessian 的定义)参见 chapter_preliminaries/calculus.md。
train_2d、show_trace_2d等工具函数的正式实现位于 d2l/torch.py,并在 d2l/mxnet.py、d2l/tensorflow.py、d2l/paddle.py 中提供四框架一致的版本;绘图工具set_figsize/plot见 d2l/torch.py。- 阅读本章其余小节可依次探索小批量随机梯度下降、动量法、AdaGrad、RMSProp、AdaDelta、Adam 与学习率调度,它们都是本文梯度下降思想的直接延伸。
- 人工智能
- 深度学习
- 机器学习
- 教程
【免费下载链接】d2l-zh
《动手学深度学习》:面向中文读者、能运行、可讨论。中英文版被70多个国家的500多所大学用于教学。
相关推荐
Pandoc Markdown 读取器对 `i<j` 类尖括号文本的解析行为详解(附原生 AST 输出分析)
Pandoc Markdown 读取器对 i<j 类尖括号文本的解析行为详解(附原生 AST 输出分析) 导读 本文围绕 Pandoc 仓库中的命令行回归测试用
人工智能深度学习机器学习教程Lenis:轻量级平滑滚动库实战指南
Lenis:轻量级平滑滚动库实战指南 Lenis 是一个只有几 KB 的平滑滚动库(lenis 在拉丁语里就是"平滑"的意思)。它和 Locomotive Sc
人工智能深度学习机器学习教程GetQzonehistory 完整指南:一键导出QQ空间全部历史说说成 Excel
GetQzonehistory 完整指南:一键导出QQ空间全部历史说说成 Excel 时间一长,你很少再打开QQ空间,可当想做班级回忆册、翻一翻上学那会儿的旧说
网页爬虫数据分析
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考