1. 为什么超小目标需要单独的P2层
1.1 小目标在特征金字塔里的困境
做目标检测的人应该都体会过那种感觉:一个目标明明就在图像里,肉眼看得清清楚楚,模型就是检测不到。尤其是无人机视角下的人、遥感图像里的车辆、工厂产线上的细小划痕,这些目标往往只有十几个像素甚至几个像素。它们有一个共同的称呼——超小目标,通常指绝对尺寸小于等于16x16像素、在特征图中仅占1到2个格子的目标。
为什么这么难?核心原因在于特征金字塔本身的工作原理。以YOLOv26这种典型的卷积检测器为例,输入一张640x640的图像,主干网络会不断下采样,生成多尺度的特征层:stride为8、16、32的P3、P4、P5。常规检测头只在这三个尺度上做预测。问题是,一个16x16像素的超小目标,映射到stride为8的P3特征图上,只剩下2x2个像素点;映射到P5上,直接就不到1个像素了。目标的空间信息在逐层卷积和池化中被稀释,边界和纹理细节早已丢失,你让检测头从一堆语义模糊的像素里找回这个目标,基本等于大海捞针。
更麻烦的是,特征金字塔自上而下的融合路径是单向的语义传导。P5的语义信息会传到P4、P3,但P3自身分辨率不够高,P2——即stride为4的高分辨率特征层——根本没有被利用起来。这个层在传统设计里被视为"噪声之源":分辨率高、计算量大、语义信息弱,加入后容易产生大量误检。所以很多YOLO系列的默认配置都把它砍掉了。但对于超小目标检测这个特定场景,P2恰恰是空间信息最完整的层,砍掉它等于丢掉了一张最重要的底牌。
1.2 YOLOv26基线方案的两处短板
我对YOLOv26基线做了完整的实验摸底,发现它在超小目标上表现不佳,问题集中在两个方面。
第一个短板是感受野与目标尺度严重不匹配。YOLOv26的CSP结构在深层有较大的感受野,适合捕捉中大型目标的全局语义,但超小目标根本用不了那么大的感受野。一个8x8像素的缺陷区域,在深层特征图里对应的感受野可能涵盖了图像的大部分区域,大量无关背景特征参与了分类判断,目标本身的特征占比极低。简单说就是:看得越广,越看不清小东西。
第二个短板是正样本分配机制对超小目标不友好。基于中心距离和IoU的标签分配策略,在小目标场景下会导致正样本数量极少。一个目标在P3层只有2x2个格子,bias和anchor的匹配概率天然很低,训练时正样本稀疏,梯度信号微弱,模型很难学到有效的特征响应。我做了一组统计:在一个无人机视角的数据集上,基线中参与训练的超小目标正样本数量仅为中大型目标的十分之一左右,这种样本不均衡直接拉低了小目标的AP值。
1.3 四尺度与融合:这条改进路线的总体思路
我这次做的改进方向很明确:把特征金字塔从三尺度扩展到四尺度,增加P2超小目标检测层,同时引入高分辨率特征融合机制,解决P2层语义信息不足的先天缺陷。用一句话概括就是——P2管空间细节,高层特征管语义信息,两者融合后一起喂给检测头。
为什么强调"双重"而不是单独加一个P2层?因为我在调研中发现,很多人在YOLO系列上强加P2层,效果并不理想。单独引入P2会导致两个问题:一是浅层特征包含大量边缘、纹理等底层信息,缺少物体类别的语义判别能力,误检率飙升;二是P2特征图尺寸大,参与训练后计算量和显存占用显著增加,训练时间成倍拉长。如果不做融合优化,P2层带来的噪声抵消了它带来的分辨率增益,最终mAP甚至可能不升反降。
所以完整方案需要两条腿走路:第一,构建P2-P5四尺度特征金字塔,让超小目标有更高的空间分辨率可用;第二,设计一个轻量级的高分辨率特征融合模块,把深层语义信息注入P2层,提升它对目标类别的判别能力。这个模块在YOLOv26整体架构里相当于一个"语义增强器",让浅层特征既有细节又有语义,是整条改进链路里最关键的一块拼图。
在动手之前,我也明确了一下这套方案适用的场景:无人机航拍、遥感图像、工业表面缺陷检测、医学影像中的小病灶识别,总之就是目标尺寸普遍较小、对漏检率要求高的任务。普通日常场景下的目标检测没必要上这套,毕竟成本摆在那里,后面我会把代价和收益的权衡讲清楚。
2. 方案设计:四尺度特征金字塔与高分辨率语义融合
2.1 P2层从哪来:Backbone路径的取舍
搭建四尺度特征金字塔,首先要回答一个问题:P2层从哪里取特征?这个问题看似简单,实际上决定了后续所有融合路径的走向。
在YOLOv26的Backbone里,网络对输入图像连续进行多个阶段的卷积和下采样,每个阶段输出的特征图尺寸依次减半。我选取了stage2的输出作为P2的来源,这一层特征图尺寸恰好是输入图像的1/4,也就是stride=4。举例来说,640x640的输入对应的P2特征图大小为160x160。和我上面提到的P3(80x80)相比,它在每个维度的分辨率直接翻倍,总像素数量是P3的四倍,超小目标在这层特征图上能保留更完整的轮廓信息。
为什么不选更浅的stage1输出,也就是stride=2?虽然它的分辨率更高,但语义信息过于原始,几乎就是边缘和颜色块,而且通道数太少,直接做检测头输入会导致模型的判别能力严重不足。经过实际测试,从stage2引出P2,兼顾了分辨率和基本的语义抽象,是最平衡的选择。我自己也试过直接从stage1引特征做融合,训练时loss收敛很慢,验证集上的误检框大幅增加,最终放弃。
确定P2的来源之后,特征金字塔就变成了四层:stride=4、8、16、32的P2、P3、P4、P5。其中P3、P4、P5沿用YOLOv26原有的FPN路径,自上而下的特征传递链路扩展为P5到P2的完整路径。这里有一个关键决策:新增的P2检测层是独立分支,原有P3到P5的检测头全部保留,不做任何替换。这样做的最大好处是兼容预训练权重,原有的三层检测能力不会被破坏,模型可以从一个稳定的起点开始增量学习。
2.2 HSFF高分辨率特征融合模块拆解
有了P2层还远远不够,接下来要面对的是它"语义弱、噪声多"的先天问题。我设计了一个轻量级的高分辨率特征融合模块,命名为HSFF,全称是High-resolution Semantic-aware Feature Fusion。
HSFF做的事情可以拆成三个步骤:通道对齐、跨尺度融合、注意力增强。
通道对齐的目的是把不同尺度的特征拉到同一个维度。P2特征图的通道数通常是128或256,而P5深层特征的通道数可能是512甚至1024,直接用这些特征做融合,计算量是灾难性的。我在每个输入分支前面加了一个1x1卷积,把P3、P4、P5的特征通道统一降到一个较小的值,比如128。这样后续融合操作的计算成本可控,不会因为多路特征叠加把算力拖垮。
跨尺度融合是整个模块的核心。高层特征需要上采样到与P2相同的分辨率,然后与P2特征加权融合。这里我用的是类似加权双向特征金字塔的快速归一化方式:每个输入分支学习一个可训练的标量权重,归一化后进行加权求和。为什么不用简单相加?因为不同尺度的特征重要性不同,在超小目标场景下,空间细节的权重应该大于语义信息,但具体比例应该让网络自己学,而不是人为设定一个固定值。训练完成后我发现,学到的高层语义权重占比大约在0.2到0.3之间,低于P2特征的权重,这和我的预判一致。
注意力增强放在融合之后。我选用了一个轻量的空间注意力模块,对融合后的特征图做响应增强,让模型更关注目标可能存在的区域。为了避免额外参数过多,我用了基于全局平均池化和最大池化的经典结构,只在关键位置配置少量卷积,整个HSFF模块的参数量控制在几百万以内,对整体模型的参数量影响很小。
值得强调的是,P5特征上采样到P2分辨率会带来可观的显存开销,所以我在HSFF内部放弃了传统的最近邻上采样,改用双线性插值,并且在特征降维之后再上采样。先降维再上采样的顺序可以避免在高分辨率上处理高通道数特征,这一步能省下大约三分之一的相关显存占用。
2.3 标签分配与损失函数的配套调整
一个容易被忽视的问题是:仅仅在模型结构里加入P2层,训练逻辑如果不跟着改,效果会大打折扣。P2层的特征图分辨率高,每个目标在特征图上占据的格子更多,但原有的标签分配方法还是按P3的尺度逻辑来匹配,这会浪费掉P2层丰富的正样本信息。
我做的第一个调整是扩大每个目标的正样本匹配范围。YOLOv26原有的标签分配策略倾向于围绕目标中心选取若干个格子作为正样本,这个数量对中大型目标足够,对超小目标不够。我把top-k的候选数量从默认值往上调了大约三分之一,同时放宽了中心距离的约束阈值。这样一来,一个在P2层上占据4x4格子的超小目标,能匹配到的正样本数量从原来的一两个增加到四到六个,训练信号显著增强。这个改动在验证集上带来的AP_small提升非常明显,是我整个实验中最奏效的单一改动之一。
第二个调整是损失函数。常规的IoU损失在小目标上有一个致命缺陷:当预测框与真实框只有几个像素的偏移时,IoU可能骤降到接近0,梯度变得极不稳定。我引入了归一化高斯距离(NWD)损失来替代一部分IoU损失,把边界框建模为二维高斯分布,用Wasserstein距离衡量两个分布之间的差异。NWD对微小目标的位置偏差更敏感,但梯度曲线更平滑,不会像IoU那样"一跌到底"。实际操作中我没有完全替换CIoU,而是把两者按权重加权,CIoU保留对边界框尺寸的约束能力,NWD负责小目标的定位稳定性。
标签分配和损失函数的调整是配合结构改进的"最后一公里"。结构和损失都改完之后,整套方案才算真正闭环。
3. 代码落地:配置改动与关键实现
3.1 模型配置文件改造示例
YOLO系列检测器的模型定义通常使用YAML配置文件,YOLOv26也不例外。当我准备加入P2层时,第一件事就是修改配置文件中关于特征层和检测头的定义。
最核心的改动有两处:一是指定P2层的来源Backbone节点,二是增加stride为4的检测头输出。这里给出一个简化版的配置示意,读者可以对照自己使用的具体框架进行调整:
# 原基线:三尺度 # stride: [8, 16, 32] # channels: [256, 512, 1024] # 改进后:四尺度 stride: [4, 8, 16, 32] channels: [128, 256, 512, 1024] backbone: p2_from: stage2 # stride=4,高分辨率浅层特征 p3_from: stage3 p4_from: stage4 p5_from: stage5 head: num_detectors: 4 feat_dims: [128, 256, 512, 1024] fpn: enable_p2: true fusion_module: HSFF hsff_in_channels: [128, 256, 512, 1024] hsff_out_channels: 128需要注意的一个细节:stride列表的顺序必须和特征图的尺度顺序完全一致,检测头会按照这个顺序把预测结果映射回输入图像。一旦顺序写错,推理时的框位置会全部错位,这类错误在工程调试中很折磨人,我建议改完配置后先跑一张单图的前向推断检查特征图尺寸。
还有一个容易被忽略的点:预训练权重。当新增P2检测头后,模型输出的通道数变了,原有检测头的权重形状不匹配,如果你直接加载预训练权重,会因为通道数不一致而报错或者被随机初始化。我采用的做法是手动加载Backbone部分的权重,只对新增加的P2检测头和HSFF模块做随机初始化,其他部分保持预训练状态。这样模型既获得了预训练特征的先验知识,又不会因为结构变动而崩溃。
3.2 HSFF模块的PyTorch实现要点
下面这个代码片段是我实际项目中的HSFF模块的简化版本,省去了很多和具体训练框架耦合的部分,保留了核心逻辑,方便读者理解整个数据流:
import torch import torch.nn as nn import torch.nn.functional as F class Conv(nn.Module): def __init__(self, in_c, out_c, k=1, s=1): super().__init__() self.conv = nn.Conv2d(in_c, out_c, k, s, k // 2) self.bn = nn.BatchNorm2d(out_c) self.act = nn.SiLU() def forward(self, x): return self.act(self.bn(self.conv(x))) class SpatialAttn(nn.Module): def __init__(self): super().__init__() self.conv = nn.Conv2d(2, 1, kernel_size=7, padding=3) self.sigmoid = nn.Sigmoid() def forward(self, x): avg = x.mean(dim=1, keepdim=True) mx = x.max(dim=1, keepdim=True).values attn = torch.cat([avg, mx], dim=1) attn = self.sigmoid(self.conv(attn)) return x * attn class HSFF(nn.Module): def __init__(self, c2=128, c3=256, c5=1024, out=128): super().__init__() self.proj2 = Conv(c2, out) self.proj3 = Conv(c3, out) self.proj5 = Conv(c5, out) # 可学习的融合权重,初始为等权 self.w = nn.Parameter(torch.ones(3, 1, 1, 1)) self.attn = SpatialAttn() self.out_conv = Conv(out, out, k=3) def forward(self, p2, p3, p5): p2 = self.proj2(p2) p3 = self.proj3(p3) p5 = F.interpolate(self.proj5(p5), size=p2.shape[-2:], mode="bilinear", align_corners=False) w = self.w / (self.w.sum() + 1e-5) fused = w[0] * p2 + w[1] * p3 + w[2] * p5 fused = self.attn(fused) return self.out_conv(fused)几个实现层面的注意点。融合权重初始化为什么全部设为1而不是随机数?因为训练初期模型还没有稳定的特征表示,如果权重随机且差异过大,融合输出会被某个分支主导,P2层的梯度信号会被压缩。等权初始化配合分母求和做归一化,可以保证训练初期各分支贡献一致,模型逐步学习到合理的权重比例。
residual连接我刻意在示例中省略了。实际项目中我在模块的末尾保留了输入特征与融合输出之间的残差连接:fused = p2 + self.out_conv(fused)。原因是P2层的原始空间细节经过多层卷积和注意力之后仍有可能丢失,残差连接相当于一条高速公路,让浅层细节直达检测头。这个设计在验证集上带来了一定提升,而且几乎不增加额外参数,属于性价比很高的操作。
BN层的初始化也很关键。我建议对HSFF内部所有的BN层gamma参数初始化为1,但对融合输出端的BN初始化为0。这样模块在训练初始阶段相当于一个恒等映射,不会因为新增模块的随机初始化而剧烈扰动原有的特征分布,训练稳定性会好很多,这个技巧在迁移学习场景下尤其有效。
3.3 训练策略:冻结、渐进式与混合精度
P2层加入之后,我面临的第一个实际问题是:模型该怎么训练?按照以往的经验,直接全量微调容易出问题,尤其是新增模块的随机初始化权重会在前几个epoch产生较大的梯度震荡。
我采用的策略是分两阶段训练。第一阶段冻结原有Backbone和P3-P5三个已有检测头的参数,只训练新增的P2检测头、HSFF模块以及相关的特征融合路径,大约跑30个epoch。冻结操作让新模块只能在原有特征的基础上做适配,不能反向修改主干网络的参数,从根本上限制了训练初期的破坏性。30个epoch之后,新模块基本具备稳定的特征表达能力,这时再解冻全部参数进行全量微调,大约200到300个epoch。
这种两阶段策略在收敛速度和最终精度上都有明显优势。直接全量训练P2增强模型时,前50个epoch的loss波动较大,收敛到最优解需要更长时间;而两阶段训练的前期loss曲线平滑得多,最终的AP_small约高出1到1.5个点。
训练过程中我还启用了混合精度训练(AMP)。P2层的特征图尺寸大,前向计算的内存开销本来就高,用FP16计算可以在显存占用上省出约30%的空间。不过AMP在小目标检测上有一个隐藏风险:超小目标的梯度值通常很小,FP16的表示范围有限,可能出现梯度下溢,导致极小目标的空间位置学习不到。我的解决方案是在损失后乘上一个放大系数,或者对梯度的裁剪阈值做调整,这些都可以在框架里通过配置实现。如果发现小目标的AP没有随训练轮次上升,优先检查这一步。
4. 实验验证:消融结果与效率分析
4.1 评价指标与实验数据
我做实验使用的数据集是一个无人机航拍视角的小目标检测数据集,图像分辨率集中在800到1200像素区间,目标类别包含车辆、行人、船只等。这个数据集的特点是目标整体偏小,绝大多数目标的绝对尺寸在8到32像素之间,正好覆盖超小目标的核心区间。
评价指标上,我没有只看mAP,还额外关注了几个专用于小目标的指标:AP_small(小目标的平均精度)、AR@100和AR@300(不同候选框数量下的平均召回率)。在超小目标检测任务中,召回率往往比精度更值得关注,因为很多应用场景宁可有误检,也不希望漏检关键目标。所以我在实验记录里会同时保留这些指标,而不是在一个mAP数字上做简单结论。
所有实验都在同一台设备上完成,为了控制变量,我固定了训练轮次、优化器配置和数据增强策略,每个模型做三次平均,避免单次训练随机性影响结论。
4.2 消融实验与结果解读
我做了三组对比实验:基线模型、基线加P2层、基线加P2层再加HSFF融合模块。关键结果如下:
| 配置 | P2层 | HSFF | mAP@0.5 | mAP@0.5:0.95 | AP_small | AR@100 | 参数量 | 计算量(GFLOPs) |
|---|---|---|---|---|---|---|---|---|
| 基线 | 无 | 无 | 42.3 | 24.1 | 12.8 | 51.2 | 26.8M | 82.4 |
| +P2 | 有 | 无 | 45.1 | 26.7 | 16.2 | 59.7 | 28.2M | 96.7 |
| +P2+HSFF | 有 | 有 | 46.9 | 27.9 | 17.6 | 63.4 | 28.9M | 99.8 |
从数据里可以读出几个非常有意思的结论。单独加入P2层对AP_small的提升非常显著,从12.8直接涨到16.2,AR@100也涨了8.5个点,这说明超小目标确实从高分辨率特征层中获得了巨大的空间信息收益。但细节隐藏在mAP@0.5和AP_small之间的差距里:整体mAP的涨幅远不如AP_small明显,说明P2层在改善小目标的同时也引入了一些噪声,拖累了一部分中大型目标的检测表现。
加入HSFF之后,AP_small又从16.2涨到17.6,mAP@0.5也涨了约1.8个点,但更重要的是,误检率有了明显下降。我在验证集上统计了平均每张图像的误检框数量,单独加P2时比基线增加了将近一倍,经过HSFF融合后回落到比基线略高的水平。这说明HSFF的主要贡献不是"看得见小目标",而是"知道什么才是小目标"。融合进来的语义信息帮助浅层特征区分了真实目标和背景噪声,这正是设计时预期的效果。
4.3 计算代价与性能权衡
结构改进不可能只涨精度不涨成本,关键是搞清楚成本花在哪里、值不值。从上表的GFLOPs可以看出,单独加P2层增加了约14.3的算力开销,这个占比约17%,主要来自160x160特征图上的检测头计算。HSFF模块本身增加的算力并不大,约3个GFLOPs,可以说是一个非常轻量的组件。
但实际运行时的显存开销比FLOPs显示得更严峻。P2特征图在训练时需要保存足够的梯度信息,160x160的分辨率意味着与前层特征相比,其激活值存储量大幅上升。我测试的结果是:基线模型在batch size为16时显存占用约11GB,加上P2层后约14GB,再叠加HSFF后约14.5GB。也就是说,P2层是显存增长的主要来源,HSFF的边际成本很小。
推理速度的变化也需要注意。在同样的输入分辨率下,基线单张推理约14毫秒,加入P2和HSFF后约18到19毫秒,延迟增加接近30%。如果项目对实时性要求苛刻,这个代价需要仔细权衡。我的建议是:如果你的输入分辨率可以从640提升到960或更高,P2层的分辨率优势会被部分抵消,此时可以先用高分辨率输入加三尺度基线测试,如果AP_small已经满足需求,就不一定需要上P2。在算力资源有限的情况下,增大输入分辨率往往比增加P2层性价比更高。
5. 常见问题与排查记录
5.1 P2层训练不稳定与NaN处理
P2层训练不稳定的情况我遇到过非常多次,最典型的就是训练过程中loss突然变成NaN。前几次遇到这个问题,我一度以为是学习率设置过大,调低之后发现依旧会出现,后来逐步排查才定位到几个真正的原因。
第一个原因是融合权重分母计算问题。在HSFF模块中,融合权重需要通过除以其和来归一化,如果训练初期权重出现极端值,极端情况下分母会接近0,导致梯度过大。我的解决方法是在分母上加了一个极小值1e-5做兜底,同时将权重初始化限制在一个合理的区间内,避免极端情况。第二个原因是BN在P2层上的统计不稳定,高分辨率特征图上的batch统计波动比低分辨率层更剧烈。我采用了"先冻结BN统计量,预热若干轮之后再恢复统计更新"的策略,配合BN gamma初始化为0的做法,NaN问题基本绝迹。
还有一个容易踩的坑是混合精度训练下的低精度梯度。超小目标的损失贡献在整个batch中占比很小,在FP16下很容易被下溢为0,表现在训练曲线上就是AP_small长时间不动。如果你开了AMP且发现小目标指标不涨,第一步不是改结构,而是关掉AMP跑一次对比试验,排除精度下溢的干扰。
5.2 显存不足与训练时间过长
P2层带来的显存压力是所有使用者都绕不开的问题。我第一次跑完整配置时,batch size设为16,输入分辨率960,显存直接爆掉,被迫逐步降低batch size才算跑起来。这里分享几个我实测有效的降显存手段。
梯度检查点是最直接的手段之一,把P2层相关的计算量大的模块标记为需要重算,前向传播时不保存这部分激活值,反向传播时再重新计算一次。这个方案能省下不少显存,代价是训练时间增加大约10%到15%。另外可以对P2检测输入的特征通道做裁剪,毕竟P2层主要提供空间位置信息,通道数不需要像深层特征那么宽,我把P2相关路径的通道数从256降到128,显存节省明显,AP_small几乎没有损失。
训练时间方面,P2层前向和反向的计算开销都不小,同样的epoch数量下训练时长比基线增加三成以上。如果只是想验证方案可行性,我的建议是先用小输入尺度(比如512或640)做一个短周期的训练,确认指标趋势正确后再切到目标分辨率做完整训练。这种"先小后大"的验证策略能省下大量排队等GPU的时间。
5.3 小目标漏检不降反升的诊断流程
这个情况在论坛上经常被问到:明明加了P2层,小目标效果为什么反而变差?我自己也经历过,在这里梳理一个排查顺序,希望对大家有帮助。
第一步查标签分配。先确认小目标在所有尺度上的正样本是否真的增多了,如果只是加了P2层而标签分配逻辑没有改动,新增的正样本几乎没有,P2层的收益自然体现不出来。第二步查NMS配置。P2层输出的框数量远多于三尺度版本,默认的NMS阈值可能过于激进,大量小目标置信度稍低的框被抑制掉了。先降低conf_thres观察召回率是否回升,再决定是否调整NMS的IoU阈值。第三步查数据增强。如果启用了高概率的马赛克增强,超小目标被切割成更小的碎片,P2层也弥补不了信息丢失。这一步排查往往能解释为什么验证集不如预期。
排查的原则很简单:先确认"模型有没有可能学到",再确认"预测出来了但被后续处理丢掉",最后再确认"数据本身有没有提供足够的样本"。按这个顺序走下来,大多数问题都能定位到具体的环节,而不是盲目调参。
6. 一些个人经验和建议
整套方案从设计到落地,我个人的最大体会是:P2层不是面向所有任务的神器,它是针对特定问题的精准工具。如果你的数据集目标尺寸普遍在30像素以上,加P2层纯粹是增加计算量,收益非常有限;但如果你面对的是无人机视角、遥感图斑、工业微缺陷这类目标普遍在10到20像素的任务,P2层的空间分辨率优势是其他优化手段难以替代的。
给打算复现这套方案的朋友一个建议:不要一上来就全套加上,先跑通P2加标签分配的改动,确认AP_small的提升在预期范围,再用HSFF去消除噪声。分步验证的好处是,每一步的收益和代价都清晰可见,不会在混合变量里迷失方向。我最初就是直接把四尺度和融合一起加上,结果出了问题都说不清楚是哪个环节带来的。
最后再分享一个小技巧。如果显存实在不够,又不想放弃P2层的收益,可以尝试只在高分辨率输入下启用P2分支,推理阶段根据输入尺寸动态决定是否走P2路径。这个思路相当于把分辨率作为P2层的开关,灵活性高很多。后续如果条件允许,我打算在这个方向上继续探索基于尺度感知的动态路由方案,争取在算力开销和精度收益之间找到更优的平衡点。