1. 实验环境标准化:用 Docker 锁住一切
做可复现实验,第一道坎就是环境。
你想想看,一个实验跑完,过了三个月想复现,结果 Python 升了版本,某个库不兼容了,系统依赖也变了——这实验基本就废了。我早期做研究时就吃过这个亏,审稿人要求复现,我折腾了两周才把环境配回来,别提多狼狈了。
所以这一章,咱们就聊怎么用 Docker 容器化技术,把 Python 版本、系统依赖、第三方库版本全部锁定。说白了,就是把整个实验环境打包成一个“快照”,任何时候、任何机器上都能 100% 还原。
为什么非要用 Docker?
有人可能会问:我用 virtualenv 或者 conda 不也能锁定 Python 包吗?
嗯,这里要注意。虚拟环境只能管 Python 层面的依赖,系统底层的库它管不了。比如某个科学计算库依赖 libssl 的特定版本,换台机器系统不同,可能就装不上。Docker 连操作系统一起打包,这才是真正的“环境快照”。
我个人习惯把 Docker 镜像看作实验的“DNA”——它完整记录了实验运行所需的一切。你把这个镜像给别人,他 pull 下来就能跑,不需要任何额外配置。
核心原则:实验环境 = 操作系统 + 系统依赖 + Python 版本 + 第三方库版本。缺一个都不算“可复现”。
Dockerfile 怎么写才规范?
写 Dockerfile 其实有套路。我见过不少新手把 Dockerfile 写成“安装脚本大杂烩”,层数多、缓存利用率低、镜像体积大。下面是我总结的一个标准模板:
# 1. 选择基础镜像,锁定 Python 版本
FROM python:3.10-slim-bullseye
# 2. 设置工作目录
WORKDIR /workspace
# 3. 安装系统依赖(先装,利用 Docker 缓存)
RUN apt-get update && apt-get install -y \
build-essential \
libssl-dev \
libffi-dev \
&& rm -rf /var/lib/apt/lists/*
# 4. 复制依赖文件(先复制 requirements.txt,利用缓存)
COPY requirements.txt .
# 5. 安装 Python 依赖
RUN pip install --no-cache-dir -r requirements.txt
# 6. 复制实验代码
COPY . .
# 7. 设置默认命令
CMD ["python", "run_experiment.py"]
这个模板有几个关键点:
- 基础镜像选 slim 版本——体积小,安全漏洞少。我一般用
python:3.10-slim-bullseye,稳定且长期维护。 - 系统依赖先装——因为系统依赖变动少,放在前面可以利用 Docker 的层缓存,每次改代码不用重装系统包。
- requirements.txt 单独复制——这也是为了缓存。只改代码不改依赖时,pip install 这层不会重新执行。
小技巧:在 requirements.txt 里,每个包都锁定到精确版本号,比如 numpy==1.24.3,不要用 >= 或 ~=。我曾经因为一个包的小版本升级,导致实验结果偏差了 0.3%,排查了整整一天。
requirements.txt 的锁定策略
说到版本锁定,这里有个常见的坑:很多人直接 pip freeze > requirements.txt,然后就不管了。但 pip freeze 会列出所有间接依赖,换台机器可能因为系统不同,间接依赖的版本对不上。
我建议的做法是:
- 手动维护核心依赖列表,只写你直接 import 的包。
- 用
pip-compile(来自 pip-tools 包)生成锁定版本的完整列表。 - 把生成的
requirements.txt提交到版本控制。
举个例子:
# requirements.in(手动维护)
numpy
scipy
scikit-learn
torch==2.0.1
# 运行 pip-compile requirements.in 生成 requirements.txt
# 输出如下(简化版):
numpy==1.24.3
scipy==1.10.1
scikit-learn==1.3.0
torch==2.0.1
torchvision==0.15.2
...
注意:不要直接提交 requirements.in 到生产环境。Docker 构建时只认 requirements.txt,确保它包含了所有间接依赖的精确版本。
构建与运行:两条命令搞定
环境配好了,怎么用?其实就两条命令:
# 构建镜像(在 Dockerfile 所在目录执行)
docker build -t my-experiment:v1.0 .
# 运行容器(挂载数据卷,方便调试)
docker run --rm -v $(pwd)/data:/workspace/data my-experiment:v1.0
--rm 参数会在容器退出后自动删除,避免残留容器占空间。-v 挂载数据卷,这样实验数据可以持久化,不会随容器删除而丢失。
我个人习惯在项目根目录放一个 Makefile,把这两条命令封装起来:
build:
docker build -t my-experiment:v1.0 .
run:
docker run --rm -v $(pwd)/data:/workspace/data my-experiment:v1.0
这样团队成员只需要 make build 和 make run,不用记 Docker 命令。
本章知识体系
下面这张图总结了环境标准化的核心流程:
避坑指南:我踩过的三个坑
这些年用 Docker 做实验环境,我踩过不少坑。挑三个最典型的分享给你:
| 坑 | 表现 | 解决方案 |
|---|---|---|
| 基础镜像版本漂移 | 今天 build 和三个月后 build 的镜像不一样 | 使用 python:3.10-slim-bullseye 这种带小版本号的镜像,不要用 python:3 这种浮动标签 |
| pip 缓存污染 | 本地能跑,CI 上装不上 | Dockerfile 里加 --no-cache-dir,避免使用本地 pip 缓存 |
| 时区问题 | 日志时间戳对不上 | 在 Dockerfile 里设置 ENV TZ=Asia/Shanghai,统一时区 |
我记得有一次,团队里有人用了 python:latest 作为基础镜像,结果两个月后 Python 3.11 发布,镜像自动升级,所有依赖都炸了。从那以后,我规定所有项目必须用精确到小版本的基础镜像标签。
总结一下
环境标准化这件事,说白了就是“把不确定性降到零”。Docker 给了我们一个工具,但怎么用好它,取决于你对细节的把控。
这一章的核心就三件事:
- 写规范的 Dockerfile——分层清晰,缓存利用充分
- 锁定所有依赖版本——从系统库到 Python 包,一个都不能漏
- 用 Makefile 封装命令——降低团队使用门槛