3. 版本控制基础与医学影像模型

医学影像模型的一个版本,从来不只是“一个 .pt 或 .onnx 文件”。它至少由四类对象共同定义:模型权重(几十 MB 到数 GB 的二进制张量)、训练与推理配置(YAML/JSON、预处理参数、窗宽窗位、体素间距、类别映射)、数据版本(DICOM/NIfTI 数据集、标注、划分清单)以及代码(网络结构、损失、后处理)。Git 擅长管理文本与代码,却对二进制大文件天然不友好;DVC 与 Git LFS 正是为弥补这一缺口而出现。理解三者的分工,是医学影像模型可复现发布的前提。

一、三种工具的本质差异

Git 是分布式内容寻址存储:每个对象按 SHA-1(新版本为 SHA-256)哈希存储,仓库中保存完整历史。它对文本做增量 diff,但对二进制文件每次修改都保存整份副本,导致仓库迅速膨胀。一个 200 MB 的模型权重改 10 次,仓库就多出约 2 GB,且 clone 时全量下载,这是 Git 不适合直接存权重的根本原因。

Git LFS(Large File Storage)采用“指针替换”策略:仓库中只保存一个约 130 字节的文本指针文件,真实内容存放在远端 LFS 服务器。clone 时按需拉取。它保留了 Git 的分支、标签、提交语义,适合“文件数量少、变更不频繁、需要与代码同版本”的场景,例如模型权重、少量固定测试影像。

DVC(Data Version Control)走得更远:它把大文件与目录的哈希写入 .dvc 元文件,真实数据放在本地缓存或远端存储(S3、MinIO、NAS、SSH)。DVC 的核心价值在于数据流水线——用 dvc.yaml 描述 stage 的依赖与输出,DVC 据此判断哪些阶段需要重跑,实现“数据变了才重训”。它更适合数据集、标注、中间特征这类体量大、目录结构复杂、需要追踪血缘的对象。

二、适用性对照

维度GitGit LFSDVC
存储对象代码、配置、小文本少量大二进制文件数据集、目录、权重、中间产物
仓库体积小小(仅指针)小(仅 .dvc 元文件)
版本粒度文件级文件级文件/目录级 + 流水线级
血缘追踪无无有(dvc.lock 记录依赖哈希)
典型医学影像用途训练脚本、配置、类别映射发布用模型权重、固定测试集DICOM/NIfTI 数据集、标注、特征

实践中的常见组合是:代码与配置用 Git,数据集与中间产物用 DVC,发布权重用 Git LFS 或 DVC。三者并不互斥,而是分层协作。

三、一个可落地的混合工作流

假设一个肺结节分割项目,目录结构如下:

lung-nodule/
├── src/                 # 网络、损失、后处理(Git)
├── configs/
│   └── seg_v2.yaml      # 预处理与训练配置(Git)
├── data/
│   └── train.dvc        # 指向 DICOM 数据集(DVC)
├── models/
│   └── seg_v2.pt        # 发布权重(Git LFS)
└── dvc.yaml             # 流水线定义

初始化与接入:

git init && git lfs install
git lfs track "models/*.pt"
dvc init
dvc remote add -d storage ssh://nas/medimg-dvc
dvc add data/train
git add .gitattributes data/train.dvc models/seg_v2.pt
git commit -m "feat: seg_v2 权重与数据集版本"
dvc push

其中 dvc add data/train 会生成 data/train.dvc,内容形如:

outs:
- md5: 8f3c1a9e2b7d4c05e6f1a2b3c4d5e6f7.dir
  size: 48213452800
  nfiles: 12640
  path: train

这个 md5 是目录内容的整体哈希。只要数据集任何一个文件变化,哈希就变,DVC 便能识别“数据版本已更新”。

四、流水线与可复现性

dvc.yaml 把“数据→预处理→训练→评估”串成有向无环图:

stages:
  preprocess:
    cmd: python src/preprocess.py --in data/train --out data/processed
    deps: [src/preprocess.py, data/train]
    outs: [data/processed]
  train:
    cmd: python src/train.py --cfg configs/seg_v2.yaml
    deps: [src/train.py, configs/seg_v2.yaml, data/processed]
    outs: [models/seg_v2.pt]
    metrics: [metrics.json]

执行 dvc repro 时,DVC 比对 deps 与 outs 的哈希:若 configs/seg_v2.yaml 改了学习率,train 阶段重跑;若只改了评估脚本,preprocess 与 train 均跳过。dvc.lock 记录每次运行的精确哈希,使“某次发布的权重对应哪份数据、哪份配置”可被精确回溯。

Git / DVC / Git LFS 分层协作与版本血缘 Git 仓库(文本层) src/*.py 训练与后处理代码 configs/seg_v2.yaml 配置 .dvc 元文件 / dvc.lock DVC(数据与流水线层) data/train 数据集哈希 data/processed 中间产物 dvc.yaml 阶段依赖图 Git LFS(发布权重层) models/seg_v2.pt 指针 LFS 远端对象存储 tag: v2.1.0 发布锚点 引用 发布 dvc repro 流水线:哈希比对决定重跑范围 preprocess data/train → processed train cfg + processed → .pt evaluate .pt → metrics.json release tag + push 哈希一致 → 跳过该 stage;任一 dep 变化 → 该 stage 及其下游重跑,dvc.lock 记录新哈希,保证权重可回溯到数据与配置。

五、选择判断与常见陷阱

判断用哪个工具,可先问三个问题:对象是文本还是二进制?变更频率高不高?是否需要记录“数据→模型”的血缘?文本、低频、无需血缘,用 Git;二进制、低频、需与代码同版本,用 Git LFS;体量大、目录复杂、需要流水线复现,用 DVC。

常见陷阱包括:把 DICOM 直接 git add,仓库数月内膨胀到数十 GB;git lfs track 写在提交之后,历史大文件仍留在 Git 对象库,需用 git lfs migrate import 重写历史;DVC 只 dvc add 却忘记 dvc push,换机器后 dvc pull 失败;以及把患者隐私数据推到公共远端。医学影像场景下,远端存储应启用访问控制与加密,DICOM 中的 PHI 需先脱敏再入库。

发布一个模型版本时,建议同时打 Git tag(如 v2.1.0)并记录三样东西:commit SHA、DVC 数据哈希、LFS 权重对象 ID。三者齐全,任何一次发布都能被精确重建。

把版本控制做对,模型发布才不是“把文件拷到服务器”。Git 管代码与配置,DVC 管数据与流水线,Git LFS 管发布权重,三者各司其职,共同构成医学影像模型可追溯、可复现的版本基线。