【论文解读-目标检测】DynamicHead
参考资料
介绍
这篇文章主要是为了解决特征采样细粒度丰富度的问题。之前因为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