這是南開大學(xué)在ICCV2023會議上新提出的旋轉(zhuǎn)目標(biāo)檢測算法,基本原理就是通過一系列Depth-wise 卷積核和空間選擇機制來動態(tài)調(diào)整目標(biāo)的感受野,從而允許模型適應(yīng)不同背景的目標(biāo)檢測。
論文地址:https://arxiv.org/pdf/2303.09030.pdf
代碼地址(可以直接使用mmrotate框架實現(xiàn)):GitHub - zcablii/LSKNet: (ICCV 2023) Large Selective Kernel Network for Remote Sensing Object Dyetection
?一、引言
目前基于旋轉(zhuǎn)框的遙感影像目標(biāo)檢測算法已經(jīng)取得了一定的進(jìn)展,但是很少考慮存在于遙感影像中的先驗知識。遙感影像中的目標(biāo)往往尺寸很小,僅僅基于其表觀特征很難識別,如果結(jié)合其背景信息,如周邊環(huán)境,就可以提供形狀、方向等有意義的信息。據(jù)此,作者分析了兩條重要的先驗知識:
- ?精確識別遙感影像中的目標(biāo)往往需要大范圍的背景信息,有限的背景區(qū)域會影響模型的識別效果,例如當(dāng)背景信息很少時,容易將十字路口識別為道路。
- 不同類型的目標(biāo)所需要的背景信息范圍是不同的,如足球場可通明顯的球場邊界線進(jìn)行區(qū)分,所需的背景信息不多,但是十字路口與道路相似,容易受到樹木和其他遮擋物的影響,因此需要足夠的背景范圍信息才能進(jìn)行識別。


為了解決上述問題,作者提出了一種新的遙感影像目標(biāo)識別方法,即Large Selective Kernel Network (LSKNet)。該方法通過在特征提取模塊動態(tài)調(diào)整感受野,更有效地處理了不同目標(biāo)所需的背景信息差異。其中,動態(tài)感受野由一個空間選擇機制實現(xiàn),該機制對一大串Depth-wise 卷積核所處理的特征進(jìn)行有效加權(quán)和空間融合。這些卷積核的權(quán)重根據(jù)輸入動態(tài)確定,同時允許模型針對空間上的不同目標(biāo)自適應(yīng)地選擇不同大小的核并調(diào)整感受野。
經(jīng)驗證,LSKNet網(wǎng)絡(luò)雖然結(jié)構(gòu)簡單,但能夠獲得優(yōu)異的檢測性能,在HRSC2016、DOTA-v1.0、FAIR1M-v1.0三個典型數(shù)據(jù)集上都取得了SOTA。
二、算法原理
1. LSKNet的架構(gòu)
結(jié)構(gòu)層級依次為:
LSK module(大核卷積序列+空間選擇機制) < LSK Block (LK Selection + FFN)<LSKNet(N個LSK Block)


LSKNet 是主干網(wǎng)絡(luò)中的一個可重復(fù)堆疊的塊(Block),每個LSK Block包括兩個殘差子塊,即大核選擇子塊(Large Kernel Selection,LK Selection)和前饋網(wǎng)絡(luò)子塊(Feed-forward Network ,F(xiàn)FN),如圖8。LK Selection子塊根據(jù)需要動態(tài)地調(diào)整網(wǎng)絡(luò)的感受野,F(xiàn)FN子塊用于通道混合和特征細(xì)化,由一個全連接層、一個深度卷積、一個 GELU 激活和第二個全連接層組成。
LSK module(LSK 模塊,圖4)由一個大核卷積序列(large kernel convolutions)和一個空間核選擇機制(spatial kernel selection mechanism)組成,被嵌入到了LSK Block 的 LK Selection子塊中(圖8橙色塊)。
2. Large Kernel Convolutions
因為不同類型的目標(biāo)對背景信息的需求不同,這就需要模型能夠自適應(yīng)選擇不同大小的背景范圍。因此,作者通過解耦出一系列具有大卷積核、且不斷擴(kuò)張的Depth-wise 卷積,構(gòu)建了一個更大感受野的網(wǎng)絡(luò)。
具體地,假設(shè)序列中第i個Depth-wise 卷積核的大小為 ,擴(kuò)張率為 ,感受野為 ,它們滿足以下關(guān)系:
卷積核大小和擴(kuò)張率的增加保證了感受野能夠快速增大。此外,我們設(shè)置了擴(kuò)張率的上限,以保證擴(kuò)張卷積不會引入特征圖之間的差距。
Table2的卷積核大小可根據(jù)公式(1)和(2)計算,詳見下圖:
這樣設(shè)計的好處有兩點。首先,能夠產(chǎn)生具有多種不同大小感受野的特征,便于后續(xù)的核選擇;第二,序列解耦比簡單的使用一個大型卷積核效果更好。如上圖表2所示,解耦操作相對于標(biāo)準(zhǔn)的大型卷積核,有效地將低了模型的參數(shù)量。
為了從輸入數(shù)據(jù) 的不同區(qū)域獲取豐富的背景信息特征,可采用一系列解耦的、不用感受野的Depth-wise 卷積核:
其中,是卷積核為 、擴(kuò)張率為 的Depth-wise 卷積操作。假設(shè)有個解耦的卷積核,每個卷積操作后又要經(jīng)過一個的卷積層
進(jìn)行空間特征向量的通道融合。
之后,針對不同的目標(biāo),可基于獲取的多尺度特征,通過下文中的選擇機制動態(tài)選擇合適的卷積核大小。
這一段的意思可以簡單理解為:
把一個大的卷積核拆成了幾個小的卷積核,比如一個大小為5,擴(kuò)張率為1的卷積核加上一個大小為7,擴(kuò)張率為3的卷積核,感受野為23,與一個大小為23,擴(kuò)張率為1的卷積核的感受野是一樣的。因此可用兩個小的卷積核替代一個大的卷積核,同理一個大小為29的卷積核也可以用三個小的卷積代替(Table 2),這樣可以有效的減少參數(shù),且更靈活。
將輸入數(shù)據(jù)依次通過這些小的卷積核(公式3),并在每個小的卷積核后面接上一個1×1的卷積進(jìn)行通道融合(公式4)。
3. Spatial Kernel Selection
為了使模型更關(guān)注目標(biāo)在空間上的重點背景信息,作者使用空間選擇機制從不同尺度的大卷積核中對特征圖進(jìn)行空間選擇。
首先,將來自于不同感受野卷積核的特征進(jìn)行concate拼接:
然后,應(yīng)用通道級的平均池化和最大池化
提取空間關(guān)系 :
其中, 和 是平均池化和最大池化后的空間特征描述符。為了實現(xiàn)不同空間描述符的信息交互,作者利用卷積層將空間池化特征進(jìn)行拼接,將2個通道的池化特征轉(zhuǎn)換為N個空間注意力特征圖:
之后,將Sigmoid激活函數(shù)應(yīng)用到每一個空間注意力特征圖,可獲得每個解耦的大卷積核所對應(yīng)的獨立的空間選擇掩膜:
又然后,將解耦后的大卷積核序列的特征與對應(yīng)的空間選擇掩膜進(jìn)行加權(quán)處理,并通過卷積層進(jìn)行融合獲得注意力特征 :
最后LSK module的輸出可通過輸入特征 與注意力特征 的逐元素點成獲得,即:
公式對應(yīng)于結(jié)構(gòu)圖上的操作如下:
三、實驗結(jié)果
1. 實驗數(shù)據(jù)集
包括HRSC2016、DOTA-v1.0和FAIR1M-v1.0三個。
2. 訓(xùn)練細(xì)節(jié)
骨干網(wǎng)絡(luò)先在ImageNet-1K上預(yù)訓(xùn)練,然后再在實驗數(shù)據(jù)集上微調(diào)。消融實驗中,骨干網(wǎng)絡(luò)預(yù)訓(xùn)練迭代了100個epoch。為了獲得更優(yōu)異的檢測性能,采用了預(yù)訓(xùn)練300epoch的骨干網(wǎng)絡(luò)獲取主要結(jié)果。LSKNet默認(rèn)構(gòu)建在Oriented R-CNN上,優(yōu)化器為AdamW。
3. 消融實驗(DOTA-v1.0)
- 大型卷積核解耦:證明了將一個大型卷積核解耦為兩個Depth-wise卷積核能夠在速度和精度上獲得更好的平衡(Table 3)。
- 感受野大小和選擇類型:過小或過大的感受野會限制模型的性能,大小為23的感受野被證明是最有效的。此外,實驗證明了本文提出的空間選擇方法相比通道注意力機制具有更優(yōu)異的性能(Table 4)。
- 空間選擇的池化層:實驗表明,在空間選擇部分同時采用最大和平均池化能夠獲得最優(yōu)異的性能,也不會帶來推理速度的損失(Table 5)。
- 不同網(wǎng)絡(luò)框架下LSKNet主干網(wǎng)絡(luò)的性能:實驗證明,與ResNet-18相比,LSKNet作為主干網(wǎng)絡(luò)能夠有效提升網(wǎng)絡(luò)性能,同時只有38%的參數(shù)量和50%的FLOPS。
- 與其他大核卷積、其他選擇性注意力骨干網(wǎng)絡(luò)及其他網(wǎng)絡(luò)結(jié)構(gòu)對比,LSKNet都有明顯的優(yōu)越性。
4. 分析
為了研究每個目標(biāo)類別的感受野范圍,將 定義為類別c的期望選擇感受野的面積與地面邊界框面積的比值:
為包含目標(biāo)類別 的影像數(shù)量,是輸入影像 中所有LSK block輸出的空間選擇激活的總和, 是LSKNet的block數(shù)量, 是一個LSK module解耦得到的卷積核數(shù)量, 是所有標(biāo)注的地面真實目標(biāo)框 的總像元面積。

四、代碼詳解
該算法代碼采用mmrotate框架,可作為Oriented RCNN、RoI Transformer等基礎(chǔ)網(wǎng)絡(luò)的backbone。將該代碼集成至已有的mmrotate框架中,只需要將mmrotate/models/backbones/lsknet.py文件拷貝至對應(yīng)的的已有的文件夾,同時在__init__.py中導(dǎo)入并在config文件中修改對應(yīng)配置即可。文章來源:http://www.zghlxwxcb.cn/news/detail-726586.html
LSKNet的代碼如下:文章來源地址http://www.zghlxwxcb.cn/news/detail-726586.html
import torch
import torch.nn as nn
from torch.nn.modules.utils import _pair as to_2tuple
from mmcv.cnn.utils.weight_init import (constant_init, normal_init,
trunc_normal_init)
from ..builder import ROTATED_BACKBONES
from mmcv.runner import BaseModule
from timm.models.layers import DropPath, to_2tuple, trunc_normal_
import math
from functools import partial
import warnings
from mmcv.cnn import build_norm_layer
class Mlp(nn.Module):
def __init__(self, in_features, hidden_features=None, out_features=None, act_layer=nn.GELU, drop=0.):
super().__init__()
out_features = out_features or in_features
hidden_features = hidden_features or in_features
self.fc1 = nn.Conv2d(in_features, hidden_features, 1)
self.dwconv = DWConv(hidden_features)
self.act = act_layer()
self.fc2 = nn.Conv2d(hidden_features, out_features, 1)
self.drop = nn.Dropout(drop)
def forward(self, x):
x = self.fc1(x)
x = self.dwconv(x)
x = self.act(x)
x = self.drop(x)
x = self.fc2(x)
x = self.drop(x)
return x
class LSKblock(nn.Module):
def __init__(self, dim):
super().__init__()
self.conv0 = nn.Conv2d(dim, dim, 5, padding=2, groups=dim)
self.conv_spatial = nn.Conv2d(dim, dim, 7, stride=1, padding=9, groups=dim, dilation=3)
self.conv1 = nn.Conv2d(dim, dim//2, 1)
self.conv2 = nn.Conv2d(dim, dim//2, 1)
self.conv_squeeze = nn.Conv2d(2, 2, 7, padding=3)
self.conv = nn.Conv2d(dim//2, dim, 1)
def forward(self, x):
attn1 = self.conv0(x)
attn2 = self.conv_spatial(attn1)
attn1 = self.conv1(attn1)
attn2 = self.conv2(attn2)
attn = torch.cat([attn1, attn2], dim=1)
avg_attn = torch.mean(attn, dim=1, keepdim=True)
max_attn, _ = torch.max(attn, dim=1, keepdim=True)
agg = torch.cat([avg_attn, max_attn], dim=1)
sig = self.conv_squeeze(agg).sigmoid()
attn = attn1 * sig[:,0,:,:].unsqueeze(1) + attn2 * sig[:,1,:,:].unsqueeze(1)
attn = self.conv(attn)
return x * attn
class Attention(nn.Module):
def __init__(self, d_model):
super().__init__()
self.proj_1 = nn.Conv2d(d_model, d_model, 1)
self.activation = nn.GELU()
self.spatial_gating_unit = LSKblock(d_model)
self.proj_2 = nn.Conv2d(d_model, d_model, 1)
def forward(self, x):
shorcut = x.clone()
x = self.proj_1(x)
x = self.activation(x)
x = self.spatial_gating_unit(x)
x = self.proj_2(x)
x = x + shorcut
return x
class Block(nn.Module):
def __init__(self, dim, mlp_ratio=4., drop=0.,drop_path=0., act_layer=nn.GELU, norm_cfg=None):
super().__init__()
if norm_cfg:
self.norm1 = build_norm_layer(norm_cfg, dim)[1]
self.norm2 = build_norm_layer(norm_cfg, dim)[1]
else:
self.norm1 = nn.BatchNorm2d(dim)
self.norm2 = nn.BatchNorm2d(dim)
self.attn = Attention(dim)
self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity()
mlp_hidden_dim = int(dim * mlp_ratio)
self.mlp = Mlp(in_features=dim, hidden_features=mlp_hidden_dim, act_layer=act_layer, drop=drop)
layer_scale_init_value = 1e-2
self.layer_scale_1 = nn.Parameter(
layer_scale_init_value * torch.ones((dim)), requires_grad=True)
self.layer_scale_2 = nn.Parameter(
layer_scale_init_value * torch.ones((dim)), requires_grad=True)
def forward(self, x):
x = x + self.drop_path(self.layer_scale_1.unsqueeze(-1).unsqueeze(-1) * self.attn(self.norm1(x)))
x = x + self.drop_path(self.layer_scale_2.unsqueeze(-1).unsqueeze(-1) * self.mlp(self.norm2(x)))
return x
class OverlapPatchEmbed(nn.Module):
""" Image to Patch Embedding
"""
def __init__(self, img_size=224, patch_size=7, stride=4, in_chans=3, embed_dim=768, norm_cfg=None):
super().__init__()
patch_size = to_2tuple(patch_size)
self.proj = nn.Conv2d(in_chans, embed_dim, kernel_size=patch_size, stride=stride,
padding=(patch_size[0] // 2, patch_size[1] // 2))
if norm_cfg:
self.norm = build_norm_layer(norm_cfg, embed_dim)[1]
else:
self.norm = nn.BatchNorm2d(embed_dim)
def forward(self, x):
x = self.proj(x)
_, _, H, W = x.shape
x = self.norm(x)
return x, H, W
@ROTATED_BACKBONES.register_module()
class LSKNet(BaseModule):
def __init__(self, img_size=224, in_chans=3, embed_dims=[64, 128, 256, 512],
mlp_ratios=[8, 8, 4, 4], drop_rate=0., drop_path_rate=0., norm_layer=partial(nn.LayerNorm, eps=1e-6),
depths=[3, 4, 6, 3], num_stages=4,
pretrained=None,
init_cfg=None,
norm_cfg=None):
super().__init__(init_cfg=init_cfg)
assert not (init_cfg and pretrained), \
'init_cfg and pretrained cannot be set at the same time'
if isinstance(pretrained, str):
warnings.warn('DeprecationWarning: pretrained is deprecated, '
'please use "init_cfg" instead')
self.init_cfg = dict(type='Pretrained', checkpoint=pretrained)
elif pretrained is not None:
raise TypeError('pretrained must be a str or None')
self.depths = depths
self.num_stages = num_stages
dpr = [x.item() for x in torch.linspace(0, drop_path_rate, sum(depths))] # stochastic depth decay rule
cur = 0
for i in range(num_stages):
patch_embed = OverlapPatchEmbed(img_size=img_size if i == 0 else img_size // (2 ** (i + 1)),
patch_size=7 if i == 0 else 3,
stride=4 if i == 0 else 2,
in_chans=in_chans if i == 0 else embed_dims[i - 1],
embed_dim=embed_dims[i], norm_cfg=norm_cfg)
block = nn.ModuleList([Block(
dim=embed_dims[i], mlp_ratio=mlp_ratios[i], drop=drop_rate, drop_path=dpr[cur + j],norm_cfg=norm_cfg)
for j in range(depths[i])])
norm = norm_layer(embed_dims[i])
cur += depths[i]
setattr(self, f"patch_embed{i + 1}", patch_embed)
setattr(self, f"block{i + 1}", block)
setattr(self, f"norm{i + 1}", norm)
def init_weights(self):
print('init cfg', self.init_cfg)
if self.init_cfg is None:
for m in self.modules():
if isinstance(m, nn.Linear):
trunc_normal_init(m, std=.02, bias=0.)
elif isinstance(m, nn.LayerNorm):
constant_init(m, val=1.0, bias=0.)
elif isinstance(m, nn.Conv2d):
fan_out = m.kernel_size[0] * m.kernel_size[
1] * m.out_channels
fan_out //= m.groups
normal_init(
m, mean=0, std=math.sqrt(2.0 / fan_out), bias=0)
else:
super(LSKNet, self).init_weights()
def freeze_patch_emb(self):
self.patch_embed1.requires_grad = False
@torch.jit.ignore
def no_weight_decay(self):
return {'pos_embed1', 'pos_embed2', 'pos_embed3', 'pos_embed4', 'cls_token'} # has pos_embed may be better
def get_classifier(self):
return self.head
def reset_classifier(self, num_classes, global_pool=''):
self.num_classes = num_classes
self.head = nn.Linear(self.embed_dim, num_classes) if num_classes > 0 else nn.Identity()
def forward_features(self, x):
B = x.shape[0]
outs = []
for i in range(self.num_stages):
patch_embed = getattr(self, f"patch_embed{i + 1}")
block = getattr(self, f"block{i + 1}")
norm = getattr(self, f"norm{i + 1}")
x, H, W = patch_embed(x)
for blk in block:
x = blk(x)
x = x.flatten(2).transpose(1, 2)
x = norm(x)
x = x.reshape(B, H, W, -1).permute(0, 3, 1, 2).contiguous()
outs.append(x)
return outs
def forward(self, x):
x = self.forward_features(x)
# x = self.head(x)
return x
class DWConv(nn.Module):
def __init__(self, dim=768):
super(DWConv, self).__init__()
self.dwconv = nn.Conv2d(dim, dim, 3, 1, 1, bias=True, groups=dim)
def forward(self, x):
x = self.dwconv(x)
return x
def _conv_filter(state_dict, patch_size=16):
""" convert patch embedding weight from manual patchify + linear proj to conv"""
out_dict = {}
for k, v in state_dict.items():
if 'patch_embed.proj.weight' in k:
v = v.reshape((v.shape[0], 3, patch_size, patch_size))
out_dict[k] = v
return out_dict
到了這里,關(guān)于【論文閱讀】LSKNet: Large Selective Kernel Network for Remote Sensing Object Detection的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!