
很多同学第一次打开 mmDetection 的仓库都有一种“从入门到放弃”的冲动mmdet/models目录下躺着backbones、necks、dense_heads、roi_heads一堆文件夹点进configs发现每份配置文件长得像一棵树尤其model字段嵌套七八层里面全是dict。我当年第一次看 Faster R-CNN 的配置时脑子里只有一个念头这玩意儿到底是怎么变成一个能跑的网络的这篇笔记想解决的就是这个问题。我会沿着 mmDetection 的网络架构这条线把model配置里的每个字段掰开揉碎告诉你 backbone、neck、head 各自扮演什么角色它们怎么组合成一个完整的检测器以及如果你想把某一层换掉、加一个模块应该改哪里、怎么改。适合谁看刚接触 mmDetection、跑通了官方 demo 但对配置一脸懵的科研小白以及在框架里做过小改动、却始终没弄清“为什么这样写”的开发者。1. 先建立整体印象mmDetection 的网络分层思想1.1 检测器不是一个黑盒而是一条流水线目标检测和图像分类最大的区别在于它要同时回答两个问题物体在哪里定位和物体是什么分类。分类任务可以干脆利落地拿一个网络把图像映射成类别概率但检测任务不行——你既要提取特征又要在特征上做密集的候选框预测还要对候选框做精修这么多事不可能靠一个单一的“黑盒网络”完成。mmDetection 的做法是把这条链路拆成几个职责清晰的模块典型结构是Backbone主干网络负责从原始图像中提取多层次特征。Neck颈部模块在 backbone 输出的多尺度特征上做融合或增强最典型的就是 FPN。Head检测头在融合后的特征上做最终的分类和回归预测。双阶段检测器还会拆成 RPN Head 和 RoI Head。Detector检测器外壳把上面这些模块拼装起来同时负责训练和推理流程的编排。这个设计思路很像工厂里的流水线原料图像先进第一道工序backbone粗加工再进第二道工序neck做精细处理最后由检测头head完成质检和打包。生产线上的每个环节都可以独立替换这也是 mmDetection 能支持几十种检测器的根本原因。1.2 你一定会见到的几个“角色”字段翻看任意一份 mmDetection 配置model字段下常见的关键字就那么几个model dict( typeFasterRCNN, backbonedict(...), neckdict(...), rpn_headdict(...), roi_headdict(...), )单阶段检测器比如 RetinaNet、YOLO 系列通常只有backbone、neck、bbox_head没有rpn_head和roi_head因为单阶段不需要“先提候选框再精修”这两步直接在特征图上做密集预测。多阶段检测器Faster R-CNN、Mask R-CNN、Cascade R-CNN则多出rpn_head和roi_headRPN 先快速筛出一批可能包含物体的候选框RoI Head 再对这些候选框对应的特征区域做精细分类和回归。这个“粗筛 精修”的思路是双阶段方法在精度上长期优于单阶段方法的关键原因之一。看到这里你就明白了mmDetection 的model配置不是一个“模型的形状描述”而是一份组件清单。每个组件都是一个dict里面写着“我要用什么模块、参数是多少”。搞清楚每个组件的参数含义是理解这份清单的前提。2. 主干网络 Backbone特征的底层来源2.1 ResNet 配置字段逐项解读几乎九成以上的 mmDetection 模型都基于 ResNet。打开一份 Faster R-CNN 配置backbone常常长这样backbonedict( typeResNet, depth50, num_stages4, out_indices(0, 1, 2, 3), frozen_stages1, norm_cfgdict(typeBN, requires_gradTrue), norm_evalTrue, stylepytorch),这些字段看着简单但每一个都可能影响你的实验成败。depth很好理解50 代表 ResNet-50。很多人会随手改成 101然后发现训练速度慢了一大截、显存爆掉这很正常毕竟更深的主干意味着更大的计算量。改depth之前先掂量一下自己的 GPU。num_stages是 ResNet 的 stage 数量。ResNet 默认 4 个 stage每个 stage 对应一个下采样倍率stage1 输出 1/4 分辨率特征stage2 是 1/8stage3 是 1/16stage4 是 1/32。这也是out_indices的可选范围为什么是 0 到 3。out_indices决定 backbone 向外输出哪些 stage 的特征。(0, 1, 2, 3)表示把四个 stage 的特征全部输出。如果你是做语义分割或者轻量检测可能只需要(2, 3)甚至(3,)这样可以省掉前面阶段的输出计算但对检测来说多尺度特征很重要一般会全部输出或者至少输出后三个。frozen_stages是很多人容易忽略的字段。它表示冻结前几个 stage 的参数不参与训练。默认值是 1也就是 stage1 之前的 stem 和 stage1 整个被冻住。这么做的原因是主干网络通常使用在 ImageNet 上预训练的权重浅层特征边缘、纹理比较通用不需要在目标任务上重新学冻结它们可以省显存、加快收敛。如果你在任务上与 ImageNet 差异很大或者数据量很少也可以把frozen_stages设为 0 让全部参数都参与训练但这会显著增加显存占用和训练时间需要自己权衡。norm_cfg和norm_eval是一对需要一起理解的概念。norm_cfgdict(typeBN, requires_gradTrue)表示使用 BatchNorm且 BN 层的weight和bias是允许梯度更新的。但注意norm_evalTrue会强制所有 BN 层在训练时也切换到 eval 模式意思是 BN 的均值/方差不更新只更新缩放和平移参数。这个组合在迁移学习里非常常见预训练 BN 的统计量已经足够好微调时不更新它们可以有效避免小 batch size 带来的统计量抖动。如果你自己改网络时把 BN 换成了 GroupNorm 之类的记得相应调整这套配置。stylepytorch和stylecaffe的区别在于 stem 结构。PyTorch 官方实现里 ResNet 的 stem 是一个 7x7 卷积加一个 3x3 的 max poolingCaffe 版本则没有 pooling或者在第一层卷积上直接 stride2。这个字段影响了 backbone 实际搭出来的结构也决定了加载预训练权重的方式通常保持默认的pytorch不需要管。2.2 更换主干网络时的三个注意点我在实际项目里换过不少 backbone踩过的坑基本集中在三个地方。第一注意out_indices与后面 neck 的in_channels要对齐。比如 ResNet-50 四个 stage 的输出通道数是[256, 512, 1024, 2048]如果你换了一个输出通道数不同的主干比如某些轻量网络输出[24, 56, 120, 248]那 FPN 的in_channels必须同步修改否则 1x1 卷积的输入维度直接 mismatch跑起来就是 TypeError。第二预训练权重的键名匹配问题。mmDetection 加载预训练权重时是拿 state dict 的 key 做严格匹配的如果你换了一个结构稍微不同的 backbone比如 ResNet 的 stem 换了那么预训练权重里对应层的 key 就对不上。这时候你需要在训练脚本里加--cfg-options model.backbone.pretrained...或者用load_from配合strictFalse来忽略不匹配的层。有一次我改了 stem 的结构忘了处理权重加载结果训练了 20 个 epoch 后 mAP 一直在 0 附近排查了半天才意识到是预训练权重根本没加载上。这个问题很隐蔽因为训练不会报错只是精度起不来。第三frozen_stages和norm_eval需要根据你的 backbone 类型调整。如果你换了一个没有 BN 层的 backbone或者用的 BN 在微调时会产生很大问题保持默认配置反而会拖慢收敛。适合初学者的建议是先保持默认配置跑通确认架构没问题再去调这些“精细化”参数。3. 特征金字塔 Neck让不同尺度的目标都被看见3.1 FPN 的配置与机制FPNFeature Pyramid Network是当前检测框架里最常见、也最重要的 neck。它在配置里长这样neckdict( typeFPN, in_channels[256, 512, 1024, 2048], out_channels256, num_outs5),这份配置的意思是FPN 接收来自 backbone 的四个不同尺度的特征通道数分别是 256、512、1024、2048经过处理后统一输出 256 通道的特征图最终向外输出 5 个尺度的特征。FPN 的核心操作分两条路径。自底向上的路径就是 backbone 本身——分辨率越来越低但语义越来越丰富。FPN 额外加了一条自顶向下的路径从分辨率最低但语义最强的 stage4 特征开始通过 1x1 卷积统一通道数然后逐级上采样与自底向上的特征做逐元素相加。这样做的效果是高层特征获得了高分辨率的细节信息低层特征获得了丰富的语义信息每一层都同时拥有“是什么”和“在哪里”两种能力。目标检测中尺度变化很常见——一张图里大象可能占半个画面远处的猫只有 20 个像素。没有 FPN 之前老方法要么只在单一特征图上检测对不同尺度很不友好要么在多个尺度上分别跑一遍网络计算量爆炸。FPN 用很小的额外计算量就拿到了多尺度的能力这也是它成为标配的原因。3.2 配置中的几个隐蔽细节num_outs表示 FPN 输出的特征金字塔层数。它通常比in_channels的列表长度多 1比如这里in_channels有 4 个num_outs5意味着除了 backbone 的 4 层特征FPN 还会在最高层1/32 分辨率之上再做一次 stride2 的下采样额外产生一个 1/64 分辨率的特征层通常叫 P6。P6 对检测超大目标有帮助但如果你处理的任务里目标普遍偏小反而没必要保留这一层——小目标天然在高分辨率特征层上更容易被检测到。我曾经在一个小目标数据集上测试过把num_outs从 5 改成 4精度反而略有提升因为 P6 层贡献的全是误检。out_channels是 FPN 所有输出层的统一通道数。它在后续 head 的in_channels里必须保持一致。很多人改网络时只调了 backbone 的深度忘了 FPN 的out_channels也要匹配 head 的输入通道结果又是一堆维度报错。如果你用的是PAFPNPath Aggregation FPN它会在 FPN 的基础上再加一条自底向上的增强路径让底层细节信息也能向上传递对小目标更友好。配置上的差别是把typeFPN换成typePAFPN同时多了num_outs之外的一些下采样配置。YOLO 系列检测器里常用的就是这种结构。经验之谈如果项目里目标尺寸跨度很大优先考虑保留完整的 5 层 FPN如果目标普遍偏小可以试试去掉最深层或者在neck后面接一个尺寸较大的特征层做检测。这类小改动往往比更换 backbone 收益来得更快。4. 检测头 Head分类和回归真正发生的地方4.1 单阶段 dense head 的配置逻辑单阶段检测器的 head 直接在 neck 输出的特征图上做密集预测。以 RetinaNet 为例bbox_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, 8, 16, 32, 64]), bbox_coderdict( typeDeltaXYWHBBoxCoder, target_means[0.0, 0.0, 0.0, 0.0], target_stds[1.0, 1.0, 1.0, 1.0]), loss_clsdict( typeFocalLoss, use_sigmoidTrue, loss_weight1.0), loss_bboxdict(typeL1Loss, loss_weight1.0)),核心概念有两个anchor 的生成和bbox 的编解码。anchor_generator里的strides指的是特征图下采样倍数也就是该层特征图上每个像素对应原图上的步长。特征图的每个像素都会铺上若干 anchor。octave_base_scale4表示基础尺寸为 4 像素scales_per_octave3表示在每个“倍频程”内取 3 种尺度ratios[0.5, 1.0, 2.0]表示 3 种长宽比。所以每个像素上RetinaNet 默认生成3 (scales) x 3 (ratios) 9个 anchor这些 anchor 覆盖了不同尺度、不同形状的目标候选。bbox_coder负责的是“框的表示方式”。检测器一般不会直接回归框的四个坐标绝对值而是回归“相对 anchor 的偏移量”因为直接回归绝对值太难学。DeltaXYWHBBoxCoder把真实框转换成相对于 anchor 的(dx, dy, dw, dh)训练时让网络去预测这四个值。target_stds是归一化用的标准差RPN 里通常是[1.0, 1.0, 1.0, 1.0]RCNN 精修头里通常是[0.1, 0.1, 0.2, 0.2]因为精修阶段框的偏移量更小用更小的标准差可以让回归目标放大一些训练更好收敛。这里要提醒一个非常容易踩的坑num_classes一定不要随便写。它应该等于“目标类别数“不含背景类别。比如你的数据集有 10 类物体num_classes就是 10。如果你从别人那里复制了一份 COCO 的配置里面是 80忘了改训练时 loss 计算、精度评估都会错得离谱而且往往表现出一种“loss 降了但 mAP 极低”的诡异状态。4.2 双阶段 RPN Head RoI Head 的配合双阶段检测器把检测头分成两个阶段这是它在精度上更“讲究”的地方。RPNRegion Proposal Network的作用是快速生成候选框。它在每个特征层上做一次二分类有没有物体和一次粗回归挑出几千个候选框再通过 NMS 筛到几百个。RPN 本身并不知道框里是什么类别它只负责“这里可能有东西”。这个设计来自人类视觉的一个直觉先快速扫一遍场景锁定可疑区域再仔细看清每个区域里到底是什么。RoI Head 则是真正的“精修专家”。它对 RPN 给出的每个候选框用RoI Align从特征图上抠出对应的区域特征然后统一缩放到固定尺寸比如 7x7送到全连接层里做分类和精细回归。RoI Align 是双阶段方法里特别重要的组件。在它之前RoI Pooling 用的是两次量化取整会引入像素级的偏差对小目标尤其不友好。RoI Align 用双线性插值避免了量化误差这也是 Mask R-CNN 之后几乎所有双阶段方法都默认使用 RoI Align 的原因。配置里对应的字段是roi_headdict( typeStandardRoIHead, bbox_roi_extractordict( typeSingleRoIExtractor, roi_layerdict(typeRoIAlign, output_size7, sampling_ratio0), out_channels256, featmap_strides[4, 8, 16, 32]), bbox_headdict( typeShared2FCBBoxHead, in_channels256, fc_out_channels1024, roi_feat_size7, num_classes80, ... ), )这里有个细节roi_layer里的output_size7和bbox_head里的roi_feat_size7必须一致。output_size决定 RoI Align 输出特征图的高宽roi_feat_size决定后面全连接层的输入尺寸。如果你把output_size改成了 14roi_feat_size还留着 7那全连接的输入维度就会对不上。这种错误在改网络时特别容易出现因为报错信息往往发生在网络前向的最后阶段定位起来比较费时间。双阶段方法还有一个隐含的好处RPN 已经把绝大多数背景框筛掉了RoI Head 处理的候选框数量远少于单阶段密集检测的 anchor 数量所以分类器学起来更容易精度自然更高。代价就是推理速度慢不少毕竟多跑了一轮网络。到这一步你应该也理解了为什么单阶段与双阶段的配置结构有那么大差异——本质上就是“一次到位”和“由粗到精”两种思路的区别。5. 一个最小但完整的检测器配置逐行拆解5.1 用 Faster R-CNN 走一遍完整配置我不会让你去对着论文看结构直接拆一份实际的 Faster R-CNN 配置把它当成一张“JSON 地图”逐段翻译。完整的model配置长这样model dict( typeFasterRCNN, backbonedict( typeResNet, depth50, num_stages4, out_indices(0, 1, 2, 3), frozen_stages1, norm_cfgdict(typeBN, requires_gradTrue), norm_evalTrue, stylepytorch), neckdict( typeFPN, in_channels[256, 512, 1024, 2048], out_channels256, num_outs5), 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, 0.0, 0.0], target_stds[1.0, 1.0, 1.0, 1.0]), loss_clsdict( typeCrossEntropyLoss, use_sigmoidTrue, loss_weight1.0), loss_bboxdict(typeL1Loss, loss_weight1.0)), roi_headdict( typeStandardRoIHead, bbox_roi_extractordict( typeSingleRoIExtractor, roi_layerdict(typeRoIAlign, output_size7, sampling_ratio0), out_channels256, featmap_strides[4, 8, 16, 32]), bbox_headdict( typeShared2FCBBoxHead, in_channels256, fc_out_channels1024, roi_feat_size7, num_classes80, bbox_coderdict( typeDeltaXYWHBBoxCoder, target_means[0.0, 0.0, 0.0, 0.0], target_stds[0.1, 0.1, 0.2, 0.2]), reg_class_agnosticFalse, loss_clsdict( typeCrossEntropyLoss, use_sigmoidFalse, loss_weight1.0), loss_bboxdict(typeL1Loss, loss_weight1.0))) )我们来一步一步地看。typeFasterRCNN是检测器外壳的类型它决定了训练流程怎么编排、前向推理走哪条路。backbone是 ResNet-50输出四层特征neck是 FPN把四层特征统一成 256 通道的 5 层金字塔rpn_head在每层特征上生成scales[8]和 3 种宽高比组合出的 3 个 anchor实际 RPN 一般是 3 个因为scales只有 1 个值、ratios有 3 个roi_head用 RoI Align 提取候选框特征然后通过两层全连接做分类和边界框精回归。一个需要注意的对应关系rpn_head的anchor_generator.strides[4, 8, 16, 32, 64]有 5 个值和neck的num_outs5一一对应。如果 FPN 输出层的数量或者这些层对应的 stride 变了这里必须同步修改。这个对应关系在改num_outs时很容易漏掉出错的表现是 RPN 在某个特征层上找不到匹配的 anchor 配置直接报错。bbox_head里的reg_class_agnosticFalse是一个值得理解的字段。False意味着回归分支为每个类别各预测一组边界框偏移量框修正得更精细但参数量也更大最后的全连接层输出维度从 4 变成4 * num_classes。True则表示所有类别共用一组回归参数参数量小、速度更快但精度有轻微损失。实际小数据集项目里我一般会保持默认False这也是官方配置的选择。5.2 从配置到模型的实例化流程配置里的dict最终是怎么变成可执行的网络模块的mmDetection 的答案是读取type字段去对应的注册表里找到这个类的定义把剩下的参数作为关键字参数传进去完成实例化。这条链路的起点是from mmdet.models import build_detector model build_detector(cfg.model, test_cfgcfg.get(test_cfg))build_detector展开之后实际就是调用了build_from_cfg先根据model.type拿到FasterRCNN这个类然后创建backbone、neck、rpn_head、roi_head这些子模块再全部作为参数塞进FasterRCNN的构造函数。子模块的构建方式也是一样的——读各自的type找注册表实例化。这套机制的好处是你不需要写一堆if backbone resnet50: return ResNet()之类的分支代码。所有模块都把自己注册到一个全局字典里配置里写什么type框架就找什么类。这也是为什么你需要看mmdet/models/backbones/resnet.py源码时在文件末尾总会看到一句BACKBONES.register_module() class ResNet(nn.Module): ...如果你调试网络结构时想知道某个中间结果的 shape最简单的办法是在对应模块的forward里打印x.shape。因为模块都是直接通过配置构建的打断点也好、加打印也好都和你自己写的一个普通 PyTorch 模型没有区别这也是 mmDetection 对新手比较友好的地方。6. 网络的可定制性注册器机制与自定义组件6.1 注册器机制到底做了什么这一节可能是 mmDetection 全网架构里最“劝退”也最有价值的部分。注册器Registry本质就是一个“类名到类的映射表”但它的巧妙之处在于把“注册”这件事做成了装饰器。当你新建一个自定义 backbone代码大概是这样的from mmdet.models.builder import BACKBONES BACKBONES.register_module() class MyBackbone(nn.Module): def __init__(self, arg1, arg2): super().__init__() self.arg1 arg1 self.arg2 arg2 ... def forward(self, x): ... return outs只要这个文件被 import 过MyBackbone就会被自动写进BACKBONES这个注册表。之后你在配置里写backbonedict( typeMyBackbone, arg1..., arg2...)框架构建时就会执行BACKBONES.get(MyBackbone)找到这个类然后用arg1和arg2作为参数实例化。我强烈建议你搞清楚mmdet/models/builder.py里那几十行代码里面定义了BACKBONES、NECKS、ROI_EXTRACTORS、HEADS、DETECTORS等注册表。搞清楚这些注册表的名字你就知道自己的模块应该注册到哪里新主干网络 →BACKBONES.register_module()新特征融合模块 →NECKS.register_module()新检测头 →HEADS.register_module()新检测器外壳 →DETECTORS.register_module()这个机制是 mmDetection 支持复用的核心。也正是因为注册器的存在你会发现很多模块的内部实现可能不一样但只要它们注册到同一个表里、接口一致就能无缝替换。这就是“配置驱动”的真正含义。6.2 自定义组件时最常见的三个坑我见过太多人卡在自定义这一步基本都是三类问题。第一类配置里写了typeMyModule运行时报KeyError: MyModule is not in the ... registry。原因几乎总是你虽然写好了类和装饰器但这个文件没有被导入。mmDetection 默认只会导入mmdet/models/__init__.py里的东西你自己新建的.py文件如果不被任何__init__.pyimport注册表里就没有它。解决办法是在对应的__init__.py里加一行from .my_module import MyBackbone。这行代码不直接做事但它触发文件执行装饰器才算生效。第二类build_from_cfg报类型不匹配。这通常是因为你的模块__init__里声明的参数名和配置 dict 里的 key 不一致或者__init__的参数没有给默认值导致cfg里的参数匹配不上。调试方法很简单在模块的__init__里打一个断点看实际收到的 kwargs 是什么。第三类改了网络结构后加载预训练权重报错。这与注册器没关系但最容易让人心态爆炸。报错信息一般是size mismatch for ...。原因很简单预训练权重的层和你的新网络层不完全对应。规范做法是在配置的load_from里指定预训练权重然后训练脚本里用--cfg-options把model.train_cfg相关的严格加载关掉或者在代码里调用load_state_dict时设置strictFalse。不过要注意strictFalse是一个有风险的操作它会静默忽略缺失的层有时候反而掩盖了真正的维度错误。我的建议是加载报错后先明确搞清楚哪些层不匹配确认是预期的结构差异再关掉严格模式。7. 自主修改网络架构的实操经验7.1 一次“换主干加检测头”的完整过程假设你想把默认的 ResNet-50 backbone 换成一个新的轻量网络再在 FPN 后面加一个小模块具体可以怎么做我会分步骤介绍这套流程我自己反复用过很多次是通用的。第一步新建一个文件mmdet/models/backbones/my_backbone.py或者放在你自己的插件目录里实现网络结构。不需要从头写卷积层复用torchvision或timm里的现成模型也行只要保证forward返回一个 tuple 或者 list里面是不同 stage 的特征图。然后在文件末尾加上BACKBONES.register_module()。第二步注册。在mmdet/models/backbones/__init__.py里导入这个文件。注意__init__.py的顺序不影响执行结果只要有 import 语句就够了。第三步改配置。把backbone里的type改成你的类名并根据你的网络实际输出通道数调整out_indices和后面neck.in_channels。一个常见做法是先在骨干网络内部做一次print(outs.shape)跑一次单 batch 的前向确认输出维度和预期一致再改配置。这一步能帮你省大量排查时间。第四步改neck和head的通道数使其与 backbone 输出对齐。如果你只是换了depth而没有换结构这一步甚至可以不动如果你换了完全不同的网络那么in_channels必须改。同时检查一下rpn_head的strides和roi_head里featmap_strides是否和你的 FPN 输出层级 match。第五步处理预训练权重。如果新 backbone 有预训练权重用load_from指定如果结构差异很大用转换脚本先对齐状态字典里的 key实在对齐不了的才考虑strictFalse。整个过程听起来繁琐但这就是科研里最常见的“改配置迭代”流程。跑通一次之后你会觉得灵活度很高所有组件的替换逻辑其实都是一样的。7.2 常见问题排查速查表我把实际项目里高频出现的报错和解决办法整理如下现象原因解决办法KeyError: xxx is not in the ... registry模块没有注册确认xxx.register_module()存在且文件被 import前向时报size mismatch某个模块的输入输出通道数不一致检查 backbone 输出通道、neckin_channels、headin_channelsRoI Align相关 shape 错误roi_layer.output_size和roi_feat_size不一致保持两个值一致并检查 head 里 fc 层的输入维度加载权重时报size mismatch新网络和预训练权重结构不同调整 key 对齐必要时设置strictFalse训练 loss 为 NaN学习率太大或正则化设置不当降低学习率检查norm_cfg设置排查数据是否存在异常值改了num_classes但评估结果异常类别数不匹配确认num_classes等于数据集的类别数量不含背景类显存溢出batch size 过大或 backbone 过深减小 batch size降低输入分辨率考虑使用更浅的 backbone还有一个容易被忽略的问题很多人会直接修改官方配置里的model字段但配置继承时没有处理好。如果一份配置用了_base_继承另一个配置你在子配置里写了新的model那么整体 model dict 会替换掉 base 里的 model而不是做深层的逐 key 合并。如果你只是想覆盖某一项比如只改num_classes正确的写法是写一个modeldict里面只放需要覆盖的字段吗不对——mmDetection 的配置继承只做顶层 dict 的合并所以你要改model.roi_head.bbox_head.num_classes最安全的做法是把整条链路都写全modeldict(roi_headdict(bbox_headdict(num_classes10)))。从顶层开始每一级都给出 dict避免合并时悄悄丢了其他字段。我在早期因为这个机制吃过亏只写了modeldict(roi_headdict(bbox_headdict(num_classes2)))结果整个roi_head被替换成了只有bbox_head的 dict跑起来一堆配置缺失。搞清楚配置继承规则能帮你少折腾很多个晚上。最后聊几句我在实际使用这个框架的过程中体会到mmDetection 的网络架构设计最值得学习的反而不是某个检测算法本身而是它这套模块拆分 注册机制 配置驱动的组装理念。刚开始学的时候别急着追求理解每个算法的数学细节先学会“看配置猜结构”拿到一份配置在脑子里过一遍 backbone 的输出是什么形状、neck 怎么融合、head 在哪一层做分类和回归基本上网络就活了。等你改过几次 backbone、加过几次自定义模块之后再回头看那些论文里的检测器会发现它们之间的距离其实没有想象中那么大。最后再分享一个小技巧如果你想快速验证某个 backbone 改动到底效果如何不需要把整个训练流程跑完。先拿一个很小的数据集、只训练几个 epoch观察 loss 的下降趋势和大致精度心里有数了再上全量数据。这个阶段跑得快迭代才会快。祝你的实验早日跑通指标早日漂亮起来。