1. 为什么 mmdetection v2.x 至今仍值得投入精力如果你最近一两年才接触目标检测,大概率一上来用的就是 mmdetection 3.x 甚至更新的版本。但只要你翻一翻 GitHub 上大量开源项目的 issue 区,或者接手过一些两三年没动过的老仓库,就会发现一个很现实的问题:大量已经落地、已经调好参、已经跑出论文结果的工程,仍然跑在 mmdetection v2.x 上。这不是大家懒得升级,而是 v2.x 和 3.x 之间的配置体系、注册机制、API 命名差异非常大,迁移成本高,很多团队宁愿把 v2.x 的环境锁死。我自己就踩过这个坑。手头一个工业质检的检测项目,模型是两年前基于 v2.x 训出来的,权重、配置文件、数据增强流程全都是 v2.x 那一套。后来想直接迁到 3.x,结果 config 里的img_norm_cfg、train_pipeline、optimizer_config这些字段全变了,光是改配置就花了大半天,最后发现某些自定义的 backbone 注册方式也不兼容。从那以后我就学乖了:老项目就老老实实用 v2.x,别折腾。所以这篇内容我想聊的,就是 mmdetection v2.x 从环境搭建、数据准备、配置修改、模型训练到测试推理这一整条链路。它适合几类人:一是接手了老检测项目、需要快速把模型跑起来的工程师;二是想复现某些经典检测算法(比如 Faster R-CNN、RetinaNet、Cascade R-CNN)论文结果的学生;三是想搞明白检测框架底层配置逻辑、不想只会调 API 的进阶学习者。我会尽量把每一步的“为什么这么做”讲清楚,而不是甩一堆命令让你照抄。需要提前说明的是,v2.x 的版本号跨度也不小,从 2.0 到 2.28 之间有些细节有差异。我下面讲的内容以2.20 到 2.28 这个区间为主,这也是当年最稳定、用得最广的一段。如果你用的是更早的 2.0 附近版本,个别字段名可能略有出入,我会在关键位置提醒。2. 环境搭建:版本对齐是成败的第一步2.1 为什么 v2.x 的环境比想象中难搞很多人觉得装个检测框架不就是pip install一下的事吗?在 mmdetection v2.x 这里真不是。它依赖三个核心组件:PyTorch、MMCV、MMDetection,三者之间有严格的版本对应关系。尤其是 MMCV,v2.x 的 mmdet 对 mmcv 的版本卡得非常死,装错一个小版本就可能出现ImportError或者某些算子找不到的问题。我见过太多人在这一步翻车:随便装了个最新版 mmcv,然后 mmdet 一跑就报ModuleNotFoundError: No module named 'mmcv._ext'。这个报错本质上是 mmcv 的 C++ 扩展没编译好,或者版本和 mmdet 不匹配。所以第一步不是急着装,而是先查对应表。下面这张表是我根据实际使用经验整理的常见组合,可以直接参考:mmdetection 版本推荐 MMCV 版本推荐 PyTorchPython2.20mmcv-full 1.3.171.6 ~ 1.83.7 / 3.82.24mmcv-full 1.4.81.8 ~ 1.93.7 / 3.82.25mmcv-full 1.5.01.8 ~ 1.103.82.28mmcv-full 1.6.01.9 ~ 1.133.8 / 3.9注意:这里的 mmcv 一定要用mmcv-full,不要用精简版的mmcv。精简版没有编译 CUDA 算子,训练时像RoIAlign、DeformConv这些会直接报错。2.2 用 conda 隔离环境的具体操作我强烈建议用 conda 建独立环境,别在 base 环境里折腾。命令大致是这样:conda create -n mmdet2x python=3.8 -y conda activate mmdet2x # 安装 PyTorch,注意 cudatoolkit 版本要和你显卡驱动匹配 conda install pytorch==1.10.0 torchvision==0.11.0 cudatoolkit=11.3 -c pytorch -y # 安装 mmcv-full,用官方提供的预编译包,别自己编译,太慢 pip install mmcv-full==1.6.0 -f https://download.openmmlab.com/mmcv/dist/cu113/torch1.10/index.html这里有个关键点:mmcv-full 的安装链接里,cu113 和 torch1.10 必须和你实际装的 PyTorch、CUDA 版本对应。如果你装的是 cu102 的 PyTorch,链接里的 cu113 就要改成 cu102,否则装出来的 mmcv 和 PyTorch 的 CUDA 版本对不上,运行时照样报错。装完 mmcv 之后,再装 mmdet:git clone https://github.com/open-mmlab/mmdetection.git cd mmdetection git checkout v2.28.0 pip install -v -e .用-e是开发模式安装,好处是你改了源码里的东西能直接生效,方便调试。装完之后验证一下:python -c "import mmdet; print(mmdet.__version__)"能打印出版本号就说明基本 OK 了。2.3 环境验证时最容易忽略的一步很多人装完就急着跑训练,结果一跑就崩。我建议装完后先跑一个官方的推理 demo,确认整个链路是通的:python demo/image_demo.py demo/demo.jpg \ configs/faster_rcnn/faster_rcnn_r50_fpn_1x_coco.py \ checkpoints/faster_rcnn_r50_fpn_1x_coco_20200130-047c8118.pth \ --device cuda:0如果这一步能正常输出带框的图片,说明 PyTorch、CUDA、mmcv 的算子、mmdet 的推理流程全部正常。这一步能帮你排除掉 90% 的环境问题,比直接上训练省心得多。如果这里就报错,那问题一定在环境层面,别往下走。3. 配置文件体系:v2.x 的灵魂所在3.1 继承机制到底怎么工作的mmdetection v2.x 的配置系统是它最优雅也最容易让人懵的地方。它用的是继承 + 覆盖的机制。比如你想训一个 Faster R-CNN,不需要从头写几百行配置,只要这样:_base_ = '../faster_rcnn/faster_rcnn_r50_fpn_1x_coco.py' data_root = 'data/my_dataset/' classes = ('defect_a', 'defect_b', 'defect_c') data = dict( train=dict( ann_file=data_root + 'annotations/train.json', img_prefix=data_root + 'train/', classes=classes), val=dict( ann_file=data_root + 'annotations/val.json', img_prefix=data_root + 'val/', classes=classes), test=dict( ann_file=data_root + 'annotations/val.json', img_prefix=data_root + 'val/', classes=classes))_base_指向的那个基础配置里,已经定义好了模型结构、优化器、学习率策略、数据增强 pipeline 等等。你只需要覆盖数据集相关的部分就行。这种设计的好处是:同一套模型结构可以复用到不同数据集上,改的只是数据部分。但这里有个坑:继承是递归的。基础配置本身可能还继承了更底层的配置,比如faster_rcnn_r50_fpn_1x_coco.py又继承了faster_rcnn_r50_fpn.py和coco_detection.py。所以当你覆盖某个字段时,要清楚它最终生效的是哪一层。我一般会先用工具把完整配置打印出来看:python tools/misc/print_config.py configs/my_config.py这个命令会把继承合并后的完整配置输出,非常有用。改配置之前先打印一遍,能避免很多“我明明改了怎么没生效”的问题。3.2 类别数修改:新手最容易翻车的地方自定义数据集最常见的问题就是类别数不对。COCO 是 80 类,你的数据集可能是 3 类。这时候要改的地方不止一处:model = dict( roi_head=dict( bbox_head=dict(num_classes=3)), # 如果是 RPN 相关的,还要注意 )对于两阶段检测器(Faster R-CNN、Cascade R-CNN),要改的是roi_head.bbox_head.num_classes。对于单阶段检测器(RetinaNet、FCOS),改的是bbox_head.num_classes。改错了地方,训练时不会报错,但 loss 会一直不下降,或者测试时框全乱。我踩过的一个典型坑:改了num_classes但忘了改data里的classes元组,结果训练能跑,但测试时类别名对不上,可视化出来的标签全是错的。所以这两个地方一定要同步改。3.3 学习率与 batch size 的联动调整v2.x 的默认配置基本都是8 卡、每卡 2 张图,总 batch size 16,对应的学习率是 0.02。如果你只有单卡,batch size 变成 2,那学习率也要按比例缩小:new_lr = base_lr * (new_batch_size / base_batch_size) = 0.02 * (2 / 16) = 0.0025这个线性缩放规则在检测任务里是经验法则,不是绝对的,但作为起点很靠谱。配置里这样写:optimizer = dict(type='SGD', lr=0.0025, momentum=0.9, weight_decay=0.0001) lr_config = dict( policy='step', warmup='linear', warmup_iters=500, warmup_ratio=0.001, step=[8, 11])step=[8, 11]表示在第 8 和第 11 个 epoch 时学习率衰减。总 epoch 是 12。如果你数据量小,可以适当减少总 epoch,比如改成 6,step 改成[4, 5]。提示:warmup 在检测任务里很重要,尤其是 batch size 小的时候。它能防止训练初期 loss 爆炸。别嫌麻烦,保留它。4. 数据准备与格式转换的实操细节4.1 为什么推荐 COCO 格式而不是 VOCmmdetection v2.x 支持 VOC 和 COCO 两种主流格式,但我强烈建议用COCO 格式。原因有三:一是 COCO 格式的表达能力更强,支持一张图多个类别、密集小目标;二是 v2.x 里 COCO 格式的评估指标更全(AP、AP50、AP75、APs、APm、APl);三是很多数据增强和 pipeline 对 COCO 格式支持更好。COCO 格式的标注文件是一个大 JSON,结构大致是:{ "images": [{"id": 1, "file_name": "001.jpg", "width": 640, "height": 480}], "annotations": [ {"id": 1, "image_id": 1, "category_id": 1, "bbox": [x, y, w, h], "area": 1234, "iscrowd": 0} ], "categories": [{"id": 1, "name": "defect_a"}] }注意bbox是左上角 x、y 加宽高,不是右下角坐标。这个细节在转换脚本里最容易搞错,一旦搞错,框会整体偏移。4.2 从 VOC 转 COCO 的脚本思路如果你手头是 VOC 的 XML 标注,可以用官方提供的转换脚本:python tools/dataset_converters/pascal_voc.py \ data/VOCdevkit \ --out-dir data/coco_format \ --xml-dir annotations但官方脚本有时候不完全符合你的目录结构,我一般会自己写一个小转换脚本,核心逻辑就是遍历 XML,解析出bndbox,然后按 COCO 结构组装。这里有个经验:转换完之后一定要抽样可视化几张图,确认框的位置和类别都对。我见过太多人转换完直接开训,训了半天发现框全是错的,白白浪费算力。4.3 数据增强 pipeline 的取舍v2.x 默认的 train pipeline 大致是这样:train_pipeline = [ dict(type='LoadImageFromFile'), dict(type='LoadAnnotations', with_bbox=True), dict(type='Resize', img_scale=(1333, 800), keep_ratio=True), dict(type='RandomFlip', flip_ratio=0.5), dict(type='Normalize', **img_norm_cfg), dict(type='Pad', size_divisor=32), dict(type='DefaultFormatBundle'), dict(type='Collect', keys=['img', 'gt_bboxes', 'gt_labels']), ]这里面有几个点值得说。Resize的img_scale=(1333, 800)是 COCO 的标准设置,短边缩到 800,长边不超过 1333。如果你的图片本身很小(比如工业质检里的 512x512),可以适当调小,比如(800, 480),这样能加快训练。Pad的size_divisor=32是因为 FPN 有 5 层下采样,总共 32 倍,所以图片尺寸要能被 32 整除。这个不能省,否则会报维度不匹配。如果你想加数据增强,比如RandomCrop、PhotoMetricDistortion、Mosaic(YOLO 那套),可以插在RandomFlip前后。但要注意:增强不是越多越好。小数据集上堆太多增强反而会让模型学不到东西。我的经验是,先跑 baseline,看指标,再针对性加增强。5. 模型训练:从启动到收敛的完整过程5.1 单卡与多卡训练的启动方式单卡训练:python tools/train.py configs/my_config.py \ --work-dir work_dirs/exp1 \ --seed 42多卡训练(假设 4 卡):bash tools/dist_train.sh configs/my_config.py 4 \ --work-dir work_dirs/exp1 \ --seed 42--seed这个参数我建议一定要加。检测任务对随机种子比较敏感,固定种子能让你的实验可复现。--work-dir指定工作目录,日志、权重、配置文件副本都会存在那里。5.2 训练日志怎么看训练启动后,终端会不断输出日志,格式大致是:Epoch [1][50/500] lr: 2.5e-03, eta: 0:12:34, loss: 0.8234, loss_rpn_cls: 0.1234, ...重点看几个东西:total loss 是否在下降、各分支 loss 是否合理、lr 是否符合预期。如果 total loss 一直震荡不降,可能是学习率太大;如果某个分支 loss 是 NaN,可能是数据里有非法框(比如宽高为 0)。日志还会定期做验证,输出bbox_mAP、bbox_mAP_50等指标。mAP_50 是最直观的指标,一般训练几个 epoch 后应该能看到明显上升。5.3 训练中断了怎么办训练到一半机器崩了、或者你想加 epoch,可以用--resume-from恢复:python tools/train.py configs/my_config.py \ --resume-from work_dirs/exp1/latest.pth \ --work-dir work_dirs/exp1注意latest.pth是最近一个 epoch 的权重,epoch_X.pth是每个 epoch 的存档。如果你要改总 epoch 数,记得同时改total_epochs和lr_config.step,否则学习率衰减的时机就乱了。实操心得:我习惯把checkpoint_config里的interval设成 1,每个 epoch 都存一次。虽然占空间,但万一训练崩了,损失最小。磁盘紧张的话设成 2 或 3 也行。6. 测试与推理:把模型真正用起来6.1 用测试集评估模型训练完之后,用tools/test.py在验证集或测试集上评估:python tools/test.py configs/my_config.py \ work_dirs/exp1/latest.pth \ --eval bbox \ --out results.pkl--eval bbox表示评估检测框指标。--out把结果存成 pkl,方便后续分析。如果只想看指标,不加--out也行。评估结果会打印出类似:Average Precision (AP) @[ IoU=0.50:0.95 | area= all | maxDets=100 ] = 0.412 Average Precision (AP) @[ IoU=0.50 | area= all | maxDets=100 ] = 0.623AP50 到 0.6 以上通常算可用,0.7 以上算不错。当然具体看任务难度,工业质检这种场景往往要求更高。6.2 单张图片推理与可视化评估指标是一回事,实际看效果是另一回事。我一般会用demo/image_demo.py或者自己写个小脚本,把预测结果画出来:from mmdet.apis import init_detector, inference_detector import mmcv config_file = 'configs/my_config.py' checkpoint_file = 'work_dirs/exp1/latest.pth' model = init_detector(config_file, checkpoint_file, device='cuda:0') result = inference_detector(model, 'test.jpg') model.show_result('test.jpg', result, score_thr=0.3, out_file='result.jpg')score_thr=0.3是置信度阈值,低于这个值的框不显示。这个阈值要根据实际场景调:召回优先就调低,精度优先就调高。6.3 批量推理与结果导出实际部署时往往要批量处理图片,这时候可以写个循环:import os from mmdet.apis import init_detector, inference_detector model = init_detector(config_file, checkpoint_file, device='cuda:0') img_dir = 'data/test_images/' for img_name in os.listdir(img_dir): img_path = os.path.join(img_dir, img_name) result = inference_detector(model, img_path) # 把 result 里的 bbox、label、score 导出成 json 或 csv这里有个性能优化的点:inference_detector每次都会重新做预处理,如果图片尺寸一致,可以自己组 batch 推理,速度能快不少。不过 v2.x 的 API 对 batch 推理支持不算特别友好,需要手动改一些东西,这个后面可以单独展开。7. 常见问题与排查技巧实录7.1 训练相关的高频报错报错信息可能原因解决方法CUDA out of memorybatch size 太大或图片分辨率太高减小 batch size,或调小img_scaleloss is NaN学习率过大或数据有非法框降低 lr,检查标注里是否有宽高为 0 的框AssertionError: Thenum_classesof bbox_head should be consistent类别数没改对检查num_classes和classes是否一致KeyError: 'xxx' is not in the models registry自定义模块没注册检查@DETECTORS.register_module()是否加了mmcv._ext not foundmmcv 版本不对或没装 full 版重装对应版本的 mmcv-full7.2 指标不上升的排查思路如果训练 loss 在降但 mAP 不涨,或者涨得很慢,可以按这个顺序排查:数据对不对:抽样可视化几张训练图,确认框和标签正确。学习率合不合适:太大 loss 震荡,太小收敛慢。可以画 lr-loss 曲线看。预训练权重加载了吗:v2.x 默认会加载 ImageNet 预训练 backbone,如果没加载,收敛会慢很多。检查配置里的pretrained字段。评估流程对不对:确认val的ann_file和img_prefix指向正确,类别数一致。是不是过拟合了:训练 mAP 高但验证 mAP 低,说明过拟合,加数据增强或正则化。7.3 几个我踩过的坑坑一:改了配置但没生效。原因是配置继承层级太深,我改的那一层被更底层覆盖了。解决办法就是前面说的,先print_config看完整配置。坑二:多卡训练时 batch size 理解错。v2.x 里samples_per_gpu是每卡的 batch size,总 batch 是samples_per_gpu * num_gpus。我一开始以为samples_per_gpu是总数,学习率调错了,训了半天效果很差。坑三:测试时忘了切 eval 模式。虽然tools/test.py会自动处理,但自己写推理脚本时如果忘了model.eval(),BN 层和 Dropout 会保持训练状态,结果会不稳定。坑四:权重文件加载了但没完全加载。用load_from加载预训练权重时,如果类别数不一致,分类头的权重会被跳过,这是正常的。但如果你发现 backbone 的权重也没加载,那就要检查 key 名是否匹配。8. 一些让训练更顺手的经验补充8.1 日志可视化v2.x 支持 TensorBoard,配置里加一行:log_config = dict( interval=50, hooks=[ dict(type='TextLoggerHook'), dict(type='TensorboardLoggerHook') ])然后tensorboard --logdir work_dirs/exp1就能看 loss 曲线和 mAP 曲线。曲线比数字直观得多,尤其是判断过拟合和欠拟合的时候。8.2 学习率策略的选择v2.x 默认是 step 策略,但 cosine 策略在很多任务上表现更平滑。改法:lr_config = dict( policy='CosineAnnealing', warmup='linear', warmup_iters=500, warmup_ratio=0.001, min_lr_ratio=1e-5)cosine 的好处是不用手动设 step,学习率平滑下降,对超参没那么敏感。我最近几个项目都换成了 cosine,效果稳定。8.3 模型集成的简单做法如果单模型指标差一点,可以试试 WBF(Weighted Boxes Fusion)或者简单的 NMS 集成。v2.x 本身不直接提供 WBF,但可以自己写后处理。思路是把多个模型的预测结果合并,用 WBF 算法融合。这个在竞赛里很常用,能涨一两个点。8.4 导出 ONNX 的注意事项如果要把模型部署到其他平台,可能需要导出 ONNX:python tools/deployment/pytorch2onnx.py \ configs/my_config.py \ work_dirs/exp1/latest.pth \ --output-file model.onnx \ --input-img test.jpg \ --shape 800 1333这里--shape要和训练时的输入尺寸匹配。导出后建议用onnxruntime验证一下输出是否和 PyTorch 一致,避免部署时才发现问题。我个人在实际操作中的体会是,mmdetection v2.x 虽然版本老,但它的配置体系和训练流程设计得非常清晰,把这一套吃透之后,再去看 3.x 或者其他检测框架,会发现底层逻辑是相通的。真正花时间的从来不是敲命令,而是理解每个配置项背后的意图,以及遇到问题时知道往哪个方向排查。最后再分享一个小技巧:每次开新实验前,把配置文件、日志、关键指标记到一个表格里,实验多了之后回头看,能快速定位哪种配置组合效果最好,比凭记忆靠谱得多。
1. 为什么 mmdetection v2.x 至今仍值得投入精力如果你最近一两年才接触目标检测,大概率一上来用的就是 mmdetection 3.x 甚至更新的版本。但只要你翻一翻 GitHub 上大量开源项目的 issue 区,或者接手过一些两三年没动过的老仓库,就会发现一个很现实的问题:大量已经落地、已经调好参、已经跑出论文结果的工程,仍然跑在 mmdetection v2.x 上。这不是大家懒得升级,而是 v2.x 和 3.x 之间的配置体系、注册机制、API 命名差异非常大,迁移成本高,很多团队宁愿把 v2.x 的环境锁死。我自己就踩过这个坑。手头一个工业质检的检测项目,模型是两年前基于 v2.x 训出来的,权重、配置文件、数据增强流程全都是 v2.x 那一套。后来想直接迁到 3.x,结果 config 里的img_norm_cfg、train_pipeline、optimizer_config这些字段全变了,光是改配置就花了大半天,最后发现某些自定义的 backbone 注册方式也不兼容。从那以后我就学乖了:老项目就老老实实用 v2.x,别折腾。所以这篇内容我想聊的,就是 mmdetection v2.x 从环境搭建、数据准备、配置修改、模型训练到测试推理这一整条链路。它适合几类人:一是接手了老检测项目、需要快速把模型跑起来的工程师;二是想复现某些经典检测算法(比如 Faster R-CNN、RetinaNet、Cascade R-CNN)论文结果的学生;三是想搞明白检测框架底层配置逻辑、不想只会调 API 的进阶学习者。我会尽量把每一步的“为什么这么做”讲清楚,而不是甩一堆命令让你照抄。需要提前说明的是,v2.x 的版本号跨度也不小,从 2.0 到 2.28 之间有些细节有差异。我下面讲的内容以2.20 到 2.28 这个区间为主,这也是当年最稳定、用得最广的一段。如果你用的是更早的 2.0 附近版本,个别字段名可能略有出入,我会在关键位置提醒。2. 环境搭建:版本对齐是成败的第一步2.1 为什么 v2.x 的环境比想象中难搞很多人觉得装个检测框架不就是pip install一下的事吗?在 mmdetection v2.x 这里真不是。它依赖三个核心组件:PyTorch、MMCV、MMDetection,三者之间有严格的版本对应关系。尤其是 MMCV,v2.x 的 mmdet 对 mmcv 的版本卡得非常死,装错一个小版本就可能出现ImportError或者某些算子找不到的问题。我见过太多人在这一步翻车:随便装了个最新版 mmcv,然后 mmdet 一跑就报ModuleNotFoundError: No module named 'mmcv._ext'。这个报错本质上是 mmcv 的 C++ 扩展没编译好,或者版本和 mmdet 不匹配。所以第一步不是急着装,而是先查对应表。下面这张表是我根据实际使用经验整理的常见组合,可以直接参考:mmdetection 版本推荐 MMCV 版本推荐 PyTorchPython2.20mmcv-full 1.3.171.6 ~ 1.83.7 / 3.82.24mmcv-full 1.4.81.8 ~ 1.93.7 / 3.82.25mmcv-full 1.5.01.8 ~ 1.103.82.28mmcv-full 1.6.01.9 ~ 1.133.8 / 3.9注意:这里的 mmcv 一定要用mmcv-full,不要用精简版的mmcv。精简版没有编译 CUDA 算子,训练时像RoIAlign、DeformConv这些会直接报错。2.2 用 conda 隔离环境的具体操作我强烈建议用 conda 建独立环境,别在 base 环境里折腾。命令大致是这样:conda create -n mmdet2x python=3.8 -y conda activate mmdet2x # 安装 PyTorch,注意 cudatoolkit 版本要和你显卡驱动匹配 conda install pytorch==1.10.0 torchvision==0.11.0 cudatoolkit=11.3 -c pytorch -y # 安装 mmcv-full,用官方提供的预编译包,别自己编译,太慢 pip install mmcv-full==1.6.0 -f https://download.openmmlab.com/mmcv/dist/cu113/torch1.10/index.html这里有个关键点:mmcv-full 的安装链接里,cu113 和 torch1.10 必须和你实际装的 PyTorch、CUDA 版本对应。如果你装的是 cu102 的 PyTorch,链接里的 cu113 就要改成 cu102,否则装出来的 mmcv 和 PyTorch 的 CUDA 版本对不上,运行时照样报错。装完 mmcv 之后,再装 mmdet:git clone https://github.com/open-mmlab/mmdetection.git cd mmdetection git checkout v2.28.0 pip install -v -e .用-e是开发模式安装,好处是你改了源码里的东西能直接生效,方便调试。装完之后验证一下:python -c "import mmdet; print(mmdet.__version__)"能打印出版本号就说明基本 OK 了。2.3 环境验证时最容易忽略的一步很多人装完就急着跑训练,结果一跑就崩。我建议装完后先跑一个官方的推理 demo,确认整个链路是通的:python demo/image_demo.py demo/demo.jpg \ configs/faster_rcnn/faster_rcnn_r50_fpn_1x_coco.py \ checkpoints/faster_rcnn_r50_fpn_1x_coco_20200130-047c8118.pth \ --device cuda:0如果这一步能正常输出带框的图片,说明 PyTorch、CUDA、mmcv 的算子、mmdet 的推理流程全部正常。这一步能帮你排除掉 90% 的环境问题,比直接上训练省心得多。如果这里就报错,那问题一定在环境层面,别往下走。3. 配置文件体系:v2.x 的灵魂所在3.1 继承机制到底怎么工作的mmdetection v2.x 的配置系统是它最优雅也最容易让人懵的地方。它用的是继承 + 覆盖的机制。比如你想训一个 Faster R-CNN,不需要从头写几百行配置,只要这样:_base_ = '../faster_rcnn/faster_rcnn_r50_fpn_1x_coco.py' data_root = 'data/my_dataset/' classes = ('defect_a', 'defect_b', 'defect_c') data = dict( train=dict( ann_file=data_root + 'annotations/train.json', img_prefix=data_root + 'train/', classes=classes), val=dict( ann_file=data_root + 'annotations/val.json', img_prefix=data_root + 'val/', classes=classes), test=dict( ann_file=data_root + 'annotations/val.json', img_prefix=data_root + 'val/', classes=classes))_base_指向的那个基础配置里,已经定义好了模型结构、优化器、学习率策略、数据增强 pipeline 等等。你只需要覆盖数据集相关的部分就行。这种设计的好处是:同一套模型结构可以复用到不同数据集上,改的只是数据部分。但这里有个坑:继承是递归的。基础配置本身可能还继承了更底层的配置,比如faster_rcnn_r50_fpn_1x_coco.py又继承了faster_rcnn_r50_fpn.py和coco_detection.py。所以当你覆盖某个字段时,要清楚它最终生效的是哪一层。我一般会先用工具把完整配置打印出来看:python tools/misc/print_config.py configs/my_config.py这个命令会把继承合并后的完整配置输出,非常有用。改配置之前先打印一遍,能避免很多“我明明改了怎么没生效”的问题。3.2 类别数修改:新手最容易翻车的地方自定义数据集最常见的问题就是类别数不对。COCO 是 80 类,你的数据集可能是 3 类。这时候要改的地方不止一处:model = dict( roi_head=dict( bbox_head=dict(num_classes=3)), # 如果是 RPN 相关的,还要注意 )对于两阶段检测器(Faster R-CNN、Cascade R-CNN),要改的是roi_head.bbox_head.num_classes。对于单阶段检测器(RetinaNet、FCOS),改的是bbox_head.num_classes。改错了地方,训练时不会报错,但 loss 会一直不下降,或者测试时框全乱。我踩过的一个典型坑:改了num_classes但忘了改data里的classes元组,结果训练能跑,但测试时类别名对不上,可视化出来的标签全是错的。所以这两个地方一定要同步改。3.3 学习率与 batch size 的联动调整v2.x 的默认配置基本都是8 卡、每卡 2 张图,总 batch size 16,对应的学习率是 0.02。如果你只有单卡,batch size 变成 2,那学习率也要按比例缩小:new_lr = base_lr * (new_batch_size / base_batch_size) = 0.02 * (2 / 16) = 0.0025这个线性缩放规则在检测任务里是经验法则,不是绝对的,但作为起点很靠谱。配置里这样写:optimizer = dict(type='SGD', lr=0.0025, momentum=0.9, weight_decay=0.0001) lr_config = dict( policy='step', warmup='linear', warmup_iters=500, warmup_ratio=0.001, step=[8, 11])step=[8, 11]表示在第 8 和第 11 个 epoch 时学习率衰减。总 epoch 是 12。如果你数据量小,可以适当减少总 epoch,比如改成 6,step 改成[4, 5]。提示:warmup 在检测任务里很重要,尤其是 batch size 小的时候。它能防止训练初期 loss 爆炸。别嫌麻烦,保留它。4. 数据准备与格式转换的实操细节4.1 为什么推荐 COCO 格式而不是 VOCmmdetection v2.x 支持 VOC 和 COCO 两种主流格式,但我强烈建议用COCO 格式。原因有三:一是 COCO 格式的表达能力更强,支持一张图多个类别、密集小目标;二是 v2.x 里 COCO 格式的评估指标更全(AP、AP50、AP75、APs、APm、APl);三是很多数据增强和 pipeline 对 COCO 格式支持更好。COCO 格式的标注文件是一个大 JSON,结构大致是:{ "images": [{"id": 1, "file_name": "001.jpg", "width": 640, "height": 480}], "annotations": [ {"id": 1, "image_id": 1, "category_id": 1, "bbox": [x, y, w, h], "area": 1234, "iscrowd": 0} ], "categories": [{"id": 1, "name": "defect_a"}] }注意bbox是左上角 x、y 加宽高,不是右下角坐标。这个细节在转换脚本里最容易搞错,一旦搞错,框会整体偏移。4.2 从 VOC 转 COCO 的脚本思路如果你手头是 VOC 的 XML 标注,可以用官方提供的转换脚本:python tools/dataset_converters/pascal_voc.py \ data/VOCdevkit \ --out-dir data/coco_format \ --xml-dir annotations但官方脚本有时候不完全符合你的目录结构,我一般会自己写一个小转换脚本,核心逻辑就是遍历 XML,解析出bndbox,然后按 COCO 结构组装。这里有个经验:转换完之后一定要抽样可视化几张图,确认框的位置和类别都对。我见过太多人转换完直接开训,训了半天发现框全是错的,白白浪费算力。4.3 数据增强 pipeline 的取舍v2.x 默认的 train pipeline 大致是这样:train_pipeline = [ dict(type='LoadImageFromFile'), dict(type='LoadAnnotations', with_bbox=True), dict(type='Resize', img_scale=(1333, 800), keep_ratio=True), dict(type='RandomFlip', flip_ratio=0.5), dict(type='Normalize', **img_norm_cfg), dict(type='Pad', size_divisor=32), dict(type='DefaultFormatBundle'), dict(type='Collect', keys=['img', 'gt_bboxes', 'gt_labels']), ]这里面有几个点值得说。Resize的img_scale=(1333, 800)是 COCO 的标准设置,短边缩到 800,长边不超过 1333。如果你的图片本身很小(比如工业质检里的 512x512),可以适当调小,比如(800, 480),这样能加快训练。Pad的size_divisor=32是因为 FPN 有 5 层下采样,总共 32 倍,所以图片尺寸要能被 32 整除。这个不能省,否则会报维度不匹配。如果你想加数据增强,比如RandomCrop、PhotoMetricDistortion、Mosaic(YOLO 那套),可以插在RandomFlip前后。但要注意:增强不是越多越好。小数据集上堆太多增强反而会让模型学不到东西。我的经验是,先跑 baseline,看指标,再针对性加增强。5. 模型训练:从启动到收敛的完整过程5.1 单卡与多卡训练的启动方式单卡训练:python tools/train.py configs/my_config.py \ --work-dir work_dirs/exp1 \ --seed 42多卡训练(假设 4 卡):bash tools/dist_train.sh configs/my_config.py 4 \ --work-dir work_dirs/exp1 \ --seed 42--seed这个参数我建议一定要加。检测任务对随机种子比较敏感,固定种子能让你的实验可复现。--work-dir指定工作目录,日志、权重、配置文件副本都会存在那里。5.2 训练日志怎么看训练启动后,终端会不断输出日志,格式大致是:Epoch [1][50/500] lr: 2.5e-03, eta: 0:12:34, loss: 0.8234, loss_rpn_cls: 0.1234, ...重点看几个东西:total loss 是否在下降、各分支 loss 是否合理、lr 是否符合预期。如果 total loss 一直震荡不降,可能是学习率太大;如果某个分支 loss 是 NaN,可能是数据里有非法框(比如宽高为 0)。日志还会定期做验证,输出bbox_mAP、bbox_mAP_50等指标。mAP_50 是最直观的指标,一般训练几个 epoch 后应该能看到明显上升。5.3 训练中断了怎么办训练到一半机器崩了、或者你想加 epoch,可以用--resume-from恢复:python tools/train.py configs/my_config.py \ --resume-from work_dirs/exp1/latest.pth \ --work-dir work_dirs/exp1注意latest.pth是最近一个 epoch 的权重,epoch_X.pth是每个 epoch 的存档。如果你要改总 epoch 数,记得同时改total_epochs和lr_config.step,否则学习率衰减的时机就乱了。实操心得:我习惯把checkpoint_config里的interval设成 1,每个 epoch 都存一次。虽然占空间,但万一训练崩了,损失最小。磁盘紧张的话设成 2 或 3 也行。6. 测试与推理:把模型真正用起来6.1 用测试集评估模型训练完之后,用tools/test.py在验证集或测试集上评估:python tools/test.py configs/my_config.py \ work_dirs/exp1/latest.pth \ --eval bbox \ --out results.pkl--eval bbox表示评估检测框指标。--out把结果存成 pkl,方便后续分析。如果只想看指标,不加--out也行。评估结果会打印出类似:Average Precision (AP) @[ IoU=0.50:0.95 | area= all | maxDets=100 ] = 0.412 Average Precision (AP) @[ IoU=0.50 | area= all | maxDets=100 ] = 0.623AP50 到 0.6 以上通常算可用,0.7 以上算不错。当然具体看任务难度,工业质检这种场景往往要求更高。6.2 单张图片推理与可视化评估指标是一回事,实际看效果是另一回事。我一般会用demo/image_demo.py或者自己写个小脚本,把预测结果画出来:from mmdet.apis import init_detector, inference_detector import mmcv config_file = 'configs/my_config.py' checkpoint_file = 'work_dirs/exp1/latest.pth' model = init_detector(config_file, checkpoint_file, device='cuda:0') result = inference_detector(model, 'test.jpg') model.show_result('test.jpg', result, score_thr=0.3, out_file='result.jpg')score_thr=0.3是置信度阈值,低于这个值的框不显示。这个阈值要根据实际场景调:召回优先就调低,精度优先就调高。6.3 批量推理与结果导出实际部署时往往要批量处理图片,这时候可以写个循环:import os from mmdet.apis import init_detector, inference_detector model = init_detector(config_file, checkpoint_file, device='cuda:0') img_dir = 'data/test_images/' for img_name in os.listdir(img_dir): img_path = os.path.join(img_dir, img_name) result = inference_detector(model, img_path) # 把 result 里的 bbox、label、score 导出成 json 或 csv这里有个性能优化的点:inference_detector每次都会重新做预处理,如果图片尺寸一致,可以自己组 batch 推理,速度能快不少。不过 v2.x 的 API 对 batch 推理支持不算特别友好,需要手动改一些东西,这个后面可以单独展开。7. 常见问题与排查技巧实录7.1 训练相关的高频报错报错信息可能原因解决方法CUDA out of memorybatch size 太大或图片分辨率太高减小 batch size,或调小img_scaleloss is NaN学习率过大或数据有非法框降低 lr,检查标注里是否有宽高为 0 的框AssertionError: Thenum_classesof bbox_head should be consistent类别数没改对检查num_classes和classes是否一致KeyError: 'xxx' is not in the models registry自定义模块没注册检查@DETECTORS.register_module()是否加了mmcv._ext not foundmmcv 版本不对或没装 full 版重装对应版本的 mmcv-full7.2 指标不上升的排查思路如果训练 loss 在降但 mAP 不涨,或者涨得很慢,可以按这个顺序排查:数据对不对:抽样可视化几张训练图,确认框和标签正确。学习率合不合适:太大 loss 震荡,太小收敛慢。可以画 lr-loss 曲线看。预训练权重加载了吗:v2.x 默认会加载 ImageNet 预训练 backbone,如果没加载,收敛会慢很多。检查配置里的pretrained字段。评估流程对不对:确认val的ann_file和img_prefix指向正确,类别数一致。是不是过拟合了:训练 mAP 高但验证 mAP 低,说明过拟合,加数据增强或正则化。7.3 几个我踩过的坑坑一:改了配置但没生效。原因是配置继承层级太深,我改的那一层被更底层覆盖了。解决办法就是前面说的,先print_config看完整配置。坑二:多卡训练时 batch size 理解错。v2.x 里samples_per_gpu是每卡的 batch size,总 batch 是samples_per_gpu * num_gpus。我一开始以为samples_per_gpu是总数,学习率调错了,训了半天效果很差。坑三:测试时忘了切 eval 模式。虽然tools/test.py会自动处理,但自己写推理脚本时如果忘了model.eval(),BN 层和 Dropout 会保持训练状态,结果会不稳定。坑四:权重文件加载了但没完全加载。用load_from加载预训练权重时,如果类别数不一致,分类头的权重会被跳过,这是正常的。但如果你发现 backbone 的权重也没加载,那就要检查 key 名是否匹配。8. 一些让训练更顺手的经验补充8.1 日志可视化v2.x 支持 TensorBoard,配置里加一行:log_config = dict( interval=50, hooks=[ dict(type='TextLoggerHook'), dict(type='TensorboardLoggerHook') ])然后tensorboard --logdir work_dirs/exp1就能看 loss 曲线和 mAP 曲线。曲线比数字直观得多,尤其是判断过拟合和欠拟合的时候。8.2 学习率策略的选择v2.x 默认是 step 策略,但 cosine 策略在很多任务上表现更平滑。改法:lr_config = dict( policy='CosineAnnealing', warmup='linear', warmup_iters=500, warmup_ratio=0.001, min_lr_ratio=1e-5)cosine 的好处是不用手动设 step,学习率平滑下降,对超参没那么敏感。我最近几个项目都换成了 cosine,效果稳定。8.3 模型集成的简单做法如果单模型指标差一点,可以试试 WBF(Weighted Boxes Fusion)或者简单的 NMS 集成。v2.x 本身不直接提供 WBF,但可以自己写后处理。思路是把多个模型的预测结果合并,用 WBF 算法融合。这个在竞赛里很常用,能涨一两个点。8.4 导出 ONNX 的注意事项如果要把模型部署到其他平台,可能需要导出 ONNX:python tools/deployment/pytorch2onnx.py \ configs/my_config.py \ work_dirs/exp1/latest.pth \ --output-file model.onnx \ --input-img test.jpg \ --shape 800 1333这里--shape要和训练时的输入尺寸匹配。导出后建议用onnxruntime验证一下输出是否和 PyTorch 一致,避免部署时才发现问题。我个人在实际操作中的体会是,mmdetection v2.x 虽然版本老,但它的配置体系和训练流程设计得非常清晰,把这一套吃透之后,再去看 3.x 或者其他检测框架,会发现底层逻辑是相通的。真正花时间的从来不是敲命令,而是理解每个配置项背后的意图,以及遇到问题时知道往哪个方向排查。最后再分享一个小技巧:每次开新实验前,把配置文件、日志、关键指标记到一个表格里,实验多了之后回头看,能快速定位哪种配置组合效果最好,比凭记忆靠谱得多。