恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
早停策略与模型权重保存:深度学习训练的“刹车与记录仪”
首页
资讯中心
/
早停策略与模型权重保存:深度学习训练的“刹车与记录仪”
早停策略与模型权重保存:深度学习训练的“刹车与记录仪”
发布时间:2026/10/22 7:50:27
用户在你的输入中给出的“项目正文”部分只有标题、热词和一句占位文本没有真正的正文。下面基于标题和场景给出完整博文。注意如果你的实际内容是其他领域比如生活、职场请重新提供更完整的项目正文我再按该领域重写。当前按“深度学习训练实操”领域处理。Day 38 早停策略和模型权重的保存是我自建深度学习项目“某图像分类Demo”打卡进度里很关键的一天。在此之前我的训练流程一直有一个毛病模型训练到后半程验证损失明显回升明明过拟合已经开始了却还在按固定epoch傻跑浪费时间不说最后保存下来的还不一定是最优的那一版。早停策略和模型权重的保存就是专门解决这两个问题的组合拳。这篇博文我尽量把原理、参数设置、实际踩过的坑都说清楚适合正在用Keras或PyTorch做模型训练、又对“如何优雅地结束训练”和“如何安全留下最优模型”还没有完整概念的读者。1. 早停策略别让训练为过拟合买单1.1 为什么要早停过拟合与训练成本的双重压力很多初学者以为训练就是“跑够设定的epoch数”这个想法在数据量充足、任务简单时勉强能用但一旦模型参数上来了跑满固定epoch几乎一定会遇到验证集性能先升后降的经典曲线。过拟合的本质是模型开始记住训练集里的噪声和特例而不是泛化规律表现在指标上就是训练损失持续下降验证损失却开始反弹。如果不干预后面那些epoch不仅白跑还会把模型参数拖到更差的位置。早停策略监控一个指定的指标比如验证损失设置好patience之后一旦指标连续多个epoch没有改善就主动终止训练。它不改变梯度下降的更新方式不修改网络结构只是一个训练过程中的“熔断机制”。我最初听到这个概念时觉得很简单真做起来才发现如果选错监控指标或者patience设得太激进效果还不如固定epoch数跑完。从成本角度看训练一次模型可能几小时甚至几天。早停不仅是技术洁癖也是实打实的资源管理。如果你在云主机上按小时计费或者有多组实验排队等着跑每一个多余的epoch都是在烧时间和算力。1.2 监控指标怎么选val_loss与val_acc的取舍早停需要先回答一个问题你要盯哪个指标最常用的两个选项是验证损失val_loss和验证精度val_acc。在不同框架里Keras的EarlyStopping默认monitorval_lossPyTorch则是你自己在训练循环里写判断逻辑。我的经验是优先使用val_loss而不是val_acc。原因在于损失函数是模型优化的直接目标它的变化比精度更平滑而且不会因为准确率在某些阈值附近抖动而产生误判。比如二分类问题中验证精度可能连续几个epoch卡在同一个值但val_loss还在稳定下降这时候如果monitorval_acc且设置了很小的min_delta会误以为模型没有进步提前触发早停。但val_loss也有自己的问题比如某些任务里损失值减小的幅度越来越小看起来像走进了“平台期”实际再跑几个epoch可能就会出现明显下降。这时候就需要跟patience配合下面一节细说。还有一类情况要特别提醒如果你的数据类别极度不平衡准确率本身就是一个有误导性的指标模型可能一直在把多数类全猜中少数类完全没学会acc却表现得很高。这时候必须回到loss或者更细化的F1、AUC等指标上。早停的监控指标和你的最终业务目标保持一致这条原则怎么强调都不过分。1.3 patience和min_delta到底该怎么设patience是指在监控指标停止改善之后你决定继续容忍多少个epoch再停。min_delta则是定义一个“最小改善量”指标变化量达不到这个值就视作没有改善。两块参数共同决定了早停的敏感度。先说min_delta一般设置成非常小的数比如1e-4。如果设得太大会漏掉那些“走得慢但方向正确”的优化阶段。我在一个语义分割项目里把min_delta设成了0.001结果val_loss本来还在缓慢下降就被我早早掐断后来调回1e-4模型才正常收敛。patience的设置和数据集大小、模型复杂度、学习率都有关系。学习率较大时验证指标容易震荡patience设大一些比如15或20避免一两次正常的抖动就提前终止。学习率较小、训练曲线平滑时patience可以设成5到10。我自己的基准做法是先设10然后观察tensorboard里验证曲线的振荡幅度再决定要不要加码到20。还有一个实用技巧如果配合学习率衰减reduce LR on plateau使用可以把patience设置成学习率衰减调度器的patience的1.5到2倍这样早停不会抢在“学习率降低后重新下降”之前触发。提示patience的单位是epoch不是step。如果你的一个epoch非常长比如在几万张图片上训练那patience20意味着模型已经“没有改善”地跑了20轮完整数据时间成本很高这时候建议缩小patience或者改用step级别监控。2. 模型权重的保存不是所有“存模型”都叫checkpoint2.1 保存范围结构、权重、优化器状态一个都不能少很多教程里拿“model.save()”一句话带过好像保存模型就是把结果随便写个文件。真正到了要恢复训练、做迁移学习或者部署的时候你才发现保存的粒度太重要了。从保存内容上区分至少要理解两层第一层只保存模型权重也就是网络里所有的可训练参数。在PyTorch里对应state_dict在Keras里对应weights。这种保存方式体积最小适合做最后推理部署以及迁移学习场景中加载预训练权重。缺点是你必须自己记住模型结构代码里得能重新实例化出完全一样的模型来加载权重。第二层是完整快照包含模型结构、权重、优化器状态、当前epoch数、学习率调度器的状态等。这种checkpoint主要用于“断点续训”。你训练到第100个epoch时断电或者想换一台机器继续完整快照能让你精确从100个epoch接着跑而不是从100个epoch的权重重新开始用手动补的lr设置继续那样很容易出现学习率错乱。我强烈建议把保存内容的粒度分开对待日常训练的每个checkpoint都保存完整快照单独维护一个best_model路径只存权重或完整模型用于最终使用。2.2 三种保存策略的场景化对比我现在会把权重保存策略分成三类每N轮保存一次、只保存最优、连续保存最近K次。三者可以组合没有银弹。每N轮保存一次适合训练周期长、需要中途观察不同阶段效果的场合。比如N5一轮训练50个epoch就会生成10个快照。虽然占磁盘但可以回溯模型在哪个阶段开始过拟合这对调参复盘很有价值。只保存最优是最高频的需求早停策略配合save_best_onlyTrue每轮结束对比验证指标刷新最优就覆盖旧文件。优点是磁盘占用恒定为1份缺点是如果最优值出现在第30个epoch而训练到第37个epoch才触发早停你最终拿到的“最好”模型就是第30个epoch的那个中间7轮的更差结果不会覆盖它这也是我推荐save_best_only的原因。连续保存最近K次本质上是给“只存最优”加了一层保险。某些场景里验证指标最优并不代表最终业务效果最好比如存在后处理环节你可能会想回看最近几个epoch的模型做集成或者对比。K3时每次发现“当前比历史最好还好”就淘汰最老的那个最优副本始终保留最优秀的3个。这种策略在比赛和项目里很实用。三种策略不是三选一我常用组合是全量保存目录里存最近K个完整快照权重目录里另存一个best训练结束后对比哪一版在真实测试集上表现最好再做一次人工兜底选型。2.3 checkpoint命名与版本管理的工程习惯模型权重文件看起来就是个.pt或者.h5文件文件命名如果不规范一周后你自己都会忘掉这个文件是哪个模型、训练到什么阶段、验证效果多少。我现在的命名模板是{model_name}_{dataset_tag}_epoch{epoch}_val{metric:.4f}.ext但问题在于val指标经常会变化比如第三十个epoch的精度是0.8921写成val0.8921会让文件名特别长。后面我调整成了只记录关键信息的方式{date}_{exp_name}_e{epoch}_best.{ext}同时配合一个训练脚本同名的JSON记录文件里面存项目名、监控指标、patience、batch_size、lr、本轮最优指标、保存路径。这比在文件名塞所有信息要干净得多。千万不要把checkpoint存在项目代码目录里那种临时文件混进Git仓库的体验太糟糕了。我习惯单独建一个checkpoints或weights目录然后在.gitignore里把它忽略掉。3. 实操过程以Keras和PyTorch各跑一遍完整流程3.1 Keras回调写法与参数配置Keras或者说TensorFlow的Keras提供了内置的EarlyStopping和ModelCheckpoint回调用起来最直接很适合第一版基线模型。示例代码如下我假设这个项目是“某图像分类Demo”输入尺寸是Imagenet风格的三通道图先构建一个MobileNetV2为基础的分类模型from tensorflow.keras.models import Model from tensorflow.keras.layers import Dense, GlobalAveragePooling2D from tensorflow.keras.applications import MobileNetV2 from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint, ReduceLROnPlateau base_model MobileNetV2(weightsimagenet, include_topFalse, input_shape(224, 224, 3)) base_model.trainable False x base_model.output x GlobalAveragePooling2D()(x) x Dense(128, activationrelu)(x) predictions Dense(5, activationsoftmax)(x) model Model(inputsbase_model.input, outputspredictions) model.compile(optimizeradam, losscategorical_crossentropy, metrics[accuracy]) early_stop EarlyStopping( monitorval_loss, patience10, min_delta1e-4, restore_best_weightsTrue, verbose1 ) checkpoint ModelCheckpoint( filepathcheckpoints/mobilev2_best.h5, monitorval_loss, save_best_onlyTrue, save_weights_onlyFalse, modeauto, verbose1 ) lr_scheduler ReduceLROnPlateau( monitorval_loss, factor0.5, patience5, min_lr1e-6 ) history model.fit( train_generator, validation_dataval_generator, epochs100, callbacks[early_stop, checkpoint, lr_scheduler] )这里的EarlyStopping我重点说两个容易被忽略的细节。restore_best_weightsTrue非常关键很多教程没写这行最后训练结束拿到的model还是最后一轮的权重早停只是停了但没有把模型恢复到历史最优状态等于早停白做了一半。另一个是verbose1早停触发时控制台会打印一行提示别小看这个输出多实验并发跑的时候它告诉你每个实验到底是因为patience耗尽停下还是正常跑满epoch。ModelCheckpoint里save_weights_onlyFalse会把整个模型存成.h5文件里面包含网络结构和权重加载时不需要再手动搭建模型对新手最友好。如果只关心权重想减小文件体积改成True并且保存路径写成.weights.h5配合requires的模型结构定义代码来加载更干净。3.2 PyTorch手写早停与保存逻辑PyTorch没有自带回调机制初学时会觉得不方便但写一遍之后你会对训练流程的理解更深刻。本质上早停就是在验证结束后做一次指标比较用一个循环配合记录最优值的变量即可。我通常维护一个简单的类代码量不大还比Keras的回调更透明import torch import copy class EarlyStopping: def __init__(self, patience10, min_delta1e-4, pathbest_model.pth): self.patience patience self.min_delta min_delta self.path path self.counter 0 self.best_score None self.early_stop False def __call__(self, val_loss, model, optimizer, epoch): score -val_loss if self.best_score is None: self.best_score score self.save_checkpoint(model, optimizer, epoch) elif score self.best_score self.min_delta: self.counter 1 if self.counter self.patience: self.early_stop True else: self.best_score score self.counter 0 self.save_checkpoint(model, optimizer, epoch) def save_checkpoint(self, model, optimizer, epoch): checkpoint { model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), epoch: epoch, best_loss: -self.best_score, } torch.save(checkpoint, self.path)注意我在这里保存的是完整优化器状态。你可能会问只是最优模型保存为什么要把optimizer也存进去因为很多时候我们发现最好的模型在第33个epoch后面继续训练到第40个epoch时早停触发此时最优模型状态伴随着的是第33个epoch的优化器状态。如果你只存model_state_dict之后想从这个best继续以不同学习率微调就得凭空手动构造一个优化器优化器内部的动量、二阶动量缓存全都没了当然后续也能训练但效果几乎一定不如接着原始优化器状态恢复来得好。这是只存权重容易踩的暗坑。在训练循环里验证结束后的调用也很简单early_stopping EarlyStopping(patience10, min_delta1e-4, pathcheckpoints/best.pth) for epoch in range(100): train_one_epoch(model, train_loader, optimizer, criterion) val_loss, val_acc validate(model, val_loader, criterion) print(fEpoch {epoch1}, val_loss: {val_loss:.4f}, val_acc: {val_acc:.4f}) early_stopping(val_loss, model, optimizer, epoch) if early_stopping.early_stop: print(Early stopping triggered) break3.3 恢复训练与验证恢复效果保存了checkpoint就得把恢复训练的流程跑通否则断点续训就是空话。PyTorch恢复训练的标准动作是checkpoint torch.load(checkpoints/best.pth, map_locationcuda:0) model.load_state_dict(checkpoint[model_state_dict]) optimizer.load_state_dict(checkpoint[optimizer_state_dict]) start_epoch checkpoint[epoch] 1 lr_scheduler.load_state_dict(checkpoint[lr_scheduler_state_dict]) # 前提是保存时也把它写进去如果只使用保存时的epoch信息配合torch.save也这么干关键是必须连学习率调度器的状态一起保存否则你之前的ReduceLROnPlateau或CosineAnnealing已经改了好几轮学习率恢复后在错误的学习率下继续训练曲线会直接飞掉。Keras那边恢复训练通常就是load_model然后model.fit继续跑同样的如果用了lr_scheduler回调最好也从checkpoint里把学习率恢复到正确值不过Keras的save_model不会连带保存lr调度器的内部状态所以我自己在实际做多阶段训练时一般采取“固定总epoch分段保存”的办法第一段结束后手动torch.load接着跑反而更好控制。加载权重之后还得做个验证动作别直接投入训练就去吃饭。我通常拿训练集里的一个固定batch做前向对比看恢复模型输出和保存模型输出是否一致。如果恢复后输出跟保存时一致说明checkpoint没坏再继续训练才靠谱。4. 常见问题与排查技巧实录4.1 早停失灵的几个典型现象早停策略看起来就两个参数实际使用中翻车的原因很多。我列几个我真实遇到过的第一monitor指标名写错。Keras的History对象里指标名可能是val_loss也可能是val_accuracy取决于你在compile里metrics是怎么写的。我见过写成val_acc直接报错或定义一个不存在的监控键最后回调一直不触发训练照样跑满。排查方法很简单训练过程中打印history.history.keys()看里面到底有哪些字段。第二min_delta过小导致早停失效。如果val_loss在0.0001量级附近震荡min_delta1e-4约等于没有标准模型会一直停不下来。这时候要看训练曲线的量级把min_delta设置成损失值绝对量级的千分之一左右会比较合理。第三patience太小早停过于频繁。尤其使用Adam优化器时后期学习率最小到1e-5、1e-6模型的验证损失基本在缓慢爬行patience3很容易在第20个epoch触发离真正的收敛点还很远。解决方式是先用较大的patience跑一轮看它在哪个epoch附近触发再反过来调参。第四忽略了验证集规模。验证集很小比如只有200张图每次算出来的val_loss方差很大哪怕模型没实质变化指标数值也一堆毛刺。早停可能提前或延后触发。实际做法是加大验证集或者连续多个epoch取平均构造平滑指标。4.2 checkpoint加载报错排查常见的加载报错集中在“权重维度不匹配”和“字典键前缀不匹配”两类。维度不匹配经常发生在换模型结构后加载旧权重比如全连接层输出类别数改了最后分类层的key还在旧checkpoint里加载必然报size mismatch。解决途径有两种严格保持模型结构不变或者加载时过滤掉不匹配的键名。后者在迁移学习里很常见一般会使用strictFalse加载预训练权重再把缺失的层随机初始化。但我要提醒strictFalse相当于“能匹配的就加载匹配不上的无视”对模型结构上的错误很宽容容易掩盖bug建议打印一下incompatible_keys再决定。字典键前缀不匹配是PyTorch的DDP或DataParallel训练后遗留的问题。模型保存时用的是module前缀加载时却用了原始模型键名直接对不上。要么保存时model.module.state_dict()取裸模型状态要么加载时也把权重套回module前缀。这个问题在分布式训练里出现频率极高所以我的建议很朴素所有训练代码里保存前统一调一个get_raw_state_dict辅助函数把前缀问题彻底从流程里抹掉。4.3 多轮训练中的磁盘与版本管理早停和checkpoint合起来用磁盘里会攒下越来越多的权重文件。我的做法是按实验名建目录并为每次run生成一个ID目录结构是checkpoints/ run01/ best.pth last_epoch40.pth states.json run02/一个训练跑完后我只保留best和last两个文件中间过渡版本的checkpoint优先删除。磁盘紧张时给每个run设置一个配额把训练的epoch数、模型大小、预计生成的checkpoint数量和磁盘空间手册比对一下。我之前有一个分割模型权重单个文件700MBpatience20加上save_best_only只保留一份看起来没问题但中间有一次我临时开了“每5轮保存一次”来调试一夜之间把30GB磁盘全塞满了第二天实验全废教训非常深刻。5. 从Day 38往后延伸早停与权重管理的进阶用法走完Day 38的完整流程后你的训练管道已经具备“该停就停、该存就存”的基础能力。但如果止步于此后面遇到更复杂的训练范式会觉得不够用。我想顺带分享几个进阶方向方便你自己做扩展。早停策略不应该只绑定在验证损失上。做目标检测的人更常监控mAP或AR值做语言模型的会盯perplexity。监控维度变更后早停的min_delta、patience都要跟着调整不是简单套用分类任务的数值。另外一个方向是把早停用在超参搜索里比如网格搜索或随机搜索训练一组参数时早停能大幅缩短劣质参数组合的训练时长让整个搜索进程节约大量时间。权重保存的进阶方向是断点续训的自动化。你可以在训练脚本里捕获SIGINT信号收到信号时优雅地保存一个临时checkpoint再退出下次拉起时自动加载。这个做法在长时间训练任务里价值很高能避免“训练30个小时后在epoch14被系统kill掉”的惨案。配合云上的定时快照机制训练环境本身就变得更可控了。还有一个我比较推崇的实践是把“最优权重”和“最优epoch”同时记录到日志中。很多框架只存权重不记录对应的训练步数导致后续分析训练曲线时不知道那个最优checkpoint对应哪个阶段。我的做法是延续前面提到的states.json训练过程中每次刷新best时就把epoch写入训练结束直接可以拿到“第33轮最优随后30轮无提升”这类结论。注意以上进阶内容建立在Day 38的基础流程已经跑通的前提下。先不要把功能一次性堆满因为每一个缓存、恢复、监控环节都可能引入新的bug。小步快跑才是训练代码保持稳定的方式。我现在已经习惯把早停和权重保存当作一套组合拳来使用。早停像是训练过程中的刹车知道什么时候该停下来权重保存像是行车记录仪把最好的状态留下来。两者配合好我可以把注意力放在数据增强、模型结构优化和损失函数调整上而不用时刻盯着曲线担心训练白跑或者最优模型被覆盖。如果你之前从来没有用过这两个机制我建议下一次训练时就把它们加进去跑一个epoch看下打印再对比一下停止后的模型和最后一轮模型在测试集上的差异这种直观的体验比读十篇教程都有效。