4. 版本控制策略

医学影像实验的可复现性,往往不是败在算法本身,而是败在“代码、数据、模型权重三者版本对不上”。一个典型的翻车场景是:三个月前跑出 Dice 0.89 的实验,今天想复现,代码仓库已经合并了十几个提交,训练集里补了 40 例新标注,而当时保存的权重文件被后来的一次覆盖操作冲掉了。要避免这种情况,必须把三类资产分开管理、统一锚定。

三类资产的版本控制分工

核心原则是:Git 管“轻”的,DVC/Git-LFS 管“重”的,用指针把两者锁在一起。

  • Git 管理:训练/推理代码、配置文件(YAML/JSON)、环境依赖(requirements.txt、conda env 导出)、数据划分清单(split 的 CSV/JSON)、评估脚本、实验记录文档。这些是文本,体积小、diff 可读、合并友好。
  • DVC 或 Git-LFS 管理:DICOM/NIfTI 原始影像、预处理后的张量缓存、标注掩膜、模型权重(.pth/.ckpt/.onnx)、可视化输出。这些是二进制大文件,Git 无法有效 diff,直接提交会让仓库膨胀到无法克隆。

两者的关键差别在于:Git-LFS 是“把大文件替换成指针,文件本身存在 LFS 服务器”,它擅长单文件级别的版本追踪;DVC 则在文件之上增加了数据流水线(pipeline)与依赖关系,能记录“这份预处理数据是由哪个脚本、哪份原始数据、哪组参数生成的”,更适合需要追溯数据血缘的影像实验。实践中常见组合是:代码与配置用 Git,数据与权重用 DVC,DVC 的 .dvc 指针文件随 Git 一起提交。

代码 · 数据 · 模型 的版本锚定关系 Git 仓库(轻量文本) train.py / eval.py config.yaml split_v3.csv requirements.txt data/raw.dvc md5: 9f2c… ← 数据指针 DVC / Git-LFS 存储(大体积) DICOM 原始影像 标注掩膜 预处理缓存 模型权重 .pth 远程存储 remote S3 / NAS / 对象存储 指针引用 一次实验提交 = 一个 Git commit 同时锚定 commit a3f9c1 → code@a3f9c1 + data/raw.dvc@a3f9c1 + model/best.pth.dvc@a3f9c1 三者哈希一致,才能精确回溯到“当时那一版” 分支策略:main 稳定 / exp/* 并行实验 main v1.0 v1.1 v1.2 release exp/unet-aug exp/resnet-focal

用 DVC 建立可追溯的数据流水线

假设项目目录为 proj/,原始数据放在 data/raw/。初始化与追踪的基本操作如下:

cd proj
git init
dvc init
dvc remote add -d storage /mnt/nas/dvc-store   # 或 s3://bucket/dvc-store

# 追踪原始影像目录(生成 data/raw.dvc 指针文件)
dvc add data/raw

# 把指针文件纳入 Git
git add data/raw.dvc .gitignore
git commit -m "track raw DICOM data"

# 推送真实数据到远程存储
dvc push

关键在于:data/raw.dvc 是一个几十字节的文本,里面记录了 md5 与 size。别人克隆仓库后执行 dvc pull,DVC 会按 md5 从远程存储拉取对应版本的数据。这样,Git 提交历史与数据版本就通过指针文件一一对应了。

更进一步,用 dvc stage 定义预处理与训练阶段,让数据血缘自动记录:

dvc stage add -n preprocess \
  -d src/preprocess.py -d data/raw \
  -p preprocess.spacing,preprocess.window \
  -o data/processed \
  python src/preprocess.py

dvc stage add -n train \
  -d src/train.py -d data/processed \
  -p train.lr,train.epochs \
  -o model/best.pth \
  python src/train.py

dvc repro        # 依赖或参数变化时自动重跑
dvc dag          # 查看流水线依赖图

执行 dvc repro 时,DVC 会比对每个阶段的依赖哈希与参数哈希,只有变化的阶段才重跑。这意味着:改了 train.lr,预处理不会重跑,只重跑训练;换了原始数据,则预处理和训练都会重跑。这既节省算力,又保证结果与输入严格对应。

分支策略:让并行实验互不干扰

推荐采用“主干稳定 + 实验分支”的结构:

  • main:只保留经过验证、可复现的稳定版本,每次合并打 tag(如 v1.2)。
  • exp/<主题>:每个实验一个分支,例如 exp/unet-aug、exp/resnet-focal。分支名直接体现变量,便于回溯。
  • fix/<问题>:修复数据泄漏、标签错位等缺陷时使用。

实验分支上,代码改动走 Git 提交,数据与权重改动走 dvc add + git commit。由于 DVC 指针文件也在 Git 里,切换分支时执行 dvc checkout,工作区的数据与权重会自动切换到该分支对应的版本——这正是“代码与数据同步切换”的关键动作。

回溯一次历史实验的完整动作:
1. git checkout v1.1 —— 切到目标版本;
2. dvc checkout —— 按指针恢复当时的数据与权重;
3. dvc repro —— 若需重跑,按记录的参数与依赖复现;
4. 对比 metrics.json 中的 Dice/HD95,确认与记录一致。

实践中的几个判断点

选 DVC 还是 Git-LFS?如果只是想把大文件存起来、按版本取回,Git-LFS 更轻;如果需要记录“数据从哪来、经过什么处理、依赖哪些参数”,DVC 更合适。影像实验通常涉及多阶段预处理,DVC 的流水线能力价值更高。

什么不该进版本控制?中间缓存、日志、TensorBoard 事件文件、临时可视化图,应写入 .gitignore 与 .dvcignore,只保留可复现所需的最小集合。权重文件若每轮都存,建议只追踪最优与最终两个,其余按需清理。

常见陷阱:只提交了代码却忘了 dvc push,别人 dvc pull 时会因远程缺少对应 md5 而失败;或在实验分支上直接 dvc add 后未提交指针文件,导致数据版本游离于 Git 历史之外。每次实验结束前,用 git status 与 dvc status 双重确认,是成本最低的防护。

把这三类资产用“Git 提交 + DVC 指针”统一锚定后,任何一个历史结果都能被精确重建:代码回到那个 commit,数据与权重回到那个 md5,参数回到那份 config。版本控制在这里不只是备份手段,而是医学影像实验可复现性的基础设施。