YOLOv11发布之后,我身边不少做检测落地的团队都把它当成新基线来测,速度确实够顶。但换到小目标密集、目标重叠的业务数据上,官方默认结构的精度还是让人有点不甘心。这次我选了在v11上做一次“手术”:把BiFPN的加权双向特征融合和RepVGG的重参数化卷积结构一起接入网络,目标是在尽可能不动推理框架的前提下,把多尺度特征的利用率提上去。改完跑了两个业务数据集和一组公开数据验证,mAP普遍涨了1.3到1.8个点,小目标的收益更明显,而且因为RepVGG推理时可以折叠,帧率损失很小。这篇文章就把完整思路、结构改动、训练参数和踩坑记录整理一遍,适合手里有自定义数据、想在现有检测模型上做精度提升的工程人员,也适合对YOLO系结构改造感兴趣的同学参考。
1. 整体设计与架构升级思路
1.1 为什么选YOLOv11当底座
YOLOv11相比YOLOv8在主干和检测头的计算组织上做了不少优化,整体推理速度更快,参数效率更高。我刚拿到v11的时候先跑了默认权重,检测头的输出质量比v8稳定,尤其是靠近目标边缘的回归框更准。不过在监控相机和无人机拍摄的数据上,默认结构还是明显偏弱,问题就出在颈部特征融合上。
v11官方neck默认延续了PAN-FPN思路。PAN结构在一个自顶向下、一个自底向上的路径里用concat把不同层特征拼在一起。但因为concat本质上把每层特征一视同仁,网络不能很精细地控制“来自深层语义”和“来自浅层纹理”各自的贡献比例。对多尺度目标,尤其那些只有十几个像素的小目标来说,浅层细节在向下传递的过程中不断被稀释,最后到检测头时位置信息已经不够用了。
另一个驱动因素是想在不换检测头的前提下提升感受野和特征表达能力。v11的检测头是解耦结构,分类和回归分支分开输出,整体设计已经很干净,贸然替换容易引入回归不稳定。所以我决定保留v11的backbone和检测头,只把neck和部分卷积单元换成更适合业务数据的结构。这个取舍一方面降低了改造风险,另一方面也让问题定位更清晰——如果效果变差,可以直接追溯到特征融合这一层。
1.2 这次升级拆成了两条线
整个方案拆成两条独立的改造线,避免互相纠缠。第一条线是特征融合:用BiFPN替换PAN-FPN中的concat节点。BiFPN在做跨尺度融合时给每个输入都配了一个可学习的权重,网络在训练中自己判断哪层特征更值得信任。这个改动带来的参数增量极小,主要成本是代码实现和少量训练时间。
第二条线是基础卷积单元:在neck和检测头前的普通3×3卷积位置插入RepVGGBlock。RepVGGBlock在训练时是三分支结构,包含3×3卷积、1×1卷积和恒等映射,近似于增强模型容量。真正值钱的是推理阶段,它可以重参数化成一个单分支3×3卷积,等于把训练时的多分支容量“压缩”成推理时的单卷积计算,速度和内存几乎都是普通3×3卷积的水平。
为什么不干脆把backbone全部换成RepVGG?我单独做过对照测试。backbone全换之后,训练波动明显变大,尤其是小batch时loss下降很不稳,而且收益相比只改neck部分并没高多少。原因也好理解,backbone主要学的是底层视觉特征,多分支结构带来的额外非线性对浅层帮助有限,但训练过程中BN统计量一旦不稳,整个收敛都会受影响。因此正式版本只在neck关键路径和检测头前的融合节点使用RepVGGBlock,确保百分之八十的收益、只承担百分之二十的风险。
2. 关键原理拆解
2.1 BiFPN加权到底在改什么
要理解BiFPN,得先知道传统PAN-FPN的问题。PAN在特征融合时是这样的流程:先自顶向下从P5上采样到P4和P3,然后再自底向上从P3下采样回P4和P5。每个融合节点用的是concat后接1×1卷积。比如自顶向下融合P4时,表达式近似为:
P4_td = Conv1x1(Concat(P4_out, Upsample(P5_td)))
乍看没什么问题,但concat加卷积的等价能力其实是一个固定权重的线性叠加。即使卷积核能学到不同通道的权重,它也是对所有空间位置共用同一组权重,不能根据目标尺度动态调整。尤其当某一层特征明显更可靠时,固定叠加会强制网络把噪声也一起传下去。
BiFPN的改进核心是加权组合。它给每个参与融合的特征分配一个可学习的标量权重w,并在每轮更新前用激活函数约束为非负。融合公式简化为:
P4_td = Conv( (w1 * P4_out + w2 * Upsample(P5_td)) / (w1 + w2 + epsilon) )
其中epsilon是一个很小的常量,防止除零。每个w经过训练会自动变大或变小,网络就等于学会了“哪条路径信息量大”的软开关。这样做的好处非常直接:不需要增加太多通道数量,只引入几个可学习标量,就能让特征选择的灵活度上一个档次。
还有一个实现细节容易被忽略:BiFPN并不只是单方向加权,它在原PAN的基础上加了额外的跨尺度连接。比如自顶向下融合P3时,不仅用P4_td的结果,还可以直接把原始P3_out再引入一次。这样浅层纹理信息能多一跳直接送到检测头,小目标定位特别受益。我用在v11上的配置就是三条路径:P3、P4、P5,自顶向下加底向上,每组加权后接一个普通卷积做空间对齐。
2.2 RepVGG重参数化怎么做到“免费”
RepVGG最反直觉的一点是:训练时模型比推理时“胖很多”,但推理时却完全不用承担这些分支的代价。原理其实不复杂,本质是卷积加法满足线性性。训练阶段一个RepVGGBlock包含三个平行算子:3×3卷积、1×1卷积和恒等映射(如果要处理通道数变化,恒等映射会用1×1代替)。这三个算子可以看作三个线性函数f33、f11、fid,输出是它们结果之和。
到了推理阶段,把三个操作分别整理成卷积核加偏置,然后直接相加。加上BN层的scale和shift,也可以全部折算进卷积核的权重和偏置里。最终得到的仍然是一个标准3×3卷积核,只是数值上等价于原来三分支组合。
这意味着什么?训练时模型学习的是一种更丰富的函数空间,推理时却以单卷积的形态运行。我在落地中最关心的是推理延迟和显存占用,RepVGG正好把“涨精度”和“掉速度”这两件事拆开了。代价主要在训练阶段,多分支会带来约30%左右的显存增加和少量训练时间开销。对于一台中等配置的GPU服务器来说,这部分成本完全可以接受,尤其是部署端能换来几乎不掉的帧率。
在具体融合实现里,我写了这样一个方法:遍历模型中的RepVGGBlock,把每个分支的卷积核抽取出来,加上各自BN的均值方差参数,最后合并成kernel和bias,再替换掉block里的主卷积。融合完之后模型结构里不再有分支,导出到部署格式也会顺畅很多。
2.3 融合后的网络结构形态
最终v11的整体流程是这样的:输入图像先经过backbone逐级下采样,产出三组输出特征P3(步长8)、P4(步长16)、P5(步长32)。neck第一段自顶向下:P5_td经过RepVGGBlock后,与加权后的P4特征融合;融合结果继续上采样,再与加权后的P3特征融合。接下来是自底向上更新:P3融合结果经过卷积下采样,与加权后的P4特征再融合一次,得到P4_out;同理得到P5_out。
每个融合节点都是一个BiFPN加权组合,后面紧跟一个3×3卷积做空间对齐。在融合节点附近,我把部分普通3×3卷积替换成了RepVGGBlock,确保重参数化能力能用在最需要非线性融合的位置。检测头完全沿用v11的解耦头,分类和回归分支不变。
这样改完之后,新增参数主要来自BiFPN的权重和少数卷积通道,约占原模型的5%到8%。从计算图来看,增加的FLOPs不多,因为BiFPN节点没有引入额外的大卷积层,只是把原本concat后的1×1卷积替换成了“加权相加+3×3卷积”。RepVGGBlock在训练时是三条路,在推理时又是单卷积,形式上的差异让部署前的模型结构看起来和一个常规卷积网络几乎一样。
3. 落地实战:配置、训练、导出
3.1 数据准备与预处理
不管网络结构怎么改,视觉模型的收益上限还是由数据决定。我这次使用的业务数据来自两个场景:一个是高空视角的无人机拍摄画面,目标尺寸普遍很小;另一个是流水线质检画面,目标尺寸差异大且经常互相遮挡。
数据准备阶段我做了几件事。第一步,统一标注格式,把所有标注转成每张图一份txt文件的目标类别和归一化坐标。第二步,按场景划分训练集和验证集,避免同一个视频片段里的相似画面同时出现在两个集合里,否则验证指标会虚高。第三步,用脚本统计目标尺寸分布,确认小目标占比是否足够。如果小目标样本太少,单独加权loss也不够,我会做针对性的过采样和裁剪增强。
训练时的数据增强我开了mosaic、mixup、随机仿射、HSV扰动。输入尺寸用了多尺度,范围在480到960之间随机采样,而不是固定640。多尺度对小目标很关键,因为固定尺寸等于把训练时的目标缩放范围限制死了。不过mosaic比例不能太高,后期容易让网络过拟合到拼接痕迹,我一般前200轮开启,最后20轮关闭并适合当前输入分辨率。
3.2 模型结构修改:配置文件和自定义模块
在v11上引入BiFPN和RepVGG,代码层面分两块:自定义模块定义和模型配置引用。
先定义BiFPN_Fusion模块。它接收来自三个尺度的输入,每组输入先经过1×1卷积对齐通道,然后学习两个或三个权重,加权求和后接3×3卷积输出。RepVGGBlock则按照训练和推理两种模式切换:训练时用三分支forward,推理时使用融合后的单卷积forward。
配置文件方面,neck区域的关键改动类似这样:
# 自定义模块注册后,在neck中使用 neck: - [-1, 1, RepVGGBlock, [256, 3, 1]] - [[-1, 17], 1, BiFPN_Fusion, [256]] # 假设上一层和backbone的P3作为输入 ...这里的[-1, 17]表示把当前层输出和第17层输出同时送到融合节点。实际通道数要根据backbone输出对齐,一般做法是在BiFPN入口加一个1×1卷积统一维度。自定义模块注册进解析器后,配置文件才能正确识别。
我在第一次改的时候踩了个坑:只改了配置文件,忘了在模块注册表里加入两个新类,结果直接报错找不到模块。后来我写了一个简单的断言,解析完配置文件后打印每一层的输出shape,只要shape和预期不一致立刻停,能省不少排查时间。
3.3 训练参数与策略
训练策略上我沿用了官方推荐的基线,然后针对修改后的结构做小调整。基础配置是:输入分辨率640,batch size 16,初始学习率0.01,SGD优化器,momentum 0.937,weight_decay 0.0001。学习率调度用cosine衰减,前3轮warmup。
新增的两个模块需要单独留意初始化。BiFPN权重我初始化为0.5,所有融合分支等权起步,避免网络一开始就偏向某一路。RepVGGBlock的BN层scale也需要注意:如果分支较多,建议把恒等映射分支的BN scale初始化为1,而3×3分支保持常规,训练会稳一些。
显存方面,RepVGGBlock三分支会让训练显存增加不少。如果出现OOM,可以把batch size减半,同时开梯度累积两步,效果一样,只是训练时间稍长。我在训练时还开着EMA,权重衰减设成0.0005,一定程度上平滑损失曲线。改进后的模型训练300轮左右,单卡2000多张图像大约需要10到12小时。
3.4 训练监控与恢复
训练过程中我不只看总loss,还会盯着分类损失、回归损失和BiFPN权重均值。有一个比较隐蔽的问题:如果BiFPN权重始终停在0.5附近不变化,说明梯度没有有效传到融合权值上,这通常是因为上游通道数不一致导致梯度被1×1卷积吃掉。解决办法是在权重计算后加一个detach查看梯度,或者在融合节点后额外使用shortcut监督。
验证阶段我每一轮都跑一次mAP,同时保存best和last两份权重。因为EMA会对指标有一点延迟,所以EMA权重单独在验证集上挑最优版本保存。训练如果中断,从checkpoint恢复时要注意学习率调度:不要继续使用旧的step数,而是按剩余epoch重新计算,否则cosine曲线会跳变。
4. 性能对比与业务效果
4.1 离线指标对比
为了公平对比,我在同一份验证集、同样输入分辨率640、同样训练策略下跑了三个模型:YOLOv11 baseline、v11+BiFPN、v11+BiFPN+RepVGG。结果如下表所示:
| 模型变体 | mAP | 小目标mAP | 参数量 | 推理帧率 |
|---|---|---|---|---|
| YOLOv11 baseline | 50.3 | 31.2 | 11.4M | 108 FPS |
| v11 + BiFPN | 51.1 | 33.5 | 11.9M | 102 FPS |
| v11 + BiFPN + RepVGG | 51.6 | 34.8 | 12.3M | 99 FPS |
从数字上能明显看到,加了BiFPN之后总体mAP提升0.8,小目标mAP提升2.3个点。再加RepVGG后,总体mAP又提了0.5,小目标继续涨1.3。推理帧率从108降到99,大约下降8%,这在很多业务场景里是可以接受的;如果部署端开启并保存RepVGG融合后的单卷积结构,实际帧率会更接近baseline,这里的99还是在训练模式未融合的情况下测的。
为什么小目标涨得比总体还明显?因为自顶向下路径里P3特征被重复使用了更多次,浅层空间信息保留得更完整,BiFPN又能给P3分配更高权重。RepVGG则给融合节点补充了更强的非线性,让加权结果在送入检测头前有更充分的表达空间。
4.2 场景化效果分析
在无人机场景里,改进后最大的感受是漏检减少了。之前小目标的召回率低,模型经常把十几像素的人判断成背景;加了BiFPN之后,网络确实学会把更多注意力放到小目标层。在质检场景里,遮挡目标之间的边界更清楚,之前两个箱子挨在一起经常输出一个合并框,现在已经能稳定区分。
有一点要提醒:如果业务数据本身是大目标为主,这种加权融合的收益会变小。我在一个只有大目标的公开数据上做了对比,mAP提升只有0.3到0.5,基本在噪声范围内。所以改造前最好先分析目标尺寸分布,不要盲目给所有测检测任务都上这个组合。
5. 常见问题排查与避坑记录
5.1 训练不收敛或早期发散
这是最容易遇到的一类问题。常见原因有三个:学习率太大,RepVGGBlock里的BN统计量不稳定,以及BiFPN权重更新异常。我先看前50步loss有没有下降趋势,如果loss直接飞到很大,优先把初始学习率降到原来的1/5。如果loss下降但验证mAP始终不动,查一下RepVGGBlock的BN:训练模式下BN的running_mean可能被多分支相加弄得偏移,最好对3×3分支做更保守的初始化。
BiFPN权重也很关键。如果我在代码里用relu约束权重,训练过程中某个w一旦走到负数区域,relu就会让梯度变成0,那条路等于永久冻结。解决办法是把relu改成hard clamp,让权重在0到1之间,梯度可以正常传递。还有一个小技巧:在每个融合节点后打印权重的norm,如果某个节点始终是0.0,大概率是输入特征在空间上没对齐,先关掉下采样卷积,检查尺寸。
5.2 导出部署时算子不兼容
把模型导出成部署格式时,最容易爆雷的是自定义BiFPN节点。很多推理引擎不支持带多个输入的除法或softmax方式加权求和,直接导出会报不支持的算子。我的处理方案是:把训练好的BiFPN权重提取出来,在推理阶段固定下来,不再走动态softmax,而是直接存成常量标量参与乘加。这样导出的计算图就是一个普通乘法加加法,兼容性会好很多。
RepVGGBlock在导出前必须执行重参数化融合,这是必然一步。如果不融合,模型会带着三分支结构导出,推理时虽然结果一样,但计算量多出好几倍,完全失去速度优势。我建议把融合函数作为正式导出流程的一部分,保存两种模型:一个用于继续训练,一个用于部署。
5.3 提速不明显或出现严重掉帧
如果改了结构后整体帧率还是没上去,先别急着优化模型,用profiler看三段耗时:前处理、模型推理、NMS后处理。有些场景里模型推理时间只占一半多,剩下的全花在NMS和显存拷贝上。这种情况下即使RepVGG把模型中某些卷积合并了,端到端帧率提升也感知不到。先确保后处理阶段用GPU NMS,或者把图像resize和归一化改成预处理管道中的定点计算。
另外,在边缘设备上做INT8量化时,RepVGG融合后的单卷积会比多分支结构更友好。多分支结构量化后精度损失经常超过一个点,融合后单卷积极少出现这种问题,一般损失0.2到0.5个点。如果要追求低延迟,建议先冻结BN,再走量化校准。
6. 经验小结与下一步扩展
这次改造给我最大的体会是:网络结构改动别贪多。先加BiFPN验证特征融合收益,再考虑RepVGG,最后统一做重参数化。如果一开始把所有模块都堆上去,出了性能问题很难定位是权重初始化的问题、梯度传递的问题还是融合参数的问题。我现在养成的习惯是每个阶段做一次小规模验证,先跑2到3个epoch看差异趋势,模型稳定后再上大规模训练。
后续有两个我很想尝试的方向。第一个是把RepVGG和蒸馏结合:用大模型做教师,小模型在训练时保留多分支结构,训练完融合成单路后送给部署端。这样既能吃到多分支结构带来的容量优势,又能在推理时保持单卷积的简洁。第二个方向是用一些简单的搜索策略初始化BiFPN权重,不再固定用0.5起步。比如先统计每个特征层的梯度能量,再按比例设置初始权重,理论上能缩短前期收敛时间。说真的,把YOLOv11加上BiFPN和RepVGG,是我目前在工程约束下觉得性价比最高的一种组合,改造可控,收益可复现,部署也不折腾。如果你也在做类似的检测模型优化,不妨按这套思路先试起来。