实验追踪与超参数扫描

Build~60 minutesPython先修要求: Phase 2 · 12 (Hyperparameter Tuning), Phase 3 · 11 (Intro to PyTorch)
三周之后,你不会记得 run_47 和 run_48 哪个学习率更好。本课将为你打造一个替你记住一切的追踪器。
学习目标
  • 解释为什么临时追踪方式(CSV 文件、git 提交信息、记忆)会在实验数量增加时失效。
  • 实现一个基于 JSONL 的实验追踪器,记录配置、每步指标、环境快照以及可查询的运行历史,且不依赖外部服务。
  • 比较网格、随机与贝叶斯扫描,并说明当只有少数超参数真正重要时,随机搜索为何更高效。
  • 实现 ASHA 风格的逐次减半早停,并统计它在运行达到完整预算前终止了多少运行。
  • 利用追踪器从记录的配置与种子复现一次运行。

问题

项目进行到第三周时,每个团队都会有一个像这样的目录:

textrun_47_final.pt
run_48_final.pt
run_49_backup_final.pt
best_model_so_far.pt
best_model_so_far_v2.pt

这些名字是谎言。数字不编码任何信息。而真正重要的那个文件——验证准确率达到 94.2% 的 checkpoint——已经不可能复现,因为没人记下学习率、随机种子,以及这次运行到底用了 0.3 还是 0.5 的 dropout。

这就是临时追踪死亡螺旋:

  • 记忆最先失效。 周一你调整了 lr、momentum 和 batch_size,到周三你已经无法把某张图和产生它的代码对应起来。
  • 电子表格和 CSV 日志第二个失效。 列名在不同运行之间漂移,文件名是手打的,而 val_loss 最好的那一行没有提交哈希、没有种子、也没有超参数快照。
  • git 注释第三个失效。 每次运行后都提交会污染历史,而且仍然无法记录中间指标、包版本或确切的随机初始化。

真正的成本不是丢失文件,而是丢失决策。你上线了一个模型,只因它“上周看起来更好”,却无法证明、复现或解释它为什么胜过替代方案。实验追踪把这些决策变成数据。

概念

实验追踪器到底记录什么

追踪器只是一个包含五个部分的结构化日志:

部分它是什么为什么重要
Run(运行)一次训练执行你比较和复现的最小单位
Config(配置)每个超参数和种子没有输入,输出就没有意义
Metrics(指标)每步标量历史你需要曲线,而不只是最终数字
Artifacts(产物)Checkpoint、数据集、预测结果一次运行产生的文件,一并版本化
Environment(环境)Python 版本、平台、工作目录让运行可复现的上下文

Weights & Biases、MLflow、TensorBoard 等生产工具都封装了同样的模型。它们增加了网页 UI、共享项目页、报告构建器,以及(在 W&B 中)离线模式:先写入本地目录,之后再同步。无论你使用托管服务还是 200 行的标准库实现,概念都是一样的。

扫描:网格、随机与贝叶斯

扫描就是尝试多种配置并记录每次结果的循环。三种经典策略的区别在于如何选择下一个配置。

网格搜索(Grid search) 评估指定值的每一种组合。它全面且易于审计,但成本随超参数数量指数增长。

随机搜索(Random search) 从分布中采样配置。Bergstra & Bengio(2012)证明,在相同预算下,随机搜索通常能找到比网格搜索更好的配置,因为大多数问题的有效维度很低:只有 1-2 个超参数真正重要。网格搜索把大部分评估浪费在不重要的维度上,而随机搜索在重要维度上覆盖得更密集。

贝叶斯优化(Bayesian optimization) 从已有评估中学习。它用一个廉价的替代模型(通常是高斯过程)拟合已见的 (config, score) 对,然后用采集函数(如期望改进 Expected Improvement)选择下一个点:

\text{EI}(x) = \mathbb{E}\big[ \max(0, f(x) - f^*) \big]

其中 f^* 是目前观察到的最佳分数。贝叶斯优化通常比随机搜索少用 2-5 倍评估就能找到好配置,代价是更多的记录工作。

ASHA:早停坏运行

不是每个配置都值得完整的训练预算。如果一个模型在训练几个 epoch 后仍然很差,那它在几百个 epoch 后几乎肯定仍然很差。逐次减半(及其异步版本 ASHA)给很多运行分配小预算,保留最好的一部分,再让幸存者获得更多 epoch。

使用乘数 \eta 的 rung 计划看起来像这样:

r,\; \eta r,\; \eta^2 r,\; \dots,\; R

在第 \eta^k r 个 rung,每个幸存运行已经训练了 \eta^k r 步。运行按目标指标排名,只有前 1/\eta 被晋级。中位停止规则本质相同:如果一次运行的中间结果比同一步已完成运行的中位数还差,就终止它。

ASHA 以最好的方式表现得很“残酷”。一个原本会浪费 27 个 epoch 的配置,常常在 1 或 3 个 epoch 后就被终止,把算力释放出来给有希望的配置。

如何阅读平行坐标图

平行坐标图是扫描版电子表格。每条竖轴是一个超参数或指标,每条线是一次运行。阅读时要注意:

  • 寻找好线聚集的区域——反复落到同一区域的配置。
  • 寻找顶部扩散很小的轴——那些超参数不那么重要。
  • 寻找急剧转折处——某个轴上微小的变化把好坏运行分开。那就是你下一步应该重点调节的旋钮。

动手实现

本课在 code/main.py 中提供了一个可复用的标准库追踪器。没有网络调用、没有守护进程、不需要账号。把它导入任何训练脚本,你就能获得与生产工具相同的原语。

Tracker:每次运行一条 JSONL 记录

pythonfrom main import Tracker

tracker = Tracker(".runs")
metrics = [
    {"step": 1, "loss": 2.3, "accuracy": 0.21},
    {"step": 2, "loss": 1.8, "accuracy": 0.45},
]
run_id = tracker.log_run({"lr": 0.01, "batch_size": 64, "seed": 42}, metrics)

每个 .jsonl 文件只包含一行,里面有运行 id、时间戳、环境快照、配置和完整的指标历史。这种格式便于追加、便于 diff,也便于用 json.loads 读取。

Sweeper:网格、随机与 ASHA

pythonfrom main import Sweeper, toy_objective

tracker = Tracker(".runs")
sweeper = Sweeper(tracker, toy_objective, max_steps=27, rung_multiplier=3)

param_grid = {
    "lr": [0.05, 0.1, 0.2],
    "momentum": [0.0, 0.5, 0.9],
}
configs = sweeper.grid_configs(param_grid)
survivors = sweeper.run_asha(configs, metric="loss", minimize=True)

run_asha 在第一个 rung(1 步)训练每个配置,保留前三分之一,让这些幸存者在 3 步继续训练,再次保留前三分之一,如此继续直到完整预算 27 步。被终止的运行仍然会被记录,因此你可以精确研究什么被终止以及为什么被终止。

玩具任务

内置目标函数是一个二维病态二次函数:

L(w) = w_0^2 + 2 w_1^2

用带动量的 SGD 优化。它是凸的、确定性的、快速的,非常适合演示扫描确实能找到最佳的 (lr, momentum) 组合,以及两次相同种子的运行会产生完全一致的指标历史。

对运行数据库的查询

pythonbest = tracker.best_run("loss", minimize=True)
fast = tracker.filter_runs(lambda r: r["config"].get("lr") == 0.1)

best_run 返回最终指标最好的运行;filter_runs 返回配置满足某个断言的所有运行。这两个函数替代了“在日志文件里 grep”的工作流。

运行演示

consolepython3 main.py

演示会扫描九组 (lr, momentum) 组合,打印排行榜,报告 ASHA 提前终止了多少运行,并通过把同种子的运行记录两次来验证可复现性。

验证

code/tests/test_main.py 锁定了让追踪器真正有用的行为:

  • 环境快照包含 Python 版本和平台信息;
  • log_run 写入的 JSONL 文件能被 load_runs 原样读回;
  • best_run 正确选择最小和最大指标;
  • filter_runs 按配置断言筛选运行;
  • 网格和随机配置生成器产生预期数量的组合,并尊重分布边界;
  • ASHA 提前终止没希望的运行,同时晋级幸存者;
  • 相同种子的相同配置产生完全一致的指标历史。

为什么这对工程师很重要

  • 共享运行记录。 追踪器是团队的单一事实来源。没人需要再问“run_48 的学习率是多少?”,因为答案就在运行记录里。
  • 可复现性。 配置加种子加环境快照构成一个可复现包。两个月后你仍能重现完全相同的训练曲线。
  • 成本控制。 ASHA 和中位剪枝可以通过提前终止注定失败的运行,把扫描的算力账单砍掉 50-80%。在云 GPU 预算上,这是真金白银。
  • 决策证据。 排行榜和平行坐标图把“我觉得这个模型更好”变成“这个配置以更少的 epoch 获得了更低的验证损失”。
  • 离线优先的安全性。 生产追踪器支持离线日志是有原因的:实验会在隔离集群、spot 实例和飞机上运行。先写本地 JSONL 再同步,是更稳健的模式。

提示

  • 务必将随机种子记录在配置中。一个无法复现的好结果是幽灵。
  • 如果最佳超参数值落在搜索空间边缘,就扩大空间。最优值很可能在外面。
  • 扫描不能替代理解。在跑 100 次试验之前,先跑 10 次并画出学习曲线,确保预算足够让指标有意义。
  • 平行坐标图需要足够大的样本量。运行少于 20 次时,这张图 mostly 是噪声。
  • 比较两次运行时,比较整条曲线,而不只是最终数字。收敛更慢的运行可能泛化更好。

关键术语

术语人们怎么说实际含义
Run"一次训练"训练脚本的一次执行,由唯一 id 标识,并附带配置与指标
Config"超参数"训练前你控制的所有输入:学习率、种子、架构、batch size 等
Metrics history"损失曲线"每步记录的标量时间序列,而不仅是最终值
Artifact"checkpoint"一次运行产生的任何文件:模型权重、tokenizer、数据集划分、预测 CSV
Grid search"穷举所有组合"在超参数网格的笛卡尔积上进行全面搜索,成本指数增长
Random search"随机采样"从分布中采样配置。当有效维度低时比网格搜索更高效
Bayesian optimization"智能搜索"用替代模型和采集函数基于已有结果选择下一个配置
ASHA"早停"逐次减半调度器,只把最好的一部分运行晋升到更长预算
Median stopping rule"终止落后者"如果某次运行的中间指标比同一步已完成运行的中位数差,就终止它
Offline mode"离线记录"先把运行数据写到本地,之后再上传,训练不依赖实时服务

“Stay hungry, stay foolish.”

— Steve Jobs

esc
↑↓切换↵打开esc关闭