將軟注意力機制分爲三大注意力預:
(1) 空間域——對空間進行掩碼的生成,進行打分,代表是Spatial Attention Module。忽略了通道域中的信息,將每個通道中的圖片特徵同等處理,這種做法會將空間域變換方法侷限在原始圖片特徵提取階段,應用在神經網絡層其他層的可解釋性不強。
(2) 通道域——對通道生成掩碼mask,進行打分,代表是senet, Channel Attention Module。對一個通道內的信息直接全局平均池化,而忽略每一個通道內的局部信息。
(3) 混合域——同時對通道注意力和空間注意力進行評價打分,代表的有BAM, CBAM。
1 Squeeze-and-Excitation Networks(SENet)
通道域
對C張feature map先做一個Global Average Pooling(圖中的Fsq(.),作者稱爲Squeeze過程),輸出的1x1xC數據。
再經過兩級全連接(圖中的Fex(.),作者稱爲Excitation過程,非線性變換)。參數r的目的是爲了減少全連接層的參數。
最後用sigmoid(論文中的self-gating mechanism)限制到[0,1]的範圍,把這個值作爲scale乘到U的C個通道上, 作爲下一級的輸入數據。
class SELayer(nn.Module):
def __init__(self, channel, reduction=16):
super(SELayer, self).__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.fc = nn.Sequential(
nn.Linear(channel, channel // reduction, bias=False),
nn.ReLU(inplace=True),
nn.Linear(channel // reduction, channel, bias=False),
nn.Sigmoid()
)
def forward(self, x):
b, c, _, _ = x.size()
y = self.avg_pool(x).view(b, c)
y = self.fc(y).view(b, c, 1, 1)
return x * y.expand_as(x)
2 Convolutional Block Attention Module(CBAM)
依次應用通道和空間注意模塊,來來增加表現力,關注重要特徵並抑制不必要的特徵。
2.1通道注意力模塊
CBAM在S步採取了全局平均池化以及全局最大池化(即對每張特徵圖求mean及max),提取的高層次特徵更加豐富。接着在E步同樣通過兩個全連接層和相應的激活函數建模通道之間的相關性,合併兩個輸出得到各個特徵通道的權重。最後,得到特徵通道的權重之後,通過乘法逐通道加權到原來的特徵上,完成在通道維度上的原始特徵重標定。
class ChannelAttention(nn.Module):
def __init__(self, in_planes, rotio=16):
super(ChannelAttention, self).__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.max_pool = nn.AdaptiveMaxPool2d(1)
self.sharedMLP = nn.Sequential(
nn.Conv2d(in_planes, in_planes // ratio, 1, bias=False), nn.ReLU(),
nn.Conv2d(in_planes // rotio, in_planes, 1, bias=False))
self.sigmoid = nn.Sigmoid()
def forward(self, x):
avgout = self.sharedMLP(self.avg_pool(x))
maxout = self.sharedMLP(self.max_pool(x))
return self.sigmoid(avgout + maxout)
2.2空間注意力模塊
在通道這個維度上把所有輸入通道池化成2個實數,由(hwc)形狀的輸入得到兩個(hw1)的特徵圖。接着使用一個 77 的卷積核,卷積後形成新的(hw*1)的特徵圖。注意力模塊特徵與得到的新特徵圖相乘得到經過雙重注意力調整的特徵圖。
class SpatialAttention(nn.Module):
def __init__(self, kernel_size=7):
super(SpatialAttention, self).__init__()
assert kernel_size in (3,7), "kernel size must be 3 or 7"
padding = 3 if kernel_size == 7 else 1
self.conv = nn.Conv2d(2,1,kernel_size, padding=padding, bias=False)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
avgout = torch.mean(x, dim=1, keepdim=True)
maxout, _ = torch.max(x, dim=1, keepdim=True)
x = torch.cat([avgout, maxout], dim=1)
x = self.conv(x)
return self.sigmoid(x)
2.3 整體代碼
class BasicBlock(nn.Module):
expansion = 1
def __init__(self, inplanes, planes, stride=1, downsample=None):
super(BasicBlock, self).__init__()
self.conv1 = conv3x3(inplanes, planes, stride)
self.bn1 = nn.BatchNorm2d(planes)
self.relu = nn.ReLU(inplace=True)
self.conv2 = conv3x3(planes, planes)
self.bn2 = nn.BatchNorm2d(planes)
self.ca = ChannelAttention(planes)
self.sa = SpatialAttention()
self.downsample = downsample
self.stride = stride
def forward(self, x):
residual = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.ca(out) * out # 廣播機制
out = self.sa(out) * out # 廣播機制
if self.downsample is not None:
residual = self.downsample(x)
out += residual
out = self.relu(out)
return out
3 Selective Kernel Networks(SKNet)
Selective Kernel Networks(SKNet)發表在CVPR 2019,是對Momenta發表於CVPR 2018上論文SENet的改進。是針對卷積核的注意力機制研究。
SKNet對不同圖像使用的卷積核權重不同,即一種針對不同尺度的圖像動態生成卷積核。
網絡主要由Split、Fuse、Select三部分組成。
-
Split
對原特徵圖經過不同大小的卷積核部分進行卷積的過程,這裏可以有多個分支。
對輸入X使用不同大小卷積核分別進行卷積操作(圖中的卷積核size分別爲3x3和5x5兩個分支,但是可以有多個分支)。操作包括卷積、efficient grouped/depthwise convolutions、BN。 -
Fuse
計算每個卷積核權重的部分。
將兩部分的特徵圖按元素求和
U通過全局平均池化(GAP)生成通道統計信息。得到的Sc維度爲C * 1
經過全連接生成緊湊的特徵z(維度爲d * 1), δ是RELU激活函數,B表示批標準化(BN),z的維度爲卷積核的個數,W維度爲d×C, d代表全連接後的特徵維度,L在文中的值爲32,r爲壓縮因子。
-
Select
根據不同權重卷積覈計算後得到的新的特徵圖。進行softmax計算每個卷積核的權重,計算方式如下圖所示。如果是兩個卷積核,則 ac + bc = 1。z的維度爲(d * 1)A的維度爲(C * d),B的維度爲(C * d),則a = A x z的維度爲1 * C。
Ac、Bc爲A、B的第c行數據(1 * d)。ac爲a的第c個元素,這樣分別得到了每個卷積核的權重。
將權重應用到特徵圖上。其中V = [V1,V2,…,VC], Vc 維度爲(H x W),如果
select中softmax部分可參考下圖(3個卷積核)
class SKConv(nn.Module):
def __init__(self, features, WH, M, G, r, stride=1, L=32):
super(SKConv, self).__init__()
d = max(int(features / r), L)
self.M = M
self.features = features
self.convs = nn.ModuleList([])
for i in range(M):
# 使用不同kernel size的卷積
self.convs.append(
nn.Sequential(
nn.Conv2d(features,
features,
kernel_size=3 + i * 2,
stride=stride,
padding=1 + i,
groups=G), nn.BatchNorm2d(features),
nn.ReLU(inplace=False)))
self.fc = nn.Linear(features, d)
self.fcs = nn.ModuleList([])
for i in range(M):
self.fcs.append(nn.Linear(d, features))
self.softmax = nn.Softmax(dim=1)
def forward(self, x):
for i, conv in enumerate(self.convs):
fea = conv(x).unsqueeze_(dim=1)
if i == 0:
feas = fea
else:
feas = torch.cat([feas, fea], dim=1)
fea_U = torch.sum(feas, dim=1)
fea_s = fea_U.mean(-1).mean(-1)
fea_z = self.fc(fea_s)
for i, fc in enumerate(self.fcs):
print(i, fea_z.shape)
vector = fc(fea_z).unsqueeze_(dim=1)
print(i, vector.shape)
if i == 0:
attention_vectors = vector
else:
attention_vectors = torch.cat([attention_vectors, vector],
dim=1)
attention_vectors = self.softmax(attention_vectors)
attention_vectors = attention_vectors.unsqueeze(-1).unsqueeze(-1)
fea_v = (feas * attention_vectors).sum(dim=1)
return fea_v