本文整理了常见的深度学习优化器,仅供本人学习使用,大部分内容来自于张觉非,陈震《用Python实现深度学习框架》,仅做了少量的修正和新增。

梯度下降优化器

梯度下降算法的基本原理:将样本视为常量,将参数视为自变量,将损失值视为因变量,然后计算损失值对参数的梯度。经典的梯度下降算法可以根据训练样本的选取方式不同分为如下三种:

  • BGD(Batch Gradient Descent,批量梯度下降法)
  • SGD(Stochastic Gradient Descent,随机梯度下降法)
  • MBGD(Mini Batch Gradient Descent,小批量梯度下降法)

BGD 是把所有的训练样本视作常量,把所有样本的平均损失值视作因变量,然后计算所有样本的平均损失值对参数的梯度。因为求梯度的过程(求导)是线性运算,因此所有样本的平均损失对参数的梯度就等于每个样本的损失值对参数的梯度的平均:

\[\nabla \left( \frac{1}{n} \sum_{i=1}^n \text{loss}(\boldsymbol{w} | \boldsymbol{x_i}) \right) = \frac{1}{n} \sum_{i=1}^n \nabla \left( \text{loss}(\boldsymbol{w} | \boldsymbol{x_i}) \right)\]

其中,$n$ 是训练样本总数,$\boldsymbol{w}$ 代表模型的全部参数,$\boldsymbol{x_i}$ 是第 $i$ 个样本,$\text{loss}(\boldsymbol{w} \vert \boldsymbol{x_i})$ 是在第 $i$ 个训练样本上的损失值,$\nabla \left( \text{loss}(\boldsymbol{w} \vert \boldsymbol{x_i}) \right)$ 是第 $i$ 个样本上的损失值对模型参数的梯度。我们可以看出,BGD 实际上是以所有训练样本的平均损失值为损失值,求它对模型参数 $\boldsymbol{w}$ 的梯度。

因为梯度运算符 $\nabla$ 是线性的,所以才会有上面的公式结论。这使得我们可以先依次计算出训练集中每个样本的损失值对参数的梯度,然后再求这些梯度的平均值。它等价于 BGD 的真正损失值(所有样本的平均损失值)对参数的梯度。

另一个极端是 SGD,它是用训练集中每个样本的损失值对参数的梯度来更新参数。之所以叫“随机”,是因为每次更新都是从训练集中随机取出一个样本,并以它为常量,用损失值对参数的梯度来更新参数。当然,由于训练集中的样本没有顺序,所以“依次取”就相当于是“随机取”了。

MBGD 是对上述两个极端方法的折中。它即不用全体样本的平均损失值,也不用担个样本的损失值,而是以一部分(mini batch)样本的平均损失值为损失值。一个 mini batch 的样本数量称为“批大小”(batch size)。显然,若批大小为 $1$,则 MBSGD 就是 SGD,若批大小为 $n$,则 MBGD 就是 BGD。

训练集中的样本是对现实世界中总体样本的采样。从随机样本计算出来的损失值也是随机变量,训练及中的每个样本的损失值都是对这个随机变量的采样。平均损失值也是对损失值随机变量的估计,一个样本就是用一个采样进行估计,$n$ 个样本就是就是用 $n$ 个采样进行估计,那么,“批大小”个样本就是用“批大小”个采样进行估计。

根据大数定律,样本量越大,估计得越准确。根据这一定律,BGD 使用全部样本的平均来估计损失值是最好的。但是对于现实问题,训练集未必能充分地反映总体,另外,在训练样本较多的情况下,由于硬件资源的限制,一次性计算所有样本的损失也是比较困难的。

batch size的选取通常需要考虑一下几点:

  1. 更大的batch size会得到更精确的梯度估计值,但其估计梯度的回报是低于线性的(边际收益递减);
  2. 如果训练集较小,可以直接使用梯度下降法,batch size等于样本集大小;
  3. 《Deep Learning》 书中提到,在某些硬件上使用特定大小的数组时,运行时间会更少。尤其是在使用 GPU 时,通常使用 2 的幂数作为 batch size 可以获得更少的运行时间。

使用梯度下降算法对模型参数做更新的公式:

\[\boldsymbol{w}^{\text{new}} = \boldsymbol{w}^{\text{old}} - \eta \cdot \nabla\text{loss}(\boldsymbol{w}^{\text{old}})\]

其中,$\nabla\text{loss}(\boldsymbol{w}^{\text{old}})$ 是损失值对参数的梯度,它决定了参数更新的方向;$\eta$ 是学习率,它对梯度的长度进行缩放,决定了参数更新的速度。为了更清晰地描述这个过程,我们把它分解成三步,用 $\boldsymbol{g}$ 表示梯度,$\boldsymbol{v}$ 表示学习率 $\eta$ 乘以反梯度 $-\boldsymbol{g}$,它就是参数向量的更新量:

\[\left\{ \begin{aligned} \boldsymbol{g} &= \nabla{f(\boldsymbol{w}^{\text{old}})} \\ \boldsymbol{v} &= -\eta\cdot\boldsymbol{g} \\ \boldsymbol{w}^{\text{new}} & = \boldsymbol{w}^{\text{old}} + \boldsymbol{v} \end{aligned} \right.\]

接下来介绍朴素梯度下降算法的局限性和改进后的梯度下降算法。

朴素梯度下降法的局限

  1. 学习率 $\eta$ 在训练过程中固定不变,而它决定了每一次更新的步长:如果学习率过小,则收敛速度会很慢,且容易陷入局部极小点(local minimal)无法逃脱;若学习率过大,又会导致训练过程在某些“地形”上发生震荡甚至无法收敛;
  2. 每次参数更新时只考虑当前时刻的损失值对参数的梯度,没有考虑梯度变化的历史。

常见的梯度下降优化器变体

冲量优化器

冲量法借用了运动学中的动量(momentum)概念。它通过累积历史梯度来调整当前的梯度方向,从而改进了朴素梯度下降算法。该法以带衰减的方式将历史上的 $-\eta\cdot\boldsymbol{g}$ 累加在向量 $\boldsymbol{v}$ 中。衰减系数为 $\beta$ (通常取0.9):

\[\left\{ \begin{aligned} \boldsymbol{g} &= \nabla{f(\boldsymbol{w}^{\text{old}})} \\ \boldsymbol{v}^{\text{new}} &= \beta\cdot\boldsymbol{v}^{\text{old}} - \eta\cdot\boldsymbol{g} \\ \boldsymbol{w}^{\text{new}} & = \boldsymbol{w}^{\text{old}} + \boldsymbol{v}^{\text{new}} \end{aligned} \right.\]

直观来看,如果近期的梯度接近同向,则 $\boldsymbol{v}$ 的长度会比较大,加速收敛;如果近期梯度变向频繁,犹如行走的醉汉,则它们相互抵消,$\boldsymbol{v}$ 的长度会变小,更新会变慢,从而缓解震荡。

AdaGrad 优化器

无论是朴素梯度下降法还是冲量法,学习率对梯度(或者是 $\boldsymbol{v}$)的每个分量都是相同的。而 AdaGrad 则针对梯度的每个分量各自的历史采用了不同的学习率:

\[\left\{ \begin{aligned} \boldsymbol{g} &= \nabla{f(\boldsymbol{w}^{\text{old}})} \\ \boldsymbol{s}^{\text{new}} &= \boldsymbol{s}^{\text{old}} + \boldsymbol{g} \otimes \boldsymbol{g} \\ \boldsymbol{w}^{\text{new}} & = \boldsymbol{w}^{\text{old}} - \frac{\eta}{\sqrt{\boldsymbol{s}^{\text{new}} + \epsilon}} \cdot \boldsymbol{g} \end{aligned} \right.\]

$\boldsymbol{s}$ 是与梯度同维的一个向量,它的各分量分别累加了历史梯度各分量的平方。更新参数时,求 $\boldsymbol{s}$ 各分量的平方根,放在分母上对梯度各分量的学习率分别进行调节:若梯度的某分量在历史上一直较大,则 $\boldsymbol{s}$ 的对应分量就较大,对应的参数的学习率则较小;反之,若梯度的某分量在历史上一直较小,则 $\boldsymbol{s}$ 的对应分量就较小,对应参数的学习率则较大。

如果不是用一个统一的学习率(标量)去乘梯度,而是用不同的标量去乘梯度的各分量,即用一个向量 $\eta / \sqrt{\boldsymbol{s}^{\text{new}} + \epsilon}$ 与梯度做内积,或者说是将梯度向该向量做投影的话,这其实是改变了更新的方向,已经不再是沿着梯度的反方向了。

对于一元函数来说,可以用两个不同位置的函数值近似计算斜率,这其实是在不求导的情况下,用函数值的历史来近似计算导数。二阶导数是导函数的导函数,累积导数的历史则可以近似计算二阶导数。梯度是导数的推广,累积梯度的历史则可以近似模拟多元函数的“二阶导”——黑塞矩阵。黑塞矩阵蕴涵着多元函数的局部二阶信息,它能更准确的反映函数的局部状态。

小技巧:公式中的 $\epsilon$ 是一个极小的值,设置它可以避免出现分母为 0 的情况,提高数值的稳定性。有些实现会把 $\epsilon$ 放在根号外面,它们在本质上没有区别。

RMSProp 优化器

AdaGrad 算法累积了全部的历史梯度,这其实是不怡当的。应该更多地考虑近期的历史梯度, RMSProp 算法就针对这一点提出了改进,其全称是 Root Mean Square Propagation,RMSProp 算法可表示如下:

\[\left\{ \begin{aligned} \boldsymbol{g} &= \nabla{f(\boldsymbol{w}^{\text{old}})} \\ \boldsymbol{s}^{\text{new}} &= \beta \cdot \boldsymbol{s}^{\text{old}} + (1-\beta) \cdot \boldsymbol{g} \otimes \boldsymbol{g} \\ \boldsymbol{w}^{\text{new}} & = \boldsymbol{w}^{\text{old}} - \frac{\eta}{\sqrt{\boldsymbol{s}^{\text{new}} + \epsilon}} \cdot \boldsymbol{g} \end{aligned} \right.\]

它与 AaaGrad 算法的区别仅在第2步,RMSProp 算法引人了衰减系数 $\beta$($0<\beta<1$),它一般取值 0.9。$\boldsymbol{s}$ 是历史梯度分量平方的滑动加权累积,这也是 Root Mean Square 的由来。RMSProp 算法与 AdaGrad 算法非常类似,区别仅在于 RMSProp 算法对久远的历史梯度做了衰减。

Adam 优化器

冲量法累积历史梯度,RMSProp 算法累积历史梯度分量的平方,而 Adam(Adaptive Moment Estimation)算法则结合这两种算法的思想,做了进一步的改进:

\[\left\{ \begin{aligned} \boldsymbol{g} &= \nabla{f(\boldsymbol{w}^{\text{old}})} \\ \boldsymbol{v}^{\text{new}} &= \beta_1\cdot\boldsymbol{v}^{\text{old}} + (1-\beta_1)\cdot\boldsymbol{g} \\ \boldsymbol{s}^{\text{new}} &= \beta_2 \cdot \boldsymbol{s}^{\text{old}} + (1-\beta_2) \cdot \boldsymbol{g} \otimes \boldsymbol{g} \\ \boldsymbol{w}^{\text{new}} & = \boldsymbol{w}^{\text{old}} - \eta \cdot \frac{\boldsymbol{v}^{\text{new}}}{\sqrt{\boldsymbol{s}^{\text{new}} + \epsilon}} \end{aligned} \right.\]

Adam 算法将冲量法和 RMSProp 算法的机制都运用了起来。它用 $\boldsymbol{v}$ 累积历史梯度,用 $\boldsymbol{s}$ 累积历史梯度各分量的平方,这两种累积都采用了滑动平均的形式,于是这个算法引人了两个衰减系数 $\beta_1$ 和 $\beta_2$($0<\beta_1,\beta_2<1$),分别用于 $\boldsymbol{v}$ 和 $\boldsymbol{s}$。$\beta_1$ 和 $\beta_2$ 的典型取值为 0.9 和 0.99,都接近于 1。

迭代初期$\boldsymbol{v}$ 和 $\boldsymbol{s}$ 近似为零。为了修正这一点,有些实现将 $\boldsymbol{v}$ 和 $\boldsymbol{s}$ 分别除以 $1-\beta_1^t$ 和 $1-\beta_2^t$,其中 $t$ 是选代步数。选代开始时,这两个除数都很小,除以很小的除数使得记利 $\boldsymbol{s}$ 远离了零向量。随着迭代的进行,这两个除数趋近手 1,修正的效果消失。

参考资料

  1. 张觉非,陈震《用Python实现深度学习框架》第3章