ShuffleNet v1是由旷视科技在2017年底提出的轻量级可用于移动设备的卷积神经网络。
该网络创新之处在于,使用 group convolution还有channel shuffle,保证网络准确率的同时,大幅度降低了所需的计算资源。
在近期的网络中,pointwise convolution(1X1conv)的出现使得所需计算量极大的增多,于是作者提出了pointwise group convolution来降低计算量,但是group与group之间的几乎没有联系,影响了网络的准确率,于是作者又提出了channel shuffle来加强group之间的联系。在一定计算复杂度下,网络允许更多的通道数来保留更多的信息,这恰恰是轻量级网络所追求的。
就想解读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 map,而普通卷积只能生成一个feature map。
所以深度分离卷积几乎是构造轻量高效模型的必用结构,如Xception, MobileNet, MobileNet V2, ShuffleNet, ShuffleNet V2, CondenseNet等轻量型网络结构中的必用结构。 关于逐层卷积的具体解释详见我关于MobileNet系列的文章。
对于上述的Group Convolution,很容易想到一个问题就是在卷积的时候,仅仅是将这一个Group的特征图进行了融合,但是不同的组别之间缺没有充分的连接,长此以往,不同的特征图对于对方的了解就越来越少,虽然网络的全连接层会帮助不同特征图相互连接,但是可以预想的是这样的连接融合的次数较少,不如不分组的情况。
基于上述的情况,作者提出把每个组的特征图分为一定组在每一层都进行一定程度的乱序结合,以这样的方式增加特征图的连接融合次数,过程如下图所示:
如上图(a)是正常的组卷积模式,不同分组(不同颜色表示不同分组)几乎没有信息交流;(b)和(c)描述的是channel shuffle的方式。
整个单元其实比较好理解,直接上图如下:
如同所示(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全局池化。
下图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 倍。下表是作者的一些实验数据。
论文发现,作为衡量计算复杂度的指标,FLOPs实际并不等同于速度。FLOPs相似的网络,其速度却有较大的差别,只用FLOPs作为衡量计算复杂度的指标是不够的,还要考虑内存访问消耗以及GPU并行。基于上面的发现,论文从理论到实验列举了轻量级网络设计的5个要领,然后再根据设计要领提出ShuffleNet V2。
G1: Equal channel width minimizes memory access cost (MAC).
相同维度的通道数将最小化内存访问成本,如下图所示,当input channles = output channels时,每秒处理的照片数量越多。
G2: Excessive group convolution increases MAC
过多的分组卷积会加大内存访问成本,如下图所示,越多的分组会导致速度急速下降,特别是在GPU上,下降的十分严重,一个显卡跑的话,8个Group Convolution会使得速度下降4倍!(这一这里作者依旧是在不同的条件下使用不同的通道数保证FLOPs是一样的)
G3: Network fragmentation reduces degree of parallelism
碎片操作将减小网络的平行度,这里的碎片操作指的是将一个大的卷积操作分为多个小的卷积操作进行。作者这里使用自己搭建了一些网络进行验证,网络的结构如下:
在实际设备上进行对比,在固定FLOPs情况下,分别对比串行和并行分支结构的性能。结果如上图所示,这里有一个比较有趣的结果,就是我们认为可能增加并行度的平行结构,最后居然减低了速度,不过这里由于还有下一个guide line的实验说到了元素级的操作也会对速度有一定的影响,因此这里还不能下定论到底是因为平行还是因为最后的相加拉低了时间。
G4: Element-wise operations are non-negligible
随后作者分析道最近的一些比较火的网络结构:
ShuffleNetV1违反了G2,bottleneck的结构违反了G1,而MobileNetV2使用的inverse bottleneck的结构违反了G1,其中夹杂的DWconv和Relu都违反了G4,自动生成结构(auto-generated structures)高度碎片化违反了G3。
作者首先复盘了ShuffleNetV1,认为目前比较关键的问题是如何在全卷积或者分组卷积中维护大多数的卷积是输入通道与输出通道相等的。针对这个目标,作者提出了Channel Split的操作,同时构建了ShuffleNetV2 的unit,如下图所示:
如上图所示:(a)(b)对应shufflenetV1是uints; (c),(d)对应改进后的V2版units。
这里结合个人看法说说这么做的好处:
小疑问?分组卷积看似在减少运算参数,但是却影响了运行速度;那么究竟怎么权衡?
小疑问?既然不需要降维,那么第一个1X1的conv还是否有存在的必要?
小疑问?对于残差结构来说,concat操作和add操作到底哪个更好用?另外,由于捷径分支不在是空集操作,那么这样的结构是否还符合short-cut的初衷(即bottleneck学到的是残差Residual部分)?但是可以想到的是经过后面的Channel Shuffle的乱序之后,每个通道应该都会经过一次bottleneck结构。
最后, 给出ShuffleNetV2的网络结构详细参数:
值得注意的是:channel数都比较的小, 这里作者并没有特别的解释这个现象(按照MobileNetV2中对于Relu的分析,这种数量的通道设计不太适合relu激活函数)。
这里给出模型搭建的python代码(基于pytorch实现)。完整的代码是基于图像分类问题的(包括训练和推理脚本,自定义层等)详见我的GitHub:完整代码链接
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"
`.
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"
`.
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网络结构,具体讨论和分析见上文。