前言
前面我们学了MobileNetV1-3,从这篇开始我们学习ShuffleNet系列。ShuffleNet是Face++(旷视)在2017年发布的一个高效率可以运行在手机等移动设备的网络结构,论文发表在CVRP2018上。这个新的轻量级网络使用了两个新的操作:pointwise group convolution和 channel shuffle,在保持精度的同时大大降低计算成本。
学习资料:
- 论文题目:《ShuffleNet: An Extremely Efficient Convolutional Neural Network for Mobile Devices》(《ShuffleNet:一种用于移动设备的极其高效的卷积神经网络)
- 原文地址:ShuffleNet
- 项目地址:GitHub - jaxony/ShuffleNet: ShuffleNet in PyTorch. Based on https://arxiv.org/abs/1707.01083
前期回顾:
【轻量化网络系列(1)】MobileNetV1论文超详细解读(翻译 +学习笔记+代码实现)
【轻量化网络系列(2)】MobileNetV2论文超详细解读(翻译 +学习笔记+代码实现)
【轻量化网络系列(3)】MobileNetV3论文超详细解读(翻译 +学习笔记+代码实现)
Abstract—摘要
翻译
我们介绍了一种名为ShuffleNet的计算效率极高的CNN架构,该架构是专为计算能力非常有限(例如10-150 MFLOP)的移动设备设计的。新架构利用了两个新的操作,逐点组卷积和通道混洗,可以在保持准确性的同时大大降低计算成本。 ImageNet分类和MS COCO对象检测的实验证明了ShuffleNet优于其他结构的性能,例如在40个MFLOP的计算预算下,比最近的MobileNet [12]在ImageNet分类任务上的top-1错误要低(绝对7.8%)。在基于ARM的移动设备上,ShuffleNet的实际速度是AlexNet的13倍,同时保持了相当的准确性。
精读
主要内容
(1)CNN架构ShuffleNet是专门为计算能力有限的移动设备而设计
(2)ShuffleNet利用两种新的运算方法:
- 分组逐点卷积(pointwise group convolution)
- 通道重排(channel shuffle)
(3)实现效果: 在40 MFLOPs计算预算下,ImageNet分类和MS COCO目标检测实验表明,ShuffleNet的性能优于其他结构
一、Introduction—简介
翻译
建立更深,更大的卷积神经网络(CNN)是解决主要视觉识别任务的主要趋势[21,9,33,5,28,24]。最精确的CNN通常具有数百个层和数千个通道[9、34、32、40],因此需要数十亿个FLOP进行计算。本报告探讨了相反的极端情况:在非常有限的计算预算中以数十或数百个MFLOP追求最佳准确性,重点放在无人机,机器人和智能手机等常见的移动平台上。请注意,许多现有的作品[16,22,43,42,38,27]专注于修剪,压缩或代表“基本”网络体系结构的低位。在这里,我们旨在探索一种专为我们所需的计算范围而设计的高效基本架构。
我们注意到,由于代价高昂的密集1×1卷积,诸如Xception [3]和ResNeXt [40]之类的最新基础架构在极小的网络中的效率降低。我们建议使用逐点分组卷积来减少1×1卷积的计算复杂度。 为了克服群卷积带来的副作用,我们提出了一种新颖的通道随机操作,以帮助信息流过特征通道。基于这两种技术,我们构建了一种高效的体系结构,称为ShuffleNet。 与流行的结构[30,9,40]相比,在给定的计算复杂度预算的情况下,我们的ShuffleNet允许更多的特征映射通道,这有助于编码更多的信息,并且对于超小型网络的性能尤为关键。
我们根据具有挑战性的ImageNet分类[4,29]和MS COCO对象检测[23]任务评估我们的模型。一系列受控实验显示了我们设计原理的有效性以及优于其他结构的性能。与最先进的架构MobileNet [12]相比,ShuffleNet可以显着提高性能,例如。在40个MFLOP级别上,ImageNet top-1错误绝对降低了7.8%。
我们还检查了实际硬件(即基于ARM的现成计算核心)上的加速情况。 ShuffleNet模型比AlexNet [21]达到了约13倍的实际提速(理论提速为18倍),同时保持了相当的精度。
精读
本文目标
探索一个高效的基础架构,专门为我们所需的计算范围设计
本文采用方法
(1)使用分组逐点卷积来降低1×1卷积的计算复杂度
(2)使用通道重排操作来帮助信息在特征通道间流动
二、Related Work—相关工作
Efficient Model Designs—高效模型设计
翻译
高效的模型:设计最近几年,深层神经网络在计算机视觉任务中取得了成功[21、36、28],其中模型设计发挥了重要作用。在嵌入式设备上运行高质量深度神经网络的需求不断增长,鼓励了对有效模型设计的研究[8]。例如,与简单堆叠卷积层相比,GoogLeNet [33]以较低的复杂性增加了网络的深度。 SqueezeNet [14]在保持精度的同时显着减少了参数和计算量。 ResNet [9,10]利用高效的瓶颈结构来实现令人印象深刻的性能。 SENet [13]引入了一种架构单元,它以较低的计算成本提高了性能。与我们同时进行的是一项非常近期的工作[46],它采用强化学习和模型搜索来探索有效的模型设计。 拟议的移动NASNet模型在性能上与我们的ShuffleNet模型相当(对于ImageNet分类错误,其@ 564 MFLOP为26.0%,而524 MFLOP为26.3%)。 但是[46]没有报告极小的模型的结果(例如,复杂度小于150 MFLOP),也没有评估移动设备上的实际推理时间。
精读
- GoogLeNet: 以更低的复杂度增加了网络的深度。
- SqueezeNet: 在保持精度的同时,显著降低了参数和计算量。
- ResNet: 利用高效的瓶颈结构实现了令人印象深刻的性能。
- SENet: 引入了一种架构单元,以很少的计算成本提高性能。
- NasNet: 利用强化学习和模型搜索,与ShuffleNet性能相当。但是NASNet没有报告极小模型的结果,也不会评估移动设备上的实际推理时间。
Group Convolution—分组卷积
翻译
分组卷积 分组卷积的概念最早出现在AlexNet [21]中,用于在两个GPU上分配模型,已在ResNeXt [40]中得到了充分证明。 Xception [3]中提出的深度可分离卷积概括了Inception系列[34,32]中的可分离卷积的思想。最近,MobileNet [12]利用深度可分离卷积并获得了轻量模型中的最新结果。我们的工作以一种新颖的形式概括了群卷积和深度可分离卷积。
精读
概念最早出现在AlexNet 上,将模型分布在两个GPU上
Xception 中提出的深度可分离卷积概括了Inception系列中可分离卷积的思想
MobileNet 采用了深度可分离卷积
Channel Shuffle Operation—通道重排操作
翻译
通道重排操作:据我们所知,尽管在CNN库cuda-convnet [20]支持“随机稀疏卷积”层(相当于随机信道)的前提下,在有效模型设计的先前工作中很少提及信道随机化操作的想法。洗牌,然后是组卷积层。这种“随机混洗”操作具有不同的目的,以后很少被利用。最近,另一项并行的工作[41]也采用了这种思想进行两阶段卷积。然而,[41]没有专门研究信道改组本身的有效性及其在微型模型设计中的使用。
精读
CNN库cuda-convnet支持“随机稀疏卷积”层,这相当于随机通道重排后接一分组卷积层
- 但是通道重排操作的思想在之前的工作中很少被提及
最近有个two-stage卷积的工作采用了这个方法
- 但是并没有深入研究通道混洗本身以及它在小模型设计上的作用
Model Acceleration—模型加速
翻译
模型加速: 此方向旨在加快推理速度,同时保持预训练模型的准确性。修剪网络连接[6、7]或通道[38]可在保持性能的同时减少预训练模型中的冗余连接。在文献中提出了量化[31、27、39、45、44]和因式分解[22、16、18、37],以减少计算中的冗余以加速推理。在不修改参数的情况下,通过FFT [25,35]和其他方法[2]实现的优化卷积算法可以减少实践中的时间消耗。提炼[11]将知识从大型模型转移到小型模型,这使得对小型模型的训练更加容易。
精读
目的: 保持预训练模型精度的同时,加速推理
(1)剪枝: 减少模型的冗余连接
(2)量化和因式分解: 减少计算的冗余
(3)FFT(快速傅里叶变换): 降低了时间消耗
(4)蒸馏: 将知识从大模型转移到小模型,训练小模型更容易
三、Approach—方法
3.1 Channel Shuffle for Group Convolutions—用于分组卷积的通道重排
翻译
现代卷积神经网络[30、33、34、32、9、10]通常由具有相同结构的重复构建块组成。其中,最先进的网络,例如Xception [3]和ResNeXt [40],将有效的深度可分离卷积或组卷积引入到构建块中,从而在表示能力和计算成本之间取得了很好的折衷。但是,我们注意到,两种设计都没有完全考虑1×1卷积(在[12]中也称为点状卷积),这需要相当大的复杂性。例如,在ResNeXt [40]中,只有3×3层配备了组卷积。结果,对于ResNeXt中的每个残差单元,逐点卷积占据93.4%的乘积(基数= 32,如[40]中所建议)。在小型网络中,昂贵的逐点卷积导致通道数量有限,无法满足复杂性约束,这可能会严重影响精度。
图2. ShuffleNet单元。 a)具有深度卷积(DWConv)[3,12]的瓶颈单元[9]; b)具有按点分组卷积(GConv)和通道混洗的ShuffleNet单元; c)步幅= 2的ShuffleNet单元
为了解决这个问题,一个简单的解决方案是在1×1层上应用通道稀疏连接,例如组卷积。 通过确保每个卷积仅在相应的输入通道组上运行,组卷积显着降低了计算成本。 但是,如果多个组卷积堆叠在一起,则会产生一个副作用:某个通道的输出仅从一小部分输入通道派生。图1(a)展示了两个堆叠的组卷积层的情况。 显然,某个组的输出仅与该组内的输入有关。 此属性阻止通道组之间的信息流并削弱表示。
如果我们允许组卷积从不同组中获取输入数据(如图1(b)所示),则输入和输出通道将完全相关。具体来说,对于从上一个组层生成的特征图,我们可以先将每个组中的通道划分为几个子组,然后再将下一个层中的每个组提供给不同的子组。这可以通过通道混洗操作有效而优雅地实现(图1(c)):假设一个具有g个组的卷积层,其输出具有g×n个通道;我们首先将输出通道的尺寸调整为(g,n),进行转置,然后再变平,作为下一层的输入。请注意,即使两个卷积具有不同数量的组,该操作仍然会生效。此外,信道混洗也是可区分的,这意味着可以将其嵌入到网络结构中以进行端到端训练。
通道混洗操作可以构建具有多个组卷积层的更强大的结构。在下一个小节中,我们将介绍一个具有信道混洗和组卷积的高效网络单元。
精读
之前的问题
- 没有完全考虑到1×1卷积(逐点卷积)
- 在微型网络中,反复堆叠的逐点卷积会导致有限的通道来满足复杂度约束,这可能会严重损害精度
之前的解决方法
1×1层上应用通道稀疏连接,例如分组卷积
(a) 两个具有相同组数的叠加卷积层
优点: 通过确保每个卷积仅在相应的输入通道组上运行,组卷积降低了计算成本
不足之处: 分组卷积的副作用:阻塞通道之间的信息并削弱表征能力
本文方法
本文采用channel shuffle操作,通过将每个分组后的通道再分成几组子通道,并将不同分组中的不同子通道进行混合(洗牌操作),那么就可以保证不同分组间的特征通道的特征信息可以共享。
(b) 第二个分组卷积是从第一个分组卷积里不同的组里拿数据(b是一种思想)
(c) 利用通道重排产生与(b) 等效的实现(c是一种实现)
(c)的实现: 卷积层有g组,输出有g×n个通道,先将输出通道维数reshape为(g,n),转置,展平,作为下一层的输入(即使两个卷积的组数不同,操作依然有效)
channel shuffle重排过程
3.2 ShuffleNet Unit—ShuffleNet单元
翻译
利用信道随机播放操作的优势,我们提出了一种专门为小型网络设计的新型ShuffleNet单元。我们从图2(a)中的瓶颈单元[9]的设计原理开始。这是一个剩余的块。在其剩余分支中,对于3×3层,我们在瓶颈特征图上应用了经济的3×3深度卷积计算方法[3]。然后,我们将第一个1×1层替换为逐点组卷积,然后进行通道随机混合操作,以形成ShuffleNet单元,如图2(b)所示。第二次逐点分组卷积的目的是恢复通道尺寸以匹配快捷方式路径。为简单起见,我们在第二个逐点层之后不应用额外的通道随机播放操作,因为它会产生可比的得分。批处理归一化(BN)[15]和非线性的用法与[9,40]相似,不同之处在于我们不按[3]的建议在深度卷积后使用ReLU。对于ShuffleNet跨步应用的情况,我们只需进行两个修改(见图2(c)):(i)在快捷路径上添加3×3平均池; (ii)用通道级联替换逐元素加法,这使得扩展通道尺寸变得容易,而额外的计算成本却很少。
由于具有通道混洗的逐点分组卷积,可以高效地计算ShuffleNet单元中的所有分量。 与ResNet [9](瓶颈设计)和ResNeXt [40]相比,我们的结构在相同设置下具有较低的复杂性。 例如,给定输入大小c×h×w和瓶颈通道m,ResNet单位需要hw(2cm + 9m2)FLOP,ResNeXt则需要hw(2cm + 9m2 / g)FLOP,而我们的ShuffleNet单位仅需要hw(2cm / g + 9m)FLOP,其中g表示卷积的组数。 换句话说,给定计算预算,ShuffleNet可以使用更宽的特征图。 我们发现这对于小型网络至关重要,因为小型网络通常没有足够的通道来处理信息。
此外,在ShuffleNet中,深度卷积仅对瓶颈特征图执行。尽管深度卷积通常具有非常低的理论复杂度,但我们发现很难在低功率移动设备上有效实现,这可能是由于与其他密集操作相比更差的计算/内存访问比所致。在[3]中也提到了这种缺点,它具有基于TensorFlow [1]的运行时库。在ShuffleNet单元中,我们故意仅在瓶颈上使用深度卷积,以尽可能避免开销。
精读
一个ShuffleNet Unit是通过对一个Resdual block进行改进后得到
图(a)为一个Resdual block
- ①1×1卷积(降维)+3×3深度卷积+1×1卷积(升维)
- ②之间有BN和ReLU
- ③最后通过add相加
图(b)为输入输出特征图大小不变的ShuffleNet Unit
- ①将第一个用于降低通道数的1×1卷积改为1×1分组卷积 + Channel Shuffle
- ②去掉原3×3深度卷积后的ReLU
- ③ 将第二个用于扩增通道数的1×1卷积改为1×1分组卷积
图(c)为输出特征图大小为输入特征图大小一半的ShuffleNet Unit
- ①将第一个用于降低通道数的1×1卷积改为1×1分组卷积 +Channel Shuffle
- ②令原3×3深度卷积的步长stride=2, 并且去掉深度卷积后的ReLU
- ③将第二个用于扩增通道数的1×1卷积改为1×1分组卷积
- ④shortcut上添加一个3×3平均池化层(stride=2)用于匹配特征图大小
- ⑤对于块的输出,将原来的add方式改为concat方式
Q1:为什么逐点分组卷积后并没有使用channel shuffle?
因为此时得到的结果已经相当不错了,所以不需要进行通道重排了
Q2:为什么去掉第二个ReLU?
这个我们在MobileNetV2(点这里)里介绍过,作者发现训练过程中depthwise部分得到卷积核会废掉,认为造成这样的原因是由于ReLU函数造成的
Q3:为什么用concat代替add?
扩大通道尺寸,无需额外计算成本
3.3 Network Architecture—网络体系结构
翻译
在ShuffleNet单元的基础上,我们在表1中介绍了整个ShuffleNet架构。所提议的网络主要由分成三个阶段的ShuffleNet单元堆栈组成。在步幅= 2的情况下应用第一个构建块级。阶段中的其他超参数保持不变,并且对于下一个阶段,输出通道加倍。类似于[9],我们将每个ShuffleNet单元的瓶颈通道数设置为输出通道的1/4。我们的目的是提供尽可能简单的参考设计,尽管我们发现进一步的超参数调整可能会产生更好的结果。
在ShuffleNet单元中,组号g控制逐点卷积的连接稀疏性。表1探索了不同的组号,我们调整了输出通道以确保总体计算成本大致不变(〜140 MFLOP)。显然,对于给定的复杂性约束,较大的组数会导致更多的输出通道(因此,需要更多的卷积滤波器),这有助于编码更多信息,尽管由于受限的相应输入通道,这也可能导致单个卷积滤波器的性能下降。在第4.1.1节中,我们将研究此数字在不同计算约束下的影响。
为了将网络定制为所需的复杂度,我们可以简单地在通道数上应用比例因子s。例如,我们在表1中将网络表示为“ ShuffleNet 1×”,然后“ ShuffleNet s×”表示将ShuffleNet 1×中的过滤器数量缩放s倍,因此总体复杂度将约为ShuffleNet 1×s^2倍。
精读
表1. ShuffleNet架构
(1)首先使用的普通的3x3的卷积和max pool层
(2)接着分为三个阶段:
- 每个阶段都是重复堆积了几个ShuffleNet的基本单元
- 对于每个阶段,第一个基本单元采用的是stride=2,这样特征图width和height各降低一半,而通道数增加一倍
- 后面的基本单元都是stride=1,特征图和通道数都保持不变
(3)对于基本单元来说,其中瓶颈层,就是3x3卷积层的通道数为输出通道数的1/4,这和残差单元的设计理念是一样的
四、Experiments—实验
翻译
我们主要在ImageNet 2012分类数据集上评估模型[29,4]。 我们遵循[40]中使用的大多数训练设置和超参数,但有两个例外:(i)将权重衰减设置为4e-5而不是1e-4,并使用线性衰减学习率策略(从0.5降低 至0); (ii)我们在数据预处理中使用略微较少的积极规模扩展。 在[12]中也引用了类似的修改,因为这样的小型网络通常遭受欠拟合而不是过度拟合的困扰。 在4个GPU上训练3×105迭代的模型需要1或2天的时间,其批处理大小设置为1024。为进行基准测试,我们在ImageNet验证集上比较了单作物top-1性能。 从256×输入图像中裁剪224×224中心视图并评估分类准确性。 我们对所有模型使用完全相同的设置,以确保公平地进行比较。
精读
数据集: ImageNet 2012分类数据集
在训练设置和超参数上有两个例外
(i)将权重衰减设置为4e-5而不是1e-5,采用线性衰减学习率策略(由0.5降至0)
(ii)使用稍微不那么激进的规模扩大(scale augmentation)来进行数据预处理
标准: 比较在ImageNet验证集上的single-crop top-1性能
4.1 Ablation Study—消融实验
4.1.1 Pointwise Group Convolutions—分组逐点卷积
翻译
为了评估逐点群卷积的重要性,我们比较了复杂度相同的ShuffleNet模型,其群数范围为1到8。如果群数等于1,则不涉及逐点群卷积,然后ShuffleNet单元变为“ Xception-喜欢” [3]结构。为了更好地理解,我们还将网络的宽度缩放到3个不同的复杂度,并分别比较它们的分类性能。结果如表2所示。
从结果可以看出,具有组卷积(g> 1)的模型的性能始终优于没有逐点组卷积(g = 1)的模型。较小的模型往往会从团体中受益更多。例如,对于ShuffleNet 1x,最佳条目(g = 8)比同类条目好1.2%,而对于ShuffleNet 0.5x和0.25x,差距分别变为3.5%和4.4%。请注意,对于给定的复杂性约束,组卷积允许更多的特征图通道,因此我们假设性能增益来自有助于编码更多信息的更宽的特征图。另外,较小的网络涉及较薄的特征图,这意味着它从扩大的特征图中受益更多。
表2还显示,对于某些模型(例如ShuffleNet 0.5x),当组数变得相对较大(例如g = 8)时,分类得分会饱和甚至下降。随着组数的增加(因此,特征图会更宽),每个卷积滤波器的输入通道会越来越少,这可能会损害表示能力。有趣的是,我们还注意到,对于ShuffleNet等较小的模型,如0.25×较大的组号,往往会始终如一地获得更好的结果,这表明较宽的特征图为较小的模型带来了更多好处。
精读
实验方法
本文比较了具有相同复杂度的ShuffleNet模型,其组数从1到8不等。如果组数等于1,则不涉及分组逐点卷积,另外还将网络的宽度扩展到3种不同的复杂性,并分别比较它们的分类性能。
表2. 分类误差VS组数g(较小的数字代表更好的性能)
结论:
(1)有分组卷积(g>1)的模型始终比没有分组逐点卷积(g=1)的模型表现得更好,较小的模型往往从分组中获益更多
(2)对于某些模型((如ShuffleNet 0.5×),当组数变得相对较大时(例如g=8),分类分数饱和甚至下降。
4.1.2 Channel Shuffle vs. No Shuffe—通道重排 vs 不重排
翻译
随机操作的目的是为多个组卷积层启用跨组信息流。表3比较了带/不带通道混洗的ShuffleNet结构(例如,组号设置为3或8)的性能。评估是在三种不同的复杂度范围内进行的。显然,频道改组可以持续提高不同设置的分类得分。特别地,当组数相对较大(例如,g = 8)时,具有信道混洗的模型在性能上优于对应的模型,这表明了跨组信息交换的重要性。
精读
表3. 具有/不具有通道重排的ShuffleNet(数值越小表示性能越好)
结论:
(1)在不同的设置下,通道重排可以不断地提高分类分数。
(2)当组数较大时(如g=8),通道重排模型的性能明显优于同类模型,说明了跨组信息交换的重要性。
4.2 Comparison with Other Structure Units—与其他结构单元比较
翻译
VGG [30],ResNet [9],GoogleNet [33],ResNeXt [40]和Xception [3]中最近的领先卷积单元已经在大型模型(例如≥1GFLOP)上追求了最新的结果,但是没有充分探索低复杂性条件。在本节中,我们调查了各种构建基块,并在相同的复杂性约束下与ShuffleNet进行了比较。
为了公平比较,我们使用表1所示的总体网络架构。我们将Stage 2-4中的ShuffleNet单元替换为其他结构,然后调整通道数以确保复杂度保持不变。我们探索的结构包括:
VGG-like。遵循VGG网络的设计原理[30],我们使用两层3×3卷积作为基本构建块。与[30]不同,我们在每个卷积之后添加了一个批处理归一化层[15],以简化端到端的训练。
Xception-like。 [3]中提出的原始结构涉及不同阶段的精美设计或超参数,我们发现很难在小模型上进行公平比较。取而代之的是,我们从ShuffleNet(也等效于g = 1的ShuffleNet)中删除了逐点分组卷积和通道shuffle操作。派生结构与[3]中的“深度可分离卷积”想法相同,在此称为Xception-like结构。
ResNeXt。如[40]中所建议的,我们使用基数= 16和瓶颈比率= 1:2的设置。我们还将探索其他设置,例如瓶颈比率= 1:4,并获得相似的结果。
我们使用完全相同的设置来训练这些模型。结果如表4所示。在不同的复杂度下,我们的ShuffleNet模型要比其他大多数模型大得多。 有趣的是,我们发现了特征图通道与分类精度之间的经验关系。 例如,在38个MFLOP的复杂性下,类VGG,ResNet,ResNeXt,类Xception,ShuffleNet的第4阶段(请参见表1)的输出通道分别为50192、192、288、576,与 提高准确性。 由于ShuffleNet的高效设计,对于给定的计算预算,我们可以使用更多的通道,因此通常可以获得更好的性能。
请注意,以上比较不包括GoogleNet或Inception系列[33、34、32]。我们发现将这样的Inception结构生成到小型网络并非易事,因为Inception模块的原始设计涉及太多的超参数。作为参考,第一个GoogleNet版本[33]具有31.3%的top-1错误,但代价是1.5 GFLOP(请参见表6)。然而,更复杂的盗版版本[34,32]更加准确,但是复杂度也大大增加。最近,金等提出了一种轻量级的网络结构,称为PVANET [19],它采用了Inception单元。我们重新实现的PV ANET(输入大小为224×224)具有29.7%的分类错误,计算复杂度为557 MFLOP,而我们的ShuffleNet 2x模型(g = 3)在524 MFLOP的情况下获得了26.3%(参见表6)。
精读
表4. 类误差vs各种结构(%,数值越小表示性能越好)
结论:
(1)ShuffleNet模型比大多数其他模型有显著的优势
(2)通道越多,分类精度越高
表6. 复杂度比较。*由BVLC实现
结论:ShuffleNet模型效果要好点
4.3 Comparison with MobileNets and Other Frameworks—与MobileNets和其他框架进行比较
翻译
最近霍华德等。已经提出了MobileNets [12],其主要侧重于移动设备的有效网络架构。 MobileNet从[3]中采用了深度可分离卷积的思想,并在小型模型上获得了最新的结果。
表5比较了各种复杂性级别下的分类得分。 显然,我们的ShuffleNet模型在所有复杂性方面都优于MobileNet。 尽管我们的ShuffleNet网络是专为小型机型(<150 MFLOP)设计的,但我们发现它在计算成本方面比MobileNet更好,例如, 在500个MFLOP的代价下,其精度比MobileNet 1×高3.1%。 对于较小的网络(约40个MFLOP),ShuffleNet比MobileNet超出7.8%。 请注意,我们的ShuffleNet架构包含50层,而MobileNet仅包含28层。 为了更好地理解,我们还通过在阶段2-4中删除一半的块来尝试在26层体系结构上使用ShuffleNet(请参阅表5中的“ ShuffleNet 0.5×浅(g = 3)”)。 结果表明,较浅的模型仍然比相应的MobileNet更好,这表明ShuffleNet的有效性主要来自其有效的结构,而不是深度。
表6比较了我们的ShuffleNet和一些流行的模型。结果表明,以相似的精度ShuffleNet比其他的效率更高。例如,ShuffleNet 0.5×理论上比具有类似分类分数的AlexNet [21]快18倍。我们将在4.5节中评估实际的运行时间。
还值得注意的是,简单的体系结构设计使向ShuffeNets轻松配备诸如[13,26]之类的最新进展。例如,在[13]中,作者提出了挤压和激发(SE)块,该块可以在大型ImageNet模型上获得最新的结果。我们发现SE模块也可以与主链ShuffleNets结合使用,例如,将ShuffleNet 2×的top-1误差提高到24.7%(如表5所示)。有趣的是,尽管理论复杂性的增加可以忽略不计,但我们发现带有SE模块的ShuffleNets通常比移动设备上的“原始” ShuffleNets慢25%到40%,这意味着实际的加速评估对于低成本架构设计至关重要。在第4.5节中,我们将进一步讨论。
精读
表5. ShuffleNet vs. MobileNet (在ImageNet分类任务上)
结论:ShuffleNet的有效性主要来源于高效的结构设计,而不是深度。
4.4 Generalization Ability—泛化能力
翻译
为了评估迁移学习的泛化能力,我们在MS COCO对象检测任务上测试了ShuffleNet模型[23]。我们采用Faster-RCNN [28]作为检测框架,并使用公开发布的Caffe代码[28,17]进行默认设置的训练。与[12]类似,模型在不包括5000个最小图像的COCO序列+ val数据集上进行训练,我们对最小集合进行测试。表7显示了在两种输入分辨率下训练和评估的结果的比较。将ShuffleNet 2x和MobileNet的复杂度相媲美(524 vs. 569 MFLOP),我们的ShuffleNet 2x在两个分辨率上都远远超过MobileNet。我们的ShuffleNet 1×在600×分辨率下也可以与MobileNet取得可比的结果,但复杂度降低了约4倍。我们猜想,这一重大收益部分是由于ShuffleNet的架构设计简单而没有花哨。
精读
表7. MS COCO上的目标检测结果(数值越大表示性能越好)
结论:ShuffleNet 2x在两个分辨率上都远远超过MobileNet,ShuffleNet 1×在600×分辨率下也可以与MobileNet取得可比的结果,但复杂度降低了约4倍。这个结果的原因可能是由于ShuffleNet的架构设计简单。
4.5 Actual Speedup Evaluation—实际加速评估
翻译
最后,我们评估具有ARM平台的移动设备上ShuffleNet模型的实际推理速度。尽管具有较大组号(例如g = 4或g = 8)的ShuffleNets通常具有更好的性能,但我们发现它在当前的实现中效率较低。根据经验,g = 3通常会在准确性和实际推理时间之间取得适当的折衷。如表8所示,测试使用了三种输入分辨率。由于内存访问和其他开销,我们发现理论上每降低4倍的理论复杂度通常会导致实际速度提高2.6倍。尽管如此,与AlexNet [21]相比,我们的ShuffleNet 0.5×模型在可比的分类精度下(理论上的速度为18倍)仍可实现约13倍的实际速度,这比以前的AlexNet级别的模型或诸如[14]的速度方法要快得多。[14、16、22、42、43、38]。
精读
表8. 移动设备上的实际推理时间(数值越小表示性能越好)
结论:每减少4倍理论上的复杂性,就会导致约2.6倍实际加速,但效果仍比AlexNet要好
🌟代码实现
import torch import torch.nn as nn import torchvision # 分类数 num_class = 5 # DW卷积 def Conv3x3BNReLU(in_channels,out_channels,stride,groups): return nn.Sequential( nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=3, stride=stride, padding=1,groups=groups), nn.BatchNorm2d(out_channels), nn.ReLU6(inplace=True) ) # 普通的1x1卷积 def Conv1x1BNReLU(in_channels,out_channels,groups): return nn.Sequential( nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=1, stride=1,groups=groups), nn.BatchNorm2d(out_channels), nn.ReLU6(inplace=True) ) # PW卷积(不使用激活函数) def Conv1x1BN(in_channels,out_channels,groups): return nn.Sequential( nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=1, stride=1,groups=groups), nn.BatchNorm2d(out_channels) ) # channel重组操作 class ChannelShuffle(nn.Module): def __init__(self, groups): super(ChannelShuffle, self).__init__() self.groups = groups # 进行维度的变换操作 def forward(self, x): # Channel shuffle: [N,C,H,W] -> [N,g,C/g,H,W] -> [N,C/g,g,H,w] -> [N,C,H,W] N, C, H, W = x.size() g = self.groups return x.view(N, g, int(C / g), H, W).permute(0, 2, 1, 3, 4).contiguous().view(N, C, H, W) # ShuffleNetV1的单元结构 class ShuffleNetUnits(nn.Module): def __init__(self, in_channels, out_channels, stride, groups): super(ShuffleNetUnits, self).__init__() self.stride = stride # print("in_channels:", in_channels, "out_channels:", out_channels) # 当stride=2时,为了不因为 in_channels+out_channels 不与 out_channels相等,需要先减,这样拼接的时候数值才会不变 out_channels = out_channels - in_channels if self.stride >1 else out_channels # 结构中的前一个1x1组卷积与3x3组件是维度的最后一次1x1组卷积的1/4,与ResNet类似 mid_channels = out_channels // 4 # print("out_channels:",out_channels,"mid_channels:",mid_channels) # ShuffleNet基本单元: 1x1组卷积 -> ChannelShuffle -> 3x3组卷积 -> 1x1组卷积 self.bottleneck = nn.Sequential( # 1x1组卷积升维 Conv1x1BNReLU(in_channels, mid_channels,groups), # channelshuffle实现channel重组 ChannelShuffle(groups), # 3x3组卷积改变尺寸 Conv3x3BNReLU(mid_channels, mid_channels, stride,groups), # 1x1组卷积降维 Conv1x1BN(mid_channels, out_channels,groups) ) # 当stride=2时,需要进行池化操作然后拼接起来 if self.stride > 1: # hw减半 self.shortcut = nn.AvgPool2d(kernel_size=3, stride=2, padding=1) self.relu = nn.ReLU6(inplace=True) def forward(self, x): out = self.bottleneck(x) # 如果是stride=2,则将池化后的结果与通过基本单元的结果拼接在一起, 否则直接将输入与通过基本单元的结果相加 out = torch.cat([self.shortcut(x),out],dim=1) if self.stride >1 else (out + x) # 假设当前想要输出的channel为240,但此时stride=2,需要将输出与池化后的输入作拼接,此时的channel为24,24+240=264 # torch.Size([1, 264, 28, 28]), 但是想输出的是240, 所以在这里 out_channels 要先减去 in_channels # torch.Size([1, 240, 28, 28]), 这是先减去的结果 # if self.stride > 1: # out = torch.cat([self.shortcut(x),out],dim=1) # 当stride为1时,直接相加即可 # if self.stride == 1: # out = out+x return self.relu(out) class ShuffleNet(nn.Module): def __init__(self, planes, layers, groups, num_classes=num_class): super(ShuffleNet, self).__init__() # Conv1的输入channel只有24, 不算大,所以可以不用使用组卷积 self.stage1 = nn.Sequential( Conv3x3BNReLU(in_channels=3,out_channels=24,stride=2, groups=1), # torch.Size([1, 24, 112, 112]) nn.MaxPool2d(kernel_size=3,stride=2,padding=1) # torch.Size([1, 24, 56, 56]) ) # 以Group = 3为例 4/8/4层堆叠结构 # 24 -> 240, groups=3 4层 is_stage2=True,stage2第一层不需要使用组卷积,其余全部使用组卷积 self.stage2 = self._make_layer(24,planes[0], groups, layers[0], True) # 240 -> 480, groups=3 8层 is_stage2=False,全部使用组卷积,减少计算量 self.stage3 = self._make_layer(planes[0],planes[1], groups, layers[1], False) # 480 -> 960, groups=3 4层 is_stage2=False,全部使用组卷积,减少计算量 self.stage4 = self._make_layer(planes[1],planes[2], groups, layers[2], False) # in: torch.Size([1, 960, 7, 7]) self.global_pool = nn.AvgPool2d(kernel_size=7, stride=1) self.dropout = nn.Dropout(p=0.2) # group=3时最后channel为960,所以in_features=960 self.linear = nn.Linear(in_features=planes[2], out_features=num_classes) # 权重初始化操作 self.init_params() def _make_layer(self, in_channels,out_channels, groups, block_num, is_stage2): layers = [] # torch.Size([1, 240, 28, 28]) # torch.Size([1, 480, 14, 14]) # torch.Size([1, 960, 7, 7]) # 每个Stage的第一个基本单元stride均为2,其他单元的stride为1。且stage2的第一个基本单元不使用组卷积,因为参数量不大。 layers.append(ShuffleNetUnits(in_channels=in_channels, out_channels=out_channels, stride=2, groups=1 if is_stage2 else groups)) # 每个Stage的非第一个基本单元stride均为1,且全部使用组卷积,来减少参数计算量, 再叠加block_num-1层 for idx in range(1, block_num): layers.append(ShuffleNetUnits(in_channels=out_channels, out_channels=out_channels, stride=1, groups=groups)) return nn.Sequential(*layers) # 初始化权重 def init_params(self): for m in self.modules(): if isinstance(m,nn.Conv2d): nn.init.kaiming_normal_(m.weight) nn.init.constant_(m.bias,0) elif isinstance(m, nn.BatchNorm2d) or isinstance(m, nn.Linear): nn.init.constant_(m.weight,1) nn.init.constant_(m.bias, 0) def forward(self, x): x = self.stage1(x) # torch.Size([1, 24, 56, 56]) x = self.stage2(x) # torch.Size([1, 240, 28, 28]) x = self.stage3(x) # torch.Size([1, 480, 14, 14]) x = self.stage4(x) # torch.Size([1, 960, 7, 7]) x = self.global_pool(x) # torch.Size([1, 960, 1, 1]) x = x.view(x.size(0), -1) # torch.Size([1, 960]) x = self.dropout(x) x = self.linear(x) # torch.Size([1, 5]) return x # planes 是Stage2,Stage3,Stage4输出的参数 # layers 是Stage2,Stage3,Stage4的层数 # g1/2/3/4/8 指的是组卷积操作时的分组数 def shufflenet_g8(**kwargs): planes = [384, 768, 1536] layers = [4, 8, 4] model = ShuffleNet(planes, layers, groups=8) return model def shufflenet_g4(**kwargs): planes = [272, 544, 1088] layers = [4, 8, 4] model = ShuffleNet(planes, layers, groups=4) return model def shufflenet_g3(**kwargs): planes = [240, 480, 960] layers = [4, 8, 4] model = ShuffleNet(planes, layers, groups=3) return model def shufflenet_g2(**kwargs): planes = [200, 400, 800] layers = [4, 8, 4] model = ShuffleNet(planes, layers, groups=2) return model def shufflenet_g1(**kwargs): planes = [144, 288, 576] layers = [4, 8, 4] model = ShuffleNet(planes, layers, groups=1) return model if __name__ == '__main__': # model = shufflenet_g3() # 常用 model = shufflenet_g8() # print(model) input = torch.randn(1, 3, 224, 224) out = model(input) print(out.shape)
本文参考: