依赖管理:requirements.txt 与 pip freeze
从一次环境灾难说起
几年前我接手一个图像分类项目,代码在同事机器上跑得好好的,到我这儿就是各种报错。折腾了两天,最后发现是 numpy 版本差了一个小版本号。嗯,从那以后,我对依赖管理再也不敢马虎了。
说白了,依赖管理 就是把你项目用到的所有 Python 包和它们的版本号记下来。别人拿到这份记录,就能还原出跟你一模一样的环境。这件事在 AI 项目里尤其重要——模型训练的结果经常因 torch、transformers 甚至 scikit-learn 的版本不同而产生微妙差异。
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 版本的问题?
== 精确锁定。辅助工具库可以用范围约束。
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 既包含直接依赖,也包含间接依赖,并且每个包都有精确版本号。干净、完整、可复现。
依赖锁定与版本管理的艺术
依赖管理不只是写个文件那么简单。我总结了几条实战经验,分享给你:
- 区分开发依赖和生产依赖:训练用的 jupyter、tensorboard 不需要出现在推理环境的 requirements 里。拆成
requirements-dev.txt和requirements-prod.txt。 - 每次训练前记录依赖快照:我习惯在每次跑实验前,执行一次
pip freeze > requirements_$(date +%Y%m%d_%H%M%S).txt。这样即使后来升级了包,也能回溯到某个实验的原始环境。 - docker 镜像里锁定系统级依赖:Python 依赖只是一部分。CUDA、cuDNN、系统库的版本同样关键。建议用 Dockerfile 把整个环境固化下来。
- 不要忽略 Python 版本:在 requirements.txt 旁边放一个
.python-version文件,或者直接用pyproject.toml声明。
下面这张图,帮你理清从项目定义到环境复现的整个依赖管理流程:
实战:构建可复现的依赖配置
好了,理论说完了,咱们上手跑一遍。这是我个人推荐的标准操作流程:
- 创建虚拟环境:用
python -m venv .venv或 conda 创建一个干净的环境。 - 手动安装直接依赖:一个一个装,或者从旧项目复制
requirements.in。 - 生成锁定文件:用
pip freeze或pip-compile生成全量版本。 - 版本控制:把
requirements.in和requirements.txt都提交到 Git。 - 验证复现:在干净环境执行
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 文件;每次跑实验,记录当时的依赖快照。坚持下来,你会发现"环境不一致"这种坑,越来越少。