# ShuffleNet v2

ShuffleNet v2 论文最大贡献不是提出了一个新的模型，而是提出了 4 条设计高效 CNN 的规则，该 4 条规则考虑的映射比较多，不仅仅 FLOPS 参数，还可以到内存使用量、不同平台差异和速度指标等等，非常全面。在不违背这 4 条规则的前提下进行改进有望设计出速度和精度平衡的高效模型。

## 1 算法设计

### 1.1 算法核心

到目前为止，前人所提轻量级 CNN 框架取得了很好效果，例如 Xception、MobileNet、MobileNetV2 和 ShuffleNet v1 等，主要采用技术手段是**分组卷积 Group convolution 和逐深度方向卷积 depthwise convolution**。

在上述算法中，除了精度要求外，评价模型复杂度都是采用 FLOPs 指标(每秒浮点加乘运算次数)，在 mobilnetv1、v2 和 ShuffleNet 都是采用该指标的。然而 FLOPs 其实是一种间接指标，它只是真正关心的直接指标(如速度或延迟)的一种近似形式，通常无法与直接指标划等号。先前研究已对这种差异有所察觉，比如 MobileNet V2 要远快于 NASNET-A(自动搜索出的神经网络)，但是两者 FLOPs 相当，它表明 FLOPs 近似的网络也会有不同的速度。所以，将 FLOPs 作为衡量计算复杂度的唯一标准是不够的，这样会导致次优设计。

研究者注意到 FLOPs 仅和卷积部分相关，尽管这一部分需要消耗大部分的时间，但其它过程例如数据 I/O、数据重排和元素级运算（张量加法、ReLU 等）也需要消耗一定程度的时间。

间接指标(FLOPs)和直接指标(速度)之间存在差异的原因可以归结为两点：

* 对速度有较大影响的几个重要因素对 FLOPs 不产生太大作用，例如内存访问成本 (MAC)。在某些操作(如组卷积)中，MAC 占运行时间的很大一部分，对于像 GPU 这样具备强大计算能力的设备而言，这就是瓶颈，在网络架构设计过程中，内存访问成本不能被简单忽视
* 并行度。当 FLOPs 相同时，高并行度的模型可能比低并行度的模型快得多

其次，FLOPs 相同的运算可能有着不同的运行时间，这取决于平台。例如，早期研究广泛使用张量分解来加速矩阵相乘。但是，近期研究发现张量分解在 GPU 上甚至更慢，尽管它减少了 75%的 FLOPs。本文研究人员对此进行了调查，发现原因在于最近的 CUDNN 库专为 3×3 卷积优化：当然不能简单地认为 3×3 卷积的速度是 1×1 卷积速度的 1/9。

总结如下就是：

1. **影响模型运行速度还有别的指标，例如 MAC(memory access )，并行度(degree of parallelism)**
2. **不同平台有不同的加速算法，这导致 flops 相同的运算可能需要不同的运算时间。**

据此，提出了高效网络架构设计应该考虑的两个基本原则：第一，应该用直接指标(例如速度)替换间接指标(例如 FLOPs)；第二，这些指标应该在目标平台上进行评估。在这项研究中，作者遵循这两个原则，并提出了一种更加高效的网络架构。

### 1.2 4 条高效网络设计原则

**G1. 相同的通道宽度可最小化内存访问成本(MAC)**

假设内存足够大一次性可存储所有特征图和参数；卷积核大小为$1*1$ ；输入通道有 c1 个；输出通道有 c2 个；特征图分辨率为$h*w$，则在 1x1 的卷积上，FLOPS：$B = hwc1c2$，$MAC = hw(c1+c2)+c1c2$，容易推出：

![](http://static.zybuluo.com/huanghaian/koq1yvjfyak8fzwbi3mijg1o/image.png)

MAC 是内存访问量，hwc1 是输入特征图内存大小，hwc2 是输出特征图内存大小，c1xc2 是卷积核内存大小。从公式中我们可以得出 MAC 的一个下界，即当 c1==c2 时，MAC 取得最小值。以上是理论证明，下面是实验证明：

![](http://static.zybuluo.com/huanghaian/yv64a5caqrqjuzkopbidkzxo/image.png)

可以看出，当 c1==c2 时，无论在 GPU 平台还是 ARM 平台，均获得了最快的 runtime。

**G2. 过度使用组卷积会增加 MAC**

和(1)假设一样，设 g 表示分组数，则有：

![](http://static.zybuluo.com/huanghaian/gf05fj9iqia4p0dlcbdf658n/image.png)

其中$B = hwc1c2/g$，当固定 c1 w h 和 B，增加分组 g，MAC 也会增加，证明了上述说法。其中 c1 w h 固定，g 增加的同时 B 复杂度也要固定，则需要把输出通道 c2 增加，因为 g 增加，复杂度是要下降的。以上是理论证明，下面是实验证明：

![](http://static.zybuluo.com/huanghaian/rb7b27ae7615zpjthifnkx61/image.png)

可以看出，g 增加，runtime 减少。

**G3. 网络碎片化（例如 GoogLeNet 的多路径结构）会降低并行度**

理论上，网络的碎片化虽然能给网络的 accuracy 带来帮助，但是在平行计算平台（如 GPU）中，网络的碎片化会引起并行度的降低，最终增加 runtime，同样的该文用实验验证:

![](http://static.zybuluo.com/huanghaian/0o753w4ukjdpr9cj33opdjmg/image.png)

可以看出，碎片化越多，runtime 越大。

**G4. 元素级运算不可忽视**

![](http://static.zybuluo.com/huanghaian/1cbbnr18bb23ooioygxn7n16/image.png)

element-wise operations 也占了不少 runtime，尤其是 GPU 平台下，高达 15%。ReLU、AddTensor 及 AddBias 等这一类的操作就属于 element-wise operations. 这一类操作，虽然 flops 很低，但是 MAC 很大，因而也占据相当时间。同样地，通过实验分析 element-wise operations 越少的网络，其速度越快。

![](http://static.zybuluo.com/huanghaian/2yhpfoz9vwiex04obsi66rwl/image.png)

以上就是作者提出的 4 点设计要求，总结如下

* 1x1 卷积进行平衡输入和输出的通道大小
* 组卷积要谨慎使用，注意分组数
* 避免网络的碎片化
* 减少元素级运算

结合上述 4 点可以对目前移动端网络进行分析：

* ShuffleNet v1 严重依赖组卷积(违反 G2)和瓶颈形态的构造块(违反 G1)
* MobileNetV2 使用倒置的瓶颈结构(违反 G1)，并且在“厚”特征图上使用了深度卷积和 ReLU 激活函数(违反了 G4)
* 自动生成结构的碎片化程度很高，违反了 G3

### 1.3 算法模型

在 ShuffleNet v1 的模块中，大量使用了 1x1 组卷积，这违背了 G2 原则，另外 v1 采用了类似 ResNet 中的瓶颈层(bottleneck layer)，输入和输出通道数不同，这违背了 G1 原则。同时使用过多的组，也违背了 G3 原则。短路连接中存在大量的元素级 Add 运算，这违背了 G4 原则。

基于以上缺点，作者重新设计了 v2 版本。V2 主要结构是多个 blocks 堆叠构成，block 又分为两种，一种是带通道分离的 Channel Spilit，一种是带 Stride=2。图示如下：

![](http://static.zybuluo.com/huanghaian/z9yknh3yyxg9tju06uyu1s6o/image.png)

(a)为 v1 中的 stride=1 的 block，(b)为 v1 中的 stride=2 的 block，(c)是新设计的 v2 中的 stride=1 的 block，(d)是新设计的 v2 中的 stride=2 的 block。

在每个单元开始，c 特征通道的输入被分为两支，分别占据 c−c'和 c'个通道(一般设置 c=c'\*2)。左边分支做同等映射，右边的分支包含 3 个连续的卷积，并且输入和输出通道相同，这符合 G1。而且两个 1x1 卷积不再是组卷积，这符合 G2，另外两个分支相当于已经分成两组。两个分支的输出不再是 Add 元素，而是 concat 在一起，紧接着是对两个分支 concat 结果进行 channle shuffle，以保证两个分支信息交流。其实 concat 和 channel shuffle 可以和下一个模块单元的 channel split 合成一个元素级运算，这符合原则 G4。

对于 stride=2 的模块，不再有 channel split，而是每个分支都是直接 copy 一份输入，每个分支都有 stride=2 的下采样，最后 concat 在一起后，特征图空间大小减半，但是通道数翻倍。

具体代码上非常简单，如下所示：

```python
class InvertedResidual(nn.Module):
    def __init__(self,
                 in_channels,
                 out_channels,
                 stride=1,
                 conv_cfg=None,
                 norm_cfg=dict(type='BN'),
                 act_cfg=dict(type='ReLU'),
                 with_cp=False):
        super(InvertedResidual, self).__init__()
        self.stride = stride
        branch_features = out_channels // 2
        if self.stride > 1:
            # 当stride=2才需要
            self.branch1 = nn.Sequential(
                ConvModule(
                    in_channels,
                    in_channels,
                    kernel_size=3,
                    stride=self.stride,
                    padding=1,
                    groups=in_channels,
                    conv_cfg=conv_cfg,
                    norm_cfg=norm_cfg,
                    act_cfg=None), # 没有激活函数
                ConvModule(
                    in_channels,
                    branch_features,
                    kernel_size=1,
                    stride=1,
                    padding=0,
                    conv_cfg=conv_cfg,
                    norm_cfg=norm_cfg,
                    act_cfg=act_cfg),
            )
        # 和mobilenetv2中的反转残差块差不多，但是激活函数relu位置不一样
        self.branch2 = nn.Sequential(
            ConvModule(
                in_channels if (self.stride > 1) else branch_features,
                branch_features,
                kernel_size=1,
                stride=1,
                padding=0,
                conv_cfg=conv_cfg,
                norm_cfg=norm_cfg,
                act_cfg=act_cfg),
            ConvModule(
                branch_features,
                branch_features,
                kernel_size=3,
                stride=self.stride,
                padding=1,
                groups=branch_features,
                conv_cfg=conv_cfg,
                norm_cfg=norm_cfg,
                act_cfg=None),
            ConvModule(
                branch_features,
                branch_features,
                kernel_size=1,
                stride=1,
                padding=0,
                conv_cfg=conv_cfg,
                norm_cfg=norm_cfg,
                act_cfg=act_cfg))

    def forward(self, x):
        def _inner_forward(x):
            if self.stride > 1:
                # stride=2的模块
                out = torch.cat((self.branch1(x), self.branch2(x)), dim=1)
            else:
                # stride=1的模块，直接spilt，x1不变，x2进行逐深度可分离卷积计算
                x1, x2 = x.chunk(2, dim=1)
                out = torch.cat((x1, self.branch2(x2)), dim=1)
            out = channel_shuffle(out, 2)
            return out
        return _inner_forward(x)
```

以上两个部分就可以构成各种 block，在 block 基础上构造网络，如下图：

![](http://static.zybuluo.com/huanghaian/hizmp2of4hnma9musbbjijjr/image.png)

同样的有宽度因子来对通道进行缩减，主要 0.5、1.0、1.5 和 2.0 4 中配置。

## 2 实验结果

![](http://static.zybuluo.com/huanghaian/cbsa35zn12p8zh174ooa8l0r/image.png)

可以看出，和 v1 相比，在相同 FLOP 情况下，错误率更低，且和其他模型对比，复杂度明显下降。

![](http://static.zybuluo.com/huanghaian/uydjzgeuat8nenxjg1m2am79/image.png)

上述结果是在 coco 目标检测上的结果，输入图像大小是 800×1200。FLOPs 行列出了输入图像大小为 224×224 时的复杂度水平。带\*的 ShuffleNet v2 是变种，区别是扩大了感受野，具体为在每个 block 的第一个 pointwise 卷积的前面插入一个额外的 3x3 深度卷积。可以看出，精度提高不少，且 FLOPs 仅仅增加一点。这就留下一个疑问：如何增加网络的感受野也是一个值得探讨的问题。

## 3 总结

ShuffleNet v2 针对目前移动端模型都仅仅考虑精度和 FLOPS 参数，但是这两个参数无法直接反映真正高效网络推理要求，故作者提出了 4 条高效网络设计准则，全面考虑了各种能够影响速度和内存访问量的因素，并给出了设计规则。再此基础上设计了 ShuffleNet v2，实验结果表明速度和精度更加优异。

## 4 参考文献

[ShuffleNet v2：轻量级 CNN 网络中的桂冠](https://zhuanlan.zhihu.com/p/48261931) [ShuffleNet V2: Practical Guidelines for Efficient CNN Architecture Design](https://arxiv.org/abs/1807.11164)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://openmmlabbook.gitbook.io/openmmlab-book/image-classification/mobile/shufflenetv2.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
