可变形卷积即DCN(缩写取自Deformable ConvNets)提出于ICCV 2017的paper:Deformable Convolutional Networks
论文paper地址:https://openaccess.thecvf.com/content_ICCV_2017/papers/Dai_Deformable_Convolutional_Networks_ICCV_2017_paper.pdf
codebase地址:(很多框架中都已实现,这里选择以pytorch的为例)https://github.com/4uiiurz1/pytorch-deform-conv-v2/blob/master/deform_conv_v2.py
目录
DCN
文章创新点(贡献):
一、可变形卷积(DCN/DConv)
为什么要用DConv?
可变形卷积怎么实现?
总结:DCN的操作流程(全过程)
实现DCN中的两个问题Q&A
二、可变形RoI池化
MMdetection里的DCN源码解析
DCNv2
DCN
文章创新点(贡献):
本文提出了两个新模块:可变形卷积和可变形RoI池化
新模块可以很容易地取代现有CNN中的普通模块,并且可以通过标准反向传播轻松地进行端到端训练。
DCN目前也出到了v2,值得一提的是,DCN的思维也算一种可学习的自适应模块,跟注意力机制模块BAM/CBAM的思路有点像。
mmdetection里也有相关实现,可轻松移植进自己的项目,DCN对于大多数检测场景尤其是比赛都是有用的★
一、可变形卷积(DCN/DConv)
(a)是普通的卷积操作
(b)、(c)、(d)是可变形卷积(deformable convolution,即DConv)
可变形卷积实际是指标准卷积操作中采样位置增加了一个偏移量offset,这样卷积核就能在训练过程中扩展到很大的范围。(c)(d)是(b)的特例,表明可变形卷积推广了尺度、长宽比和旋转的各种变换。
为什么要用DConv?
卷积单元(卷积核)对输入的特征图在固定的位置进行采样;池化层不断减小着特征图的尺寸;RoI池化层产生空间位置受限的RoI。然而,这样做会产生一些问题,比如,卷积核权重的固定导致同一CNN在处理一张图的不同位置区域的时候感受野尺寸都相同,这对于编码位置信息的深层卷积神经网络是不合理的。因为不同的位置可能对应有不同尺度或者不同形变的物体,这些层需要能够自动调整尺度或者感受野的方法。再比如,目标检测的效果很大程度上依赖于基于特征提取的边界框,这并不是最优的方法,尤其是对于非网格状的物体而言。
左图a是正常卷积,b是可变形卷积。
最上面的图像是在大小不同的物体上的激活单元。
中间层是为了得到顶层激活单元所进行的采样过程(可以看作是一个卷积操作,3*3卷积核对应九个点九个数最后得到上面一个点即一个数值),左图是标准的3x3方阵采样,右图是非标准形状的采样,但是采样的点依然是3x3,符合3*3卷积的广义定义。
最下面一层是为了得到中间层进行的采样区域,同理。
明显发现,可变形卷积在采样时可以更贴近物体的形状和尺寸,更具有鲁棒性,而标准卷积无法做到这一点。
可变形卷积怎么实现?
如上图所示,偏差offset通过一个卷积层conv获得,输入特征图,输出偏差。生成通道维度是2N,其中的2分别对应X和Y这2个2D偏移,N具体是靠卷积核大小计算得到,比如常见的3*3卷积,9个参数那么N=9。
一共有两种卷积核:卷积核和卷积核学习offset对应的卷积层内的卷积核,这两种卷积核通过双线性插值反向传播同时进行参数更新。
这种实现方式相当于于比正常的卷积操作多学习了卷积核的偏移offset。
总的来说整个流程如下图所示,DCN(也有的地方称为DConv)多了右边灰色框里的东西。
总结:DCN的操作流程(全过程)
总的来说,DConv具体操作流程是:
① 我们一开始,和正常的卷积神经网络一样,根据输入的图像,利用传统的卷积核提取特征图。
②我们把得到的特征图作为输入,对特征图再施加一个卷积层,这么做的目的是为了得到可变形卷积的变形的偏移量。(重点)其中,偏移层是2N,因为我们在平面上做平移,需要改变x值和y值两个方向。
③在训练的时候,用于生成输出特征的卷积核和用于生成偏移量的卷积核是同步学习的。其中偏移量的学习是利用插值算法,通过反向传播进行学习。
实现DCN中的两个问题Q&A
Q:
1、如何将可变形卷积变成单独的一个层,而不影响别的层;
2、在前向传播实现可变性卷积中,如何能有效地进行反向传播。
A:
1、在实际操作时,并不是真正地把卷积核进行扩展,而是对卷积前图片的像素重新整合,变相地实现卷积核的扩张。也就是说,实际上变的是每次进行卷积后得到的带偏移值的坐标值,根据这些坐标取像素点,然后双线性差值,得到新feature map,然后作为输出并成为下一层的新输入。
2、在图片像素整合时,需要对像素进行偏移操作,偏移量的生成会产生浮点数类型,而偏移量又必须转换为整形,直接对偏移量取整的话无法进行反向传播,这时采用双线性差值的方式来得到对应的像素。
二、可变形RoI池化
跟可变形卷积的区别就是输入是经过普通RoI池化后的feature map,进入一个全连接层(不是卷积层),得到一个偏移。注意这里的偏移量要归一化,为了匹配RoI的尺寸。
可变形RoI池化可用的场景也不多,也没有什么好借鉴的,因此就不作过多介绍,感兴趣的可以去看论文和源码对应部分深入了解。
MMdetection里的DCN源码解析
这里我们选用mmdetection框架进行DCN的源码分析,注意我用的版本是mmdetection=2.23.0:
GitHub - open-mmlab/mmdetection at v2.23.0
在使用dcn之前首先要明确,dcn一般放在backbone里,哪怕neck或者后面的head里也有一些卷积模块,一般不会用dcn代替普通的卷积。
如果要在某个backbone使用dcn,可以参考configs/dcn或者configs/dcnv2里的官方config来进行修改,这里以configs/dcn/faster_rcnn_r50_fpn_dconv_c3-c5_1x_coco.py为例,config相关部分的代码为
_base_ = '../faster_rcnn/faster_rcnn_r50_fpn_1x_coco.py' model = dict( backbone=dict( dcn=dict(type='DCN', deform_groups=1, fallback_on_stride=False), stage_with_dcn=(False, True, True, True)))
可以看出这个config基本上继承faster_rcnn_r50_fpn_1x_coco.py,即用faster_rcnn+backbone为resnet-50+neck为fpn来给coco数据集train 12个epoch,只是把backbone即resnet-50里的普通conv改成了dcn,其中resnet-50有4个stage,第2-第4个stage都进行了这种修改,第1个stage保留原有的conv。
在resnet的源码中:
for i, num_blocks in enumerate(self.stage_blocks):
stride = strides[i]
dilation = dilations[i]
dcn = self.dcn if self.stage_with_dcn[i] else None
if plugins is not None:
stage_plugins = self.make_stage_plugins(plugins, i)
else:
stage_plugins = None
planes = base_channels * 2**i
res_layer = self.make_res_layer(
block=self.block,
inplanes=self.inplanes,
planes=planes,
num_blocks=num_blocks,
stride=stride,
dilation=dilation,
style=self.style,
avg_down=self.avg_down,
with_cp=with_cp,
conv_cfg=conv_cfg,
norm_cfg=norm_cfg,
dcn=dcn,
plugins=stage_plugins,
init_cfg=block_init_cfg)
这里的res_layer具体实现源码在mmdet/models/utils/res_layer.py,但是这里的dcn这个变量实际上真正起作用的是在block里,比如resnet-50用的block是Bottleneck
self.conv2 = build_conv_layer(
dcn,
planes,
planes,
kernel_size=3,
stride=self.conv2_stride,
padding=dilation,
dilation=dilation,
bias=False)
这里跳转过去会发现实际上是mmcv对torch.nn.Conv2d加了个封装,因此实现还是以pytorch的为准。
具体源码在:
pytorch-deform-conv-v2/deform_conv_v2.py at 529abbbe9b81e852d272220c855255fd631c43c6 · 4uiiurz1/pytorch-deform-conv-v2 · GitHub
class DeformConv2d(nn.Module):
def __init__(self, inc, outc, kernel_size=3, padding=1, stride=1, bias=None, modulation=False):
"""
Args:
modulation (bool, optional): If True, Modulated Defomable Convolution (Deformable ConvNets v2).
"""
super(DeformConv2d, self).__init__()
self.kernel_size = kernel_size
self.padding = padding
self.stride = stride
self.zero_padding = nn.ZeroPad2d(padding)
self.conv = nn.Conv2d(inc, outc, kernel_size=kernel_size, stride=kernel_size, bias=bias)
# 可变形卷积多出来的部分
self.p_conv = nn.Conv2d(inc, 2*kernel_size*kernel_size, kernel_size=3, padding=1, stride=stride)
nn.init.constant_(self.p_conv.weight, 0)
self.p_conv.register_backward_hook(self._set_lr)
self.modulation = modulation
if modulation:
self.m_conv = nn.Conv2d(inc, kernel_size*kernel_size, kernel_size=3, padding=1, stride=stride)
nn.init.constant_(self.m_conv.weight, 0)
self.m_conv.register_backward_hook(self._set_lr)
@staticmethod
def _set_lr(module, grad_input, grad_output):
grad_input = (grad_input[i] * 0.1 for i in range(len(grad_input)))
grad_output = (grad_output[i] * 0.1 for i in range(len(grad_output)))
def forward(self, x):
offset = self.p_conv(x)
if self.modulation:
m = torch.sigmoid(self.m_conv(x))
dtype = offset.data.type()
ks = self.kernel_size
N = offset.size(1) // 2
if self.padding:
x = self.zero_padding(x)
# (b, 2N, h, w)
p = self._get_p(offset, dtype)
# (b, h, w, 2N)
p = p.contiguous().permute(0, 2, 3, 1)
q_lt = p.detach().floor()
q_rb = q_lt + 1
q_lt = torch.cat([torch.clamp(q_lt[..., :N], 0, x.size(2)-1), torch.clamp(q_lt[..., N:], 0, x.size(3)-1)], dim=-1).long()
q_rb = torch.cat([torch.clamp(q_rb[..., :N], 0, x.size(2)-1), torch.clamp(q_rb[..., N:], 0, x.size(3)-1)], dim=-1).long()
q_lb = torch.cat([q_lt[..., :N], q_rb[..., N:]], dim=-1)
q_rt = torch.cat([q_rb[..., :N], q_lt[..., N:]], dim=-1)
# clip p
p = torch.cat([torch.clamp(p[..., :N], 0, x.size(2)-1), torch.clamp(p[..., N:], 0, x.size(3)-1)], dim=-1)
# bilinear kernel (b, h, w, N)
g_lt = (1 + (q_lt[..., :N].type_as(p) - p[..., :N])) * (1 + (q_lt[..., N:].type_as(p) - p[..., N:]))
g_rb = (1 - (q_rb[..., :N].type_as(p) - p[..., :N])) * (1 - (q_rb[..., N:].type_as(p) - p[..., N:]))
g_lb = (1 + (q_lb[..., :N].type_as(p) - p[..., :N])) * (1 - (q_lb[..., N:].type_as(p) - p[..., N:]))
g_rt = (1 - (q_rt[..., :N].type_as(p) - p[..., :N])) * (1 + (q_rt[..., N:].type_as(p) - p[..., N:]))
# (b, c, h, w, N)
x_q_lt = self._get_x_q(x, q_lt, N)
x_q_rb = self._get_x_q(x, q_rb, N)
x_q_lb = self._get_x_q(x, q_lb, N)
x_q_rt = self._get_x_q(x, q_rt, N)
# (b, c, h, w, N)
x_offset = g_lt.unsqueeze(dim=1) * x_q_lt + \
g_rb.unsqueeze(dim=1) * x_q_rb + \
g_lb.unsqueeze(dim=1) * x_q_lb + \
g_rt.unsqueeze(dim=1) * x_q_rt
# modulation
if self.modulation:
m = m.contiguous().permute(0, 2, 3, 1)
m = m.unsqueeze(dim=1)
m = torch.cat([m for _ in range(x_offset.size(1))], dim=1)
x_offset *= m
x_offset = self._reshape_x_offset(x_offset, ks)
out = self.conv(x_offset)
return out
def _get_p_n(self, N, dtype):
p_n_x, p_n_y = torch.meshgrid(
torch.arange(-(self.kernel_size-1)//2, (self.kernel_size-1)//2+1),
torch.arange(-(self.kernel_size-1)//2, (self.kernel_size-1)//2+1))
# (2N, 1)
p_n = torch.cat([torch.flatten(p_n_x), torch.flatten(p_n_y)], 0)
p_n = p_n.view(1, 2*N, 1, 1).type(dtype)
return p_n
def _get_p_0(self, h, w, N, dtype):
p_0_x, p_0_y = torch.meshgrid(
torch.arange(1, h*self.stride+1, self.stride),
torch.arange(1, w*self.stride+1, self.stride))
p_0_x = torch.flatten(p_0_x).view(1, 1, h, w).repeat(1, N, 1, 1)
p_0_y = torch.flatten(p_0_y).view(1, 1, h, w).repeat(1, N, 1, 1)
p_0 = torch.cat([p_0_x, p_0_y], 1).type(dtype)
return p_0
def _get_p(self, offset, dtype):
N, h, w = offset.size(1)//2, offset.size(2), offset.size(3)
# (1, 2N, 1, 1)
p_n = self._get_p_n(N, dtype)
# (1, 2N, h, w)
p_0 = self._get_p_0(h, w, N, dtype)
p = p_0 + p_n + offset
return p
def _get_x_q(self, x, q, N):
b, h, w, _ = q.size()
padded_w = x.size(3)
c = x.size(1)
# (b, c, h*w)
x = x.contiguous().view(b, c, -1)
# (b, h, w, N)
index = q[..., :N]*padded_w + q[..., N:] # offset_x*w + offset_y
# (b, c, h*w*N)
index = index.contiguous().unsqueeze(dim=1).expand(-1, c, -1, -1, -1).contiguous().view(b, c, -1)
x_offset = x.gather(dim=-1, index=index).contiguous().view(b, c, h, w, N)
return x_offset
@staticmethod
def _reshape_x_offset(x_offset, ks):
b, c, h, w, N = x_offset.size()
x_offset = torch.cat([x_offset[..., s:s+ks].contiguous().view(b, c, h, w*ks) for s in range(0, N, ks)], dim=-1)
x_offset = x_offset.contiguous().view(b, c, h*ks, w*ks)
return
DCNv2
2018年原作者团队又对DCN做了改进,做了充分的可视化来分析原版DCN的缺点
paper:https://arxiv.org/abs/1811.11168