用 fastai 解释什么是 one-cycle-policy

本贴最后更新于 1832 天前,其中的信息可能已经时移世易

fit one cycle

"Manage 1-Cycle style training as outlined in Leslie Smith's paper."

what is one-cycle-policy?

简单来说,one-cycle-policy, 使用的是一种周期性学习率,从较小的学习率开始学习,缓慢提高至较高的学习率,然后再慢慢下降,周而复始,每个周期的长度略微缩短,在训练的最后部分,学习率比之前的最小值降得更低。这不仅可以加速训练,还有助于防止模型落入损失平面的陡峭区域,使模型更倾向于寻找更平坦部分的极小值,从而缓解过拟合现象。

one cycle 3 步

One-Cycle-Policy 大概有三个步骤:

  1. 我们逐渐将学习率从 lr_max / div_factor 提高到 lr_max,同时我们逐渐减少从 mom_max 到 mom_min 的动量(momentum)。

  2. 反向再做一次:我们逐渐将学习率从 lr_max 降低到 lr_max / div_factor,同时我们逐渐增加从 mom_min 到 mom_max 的动量。

  3. 我们进一步将学习率从 lr_max / div_factor 降低到 lr_max /(div_factor x 100),我们保持动力稳定在 mom_max。

我们来分别简单说明一下:

慢启动的想法并不新鲜:通常使用较低的值来预热训练,这正是 one-cycle 第一步实现的目标。Leslie 不建议直接切换到更高的值,而是线性提高的最大值。

他在实验过程中观察到的是,在 Cycle 中期,高学习率将作为正则化的角色,并使 NN 不会过拟合。它们将阻止模型落在损失函数的陡峭区域,而是找到更平坦的最小值。他在另一篇论文中解释了通过使用这一政策,Hessian 的近似值较低,表明 SGD 正在寻找更宽的平坦区域。

然后训练的最后一部分,学习率下降直到消失,将允许我们得到更平滑的部分内的局部最小值。在高学习率的同时,我们没有看到 loss 或 acc 的显着改善,并且验证集的 loss 有时非常高。但是当我们最终在最后降低学习率时,我们会发现这是很有好处的。

reference

具体的技术细节,可以参考 Leslie 的论文。本文接下来的实验脚本都在 Cyclical LR and momentums.ipynb

建议大家直接在 colab 运行这个脚本,在 chrome 中安装 open-in-colab extension,一键打开。

本文参考 Sylvain Gugger 的帖子 The 1cycle policy,稍微会有些不同。

插播一句:Dropout 和 BN。

Dropout 和 BN 在实际使用中,都会被提到加速训练,blabla...那会有什么缺点吗。

一位神秘大佬有一天这样教育菜鸟:

Dropout 的缺点
dropout 的那个是因为给 BP 回来的 gradient 加了一个很大的 variance, 虽然 sgd 训练可以收敛, 但是由于方差太大,performance 会变差。加了 dropout 之后,估计的 gradient 贼差,虽然还是无偏估计,但是 variance 就不可控了。
对卷积层的正则化效果一般

分割线

BN 的缺点
BN 的是因为改变了 loss,所以加了 BN 之后的网络跟原网络的最优解不是同一个,而且 BN 之后的网络的 loss 会跟 batchsize 强相关, 这就是不稳定的原因。
其实 BN 的缺点在 kaiming he 的 GN 的那个 paper 里面已经说了,但是是从实验上说明的。
只需要进一步的抽象一下,就知道了实验效果的差别是因为 loss 的差别。

身为 PackageMan 的我,赶紧掏出小本本记录。

对于其中的方差偏移问题(variance shift),具体可以参考:知乎专栏
大白话《Understanding the Disharmony between Dropout and Batch Normalization by Variance Shift》和机器之心如何通过方差偏移理解批归一化与 Dropout 之间的冲突

fastai 中代码位置

fastai/callbacks/one_cycle.py

由于 one_cycle 只是用来帮助加快训练的,但是第一步还是需要找到一个初始的学习率,保证模型的 loss 在最初是一个较好的状态。我们通过实验来讲解代码。

实验

实验前准备

数据

我们使用 cifar10 作为实验数据。


from fastai.vision import *
# 下载数据并解压
path = untar_data(URLs.CIFAR); path
classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

stats = (np.array([ 0.4914 , 0.48216, 0.44653]), np.array([ 0.24703, 0.24349, 0.26159]))

size = 32
batch_size = 512  # 如果性能不好,请调低到2^n,比如16,64

def  get_data(bs):
    ds_tfms = ([*rand_pad(4, 32), flip_lr(p=0.5)], [])
    data = ImageDataBunch.from_folder(path, valid='test', classes=classes,ds_tfms=ds_tfms, bs=bs).normalize(cifar_stats)
    return data

data = get_data(batch_size)

ResNet

在 Sylvain 的示例中,定义了 ResNet 和 BasicBlock,但我这边建议,跟随最新的 pytorch,避免在最新的 fastai 和 pytorch 使用中报错。

def conv3x3(in_planes, out_planes, stride=1):
    """3x3 convolution with padding"""
    return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,
                     padding=1, bias=False)


def conv1x1(in_planes, out_planes, stride=1):
    """1x1 convolution"""
    return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False)
  
class BasicBlock(nn.Module):
    expansion = 1

    def __init__(self, inplanes, planes, stride=1, downsample=None):
        super(BasicBlock, self).__init__()
        # Both self.conv1 and self.downsample layers downsample the input when stride != 1
        self.conv1 = conv3x3(inplanes, planes, stride)
        self.bn1 = nn.BatchNorm2d(planes)
        self.relu = nn.ReLU(inplace=True)
        self.conv2 = conv3x3(planes, planes)
        self.bn2 = nn.BatchNorm2d(planes)
        self.downsample = downsample
        self.stride = stride

    def forward(self, x):
        identity = x

        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)

        if self.downsample is not None:
            identity = self.downsample(x)

        out += identity
        out = self.relu(out)

        return out

需要注意的是,在 fastai==1.0.48 中,构建 cnn 的函数直接变成了 cnn_learner,同时要求 bash_arch 参数是 Callable,因此我们需要一定的变化。具体参数查看 help(cnn_learner.

Have a look!

参考 fastai 中 resnet18 的写法

def resnet18(pretrained=False, **kwargs):
    """Constructs a ResNet-18 model.
    Args:
        pretrained (bool): If True, returns a model pre-trained on ImageNet
    """
    model = ResNet(BasicBlock, [2, 2, 2, 2], **kwargs)
    if pretrained:
        model.load_state_dict(model_zoo.load_url(model_urls['resnet18']))
    return model

reference: conv2d TypeError

同时,我们不自己写 ResNet,而是用 fastai 直接 import 的 pytorch 的定义,保证稳定~~


def get_your_model_arch(pretrained=False, **kwargs):
    model_arch = models.ResNet(BasicBlock, [9,9,9,1])
    return model_arch
model_arch = get_your_model_arch
# model2 = models.resnet18() # get exception
model2 = models.resnet18

learn = cnn_learner(data, base_arch=model_arch, metrics=metrics.accuracy)
learn.crit = F.nll_loss

到目前为止,一个 CNN 就构建好了,大家可以根据 ResNet 的参数,算一下这个是几层。

lr_find(), 找到你的初始学习率

定义好 learner 之后,如果要使用 one-cycle-policy,那首先我们需要一个最佳的学习率。在 fastai 中,直接使用 learn.lr_find 就能找到。

找到最佳学习率的思路参考:How Do You Find A Good Learning Rate


learn.lr_find(wd=1e-4,end_lr=100)

findlr.png

我们从图中可以看到,学习率可以设置为 0.08,初始的 loss 大概在 2.6.

实验

One-Cycle-Policy 大概有三个步骤:

  1. 我们逐渐将学习率从 lr_max / div_factor 提高到 lr_max,同时我们逐渐减少从 mom_max 到 mom_min 的动量(momentum)。

  2. 反向再做一次:我们逐渐将学习率从 lr_max 降低到 lr_max / div_factor,同时我们逐渐增加从 mom_min 到 mom_max 的动量。

  3. 我们进一步将学习率从 lr_max / div_factor 降低到 lr_max /(div_factor x 100),我们保持动力稳定在 mom_max。


learn.recorder.plot_lr(show_moms=True)

大概得到如下图像:

onecycleparams.png

实验 1

最大学习率: 0.08, 95 轮 one_cycle, weight decays:1e-4.

在旧版本中,是 use_clr_beta. 新版本中参数进行了修改。

  • div_factor:10, pick 1/10th of the maximum learning rate for the minimum learning rate

  • pct_start: 0.1368, dedicate 13.68% of the cycle to the annealing at the end (that's 13 epochs over 95)

  • moms = (0.95, 0.85)

  • maximum momentum 0.95

  • minimum momentum 0.85


cyc_len = 95

learn.fit_one_cycle(cyc_len=cyc_len, max_lr=0.08,div_factor=10, pct_start=0.1368,moms=(0.95, 0.85), wd=1e-4)

exp195lrchanges.pngexp195losses.png
图中可以看到,学习率从 0.08 上升到 0.8,最后下降到了很低的学习率。验证集的 loss 在中期变得不稳定,但在最后降了下来,同时,验证集和训练集的 loss 差并不大。

实验 2,更高的学习率//更小的 cycle


# 修改cyc_len和div_factor, pct_start

learn.fit_one_cycle(cyc_len=50, max_lr=0.8,div_factor=10, pct_start=0.5,moms=(0.95, 0.85), wd=1e-4)

同时,由于 one-cycle 学习率衰减的特性,允许我们使用更大的学习率。但为了保证 loss 不会太大,可能会需要更长的 cycle 来进行

exp2biglr.png
学习率非常高,我们可以更快地学习并防止过度拟合。在我们消除学习率之前,验证损失和训练损失之间的差异仍然很小。这是 Leslie Smith 描述的超收敛现象(super convergence)。

实验 3 不变的动量 momentum


learn.fit_one_cycle(cyc_len=cyc_len, max_lr=0.08,div_factor=10, pct_start=0.1368,moms=(0.9, 0.9), wd=1e-4)

在 Leslie 的实验中,减少动量会带来更好的结果。由此推测,训练时,如果希望 SGD 快速下降到一个新的平坦区域,则新的梯度需要更多的权重。0.85~0.95 是经验上的较好值。实验中,发现如果动量使用常数,在结果上,会和变化的动量在准确率上接近,但 loss 会更大一些。时间上,常数动量会快一些。

其他参数的影响:weight decays

exp5wds.png

(横坐标是学习率,纵坐标是 train_loss)

通过对 wd 的调整,发现不同的 wd 对学习率和 loss 是有很大影响的。这也是为什么在 lr_find(wd=1e-4),需要和 fit_one_cycle(wd=1e-4) 相同。

结语

one-cycle 策略本身还是属于正则化的方法,也可以在模型训练中减少其他正则化方法的使用,因为 one-cycle 本来可能更有效,也允许在更大的初始学习率中训练更长时间。

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...