八、优化算法:GD、SGD、动量法、AdaGrad、RMSProp、Adam、AMSGrad
优化算法是深度学习中的关键环节,我们经常说的炼丹主要指的就是这个环节。
1、优化算法是解决什么的?优化算法的种类?
我们已经知道,当样本数据正向传播一次,就会得到网络的一次预测,而网络的预测和样本标签又构成了我们的损失loss,我们的目标就是让loss最小化。
一般我们都把loss看做上图1的山川。就是这个loss在有的维度上比较陡峭,同时在其他有的维度上比较平坦。
而我们计算出loss时的网络参数w就是上图2的起点位置。就是起点是这批样本正向传播时的网络参数。由于这批样本是训练和学习样本,也就是说此时我们的参数还不是能很好做出预测的那组参数,所以此时的loss比较大。对应到上图2中就是红色小球在山上的较高位置。也就是小球的起始位置比较高。现在我们的目标是让loss最小化,也就是让小球逐渐走到上图的终点位置。也就是让loss收敛到最优点。
针对这个目标,就诞生了非常多的优化算法,常见的比如有:
(1)梯度下降法。而梯度下降法又细分一阶梯度下降法、二阶梯度下降法、共轭梯度法。
一阶梯度下降法:小批量梯度下降算法、带动量法的小批量梯度下降算法、自适应梯度法(Adagrad、RMSProp、Adam)
二阶梯度下降法:牛顿法、拟牛顿法
(2)启发式优化方法:启发式优化方法种类繁多,经典算法有模拟退火方法、遗传算法、蚁群算法以及粒子群算法等等。但这些方法与问题本身联系紧密,通用性较差。
所以在深度学习中,我们一般都采用梯度下降优化算法,就是通过迭代的方式,让小球一步步逐渐逼近终点,我们一般用盲人下山来比喻。
2、梯度下降法的进化史
(1)普通梯度下降GD
假设我们的模型架构已经搭建完毕,并且模型参数全部已经随机生成。此时数据从输入层输入,从模型输出层输出结果。也就是正向传播。
我们根据这些输出结果构建损失函数。此时损失函数就是模型参数的函数,我们就求损失函数中的各个模型参数的导数。求法就是反向传播。即先求损失函数关于模型输出结果的导数x模型输出结果关于激活函数的导数x激活函数关于参数的导数。这样就求出损失函数关于某个模型参数的偏导数:grad(w)。
然后我们用wt+1=wt-λ*grad(w),来更新迭代模型参数。直到迭代出一组参数,这组参数可以使损失函数达到最小即停止。其中,λ被称作学习率。
至此我们就通过梯度下降法找到了模型的这一组最佳参数,保存模型,建模成功,输入预测样本进行预测。
这种通过迭代的方式让小球一步步走向loss的最低点,我们经常比作盲人下山,那既然是盲人,也就是这个山不是能下得很顺利的,下山过程中是存在很多问题的。也就是上面的普通梯度下降法在模型训练过程中经常出现问题,比如陷入局部最小值点而无法找到全局最小值,或者在局部最小值点来回震荡无法收敛,或者计算量太大迭代一次非常耗时等问题。
比如上图,小球在山壁间震荡,往谷底方向行进得很慢。我们把左图的维度进行精简,就是右图的两种情况:一是,损失函数经常具有不太好的局部最小值或鞍点,这在高维空间非常常见。二是,梯度下降算法基本都是求导法,那么在局部最小处和鞍点处梯度就是0了,导致算法无法通过这些点而到达全局最小值点。
(2)SGD
针对计算量大的问题,后来人们就在损失函数上进行了改进。GD是全训练样本带入得到的损失函数,然后再对损失函数进行梯度下降找最小值。后来人们就不用全样本,而是用一个个小批次的样本来计算损失函数,并进行迭代下降,这就是小批量梯度下降法(BGD)。该方法的极端情况就是一次只用一个样本,就是随机梯度下降(Stochastic gradient descent,SGD)。其实现在都不太区分这几个变种。当你数据量不大时,你就可以全样本带入,当你数据量非常大的时候,训练的时候一般都是一个个小批次喂入的,所以也只能用小批量梯度下降法。所以现在这些方法都统称SGD。
这里我想强调的一点是:只要你用的不是全样本计算损失函数,那么你在下山的过程中,是走一步就变一座山的,就是你每一步的损失函数是不一样的,就是每次面临的山是不一样的。尽管你的每一步都是当前loss下的朝最小值方向迈进,但loss是随样本改变而改变的。但是这样也不妨碍我们最终到达山低,只是道路是曲折的如上图左图。当我们把全样本带入计算loss时,虽然梯度下降会是一条方向清晰的道路(图中),迭代次数少,但是深度学习的样本量都是非常大的,这样做的代价就是计算量太大,迭代一次非常耗时耗力,如果用小样本带入,虽然梯度下降是迂回的、迭代次数也多,但计算量小,迭代快。
(3)动量法
SGD仅仅是从样本量进行的改进,对于无法跳出局部最小值、鞍点的问题依旧没有得到很好解决,从上图中也可以看到,在使用随机梯度下降法时,虽然每次迭代速度很快,但是它们并不都是向着最小值点的方向前进的,会出现剧烈的振荡。算法是在这种振荡中逐渐走向最小值的位置,这个过程中也可能会在最小值的周围左右徘徊,始终无法到达。于是动量法产生了。
动量法从速度和精度两个方面进行了改进。速度方面是通过累加历史梯度信息更新梯度,让震荡方向减小,让相同的方向增强。精度方面是可能会跳出局部最优,进入更加广阔的空间寻找最小值。
所谓动量法就是说如果我下一个脚印,这里的脚印指的就是t,t+1, t+2等每步的状态,就是说如果上一个脚印落脚的时候梯度是正的方向,并且下一个脚印落脚的时候梯度也是正的方向,那么我这步就可以走得大一点,如果方向不一致,我这步就走得小一点。这就是动量法。
w0:初始的权值w0是模型自动随机生成的。用于跑通数据。下面开始迭代:
w1 = w0-lr*grad(w0)
w2 = w1 - lr*[gamma*grad(w0)+grad(w1)] #这里就是加入了动量因素,gamma一般设置为0.9,又叫衰减率。
w3 = w2- lr*[gamma*grad(w1)+grad(w2)]
.....
从迭代的表达式上看,如果第二步的梯度方向和第一步的梯度方向一致,那第二步就走了(第一步的步长+第二步的步长) ;
如果第二步的梯度方向和第一步的梯度方向不一致,那第二步就走了(第一步的步长-第二步的步长),是不是第二步的步子自动变小了,就减小了来回震荡。
同理第三步也是,如果第三步和第二步的梯度方向一致,第三步就走了(第二步的步长+第三步的步长),如不不一致就走了(第二步的步长-第三步的步长),就是走了个小步。
动量法使得在下坡(优化)的过程中,更快地向下走(优化),同时一定程度上能够从一个大坡冲上一个小坡(跳出局部最优)
下面我们调库实现一个带动量法的SGD梯度下降,然后再手写一个带动量法的梯度下降,看看二者是不是一样:
假如一个简单的损失函数是 w**2 ,初始点我们选在w=100这个点:
是不是调库、手写、背后的数学计算都是一样的。
(3)自适应梯度
如果说动量法只是多考虑了一下历史梯度的影响,减小了小球的来回大幅震荡、加强了相同方向的步伐。但是这样还是存在问题,比如下图,由于紫色维度和红色维度两个方向的学习率lr是同一个学习率,那势必会相互矛盾:比如我学习率设置的大一点,小球就会在紫色维度上大幅震荡了,如果我学习率设置得小一点,小球虽然不震荡了,但它也不会在红色维度上加速向loss更小的方向走啊。为了解决这个问题,人们又提出了自适应梯度法。自适应梯度法是通过分别调整震荡方向的步长和相同方向的步长,就是两个方向的调整分开调。通过减小震荡方向步长、增大平坦方向步长来减小震荡,加速通往谷底方向。
那有人会问:算法如何区分震荡方向和平坦方向?梯度幅度的平方较大的方向就是震荡方向;梯度幅度的平方较小的方向是平坦方向。
- AdaGrad
AdaGrad的全称是Adaptive Subgradient Methods(自适应次梯度方法),其中Adaptive是自适应的意思,subgradient是次梯度的意思。
从上图右边的数学公式看:
一是,AdaGrad的损失函数用的也是小批次数据。
二是,梯度计算也用的是该小批次数据算得的平均梯度。但是在AdaGrad的原论文里,它说它用的是次梯度。我们之前梯度计算的传统做法是:比如现在一个batch数据喂入网络,正向传播一次,我们用这个批次的所有样本的损失加和再除以batch中的样本量,作为这个batch的损失函数loss,然后从loss反向传播求参数的梯度g。但是AdaGrad说它用的次梯度是:按照每一条样本,生成一个损失函数,然后根据这个损失函数求出每个参数的梯度值。那当我们分batch训练模型的时候,我们训练一个batch,adagrad优化器都会给我们生成batch个损失函数,然后每个参数都求出了batch个梯度值,然后把这个batch个梯度值相加除以batch中的样本量,得到所有参数的梯度值g。有没有发现其实只是计算方式不一样,其实AdaGrad的次梯度和我们平时的梯度是一样一样的。所以后面不必再纠结梯度和次梯度了。
三是,AdaGrad多记录了一下每个批次的梯度平方,并且进行了累计,然后权值更新的时候,在学习率下面除了这个累计的梯度平方。这样随着迭代次数的增加,累计平方梯度就会单调的递增,就相当于学习率越往后迭代就越小。
四是,上图的g是个向量,所以AdaGrad其实是给每个参数都一个适合参数它自己的学习率。我们之前的SGD以及动量法,所有参数的学习率都是统一的一个值,现在是相当于每个参数都配一个自己的学习率。
五是,根据历史上参数梯度值的大小而自适应的匹配一个学习率的做法就意味着:如果这个参数你历史上的梯度值很大,你就会得到一个较小的学习率作为你的步长;如果那个参数的历史梯度值很小,那个参数就会得到一个较大的学习率作为它的步长。这样就很美好,小球就不会在紫色方向震荡得很厉害,因为它梯度大,并且是分母,那它就配的是一个小学习率;同时小球也不会在红色方向走得太慢,因为紫色方向梯度小,那它做分母,可不就配了一个大学习率。这样可以加快小球往全局最小处走。
AdaGrad的缺点: AdaGrad的思路是大梯度的参数配一个小学习率作为步长,小梯度参数配大学习率作为其步长。如果一个维度的梯度值越大,那么我们就希望这个维度上所使用的步长就比较小,所以AdaGrad的学习率是使用本次迭代之前的所有梯度值的平方和作为分母来控制步长大小。但这样做的一个重大弊端就是r是一直单调递增的,那势必越往后迭代学习率就太小太小而梯度消失没法迭代了。
所以我们一般不用AdaGrad,一般在特殊场景才用AdaGrad。因为它的优点和缺点都很明显,优点是自适应调整学习率,缺点是学习率会衰减的非常快,如果初始学习率设置小了,模型很快就停止在一个很小水平上的迭代,也就是说模型就迭代不动了,因为学习率小了,出现梯度消失现象了,所以AdaGrad经常在找到最优解之前就无法有效迭代了,就是我们经常看到AdaGrad停在很高的损失值上就无法下降的现象,可见这种激进的学习率衰减策略有时会消弱AdaGrad寻找最优解的能力,这让AdaGrad比较少用。而在特殊的场景下才使用AdaGrad,这个特殊场景是:AdaGrad具有非常适合稀疏矩阵的特性。特征数据是稀疏矩阵的时候就会迭代得很快很好,如果不是稀疏矩阵的话,AdaGrad就不是很擅长。所以在有的时候,有些少量的特殊样本,它的特征是非常重要的时候。就是说,对于含有少数特别重要的样本的数据集来说,我们首先要利用多数类样本将模型先提到到一个相对比较好的水平,比如90%,然后再利用AdaGrad偏向学习少数类的特点,去聚焦少数类的学习,把样本量较少的少数类也分类正确,让准确率从90%提高到99%。
为什么Adagrad适合低频数据呢?因为高频数据天然就适合对样本进行分类,而低频数据则不是,所以高频特征上的参数会有更大的梯度值,低频特征对应的参数的梯度会较小。而AdaGrad可以说是调高了低频特征对应的参数的梯度值,调低了高频特征对应得参数得梯度值。所以AdaGrad是更多的在学习低频特征,所以AdaGrad更擅长低频数据。
用代码实现AdaGrad时,最好用矩阵计算,因为矩阵计算比较快,上面的AdaGrad我们用代数形式表示,是因为代数形式好理解,下面举个栗子,展示一下AdaGrad原论文中求g^2的过程:
pytorch中实现Adagrad的类:torch.optim.Adagrad(params, lr=0.01, lr_decay=0, weight_decay=0, initial_accumulator_value=0,eps=1e-10)
params:就是模型的参数
lr:学习率,也可见Adagrad的学习率适合一开始就设置的比较大。我们SGD一般都是设置0.001开始试,而自适应的优化算法可以设置的比较大;
lr_decay:初始的G0,默认是0,可以不写;
weight_decay:pytorch的所有优化器都有这个参数,这个参数是在wt进行迭代时,直接作用在wt-1上的一个衰减项,就是wt = (1-weight_decay)*wt-1,所以当我们把这个参数打开后,模型训练就变得非常非常敏感。就是假如即使我把weight_decay设置为0.001这样一个非常小的值,模型很多情况下都开始表现出分值暴跌的情况,失去学习能力。这个参数默认也是0,一般情况我们不打开这个参数。
eps:就是分母上避免分母为0的那个常数值。其他参数可以不用管。
- RMSProp
AdaGrad提出的利用梯度值本身来削减学习率的思想成为了后续非常多优化算法的基础。为了解决AdaGrad所面临的、随着迭代学习率会变得太小的问题,许多研究者都给出了自己的解决方案,其中非常成功的一个解决方案就是RMSprop(Root Mean Square Propogation,均方根传播)算法。下面我们看看RMSProp的数学过程:
可见,RMSProp相较AdaGrad改变得就是第3步,RMSProp把历史平方梯度进行了加权平均。就类似动量法中的动量系数μ,把本轮的平方梯度和历史平方梯度进行了加权,也就是衰减了历史平方梯度值,避免历史平方梯度随之迭代单调递增,导致学习率衰减过快。 当ρ=0的时候,RMSProp就不考虑历史平方梯度值,只考虑当前梯度平方值,并根据当前梯度平方值来调节本轮学习率; 当ρ=0.9的时候,RMSProp就对历史平方梯度值进行0.9倍的衰减,如果此时梯度g陷入局部最小点或者鞍点时,随着算法继续迭代,历史平方梯度就逐渐衰减,学习率就逐渐增大,算法就很可能跳出局部最小点或鞍点,继续向全局最小值点走去。这样就是真正实现了自适应的调节学习率的效果。
在实际使用中,RMSProp展现出比其他的梯度下降算法迭代更快、更好优化的特性,因此是一个非常受欢迎的优化算法。
RMSprop很擅长处理带有随机性的数据,因此在小批量数据上会迭代得更加稳定。 通常来说,当我们在陌生的架构上尝试优化算法时,RMSprop是我们的首选,如果RMSprop效果不好,我们才会尝试一下其他的优化算法。
但是RMSprop也不是没有缺点的。它最大的问题就是违反了优化算法中的 "正定性"positive definiteness规则。这个规则就是要求:在优化算法中,随着迭代的进行,学习率是要不断减小或者不变的,就是你越往后迭代,你的步子就要越小,因为你越接近了最小值,此时你步子越大,不就在最小值附近跳来跳去嘛。但是RMSprop越往后面迭代,它的步子大小会变得飘忽不定,就是有时会大步有时会小步子。RMSprop和AdaGrad的思路都是大梯度配小学习率小梯度配大学习率,这个思路是没问题的,但是针对到迭代的每一步,并且放眼整个迭代过程,RMSprop算法可能会出现迭代后期的步子可能会比迭代前期还大的情况,因为学习率可能会出现自适应地上下剧烈跳动地情况,这就导致模型无法收敛。
所以RMSprop虽然在大部分数据集都表现比较好,但是也会在有的数据上可能难以收敛。所以RMSprop有可能一开始就得到一个很糟糕的结果,然后怎么调都调不上去,这时就可能是学习率发生了剧烈震荡,此时也不会有什么好的解决方法,只能换算法,比如换Adam。
pytorch中的RMSprop类:
torch.optim.RMSprop(params, lr=0.01, alpha=0.99, eps=1e-08, weight_decay=0, momentum=0, centered=False)
alpha: 就是上面数学推导中的ρ
momentum:是动量法里面的超参数。默认0就是不带动量法的。当你把这个参数打开后,比如momentum=0.9,就相当于是带动量法的RMSprop,就相当于在g前面再施加一个上一轮梯度的0.9倍值,也就是本轮的步长除了和本轮的梯度有关还和上一轮的梯度有关。所以使用RMSProp你可以自己调整要不要带动量的。
centered:当centered=True表示在进行计算前先对梯度进行一个中心化。论文中说的是如果我们设置centered=True会让训练中梯度更平稳。但一般来说,我们不会使用中心化这个参数。如果你的迭代过程真的是很不平稳,你可以尝试打开这个参数看看效果,但不要抱很大希望。
- Adam
亚当法是目前深度学习领域最受欢迎的优化算法之一。它的地位就类似XGboost之于机器学习,transform之于NLP, 是很多人一上来就无脑使用的一个优化算法。
截至目前,我们可以说adagrad最大的贡献就是给我们了,用梯度的大小来衰减学习率的伟大思想,正是这个思想才衍生了后面的一系列改进算法。
如果说RMSprop是在改善adagrad的基础上诞生的,那么adam就是在改善RMSprop的基础上诞生的。adam就是动量法和AdaGrad的结合体。
从Adam的数学公式上看,Adam是同时使用了动量法和自适应梯度法。
动量法:Adam对梯度g使用了动量法,就是将本轮梯度和上一轮梯度进行了加权。如果本轮梯度和上轮梯度方向相同,就加速前进,如果本轮梯度和上轮梯度方向相反,就减速。这样减小了在迭代过程中的震荡问题。
自适应梯度法:Adam通过记录历史梯度(4),自适应的调整迭代过程中各个参数的学习率,让梯度值大的参数配一个小学习率,梯度小的参数配置一个大的学习率,这样就可以加速向谷底滚动。
另外,Adam还进行了一个偏差修正(5),就是分别对累计梯度和自适应项都进行了一个除法的修正。这个操作是防止算法初期出现的冷启动情况。就是防止前期的迭代不会出现步子太小,迭代太慢的情况。因为r和v初始设置都是0,就是从0开始迭代的,刚开始迭代时,v是从梯度g的0.1倍开始迭代的,是不是梯度很小!而调整学习率的r又是累计g的平方和的,所以学习率也被调整的非常小!二者效应一叠加,就非常容易出现最开始的前几轮迭代非常缓慢。这样修正一下就是缓解这个问题的。这么一修正,前期的迭代就不会出现步子太小,迭代太慢的情况了。而后期随着Vt和Gt逐渐有一定积累后,但Gt是平方和,会增加得更快,也就是学习率会衰减得更快,这样即使在后期也不会出现剧烈波动。
pytorch中的Adam类:
class torch.optim.Adam(params, lr=0.001, betas=(0.9, 0.999), eps=1e-08, weight_decay=0, amsgrad=False)
除了参数amsgrad,其他所有的参数都和前面的算法的参数表示是一样的。
参数amsgrad如果打开就是一个全新的新算法,就是下面我们要讲的AMSGrad, 理论上应该不属于亚当算法的一部分,它是在亚当的基础上再次进行了改进,所以代码放在这个类里面。所以当参数amsgrad=False,这个优化器就是adam优化器,当参数amsgrad=True时,这个优化器就是amsgrad优化器。
- AMSGrad算法
前面讨论过RMSprop存在违反"正定性"的问题,同样基于RMSprop的Adam也存在这个问题,会导致Adam无法收敛。AMSGrad就是为解决这个问题而诞生的,论文《on the convergence of adam and beyond》将Adam的迭代流程改为:
就是在实际迭代权重w之前,要选择rt与rt-1中较大的一个用于学习率衰减。这个操作看上去只是对比这一次和上一次迭代中的r,但由于每次迭代前都进行比较,因此这一次被用于衰减学习率的r一定是过去全部t次衰减中最大的r。这样做既可以保证继承了Adam的优点,还能保证学习率在持续的迭代中,是遵守优化算法中要求的“正定性”的原则的,所以,在理论上AMSGrad算法比Adam算法更有稳定迭代的保障。但是在实际使用中,AMSGrad与Adam的结果一般都很相似。所以,当Adam表现不好时,你可以试试AMSGrad,有可能会得到出意想不到的好结果。
3、优化算法的选择
我们知道普通梯度下降和AdaGrad劣势非常明显,所以一般我们不用。其他大部分的算法都能够达到一个较低的loss,但基于不同的数据的情况,不同算法会呈现出不同的效果。但是以Adam和RMSprop为代表的衰减学习率的优化算法,通常都会得到较好的表现。所以,通常我们在使用优化算法时,首选是Adam和RMSprop,如果这两个算法的效果不好的话,则尝试AdaMax或者AMSGrad。
而二阶梯度下降中能够有较好表现的是Nesterov梯度下降,如果你用学习率衰减的算法都不理想时,你不妨试试NesterovSGD。在PyTorch中,我们可以使用SGD当中的参数Nesterov = True来调用这个算法。
总之就是,首选Adam和RMSprop,其次试试AMSGrad和SGD-Nesterov,当调到一定极限后,用动量法SGD+手动调学习率来精调。如果还是不行,也许就是数据的上限本身就很低,或者是模型上限太低,要么返回去继续考虑考虑特征工程,要么换一个更强大的模型架构。