1. 从零上手 mmdetection模型解析到底在解析什么刚接触 mmdetection 的人十有八九会被它的目录结构劝退。configs/下面几百个配置文件mmdet/models/里密密麻麻的 backbone、neck、head、loss光看名字就头大。我当初第一次跑通一个检测模型花了整整两天其中一天半都耗在这个参数到底该改哪里上。后来才明白mmdetection 的模型解析不是让你去背代码而是搞清楚三件事一个检测器由哪几个模块拼起来、每个模块的配置项控制什么、改配置的时候改动会传导到哪里。把这三件事理顺后面无论是换 backbone、调 loss 权重还是自己加一个新 head都不会再抓瞎。这篇内容面向的是已经装好 mmdetection、能跑通官方 demo、但一看到配置文件就发懵的同学。我会从整体架构讲到具体模块再落到配置文件怎么读、怎么改最后把我踩过的坑和排查思路整理出来。全程不堆砌源码重点讲为什么这么设计和实际改的时候注意什么。你不需要精通 PyTorch但至少要能看懂nn.Module的forward大概在干什么这样理解起来会顺畅很多。mmdetection 本质上是一个检测算法的乐高工厂。它把目标检测拆成了几个标准件backbone主干网络负责提特征neck颈部负责融合多尺度特征head检测头负责出预测结果loss损失函数负责算误差。你看到的每一个配置文件无非是在描述用哪块乐高、怎么拼、拼完怎么训练。理解了这套拼装逻辑再看那些配置文件就不再是天书了。2. 检测器的整体架构与模块划分2.1 为什么检测器要拆成 backbone、neck、head 三段目标检测的核心任务是在图上找到物体在哪、是什么。这件事拆开来看其实是两个子问题特征提取和位置回归分类。backbone 解决第一个问题head 解决第二个问题neck 则是为了解决不同大小的物体需要不同尺度的特征这个矛盾。拿生活里的例子类比backbone 像是你的眼睛先把画面看进来neck 像是大脑的视觉皮层把眼睛看到的信息做多层次的整合head 像是你的判断最终决定这里有个杯子、那里有只猫。这个比喻不完全严谨但能帮你快速建立直觉。mmdetection 把这三段做成可插拔的模块好处非常直接。你想把 ResNet 换成 Swin Transformer只需要改 backbone 的配置neck 和 head 基本不用动。你想试试不同的检测头也只改 head 部分。这种解耦设计是 mmdetection 能支持上百种算法组合的根本原因。2.2 一个配置文件里到底藏了哪些信息打开任意一个配置文件比如configs/faster_rcnn/faster-rcnn_r50_fpn_1x_coco.py你会发现它其实很短因为大部分内容都通过_base_继承了。这是 mmdetection 的一个核心设计配置继承。基础配置放在configs/_base_/下面分成 datasets、models、schedules、default_runtime 四类具体算法配置只写差异部分。一个完整的模型配置核心是model这个字典它下面通常有这几个键type模型类型比如FasterRCNN、YOLOX、MaskRCNNbackbone主干网络配置neck颈部配置FPN 最常见rpn_head区域提议头两阶段检测器才有roi_headROI 检测头两阶段检测器才有bbox_head边界框检测头单阶段检测器常用train_cfg/test_cfg训练和测试时的行为配置单阶段和两阶段的结构差异就体现在这些键的有无上。Faster R-CNN 有rpn_head和roi_head而 RetinaNet 只有bbox_head。理解这一点你就能一眼看出一个配置属于哪类算法。2.3 配置继承的查找顺序与覆盖规则配置继承的规则是子配置覆盖父配置字典递归合并列表整体替换。这句话很关键我展开说。假设 base 里model.backbone.depth50子配置里写model.backbone.depth101那最终就是 101。但如果 base 里model.neck.out_channels256子配置里写model.neckdict(typeFPN, out_channels128)那整个 neck 会被替换掉而不是只改 out_channels。列表是整体替换的比如 base 里strides[4,8,16,32]子配置写strides[8,16]最终就是[8,16]不会合并。这个规则我踩过坑。有一次我想在继承的配置上给train_cfg加一个字段结果直接写了个新的train_cfg字典把原来 base 里的其他字段全冲掉了训练时报了一堆莫名其妙的错。后来才学会用dict(...)显式继承再覆盖或者干脆在子配置里写全。提示改配置前先用python tools/misc/print_config.py 你的配置.py把最终合并后的配置打印出来确认每一项都符合预期这一步能省掉大量调试时间。3. 核心模块逐个拆解与配置要点3.1 Backbone特征提取的起点Backbone 的任务是把输入图像变成一组特征图。以最常用的 ResNet 为例输入[B, 3, H, W]经过 stem 和四个 stage输出四组特征图stride 分别是 4、8、16、32通道数分别是 256、512、1024、2048。这四组特征图就是后续 neck 的输入。在配置里backbone 通常长这样backbonedict( typeResNet, depth50, num_stages4, out_indices(0, 1, 2, 3), frozen_stages1, norm_cfgdict(typeBN, requires_gradTrue), norm_evalTrue, stylepytorch, init_cfgdict(typePretrained, checkpointtorchvision://resnet50), )几个关键参数值得说清楚。out_indices决定输出哪几个 stage 的特征默认全输出。frozen_stages1表示冻结 stem 和第一个 stage 的参数这在数据量小的时候能防止过拟合但数据量大的时候反而限制模型能力我一般会根据自己的数据规模决定冻不冻。norm_evalTrue表示 backbone 里的 BN 层在训练时用 running stats 而不是 batch stats这是为了和预训练权重保持一致如果你做的是小 batch 训练这个设置尤其重要否则 BN 的统计量会非常不稳定。init_cfg是加载预训练权重的地方。torchvision://resnet50表示从 torchvision 加载你也可以换成自己的权重文件路径。这里有个坑如果你改了 backbone 的结构比如改了 depth 或 out_indices预训练权重可能对不上加载时会报 key 不匹配。这时候要么用strictFalse忽略要么就老老实实用匹配的结构。3.2 Neck多尺度特征融合的关键Neck 最经典的就是 FPNFeature Pyramid Network。它的作用是把高层语义强但分辨率低的特征和低层语义弱但分辨率高的特征融合起来让模型既能检测大物体也能检测小物体。FPN 的工作流程是自顶向下从 stride 32 的特征图开始上采样后和 stride 16 的特征图相加再上采样和 stride 8 的相加以此类推。最后每个尺度再经过一个 3x3 卷积输出。配置里通常这样写neckdict( typeFPN, in_channels[256, 512, 1024, 2048], out_channels256, num_outs5, )in_channels必须和 backbone 的输出通道严格对应写错了直接报错。out_channels是融合后每个尺度的通道数统一成 256 是惯例。num_outs5表示输出 5 个尺度比 backbone 的 4 个多一个多出来的那个是 stride 64 的通过对 stride 32 的特征图再下采样得到专门用来检测超大物体。我实测下来如果你的数据集里小物体特别多FPN 的 out_channels 可以适当加大到 384 或 512但显存占用会明显上升。反过来如果都是大物体num_outs 可以减到 4省一点计算量。这些调整没有绝对的对错取决于你的数据分布。3.3 Head从特征到预测的最后一公里Head 是差异最大的部分单阶段和两阶段完全不同。两阶段检测器的 head 分成 RPN head 和 ROI head。RPN head 负责生成候选框它会在每个特征图位置上预测这里有没有物体和框该怎么调整。ROI head 则对候选框做进一步的分类和回归。配置里 RPN head 通常很简单rpn_headdict( typeRPNHead, in_channels256, feat_channels256, anchor_generatordict( typeAnchorGenerator, scales[8], ratios[0.5, 1.0, 2.0], strides[4, 8, 16, 32, 64]), bbox_coderdict( typeDeltaXYWHBBoxCoder, target_means[0., 0., 0., 0.], target_stds[1., 1., 1., 1.]), loss_clsdict(typeCrossEntropyLoss, use_sigmoidTrue, loss_weight1.0), loss_bboxdict(typeL1Loss, loss_weight1.0), )anchor_generator是重点。scales[8]表示每个位置生成一个基础尺寸的 anchorratios是宽高比strides对应特征图的步长。anchor 的设计直接决定召回率如果你的数据里物体尺寸分布很集中可以针对性调整 scales如果尺寸跨度大就多加几个 scale。我见过有人直接照搬 COCO 的 anchor 配置去跑自己的工业缺陷数据结果小缺陷全漏检就是因为 anchor 尺度和实际物体尺寸不匹配。单阶段检测器的 head 相对直接比如 RetinaNet 的 head 就是几个卷积层堆叠后接分类和回归分支。YOLOX 的 head 则做了 decoupled 设计分类和回归用不同的分支。这部分配置的核心是num_classes和loss的权重num_classes一定要和你的数据集类别数一致这是最常见的报错来源之一。3.4 Loss训练信号的来源Loss 决定了模型往哪个方向优化。检测任务的 loss 通常分两部分分类 loss 和回归 loss。分类常用 CrossEntropy 或 Focal Loss回归常用 L1、SmoothL1 或 GIoU Loss。Focal Loss 是单阶段检测器的标配它通过降低易分样本的权重让模型聚焦在难分样本上。配置里长这样loss_clsdict( typeFocalLoss, use_sigmoidTrue, gamma2.0, alpha0.25, loss_weight1.0, )gamma控制难易样本的权重差异alpha控制正负样本的平衡。这两个参数调起来比较玄学我一般先用默认值如果发现正样本太少导致训练不稳定再适当调 alpha。回归 loss 里GIoU 系列比 L1 更关注框的重叠程度通常收敛更好。但 GIoU 在训练初期可能不稳定有些实现会先用 L1 预热几个 epoch 再切换。这个细节在配置里通过loss_bbox的 type 控制切换的时候记得同步调整loss_weight。4. 配置文件实操从读懂到改对4.1 用 print_config 看清最终配置前面提过print_config.py这里展开说。它的用法是python tools/misc/print_config.py configs/faster_rcnn/faster-rcnn_r50_fpn_1x_coco.py输出的是所有继承合并后的完整配置。我习惯把它重定向到一个文件里然后用编辑器搜索关键词python tools/misc/print_config.py 你的配置.py full_config.txt这样改配置的时候先看 full_config 里这一项原本是什么再决定怎么覆盖。千万不要凭记忆改配置继承链一长你根本记不住 base 里写了什么。4.2 改类别数的正确姿势假设你要在自定义数据集上训练类别数是 5。你需要改的地方有model.bbox_head.num_classes或model.roi_head.bbox_head.num_classesmodel.rpn_head如果有分类分支也要改不过 RPN 只有前景背景两类通常不用改data.train_dataloader.dataset.metainfo.classes或classes字段data.val_dataloader.dataset和data.test_dataloader.dataset同步改我见过最常见的错误是只改了 head 的 num_classes忘了改 dataset 的 classes结果训练时类别名对不上评估阶段直接报错。还有一种是改了 num_classes 但没重新加载预训练权重导致分类头的权重形状不匹配这时候要么让框架自动忽略不匹配的层要么手动处理。4.3 调整学习率和 batch size 的联动学习率和 batch size 是联动的。mmdetection 默认的 1x schedule 是 8 卡、每卡 2 张图总 batch size 16学习率 0.02。如果你只有单卡batch size 变成 2那学习率也应该按比例缩小到 0.0025 左右。这个比例不是绝对的但线性缩放规则是个不错的起点lr base_lr * (your_batch_size / base_batch_size)。改完之后如果发现 loss 震荡再适当降低如果收敛太慢再适当提高。我一般会先跑几百个 iteration 看看 loss 曲线稳定下降就继续剧烈震荡就调小。注意改了 batch size 之后schedules里的steps也要相应调整。因为 iteration 数变了原来在第 8、11 个 epoch 降学习率的设置可能就不合适了。4.4 冻结策略与微调技巧微调的时候冻结 backbone 的前几个 stage 是常见做法。除了frozen_stages还可以用norm_eval控制 BN 的行为。如果数据量很小比如几百张图我建议冻结整个 backbone只训练 neck 和 head这样能最大程度利用预训练特征避免过拟合。如果数据量中等几千到几万张可以只冻结 stem 和第一个 stage后面的正常训练。数据量很大十万张以上那就全部放开训练甚至可以不用预训练权重从头训。这里有个经验冻结的层数越多训练越快但模型上限越低。我一般会做两组对比实验一组冻结多、一组冻结少看验证集指标决定用哪个。5. 常见问题与排查技巧实录5.1 报错速查表报错信息常见原因解决方法KeyError: xxx配置里引用了不存在的键用 print_config 检查最终配置size mismatch for xxx预训练权重和模型结构不匹配检查 num_classes、backbone 结构CUDA out of memorybatch size 太大或模型太大减小 batch size、用梯度累积loss is nan学习率太大或数据有异常值降低学习率、检查数据标注AssertionError: xxx配置项类型或取值不对对照文档检查参数类型RuntimeError: expected scalar type数据类型不一致检查输入和模型的数据类型5.2 loss 不下降的排查思路loss 不下降是最让人头疼的问题。我的排查顺序是先看数据。用tools/analysis_tools/browse_dataset.py把数据可视化出来确认标注框位置正确、类别正确。我遇到过标注文件里坐标是归一化的但配置按绝对坐标读结果框全跑到角落去了。再看学习率。学习率太大 loss 会震荡甚至爆炸太小则下降极慢。可以先跑一个 lr finder 或者手动试几个值。然后看 loss 权重。分类 loss 和回归 loss 的权重如果差太多模型会偏向一边。我一般让两者量级接近。最后看模型结构。如果前面都没问题可能是 head 的设计和任务不匹配比如小物体检测用了 stride 很大的特征图。5.3 显存不够的几种省法显存不够是常态尤其是想跑大模型的时候。我常用的省显存手段有减小 batch size配合梯度累积模拟大 batch用 AMP 混合精度训练配置里加fp16dict(loss_scale512.)能省 30% 到 50% 显存减小输入分辨率但要注意小物体可能因此检不出来冻结 backbone减少中间激活的存储用 gradient checkpointing用时间换空间但会拖慢训练速度这些手段可以组合使用我一般优先用 AMP效果最明显且对精度影响小。5.4 评估指标异常的定位评估指标异常通常有三种表现mAP 很低、某些类别 mAP 为 0、指标波动大。mAP 很低先看是不是评估配置的问题比如ann_file路径错了、类别数对不上。某些类别为 0 通常是该类样本太少或者 anchor 不匹配。指标波动大可能是 batch size 太小导致 BN 不稳定或者学习率太大。我习惯在训练过程中定期跑验证把每个类别的 AP 都打出来这样能快速定位是全局问题还是局部问题。6. 模型解析的进阶方向把基础模块搞清楚之后可以往几个方向深入。一是自定义 backbone比如把主流的视觉 Transformer 接进来这时候要注意输出特征的格式和 neck 的输入要求对齐。二是自定义 head比如针对特定任务设计特殊的预测分支这时候要理解 head 的输入特征图和输出格式的约定。三是自定义 loss比如针对类别不平衡设计新的损失函数这时候要清楚 loss 的输入是哪些张量、形状是什么。我个人的体会是不要一上来就想着改源码。先把配置层面的东西玩透用继承和覆盖能解决 80% 的需求。真正需要改源码的时候往往是你已经很清楚现有实现的局限在哪里了。这时候改起来也会更有针对性不会漫无目的地翻代码。另外mmdetection 的版本更新比较快不同版本之间 API 可能有变化。我建议锁定一个稳定版本把它的文档和源码结构吃透而不是追着最新版跑。等你对某个版本足够熟悉了再升级也不迟。升级的时候重点看 changelog 里标注的 breaking changes那些是真正会影响你配置的地方。最后分享一个小技巧养成给配置文件写注释的习惯。你改的每一个参数旁边写一句为什么改过两周再回来看能省下大量回忆的时间。我现在回头看自己早期改的配置很多地方已经想不起来当时为什么那么设了只能重新试一遍非常浪费时间。