Date: 2020/07/12
Coder: CW
Foreword:
相信大家在看paper的时候或多或少都能见到 Deformable 操作的身影,这种可变形操作是即插即用的,可嵌入到现有的算法框架中,最常见的是可变形卷积,出自于这篇paper: Deformable Convolutional Networks(DCN)
CW将 DCN 中有关可变形卷积的知识梳理了一番,同时基于Pytorch框架进行源码实现,本文将和大家分享下这些内容,我不独食,大家一起分享,更香!
Outline
I. Deformable Conv 的出道
II. 揭秘可变形卷积大法
III. 还不过瘾?这就来解析源码!
Deformable Conv 的出道
传统的卷积操作是将特征图分成一个个与卷积核大小相同的部分,然后进行卷积操作,每部分在特征图上的位置都是固定的(卷积核中心位置及其领域)。这样,对于形变比较复杂的物体,使用这种卷积的效果就可能不太好了。针对这种现象,传统的做法有:丰富数据集从而引入更多复杂形变的样本、使用各种骚里骚气的数据增强和tricks,以及人工设计一些手工特征和算法。
基于数据集和数据增强的做法都有点“暴力”,通常收敛慢而且需要较复杂的网络结构来配合;而基于手工特征算法就实在是有点“太难了”..特别是物体形变可能千变万化,这种做法本身难度就很大,而且不灵活,炼丹本身就够辛苦了,何必苦上加苦呢..
这时候,Deformable Conv 出道了!他站上演讲台,说他是个性boy,他会变形(不是变性哦!),他不像常规卷积那样死板,他更灵活,可以应对上述提到的物体复杂形变的场景。
台下有人吼道:“牛逼吹这么大?不管你是变形还是变性,来说说你的大招,是怎么解决问题的。”
Deformable Conv boy 雄赳赳,气昂昂地说到:“我在感受野中引入了偏移量,而且这偏移量是可学习的,我这招可以使得感受野不再是死板的方形,更贴近物体的实际形状,这样之后的卷积区域便始终覆盖在物体形状周围,无论物体如何形变都能搞定!” 话音刚落,他还顺带秀出了这招的纪录片来展示效果:
上图(a)中绿色的点代表原始感受野范围,(b)、(c)和(d)中的蓝色点代表加上偏移量后新的感受野位置,可以看到添加偏移量后可以应对诸如目标移动、尺寸缩放、旋转等各种情况。
揭秘可变形卷积大法
散发完霸王色霸气(海贼迷应该懂)后,台下众人(居然没有被霸王色霸气震晕过去,也是666)觉得这家伙好像是有点料,想进一步了解这招是如何炼成的,希望丰富下自己的技能包。
Deformable conv 这个boy 也是大度,这么6的大招也不私藏,因为他心里一直遵奉开源协同、共同学习、共同进步的思想,大家好才是真的好,世界才会更美好!下面他就为大家揭秘了他的大招——可变形卷积大法。
传统的卷积结构可以定义成公式1,其中是输出特征图的每个点,与卷积核中心点对应,是在卷积核大小范围内领域的每个偏移量。
而可变形卷积则在上述公式1的基础上为领域内的每个点都引入了一个偏移量,偏移量是由输入特征图与另一个卷积生成的。
由于加入偏移量后的坐标位置通常不是整数,于是并不对应feature map上实际存在的特征点,因此需要使用插值的方法来获得偏移后的特征值,通常可采用双线性插值,用公式表示如下:
上述公式的意义就是说将插值点位置的特征值设为其4领域特征点的加权和,领域4个点是离其最近的在特征图上实际存在的特征点,每个点的权重则根据它与插值点横、纵坐标的距离来设置,公式最后一行的max(0, 1-...)就是限制了插值点与领域点的距离不会超过1。
Deformable Conv boy给出了可变形卷积操作的简单示意图,可以看到offsets是额外使用一个卷积核来生成的,与最终要做卷积操作那个卷积核不是同一个。
还不过瘾?这就来解析源码!
叙述完原理后,台下众人觉得还不过瘾,毕竟炼丹本来就够玄的了,如果没有实实在在的代码,那就如同痴人说梦话!Deformable Conv boy不愧是大度的boy,他二话不说,接着对他的大招进行源码解析。
常规操作,他使用nn.Module的子类来封装可变形卷积操作,其中modulation是可选参数,若设置为True,那么在进行卷积操作时,对应卷积核的每个位置都会分配一个权重。
p_conv是生成偏移量所使用的卷积,输出通道数为卷积核尺寸(如3x3)的2倍,之所以是2倍是因为卷积核每个位置的横、纵坐标都会有对应的偏移量。
conv则代表最终实际要进行操作的卷积,注意这里步长设置为卷积核大小,因为与该卷积核进行卷积操作的特征图是由输出特征图上的每个特征点加上偏移量后扩展而来的,并不是原本的输入特征图。
比如conv是3x3卷积,输出特征图尺寸是2x3(hxw),其中的每个点对应卷积核中心,在卷积核邻域内都有9(3x3)个点(包括自身),于是与conv进行卷积的特征图尺寸应是(2x3) x (3x3),将stride设置为3,这样最终输出的特征图尺寸才能是2x3。也就是说,这里是将输出特征图上的每个特征点都扩展为其对应的3x3邻域,但这个邻域并不局限于特征点原先周围的邻域了,而是加上了学习到的偏移量,可以对应到整个特征图的任意位置。
以上还对p_conv和m_conv的权重进行了初始化。p_conv的权重初始化为0,代表初始时没有偏移量;m_conv的权重初始化为1,代表初始时卷积核每个位置的权重都为1。
下图这部分对应的就是上一节讲到的可变形卷积公式 ,即输出特征图上每点(对应到卷积核中心位置)的卷积核邻域点加上学习到的(横、纵坐标)偏移量后的位置。
offset是前面p_conv的输出,代表卷积核领域各点的可学习偏移量;p0代表将输出特征图每点对应到卷积核中心,然后映射到输入特征图中的位置;pn则是p0对应在卷积核邻域内每个位置的相对坐标。比如使用3x3卷积,那么对于卷积核的1个中心点来说,在pn中,横、纵相对坐标各3个值(-1、0、1),组合起来一共有9(3x3)个值,pn仅与卷积核尺寸相关。另外,以下注释中N等于卷积核尺寸(如果是3x3大小的卷积核,那么N就是9)。
接下来解析p0的计算,p0_y、p0_x就是输出特征图每点映射到输入特征图上的纵(y)、横(x)坐标值。根据torch.arange()那部分可知,纵、横坐标分别都有out_h和out_w个,与输出特征图尺寸对应。kc是卷积核中心位置,比如3x3卷积的话,中心点位置就是(1,1),然后根据卷积的步长和输出特征图尺寸就能得到在每个卷积过程中,中心点对应在输入特征图上的位置。
注意,这里将p0_y和p0_x进行重复(repeat)N次是因为每个特征点的领域都有N个点(包括自身),也就分别有N个纵坐标和横坐标。这样,在后续操作时就能和pn以及offset的形状对应上,从而方便计算偏移后的坐标位置。
搞定完p0,接下来撸一撸pn。与p0的计算类似,只不过其仅由卷积核尺寸决定,以卷积核中心为参考点,由于中心点位置是其尺寸的一半,于是中心点向左(上)方向移动尺寸的一半就得到起点,向右(下)方向移动另一半就得到终点,这就是以下torch.arange()部分对应的内容,得到的pn_y和pn_x分别是卷积核领域内各点相对于核中心的纵、横坐标偏移值。
OK,通过我们已经可以得到输出特征图每点对应的卷积核邻域点加上学习到的偏移量后的位置。前面说过,这些位置通常是小数,并不对应特征图上实际的位置,Deformable Conv boy 于是使用双线性插值来计算这些位置的特征值,使用这招首先需要知道每个位置点最近的4领域点,它们是特征图上实际的特征点,再根据4领域点与插值点位置的距离设置权重,最终由4邻域特征值及对应的权重进行加权求和作为插值点的特征值。
以下就是得到4领域点位置的计算过程,注意要将这些位置限制在输入特征图尺寸范围内(对应以下clamp()部分)。
注:以下注释有误, (b,c,out_h,out_w,2N) 应为 (b,out_h,out_w,2N)
注:以上变量命名中的 l, r, t, b分别代表 left, right, top, bottom
计算出4领域点的位置后,理所当然地,我们需要知道它们对应在输入特征图上的特征值,下图中由_get_x_q()这个方法得到。另外,g_xx部分计算的是4领域点对应的权重。
最后,将4领域点的权重和特征值进行加权求和得到插值点对应的特征值。
以上双线性插值的计算过程可结合下面几张图来理解。
由于邻域间特征点的横(x)、纵(y)坐标之差都是1,于是可以简化为下面的公式。
搞定这部分,我们已经可以得到偏移后位置对应的特征值了,但是还没有解释如何得到4邻域点的特征值,现在就来一探究竟。
q代表邻域点在输入特征图上的位置,其最后一维2N的前半部分代表纵(y)坐标,后半部分代表横(x)坐标,将输入特征图x进行reshape后,最后一维是高和宽的乘积,于是q[.., :N] * in_w + q[..., N:]就能够和reshape后x的索引对应起来(将h,w二维展平为h*w一维后对应的索引,相当于把多行合并为一行)。
注:以下第2行注释q的shape有误,应该是(b,out_h,out_w,2N),抱歉!
最后使用Pytorch内置的gather()方法就可以将对应位置的张量值取出。
附:gather(dim=-1)方法的效果为 x_offset[i][j][k] = x[i][j][ index[i][j][k] ]
知道了如何得到领域点的特征值,之前的困惑也烟消云散,此刻终于可以进行真正的卷积操作了!是不是很兴奋~
首先,如果设置了modulation,那么就对邻域内每个位置乘上对应的权重,m就是m_conv的输出,注意要将权重m的形状变为和x_offset一致。另外,还要对x_offset进行reshape操作,在这一节开头时谈到self.conv的步长设置时就提到了,最终进行卷积时的特征图宽、高是输出特征图宽、高的卷积核大小倍。
Deformable Conv boy 也是够“尽心尽力”,连最后一点精华也要揭秘,他还秀出是如何对x_offset进行reshape的:
(上图中其实torch.cat(.., dim=-2)就可以完事了,不需要最后一句)
但是他不再多言,最后这部分留给大家细细品味,他已算是仁至义尽,台下众人也都服了。
@最后
每当写文章涉及到源码解析的内容时我都觉得真心不容易,有些代码的实现很难用文字去叙述明白(所谓“道可道,非常道”..),几乎每句话我都会斟酌好几番才输出,写完后也会反复阅读是否有不妥,我尽量让自己在保证准确性的同时能叙述得简单易懂。另外,为了避免文章读起来晦涩无趣,我也会加入一些网络用词以营造轻松活跃的气氛。
尽管代码解析的内容看起来会有点绕,但我还是愿意在文章中将这部分内容囊括进来,因为只讲原理实在太空洞,只有结合了代码进行实践,才算是把知识点落实了下去。