文章目录
Cascade R-CNN
- Cascade R-CNN: Delving into High Quality Object Detection
- 针对R-CNN系列的两步检测模型 提出的改进方法。
- 主要是针对匹配策略提出了 multi-level 的方式。
动机
- 检测 = 分类 + 定位
- 分类来说,标签就是标签,直接计算损失就行
- 定位就比较麻烦,目前主要解决方法就是回归问题!
- 定位 比较麻烦在于,
anchor,预测Predict和真实值Gt
这三个之间存在的变换关系! - 因为Anchor有很多(HW x Anchor_num),而只存在少量"有效的Anchor"
- 通常做法是看Gt和Anchor之间的IoU,设定阈值,如0.5,大于0.5就认为是“正样本”!
- 实际上严格来说使用0.5的阈值偏低,造成的结果就如 Figure 1(a)那样会有很多,“close” false positives ,通俗的说就是有很多重复的没用的框,可以称为噪声,而我们期望的是输出像 Figure 1(b)那样
- 而一味的提高阈值,也会使得效果变差,因为:
- 提高IOU会导致正样本数量过少,然而模型比较大,会导致过拟合。
- detector肯定是在某一个IOU阈值上训练的,但是inference时,生成anchor box之后,这些anchor box跟ground truth box会有各种各样的IOU,对于这些IOU来说detector是次优化的。(离散问题不好优化)
解决途径
iterative BBox regression
:上图(b),问题:① 三个head部分是一样的,也就是说都是针对某一个IOU阈值,比如0.5,是最优化的,但是当inference时,真正的IOU大于0.5以后他是次优化suboptimal 的 ② 偏差累积,相同的head对于后面的结构来说是次优化的suboptimal 。Integral Loss
:上图©,缺点:不同IOU阈值的分布是不均衡的,所以上面的损失是不均衡的
损失函数
L ( x t , g ) = L c l s ( h t ( x t ) , y t ) + λ [ y t ≥ 1 ] L l o c ( f t ( x t , b t ) , g ) L\left(x^{t}, g\right)=L_{c l s}\left(h_{t}\left(x^{t}\right), y^{t}\right)+\lambda\left[y^{t} \geq 1\right] L_{l o c}\left(f_{t}\left(x^{t}, \mathbf{b}^{t}\right), \mathbf{g}\right) L(xt,g)=Lcls(ht(xt),yt)+λ[yt≥1]Lloc(ft(xt,bt),g)
- 其中:
b t = f t − 1 ( x t − 1 , b t − 1 ) \mathbf{b}^{t}=f_{t-1}\left(x^{t-1}, \mathbf{b}^{t-1}\right) bt=ft−1(xt−1,bt−1)
注意力机制
CV中的注意力类别
- Sequence Attention (NLP)
- 按照《Attention is all you need》说的,分为一下两类:
- dot-product attention
- additive attention
- 链接
- CBAM:Convolutional Block Attention Module
- multi-head attention:以sequence attention为例,多头attention在实现上就是并列好几个attention,最后合并
- 多个self-attention并行执行,最后的结果concatenation在一起,如果并列了8个头,concat之后,尺寸会变为原来的8倍,经过神经网络进行缩小回原来的尺寸
- 多个self-attention并行执行,最后的结果concatenation在一起,如果并列了8个头,concat之后,尺寸会变为原来的8倍,经过神经网络进行缩小回原来的尺寸
- transformer:transformer是一个完全基于attention的结构,完全没有CNN和RNN的影子,这也侧面说明了attention机制并不依赖于RNN或者CNN,可以独立的拿出来单独形成结构。pytorch在1.2版本已经加入了transformer层。 编解码结构!PS:Bert只有编码结构
视觉注意力机制在分类网络中的应用
- 空间域:将图片中的的空间域信息做对应的空间变换,从而能将关键的信息提取出来。对空间进行掩码的生成,进行打分,代表是Spatial Attention Module。
- 通道域:类似于给每个通道上的信号都增加一个权重,来代表该通道与关键信息的相关度的话,这个权重越大,则表示相关度越高。对通道生成掩码mask,进行打分,代表是senet, Channel Attention Module。
- 混合域:① 空间域的注意力是忽略了通道域中的信息,将每个通道中的图片特征同等处理,这种做法会将空间域变换方法局限在原始图片特征提取阶段,应用在神经网络层其他层的可解释性不强。② 通道域的注意力是对一个通道内的信息直接全局平均池化,而忽略每一个通道内的局部信息,这种做法其实也是比较暴力的行为。所以结合两种思路,就可以设计出混合域的注意力机制模型。同时对通道注意力和空间注意力进行评价打分,代表的有BAM, CBAM。
SENet 2017 通道注意力
Look what
- Squeeze-and-Excitation Networks
- SENet主要是通过显式地建模通道之间的相互依赖关系,自适应地重新校准通道的特征响应,换句话说,就是学习了通道之间的相关性,筛选出了针对通道的注意力,整个网络稍微增加了一点计算量,但是效果比较好。
- 对U先做一个Global Average Pooling(图中的Fsq(.),作者称为Squeeze过程),输出的1x1xC数据再经过两级全连接(图中的Fex(.),作者称为Excitation过程),最后用sigmoid(论文中的self-gating mechanism)限制到[0,1]的范围,把这个值作为scale乘到U的C个通道上, 作为下一级的输入数据。
- 这种结构的原理是想通过控制scale的大小,把重要的特征增强,不重要的特征减弱,从而让提取的特征指向性更强。
- 通俗的说就是:通过对卷积的到的feature map进行处理,得到一个和通道数一样的一维向量作为每个通道的评价分数,然后将改分数分别施加到对应的通道上,得到其结果,就在原有的基础上只添加了一个模块。
- 以Inception为例:
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)
CBAM 2018 空间注意力
Look where
- 为了强调空间和通道这两个维度上的有意义特征,作者依次应用通道和空间注意模块,来分别在通道和空间维度上学习关注什么、在哪里关注。此外,通过了解要强调或抑制的信息也有助于网络内的信息流动。
- 以Resnet为例,修改Bottleneck如下:
class Bottleneck(nn.Module):
expansion = 4
def __init__(self, inplanes, planes, stride=1, downsample=None):
super(Bottleneck, self).__init__()
self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=1, bias=False)
self.bn1 = nn.BatchNorm2d(planes)
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride,
padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(planes)
self.conv3 = nn.Conv2d(planes, planes * 4, kernel_size=1, bias=False)
self.bn3 = nn.BatchNorm2d(planes * 4)
self.relu = nn.ReLU(inplace=True)
self.ca = ChannelAttention(planes * 4)
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.relu(out)
out = self.conv3(out)
out = self.bn3(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
- 两个模块
class ChannelAttention(nn.Module):
def __init__(self, in_planes, ratio=16):
super(ChannelAttention, self).__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.max_pool = nn.AdaptiveMaxPool2d(1)
self.fc1 = nn.Conv2d(in_planes, in_planes // 16, 1, bias=False)
self.relu1 = nn.ReLU()
self.fc2 = nn.Conv2d(in_planes // 16, in_planes, 1, bias=False)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
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)
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.conv1 = nn.Conv2d(2, 1, kernel_size, padding=padding, bias=False)
self.sigmoid = nn.Sigmoid()
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)
SKNet 2019 卷积核注意力
- Selective Kernel Networks
- 卷积核的重要性,即不同的图像能够得到具有不同重要性的卷积核
- 据作者说,该模块在超分辨率任务上有很大提升,并且论文中的实验也证实了在分类任务上有很好的表现。
- SKNet对不同图像使用的卷积核权重不同,即一种针对不同尺度的图像动态生成卷积核。网络主要由Split、Fuse、Select三部分组成:
- Split部分是对原特征图经过不同大小的卷积核部分进行卷积的过程,这里可以有多个分支。操作包括卷积、efficient grouped/depthwise convolutions、BN。
- Fuse部分是计算每个卷积核权重的部分:
- 将两部分的特征图按元素求和
U = U ~ + U ^ \mathbf{U}=\tilde{\mathbf{U}}+\widehat{\mathbf{U}} U=U~+U - U通过全局平均池化 (GAP) 生成通道统计信息。得到的Sc维度为C * 1
s c = F g p ( U c ) = 1 H × W ∑ i = 1 H ∑ j = 1 W U c ( i , j ) s_{c}=\mathcal{F}_{g p}\left(\mathbf{U}_{c}\right)=\frac{1}{H \times W} \sum_{i=1}^{H} \sum_{j=1}^{W} \mathbf{U}_{c}(i, j) sc=Fgp(Uc)=H×W1i=1∑Hj=1∑WUc(i,j) - 经过全连接生成紧奏的特征z(维度为d *1),8是RELU激活函数,B表示批标准化 (BN),的维度 为卷积核的个数, W维度为dxC, d代表全连接后的特征维度, L在文中的值为32,r为压缩因子。
z = F f c ( s ) = δ ( B ( W s ) ) d = max ( C / r , L ) \begin{array}{l} \mathbf{z}=\mathcal{F}_{f c}(\mathbf{s})=\delta(\mathcal{B}(\mathbf{W} \mathbf{s})) \\ d=\max (C / r, L) \end{array} z=Ffc(s)=δ(B(Ws))d=max(C/r,L)
- 将两部分的特征图按元素求和
- Select部分是根据不同权重卷积核计算后得到的新的特征图的过程
- 进行softmax计算每个卷积核的权里, 计算方式如下图所示。如果是两个卷积核, 则 ac + bc = 1。 z 的维度为 (d *1) A的维度为 (C *d) ,B的维度为 (C ∗ d ) , \left.^{*} \mathrm{d}\right), ∗d), 则a = A × =\mathrm{A} \times =A× z的维度为1 ∗ C 0 * \mathrm{C}_{0} ∗C0
- Ac、Bc为A、B的第c行数据 (1*d)。ac为a的第C个元素, 这样分别得到了每个卷积核的权重。
a c = e A c z e A c z + e B c z , b c = e B c z e A c z + e B c z a_{c}=\frac{e^{\mathbf{A}_{c} \mathbf{z}}}{e^{\mathbf{A}_{c} \mathbf{z}}+e^{\mathbf{B}_{c} \mathbf{z}}}, b_{c}=\frac{e^{\mathbf{B}_{c} \mathbf{z}}}{e^{\mathbf{A}_{c} \mathbf{z}}+e^{\mathbf{B}_{c} \mathbf{z}}} ac=eAcz+eBczeAcz,bc=eAcz+eBczeBcz - 将权重应用到特征图上。其中V = [V1,V2,…, VC], Vc 维度为 (H x W) ,如果
V c = a c ⋅ U ~ c + b c ⋅ U ^ c , a c + b c = 1 \mathbf{V}_{c}=a_{c} \cdot \tilde{\mathbf{U}}_{c}+b_{c} \cdot \hat{\mathbf{U}}_{c}, \quad a_{c}+b_{c}=1 Vc=ac⋅U~c+bc⋅U^c,ac+bc=1
- 代码
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
- 思维导图(看个热闹)
Self-attention机制在网络中的应用: Non-local Module
- soft-attention (可微) / hard-attention (用RL学习)
- 在计算机视觉中,很多领域的相关工作(例如,分类、检测、分割、生成模型、视频处理等)都在使用Soft Attention,这些工作也衍生了很多不同的Soft Attention使用方法。这些方法共同的部分都是利用相关特征学习权重分布,再用学出来的权重施加在特征之上进一步提取相关知识。 不过施加权重的方式略有差别,可以总结如下:
- 加权可以作用在原图上;
- 加权可以作用在空间尺度上,给不同空间区域加权;
- 加权可以作用在Channel尺度上,给不同通道特征加权;
- 加权可以作用在不同时刻历史特征上,结合循环结构添加权重,例如机器翻译,或者视频相关的工作
Self-attention
- Self-attention结构自上而下分为三个分支,分别是query、key和value。
- 在query_conv卷积中,输入为
B×C×W×H
,输出为B×C/8×W×H
; - 在key_conv卷积中,输入为
B×C×W×H
,输出为B×C/8×W×H
; - 在value_conv卷积中,输入为
B×C×W×H
,输出为B×C×W×H
。
- 在query_conv卷积中,输入为
- 计算时通常分为三步:
- 将query和每个key进行相似度计算得到权重,常用的相似度函数有
点积,拼接,感知机方法
等; - 使用一个softmax函数对这些权重进行归一化;
- 将权重和相应的键值value进行加权求和得到最后的attention。
- 将query和每个key进行相似度计算得到权重,常用的相似度函数有
class Self_Attn(nn.Module):
""" Self attention Layer"""
def __init__(self,in_dim,activation):
super(Self_Attn,self).__init__()
self.chanel_in = in_dim
self.activation = activation
self.query_conv = nn.Conv2d(in_channels = in_dim , out_channels = in_dim//8 , kernel_size= 1)
self.key_conv = nn.Conv2d(in_channels = in_dim , out_channels = in_dim//8 , kernel_size= 1)
self.value_conv = nn.Conv2d(in_channels = in_dim , out_channels = in_dim , kernel_size= 1)
self.gamma = nn.Parameter(torch.zeros(1))
self.softmax = nn.Softmax(dim=-1)
def forward(self,x):
"""
inputs :
x : input feature maps( B X C X W X H)
returns :
out : self attention value + input feature
attention: B X N X N (N is Width*Height)
"""
m_batchsize,C,width ,height = x.size()
proj_query = self.query_conv(x).view(m_batchsize,-1,width*height).permute(0,2,1) # B X CX(N)
proj_key = self.key_conv(x).view(m_batchsize,-1,width*height) # B X C x (*W*H)
energy = torch.bmm(proj_query,proj_key) # transpose check
attention = self.softmax(energy) # BX (N) X (N)
proj_value = self.value_conv(x).view(m_batchsize,-1,width*height) # B X C X N
out = torch.bmm(proj_value,attention.permute(0,2,1) )
out = out.view(m_batchsize,C,width,height)
out = self.gamma*out + x
return out,attention
Non-local 2017
- Non-local Neural Networks
- 在捕捉长距离特征之间依赖关系的基础上提出了一种非局部信息统计的注意力机制——Self Attention
动机
- 文章中列出了卷积网络在统计全局信息时出现的三个问题如下:
- 捕获长范围特征依赖需要累积很多层的网络,导致学习效率太低;
- 由于网络需要累计很深,需要小心的设计模块和梯度;
- 当需要在比较远位置之间来回传递消息时,卷积或者时序局部操作很困难。
- 故作者基于图片滤波领域的非局部均值滤波操作思想,提出了一个泛化、简单、可直接嵌入到当前网络的非局部操作算子,可以捕获时间(一维时序信号)、空间(图片)和时空(视频序列)的长范围依赖。这样设计的好处是:
- 相比较于不断堆叠卷积和RNN算子,非局部操作直接计算两个位置(可以是时间位置、空间位置和时空位置)之间的关系即可快速捕获长范围依赖,但是会忽略其欧式距离,这种计算方法其实就是求自相关矩阵,只不过是泛化的自相关矩阵;
- 非局部操作计算效率很高,要达到同等效果,只需要更少的堆叠层;
- 非局部操作可以保证输入尺度和输出尺度不变,这种设计可以很容易嵌入到目前的网络架构中。
non-local block
- 通用公式:
y i = 1 C ( x ) ∑ ∀ j f ( x i , x j ) g ( x j ) y_{i}=\frac{1}{C(x)} \sum_{\forall j} f\left(x_{i}, x_{j}\right) g\left(x_{j}\right) yi=C(x)1∀j∑f(xi,xj)g(xj) - x x x是输入信号,CV中使用的一般是feature map
- i i i 代表的是输出位置,如空间、时间或者时空的索引,他的响应应该对 j j j进行枚举然后计算得到的
-
f
f
f 函数式计算
i
i
i和
j
j
j的相似度
相似性度量函数
-
g
g
g 函数计算feature map在
j
j
j位置的表示
映射函数
- 最终的 y y y是通过响应因子 C ( x ) C(x) C(x)进行标准化处理以后得到的
- 为了简化问题,作者简单地设置 g g g函数为一个 1 × 1 1 \times 1 1×1的卷积
- 而相似性度量函数可以有:
- Gaussian: f ( x i , x j ) = e x i T ⋅ x j , C ( x ) = ∑ ∀ j f ( x i , x j ) f\left(\mathrm{x}_{i}, \mathrm{x}_{j}\right)=e^{\mathrm{x}_{i}^{T} \cdot \mathrm{x}_{j}}, \mathcal{C}(x)=\sum_{\forall j} f\left(\mathrm{x}_{i}, \mathrm{x}_{j}\right) f(xi,xj)=exiT⋅xj,C(x)=∑∀jf(xi,xj)
- Embedded Gaussian: f ( x i , x j ) = e θ ( x i ) T ⋅ ϕ ( x j ) , C ( x ) = ∑ ∀ j f ( x i , x j ) f\left(\mathrm{x}_{i}, \mathrm{x}_{j}\right)=e^{\theta\left(\mathrm{x}_{i}\right)^{T} \cdot \phi\left(\mathrm{x}_{j}\right)}, \mathcal{C}(x)=\sum_{\forall j} f\left(\mathrm{x}_{i}, \mathrm{x}_{j}\right) f(xi,xj)=eθ(xi)T⋅ϕ(xj),C(x)=∑∀jf(xi,xj)
- Dot Product: f ( x i , x j ) = θ ( x i ) T ⋅ ϕ ( x j ) , C ( x ) = ∣ { i ∣ i is a valid index of x } ∣ f\left(\mathrm{x}_{i}, \mathrm{x}_{j}\right)=\theta\left(\mathrm{x}_{i}\right)^{T} \cdot \phi\left(\mathrm{x}_{j}\right), \mathcal{C}(x)=|\{i \mid i \text { is a valid index of } \mathrm{x}\}| f(xi,xj)=θ(xi)T⋅ϕ(xj),C(x)=∣{i∣i is a valid index of x}∣
- Concatenation:
f ( x i , x j ) = ReLU ( w f T ⋅ [ θ ( x i ) , ϕ ( x j ) ] ) , C ( x ) = ∣ { i ∣ i is a valid index of x } ∣ f\left(\mathrm{x}_{i}, \mathrm{x}_{j}\right)=\operatorname{ReLU} \left(\mathrm{w}_{f}^{T} \cdot\left[\theta\left(\mathrm{x}_{i}\right), \phi\left(\mathrm{x}_{j}\right)\right]\right), \mathcal{C}(x)=|\{i \mid i \text { is a valid index of } \mathrm{x}\}| f(xi,xj)=ReLU(wfT⋅[θ(xi),ϕ(xj)]),C(x)=∣{i∣i is a valid index of x}∣
这相当于embedded的两个点拼接作为带ReLU激活函数全连接层的输入。它在visual reasoning中用的比较多。
- 计算量偏大:在高阶语义层引入non local layer, 也可以在具体实现的过程中添加pooling层来进一步减少计算量
import torch
from torch import nn
from torch.nn import functional as F
class _NonLocalBlockND(nn.Module):
"""
调用过程
NONLocalBlock2D(in_channels=32),
super(NONLocalBlock2D, self).__init__(in_channels,
inter_channels=inter_channels,
dimension=2, sub_sample=sub_sample,
bn_layer=bn_layer)
"""
def __init__(self,
in_channels,
inter_channels=None,
dimension=3,
sub_sample=True,
bn_layer=True):
super(_NonLocalBlockND, self).__init__()
assert dimension in [1, 2, 3]
self.dimension = dimension
self.sub_sample = sub_sample
self.in_channels = in_channels
self.inter_channels = inter_channels
if self.inter_channels is None:
self.inter_channels = in_channels // 2
# 进行压缩得到channel个数
if self.inter_channels == 0:
self.inter_channels = 1
if dimension == 3:
conv_nd = nn.Conv3d
max_pool_layer = nn.MaxPool3d(kernel_size=(1, 2, 2))
bn = nn.BatchNorm3d
elif dimension == 2:
conv_nd = nn.Conv2d
max_pool_layer = nn.MaxPool2d(kernel_size=(2, 2))
bn = nn.BatchNorm2d
else:
conv_nd = nn.Conv1d
max_pool_layer = nn.MaxPool1d(kernel_size=(2))
bn = nn.BatchNorm1d
self.g = conv_nd(in_channels=self.in_channels,
out_channels=self.inter_channels,
kernel_size=1,
stride=1,
padding=0)
if bn_layer:
self.W = nn.Sequential(
conv_nd(in_channels=self.inter_channels,
out_channels=self.in_channels,
kernel_size=1,
stride=1,
padding=0), bn(self.in_channels))
nn.init.constant_(self.W[1].weight, 0)
nn.init.constant_(self.W[1].bias, 0)
else:
self.W = conv_nd(in_channels=self.inter_channels,
out_channels=self.in_channels,
kernel_size=1,
stride=1,
padding=0)
nn.init.constant_(self.W.weight, 0)
nn.init.constant_(self.W.bias, 0)
self.theta = conv_nd(in_channels=self.in_channels,
out_channels=self.inter_channels,
kernel_size=1,
stride=1,
padding=0)
self.phi = conv_nd(in_channels=self.in_channels,
out_channels=self.inter_channels,
kernel_size=1,
stride=1,
padding=0)
if sub_sample:
self.g = nn.Sequential(self.g, max_pool_layer)
self.phi = nn.Sequential(self.phi, max_pool_layer)
def forward(self, x):
'''
:param x: (b, c, h, w)
:return:
'''
batch_size = x.size(0)
g_x = self.g(x).view(batch_size, self.inter_channels, -1)#[bs, c, w*h]
g_x = g_x.permute(0, 2, 1)
theta_x = self.theta(x).view(batch_size, self.inter_channels, -1)
theta_x = theta_x.permute(0, 2, 1)
phi_x = self.phi(x).view(batch_size, self.inter_channels, -1)
f = torch.matmul(theta_x, phi_x)
print(f.shape)
f_div_C = F.softmax(f, dim=-1)
y = torch.matmul(f_div_C, g_x)
y = y.permute(0, 2, 1).contiguous()
y = y.view(batch_size, self.inter_channels, *x.size()[2:])
W_y = self.W(y)
z = W_y + x
return z
- 神经网络中还有一个常见的操作也是利用的全局信息,那就是Linear层,全连接层将feature map上每一个点的信息都进行了融合,Linear可以看做一种特殊的Non local操作。
NLNet不足
- Non-local Neural Networks模块依然存在以下的不足:
- 只涉及到了位置注意力模块,而没有涉及常用的通道注意力机制
- 可以看出如果特征图较大,那么两个(batch,hxw,512)矩阵乘是非常耗内存和计算量的,也就是说当输入特征图很大存在效率底下问题,虽然有其他办法解决例如缩放尺度,但是这样会损失信息,不是最佳处理办法。
- 改进思路
Non-local + Attention
动机
- Non-local Network(NLNet)使用自注意力机制来建模远程依赖。对于每个查询点(query position),NLNet 首先计算查询点与所有点之间的成对关系以得到注意力图,然后通过加权和的方式聚合所有点的特征,从而得到与此查询点相关的全局特征,最终再分别将全局特征加到每个查询点的特征中,完成远程依赖的建模过程。但是,NLNet存在着计算量大的问题。
- SENet模块中提出的SE block是使用全局上下文对不同通道进行权值重标定,对通道依赖进行调整。但是采用这种方法,并没有充分利用全局上下文信息。CBAM将注意力过程分为两个独立的部分,通道注意力模块(look what)和空间注意力模块(look where)。不仅可以节约参数和计算力,而且保证了其可以作为即插即用的模块集成到现有的网络架构中去。但是CBAM需要手工设计pooling,多层感知器等复杂操作。
DANet 2018
- Dual Attention Network for Scene Segmentation
- DANet是一种经典的应用self-Attention的网络,它引入了一种自注意力机制来分别捕获空间维度和通道维度中的特征依赖关系。
- 场景分割需要预测出图像中的像素点属于某一目标类或场景类,其图像场景的复杂多样(光照,视角,尺度,遮挡等)对于场景的理解和像素点的判别造成很大困难。
- 主流场景分割方法大致可分为以下两种类型:① 通过使用多尺度特征融合的方式增强特别的表达,例如空间金字塔结构 (PSP,ASPP) 或者高层浅层特征融合 (RefineNet)。但是这些方式没有考虑到不同特征之间的关联依赖,而这对于场景的理解确实十分重要。② 利用 RNN 网络构建特征长范围的特征关联,但这种关联往往受限于 RNN 的 long-term memorization。
- 双重注意网络(DANet)来自适应地集成局部特征和全局依赖。在传统的扩张FCN之上附加两种类型的注意力模块,分别模拟空间和通道维度中的语义相互依赖性
Position Attention Module(PAM)
- 通过所有位置处的特征的加权和来选择性地聚合每个位置的特征。无论距离如何,类似的特征都将彼此相关。
Channel Attention Module(CAM)
- 通过整合所有通道映射之间的相关特征来选择性地强调存在相互依赖的通道映射。
总结
- 总的来说,DANet网络主要思想是 CBAM 和 non-local 的融合变形。把deep feature map进行spatial-wise self-attention,同时也进行channel-wise self-attetnion,最后将两个结果进行 element-wise sum 融合。
- 在 CBAM 分别进行空间和通道 self-attention的思想上,直接使用了 non-local 的自相关矩阵 Matmul 的形式进行运算,避免了 CBAM 手工设计 pooling,多层感知器等复杂操作。
GCNet 2019
- Global Context Network
- 作者对捕获长距离依赖关系的方法分成:
- 自注意力机制来建模query对的关系
NLNet
- 对query-independent (可以理解为无query依赖) 的全局上下文建模
SENet
- 自注意力机制来建模query对的关系
- 作者觉得上面两个有缺憾:
- NLNet就是采用自注意力机制来建模像素对关系。然而NLNet对于每一个位置学习不受位置依赖的attention map,造成了大量的计算浪费
- SENet用全局上下文对不同通道进行权值重标定,来调整通道依赖。然而,采用权值重标定的特征融合,不能充分利用全局上下文
- 作者对NLNet进行了一点分析,发现可视化结果以及实验数据证明,non-local network的全局上下文在不同位置几乎是相同的,这表明学习到了无位置依赖的全局上下文
- 作者希望能够减少不必要的计算量,降低计算,并结合SENet设计,提出了GCNet融合了两者的优点,既能够有用NLNet的全局上下文建模能力,又能够像SENet一样轻量。
Simplified NL Block
- 简化的NL Block的公式定义如下:
z i = x i + ∑ j = 1 N p exp ( W k x j ) ∑ m = 1 N p exp ( W k x m ) ( W v ⋅ x j ) \mathbf{z}_{i}=\mathbf{x}_{i}+\sum_{j=1}^{N_{p}} \frac{\exp \left(W_{k} \mathbf{x}_{j}\right)}{\sum_{m=1}^{N_{p}} \exp \left(W_{k} \mathbf{x}_{m}\right)}\left(W_{v} \cdot \mathbf{x}_{j}\right) zi=xi+j=1∑Np∑m=1Npexp(Wkxm)exp(Wkxj)(Wv⋅xj)- W k W_{k} Wk 和 W v W_{v} Wv 表示为线性转换矩阵
- 进一步简化计算量
- 公式
z i = x i + W v ∑ j = 1 N p exp ( W k x j ) ∑ m = 1 N p exp ( W k x m ) x j \mathbf{z}_{i}=\mathbf{x}_{i}+W_{v} \sum_{j=1}^{N_{p}} \frac{\exp \left(W_{k} \mathbf{x}_{j}\right)}{\sum_{m=1}^{N_{p}} \exp \left(W_{k} \mathbf{x}_{m}\right)} \mathbf{x}_{j} zi=xi+Wvj=1∑Np∑m=1Npexp(Wkxm)exp(Wkxj)xj - 这样修改之后, 1x1卷积 W v W_{v} Wv 的FLOPs从 O ( H W C 2 ) \mathcal{O}\left(\mathrm{HWC}^{2}\right) O(HWC2) 降到 O ( C 2 ) \mathcal{O}\left(\mathrm{C}^{2}\right) O(C2)
- 简化版non-local block的第二项是不受位置依赖的,所有位置共享这一项。因此,作者直接将全局上下文建模为所有位置特征的加权平均值,然后聚集全局上下文特征到每个位置的特征上。
GCNet模块
- 作者提出了一种新的全局上下文建模框架,global context block(简写GCNet):能够像Non-local block一样建立有效的长距离依赖,又能够像SE block一样省计算量
- global attention pooling用于上下文建模。
- bottleneck transform来捕获通道间依赖。
- broadcast element-wise addition用于特征融合。
- 因为两层bottleneck transform增加了优化难度,所以在ReLU前面增加一个layer normalization层(降低优化难度且作为正则提高了泛化性)
代码
- GC block在ResNet中的使用位置是每两个Stage之间的连接部分,下边是GC block的官方实现(基于mmdetection进行修改):
import torch
from torch import nn
class ContextBlock(nn.Module):
def __init__(self,inplanes,ratio,pooling_type='att',
fusion_types=('channel_add', )):
super(ContextBlock, self).__init__()
valid_fusion_types = ['channel_add', 'channel_mul']
assert pooling_type in ['avg', 'att']
assert isinstance(fusion_types, (list, tuple))
assert all([f in valid_fusion_types for f in fusion_types])
assert len(fusion_types) > 0, 'at least one fusion should be used'
self.inplanes = inplanes
self.ratio = ratio
self.planes = int(inplanes * ratio)
self.pooling_type = pooling_type
self.fusion_types = fusion_types
if pooling_type == 'att':
self.conv_mask = nn.Conv2d(inplanes, 1, kernel_size=1)
self.softmax = nn.Softmax(dim=2)
else:
self.avg_pool = nn.AdaptiveAvgPool2d(1)
if 'channel_add' in fusion_types:
self.channel_add_conv = nn.Sequential(
nn.Conv2d(self.inplanes, self.planes, kernel_size=1),
nn.LayerNorm([self.planes, 1, 1]),
nn.ReLU(inplace=True), # yapf: disable
nn.Conv2d(self.planes, self.inplanes, kernel_size=1))
else:
self.channel_add_conv = None
if 'channel_mul' in fusion_types:
self.channel_mul_conv = nn.Sequential(
nn.Conv2d(self.inplanes, self.planes, kernel_size=1),
nn.LayerNorm([self.planes, 1, 1]),
nn.ReLU(inplace=True), # yapf: disable
nn.Conv2d(self.planes, self.inplanes, kernel_size=1))
else:
self.channel_mul_conv = None
def spatial_pool(self, x):
batch, channel, height, width = x.size()
if self.pooling_type == 'att':
input_x = x
# [N, C, H * W]
input_x = input_x.view(batch, channel, height * width)
# [N, 1, C, H * W]
input_x = input_x.unsqueeze(1)
# [N, 1, H, W]
context_mask = self.conv_mask(x)
# [N, 1, H * W]
context_mask = context_mask.view(batch, 1, height * width)
# [N, 1, H * W]
context_mask = self.softmax(context_mask)
# [N, 1, H * W, 1]
context_mask = context_mask.unsqueeze(-1)
# [N, 1, C, 1]
context = torch.matmul(input_x, context_mask)
# [N, C, 1, 1]
context = context.view(batch, channel, 1, 1)
else:
# [N, C, 1, 1]
context = self.avg_pool(x)
return context
def forward(self, x):
# [N, C, 1, 1]
context = self.spatial_pool(x)
out = x
if self.channel_mul_conv is not None:
# [N, C, 1, 1]
channel_mul_term = torch.sigmoid(self.channel_mul_conv(context))
out = out * channel_mul_term
if self.channel_add_conv is not None:
# [N, C, 1, 1]
channel_add_term = self.channel_add_conv(context)
out = out + channel_add_term
return out
if __name__ == "__main__":
in_tensor = torch.ones((12, 64, 128, 128))
cb = ContextBlock(inplanes=64, ratio=1./16.,pooling_type='att')
out_tensor = cb(in_tensor)
print(in_tensor.shape)
print(out_tensor.shape)