今日目标 :掌握三种轻量级注意力机制的设计思想,理解通道注意力与空间注意力的区别,学会在端侧模型中加入注意力
预计阅读 :10分钟 | 动手操作:35分钟
一、CNN为什么需要注意力?
CNN的默认行为:所有通道一视同仁
Conv2d(256, 256, 3) → 所有256个通道同等重要
但实际中:有些通道对当前任务更重要!
注意力机制的作用:
让模型学会"关注"重要的通道/区域
抑制不重要的通道/区域
类比:你看一张图片时
不是均匀地看每个像素
而是聚焦于关键区域(人脸、文字)
注意力机制就是让CNN也学会"聚焦"
三种注意力维度:
通道注意力 (Channel Attention): "哪些通道更重要?"
空间注意力 (Spatial Attention): "哪些位置更重要?"
混合注意力: 两者结合
python
import torch
import torch.nn as nn
import torch.nn.functional as F
torch.manual_seed(42)
二、核心知识点
2.1 SE (Squeeze-and-Excitation):通道注意力鼻祖
python
"""
SE-Net (2018)
论文:https://arxiv.org/abs/1709.01507
核心思想:让模型自动学习每个通道的重要性
Squeeze (压缩):
全局平均池化 → 把每个通道的H×W压缩成1个标量
(C, H, W) → (C, 1, 1)
这个标量代表该通道的"全局信息"
Excitation (激励):
两层全连接 → 学习通道之间的关系
FC(C) → ReLU → FC(C) → Sigmoid
输出: 每个通道的权重 (0-1之间)
Scale (加权):
原始特征 × 通道权重
重要通道的权重 → 1
不重要通道的权重 → 0
SE的参数量:
两个FC层: C×(C/r) + (C/r)×C = 2C²/r
r是缩减率 (默认16)
如C=256, r=16: 2×256²/16 = 8192个参数
几乎可以忽略不计!
SE是最常用的轻量级注意力
MobileNet v3、EfficientNet都用了SE
"""
class SEBlock(nn.Module):
"""
Squeeze-and-Excitation Block
参数量极少,即插即用
可以插入任何CNN的任意位置
"""
def __init__(self, channels, reduction=16):
super().__init__()
self.channels = channels
# Squeeze: 全局平均池化
self.avg_pool = nn.AdaptiveAvgPool2d(1)
# Excitation: 两层FC
self.fc = 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):
"""
x: (B, C, H, W)
"""
B, C, H, W = x.shape
# Squeeze: (B, C, H, W) → (B, C, 1, 1) → (B, C)
y = self.avg_pool(x).view(B, C)
# Excitation: (B, C) → (B, C) → (B, C, 1, 1)
y = self.fc(y).view(B, C, 1, 1)
# Scale: 逐通道加权
return x * y
def se_demo():
"""演示SE的作用"""
x = torch.randn(2, 64, 32, 32)
se = SEBlock(64, reduction=16)
y = se(x)
params = sum(p.numel() for p in se.parameters())
print("SE Block演示:")
print(f" 输入: {x.shape}")
print(f" Squeeze: (2,64,32,32) → (2,64,1,1) 全局平均池化")
print(f" Excitation: FC(64→4) → ReLU → FC(4→64) → Sigmoid")
print(f" Scale: 逐通道乘以权重")
print(f" 输出: {y.shape}")
print(f" 参数量: {params} (几乎可以忽略)")
print(f"\n 端侧AI: SE参数量极少,强烈推荐!")
se_demo()
2.2 CBAM:通道 + 空间双重注意力
python
"""
CBAM (2018)
论文:https://arxiv.org/abs/1807.06521
核心思想:SE只有通道注意力,CBAM加了空间注意力
CBAM = 通道注意力 + 空间注意力
通道注意力 (类似SE但更强):
同时用全局平均池化 + 全局最大池化
平均池化: 整体信息
最大池化: 最显著特征
共享MLP → 两个输出相加 → Sigmoid
空间注意力:
沿着通道维度做平均池化 + 最大池化
(C, H, W) → (2, H, W)
然后7×7卷积 → 1个通道 → Sigmoid
输出: 每个位置的权重
CBAM的顺序:
先通道注意力 → 再空间注意力
论文证明这个顺序效果最好
"""
class ChannelAttention(nn.Module):
"""CBAM的通道注意力模块"""
def __init__(self, channels, reduction=16):
super().__init__()
# 共享MLP
self.mlp = nn.Sequential(
nn.Conv2d(channels, channels // reduction, 1, bias=False),
nn.ReLU(inplace=True),
nn.Conv2d(channels // reduction, channels, 1, bias=False),
)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
# 平均池化
avg_out = self.mlp(F.adaptive_avg_pool2d(x, 1))
# 最大池化
max_out = self.mlp(F.adaptive_max_pool2d(x, 1))
# 相加
return self.sigmoid(avg_out + max_out)
class SpatialAttention(nn.Module):
"""CBAM的空间注意力模块"""
def __init__(self, kernel_size=7):
super().__init__()
self.conv = nn.Conv2d(2, 1, kernel_size, padding=kernel_size//2, bias=False)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
# 沿通道维度做平均和最大池化
avg_out = torch.mean(x, dim=1, keepdim=True) # (B, 1, H, W)
max_out, _ = torch.max(x, dim=1, keepdim=True) # (B, 1, H, W)
# 拼接
y = torch.cat([avg_out, max_out], dim=1) # (B, 2, H, W)
# 卷积 + Sigmoid
return self.sigmoid(self.conv(y))
class CBAM(nn.Module):
"""
CBAM = Channel Attention + Spatial Attention
顺序:输入 → 通道注意力 → 空间注意力 → 输出
"""
def __init__(self, channels, reduction=16, kernel_size=7):
super().__init__()
self.channel_attn = ChannelAttention(channels, reduction)
self.spatial_attn = SpatialAttention(kernel_size)
def forward(self, x):
# 1. 通道注意力
x = x * self.channel_attn(x)
# 2. 空间注意力
x = x * self.spatial_attn(x)
return x
def cbam_demo():
x = torch.randn(2, 64, 32, 32)
cbam = CBAM(64, reduction=16)
y = cbam(x)
params = sum(p.numel() for p in cbam.parameters())
print("CBAM演示:")
print(f" 输入: {x.shape}")
print(f" Step 1 - 通道注意力: 平均池化+最大池化 → 共享MLP → Sigmoid")
print(f" Step 2 - 空间注意力: 沿通道平均+最大 → 7×7卷积 → Sigmoid")
print(f" 输出: {y.shape}")
print(f" 参数量: {params}")
print(f"\n vs SE: 多了空间注意力,精度更高,但参数量略多")
cbam_demo()
2.3 ECA:极简通道注意力
python
"""
ECA-Net (2020)
论文:https://arxiv.org/abs/1910.03151
核心思想:SE的两个FC层是多余的!用1D卷积替代
SE的问题:
两个FC层 → 降维再升维 → 信息损失
而且FC层把通道间的关系"全连接"了
实际上只需要局部跨通道交互
ECA的改进:
用1D卷积替代FC!
1D卷积的kernel_size = 自适应计算的k
只和相邻的几个通道交互
不需要降维!
ECA的优势:
1. 参数量更少(只有k个参数)
2. 不需要降维(无信息损失)
3. 自适应kernel_size
4. 效果和SE相当甚至更好
"""
class ECABlock(nn.Module):
"""
ECA: Efficient Channel Attention
用1D卷积替代FC,参数量极低
端侧AI首选注意力模块!
"""
def __init__(self, channels, gamma=2, b=1):
super().__init__()
# 自适应计算kernel_size
# k = |log2(C)/γ + b/γ|_odd
t = int(abs((torch.log2(torch.tensor(channels, dtype=torch.float32)) + b) / gamma))
kernel_size = t if t % 2 else t + 1 # 确保是奇数
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.conv = nn.Conv1d(1, 1, kernel_size=kernel_size,
padding=kernel_size // 2, bias=False)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
"""
x: (B, C, H, W)
"""
B, C, H, W = x.shape
# Squeeze: (B, C, H, W) → (B, C, 1, 1)
y = self.avg_pool(x)
# 1D卷积: (B, C, 1) → (B, 1, C) → conv → (B, C, 1)
y = y.squeeze(-1).transpose(-1, -2) # (B, 1, C)
y = self.conv(y) # 1D卷积
y = y.transpose(-1, -2).unsqueeze(-1) # (B, C, 1, 1)
# Sigmoid
y = self.sigmoid(y)
return x * y
def eca_demo():
x = torch.randn(2, 64, 32, 32)
eca = ECABlock(64)
y = eca(x)
params = sum(p.numel() for p in eca.parameters())
print("ECA Block演示:")
print(f" 输入: {x.shape}")
print(f" 流程: 全局平局池化 → 1D卷积 → Sigmoid → 加权")
print(f" 输出: {y.shape}")
print(f" 参数量: {params} (极低!)")
print(f"\n 核心创新: 用1D卷积替代FC")
print(f" 不需要降维 → 无信息损失")
print(f" 自适应kernel_size → 根据通道数自动调整")
print(f"\n 端侧AI: ECA是最轻量的注意力!强烈推荐!")
eca_demo()
三、动手实践:三种注意力全面对比
python
def attention_comparison():
"""
SE / CBAM / ECA 全面对比
"""
comparison = {
'SE': {
'注意力类型': '通道',
'核心操作': 'GAP → FC → ReLU → FC → Sigmoid',
'参数量': '2C²/r (约8192)',
'降维': '是 (C → C/r → C)',
'信息损失': '有 (降维)',
'精度': '⭐⭐⭐',
'端侧推荐': '⭐⭐⭐⭐⭐',
},
'CBAM': {
'注意力类型': '通道 + 空间',
'核心操作': 'SE + 7×7卷积空间注意力',
'参数量': '2C²/r + 98 (约8290)',
'降维': '是',
'信息损失': '有',
'精度': '⭐⭐⭐⭐',
'端侧推荐': '⭐⭐⭐⭐',
},
'ECA': {
'注意力类型': '通道',
'核心操作': 'GAP → 1D Conv → Sigmoid',
'参数量': 'k (约3-5)',
'降维': '否',
'信息损失': '无',
'精度': '⭐⭐⭐⭐',
'端侧推荐': '⭐⭐⭐⭐⭐',
},
}
print("注意力机制全面对比:")
print(f"{'':<10s} {'SE':<20s} {'CBAM':<20s} {'ECA':<20s}")
print('-' * 70)
for attr in ['注意力类型', '核心操作', '参数量', '降维', '信息损失', '精度', '端侧推荐']:
se_val = comparison['SE'][attr]
cbam_val = comparison['CBAM'][attr]
eca_val = comparison['ECA'][attr]
print(f"{attr:<10s} {se_val:<20s} {cbam_val:<20s} {eca_val:<20s}")
attention_comparison()
四、端侧AI注意力选型
python
def edge_attention_selection():
"""
端侧AI不同场景的注意力选型
"""
guide = {
'MobileNet v3': {
'用的注意力': 'SE',
'位置': '倒残差块的最后一层之后',
'效果': '+1.5% Top-1',
'代价': '参数增加 < 5%',
},
'EfficientNet': {
'用的注意力': 'SE',
'位置': '每个MBConv块中',
'效果': '核心组件',
'代价': '参数增加约10%',
},
'YOLOv5/v8': {
'用的注意力': '无 (或可选SE/CBAM)',
'位置': 'CSP模块中',
'效果': '可选,+0.5-1% mAP',
'代价': '参数增加约3%',
},
'端侧自定义模型': {
'推荐': 'ECA (最轻量)',
'次选': 'SE (最成熟)',
'不推荐': 'CBAM (空间注意力在端侧优势不大)',
},
}
print("端侧AI注意力选型:")
for model, config in guide.items():
print(f"\n [{model}]")
for k, v in config.items():
if k != '推荐' and k != '次选' and k != '不推荐':
print(f" {k}: {v}")
else:
print(f" {k}: {v}")
edge_attention_selection()
五、常见坑点
坑1:SE的reduction_ratio选多大
python
# reduction ratio (r) 控制FC层的压缩程度
# r越大 → 中间层越小 → 参数越少
# r越小 → 中间层越大 → 表达能力越强
# 推荐值:
# r=16: 标准推荐 (论文默认)
# r=8: 通道数较少时 (< 128)
# r=4: 通道数很少时 (< 64)
# r=24: 追求极致轻量
# 端侧推荐:r=16 或 r=24
坑2:注意力放在哪里
python
# 注意力模块可以插入任何位置
# 但不同位置效果不同
# 推荐位置:
# 残差块的加法之后 (最常见)
# 1×1 Conv → 3×3 Conv → 1×1 Conv → SE → + shortcut
# 这是MobileNet v3和EfficientNet的做法
# ❌ 不推荐:
# 每个卷积层后面都加 → 参数太多,过拟合
# 只在网络开头加 → 效果有限
# ✅ 推荐:
# 每个stage的最后一个block加SE
# 不是所有层都加!
坑3:ECA的kernel_size自适应
python
# ECA的kernel_size是自适应计算的
# k = |log2(C)/γ + b/γ|_odd
# 如果手动设置:
# C=64 → k=3
# C=128 → k=3
# C=256 → k=5
# C=512 → k=5
# C=1024 → k=7
# 通道数越多,k越大
# 因为通道间的关系更复杂,需要更大的感受野
坑4:注意力不是万能的
python
# 注意力机制不是"加了一定好"
# 场景1:通道数很少(<32)→ 注意力效果有限
# 场景2:模型已经很小 → 注意力可能过拟合
# 场景3:训练数据很少 → 注意力可能过拟合
# 端侧AI建议:
# 1. 先在baseline上跑 → 看精度
# 2. 加SE/ECA → 看精度提升
# 3. 如果提升 < 0.5% → 不加(节省参数)
# 4. 如果提升 > 1% → 加上(性价比高)
六、今日作业
- 手写SE:实现SE Block,插入到简单CNN中,对比有无SE的精度
- 手写ECA:实现ECA Block,理解1D卷积替代FC的设计
- 三种注意力对比:在MNIST或CIFAR-10上对比SE/CBAM/ECA的效果
- 打卡 :评论区发你的理解,格式:"Day 41/100 打卡:注意力机制已掌握!"
今日小结
今天你学会了:
✅ 注意力机制的核心:让模型学会"关注"重要信息
✅ 通道注意力 vs 空间注意力
✅ SE: Squeeze(全局池化) → Excitation(FC) → Scale
✅ CBAM: 通道注意力 + 空间注意力
✅ ECA: 用1D卷积替代FC,最轻量
✅ SE参数量 ~2C²/r,ECA参数量 ~k
✅ 端侧AI:ECA首选,SE最成熟
✅ 4个经典坑点
明日预告
Day 42:模型量化基础(INT8/FP16)
对称量化、非对称量化、PTQ vs QAT、TensorRT量化
🔥 关注我,每天解锁一个端侧AI技能!
微信公众号:xxx | 小红书:xxx | CSDN:xxx
评论区打卡,一起坚持100天!
附:小红书图文版
封面标题建议:CNN注意力 | SE/CBAM/ECA,三分钟搞懂!👁️
P1 --- 封面
标题:注意力机制
副标题:SE / CBAM / ECA 轻量级注意力
P2 --- 为什么需要注意力
CNN默认:所有通道一视同仁
实际:有些通道更重要!
注意力:让模型自动学习"关注"什么
通道注意力:哪些通道重要?
空间注意力:哪些位置重要?
P3 --- SE (最简单)
Squeeze: 全局平均池化 → 1个标量
Excitation: FC→ReLU→FC→Sigmoid
Scale: 逐通道乘以权重
参数量极少(~8K),即插即用!
P4 --- CBAM (最强)
SE + 空间注意力
通道注意力:平均+最大池化 → MLP
空间注意力:沿通道平均+最大 → 7×7卷积
精度最高,但参数量略多
P5 --- ECA (最轻量)
SE的FC层是多余的!
用1D卷积替代 → 只有k个参数
不需要降维,无信息损失
端侧AI首选!
P6 --- 端侧选型
ECA: 最轻量,首选
SE: 最成熟,稳定
CBAM: 精度最高,略重
MobileNet/EfficientNet都在用SE!
评论区打卡 Day 41/100
标签:#注意力机制 #SE #CBAM #ECA #轻量级网络
CSDN发布提示:CSDN版本建议在SE部分放Squeeze-Excitation的流程图,在CBAM部分放通道注意力+空间注意力的双分支结构图,在ECA部分放1D卷积替代FC的对比图,在三者对比部分放对比表。