9.实验追踪基础
机器学习项目的核心就是不断尝试。调整一个超参数,换一组数据,改几行代码,然后等待结果。问题在于,尝试多了,很快就记不清每个版本到底改了什么,哪个版本的效果最好。笔记本里密密麻麻的记录,文件夹里一堆 model_v1.pkl、model_v2_final.pkl、model_v2_final_really.pkl,最后连自己都不敢确定哪个才是真的"最终版"。
DVC 的实验追踪系统就是为了解决这种混乱而生的。它让我们可以大胆尝试各种想法,同时又能保持工作区的整洁,所有实验记录都井井有条,随时可以回溯、对比和复现。
实验运行:从想法到执行
基本运行方式
最简化的实验运行就是 dvc exp run 这条命令。当工作区里的代码、数据或参数发生变化时,执行它会触发流水线重现,并将结果保存为一个实验。这个过程本质上等同于先执行 dvc repro,再执行 dvc exp save,只不过合并成了一步。
$ dvc exp run
...
Reproduced experiment(s): puffy-daks
Experiment results have been applied to your workspace.
命令执行完毕后,工作区里就是刚才实验的结果。DVC 会自动生成一个类似 puffy-daks 这样的名字来标识这次实验。这个名字虽然看起来有点随意,但比 exp_001 这样的数字序列要好记多了。如果想给实验起个更有意义的名字,可以用 --name 参数:
$ dvc exp run --name baseline-cnn
...
Reproduced experiment(s): baseline-cnn
实验名称会暴露在环境变量 DVC_EXP_NAME 中,代码里可以直接读取,方便在日志或输出里标记当前实验的身份。
参数化运行
手动修改 params.yaml 文件再运行实验,这种重复劳动很快会让人厌烦。--set-param(简写 -S)参数允许我们在命令行里直接覆盖参数值,DVC 会临时修改参数文件,执行完实验后再恢复。
$ dvc exp run -S train.epochs=20 -S train.learning_rate=0.001
这种覆盖支持 Hydra 的语法,可以进行追加、删除等操作。比如追加一个新参数:
$ dvc exp run -S +optimizer.weight_decay=0.01
或者删除某个参数:
$ dvc exp run -S ~model.dropout
默认情况下,参数修改作用于 params.yaml。如果项目使用了其他参数文件,可以通过文件名前缀指定:
$ dvc exp run -S train_config.json:+train.weight_decay=0.001
这种方式特别适合快速迭代,想试几个不同的学习率,直接在命令行里改就行,不用反复打开编辑器。
队列与并行执行
有时候想尝试的参数组合很多,一个个跑太费时间。DVC 的实验队列功能可以把多个实验排好队,然后批量执行,甚至并行跑。
先用 --queue 参数把实验加入队列:
$ dvc exp run --queue -S train.min_split=8
Queued experiment 'blowy-pail' for future execution.
$ dvc exp run --queue -S train.min_split=16
Queued experiment 'nubby-gram' for future execution.
加入队列的实验不会立即执行,而是保存在内部任务队列里。可以用 dvc queue start 启动队列处理:
$ dvc queue start
Started '1' new experiments task queue worker.
默认情况下,队列是串行执行的,一个跑完再跑下一个。如果机器有多个 CPU 核心,可以用 --jobs 参数指定并行数量:
$ dvc queue start --jobs 4
这样最多可以同时跑 4 个实验,大大缩短调参时间。不过要注意,并行实验会在独立的临时目录里运行,每个实验都有自己的隔离环境,避免相互干扰。
队列功能还支持网格搜索。Hydra 的 choice 和 range 语法可以一次性生成多个参数组合:
$ dvc exp run --queue \
-S train.arch='resnet18,shufflenet_v2_x2_0' \
-S 'train.fine_tune_args.base_lr=range(0.001, 0.01, 0.001)'
这条命令会自动生成 18 个实验(2 个架构 × 9 个学习率),全部加入队列。DVC 会智能地利用运行缓存,如果某个参数组合之前已经跑过且依赖没变化,会直接复用结果,不会浪费计算资源。
对于特别耗时的实验,可以用 --temp 参数在临时目录里运行,这样工作区可以继续做其他事情:
$ nohup dvc exp run --temp &
实验在后台运行,结果会自动保存,不会占用当前终端。
实验保存:捕获工作现场
自动保存机制
使用 dvc exp run 时,实验结果会自动保存。但有时候,我们可能直接运行了脚本,或者在其他环境里训练了模型,这时候就需要手动保存实验。
dvc exp save 命令可以把当前工作区的状态捕获为一个实验。它会检查所有被 Git 或 DVC 追踪的文件,把它们的当前状态打包保存。
$ python train.py # 直接运行脚本
$ dvc exp save --name manual-run
这个命令会创建一个叫 manual-run 的实验,包含当前所有已追踪文件的快照。如果工作区里有未追踪的文件,需要用 --include-untracked(简写 -I)参数显式包含:
$ dvc exp save --name with-new-data -I data/new.xml.dvc -I src/extratrees.py
手动保存实验
实验保存不仅限于代码和参数,还包括数据。如果新增了数据文件,先用 dvc add 追踪,再用 dvc exp save 保存:
$ dvc add data/new.xml
$ dvc exp save --name data-exp -I data/new.xml.dvc
保存实验时,可以附加一条说明信息,方便日后回忆这个实验的目的:
$ dvc exp save --name cnn-64 --message "尝试64个卷积核,效果应该比32好"
实验保存后,工作区可以随便折腾,甚至用 git reset --hard 清空所有改动。实验数据都安全地存储在 DVC 的内部引用里,不会丢失。
保存的最佳实践
实验保存的一个常见场景是,跑完一个实验后发现结果不错,但还没准备好提交到 Git 主分支。这时候先保存为实验,等确定了再转正:
$ dvc exp run # 跑完实验
$ dvc exp save --name promising # 保存下来
$ git reset --hard # 清空工作区,继续尝试其他想法
之后如果确定这个实验值得保留,可以用 dvc exp apply 恢复工作区,然后正常 Git 提交。
对于大型项目,可能只想保存特定阶段的改动。dvc exp save 支持指定目标:
$ dvc exp save dir_a/dvc.yaml --name stage-exp
这样只会缓存 dir_a/dvc.yaml 里定义的阶段的输出,其他阶段不受影响。不过 Git 追踪的文件(包括其他阶段的代码)还是会全部保存。
实验展示:一览众山小
表格化展示
跑了一堆实验后,最想知道的就是哪个效果最好。dvc exp show 命令把所有实验整理成一张大表,一目了然。
$ dvc exp show
默认展示会从 Git HEAD 派生的所有实验,包括工作区状态。表格从左到右分为几部分:实验名称、创建时间、指标(黄色)、参数(蓝色)、依赖(紫色)。指标和参数会自动从 dvc.yaml 和 DVCLive 的输出里提取。
表格默认会分页显示,按 q 退出。如果不想分页,加 --no-pager 参数:
$ dvc exp show --no-pager
自定义视图
实验多了,表格会变得很宽,很多列其实没变化。--only-changed 参数可以隐藏那些在所有实验里值都相同的列:
$ dvc exp show --only-changed
如果想精确控制显示哪些列,可以用 --keep 和 --drop 参数。--keep 指定要保留的列,支持正则表达式:
$ dvc exp show --keep 'train\..*' # 只显示 train 相关的参数
--drop 则指定要隐藏的列:
$ dvc exp show --drop 'prepare\..*' # 隐藏 prepare 相关的参数
这两个参数可以组合使用,--keep 的优先级高于 --drop。比如先隐藏所有参数,再保留特定的几个:
$ dvc exp show --drop '.*' --keep 'train\.(?!seed).*'
排序功能也很实用。--sort-by 指定按哪个指标或参数排序,--sort-order 指定升序还是降序:
$ dvc exp show --sort-by roc_auc --sort-order desc
这样就能快速找到指标最好的实验。
与其他工具集成
dvc exp show 支持多种输出格式,方便与其他工具集成。--csv、--json、--md 分别输出 CSV、JSON、Markdown 格式:
$ dvc exp show --csv > experiments.csv
$ dvc exp show --json > experiments.json
JSON 格式特别适合编程处理。Python 里可以直接用 DVC API 获取实验数据:
import dvc.api
import pandas as pd
exps = dvc.api.exp_show()
df = pd.DataFrame(exps)
这样就把实验数据转成了 DataFrame,可以随便分析和可视化。
实时指标追踪:训练不再盲目
DVCLive 基础
DVCLive 是 DVC 的实时追踪库,能在训练过程中持续记录指标、参数、图表等信息。最简单的用法是在代码里创建一个 Live 实例:
from dvclive import Live
with Live() as live:
live.log_param("epochs", 10)
for epoch in range(10):
# 训练代码
loss = train_one_epoch()
acc = evaluate()
live.log_metric("loss", loss)
live.log_metric("accuracy", acc)
live.next_step()
log_param 记录参数,log_metric 记录指标,next_step 标记一个训练步骤结束。每次调用 next_step,DVCLive 会更新报告和图表。
框架集成
手动插入日志代码有点麻烦,DVCLive 提供了主流机器学习框架的回调函数,可以自动记录指标。
对于 PyTorch Lightning,直接用 DVCLiveLogger:
from dvclive.lightning import DVCLiveLogger
trainer = Trainer(logger=DVCLiveLogger())
trainer.fit(model)
Hugging Face Transformers 从 4.36.0 版本开始内置了 DVCLive 支持,只要安装了 dvclive,设置环境变量就能自动记录:
$ export HF_DVCLIVE_LOG_MODEL=true
from transformers import TrainingArguments
args = TrainingArguments(..., report_to="dvclive")
trainer = Trainer(..., args=args)
Keras、XGBoost、LightGBM 等框架也都有对应的回调函数,用法类似。这些集成会自动记录框架输出的所有指标,还能保存模型检查点。
实时更新机制
DVCLive 默认会在 dvclive 目录下生成报告文件。report 参数可以控制报告格式:
with Live(report="html") as live: # 生成 HTML 报告
...
在 Jupyter Notebook 里,可以用 report="notebook" 让报告直接显示在单元格输出里,实时更新。
如果配置了 DVC Studio 并登录,dvc studio login,DVCLive 会把指标实时推送到云端,团队成员都能在网页上看到训练进展。这对于长时间训练的任务特别有用,不用一直守在服务器前。
实验状态查询:掌握全局
查看实验列表
dvc exp list 命令快速列出所有实验名称,比 dvc exp show 更简洁:
$ dvc exp list --all-commits
main:
cnn-32
cnn-64
cnn-128
默认只显示当前 Git HEAD 派生的实验,--all-commits 显示所有。如果想看远程仓库的实验,加上远程名称或 URL:
$ dvc exp list origin
main:
cnn-32
cnn-64
检查运行状态
实验队列启动后,可以用 dvc queue status 查看队列状态:
$ dvc queue status
这会显示正在运行的任务、等待的任务和已完成的任务。如果想看某个任务的实时日志,用 dvc queue logs:
$ dvc queue logs <task_id>
加上 --follow 参数可以持续跟踪日志输出,就像 tail -f 一样。
实验追踪是 DVC 最强大的功能之一。它把繁琐的手动记录变成了自动化流程,让我们可以专注于模型本身,而不是文件管理。从快速参数调整,到大规模网格搜索,再到团队协作,DVC 提供了一整套工具链。
下一章将深入探讨实验的比较与筛选,学习如何从几十个甚至上百个实验中,快速找出最优解,以及如何把有价值的实验正式纳入项目主线。