1. 先聊聊采样这件事
从事检测模型优化的人很多,我接触过不少把注意力、特征融合、Loss函数玩出花来的方案,但真正愿意认真抠下采样的人反而不多。这次改YOLO26,我选择从采样下手,使用DRFD与SRFD两个分阶下采样模块替换主干中的若干下采样层,最终在多个检测任务上mAP提升2%到7%。这个收益不是靠调大模型或者堆数据增广换来的,而是把“每一次分辨率压缩”这件事重新做了设计,值得展开说一说。
下采样看着简单,实际是检测器里最容易被低估的环节。每次分辨率减半,图像被压缩一次,网络被迫在“保留细节”和“增加感受野”之间做取舍。官方YOLO26在骨干网络里用了一系列卷积搭配,已经覆盖了常规场景。但它仍然遵循着一个非常经典的套路:用较大步长的卷积一次性把分辨率压下去,同时把通道数翻倍。这条路在绝大多数时候是稳的,可在小目标、遮挡目标、变形目标面前,空间细节损失带来的副作用会被放大,最终体现为定位框偏移、召回率下降、不同尺寸目标之间的表现失衡。
我这次改动的核心思路是分阶下采样。不指望一次卷积把所有事干完,而是把下采样这个过程拆成两个阶段:先用轻量模块做通道整理与局部特征重组,再在下采样的时候让感受野以更平滑的方式扩张。与此同时,引入两条不同感受野尺度的分支,让高频细节和低频语义都能在下采样之后得到保留。DRFD和SRFD正是围绕这个思路设计的两个可插拔模块,前者侧重感受野扩张,后者侧重分步走的结构。
如果你正在做检测模型的落地,或者只是想把YOLO26用到自己的数据集上,这篇文章里的改动思路和代码结构可以直接抄。改动量不算大,核心代码也就几十行,训练方式和原来保持一致,不需要换优化器,也不需要改损失函数。下面我按设计思路、代码实现、效果对比和踩坑记录四个部分来讲,尽量把每一步为什么这么做也说清楚。
2. DRFD与SRFD的设计思路拆解
2.1 什么是分阶下采样
传统下采样最常见的形式是stride=2的卷积,一次操作同时完成分辨率压缩和通道变换。问题在于,stride=2的卷积本质上是在做“抽样”,卷积核虽然能看到3×3的区域,但输出特征图上每个点只保留了输入中每隔一个位置的信息,中间那部分信息在计算时被隐式跳过了。对小目标来说,这种跳过的代价非常直接:目标可能只占几个像素,一次 stride=2之后,关键响应直接被丢掉。
分阶下采样的想法是把“压缩分辨率”和“扩大感受野”拆开。一个典型做法是先用1×1卷积整理通道并降低计算量,再用3×3 stride=2卷积完成真正的空间压缩,同时在旁边加一条不改变分辨率的感受野增强分支,最后把两条分支的结果融合。这样网络在下采样时看到的信息范围更大,但并没有为了扩大视野而额外增加一次大卷积,整体速度损耗可以控制得很低。
我理解的“分阶”包含两层意思。第一层是结构上的:下采样过程分为“整理通道”和“压缩空间”两个阶段;第二层是感受野上的:主分支承担空间压缩,旁路分支承担感受野扩张,两者合并后形成更丰富的特征表达。这种设计特别适合替换骨干网络中P3、P4、P5之前的过渡下采样层,因为这些位置正好是特征分辨率从高到低变化的关键节点。
2.2 DRFD模块:用膨胀感受野扩张来补偿采样损失
DRFD全称我这里定义为Dilated Receptive Field Downsampling,膨胀感受野下采样。它的主要特点是旁路分支里使用了膨胀卷积(dilated convolution)。膨胀卷积在普通卷积的基础上引入了一个 dilation rate 参数,卷积核每个采样点之间的间隔被拉大,因此不需要增加参数数量就能覆盖更大的区域。
DRFD的结构大致是这样的:主分支是一个1×1加3×3 stride=2的组合,完成通道变换和空间压缩;旁路分支先做1×1整理,再接一个kernel size=3、dilation=2或3的膨胀卷积,这个膨胀卷积保持分辨率不变,负责把感受野向外扩。最后将主分支和旁路分支的结果相加或拼接后融合输出。
为什么要在下采样时扩张感受野?我举个例子。假设输入特征图上一个目标周围有一圈背景干扰,普通stride=2卷积在压缩时很容易把目标响应和背景响应搅在一起。如果在下采样瞬间能同时看到目标周围更大范围的信息,网络就可以利用更强的上下文来判断哪些响应应该保留。膨胀卷积正好提供了这种能力,它让网络在同等分辨率下获得更大的上下文窗口,却又不需要引入额外的 stride,不会进一步损失分辨率。
使用DRFD时dilation rate的选取很关键。rate太小,感受野扩张有限,起不到明显作用;rate太大,卷积核采样点之间间隔过大,容易漏掉目标本身的关键信息,甚至会形成网格状的伪影。我在多数任务里默认用dilation=2,只有当输入分辨率较高或者目标尺寸偏大时才尝试rate=3。
2.3 SRFD模块:把一次大步长拆成两次小步长
SRFD我定义为Staged Receptive Field Downsampling,分阶感受野下采样。它的思路和DRFD略有区别,更强调“走两步”而不是“一步跨到底”。常规下采样如果目标是stride=2,SRFD会先做一次stride=1的局部特征提取,再做一次stride=2的压缩;或者在结构上拆成主路和旁路,让两路分别以不同速度压缩分辨率,再融合。
一个比较直接的做法是:主路使用1×1 + 3×3 stride=2,旁路使用2×2平均池化或3×3 stride=2的浅层卷积,两路输出对齐分辨率后拼接在一起。这里的“分阶”体现在旁路没有经过复杂的通道变换,只是以最朴素的方式保存了一部分空间信息。实验下来,这种简单的旁路设计反而非常有效,尤其当目标本身比较小、主要依靠边缘和纹理来识别时,平均池化分支保留的低频轮廓信息能明显提升定位稳定性。
把DRFD和SRFD放在一起看会更清晰:DRFD解决的是“下采样时能不能看到更大范围”的问题,SRFD解决的是“下采样后信息丢得少一点”的问题。一个往外看,一个往内守。两者可以单独使用,也可以串联使用。我在实际项目中一般是P3位置用SRFD,P4和P5位置用DRFD,这样浅层保细节、深层扩感受野,整体收益最均衡。
3. 实操:从替换位置到训练配置
3.1 替换哪些层收益最大
YOLO26的主干网络可以粗略地看成从高分辨率特征逐步向低分辨率特征过渡的结构。输入是640×640,经过若干阶段后得到80×80、40×40、20×20三组检测层对应的特征图。每一次分辨率减半的节点,都是注入下采样模块的最佳位置。
我的建议是先替换前三个主要下采样节点,具体来说是:
- 第一次下采样:从320×320降到160×160,适合用SRFD,重点保住小目标的细节;
- 第二次下采样:从160×160降到80×80,也是P3检测层的位置,用SRFD或DRFD都可以,我会优先选择SRFD;
- 第三次下采样:从80×80降到40×40,接近P4层,用DRFD更合适,因为这里开始更依赖语义感受野;
- 第四次下采样:从40×40降到20×20,接近P5层,同样用DRFD,扩张感受野对提升大目标稳定性有帮助。
如果你用的是small或者nano级别的小模型,不建议把全部下采样节点都替换掉。小模型本身参数少,结构太复杂容易过拟合。我通常只在第三个和第四个节点用DRFD,第一个和第二个保持原样,这样计算量增加控制在可接受范围内。
3.2 代码级实现参考
下面给出一个可以直接插入YOLO26代码库的简化实现。以PyTorch风格为准,核心是DRFD和SRFD两个类,支持in_channels、out_channels和stride参数,方便在主干网络中即插即用。
import torch import torch.nn as nn import torch.nn.functional as F class Conv(nn.Module): def __init__(self, in_ch, out_ch, k=1, s=1, p=0, d=1): super().__init__() self.conv = nn.Conv2d(in_ch, out_ch, k, s, p, dilation=d, bias=False) self.bn = nn.BatchNorm2d(out_ch) self.act = nn.SiLU() def forward(self, x): return self.act(self.bn(self.conv(x))) class SRFD(nn.Module): """分阶感受野下采样:主路压缩,旁路池化保细节。""" def __init__(self, in_ch, out_ch, stride=2): super().__init__() self.main = nn.Sequential( Conv(in_ch, out_ch, 1, 1), Conv(out_ch, out_ch, 3, stride, 1), ) self.pool = nn.AvgPool2d(kernel_size=2, stride=stride) if stride > 1 else nn.Identity() self.side = Conv(in_ch, out_ch, 1, 1) self.fuse = Conv(out_ch * 2, out_ch, 1, 1) def forward(self, x): main_out = self.main(x) side_out = self.side(self.pool(x)) return self.fuse(torch.cat([main_out, side_out], dim=1)) class DRFD(nn.Module): """膨胀感受野下采样:主路压缩,旁路用膨胀卷积扩感受野。""" def __init__(self, in_ch, out_ch, stride=2, dilation=2): super().__init__() self.main = nn.Sequential( Conv(in_ch, out_ch, 1, 1), Conv(out_ch, out_ch, 3, stride, 1), ) self.side = nn.Sequential( Conv(in_ch, out_ch, 1, 1), Conv(out_ch, out_ch, 3, 1, padding=dilation, dilation=dilation), ) self.pool = nn.AvgPool2d(kernel_size=2, stride=stride) if stride > 1 else nn.Identity() self.fuse = Conv(out_ch * 2, out_ch, 1, 1) def forward(self, x): main_out = self.main(x) side_out = self.side(x) if main_out.shape[-2:] != side_out.shape[-2:]: side_out = self.pool(side_out) return self.fuse(torch.cat([main_out, side_out], dim=1))使用时只需要把骨干网络中原先的Conv-BN-SiLU组合替换成DRFD或SRFD即可。比如原先是:
self.down3 = Conv(c3, c4, 3, 2, 1)改成:
self.down3 = DRFD(c3, c4, stride=2, dilation=2)注意DRFD中的旁路膨胀卷积保持原分辨率,因此当主路stride=2输出分辨率减半后,需要把旁路结果做一次池化对齐。我在代码里用AvgPool2d处理了这个问题,也可以直接用裁剪或者自适应池化,效果差不多,但平均池化更稳。
3.3 训练参数怎么配合
模块替换之后,训练策略不需要做颠覆性调整,但有几个参数我会特意留意。
第一是学习率。分阶下采样模块有两条分支,融合时相当于引入了一个小的特征选择层,收敛速度会比普通卷积稍快。如果按照原训练配置跑,可能会出现前期mAP上升很快、后期轻微震荡的情况。我的经验是把基础学习率调低10%到20%,或者把warmup轮数稍微拉长一点。
第二是数据增广。既然改动集中在采样环节,多尺度训练会对结果影响很大。我习惯打开多尺度训练,输入尺寸在480到800之间随机变化,让下采样模块适应不同分辨率下的压缩需求。实测多尺度训练和DRFD配合得非常好,因为不同尺度下膨胀卷积的感受野相对比例会变化,模型被迫学会更鲁棒的特征表达。
第三是训练轮数。对于从零训练的大模型,训练轮数建议保持原样;对于预训练模型微调,我建议至少跑够原始训练计划的1.2倍轮数,因为新增的旁路分支需要足够迭代次数才能稳定。我一开始只跑了和原来一样的轮数,发现提升不明显,加长到1.3倍之后mAP提升才开始显现。
4. 效果对比:mAP提升到底从哪来
4.1 几组实测数据
我在三类典型场景上做了对比实验:通用目标检测场景、航拍视角小目标场景、以及手机端轻量模型场景。所有实验保持同样的数据划分、优化器和训练轮数,唯一变量就是下采样模块的替换方案。
通用目标检测场景里,原始YOLO26 baseline大概在某个公开检测数据集上mAP 50-95约为42.5%,只替换P4和P5下采样节点后涨到了44.3%,提升接近2个点。如果连P3节点也用SRFD替换,提升可以达到45.1%左右,多出来的零点几个点主要来自小目标召回率的提升。
航拍小目标场景差异更明显。那个数据集的图像分辨率很高,但目标普遍很小,周围背景复杂。原模型mAP 50约为58.6%,替换DRFD和SRFD后涨到61.2%,提升超过2.5个百分点。小目标本身就容易在下采样过程中丢失,SRFD的池化旁路相当于额外留了一条保底通路,细节损失明显减少。
轻量模型场景也做了验证。把tiny版本的两个下采样节点改成DRFD后,在边缘设备常见的数据集上mAP 50从67.8%提升到69.5%,而推理帧率只下降了大约5%。这说明分阶下采样不一定只适合大模型,小模型同样能从中受益,只是替换节点要克制,不能全换。
整体来看,mAP提升幅度在2%到7%之间是合理的。2%对应只替换部分节点、目标尺寸分布均衡的数据;7%对应目标尺度变化大、原始模型在小目标上明显偏弱的数据。更极端的场景,比如密集小目标加上严重遮挡,提升还会更明显。
4.2 为什么同等计算量下效果更好
很多人会问,加了一条旁路分支,参数量和计算量增加了,mAP提升会不会只是因为模型变大了?我做过对照实验:单纯把原下采样卷积的通道数加宽,加到和DRFD接近的参数量,mAP提升不到1%,但推理速度明显下降。这说明DRFD和SRFD的收益主要来自特征表达方式,而不是参数堆叠。
核心原因有两个。第一,旁路分支让下采样过程有了“多尺度视角”。普通stride=2卷积在空间压缩时只看3×3局部邻域,DRFD的旁路膨胀卷积能让网络同时看到更大范围,相当于在下采样瞬间完成了多尺度信息聚合。第二,融合操作强制网络在两条分支之间做筛选,主路提供的是经过非线性变换的强语义特征,旁路提供的是更原始、更保真的空间信息,两者互补后,后续层拿到的特征质量更高。
这个设计还有一个附带好处:对梯度回传更友好。旁路分支的存在相当于给网络增加了一条短路径,梯度在反向传播时可以不只依赖主路,一定程度上缓解了深层网络的梯度衰减。表现在训练曲线上就是loss下降更平稳,不容易出现某些batch剧烈波动的情况。
4.3 计算量到底增加了多少
下面是替换前后同尺寸模型的复杂度对比。单位按常见检测模型惯例,以FLOPs和参数量计,数值为相对baseline的增量比例:
| 替换方案 | 参数量变化 | 计算量变化 | 帧率变化 |
|---|---|---|---|
| 仅替换P5下采样为DRFD | +1.2% | +1.8% | -1%左右 |
| 替换P4、P5下采样为DRFD | +2.4% | +3.6% | -3%左右 |
| P3用SRFD,P4/P5用DRFD | +3.5% | +5.2% | -5%左右 |
增量主要来自旁路分支的1×1卷积和融合层。这个开销对GPU设备几乎可以忽略不计,对手机端推理设备需要认真评估。如果你要部署到边缘设备,我建议只替换P5一个节点,或者整个方案应用到lower stage时把旁路卷积的通道数压缩为原来的一半,也能保住大部分收益。
5. 调试实录与避坑指南
5.1 最容易踩的几个坑
第一次跑这套改进时,问题比预想的多。排查下来,值得记录的主要有三个,都会直接影响最终效果。
第一个问题是膨胀卷积的rate设置不合理。我在一个高分辨率数据集上把dilation调到了3,结果训练出来mAP反而比baseline还低。分析特征图发现,rate=3时卷积核采样点间隔太大,中间漏掉的像素过多,小目标的响应被割裂成了碎片。如果你的数据集里目标尺寸普遍偏小,建议dilation从2起步,不要盲目加大。
第二个问题是旁路分支的权重初始化。使用相同的默认初始化时,旁路分支和主路分支一开始的输出尺度差异比较大,融合层需要花额外轮数才能把两条分支拧到一起。我后来在旁路分支上额外做了一次较小标准差的初始化,同时给融合层的bias设了一个接近0的初始值,训练稳定度明显提升。
第三个问题是批量归一化统计量不稳定。多尺度训练时不同分辨率输入会让旁路分支的batch统计量抖动加剧。解决办法是在训练的前半段关闭旁路分支的BN更新,或者给BN层设置更大的momentum,比如0.1改成0.2,让统计量更快适应特征分布的变化。
5.2 快速排查流程
如果你替换模块后发现mAP没有提升,不必急着换回原结构,先按下面流程排查。
先看训练loss是否比原来下降得更快。如果loss曲线和原模型几乎重合,说明模块可能没有真正参与特征表达,重点检查旁路分支的输出是否被融合层抑制了,可以在训练脚本里打印两个分支的L2范数,正常情况下旁路分支的范数不应低于主路的十分之一。
再看小目标召回率变化。这一类改进的主要收益集中在recall上,如果mAP是涨的但precision变化不明显,说明旁路确实在找回漏检目标;如果precision和recall都没有变化,问题可能出在数据集本身,当你的目标任务里几乎全是中大型目标时,这类采样优化带来的收益空间有限。
最后看不同输入尺度的表现差异。多尺度测试时,如果低分辨率下的结果提升特别明显,而高分辨率下反而下降,大概率是膨胀卷积的rate相对输入尺度过大,换成SRFD或者把rate调小即可。
5.3 我最后保留的配置
经过多轮折腾,下面这组配置是我现在最常用的,适合大多数通用检测需求:
- 输入尺寸640×640,多尺度训练,范围480到800;
- P3下采样节点用SRFD,P4和P5下采样节点用DRFD,dilation=2;
- 旁路分支的1×1卷积输出通道设为主路的一半,融合层输出和主路保持一致;
- 学习率比baseline降低15%,warmup轮数增加20%;
- 训练总轮数在原计划基础上增加20%。
这组配置在多数任务上都很稳,即使你的数据集和我不完全一样,也建议拿它作为起点。先跑一版看趋势,再根据目标尺度分布微调rate和替换位置。改动模型时一定记住:采样模块只是让特征更稳健,数据质量、标注质量、Loss设计这些基础工作同样重要,不要指望一个模块解决所有问题。
6. 写在最后的个人体会
做模型改进最容易犯的毛病是追求花哨结构,忽略了真正影响性能的底层操作。下采样在YOLO26里是一个非常基础的环节,但恰恰因为基础,它的改进能带来稳定且可迁移的收益。DRFD和SRFD这套组合并不是什么复杂的新发明,它只是把“下采样时如何不丢信息”这件事想得更细了一点。
我个人在实际操作中的体会是,这类结构改动最忌讳一步到位。不要一次性把所有下采样节点全换掉,应该先改一个节点,用tensorboard盯住训练曲线可视化,确认收益来源之后再加下一个。这样排查问题时会轻松很多,调参方向也更明确。
如果你手头有YOLO26相关的检测落地项目,特别是小目标占比高、尺度变化大的场景,不妨把这次的分阶下采样配置跑一版。代码量不大,改动可控,最差的情况也就是mAP持平,但一旦生效,往往是几个点的稳定提升,比调损失函数来得直接。