语言选择: 简体中文简体中文 line EnglishEnglish

公司动态

DL知识拾贝(Pytorch)(四):DL元素之三:优化器

知识导图

当目标函数确定后,深度学习的训练任务就是最小化目标函数,也就是这里所说的优化方法(优化器)。其实,任何最大化问题都可以很容易地转化为最小化问题,只需令目标函数的相反数为新的目标函数即可。 深度学习中绝大多数目标函数都很复杂。因此,很多优化问题并不存在解析解,而需要使用基于数值方法的优化算法找到近似解,即数值解。为了求得最小化目标函数的数值解,我们将通过优化算法有限次迭代模型参数来尽可能降低损失函数的值。

为了获得复杂目标函数的数值解,我们一般采用梯度下降及其衍生优化算法来迭代计算。 为了直观上理解梯度下降,可以将其形象为“下山问题”。如下图。

目标函数就是这里的“山”,当我们初始化了网络参数后,我们会有一个目标函数值,也就是出现在“山”上某个点。为了最小化目标函数,也就是“下山”,我们需要知道下山的每一步怎么走。梯度(可以理解为导数),可以在几何意义上反映函数图像在某处的升降情况,当我们确保了梯度一直是最优下降方向,并根据当前梯度更新下一步的函数值,就可以快速下山,取得最优解。 下图是从数学意义上的分析:【截图自《动手学深度学习(Pytorch版)》:项目地址

一个凸函数的最小值点在端点,极小值点。而一个函数有多个极小值点,如果我们任选一个极小值点最为函数的最小值,这时容易陷入局部最优,而不是全局最优。也就是上面图中不同的起点可能会走不同的路线下山,而且会到达不同的地方。从而陷入局部最优,为什么说是陷入呢?因为达到局部最优解的时候梯度为0已经不会更新了。局部最优相当于山坳,全局最优相当于山脚。我们需要考虑的是能尽量走到山脚(避免局部最优),而且要能尽快的下山(快速收敛),这就是一系列优化算法做的事情。

规则:BGD是在更新参数时使用所有的样本来进行更新:

由于是用有限次迭代来求目标函数的最小值数值解,所以事先指定好迭代轮数:epochs。

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

缺点:由于BGD对整个数据集计算梯度,所以计算起来非常慢

规则:和 BGD 的一次用所有数据计算梯度相比,SGD 每次更新时随机采样一个样本进行梯度更新,对于很大的数据集来说,可能会有相似的样本,这样 BGD 在计算梯度时会出现冗余,而 SGD 一次只进行一次更新,就没有冗余,而且比较快,并且可以新增样本。



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

缺点:SGD的噪音较BGD要多,使得SGD并不是每次迭代都向着整体最优化方向。所以虽然训练速度快,但是准确度下降,并不是全局最优。

BGD和SGD是两个极端,一个采用所有数据来梯度下降,一个用一个样本来梯度下降。对于训练速度来说,SGD由于每次仅仅采用一个样本来迭代,训练速度很快,而BGD在样本量很大的时候,训练速度不能让人满意。对于准确度来说,SGD用于仅仅用一个样本决定梯度方向,导致解很有可能不是最优。对于收敛速度来说,由于SGD一次迭代一个样本,导致迭代方向变化很大,不能很快的收敛到局部最优解。MBGD是相对于BGD和SGD的折中方法。 规则:MBGD 每一次利用一小批样本,即 n 个样本进行计算,这样它可以降低参数更新时的方差,收敛更稳定,另一方面可以充分地利用深度学习库中高度优化的矩阵操作来进行更有效的梯度计算。


由于是小批量使用数据,所以就会产生超参batch_size,对于该参数来说:

(一):在合理地范围内,增大batch_size的好处

  • 内存利用率提高了,大矩阵乘法的并行化效率提高。
  • 跑完一次 epoch(全数据集)所需的迭代次数减少,对于相同数据量的处理速度进一步加快。
  • 在一定范围内,一般来说 Batch_Size 越大,其确定的下降方向越准,引起训练震荡越小。

(二): 盲目增大batch_size的坏处

  • 内存利用率提高了,但是内存容量可能撑不住了。
  • 跑完一次 epoch(全数据集)所需的迭代次数减少,要想达到相同的精度,其所花费的时间大大增加了,从而对参数的修正也就显得更加缓慢。
  • Batch_Size 增大到一定程度,其确定的下降方向已经基本不再变化。
for i in range(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

问题分析:

1) Mini-batch gradient descent 不能保证很好的收敛性,learning rate 如果选择的太小,收敛速度会很慢,如果太大,loss function 就会在极小值处不停地震荡甚至偏离。(有一种措施是先设定大一点的学习率,当两次迭代之间的变化低于某个阈值后,就减小 learning rate,不过这个阈值的设定需要提前写好,这样的话就不能够适应数据集的特点。)对于非凸函数,还要避免陷于局部极小值处,或者鞍点处,因为鞍点周围的error是一样的,所有维度的梯度都接近于0,SGD 很容易被困在这里。(会在鞍点或者局部最小点震荡跳动,因为在此点处,如果是训练集全集带入即BGD,则优化会停止不动,如果是mini-batch或者SGD,每次找到的梯度都是不同的,就会发生震荡,来回跳动。)

2)SGD对所有参数更新时应用同样的 learning rate,如果我们的数据是稀疏的,我们更希望对出现频率低的特征进行大一点的更新。LR会随着更新的次数逐渐变小。所以应当解决不同变量在不同维度的学习率统一问题。

关于Momentum动量法,参考【3】的博客中给出非常形象的解释: 如果把梯度下降法想象成一个小球从山坡到山谷的过程,那么前面方法所描述的小球是这样移动的:从A点开始,计算当前A点的坡度,沿着坡度最大的方向走一段路,停下到B。在B点再看一看周围坡度最大的地方,沿着这个坡度方向走一段路,再停下。确切的来说,这并不像一个球,更像是一个正在下山的盲人,每走一步都要停下来,用拐杖来来探探四周的路,再走一步停下来,周而复始,直到走到山谷。而一个真正的小球要比这聪明多了,从A点滚动到B点的时候,小球带有一定的初速度,在当前初速度下继续加速下降,小球会越滚越快,更快的奔向谷底。Momentum 动量法就是模拟这一过程来加速神经网络的优化的。 动量法的梯度更新公式如下:

其中γ 为衰减因子,一般取0.9。这样的做法可以让早期的梯度对当前梯度的影响越来越小,如果没有衰减值,模型往往会震荡难以收敛,甚至发散。$v_{t-1}$表示之前所有步骤所累积的动量和。 Momentum的想法很简单,就是多更新一部分上一次迭代的更新量,来平滑这一次迭代的梯度:

Pytorch中的Momentum可以通过参数指定:

opt_Momentum=torch.optim.SGD(net_Momentum.parameters(),lr=LR,momentum=0.9)

NAG是在Momentum的基础上采用了一种更为聪明的办法。 前面说到的Momentum每下降一步都是由前面下降方向的梯度累积和当前点的梯度方向组合而成。那么既然每一步都要将两个梯度方向(历史梯度、当前梯度)做一个合并再下降,那为什么不先按照历史梯度往前走一步,走一步所到的位置的“超前梯度”来做梯度合并呢?如此一来,小球就可以先不管三七二十一先往前走一步,在靠前一点的位置看到梯度,然后按照那个位置再来修正这一步的梯度方向。如此一来,有了超前的眼光,小球就会更加”聪明。

蓝色是 Momentum 的过程,会先计算当前的梯度,然后在更新后的累积梯度后会有一个大的跳跃。而 NAG 会先在前一步的累积梯度上(棕色线)有一个大的跳跃,然后衡量一下梯度做一下修正(红色线),这种预期的更新可以避免我们走的太快。 在Pytorch中使用NAG,直接指定nesterov为True并指明动量衰减参数momentum就可以了

torch.optim.SGD(params, lr=, momentum=0.9, dampening=0, weight_decay=0, nesterov=True)

在前面的问题分析中,对于问题一上述方法可以较好的解决,而对于问题二,Pytorch提供了一种Adagrad算法来根据自变量在每个维度的梯度值的大小来调整各个维度上的学习率,从而避免统一的学习率难以适应所有维度的问题。AdaGrad算法就是将每一个参数的每一次迭代的梯度取平方累加后在开方,用全局学习率除以这个数,作为学习率的动态更新。 这里引入一个累积梯度的参量:

$g_t$为$t$时刻的梯度,通过参量$s_t$来累加,在更新梯度的时候,有:

其中η是学习率, ?是为了维持数值稳定性?添加的常数,如$10^{-6}$ 。这?开?、除法和乘法的运算都是按元素运算的。这些按元素运算使得?标函数?变量中每个元素都分别拥有的学习率。 Adagrad 的优点是减少了学习率的手动调节,它的缺点是分母会不断积累,这样学习率就会收缩并最终会变得非常小,从而到后期很难继续收敛。 在Pytorch中内置了Adagrad,需要指定学习率:

class torch.optim.Adagrad(params, lr=0.01, lr_decay=0, weight_decay=0)

'''
参数:

params (iterable) – 用于优化的可以迭代参数或定义参数组
lr (float, 可选) – 学习率(默认: 1e-2)
lr_decay (float, 可选) – 学习率衰减(默认: 0)
weight_decay (float, 可选) – 权重衰减(L2范数)(默认: 0)
'''

AdaGrad算法最明显的缺陷是收敛速度慢。因为调整学习率时,分母上的变量 一直在累加按元素平方的小批量随机梯度,所以目标函数自变量每个元素的学习率在迭代过程中一直在降低(或不变)。因此,当学习率在迭代早期降得较快且当前解依然不佳时,AdaGrad算法在迭代后期由于学习率过小,可能较难找到一个有用的解。为了解决这一问题,RMSProp算法对AdaGrad算法做了一点小小的修改。 RMSProp在AdaGrad的基础上增加了一个[0,1]区间的衰减系数γ来控制历史信息的获取多少:

同AdaGrad一样,?标函数?变量中每个元素的学习率通过按元素运算来调整。

在Pytorch中内置了RMSProp,需要指定学习率和衰减系数alpha:

class torch.optim.RMSprop(params, lr=0.01, alpha=0.99, eps=1e-08, weight_decay=0, momentum=0, centered=False)

'''
参数:

params (iterable) – 用于优化的可以迭代参数或定义参数组
lr (float, 可选) – 学习率(默认:1e-2)
momentum (float, 可选) – 动量因子(默认:0)
alpha (float, 可选) – 平滑常数(默认:0.99)
eps (float, 可选) – 增加分母的数值以提高数值稳定性(默认:1e-8)
centered (bool, 可选) – 如果为True,计算中心化的RMSProp,通过其方差的估计来对梯度进行归一化
weight_decay (float, 可选) – 权重衰减(L2范数)(默认: 0)
'''

Adadelta是基于RMSProp控制历史信息获取的方法,对Adagrad作出的改进【截图自《动手学深度学习(Pytorch版)》:项目地址】:

可以看到,如不考虑?的影响, AdaDelta算法跟RMSProp算法的不同之处在于使?√Δ$x_{t-1}$来替代学习率 。 在Pytorch中内置了Adadelta,需要指定学习率和衰减系数alpha:

class torch.optim.Adadelta(params, lr=1.0, rho=0.9, eps=1e-06, weight_decay=0)
'''
参数:

params (iterable) – 用于优化的可以迭代参数或定义参数组
rho (float, 可选) – 用于计算平方梯度的运行平均值的系数(默认值:0.9)
eps (float, 可选) – 增加到分母中以提高数值稳定性的术语(默认值:1e-6)
lr (float, 可选) – 将delta应用于参数之前缩放的系数(默认值:1.0)
weight_decay (float, 可选) – 权重衰减 (L2范数)(默认值: 0)
'''

Adam算法是另一种计算每个参数的自适应学习率的方法。相当于 RMSprop + Momentum。【截图自《动手学深度学习(Pytorch版)》:项目地址

在Pytorch中内置了Adam:

class torch.optim.Adam(params, lr=0.001, betas=(0.9, 0.999), eps=1e-08, weight_decay=0)
'''
参数:
params (iterable) – 用于优化的可以迭代参数或定义参数组
lr (float, 可选) – 学习率(默认:1e-3)
betas (Tuple[float, float], 可选) – 用于计算梯度运行平均值及其平方的系数(默认:0.9,0.999)
eps (float, 可选) – 增加分母的数值以提高数值稳定性(默认:1e-8)
weight_decay (float, 可选) – 权重衰减(L2范数)(默认: 0)
'''

以下为几种算法在鞍点和等高线上的表现:




由此可以看出都可以看出,Adagrad, Adadelta, RMSprop 几乎很快就找到了正确的方向并前进,收敛速度也相当快,而其它方法要么很慢,要么走了很多弯路才找到。由图可知自适应学习率方法即 Adagrad, Adadelta, RMSprop, Adam 在这种情景下会更合适而且收敛性更好。 所以,在优化器的选择上,对于需要更快的收敛,或者是训练更深更复杂的神经网络,自适应方法更好,一般Adam用的最多;SGD 虽然能达到极小值,但是比其它算法用的时间长,而且可能会被困在鞍点。

不过理论归理论,现实的深度学习任务中未必是其他算法吊打SGD。有人就做过这样的比较实验:原知乎贴 在图像任务中,更多人还是使用了SGD+NAG的组合,实践证明很多时候比自适应算法好,所以选择什么优化器还需要具体数据任务的实验尝试。

引用和参考:

[1].cnblogs.com/guoyaohua/p[2].jianshu.com/p/9908ecfea[3].blog.csdn.net/tsyccnh/a

平台注册入口