news 2026/10/17 15:19:53

深入解析MMDetection训练入口train.py:从启动到收敛的完整流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入解析MMDetection训练入口train.py:从启动到收敛的完整流程

1. 从一次训练中断说起:为什么值得花时间读train.py

很多人用 MMDetection 跑通第一个 demo 之后,就再也没打开过tools/train.py。配置文件改一改,命令行敲一下python tools/train.py configs/xxx.py,看到日志开始刷 iteration 就觉得自己已经会了。直到某天遇到下面这些情况,才发现绕不过去:

  • 训练到一半中断,想恢复却不知道--resume到底从哪里读状态;
  • 想改一下验证频率,翻遍 config 找不到对应字段,最后发现是 runner 在管;
  • 多卡训练时日志只在一个进程里打印,想加个自定义输出完全不知道往哪插;
  • 想接自己的分布式后端或者换一个更细粒度的 checkpoint 策略,发现 config 里根本没有对应开关。

这些问题的答案,几乎全部藏在tools/train.py这个看起来只有一两百行的入口脚本里。它本身不实现任何模型、不写任何 loss,但它决定了配置怎么被解析、模型怎么被构建、训练循环怎么被驱动、日志和权重怎么被保存。换句话说,train.py是 MMDetection 整个训练体系的“总调度台”。

这篇内容面向的是已经能跑通 MMDetection 基础训练、但想真正搞懂训练入口的读者。我会按“一个训练脚本从启动到收敛”的真实执行顺序,把train.py拆成几块讲清楚:参数解析与配置加载、环境与随机性设置、模型与数据集构建、runner 的组装逻辑、以及那些只有踩过坑才会注意到的细节。读完之后,你应该能自己动手改造这个脚本,而不是只能被动接受默认行为。

需要先说明一点:MMDetection 的版本迭代比较快,train.py在不同大版本之间结构有差异。下面讲的是以 MMEngine 为训练引擎的 3.x 体系为主,同时会点出和旧版 2.x 的关键区别,因为现在还有大量项目跑在 2.x 上,两边都懂才不至于看别人的代码时一脸懵。

2. 启动前的第一道关卡:参数解析与配置加载链路

2.1 argparse 里那几个参数到底在控制什么

打开train.py,最先看到的一定是parse_args()。别看参数不多,每一个都对应着训练流程里一个明确的开关。我把它整理成一张表,方便对照:

参数作用容易踩的坑
config指定配置文件路径相对路径是相对于当前工作目录,不是脚本目录
--work-dir覆盖配置里的工作目录不指定时用 config 里的work_dir,两者都没有会报错
--resume从最近一次 checkpoint 恢复传auto和传具体路径行为不同
--no-validate训练中关闭验证只在训练期生效,不影响最终测试
--seed设置随机种子不设置时结果不可复现
--deterministic开启确定性训练会牺牲一点速度,且部分算子不支持
--cfg-options命令行覆盖配置字段嵌套字段用点号连接,写错不报错只是不生效
--launcher指定分布式启动方式单卡时一般用 none

这里面最值得展开的是--resume和--cfg-options。

--resume的行为逻辑是这样的:如果不传,就是从头训练;如果传auto,脚本会去work_dir里找最新的last_checkpoint文件,读出里面记录的 checkpoint 路径再加载;如果传一个具体路径,就直接加载那个文件。很多人以为--resume是个布尔开关,其实它接受字符串值,auto是它的一个特殊取值。这个设计的好处是,你不需要手动去记上一次存到哪个 epoch 了。

--cfg-options则是调试利器。比如你想临时把学习率调小验证一下,不用去改配置文件,直接:

python tools/train.py configs/xxx.py --cfg-options optimizer.lr=0.001

它的解析规则是把key=value按点号拆成嵌套字典,然后深度合并到原配置上。这里有个坑:值会被当成字符串解析,如果你写optimizer.lr=0.001,它会被转成浮点数,但如果你写一个列表或者更复杂的结构,就得用类似 JSON 的写法。而且如果 key 路径写错了,它不会报错,只是默默加了一个没人用的字段,你会纳闷为什么改了没效果。

2.2 Config 从文件到对象的完整旅程

参数解析完之后,核心动作是Config.fromfile(args.config)。这一步看起来简单,实际上做了不少事。

MMDetection 的配置文件支持继承,一个典型的 config 会长这样:

_base_ = [ '../_base_/models/faster-rcnn_r50_fpn.py', '../_base_/datasets/coco_detection.py', '../_base_/schedules/schedule_1x.py', '../_base_/default_runtime.py' ]

fromfile会递归地把_base_里的文件先加载进来,然后再用当前文件的字段去覆盖。覆盖是深度合并,不是简单替换。举个例子,base 里model.backbone.depth=50,你在当前文件写model=dict(backbone=dict(depth=101)),最终depth会变成 101,而 backbone 里其他没写的字段保持不变。这个机制是 MMDetection 配置体系好用的根本原因,但也是新手最容易困惑的地方——你看到的最终配置和你写的文件往往长得不一样。

加载完之后,脚本还会做几件事:把args.work_dir覆盖进去、把--cfg-options合并进去、处理--resume相关的路径。这里有个细节值得注意:work_dir 的确定时机。如果你既没在命令行传--work-dir,config 里也没写work_dir,脚本会直接报错退出。这是有意为之的,因为训练日志和权重必须有个明确的落盘位置,不能靠默认值蒙混过关。

提示:调试配置合并结果最直接的办法,是在train.py里cfg构建完成之后加一行print(cfg.pretty_text),把最终配置完整打印出来。比对着看,比猜哪里被覆盖了高效得多。

2.3 一个被忽视的环节:配置里的变量引用

MMDetection 的 config 支持在文件内部定义变量并被引用,比如:

num_classes = 80 model = dict(bbox_head=dict(num_classes=num_classes))

这个能力来自Config类对 Python 执行环境的封装。它本质上是用exec执行配置文件,所以配置文件里能写 Python 表达式。这带来灵活性的同时也有代价:配置文件不是纯数据,它会被执行。所以不要在里面写有副作用的代码,也不要指望它能被静态解析工具完美处理。理解这一点,你就能明白为什么有些 config 里能写if判断、能引用外部变量,而有些工具却读不懂它。

3. 环境、随机性与分布式:训练开始前的隐形准备

3.1 随机种子与确定性:复现性的地基

在模型构建之前,train.py会调用init_random_seed()之类的逻辑来设置随机种子。这一步的重要性怎么强调都不过分。目标检测训练里涉及大量随机操作:数据增强的随机裁剪、随机翻转、anchor 的随机采样、dataloader 的 shuffle。任何一个环节的随机源没固定住,两次训练的结果就对不上。

MMDetection 的做法是:如果命令行传了--seed,就用它;否则从系统时间或者某个默认值生成一个种子,然后把这个种子同步到random、numpy、torch三个库,并且在分布式场景下保证所有进程用同一个种子。这里有个容易忽略的点:dataloader 的 worker 进程也需要设置种子,否则每个 worker 的增强行为会不一致。框架通过 worker_init_fn 处理了这件事,但如果你自己写了自定义 dataset 并且用了额外的随机源,就得自己保证它也被固定。

--deterministic则是更进一步,它会设置 cuDNN 为确定性模式,并关闭一些非确定性的算子。代价是速度可能下降,而且某些操作在确定性模式下会直接报错。我的建议是:日常训练不开,需要严格复现实验结论时再开。而且开了之后要接受“同样的代码在不同硬件上仍可能有微小差异”这个现实,确定性不等于跨平台完全一致。

3.2 分布式初始化:单卡和多卡的分叉点

接下来是分布式环境的初始化。MMDetection 3.x 把这部分交给了 MMEngine 的init_dist,但逻辑本身没变:根据--launcher参数决定用哪种后端。

  • none:单进程,不初始化分布式;
  • pytorch:使用 PyTorch 原生的分布式启动;
  • slurm:在集群调度环境下使用;
  • mpi:使用 MPI 后端。

初始化完成之后,会拿到当前进程的 rank、world_size、local_rank 等信息。这些信息后续会决定:每个进程读数据的哪一部分、日志在哪个 rank 上打印、checkpoint 在哪个 rank 上保存。

这里有个非常经典的坑:日志重复打印。如果你在自定义代码里直接print,多卡时每个进程都会打印一遍,日志会乱成一团。正确做法是用 MMEngine 提供的MMLogger,它内部会根据 rank 判断是否输出。同理,任何写文件的操作都要考虑是不是只有主进程该做。

还有一个隐蔽的问题:分布式采样器的行为。多卡训练时,dataloader 会用DistributedSampler,它会把数据集切分到各个进程。如果你自己改了 dataset 的长度或者顺序,要确保 sampler 的逻辑还成立,否则会出现某些样本被重复采样、某些样本永远采不到的情况。

3.3 一个实操建议:先用单卡小数据跑通

我见过太多人一上来就多卡 + 全量数据,结果报错之后根本分不清是配置问题、分布式问题还是数据问题。稳妥的做法是:先用单卡、把数据集截取一小部分、把训练轮数调到 1,完整跑一遍。确认整个链路通了,再逐步放大。train.py的很多报错信息在单卡下更清晰,多卡下会被进程间的日志淹没。

4. 模型、数据集与 runner:训练主体的组装逻辑

4.1 模型构建:从 config 字典到 nn.Module

配置准备好之后,脚本会调用类似build_detector(cfg.model)的逻辑来构建模型。这一步背后是注册器机制:config 里的type字段对应注册器里注册的类名,构建函数根据type找到对应的类,再把剩余字段作为参数传进去。

这个机制的好处是解耦:配置文件不需要 import 任何类,只需要写类名字符串。代价是类名写错时的报错不够直观,通常是一句“xxx is not in the registry”,你得自己去确认是拼写问题还是没 import 对应的模块。

模型构建完之后,还会做几件事:加载预训练权重(如果 config 里指定了load_from或pretrained)、把模型搬到 GPU、如果是分布式则用MMDistributedDataParallel包装。这里要注意load_from和resume_from的区别:前者是加载预训练权重但重新开始训练(epoch 从 0 算),后者是恢复训练(epoch 接着上次算)。这两个概念混用是新手常犯的错误。

4.2 数据集构建与 dataloader 的细节

数据集部分由build_dataset(cfg.data.train)完成。MMDetection 的数据集构建有个特点:它支持“数据集包装”的嵌套结构,比如RepeatDataset、ClassBalancedDataset、ConcatDataset。这些包装器会改变数据集的长度和采样行为,理解它们的嵌套顺序很重要。

dataloader 的构建则涉及几个关键参数:samples_per_gpu(每张卡的 batch size)、workers_per_gpu(每张卡的数据加载进程数)、persistent_workers等。workers_per_gpu的设置是个经验活:设太小数据加载跟不上 GPU 计算,设太大内存和 CPU 开销上去了。一般来说,每个 GPU 配 2 到 4 个 worker 是常见起点,具体要看你的数据增强复杂度和磁盘 IO 速度。

注意:samples_per_gpu是每张卡的 batch size,不是全局 batch size。全局 batch size 等于samples_per_gpu × num_gpus。学习率的设置通常要跟着全局 batch size 走,改了卡数不改学习率,收敛行为会变。

4.3 runner 的组装:训练循环的真正驱动者

这是train.py里最核心也最容易被略过的一段。在 MMEngine 体系下,脚本会构建一个Runner对象,把模型、数据集、优化器、学习率调度器、日志配置、checkpoint 配置全部塞进去,然后调用runner.train()。

Runner 内部封装了训练循环:什么时候做前向、什么时候反向、什么时候更新学习率、什么时候验证、什么时候保存 checkpoint。config 里的train_cfg、val_cfg、test_cfg以及default_hooks都是在配置这个 runner。

以 checkpoint 保存为例,CheckpointHook控制保存频率:

default_hooks = dict( checkpoint=dict( type='CheckpointHook', interval=1, max_keep_ckpts=3, save_best='coco/bbox_mAP' ) )

interval=1表示每个 epoch 存一次,max_keep_ckpts=3表示只保留最近 3 个,save_best表示额外保存指标最好的那个。这些字段在旧版 2.x 里是写在checkpoint_config里的,位置变了但语义基本一致。

日志方面,LoggerHook控制打印频率,interval=50表示每 50 个 iteration 打印一次。如果你觉得日志刷得太快或太慢,改的就是这里,而不是去改train.py本身。

理解 runner 的意义在于:大部分训练行为的定制,应该通过配置 hook 来实现,而不是改 train.py。train.py 负责组装,hook 负责具体行为。这个分工想清楚了,你就能判断一个问题该在哪里解决。

5. 那些只有踩过才知道的坑与定制技巧

5.1 恢复训练时学习率为什么“跳”了

这是被问得最多的问题之一。你从第 10 个 epoch 恢复训练,发现学习率不是接着第 10 个 epoch 的值,而是从头开始或者跳到了别的地方。原因通常有两个:一是--resume没有正确加载优化器状态,二是学习率调度器的配置和恢复逻辑不匹配。

正确的恢复训练,需要同时恢复模型权重、优化器状态、学习率调度器状态、以及当前的 epoch 和 iteration。MMEngine 的 resume 机制会把这些都存进 checkpoint,恢复时一起读回来。如果你用的是自定义的学习率策略,要确保它实现了state_dict和load_state_dict,否则恢复时状态丢失,学习率就会从头算。

另一个相关坑:warmup 在恢复时会被重新触发。如果你的配置里有 warmup,恢复训练时要注意 warmup 的 iteration 计数是不是被重置了。这个在跨版本时行为有差异,建议恢复后先观察几十个 iteration 的日志确认学习率曲线正常。

5.2 自定义 hook 的插入位置

想加一个自定义行为,比如每个 epoch 结束后把某些中间特征可视化出来,最规范的做法是写一个自定义 hook,然后注册到 runner 里。hook 的执行时机由priority和注册的 stage 决定,比如before_train_epoch、after_train_iter。

写 hook 的时候有个原则:不要在 hook 里做阻塞时间过长的操作,因为它会卡住整个训练循环。如果确实要做重活,考虑异步或者放到单独的进程里。另外,hook 里访问 runner 的状态要小心,比如runner.iter、runner.epoch这些值在分布式下每个进程都有一份,但只有主进程该做写操作。

5.3 从 2.x 迁移到 3.x 时 train.py 的变化

如果你手上有旧项目,迁移时train.py层面主要注意这几点:

2.x 写法3.x 写法
cfg.optimizer_config合并进optim_wrapper
checkpoint_configdefault_hooks.checkpoint
log_configdefault_hooks.logger
lr_configparam_scheduler
runner = dict(type='EpochBasedRunner')train_cfg = dict(type='EpochBasedTrainLoop')

结构变了,但概念是一一对应的。迁移时不要试图逐行翻译,而是理解每个配置块在管什么,然后用新体系的写法重新表达。这样迁移出来的配置更干净,也更容易维护。

5.4 一个实用技巧:把 train.py 当模板复制出来改

最后分享一个我常用的做法。当你需要做比较深的定制时,不要直接改框架自带的tools/train.py,而是把它复制到你的项目目录下,改成一个自己的入口脚本。这样做的好处是:框架升级时不会覆盖你的改动,而且你的定制逻辑集中在一个文件里,别人接手时一眼能看懂。

复制出来之后,你可以放心地加日志、加断点、加自定义的构建逻辑。等定制稳定了,再考虑把通用部分抽成 hook 或工具函数。这个“先复制再抽象”的路径,比一上来就追求优雅架构要务实得多。

我在实际项目里就是这么干的:一开始只是想在训练开始时打印一下数据集的类别分布,后来慢慢加上了自定义的模型初始化、特殊的权重加载逻辑、以及训练中途的指标快照。如果一开始就去改框架源码,这些改动早就随着版本升级丢光了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 15:19:37

微波通信入门:频段认知、链路预算与工程实践

通信观系列写到第三十五篇,今天聊的是微波通信。这个名词做通信相关工作的人都不陌生,但真要回答"微波到底是哪一段频率""为什么传输链路要留几十dB余量""现场装锅的时候哪些坑躲不开",很多刚入门的同学还是会…

作者头像 李华
网站建设 2026/10/17 15:18:58

OpenNLP文档分类器实战:最大熵模型训练、调优与场景适配指南

1. 文档分类这件事,为什么值得单独拿出来聊做NLP应用的人,绕不开一个基础问题:手里有一堆文本,怎么让机器自动把它们归到该去的类别里。新闻按频道分、工单按类型分、邮件按优先级分、用户反馈按情绪分,这些场景本质上…

作者头像 李华
网站建设 2026/10/17 15:18:42

Docker安装部署全攻略:从环境检查到镜像加速与踩坑排查

标题已经写得很直白,这一章就是动手装东西。不管你是刚看完前面几章准备自己折腾一套开发环境,还是公司里要在一台新机器上把运行环境铺起来,Docker安装部署都是绕不开的第一步。这篇文章我会把整个安装过程掰开揉碎,从最开始的版…

作者头像 李华
网站建设 2026/10/17 15:18:36

动手学生成式AI:从自编码器到扩散模型的四阶实践

1. 项目概述:这不是“AI画画”,而是一次对生成式模型底层逻辑的亲手拆解“动手学深度学习——8. 人物肖像画”这个标题,乍看是教你怎么用现成工具生成一张好看的人脸图,但如果你真这么理解,就完全错过了它最硬核的价值…

作者头像 李华
网站建设 2026/10/17 15:17:09

卡尔曼滤波在单目标跟踪中如何应对遮挡与重叠

简介:这份资源面向计算机视觉与机器学习方向的学习者和开发者,聚焦遮挡与重叠场景下的单目标跟踪难题。项目以卡尔曼滤波为核心,通过预测与观测更新相结合的方式,弥补最大IOU匹配在目标短暂消失时容易丢失的缺陷,并平滑…

作者头像 李华
网站建设 2026/10/17 15:16:44

LayaAir 2-Cmd与VSCode环境协同配置指南

1. 为什么“LayaAir 2-Cmd VSCode”组合容易让人反复重装环境LayaAir 2.x 是国内某跨平台游戏引擎团队在2019年前后主推的轻量级H5/小程序/小游戏开发框架,其核心设计目标是让前端开发者能用熟悉的TypeScript和Web开发思维快速产出可多端发布的2D内容。而LayaAir 2…

作者头像 李华