前言

EfficientNet源自Google Brain的论文EfficientNet: Rethinking Model Scaling for Convolutional Neural Networks. 从标题也可以看出,这篇论文最主要的创新点是Model Scaling(感觉是填了GoogLeNetV3中网络设计准则留下了的坑,好奇的同学可以看我之前的博文). 论文提出了compound scaling(混合缩放)把网络缩放的三种方式:深度、宽度、分辨率,组合起来按照一定规则缩放,从而提高网络的效果。EfficientNet在网络变大时效果提升明显,把精度上限进一步提升,成为了当前最强网络。EfficientNet-B7在ImageNet上获得了最先进的 84.4%的top-1精度 和 97.1%的top-5精度,比之前最好的卷积网络(也是谷歌自己家的GPipe, Top-1: 84.3%, Top-5: 97.0%)大小缩小8.4倍、速度提升6.1倍。


一、Movtivation

EfficientNet的主要创新点并不是结构,不像ResNet、SENet发明了shortcut或通道attention机制,EfficientNet的base结构像MobileNetV3一样, 也是利用NAS网络搜索技术搜出来的(好奇的同学可以看完之前MobileNet的讲解博文),然后使用compound scaling规则放缩,得到一系列表现优异的网络:B0~B7. 下面两幅图分别是ImageNet的Top-1 Accuracy随参数量和flops变化关系图,可以看到EfficientNet饱和值高,并且到达速度快。

在这里插入图片描述

增加网络参数可以获得更好的精度(有足够的数据,不过拟合的条件下),例如ResNet可以加深从ResNet-18到ResNet-200,GPipe将baseline模型放大四倍在ImageNet数据集上获得了84.3%的top-1精度。增加网络参数的方式有三种:深度、宽度和分辨率。探究这三种方式对网络性能的影响,以及如何同时缩放这三种因素是EifficentNet的主要贡献。

二、EifficentNetV1

1. Three scale method for network

下图很形象的描述了三种放大网络的策略,(a)是参照网络;(b)是放大网络的宽度; (c)是放大网络的深度; (d)是放大网络的输入尺寸; (e)是EifficentNet的策略,同时缩放。
在这里插入图片描述

这里对三种网络策略做详细解释:

深度:缩放网络深度在许多ConvNets都有使用,例如Vggnet,Resnet等,直觉上更深的网络可以捕获到更丰富和更复杂的特征,在新任务上也可以泛化的更好。然而,更深的网络由于梯度消失问题(这里我更倾向于说成是网络退化问题)也更难训练。尽管有一些技术,例如跨层连接、批量归一化等可以有效减缓训练问题,但是深层网络的精度回报减弱了:举个例子,ResNet-1000和ResNet-101具有类似的精度,即使它的层数更多。

宽度:缩放网络宽度也是一种常用的手段,例如GoogLeNet系列网络,正如之前讨论过的,更宽的网络可以捕捉到更细粒度的特征从而易于训练。然而,非常宽而又很浅的网络在捕捉高层次特征时有困难。

分辨率:使用更高分辨率的输入图像,ConvNets可能捕捉到更细粒度的模式。从最早的 224x224,现在有些ConvNets为了获得更高的精度选择使用 229x229 或者 331x331。目前,GPipe使用 480x480 的分辨率获得了最先进的ImageNet精度,更好的精度比如 600x600 也被广泛使用在目标检测网络中。

但是,这里单独加深某一种策略,所得到的精度回报都不是成正比的,如下图所示:

在这里插入图片描述
三种策略显示着同一规律:刚开始时有效,后面逐渐趋于饱和,而且所得的的精度回报越来越少。

2. scaling factor

我们经验上可以观察到不同缩放维度之间是不独立的,直观上来讲,对于分辨率更高的图像,我们应该增加网络深度,因为需要更大的感受野来帮助捕获更多像素点的类似特征,同时也应该增加网络宽度来获得更细粒度的特征。这些直觉指导着我们去协调平衡不同缩放维度而不是传统的单个缩放维度。

为了追去更好的精度和效率,在缩放时平衡网络所有维度至关重要。事实上,之前的一些工作已经开始在追去任意缩放网络深度和宽度,但是他们仍然需要复杂的人工微调。在本篇论文中,我们提出了一个新的复合缩放方法——使用一个复合系数ϕ统一缩放网络宽度、深度和分辨率:

在这里插入图片描述
这里的α,β和γ是通过NAS网络搜索技术得到的,下图给出了具体缩放因子在MobileNetV1V2和ResNet50网络上的提升效果:
在这里插入图片描述

3. Model Architecture

EfficientNet在模型结构设计上借鉴了MobileNet系列网络,结构同样是使用NAS技术搜索得到,不同的是优化目标,这里优化的是FLOPS而不是延迟。再次感叹一下大力出奇迹… 同时,使用了MobileNet V3中的MBConv作为模型的主干网络,同时也是用了SENet中的squeeze and excitation方法进一步提升性能,可能是因为使用的NAS搜索空间相似,所以得到的网络结构也很相似。

这里放一张图回顾一下MBConv,详见我之前MobileNet系列的博文

在这里插入图片描述
其完整的网络模型架构参数如下表所示:
在这里插入图片描述

如上表所示,EiffienctNetB0其实就是由简单的MBconv操作堆叠而成的。对于EfficientNet-B0这样的一个基线网络,如何使用复合扩展发对该网络进行扩展呢?原文中给出答案----主要就是分两步走:
在这里插入图片描述

首先,给出了最佳缩放因子α,β和γ的具体数值,分别是1.2, 1.1, 1.15。 然后我们将 α、β、γ 固定为常数并按比例放大得到B0~7一个七个网络模型。具体缩放因子如下:

在这里插入图片描述
最后,作者便由此扩展出了一系列的网络结构,如下所示:
在这里插入图片描述

二、EifficientV2

1. Motivation

首先,大家都知道,对于深度学习模型而言,模型的训练效率是非常重要的一个问题,这里的效率主要包括两个方面,一是训练时间、二是训练后的模型参数。

举个例子,GPT3,大家公认的在少样本学习上表现出来的性能很好,但是训练效率就不高,需要好几周的训练、几千块GPU集群,所以训练成本极高,对于一般用户来说,不可能有机会参与到训练或者优化的工作中,这是目前深度学习发展的一个很大的限制。

第二个是,EfficientNet模型,这个模型是19年谷歌发布的,这个模型已经开源了,系统地研究了模型缩放并且仔细验证了网络深度、宽度和分辨率之间的平衡进而获得更好的性能表现。

另外,现有的渐进式学习都是采用渐进式的更改输入图片的尺寸来实现,但是保持同样的正则化配置,作者认为这是导致模型性能下降的一大因素。可能大家还不太知道什么是渐进式学习,待会儿我会介绍一下。

因此基于上述三个动机,本文EfficientNetV2提出了相应的改进方案。总的来说,本文的贡献主要包括三点

  1. 提出了EfficientNetV2模型,谷歌在EfficientNet的基础上,引入了Fused-MBConv到搜索空间中。
  2. 修改了渐进式学习策略,提出了自适应调整正则化参数的机制,实现了加速训练、同时还能够保证准确率不降,甚至还有细微上升的效果。
  3. 最后作者在多个benchmark上进行了实验,我们可以发现模型在多个基准数据集上取得了SOTA性能,且训练效率更高。

2.EfficeientNet存在的三个问题

大图像尺寸会导致显著的内存占用

已有研究表明:EfficientNet的大图像尺寸会导致显著的内存占用。由于计算单元的总内存是固定的,我们不得不采用更小的batch训练这些模型,这无疑会降低训练速度。大家可以看下面这个图,如果训练图片尺寸设置为512,就会出现OOM的问题,如果小尺寸,则batch size的数量又会显著增加。这个很好理解,对吧。所以,需要渐进式的输入图片。

在这里插入图片描述

DepthWise卷积不能完全利用现有的加速器

DepthWise卷积虽然理论上减少了很多的计算量,但是,不能完全利用现有的加速器。DepthWise卷积已经多次提及,在之前的博文MobileNe中有详细介绍。因此Fused-MBConv被论证能够充分利用移动端和服务器端的加速器加速计算,因此引入了这个算子到搜索空间中,进一步提速。

EfficientNet的缩放策略不是最优解

值得讨论的是EfficientNet的缩放策略,提出我们需要同时考虑网络宽度、网络深度和分辨率来设计网络结构,也验证了这种方式提升了模型性能。但是采用的策略是对模型结构均匀的缩放和扩张,什么意思呢?就是每个stage的扩张倍数是一样的,举个例子,比如我把depth系数配置为2,那么每个stage的网络深度都乘2。作者认为,这种策略不是最优解,不是所有的阶段对模型的训练效率、模型性能影响都是相同的。因此,本文采用了非均匀的模型缩放策略,越往后的stage模型层数越深。

3.EfficeientNetV2的改进

有了上面的讨论,那么对于这篇论文网络架构的设计就很好理解了。

优化NAS神经网络结构搜索

作者的主要改进包括:把Fused-MBConv算子加入到搜索空间中,移除一些不重要的搜索选项。
搜索的结果是将模型前几个stage的MBconv替换成Fused-MBConv,如下表所示:

在这里插入图片描述

详解Fused-MBConv算子单元

如下图所示,与MBconv相比,Fuesd-MBconv将减少卷积操作参数量的DWconv又换成了正常的3X3卷积(大无语事件:搞半天又回去了…),这也对应着ShuffleNetV2中提出的观点:分组卷积看似减少了参数量,却也增加了运算时间(DWconv是分组卷积的极端情况);除了这点,其他的毫无一点不同。

在这里插入图片描述

每阶段的网络缩放因子应该不同

在EifficentNetV1版本中,网络在每个阶段的缩放倍率是相同的。但是,在V2中作者提出新观点:针对不同的stage应该有不同的缩放倍率。在论文中,作者也确实给出了网络的具体参数。作为一种启发式方法,作者在网络的后几个阶段stage5和6阶段添加更多的层结构但是没有解释为什么这么设置,估计也是NAS网络搜索技术得出来的。

并且,值得注意的是,作者将最大推理图像大小限制为 480,因为非常大的图像通常会导致昂贵的内存和训练速度开销。

引入改进的渐进式学习的策略

什么是渐进式学习呢?渐进式学习的话,就是在训练过程中,随时间动态的调整输入图片的尺寸,来尽可能多的使用数据集。

但是现有的渐进式学习研究,只改了输入,并没有修改模型的正则化参数,也就是没有改变模型反向调参的过程,那是不是认为不同分辨率的图片对模型的影响是同等的?

作者认为,这是导致模型Accuracy下降的一个原因。我们的经验其实可以看到,更大的模型应该需要更严格的正则化来避免过拟合,例如EfficientNet-B7就需要设置更大的dropout值以及更强的数据增强方式。

所以这篇论文,认为即使对于同一个网络,更小的图片尺寸训练出的模型性能更差,所以正则化要求更弱。反之,大尺寸的图片,依赖于更严格的正则化配置。

在这里插入图片描述

上图是原论文中的大熊猫例子,随着训练的迭代(1…100…300),输入模型的图像尺寸也在逐渐变大。同时实验配置的正则化参数也随之增大,包括三个参数,dropout的概率、随机擦除样本的行数、两种图片混合的比例,也就是数据增强的幅度在逐渐增大,来提升模型正则的要求。

完整的网络结构在‘优化NAS神经网络结构搜索’章节已经给出,对比了EfficientNetV2和EfficientNet的性能。EfficientNetV2 相较于EfficientNet, 模型参数、FLOPs提升效果有限,但是推断速度提升了3倍,如下图所示。这个结果的获得,就是刚才提到的,引入了Fuded_MBConv这个结构,还有搜索空间的调整。

在这里插入图片描述


三、 代码

这里给出模型搭建的python代码(基于pytorch实现)。完整的代码是基于图像分类问题的(包括训练和推理脚本,自定义层等)详见我的GitHub: 完整代码链接

from collections import OrderedDict
from functools import partial 
from typing import Callable
from unittest import result

import torch.nn as nn
import torch
from torch import Tensor 
from custom_layers.CustomLayers import ConvBNActivation, SqueezeExcitation
from custom_layers.CustomMethod import DropPath
class MBconv(nn.Module):
    def __init__(self, input_channels, output_channels, kernel_size, expand_ratio, stride, se_ratio, drop_ratio, norm_layer):
        super().__init__()
        if stride not in [1,2]:
            raise ValueError('illegal stride value')
        activate_layer = nn.SiLU
        expanded_channels = input_channels * expand_ratio
        # 在EfficientNetV2中,MBConv中不存在expansion=1的情况(V1存在)
        assert expand_ratio != 1
        
        # point-wise expansion
        self.expand_conv = ConvBNActivation(input_channels, expanded_channels, kernel_size=1, stride=1, padding=0, norm_layer=norm_layer, activation_layer=activate_layer)
        # depth-wise conv
        self.dw_conv = ConvBNActivation(expanded_channels, expanded_channels, kernel_size, stride, groups=expanded_channels, norm_layer=norm_layer, activation_layer=activate_layer)
        # SE-block
        self.se_block = SqueezeExcitation(expanded_channels, input_channels, scale_ratio=4) if se_ratio > 0 else nn.Identity()
        # point-wise conv
        self.pw_conv = ConvBNActivation(expanded_channels, output_channels, kernel_size=1, stride=1, padding=0, norm_layer=norm_layer, activation_layer=nn.Identity)
        
        self.output_channels = output_channels
        self.drop_ratio = drop_ratio
        self.has_shortcut = (stride == 1 and input_channels == output_channels)
        # dropPath
        if self.drop_ratio > 0 and self.has_shortcut:
            self.dropout = DropPath(drop_ratio)
        
    def forward(self, x):
        x = self.expand_conv(x)
        x = self.dw_conv(x)
        x = self.se_block(x)
        x = self.pw_conv(x)
        
        if self.has_shortcut:
            if self.drop_ratio > 0:
                result = self.dropout(x)
            result += x
        else:
            result = x
        return result

class FusedMBConv(nn.Module):
    def __init__(self, input_channels, output_channels, kernel_size, expand_ratio, stride, se_ratio, drop_ratio, norm_layer):
        super().__init__()
        assert stride in [1, 2]
        assert se_ratio == 0

        self.has_shortcut = ( stride==1 and input_channels == output_channels)
        self.drop_rate = drop_ratio
        self.has_expansion = expand_ratio != 1 
        activate_layer = nn.SiLU
        expanded_c = input_channels * expand_ratio
        
        # 只有当expand ratio不等于1时才有expand conv
        if self.has_expansion:
            # Expansion convolution
            self.expand_conv = ConvBNActivation(input_channels, expanded_c, kernel_size, stride, norm_layer=norm_layer, activation_layer=activate_layer)
            self.project_conv = ConvBNActivation(expanded_c, output_channels, kernel_size=1, norm_layer=norm_layer, activation_layer=nn.Identity)
        else:
            self.project_conv = ConvBNActivation(input_channels, output_channels, kernel_size, stride, norm_layer=norm_layer, activation_layer=activate_layer)

        self.out_channels = output_channels

        # 只有在使用shortcut连接时才使用dropout层
        self.drop_rate = drop_ratio
        if self.has_shortcut and drop_ratio > 0:
            self.dropout = DropPath(drop_ratio)
    
    def forward(self, x:Tensor):
        if self.has_expansion:
            reslut = self.expand_conv(x)
            result = self.project_conv(reslut)
        else:
            result = self.project_conv(x)
        
        if self.has_shortcut:
            if self.drop_rate>0:
                reslut = self.dropout(result)
            result += x
        return result

class EfficientNetV2(nn.Module):
    def __init__(self,
                 model_cnf: list,
                 num_classes: int = 1000,
                 num_features: int = 1280,
                 dropout_rate: float = 0.2,
                 drop_connect_rate: float = 0.2):
        super(EfficientNetV2, self).__init__()

        for cnf in model_cnf:
            assert len(cnf) == 8

        norm_layer = partial(nn.BatchNorm2d, eps=1e-3, momentum=0.1)

        stem_filter_num = model_cnf[0][4]

        self.stem = ConvBNActivation(3,
                              stem_filter_num,
                              kernel_size=3,
                              stride=2,
                              norm_layer=norm_layer)  # 激活函数默认是SiLU

        total_blocks = sum([i[0] for i in model_cnf])
        block_id = 0
        blocks = []
        for cnf in model_cnf:
            repeats = cnf[0]
            op = FusedMBConv if cnf[-2] == 0 else MBconv
            for i in range(repeats):
                blocks.append(op(kernel_size=cnf[1],
                                 input_channels=cnf[4] if i == 0 else cnf[5],
                                 output_channels=cnf[5],
                                 expand_ratio=cnf[3],
                                 stride=cnf[2] if i == 0 else 1,
                                 se_ratio=cnf[-1],
                                 drop_ratio=drop_connect_rate * block_id / total_blocks,
                                 norm_layer=norm_layer))
                block_id += 1
        self.blocks = nn.Sequential(*blocks)

        head_input_c = model_cnf[-1][-3]
        head = OrderedDict()

        head.update({"project_conv": ConvBNActivation(head_input_c,
                                               num_features,
                                               kernel_size=1,
                                               norm_layer=norm_layer)})  # 激活函数默认是SiLU

        head.update({"avgpool": nn.AdaptiveAvgPool2d(1)})
        head.update({"flatten": nn.Flatten()})

        if dropout_rate > 0:
            head.update({"dropout": nn.Dropout(p=dropout_rate, inplace=True)})
        head.update({"classifier": nn.Linear(num_features, num_classes)})

        self.head = nn.Sequential(head)

        # initial weights
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode="fan_out")
                if m.bias is not None:
                    nn.init.zeros_(m.bias)
            elif isinstance(m, nn.BatchNorm2d):
                nn.init.ones_(m.weight)
                nn.init.zeros_(m.bias)
            elif isinstance(m, nn.Linear):
                nn.init.normal_(m.weight, 0, 0.01)
                nn.init.zeros_(m.bias)

    def forward(self, x: Tensor) :
        x = self.stem(x)
        x = self.blocks(x)
        x = self.head(x)

        return x


def efficientnetv2_s(num_classes: int = 1000):
    """
    EfficientNetV2
    https://arxiv.org/abs/2104.00298
    """
    # train_size: 300, eval_size: 384

    # repeat, kernel, stride, expansion, in_c, out_c, operator, se_ratio
    model_config = [[2, 3, 1, 1, 24, 24, 0, 0],
                    [4, 3, 2, 4, 24, 48, 0, 0],
                    [4, 3, 2, 4, 48, 64, 0, 0],
                    [6, 3, 2, 4, 64, 128, 1, 4],
                    [9, 3, 1, 6, 128, 160, 1, 4],
                    [15, 3, 2, 6, 160, 256, 1, 4]]

    model = EfficientNetV2(model_cnf=model_config,
                           num_classes=num_classes,
                           dropout_rate=0.2)
    return model


def efficientnetv2_m(num_classes: int = 1000):
    """
    EfficientNetV2
    https://arxiv.org/abs/2104.00298
    """
    # train_size: 384, eval_size: 480

    # repeat, kernel, stride, expansion, in_c, out_c, operator, se_ratio
    model_config = [[3, 3, 1, 1, 24, 24, 0, 0],
                    [5, 3, 2, 4, 24, 48, 0, 0],
                    [5, 3, 2, 4, 48, 80, 0, 0],
                    [7, 3, 2, 4, 80, 160, 1, 4],
                    [14, 3, 1, 6, 160, 176, 1, 4],
                    [18, 3, 2, 6, 176, 304, 1, 4],
                    [5, 3, 1, 6, 304, 512, 1, 4]]

    model = EfficientNetV2(model_cnf=model_config,
                           num_classes=num_classes,
                           dropout_rate=0.3)
    return model


def efficientnetv2_l(num_classes: int = 1000):
    """
    EfficientNetV2
    https://arxiv.org/abs/2104.00298
    """
    # train_size: 384, eval_size: 480

    # repeat, kernel, stride, expansion, in_c, out_c, operator, se_ratio
    model_config = [[4, 3, 1, 1, 32, 32, 0, 0],
                    [7, 3, 2, 4, 32, 64, 0, 0],
                    [7, 3, 2, 4, 64, 96, 0, 0],
                    [10, 3, 2, 4, 96, 192, 1, 4],
                    [19, 3, 1, 6, 192, 224, 1, 4],
                    [25, 3, 2, 6, 224, 384, 1, 4],
                    [7, 3, 1, 6, 384, 640, 1, 4]]

    model = EfficientNetV2(model_cnf=model_config,
                           num_classes=num_classes,
                           dropout_rate=0.4)
    return model
        
    

四、 总结

EifficentNetV1:

  • 对比分析了网络深度,网络宽度和输入图像尺寸对模型性能的影响。
  • 通过NAS网络搜索技术给出了综合缩放网络的方法和参数

EifficentNetV2:

  • 提出DepthWise卷积不能完全利用现有的加速器的观点,并将V1版本中网络的前几个stage的MBconv换成Fuesd-MBconv,以提升推理速度。
  • 提出了网络不同阶段的不同缩放策略,并且限制了网络的最大输入尺寸。
  • 提出了新的渐进式学习策略:正则化应该随着输入图像尺寸的改变而改变。