1 前言

DCN是一个常见的技术,在Deformable-DETR跟PP-YOLO中都有用到DCN;
论文:《Deformable ConvNets v2: More Deformable, Better Results》

致谢:

感谢Buf哥的文章讲解——《再思考可变形卷积》

2 DCN的特点

  • 理论上可以获得任意大小的感受野(由于其deformable的特性)

3 南溪对于DCN的理解

3.1 Modulation mechanism——实现了spatial-attention

在南溪看来,modulation-mechanism实际上实现了一种spatial-attention的操作,对不同的像素值有着不同的权重;

3.2 Deformable——具有变形能力的稀疏采样

DCN通过其deformable的能力实现了一种learnable的稀疏采样,在固定参数量的情况下,使得卷积核具有变形采样的能力;
从神经元的连接性上来说,DCN实现了一种固定数量的全局连接,通过deformable能力实现与上层特征图任意位置的连接,体现了神经元连接的复杂特性;

4 DCN的使用——“在哪里加入DCNv2?”

这里我们参考了飞桨的DCN实现,以下是PyTorch格式的配置:
dcn_v2_stages: [2, 3, 4]
结论:
我试过resnet50_dcn的效果很好,以下是实验结果的截图,

5 DCNv2的实现——基于PyTorch实现

参考GitHub项目——4uiiurz1/pytorch-deform-conv-v2

5.1 DeformConv2d [source]

DCNv2模块的nn.Module类;
当前版本的代码实现仅支持3x3卷积;

5.1.1 类的构造__init__()函数

# inc表示输入通道数
# outc 表示输出通道数
# kernel_size表示卷积核尺寸
# stride 卷积核滑动步长
# bias 偏置
# modulation DCNV1还是DCNV2的开关
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_sizeself.padding = paddingself.stride = strideself.zero_padding = nn.ZeroPad2d(padding)# 普通的卷积层,即获得了偏移量之后的特征图再接一个普通卷积self.conv = nn.Conv2d(inc, outc, kernel_size=kernel_size, stride=kernel_size, bias=bias)# 获得偏移量,卷积核的通道数应该为2*ks*ks# padding = (ks-1)//2, 当ks=3时,padding=1self.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)# 注册module反向传播的hook函数, 可以查看当前层参数的梯度self.p_conv.register_backward_hook(self._set_lr)# 将modulation赋值给当前类self.modulation = modulationif modulation:# 如果是DCN V2,还多了一个权重参数,用m_conv来表示self.m_conv = nn.Conv2d(inc, kernel_size*kernel_size, kernel_size=3, padding=1, stride=stride)# 初始值设置为0,即:sigmoid(0)=0.5,初始权重为0.5nn.init.constant_(self.m_conv.weight, 0)# 注册module反向传播的hook函数, 可以查看当前层参数的梯度self.m_conv.register_backward_hook(self._set_lr)# 静态方法 类或实例均可调用,这函数的结合hook可以输出你想要的Variable的梯度
@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)))

5.1.2 _get_p()——获得变形坐标

这个函数用来获取偏移之后相对于原始特征图x的坐标(float)。

  • offset:表示偏移坐标;
  • dtype:数据类型。
def _get_p(self, offset, dtype):# N = 18 / 2 = 9,表示卷积核的参数数量# h = 32# w = 32N, h, w = offset.size(1)//2, offset.size(2), offset.size(3)# (1, 2N, 1, 1),获得N个卷积点的偏移p_n = self._get_p_n(N, dtype)# (1, 2N, h, w),获得每个像素点的坐标p_0 = self._get_p_0(h, w, N, dtype)# 卷积坐标加上之前学习出的offset后就是论文提出的公式(2)也就是加上了偏置后的卷积操作。# 比如p(在N=0时)p_0就是中心坐标,而p_n=(-1,-1),所以此时的p就是卷积核中心坐标加上# (-1,-1)(即红色块左上方的块)再加上offset。同理可得N=1,N=2...分别代表了一个卷积核# 上各个元素。p = p_0 + p_n + offset# 在进行offset的加法时,会产生广播操作,最终的结果p--[B, H, W, 2N]return p

5.1.3 _get_x_q()——取值函数

通过索引获得张量中指定位置的值;
关于这个函数,南溪之前困惑了很久,不知道为什么要用一个padded_w,这里出现padded_w的主要原因是在forward()函数中有对x进行padding,即:

if self.padding:x = self.zero_padding(x)# self.zero_padding实际上就是nn.ZeroPad2d()函数

这里南溪当时一直不是很理解这里为什么要进行padding,后来想了一下终于明白了,因为在进行卷积时,其实真正参与运算的不是x,而是经过padding之后的潜在张量x_la,所以通过p_conv获得的偏移实际上是潜在特征图x_la的坐标偏移,而不再是x的坐标偏移了,所以在使用_get_x_q取值前,首先需要获得潜在特征图x_la

  • x:需要获取的张量;
  • q:[b, h, w, 2N],表示像素点的索引。
def _get_x_q(self, x, q, N):b, h, w, _ = q.size()# 获得x_la的宽度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

5.1.2 forward()——前向传播函数

# 前向传播函数
def forward(self, x):# 获得输入特征图x的偏移量# 假设输入特征图shape是[1,3,32,32],然后卷积核是3x3,# 输出通道数为32,那么offset的shape是[1,2*3*3,32]offset = self.p_conv(x)# 如果是DCNv2那么还需要获得输入特征图x偏移特征图的权重项# 假设输入特征图shape是[1,3,32,32],然后卷积核是3x3,# 输出通道数为32,那么offset的权重shape是[1,3*3,32]if self.modulation:m = torch.sigmoid(self.m_conv(x))# 这个sigmoid用的很酷,可以增加非线性# dtype = torch.float32dtype = offset.data.type()# 卷积核尺寸大小ks = self.kernel_size# N=2*3*3/2=3*3=9N = offset.size(1) // 2# 如果需要Padding就先Paddingif self.padding:x = self.zero_padding(x)# self.zero_padding实际上就是nn.ZeroPad2d()函数# p的shape为[1, 2N, H, W]# 这个函数用来获取所有的卷积核偏移之后相对于原始特征图x的坐标(现在是浮点数)p = self._get_p(offset, dtype)# 我们学习出的量是float类型的,而像素坐标都是整数类型的,# 所以我们还要用双线性插值的方法去推算相应的值# 维度转换,现在p的维度为[B, 2N, H, W]p = p.contiguous().permute(0, 2, 3, 1)# 转换之后为[B, H, W, 2N]# l:left, r:right; t:top, b:bottom# floor是向下取整q_lt = p.detach().floor()# +1相当于原始坐标向上取整q_rb = q_lt + 1# 将q_lt即左上角坐标的值限制在图像范围内,torch.clamp用来限定张量值的范围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即右下角坐标的值限制在图像范围内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_lt的前半部分坐标q_lt_x和q_rb的后半部分q_rb_y组合成q_lbq_lb = torch.cat([q_lt[..., :N], q_rb[..., N:]], dim=-1)# dim=-1即按照维度N进行concat# 同理q_rt = torch.cat([q_rb[..., :N], q_lt[..., N:]], dim=-1)# 将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)# 双线性插值的4个系数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)# 现在只获取了坐标值,我们最终目的是获取相应坐标上的值,# 这里我们通过self._get_x_q()获取相应值。# x:# q_lt:[B, H, W, 2N]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# modulationif 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,但是x_offset的size# 是(b,c,h,w,N),我们的目的是将最终的输出结果的size变# 成和x一致即(b,c,h,w),所以在最后用了一个reshape的操作。# 这里ks=3x_offset = self._reshape_x_offset(x_offset, ks)out = self.conv(x_offset)return out

6 学习笔记

6.1 有一点疑惑的是,p_conv生成的offset跟特征向量x没有发生四则运算,而只是有索引运算那么,梯度是如何传递的呢?

有一天我在公交车上思考关于DCN的变形时,想到:p_conv生成的offset跟特征向量x没有发生四则运算,而只是有索引运算那么,那么梯度是如何传递的呢?
我猜想:可能时索引操作可以传递梯度,有两种可能,

  • x[index]操作可以传递梯度
  • x.gather()函数可以传递梯度

现在,我们通过实验来验证一下索引操作是否可以传递梯度,……
我在知乎中关于这个问题进行了提问——“关于 DCN 中的 p_conv 是如何获得梯度更新的?”;不过暂时没有同学回答;
我在这里对之前的探究进行一下总结:我感觉,总的来说,DCNv2虽然叫做“可变形卷积”,实际上它无法真正进行变形,很有可能这里的p_conv根本无法获得参数更新,实际上只是通过随机初始化形成了多个随机大小的感受野,也就是说p_conv中有多个随机大小的偏移从而有不同形状的卷积核,然后根据蒙特卡洛的思想来学习不同便宜的卷积核,总的来说,所谓的“可变形卷积”实际上无法进行变形,而只是具有多个不同形状的卷积核罢了。

《南溪的目标检测学习笔记》——DCN(DCNv2)的学习笔记相关推荐

  1. 《南溪的目标检测学习笔记》——模型预处理的学习笔记

    1 介绍 在目标检测任务中,模型预处理分为两个步骤: 图像预处理:基于图像处理算法 数值预处理:基于机器学习理论 关于图像预处理,请参考<南溪的目标检测学习笔记>--图像预处理的学习笔记 ...

  2. 《南溪的目标检测学习笔记》——COCO数据集的学习笔记

    1 COCO数据集 COCO数据集下载链接:COCO_download 1.1 数据概览 数据集大小 train: 118287张 train+val: 123287张 val: 5000张 目标数量 ...

  3. 《南溪的目标检测学习笔记》的笔记目录

    1 前言 这是<南溪的目标检测学习笔记>的目录~ 2 学习目标检测的思路--"总纲" <南溪的目标检测学习笔记>--目标检测的学习笔记 我在这篇文章中介绍了 ...

  4. 《南溪的目标检测学习笔记》——目标检测模型的设计笔记

    1 南溪学习的目标检测模型--DETR 南溪最赞赏的目标检测模型是DETR, 论文名称:End-to-End Object Detection with Transformers 1.2 decode ...

  5. 《南溪的目标检测学习笔记》——夏侯南溪的CNN调参笔记,加油

    1 致谢 感谢赵老师的教导! 感谢张老师的指导! 2 调参目标 在COCO数据集上获得mAP>=10.0的模型,现在PaddleDetection上的Anchor-Free模型[TTFNet]的 ...

  6. 夏侯南溪搭建目标检测模型——文件结构设计篇

    1 前言 此文章是夏侯南溪搭建目标检测模型--文件夹结构设计篇- 2 文件夹命名 使用小写字母命名,单词之间用短线"-"进行连接,例如: plane-detection 3 项目文 ...

  7. 2021-01-24过去十年十大AI研究热点,分别为深度神经网络、特征抽取、图像分类、目标检测、语义分割、表示学习、生成对抗网络、语义网络、协同过滤和机器翻译。

    专利申请量全球第一!清华人工智能发展报告:国内215所高校成立相关本科专业 发布时间:01-2415:20万象大会年度获奖创作者,东方财富网官方帐号 1月20日,清华大学人工智能研究院.清华-中国工程 ...

  8. CV Code | 本周新出计算机视觉开源代码汇总(含图像修复、目标检测、医学图像分割、度量学习等)...

    点击我爱计算机视觉标星,更快获取CVML新技术 CV君汇总了过去一周计算机视觉领域新出的开源代码,涉及到图像质量评价.图像去雾.图像修复.医学图像分割.目标检测.人脸对齐.度量学习等,其中有多篇来自C ...

  9. 目标检测算法(传统基于深度学习的)

    目标检测算法 最近在学目标检测各类算法,主要分为传统的目标检测方法和基于深度学习的目标检测算法,这里记录了一些基本的算法介绍.下图是目标检测算法的发展历程 传统目标检测方法 分三部分:区域选择–> ...

  10. 《南溪的目标检测学习笔记》——PyTorch模型搭建模板

    1 前言 南溪在学习基于PyTorch的目标检测模型,对一些十分好的代码模板进行学习- 2 PyTorch模型模板 2.1 Faster R-CNN ResNet-50 FPN--PyTorch官方的 ...

最新文章

  1. seo发展基本趋势优化专员必须知道!
  2. Python中单下划线开头的特性
  3. php中按引用传递参数,如何通过PHP中的引用传递可变参数的参数?
  4. hive实例,GPRS流量统计
  5. 基于深度学习模型的麻蕉疾病自动识别(增加形态计量和几何分析)
  6. C++_CopyConstructor(副本构造器 防止指针重复释放)
  7. angular跳转指定页面_angularjs,前端_怎么设置登录成功后跳转到相应的页面,angularjs,前端 - phpStudy...
  8. Docker容器中添加字体
  9. 2012考研数学二第(18)题——多元函数积分学:二重积分求面积+画曲线:心形线
  10. 计算机上面的用户帐户怎么删除,怎么删除用户帐户的教程详解【图解】
  11. mysql 有没有minus_MySQL实现差集(Minus)和交集(Intersect)
  12. 计算机病毒大多数具有自身复制的功能,《计算机基础》第五章练习题
  13. Haproxy常见的负载均衡调度算法及应用场景
  14. scanner练习:BMI计算
  15. python经典实例应用_《Python经典实例(图灵出品)》([美]史蒂文·F.洛特)【摘要 书评 试读】- 京东图书...
  16. 微信小程序使用swiper制作轮播图留白的解决方法
  17. 软件工程师的试炼之地:53道Python面试问答
  18. 【转】则表达式匹配居民身份证
  19. [Vue warn]: Duplicate keys detected: ‘user‘. This may cause an update error. 解决方法
  20. wusir 面试题答案在老男孩的视频里

热门文章

  1. 在 Chrome 调试 Javascript
  2. 标准BT.656并行数据结构
  3. [导入]【翻译】WF从入门到精通(第二章):workflow运行时
  4. [BZOJ]2194: 快速傅立叶之二
  5. 在delphi原有控件基础上画图
  6. 怎样完善和推广自己的理论模型?
  7. 天外印刷的计件工资项目结束了
  8. 【Java从0到架构师】JS_jQuery_BootStrap
  9. 跟我一起学jQuery——第二集(未完待续..)
  10. 庆祝喜提13337000编号,我悄悄把老板的Windows命令行设置成了这样