DCN可形变卷积实现1:Python实现

DCN可形变卷积实现1:Python实现

我们会先用纯 Python 实现一个 Pytorch 版本的 DCN ,然后实现其 C++/CUDA 版本。

本文主要关注 DCN 可形变卷积的代码实现,不会过多的介绍其思想,如有兴趣,请参考论文原文:

Deformable Convolutional Networks

Deformable ConvNets v2: More Deformable, Better Results

DCN简介

考虑到传统卷积必须是方方正正的 k×kk\times kk×k 的卷积核:
y(p0)=∑pn∈Rw(pn)⋅x(p0+pn)\mathbf{y}(\mathbf{p}_0)=\sum_{\mathbf{p}_n\in \mathcal{R}}\mathbf{w}(\mathbf{p}_n)\cdot \mathbf{x}(\mathbf{p}_0+\mathbf{p}_n) y(p0)=pnRw(pn)x(p0+pn)
作者认为这个感受野太规则,无法很好地捕捉特殊形状的特征,因此在其基础上加了偏置:
y(p0)=∑pn∈Rw(pn)⋅x(p0+pn+Δpn)\mathbf{y}(\mathbf{p}_0)=\sum_{\mathbf{p}_n\in \mathcal{R}}\mathbf{w}(\mathbf{p}_n)\cdot \mathbf{x}(\mathbf{p}_0+\mathbf{p}_n+\Delta\mathbf{p}_n) y(p0)=pnRw(pn)x(p0+pn+Δpn)
使得模型能够根据输入计算偏移量,自己选择对哪些位置进行卷积计算,而不用必须是正方形的样子。

在这里插入图片描述

如上图所示,传统的卷积输入只能是图 (a) 中的九个绿点,而在加上偏移量之后,皆可以四处飞,比如飞到图 (bcd) 中蓝点的位置。

而 DCNv2 则在此基础上又为每个位置乘了一个可学习的权重:
y(p0)=∑pn∈Rw(pn)⋅x(p0+pn+Δpn)⋅Δmn\mathbf{y}(\mathbf{p}_0)=\sum_{\mathbf{p}_n\in \mathcal{R}}\mathbf{w}(\mathbf{p}_n)\cdot \mathbf{x}(\mathbf{p}_0+\mathbf{p}_n+\Delta\mathbf{p}_n)\cdot\Delta\mathbf{m}_n y(p0)=pnRw(pn)x(p0+pn+Δpn)Δmn

由于网络学习出的偏移量通常是小数,因此下面会用到双线性插值(下面会有图示),这里先把原文中的公式给出来:
x(p)=∑qG(q,p)⋅x(q)\mathbf{x}(\mathbf{p})=\sum_\mathbf{q}G(\mathbf{q},\mathbf{p})\cdot\mathbf{x}(\mathbf{q}) x(p)=qG(q,p)x(q)

这里 p=(p0+pn+Δpn)\mathbf{p}=(\mathbf{p}_0+\mathbf{p}_n+\Delta\mathbf{p}_n)p=(p0+pn+Δpn) 表示任意位置(可以是小数)坐标,而 q\mathbf{q}q 是枚举特征图 x\mathbf{x}x 中所有整数空间位置,G(⋅,⋅)G(\cdot,\cdot)G(,) 就是双线性插值,注意这里的 GGG 是两个维度(x,y)的,拆分为两个单维度的话,就是:

G(q,p)=g(qx,px)⋅g(qy,py)G(\mathbf{q},\mathbf{p})=g(q_x,p_x)\cdot g(q_y,p_y) G(q,p)=g(qx,px)g(qy,py)

其中 g(a,b)=max(0,1−∣a−b∣)g(a,b)=max(0,1-|a-b|)g(a,b)=max(0,1ab)

给出公式一方面是让读者了解具体算法,更重要的一点是我们参考的 DCN 的 Pytorch 实现代码中变量的命名是与原文公式对应的,因此公式列在这里方便读者下面看代码的时候可以回头看一下各个变量对应的是算法公式中的哪一项。

纯Python实现

我们先来看一下Pytorch版本的实现,来更好地理解 DCN 可形变卷积的做法,然后用 C++/CUDA 实现高性能版本。本文参考的 Python 实现是:https://github.com/4uiiurz1/pytorch-deform-conv-v2/blob/master/deform_conv_v2.py 。

本小节参考博文:deformable convolution可变形卷积(4uiiurz1-pytorch版)源码分析

_init_

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)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 = modulationif 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)

这里重点关注 self.p_convself.m_conv ,是这两个卷积完成了对偏移量 offset 的学习,而 self.conv 是确在定偏移后的位置之后,最终进行计算的卷积。

(关于这里的 modulation 参数,如注释所言,如果为 True ,就是一个模块化的 DCN,即 DCNv2。)

具体来看这三个卷积及其参数:

  • self.conv这是负责进行最终计算的卷积。可形变卷积 DCN 虽然进行了形变,但是这是卷积输入中空间像素的位置有了偏移,而输入输出的尺寸还是不变的,因此,输入卷积的位置确定之后,最终负责完成卷积计算的 self.conv 的各个参数(输入输出通道数inc, outc、卷积核大小kernel_size、步长stride、填充padding等)就是我们整个 DCN 的对应参数参数。

  • self.p_conv该卷积操作负责计算偏移量。在卷积中,共有 kernel_size * kernel_size 个位置的像素需要参与计算,因此我们要计算出他们的偏移量,而每个位置都有宽、高两个方向的偏移量,故该卷积输出的通道数是 2 * kernel_size * kernel_size ,其他参数保持一致。

  • self.m_conv该卷积操作负责计算卷积核每个位置的权重。其输出通道数为位置数,即 kernel_size * kernel_size ,其他参数保持一致,注意这个加权的想法是 DCNv2 中的。

在这里插入图片描述

forward

看过 __init__ 函数之后,我们可以来看 forward 函数:

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_sizeN = offset.size(1) // 2if 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 + 1q_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 pp = 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# 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 *= mx_offset = self._reshape_x_offset(x_offset, ks)out = self.conv(x_offset)return out

这里的 N 是 offset 的通道数除以2,就是卷积要处理的位置的个数(即 kernal_size * kernel_size)。

整个 forward 函数的流程:

  1. 首先通过上面介绍的 p_convv_conv 计算出偏移量 offset 和加权的权重m(如果有)。

  2. 比较关键的是这里的 self._get_p 函数,该函数通过上面计算出的 offset,去得到输入到卷积的具体位置,即公式中的:
    p0+pn+Δpn\mathbf{p}_0+\mathbf{p}_n+\Delta\mathbf{p}_n p0+pn+Δpn
    关于这个函数,我们会在下一小节详细介绍。由于我们现在先过整个流程,只需要知道该函数通过 p_conv 卷积计算出的 offset,得到了要输入最终卷积的位置 p。p 是一个形状为 (bs,2∗N,h,w)(bs,2*N,h,w)(bs,2N,h,w) 的张量。

  3. 拿到 p 之后的问题是我们得到的肯定是一个浮点类型,即小数,但是像素的坐标肯定是整型,所以,这里我们需要做一个双线性插值。双线性插值的思想也很直接,就是将某个浮点坐标的左上、左下、右上、右下四个位置的像素值按照与该点的距离计算加权和,作为该点处的像素值。可参考下图,也可参考博客图像预处理之warpaffine与双线性插值及其高性能实现,后半部分有对双线性插值的讲解与 Python 实现。

    这里的 lt, rb, lb, rt 分别代表左上,右下,左下,右上。

在这里插入图片描述

  1. 现在我们通过双线性插值拿到了每个位置的坐标,下一步就是根据坐标去取到对应位置的像素值,这在代码中由 self._get_x_q 实现,会在下面的小节介绍。

  2. 这个时候如果有权重的话,要计算出 m,乘到 x_offset 上。

  3. 这时得到的 x_offset 的形状是 b,c,h,w,Nb,c,h,w,Nb,c,h,w,N,而我们要的形状肯定是 b,c,h,wb,c,h,wb,c,h,w,因此这里还有一个 reshape 的操作,由 self._reshape_x_offset 实现。

  4. 至此,我们终于得到了想要的 x_offset,接下来就将它送入 self.conv 进行卷积计算并返回结果即可。

_get_p、_get_p_0、_get_p_n

先贴一下代码:

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 + offsetreturn pdef _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_ndef _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

我们来看一下如何将 offset 传入 self._get_p 获得最终的 p,该函数会分别调用 self._get_p_0self._get_p_n 来分别获得 p_0 和 p_n,分别是卷积核的中心坐标和相对坐标,对应到公式中的 p0,pn\mathbf{p}_0,\ \mathbf{p}_np0, pn
y(p0)=∑pn∈Rw(pn)⋅x(p0+pn+Δpn)\mathbf{y}(\mathbf{p}_0)=\sum_{\mathbf{p}_n\in \mathcal{R}}\mathbf{w}(\mathbf{p}_n)\cdot \mathbf{x}(\mathbf{p}_0+\mathbf{p}_n+\Delta\mathbf{p}_n) y(p0)=pnRw(pn)x(p0+pn+Δpn)

在这里插入图片描述

关于 p_0 和 p_n 具体是什么东西其实很好理解,画个小图就明白了,以 kernel_size = 3 的卷积为例,中心位置在全图中的坐标就是 p_0,中心位置的相对坐标就是 p_n=(0,0),左上角的 p_n=(-1,-1),右下角的 p_n=(1,1) 其他位置以此类推。常规的卷积就只有 pn+p0\mathbf{p}_n+\mathbf{p}_0pn+p0 ,输入就是只能在上图中的九个格子中,而 DCN 加入 Δpn\Delta\mathbf{p}_nΔpn 之后,就可以四处飞啦。但是四处飞,也是要在 pn+p0\mathbf{p}_n+\mathbf{p}_0pn+p0 的基础上再加上偏移量来计算具体的位置。所以我们先要获得 p_0 和 p_n。

当然,p_0 和 p_n 都是固定的、不需要学习的、而且是很规则的,因此获取他们只需要根据 kernel_size 和位置 h, w (仅 p_0 需要)来计算就好了。这里代码实现中就是用 torch.arange 和 torch.meshgrid 将想要的 p_0 和 p_n,计算出来。

然后 p = p_0 + p_n + offset(对应公式),得到尺寸为 (bs,2∗N,h,w)(bs, 2*N, h, w)(bs,2N,h,w) 的 p。

_get_x_q

_get_x_q 函数是根据计算出的位置坐标,得到该位置的像素值。

再提醒一下,我们参考的 DCN 的 Pytorch 实现代码中变量的命名是与原文公式对应的,如果有变量含义不明确的,可以回上面看看公式,对应代码变量名理解。

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

_reshape_x_offset

我们在取完像素值之后得到的 x_offset 的形状是 b,c,h,w,Nb,c,h,w,Nb,c,h,w,N,而我们要的形状肯定是 b,c,h,wb,c,h,wb,c,h,w,因此这里还有一个 reshape 的操作,就是这里的 self._reshape_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

小结

至此,我们已经使用 Pytorch 实现了纯 Python 的 DCN 卷积结构,但是,如此实现由于不是原生的 C++/CUDA 算子,而且最后的 reshape 操作虽然比较巧妙,但其实空间冗余比较大,和原文作者的 cuda 版本内存占用量差了10几倍。这个是因为在 im2col 上直接操作可以去掉很冗余。下面一篇我们会再介绍一个 C++/CUDA 实现的 DCN。

Ref

  • deformable convolution可变形卷积(4uiiurz1-pytorch版)源码分析
  • 图像预处理之warpaffine与双线性插值及其高性能实现

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/532448.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

蓝牙耳机声音一顿一顿的_线控耳机党阵地转移成功,OPPO这款TWS耳机体验满分...

“你看到我手机里3.5mm的耳机孔了吗”,这可能是许多线控耳机党最想说的话了。确实,如今手机在做“减法”,而厂商们首先就拿3.5mm耳机孔“开刀”,我们也丧失了半夜边充电边戴耳机打游戏的乐趣。竟然如此,那如何在耳机、…

AI移动端优化之Im2Col+Pack+Sgemm

AI移动端优化之Im2ColPackSgemm 转自:https://blog.csdn.net/just_sort/article/details/108412760 这篇文章是基于NCNN的Sgemm卷积为大家介绍Im2ColPackSgemm的原理以及算法实现,希望对算法优化感兴趣或者做深度学习模型部署的读者带来帮助。 1. 前言 …

elementui的upload组件怎么获取上传的文本流、_抖音feed流直播间引流你还不会玩?实操讲解...

本文由艾奇在线明星优化师写作计划出品在这个全民惊恐多灾多难且带有魔幻的2020,一场突如其来的疫情改变了人们很多消费习惯,同时加速了直播电商的发展,现在直播已经成为商家必争的营销之地,直播虽然很火,但如果没有流…

FFmpeg 视频处理入门教程

FFmpeg 视频处理入门教程 转自:https://www.ruanyifeng.com/blog/2020/01/ffmpeg.html 作者: 阮一峰 日期: 2020年1月14日 FFmpeg 是视频处理最常用的开源软件。 它功能强大,用途广泛,大量用于视频网站和商业软件&…

checkbox wpf 改变框的大小_【论文阅读】倾斜目标范围框(标注)的终极方案

前言最常用的斜框标注方式是在正框的基础上加一个旋转角度θ,其代数表示为(x_c,y_c,w,h,θ),其中(x_c,y_c )表示范围框中心点坐标,(w,h)表示范围框的宽和高[1,2,7]。对于该标注方式,如果将w和h的值互换,再将θ加上或者…

彻底理解BP之手写BP图像分类你也行

彻底理解BP之手写BP图像分类你也行 转自:https://zhuanlan.zhihu.com/p/397963213 第一节:用矩阵的视角,看懂BP的网络图 1.1、什么是BP反向传播算法 BP(Back Propagation)误差反向传播算法,使用反向传播算法的多层感知器又称为B…

梯度下降法和牛顿法计算开根号

梯度下降法和牛顿法计算开根号 本文将介绍如何不调包,只能使用加减乘除法实现对根号x的求解。主要介绍梯度下降和牛顿法者两种方法,并给出 C 实现。 梯度下降法 思路/步骤 转化问题,将 x\sqrt{x}x​ 的求解转化为最小化目标函数&#xff…

汇博工业机器人码垛机怎么写_全自动码垛机器人在企业生产中的地位越来越重要...

全自动码垛机器人在企业生产中的地位越来越重要在智能化的各种全自动生产线中,全自动码垛机器人成了全自动生产线的重要机械设备,在各种生产中发挥着不可忽视的作用。全自动码垛机器人主要用于生产线上的包装过程中,不仅能够提高企业的生产率…

小说中场景的功能_《流浪地球》:从小说到电影

2019年春节贺岁档冒出一匹黑马:国产科幻片《流浪地球》大年初一上映后口碑、票房双丰收:截至9日下午,票房已破15亿,并获得9.2的高评分。著名导演詹姆斯卡梅隆通过社交媒体对我国春节期间上映的科幻影片《流浪地球》发出的祝愿&…

线性回归与逻辑回归及其实现

线性回归与逻辑回归及其实现 回归与分类 预测值定性分析,即离散变量预测时,称之为分类;预测值定量分析,即连续变量预测时,称之为回归。 如预测一张图片是猫还是狗,是分类问题;预测明年的房价…

hbase 页面访问_HBase

HBase 特点 海量存储 Hbase 适合存储 PB 级别的海量数据,在 PB 级别的数据以及采用廉价 PC 存储的情况下,能在几十到百毫秒内返回数据。这与 Hbase 的极易扩展性息息相关。正式因为 Hbase 良好的扩展性,才为海量数据的存储提供了便利。 2&…

深入理解L1、L2正则化

深入理解L1、L2正则化 转自:【面试看这篇就够了】L1、L2正则化理解 一、概述 正则化(Regularization)是机器学习中一种常用的技术,其主要目的是控制模型复杂度,减小过拟合。正则化技术已经成为模型训练中的常用技术&a…

机器学习中的概率模型

机器学习中的概率模型 转自:https://zhuanlan.zhihu.com/p/164551678 机器学习中的概率模型 概率论,包括它的延伸-信息论,以及随机过程,在机器学习中有重要的作用。它们被广泛用于建立预测函数,目标函数,以…

max std value 宏_Rust Macro/宏 新手指南

Rust语言最强大的一个特点就是可以创建和利用宏/Macro。不过创建 Rust宏看起来挺复杂,常常令刚接触Rust的开发者心生畏惧。这片文章 的目的就是帮助你理解Rust Macro的基本运作原理,学习如何创建自己的 Rust宏。相关链接:在线学编程 - 汇智网…

农林资金 大数据审计案例_大数据审计:现状与发展

大数据审计:现状与发展【摘要】传统手工环境下,审计人员常用的审计方法包括检查法、观察法、重新计算法、外部调查法、分析法、鉴定法等。随着信息技术的发展,被审计单位的运行越来越依赖于信息化环境。信息化环境下审计工作发生了巨大的变化…

angularjs sill 创建项目_开源项目——博客项目MyBlogs.Core,基于.NET 5

个人博客站项目源码,高性能低占用的博客系统,这也许是我个人目前写过的性能最高的web项目了 。目前日均处理请求数80-120w次,同时在线活跃用户数30-100人,数据量累计已达到100多万条,数据库Redis网站主程序同时运行在一…

怀旧服推荐配置_【怀旧服】狂暴战P4毕业装备推荐

在怀旧服开启P4阶段之后,狂暴战玩家的输出也得到了进一步的提升。当然,狂暴战想要打出足够的伤害离不开对应的装备,现在就给大家介绍下狂暴战P4阶段的BIS装备。散件装备狂暴战在这一阶段依旧有非常不错的散件装备,个人建议玩家入手…

高斯混合模型GMM及EM迭代求解算法(含代码实现)

高斯混合模型GMM及EM迭代求解算法(含代码实现) 高斯分布与高斯混合模型 高斯分布 高斯分布大家都很熟悉了,下面是一元高斯分布的概率密度函数(Probability Density Function,PDF): P(x)N(μ,…

十个模块_专栏 | ABAQUS Part模块的十个小技巧

作者介绍星辰_北极星2012年开始从事Abaqus仿真相关工作,服务大小课题逾百项; 主要仿真领域:石油工程、岩土工程和金属加工工艺; 重点研究方向:ABAQUS GUI二次开发、固体力学、断裂以及损伤等。Abaqus有部件(Part)和装配体(Assembl…

深度学习时代的视频理解综述

深度学习时代的视频理解综述 本文为b站bryanyzhu老师四期视频理解相关论文解读的汇总图文笔记。 我们先精读深度学习时代视频理解领域最为重要的两篇论文:双流网络和 I3D。它们分别是领域内两大类方法双流(利用光流)网络和 3D CNN 网络的代…