资讯详情 MMDetection网络结构全解析:Backbone、Neck、Head与Loss设计
📅 2026/10/13 12:24:39
1. 整体思路拆解mmDetection 的网络部分为什么要这么设计我刚开始接触 mmDetection 的时候就一个感觉模块太多了。一个检测器拆成 backbone、neck、head、loss到处是 config、registry、builder看两天源码直接想退坑。但后来切换过几个检测框架之后再回头来看才意识到这类框架把网络拆得这么细本质上是在帮你做一个特别重要的事——让“改网络”这件事变得像“搭积木”一样有章可循。先看一个最直观的问题一个目标检测模型从输入图像到输出预测框中间到底发生了什么简单说就是四步。图像进 backbone 提特征neck 把这些特征做多尺度融合head 在融合后的特征图上预测目标的位置和类别最后拿预测结果跟真实标注算 loss 来反传更新参数。mmDetection 的整个网络架构就是严格围绕这条流水线来组织的。你打开任何一份模型配置看到的 model 字段几乎都是这个结构model dict( typeFasterRCNN, backbonedict(...), neckdict(...), rpn_headdict(...), roi_headdict(...), train_cfgdict(...), test_cfgdict(...) )这种组织方式其实是从工程实践里长出来的。很多研究者在改模型的时候通常只动其中某一个环节——换一个更强的 backbone或者换一种 FPN 的变体很少有人把整个检测器从零重写。如果框架把网络结构写死换个模块就要牵一发动全身那做实验的效率就太低了。所以 mmDetection 把网络拆成“组件”每个组件用字符串注册表管理配置文件里写清楚每个组件用哪个类、传什么参数框架启动时再根据配置把整个模型装配起来。这样做带来的直接好处有三个。第一个是对比实验变得极其便宜。你想试试 Swin Transformer 比 ResNet 好多少不用改检测头的代码配置文件里把 backbone 的类型换掉、参数填上就行。第二个是可读性变好了一个上千行的模型文件被拆成各自独立的小文件每个模块的职责单一代码读起来比动辄几百行的单体模型清晰得多。第三个是扩展新结构时有清晰的位置可以放。我自己的经历是第一次在框架里加自定义模块本来以为要理解全部源码实际上只需要写一个类、注册进去、改配置大部分时间花在理解自己要加的模块怎么和上下游对接上而不是跟框架本身搏斗。当然这种拆分也带来了一些学习成本。最典型的就是“代码在哪儿”的问题。你在配置文件里写typeResNet想去看它的实现得知道框架的 registry 机制顺着BACKBONES.register_module()去找到源码位置。刚开始会很不习惯觉得一个模型怎么被拆得东一块西一块。但等你习惯了这种“配置 注册 构建”的模式反过来写代码也会不自觉地往模块化的方向靠。2. 四个核心组成部分backbone、neck、head、loss 各自管什么2.1 Backbone特征提取的“底子”决定模型能“看”到什么程度Backbone 是整个检测模型的“眼睛”负责从原始图像里提取特征。它的输入是一张 3 通道的图比如 800×800×3输出是一系列特征图通常是原图下采样若干倍的结果——比如 stride 为 4、8、16、32 的特征图通道数从 64 到 2048 不等。为什么需要不同尺度的特征图这一点对小白来说很容易忽略。因为一张图里的目标大小差异很大一只鸟可能只占图像的一小块一辆大巴车几乎横跨整张图。尺度小的特征图比如 stride 32感受野大适合找大目标尺度大的特征图比如 stride 4保留了更多细节适合找小目标。backbone 天然的层级结构本身就会在不同 stage 输出不同分辨率的特征后面 neck 要做的其实就是把这些不同层的特征更好地利用起来。在 mmDetection 里backbone 有很多现成的选择。ResNet 系列最常见因为它简单、稳定、在大规模数据集上预训练权重好找。Swin Transformer 这类结构在近年来的论文里很流行但计算量大显存占用高做实验前要确认自己的显卡扛不扛得住。用 backbone 的时候有几个参数需要重点关注。depth决定用 ResNet 的哪个版本是 50 还是 101层数越多能力越强但显存和训练时间也翻倍。out_indices决定输出哪些层的特征比如out_indices(0, 1, 2, 3)表示输出四个 stage 的特征这个参数直接决定了后续 neck 能拿到哪些尺度的特征图。norm_cfg设置归一化方式比如norm_cfgdict(typeBN, requires_gradTrue)是训练时冻结 BN 层的统计量用预训练模型微调或者做检测任务时经常要这么设。2.2 Neck多尺度特征融合的“中转站”Backbone 输出的多层特征虽然包含了不同尺度的信息但直接拿来做预测还不够好。因为低层特征语义信息弱有太多低级的纹理和边缘信息高层特征分辨率太低小目标的细节全丢了。Neck 的职责就是把这些特征做整合让每一层都同时具备“高分辨率细节”和“强语义信息”。最经典的 neck 就是 FPN特征金字塔网络。它的工作方式可以简单理解为两条路径。自底向上的路径就是 backbone 各层输出的原始特征自顶向下的路径从最高层开始通过最近邻上采样把分辨率放大跟下一层特征做逐元素相加每一层融合后再接一个 1×1 卷积调整通道数。这样一来每一层输出特征都同时包含了浅层的细节和深层的语义小目标和大目标都能得到比较好的表达。mmDetection 里 FPN 的典型配置长这样neckdict( typeFPN, in_channels[256, 512, 1024, 2048], out_channels256, num_outs5, )in_channels要严格对应 backbone 输出各 stage 的通道数ResNet50 的四个 stage 输出通道正好是 256、512、1024、2048。out_channels是 FPN 每层统一输出的通道数一般设 256 就够了。num_outs是最终输出多少层特征图设为 5 表示在原本 4 层的基础上还会额外从最高层往下采样一层多出来的这层是用来捕捉超大目标的。从实际使用体验讲FPN 几乎是所有两阶段检测器的标配。你完全可以把 neck 换成其他结构比如 PANet在 FPN 基础上额外增加了一条自底向上的路径或 BiFPN但要注意跟 head 的输入要求对齐。改了 neck 的输出层数或者通道数下游 head 的in_channels也要跟着改否则构建模型的时候直接报错。2.3 Head真正出预测结果的地方Head 是模型做“决策”的部分它接在 neck 输出的特征图之后完成两个任务告诉你在哪里有一个目标回归边界框坐标以及这个目标是什么分类。在 Faster R-CNN 这个经典两阶段模型里head 被拆成两个。第一个是 RPNRegion Proposal Network它的作用是“先粗筛”在特征图的每个位置上预设一堆不同大小和长宽比的锚框然后判断这些锚框里有没有目标并粗略调整锚框的位置。第二个是 ROI Head它接收 RPN 筛选出来的候选框从特征图上裁剪出对应的区域然后做精细的分类和回归。两阶段的设计逻辑其实是把“找目标”这件事拆成了“先找出可能的位置再仔细确认”两步每一步的难度都降低了。但代价是结构更复杂、速度更慢。所以目前很多工业落地场景倾向用单阶段检测器比如 RetinaNet 或 YOLO 系列把分类和回归一次性完成速度更快但精度上通常需要一些 trick 才能追平两阶段模型。在 mmDetection 里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, 0.0, 0.0], target_stds[1.0, 1.0, 1.0, 1.0] ) )这里面的anchor_generator定义了锚框的生成策略。scales[8]表示基础锚框的尺度ratios是长宽比strides对应特征图的步长。每一层特征图上的锚框数量是len(scales) × len(ratios)个。比如这里就是每层 3 个锚框。理解锚框的生成方式很重要因为 anchor-based 模型的检测能力上限很大程度上取决于锚框设置是否匹配你的数据集——如果你的数据集里目标普遍很长条形那 ratios 里最好加上 5:1 之类的极端比例否则召回率上不去。2.4 Loss训练信号从哪来怎么算网络结构搭好之后还差最后一块拼图怎么告诉模型“你预测错了”。这部分不直接体现在模型结构里但对网络训练至关重要。mmDetection 里最常用的分类损失是 CrossEntropyLoss配合class_weight可以处理类别不平衡的问题。回归损失的选择则有很多讲究。早期多用 SmoothL1Loss它对离群值不敏感训练更稳定。后来一些论文提出 GIoU Loss、DIoU Loss、CIoU Loss它们直接优化预测框和真值框之间的 IoU收敛更快对小目标也更友好。在配置里loss 是绑定在 head 上的loss_clsdict( typeCrossEntropyLoss, use_sigmoidTrue, loss_weight1.0 ), loss_bboxdict( typeL1Loss, loss_weight1.0 )loss_weight是各类损失之间的平衡权重。两阶段模型里 RPN 的损失和 ROI Head 的损失是分开计算的最终的总损失是它们的加权和。如果你发现模型训练时定位不准但分类很准可以适当调大loss_bbox的权重反过来如果分类错误多就把loss_cls的权重调大。这个平衡在实验里是需要反复试的没有绝对标准的答案。3. 用一份真实配置把整个网络串起来Faster R-CNN 网络架构全解析前面把各个组件单独讲了这一步我用一份完整可跑的 Faster R-CNN 配置来做一次拆解看看这些组件到底是怎么拼成一个完整网络的。建议你打开自己的 mmDetection 项目随便找一份faster_rcnn_r50_fpn_1x_coco.py对照下面的内容一起看。3.1 从配置文件认识整体模型结构完整配置的核心 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, init_cfgdict(typePretrained, checkpointtorchvision://resnet50) ), 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(...), 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(...), loss_clsdict(...), loss_bboxdict(...) ) ), train_cfgdict( rpndict( assignerdict(typeMaxIoUAssigner, pos_iou_thr0.7, neg_iou_thr0.3, ...), samplerdict(typeRandomSampler, num256, pos_fraction0.5, ...), ... ), rcnndict( assignerdict(typeMaxIoUAssigner, pos_iou_thr0.5, neg_iou_thr0.5, ...), samplerdict(typeRandomSampler, num512, pos_fraction0.25, ...), ... ) ), test_cfgdict( rpndict(...), rcnndict(...) ) )别看这份配置密密麻麻逐个字段拆开其实就两件事定义网络的组件和定义训练/推理过程的行为。backbone、neck、rpn_head、roi_head 属于前者train_cfg 和 test_cfg 属于后者。3.2 配置和数据流之间的对应关系模型构建完成之后一次完整的前向传播大概是这样的输入一张任意尺寸的图先被数据预处理缩放到配置里的img_scale比如 (1333, 800)保持长宽比不变缩放到短边 800、长边不超过 1333。之后图像送入 backboneResNet 的四个 stage 依次输出四种不同尺度的特征图。这些特征图进 FPNFPN 内部做上采样和融合输出 5 层特征P2-P6。RPN 在这 5 层特征上分别滑动生成锚框用一个小网络判断每个锚框里有没有目标同时给出粗略的坐标修正。推理时 RPN 会从成千上万个锚框中筛出几百个得分最高的候选框。ROI Head 接收这些候选框用 RoIAlign 从对应特征图上抠出固定大小比如 7×7的特征块送进两层全连接网络做精细分类和回归最终输出类别和精确的边界框坐标。值得留意的是数据在这个流程中每个环节的尺寸和通道数变化决定了配置里每个字段该怎么填。比如 FPN 的in_channels必须和 ResNet 的out_indices指定的 stage 输出通道对上RPN 的in_channels必须等于neck的out_channels。很多小白调配置报错百分之七八十都是这类“通道数没对上”的问题。3.3 训练阶段和推理阶段的网络行为差异同一个模型结构训练和推理时行为不完全一样。训练时 RPN 和 ROI Head 都需要“采样”正负样本所以 train_cfg 里有pos_iou_thr、neg_iou_thr、sampler这些字段它们控制哪些锚框算正样本、哪些算负样本、各采样多少个。RPN 的 positve IoU 阈值设在 0.7意思是锚框跟真值框的 IoU 大于 0.7 才被认为是正样本ROI Head 的阈值只有 0.5因为此时候选框已经被 RPN 粗筛过一轮标准可以适当放宽。推理阶段这些采样过程不参与test_cfg 里主要控制 NMS 的参数比如nms_thr0.7表示两个 IoU 超过 0.7 的框只保留得分最高的那个max_per_img100表示每张图最多输出 100 个检测结果。配置里的typeFasterRCNN决定了使用哪个顶层模型类这个类会把 backbone、neck、rpn_head、roi_head 按顺序组织起来并且实现完整的 train_step 和 val_step 流程。如果你想自定义一个全新的检测器结构——比如加一个额外的分支网络——最简单的入手点通常是复制一份现有的 detector 类在 forward 流程里插入你的模块而不是从零实现一个 model 类。这个思路适配大多数论文里的“改进型检测器”。4. 我要换网络组件怎么办注册机制与自定义模块的实操流程4.1 Registry 机制到底是怎么工作的mmDetection 里改网络最常见的方式是“换组件”而不是“改源码”。这背后依赖的是一套叫 Registry 的注册机制理解它基本等于掌握了框架的扩展接口。它的核心逻辑用大白话说就是框架在某个模块文件里维护了一张“名称到类”的映射表类定义的时候用装饰器把自己注册进去。配置里写的typeXXX相当于在表里查一下用的哪个类再根据配置里的其他参数创建实例。具体到代码层面是这样的。框架有一个全局变量本质是个字典class Registry: def __init__(self, name): self._registry {} def register_module(self, nameNone): def decorator(cls): self._registry[name or cls.__name__] cls return cls return decorator然后任何 backbone 都可以这样注册BACKBONES.register_module() class ResNet(nn.Module): ...启动时框架调用build_backbone(cfg)根据 cfg 里的type在BACKBONES._registry里查类生成实例。这套机制最大的好处是你完全不需要修改框架源码就能往里面塞新模块。只需要确保你写的类所在的文件被 import 过就行。在 mmdet 的代码里mmdet/models/backbones/__init__.py里会有类似from .resnet import ResNet的导入语句把模块加载进来。自定义文件也必须在某个被执行到的位置被 import否则注册操作不会发生框架找不到你的类。4.2 动手实现一个自定义 Backbone 并接入训练假设我想用某种简单的自定义 backbone 做实验。第一步写类文件比如放到mmdet/models/backbones/my_backbone.pyimport torch.nn as nn from mmdet.models.builder import BACKBONES BACKBONES.register_module() class MyBackbone(nn.Module): def __init__(self, in_channels3, out_channels[64, 128, 256], strides[4, 8, 16]): super().__init__() self.stages nn.ModuleList() for i, (oc, s) in enumerate(zip(out_channels, strides)): self.stages.append( nn.Sequential( nn.Conv2d(in_channels if i 0 else out_channels[i-1], oc, 3, strides, padding1), nn.BatchNorm2d(oc), nn.ReLU(inplaceTrue) ) ) self.out_channels out_channels self.strides strides def forward(self, x): outs [] for stage in self.stages: x stage(x) outs.append(x) return tuple(outs)第二步在mmdet/models/backbones/__init__.py里加上from .my_backbone import MyBackbone第三步修改配置backbonedict( typeMyBackbone, in_channels3, out_channels[64, 128, 256], strides[4, 8, 16] )同时把 neck 的in_channels改成[64, 128, 256]。然后就可以开始训练了。这个过程看起来简单但有几个坑是新手一定会踩的。一个是输出特征的通道数必须和 neck 的 in_channels 严格对齐另一个是输出特征图的 stride 要能覆盖你数据集里目标的主要尺度范围。如果数据集里小目标居多backbone 的 stride 从 4 开始还不够你可能需要 stride2 甚至更密集的特征层这需要重新设计下采样结构。4.3 修改现有 Head 的注意事项相比从头实现一个 backbone更常见的实验操作是在现有 head 上做小改动。比如想把 Faster R-CNN 的 bbox head 从两层全连接改成三层或者加一个 dropout。这些操作的切入点是直接继承现有的 head 类重写相关方法。举个例子假设我想在Shared2FCBBoxHead基础上多加一层全连接。正确做法是写一个子类from mmdet.models.roi_heads.bbox_heads.bbox_head import BBoxHead from mmdet.models.builder import HEADS HEADS.register_module() class Shared3FCBBoxHead(BBoxHead): def __init__(self, num_fcs3, fc_out_channels1024, *args, **kwargs): super().__init__(*args, **kwargs) self.fcs nn.ModuleList() in_channels self.in_channels for i in range(num_fcs): fc_in in_channels if i 0 else fc_out_channels self.fcs.append(nn.Linear(fc_in, fc_out_channels)) def forward(self, x): # 实现细节省略 ...然后配置里把bbox_head的type改成Shared3FCBBoxHead注册后就能直接用。这类改动通常不需要动 loss 和 assigner 的配置因为输入输出的语义没变——输入还是 RoIAlign 抠出来的特征输出还是分类分数和边界框回归量。从这里也能看出mmDetection 的自定义难度是分层的。加一个小改动比如给某个模块换种激活函数只需要改类内部实现换启一个新结构比如把 FPN 换成 PANet主要工作在写新的 neck 类和对齐通道数而设计全新的检测范式比如 DETR 那种抛弃锚框和手工组件的方案就需要对整个 model 类做改造了。新手的进阶路线应该是从第一层开始逐步往后走不要一上来就想着改整个模型。5. 从实际训练结果反推网络问题几次踩坑实录与排查建议5.1 现象一损失能下降但 mAP 一直很低这个情况我遇到过不止一次而且特别容易出现在自定义 backbone 或者修改了 FPN 结构的实验里。损失曲线看起来一切都正常下降得很平滑但验证集的 mAP 就是上不去。后来排查发现典型原因有两个。第一个是 final 层特征图和 GT 的尺度对不上。比如你修改了 backbone 的下采样倍数但没同步修改 anchor_generator 的 strides导致某些尺度的目标在所有锚框设置下都没有合适的匹配模型学到的东西几乎没有正样本支撑自然无法收敛到好的效果。第二个是 FPN 的输出层数变得太多或太少时RPN 的 assigner 和 sampler 配置没适配导致正样本数量过少或者正负样本比例失衡。遇到这类问题我的排查顺序一般是先打印训练日志里每个 loss 的数值分布如果loss_rpn_cls一直很低且不波动说明 RPN 根本没学到东西优先检查 anchor 设置和 assigner 阈值。如果 RPN 正常但loss_cls表现差再去看 RoI Head 的采样配置。5.2 现象二显存占用异常爆高有一次我用 Swin-T 替换 backbone同样 batch size 下显存直接翻了三倍。一开始我还以为是数据加载的问题排查之后发现是 backbone 的out_indices没有调整Swin-T 输出特征图的通道数比 ResNet 大很多FPN 跟着爆显存。解决思路是在模型结构不变的前提下合理调整 FPN 的out_channels。比如把默认的 256 降到 128 或者 64虽然理论上会损失一点精度但显存压力会小很多。另一个常见做法是在配置里给不同尺度特征图设置不同的梯度回传策略比如冻结 backbone 的前几层让低层特征不参与反传显存占用也会大幅下降。还有一个特别隐蔽的坑训练时norm_evalTrue设置的是 backbone 的 BN 层不更新统计量但如果你自定义的模块里也用了 BN默认情况下它是会正常更新统计量的。训练和测试时的行为不一致会直接导致验证集性能崩掉。所以自定义网络里凡是用了 BN 的地方都要确认是否需要在训练时冻结。5.3 现象三换了数据集之后训练发散这个现象也极其常见。从 COCO 预训练模型迁移到自己的数据集上如果你只改了num_classes其他参数全部沿用 COCO 的设置大概率会遇到前几个 iteration 的 loss 变成 NaN 或者冲得非常高。原因一般是学习率没调回来。COCO 数据集大默认设置的初始学习率是 0.02配合 batch size 8 有配套的 warmup 策略。自己的数据集如果是几千张的小数据集这个学习率太高了第一个 step 就可能把网络参数冲散。这时候可以把学习率降到 0.005 甚至 0.001同时把 warmup 的warmup_iters稍微调大比如从 500 调到 1000给网络一个缓慢进入状态的窗口。另外检查一下数据预处理里有没有做 Normalize如果像素值范围不符合预训练模型的输入分布也会导致训练不稳定。5.4 如何高效定位是哪个模块出了问题新手最容易踩的误区是一上来就改训练配置其实多数网络问题是可以直接从代码层面定位的。我的做法是先做一个“数据流检查”用一个小脚本或者直接在训练脚本里 debug把一张图送入模型打印中间每一层输出的 shape。如果某个环节的 shape 不符合预期问题就锁定在那一层上下游的配置上。具体来说要检查的信息包括backbone 输出的层数是否等于out_indices的长度、每层通道数是否和 neck 的in_channels一致、FPN 输出的层数和步长是否和 anchor_generator 的strides一致、RoIAlign 的featmap_strides是否匹配 FPN 的实际输出步长。只要这几处的形状全部对上网络结构本身基本不会有大问题。还有一个值得养成的习惯改完网络配置之后先跑一个 1 到 2 个 iteration 的 smoke test确认能正常前向和反传再启动正式训练。mmDetection 官方配置里一般都有--test或者直接用小数据集跑少量 iteration 的做法。比起训练一晚上第二天早上发现 NaN花五分钟做一次前向测试要划算得多。6. 一些实操过程中的个人体会前面讲了这么多原理和配置相关的细节最后结合我的实际使用感受再补几点。这些东西很难从官方文档里直接学到但确实能帮你在用这个框架时少走不少弯路。第一个体会是初学阶段不要死磕源码。mmDetection 的代码量很大想读完全部源码再开始做实验基本等于放弃。更高效的路径是“用起来再回头看”先拿一份官方配置跑通训练和测试遇到不懂的模块再用 debug 方式去看它的输入输出和内部结构这时候理解会比直接空读源码快得多。我自己就是在改了三次配置文件之后才真正对 registry 机制有感觉的。第二个体会是配置文件的嵌套和继承关系要尽早掌握。mmDetection 的配置支持_base_继承很多官方配置都从faster_rcnn_r50_fpn.py这类基础配置里引用了公共设置。你如果复制一份完整配置来改文件会很长很乱但如果你理解了继承机制自定义实验时只需要新建一个很小的配置文件把要改的字段覆盖掉就行。这不仅让配置更清晰也便于做对比实验——同一份 base跑不同的改法一目了然。第三个体会是关于网络架构学习的顺序。如果你是想系统地理解检测网络建议不要每篇论文都去复现而是把一个模型吃透。把 Faster R-CNN 的每个环节都搞清楚从数据流到损失计算到后处理全都走一遍之后再去看 RetinaNet、Cascade R-CNN 或者 DETR 这些结构会发现它们只是在某个环节做了替换或增强理解成本会骤然降低。反过来如果每个模型都浅尝辄止很快就会迷失在层出不穷的新结构里。第四个体会是在改网络的实验规划上。每次只改一个变量这一点怎么强调都不过分。很多初学者特别喜欢一次换好几个模块——换 backbone 又换 neck 又改 loss 权重最后模型效果确实变好了但根本不知道是哪个改动起的作用。科研实验讲究可解释性一次一个变量记录清楚每次变化的差异这个习惯会大大提升你做实验的效率和质量。最后再说一个小技巧。如果你的实验室机器 GPU 资源紧张做网络架构改动实验时可以在配置里把interval调大评估间隔拉长减少验证带来的时间和显存开销。另外用混合精度训练fp16能显著降低显存占用和加速训练mmDetection 里把fp16 dict(loss_scale512.)配置加到训练设置里就行大多数结构都兼容亲测稳定。网络架构这部分是理解和修改 mmDetection 的核心也是后续所有实验的基础。把这个部分的逻辑理顺了后面不管是做模型改进、新数据集适配还是工程部署都会顺手很多。