2

deep learning笔记:学习率衰减与批归一化

 2 years ago
source link: https://gsy00517.github.io/deep-learning20191001151454/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
deep learning笔记:学习率衰减与批归一化

deep learning笔记:学习率衰减与批归一化

发表于 2019-10-01 | 更新于: 2020-02-27 | 分类于 人工智能 | | 阅读次数:
字数统计: 3.7k字 | 阅读时长 ≈ 14分钟

一段时间之前,在一个深度学习交流群里看到一个群友发问:为什么他的训练误差最后疯狂上下抖动而不是一直降低。

作为一个很萌的萌新,我当时也很疑惑。但后来我结合所学,仔细思考之后,发现这是一个挺容易犯的错误。

References

电子文献:
https://blog.csdn.net/bestrivern/article/details/86301619
https://www.jianshu.com/p/9643cba47655
https://www.cnblogs.com/eilearn/p/9780696.html
https://blog.csdn.net/donkey_1993/article/details/81871132
https://www.pytorchtutorial.com/how-to-use-batchnorm/


事实上,这是一个在机器学习中就有可能遇到的问题,当学习速率α设置得过大时,往往在模型训练的后期难以达到最优解,而是在最优解附近来回抖动。还有可能反而使损失函数越来越大,甚至达到无穷,如下图所示。

而在深度学习中,假设我们使用mini-batch梯度下降法,由于mini-batch的数量不大,大概64或者128个样本,在迭代过程中会有噪声。这个时候使用固定的学习率导致的结果就是虽然下降朝向最小值,但不会精确地收敛,只会在附近不断地波动(蓝色线)。

但如果慢慢减少学习率,在初期,学习还是相对较快地,但随着学习率的变小,步伐也会变慢变小,所以最后当开始收敛时,你的曲线(绿色线)会在最小值附近的一个较小区域之内摆动,而不是在训练过程中,大幅度地在最小值附近摆动。

对于这个问题,我目前收集了有下面这些解决办法。


直接修改学习率

在吴恩达的机器学习课程中,他介绍了一种人为选择学习率的规则:每三倍选择一个学习率。
比如:我们首先选择了0.1为学习率,那么当这个学习率过大时,我们修改成0.3。倘若还是偏大,我们继续改为0.01、0.003、0.001…以此类推,当学习率偏小是也是以三倍增加并尝试检验,最终选出比较合适的学习率。
但这种方法只适用于模型数量小的情况,且这种方法终究还是固定的学习率,依旧无法很好地权衡从而达到前期快速下降与后期稳定收敛的目的。


学习率动态衰减

学习率衰减的本质在于,在学习初期,你能承受并且需要较大的步伐,但当开始收敛的时候,小一些的学习率能让你步伐小一些,从而更稳定地达到精确的最优解。
为此,我们另外增添衰减率超参数,构建函数使学习率能够在训练的过程中动态衰减。

α=11+decayrate∗epochnum∗α0 α=11+decayrate∗epochnum∗α0

其中decay rate称为衰减率,epoch num是代数,α0α0是初始学习率。
此外还有下面这些构造方法:
指数衰减:α=0.95epochnum∗α0α=0.95epochnum∗α0
其他常用方法:

α=kepochnum−−−−−−−−√∗α0 α=kepochnum∗α0
α=kt√α0 α=ktα0

其中k为mini-batch的数字。


几种衰减方法的实现

在pytorch中,学习率调整主要有两种方式:
1.直接修改optimizer中的lr参数。
2.利用lr_scheduler()提供的几种衰减函数。即使用torch.optim.lr_scheduler,基于循环的次数提供了一些方法来调节学习率。
3.利用torch.optim.lr_scheduler.ReduceLROnPlateau,基于验证测量结果来设置不同的学习率.
下面提供几种实现方法:
准备(对下列通用):

import torch
from torch.optim import * #包含Adam,lr_scheduler等
import torch.nn as nn

#生成一个简单全连接神经网络
class net(nn.Module):
def __init__(self):
super(net, self).__init__()
self.fc = nn.Linear(1, 10)
def forward(self, x):
return self.fc(x)
  1. 手动阶梯式衰减

    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    for epoch in range(100):
    if epoch % 5 == 0:
    for p in optimizer.param_groups:
    p['lr'] *= 0.9 #学习率超参的位置:optimizer.state_dict()['param_groups'][0]['lr']

    这里是每过5个epoch就进行一次衰减。

  2. lambda自定义衰减

    import numpy as np 
    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    lambda1 = lambda epoch: np.sin(epoch) / epoch
    scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda = lambda1)
    for epoch in range(100):
    scheduler.step()

    lr_lambda会接收到一个int参数:epoch,然后根据epoch计算出对应的lr。如果设置多个lambda函数的话,会分别作用于optimizer中的不同的params_group。

  3. StepLR阶梯式衰减

    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    scheduler = lr_scheduler.StepLR(optimizer, step_size = 5, gamma = 0.8)
    for epoch in range(100):
    scheduler.step()

    每个epoch,lr会自动乘以gamma。

  4. 三段式衰减

    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    scheduler = lr_scheduler.MultiStepLR(optimizer, milestones = [20,80], gamma = 0.9)
    for epoch in range(100):
    scheduler.step()

    这种方法就是,当epoch进入milestones范围内即乘以gamma,离开milestones范围之后再乘以gamma。
    这种衰减方式也是在学术论文中最常见的方式,一般手动调整也会采用这种方法。

  5. model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    scheduler = lr_scheduler.ExponentialLR(optimizer, gamma = 0.9)
    for epoch in range(100):
    scheduler.step()

    这种方法就是在每个epoch中lr都乘以gamma,从而达到连续衰减的效果。

  6. 余弦式调整

    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    scheduler = lr_scheduler.CosineAnnealingLR(optimizer, T_max = 20)
    for epoch in range(100):
    scheduler.step()

    这里的T_max对应1/2个cos周期所对应的epoch数值。

  7. 基于loss和accuracy

    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    scheduler = lr_scheduler.ReduceLROnPlateau(optimizer, mode = 'min', factor = 0.1, patience = 10, verbose = False, threshold = 0.0001, threshold_mode = 'rel', cooldown = 0, min_lr = 0, eps = 1e-08)
    for epoch in range(100):
    scheduler.step()

    当发现loss不再降低或者accuracy不再提高之后,就降低学习率。

    注:上面代码中各参数意义如下:
    mode:’min’模式检测metric是否不再减小,’max’模式检测metric是否不再增大;
    factor:触发条件后lr*=factor;
    patience:不再减小(或增大)的累计次数;
    verbose:触发条件后print;
    threshold:只关注超过阈值的显著变化;
    threshold_mode:有rel和abs两种阈值计算模式,rel规则:max模式下如果超过best(1+threshold)为显著,min模式下如果低于best(1-threshold)为显著;abs规则:max模式下如果超过best+threshold为显著,min模式下如果低于best-threshold为显著;
    cooldown:触发一次条件后,等待一定epoch再进行检测,避免lr下降过速;
    min_lr:最小的允许lr;
    eps:如果新旧lr之间的差异小与1e-8,则忽略此次更新。

这里非常感谢facebook的员工给我们提供了如此多的选择与便利!
对于上述方法如有任何疑惑,还请查阅torch.optim文档


批归一化(Batch Normalization)

除了对学习率进行调整之外,Batch Normalization也可以有效地解决之前的问题。
我是在学习ResNet的时候第一次遇到批归一化这个概念的。随着深度神经网络深度的加深,训练越来越困难,收敛越来越慢。为此,很多论文都尝试解决这个问题,比如ReLU激活函数,再比如Residual Network,而BN本质上也是解释并从某个不同的角度来解决这个问题的。
通过使用Batch Normalization,我们可以加快网络的收敛速度,这样我们就可以使用较大的学习率来训练网络了。此外,BN还提高了网络的泛化能力。
BN的基本思想其实相当直观:
首先,因为深层神经网络在做非线性变换前的激活输入值(就是x=WU+B,U是输入)随着网络深度加深或者在训练过程中,其分布逐渐发生偏移或者变动,之所以训练收敛慢,一般是整体分布逐渐往非线性函数的取值区间的上下限两端靠近(对于Sigmoid函数来说,意味着激活输入值WU+B是大的负值或正值),这就导致了反向传播时低层神经网络的梯度消失,这是训练深层神经网络收敛越来越慢的本质原因。
事实上,神经网络学习过程本质上是为了学习数据的分布,而BN就是通过一定的规范化手段,把每层神经网络任意神经元这个输入值的分布强行拉回到均值为0、方差为1的标准正态分布,其实就是把越来越偏的分布强制拉回比较标准的分布,这样使得激活输入值落在非线性函数对输入比较敏感的区域,这样输入的小变化就会导致损失函数较大的变化,从而让梯度变大,避免梯度消失问题产生,而且梯度变大意味着学习收敛速度快,因此通过BN能大大加快训练速度。
下面来看看BN的具体操作过程:

即以下四个步骤:
1.计算样本均值。
2.计算样本方差。
3.对样本数据进行标准化处理。
4.进行平移和缩放处理。这里引入了γ和β两个参数。通过训练可学习重构的γ和β这两个参数,让我们的网络可以学习恢复出原始网络所要学习的特征分布。
下面是BN层的训练流程:

这里的详细过程如下:
输入:待进入激活函数的变量。
输出:
1.这里的K,在卷积网络中可以看作是卷积核个数,如网络中第n层有64个卷积核,就需要计算64次。

注意:在正向传播时,会使用γ与β使得BN层输出与输入一样。

2.在反向传播时利用γ与β求得梯度从而改变训练权值(变量)。
3.通过不断迭代直到训练结束,求得关于不同层的γ与β。
4.不断遍历训练集中的图片,取出每个batch_size中的γ与β,最后统计每层BN的γ与β各自的和除以图片数量得到平均值,并对其做无偏估计直作为每一层的E[x]与Var[x]。
5.在预测的正向传播时,对测试数据求取γ与β,并使用该层的E[x]与Var[x],通过图中11:所表示的公式计算BN层输出。

注意:在预测时,BN层的输出已经被改变,因此BN层在预测中的作用体现在此处。

上面输入的是待进入激活函数的变量,在残差网络ResNet中,的确也是先经过BN层再用relu函数做非线性处理的。那么,为什么BN层一般用在线性层和卷积层的后面,而不是放在非线性单元即激活函数之后呢?
因为非线性单元的输出分布形状会在训练过程中变化,归一化无法消除他的方差偏移。相反的,全连接和卷积层的输出一般是一个对称、非稀疏的一个分布,更加类似高斯分布,对他们进行归一化会产生更加稳定的分布。
比如,我们对一个高斯分布的数据relu激活,那么小于0的直接就被抑制了,这样得到的结果很难是高斯分布了,这时候再添加一个BN层就很难达到所需的效果。
很多实验证明,BatchNorm只要用了就有效果,所以在一般情况下没有理由不用。但也有相反的情况,比如当每个batch里所有的sample都非常相似的时候,相似到mean和variance都基本为0时,最好不要用BatchNorm。此外如果batch size为1,从原理上来讲,此时用BatchNorm是没有任何意义的。

注意:通常我们在进行Transfer Learning的时候,会冻结之前的网络权重,注意这时候往往也会冻结BatchNorm中训练好的moving averages值。这些moving averages值只适用于以前的旧的数据,对新数据不一定适用。所以最好的方法是在Transfer Learning的时候不要冻结BatchNorm层,让moving averages值重新从新的数据中学习。


批归一化实现

这里还是使用pytorch进行实现。
准备(对下列通用):

import torch
import torch.nn as nn
  1. 2d或3d输入

    # 添加了可学习的仿射变换参数
    m = nn.BatchNorm1d(100)
    # 未添加可学习的仿射变换参数
    m = nn.BatchNorm1d(100, affine = False)
    input = torch.autograd.Variable(torch.randn(20, 100))
    output = m(input)

    我们查看m,可以看到有如下形式:

    BatchNorm1d(100, eps=1e-05, momentum=0.1, affine=False, track_running_stats=True)

    这里解释一下涉及到的参数:
    num_features:来自期望输入的特征数,该期望输入的大小为:batch_size * num_features(* width)
    eps:为保证数值稳定性(分母不能趋近或取0),给分母加上的值,默认为1e-5。
    momentum:计算动态均值和动态方差并进行移动平均所使用的动量,默认为0.1。
    affine:一个布尔值,当设为true时,就给该层添加可学习的仿射变换参数。仿射变换将在后文做简单介绍。
    BatchNorm1d可以有两种输入输出:
    1.输入(N,C),输出(N,C)。
    2.输入(N,C,L),输出(N,C,L)。

  2. 3d或4d输入

    m = nn.BatchNorm2d(100)
    #或者
    m = nn.BatchNorm2d(100, affine = False)
    input = torch.autograd.Variable(torch.randn(20, 100, 35, 45))
    output = m(input)

    BatchNorm2d也可以有两种输入输出:
    1.输入(N,C,L),输出(N,C,L)。
    2.输入(N,C,H,W),输出(N,C,H,W)。

  3. 4d或5d输入

    m = nn.BatchNorm3d(100)
    #或者
    m = nn.BatchNorm3d(100, affine=False)

    BatchNorm3d同样支持两种输入输出:
    1.输入(N,C,H,W),输出(N,C,H,W)。
    2.输入(N,C,D,H,W),输出(N,C,D,H,W)。


这里我简单介绍一下仿射变换的概念,仿射变换(Affine Transformation或Affine Map)是一种二维坐标(x, y)到二维坐标(u, v)的变换,它是另外两种简单变换的叠加,一是线性变换,二是平移变换。同时,仿射变换保持了二维图形的“平直性”、“平行性”和“共线比例不变性”,非共线的三对对应点确定一个唯一的仿射变换。

补充:
共线性:若几个点变换前在一条线上,则仿射变换后仍然在一条线上。
平行性:若两条线变换前平行,则变换后仍然平行。
共线比例不变性:变换前一条线上两条线段的比例,在变换后比例不变。

在二维图像变换中,它的一般表达如下:

可以视为线性变换R和平移变换T的叠加。
另外,仿射变换可以通过一系列的原子变换的复合来实现,包括平移,缩放,翻转,旋转和剪切。因此我们可以将几种简单的变换矩阵相乘来实现仿射变换。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK