YOLO26的目标检测框架迭代到这个阶段,常规的卷积替换、注意力模块基本都被大家试过一轮了,再想往上涨点确实难。真正拉开差距的往往不是某个花哨机制,而是感受野的组织方式。这次我把RFB(Receptive Field Block,感受野块)引入YOLO26的卷积模块,目的很明确:在不大幅增加参数量的前提下,把多尺度特征捕获能力提上去,尤其是小目标漏检问题。这篇文章不聊虚的,直接从结构拆解、嵌入位置、参数量计算、训练调参到踩坑排查,完整记录一遍。
适合谁看:正在做YOLO系列改进、跑检测竞赛、搞毕设课题的同学。如果你已经被“小目标AP_s上不去”卡了很久,这篇值得认真读完。
1. 为什么要给YOLO26引入RFB感受野块:小目标漏检的瓶颈分析
1.1 多尺度检测做了,小目标还是漏,问题出在哪
YOLO26的检测头本身是分P3、P4、P5三个尺度输出的,按理说小目标交给高分辨率特征图那条路就够了。但实际操作中你会发现,AP_s(小目标平均精度)往往是整体mAP里最拖后腿的指标。问题不在检测头,而在背骨(Backbone)的感受野分配不合理。
深层的P5特征图下采样了32倍,理论感受野看着很大,但骨干网络实际建模出来的有效感受野远小于理论值,而且集中在目标中心附近。对小目标来说,它本身的像素覆盖范围小,周围上下文信息又少,深层特征传到浅层时已经衰减得差不多。FPN、PAN这些结构能做的事情是“融合已经提取到的特征”,它不能凭空变出更大范围的上下文语义。这就相当于几个侦察兵视野都只有50米,你把他们的报告汇总得再好,也看不到100米外的情况。
拿人眼做类比就更好理解:你要在小目标检测中判断“这个模糊的小点是船还是漂浮物”,近处看细节是不够的,得结合周围的波纹走向、航道环境、附近其他船只的分布才能下判断。这个“拉远看”的能力,正是YOLO26原本缺的。
1.2 RFB的工作原理:多分支卷积如何模拟视觉感知
RFB最早出现在一篇经典的SSD改进工作中,设计灵感来自人类视觉皮层的感受野机制:视觉皮层里不同神经元的感受野尺寸和偏心率成正比,中心区域感受野小,负责精细分辨;外围区域感受野大,负责捕捉上下文信息。RFB把这个规律翻译成了可计算的卷积结构。
具体做法是构造多个并行的卷积分支,每个分支先做1x1卷积降通道,再跟不同膨胀率(dilation rate)的3x3卷积,最后把所有分支的输出拼接起来,再用1x1卷积融合。空洞卷积的核心价值在于:不增加卷积核参数和计算负担的情况下,扩大感受野。一个普通的3x3卷积,等效感受野只有3x3;膨胀率为3的3x3卷积,等效感受野是7x7;膨胀率为5,等效感受野是11x11。公式很简单:
等效核大小 = dilation * (kernel_size - 1) + 1
所以同样是3x3卷积,一个模块里通过三四个分支就能同时覆盖1、3、7、11四种尺寸的感知范围,相当于同一时刻既有“近视眼”看纹理,又有“远视眼”看全局,最后把信息拼在一起。这就是多尺度特征捕获的底层原理。
1.3 RFB、ASPP、Inception三者怎么选
做结构改进时,很多人容易把RFB和另外两个经典多尺度模块搞混:ASPP(空洞空间金字塔池化)和Inception。它们的思路有交集,但适用场景差别很大。
| 模块 | 结构特点 | 参数量 | 感受野范围 | 适合场景 |
|---|---|---|---|---|
| Inception | 不同尺寸卷积核并行(1x1/3x3/5x5) | 较高,5x5卷积开销大 | 固定核尺寸,范围跳跃 | 分类网络中的多尺度特征提取 |
| ASPP | 多个并行空洞卷积,无1x1预处理 | 中等,缺少跨分支信息压缩 | 范围广,但上下文融合粗暴 | 语义分割中的上下文捕捉 |
| RFB | 1x1降通道 + 不同膨胀率3x3 + 1x1融合,可选残差 | 较低,参数利用率高 | 连续覆盖1/3/7/11感受野 | 检测网络中的小目标增强 |
实测下来,ASPP更偏“池化”思维,强调从不同尺度池化上下文;Inception靠不同尺寸卷积核硬堆,大卷积核参数增长太猛。RFB的巧妙之处在于用空洞卷积打了一个时间差:同样覆盖11x11的感受野,Inception可能需要一个5x5或7x7卷积,参数量和计算量都是RFB的几倍。对YOLO这种本身就要兼顾速度的检测器来说,RFB是性价比最高的那个。
2. RFB多分支卷积结构拆解:从参数计算到嵌入YOLO26的三种位置
2.1 我用的RFB变体结构细节
我这里采用的RFB变体结构如下:输入特征图先进四个并行分支,每个分支先用1x1卷积把通道压缩为原来的四分之一(reduce ratio r=4),再在部分分支后接不同膨胀率的3x3卷积,最后把四个分支的输出按通道拼接,做一次1x1融合,再接残差连接回主路径。
| 分支 | 具体结构 | 膨胀率 | padding | 等效感受野 |
|---|---|---|---|---|
| branch0 | 1x1卷积 | 1 | 0 | 1x1 |
| branch1 | 1x1卷积 + 3x3卷积 | 1 | 1 | 3x3 |
| branch2 | 1x1卷积 + 3x3卷积 | 3 | 3 | 7x7 |
| branch3 | 1x1卷积 + 3x3卷积 | 5 | 5 | 11x11 |
这里有两个容易被忽略的细节。第一个是为什么每个分支要先做1x1降通道:如果不降,四个分支的3x3空洞卷积都在原通道数上做,计算量直接翻3倍以上,对训练速度和显存都不友好。降到四分之一后,每个分支只负责“提出一种感受野下的特征假设”,最后由融合卷积统一整合,信息量并不会受太大影响。第二个是padding必须等于膨胀率,否则特征图尺寸对不上,输出就拼接不了。这个很多新手实现时会漏,结果就是尺寸不匹配报错。
shortcut残差连接在这类模块里不是可选项,是必须项。出现梯度消失、训练震荡时,你排查半天,最后发现就是缺了一条残差旁路。我的经验是:通道数一致就用恒等映射,不一致就加1x1卷积做投影,别省这个操作。
2.2 嵌入YOLO26的三种位置方案
RFB模块接在哪,直接决定了效果上限和计算开销。我试过三个位置,结论比较明确。
第一种,替换Backbone最后一个C2f模块(P5层之前)。这是最经典的做法,目的是扩大最深层特征图的感受野,让顶层语义信息“看得更远”。优点是实现简单、对整体推理速度影响小,缺点是它服务的是stride=32的特征图,对32像素以下的小目标帮助有限。
第二种,在Neck的PANet路径中,P4和P5之间额外插入一个RFB模块。这样做的好处是让大感受野的语义信息在自上而下融合过程中就参与进来,P3、P4这些浅层特征图也能“继承”深层视野,小目标直接受益。缺点是多一个RFB就是多一份计算量,推理帧率会掉。
第三种,在检测头之前再接一个RFB。这个位置能最大化利用感受野,但过于激进,容易把精细位置信息洗掉,表现往往是AP_s提升了一两个点,AP大目标反而掉了。我不建议一上来就做这个位置。
我推荐的接入策略是:先在P5层做单点替换,验证模块本身有没有效果;确认有效后再考虑P4+P5双点方案。别一上来就把三个位置全塞满,这样的话你连问题出在哪个位置都定位不了。如果训练框架是YAML配置文件定义网络的,就在对应层级的模块列表里把C2f换成RFB并指定输出通道,其他层的通道数不用改。
2.3 参数量与计算量实测
很多人在结构设计阶段就卡住了,担心RFB参数太大。我直接把计算过程写出来。
假设输入输出通道都是C=256,reduce ratio r=4,每个分支的中间通道数是64。四个分支里的1x1卷积参数量:256x64x4 = 65536。分支1、2、3里的3x3卷积参数量:64x64x9x3 = 110592。最后的1x1融合卷积参数量:256x256 = 65536。合计约24.2万参数。
对比来看,YOLO26基础版参数量大概在21.3M左右,单个RFB模块占整体的1%都不到。在640x640输入下,单个RFB模块增加的FLOPs大约0.1G到0.2G量级,训练和推理都是可接受的。我实际在项目里测得的数据如下:
| 模型配置 | 参数量 | 推理帧率(fps) | 备注 |
|---|---|---|---|
| YOLO26 baseline | 21.3M | 95 | 原版结构 |
| + RFB@P5 | 21.4M | 86 | 单点替换,速度影响约9% |
| + RFB@P4+P5 | 21.6M | 79 | 双点叠加,精度上限更高 |
参数只加了0.1到0.3M,但推理时间增加比较明显,因为多分支结构在推理阶段无法像重参数卷积那样合并成单路。这一点后面排查部分还会细说。
2.4 PyTorch实现代码
这是我最常用的一版实现,基于PyTorch,直接用SiLU激活函数保持和YOLO系结构一致:
import torch import torch.nn as nn def autopad(k, p=None, d=1): # 计算padding,保证输出尺寸不变 if d > 1: k = d * (k - 1) + 1 if isinstance(k, int) else [d * (x - 1) + 1 for x in k] if p is None: p = k // 2 return p class Conv(nn.Module): def __init__(self, c1, c2, k=1, s=1, d=1): super().__init__() self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, None, d), dilation=d, bias=False) self.bn = nn.BatchNorm2d(c2) self.act = nn.SiLU() def forward(self, x): return self.act(self.bn(self.conv(x))) class RFB(nn.Module): def __init__(self, c1, c2, r=4): super().__init__() c = c1 // r # 中间通道压缩为输入的1/4 self.branch0 = Conv(c1, c, 1) self.branch1 = nn.Sequential(Conv(c1, c, 1), Conv(c, c, 3, d=1)) self.branch2 = nn.Sequential(Conv(c1, c, 1), Conv(c, c, 3, d=3)) self.branch3 = nn.Sequential(Conv(c1, c, 1), Conv(c, c, 3, d=5)) self.fuse = Conv(c * 4, c2, 1) self.shortcut = nn.Sequential(Conv(c1, c2, 1)) if c1 != c2 else nn.Identity() def forward(self, x): out = torch.cat( [self.branch0(x), self.branch1(x), self.branch2(x), self.branch3(x)], dim=1 ) return self.fuse(out) + self.shortcut(x)这段代码可以直接拿去用。核心就一句话:四个分支并行,膨胀率分别是1、3、5,concat后融合,再接残差。膨胀率5的分支,autopad函数会自动算出padding=5,不需要手动调。
3. 在YOLO26上做RFB消融实验:训练配置、超参数调整与结果解读
3.1 验证场景:工业零件表面缺陷检测
结构改造不能只跑公开数据集里的理想场景,我选择的验证场景是某条质检线上的工业零件表面缺陷检测。数据集中有三类:螺钉划痕、铸件气孔、焊点飞溅。总共1.2万张图像,图像尺寸1280x1280,目标平均边长只有28到45像素,属于典型的小目标场景,而且缺陷和背景的对比度很低。这种场景对感受野增强的敏感度非常高,因为很多缺陷光看局部根本判断不了。
数据划分按8:1:1拆成训练、验证、测试。数据增强方面我没有开太多花样,保留了mosaic和HSV抖动,但在最后10个epoch关掉了mosaic。这里有个经验:小目标场景下mosaic增强虽然能提升多样性,但最后阶段再开只会让模型对“拼接痕迹”过拟合,导致验证集上AP波动很大。
3.2 训练配置与超参数调整
你加了一个新模块,训练参数不可能完全沿用baseline。我这边最明显的调整是把学习率从1e-3降到8e-4,warmup从2个epoch延长到3个epoch。原因很简单:RFB的多分支结构扩展了感受野,也等于扩展了有效梯度路径。梯度路径越多,同等学习率下越容易震荡。
训练配置如下:
| 超参数 | baseline | + RFB |
|---|---|---|
| 优化器 | AdamW | AdamW |
| 初始学习率 | 1e-3 | 8e-4 |
| weight_decay | 5e-4 | 5e-4 |
| warmup | 2 epochs | 3 epochs |
| 输入尺寸 | 1280/1024多尺度 | 1280/1024多尺度 |
| 总epoch | 300 | 300 |
| batch size | 16 | 16 |
其他像BN momentum、eps都用默认值,没动。加RFB后BN的统计量会比baseline更敏感,如果你在训练第50轮左右发现验证集抖动,可以查看BN层的running_mean和running_var是否异常,一般不需要手动干预。
3.3 消融实验
我把结果整理成一张表,方便你对照自己的实验数据:
| 模型 | mAP50 | mAP50-95 | AP_s | 参数量 | 帧率(fps) |
|---|---|---|---|---|---|
| YOLO26 baseline | 0.752 | 0.518 | 0.286 | 21.3M | 95 |
| + RFB@P5 | 0.764 | 0.535 | 0.312 | 21.4M | 86 |
| + RFB@P4+P5 | 0.769 | 0.541 | 0.326 | 21.6M | 79 |
第一组对比就能看到关键结论:P5单点替换带来的mAP50提升约1.2个点,但AP_s提升了2.6个点,说明RFB对“小目标上下文感知”的改善远大于整体mAP。第二组双点方案把AP_s又推高了1.4个点,代价是推理速度掉到79帧。
从特征热力图来看,加RFB后缺陷中心区域的响应更集中,背景误检明显减少。原因也好理解:像螺钉划痕这种目标,出现位置和纹理走向往往受周边区域约束,RFB给了网络一个“瞟一眼旁边”的机会,判断自然更准。
3.4 结果解读与可视化
提升最大的类别是焊点飞溅,AP50从0.61涨到0.69;铸件气孔提升较小,从0.70到0.72。所以看实验结果时不要只看mAP,要按类别拆开分析。如果某个类别提升不明显,很可能这个类别目标本身尺度均匀,感受野增强对它没有额外收益,这不一定说明模块没用。
我还做了一个对比实验:在P5位置加RFB的同时,在P3浅层输出接了一个轻量的SE注意力模块。结果mAP50-95是0.539,比单独RFB略高一点,但AP_s没有继续涨,反而是大目标AP有轻微上涨。这验证了一个结论:通道注意力优化的是“特征选择”,RFB优化的是“特征视野”,两者不是替代关系,但叠加效果取决于当前瓶颈在哪。
4. YOLO26加RFB的踩坑记录:Loss震荡、小目标反降与推理变慢的排查
4.1 训练Loss震荡和NaN的排查
第一次跑RFB版本时,我在第3个epoch就遇到了loss突然变成NaN的情况,训练直接崩掉。当时第一反应是梯度爆炸,把梯度裁剪从默认值改到0.5,仍然没用。
后来把输入缩减到单卡batch=4,逐步打印每一层的输出数值范围,定位到膨胀率为5的那个分支在FP16混合精度下出现数值溢出。空洞卷积本身并不会导致NaN,但它和混合精度配合时,大感受野下激活值波动范围确实更大,中间层输出偶尔会超过FP16的表示范围。
最终解决方案是:保持FP16但给RFB模块单独设置FP32计算范围,同时把初始学习率降到8e-4。如果你不想动混合精度的配置,也可以简单换成BF16,数值稳定性好很多。
还有一个经验:如果RFB分支里的BN初始化不当,训练初期就会有明显的loss抖动。解决办法是确保分支里的BN层和主网络其他BN层用同样的初始化参数,别额外加重。
4.2 小目标AP反降的几个原因
如果加入RFB后AP_s不升反降,我遇到过三种原因,你可以对照排查。
原因一是模块位置放错。把RFB放在P5(stride=32)是最安全的,因为它只影响深层语义;但有些人想一步到位把它放在P3(stride=8),结果小目标确实“看到了更大的范围”,同时把周围背景信息也一起学进去了,局部纹理分辨能力被稀释,AP_s反而下跌。浅层特征图分辨率高、通道多,放RFB的计算开销也更大。
原因二是通道压缩太狠。reduce ratio如果从4改成8,每个分支中间通道数太少,多尺度信息还没足够表达就被融合卷积压掉了。RFB的核心竞争力是“多分支表达丰富度”,你把每个分支砍得太细,它就退化成堆空洞卷积了。
原因三是训练增强和感受野变化不适应。小目标数据集中mosaic增强会把多个缺陷拼到一张图,RFB感受野扩大后会把“拼接缝”也当成上下文信息学进去,小目标AP剧烈波动。遇到这种问题,把mosaic概率降低或者提前关闭,观察是否恢复。
4.3 推理速度退化明显怎么取舍
RFB不能像RepVGG那样训练后重参数化合并成单路卷积,这是它的天生短板。训练时可以随意,部署时每个分支都必须实实在在跑一遍,所以帧率下降是必然的。实测641x640输入下,单RFB版本从95fps降到86fps,双RFB版本降到79fps。
我的取舍建议分两种:第一种是精度优先,比如工业质检、遥感检测这种离线或准实时场景,双RFB方案完全能接受,推荐。第二种是速度优先,比如视频流实时检测,那就只保留P5单点RFB,并把两个分支的3x3标准卷积换成深度可分离卷积,精度损失控制在1个点以内,帧率能回到90以上。
部署到推理引擎时还要注意,空洞卷积对padding的处理在不同引擎上有差异,导出ONNX时务必检查动态轴是否正常,避免在边缘设备上出现结果偏移。
4.4 显存与运行时常见错误
显存不够是最常见的报错。RFB本身参数量不大,但多分支并行会提高中间激活值的内存占用。batch从16降到8可以解决大部分问题;如果还不够,开启梯度检查点(gradient checkpointing)只作用于RFB模块就够了。
另一个常见的低级错误是设置膨胀率时把padding漏了,报错信息往往很抽象,比如“Dilation rate must be greater or equal to 1”,实际上是padding和dilation不匹配导致输出尺寸异常。使用我之前给的autopad函数可以规避这个坑。
还有一点要提醒:如果部署阶段开了TTA(测试时增强),多尺度测试时膨胀卷积的感受野会叠加得更大,结果框容易出现偏移。我的建议是TTA只保留水平翻转,不要叠加多尺度,否则RFB的效果会被波动抵消。
5. RFB还能怎么玩:注意力叠加、轻量化变体与动态膨胀率
5.1 RFB与注意力模块叠加的顺序
我在实验中发现一个明显规律:先RFB后SE(通道注意力)效果稳定,先SE后RFB收益很弱。用比喻解释就是:RFB是一台扩大视野的望远镜,SE是一个告诉你要看重哪部分的指挥员。先拉远视野,再聚焦重点,逻辑才通。反过来先聚焦后拉远,重点已经定死在局部视野里了,拉远后信息反而被稀释。
5.2 轻量化变体RFB-Lite
如果你目标设备是边缘盒子或者手机,可以试试RFB-Lite:把所有3x3标准卷积换成深度可分离卷积,1x1卷积通道压缩比改成r=6。这样单个模块参数量降到原来的五分之一到七分之一,AP_s只掉了0.5个点左右,换来的推理时间节省非常可观。对于嵌入式部署来说,这是在精度和速度之间比较划算的平衡点。
5.3 动态感受野方向
更进阶的方向是做动态感受野:把膨胀率变成可学习的参数,或者加一个轻量门控分支,根据输入图像内容自动决定当前层应该更依赖3x3还是11x11视野。这个方向目前开源实现还不多,但如果你在RFB基础上做个可学习版本,发小论文或者做大作业会是比较亮点的工作。
我个人在实际操作中最大的体会是:RFB这类结构改进,效果好坏很大程度上取决于你的数据集尺度分布是否足够宽。如果你的目标普遍在30像素以下,RFB放在深层是远远不够的,必须做多位置融合;如果目标尺寸都很大,那RFB带来的收益就很有限,不如把精力放在特征金字塔本身的融合策略上。最后再强调一个小技巧:做消融实验时,先把原版baseline训练完整跑通,别急着同时改多个模块。YOLO26的训练成本已经不低,一次只换一个变量,你才知道每个点到底值不值。