ShuffleNet V1
创新点:1、pointwise group convolution 2、channel shuffle。
一、pointwise group convolution
pointwise group convolution即将逐点卷积和组卷积两个操作整合在一起。目的是为了减少模型参数量,提高检测速度。
逐点卷积:在MobileNet系列中,提到深度可分离卷积中包含深度卷积(depthwise convolution)和逐点卷积(pointwise convolution)。逐点卷积其实就是1x1的卷积核,其作用是弥补深度卷积的弊端。因为深度卷积只在各个单通道上操作,而没有利用通道间相互的关联信息。
组卷积:组卷积其实不是新鲜的东西,这是由于当时的GTX 580显存太小,没法放下整个网络,所以Alex采用Group convolution将整个网络分成两组后,分别放入一张卡进行训练。示意图如下,将输入的feature map和卷积核在通道维数上分为g组,原本的feature map维度为[C,H,W],分组之后每组的维度为[C/g,H,W]。当g=C时,每组的维度就变为[1,H,W],每组为单通道,就等同于上图的深度卷积了。
AlexNet
标准卷积和分组卷积
故pointwise group convolution,其实就是将上图分组卷积中的卷积核h,w为1x1的时候,然后分成g组。
参数量和计算量计算:
参数量和计算量计算
相比加入Depthwise的ResNet缩小了很多的计算量。所以ShuffleNet相当于保留ResNet结构,同时又压低计算量的改进版。同时g越大,即分的组越多,其参数量越少。而g=1即不分组时,参数量就等同于ResNet的情况。g是一个超参数,下图为g不同取值的分类错误率,越小代表模型越好。
不同分组g下的错误率
二、channel shuffle
之前提到MobileNet的逐点卷积是为了弥补深度卷积未利用通道间的缺陷,那么分组逐点卷积也只是利用组内的通道信息,没有将组与组之间的通道关联信息加以利用,为了解决该问题,作者引入了组间信息交换的机制,即channel shuffle,示意图如下。
即将每组之间的channel提取出来,进行洗牌,但是洗牌也是有规则的。类似,现有9名同学,被分到3个班级,每班有3名同学,序号为1,2,3。考试期间将相同班级的同学隔开,打乱顺序。故将每班序号为1的同学放到一班,序号为2号的同学放到二班,序号为3号的同学放到3班,这样原先每个班级都有别的班级的同学了。
channel shuffle示意图
这么代码中应如何实现呢?
import torch
students=torch.tensor([1,2,3,1,2,3,1,2,3])
print('所有同学')
print(students)
groups=3 #班级个数
clas=students.view(groups,students.size(0)//groups)
print('现有班级')
print(clas)
new_clas=clas.transpose(1,0).contiguous().view(-1)
print('洗牌之后的同学')
print(new_clas)
>>>
所有同学
tensor([1, 2, 3, 1, 2, 3, 1, 2, 3])
现有班级
tensor([[1, 2, 3],
[1, 2, 3],
[1, 2, 3]])
洗牌之后的同学
tensor([1, 1, 1, 2, 2, 2, 3, 3, 3])
Shuffle Net V1存在的问题:
1、Shuffle channel在实现的时候需要大量的指针跳转和Memory set,这本身就是极其耗时的;同时又特别依赖实现细节,导致实际运行速度不会那么理想。
2、Shuffle channel规则是人工设计出来的,不是网络自己学出来的。这不符合网络通过负反馈自动学习特征的基本原则,又陷入人工设计特征的老路(如sift/HOG等)。
Pytorch搭建Shuffle Net V1
import torch
import torch.nn as nn
class ShuffleNetUnit(nn.Module):
def __init__(self, in_channel, out_channel, stride, group):
super(ShuffleNetUnit, self).__init__()
# 作者提到不在stage2的第一个pointwise层使用组卷积,因为输入channel数量太少,只有24
self.group = group if in_channel != 24 else 1
self.stride = stride
# bottleneck层中间层的channel数变为输出channel数的1/4
mid_channle = int(out_channel / 4)
self.GConv1 = nn.Conv2d(in_channel, mid_channle, kernel_size=1, stride=1, bias=False, groups=self.group)
self.bn1 = nn.BatchNorm2d(mid_channle)
self.DWConv = nn.Conv2d(mid_channle, mid_channle, kernel_size=3, stride=stride, padding=1, bias=False,
groups=mid_channle)
self.bn2 = nn.BatchNorm2d(mid_channle)
self.GConv2 = nn.Conv2d(mid_channle, out_channel, kernel_size=1, stride=1, bias=False, groups=self.group)
self.bn3 = nn.BatchNorm2d(out_channel)
self.relu6 = nn.ReLU6(inplace=True)
if stride == 2:
self.avgpool = nn.AvgPool2d(kernel_size=3, stride=2, padding=1)
else:
self.avgpool = None
def forward(self, input):
residual = input if self.avgpool is None else self.avgpool(input)
out = self.relu6(self.bn1(self.GConv1(input)))
N, C, H, W = out.shape
# channel shuffle 维度变换之后必须要使用.contiguous()使得张量在内存连续之后才能调用view函数
out = out.view(N, self.group, int(C / self.group), H, W). \
permute(0, 2, 1, 3, 4).contiguous().view(N, C, H, W)
out = self.bn2(self.DWConv(out))
out = self.bn3(self.GConv2(out))
if self.stride == 2:
out = torch.cat([out, residual], dim=1)
else:
out = out + residual
out = self.relu6(out)
return out
class ShuffleNet(nn.Module):
def __init__(self, cfg):
super(ShuffleNet, self).__init__()
out_channel = cfg['out_channel']
num_blocks = cfg['num_blocks']
groups = cfg['groups']
num_class = cfg['num_class']
self.in_channel = 24
self.conv1 = nn.Conv2d(3, 24, kernel_size=3, stride=2, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(24)
self.relu6 = nn.ReLU6(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.layer1 = self._make_layer(out_channel[0], num_blocks[0], groups)
self.layer2 = self._make_layer(out_channel[1], num_blocks[1], groups)
self.layer3 = self._make_layer(out_channel[2], num_blocks[2], groups)
self.global_pool = nn.AvgPool2d(kernel_size=7)
self.fc = nn.Linear(out_channel[2], num_class)
def _make_layer(self, out_channel, num_blocks, groups):
layers = []
for i in range(num_blocks):
if i == 0:
layers.append(ShuffleNetUnit(self.in_channel, out_channel - self.in_channel, stride=2, group=groups))
else:
layers.append(ShuffleNetUnit(self.in_channel, out_channel, stride=1, group=groups))
self.in_channel = out_channel
return nn.Sequential(*layers)
def forward(self, input):
out = self.relu6(self.bn1(self.conv1(input)))
out = self.maxpool(out)
out = self.layer1(out)
out = self.layer2(out)
out = self.layer3(out)
out = self.global_pool(out)
out = out.view(out.size(0), -1)
out = self.fc(out)
return out
def ShuffleNetG2():
cfg = {
'out_channel': [200, 400, 800],
'num_blocks': [4, 8, 4],
'groups': 2,
'num_class': 100
}
return ShuffleNet(cfg)
def ShuffleNetG3():
cfg = {
'out_channel': [240, 480, 960],
'num_blocks': [4, 8, 4],
'groups': 3,
'num_class': 100
}
return ShuffleNet(cfg)
if __name__ == '__main__':
model = ShuffleNetG2()
print(model)
data = torch.rand(1, 3, 224, 224).float()
result = model(data)
print(result.shape)
Shuffle Net V2
提出的问题:通常我们评估模型的计算复杂度,使用的是FLOPS指标,但是该指标并不是一个直接评估的标准。下图为几乎拥有相同FLOPS数量的模型但运行速度却大不相同。速度和延迟作为直接指标,除了与FLOPS有关,还与内存访问效率memory access cost (MAC),计算硬件和并行化程度有关。所以衡量模型的速度最直接的就是在目标平台上跑一遍,记录运行时间,而不是通过FLOPS这个间接量来评估,也就是是骡子是马拉出来溜溜,别一直哔哔赖赖。
作者实验了Shuffle Net V1和Mobile Net V2在不同硬件上的不同操作的时间占比
Shuffle Net V1和Mobile Net V2在GPU和ARM上的时间占比
并结合理论与实验得到了4条实用的指导原则:
(1)相同的通道数可以减少内存访问量。即输入feature map的通道数等于输出feature map的通道数,可以减少内存消耗。下图为一个1x1卷积核的内存占据量的计算方式
计算一个1x1卷积的内存占用量
仅当c1=c2时,MAC取最小值,这个理论分析也通过实验得到证实,如下图所示,通道比为1:1时速度更快。
C1C2不同比值时的速度
(二)过多的组卷积会增加内存消耗。组卷积核的内存占有公式如下图,可以看到分组g越多,其内存消耗MAC越大。下图的对比实验可以看出,g越大,每秒能够处理的图片数量也就越少。
组卷积核的内存占用
规则2,不同g的证明实验
(三)网络碎片化会减少并行速度。这些碎片化操作是指结构中存在多路的卷积或池化操作。
碎片化结构
(四)不能忽略元素级操作。对于元素级(element-wise operators)比如ReLU、Add和Bias,虽然它们的FLOPs较小,但是却需要较大的MAC。这里实验发现如果将ResNet中残差单元中的ReLU和shortcut移除的话,速度有20%的提升。
网络结构
根据以上4条实验总结出的原则,对Shuffle Net V1进行改进,提出了Shuffle Net V2网络结构。
Shuffle Net V2对Shuffle Net V1的改进
Shuffle Net V1存在的缺陷:一、V1中采用了类似ResNet中的瓶颈层(bottleneck layer),输入和输出通道数不同,这违背了G1原则。二、V1中大量使用了逐点组卷积导致内存占用增加,这违背了G2。3三、V1采用了ResNet相似的瓶颈结构(bottleneck structures),这中多路结构中的卷积和池化层违背了G3。四、短路连接中存在大量的元素级Add运算,这违背了G4原则。
为了改善上述缺陷,Shuffle Net V2引入了channel split操作,将输入的feature map维度为[c,h,w]在通道维数上分为c'和c-c'。split之后的维度一个为[c',h,w],另一个为[c-c',h,w]。如下图,
一、当stride等于1,不进行下采样时,下边的红色分支不进行操作。上边的绿色分支包含3个连续的卷积,并且输入和输出通道相同,这符合G1规则。
二、3个卷积中的1x1卷积核不再是组卷积,这符合G2规则。
三、最后将按照通道维数将上下两个分支拼接在一起,随后对拼接的结果进行channle shuffle,以保证两个分支信息交流。而不是类似ResNet的Add操作,这符合G4规则。
stride等于1时的ShuffleNet unit
对于stride等于2的下采样操作,不再进行channel split,而是每个分支都是直接复制一份输入,每个分支都有stride=2的下采样,最后拼接在一起后,特征图空间大小减半,但是通道数翻倍。
模型的整体结构
Shuffle Net V2整体结构
pytorch实现Shuffle Net V2
https://github.com/Randl/ShuffleNetV2-pytorch
思考
最近看了一些对模型结构进行改进的轻量型网络,大体思路都是对单通道的feature map进行卷积,卷积核便是深度卷积核。最后由于这些卷积操作只是在特定数量的通道内,各个通道之间的信息被忽略了,于是MobileNet便用逐点卷积来整合通道间的信息流,这些1x1的卷积核参数占据了整体网络的大部分参数量,而ShuffleNet则是用channel shuffle来利用通道间的信息,而shuffle操作本身就是人为定义且存在大量的指针跳转,极其耗时。如果你能够想到有效利用通道间信息的方法,便可以发篇paper了:)
参考博客
https://zhuanlan.zhihu.com/p/35405071
https://zhuanlan.zhihu.com/p/48261931
网友评论