4. 注意力机制
注意力机制,说白了就是让网络学会「看哪里」。
小目标检测最头疼的问题是什么?背景杂乱、目标太小、特征容易被淹没。
注意力机制就是干这个的——它告诉网络:「别瞎看,重点在这儿!」
我最早接触注意力是在分类任务上,当时觉得效果也就那样。
直到做小目标检测,才发现这东西简直是救星。
你想想看,一张1080p的图里,目标可能只有十几个像素,不加注意力,网络根本不知道往哪瞅。
这一章,咱们把四个主流注意力模块掰开揉碎讲清楚:
SE、CBAM、CA、SimAM。
每个我都会讲原理、画结构、给代码,最后告诉你YOLO里怎么集成。
4.1 SE模块:通道维度的「注意力放大器」
SE,全称Squeeze-and-Excitation Networks,2017年提出。
它的核心思想很简单:让网络自己学习每个通道的重要性。
打个比方,你有一堆特征图,有的通道包含目标信息,有的全是背景噪声。
SE做的就是:把有用的通道放大,没用的通道抑制。
SE的工作原理
- Squeeze(压缩):全局平均池化,把每个通道压缩成一个数值
- Excitation(激励):两个全连接层 + 激活函数,学习通道间关系
- Scale(缩放):把学到的权重乘回原特征图
关键点:SE的压缩比r(reduction ratio)很关键。
我习惯设r=16,既减少参数量,又不损失太多信息。
太小了参数量大,太大了信息丢失严重。
SE的PyTorch实现
class SEBlock(nn.Module):
def __init__(self, channels, reduction=16):
super().__init__()
self.squeeze = nn.AdaptiveAvgPool2d(1)
self.excitation = nn.Sequential(
nn.Linear(channels, channels // reduction, bias=False),
nn.ReLU(inplace=True),
nn.Linear(channels // reduction, channels, bias=False),
nn.Sigmoid()
)
def forward(self, x):
b, c, _, _ = x.size()
y = self.squeeze(x).view(b, c)
y = self.excitation(y).view(b, c, 1, 1)
return x * y.expand_as(x)
YOLO中集成SE
我一般把SE加在Backbone的每个CSP块后面,或者Neck的FPN层之间。
具体位置:
- Backbone末端:增强高层语义特征的通道选择
- Neck的PANet层:改善多尺度特征融合时的通道对齐
我的经验:SE对小目标的提升有限,因为它只关注通道,不关注位置。
但胜在轻量,几乎不增加推理时间。
如果模型已经很大了,加SE性价比很高。
4.2 CBAM:通道+空间,双管齐下
CBAM(Convolutional Block Attention Module)是SE的升级版。
它不光做通道注意力,还加了空间注意力。
说白了就是:既告诉网络「哪些通道重要」,又告诉「哪些位置重要」。
我记得第一次在YOLOv5上试CBAM,mAP涨了1.2个点。
当时还挺惊讶的,毕竟就加了一个小模块。
CBAM的结构
- 通道注意力模块:类似SE,但用了最大池化+平均池化并行
- 空间注意力模块:对通道维度做池化,然后卷积生成空间权重
- 串行连接:先通道后空间(验证串行优于并行)
CBAM的PyTorch实现
class CBAMBlock(nn.Module):
def __init__(self, channels, reduction=16, kernel_size=7):
super().__init__()
# 通道注意力
self.channel_att = nn.Sequential(
nn.AdaptiveAvgPool2d(1),
nn.AdaptiveMaxPool2d(1),
nn.Flatten(),
nn.Linear(channels, channels // reduction, bias=False),
nn.ReLU(inplace=True),
nn.Linear(channels // reduction, channels, bias=False),
nn.Sigmoid()
)
# 空间注意力
self.spatial_att = nn.Sequential(
nn.Conv2d(2, 1, kernel_size, padding=kernel_size//2, bias=False),
nn.Sigmoid()
)
def forward(self, x):
# 通道注意力
b, c, h, w = x.size()
y = self.channel_att(x).view(b, c, 1, 1)
x = x * y.expand_as(x)
# 空间注意力
avg_out = torch.mean(x, dim=1, keepdim=True)
max_out, _ = torch.max(x, dim=1, keepdim=True)
spatial = torch.cat([avg_out, max_out], dim=1)
spatial = self.spatial_att(spatial)
return x * spatial
注意:CBAM的空间注意力用了7×7卷积,对小目标来说感受野太大了。
我曾经试过改成3×3,对小目标检测反而更好。
如果你做小目标,建议把kernel_size改成3。
YOLO中集成CBAM
CBAM我一般放在:
- Neck的每个上采样/下采样之后:增强特征融合时的空间定位
- 检测头之前:让检测头看到更「干净」的特征
4.3 CA模块:给小目标装上「GPS」
CA(Coordinate Attention)是2021年提出的。
它解决了SE和CBAM的一个共同问题:位置信息丢失。
你想想看,SE全局池化后,位置信息全没了。
CBAM虽然加了空间注意力,但用的是卷积,对大目标还行,小目标就模糊了。
CA的做法很巧妙:分别对高度和宽度方向做编码。
CA的核心创新
- 水平方向池化:对每一行做平均池化,得到C×H×1的特征
- 垂直方向池化:对每一列做平均池化,得到C×1×W的特征
- 拼接+卷积:把两个方向的特征拼起来,学习坐标感知的注意力
- 分离+缩放:再分开成两个方向,分别乘回原特征图
为什么CA对小目标友好?
小目标的位置信息极其重要。
CA保留了每个像素的「坐标感」,网络能精确知道目标在哪儿。
我实测在VisDrone数据集上,CA比CBAM高了0.8个mAP。
CA的PyTorch实现
class CABlock(nn.Module):
def __init__(self, channels, reduction=32):
super().__init__()
self.pool_h = nn.AdaptiveAvgPool2d((None, 1))
self.pool_w = nn.AdaptiveAvgPool2d((1, None))
mid_channels = max(8, channels // reduction)
self.conv1 = nn.Conv2d(channels, mid_channels, kernel_size=1, bias=False)
self.bn1 = nn.BatchNorm2d(mid_channels)
self.act = nn.ReLU(inplace=True)
self.conv_h = nn.Conv2d(mid_channels, channels, kernel_size=1, bias=False)
self.conv_w = nn.Conv2d(mid_channels, channels, kernel_size=1, bias=False)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
b, c, h, w = x.size()
x_h = self.pool_h(x) # b,c,h,1
x_w = self.pool_w(x).permute(0, 1, 3, 2) # b,c,1,w -> b,c,w,1
y = torch.cat([x_h, x_w], dim=2) # b,c,h+w,1
y = self.conv1(y)
y = self.bn1(y)
y = self.act(y)
x_h, x_w = torch.split(y, [h, w], dim=2)
x_w = x_w.permute(0, 1, 3, 2) # b,c,1,w
att_h = self.sigmoid(self.conv_h(x_h))
att_w = self.sigmoid(self.conv_w(x_w))
return x * att_h * att_w
YOLO中集成CA
CA我强烈建议加在:
- Backbone的每个stage输出后:让特征提取阶段就保留位置信息
- 检测头之前:最后一关强化位置感知
我的配置建议:
CA的reduction我设32,比SE的16更大。
因为CA有两个方向,中间特征维度mid_channels = max(8, channels//32)。
这样参数量控制得很好,不会拖慢速度。
4.4 SimAM:不用参数的注意力
SimAM(Simple Attention Module)是2021年的工作。
它的理念很激进:不需要额外参数。
怎么做到的?
SimAM基于神经科学中的「神经元抑制」理论。
它计算每个神经元和周围神经元的差异,差异大的就是重要的。
SimAM的原理
说白了,就是找「与众不同」的神经元。
如果一个像素和周围像素差别很大,那它很可能就是目标边缘或关键点。
SimAM用能量函数计算每个位置的「重要性分数」。
class SimAMBlock(nn.Module):
def __init__(self, channels=None, e_lambda=1e-4):
super().__init__()
self.act = nn.Sigmoid()
self.e_lambda = e_lambda
def forward(self, x):
b, c, h, w = x.size()
n = h * w - 1
# 计算每个像素与均值的差异
x_mean = x.mean(dim=[2, 3], keepdim=True)
x_minus_mean = x - x_mean
x_var = (x_minus_mean ** 2).mean(dim=[2, 3], keepdim=True)
# 能量函数
d = (x_minus_mean ** 2) / (4 * (x_var + self.e_lambda)) + 0.5
att = self.act(1.0 / d)
return x * att
注意:SimAM虽然无参数,但效果不如CA和CBAM稳定。
我试过在YOLOv8上集成,mAP提升只有0.3-0.5个点。
但它有个巨大优势:零参数量,零FLOPs增加。
适合对模型大小有严格要求的场景。
4.5 四种注意力机制对比总结
| 模块 | 注意力类型 | 参数量 | 小目标效果 | 推理速度 | 推荐场景 |
|---|---|---|---|---|---|
| SE | 通道 | 低 | 中等 | 快 | 轻量模型、通道选择 |
| CBAM | 通道+空间 | 中 | 高 | 中等 | 通用场景、精度优先 |
| CA | 坐标感知 | 低 | 最高 | 快 | 小目标检测、位置敏感任务 |
| SimAM | 无参数 | 零 | 中等 | 最快 | 模型压缩、边缘部署 |
4.6 YOLO中集成注意力机制的通用方法
不管用哪种注意力,集成到YOLO里的套路都差不多。
我以YOLOv8为例,给一个通用模板:
# 在ultralytics/nn/modules/下新建attention.py
# 把上面四个模块的代码放进去
# 在ultralytics/nn/modules/__init__.py中导入
from .attention import SEBlock, CBAMBlock, CABlock, SimAMBlock
# 在ultralytics/nn/tasks.py中修改parse_model
# 在需要的位置添加注意力层
def parse_model(d, ch, verbose=True):
# ... 原有代码 ...
if m in {SEBlock, CBAMBlock, CABlock, SimAMBlock}:
args = [ch[f], *args]
# ... 后续代码 ...
我的集成建议:
1. 不要每个层都加注意力,选3-5个关键位置就行
2. 小目标检测优先用CA,其次是CBAM
3. 如果模型已经很大了,用SE或SimAM做轻量化增强
4. 训练时学习率可以调低一点,注意力模块收敛较慢
嗯,这一章的内容就到这儿。
四种注意力机制,各有各的脾气。
SE轻量、CBAM全面、CA精准、SimAM零成本。
选哪个,看你手头的任务和资源。