版本控制与 DVC
这一课你将能做到
- 说清 Git 与 DVC 的分工:Git 管代码和小文件,DVC 管数据/模型的存储指针
- 用 dvc.yaml + params.yaml 把「造数据 → 清洗 → 特征 → 训练」定义成一条可重放管道
- 配置 remote 存储并完成 push/pull,掌握团队协作时数据与模型文件的流转方式
为什么要学
Git 只适合管文本:仓库超过几百 MB 就开始卡,二进制 diff 没有意义。而 ML 项目最大的两块资产恰恰是数据集(GB 级)和模型文件(百 MB 级)。没有数据版本控制,你永远回答不了「线上这个模型是用哪一版数据训的」这种致命问题。
DVC(Data Version Control)的思路:数据本体放在 Git 仓库之外的存储(本地盘/S3/OSS),Git 里只提交一个几字节的 .dvc 指针文件。于是「提交代码 v3 + 数据 v7」在 Git 历史里原子地绑定在一起,checkout 任意历史提交后 dvc checkout 就能还原对应的数据。
人话:Git 像仓库管理员只登记货单(指针),DVC 像仓库货架(cache + remote),货单和货架通过 MD5 编号对应。本课把上一课的判题日志数据集和特征表全部纳入版本管理。
先建立图像
你的工作区 Git 仓库 DVC Remote
├─ src/train.py ────────────────▶ 存代码本体
├─ data/raw/judge_logs.parquet ──▶ data/raw.dvc (指针, ~100B)
│ 数据本体 ───────────────────────────────────▶ .dvc/cache (MD5 编号)
└─ models/timeout.joblib ───────▶ models.dvc (指针)
模型本体 ──────────────────────────────────▶ 远端存储 (S3/OSS/MinIO)
git checkout v1.2 → 指针文件回到 v1.2
dvc checkout → 按 v1.2 的指针从 cache/remote 还原数据与模型
跟着做
步骤 1:初始化 Git 与 DVC
pip install dvc
git init
dvc init
git commit -m "init dvc"
步骤 2:把上一课的数据纳入 DVC
沿用上一课的 src/make_dataset.py、src/clean.py、src/features.py,先生成数据,再让 DVC 接管:
python src/make_dataset.py && python src/clean.py && python src/features.py
dvc add data/raw/judge_logs.jsonl data/processed/problem_daily.parquet
git add data/raw/judge_logs.jsonl.dvc data/processed/problem_daily.parquet.dvc .gitignore
git commit -m "data: 判题日志 v1 (5000 行)"
观察两件事:原始 jsonl 被写进了 .gitignore(DVC 自动加的);.dvc 文件内容是指针:
outs:
- path: data/raw/judge_logs.jsonl
hash: md5
size: 1234567
hash: 6f1a3c.... # 数据本体的 MD5,存在 .dvc/cache 里
步骤 3:配置 remote 并推送
# 团队共享存储:S3/OSS/MinIO 均可,本地练习用目录模拟
dvc remote add -d storage /tmp/dvcstore # Windows 用 D:/dvcstore 这类绝对路径
dvc push
同事拿到你的仓库后:
git pull && dvc pull # 代码 + 数据一步到位
步骤 4:用 dvc.yaml 定义管道(本课核心)
把「命令怎么跑」从口头/文档变成代码:
# dvc.yaml
stages:
make_data:
cmd: python src/make_dataset.py
deps:
- src/make_dataset.py
outs:
- data/raw/judge_logs.jsonl
clean:
cmd: python src/clean.py
deps:
- src/make_dataset.py
- data/raw/judge_logs.jsonl
outs:
- data/processed/judge_clean.parquet
features:
cmd: python src/features.py
deps:
- src/clean.py
- data/processed/judge_clean.parquet
params:
- features.tle_window
outs:
- data/processed/problem_daily.parquet
# params.yaml
features:
tle_window: 7 # 滚动 TLE 率窗口(天)
src/features.py 相应改为读取参数(保持口径一处定义):
# src/features.py(关键改动)
import yaml
import pandas as pd
with open("params.yaml", encoding="utf-8") as f:
WINDOW = yaml.safe_load(f)["features"]["tle_window"]
df = pd.read_parquet("data/processed/judge_clean.parquet")
# ...同上一课的聚合逻辑,把硬编码的 7 全部换成 WINDOW...
先清掉手动生成的产物,改由管道统一管理:
dvc remove data/processed/problem_daily.parquet.dvc 2>$null; dvc repro
步骤 5:改参数,体验增量重算
dvc dag # 查看管道依赖图
# 把 params.yaml 的 tle_window 改成 30,然后:
dvc repro
# 输出示意:
# Stage 'features' changed → rerunning
# Data and pipelines are up to date.(make_data/clean 的缓存命中,只重跑了 features)
DVC 会对比每个 stage 的 deps/params/outs 的哈希:没变的直接用缓存,变了的重跑。这就是「数据管道的 makefile」。最后把管道定义提交:
git add dvc.yaml params.yaml data && git commit -m "pipeline: 特征窗口参数化"
讲清楚要点
- DVC 的缓存是内容寻址的:文件按 MD5 存进
.dvc/cache,同一份内容全仓库只存一份。改一个字节,哈希就变,所以「版本」天然精确到字节。 - .dvc 指针文件才是 Git 的主角:永远
git add xxx.dvc而不是数据本体。反过来,DVC 自动把大文件写进 .gitignore 防止你手滑。 - dvc.yaml 让管道可执行、可增量:
dvc repro依据哈希对比决定重跑范围;改了窗口参数,只会重算下游 stage。 - 模型文件同样用 DVC 管:
dvc add models/timeout-v1.joblib。DVC 管「训练产出的物理文件」,MLflow 管「实验元数据」——第 5 课两者会打通。 - 团队协作流程固定为三步:拉代码(git pull)→ 拉数据(dvc pull)→ 复现管道(dvc repro)。谁改了数据或参数,Git 历史里一目了然。
你可能会懵的地方
- DVC 和 Git LFS 有什么区别? Git LFS 把大文件塞进 Git 服务器生态,历史里每个版本都要占远端空间,仓库越拉越大;DVC 的 cache 按内容寻址去重,且能接任意对象存储(S3/OSS/SSH/HTTP),与 Git 仓库解耦。大数据集、模型文件场景 DVC 更合适。
- dvc repro 什么时候会重跑一个 stage?
它声明的
cmd、任何deps文件、params参数、outs产物哈希有任一变化就重跑;全部一致则直接复用缓存。所以把输入写全(包括依赖的 .py 文件)是管道正确的前提。 - 数据和模型都该用 DVC,那 MLflow 还要吗? 要,分工不同。DVC 回答「这份模型文件怎么来的」(文件级血缘),MLflow 回答「哪个实验/哪组参数/哪个指标最优」(实验元数据与注册中心)。生产上常见组合:MLflow Registry 登记逻辑版本,artifact 指向 DVC/S3。
- 缓存误删了怎么办?
dvc fetch从 remote 把哈希对应的文件拉回本地 cache。这也是 remote 除了团队共享外的第二个作用——本地灾难恢复。 - 为什么不用 cron 直接跑 make/clean/features? cron 不知道依赖关系,任何一环失败都会产出半成品;dvc repro 保证顺序、增量与一致性。调度(什么时候跑)是第 6 课编排工具的职责,两者互补。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
git add data/train.csv 把大文件进 Git | 仓库膨胀、clone 卡死 | 只提交 .dvc 指针,数据走 dvc push |
| 手工编辑 .dvc 文件里的 md5 | 指针与 cache 对不上,checkout 失败 | 永远用 dvc add / dvc repro 生成 |
| 超参硬编码在 train.py 里 | 改参数触发全量重算,且历史不可查 | 写进 params.yaml,dvc repro 增量感知 |
| dvc push 之前换电脑 | 数据只存在本机,团队拿不到 | remote 配好后每次 repro 完即 push |
| 每次全量重新生成所有 stage | 白白浪费几十分钟 | 依赖声明写全,让缓存命中 |
卡点急救
- dvc add 提示文件已被 Git 追踪:先
git rm --cached 文件名,再重新dvc add。 - dvc repro 报 stage 的依赖未声明:报错里会指出哪个文件变了但不在 deps 里;把它补进对应 stage 的 deps 列表。
- dvc pull 找不到 remote:
dvc remote list检查配置;本地路径 remote 在 Windows 上要用盘符绝对路径,团队协作建议直接上 S3/OSS。
面试常问
- DVC 的版本控制原理? 内容寻址:数据本体按 MD5 存入本地 cache,Git 提交的 .dvc 指针记录哈希与大小;remote 存全量哈希对象。checkout 历史指针 + dvc checkout 即可精确还原任意版本,同一内容自动去重。
- ML 项目如何做到「一键复现三个月前的结果」? Git 锁代码与 dvc.yaml/params.yaml,DVC 锁数据版本,训练记录里保存 run_id 与数据哈希的关联(MLflow tag);三者取同一历史提交,dvc repro 或重放记录的命令即可复现。
- dvc.yaml 管道和 Airflow DAG 的边界? dvc.yaml 描述「数据变换的依赖与增量」(可重放性),适合离线特征与训练产物;Airflow/Prefect 描述「任务何时跑、失败怎么救」(调度与运维),负责把 dvc repro 作为其中一个任务定时触发。
自测三问
- Git 仓库里关于数据的那 100 字节 .dvc 文件包含什么信息?
- dvc repro 判断一个 stage 是否重跑,对比哪些东西的哈希?
- DVC 与 MLflow 的职责如何划分?各自回答什么问题?
小结一句话
DVC 用「Git 管指针 + 内容寻址缓存 + remote 存储」给数据和模型补上版本控制,并用 dvc.yaml 把数据管道变成可增量重放的代码。
延伸
- DVC 官方文档:Get Started 与 User Guide——本课所有命令的权威出处,重点读 dvc.yaml 与 remote 两章。
- 下一课:训练与实验跟踪——MLflow 怎么把每次训练的超参、指标、模型文件自动记录下来。