
1. 项目背景与设计思路1.1 为什么把“金字塔”和“注意力”放在一起做视觉模型的人应该都有这种感觉注意力机制这几年已经从“可选插件”变成了“默认标配”。从最早的SENet通道注意力到后来的CBAM、CA、Coordinate Attention再到Transformer系的全局自注意力大家其实都在解决同一个问题——让网络知道“该看哪里”和“该重视什么”。但真正落到工程里你会发现一个很现实的问题大部分注意力模块在提升精度的同时也在悄悄增加计算量和参数量有些模块甚至会让推理速度明显变慢。Pyramid Squeeze Attention按字面理解就是“金字塔挤压注意力”。这个命名很直接地透露了三件事第一它用了金字塔结构来捕捉多尺度信息第二它借鉴了SENet里经典的Squeeze操作把空间信息压缩成通道描述符第三它最终输出的是注意力权重用来对特征图做重标定。说白了这就是一个轻量化、多尺度、即插即用的通道注意力模块。我最初接触这个思路是因为一个很具体的场景给一个移动端分类模型提点但又不能引入太多额外耗时。当时的约束很明确——FLOPs增量控制在3%以内内存占用不能有明显跳变而且要能直接塞进已有训练管线不改变原有损失函数和优化策略。试过加SE模块涨点有但不够明显试过加非局部注意力精度上去了可速度掉得心疼。后来看到Pyramid Squeeze Attention这个方向才发现问题可能出在“单尺度描述”上。1.2 这个模块到底能解决什么问题先讲一个很容易被忽视的细节通道注意力的核心是“全局描述符”。SENet用全局平均池化把整张特征图压成一个向量再学习每个通道的重要性。但全局平均池化有个天然缺陷——它把不同尺度的目标一视同仁地压成了一个点。如果图像里同时存在一只鸟和一整片树林全局平均池化只会得到一个“混合了鸟和树林”的平均响应通道重要性自然分不清主次。Pyramid Squeeze Attention的思路很直接既然单尺度池化不够那就用多个不同尺寸的池化核同时做压缩再把多尺度描述融合起来。小鸟对应的小目标信息从细粒度池化里保留大片的树林背景从粗粒度池化里保留最后融合成一个信息量更丰富的通道描述符。这个设计既保留了SENet那种“轻量高效”的优点又补上了“多尺度上下文缺失”的短板。这个模块适合谁来用如果你正在做图像分类、目标检测、语义分割这类基础视觉任务想在不大改网络结构的前提下提点或者你在做模型轻量化、需要找一个“性价比高”的注意力插件那这类金字塔挤压思路很值得一试。它不挑主干网络ResNet、MobileNet、RepVGG都能直接往上挂也不需要额外标注数据纯监督训练就能生效。2. 核心机制拆解金字塔聚合、挤压编码与通道重标定2.1 金字塔结构是怎么搭起来的金字塔这个词在视觉领域其实有两种常见的理解方式。一种是图像金字塔把输入图像不断缩放成不同分辨率像经典的目标检测做法那样。另一种是特征金字塔在不同层级的特征图上做信息融合。Pyramid Squeeze Attention里的“金字塔”更准确地说是介于这两者之间——它不改变输入分辨率而是在同一张特征图上做多尺度空间池化。具体来说给定一个形状为 C×H×W 的特征图 X金字塔分支会把它划分成不同粒度的空间网格。最粗的分支对应1×1网格也就是整张图一个池化区域中间分支对应2×2网格把特征图切成四块分别池化更细的分支可以对应4×4甚至6×6网格。每个网格区域内部做平均池化得到一个值。这样1×1分支输出1个值2×2分支输出4个值4×4分支输出16个值。这些值组合起来就是这张特征图在不同空间粒度下的响应概览。拿实际场景举例如果图像里有一只小猫占画面中间1/4区域那么2×2池化中左上那块网格会产生较高的响应而1×1池化会把小猫和背景混在一起响应被稀释。网络同时拿到这两种描述后就能既感知“局部有目标”又感知“全局大致长什么样”。2.2 Squeeze操作在金字塔分支里扮演的角色SENet的Squeeze操作本质上就是一个全局平均池化把 H×W 的空间维度压成1×1得到 C 维向量。这个操作的意义在于把空间信息聚合成“通道级统计量”。但在Pyramid Squeeze Attention里Squeeze不是只做一次而是在金字塔的每个分支上分别做。每个分支做完池化之后输出形状是 1×K×KK是池化网格尺寸不能直接拼成向量。所以还需要一个轻量级的映射层把这些多尺度描述统一映射到相同的维度空间。常见的做法是用1×1卷积或全连接层把每个分支的输出压缩到预设维度再展平拼接最终得到一个融合了多尺度信息的通道描述向量 z。这里有个设计细节值得注意每个分支的压缩层可以是独立的也可以是共享的。用共享映射层可以省参数但不同尺度的池化输出统计特性差异较大共享参数可能存在信息干扰。实际工程中我给每个分支单独配了一个1×1卷积维度设置为总通道数的 1/reduction再在所有分支间平均分配。比如某层输入是128通道reduction取16那总压缩维度是83个分支各分配约3维最后拼接成一个8维向量。这里的reduction机制和SENet里同名的超参是一个作用——控制注意力模块的瓶颈宽度折中表达能力和额外开销。2.3 注意力权重如何生成并作用到特征图上拿到融合后的通道描述向量 z 之后接下来就是经典的“注意力生成”环节。先用一个全连接层或者1×1卷积把 z 映射回原始通道数 C再经过Sigmoid激活得到每个通道的权重系数。这一步的数学表达很简单s Sigmoid(FC(z))Y X * s其中 s 是 C 维向量每个值落在0到1之间表示对应通道的重要性。X 是输入特征图Y 是重标定后的输出特征图。为了避免Sigmoid输出饱和有些实现会在映射层后加一个LayerNorm或BatchNorm把激活前的分布拉回零均值附近。这个细节在训练初期特别有用我在后面“常见问题”部分会展开说。整体来看Pyramid Squeeze Attention的前向过程可以概括成三步多尺度池化提取空间概览 → 分组映射压缩成通道描述 → 全连接恢复并加权原始特征。每一步都很轻加起来也就是几十个卷积参数和几次池化操作对整体FLOPs的影响非常小。这也是它作为“即插即用模块”的底气所在。3. 从公式到代码一个可落地的PyTorch实现3.1 模块实现细节概念讲再多不如直接看代码。下面这个实现是我在实际项目中验证过的版本去掉了花哨的部分保留了核心逻辑。它接收一个四维张量batch, channels, height, width输出相同形状的加权特征图。import torch import torch.nn as nn import torch.nn.functional as F class PyramidSqueezeAttention(nn.Module): def __init__(self, in_channels, reduction16, pyramid_levels(1, 2, 4)): super().__init__() self.in_channels in_channels self.reduction reduction self.pyramid_levels pyramid_levels self.num_branches len(pyramid_levels) # 每个金字塔分支独立的轻量映射层1x1卷积输出维度按照分支数均分 self.branch_convs nn.ModuleList() hidden_dim max(in_channels // reduction // self.num_branches, 1) for _ in range(self.num_branches): self.branch_convs.append( nn.Conv2d(in_channels, hidden_dim, kernel_size1, biasFalse) ) # 融合层把多分支特征投影到统一空间 self.fuse nn.Sequential( nn.Conv2d(hidden_dim * self.num_branches, in_channels, kernel_size1, biasFalse), nn.BatchNorm2d(in_channels), nn.ReLU(inplaceTrue) ) # 注意力生成层先把通道压缩到 bottleneck再映射回原通道数 bottleneck max(in_channels // reduction, 4) self.attention nn.Sequential( nn.Conv2d(in_channels, bottleneck, kernel_size1, biasTrue), nn.ReLU(inplaceTrue), nn.Conv2d(bottleneck, in_channels, kernel_size1, biasTrue), nn.Sigmoid() ) def forward(self, x): batch_size, channels, height, width x.shape branch_features [] for level, conv in zip(self.pyramid_levels, self.branch_convs): # 自适应池化到 level x level保持batch和channel维度 pooled F.adaptive_avg_pool2d(x, (level, level)) # 1x1卷积压缩通道维度再展平为向量 compressed conv(pooled).view(batch_size, -1) branch_features.append(compressed) # 拼接所有分支特征reshape为 Cx1x1 的张量形式便于后续卷积处理 z torch.cat(branch_features, dim1) z z.view(batch_size, -1, 1, 1) # 生成通道注意力权重 s self.attention(z) return x * s这段代码有几个细节值得解释。首先pyramid_levels控制金字塔的粒度默认 (1, 2, 4) 表示三个分支分别做 1×1、2×2、4×4 的自适应池化。这里的分支数量不是越多越好后面消融实验部分会专门分析。其次branch_convs每个分支独立使用1×1卷积目的是统一不同池化粒度下的通道维度。为什么不用全连接层因为1×1卷积天然接受任意空间尺寸的输入配合adaptive_avg_pool2d后输出固定尺寸用卷积处理起来比全连接更干净而且共享权重更省参数。最后fuse模块先拼接所有分支向量再用1×1卷积映射回原始通道数。这里我加了一个BatchNorm和ReLU虽然是可选项但实测下来对训练稳定性有一些帮助。注意fuse的输入通道数是hidden_dim * num_branches如果hidden_dim是向下取整得到的拼接后的总维度可能略小于in_channels / reduction这个偏差影响不大但心里要有数。3.2 如何嵌入主流骨干网络模块写好了怎么用才是关键。最省事的方式是挂在每个残差块的后面。以ResNet为例标准残差块的结构是输入 x → conv1 → bn1 → relu → conv2 → bn2 → relu → conv3 → bn3 → 残差相加 → relu在残差相加之后、最终ReLU之前插入注意力模块是效果最好的位置。因为这个位置的特征已经经过了完整的残差变换信息最充分注意力模块可以基于当前层“真正学到的东西”来重新分配权重而不是只看到中间状态的局部特征。class BottleneckWithPSA(nn.Module): def __init__(self, in_channels, out_channels, stride1, reduction16): super().__init__() # 标准Bottleneck的三个卷积层 self.conv1 nn.Conv2d(in_channels, out_channels, kernel_size1, biasFalse) self.bn1 nn.BatchNorm2d(out_channels) self.conv2 nn.Conv2d(out_channels, out_channels, kernel_size3, stridestride, padding1, biasFalse) self.bn2 nn.BatchNorm2d(out_channels) self.conv3 nn.Conv2d(out_channels, out_channels * 4, kernel_size1, biasFalse) self.bn3 nn.BatchNorm2d(out_channels * 4) # 金字塔挤压注意力模块 self.psa PyramidSqueezeAttention(out_channels * 4, reductionreduction) # 残差连接的downsample self.downsample None if stride ! 1 or in_channels ! out_channels * 4: self.downsample nn.Sequential( nn.Conv2d(in_channels, out_channels * 4, kernel_size1, stridestride, biasFalse), nn.BatchNorm2d(out_channels * 4), ) def forward(self, x): identity x out self.conv1(x); out self.bn1(out); out F.relu(out) out self.conv2(out); out self.bn2(out); out F.relu(out) out self.conv3(out); out self.bn3(out) out self.psa(out) if self.downsample is not None: identity self.downsample(identity) out identity return F.relu(out)MobileNet这类深度可分离卷积网络也适用不过插入位置可以更灵活——在Depthwise卷积之后加注意力模块可以增强通道间的信息交互弥补深度可分离卷积“通道独立性过强”的问题。3.3 参数量与计算量评估写代码的时候可能不觉得但真到了训练大模型时参数量和FLOPs的增量必须提前算清楚。下面我以ResNet50的某个Bottleneck输入256通道输出1024通道为例对比几种常见注意力模块的开销方便你直观感受。在输入特征图尺寸为 56×56、通道数为256的情况下各模块的理论开销大致如下模块额外参数量额外FLOPs备注SE~16.4K~0.1M仅全局池化两个全连接CBAM~16.4K 3.5K~0.7M多了空间注意力分支Pyramid Squeeze Attention~9.2K~0.5M多分支池化映射层从表里能看出PSA的参数量比SE还少一些FLOPs略高于SE但远低于CBAM。原因在于SE的全连接层是“C→C/r→C”的完整映射而PSA先用分支卷积把通道压缩到更小的维度再统一映射整体参数池更小。这个特点在通道数大的深层网络里优势更明显。我在某图像分类Demo上实测过ResNet50在最后一个Stage的全部Bottleneck挂上PSA之后总参数量增加约0.8%单张图片推理时间增加不到2%。这个增量对于绝大多数视觉任务来说完全可接受。4. 实验配置、对照结果与消融分析4.1 基线设置与训练细节讲完模块设计接下来是验证它是否真的好用。我把PSA装进了一个轻量分类网络用某开源图像分类数据集做了对比实验。为了让结果有说服力我严格控制了变量除了是否插入PSA之外训练超参、数据增强、学习率策略全部保持一致。训练配置如下输入分辨率224×224Batch Size128优化器SGDmomentum0.9weight_decay5e-4学习率初始0.1采用CosineAnnealing衰减到0训练轮数120数据增强RandomResizedCrop RandomHorizontalFlip ColorJitter基线网络选了一个结构类似ResNet18但更轻的变体为了验证PSA在不同容量下的表现我还加测了一个“基线SE”的组合作为第二对照组。所有模型训练3次取平均避免随机性干扰结论。4.2 主干效果对照挑关键结果来看。下表是三个模型在验证集上的Top-1准确率和推理耗时对比模型Top-1 (%)Top-5 (%)推理耗时(ms/张)基线70.3292.154.8基线 SE71.0992.574.9基线 PSA71.4892.814.9从数字上看PSA比基线涨了1.16个百分点比SE多涨了0.39个百分点。推理耗时几乎没变这个涨幅完全来自注意力机制本身的信息增益。而且我注意到一个趋势PSA在训练后期80轮以后的收敛稳定性更好验证集曲线波动比SE小很多可能是因为多尺度描述提供了更平滑的梯度信号。4.3 消融实验金字塔分支数量的影响金字塔的“层数”是PSA最核心的超参。我做了三组对比配置分支设置Top-1 (%)单分支等效SE(1,)71.09双分支(1, 2)71.24三分支(1, 2, 4)71.48四分支(1, 2, 4, 8)71.51从单分支到三分支涨点非常明显从三分支到四分支收益几乎可以忽略。这说明2×2、4×4这两个粒度已经能覆盖大多数目标尺度8×8的细粒度池化与4×4的信息冗余度太高反而带来额外的计算量。在实际项目中我建议先试 (1, 2, 4) 三档组合不要在分支数量上贪多。4.4 可视化与行为分析除了看准确率我还做了通道响应可视化。方法很简单把某一层特征图在通道维度上计算平均激活值对比PSA施加前后的分布变化。直观感受是基线网络不同通道的激活值分布比较均匀很多通道贡献度相似信息“摊”得很平加了PSA之后少数关键通道的激活值被明显放大其余通道被压制特征表示变得更稀疏、更有区分度。注意力机制的本质作用不是创造新信息而是把已有信息重新排序让网络把资源集中在真正重要的通道上。金字塔分支的贡献在可视化里也看得见。比如一张同时含大块天空和小型行人的图像1×1分支的权重倾向于放大天空对应通道4×4分支则更关注行人局部细节最终融合后的权重能兼顾两者。这是单尺度池化做不到的。5. 常见问题与调优实录5.1 加了模块之后不涨点先检查这三点这是我在实际项目里踩过最大的坑。PSA装在网络里跑完完整训练流程发现和基线几乎一样甚至略低。排查了一整天才找到原因。第一插入位置不对。如果把PSA放在残差块内部卷积之间信息流还没完整生成注意力基于的是半成品特征效果自然差。正确做法是放在残差相加之前或之后让注意力看到完整特征。第二reduction值太小。有些实现为了省参数把reduction设成32甚至64。这样bottleneck维度被压得过低融合层几乎没有表达能力注意力学习不到有效的通道关系。我建议reduction控制在8到16之间宁可多花一点点参数也要保住信息量。第三学习率策略问题。注意力模块的收敛速度和主干不完全同步。如果主干用SGD、注意力模块也沿用同样的学习率有时候前期会震荡。可以试试给注意力模块单独设置一个较小的学习率倍率比如0.1或者干脆用AdamW对整个模型训练混用优化器在某些框架里也不是不行但调起来更麻烦。5.2 金字塔池化粒度怎么选最合理数据集目标尺寸偏小比如人脸、细胞图像多关注细粒度分支。数据集目标尺寸跨度大比如通用分类数据集(1, 2, 4) 是黄金组合。数据集目标尺寸偏大比如遥感图像可以加一个8×8甚至12×12的分支。还有一个调法把金字塔分支的池化方式从平均池化换成最大池化试试。平均池化关注“整体响应水平”最大池化关注“最显著响应位置”。实际测试中两者结合一部分分支平均、一部分分支最大的效果有时候会比纯平均好0.2%左右代价是代码稍微复杂一点。5.3 和空间注意力一起用顺序怎么摆很多读者会问PSA是通道注意力那我要不要再加一个空间注意力模块组合顺序确实有讲究。我的建议是通道注意力在前、空间注意力在后。先通过通道维度把“看什么特征”的问题解决掉再通过空间维度把“看哪里的特征”的问题解决掉。如果顺序反过来空间注意力会基于未重标定的特征选择区域而通道注意力又要去纠正已经被空间模块改变过的特征分布两个模块的目标会互相干扰。另外同一个模块不要重复堆叠太多次。我在网络中每个Stage都加PSA结果在Stage3和Stage4收益最明显在浅层Stage1、Stage2加了反而略掉点。浅层特征还是以边缘纹理为主通道间的区分度不高强行加注意力可能导致信息损失。建议只在网络深层的2到3个Stage挂载PSA。5.4 训练稳定性BN层位置和权重初始化我前面代码里在fuse中加了BatchNorm这是有讲究的。如果不加BN注意力模块输出的分布会随着主干特征的分布一起漂移尤其在训练前几十个epochSigmoid的输入可能长期落在饱和区导致梯度消失。加了BN之后权重分布被拉回标准范围训练明显平稳。初始化方面PyTorch默认的kaiming初始化对1×1卷积基本够用但有一个技巧值得试试把注意力最后一层卷积的bias初始化为0权重用小标准差初始化比如0.01。这样模块初始输出的Sigmoid值接近0.5不会在训练初期对主干特征产生过强的扰动。这一点在迁移学习场景尤其重要。我在一个从零训练不收敛的案例里就是用这个初始化方式救回来的。结尾最后分享一点个人体会。做注意力模块的调优难点不在于把代码跑通而在于判断“为什么有效”和“为什么不涨点”。我在这类模块上踩过不少次坑之后最大的体会是先想清楚你的任务到底缺哪种信息再决定用什么样的注意力结构。缺全局上下文就上多尺度池化缺少精细化区域选择就上空间注意力两种都缺再考虑串行组合。Pyramid Squeeze Attention解决的是“全局多尺度上下文缺失”的问题它不包治百病但在大多数视觉任务里都能带来稳定的小幅增益。如果你后续想继续扩展可以试试把金字塔池化换成可学习的空间采样网格或者把分支映射层升级成轻量自注意力让跨尺度信息融合更充分。这个方向的上限比想象中高值得持续关注。