11.高级实验工作流
当实验数量从几个增长到几十个甚至上百个时,手动管理每个实验的运行、比较和分享就变得不可行了。这时候就需要更系统化的工作流来支撑大规模的实验探索。DVC 提供了一整套高级工具,让实验管理从手工作坊模式升级为工业化流水线。
实验队列:批量管理的基石
实验队列是 DVC 高级工作流的核心基础设施。它允许我们将实验任务排队,然后批量执行,而不是在终端前守着每个实验跑完再启动下一个。这种机制特别适合超参数搜索、消融研究等需要运行大量相似实验的场景。
队列的基本操作
使用 dvc exp run --queue 就能把实验加入队列,而不会立即执行。比如我们想尝试不同的学习率:
$ dvc exp run --queue -S train.lr=0.001
Queueing with overrides '{'params.yaml': ['train.lr=0.001']}'.
Queued experiment 'blowy-pail' for future execution.
$ dvc exp run --queue -S train.lr=0.01
Queueing with overrides '{'params.yaml': ['train.lr=0.01']}'.
Queued experiment 'nubby-gram' for future execution.
这两个命令会创建两个实验任务,分别使用不同的学习率。DVC 会给每个实验生成一个唯一的标识符(像 blowy-pail 这样的随机名称),方便后续引用。
队列创建后,使用 dvc queue start 启动执行:
$ dvc queue start
默认情况下,队列是串行处理的,一个接一个运行。如果机器有多个 CPU 核心,可以通过 -j 参数指定并行任务数:
$ dvc queue start -j 4
这会启动 4 个工作进程,同时处理队列中的实验。需要注意的是,并行执行时要确保系统资源足够,特别是 GPU 内存。如果所有实验都要用 GPU,并行数最好不要超过可用的 GPU 数量。
队列的状态监控
实验在后台运行时,我们可能需要查看进度或调试问题。dvc queue status 命令提供了队列的整体状态:
$ dvc queue status
Task Name Status
blowy-pail Running
nubby-gram Queued
可以看到每个任务的当前状态:正在运行、排队中、已完成或失败。
如果想看某个具体实验的输出日志,用 dvc queue logs:
$ dvc queue logs blowy-pail
这个命令会实时显示实验的控制台输出,就像实验在前台运行一样。对于调试失败的实验特别有用。
有时候实验可能卡住了或者跑错了,需要强制终止。dvc queue kill 可以停止正在运行的任务:
$ dvc queue kill blowy-pail
而 dvc queue remove 则用于从队列中删除尚未运行的任务:
$ dvc queue remove --queued
这个命令会清空整个队列,让一切重新开始。
队列的实现机制
DVC 的队列系统基于 Celery 和 dvc-task 构建。每个实验任务实际上是一个独立的进程,在 .dvc/tmp/exps/ 目录下的临时工作空间中运行。这种隔离设计确保实验之间不会互相干扰,每个实验都基于它被加入队列时的代码状态执行。
临时工作空间共享同一个 DVC 缓存,所以数据不需要重复存储。但这也意味着,如果两个实验依赖相同的数据和代码,后运行的实验可能会命中缓存而跳过实际计算。这在超参数搜索时是很大的优势,能节省大量时间。
对于长时间运行的实验,如果不想阻塞当前终端,可以结合 --temp 标志使用:
$ nohup dvc exp run --temp &
这样实验在后台临时目录运行,我们可以继续在当前工作空间做其他事情。
超参数调优:从手动到自动化
超参数调优是机器学习实验中最常见的重复性工作。DVC 的队列系统与参数覆盖功能结合,能轻松实现网格搜索、随机搜索等调优策略。
网格搜索的实现
网格搜索需要尝试多个参数的组合。手动写脚本生成所有组合很繁琐,DVC 提供了简洁的语法。比如我们要搜索学习率和批量大小的最佳组合:
$ dvc exp run --queue \
-S 'train.lr=0.001,0.01,0.1' \
-S 'train.batch_size=32,64,128'
这条命令会自动创建 9 个实验(3 个学习率 × 3 个批量大小),每个实验使用不同的参数组合。DVC 会解析逗号分隔的值,生成所有可能的组合。
更复杂的场景可能需要使用范围语法。比如学习率在 0.001 到 0.01 之间以 0.002 为步长搜索:
$ dvc exp run --queue -S 'train.lr=range(0.001, 0.01, 0.002)'
这会生成 5 个实验,学习率分别为 0.001、0.003、0.005、0.007 和 0.009。
参数覆盖的高级用法
参数覆盖不仅限于修改已有参数,还可以添加新参数或删除参数。语法遵循 Hydra 的覆盖规范:
- 修改参数:
train.lr=0.01 - 添加参数:
+train.weight_decay=0.001 - 删除参数:
~train.dropout
比如想在实验中尝试添加权重衰减:
$ dvc exp run -S '+train.weight_decay=0.0001'
这会临时在 params.yaml 中添加 weight_decay 参数,但不会影响原始文件。实验结束后,工作空间的参数文件保持不变。
参数覆盖也支持自定义参数文件。如果项目使用多个参数文件,可以用文件名前缀指定:
$ dvc exp run -S 'train_config.json:train.lr=0.01'
这只会修改 train_config.json 中的学习率,而不影响默认的 params.yaml。
实验的批量比较
大量实验运行完后,如何快速找到最好的那个?dvc exp show 提供了表格视图,但实验太多时可能不够直观。可以结合 --no-pager 和 grep 过滤:
$ dvc exp show --no-pager | grep -E '(Experiment|lr|batch_size|accuracy)'
更好的方式是使用 dvc exp diff 比较特定实验:
$ dvc exp diff main blowy-pail nubby-gram
这会显示基准分支(main)与两个实验之间的参数和指标差异。对于超参数调优,通常关注验证准确率或损失的变化趋势。
如果实验数量实在太多,可以考虑用 dvc exp remove 清理表现不佳的实验,保持实验列表的整洁:
$ dvc exp remove --rev HEAD --all
这会删除当前提交关联的所有实验,但保留最后一次运行的结果。
Hydra 配置组合:管理复杂配置
当项目规模扩大,参数文件会变得庞大且难以维护。不同模型、数据集、优化器的组合可能产生爆炸性的配置数量。Hydra 是一个专门解决这类问题的配置框架,DVC 从 2.0 版本开始原生支持 Hydra 的配置组合功能。
Hydra 集成的基础设置
使用 Hydra 前,需要先启用 DVC 的 Hydra 支持:
$ dvc config hydra.enabled True
这会告诉 DVC 在运行实验时使用 Hydra 来组合配置。默认情况下,DVC 会在 conf 目录查找 Hydra 配置。
典型的 Hydra 配置目录结构如下:
conf/
├── config.yaml # 默认配置列表
├── dataset/
│ ├── imagenette.yaml
│ └── imagewoof.yaml
└── train/
├── baseline.yaml
├── model/
│ ├── alexnet.yaml
│ ├── efficientnet.yaml
│ └── resnet.yaml
└── optimizer/
├── adam.yaml
└── sgd.yaml
config.yaml 定义了默认的组合:
defaults:
- dataset: imagenette
- train/model: resnet
- train/optimizer: sgd
这意味着默认使用 imagenette 数据集、ResNet 模型和 SGD 优化器。每个子目录代表一个配置组,组内的 YAML 文件定义了具体的参数。
配置组合的运行机制
当执行 dvc exp run 时,Hydra 会先根据 config.yaml 中的默认列表,从各个配置组中加载对应的文件,合并成一个完整的 params.yaml,然后 DVC 流水线基于这个文件运行。
比如 train/model/resnet.yaml 可能包含:
train:
model:
name: ResNet
size: 50
weights: ResNet50_Weights.IMAGENET1K_V2
而 train/optimizer/sgd.yaml 包含:
train:
optimizer:
name: SGD
lr: 0.001
momentum: 0.9
Hydra 会将这些片段合并成最终的 params.yaml,包含完整的模型和优化器配置。
运行时覆盖配置组
Hydra 的真正威力在于运行时动态切换配置组。比如想尝试 EfficientNet 模型,不需要修改配置文件,直接在命令行指定:
$ dvc exp run --set-param 'train/model=efficientnet'
DVC 会告诉 Hydra 用 train/model/efficientnet.yaml 替换默认的 resnet.yaml,重新组合 params.yaml 后运行实验。
更复杂的场景是同时切换多个配置组:
$ dvc exp run --queue \
-S 'train/model=alexnet,resnet' \
-S 'train/optimizer=adam,sgd'
这会生成 4 个实验组合(2 个模型 × 2 个优化器),每个实验使用不同的配置组合。
细粒度参数调整
除了切换整个配置组,Hydra 也支持修改组合后配置中的具体值。比如使用 EfficientNet 但把学习率调高:
$ dvc exp run --set-param 'train/model=efficientnet' \
--set-param 'train.optimizer.lr=0.01'
这种灵活性让我们既能保持配置的模块化,又能对特定实验做微调。
从 Hydra 迁移到 DVC
如果项目已经在使用 Hydra,迁移到 DVC 需要一点代码调整。传统的 Hydra 应用使用 @hydra.main() 装饰器:
import hydra
from omegaconf import DictConfig
@hydra.main(version_base=None, config_path="conf", config_name="config")
def main(cfg: DictConfig) -> None:
# 使用 cfg 训练模型
pass
在 DVC 集成模式下,这个装饰器不再需要。DVC 会提前组合好配置并写入 params.yaml,代码只需读取这个文件:
from omegaconf import OmegaConf
def main() -> None:
cfg = OmegaConf.load("params.yaml")
# 使用 cfg 训练模型
pass
这种分离让配置管理更纯粹:Hydra 负责组合,DVC 负责版本控制和实验追踪,代码只需关心如何使用配置。
高级 Hydra 功能
DVC 支持 Hydra 的自定义解析器(custom resolver)。比如在配置中需要动态计算路径,可以注册一个 join 解析器:
# hydra_plugins/my_resolver.py
import os
from omegaconf import OmegaConf
OmegaConf.register_new_resolver('join', lambda x, y: os.path.join(x, y))
然后在 YAML 配置中使用:
dir: raw/data
relpath: dataset.csv
fullpath: ${join:${dir},${relpath}}
最终生成的 params.yaml 中,fullpath 会是 raw/data/dataset.csv。这种动态计算能力让配置更灵活,避免硬编码路径。
实验的分享与协作
实验的价值往往在分享中体现。无论是向同事展示结果,还是在不同机器间同步进度,DVC 都提供了完整的实验分享机制。
实验推送的基本流程
dvc exp push 命令将本地实验推送到 Git 远程仓库和 DVC 远程存储。与普通的 git push 不同,实验推送会同时处理代码和数据:
$ dvc exp push origin
这条命令会将当前提交关联的所有实验推送到名为 origin 的 Git 远程。DVC 会将实验存储在特殊的 Git 引用中(refs/exps/),这些引用不会出现在常规的分支列表里,因此不会污染仓库。
推送时,DVC 会自动处理数据部分,将实验用到的模型、数据集等缓存文件推送到 DVC 远程存储。同事拉取实验时,能完整重现所有结果。
选择性推送实验
如果只想推送特定实验,可以指定实验名称:
$ dvc exp push origin blowy-pail
或者推送某个基准提交的所有实验:
$ dvc exp push origin --rev HEAD~2
对于需要分享整个项目历史的情况,--all-commits 标志会推送所有提交的所有实验:
$ dvc exp push origin --all-commits
这在项目收尾或迁移时很有用,但通常不推荐,因为会产生大量数据传输。
实验拉取与重现
同事分享实验后,我们用 dvc exp pull 获取:
$ dvc exp pull origin
这会从 Git 远程下载实验元数据,从 DVC 远程下载相关数据。拉取后,实验出现在本地的实验列表中:
$ dvc exp list
main:
blowy-pail
nubby-gram
要查看实验的具体内容,用 dvc exp show 或 dvc exp diff。如果想在 workspace 中实际使用实验的结果,需要 dvc exp apply:
$ dvc exp apply blowy-pail
这会将实验的代码、参数、数据和模型恢复到当前工作空间,就像实验是在本地运行的一样。之后可以基于这个结果继续开发,或者用 git commit 将其固化为常规提交。
实验的持久化
实验本质上是一种轻量级的 Git 提交,存在于特殊的引用空间中。如果想把某个实验正式纳入项目历史,可以将其转为常规分支:
$ dvc exp branch blowy-pail my-feature-branch
Git branch 'my-feature-branch' has been created from experiment 'blowy-pail'.
这会创建一个名为 my-feature-branch 的常规 Git 分支,包含实验的所有更改。之后就可以像普通分支一样推送、合并和审查。
自动推送配置
对于团队协作频繁的项目,可以配置实验自动推送。设置 exp.auto_push 选项:
$ dvc config exp.auto_push true
启用后,每次 dvc exp run 或 dvc exp save 都会自动将实验推送到远程。默认推送到 origin,可以通过 exp.git_remote 修改目标远程。
对应的,也可以用环境变量控制:
$ export DVC_EXP_AUTO_PUSH=true
$ export DVC_EXP_GIT_REMOTE=upstream
这种自动化适合 CI/CD 环境或共享服务器,确保实验结果不会丢失。
实验清理与存储管理
实验分享后,本地可能积累大量实验数据。dvc exp remove 可以清理不需要的实验:
$ dvc exp remove --rev HEAD --all
这会删除当前提交关联的所有实验,但不会影响已经推送到远程的实验。如果确定某个实验不再需要,可以从远程删除:
$ dvc exp push origin --force --no-cache blowy-pail
--force 会覆盖远程的实验,--no-cache 避免重复推送数据。实际上 DVC 没有专门的远程实验删除命令,通常通过 Git 的引用清理机制处理。
对于长期项目,建议定期清理表现不佳的实验,保持实验列表的整洁。可以基于指标阈值批量删除:
$ dvc exp show --no-pager | awk '$4 < 0.8 {print $1}' | xargs -I {} dvc exp remove {}
这条命令会删除准确率低于 0.8 的所有实验(假设准确率在第 4 列)。
总结
高级实验工作流将 DVC 从简单的版本控制工具升级为完整的实验管理平台。实验队列让批量运行变得可靠,Hydra 集成解决了复杂配置的维护难题,而推送拉取机制则打通了团队协作的最后一公里。
这些功能的核心思想是自动化和标准化。通过将重复性工作交给 DVC,我们可以专注于实验设计本身,而不是繁琐的执行管理。无论是个人研究还是团队项目,这种工业化思维都能显著提升迭代效率。
下一章将介绍 DVCLive,它能在实验运行时实时记录指标和可视化结果,与高级工作流结合,构建从执行到监控的完整闭环。