3. 版本控制:Git与DVC基础、数据版本管理、模型版本管理

聊到训练流水线,版本控制是绕不开的第一道坎。很多人觉得「不就是 Git 吗?我天天用」。嗯,但训练项目跟普通软件项目还真不一样——你想想看,一个模型动辄几百 MB,数据集甚至上 TB,Git 根本管不了。所以这一章,我们得把 Git 和 DVC 怎么配合讲清楚。

3.1 为什么 Git 不够用?

Git 设计出来是管代码的,它擅长处理文本文件。但我们的训练项目里,真正值钱的东西往往是这三样:代码、数据、模型。代码用 Git 没问题,数据和模型呢?

我在项目中遇到过最典型的场景:同事把 2GB 的 CSV 文件直接 push 到仓库,结果整个团队 clone 一次要等半小时。更麻烦的是,Git 对大文件的 diff 完全无能为力——你根本看不出两个版本的 CSV 到底改了哪几行。

核心矛盾:Git 管理代码的「文本差异」能力很强,但数据和模型是「二进制大文件」,Git 既存不下也 diff 不了。

所以我们需要一个专门管大文件的工具,跟 Git 配合使用。这就是 DVC(Data Version Control)出场的原因。

3.2 Git + DVC 的分工体系

说白了,DVC 不是要替代 Git,而是给 Git 打工。它的核心思路是:

  • Git 管代码和元数据:Python 脚本、配置文件、DVC 的 .dvc 文件(这些都很小)
  • DVC 管数据和模型:训练集、测试集、模型权重文件(这些都很大)

DVC 会在你的仓库里生成一些 .dvc 文件,这些文件只有几 KB,里面存的是大文件的哈希值和存储位置。Git 只跟踪这些 .dvc 文件,真正的数据文件则被 DVC 推送到远程存储(比如 S3、GCS、NAS)。

我习惯把这种模式叫做「指针文件 + 对象存储」——.dvc 文件就是指针,指向远程存储里的具体对象。

Git + DVC 协作架构 Git 仓库(代码 + 元数据) train.py, config.yaml data.dvc, model.dvc (文本文件,几 KB) DVC 缓存(本地) .dvc/cache/ 按哈希值存储的文件 (二进制大文件) 远程存储 S3 / GCS / NAS / SSH .dvc 指针文件 dvc push / dvc pull Git 只跟踪 .dvc 文件(几 KB),真正的数据文件由 DVC 管理

3.3 DVC 基础操作

安装 DVC 很简单,一行命令搞定:

pip install dvc

然后初始化:

cd your-project
git init
dvc init

注意顺序——先有 Git 仓库,再在仓库里初始化 DVC。DVC 会在 .dvc/ 目录下创建自己的配置和缓存。

3.3.1 跟踪数据文件

假设你有一个 data/ 目录,里面是训练集:

dvc add data/train.csv

这条命令做了三件事:

  1. 把 data/train.csv 复制到 DVC 缓存(.dvc/cache/)
  2. 生成 data/train.csv.dvc 指针文件
  3. 把 data/train.csv 加入 .gitignore(防止 Git 误跟踪)

然后你只需要把 .dvc 文件提交到 Git:

git add data/train.csv.dvc .gitignore
git commit -m "add training data"

我的习惯:每次 dvc add 之后立刻 git commit,保持 .dvc 文件与代码版本一一对应。这样回溯到某个 Git commit 时,对应的数据版本也自动确定了。

3.3.2 配置远程存储

本地缓存只是临时中转,真正的数据要推到远程。我常用 S3 兼容的对象存储:

dvc remote add -d myremote s3://my-bucket/dvc-store
dvc push

其他团队成员 clone 仓库后,只需要:

dvc pull

就能把当前版本对应的数据文件拉下来。整个过程对开发者来说几乎是透明的——你只看到 .dvc 文件变了,但背后 DVC 帮你搞定了大文件的版本切换。

3.4 数据版本管理实战

数据版本管理最核心的场景是:你改了数据,想回退到旧版本。

举个例子。你训练了一个模型,效果不错。后来同事更新了数据,重新训练后效果反而变差了。你想看看是不是数据出了问题。

用 Git 查看历史:

git log --oneline
# a1b2c3d add new training data
# e4f5g6h initial data version

切回旧版本:

git checkout e4f5g6h
dvc checkout

dvc checkout 会根据当前 .dvc 文件里的哈希值,从缓存里恢复对应的数据文件。整个过程不需要重新下载——只要缓存里还有旧版本的数据。

注意:dvc checkout 只恢复 DVC 跟踪的文件,不会影响 Git 管理的代码。如果你同时需要旧代码和旧数据,先 git checkout 再 dvc checkout。

3.4.1 数据集的增量更新

我在项目中遇到过另一个常见问题:数据集很大,每次只新增了几个样本,但 dvc add 会把整个目录重新哈希一遍,很慢。

DVC 2.0 之后引入了 dvc commit 模式:

# 先修改数据文件
# 然后告诉 DVC 文件变了
dvc status  # 会显示 data/ 有变更
dvc commit data/  # 只把变更的文件加入缓存

这样避免了全量重新哈希,对大数据集友好很多。

3.5 模型版本管理

模型文件本质上也是大文件,管理方式跟数据一样。但我建议把模型和数据分开跟踪,因为它们的更新频率和生命周期不同。

我习惯在项目里建一个 models/ 目录:

dvc add models/model_v1.pkl
git add models/model_v1.pkl.dvc
git commit -m "model v1: ResNet50, 85.2% accuracy"

这样每个模型版本都有一个对应的 Git commit,commit message 里可以写清楚模型的关键信息(架构、精度、训练数据版本)。

3.5.1 模型注册表思路

当模型版本多起来之后,光靠 Git log 查找会变得很痛苦。我建议用 models/registry.yaml 来维护一个简单的模型注册表:

models:
  - name: resnet50_v1
    path: models/resnet50_v1.pkl
    accuracy: 0.852
    data_version: e4f5g6h
    trained_by: "张三"
    date: "2024-03-15"

  - name: resnet50_v2
    path: models/resnet50_v2.pkl
    accuracy: 0.873
    data_version: a1b2c3d
    trained_by: "李四"
    date: "2024-03-20"

这个 registry.yaml 本身由 Git 管理,每次训练完模型后更新它。这样你只需要看这个文件,就能知道所有模型版本的来龙去脉。

最佳实践:模型文件名里不要只写 v1、v2,建议包含关键指标,比如 resnet50_acc87.pkl。这样即使脱离 registry,光看文件名也能知道大概。

3.6 常见问题与避坑

最后分享几个我踩过的坑:

问题 原因 解决方案
dvc push 很慢 网络带宽不足,或远程存储距离远 使用 dvc push --jobs 4 并行上传;或配置就近的远程存储
缓存占用磁盘太大 .dvc/cache 里积累了所有历史版本 定期 dvc gc 清理无用缓存;或设置缓存大小上限
多人同时修改数据 DVC 没有锁机制,可能冲突 约定数据修改前先 git pull + dvc pull,改完立即 push
.dvc 文件冲突 两个分支都改了同一个 .dvc 文件 手动合并 .dvc 文件(本质是 YAML,可读性还行)

我曾经有一次因为没及时 dvc gc,缓存撑爆了服务器磁盘,导致整个团队的 CI 全部挂掉。嗯,从那以后我养成了每周跑一次 dvc gc 的习惯。

小技巧:在 .dvc/config 里可以设置 cache.type = symlink,这样工作目录里的文件是符号链接,不占用额外磁盘空间。但要注意,Windows 上 symlink 需要管理员权限。

版本控制这件事,说白了就是给项目的每个关键状态拍一张快照。Git 拍代码的快照,DVC 拍数据和模型的快照。两者配合好了,你就能随时回到任何一个历史状态,复现当时的实验结果。这是训练流水线自动化的基石,也是后续所有自动化流程的前提。