3.9, 2019:修改了对于num_groups参数的理解, 之前貌似理解错了.重新看了一遍代码, 如果还有问题, 请大家即使指出.
上一节写了关于pytorch如何进行c++和cuda的extension, 主要讲了一下大致流程和一些我踩过的坑, 这一节我写一下deformable_conv的一些实现细节以及一些坑点. 写这个的目的是为了让以后如果有需要进行deformable卷积实现的人能够少走弯路. 欢迎大家留言, 我也是新手, 如果有什么不对的地方请及时指正.
上一节的文章:
王火华至秦:deformable变形卷积pytorch实现(第一节Custom op extension)zhuanlan.zhihu.com
pytorch版本的github地址:
BIGKnight/deformable_conv2d_pytorchgithub.com
我只实现了我所需要的deformable_conv2d部分, 至于deformable roi部分我并没有实现, 但只要会了过程, 也大同小异, 因为其实复现只是在翻译原文章, 只要知道了"映射规则", 结果自然很容易得出. 原文和官方代码在这:
Deformable ConvNets v2: More Deformable, Better Resultsarxiv.org msracver/Deformable-ConvNetsgithub.com
DEFORMABLE CONVOLUTION VERSION 2
具体原理请见论文. 我们的目的是为了让网络能够掌握自己学习出卷积核的形状的能力, 作者的方法是通过每次让一个额外的卷积层Conv2d去学习每个卷积位置的位移offset和置信mask参数, 然后当数据流到当前DeformableConv2d节点时, 先让数据流过Conv2d, 输出offset和mask这两个tensor, 再将这个两个tensor和相应的卷积核filter送到变形卷积DeformableConv2d节点里进行卷积, 再输出.
参数
这里有两个卷积核, 一个是正常的卷积核filter是在变形卷积时用到的, 还有一个是学习offset和mask时的那个额外卷积层的卷积核, 前者代表的是当前的变形卷积层的学习能力, 后者代表的是对于当前变形卷积层卷积核形状的学习能力. 我们需要实现的是变形卷积的过程而不是提取offset和mask的过程, 所以我们的deformableconv2d前向Forward部分输入就可以确定了, 首先是数据流x, 然后是卷积核filter, 然后依次为offset, mask. 这4项的数据类型都是tensor, 也是需要回传梯度的几个输入. 之后还有一些fixed的参数像stride, padding, dilation这些. 值得注意的几个fixed_parameter有三个: num_groups, deformable_groups, im2col_step. 这三个参数不太好理解, 我解释一些他们分别的作用(懂的就不用看这部分了).
- num_groups: 将原通道分为groups份, 且卷积核通道也减为1 / groups.
- 过程举例说明:原输入tensor的shape为(4, 256, 768, 1024)代表每个batch有4个sample, 每个sample有256个通道, 若out_channels = 64, groups = 2, kernel_size = 3,则此时卷积核的shape应该是(64, 128, 3, 3), 第一维度为输出通道数, 第二维度为in_channel数, 之后二个代表kernel_size. 对于一sample, 原输入通道为256, 但是卷积核的输入通道只有128, 按照我对于deformable代码的理解上, 我认为是这样的: 卷积核会被划分为2部分, 也就是说会将其变形为(32, 2, 128, 3, 3), 此时我们可以将卷积核视作32个组, 每个组有2个filter, 记为A, B, 每个filter的通道数为128, 这样处理时, 首先A对于sample的前128个通道进行卷积, 输出一个通道的feature map, B再对该sample剩下的128个通道进行卷积, 输出一个通道的feature map. 也就是说一个组处理一个sample会输出2个feature map, 但是要注意的是, 之前的out_channel的数目被除2了(也就是说只有32个组, 每个组输出2个feature mao), 那么其实总共的输出featuer map刚好是64, 等于设定的out_channel值.
- deformable_groups: 这个的意思是指相对于offset和mask, 将原通道打包成几份的意思,origin的情况的话,所有输入通道共享一对偏移,但是若num_deformable_groups > 1,则等于将输入通道分成num_deformable_groups份,每份单独拥有各自的一对偏移,则此时offset的通道个数应该为
- im2col_step: 这个是v2中新加的参数, 目的是为了加速im2col这块的速度, 我们都知道卷积在底层实现的时候是先将输入tensor变形为一个K * N的矩阵column, K为filter中一个卷积核参数个数
, N为输出tensor的长乘宽(其实就是内积点的个数), 然后卷积核可以展开为一个M * K的矩阵, 其中M为输出的通道数, 然后将这两个矩阵进行矩阵乘法, 就得到了输出M * N (大小刚好是( C x H x W )), 如此进行batch次就得到最后结果.
- 这里有一个非常重要的操作就是如何得到矩阵column, 我们把这个过程称为im2col. 然后im2col_step的作用其实就是改变每次进行im2col的sample的个数, 本来一次im2col_cuda_kernel只处理一个sample, 但是当im2col_step = n时, 它就每次处理n个sample. 从GPU的角度上讲, 若每次调用一个im2col_cuda_kernel核是处理一个sample, 那么当im2col_step增大时, 它需要调用im2col_cuda_kernel的个数就减小了, 也就是在他眼里sample数目减小了, 等同于输入tensor变为(batch/im2col_step, im2col_step * C, H, W). 要注意的一点是, 由于卷积核的大小在输入的时候就是固定的, 所以M * K不能变, 但是每次矩阵乘法的结果需要是原来的im2col_step倍(因为batch数减少了im2col_step倍而最后结果不能变). 所以只能让生成的column矩阵增大im2col_step倍, 故N需要增大im2col_step倍, 但是K不能变, 所以只能是N变大im2col_step倍, 则column的shape就是(in_channels * kernel_h * kernel_w, im2col_step * N). 则weights和column的结果是(out_channels, im2col_step * N), 循环batch/im2col_step的结果就是(batch/im2col_step, out_channels, im2col_step, N). 注意到没有, 和正常的输出(batch, out_channels, N), 这个结果的维度是不正确的, 本来从batch分离出去的im2col_step跑到了通道的右侧. 如果有点绕晕了可以画一下图(当时我就画了半天图, 我有时间画一下贴上来), 所以我们需要交换out_channels, im2col_step这两个维度, 当然需要cuda来实现.
cuda核
需要实现的cuda函数有三个, deformable_im2col, deformable_col2im, deformable_col2im_coord, 我分别讲一下他们的作用.
- deformable_im2col: 这个函数用来生成column矩阵, 在前向和后向过程都会用的这个函数, 这个函数host的声明部分如下:
- deformable_col2im: 这个函数用来生成向前一层传递的梯度, 也就是输入数据流的梯度, 只会在后向过程中用到这个函数, 这个函数的host声明部分如下:
- deformable_col2im_coord: 这个函数用来生成offset和mask的回传梯度, 只会在后向过程中用到这个函数, 这个函数的host声明部分如下:
CPP部分
首先, 就像我在上一篇文章说的, 我们需要实现的其实就是forward和backward两个函数.
1.forward
- forward部分首先需要对输入进行完整性和正确性检查, 这部分我在我的tensorflow版本里写的很详细, pytorch版本里偷懒省略了很多. 大体上就是查看输入的维度以及各维度大小是否和fixed参数相匹配. 然后pytorch里还需要检查一下是否是cuda_tensor, 因为我们只需要实现gpu版本就行了(额反正有兴趣实现cpu的版本也很简单的, 就只要把所有cuda函数改成c++函数就行了).
- 其次是提取和计算一些实现过程中需要的参数, 例如之前提到的M, N, K, 还有batch_size, 卷积核的长宽等等.
- 接下来需要获取所有输入tensor的一维展开后的头指针以及申请temp空间和输出tensor的存储空间. 总共是四个输入tensor, 一个tmp张量以及一个输出张量. 所以总共有6个指针变量. 其中col_buffer就是上面说的column矩阵,然后output是输出tensor.
- 我们都知道卷积在底层实现的时候是先将输入tensor变形为一个K * N的矩阵column, K为filter中一个卷积核参数个数(in_channels * kernel_h * kernel_w), N为输出tensor的长乘宽(其实就是内积点的个数), 然后卷积核可以展开为一个M * K的矩阵, 其中M为输出的通道数, 然后将这两个矩阵进行矩阵乘法, 就得到了输出M * N (大小刚好是( C x H x W )), 如此进行batch次就得到最后结果.
-
- 这里deformable_im2col是一个cuda函数, 其用途是计算column, 然后下面那个循环体是在进行矩阵乘法, 这里之所以要循环group_ = num_groups次, 是因为当其大于1的时候, 卷积核的shape是(out_channels, in_channels / num_groups, kernel_h, kernel_w). 也就是
但是生成的column矩阵的第一个维度大小是
, 所以需 要反复进行num_groups次, 才能得到正确结果.
- 这里有个神坑的一点是THCudaBlas_Sgemm这个函数比较不好懂, 这个函数是在进行矩阵的乘法操作, 和cublas中的sgemm参数是一样的, 我估计就是进行了一层封装. 这里我找到一篇比较不错的博文, 解释的很详细, 关于这个操作各操作的作用. 非常非常坑的一点, 也是新人很容易被坑到的一点是, 在c++中, 所有tensor都是row_major, 但是cuda的眼里所有的tensor都是column_major. 也就是说你用c++申请一个shape为(3, 4)的矩阵, 并按行填上1 ~ 12的数字, 那么存到显存里, 就是((1, 2, 3, 4), (5, 6, 7, 8) , (9, 10, 11, 12)). 但是cuda认为你这个是按列存的, 也就是说, 他解读后是((1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12)). 从线性展开的角度看其实是进行了一个转置了. 我们的解决方法是利用方程进行计算:
这是博文:
有关CUBLAS中的矩阵乘法函数 - 爨爨爨好 - 博客园www.cnblogs.com
到这里, forward的部分就结束了, 具体cuda核函数可以看源码, 不难看懂, 核心我认为就是这部分代码:
就是去计算卷积核中每个参数的位置, 然后传到二分线性插值核中取值, 最后乘上一个置信mask.
backward以及其余部分我留到下几篇文章去讲. 就先到这里吧. 写的有点匆忙, 欢迎留言讨论. O(∩_∩)O~~