论文 https://arxiv.org/abs/1707.01083
目录
Group Convolution
上图左边是普通卷积,右边是组卷积。
假设输入feature map的shape为C×H×W,卷积核数量为N,则输出feature map的通道数也为N。每个卷积核的shape为C×K×K,则N个卷积核的总参数量为N×C×K×K,输入与输出的连接方式如上图左所示。
Group Convolution是对输入feature map进行分组,然后每组分别进行卷积。假设输入尺寸不变,输出通道数量也不变,假设要分成G个groups,则每组输入feature map的通道数量为C/G,每组的输出feature map的通道数为N/G,每个卷积核的shape为C/G×K×K,卷积核的总数仍为N,每组的卷积核数量为N/G,卷积核只与同组的输入进行卷积,则卷积核的总参数量为N×C/G×K×K,总参数量变为原来的1/G,在本例中,C=12,N=6,G=4,输入和输出的连接方式如上图右所示。
Group Convolution的用途
- 减少参数量,分成G组,该层的参数量减少为原来的1/G
- Group Convolution可以看成是structured sparse,每个卷积核的尺寸由C×K×K变成C/G×K×K,可以将其余(C-C/G)×K×K的参数视为0,有时甚至可以在减少参数量的同时获得更好的效果(相当于正则)
- 当分组数量等于输入通道数,卷积核数量也等于输入通道数,即C=G=N时,Group Convolution就变成了Depthwise Convolution,参数量进一步缩减
- 更进一步,当C=G=N,卷积核的大小与输入feature map也相等,即K=H=W时,输出变成了1×1×C的向量,此时称之为Global Depthwise Convolution (GDC),见MobileFaceNet,可以看成是全局加权池化,与Global Average Pooling (GAP)不同的是,GDC给每个位置赋予了可学习的权重(对于已对齐的图像这很有效,比如人脸,中心位置和边界位置的权重自然应该不同)。而GAP是全局取平均,每个位置的权重相同。
pointwise group convolution
作者注意到,尽管在Xception和ResNeXt这类SOTA模型中,在设计block的过程中引入了深度可分离卷积(depthwise separable convolution)和组卷积(group convolution),从而在模型的表达能力和计算代价之间取得了一个比较好的平衡。但是这些模型在设计时都没有充分考虑到1×1卷积(即逐点卷积 pointwise convolution)的影响,而逐点卷积消耗了大量的时间。例如在ResNeXt中,只有3×3卷积层使用了组卷积,结果就是,在ResNeXt的每个残差单元中,逐点卷积占据了93.4%的乘加运算。因此作者将组卷积也引入到1×1卷积层中,提出pointwise group convolution,大大减小了计算量。
channel shuffle
但是,当多个组卷积堆叠在一起时,会产生一个副作用:某个通道的输出结果,仅来自于一小部分输入通道
从上图中可以很明显的看出,当堆叠两个组卷积时,最终某个组的输出仅和同组的输入有关系,这会导致组之间信息流动的阻塞以及模型表达能力的弱化。为了解决这个问题,作者提出了channel shuffle
如上图所示,channel shuffle就是将第一层组卷积得到的特征进行均匀的重组,使得下一层的组卷积中每一组的输入都来自不同的组,保证了组之间信息的流动。
channel shuffle的代码如下所示
def channel_shuffle(x, groups):
"""Channel Shuffle operation.
This function enables cross-group information flow for multiple groups
convolution layers.
Args:
x (Tensor): The input tensor.
groups (int): The number of groups to divide the input tensor
in the channel dimension.
Returns:
Tensor: The output tensor after channel shuffle operation.
"""
batch_size, num_channels, height, width = x.size()
assert (num_channels % groups == 0), ('num_channels should be '
'divisible by groups')
channels_per_group = num_channels // groups
x = x.view(batch_size, groups, channels_per_group, height, width)
x = torch.transpose(x, 1, 2).contiguous()
x = x.view(batch_size, -1, height, width)
return x
ShuffleUnit
ShuffleNet的基本单元是由残差单元改进而来的
如图所示,首先将一开始的1×1卷积改成1×1组卷积并接上channel shuffle层,然后是3×3的深度卷积,注意这里去掉了3×3卷积后的ReLU层,然后再接1×1组卷积,这一层后面没有channel shuffle层。
当stride=1时,shortcut直接进行Add。当stride=2时,将深度卷积层的步长设置为2,原始输入进行stride=2的3×3平均池化,然后两者进行Concat。注意,在论文给出的模型结构中,每个stage的output channel是一定的,因此当stride=2时,最后一个1×1组卷积的输出channel并不是这个stage的output channel,而是output channel - input channel,因此Concat之后得到了通道数为output channel的最终输出。
if self.combine == 'concat':
self.out_channels -= self.in_channels
有一个细节需要注意,如论文中的结构图所示,ReLU是跟在Add和Concat之后的,但是在官方实现中,当stride=1时,ReLU是跟在Add之后。但当stride=2时,是先对图右边最后1×1组卷积+BN的结果进行ReLU操作,然后再与原始输入进行Concat。
if self.stride == 1:
return F.relu(x + x_proj)
elif self.stride == 2:
return torch.cat((self.branch_proj(x_proj), F.relu(x)), 1)
ShuffleNet v1的网络结构
ShuffleNet v1的完整结构如图所示
还有一个细节需要注意,在第一个block也就是Stage2的第一个block中的第一个1×1卷积层不使用组卷积,也就是将conv的group参数设置为1。
参考
Group Convolution分组卷积,以及Depthwise Convolution和Global Depthwise Convolution - shine-lee - 博客园