(首先给助教老师说声抱歉,由于我们组的疏忽导致读书报告交晚了,非常感激助教老师的提醒,谢谢!由于微信群文件没有保存,我们没有查到组号,我们的成员是:刘博艺、刁颂辉、陈帆、李在林)
ShuffleNet是旷视科技最近提出的一种计算高效的CNN模型,其和MobileNet和SqueezeNet等一样主要是想应用在移动端。所以,ShuffleNet的设计目标也是如何利用有限的计算资源来达到最好的模型精度,这需要很好地在速度和精度之间做平衡。ShuffleNet的核心是采用了两种操作:pointwise 。shuffle,这在保持精度的同时大大降低了模型的计算量。目前移动端CNN模型主要设计思路主要是两个方面:模型结构设计和模型压缩。ShuffleNet和MobileNet一样属于前者,都是通过设计更高效的网络结构来实现模型变小和变快,而不是对一个训练好的大模型做压缩或者迁移。下面我们将详细讲述ShuffleNet的设计思路,网络结构及模型效果,最后使用Pytorch来实现ShuffleNet网络。
设 计 思 想
卷积神经网络是现代视觉人工智能系统的核心组件。近年来关于卷积模型的研究层出不穷,产生了如VGG、ResNet、Xception 和 ResNeXt等性能优异的网络结构,在多个视觉任务上超过了人类水平。然而,这些成功的模型往往伴随着巨大的计算复杂度(数十亿次浮点操作,甚至更多)。这就限制了此类模型只能用于高性能的服务器集群,而对于很多移动端应用(通常最多容许数百万至数千万次浮点操作)则无能为力。
解决这一难题的方法之一是设计更为轻量级的模型结构。现代卷积神经网络的绝大多数计算量集中在卷积操作上,因此高效的卷积层设计是减少网络复杂度的关键。其中,稀疏连接(sparse connection)是提高卷积运算效率的有效途径,当前不少优秀的卷积模型均沿用了这一思路。例如,谷歌的”Xception“网络[1]引入了”深度可分离卷积”的概念,将普通的卷积运算拆分成逐通道卷积(depthwise convolution)和逐点卷积(pointwise convolution)两部进行,有效地减少了计算量和参数量;而 Facebook 的“ResNeXt”网络[2]则首先使用逐点卷积减少输入特征的通道数,再利用计算量较小的分组卷积(group convolution)结构取代原有的卷积运算,同样可以减少整体的计算复杂度。ShuffleNet 网络结构同样沿袭了稀疏连接的设计理念。作者通过分析 Xception 和 ResNeXt模型,发现这两种结构通过卷积核拆分虽然计算复杂度均较原始卷积运算有所下降,然而拆分所产生的逐点卷积计算量却相当可观,成为了新的瓶颈。例如对于ResNeXt 模型逐点卷积占据了 93.4% 的运算复杂度。可见,为了进一步提升模型的速度,就必须寻求更为高效的结构来取代逐点卷积。受 ResNeXt 的启发,作者提出使用分组逐点卷积(group pointwise convolution)来代替原来的结构。通过将卷积运算的输入限制在每个组内,模型的计算量取得了显著的下降。然而这样做也带来了明显的问题:在多层逐点卷积堆叠时,模型的信息流被分割在各个组内,组与组之间没有信息交换(如图 1(a) 所示)。这将可能影响到模型的表示能力和识别精度。
图 1 逐点卷积与通道重排操作
因此,在使用分组逐点卷积的同时,需要引入组间信息交换的机制。也就是说,对于第二层卷积而言,每个卷积核需要同时接收各组的特征作为输入,如图 1(b) 所示。作者指出,通过引入“通道重排”(channel shuffle,见图 1(c) )可以很方便地实现这一机制;并且由于通道重排操作是可导的,因此可以嵌在网络结构中实现端到端的学习。
网 络 结 构
首先来构造ShuffleNet的基本单元,如图2所示。ShuffleNet的基本单元是在一个残差单元的基础上改进而成的。如图2-a所示,这是一个包含3层的残差单元:首先是1x1卷积,然后是3x3的depthwise convolution(DWConv,主要是为了降低计算量),这里的3x3卷积是瓶颈层(bottleneck),紧接着是1x1卷积,最后是一个短路连接,将输入直接加到输出上。现在,进行如下的改进:将密集的1x1卷积替换成1x1的group convolution,不过在第一个1x1卷积之后增加了一个channle shuffle操作。值得注意的是3x3卷积后面没有增加channle shuffle,按paper的意思,对于这样一个残差单元,一个channle shuffle操作是足够了。还有就是3x3的depthwise convolution之后没有使用ReLU激活函数。改进之后如图2-b所示。对于残差单元,如果stride=1时,此时输入与输出shape一致可以直接相加,而当stride=2时,通道数增加,而特征图大小减小,此时输入与输出不匹配。一般情况下可以采用一个1x1卷积将输入映射成和输出一样的shape。但是在ShuffleNet中,却采用了不一样的策略,如图2-c所示:对原输入采用stride=2的3x3avg pool,这样得到和输出一样大小的特征图,然后将得到特征图与输出进行连接(concat),而不是相加。这样做的目的主要是降低计算量与参数大小。
图2 ShuffleNet的基本单元
基于上面改进的ShuffleNet基本单元,设计的ShuffleNet模型如表1所示。可以看到开始使用的普通的3x3的卷积和max pool层。然后是三个阶段,每个阶段都是重复堆积了几个ShuffleNet的基本单元。对于每个阶段,第一个基本单元采用的是stride=2,这样特征图width和height各降低一半,而通道数增加一倍。后面的基本单元都是stride=1,特征图和通道数都保持不变。对于基本单元来说,其中瓶颈层,就是3x3卷积层的通道数为输出通道数的1/4,这和残差单元的设计理念是一样的。不过有个细节是,对于stride=2的基本单元,由于原输入会贡献一部分最终输出的通道数,那么在计算1/4时到底使用最终的通道数,还是仅仅未concat之前的通道数。文章没有说清楚,但是个人认为应该是后者吧。其中g控制了group convolution中的分组数,分组越多,在相同计算资源下,可以使用更多的通道数,所以g越大时,采用了更多的卷积核。这里给个例子,当g=3时,对于第一阶段的第一个基本单元,其输入通道数为24,输出通道数为240,但是其stride=2,那么由于原输入通过avg pool可以贡献24个通道,所以相当于左支只需要产生240-24=216通道,中间瓶颈层的通道数就为216/4=54。其他的可以以此类推。当完成三阶段后,采用global pool将特征图大小降为1x1,最后是输出类别预测值的全连接层。
表1 ShuffleNet网络结构
实 验 结 果
作者通过一系列在 ImageNet 2016 分类数据集上的控制实验说明了 ShuffleNet结构单元每个部件存在的必要性、对于其他网络结构单元的优越性。接着作者通过在 MS COCO目标检测上的结果说明模型的泛化能力。最后,作者给出了在 ARM 计算平台上 ShuffleNet 实际运行时的加速效果。分组化逐点卷积。作者对于计算复杂度为 140 MFLOPs 、 40 MFLOPs、13 MFLOPs的 ShuffleNet模型,在控制模型复杂度的同时对比了分组化逐点卷积的组数在1~8时分别对于性能的影响。从 表1中可以看出,带有分组的(g>1)的网络的始终比不带分组(g=1)的网络的错误率低。作者观察到对于较小的网络(如 ShuffleNet 0.25x),较大的分组会得到更好结果,认为更宽的通道对于小网络尤其重要。受这点启发,作者移除了网络第三阶段的两个结构单元,将节省下来的运算量用来增加网络宽度后,网络性能进一步提高。
表1 组数对分类错误率的影响
通道重排
通道重排的目的是使得组间信息能够互相交流。在实验中,有通道重排的网络始终优于没有通道重排的网络,错误率降低 0.9%~4.0%。尤其是在组数较大时(如g=8),前者远远优于后者。对比其他结构单元作者使用一样的整体网络布局,在保持计算复杂度的同时将 ShuffleNet 结构单元分别替换为 VGG-like、ResNet、Xception-like 和 ResNeXt 中的结构单元,使用完全一样训练方法。
表2 中的结果显示在不同的计算复杂度下,ShuffleNet 始终大大优于其他网络。
表2 和其他网络结构的分类错误率对比(百分制)对比MobileNets和其他的一些网络结构最近 Howard et al. 提出了 MobileNets[4],利用[1]里的逐通道卷积的设计移动设备上高效的网络结构。虽然ShuffleNet 是为了小于 150 MFLOPs 的模型设计的,在增大到 MobileNet 的 500~600 MFLOPs量级,依然优于 MobileNet。而在 40 MFLOPs 量级,ShuffleNet 比 MobileNet 错误率低 6.7%。详细结果可以从表3中得到。
表3 ShuffleNet 和 MobileNet 对比
和其他一些网络结构相比,ShuffleNet 也体现出很大的优势。从表4中可以看出,ShuffleNet 0.5x 仅用 40
MFLOPs 就达到了 AlexNet 的性能,而 AlexNet 的计算复杂度达到了 720 MFLOPs,是 ShuffleNet 的
18 倍。
表4 ShuffleNet 和其他网络结构计算复杂度的对比
MS COCO物体检测
在 Faster-RCNN[5]框架下,和 1.0 MobileNet-224 网络复杂度可比的 ShuffleNet 2x,在 600 分辨率的图上的 mAP 达到 24.5%,而 MobileNet 为 19.8%,表明网络在检测任务上良好的泛化能力。最后作者在一款 ARM 平台上测试了网络的实际运行速度。在作者的实现里 40 MFLOPs 的 ShuffleNet对比相似精度的 AlexNet 实际运行速度快约 13x 倍。224 x 224 输入下只需 15.2 毫秒便可完成一次推理,在 1280 x720 的输入下也只需要 260.1 毫秒。
Python实现(参考相关博客和github)
这里我们使用Pytorch来实现ShuffleNet,Pytorch是Facebook提出的一种深度学习动态框架,之所以采用Pytorch是因为其nn.Conv2d天生支持group
convolution,不过尽管TensorFlow不支持直接的group
convolution,但是其实可以自己间接地来实现。不过患有懒癌的我还是使用Pytorch吧。
首先我们来实现channle shuffle操作,就按照前面讲述的思路来实现:
defshuffle_channels(x, groups):
"""shuffle channels of a 4-D Tensor"""
batch_size, channels, height, width
= x.size()
assertchannels % groups ==0
channels_per_group = channels// groups
# split into groups
x = x.view(batch_size, groups, channels_per_group,
height, width)
# transpose1,2axis
x = x.transpose(1,2).contiguous()
# reshape into orignal
x = x.view(batch_size, channels, height, width)
returnx
然后我们实现ShuffleNet中stride=1的基本单元:
classShuffleNetUnitA(nn.Module):
"""ShuffleNet unit for stride=1"""
def__init__(self, in_channels, out_channels, groups=3):
super(ShuffleNetUnitA, self).__init__()
assertin_channels == out_channels
assertout_channels %4==0
bottleneck_channels = out_channels //4
self.groups = groups
self.group_conv1 = nn.Conv2d(in_channels, bottleneck_channels,
1, groups=groups, stride=1)
self.bn2 = nn.BatchNorm2d(bottleneck_channels)
self.depthwise_conv3 = nn.Conv2d(bottleneck_channels,
bottleneck_channels,3, padding=1, stride=1,
groups=bottleneck_channels)
self.bn4 = nn.BatchNorm2d(bottleneck_channels)
self.group_conv5 = nn.Conv2d(bottleneck_channels, out_channels,
1, stride=1, groups=groups)
self.bn6 = nn.BatchNorm2d(out_channels)
defforward(self, x):
out = self.group_conv1(x)
out = F.relu(self.bn2(out))
out = shuffle_channels(out, groups=self.groups)
out = self.depthwise_conv3(out)
out = self.bn4(out)
out = self.group_conv5(out)
out = self.bn6(out)
out = F.relu(x + out)
然后是中stride=2的基本单元:
classShuffleNetUnitB(nn.Module):
"""ShuffleNet unit for stride=2"""
def__init__(self, in_channels, out_channels, groups=3):
super(ShuffleNetUnitB, self).__init__()
out_channels -= in_channels
assertout_channels %4==0
bottleneck_channels = out_channels //4
self.groups = groups
self.group_conv1 = nn.Conv2d(in_channels, bottleneck_channels,
1, groups=groups, stride=1)
self.bn2 = nn.BatchNorm2d(bottleneck_channels)
self.depthwise_conv3 = nn.Conv2d(bottleneck_channels,
bottleneck_channels,3, padding=1, stride=2,groups=bottleneck_channels)
self.bn4 = nn.BatchNorm2d(bottleneck_channels)
self.group_conv5 = nn.Conv2d(bottleneck_channels, out_channels,
1, stride=1, groups=groups)
self.bn6 = nn.BatchNorm2d(out_channels)
defforward(self, x):
out = self.group_conv1(x)
out = F.relu(self.bn2(out))
out = shuffle_channels(out, groups=self.groups)
out = self.depthwise_conv3(out)
out = self.bn4(out)
out = self.group_conv5(out)
out = self.bn6(out)
x = F.avg_pool2d(x,3, stride=2, padding=1)
out = F.relu(torch.cat([x, out], dim=1))
returnout
最后是g=3的ShuffleNet的实现:
classShuffleNet(nn.Module):
"""ShuffleNet for groups=3"""
def__init__(self, groups=3, in_channels=3, num_classes=1000):
super(ShuffleNet, self).__init__()
self.conv1 = nn.Conv2d(in_channels,24,3, stride=2, padding=1)
stage2_seq = [ShuffleNetUnitB(24,240, groups=3)] +
[ShuffleNetUnitA(240,240, groups=3)foriinrange(3)]
self.stage2 = nn.Sequential(*stage2_seq)
stage3_seq = [ShuffleNetUnitB(240,480, groups=3)] +
[ShuffleNetUnitA(480,480, groups=3)foriinrange(7)]
self.stage3 = nn.Sequential(*stage3_seq)
stage4_seq = [ShuffleNetUnitB(480,960, groups=3)] +
[ShuffleNetUnitA(960,960, groups=3)foriinrange(3)]
self.stage4 = nn.Sequential(*stage4_seq)
self.fc = nn.Linear(960, num_classes)
defforward(self, x):
net = self.conv1(x)
net = F.max_pool2d(net,3, stride=2, padding=1)
net = self.stage2(net)
net = self.stage3(net)
net = self.stage4(net)
net = F.avg_pool2d(net,7)
net = net.view(net.size(0),-1)
net = self.fc(net)
logits = F.softmax(net)
returnlogits