DyHead: 统一的动态目标检测头
DyHead: 基于注意力机制的目标检测头
论文:https://arxiv.org/pdf/2106.08322.pdf
代码:https://github.com/microsoft/DynamicHead
一、摘要
目标检测中定位和分类相结合的复杂性导致了方法的蓬勃发展。以前的工作试图提高各种目标检测头的性能,但未能提供统一的视图。在本文中,我们提出了一种新的动态头部框架来统一目标检测头部和注意力。该方法通过在尺度感知的特征层、空间感知的空间位置以及任务感知的输出通道内连贯地结合多头self-attention注意机制,显著提高了目标检测头的表示能力,而无需任何计算开销。进一步的实验证明了所提出的动态头在COCO基准上的有效性和效率。借助标准的ResNeXt-101-DCN主干网,我们在很大程度上提高了与流行对象检测器相比的性能,并实现了最先进的性能54.0 AP。此外,利用最新的transformer主干和额外数据,我们可以将当前最佳COCO结果推至60.6 AP的新纪录!
二、论文图片
DyHead的架构设计
首先这个图第一眼啥都看不出来,唯一能确定的是模型是把尺度注意力、空间注意力、任务注意力叠加了,一些细节好像也看不出什么,以下是对图1的解读:
- 首先是一般视图,这里的\(L,S,C\)分别标识了不同尺度(不同layer或者stage的特征图)、空间位置信息(即\(S=H \times W\))、通道信息;
- \(\pi _{L}\)是Scale-aware注意力、\(\pi _{S}\)是空间注意力、\(\pi _{C}\)是 信道注意力;
- DyHead就是三种注意力的叠加,注意三种注意力的堆叠只是一个block,真实的head是块的叠加。
DyHead注意力块
上面左边的图是块的示意图,右边是整个head的示意图,这里我们不再对右图进行说明,重点看左图:
-
\(\pi _{L}\)是Scale-aware注意力:全局池化 + \(1 \times 1\)卷积 + ReLU激活 + hard sigmoid激活
AdaptiveAvgPool2d负责在\(H \times W\)上面进行全局池化,得到特征图的最大值;
卷积负责将所有通道整合到一起,形成\(L\)个数,后面两个是激活!
-
\(\pi _{S}\)是空间注意力:形变卷积V2(offset偏移量 + 特征幅值调制)
-
\(\pi _{C}\)是信道注意力:两层全连接神经网络进行信道建模
-
注意每个注意力分支和shortcut分支链接的标记都不一样!(是不是意味着分支合并时处理会不一样?)
这里的注意力块我们很好理解,但是你在看源码的时候可能就懵逼了这咋和论文不一样呢?为了降低大家阅读的障碍,这里我绘制了如下的示意图进行说明:(为了简单,我将5个stage转换为3个)
这里叠加注意力的顺序和论文还是有区别的,先做空间感知,接尺度感知,再接任务感知!
DyHead尺度感知模块的有效性
这里图例告诉我们是5个level,猜测是\(P_{2},P_{3},\cdots,P_{6}\)(分辨率从大到小)这五个stage所做的尺度注意力(已经验证)!这里横坐标标识的是比率,高分辨率学习权重除以低分辨率学习权重的比率,纵坐标是数量!这里层级和比例没有明显的顺序关系,为了更好地理解作者的意图,我们先解释作者的实现方式:
- DyHead的尺度感知注意力并不是在所有layer上进行做的!因为特征图的尺度从\(P_{2},P_{3},\cdots,P_{6}\)差距很大,所以作者分6次进行计算,每次取一个窗口为3的特征张量序列进行尺度感知注意力计算。如取\(P_{2},P_{3}\)、 \(P_{2},P_{3},P_{4}\)、 \(P_{3},P_{4},P_{5}\)、 ……、\(P_{4},P_{5},P_{6}\)、\(P_{5},P_{6}\)。
高分辨率权重除以低分辨率权重,这个值超过1说明高分辨率的特征配了更高的权值,反之低分辨率的特征配了更高的权值!根据作者所说图中紫色是高分辨率,那么level5就应该是\(P_{2}\),level1就应该是\(P_{6}\)!所以我们有如下解读:
- 紫色部分是高分辨率特征图的权值比率,小于1,说明模型将更多注意力放到了比它语义稍高的特征图上;蓝色部分是将较高分辨率部分特征图的权值配的大一些,以为低分辨率特征图引入更多低级语义;
- 中间三种在特征图间的配全差异并不大,起到了融合不同层级特征的作用!
这个图也向我们展示了尺度感知是工作良好的!
DyBlock块不同数量下的注意力分布
上图很明显地展示了设置的块越多,模型就越关注实例区域!这意味着空间感知注意力是工作良好的!这里我们可以反思为什么会越来越好?这和我们使用Deformable卷积有直接关系,形变卷积本身就会自适应物体的形状,但是简单的浅层不足以让模型适应那么好,所以足够的块叠加,也即多层卷积后模型就能够适应的很好了!那是不是叠加的越多,模型适应的越好,甚至媲美实例分割?答案当然是否定的,因为随着深度进一步增加,模型的冗余就会占据主导地位,本身就难以训练,更高的容量也就容易使模型学到一些非必要或者说不重要的特征,导致模型的波动进行性能降低。作者在实验中也证实了6个块是最好的配置,实际使用中我们可以以6的超参数进行上下调整,以获取更好的性能(这里主要还是要注意多分析自己的数据特点)。
三、论文表格
DyHead三种注意力模型消融
这里可以看出:
- 单个注意力时,空间注意力是在AP上表现更好,这也说明了图像数据在空间维度上的注意力是很重要的!
- 两个注意力时,有空间注意力的两种情况都要好一些;
- 三者都加时,性能提升很大!这里我们不知道基本模型是什么,但是足足提升了3.6的AP,这项工作的价值可见一斑!
根据后面的介绍,作者很可能使用的是ATSS模型(这个模型主要是做样本采样均衡的!)
DyBlock块的数量
这里选择6是最优解,为什么会这样其实前面我们已经说过了,建议回顾图4的讲解!
不同模型使用DyHead的性能
这里面除最后一个模型不熟悉之外,其他都挺了解的!ATSS本身就是RetinaNet --> FCOS --> ATSS的模型进化,所以有这种性能变化就很正常了!写这篇博客时,我正好在借用ATSS技术,由于使用VAN在大核卷积注意力模块使用上,出现一些问题,经过分析模型的头建模性能不太好,所以DyHead是一个非常好的适用技术,最后我将结合VAN+DyHead + ATSS做实验,如果效果很好,我将会在这篇博客介绍其性能表现,如果有任何问题,我也会在最后一章进行表诉。
不同backbone的性能表现
在相同的backbone下面,DyHead的性能是最好的!而且提升不小!
DyHead与SOTA模型的性能比较
这里告诉我们实验DyHead,原模型可能会提分几个点。SpineNet模型是某些指标更好,但是它训练的次数几乎是DyHead的20倍,性能差异还不明显,这些差异我们使用其他技术很容易补上去!
DyHead使用Swin transformer的性能表现
验证集上效果很不错,测试集上性能也非常好!
四、论文结论
在本文中,我们提出了一种新的目标检测头,它将尺度感知、空间感知和任务感知的注意统一在一个框架中。提出了一种新的目标检测头的观点。作为一个插件块,动态头部可以灵活地集成到任何现有的对象检测器框架中,以提高其性能。此外,我们的研究表明,在目标检测头中设计和学习注意力是一个有趣的方向,值得进一步研究。这项工作只是迈出了一步,可以在以下方面进一步改进:如何使全注意模型易于学习和有效计算,以及如何在头部设计中系统地考虑更多的注意方式以获得更好的表现。
提交后,我们不断提高成绩。最近,将transformer作为视觉backbone并展示其良好性能成为一种热门趋势。当使用最新的transformer主干[19],额外的数据和增加的输入大小来训练我们的DyHead时,我们可以在COCO基准上进一步改进当前的SOTA。
五、个人使用实验
待续……
后面的东西忽略……
DyBlock实现
detectron的实现逻辑是用字典输出结果,如:
# resnet的输出为:
outputs={
"stem": x_res1, "res2": x_res2,
"res3": x_res3, "res4": x_res4,
"res5": x_res5, "linear": x_last
}
# FPN的输出:
outputs = {
"p2": p2, "p3": p3, "p4": p4, "p5": p5, "p6": p6
}
我们的DyHead是接FPN这种结构,原作者是基于detectron去做的,所以我们就按照这种数据结构讲解!
class ConvBlock(nn.Module):
"""
基础卷积块的朴素实现
self.BN: 在每个样本的信道独立分成num_groups份进行标准化操作
"""
def __init__(self, in_channels=256, out_channels=256, kernel_size=3, stride=1, padding=1,
spitial_deformable=True, num_groups=16):
if spitial_deformable:
# todo: 添加带调制的形变卷积
pass
else:
self.Conv2d = nn.Conv2d(in_channels=in_channels, out_channels=out_channels,
kernal_size=3, stride=stride, padding=1)
self.BN = nn.GroupNorm(num_groups=16, num_channels=out_channels)
def forward(self, x):
x = self.Conv2d(x)
x = self.BN(x)
return x
class HardSigmoid(nn.Module):
"""
.. math::
\text{ReLU6}(x) = \min(\max(0,x), 6)
"""
def __init__(self, inplace=True, h_max=1):
super(HardSigmoid, self).__init__()
self.ReLU = nn.ReLU6(inplace=inplace)
self.h_max = h_max
def forward(self, x):
return self.ReLU(x + 3) * self.h_max / 6
class DyBlock(nn.Module):
def __init__(self, in_channels=256, out_channels=256, spitial_deformable=True):
super(DyBlock, self).__init__()
self.DyS = nn.Moduleslist()
self.DyS.append(ConvBlock(in_channels=in_channels, out_channels=out_channels,
kernel_size=3, stride=1, padding=1, num_groups=16,
spitial_deformable=spitial_deformable))
self.DyS.append(ConvBlock(in_channels=in_channels, out_channels=out_channels,
kernel_size=3, stride=1, padding=1, num_groups=16,
spitial_deformable=spitial_deformable))
self.DyS.append(ConvBlock(in_channels=in_channels, out_channels=out_channels,
kernel_size=3, stride=2, padding=1, num_groups=16,
spitial_deformable=spitial_deformable))
self.DyL = nn.Modelelist()
self.DyL.append(nn.AdaptiveAvgPool2d(1))
self.DyL.append(nn.Conv2d(in_channels=in_channels, out_channels=1, kernel_size=1))
self.DyL.append(nn.ReLU(inplace=True))