ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

MXNet Gluon 学习率探测器(Learning Rate Finder)实战:用一次小规模扫描为训练选定最优初始学习率

2026/9/21 15:04:42 拓冰建站 浏览量
MXNet Gluon 学习率探测器(Learning Rate Finder)实战:用一次小规模扫描为训练选定最优初始学习率 深度学习机器学习人工智能【免费下载链接】mxnetLightweight, Portable, Flexible Distributed/Mobile Deep Learning with Dynamic, Mutation-aware Dataflow Dep Scheduler; for Python, R, Julia, Scala, Go, Javascript and more项目地址https://gitcode.com/gh_mirrors/mxnet1/mxnet点击查看免费下载导读学习率learning rate是随机梯度下降SGD类优化器中最重要的超参数之一它同时决定了收敛速度和最终精度设置过低会让训练龟速前进设置过高则可能越过最优解甚至完全无法收敛。本教程基于 Leslie Smith 在论文Cyclical Learning Rates for Training Neural NetworksarXiv:1506.01186中提出的方法讲解如何在 MXNet 的 Gluon API 下实现一个Learning Rate Finder学习率探测器让网络从极小学习率开始逐 batch 训练并指数增长学习率记录损失变化曲线从而在几分钟内科学地为你的网络选定初始学习率。读完本文你将掌握把训练代码重构为单迭代可调学习率的 Learner 封装、无限数据流采样器、探测器与停止准则的完整实现以及如何在 ResNet 模型上验证所选学习率的效果。简单而强大的核心思想在训练开始前我们面对的是一张白纸网络已初始化、损失函数已定义、训练数据已就绪。Learning Rate Finder 的过程只有三步一次只训练一个 batch即一次迭代 iteration从一个非常小的学习率例如0.000001开始每迭代一次就缓慢增大学习率记录每次迭代的训练损失持续扫描直到观察到损失开始发散。随后把学习率 vs 训练损失绘制成图注意两个坐标轴都使用对数刻度即可直观地判断合适的初始学习率范围。其背后的规律非常直观当学习率极小时参数更新量可忽略不计损失几乎不变当学习率进入合理区间损失开始快速下降当学习率过大更新步长会跳过最优区域损失发散。因此损失开始显著下降与损失开始发散之间的区域就是值得选取的初始学习率区间。由于我们倾向于在损失尚稳定前提下尽量取大一般会选取该区间中偏大的值例如下图分析中选定0.05此时损失在0.001处开始下降、在0.1处开始发散。该方法与周期性学习率cyclical learning rate调度同源本仓库配套的 基础学习率调度教程 和 进阶学习率调度教程 分别介绍了mx.lr_scheduler中预置的各类调度器以及周期性调度背后的思想可与本教程对照阅读。从 Epoch 到 Iteration重构训练代码日常训练中我们习惯以epoch完整遍历一遍数据集为工作单元且学习率在一个 epoch 内保持不变。但 Learning Rate Finder以及周期性学习率调度要求在每次迭代时改变学习率。为此需要把训练逻辑重构成给定学习率跑一个 batch的形式。下面这个Learner类给出了推荐的封装方式在__init__中完成网络初始化、损失函数定义和 Trainer 创建把单 batch 前向/反向/更新的逻辑放在iteration方法中import mxnet as mx # Set seed for reproducibility mx.random.seed(42) class Learner(): def __init__(self, net, data_loader, ctx): :param net: network (mx.gluon.Block) :param data_loader: training data loader (mx.gluon.data.DataLoader) :param ctx: context (mx.gpu or mx.cpu) self.net net self.data_loader data_loader self.ctx ctx # So we dont need to be in for batch in data_loader scope # and can call for next batch in iteration self.data_loader_iter iter(self.data_loader) self.net.initialize(mx.init.Xavier(), ctxself.ctx) self.loss_fn mx.gluon.loss.SoftmaxCrossEntropyLoss() self.trainer mx.gluon.Trainer(net.collect_params(), sgd, {learning_rate: .001}) def iteration(self, lrNone, take_stepTrue): :param lr: learning rate to use for iteration (float) :param take_step: take trainer step to update weights (boolean) :return: iteration loss (float) # Update learning rate if different this iteration if lr and (lr ! self.trainer.learning_rate): self.trainer.set_learning_rate(lr) # Get next batch, and move context (e.g. to GPU if set) data, label next(self.data_loader_iter) data data.as_in_context(self.ctx) label label.as_in_context(self.ctx) # Standard forward and backward pass with mx.autograd.record(): output self.net(data) loss self.loss_fn(output, label) loss.backward() # Update parameters if take_step: self.trainer.step(data.shape[0]) # Set and return loss. self.iteration_loss mx.nd.mean(loss).asscalar() return self.iteration_loss def close(self): # Close open iterator and associated workers self.data_loader_iter.shutdown()其中几个设计细节值得展开说明动态改学习率iteration通过self.trainer.set_learning_rate(lr)在每次迭代前更新优化器学习率。该方法在 Gluon 的 trainer.py 中实现内部转发给底层优化器Optimizer.set_learning_rate注意在第一次step之前 Trainer 尚未真正创建优化器对象因此代码中先通过take_stepFalse的空迭代触发初始化。take_step开关探测器在正式扫描前会先跑一次不更新参数的空迭代用于触发 MXNet 的惰性权重初始化lazy initialization——net.initialize时并不立即分配参数内存只有第一次前向传播才会真正初始化。单 batch 损失mx.nd.mean(loss).asscalar()把 batch 内平均损失转成 Python 浮点数作为停止准则与绘图的输入。上下文切换data.as_in_context(self.ctx)把 batch 数据搬到目标设备GPU/CPU保证网络参数与数据在同一设备上。用无限数据流替代单轮遍历普通DataLoader在一个 epoch 结束后就会停止产出无法满足探测器连续迭代直到损失发散的需求。解决方案是自定义一个ContinuousBatchSampler它基于mx.gluon.data.RandomSampler源码见 sampler.py不断从数据集随机抽取索引并拼成 batch永不终止。下面代码以 CIFAR-10 图像分类为例先做数据预处理转 CHW 布局的float32张量 通道级标准化再构建无限数据流from mxnet.gluon.data.vision import transforms transform transforms.Compose([ # Switches HWC to CHW, and converts to float32 transforms.ToTensor(), # Channel-wise, using pre-computed means and stds transforms.Normalize(mean[0.4914, 0.4822, 0.4465], std[0.2023, 0.1994, 0.2010]) ]) dataset mx.gluon.data.vision.datasets.CIFAR10(trainTrue).transform_first(transform) class ContinuousBatchSampler(): def __init__(self, sampler, batch_size): self._sampler sampler self._batch_size batch_size def __iter__(self): batch [] while True: for i in self._sampler: batch.append(i) if len(batch) self._batch_size: yield batch batch [] sampler mx.gluon.data.RandomSampler(len(dataset)) batch_sampler ContinuousBatchSampler(sampler, batch_size128) data_loader mx.gluon.data.DataLoader(dataset, batch_samplerbatch_sampler)要点RandomSampler(len(dataset))产生[0, length)的随机排列索引不重复抽取外层while True循环保证采样器可以无限迭代batch_size128决定单次迭代的样本量进而影响trainer.step(data.shape[0])中传入的梯度归一化因子数据预处理与数据集解耦dataset.transform_first(transform)在取 batch 时即完成变换因此探测器每次迭代拿到的都是标准化后的数据。探测器实现扫描、记录与状态恢复LRFinder封装了完整的探测流程以lr_start为起点每迭代一次就把学习率乘以lr_multiplier逐条记录(lr, loss)直到LRFinderStoppingCriteria判定损失发散为止。find返回(learning_rate, loss)二元组列表plot负责可视化。from matplotlib import pyplot as plt class LRFinder(): def __init__(self, learner): :param learner: able to take single iteration with given learning rate and return loss and save and load parameters of the network (Learner) self.learner learner def find(self, lr_start1e-6, lr_multiplier1.1, smoothing0.3): :param lr_start: learning rate to start search (float) :param lr_multiplier: factor the learning rate is multiplied by at each step of search (float) :param smoothing: amount of smoothing applied to loss for stopping criteria (float) :return: learning rate and loss pairs (list of (float, float) tuples) # Used to initialize weights; pass data, but dont take step. # Would expect for new model with lazy weight initialization self.learner.iteration(take_stepFalse) # Used to initialize trainer (if no step has been taken) if not self.learner.trainer._kv_initialized: self.learner.trainer._init_kvstore() # Store params and optimizer state for restore after lr_finder procedure # Useful for applying the method partway through training, not just for initialization of lr. self.learner.net.save_parameters(lr_finder.params) self.learner.trainer.save_states(lr_finder.state) lr lr_start self.results [] # List of (lr, loss) tuples stopping_criteria LRFinderStoppingCriteria(smoothing) while True: # Run iteration, and block until loss is calculated. loss self.learner.iteration(lr) self.results.append((lr, loss)) if stopping_criteria(loss): break lr lr * lr_multiplier # Restore params (as finder changed them) self.learner.net.load_parameters(lr_finder.params, ctxself.learner.ctx) self.learner.trainer.load_states(lr_finder.state) return self.results def plot(self): lrs [e[0] for e in self.results] losses [e[1] for e in self.results] plt.figure(figsize(6,8)) plt.scatter(lrs, losses) plt.xlabel(Learning Rate) plt.ylabel(Loss) plt.xscale(log) plt.yscale(log) axes plt.gca() axes.set_xlim([lrs[0], lrs[-1]]) y_lower min(losses) * 0.8 y_upper losses[0] * 4 axes.set_ylim([y_lower, y_upper]) plt.show()这段实现里有三个容易被忽略但对正确性至关重要的设计惰性初始化的双重触发第一次iteration(take_stepFalse)只做前向/反向但不更新参数目的是让网络参数真正分配内存随后检查trainer._kv_initialized若 Trainer 尚未初始化 KVStore例如此前从未调用过step则手动调用_init_kvstore()。这两步保证了后续扫描时参数和优化器都处于可用状态。参数与优化器状态快照net.save_parameters(lr_finder.params)与trainer.save_states(lr_finder.state)分别保存网络参数和优化器内部状态如动量。扫描结束后通过load_parameters/load_states恢复现场使得Learning Rate Finder 可以随时介入训练流程而不破坏模型状态——既能用于初始学习率的选取也能在训练中途重新校准。save_parameters/load_parameters定义于 Gluon 的 block.pysave_states/load_states定义于 trainer.py。学习率按几何级数增长lr lr * lr_multiplier默认lr_multiplier1.1即每次迭代学习率提升 10%。配合双对数坐标绘图扫描结果在图上呈近似均匀分布便于目视选取。停止准则平滑移动平均判发散何时停止扫描经验表明直接使用原始损失做阈值容易受单 batch 噪声干扰采用平滑后的移动平均能得到更稳定的停止规则。LRFinderStoppingCriteria的规则是当平滑平均损失超过初始损失的两倍且迭代次数已达到最小值min_iter时停止。class LRFinderStoppingCriteria(): def __init__(self, smoothing0.3, min_iter20): :param smoothing: applied to running mean which is used for thresholding (float) :param min_iter: minimum number of iterations before early stopping can occur (int) self.smoothing smoothing self.min_iter min_iter self.first_loss None self.running_mean None self.counter 0 def __call__(self, loss): :param loss: from single iteration (float) :return: indicator to stop (boolean) self.counter 1 if self.first_loss is None: self.first_loss loss if self.running_mean is None: self.running_mean loss else: self.running_mean ((1 - self.smoothing) * loss) (self.smoothing * self.running_mean) return (self.running_mean self.first_loss * 2) and (self.counter self.min_iter)参数含义与调优建议smoothing0.3控制移动平均的平滑程度。running_mean (1 - smoothing) * loss smoothing * running_mean值越大对历史依赖越强、曲线越平滑但响应发散越滞后。如果损失曲线噪声很大例如 batch 很小可以适当增大min_iter20防止在扫描早期因个别噪声点误触发停止保证至少扫描 20 次迭代让学习率从1e-6增长到约1e-6 * 1.1^19 ≈ 6.1e-6以上才开始考虑终止初始损失first_loss取第一次迭代的损失作为基准后续平滑均值超过其 2 倍即视为发散——这对应损失进入上升区的典型特征。该准则可按需自定义例如改用更大的倍数、更长的最小迭代数或结合验证集损失关键是保持阈值 最小迭代数的双重保护避免误判。实战用 Pre-activation ResNet-18 探测 CIFAR-10准备工作完成后即可从 Gluon 模型库加载Pre-activation ResNet-18resnet18_v2定义见 resnet.py实例化 Learner 并启动探测器ctx mx.gpu() if mx.context.num_gpus() else mx.cpu() net mx.gluon.model_zoo.vision.resnet18_v2(classes10) learner Learner(netnet, data_loaderdata_loader, ctxctx) lr_finder LRFinder(learner) lr_finder.find(lr_start1e-6) lr_finder.plot()代码会优先使用 GPUmx.context.num_gpus() 0时否则回退到 CPUclasses10对应 CIFAR-10 的 10 个类别。运行后plot()绘制出双对数坐标下的学习率–损失散点图按前文原则选择初始学习率损失持续下降约0.001到0.05区间但尚未发散0.1处开始发散且尽量取较大的值因此这里选定0.05。验证一学习率 0.05推荐值用同一批初始参数先save_parameters(net.params)保存基线保证不同学习率的对比起点一致训练 300 次迭代learner.net.save_parameters(net.params) lr 0.05 for iter_idx in range(300): learner.iteration(lrlr) if ((iter_idx % 100) 0): print(Iteration: {}, Loss: {:.5g}.format(iter_idx, learner.iteration_loss)) print(Final Loss: {:.5g}.format(learner.iteration_loss))输出Iteration: 0, Loss: 2.785 Iteration: 100, Loss: 1.6653 Iteration: 200, Loss: 1.4891 Final Loss: 1.1812损失从约 2.7 显著下降到 1.2。注意这里只用了单个 128 样本的 batch 评估损失估计方差较大但足以给出总体趋势。验证二学习率 0.5偏大作为对照用同样的初始参数load_parameters(net.params)恢复基线尝试偏大的学习率net mx.gluon.model_zoo.vision.resnet18_v2(classes10) learner Learner(netnet, data_loaderdata_loader, ctxctx) learner.net.load_parameters(net.params, ctxctx) lr 0.5 for iter_idx in range(300): learner.iteration(lrlr) if ((iter_idx % 100) 0): print(Iteration: {}, Loss: {:.5g}.format(iter_idx, learner.iteration_loss)) print(Final Loss: {:.5g}.format(learner.iteration_loss))输出Iteration: 0, Loss: 2.6469 Iteration: 100, Loss: 1.9666 Iteration: 200, Loss: 1.6919 Final Loss: 1.366损失虽然在下降但最终只到 1.366明显差于 0.05 的结果——这正是学习率越过发散临界点后更新步长过大、难以稳定收敛的典型表现。验证三学习率 0.005偏小再看一个偏保守的学习率net mx.gluon.model_zoo.vision.resnet18_v2(classes10) learner Learner(netnet, data_loaderdata_loader, ctxctx) learner.net.load_parameters(net.params, ctxctx) lr 0.005 for iter_idx in range(300): learner.iteration(lrlr) if ((iter_idx % 100) 0): print(Iteration: {}, Loss: {:.5g}.format(iter_idx, learner.iteration_loss)) print(Final Loss: {:.5g}.format(learner.iteration_loss))输出Iteration: 0, Loss: 2.605 Iteration: 100, Loss: 1.8621 Iteration: 200, Loss: 1.6316 Final Loss: 1.2919最终损失 1.2919 与 0.05 的结果相近因为 0.005 仍处于探测器图中损失下降区内但收敛明显更慢。三组实验对比清晰地说明探测器选出的 0.05 在相同迭代预算下取得了最低的最终损失同时保持训练稳定。总结与延伸Learning Rate Finder 把凭经验拍学习率变成了可量化的过程只需为你的网络封装一个支持单迭代指定学习率的 Learner配合无限数据采样器和本文的LRFinder/LRFinderStoppingCriteria一次扫描即可获得学习率–损失曲线从而在发散点之前选取尽可能大的初始学习率兼顾收敛速度与稳定性。更进一步探测器选出的初始学习率还可以与各类学习率调度器配合使用固定衰减、阶梯衰减、余弦退火乃至周期性学习率调度可参考本仓库的 基础学习率调度教程覆盖mx.lr_scheduler中预置调度器的可视化与自定义实现和 进阶学习率调度教程深入周期性调度思想。把它们组合起来你就能为每次训练任务快速锁定起点高、过程稳、终局好的完整学习率方案。赞分享深度学习机器学习人工智能【免费下载链接】mxnetLightweight, Portable, Flexible Distributed/Mobile Deep Learning with Dynamic, Mutation-aware Dataflow Dep Scheduler; for Python, R, Julia, Scala, Go, Javascript and more项目地址https://gitcode.com/gh_mirrors/mxnet1/mxnet点击查看免费下载相关推荐MXNet Gluon 学习率查找器Learning Rate Finder实战用一次训练快速定位最优初始学习率MXNet Gluon 学习率查找器Learning Rate Finder实战用一次训练快速定位最优初始学习率 导读 学习率是随机梯度下降SGD训练人工智能深度学习机器学习MXNet Gluon 学习率探测器Learning Rate Finder实战以迭代为单位扫描并定位最优初始学习率MXNet Gluon 学习率探测器Learning Rate Finder实战以迭代为单位扫描并定位最优初始学习率 学习率是随机梯度下降SGD训练中深度学习人工智能机器学习分布式训练MXNet Gluon 学习率完全指南从 Learning Rate Finder 到高级调度策略MXNet Gluon 学习率完全指南从 Learning Rate Finder 到高级调度策略 学习率是随机梯度下降训练中最关键的超参数它同时决定收敛速人工智能深度学习机器学习创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考