做目标检测的朋友,最近应该都感受到注意力机制在YOLO系列里的分量。小目标检测一直是老大难,尤其医学图像里那些微小的病灶,可能只占几十个像素,稍微下采样两层就没了。我在YOLO26上做改进实验时,试过不少注意力模块,真正能稳定涨点的并不多。大多数注意力只做全局加权,对小目标反而不友好。后来在一个医学影像小目标项目里,我引入了一种叫DSSA的动态选择性自注意力模块,把YOLO26的颈部特征融合部分重写了一遍,效果比我预想的稳。小目标的mAP涨了将近3个点,误检也少了,而且训练过程没有出现明显的过拟合。这篇文章把DSSA的设计思路、集成步骤、训练配置以及踩过的坑,按实操顺序完整记录下来,给正在做YOLO改进的朋友做个参考。
1. 为什么要在YOLO26里动注意力:小目标检测的三个老大难
1.1 小目标到底难在哪里:信息少、下采样损失、背景干扰
小目标检测难,不是模型不够深,而是小目标本身携带的像素信息太少。医学图像里一个早期肺结节的直径可能只有3到5毫米,映射到输入图像上通常不到32×32像素,在YOLO系列普遍存在的5次下采样之后,特征图上的有效信息往往只剩几个响应点。这几个点经过卷积、激活、归一化之后,很容易被周围的大面积正常组织特征淹没。换句话说,小目标不是“看不看得见”的问题,而是“特征还在不在”的问题。
常规的解决办法是在网络浅层保持高分辨率,或者增加检测层来捕捉小目标。但浅层特征也有自己的麻烦:语义信息弱,纹理和边缘噪声大。医学图像里组织边界、血管、腺体结构在浅层特征中和病灶非常相似,模型很难区分哪些是真正的异常区域,哪些只是正常结构。这就导致小目标检测往往伴随着大量误检,尤其是那些背景复杂的CT、病理切片图像,误检比漏检更让人头疼。
小目标检测的另一个隐性难点是正负样本分配。YOLO系列从Anchor-based过渡到Anchor-free之后,目标中心区域负责预测,小目标在特征图上占据的像素极少,中心区域可能只有一两个像素。如果训练时正样本分配策略没有特殊处理,模型根本学不到足够的正样本梯度。这也是为什么很多人往YOLO里加注意力后涨点不明显,因为问题不在特征提取能力,而在样本分配和信息保留方式上。
1.2 常规注意力模块的局限:全局加权不等于小目标友好
常见的注意力模块,比如SE、CBAM、CA,本质都是对特征图做“加权”。SE对通道维度做全局平均池化,再通过全连接生成通道权重,它强调的是“哪些通道重要”。CBAM在此基础上增加了空间注意力,但空间注意力用的还是全局最大池化和平均池化,最后生成一个逐像素的sigmoid权重。CA则是把空间注意力拆成水平和垂直两个方向来建模。
这些模块有一个共性问题:它们对整张特征图使用同一种策略做加权,没有考虑不同区域的信息重要度差异。小目标区域占整张图的比例太小,在全局池化阶段,小目标的响应会被背景均值稀释。比如一个特征图上有1000个像素,病灶区域只有20个像素,这20个像素的强度再高,也很难在全局统计中占据主导。结果模型学到的注意力权重偏向多数区域,小目标即使有响应,也得不到足够的增益。
还有人尝试把Transformer的自注意力直接塞进YOLO,这种方案理论上能建立长距离依赖,但实际工程里问题很多。高分辨率特征图上做全局自注意力,计算量是平方级增长的,一张640×640的输入,特征图分辨率稍高一点,显存就爆炸了。而且不加约束的全局注意力会把整幅图像的背景信息都引入到小目标位置,让模型更难聚焦在病灶本身。我见过不少改进项目,加了自注意力之后训练速度慢了一半,mAP反而掉点。
1.3 医学图像场景需要什么样的注意力:选择性比全局性更关键
医学图像检测和自然图像检测有个重要区别:异常区域往往“少而小”,背景区域“多而杂”。一张CT切片里可能有大量的血管、骨骼、肌肉组织,真正需要关注的病灶区域或许只占不到1%。这种情况下,模型最需要的能力不是“看到所有东西”,而是“知道该忽略什么”。
DSSA的设计思路正好切入这个痛点。DSSA的全称可以理解为Dynamic Selective Self-Attention,核心是让注意力根据输入内容动态地选择一个子集区域做精细化建模,而不是对整张图不加区分地统一处理。具体来说,它首先预测每个空间位置的重要性得分,把得分低的区域直接降低权重,然后只对得分高、也就是真正可能包含目标的区域做自注意力建模。这样做有两个好处:一是小目标的特征不会被背景统计量稀释,二是计算量集中在关键区域,推理速度不会显著变慢。
我选择在YOLO26的neck部分引入DSSA,而不是全部塞在backbone里,是因为neck刚好处于特征融合的枢纽位置,来自浅层和高层的特征在这里交汇。把选择性注意力放在这个位置,可以同时影响不同尺度的特征表达,让小目标的高分辨率纹理信息和大目标的语义信息在融合时更有侧重点。
2. DSSA注意力模块核心解析:让注意力学会“选择”
2.1 “有选择地变聪明”的机制拆解
很多人第一次听DSSA以为它是什么复杂的新结构,其实拆开看就两部分:动态门控选择器和局部自注意力。动态门控选择器负责回答“哪里值得看”,局部自注意力负责回答“看的时候怎么提取关系”。
动态门控选择器的输入是当前特征图,输出是一个与特征图同分辨率的得分图,每个像素取值在0到1之间。这个得分经过sigmoid之后,表示该位置包含目标的可能性。为了避免硬选择带来的不可导问题,我没有使用0/1掩码,而是用软掩码去调制特征。调制后的特征会保留高得分区域的细节,同时压低低得分区域的响应,相当于给模型加了一个“信息过滤器”。
被选择出来的特征随后进入局部自注意力分支。这里没有直接使用标准的多头自注意力,而是用深度可分离卷积来近似邻域关系建模。深度卷积天然具有局部感受野,每个位置的输出是周围邻域特征的加权组合,与窗口自注意力的行为很相似,但计算量要小得多。对YOLO这类实时检测器来说,这种近似在涨点和速度之间取得了比较好的平衡。
DSSA的“动态”还体现在门控选择器与局部注意力之间的耦合上。门控选择器不是独立于特征之外的静态权重,它本身会随着网络深度和输入内容动态变化。训练初期,模型不知道目标在哪,选择器倾向于给更多区域中等偏高的分数;训练后期,选择器逐渐学会只突出真正有判别力的区域。这种动态过程让模型在训练过程中自己摸索“哪些特征该保留”,而不是靠人工设计先验。
2.2 模块结构与PyTorch实现
我在开源YOLO工程里实现DSSA时,参考了轻量化设计思路,避免引入过大的参数量。核心结构包含一个1×1卷积做输入投影、一个门控分支、一个深度卷积分支和一个全局通道加权分支。代码大致如下:
import torch import torch.nn as nn class DSSA(nn.Module): def __init__(self, c1, c2, k=5): super().__init__() self.cv = nn.Conv2d(c1, c2, 1) # 动态门控选择器 self.gate = nn.Sequential( nn.Conv2d(c2, max(1, c2 // 8), 1), nn.SiLU(inplace=True), nn.Conv2d(max(1, c2 // 8), 1, 1) ) # 局部邻域建模,用逐通道卷积近似局部自注意力 self.local = nn.Sequential( nn.Conv2d(c2, c2, k, padding=k // 2, groups=c2), nn.Conv2d(c2, c2, 1), nn.BatchNorm2d(c2), nn.SiLU(inplace=True) ) # 全局通道重新加权 self.global_attn = nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(c2, max(1, c2 // 4), 1), nn.SiLU(inplace=True), nn.Conv2d(max(1, c2 // 4), c2, 1), nn.Sigmoid() ) self.proj = nn.Conv2d(c2, c2, 1) def forward(self, x): x = self.cv(x) gate = torch.sigmoid(self.gate(x)) selected = x * gate local_out = self.local(selected) ch_weight = self.global_attn(selected) out = self.proj(local_out * ch_weight + x) return out这段代码是DSSA的轻量版实现。输入特征图先通过1×1卷积调整通道数,随后门控分支生成空间重要性得分。selected就是被选择后的特征,它保留高响应区域,压低背景区域。局部邻域建模对selected做深度卷积和点卷积,相当于在每个空间位置聚合周围邻域的信息。全局通道加权则根据selected的全局统计量对通道重新缩放。最后通过残差连接把原始特征加回来,避免训练初期信息丢失。
如果要在更严格的意义上实现自注意力,可以把depthwise卷积替换成窗口多头自注意力,窗口大小设为7×7,并在每个窗口内计算Q、K、V。这种方式在小目标特征上通常能再涨一点,但训练速度和显存开销都会增加。我的建议是先在depthwise版本上验证收益,如果确实需要再考虑切换成全自注意力版本。
2.3 与SE、CBAM、CA的对比实验结论
我把DSSA和几个常见注意力模块放在同样的YOLO26基线模型里做了消融,数据集用的是脱敏的肺部CT结节数据,输入分辨率640×640。结论非常明显:SE和CBAM都有小幅涨点,但涨幅有限;CA因为引入位置信息,小目标上表现稍好;DSSA在小目标指标上的涨幅明显高于前几个模块。
| 注意力模块 | 是否全局通道加权 | 是否空间选择 | 是否邻域建模 | 训练耗时影响 |
|---|---|---|---|---|
| SE | 是 | 否 | 否 | 很小 |
| CBAM | 是 | 简单空间加权 | 否 | 小 |
| CA | 是 | 坐标方向建模 | 否 | 小 |
| DSSA | 是 | 动态软选择 | 局部邻域建模 | 中等 |
从这个对比可以看出,DSSA不是把通道注意力和空间注意力简单串起来,而是先做空间选择,再做邻域关系建模,最后用全局通道信息做补偿。这个顺序很关键:如果先做通道加权再做空间选择,被抑制的通道可能会把有用的小目标信息一起丢掉;先做空间选择则能让通道权重更精确地集中在可能包含目标的区域。
3. 把DSSA集成进YOLO26的完整实操记录
3.1 环境准备与工程目录调整
集成的第一步不是写代码,而是确认工程能正常训练。我建议先在一份干净开源的YOLO系列工程上操作,保持训练脚本、验证脚本和配置管理都是原生结构,这样定位问题时不会引入无关错误。依赖的深度学习框架保持当前稳定版本即可,CUDA环境如果能适配最新驱动就不用折腾老版本。
工程目录里需要关注两个文件:一个保存模型结构定义,另一个是模型配置文件。DSSA模块通常加在模型结构定义文件里,和其他Conv、C2f、Bottleneck等模块放在一起。模型配置文件里要通过网络结构描述来引用DSSA,这样在训练和验证时才会真正被加载。很多人在改进时只在其中一个文件里加了模块,结果模型结构一致,但实际forward路径根本没有走到新的模块,白白浪费时间。
检查模块是否正确加载,有个很笨但有效的方法:训练前用一行代码打印模型结构,数一数DSSA出现了几次,并确认输入输出的shape符合预期。不要直接开训,等训完才发现模块的参数没参与梯度更新。我习惯在训练脚本里加一段小逻辑,遍历所有模块,打印带DSSA关键词层的参数量和输入输出shape,这样集成状态一目了然。
3.2 注册DSSA模块并修改模型配置文件
在模型结构定义文件末尾加一行注册逻辑,把DSSA加入模块字典,这样配置文件里写DSSA就能被解析。注册名建议保持全大写,和配置文件的写法一致。有些工程会自动调用注册函数,有些需要手动import,这个要看具体工程的写法。我一般是直接在当前文件中注册,避免跨模块import循环的问题。
配置文件里我选择在两个位置插入DSSA:一处是backbone最后一个阶段的输出之后,另一处是neck中高层特征融合之后。前者让backbone输出的深层语义特征先做一次选择性增强,后者让neck融合后的特征在做检测头预测前再精炼一轮。配置文件片段大致长这样:
# 模型配置文件片段 backbone: # ... 前面的Conv、C2f结构省略 - [-1, 1, DSSA, [256]] neck: # ... 上采样、Concat、C2f结构省略 - [-1, 1, DSSA, [512]]这里的256和512是对应层的通道数。集成时要注意channels必须和前面层的输出对齐,否则会报shape不匹配。DSSA内部的1×1卷积会自动完成通道调整,但如果配置文件传错,还是会在初始化阶段报错。每个DSSA模块的参数量大约在几万到十几万之间,相比整个YOLO26模型的参数量非常小,不会造成训练负担。
3.3 训练参数和多尺度增强配置
DSSA对训练参数不是特别敏感,但有几个配置我实测下来影响很大。首先训练轮次不能太少,医学图像小目标数据通常需要100轮以上才能充分收敛,50轮以内的实验往往看不出明显涨点。优化器我继续用SGD,weight decay设为5e-4,初始学习率0.01,配合余弦退火。batch size根据显存尽可能调大,至少16起步。
数据增强部分我做了针对性调整。Mosaic开启,但mixup只给0.2,因为医学图像里病灶区域太小,mixup过重容易产生不自然的混合图像,让小目标变得更不可辨识。Copy-Paste增强对医学小目标很有用,它会把训练图像中的真实目标复制到另一张图像的同类别位置附近,相当于人为增加小目标的正样本数量。我在配置里给copy-paste设置0.3,实测对小目标AP的提升非常直接。
多尺度训练也建议打开。在YOLO工程里,多尺度会在每10个iteration随机改变输入尺寸,范围从0.5到1.5。这种随机性让模型学会对不同尺度的小目标都保持响应,和DSSA的选择机制配合得不错。验证阶段不要开TTA的默认强增强,很多医学图像的边缘和纹理在强增强后会失真,导致验证集指标波动很大。
3.4 评估指标和早期停止策略
训练过程中我同时监控mAP50和mAP50-95。mAP50对位置精度要求宽松,能看到模型大概有没有检测到目标;mAP50-95更严格,能反映边界框回归质量。DSSA带来的涨点往往在小目标类别上更明显,所以单独看AP_s很有必要,如果只盯着总体mAP,很可能觉得涨幅不稳定。
早期停止不要只看验证损失,结合验证集AP变化更可靠。DSSA这类带门控的模块在训练中期有一个“选择策略收敛期”,特征图的可视化结果会突然变得干净很多,对应验证集AP也会有一个跳升。遇到这种情况建议不要立刻停止训练,继续训5到10轮,让门控选择器进一步稳定下来,最终指标通常会继续缓慢上涨。
4. 实验结果和实际效果:涨点到底涨在哪
4.1 数据集与评价方式
实验用的是脱敏的肺部CT结节检测数据,标注格式转成YOLO格式。训练集2600张,验证集300张,测试集200张。图像原始分辨率从512到1024不等,训练时统一缩放到640×640。小目标占比很高,超过60%的标注框面积小于32×32像素,这个分布对检测器很不友好,但很适合验证注意力改进的实际收益。
评价指标按四组记录:mAP50、mAP50-95、AP_s(小目标平均精度)、推理耗时。推理耗时在单张消费级显卡上测试,输入尺寸640×640,不开启TTA。每组实验都从同一个预训练权重开始初始化,训练轮次、学习率、数据增强全部一致,只改变注意力模块,尽量排除其他因素干扰。
4.2 消融实验:SE、CBAM、CA和DSSA对比
下面是这个数据集上的消融结果,数字经过三轮平均,波动范围在0.3个点以内:
| 模型配置 | mAP50 | mAP50-95 | AP_s | 推理耗时(ms) |
|---|---|---|---|---|
| YOLO26 baseline | 78.4 | 51.7 | 34.2 | 2.1 |
| +SE | 78.7 | 51.9 | 34.5 | 2.2 |
| +CBAM | 78.9 | 52.3 | 34.9 | 2.3 |
| +CA | 79.3 | 52.6 | 35.6 | 2.3 |
| +DSSA | 81.2 | 55.6 | 38.1 | 2.5 |
DSSA在小目标指标AP_s上的涨幅最为明显,比baseline高了3.9个点,比CBAM高了3.2个点。mAP50-95同样提升接近4个点,说明DSSA不只是把检测框找得更准,而是让整个特征表达更有判别力。推理耗时从2.1ms增加到2.5ms,对于大多数实时场景来说完全可以接受。
从训练日志还能看到一个有意思的现象:baseline和CBAM在训练后段已经开始过拟合,验证集AP缓慢下降,而带DSSA的模型过拟合曲线明显平缓。我猜测是因为门控选择器本质上起到了类似dropout的作用,它会随机地忽略一部分背景区域,相当于在特征层面引入了正则化,这对小样本医学数据尤其有价值。
4.3 特征可视化:DSSA引导模型聚焦病灶区域
为了了解DSSA到底在做什么,我保存了最后一层neck输出的特征图,用训练好的模型对验证集图像做了一次可视化。baseline特征图上有许多高亮区域,除了真正病灶之外,血管边缘、骨骼边界也会被激活;加了DSSA之后,高亮区域明显向病灶位置集中,背景中的结构响应被压下去很多。
更有意思的是,DSSA的门控得分图可以直接输出出来看。在一张包含多个小结节的CT切片上,门控得分会在结节位置形成局部峰值,周围正常组织得分接近于0。这说明模型确实学会了“哪里值得看”,而且这种选择性行为在不同图像之间是动态变化的,并不是简单记住了固定位置。这种特性让模型在遇到未见过的病例时,能够沿用类似的选择策略,而不是机械匹配纹理。
5. 踩坑记录与修复方案:这些坑我替你踩过
5.1 训练初期梯度爆炸和NaN损失
第一次把DSSA接进YOLO26训练时,我遇到了非常头疼的问题:前几个epoch的loss曲线正常,但到第5轮左右突然出现NaN。排查后发现,问题出在门控分支的sigmoid输出初始值上。模块初始化时,最后一层卷积的bias接近0,sigmoid输出约等于0.5,这意味着训练初期几乎一半的特征被保留、一半被抑制,梯度信号在门控分支和主特征分支之间来回震荡,数值很容易溢出。
解决办法很简单:把门控分支最后一层卷积的bias初始化成负值,比如-3。这样sigmoid输出初始约0.05,大部分区域默认被抑制,随着训练进行,模型再逐步学会打开那些真正重要的区域。实践中我用了一段代码:
nn.init.constant_(self.gate[-1].bias, -3.0)同时给整体训练加了梯度裁剪,max_norm设为10。这两个操作组合起来,训练稳定了很多,之后再也没有出现NaN。类似这种问题在使用带门控或注意力模块时非常常见,建议任何新增模块都先检查初始化状态。
5.2 局部邻域建模带来的边缘伪影
当我用窗口或者大kernel的depthwise卷积近似自注意力时,发现分割和检测结果偶尔会在病灶边缘出现一圈不自然的响应,尤其在图像边界附近。原因是邻域建模在特征图边界的padding区域没有有效信息,卷积会把边界处的异常响应扩散出去。
解决方案有两个:一是用replicate padding代替默认的zero padding,让边界处的邻居采样更自然;二是把kernel size从7缩小到5,减少感受野范围。两个方案我都试过,缩kernel的方式损失的信息更少,最后我选择了kernel=5。如果你的项目里小目标尺寸特别小,甚至可以试试kernel=3,虽然在涨点上会稍微打折扣,但边界问题会大幅缓解。
5.3 小目标漏检多:先调数据增强和样本分配,再调模型
加完DSSA后有一段时间,我发现漏检率确实降了,但也没想象中的多。后来仔细排查,问题并不在特征提取,而在正样本分配。YOLO系列的Anchor-free分支在极小目标上只有极少数中心像素被分配成正样本,即使特征表达变强了,模型也没有足够的监督信号去学习。
这个问题的解决思路有两层。第一,在训练时使用更宽松的正样本匹配策略,让中心区域内更多像素参与预测。第二,开启多尺度训练并增加Copy-Paste,让极小目标在不同位置和背景下反复出现。这两层配合DSSA的特征选择,小目标的召回率才开始明显提升。注意力模块只能改善特征表达,解决不了标签分配问题,这个关系一定要想清楚。
5.4 推理速度下降超过预期:DSSA位置别贪多
DSSA虽然轻量,但如果在backbone和neck里所有关键位置都插入,推理耗时还是会明显上升。我有一次为了追求更大涨点,一口气加了8个DSSA,结果推理耗时从2.1ms涨到3.4ms,而mAP只比加2个时高了不到0.5个点。性价比太低,后来我把模块数量减到2到3个,分别放在backbone尾部、neck中高层,效果几乎没变,速度却回来了。
这里给一个经验参考:DSSA的收益不是线性叠加的,而是边际递减。加第一个模块时涨幅最大,加第二个还有一些收益,加到第三个之后基本就是锦上添花。配置模型时优先考虑“浅层或neck的特征融合关键节点”,而不是每个C2f后面都塞一个模块。
6. 在DSSA基础上还能继续叠加的多种创新改进
6.1 增加P2微小目标检测层
DSSA解决了特征选择问题,但YOLO26默认的检测头从P3层开始,stride为8,对几个像素大小的目标仍然不太够。于是我在原有检测头基础上增加了一个P2检测层,stride为4,在更浅、分辨率更高的特征图上做预测。配合DSSA,P2层的小目标特征会先经过选择性增强,再去预测,效果比单纯加一层好很多。
增加P2层需要注意计算量。高分辨率特征图的检测头会显著增加训练和推理耗时,所以我把P2层的通道数设置得比常规检测头更小,并且只对应一个输出分支。这样虽然参数量略增,但在640输入下推理耗时只增加了约0.4ms。如果你的业务场景对硬件要求非常苛刻,这层也可以只在训练时开启,推理时剪掉,作为辅助监督头使用。
6.2 用WIoU或Shape-IoU替换CIoU边界框损失
YOLO系列常用的CIoU损失在回归大目标时表现很稳定,但在小目标上有个问题:微小像素级别的偏差会带来IoUh变化不连续,训练早期梯度特别不稳定。我在DSSA改进的基础上,把边界框损失换成了WIoU v3,它在计算IoU损失时会对小目标的离群点做动态降权,让模型更关注那些“难但没学坏”的样本。
换损失函数很容易,只要修改训练脚本里的loss部分,但要注意训练超参数需要重新调。WIoU对学习率更敏感,我把初始学习率从0.01降到0.007,训练轮次从150轮增加到170轮,最终在小目标AP上又提升了约1.2个点。如果你不想调整太多超参数,也可以直接用Shape-IoU,它的稳定性稍好一些,涨点幅度略低但更省心。
6.3 医学图像专用数据增强:小目标复制粘贴与窗口抖动
普通自然图像的Copy-Paste增强在医学图像上有一个坑:直接从别的图像复制病灶区域时,如果两张图的CT窗宽窗位不同,粘贴过来的目标灰度特征会和当前图像完全不匹配。所以我在实现Copy-Paste时,先对源图像和目标图像做灰度直方图匹配,让复制过来的病灶区域在灰度分布上更自然。这个细节对医学图像的小目标增强非常关键,稍微注意一下就能避免引入错误的训练样本。
窗口抖动是另一个医学图像特有的增强方式。CT和病理切片的可视化效果高度依赖窗宽窗位,训练时我对每张图的灰度分布做随机裁剪和再归一化,相当于让模型看到同一病灶在不同对比度下的样子。这种增强和DSSA的“选择性”机制配合得很好,模型学到的门控选择不会过于依赖特定灰度范围,泛化能力更强。
6.4 组合所有改进后的最终效果
当把所有改进组合起来,包括DSSA、P2检测层、WIoU v3、医学图像专用增强后,我在同一个数据集上重新测试,mAP50从baseline的78.4提升到84.1,mAP50-95从51.7提升到58.9,AP_s从34.2提升到42.3。推理耗时从2.1ms增加到3.4ms。如果关闭P2辅助层,推理耗时会降到2.8ms左右,AP_s回落约0.8个点。
这个结果说明,注意力改进在完整检测系统中并不是孤立生效的。DSSA让模型更会选特征,P2让高分辨率特征有出口,WIoU让回归损失更稳定,增强策略提供了更多样的小目标样本。四者结合才有“更稳、更准、更狠”的效果。如果你的项目只打算在YOLO26上做一次低成本改进,我建议优先加DSSA和WIoU,这两个改动小、见效快;如果追求极致指标,再考虑P2层和更复杂的数据增强。
最后再分享一个实际操作中的体会:每加一个改进模块,都要单独做一次消融实验,不要一次性全塞进去。因为不同模块之间存在相互影响,如果直接组合,出了问题很难定位到底是谁在起作用。我在实验过程中就吃过这个亏,后来养成了每次只改动一个变量的习惯,虽然时间成本高了,但每一步的收益都能看得清清楚楚,也方便在最终部署时决定到底哪些改进值得保留。希望这份记录能让你少走几天弯路。