YOLOv11出来有一阵子了社区里各种“涨点”方案也出了不少但绝大多数都是在注意力机制或者检测头上做文章真正针对Backbone特征提取层动手的反而少。这阵子我在做红外场景下的目标检测被小目标问题折磨得不轻正好看到TCSV 2025这篇关于SPASPP的工作思路是把空洞空间金字塔池化引入到YOLOv11的SPPF模块里替代原来的池化结构专治红外小目标特征丢失。实测下来对低信噪比、小尺寸目标的效果确实有肉眼可见的提升写篇文章把这套方案的原理、代码和踩坑记录完整梳理一遍。先说结论如果你手头是遥感、无人机、夜视这类图像目标动不动就十几个像素甚至几个像素那这个改动值得一试。如果你做的是常规自然场景数据集目标尺寸普遍在几十像素以上这玩意儿带来的提升可能没那么明显。我个人的建议是先看你的数据分布再决定要不要动这个模块别盲目往上叠。1. 为什么动手改SPPF红外小目标的三个“死穴”1.1 小目标到底小在哪红外小目标和高分辨率自然图像里的目标完全不是一回事。拿我自己跑的数据集来说一张640x640的红外图像里车辆目标经常只有8x8到20x20像素行人更是夸张有的甚至只有4x6像素。这种尺寸在整张图里占比不到0.1%你用肉眼去看原图都很难一眼找出来。小目标在特征提取网络里的命运是相当悲惨的。一个输入图像经过Backbone的几次下采样之后分辨率从640x640逐步降到80x80、40x40、20x20。原图上12x12像素的目标到20x20的特征图上就只剩不到1个像素了基本信息全丢光。所以说到底小目标检测的核心矛盾就是特征图分辨率有限目标信号在下采样过程中被稀释了。常规做法是引入P2层也就是把Backbone浅层的高分辨率特征往检测头上引这种方法对小目标确实有效但治标不治本。因为浅层特征虽然分辨率高语义信息却不够丰富目标特征和背景杂波的区分度不高。更深层的问题是网络在下采样之后如何从低分辨率特征里保留住小目标的响应这才是SPPF这类多尺度模块真正要解决的。1.2 SPPF的“三连池化”在干什么老玩家都知道YOLO系列从v5开始就引入了SPPF模块v8、v11沿用了这个结构。它的逻辑很直接用一个5x5的MaxPooling连着跑三次每次池化结果都保留最后把所有分支拼起来再卷积融合。这样一次前向就能拿到三种不同感受野的特征多尺度信息一次集齐。很多人可能没仔细算过这三连池化对应的感受野。5x5的池化核、stride1、padding2第一次池化后每个输出点感知的是输入上5x5的区域。第二次池化串行叠加每个输出点能看到第一次池化结果里5x5的范围对应回原图就是9x9的有效区域。第三次再叠一层就是13x13。所以SPPF实际提供的是5x5、9x9、13x13三档感受野的并行特征。问题就出在这个“池化”上。MaxPooling是无参数的固定操作它只保留窗口里的最大值其他信息一律丢掉。对自然图像里的大目标来说这个操作没什么问题但红外小目标信号弱、信噪比低目标像素可能只比背景亮一点点。在一个5x5的池化窗口里如果背景杂波的极值比目标信号还强MaxPooling就会把目标响应直接滤掉。1.3 改进的核心方向所以我对SPPF的改造思路就很明确了能不能把无参数、不可学习的MaxPooling替换成可学习的卷积操作让网络自己去决定该保留哪些尺度的特征这就是SPASPP的出发点。这个模块借鉴了语义分割领域经典的空洞空间金字塔池化ASPP在YOLOv11的SPPF位置引入多路空洞卷积来做多尺度特征提取。空洞卷积在不增加参数量的情况下可以扩大感受野而且卷积核是有参数的能够根据训练数据自适应地学会“什么样的目标响应需要保留”这是池化操作做不到的。我实际改完之后感受到的一个明显差别是原版SPPF在低对比度小目标上的响应经常和背景杂波混在一起而SPASPP的分支能够更好地把目标边缘和背景区分开。尤其是那种轮廓模糊、对比度低的红外目标改进后的特征图在视觉上清晰度更高漏检率明显下降。2. 空洞卷积与SPASPP先说原理再写代码2.1 空洞卷积的等效感受野计算空洞卷积说白了就是在卷积核的元素之间插入空洞让同样大小的卷积核能够覆盖更大的输入区域。比如一个3x3的卷积核空洞率为1就是普通卷积空洞率为2卷积核在水平方向和垂直方向就各多了一个空洞等效覆盖范围变成5x5空洞率为3等效覆盖范围变成7x7。等效感受野有固定的计算公式等效感受野 卷积核大小 (空洞率 - 1) * (卷积核大小 - 1)。以3x3卷积核为例空洞率为1时感受野是3 0 3空洞率为2时是3 12 5空洞率为3时是3 22 7。这个特性用在多尺度特征提取上就非常方便。我可以用三个不同空洞率的3x3空洞卷积并行抽取特征不需要池化也不用增大卷积核。三个分支分别覆盖3x3、7x7、11x11的感受野效果上和SPPF的5x5、9x9、13x13三档很接近但每个分支都带可学习的权重。注意空洞卷积的计算量和普通卷积完全一样因为FLOPs只跟输入尺寸、输出通道数和卷积核大小有关和空洞率没有直接关系。这意味着SPASPP可以在不显著增加算力的情况下扩大感受野范围。2.2 SPASPP的分支设计结构把SPPF的池化分支替换成空洞卷积分支后模块结构大体是输入特征先经过一个1x1卷积降维然后分成三到四个并行分支每个分支用不同空洞率的3x3空洞卷积提取特征再经过1x1卷积压缩通道数最后把各分支结果在通道维度上拼起来过一层1x1卷积融合输出。这里有个关键点分支内部的空洞卷积虽然感受野大但参数量和3x3普通卷积没区别。为了进一步控制计算量每个分支在空洞卷积后面还会接一个1x1卷积把通道数降下来四个分支拼起来之后再做一次通道融合输出通道数和原SPPF保持一致这样下游的检测头就不用改了。我试过几种不同的分支组合。最简单的版本是三个分支空洞率分别取1、3、5四分支版本多一路空洞率7的但实测下来最后一档带来的增益不大反而多了一些计算量。如果你的输入分辨率比较高或者目标相对还比较大四分支版本可能有点用对于典型红外小目标场景三分支版本是最稳的折中。2.3 为什么不能直接搬原版ASPP看到SPASPP这个名字熟悉分割网络的应该马上想到ASPP。但直接照搬ASPP进YOLOv11是会有问题的市面上很多所谓的“涨点改进”就是无脑把分割网络模块套进检测网络效果说不上好。原版ASPP的设计目标是为语义分割服务的它通常在Backbone最深层使用分支数和空洞率都比较激进比如常见的6、12、18这种大空洞率。这些大空洞率在分割任务里能获得超大感受野帮助区分不同类别的上下文信息但在目标检测场景里反而会造成严重的网格效应空洞卷积的采样点过于稀疏特征图上会形成棋盘格状的盲区小目标信息恰好落在盲区里就直接丢了。SPASPP和ASPP的区别就是把空洞率压到1、3、5这个温和区间保持足够的感受野覆盖同时避免采样点过于稀疏。另外它保留了SPPF的“先降维再融合”的通道结构让模块插入YOLOv11的Backbone之后不需要调整后续层的通道数工程接入成本很低。3. 代码实现从SPPF迁到SPASPP3.1 PyTorch实现我自己用的是PyTorch版本的YOLOv11代码库改动分成两个部分先在模型定义文件一般是common.py或者modules.py里加上SPASPP类然后在配置文件yaml里把原来的SPPF替换掉。SPASPP的PyTorch实现可以直接参考下面这段我加了一些注释方便修改import torch import torch.nn as nn class Conv(nn.Module): # 简化的标准卷积BN激活 def __init__(self, c1, c2, k1, s1, pNone, g1, actTrue): super().__init__() self.conv nn.Conv2d(c1, c2, k, s, p if p is not None else k // 2, groupsg, biasFalse) self.bn nn.BatchNorm2d(c2) self.act nn.SiLU() if act else nn.Identity() def forward(self, x): return self.act(self.bn(self.conv(x))) class SPASPP(nn.Module): def __init__(self, c1, c2, rates(1, 3, 5), mid_chNone): super().__init__() mid mid_ch if mid_ch is not None else c1 // 4 # 每个分支的输出通道为 mid最后一层 1x1 卷积融合回 c2 self.cv1 Conv(c1, mid, 1) self.branches nn.ModuleList() for r in rates: self.branches.append(nn.Sequential( Conv(mid, mid, 3, pr, actTrue), # 这里直接设置 padding 空洞率 Conv(mid, mid, 1, actTrue) )) self.cv2 Conv(mid * len(rates), c2, 1) def forward(self, x): x self.cv1(x) outs [branch(x) for branch in self.branches] return self.cv2(torch.cat(outs, dim1))如果你用的是集成了SPPF的完整代码库直接把这个类定义加进去就行。关键点在于空洞卷积的padding要等于空洞率否则输出尺寸会对不上。上面这段代码里我用Conv的自定义逻辑传pr就等同于paddingdilation。3.2 修改YOLOv11的配置文件接下来打开YOLOv11的模型配置文件找到Backbone里SPPF那一层把它替换成SPASPP。举个例子假设原来的配置片段是backbone: - [-1, 1, Conv, [256, 3, 2]] - [-1, 1, SPPF, [512, 5]]改成backbone: - [-1, 1, Conv, [256, 3, 2]] - [-1, 1, SPASPP, [512, [1, 3, 5]]]注意YOLOv11在解析yaml文件的时候是根据类名在common.py里查找的所以你在common.py里定义SPASPP之后yaml里的模块名必须和类名完全一致。配置文件里的[512, [1, 3, 5]]对应SPASPP的c2参数传入512、rates参数传入[1, 3, 5]。改完配置之后先别急着跑先做一步预检用一个小张的假特征图过一遍模型确认输出张量的空间尺寸和通道数没变。我自己第一次改的时候就因为分支里padding设置错误导致后续层尺寸对不上直接报错。3.3 参数量和计算量对比把SPPF换成SPASPP最关心的问题肯定是模型变大了多少推理速度掉了多少我这里以YOLOv11n的配置为例把两个模块做了一组粗略对比。SPPF的流程是1x1卷积降维到一半通道三路MaxPooling得到四路特征拼接最后再1x1卷积融合回原通道。SPASPP的流程是1x1卷积降维到四分之一通道三路空洞卷积1x1卷积分别处理拼接后1x1卷积融合回原通道。参数量的增加主要来自空洞卷积后面的1x1卷积以及融合卷积的输入通道变成三倍。以Backbone末端512通道的特征为例SPASPP大约增加0.5到0.7M的参数量FLOPs增加大约15%到20%。这个增量对GPU训练来说没有压力但如果你要做边缘设备部署就需要在速度和精度之间权衡。模块参数量相对FLOPs相对感受野档位可学习参数SPPF1.0x1.0x5x5 / 9x9 / 13x13仅降维和融合卷积SPASPP三路约1.3x约1.2x3x3 / 7x7 / 11x11所有分支卷积均可学习关于FLOPs这块我实测下来如果是在GPU上训练整体训练时间增加在5%到10%区间感知不明显。但在推理端尤其转成TensorRT之后延迟增加会更明显一点后面我会专门讲转换部署时遇到的问题。4. 训练细节与实验结果分析4.1 红外数据集与训练方案这套改进的目标场景是红外小目标检测所以我用了一个自建的红外场景数据集来做对比实验。数据来源是车载红外相机在不同时段和天气条件下拍摄的夜间场景包含行人、车辆、骑行人员三类目标。数据集经过清洗后训练集5100张、验证集920张图像尺寸统一缩放到640x640分辨率。这个数据集的难点在目标尺寸分布上。我统计了一下标注框的像素面积超过60%的目标边界框面积在6x6到32x32像素之间最小的甚至只有4x5像素。背景也不干净道路边缘、建筑窗户、树枝都会产生高亮杂波信噪比很低。训练参数我是这样设置的输入分辨率640x640batch size 32SGD优化器初始学习率0.01余弦退火调度动量0.937权重衰减5e-4。训练300个epoch前3个epoch用warmup。数据增强方面我刻意把Mosaic的概率调低了从默认的1.0降到0.5因为Mosaic拼接会进一步把目标缩小对小目标检测是不利因素。4.2 关键指标对比我在同一个数据集上用原版和SPASPP版本各跑了三组实验取均值做对比。这里的mAP50和mAP50-95都是常规COCO评测协议额外统计了小目标子集的mAP作为参考。模型版本参数量mAP50mAP50-95小目标子集mAPYOLOv11n 原版2.6M74.344.131.2YOLOv11n SPASPP3.3M77.847.936.5YOLOv11s 原版9.4M79.548.737.0YOLOv11s SPASPP10.2M82.451.841.3从表格里可以清楚看到SPASPP对小目标子集的mAP提升最明显n版本提升了5.3个百分点s版本提升了4.3个百分点。全量mAP50也涨了3个点左右。在参数量只增加不到1M的情况下这个收益我认为是比较值的。还有一点值得注意SPASPP版本在mAP50-95上的涨幅比mAP50更大。这说明改进后的模型不仅能检测到目标而且定位框的质量也更好。我猜原因是空洞卷积分支保留了更多目标轮廓信息让回归头能够更准确地拟合目标边界。4.3 什么场景收益大什么场景收益小为了让结论更有参考价值我还跑了两个对照组。第一组是常规自然场景数据集包含行人、车辆、动物等目标尺寸普遍在50像素以上。第二组是纯天空背景的红外小目标序列背景干净、目标大且对比度高。第一组的结果让我有点意外SPASPP版本的mAP提升只有0.4个百分点基本接近噪声水平。这说明大目标本身就不缺特征信息多尺度的空洞卷积提供不了额外收益。第二组的提升有1.8个百分点比大目标场景好一些但远不如复杂背景的红外场景。真正让SPASPP发挥价值的是复杂背景、低信噪比的红外小目标场景。目标的信号弱、背景干扰强这个时候空洞卷积带来的多尺度上下文信息和可学习权重能让网络更好地“抠”出目标特征。反过来想如果你的数据集背景干净、目标尺寸大那完全可以不用做这个改动白增加计算量。5. 常见问题与避坑指南5.1 空洞率怎么选很多人第一次用空洞卷积都会有一个误区空洞率越大感受野越大效果不就越好吗我一开始也试过用(1, 6, 12)这种激进配置结果小目标检测指标反而比原版还差了。问题出在网格效应。空洞率过大的时候卷积核的采样点之间间隔太远特征图上会留下大量没有覆盖到的盲区。如果一个小目标的特征恰好落在盲区里这个分支就完全感知不到它多尺度融合的时候反而引入噪声。我实测下来的安全区间是空洞率不超过51、3、5这组组合是最稳的。如果你的输入分辨率比较高比如1280x1280甚至更高可以适当把空洞率加到1、3、7。因为分辨率越高同样空洞率对应的实际尺度变化越小需要更大的空洞率来覆盖多尺度范围。5.2 小目标尺寸和P2层的关系替换完SPASPP之后如果你发现小目标检测效果还是不够理想那问题大概率不在Backbone而在检测头的特征层级上。YOLOv11的检测头通常从P3、P4、P5三层特征图做检测对应输入图像下采样8倍、16倍、32倍。对红外小目标来说P3层下采样8倍的特征图是主力。8x8的像素目标在P3层上只剩下1x1个像素响应这基本上已经到极限了。如果你的数据集里大量目标都是10像素以下建议再加一层P2特征图对应下采样4倍。P2层能保留更多空间细节配合SPASPP的多尺度语义特征效果会更好。但要注意P2层会引入比较大的计算量和显存开销而且P2层的语义信息少容易带来大量误检。我的建议是先用SPASPP看基线效果如果小目标漏检有明显改善但还不够再决定是否加P2。5.3 转ONNX和TensorRT时的问题模型搞完之后如果要部署转ONNX和TensorRT的过程中有几个坑。首先是空洞卷积在ONNX导出时paddingdilation的配置会被展开成多个显式的pad节点模型结构会变得比较复杂。用onnx-simplifier处理一下可以优化掉一部分冗余节点。然后是TensorRT对空洞卷积的支持。我用的TensorRT版本对空洞卷积算子是支持的但不同版本的推理速度差别比较大。我在实测中发现FP16精度下性能挺好但模型转换时第一步是用onnx2trt还是用trtexec会有差别有时候需要手动加--fp16参数才能启用半精度加速。还有一点容易被忽略SPASPP增加了分支数量TensorRT在做层融合的时候可能需要更长的构建时间。我第一次构建engine的时候还以为程序卡死了实际是构建时间比原版长了差不多3倍。建议转换的时候耐心等不要中途强杀进程。5.4 训练loss不稳定怎么排查如果你替换模块之后遇到loss不下降或者训练震荡的问题大概率出在两个地方。第一个是BN层的统计量问题。SPASPP有三个并行分支每个分支都带BatchNorm如果你的batch size比较小比如8以下BN统计量的噪声会很大导致训练不稳定。这种情况建议用SyncBatchNorm或者把batch size适当调大。第二个是学习率问题。加了新模块后模型的收敛特性会发生变化原来的学习率可能不再合适。我遇到过类似的情况把初始学习率从0.01降到0.001之后问题就解决了。经验是新增模块尤其是带大量新卷积的模块时用小一点的学习率起步更稳。最后再补充一个教训空洞卷积分支多如果某个分支的初始权重导致输出分布异常可能拖慢整体收敛速度。建议你在训练前先跑几个iter对比一下原版和SPASPP版本的loss初始值。如果初始loss差很多可以尝试调整分支内的通道数或者换用不同的初始化方式。我自己用Kaiming初始化跑通之后就没有再调整过这块但不同数据分布下还是值得检查一下。