ShuffleNet v2:https://link.springer.com/chapter/10.1007/978-3-030-01264-9_8
pytorch代码:https://github.com/Randl/ShuffleNetV2-pytorch/blob/master/model.py
keras代码:https://github.com/opconty/keras-shufflenetV2
目前大部分的模型加速和压缩文章在对比加速效果时用的指标都是FLOPs(float-point operations),这个指标主要衡量的就是卷积层的乘法操作。但是这篇文章通过一系列的实验发现FLOPs并不能完全衡量模型速度,这主要有一下两个原因:
因此,作者提出了两个设计网络应该考虑的两个准则:
在论文接下来的部分,作者首先通过实验得出高效网络设计的四个准则,然后根据这些准则对现有ShuffleNet v1进行改进,提出ShuffleNet v2。
作者首先对现有一些网络各个部分占用的时间进行了实验统计,如图1
从图1可以看出,FLOPs只考虑了卷积操作,虽然在网络运行时间中占了很大比重,但像data I/O, datashuffle and element-wise operations (AddTensor, ReLU, etc) 也是不容忽视的,因此只考虑FLOPs是不合理的,鉴于上述考虑,作者设计了四组对比实验来探究网络设计的高效准则。
结论是卷积层的输入和输出特征通道数相等时MAC最小,此时模型速度最快。
轻量级网络通常采用通道分离卷积(depthwise seperate conviolution), 而当中pointwise convolution(也就是11卷积)占了很大比重的计算复杂度,所以作者以11卷积为例,研究了11卷积与MAC之间的关系。
假设一个11卷积层的输入特征通道数是 c 1 c_{1} c1,输出特征尺寸是h和w,输出特征通道数是 c 2 c_{2} c2,那么这样一个1*1卷积层的FLOPs即为 B = h w c 1 c 2 B=hwc_{1}c_{2} B=hwc1c2.
那么MAC,即 内存访问操作数为:
M A C = h w ( c 1 + c 2 ) + c 1 c 2 ( 1 ) \mathrm{MAC}=h w\left(c_{1}+c_{2}\right)+c_{1} c_{2}\text{ }\text{ }\text{ }\text{ }(1) MAC=hw(c1+c2)+c1c2 (1)
其中 h w c 1 hwc_{1} hwc1表示输入特征所需存储空间, h w c 2 hwc_{2} hwc2表示输出特征所需存储空间, c 1 c 2 c_{1}c_{2} c1c2表示卷积核所需存储空间。
根据均值不等式( a + b > = 2 a b a+b>=2\sqrt{ab} a+b>=2ab), 将(1)式前半部分运用均值不等式,并将便两个用B进行替换得到:
M A C ≥ 2 h w B + B h w ( 2 ) \mathrm{MAC} \geq 2 \sqrt{h w B}+\frac{B}{h w}\text{ }\text{ }\text{ }\text{ }(2) MAC≥2hwB+hwB (2)
因此等式成立的条件是c1=c2,也就是输入特征通道数和输出特征通道数相等时,在给定FLOPs前提下,MAC达到取值的下界。
因此就有了Table1这个实验,这些实验的网络是由10个block组成,每个block包含2个1*1卷积层,第一个卷积层的输入输出通道分别是c1和c2,第二个卷积层相反。4行结果分别表示不同的c1:c2比例,但是每种比例的FLOPs都是相同的,可以看出在c1和c2比例越接近时,速度越快,尤其是在c1:c2比例为1:1时速度最快。这和前面介绍的c1和c2相等时MAC达到最小值相对应。
结论是过多的group操作会增大MAC,从而使模型速度变慢。
在设计网络时,为了减小网络的计算复杂度,会采用分组卷积,一方面对于给定FLOPs,分组卷积会增加网络的宽度,提高模型的容量;另一方面, 增加的filter channels 也会增减MAC。
对于1 *1分组卷积,MAC 和FLOPs的关系为:
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 ( 3 ) \begin{aligned} \mathrm{MAC} &=h w\left(c_{1}+c_{2}\right)+\frac{c_{1} c_{2}}{g} \\ &=h w c_{1}+\frac{B g}{c_{1}}+\frac{B}{h w} \end{aligned}\text{ }\text{ }\text{ }\text{ }(3) MAC=hw(c1+c2)+gc1c2=hwc1+c1Bg+hwB (3)
其中 B = h w c 1 c 2 / g B = hwc_{1}c_{2}/g B=hwc1c2/g, 从(3)式可以看出对于给定输入 h w c 1 hwc_{1} hwc1和计算复杂度B , MAC会随着分组数g的增加而增大。
Table2是关于卷积的group操作对速度的影响,通过控制参数c可以使得每个实验的FLOPs相同,可以看出随着g的不断增大,c也不断增大,这和前面说的在基本不影响FLOPs的前提下,引入group操作后可以适当增加网络宽度吻合。从速度上看,group数量的增加对速度的影响还是很大的,原因就是group数量的增加带来MAC的增加(公式3),而MAC的增加带来速度的降低。
网络结构设计上,文章用了一个词:fragment,翻译过来就是分裂的意思,可以简单理解为网络的支路数量。为了研究fragment对模型速度的影响,作者做了Table3这个实验,采用的结构如图2, 可以看出在相同FLOPs的情况下,单卷积层(1-fragment)的速度最快。因此模型支路越多(fragment程度越高)对于并行计算越不利,这样带来的影响就是模型速度变慢,比如Inception、NASNET-A这样的网络。
结论是element-wise操作所带来的时间消耗远比在FLOPs上的体现的数值要多,因此要尽可能减少element-wise操作。
Element-wise操作主要包括:ReLU, AddTensor, AddBias, 另外对于depthwise convolution这种高MAC/FLOPs比率的操作,也归结为Element-wise operation.
Table4的实验是基于ResNet的bottleneck进行的,short-cut其实表示的就是element-wise操作,因为有AddTensor操作。
ShuffleNet v1结构如图3 (a) (b)所示,采用了pointwise group convolution 和 bottleneck-like 结构, 并且采用了”channel shuffle“, 将不同组的信息建立关联。根据第一部分中讨论结果, pointwise group convolution 和 bottleneck-like 结构违背了G2和G1, 太多的分组也会违背G3以及shortcut中”Add“操作违背了G4。
ShuffleNet v2结构如图3 (c ), 首先在开始处增加了一个channel split操作,这个操作将输入特征的通道分成c-c’和c’,c’在文章中采用c/2,其中一个分支采用identity连接(与G3相对应,没有任何fragmentation), 另一个分支采用相同输入和输出通道的三个卷积层(与G1相对应),其中两个1*1的卷积层也没有采用分组卷积(与G2相对应),其实channel split已经产生了分组的效果了。卷积完之后,将两个分支的结果concatenate一起,保证整个shuffle unit的输入和输出通道数保持一致(与 G1相对应),最后利用channel shuffle操作将concatenate后的信息进行混合。值得注意的是,ShuffeNetv2没有采用v1中的”Add“操作(与G4相对应)。当网络进行下采样时,采用的ShuffleNet Unit 如图3(d)所示,可以看出经过该模块特征图缩小一般,网络宽度加倍。
整个网络结构如Table 5,在stage2-4, 采用堆叠ShuffleNet Unit单元的方式,遇到下采样时采用图3(d)的结构,另外在全局平均池化层之前采用1*1的卷积将输入的特征进行混合。另外,采用类似MobileNet 通道缩放方式,来构建不同复杂度的网络。
ShuffleNet v2不光速度快而且具有较高的正确率,作者将其归结于两个原因:
准确率&速度
Table 8. Comparison of several network architectures over classification error (on
validation set, single center crop) and speed, on two platforms and four levels of computation complexity. Results are grouped by complexity levels for better comparison.The batch size is 8 for GPU and 1 for ARM. The image size is 224 × 224 except: [*]160×160 and [**] 192×192. We do not provide speed measurements for CondenseNets[10] due to lack of efficient implementation currently
Table8是关于一些模型在速度、精度、FLOPs上的详细对比。实验中不少结果都和前面几点发现吻合,比如MobileNet v1速度较快,很大一部分原因是因为简单的网络结构,没有太多复杂的支路结构;IGCV2和IGCV3因为group操作较多,所以整体速度较慢;Table8最后的几个通过自动搜索构建的网络结构,和前面的第3点发现对应,因为支路较多,所以速度较慢。
object detection
Table7是在COCO数据集上的速度和精度对比。ShuffleNet v2是指在每个block的第一个pointwise卷积层前增加一个33的depthwise卷积层,目的是增加感受野,这样有助于提升检测效果(受Xception启发)。
最终,在分类正确率排名上ShuffleNet v2 ≥ MobileNet v2 > ShuffeNet v1 > Xception
在目标检测正确率排名上ShuffleNet v2 > Xception ≥ ShuffleNet v1 ≥ MobileNet v2
ShuffleNet v2 with SE
加入SE结构后,在牺牲一定速度的前提下正确率提升了0.5%,具体结构如图4所示。
泛化到大模型的能力
ShuffleNet v2可以泛化到大模型( FLOPs>=2G ), 作者构建了50层的ShuffleNet v2 大模型,相比于ResNet 50 ,有少于40%的计算代价,有更高的准确率。结构如Appendix Table 2,实验结果如Table 6.
采用tensorflow2.0, tf.keras进行编写。
# -*- coding: UTF-8 -*-
"""
shufflenetv2 in pytorch
[1] Ningning Ma, Xiangyu Zhang, Hai-Tao Zheng, Jian Sun
ShuffleNet V2: Practical Guidelines for Efficient CNN Architecture Design
https://arxiv.org/abs/1807.11164
"""
import os
import tensorflow as tf
import tensorflow.keras as keras
from tensorflow.keras import Sequential, layers
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
def channel_split(x, num_splits=2):
"""split the input tensor into two equal dimension
Args:
x: the input tensor
num_splits: the numbers of tensors after splitting
"""
if num_splits == 2:
return tf.split(x, axis=3, num_or_size_splits=num_splits)
else:
raise ValueError('The num_splits is 2')
def channel_shuffle(x, groups):
"""channel shuffle operation.
Args:
x: the input tensor
groups: input branch number
"""
_, height, width, channels = x.shape
channels_per_group = channels // groups
x = tf.reshape(x, [-1, height, width, groups, channels_per_group])
x = tf.transpose(x, perm=[0, 1, 2, 4, 3])
x = tf.reshape(x, [-1, height, width, channels])
return x
class SELayer(keras.Model):
"""this is the implement of SE unit."""
def __init__(self, out_channels, reduction=16):
super(SELayer, self).__init__()
self.avg_pool = layers.GlobalAveragePooling2D()
self.fc = Sequential([
layers.Dense(out_channels//reduction),
layers.Activation('relu'),
layers.Dense(out_channels),
layers.Activation('sigmoid')
])
def call(self, inputs, training=None):
_, _, _, c = inputs.shape
out = self.avg_pool(inputs)
out = self.fc(out)
out = tf.reshape(out, [-1, 1, 1, c])
return inputs * out
class ShuffleNetUnit(keras.Model):
"""this is the implement of shufflenet v2 unit including stride=1 and 2."""
def __init__(self, out_channels, stride=1, se=False):
super(ShuffleNetUnit, self).__init__()
self.stride = stride
self.se = se
self.out_channels = out_channels//2
self.residual = Sequential([
layers.Conv2D(self.out_channels, (1, 1), use_bias=False),
layers.BatchNormalization(),
layers.Activation('relu'),
layers.DepthwiseConv2D((3, 3), strides=self.stride, padding='same', use_bias=False),
layers.BatchNormalization(),
layers.Conv2D(self.out_channels, (1, 1), use_bias=False),
layers.BatchNormalization(),
layers.Activation('relu')
])
if stride == 1:
self.short_cut = Sequential()
else:
self.short_cut = Sequential([
layers.DepthwiseConv2D((3, 3), strides=self.stride, padding='same', use_bias=False),
layers.BatchNormalization(),
layers.Conv2D(self.out_channels, (1, 1), use_bias=False),
layers.BatchNormalization(),
layers.Activation('relu')
])
if self.se:
self.se_layer = SELayer(self.out_channels)
def call(self, inputs, training=None):
if self.stride == 1:
residual, short_cut = channel_split(inputs)
else:
residual, short_cut = inputs, inputs
residual = self.residual(residual)
short_cut = self.short_cut(short_cut)
if self.se:
residual = self.se_layer(residual)
out = layers.concatenate([residual, short_cut], axis=-1)
out = channel_shuffle(out, 2)
return out
class ShuffleNetV2(keras.Model):
"""ShuffleNet v2 implement."""
def __init__(self, scale, se=False, num_classes=1000):
super(ShuffleNetV2, self).__init__()
self.se = se
if scale == 0.5:
out_channels = [48, 96, 192, 1024]
elif scale == 1:
out_channels = [116, 232, 464, 1024]
elif scale == 1.5:
out_channels = [176, 352, 704, 1024]
elif scale == 2:
out_channels = [244, 488, 976, 2048]
else:
raise ValueError('The value of scale must be of [0.5, 1, 1.5, 2]')
self.conv1 = Sequential([
layers.Conv2D(24, (3, 3), strides=2, padding='same', use_bias=False),
layers.BatchNormalization()
])
self.max_pool = layers.MaxPool2D((3, 3), strides=2, padding='same')
self.stage2 = self._make_stage(3, out_channels[0])
self.stage3 = self._make_stage(7, out_channels[1])
self.stage4 = self._make_stage(3, out_channels[2])
self.conv5 = Sequential([
layers.Conv2D(out_channels[3], (1, 1), use_bias=False),
layers.BatchNormalization(),
layers.Activation('relu')
])
self.avg_pool = layers.GlobalAveragePooling2D()
self.fc = layers.Dense(num_classes)
def call(self, inputs, training=None):
out = self.conv1(inputs)
out = self.max_pool(out)
out = self.stage2(out)
out = self.stage3(out)
out = self.stage4(out)
out = self.conv5(out)
out = self.avg_pool(out)
out = self.fc(out)
return out
def _make_stage(self, num_stages, out_channels):
layers = []
layers.append(ShuffleNetUnit(out_channels, stride=2, se=self.se))
for i in range(num_stages):
layers.append(ShuffleNetUnit(out_channels, stride=1, se=self.se))
return Sequential(layers)
if __name__ == '__main__':
model = ShuffleNetV2(scale=2, se=False)
model.build(input_shape=(None, 224, 224, 3))
model.summary()
print(model.predict(tf.ones((10, 224, 224, 3))).shape)