专栏首页AI算法与图像处理万字长文!攻克目标检测难点秘籍一,模型加速之轻量化网络

万字长文!攻克目标检测难点秘籍一,模型加速之轻量化网络

来源:AI算法修炼营

目标检测难点概述

目标检测是计算机视觉中一个重要问题,在行人跟踪、车牌识别、无人驾驶等领域都具有重要的研究价值。近年来,随着深度学习对图像分类准确度的大幅度提高,基于深度学习的目标检测算法逐渐成为主流。

自 2014 年以来,目标检测框架分为two-stage 和 one-stage 两大类,前者以经典方法 Faster R-CNN 为代表,后者以 YOLO 和 SSD 为主要框架。近年来,两类最深刻的idea:更好的基础网络 & 融合不同卷积层的特征叠加在经典方法上,产生了大量变体。

结合近两年的计算机视觉会议的相关论文可以看出,对于目标检测而言,目前主要存在以下几个问题:

  • 对于小目标物体的检测(如小于30像素的目标物体)、遮挡面积较大的目标以及区分图像中与目标物体外形相似的非目标物体等问题需要在今后的研究中继续加强。
  • 实时性检测与处理。对于自动驾驶或汽车辅助驾驶等对实时处理能力要求较高的应用场景,进一步提高目标检测的计算速度和准确度是至关重要的。
  • 提高小数据量训练的检测效果。目前迁移学习的实现策略是先在现有大数据中进行训练,然后再将模型进行微调。虽然此方法能够实现,但检测精度和速度还有待进一步提高。
  • 目前基于深度学习的目标检测所涉及的行业领域越来越多,很难获取大量的监督数据或数据的标准成本过高,进而导致缺少用于网络训练的样本数据。

近年来,许多学者在目标检测难点方面已经做出许多成果,本文主要介绍秘籍一:模型加速之轻量化网络。

秘籍一. 模型加速之轻量化网络

从模型设计时就采用一些轻量化的思想,例如采用深度可分离卷积、分组卷积等轻量卷积方式,减少卷积过程的计算量。此外,利用全局池化来取代全连接层,利用1×1卷积实现特征的通道降维,也可以降低模型的计算量,这两点在众多网络中已经得到了应用。

对于轻量化的网络设计,目前较为流行的有SqueezeNet、MobileNet及ShuffleNet等结构。其中,SqueezeNet采用了精心设计的压缩再扩展的结构,MobileNet使用了效率更高的深度可分离卷积,而ShuffleNet提出了通道混洗的操作,进一步降低了模型的计算量。

下面将详细介绍:

1 SqueezeNet:压缩再扩展

随着网络结构的逐渐加深,模型的性能有了大幅度提升,但这也增加了网络参数与前向计算的时间。SqueezeNet从网络结构优化的角度出发,使用了如下3点策略来减少网络参数,提升网络性能:

1. 使用1×1卷积来替代部分的3×3卷积,可以将参数减少为原来的1/9。

2. 减少输入通道的数量,这一点也是通过1×1卷积来实现,通道数量的减少可以使后续卷积核的数量也相应地减少。

3. 将降采样后置。在减少通道数之后,使用多个尺寸的卷积核进行计算,以保留更多的信息,提升分类的准确率。

基于以上3点,SqueezeNet提出了基础模块 Fire Module。图中输入特征尺寸为H×W,通道数为M,依次经过一个Squeeze层与Expand层,然后进行融合处理。

  • SqueezeNet层:首先使用1×1卷积进行降维,特征图的尺寸不变,这里的S1小于M,达到了压缩的目的。
  • Expand层:并行地使用1×1卷积与3×3卷积获得不同感受野的特征图,类似Inception模块,达到扩展的目的。
  • Concat合并:对得到的两个特征图进行通道拼接,作为最终输出。·模块中的S1、e1与e2都是可调的超参,Fire Module默认e1=e2=4×S1。激活函数使用了ReLU函数。

可以使用PyTorch来搭建一个单独的SqueezeNet的Fire模块。

			class InvertedResidual(nn.Module):

    def __init__(self, inp, oup, stride, expand_ratio):
        super(InvertedResidual, self).__init__()
        self.stride = stride
        hidden_dim = round(inp * expand_ratio)
        self.conv = nn.Sequential(
            nn.Conv2d(inp, hidden_dim, 1, 1, 0, bias=False),
            nn.BatchNorm2d(hidden_dim),
            nn.ReLU6(inplace=True),

            nn.Conv2d(hidden_dim, hidden_dim, 3, stride, 1,
                        groups=hidden_dim, bias=False),
            nn.BatchNorm2d(hidden_dim),
            nn.ReLU6(inplace=True),
            
            nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False),
            nn.BatchNorm2d(oup),
        )

    def forward(self, x):
        return x + self.conv(x)

基于Fire Module, SqueezeNet的网络结构如下图所示。

输入图像首先送入Conv 1,得到通道数为96的特征图,然后依次使用8个Fire Module,通道数也逐渐增加。图中横线上的值代表了通道数。最后一个卷积为Conv 10,输入通道数为N的特征图,N代表需要物体的类别数。

SqueezeNet是一个精心设计的轻量化网络,使用常见的模型压缩技术,如SVD、剪枝和量化等,可以进一步压缩该模型的大小。例如,使用Deep Compresion技术对其进行压缩时,在几乎不损失性能的前提下,模型大小可以压缩到0.5MB。

基于其轻量化的特性,SqueezeNet可以广泛地应用到移动端,促进了目标检测技术在移动端的部署与应用。

2 MobileNet:深度可分离

2.1 MobileNet V1

标准卷积在卷积时,同时考虑了图像的区域与通道信息,那么为什么不能分开考虑区域与通道呢?

基于此想法,诞生了深度可分离卷积(Depthwise Separable Convolution),将卷积的过程分为逐通道卷积与逐点1×1卷积两步。

逐通道卷积有如下几个特点:

1.卷积核参数量少。卷积核参数量为Ci×3×3,远少于标准卷积Ci×3×3×Co的数量。

2.通道之间相互独立,没有各通道间的特征融合,这也是逐通道卷积的核心思想,输出特征的每一个点只对应输入特征一个通道上的3×3大小的特征,而不是标准卷积中Ci×3×3大小。

3.由于只在通道间进行卷积,导致输入与输出特征图的通道数相同,无法改变通道数。

由于逐通道卷积通道间缺少特征的融合,并且通道数无法改变,因此后续还需要继续连接一个逐点的1×1的卷积,一方面可以融合不同通道间的特征,同时也可以改变特征图的通道数。

MobileNet v1整体的网络是由上述深度可分离卷积基本单元组成的。与VGGNet类似,也是一个逐层堆叠式网络。

图中的Dw代表一个深度分解卷积,其后需要跟一个1×1卷积,s2代表步长为2的卷积,可以缩小特征图尺寸,起到与Pooling层一样的作用。

可以看到网络最后利用一个全局平均池化层,送入到全连接与Softmax进行分类预测。如果用于目标检测,只需要在之前的特征图上进行特征提取即可。

在基本的结构之外,MobileNet v1还设置了两个超参数,用来控制模型的大小与计算量,具体如下:

1. 宽度乘子:用于控制特征图的通道数,记做α,当α<1时,模型会变得更薄,可以将计算量减少为原来的α2。

2. 分辨率乘子:用于控制特征图的尺寸,记做ρ,在相应的特征图上应用该乘子,也可以有效降低每一层的计算量。

可以使用PyTorch来搭建一个MobileNet V1网络。

class MobileNetV1(nn.Module):
    def __init__(self):
        super(MobileNet, self).__init__()

        def conv_bn(dim_in, dim_out, stride):
            return nn.Sequential(
                nn.Conv2d(dim_in, dim_out, 3, stride, 1, bias=False),
                nn.BatchNorm2d(dim_out),
                nn.ReLU(inplace=True)
            )

        def conv_dw(dim_in, dim_out, stride):
            return nn.Sequential(
                nn.Conv2d(dim_in, dim_in, 3, stride, 1,
                             groups= dim_in, bias=False),
                nn.BatchNorm2d(dim_in),
                nn.ReLU(inplace=True),
                nn.Conv2d(dim_in, dim_out, 1, 1, 0, bias=False),
                nn.BatchNorm2d(dim_out),
                nn.ReLU(inplace=True),
            )
        self.model = nn.Sequential(
            conv_bn(  3,  32, 2),
            conv_dw( 32,  64, 1),
            conv_dw( 64, 128, 2),
            conv_dw(128, 128, 1),
            conv_dw(128, 256, 2),
            conv_dw(256, 256, 1),
            conv_dw(256, 512, 2),
            conv_dw(512, 512, 1),
            conv_dw(512, 512, 1),
            conv_dw(512, 512, 1),
            conv_dw(512, 512, 1),
            conv_dw(512, 512, 1),
            conv_dw(512, 1024, 2),
            conv_dw(1024, 1024, 1),
            nn.AvgPool2d(7),
        )
        self.fc = nn.Linear(1024, 1000)

    def forward(self, x):
        x = self.model(x)
        x = x.view(-1, 1024)
        x = self.fc(x)
        return x

总体上,MobileNet v1利用深度可分离的结构牺牲了较小的精度,带来了计算量与网络层参数的大幅降低,从而也减小了模型的大小,方便应用于移动端。

但MobileNet v1也有其自身结构带来的缺陷,主要有以下两点:

1. 模型结构较为复古,采用了与VGGNet类似的卷积简单堆叠,没有采用残差、特征融合等先进的结构。

2. 深度分解卷积中各通道相互独立,卷积核维度较小,输出特征中只有较少的输入特征,再加上ReLU激活函数,使得输出很容易变为0,难以恢复正常训练,因此在训练时部分卷积核容易被训练废掉

2.2 MobileNet V2

针对以上MobileNet V1的缺点,2018年诞生的MobileNet v2吸收了残差网络的思想,主要从两个方面改善了网络结构,提升了MobileNet的检测精度。

首先,MobileNet V2利用残差结构取代了原始的卷积堆叠方式,提出了一个Inverted ResidualBlock结构。

依据卷积的步长,该结构可分为两种情形,在步长为1时使用了残差连接,融合的方式为逐元素相加。

相比于MobileNet V1与原始的残差结构,这里有两点不同:

1. Inverted Residual Block结构

与 residuals block 相反,通常使用的 residuals block 是先经过 1×1 的卷积,降低 feature map 通道数,然后再通过3×3 卷积,最后重新经过 1×1 卷积将 feature map 通道数扩张回去,MobileNetV2 采用的方式是先对采用先升维,再降维的方法,这样做的理由是 MobileNetV2 将residuals block 的 bottleneck 替换为了 Depthwise Convolutions,因其参数少,提取的特征就会相对的少,如果再进行压缩的话,能提取的特征就更少了,因此MobileNetV2 就执行了扩张→卷积特征提取→压缩的过程。

2. 去掉ReLU6

深度可分离卷积得到的特征对应于低维空间,特征较少,如果后续接线性映射则能够保留大部分特征,而如果接非线性映射如ReLU,则会破坏特征,造成特征的损耗,从而使得模型效果变差。

针对此问题,MobileNetV2直接去掉了每一个Block中最后的ReLU6层,减少了特征的损耗,获得了更好的检测效果。

下面利用PyTorch构建MobileNet V2的残差模块。

def conv_bn(inp, oup, stride):
    return nn.Sequential(
        nn.Conv2d(inp, oup, 3, stride, 1, bias=False),
        nn.BatchNorm2d(oup),
        nn.ReLU6(inplace=True)
    )


def conv_1x1_bn(inp, oup):
    return nn.Sequential(
        nn.Conv2d(inp, oup, 1, 1, 0, bias=False),
        nn.BatchNorm2d(oup),
        nn.ReLU6(inplace=True)
    )


class InvertedResidual(nn.Module):
    def __init__(self, inp, oup, stride, expand_ratio):
        super(InvertedResidual, self).__init__()
        self.stride = stride
        assert stride in [1, 2]

        hidden_dim = round(inp * expand_ratio)
        self.use_res_connect = self.stride == 1 and inp == oup

        if expand_ratio == 1:
            self.conv = nn.Sequential(
                # dw
                nn.Conv2d(hidden_dim, hidden_dim, 3, stride, 1, groups=hidden_dim, bias=False),
                nn.BatchNorm2d(hidden_dim),
                nn.ReLU6(inplace=True),
                # pw-linear
                nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False),
                nn.BatchNorm2d(oup),
            )
        else:
            self.conv = nn.Sequential(
                # pw
                nn.Conv2d(inp, hidden_dim, 1, 1, 0, bias=False),
                nn.BatchNorm2d(hidden_dim),
                nn.ReLU6(inplace=True),
                # dw
                nn.Conv2d(hidden_dim, hidden_dim, 3, stride, 1, groups=hidden_dim, bias=False),
                nn.BatchNorm2d(hidden_dim),
                nn.ReLU6(inplace=True),
                # pw-linear
                nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False),
                nn.BatchNorm2d(oup),
            )

    def forward(self, x):
        if self.use_res_connect:
            return x + self.conv(x)
        else:
            return self.conv(x)


class MobileNetV2(nn.Module):
    def __init__(self, n_class=1000, input_size=224, width_mult=1.):
        super(MobileNetV2, self).__init__()
        block = InvertedResidual
        input_channel = 32
        last_channel = 1280
        interverted_residual_setting = [
            # t, c, n, s
            [1, 16, 1, 1],
            [6, 24, 2, 2],
            [6, 32, 3, 2],
            [6, 64, 4, 2],
            [6, 96, 3, 1],
            [6, 160, 3, 2],
            [6, 320, 1, 1],
        ]

        # building first layer
        assert input_size % 32 == 0
        input_channel = int(input_channel * width_mult)
        self.last_channel = int(last_channel * width_mult) if width_mult > 1.0 else last_channel
        self.features = [conv_bn(3, input_channel, 2)]
        # building inverted residual blocks
        for t, c, n, s in interverted_residual_setting:
            output_channel = int(c * width_mult)
            for i in range(n):
                if i == 0:
                    self.features.append(block(input_channel, output_channel, s, expand_ratio=t))
                else:
                    self.features.append(block(input_channel, output_channel, 1, expand_ratio=t))
                input_channel = output_channel
        # building last several layers
        self.features.append(conv_1x1_bn(input_channel, self.last_channel))
        # make it nn.Sequential
        self.features = nn.Sequential(*self.features)

        # building classifier
        self.classifier = nn.Sequential(
            nn.Dropout(0.2),
            nn.Linear(self.last_channel, n_class),
        )

        self._initialize_weights()

    def forward(self, x):
        x = self.features(x)
        x = x.mean(3).mean(2)
        x = self.classifier(x)
        return x

    def _initialize_weights(self):
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
                m.weight.data.normal_(0, math.sqrt(2. / n))
                if m.bias is not None:
                    m.bias.data.zero_()
            elif isinstance(m, nn.BatchNorm2d):
                m.weight.data.fill_(1)
                m.bias.data.zero_()
            elif isinstance(m, nn.Linear):
                n = m.weight.size(1)
                m.weight.data.normal_(0, 0.01)
                m.bias.data.zero_()

总体上,MobileNet V2在原结构的基础上进行了简单的修改,通过较少的计算量即可获得较高的精度,非常适合于移动端的部署。

主要特点

  • 扩张(1x1 conv) -> 抽取特征(3x3 depthwise)-> 压缩(1x1 conv)
  • 当且仅当输入输出具有相同的通道数时,才进行残余连接
  • 在最后“压缩”完以后,没有接ReLU激活,作者认为这样会引起较大的信息损失
  • 该结构在输入和输出处保持紧凑的表示,同时在内部扩展到更高维的特征空间,以增加非线性每通道变换的表现力。

2.3 MobileNet V3

整体来说MobileNetV3有两大创新点:

1. 互补搜索技术组合:由资源受限的NAS执行模块级搜索,NetAdapt执行局部搜索。

资源受限的NAS(platform-aware NAS):计算和参数量受限的前提下搜索网络的各个模块,所以称之为模块级的搜索(Block-wise Search)。

NetAdapt:用于对各个模块确定之后网络层的微调。

对于模型结构的探索和优化来说,网络搜索是强大的工具。研究人员首先使用了神经网络搜索功能来构建全局的网络结构,随后利用了NetAdapt算法来对每层的核数量进行优化。

对于全局的网络结构搜索,研究人员使用了与Mnasnet中相同的,基于RNN的控制器和分级的搜索空间,并针对特定的硬件平台进行精度-延时平衡优化,在目标延时(~80ms)范围内进行搜索。随后利用NetAdapt方法来对每一层按照序列的方式进行调优。在尽量优化模型延时的同时保持精度,减小扩充层和每一层中瓶颈的大小。

2. 网络结构改进:将最后一步的平均池化层前移并移除最后一个卷积层,引入h-swish激活函数。

h-swish激活函数,计算公式如下:

作者发现swish激活函数能够有效提高网络的精度,然而,swish的计算量太大了。作者提出h-swish(hard version of swish)如下所示。

以下是平滑版本的Sigmoid/Swish和Hard版本的Sigmoid/Swish对比。

MobileNetV3是综合了以下三种模型的思想:

MobileNetV1的深度可分离卷积(depthwise separable convolutions)、MobileNetV2的具有线性瓶颈的逆残差结构(the inverted residual with linear bottleneck)和MnasNet的基于squeeze and excitation结构的轻量级注意力模型

综合了以上三种结构的优点设计出了高效的MobileNetV3模块。

主要特点

l swish非线性激活

l Squeeze and Excitation思想

l 为了减轻计算swish中传统sigmoid的代价,提出了hard sigmoid

可以使用PyTorch来搭建一个MobileNet V3网络。

class hswish(nn.Module):
    def forward(self, x):
        out = x * F.relu6(x + 3, inplace=True) / 6
        return out


class hsigmoid(nn.Module):
    def forward(self, x):
        out = F.relu6(x + 3, inplace=True) / 6
        return out


class SeModule(nn.Module):
    def __init__(self, in_size, reduction=4):
        super(SeModule, self).__init__()
        self.se = nn.Sequential(
            nn.AdaptiveAvgPool2d(1),
            nn.Conv2d(in_size, in_size // reduction, kernel_size=1, stride=1, padding=0, bias=False),
            nn.BatchNorm2d(in_size // reduction),
            nn.ReLU(inplace=True),
            nn.Conv2d(in_size // reduction, in_size, kernel_size=1, stride=1, padding=0, bias=False),
            nn.BatchNorm2d(in_size),
            hsigmoid()
        )

    def forward(self, x):
        return x * self.se(x)


class Block(nn.Module):
    expand + depthwise + pointwise
    def __init__(self, kernel_size, in_size, expand_size, out_size, nolinear, semodule, stride):
        super(Block, self).__init__()
        self.stride = stride
        self.se = semodule

        self.conv1 = nn.Conv2d(in_size, expand_size, kernel_size=1, stride=1, padding=0, bias=False)
        self.bn1 = nn.BatchNorm2d(expand_size)
        self.nolinear1 = nolinear
        self.conv2 = nn.Conv2d(expand_size, expand_size, kernel_size=kernel_size, stride=stride, padding=kernel_size//2, groups=expand_size, bias=False)
        self.bn2 = nn.BatchNorm2d(expand_size)
        self.nolinear2 = nolinear
        self.conv3 = nn.Conv2d(expand_size, out_size, kernel_size=1, stride=1, padding=0, bias=False)
        self.bn3 = nn.BatchNorm2d(out_size)

        self.shortcut = nn.Sequential()
        if stride == 1 and in_size != out_size:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_size, out_size, kernel_size=1, stride=1, padding=0, bias=False),
                nn.BatchNorm2d(out_size),
            )

    def forward(self, x):
        out = self.nolinear1(self.bn1(self.conv1(x)))
        out = self.nolinear2(self.bn2(self.conv2(out)))
        out = self.bn3(self.conv3(out))
        if self.se != None:
            out = self.se(out)
        out = out + self.shortcut(x) if self.stride==1 else out
        return out


class MobileNetV3_Large(nn.Module):
    def __init__(self, num_classes=1000):
        super(MobileNetV3_Large, self).__init__()
        self.conv1 = nn.Conv2d(3, 16, kernel_size=3, stride=2, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(16)
        self.hs1 = hswish()

        self.bneck = nn.Sequential(
            Block(3, 16, 16, 16, nn.ReLU(inplace=True), None, 1),
            Block(3, 16, 64, 24, nn.ReLU(inplace=True), None, 2),
            Block(3, 24, 72, 24, nn.ReLU(inplace=True), None, 1),
            Block(5, 24, 72, 40, nn.ReLU(inplace=True), SeModule(40), 2),
            Block(5, 40, 120, 40, nn.ReLU(inplace=True), SeModule(40), 1),
            Block(5, 40, 120, 40, nn.ReLU(inplace=True), SeModule(40), 1),
            Block(3, 40, 240, 80, hswish(), None, 2),
            Block(3, 80, 200, 80, hswish(), None, 1),
            Block(3, 80, 184, 80, hswish(), None, 1),
            Block(3, 80, 184, 80, hswish(), None, 1),
            Block(3, 80, 480, 112, hswish(), SeModule(112), 1),
            Block(3, 112, 672, 112, hswish(), SeModule(112), 1),
            Block(5, 112, 672, 160, hswish(), SeModule(160), 1),
            Block(5, 160, 672, 160, hswish(), SeModule(160), 2),
            Block(5, 160, 960, 160, hswish(), SeModule(160), 1),
        )


        self.conv2 = nn.Conv2d(160, 960, kernel_size=1, stride=1, padding=0, bias=False)
        self.bn2 = nn.BatchNorm2d(960)
        self.hs2 = hswish()
        self.linear3 = nn.Linear(960, 1280)
        self.bn3 = nn.BatchNorm1d(1280)
        self.hs3 = hswish()
        self.linear4 = nn.Linear(1280, num_classes)
        self.init_params()

    def init_params(self):
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                init.kaiming_normal_(m.weight, mode=fan_out)
                if m.bias is not None:
                    init.constant_(m.bias, 0)
            elif isinstance(m, nn.BatchNorm2d):
                init.constant_(m.weight, 1)
                init.constant_(m.bias, 0)
            elif isinstance(m, nn.Linear):
                init.normal_(m.weight, std=0.001)
                if m.bias is not None:
                    init.constant_(m.bias, 0)

    def forward(self, x):
        out = self.hs1(self.bn1(self.conv1(x)))
        out = self.bneck(out)
        out = self.hs2(self.bn2(self.conv2(out)))
        out = F.avg_pool2d(out, 7)
        out = out.view(out.size(0), -1)
        out = self.hs3(self.bn3(self.linear3(out)))
        out = self.linear4(out)
        return out



class MobileNetV3_Small(nn.Module):
    def __init__(self, num_classes=1000):
        super(MobileNetV3_Small, self).__init__()
        self.conv1 = nn.Conv2d(3, 16, kernel_size=3, stride=2, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(16)
        self.hs1 = hswish()

        self.bneck = nn.Sequential(
            Block(3, 16, 16, 16, nn.ReLU(inplace=True), SeModule(16), 2),
            Block(3, 16, 72, 24, nn.ReLU(inplace=True), None, 2),
            Block(3, 24, 88, 24, nn.ReLU(inplace=True), None, 1),
            Block(5, 24, 96, 40, hswish(), SeModule(40), 2),
            Block(5, 40, 240, 40, hswish(), SeModule(40), 1),
            Block(5, 40, 240, 40, hswish(), SeModule(40), 1),
            Block(5, 40, 120, 48, hswish(), SeModule(48), 1),
            Block(5, 48, 144, 48, hswish(), SeModule(48), 1),
            Block(5, 48, 288, 96, hswish(), SeModule(96), 2),
            Block(5, 96, 576, 96, hswish(), SeModule(96), 1),
            Block(5, 96, 576, 96, hswish(), SeModule(96), 1),
        )


        self.conv2 = nn.Conv2d(96, 576, kernel_size=1, stride=1, padding=0, bias=False)
        self.bn2 = nn.BatchNorm2d(576)
        self.hs2 = hswish()
        self.linear3 = nn.Linear(576, 1280)
        self.bn3 = nn.BatchNorm1d(1280)
        self.hs3 = hswish()
        self.linear4 = nn.Linear(1280, num_classes)
        self.init_params()

3 ShuffleNet:通道混洗

3.1 ShuffleNet V1

为了降低计算量,当前先进的卷积网络通常在3×3卷积之前增加一个1×1卷积,用于通道间的信息流通与降维。然而在ResNeXt、MobileNet等高性能的网络中,1×1卷积却占用了大量的计算资源。

2017年的ShuffleNet v1从优化网络结构的角度出发,利用组卷积与通道混洗(ChannelShuffle)的操作有效降低了1×1逐点卷积的计算量,是一个极为高效的轻量化网络。而2018年的ShuffleNet v2则在ShuffleNet v1版本的基础上实现了更为优越的性能。

当前先进的轻量化网络大都使用深度可分离卷积或者组卷积,以降低网络的计算量,但这两种操作都无法改变特征的通道数,因此还需要使用1×1卷积。

总体来讲,逐点的1×1卷积有如下两点特性:

1. 可以促进通道之间信息的融合,改变通道至指定维度。

2. 轻量化网络中1×1卷积占据了大量的计算,并且致使通道之间充满约束,一定程度上降低了模型的精度

为了进一步降低计算量,ShuffleNet提出了通道混洗的操作,通过通道混洗也可以完成通道之间信息的融合。

如果没有逐点的1×1卷积或者通道混洗,最终输出的特征仅由一部分输入通道的特征计算得出,这种操作阻碍了信息的流通,进而降低了特征的表达能力。

因此,我们希望在一个组卷积之后,能够将特征图之间的通道信息进行融合,类似于下图中b的操作,将每一个组的特征分散到不同的组之后,再进行下一个组卷积,这样输出的特征就能够包含每一个组的特征,而通道混洗恰好可以实现这个过程。

通道混洗可以通过几个常规的张量操作巧妙地实现,主要有三个步骤:

  • Reshape:首先将输入通道一个维度Reshape成两个维度,一个是卷积组数,一个是每个卷积组包含的通道数。
  • Transpose:将扩展出的两维进行置换。
  • Flatten:将置换后的通道Flatten平展后即可完成最后的通道混洗。

ShuffleNet基本结构单元

a图是一个带有深度可分离卷积的残差模块,这里的1×1是逐点的卷积。相比深度可分离卷积,1×1计算量较大。

b图则是基本的ShuffleNet基本单元,可以看到1×1卷积采用的是组卷积,然后进行通道的混洗,这两步可以取代1×1的逐点卷积,并且大大降低了计算量。3×3卷积仍然采用深度可分离的方式。

c图是带有降采样的ShuffleNet单元,在旁路中使用了步长为2的3×3平均池化进行降采样,在主路中3×3卷积步长为2实现降采样。另外,由于降采样时通常要伴有通道数的增加,ShuffleNet直接将两分支拼接在一起来实现了通道数的增加,而不是常规的逐点相加。

得益于组卷积与通道混洗,ShuffleNet的基本单元可以很高效地进行计算。

ShuffleNet V1整体结构

关于ShuffleNet V1的整体结构,有以下3点需要注意:

  • g代表组卷积的组数,以控制卷积连接的稀疏性。组数越多,计算量越少,因此在相同的计算资源,可以使用更多的卷积核以获取更多的通道数。
  • ShuffleNet在3个阶段内使用了其特殊的基本单元,这3个阶段的第一个Block的步长为2以完成降采样,下一个阶段的通道数是上一个的两倍。
  • 深度可分离卷积虽然可以有效降低计算量,但其存储访问效率较差,因此第一个卷积并没有使用ShuffleNet基本单元,而是只在后续3个阶段使用。

可以使用PyTorch来搭建一个ShuffleNet V1网络。

class ShuffleNetV1(nn.Module):


    def __init__(self, groups=3, in_channels=3, num_classes=1000):
        super(ShuffleNet, self).__init__()
        self.groups = groups
        self.stage_repeats = [3, 7, 3]
        self.in_channels =  in_channels
        self.num_classes = num_classes
        self.stage_out_channels = [-1, 24, 240, 480, 960]
        
        self.conv1 = conv3x3(self.in_channels,
                             self.stage_out_channels[1], # stage 1
                             stride=2)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        self.stage2 = self._make_stage(2)
        self.stage3 = self._make_stage(3)
        self.stage4 = self._make_stage(4)
        num_inputs = self.stage_out_channels[-1]
        self.fc = nn.Linear(num_inputs, self.num_classes)

    def _make_stage(self, stage):
        modules = OrderedDict()
        stage_name = ShuffleUnit_Stage{}.format(stage)
        grouped_conv = stage > 2
        first_module = ShuffleUnit(
            self.stage_out_channels[stage-1],
            self.stage_out_channels[stage],
            groups=self.groups,
            grouped_conv=grouped_conv,
            combine=concat
            )
        modules[stage_name+_0] = first_module
        for i in range(self.stage_repeats[stage-2]):
            name = stage_name + _{}.format(i+1)
            module = ShuffleUnit(
                self.stage_out_channels[stage],
                self.stage_out_channels[stage],
                groups=self.groups,
                grouped_conv=True,
                combine=add
                )
            modules[name] = module
        return nn.Sequential(modules)

    def forward(self, x):
        x = self.conv1(x)
        x = self.maxpool(x)
        x = self.stage2(x)
        x = self.stage3(x)
        x = self.stage4(x)
        x = F.avg_pool2d(x, x.data.size()[-2:])
        x = x.view(x.size(0), -1)
        x = self.fc(x)
        return F.log_softmax(x, dim=1)

总体上,ShuffleNet提出了一个巧妙的通道混洗模块,在几乎不影响准确率的前提下,进一步地降低了计算量,性能优于ResNet和Xception等网络,因此也更适合部署在移动设备上。

3.2 ShuffleNet V2

在2018年,旷视的团队进一步升级了ShuffleNet,提出了新的版本ShuffleNet v2。

相比于ShuffleNet v1, ShuffleNet v2进一步分析了影响模型速度的因素,提出了新的规则,并基于此规则,改善了原版本的不足。

原有的一些轻量化方法在衡量模型性能时,通常使用浮点运算量FLOP(s Floating PointOperations)作为主要指标。FLOPs是指模型在进行一次前向传播时所需的浮点计算次数,其单位为FLOP,可以用来衡量模型的复杂度。

然而,通过一系列实验发现ShuffleNet v2仅仅依赖FLOPs是有问题的,FLOPs近似的网络会存在不同的速度,还有另外两个重要的指标:内存访问时间(Memory Access Cost, MAC)与网络的并行度

以此作为出发点,ShuffleNet v2做了大量的实验,分析影响网络运行速度的原因,提出了建立高性能网络的4个基本规则:

(1)卷积层的输入特征与输出特征通道数相等时,MAC最小,此时模型速度最快。

(2)过多的组卷积会增加MAC,导致模型的速度变慢。

(3)网络的碎片化会降低可并行度,这表明模型中分支数量越少,模型速度会越快。

(4)逐元素(Element Wise)操作虽然FLOPs值较低,但其MAC较高,因此也应当尽可能减少逐元素操作

以这4个规则为基础,可以看出ShuffleNet v1有3点违反了此规则:

  • 在Bottleneck中使用了1×1组卷积与1×1的逐点卷积,导致输入输出通道数不同,违背了规则1与规则2。
  • 整体网络中使用了大量的组卷积,造成了太多的分组,违背了规则3。
  • 网络中存在大量的逐点相加操作,违背了规则4。

针对v1的问题,ShuffleNet v2提出了一种新的网络基本单元,具体如下图所示。图a与图b为v1版本的基础结构,图c与图d是v2提出的新结构。

ShuffleNet v2的基本单元有如下3点新特性:

1. 提出了一种新的Channel Split操作,将输入特征分成两部分,一部分进行真正的深度可分离计算,将计算结果与另一部分进行通道Concat,最后进行通道的混洗操作,完成信息的互通。

2. 整个过程没有使用到1×1组卷积,也避免了逐点相加的操作

3. 在需要降采样与通道翻倍时,ShuffleNet v2去掉了Channel Split操作,这样最后Concat时通道数会翻倍。

与ShuffleNet v1相比,ShuffleNet v2在全局平均池化之前增加了一个1×1卷积来融合特征。依据输出特征的通道数多少,ShuffleNet v2也给出了多种配置,相应的模型精度、大小也会有所区别。

可以使用PyTorch来搭建一个ShuffleNet V1网络。

class ShuffleBlock(nn.Module):
    def __init__(self, groups):
        super(ShuffleBlock, self).__init__()
        self.groups = groups

    def forward(self, x):
        Channel shuffle: [N,C,H,W] -> [N,g,C/g,H,W] -> [N,C/g,g,H,w] -> [N,C,H,W]
        N,C,H,W = x.size()
        g = self.groups
        # 维度变换之后必须要使用.contiguous()使得张量在内存连续之后才能调用view函数
        return x.view(N,g,int(C/g),H,W).permute(0,2,1,3,4).contiguous().view(N,C,H,W)


class Bottleneck(nn.Module):
    def __init__(self, in_planes, out_planes, stride, groups):
        super(Bottleneck, self).__init__()
        self.stride = stride

        # bottleneck层中间层的channel数变为输出channel数的1/4
        mid_planes = int(out_planes/4)


        g = 1 if in_planes==24 else groups
        # 作者提到不在stage2的第一个pointwise层使用组卷积,因为输入channel数量太少,只有24
        self.conv1 = nn.Conv2d(in_planes, mid_planes,
                               kernel_size=1, groups=g, bias=False)
        self.bn1 = nn.BatchNorm2d(mid_planes)
        self.shuffle1 = ShuffleBlock(groups=g)
        self.conv2 = nn.Conv2d(mid_planes, mid_planes,
                               kernel_size=3, stride=stride, padding=1,
                               groups=mid_planes, bias=False)
        self.bn2 = nn.BatchNorm2d(mid_planes)
        self.conv3 = nn.Conv2d(mid_planes, out_planes,
                               kernel_size=1, groups=groups, bias=False)
        self.bn3 = nn.BatchNorm2d(out_planes)

        self.shortcut = nn.Sequential()
        if stride == 2:
            self.shortcut = nn.Sequential(nn.AvgPool2d(3, stride=2, padding=1))

    def forward(self, x):
        out = F.relu(self.bn1(self.conv1(x)))
        out = self.shuffle1(out)
        out = F.relu(self.bn2(self.conv2(out)))
        out = self.bn3(self.conv3(out))
        res = self.shortcut(x)
        out = F.relu(torch.cat([out,res], 1)) if self.stride==2 else F.relu(out+res)
        return out


class ShuffleNet(nn.Module):
    def __init__(self, cfg):
        super(ShuffleNet, self).__init__()
        out_planes = cfg[out_planes]
        num_blocks = cfg[num_blocks]
        groups = cfg[groups]

        self.conv1 = nn.Conv2d(3, 24, kernel_size=1, bias=False)
        self.bn1 = nn.BatchNorm2d(24)
        self.in_planes = 24
        self.layer1 = self._make_layer(out_planes[0], num_blocks[0], groups)
        self.layer2 = self._make_layer(out_planes[1], num_blocks[1], groups)
        self.layer3 = self._make_layer(out_planes[2], num_blocks[2], groups)
        self.linear = nn.Linear(out_planes[2], 10)

    def _make_layer(self, out_planes, num_blocks, groups):
        layers = []
        for i in range(num_blocks):
            if i == 0:
                layers.append(Bottleneck(self.in_planes,
                                         out_planes-self.in_planes,
                                         stride=2, groups=groups))
            else:
                layers.append(Bottleneck(self.in_planes,
                                         out_planes,
                                         stride=1, groups=groups))
            self.in_planes = out_planes
        return nn.Sequential(*layers)

    def forward(self, x):
        out = F.relu(self.bn1(self.conv1(x)))
        out = self.layer1(out)
        out = self.layer2(out)
        out = self.layer3(out)
        out = F.avg_pool2d(out, 4)
        out = out.view(out.size(0), -1)
        out = self.linear(out)
        return out

总体上,ShuffleNet V2模型从更直接影响模型运行速度的角度出发,在速度与精度的平衡上达到了当前的最佳水平,非常适合于移动端模型的应用。

总结

SqueezeNet精心设计了一个压缩再扩展的模块,有效降低了卷积计算量;

MobileNet 系列则发挥了深度可分离卷积的优势,提升了卷积计算的效率。

ShuffleNet系列更进一步,在分组卷积的思想上提出了通道混洗操作,避免了大量1×1卷积的操作,可谓经典。

通常情况下,将这几种轻量化网络应用到检测框架中,在速度上均可以得到不同程度的提升。

参考:

1.深度学习之PyTorch物体检测实战 董洪义

2.https://blog.csdn.net/sinat_37532065/article/details/90813655#4.3%20MobileNet-v3%E5%AE%9A%E4%B9%89

3.https://blog.csdn.net/qiu931110/article/details/86586704https://zhuanlan.zhihu.com/p/101544149

-END-

本文分享自微信公众号 - AI算法与图像处理(AI_study)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-02-13

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 基于UNet网络实现的人像分割 | 附数据集

    人像分割的相关应用非常广,例如基于人像分割可以实现背景的替换做出各种非常酷炫的效果。我们将训练数据扩充到人体分割,那么我们就是对人体做美颜特效处理,同时对背景做...

    AI算法与图像处理
  • Face Recognition Loss on Mnist with Pytorch

    这篇文章的重点不在于讲解FR的各种Loss,因为知乎上已经有很多,搜一下就好,本文主要提供了各种Loss的Pytorch实现以及Mnist的可视化实验,一方面让...

    AI算法与图像处理
  • 全套 | 人脸检测 & 人脸关键点检测 & 人脸卡通化

    可能跟我一样,人脸检测是很多人学习图像处理的第一个自驱动型的任务,OpenCV刚上手没几天可能就想先跑一跑人脸检测,然后一个坑接着一个坑的往里跳。我个人对人脸检...

    AI算法与图像处理
  • InsightFace力作:RetinaFace单阶段人脸检测器

    RetinaFace是2019年5月来自InsightFace的又一力作,它是一个鲁棒性较强的人脸检测器。它在目标检测这一块的变动其实并不大,主要贡献是新增了一...

    BBuf
  • LeetCode 155. 最小栈

    设计一个支持 push,pop,top 操作,并能在常数时间内检索到最小元素的栈。

    freesan44
  • Python开发植物大战僵尸游戏

    ------------------- End -------------------

    python学习教程
  • Python 3 之 运算符重载详解

    实际上,“运算符重载”只是意味着在类方法中拦截内置的操作……当类的实例出现在内置操作中,Python自动调用你的方法,并且你的方法的返回值变成了相应操作的结果。...

    py3study
  • python笔记:随机数,md5,en/decoder

    超级大猪
  • Python开发植物大战僵尸游戏,详细教程

    一墨编程学习
  • 毕业设计:基于CNN的图像分类算法

    近年来人工智能越来越火,好像本科毕设不搞人工智能都很丢人似的。但是普通本科生对于这个领域的了解显然很少,基本也就是做做数据搜集、数据标注、工程实现、调调参的工作...

    用户2145057

扫码关注云+社区

领取腾讯云代金券