【论文解读-目标检测】DynamicHead

参考资料

github

介绍

这篇文章主要是为了解决特征采样细粒度丰富度的问题。之前因为FPN的引入,我们能获取到不同层级的特征,也代表着不同级别的细粒度,但是往往一个目标只会分配给最为契合的细粒度特征图上进行采样,例如小物体往往会分配给最底层的特征图,它的细粒度级别更大,而大物体更倾向高层特征图,它细粒度级别更低。

所以该文章想尝试给一个物体采样多种细粒度的特征进行组合,从而提升特征表达能力,如下图所示。
图片说明

如何实现细粒度组合

文章提出细粒度***络的总体架构:
图片说明

蓝色圆圈表示细粒度动态路由器,使用数据相关的空间门控,有条件地选择子区域进行连接。虚线箭头表示一个预定义的网络,用于变换所选子区域的特征。也就是说,根据输入子区域的不同,网络连接将发生改变。所以, 提出的***络可以有更多的参数容量,并保持较低的计算复杂度。

为了实现该效果,我们使用空间稀疏卷积代替传统网络中的常规卷积,减少了空间上的计算量。此外,如下式所示,我们提出了一个新的门控激活函数,以实现完全端到端训练。
图片说明

实验分析

待补充。。。

代码解读

这一篇和DeFCN一样也是基于cvpods的开源框架进行开发的。
通过查看fcos dynamic head源码,我们可以发现,这篇文章对于FCOS的原本思想并没有进行过多的改变,z在forward过程中依旧利用tower思想,唯一不同点在于,这次进去的是所有层的feature,而非每层单独卷积,这也是这篇文章的核心,通过构建DynamicBottleneck,实现对所有尺度的feature进行采样组合。

logits = []
bbox_reg = []
centerness = []

cls_subnets = self.cls_subnet(features)
bbox_subnets = self.bbox_subnet(features)

for level, (cls_subnet, bbox_subnet) in enumerate(zip(cls_subnets, bbox_subnets)):
logits.append(self.cls_score(cls_subnet))
    if self.centerness_on_reg:
         centerness.append(self.centerness(bbox_subnet))
    else:
         centerness.append(self.centerness(cls_subnet))

    bbox_pred = self.scales[level](self.bbox_pred(bbox_subnet))
    if self.norm_reg_targets:
        bbox_reg.append(F.relu(bbox_pred) * self.fpn_strides[level])
    else:
        bbox_reg.append(torch.exp(bbox_pred))
    return logits, bbox_reg, centerness

DynamicBottleneck相较于普通的Bottleneck,新增了gate_activation, gate_activation_kargs两个参数,这两个参数关联到门函数模块,也就是最终实现动态路径的关键。

class DynamicBottleneck(nn.Module):
    def __init__(
        self, 
        in_channels : int,
        out_channels : int,
        kernel_size : int = 1,
        padding : int = 0,
        stride : int = 1,
        num_groups : int = 1,
        norm: str = "GN",
        gate_activation : str = "ReTanH",
        gate_activation_kargs : dict = None 
    ):

如代码所示,门函数模块定义如下:

class SpatialGate(nn.Module):
    def __init__(
        self,
        in_channels : int,
        num_groups : int = 1,
        kernel_size : int = 1,
        padding : int = 0,
        stride : int = 1,
        gate_activation : str = "ReTanH",
        gate_activation_kargs : dict = None,
        get_running_cost : callable = None
    ):
...
# 对于不同的激活方式,门函数模块采用不同的策略来筛选特征
if gate_activation == "ReTanH":
    self.gate_activate = lambda x : torch.tanh(x).clamp(min=0)
elif gate_activation == "Sigmoid":
    self.gate_activate = lambda x : torch.sigmoid(x)
elif gate_activation == "GeReTanH":
    assert "tau" in gate_activation_kargs
    tau = gate_activation_kargs["tau"]
    ttau = math.tanh(tau)
    self.gate_activate = lambda x : ((torch.tanh(x - tau) + ttau) / (1 + ttau)).clamp(min=0)
else:
    raise NotImplementedError()

那具体是如何实现对每个层级的分别采样组合呢,主要是由以下三个内部成员函数所决定:

  • 先来看这个模块的前向如何操作

    def forward(self, data_input, gate_input, masked_func=None):
      gate = self.gate_activate(self.gate_conv(gate_input)) # 对输入的特征进行激活编码
      self.update_running_cost(gate)  # 可选项
      if masked_func is not None: 
          data_input = masked_func(data_input, gate) # 可选项
      data, gate = self.encode(data_input, gate) # 把每层特征解出来
      output, = self.decode(data * gate) # 把每层特征再编码
      return output
  • 其次是encode和decode

    def encode(self, *inputs):
      outputs = [x.view(x.shape[0] * self.num_groups, -1, *x.shape[2:]) for x in inputs]
      return outputs
    def decode(self, *inputs):
      outputs = [x.view(x.shape[0] / self.num_groups, -1, *x.shape[2:]) for x in inputs]
      return outputs
  • 在基础模块中如何体现

    def forward(self, x, gate):
      if self.shortcut is not None:
          shortcut = self.shortcut(x)
      else:
          shortcut = x
    
      if not self.training:
          out = gate(x, x, self.masked_inference)
      else:
          out = self.conv1(x)
          out = self.activation(out)
          out = self.conv2(out)
          out = gate(out, x)
    
      out = self.activation(out + shortcut)
      return out
全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务