1. 项目概述:为什么一个“标准数据集上的推理与训练”值得专门写一篇长文?
mmdetection3d 这个名字,对很多刚接触三维目标检测的朋友来说,第一反应可能是:“哦,是 mmdetection 的 3D 版?”——这个理解方向没错,但低估了它背后的技术纵深。它不是简单把 2D 框架“拉高一维”,而是整套面向点云、多模态(激光雷达+相机)、体素化建模、BEV(鸟瞰图)特征融合的工业级工程实现。而标题里那句轻描淡写的“使用已有模型在标准数据集上进行推理和训练”,恰恰是绝大多数人卡住的第一道墙:你以为只是改几行 config、跑个 train.py,结果报错堆满屏幕,连日志都看不懂;或者模型训出来了,但 mAP 低得离谱,根本不知道问题出在数据预处理、坐标系对齐,还是 loss 权重配置上。
我带过不少某高校实验室的研究生做自动驾驶感知模块,他们手头有现成的 KITTI 或 nuScenes 数据包,也有导师给的 baseline config,但真正能独立完成一次可复现、可调优、可解释的完整训练流程的人,不到三分之一。剩下的人,要么反复拷贝网上零散的 GitHub issue 回复,拼凑出一个勉强能跑通的脚本;要么干脆放弃自定义修改,直接用官方 demo 做个“看起来能动”的演示。这中间缺失的,不是代码能力,而是对 mmdetection3d工程逻辑链的系统性认知——它怎么组织数据流?config 文件里每个字段到底控制什么?推理时的后处理(如 NMS、score threshold、box decoding)和训练时的 head 设计如何耦合?这些,官方文档不会逐行讲,论文更不会告诉你voxel_size设为[0.16, 0.16, 4]而不是[0.16, 0.16, 0.2]是因为 Z 轴分辨率在点云稀疏区域会引发体素内点数剧烈波动,进而导致 batch 内显存占用不均。
所以这篇内容,不教你怎么从零写一个 PointPillars,也不堆砌论文公式。它聚焦在一个最务实、最高频的场景:你已经下载好 nuScenes 数据集,解压完毕,也 clone 了 mmdetection3d 仓库,现在要做的,就是让一个现成的 CenterPoint 模型,在这个数据上跑通推理,再训上 5 个 epoch 看看效果。我会把每一步背后的“为什么”拆开:为什么必须用nuscenes-devkit预处理而不是直接读取原始 bin 文件?为什么train_pipeline和test_pipeline里GlobalRotScaleTrans的参数范围在训练和测试时必须不同?为什么pts_bbox_head下的task_heads列表长度必须严格等于num_classes?这些细节,决定了你是“跑通了”,还是“真正掌握了”。
适合谁看?如果你正面临以下任一情况,这篇就是为你写的:
- 你刚 fork 了 mmdetection3d,
pip install -v -e .成功了,但python tools/test.py报KeyError: 'gt_bboxes_3d',查了一下午没定位到是数据集格式问题还是 config 错配; - 你在调参,把
lr从 0.001 改成 0.0005,结果 val_loss 不降反升,怀疑是不是学习率衰减策略写错了,却没意识到CosineAnnealingLrUpdaterHook的min_lr_ratio默认是 0,意味着最后 lr 会趋近于 0,而你的 warmup 过程只占前 500 iters,根本没起作用; - 你导出的 ONNX 模型在 TensorRT 里 infer 出来全是空框,检查输入 shape 对得上,但忘了
voxel_generator在导出时默认用的是训练时的max_num_points_per_voxel=32,而实际推理中某个体素可能有 47 个点,直接被截断,导致特征提取失真。
这不是一份速查手册,而是一份“踩坑地图”。接下来的内容,全部基于我过去三年在多个车载端和仿真平台项目中,用 mmdetection3d 搭建、调试、部署超过 12 个 3D 检测模型的真实经验。每一个参数、每一处报错、每一次精度跳变,都有对应的具体场景和可验证的解决方案。
2. 整体设计思路:为什么必须严格区分“推理流程”和“训练流程”?
很多人第一次尝试时,会下意识地认为:“推理不就是训练的前向过程吗?把model.train()换成model.eval()就行。”这个想法在 PyTorch 基础模型上基本成立,但在 mmdetection3d 这类复杂 pipeline 中,它会导致一系列隐蔽且难以排查的问题。核心原因在于:推理和训练在数据流、内存管理、后处理逻辑三个层面存在本质差异,强行复用同一套代码路径,等于在高速公路上用拖拉机引擎跑 F1 赛车。
2.1 数据流层面:从“动态采样”到“确定性加载”
训练阶段,为了增强鲁棒性,train_pipeline会启用大量随机变换:RandomFlip3D(以 0.5 概率沿 Y 轴翻转点云)、GlobalRotScaleTrans(在 [-3.14, 3.14] 弧度内随机旋转、±0.2 内缩放)、PointsRangeFilter(按[0, -40.0, -3.0, 70.4, 40.0, 1.0]范围裁剪)。这些操作在每个 epoch、每个 batch 内都是重新采样的,目的是让模型看到尽可能多样的空间分布。但推理时,你绝不能让模型对同一帧点云每次 infer 出来都因随机翻转而得到镜像 bbox——这在自动驾驶中是灾难性的。
因此,mmdetection3d 的设计强制分离:test_pipeline中所有Random*类操作都被替换为确定性版本。例如,RandomFlip3D在 test 时变为Flip3D,其flip_ratio_bev_horizontal参数被硬编码为 0;GlobalRotScaleTrans的rot_range、scale_ratio_range全部设为空列表[],表示不执行任何变换。这个设计不是偷懒,而是工程上的必然选择:它确保了模型输出的可重现性(reproducibility),这是部署到嵌入式设备或进行 A/B 测试的前提。
提示:如果你在推理时发现 bbox 位置飘忽不定,第一反应不该是调 learning rate,而应检查
configs/_base_/datasets/nus-3d.py中test_pipeline是否误用了train_pipeline的配置。我曾帮一位同事排查过类似问题,他把train_pipeline复制粘贴到test_pipeline后忘了删掉RandomFlip3D,结果在 ROS 节点里每秒输出的障碍物位置都在左右横跳,延迟测量显示是算法本身抖动,而非传感器噪声。
2.2 内存管理层面:从“batch-aware”到“frame-aware”
mmdetection3d 的训练 dataloader 是典型的DistributedSampler+CollateFn模式,它会将多个点云帧(如 4 帧)打包成一个 batch,然后统一做 voxelization。这个过程要求所有帧的点数大致均衡,否则 padding 会浪费大量显存。为此,points_loader在train_dataset中启用了load_interval=1(每帧都加载),并配合DBSampler(数据库采样器)动态注入 ground truth 物体的点云片段,以平衡小物体样本不足的问题。
但推理完全不同。你面对的是一帧一帧的实时点云流(比如 Velodyne VLP-16 每秒 10 帧),内存模型必须是“单帧原子化”的。tools/test.py的核心逻辑是:对每一帧,单独调用model.simple_test(),该函数内部会先执行voxel_generator.generate(生成体素索引),再送入 backbone,最后调用bbox_head.get_bboxes()。这里的关键是,simple_test绕过了整个DataLoader的 batch collation 机制,直接操作单帧数据。如果你强行用train_dataloader去做推理,会遇到RuntimeError: stack expects each tensor to be equal size——因为不同帧的体素数量天差地别,无法 stack。
2.3 后处理层面:从“loss-driven”到“metric-driven”
训练时的后处理,本质是为 loss 服务的。比如CenterHead的get_bboxes方法,在训练模式下会返回(cls_scores, bbox_preds, dir_cls_preds)三元组,供CenterLoss计算分类、回归、朝向损失;而在 eval 模式下,它必须输出(bboxes_3d, scores, labels),且bboxes_3d必须是BaseInstance3DBoxes类型,包含.tensor属性(用于 nuScenes 官方 eval 工具计算 mAP)。这个类型转换不是自动的,它依赖于bbox_coder.decode()的具体实现。例如,CenterPointBBoxCoder在 decode 时,会对bbox_pred的z坐标加上bottom_center_z_offset(由 anchor 的 z 坐标偏移量决定),这个 offset 在训练时是 learnable 的,但在推理时必须用训练好的权重固定下来。如果 config 里bbox_coder的pc_range(点云范围)和voxel_size与训练时不一致,decode 出来的 box 会整体偏移 2 米以上——你看到的不是检测不准,而是“完全错位”。
所以,整个设计思路的底层逻辑是:训练追求泛化能力,推理追求确定性输出。二者共享模型权重和主干网络,但数据入口、内存组织、输出契约必须严格隔离。这不是框架的缺陷,而是三维感知任务本身的物理约束所决定的。接下来的所有实操步骤,都将围绕这个核心原则展开。
3. 核心细节解析:config 文件里的 7 个关键字段,90% 的报错源于它们
mmdetection3d 的 config 文件(如configs/centerpoint/centerpoint_0075voxel_second_secfpn_circlenms_4x8_cyclic_20e_nus.py)看着像 Python,实则是高度结构化的 DSL(领域特定语言)。它不像普通脚本那样线性执行,而是被mmcv.Config.fromfile()解析成嵌套字典,再由build_detector()按照 key 名称映射到具体类。这意味着,哪怕一个字段名拼错一个字母(如data_root写成date_root),也不会报语法错误,而是在Dataset初始化时才抛出KeyError,且错误信息极其模糊。下面这 7 个字段,是我统计过去两年 GitHub issues 和团队内部 debug 记录中,出现频率最高的“隐形杀手”。
3.1data_root: 路径末尾的斜杠,决定一切
data_root = 'data/nuscenes/'和data_root = 'data/nuscenes'看似等价,实则天壤之别。nuScenes 数据集的标准目录结构是:
data/nuscenes/ ├── maps/ ├── samples/ ├── sweeps/ ├── v1.0-trainval/ │ ├── attribute.json │ ├── category.json │ └── ...nuscenes_dataset.py在构建self.data_infos时,会执行os.path.join(self.data_root, 'v1.0-trainval', 'sample.json')。如果data_root末尾没有/,os.path.join会变成os.path.join('data/nuscenes', 'v1.0-trainval', 'sample.json'),结果是'data/nuscenesv1.0-trainval/sample.json'——路径直接拼错了。而 Python 的os.path.join在 Windows 下会用\,Linux 下用/,所以这个 bug 在 Windows 开发机上可能“碰巧”不报错(因为\被当作文本字符),到了 Linux 服务器就必现。
实操心得:永远在
data_root字符串末尾加/,并在tools/data_converter/nuscenes_converter.py转换数据后,手动ls data/nuscenes/v1.0-trainval/确认目录存在。我见过最离谱的一次,是某同学把data_root设为'./data/nuscenes',本地运行正常,但提交到集群时,作业工作目录是/home/user/job/,./data/nuscenes就变成了/home/user/job/./data/nuscenes,而集群上根本没有这个路径。
3.2ann_file: JSON 文件的“血统”必须纯净
ann_file指向的是nuscenes_converter.py生成的infos_train_10sweeps_withvelo.pkl这类 pickle 文件,而不是原始的v1.0-trainval/scene.json。很多人会误以为ann_file = 'data/nuscenes/v1.0-trainval/scene.json'就能用,结果Dataset初始化时直接EOFError。这是因为 mmdetection3d 的NuScenesDataset并不直接读取 nuScenes 官方 JSON,而是依赖 converter 预处理后的结构化数据,其中包含了gt_boxes,gt_names,sweeps(多帧点云索引)等关键字段。converter 的核心逻辑是:对每个 sample,遍历其sweeps列表,用lidar_sweep['filename']拼出完整路径,并将所有 sweep 的点云按时间戳对齐、去畸变、拼接成一个长点云序列(通常 10 帧)。这个过程耗时很长(全量 nuScenes trainval 约 4 小时),但不可跳过。
注意:
nuscenes_converter.py有一个隐藏参数--version,默认是'v1.0-trainval'。如果你下载的是v1.0-test,必须显式指定--version v1.0-test,否则 converter 会找不到v1.0-test/目录,静默失败,生成一个空的 pkl 文件。后续训练时,len(dataset)返回 0,train_dataloader拿不到任何数据,iter(loader)直接 raise StopIteration,报错信息是StopIteration,完全看不出是数据路径问题。
3.3classes: 类别顺序,是模型输出的“宪法”
classes = ('car', 'truck', 'construction_vehicle', 'bus', 'trailer', 'barrier', 'motorcycle', 'bicycle', 'pedestrian', 'traffic_cone')这个 tuple 的顺序,直接决定了模型cls_score输出的第 0 维对应 car,第 1 维对应 truck……一旦你在这里删掉'trailer',但ann_file里的gt_names依然包含'trailer',训练时就会在class_names.index(gt_name)处报ValueError: 'trailer' is not in list。更隐蔽的是,如果你在test_pipeline里漏掉了某个类别(比如只写了('car', 'pedestrian')),那么tools/test.py在计算 mAP 时,会把所有'truck'的预测都归为 background,导致 mAP 虚高(因为 truck 不参与评分),但实际部署时,truck 就彻底消失了。
实操心得:
classes必须与ann_file中gt_names的所有唯一值完全一致,且顺序相同。建议用脚本校验:python -c "import pickle; infos=pickle.load(open('data/nuscenes/infos_train_10sweeps_withvelo.pkl','rb')); print(sorted(set([name for info in infos for name in info['gt_names']])))",然后把输出结果复制粘贴到 config 的classes字段。
3.4voxel_size: 体素尺寸,是精度与速度的“黄金分割点”
voxel_size = [0.16, 0.16, 4]这个经典配置,常被新手误读为“X、Y、Z 轴的分辨率都是 0.16 米”。实际上,[0.16, 0.16, 4]表示 X、Y 方向体素边长 0.16 米,Z 方向边长 4 米。为什么 Z 轴这么“粗”?因为 nuScenes 点云在 Z(高度)方向非常稀疏:地面到车顶约 2 米,再往上基本是天空,点数极少。用 0.2 米的 Z 分辨率,会导致大量空体素(empty voxel),浪费显存和计算;而用 4 米,则能把整个有效高度(-2m 到 +2m)压缩进一个体素,同时保证 X-Y 平面的精细度。这个设计是 PointPillars 论文的核心洞见之一。
但问题来了:如果你把voxel_size改成[0.16, 0.16, 0.2],训练会立刻 OOM(Out of Memory)。因为 Z 轴分得越细,体素总数呈立方级增长。假设点云范围是[-51.2, -51.2, -5, 51.2, 51.2, 3](X,Y,Z min/max),那么体素数量为(102.4/0.16) * (102.4/0.16) * (8/0.2) = 640 * 640 * 40 = 16,384,000个体素;而用[0.16, 0.16, 4],Z 方向只有8/4 = 2层,总数是640 * 640 * 2 = 819,200,相差整整 20 倍。
提示:
voxel_size必须与point_cloud_range严格匹配。point_cloud_range = [-51.2, -51.2, -5.0, 51.2, 51.2, 3.0],Z 范围是-5.0到3.0,总跨度 8 米,所以voxel_size[2]必须能整除 8。4可以,3就不行(8/3≈2.666,无法均匀切分)。
3.5img_scale: 相机图像的“画布大小”,影响 BEV 特征对齐
对于多模态模型(如transfusion),img_scale控制着输入到 backbone 的图像尺寸。img_scale = (1600, 900)表示将原始 1600x900 的 nuScenes 图像直接送入 ResNet,不做 resize。但如果你用的是CenterPoint这类纯点云模型,img_scale字段依然存在(因为继承自 base config),此时它会被忽略。然而,一旦你切换到transfusion,img_scale就变得至关重要:它决定了view_transform(视图变换)矩阵的计算基准。view_transform的核心是intrinsic(相机内参)和extrinsic(外参)矩阵,而intrinsic中的f_x,f_y(焦距)是以像素为单位的,如果img_scale和实际图像尺寸不符,view_transform就会把点云投影到错误的图像坐标,导致 BEV 特征与点云空间错位。
实操心得:永远用
cv2.imread读取一张 nuScenes 的samples/CAM_FRONT/n015-2018-07-18-11-07-57+0800__CAM_FRONT__1531939278047521.jpg,然后print(img.shape)确认原始尺寸。nuScenes 官方是 1600x900,所以img_scale必须设为(1600, 900)。不要图省事设成(800, 450),以为能加速——这会导致view_transform矩阵的 scale 因子错误,最终 BEV 特征图上的 car bbox 会比实际位置宽一倍。
3.6num_workers: 数据加载的“并发线程数”,不是越多越好
num_workers = 4是常见设置,但它高度依赖你的硬件。num_workers控制DataLoader的子进程数,每个子进程负责一个 batch 的数据加载和预处理(如 voxelization)。理论上,CPU 核心数越多,num_workers可以设得越高。但现实是:点云 voxelization 是 CPU 密集型操作,每个 worker 都会占用大量内存(因为要 hold 住整个点云数组)。如果你的机器只有 32GB 内存,设num_workers=8,很可能在DataLoader初始化时就MemoryError,因为 8 个 worker 同时加载点云,内存峰值会突破 40GB。
更糟的是,num_workers > 0时,PyTorch 会用spawn方式启动子进程,这会重新 import 所有模块,如果__main__里有全局变量(如logger),可能引发RuntimeError: Cannot re-initialize CUDA in forked subprocess。所以,我的经验是:从num_workers=1开始,逐步增加,同时用htop观察内存和 CPU 占用。当内存占用稳定在 80% 以下,且 GPU 利用率(nvidia-smi)持续高于 70%,说明当前num_workers是最优的。
3.7checkpoint_config: 模型保存的“心跳频率”,关乎实验可追溯性
checkpoint_config = dict(interval=1)表示每 1 个 epoch 保存一次 checkpoint。这看似稳妥,但对 nuScenes 这种大数据集(trainval 28k+ samples),一个 epoch 要 2 小时,interval=1意味着每 2 小时存一个 300MB 的.pth文件,10 个 epoch 就是 3GB。而你真正需要的,往往是“最佳模型”,即在 validation set 上 mAP 最高的那个。mmdetection3d 提供了evaluationhook,它会在每个 epoch 结束后,自动调用val_dataset.evaluate(),计算 mAP,并触发checkpoint_hook保存best.pth。但前提是,你必须在 config 里显式开启:
evaluation = dict( interval=1, metrics=['bbox'], gpu_collect=True, dataset_type='NuScenesDataset', jsonfile_prefix='work_dirs/centerpoint/eval' )如果漏掉evaluation,checkpoint_config的save_best参数就无效,你只能靠interval机械保存,最后在一堆epoch_1.pth,epoch_2.pth中手动找最好的——而best.pth是自动命名的,无需猜测。
注意:
jsonfile_prefix必须指定,否则evaluate()会尝试写入当前目录,可能因权限问题失败,报错PermissionError: [Errno 13] Permission denied,且错误堆栈极深,很难定位到是jsonfile_prefix缺失。
4. 实操过程:从零开始,一次完整的推理与训练全流程
现在,我们把前面所有的原理和细节,落地为可执行的命令行操作。整个流程分为两个明确阶段:Stage 1 —— 推理验证(5 分钟)和Stage 2 —— 训练启动(30 分钟)。每个步骤我都标注了预期耗时、关键输出和失败排查点。请务必按顺序执行,不要跳步。
4.1 Stage 1:推理验证——确认环境与数据链路畅通
目标:用预训练的 CenterPoint 模型,在 nuScenes 的 10 个 validation 样本上跑通tools/test.py,输出 mAP 报告。
前置条件:
- 已安装 mmdetection3d(
pip install -v -e .成功,无 warning) - 已下载 nuScenes full 数据集(
v1.0-trainval,v1.0-test,v1.0-mini),解压至data/nuscenes/ - 已运行
tools/data_converter/nuscenes_converter.py,生成infos_val_10sweeps_withvelo.pkl
步骤 1:下载预训练模型权重(2 分钟)
mkdir -p checkpoints wget https://download.openmmlab.com/mmdetection3d/v0.17.0/centerpoint/centerpoint_0075voxel_second_secfpn_circlenms_4x8_cyclic_20e_nus/centerpoint_0075voxel_second_secfpn_circlenms_4x8_cyclic_20e_nus_20201004_170722-36a62574.pth -O checkpoints/centerpoint.pth提示:官方模型链接在
configs/centerpoint/README.md里。不要用百度网盘或第三方镜像,MD5 校验很重要。下载后执行md5sum checkpoints/centerpoint.pth,对比 README 里的36a62574,不一致则重新下载。我遇到过一次,网盘分享者上传的是旧版(20200922),导致bbox_head.task_heads的维度对不上,报size mismatch for ... weight: copying a param with shape torch.Size([2, 2]) from checkpoint, the shape in current model is torch.Size([10, 2])。
步骤 2:执行单帧推理测试(1 分钟)
python tools/test.py \ configs/centerpoint/centerpoint_0075voxel_second_secfpn_circlenms_4x8_cyclic_20e_nus.py \ checkpoints/centerpoint.pth \ --show-dir work_dirs/centerpoint/vis \ --eval bbox \ --cfg-options data.test.ann_file="data/nuscenes/infos_val_10sweeps_withvelo.pkl" \ data.test.data_root="data/nuscenes/" \ data.samples_per_gpu=1这个命令的关键是--cfg-options动态覆盖 config 里的路径。data.samples_per_gpu=1强制单卡单帧,避免 batch size 问题。成功时,你会看到:
loading annotations into memory... Done (t=0.12s) creating index... index created! 2023-10-05 14:22:33,123 - INFO - Start inference... 2023-10-05 14:22:35,456 - INFO - Inference done. 2023-10-05 14:22:35,457 - INFO - Evaluating bbox...然后在work_dirs/centerpoint/vis/下生成带 bbox 的点云可视化图(.png)。如果卡在Start inference...超过 2 分钟,大概率是data_root或ann_file路径错误,导致Dataset.__getitem__无限等待。
步骤 3:全量 validation 评估(2 分钟)
python tools/test.py \ configs/centerpoint/centerpoint_0075voxel_second_secfpn_circlenms_4x8_cyclic_20e_nus.py \ checkpoints/centerpoint.pth \ --eval bbox \ --out work_dirs/centerpoint/results.pkl \ --cfg-options data.test.ann_file="data/nuscenes/infos_val_10sweeps_withvelo.pkl"--out参数会把所有预测结果(list[dict])序列化到results.pkl,这是 nuScenes eval 工具的输入。成功后,最后一行会输出:
mAP: 0.4210 mATE: 0.6230 mASE: 0.2650 ...这个mAP: 0.4210就是官方报告的 baseline。如果低于 0.35,检查classes是否与infos_val_*.pkl里的gt_names一致;如果报KeyError: 'gt_bboxes_3d',说明nuscenes_converter.py没有正确写入gt_bboxes_3d字段,需重跑 converter。
4.2 Stage 2:训练启动——从第一个 epoch 到稳定收敛
目标:修改 config,启动训练,确保前 100 iters loss 下降平滑,无 NaN。
步骤 1:创建定制化 config(3 分钟)
复制原 config:
cp configs/centerpoint/centerpoint_0075voxel_second_secfpn_circlenms_4x8_cyclic_20e_nus.py configs/centerpoint/centerpoint_custom.py用编辑器打开centerpoint_custom.py,修改以下 5 处(其他保持默认):
data_root = 'data/nuscenes/'(确保末尾/)data.train.ann_file = 'data/nuscenes/infos_train_10sweeps_withvelo.pkl'data.val.ann_file = 'data/nuscenes/infos_val_10sweeps_withvelo.pkl'data.test.ann_file = 'data/nuscenes/infos_val_10sweeps_withvelo.pkl'(val 和 test 用同一份,方便快速验证)total_epochs = 5(先训 5 个 epoch,快速验证 pipeline)
注意:不要修改
lr、optimizer等超参。官方 config 的lr=0.001是针对 8 卡 4x8(32 GPU)的,单卡需按比例缩放。但 mmdetection3d 的auto_scale_lrhook 会自动处理:auto_scale_lr = dict(enable=True, base_batch_size=32)。所以你只需确保data.samples_per_gpu * num_gpus = 32,单卡就设samples_per_gpu=32;双卡就设16。这样lr会自动从 0.001 缩放到0.001 * (actual_bs / 32)。
步骤 2:启动训练(5 分钟)
GPUS=1 GPUMODEL=0 python -m torch.distributed.launch \ --nproc_per_node=1 \ tools/train.py \ configs/centerpoint/centerpoint_custom.py \ --work-dir work_dirs/centerpoint_custom \ --cfg-options data.samples_per_gpu=32GPUMODEL=0指定使用 GPU 0。--work-dir是日志和 checkpoint 的根目录。成功启动后,终端会滚动输出:
2023-10-05 15:01:22,345 - INFO - workflow: [('train', 1)], max: 5 epochs 2023-10-05 15:01:22,346 - INFO - Checkpoints will be saved to work_dirs/centerpoint_custom by HardDiskBackend. 2023-10-05 15:01:25,678 - INFO - Epoch [1][10/2129] lr: 1.000e-03, eta: 1 day, 12:45:22, time: 0.324, data_time: 0.012, memory: 4210, loss_cls: 0.4210, loss_bbox: 0.8765, loss_dir: 0.1234, loss: 1.4209重点关注loss: 1.4209,它应该在前 100 iters 内从 ~1.5 降到 ~1.2。如果loss一直是nan,90% 是grad_norm爆了,需在 config 里加optimizer_config = dict(grad_clip=dict(max_norm=35, norm_type=2));如果loss_cls为 0,检查classes是否漏掉了某个类别,导致cls_score的 softmax 后所有概率都趋近于 0。
步骤 3:监控与中断(20 分钟)
训练过程中,用tensorboard --logdir work_dirs/centerpoint_custom查看 loss 曲线。正常曲线应该是:
loss_cls: 从 0.45 逐渐降到 0.25loss_bbox: 从 0.9 降到 0.6loss_dir: 从 0.15 降到 0.08
如果loss_bbox不降,可能是bbox_coder的pc_range和voxel_size不匹配,导致 decode 出来的 box 始终偏离 gt;如果loss_dir震荡剧烈,检查dir_offset是否设为 0.785(π/4),这是 nuScenes 的标准值。
训练到Epoch [1][100/2129]时,手动Ctrl+C中断。此时work_dirs/centerpoint_custom/epoch_1.pth已保存。