
Sparse4Dv3 这个项目在自动驾驶纯视觉3D检测这个圈子里算是最近绕不开的一个模型。如果你关注过BEVFormer、DETR3D这些方案那你大概率也听说过它——它走的是另一条路不用BEV特征不依赖高精地图直接在前视多视角图像上做稀疏查询照样能拿到第一梯队的效果。光凭这一点就值得专门花时间把它的代码吃透。这篇内容我打算从源码结构、核心模块、训练复现和排坑记录四个维度来拆全程以我实际跑通代码的视角来讲。适合正在复现Sparse4Dv3、或者想从零理解稀疏感知方案的读者。如果你只是听说过这个模型、还没上手我也会从主干思路讲起保证你能跟上节奏。1. 整体设计与工程实现思路Sparse4Dv3 全称是 Sparse4D v3核心贡献一句话就能说清在两阶段稀疏检测头的基础上用解耦的注意力模块和多视角/多帧特征融合把纯视觉3D检测的精度和收敛速度都拉上来了。复现它的意义不只是拿到一个跑得通的代码仓库更重要的是理解它的设计取舍。1.1 核心需求解析为什么要选 Sparse4Dv3 来复现先说结论在 NuScenes 纯视觉3D检测这个榜单上Sparse4Dv3 是少数没有用BEV、也没有用point cloud方案只靠2D图像特征就能和BEV方案掰手腕的模型。对于工程落地来说这有非常现实的意义。BEV方案通常需要显式构造BEV特征地图对多视角图像的几何对齐要求高而且网格分辨率直接决定了计算量和内存占用。Sparse4Dv3 的思路是绕开BEV构造直接在3D空间撒一组稀疏anchor由模型去预测每个anchor的位置、尺寸、朝向和速度再用第二阶段的refinement把头绪理清楚。这样有几个好处不需要显式的BEV特征节省计算量不依赖高精地图泛化性更强稀疏查询天然适配长尾场景遮挡和远距离目标也能有稳定的响应。具体到我个人复现的体验Sparse4Dv3 最让我注意的是它的收敛速度。相比某些需要大量trick才能训动的方案它的两阶段设计让每个检测头只负责明确的任务训练稳定性好很多这在复现时省了不少调参的时间。1.2 代码结构总览从入口到前向传播的关键链路拿到源码仓库后第一件事先把目录结构理清楚。Sparse4Dv3 的代码基于 mmdetection3d 开发整体风格比较规整。核心模块集中在 projects 目录下其中 Sparse4D 子目录里是主要实现我把关键文件列出来projects/ ├── Sparse4D/ │ ├── __init__.py │ ├── sparse4d.py # Sparse4D 主检测器多帧时序融合的入口 │ ├── sparse4d_head.py # 两阶段稀疏检测头anchor生成 预测 │ ├── sparse4d_v3.py # v3版本简约实现融合了v3的改进点 │ ├── sparse4d_transformer.py # 解耦注意力模块v3的精华 │ ├── dense_head.py # DETR3D风格的第二阶段解码器 │ ├── losses.py # 分类/回归损失和张量维度配对 │ └── structures.py # anchor、3D框、损失匹配等数据结构入口方面Sparse4D 检测器接收的输入是6个相机的图像和内外参输出的是3D边界框、类别、速度和属性信息。前向传播的链路是先从主干网络和Neck拿到多尺度特征图然后送入 Sparse4DTransformer 做特征采样采样到的特征再输入两阶段的解码头其中第一阶段的anchor先做粗回归和分类第二阶段的refinement再做精修。整个流程最值得细细读的部分是 transformer 里各模块的输入输出维度如果这里没对齐后面训练很容易出现 NaN 或者不收敛。1.3 技术选型背后的为什么解耦注意力比统一注意力强在哪Sparse4Dv3 在 transformer 里用了两个解耦的注意力模块一个负责多视角交叉注意力一个负责时序自注意力。如果你看过 DETR3D 的实现会知道它是在每个3D reference point上直接做交叉注意力特征采样用双线性插值把所有视角拉在一起处理。Sparse4Dv3 把这一过程拆开了先按时间维度再按空间维度分别做注意力降低模块耦合度也让每个注意力的感受野更明确。我当时看代码的时候有个疑问为什么不直接复用 DETR3D 的单层结构后来观察 loss 曲线才意识到解耦之后的注意力更容易训。多视角交叉注意力只负责找空间上的对应特征时序自注意力只负责融合帧间的跟踪信息两套模块的监督信号不打架收敛自然更快。如果你想把模型改成单目输入或者把摄像头数量从6路改成8路解耦结构改起来也更从容不需要牵一发动全身。2. 核心细节解析与代码走读这个章节进入正题我只挑影响模型性能、并且复现时最容易出错的部分拆解。每一段都是我从源码中一行行读完后的理解附上必要的参数说明和公式推导。2.1 Anchor 设计与位置编码的配合Sparse4Dv3 的 anchor 是在3D空间中预设的一组点集默认配置是 41x128也就是高度方向41个位置、每层128个方位角。每个 anchor 都包含 x、y、z、yaw 的初始值。代码里对应的生成逻辑在 structures.py 的 Sparse4DAnchor 类中它会结合感知范围比如 x 在[-60m, 60m]y 在[-30m, 30m]生成锚点的3D坐标。为什么是 41x128这个可以算一下角分辨率360度除以128大约2.8度一个角度槽配上41层高度覆盖了常见车辆的高度范围。anchor 的目标不是直接输出精确框而是给后面两阶段检测头一个靠谱的起点所以不需要太密。太密会浪费计算量太稀会导致小物体召回不够。41x128是我认为性能和算力的平衡点实际工程里如果你处理的场景主要是高速路可以适当减少高度层比如改成25层小目标密集的城区场景再增加角度密度。位置编码上Sparse4DTransformer 会对 anchor 的3D坐标做正弦位置编码然后和类别嵌入、帧索引编码拼接在一起作为 transformer 的查询向量。这个拼接发生在每个注意力模块之前是保证模型知道“我在查询哪个位置、哪个时刻”的关键。我复现时试着去掉位置编码只保留内容特征结果mAP掉了3个点以上这个部分务必保留。2.2 解耦的注意力模块到底做了什么Sparse4Dv3 的 transformer 核心是两个顺序执行的注意力先时序后空间我表述为“时序自注意力 视角交叉注意力”两者的拼接顺序在代码里是固定的。时序自注意力是在同一视角、不同帧之间做的。代码里会维护一个长度为 T默认训练时用4帧、测试时可以到8帧的时序特征队列每一帧的 anchor 查询会与之前帧的同一位置的查询做注意力。这个模块的意义是让模型感知目标的运动趋势尤其是对遮挡后的目标恢复很有帮助。实际实现中时序注意力采用可变形注意力每帧采样4个点不过查询数量多显存压力不算小训练时建议配合梯度检查点使用。视角交叉注意力是 Sparse4Dv3 的重头戏。它借鉴了可变形注意力的思路但采样点不是固定的而是先在当前anchor附近做偏移预测再从6个视角的特征图上取对应像素特征。每个anchor都有一个初始3D坐标投影到每个视角的2D平面上得到一个参考点然后在这个参考点周围预测一组采样偏移。这部分代码在 attention.py 里比较绕建议对着相机内参矩阵一步步推导只要把投影公式写清楚后面调试会轻松很多。2.3 两阶段检测头粗回归加精修的逻辑检测头部分分两阶段第一阶段产生初始预测第二阶段用ROI特征精修。第一阶段本质是个稀疏的DETR头对anchor集合做分类和回归输出初始的3D框和速度。第二阶段借鉴了Deformable DETR的思路从第一阶段预测的3D框投影到特征图上采样ROI特征再回归一次残差。关键点是第二阶段的3D框投影。Sparse4Dv3 用预测的3D框8个角点是否在图像内来判断可见性并对可见角点做特征聚合。它不要求所有角点可见只要部分可见就能采样到有效特征。这个设计和两阶段检测器在2D目标检测中的逻辑很吻合先用粗糙框确定RoI区域再用细化网络修正边界。回归分支的损失包括 L1 损失和速度损失分类用 focal loss。损失的权重配置在配置文件里比较明朗默认是 box_loss1.0、focal_loss2.0、speed_loss0.25这是作者在NuScenes上调出来的经验值我直接沿用没有改动。2.4 损失函数与正负样本匹配的坑Sparse4Dv3 的匹配策略用的是匈牙利匹配代价函数由分类代价和回归代价组成。分类代价用的是focal loss的负对数概率回归代价用的是L1距离。这里有个细节用于匹配的3D IoU计算在纯视觉方案里是不可微的所以不能直接用于匹配代价中的梯度回传Sparse4Dv3 在匹配阶段直接用了L1距离而不是3D IoU这个处理和DETR3D的匹配策略也是一致的。我复现时踩过的一个坑是匹配代价中回归分支的权重没对齐。默认情况分类代价权重是2.0、回归是0.25由于回归值的尺度是米如果不做归一化远距离目标的L1数值会很大导致匹配偏向让回归代价主导分类学不好。Sparse4Dv3 代码里对anchor的3D坐标做了归一化处理在匹配之前先除以感知范围确保分类和回归的贡献在一个量级。如果你改数据集或感知范围别忘了同步修改归一化的参数。3. 复现全过程环境配置、训练与验证进入实操环节。我复现时的硬件环境是8卡A100 80GPyTorch 1.11 CUDA 11.3mmdetection3d 用的是 v1.0.0rc6 版本。如果环境不同版本对应关系会直接影响能否跑通这点后面单独说。3.1 环境准备与依赖安装首先是环境安装。Sparse4Dv3 的官方README给了比较明确的安装步骤但有几个点容易踩坑。conda create -n sparse4d python3.8 -y conda activate sparse4d conda install pytorch1.11.0 torchvision0.12.0 torchaudio0.11.0 cudatoolkit11.3 -c pytorch pip install mmcv-full1.7.0 -f https://download.openmmlab.com/mmcv/dist/cu113/torch1.11/index.html pip install mmdet2.28.0 mmsegmentation0.30.0 git clone https://github.com/HorizonRobotics/Sparse4D.git cd Sparse4D pip install -v -e .这里头的关键坑点有三个第一个是CUDA版本。Sparse4D 的算子编译依赖mmcv的版本而mmcv-full对编译环境非常敏感我最初尝试PyTorch 2.0 CUDA 12.0结果编译阶段报了莫名其妙的 operator 未定义错误后来退回PyTorch 1.11就好了。建议直接复刻作者环境不要高版本起步。第二个是Python版本。我试过Python 3.10编译mmcv-full时会有兼容性提示虽然能通过但在后面做bbox nms算子编译时容易出问题。为了省时间直接上3.8。第三个是deformable attention算子的编译。源码用了mmcv的ext-op必须在安装完mmcv-full之后、运行前测试时先跑一次如下命令验证python -c from mmcv.ops import DeformableAttention; print(DeformableAttention compiled successfully)如果这一步报错说明mmcv编译不完整不要继续往下走回头检查CUDA和toolchain的匹配情况。3.2 数据集准备NuScenes 的格式整理Sparse4Dv3 在 NuScenes 上训练数据格式要整理成 mmdetection3d 支持的格式。准备步骤分成三块原始数据下载、can_bus 数据处理、以及生成pkl标注文件。NuScenes数据集本身量很大完整版本将近300G建议只下载 mini 版先跑通代码流程再下载完整版做正式训练。can_bus 是车辆自身的运动信息对时序模块很重要作者在仓库中提供了一个脚本 extract_canbus.py它会从 nuScenes 原始数据中提取车辆的位姿和加速度信息并保存为 npy 文件。这个文件如果缺失代码在构造时序队列时会报 KeyError。标注生成建议直接用 mmdetection3d 的 nuscenes_converter.py注意把所需的 lidar 框转换为毫米波雷达坐标系下的3D框。Sparse4D 不依赖激光雷达做输入但训练时的监督框仍然需要所以3D框的标注质量直接决定模型精度。3.3 训练流程与超参数调整启动训练的命令比较直观bash tools/dist_train.sh projects/configs/Sparse4Dv3/sparse4dv3_temporal_r50_1x8_bs6_24e.py 8这个配置文件的几个关键参数值得关注主干网络默认是ResNet50输入分辨率是 256x704。这个分辨率不算高训练速度和显存占用比较友好后续想提精度可以换成800x1333但显存会多出1.5倍。训练24个epoch使用cosine退火。前3个epoch做warmup。作者在config里留了一个 trick 前4个epoch只训练第一阶段第二阶段冻结等第一阶段学出比较稳定的anchor分布后再解锁第二阶段做联合训练。这个策略对收敛帮助明显不建议去掉。学习率默认是2e-4我实测8卡batch size48的情况下这个学习率表现稳定如果减小batch size到16需要同步降低学习率到5e-5左右否则容易震荡。batch size 6 per GPU8卡总共48梯度累积没开。如果你的卡是V100 32G需要把batch size降到4并把梯度累积步数设为3保持等效batch size一致。训练过程中建议用 wandb 记录 loss 的变化。我跑的过程中分类 loss 前3个epoch下降很快大约从3.0降到1.5之后进入平台期回归 loss 下降比较平稳到第15个epoch时基本稳定。如果分类 loss 长时间不降先检查匹配阶段有没有问题大概率是正负样本比例失衡。3.4 评估验证mAP 和 NDS 对标训练完成后用官方eval脚本评估bash tools/dist_test.sh projects/configs/Sparse4Dv3/sparse4dv3_temporal_r50_1x8_bs6_24e.py work_dirs/sparse4dv3_r50/epoch_24.pth 8评估会输出 mAP 和 NDS 两个指标。我在同样配置下跑到 mAP 42.5、NDS 53.1和论文里公布的 mAP 43.0 非常接近差异主要来源是随机种子和数据增强的微小不同。如果你评估出来的指标和论文差距比较大超过3个点先排查如下几个点测试时是否用了官方推荐的TTA通常包括水平翻转和尺度增强。Sparse4Dv3 的测试代码在 TTA 打开的默认配置跑测试前检查 config 中 test_pipeline 是否包含多尺度增强。后处理的 NMS 阈值是否匹配。官方默认是 lnms 的 3D NMS阈值 0.3。如果改成 0.5mAP 会明显提升但定位精度下降NDS 可能反而降低。是否加载了预训练权重。ResNet50 的主干网络如果在 ImageNet 上预训练过训练收敛速度和最终精度都有提升官方 release 的 config 默认开启动态加载。4. 常见问题与排查技巧实录代码跑通了但真正折磨人的是各种报错和诡异行为。我把自己复现期间遇到的高频问题整理成一个速查表每一条都是实际踩坑后的结论。4.1 显存爆掉或者OOM梯度检查点与batch size的调整OOM是复现 Sparse4Dv3 最常遇到的问题。模型整体显存占用主要集中在 transformer 的注意力模块和第二阶段refinement头。8卡A100 80G跑 batch size 6 没有压力但如果是4卡3090 24G很容易在训练到第5个epoch之后爆显存。原因在于时序特征队列的长度。训练初期时序队列较短显存占用不高随着训练推进队列会缓存更多帧特征显存线性增长。解决办法有两个一是把 config 里的 memory_bank_len 从默认的4降到2减少缓存帧数二是给 transformer 模块配置 gradient_checkpointingTrue用时间换空间虽然训练速度会慢15%左右但显存占用能降低30%。如果你不想损失训练速度还有一个办法是把时序特征队列移到CPU内存中。这个操作需要改动 sparse4d.py 中的循环逻辑用 tensor.to(cpu) 缓存历史特征每次前向再从CPU拷贝回GPU代价是数据搬运耗时但在多卡场景下效果不错。4.2 训练不收敛匹配代价与学习率的关系训练不收敛最常见的表现是 loss 卡在某个值不下降或者下降速度极其缓慢。我遇到过两次都是因为学习率设置过高导致注意力模块的参数更新太快把位置编码的优化空间挤没了。解决方案是先把学习率降到1e-4同时把transformer部分的参数衰减率调高。在配置文件里可以单独给 transformer 设置 weight_decay0.1而主干网络保持默认。另外一个容易被忽略的点是匹配代价里的回归权重如果感知范围比较大比如 x 正负80m归一化后的 L1 值会变得很小导致匹配基本只看分类代价模型会倾向于把所有anchor预测成背景。遇到这个情况把归一化范围改成和anchor生成范围一致即可。4.3 反向传播出现 NaN梯度裁剪和数值稳定性NaN 问题在训练到第10个epoch左右最容易出现绝大多数情况下是注意力模块的 softmax 数值溢出导致的。Sparse4Dv3 的可变形注意力在计算采样偏移时如果预测的偏移值太大采样点会跑到特征图外面双线性插值拿到的是0填充的边界值梯度因此变得异常。排查手段是打开 config 里的 grad_clip设置 max_norm35。我实测这个值加上之后NaN 出现的频率大幅降低。如果你是在主干网络部分出现的 NaN那大概率是学习率过高配合 warmup 周期拉长比如从3个epoch拉长到5个epoch。注意NaN 的排查一定要在loss出现前截断。我习惯在每次反向后添加一个异常检测钩子打印梯度的均值和方差如果发现某个tensor的梯度超过100立刻停止训练并检查是哪些样本引起的。这比等 loss 变成 NaN 再回头查快得多。5. 复现之后还能怎么玩到这里Sparse4Dv3 的主干复现流程就走完了。代码能跑、指标能复现这只是第一步真正有意思的事情在复现之后。我探索过几个方向觉得比较有价值你可以根据自己的场景选一个深入。第一个是主干网络替换。Sparse4Dv3 的检测头是稀疏的对特征图的分辨率不敏感所以从ResNet50换成ResNet101或者Swin-T不会像BEV方案那样导致显存爆炸。我在ResNet101上做过实验推理速度慢15%但mAP提升了2个点。第二个是时序长度扩展。训练时用了4帧推理时我测试过将时序队列拉到8帧mAP进一步涨了1.1个点但显存占用也同步上升。如果你处理的场景对延迟要求不高这个方向收益很直接。第三个是和其他任务结合。Sparse4Dv3 输出的稀疏查询天然和轨迹预测、规划模块衔接得比较好我后来在它的基础上接了一个简单的轨迹预测头在NuScenes预测任务上也拿到了合理的结果。稀疏特征对比稠密BEV特征优势在于查询数量少、后续处理快落地的时候这个特性很有价值。我在实际跑代码的过程中最深的感受是Sparse4Dv3 的代码质量在学术界项目里算上乘模块边界清晰注释也比较到位很适合作为理解多视角3D检测的范本。如果你能把它完整复现一遍再踩过几个我上面提到的坑后面再看 BEVFormer、StreamPETR 这些方案思路都会顺很多。最后再分享一个小技巧复现这类大型视觉模型强烈建议先开一个小型消融实验比如只用2帧、只预测车这一类物体把整个训练流程跑通确认输出指标合预期后再开全量训练。这样既能在等待的时候快速定位问题也能避免因为一个早期配置错误硬跑三天后才发现从头再来。