mmdetection模型解析:从配置文件到检测框的完整指南
1. 从配置文件到检测框mmdetection模型解析到底在讲什么刚接触mmdetection的人十有八九会被它那套配置文件体系绕晕。打开一个configs目录里面密密麻麻全是py文件什么faster_rcnn_r50_fpn_1x_coco.py、mask_rcnn_r101_fpn_mstrain_2x_coco.py光看名字就够劝退的。但如果你真想把这个框架用起来而不是停留在“跑通demo”的阶段那模型解析这一关是绕不过去的。我所说的“模型解析”不是让你去逐行读源码而是搞清楚三件事一个检测模型在mmdetection里是怎么被描述的、这些描述最终怎么变成一个能跑的网络、以及当你想改结构时应该动哪里。这三件事搞明白了你就能从“调包侠”变成“能改模型的人”。这篇文章适合谁看如果你已经装好了mmdetection能跑通官方demo但面对配置文件还是一头雾水想自己换backbone、改neck、加head却不知道从哪下手那这篇内容就是写给你的。我会用从业者的视角把mmdetection的模型描述体系拆开揉碎配上可直接复现的配置片段和踩坑经验让你看完就能动手改自己的模型。先给个整体认知mmdetection的模型定义走的是“配置驱动”路线。你几乎不需要写Python类来定义网络而是通过一个字典结构描述每一层是什么、参数是多少、层与层怎么连。这个字典最终被一个叫build_from_cfg的机制实例化成真正的nn.Module。理解这条链路是理解一切配置的钥匙。2. 配置驱动mmdetection模型描述的核心思路2.1 为什么用配置而不是直接写类很多人第一次看到mmdetection的配置会问为什么不直接像写普通PyTorch那样定义一个class MyDetector(nn.Module)把backbone、neck、head都写死在__init__里这样不是更直观吗答案在于组合爆炸。目标检测领域的主干网络有ResNet、ResNeXt、Swin、ConvNeXt等十几种neck有FPN、PAFPN、BiFPN等head有RetinaHead、FCOSHead、ATSSHead等再加上数据集、训练策略、数据增强的组合如果每个组合都写一个类代码量会失控。配置驱动的本质是把“结构描述”和“结构实现”解耦实现只写一份描述可以无限组合。具体到代码层面mmdetection用了一个叫Registry注册表的机制。每个可复用的组件backbone、neck、head、loss等在定义时用BACKBONES.register_module()这样的装饰器注册进去配置里写typeResNet构建时框架就去BACKBONES这个注册表里找名为ResNet的类把配置里的其他键作为参数传进去实例化。这就是build_from_cfg做的事。理解这一点很关键配置里的type字段就是注册表里的键其他字段就是构造函数的参数。你看到typeFPN就知道要去mmdet/models/necks/fpn.py里找FPN类看它的__init__签名就知道配置里能写哪些参数。这是读配置最实用的方法。2.2 一个检测模型的四段式结构mmdetection里几乎所有检测模型都遵循同一个骨架我把它叫做“四段式”backbone特征提取主干输入图像输出多尺度特征图。比如ResNet输出C2到C5四层。neck特征融合颈部把backbone的多尺度特征进一步处理。最典型的是FPN把C5上采样后与C4相加逐层往下得到P2到P5。head检测头在融合后的特征上做分类和回归。两阶段的有RPN head和ROI head单阶段的就是一个密集预测head。roi_head仅两阶段对RPN产生的候选框做进一步的分类和精修。这个结构在配置里体现得非常清晰。你打开任何一个两阶段检测器的配置都能看到model dict(typeFasterRCNN, backbone..., neck..., rpn_head..., roi_head...)这样的结构。单阶段则是model dict(typeRetinaNet, backbone..., neck..., bbox_head...)。记住这个骨架读任何配置都不会迷路。提示不同版本的mmdetection字段名略有差异比如早期版本用pretrained新版本改用init_cfg。读配置前先确认你的版本否则会对着不存在的参数发呆。2.3 继承机制配置不是孤立的mmdetection的配置有个非常实用的特性继承。你看到的faster_rcnn_r50_fpn_1x_coco.py往往只有十几行因为它的大部分内容是从base配置继承来的。配置开头通常有_base_ [../_base_/models/faster_rcnn_r50_fpn.py, ...]这样的语句。继承的规则是子配置会覆盖父配置中同名的键字典是递归合并的。这意味着你改模型时不需要复制整个配置只需要写你要改的那部分。比如想把backbone从ResNet50换成ResNet101只需要在子配置里写model dict( backbonedict( depth101, init_cfgdict(typePretrained, checkpointtorchvision://resnet101)))框架会自动把这段合并到父配置的model.backbone里其他参数保持不变。这个机制极大减少了重复但也带来一个坑你看到的配置不完整真正的完整配置是继承链合并后的结果。想看清楚最终配置可以用tools/misc/print_config.py把合并后的配置打印出来这是排查配置问题的第一步。3. 逐层拆解backbone、neck、head的配置细节3.1 backbone配置从depth参数说起backbone的配置看起来简单但细节不少。以最常用的ResNet为例配置通常长这样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))这里每个参数都有讲究。depth50决定用哪个版本的ResNet可选18、34、50、101、152。out_indices(0,1,2,3)表示输出四个stage的特征对应C2到C5。如果你只想用C5做单尺度检测可以改成(3,)。frozen_stages1表示冻结第一个stage的参数不训练这在数据量小的时候能防止过拟合但如果你数据充足建议设为-1全部训练或0不冻结。norm_evalTrue是个容易被忽略但很重要的参数。它表示backbone里的BN层在训练时也保持eval模式用全局统计量而不是batch统计量。为什么这么做因为检测任务batch size通常很小受显存限制小batch的BN统计量不稳定用全局统计量更可靠。这个细节在官方文档里不会强调但实际训练中影响不小。stylepytorch控制的是stride的位置。PyTorch风格的ResNet在3x3卷积上用stride2而Caffe风格在第一个1x1卷积上用stride2。两者精度略有差异但更重要的是预训练权重必须和style匹配否则加载权重时形状对不上。用torchvision的预训练权重就选pytorch用原版Caffe权重就选caffe。3.2 neck配置FPN的in_channels和out_channelsneck部分以FPN为例配置通常是这样neckdict( typeFPN, in_channels[256, 512, 1024, 2048], out_channels256, num_outs5)in_channels必须和backbone的输出通道严格对应。ResNet50的四个stage输出通道是256、512、1024、2048所以这里写这四个数。如果你换了backbone比如Swin Transformer通道数完全不同这里必须跟着改否则构建时就会报通道不匹配的错误。这是新手换backbone时最常踩的坑。out_channels256是FPN每一层输出的统一通道数。为什么是256这是原论文的选择也是实践中的默认值。理论上你可以改成512获得更强的表达能力但显存和计算量会上升。num_outs5表示输出5个尺度的特征P2到P6其中P6是P5经过一个stride2的卷积得到的用于检测大目标。如果你只检测中小目标可以设为4。FPN之后通常还会接一个add_extra或者直接用num_outs控制。有些配置里会看到extra_levels或者start_level这样的参数它们控制从哪个stage开始构建金字塔。这些参数在自定义neck时很有用但默认配置一般不需要动。3.3 head配置anchor和loss的细节head是配置里最复杂的部分因为它涉及anchor设置、loss权重、正负样本分配等。以RetinaNet的head为例bbox_headdict( typeRetinaHead, num_classes80, in_channels256, stacked_convs4, feat_channels256, anchor_generatordict( typeAnchorGenerator, octave_base_scale4, scales_per_octave3, ratios[0.5, 1.0, 2.0], strides[8, 16, 32, 64, 128]), bbox_coderdict( typeDeltaXYWHBBoxCoder, target_means[.0, .0, .0, .0], target_stds[1.0, 1.0, 1.0, 1.0]), loss_clsdict( typeFocalLoss, use_sigmoidTrue, gamma2.0, alpha0.25, loss_weight1.0), loss_bboxdict(typeL1Loss, loss_weight1.0))num_classes必须和你的数据集类别数一致这个不用多说。stacked_convs4表示head里堆叠4个卷积层这是RetinaNet的设计。anchor_generator里的octave_base_scale4和scales_per_octave3共同决定了每个位置有3个尺度的anchor尺度分别是4、42^(1/3)、42^(2/3)再乘以ratios的3种宽高比所以每个位置共9个anchor。这个计算过程很多人不清楚但调anchor时必须要懂。strides[8,16,32,64,128]对应FPN输出的5个特征图的步长。anchor的绝对大小等于scale * stride所以P3stride8上的anchor基础大小是32像素。如果你检测的目标普遍偏小可以把octave_base_scale调小或者增加scales_per_octave让尺度更密集。loss_cls用Focal Loss是RetinaNet的核心创新gamma2.0和alpha0.25是原论文的超参。loss_bbox用L1Loss注意这里不是Smooth L1因为Focal Loss已经处理了正负样本不平衡回归损失不需要再做特殊处理。这些细节在调loss权重时都要考虑。4. 从配置到网络build机制与实操验证4.1 build_from_cfg到底做了什么前面提到配置最终会被build_from_cfg实例化这个过程值得展开说。当你调用build_detector(cfg.model)时框架会取出cfg.model里的type字段比如FasterRCNN。去MODELS注册表里找名为FasterRCNN的类。把cfg.model里除type外的其他键作为关键字参数递归地构建子组件backbone、neck等也是通过同样的机制构建。把构建好的子组件传给FasterRCNN的构造函数得到模型实例。这个递归构建是关键。backbonedict(typeResNet, ...)在构建时框架会先构建ResNet实例再把实例传给FasterRCNN。所以配置里的嵌套字典最终会变成嵌套的模块对象。理解这一点你就明白为什么配置里可以无限嵌套也明白为什么改配置时要注意层级。4.2 打印最终配置排查问题的第一步前面说过配置有继承你看到的不是全部。想看清楚最终生效的配置用这个命令python tools/misc/print_config.py configs/faster_rcnn/faster_rcnn_r50_fpn_1x_coco.py它会输出合并后的完整配置。我建议你在改任何配置之前先打印一遍原始配置对照着改。这样你能清楚知道哪些参数是继承来的哪些是你新加的。很多“改了没生效”的问题都是因为改的键被父配置覆盖了或者键名写错了层级。4.3 实操把Faster R-CNN的backbone换成ResNet101光说不练假把式。我们来完整走一遍换backbone的流程。假设你要基于faster_rcnn_r50_fpn_1x_coco.py改一个ResNet101的版本。第一步新建配置文件my_faster_rcnn_r101.py内容如下_base_ ./faster_rcnn_r50_fpn_1x_coco.py model dict( backbonedict( depth101, init_cfgdict(typePretrained, checkpointtorchvision://resnet101)))第二步打印配置确认修改生效python tools/misc/print_config.py my_faster_rcnn_r101.py | grep -A 10 backbone你应该能看到depth101和对应的checkpoint路径。第三步用这个配置跑一次训练的前几个iter确认模型能正常构建和前向python tools/train.py my_faster_rcnn_r101.py --cfg-options total_epochs1如果报通道不匹配的错误检查neck.in_channels是否还是[256,512,1024,2048]。ResNet101的通道和ResNet50一样所以这里不用改。但如果你换成ResNeXt101通道会变成[256,512,1024,2048]32x4d或[256,512,1024,2048]64x4d具体看变体这时就要改in_channels。注意换backbone后预训练权重的加载可能会因为key不匹配而部分失败。框架会打印哪些key没加载上如果只是分类头没加载正常因为检测的分类头是新的可以忽略如果是backbone的key大量没加载说明权重和结构不匹配要检查style和depth。4.4 实操修改anchor尺度适配小目标再走一个更实用的场景你的数据集里目标普遍偏小默认anchor太大导致召回低。我们来调小anchor。在配置文件里加model dict( bbox_headdict( anchor_generatordict( octave_base_scale2, scales_per_octave4)))octave_base_scale从4降到2anchor基础尺度减半scales_per_octave从3增到4尺度更密集。这样每个位置的anchor数从9变成12计算量略增但对小目标的覆盖更好。改完后同样打印配置确认然后跑训练观察loss_rpn_cls和召回率的变化。如果召回上去了但误检也多了说明anchor太小引入了太多负样本可以适当调高loss_cls的权重或者用ATSSAssigner替代默认的MaxIoUAssigner做自适应分配。5. 常见问题与排查技巧实录5.1 配置改了不生效怎么办这是最高频的问题。排查顺序如下现象可能原因排查方法改了参数但训练行为没变键名写错层级用print_config确认最终配置改了backbone但通道报错neck的in_channels没同步改对照backbone输出通道检查加载预训练权重报key不匹配style或depth不匹配检查权重来源和配置是否一致继承的配置覆盖了你的修改子配置键名和父配置不一致确认键的完整路径我踩过最坑的一次是改num_classes写在了model层级而不是model.bbox_head层级结果配置合并后bbox_head.num_classes还是80训练时类别数对不上loss直接nan。后来养成习惯改任何参数前先print_config改完再print一次对比。5.2 自定义模块怎么注册如果你想加一个自己写的backbone或head需要两步定义类并注册然后在配置里用你的type名。from mmdet.registry import MODELS import torch.nn as nn MODELS.register_module() class MyBackbone(nn.Module): def __init__(self, in_channels3, out_channels256): super().__init__() self.conv nn.Conv2d(in_channels, out_channels, 3, padding1) def forward(self, x): return [self.conv(x)]然后在配置里写backbonedict(typeMyBackbone, out_channels512)。注意你的模块必须被import到否则注册表里没有这个键。通常做法是把自定义模块放在项目目录下在配置里用custom_imports导入或者直接在训练脚本里import。5.3 显存不够时改哪里显存不够是另一个高频问题。按影响从大到小可以调这些减小imgs_per_gpu旧版或samples_per_gpu新版这是最直接的。减小输入尺寸在train_pipeline里改Resize的scale。把backbone.frozen_stages设大冻结更多层。减小neck.out_channels比如从256降到128。用梯度累积模拟大batchoptimizer_config里设cumulative_iters。我一般优先调batch size和输入尺寸因为这两个对精度影响可控且容易恢复。改模型结构是最后手段因为会改变模型容量可能影响最终精度。5.4 多尺度训练怎么配多尺度训练mstrain对检测精度提升明显配置里通常在train_pipeline的Resize里加multiscale_modedict( typeResize, img_scale[(1333, 640), (1333, 800), (1333, 960)], multiscale_modevalue, keep_ratioTrue)multiscale_modevalue表示从列表里随机选一个尺度range表示在范围内随机采样。注意多尺度训练会增大显存波动因为最大尺度的特征图更大显存要按最大尺度预留。如果你的显存刚好够单尺度开多尺度可能会OOM这时要么减小最大尺度要么降低batch size。6. 模型解析的进阶方向把上面这些搞明白你已经能读懂和修改绝大多数mmdetection配置了。再往深走有几个方向值得投入一是读mmdet/models/dense_heads下的源码理解正负样本分配和loss计算的细节这是调精度的核心二是研究mmdet/core/bbox/assigners里的各种分配器从MaxIoU到ATSS到SimOTA理解它们的设计哲学三是看mmdet/models/detectors里的检测器基类理解训练和推理的完整流程。我个人在实际操作中的体会是mmdetection的配置体系初看繁琐但一旦理解了“注册表配置驱动继承”这三板斧后面就是查文档和读源码的体力活了。真正花时间的不是搞懂机制而是针对自己的数据集调参和排查各种形状不匹配的问题。建议你从改一个现成配置开始每改一处就print_config确认跑通再改下一处这样积累下来的经验比看十篇教程都管用。