诚信为本,市场在变,诚信永远不变...
  咨询电话:400-123-4567

行业新闻

详解 Deep Learning 的各种优化器(一)

.梯度下降是优化神经网络和许多其他机器学习算法的首选方法。本文将介绍各种基于梯度下降的优化器,如 Momentum,Adagrad 以及 Adam 的具体细节

本文将讲解以下概念:

  • Gradient Descent
  • Batch Gradient Descent
  • Stochastic Gradient Descent(SGD)
  • Min-batch Gradient Descent
  • Momentum
  • Nesterov accelerated gradient(NAG)
  • Adagrad
  • Adadelta
  • RMSprop
  • Adam
  • AdaMax
  • Nadam
  • AMSGrad

? 在微积分中,函数 f(x) 在点 x_0 上的导数定义为: f'(x_0)=\\lim\\limits_{x \	o x_0}\\frac{f(x)-f(x_0)}{x-x_0},这在几何上指的是函数 f(x) 在点 x_0 处的切线方向。为了计算某个函数 f(x) 的最值,通常都会计算它的导函数 f'(x) ,当 f'(x)=0 时得到函数的极值点( f(x)<0,\\forall x < 0f(x)>0,\\forall x>0 ) 。但是临界点并不一定是全局最大值或者全局最小值,甚至不是局部的最大值或者局部最小值(如鞍点)

从 Taylor 级数的角度来看,f(x)x_0 附近的 Taylor 级数(皮亚诺余项)是:

f(x)=f(x_0) + f^{'}(x_0)(x-x_0) + \\frac{f^"(x_0)}{2}(x-x_0)^2 + O(|x-x_0|^3)

x_0 为临界点,则其满足条件:f'(x_0)=0 。当 f^{"}(x_0) > 0 时,可以得到 x_0f(x) 的局部最小值;当 f^{"}(x_0)<0 时,可以得到 x_0f(x) 的局部最大值。而对于上面的例子 f(x)=x^3 而言,临界点 0 的二阶导数则是 f^{"}(0)=0,因此使用上面的方法则无法判断临界点 0 是否是局部极值。

? 对于多元函数 f(x)=f(x_1,...,x_n) 而言,同样可以计算它们的“导数”,也就是偏导数和梯度。梯度可以定义为:

\
abla f(x)=(\\frac{\\partial f}{\\partial x_1}(x),...,\\frac{\\partial f}{\\partial x_n}(x))

而多元函数 f(x) 在点 x_0 上的 Taylor 级数(皮亚诺余项)为:

f(x)=f(x_0) + \
abla f(x_0)(x-x_0) + \\frac{1}{2}(x-x_0)^T\\mathbf{H}(x-x_0) + O(|x-x_0|^3)

其中 \\mathbf{H} 表示黑塞矩阵(Hessian Matrix)。如果 x_0 为临界点,并且黑塞矩阵为正定矩阵时,f(x)x_0 处达到局部极小值。

\\mathbf H=\\begin{bmatrix}\\dfrac{\\partial^2 f}{\\partial x_1^2}& \\dfrac{\\partial^2 f}{\\partial x_1\\,\\partial x_2}& \\cdots & \\dfrac{\\partial^2 f}{\\partial x_1\\,\\partial x_n}\\\\[2.2ex]\\dfrac{\\partial^2 f}{\\partial x_2\\,\\partial x_1}& \\dfrac{\\partial^2 f}{\\partial x_2^2}& \\cdots & \\dfrac{\\partial^2 f}{\\partial x_2\\,\\partial x_n}\\\\[2.2ex]\\vdots & \\vdots & \\ddots & \\vdots \\\\[2.2ex]\\dfrac{\\partial^2 f}{\\partial x_n\\,\\partial x_1}& \\dfrac{\\partial^2 f}{\\partial x_n\\,\\partial x_2}& \\cdots & \\dfrac{\\partial^2 f}{\\partial x_n^2}\\end{bmatrix}

? 梯度下降法基于以下定义:如果实值函数 f(x) 在点 a 处可微且有定义,那么函数 f(x)a 点沿着梯度相反的方向 -\
abla f(a) 下降最多。

? 因而,如果 b={a}-\\gamma \
abla f({a}) 对于 \\gamma > 0\\gamma \\rightarrow 0 时成立,那么 f({a}) \\geq f({b}) 。故我们可以从函数 f 的局部极小值的初始估计 x_0 出发,并依次可得到如下序列 x_0,x_1,x_2,... 使得  x_{n+1}=x_n - \\gamma_{n}\
abla f(x_n),n \\geq 0 因此可得到 f(x_0) \\geq f(x_1) \\geq f(x_2) \\geq ... ,如果顺利的话序列(x_n)收敛到期望的局部极小值,值得注意的是迭代步长 \\gamma 并不是定值

? 上图示例展示这一过程,这里假设 f 定义在平面上,并且函数图像是一个碗形。蓝色的曲线是等高线,即函数 f 为常数的集合构成的曲线。红色的箭头指向该点梯度的反方向。(一点处的梯度方向与通过该点的等高线垂直)。沿着梯度下降方向,将最终到达碗底,即函数 f 局部极小值的点。

? 在神经网络中,梯度下降给调整网络参数提供了一种可行的方法。初始权重和偏置可解释为单个向量 \	heta_0 ,理论上迭代步骤可以用来确定模型的最佳参数 \	heta^* 。实际上我们试图最小化的函数是根据整个数据集 \\mathcal{D} 来定义的:J(\	heta) \\, \\,=\\, \\, \\frac{1}{|\\mathcal{D}|}\\, \\sum_{x\\in\\mathcal{D}}\\, f(x \\, | \\, \	heta),其中 f(x|\	heta) 表示使用参数向量 \	heta 时,数据集元素 x 的损失。

? 总的来说,梯度下降是一种通过更新模型中参数 \	heta \\in \\mathbb{R}^ d 来最小化 J(\	heta) 的方法(通过计算目标函数梯度 \
abla_\	heta J(\	heta),并反向更新参数)。学习率 \\eta 决定了我们为了达到(局部)最小数而采取的跨度大小。

? 梯度下降有 3 种变体,它们在计算目标函数的梯度所用的数据量方面有所差异。根据数据量的大小,我们在参数更新的准确性和执行时间之间进行权衡。

? Batch Gradient Descent 通过计算参数 \	heta 关于整个训练集的损失函数的梯度 \	heta=\	heta - \\eta \\cdot \
abla_\	heta J( \	heta)

? 但由于我们需要计算整个数据集来执行一次梯度更新,因此 Batch Gradient Descent 可能非常缓慢,并且对于大型数据集(大于内存大小)的训练将会变得十分棘手。此外 Batch Gradient Descent 也不允许我们在线更新模型。

for i in range(nb_epochs):
  params_grad=evaluate_gradient(loss_function, data, params)
  params=params - learning_rate * params_grad

以上为 Batch Gradient Descent 的简单代码示例。其中我们预定义 epoch 数量,首先计算参数 params 关于损失函数对整个数据集的梯度向量 params_grad (大多数深度学习库都提供了自动微分可有效计算一些参数的梯度)。然后我们以梯度相反的反向更新参数,学习率learning_rate决定了我们每次更新速度。对于 Convex Error Surface 而言 Batch Gradient Descent 可以保证收敛到全局最小值,对于 Non-convex Surface 则可以保证收敛到局部最小值。

? 与 Batch Gradient Descent 相对应的是 Sotchastic Gradient Descent ,其为每个训练样本 x^{(i)} 和标签 y^{(i)} 执行参数更新:\	heta=\	heta - \\eta \
abla_{\	heta}J(\	heta;x^{(i)};y^{(i)}) ( 文中其余部分为了简单起见,省略了参数 x^{(i:i+n)};y^{(i:i+n)}

? Batch Gradient Descent 对大型数据集有大量的冗余计算,因为它会在每个参数更新前重新计算相似的梯度。而 SGD 针对每个样本进行一次参数更新。由于 SGD 频繁执行更新,且变化很大,这导致目标函数震荡十分剧烈。(下图来源:维基百科)

当 Batch Gradient Descent 下降收敛到参数某个局部最小值时不再继续收敛。而 SGD 的不稳定性使得有收敛到更好的局部最小值的可能性( 收敛的过程也因此变得过分复杂)。已经表明,当缓慢降低学习率时,SGD 会显示与 Batch Gradient Descent 相同的收敛行为,对于非凸优化和凸优化,它们分别收敛到局部最小值和全局最小值。

for i in range(nb_epochs):
  np.random.shuffle(data)
  for example in data:
    params_grad=evaluate_gradient(loss_function, example, params)
    params=params - learning_rate * params_grad

? 上段代码只是增加了一端循环计算损失函数在每个样本上的梯度。

? Mini-batch Gradient Descent 同时兼顾了上述两种方法的优势,针对 n 个训练样本的 mini-batch? 计算损失进行参数梯度更新:\	heta=\	heta - \\eta \
abla_{\	heta}J(\	heta;x^{(i:i+n)};y^{(i:i+n)})

? 其优点在于:

  • 降低了参数更新的方差,可以更稳定地收敛(与 SGD 相比较)
  • 利用深度学习库对常见大小 min-batch 的矩阵进行高度优化的特性,可非常高效计算出其梯度

? 常见 mini-batch 大小在 50 至 256 之间,但会因不同的应用场景而有所不同。训练神经网络时,通常选择 min-batch(而当使用 mini-batch 时,通常也使用术语 SGD )。

注意:在下文的改进的SGD中,为了简单,我们省略了参数 x^{(i:i+n)};y^{(i:i+n)}

以下是大小为 50 的 mini-batch? 示例代码:

for i in range(nb_epochs):
  np.random.shuffle(data)
  for batch in get_batches(data, batch_size=50):
    params_grad=evaluate_gradient(loss_function, batch, params)
    params=params - learning_rate * params_grad

? Mini-batch Gradient Descent 虽然具有综合优势,但其并不能保证良好的收敛性,其还有一些需要解决的挑战:

  • 选择合适的学习率十分困难。学习率太小会导致收敛时间过长,而学习率过大又会阻碍收敛导致损失函数在最小值附近波动甚至发散。
  • 学习率表尝试通过例如调整训练过程中的学习率(例如退火)。即根据预定义的时间表或在各个时期之间的目标函数变化降到阈值以下时降低学习率。但是,这些计划和阈值必须预先定义,因此无法适应数据集的特征。
  • 对于参数更新,通常我们采用相同学习率用于所有参数。如果数据集稀疏且模型特性有非常不同的出现频率,我们可能并不想将所有模型特征更新到相同的程度,且对很少出现的特性执行较大的更新。
  • 另一个关键挑战在最小化高度非凸损失函数(神经网络中十分常见)如何避免陷入众多的局部最小值。Dauphin 等人认为关键困难在于并不是由于局部最小值,而是在于鞍点。这使得 SGD 很难逃脱,因为在所有维度上梯度都接近于 0 。

? 接下来,我们将讨论一些广泛使用的算法来应对上述挑战。但我们并不会讨论在实际中无法应用于高维数据集的算法,例如二阶算法(如牛顿法)。

? 我们将最小化目标函数的任务比作从坡顶到坡底的过程。SGD 难以在沟壑中有效移动,在局部上有些维度上的弯曲程度要比正确下降维度要陡峭得多,这在局部最优的情况下很常见。这些情况下,SGD 会在山坡上震荡,沿着底部最优的方向缓慢下降,如下图所示。

Momentum 通过将上一个步骤的权重更新向量乘以 momentum 参数 \\gamma 加在当前权重更新向量上达到了在相关方向上加速 SGD 并抑制振荡。

\\begin{align}\\begin{split}v_t &=\\gamma v_{t-1}+ \\eta \
abla_\	heta J( \	heta) \\\\  \	heta &=\	heta - v_t  \\end{split}\\end{align}

通常 momentum 参数 \\gamma 设置为 0.9 。与 SGD 相比较,Momentum 轨迹如下图所示:

? 本质上,当使用 Momentum 时,我们可以将该过程比作将球推下山坡的过程。球在下坡滚动时会积累动量,并且在途中越来越快(如果存在空气阻力,即 \\gamma < 1 ,直到达到最终速度)。我们的参数更新也发生了同样的事情:动量参数对于梯度相同方向进行促进,而在梯度改变方向进行抑制。最终,我们获得了更快的收敛并减少了振荡。

? 然而,从山上滚下来的球盲目地跟随斜坡是不尽如人意的。我们希望有一个更聪明达到球,能够知道在坡度变大之前放慢速度。

? NAG 是一种能够给动量上述能力的方法。我们知道我们使用动量项 \\gamma v_{t-1} 跟新参数 \	heta 。因此,计算 \	heta - \\gamma v_{t-1} 可得出参数下一个位置的近似值(还缺少梯度)。通过计算关于参数未来的近似位置的梯度,而不是关于当前的参数 \	heta 的梯度,我们可以高效的求解:

\\begin{align}\\begin{split}v_t &=\\gamma v_{t-1}+ \\eta \
abla_\	heta J( \	heta - \\gamma v_{t-1}) \\\\ \	heta &=\	heta - v_t  \\end{split}\\end{align}

? 同样,我们设置动量项 \\gamma 大约为 0.9。Momentum (下图蓝色向量)首先计算当前的梯度值,然后再更新累积梯度方向上前进,NAG 首先在先前累积梯度(棕色向量)方向上前进一大步,计算梯度值,然后做一个修正(红色向量),从而完成 NAG 更新(绿色向量)。这个具有预见性的更新防止我们前进得太快,同时增强了算法的响应能力,这一点在很多的任务中对于RNN的性能提升有着重要的意义。

? 有关 NAG 更多直观的解释,可以参考 cs231n.github.io/neural

? 现在我们能够根据损失函数的斜率调整我们的更新得以加速 SGD ,接下来我们希望根据每个参数的重要性调整我们的更新以执行更大或更小的更新。

? Adagrad 是一种基于梯度的优化算法,它具体是通过学习率适应参数:低学习率执行较小的更新(与频繁出现的特征相关联的参数),高学习率执行较大更新(与不常见特征相关联的参数)。因此,它非常适合处理稀疏数据。Dean 等人发现 Adagrad 可以极大提高 SGD 的鲁棒性,并将其用于训练 Google 的大规模神经网络,包括识别 Youtube 视频中的猫咪。此外,Pennington 等人使用 Adagrad 来训练 GloVe 词嵌入,因为不常见的词需要的更新跨度比频繁出现的词大得多。

? 之前的算法我们每次都对 \	heta 的所有参数进行了更新,对每一个 \	heta_i 都使用了相同的学习率 \\eta 。由于 Adagrad 再每个时刻 t 对每个参数 \	heta_i 使用了不同的学习率,我们首先展示 Adagrad 的每个参数更新,然后我们将其量化。为了方便表示,我们使用 g_t 表示时刻 t 处的梯度。g_{t,i} 表示目标函数在时刻 t 关于 \	heta_i 的偏导:

g_{t, i}=\
abla_\	heta J( \	heta_{t, i})

参数 \	heta_i 在时刻 t 的 SGD 更新过程变为:

\	heta_{t+1, i}=\	heta_{t, i}- \\eta \\cdot g_{t, i}

基于上述的更新规则,在 t 时刻,对于 \	heta_i 计算过的历史梯度,Adagrad 修正对每一个 \	heta_i 的学习率:

\	heta_{t+1, i}=\	heta_{t, i}- \\dfrac{\\eta}{\\sqrt{G_{t, i}+ \\epsilon}}\\cdot g_{t, i}\\\\ G_{t}^{(i,i)}=\\sum_{\	au}^t (g_{\	au}^{(i)})^2

其中,G_{t}\\in \\mathbb{R}^{d \	imes d} 是一个对角矩阵,对角线上的元素 i 是直到 t 时刻为止,所有关于 \	heta_i 的梯度平方和,而 \\epsilon 是一个平滑项,可以避免分母为零(通常 \\epsilon 10^{-8} 的数量级上)。Duchi 等人将该矩阵作为包含所有先前梯度的外积的全矩阵 G_t 的替代,因为计算全矩阵的平方根是不切实际的,尤其是在非常高的维度上(即使对于中等参数量,矩阵的均方根的计算都是不切实际的)。从另一方面看,计算平方根和对角线 G_t 的倒数却能轻易实现。

? 为了更直观展示上式,我们将其展开:

进一步地,我们简化学习率部分,

所以,我们可以用

最后得到,

则我们最后可以这样表示:

\	heta_{t+1}=\	heta_{t}- \\dfrac{\\eta}{\\sqrt{G_{t}+ \\epsilon}}\\odot g_{t}

有趣的是,如果没有平方根运算,算法的性能会差很多。

? Adagrad算法的一个主要优点是无需手动调整学习率。在大多数的应用场景中,通常采用常数 0.01。

? Adagrad的一个主要缺点是它在分母中累加梯度的平方:由于没增加一个正项,在整个训练过程中,累加的和会持续增长。这会导致学习率变小以至于最终变得无限小,在学习率无限小时,Adagrad算法将无法取得额外的信息。接下来的算法旨在解决这个不足。

? Adadelta 是 Adagrad 的一种扩展算法,以处理 Adagrad 学习率单调递减的问题。不是计算所有梯度平方,Adadelta 将积累过去梯度的窗口大小限制为一个固定值 w

? 在 Adadelta 中,无需存储先前的 w 个平方梯度,而是将梯度的平方递归地表示所有历史梯度平方的均值。在 t 时刻的均值 E[g^2]_t 只取决于先前的均值和当前的梯度(分量 \\gamma 类似于动量项):

E[g^2]_t=\\gamma E[g^2]_{t-1}+ (1 - \\gamma) g^2_t

我们将 \\gamma 设置成与动量项相识的值,即为 0.9 左右。为了简单起见,我们利用参数更新向量 \\Delta \	heta_t 重新表示 SGD 的更新过程:

\\begin{align}\\begin{split}\\Delta \	heta_t &=- \\eta \\cdot g_{t, i}\\\\  \	heta_{t+1}&=\	heta_t + \\Delta \	heta_t \\end{split}\\end{align}

Adagrad 参数更新向量为:

\\Delta \	heta_t=- \\dfrac{\\eta}{\\sqrt{G_{t}+ \\epsilon}}\\odot g_{t}

现在我们简单将对角矩阵 G_t 替换为历史梯度均值 E[g^2]_t :

\\Delta \	heta_t=- \\dfrac{\\eta}{\\sqrt{E[g^2]_t + \\epsilon}}g_{t}

由于分母只是梯度的均方根 (RMS)误差,我们可以简写为:

\\Delta \	heta_t=- \\dfrac{\\eta}{RMS[g]_{t}}g_t

但作者指出,此更新(以及 SGD、Momentum 或者 Adagrad)中的单位不匹配,即更新具与参数具有相同的假设单位。为了实现这个要求,作者首次定义了另一个指数衰减均值,这次不是梯度平方,而是参数的平方更新:

E[\\Delta \	heta^2]_t=\\gamma E[\\Delta \	heta^2]_{t-1}+ (1 - \\gamma) \\Delta \	heta^2_t

因此,参数更新的均方根误差为:

RMS[\\Delta \	heta]_{t}=\\sqrt{E[\\Delta \	heta^2]_t + \\epsilon}

由于 RMS[\\Delta \	heta]_t 是未知的,我们利用参数的均方根误差来近似跟新。利用 RMS[\\Delta \	heta]_{t-1} 替换先前的更新规则中的学习率 \\eta ,最终得到 Adadelta 的更新规则:

\\begin{align}\\begin{split}\\Delta \	heta_t &=- \\dfrac{RMS[\\Delta \	heta]_{t-1}}{RMS[g]_{t}}g_{t}\\\\  \	heta_{t+1}&=\	heta_t + \\Delta \	heta_t  \\end{split}\\end{align}

使用 Adadelta,我们甚至不需要设置默认学习率,因为它已经从更新规则中删除。

平台注册入口