参考资料:
论文:
ShuffleNet: An Extremely Efficient Convolutional Neural Network for Mobile Devices
博客:
一、前言
在最先进的基础网络结构中,像 Xception
和 ResNeXt
在小网络中效率会降低,因为密集的 1×1卷积
代价很高 ,基于此作者提出了 pointwise group convolution
以减少1×1卷积的计算复杂度,为克制 pointwise group convolution 带来的副作用,提出了 channel shuffle
的操作,用于实现信息在特征通道之间流动 。
ShuffleNet 使用是一种计算效率极高的CNN架构,它是专门为计算能力非常有限的移动设备设计 :通过 逐点分组卷积(Pointwise Group Convolution) 和 通道洗牌(Channel Shuffle) 两种新运算,在保持精度的同时大大降低了计算成本 ;
- ShuffleNet 比最近的 MobileNet 在 ImageNet 分类任务上的 top-1误差更低 (绝对7.8%) ;
- 在基于ARM的移动设备上,ShuffleNet 比 AlexNet 实现了约13倍的实际加速,并保持了相当的精度 ;
二、模型结构
2.1 Depthwise Sparable Convolutions
它的核心思想是将一个完整的卷积运算分解为两步进行,分别为逐深度卷积(Depthwise Convolution)与逐点卷积(Pointwise Convolution)。
(1)常规卷积
假设输入层为一个大小为64×64像素、3通道彩色图片。经过一个包含4个Filter的卷积层,最终输出4个Feature Map,且尺寸与输入层相同。整个过程可以用下图来概括:
此时,卷积层共4个Filter,每个Filter包含了3个Kernel,每个Kernel的大小为3×3。因此卷积层的参数数量可以用如下公式来计算:N_std = 4 × 3 × 3 × 3 = 108
(2)逐深度卷积(Depthwise Convolution)
同样是上述例子,一个大小为64×64像素、3通道彩色图片首先经过第一次卷积运算,不同之处在于此次的卷积完全是在二维平面内进行,且Filter的数量与上一层的Depth相同。所以一个三通道的图像经过运算后生成了3个Feature map,如下图所示。
其中一个Filter只包含一个大小为3×3的Kernel,卷积部分的参数个数计算如下:N_depthwise = 3 × 3 × 3 = 27
Depthwise Convolution完成后的Feature map数量与输入层的depth相同,但是这种运算对输入层的每个channel独立进行卷积运算后就结束了,没有有效的利用不同map在相同空间位置上的信息。因此需要增加另外一步操作来将这些map进行组合生成新的Feature map,即接下来的Pointwise Convolution。
(3)逐点卷积(Pointwise Convolution)
Pointwise Convolution的运算与常规卷积运算非常相似,不同之处在于卷积核的尺寸为 1×1×M × N,M为上一层的depth,N为新生成的Feature map的个数。所以这里的卷积运算会将上一步的map在深度方向上进行加权组合,生成新的Feature map。有几个Filter就有几个Feature map。如下图所示。
由于采用的是1×1卷积的方式,此步中卷积涉及到的参数个数可以计算为:N_pointwise = 1 × 1 × 3 × 4 = 12
2.2 Group Convolution
分组卷积(Group Convolution)
起源于2012年的 AlexNet。由于当时硬件资源的限制,因为作者将Feature Maps分给多个GPU进行处理,最后把多个GPU的结果进行融合。如下图:
标准的2D卷积:步骤如下图所示,输入特征为 (H × W × C) ,然后应用 C’ 个filters(每个filter的大小为 (h × w × c),输入层被转换为大小为 (H’ × W’ × C’) 的输出特征。
分组卷积的表示如下图(下图表示的是被拆分为 2 个filters组的分组卷积) :
- 首先每个filters组,包含 C’/2个 数量的filter, 每个filter 的通道数为传统2D-卷积filter的一半。
- 每个filters组作用于原来 W × H × C 对应通道数的一半,也就是 W × H × C/2。
- 最终每个filters组对应输出输出 C’ / 2 个通道的特征。
- 最后将通道堆叠得到了最终的 C’个通道,实现了和上述标准2D 卷积一样的效果。
分组卷积的优势:
(1)降低参数量:
- 标准2D卷积:w × h × C × C’;
- 分组卷积:w × h × C 2 \frac{C}{2} 2C × C ′ 2 \frac{C'}{2} 2C′ + w × h × C 2 \frac{C}{2} 2C × C ′ 2 \frac{C'}{2} 2C′ = w × h × C × C ′ 2 \frac{C'}{2} 2C′
参数量减少到原来的 1 2 \frac{1}{2} 21!当Group为4的时候,参数量减少到原来的 1 4 \frac{1}{4} 41,所以参数量为原来的 1 g r o u p \frac{1}{group} group1
(2)增加相邻层filter之间的对角相关性:
在某些情况下,分组卷积能带来的模型效果确实要优于标准的2D 卷积,是因为组卷积的方式能够增加相邻层filter之间的对角相关性,而且能够减少训练参数,不容易过拟合,这类似于正则的效果。
2.3 Channel Shuffle
分组卷积
的概念最先于 AlexNet中提出,当时受制于计算机硬件算力的约束,使用分布式GPU进行训练。后来被 ResNeXt 证实了分组卷积的高效实用性。
Depthwise Separable Convolution
深度可分离卷积最先于 Xception提出,并且在 InceptionNet系列得到中推广。最近的一些研究,MobileNet使用深度可分离卷积,在轻量级网络中达到了SOTA的表现。
然而在小型网络中,昂贵的逐点卷积会导致满足复杂度约束的通道数量有限,从而严重的影响精度 。
最直接的解决方案是:采用通道稀疏连接( channel sparse connections ),例如分组卷积
可以大大降低计算成本 。
这样就会出现一个 问题 :某个通道的输出只能来自一小部分输入通道,这样阻止了通道之间的信息流,也就削弱了神经网络表达能力 ;
作者通过 通道洗牌(Channel Shuffle)
允许分组卷积
从不同的组中获取输入数据,从而实现输入通道和输出通道相关联。
先从group操作说起,一般卷积操作中比如输入feature map的数量是N,该卷积层的filter数量是M,那么M个filter中的每一个filter都要和N个feature map的某个区域做卷积,然后相加作为一个卷积的结果。假设你引入group操作,设group为g,那么N个输入feature map就被分成g个group,M个filter就被分成g个group,然后在做卷积操作的时候,第一个group的M/g个filter中的每一个都和第一个group的N/g个输入feature map做卷积得到结果,第二个group同理,直到最后一个group,如Figure1(a)。不同的颜色代表不同的group,图中有三个group。这种操作可以大大减少计算量,因为你每个filter不再是和输入的全部feature map做卷积,而是和一个group的feature map做卷积。
但是如果多个group操作叠加在一起,如Figure1(a)的两个卷积层都有group操作,显然就会产生边界效应,什么意思呢?就是某个输出channel仅仅来自输入channel的一小部分。这样肯定是不行的的,学出来的特征会非常局限。于是就有了channel shuffle来解决这个问题。
再看Figure1(b),在进行GConv2之前,对其输入feature map做一个分配,也就是每个group分成几个subgroup,然后将不同group的subgroup作为GConv2的一个group的输入,使得GConv2的每一个group都能卷积输入的所有group的feature map,这和Figure1(c)的channel shuffle的思想是一样的。
具体方法为:假设有G组,每组有N个通道,将通道Reshape为(G,N)并进行转转置,转置结束后对通道做Flatten操作,这样一来,就可以实现channel shuffle操作了。
2.4 ShuffleNet Unit
基于残差块(residual block)和 通道洗牌(channel shuffle)设计的 ShuffleNet Unit
;
- (a)深度卷积;
- (b)逐点分组卷积;
- (c)逐点分组卷积 ( stride=2 );
第二个
1×1 GConv
的作用是:改变通道维数,实现与旁路的 shutcut 的维度相同 ;图(c)用通道级联
Concat
代替逐元素加法Add
,这样可以很容易地扩大通道尺寸,而只需很少的额外计算开销。
2.5 网络结构
所提出的网络由分成三个阶段ShuffleNet Unit分组而成:
- 在每个阶段中的
第一个块
的步长设置为2
,其余保持一致。 - 下个阶段输出通道
翻倍
。 - 与ResNet一样,对于每个Shuffle Unit的瓶颈结构的通道数量设置为输出通道的
1/4
。 - 在ShuffleNet Unit中,分组数量 g 控制着点卷积连接稀疏性。
三、实验结果
(1)评估Gconv的分组数g
和网络宽度w
对网络的影响:
网络宽度w:1×表示正常,0.5×表示filters数量减少到0.5倍;
分组数g:ShuffleNet中分组数量 g 设置为从1到8。如果分组数量为1,那么ShuffelNet中就不存在分组点卷积了;
结论:
- 当分组数量 g>1时,要比没有点卷积(g=1)的表现要好。
- 小型网络在分组数量较多时,会表现优秀。
- 例如,ShuffleNet 1x 变现最好的是当g=8是,比g=1时,性能提升了1.2%。ShuffleNet0.5x 和ShuffleNet0.25x 分别提升了3.5%和4.4%。
- 在给定计算复杂度限制下,组卷积可以获得更多的特征图通道数量,这个性能方面的提升可能来自于宽度更大的特征图,从而更好的编码信息。此外,小型网络通常包括更"薄"的特征图,所以它获得信息来自于变宽之后的特征图。
- 同时也证实了对于一些网络 ShuffleNet 0.5x,当分组卷积数量增加之后性能会逐渐饱和甚至退化。
- 对于ShuffleNet 0.25x这个小型网络,随着分组数量越来越多,性能是逐渐变好,这意味着更宽的特征图对小型网络有性能促进作;
(2)评估Channel Shuffle
操作:
通道打乱的目的在于能都使多个分组卷积进行跨组之间信息交流,下表是在通道打乱有无情况下的性能对比。
- 在不同的环境变量设置下,channel shuffle提高了模型性能,也就是评价指标错误率是越来越低的。
- 当g=8时,所降低的错误率比没有设置打乱那组提高了一大截。
- 这个实验证实跨组之间的信息交互非常重要。
四、复现
参考:
(1)定义channel_shuffle操作:
- 假设有G组,每组有N个通道,将通道Reshape为(G,N)并进行转转置,转置结束后对通道做Flatten操作,这样一来,就可以实现channel shuffle操作了。
def channel_shuffle(x, groups):
# (B, C, H, W)
batchsize, num_channels, height, width = x.size()
# 每个组有多少个channel
channels_per_group = num_channels // groups
# reshape: b, num_channels, h, w --> b, groups, channels_per_group, h, w
x = x.view(batchsize, groups, channels_per_group, height, width)
# channelshuffle
# torch.transpose(x,dim_1,dim_2)表示将x这个tensor的dim_1和dim_2进行交换
# 而contiguous方法就类似深拷贝,使得上面这些操作不会改变元数据
x = torch.transpose(x, 1, 2).contiguous()
# flatten
x = x.view(batchsize, -1, height, width)
return x
(2)定义ShuffleNet Unit:
- 如果stride==2,此时ShuffleNet Unit输出的维度=输入维度+直连边输出的维度
class shuffleNet_unit(nn.Module):
expansion = 1
def __init__(self, in_channels, out_channels, stride, groups):
super(shuffleNet_unit, self).__init__()
# 与ResNet一样,对于每个Shuffle Unit的瓶颈结构的通道数量设置为输出通道的1/4
mid_channels = out_channels//4
self.stride = stride
if in_channels == 24:
self.groups = 1
else:
self.groups = groups
# 1x1 GConv1
self.GConv1 = nn.Sequential(
nn.Conv2d(in_channels, mid_channels,
kernel_size=1, stride=1, groups=self.groups, bias=False),
nn.BatchNorm2d(mid_channels),
nn.ReLU(inplace=True)
)
# 3x3 DWConv
self.DWConv = nn.Sequential(
nn.Conv2d(mid_channels, mid_channels,
kernel_size=3, stride=self.stride,
padding=1, groups=self.groups, bias=False),
nn.BatchNorm2d(mid_channels)
)
# 1x1 GConv2
self.GConv2 = nn.Sequential(
nn.Conv2d(mid_channels, out_channels,
kernel_size=1, stride=1, groups=self.groups, bias=False),
nn.BatchNorm2d(out_channels)
)
# 如果步长为2,in_channels!=out_channels,此时直连边需要变维
if self.stride == 2:
self.shortcut = nn.Sequential(
nn.AvgPool2d(kernel_size=3, stride=2, padding=1)
)
else:
self.shortcut = nn.Sequential()
def forward(self, x):
out = self.GConv1(x) # GConv1
out = channel_shuffle(out, self.groups) # channel_shuffle
out = self.DWConv(out) # DWConv
out = self.GConv2(out) # GConv2
short = self.shortcut(x) # shortcut
if self.stride == 2:
out = F.relu(torch.cat([out, short], dim=1))
else:
out += short
out = F.relu(out)
return out
(3)主体结构:
class ShuffleNet(nn.Module):
def __init__(self, groups, num_layers, num_channels, num_classes=1000):
super(ShuffleNet, self).__init__()
self.groups = groups
# first layer
self.conv1 = nn.Sequential(
nn.Conv2d(in_channels=3, out_channels=24,
kernel_size=3, stride=2, padding=1, bias=False),
nn.BatchNorm2d(24),
nn.ReLU(inplace=True),
)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
# 迭代shuffleNet_unit
self.stage2 = self.make_layers(24, num_channels[0], num_layers[0], groups)
self.stage3 = self.make_layers(num_channels[0], num_channels[1], num_layers[1], groups)
self.stage4 = self.make_layers(num_channels[1], num_channels[2], num_layers[2], groups)
# last layers
self.globalpool = nn.AvgPool2d(kernel_size=7, stride=1)
self.fc = nn.Linear(num_channels[2], num_classes)
def make_layers(self, in_channels, out_channels, num_layers, groups):
layers = []
# 每个stage的第一个shuffleNet_unit的stride=2,且只重复1次
# 经过这层之后,维度变为out_channels - in_channels + 直连边的维度(in_channels) = out_channels
layers.append(shuffleNet_unit(in_channels, out_channels - in_channels,
stride=2, groups=groups))
# 更新此时的in_channels
in_channels = out_channels
for i in range(num_layers - 1):
layers.append(shuffleNet_unit(in_channels, out_channels,
stride=1, groups=groups))
return nn.Sequential(*layers)
def forward(self, x):
x = self.conv1(x)
x = self.maxpool(x)
x = self.stage2(x)
x = self.stage3(x)
x = self.stage4(x)
x = self.globalpool(x)
x = x.view(x.size(0), -1)
out = self.fc(x)
return out
(4)针对不同的g,设计不同的网络结构
def ShuffleNet_g1(**kwargs):
num_layers = [4, 8, 4]
num_channels = [144, 288, 576]
model = ShuffleNet(1, num_layers, num_channels, **kwargs)
return model
def ShuffleNet_g2(**kwargs):
num_layers = [4, 8, 4]
num_channels = [200, 400, 800]
model = ShuffleNet(2, num_layers, num_channels, **kwargs)
return model
def ShuffleNet_g3(**kwargs):
num_layers = [4, 8, 4]
num_channels = [240, 480, 960]
model = ShuffleNet(3, num_layers, num_channels, **kwargs)
return model
def ShuffleNet_g4(**kwargs):
num_layers = [4, 8, 4]
num_channels = [272, 544, 1088]
model = ShuffleNet(4, num_layers, num_channels, **kwargs)
return model
def ShuffleNet_g8(**kwargs):
num_layers = [4, 8, 4]
num_channels = [384, 768, 1536]
model = ShuffleNet(8, num_layers, num_channels, **kwargs)
return model
def test():
net = ShuffleNet_g1()
#创建模型,部署gpu
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
net.to(device)
summary(net, (3, 224, 224))
if __name__ == '__main__':
test()