本文是转载文章,转载自深入剖析:为什么MobileNet及其变体(如ShuffleNet)会变快?,删除了文中冗余的部分,加入许多自己的理解,有些部分也通过pytorch进行了实现,并通过引入具体的计算更清晰的反映出轻量级神经网络的本质。
从MobileNet等CNN模型的组成部分出发,概述了高效CNN模型(如MobileNet及其变体)中使用的组成部分(building blocks),并解释了它们如此高效的原因。特别地,我提供了关于如何在空间和通道域进行卷积的直观说明。
在解释具体的高效CNN模型之前,我们先检查一下高效CNN模型中使用的组成部分的计算量,看看卷积在空间和通道域中是如何进行的。 假设 H x W 为输出feature map的空间大小,N为输入通道数,K x K为卷积核的大小,M为输出通道数,则标准卷积的计算量为 H*W*N*K*K*M 。这里重要的一点是,标准卷积的计算量与(1)输出特征图H x W的空间大小,(2)卷积核K x K的大小,(3)输入输出通道的数量N x M成正比。当在空间域和通道域进行卷积时,需要上述计算量。通过分解这个卷积,可以加速 CNNs,如上图所示。
首先,我提供了一个直观的解释,关于空间和通道域的卷积是如何对进行标准卷积的,它的计算量是H*W*N*K*K*M。
这里连接输入和输出之间的线,以可视化输入和输出之间的依赖关系。直线数量大致表示空间(spatial)和通道(channel)域中卷积的计算量。 例如,最常用的卷积——conv3x3,可以如上图所示。我们可以看到,输入和输出在空间域是局部连接的,而在通道域是全连接的。你可能看的不太明白,那我们换张图试试,下面是空间域中输入和输出的关系,可以看出空间域确实是局部连接的。空间域可以想象为,把特征图输入和输出的神经元拉平,然后进行连接,事实上,在计算机内部就是这么做的。
下面是通道域中输入和输出的关系(隐藏红线框部分),下图输入通道是3,输出通道是1,输入的3个通道都连接在了输出的1个通道上,这也证明了通道域是全连接的。如果输出通道有多个,也能想象出来吧。
接下来,如上所示用于改变通道数的conv1x1,或pointwise convolution。由于kernel的大小是1x1,所以这个卷积的计算量是 H*W*N*M,(H x W 为输出feature map的空间大小,N为输入通道数,M为输出通道数),计算量比conv3x3降低了1/9。这种卷积被用来“混合”通道之间的信息。
分组卷积是卷积的一种变体,将输入的feature map的通道分组,对每个分组的通道独立地进行卷积。
假设 G 表示组数,分组卷积的计算量为 H*W*N*K*K*M/G,计算量变成标准卷积的1/G。 举个例子,假如,卷积核大小为3x3,输入通道数为10,输出通道数为20,输出特征图大小为15x15,对于不分组情况下,计算量是15*15*10*3*3*20=405000,如果把输入的feature map的通道分为2组,即输入通道数为10分为2组,每组的输入通道数为5,每组输出通道数为10,输出的总通道数为20,则计算量为15*15*5*3*3*10*2=202500,可以看到计算量变成标准卷积的1/2。
在conv3x3 而且 G=2的情况。我们可以看到,通道域中的连接数比标准卷积要小,说明计算量更小。
在 conv3x3,G=3的情况下,连接变得更加稀疏。
在 conv1x1,G=2的情况下,conv1x1也可以被分组。这种类型的卷积被用于ShuffleNet中。
在深度卷积中,对每个输入通道分别进行卷积。它也可以定义为分组卷积的一种特殊情况,其中输入和输出通道数相同,G等于通道数。不得不提的是,Depthwise Convolution在通道域上把计算量降低到了极致 如上所示,depthwise convolution 通过省略通道域中的卷积,大大降低了计算量。
Channel shuffle是一种操作(层),它改变 ShuffleNet 中使用的通道的顺序。这个操作是通过张量reshape和 transpose 来实现的。
假设G表示分组卷积的组数,N表示输入通道的数量,首先将输入通道的维数reshape 为(G, N '),即G*N '=N,然后将(G, N ')转置为(N ', G),最后将其view成与输入相同的形状。pytorch实现如下:
import torch def channel_shuffle(x, groups): batchsize, num_channels, height, width = x.data.size() channels_per_group = num_channels // groups # reshape x = x.view(batchsize, groups, channels_per_group, height, width) # transpose # - contiguous() required if transpose() is used before view(). # See https://github.com/pytorch/pytorch/issues/764 x = torch.transpose(x, 1, 2).contiguous() # flatten x = x.view(batchsize, -1, height, width) return x x = torch.randn(1,10,224,224) x = channel_shuffle(x,2) x.size() # 输出:torch.Size([1, 10, 224, 224])虽然Channel Shuffle的操作不能像计算卷积那样来定义计算量,但应该有一些开销。
G=2时的channel shuffle 情况如下,这里无卷积操作,只是改变了通道的顺序。 打乱的通道数 G=3,情况如下
下面,对于高效的CNN模型,我将直观地说明为什么它们是高效的,以及如何在空间和通道域进行卷积。
ResNet 中使用的带有bottleneck 架构的残差单元是与其他模型进行进一步比较的良好起点。 如上所示,具有bottleneck架构的残差单元由conv1x1、conv3x3、conv1x1组成。第一个conv1x1减小了输入通道的维数,降低了随后的conv3x3的计算量。最后的conv1x1恢复输出通道的维数。
ResNeXt是一个高效的CNN模型,可以看作是ResNet的一个特例,将conv3x3替换为成组的conv3x3。通过使用有效的分组conv,与ResNet相比,conv1x1的通道减少率变得适中(可以让conv1x1不用降维那么多,因为用分组conv已经降低了很多计算量了),从而在相同的计算代价下获得更好的精度。
MobileNet是一个深度可分离卷积模块的堆叠,由depthwise conv和conv1x1 (pointwise conv)组成。 深度可分离卷积分别在空间域和通道域独立执行卷积。这种卷积分解显著降低了计算量,从H*W*N*K*K*M 降低到 H*W*N*K*K(depthwise) + H*W*N*M(conv1x1)=HWN(K² + M)。一般情况下,输出通道M远远大于卷积核大小K(如K=3和M≥32),减小率约为1/8-1/9。
这里你可能对于上述计算比较懵,那我们分解一下吧。
假设 H x W 为输出feature map的空间大小,N为输入通道数,K x K为卷积核的大小,M为输出通道数,则标准卷积的计算量为 H*W*N*K*K*M。depthwise conv可以看做分组卷积的一种特殊情况,其中输入和输出通道数相同,分组数G等于通道数,其计算量为H*W*N*K*K*N/N=H*W*N*K*K。conv1x1的输入通道数是N,输出通道数是M,计算量为H*W*N*M。ShuffleNet的动机是如上所述,conv1x1是在空间域上已经把计算量降低到了极致,所以在空间域上已经没有改进的空间,而分组conv1x1可以在通道域上再次降低计算量。 上图说明了用于ShuffleNet的模块。这里重要的使用的组成部分(building blocks)是channel shuffle层,它在分组卷积中对通多在组间的顺序进行“shuffles”。如果没有channel shuffle,分组卷积的输出在组之间就不会被利用,导致精度下降。
MobileNet-v2采用类似ResNet中带有bottleneck架构残差单元的模块架构;并用深度卷积(depthwise convolution)代替conv3x3,是残差单元的改进版本。 从上面可以看到,与标准的 bottleneck 架构相反,第一个conv1x1增加了通道维度,然后执行depthwise conv,最后一个conv1x1减少了通道维度。
通过如上所述对组成部分(building blocks) 进行重新排序,将其与MobileNet-v1(Depthwise Separable Conv)进行比较,我们可以看到这个体系结构(MobileNet-v2)是如何工作的 (这种重新排序不会改变整个模型体系结构,因为MobileNet-v2是这个模块的堆叠,所以不会有影响的)。
也就是说,上述模块可以看作是深度可分离卷积的一个改进版本,其中可分离卷积中的单个conv1x1被分解为两个conv1x1。让T表示通道维数的扩展因子,两个 conv1x1 的计算量为 2*H*W*N*N/T ,而深度可分离卷积下的conv1x1的计算量为 HWN²。如果T = 6,将 conv1x1 的计算成本降低了3倍(一般为T/2)。
这里可能有人看的不太理解,我们来详细算一下吧(以上图为例子)。
上图中第一个depthwise conv,第二个和第三个为conv1x1,假设第二个Conv1x1的输入通道为N,则第二个Conv1x1输出通道就为N/T,因为第二个Conv1x1是经过扩展因子扩大了T倍,则第三个Conv1x1的输入通道是N/T,则第三个Conv1x1的输出通道是N。
这样就可以算出,第二个Conv1x1的计算量为H*W*N*N/T,第三个Conv1x1的计算量为H*W*(N/T)*N,两个Conv的总计算量为2*H*W*N*N/T,而深度可分离卷积下的conv1x1的计算量为 HWN²,如果T = 6,成本计算成本就是降低了3蓓。
可能有人要问,PW升维不是增加参数量了么,你这么一算咋减小了?是的,用PW升维是增加了一部分参数量,不过正因为是1x1Conv,所以增加的参数量并不多。上面,我们对组成部分(building blocks) 进行重新排序并进行了计算,在高维度下两个conv1x1比低维度下深度可分离卷积下的conv1x1参数可降低了不少呢!
最后,介绍 Fast-Downsampling MobileNet (FD-MobileNet)。在这个模型中,与MobileNet相比,下采样在较早的层中执行。这个简单的技巧可以降低总的计算成本。
从VGGNet开始,许多模型采用相同的下采样策略:执行向下采样,然后将后续层的通道数增加一倍。对于标准卷积,下采样后计算量不变,因为根据定义得 H*W*N*K*K*M 。而对于深度可分离卷积,下采样后其计算量减小:由 HWN(K² + M) 降为 (H/2)*(W/2)* (2N)*(K² + 2M) = HWN(K²/2 + M)。当M不是很大时(即较早的层),这是相对占优势的。注意:这里的2N和2M是因为执行向下采样后,后续层的通道数了增加一倍。
下面是对全文的总结: