.. _sec_sgd:
随机梯度下降
============
在前面的章节中,我们一直在训练过程中使用随机梯度下降,但没有解释它为什么起作用。为了澄清这一点,我们刚在
:numref:`sec_gd`\ 中描述了梯度下降的基本原则。本节继续更详细地说明\ *随机梯度下降*\ (stochastic
gradient descent)。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
def f(x1, x2): # 目标函数
return x1 ** 2 + 2 * x2 ** 2
def f_grad(x1, x2): # 目标函数的梯度
return 2 * x1, 4 * x2
def sgd(x1, x2, s1, s2, f_grad):
g1, g2 = f_grad(x1, x2)
# 模拟有噪声的梯度
g1 += np.random.normal(0.0, 1, (1,)).item()
g2 += np.random.normal(0.0, 1, (1,)).item()
eta_t = eta * lr()
return (x1 - eta_t * g1, x2 - eta_t * g2, 0, 0)
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))
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
[07:02:45] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU
epoch 50, x1: -0.472513, x2: 0.110780
.. figure:: output_sgd_baca77_18_1.svg
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
def f(x1, x2): # 目标函数
return x1 ** 2 + 2 * x2 ** 2
def f_grad(x1, x2): # 目标函数的梯度
return 2 * x1, 4 * x2
def sgd(x1, x2, s1, s2, f_grad):
g1, g2 = f_grad(x1, x2)
# 模拟有噪声的梯度
g1 += torch.normal(0.0, 1, (1,)).item()
g2 += torch.normal(0.0, 1, (1,)).item()
eta_t = eta * lr()
return (x1 - eta_t * g1, x2 - eta_t * g2, 0, 0)
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))
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
epoch 50, x1: 0.020569, x2: 0.227895
.. figure:: output_sgd_baca77_21_1.svg
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
def f(x1, x2): # 目标函数
return x1 ** 2 + 2 * x2 ** 2
def f_grad(x1, x2): # 目标函数的梯度
return 2 * x1, 4 * x2
def sgd(x1, x2, s1, s2, f_grad):
g1, g2 = f_grad(x1, x2)
# 模拟有噪声的梯度
g1 += tf.random.normal([1], 0.0, 1)
g2 += tf.random.normal([1], 0.0, 1)
eta_t = eta * lr()
return (x1 - eta_t * g1, x2 - eta_t * g2, 0, 0)
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))
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
epoch 50, x1: -0.051145, x2: -0.028135
.. figure:: output_sgd_baca77_24_1.svg
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
def f(x1, x2): # 目标函数
return x1 ** 2 + 2 * x2 ** 2
def f_grad(x1, x2): # 目标函数的梯度
return 2 * x1, 4 * x2
def sgd(x1, x2, s1, s2, f_grad):
g1, g2 = f_grad(x1, x2)
# 模拟有噪声的梯度
g1 += paddle.normal(0.0, 1, (1,)).item()
g2 += paddle.normal(0.0, 1, (1,)).item()
eta_t = eta * lr()
return (x1 - eta_t * g1, x2 - eta_t * g2, 0, 0)
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))
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
epoch 50, x1: 0.075573, x2: 0.082887
.. figure:: output_sgd_baca77_27_1.svg
.. raw:: html
.. raw:: html
正如我们所看到的,随机梯度下降中变量的轨迹比我们在
:numref:`sec_gd`\ 中观察到的梯度下降中观察到的轨迹嘈杂得多。这是由于梯度的随机性质。也就是说,即使我们接近最小值,我们仍然受到通过\ :math:`\eta \nabla f_i(\mathbf{x})`\ 的瞬间梯度所注入的不确定性的影响。即使经过50次迭代,质量仍然不那么好。更糟糕的是,经过额外的步骤,它不会得到改善。这给我们留下了唯一的选择:改变学习率\ :math:`\eta`\ 。但是,如果我们选择的学习率太小,我们一开始就不会取得任何有意义的进展。另一方面,如果我们选择的学习率太大,我们将无法获得一个好的解决方案,如上所示。解决这些相互冲突的目标的唯一方法是在优化过程中\ *动态*\ 降低学习率。
这也是在\ ``sgd``\ 步长函数中添加学习率函数\ ``lr``\ 的原因。在上面的示例中,学习率调度的任何功能都处于休眠状态,因为我们将相关的\ ``lr``\ 函数设置为常量。
动态学习率
----------
用与时间相关的学习率\ :math:`\eta(t)`\ 取代\ :math:`\eta`\ 增加了控制优化算法收敛的复杂性。特别是,我们需要弄清\ :math:`\eta`\ 的衰减速度。如果太快,我们将过早停止优化。如果减少的太慢,我们会在优化上浪费太多时间。以下是随着时间推移调整\ :math:`\eta`\ 时使用的一些基本策略(稍后我们将讨论更高级的策略):
.. math::
\begin{aligned}
\eta(t) & = \eta_i \text{ if } t_i \leq t \leq t_{i+1} && \text{分段常数} \\
\eta(t) & = \eta_0 \cdot e^{-\lambda t} && \text{指数衰减} \\
\eta(t) & = \eta_0 \cdot (\beta t + 1)^{-\alpha} && \text{多项式衰减}
\end{aligned}
在第一个\ *分段常数*\ (piecewise
constant)场景中,我们会降低学习率,例如,每当优化进度停顿时。这是训练深度网络的常见策略。或者,我们可以通过\ *指数衰减*\ (exponential
decay)来更积极地减低它。不幸的是,这往往会导致算法收敛之前过早停止。一个受欢迎的选择是\ :math:`\alpha = 0.5`\ 的\ *多项式衰减*\ (polynomial
decay)。在凸优化的情况下,有许多证据表明这种速率表现良好。
让我们看看指数衰减在实践中是什么样子。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
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))
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
epoch 1000, x1: -0.820457, x2: 0.004701
.. figure:: output_sgd_baca77_33_1.svg
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
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))
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
epoch 1000, x1: -0.998659, x2: 0.023408
.. figure:: output_sgd_baca77_36_1.svg
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
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))
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
epoch 1000, x1: -0.749494, x2: -0.058892
.. figure:: output_sgd_baca77_39_1.svg
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
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))
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
epoch 1000, x1: -0.774738, x2: -0.041723
.. figure:: output_sgd_baca77_42_1.svg
.. raw:: html
.. raw:: html
正如预期的那样,参数的方差大大减少。但是,这是以未能收敛到最优解\ :math:`\mathbf{x} = (0, 0)`\ 为代价的。即使经过1000个迭代步骤,我们仍然离最优解很远。事实上,该算法根本无法收敛。另一方面,如果我们使用多项式衰减,其中学习率随迭代次数的平方根倒数衰减,那么仅在50次迭代之后,收敛就会更好。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
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))
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
epoch 50, x1: 0.025029, x2: 0.115820
.. figure:: output_sgd_baca77_48_1.svg
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
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))
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
epoch 50, x1: -0.174174, x2: -0.000615
.. figure:: output_sgd_baca77_51_1.svg
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
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))
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
epoch 50, x1: -0.061881, x2: -0.026958
.. figure:: output_sgd_baca77_54_1.svg
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
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))
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
epoch 50, x1: 0.023912, x2: -0.036424
.. figure:: output_sgd_baca77_57_1.svg
.. raw:: html
.. raw:: html
关于如何设置学习率,还有更多的选择。例如,我们可以从较小的学习率开始,然后使其迅速上涨,再让它降低,尽管这会更慢。我们甚至可以在较小和较大的学习率之间切换。现在,让我们专注于可以进行全面理论分析的学习率计划,即凸环境下的学习率。对一般的非凸问题,很难获得有意义的收敛保证,因为总的来说,最大限度地减少非线性非凸问题是NP困难的。有关的研究调查,请参阅例如2015年Tibshirani的优秀\ `讲义笔记