我卷我自己——cvpr2021:Involution
引言
本文重新回顧了常規(guī)卷積的設(shè)計(jì),其具有兩個(gè)重要性質(zhì),一個(gè)是空間無關(guān)性,比如3x3大小的卷積核是以滑窗的形式,滑過特征圖每一個(gè)像素(即我們所說的參數(shù)共享)。另外一個(gè)是頻域特殊性,體現(xiàn)在卷積核在每個(gè)通道上的權(quán)重是不同的。
我們對(duì)以上的設(shè)計(jì)原則進(jìn)行了"反轉(zhuǎn)",設(shè)計(jì)了一種 involution(內(nèi)卷???)的操作,一方面能降低模型的參數(shù)量,另一方面也能提升模型性能,還能和最近很火的自注意力機(jī)制聯(lián)系起來。該模塊在各大圖像任務(wù)上都有不錯(cuò)的性能提升。
簡單回顧卷積
最初的神經(jīng)網(wǎng)絡(luò)都是由一層層全連接層網(wǎng)絡(luò)疊加起來,對(duì)于簡單的任務(wù)來說參數(shù)量還好。但是對(duì)于圖像任務(wù),動(dòng)輒幾百上千的像素,則全連接層的參數(shù)量會(huì)十分巨大。如果是全連接層處理二維圖像,那么大致形式如下

而卷積神經(jīng)網(wǎng)絡(luò)考慮了局部連接性,只考慮了局部的像素,從而讓參數(shù)量大大減少,形式如下

由于常規(guī)卷積核是對(duì)所有輸入通道進(jìn)行計(jì)算,在起初的一些低算力設(shè)備上計(jì)算損耗還是很大,Alexnet提出分組卷積,對(duì)輸入通道進(jìn)行分組,然后單獨(dú)卷積,形式如下

而谷歌提出的Depthwise Conv則將分組卷積推向了極端——分組數(shù)是輸入通道數(shù)目,即每個(gè)輸入通道單獨(dú)卷積,形式如下

卷積核形式的演進(jìn)還是基于通道做的,最基礎(chǔ)的兩個(gè)性質(zhì)空間無關(guān)性和頻域特殊性依舊沒有改變。而Involution操作給出了一個(gè)不同的思路。
Involution的設(shè)計(jì)原則
Involution的設(shè)計(jì)原則就是顛倒常規(guī)卷積核的兩個(gè)設(shè)計(jì)原則,即從空間無關(guān)性,頻域特殊性轉(zhuǎn)變成空間特殊性,頻域無關(guān)性

卷積神經(jīng)網(wǎng)絡(luò)存在下采樣層,導(dǎo)致各個(gè)階段的特征圖長寬會(huì)變化。既然要與空間域聯(lián)系起來,那么第一個(gè)問題是如何參數(shù)化一個(gè)Invotion的卷積核。一個(gè)很自然的想法就是設(shè)置一個(gè)函數(shù) ,讓他根據(jù)輸入的張量,輸出一個(gè)跟特征圖長寬相關(guān)的張量,再把它作為卷積核。
該函數(shù)公式寫為
在實(shí)際的代碼中,作者用一個(gè)類似BottleNeck的形式,可以通過控制縮放比例調(diào)整參數(shù)量,用兩個(gè)1x1卷積對(duì)通道進(jìn)行縮放,最后一個(gè)卷積輸出通道數(shù)為(K * K * Groups),其中K代表后續(xù)involution卷積核大小,Groups代表involution操作的分組數(shù)。(如果遇到需要下采樣的情況,則接一個(gè)步長為2的平均池化層。),最后我們可以得到一個(gè)形狀為N*(K * K * Groups)HW的張量,下面是這部分操作的代碼
...
reduction_ratio = 4
self.group_channels = 16
self.groups = self.channels // self.group_channels
self.conv1 = ConvModule(
in_channels=channels,
out_channels=channels // reduction_ratio,
kernel_size=1,
conv_cfg=None,
norm_cfg=dict(type='BN'),
act_cfg=dict(type='ReLU'))
self.conv2 = ConvModule(
in_channels=channels // reduction_ratio,
out_channels=kernel_size**2 * self.groups,
kernel_size=1,
stride=1,
conv_cfg=None,
norm_cfg=None,
act_cfg=None)
def forward(self, x):
weight = self.conv2(self.conv1(x if self.stride == 1 else self.avgpool(x)))
...
下面就會(huì)拿這個(gè)weight來做當(dāng)作一個(gè)卷積核,對(duì)x卷積。
讀到這里可能會(huì)比較奇怪,為什么卷積核形狀長這樣,我們常見的卷積核應(yīng)該是(C_in, C_out, K, K)。這其實(shí)也是這篇工作的關(guān)鍵之處,上面我們提到他這里注重的是頻域無關(guān)性,空間特殊性。因此它分組卷積的做法是 每一組內(nèi)的特征圖共享一個(gè)卷積核的參數(shù),但是 同一組內(nèi),不同空間位置,使用的是不同的卷積核。
原文是 an involution kernel located at the corresponding coordinate (i, j), but shared over the channels.
這段比較費(fèi)解,我畫了一個(gè)簡單的示意圖

為了方便演示,這里設(shè)置N為1,特征圖通道為16個(gè),分組數(shù)為4,ksize=3
首先輸入特征圖被分為四組,每組有4個(gè)特征圖 之前經(jīng)過兩次1x1卷積,我們得到了involution所需的權(quán)重,形狀為(N, Groups, ksize * ksize, H, W), 在該例子中為(1, 4, 3 * 3, H, W) ,那么分配給每個(gè)組的,就是一個(gè)(1, 3 * 3, H, W),不考慮Batchsize的話,那么每組就有H * W個(gè)3x3的卷積核。
在通道維上,每組的特征圖共享一個(gè)卷積核,而在同一組的不同空間位置,使用不同的卷積核。
處理完后,再把各組的結(jié)果拼接回來,下面是完整的involution操作代碼
import torch.nn as nn
from mmcv.cnn import ConvModule
class involution(nn.Module):
def __init__(self,
channels,
kernel_size,
stride):
super(involution, self).__init__()
self.kernel_size = kernel_size
self.stride = stride
self.channels = channels
reduction_ratio = 4
self.group_channels = 16
self.groups = self.channels // self.group_channels
self.conv1 = ConvModule(
in_channels=channels,
out_channels=channels // reduction_ratio, # 通過reduction_ratio控制參數(shù)量
kernel_size=1,
conv_cfg=None,
norm_cfg=dict(type='BN'),
act_cfg=dict(type='ReLU'))
self.conv2 = ConvModule(
in_channels=channels // reduction_ratio,
out_channels=kernel_size**2 * self.groups,
kernel_size=1,
stride=1,
conv_cfg=None,
norm_cfg=None,
act_cfg=None)
if stride > 1:
# 如果步長大于1,則加入一個(gè)平均池化
self.avgpool = nn.AvgPool2d(stride, stride)
self.unfold = nn.Unfold(kernel_size, 1, (kernel_size-1)//2, stride)
def forward(self, x):
weight = self.conv2(self.conv1(x if self.stride == 1 else self.avgpool(x))) # 得到involution所需權(quán)重
b, c, h, w = weight.shape
weight = weight.view(b, self.groups, self.kernel_size**2, h, w).unsqueeze(2) # 將權(quán)重reshape成 (B, Groups, 1, kernelsize*kernelsize, h, w)
out = self.unfold(x).view(b, self.groups, self.group_channels, self.kernel_size**2, h, w) # 將輸入reshape
out = (weight * out).sum(dim=3).view(b, self.channels, h, w) # 求和,reshape回NCHW形式
return out
實(shí)驗(yàn)結(jié)果
作者基于ResNet模型,將Bottleneck模塊的中間卷積塊,替換成7x7大小的involution操作。改進(jìn)后的模型稱為RedNet

可以看到實(shí)驗(yàn)結(jié)果還是很不錯(cuò)的,不僅壓縮了網(wǎng)絡(luò)參數(shù),在中小網(wǎng)絡(luò)也能提升模型精度。(但我更好奇的是實(shí)際運(yùn)行的速度,如每秒能處理多少圖片),在其他圖像任務(wù)上也有提升,這里就不放出來了,有興趣的讀者可以去讀下原文。
對(duì)于Involution操作的分組數(shù),Kernel大小,作者也做了相關(guān)消融實(shí)驗(yàn)

可以看到從3x3到7x7,精度是穩(wěn)定提高的,但是加到9x9以后提升有限。為了平衡參數(shù)量和精度,作者選擇了7x7大小的Kernel,分組通道數(shù)為16,生成Kernel的卷積模塊里,reduction參數(shù)設(shè)為4。
總結(jié)
這篇論文還是挺有意思的,作者陣容也很豪華,其中包括SENet的作者HuJie?,F(xiàn)在的卷積核改進(jìn)基本都是從通道維度去做,而這篇工作顛覆了這種思想,跟常規(guī)卷積反著來,做了一個(gè)自己卷自己的內(nèi)卷操作。
論文還提到了這個(gè)操作和自注意力機(jī)制的關(guān)系,但是筆者并沒有讀太懂,就沒有闡述(還望相關(guān)作者解答下)。作者還留了一些坑,我未來也很期待NAS在該模塊上更多的探索。
歡迎關(guān)注GiantPandaCV, 在這里你將看到獨(dú)家的深度學(xué)習(xí)分享,堅(jiān)持原創(chuàng),每天分享我們學(xué)習(xí)到的新鮮知識(shí)。( ? ?ω?? )?
有對(duì)文章相關(guān)的問題,或者想要加入交流群,歡迎添加BBuf微信:
為了方便讀者獲取資料以及我們公眾號(hào)的作者發(fā)布一些Github工程的更新,我們成立了一個(gè)QQ群,二維碼如下,感興趣可以加入。
