作者丨皮特潘
編輯丨極市平臺
前言
本文盤點一些CNN網絡中設計比較精巧而又實用的“插件”。所謂“插件”,就是不改變網絡主體結構, 可以很容易嵌入到主流網絡當中,提高網絡提取特征的能力,能夠做到plug-and-play。網絡也有很多類似盤點工作,都宣稱所謂的即插即用、無痛漲點。不過根據筆者經驗和收集,發現很多插件都是不實用、不通用、甚至不work的,于是有了這一篇。
首先,我的認識是:既然是“插件”,就要是錦上添花的,又容易植入,容易落地的,真正的即插即用。本文盤點的“插件”,在很多SOTA網絡中會看到它們的影子。是值得推廣的良心“插件”,真正能做到plug-and-play。總之一句話,就是能夠work的“插件”。很多“插件”都為提升CNN能力而推出的,例如平移、旋轉、scale等變性能力,多尺度特征提取能力,感受野等能力,感知空間位置能力等等。
入圍名單:STN、ASPP、Non-local、SE、CBAM、DCNv1&v2、CoordConv、Ghost、BlurPool、RFB、ASFF
STN
出自論文:Spatial Transformer Networks
論文鏈接:
https://arxiv.org/pdf/1506.02025.pdf
核心解析:
在OCR等任務中,你會經常看到它的身影。對于CNN網絡,我們希望其具有對物體的姿態、位置等有一定的不變性。即在測試集上可以適應一定的姿態、位置的變化。不變性或等變性可以有效提高模型泛化能力。雖然CNN使用sliding-window卷積操作,在一定程度上具有平移不變性。但很多研究發現,下采樣會破壞網絡的平移不變性。所以可以認為網絡的不變性能力非常弱,更不用說旋轉、尺度、光照等不變性。一般我們利用數據增強來實現網絡的“不變性”。
本文提出STN模塊,顯式將空間變換植入到網絡當中,進而提高網絡的旋轉、平移、尺度等不變性。可以理解為“對齊”操作。STN的結構如上圖所示,每一個STN模塊由Localisation net,Grid generator和Sampler三部分組成。Localisation net用于學習獲取空間變換的參數,就是上式中的 六個參數。Grid generator用于坐標映射。Sampler用于像素的采集,是利用雙線性插值的方式進行。
STN的意義是能夠把原始的圖像糾正成為網絡想要的理想圖像,并且該過程為無監督的方式進行,也就是變換參數是自發學習獲取的,不需要標注信息。該模塊是一個獨立模塊,可以在CNN的任何位置插入。符合本次“插件”的盤點要求。
核心代碼:
class SpatialTransformer(nn.Module):
def __init__(self, spatial_dims):
super(SpatialTransformer, self).__init__()
self._h, self._w = spatial_dims
self.fc1 = nn.Linear(32*4*4, 1024) # 可根據自己的網絡參數具體設置
self.fc2 = nn.Linear(1024, 6)
def forward(self, x):
batch_images = x #保存一份原始數據
x = x.view(-1, 32*4*4)
# 利用FC結構學習到6個參數
x = self.fc1(x)
x = self.fc2(x)
x = x.view(-1, 2,3) # 2x3
# 利用affine_grid生成采樣點
affine_grid_points = F.affine_grid(x, torch.Size((x.size(0), self._in_ch, self._h, self._w)))
# 將采樣點作用到原始數據上
rois = F.grid_sample(batch_images, affine_grid_points)
return rois, affine_grid_points
ASPP
插件全稱:atrous spatial pyramid pooling
出自論文:DeepLab: Semantic Image Segmentation with Deep Convolutional Nets, Atrous Conv
論文鏈接:
https://arxiv.org/pdf/1606.00915.pdf
核心解析:
本插件是帶有空洞卷積的空間金字塔池化模塊,主要是為了提高網絡的感受野,并引入多尺度信息而提出的。我們知道,對于語義分割網絡,通常面臨是分辨率較大的圖片,這就要求我們的網絡有足夠的感受野來覆蓋到目標物體。對于CNN網絡基本是靠卷積層的堆疊加上下采樣操作來獲取感受野的。本文的該模塊可以在不改變特征圖大小的同時控制感受野,這有利于提取多尺度信息。其中rate控制著感受野的大小,r越大感受野越大。
ASPP主要包含以下幾個部分:1. 一個全局平均池化層得到image-level特征,并進行1X1卷積,并雙線性插值到原始大小;2. 一個1X1卷積層,以及三個3X3的空洞卷積;3. 將5個不同尺度的特征在channel維度concat在一起,然后送入1X1的卷積進行融合輸出。
核心代碼:
class ASPP(nn.Module):
def __init__(self, in_channel=512, depth=256):
super(ASPP,self).__init__()
self.mean = nn.AdaptiveAvgPool2d((1, 1))
self.conv = nn.Conv2d(in_channel, depth, 1, 1)
self.atrous_block1 = nn.Conv2d(in_channel, depth, 1, 1)
# 不同空洞率的卷積
self.atrous_block6 = nn.Conv2d(in_channel, depth, 3, 1, padding=6, dilation=6)
self.atrous_block12 = nn.Conv2d(in_channel, depth, 3, 1, padding=12, dilation=12)
self.atrous_block18 = nn.Conv2d(in_channel, depth, 3, 1, padding=18, dilation=18)
self.conv_1x1_output = nn.Conv2d(depth * 5, depth, 1, 1)
def forward(self, x):
size = x.shape[2:]
# 池化分支
image_features = self.mean(x)
image_features = self.conv(image_features)
image_features = F.upsample(image_features, size=size, mode='bilinear')
# 不同空洞率的卷積
atrous_block1 = self.atrous_block1(x)
atrous_block6 = self.atrous_block6(x)
atrous_block12 = self.atrous_block12(x)
atrous_block18 = self.atrous_block18(x)
# 匯合所有尺度的特征
x = torch.cat([image_features, atrous_block1, atrous_block6,atrous_block12, atrous_block18], dim=1)
# 利用1X1卷積融合特征輸出
x = self.conv_1x1_output(x)
return x
Non-local
出自論文:Non-local Neural Networks
論文鏈接:
https://arxiv.org/abs/1711.07971
核心解析:
Non-Local是一種attention機制,也是一個易于植入和集成的模塊。Local主要是針對感受野(receptive field)來說的,以CNN中的卷積操作和池化操作為例,它的感受野大小就是卷積核大小,而我們常用3X3的卷積層進行堆疊,它只考慮局部區域,都是local的運算。不同的是,non-local操作感受野可以很大,可以是全局區域,而不是一個局部區域。捕獲長距離依賴(long-range dependencies),即如何建立圖像上兩個有一定距離的像素之間的聯系,是一種注意力機制。所謂注意力機制就是利用網絡生成saliency map,注意力對應的是顯著性區域,是需要網絡重點關注的區域。
- 首先分別對輸入的特征圖進行 1X1的卷積來壓縮通道數,得到 特征。
- 通過reshape操作,轉化三個特征的維度,然后對 進行矩陣乘操作,得到類似協方差矩陣, 這一步為了計算出特征中的自相關性,即得到每幀中每個像素對其他所有幀所有像素的關系。
- 然后對自相關特征進行 Softmax 操作,得到0~1的weights,這里就是我們需要的 Self-attention系數。
- 最后將 attention系數,對應乘回特征矩陣g上,與原輸入 feature map X 殘差相加輸出即可。
這里我們結合一個簡單例子理解一下,假設g為(我們暫時不考慮batch和channel維度):
g = torch.tensor([[1, 2],
[3, 4]).view(-1, 1).float()
為:
theta = torch.tensor([2, 4, 6, 8]).view(-1, 1)
為:
phi = torch.tensor([7, 5, 3, 1]).view(1, -1)
那么, 和 矩陣相乘如下:
tensor([[14., 10., 6., 2.],
[28., 20., 12., 4.],
[42., 30., 18., 6.],
[56., 40., 24., 8.]])
進過softmax(dim=-1)后如下,每一行代表著g里面的元素的重要程度,每一行前面的值比較大,因此希望多“注意”到g前面的元素,也就是1比較重要一點。或者這樣理解:注意力矩陣代表著g中每個元素和其他元素的依賴程度。
tensor([[9.8168e-01, 1.7980e-02, 3.2932e-04, 6.0317e-06],
[9.9966e-01, 3.3535e-04, 1.1250e-07, 3.7739e-11],
[9.9999e-01, 6.1442e-06, 3.7751e-11, 2.3195e-16],
[1.0000e+00, 1.1254e-07, 1.2664e-14, 1.4252e-21]])
注意力作用上之后,整體值向原始g中的值都向1靠攏:
tensor([[1.0187, 1.0003],
[1.0000, 1.0000]])
核心代碼:
class NonLocal(nn.Module):
def __init__(self, channel):
super(NonLocalBlock, self).__init__()
self.inter_channel = channel // 2
self.conv_phi = nn.Conv2d(channel, self.inter_channel, 1, 1,0, False)
self.conv_theta = nn.Conv2d(channel, self.inter_channel, 1, 1,0, False)
self.conv_g = nn.Conv2d(channel, self.inter_channel, 1, 1, 0, False)
self.softmax = nn.Softmax(dim=1)
self.conv_mask = nn.Conv2d(self.inter_channel, channel, 1, 1, 0, False)
def forward(self, x):
# [N, C, H , W]
b, c, h, w = x.size()
# 獲取phi特征,維度為[N, C/2, H * W],注意是要保留batch和通道維度的,是在HW上進行的
x_phi = self.conv_phi(x).view(b, c, -1)
# 獲取theta特征,維度為[N, H * W, C/2]
x_theta = self.conv_theta(x).view(b, c, -1).permute(0, 2, 1).contiguous()
# 獲取g特征,維度為[N, H * W, C/2]
x_g = self.conv_g(x).view(b, c, -1).permute(0, 2, 1).contiguous()
# 對phi和theta進行矩陣乘,[N, H * W, H * W]
mul_theta_phi = torch.matmul(x_theta, x_phi)
# softmax拉到0~1之間
mul_theta_phi = self.softmax(mul_theta_phi)
# 與g特征進行矩陣乘運算,[N, H * W, C/2]
mul_theta_phi_g = torch.matmul(mul_theta_phi, x_g)
# [N, C/2, H, W]
mul_theta_phi_g = mul_theta_phi_g.permute(0, 2, 1).contiguous().view(b, self.inter_channel, h, w)
# 1X1卷積擴充通道數
mask = self.conv_mask(mul_theta_phi_g)
out = mask + x # 殘差連接
return out
SE
出自論文:Squeeze-and-Excitation Networks
論文鏈接:
https://arxiv.org/pdf/1709.01507.pdf
核心解析:
本文是ImageNet最后一屆比賽的冠軍作品,你會在很多經典網絡結構中看到它的身影,例如Mobilenet v3。其實是一種通道注意力機制。由于特征壓縮和FC的存在,其捕獲的通道注意力特征是具有全局信息的。本文提出了一種新的結構單元——“Squeeze-and Excitation(SE)”模塊,可以自適應的調整各通道的特征響應值,對通道間的內部依賴關系進行建模。有以下幾個步驟:
- Squeeze: 沿著空間維度進行特征壓縮,將每個二維的特征通道變成一個數,是具有全局的感受野。
- Excitation: 每個特征通道生成一個權重,用來代表該特征通道的重要程度。
- Reweight:將Excitation輸出的權重看做每個特征通道的重要性,通過相乘的方式作用于每一個通道上。
核心代碼:
class SE_Block(nn.Module):
def __init__(self, ch_in, reduction=16):
super(SE_Block, self).__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1) # 全局自適應池化
self.fc = nn.Sequential(
nn.Linear(ch_in, ch_in // reduction, bias=False),
nn.ReLU(inplace=True),
nn.Linear(ch_in // reduction, ch_in, bias=False),
nn.Sigmoid()
)
def forward(self, x):
b, c, _, _ = x.size()
y = self.avg_pool(x).view(b, c) # squeeze操作
y = self.fc(y).view(b, c, 1, 1) # FC獲取通道注意力權重,是具有全局信息的
return x * y.expand_as(x) # 注意力作用每一個通道上
CBAM
出自論文:CBAM: Convolutional Block Attention Module
論文鏈接:
https://openaccess.thecvf.com/content_ECCV_2018/papers/Sanghyun_Woo_Convolutional_Block_Attention_ECCV_2018_paper.pdf
核心解析:
SENet在feature map的通道上進行attention權重獲取,然后與原來的feature map相乘。這篇文章指出,該種attention方法法只關注了通道層面上哪些層會具有更強的反饋能力,但是在空間維度上并不能體現出attention。CBAM作為本文的亮點,將attention同時運用在channel和spatial兩個維度上, CBAM與SE Module一樣,可以嵌入在大部分的主流網絡中,在不顯著增加計算量和參數量的前提下能提升模型的特征提取能力。
通道注意力: 如上圖所示,輸入是一個 H×W×C 的特征F,我們先分別進兩個空間的全局平均池化和最大池化得到 兩個 1×1×C 的通道描述。再將它們分別送進一個兩層的神經網絡,第一層神經元個數為 C/r,激活函數為 Relu,第二層神經元個數為 C。注意,這個兩層的神經網絡是共享的。 然后,再將得到的兩個特征相加后經過一個 Sigmoid 激活函數得到權重系數 Mc。最后,拿權重系數和 原來的特征 F 相乘即可得到縮放后的新特征。 偽代碼:
def forward(self, x):
# 利用FC獲取全局信息,和Non-local的矩陣相乘本質上式一樣的
avg_out = self.fc2(self.relu1(self.fc1(self.avg_pool(x))))
max_out = self.fc2(self.relu1(self.fc1(self.max_pool(x))))
out = avg_out + max_out
return self.sigmoid(out)
空間注意力: 與通道注意力相似,給定一個 H×W×C 的特征 F‘,我們先分別進行一個通道維度的平均池化和最大池化得到兩個 H×W×1 的通道描述,并將這兩個描述按照通道拼接在一起。然后,經過一個 7×7 的卷積層, 激活函數為 Sigmoid,得到權重系數 Ms。最后,拿權重系數和特征 F’ 相乘即可得到縮放后的新特征。 偽代碼:
def forward(self, x):
# 這里利用池化獲取全局信息
avg_out = torch.mean(x, dim=1, keepdim=True)
max_out, _ = torch.max(x, dim=1, keepdim=True)
x = torch.cat([avg_out, max_out], dim=1)
x = self.conv1(x)
return self.sigmoid(x)
DCN v1&v2
插件全稱:Deformable Convolutional
出自論文:
v1: [Deformable Convolutional Networks]
https://arxiv.org/pdf/1703.06211.pdf
v2: [Deformable ConvNets v2: More Deformable, Better Results]
https://arxiv.org/pdf/1811.11168.pdf
核心解析:
變形卷積可以看作變形+卷積兩個部分,因此可以當作插件使用。在各大主流檢測網絡中,變形卷積真是漲點神器,網上解讀也非常之多。和傳統的固定窗口的卷積相比,變形卷積可以有效地對幾何圖形,因為它的“局部感受野”是可學習的,面向全圖的。這篇論文同時提出了deformable ROI pooling,這兩個方法都是增加額外偏移量的空間采樣位置,不需要額外的監督,是自監督的過程。
如上圖所示,a為不同的卷積,b為變形卷積,深色的點為卷積核實際采樣的位置,和“標準的”位置有一定的偏移。c和d為變形卷積的特殊形式,其中c為我們常見到的空洞卷積,d為具有學習旋轉特性的卷積,也具備提升感受野的能力。
變形卷積和STN過程非常類似,STN是利用網絡學習出空間變換的6個參數,對特征圖進行整體變換,旨在增加網絡對形變的提取能力。DCN是利用網絡學習數整圖offset,比STN的變形更“全面一點”。STN是仿射變換,DCN是任意變換。公式不貼了,可以直接看代碼實現過程。
變形卷積具有V1和V2兩個版本,其中V2是在V2的基礎上進行改進,除了采樣offset,還增加了采樣權重。V2認為3X3采樣點也應該具有不同的重要程度,因此該處理方法更具有靈活性和擬合能力。
核心代碼:
def forward(self, x):
# 學習出offset,包括x和y兩個方向,注意是每一個channel中的每一個像素都有一個x和y的offset
offset = self.p_conv(x)
if self.v2: # V2的時候還會額外學習一個權重系數,經過sigmoid拉到0和1之間
m = torch.sigmoid(self.m_conv(x))
# 利用offset對x進行插值,獲取偏移后的x_offset
x_offset = self.interpolate(x,offset)
if self.v2: # V2的時候,將權重系數作用到特征圖上
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 *= m
out = self.conv(x_offset) # offset作用后,在進行標準的卷積過程
return out
CoordConv
出自論文:An intriguing failing of convolutional neural networks and the CoordConv solution
論文鏈接:
https://arxiv.org/pdf/1807.03247.pdf
核心解析:
在Solo語義分割算法和Yolov5中你可以看到它的身影。本文從幾個小實驗為出發點,探究了卷積網絡在坐標變換上的能力。就是它無法將空間表示轉換成笛卡爾空間中的坐標。如下圖所示,我們向一個網絡中輸入(i, j)坐標,要求它輸出一個64×64的圖像,并在坐標處畫一個正方形或者一個像素,然而網絡在測試集上卻無法完成。雖然這項任務是我們人類認為極其簡單的工作。分析原因是卷積作為一種局部的、共享權重的過濾器應用到輸入上時,它是不知道每個過濾器在哪,無法捕捉位置信息的。因此我們可以幫助卷積,讓它知道過濾器的位置。僅僅需要在輸入上添加兩個通道,一個是i坐標,另一個是j坐標。具體做法如上圖所示,送入濾波器之前增加兩個通道。這樣,網絡就具備了空間位置信息的能力,是不是很神奇?你可以隨機在分類、分割、檢測等任務中使用這種掛件。
如上面第一組圖片,傳統的CNN在根據坐標數值生成圖像的任務中,訓練集很好,測試集一團糟。第二組圖片增加了 CoordConv 之后可以輕松完成該任務,可見其增加了CNN空間感知的能力。
核心代碼:
ins_feat = x # 當前實例特征tensor
# 生成從-1到1的線性值
x_range = torch.linspace(-1, 1, ins_feat.shape[-1], device=ins_feat.device)
y_range = torch.linspace(-1, 1, ins_feat.shape[-2], device=ins_feat.device)
y, x = torch.meshgrid(y_range, x_range) # 生成二維坐標網格
y = y.expand([ins_feat.shape[0], 1, -1, -1]) # 擴充到和ins_feat相同維度
x = x.expand([ins_feat.shape[0], 1, -1, -1])
coord_feat = torch.cat([x, y], 1) # 位置特征
ins_feat = torch.cat([ins_feat, coord_feat], 1) # concatnate一起作為下一個卷積的輸入
Ghost
插件全稱:Ghost module
出自論文:GhostNet: More Features from Cheap Operations
論文鏈接:
https://arxiv.org/pdf/1911.11907.pdf
核心解析:
在ImageNet的分類任務上,GhostNet在相似計算量情況下Top-1正確率達75.7%,高于MobileNetV3的75.2%。其主要創新點就是提出了Ghost 模塊。在CNN模型中,特征圖是存在大量的冗余,當然這也是非常重要和有必要的。如下圖所示,其中標“小扳手”的特征圖都存在冗余的特征圖。那么能否降低卷積的通道數,然后利用某種變換生成冗余的特征圖?事實上這就是GhostNet的思路。
而本文就從特征圖冗余問題出發,提出一個僅通過少量計算(論文稱為cheap operations)就能生成大量特征圖的結構——Ghost Module。而cheap operations就是線性變換,論文中采用卷積操作實現。具體過程如下:
- 使用比原始更少量卷積運算,比如正常用64個卷積核,這里就用32個,減少一半的計算量。
- 利用深度分離卷積,從上面生成的特征圖中變換出冗余的特征。
- 上面兩步獲取的特征圖concat起來輸出,送入后續的環節。
核心代碼:
class GhostModule(nn.Module):
def __init__(self, inp, oup, kernel_size=1, ratio=2, dw_size=3, stride=1, relu=True):
super(GhostModule, self).__init__()
self.oup = oup
init_channels = math.ceil(oup / ratio)
new_channels = init_channels*(ratio-1)
self.primary_conv = nn.Sequential(
nn.Conv2d(inp, init_channels, kernel_size, stride, kernel_size//2, bias=False),
nn.BatchNorm2d(init_channels),
nn.ReLU(inplace=True) if relu else nn.Sequential(), )
# cheap操作,注意利用了分組卷積進行通道分離
self.cheap_operation = nn.Sequential(
nn.Conv2d(init_channels, new_channels, dw_size, 1, dw_size//2, groups=init_channels, bias=False),
nn.BatchNorm2d(new_channels),
nn.ReLU(inplace=True) if relu else nn.Sequential(),)
def forward(self, x):
x1 = self.primary_conv(x) #主要的卷積操作
x2 = self.cheap_operation(x1) # cheap變換操作
out = torch.cat([x1,x2], dim=1) # 二者cat到一起
return out[:,:self.oup,:,:]
BlurPool
出自論文:Making Convolutional Networks Shift-Invariant Again
論文鏈接:
https://arxiv.org/abs/1904.11486
核心解析:
我們都知道,基于滑動窗口的卷積操作是具有平移不變性的,因此也默認為CNN網絡具有平移不變性或等變性,事實上真的如此嗎?實踐發現,CNN網絡真的非常敏感,只要輸入圖片稍微改一個像素,或者平移一個像素,CNN的輸出就會發生巨大的變化,甚至預測錯誤。這可是非常不具有魯棒性的。一般情況下我們利用數據增強獲取所謂的不變性。本文研究發現,不變性的退化根本原因就在于下采樣,無論是Max Pool還是Average Pool,抑或是stride>1的卷積操作,只要是涉及步長大于1的下采樣,均會導致平移不變性的丟失。具體示例如下圖所示,僅僅平移一個像素,Max pool的結果就差距很大。
為了保持平移不變性,可以在下采樣之前進行低通濾波。傳統的max pool可以分解為兩部分,分別是stride = 1的max + 下采樣 。因此作者提出的MaxBlurPool = max + blur + 下采樣來替代原始的max pool。實驗發現,該操作雖然不能徹底解決平移不變性的丟失,但是可以很大程度上緩解。
核心代碼:
class BlurPool(nn.Module):
def __init__(self, channels, pad_type='reflect', filt_size=4, stride=2, pad_off=0):
super(BlurPool, self).__init__()
self.filt_size = filt_size
self.pad_off = pad_off
self.pad_sizes = [int(1.*(filt_size-1)/2), int(np.ceil(1.*(filt_size-1)/2)), int(1.*(filt_size-1)/2), int(np.ceil(1.*(filt_size-1)/2))]
self.pad_sizes = [pad_size+pad_off for pad_size in self.pad_sizes]
self.stride = stride
self.off = int((self.stride-1)/2.)
self.channels = channels
# 定義一系列的高斯核
if(self.filt_size==1):
a = np.array([1.,])
elif(self.filt_size==2):
a = np.array([1., 1.])
elif(self.filt_size==3):
a = np.array([1., 2., 1.])
elif(self.filt_size==4):
a = np.array([1., 3., 3., 1.])
elif(self.filt_size==5):
a = np.array([1., 4., 6., 4., 1.])
elif(self.filt_size==6):
a = np.array([1., 5., 10., 10., 5., 1.])
elif(self.filt_size==7):
a = np.array([1., 6., 15., 20., 15., 6., 1.])
filt = torch.Tensor(a[:,None]*a[None,:])
filt = filt/torch.sum(filt) # 歸一化操作,保證特征經過blur后信息總量不變
# 非grad操作的參數利用buffer存儲
self.register_buffer('filt', filt[None,None,:,:].repeat((self.channels,1,1,1)))
self.pad = get_pad_layer(pad_type)(self.pad_sizes)
def forward(self, inp):
if(self.filt_size==1):
if(self.pad_off==0):
return inp[:,:,::self.stride,::self.stride]
else:
return self.pad(inp)[:,:,::self.stride,::self.stride]
else:
# 利用固定參數的conv2d+stride實現blurpool
return F.conv2d(self.pad(inp), self.filt, stride=self.stride, groups=inp.shape[1])
RFB
插件全稱:Receptive Field Block
出自論文:Receptive Field Block Net for Accurate and Fast Object Detection
論文鏈接:
https://arxiv.org/abs/1711.07767
核心解析:
論文發現目標區域要盡量靠近感受野中心,這會有助于提升模型對小尺度空間位移的魯棒性。因此受人類視覺RF結構的啟發,本文提出了感受野模塊(RFB),加強了CNN模型學到的深層特征的能力,使檢測模型更加準確。RFB可以作為一種通用模塊嵌入到絕大多數網路當中。下圖可以看出其和inception、ASPP、DCN的區別,可以看作是inception+ASPP的結合。
具體實現如下圖,其實和ASPP類似,不過是使用了不同大小的卷積核作為空洞卷積的前置操作。
核心代碼:
class RFB(nn.Module):
def __init__(self, in_planes, out_planes, stride=1, scale = 0.1, visual = 1):
super(RFB, self).__init__()
self.scale = scale
self.out_channels = out_planes
inter_planes = in_planes // 8
# 分支0:1X1卷積+3X3卷積
self.branch0 = nn.Sequential(conv_bn_relu(in_planes, 2*inter_planes, 1, stride),
conv_bn_relu(2*inter_planes, 2*inter_planes, 3, 1, visual, visual, False))
# 分支1:1X1卷積+3X3卷積+空洞卷積
self.branch1 = nn.Sequential(conv_bn_relu(in_planes, inter_planes, 1, 1),
conv_bn_relu(inter_planes, 2*inter_planes, (3,3), stride, (1,1)),
conv_bn_relu(2*inter_planes, 2*inter_planes, 3, 1, visual+1,visual+1,False))
# 分支2:1X1卷積+3X3卷積*3代替5X5卷積+空洞卷積
self.branch2 = nn.Sequential(conv_bn_relu(in_planes, inter_planes, 1, 1),
conv_bn_relu(inter_planes, (inter_planes//2)*3, 3, 1, 1),
conv_bn_relu((inter_planes//2)*3, 2*inter_planes, 3, stride, 1),
conv_bn_relu(2*inter_planes, 2*inter_planes, 3, 1, 2*visual+1, 2*visual+1,False) )
self.ConvLinear = conv_bn_relu(6*inter_planes, out_planes, 1, 1, False)
self.shortcut = conv_bn_relu(in_planes, out_planes, 1, stride, relu=False)
self.relu = nn.ReLU(inplace=False)
def forward(self,x):
x0 = self.branch0(x)
x1 = self.branch1(x)
x2 = self.branch2(x)
# 尺度融合
out = torch.cat((x0,x1,x2),1)
# 1X1卷積
out = self.ConvLinear(out)
short = self.shortcut(x)
out = out*self.scale + short
out = self.relu(out)
return out
ASFF
插件全稱:Adaptively Spatial Feature Fusion
出自論文:Adaptively Spatial Feature Fusion Learning Spatial Fusion for Single-Shot Object Detection
論文鏈接:
https://arxiv.org/abs/1911.09516v1
核心解析:
為了更加充分的利用高層語義特征和底層細粒度特征,很多網絡都會采用FPN的方式輸出多層特征,但是它們都多用concat或者element-wise這種融合方式,本論文認為這樣不能充分利用不同尺度的特征,所以提出了Adaptively Spatial Feature Fusion,即自適應特征融合方式。FPN輸出的特征圖經過下面兩部分的處理:
Feature Resizing:特征圖的尺度不同無法進行element-wise融合,因此需要進行resize。對于上采樣:首先利用1X1卷積進行通道壓縮,然后利用插值的方法上采樣特征圖。對于1/2的下采樣:利用stride=2的3X3卷積同時進行通道壓縮和特征圖縮小。對于1/4的下采樣:在stride=2的3X3的卷積之前插入tride=2的maxpooling。
Adaptive Fusion:特征圖自適應融合,公式如下
其中x n→l表示在(i,j)位置的特征向量,來自n特征圖,經過上述resize到l尺度。Alpha。Beta,gamma為空間注意力權重,經過softmax處理,如下:
代碼解析:
class ASFF(nn.Module):
def __init__(self, level, rfb=False):
super(ASFF, self).__init__()
self.level = level
# 輸入的三個特征層的channels, 根據實際修改
self.dim = [512, 256, 256]
self.inter_dim = self.dim[self.level]
# 每個層級三者輸出通道數需要一致
if level==0:
self.stride_level_1 = conv_bn_relu(self.dim[1], self.inter_dim, 3, 2)
self.stride_level_2 = conv_bn_relu(self.dim[2], self.inter_dim, 3, 2)
self.expand = conv_bn_relu(self.inter_dim, 1024, 3, 1)
elif level==1:
self.compress_level_0 = conv_bn_relu(self.dim[0], self.inter_dim, 1, 1)
self.stride_level_2 = conv_bn_relu(self.dim[2], self.inter_dim, 3, 2)
self.expand = conv_bn_relu(self.inter_dim, 512, 3, 1)
elif level==2:
self.compress_level_0 = conv_bn_relu(self.dim[0], self.inter_dim, 1, 1)
if self.dim[1] != self.dim[2]:
self.compress_level_1 = conv_bn_relu(self.dim[1], self.inter_dim, 1, 1)
self.expand = add_conv(self.inter_dim, 256, 3, 1)
compress_c = 8 if rfb else 16
self.weight_level_0 = conv_bn_relu(self.inter_dim, compress_c, 1, 1)
self.weight_level_1 = conv_bn_relu(self.inter_dim, compress_c, 1, 1)
self.weight_level_2 = conv_bn_relu(self.inter_dim, compress_c, 1, 1)
self.weight_levels = nn.Conv2d(compress_c*3, 3, 1, 1, 0)
# 尺度大小 level_0 < level_1 < level_2
def forward(self, x_level_0, x_level_1, x_level_2):
# Feature Resizing過程
if self.level==0:
level_0_resized = x_level_0
level_1_resized = self.stride_level_1(x_level_1)
level_2_downsampled_inter =F.max_pool2d(x_level_2, 3, stride=2, padding=1)
level_2_resized = self.stride_level_2(level_2_downsampled_inter)
elif self.level==1:
level_0_compressed = self.compress_level_0(x_level_0)
level_0_resized =F.interpolate(level_0_compressed, 2, mode='nearest')
level_1_resized =x_level_1
level_2_resized =self.stride_level_2(x_level_2)
elif self.level==2:
level_0_compressed = self.compress_level_0(x_level_0)
level_0_resized =F.interpolate(level_0_compressed, 4, mode='nearest')
if self.dim[1] != self.dim[2]:
level_1_compressed = self.compress_level_1(x_level_1)
level_1_resized = F.interpolate(level_1_compressed, 2, mode='nearest')
else:
level_1_resized =F.interpolate(x_level_1, 2, mode='nearest')
level_2_resized =x_level_2
# 融合權重也是來自于網絡學習
level_0_weight_v = self.weight_level_0(level_0_resized)
level_1_weight_v = self.weight_level_1(level_1_resized)
level_2_weight_v = self.weight_level_2(level_2_resized)
levels_weight_v = torch.cat((level_0_weight_v, level_1_weight_v,
level_2_weight_v),1)
levels_weight = self.weight_levels(levels_weight_v)
levels_weight = F.softmax(levels_weight, dim=1) # alpha產生
# 自適應融合
fused_out_reduced = level_0_resized * levels_weight[:,0:1,:,:]+
level_1_resized * levels_weight[:,1:2,:,:]+
level_2_resized * levels_weight[:,2:,:,:]
out = self.expand(fused_out_reduced)
return out
結語
本文盤點了近年來比較精巧而又實用的CNN插件,希望大家活學活用,用在自己的實際項目中。