动手学深度学习 7.4 动量法

前言

从零开始学习ai文章系列计划是个人在《动手学深度学习》和《磨菇书》两本书的学习中的个人笔记,文章也会以课本中的章节分开,即每个章节一片笔记。我会尽量的把主要内容以及遇到的难点进行记录与解决,如果哪里有错误的欢迎指正。或者不清晰的可以直接查看原文部分。

《动手学深度学习》原文(课本):https://tangshusen.me/Dive-into-DL-PyTorch/#/

《动手学深度学习》代码:https://github.com/ShusenTang/Dive-into-DL-PyTorch

(由于有时候公式太多,可能会直接贴图片)


在7.2节(梯度下降和随机梯度下降)中我们提到,目标函数有关自变量的梯度代表了目标函数在自变量当前位置下降最快的方向。因此,梯度下降也叫作最陡下降(steepest descent)。

1. 梯度下降的问题

让我们考虑一个输入和输出分别为二维向量 \(x=[x_1,x_2]^⊤\) 和标量的目标函数 \(f(x)=0.1x_1^2+2x_2^2\) 。与7.2节中不同,这里将 \(x_1^2\) 系数从 \(1\) 减小到了 \(0.1\) 。下面实现基于这个目标函数的梯度下降,并演示使用学习率为 \(0.4\) 时自变量的迭代轨迹。

1
2
3
4
5
6
7
8
9
eta = 0.4 # 学习率

def f_2d(x1, x2):
return 0.1 * x1 ** 2 + 2 * x2 ** 2

def gd_2d(x1, x2, s1, s2):
return (x1 - eta * 0.2 * x1, x2 - eta * 4 * x2, 0, 0)

d2l.show_trace_2d(f_2d, d2l.train_2d(gd_2d))

可以看到,同一位置上,目标函数在竖直方向(x2轴方向)比在水平方向(x1轴方向)的斜率的绝对值更大。因此,给定学习率,梯度下降迭代自变量时会使自变量在竖直方向比在水平方向移动幅度更大。

那么,我们需要一个较小的学习率从而避免自变量在竖直方向上越过目标函数最优解。然而,这会造成自变量在水平方向上朝最优解移动变慢

下面我们试着将学习率调得稍大一点,此时自变量在竖直方向不断越过最优解并逐渐发散。

1
2
eta = 0.6
d2l.show_trace_2d(f_2d, d2l.train_2d(gd_2d))

2. 动量法

动量法的提出是为了解决梯度下降的上述问题。

由于小批量随机梯度下降比梯度下降更为广义,本章后续讨论将沿用7.3节(小批量随机梯度下降)中时间步t 的小批量随机梯度 \(g_t\) 的定义。

设时间步\(t\) 的自变量为 \(x_t\) ,学习率为 \(\eta_t\) 。 在时间步0,动量法创建速度变量 \(v_0\) ,并将其元素初始化成0。在时间步 \(t>0\) ,动量法对每次迭代的步骤做如下修改: \[ \begin{align*} v_t &\leftarrow \gamma v_{t-1} + \eta_t g_t \\ x_t &\leftarrow x_{t-1} - v_t \end{align*} \] 其中,动量超参数 \(\gamma\) 满足 \(0≤\gamma<1\) 。当 \(γ=0\) 时,动量法等价于小批量随机梯度下降。

在解释动量法的数学原理前,让我们先从实验中观察梯度下降在使用动量法后的迭代轨迹。

1
2
3
4
5
6
7
def momentum_2d(x1, x2, v1, v2):
v1 = gamma * v1 + eta * 0.2 * x1
v2 = gamma * v2 + eta * 4 * x2
return x1 - v1, x2 - v2, v1, v2

eta, gamma = 0.4, 0.5
d2l.show_trace_2d(f_2d, d2l.train_2d(momentum_2d))

可以看到使用较小的学习率 \(η=0.4\) 和动量超参数 \(γ=0.5\) 时,动量法在竖直方向上的移动更加平滑,且在水平方向上更快逼近最优解。下面使用较大的学习率η=0.6,此时自变量也不再发散。

2.1 指数加权移动平均

为了从数学上理解动量法,让我们先解释一下指数加权移动平均(exponentially weighted moving average)。

给定超参数 \(0≤γ<1\) ,当前时间步t 的变量 \(y_t\) 是上一时间步 \(t−1\) 的变量 \(y_{t−1}\) 和当前时间步另一变量 \(x_t\) 的线性组合:

我们可以对 \(y_t\) 展开:

\(n= \frac {1}{1-\gamma}\) ,那么 \((1- \frac {1}{n} )^n = \gamma^{1/(1-\gamma)}\) ,这里我们\(n\) 构建了 \(γ\) 的指数形式\(\gamma^{1/(1-\gamma)}\)

因为 \(\lim_{n \to \infty} (1- \frac {1}{n})^n = exp(-1) \approx 0.3679\)

所以当 $ {} $ 时,因为 \(n= \frac {1}{1-\gamma}\) ,所以 $ {n } $ ,此时 $(1- )^n = ^{1/(1-)} = exp(-1) $ ,

\(\gamma = 0.95\) 时,\(\gamma^{1/(1-\gamma)} = 0.95^{20} \approx exp(-1)\)

如果\(exp(−1)\) 当作一个比较小的数,我们可以在近似中忽略所有含 \(\gamma^{1/(1-\gamma)}\) 和比 \(\gamma^{1/(1-\gamma)}\) 更高阶的系数的项

那么把 \(\gamma = 0.95\) 带入到原本的 \(y_t\)\[ y_t \approx 0.05 \sum_{i=0}^{19}{0.95^ix_{t-i}} \]

2.2 由指数加权移动平均理解动量法

现在,我们对动量法的速度变量做变形: \[ \eta_tg_t = (1-\gamma)(\frac{\eta_t}{1-\gamma}g_t) \]

由指数加权移动平均的形式可得,速度变量 \(v_t\) 实际上对序列
\[ \frac {\eta_{t-i}}{(1-\gamma)}g_{t-i}: i=0,...,\frac{1}{(1-\gamma)}-1 \] 做了指数加权移动平均。

为什么是以 \(\frac{1}{(1-\gamma)}-1\) 结尾呢,因为

我们2.1的原理里面学过

然后我们说过可以在近似中忽略所有含 \(\gamma^{1/(1-\gamma)}\) 和比 \(\gamma^{1/(1-\gamma)}\) 更高阶的系数的项

那么我们单独看指数项,其实就是 0 到 \(\frac{1}{1-\gamma}\) 个项的 \(\gamma\) 指数,由于我们忽略了第 \(\frac{1}{1-\gamma}\) 项,即 \(\gamma^{1/(1-\gamma)}\) ,所以一共就是 0 到 \(\frac{1}{1-\gamma}-1\) 个项。

换句话说,相比于小批量随机梯度下降,动量法在每个时间步的自变量更新量近似于将最近 \(\frac{1}{1-\gamma}\) 个时间步的普通更新量(即学习率乘以梯度)做了指数加权移动平均后再除以1−γ。就是下面这个 \[ \frac {\eta_{t}}{(1-\gamma)}g_{t} \]

我们可以看到 \(\frac {\eta_{t}}{(1-\gamma)}g_{t}\) 相当于指数加权移动平均中的 \(x_t\)

那么我们将 \(\frac {\eta_{t}}{(1-\gamma)}g_{t}\) 带入到展开式中

我们能抵消掉 \((1-\gamma)\) ,最终结果实际上就是 \(\eta_{t}g_t\) 的用权重 \(\gamma\) 做的指数加权平均

所以,在动量法中,自变量在各个方向上的移动幅度不仅取决当前梯度,还取决于过去的各个梯度在各个方向上是否一致。在本节之前示例的优化问题中,所有梯度在水平方向上为正(向右),而在竖直方向上时正(向上)时负(向下)。这样,我们就可以使用较大的学习率,从而使自变量向最优解更快移动。

3.3 小总结

我们原本的动量法公式

实际上我们展开后相当于

3. 从零开始实现

相对于小批量随机梯度下降,动量法需要对每一个自变量维护一个同它一样形状的速度变量,且超参数里多了动量超参数。实现中,我们将速度变量用更广义的状态变量states表示。

1
2
3
4
5
6
7
8
9
10
11
12
features, labels = d2l.get_data_ch7()

def init_momentum_states():
v_w = torch.zeros((features.shape[1], 1), dtype=torch.float32)
v_b = torch.zeros(1, dtype=torch.float32)
return (v_w, v_b)

def sgd_momentum(params, states, hyperparams):
for p, v in zip(params, states):
v.data = hyperparams['momentum'] * v.data + hyperparams['lr'] * p.grad.data
p.data -= v.data

我们先将动量超参数momentum设0.5,这时可以看成是特殊的小批量随机梯度下降:其小批量随机梯度为最近2个时间步2倍的小批量梯度的加权平均

1
2
d2l.train_ch7(sgd_momentum, init_momentum_states(),
{'lr': 0.02, 'momentum': 0.5}, features, labels)

将动量超参数momentum增大到0.9,这时依然可以看成是特殊的小批量随机梯度下降:其小批量随机梯度为最近10个时间步的10倍小批量梯度的加权平均。我们先保持学习率0.02不变。

1
2
d2l.train_ch7(sgd_momentum, init_momentum_states(),
{'lr': 0.02, 'momentum': 0.9}, features, labels)

可见目标函数值在后期迭代过程中的变化不够平滑。直觉上,10倍小批量梯度比2倍小批量梯度大了5倍,我们可以试着将学习率减小到原来的1/5。此时目标函数值在下降了一段时间后变化更加平滑。

1
2
d2l.train_ch7(sgd_momentum, init_momentum_states(),
{'lr': 0.004, 'momentum': 0.9}, features, labels)

小结

前面说的几倍几倍的,这里解释一下。

先回忆我们的公式

我们将权重部分提取出来,趋于无限,加和得到

那么带入0.5和0.9结果如下

以0.9为例,如果趋于无限项的时候,上图中权重和是10,那么就说明我们 \(v_t\) 里面带有10倍的 单步梯度的量级 ( 可以理解成“单步梯度” = 当前一步的小批量梯度平均值 )

因此 动量0.9 的 \(v\) 相对于 动量0.5的 \(v\) 来说,其数值大小相差了5倍。

实际上我们项是有限的

换成我们的项数,得到公式

然后带入γ = 0.5和γ = 0.9,分别得到刚才图中的中间列

数值上来说 γ=0.9的实际权重总和 仍然近似是 γ=0.5的实际权重总和 5倍。

好了,现在我们知道以下结果

动量0.9梯度 = 10倍的 单步梯度的量级

动量0.5梯度 = 2倍的 单步梯度的量级

那么在将动量从0.5修改成0.9的时候,我们应该将学习率变成原来的1/5保持在动量0.5更新时候的更新幅度(前面实验看到那个时候是比较平滑的)这样,即使动量更大,参数每次更新的实际幅度仍然和动量较小时相当,保证训练平稳

也就是说将以下公式中的 η 变成原来的1/5,那么实际上就是 v_0.9 = v_0.5 * 1/5

4. 简洁实现

在PyTorch中,只需要通过参数momentum来指定动量超参数即可使用动量法。

这里用指针的方式直接传入参数。(我们能发现实际就是原本 随机梯度下降的 torch.optim.SGD 里面在添加学习率 lr 参数的同时,将参数 momentum 也传入,就会自动切换成动量法的随机梯度下降 )