**文献信息:**Deformable Convolutional Networks arxiv.org/abs/1703.06211
发表于ICCV 2017,提出了可变形卷积DCN(Deformable ConvNets)
摘要
卷积神经网络(CNN)由于其构建模块固定的几何结构天然地局限于建模几何变换。
为了提高CNN的转换建模能力,作者提出了可变形卷积和可变形RoI池化。两者都基于这样的想法:增加模块中的空间采样位置以及额外的偏移量,并且从目标任务中学习偏移量。并且新的模块可以很方便的替换普通的CNN模块,并且可以通过标准反向传播便易地进行端对端训练。
实验证明了在深度CNN中学习密集空间变换对于复杂的视觉任务(如目标检测和语义分割)是有效的。
Abstract
This week’s report examines Deformable Convolutional Networks (DCN). DCN introduce deformable convolutions and deformable RoI pooling, which add adaptive offsets to the standard grid sampling positions in convolutional and pooling layers. These offsets are learned from the data, allowing the network to capture complex spatial variations. The report explains how deformable convolutions and pooling layers are integrated into CNN and trained end-to-end via standard backpropagation. Experiments on tasks like object detection and semantic segmentation demonstrate significant improvements in performance, highlighting the effectiveness of DCN in handling geometric deformations in visual data.
可变形卷积
CNN本质上局限于建模大量,未知的数据转换。该限制源于CNN模块的固定几何结构:卷积单元在固定位置对输入特征图进行采样;池化层以一个固定的比例降低空间分辨率;一个RoI(感兴趣区域)池化层把RoI分成固定的空间组块等等。
而缺乏处理几何变换的内部机制。这会导致明显的问题。举一个例子,同一CNN层中所有激活单元的感受野大小是相同的。对于在空间位置上编码语义的高级CNN层来说,这是不可取的。由于不同的位置可能对应不同尺度或形变的目标,所以对于具有精细定位的视觉识别来说,例如使用全卷积网络的语义分割,尺度或感受野大小的自适应确定是理想的情况。
可变形卷积将2D偏移添加到标准卷积中的常规网格采样位置上。它可以使采样网格自由形变。
(a)标准卷积的定期采样网格(绿点)。(b)变形的采样位置(深蓝色点)和可变形卷积中增大的偏移量(浅蓝色箭头)。©(d)是(b)的特例,表明可变形卷积推广了尺度、长宽比和旋转的各种变换。
可变形卷积层
定义标准的卷积过程,对输入的2Dfeature map y上的每一个位置
P
0
P_0
P0,进行以下卷积操作
y
(
P
0
)
=
∑
P
n
∈
R
w
(
P
n
)
⋅
x
(
P
0
+
P
n
)
y(P_0)=\sum_{P_n\in R}w(P_n)\cdot x(P_0+P_n)
y(P0)=Pn∈R∑w(Pn)⋅x(P0+Pn)
其中,P_n是卷积核的每一个位置,w是卷积核.
网格R定义了感受野的大小和扩张,如:定义了一个扩张大小为1的3×3卷积核
R
=
{
(
−
1
,
−
1
)
,
(
−
1
,
0
)
,
.
.
.
,
(
0
,
1
)
,
(
1
,
1
)
}
R = \{(−1, −1),(−1, 0), . . . ,(0, 1),(1, 1)\}
R={(−1,−1),(−1,0),...,(0,1),(1,1)}
在标准卷积操作中,对每一个位置
P
0
P_0
P0,对其与它在R中的所有偏移位置(上下左右及对角线)的特征点与卷积核对应的位置进行加权求和,得到新特征图上对应的P_0点。例如
R
[
0
]
=
(
−
1
,
−
1
)
R[0]=(-1,-1)
R[0]=(−1,−1),就是对应
P
0
P_0
P0点的左上角的点。
输出特征映射y上的每个位置
p
0
p_0
p0,我们有
y
(
P
0
)
=
∑
P
n
∈
R
w
(
P
n
)
⋅
x
(
P
0
+
P
n
+
Δ
P
n
)
y(P_0)=\sum_{P_n\in R}w(P_n)\cdot x(P_0+P_n+\Delta P_n)
y(P0)=Pn∈R∑w(Pn)⋅x(P0+Pn+ΔPn)
其中,
{
Δ
P
n
∣
n
=
1
,
.
.
.
,
N
}
\{\Delta P_n|n=1,...,N \}
{ΔPn∣n=1,...,N},
N
=
∣
R
∣
N = |R|
N=∣R∣,对应着图中的offsets的每一个位置。
在可形变卷积的操作中,在原来R的偏移量的基础上又加入了一个二维偏移 Δ P n \Delta P_n ΔPn(x、y轴上的偏移),这个 Δ P n \Delta P_n ΔPn的值对应图1中offsets对应位置的值。
由于offsets要通过学习得到,所以是一个浮点值,因此对应的不是特征图上一个真实的位置,如果直接使用取整函数的话无法反向传播,因此该位置的值是通过计算周围4个真实值的双线性插值得到的。
x
(
p
)
=
∑
q
G
(
q
,
p
)
⋅
x
(
q
)
x(p)=\sum_{q}G(q,p) \cdot x(q)
x(p)=q∑G(q,p)⋅x(q)
其中,
g
(
a
,
b
)
=
m
a
x
(
0
,
1
−
∣
a
−
b
∣
)
g(a,b)=max(0,1-|a-b|)
g(a,b)=max(0,1−∣a−b∣)
对每一个 P 0 P_0 P0, P n P_n Pn有N个值,对应着卷积核的大小, Δ P n \Delta P_n ΔPn同样也有N个值,对应上图中offset field特征图的N个通道,对于输出的特征图(对应下图中的output feature map)上的每个点,可以单独决定他在原图上采样的3x3的特征点的空间位置。
可变形池化
对一个输入特征图x和一个 w × h w \times h w×h的RoI,RoI pooling将RoI分割成一个 k × k k \times k k×k的区域(bin),并输出一个 k × k k \times k k×k的特征图y。对于第(i,j)的区域:
1)标准RoI 池化的操作过程:
y
(
i
,
j
)
=
∑
p
∈
b
i
n
(
i
,
j
)
x
(
P
0
+
P
)
/
n
i
j
y(i,j)=\sum_{p \in bin(i,j)} x(P_0+P)/n_{ij}
y(i,j)=p∈bin(i,j)∑x(P0+P)/nij
2)可形变 RoI 池化的操作过程:
y
(
i
,
j
)
=
∑
p
∈
b
i
n
(
i
,
j
)
x
(
P
0
+
P
+
Δ
P
i
j
)
/
n
i
j
y(i,j)=\sum_{p \in bin(i,j)} x(P_0+P+\Delta P_{ij})/n_{ij}
y(i,j)=p∈bin(i,j)∑x(P0+P+ΔPij)/nij
这个操作过程跟可形变卷积的基本一样。
讲下 Δ P i j \Delta P_{ij} ΔPij的计算。
对输入特征图x先做一次标准的RoI池化,然后通过一个全连接层,输出一个标准的
k
×
k
k \times k
k×k 的offsets
Δ
P
i
j
^
\Delta \hat{P_{ij}}
ΔPij^,然后根据公式:
Δ
P
i
j
=
γ
⋅
Δ
P
i
j
^
⋅
(
w
,
h
)
\Delta P_{ij}=\gamma \cdot \Delta \hat{P_{ij}} \cdot (w,h)
ΔPij=γ⋅ΔPij^⋅(w,h)计算出
Δ
P
i
j
\Delta P_{ij}
ΔPij。其中,
γ
\gamma
γ是一个超参数设置为0.1。
感受野的变化
当可变形卷积叠加时,复合变形的影响是深远的。标准卷积中的感受野和采样位置在顶部特征映射上是固定的(左)。它们在可变形卷积中(右)根据目标的尺寸和形状进行自适应调整。
标准卷积(a)中的固定感受野和可变形卷积(b)中的自适应感受野的图示,使用两层。顶部:顶部特征映射上的两个激活单元,在两个不同尺度和形状的目标上。激活来自3×3滤波器。中间:前一个特征映射上3×3滤波器的采样位置。另外两个激活单元突出显示。底部:前一个特征映射上两个3×3滤波器级别的采样位置。突出显示两组位置,对应于上面突出显示的单元。
每个图像三元组在三级3×3可变形滤波器中显示了三个激活单元(绿色点)分别在背景(左)、小目标(中)和大目标(右)上的采样位置(每张图像中的93=72993=729个红色点)。
结果
四个模型的backbone层都是使用的ResNet-101,且在相同层将标准卷积替换为可形变卷积。@V和@C分别对应VOC 2012和PASCAl VOC数据集。其余三个网络对应的是目标检测任务,使用VOC2007数据集,并使用mAP作为检验标准,@0.5和@0.7分别对应使用0.5和0.7的IoU。
从图中我们可以看出加入可形变卷积后,每个模型的准确率都得到了提升。DeepLab在加入3层可形变卷积后准确率最高,其余网络在加入6层可形变卷积后准确率最高。
实验
https://github.com/4uiiurz1/pytorch-deform-conv-v2
可变形卷积的pytorch模块,modulation=True可以设置为DCNv2版本。
偏移量预测卷积层self.p_conv
,用于生成偏移量。
对调整后的输入特征图进行卷积操作,普通卷积层self.conv
。
使用可变形卷积模块可以很方便的替换原有的CNN模块,直接使用。
import torch
from torch import nn
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 x_offset