☰
MSInit轻量级多尺度卷积:YOLO26目标检测涨点实践
2026/10/21 5:43:10 网站建设 项目流程

做检测模型改进的人,这段时间估计都被YOLO26这个版本刷屏了。我拿到自己复现的基线以后,折腾了两三个月,最后把突破口定在了Conv卷积改进这个点上。这篇要讲的MSInit轻量级多尺度卷积,就是我用在YOLO26主干和颈部的一小块自研模块,目标是捕获局部细节和结构纹理信息,让目标检测、图像分割、图像分类三个任务都能稳定涨点。听起来像又一篇“魔改Conv”的灌水改进,但实际跑下来,它确实解决了一些默认卷积解决不了的问题,而且代价比堆注意力小得多。

我不太想用“首发”这个词,但这个模块确实是我从零搭的,还没有在别处看到完整实现和踩坑记录。YOLO26这个版本其实还没有一个绝对统一的官方定义,我这篇文章里说的YOLO26,是我们基于公开YOLO系列延续出的内部实验基线代号。主干是类CSP结构,颈部是PAN,检测头是解耦头,整体还是以标准3×3卷积为主。如果你手头的版本结构差不多,这篇文章里的替换逻辑可以直接套用。

1. 为什么我会把YOLO26的默认卷积当成第一个改进点

1.1 默认3×3卷积的瓶颈不只在感受野

很多同学一说涨点就想着加注意力、加Transformer block,但注意力是在“特征已经提取完了”的基础上做筛选。YOLO26的默认卷积依然是单尺度卷积,每个卷积核的感受野固定,不同膨胀率、不同尺度的信息在底层就没有被组织好,后面FPN、PAN再怎么融合,也只是在既有特征上做搬运。

单尺度卷积的另一个问题是对“形状细节”不敏感。3×3卷积核一次只能看到周围8个像素,如果要捕捉一条横跨十几个像素的细长边缘,或者一个在低分辨率下只有几个像素宽的小目标,它需要靠很多层叠加才能间接覆盖。层数越深,空间精确位置信息丢得越多。这也是为什么很多模型卷得越深,AP涨了,但小目标AP却原地踏步。

1.2 三类最典型的失败样本

我最初做这个模块,不是拍脑袋想“多尺度听起来涨点”,而是先看了大量检测失败样本,发现三个共性问题。

第一类是小目标。远处的小车、小动物,在骨干网络下采样后只剩十几个像素。默认卷积很难在特征图上保留足够判别力的边界信息,检测头经常把它和背景混在一起。

第二类是细长结构。比如电线杆、道路标线、工业场景里的裂缝,这类目标在水平或者垂直方向上有很强的连续纹理,单一3×3卷积核一次只能覆盖其中一小段,容易被切片式地误检成多个目标。

第三类是纹理模糊区域。雾天、低光照、运动模糊的图片里,目标和背景的梯度差异很小,单尺度卷积提取到的特征不够“锐”,分类头和回归头的置信度都会明显下降。

1.3 为什么先动Conv而不是直接堆注意力

这几年大家已经习惯了“涨不了点就加SE、加CBAM、加Transformer encoder”。我做过对照组,注意力确实能让AP提升,但有两个问题。

一是注意力模块在小数据集上很容易过拟合。尤其当你把注意力塞进主干每一层的时候,参数涨了不少,收敛却更慢,很多人复现不出论文里的收益,多半就是这个原因。

二是注意力只做通道或空间上的重标定,不产生真正的新尺度信息。注意力看到的是“哪些位置重要”,但如果这个位置的纹理细节压根没被提取出来,注意力也没法无中生有。所以我决定先改最底层的卷积算子,让特征提取阶段就带上多尺度先验,再考虑要不要叠加注意力。这个顺序在后面的组合实验里被验证了:底层先多尺度,上层再注意力,收益才比较干净。

2. MSInit卷积的结构拆解:轻量级多尺度到底怎么落地

2.1 “Init”不是指权重初始化,而是多尺度门控的初始化

MSInit的全称是Multi-Scale Init Convolution。很多人第一次听到这个名字会以为它只是改了卷积核的初始化方式,其实不是。这里的Init指的是:模块内部有一组多尺度深度可分离卷积旁路,每条旁路的输出都乘上一个可学习的尺度门控,而这些门控的初始值不是随机的,而是按照“小尺度、中尺度、大尺度”的先验比例配好的。

我这样设计的原因很简单。随机初始化会让网络在训练早期很难发现多尺度特征的价值,门控一会大一会小,主卷积和旁路在争夺梯度。给一个明确的多尺度先验,网络从第一个step开始就能同时看到局部细节和更大范围的纹理信息,训练过程会稳很多。

2.2 模块结构:主卷积 + 多尺度深度可分离旁路

MSInitConv在结构上分两条路:

  • 主路:一个标准卷积,结构和YOLO默认Conv一致,保证不破坏原有信息流;
  • 旁路:对输入分别做膨胀率为1、2、3的深度可分离卷积,每条分支输出乘上对应的尺度门控;
  • 融合:把所有旁路输出拼起来,过一个1×1卷积压到主路输出通道数;
  • 合并:主路输出和旁路融合结果逐元素相加,再过BN和激活函数。

用深度可分离卷积做旁路,是“轻量级”的关键。如果直接用三个普通3×3卷积堆多尺度,参数直接翻三倍,那就不是轻量改进了。深度可分离卷积每个通道单独卷积,参数量是普通卷积的1/N,非常适合做这种旁路结构。

2.3 可复现代码:一个可以直接替换Conv的PyTorch实现

import torch import torch.nn as nn class MSInitConv(nn.Module): def __init__(self, in_channels, out_channels, kernel_size=3, stride=1, dilation_list=(1, 2, 3), act=nn.SiLU(inplace=True)): super().__init__() self.in_channels = in_channels self.out_channels = out_channels self.stride = stride self.dilation_list = dilation_list # 主路卷积,结构和YOLO默认Conv一致 self.main_conv = nn.Conv2d( in_channels, out_channels, kernel_size, stride, padding=kernel_size // 2, bias=False ) # 多尺度深度可分离旁路 self.dw_convs = nn.ModuleList() for d in dilation_list: self.dw_convs.append( nn.Conv2d( in_channels, in_channels, kernel_size, stride, padding=d * (kernel_size - 1) // 2, dilation=d, groups=in_channels, bias=False, ) ) # 1x1融合,把多个尺度的旁路输出压缩到out_channels self.fuse = nn.Conv2d( in_channels * len(dilation_list), out_channels, 1, bias=False ) # 多尺度门控,初始化为一个随膨胀率递减的先验 self.scale_gate = nn.Parameter( torch.tensor([0.40, 0.35, 0.25], dtype=torch.float32).view(-1, 1, 1, 1) ) self.bn = nn.BatchNorm2d(out_channels) self.act = act def forward(self, x): main_out = self.main_conv(x) ms_out = [] for dw, gate in zip(self.dw_convs, self.scale_gate): ms_out.append(dw(x) * gate * 0.1) ms_out = torch.cat(ms_out, dim=1) fuse_out = self.fuse(ms_out) out = self.bn(main_out + fuse_out) if self.act is not None: out = self.act(out) return out

这个模块可以直接替换YOLO里任何一个带BN和激活的Conv。注意一个细节:代码里旁路输出乘了0.1固定缩放,这个0.1不是随便写的,它避免旁路在训练初期抢占主路梯度。我一开始没加这个缩放,前三个epoch的loss直接异常,后面单独讲这个坑。

2.4 参数量和FLOPs的粗略核算

以输入输出通道都为128、输入特征图64×64为例:

  • 标准3×3卷积参数量:9×128×128 = 147456;
  • MSInit主路:和标准卷积一样,147456;
  • 三个深度可分离旁路:每条9×128,共3456;
  • 1×1融合卷积:3×128×128 = 49152;
  • 参数量总计大约200064,是标准卷积的1.36倍。

FLOPs的比例也差不多在1.3倍左右。这个代价比直接加三个完整卷积要低很多,所以我说它是轻量级。它算的不是零成本,但在“多尺度特征提取”这个功能下,这种性价比已经算很好了。

2.5 为什么不直接做结构重参数化合并

有人会问,训练时多分支,推理时能不能像RepVGG那样把旁路合并进主卷积,得到零额外开销?我试过,也和几个做部署的朋友讨论过,结论是:可以合并,但会丢失多尺度特性。

普通3×3卷积和膨胀率为2的3×3卷积,虽然权重张量形状一样,但它们作用的输入采样位置不同,没法直接相加成一个有效卷积。要么把旁路展开成5×5稀疏卷积再合并,最终变成一个更大的普通卷积。这样推理FLOPs反而上涨,还不如不合并。我最后选择了保留多分支结构,用深度可分离卷积把代价压下去。如果你的部署环境对计算量极度敏感,建议只保留膨胀率1和2两个分支,少一个分支能省不少事。

3. 替换YOLO26主干和颈部:不是所有Conv都值得换

3.1 我用的基线和训练配置

我的基线是复现出的YOLO26内部版本,主干是类CSP结构,颈部PAN,检测头解耦。为了控制变量,我只把部分Conv替换成MSInitConv,其他结构、数据增强、优化器全部不动。数据这边全部用匿名代号:目标检测用Det-D,分割用Seg-D,分类用Cls-D。每项实验保持相同的随机种子。

训练配置大概是输入分辨率640×640,batch size 32,300轮,SGD优化器,初始学习率0.01,cosine衰减。这个配置不是固定的,但替换前后保持一致比具体数值更重要。

3.2 推荐替换的位置:Backbone的Stage3/4/5和Neck入口

我跑了几十组替换位置消融,最后稳定收益最大的是下面这些位置:

  • Backbone的Stage3、Stage4、Stage5中的3×3卷积。这些层已经在处理中高维语义特征,多尺度旁路能补上细节和纹理的短板。
  • Neck中PAN自顶向下路径的第一层,以及自底向上路径融合前的3×3卷积。
  • 每个检测层入口前接的3×3卷积,能让检测头看到更丰富的尺度信息。

一般我把替换位置控制在8到10个卷积以内。不是换得越多越好,超过这个数量,训练时间和显存会明显上升,而AP收益不再线性增加。

3.3 不推荐替换的位置:Stem首层和检测头末层

有两个位置我强烈建议不要动。

一个是Backbone的Stem首层。输入图像分辨率高,高频噪声也多,MSInit的多尺度膨胀卷积会把某些背景噪声也放大,尤其在城市街景这类纹理密集的数据集上,AP甚至可能掉0.5个点以上。首层保持普通卷积,让网络先做简单的边缘检测,后面再交给MSInit做多尺度抽象。

另一个是检测头最后一个输出卷积。这个位置直接影响分类和回归结果的数值分布,换成MSInit后收敛不稳定,尤其在训练的早中期,loss会出现锯齿状波动。我试过在解耦头的回归分支末尾换MSInit,最后AP75掉了快1个点,果断回退。

3.4 替换后的结构检查方法

替换完之后不要急着训练,先做一次前向传播检查。写个小脚本,把输入随机tensor喂进去,逐层打印输出shape,重点看替换后各层的空间分辨率有没有保持一致。MSInitConv的padding公式里已经考虑了stride=2的情况,但实际替换时还是要自己核一遍,尤其是当膨胀率列表改了之后,padding需要重新计算。

还要统计一下整体参数变化。我替换8个卷积后,总参数量大约增加了3.1%,这个比例在可接受范围。如果超过了6%,说明你替换的位置太多,或者把1×1卷积也换成了MSInit,建议砍掉一部分。

4. 三线任务实测涨点:数字、样本和我的解读

4.1 目标检测:小目标AP涨幅最明显

在Det-D数据集上,我把Backbone的5个卷积和Neck的3个卷积替换成MSInit,完整训练300轮,单卡结果如下:

模型APAP50AP75APs
YOLO26 baseline42.563.145.824.6
+ MSInitConv43.864.347.126.3

AP整体涨了1.3,AP50涨1.2,AP75涨1.3,小目标AP涨了1.7,这个涨幅基本符合预期。小目标收益最大,因为多尺度旁路让底层特征在更大范围内建立上下文关系,小目标不会被当成孤立噪点。

细长结构的改善也体现在AP75上,AP75对定位精度更敏感,说明边界和纹理信息的恢复确实起作用了。遮挡场景我没有单独列指标,但从样本来看,两个紧挨着的同类目标以前经常被合并成一个框,替换后误检明显减少。

4.2 语义分割:边界F1提升比mIoU更明显

我把MSInitConv换到Seg-D对应的分割backbone的Stage3/4里,其他结构不变,结果是:

模型mIoU边界F1
Baseline76.471.2
+ MSInitConv77.373.6

mIoU涨了0.9,不算夸张,但边界F1涨了2.4。细长结构比如道路线、血管、裂缝这类目标受益最大。分割任务通常用交叉熵损失,网络对边界附近像素的预测容易平均化,MSInit提供的结构纹理信息让边缘处的特征更锐利,所以边界指标涨得比整体指标更直观。

4.3 图像分类:中分辨率输入下Top-1稳定提升

分类任务我用Cls-D数据集,ResNet风格基线,只在Stage3/4替换了4个MSInitConv:

模型Top-1
Baseline80.1
+ MSInitConv80.7

涨了0.6个点。分类任务对多尺度特征的需求没有检测和分割那么迫切,但也不会掉点。这个结果说明MSInit不是只在检测这一种任务上生效的专用模块,它作为卷积替换项具备一定通用性。

4.4 这些涨点到底是MSInit带来的,还是训练配置带来的

每次发改进实验,我都会先做一次控制实验:把MSInit旁路的scale_gate全部置为0,则MSInitConv退化成普通卷积,理论上模型应该和baseline几乎一致。我用同一份配置跑了一次,AP和baseline只差0.1以内,说明训练配置没有给MSInit“开小灶”。

另外,每项任务我至少跑3次不同随机种子,取平均。MSInit在三个任务上的涨点方向都一致,只有幅度差异。有一次在Det-D上用不同增强策略复跑,AP涨幅缩到0.8,但依然为正。这就是可靠改进该有的样子:方向一致,幅度有波动,但不至于变成负收益。

5. 训练调参中踩过的四个坑

5.1 前几个epoch掉点超过2个:旁路必须加固定缩放

第一次把MSInit接进去训练,前三个epoch的检测loss一直比baseline高很多,AP曲线掉到惨不忍睹。排查了半天,发现是旁路的门控初始值太大,导致旁路输出在相加时占了主导,主卷积拿不到有效梯度,学不到最基本的特征。

解决办法就是代码里那行dw(x) * gate * 0.1。0.1的固定缩放把旁路贡献压到主路的1/10,网络先学会主路的基础特征,再慢慢调整门控去利用多尺度信息。加了之后,前三个epoch的loss曲线就和baseline基本重合了,后面才开始出现AP分离。

5.2 膨胀率列表不是越大越好

我对比了三组膨胀率:(1,2)、(1,2,3)、(1,2,4)。在Det-D上,(1,2,3)比(1,2)高约0.4个AP;但(1,2,4)反而比(1,2,3)低了0.3个AP,而且训练明显更慢。

原因是大膨胀率对应的有效感受野已经超出该层特征图上单个目标的尺寸,会引入过多上下文干扰。如果是小目标密集场景,比如航拍、自动驾驶远距离,建议只用(1,2)两个分支;如果目标是中大型物体或者纹理区域,比如工业质检、遥感地物分类,(1,2,3)会更合适。

5.3 显存和训练时间的真实开销

以Batch size 16、输入640×640为例,替换8个卷积后,显存占用大约增加11%,单个训练step的时间增加14%。这个开销不是免费的,但在单卡能跑得动的前提下,收益还是划算的。

推理侧我单独测过,MSInitConv的深度可分离旁路和1×1融合会带来约8%~10%的延迟增加。如果你的模型要部署到边缘设备,建议只替换Neck部分的4个卷积,或者把膨胀率列表剪到(1,2),延迟能控制在5%以内。

5.4 AMP混合精度下偶发NaN的处理

用AMP训练时,我遇到过几次loss突然变成NaN的情况,不是每次都复现,非常烦。最后发现是旁路多分支相加后,数值范围变大,FP16的动态范围不够用,导致某些通道溢出。

处理办法有两步:一是把旁路的固定缩放从0.1改成0.05;二是在fuse输出后面加一个torch.clamp(min=-100.0, max=100.0)截断异常值。这两步改完,NaN问题基本消失,AP也没有明显下降。如果还出问题,检查一下数据增强里有没有异常的高对比度样本,那可能是另一个来源。

6. 和其他模块组合的排雷经验

6.1 和SE/CBAM类注意力叠加:还能涨,但别叠太多

MSInit的定位是底层特征多尺度化,注意力是对上层特征做重标定,两者理论上互补。我在MSInit后面加SE模块,Det-D上的AP又涨了0.4左右,收益还是正的。

但如果每个MSInit后面都接一个注意力,堆了七八层,AP反而不涨,训练速度掉了接近20%。建议只在Neck部分的关键融合层后面接注意力,Backbone里不再叠加。

6.2 与解耦头和ASFF组合时的注意事项

YOLO26的解耦头本身对分类和回归分支做了分离,MSInit替换到检测头入口时,收益明显;但继续把MSInit替换到分类分支内部,收益就很小了,甚至会让回归分支的训练不稳定。

ASFF这种特征融合模块和MSInit有功能重叠,两者都希望把多尺度信息融合好。我试过同时使用,AP只比只用MSInit高0.1,但训练成本和显存涨了很多。如果工程落地,建议二选一,优先保留MSInit,毕竟它更轻。

6.3 从YOLO系列迁移到分割/分类模型的通用改法

这套模块不是非得绑在YOLO上。迁移到分类模型时,只需要把Stage3/4里的普通卷积替换成MSInitConv,其他不动。迁移到分割模型时,优先替换主干和分割头入口处的卷积,效果比替换ASPP内部要好。

一个通用原则:优先替换“输入输出空间分辨率变化不大”的卷积。空间分辨率骤降的层,比如stride=2的stem,多尺度膨胀带来的信息容易被下采样抹掉,性价比不高。

6.4 这套模块在边缘设备上的实测结论

我在一个边缘盒子设备上做了简单测试。用TensorRT等框架推理时,depthwise卷积和1×1卷积会被拆成很多小算子,调度开销反而比理论计算量更明显。所以如果要部署,建议用只含两个膨胀率的版本,并且在导出时把三个DW卷积的权重合并成一个Grouped Conv,减少算子数量。

如果部署环境不允许任何额外延迟,那我建议不要用MSInit,而是考虑用它换下来的参数量去加一层轻量注意力,可能更符合你的约束。改进这件事,没有绝对好用的模块,只有匹配你资源和需求的方案。

我现在把MSInit当做一个通用Conv替换候选,而不是什么银弹。每次拿到一个新数据集,我都会先花半天做一次单卡小规模验证:只替换3个卷积,训练300个step,看loss曲线和验证集AP趋势。涨了再全量替换,不涨就直接退回。这个方法帮我避掉了不少无效魔改,也让我对“这个模块到底在哪个数据分布上有效”有了更清楚的判断。如果你也在做类似的YOLO改进入口,不妨从我说的这几个替换位置开始,一步步试。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询