随机梯度下降 :label: 在前面的章节中,我们一直在训练过程中使用随机梯度下降,但没有解释它为什么起作用。为了澄清这一点,我们刚在 :numref: 中描述了梯度下降的基本原则。本节继续更详细地说明随机梯度下降(stochastic gradient descent)。 随机梯度更新 在深度学习中,目标函数通常是训练数据集中每个样本的损失函数的平均值。给定$n$个样本的训练数据集,我们假设$fi(\mathbf{x})$是关于索引$i$的训练样本的损失函数,其中$\mathbf{x}$是参数向量。然后我们得到目标函数 $$f(\mathbf{x}) = \frac{1}{n} \sum{i = 1}^n fi(\mathbf{x}).
🏷sec_sgd
在前面的章节中,我们一直在训练过程中使用随机梯度下降,但没有解释它为什么起作用。为了澄清这一点,我们刚在 :numref:sec_gd中描述了梯度下降的基本原则。本节继续更详细地说明随机梯度下降(stochastic gradient descent)。
%matplotlib inline from d2l import mxnet as d2l import math from mxnet import np, npx npx.set_np()
#@tab pytorch %matplotlib inline from d2l import torch as d2l import math import torch
#@tab tensorflow %matplotlib inline from d2l import tensorflow as d2l import math import tensorflow as tf
#@tab paddle %matplotlib inline from d2l import paddle as d2l import warnings warnings.filterwarnings("ignore") import math import paddle
在深度学习中,目标函数通常是训练数据集中每个样本的损失函数的平均值。给定n个样本的训练数据集,我们假设f_i(\mathbf{x})是关于索引i的训练样本的损失函数,其中\mathbf{x}是参数向量。然后我们得到目标函数
\mathbf{x}的目标函数的梯度计算为
如果使用梯度下降法,则每个自变量迭代的计算代价为\mathcal{O}(n),它随n线性增长。因此,当训练数据集较大时,每次迭代的梯度下降计算代价将较高。
随机梯度下降(SGD)可降低每次迭代时的计算代价。在随机梯度下降的每次迭代中,我们对数据样本随机均匀采样一个索引i,其中i\in\{1,\ldots, n\},并计算梯度\nabla f_i(\mathbf{x})以更新\mathbf{x}:
其中\eta是学习率。我们可以看到,每次迭代的计算代价从梯度下降的\mathcal{O}(n)降至常数\mathcal{O}(1)。此外,我们要强调,随机梯度\nabla f_i(\mathbf{x})是对完整梯度\nabla f(\mathbf{x})的无偏估计,因为
这意味着,平均而言,随机梯度是对梯度的良好估计。
现在,我们将把它与梯度下降进行比较,方法是向梯度添加均值为0、方差为1的随机噪声,以模拟随机梯度下降。
#@tab all def f(x1, x2): # 目标函数 return x1 ** 2 + 2 * x2 ** 2 def f_grad(x1, x2): # 目标函数的梯度 return 2 * x1, 4 * x2
#@tab mxnet, pytorch, paddle def sgd(x1, x2, s1, s2, f_grad): g1, g2 = f_grad(x1, x2) # 模拟有噪声的梯度 g1 += d2l.normal(0.0, 1, (1,)) g2 += d2l.normal(0.0, 1, (1,)) eta_t = eta * lr() return (x1 - eta_t * g1, x2 - eta_t * g2, 0, 0)
#@tab tensorflow def sgd(x1, x2, s1, s2, f_grad): g1, g2 = f_grad(x1, x2) # 模拟有噪声的梯度 g1 += d2l.normal([1], 0.0, 1) g2 += d2l.normal([1], 0.0, 1) eta_t = eta * lr() return (x1 - eta_t * g1, x2 - eta_t * g2, 0, 0)
#@tab all def constant_lr(): return 1 eta = 0.1 lr = constant_lr # 常数学习速度 d2l.show_trace_2d(f, d2l.train_2d(sgd, steps=50, f_grad=f_grad))
正如我们所看到的,随机梯度下降中变量的轨迹比我们在 :numref:sec_gd中观察到的梯度下降中观察到的轨迹嘈杂得多。这是由于梯度的随机性质。也就是说,即使我们接近最小值,我们仍然受到通过\eta \nabla f_i(\mathbf{x})的瞬间梯度所注入的不确定性的影响。即使经过50次迭代,质量仍然不那么好。更糟糕的是,经过额外的步骤,它不会得到改善。这给我们留下了唯一的选择:改变学习率\eta。但是,如果我们选择的学习率太小,我们一开始就不会取得任何有意义的进展。另一方面,如果我们选择的学习率太大,我们将无法获得一个好的解决方案,如上所示。解决这些相互冲突的目标的唯一方法是在优化过程中动态降低学习率。
这也是在sgd步长函数中添加学习率函数lr的原因。在上面的示例中,学习率调度的任何功能都处于休眠状态,因为我们将相关的lr函数设置为常量。
用与时间相关的学习率\eta(t)取代\eta增加了控制优化算法收敛的复杂性。特别是,我们需要弄清\eta的衰减速度。如果太快,我们将过早停止优化。如果减少的太慢,我们会在优化上浪费太多时间。以下是随着时间推移调整\eta时使用的一些基本策略(稍后我们将讨论更高级的策略):
在第一个分段常数(piecewise constant)场景中,我们会降低学习率,例如,每当优化进度停顿时。这是训练深度网络的常见策略。或者,我们可以通过指数衰减(exponential decay)来更积极地减低它。不幸的是,这往往会导致算法收敛之前过早停止。一个受欢迎的选择是\alpha = 0.5的多项式衰减(polynomial decay)。在凸优化的情况下,有许多证据表明这种速率表现良好。
让我们看看指数衰减在实践中是什么样子。
#@tab all def exponential_lr(): # 在函数外部定义,而在内部更新的全局变量 global t t += 1 return math.exp(-0.1 * t) t = 1 lr = exponential_lr d2l.show_trace_2d(f, d2l.train_2d(sgd, steps=1000, f_grad=f_grad))
正如预期的那样,参数的方差大大减少。但是,这是以未能收敛到最优解\mathbf{x} = (0, 0)为代价的。即使经过1000个迭代步骤,我们仍然离最优解很远。事实上,该算法根本无法收敛。另一方面,如果我们使用多项式衰减,其中学习率随迭代次数的平方根倒数衰减,那么仅在50次迭代之后,收敛就会更好。
#@tab all def polynomial_lr(): # 在函数外部定义,而在内部更新的全局变量 global t t += 1 return (1 + 0.1 * t) ** (-0.5) t = 1 lr = polynomial_lr d2l.show_trace_2d(f, d2l.train_2d(sgd, steps=50, f_grad=f_grad))
关于如何设置学习率,还有更多的选择。例如,我们可以从较小的学习率开始,然后使其迅速上涨,再让它降低,尽管这会更慢。我们甚至可以在较小和较大的学习率之间切换。现在,让我们专注于可以进行全面理论分析的学习率计划,即凸环境下的学习率。对一般的非凸问题,很难获得有意义的收敛保证,因为总的来说,最大限度地减少非线性非凸问题是NP困难的。有关的研究调查,请参阅例如2015年Tibshirani的优秀讲义笔记。
以下对凸目标函数的随机梯度下降的收敛性分析是可选读的,主要用于传达对问题的更多直觉。我们只限于最简单的证明之一 :cite:Nesterov.Vial.2000。存在着明显更先进的证明技术,例如,当目标函数表现特别好时。
假设所有\boldsymbol{\xi}的目标函数f(\boldsymbol{\xi}, \mathbf{x})在\mathbf{x}中都是凸的。更具体地说,我们考虑随机梯度下降更新:
其中f(\boldsymbol{\xi}_t, \mathbf{x})是训练样本f(\boldsymbol{\xi}_t, \mathbf{x})的目标函数:\boldsymbol{\xi}_t从第t步的某个分布中提取,\mathbf{x}是模型参数。用
表示期望风险,R^*表示对于\mathbf{x}的最低风险。最后让\mathbf{x}^*表示最小值(我们假设它存在于定义\mathbf{x}的域中)。在这种情况下,我们可以跟踪时间t处的当前参数\mathbf{x}_t和风险最小化器\mathbf{x}^*之间的距离,看看它是否随着时间的推移而改善:
:eqlabel:eq_sgd-xt+1-xstar
我们假设随机梯度\partial_\mathbf{x} f(\boldsymbol{\xi}_t, \mathbf{x})的L_2范数受到某个常数L的限制,因此我们有
:eqlabel:eq_sgd-L
我们最感兴趣的是\mathbf{x}_t和\mathbf{x}^*之间的距离如何变化的期望。事实上,对于任何具体的步骤序列,距离可能会增加,这取决于我们遇到的\boldsymbol{\xi}_t。因此我们需要点积的边界。因为对于任何凸函数f,所有\mathbf{x}和\mathbf{y}都满足f(\mathbf{y}) \geq f(\mathbf{x}) + \langle f'(\mathbf{x}), \mathbf{y} - \mathbf{x} \rangle,按凸性我们有
:eqlabel:eq_sgd-f-xi-xstar
将不等式 :eqref:eq_sgd-L和 :eqref:eq_sgd-f-xi-xstar代入 :eqref:eq_sgd-xt+1-xstar我们在时间t+1时获得参数之间距离的边界,如下所示:
:eqlabel:eqref_sgd-xt-diff
这意味着,只要当前损失和最优损失之间的差异超过\eta_t L^2/2,我们就会取得进展。由于这种差异必然会收敛到零,因此学习率\eta_t也需要消失。
接下来,我们根据 :eqref:eqref_sgd-xt-diff取期望。得到
最后一步是对t \in \{1, \ldots, T\}的不等式求和。在求和过程中抵消中间项,然后舍去低阶项,可以得到
:eqlabel:eq_sgd-x1-xstar
请注意,我们利用了给定的\mathbf{x}_1,因而可以去掉期望。最后定义
因为有
根据詹森不等式(令 :eqref:eq_jensens-inequality中i=t,\alpha_i = \eta_t/\sum_{t=1}^T \eta_t)和R的凸性使其满足的E[R(\mathbf{x}_t)] \geq E[R(\bar{\mathbf{x}})],因此,
将其代入不等式 :eqref:eq_sgd-x1-xstar得到边界
其中r^2 \stackrel{\mathrm{def}}{=} \|\mathbf{x}_1 - \mathbf{x}^*\|^2是初始选择参数与最终结果之间距离的边界。简而言之,收敛速度取决于随机梯度标准的限制方式(L)以及初始参数值与最优结果的距离(r)。请注意,边界由\bar{\mathbf{x}}而不是\mathbf{x}_T表示。因为\bar{\mathbf{x}}是优化路径的平滑版本。只要知道r, L和T,我们就可以选择学习率\eta = r/(L \sqrt{T})。这个就是上界rL/\sqrt{T}。也就是说,我们将按照速度\mathcal{O}(1/\sqrt{T})收敛到最优解。
到目前为止,在谈论随机梯度下降时,我们进行得有点快而松散。我们假设从分布p(x, y)中采样得到样本x_i(通常带有标签y_i),并且用它来以某种方式更新模型参数。特别是,对于有限的样本数量,我们仅仅讨论了由某些允许我们在其上执行随机梯度下降的函数\delta_{x_i}和\delta_{y_i}组成的离散分布p(x, y) = \frac{1}{n} \sum_{i=1}^n \delta_{x_i}(x) \delta_{y_i}(y)。
但是,这不是我们真正做的。在本节的简单示例中,我们只是将噪声添加到其他非随机梯度上,也就是说,我们假装有成对的(x_i, y_i)。事实证明,这种做法在这里是合理的(有关详细讨论,请参阅练习)。更麻烦的是,在以前的所有讨论中,我们显然没有这样做。相反,我们遍历了所有实例恰好一次。要了解为什么这更可取,可以反向考虑一下,即我们有替换地从离散分布中采样n个观测值。随机选择一个元素i的概率是1/n。因此选择它至少一次就是
类似的推理表明,挑选一些样本(即训练示例)恰好一次的概率是
这导致与无替换采样相比,方差增加并且数据效率降低。因此,在实践中我们执行后者(这是本书中的默认选择)。最后一点注意,重复采用训练数据集的时候,会以不同的随机顺序遍历它。
:begin_tab:mxnet
Discussions
:end_tab:
:begin_tab:pytorch
Discussions
:end_tab:
:begin_tab:tensorflow
Discussions
:end_tab:
:begin_tab:paddle
Discussions
:end_tab: