论文阅读《Partial Is Better Than All: Revisiting Fine-tuning Strategy for Few-shot Learning》

Background & Motivation

基于元学习、度量学习或者迁移学习的方法大多数都依赖在 base 类上训练的 pre-trained knowledge,之后的做法都大同小异:冻结 backbone,利用 Support Set 直接微调或者利用度量函数来微调网络的 Head 部分。

Motivation 是迁移学习中直接将 base 类数据训练出的网络参数迁移到 novel 类不是最优的办法,因为可能会存在“偏见”或者“阻碍”对 novel 类的检测/分类。尽管 base 类和 novel 类的数据域特征差别没有很大,但还是很可能影响精度。因此提出了 Partial Transfer(P-Transfer)部分迁移的范式。Meta-SGD 和 MAML++ 也对不同层的网络采用了不同的学习率,但都是基于 MAML。

Partial Transfer

如果在预训练模型上不冻结网络,使用 novel 类数据微调网络中的所有层,精度会下降。因此 Partial Transfer 是通过冻结或微调预训练模型中的特定层来实现转移部分知识。

目标函数是找到使迁移学习精度达到最高的参数学习率:

Vlr 是微调阶段网络中每一层的学习率,W 是网络的参数。Partial Transfer 包含三步:基类预训练、进化搜索和部分迁移:

在预训练模型上通过算法在 Support Set 上找到最佳的学习率(只找不改变模型参数),再在 backbone 上用这个学习率在 Query Set 上对模型完成迁移学习,重点在第二步进化搜索。随着网络深度的加深,其搜索空间也不断增大:

其中 m 是簇中学习率的个数,K 是网络的层数。采用的方法是遗传算法,包括繁殖、交叉和突变阶段:

遗传算法的目标是在既定的区间内找出函数的最大值。函数从图像上具体表现为,极大值像是一座座山峰,极小值则是像一座座山谷。因此,我们也可以把遗传算法的过程看作是一个在多元函数里面求最优解的过程。

这些山峰对应着局部最优解,其中有一个山峰是海拔最高的,这个山峰则对应的是全局最优解。那么,遗传算法要做的就是尽量爬到最高峰,而不是困在较低的小山峰上(如果问题求解是最小值,那么要做的就是尽量走到最低谷,道理是一样的)。

既然我们把函数曲线理解成一个一个山峰和山谷组成的山脉。那么我们可以设想所得到的每一个解就是一只袋鼠,我们希望它们不断的向着更高处跳去,直到跳到最高的山峰。

而遗传算法可以理解为:有很多袋鼠,它们降落到喜玛拉雅山脉的任意地方。这些袋鼠并不知道它们的任务是寻找珠穆朗玛峰。但每过几年,就在一些海拔高度较低的地方射杀一些袋鼠。于是,不断有袋鼠死于海拔较低的地方,而越是在海拔高的袋鼠越是能活得更久,也越有机会生儿育女。就这样经过许多年,这些袋鼠们竟然都不自觉地聚拢到了一个个的山峰上,可是在所有的袋鼠中,只有聚拢到珠穆朗玛峰的袋鼠才是我们要找的袋鼠。

遗传算法并不保证你能获得问题的最优解,但是使用遗传算法的最大优点在于你不必去了解和操心如何去“找”最优解(不必去指导袋鼠向那边跳,跳多远。),而只要简单的“否定”一些表现不好的个体就行了(把那些总是爱走下坡路的袋鼠射杀,这就是遗传算法的精粹!)。

Experiments

微调阶段的精度对 batchsize 的大小非常敏感,小样本情况下 batch norm 不能很好的发挥作用。因此引入了 group norm 做消融实验,但是为了与其他方法对比更公平,对比时还是使用的 batch norm。

将微调网络中的哪一层可视化:

当 base 类和 novel 类差别较大(即跨域迁移)时,网络中更多的层需要微调,而这在一般的方法里是做不到的。

Conclusion

微调特定层、使用遗传算法来赋予每一层不同的学习率确实更有意义也是更正确的做法,微调全部层和冻结 backbone 只微调 head 可以看作是该方法的特例。

附加

  • batchnorm

batchnorm 是对数据进行归一化的方式,因为深度神经网络主要就是为了学习训练数据的分布,并在测试集上达到很好的泛化效果。但是如果我们每一个 batch 输入的数据都具有不同的分布,显然会给网络的训练带来困难。另一方面,数据经过一层层网络计算后,其数据分布也在发生着变化,此现象称为 Internal Covariate Shift,为了是网络每一层都学习到相同的数据分布,提出了 batchnorm。

一般来说每块 GPU 上的 batch size 设置为 32 更合适。

另外 batchnorm 是在 batch 这个维度上进行归一化,但是这个维度并不是固定不变的,比如训练和测试时一般不一样。一般都是训练的时候在训练集上通过滑动平均预先计算好 mean 和 variance 参数。在测试的时候,不再计算这些值,而是直接调用这些预计算好的来用。但是,当训练数据和测试数据分布有差异时,网络中的参数不能代表测试数据,就导致在训练、验证和测试三个阶段存在不一致。

于是如果避开 batch 这个维度,就解决了上述问题,所以诞生了 group norm。layer norm 和 instance norm 都可以看作是 group norm 的特殊情况。上图形象的表示了四种norm的工作方式:

BN 在 batch 的维度上 norm,归一化维度为 [N,H,W],对 batch 中对应的 channel 归一化;LN 避开了 batch 维度,归一化的维度为 [C,H,W];IN 归一化的维度为 [H,W];而 GN 介于 LN 和 IN 之间,其首先将 channel 分为许多组(group),对每一组做归一化,即先将 feature 的维度由 [N, C, H, W] reshape 为 [N, G,C//G , H, W],归一化的维度为 [C//G , H, W]。

  • DropBlock

DropBlock 由原本 Dropout 的随机丢弃点改变为丢弃整个块,如上图所述。该论文认为随机丢弃的像素点(其实是特征图的一个点)可能会由它附近的点的关联信息所表示出来,这样就起不到原本想要的正则效果,因此直接丢弃一个块。如上图的 c,绿色部分表示激活的特征单元,通过dropout掉一部分相邻的整片的区域(比如头和脚),网络就会去注重学习狗的别的部位的特征,来实现正确分类,从而表现出更好的泛化。

DropBlock 模块主要有2个参数:block_size,γ,γ 表示 drop 的概率。实验得出在 Resnet-50 的第3,4个 block 加 Drouout 比只在第4个 block 加 Drouout 更有效,在 Resnet-50 的卷积层和 skip connection 都使用 Drouout 效果更好。

# 输入大小为4维向量,否则会出错。丢弃的点为随机选取。
import torch
import torch.nn.functional as F
from torch import nn
 
class Drop(nn.Module):
    def __init__(self, drop_prob=0.1, block_size=7):
        super(Drop, self).__init__()
        self.drop_prob = drop_prob
        self.block_size = block_size
 
    def forward(self, x):
        if self.drop_prob == 0:
            return x
        # 设置gamma,比gamma小的设置为1,大于gamma的为0,对应第五步
        # 这样计算可以得到丢弃的比率的随机点个数
        gamma = self.drop_prob / (self.block_size**2)
        mask = (torch.rand(x.shape[0], *x.shape[2:]) < gamma).float()
 
        mask = mask.to(x.device)
 
        # compute block mask
        block_mask = self._compute_block_mask(mask)
        # apply block mask,为算法图的第六步
        out = x * block_mask[:, None, :, :]
        # Normalize the features,对应第七步
        out = out * block_mask.numel() / block_mask.sum()
        return out
 
    def _compute_block_mask(self, mask):
        # 取最大值,这样就能够取出一个block的块大小的1作为drop,当然需要翻转大小,使得1为0,0为1
        block_mask = F.max_pool2d(input=mask[:, None, :, :],
                                  kernel_size=(self.block_size,
                                               self.block_size),
                                  stride=(1, 1),
                                  padding=self.block_size // 2)
        if self.block_size % 2 == 0:
            # 如果block大小是2的话,会边界会多出1,要去掉才能输出与原图一样大小.
            block_mask = block_mask[:, :, :-1, :-1]
        block_mask = 1 - block_mask.squeeze(1)
        return block_mask

是分类问题中错误标注的一种解决方法。

交叉熵(Cross-Entropy)损失函数是分类模型中的一种非常重要的目标函数。在二分类问题中,交叉熵损失函数的形式如下:

如果分类准确,交叉熵损失函数的结果是0(即上式中 p 和 y 一致的情况),否则交叉熵为无穷大。也就是说交叉熵对分类正确给的是最大激励。换句话说,对于标注数据来说,这个时候我们认为其标注结果是准确的(不然这个结果就没意义了)。但实际上,有一些标注数据并不一定是准确的。那么这时候,使用交叉熵损失函数作为目标函数并不一定是最优的。

对于这个问题,我们还可以这么去理解。在分类任务中,我们通常对类别标签的编码使用 [0,1,2,…] 这种形式。在深度学习中,通常在全连接层的最后一层,加入一个 softmax 来计算输入数据属于每个类别的概率,并把概率最高的作为这个类别的输入,然后使用交叉熵作为损失函数。这会导致模型对正确分类的情况奖励最大,错误分类惩罚最大。如果训练数据能覆盖所有情况,或者是完全正确,那么这种方式没有问题。但事实上,这不可能。所以这种方式可能会带来泛化能力差的问题,即过拟合。

在2016年,Szegedy 等人提出了 inception v2 的模型(论文:Rethinking the inception architecture for computer vision)。其中提到了 Label Smoothing 技术,用以减轻这个问题。我们先来看一下原理。假设我们的分类只有两个,一个是猫一个不是猫,分别用1和0表示。Label Smoothing的工作原理是对原来的 [0,1] 这种标注做一个改动,假设我们给定Label Smoothing的值为0.1:

可以看到,原来的 [0,1] 编码变成了 [0.05,0.95] 了。这个 label_smoothing 的值假设为ϵ,那么就是说原来分类准确的时候,p=1,不准确为p=0,现在变成了 p=1−ϵ 和 ϵ,也就是说对分类准确做了一点惩罚。

Label Smoothing在很多问题上对模型都有一定的提升。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值