目录
一、前言
二、轻量化网络发展
三、论文解读
(一)为什么FLOPs≠速度
(二)如何减小MAC
(三)其他影响因素
(四)结论
四、ShuffleNet_v2网络结构
(一)网络结构
(二)block设计
五、实验验证
六、代码实现
七、完整代码
一、前言
ShuffleNet V2中提出了很多新颖的观点,推动了轻量级神经网络的发展。
这里简单说一下我对这篇论文的理解。首先,作者开门见山提出过去绝大多数轻量模型都只通过FLOPs来反应模型的大小,但这种衡量方式其实是不准确的(这点是作者通过比较相同FLOPs下不同模型的速度不同得出的结论),我们需要通过更直接的方式(速度)来衡量模型的轻量与否,而在设备上运行的速度往往取决于设备的类型以及内存的访问(个人认为这也促使Mnasnet做神经架构搜索时候直接将设备上运行的时间作为强化学习奖励的标准吧)。因此,作者基于上述分析进一步做了研究,进而给出了四条轻量化网络设计准则。
二、轻量化网络发展
SqueezeNet是早期经典的轻量型神经网络,提出了一种新的网络架构Fire Module,通过减少模型参数来实现压缩,此外研究了压缩比的影响;Xception受Inception结构影响,提出了一种深度可分离卷积,实现模型压缩;MobileNet_v1基于深度可分离卷积搭建了轻量化网络;ShuffleNet提出利用逐点组卷积以及通道重排技术来减少运算量;MobileNet_v2在v1基础上改进,提出一种倒残差结构,同时得出relu激活函数会对低维信息造成损失;ShuffleNet_v2打破常规衡量模型轻量与否的准则(FLOPs),提出四种设计轻量网络的准则;MnasNet利用神经架构搜索并直接在设备上测量速度来寻找网络结构;MobileNet_v3对MnasNet搜索出的结构做了简要修改,同时提出一种新的激活函数hard-swish;后面就是EfficientNet。
三、论文解读
(一)为什么FLOPs≠速度
首先作者要解释的第一个问题是为什么FLOPs不能直接反应在设备上的速度。
如上图所示,蓝色框内表示同样设备,左侧为GPU上结果,右侧为ARM上的结果。从下面两幅图可以看出,当不同模型MFLOPs相同时,速度却不同,这也证明了作者开篇提出的观点是正确的。
既然速度不完全由FLOPs决定,那么还有什么因素会影响设备上运行的速度呢?作者提出内存访问(MAC)和设备类型也会影响设备运行速度,此外作者还将ShuffleNet_v1和MobileNet_v2的运行时间做了分解来观测耗时因素。
由上图可知,Shuffle(通道重排)、 Elemenwise(张量操作,包括Add、ReLU等等)、Data(数据清洗)以及其他因素都会影响速度。
(二)如何减小MAC
作者深入研究了影响MAC的因素(这也正是他提出第一、二条准则的理论依据)。
理论证明(1):
首先,研究了1*1普通卷积的MAC,假设输入特征图的尺寸为H*W*,卷积核个数为,那么输出特征图尺寸为H*W*,此时FLOPs=H*W**,这里我们将其记为B。该操作的MAC=H*W*+H*W*+*(三项分别为输入特征图、输出特征图、卷积核占用的内存),进一步可以写为 MAC=H*W*(+)+* ,根据均值不等式,并将B带入表达式(很简单的推导),得到,也就是说MAC是有下届的,即当C1=C2时候,MAC得到最小值(虽然FLOPs中也包含C1、C2,但这部分作者主要是考虑减小MAC,也就是在保证FLOPs一定的前提下)。
实验验证(1):
作者做了相应的实验来验证自己的观点,如下图所示,从图中可以看出,无论是在GPU设备还是ARM上,当c1:c2=1时候速度最快,并且在GPU上受C1:C2影响较大。
结论(1):
基于上述分析,作者提出了第一条准则G1: Equal channel width minimizes memory access cost (MAC),即输入channel=输出channel时候可以最小化MAC。
理论证明(2):
接着,作者研究了1*1组卷积, 同样假设输入特征图的尺寸为H*W*,假设我们groups为g,输出特征图尺寸为H*W*,此时FLOPs=H*W**/g,这里同样将其记为B。该操作的MAC=H*W*(+)+*/g,将B带入得到如下表达式:,此处控制H、W、C1与B恒定可以看出,g越大MAC越大(这里说下我个人见解,因为B中也包含g,当g改变时候原本B也会变化,实际应该是通过改变C2来维持B不变)。
实验验证(2):
由上图可知, 随着g的增大,推理速度会显著降低(同样GPU上受g的影响较大),在GPU上使用8个组比使用1个组要慢两倍以上,在ARM上慢30%左右。
结论(2):
基于上述分析,作者提出了第二条准则G2:Excessive group convolution increases MAC,即过大的group会增加MAC。
(三)其他影响因素
作者发现过去大多数网络多采用一些碎片化的操作(“multi-path” structure,即分多个branch),这些碎片化操作虽然能提高准确率,但却引入了额外的开销。(个人认为这里没有什么理论依据)。
作者做了实验验证自己的观点,如下图所示。1-fragment表示没有branch分支,2-fragement-series表示两个串行连接的分支(同样没有branch),2-fragment-parallel表示2个并行的branch分支,从表中可以看出这种分支对GPU设备的速度影响较大(CPU上最大仅差了7个点不到),而且有意思的是4个并行分支要比2个并行分支速度快。
基于上述分析,作者提出了第三条准则G3:Network fragmentation reduces degree of parallelism.,即网络的碎片化操作会减小并行度。
(我个人这里不太明白,上述实验貌似和结论不太匹配,实验结果表明parallel并行连接不好,而得出的结论是碎片化操作会减小并行度,而且原文在开始时候提出,并行化程度越高速度越快,所以这里的碎片化操作到底指什么)
原文:
除碎片化操作的影响外,作者观察Runtime图发现,Elemenwise操作也占用了较多时间,并且通过下图实验结果可以看出,不使用ReLU和short-cut能够增加速度。
因此,作者提出了第四条准则G4:Element-wise operations are non-negligible.,即张量操作也是不可忽视的。
(四)结论
基于上述全部分析,作者共提出了四条设计准则:
1)Equal channel width minimizes memory access cost (MAC).
2)Excessive group convolution increases MAC.
3)Network fragmentation reduces degree of parallelism.
4)Element-wise operations are non-negligible.
四、ShuffleNet_v2网络结构
(一)网络结构
可以看出,ShuffleNet_v2与ShuffleNet_v1的整体结构是极其相同的,唯一不同的点在于v2在Stage4后面加了一个卷积层,需要注意的是每个Stage中第一个block的stride为2
(二)block设计
基于上述四条准则,作者设计了新的block。
(a)(b)是ShuffleNet_v1的block,(c)(d)是ShuffleNet_v2当stride分别为1、2时候的block。
首先,当stride为1时候,先将输入通过Channel Split在channel维度均分为两份,接着将第一个分支branch1直接(减少了碎片化操作,符合G3准则,同时根据研究相邻层的特征复用能提高准确率)与右边分支branch2进行拼接(符合设计准则G1,G4),右边分支将v1中全部的1*1组卷积变为了普通卷积(符合设计准则G2),还需要注意的一点是右边分支最后那个1*1卷积后面是跟了BN以及ReLU的,这与v1先Add再ReLU不同。
当stride为2时候,由于输入直接送到两个分支中,因此最终输出的channel是翻倍的(这与表中stride为2那部分相同),同样将1*1组卷积全部换成了普通卷积,中间仍采用了3*3DW卷积。
最终,两个branch进行concat拼接后,通过通道重排保证信息流动。
五、实验验证
最后,作者对比了ShuffleNet_v2与其他网络在图像分类以及目标检测上的效果。
图像分类:
由下图可以看出,在MFLOPs近似相同的情况下,GPU设备上0.25 MobileNet _v1 速度是最快的;ShuffleNet_v2在ARM以及GPU上准确率以及速度都是相当不错的。
目标检测:
ShuffleNet v2*是添加额外3*3dw卷积后的模型(受Xception的启发)。
通过分析上述结果,在图像分类和目标检测上,针对不同的任务,模型的效率是不同的。比如,Xception在分类任务上效果不太好但在目标检测任务上却仅次于ShuffleNet_v2,而MobileNet_v2在分类任务上效果较好,但在目标检测上效果却最差。
六、代码实现
下面附上我用PyTorch实现ShuffleNet_v2在CIFAR 10上图像分类的代码。
模型部分:
import torch.nn as nn
import torch
from collections import OrderedDict
import torch.nn.functional as F
#定义Channel Shuffle
from torchsummary import summarydef Channel_shuffle(x,groups):batch,channel,height,width=x.size()split_channel=channel//groupsx=x.view(batch,groups,split_channel,height,width)x=torch.transpose(x,1,2).contiguous()x=x.view(batch,-1,height,width)return x#定义基本的卷积\bn\relu
class baseConv_1(nn.Module):def __init__(self,inchannels,outchannels,kernel_size,stride):super(baseConv_1, self).__init__()if kernel_size==1:#1*1卷积self.conv=nn.Sequential(nn.Conv2d(inchannels,outchannels,kernel_size=kernel_size,bias=False),nn.BatchNorm2d(outchannels),nn.ReLU(inplace=True))else:#dw卷积self.conv=nn.Sequential(nn.Conv2d(inchannels,inchannels,kernel_size,stride,1,groups=inchannels,bias=False),nn.BatchNorm2d(inchannels))def forward(self,x):out=self.conv(x)return out#定义残差结构
class Residual_block(nn.Module):def __init__(self,inchannels,outchannels,stride):super(Residual_block, self).__init__()self.stride=stridebranch_channel=outchannels//2 #每个branch分支的channelif stride==1:#stride=1self.Channel1=nn.Sequential(baseConv_1(branch_channel,branch_channel,1,1),baseConv_1(branch_channel,branch_channel,3,1),baseConv_1(branch_channel,branch_channel,1,1))self.Channel2=nn.Identity()else:#stride=2 此时inchannels!=输出channelself.Channel1=nn.Sequential(baseConv_1(inchannels,branch_channel,1,1),baseConv_1(branch_channel,branch_channel,3,2),baseConv_1(branch_channel,branch_channel,1,1))self.Channel2=nn.Sequential(baseConv_1(inchannels,inchannels,3,2),baseConv_1(inchannels,branch_channel,1,1))def forward(self,x):if self.stride==1:#Channel Splitsplit_channel=x.size(1)//2x1,x2=x[:,:split_channel,:,:],x[:,split_channel:,:,:] #或者采用 x1,x2=x.chunk(2,dim=1) 也可以channel对半分开out1=self.Channel1(x1)out2=self.Channel2(x2)out=torch.cat((out1,out2),dim=1)else:out=torch.cat((self.Channel1(x),self.Channel2(x)),dim=1)#Channel Shufflereturn Channel_shuffle(out,2)#定义Shuffle_Net_v2网络结构
class ShuffleNet_v2(nn.Module):def __init__(self,out_channel_list,numclasses):super(ShuffleNet_v2, self).__init__()repeat_num=[1,3,1,7,1,3,1]net_config=[[24,out_channel_list[0],2],[out_channel_list[0],out_channel_list[1],1],[out_channel_list[1],out_channel_list[2],2],[out_channel_list[2],out_channel_list[3],1],[out_channel_list[3],out_channel_list[4],2],[out_channel_list[4],out_channel_list[5],1]]#定义有序字典存放网络结构self.ModuList=OrderedDict()self.ModuList.update({'Conv1_Pool':nn.Sequential(nn.Conv2d(3,24,3,2,1,bias=False),nn.BatchNorm2d(24),nn.ReLU(inplace=True),nn.MaxPool2d(3,2,1))})for idx,item in enumerate(net_config):for j in range(repeat_num[idx]):self.ModuList.update({'Stage{}_{}'.format(idx,j):Residual_block(item[0],item[1],item[2])})item[0]=item[1]item[-1]=1#Conv5self.ModuList.update({'Conv5':baseConv_1(item[1],1024,1,1)})self.ModuList=nn.Sequential(self.ModuList)#linearself.linear=nn.Linear(1024,numclasses)def forward(self,x):x=self.ModuList(x)x=F.adaptive_avg_pool2d(x,(1,1)).flatten(1) #Global Pool 或者采用x.mean([2,3])也是相同效果x=self.linear(x)return xdef ShuffleNet_v2_1x(numclasses):out_list=[116,116,232,232,464,464]return ShuffleNet_v2(out_channel_list=out_list,numclasses=numclasses)def ShuffleNet_v2_0_5x(numclasses):out_list=[48,48,96,96,192,192]return ShuffleNet_v2(out_channel_list=out_list,numclasses=numclasses)if __name__ == '__main__':net=ShuffleNet_v2_1x(10).to('cuda')summary(net,(3,224,224))
七、完整代码
代码地址:链接: https://pan.baidu.com/s/1zz4IaEnYiTkv1_Mu06UCWA 提取码:ds18
权重下载地址:https://download.pytorch.org/models/shufflenetv2_x1-5666bf0f80.pth