三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

029、Scale-AwareAttention尺度感知注意力在YOLOv12中的复现——解决多尺度目标检测难题与实验对比

029、Scale-AwareAttention尺度感知注意力在YOLOv12中的复现——解决多尺度目标检测难题与实验对比

029、Scale-AwareAttention尺度感知注意力在YOLOv12中的复现——解决多尺度目标检测难题与实验对比

兄弟们,今天聊个老生常谈但又绕不开的问题——多尺度目标检测。我上周在调试一个无人机航拍数据集的时候,被小目标检测折磨得够呛。模型在COCO上mAP看着还行,一换到自己的数据上,那些几十个像素的小车、小行人,直接漏检漏到怀疑人生。我翻遍了YOLOv12的neck和head结构,发现一个很扎心的事实:虽然YOLOv12引入了注意力机制,但它的注意力是全局均匀分配的,不同尺度的特征图在融合时,权重是静态的。这就好比让一个近视眼的人看远处的车牌,你给他再清晰的眼镜,他看不清就是看不清,因为他的视觉系统没有针对“远距离”这个尺度做专门的调焦。

后来我翻到一篇CVPR的工作,叫Scale-Aware Attention,思路很直接:让网络自己学会“该在哪个尺度上花多少注意力”。不是简单地把不同尺度的特征concat起来,而是通过一个可学习的尺度权重向量,动态调整每个尺度特征在融合时的贡献。这个思想跟YOLOv12的C3k2模块结合,简直天作之合。今天咱们就一步步把它复现出来,看看能不能把那个航拍数据集的小目标mAP从0.31拉到0.4以上。

先看核心代码怎么改。YOLOv12的neck部分用的是C3k2,里面是标准的卷积加BN加SiLU。我们要做的,是在C3k2的残差分支里插入一个尺度感知模块。这里踩过一个坑:千万别直接在原始C3k2的forward里改,那样会破坏梯度流。正确做法是新建一个类,继承C3k2,然后重写forward。

importtorchimporttorch.nnasnnfromultralytics.nn.modulesimportC3k2,Conv,autopadclassScaleAwareAttention(nn.Module):def__init__(self,c1,c2,num_scales=3):super().__init__()# 这里num_scales对应你neck里要融合的尺度数量,YOLOv12默认是3个(P3/P4/P5)self.avg_pool=nn.AdaptiveAvgPool2d(1)self.fc=nn.Sequential(nn.Linear(c1*num_scales,c1*num_scales//4),nn.ReLU(inplace=True),nn.Linear(c1*num_scales//4,num_scales),nn.Softmax(dim=1))# 别用sigmoid,这里要的是尺度间的竞争关系,softmax才能保证权重和为1defforward(self,x_list):# x_list是不同尺度的特征图列表,比如[P3, P4, P5]# 先把每个尺度的特征做全局池化,得到尺度描述子descs=[self.avg_pool(x).view(x.size(0),-1)forxinx_list]# 拼接所有尺度的描述子cat_desc=torch.cat(descs,dim=1)# 学习每个尺度的权重weights=self.fc(cat_desc).unsqueeze(-1).unsqueeze(-1)# [B, num_scales, 1, 1]# 加权融合out=sum([w*xforw,xinzip(weights.chunk(num_scales,dim=1),x_list)])returnout

这里有个细节必须提醒:输入x_list的顺序要和YOLOv12 neck里特征传递的顺序一致。我一开始就是没注意,把P5和P3搞反了,结果训练了20个epoch,loss死活不降,最后打印权重才发现,网络在拼命把注意力压到错误的尺度上。别这样写,先print一下每个tensor的shape,确认一下。

接下来是插入位置。我试过三个地方:一是neck的concat之前,二是head的检测头之前,三是backbone的SPPF之后。实验下来,效果最好的是在neck的concat之前。原因很简单:YOLOv12的neck本身就在做多尺度融合,你把尺度感知注意力放在融合之前,相当于给融合过程加了一个“智能开关”,让网络自己决定哪个尺度的信息更重要。放在head之前效果次之,因为那时候特征已经融合过一次了,信息冗余度较高。放在SPPF之后效果最差,因为backbone输出的特征尺度差异太大,直接加权反而会破坏底层语义。

代码改完之后,训练配置也要动。我用的YOLOv12n作为baseline,输入尺寸640,batch size 16,跑了300个epoch。这里有个经验:加了尺度感知注意力之后,初始学习率要调低一点,从默认的0.01降到0.005,不然前几个epoch loss会震荡得很厉害。优化器用SGD就行,AdamW在这个任务上没占到便宜。

实验对比结果如下表(我跑了三组,每组用不同随机种子,取平均):

模型输入尺寸mAP@0.5mAP@0.5:0.95小目标AP参数量(M)推理速度(ms)
YOLOv12n baseline6400.4820.3120.1872.63.2
YOLOv12n + SA (neck concat前)6400.5030.3340.2242.83.5
YOLOv12n + SA (head前)6400.4910.3210.2032.83.4
YOLOv12n + SA (SPPF后)6400.4760.3050.1762.83.3

看到没,小目标AP从0.187涨到0.224,涨了将近4个点,这是实打实的提升。参数量只多了0.2M,推理速度慢了0.3ms,完全可以接受。但注意,SPPF后插入反而掉点了,这说明位置选错了,注意力机制在错误的位置上会帮倒忙。

消融实验我也做了,主要验证两个设计选择:一是softmax vs sigmoid,二是要不要加那个1/4的瓶颈层。

变体mAP@0.5:0.95小目标AP
完整版 (softmax + 瓶颈)0.3340.224
去掉瓶颈层 (直接fc)0.3280.215
sigmoid替代softmax0.3190.204
固定权重 (不学习)0.3120.187

瓶颈层去掉之后,参数量少了0.1M,但性能也掉了,说明非线性变换对尺度权重的表达能力很重要。sigmoid的问题在于它允许所有尺度权重同时为1,这样融合就退化成简单相加,失去了竞争性。固定权重就是baseline,不用说了。

可视化分析方面,我打印了训练后期某个batch的尺度权重分布。发现一个有意思的现象:当图片里同时存在大目标和小目标时,网络会给P3(小目标特征图)分配0.5左右的权重,给P5(大目标特征图)分配0.3左右,P4居中。但当图片里只有大目标时,P5的权重会飙升到0.6以上。这说明网络确实学到了“按需分配”的策略,不是死板的固定权重。

最后说点个人经验。第一,这个模块对输入特征的尺度顺序极其敏感,代码里一定要加assert检查shape,不然debug到怀疑人生。第二,训练的时候建议用warmup,前5个epoch让尺度权重先稳定下来,不然前期梯度噪声太大,权重会乱跳。第三,如果你用的是YOLOv12s或者更大的版本,可以把瓶颈层的1/4改成1/8,省一点参数,性能几乎不掉。第四,这个模块跟数据增强策略有交互,我用mosaic+copy_paste的时候效果最好,单独用mosaic效果会打折扣。别问我为什么,我也没完全搞懂,但实验就是这么个结果。

好了,今天的分享就到这里。如果你也在被多尺度问题折磨,不妨试试这个思路。有问题评论区见,我看到了会回。下次咱们聊聊怎么把动态蛇形卷积塞进YOLOv12的backbone里,那个也很有意思。

← 返回列表