Slim-neck by GSConv:自动驾驶车辆检测器架构的更好设计范式
- 摘要
- 引言
- 相关工作
- 本文方法
- GSConv的优势在于轻量级检测器,这些检测器通过添加DSC层和Shuffle来增加非线形表达能力。但是,如果GSConv在模型的所有阶段都使用,模型的网络层会变得更深,这些深层将加重数据流的阻力并显著增加推理时间,当这些特征图到达瓶颈阶段时,他们已经足够细(通道维度达到最大值,宽度和高度维度达到最小值),转换已经变得适度。因此,在瓶颈阶段 仅使用GSConv是更好的选择,使用CSConv处理连接的特征图是合适的:冗余的重复信息较少并且不需要压缩,同时注意力模块的效果更好(SPP或CA)
- Slim-Neck
- 代码
摘要
目标检测是计算机视觉中一项重要的下游任务。对于车载边缘计算平台来说,巨型模型难以实现实时检测要求,而且,由大量深度可分离卷积层构建的轻量级模型无法实现足够的准确率。引入了一种新的轻量级卷积技术 ——GSConv,以减轻模型负担同时保持准确性。GSConv在模型准确性和速度之间达到了出色的权衡。而且,还提供了一种设计范式:Slim-neck,以实现检测器的更高计算成本效益。方法在二十多组对比实验中得到了稳健的验证。特别是,通过我们的方法改进的检测器相比于原始模型得到了最新进的结果。
引言
目标检测是自动驾驶汽车所需的基本感知能力。目前,深度学习的检测算法在该领域占据主导地位。根据检测器阶段的不同,这些算法可以分为一阶段检测器和两阶段检测器。两阶段检测器在检测小目标和获取更高的平均精度方面表现更好,这事通过稀疏检测的原理实现的,但这些检测器的性能需要以速度为代价。一阶段检测器在检测和定位小目标方面,不如两阶段检测器有效,但在工作速度上比两阶段快,这对于工业应用非常重要。但是,随着Transformer在计算机视觉领域的应用,这种情况正在发生变化。从类脑研究的直观理解来看,神经元越多的模型具有更强的非线性表达能力。但不容忽视的是,生物大脑在处理信息方面具有强大的能力和低能耗,这远远超越了计算机。不能简单的通过增加模型参数的数量来构建强大的模型,在当前阶段,采用轻量级设计有效地减轻了高计算成本的负担。这一目的主要通过使用深度可分离卷积操作来实现(DSC)减少参数和浮点运算(FLOPs,乘加法的数量)来实现,其效果显著。然而,
DSC的缺点也很明显:在计算过程中,输入图像的通道信息被分离,下图 a,b展示了标准卷积核DSC的计算过程,这个缺陷导致DSC的特征提取和融合能力远低于SC。
对于自动驾驶汽车来说,速度和准确性同样重要,以前的轻量级工作,如xception、MobileNets和ShuffleNets,通过DSC操作大大提高了检测器的速度,但是,当这些模型应用于自动驾驶汽车时,他们的精度较低时令人担忧的。实际上,这些工作提出了一些方法来缓解DSC固有缺陷(也使其特殊性):
- MobileNets使用大量的1X1密集卷积来融合通道信息,这些信息时独立计算的;
- ShuffleNets使用“通道混洗”来实现通道信息的相互作用;
- GhostNet使用了减半SC操作来保留通道之间的交互信息。
但是:1X1密集卷积需要更多的计算资源,使用“通道混洗”的效果仍然无法与SC相提并论,而GhostNet或多或少又回到了SC的轨道上,影响可能来自各个方面。许多轻量级模型使用类似的思路来设计基本架构:只在深度神经网络的起点到终点使用DSC。然而,无论是用于图像分类还是目标检测,DSC的缺陷在骨干网络中直接放大。我们认为SC和DSC可以配合使用。我们注意到:仅通过对DSC的输出通道进行混洗来生成的特征图仍然是“深度分离的”。
为了使DSC的输出尽可能接近SC,引入了一种新的方法-使用SC、DSC和混洗的组合卷积,命名为GSConv。如下图所示:
我们使用混洗将SC(通道密集卷积操作)生成的信息渗透到DSC生成的每一部分信息中。混洗是一种统一的混合策略。该方法允许来自SC的信息通过统一的方式在不同通道上交换局部特征信息,完全混合到DSC的输出中,而不需要额外的修饰。下图显示了SC、DSC和GSConv的可视化结果。
GSConv的特征图与SC的相似性明显高于DSC与SC的相似性。
在轻量级模型上,仅通过使用GSConv层替换SC层,我们实现了显著的精度提升,在其他模型上,当我们的骨干网络中使用SC,并在neck(slim-neck)中使用GSConv时,模型的精度与原始模型非常接近;如果我们加入一些技巧,模型的精度和速度将超过原始模型。
GSConv结合Slim-Neck方法减小了DSC缺陷对模型的负面影响,并有效利用了DSC的优点。
我们的主要贡献可总结如下: - 我们引入了一种新的轻量级卷积方法GSConv,该方法使卷积计算的输出尽量接近SC的输出,并降低了计算成本
- 我们提出了一种设计范式,即使用Slim- Neck来进行自动驾驶汽车的检测器架构
- 我们验证了在GSConv-Slim-Neck检测器上使用不同常用技巧的有效性
相关工作
通常,基于卷积神经网络的检测器由三部分组成:骨干网络、颈部和头部,骨干网络用于提取输入的特征,颈部将这些特征优化和合并后传递给头部,头部通过颈部的特征进行对对象检测。
对于骨干网络:AlexNet展示了CNN的强大特征提取能力。之后,检测器或分类器的骨干网络开始使用SC进行设计,例如VGG、ResNet和DarkNet。然而,这些**“笨重”**的模型对于边缘计算设备来说是非常不友好的。在模型结构设计方面,研究人员为边缘设备提出了经典的轻量级模型,如MobileNet、ShuffleNet和GhostNet。
在颈部方面:FPN通过独立地对不同尺度的特征图层执行预测操作,提高了对象检测模型的速度和准确率
对于头部:主要的区别在于模型使用基于锚点或者无锚点的方法来完成对象定位任务,前者可能对设计师和用户更可控,但必须使用NMS过滤出具有更高IoU阈值得分的预测框,后者更加灵活,无需手动控制更多参数,但这也导致了模型的不稳定性增加。锚点法或无锚点法的选择并不是本文的重点,但值得注意的是,目前SOTA模型仍然使用锚点法。
此外,注意力机制,如SPP、SE、CBAM、CA可以提高检测器的效率和性能,特别是对于轻量级的检测器,通过适当使用这些模块,可以获得更好的性价比。
本文方法
目标:构建一个简单高效的检测器连接部分,因此要考虑许多因素,包括卷积方法、特征融合结构、计算效率和计算代价效益以及其他许多因素。
为了加快CNN中预测计算的速度,图像经常需要在骨干网络中经历类似的转换过程:空间信息逐步传递到通道中,**每次对特征图进行空间压缩和通道扩展都会导致语义信息的损失。**通道稠密卷积计算最大程度的保留了每个通道之间的隐藏连接,但通道稀疏卷积完全断开了这些连接。GSConv则通过更低的时间复杂度尽可能的保留了这些连接。通常卷积计算的时间复杂度由FLOPs定义。因此,SC(通道稠密卷积)、DSC(通道稀疏卷积)和GSConv的时间复杂度:
下表,比较了五种不同的卷积块对模型性能的贡献:
在GSConv中,我们希望以尽可能简单的方式完成shuffle操作,并且不增加额外的FLOPs消耗。其中一种选择是通过转制操作平均混合特征,然后将他们重构到原始大小,以这种方式不增加额外的FLOPs消耗,但严格来说,在某些设备上不支持。
另一种选择是使用线性操作进行shuffle任务,这种方式计算成本较低,并且所有支持卷积计算的设备都可以支持。下表报告了对这两种shuffle方法性能的消融研究结果。
GSConv的优势在于轻量级检测器,这些检测器通过添加DSC层和Shuffle来增加非线形表达能力。但是,如果GSConv在模型的所有阶段都使用,模型的网络层会变得更深,这些深层将加重数据流的阻力并显著增加推理时间,当这些特征图到达瓶颈阶段时,他们已经足够细(通道维度达到最大值,宽度和高度维度达到最小值),转换已经变得适度。因此,在瓶颈阶段 仅使用GSConv是更好的选择,使用CSConv处理连接的特征图是合适的:冗余的重复信息较少并且不需要压缩,同时注意力模块的效果更好(SPP或CA)
Slim-Neck
我们研究了增强卷积神经网络学习能力的广义方法,如DenseNet、VoVNet和CSPNet,并根据这些方法的理论设计了Slim-neck的结构。我们设计Slim-neck来降低检测器的计算复杂度和推理时间,同时保持准确性。GSConv完成了降低计算复杂度的任务,而降低推理时间和保持准确性的任务需要新的模型
GSConv的计算成本约为SC的50%,但其对模型的学习能力的贡献与后者相当。在GSConv的基础上,我们进一步引入了GS瓶颈,下图展示了GS瓶颈模块的结构。
上图b、c、d分别展示了VoV GSCSP的三种设计方案。其中b简单直观、推理速度更快,而c、d对特征的重用率特别高,事实上,由于邮件友好性,更简单的结构模块更容易被使用。下表报告了三种结构的消融结果实验,b显示出更高的性价比,最后,我们需要有灵性的使用这四个模块。
代码
from conv import autopad
# ---------------------------GSConv Begin---------------------------
class Conv_Mish(nn.Module):
# Standard convolution
def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True): # ch_in, ch_out, kernel, stride, padding, groups
super().__init__()
self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p), groups=g, bias=False)
self.bn = nn.BatchNorm2d(c2)
self.act = nn.Mish() if act else nn.Identity()
def forward(self, x):
return self.act(self.bn(self.conv(x)))
def forward_fuse(self, x):
return self.act(self.conv(x))
class GSConv(nn.Module):
# GSConv https://github.com/AlanLi1997/slim-neck-by-gsconv
def __init__(self, c1, c2, k=1, s=1, g=1, act=True):
super().__init__()
c_ = c2 // 2
self.cv1 = Conv_Mish(c1, c_, k, s, None, g, act)
self.cv2 = Conv_Mish(c_, c_, 5, 1, None, c_, act)
def forward(self, x):
x1 = self.cv1(x)
x2 = torch.cat((x1, self.cv2(x1)), 1)
# shuffle
# y = x2.reshape(x2.shape[0], 2, x2.shape[1] // 2, x2.shape[2], x2.shape[3])
# y = y.permute(0, 2, 1, 3, 4)
# return y.reshape(y.shape[0], -1, y.shape[3], y.shape[4])
b, n, h, w = x2.data.size()
b_n = b * n // 2
y = x2.reshape(b_n, 2, h * w)
y = y.permute(1, 0, 2)
y = y.reshape(2, -1, n // 2, h, w)
return torch.cat((y[0], y[1]), 1)
class GSConvns(GSConv):
# GSConv with a normative-shuffle https://github.com/AlanLi1997/slim-neck-by-gsconv
def __init__(self, c1, c2, k=1, s=1, g=1, act=True):
super().__init__(c1, c2, k=1, s=1, g=1, act=True)
c_ = c2 // 2
self.shuf = nn.Conv2d(c_ * 2, c2, 1, 1, 0, bias=False)
def forward(self, x):
x1 = self.cv1(x)
x2 = torch.cat((x1, self.cv2(x1)), 1)
# normative-shuffle, TRT supported
return nn.ReLU(self.shuf(x2))
class GSBottleneck(nn.Module):
# GS Bottleneck https://github.com/AlanLi1997/slim-neck-by-gsconv
def __init__(self, c1, c2, k=3, s=1, e=0.5):
super().__init__()
c_ = int(c2 * e)
# for lighting
self.conv_lighting = nn.Sequential(
GSConv(c1, c_, 1, 1),
GSConv(c_, c2, 3, 1, act=False))
self.shortcut = Conv_Mish(c1, c2, 1, 1, act=False)
def forward(self, x):
return self.conv_lighting(x) + self.shortcut(x)
class VoVGSCSP(nn.Module):
# VoVGSCSP module with GSBottleneck
def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):
super().__init__()
c_ = int(c2 * e) # hidden channels
self.cv1 = Conv_Mish(c1, c_, 1, 1)
self.cv2 = Conv_Mish(c1, c_, 1, 1)
# self.gc1 = GSConv(c_, c_, 1, 1)
# self.gc2 = GSConv(c_, c_, 1, 1)
# self.gsb = GSBottleneck(c_, c_, 1, 1)
self.gsb = nn.Sequential(*(GSBottleneck(c_, c_, e=1.0) for _ in range(n)))
self.res = Conv_Mish(c_, c_, 3, 1, act=False)
self.cv3 = Conv_Mish(2 * c_, c2, 1) #
def forward(self, x):
x1 = self.gsb(self.cv1(x))
y = self.cv2(x)
return self.cv3(torch.cat((y, x1), dim=1))
class GSBottleneckC(GSBottleneck):
# cheap GS Bottleneck https://github.com/AlanLi1997/slim-neck-by-gsconv
def __init__(self, c1, c2, k=3, s=1):
super().__init__(c1, c2, k, s)
self.shortcut = DWConv(c1, c2, k, s, act=False)
class VoVGSCSPC(VoVGSCSP):
# cheap VoVGSCSP module with GSBottleneck
def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):
super().__init__(c1, c2)
c_ = int(c2 * 0.5) # hidden channels
self.gsb = GSBottleneckC(c_, c_, 1, 1)
# ---------------------------GSConv End---------------------------