依赖管理:requirements.txt 与 pip freeze

从一次环境灾难说起

几年前我接手一个图像分类项目,代码在同事机器上跑得好好的,到我这儿就是各种报错。折腾了两天,最后发现是 numpy 版本差了一个小版本号。嗯,从那以后,我对依赖管理再也不敢马虎了。

说白了,依赖管理 就是把你项目用到的所有 Python 包和它们的版本号记下来。别人拿到这份记录,就能还原出跟你一模一样的环境。这件事在 AI 项目里尤其重要——模型训练的结果经常因 torch、transformers 甚至 scikit-learn 的版本不同而产生微妙差异。

核心观点:可重复的依赖配置,是 AI 模型可重复性的第一道防线。没有它,其他所有努力都是空中楼阁。

requirements.txt 的标准写法

requirements.txt 是 Python 社区最基础的依赖声明文件。格式很简单:每行一个包名,可以带版本约束。

numpy==1.24.3
torch>=2.0.0,<2.1.0
transformers~=4.30.0
scikit-learn
opencv-python-headless==4.8.0.74

这里有几种版本指定方式,我列个表,你一看就明白:

写法 含义 使用场景
numpy==1.24.3 精确锁定,必须这个版本 核心科学计算库,版本敏感
torch>=2.0.0 最低版本,允许更高 需要新特性,但兼容后续
torch>=2.0.0,<2.1.0 版本范围,卡住大版本 避免大版本升级破坏API
transformers~=4.30.0 兼容性锁定,允许 patch 版本 希望小修小补,不换大版本
scikit-learn 无约束,装最新版 不敏感的工具库,慎用

我在项目中遇到过这样的坑:一个同事写了 torch>=1.13,结果半年后新机器装了 2.1.0,模型推理结果跟原来差了 2% 的准确率。你想想看,这么大的偏差,谁能想到是 torch 版本的问题?

我的习惯:核心计算库(torch、tensorflow、numpy、pandas)一律用 == 精确锁定。辅助工具库可以用范围约束。

pip freeze 的利与弊

很多人图省事,直接用 pip freeze > requirements.txt 来生成依赖文件。这样做确实快,但问题也不少。

pip freeze 会把当前环境中所有已安装的包都列出来,包括那些你根本没在代码里 import 过的间接依赖。举个例子:

pip freeze > requirements.txt
cat requirements.txt

numpy==1.24.3
torch==2.0.1
torchvision==0.15.2
transformers==4.30.2
tokenizers==0.13.3
safetensors==0.3.3
huggingface-hub==0.16.4
Pillow==9.5.0
requests==2.31.0
urllib3==2.0.3
certifi==2023.5.7
...

这里面 tokenizers、safetensors、huggingface-hub 其实是 transformers 的间接依赖。你用 pip freeze 把它们也锁死了。这样做的好处是环境还原度极高,坏处是——换个项目,这些间接依赖的版本冲突可能让你头疼到怀疑人生。

注意:不要直接把 pip freeze 的输出当成项目的 requirements.txt 提交到仓库。应该把它拆成两份文件:一份手写的 requirements.in(只列直接依赖),一份工具生成的 requirements.txt(全量锁定)。

我建议的做法是:维护一个 requirements.in 文件,只写你项目真正需要的包:

# requirements.in - 只列直接依赖
torch==2.0.1
torchvision==0.15.2
transformers==4.30.2
scikit-learn==1.3.0
matplotlib==3.7.2
pillow==10.0.0

然后用 pip-compile(来自 pip-tools 包)来生成锁定的 requirements.txt:

pip install pip-tools
pip-compile requirements.in --output-file requirements.txt

这样生成的 requirements.txt 既包含直接依赖,也包含间接依赖,并且每个包都有精确版本号。干净、完整、可复现。

依赖锁定与版本管理的艺术

依赖管理不只是写个文件那么简单。我总结了几条实战经验,分享给你:

  1. 区分开发依赖和生产依赖:训练用的 jupyter、tensorboard 不需要出现在推理环境的 requirements 里。拆成 requirements-dev.txt 和 requirements-prod.txt。
  2. 每次训练前记录依赖快照:我习惯在每次跑实验前,执行一次 pip freeze > requirements_$(date +%Y%m%d_%H%M%S).txt。这样即使后来升级了包,也能回溯到某个实验的原始环境。
  3. docker 镜像里锁定系统级依赖:Python 依赖只是一部分。CUDA、cuDNN、系统库的版本同样关键。建议用 Dockerfile 把整个环境固化下来。
  4. 不要忽略 Python 版本:在 requirements.txt 旁边放一个 .python-version 文件,或者直接用 pyproject.toml 声明。
我曾经犯过一个低级错误:requirements.txt 里锁了 torch 1.13.1,但没注意 Python 版本。换了台 Python 3.11 的机器,torch 1.13.1 压根不支持,装都装不上。所以,Python 版本也要一起锁定。

下面这张图,帮你理清从项目定义到环境复现的整个依赖管理流程:

依赖管理核心流程 手动维护 requirements.in 直接依赖 torch、transformers… 全量锁定 pip freeze / pip-compile 全量依赖文件 requirements.txt 环境复现 pip install -r 为什么两步走? 直接依赖由你手动维护 间接依赖由工具自动锁定 既清晰又完整

实战:构建可复现的依赖配置

好了,理论说完了,咱们上手跑一遍。这是我个人推荐的标准操作流程:

  1. 创建虚拟环境:用 python -m venv .venv 或 conda 创建一个干净的环境。
  2. 手动安装直接依赖:一个一个装,或者从旧项目复制 requirements.in。
  3. 生成锁定文件:用 pip freeze 或 pip-compile 生成全量版本。
  4. 版本控制:把 requirements.in 和 requirements.txt 都提交到 Git。
  5. 验证复现:在干净环境执行 pip install -r requirements.txt,跑一遍测试脚本。
# 完整流程示例
python -m venv .venv
source .venv/bin/activate

# 安装直接依赖
pip install torch==2.0.1 torchvision==0.15.2 transformers==4.30.2

# 生成全量锁定文件
pip freeze > requirements.txt

# 验证
pip install -r requirements.txt --no-deps  # 检查是否有缺失
踩坑提醒:我用 pip freeze 时曾经遇到过一个问题——它会把一些 pip 自带的包(比如 setuptools、wheel)也锁进去。这些包在别人的环境里版本可能不一样,导致安装警告甚至失败。所以生成后最好检查一下,把 setuptools、wheel、pip 本身从 requirements.txt 里删掉。

依赖管理这件事,说难不难,说简单也不简单。关键是要养成习惯:每次安装新包,立刻更新 requirements 文件;每次跑实验,记录当时的依赖快照。坚持下来,你会发现"环境不一致"这种坑,越来越少。