EPSANet: 卷积神经网络上的有效金字塔挤压注意力块
摘要
引言
注意机制已被广泛应用于多个计算机视觉领域,并非仅限于图像分类、目标检测、实例分割、语义分割、场景解析和动作定位等技术方向[1-7]。具体而言,注意方法可分为两类:渠道注意与空间注意[8-12]。其中,采用渠道注意力模块能够显著提升性能[13]。然而,SENet存在忽视空间信息的重要性的缺陷,为此提出了瓶颈注意力模块(BAM)[14]以及卷积块注意力模块(CBAM)[5],通过融合空间与渠道注意以丰富注意图特征[5]。然而,这一方向仍面临两个关键挑战:第一,如何高效提取并利用多尺度特征图的空间信息以拓展特征空间;第二,现有渠道/空间注意力机制仅能捕捉本地信息而难以建立远程通道依赖性关系[2,18,19]。针对上述问题,已有诸多研究试图提供解决方案:例如基于多尺度特征表示及跨通道信息交互的方法如PyConv[15]、Res2Net[16]以及hs-resnet[17]等;另一类则可建立远程信道依赖性关系[2,18,19]等。然而,现有方法往往导致模型复杂度显著提高,从而给网络带来了沉重的计算负担。鉴于此,开发一个既能保证性能又能降低计算开销的成本低效注意机制具有重要意义[5].在此研究中,我们提出了一种新型低成本高性能的金字塔挤压注意力(PSA)模块.该模块具备多尺度处理输入张量的能力:具体而言,通过构建多尺度金字塔卷积结构来整合输入特征图的信息,同时借助通道压缩操作有效提取不同尺度的空间信息,从而实现相邻尺度上下文特征的精确融合**;此外,通过Softmax操作对各通道的关注权重进行归一化校准,最终构建出具有远程通道依赖性的跨维度交互关系.将该PSA模块替换传统ResNet瓶颈块中的3×3卷积操作所得新型块命名为高效金字塔挤压注意力(EPSA)块;并通过将EPSA块串联成ResNet式结构构建了新型网络EPSANet.实验结果表明:所提出的EPSANet不仅在Top-1精度指标上超越现有技术方案;其参数规模也呈现出显著优势(如图所示).
这项工作的主要贡献总结如下:
- 提出了一种新颖的高效金字塔挤压注意 (EPSA) 块,该块可以有效地提取多尺度空间信息,并发展出远程信道依赖性。所提出的EPSA模块非常灵活且可扩展,因此可以应用于各种各样的网络体系结构,用于计算机视觉的许多任务。
- 提出了一种名为EPSANet的新型主干体系结构,该体系结构可以学习更丰富的多尺度特征表示,并自适应地重新校准跨维度的通道注意力权重。
- 广泛的实验表明,所提出的EPSANet可以在ImageNet和COCO数据集上跨图像分类,对象检测和实例分割实现良好的效果
相关工作
注意力机制
注意机制旨在强化信息最具有影响力的特征表达,并抑制那些不具竞争力的特征表达。通过这种方式使模型能够根据上下文动态聚焦于关键区域。[13] 中所提到的挤压与激励 (SE) 注意机制可以通过调节通道规模来捕捉通道间的相关性关系。[5] 中提出的CBAM方法则通过引入大尺寸内核的最大池特征来丰富注意力图谱。在CBAM的基础上,[20] 提出了GSoP方法以实现二阶池化技术以获取更为丰富的特征聚合结果。最近研究表明,[19] 非本地块构建密集的空间特征图并有效捕获远程依赖性能力显著提升模型性能。基于非局部块构建的双注意网络(A2Net)[8]引入了一种创新的关系函数将空间信息嵌入到特征图中进行处理。随后,[21] 提出了SKNet动态选择注意机制允许神经元根据输入特征图多尺度自适应地调整感受野大小以增强模型灵活性与适应性能力。ResNeSt [12] 则通过拆分注意力块实现了跨输入特征图组的关注机制从而提升计算效率与模型性能表现。Fcanet [9] 采用多频谱信道注意机制实现了频域内的信道相关性预处理过程并显著提升了模型对复杂数据模式识别能力。GCNet [1] 简化了空间注意模块设计成功降低了完全连接层带来的计算开销并增强了模型对长距离依赖关系的捕捉能力ECANet [11] 通过一维卷积层有效降低了完全连接层带来的冗余计算问题同时保留了深度学习模型的核心优势特性.DANet [18] 创新性地将来自不同分支的两个注意模块相加实现了局部与全局注意力的有效融合从而提高了模型的整体性能与计算效率上述研究方法主要集中在设计更为复杂的注意力模块或是构建更加高效的依赖关系网络之间取得折中平衡以期达到提升模型性能的目的为此我们提出了一种低复杂度注意力权重学习的新模块PSA该模块不仅能够有效整合局部与全局注意力信息还能够建立更加完善的远程通道依赖关系网络从而进一步优化了模型结构提升了运行效率与整体性能表现
注意机制用于加强信息最丰富的特征表达的分配,同时抑制不太有用的特征表达,从而使模型自适应地关注上下文中的重要区域.[13]
中的挤压和激励(SE)Notice可以通过选择性地调制通道的规模来捕获通道相关性.[5]
中的CBAM通过为具有大尺寸内核的渠道注意力添加最大池特征来丰富注意力图.
在CBAM的基础上,[20]
提出了GSoP提出了一种二阶池化方法来提取更丰富的特征聚合.
最近提出了非本地块[19]
来构建密集的空间特征图,并通过非本地操作捕获远程依赖性.
基于非局部块,双注意网络(A2Net)[8]
引入了一种新颖的关系函数,将具有空间信息
多尺度表示
多尺度特征表示能力对多种视觉任务具有重要意义,在实例分割等问题[22]、人脸分析等问题[23]、对象检测等问题[24]、显着对象检测等问题[25]以及语义分割等问题[7]等方面均发挥着关键作用。构建能够更高效地提取多尺度特征以实现视觉识别任务的关键作用在于卷积神经网络(CNN)的设计。另一方面,在CNN架构中自然形成的机制能够逐步捕捉从粗等到精细的多层次特征。值得注意的是,在改进CNN算法时关注于优化其在多层次特征表示方面的性能同样具有重要意义。
方法
回顾SEnet
通道注意机制通过有选择地对各通道重要性赋予权重(权重),从而进一步提高网络的感知能力;随后通过 squeeze 和 excitation 两个模块分别编码全局信息和自适应调整信道关系;通常可通过 全局平均池 生成信道统计信息(统计量),该池可将全局空间信息融入到信道描述符中。

通过引入两个全连接层(...),我们可以提升各信道间关系处理效率的同时促进不同尺度特征间的互动。其中σ代表激活函数,在实际应用中常用Sigmoid作为激活函数。借助激活机制,在信道间交互完成之后赋予各信道一个权重系数(...),从而使得信道间能够更加高效地融合特征。
PSA模块

PSA模块主要通过四个步骤实现。
首先, 利用SPC模块从信道中提取多尺度特征图.
其次, 通过SEWeight模块从不同比例中提取对应的注意力, 得到各通道独立的空间感知信息.
再次, 借助Softmax函数对通道注意向量进行归一化处理, 从而实现多尺度空间感知权重的学习.
最终, 采用加权乘法操作对重新归一化的权重与原始特征图进行融合, 最终输出更加丰富的多尺度精细特征信息.

如图4所示,在所提出的PSA架构中实现了多尺度特征提取的关键组件为SPC。我们采用了多支路设计来提取输入特征图的空间信息,并设定每支路的输入通道数为C。每个支路能够独立学习不同尺度的空间表示,并通过本地机制构建跨支路交互关系。值得注意的是,在内核尺寸扩大时会带来大量参数量的增长。为此,提出了一种群卷积机制。通过引入一种群卷积方法,则可以有效缓解这一挑战问题并提升模型性能。此外还设计了一种新的优化准则用于调节组尺寸的选择过程以进一步提高模型效率和准确性。



该模块SEWeight从多尺度输入特征图中提取注意力权重。在这一过程中, 我们的PSA模块能够融合不同层次的信息, 并生成更高层次的关注力。进一步而言, 为了实现信息交互, 在不影响原始通道关注的前提下整合多维数据. 从而通过串联形式整合所有层次通道关注.

其中通过Softmax运算获得各尺度通道的重新加权系数...这些系数包含了空间域内所有位置的信息以及各通道内的关注权重值。经过这种设计,在本地与全局渠道注意力之间实现了有效的信息交互机制建立。随后将各通道经过重新加权后的注意力特征以串联的形式融合在一起,并完成拼接操作过程。这样最终构建了一个整合了各通道注意力信息的整体关注向量。


import torch
import torch.nn as nn
import math
class SEWeightModule(nn.Module):
def __init__(self, channels, reduction=16):
super(SEWeightModule, self).__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.fc1 = nn.Conv2d(channels, channels//reduction, kernel_size=1, padding=0)
self.relu = nn.ReLU(inplace=True)
self.fc2 = nn.Conv2d(channels//reduction, channels, kernel_size=1, padding=0)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
out = self.avg_pool(x)
out = self.fc1(out)
out = self.relu(out)
out = self.fc2(out)
weight = self.sigmoid(out)
return weight
def conv(in_planes, out_planes, kernel_size=3, stride=1, padding=1, dilation=1, groups=1):
"""standard convolution with padding"""
return nn.Conv2d(in_planes, out_planes, kernel_size=kernel_size, stride=stride,
padding=padding, dilation=dilation, groups=groups, bias=False)
class PSAModule(nn.Module):
def __init__(self, input_channels, out_channels, conv_kernels=[3, 5, 7, 9], stride=1, conv_groups=[1, 4, 8, 16]):
super(PSAModule, self).__init__()
#四个不同大小的卷积核,采用的是分组卷积。
self.conv_1 = conv(input_channels, out_channels // 4, kernel_size=conv_kernels[0], padding=conv_kernels[0] // 2,
stride=stride, groups=conv_groups[0])
self.conv_2 = conv(input_channels, out_channels // 4, kernel_size=conv_kernels[1], padding=conv_kernels[1] // 2,
stride=stride, groups=conv_groups[1])
self.conv_3 = conv(input_channels, out_channels // 4, kernel_size=conv_kernels[2], padding=conv_kernels[2] // 2,
stride=stride, groups=conv_groups[2])
self.conv_4 = conv(input_channels, out_channels // 4, kernel_size=conv_kernels[3], padding=conv_kernels[3] // 2,
stride=stride, groups=conv_groups[3])
self.se = SEWeightModule(out_channels // 4)
self.split_channel = out_channels // 4
self.softmax = nn.Softmax(dim=1)
def forward(self, x):
batch_size = x.shape[0]
x1 = self.conv_1(x)
x2 = self.conv_2(x)
x3 = self.conv_3(x)
x4 = self.conv_4(x)
feats = torch.cat((x1, x2, x3, x4), dim=1)
feats = feats.view(batch_size, 4, self.split_channel, feats.shape[2], feats.shape[3])
x1_se = self.se(x1)
x2_se = self.se(x2)
x3_se = self.se(x3)
x4_se = self.se(x4)
x_se = torch.cat((x1_se, x2_se, x3_se, x4_se), dim=1)
attention_vectors = x_se.view(batch_size, 4, self.split_channel, 1, 1)
attention_vectors = self.softmax(attention_vectors)
feats_weight = feats * attention_vectors
for i in range(4):
x_se_weight_fp = feats_weight[:, i, :, :]
if i == 0:
out = x_se_weight_fp
else:
out = torch.cat((x_se_weight_fp, out), 1)
return out
if __name__=='__main__':
model=PSAModule(input_channels=64, out_channels=64)
input=torch.randn(1,64,64,64)
output=model(input)
print(output.shape)

上图介绍了三个不同的resnet风格的残差块。

参考文献:
该研究提出了一种新型的多尺度通道注意力机制EPSANet,在模型架构设计中巧妙地融入了金字塔式分割注意力模块这一核心创新点。通过即时接入式设计实现了模块间的无缝集成与协同工作,在实验测试中展现出显著的性能优势。该方法不仅具有较高的计算效率,在实现上也极为便捷灵活,并且已经开源并提供研究人员参考使用...
import torch
import torch.nn as nn
import math
class SEWeightModule(nn.Module):
def __init__(self, channels, reduction=16):
super(SEWeightModule, self).__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.fc1 = nn.Conv2d(channels, channels//reduction, kernel_size=1, padding=0)
self.relu = nn.ReLU(inplace=True)
self.fc2 = nn.Conv2d(channels//reduction, channels, kernel_size=1, padding=0)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
out = self.avg_pool(x)
out = self.fc1(out)
out = self.relu(out)
out = self.fc2(out)
weight = self.sigmoid(out)
return weight
def conv(in_planes, out_planes, kernel_size=3, stride=1, padding=1, dilation=1, groups=1):
"""standard convolution with padding"""
return nn.Conv2d(in_planes, out_planes, kernel_size=kernel_size, stride=stride,
padding=padding, dilation=dilation, groups=groups, bias=False)
def conv1x1(in_planes, out_planes, stride=1):
"""1x1 convolution"""
return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False)
class PSAModule(nn.Module):
def __init__(self, inplans, planes, conv_kernels=[3, 5, 7, 9], stride=1, conv_groups=[1, 4, 8, 16]):
super(PSAModule, self).__init__()
self.conv_1 = conv(inplans, planes//4, kernel_size=conv_kernels[0], padding=conv_kernels[0]//2,
stride=stride, groups=conv_groups[0])
self.conv_2 = conv(inplans, planes//4, kernel_size=conv_kernels[1], padding=conv_kernels[1]//2,
stride=stride, groups=conv_groups[1])
self.conv_3 = conv(inplans, planes//4, kernel_size=conv_kernels[2], padding=conv_kernels[2]//2,
stride=stride, groups=conv_groups[2])
self.conv_4 = conv(inplans, planes//4, kernel_size=conv_kernels[3], padding=conv_kernels[3]//2,
stride=stride, groups=conv_groups[3])
self.se = SEWeightModule(planes // 4)
self.split_channel = planes // 4
self.softmax = nn.Softmax(dim=1)
def forward(self, x):
batch_size = x.shape[0]
x1 = self.conv_1(x)
x2 = self.conv_2(x)
x3 = self.conv_3(x)
x4 = self.conv_4(x)
feats = torch.cat((x1, x2, x3, x4), dim=1)
feats = feats.view(batch_size, 4, self.split_channel, feats.shape[2], feats.shape[3])
x1_se = self.se(x1)
x2_se = self.se(x2)
x3_se = self.se(x3)
x4_se = self.se(x4)
x_se = torch.cat((x1_se, x2_se, x3_se, x4_se), dim=1)
attention_vectors = x_se.view(batch_size, 4, self.split_channel, 1, 1)
attention_vectors = self.softmax(attention_vectors)
feats_weight = feats * attention_vectors
for i in range(4):
x_se_weight_fp = feats_weight[:, i, :, :]
if i == 0:
out = x_se_weight_fp
else:
out = torch.cat((x_se_weight_fp, out), 1)
return out
class EPSABlock(nn.Module):
expansion = 4
def __init__(self, inplanes, planes, stride=1, downsample=None, norm_layer=None, conv_kernels=[3, 5, 7, 9],
conv_groups=[1, 4, 8, 16]):
super(EPSABlock, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
# Both self.conv2 and self.downsample layers downsample the input when stride != 1
self.conv1 = conv1x1(inplanes, planes)
self.bn1 = norm_layer(planes)
self.conv2 = PSAModule(planes, planes, stride=stride, conv_kernels=conv_kernels, conv_groups=conv_groups)
self.bn2 = norm_layer(planes)
self.conv3 = conv1x1(planes, planes * self.expansion)
self.bn3 = norm_layer(planes * self.expansion)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
self.stride = stride
def forward(self, x):
identity = 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)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
class EPSANet(nn.Module):
def __init__(self,block, layers, num_classes=1000):
super(EPSANet, self).__init__()
self.inplanes = 64
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.layer1 = self._make_layers(block, 64, layers[0], stride=1)
self.layer2 = self._make_layers(block, 128, layers[1], stride=2)
self.layer3 = self._make_layers(block, 256, layers[2], stride=2)
self.layer4 = self._make_layers(block, 512, layers[3], stride=2)
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(512 * block.expansion, num_classes)
for m in self.modules():
if isinstance(m, nn.Conv2d):
n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
m.weight.data.normal_(0, math.sqrt(2. / n))
elif isinstance(m, nn.BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
def _make_layers(self, block, planes, num_blocks, stride=1):
downsample = None
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
nn.Conv2d(self.inplanes, planes * block.expansion,
kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(planes * block.expansion),
)
layers = []
layers.append(block(self.inplanes, planes, stride, downsample))
self.inplanes = planes * block.expansion
for i in range(1, num_blocks):
layers.append(block(self.inplanes, planes))
return nn.Sequential(*layers)
def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = self.avgpool(x)
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
def epsanet50():
model = EPSANet(EPSABlock, [3, 4, 6, 3], num_classes=1000)
return model
def epsanet101():
model = EPSANet(EPSABlock, [3, 4, 23, 3], num_classes=1000)
return model
