【CNN】ShuffleNet系列(V1,V2)

前沿

ShuffleNet v1是由旷视科技在2017年底提出的轻量级可用于移动设备的卷积神经网络。

该网络创新之处在于,使用 group convolution还有channel shuffle,保证网络准确率的同时,大幅度降低了所需的计算资源。

在近期的网络中,pointwise convolution(1X1conv)的出现使得所需计算量极大的增多,于是作者提出了pointwise group convolution来降低计算量,但是group与group之间的几乎没有联系,影响了网络的准确率,于是作者又提出了channel shuffle来加强group之间的联系。在一定计算复杂度下,网络允许更多的通道数来保留更多的信息,这恰恰是轻量级网络所追求的。

一、Group Convolution

就像解读MobileNet不得不说深度可分离卷积一样,解读ShuffleNet就不得不说组卷积了。这里对比着普通卷积和深度可分离卷积来说说组卷积:
在这里插入图片描述
上图为普通卷积示意图,为方便理解,图中只有一个卷积核,此时输入输出数据为:
输入feature map尺寸: W×H×C ,分别对应feature map的宽,高,通道数;
单个卷积核尺寸: k×k×C ,分别对应单个卷积核的宽,高,通道数;
输出feature map尺寸 :W’×H’ ,输出通道数等于卷积核数量,输出的宽和高与卷积步长有关,这里不关心这两个值。
参数量 : k2×C
运算量 : k2×C×W’×H’ (这里只考虑浮点乘数量,不考虑浮点加)。
请添加图片描述
将图一卷积的输入feature map分成组,每个卷积核也相应地分成组,在对应的组内做卷积,如上图2所示,图中分组数,即上面的一组feature map只和上面的一组卷积核做卷积,下面的一组feature map只和下面的一组卷积核做卷积。每组卷积都生成一个feature map,共生成个feature map。

输入feature map尺寸: W×H×C/g ,分别对应feature map的宽,高,通道数, 共有g组(上图g=2):
单个卷积核尺寸: k×k×C/g ,分别对应单个卷积核的宽,高,通道数,一个卷积核被分成g组;
输出feature map尺寸 :W’×H’×g ,共生成g个feature maps。
参数量 : k2×C/g×g = k2×C
运算量 : k2×C/g×W’×H’×g = k2×C×W’×H’

对比普通卷积来看,虽然参数量和运算量相同,但是,我们得到了g倍的feature map数量

所以group conv常用在轻量型高效网络中,因为它用少量的参数量和运算量就能生成大量的feature map,大量的feature map意味着能提取更多的信息。

从分组卷积的角度来看,分组数g就像一个控制旋钮,最小值是1,此时的卷积就是普通卷积;最大值是输入feature map的通道数,此时的卷积就是depthwise sepereable convolution,即深度分离卷积,又叫逐通道卷积。
请添加图片描述
如上图所示,深度分离卷积是分组卷积的一种特殊形式,其分组数是feature map的通道数。 这种卷积形式是最高效的卷积形式,相比普通卷积,用同等的参数量和运算量就能够生成通道数个feature maps,而普通卷积只能生成一个feature map。

所以深度分离卷积几乎是构造轻量高效模型的必用结构,如Xception, MobileNetV1, MobileNet V2, ShuffleNetV1, ShuffleNet V2, CondenseNet等轻量型网络结构中的必用结构。

二、ShuffleNetV1

1. Channel Shuffle

对于上述的Group Convolution,很容易想到一个问题就是在卷积的时候,仅仅是将这一个Group内的特征图进行了融合,但是不同的组别之间缺乏计算,长此以往,不同组中的特征图对于其他组的特征了解就越来越少,虽然网络顶层的全连接层会帮助不同特征图相互连接,但是可以预想的是这样的连接融合的次数较少,不如不分组的情况。

基于上述的情况,作者提出把每个组的特征图经过组卷积计算之后,结果进行一定程度的乱序结合再送入下一层组卷积,以这样的方式增加特征图的连接融合次数,过程如下图所示:
请添加图片描述
如上图(a)是正常的组卷积模式,不同分组(不同颜色表示不同分组)几乎没有信息交流;(b)和(c)描述的是channel shuffle的方式。

2. ShuffleNet unit

整个单元其实比较好理解,直接上图如下:
在这里插入图片描述
如图所示(a)是MobileNet系列网络中的DWconv。(b)和(c)是本文中提出的shuffle unit,(b)是3X3卷积步幅等于1的情况,可以看出与DWconv非常像,只是为了进一步减少参数量将1X1卷积优化成1X1组卷积,而且添加channel shuffle来确保不同组之间的信息交互。注意:Channel Shuffle操作在1×1的卷积操作之后,也就是先对通道进行了收缩,随后进行通道调整,最后卷积在调整回原来的通道数;(c)是步幅等于2的情况,输出特征图尺寸减半,channel维度增加为原先的2倍,为了保证最后的concat连接,需要保证两个分支的输出特征图尺寸相同,因此,在捷径分支上添加步幅为2的3X3全局池化。

3. Model Architecture

下图Table1是网络的结构的详细参数。
stride表示步幅,不同步幅有自己不同的shuffle unit;repeat代表重复次数,例如stage3的意思是重复stride=2的shuffle unit一次,重复stride=1的shuffle unit单元7次。
在这里插入图片描述
从上表的最后一行可以看到,随着分组的增加,最终的复杂度(论文中以FLOPS作为衡量标准)相应的减少,这和我们对于Group Convolution操作的期望相同;随之而来的一个问题是,采用了这样的方式会对准确率有影响吗?出人意料的,该改进也比传统的网络优秀一些,如下图所示。
请添加图片描述
除了标准网络,作者也按照MobileNetV1的思路,对于网络设置了一些超参数s,表示通道数的多少,例如s=1,即标准的网络结构,通道数如上图Table1所示;s=0.5表明每个stage的输出和输入通道数都为上图中通道数的一半,其他的类似。通过通道缩放s倍,整个计算复杂度参数均下降s2 倍。下表是作者的一些实验数据。
在这里插入图片描述
在这里插入图片描述

二、ShuffleNetV2

1. Motivation

论文发现,作为衡量计算复杂度的指标,FLOPs实际并不等同于速度。FLOPs相似的网络,其速度却有较大的差别,只用FLOPs作为衡量计算复杂度的指标是不够的,还要考虑内存访问消耗以及GPU并行。基于上面的发现,论文从理论到实验列举了轻量级网络设计的5个要领,然后再根据设计要领提出ShuffleNet V2。

2. Practical Guidelines for Efficient Network Design

(1)G1: Equal channel width minimizes memory access cost (MAC).
相同维度的通道数将最小化内存访问成本,如下图所示,当input channles = output channels时,每秒处理的照片数量越多。
在这里插入图片描述
(2)G2: Excessive group convolution increases MAC
过多的分组卷积会加大内存访问成本,如下图所示,越多的分组会导致速度急速下降,特别是在GPU上,下降的十分严重,一个显卡跑的话,8个Group Convolution会使得速度下降4倍!(这里作者依旧是在不同的条件下使用不同的通道数保证FLOPs是一样的)
在这里插入图片描述
(3)G3: Network fragmentation reduces degree of parallelism
碎片操作将减小网络的平行度,这里的碎片操作指的是将一个大的卷积操作分为多个小的卷积操作进行。作者这里使用自己搭建了一些网络进行验证,网络的结构如下:
在这里插入图片描述
在这里插入图片描述
在实际设备上进行对比,在固定FLOPs情况下,分别对比串行和并行分支结构的性能。结果如上图所示,这里有一个比较有趣的结果,就是我们认为可能增加并行度的平行结构,最后居然减低了速度,不过这里由于还有下一个guide line的实验说到了元素级的操作也会对速度有一定的影响,因此这里还不能下定论到底是因为平行还是因为最后的相加拉低了时间。
(4)G4: Element-wise operations are non-negligible
不要忽略元素级操作, 这里元素级操作指的就是Relu,TensorAdd,BiasAdd等等的矩阵元素级操作,可以推测到这些操作其实基本没有被算到FLOPs中,但是对于内存访问成本(MAC)这个参数的影响确实比较大的。
作者为了验证这个想法,对bottleneck这个层级进行了相应的修改,测试了是否含有Relu和short-cut两种操作的情况,对比如下:
在这里插入图片描述
结论一目了然,没有两种操作的时候,更快一些。而且一个有意思的现象是,去掉short-cut对于速度的提升比Relu快一些,可以想到的是Relu只是对一个tensor进行操作,而short-cut是对两个tensor进行的操作。
在这里插入图片描述
如上图所示,作者还分析了mobilenet和本文模型中具体操作的时间占用。Elemwise指的就是激活函数,残差连接等非线性操作,可以看到其时间占用并不能像计算FLOPS那样被忽略。

随后作者分析最近的一些比较火的网络结构:
ShuffleNetV1违反了G2,bottleneck的结构违反了G1,而MobileNetV2使用的inverse bottleneck的结构违反了G1,其中夹杂的DWconv和Relu都违反了G4,自动生成结构(auto-generated structures)高度碎片化违反了G3。

3. Model Architecture

作者首先复盘了ShuffleNetV1,认为目前比较关键的问题是如何在全卷积或者分组卷积中维护大多数的卷积使输入通道与输出通道相等的。针对这个目标,作者提出了Channel Split的操作,同时构建了ShuffleNetV2 的unit,如下图所示:
在这里插入图片描述
如上图所示:(a)(b)对应shufflenetV1是uints; (c),(d)对应改进后的V2版units。
这里结合个人看法说说这么做的好处:

(1)split channel把整个特征图分为两个组了(模拟分组卷积的分组操作,接下来的1X1卷积又变回了正常卷积),这样的分组避免了像分组卷积一样增加了卷积时的组数,符合G2;

小疑问?分组卷积看似在减少运算参数,但是却影响了运行速度;那么究竟怎么权衡?

(2)split channel之后,一个小组的数据是通过short-cut通道,而另一个小组的数据经过bottleneck层;这时,由于split channel已经降低了维度,因此bottleneck的1X1就不需要再降维了,输入输出的通道数就可以保持一致,符合G1;

小疑问?既然不需要降维,那么第一个1X1的conv还是否有存在的必要?

(3)同时,由于最后使用的concat操作,没有用TensorAdd操作,符合G4;

小疑问?对于残差结构来说,concat操作和add操作到底哪个更好用?另外,由于捷径分支不在是空集操作,那么这样的结构是否还符合short-cut的初衷(即bottleneck学到的是残差Residual部分)?但是可以想到的是经过后面的Channel Shuffle的乱序之后,每个通道应该都会经过一次bottleneck结构。

最后, 给出ShuffleNetV2的网络结构详细参数:
在这里插入图片描述
值得注意的是:channel数都比较的小, 这里作者并没有特别的解释这个现象(按照MobileNetV2中对于Relu的分析,这种数量的通道设计不太适合relu激活函数)。

四、代码

from typing import List

import torch
from torch import Tensor
import torch.nn as nn 
from custom_layers.CustomLayers import ConvBatchNormalization, ConvBNActivation
from custom_layers.CustomMethod import channel_shuffle



class ShuffleResidual(nn.Module):
    def __init__(self, input_channels, output_channels, stride):
        super().__init__()
        
        if stride not in [1,2]:
            raise ValueError('illegal stride value')
        self.stride = stride
        branch_features = output_channels //2
        assert output_channels % 2 ==0
        # 当stride为1时,input_channel应该是branch_features的两倍, python中 '<<' 是位运算,可理解为计算×2的快速方法
        assert (self.stride !=1) or (input_channels == branch_features <<1)
        
        if self.stride == 2:
            self.branch1 = nn.Sequential(
                # depth-wise conv and bn
                ConvBatchNormalization(input_channels, input_channels, kernel_size=3, stride=self.stride, padding=1, groups=input_channels),
                # point-wise conv and bn
                ConvBNActivation(input_channels, branch_features, kernel_size=1, stride=1, padding=0)           
            )
        else:
            self.branch1 = nn.Sequential()
        
        input_c = input_channels if self.stride >1 else branch_features
        self.branch2 = nn.Sequential(
            # point-wise conv
            ConvBNActivation(input_channels=input_c, output_channels=branch_features, kernel_size=1, stride=1, padding=0),
            # depth-wise conv
            ConvBatchNormalization(input_channels=branch_features, output_channels=branch_features, kernel_size=3, stride=self.stride, padding=1, groups=branch_features),
            # point-wise conv
            ConvBNActivation(input_channels=branch_features, output_channels=branch_features, kernel_size=1, stride=1, padding=0)
        )
    def forward(self, x:Tensor):
        if self.stride == 1:
            x1 , x2 = x.chunk(2, dim=1)
            x1 = x1
            x2 =  self.branch2(x2)
            out = torch.cat((x1, x2), dim=1)
        else:
            x1 = self.branch1(x)
            x2 = self.branch2(x)
            out = torch.cat((x1, x2), dim=1)
        
        out = channel_shuffle(out, 2)
        return out

class ShuffleNetV2(nn.Module):
    def __init__(self, stages_repeats: List[int], stages_out_channels:List[int], num_classes:int, shuffle_residual = ShuffleResidual):
        super(ShuffleNetV2, self).__init__()
        
        if len(stages_repeats) != 3:
            raise ValueError("expected stages_repeats as list of 3 positive ints")
        if len(stages_out_channels) != 5:
            raise ValueError("expected stages_out_channels as list of 5 positive ints")
        self._stage_out_channels = stages_out_channels
        
        # input RGB images
        input_channels = 3
        output_channels = self._stage_out_channels[0]
        
        self.conv1 =  ConvBNActivation(input_channels, output_channels, kernel_size=3, stride=2, padding=1, bias=False)
     
        input_channels = output_channels
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)        
        self.stage2 = nn.Sequential
        self.stage3 = nn.Sequential
        self.stage4 = nn.Sequential
        
        stage_names = ["stage{}".format(i) for i in [2, 3, 4]]
        for name, repeats, output_channels in zip(stage_names, stages_repeats, self._stage_out_channels[1:]):
            seq = [shuffle_residual(input_channels, output_channels, 2)]
            for i in range(repeats -1):
                seq.append(shuffle_residual(output_channels, output_channels,1))
            setattr(self, name, nn.Sequential(*seq))
            input_channels = output_channels
        
        output_channels = self._stage_out_channels[-1]
        self.conv5 = ConvBNActivation(input_channels, output_channels, kernel_size=1, stride=1, padding=0)
        self.fc = nn.Linear(output_channels, num_classes)
        
    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 = self.conv5(x)
        x = x.mean([2, 3]) # global pooling
        x = self.fc(x)
        return x

def shufflenet_v2_x1_0(num_classes=1000):
    """
    Constructs a ShuffleNetV2 with 1.0x output channels, as described in
    `"ShuffleNet V2: Practical Guidelines for Efficient CNN Architecture Design"
    <https://arxiv.org/abs/1807.11164>`.
    weight: https://download.pytorch.org/models/shufflenetv2_x1-5666bf0f80.pth
    :param num_classes:
    :return:
    """
    model = ShuffleNetV2(stages_repeats=[4, 8, 4],
                         stages_out_channels=[24, 116, 232, 464, 1024],
                         num_classes=num_classes)

    return model


def shufflenet_v2_x0_5(num_classes=1000):
    """
    Constructs a ShuffleNetV2 with 0.5x output channels, as described in
    `"ShuffleNet V2: Practical Guidelines for Efficient CNN Architecture Design"
    <https://arxiv.org/abs/1807.11164>`.
    weight: https://download.pytorch.org/models/shufflenetv2_x0.5-f707e7126e.pth
    :param num_classes:
    :return:
    """
    model = ShuffleNetV2(stages_repeats=[4, 8, 4],
                         stages_out_channels=[24, 48, 96, 192, 1024],
                         num_classes=num_classes)

    return model

五、总结

ShuffleNetV1:提出使用组卷积优化1X1卷积,来降低Flops;同时提出channel shuffle的概念来增加不同组间数据的交互;

ShuffleNetV2:提出了设计轻量快速模型的四个准则;并根据准则重新优化了shufflenet网络结构,具体讨论和分析见上文。
参考:
原文链接

猜你喜欢

转载自blog.csdn.net/lingchen1906/article/details/129491184