把红外图和可见光图直接拼到一起送进YOLO26,一开始看着挺合理,但真正跑到夜间场景或者传感器带噪声的数据集上时,精度掉得让人怀疑人生。最夸张的一次,我在某个跨模态检测任务里用简单的通道拼接,mAP比单用可见光还低了近4个点。后来把融合逻辑换成了内容引导的注意力机制,也就是这次要聊的CGAFusion,才把跨模态的优势真正用起来。
CGAFusion 全称 Content-Guided Attention Fusion,核心思路是让网络自己判断当前区域的RGB特征和红外特征谁更可信、谁更该被抑制,然后动态生成融合权重。它在抑制噪声的同时保留互补信息,对光照突变、雨雾、红外坏点这类干扰特别有效。如果你是做检测模型部署的,或者正在调多传感器融合的算法,这篇记录应该能帮你省不少弯路。
1. 为什么跨模态融合总在噪声面前翻车
1.1 简单融合方案的三个隐患
跨模态检测一般就是指可见光加红外、可见光加深度、或者多光谱组合。拿可见光+红外来说,两种模态信息高度互补:白天可见光纹理清楚,晚上红外能感知热辐射。但这两种信号在成像机理上完全不一样,直接在特征空间做合并会有很多隐藏问题。
第一个隐患是空间对齐误差。可见光相机和红外相机通常放在不同位置,即使做了标定,特征图上的同一位置也未必对应物理世界的同一点。简单相加或者通道拼接会把错位的边缘和轮廓叠在一起,等于给模型加了错误监督信号。第二个隐患是噪声分布不一致。红外传感器容易出现坏点和条纹噪声,尤其在温度剧烈变化时;可见光在低照度下又会有大量高斯噪声。如果融合权重是固定的,模型就会被噪声更强的模态带偏。第三个隐患是语义粒度不对等。RGB特征能提供颜色和纹理,红外特征提供热轮廓和温度分布,它们的高层语义也许一致,但底层的表观细节差异很大,硬拼会让后续卷积核学到一些“平均”但无效的响应。
这些坑,单靠加大训练数据很难填平,需要在融合方式上做文章。
1.2 注意力机制为什么能救场
注意力机制本质上是给特征打一个动态权重。比如通道注意力会让网络更关注有判别力的通道,空间注意力会让网络更关注目标区域。跨模态融合里的注意力,则要解决“当前这个位置或通道上,哪个模态的信息更可靠”的问题。
用一个生活化的类比:你开车时既看挡风玻璃前的画面,又听导航提示。晴天时画面信息多,你会以画面为主;大雾天画面模糊,你会更依赖导航语音。固定融合方案相当于无论什么天气都给画面和语音分配一样的注意力,那大雾天必然翻车。CGAFusion 的“内容引导”就是根据图像内容本身来判断当前环境属于“晴天”还是“大雾天”,动态调整两个模态的贡献。
这里的关键不是单纯加一个注意力模块,而是让注意力权重直接受到输入内容调制。这样可以做到:当可见光区域纹理清晰时,给可见光更大的权重;当红外区域出现强噪声时,自动压低对应位置的响应。这样既能保留互补信息,又能抑制噪声。对比常见的SE、CBAM等单模态注意力,CGAFusion 关注的是模态间的竞争与合作,机制上更接近“互注意力”,但实现上又更轻量。
2. CGAFusion 模块设计:内容引导是怎么落地的
2.1 从输入到融合权重的计算路径
CGAFusion 的输入是同一尺寸的两组特征,记为 F_rgb 和 F_ir。形状一般是 [B, C, H, W]。输出也是同样形状的融合特征 F_fused。核心过程分三步:先提取内容描述符,再生成两组注意力权重,最后做加权融合。
内容描述符不能只用一个全局平均池化,因为噪声在空间上往往是局部的。我的做法是把 F_rgb 和 F_ir 先做逐元素相加和相减,得到两个中间张量:一个代表共享内容,一个代表模态差异。共享内容经过一个全局加局部的描述模块,输出内容描述符。这里会用到两个方向的池化:全局平均池化捕捉整体场景状态,局部卷积捕捉细节差异。两者拼接后过一个 1x1 卷积,压缩成内容向量,再通过 sigmoid 映射到 0 到 1 之间。
然后根据内容描述符生成每个模态的注意力权重。最简单的方式是用两个独立的 1x1 卷积把内容描述符映射成空间权重图 A_rgb 和 A_ir,再在通道维度做归一化。但我实际测试下来,直接 softmax 容易让两个权重变得“非黑即白”,信息丢失太严重。所以我用了“sigmoid + 可学习温度系数”的方式:先算出一个融合系数 t,让 A_rgb = sigmoid(t * g_rgb),A_ir = 1 - A_rgb。这样做的好处是权重连续可微,且网络可以自己学会“两个模态都信一点”而不是强行二选一。
2.2 噪声抑制分支和残差门控
只加权融合还不够。真正让 CGAFusion 能在强噪声下稳定的,是后面接的那个噪声抑制分支。这个分支的思路是:把共享内容与模态差异分开后,差异特征里既包含有用的互补信息,也包含噪声。噪声和互补信息很难直接区分,但通过一个可学习的门控,可以让网络决定“这个位置该信哪个模态”。
具体实现上,我用了一个轻量的子网络,输入是 F_rgb - F_ir 的绝对值,经过两层 3x3 卷积加 LeakyReLU,最后通过 sigmoid 生成一个噪声掩码 N。这个 N 的值越接近 1,说明对应位置的模态差异越可能是噪声。然后在加权融合时,把注意力权重乘上 (1 - N),高噪声位置会被自动抑制。残差连接也在这里起关键作用:融合结果不是直接用 A_rgb * F_rgb + A_ir * F_ir,而是先算出融合主项,再叠加上一个残差分支。残差分支相当于让模型在融合结果基础上微调细节,避免信息被过度平滑。
这种“内容引导 + 噪声掩码 + 残差门控”的组合,本质上是在做特征级的动态滤波。我实测下来,对椒盐噪声、高斯噪声和红外坏点都有明显改善,而且模块开销很小,只有几个卷积层,基本不影响推理速度。
2.3 放进 YOLO26 哪个位置最合适
YOLO 系列通常分成 Backbone、Neck、Head 三段。跨模态融合模块可以放在不同位置,效果差别很大。我的建议是优先放在 Backbone 输出之后、Neck 之前,也就是把 YOLO26 原本的单流 Backbone 扩展成双流 Backbone:一个分支处理可见光,一个分支处理红外,两条分支各自提取初步特征,然后在 P3、P4、P5 三个尺度上都做一次 CGAFusion。
为什么放在这个位置?因为低层特征分辨率高、空间信息完整,这时候做融合能最大限度保留两个模态的细节;如果放到 Neck 里面,特征已经经过了多次下采样,红外的小目标信息可能已经丢了。P3、P4、P5 分别对应浅层、中层、深层,每个尺度做独立融合,可以让网络在不同感受野下分别决策。
另外,也可以只在 P4 层做一次融合来省显存,但精度会比三尺度融合少 1 到 1.5 个点。如果你的训练资源紧张,可以从 P4 单尺度开始,验证模块有没有效果,再逐步扩展到三个尺度。注意不能把 CGAFusion 放到 Head 之后,因为 Head 输出的分类和回归特征已经比较“成熟”,融合权重很难再影响检测头的决策,等于白加。
3. 实操记录:在YOLO26里接入CGAFusion的全过程
3.1 数据准备与增强策略
跨模态数据比普通目标检测数据难准备。首先要求可见光和红外的图像已经做过时间同步和空间配准。我用的测试数据集是模拟项目X,一个虚构的夜间人车检测数据集,每一对图像都经过严格标定,不会出现“同一目标在两张图上差了几十像素”这种问题。如果你的数据没有配准,建议先跑一遍标定流程,否则 CGAFusion 再强也救不了对齐误差。
增强策略上,除了常规的随机翻转、随机缩放、颜色抖动,我特别加了两个与模态相关的增强。第一个是对红外的随机椒盐噪声,模拟传感器坏点;第二个是随机对可见光通道加高斯噪声,模拟低照度。这样训练时模型见过“某个模态突然变脏”的情况,推理时就不容易崩。我还用了模态随机丢弃:以一定概率将其中一个模态的特征全部置零,强迫融合模块学习“只剩一个模态也能检测”的退化模式。这个策略对实际部署特别重要,因为传感器故障在真实场景里并不少见。
3.2 模块实现与网络接入
下面给一份核心实现,偏伪代码,方便你迁移到自己的框架里。
class CGAFusion(nn.Module): def __init__(self, in_channels, reduction=16): super().__init__() self.conv_share = Conv1x1(in_channels * 2, in_channels) self.conv_diff = Conv1x1(in_channels * 2, in_channels) self.noise_net = nn.Sequential( Conv3x3(in_channels, in_channels // reduction), Conv3x3(in_channels // reduction, in_channels), ) self.gate_rgb = Conv1x1(in_channels, in_channels) self.gate_ir = Conv1x1(in_channels, in_channels) self.temperature = nn.Parameter(torch.tensor(1.0)) def forward(self, f_rgb, f_ir): cat = torch.cat([f_rgb, f_ir], dim=1) shared = self.conv_share(cat) diff = self.conv_diff(cat) content = shared * torch.sigmoid(diff) g_rgb = self.gate_rgb(content) g_ir = self.gate_ir(content) t = torch.sigmoid(self.temperature) a_rgb = torch.sigmoid(t * g_rgb) a_ir = 1 - a_rgb noise_feat = torch.abs(f_rgb - f_ir) noise_mask = torch.sigmoid(self.noise_net(noise_feat)) fused = a_rgb * f_rgb + (1 - a_rgb) * f_ir fused = fused * (1 - noise_mask) + f_rgb * noise_mask * (1 - a_rgb) return fused这段代码是经过简化的,真实版本里我会把 gate_rgb 和 gate_ir 换成带 group norm 的卷积,防止训练前期权重震荡。接入 YOLO26 时,你需要在双流 Backbone 的 forward 里拿到两个分支的 P3、P4、P5 特征,然后用一个字典传入 CGAFusion。注意每个尺度的模块要独立创建,不要共用一个模块实例,因为不同尺度的统计分布完全不同。
参数初始化也要说清楚:温度系数初始化为 1,门控卷积的权重用正态分布初始化,偏置设为 0。我试过把 temperature 初值设成 5,前期融合权重几乎变成 0/1,训练很不稳,后来改回 1 就好了。
3.3 训练参数与效果对比
训练配置:输入分辨率 640x640,batch size 32,优化器 SGD 或 AdamW,初始学习率 0.001,使用余弦退火。总轮数 120 轮。前 10 轮冻结双流 Backbone 的前两个 Stage,只训练 CGAFusion 和 Neck/Head,让模块先学习一个合理的融合策略;第 11 轮开始解冻全部网络。这个“先融后检测”的顺序很关键,如果一开始就全量更新,两个模态的梯度会互相打架,损失波动很大。
对比实验是在模拟项目X的测试集上做的,基准是 YOLO26 原始单流模型,只输入可见光,以及两个简单融合方案:通道拼接和逐元素相加。测试时额外加了一组“强噪声测试”,在红外图上叠加 10% 的椒盐噪声。结果如下表:
| 方法 | mAP@0.5 | mAP@0.5:0.95 | 强噪声下mAP@0.5 | 参数量增加 |
|---|---|---|---|---|
| 单流RGB | 71.2 | 52.4 | 63.8 | 0 |
| 通道拼接 | 68.5 | 49.7 | 55.2 | +1.2M |
| 逐元素相加 | 66.9 | 48.1 | 52.7 | +0 |
| CBAM后融合 | 70.3 | 51.2 | 58.6 | +0.8M |
| CGAFusion | 76.4 | 57.9 | 71.5 | +1.6M |
通道拼接反而是负优化,这个结果挺有代表性。原因是拼接通道后,模型需要自己学出跨模态关系,但浅层卷积很难学到那么复杂的非线性权重。CGAFusion 在正常测试下比单流高 5 个点,在强噪声下依然能保持 71.5,说明噪声抑制确实起作用了。参数量只增加了 1.6M,在模型整体里算很小的一部分。
4. 测试期最容易踩的坑
4.1 融合后精度不升反降怎么办
如果你照着上面的结构接完,发现 mAP 比单流还低,先别急着调超参,大概率是三个原因之一。
第一个原因是两个模态的特征统计量差异太大。RGB 特征和红外特征的范围可能不在一个量级,比如一个均值接近 0,另一个均值明显偏正。此时在融合前加一个 BatchNorm 或者 Instance Norm 对齐统计量,能解决大部分问题。我在第二个版本里给每个分支的前面加了一个轻量归一化层,效果非常明显。第二个原因是训练轮次不够。CGAFusion 学到合理权重需要更多迭代,尤其在前 10 轮解冻策略没做的情况下。建议至少跑 50 轮再看趋势,前 20 轮只看训练损失不要急着验证。第三个原因是梯度爆炸。融合门控中的 sigmoid 对输入很敏感,如果学习率太高,权重会在 0 和 1 之间来回跳。把学习率降到原来的 1/5,或者给门控卷积加梯度裁剪,能缓解。
4.2 单模态丢失时的退化处理
实际部署里,红外相机可能被遮挡、失效,甚至输出全黑图。我在训练时用模态随机丢弃处理过这个问题,但推理时如果出现整张红外图全黑,还是需要专门处理。
我的做法是在 CGAFusion 前面加一个模态有效检测器。对每个输入批次,计算每个模态的均值方差,如果一个模态的方差低于阈值且均值接近 0,就判定为“丢失”,此时直接把融合权重设为 0 和 1,也就是完全退化成单模态检测。这个逻辑放在数据加载环节,比放在网络内部更好控制。注意不要把全黑图当成正常数据送进去训练,否则模型会学着忽略红外。
另外,如果只有单模态输入,YOLO26 的双流 Backbone 还是会正常计算两个分支,但其中一个分支输入是 0。这样会有约一半的计算量浪费。我在推理时用了一个标志位,把丢失模态的分支整体跳过,只保留另一个分支,速度能提升大约 30%。
4.3 推理速度和显存优化
CGAFusion 不是重模块,但在三个尺度都加的话,显存占用还是会上去。实测输入 640x640、batch size 32 时,三个尺度融合比单流多占用约 1.8GB 显存。如果你的卡比较紧张,有几种优化手段。
第一,把 3x3 卷积换成深度可分离卷积。噪声估计分支的两个 3x3 卷积是最大开销,换成深度可分离之后,参数量和计算量都能降到原来的 1/5 左右,精度损失不到 0.3 个点。第二,减少融合尺度,只在 P4 和 P5 两个高层尺度做融合,跳过 P3。这样显存占用降低约 40%,但小目标精度会损失 1 到 2 个点,适合目标尺寸较大的场景。第三,用半精度训练和推理。框架自带混合精度支持,CGAFusion 里的 sigmoid 和乘法在 FP16 下没有精度问题,直接开就行。
4.4 噪声鲁棒性怎么量化评估
很多人做完融合模块,只在干净测试集上报告 mAP,这是不够的。跨模态融合的核心价值恰恰体现在退化场景里,所以我建议每次实验都做一组噪声鲁棒性测试。
我的评估方式是对测试集中的每个模态分别注入不同强度的噪声,比如红外加 5%、10%、15% 的椒盐噪声,可见光加不同程度的高斯噪声,然后绘制 mAP 随噪声强度变化的曲线。这个曲线能直观看出模块在噪声干扰下有没有保持优势。如果加了模块之后,干净精度提升了,噪声一多立刻掉回原点,说明网络只是过拟合了数据分布,没有真正学到噪声抑制。我自己的经验是,至少在三个噪声档位下看趋势,而不是只看一个档位。
最后分享一个调试小技巧:训练过程中把 CGAFusion 的融合权重可视化,输出成热力图叠加在原始图上。如果一个本来很清晰的区域权重一直在 0.5 附近抖动,多半是温度系数或门控初始化有问题;如果红外坏点区域权重始终很高,说明噪声掩码没学会。把可视化加到验证流程里,定位问题比看 loss 曲线快得多。这招帮我省了大量的试错时间,你也可以试试。