在进行神经网络训练时,很多同学都不太注重损失函数图和损失函数的优化算法的理解,造成的结果就是:看起来效果不错,但是不知道训练的参数是否合理,也不知道有没有进一步优化的空间,也不知道初始点的选择是否恰当。本篇文章着重介绍神经网络损失函数最小化过程中使用的优化算法,前面先对梯度下降法进行介绍,后面介绍梯度下降法中使用的几种优化算法,中间穿插着自变量的更新轨迹和损失函数图像的讲解,以期同学们对损失函数相关的知识有清晰明确的认识,这是学好神经网络的基础。

梯度下降法

梯度下降法是最原始的对模型的参数向量进行更新的方法,它的目标函数是训练数据集中有关各个样本的损失函数的平均。设



是有关索引为



的训练数据样本的损失函数,



是训练数据样本数,



是模型的参数向量,那么目标函数定义为






目标函数在



处的梯度为:






模型的参数向量更新:





例如,对于目标函数



,使用上述迭代方法更新自变量的轨迹如下(学习率为0.1,迭代了20次):





对比损失函数pytorch_算法函数


可以看到自变量的迭代轨迹与等高线是垂直的,说明迭代一直朝着梯度的反方向进行(直达目的地)。但是梯度下降法每次更新模型参数需要对所有样本计算梯度,所以每次自变量迭代的计算开销为


.因此,当训练数据样本数很大时,梯度下降每次迭代的计算开销很高。


随机梯度下降法

为了减少计算的开销,采用了随机梯度下降(SGD)方法。在随机梯度下降的每次迭代中,我们随机均匀采样的一个样本索引


,并计算梯度


来迭代




我们的目标是把所有样本的损失降到一个极小值,但是这种方式只能把一个样本的损失给降低,那么这样能达到我们的目标吗?可以证明:经过很多次的迭代之后随机梯度下降能达到梯度下降相同的效果,也就是说随机梯度


是对梯度


的无偏估计。同样是对于目标函数


,采用随机梯度下降更新自变量的轨迹如下(学习率为0.1,迭代了20次):


对比损失函数pytorch_对比损失函数pytorch_02


可以看到自变量的迭代轨迹与等高线不是垂直的,说明迭代没有完全按照梯度下降的方向走,但是最后也到达了跟梯度下降方法同样的终点,再次证明了我们上述说的“经过很多次的迭代之后随机梯度下降能达到梯度下降相同的效果”。但是此时可能存在损失函数图像是以下形状:


对比损失函数pytorch_迭代_03


出现这种损失函数在某个值附近波动,说明学习率太大了,以至于在这个值附近跳来跳去,无法继续朝着梯度的反方向达到极小值点,通常需要降低学习率。

小批量随机梯度下降法

除了梯度下降和随机梯度下降,我们还能每次处理一批样本,当这一批样本的数量等于整个训练集时,就成了梯度下降;当这一批样本的数量为一个样本时,就成了随机梯度下降。

小批量随机梯度下降对自变量的迭代如下:




对于目标函数


,采用随机梯度下降更新自变量的轨迹如下(学习率为0.1,迭代了20次):(注意这里把


的系数变为了0.1,刚才


的系数都是1,也就是说这里在两个方向上,方向导数差了一个数量级):


对比损失函数pytorch_算法函数_04


可以看到小批量随机梯度下降也大体是朝着等高线垂直的方向前进。但是注意:在前几次的迭代过程中垂直方向上每一步的跨度都超过了水平方向上的跨度,这是因为两个方向的方向导数差别太大导致的,这种情况如果我们更新参数的时候在各个方向还是使用相同的学习率就会带来不好的结果:具体来说就是当学习率设置的大的时候方向导数比较大的那个维度容易发散,这样就找不到极小值了。当学习率设置的比较小的时候方向导数比较小的那个维度移动较慢,可能训练已经结束了,还没达到极小值点(在实际的训练中经常发生这样的事情,因为高维空间的函数及其复杂,可能存在大片平坦区域,很久也走不出去)。针对这种情况,出现了两种优化方法:动量法和AdaGrad算法。下面分别介绍。

动量法

动量法的思想很简单,就是要把之前时间步的梯度考虑进来,比如在某一个维度上,前面好几个时间步的梯度都是同一个符号,说明在这个方向上一直在下坡,还没有跳过极小值点,那么就需要加速,一次跳的更远一些,这样接近极小值更快一些。再比如在另一个维度上,前面好几个时间步的梯度的正负符号不一致,那么说明在这个区域内,这个维度在极小值附近跳来跳去,也就是说当前的(学习率*梯度)太大了,以至于不能更靠近极小值。这两种情况都可以通过一个办法解决,那就是把最近几个时间步的学习率*梯度考虑进来,如果最近几个时间步的都为同一个符号,那么它们相加就会变大,更新的就快;如果最近几个时间步的为不同的符号,那么它们相加就会相互抵消,就会变小,就更容易靠近极小值点。动量法的更新方法如下:




相比以前,多了一步


的计算,并且


的更新不再是减


了,而是减


,但是看这个式子其实体现不出上面描述的思想,我们把这个式子变形一下可得:



例如:


=0.9,那么上式就是



其实就是,本来应该减


的,现在把最近的10个


分别乘以它们的权值之后相加,通过这样就把最近10个时间步的


考虑了进来,实现了上述描述的思想。


同样是上述目标函数,这里我们把学习率设置的和刚才一样,但是使用动量法并设置动量超参数为


=0.5,自变量更新的轨迹如下:


对比损失函数pytorch_损失函数_05


可以看到动量法在竖直方向上的移动更加平滑。

AdaGrad算法

还是要解决刚才的问题,动量法是把最近的(学习率*梯度)考虑了进来,另一种想法就是针对每一维我们设置不同的学习率不就行了吗,如果某个维度的导数比较大,我们就设置比较小的学习率,如果某个维度的方向导数比较小,我们就设置稍微大一点的学习率,这正是AdaGrad算法算法的思路。

AdaGrad算法的更新方法如下(时间步0时,


的每个元素初始化为0):




其中


是学习率,


是为了维持数值稳定性而添加的常数,如


.这个式子看起来不太容易,我们把这个式子变形一下可得:



上式为了好看把


去掉了,从上述可以清楚的看出AdaGrad是如何针对每一个维度设置学习率的,它仅仅是把当前时间步之前的所有梯度按元素平方然后累加开方求倒数。所以如果某一维梯度比较大,那么相加开方之后这一维仍然比较大,求倒数之后就变得很小,也就是学习率很小。而且需要注意到,在每一个时间步,上式的分母都在累加,所以每个时间步学习率都在降低,所以,

当学习率在迭代早期降得较快且当前解依然不佳时,AdaGrad算法在迭代后期由于学习率过小,可能较难找到一个有用的解

还是使用目标函数


及学习率0.4,使用AdaGrad法自变量更新的轨迹如下:


对比损失函数pytorch_迭代_06


损失函数的变化图形大致如下所示:


对比损失函数pytorch_损失函数_07


可以看到由于前期学习率下降太快,导致后期的迭代变得特别慢,20轮迭代之后,自变量仍未达到极小值点。由于在实际的机器学习过程中,损失函数都是高维函数,函数将变得非常复杂,学习率下降太快会导致不管训练了多久可能都出不了某个区域的情况。

RMSProp算法

针对上面这个问题,提出了RMSProp算法。RMSProp算法综合了动量法和AdaGrad算法的思想,它既想用过去的时间步的梯度来更新当前


向量,又不想学习率下降的太快导致后期找不到有效解。因此RMSProp算法采用的方法是:




像刚才一样,我们还是做一个变形:



相当于给每个


加了个权重。然后计算方法照AdaGrad算法这样进行。


还是使用目标函数


及学习率0.4,使用RMSProp算法对自变量更新的轨迹如下(


=0.9):


对比损失函数pytorch_迭代_08


损失函数的变化图形大致如下所示:


对比损失函数pytorch_算法函数_09


可以看到,RMSProp算法可以更快逼近最优解,并且像动量法一样在竖直方向上的移动更加平滑,也克服了AdaGrad法迭代较慢的现象。

Adam算法

Adam算法在RMSProp算法基础上对小批量随机梯度也做了指数加权移动平均,可以看做是RMSProp算法与动量法的结合,也就是RMSProp算法的增强和优化版本。

Adam算法使用了动量变量


和RMSProp算法中的


,并在时间步0将它们中每个元素初始化为0,Adam算法更新自变量的过程如下:





另外还有一点,就是上面的


还可以写成


这个公式其实就是t个向量相加,每一个向量是一个系数


乘以梯度向量


,举个例子,比如t=2时,


,大家会发现系数相加0.3+0.6不等于1,实际上上面的t个系数相加:


,但是让各权值相加为1才能更好的把历史梯度考虑进来,所以我们让上述系数除以一个


,从而使过去各时间步小批量随机梯度权值之和为1,对应刚才的那个例子,就相当于



所以Adam算法真正的公式如下:





Adam算法是应用最广泛、效果最好的优化算法之一,近年来又提出了很多优化算法,但核心思想都是为了解决梯度下降过程中存在的上述几个问题。把这几个算法理解清楚,就理解了整个梯度下降的过程,对于后续研究其它优化算法也打下了坚实的基础。