文章地址:https://arxiv.org/pdf/1811.11168.pdf
可变形卷积网络的优越性能源于其可以适应物体几何变化的能力。虽然对其神经特征的空间支持比常规convnet更符合对象结构,但这种支持可能会远远超出感兴趣的区域,导致特征受到无关图像内容的影响。为了解决这个问题,本文提出了一种Deformable ConvNet的新形式,通过增强建模能力和更强的训练,提高了其聚焦相关图像区域的能力。通过在网络中更全面地集成可变形卷积,并通过引入一种扩展变形建模范围的调制机制,增强了建模能力。为了有效地利用这种丰富的建模能力,通过一种提出的特征模拟方案来指导网络训练,该方案帮助网络学习反映RCNN特征的对象焦点和分类能力的特征。
在本文中,提出了一种新版本的Deformable ConvNets,称为Deformable ConvNets v2(DCNv2),它具有增强模型的建模能力。建模能力的提高有两种互补形式。第一个是在网络中扩展使用可变形卷积层。为更多卷积层配备偏移学习能力,允许DCNv2在更大范围的功能级别上控制采样。第二种是可变形卷积模块中的调制机制,其中每个样本不仅经历学习的偏移,而且还通过学习的特征振幅进行调制。因此,网络模块能够改变其样本的空间分布和相对影响。
本文在ResNet-50的conv3、conv4和conv5阶段将可变形卷积应用于所有3×3 conv层。因此,网络中有12层可变形卷积。相比之下,在Deformable ConvNets中只使用了三层可变形卷积,并且都在conv5阶段。Deformable ConvNets中观察到,对于相对简单和小规模的PASCAL VOC,当堆叠超过三层时,性能会饱和。此外,COCO上误导性的偏移可视化可能阻碍了对更具挑战性benchmarks的进一步探索。在实验中,作者观察到在conv3-conv5阶段使用可变形层可以在COCO上实现目标检测的精度和效率之间的最佳折衷。
为了进一步增强可变形卷积网络操纵空间支撑区域的能力,本文引入了一种调制机制。有了它,可变形ConvNets模块不仅可以调整感知输入特征的偏移量,还可以调节来自不同空间位置的输入特征振幅。在极端情况下,模块可以通过将其特征振幅设置为零来决定不感知来自特定位置/bin的信号。因此,来自相应空间位置的图像内容将显著减少或不会对模块输出产生影响。因此,调制机制为网络模块提供另一维度的自由来调整其空间支撑区域。
给定K个采样点的卷积核,让 w k w_k wk和 p k p_k pk表示每个采样点的权重和预先指定的偏移量。例如,K=9和 p k ∈ { ( − 1. − 1 ) , ( − 1 , 0 ) , . . . , ( 1 , 1 ) } p_k∈ \{(−1.−1), (−1, 0), . . . , (1,1)\} pk∈{(−1.−1),(−1,0),...,(1,1)}定义了扩张率为1的3×3卷积核。设 x ( p ) x(p) x(p)和 y ( p ) y(p) y(p)分别表示输入特征映射x和输出特征映射y中位置p处的特征。调整后的的可变形卷积可以表示为 :
∆ p k ∆p_k ∆pk和 ∆ m k ∆m_k ∆mk分别是第k个位置的可学习偏移量和调节标量。
调节标量 ∆ m k ∆m_k ∆mk在[0,1]范围内,而 ∆ p k ∆p_k ∆pk是一个范围不受限制的实数。
p + p k + ∆ p k p+p_k+∆p_k p+pk+∆pk是小数的,双线性插值在计算x(p+pk+∆pk)。
x ( p + p k + ∆ p k ) x(p+p_k+∆p_k) x(p+pk+∆pk)是双线性插值的计算方式。 ∆ p k ∆p_k ∆pk和 ∆ m k ∆m_k ∆mk通过应用于相同输入特征映射x上的单个卷积层获得。该卷积层与当前卷积层具有相同的空间分辨率和伸缩性。 输出通道为3K,其中前2K的通道对应于学习的偏移 { ∆ p k } k = 1 K \{∆p_k\}^K_{k=1} {∆pk}k=1K,剩余的K个通道进一步送到sigmoid层以获得调制标量 { ∆ m k } k = 1 K \{∆m_k\}^K_{k=1} {∆mk}k=1K。 这个单独的卷积层中的内核权重被初始化为零。 因此 ∆ p k ∆p_k ∆pk和 ∆ m k ∆m_k ∆mk分别为0和0.5。用于偏移和调制学习的新增conv层的学习速率设置为现有层的0.1倍。
这部分实现的代码如下,假设我们传入的张量大小为
[1,2,5,5] (Batch_size=1,Channel=2,H,W=5):
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)
# 初始化模型的权重为0
nn.init.constant_(self.p_conv.weight, 0)
#将self.p_conv加载到一个可读可写的空间内
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)
# 操作过程与p_conv的方法一样
nn.init.constant_(self.m_conv.weight, 0)
self.m_conv.register_backward_hook(self._set_lr)
# 将学习速率设置为现有层的0.1倍
@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)))
先解释一下self.p_conv的操作:
self.p_conv就是学习到的offset,这个操作得到的是[1,233,5,5]大小的Tensor。通道 2 ∗ 3 ∗ 3 2*3*3 2∗3∗3就是原论文中的2K,2k的含义如下:
先上图。
上图中绿的矩形框就是原图的大小 ( 5 ∗ 5 ) (5*5) (5∗5),蓝色是 3 ∗ 3 3*3 3∗3大小的的卷积核,红圈是经过padding=1操作之后的特征图的大小。当蓝色卷积核在原图上滑动过程第一次就是如上图所示,由于这是可变形卷积。这个卷积核中的8个蓝色色的块加上这个黄色的块即原图上的卷积采样点都要学习一组offset的,一共9组offset。每一组offset都有x偏移和y偏移总共18个offset。同时这18个offset是由卷积采样点中心负责学习,一共有25个卷积采样中心点,因为每次滑动窗口的中心点都是图中绿色小矩形框。故这里得到的是 [ 1 , 2 ∗ 3 ∗ 3 , 5 , 5 ] [1,2*3*3,5,5] [1,2∗3∗3,5,5]大小的Tensor。
变形卷积的前向推理如下:
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)
在前向推理中我们可以看出,首先由self.p_conv(x)学习到18个偏移量,其shape为(1,18,5,5)。后进行判断语句,modulation为是否给这些offset后的像素值都学习一个权重。如果设置的是True.那么这里会学习到一个权重即为原文中的 ∆ m k ∆m_k ∆mk,由self.m_conv(x)获得,其shape为[1,9,5,5]。同时将其通过一个sigmoid变成成一个0-1的正小数。self.padding的含义是是否将其全零填充。默认是padding等于1的。所以此时x的大小不是[5,5]了,而是[7,7]。
调节可变形ROI池化的设计与此类似。给定一个输入RoI,RoI池将其划分为K个空间单元(例如7×7)。 在每个空间内,应用均匀空间间隔的采样网格(例如2×2)。 对网格上的采样值进行平均,以计算bin的输出。 ∆ p k ∆p_k ∆pk和 ∆ m k ∆m_k ∆mk是第k个bin的可学习偏移量和调节标量。输出bin特征y(k)的计算如下:
其中 p k j p_{kj} pkj是第k个bin中第j个网格单元的采样位置, n k n_k nk表示采样网格单元的数量。双线性插值用于获得特征 x ( p k j + ∆ p k ) x(p_{kj}+∆p_k) x(pkj+∆pk)。 ∆ m k ∆m_k ∆mk是第k个位置的调节标量。 ∆ p k ∆p_k ∆pk和 ∆ m k ∆m_k ∆mk的数值由输入特征图上的同级分支生成。 在这个分支中,RoI池化在RoI上生成特征图,然后是两个1024-D的fc层(用标准差0.01的高斯分布初始化)。另一个fc层产生3K通道的输出(权重初始化为零)。 前2K个通道是标准化的可学习偏移量,其中通过RoI宽度和高度的元素乘法以获得 { ∆ p k } k = 1 K \{∆p_k\}^K_{k=1} {∆pk}k=1K。 剩余的K个通道通过一个sigmoid层进行归一化,以产生 { ∆ m k } k = 1 K \{∆m_k\}^K_{k=1} {∆mk}k=1K。 用于偏移学习的添加fc层的学习率与现有层的学习率相同。
前向传播的self._get_p()函数。其实这个函数就是在复现上面的公式。代码如下:
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
p_0就是一个 3 ∗ 3 3*3 3∗3卷积核在输入特征图上进行采样的 3 ∗ 3 3*3 3∗3区域的中心点,可观察到上图中所有的绿小矩形框就是一个p_0,如下图所示。
def _get_p_0(self, h, w, N, dtype):
# 按图片横纵的尺寸来给每一个像素进行坐标赋值横纵坐标分别为(1, h)、(1, w)
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
输出结果为:
p_0 = [[[[1., 1., 1., 1., 1.],
[2., 2., 2., 2., 2.],
[3., 3., 3., 3., 3.],
[4., 4., 4., 4., 4.],
[5., 5., 5., 5., 5.]],
[[1., 1., 1., 1., 1.],
[2., 2., 2., 2., 2.],
[3., 3., 3., 3., 3.],
[4., 4., 4., 4., 4.],
[5., 5., 5., 5., 5.]],
[[1., 1., 1., 1., 1.],
[2., 2., 2., 2., 2.],
[3., 3., 3., 3., 3.],
[4., 4., 4., 4., 4.],
[5., 5., 5., 5., 5.]],
[[1., 1., 1., 1., 1.],
[2., 2., 2., 2., 2.],
[3., 3., 3., 3., 3.],
[4., 4., 4., 4., 4.],
[5., 5., 5., 5., 5.]],
[[1., 1., 1., 1., 1.],
[2., 2., 2., 2., 2.],
[3., 3., 3., 3., 3.],
[4., 4., 4., 4., 4.],
[5., 5., 5., 5., 5.]],
[[1., 1., 1., 1., 1.],
[2., 2., 2., 2., 2.],
[3., 3., 3., 3., 3.],
[4., 4., 4., 4., 4.],
[5., 5., 5., 5., 5.]],
[[1., 1., 1., 1., 1.],
[2., 2., 2., 2., 2.],
[3., 3., 3., 3., 3.],
[4., 4., 4., 4., 4.],
[5., 5., 5., 5., 5.]],
[[1., 1., 1., 1., 1.],
[2., 2., 2., 2., 2.],
[3., 3., 3., 3., 3.],
[4., 4., 4., 4., 4.],
[5., 5., 5., 5., 5.]],
[[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.]],
[[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.]],
[[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.]],
[[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.]],
[[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.]],
[[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.]],
[[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.]],
[[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.]],
[[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.]]]]
shape=(1,18,5,5) ,九个为一组,前面是x,后面是y。将xy一一对应组合起来就是每一个p_0。
p_n是每一个采样中心点的九个值的偏移量。
def _get_p_n(self, N, dtype):
p_n_x, p_n_y = torch.meshgrid(
# 形成p_n的坐标,也就是上图中的蓝色网格[-1, 2)
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
输出的结果为:
p_n = [[[[-1.]],[[-1.]],[[-1.]],[[ 0.]],[[ 0.]],[[ 0.]],[[ 1.]],[[ 1.]],[[ 1.]],
[[-1.]],[[ 0.]],[[ 1.]],[[-1.]],[[ 0.]],[[ 1.]],[[-1.]],[[ 0.]],[[ 1.]]]]
以上输出为shape=(1,18,1,1) 前9个是x偏移,后9个是y偏移。
一一对应为一组。得出的这9个点就是(x-1,y-1),(x-1,y),(x-1,y+1),(x,y-1),(x,y),(x,y+1),(x+1,y-1),(x+1,y),(x+1,y+1)这9个点就是以(x,y)为中心的卷积核采样区域。
如果运算p_0+p_n得到的就是下面这个Tensor
[[[[0., 0., 0., 0., 0.],
[1., 1., 1., 1., 1.],
[2., 2., 2., 2., 2.],
[3., 3., 3., 3., 3.],
[4., 4., 4., 4., 4.]],
[[0., 0., 0., 0., 0.],
[1., 1., 1., 1., 1.],
[2., 2., 2., 2., 2.],
[3., 3., 3., 3., 3.],
[4., 4., 4., 4., 4.]],
[[0., 0., 0., 0., 0.],
[1., 1., 1., 1., 1.],
[2., 2., 2., 2., 2.],
[3., 3., 3., 3., 3.],
[4., 4., 4., 4., 4.]],
[[1., 1., 1., 1., 1.],
[2., 2., 2., 2., 2.],
[3., 3., 3., 3., 3.],
[4., 4., 4., 4., 4.],
[5., 5., 5., 5., 5.]],
[[1., 1., 1., 1., 1.],
[2., 2., 2., 2., 2.],
[3., 3., 3., 3., 3.],
[4., 4., 4., 4., 4.],
[5., 5., 5., 5., 5.]],
[[1., 1., 1., 1., 1.],
[2., 2., 2., 2., 2.],
[3., 3., 3., 3., 3.],
[4., 4., 4., 4., 4.],
[5., 5., 5., 5., 5.]],
[[2., 2., 2., 2., 2.],
[3., 3., 3., 3., 3.],
[4., 4., 4., 4., 4.],
[5., 5., 5., 5., 5.],
[6., 6., 6., 6., 6.]],
[[2., 2., 2., 2., 2.],
[3., 3., 3., 3., 3.],
[4., 4., 4., 4., 4.],
[5., 5., 5., 5., 5.],
[6., 6., 6., 6., 6.]],
[[2., 2., 2., 2., 2.],
[3., 3., 3., 3., 3.],
[4., 4., 4., 4., 4.],
[5., 5., 5., 5., 5.],
[6., 6., 6., 6., 6.]],
[[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.]],
[[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.]],
[[2., 3., 4., 5., 6.],
[2., 3., 4., 5., 6.],
[2., 3., 4., 5., 6.],
[2., 3., 4., 5., 6.],
[2., 3., 4., 5., 6.]],
[[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.]],
[[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.]],
[[2., 3., 4., 5., 6.],
[2., 3., 4., 5., 6.],
[2., 3., 4., 5., 6.],
[2., 3., 4., 5., 6.],
[2., 3., 4., 5., 6.]],
[[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.]],
[[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.]],
[[2., 3., 4., 5., 6.],
[2., 3., 4., 5., 6.],
[2., 3., 4., 5., 6.],
[2., 3., 4., 5., 6.],
[2., 3., 4., 5., 6.]]]]
shape =(1,18,5,5),
[[0., 0., 0., 0., 0.], [[0., 1., 2., 3., 4.],
[1., 1., 1., 1., 1.], [0., 1., 2., 3., 4.],
[2., 2., 2., 2., 2.], [0., 1., 2., 3., 4.],
[3., 3., 3., 3., 3.], [0., 1., 2., 3., 4.],
[4., 4., 4., 4., 4.]] [0., 1., 2., 3., 4.]]
这一组分别是[1,0,5,5]和[1,9,5,5]对应的是x_0,y_0,其坐标分别为
(0,0),(0,1),(0,2)…(4,0),(4,1),(4,2),(4,3),(4,4)这些点。这些点就是每个卷积采样区域的9个点中左上角的点的集合。
最终加上offset。就得出了这里所有卷积采样点的一个偏移。因为offset的偏移量的shape是(1,18,5,5)的Tensor的。这个Tensor的[1,0,5,5]其实是对应所有卷积采样点中左上角坐标点的x偏移,[1,9,5,5]其实是对应着所有卷积采样点中左上角坐标点的y偏移。同理[1,8,5,5]和[1,17,5,5]是对应着右下角坐标点的x,y偏移的。
到此为止,每个卷积采样区域都学习到了一组offset,但这些offset是存在小数的,这些小数是不能直接取整的,举个例子,当(0,0)这个坐标点学习到了一组offset为(0.2,0.3),那么offset后这个坐标点为(0+0.2,0+0.3)=(0.2,0.3)。如果直接取整得出的还是(0,0)。那么就是一个恒等映射即y=x,那么学习的效果就大打折扣。得到了offset后应该用双线性插值来计算该点的特征值。