目标检测是计算机视觉中一项重要的下游任务。对于车载边缘计算平台来说,巨型模型难以实现实时检测要求,而且,由大量深度可分离卷积层构建的轻量级模型无法实现足够的准确率。引入了一种新的轻量级卷积技术 ——GSConv,以减轻模型负担同时保持准确性。GSConv在模型准确性和速度之间达到了出色的权衡。而且,还提供了一种设计范式:Slim-neck,以实现检测器的更高计算成本效益。方法在二十多组对比实验中得到了稳健的验证。特别是,通过我们的方法改进的检测器相比于原始模型得到了最新进的结果。
目标检测是自动驾驶汽车所需的基本感知能力。目前,深度学习的检测算法在该领域占据主导地位。根据检测器阶段的不同,这些算法可以分为一阶段检测器和两阶段检测器。两阶段检测器在检测小目标和获取更高的平均精度方面表现更好,这事通过稀疏检测的原理实现的,但这些检测器的性能需要以速度为代价。一阶段检测器在检测和定位小目标方面,不如两阶段检测器有效,但在工作速度上比两阶段快,这对于工业应用非常重要。但是,随着Transformer在计算机视觉领域的应用,这种情况正在发生变化。从类脑研究的直观理解来看,神经元越多的模型具有更强的非线性表达能力。但不容忽视的是,生物大脑在处理信息方面具有强大的能力和低能耗,这远远超越了计算机。不能简单的通过增加模型参数的数量来构建强大的模型,在当前阶段,采用轻量级设计有效地减轻了高计算成本的负担。这一目的主要通过使用深度可分离卷积操作来实现(DSC)减少参数和浮点运算(FLOPs,乘加法的数量)来实现,其效果显著。然而,
DSC的缺点也很明显:在计算过程中,输入图像的通道信息被分离,下图 a,b展示了标准卷积核DSC的计算过程,这个缺陷导致DSC的特征提取和融合能力远低于SC。
对于自动驾驶汽车来说,速度和准确性同样重要,以前的轻量级工作,如xception、MobileNets和ShuffleNets,通过DSC操作大大提高了检测器的速度,但是,当这些模型应用于自动驾驶汽车时,他们的精度较低时令人担忧的。实际上,这些工作提出了一些方法来缓解DSC固有缺陷(也使其特殊性):
通常,基于卷积神经网络的检测器由三部分组成:骨干网络、颈部和头部,骨干网络用于提取输入的特征,颈部将这些特征优化和合并后传递给头部,头部通过颈部的特征进行对对象检测。
对于骨干网络: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方法性能的消融研究结果。
我们研究了增强卷积神经网络学习能力的广义方法,如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---------------------------