深度学习:优化算法
1 梯度下降
为什么梯度下降算法可以优化目标函数?
考虑一类连续可微实值函数\(f: \mathbb{R} \rightarrow \mathbb{R}\),
利用泰勒展开,我们可以得到
这意味着,如果我们使用
来迭代\(x\),函数\(f(x)\)的值可能会下降。
因此,在梯度下降中,我们首先选择初始值\(x\)和常数\(\eta > 0\),
然后使用它们连续迭代\(x\),直到停止条件达成。
例如,当梯度\(|f'(x)|\)的幅度足够小或迭代次数达到某个值时。
📣
-
学习率的大小很重要:学习率太大会使模型发散,学习率太小会没有进展。
-
梯度下降会可能陷入局部极小值,而得不到全局最小值。
-
在高维模型中,调整学习率是很复杂的。
-
预处理有助于调节比例。
2 随机梯度下降
随机梯度下降(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})\)的无偏估计,因为
这意味着,平均而言,随机梯度是对梯度的良好估计。
3 小批量随机梯度下降
基于梯度的学习方法中遇到了两个极端情况:
- 梯度下降中使用完整数据集来计算梯度并更新参数,
- 随机梯度下降中一次处理一个训练样本来取得进展。
二者各有利弊:每当数据非常相似时,梯度下降并不是非常“数据高效”。
而由于CPU和GPU无法充分利用向量化,随机梯度下降并不特别“计算高效”。
这暗示了两者之间可能有折中方案,这便涉及到小批量随机梯度下降(minibatch gradient descent)。
每当我们执行\(\mathbf{w} \leftarrow \mathbf{w} - \eta_t \mathbf{g}_t\)时,消耗巨大。其中
我们将梯度\(\mathbf{g}_t\)替换为一个小批量而不是单个观测值
尽管在处理的样本数方面,随机梯度下降的收敛速度快于梯度下降,但与梯度下降相比,它需要更多的时间来达到同样的损失,因为逐个样本来计算梯度并不那么有效。 小批量随机梯度下降能够平衡收敛速度和计算效率。 大小为10的小批量比随机梯度下降更有效; 大小为100的小批量在运行时间上甚至优于梯度下降。
- 在小批量随机梯度下降中,我们处理通过训练数据的随机排列获得的批量数据(即每个观测值只处理一次,但按随机顺序)。
- 在训练期间降低学习率有助于训练。
- 一般来说,小批量随机梯度下降比随机梯度下降和梯度下降的速度快,收敛风险较小。
4 动量法
用泄漏平均值(leaky average)取代梯度计算:
其中\(\beta \in (0, 1)\)。
这有效地将瞬时梯度替换为多个“过去”梯度的平均值。
\(\mathbf{v}\)被称为动量(momentum),
它累加了过去的梯度。即
5 AdaGrad算法
算法原理:
def init_adagrad_states(feature_dim):
s_w = np.zeros((feature_dim, 1))
s_b = np.zeros(1)
return (s_w, s_b)
def adagrad(params, states, hyperparams):
eps = 1e-6
for p, s in zip(params, states):
s[:] += np.square(p.grad)
p[:] -= hyperparams['lr'] * p.grad / np.sqrt(s + eps)
- AdaGrad算法会在单个坐标层面动态降低学习率。
- AdaGrad算法利用梯度的大小作为调整进度速率的手段:用较小的学习率来补偿带有较大梯度的坐标。
- 在深度学习问题中,由于内存和计算限制,计算准确的二阶导数通常是不可行的。梯度可以作为一个有效的代理。
- 如果优化问题的结构相当不均匀,AdaGrad算法可以帮助缓解扭曲。
- AdaGrad算法对于稀疏特征特别有效,在此情况下由于不常出现的问题,学习率需要更慢地降低。
6 RMSprop
算法原理:
常数\(\epsilon > 0\)通常设置为\(10^{-6}\),以确保我们不会因除以零或步长过大而受到影响。
鉴于这种扩展,我们现在可以自由控制学习率\(\eta\),而不考虑基于每个坐标应用的缩放。
就泄漏平均值而言,我们可以采用与之前在动量法中适用的相同推理。
扩展\(\mathbf{s}_t\)定义可获得
def rmsprop_2d(x1, x2, s1, s2):
g1, g2, eps = 0.2 * x1, 4 * x2, 1e-6
s1 = gamma * s1 + (1 - gamma) * g1 ** 2
s2 = gamma * s2 + (1 - gamma) * g2 ** 2
x1 -= eta / math.sqrt(s1 + eps) * g1
x2 -= eta / math.sqrt(s2 + eps) * g2
return x1, x2, s1, s2
def f_2d(x1, x2):
return 0.1 * x1 ** 2 + 2 * x2 ** 2
eta, gamma = 0.4, 0.9
def init_rmsprop_states(feature_dim):
s_w = np.zeros((feature_dim, 1))
s_b = np.zeros(1)
return (s_w, s_b)
def rmsprop(params, states, hyperparams):
gamma, eps = hyperparams['gamma'], 1e-6
for p, s in zip(params, states):
s[:] = gamma * s + (1 - gamma) * np.square(p.grad)
p[:] -= hyperparams['lr'] * p.grad / np.sqrt(s + eps)
- RMSProp算法与Adagrad算法非常相似,因为两者都使用梯度的平方来缩放系数。
- RMSProp算法与动量法都使用泄漏平均值。但是,RMSProp算法使用该技术来调整按系数顺序的预处理器。
- 在实验中,学习率需要由实验者调度。
- 系数\(\gamma\)决定了在调整每坐标比例时历史记录的时长。
7 Adadelta
算法原理:
使用重新缩放的梯度\(\mathbf{g}_t'\)执行更新,即
那么,调整后的梯度\(\mathbf{g}_t'\)是什么?我们可以按如下方式计算它:
其中\(\Delta \mathbf{x}_{t-1}\)是重新缩放梯度的平方\(\mathbf{g}_t'\)的泄漏平均值。我们将\(\Delta \mathbf{x}_{0}\)初始化为\(0\),然后在每个步骤中使用\(\mathbf{g}_t'\)更新它,即
和\(\epsilon\)(例如\(10^{-5}\)这样的小值)是为了保持数字稳定性而加入的。
%matplotlib inline
from mxnet import np, npx
from d2l import mxnet as d2l
npx.set_np()
def init_adadelta_states(feature_dim):
s_w, s_b = np.zeros((feature_dim, 1)), np.zeros(1)
delta_w, delta_b = np.zeros((feature_dim, 1)), np.zeros(1)
return ((s_w, delta_w), (s_b, delta_b))
def adadelta(params, states, hyperparams):
rho, eps = hyperparams['rho'], 1e-5
for p, (s, delta) in zip(params, states):
# In-placeupdatesvia[:]
s[:] = rho * s + (1 - rho) * np.square(p.grad)
g = (np.sqrt(delta + eps) / np.sqrt(s + eps)) * p.grad
p[:] -= g
delta[:] = rho * delta + (1 - rho) * g * g
data_iter, feature_dim = d2l.get_data_ch11(batch_size=10)
d2l.train_ch11(adadelta, init_adadelta_states(feature_dim),
{'rho': 0.9}, data_iter, feature_dim);
- Adadelta没有学习率参数。相反,它使用参数本身的变化率来调整学习率。
- Adadelta需要两个状态变量来存储梯度的二阶导数和参数的变化。
- Adadelta使用泄漏的平均值来保持对适当统计数据的运行估计。
8 Adam
Adam算法的关键组成部分之一是:它使用指数加权移动平均值来估算梯度的动量和二次矩,即它使用状态变量
这里\(\beta_1\)和\(\beta_2\)是非负加权参数。
常将它们设置为\(\beta_1 = 0.9\)和\(\beta_2 = 0.999\)。
也就是说,方差估计的移动远远慢于动量估计的移动。
注意,如果我们初始化\(\mathbf{v}_0 = \mathbf{s}_0 = 0\),就会获得一个相当大的初始偏差。
我们可以通过使用\(\sum_{i=0}^t \beta^i = \frac{1 - \beta^t}{1 - \beta}\)来解决这个问题。
相应地,标准化状态变量由下式获得
有了正确的估计,我们现在可以写出更新方程。
首先,我们以非常类似于RMSProp算法的方式重新缩放梯度以获得
与RMSProp不同,我们的更新使用动量\(\hat{\mathbf{v}}_t\)而不是梯度本身。
此外,由于使用\(\frac{1}{\sqrt{\hat{\mathbf{s}}_t} + \epsilon}\)而不是\(\frac{1}{\sqrt{\hat{\mathbf{s}}_t + \epsilon}}\)进行缩放,两者会略有差异。
前者在实践中效果略好一些,因此与RMSProp算法有所区分。
通常,我们选择\(\epsilon = 10^{-6}\),这是为了在数值稳定性和逼真度之间取得良好的平衡。
最后,我们简单更新:
%matplotlib inline
from mxnet import np, npx
from d2l import mxnet as d2l
npx.set_np()
def init_adam_states(feature_dim):
v_w, v_b = np.zeros((feature_dim, 1)), np.zeros(1)
s_w, s_b = np.zeros((feature_dim, 1)), np.zeros(1)
return ((v_w, s_w), (v_b, s_b))
def adam(params, states, hyperparams):
beta1, beta2, eps = 0.9, 0.999, 1e-6
for p, (v, s) in zip(params, states):
v[:] = beta1 * v + (1 - beta1) * p.grad
s[:] = beta2 * s + (1 - beta2) * np.square(p.grad)
v_bias_corr = v / (1 - beta1 ** hyperparams['t'])
s_bias_corr = s / (1 - beta2 ** hyperparams['t'])
p[:] -= hyperparams['lr'] * v_bias_corr / (np.sqrt(s_bias_corr) + eps)
hyperparams['t'] += 1
data_iter, feature_dim = d2l.get_data_ch11(batch_size=10)
d2l.train_ch11(adam, init_adam_states(feature_dim),
{'lr': 0.01, 't': 1}, data_iter, feature_dim);
- Adam算法将许多优化算法的功能结合到了相当强大的更新规则中。
- Adam算法在RMSProp算法基础上创建的,还在小批量的随机梯度上使用EWMA。
- 在估计动量和二次矩时,Adam算法使用偏差校正来调整缓慢的启动速度。
- 对于具有显著差异的梯度,我们可能会遇到收敛性问题。我们可以通过使用更大的小批量或者切换到改进的估计值\(\mathbf{s}_t\)来修正它们。
参考文献
《动手学深度学习》