[关闭]
@Team 2019-08-13T04:58:57.000000Z 字数 6877 阅读 244

《Computer vision》笔记-shufflenet(10)

石文华


1、前言

卷积神经网络在计算机视觉任务中表现优异,但由于嵌入式设备内存空间小,能耗要求低,因此需要使用更加高效的模型。例如,与单纯的堆叠卷积层相比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在保持原有准确率的条件下大大降低计算量。

2、 分组卷积(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。分组卷积有个缺点就是每个输出通道只能从有限输入通道获得信息,即一个分组的输出只和这个分组的输入有关,阻止通道组之间的信息流的流动,限制了模型表达能力。
image.png-66.5kB
上图表示用两个叠加的分组卷积进行信道shuffle操作。gconv代表分组卷积。a)中两个具有相同组数的叠加卷积层。每个输出通道仅与组内的输入通道相关。没有信息交流;b)中当gconv1之后gconv2从不同分组获取部分通道信息;c)是b)中通道重排的等效实现,将每个 group 分为更小的n个 subgroup,然后将每个 subgroup 输出分散到每个 group 下一层输入。具体来说就是加入 GConv1 有 g x n 个输出通道,则首先将输出通道变维为 (g, n),再转置,最后展开成一维送入下一个层,这样就实现了通道重排,如下所示:

11916555-5bcbec5c5e84baf1.png-12.2kB

3、ShuffleNet Units

image.png-54.6kB

利用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),从而弥补了分辨率减小而带来的信息损失。

4、模型结构

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

5、改进(shufflenet v2)

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

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

(2)ShuffleNet V2

image.png-79kB
上图(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卷积层来混合特征。

6、代码:

(1)Conv-Bn-Relu moduule

  1. """
  2. Conv-Bn-Relu moduule
  3. """
  4. class ConvBnRelu(nn.Module):
  5. def __init__(self,in_channels,out_channels,kernel_size,stride=1,
  6. padding=0,dilation=1,groups=1,relu6=False):
  7. super(ConvBnRelu,self).__init__()
  8. self.conv=nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding, dilation, groups, bias=False)
  9. self.bn=nn.BatchNorm2d(out_channels)
  10. self.relu=nn.ReLU6(inplace=True) if relu6 else nn.ReLU(inplace=True)
  11. def forward(self,x):
  12. x=self.conv(x)
  13. x=self.bn(x)
  14. x=self.relu(x)
  15. return x

(2)channel_shuffle

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

(3)ShuffleNetUtil(V1)

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

(4)DWConv

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

(5)ShuffleNetV2Util

  1. class ShuffleNetV2Util(nn.Module):
  2. '''
  3. 有效的网络架构设计推导出的一些实用指南:
  4. (Ⅰ)G1:相等的通道宽度可最大限度地降低内存访问成本(MAC);
  5. (Ⅱ)G2:过多的组卷积会增加MAC;
  6. (Ⅲ)G3:网络碎片降低了并行度;
  7. (Ⅳ)G4:逐元素操作的执行时间是不可忽略的;
  8. 基于上述指导原则和研究,有效的网络架构应该:
  9. (Ⅰ)使用“平衡卷积"(相等的通道宽度);
  10. (Ⅱ)注意使用组卷积的成本;
  11. (Ⅲ)降低碎片程度;
  12. (Ⅳ)减少逐元素操作。
  13. '''
  14. def __init__(self,in_channels,out_channels,stride,dilation=1):
  15. super(ShuffleNetV2Util,self).__init__()
  16. assert stride in [1,2,3]
  17. self.stride = stride
  18. self.dilation = dilation
  19. inter_channels = out_channels // 2 #channel split
  20. if(stride>1 or dilation>1):#带下采样的模块,左边的路径的特征图也需要进行相应的下采样,同时也不使用channel split
  21. self.branch1=nn.Sequential(
  22. DWConv(in_channels,in_channels,3,stride, dilation, dilation),
  23. nn.BatchNorm2d(in_channels),
  24. ConvBnRelu(in_channels,inter_channels,1))
  25. self.branch2=nn.Sequential(#如果带下采样的模块,右侧的路径有所不同,也就是不需要进行channel split
  26. ConvBnRelu(in_channels if (stride > 1) else inter_channels,inter_channels, 1),
  27. DWConv(inter_channels,inter_channels,3,stride,dilation, dilation),
  28. nn.BatchNorm2d(inter_channels),
  29. ConvBnRelu(inter_channels,inter_channels,1)
  30. )
  31. def init_weights(self):
  32. for m in self.modules():
  33. if isinstance(m, nn.Conv2d):
  34. nn.init.kaiming_normal_(m.weight, mode='fan_out')
  35. if m.bias is not None:
  36. nn.init.zeros_(m.bias)
  37. elif isinstance(m, nn.BatchNorm2d):
  38. nn.init.ones_(m.weight)
  39. nn.init.zeros_(m.bias)
  40. elif isinstance(m, nn.Linear):
  41. nn.init.normal_(m.weight, 0, 0.01)
  42. if m.bias is not None:
  43. nn.init.zeros_(m.bias)
  44. def forward(self,x):
  45. if(self.stride==1 and self.dilation==1):#如果不进行下采样,则左路不需要做任何运算
  46. x1,x2=x.chunk(2,dim=1)#torch.chunkinput, chunks, dim),与torch.cat()的作用相反。注意,返回值的数量会随chunks的值而发生变化.
  47. out=torch.cat((x1, self.branch2(x2)), dim=1)
  48. else:
  49. out=torch.cat((self.branch1(x), self.branch2(x)),dim=1)
  50. out=channel_shuffle(out,2)#参数2表示groups2组,因为分成两条路径,生成两组特征图
  51. 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

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注