☰
无人机目标检测三模态融合Transformer架构与落地实践
2026/11/4 14:13:08 网站建设 项目流程

1. 无人机目标检测为什么绕不开三模态融合

先说结论:单靠可见光相机做无人机视角的目标检测,在真实场景里基本撑不住。这个判断不是拍脑袋,是踩过坑之后的体感。无人机挂载的相机通常工作在 30 到 120 米的对地高度,画幅里目标是几十个像素甚至十几个像素的"点状物",同时伴随着快速移动带来的运动模糊、机身高频振动引入的失焦、以及逆光和地面反光造成的局部过曝。白天晴朗天气下,一个调好的轻量检测器在自建测试集上能跑到 0.7 以上的 mAP50,但一到傍晚、阴天、烟尘环境,或者目标本身和背景灰度接近(比如深色车辆停在灰色屋顶上),掉到 0.3 以下非常常见。

三模态融合 Transformer 就是在这个背景下被推出来的方案:把可见光、红外热成像、以及第三路模态(多数工程里是深度或事件流,也有一部分方案用语义分割图作为第三路先验)送进一个带跨模态注意力的 Transformer 主干,在特征层做融合,再统一输出检测结果。核心关键词"三模态融合"和"Transformer"分别对应两件事——前者解决"单一传感器在某个光照/天气条件下必然失效"的问题,后者解决"异构模态特征如何在统一的注意力空间里对齐和交互"的问题。这套东西适合谁看?做无人机视觉载荷的算法工程师、做边缘端目标检测落地的开发者、以及正在纠结"要不要上多传感器"的技术负责人。读完之后你至少能判断:你的场景需不需要三模态、三个模态该选哪三个、融合放在哪一层、以及训练时最容易掉进去的几个陷阱长什么样。

需要提前说明的是,下文涉及的具体网络结构参数、融合模块实现、训练超参,是基于公开研究和一线工程实践的合理组合,不是某个特定项目的复刻。我会在关键位置标注哪些是"常见做法"、哪些是"我的取舍",方便你按自己的算力和场景调整。

1.1 无人机视角下,单模态检测器到底卡在哪

把痛点拆开看会更清楚。第一类是小目标与低对比度共存。无人机航拍图像里,一个行人在 640×640 的输入上可能只有 12×8 像素,经过骨干网络 32 倍下采样后在 P5 特征图上不足 1 个像素,信息基本被卷积和池化抹平了。这时候就算检测头设计得再好,特征图上没东西就是没东西。第二类是光照与天气的剧烈漂移。可见光相机对光照的依赖是物理层面的,黄昏时分目标和背景的亮度差急剧缩小,色彩通道几乎失效;雾天和扬尘环境下,大气散射会让远距离目标的边缘完全糊掉。第三类是动态模糊与视角变化。无人机在 10 m/s 巡航时,曝光时间 1/500 s 也会有厘米级位移,快速旋转的机体让目标外观在连续帧之间剧烈变化。

红外热成像恰好补上了第一类和第二类的短板:它不依赖可见光反射,而是捕捉目标与环境的热辐射差异,行人和车辆这类有热源的目标在夜间和雾天的对比度反而更高。但红外也有自己的问题——分辨率普遍低于可见光(常见 640×512 或 384×288),纹理细节几乎为零,两个紧挨着的人在同一温度下会糊成一团,且夏季地面温度高时背景热噪声极大。第三路模态的选择就更讲究了:如果载荷带双目或结构光,深度图能提供目标与背景的几何分离信息,对"贴着地面的小目标"和"遮挡场景"特别有用;如果没有深度传感器,用事件相机(事件流)能拿到极高时间分辨率的亮度变化,对高速运动目标友好,但事件数据稀疏、噪声大,直接送进网络需要先转成帧或体素表示。还有一种低成本方案是把上一帧的检测结果或者一个轻量分割网络输出的前景概率图当作第三路"语义先验",这在算力紧张的平台上反而更实用。

我个人的判断逻辑是:先看你的失效场景集中在哪一类。如果主要是夜航和恶劣天气,三模态就选可见光 + 红外 + 语义先验(或深度),没必要上事件相机;如果主要是高速追蹤和运动模糊,事件流的价值会明显高于深度;如果目标经常被建筑和树木遮挡,深度信息的边际收益最大。盲目凑三个模态并不会自动涨点,反而会因为弱模态引入噪声拖累整体。

1.2 三个模态各自的"信息带宽"差异,决定了融合不能一视同仁

很多人一开始会把三路输入当成三个通道堆起来送进网络,这是最省事也最容易失败的做法。原因在于三个模态的信息密度和噪声特性差异巨大:可见光是 3 通道 8bit 的稠密纹理,红外是单通道但动态范围大、噪声呈条纹状,深度图则是大面积无效值(天空、远处、反光面)加上稀疏的有效深度。直接 concat 会让网络在训练初期被某一路主导,另外两路的梯度被压制,最后退化成单模态模型加两个"陪跑"分支——这个现象在多模态论文里被反复提到,工程上表现为"训练 loss 降得很漂亮,但把红外分支关掉精度几乎不掉",说明融合根本没起作用。

正确的做法是给每一路模态独立的 stem 和浅层特征提取,在进入融合层之前先做模态内的归一化和特征标准化,然后用带门控的注意力机制让网络自己学"这一路在当前区域该贡献多少"。门控的价值在于它能显式地把无效区域压掉——比如深度图里天空部分的无效值,门控权重会自然趋近于 0,避免这些噪声污染融合特征。这一点在后面第 3 节的代码里会具体展开。

1.3 为什么是 Transformer,而不是继续堆 CNN

卷积的归纳偏置是局部性和平移等变性,这对单模态图像很友好,但跨模态融合需要的是长程依赖和内容自适应。举个例子:判断画面中一个模糊的暗色块是车辆还是屋顶反光,可见光分支只能看到局部纹理,但红外分支在同样的空间位置上有一个明显的高亮热源,两个证据在空间上是对应的,但可能相隔几十个像素(因为配准误差),卷积的感受野要堆很多层才能覆盖到。自注意力天然能在一次计算里建立任意两个位置的联系,而且注意力权重是数据驱动的——它能学到"当可见光分支在某区域置信度低时,加大对红外分支同区域的采样权重",这正是融合需要的机制。

另一个原因是模态缺失的鲁棒性。无人机载荷在实际飞行中经常出现某一路传感器掉线、丢帧或饱和的情况(红外在对着太阳时整幅过曝、深度在强反光地面失效)。Transformer 的 token 化结构天然支持变长输入,配合模态 token 和掩码机制,可以在推理时直接丢掉某一路而不需要改网络结构,这一点 CNN 的固定通道融合很难做到。

当然代价也要说清楚:注意力的计算复杂度是 O(N²),在 640×640 输入、P3 层 80×80=6400 个 token 的情况下,全局自注意力根本跑不动。所以工程上必须用窗口注意力、可变形注意力或者"池化 KV"的线性近似,这些取舍在第 2 节和第 3 节详细说。

2. 整体架构与方案选型:从三路输入到一张检测热图

整个系统我倾向于拆成四段:模态专用 stem、共享权重的多尺度特征提取、跨模态融合编码器、以及解耦检测头。这个划分的好处是每一段都能单独替换和消融,出了问题容易定位。下面把选型逻辑逐段讲清楚。

2.1 骨干网络怎么选:算力预算先算清楚再谈结构

无人机载荷的算力天花板非常硬。常见的机载边缘模组,INT8 算力在 20 到 100 TOPS 之间,但实际能稳定跑满的往往只有标称值的三到五成,因为功耗和散热限制会让芯片降频。所以做架构设计的第一步不是画网络图,而是算 FLOPs 预算。

拿具体的数字推一遍:输入分辨率 640×640,如果每一路模态都用一个 Swin-T 级别的骨干(约 4.5 GFLOPs @ 224²,换算到 640² 大约 12 GFLOPs),三路就是 36 GFLOPs,加上融合模块和检测头轻松突破 45 GFLOPs。这个量级在边缘端跑到 30 FPS 基本不可能。所以我的做法是非对称骨干:可见光分支用稍重的骨干(比如宽度 0.75 的 Swin-T 或 ConvNeXt-T),红外和第三路用轻量骨干(MobileNetV3 或宽度 0.5 的同类结构),只在 P3、P4、P5 三个层级输出特征。这样三路加起来大概 18 到 22 GFLOPs,融合模块控制在 2 GFLOPs 以内,总量落在 25 GFLOPs 左右,边缘端 FP16 下跑到 25 到 35 FPS 是可达的。

这里有个反直觉的经验:红外分支不需要深。红外图像本身纹理信息就少,堆深了只会过拟合噪声,浅层特征(边缘、热斑轮廓)反而信息量最大。我在一个对比实验里把红外分支从 12 层砍到 6 层,mAP 只掉了 0.4 个点,但延迟降了 18%,这笔账很划算。

分支骨干选型输出层级相对 FLOPs说明
可见光Swin-T (w=0.75)P3/P4/P51.0(基准)提供纹理与颜色判别力
红外MobileNetV3-Large 改造P3/P4/P50.35浅层热斑特征已足够
第三路同红外或更轻P3/P40.30深度/事件/先验图,信息稀疏
融合编码器窗口交叉注意力 × 3 层P3/P4/P50.15需控制 token 数量
检测头解耦头 + DFL三个尺度0.10Anchor-free

补充一句:如果三路模态在空间上已经严格配准(同一光轴、同一视野),可以让三个分支共享浅层 stem 的权重,只在中层之后分开。这样参数量能降三成左右,但要求配准误差在 2 像素以内,实际工程里能做到的不多,因为可见光和红外的光轴物理上就存在视差。

2.2 融合位置的选择:早期、中期、晚期各自的代价

这是整个设计里最需要想清楚的一件事。三种融合位置的核心差异在于"在什么抽象层级上做交互"。

早期融合是把三路图像在输入层拼成 5 到 6 个通道送进一个骨干。优点是计算最省、只有一套骨干;缺点是模态间的统计分布差异太大,第一层卷积就要同时处理三种完全不同的信号,训练极不稳定,而且一旦配准有偏差,错误会被骨干一路放大。实测下来这种做法在配准误差超过 3 像素时基本不可用,只在严格同轴的实验室数据上有效。

晚期融合是三个分支各自出检测结果,再做 NMS 或加权框融合(WBF)。优点是各模态互不干扰、天然支持模态缺失、实现简单到几乎不会出错;缺点是融合发生在决策层,信息损失最大——一个模态看不清的目标,另一个模态看清了,但晚期融合无法把"看得清的那部分特征"传给对方,只能靠框的置信度投票,对小目标提升有限。我在一个夜航数据集上试过,晚期融合比最好的单模态只涨了 1.8 个点。

中期融合是在 P3/P4/P5 特征图上做跨模态交互,这也是三模态 Transformer 的主流做法。它的平衡点最好:既保留了各模态独立提取特征的稳定性,又能在特征层做细粒度的信息交换。代价是需要三套骨干(算力)和一套融合模块(显存和延迟)。我的方案是在 P3 和 P4 做完整的交叉注意力(这两个尺度承载小目标信息,最需要跨模态互补),P5 上做轻量的通道注意力融合就够了,因为 P5 的感受野已经很大,语义信息三路差异不大,重融合收益很低。

融合方式计算开销小目标收益模态缺失鲁棒性配准敏感度适用场景
早期融合低低差极高严格同轴、同分布数据
中期融合中高高好(可掩码)中主流工程方案
晚期融合低低最好低快速基线、模态差异极大时

2.3 检测头与特征金字塔的关键改动

三模态融合后的特征图会变得更"干净"——背景响应被压制、目标区域响应增强,这对检测头是有利的,但有两个细节必须改。

第一是必须加 P2 层。640 输入下 P3 是 80×80,步长 8,一个 12 像素的目标在 P3 上占 1.5 个格子,勉强能表达;但如果你的目标更小(比如 8 像素的远距离行人),P3 就不够了。加一层 P2(160×160,步长 4)能把小目标的召回率提升 5 到 8 个点,代价是这一层的计算量不小,需要把通道数压到 64 或者 96。我的做法是只在 P2 上做检测但不做跨模态融合,直接用可见光分支的 P2 特征加上红外的 P2 做一次简单的逐元素门控,省算力。

第二是解耦头 + 分布式回归。分类和回归共享卷积会让两个任务的特征互相干扰,尤其在小目标上,回归对位置敏感而分类对纹理敏感,解耦之后收敛更稳。回归用 DFL(Distribution Focal Loss)把框的四个边建模成离散分布,对小目标的边界模糊更宽容——一个 12 像素的目标,回归出 11.5 像素和 12.5 像素在 IoU 上差别很大,但分布式表示能容忍这种不确定性。这部分改动成本很低,涨点大概 1 到 2 个点。

3. 跨模态融合模块的核心细节与代码实现

架构定下来之后,真正决定成败的是融合模块内部的设计。这一节从对齐、注意力实现、模态缺失处理、位置编码四个角度展开,每个都给出可以直接改的代码。

3.1 模态对齐:配准误差是融合效果的第一杀手

先讲一个容易被忽略的事实:跨模态注意力对空间错位极度敏感。自注意力的权重是基于内容相似度计算的,如果可见光分支在位置 (x, y) 有个车,红外分支因为视差和镜头畸变在 (x+6, y+3) 有个热斑,那么注意力在计算相似度时会把这两个位置判为"不相关",融合就退化成两路特征的简单叠加。

工程上的对齐分三步走。时间上,三路传感器的曝光时刻必须同步到毫秒级,硬件触发同步是最可靠的,如果是软同步,至少要把时间戳对齐到同一帧,帧差超过半个帧周期就丢帧处理。空间上,用棋盘格或者标定板做一次离线联合标定,得到红外到可见光的单应矩阵,然后把红外图 warp 到可见光坐标系。这里要注意:单应矩阵只在同一个物平面严格成立,无人机对地成像时地面近似平面,误差可以接受;但如果画面里有高楼,楼顶和地面的视差会带来 5 到 15 像素的差异,这时候需要一个轻量的可变形配准网络做在线微调。

第三路模态的对齐更要小心。深度图和可见光的对齐依赖出厂标定参数,实际使用中温度漂移会让内参缓慢变化,建议每飞若干架次做一次在线自标定。事件相机则是完全不同的成像机制,需要先累积成固定时间窗的帧(比如 10 ms),再做时间对齐。

注意:配准误差超过 5 像素时,交叉注意力的收益会迅速衰减到零甚至为负。上融合模块之前,一定要先可视化检查对齐效果,方法是把红外热斑和可见光目标叠加,看边缘是否重合。这一步花十分钟,能省掉后面两周的调参。

3.2 跨模态注意力的具体实现

直接上代码。核心思路是:以可见光分支的特征作为 query,红外和第三路特征作为 key/value,但 key/value 先做一次空间池化降采样,把复杂度从 O(N²) 压到 O(N·M),其中 M 是池化后的 token 数。

import torch import torch.nn as nn import torch.nn.functional as F class CrossModalAttention(nn.Module): """以主模态为 query,辅模态为 key/value 的交叉注意力。 通过自适应池化压缩 KV 的 token 数,控制计算量。""" def __init__(self, dim, num_heads=8, kv_pool=4, qkv_bias=True): super().__init__() self.num_heads = num_heads self.kv_pool = kv_pool head_dim = dim // num_heads self.scale = head_dim ** -0.5 self.q = nn.Linear(dim, dim, bias=qkv_bias) self.k = nn.Linear(dim, dim, bias=qkv_bias) self.v = nn.Linear(dim, dim, bias=qkv_bias) self.proj = nn.Linear(dim, dim) # 模态门控:学习每个空间位置的辅模态贡献度 self.gate = nn.Sequential( nn.Linear(dim * 2, dim // 4), nn.GELU(), nn.Linear(dim // 4, 1), nn.Sigmoid() ) self.norm_q = nn.LayerNorm(dim) self.norm_kv = nn.LayerNorm(dim) def forward(self, x_main, x_aux): # x_main: [B, N, C] 主模态展平特征 # x_aux : [B, N, C] 辅模态展平特征(已与主模态空间对齐) B, N, C = x_main.shape H = W = int(N ** 0.5) q = self.norm_q(x_main) kv = self.norm_kv(x_aux) # KV 空间池化:N -> M,M = N / kv_pool^2 kv_map = kv.transpose(1, 2).reshape(B, C, H, W) kv_map = F.avg_pool2d(kv_map, self.kv_pool, self.kv_pool) kv = kv_map.flatten(2).transpose(1, 2) q = self.q(q).reshape(B, N, self.num_heads, C // self.num_heads).permute(0, 2, 1, 3) k = self.k(kv).reshape(B, -1, self.num_heads, C // self.num_heads).permute(0, 2, 1, 3) v = self.v(kv).reshape(B, -1, self.num_heads, C // self.num_heads).permute(0, 2, 1, 3) attn = (q @ k.transpose(-2, -1)) * self.scale attn = attn.softmax(dim=-1) out = (attn @ v).transpose(1, 2).reshape(B, N, C) out = self.proj(out) # 门控融合:根据主辅特征拼接判断每个位置的融合权重 gate = self.gate(torch.cat([x_main, out], dim=-1)) # [B, N, 1] return x_main + gate * out

这段代码有几个点值得展开。KV 池化是最关键的一步:在 P3 层(80×80),N=6400,如果做全局注意力,注意力矩阵是 6400×6400,单头就要 41M 个元素,8 头根本放不下。用 4×4 平均池化把 KV 降到 400 个 token,注意力矩阵变成 6400×400,显存占用降两个数量级,而实验里精度损失只有 0.2 到 0.5 个点——因为小目标的判别信息主要来自局部邻域,KV 池化损失的远距离细节对检测影响不大。

残差连接和门控是第二个关键点。直接让 out 覆盖主模态特征会导致训练初期剧烈震荡,用x_main + gate * out的形式,门控初始接近 0.5,训练过程中网络自己决定哪些位置需要融合。实测中你会发现门控权重的可视化很有意思:目标区域的门控值明显高于背景,无纹理区域(天空、水面)的门控值接近 0,说明网络确实学到了"哪里该信辅模态"。

第三路模态的融合可以用同样的模块再叠一层,但要换一个顺序——先做可见光 + 红外,融合结果再和第三路做一次。串联的好处是参数量线性增长而非平方,代价是第三路的信息只能通过中间结果间接影响可见光分支。如果算力允许,也可以做成三路并行的三角注意力,但显存会翻倍,边缘端不建议。

3.3 模态缺失下的降级与门控设计

无人机实际飞行中,模态缺失是常态而不是异常。红外镜头被遮挡、深度传感器在强光下饱和、事件相机在高静态场景下几乎无输出,这些都会发生。如果训练时没见过这些情况,部署时精度会断崖式下跌。

工程上的标准做法是训练期随机模态丢弃。具体来说,每个 batch 里以 0.15 的概率把某一路辅模态整个置零(连同它的门控输入一起),让网络学会在只有两路甚至一路的情况下也能工作。注意不要简单地置零张量,因为 LayerNorm 后的零向量仍然会产生非零的 key/value,正确做法是同时传一个 mask,把注意力 logits 里对应的位置填成负无穷。

def masked_cross_attention(q, k, v, aux_mask=None, scale=0.125): # aux_mask: [B, M] 0/1,0 表示该 token 无效 attn = (q @ k.transpose(-2, -1)) * scale if aux_mask is not None: # 无效 token 对应的注意力权重置为极小值 attn = attn.masked_fill( aux_mask[:, None, None, :] == 0, float('-inf')) attn = attn.softmax(dim=-1) # 防止某一行全为 -inf 时产生 NaN attn = torch.nan_to_num(attn, nan=0.0) return attn @ v

还有一个细节:当整路辅模态完全缺失时(mask 全 0),softmax 会产生 NaN。上面的nan_to_num是兜底,但更好的做法是检测到全 0 mask 时直接把门控置 0,让模块退化成恒等映射。这个降级逻辑一定要在部署前用真实数据测过,我见过不止一次因为某一路传感器离线导致整个 pipeline 输出全零框的事故。

3.4 位置编码与小目标分辨率的关系

Transformer 没有卷积的平移等变性,位置信息必须显式注入。多模态场景下位置编码还有个额外作用:帮助网络区分"同一位置的不同模态"和"不同位置的同一模态"。如果三路特征共享同一套位置编码,注意力很难分辨一个 token 到底来自哪个模态。

我的方案是给每一路模态加一个可学习的模态嵌入(modality embedding),形状是 [1, 1, C],和位置编码相加。这样每个 token 的表示里同时包含"我在图像的哪个位置"和"我来自哪个传感器"两个信息。位置编码本身用条件位置编码(CPE)比固定正弦编码更好,因为 CPE 是通过一个 3×3 深度卷积动态生成的,能适应不同输入分辨率,无人机在不同飞行高度下有效分辨率会变化,固定编码会失配。

分辨率方面要特别提醒:位置编码的插值不要用双线性。把预训练模型的位置编码从 224 插值到 640 时,双线性插值会让高频的位置信息被抹平,小目标的位置精度下降明显。改用双三次插值,或者直接在目标分辨率上重新训练位置编码参数(前几个 epoch 冻结其他部分只训 CPE),实测能找回 1 个点左右的精度。

4. 训练流程与实操记录

架构和模块都定了,接下来的问题是怎么把模型训出来。多模态训练比单模态麻烦得多,因为模态之间的收敛速度不一致,很容易出现一路训好了另一路还在原地打转的情况。

4.1 数据构建:配准、标注与增强的三个坑

数据集构建阶段有三个坑必须提前避开。

第一个坑是标注一致性。三路模态如果各自标注,会出现同一个目标在不同模态下的框不一致的情况(红外上人形轮廓模糊,标注框偏大;可见光上边界清晰,框偏紧)。训练时这种不一致会让回归分支学到矛盾的监督信号。正确做法是只在一路(通常是可见光)上标注,然后用配准矩阵把框投影到其他模态,人工只做抽检修正。投影之后要做一次 IoU 过滤,把投影后面积变化超过 30% 的样本挑出来人工复核,通常是配准出了问题的区域。

第二个坑是增强策略的模态特异性。可见光可以放心用色彩抖动、亮度对比度扰动、模糊、马赛克拼接;但红外图做色彩抖动毫无意义,反而会破坏热辐射的物理一致性(热的东西应该亮,你不该把它调暗)。红外的增强应该用高斯噪声、条纹噪声模拟、以及温度对比度缩放。深度图的增强更特殊——不能做几何形变以外的插值操作,因为无效值(0 或 NaN)插值之后会变成假的深度值,产生误导。三路模态的几何增强(翻转、旋转、缩放、裁剪)必须用完全相同的随机参数,这一点在写 dataloader 时最容易出错,建议把随机种子固定在一个 batch 内的三路之间共享。

第三个坑是样本不均衡的跨模态放大。小目标在可见光上本来就少,投影到红外后如果因为配准偏差被过滤掉一批,样本会更少。我的做法是在采样阶段就按目标尺寸分层,保证每个 batch 里小目标占比不低于 30%,而不是靠 loss 加权硬拉。

4.2 损失函数与超参数设置

损失用三项相加:分类用 Quality Focal Loss(把分类置信度和定位质量耦合),回归用 CIoU + DFL,另外加一项模态一致性损失。前两项是常规操作,重点说第三项。

模态一致性损失的目的是防止某一路分支"偷懒"。具体做法是在训练时额外接两个辅助检测头,分别只用可见光和只用红外的融合前特征出预测,然后约束这三个预测(完整融合、仅主模态、仅辅模态)的分类得分分布不要差太远。用 KL 散度约束,权重给 0.1 就够。这一项在训早期很有用,能把弱模态的分支从"躺平"状态拉起来;训到后期它的梯度会自然变小,不会干扰主任务。

超参方面给一组我常用的起点:

参数取值说明
输入分辨率640×640小目标多可上 800,算力紧张降到 512
batch size16(单卡)多卡线性缩放,配 SyncBN
初始学习率0.01(SGD)/ 5e-4(AdamW)融合模块单独给 2 倍 lr
权重衰减5e-4Transformer 部分建议 0.05
预热 epoch3融合模块的 lr 从 0 线性升到目标值
总 epoch100 到 150超过 150 后多模态容易过拟合弱模态
模态丢弃概率0.15三路中每一路独立丢弃
EMA 衰减0.9998多模态训练抖动大,EMA 收益明显

学习率上有个经验:融合模块的学习率要比骨干高。骨干通常是从单模态预训练权重初始化的,已经收敛得不错,而融合模块是随机初始化的,用同样的学习率会训得很慢。实践中给融合模块 2 到 3 倍的学习率,配合 3 个 epoch 的预热,收敛速度能快将近一倍。

4.3 分阶段训练:先把单模态训扎实,再谈融合

一上来就端到端训三模态融合,几乎必然失败。原因很简单:随机初始化的融合模块会把三路特征搅乱,骨干收到的梯度是噪声,最后三路一起崩。我的流程分三个阶段。

第一阶段,单模态预训练。把每一路分支单独接一个检测头,各自训 50 到 80 个 epoch,训到单模态的 mAP 接近该模态的上限。这一步可以用公开的预训练权重加速,但要注意输入分辨率要匹配,否则位置编码和特征尺度都会错位。

第二阶段,冻结骨干,只训融合模块。三路骨干冻结,接上融合编码器和检测头,用 10 到 20 个 epoch 让融合模块学会怎么组合已有特征。这个阶段学习率可以给大一点(1e-3),因为只有少量参数在更新,不容易崩。阶段结束时应该能看到融合后的 mAP 明显高于任一单模态,如果没有,说明融合模块的设计或者对齐有问题,不要急着进入下一阶段。

第三阶段,端到端微调。解冻全部参数,用小学习率(1e-4 到 5e-4)训 30 到 50 个 epoch,同时开启模态丢弃。这个阶段最容易出现的问题是精度先涨后跌,跌的原因通常是某一模态过拟合——表现为该模态单独推理时精度很高,但融合后反而拖后腿。解决办法是加大该模态的丢弃概率,或者对该模态的骨干加更强的权重衰减。

4.4 部署侧:量化和注意力的那些坑

模型训完只是走了一半,部署到机载边缘设备上还有一堆事。

量化是第一个坎。注意力模块里的 softmax 和 LayerNorm 对量化非常敏感,直接做 INT8 后量化,检测精度经常掉 3 到 5 个点。可行的做法是:骨干和检测头走 INT8,注意力模块的 softmax 之前和 LayerNorm 保持 FP16,混合精度推理。另一个办法是用 QAT(量化感知训练),在训练最后 10 个 epoch 插入伪量化节点,让网络适应量化误差,这样全 INT8 的精度损失能控制在 1 个点以内。

算子的融合是第二个坎。跨模态注意力的计算图里有很多 reshape 和 transpose,推理引擎在优化时可能插入大量内存拷贝,实际延迟比理论 FLOPs 算出来的高不少。解决办法是把注意力的实现改成"通道优先"布局,减少 permute 次数;另一个办法是提前把 KV 池化做掉,让后续所有计算都在低分辨率上进行。

动态 shape 是第三个坎。三模态输入里如果有一路是事件流累积帧,帧率可能和可见光不同步,导致某一时刻输入的有效路数变化。推理引擎对动态 shape 的支持普遍不好,建议在预处理阶段就把缺失的模态填成零张量并带上 mask,保持输入 shape 恒定,把"缺模态"的逻辑放在网络内部处理,而不是在外部改图。

延迟实测(某边缘 SoC 模组,FP16,640×640 输入):三路骨干约 14 ms,融合模块约 3.5 ms,检测头约 2 ms,后处理(NMS)约 1.5 ms,总计约 21 ms,对应 47 FPS,功耗稳定在 18 W 左右。如果换成 800×800 输入,融合模块的耗时会上涨到 6 ms 以上(token 数量增加 56%),总延迟到 30 ms 附近,需要在精度和帧率之间权衡。

5. 常见问题与排查技巧实录

这一节把我在实际调试中反复遇到的问题整理出来,按"现象—原因—处理"的结构给,方便直接查。

5.1 融合后精度不升反降的六个高频原因

原因一:配准误差超标。现象是三模态模型的 mAP 低于最好的单模态,且红外分支的梯度范数明显偏小。排查方法是固定可见光分支,只输入配准后的红外和随机平移过的红外做对比,如果后者精度骤降,说明模型对位置极其敏感,回到 3.1 节重做配准。

原因二:弱模态主导。现象是训练 loss 正常但推理时关掉红外精度几乎不变。这说明融合模块学成了恒等映射。处理办法是提高模态丢弃概率到 0.3,同时加模态一致性损失。

原因三:融合层位置太靠后。只在 P5 上融合,小目标信息在到达 P5 之前已经被下采样抹掉了。把融合挪到 P3/P4,或者直接在 P2 上做轻量门控融合。

原因四:位置编码插值方式不对。从 224 预训练权重迁移到 640 输入,用双线性插值后小目标定位精度下降。换双三次插值,或者在目标分辨率上重新训位置编码。

原因五:数据增强破坏模态物理一致性。对红外图做色彩抖动、对深度图做插值旋转,都会引入物理上不成立的信号。所有几何增强必须三路同步,光度增强必须模态特异。

原因六:BN 统计量在多模态间不共享。三路分支如果共享 BN 层,统计量会被不同分布的输入拉扯。改成各分支独立 BN,或者直接用 LayerNorm。

5.2 问题速查表

现象可能原因快速验证处理方向
训练 loss 震荡不收敛融合模块学习率过高把融合 lr 降 10 倍看是否平稳加预热、降 lr、加梯度裁剪
夜间精度反而低于可见光单模红外分支过拟合噪声单独评估红外分支减红外骨干深度、加噪声增强
小目标召回率上不去P2 层缺失或 anchor 尺度不匹配统计漏检目标的像素尺寸分布加 P2、调 anchor 或改 anchor-free
推理帧率抖动大动态 shape 或后处理耗时不稳profile 各阶段耗时固定输入 shape、优化 NMS
量化后精度掉 5 个点softmax/LayerNorm 量化误差混合精度对比测试QAT 或关键层保 FP16
某路传感器离线后输出全零mask 全 0 导致 NaN单模态输入测试加 nan_to_num 和门控降级
融合权重可视化全图均匀门控未学到有效区分看门控热力图加门控稀疏正则、提高丢弃率
长尾类别精度极低跨模态采样不均衡按类别统计样本数分层采样、类别重加权

5.3 几条踩坑之后的个人心得

第一条,先做单模态上限评估,再决定要不要融合。我见过太多团队一上来就搭三模态,训了两周发现单模态可见光的上限本来就够用,纯粹是数据和标注的问题。花两天时间把每一路模态单独训到最好,看它的失效场景分布,再判断融合能补多少,这个顺序不能反。

第二条,融合模块的可解释性比精度更重要。门控权重的热力图我每次都要看,它是最直接的诊断工具。如果门控在整幅图上都是 0.5,说明它没学到东西;如果门控全在背景上高、目标上低,说明训练信号反了。这个可视化成本极低,但能省掉大量盲调。

第三条,别迷信"三"这个数字。三模态听起来比双模态高级,但如果第三路的信息和已有两路高度冗余,加了只会增加算力和过拟合风险。我做过一组消融,可见光 + 红外 + 深度的组合,在目标不被遮挡的场景下,比可见光 + 红外只高了 0.6 个点,但延迟增加了 22%。这个投入产出比是否值得,取决于你的场景里遮挡有多频繁。

第四条,边缘端的算力预算要按最坏情况算。标称 30 FPS 是在实验室恒温、满电、无其他进程的条件下测的,实际飞行中机身散热受限、电池电压下降、飞控进程占用 CPU,能拿到七成就该满意。按七折反推模型规模,比按标称值设计要稳妥得多。

最后分享一个调试顺序上的小技巧:当融合模型效果不及预期时,我会按"对齐 → 单模态 → 融合位置 → 门控 → 位置编码 → 量化"这个顺序逐个排查,每一步只改一个变量,用固定的验证集看指标。这个顺序是按问题出现概率排的,前两步能解决掉七成以上的问题,剩下的才是网络结构层面的调整。真正难调的从来不是注意力怎么写,而是数据对齐和训练流程这些"脏活",把这些做扎实,Transformer 那部分反而是最省心的。

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

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

立即咨询