1.DCNv1
可变形卷积顾名思义就是卷积的位置是可变形的,并非在传统的N × N的网格上做卷积,这样的好处就是更准确地提取到我们想要的特征(传统的卷积仅仅只能提取到矩形框的特征) DCN v1的核心思想在于它认为卷积核不应该是一个简简单单的矩形 在不同的阶段,不同的特征图,甚至不同的像素点上都可能有其最优的卷积核结构。 因此DCN v1提出在方形卷积核上的每个点学习一个偏移(offset),卷积核可以根据不同的数据学习不同的卷积核结构,如图1所示。
图1:可变形卷积核。(a)是标准的3×3卷积。(b),( c),(d)是给普通卷积加上偏移之后形成的可变形的卷积核,其中蓝色的是新的卷积点,箭头是位移方向。
可变形卷积的结构可以分为上下两个部分:
- 上面那部分是基于输入的特征图生成x,y方向的offset
- 下面那部分是基于特征图和offset通过可变形卷积获得输出特征图
假设输入的特征图宽高分别为w,h,下面那部分的卷积核尺寸是
k
h
k_h
kh?和
k
w
k_w
kw?,那么上面那部分卷积层的卷积核数量应该是
2
?
k
h
?
k
w
2*k_h*k_w
2?kh??kw?,其中2代表x,y两个方向的offset。
并且,这里输出特征图的维度和输入特征图的维度一样,那么offset的维度就是[batch,
k
h
k_h
kh?,
k
w
k_w
kw?,h,w]
假设下面那部分设置了group参数(代码实现中默认为4),那么第一部分的卷积核数量就是
2
?
k
h
?
k
w
?
g
r
o
u
p
2*k_h*k_w*group
2?kh??kw??group,即每一个group共用一套offset。下面的可变形卷积可以看作先基于上面那部分生成的offset做了一个插值操作,然后再执行普通的卷积。
具体实现的流程大概如下:
-
step1:求输入特征图
U
U
U(维度=[b,h,w,c])每个像素的偏移量:经过一个普通卷积,padding设置为same,对应的输出结果维度是[b,h,w,2c],记作
V
V
V。 -
将输入特征图
U
U
U中的像素索引值与V相加,得到偏移后的position(即在原始图片U中的坐标值),需要将position值限定为图片大小以内。position的大小为(bhw*2c),但position只是一个坐标值,而且还是float类型的,我们需要这些float类型的坐标值获取像素(双线性差值)。
- 举个例子:我们取一个坐标值,将其转换为四个整数floor(a), ceil(a), floor(b), ceil(b)。
- 将这四个整数进行整合,得到四对坐标(floor(a),floor(b)), ((floor(a),ceil(b)), ((ceil(a),floor(b)), ((ceil(a),ceil(b))。
- 这四对坐标每个坐标都对应
U
U
U中的一个像素值,而我们需要得到(a,b)的像素值,这里采用双线性差值的方式计算(一方面是因为获得的像素更精确,另外一方面是因为可以进行反向传播)。
-
在得到position的所有像素后,即得到了输出特征图
M
M
M,将这个新图片M作为输入数据输入到别的层中,如普通卷积。
2.DCNv2
- 增加更多的可变形卷积层
- 除了让模型学习采样点的偏移,还要学习每个采样点的权重,这是对减轻无关因素干扰的最重要的工作
- 使用R-CNN对Faster R-CNN进行知识蒸馏
- 目前只能实现3*3大小的卷积
更多的可变形卷积:
- 在DCN v2将conv3到conv5 block的 3*3卷积全部替换为了可变形卷积,因此可变形卷积层数达到了 12个。这一操作在场景更复杂的COCO数据集有着比较明显的性能提升。
加权采样点偏移:
- DCNv2在DCNv1的基础上增加了一个权重系数
m
m
m,对卷积的在输入特征图上的采样点的偏移量计算权重,去除无关的上下文信息,对于某些不想要的采样点权重可以直接学习成0。
使用R-CNN对Faster R-CNN进行知识蒸馏:
- 在DCN v2中,作者使用了特征模仿(Feature Mimicking)的方式来用R-CNN对Faster R-CNN的优化。特征模仿是一个比较高级的模型迁移策略,它主要用在知识蒸馏中。
- 它先训练一个准确率比较高但是速度慢的大型的Teacher网络,然后再训练一个速度快但是泛化能力略差的小的Student网络。
- 在训练Student网络的过程中,模型不仅要优化它本身任务的损失函数,还要以Teacher网络学习到的特征为目标,优化它的中间的特征层。
- 因为Student网络的参数量是固定的,所以通过这个策略优化之后的Student网络不仅速度很快,而且能够达到了Teacher类似的准确率。
- DCN v2使用R-CNN来指导加了了可变形卷积的Faster R-CNN训练的目的是使可变形卷积能够自行的学到更好的采样点的偏移和权重,从而减少无关因素对模型的负面影响。
代码:
import torch
from torch import nn
from yolox.models.network_blocks import get_activation
class DeformConv2d(nn.Module):
def __init__(self, inc, outc, kernel_size=3, stride=1, padding=1, bias=None, modulation=False,act="silu"):
"""
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.bn = nn.BatchNorm2d(outc)
self.act = get_activation(act, inplace=True)
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)
p = self._get_p(offset, dtype)
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)
p = torch.cat([torch.clamp(p[..., :N], 0, x.size(2)-1), torch.clamp(p[..., N:], 0, x.size(3)-1)], dim=-1)
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:]))
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)
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 = 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.act(self.bn(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))
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)
p_n = self._get_p_n(N, dtype)
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)
x = x.contiguous().view(b, c, -1)
index = q[..., :N]*padded_w + q[..., 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
参考文献1 参考文献2
|