首页 / 资讯中心 / 文章详情

BEVFusion复现全记录:mmdet3d多模态融合踩坑指南

BEVFusion复现全记录:mmdet3d多模态融合踩坑指南 ★ FEATURED ARTICLE
做3D目标检测的绕不开一个老问题——激光雷达和摄像头到底怎么融合纯点云方案在远处小目标和雨雾天确实拉胯纯视觉方案又缺可靠的深度把两边各自的结果硬怼在一起又经常互相打脸。我最近基于mmdet3d把目前很经典的BEVFusion完整复现了一遍从数据准备、环境搭建、训练评估到踩坑排查都走了一轮。这篇不是论文解读就是一份折腾记录写给同样在研究多模态融合、或者正打算在mmdet3d里跑通融合模型的同学。如果你已经对单模态检测有基础但还没摸过BEV融合这篇文章会让你少踩一半的坑。1. 为什么是BEV先把BEVFusion的思路拆透1.1 BEV坐标系本质上是一张“公共地图”BEVBird‘s Eye View鸟瞰图在我眼里就是一个把所有人拽到同一张地图上的坐标系。数据集里3D目标框的标注本身就用俯视图下的中心点、朝向、长宽高来表示激光雷达打在车体上的坐标也天然是这个视角。摄像头成像本来是透视的但通过内外参数可以把像素投影到地面把2D特征“掰”到俯视图上。更关键的是下游的运动规划、轨迹预测模块同样工作在这个坐标系里。如果所有传感器都先把特征提到BEV再做融合前面的表示统一了后面所有任务都会受益。这个选择不是拍脑袋。透视视角下物体大小随距离剧烈变化一个行人在10米远处可能占了100个像素到50米远就缩成了十几像素网络很难用同一组卷积核对不同尺度的目标保持稳定的响应。而BEV视角下目标尺寸基本只跟它本身的长宽有关不随距离剧烈缩放。这意味着检测头的感受野设计、anchor尺寸设置、NMS阈值调整都更容易保持一致性。1.2 传感器融合的三个层级到底该融在哪一层多传感器融合从粗到细可以分成三个层级我常用做饭来类比融合层级典型做法优势劣势类比数据级融合直接把图像像素和点云坐标拼在一起输入网络信息无损数据格式异构网络难学计算量爆炸洗菜前就把菜和米倒进一个锅特征级融合各传感器先各自提取特征在BEV或透视层面对齐后融合兼顾信息量与工程可行性需要设计对齐模块和深度估计各自调料到一半再下锅一起炒目标级融合相机出框雷达出框再用规则或网络合并结果实现简单算力省早期就丢失了底层信息错误无法恢复两盘菜都做好了再拼盘不少量产方案用的是目标级融合因为好解释、好回滚。但只要两边置信度发生冲突——比如视觉在强光下丢了目标雷达在远处漏检——后融合根本没机会互相补偿。特征级融合最大的价值在于雷达分支和相机分支的梯度是互相传递的网络可以学习“哪里该信谁”而不是由工程师手工定规则。BEVFusion走的就是特征级融合路线而且是很彻底的那种。它的图像分支不是简单把2D特征贴到地面上而是对每个像素预测一组离散深度分布把图像特征“抬升”成3D体素特征再沿高度维度压扁成BEV特征图。激光雷达分支则把点云体素化经过Encoder得到同样网格大小的BEV特征。两者在通道维度拼接起来后续接一个轻量的BEV Encoder再送入3D检测头输出目标框、类别和速度估计。1.3 图像分支的“2D到3D”是怎么完成的图像分支的核心是深度分布估计这是整个方案最容易被忽略、却最影响精度的地方。简单解释对图像特征图上的每一个像素位置网络预测它在N个离散深度区间上的概率分布然后用这个概率分布把2D特征“撒”到3D空间。这一步本质上是让网络自己学习这个像素到底在多远的距离上。不需要深度真值只需要检测loss的梯度反传回来网络就会在训练中自己调整深度分布凡是能帮助检测收敛的深度估计方式都会被保留。这样一来每个像素的特征会按概率填充到多个深度位置的体素里再经过一个求和或者加权操作把3D体素压到BEV平面。结果就是相机分支也能输出和雷达分支一样网格对齐的BEV特征。两条分支的BEV特征能直接拼接是因为两者经过了相同的网格尺寸和坐标范围设定不需要再做复杂的跨模态对齐网络。1.4 为什么选择mmdet3d来复现而不是自己从零搭从零复现BEVFusion并不是不可能但需要同时维护数据pipeline、模型实现、训练环、评估脚本和可视化工具工作量会翻好几倍。mmdet3d把这些基础设施都做完了数据集解析、坐标变换、体素化、3D检测头、nuScenes官方评估指标都是开箱即用的。我选择mmdet3d的另一个原因是它的配置体系非常透明。整个模型结构、数据增强策略、优化器参数全部写在config文件里想改哪一行直接改不需要像某些框架那样把逻辑散落在各处代码里。对于“复现”这件事来说可读性好是最重要的——你不需要理解每一行底层代码也能把模型跑起来然后逐步深入。2. 复现路径梳理mmdet3d里数据流是怎么走的2.1 mmdet3d的模块划分和BEVFusion的对应关系mmdet3d的模型组织方式继承了MM系列的风格从数据到损失大体是这条流水线Dataset负责读取原始数据pipeline对每个样本做加载和预处理然后进入Model。Model内部再按主干网络、颈部网络、检测头的结构分开。BEVFusion在mmdet3d里对应的结构非常清晰两个骨干分别处理图像和点云一个专门的融合模块在BEV层拼接特征接上类似CenterPoint或者TransFusion的检测头输出结果。实际打开配置文件你会看到图像骨干通常是一个经过预训练的ResNet系列点云分支采用常见的体素编码结构。复现过程中你需要改动的部分其实很有限常见的操作是调输入图像分辨率、BEV网格大小、骨干网络的通道数。理解了这条流水线后面遇到loss曲线不收敛时才不会连问题出在哪个分支都搞不清楚。2.2 从原始数据到可训练的pkl中间经历了什么数据准备是多模态融合项目里最耗时间的一步没有之一。以nuScenes为例原始数据解压后分为samples、sweeps、maps等目录图像、点云、标注信息分散在不同文件夹还需要通过json文件中的token去关联。mmdet3d的做法是先把这些信息整理成info文件也就是一个巨大的pkl或者json每个样本里保存了图像路径、点云路径、相机内外参、激光雷达到各传感器的外参、标注框和时间戳。我踩过的第一个坑就在这里不同传感器采样频率不一样图像可能比点云快得多。mmdet3d在生成info时会把每个样本最近的sweep点云、相邻图像都关联起来但如果原始数据的token对不上或者时间同步字段缺失训练时会出现图像全黑、点云全空之类的问题。所以强烈建议先用官方脚本把数据完整转好再单独抽一个样本打印出来检查确认图像、点云、标注框都在同一坐标下对齐了再进入训练。2.3 复现时我对硬件和时间的基本预期很多同学问我要什么显卡才能复现BEVFusion。我的经验是如果只是推理和可视化一张12GB显存的卡就够如果要完整训练单卡会很痛苦最好有8张24GB左右的卡。BEVFusion的显存开销主要来自图像分支因为要先生成多路相机的图像特征再抬升到3D体素这个过程的中间张量非常大。图像分辨率每翻一倍显存开销接近翻四倍。训练时间上nuScenes完整训练集大概有两万多个样本在8卡环境下一个epoch可能需要20到30分钟完整训练几十个epoch需要一天以上。我第一次复现时天真地想用单卡把整个流程跑完后来发现光是等eval就让人崩溃。如果你手里只有单卡方案也很明确先减小输入分辨率、调小batch size或者直接先跑mini训练集验证流程可用再用公开权重做推理最后再上全量训练。3. 从环境到训练一步步把BEVFusion跑起来3.1 环境搭配这件事版本比什么都重要mmdet3d对环境版本非常敏感尤其是mmcv和PyTorch的版本组合。我复现时验证过一套相对稳定的搭配基本是Python 3.8、PyTorch 1.8或1.9、对应的mmcv-full版本、以及配套的mmdet和mmseg版本。推荐用mim来安装mmcv-full它会根据你当前的PyTorch和CUDA版本自动选择预编译包省去手工编译的麻烦。环境安装的核心命令大致是这样conda create -n bev python3.8 -y conda activate bev pip install torch1.8.1cu111 torchvision0.9.1cu111 -f https://download.pytorch.org/whl/torch_stable.html mim install mmcv-full1.4.0 pip install mmdet2.14.0 mmsegmentation0.14.1装完mmdet3d本体后建议用pip install -e .以可编辑模式安装这样后续改了包内代码不需要重新安装。这里有个容易被忽略的小问题mmcv、mmdet、mmdet3d三个库的版本必须彼此兼容否则会出现导入失败或者算子缺失。查版本兼容性时直接看mmdet3d官方文档里标明的最简版本要求别自己随便搭。3.2 配置文件里的几个关键参数改之前先看懂mmdet3d的配置文件是可以多级继承的常见的形式是一个基础配置定义数据路径和训练策略另一个配置定义模型结构最后合成一个总配置。以BEVFusion为例你需要重点关注的字段有几个。数据相关的字段里data_root指定数据集根目录ann_file指定前面生成的info文件路径。pipeline中的LoadMultiViewImageFromFiles负责加载多路图像LoadPointsFromMultiSweeps负责加载多帧点云。模型字段里pts_voxel_layer定义了点云的体素尺寸和点云范围img_backbone定义图像主干fusion_layer是BEV融合核心。优化器字段里要注意不同分支的学习率可能不同图像分支如果加载了预训练权重通常需要一个更小的学习率否则前期loss容易爆炸。我习惯在真正训练之前先用一个打印脚本把config加载出来确认模型有多少参数、数据流水线每个环节输出的tensor长什么样。这一步能帮你把大多数配置错误暴露在启动训练之前而不是等到训练到一半才报错。3.3 启动训练、断点续训和单卡改法如果一切配置正常启动训练的命令非常简单。多卡环境下用mmdet3d自带脚本bash tools/dist_train.sh configs/bevfusion/bevfusion_nuscenes.py 8单卡环境下直接python tools/train.py configs/bevfusion/bevfusion_nuscenes.py训练中断是常有的事尤其是跑长训练时突然显存溢出或者服务器重启。mmdet3d支持从断点续训python tools/train.py configs/bevfusion/bevfusion_nuscenes.py --resume-from work_dirs/epoch_20.pth这里有一个重要的注意点--resume-from和--cfg-options可以配合使用比如恢复训练时想改学习率可以直接在命令行追加参数不必另存config。我个人的习惯是每5个epoch存一次checkpoint同时保留最近一次的best模型方便后续做模型对比。4. 训练过程中“不讲道理”的几个坑与排查4.1 预训练权重加载不上不是代码坏了是key对不上复现BEVFusion时最常遇到的报错是加载图像主干预训练权重时报missing keys或者unexpected keys。原因很简单mmdet3d加载权重时默认要求checkpoint里的key和模型的key完全对应而预训练权重可能是在另一个框架下训练的key命名风格不一样。比如ResNet在某个框架下保存的key是layer1.0.conv1.weight在mmdet3d里却带了img_backbone前缀。解决办法不是改模型而是写一个简单的权重转换脚本把不需要的分类头参数过滤掉再把剩余key加上对应前缀。核心逻辑是遍历checkpoint里的所有key只保留主干部分对命名空间做映射最后重新保存为一个新的pth文件。转换完成后加载就会非常干净。自己写的时候注意一个细节BatchNorm层的running_mean和running_num_batches_tracked也必须一起转换否则模型收敛会变慢甚至精度有明显下降。4.2 loss震荡和NaN先分头查再动学习率多模态融合模型最常见的训练表现是总loss一直在降但某个分支的loss突然跳到几百甚至变成NaN。排查这种问题我的固定套路是先打印每个loss分量再定位是图像分支还是点云分支出了问题。如果是图像分支的loss先炸大概率是主干学习率太大或者预训练权重没有加载成功。如果是融合模块附近出现NaN要考虑BEV特征里的数值范围是不是相差太大。点云特征经过体素化后通常数值比较小而图像特征如果经过了某个不带归一化的操作可能出现几十甚至上百的量级拼接在一起后梯度就容易异常。解决方案也很务实给两个分支分别设置学习率点云分支1倍图像分支0.1倍或更低另外把训练前几百个iteration的loss打印出来看看初始状态是否稳定。如果初始loss就是NaN那说明数据里存在异常样本比如空点云或者全黑图要回到数据准备环节排查。4.3 显存爆掉的三个常见来源和对应解法显存溢出排在所有复现问题的前三名。BEVFusion的显存消耗大头很明确图像多视角特征抬升3D体素的过程会生成一个巨大的中间张量。算一笔账假设图像分辨率是800×16006路相机特征通道256深度离散化64层这个中间阶乘量是非常恐怖的。如果显存不够我提供三个依次尝试的选择。第一个选择是降低输入图像分辨率这能立刻带来四倍左右的显存节省。第二个选择是减小点云的体素分辨率比如把体素尺寸从0.05米放宽到0.1米点云分支的显存会大幅下降。第三个选择是打开梯度检查点gradient checkpointing用少量重复计算换取显存释放。我实际测试中通常是先降图像分辨率再打开梯度检查点基本能在保证精度的前提下把显存压到单卡可训范围。4.4 评估指标到底看哪个mAP和NDS的关系nuScenes的评估体系比KITTI复杂得多除了常规的mAP还有NDSNuScenes Detection Score它把mAP和目标框的平移误差、尺寸误差、朝向误差、速度误差综合成了一个加权分。复现BEVFusion时如果你只盯着mAP看会发现有些模型mAP很高但NDS不高这通常意味着目标框定位和速度估计不够准。跑评估时用命令python tools/test.py configs/bevfusion/bevfusion_nuscenes.py work_dirs/best.pth --eval bbox输出结果里会同时呈现mAP和NDS。我建议在训练过程中同时记录这两个指标因为它们对超参数的敏感性不同。mAP主要反映分类和召回NDS更关注回归精度。如果你的NDS明显偏低优先检查速度预测分支的设计和目标框回归loss的权重。4.5 一个隐蔽的数据对齐问题图像全黑和点云错位这个坑在单帧推理时几乎不会出现但在训练时偶发并且一旦出现就是一批样本一起出错。症状是训练日志里图像特征全为0但loss没有立刻变成NaN只是整体精度一直上不去。排查方向是数据pipeline里的索引和时间戳。某些数据集的sweep采样和keyframe时间戳不是严格对齐的mmdet3d在定义pipeline时通过LoadPointsFromMultiSweeps后面跟的use_dim、pitch_axis等参数来控制多帧点云的拼接方式。如果索引配置错误训练集会混入大量错位样本。我的做法是在数据生成阶段就加一个断言对每个样本检查点云时间戳是否在图像时间戳前后一个合理窗口内。5. 复现完之后的进一步方向别停在训练出精度5.1 工程化部署从PyTorch模型到实际推理复现出一个高精度模型只是第一步真正落地的难度在部署。BEVFusion在部署时的一个难点是图像分支的深度分布估计过程涉及多个自定义算子导出到TensorRT时不一定都有现成实现。常规解决方案是把深度估计头单独导出提前在推理框架里做一次张量变换或者用CUDA扩展手写算子。还有一个工程化思路是量化。多模态模型在FP16下通常表现稳定但INT8量化需要更仔细的校准。不同模态分支的数值分布差异大最好给图像分支和点云分支分别校准而不是在整个模型上统一做。这块坑很多但它才是决定模型能不能真正上车的关键。5.2 长尾场景别让多模态融合变成“多模态妥协”复现过程中你会发现融合模型在正常场景下确实比单模态强不少但在夜间、雨雾、强逆光等长尾场景下图像分支的深度分布估计会明显退化。这时候融合模型不是自动切换到雷达分支而是可能被图像分支的错误深度带偏。解决思路有两个方向一是对图像分支做更鲁棒的训练比如加入图像退化增强二是在融合模块中学习一个模态置信度让网络在相机不可信时自动降低其权重。我建议在复现之后花时间跑一批夜间样本专门分析BEV特征。你会发现BEV融合后的特征图在夜间并不是均匀退化往往是在图像深度混乱的区域出现特征“污染”。理解了这一点你后续的针对性优化就会非常有的放矢。5.3 BEV特征的价值不该止步于检测既然你已经得到了一组高质量的BEV融合特征完全可以接更多下游任务。比如轨迹预测模块可以直接把BEV特征作为输入替代原先手工设计的栅格化输入或者把BEV特征和地图信息融合起来做在线建图。我在实际项目里把BEV特征同时接入了检测头和一个小型的占位预测头训练后检测精度没有掉多任务反而让BEV特征的表达更稳定。这就是复现经典模型最大的收益你亲手搭起来了一条完整的多模态感知链路后面加什么模块都变得顺理成章。最后再分享两个我自己调试时的习惯一个是训练时把每个loss单独打出来别看一个总loss就以为天下太平另一个是先用mini数据集完整跑通train、eval、可视化这一圈再上全量。这套流程走下来多模态融合这件事从“玄学”变成了“工程”。如果你也正准备在mmdet3d里复现BEVFusion这就是值得一试的路径。
阅读完成 · 觉得有帮助?
咨询建站