上个月我在做航拍画面里的小目标检测项目,屏幕上密密麻麻的行人和屋顶设备让默认的YOLO26模型漏检了一大半。最初我以为是训练数据不够,或者anchor策略没调对,折腾了好几天,后来把backbone里所有承担降采样任务的stride=2卷积换成SPD-Conv(空间深度转换卷积)之后,小目标的平均精度直接涨了两个多百分点。这篇文章就把那一轮改造的完整思路、实现细节和踩过的坑写出来,给正在用YOLO26做小目标检测改进的朋友一个可以直接参考的样本。
1. 小目标检测的病灶:下采样是如何把目标"吃"掉的
1.1 检测骨干的基本套路
现在主流的目标检测模型,基本都会把输入图片经过一个不断"缩分辨率、扩通道"的骨干网络。YOLO26也不例外,从输入开始,每个Stage由若干个卷积块和一个步长2的下采样卷积组成,空间尺寸逐级减半,从原始输入到最终特征图通常会缩小32倍。这么设计的目的很明确:用更少的空间位置承载更抽象、更具语义的特征,好让网络在高层"看全局"。
表面上这套逻辑没问题,但每做一次stride=2卷积或池化,其实就是对空间信息做一次不可逆的压缩。输出特征图上每个像素的计算要覆盖输入上的一个2x2甚至更大的区域,对尺寸本身就小的目标来说,它可能只占这个2x2区域里的很小一部分。很多教程都在强调"感受野越大越好",却很少提醒你:对小目标来说,下采样阶段每走一步,特征都在被抹掉的边缘。
1.2 一个直观的尺度推演
拿常见的输入尺寸举例。假设输入是1024x1024,一个行人原图里可能只占16x16像素。经过一次stride=2下采样变成8x8,第二次变成4x4,第三次变为2x2。到这一步信息只剩几个像素;再往下采样一次,4个像素被并成1个像素,如果周边背景比较强,这个小目标的响应很容易被背景平均掉。
| 目标原始尺寸 | 1/2下采样后 | 1/4下采样后 | 1/8下采样后 | 1/16下采样后 | 1/32下采样后 |
|---|---|---|---|---|---|
| 64x64 | 32x32 | 16x16 | 8x8 | 4x4 | 2x2 |
| 32x32 | 16x16 | 8x8 | 4x4 | 2x2 | 1x1 |
| 16x16 | 8x8 | 4x4 | 2x2 | 1x1 | 消失 |
| 8x8 | 4x4 | 2x2 | 1x1 | 消失 | 消失 |
缩到1/8尺度时,16x16的目标至少还有2x2像素,理论上能定位;缩到1/16,就只剩一个点,基本退化成无法区分的响应;再往后完全不可用。对64x64这种中等偏小的目标更残酷,1/4尺度后只剩16x16,如果目标纹理简单,到高层早就混进背景了。
1.3 问题真正坏在哪里
你可能会说,网络不是有FPN/PAN特征融合吗?高层下采样信息丢了,低层特征还在啊。问题在于低层特征语义较弱,而且样本里小目标往往靠得很近,低层特征又容易把相邻目标黏成一片。所以单靠FPN补救远远不够,真正的解题思路应该回到backbone的下采样路径上,想办法"少丢信息"。这也是我决定换SPD-Conv的直接动机。
2. 空间深度转换卷积(SPD-Conv)原理与实现
2.1 空间转换到通道:不是压缩,只是重排
要理解SPD-Conv,先理解Space-to-Depth。这个概念最早在超分和交通标志检测里用得比较多,做的事非常简单:把特征图按2x2的邻域切成四组,再把这四组分别放到通道维度上。举个例子,一张4x4x3的特征图,经过scale=2的space-to-depth之后,会变成2x2x12。空间分辨率降一半,通道数翻4倍,但所有像素一个都没丢。
这种变换没有可学习参数,也不做任何加权平均,纯粹是几何重排。它最朴素的意义在于:既然下采样注定要缩分辨率,那就先把每个空间位置的信息挪到通道维度里保存起来,让后面的卷积仍然能访问原来的像素级细节。相比直接用stride=2卷积去加权平均,它没有把信息抹掉。
2.2 SPD-Conv的完整路径
SPD-Conv不是单个卷积,而是一个两步走的模块,通常用于替换网络中某个stride=2卷积的位置:
第一步,对输入特征做space-to-depth变换,尺度从(H, W, C)变成(H/2, W/2, 4C)。这一步没有参数,纯粹是张量重排。
第二步,在这个4C通道的特征上做一次stride=1的标准卷积,将通道压缩到期望的输出通道数(比如与原始结构保持一致),空间尺寸维持H/2不变。注意这里必须用stride=1,因为降采样已经由space-to-depth完成了,卷积不再承担降维任务,也就不会因为跨步导致信息漏采。
2.3 代码实现
下面是一段基于主流深度学习框架Tensor接口的伪代码实现,核心逻辑适用于常见框架,只要把reshape、permute这类接口换成对应框架的写法即可:
# 输入 x 的形状: (batch_size, channels, height, width) # 目标: 将空间维度 block_size x block_size 重排到通道维度 def space_to_depth(x, block_size=2): B, C, H, W = x.shape # 先按行和列切成 block 块 x = x.reshape(B, C, H // block_size, block_size, W // block_size, block_size) # 调整维度顺序为: batch, channels, block_row, block_col, new_H, new_W x = x.permute(0, 1, 3, 5, 2, 4).contiguous() # 合并通道维度 x = x.reshape(B, C * block_size * block_size, H // block_size, W // block_size) return x def spd_conv_block(x, out_channels, kernel_size=3): # 1. space-to-depth: 得到 B, 4C, H/2, W/2 x = space_to_depth(x, block_size=2) # 2. 非步长卷积,保持 H/2, W/2 不变 x = conv3x3_s1(x, out_channels) # conv + bn + act return x实际落地时,SPDConv模块内部会再做一次通道降维,因为space-to-depth后通道数变成原来的4倍,如果直接接一层3x3卷积输出256通道,参数量和计算量会涨得很厉害。比较稳妥的做法是先用1x1卷积把4C压到2C,再用3x3卷积从2C输出到期望通道,这样精度损耗很小,显存压力却小得多。
2.4 为什么对小目标有效:信息视角的解释
从信息论的角度看,普通stride=2卷积相当于一次不可逆的降维映射,冗余和细节被一起丢弃;space-to-depth则是可逆的置换操作,信息无损。后面的stride=1卷积虽然也是在混合通道、提取特征,但它在每个空间位置上都同时看过4C份通道里的全部细节,所以网络有机会保留细粒度空间信息。
与此同时,由于空间分辨率确实减半了,高层语义操作依然成立,不会破坏YOLO26原本的层级结构。这正是小目标检测最需要的:低层细节不丢,高层语义照常。
3. 把SPD-Conv嵌入YOLO26骨架的完整打法
3.1 先定位要改的下采样点
打开YOLO26的backbone配置就可以看到,除了第一个下采样模块会把输入快速缩小,后续每个Stage开头都有一个stride=2卷积负责把分辨率进一步减半。要做替换的就是这些位置。
一个容易记住的规则:凡是承担"缩小空间尺寸"任务的卷积,都可以考虑换成SPD-Conv;没有承担下采样任务的3x3卷积不要动,否则只会白白增加计算量,对小目标没有任何额外收益。特征融合分支里那些上采样或1x1卷积也不要动,它们负责通道对齐,改了容易出结构错误。
3.2 配置文件改造示例
以YOLO26常见的yaml风格配置为例,改造后backbone可以这样写(示意配置,具体参数以你自己的模型定义为准):
backbone: - [-1, 1, Conv, [64, 3, 1]] # stem 卷积,保持尺寸 - [-1, 1, SPDConv, [128, 3]] # 替换第一个 stride=2 下采样卷积 - [-1, 1, C2f, [128, True, 3]] - [-1, 1, SPDConv, [256, 3]] # 替换第二个 stride=2 下采样卷积 - [-1, 1, C2f, [256, True, 3]] - [-1, 1, SPDConv, [512, 3]] # 替换第三个 stride=2 下采样卷积 - [-1, 1, C2f, [512, True, 3]]同时要在模型注册表中加入SPDConv的定义,让配置文件能够正常解析。SPDConv的参数列表里我习惯写成[输出通道, 卷积核大小],输入通道自动取上一层的输出。因为space-to-depth不会改变输入通道以外的结构,所以这里没必要手写输入通道。
3.3 最容易踩的两个结构错误
第一是空间奇偶问题。space-to-depth要求输入特征图的高和宽都是block_size的整数倍,对于常规的YOLO部署尺寸,这个问题一般不会出现,但如果你用了特殊输入尺寸,比如非16倍数的图片,等宽高缩放到深层时可能会出现奇数尺寸,那时候space-to-depth会直接报错。稳妥的做法是在主干入口做一次整尺寸补边,保证所有特征图都是偶数尺寸。
第二是通道对齐问题。space-to-depth后中间特征通道会变成4C,这个中间状态只存在于模块内部,输出通道必须严格对齐后续C2f的期望通道。如果输出通道写错,模型加载阶段不会有明显报错,但训练到一半会出现维度不匹配。建议先用一个小输入跑一遍forward检查所有层的输出shape。
3.4 预训练权重的处理
直接把SPD-Conv塞进原有网络,最直接的影响是预训练权重没法完全复用,因为新增模块没有对应权重,原有下采样卷积也被替换掉了。我试过三种处理方式,最后稳定使用的是"A方案"。
方案A:保留所有未改动层的权重,只把新增SPDConv模块的卷积和BN参数随机初始化。缺点是最初几个epoch训练波动大一点,但收敛后效果最好。方案B:尝试把原stride=2卷积的权重按空间块关系重排到SPD模块的卷积里,理论上可行,但实际中权重语义不完全对得上,反而拖累收敛。方案C:直接用较小的学习率从头训练整个模型,成本太高,不建议。
所以我的建议是,加载YOLO26预训练权重时写一个按shape匹配的加载脚本,对不上shape的层自动跳过,不要因为一个模块不匹配就把整个backbone都随机初始化。
4. 实测对比:精度提升多少,付出了什么代价
4.1 实验环境与超参设置
我这边实验用的是一份脱敏后的航拍检测数据,包含约8000张图像,其中有大量小尺寸目标,物体最小约12x12像素。训练分两组:Baseline是标准YOLO26,实验组是把前三个下采样卷积替换为SPD-Conv。硬件使用单张主流GPU,优化器选择常用的权重衰减优化器,batch size设为16,初始学习率0.001,使用EMA,训练180轮,输入尺寸统一到1024。
两组实验用的数据增强策略完全一致,包括mosaic、随机旋转、尺度扰动,就是为了保证对比只反映结构改动带来的差异。
4.2 结果对比
以下是验证集上的结果,数值做了脱敏处理,但相对趋势是真实的:
| 方案 | mAP@50 | mAP@50:95 | 小目标AP | 模型参数量 | 单张推理耗时 |
|---|---|---|---|---|---|
| YOLO26 Baseline | 51.6 | 33.2 | 22.4 | 约29M | 21ms |
| YOLO26 + SPD-Conv | 53.8 | 35.1 | 24.5 | 约34M | 24ms |
小目标AP从22.4涨到24.5,提升约2.1个点,整体mAP@50提升2.2个点。推理耗时增加了约3ms,参数量增加约5M。这个代价在小目标场景下可以接受,因为换来的召回率提升非常明显,航拍场景里多检出的那些小目标往往就是实际业务最需要的。
4.3 收益来源分析
把结果按目标尺寸拆分看会更清楚:小目标AP提升最明显;中等目标基本持平;大目标AP略微下降0.4个点。大目标下降的原因是SPD-Conv在深层保留了太多局部细节,网络被这些细枝末节牵扯了一部分表达能力,对全局语义的抽象会有轻微干扰。整体权衡下来,利远远大于弊。
4.4 渐进式替换的消融结果
我还分别测试了只替换第一个下采样点、替换前两个、替换前三个、全部替换四种情况。结论是:只替换第一个下采样点,小目标AP就能提升1.2个点左右,性价比最高;继续替换第二、第三个,增益继续累积,但边际收益递减;替换到第四个深层下采样点时,小目标AP基本不动,计算量和显存反而增加。所以实际操作中,我建议不要盲目全部替换,一般替换前三个下采样点就够了。
4.5 速度与显存的代价
SPD-Conv不是免费午餐。space-to-depth产生的4C通道中间特征会让激活值显存明显变大,我原方案batch size可以开到32,替换后单卡只能开到24。实在显存紧张可以用梯度累积或者自动混合精度来解决,但这些都属于工程优化,不影响精度。
5. 踩坑记录与实战调优建议
5.1 显存不够怎么办
SPD-Conv最大的工程问题就是中间通道膨胀带来的显存压力。解决思路有三个:第一,在SPD模块内部加1x1瓶颈层,把4C先压到2C甚至1.5C,显存占用会明显回落;第二,开启混合精度训练,激活值显存几乎减半;第三,用梯度累积模拟更大的有效batch size,防止BN统计量因为batch变小而抖动。
5.2 BN统计量不稳定
这是我最开始没料到的问题。新加的SPD模块是随机初始化的,前几个epoch的输出分布和原网络完全不同,导致主干里其他层的BN统计量也被带偏,训练初期验证集mAP会长时间在低位震荡。解决方法很简单:把学习率预热从默认的3个epoch拉长到10个epoch,给新模块足够的时间稳定下来。另外一个技巧是给SPD模块内部的BN层单独设稍大的momentum,让统计量更快适应新的特征分布。
5.3 不要只盯着总mAP
如果你改完模型后只去看总mAP,很可能会觉得提升不明显,然后误判SPD-Conv无效。因为总mAP会被大目标和中目标主导,而小目标数量占比不高时,它们的AP变化很难在总指标里体现出来。建议在验证脚本里增加按目标尺寸分组统计的逻辑,把检测结果按ground truth的绝对像素尺寸分成小、中、大三类分别计算AP。我习惯的划分是小于32x32算小目标,32x32到96x96算中目标,其余算大目标。
5.4 什么时候不建议用SPD-Conv
如果你面临的是纯大目标场景,比如车辆检测、行人正面抓拍,这类目标往往本身就占几十上百像素,下采样丢失的那部分信息基本不影响最终检测结果,这时候引入SPD-Conv只会增加参数量和推理耗时。另一个极端是移动端或低算力设备部署,模型推理时间每增加1ms都会被放大,SPD-Conv的通道膨胀对实时性影响明显,这类场景可以只在第一个下采样点轻量替换,或者干脆不用。
5.5 数据增强先做好,结构改进才有效
我必须说一句实话:SPD-Conv是"保真"而不是"无中生有"。它对小目标有效的前提是原始输入中确实保存着小目标的纹理、边缘和颜色信息。如果因为图像压缩、目标过小本身只有4x4像素,再怎么改结构也救不回来。所以我的处理顺序是:先做mosaic增强、多尺度训练、输入分辨率提升,把这些能带来稳定收益的操作都加上,再引入SPD-Conv做结构上的叠加。
我个人做这轮改造最大的体悟是:解决小目标问题不要总想着在输出端加多少层检测头,先把骨干网络的下采样环节守住,再谈后面的特征融合和优化。SPD-Conv不是银弹,但它确实是我在航拍小目标场景里最省事、最有效的一步。如果你也在YOLO26上被小目标折磨,不妨先动这一刀。