MMDetection实战指南:从环境搭建到模型训练与评估 前几天有个学弟来问我说跟着网上的教程装MMDetection框架装了整整两天还在报错差点把电脑都砸了。我让他把报错贴过来一看好家伙先是mmcv和mmdet版本对不上后面又踩了数据格式的坑再往下看训练根本不收敛。这其实是很多入门者都会遇到的典型路径以为框架难在模型结构结果一上手才发现环境、数据、配置、训练、评估每一步都可能卡住。这篇东西不打算讲那些花里胡哨的源码分析就按照我自己从零跑通MMDetection的真实流程来写。环境怎么搭、数据怎么转、配置怎么写、训练怎么看、评估怎么做、坑都踩在哪全部按实操顺序捋一遍。不管你是准备做毕设、搞工程落地还是想复现论文做对比实验只要能看懂Python基础照着走基本都能把目标检测模型跑起来。1. MMDetection框架到底是什么为什么大家都选它1.1 一句话先把它说明白MMDetection是OpenMMLab团队开源的目标检测工具箱底子是基于PyTorch做的PyTorch基础框架里的模型训练、自动求导、GPU加速这些能力它全部复用然后在上层把目标检测里那些重复性极高的环节全部封装好。你拿到一套标注好的图片数据从训练到评估基本上就是改改配置文件、跑两条命令的事。早期做目标检测有多痛苦我记忆很深。我刚接触检测任务的时候光是把数据读入、做数据增强、生成anchor、算损失函数、做NMS后处理、画PR曲线这一套流程手写出来就够写上千行代码。而且换一个模型结构这些公共代码几乎全要重写。MMDetection做的就是把backbone、neck、head、数据pipeline、训练调度器、评估指标这些模块全部拆开用注册机制统一管理让“换模型”这个动作从改代码降级成改配置。1.2 模块化设计像搭乐高一样搭检测器MMDetection的核心设计哲学就是模块化。一个完整的检测器被拆成几个大块backbone负责提特征比如ResNet、Swin Transformerneck负责特征融合最典型的是FPNhead负责输出最终结果比如RPN的候选框分支、分类分支、回归分支。在MMDetection里你还可以通过注册机制自定义模块写一个BACKBONES.register_module()装饰的类然后把名字写进配置框架就能自动加载它。这个设计的好处是巨大的。我的体验是做实验的时候往往只改一个小模块比如把ResNet换成一个自己改的变体或者给FPN加一个注意力分支。你不需要理解整个框架的所有代码细节只需要盯住自己改动的那个文件配置好其他模块的调用方式就行。1.3 版本演进选错版本是踩坑的第一大来源这个必须单独拿出来说因为我见过太多人卡死在这里。MMDetection从2.x走到3.x内部架构做了非常大的调整。2.x时代需要手动安装mmcv-full配置系统也有一些老写法3.x版本全面切换到mmengine底座配置文件结构变了很多训练参数的位置也变了比如优化器的配置从optimizer变成了optim_wrapper.optimizer。网上大量教程还是基于2.x写的如果你照着旧教程去配3.x环境报错会多到怀疑人生。我的建议是2024年往后新开始的项目直接学3.x不要在2.x上浪费时间。旧教程可以参考思路但命令和配置写法还是要以官方文档为准。下面所有内容我都按MMDetection 3.x的写法来。2. 环境搭建这一关为什么拦住了90%的新手2.1 先定版本再动手别急着复制粘贴装环境最关键的一步是先明确自己的硬件和CUDA版本。很多人一上来就“conda install一条龙”结果PyTorch、CUDA、mmcv三者互相不匹配跑起来就是各种undefined symbol和ModuleNotFoundError。如果你是NVIDIA显卡先跑一下nvidia-smi看清楚驱动支持的CUDA版本。如果只是做CPU训练不推荐但实在没显卡也能跑那就装CPU版本的PyTorch和mmcv。我在实际教学中发现新手最容易犯的错是驱动是CUDA 12.x却装了个CUDA 11.8的PyTorch跑一遍倒是能import但训练的时候莫名其妙的报错全是版本不兼容导致的。2.2 从零到踩通import的完整命令这里我按大多数人的情况给一套比较稳的安装流程假设你已经有Anaconda并且显卡驱动比较新# 1. 创建独立环境防止污染其他项目 conda create -n mmdet python3.8 -y conda activate mmdet # 2. 安装PyTorch当前仍推荐1.13到2.x的稳定版本 # 以CUDA 11.8为例其他版本去PyTorch官网找对应命令 pip install torch2.0.1 torchvision0.15.2 --index-url https://download.pytorch.org/whl/cu118 # 3. 安装OpenMMLab全家桶的安装工具mim pip install -U openmim # 4. 用mim装mmengine、mmcv mim install mmengine mim install mmcv # 5. 从源码安装mmdet这样能保证你随时切到最新特性 git clone https://github.com/open-mmlab/mmdetection.git cd mmdetection pip install -v -e .为什么推荐用mim install mmcv而不是直接pip install mmcv因为mmcv需要根据CUDA和PyTorch版本编译对应的二进制包mim会自动检测环境并选择合适的版本比自己去找whl包可靠得多。装完之后跑几个检查命令能正常输出版本号就说明基本环境OK了python -c import torch; print(torch.__version__) python -c import mmcv; print(mmcv.__version__) python -c import mmdet; print(mmdet.__version__)2.3 版本匹配速查表我把常见的版本对应关系整理成一张表照着选会省掉很多排查时间。MMDetection版本mmcv版本mmenginePyTorch建议版本3.xmmcv 2.0.0mmengine 0.7.01.13 / 2.0 / 2.12.25.xmmcv-full 1.6.x不需要1.8 / 1.10 / 1.122.28.xmmcv-full 1.7.x不需要1.10以上这里有个容易混淆的点3.x版本彻底淘汰了mmcv-full这个名字直接统一叫mmcv但要求版本号必须大于等于2.0。如果你老项目里还在用mmcv-full不要混着装到同一个环境老老实实把环境分开。3. 数据准备训练的天花板往往由数据决定3.1 COCO格式你真的搞懂了吗MMDetection里最常用的数据格式是COCO格式。这里说的COCO不只是那个公开数据集更重要的是一套标准标注文件的组织规范。一个标准的COCO标注json文件顶层是五个字段info、licenses、images、annotations、categories。实际训练时框架主要读取的是后三个。images是一个列表每个元素包含图片的id、file_name、width、heightannotations列表里每一项对应一个标注框核心字段包括image_id、bbox[x, y, width, height]、area、category_id、iscrowdcategories就是类别列表每个类别有id和name。有个细节非常容易踩COCO里bbox存的是左上角坐标加宽高不是中心点坐标加宽高。YOLO格式用的是归一化之后的中心点坐标很多从YOLO转过来的同学会在这里犯晕。我后来统一的做法是在转换脚本里把所有的坐标都先打印出来人工目检几张确认框的位置确实贴合目标再进训练流程。3.2 从你的“野路子”数据转成COCO格式大多数人手上的数据不是标准的COCO可能是VOC格式的XML可能是CSV甚至可能是纯手工标的一堆txt。我建议不要手写标注文件而是写一个转换脚本自动生成COCO的json文件。核心逻辑其实不复杂读入你的标注信息然后动态构建三个列表images、annotations、categories最后写成一个json文件。这里给一个从VOC XML转COCO的极简示例方便你有概念import json import os import xml.etree.ElementTree as ET # 类别名称到id的映射从1开始千万注意不是0 cat_id_map {dog: 1, cat: 2} images, annotations, ann_id [], [], 0 for img_id, xml_file in enumerate(os.listdir(xmls)): tree ET.parse(os.path.join(xmls, xml_file)) root tree.getroot() image { id: img_id, file_name: root.find(filename).text, width: int(root.find(size/width).text), height: int(root.find(size/height).text) } images.append(image) for obj in root.findall(object): cat_name obj.find(name).text bndbox obj.find(bndbox) x1 float(bndbox.find(xmin).text) y1 float(bndbox.find(ymin).text) x2 float(bndbox.find(xmax).text) y2 float(bndbox.find(ymax).text) annotations.append({ id: ann_id, image_id: img_id, bbox: [x1, y1, x2 - x1, y2 - y1], area: (x2 - x1) * (y2 - y1), iscrowd: 0, category_id: cat_id_map[cat_name], }) ann_id 1 coco {images: images, annotations: annotations, categories: [{id: v, name: k} for k, v in cat_id_map.items()]} with open(annotations/instances_train.json, w) as f: json.dump(coco, f)这个脚本虽然简单但说明了一个很重要的点你自己做转换的时候一定要检查category_id是不是从1开始的。COCO格式里背景是0但目标类别的id是从1开始编号的。很多人自定义数据集mAP一直是0查到最后就是类别id从0开始导致模型训练时把所有类别都当成了背景。3.3 数据集目录怎么组织最省心MMDetection对数据目录没有强制要求但按官方推荐的目录结构来会少改很多配置。我一般按这种形式组织data/ └── custom_dataset/ ├── annotations/ │ ├── instances_train.json │ └── instances_val.json ├── train/ └── val/train和val目录下就是图片文件注解文件指到annotations目录。训练集和验证集的划分我习惯按8:2来但如果数据总量很小建议先分一个固定验证集出来做开发调试不要把验证集也参与训练。4. 配置文件与训练流程跑通第一个模型才算入门4.1 配置文件是怎么一层层继承出来的MMDetection的配置系统是我见过做得最实用的设计之一。配置文件本身是Python文件不是JSON也不是YAML所以可以在里面写表达式、做变量计算。最核心的机制是_base_继承。以configs/faster_rcnn/faster_rcnn_r50_fpn_1x_coco.py为例它的内容大致是_base_ [ ../_base_/models/faster_rcnn_r50_fpn.py, ../_base_/datasets/coco_detection.py, ../_base_/schedules/schedule_1x.py, ../_base_/default_runtime.py ]看到没有它自己本身没有太多内容主要是把模型结构、数据集配置、训练调度、运行时配置四块组合在一起。这种设计的好处是你的实验往往只改动其中某一块比如换数据集、加训练轮数、换学习率不用把整份几百行的配置复制一遍只需要继承基础配置然后覆盖对应字段。第一次看配置的时候不要怕文件长先抓住关键位置model、data、optim_wrapper、train_cfg、test_cfg。4.2 从零修改一份自己的配置现在假设我要训练一个Faster R-CNN但类别数不是COCO的80类而是5类数据集路径也换成自己的data/custom。我一般不会直接改官方配置而是在自己的项目目录下新建一个配置文件然后继承官方的只覆盖改动部分_base_ mmdetection/configs/faster_rcnn/faster_rcnn_r50_fpn_1x_coco.py # 1. 修改类别数 model dict( roi_headdict( bbox_headdict(num_classes5) ) ) # 2. 修改数据路径 data_root data/custom_dataset/ data dict( traindict( data_rootdata_root, ann_fileannotations/instances_train.json, data_prefixdict(imgtrain/) ), valdict( data_rootdata_root, ann_fileannotations/instances_val.json, data_prefixdict(imgval/) ), testdict( data_rootdata_root, ann_fileannotations/instances_val.json, data_prefixdict(imgval/) ) ) # 3. 如果你的显卡显存不大把batch size调小一点 train_dataloader dict(batch_size4, num_workers2)这里有一个隐藏很深的点修改num_classes的时候不止一个地方要改。Faster R-CNN里RPN头部的输出维度不依赖类别数但后面的ROI head里的bbox_head是必要改的。如果用了mask分支mask_head的类别数也要一起改。我每次新建配置都会在写完model之后用工具打印一下模型结构确保没有漏改。还有学习率的调整。MMDetection官方默认的0.02是在batch_size16的前提下调的。如果你batch_size改成4按线性缩放的经验法则学习率大概改成0.005附近比较稳。这个数字不必精确但同一个数量级是比较安全的区间。4.3 启动训练与日志解读配置写好后训练命令非常简单python tools/train.py /path/to/my_config.py --work-dir work_dirs/custom_faster_rcnn训练一旦启动终端上会刷出大量日志。第一次跑的人往往会一头雾水这里我挑几个关键值解释一下loss_rpn_clsRPN阶段的前景背景分类损失稳定在0.01级别说明RPN已经能区分前景背景。loss_rpn_bboxRPN阶段的框回归损失数值比分类损失高是正常的。loss_cls最终分类分支的损失如果模型学到东西会整体缓慢下降。loss_bbox最终回归分支的损失。loss前面所有损失加权的总和是训练最核心的监控指标。训练过程中每隔一个周期会保存一个checkpoint如果一个周期的mAP已经超过历史最好值会保存一份best_coco_bbox_mAP开头的权重文件。这比你自己手动挑选checkpoint要靠谱很多。5. 训练完怎么评估、推理和看结果5.1 拿到mAP报告这些指标到底是什么训练结束或者你想对已经训好的权重做评估执行python tools/test.py /path/to/my_config.py /path/to/best_ckpt.pth --eval bbox跑完会输出一张COCO风格的评估表。最上面的Average Precision (AP) [ IoU0.50:0.95 | area all | maxDets100 ]是综合评价的核心指标通常直接叫mAP。下面的AP [IoU0.50]是IoU阈值取0.5时的AP也就是AP50AP [IoU0.75]是更严格阈值下的AP75。做工程落地的时候我们要重点看AP50因为实际业务里往往框的大概位置比像素级的精确更重要。发论文或者做严谨的算法对比则必须看IoU0.50:0.95的mAP因为它对框的定位精度更敏感。我自己的习惯是先看带AP50的值确认整体有没有崩掉再看mAP确认模型精度上限。5.2 单张图片和视频推理评估只是跑数字实际效果还是要看可视化图片。MMDetection官方提供了一套demo脚本用起来非常方便python demo/image_demo.py demo/demo.jpg /path/to/my_config.py /path/to/best_ckpt.pth --device cuda:0默认会调用MMDetInferencer自动完成推理和画框并显示图片。如果是在服务器上跑没有显示环境可以加参数把结果保存下来。我经常通过可视化结果来判断模型的定位风格比如框是不是偏大或偏小漏检主要集中在哪类目标这些信息在mAP数字里看不到。5.3 从研究到部署存下结果为后续使用做准备很多教程到这里就结束了但实际项目里训练完只是开始。test.py可以加--out result.pkl把推理结果序列化保存后面做错误分析、ensemble时直接复用这些结果就不用重新推理一遍了。如果要做模型的工业级部署还可以考虑导出ONNX或TensorRT格式但那属于另一个大话题入门阶段先把PyTorch的结果跑通即可。6. 排错实录高频问题与解决思路6.1 环境类问题ModuleNotFoundError: No module named mmcv这个报错很唬人但很多时候并不是没装mmcv而是装了但版本不匹配。比如在MMDetection 3.x下面装了一个1.x版本的mmcvimport时就会报错或者某些模块找不到。排查第一步是打印mmcv.__version__然后再检查它和mmdet的版本兼容性。另外确认你是在正确的conda环境里跑而不是切到了base环境。CUDA out of memory训练时显存溢出的一个高频诱因是batch_size太大。显存不够时优先把train_dataloader.batch_size调小到2或1如果调到头还是爆显存再考虑开启梯度累积或者换小一点的输入分辨率。这里我提一个实用技巧在配置里把data_preprocessor的batch_augments去掉或者关闭一些花哨的增强也能省下不少显存。6.2 数据类问题KeyError: gt_labels 或 bbox出现这个报错通常意味着数据集的加载结果里没有期望的字段。最常见的原因是你的标注文件里字段名写错了或者读入的图像路径不对导致对应的标注被过滤掉了。先去检查你的ann_file路径是否和配置里的data_root拼起来正确再检查图片是不是真的能被PIL打开。训练mAP一直是0这个问题绝大多数出在类别id上前面说过COCO的类别id必须从1开始。如果自定义数据里类别的id是0模型会把所有gt都当背景训练出来的检测结果自然全是空。另外一个可能原因是metainfo里的类别顺序和标注文件里的categories顺序不一致导致模型的类别索引和标注的类别id对不上。6.3 训练类问题loss变成nanloss为nan先看学习率是不是太大把学习率降到原来的1/10试试。如果数据量少、标注框太小也有可能触发数值不稳定。还有可能就是数据里出现了极端值比如宽度或高度为0的bbox这种框在计算IoU的时候会出问题。我写过一个小脚本在训练前扫一遍所有bbox把宽高小于1个像素的标注全部过滤掉之后这类问题少了很多。模型不收敛loss居高不下这种情况先不要动模型直接看backbone是不是随机初始化。如果你用了ImageNet预训练权重但配置里的load_from路径写错模型其实是在从头训练收敛就会非常慢。另外检查一下训练集和验证集的数据增强是否合理有些增强在训练集上过于激进会让模型学到错误分布。训练正常但验证mAP明显偏低最常见原因是模型过拟合到训练集了尤其是小数据集上比较明显。可以尝试加大数据增强的力度或者用更简单的模型结构先把baseline跑通再逐步加复杂度。6.4 我最后想分享的一条经验踩过这么多坑之后我最大的体会是用MMDetection做目标检测不要一开始就想着把所有原理都弄懂再动手。这个框架最大的优势就是能让你先跑通流程再深入细节。我建议新手朋友先拿官方COCO数据集里抽出来的一个小子集用默认配置完整跑一遍训练和评估感受整个pipeline的节奏然后再替换成自己的数据。这样出了问题你至少能判断是流程问题、数据问题还是模型问题。等整个链路都熟了再回头读源码、改模块效率会高很多。