神经网络训练细节系列笔记:
通过学习,我们知道,因为训练神经网络有个过程:
<1>Sample 获得一批数据;
<2>Forward 通过计算图前向传播,获得loss;
<3>Backprop 反向传播计算梯度,这个梯度能告诉我们如何去调整权重,最终能够更好的分类图片;
<4>Update 用计算出的梯度去更新参数
用一段伪码表述上述文字:
while True:
data_batch = dataset.sample_data_batch()
loss = network.forward(data_batch)
dx = network.backward()
x += - learninng_rate * dx
当我们谈论参数更新时,指的是上述伪码的最后一行,这一篇博客我们所要讨论的就是如何把这一行的更新迭代做的高级一点:
x += - learninng_rate * dx
梯度下降算法是深度学习中使用非常广泛的优化算法,也是众多机器学习算法中最常用的优化方法。几乎当前每一个先进的(state-of-the-art)机器学习库或者深度学习库都会包括梯度下降算法的不同变种实现。但是,它们就像一个黑盒优化器,很难得到它们优缺点的实际解释。这篇博客旨在提供梯度下降算法中的不同变种的介绍。
首先介绍梯度下降算法的三种框架,然后介绍它们存在的问题和挑战,接着介绍如何进行改进来解决存在的问题即算法的优化。
旨在每次使用全量的训练集样本来更新模型参数,即:θ=θ−η⋅∇θJ(θ)
伪码如下:
for i in range(nb_epochs):
params_grad = evaluate_gradient(loss_function,data,params)
params = params - learning_rate * params_grad
nb_epochs是用户输入的最大迭代次数。使用全量的训练集样本计算损失函数loss_function的梯度params_grad,然后使用学习速率learning_rate朝着梯度相反方向去更新模型的每一个参数params。
批量梯度下降每次学习都使用整个训练集,因此其优点在于每次更新都会朝着正确的方向进行,最后能够保证收敛于极值点(凸函数收敛于全局极值点,非凸函数可能会收敛于局部极值点,属于凸理论的问题了),但是其缺点在于每次学习时间过长,并且如果训练集很大以至于需要消耗大量的内存,并且全量梯度下降不能进行在线模型参数更新。
旨在每次从训练集中随机选择一个样本来进行学习,即:θ=θ−η⋅∇θJ(θ;xi;yi)
伪码如下:
for i in range(nb_epochs):
np.random.shuffle(data)
for example in data:
params_grad = evaluate_gradient(loss_functon,example,params)
params = params - learning_rate * params_grad
批量梯度下降算法每次都会使用全部训练样本,因此这些计算是冗余的,因为每次都使用完全相同的样本集。而随机梯度下降算法每次只随机选择一个样本来更新模型参数,因此每次的学习是非常快速的,并且可以进行在线更新。
我们可以对不同的参数方案和他们如何快速优化有一个直观的认识:
特别观察一下SGD,红色的那条线,从图中可以看出SGD实际上是所有方法中最慢的一个,所以在实际中很少应用它,我们可以使用更好的方案。
那我们来分析一下SGD的问题,是什么原因导致它的的速度这么慢?
举个栗子:
我们可以从水平方向上可以看出,它的梯度很是非常小的,因为处于在一个比较浅的水平里;但是垂直有很大的速率,因为它是一个非常陡峭的函数,所以出现了这种状况:
在这种情况下使用SGD,在水平方向上进行比较缓慢,而在垂直方向上进展的很快,所以产生了很大的上下震荡。
* 小批量梯度下降(Mini-batch gradient descent)
旨在综合了 batch 梯度下降与 stochastic 梯度下降,在每次更新速度与更新次数中间取得一个平衡,其每次更新从训练集中随机选择 m,m
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梯度下降降低了收敛波动性,即降低了参数更新的方差,使得更新更加稳定。相对于全量梯度下降,其提高了每次学习的速度。并且其不用担心内存瓶颈从而可以利用矩阵运算进行高效计算。一般而言每次更新随机选择[50,256]个样本进行学习,但是也要根据具体问题而选择,实践中可以进行多次试验,选择一个更新速度与更次次数都较适合的样本数。mini-batch梯度下降可以保证收敛性,常用于神经网络中。
虽然梯度下降算法效果很好,并广泛使用,但是也存在着问题和挑战需要解决:
1. 选择一个合理的学习速率很难。如果学习速率过小,则会导致收敛速度很慢;如果学习速率过大,那么就会阻碍收敛,即在极值点附近会震荡。
学习速率调整(又称学习速率调度,Learning rate schedules),在每次更新过程中,改变学习速率,如退火。一般使用某种事先设定的策略或者在每次迭代中衰减一个较小的阈值。无论哪种调整方法,都需要事先进行固定设置,这便无法自适应每次学习的数据集特点。
2.模型所有的参数每次更新都是使用相同的学习速率。如果数据特征是稀疏的或者每个特征有着不同的取值统计特征与空间,那么便不能在每次更新中每个参数使用相同的学习速率,那些很少出现的特征应该使用一个相对较大的学习速率。
3.对于非凸目标函数,容易陷入那些次优的局部极值点中,如在神经网路中。那么如何避免呢。而更严重的问题不是局部极值点,而是鞍点。
如果把要优化的目标函数看成山谷的话,可以把要优化的参数看成滚下山的石头,参数随机化为一个随机数可以看做在山谷的某个位置以0速度开始往下滚。目标函数的梯度可以看做给石头施加的力,由力学定律知:F=m∗a,所以梯度与石头下滚的加速度成正比。因而,梯度直接影响速度,速度的累加得到石头的位置,对这个物理过程进行建模,可以得到参数更新过程为
# Momentum update
v = momentum * v - learning_rate * dx # integrate velocity
x += v # integrate position
代码中v指代速度,其计算过程中有一个超参数momentum,称为动量(momentum)。虽然名字为动量,其物理意义更接近于摩擦,其可以降低速度值,降低了系统的动能,防止石头在山谷的最底部不能停止情况的发生。如果没有momentum * v,那么小球就永远都不会停下了,会在平面上滚动,不会有能量的损失,损失函数就很难最小化。动量的取值范围通常为[0.5, 0.9, 0.95, 0.99],一种常见的做法是在迭代开始时将其设为0.5,在一定的迭代次数(epoch)后,将其值更新为0.99。
加上动量项就像从山顶滚下一个球,球往下滚的时候累积了前面的动量(动量不断增加),因此速度变得越来越快,直到到达终点。同理,在更新模型参数时,对于那些当前的梯度方向与上一次梯度方向相同的参数,那么进行加强,即这些方向上更快了;对于那些当前的梯度方向与上一次梯度方向不同的参数,那么进行削减,即这些方向上减慢了。即在陡峭的方向上削弱这些动荡,在一致的浅的方向激励此过程。因此可以获得更快的收敛速度与减少振荡。
回头看一下原始的Momentum:
v = momentum * v - learning_rate * dx
图中的momentum step(绿色的变量)代表代码中的动能项(momentum * v),gradient step (红色的变量)代表梯度也就是损失函数减少的方向( - learning_rate * dx),蓝色的线就是他们俩的向量和。
Nesterov Momentum要比Momentum的效果会好一些,我们先不管现输入是什么,所以在我们还没有计算出gradient step(红色的变量)情况下,我们已经建立了momentum step(绿色的变量),并得到了未知的梯度。也就是说,Nesterov Momentum想让我们来预测结果–gradient step,也就是计算在momentum step绿色箭头这一点的梯度确定gradient step,这样得到了和之前细微不同的更新结果,理论上,这一方法有着更好的收敛效果,在实际上确实要比Momentum好得多。
通俗的解释一下,由于从山顶往下滚的球会盲目地选择斜,Nesterov Momentum是在遇到倾斜向上之前应该减慢速度。
Nesterov Momentum和Momentum的差别在这里:
和之前Momentum不同的是,Nesterov Momentum在计算的梯度的时候,加上了μ*Vt-1,这种方式预估了下一次参数所在的位置,选择了和之前计算梯度不同的位置,称之为预测位置(lookahead position),所以Nesterov Momentum的效果会更好。
Adagrad也是一种基于梯度的优化算法。
cache += dx**2
x += - learning_rate * dx / (np.sqrt(cache) + le-7)
相比于SGD,增加了个附加变量—-cache来放缩梯度,并且是不停的增加这一附加变量。这里的变量cache是一个联合矢量,和主向量是一样大的,因为cache在每一维度计算其相应梯度的平方和,我们将这些cache构造起来,然后逐项用这一函数去除以cache的平方,使得对每个参数自适应不同的学习速率,对稀疏特征,得到大的学习更新,对非稀疏特征,得到较小的学习更新,因此该优化算法适合处理稀疏特征数据。
So,举个栗子 :
Adagrad在垂直方向上的梯度会加到cache中,然后相应的会除以越来越大的数,所以在垂直方向上会得到越来越小的更新。当我们在垂直方向上看到许多大的梯度,Adagrad就会衰减学习速率,使垂直方向的更新步长越来越小。在水平方向上的梯度是很小的,所以分母会变小,相比于垂直方向,水平方向更新更快。这就是对每个参数自适应不同的学习速率,针对不同梯度方向的补偿措施。
但是,此时,我们思考一个问题,如果我们训练整个神经网络,更新过程中步长大小会发生什么变化?
A:在Adagrad算法中,不断有正数加到分母的cache变量中,步长就会逐渐衰减到0,最后完全停止学习。
但是在神经网络中,我们需要整个网络不断变化从而修正数据,这才是正确的思考方式。它需要持续的活力来不断更新数据,而不是衰退到停止。所以,针对这一问题,Geoff Hinton对Adagrad做了个小小的改变,也被称之为RMSProp算法。
RMSProp是由Adagrad演化过来的:
RMSProp主要的思想是:cache不再是每一维度计算平方和,而是变成一个“泄露”的变量,利用衰减率(decay_rate)这个超参数,通常将decay_rate设置为0.99,接着计算引入衰减率发生“泄露”的平方和。所以我们仍然保持了在梯度较大或较小方向上,对于更新步长的补偿效果,但是不会再发生更新停止的情况。
Adam也是一种不同参数自适应不同学习速率方法,它是Adagrad算法和Momentum算法的结合体,可称之为“极品”,哈哈。正如你所见:
# Adam
m = beta1 * m + (1-beta1) * dx
v = beta2 * v + (1-beta2) * (dx**2)
x += - learning_rate * m / (np.sqrt(v)) + le-7)
m与v分别是梯度的带权平均和带权有偏方差,初始为0向量,Adam的作者发现他们倾向于0向量(接近于0向量),特别是在衰减因子(衰减率)β1,β2接近于1时。为了改进这个问题,对m与v进行偏差修正(bias-corrected),偏差修正取决于时间步长t:
# Adam
m = beta1 * m + (1-beta1) * dx
v = beta2 * v + (1-beta2) * (dx**2)
m /= 1-beta1**t
v /= 1-beta2**t
x += - learning_rate * m / (np.sqrt(v)) + le-7)
牛顿法是二阶收敛,梯度下降是一阶收敛,所以牛顿法就更快。如果更通俗地说的话,比如你想找一条最短的路径走到一个盆地的最底部,梯度下降法每次只从你当前所处位置选一个坡度最大的方向走一步,牛顿法在选择方向时,不仅会考虑坡度是否够大,还会考虑你走了一步之后,坡度是否会变得更大。所以,可以说牛顿法比梯度下降法看得更远一点,能更快地走到最底部。
根据wiki上的解释,从几何上说,牛顿法就是用一个二次曲面去拟合你当前所处位置的局部曲面,而梯度下降法是用一个平面去拟合当前的局部曲面,通常情况下,二次曲面的拟合会比平面更好,所以牛顿法选择的下降路径会更符合真实的最优下降路径。
红色的牛顿法的迭代路径,绿色的是梯度下降法的迭代路径。
迭代公式:
我们可以通过公式可以看出二阶收敛的优势,没有学习速率,没有超参数,只要知道方向和曲率,就能达到近似的最低值,牛顿法的具体推导过程在此不再赘述。但在实际应用中,牛顿法不是很受欢迎,在迭代公式中的H是指Hessian矩阵,比如你有1亿的数据,Hessian矩阵是1亿行和1亿列,然后进行逆运算,Good Luck!这是不可能发生的。
还有一些二阶的梯度下降算法,我们不准备使用它们,在此一并简单介绍:
BGFS算法不用对Hessian矩阵进行逆运算,通过秩为1的矩阵连续更新得到一个近似的Hessian矩阵,但是依然要存储Hessian矩阵的值,所以对大型神经网络仍然不适用。