本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
beginning
今天给小伙伴介绍一个轻量化的卷积神经网络——ShuffleNet V1。ShuffleNet是国内AI四小龙之一(其他三个分别是商汤、依图、云从)的旷视科技提出的最具代表性算法,特别设计用于在计算资源受限的设备上进行高效的图像分类任务。它采用了一些创新的设计来减少模型的计算量和参数数量,同时由于不错的性能,被广泛应用到移动设备和应用上,比如人脸解锁、短视频特效、测体温、重识别等等。废话不多说啦,如果你也对轻量化网络感兴趣,想了解ShuffleNet的独特之处,请跟我一起愉快的学习叭🎈🎈🎈
1.ShuffleNet的两个创新点
之前在介绍知识蒸馏时,我们是要把冗余大模型抽取成轻量化小模型,这其实就对应着轻量化网络其中的一个技术路线(总结的四个路线如下):
- 压缩已经训练好的模型:知识蒸馏,权值量化,剪枝,注意力迁移等
- 直接训练轻量化网络:SqueezeNet、MobileNet、Xception、ShuffleNet等
- 加速卷积运算:低秩分解,im2col+GEMM等
- 硬件部署:TensorRT、Tensorflow-slim、FPGA、集成电路等
其实我们今天要学的ShuffleNet就属于第二个路线——直接设计并训练出一个轻量化的网络。与现有网络模型相比,ShuffleNet呢有两个创新点——分组1×1卷积(Group Point Convolution)和通道重排(Channel Shuffle)。下面就让我们一个个来学习理解吧🌈🌈🌈
1.1分组1×1卷积
咱们先不看分组1×1卷积,先来了解一下分组卷积是怎么回事🧐🧐🧐
标准的卷积是一个卷积核在输入上滑动,输入有多少个通道,卷积核就有多少个通道。就是说,一个卷积核是处理所有通道的,一个卷积核就对应一个二维的feature map(或者说是channel)。把卷积核的权重和输入对应位置的像素相乘求和(即向量的点乘),依次卷积生成一个标量填在输出的对应位置。
如上图所示,输入是三通道,那么卷积核也得是三通道的,一个卷积核对应一个二维的特征图。如果有128个卷积核就会生成128个特征图,我们把这128张“纸”摞起来,就变成最终的卷积结果。把这结果喂到下一层,那么下一层的卷积核就会有128个通道✨✨✨
与标准卷积不同,在分组卷积中,我们让每个卷积核只处理一部分通道,如下图所示。比如说我用三个卷积核,红色的卷积核只处理红色输入的两个通道,绿色只处理绿色输入通道,黄色只处理黄色通道。这时候仍然是两个卷积核通道在这两个输入里面滑动卷积,仍然是一个卷积核生成一个feature map✨✨✨
下面这张图(来自Condensenet这篇论文)可能会更清楚地说明分组卷积。左侧原始的卷积是一个卷积核处理所有通道,这里用了6个卷积核生成了6个feature map,每一个卷积核都是12个通道。现在呢,我把输入的这12个通道分成黄绿黄三组,每个卷积核只处理4个通道。那么红色的卷积核有两个,只处理4个红色通道;绿色的卷积核有两个,只处理4个绿色通道;黄色的卷积核也有两个,只处理4个黄色通道。最后就会生成红绿黄的channel各两个✨✨✨
为什么要分组呢?我们算一下参数数量就明白啦:在常规卷积里,假设卷积核尺寸是3×3,通道数是12,一共有6个卷积核,那么它的参数量是3×3×12×6=648。现在分组卷积的话,卷积核尺寸仍然是3×3,但它的通道数变为原来的三分之一,卷积核个数为2,共有三组,那么参数量为3×3×(12/3)×(6/3)×3=216。这时候小伙伴们就看明白了叭,分成三组的话,它的参数量就降为原来的三分之一,当然计算量也会相应的降低。这就是分组卷积的好处,可以显著地降低参数量和计算量✨✨✨
小伙伴们大开一下脑洞,想象一下,若这时候我们来做个特例——把分组卷积的组数进一步地增多,有多少个输入的channel我们就分为多少个组,也就是说每个卷积核只处理一个输入的channel,那么这个时候就变成了深度可分离卷积Depthwise Convolution。通俗讲,输入是一个薄片,卷积核也是一个薄片,这两个薄片来做卷积,然后把得到的通道摞起来,再用1×1卷积,即先Depthwise,再Pointwise,这其实就是MobileNet的原理,如下图所示(一文了解多种卷积原理,真不戳)😁😁😁
明白了分组卷积之后,咱们再来看分组1×1卷积就简单多了。1×1卷积是非常密集的,且目前算法都没有考虑到1×1卷积的优化加速,但就是这个1×1卷积它需要相当的复杂度和算力,绝大部分计算都消耗在1×1卷积上。针对这个问题,ShuffleNet对1×1卷积做出了优化,即把分组用在了1×1卷积上(如上文所讲)。
1.2通道重排
分组卷积固然好,但是小伙伴们有没有发现其中存在的问题腻🧐🧐🧐(仔细想一想)没戳,就是“近亲繁殖”问题。你可能一头雾水,且听我慢慢道来。
大家看下图中的(a),卷积呢分成了红绿蓝三组,红的跟红的卷积,绿的跟绿的卷积,蓝的跟蓝的卷积,生成了红绿蓝三组,再卷积再生成了三个组……你会发现,红色永远都在跟红色卷积,绿色蓝色也是一样,这三个组之间是没有任何信息交融的,每一个组都是“近亲繁殖”。在大自然中我们都知道,近亲繁殖是不好的,它会丧失掉基因的多样性。基于此,ShuffleNet引入了通道重排(channel shuffle)操作。现在我们把红绿蓝每个“村子”等分成三份,把每个村子的第一份收集起来,作为下一个组;第二份收集起来,作为下一个组;第三份收集起来,作为下一个组。正如图中(b)和(c)所示(b图和c图是等价的)。这样就实现了跨组、跨group的信息交流,从原来的“近亲繁殖”变成了“混血儿”✨✨✨
那在ShuffleNet里面具体是怎么进行通道重排的呢?过程如下图所示。把原来的分成了三个组,每一个组我们等分成四份,每一个圆圈都表示一个或多个channel。首先把它Reshape成g行n列的矩阵,其中,g表示分组卷积的组数(这里红蓝绿是3),n表示等分成几份(这里为4)。然后把这个矩阵进行线性代数中的转置操作,即第一行变成第一列,第二行变成第二列……最后进行Flatten操作,以上就是通道重排的过程。经过Reshape、Transpose和Flatten之后,每一个组里面都包含了原来三个组的信息,所以这可以直接调用pytorch的API来实现,非常高效,它也是可微分可导的,能端到端的训练;并且也没有引入额外的计算量✨✨✨
2.ShuffleNet网络结构
明白了分组卷积和通道重排之后,咱们再来看一看ShuffleNet的基本设计单元。
下图中的(a)是由ResNet中的bottleneck模块改进的,把原来的3×3标准卷积改成了3×3的Depthwise卷积。先1×1卷积降维,然后再3×3Depthwise卷积 ,接着再1×1卷积升维,最后恒等映射来个逐元素相加再ReLU激活✨✨✨
图中的(b)是在ShuffleNet里面,我们把1×1卷积降维和升维都改成了组卷积GConv,可以有效的降低参数量和计算量;为了防止“近亲繁殖”,在降维之后引入了通道重排,升维之后就不再引入了,因为我们发现目前这个效果就挺好的。(b)就是一个标准的ShuffleNet V1模块✨✨✨
如果要下采样的话,就变成了图中的(c)。在featue map的长宽方向要缩减为原来的一半,通道数要加倍,那么就在shortcut那一路引入了一个步长为2的平均池化,把这两路摞起来,就实现通道数加倍了。(a)和(b)都是逐元素的相加求和,而(c)是把两路摞起来,换成了Concat操作。下采样就用(c),不下采样就用(b)✨✨✨
了解了基本单元之后,ShuffleNet网络结构其实就是由若干个这样的基本单元堆叠起来的。不同分组数的ShuffleNet网络结构如下图所示。
首先输入一个224×224的彩色图像,对这个彩色图像先进行一次普通卷积和最大池化,得到的是24个通道,这24个通道就有了Stage2、Stage3和Stage4这三个阶段。这三个阶段用的ShuffleNet模块分别是4、8、4。在每个Stage的第一个模块要用到下采样模块(即c模块),其他的模块都用的普通模块(即b模块),跨Stage的时候会下采样一次,通道数会加倍,然后得到的是一个7×7大小的feature map,最后再用一个全局平均池化(比如把7×7×960变成1×1×960),再接一个1000个神经元的全连接层作为分类,输出logits,对logits进行softmax,得到1000个类别的概率🌟🌟🌟
3.ShuffleNet代码实战
旷视自己开源了一个ShuffleNet代码,其中blocks.py是它的基本模块,network.py是由这些模块堆叠而成的整个ShuffleNet V1的网络,train.py是训练ImageNet图像分类,utils.py是一些常用的函数。小伙伴们可以直接按照它的文档来复现。学完原理之后,咱们一起来看看在代码中是怎么体现的叭🌞🌞🌞
基本模块单元重点代码:
class ShuffleV1Block(nn.Module):
def __init__(self, inp, oup, *, group, first_group, mid_channels, ksize, stride):
super(ShuffleV1Block, self).__init__()
self.stride = stride
assert stride in [1, 2]
self.mid_channels = mid_channels
self.ksize = ksize
pad = ksize // 2
self.pad = pad
self.inp = inp
self.group = group
if stride == 2:
outputs = oup - inp
else:
outputs = oup
branch_main_1 = [
# pw
nn.Conv2d(inp, mid_channels, 1, 1, 0, groups=1 if first_group else group, bias=False),
nn.BatchNorm2d(mid_channels),
nn.ReLU(inplace=True),
# dw
nn.Conv2d(mid_channels, mid_channels, ksize, stride, pad, groups=mid_channels, bias=False),
nn.BatchNorm2d(mid_channels),
]
branch_main_2 = [
# pw-linear
nn.Conv2d(mid_channels, outputs, 1, 1, 0, groups=group, bias=False),
nn.BatchNorm2d(outputs),
]
self.branch_main_1 = nn.Sequential(*branch_main_1)
self.branch_main_2 = nn.Sequential(*branch_main_2)
if stride == 2:
self.branch_proj = nn.AvgPool2d(kernel_size=3, stride=2, padding=1)
def forward(self, old_x):
x = old_x
x_proj = old_x
x = self.branch_main_1(x)
if self.group > 1:
x = self.channel_shuffle(x)
x = self.branch_main_2(x)
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)
def channel_shuffle(self, x):
batchsize, num_channels, height, width = x.data.size()
assert num_channels % self.group == 0
group_channels = num_channels // self.group
x = x.reshape(batchsize, group_channels, self.group, height, width)
x = x.permute(0, 2, 1, 3, 4)
x = x.reshape(batchsize, num_channels, height, width)
return x
代码是用pytorch实现的。在定义的ShuffleV1Block()类中,branch_main_1和branch_main_2依次进行了1×1分组卷积降维、3×3Depthwise卷积、1×1分组卷积升维操作,如果是下采样的话,还要执行下采样-concat拼接操作。在函数channel_shuffle()中定义了通道重排。最后在函数forward()执行这些定义好的模块。代码不难,小伙伴们照着上文给的图和表仔细看看叭🌈🌈🌈
搭建ShuffleNet V1网络结构:
class ShuffleNetV1(nn.Module):
def __init__(self, input_size=224, n_class=1000, model_size='2.0x', group=None):
super(ShuffleNetV1, self).__init__()
print('model size is ', model_size)
assert group is not None
self.stage_repeats = [4, 8, 4]
self.model_size = model_size
if group == 3:
if model_size == '0.5x':
self.stage_out_channels = [-1, 12, 120, 240, 480]
elif model_size == '1.0x':
self.stage_out_channels = [-1, 24, 240, 480, 960]
elif model_size == '1.5x':
self.stage_out_channels = [-1, 24, 360, 720, 1440]
elif model_size == '2.0x':
self.stage_out_channels = [-1, 48, 480, 960, 1920]
else:
raise NotImplementedError
elif group == 8:
if model_size == '0.5x':
self.stage_out_channels = [-1, 16, 192, 384, 768]
elif model_size == '1.0x':
self.stage_out_channels = [-1, 24, 384, 768, 1536]
elif model_size == '1.5x':
self.stage_out_channels = [-1, 24, 576, 1152, 2304]
elif model_size == '2.0x':
self.stage_out_channels = [-1, 48, 768, 1536, 3072]
else:
raise NotImplementedError
# building first layer
input_channel = self.stage_out_channels[1]
self.first_conv = nn.Sequential(
nn.Conv2d(3, input_channel, 3, 2, 1, bias=False),
nn.BatchNorm2d(input_channel),
nn.ReLU(inplace=True),
)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.features = []
for idxstage in range(len(self.stage_repeats)):
numrepeat = self.stage_repeats[idxstage]
output_channel = self.stage_out_channels[idxstage+2]
for i in range(numrepeat):
stride = 2 if i == 0 else 1
first_group = idxstage == 0 and i == 0
self.features.append(ShuffleV1Block(input_channel, output_channel,
group=group, first_group=first_group,
mid_channels=output_channel // 4, ksize=3, stride=stride))
input_channel = output_channel
self.features = nn.Sequential(*self.features)
self.globalpool = nn.AvgPool2d(7)
self.classifier = nn.Sequential(nn.Linear(self.stage_out_channels[-1], n_class, bias=False))
self._initialize_weights()
定义好基本单元之后就可以搭建出整个网络结构了。self.stage_repeats = [4, 8, 4]指的是Stage2为4,Stage3为8,Stage4为4。最基础的分组数group=3,它的各层channel个数是240、480和960。如果group=8的话,就变为了384、768和1536。在定义它的每一个层的代码中,依次进行了普通卷积和池化、遍历每一个stage、遍历每一个Block、全局平均池化、输出分类🌈🌈🌈
ending
看到这里相信盆友们都对轻量化卷积神经网络ShuffleNet有了一个全面深入的了解啦🌴🌴🌴很开心能把学到的知识以文章的形式分享给大家。如果你也觉得我的分享对你有所帮助,please一键三连嗷!!!下期见