来源 | https://zhuanlan.zhihu.com/p/456432890
本文目的是结合代码对该工作中的trick进行梳理,帮助广大工程师童鞋抄作业,整合到自己的项目中。
我愿称之为2022年cv算法工程师抄作业必备手册,手把手教你改模型,把ResNet50从76.1一步步干到82.0。或许对于广大researcher而言这只是一个堆trick的工作,但对于工程师来说,光是验证哪些trick能work,哪些trick堆叠在一起能都产生收益,这件事本身就已经散发着money的味道了。现在大佬们烧了这么多电费把结果摆到大家面前,还要什么自行车。
本文的目的是结合代码对该工作中的trick进行梳理,帮助广大工程师童鞋抄作业,整合到自己的项目中。
论文题目:A ConvNet for the 2020s
论文链接:https://arxiv.org/abs/2201.03545
代码链接:GitHub - facebookresearch/ConvNeXt: Code release for ConvNeXt model
深度学习发展了这么久,除了结构上的创新,各种训练策略也在升级。2021年timm和torchvision团队均有工作讲述如何通过优化训练策略来使resnet50性能提升到80以上。
考虑到跟Swin Transformer的公平对比,本文的训练策略没有完全follow前面的工作,但仍然可以将ResNet50从76.1提升到78.8。
这里我汇总了一下训练策略横向对比,方便大家查表:
改变layer0到layer3的block数量比例,由标准的(3,4,6,3)改为Swin-T使用的(3,3,9,3),即1:1:3:1。对于更大的模型,也跟进了Swin所使用的1:1:9:1。
从ViT开始,为了将图片转化为token,图片都会先被分割成一个一个的patch,而在传统ResNet中stem层是使用一个stride=2的7x7卷积加最大池化层。
本文仿照Swin-T的做法,用stride=4的4x4卷积来进行stem,使得滑动窗口不再相交,每次只处理一个patch的信息。
# 标准ResNet
stem = nn.Sequential(
nn.Conv2d(in_chans, dims[0], kernel_size=7, stride=2),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)
# ConvNeXt
stem = nn.Sequential(
nn.Conv2d(in_chans, dims[0], kernel_size=4, stride=4),
LayerNorm(dims[0], eps=1e-6, data_format="channels_first")
)
通过代码我们可以注意到,在stem中还加入了一个LN。
由于ResNeXt在FLOPs/accuracy的trade-off比ResNet更优秀,于是进行了一些借鉴,主要是使用了分组卷积。
ResNeXt的指导准则是“分更多的组,拓宽width”,因此本文直接使用了depthwise conv,即分组数等于输入通道数。这个技术在之前主要是应用在MobileNet这种轻量级网络中,用于降低计算量。但在这里,作者发现dw conv由于每个卷积核单独处理一个通道,这种形式跟self-attention机制很相似,都是在单个通道内做空间信息的混合加权。
将bottleneck中的3x3卷积替换成dw conv,再把网络宽度从64提升到96。
在标准ResNet中使用的bottleneck是(大维度-小维度-大维度)的形式来减小计算量。后来在MobileNetV2中提出了inverted bottleneck结构,采用(小维度-大维度-小维度)形式,认为这样能让信息在不同维度特征空间之间转换时避免压缩维度带来的信息损失,后来在Transformer的MLP中也使用了类似的结构,中间层全连接层维度数是两端的4倍。
由于Swin-T中使用了7x7卷积核,这一步主要是为了对齐比较。又因为inverted bottleneck放大了中间卷积层的缘故,直接替换会导致参数量增大,因而作者把dw conv的位置进行了调整,放到了反瓶颈的开头。最终结果相近,说明在7x7在相同参数量下效果是一致的。
主要是为了对齐比较,并没有带来提升。
由于Transformer中只使用了一个激活层,因此在设计上进行了效仿,结果发现只在block中的两个1x1卷积之间使用一层激活层,其他地方不适用,反而带来了0.7个点的提升。这说明太频繁地做非线性投影对于网络特征的信息传递实际上是有害的。
基于跟减少激活层相同的逻辑,由于Transformer中BN层很少,本文也只保留了1x1卷积之前的一层BN,而两个1x1卷积层之间甚至没有使用归一化层,只做了非线性投影。
由于Transformer中使用了LN,且一些研究发现BN会对网络性能带来一些负面影响,本文将所有的BN替换为LN。
标准ResNet的下采样层通常是stride=2的3x3卷积,对于有残差结构的block则在短路连接中使用stride=2的1x1卷积,这使得CNN的下采样层基本与其他层保持了相似的计算策略。而Swin-T中的下采样层是单独的,因此本文用stride=2的2x2卷积进行模拟。又因为这样会使训练不稳定,因此每个下采样层后面增加了LN来稳定训练。
self.downsample_layers = nn.ModuleList()
# stem也可以看成下采样层,一起存到downsample_layers中,推理时通过index进行访问
stem = nn.Sequential(
nn.Conv2d(in_chans, dims[0], kernel_size=4, stride=4),
LayerNorm(dims[0], eps=1e-6, data_format="channels_first")
)
self.downsample_layers.append(stem)
for i in range(3):
downsample_layer = nn.Sequential(
LayerNorm(dims[i], eps=1e-6, data_format="channels_first"),
nn.Conv2d(dims[i], dims[i+1], kernel_size=2, stride=2),
)
self.downsample_layers.append(downsample_layer)
# 由于网络结构是downsample-stage-downsample-stage的形式,所以stem和后面的下采样层中的LN是不会连在一起的
对以上内容进行整合,最终得到了单个block的设计及代码:
class Block(nn.Module):
def __init__(self, dim, drop_path=0., layer_scale_init_value=1e-6):
super().__init__()
# 分组卷积+大卷积核
self.dwconv = nn.Conv2d(dim, dim, kernel_size=7, padding=3, groups=dim)
# 在1x1之前使用唯一一次LN做归一化
self.norm = LayerNorm(dim, eps=1e-6)
# 全连接层跟1x1conv等价,但pytorch计算上fc略快
self.pwconv1 = nn.Linear(dim, 4 * dim)
# 整个block只使用唯一一次激活层
self.act = nn.GELU()
# 反瓶颈结构,中间层升维了4倍
self.pwconv2 = nn.Linear(4 * dim, dim)
# gamma的作用是用于做layer scale训练策略
self.gamma = nn.Parameter(layer_scale_init_value * torch.ones((dim)),
requires_grad=True) if layer_scale_init_value > 0 else None
# drop_path是用于stoch. depth训练策略
self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity()
def forward(self, x):
input = x
x = self.dwconv(x)
# 由于用FC来做1x1conv,所以需要调换通道顺序
x = x.permute(0, 2, 3, 1) # (N, C, H, W) -> (N, H, W, C)
x = self.norm(x)
x = self.pwconv1(x)
x = self.act(x)
x = self.pwconv2(x)
if self.gamma is not None:
x = self.gamma * x
x = x.permute(0, 3, 1, 2) # (N, H, W, C) -> (N, C, H, W)
x = input + self.drop_path(x)
return x
通过代码可以注意到,以上Block中两层1x1卷积是用全连接层来实现的,按照作者的说法,这样会比使用卷积层略快。
但作者是在GPU上进行的实验,考虑到CPU上很多情况会不同,因此我缩减得到了一个轻量的ConvNeXt-ExTiny模型,并转换成MNN模型,测试了两种实现方案的速度,发现在CPU上还是使用1x1卷积层的速度更快。
实现如下:
class Block(nn.Module):
def __init__(self, dim, drop_path=0., layer_scale_init_value=1e-6):
super().__init__()
self.dwconv = nn.Conv2d(dim, dim, kernel_size=7, padding=3, groups=dim) # depthwise conv
self.act = nn.GELU()
self.norm = LayerNorm(dim, eps=1e-6, data_format="channels_first")
self.pwconv1 = nn.Conv2d(dim, dim*4, kernel_size=1, stride=1)
self.pwconv2 = nn.Conv2d(dim*4, dim, kernel_size=1, stride=1)
self.gamma = nn.Parameter(layer_scale_init_value * torch.ones((dim,1,1)),
requires_grad=True) if layer_scale_init_value > 0 else None
self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity()
def forward(self, x):
input = x
x = self.dwconv(x)
x = self.norm(x)
x = self.pwconv1(x)
x = self.act(x)
x = self.pwconv2(x)
if self.gamma is not None:
x = self.gamma * x
x = input + self.drop_path(x)
return x
MNN下CPU推理速度对比:
fc版:
mnn_inference: 16.39620065689087
mnn_inference: 17.782490253448486
mnn_inference: 17.42337703704834
mnn_inference: 16.68517827987671
mnn_inference: 15.608322620391846
1x1 conv版本:
mnn_inference: 14.232232570648193
mnn_inference: 14.07259225845337
mnn_inference: 13.94277572631836
mnn_inference: 14.112122058868408
mnn_inference: 13.633315563201904
ConvNext这篇论文,通过借鉴Swin Transformer精心构建的 tricks,使得卷积在图像领域反超了Transformer。这些技巧对分类问题的下游问题也有效果。不禁令人深思,在深度学习中如何设计网络,如何使得训练神经网络具有更加优异的效果,以及思考网络设计的物理意义(尽管到现在为止很多网络模型的构建可能更多来源于“灵感”之类的)。
ConvNeXt借鉴了以下tricks:
1. 训练技巧、预训练和网络结构比例
2. 减少激活层和归一化层的使用(Relu和Norm会造成特征损失)
3. 运用了7*7大尺寸的卷积核且效果更优(由于运用了Inverted Bottleneck和depth-wise Convs使得计算量基本稳定的情况下增加了性能)
4. 分离的下采样层和LN层。
1.2、CNN网络的反击
1.2.1、Depth-wise conv的反击
微软在去年6年份的工作已经讨论了用7x7 depth-wise conv模拟Local Vision Transformer,并且实现了比Swin Transformer更好的效果。微软在论文Demystifying Local Vision Transformer: Sparse Connectivity, Weight Sharing, and Dynamic Weight上系统地总结了Local Vision Transformer的三大特性:
Sparse connectivity:每个token的输出只依赖于其所在local window上tokens,而且各个channel之间是无联系的;(这里忽略了attention中query,key和valude的linear projections,那么attention就其实可以看成在计算好的权重下对tokens的特征进行加权求和,而且是channel-wise的);
Weight sharing:权重对于各个channel是共享的;
Dynamic weight:权重不是固定的,而是基于各个tokens动态生成的。
这样总结的话,local attention就和depth-wise convs存在很多的相似之处。首先后者也具有Sparse connectivity:计算只在kernel size范围内,而且各个channel之间无连接。然后depth-wise convs也具有weight sharing特性,但是卷积核是在所有的空间位置上共享的,而且不同channle采用不同的卷积核。对于第三点,depth-wise conv的卷积核是训练参数,一旦完成训练就是固定的,而不像attention那样是动态的。但是local attention丢失了位置信息,需要位置编码,而depth-wise convolution则不需要。
下面是不同操作的区别:
1.2.2、ConvNeXt
MetaAI在论文A ConvNet for the 2020s中, 从ResNet出发并借鉴Swin Transformer提出了一种新的 CNN 模型:ConvNeXt,其效果无论在图像分类还是检测分割任务上均能超过Swin Transformer,而且ConvNeXt和vision transformer一样具有类似的scalability(随着数据量和模型大小增加,性能同比提升)。
ResNet到ConvNeXt
ConvNeXt 从原始的 ResNet 出发,逐步加入swin transform 的 trick,来改进模型。论文中适用 ResNet模型:ResNet50和ResNet200。其中ResNet50和Swin-T有类似的FLOPs(4G vs 4.5G),而ResNet200和Swin-B有类似的FLOPs(15G)。
首先做的改进是调整训练策略,然后是模型设计方面的递进优化:宏观设计->ResNeXt化->改用Inverted bottleneck->采用large kernel size->微观设计。由于模型性能和FLOPs强相关,所以在优化过程中尽量保持FLOPs的稳定。
模型效果提升 tricks 总览:
2.1、训练策略
原生的ViT需要大规模数据的预训练,而MetaAI在DeiT论文中提出了一种增强版本的训练策略来解决这个问题,这个训练策略也被随后的vision transformer模型所采用。
对于ResNet50,其训练策略比较简单(torchvision版本):batch size是32*8,epochs为90;优化器采用momentum=0.9的SGD,初始学习速率为0.1,然后每30个epoch学习速率衰减为原来的0.1;正则化只有L2,weight decay=1e-4;数据增强采用随机缩放裁剪(RandomResizedCrop)+水平翻转(RandomHorizontalFlip)。
而DeiT的训练策略则非常heavy:采用了比较多的数据增强如Mixup,Cutmix和RandAugment;训练的epochs增加至300;训练的optimizer采用AdamW,学习速率schedule采用cosine decay;采用 smooth label 和EMA等优化策略。
这里直接将DeiT的训练策略(具体参数设置如下表)应用在ResNet50模型,其性能从原来的76.1%提升至78.8%(+2.7)。这也说明vision transformer模型相比CNN模型的提升很多程度上归功于训练策略的优化,关于这个问题,另外一篇论文Visformer也论证过这一点,而且最近timm库和torchvison库也分别发布了ResNet新的训练策略,并将ResNet50的性能提升至80%+。
我们框架中也可能存在adam优化器的问题, 2014年被提出的Adam优化器的收敛性被证明是错误的,之前大部分机器学习框架中对于Adam的权重衰减的实现也都是错误的。实际上,L2正则化和权重衰减在大部分情况下并不等价,只在SGD优化的情况下是等价的。而大多数框架中对于Adam+L2正则使用的是权重衰减的方式,两者不能混为一谈。
在 ConvNeXt 使用模型与训练的方法, 如下:
2.2、Changing stage compute ratio
Swin Transfomer和CNN一样也采用金字塔结构:包含4个stage,每个stage输出不同尺度的特征。这里考虑Swin Transformer和ResNet在宏观设计上的区别,主要有两点:每个stage的计算量占比以及 stem cell结构(即stage1之前的模块)。
首先是各个stage的计算量占比,对比ResNet50,4个stage的blocks数量分别是(3,4,6,3),而Swin-T的设置为(2,2,6,2),4个stage的计算量比约为1:1:3:1。这里调整ResNet50各个stage的blocks数量以和Swin-T一致:从原来的(3,4,6,3)调整至(3,3,9,3)。
调整之后模型性能从78.8%提升至79.4%,不过这里要注意的一点是,调整后其实blocks数量增加了,模型的FLOPs从原来的4G增加至4.5G,基本和Swin-T一致了,所以这个性能的提升很大程度上归功于FLOPs的增加。
关于各个stage的计算量分配,并没有一个理论上的参考,不过RegNet和EfficientNetV2论文中都指出,后面的stages应该占用更多的计算量。
第二个就是stem的区别。对于Swin-T模型,其stem是一个patch embedding layer,实际上就是一个stride=4的4x4 conv。而 ResNet50的stem相对更复杂一些:首先是一个stride=2的7x7 conv,然后是一个stride=2 的 3x3 max pooling。
两种stem最后均是得到1/4大小的特征,所以这里可以直接用Swin的stem来替换ResNet的stem,这个变动对模型效果影响较小:从79.4%提升至79.5%,可能是去掉最大池化起到了作用 ,最大池化会造成特征损失。
对于ViT模型,其 patch size一般较大(eg. 16),只采用一个stride较大的conv来做patch embedding往往会存在一定问题,比如Mocov3论文中就指出patch embed可能会导致训练不稳定,而论文Early Convolutions Help Transformers See Better指出将 patch embed 设计成几个堆叠的 stride=2 的3*3 conv,无论是在模型效果上,还是在训练稳定性以及收敛速度都更好;而Swin-T的 patch size相对较小,不会出现ViT的上述问题,不过Swin-T采用的是 non-overlapping conv,后面有论文指出采用overlapping conv(eg stride=4 的7x7 conv)会带来一定的性能提升。
2.3、ResNeXt-ify (depth-wise)
相比 ResNet,ResNeXt通过采用group conv来提升性能,标准的conv其输入是所有的channels,而group conv会对channels进行分组来减少计算量,这样节省下来的计算量用来增加网络的width即特征channels。对于group conv,其最极端的情况就是每个channel一个group,这样就变成了depthwise conv(简称dw conv),dw conv首先在MobileNet中应用,后来也被其它CNN模型广泛采用。
对于dw conv,其和 local attention 有很多的相似的地方,local attention其实就是对window里的各个token的特征做加权和,而且操作是per-channel的;而dw conv是对kernel size范围的token的特征求加权和,也是分channel的。这里的最大区别就是:self-attention的权重是动态计算的(data dependent),而dw conv的权重就是学习的kernel。
这里将 ResNet50中的 3x3 conv替换成3x3 dw conv,为了弥补 FLOPs 的减少,同时将 ResNet50 的 base width从原来的64增加至96(和Swin-T一致,这里的base width是指stem后的特征大小),此时模型的FLOPs有所增加(5.3G),模型性能提升至80.5%。
2.4、Inverted Bottleneck
如果把self-attention看成一个dw conv的话(这里忽略self-attention的linear projection操作),那么一个transformer block可以近似看成一个inverted bottleneck,因为MLP等效于两个1x1 conv,并且MLP中间隐含层特征是输入特征大小的4倍(expansion ratio=4)。
inverted bottleneck最早在MobileNetV2中提出,随后的EfficientNet也采用了这样的结构。ResNet50采用的是正常的residual bottleneck,这里将其改成inverted bottleneck,即从图(a)变成图(b),虽然dw conv的计算量增加了,但是对于包含下采样的residual block中,用于shortcut的1x1 conv计算量却大大降低,最终模型的FLOPs减少为4.6G。这个变动对ResNet50的影响较小(80.5%->80.6%)。
2.5、Large Kernel Sizes
自从VGG之后,主流的CNN往往采用较小的kernel size,如3x3和5x5,其中3x3 conv在GPU上有高效的实现。然而Swin-T采用的 window size为7x7,这比3x3 conv对应的 window size要大得多,所以这里考虑采用更大的kernel size。
在这之前,首先将dw conv移到inverted bottleneck block的最开始,如上图(c)所示。对于transformer block,其实self-attention也是在开始,同时由于采用inverted bottleneck,将dw conv移动到最前面可以减少计算量(4.1G),后续采用较大的kernel size后模型的FLOPs变动更少。
由于模型FLOPs的降低,模型性能也出现一定的下降:80.6%->79.9%。然后调整dw conv的kernel size,这里共实验了5种kernel size:3x3,5x5,7x7,9x9和11x11。
实验发现kernel size增加,模型性能有提升,但是在7x7之后采用更大的kernel size性能达到饱和。所以最终选择7x7,这样也和Swin-T的window size一致,由于前面的dw conv位置变动,采用7x7的kernel size基本没带来FLOPs的增加。采用7x7 dw conv之后,模型的性能又回到 80.6%。
2.6、微观设计
2.6.1 ReLU -> GreLU
经过前面的改动,模型的性能已经提升到80%以上,此时改动后的ResNet50也和Swin-T在整体结构上很类似了,下面我们开始关注一些微观设计上的差异,或者说是layer级别的不同。
首先是激活函数,CNN模型一般采用ReLU,而transformer模型常常采用GELU。这里把激活函数都从ReLU改成GELU,模型效果没有变化(80.6%)。
2.6.2 减少激活层和归一化层
如下图示,这里只保留中间1x1 conv之后的GELU,就和Swin-T基本保持一致了,这个变动使模型性能从80.6%提升至81.3%。
对于norm层,也存在和激活函数一样的问题,transformer中只在self-attention和MLP的开始采用了LayerNorm,而ResNet每个conv之后采用BatchNorm,比transformer多一个norm层。这里去掉其它的BatchNorm,只保留中间1x1 conv前的BatchNorm,此时模型性能有0.1%的提升。实际上要和 transformer 保持一致,应该在block最开始增加一个BatchNorm,但是这个并没有提升性能,所以最终只留下了一个norm层。
另外,transformer的norm层采用 LayerNorm,而CNN常采用BatchNorm,一般情况下BatchNorm要比LayerNorm效果要好,但是BatchNorm受batch size的影响较大。这里将BatchNorm替换成LayerNorm后,模型性能提升至81.5%。
最后一个差异是下采样,ResNet中的下采样一般放在每个stage的最开始的block中,采用 stride=2 的3x3 conv;但是ConvNeXt 采用分离的下采样去近似 Swin Transform的 Patch Merging layer(可以看补充知识去理解 Patch Merging),即下采样是放在两个stage之间,通过一个stride=2的 2x2 conv。
但是实验发现,如果直接改用Swin-T的下采样,会出现训练发散问题,解决的办法是在添加几个norm层:在stem之后,每个下采样层之前以及global avg pooling之后都增加一个LayerNom(Swin-T也是这样做的)。最终模型的性能提升至82.0%,超过Swin-T(81.3%)。
2.7、其它数据
前面我们从ResNet50的Swin-T化,最终得到了ConvNeXt-T模型,对于更大的模型,可以通过调整特征的维度C和各个stage的blocks数量。论文共设计5个ConvNeXt模型,其中前4个模型分别对标Swin,而最后ConvNeXt-XL是一个更大的模型,用来验证模型的scalability。
ConvNeXt-T: C = (96, 192, 384, 768), B = (3, 3, 9, 3)
ConvNeXt-S: C = (96, 192, 384, 768), B = (3, 3, 27, 3)
ConvNeXt-B: C = (128, 256, 512, 1024), B = (3, 3, 27, 3)
ConvNeXt-L: C = (192, 384, 768, 1536), B = (3, 3, 27, 3)
ConvNeXt-XL: C = (256, 512, 1024, 2048), B = (3, 3, 27, 3)
ConvNeXt在ImageNet1K数据集上的分类效果如下所示,可以看到,ConvNeXt在不同的FLOPs均可以超过Swin,如果采用ImageNet22K预训练后,模型性能有进一步的提升,其中ConvNeXt-XL可以达到87.8%,仅略低于目前的SOTA一点(MViT-H, 512^2,88.8%,只有ImageNet21K)。
在下游任务检测和分割上,ConvNeXt也可以超过Swin,如下表所示:
由于FLOPs往往不能准确地反映实际的推理速度,所以上面的对比结果也包括模型速度(分类上用throughout,检测上用FPS),可以看到在实际推理速度上,ConvNeXt也略好于Swin。
在显存使用上,两者也相当,比如基于ConvNeXt-B的Cascade Mask-RCNN需要17.4GB peak memory,而Swin-B 需要18.5GB。另外,如果在A100上(支持TF32),采用channel last,ConvNeXt相比Swin有明显的速度优势:
Robustness Evaluation , ConVNeXt 也优于 swin-Transform
CVPR2022 | A ConvNet for the 2020s & 如何设计神经网络总结 (qq.com)
ConvNeXt:手把手教你改模型 - 知乎