7.流水线构建与执行
在机器学习项目中,代码只是整个故事的一部分。真正让项目运转起来的是数据流转的过程:原始数据经过清洗、特征工程、模型训练,最终产出结果。这个过程就像一条生产线,每个环节都依赖前一个环节的输出。DVC 的流水线功能就是为这条生产线设计的,它让我们能够用代码的方式定义、执行和复现整个工作流程。
Stage 添加与修改
构建流水线的第一步是定义 Stage。每个 Stage 代表一个独立的处理步骤,通常是一个脚本或命令,加上它的输入依赖和输出文件。DVC 提供了两种方式来管理 Stage:命令行工具和直接编辑配置文件。
使用 dvc stage add 创建 Stage
dvc stage add 是最直接的创建方式。它把命令、依赖和输出打包成一个 Stage,写入 dvc.yaml 文件。看个简单的例子:假设我们有一个文本文件,想统计其中的数字个数。
$ dvc stage add -n filter \
-d text.txt \
-o numbers.txt \
"cat text.txt | egrep '[0-9]+' > numbers.txt"
这条命令创建了一个名为 filter 的 Stage。-n 指定名称,-d 指定依赖文件,-o 指定输出文件,最后是要执行的命令。执行后,dvc.yaml 文件中会多出这样的定义:
stages:
filter:
cmd: cat text.txt | egrep '[0-9]+' > numbers.txt
deps:
- text.txt
outs:
- numbers.txt
创建第二个 Stage 来处理过滤后的数字:
$ dvc stage add -n count \
-d numbers.txt \
-d process.py \
-M count.txt \
"python process.py numbers.txt > count.txt"
这里用 -M 标记 count.txt 为指标文件,这类文件通常很小,可以直接用 Git 追踪,不需要 DVC 缓存。现在 dvc.yaml 包含两个 Stage,DVC 会自动识别它们之间的依赖关系——count 依赖 filter 的输出 numbers.txt。
手动编辑 dvc.yaml
虽然 dvc stage add 很方便,但直接编辑 dvc.yaml 更灵活。YAML 格式清晰易读,支持版本控制,也便于代码审查。上面的例子手动写成:
stages:
filter:
cmd: cat text.txt | egrep '[0-9]+' > numbers.txt
deps:
- text.txt
outs:
- numbers.txt
count:
cmd: python process.py numbers.txt > count.txt
deps:
- numbers.txt
- process.py
outs:
- count.txt
手动编辑的优势在于可以使用一些高级功能,比如模板化配置,这是命令行工具不支持的。团队协同时,直接在 YAML 文件中修改也更容易追踪变更。
修改现有 Stage
项目迭代过程中,Stage 的定义经常需要调整。比如修改训练脚本的学习率参数,或者增加新的数据依赖。这时可以用 -f 强制覆盖已有 Stage:
$ dvc stage add -n train \
-f \
-d train_model.py \
-d matrix-train.p \
-o model.p \
python train_model.py 18494003 model.p
-f 会完全替换同名的 Stage 定义。如果只想添加依赖或输出,而不想重新执行 Stage,可以直接编辑 dvc.yaml,然后运行 dvc commit 更新状态。假设 prepare Stage 已经执行过,后来发现漏掉了 data/raw.csv 依赖和 data/validate 输出:
stages:
prepare:
cmd: python src/prepare.py
deps:
- src/prepare.py
- data/raw.csv # 新增的依赖
outs:
- data/train
- data/validate # 新增的输出
如果 data/raw.csv 和 data/validate 已经存在于工作区,运行 dvc commit 就能把这些新指定的输出加入缓存,同时更新 dvc.lock 中的哈希值,无需重新执行耗时的 prepare 阶段。
流水线执行 repro
定义好 Stage 后,下一步是让流水线运转起来。dvc repro 是核心命令,它分析依赖图,智能决定哪些 Stage 需要执行,然后按正确顺序运行它们。
基本执行流程
执行 dvc repro 时,DVC 会读取所有 dvc.yaml 文件,构建完整的依赖图(DAG)。然后检查每个 Stage 的状态:
- 如果依赖文件的内容哈希值发生变化,该 Stage 需要重新执行
- 如果输出文件缺失,该 Stage 需要重新执行
- 如果依赖和输出都没变化,跳过该 Stage
看个完整的例子。假设我们已经定义了 filter 和 count 两个 Stage,第一次执行:
$ dvc repro
Running stage 'filter':
> cat text.txt | egrep '[0-9]+' > numbers.txt
Generating lock file 'dvc.lock'
Updating lock file 'dvc.lock'
Running stage 'count':
> python process.py numbers.txt > count.txt
Updating lock file 'dvc.lock'
Use `dvc push` to send your updates to remote storage.
DVC 先执行 filter,生成 numbers.txt,然后执行 count,生成最终结果。同时创建 dvc.lock 文件记录当前状态。这个文件类似 Git 的提交记录,保存了每个 Stage 的依赖和输出的哈希值。
增量执行与缓存
DVC 的强大之处在于能跳过未变化的 Stage。修改 process.py 文件,增加一行打印:
import sys
num_lines = 0
with open(sys.argv[1], 'r') as f:
for line in f:
num_lines += 1
print('Number of lines:') # 新增的行
print(num_lines)
再次运行 dvc repro:
$ dvc repro
Stage 'filter' didn't change, skipping
Running stage 'count' with command:
python process.py numbers.txt > count.txt
Updating lock file 'dvc.lock'
DVC 检测到 filter 的依赖没有变化,直接跳过,只执行受影响的 count Stage。这种增量执行在大规模流水线中节省大量时间。
常用执行选项
dvc repro 提供了丰富的选项控制执行行为:
--dry:只打印要执行的命令,不实际运行。适合检查流水线状态--force:强制重新执行所有 Stage,忽略缓存--single-item:只执行指定的 Stage,不递归处理依赖--downstream:从指定 Stage 开始,向后执行下游 Stage--interactive:每个 Stage 执行前询问确认
比如,只想执行 count Stage,忽略上游变化:
$ dvc repro --single-item count
如果只想执行 filter 之后的 Stage(即 count):
$ dvc repro --downstream filter
这在调试时很有用,可以快速验证下游 Stage 的逻辑。
处理执行失败
流水线执行中难免遇到错误。默认情况下,一个 Stage 失败会终止整个流程。但有时我们希望继续执行其他独立分支。--keep-going 选项让 DVC 跳过依赖失败 Stage 的其他 Stage,继续执行剩余任务。
$ dvc repro --keep-going
--ignore-errors 更激进,它会忽略所有错误,执行所有能执行的 Stage。这在某些容错场景下有用,但通常不推荐。
缓存机制加速
DVC 的缓存机制是提升效率的核心。它不仅缓存数据文件,还缓存 Stage 的执行结果,这就是运行缓存(run cache)。
运行缓存的工作原理
每次执行 Stage 时,DVC 会计算一个综合哈希值,包括:
- 依赖文件的内容哈希
- 参数值
- 执行命令
- 输出路径
这个哈希值唯一标识一次执行。如果后续执行发现相同的哈希,就直接从缓存恢复输出,不再运行命令。运行缓存存储在 .dvc/cache/runs 目录中。
看个实际例子。修改 params.yaml 中的训练参数:
train:
n_est: 100 # 从 50 改为 100
执行 dvc repro:
$ dvc repro
Stage 'prepare' didn't change, skipping
Stage 'featurize' didn't change, skipping
Running stage 'train':
> python src/train.py data/features model.pkl
Updating lock file 'dvc.lock'
训练完成后,把参数改回 50,再次执行:
$ dvc repro
Stage 'prepare' didn't change, skipping
Stage 'featurize' didn't change, skipping
这次 train Stage 也没有执行!因为 DVC 发现之前已经用相同的参数执行过,直接从运行缓存恢复了结果。这在调参时特别有用,可以避免重复训练。
控制缓存行为
有时我们不希望使用缓存,比如调试时需要重新运行。--no-run-cache 选项禁用运行缓存:
$ dvc repro --no-run-cache
另一个常见场景是实验性执行,不想污染缓存。--no-commit 选项执行 Stage 但不把输出存入缓存:
$ dvc repro --no-commit
执行后,dvc status 会显示输出文件不在缓存中:
$ dvc status
train:
changed outs:
not in cache: model.pkl
确认结果满意后,用 dvc commit 手动提交到缓存:
$ dvc commit
$ dvc status
Data and pipelines are up to date.
这种方式适合探索性工作,避免生成大量无用的缓存数据。
缓存与 Git 的协同
运行缓存与 Git 提交是独立的。即使切换 Git 分支,运行缓存仍然可用。这意味着在不同实验分支间切换时,如果依赖和参数相同,DVC 可以直接复用缓存的结果,无需重新计算。这种设计让实验管理更加高效。
外部依赖处理
实际项目中,数据不一定都在本地工作区。可能要从外部 URL 下载数据,或者把结果直接写入云存储。DVC 支持外部依赖和输出,让流水线能处理更复杂的场景。
定义外部依赖
外部依赖可以是 HTTP URL、云存储路径等。DVC 会监控这些外部资源的变化。例如,从网络下载数据:
stages:
download:
cmd: wget https://example.com/data.zip -O data.zip
deps:
- https://example.com/data.zip
outs:
- data.zip
每次执行时,DVC 会检查 URL 的内容是否变化(通过 ETag 或 Last-Modified 头)。如果远程文件更新,Stage 会重新执行。
外部输出与缓存控制
有时 Stage 的输出需要直接写入远程存储,不需要本地缓存。比如把处理结果直接上传到 S3:
stages:
prepare:
cmd: python sm_prepare.py --bucket mybucket --prefix project-data
deps:
- sm_prepare.py
- https://sagemaker-sample-data.s3.amazonaws.com/data.zip
outs:
- s3://mybucket/project-data/input_data:
cache: false
cache: false 告诉 DVC 不要缓存这个输出。这在处理大规模数据时很有用,避免本地存储成为瓶颈。需要注意的是,标记为 cache: false 的 Stage 会禁用运行缓存,因为 DVC 无法追踪外部存储的内容变化。
外部依赖的注意事项
使用外部依赖时,要确保命令能正确处理网络异常和认证。DVC 不会自动重试失败的下载,这需要在脚本中实现。另外,外部资源的可用性直接影响流水线的可复现性。如果远程数据被删除或修改,历史版本可能无法重现。因此,对于关键数据,建议用 dvc import-url 导入到 DVC 控制下,而不是直接作为外部依赖。
并行执行控制
DVC 的流水线基于 DAG,天然支持并行执行。只要 Stage 之间没有依赖关系,就可以同时运行。
手动并行执行
假设流水线有两个独立分支:
$ dvc dag
+--------+ +--------+
| A1 | | B1 |
+--------+ +--------+
* *
* *
* *
+--------+ +--------+
| A2 | | B2 |
+--------+ +--------+
* *
** **
* *
+------------+
| train |
+------------+
A 和 B 两个分支相互独立。可以在两个终端分别执行:
# 终端 1
$ dvc repro A2
# 终端 2
$ dvc repro B2
DVC 会锁定正在执行的 Stage,防止冲突。两个分支执行完成后,再运行最终的 train Stage:
$ dvc repro train
DVC 检测到 A2 和 B2 都已是最新状态,只执行 train。
并行执行的注意事项
并行执行时,要确保 Stage 之间确实没有依赖冲突。如果两个 Stage 写入同一个输出文件,会导致不确定的结果。DVC 的依赖图检查能发现一些明显的冲突,但无法检测逻辑上的竞争条件。
另外,并行执行会增加系统负载。对于计算密集型 Stage,要控制并行数量,避免资源耗尽。可以通过 --jobs 选项限制同时运行的任务数。
自动化并行
对于大规模参数搜索,DVC 提供了实验队列功能。可以把多个参数组合加入队列,然后并行执行:
$ dvc exp run --queue --set-param "train.batch_size=8,16,24"
这会创建三个实验任务。然后用 dvc queue start --jobs 3 启动三个并行 worker 执行。这个功能在超参数调优时特别有用,能充分利用多核 CPU 或集群资源。
总结
流水线构建与执行是 DVC 的核心能力。通过 dvc stage add 或手动编辑 dvc.yaml,我们把数据处理步骤定义为可管理的 Stage。dvc repro 智能地执行流水线,利用缓存机制避免重复工作。外部依赖和并行执行的支持,让流水线能应对真实世界的复杂场景。
掌握这些基础后,下一章将深入参数与指标管理。参数化让流水线更灵活,指标追踪让实验效果可量化。结合本章的构建执行能力,就能搭建完整的机器学习实验框架,实现真正的可复现研究。