11. 高级实验工作流

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,它能在实验运行时实时记录指标和可视化结果,与高级工作流结合,构建从执行到监控的完整闭环。