DCN可形变卷积实现1:Python实现
DCN可形變卷積實(shí)現(xiàn)1:Python實(shí)現(xiàn)
我們會(huì)先用純 Python 實(shí)現(xiàn)一個(gè) Pytorch 版本的 DCN ,然后實(shí)現(xiàn)其 C++/CUDA 版本。
本文主要關(guān)注 DCN 可形變卷積的代碼實(shí)現(xiàn),不會(huì)過(guò)多的介紹其思想,如有興趣,請(qǐng)參考論文原文:
Deformable Convolutional Networks
Deformable ConvNets v2: More Deformable, Better Results
DCN簡(jiǎn)介
考慮到傳統(tǒng)卷積必須是方方正正的 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?)=pn?∈R∑?w(pn?)?x(p0?+pn?)
作者認(rèn)為這個(gè)感受野太規(guī)則,無(wú)法很好地捕捉特殊形狀的特征,因此在其基礎(chǔ)上加了偏置:
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?)=pn?∈R∑?w(pn?)?x(p0?+pn?+Δpn?)
使得模型能夠根據(jù)輸入計(jì)算偏移量,自己選擇對(duì)哪些位置進(jìn)行卷積計(jì)算,而不用必須是正方形的樣子。
如上圖所示,傳統(tǒng)的卷積輸入只能是圖 (a) 中的九個(gè)綠點(diǎn),而在加上偏移量之后,皆可以四處飛,比如飛到圖 (bcd) 中藍(lán)點(diǎn)的位置。
而 DCNv2 則在此基礎(chǔ)上又為每個(gè)位置乘了一個(gè)可學(xué)習(xí)的權(quán)重:
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?)=pn?∈R∑?w(pn?)?x(p0?+pn?+Δpn?)?Δmn?
由于網(wǎng)絡(luò)學(xué)習(xí)出的偏移量通常是小數(shù),因此下面會(huì)用到雙線性插值(下面會(huì)有圖示),這里先把原文中的公式給出來(lái):
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)=q∑?G(q,p)?x(q)
這里 p=(p0+pn+Δpn)\mathbf{p}=(\mathbf{p}_0+\mathbf{p}_n+\Delta\mathbf{p}_n)p=(p0?+pn?+Δpn?) 表示任意位置(可以是小數(shù))坐標(biāo),而 q\mathbf{q}q 是枚舉特征圖 x\mathbf{x}x 中所有整數(shù)空間位置,G(?,?)G(\cdot,\cdot)G(?,?) 就是雙線性插值,注意這里的 GGG 是兩個(gè)維度(x,y)的,拆分為兩個(gè)單維度的話,就是:
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,1?∣a?b∣) 。
給出公式一方面是讓讀者了解具體算法,更重要的一點(diǎn)是我們參考的 DCN 的 Pytorch 實(shí)現(xiàn)代碼中變量的命名是與原文公式對(duì)應(yīng)的,因此公式列在這里方便讀者下面看代碼的時(shí)候可以回頭看一下各個(gè)變量對(duì)應(yīng)的是算法公式中的哪一項(xiàng)。
純Python實(shí)現(xiàn)
我們先來(lái)看一下Pytorch版本的實(shí)現(xiàn),來(lái)更好地理解 DCN 可形變卷積的做法,然后用 C++/CUDA 實(shí)現(xiàn)高性能版本。本文參考的 Python 實(shí)現(xiàn)是:https://github.com/4uiiurz1/pytorch-deform-conv-v2/blob/master/deform_conv_v2.py 。
本小節(jié)參考博文: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)這里重點(diǎn)關(guān)注 self.p_conv 和 self.m_conv ,是這兩個(gè)卷積完成了對(duì)偏移量 offset 的學(xué)習(xí),而 self.conv 是確在定偏移后的位置之后,最終進(jìn)行計(jì)算的卷積。
(關(guān)于這里的 modulation 參數(shù),如注釋所言,如果為 True ,就是一個(gè)模塊化的 DCN,即 DCNv2。)
具體來(lái)看這三個(gè)卷積及其參數(shù):
-
self.conv:這是負(fù)責(zé)進(jìn)行最終計(jì)算的卷積??尚巫兙矸e DCN 雖然進(jìn)行了形變,但是這是卷積輸入中空間像素的位置有了偏移,而輸入輸出的尺寸還是不變的,因此,輸入卷積的位置確定之后,最終負(fù)責(zé)完成卷積計(jì)算的 self.conv 的各個(gè)參數(shù)(輸入輸出通道數(shù)inc, outc、卷積核大小kernel_size、步長(zhǎng)stride、填充padding等)就是我們整個(gè) DCN 的對(duì)應(yīng)參數(shù)參數(shù)。
-
self.p_conv:該卷積操作負(fù)責(zé)計(jì)算偏移量。在卷積中,共有 kernel_size * kernel_size 個(gè)位置的像素需要參與計(jì)算,因此我們要計(jì)算出他們的偏移量,而每個(gè)位置都有寬、高兩個(gè)方向的偏移量,故該卷積輸出的通道數(shù)是 2 * kernel_size * kernel_size ,其他參數(shù)保持一致。
-
self.m_conv:該卷積操作負(fù)責(zé)計(jì)算卷積核每個(gè)位置的權(quán)重。其輸出通道數(shù)為位置數(shù),即 kernel_size * kernel_size ,其他參數(shù)保持一致,注意這個(gè)加權(quán)的想法是 DCNv2 中的。
forward
看過(guò) __init__ 函數(shù)之后,我們可以來(lái)看 forward 函數(shù):
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 的通道數(shù)除以2,就是卷積要處理的位置的個(gè)數(shù)(即 kernal_size * kernel_size)。
整個(gè) forward 函數(shù)的流程:
首先通過(guò)上面介紹的 p_conv 和 v_conv 計(jì)算出偏移量 offset 和加權(quán)的權(quán)重m(如果有)。
比較關(guān)鍵的是這里的 self._get_p 函數(shù),該函數(shù)通過(guò)上面計(jì)算出的 offset,去得到輸入到卷積的具體位置,即公式中的:
p0+pn+Δpn\mathbf{p}_0+\mathbf{p}_n+\Delta\mathbf{p}_n p0?+pn?+Δpn?
關(guān)于這個(gè)函數(shù),我們會(huì)在下一小節(jié)詳細(xì)介紹。由于我們現(xiàn)在先過(guò)整個(gè)流程,只需要知道該函數(shù)通過(guò) p_conv 卷積計(jì)算出的 offset,得到了要輸入最終卷積的位置 p。p 是一個(gè)形狀為 (bs,2?N,h,w)(bs,2*N,h,w)(bs,2?N,h,w) 的張量。
拿到 p 之后的問(wèn)題是我們得到的肯定是一個(gè)浮點(diǎn)類型,即小數(shù),但是像素的坐標(biāo)肯定是整型,所以,這里我們需要做一個(gè)雙線性插值。雙線性插值的思想也很直接,就是將某個(gè)浮點(diǎn)坐標(biāo)的左上、左下、右上、右下四個(gè)位置的像素值按照與該點(diǎn)的距離計(jì)算加權(quán)和,作為該點(diǎn)處的像素值??蓞⒖枷聢D,也可參考博客圖像預(yù)處理之warpaffine與雙線性插值及其高性能實(shí)現(xiàn),后半部分有對(duì)雙線性插值的講解與 Python 實(shí)現(xiàn)。
這里的 lt, rb, lb, rt 分別代表左上,右下,左下,右上。
現(xiàn)在我們通過(guò)雙線性插值拿到了每個(gè)位置的坐標(biāo),下一步就是根據(jù)坐標(biāo)去取到對(duì)應(yīng)位置的像素值,這在代碼中由 self._get_x_q 實(shí)現(xiàn),會(huì)在下面的小節(jié)介紹。
這個(gè)時(shí)候如果有權(quán)重的話,要計(jì)算出 m,乘到 x_offset 上。
這時(shí)得到的 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,因此這里還有一個(gè) reshape 的操作,由 self._reshape_x_offset 實(shí)現(xiàn)。
至此,我們終于得到了想要的 x_offset,接下來(lái)就將它送入 self.conv 進(jìn)行卷積計(jì)算并返回結(jié)果即可。
_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我們來(lái)看一下如何將 offset 傳入 self._get_p 獲得最終的 p,該函數(shù)會(huì)分別調(diào)用 self._get_p_0 和 self._get_p_n 來(lái)分別獲得 p_0 和 p_n,分別是卷積核的中心坐標(biāo)和相對(duì)坐標(biāo),對(duì)應(yīng)到公式中的 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?)=pn?∈R∑?w(pn?)?x(p0?+pn?+Δpn?)
關(guān)于 p_0 和 p_n 具體是什么東西其實(shí)很好理解,畫(huà)個(gè)小圖就明白了,以 kernel_size = 3 的卷積為例,中心位置在全圖中的坐標(biāo)就是 p_0,中心位置的相對(duì)坐標(biāo)就是 p_n=(0,0),左上角的 p_n=(-1,-1),右下角的 p_n=(1,1) 其他位置以此類推。常規(guī)的卷積就只有 pn+p0\mathbf{p}_n+\mathbf{p}_0pn?+p0? ,輸入就是只能在上圖中的九個(gè)格子中,而 DCN 加入 Δpn\Delta\mathbf{p}_nΔpn? 之后,就可以四處飛啦。但是四處飛,也是要在 pn+p0\mathbf{p}_n+\mathbf{p}_0pn?+p0? 的基礎(chǔ)上再加上偏移量來(lái)計(jì)算具體的位置。所以我們先要獲得 p_0 和 p_n。
當(dāng)然,p_0 和 p_n 都是固定的、不需要學(xué)習(xí)的、而且是很規(guī)則的,因此獲取他們只需要根據(jù) kernel_size 和位置 h, w (僅 p_0 需要)來(lái)計(jì)算就好了。這里代碼實(shí)現(xiàn)中就是用 torch.arange 和 torch.meshgrid 將想要的 p_0 和 p_n,計(jì)算出來(lái)。
然后 p = p_0 + p_n + offset(對(duì)應(yīng)公式),得到尺寸為 (bs,2?N,h,w)(bs, 2*N, h, w)(bs,2?N,h,w) 的 p。
_get_x_q
_get_x_q 函數(shù)是根據(jù)計(jì)算出的位置坐標(biāo),得到該位置的像素值。
再提醒一下,我們參考的 DCN 的 Pytorch 實(shí)現(xiàn)代碼中變量的命名是與原文公式對(duì)應(yīng)的,如果有變量含義不明確的,可以回上面看看公式,對(duì)應(yīng)代碼變量名理解。
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
我們?cè)谌⊥晗袼刂抵蟮玫降?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,因此這里還有一個(gè) 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小結(jié)
至此,我們已經(jīng)使用 Pytorch 實(shí)現(xiàn)了純 Python 的 DCN 卷積結(jié)構(gòu),但是,如此實(shí)現(xiàn)由于不是原生的 C++/CUDA 算子,而且最后的 reshape 操作雖然比較巧妙,但其實(shí)空間冗余比較大,和原文作者的 cuda 版本內(nèi)存占用量差了10幾倍。這個(gè)是因?yàn)樵?im2col 上直接操作可以去掉很冗余。下面一篇我們會(huì)再介紹一個(gè) C++/CUDA 實(shí)現(xiàn)的 DCN。
Ref
- deformable convolution可變形卷積(4uiiurz1-pytorch版)源碼分析
- 圖像預(yù)處理之warpaffine與雙線性插值及其高性能實(shí)現(xiàn)
總結(jié)
以上是生活随笔為你收集整理的DCN可形变卷积实现1:Python实现的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 做软件测试必须了解的7个常用术语
- 下一篇: 综述(十)北京在安全测试示范区上的政策与