从零搭建可复现的AI实验仓库:目录、环境与追踪规范 harveyai / harvey-labs 这个名字第一眼看上去像一个开源账号下的实验室仓库。在 GitHub 上这类命名很常见组织或用户名harveyai加上实验室后缀harvey-labs用来集中存放 AI 实验、原型代码和工具脚本。真正的问题不是怎么命名而是这类仓库经常在实验一多以后变得不可维护notebook 散落、环境时好时坏、模型文件找不到、旧实验无法复现。这篇文章以构建一个 harvey-labs 式 AI 实验仓库为主线从目录结构、环境管理、训练脚本、结果记录、依赖锁死到常见排错给出一套可以照着搭的工程规范。适合算法工程师、AI 方向学生和开源项目维护者阅读。学完之后你能把一个新 idea 快速变成目录清晰、可复现、可追溯的实验室项目而不是一整台机器上的“代码废墟”。1. 先理解实验室仓库为什么比脚本堆积更值得搭建1.1 一个实验代码散落时会出现什么问题很多 AI 实验是从 notebook 或单文件脚本开始的。初期很顺畅数据文件放一目录模型代码写在一个train.py里改个参数直接改全局变量。但实验一旦变多问题会按照相似路径出现。第一个问题是路径混乱。有人把数据放在./data/有人在代码里写成D:/experiment/data/raw/train.csv。换一台机器或者换一个同事协作路径立刻失效。第二个问题是环境漂移。两周前训练用的依赖版本没有记录今天重新安装的 PyTorch 版本变了同样的模型和随机种子可能得到完全不同的结果。第三个问题是实验记录缺失。跑完一个模型只留下一个.pth文件没有指标、没有参数、没有数据版本后续想比较效果只能重新训练。这些问题叠加起来会造成很典型的后果实验无法复现论文或项目汇报时拿不出清晰的训练配置模型文件越来越多不知道哪个是“最终版本”旧代码需要复用时要花大量时间把环境找回来。harvey-labs 这类仓库的设计初衷就是通过目录和规范把实验过程变成可管理、可查询、可回滚的资产。1.2 harvey-labs 类仓库的结构化思路结构化的核心不是把文件塞进很多文件夹而是给代码、数据、模型、配置和记录定义明确职责。推荐遵守三条原则。第一实验之间相互独立。每个实验有自己的代码目录、配置文件和输出目录避免两个实验修改同一个脚本然后互相覆盖。第二公共能力下沉共享。数据读取、评估指标、可视化工具、模型定义中会被多个实验复用的部分抽到独立模块里不复制粘贴。第三结果和代码分开。训练产物属于生成文件不应该和源代码混在一起否则 Git 仓库会快速膨胀。这样设计以后仓库的层级会从“所有代码平铺”变成“公共库 实验层 结果层”。harvey-labs 作为实验室仓库可以同时容纳多个实验但每个实验都应该像一个小型独立项目。1.3 实验仓库应该包含的最小闭环一个实验仓库的最小闭环包含五部分环境描述、数据入口、训练代码、结果记录、产物输出。环境描述告诉你怎么运行数据入口说明数据从哪里来训练代码是主体逻辑结果记录保存指标和参数产物输出保存模型权重和日志。如果缺了一部分复现链路就会断裂。没有环境描述代码能看但跑不起来没有结果记录跑完不知道当前状态没有明确的产物输出目录模型和日志混在原代码里后续维护成本极高。下面各节会围绕这个最小闭环展开每个环节都会给出直接能用的文件模板。2. 环境准备从 Python 版本到依赖管理的基线2.1 先确认四类基础环境在创建仓库之前先确认机器上的基础工具。不同操作系统的命令略有差异但检查思路一致。检查项目的常见命令Python 版本确认能支持目标框架python --versionCUDA 驱动决定 PyTorch 是否能用 GPUnvidia-smiGit 版本拉取和提交代码git --version内存和磁盘评估数据和模型缓存空间free -m、df -h很多 AI 实验环境问题并不是框架代码写错而是 CUDA 驱动、PyTorch 版本和显卡驱动不匹配。nvidia-smi显示的是驱动支持的最高 CUDA 版本不代表 PyTorch 实际使用的版本。验证 GPU 是否可用更可靠的方式是在创建完环境后运行一小段 PyTorch 脚本检查torch.cuda.is_available()的结果。2.2 用 venv 或 conda 隔离实验环境Python 项目不要全局安装依赖。不同实验对 Python 版本和包版本要求差异很大全局安装会出现“既要 3.9 又要 3.11”的矛盾。推荐在仓库根目录使用虚拟环境让每个仓库自带一套依赖。python -m venv .venv source .venv/bin/activate # Linux / macOS # Windows PowerShell 下执行 .venv\Scripts\Activate.ps1如果需要在多个 Python 版本之间切换可以用 conda 创建带指定 Python 版本的环境conda create -n harvey-labs python3.10 conda activate harvey-labs虚拟环境目录不要提交到 Git。.gitignore中至少包含.venv/、__pycache__/、*.pyc、data/、outputs/、checkpoints/这些常见条目。2.3 用 pyproject.toml 管理依赖而不是裸 requirements.txt传统做法是维护一个requirements.txt直接列出包名。简单项目足够但进入实验仓库阶段会暴露问题没有明确的依赖分组不区分运行依赖和开发依赖传递依赖版本无法锁定安装时可能出现“本机能跑、别人机器不能跑”的差异。推荐使用pyproject.toml管理项目元信息和依赖。对 AI 实验仓库一个基础版本可以这样写[project] name harvey-labs version 0.1.0 description AI experiment repository requires-python 3.10 dependencies [ torch2.0,2.5, numpy1.24, pyyaml6.0, pandas2.0, ] [project.optional-dependencies] dev [ pytest7.0, ruff0.1, pre-commit3.0, ] [build-system] requires [setuptools68] build-backend setuptools.build_meta在这个配置中日常运行只需要安装dependencies开发环境再安装dev分组。这样做的好处是分工明确后面接入 CI 时测试镜像可以不安装开发工具减少体积。2.4 环境检查清单创建环境后建议执行下面清单Python 能正常import目标框架。GPU 可用性通过实际代码验证不只依赖nvidia-smi。虚拟环境在仓库根目录下创建并在.gitignore中排除。依赖写入pyproject.toml没有用pip install xxx直接装完就忘。记录安装时间和安装来源便于回溯。这里的核心不是依赖越全越好而是依赖必须可追溯。只要新环境能通过这份清单后面跑实验时就能少掉一半排查时间。3. 目录结构设计让每个实验独立又共享公共能力3.1 推荐的仓库目录结构harvey-labs 这类实验室仓库可以采用下面这套目录结构。它不是唯一答案但足够清晰适合大多数以 Python 为主的 AI 实验。harvey-labs/ ├── src/harvey_labs/ # 公共代码库 │ ├── __init__.py │ ├── config.py # 配置加载 │ ├── data.py # 数据读取与预处理 │ ├── metrics.py # 评估指标 │ ├── models/ # 模型定义 │ └── utils.py # 日志、路径、工具函数 ├── experiments/ # 每个实验一个子目录 │ ├── exp001_baseline/ │ │ ├── README.md │ │ ├── config.yaml │ │ ├── train.py │ │ └── outputs/ │ └── exp002_finetune/ ├── data/ # 原始数据与中间数据 │ ├── raw/ │ ├── processed/ │ └── external/ ├── scripts/ # 辅助脚本 │ ├── prepare_data.py │ └── export_results.py ├── tests/ ├── pyproject.toml ├── Makefile └── README.md3.2 每个目录的职责src/harvey_labs是公共代码库。数据预处理、指标计算、模型基础结构都放这里。它解决的问题是不同实验尽量不要各自复制一份utils.py。如果复制后续修改 bug 时要改好几个地方很容易漏。experiments是实验目录。每个实验是一个子目录包含自己的 README、配置、训练代码和输出。实验之间通过目录隔离避免冲突。data是数据目录分为raw、processed、external三部分。raw放原始数据processed放清洗后数据external放外部公开数据。数据的生成代码要放进scripts保证数据可以重建。tests是测试目录重点覆盖公共代码库和数据预处理逻辑。outputs不单独放根目录而是放在每个实验目录里这样每个实验的产物自带上下文。3.3 数据文件与代码分离的原因数据文件通常体积大、二进制格式多、变化频繁。如果放进 Git 仓库仓库体积会迅速膨胀clone 时间变长且每次数据更新都会产生大量 Git 历史。更合理的做法是让代码和路径分离通过配置或环境变量指定数据位置。在实验仓库里可以把数据路径写成相对路径统一从仓库根目录读取。比如data/raw/train.csv在训练脚本中解析为from pathlib import Path ROOT_DIR Path(__file__).resolve().parents[2] DATA_DIR ROOT_DIR / data这样换机器后只要数据目录存在代码不需要改动。对于大规模数据可以考虑使用外部对象存储或网盘并在 README 中说明下载方式而不是把数据直接塞进 Git。3.4 配置文件模板配置文件负责把参数和代码解耦。一个训练实验的config.yaml可以这样写experiment_name: exp001_baseline seed: 42 data: train_path: data/processed/train.csv eval_path: data/processed/eval.csv batch_size: 32 model: name: resnet18 pretrained: true train: epochs: 20 learning_rate: 0.001 optimizer: adam log_interval: 50 output: checkpoint_dir: experiments/exp001_baseline/outputs/checkpoints log_dir: experiments/exp001_baseline/outputs/logs配置的好处是实验参数一目了然跑实验时不需要在命令行里传入几十个参数也方便后续把同一份配置记录到实验管理系统中。配置中的路径建议使用相对路径从仓库根目录出发保持可移植性。4. 实现一个可复现的训练实验模板4.1 最小训练脚本train.py 的骨架训练脚本是实验仓库的核心。一个最小可用模板需要包含加载配置、设置随机种子、准备数据、训练循环、保存模型。下面的示例展示骨架忽略真实网络结构只表达控制流程。import argparse import random from pathlib import Path import numpy as np import torch import yaml def load_config(path): with open(path, r, encodingutf-8) as f: return yaml.safe_load(f) def set_seed(seed): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed) def build_model(cfg): # 这里按照 config 中的 model 字段构建模型 # 示例省略实际网络结构 return torch.nn.Linear(10, 2) def main(cfg): set_seed(cfg[seed]) device torch.device(cuda if torch.cuda.is_available() else cpu) model build_model(cfg).to(device) print(fDevice: {device}) print(fModel: {cfg[model][name]}) # 训练循环应在这里实现 # 同时把指标写入日志文件 output_dir Path(cfg[output][checkpoint_dir]) output_dir.mkdir(parentsTrue, exist_okTrue) model_path output_dir / model.pt torch.save(model.state_dict(), model_path) print(fModel saved to {model_path}) if __name__ __main__: parser argparse.ArgumentParser() parser.add_argument(--config, typestr, defaultconfig.yaml) args parser.parse_args() config load_config(args.config) main(config)这个脚本虽然没有完整训练逻辑但已经体现了关键控制点配置从外部文件读取随机种子固定设备自动选择输出目录自动创建。实际项目在这个骨架上扩展数据和训练循环即可。4.2 把参数从代码中拆到 YAML很多初学项目喜欢把参数写在代码顶部比如LEARNING_RATE 0.001。跑一个实验要改参数直接改代码。这种做法的缺点是改完参数后代码版本和实验参数绑定在一起回看实验记录时无法明确知道某次结果对应哪组参数。把参数放到 YAML 后训练脚本通过load_config读取实验参数作为独立文件保存。每一次实验跑完把配置文件复制到输出目录实验和参数就形成了固定对应关系。后续对比不同实验时只需要查看每个实验的config.yaml不需要翻 Git 历史。参数拆分时要注意不要过度拆分。路径、批大小、学习率、模型名称属于需要变化的高频参数应该放进配置。常量计算方式、固定不变的数据列名可以在代码中定义。两者结合既保证灵活性又避免配置文件过长。4.3 日志和指标记录训练过程中的 loss、accuracy、学习率等指标必须落盘。标准输出会滚动消失notebook 单元格会被清空只有文件才能稳定追溯。最简单的做法是使用 Python 标准库logging配合csv写指标。import csv from pathlib import Path class MetricLogger: def __init__(self, log_dir): self.log_dir Path(log_dir) self.log_dir.mkdir(parentsTrue, exist_okTrue) self.header_written False def write(self, metrics: dict): csv_path self.log_dir / metrics.csv with open(csv_path, a, newline, encodingutf-8) as f: writer csv.DictWriter(f, fieldnameslist(metrics.keys())) if not self.header_written and csv_path.stat().st_size 0: writer.writeheader() writer.writerow(metrics)使用时每个 epoch 末尾把epoch、train_loss、eval_metric等写入MetricLogger。这样训练结束后可以用 pandas 读取metrics.csv画曲线也可以上传到实验管理平台。4.4 模型保存与加载模型保存不要只保存model.state_dict()建议把配置、环境信息和关键指标一并保存成一个 checkpoint 文件。完整保存字段至少包含checkpoint { model_state_dict: model.state_dict(), config: config, epoch: epoch, metrics: last_metrics, torch_version: torch.__version__, } torch.save(checkpoint, output_dir / checkpoint.pt)加载时先读取checkpoint字典再恢复模型和训练状态。这样虽然文件变大但复现时不需要额外查询配置和日志。对于只用于推理的模型可以另存一个轻量的model.bin避免每次都解析完整 checkpoint。5. 实验结果追踪没有记录等于白跑5.1 实验记录应该保存什么字段实验记录的核心目标是回答三类问题我跑了什么、效果怎么样、能不能重新跑。为此每个实验至少保存四组信息。第一组是实验标识包括实验编号、名称、时间、Git commit。第二组是参数配置直接保存config.yaml副本。第三组是指标结果包括每个 epoch 的 loss、准确率等。第四组是产物位置包括模型权重、日志文件、预测结果所在的路径。建议在实验目录里保存一份METADATA.json结构如下{ experiment_id: exp001_baseline, description: resnet18 baseline on custom dataset, git_commit: a1b2c3d, start_time: 2025-01-15T10:00:00, datasets: v1.0, train_metrics: { final_loss: 0.182, final_accuracy: 0.941 }, checkpoint_path: experiments/exp001_baseline/outputs/checkpoints/checkpoint.pt }5.2 用 CSV / JSON 做轻量实验管理对于个人实验室或小团队不需要一开始就上重型平台。可以用一张experiments/results_summary.csv记录所有实验的结果。每一行是一个实验包含实验编号、模型名称、数据版本、关键指标、配置路径。experiment_id,model,dataset,accuracy,loss,config_path exp001_baseline,resnet18,custom_v1,0.941,0.182,experiments/exp001_baseline/config.yaml exp002_finetune,resnet18,custom_v1,0.955,0.141,experiments/exp002_finetune/config.yaml这种方式足够处理几十个实验的对比。当实验数量增长到几百个、指标维度增加时再迁移到实验管理平台。5.3 接入 MLflow 做指标和模型追踪当实验数量上升后手动维护 CSV 会漏记或难查。MLflow 是常见的实验跟踪工具可以记录参数、指标、模型和产物。安装后在训练脚本中加入几行代码即可接入。pip install mlflow训练脚本中的使用方式import mlflow mlflow.set_experiment(harvey-labs) with mlflow.start_run(run_nameconfig[experiment_name]): mlflow.log_params(config[model]) mlflow.log_metric(accuracy, final_accuracy) mlflow.log_artifact(config_path) mlflow.pytorch.log_model(model, artifact_pathmodel)MLflow 的价值不只是记录而是提供查询界面。你可以按模型名、数据集、指标值筛选历史实验快速找到最优结果对应的配置。生产环境或团队协作时它比 CSV 可靠得多。5.4 数据集版本控制模型指标必须和数据集版本绑定。数据集一旦变化之前的结果就失去可比性。最轻量的做法是用文件记录数据集的哈希或版本号训练脚本启动时把它写进METADATA.json。sha256sum data/processed/train.csv data/processed/train.csv.sha256进一步可以使用 DVCData Version Control管理数据版本。DVC 会把数据指针纳入 Git数据文件本身存储在本机或远程存储中。切换 Git commit 时可以用dvc checkout拉回对应版本的数据实现代码和数据版本同步。6. 固定运行环境让实验在不同机器上可复现6.1 依赖锁定的三种方式pyproject.toml中写的是宽松版本范围比如torch2.0,2.5。这适合描述依赖的上限和下限但不能保证所有人安装到完全相同的版本。实验复现要求精确锁定版本。常见锁定方式有三种方式命令优点适用场景requirements.txt 锁定pip freeze requirements.lock简单、直观单机快速复现uv lockuv lock速度快管理集中Python 项目工程化Docker 镜像docker build连系统库一起锁住生产、复杂环境推荐的做法是pyproject.toml维护依赖声明锁定文件保存实际安装的版本Docker 负责系统级环境。三者职责不同不是互相替代的关系。6.2 使用 Docker 包住完整运行环境AI 实验的依赖不仅包括 Python 包还包括 CUDA 库、系统库、环境变量。Docker 可以把这些全部封装进镜像让该实验在任何装有 Docker 的机器上运行。下面是一个基础 Dockerfile 示例FROM python:3.10-slim WORKDIR /workspace COPY pyproject.toml ./ RUN pip install --no-cache-dir .[dev] COPY src ./src COPY experiments ./experiments COPY data ./data ENV PYTHONPATH/workspace/src CMD [python, experiments/exp001_baseline/train.py, --config, experiments/exp001_baseline/config.yaml]这个镜像仍然没有固定 PyTorch 的 CUDA 版本生产环境应使用官方 PyTorch 镜像作为基础镜像。书写时不要凭空指定版本号落地前确认与实际 GPU 驱动匹配即可。6.3 Makefile 把常用命令收口每次运行实验都要写很长的命令容易出错。用 Makefile 把高频操作收口让 README 里的说明变短。下面是一个适合实验仓库的 Makefile 示例.PHONY: setup train test lint setup: python -m venv .venv pip install -e .[dev] train: python experiments/exp001_baseline/train.py --config experiments/exp001_baseline/config.yaml test: pytest tests/ lint: ruff check src experiments tests使用make setup初始化环境make train跑训练make test执行测试。命令语义清楚新成员加入时不需要翻 README 找命令。6.4 运行验证从一个新 clone 的仓库跑通实验验证复现性最有效的方式是模拟从零开始。步骤如下在一台干净机器上git clone仓库。执行make setup安装环境。确认数据目录存在或执行数据准备脚本。运行make train。检查输出目录中是否生成metrics.csv和checkpoint.pt。对比当前结果和实验记录中的指标是否一致。这一步要纳入仓库 README作为 CI 之前的人工验证。只要新 clone 能跑通这个仓库才算具备基本可复现能力。7. 常见问题与排查链路7.1 GPU 不可用或显存不足现象有两种程序运行在 CPU 上速度非常慢程序启动后立刻报 CUDA out of memory。先检查 GPU 是否被识别import torch print(torch.cuda.is_available()) print(torch.cuda.device_count())如果第一条输出是False说明 PyTorch 版本和 CUDA 驱动不匹配或安装的是 CPU 版本。需要重新安装与机器驱动匹配的 PyTorch。如果显存不足先把batch_size调小再看数据加载时是否有未释放的显存变量或者改用梯度累积。不要一开始就换更大的显卡先看是否代码中保留了过多不需要的中间变量。7.2 依赖冲突表现是安装某个包时提示另一个包依赖版本冲突或者运行时报ImportError。优先查看pip freeze中的实际版本确认冲突发生在传递依赖上。排查顺序是先看错误信息中提到的包名和版本再用pip show package查看当前版本然后用pip install packageversion锁定一方版本最后重新测试。如果冲突反复出现考虑使用独立虚拟环境或 Docker 隔离避免多个项目共用环境。7.3 换机后路径失效最常见的错误原因是代码里写了绝对路径比如C:/Users/xxx/data/train.csv或/home/ubuntu/experiments/data/。解决办法是统一使用相对路径并以仓库根目录为基准。可以在config.py中维护一个工具函数from pathlib import Path PROJECT_ROOT Path(__file__).resolve().parents[2] def data_path(relative_path: str) - str: return str(PROJECT_ROOT / relative_path)训练脚本所有路径都走data_path()换机器后只要仓库结构完整路径就不会失效。检查和排查时优先搜索代码中的盘符路径和绝对路径写法。7.4 实验记录丢失现象是训练跑完了但找不到对应的参数和指标。主要原因可能是训练脚本没有写日志或者写到了临时目录、notebook 内存里。解决方式是统一使用MetricLogger和METADATA.json并在训练结束时把关键信息写盘。建议在训练脚本的finally块中保存元信息避免程序中途异常退出导致记录缺失。7.5 模型文件管理混乱文件多了以后不知道哪一个是最终模型。解决方案是给产物文件命名带上实验编号和关键指标例如exp001_resnet18_acc0941.pt同时在实验 README 中写明“当前最优模型”指向哪个文件。更规范的做法是使用 MLflow Model Registry 或模型注册表把模型生命周期纳入管理。对于个人仓库命名规则已经能解决大部分问题。问题现象常见原因检查方式处理建议GPU 无法使用安装了 CPU 版 PyTorch 或驱动版本不对运行torch.cuda.is_available()重装匹配驱动和 CUDA 版本的 PyTorch显存不足batch_size 过大或中间变量占用观察报错位置和显存曲线调小 batch_size清理无用变量依赖冲突传递依赖版本不兼容pip show查看实际版本锁定版本或使用 Docker 隔离路径失效代码写死绝对路径搜索C:或/home/路径统一使用仓库根目录相对路径实验记录丢失指标只打印到控制台检查输出目录有没有日志文件接入 MetricLogger 并保存 METADATA8. 最佳实践与扩展方向8.1 可以落地的仓库规则给 harvey-labs 这类实验仓库定几条可执行规则比追求复杂工具更重要。第一每个实验必须有一个README.md写清楚实验目的、运行方式、数据集来源和当前结果。第二训练产生的不需要提交 Git 的文件必须写在.gitignore中。第三每个实验结束前复制一份config.yaml到输出目录。第四所有训练脚本运行前先检查数据路径是否存在不存在就报错并提示如何下载。第五不把 notebook 作为唯一入口关键数据预处理和模型定义必须沉淀为.py模块。这些规则可以在仓库初始化时写成带注释的模板文件让新实验复制模板而不是从空白开始。8.2 从单机实验到团队协作需要补的能力个人实验仓库满足单人使用后加入团队还需要补三块能力。第一是代码评审流程。实验代码虽然强调快速迭代但公共库src/harvey_labs中的改动必须走评审防止破坏多个实验。第二是持续集成。每次提交后运行pytest和ruff确保公共模块没有被改坏。第三是产物共享。模型权重和结果文件不能只存在个人电脑上需要上传到统一存储或对象存储并在仓库中记录下载地址。在团队协作中实验记录从“个人笔记”变成“团队资产”需要有人负责维护实验目录的命名和归档否则多人协作下仓库会重新混乱。8.3 扩展方向自动训练、模型注册、实验看板当实验数量继续增长可以沿三个方向扩展。自动训练方向使用脚本或调度平台批量运行多个实验把config.yaml中的参数组合提交到 GPU 排队训练。模型注册方向把所有候选模型上传到模型注册中心记录评测指标、数据和代码版本供下游服务调用。实验看板方向把 CSV 或 MLflow 数据同步到可视化面板按模型、数据集、时间等维度筛选结果。这些扩展都要建立在基础实验仓库稳定之后。直接引入大型平台而不先解决目录、环境和记录只会让问题更复杂。harvey-labs 式的实验室仓库真正价值不在于文件夹多丰富而在于跑出来的每个结果都能被解释、被复现、被复用。新项目初始化时把这份规范代入实验迭代的效率会明显高于从临时脚本开始。