深度学习炼丹-超参数设定和网络训练

时间:2022-12-13 07:08:23

前言

所谓超参数,即不是通过学习算法本身学习出来的,需要作者手动调整(可优化参数)的参数(理论上我们也可以设计一个嵌套的学习过程,一个学习算法为另一个学习算法学出最优超参数),卷积神经网络中常见的超参数有: 优化器学习率、训练 Epochs 数、批次大小 batch_size 、输入图像尺寸大小。

一般而言,我们将训练数据分成两个不相交的子集,其中一个用于学习参数,另一个作为验证集,用于估计训练中或训练后的泛化误差,用来更新超参数。

  • 用于学习参数的数据子集通常仍被称为训练集(不要和整个训练过程用到的更大的数据集搞混)。
  • 用于挑选超参数的数据子集被称为验证集(validation set)。

通常,80% 的训练数据用于训练,20% 用于验证。因为验证集是用来 “训练” 超参数的,所以验证集的误差通常会比训练集误差小,验证集会低估泛化误差。完成所有超参数优化后,需要使用测试集估计泛化误差

网络层参数

在设计网络架构的时候,我们通常需要事先指定一些网络架构参数,比如:

  • 卷积层(convlution)参数: 卷积层通道数、卷积核大小、卷积步长。
  • 池化层(pooling)参数: 池化核大小、步长等。
  • 权重参数初始化,常用的初始化方法有 Xavierkaiming 系列;或者使用模型 fintune 初始化模型权重参数。
  • 网络深度(这里特指卷积神经网络 cnn),即 layer 的层数;网络的深度一般决定了网络的表达(抽象)能力,网络越深学习能力越强。
  • 网络宽度,即卷积层通道(channel)的数量,也是滤波器(3 维)的数量;网络宽度越宽,代表这一层网络能学习到更加丰富的特征。

这些参数一般在设计网络架构时就已经确定下来了,参数的取值一般可以参考经典 paper 和一些模型训练的经验总结,比如有以下经验:

使用 3x3 卷积

\(3\times 3\) 卷积层是 cnn 的主流组件,比如提取图像特征的 backbone 网络中,其卷积层的卷积核大小大部分都是 \(3\times 3\)。比如 vgg 和 resnet 系列网络具体参数表如下所示。

深度学习炼丹-超参数设定和网络训练

使用 cbr 组合

cnn 模型中,卷积层(conv)一般后接 bnrelu 层,组成 cbr 套件。模型训练好后,模型推理时的卷积层和其后的 BN 层可以等价转换为一个带 bias 的卷积层(也就是通常所谓的“吸BN”),其原理参考深度学习推理时融合BN,轻松获得约5%的提速

对于 cv 领域的任务,建议无脑用 ReLU 激活函数。

# cbr 组件示例代码
def convbn_relu(in_planes, out_planes, kernel_size, stride, pad, dilation):

    return nn.Sequential(nn.Conv2d(in_planes, out_planes, kernel_size=kernel_size, stride=stride,
                                   padding=dilation if dilation > 1 else pad, dilation=dilation, bias=False),
                         nn.BatchNorm2d(out_planes),
                         nn.ReLU(inplace=True))

尝试不同的权重初始化方法

尝试不同的卷积核权重初始化方式。目前常用的权重初始化方法Xavierkaiming 系列,pytorch 在 torch.nn.init 中提供了常用的初始化方法函数,默认是使用 kaiming 均匀分布函数: nn.init.kaiming_uniform_()

深度学习炼丹-超参数设定和网络训练

下面是一个使用 kaiming_normal_(kaiming正态分布)设置卷积层权重初始化的示例代码。

import torch
import torch.nn as nn

# 定义一个卷积层
conv = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3, stride=1, padding=1)

# 使用He初始化方式设置卷积层的权重
nn.init.kaiming_normal_(conv.weight, mode="fan_out", nonlinearity="relu")

使用不同的卷积层权重初始化方式,会有不同的输出效果。分别使用 xavier_normal_xavier_normal_ 初始化权重,并使一个输入图片经过一层卷积层,其输出效果是不同的,对比图如下所示:

深度学习炼丹-超参数设定和网络训练

图片尺寸与数据增强

1,在显存满足的情况下,一般输入图片尺寸越大,模型精度越高

2,数据增强(图像增强)的策略必须结合具体任务来设计!同时送入到模型训练的数据一定要打乱(shuffle)!

数据增强的手段有多种,常见的如下(除了前三种以外,其他的要慎重考虑):

  • 水平 / 竖直翻转
  • 90°,180°,270° 旋转
  • 翻转 + 旋转(旋转和翻转其实是保证了数据特征的旋转不变性能被模型学习到,卷积层面的方法可以参考论文 ACNet)
  • 亮度,饱和度,对比度的随机变化
  • 随机裁剪(Random Crop)
  • 随机缩放(Random Resize)
  • 加模糊(Blurring)
  • 加高斯噪声(Gaussian Noise)

批量大小 batch size

背景知识

深度学习中经常看到 epoch、 iteration 和 batchsize 这三个名字的区别:

  • batchsize:批大小。在深度学习中,一般采用 SGD 训练,即每次训练在训练集中取 batch_size 个样本训练;
  • iteration:1 个 iteration 等于使用 batch_size 个样本训练一次;
  • epoch:1 个 epoch 等于使用训练集中的全部样本训练一次;

batch size 定义

batch 一般被翻译为批量,设置 batch_size 的目的让模型在训练过程中每次选择批量的数据来进行处理。Batch Size 的直观理解就是一次训练所选取的样本数

batch Size 的大小会影响模型的收敛速度和优化程度。同时其也直接影响到 GPU 内存的使用情况,如果你的 GPU 内存(显存)不大,该数值最好设置小一点,否则会出现显存溢出的错误。

选择合适大小的 batch size

batch size 是所有超参数中最好调的一个,也是应该最早确定下来的超参数,其设置的原则就是,batch size 别太小,也别太大,取中间合适值为宜,通常最好是 2 的 n 次方,如 16, 32, 64, 128。在常见的 setting(~100 epochs),batch size 一般不会低于 16

batch size 太小和太大的问题:

  • batch size 太小:每次计算的梯度不稳定,引起训练的震荡比较大,很难收敛。
  • batch size 太大: 深度学习的优化(training loss降不下去)和泛化(generalization gap很大)都会出问题。

设置为 2 的 n 次方的原因:计算机的 gpu 和 cpu 的 memory 都是 2 进制方式存储的,设置 2 的 n 次方可以加快计算速度。

合适的 batch size 范围和训练数据规模、神经网络层数、单元数都没有显著的关系。

合适的 batch size 范围主要和收敛速度、随机梯度噪音有关。

参考知乎问答-怎么选取训练神经网络时的Batch size?

学习率参数设定

什么是学习率

模型架构和数据集构建好后,就可以训练模型了,模型训练的一个最关键超参数是是模型学习率(learning rate),一个理想的学习率会促进模型收敛,而不理想的学习率甚至会直接导致模型直接目标函数损失值 “爆炸” 无法完成训练。

学习率(learning rate)决定目标函数能否收敛到局部最小值,以及何时收敛到最小值。学习率越低,损失函数的变化速度就越慢。虽然使用低学习率可以确保我们不会错过任何局部极小值,但也意味着我们将花费更长的时间来进行收敛,特别是在被困在高原区域的情况下。下述公式表示了上面所说的这种关系。

new_weight = existing_weight — learning_rate * gradient

深度学习炼丹-超参数设定和网络训练

图中公式是随机梯度下降法(SGD)的公式,\(\alpha\)学习率参数\(\theta\) 是待更新的权重参数。

采用小学习速率(顶部)和大学习速率(底部)的梯度下降。来源:Coursera 上吴恩达(Andrew Ng)的机器学习课程。

如何设置学习率

训练 CNN 模型,如何设置学习率有两个原则可以遵守:

  1. 模型训练开始时的初始学习率不宜过大cv 类模型以 0.010.001 为宜;
  2. 模型训练过程中,学习率应随轮数(epochs)增加而衰减

除以上固定规则的方式之外,还有些经验可以参考:

  1. 对于图像分类任务,使用 finetune 方式训练模型,训练过程中,冻结层的不需要过多改变参数,因此需要设置较小的学习率,更改过的分类层则需要以较大的步子去收敛,学习率往往要设置大一点。(来源-pytorch 动态调整学习率
  2. 寻找理想学习率或诊断模型训练学习率是否合适时也可借助模型训练曲线(learning curve)的帮助。学习率的好坏和损失函数大小的关系如下图所示。

深度学习炼丹-超参数设定和网络训练

以上是理论分析,但在实际应用中,以 pytorch 框架为例,pyTorch 提供了六种学习率调整方法,可分为三大类,分别是:

  1. 有序调整: 按照一定规律有序进行调整,这一类是最常用的,分别是等间隔下降(Step),
    按需设定下降间隔(MultiStep),指数下降(Exponential)和 CosineAnnealing。这四种方法的调整时机都是人为可控的,也是训练时常用到的。
  2. 自适应调整: 如依据训练状况伺机调整 ReduceLROnPlateau 方法。该法通过监测某一指标的变化情况,当该指标不再怎么变化的时候,就是调整学习率的时机,因而属于自适应的调整。
  3. 自定义调整: 自定义调整 Lambda。Lambda 方法提供的调整策略十分灵活,我们可以为不同的层设定不同的学习率调整方法,这在 fine-tune 中十分有用,我们不仅可为不同的层设定不同的学习率,还可以为其设定不同的学习率调整策略,简直不能更棒了!

常见的学习率调整方法有:

  • lr_scheduler.StepLR: 等间隔调整学习率。调整倍数为 gamma 倍,调整间隔为 step_size
  • lr_scheduler.MultiStepLR: 按设定的间隔调整学习率。适合后期使用,通过观察 loss 曲线,手动定制学习率调整时机。
  • lr_scheduler.ExponentialLR: 按指数衰减调整学习率,调整公式: \(lr = lr * gamma^{epoch}\)
  • lr_scheduler.CosineAnnealingLR: 以余弦函数为周期,并在每个周期最大值时重新设置学习率。
  • lr_scheduler.ReduceLROnPlateau: 当某指标不再变化(下降或升高),调整学习率(非常实用的策略)。
  • lr_scheduler.LambdaLR: 为不同参数组设定不同学习率调整策略。

学习率调整方法类的详细参数及类方法定义,请参考 pytorch 官方库文档-torch.optim

注意,PyTorch 1.1.0 之后版本,学习率调整策略的设定必须放在优化器设定的后面! 构建一个优化器,首先需要为它指定一个待优化的参数的可迭代对象,然后设置特定于优化器的选项,比如学习率、权重衰减策略等。

在 PyTorch 1.1.0 之前,学习率调度器应该在优化器更新之前被调用;1.1.0 以打破 BC 的方式改变了这种行为。 如果在优化器更新(调用 optimizer.step())之前使用学习率调度器(调用 scheduler.step()),这将跳过学习率调度的第一个值。

使用指数级衰减的学习率调整策略的模板代码如下。

import torchvision.models as models
import torch.optim as optim
model = models.resnet50(pretrained=False)

optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9) # 构建优化器,lr 是初始学习率
scheduler = optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.9) # 设定学习率调整策略

for epoch in range(20):
    for input, target in dataset:
        optimizer.zero_grad()
        output = model(input)
        loss = loss_fn(output, target)
        loss.backward()
        optimizer.step()
    scheduler.step()
    print_lr(is_verbose=true) # pytorch 新版本可用,1.4及以下版本不可用

优化器选择

优化器定义

优化器(优化算法)优化的是神经元参数的取值 \((w、b)\)。优化过程如下:假设 \(\theta\) 表示神经网络中的参数,\(J(\theta)\) 表示在给定的参数取值下,训练数据集上损失函数的大小(包括正则化项),则优化过程即为寻找某一参数 \(\theta\),使得损失函数 \(J(\theta)\) 最小。

在完成数据预处理、数据增强,模型构建和损失函数确定之后,深度学习任务的数学模型也就确定下来了,接下来自然就是选择一个合适的优化器(Optimizer)对该深度学习模型进行优化(优化器选择好后,选择合适的学习率调整策略也很重要)。

如何选择适合不同ml项目的优化器

选择优化器的问题在于没有一个可以解决所有问题的单一优化器。实际上,优化器的性能高度依赖于设置。所以,优化器选择的本质其实是: 哪种优化器最适合自身项目的特点?

深度卷积神经网络通常采用随机梯度下降类型的优化算法进行模型训练和参数求解。最为常用且经典的优化器算法是 (基于动量的)随机梯度下降法 SGD(stochastic gradient descent)Adam 法,其他常见的优化器算法有 Nesterov 型动量随机下降法、Adagrad 法、Adadelta 法、RMSProp 法。

优化器的选择虽然没有通用的准则,但是也还是有些经验可以总结的:

  • SGD 是最常见的神经网络优化方法,收敛效果较稳定,但是收敛速度过慢。
  • Adam 等自适应学习率算法对于稀疏数据具有优势,且且收敛速度很快,但是收敛效果不稳定(容易跳过全局最优解)。

下表 1 概述了几种优化器的优缺点,通过下表可以尝试找到与数据集特征、训练设置和项目目标相匹配的优化器。

某些优化器在具有稀疏特征的数据上表现出色,而其他优化器在将模型应用于以前未见过的数据时可能表现更好。一些优化器在大批量(batch_size 设置较大)下工作得很好,而而另一些优化器会在泛化不佳的情况下收敛到极小的最小值。

深度学习炼丹-超参数设定和网络训练

来源 Which Optimizer should I use for my ML Project?

网络上有种 tricks 是将 SGDAdam 组合使用,先用 Adam 快速下降,再用 SGD 调优。但是这种策略也面临两个问题: 什么时候切换优化器和切换后的 SGD 优化器使用什么样的学习率?论文 SWATS Improving Generalization Performance by Switching from Adam to SGD给出了答案,感兴趣的读者可以深入阅读下 paper

PyTorch 中的优化器

Pytorch 框架为例,PyTorch 中所有的优化器(如: optim.Adadelta、optim.SGD、optim.RMSprop 等)均是 Optimizer 的子类,Optimizer 中也定义了一些常用的方法:

  • zero_grad(): 将梯度清零。
  • step(closure): 执行一步权值更新, 其中可传入参数 closure(一个闭包)。
  • state_dict(): 获取模型当前的参数,以一个有序字典形式返回,key 是各层参数名,value 就是参数。
  • load_state_dict(state_dict): 将 state_dict 中的参数加载到当前网络,常用于模型 finetune
  • add_param_group(param_group): 给 optimizer 管理的参数组中增加一组参数,可为该组参数定制 lr, momentum, weight_decay 等,在 finetune 中常用。

优化器设置和使用的模板代码如下:

# optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
# 指定每一层的学习率
optim.SGD([
            {'params': model.base.parameters()},
            {'params': model.classifier.parameters(), 'lr': 1e-3}
          ], lr=1e-2, momentum=0.9
        )
for input, target in dataset:
    optimizer.zero_grad()
    output = model(input)
    loss = loss_fn(output, target)
    loss.backward()
    optimizer.step()

优化器的算法原理可以参考花书第八章内容,Pytorch 框架优化器类的详细参数及类方法定义,请参考 pytorch 官方库文档-torch.optim

模型 finetune

一般情况下,我们在做深度学习任务时,backbone 一般会用 imagenet 的预训练模型的权值参数作为我们自定义模型的初始化参数,这个过程称为 finetune,更广泛的称之为迁移学习。fintune 的本质其实就是,让我们有一个较好的权重初始化值。模型 finetune 一般步骤如下:

  • 获取预训练模型的权重(如 imagenet );
  • 使用预训练模型权重初始化我们的模型,即加载预训练模型权重参数。

模型 finetune 一般有两种情况:

  1. 直接使用 imagenetresnet50 预训练模型权重当作特征提取器,只改变模型最后的全连接层。代码示例如下:
import torch
import torchvision.models as models

model = models.resnet50(pretrained=True)

#遍历每一个参数,将其设置为不更新参数,即冻结模型所有参数
for param in model.parameters():
    param.requires_grad = False

# 只替换最后的全连接层, 改为训练10类,全连接层requires_grad为True
model.fc = nn.Linear(2048, 10)

print(model.fc) # 这里打印下全连接层的信息
# 输出结果: Linear(in_features=2048, out_features=10, bias=True)
  1. 使用自定义模型结构,保存某次训练好后比较好的权重用于后续训练 finetune,可节省模型训练时间,示例代码如下:
import torch
import torchvision.models as models

# 1,保存模型权重参数值
# 假设已经创建了一个 net = Net(),并且经过训练,通过以下方式保存模型权重值
torch.save(net.state_dict(), 'net_params.pth')
# 2,加载模型权重文件
# load(): Loads an object saved with :func:`torch.save` from a file
pretrained_dict = torch.load('net_params.pth')
# 3,初始化模型
net = Net() # 创建 net
net_state_dict = net.state_dict() # 获取已创建 net 的 state_dict
# (可选)将 pretrained_dict 里不属于 net_state_dict 的键剔除掉:
pretrained_dict_new = {k: v for k, v in pretrained_dict.items() if k in net_state_dict}
# 用预训练模型的参数字典 对 新模型的参数字典 net_state_dict 进行更新
net_state_dict.update(pretrained_dict_new)
# 加载需要的预训练模型参数字典
net.load_state_dict(net_state_dict)

更进一步的模型 finetune,可以为不同网络层设置不同的学习率,请参考《PyTorch_tutorial_0.0.5_余霆嵩》第二章。

模型 finetune 是属于迁移学习技术的一种。

模型可视化

可视化权重

可视化激活

参考资料