参考资料:
论文:
ShuffleNet V2: Practical Guidelines for Efficient CNN Architecture Design
博客:
一、摘要
当前,神经网络结构的设计基本由间接的计算复杂度主导,例如 FLOPs
,但是直接的度量如速度,还取决于其他因素,例如内存的获取损耗和平台特性。因此,我们将使用直接的标准衡量,而不仅仅是 FLOPs
。因此本文建议直接在目标平台上用直接度量进行测试。基于一系列控制条件实验,作者提出了设计高效网络结构的一些实用指导思想,并据此提出了一个称之为 ShuffleNet V2
的新结构。综合的对比实验证明了作者的模型在速度和准确性上取得了最佳的平衡(state-of-the-art
)。
二、前言
2.1 FLOPs
和FLOPS
参考资料:
(1)FLOPs:英文命名为floating point operations
,注意s是小写,指浮点运算数,理解为计算量,可以用来衡量算法/模型的复杂度。
(2)FLOPS:全大写,指每秒浮点运算次数,可以理解为计算的速度。是衡量硬件性能
的一个指标。
2.2 FLOPs
和MACs
Multiply–Accumulate Operations:乘加累积操作数,常常被人们与FLOPs概念混淆。实际上1MACs包含一个乘法操作与一个加法操作,大约包含2FLOPs。通常MACs与FLOPs存在一个2倍的关系。
(1)全连接网络中FLOPs的计算:
F L O P s = ( 2 × I − 1 ) × O FLOPs=(2×I−1)×O FLOPs=(2×I−1)×O
其中:
I = input neuron numbers(输入神经元的数量)
O = output neuron numbers(输出神经元的数量)
不考虑bias时有-1,有bias时没有-1;
2是因为一个MAC算2个operations;
(2)卷积网络中FLOPs的计算:
F L O P s = ( 2 C i n K 2 − 1 ) × H × W × C o u t FLOPs=(2C_{in}K^2−1)\times H\times W\times C_{out} FLOPs=(2CinK2−1)×H×W×Cout
其中:
- C i n C_{in} Cin= input channel
- K K K= kernel size
- H H H、 W W W= output feature map size
- C o u t C_{out} Cout= output channel
- 2是因为一个MAC算2个operations;
有时为了简写,会计算MACs
M
A
C
s
=
(
C
i
n
K
2
)
×
H
×
W
×
C
o
u
t
MACs=(C_{in}K^2)\times H\times W\times C_{out}
MACs=(CinK2)×H×W×Cout
(3)计算FLOPs的代码或包:
torchstat
:可以用来计算pytorch构建的网络的参数,空间大小,MAdd,FLOPs等指标。
from torchstat import stat
import torchvision.models as models
model = models.alexnet()
stat(model, (3, 224, 224))
打印结果如下:
[MAdd]: AdaptiveAvgPool2d is not supported!
[Flops]: AdaptiveAvgPool2d is not supported!
[Memory]: AdaptiveAvgPool2d is not supported!
[MAdd]: Dropout is not supported!
[Flops]: Dropout is not supported!
[Memory]: Dropout is not supported!
[MAdd]: Dropout is not supported!
[Flops]: Dropout is not supported!
[Memory]: Dropout is not supported!
module name input shape output shape params memory(MB) MAdd Flops MemRead(B) MemWrite(B) duration[%] MemR+W(B)
0 features.0 3 224 224 64 55 55 23296.0 0.74 140,553,600.0 70,470,400.0 695296.0 774400.0 52.34% 1469696.0
1 features.1 64 55 55 64 55 55 0.0 0.74 193,600.0 193,600.0 774400.0 774400.0 5.13% 1548800.0
2 features.2 64 55 55 64 27 27 0.0 0.18 373,248.0 193,600.0 774400.0 186624.0 11.05% 961024.0
3 features.3 64 27 27 192 27 27 307392.0 0.53 447,897,600.0 224,088,768.0 1416192.0 559872.0 1.12% 1976064.0
4 features.4 192 27 27 192 27 27 0.0 0.53 139,968.0 139,968.0 559872.0 559872.0 0.00% 1119744.0
5 features.5 192 27 27 192 13 13 0.0 0.12 259,584.0 139,968.0 559872.0 129792.0 0.33% 689664.0
6 features.6 192 13 13 384 13 13 663936.0 0.25 224,280,576.0 112,205,184.0 2785536.0 259584.0 0.22% 3045120.0
7 features.7 384 13 13 384 13 13 0.0 0.25 64,896.0 64,896.0 259584.0 259584.0 0.11% 519168.0
8 features.8 384 13 13 256 13 13 884992.0 0.17 299,040,768.0 149,563,648.0 3799552.0 173056.0 0.33% 3972608.0
9 features.9 256 13 13 256 13 13 0.0 0.17 43,264.0 43,264.0 173056.0 173056.0 0.00% 346112.0
10 features.10 256 13 13 256 13 13 590080.0 0.17 199,360,512.0 99,723,520.0 2533376.0 173056.0 0.33% 2706432.0
11 features.11 256 13 13 256 13 13 0.0 0.17 43,264.0 43,264.0 173056.0 173056.0 0.11% 346112.0
12 features.12 256 13 13 256 6 6 0.0 0.04 73,728.0 43,264.0 173056.0 36864.0 0.00% 209920.0
13 avgpool 256 6 6 256 6 6 0.0 0.04 0.0 0.0 0.0 0.0 8.04% 0.0
14 classifier.0 9216 9216 0.0 0.04 0.0 0.0 0.0 0.0 0.00% 0.0
15 classifier.1 9216 4096 37752832.0 0.02 75,493,376.0 37,748,736.0 151048192.0 16384.0 19.09% 151064576.0
16 classifier.2 4096 4096 0.0 0.02 4,096.0 4,096.0 16384.0 16384.0 0.11% 32768.0
17 classifier.3 4096 4096 0.0 0.02 0.0 0.0 0.0 0.0 0.00% 0.0
18 classifier.4 4096 4096 16781312.0 0.02 33,550,336.0 16,777,216.0 67141632.0 16384.0 0.56% 67158016.0
19 classifier.5 4096 4096 0.0 0.02 4,096.0 4,096.0 16384.0 16384.0 0.00% 32768.0
20 classifier.6 4096 1000 4097000.0 0.00 8,191,000.0 4,096,000.0 16404384.0 4000.0 1.12% 16408384.0
total 61100840.0 4.19 1,429,567,512.0 715,543,488.0 16404384.0 4000.0 100.00% 253606976.0
=======================================================================================================================================================
Total params: 61,100,840
-------------------------------------------------------------------------------------------------------------------------------------------------------
Total memory: 4.19MB
Total MAdd: 1.43GMAdd
Total Flops: 715.54MFlops
Total MemR+W: 241.86MB
2.3 FLOPs
和Speed
作者认为FLOPs是一种简介的测量指标,是一个近似值,并不是我们真正关心的。我们需要的是直接的指标,比如速度和延迟。如下图(c)和(d),横轴代表FLOPs,纵轴表示速度,可以看出四种不同的模型在FLOPs近似相等时,模型的运行速度是不相等的。因此,作者认为,使用FLOP作为计算复杂度的唯一指标是不充分的。
FLOPs和速度的不一致,作者认为有 2 个原因:
- 首先影响速度的不仅仅是FLOPs,如内存使用量(memory access cost,
MAC
),这不能忽略,对于GPUs来说可能会是瓶颈。另外模型的并行程度也影响速度,并行度高的模型速度相对更快。 - 另外一个原因,模型在不同平台上的运行速度(runninng time)是有差异的,如GPU和ARM,而且采用不同的库也会有影响。
首先,作者分析了两个经典结构 ShuffleNet v1 和 MobileNet v2 的运行时间:
从图 2 可以看出,虽然以 FLOPs
度量的卷积占据了大部分的时间,但其余操作也消耗了很多运行时间,比如数据输入输出、通道打乱和逐元素的一些操作(张量相加、激活函数)。因此,FLOPs 不是实际运行时间的一个准确估计。
三、高效网络设计的实用指导思想
针对以上的问题,作者结合理论与实验得到了 4 条实用的指导原则:
- G1:卷积层使用相同的输入输出通道数;
- G2:避免使用过多的分组卷积操作;
- G3:减少模型中的串并联分支数;
- G4:减少Element-wise操作,如ReLU,short-cut等;
之前轻量级神经网络体系结构的进展主要是基于 FLOPs
的度量标准,并没有考虑上述 4 个属性,比如:
ShuffleNet v1
严重依赖分组卷积,这违反了G2
;多处使用bottleneck结构块,有违G1
;MobileNet v2
利用了反转瓶颈结构,这违反了G1
,而且在通道数较多的扩展层使用ReLU
和深度卷积,违反了G4
,NAS
网络生成的结构碎片化很严重,这违反了G3
。
(G1)相同的通道宽度大小最小化内存访问量
对于轻量级CNN网络,常采用深度可分离卷积(depthwise separable convolutions),其中点卷积( pointwise convolution)即1x1卷积复杂度最大。
假设输入特征图大小为
h
×
w
×
c
1
h\times w\times c_1
h×w×c1,输出特征图的长宽不变,输出通道数为
c
2
c_2
c2,使用
1
×
1
1\times 1
1×1卷积核:
F
L
O
P
s
(
M
A
C
s
)
=
h
×
w
×
c
1
×
c
2
FLOPs(MACs)=h\times w\times c_1\times c_2
FLOPs(MACs)=h×w×c1×c2
论文中
FLOPs
的计算是把乘加当作一次浮点运算的,所以其实等效于我们通常理解的MACs
计算公式。
简单起见,我们假设计算设备的缓冲足够大能够存放下整个特征图和参数,那么 1×1 卷积层的内存访问代价(内存访问次数):
M
A
C
=
h
w
c
1
+
h
w
c
2
+
c
1
c
2
=
h
w
(
c
1
+
c
2
)
+
c
1
c
2
MAC=hwc_1+hwc_2+c_1c_2=hw(c_1+c_2)+c_1c_2
MAC=hwc1+hwc2+c1c2=hw(c1+c2)+c1c2
等式的三项分别代表输入特征图、输出特征图和权重参数的代价。
由均值不等式,我们有:
当且仅当 c1=c2 时MAC取得最小值。
论文中不仅给出了证明,同时也做了一系列的对照实验来证明,下表为不同输入输出通道比(c1:c2)下GPU和CPU下网络的处理速度,可以发现当c1=c2时,网络处理的速度最快。
【注意:这里要控制FLOPs不变,因此不仅调节c1、c2的比例,还会将他们的通道数进行一定的改变以达到FLOPs不变】
(G2)过多的分组卷积操作会增大MAC,从而使模型速度变慢
分组卷积是现在网络结构设计的核心,它通过通道之间的稀疏连接(也就是只和同一个组内的特征连接)来降低计算复杂度。一方面,它允许我们使用更多的通道数来增加网络容量进而提升准确率,但另一方面随着通道数的增多也对带来更多的 MAC
,MAC
消耗的越多,模型速度也就变慢了。
针对 1×1 的分组卷积,我们有:
分组卷积
FLOPs
的计算公式,参考 MobileNet v1 论文详解 有给出推导。
F L O P s = B = h × w × 1 × 1 × c 1 g × c 2 g × g = h w c 1 c 2 g FLOPs=B=h\times w\times 1\times 1\times \frac{c_1}{g} \times \frac{c_2}{g}\times g=\frac {hwc_1c_2}{g} FLOPs=B=h×w×1×1×gc1×gc2×g=ghwc1c2
M A C = h w ( c 1 + c 2 ) + c 1 c 2 g = h w c 1 + B g c 1 + B h w MAC=hw(c_1+c_2)+\frac{c_1c_2}g=hwc_1+\frac{Bg}{c_1}+\frac{B}{hw} MAC=hw(c1+c2)+gc1c2=hwc1+c1Bg+hwB
固定 c 2 g \frac{c_2}g gc2 的比值,又因为输入特征图 c 1 × h × w c_1×h×w c1×h×w 固定,从而也就固定了计算代价 B B B ,所以可得上式中 M A C MAC MAC 与 g g g 成正比的关系。
由下图可知,在B固定不变时,MAC和分组数成正相关,即分组卷积的分组数 g 越大, MAC 越大。
同样的,论文中对这部分也做了一系列的对照实验,下表为不同分组 g 下GPU和CPU下网络的处理速度,可以发现分组数越大,网络的处理速度越慢。
【注意:同样需要保持FLOPs不变,这里也是通过改变通道数c来调节FLOPs的,后面的所有对照实验都是保持FLOPs不变的】
很明显使用分组数多的网络速度更慢,比如 分为 8
个组要比 1
个组慢得多,主要原因在于 MAC
的增加 。因此,本文建议要根据硬件平台和目标任务谨慎地选择分组卷积的组数,不能简单地因为可以提升准确率就选择很大的组数,而忽视了内存访问代价(MAC
)的增加。
(G3)模型中的分支数量越少,模型速度越快
作者认为,模型中的网络结构太复杂(分支和基本单元过多)会降低网络的并行程度,模型速度越慢。
同样,作者还是做了实验验证以上猜想。每个模型由block组成,每个block由1×1卷积组成,分别将它们重复10次。可以看出在相同FLOPs的情况下,单卷积层(1-fragment)的速度最快。
至于上表中的x-fragment可在论文结尾附录第一张图找到,series表示串联,parallel表示并联,如下图:
(G4)Element-wise逐元素的操作不能被忽略
逐元素的操作在作者看来对耗时的影响也是不可忽略的,逐元素的操作包括Relu、Add等等。这部分同样没有理论上的证明,作者做了一系列的对照实验来进行说明,采用的是Resnet50的瓶颈结构(bottleneck),除去跨层链接short-cut和 ReLU:
下表展现了是否有Relu和short-cut对网络速度的影响,结果显示没有Relu和short-cut时网络速度更快。
四、ShuffleNet V2模型
先回顾ShuffleNet V1的 创新点 :
- pointwise group convolution 逐点分组卷积
- channel shuffle 通道混洗
Group Conv虽然能减少参数量和计算量,但Group Conv中不同组之间信息没有交流,,将输入特征分成g个互不相干的组,各走各走,组间没有交流,会降低网络的特征提取能力。为了解决这个问题,ShuffleNet V1给出的方法为 “交换通道” 。
因为在同一组中不同的通道蕴含的信息可能是相同的,如果不进行通道交换的话,学出来的特征会非常局限。如果在不同的组之后交换一些通道,那么就能交换信息,使得各个组的信息更丰富,能提取到的特征自然就更多,这样是有利于得到更好的结果。如图c所示,每组中都有其他所有组的特征。
但是ShuffleNet V1违背了相关的设计准则:
- (1)分组卷积增加了MAC,有违G2准则;
- (2)残差结构违背了G1;
- (3)多块串联违背了G3;
- (4)Add操作是元素级加法操作,违反了G4。
4.1 ShuffleNetV2 Unit
(1)基础单元
如图(c)所示:首先是Channel Split操作,将含c个通道的输入特征图一分为二,一部分含 c ′ c' c′个通道,则另一部分还 c − c ’ c-c’ c−c’个通道(论文中是这样表述的,通常是将输入通道数平均分成两份)。
【注意】:
- 这里虽然没有使用通道重排,但Split操作是类似Channel Shuffle的,但又没有使用分组卷积,十分巧妙!!!
左侧分支是一个恒等映射,右侧分支是三个卷积操作,最终将两部分Concat在一起。
【注意】:
- 这里使用的是Concat,不是Add,因此没有了逐元素操作带来的耗时,满足G4;
- 俩部分Concat结合在一起时,输入输出的通道数是一致的,满足G1;
再来看右侧的三个卷积,注意这时没有采用ShuffleNetV1 提出的分组卷积,而是仅仅使用1x1卷积,没有使用分组卷积自然不要Channel Shuffle,3x3的DWConv没有进行改变。
【注意】:
- 这里的三次卷积同样会满足输入输出的通道数相等,满足G1;
- 没有使用分组卷积,满足G2;
最后就是Concat后又进行了一次Channel Shuffle。
【注意】:
- 这里Channel Shuffle是保证两个分支间能进行信息交流。
(2)下采样单元
如图(d)所示:主要的改变是将DWConv的步长s调整为2,并删除了Channel Split的操作。这样做的目的就是要下采样,使特征图尺寸减半,通道数翻倍。并且在shortcut分支中使用3x3的DWConv和1x1的Conv代替3x3的平均池化。
4.2 网络结构
和 v1 一样,v2 的 block
的通道数是按照 0.5x 1x
等比例进行缩放,以生成不同复杂度的 ShuffleNet v2 网络,并标记为 ShuffleNet v2 0.5×、ShuffleNet v2 1× 等模型。
v1 和 v2 block 的区别在于, v2 在全局平均池化层(global averaged pooling)之前添加了一个 1×1 卷积Conv5来混合特征(mix up features)。
ShuffleNet v2
不仅高效而且精度也高。有两个主要理由:
- 一是高效的卷积
block
结构允许我们使用更多的特征通道数,网络容量较大。 - 二是当 c′=c/2 时,一半的特征图直接经过当前卷积 block 并进入下一个卷积 block,这类似于
DenseNet
和CondenseNet
的特征重复利用。
DenseNet 是一种具有密集连接的卷积神经网络。在该网络中,任何两层之间都有直接的连接,也就是说,网络每一层的输入都是前面所有层输出的并集,而该层所学习的特征图也会被直接传给其后面所有层作为输入。
在 DenseNet 论文中,作者通过画不同权重的 L1
范数值来分析特征重复利用的模式,如图 4(a)所示。可以看到,相邻层之间的关联性是远远大于其它层的,这也就是说所有层之间的密集连接可能是多余的,最近的论文 CondenseNet 也支持这个观点。
因此,和 DenseNet 一样,Shufflenet v2 的结构通过设计实现了特征重用模式,从而得到高精度,并具有更高的效率。
五、论文复现
参考:
【深度学习基础】PyTorch实现ShuffleNet-v2亲身实践
(1)Channel Split:
def split(x, groups):
# chunk方法可以对张量分块,返回一个张量列表:
# torch.chunk(tensor, chunks, dim=0) → List of Tensors
# chunks (int) – number of chunks to return(分割的块数)
# dim (int) – dimension along which to split the tensor(沿着哪个轴分块)
out = x.chunk(groups, dim=1)
return out
(2)Channel Shuffle:
def channel_shuffle(x, groups=2):
# (B, C, H, W)
batchsize, num_channels, height, width = x.size()
# 每个组有多少个channel
channels_per_group = num_channels // groups
# reshape: b, num_channels, h, w --> b, groups, channels_per_group, h, w
x = x.view(batchsize, groups, channels_per_group, height, width)
# Channel Shuffle
# torch.transpose(x,dim_1,dim_2)表示将x这个tensor的dim_1和dim_2进行交换
# 而contiguous方法就类似深拷贝,使得上面这些操作不会改变元数据
x = torch.transpose(x, 1, 2).contiguous()
# flatten
x = x.view(batchsize, -1, height, width)
return x
(3)ShuffleNetV2 Unit:
class ShuffleUnit(nn.Module):
def __init__(self, in_channels, out_channels, stride, groups):
super().__init__()
self.stride = stride
# 与ResNet不一样,这里对于每个Shuffle Unit的瓶颈结构的通道数量设置为输出通道的1/2
mid_channels = out_channels // 2
# 如图(d)所示结构
if stride > 1:
# (d)版本里面 两条分支的channels为out_channels的一半,最后合并起来得到out_channels_s2
out_channels_s2 = out_channels // 2
# shortcut分支
self.branch1 = nn.Sequential(
# 3x3 DWConv (groups=in_channels)
nn.Conv2d(in_channels, in_channels, 3, stride=stride, padding=1, groups=in_channels, bias=False),
nn.BatchNorm2d(in_channels),
nn.Conv2d(in_channels, out_channels_s2, 1, bias=False),
nn.BatchNorm2d(out_channels_s2),
nn.ReLU(inplace=True)
)
# 主干分支
self.branch2 = nn.Sequential(
nn.Conv2d(in_channels, mid_channels, 1, bias=False),
nn.BatchNorm2d(mid_channels),
nn.ReLU(inplace=True),
nn.Conv2d(mid_channels, mid_channels, 3, stride=stride, padding=1, groups=mid_channels, bias=False),
nn.BatchNorm2d(mid_channels),
nn.Conv2d(mid_channels, out_channels_s2, 1, bias=False),
nn.BatchNorm2d(out_channels_s2),
nn.ReLU(inplace=True)
)
# 如图(c)所示结构
else:
# 这里的input_channels_split实际上是通道数为in_channels的一半,比如规定输入为24,通过split分割之后为12
input_channels_split = in_channels // 2
# 最后得到的输出通道数要和input_channels_split加起来=规定的输出通道数out_channels
out_channels_s1 = out_channels - input_channels_split
self.branch1 = nn.Sequential() # shortcut分支的通道数是input_channels_split
self.branch2 = nn.Sequential(
nn.Conv2d(input_channels_split, mid_channels, 1, bias=False),
nn.BatchNorm2d(mid_channels),
nn.ReLU(inplace=True),
nn.Conv2d(mid_channels, mid_channels, 3, stride=stride, padding=1, groups=mid_channels, bias=False),
nn.BatchNorm2d(mid_channels),
nn.Conv2d(mid_channels, out_channels_s1, 1, bias=False),
nn.BatchNorm2d(out_channels_s1),
nn.ReLU(inplace=True)
)
def forward(self, x):
# 如果步长为1
if self.stride == 1:
# Channel Split将输入按groups=2分割
x1, x2 = split(x, groups=2)
# 输出分别经过2个分支
out = torch.cat((self.branch1(x1), self.branch2(x2)), dim=1)
# 如果步长为2,不需要先Channel Split,而是直接将输入通过两个分支
else:
out = torch.cat((self.branch1(x), self.branch2(x)), dim=1)
# Channel Shuffle
out = channel_shuffle(out, 2)
return out
(4)主体结构:
class ShuffleNetV2(nn.Module):
def __init__(self, groups, num_layers, num_channels, num_classes=1000):
super(ShuffleNetV2, self).__init__()
self.groups = groups
# first layer
self.conv1 = nn.Sequential(
nn.Conv2d(in_channels=3, out_channels=24,
kernel_size=3, stride=2, padding=1, bias=False),
nn.BatchNorm2d(24),
nn.ReLU(inplace=True),
)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
# 迭代shuffleNet_unit
self.stage2 = self.make_layers(24, num_channels[0], num_layers[0], groups)
self.stage3 = self.make_layers(num_channels[0], num_channels[1], num_layers[1], groups)
self.stage4 = self.make_layers(num_channels[1], num_channels[2], num_layers[2], groups)
# last layers
self.conv5 = nn.Sequential(
nn.Conv2d(num_channels[2], 1024, 1, bias=False),
nn.BatchNorm2d(1024),
nn.ReLU(inplace=True)
)
self.globalpool = nn.AvgPool2d(kernel_size=7, stride=1)
self.fc = nn.Linear(1024, num_classes)
def make_layers(self, in_channels, out_channels, num_layers, groups):
layers = []
# 每个stage的第一个shuffleNet_unit的stride=2,且只重复1次
layers.append(ShuffleUnit(in_channels, out_channels, stride=2, groups=groups))
# 更新此时的in_channels
in_channels = out_channels
for i in range(num_layers - 1):
layers.append(ShuffleUnit(in_channels, out_channels, stride=1, groups=groups))
return nn.Sequential(*layers)
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 = self.globalpool(x)
x = x.view(x.size(0), -1)
out = self.fc(x)
return x
def ShuffleNet_g2(**kwargs):
num_layers = [4, 8, 4]
num_channels = [48, 96, 192]
model = ShuffleNetV2(groups=2, num_layers=num_layers, num_channels=num_channels)
return model
def test():
net = ShuffleNet_g2()
#创建模型,部署gpu
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
net.to(device)
summary(net, (3, 224, 224))
if __name__ == '__main__':
test()