深度学习模型的构建三、优化函数optimizer


深度学习中有众多有效的优化函数,比如应用最广泛的SGD,Adam等等,而它们有什么区别,各有什么特征呢?下面就来详细解读一下

一、先来看看有哪些优化函数

BGD 批量梯度下降

所谓的梯度下降方法是无约束条件中最常用的方法。假设f(x)是具有一阶连续偏导的函数,现在的目标是要求取最小的f(x) : min f(x)

核心思想:负梯度方向是使函数值下降最快的方向,在迭代的每一步根据负梯度的方向更新x的值,从而求得最小的f(x)。因此我们的目标就转变为求取f(x)的梯度。

当f(x)是凸函数的时候,用梯度下降的方法取得的最小值是全局最优解,但是在计算的时候,需要在每一步(xk处)计算梯度,它每更新一个参数都要遍历完整的训练集,不仅很慢,还会造成训练集太大无法加载到内存的问题,此外该方法还不支持在线更新模型。其代码表示如下:

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

我们首先需要针对每个参数计算在整个训练集样本上的梯度,再根据设置好的学习速率进行更新。
公式表示如下:
假设h(theta)是我们需要拟合的函数,n表示参数的个数,m表示训练集的大小。J(theta)表示损失函数。
在这里插入图片描述
不难看出,在批量梯度下降法中,因为每次都遍历了完整的训练集,其能保证结果为全局最优,但是也因为我们需要对于每个参数求偏导,且在对每个参数求偏导的过程中还需要对训练集遍历一次,当训练集(m)很大时,这个计算量是惊人的!
所以,为了提高速度,减少计算量,提出了SGD随机梯度下降的方法,该方法每次随机选取一个样本进行梯度计算,大大降低了计算成本。


SGD随机梯度下降

随机梯度下降算法和批量梯度下降的不同点在于其梯度是根据随机选取的训练集样本来决定的,其每次对theta的更新,都是针对单个样本数据,并没有遍历完整的参数。当样本数据很大时,可能到迭代完成,也只不过遍历了样本中的一小部分。因此,其速度较快,但是其每次的优化方向不一定是全局最优的,但最终的结果是在全局最优解的附近。
需要:学习速率 ϵ, 初始参数 θ
每步迭代过程:

  1. 从训练集中的随机抽取一批容量为m的样本{x1,…,xm},以及相关的输出yi
  2. 计算梯度和误差并更新参数

虽然BGD可以让参数达到全局最低点并且停止,而SGD可能会让参数达到局部最优,但是仍然会波动,甚至在训练过程中让参数会朝一个更好的更有潜力的方向更新。但是众多的实验表明,当我们逐渐减少学习速率时,SGD和BGD会达到一样的全局最优点。
优点:训练速度快,避免了批量梯度更新过程中的计算冗余问题,对于很大的数据集,也能够以较快的速度收敛.
缺点:由于是抽取,因此不可避免的,得到的梯度肯定有误差.因此学习速率需要逐渐减小,否则模型无法收敛
因为误差,所以每一次迭代的梯度受抽样的影响比较大,也就是说梯度含有比较大的噪声,不能很好的反映真实梯度.并且SGD有较高的方差,其波动较大,如下图:
在这里插入图片描述

  • 另外,需要注意的是因为存在样本选择的随机性,所以在梯度下降过程中会存在较大的噪声,因此学习速率应该要逐渐减小,来寻找一个相对全局最优的方向。
  • 同时也考虑到每次只选择一个样本进行梯度更新存在较大的噪声,学者们开始尝试每次选择一小批样本进行梯度更新,在降低噪声的同时提高速度,因此就有了下面的MBGD小批量梯度下降法。

MBGD小批量梯度下降

为了综合上述两种方法,提出了小批量梯度下降。它:
(1)降低在SGD中高方差的问题,能使得收敛更加稳定;
(2)可以利用深度学习中最先进的库进行矩阵优化的操作,加速操作;
(3)一般的小批量介于50~256,但是当适用很小的批量时,有时也统称为SGD。
核心思想:在每次迭代时考虑一小部分样本,比如考虑10个样本,同时计算在这10个样本点上的每个参数的偏导数,对于每个优化参数,将该参数在这10个样本点的偏导数求和。
但是,需要注意的是因为这里也存在样本选择的随机性,学习速率应该要逐渐减小,同时上述方法并不能保证好的收敛性。主要存在的挑战有:

  • 选择适当的学习率可能很困难。 太小的学习率会导致收敛性缓慢,而学习速度太大可能会妨碍收敛,并导致损失函数在最小点波动。
    使用学习率计划:尝试在训练期间调整学习率。 比如根据预先制定的规则缓慢的降低学习速率,或者当每次迭代之间的偏导差异已经低于某个阈值时,就降低学习速率。但是这里面的学习速率更新规则,以及阈值都是需要预先设定的,因此不适应于所有的数据集。
  • 此外,使用梯度更新的方法会导致所有参数都用学习速率更新。但是当训练集数据是稀疏的,或者特征的频率是不同的,我们可能不希望它们更新到同样的程度,因此使用相同的学习速率会导致那些很少出现的特征有较大的变化。
    在求取那些高度非凸的误差函数的最小值时,我们应该避免陷入局部最优解,实验表明,最困难的不是从局部最优而是鞍点,鞍点就是沿着某一个方向他是稳定的,沿着另一个方向不稳定,既不是最小点也不是最大点。这会使得该点在所有维度上梯度为0,让SGD难以逃脱。
    基于上述问题,又有了如下更多的优化策略!

momentum

上述SGD和MBGD算法都存在样本选择的随机性,因此含有较多的噪声,而momentum能解决上述噪声问题,尤其在面对小而较多噪声的梯度时,它往往能加速学习速率。
在这里插入图片描述
核心思想:Momentum借用了物理中的动量概念,即前几次的梯度也会参与运算。为了表示动量,引入了一个新的变量v(velocity)。v是之前的梯度的累加,但是每回合都有一定的衰减。

每步迭代过程:

  1. 从训练集中的随机抽取一批容量为m的样本{x1,…,xm},以及相关的输出yi
  2. 计算梯度和误差,并更新速度v和参数θ:
ĝ ← 1+m∇θ∑iL(f(xi;θ),yi)
v ← αv−ϵĝ
θ ← θ+v

其中参数α表示每回合速率v的衰减程度.同时也可以推断得到,如果每次迭代得到的梯度都是g,那么最后得到的v的稳定值为 ϵ∥g∥/1−α

也就是说,Momentum最好情况下能够将学习速率加速1/1−α倍.一般α的取值为0.9或者更小。当然,也可以让α的值随着时间而变化,一开始小点,后来再加大.不过这样一来,又会引进新的参数.

特点:
本质上来说,就和我们把球从山上退下来一样,球的速度会越来越快。和我们的参数更新一样,当方向一致时,动量项会增加;当方向不一致时,动量项会降低。
即:
前后梯度方向一致时,能够加速学习
前后梯度方向不一致时,能够抑制震荡


AdaGrad

AdaGrad可以自动变更学习速率,只是需要设定一个全局的学习速率ϵ,但是这并非是实际学习速率,实际的速率是与以往参数的模之和的开方成反比的.也许说起来有点绕口,不过用公式来表示就直白的多:
在这里插入图片描述
其中δ是一个很小的常亮,大概在10−7,防止出现除以0的情况.

核心思想:对于频繁出现的参数使用更小的更新速率,对于不频繁出现的参数使用更大的更新速率。
正因为如此,该优化函数脚适用于稀疏的数据,比如在Google从YouTube视频上识别猫时,该优化函数大大提升了SGD的鲁棒性。在训练GloVe词向量时该优化函数更加适用。

具体实现:
需要:全局学习速率 ϵ, 初始参数 θ, 数值稳定量δ
中间变量: 梯度累计量r(初始化为0)
每步迭代过程:

  1. 从训练集中的随机抽取一批容量为m的样本{x1,…,xm},以及相关的输出yi
  2. 计算梯度和误差,更新r,再根据r和梯度计算参数更新量

在SGD中,我们对所有参数进行同时更新,这些参数都使用同样的学习速率。

优点:
能够实现学习率的自动更改。如果这次梯度大,那么学习速率衰减的就快一些;如果这次梯度小,那么学习速率衰减的就慢一些。

缺点:
最大的缺点在于分母中那个G是偏导的累积,随着时间的推移,分母会不断的变大,最后会使得学习速率变的非常小,而此时会使得模型不再具备学习其他知识的能力。
经验表明,在普通算法中也许效果不错,但在深度学习中,深度过深时会造成训练提前结束。因为它到后面的衰减可能越来越慢,然后就提前结束了。为了解决提前结束的问题,引入了如下的算法:Adadelta!RMSprop!


Adadelta

adadelta是adagrad的延伸,不同于adadelta将以前所有的偏导都累加起来,adadelta控制了累加的范围到一定的窗口中。但是,并非简单的将窗口大小设置并且存储,我们是通过下式动态改变的上述的G:
在这里插入图片描述
这里面的gamma类似于momentum里面的项(通常取值0.9),用来控制更新的权重。
因此以前的:
在这里插入图片描述
将被改变为:
在这里插入图片描述


Adam

Adam(Adaptive Moment Estimation)是另外一种给每个参数计算不同更新速率的方法,其本质上是带有动量项的RMSprop,它利用梯度的一阶矩估计和二阶矩估计动态调整每个参数的学习率。Adam的优点主要在于经过偏置校正后,每一次迭代学习率都有个确定范围,使得参数比较平稳。它和上述的adadelta和RMSprop一样,都存储了以前的偏导平方衰减平均值,此外,它还存储以前的偏导衰减平均值。

具体实现:
需要:步进值 ϵ, 初始参数 θ, 数值稳定量δ,一阶动量衰减系数ρ1, 二阶动量衰减系数ρ2
其中几个取值一般为:δ=10−8,ρ1=0.9,ρ2=0.999
中间变量:一阶动量s,二阶动量r,都初始化为0
每步迭代过程:

  1. 从训练集中的随机抽取一批容量为m的样本{x1,…,xm},以及相关的输出yi
  2. 计算梯度和误差,更新r和s,再根据r和s以及梯度计算参数更新量
    在这里插入图片描述
    其中的Mt和Vt分别表示平均值角度和非中心方差角度的偏导。
    在这里插入图片描述
    此方法的作者建议 β1取0.9, β2取0.999 ,ϵ取10-8。
    并且声称Adam在实践中比其他的自适应算法有更好的表现

1.2、可视化

让我们来可视化的看看它们的表现:
比较一下速度:
在这里插入图片描述
比较一下在鞍点的性能:
在这里插入图片描述

1.3、如何选择

  • 如果你的数据很稀疏,那应该选择有自适应性的优化函数。并且你还可以减少调参的时间,用默认参数取得好的结果。
  • Kingma【Kingma, D. P., & Ba, J. L. (2015). Adam: a Method for Stochastic Optimization. International Conference on Learning Representations, 1–13.】的实验表示,偏差校正使得Adam在优化到后面梯度变的稀疏的时候使得其优化性能最好。
    可能Adam是最好的优化函数。
    如果你希望你的训练能变的更快,或者你要训练的是一个复杂的深度的网络,尽量选择自适应的优化函数。

注: 优化器这一部分参考了樱夕夕的博客


二、Adam()简介

举例:

class torch.optim.Adam(params, lr=0.001, betas=(0.9, 0.999), eps=1e-08, weight_decay=0, amsgrad=False)

参数:

  1. params (iterable):需要优化的网络参数,传进来的网络参数必须是Iterable(官网对这个参数用法讲的不太清楚,下面有例子清楚的说明param具体用法)。
    1. 如果优化一个网络,网络的每一层看做一个parameter group,一整个网络就是parameter groups(一般给赋值为net.parameters()),补充一点,net.parameters()函数返回的parameter groups实际上是一个变成了generator的字典;
    2. 如果同时优化多个网络,有两种方法:
      1. 将多个网络的参数合并到一起,当成一个网络的参数来优化(一般赋值为[*net_1.parameters(), *net_2.parameters(), …, *net_n.parameters()]或itertools.chain(net_1.parameters(), net_2.parameters(), …, net_n.parameters());
      2. 当成多个网络优化,这样可以很容易的让多个网络的学习率各不相同(一般赋值为
        [{ params': net_1.parameters()}, { params': net_2.parameters()}, …, { params': net_n.parameters()})。
  2. lr (float, optional):学习率;
  3. betas (Tuple[float, float], optional) – 用于计算梯度的运行平均值,及其平方的系数 (default: (0.9, 0.999));
  4. eps (float, optional) – 可以提高数值的稳定性 (default: 1e-8);
  5. weight_decay (float, optional) – 权重衰减(L2惩罚) (default: 0);
  6. amsgrad (boolean, optional) 是否使用该算法的AMSGrad变体 (default: False)。

属性:

optimizer.defaults: 字典,存放这个优化器的一些初始参数,有:‘lr’, ‘betas’, ‘eps’, ‘weight_decay’, ‘amsgrad’。事实上这个属性继承自torch.optim.Optimizer父类;
optimizer.param_groups:列表,每个元素都是一个字典,每个元素包含的关键字有:‘params’, ‘lr’, ‘betas’, ‘eps’, ‘weight_decay’, ‘amsgrad’,params类是各个网络的参数放在了一起。这个属性也继承自torch.optim.Optimizer父类。
由于上述两个属性都继承自所有优化器共同的基类,所以是所有优化器类都有的属性,并且两者字典中键名相同的元素值也相同(经过lr_scheduler后lr就不同了)。
下面是用法示例(三种参数上传方法对比):

import torch
import torch.nn as nn
from torch.optim.lr_scheduler import LambdaLR
import itertools


initial_lr = 0.1

class model(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(in_channels=3, out_channels=3, kernel_size=3)
        self.conv2 = nn.Conv2d(in_channels=3, out_channels=3, kernel_size=3)

    def forward(self, x):
        pass

net_1 = model()
net_2 = model()

optimizer_1 = torch.optim.Adam(net_1.parameters(), lr = initial_lr)
print("******************optimizer_1*********************")
print("optimizer_1.defaults:", optimizer_1.defaults)
print("optimizer_1.param_groups长度:", len(optimizer_1.param_groups))
print("optimizer_1.param_groups一个元素包含的键:", optimizer_1.param_groups[0].keys())
print()

optimizer_2 = torch.optim.Adam([*net_1.parameters(), *net_2.parameters()], lr = initial_lr)
# optimizer_2 = torch.opotim.Adam(itertools.chain(net_1.parameters(), net_2.parameters())) # 和上一行作用相同
print("******************optimizer_2*********************")
print("optimizer_2.defaults:", optimizer_2.defaults)
print("optimizer_2.param_groups长度:", len(optimizer_2.param_groups))
print("optimizer_2.param_groups一个元素包含的键:", optimizer_2.param_groups[0].keys())
print()

optimizer_3 = torch.optim.Adam([{
    
    "params": net_1.parameters()}, {
    
    "params": net_2.parameters()}], lr = initial_lr)
print("******************optimizer_3*********************")
print("optimizer_3.defaults:", optimizer_3.defaults)
print("optimizer_3.param_groups长度:", len(optimizer_3.param_groups))
print("optimizer_3.param_groups一个元素包含的键:", optimizer_3.param_groups[0].keys())

******************optimizer_1*********************
optimizer_1.defaults: {
    
    'lr': 0.1, 'betas': (0.9, 0.999), 'eps': 1e-08, 'weight_decay': 0, 'amsgrad': False}
optimizer_1.param_groups长度: 1
optimizer_1.param_groups一个元素包含的键: dict_keys(['params', 'lr', 'betas', 'eps', 'weight_decay', 'amsgrad'])

******************optimizer_2*********************
optimizer_2.defaults: {
    
    'lr': 0.1, 'betas': (0.9, 0.999), 'eps': 1e-08, 'weight_decay': 0, 'amsgrad': False}
optimizer_2.param_groups长度: 1
optimizer_2.param_groups一个元素包含的键: dict_keys(['params', 'lr', 'betas', 'eps', 'weight_decay', 'amsgrad'])

******************optimizer_3*********************
optimizer_3.defaults: {
    
    'lr': 0.1, 'betas': (0.9, 0.999), 'eps': 1e-08, 'weight_decay': 0, 'amsgrad': False}
optimizer_3.param_groups长度: 2
optimizer_3.param_groups一个元素包含的键: dict_keys(['params', 'lr', 'betas', 'eps', 'weight_decay', 'amsgrad'])

注意:lr_scheduler更新optimizer的lr,是更新的optimizer.param_groups[n][‘lr’],而不是optimizer.defaults[‘lr’]

三、lr_scheduler简介

PyTorch学习率调整策略通过torch.optim.lr_scheduler接口实现。PyTorch提供的学习率调整策略分为三大类,分别是

  1. 有序调整:等间隔调整(Step),按需调整学习率(MultiStep),指数衰减调整(Exponential)和余弦退火CosineAnnealing。
  2. 自适应调整:自适应调整学习率 ReduceLROnPlateau。
  3. 自定义调整:自定义调整学习率 LambdaLR。

学习率的调整应该放在optimizer更新之后,下面是一个参考蓝本:

	scheduler = ...
	for epoch in range(100):
	    train(...)
	     validate(...)
	     scheduler.step()

注意: 在PyTorch 1.1.0之前的版本,学习率的调整应该被放在optimizer更新之前的。如果我们在 1.1.0 及之后的版本仍然将学习率的调整(即 scheduler.step())放在 optimizer’s update(即 optimizer.step())之前,那么 learning rate schedule 的第一个值将会被跳过。所以如果某个代码是在 1.1.0 之前的版本下开发,但现在移植到 1.1.0及之后的版本运行,发现效果变差,需要检查一下是否将scheduler.step()放在了optimizer.step()之前。

3.2 自定义调整:LambdaLR

torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda, last_epoch=-1)

参数:

  • optimizer (Optimizer):要更改学习率的优化器;
  • lr_lambda(function or list):根据epoch计算λ \lambdaλ的函数;或者是一个list的这样的function,分别计算各个parameter groups的学习率更新用到的λ \lambdaλ;
  • last_epoch (int):最后一个epoch的index,如果是训练了很多个epoch后中断了,继续训练,这个值就等于加载的模型的epoch。默认为-1表示从头开始训练,即从epoch=1开始。
import torch
import torch.nn as nn
from torch.optim.lr_scheduler import LambdaLR

initial_lr = 0.1

class model(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(in_channels=3, out_channels=3, kernel_size=3)

    def forward(self, x):
        pass

net_1 = model()

optimizer_1 = torch.optim.Adam(net_1.parameters(), lr = initial_lr)
scheduler_1 = LambdaLR(optimizer_1, lr_lambda=lambda epoch: 1/(epoch+1))

print("初始化的学习率:", optimizer_1.defaults['lr'])

for epoch in range(1, 11):
    # train

    optimizer_1.zero_grad()
    optimizer_1.step()
    print("第%d个epoch的学习率:%f" % (epoch, optimizer_1.param_groups[0]['lr']))
    scheduler_1.step()

3.2 lr_scheduler调整策略:根据训练次数

torch.optim.lr_scheduler中大部分调整学习率的方法都是根据epoch训练次数,这里介绍常见的几种方法,其他方法以后用到再补充。要了解每个类的更新策略,可直接查看官网doc中的源码,每类都有个get_lr方法,定义了更新策略.

3.2.1 StepLR(固定间隔)

cclass torch.optim.lr_scheduler.StepLR(optimizer, step_size, gamma=0.1, last_epoch=-1)

功能: 等间隔调整学习率,调整倍数为gamma倍,调整间隔为step_size。间隔单位是step。需要注意的是,step通常是指epoch。
使用举例:

import torch
import torch.optim as optim
from torch.optim import lr_scheduler
from torchvision.models import AlexNet
import matplotlib.pyplot as plt


model = AlexNet(num_classes=2)
optimizer = optim.SGD(params=model.parameters(), lr=0.01)

# lr_scheduler.StepLR()
# Assuming optimizer uses lr = 0.05 for all groups
# lr = 0.05     if epoch < 30
# lr = 0.005    if 30 <= epoch < 60
# lr = 0.0005   if 60 <= epoch < 90

scheduler = lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)
plt.figure()
x = list(range(100))
y = []
for epoch in range(100):
    scheduler.step()
    lr = scheduler.get_lr()
    print(epoch, scheduler.get_lr()[0])
    y.append(scheduler.get_lr()[0])
plt.xlabel("epoch")
plt.ylabel("learning rate")
plt.plot(x, y)

在这里插入图片描述

3.2.2 MultiStepLR(自定义间隔)

StepLR的区别是,调节的epoch是自己定义,无须一定是【30, 60, 90】 这种等差数列;
请注意,这种衰减是由外部的设置来更改的。 当last_epoch=-1时,将初始LR设置为LR。

torch.optim.lr_scheduler.MultiStepLR(optimizer, milestones, gamma=0.1, last_epoch=-1)

optimizer(Optimizer):优化器
milestones (list):lr改变时的epoch数目,一定是上升的,如【30,80】,就在第30个和第80个epcho进行改变
gamma(float):学习率调整倍数,默认为0.1倍,即下降10倍。
last_—epoch (int):最后一个epoch的index,如果是训练了很多个epoch后中断了,继续训练,这个值就等于加载的模型的epoch。默认为-1表示从头开始训练,即从epoch=1开始。

3.2.3 ExponentialLR(指数形式衰减)

torch.optim.lr_scheduler.ExponentialLR(optimizer, gamma, last_epoch=-1)

在这里插入图片描述

3.2.4 CosineAnnealingLR(余弦形式衰减)

torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max, eta_min=0, last_epoch=-1)
  • 参数
  1. Optimizer:要更改学习率的优化器

  2. T_max:因为学习率是周期性变化的,该参数表示周期的1/2,也就是说,初始的学习率为a,经过2*T_max时间后,学习率经过了一个周期变化后还是a。在实验中,将T_max的值设定为总的训练epoch,其学习率的变化如下图所示:
    在这里插入图片描述

  3. eta_min:表示学习率可变化的最小值,默认为0

  4. 有时候会因为不可抗拒的外界因素导致训练被中断,在pytorch中恢复训练的方法就是把最近保存的模型重新加载,然后重新训练即可。假设我们从epoch10开始恢复训练,可以利用lr_scheduler的last_epoch参数,last_epoch指定为10,则网络会从第11个epoch开始接着训练。

import torch
from torchvision.models import AlexNet
from torch.optim.lr_scheduler import CosineAnnealingLR
import matplotlib.pyplot as plt
%matplotlib inline

model = AlexNet(num_classes=2)
optimizer = torch.optim.Adam([{
    
    'params': model.parameters(), 'initial_lr': 1e-3}], lr=1e-3)
#eta_min最小的学习率
epochs = 500
scheduler =torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs, eta_min=1e-6, last_epoch=-1) 
# scheduler = torch.optim.lr_scheduler.CosineAnnealingWarmRestarts(optimizer,T_0=100,T_mult=2,eta_min=1e-6)
plt.figure()
x = list(range(epochs))
y = []
for epoch in range(epochs):
    optimizer.zero_grad()
    optimizer.step()
#     print("第%d个epoch的学习率:%f" % (epoch,optimizer.param_groups[0]['lr']))
    scheduler.step()
    y.append(scheduler.get_lr()[0])

# 画出lr的变化    
plt.plot(x, y)
plt.xlabel("epoch")
plt.ylabel("lr")
plt.title("learning rate's curve changes as epoch goes on!")

3.3 其他策略

3.3.3 poly

poly学习率变化策略公式如下:(据说很好用)
在这里插入图片描述
其中lr 为初始学习率,iter为当前迭代的step数,max_iter为训练过程中总的迭代步数。

猜你喜欢

转载自blog.csdn.net/Gw2092330995/article/details/127055767