文章目录
- 论文详解
- Stacking More Deformable Conv Layers
- Modulated Deformable Modules
- R-CNN Feature Mimicking
- 代码详解
DCNv2 是在DCNv1的基础上的改进版。
理解DCNv2之前,建议先读 《论文及代码详解——可变形卷积(DCNv1)》
论文详解
DCNv2的改进主要包括如下三点
- Stacking More Deformable Conv Layers (使用更多的可变形卷积)
- Modulated Deformable Modules
- R-CNN Feature Mimicking
Stacking More Deformable Conv Layers
原文翻译:
由于观察到可变形卷积网络可以在具有挑战性的基准上有效地模拟几何变换,我们大胆地用它们的可变形的对应层替换更规则的转换层。我们期望通过叠加更多可变形的转换层,可以进一步增强整个网络的几何变换建模能力。
本文在ResNet-50中,将可变形卷积应用于所有的3 × 3卷积层,在阶段conv3、conv4和conv5中。因此,网络中有12层可变形的卷积。相比之下,[8]只使用了三层可变形卷积,都在conv5阶段。在[8]中可以观察到,对于相对简单和小规模的PASCAL VOC基准,当叠加超过三层时,性能达到饱和。此外,在COCO上的误导偏移可视化可能会阻碍在更有挑战性的基准上的进一步探索。在实验中,我们观察到在conv3-conv5阶段利用可变形层实现了COCO上的目标检测的精度和效率之间的最佳权衡。详见5.2节。
个人理解:
改进点1: 使用更多的可变形卷积
- DCNv1:ResNet-50 Conv5里边的3×3的卷积层都使用可变形卷积替换。Aligned RoI pooling 由 Deformable RoI Pooling取代。
- DCNv2:在Conv3, Conv4, Conv5中所有的3×3的卷积层全部被替换掉。
Modulated Deformable Modules
原文翻译:
为了进一步增强可变形卷积神经网络对空间支持区域的控制能力,引入了一种调制机制。利用该方法,可变形卷积神经网络模块不仅可以调节感知输入特征的偏移量,还可以调节来自不同空间位置/ bin的输入特征幅值。在极端情况下,模块可以通过将其特征振幅设置为零来决定不接收来自特定位置/ bin的信号。因此,来自相应空间位置的图像内容将大大减少或不会对模块输出产生影响。因此,调制机制为网络模块提供了另一个自由维度来调整其空间支持区域。
给定 K K K 个采样位置的卷积核,令 w k w_k wk和 p k p_k pk分别表示第K个位置的权值和预先指定的偏移量。例如, K = 9 K=9 K=9 并且 p k ∈ { ( − 1 , − 1 ) , ( − 1 , 0 ) , … , ( 1 , 1 ) } p_k \in\{(-1,-1),(-1,0), \ldots,(1,1)\} pk∈{(−1,−1),(−1,0),…,(1,1)} 定义了一个dilation=1 的 3 × 3 3\times 3 3×3的卷积,让设 x ( p ) x(p) x(p)和 y ( p ) y(p) y(p)分别表示输入特征映射x和输出特征映射y中位置p的特征。调制的可变形卷积可以表示为 :
y ( p ) = ∑ k = 1 K w k ⋅ x ( p + p k + Δ p k ) ⋅ Δ m k y(p)=\sum_{k=1}^K w_k \cdot x\left(p+p_k+\Delta p_k\right) \cdot \Delta m_k y(p)=k=1∑Kwk⋅x(p+pk+Δpk)⋅Δmk
其中 ∆ p k ∆p_k ∆pk和 ∆ m k ∆m_k ∆mk分别是第 k k 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 + p k + ∆ p k ) x(p + p_k +∆p_k) x(p+pk+∆pk)时,采用[8]中的双线性插值。 ∆ p k ∆p_k ∆pk和 ∆ m k ∆m_k ∆mk都是通过在相同的输入特征图 x x 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。增加的偏移和调制学习的转换层的学习速率设置为现有层的0.1倍。
modulated deformable RoIpooling的设计与此类似。给定一个输入RoI, RoIpooling将其划分为K个spatial bins(例如7 × 7)。在每个bin中,应用均匀空间间隔的采样网格(例如2 × 2)。网格上的采样值取平均值,计算bin的输出。设 ∆ p k ∆p_k ∆pk和 ∆ m k ∆m_k ∆mk为第k个bin的可学习偏移和调制标量。输出分块特征 y ( k ) y(k) y(k)计算为 :
y ( k ) = ∑ j = 1 n k x ( p k j + Δ p k ) ⋅ Δ m k / n k y(k)=\sum_{j=1}^{n_k} x\left(p_{k j}+\Delta p_k\right) \cdot \Delta m_k / n_k y(k)=j=1∑nkx(pkj+Δpk)⋅Δmk/nk
其中 P k j P_{kj} Pkj为第 k k k个bin中第 j j j个网格单元的采样位置, n k n_k nk为采样的网格单元数。采用双线性插值得到特征 x ( p k j + ∆ p k ) x(p_{kj} +∆p_k) x(pkj+∆pk)。 ∆ p k ∆p_k ∆pk和 ∆ m k ∆m_k ∆mk的值由输入特征图上的一个兄弟分支产生。
在这个分支中,RoIpooling生成RoI上的特征,然后是1024-D的两个fc层(初始化为标准推导的高斯分布0.01)。在此之上,一个额外的fc层产生了3K通道的输出(权值初始化为0)。前2K通道是归一化的可学习偏移,其中计算与RoI的宽度和高度的元素级乘法,得到 ∆ p k K = 1 K {∆p_k}^K_{K =1} ∆pkK=1K。剩余的K通道通过sigmoid层归一化产生 ∆ m k K = 1 K {∆m_k}^K_{K=1} ∆mkK=1K。增加的偏移学习fc层的学习速率与现有层的学习速率相同。
个人理解
改进点2:添加幅值参数
- 在DCNV1里,Deformable Conv只学习offset:
y ( p ) = ∑ k = 1 K w k ⋅ x ( p + p k + Δ p k ) y(p)=\sum_{k=1}^K w_k \cdot x\left(p+p_k+\Delta p_k\right) y(p)=k=1∑Kwk⋅x(p+pk+Δpk)
- 在DCNV2里,加入了对每个采样点的权重:
y ( p ) = ∑ k = 1 K w k ⋅ x ( p + p k + Δ p k ) ⋅ Δ m k y(p)=\sum_{k=1}^K w_k \cdot x\left(p+p_k+\Delta p_k\right) \cdot \Delta m_k y(p)=k=1∑Kwk⋅x(p+pk+Δpk)⋅Δmk
其中 Δ p k Δp_k Δpk是学到的offset, Δ m k Δm_k Δmk是学到的权重。
为了解决引入了一些无关区域的问题,在DCN v2中我们不只添加每一个采样点的偏移,还添加了一个权重系数,来区分我们引入的区域是否为我们感兴趣的区域,假如这个采样点的区域我们不感兴趣,则把权重学习为0
具体做法:
-
Δ p k Δp_k Δpk和 Δ m k Δm_k Δmk都是通过在相同的输入feature map 上应用的单独卷积层获得的。 该卷积层具有与当前卷积层相同的空间分辨率。 输出为3K通道,其中前2K通道对应于学习的偏移,剩余的K通道进一步馈送到sigmoid层以获得调制量。
-
对于deformable RoIPooling,DCNV2的修改类似:
y ( k ) = ∑ j = 1 n k x ( p k j + Δ p k ) ⋅ Δ m k / n k y(k)=\sum_{j=1}^{n_k} x\left(p_{k j}+\Delta p_k\right) \cdot \Delta m_k / n_k y(k)=j=1∑nkx(pkj+Δpk)⋅Δmk/nk
Δ p k Δp_k Δpk和 Δ m k Δm_k Δmk的值由输入特征图上的分支产生。 在这个分支中,RoIpooling在RoI上生成特征,然后是两个1024-D的fc层。 额外的fc层产生3K通道的输出(权重被初始化为零)。 前2K通道是可学习偏移 Δ p k Δp_k Δpk, 剩余的K个通道由sigmoid层标准化以产生 Δ m k Δm_k Δmk。
R-CNN Feature Mimicking
原文翻译:
如图2所示,对于Regular ConvNets和Deformable ConvNets,每个RoI分类节点的error-bounded saliency region都可以延伸到RoI之外。因此,RoI以外的图像内容可能会影响提取的特征,从而降低最终的目标检测结果。
在[6]中,作者发现冗余上下文是Faster R-CNN检测误差的一个来源。结合其他动机(例如,在分类和边界框回归分支之间共享更少的特征),作者提出将Faster R-CNN和R-CNN的分类分数相结合,得到最终的检测分数。由于R-CNN分类分数是针对从输入的RoI中裁剪出来的图像内容进行的,将它们进行合并有助于缓解上下文冗余问题,提高检测精度。然而,由于fast - rcnn和R-CNN分支在训练和推理中都需要用到,因此该组合系统的速度较慢。
同时,可变形卷积神经网络在调整空间支持区域方面具有很强的能力。特别是对于Deformable ConvNets v2,调制的Deformable RoIpooling模块可以简单地设置bins的调制标量,以排除冗余上下文。然而,我们在第5.3节中的实验表明,即使使用调制的可变形模块,这种表示也不能通过标准的Faster R-CNN训练过程很好地学习。我们怀疑这是因为传统的Faster RCNN训练缺失不能有效地驱动这种表征的学习。需要额外的指导来指导培训。
受最近的特征模拟工作的激励[2,22,28],我们在每个roi的变形更快的R-CNN特征中加入了一个特征模拟损失,以迫使它们与从裁剪图像中提取的R-CNN特征相似。这个辅助训练目标的目的是驱动可变形更快的R-CNN学习更多像R-CNN一样的“聚焦”特征表示。我们注意到,基于图2中可视化的空间支持区域,对于图像背景上的负roi,聚焦的特征表示可能不是最优的。对于背景区域,可能需要考虑更多的上下文信息,以避免产生假阳性检测。因此,特征模拟损失只在与地真物体充分重叠的正roi上实施。
训练可变形的Faster R-CNN的网络架构如图3所示。除了 Faster R-CNN网络,添加了一个额外的R-CNN分支去feature mimicking。给定用于特征模拟的RoI b,裁剪与之对应的图像patch,并将其大小调整为224 × 224像素。在R-CNN分支中,主干网对缩放后的图像patch进行操作,生成14 × 14空间分辨率的特征图。在特征图的上方应用一个(调制的)可变形的RoIpooling层,其中输入的RoI覆盖整个缩放后的图像patch(左上角为(0,0),高度和宽度为224像素)。 然后,应用1024-D的2个fc层,得到输入图像patch的R-CNN特征表示,用 f R C N N ( b ) f_{RCNN}(b) fRCNN(b)表示。采用A (C+1)-way Softmax分类器进行分类,其中C表示前景类别的数量,1表示背景类别。在R-CNN特征表征 f R C N N ( b ) f_{RCNN}(b) fRCNN(b)和Faster R-CNN中的对应特征表征 f F R C N N ( b ) f_{FRCNN}(b) fFRCNN(b)之间实施了feature mimic loss,后者也是1024-D,由Fast R-CNN头部的2 fc层产生。feature mimic loss定义为fRCNN(b)与fFRCNN(b)的余弦相似度,计算为 :
L mimic = ∑ b ∈ Ω [ 1 − cos ( f R C N N ( b ) , f F R C N N ( b ) ) ] L_{\text {mimic }}=\sum_{b \in \Omega}\left[1-\cos \left(f_{\mathrm{RCNN}}(b), f_{\mathrm{FRCNN}}(b)\right)\right] Lmimic =b∈Ω∑[1−cos(fRCNN(b),fFRCNN(b))]
其中Ω表示用于feature mimic training 的采样roi集合。在SGD训练中,给定一个输入图像,将RPN生成的32个positive region proposal 随机采样到Ω中。cross-entropy classification loss在R-CNN分类头上执行,也在Ω中的roi上计算。网络训练是由feature miimc loss和R-CNN分类损失以及Faster R-CNN中原始损失项驱动的。新引入的两个损失项的损失权是原来Faster R-CNN损失项的0.1倍。R-CNN和Faster R-CNN分支中相应模块之间的网络参数共享,包括骨干网、(modulated) deformable RoIpooling 和2个fc头(两个分支的分类头不共享)。在推理阶段,测试图像只使用Faster R-CNN网络,不使用辅助的R-CNN分支。因此,在推理中,R-CNN feature mimicking没有引入额外的计算。
个人理解:
改进点3: 提出了特征模拟方案指导网络培训
- 《Revisiting RCNN: On Awakening the Classification Power of Faster RCNN》发现把R-CNN和Faster RCNN的classification score结合起来可以提升performance,说明R-CNN学到的focus在物体上的feature可以解决redundant context(上下文无关)的问题。但是增加额外的R-CNN会使inference速度变慢很多。
- DCNV2里的解决方法是把R-CNN当做teacher network,让DCNV2的ROIPooling之后的feature去模拟R-CNN的feature。
- 左边的网络为主网络(Faster RCNN),右边的网络为子网络(RCNN)。实现上大致是用主网络训练过程中得到的RoI去裁剪原图,然后将裁剪到的图resize到224×224大小作为子网络的输入,这部分最后提取的特征和主网络输出的1024维特征作为feature mimicking loss的输入,用来约束这2个特征的差异(通过一个余弦相似度计算)
- 同时子网络通过一个分类损失进行监督学习,因为并不需要回归坐标,所以没有回归损失。在inference阶段仅有主网络部分,因此这个操作不会在inference阶段增加计算成本。
- 因为RCNN这个子网络的输入就是RoI在原输入图像上裁剪出来的图像,因此不存在RoI以外区域信息的干扰,这就使得RCNN这个网络训练得到的分类结果更加可靠,以此通过一个损失函数监督主网络Faster RCNN的分类支路训练就能够使网络提取到更多RoI内部特征,而不是自己引入的外部特征。
代码详解
DCNv2的代码就是在DCNv1( DCNv1代码的详细讲解)的基础上加了权重项sigmoid。
此处代码不再详细注释,只标注和V1不同的地方
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):
"""
新增modulation 参数: 是DCNv2中引入的调制标量
"""
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)
# 输出通道是2N
nn.init.constant_(self.p_conv.weight, 0) #权重初始化为0
self.p_conv.register_backward_hook(self._set_lr)
self.modulation = modulation
if modulation: # 如果需要进行调制
# 输出通道是N
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) # 在指定网络层执行完backward()之后调用钩子函数
@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): # x: (b,c,h,w)
offset = self.p_conv(x) # (b,2N,h,w) 学习到的偏移量 2N表示在x轴方向的偏移和在y轴方向的偏移
if self.modulation: # 如果需要调制
m = torch.sigmoid(self.m_conv(x)) # (b,N,h,w) 学习到的N个调制标量
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
# 如果需要调制
if self.modulation: # m: (b,N,h,w)
m = m.contiguous().permute(0, 2, 3, 1) # (b,h,w,N)
m = m.unsqueeze(dim=1) # (b,1,h,w,N)
m = torch.cat([m for _ in range(x_offset.size(1))], dim=1) # (b,c,h,w,N)
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