查看原文
其他

《Computer vision》笔记-shufflenet(10)

石文华 机器学习算法工程师 2021-12-31

   作者:石文华           

编辑:陈人和           


前  言

卷积神经网络在计算机视觉任务中表现优异,但由于嵌入式设备内存空间小,能耗要求低,因此需要使用更加高效的模型。例如,与单纯的堆叠卷积层相比GoogLeNet增加了网络的宽度但降低了复杂度,SqueezeNet在保持精度的同时大大减少参数和计算量,ResNet利用高效的bottleneck结构实现惊人的效果,Xception中提出深度可分卷积概括了Inception序列,MobileNet利用深度可分卷积构建的轻量级模型获得了不错的效果,Senet(取得了 ImageNet 2017 的冠军)引入了一个架构单元对 Channel 之间的信息进行显式建模,它以很小的计算成本提高了性能。而ShuffleNet主要使用了pointwise group convolution(分组的1×1卷积核)代替密集的1×1卷积来减小计算复杂度(因为作者发现,对于resnext中的每个residual unit,1×1卷积(逐点卷积)占据93.4%的运算)。同时为了消除pointwise group convolution带来的副作用,提出了一个新的channel shuffle操作来使得信息能够在特征通道上进行交叉流动,因此shuffleNet在保持原有准确率的条件下大大降低计算量。


章节目录

  • 分组卷积(group convolution)和通道重排(Channel shuffle)

  • ShuffleNet Units

  • 模型结构

  • 改进(shufflenet v2)

  • 代码



01

分组卷积(group convolution)和通道重排(Channel shuffle)

分组卷积是在通道上采用稀疏连接方式,将特征图在通道上切分为多个组(可以想象成一根甘蔗,横着切成多份),然后通过确保每个卷积只在相应的输入信道组上运行,来减少了计算量。为什么计算量会减少呢。假设input的特征图大小是W*H*C1,output的特征图大小是W*H*C2,卷积核大小为1*1的卷积,那么如果不采用分组卷积的话,卷积的计算量为1*1*C1*C2*W*H,如果采用分组卷积,将特征图按照通道分为g分,那么每一份的大小是W*H*C1/g,由于输出通道最终为C2,所以平分下来每一份单独卷积之后的输出也只需要C2/g个通道就行了,因此对于每一份来说,它的计算量为1*1*C1/g*C2/g*W*H,所以g份总的计算量为g*(1*1* C1/g*C2/g *W*H)=1*1*C1*C2*W*H/g。可以发现,分为g份,计算量就降低到1/g。分组卷积有个缺点就是每个输出通道只能从有限输入通道获得信息,即一个分组的输出只和这个分组的输入有关,阻止通道组之间的信息流的流动,限制了模型表达能力。

上图表示用两个叠加的分组卷积进行信道shuffle操作。gconv代表分组卷积。a)中两个具有相同组数的叠加卷积层。每个输出通道仅与组内的输入通道相关。没有信息交流;b)中当gconv1之后gconv2从不同分组获取部分通道信息;c)是b)中通道重排的等效实现,将每个 group 分为更小的n个 subgroup,然后将每个 subgroup 输出分散到每个 group 下一层输入。具体来说就是加入 GConv1 有 g x n 个输出通道,则首先将输出通道变维为 (g, n),再转置,最后展开成一维送入下一个层,这样就实现了通道重排,如下所示:



02

ShuffleNet Units

利用channel shuffle操作的优点,提出了一种专为小型网络设计的ShuffleNet unit。从图2(a)中可以看到ShuffleNet unit采用残差单元的设计原理。不同之处在于将之前残差单元分支中的3*3卷积改用为深度可分离卷积(DWConv)。然后,将第一个1×1层替换为1x1的pointwise group convolution(GConv),然后进行channel shuffle操作,形成channel shuffle单元,如图2(b)所示。第二个pointwise group convolution(GConv)的目的是为了恢复通道尺寸使其能够和快捷路径的特征图进行Add运算。为了简单起见,第二个GConv之后没有使用channel shuffle操作。对于带下采样功能的 ShuffleNet unit,参见图2(c):(1)在快捷路径上添加一个3×3的AVG Pool;(2)用concat连接替换元素加操作(Add),从而弥补了分辨率减小而带来的信息损失。


03

模型结构

该网络有三个阶段组成,对应的分辨率分别是图中的28,14,7,对应的shuffle util重复次数分别是3,7,3。每个阶段的第一个shuffle util块的步幅为2,同一个阶段下的shuffle util中的其他超参数保持不变,shuffle util的瓶颈通道的数量设置为每个shuffle util单元输出通道的1/4。


04

改进(shufflenet v2)

(1)有效的网络架构设计推导出的一些实用指南:

(Ⅰ)G1:相等的通道宽度可最大限度地降低内存访问成本(MAC);
(Ⅱ)G2:过多的组卷积会增加MAC;
(Ⅲ)G3:网络碎片降低了并行度;
(Ⅳ)G4:逐元素操作的执行时间是不可忽略的;
基于上述指导原则和研究,有效的网络架构应该:
(Ⅰ)使用“平衡卷积"(相等的通道宽度);
(Ⅱ)注意使用组卷积的成本;
(Ⅲ)降低碎片程度;
(Ⅳ)减少逐元素操作。

(2)ShuffleNet V2

上图(a)(b)是shufflenet v1中的结构,在ShuffleNet v1中采用了两种技术:逐点组卷积和瓶颈状结构。然后引入“channel shuffle”操作以实现不同信道组之间的信息通信并提高准确性。根据指导原则,逐点组卷积和瓶颈结构都会增加MAC(G1和G2),使用太多组违反了G3。在直连通道中进行逐元素相加的操作也是不合需要的(G4)。
因此,为实现高模型容量和效率,关键问题是如何保持大量且同样宽的信道,既没有密集卷积也没有太多组。为了达到上述目的,我们引入了一个名为channel split的简单运算符。如上图(c)所示。在每个单元的开始处,c个特征通道的输入被分成两个分支,分别具有c−c′和c′个通道(为简单起见,c′=c/2)。按照G3,一个分支是恒等函数。另一个分支由三个卷积组成,这三个卷积具有相同的输入和输出通道以满足G1。不同于ShuffleNetV1,两个1×1的卷积不再是分组的了。这部分是为了遵循G2,部分原因是拆分操作已经产生了两个组。卷积后,两个分支连接在一起。因此,通道数保持不变(G1)。然后使用与ShuffleNet V1中相同的“channel shuffle“操作来实现两个分支之间的信息通信。shuffle后,进入了下一个网络块。请注意,ShuffleNet v1中的“Add“操作不再存在。ReLU和depthwise convolutions等元素操作仅存在于一个分支中。此外,三个连续的元素操作,”Concat“,“Channel Shuffle”和”Channel Split“,合并为单个逐元素操作。根据G4,这些更改是有益的。
对于空间下采样,该单元稍作修改,如上图(d)所示。删除了channel split。因此,输出通道的数量加倍。还需要注意的是,shufflenetv2在全局平均池化之前添加额外的1×1卷积层来混合特征。


05

代码

(1)Conv-Bn-Relu moduule
""" Conv-Bn-Relu moduule """ class ConvBnRelu(nn.Module): def __init__(self,in_channels,out_channels,kernel_size,stride=1, padding=0,dilation=1,groups=1,relu6=False): super(ConvBnRelu,self).__init__() self.conv=nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding, dilation, groups, bias=False) self.bn=nn.BatchNorm2d(out_channels) self.relu=nn.ReLU6(inplace=True) if relu6 else nn.ReLU(inplace=True) def forward(self,x): x=self.conv(x) x=self.bn(x) x=self.relu(x) return x

(2)channel_shuffle

def channel_shuffle(x,groups): n,c,h,w=x.size() channels_per_group=c//groups x = x.view(n, groups, channels_per_group, h, w) x = torch.transpose(x, 1, 2).contiguous() x = x.view(n, -1, h, w) return x

(3)ShuffleNetUtil(V1)

class ShuffleNetUtil(nn.Module): def __init__(self,in_channels,out_channels,stride,groups,dilation=1): super(ShuffleNetUtil,self).__init__() self.stride=stride #步幅 self.groups=groups #分组 self.dilation=dilation #空洞率 assert stride in [1,2,3] # inter_channels=out_channels//4 #瓶颈通道的数量 if(stride>1): self.shortcut = nn.AvgPool2d(3, stride, 1) out_channels -= in_channels elif(dilation>1): out_channels-=in_channels if in_channels==24: g=1 else: g=groups self.conv1=ConvBnRelu(in_channels,inter_channels,1,groups=g) self.conv2=ConvBnRelu(inter_channels,inter_channels,3,stride,dilation,dilation,groups) self.conv3=nn.Sequential( nn.Conv2d(inter_channels, out_channels, 1, groups=groups, bias=False), nn.BatchNorm2d(out_channels)) def forward(self,x): out=self.conv1(x) #分组卷积 out=channel_shuffle(out,self.groups) #channle shuffle out=self.conv2(out) #深度可分卷积 out=self.conv3(out) #分组卷积,不进行激活 if self.stride > 1: x = self.shortcut(x) #下采样功能的 ShuffleNet unit中,需要将x的分辨率降低,然后才能concat连接,使用concat连接这样做的目的主要是降低计算量与参数大小 out = torch.cat([out, x], dim=1) elif self.dilation > 1: #如果采用空洞卷积的话,也是采用concat的连接方式 out = torch.cat([out, x], dim=1) else: #残差块里面如果没有降低分辨率的,连接方式使用加操作(Add) out = out + x out = F.relu(out) return out

(4)DWConv

class DWConv(nn.Module): def __init__(self,in_channels,out_channels,kernel_size,stride=1,padding=0, dilation=1,bias=False): super(DWConv,self).__init__() self.conv=nn.Conv2d(in_channels,out_channels,kernel_size,stride, padding, dilation, groups=in_channels, bias=bias) def forward(self,x): return self.conv(x)

(5)ShuffleNetV2Util

class ShuffleNetV2Util(nn.Module): ''' 有效的网络架构设计推导出的一些实用指南: (Ⅰ)G1:相等的通道宽度可最大限度地降低内存访问成本(MAC); (Ⅱ)G2:过多的组卷积会增加MAC; (Ⅲ)G3:网络碎片降低了并行度; (Ⅳ)G4:逐元素操作的执行时间是不可忽略的; 基于上述指导原则和研究,有效的网络架构应该: (Ⅰ)使用“平衡卷积"(相等的通道宽度); (Ⅱ)注意使用组卷积的成本; (Ⅲ)降低碎片程度; (Ⅳ)减少逐元素操作。 ''' def __init__(self,in_channels,out_channels,stride,dilation=1): super(ShuffleNetV2Util,self).__init__() assert stride in [1,2,3] self.stride = stride self.dilation = dilation inter_channels = out_channels // 2 #channel split if(stride>1 or dilation>1):#带下采样的模块,左边的路径的特征图也需要进行相应的下采样,同时也不使用channel split self.branch1=nn.Sequential( DWConv(in_channels,in_channels,3,stride, dilation, dilation), nn.BatchNorm2d(in_channels), ConvBnRelu(in_channels,inter_channels,1)) self.branch2=nn.Sequential(#如果带下采样的模块,右侧的路径有所不同,也就是不需要进行channel split ConvBnRelu(in_channels if (stride > 1) else inter_channels,inter_channels, 1), DWConv(inter_channels,inter_channels,3,stride,dilation, dilation), nn.BatchNorm2d(inter_channels), ConvBnRelu(inter_channels,inter_channels,1) ) def init_weights(self): for m in self.modules(): if isinstance(m, nn.Conv2d): nn.init.kaiming_normal_(m.weight, mode='fan_out') if m.bias is not None: nn.init.zeros_(m.bias) elif isinstance(m, nn.BatchNorm2d): nn.init.ones_(m.weight) nn.init.zeros_(m.bias) elif isinstance(m, nn.Linear): nn.init.normal_(m.weight, 0, 0.01) if m.bias is not None: nn.init.zeros_(m.bias) def forward(self,x): if(self.stride==1 and self.dilation==1):#如果不进行下采样,则左路不需要做任何运算 x1,x2=x.chunk(2,dim=1)#torch.chunk(input, chunks, dim),与torch.cat()的作用相反。注意,返回值的数量会随chunks的值而发生变化. out=torch.cat((x1, self.branch2(x2)), dim=1) else: out=torch.cat((self.branch1(x), self.branch2(x)),dim=1) out=channel_shuffle(out,2)#参数2表示groups为2组,因为分成两条路径,生成两组特征图 return out



参考

https://mp.weixin.qq.com/s/-AJ3RQK9vpV1rYNk4CLQ_A
https://mp.weixin.qq.com/s/0MvCnm46pgeMGEw-EdNv_w
https://arxiv.org/pdf/1707.01083.pdf
https://arxiv.org/pdf/1807.11164.pdf






 

END








往期回顾之作者刘才权

【1】《机器学习》笔记-半监督学习(13)

【2】《机器学习》笔记-计算学习理论(12)

【3】《机器学习》笔记-特征选择与稀疏学习(11)

【4】《机器学习》笔记-降维与度量学习(10)

【5】《机器学习》笔记-聚类(9)

【6】《机器学习》笔记-集成学习(8)

【7】《机器学习》笔记-贝叶斯分类器(7)









机器学习算法工程师


                            一个用心的公众号

长按,识别,加关注

进群,学习,得帮助

你的关注,我们的热度,

我们一定给你学习最大的帮助





你点的每个赞,我都认真当成了喜欢










: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存