大家好,我是灿视,祝大家端午节快乐!
今天我们继续肝一道面试题,我们一起,要在假期偷偷变强哈。
说到轻量级网络,那么肯定要说的就是
网络系列。这不,粉丝前两天面试的时候,就被问到了
的网络。那么我们今天,回顾下其中的考点与重点呀~
看文章前,可以先关注下我们。
专注于分享最优质的计算机视觉面经,持续关注AI在互联网与银行等单位中的工作机会。
系列算法之
是
于
年提出的轻量级深层神经网络。在
竞赛和
竞赛中均表现了比其他移动端先进网络更优越的性能。
主要有以下两个亮点:
来降低
卷积(也即是 1*1 卷积)的计算复杂度。
来改善跨特征通道的信息流动。
先简单介绍一下
(组卷积)的概念,如下图
所示,左边为常见的普通卷积运算,输出的每一个维度的特征都需要输入特征的每一个维度经过计算获得,但这样的计算量会比较大。因而在
、
等网络中采用了
,在输入特征图的通道方向执行分组操作得到最后的输入特征,如图
中左边所示。
只是
的一种特殊形式,特殊的地方在于它的卷积核的核大小为
*
。
图
左边为普通卷积 右边为组卷积
那么为什么
网络要使用逐点组卷积呢?
其实是由于
本身的问题导致的,我们知道使用
的网络有很多,如
,
,
等。
等模型采用了
,这是一种比较特殊的
,此时分组数恰好等于通道数,意味着每个组只有一个特征图。这些网络存在一个很大的弊端是采用了密集的
x
的
,在
模型中
x
基本上占据了
%的乘加运算。那么在
自然而然就提出了
来降低网络中
×
的计算量。
在原论文中作者也给出了
、
和
这三种模型中的一个
的计算量对比,从下面的计算公式可知
的计算量确实是最小的。下图
为
的一个残差块,图
分别为
(将图中
×
的
换成
×
的
)和
的
。
图
的残差块
图
左为
右为
由上可知,虽然
可以降低计算量,但是如果多个组卷积堆叠在一起,会产生一个副作用:某个通道的输出结果,仅来自于一小部分输入通道,这个副作用会导致在组与组之间信息流动的阻塞,以及表达能力的弱化。那么我们如何解决这个问题呢?这用到了本文的第二个创新点—
。
为达到特征之间通信目的,作者提出了
。如图
-
为正常采用组卷积提取出来的特征(相同颜色的通道表示是在同一个
)。图
-
就是采用
思想对
之后的特征图进行“重组”,这样可以保证接下了采用的
其输入来自不同的组,因此信息可以在不同组之间流转。图
-
进一步的展示了这一过程并随机,其实是“均匀地打乱”。在程序上实现
是非常容易的:假定将输入层分为
组,总通道数为
×
,首先将通道那个维度拆分为
两个维度,然后将这两个维度转置变成
,最后重新
成一个维度
×
。仅需要简单的维度操作和转置就可以实现均匀的
。采用
之后就可以充分发挥
的优点,完美的避开其缺点啦。
图
的
代码如下:
def shuffle_channels(x, groups):
"""shuffle channels of a 4-D Tensor"""
batch_size, channels, height, width = x.size()
assert channels % groups == 0
channels_per_group = channels // groups
# split into groups
x = x.view(batch_size, groups, channels_per_group,
height, width)
# transpose 1, 2 axis
x = x.transpose(1, 2).contiguous()
# reshape into orignal
x = x.view(batch_size, channels, height, width)
return x
细节
的基本单元是在一个残差单元的基础上采用上面的设计理念改进而成的。
图
上图
-
所示为一个包含 3 层的残差单元:首先是
x
卷积,然后是
x
的
,这里的
x
卷积是瓶颈层(
),紧接着是
x
卷积,最后是一个
连接,将输入直接加到输出上。
现在,进行如下的改进:将密集的
×
卷积替换成
×
的
,不过在第一个
×
卷积之后增加了一个
操作。另外
×
的
之后没有使用 ReLU 激活函数。需要注意的是,这里的
=
。改进之后如图
-
所示。
对于残差单元,如果
=
时,此时输入与输出
一致可以直接相加,而当
=
时,通道数增加,而特征图大小减小,此时输入与输出不匹配。为了解决这个问题在
中,对原输入采用
=
的
x
,这样得到和输出一样大小的特征图,然后将得到特征图与输出进行连接(
),而不是相加。这样做的目的主要是降低计算量与参数大小,如图
-
所示。
=
和
=
下的
代码如下:
# stride$=1
class ShuffleNetUnitA(nn.Module):
"""ShuffleNet unit for stride=1"""
def __init__(self, in_channels, out_channels, groups=3):
super(ShuffleNetUnitA, self).__init__()
assert in_channels == out_channels
assert out_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)
def forward(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)
return out
# -----------------------------------------------------------
# stride$=1
class ShuffleNetUnitB(nn.Module):
"""ShuffleNet unit for stride=2"""
def __init__(self, in_channels, out_channels, groups=3):
super(ShuffleNetUnitB, self).__init__()
out_channels -= in_channels
assert out_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)
def forward(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))
return out
整体网络结构
图
整体网络结构
的整体网络结构如图
所示。首先是普通的
x
的卷积和
。然后是三个
,每个
都是重复堆积若干
。对于每个
,第一个
的
=
,这样特征图
和
各降低一半,而通道数增加一倍。
后面的
都是
=
,特征图和通道数都保持不变。其中
控制了
中的分组数,分组越多,在相同计算资源下,可以使用更多的通道数,所以
越大时,采用了更多的卷积核。当完成三个
后,采用
将特征图大小降为
×
,最后是输出类别预测值的全连接层。
作者做了大量的对比实验来证明
的优秀性能,这里给出一部分实验结果。
图
给出了采用不同的
值的
在
上的表现结果。可以看到基本上当
越大时,错误率越低,这是因为采用更多的分组后,在相同的计算约束下可以使用更多的通道数,或者说特征图数量增加,网络的特征提取能力增强,网络性能得到提升。注意
是基准模型,而
× 和
× 表示的是在基准模型上将通道数缩小为原来的
和
。
图
不同的
值的
的表现结果
除此之外,作者还对比了不采用
和采用之后的网络性能对比,如下图
所示。可以清楚的看到,采用
之后,网络性能更好,从而证明
的有效性。
图
不采用
和采用之后的网络性能对比
作者也对比了
与
的计算量和精度,如下图
所示。可以看到
不仅计算复杂度更低,而且精度更好。
图
与
的计算量和精度对比
其他一些实验对比结果大家可以阅读原论文获取。原论文链接放在了引用中,大家自提哈。
针对现大多数模型采用的逐点卷积存在的问题,提出了
和
的处理方法,并基于这两个操作提出了一个
,最后在多个竞赛中证明了这个网络的效率和精度。
系列算法之
-
,它是由旷视提出的
升级版本,能够在同等复杂度下,比
-
和
-
更准确。
主要有以下一些亮点:
、
、
、
-
的两种
做了升级
在
-
这篇论文中,作者先是做了两个实验来抛砖引玉,为下文提出的四个高效网络设计指南以及创新点埋下伏笔。
目前衡量模型复杂度的一个通用指标是
,具体指的是
数量。在论文中作者一上来先提出了一个问题:
是不是准确率和
(浮点数计算的数量)就可以代表一个网络模型的性能呢?
为此他们做了如下的实验:比较了分别以
和
为变量下不同平台下各个模型的推理时间。实验结果如下图
所示。
图
从图中我们可以看出:
、在不同平台下同一个模型的推理速度是不同的;
、相同精度或相同
下的模型推理时间是不同的。
作者经过分析后,认为出现这种情况的原因主要有:
。例如:内存访问成本
(
)和并行度(
)。
等。
据此作者提出了
个网络执行效率对比的准则:论文也是按照以下这两个准则,对多种网络(包括
)进行评估。
(1)使用直接度量方式如速度代替 FLOPs。
(2)在同一环境平台上进行评估。
如下图
所示,作者分别在
和
上对
-
,
-
的推理时间进行了测试。从图中可以看出,整个推理时间被分解用于不同的操作。处理器在运算的时候,不光只是进行卷积运算,也在进行其他的运算,特别是在
上,卷积运算只占了运算时间的一半左右。作者将卷积部分认为是
操作。虽然这部分消耗的时间最多,但其他操作包括数据
、
和逐元素操作(
,
等)也占用了相当多的时间。因此,再次确认了模型只使用
指标对实际运行时间的估计是不够准确的。
图
-
,
-
在
和
上的推理时间占比
综上两个实验,作者提出了四个高效网络设计指南
、
、
、
。下面让我们一起来看看这四个东东都是什么吧。
:
的意思为:卷积的输入输出具有相同
的时候,内存消耗是最小的。作者以
×
卷积为例,假设输入和输出的特征通道数分别为
和
,特征图的大小为
×
,则
×
卷积的
为:
对应的 MAC(
)为:
并且我们知道以下均值不等式:
最后整理一下上面的式子可以知道:
那么在相同
即当
不变的时候,只有当
=
的时候,
才能最小。如下图
所示,作者也做了一系列的实验,从图中可看出,只有当
=
的时候,在
或者
下的推理速度是最快的。
图
不同输入和输出下的推理速度
:
意思为:过多的分组卷积操作会增大
,从而使模型速度变慢。和前面同理,
为分组的数量,带
操作的
×
卷积的
为:
其
值为:
可以看出,在
不变时,
越大,
也越大。如下图
所示,作者也做了实验对比,从图可以看出随着
越大,在
和
的推理速度也越慢。
图
不同
值下
和
的推理速度
:
的意思是网络内部分支操作会降低并行度。作者认为,模型中的网络结构太复杂(分支和基本单元过多)会降低网络的并行程度,模型速度越慢。文章用了一个词:
,翻译过来就是分裂的意思,可以简单理解为网络的单元或者支路数量。
为了研究
对模型速度的影响,作者做了第四个实验。如图
所示,
-
-
表示单个卷积层;
-
-
表示一个
中有
个卷积层串行,也就是简单的叠加;
-
-
表示一个
中有
个卷积层并行,类似
的整体设计。图
为测试结果,可以看出在相同
的情况下,单卷积层(
-
)的速度最快。
图
图
不同分支结构下
和
下的推理速度
:
-
-
的意思为:
-
操作不能被忽略。如图
所示,
操作例如数据拷贝虽然
非常低,但是带来的时间消耗还是非常明显的,尤其是在
上。元素操作操作虽然基本上不增加
,但是所带来的时间消耗占比却不可忽视。于是作者做了一个实验,采用的是
的 bottleneck,除去跨层链接
和
之后测试其推理速度如下图
所示,在
和
结构上都获得了接近
% 的提速。
图
和
的推理速度
-
设计
终于到了最后的部分了!这一部分,作者根据之前所提出的设计指南,在
-
的基础上进行修改,得到了
-
。
-
的
升级
作者在遵循 G1-G4 的设计准则的条件下,对于
-
的
进行了改进,如下图
所示为
-
和
的
对比。图
-
和图
-
分别为
-
中
=
和
=
的
,图
-
和图
-
分别为对应的改进版。
图
作者分析了原
-
中违背
-
的一些结构:
1、图
-
中的逐点组卷积增加了
违背了
;
2、图
-
和
-
中过多的分组违背了
;
3、图
-
和
-
中卷积输入和输出不相等违背了
;
4、图
-
中使用了
操作违背了
;
针对于以上问题,作者在
-
中做了以下改进:
1、对于
=
的模块来说,如图
-
,在每个单元的开始,通过
将
特征通道的输入被分为两支,分别为
−
和
个通道。按照准则
,一个分支的结构仍然保持不变,另一个分支由三个卷积组成,为了满足
,令输入和输出通道相同。与
-
不同的是,两个
×
卷积不再是组卷积(
)(遵循
),因为
分割操作已经产生了两个组。
2、卷积之后,把两个分支拼接(
)起来,避免使用
操作(遵循
),从而通道数量保持不变 (遵循
)。然后进行与
-
相同的
操作来保证两个分支间能进行信息交流(将 shuffle 移到了 concat 之后遵循
)。
3、对于
=
的模块来说,如图
-
,使用一个
×
的 DW 卷积和
×
的卷积来替代
当中的平均池化操作。也将原来的
×
组卷积换成了普通的
×
卷积(遵循
)。
如下所示为
版本
-
的结构单元代码:
def channel_shuffle(x: Tensor, groups: int) -> Tensor:
batch_size, num_channels, height, width = x.size()
channels_per_group = num_channels // groups
# reshape
# [batch_size, num_channels, height, width] -> [batch_size, groups, channels_per_group, height, width]
x = x.view(batch_size, groups, channels_per_group, height, width)
x = torch.transpose(x, 1, 2).contiguous()
# flatten
x = x.view(batch_size, -1, height, width)
return x
class InvertedResidual(nn.Module):
def __init__(self, input_c: int, output_c: int, stride: int):
super(InvertedResidual, self).__init__()
if stride not in [1, 2]:
raise ValueError("illegal stride value.")
self.stride = stride
assert output_c % 2 == 0
branch_features = output_c // 2
# 当stride为1时,input_channel应该是branch_features的两倍
# python中 '<<' 是位运算,可理解为计算×2的快速方法
assert (self.stride != 1) or (input_c == branch_features << 1)
if self.stride == 2:
self.branch1 = nn.Sequential(
self.depthwise_conv(input_c, input_c, kernel_s=3, stride=self.stride, padding=1),
nn.BatchNorm2d(input_c),
nn.Conv2d(input_c, branch_features, kernel_size=1, stride=1, padding=0, bias=False),
nn.BatchNorm2d(branch_features),
nn.ReLU(inplace=True)
)
else:
self.branch1 = nn.Sequential()
self.branch2 = nn.Sequential(
nn.Conv2d(input_c if self.stride > 1 else branch_features, branch_features, kernel_size=1,
stride=1, padding=0, bias=False),
nn.BatchNorm2d(branch_features),
nn.ReLU(inplace=True),
self.depthwise_conv(branch_features, branch_features, kernel_s=3, stride=self.stride, padding=1),
nn.BatchNorm2d(branch_features),
nn.Conv2d(branch_features, branch_features, kernel_size=1, stride=1, padding=0, bias=False),
nn.BatchNorm2d(branch_features),
nn.ReLU(inplace=True)
)
@staticmethod
def depthwise_conv(input_c: int,
output_c: int,
kernel_s: int,
stride: int = 1,
padding: int = 0,
bias: bool = False) -> nn.Conv2d:
return nn.Conv2d(in_channels=input_c, out_channels=output_c, kernel_size=kernel_s,
stride=stride, padding=padding, bias=bias, groups=input_c)
def forward(self, x: Tensor) -> Tensor:
if self.stride == 1:
x1, x2 = x.chunk(2, dim=1)
out = torch.cat((x1, self.branch2(x2)), dim=1)
else:
out = torch.cat((self.branch1(x), self.branch2(x)), dim=1)
out = channel_shuffle(out, 2)
return out
-
的整体网络结构
图
-
的整体网络结构
-
的整体网络结构如图 9 所示。基本上延续了和
相似的结构,每个
都包含了若干
=
和
=
的模块,需要主要的是,每经过一个
通道数都会增加一倍的原因是由于
=
的模块(图
-
)两个分支的通道数都等于输入通道,再经过
之后输出通道自然增加两倍。
-
实验结果
图
-
的实验结果
如图
所示为
-
的实验结果,作者比较了
-
和
系列、
、
等网络在
和
下的推理速度和精度。从图中可知,
-
在精度和推理速度上都是更加杰出的。
1、
-
完善了网络性能对比的准则,以
、推理速度和平台这三个因素来综合评价网络的性能。
2、提出了四个高效网络设计指南,并据此设计了
-
中的结构单元。