1. 训练中断恢复:为什么这比你想的更重要
搞目标检测模型训练,尤其是用MMDetection这种框架,最让人头疼的莫过于训练到一半突然中断。我遇到过各种情况:服务器机房断电、GPU内存爆掉、甚至实验室保洁阿姨不小心踢掉了电源线。每次看到训练了十几个小时的进度突然归零,那种心情真是五味杂陈。
其实训练中断恢复不仅仅是“接着训练”那么简单,它涉及到几个关键问题:模型权重怎么加载?优化器状态要不要恢复?学习率调度器从哪开始?如果这些没处理好,你可能会发现恢复后的训练效果还不如从头开始。更糟糕的是,有些中断恢复不当会导致模型发散,前面几十个epoch的努力全白费了。
在MMDetection里,中断恢复的核心机制就是--resume参数。但很多人对这个参数的理解只停留在表面,以为就是简单指定一个checkpoint文件。实际上,MMDetection从2.x版本升级到3.x后,整个训练恢复机制有了很大变化,特别是引入了MMEngine作为底层引擎,很多细节需要重新理解。
我刚开始用MMDetection 3.0的时候,就踩过一个坑:以为恢复训练只需要改配置文件里的max_epochs,结果发现模型虽然接着训练了,但验证集上的指标一直在原地踏步。后来才发现,是因为学习率调度器没有正确恢复,导致学习率始终停留在很低的水平,模型参数几乎不更新。
2. 理解MMDetection的checkpoint机制
要正确恢复训练,首先得明白MMDetection是怎么保存训练状态的。在MMDetection 3.x里,一个完整的checkpoint文件(通常是.pth格式)包含的内容远不止模型权重。
当你用默认配置训练时,MMDetection会在每个epoch结束后保存一个checkpoint。我们来看看这个checkpoint里到底有什么:
import torch
# 加载一个保存的checkpoint看看结构
checkpoint = torch.load('work_dirs/faster_rcnn/epoch_12.pth', map_location='cpu')
print("Checkpoint包含的键:")
for key in checkpoint.keys():
print(f"- {key}")
# 输出通常包括:
# - meta: 元信息,包括训练配置、时间戳等
# - state_dict: 模型权重
# - optimizer: 优化器状态
# - param_schedulers: 学习率调度器状态
# - epoch: 当前的epoch数
# - iter: 当前的迭代次数
这里有个关键点:resume和load_from是完全不同的两个概念。很多人容易混淆,结果用错了参数,训练效果大打折扣。
load_from 只加载模型权重,其他所有状态(优化器、学习率调度器、当前epoch)都从头开始。这相当于用预训练权重初始化模型,然后从零开始训练。适合迁移学习或者fine-tuning的场景。
resume 则是完整恢复训练状态,包括模型权重、优化器状态、学习率调度器状态、当前的epoch和iter计数。这才是真正意义上的“接着上次训练”。
我整理了一个对比表格,让你一目了然:
| 特性 | load_from | resume |
|---|---|---|
| 模型权重 | 加载 | 加载 |
| 优化器状态 | 不加载 | 加载 |
| 学习率调度器 | 重新初始化 | 恢复状态 |
| 当前epoch | 从0开始 | 从保存的epoch继续 |
| 训练历史 | 不保留 | 保留 |
| 适用场景 | 迁移学习、fine-tuning | 训练意外中断恢复 |
在实际操作中,如果你用错了,很容易看出来。比如用load_from恢复中断的训练,你会发现训练日志里的loss曲线在恢复点有个明显的跳跃,因为优化器动量等状态都重置了。
3. 从指定epoch恢复训练的完整操作流程
现在我们来一步步操作,假设你的Faster R-CNN训练在epoch 12时中断了,你想从epoch 12继续训练。
3.1 第一步:找到并修改配置文件
训练开始后,MMDetection会在work_dirs目录下生成一个配置文件副本。这个副本很重要,因为它是实际训练时使用的配置,包含了所有运行时信息。
# 假设你的原始配置文件在configs/faster_rcnn/
# 训练生成的配置文件在work_dirs/faster-rcnn_r50_fpn_1x_coco/
# 首先进入工作目录
cd work_dirs/faster-rcnn_r50_fpn_1x_coco/
# 查看生成的配置文件
ls -la *.py
# 你会看到一个类似 faster-rcnn_r50_fpn_1x_coco.py 的文件
用你喜欢的编辑器打开这个文件,我习惯用VSCode,但vim或者nano都可以:
vim faster-rcnn_r50_fpn_1x_coco.py
在这个文件里,你需要修改几个关键参数。用Ctrl+F搜索功能找到这些位置:
-
修改
max_epochs:如果你原本计划训练24个epoch,现在想增加到36个,就把max_epochs从24改成36。 -
调整
milestones:这是学习率下降的节点。如果原本的配置是milestones=[8, 11],表示在第8和第11个epoch时学习率会下降。现在总epoch数从24增加到36,你需要等比例调整这些里程碑点。计算很简单:新的里程碑 = 原里程碑 × (新总epoch数 / 原总epoch数)
比如:
8 × (36/24) = 12,11 × (36/24) = 16.5,取整后就是[12, 17]。在配置文件里找到
param_scheduler部分,修改对应的值:
param_scheduler = [
# ... 可能还有其他调度器
dict(
type='MultiStepLR',
by_epoch=True,
milestones=[12, 17], # 修改这里
gamma=0.1)
]
- 检查
train_cfg:确保max_epochs已经更新:
train_cfg = dict(
type='EpochBasedTrainLoop',
max_epochs=36, # 修改这里
val_interval=1)
3.2 第二步:使用resume参数恢复训练
修改完配置文件后,就可以开始恢复训练了。这里有两种常见情况,你需要根据实际情况选择:
情况A:从最新的checkpoint恢复
如果你不确定具体要恢复哪个epoch,或者想从最近的一次保存点继续,可以这样操作:
python tools/train.py work_dirs/faster-rcnn_r50_fpn_1x_coco/faster-rcnn_r50_fpn_1x_coco.py --resume
不加具体路径的--resume参数会让MMDetection自动查找work_dirs目录下最新的checkpoint文件。系统会按照文件名排序,选择epoch编号最大的那个文件。
情况B:从指定epoch恢复
有时候你可能想从更早的某个epoch恢复,比如你发现epoch 12之后模型开始过拟合,想回到epoch 10重新调整训练策略:
python tools/train.py work_dirs/faster-rcnn_r50_fpn_1x_coco/faster-rcnn_r50_fpn_1x_coco.py --resume work_dirs/faster-rcnn_r50_fpn_1x_coco/epoch_10.pth
这里有个实用技巧:恢复训练前,先检查一下checkpoint文件是否完整。有时候训练中断可能导致最后一个checkpoint损坏。
import torch
def check_checkpoint(filepath):
try:
checkpoint = torch.load(filepath, map_location='cpu')
required_keys = ['meta', 'state_dict', 'optimizer', 'epoch']
for key in required_keys:
if key not in checkpoint:
print(f"警告:checkpoint缺少必要的键 {key}")
return False
print(f"checkpoint检查通过,保存于epoch {checkpoint.get('epoch', '未知')}")
return True
except Exception as e:
print(f"加载checkpoint失败:{e}")
return False
# 使用示例
check_checkpoint('work_dirs/faster-rcnn_r50_fpn_1x_coco/epoch_12.pth')
3.3 第三步:验证恢复是否成功
开始恢复训练后,你需要确认一切是否正常。观察训练开始的日志输出:
2024-01-15 10:30:15,789 - mmengine - INFO - Load checkpoint from work_dirs/faster-rcnn_r50_fpn_1x_coco/epoch_12.pth
2024-01-15 10:30:16,123 - mmengine - INFO - resumed epoch 12, iter 5000
2024-01-15 10:30:16,124 - mmengine - INFO - Start running from epoch 13, iter 5001
关键信息是resumed epoch 12和Start running from epoch 13,这说明系统正确识别了要从epoch 13开始训练(因为epoch 12已经完成)。
如果看到的是Start running from epoch 1,那说明恢复没成功,可能是checkpoint文件有问题,或者--resume参数没生效。
4. 配置文件深度解析:不只是改几个数字
很多人在修改配置文件时只关注max_epochs和milestones,其实配置文件里还有很多细节会影响恢复训练的效果。
4.1 理解配置文件的继承结构
MMDetection的配置文件采用继承机制,这既方便了管理,也增加了恢复训练的复杂性。比如典型的Faster R-CNN配置:
# 在 faster-rcnn_r50_fpn_1x_coco.py 中
_base_ = [
'../_base_/models/faster-rcnn_r50_fpn.py',
'../_base_/datasets/coco_detection.py',
'../_base_/schedules/schedule_1x.py',
'../_base_/default_runtime.py'
]
当你训练模型时,MMDetection会把所有这些基础配置合并,然后在work_dirs下生成一个完整的配置文件。恢复训练时,你修改的是这个合并后的文件,而不是原始的基础配置文件。
这里有个坑:如果你在恢复训练后想再次修改配置,应该修改work_dirs下的文件,而不是原始配置文件。因为训练过程中可能会通过--cfg-options参数覆盖某些配置,这些覆盖只体现在工作目录的配置文件中。
4.2 关键参数详解
在恢复训练的配置文件中,有几个参数需要特别关注:
train_cfg.val_interval:验证间隔。如果你之前是每1个epoch验证一次,但训练到后期模型变化缓慢,可以考虑增大这个值,比如改为每2个或每3个epoch验证一次,以加快训练速度。
default_hooks.checkpoint.interval:checkpoint保存间隔。默认是每个epoch保存一次。如果你的模型很大,保存频繁会影响训练速度。可以考虑改为每2-3个epoch保存一次,但这样会减少恢复训练的粒度。
param_scheduler:学习率调度器配置。除了MultiStepLR,MMDetection还支持CosineAnnealingLR、LinearLR、PolyLR等多种调度器。恢复训练时,要确保调度器的begin和end参数与新的总epoch数匹配。
举个例子,如果你使用余弦退火调度器:
param_scheduler = [
dict(
type='CosineAnnealingLR',
T_max=36, # 总epoch数,需要修改
eta_min=1e-7,
begin=0,
end=36, # 需要修改
by_epoch=True)
]
optim_wrapper:优化器包装器配置。恢复训练时,优化器的状态会被完整恢复,包括动量、二阶矩估计等。但如果你发现恢复后训练不稳定,可以考虑重置优化器状态,这需要一些技巧操作。
4.3 使用cfg-options动态修改配置
有时候你可能不想直接修改配置文件,而是想通过命令行参数临时调整某些配置。这时可以用--cfg-options参数:
python tools/train.py work_dirs/faster-rcnn_r50_fpn_1x_coco/faster-rcnn_r50_fpn_1x_coco.py \
--resume work_dirs/faster-rcnn_r50_fpn_1x_coco/epoch_12.pth \
--cfg-options train_cfg.max_epochs=36 \
param_scheduler.0.milestones="[12,17]" \
default_hooks.checkpoint.interval=2
这种方式的优点是灵活,不会永久修改配置文件。但要注意,通过--cfg-options修改的配置只在本次训练中生效,不会保存到配置文件里。
5. 学习率调整策略:恢复训练的核心技巧
恢复训练时,学习率的处理是最容易出问题的地方。很多人恢复训练后效果变差,根本原因就是学习率没设置好。
5.1 学习率预热(Warmup)的恢复
如果你的训练配置中包含学习率预热(这是现代深度学习训练的常见技巧),恢复训练时需要特别注意。
假设你的原始配置有500次迭代的线性warmup:
param_scheduler = [
dict(
type='LinearLR',
start_factor=0.001,
by_epoch=False,
begin=0,
end=500), # 前500次迭代warmup
dict(
type='MultiStepLR',
by_epoch=True,
milestones=[8, 11],
gamma=0.1)
]
当你从epoch 12恢复训练时,warmup阶段应该跳过吗?这取决于你的判断。
如果你觉得模型已经训练得比较充分,warmup可能不再需要,可以修改配置去掉warmup阶段:
param_scheduler = [
# 注释掉或删除LinearLR部分
dict(
type='MultiStepLR',
by_epoch=True,
milestones=[12, 17], # 调整后的里程碑
gamma=0.1)
]
如果你认为还需要warmup,但要确保从适当的学习率开始,可以调整start_factor:
param_scheduler = [
dict(
type='LinearLR',
start_factor=0.1, # 从10%的基础学习率开始,而不是0.1%
by_epoch=False,
begin=0,
end=100), # 缩短warmup周期
dict(
type='MultiStepLR',
by_epoch=True,
milestones=[12, 17],
gamma=0.1)
]
5.2 自适应学习率调整
有时候,恢复训练后你可能想改变学习率策略。比如,原始训练使用固定学习率,但恢复后你想改用余弦退火:
param_scheduler = [
dict(
type='CosineAnnealingLR',
T_max=36, # 总epoch数
eta_min=1e-6, # 最小学习率
begin=12, # 从当前epoch开始
end=36,
by_epoch=True)
]
这里的关键是begin=12,表示余弦退火从第12个epoch开始计算。这样前12个epoch的学习率历史被忽略,从第12个epoch开始应用新的调度策略。
5.3 梯度累积与学习率缩放
如果你的恢复训练使用了不同的batch size(比如原来用4张GPU,每GPU batch size=2,现在用2张GPU,每GPU batch size=4),那么学习率可能需要重新调整。
根据线性缩放规则(Linear Scaling Rule),学习率应该与batch size成比例。但这里有个细节:恢复训练时,优化器状态中保存的梯度统计信息是基于原始batch size的,直接缩放学习率可能不准确。
比较稳妥的做法是:如果batch size变化不大(比如从8到16),可以按比例缩放学习率;如果变化很大,建议重置优化器状态,从头开始积累梯度统计信息。
6. 常见问题排查与解决方案
在实际操作中,恢复训练可能会遇到各种问题。我总结了一些常见的情况和解决方法。
6.1 问题一:恢复训练后loss突然飙升
这是最让人头疼的问题。你可能看到这样的日志:
Epoch 13, iter 100: loss=0.85
Epoch 13, iter 200: loss=15.32 # 突然飙升!
Epoch 13, iter 300: loss=nan # 直接爆炸
可能原因及解决方案:
-
学习率过高:恢复训练时,如果学习率调度器状态恢复错误,可能导致学习率突然变大。
检查方法:查看训练日志中的
lr字段,确认学习率是否合理。解决方案:手动调整学习率,在恢复训练命令中添加
--cfg-options optim_wrapper.optimizer.lr=0.002,将学习率设为较小的值。 -
优化器状态不匹配:如果恢复的checkpoint中的优化器状态与当前模型结构不匹配(比如你修改了模型结构),会导致梯度计算错误。
检查方法:尝试不使用优化器状态恢复,只加载模型权重:
# 先只加载权重,不恢复优化器状态 python tools/train.py config.py --load-from epoch_12.pth # 如果这样正常,说明是优化器状态问题 -
梯度爆炸:可以尝试添加梯度裁剪:
optim_wrapper = dict( type='OptimWrapper', optimizer=dict(type='SGD', lr=0.02, momentum=0.9, weight_decay=0.0001), clip_grad=dict(max_norm=35, norm_type=2)) # 添加梯度裁剪
6.2 问题二:恢复训练后性能不再提升
模型训练一段时间后,验证集指标不再提升,甚至下降。恢复训练后,这个问题依然存在。
可能原因及解决方案:
-
模型已收敛:可能模型已经训练到最优,继续训练只会过拟合。
检查方法:观察训练集loss是否还在下降,验证集loss是否开始上升。
解决方案:提前停止训练,或者尝试更强的正则化(增加weight decay、添加dropout等)。
-
学习率策略问题:学习率可能已经降得太低,模型参数更新缓慢。
解决方案:重置学习率到较高的值,或者改用周期性学习率(CyclicLR):
param_scheduler = [ dict( type='CyclicLR', target_ratio=(10, 1e-4), # 学习率在最大值和最小值之间循环 cyclic_times=1, step_ratio_up=0.4, by_epoch=True, begin=12, end=36) ] -
数据问题:检查训练数据是否有变化,或者数据增强是否过于强烈。
6.3 问题三:多GPU训练恢复失败
在多GPU环境下恢复训练,可能会遇到各种同步问题。
常见错误及解决方案:
-
GPU数量变化:原来用4张GPU训练,现在用8张GPU恢复。
解决方案:MMDetection通常能处理这种情况,但要注意batch size的变化。如果GPU数量翻倍,你可能需要相应调整学习率,或者使用
--cfg-options auto_scale_lr.enable=True启用自动学习率缩放。 -
checkpoint保存格式问题:有些情况下,多GPU训练的checkpoint在单GPU环境下无法加载。
解决方案:使用MMEngine提供的转换工具:
from mmengine.runner import save_checkpoint, load_checkpoint # 加载多GPUcheckpoint checkpoint = load_checkpoint('multi_gpu_checkpoint.pth') # 保存为单GPU格式 save_checkpoint(checkpoint, 'single_gpu_checkpoint.pth') -
分布式训练端口冲突:如果同一台机器上同时运行多个恢复训练任务,可能会端口冲突。
解决方案:指定不同的端口号:
# 第一个任务 CUDA_VISIBLE_DEVICES=0,1,2,3 PORT=29500 ./tools/dist_train.sh config.py 4 --resume checkpoint1.pth # 第二个任务 CUDA_VISIBLE_DEVICES=4,5,6,7 PORT=29501 ./tools/dist_train.sh config.py 4 --resume checkpoint2.pth
6.4 问题四:迭代次数不连续
有时候你会发现,恢复训练后迭代次数不是从上次结束的地方开始,而是有跳跃。
可能原因:
-
checkpoint保存间隔:如果之前设置的是每2个epoch保存一次checkpoint,那么你恢复时可能丢失了中间迭代的信息。
解决方案:这是正常现象,不影响训练效果。但如果想保持迭代连续性,可以设置
default_hooks.checkpoint.by_epoch=False和default_hooks.checkpoint.interval=1000,按迭代次数保存。 -
数据加载器随机种子:恢复训练时,数据加载器的随机状态可能不同,导致数据顺序变化。
解决方案:在配置中设置固定的随机种子:
randomness = dict(seed=42, deterministic=True)
7. 高级技巧与最佳实践
经过多次实战,我总结了一些恢复训练的高级技巧,能帮你避免很多坑。
7.1 创建恢复训练检查点
在开始长时间训练前,我习惯先创建一个“恢复训练检查点”,特别是当我要修改配置或者尝试新的训练策略时。
具体做法:
- 先训练少量epoch(比如5个),确保一切正常
- 保存这个中间状态
- 基于这个检查点尝试不同的恢复训练策略
- 选择效果最好的策略进行完整训练
这样即使恢复训练失败,损失也不大,最多就是5个epoch的时间。
7.2 使用实验管理工具
如果你经常需要中断和恢复训练,建议使用实验管理工具,比如MMEngine自带的日志系统,或者第三方工具如Weights & Biases、TensorBoard等。
这些工具能帮你:
- 自动记录每次训练的超参数
- 可视化比较不同恢复策略的效果
- 管理多个checkpoint文件
7.3 自动化恢复脚本
对于生产环境,可以编写自动化恢复脚本,定期检查训练状态,遇到中断自动恢复。
#!/usr/bin/env python3
import os
import subprocess
import time
from pathlib import Path
def find_latest_checkpoint(work_dir):
"""查找最新的checkpoint文件"""
checkpoint_dir = Path(work_dir)
if not checkpoint_dir.exists():
return None
pth_files = list(checkpoint_dir.glob('*.pth'))
if not pth_files:
return None
# 按修改时间排序,取最新的
latest_file = max(pth_files, key=lambda x: x.stat().st_mtime)
return str(latest_file)
def auto_resume_training(config_path, work_dir, max_retries=3):
"""自动恢复训练"""
for attempt in range(max_retries):
try:
# 查找最新checkpoint
checkpoint = find_latest_checkpoint(work_dir)
if checkpoint:
cmd = f"python tools/train.py {config_path} --resume {checkpoint}"
print(f"尝试恢复训练,使用checkpoint: {checkpoint}")
else:
cmd = f"python tools/train.py {config_path}"
print("未找到checkpoint,从头开始训练")
# 执行训练命令
result = subprocess.run(cmd, shell=True, check=True)
if result.returncode == 0:
print("训练正常完成")
return True
except subprocess.CalledProcessError as e:
print(f"训练中断,错误码: {e.returncode}")
print(f"等待10秒后重试... (尝试 {attempt + 1}/{max_retries})")
time.sleep(10)
print(f"经过{max_retries}次重试后仍失败")
return False
if __name__ == "__main__":
config_path = "configs/faster_rcnn/faster-rcnn_r50_fpn_1x_coco.py"
work_dir = "work_dirs/faster-rcnn_r50_fpn_1x_coco"
auto_resume_training(config_path, work_dir)
7.4 混合精度训练恢复
如果你使用混合精度训练(AMP),恢复训练时需要额外注意。混合精度训练的状态(如梯度缩放器)也保存在checkpoint中。
确保你的恢复训练命令包含AMP相关参数:
python tools/train.py config.py \
--resume checkpoint.pth \
--amp # 启用混合精度训练
如果恢复训练后出现NaN loss,可能是梯度缩放器状态有问题,可以尝试重置:
# 在配置文件中
optim_wrapper = dict(
type='AmpOptimWrapper', # 使用AmpOptimWrapper
loss_scale='dynamic', # 动态损失缩放
dtype='float16')
7.5 跨版本恢复训练
有时候你可能需要在不同版本的MMDetection之间恢复训练,这需要格外小心。
版本兼容性检查清单:
- 模型结构兼容性:确保两个版本的模型定义相同
- 配置格式兼容性:MMDetection 2.x和3.x的配置格式有很大变化
- checkpoint格式:使用MMEngine提供的转换工具
如果必须跨版本恢复,建议的步骤:
- 在新版本中加载旧checkpoint
- 只加载模型权重(
--load-from),不加载优化器状态 - 用较小的学习率微调几个epoch,让模型适应新版本
- 再恢复正常训练
8. 实战案例:从训练中断到成功恢复
让我分享一个真实案例。去年我们在训练一个自定义的检测模型时,服务器在epoch 47时突然宕机。这个模型已经训练了3天,batch size很小,每个epoch要6个小时。
第一步:评估损失
首先检查最后一个完整的checkpoint(epoch 46)是否可用:
import torch
checkpoint = torch.load('epoch_46.pth', map_location='cpu')
print(f"Epoch: {checkpoint['epoch']}")
print(f"保存时间: {checkpoint['meta']['time']}")
第二步:分析训练状态
查看训练日志,发现epoch 46的mAP是0.412,还在稳步提升,没有过拟合迹象。学习率当前是0.002(原始学习率0.02的10%)。
第三步:制定恢复策略
原计划训练100个epoch,已经完成46个。我们决定:
- 增加到120个epoch,给模型更多时间收敛
- 调整学习率里程碑:原计划在[80, 110]下降,现在改为[96, 132]
- 增加验证间隔,从每1个epoch改为每2个epoch,节省时间
第四步:修改配置文件
# 在work_dirs下的配置文件中修改
train_cfg = dict(
type='EpochBasedTrainLoop',
max_epochs=120, # 从100增加到120
val_interval=2) # 验证间隔改为2
param_scheduler = [
dict(
type='MultiStepLR',
by_epoch=True,
milestones=[96, 132], # 等比例调整
gamma=0.1)
]
第五步:恢复训练
python tools/train.py work_dirs/my_model/config.py \
--resume work_dirs/my_model/epoch_46.pth \
--cfg-options \
train_cfg.max_epochs=120 \
train_cfg.val_interval=2 \
param_scheduler.1.milestones="[96,132]"
第六步:监控恢复效果
恢复训练后,我们密切监控前几个epoch:
- epoch 47的loss应该与epoch 46末尾的loss连续,没有跳跃
- 学习率应该是0.002,与中断前一致
- 验证集mAP应该平稳过渡
实际结果:恢复训练后,模型在epoch 60达到最佳mAP 0.438,比原计划提前了20个epoch。总共节省了大约5天的训练时间。
这个案例的关键经验是:恢复训练不只是技术操作,更需要根据模型的实际状态做出合理决策。有时候,训练中断反而给了我们重新评估训练策略的机会。
通过这个完整流程,你应该能够应对大多数MMDetection训练中断的情况。记住,恢复训练的核心是理解checkpoint的完整内容,以及MMDetection的训练状态管理机制。每次中断都是一次学习的机会,熟练掌握恢复技巧,能让你在深度学习实践中更加从容。
415



被折叠的 条评论
为什么被折叠?



