本文基于 YOLOv13 官方仓库(iMoonLab/yolov13,ultralytics 8.3.63 fork) 实测整理,Windows/CPU 环境全程可跑(flash_attn 自动降级为标准注意力)。注意力机制模块本身与模型版本无关,但插入位置、yaml 层号、注册细节在 v13 里完全不同,这篇把四个最经典的注意力模块——SE、CBAM、ECA、CA——一次性按 v13 重讲,并给出可直接复制运行的 yaml。
前言
YOLOv13(清华团队,HyperACE 超图 + FullPAD 全流水线聚合架构)把检测 backbone 换成了 DSC3k2/A2C2f/DSConv 的全新组合,还引入了 HyperACE 超图模块和 7 个 FullPAD_Tunnel 门控——注意力的插入位置必须按 v13 结构重新规划(backbone 末端是 A2C2f,全网 7 个门控的引用都要同步顺移)。这篇以 SE/CBAM/ECA/CA 四个经典模块为例,演示注意力模块如何接入 v13:结构分析、插入点选择、层号顺移、注册适配,全套实测验证。
专栏目录:YOLOv13改进目录一览
一、先看懂 YOLOv13 的结构:插入点变了
对照官方 yolov13.yaml(节选,层号从 0 数起):
backbone:
- [-1, 1, Conv, [64, 3, 2]] # 0-P1/2
- [-1, 1, Conv, [128, 3, 2, 1, 2]] # 1-P2/4
- [-1, 2, DSC3k2, [256, False, 0.25]]# 2
- [-1, 1, Conv, [256, 3, 2, 1, 4]] # 3-P3/8
- [-1, 2, DSC3k2, [512, False, 0.25]]# 4
- [-1, 1, DSConv, [512, 3, 2]] # 5-P4/16
- [-1, 4, A2C2f, [512, True, 4]] # 6
- [-1, 1, DSConv, [1024, 3, 2]] # 7-P5/32
- [-1, 4, A2C2f, [1024, True, 1]] # 8 ★backbone末端
head:
- [[4, 6, 8], 2, HyperACE, [...]] # 9 ★超图模块,吃三个尺度
- [-1, 1, nn.Upsample, [...]] # 10
- [ 9, 1, DownsampleConv, []] # 11
- [[6, 9], 1, FullPAD_Tunnel, []] # 12 ★门控引用了第9层!
...(共7个FullPAD_Tunnel分散在全网)
- [[23, 27, 31], 1, Detect, [nc]] # 32
YOLOv13 结构的三大关键点:
- 没有 SPPF 和 C2PSA:backbone 末端是 A2C2f(层8),注意力的插入位置必须重新规划;
- HyperACE(层9)聚合 [4,6,8] 三个尺度,是 v13 的增强核心;
- 7 个 FullPAD_Tunnel 把增强特征分发回全网,它们的 from 引用横跨 4-27 层——插入任何新层,这些引用全部要顺移,漏一个就报错。
插入点选择:本文选在 层 8(backbone 末端 A2C2f)之后,其他合理位置还有层 9 后(HyperACE 之后)、层 17/21/26/30 后(Neck 各尺度),方法相同。新层成为 第 9 层,后面所有层顺移 +1(这就是为什么下文 yaml 里 head 的引用统一 +1)。
二、四个模块原理速览
模块本身与模型版本无关,这里简要回顾。四个都是通道/空间重加权类注意力,区别在于"如何生成权重"。
2.1 SE:Squeeze-and-Excitation 通道注意力(CVPR 2018)
SE 只做一件事:让每个通道自己学一个重要性系数。
- Squeeze:对特征图沿空间维度做全局平均池化,把
(B, C, H, W)压成(B, C),得到每个通道的全局描述; - Excitation:用两层全连接(先降维
C → C/r,再升维回C,中间 ReLU,最后 Sigmoid)学出C个 0~1 之间的权重; - 权重乘回原特征图,重要通道被放大,冗余通道被抑制。
降维比例 r 控制复杂度,常用 16。
2.2 CBAM:通道 + 空间串联注意力(ECCV 2018)
CBAM 认为只加权通道还不够,空间上"看哪里"同样重要,于是把两个子模块串联:
- 通道注意力:在 SE 基础上加强——全局平均池化和全局最大池化双路并行,共享同一个多层感知机(降维→ReLU→升维),两路输出相加过 Sigmoid 得到通道权重;
- 空间注意力:沿通道维分别取平均值和最大值,得到两张单通道图,拼接后用一个 7×7 卷积压缩成空间权重图,Sigmoid 后逐像素乘回特征图。
⚠️ 一个很多人不知道的细节:ultralytics 框架其实已经内置了 CBAM,但它是简化版——内置的
ChannelAttention只有平均池化 + 单层不降维卷积,没有论文中的双池化共享 MLP 结构。做毕业论文/期刊实验时,建议使用下面的论文原版实现CBAMAtt并在文中注明出处,创新性和严谨性都更好。本文注册名用CBAMAtt以避开与内置CBAM的重名冲突。
2.3 ECA:无降维的高效通道注意力(CVPR 2020)
ECA 指出了 SE 的一个问题:降维全连接是有害的——它切断了通道与其权重之间的直接对应关系。ECA 的做法极其简洁:
- 同样先全局平均池化,但不做任何降维;
- 让
C个权重经过一个一维卷积(kernel 大小为k),让相邻通道之间交换信息; k不用手调,按公式k = |log₂(C)/γ + b/γ|自适应计算(γ=2, b=1,取最近的奇数)。
参数量几乎为零(只有一个 k×k 的一维卷积核),效果却常与 SE 持平甚至更好。
2.4 CA:方向感知的坐标注意力(CVPR 2021)
SE 和 ECA 都只建模了通道关系,丢失了位置信息;CBAM 用大卷积核补位置信息但感受野受限。CA 的思路是把全局池化拆成方向:
- 沿高度方向池化得
(H,1)、沿宽度方向池化得(1,W),分别保留某一方向上的长程依赖; - 两路拼接 → 1×1 卷积降维 → BN + HardSwish → 再拆开,各自经 1×1 卷积恢复维度后 Sigmoid,得到高度、宽度两组权重;
- 两组权重分别与特征图相乘,相当于给每个像素位置一个"行权重 × 列权重"的组合,既建模通道又精确定位。
对小目标和长条形目标尤其友好,是检测任务里涨点比较稳的一个模块。
三、实现代码
六个类整体粘贴即可(SE、ChannelAtt、SpatialAtt、CBAMAtt、ECA、CA)。为避免与框架内置的 ChannelAttention / SpatialAttention / CBAM 重名冲突,论文原版类名带 Att 后缀。
import torch
from torch import nn
import math
class SE(nn.Module):
"""SE通道注意力 (Hu et al., CVPR 2018, SENet)。"""
def __init__(self, c1, reduction=16):
super().__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.fc = nn.Sequential(
nn.Linear(c1, c1 // reduction, bias=False),
nn.ReLU(inplace=True),
nn.Linear(c1 // reduction, c1, bias=False),
nn.Sigmoid(),
)
def forward(self, x):
b, c, _, _ = x.shape
y = self.avg_pool(x).view(b, c) # Squeeze: (B,C)
y = self.fc(y).view(b, c, 1, 1) # Excitation: (B,C,1,1)
return x * y.expand_as(x)
class ChannelAtt(nn.Module):
"""通道注意力:双池化 + 共享MLP(CBAM论文原版)。"""
def __init__(self, c1, ratio=16):
super().__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.max_pool = nn.AdaptiveMaxPool2d(1)
self.mlp = nn.Sequential(
nn.Conv2d(c1, c1 // ratio, 1, bias=False),
nn.ReLU(inplace=True),
nn.Conv2d(c1 // ratio, c1, 1, bias=False),
)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
out = self.mlp(self.avg_pool(x)) + self.mlp(self.max_pool(x))
return self.sigmoid(out)
class SpatialAtt(nn.Module):
"""空间注意力:通道均值+最大值拼接后7x7卷积。"""
def __init__(self, kernel_size=7):
super().__init__()
assert kernel_size in (3, 7)
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)
max_out, _ = torch.max(x, dim=1, keepdim=True)
return self.sigmoid(self.conv(torch.cat([avg_out, max_out], dim=1)))
class CBAMAtt(nn.Module):
"""CBAM (Woo et al., ECCV 2018):先通道后空间。"""
def __init__(self, c1, ratio=16, kernel_size=7):
super().__init__()
self.ca = ChannelAtt(c1, ratio)
self.sa = SpatialAtt(kernel_size)
def forward(self, x):
x = self.ca(x) * x # 通道重加权
return self.sa(x) * x # 空间重加权,返回完整特征图
class ECA(nn.Module):
"""ECA高效通道注意力 (Wang et al., CVPR 2020)。"""
def __init__(self, c1, k_size=None):
super().__init__()
if k_size is None: # 自适应核大小: k = |log2(C)/2 + 1/2|,取奇数
t = int(abs((math.log(c1, 2) + 1) / 2))
k_size = t if t % 2 else t + 1
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.conv = nn.Conv1d(1, 1, kernel_size=k_size,
padding=k_size // 2, bias=False)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
y = self.avg_pool(x) # (B,C,1,1)
y = self.conv(y.squeeze(-1).transpose(-1, -2)) # (B,1,C),跨通道一维卷积
y = y.transpose(-1, -2).unsqueeze(-1) # (B,C,1,1)
return x * self.sigmoid(y).expand_as(x)
class CA(nn.Module):
"""坐标注意力 Coordinate Attention (Hou et al., CVPR 2021)。"""
def __init__(self, c1, reduction=32):
super().__init__()
self.pool_h = nn.AdaptiveAvgPool2d((None, 1)) # 沿宽度池化 -> (H,1)
self.pool_w = nn.AdaptiveAvgPool2d((1, None)) # 沿高度池化 -> (1,W)
mip = max(8, c1 // reduction)
self.conv1 = nn.Conv2d(c1, mip, 1, 1, 0)
self.bn1 = nn.BatchNorm2d(mip)
self.act = nn.Hardswish(inplace=True)
self.conv_h = nn.Conv2d(mip, c1, 1, 1, 0)
self.conv_w = nn.Conv2d(mip, c1, 1, 1, 0)
def forward(self, x):
identity = x
n, c, h, w = x.size()
x_h = self.pool_h(x) # (N,C,H,1)
x_w = self.pool_w(x).permute(0, 1, 3, 2) # (N,C,W,1)
y = torch.cat([x_h, x_w], dim=2) # (N,C,H+W,1) 共享编码
y = self.act(self.bn1(self.conv1(y)))
x_h, x_w = torch.split(y, [h, w], dim=2)
x_w = x_w.permute(0, 1, 3, 2) # (N,C,1,W)
a_h = torch.sigmoid(self.conv_h(x_h)) # 行权重
a_w = torch.sigmoid(self.conv_w(x_w)) # 列权重
return identity * a_w * a_h
四、添加步骤(v13 版,注意注册分支)
1. 修改 ultralytics/nn/modules/block.py
六个类整体粘贴到文件末尾(yolov13 的 block.py 有 60 个类,粘贴到末尾即可)。
2. 修改 ultralytics/nn/modules/__init__.py
⚠️ v13 坑①:from .block import (...) 块的最后一项 DSC3k2 没有尾逗号(__all__ 元组的最后一项 "DSConv" 同样没有),直接在闭合括号前插入新行会报 SyntaxError: invalid syntax。插入后记得给上一行补逗号。
两处改动——from .block import (...) 与 __all__ 都要加:
# ① 在 from .block import ( ... ) 列表末尾追加导出名
from .block import (
...
DSC3k2, # ← 补上这个逗号
SE,
CBAMAtt,
ECA,
CA,
)
# ② 在 __all__ 元组末尾追加同样的名字(注意最后一项是 "DSConv",给它补逗号)
__all__ = (
...
"DSC3k2",
"DSConv", # ← 补上这个逗号
"SE",
"CBAMAtt",
"ECA",
"CA",
)
3. 修改 ultralytics/nn/tasks.py
from ultralytics.nn.modules import (...) 导入块同样以无尾逗号的 DSC3k2 结尾,补逗号后追加:
from ultralytics.nn.modules import (
...
DSC3k2, # ← 补上这个逗号
SE, # 新增
CBAMAtt, # 新增(论文原版,避开内置 CBAM)
ECA, # 新增
CA, # 新增
)
4. parse_model() 里加注册分支(本篇关键新坑)
⚠️ v13 新坑:SE/CBAMAtt/ECA/CA 都属于"只需输入通道数"的模块——yaml 里写 [] 空参数,需要把输入通道 ch[f] 自动塞进第一个参数。v13 原版的 parse_model() 没有任何注意力模块的现成分支(只有 AIFI、HGStem/HGBlock、FullPAD_Tunnel 等结构模块的分支),需要自己在 elif m is FullPAD_Tunnel: 之后、else: 之前加一条集合分支:
elif m in {SE, CBAMAtt, ECA, CA}: # 注意力模块:只需输入通道数
args.insert(0, ch[f])
这一行的含义:这类模块的第一个参数永远是输入通道数
ch[f],其余参数(如reduction=16、kernel_size)由模块默认值兜底,yaml 中写[]即可。集合里的名字必须和上面导入的名字一一对应;SimAM 这类无参数模块不需要这个分支,走默认else: c2 = ch[f]透传即可。
五、yaml 模型文件
新建 yolov13n-SE.yaml(文件名里的 n 会自动匹配 scales 中的 n 尺度)。这里把注意力加在 层 8(backbone 末端的 A2C2f)之后,成为 第 9 层(c1=256,因为 v13n 的 width=0.25,1024×0.25=256)——新增层后所有层号都要顺移 +1,7 个 FullPAD_Tunnel、Concat、DownsampleConv、Detect 的引用全部同步更新,可直接复制运行:
# Ultralytics YOLOv13n + SE/CBAM/ECA/CA 🚀
# 基于官方 yolov13.yaml,在 backbone 末端(原层8 A2C2f)后插入注意力层9
nc: 80 # number of classes
scales:
n: [0.50, 0.25, 1024]
s: [0.50, 0.50, 1024]
l: [1.00, 1.00, 512]
x: [1.00, 1.50, 512]
backbone:
# [from, repeats, module, args]
- [-1, 1, Conv, [64, 3, 2]] # 0-P1/2
- [-1, 1, Conv, [128, 3, 2, 1, 2]] # 1-P2/4
- [-1, 2, DSC3k2, [256, False, 0.25]]# 2
- [-1, 1, Conv, [256, 3, 2, 1, 4]] # 3-P3/8
- [-1, 2, DSC3k2, [512, False, 0.25]]# 4
- [-1, 1, DSConv, [512, 3, 2]] # 5-P4/16
- [-1, 4, A2C2f, [512, True, 4]] # 6
- [-1, 1, DSConv, [1024, 3, 2]] # 7-P5/32
- [-1, 4, A2C2f, [1024, True, 1]] # 8
- [-1, 1, SE, []] # 9 ★SE(新增层,后续层号全部+1)
head:
- [[4, 6, 8], 2, HyperACE, [512, 8, True, True, 0.5, 1, "both"]] # 10
- [-1, 1, nn.Upsample, [None, 2, "nearest"]] # 11
- [ 10, 1, DownsampleConv, []] # 12(原from 9→10)
- [[6, 10], 1, FullPAD_Tunnel, []] # 13(原9→10)
- [[4, 11], 1, FullPAD_Tunnel, []] # 14(原10→11)
- [[8, 12], 1, FullPAD_Tunnel, []] # 15(原11→12)
- [-1, 1, nn.Upsample, [None, 2, "nearest"]] # 16
- [[-1, 13], 1, Concat, [1]] # 17 cat backbone P4
- [-1, 2, DSC3k2, [512, True]] # 18
- [[-1, 10], 1, FullPAD_Tunnel, []] # 19(原9→10)
- [ 18, 1, nn.Upsample, [None, 2, "nearest"]] # 20
- [[-1, 14], 1, Concat, [1]] # 21 cat backbone P3
- [-1, 2, DSC3k2, [256, True]] # 22
- [ 11, 1, Conv, [256, 1, 1]] # 23(原10→11)
- [[22, 23], 1, FullPAD_Tunnel, []] # 24(原21/22→22/23)
- [-1, 1, Conv, [256, 3, 2]] # 25
- [[-1, 19], 1, Concat, [1]] # 26 cat head P4
- [-1, 2, DSC3k2, [512, True]] # 27
- [[-1, 10], 1, FullPAD_Tunnel, []] # 28(原9→10)
- [ 27, 1, Conv, [512, 3, 2]] # 29(原26→27)
- [[-1, 15], 1, Concat, [1]] # 30 cat head P5
- [-1, 2, DSC3k2, [1024,True]] # 31
- [[-1, 12], 1, FullPAD_Tunnel, []] # 32(原11→12)
- [[24, 28, 32], 1, Detect, [nc]] # 33 Detect(P3,P4,P5)
换成 CBAM / ECA / CA 只需改第 9 行的模块名:把 - [-1, 1, SE, []] 改成 - [-1, 1, CBAMAtt, []]、- [-1, 1, ECA, []] 或 - [-1, 1, CA, []] 即可,其余层号与引用完全不用动(四个模块插入位置、输入输出通道一致,c1 都是 256)。
六、成功运行结果
yolo detect train model=yolov13n-SE.yaml data=mydata.yaml epochs=100 imgsz=640 batch=16
Python 方式(推荐,能继承预训练权重,收敛更快):
from ultralytics import YOLO
model = YOLO("yolov13n-SE.yaml").load("yolov13n.pt")
model.train(data="mydata.yaml", epochs=100, imgsz=640, batch=16)
本文实测(YOLOv13 官方仓库,Windows CPU,v13n 结构):
基线 yolov13n: 2,494,151 parameters, 6.5 GFLOPs
+SE: 2,502,343 parameters, 6.5 GFLOPs 第9层=SE []
+CBAMAtt: 2,502,441 parameters, 6.6 GFLOPs 第9层=CBAMAtt []
+ECA: 2,494,156 parameters, 6.5 GFLOPs 第9层=ECA []
+CA: 2,500,831 parameters, 6.5 GFLOPs 第9层=CA []
✅ 单元与整网断言全部通过:第 9 层分别为
SE/CBAMAtt/ECA/CA,层号顺移后的 34 层(0~33)yaml 全部引用正确;四个模块在 yaml 中互换均验证通过,模型正常构建并训练。
| 模型 | Params | GFLOPs | 参数量增量 | 说明 |
|---|---|---|---|---|
| YOLOv13n 基线 | 2.49M | 6.5 | - | 官方结构(不含SPPF/C2PSA) |
| +SE | 2.50M | 6.5 | +8,192 | 通道注意力,即插即用 |
| +CBAMAtt | 2.50M | 6.6 | +8,290 | 通道+空间双重加权 |
| +ECA | 2.49M | 6.5 | +5 | 近乎零成本的涨点方案 |
| +CA | 2.50M | 6.5 | +6,680 | 方向感知,小目标友好 |
提示:以上为 YOLOv13n(width=0.25,层9 输入通道 c1=256)实测值;mAP 消融请在你自己的数据集上完成——同一模块在不同任务上的表现可能相反,这正是消融实验的价值所在。
七、总结
v13 迁移的四个关键:插入点按 v13 结构重新选(backbone 末端是 A2C2f 不是 C2PSA,新层插在层 8 后成为层 9)、FullPAD_Tunnel/Concat/DownsampleConv/Detect 的引用层号全量顺移 +1、导入块尾逗号要自己补(from .block import 最后一项 DSC3k2、__all__ 最后一项 DSConv 都没有逗号)、别忘了给 parse_model 加 elif m in {SE, CBAMAtt, ECA, CA} 注册分支(v13 原版没有现成注意力分支,要自己加;这类"只需输入通道数"的模块必须塞 ch[f],否则 yaml 写 [] 会报错)。四个模块都是即插即用,开销极小:ECA 近乎零参数,SE/CA 只加几千参数,CBAM 多一个空间分支也才 +8,290。
选型建议:一般小目标多优先试 CA/CBAMAtt,算力紧张优先试 ECA;具体涨点一定要在自己的数据集上做消融。
觉得有帮助的话,点赞收藏关注三连支持一下,评论区欢迎交流你的实验结果~
专栏目录:YOLOv13改进目录一览 专栏地址:YOLOv13改进专栏——持续更新各方向即插即用改进
2575




被折叠的 条评论
为什么被折叠?



