机器学习研究代码可复现性:从依赖管理到工程化实践
1. 项目概述:为什么机器学习研究需要“工程化”?
如果你在机器学习领域摸爬滚打过几年,大概率经历过这样的场景:兴冲冲地打开一篇顶会论文的GitHub仓库,准备复现其惊艳的实验结果,却发现README里只有一句“运行python train.py”,然后就是无尽的依赖冲突、版本不兼容和神秘的“环境错误”。折腾几天后,你可能会怀疑人生:究竟是自己的姿势不对,还是论文的结果本就无法复现?
这恰恰是当前机器学习研究领域一个普遍而尖锐的问题:代码可复现性的缺失。我们投入大量精力设计精巧的模型和算法,却在最基础的“让别人能跑通代码”这一步上频频翻车。最近,波恩大学和亚琛工业大学的研究团队对NeurIPS、ICML、ICLR等顶级会议近十年的代码仓库进行了一次大规模“体检”,结果令人深思。他们发现,尽管Python已成为ML研究的绝对主流语言,但许多能保障可复现性的基础软件工程实践,其采纳率却低得惊人。例如,提供明确依赖声明(如requirements.txt或pyproject.toml)的项目在2024年的ICML上仍不足50%,而采用标准化打包(如pyproject.toml)的项目比例仅在20%-40%之间徘徊。
这不仅仅是“懒”或“时间紧”的问题。其背后反映的是一种认知偏差:许多研究者(尤其是学术圈)潜意识里认为,软件工程是工业界的事,研究代码只要能“work for me”就行,发布出去更像是一种“成果展示”而非“可复用的工具”。然而,机器学习研究本质上是高度经验性的科学,其结论严重依赖于代码实现的实验结果。如果代码无法被他人独立复现,那么论文的科学主张就如同空中楼阁,其可信度将大打折扣。更现实的是,糟糕的代码工程会制造巨大的“技术债”——今天你为了赶DDL省下的半小时,明天可能需要你或你的继任者花上数天甚至数周来调试和重构。
因此,提升机器学习研究的可复现性,绝非简单的“道德呼吁”,而是一项具有极高技术价值和实践必要性的工程任务。它关乎研究的可信度、社区协作的效率和整个领域知识积累的稳健性。本文将深入拆解这项调研的核心发现,并结合我多年在工业界和学术界“踩坑填坑”的经验,为你提供一套从思想到实操的、可落地的软件工程实践指南。我们的目标很明确:让你写的每一行研究代码,都成为他人可以信赖和构建的坚实基石。
2. 核心困境解析:当前ML研究代码的“工程化”短板在哪里?
调研数据像一面镜子,清晰地照出了社区在软件工程实践上的集体短板。理解这些短板的具体表现和成因,是我们寻求改进的第一步。
2.1 依赖管理的混乱现状:从“它在我机器上能跑”说起
依赖管理是可复现性的第一道关卡,也是最容易出问题的地方。调研显示,即使在2024年,主流会议上仍有超过一半的项目没有提供可靠的依赖声明文件。
问题根源在于“隐式依赖”和“版本漂移”。很多项目的做法是:在个人电脑或实验室服务器上,通过pip install torch numpy pandas等命令手动安装一堆包,代码跑通了,就认为环境准备好了。他们可能通过pip freeze > requirements.txt生成一个包含所有包(包括间接依赖)的锁定文件,但往往忽略了系统库、CUDA版本、Python解释器版本等关键因素。更常见的是,他们根本不提供任何依赖说明。
注意:直接使用
pip freeze生成的requirements.txt是一个“环境快照”,它锁定了当时环境中所有包的精确版本。这虽然有利于复现,但文件会非常臃肿,且当你想升级某个核心库(如PyTorch)时,会异常困难,因为它可能与其他数十个包的旧版本绑定。
现代Python生态的依赖管理工具已经相当成熟,但社区采纳度却不高。除了经典的requirements.txt,我们还看到:
pyproject.toml(PEP 621): 这是当前Python官方推荐的项目元数据和依赖声明标准文件。它清晰地区分了项目依赖(dependencies)和开发依赖(optional-dependencies),结构清晰。pylock.toml(PEP 751): 新兴的、标准化的依赖锁文件格式,旨在统一各种工具(如uv,pdm,poetry)生成的锁文件,是未来解决依赖锁定的希望。environment.yml: Conda环境文件,优势在于可以管理Python版本和非Python依赖(如特定版本的MKL数学库或CUDA工具包),非常适合复杂的科学计算环境。
调研发现,pyproject.toml的采用率正在上升,这是好现象。但pylock.toml由于太新,还很少见。而很多研究者误用Conda,将其仅用作Python包管理器,而不是利用其真正的优势去管理异构的、隔离的完整软件环境。
实操心得:依赖管理的“最小可行方案”对于时间紧迫的研究者,我建议一个分层策略:
- 必须做:在项目根目录创建一个
pyproject.toml文件,在[project]部分用dependencies列出你的直接依赖及其宽松的版本范围。例如:torch>=2.0, <3.0。 - 应该做:同时提供一个锁文件。如果你用
uv(推荐,速度极快),运行uv lock会自动生成uv.lock(未来是pylock.toml)。如果你用pip,可以运行pip freeze > requirements.lock.txt来生成一个精确锁定的环境快照,但请将它与声明性的requirements.txt或pyproject.toml区分开。 - 进阶做:如果项目严重依赖特定系统库或CUDA版本,提供一份
environment.yml来定义Conda环境。同时,在README中明确说明预期的操作系统、Python版本和CUDA版本。
2.2 代码打包的缺失:为何我们还在复制粘贴utils.py?
“打包”听起来很工程化,但其核心思想很简单:让你的代码能像安装numpy一样,被其他人通过pip install .轻松安装和使用。调研数据显示,仅有约20%-40%的项目提供了打包配置(如setup.py或pyproject.toml中的构建配置)。
不打包的代价是巨大的。最常见的反模式是:论文A的作者写了一个优秀的Transformer模块,放在model.py里。论文B的作者看到了,觉得有用,于是直接把整个model.py文件复制到自己项目的根目录下。几个月后,论文A的作者修复了model.py中的一个重要bug。论文B的项目对此一无所知,依然运行着有缺陷的代码副本。这就是典型的“代码复制地狱”,它导致了bug的扩散、功能的碎片化,并使得代码库难以维护。
打包解决了什么问题?
- 明确的依赖声明:打包工具会读取
pyproject.toml中的dependencies,确保安装你的包时,所有依赖也被正确安装。 - 可发现的入口点:通过打包,你可以定义命令行工具。例如,你的训练脚本可以被打包成一个命令
my-research-train,用户在任何地方都能调用。 - 版本化管理:你可以为你的代码分配版本号(如
1.0.0),方便他人追踪和引用。 - 促进代码复用:他人只需
pip install git+https://github.com/yourname/your-repo.git,然后import your_package即可使用,无需复制任何文件。
从setup.py到pyproject.toml的演进过去,打包依赖于setup.py——一个可执行的Python脚本。这种方式非常灵活,但也容易出错且不透明。现代实践已经完全转向pyproject.toml。在这个文件里,你可以用标准的[project]表来���义包名、版本、作者、依赖等信息,构建后端(如setuptools,hatch,poetry)会读取这些信息并完成打包。
踩坑记录:我曾接手一个老项目,它的
setup.py长达200行,里面充满了复杂的逻辑和条件判断,用于处理不同操作系统下的数据文件安装。当我想升级一个依赖时,完全无法确定这些逻辑是否会受到影响。后来我花了大力气将其迁移到pyproject.toml并配合setuptools的静态配置,可维护性瞬间提升。
2.3 测试与自动化的边缘化:没有测试的代码如何敢称“科学”?
测试是软件工程的基石,但在研究代码中却常常被视为“奢侈品”。调研发现,拥有独立tests/文件夹的项目仅占约四分之一,而配置了自动化工作流(如GitHub Actions)或测试工具(如tox/nox)的项目比例更低。
研究者的常见理由是:“我的模型一直在变,写测试太费时间”或“实验本身不就是测试吗?”。这是一种误解。科学实验验证的是科学假设,而软件测试验证的是代码行为的正确性。两者缺一不可。
研究代码需要测试什么?
- 核心算法与组件的正确性:例如,你实现了一个新的注意力机制,需要单元测试来验证其数学等价性(如前向传播与一个简单的参考实现是否一致),以及梯度计算是否正确(可以通过
torch.autograd.gradcheck)。 - 数据加载与预处理管道:确保数据转换的流程可重复,不会因为随机性导致训练和评估时数据不一致。
- 模型配置的加载与保存:测试你的模型配置(如YAML文件)能否正确被解析并实例化出模型,以及模型
state_dict的保存与加载是否无损。 - 集成测试:确保从数据加载、模型训练到评估的整个pipeline能够从头到尾跑通,即使是在一个小规模的伪数据集上。
自动化是可持续性的关键手动运行测试是不可靠的。tox或nox这类工具可以为你自动创建干净的虚拟环境,安装依赖和你的包,然后运行测试套件。将它们与GitHub Actions等CI/CD平台集成后,每次代码推送都会自动触发测试。这不仅能即时发现因修改引入的bug,更重要的是,它为项目的合作者(包括未来的你)提供了一个安全网,让他们有信心进行修改和重构。
JAX生态的启示:Chex调研中提到了JAX生态中的Chex库,它提供了一个优雅的范例。Chex的@variants装饰器能自动测试一个函数在JIT编译、多设备映射等不同JAX特性下的行为一致性。这启示我们,研究框架的测试可以更“聪明”,与框架的特性深度结合,而不是简单的断言。
2.4 随机性控制:被忽视的复现性“杀手”
神经网络训练充满了随机性:参数初始化、数据打乱(DataLoader)、Dropout、甚至某些CUDA操作。如果这些随机源不被控制,即使使用相同的代码和依赖,两次运行也可能产生截然不同的结果。
调研指出,明确固定随机数种子是达到“严格复现”的必要条件。PyTorch提供了torch.manual_seed()和torch.cuda.manual_seed_all(),但需要你在脚本的多个地方(模型初始化、数据加载器等)主动调用。JAX的设计更显式,它将随机状态封装在一个key对象中,并通过jax.random.PRNGKey(seed)创建,然后通过split传递和更新,这种函数式风格使得随机状态的管理更清晰、更可预测。
实操要点:系统性控制随机性
- 固定所有种子:在脚本开头,固定Python内置
random、numpy和深度学习框架(PyTorch/TensorFlow/JAX)的随机种子。 - 确定性算法:对于PyTorch,设置
torch.backends.cudnn.deterministic = True和torch.backends.cudnn.benchmark = False。注意,这可能会牺牲一些性能。 - DataLoader的Worker:如果使用多进程加载数据 (
num_workers > 0),需要为每个Worker设置不同的随机种子,通常通过worker_init_fn参数实现。 - 记录种子:将使用的所有种子值记录在实验配置或日志中。这是复现的“钥匙”。
3. 构建可复现ML项目的标准化工作流
理解了问题所在,接下来我们构建一个从零开始、贯穿项目生命周期的标准化工作流。这套流程的目标是:以最小的额外开销,最大化项目的可复现性、可维护性和协作便利性。
3.1 项目初始化:用模板告别混乱
万事开头难,一个好的开始是成功的一半。不要每次开新项目都从空白文件夹开始。
强力推荐:使用项目模板cookiecutter是一个创建项目模板的工具。你可以找到一个社区维护的、针对ML研究的模板(例如搜索cookiecutter ml),或者花点时间为自己团队创建一个内部模板。
一个基础的ML研究项目模板应该包含以下结构:
my_awesome_project/ ├── .github/ │ └── workflows/ # GitHub Actions 配置文件 │ └── test.yml ├── .pre-commit-config.yaml # 提交前自动检查配置 ├── .gitignore ├── pyproject.toml # 项目元数据和依赖声明 (PEP 621) ├── README.md ├── LICENSE ├── src/ │ └── my_project/ # 你的包源码 │ ├── __init__.py │ ├── data.py │ ├── models.py │ └── utils.py ├── tests/ # 测试代码 │ ├── __init__.py │ ├── test_data.py │ └── test_models.py ├── configs/ # 实验配置文件 (YAML/JSON) ├── scripts/ # 可执行的训练/评估脚本 │ ├── train.py │ └── evaluate.py ├── notebooks/ # Jupyter notebooks (用于探索性分析) └── docs/ # 文档 (可选,可用Sphinx生成)使用模板,你可以在几秒钟内获得一个结构清晰、工具链预配置的项目骨架,这能强制你养成好习惯。
3.2 依赖与环境管理:构建可复现的基石
这是可复现性的核心。我们采用“声明式依赖+锁文件”的双层策略。
第一步:创建pyproject.toml这是你的项目“身份证”和“菜单”。一个最小化的示例如下:
[project] name = "my-awesome-project" version = "0.1.0" authors = [ {name = "Your Name", email = "you@example.com"}, ] description = "A fantastic ML research project." readme = "README.md" requires-python = ">=3.9" license = {text = "MIT"} dependencies = [ "torch>=2.0.0", "numpy>=1.21.0", "pandas>=1.3.0", "scikit-learn>=1.0.0", # 只声明你直接import的包 ] [project.optional-dependencies] dev = [ # 开发依赖,如测试、代码格式化工具 "pytest>=7.0.0", "black>=23.0.0", "ruff>=0.1.0", ] docs = [ # 文档生成依赖 "sphinx>=5.0.0", ] [build-system] requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta"第二步:使用现代工具管理环境与依赖锁强烈推荐使用uv。它集成了pip、虚拟环境管理和打包工具,速度极快。
- 安装uv:
curl -LsSf https://astral.sh/uv/install.sh | sh - 创建并激活虚拟环境:
uv venv然后source .venv/bin/activate(Linux/macOS) 或.venv\Scripts\activate(Windows)。 - 安装项目依赖:
uv pip install -e .。-e代表“可编辑模式”,这样你对src/下的代码修改会立即生效。 - 安装开发依赖:
uv pip install -e ".[dev]"。 - 生成锁文件:
uv lock。这会创建一个uv.lock文件,精确锁定所有依赖的版本、哈希值。这个文件必须提交到版本控制!它是复现环境的终极保障。
重要提示:
uv.lock(或未来的pylock.toml)与pyproject.toml分工明确。后者是给人看的、宽松的依赖声明;前者是给机器看的、精确的环境快照。协作时,新成员只需uv sync即可获得与作者完全一致的环境。
3.3 代码质量与风格:让代码像论文一样清晰
混乱的代码会极大地增加理解和复现的难度。自动化工具可以无痛地保证代码风格一致。
推荐工具链:Ruff + Black + pre-commit
- Ruff: 一个用Rust写的极速Python linter和格式化工具,它集成了Flake8、isort等数十种工具的功能。在
pyproject.toml中配置:
[tool.ruff] line-length = 88 select = ["E", "F", "I", "B", "C4"] # 选择需要检查的规则集 ignore = ["E501"] # 忽略行长度检查(Black会处理)- Black: “毫不妥协”的代码格式化器。它消除了关于代码风格的争论,让所有代码看起来都一样。在
pyproject.toml中配置[tool.black]。 - pre-commit: 在代码提交到Git前自动运行检查的框架。创建
.pre-commit-config.yaml:
repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.1.0 hooks: - id: ruff args: [--fix] - repo: https://github.com/psf/black-pre-commit rev: 23.1.0 hooks: - id: black运行pre-commit install后,每次git commit时,Ruff和Black都会自动运行,修复可自动修复的问题,并阻止包含风格错误的提交。这保证了代码库的整洁。
3.4 测试策略:为研究代码注入可靠性
为研究代码设计测试需要一些技巧,目标不是100%覆盖率,而是覆盖关键逻辑和常见失败点。
测试结构:在tests/目录下,镜像你的src/结构。例如,src/my_project/models.py对应tests/test_models.py。
测试什么?
- 工具函数: 所有纯函数(如数据标准化、指标计算)都应该有单元测试。
- 模型层: 测试模型的前向传播形状是否正确,梯度是否存在(
assert param.grad is not None)。 - 数据管道: 测试数据加载器能否迭代,输出的数据形状和类型是否符合预期。
- 集成测试: 一个“冒烟测试”(smoke test),用最小的配置(如2个样本,1个epoch)跑一遍训练循环,确保没有运行时错误。
示例:测试一个自定义的损失函数
# src/my_project/losses.py import torch import torch.nn as nn class MyFancyLoss(nn.Module): def forward(self, predictions, targets): # 假设这是一个复杂的自定义损失 return torch.mean((predictions - targets) ** 2) # tests/test_losses.py import torch from my_project.losses import MyFancyLoss def test_my_fancy_loss_output_shape(): loss_fn = MyFancyLoss() preds = torch.randn(4, 10) # [batch_size, features] targets = torch.randn(4, 10) loss = loss_fn(preds, targets) assert loss.shape == torch.Size([]) # 标量损失 assert not torch.isnan(loss) def test_my_fancy_loss_gradient(): loss_fn = MyFancyLoss() preds = torch.randn(4, 10, requires_grad=True) targets = torch.randn(4, 10) loss = loss_fn(preds, targets) loss.backward() assert preds.grad is not None assert not torch.all(preds.grad == 0) # 梯度不应全为零使用pytest运行测试:pytest tests/ -v。将其集成到tox或 GitHub Actions 中实现自动化。
3.5 文档与协作:让别人(和未来的你)能看懂
代码是写给人看的,偶尔给机器执行。良好的文档能极大降低协作成本。
1. README.md:项目的门面必须包含:
- 项目标题与简介: 一句话说清楚是干什么的。
- 快速开始: 5分钟内让代码跑起来的步骤。
uv sync && python scripts/train.py --config configs/basic.yaml - 安装指南: 详细的环境配置说明。
- 使用方法: 如何训练、评估、使用模型。
- 引用: 如果关联论文,提供BibTeX引用格式。
- 许可证: 明确代码的使用权限。
2. 代码文档字符串(Docstrings)使用Google风格或NumPy风格的Docstrings为所有公开的函数、类、模块编写文档。这不仅能被Sphinx自动提取生成API文档,更是IDE智能提示和代码理解的利器。
def compute_metrics(predictions: torch.Tensor, labels: torch.Tensor) -> Dict[str, float]: """ 计算分类任务的多种评估指标。 Args: predictions: 模型预测的概率或logits,形状为 (N, C)。 labels: 真实标签,形状为 (N,)。 Returns: 一个字典,包含以下键值: - 'accuracy': 整体准确率。 - 'precision': 精确率(宏平均)。 - 'recall': 召回率(宏平均)。 - 'f1': F1分数(宏平均)。 Raises: ValueError: 如果 `predictions` 和 `labels` 形状不匹配。 """ # ... 实现代码3. 实验记录与配置所有实验的超参数、模型结构、数据集划分等,都应通过配置文件(如YAML)来管理,而不是硬编码在脚本中。每次实验运行,都应将完整的配置文件和随机种子记录到日志或实验跟踪工具(如Weights & Biases, MLflow)中。这是复现实验的“配方”。
4. 从个人到社区:如何推动可复现性文化?
个人的努力是基础,但社区文化的改变才能产生广泛影响。调研论文最后提出的“审稿人检查清单”是一个极佳的起点。我们可以将其具体化、场景化。
作为作者,你可以主动做的:
- 在项目主页明确状态: 在README最前面用徽章(badge)标明可复现性状态,例如:“✅ 依赖锁定”、“✅ 单元测试通过”、“✅ 一键运行脚本”。这既是承诺,也是宣传。
- 提供“一键复现”脚本: 创建一个
reproduce.sh或Makefile,里面包含从环境搭建到运行最终实验的所有命令。理想情况下,一行命令就能完成所有步骤。 - 拥抱容器化(可选但强力推荐): 使用Docker或Singularity将整个环境(包括操作系统库)打包成镜像。这是实现“在任何机器上完全一致”的终极方案。虽然对新手有门槛,但对于复杂依赖的项目是值得的。
作为审稿人/读者,你可以要求的:审稿时,除了算法创新和实验设计,也应将代码质量作为评估维度。可以温和地提出以下问题:
- “感谢开源代码。为了便于社区复现和验证,能否在仓库根目录添加一个
pyproject.toml或requirements.txt来明确项目依赖?” - “论文中提到了随机种子为42,但在代码中似乎没有找到设置种子的地方。为确保结果可复现,能否明确固定所有随机源(包括PyTorch/Numpy的随机种子和DataLoader的worker种子)?”
- “代码结构很清晰。考虑到未来的可维护性,是否考虑为核心模块添加一些简单的单元测试?这也有助于他人理解接口的正确用法。”
作为导师/团队负责人,你可以倡导的:
- 设立代码审查环节: 在论文投稿前,组织内部或小组间的代码审查,重点关注可复现性设置。
- 分享工具与模板: 在团队内部推广本文提到的工具链(
uv,ruff,pytest,pre-commit)和项目模板,降低入门成本。 - 奖励“工程卓越”: 在组会或团队内部,表扬和展示那些在代码可复现性和工程化上做得好的项目,将其树立为榜样。
5. 常见问题与避坑指南
在实践中,即使有了最佳实践的指导,仍然会遇到各种具体问题。以下是我总结的一些高频“坑点”及解决方案。
Q1:依赖冲突怎么办?特别是PyTorch与CUDA版本。A1:这是最常见的问题。绝对不要在requirements.txt或pyproject.toml里写死torch==1.13.1+cu117这种带系统环境后缀的版本。正确的做法是:
- 在
pyproject.toml中声明宽松的PyTorch版本:"torch>=2.0.0"。 - 在
README.md的“安装”部分,提供根据用户环境选择安装命令的指引。例如:
# 对于 CUDA 11.8 的用户 uv pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 对于 CPU 用户 uv pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu- 更高级的做法是使用
environment.yml来定义Conda环境,它可以很好地处理CUDA和cuDNN的版本。
Q2:我的项目有很多探索性的Jupyter Notebook,怎么管理?A2:Notebook非常适合探索,但不利于复现和版本控制。
- 清理与重构: 将Notebook中最终确定的、稳定的代码重构到
src/目录下的Python模块中。 - Notebook仅用于展示: 保留的Notebook应专注于可视化、结果分析和讲故事,其代码应尽量简洁,主要调用
src/中封装好的函数。 - 使用工具保证Notebook输出可复现: 使用
nbconvert或jupytext将Notebook转换为脚本,并纳入测试。使用papermill等工具参数化地运行Notebook。 - 预执行并清理输出: 提交到Git的Notebook最好是不含巨大输出(如图片、模型权重)的“清洁”版本,或者使用
nbstripout这样的Git钩子在提交时自动清理输出。
Q3:实验配置繁多,如何有效管理?A3:切忌将超参数硬编码在训练脚本里。
- 使用配置文件: 采用YAML或JSON文件管理配置。推荐使用
omegaconf或hydra库,它们支持配置继承、覆盖和命令行接口,非常强大。 - 配置与代码分离: 训练脚本只负责读取配置、构建pipeline并运行。所有可调参数都应来自配置文件。
- 记录配置: 每次实验运行时,都必须将完整的配置(包括所有默认值)保存到日志或实验跟踪系统中。这样,任何结果都能追溯到产生它的精确配置。
Q4:数据集很大或需要特殊权限,如何分享?A4:数据可复现性是另一大挑战。
- 提供明确的数据获取与预处理脚本: 在
README或单独的DATA.md中,详细说明数据来源、下载链接(或申请方式)、以及运行了哪些预处理命令(例如python scripts/preprocess_data.py --raw-dir ./raw --output-dir ./processed)。 - 生成并共享数据指纹: 对处理后的数据文件计算MD5或SHA256哈希值,并公布。他人运行预处理脚本后,可以比对哈希值来验证数据是否一致。
- 使用DVC(Data Version Control): 对于需要版本控制的大数据,DVC是一个优秀的选择。它可以将大文件存储在云存储(S3, GCS等)中,而在Git中只保存元数据指针。
Q5:训练一个模型要好几周,如何提供可复现的“最终模型”?A5:提供训练脚本和代码是基础,但有时让他人从头训练不现实。
- 发布训练好的模型权重(checkpoint): 将最终训练好的模型权重文件(如
.pt或.safetensors格式)上传到稳定的存储服务(如Hugging Face Model Hub, Zenodo)并提供永久链接。 - 提供推理脚本和示例: 附带一个简单的
inference.py脚本,展示如何加载发布的权重并对新数据进行预测。 - 说明训练细节: 在模型卡(Model Card)或README中,详细说明训练所用的硬件(GPU型号、数量)、训练时间、达到最终性能的具体epoch数等。这些信息对于他人评估和比较至关重要。
推动机器学习研究的可复现性,是一场需要研究者、审稿人、会议组织者和整个社区共同参与的“工程”。它始于我们对代码质量多一份的重视,对协作便利多一点的思考。每一次我们为项目添加一个清晰的pyproject.toml,每一次我们为关键函数编写一个测试,每一次我们在审稿中友善地提出工程改进建议,都是在为这座大厦添砖加瓦。这个过程或许不会让你的论文多一个创新点,但它会让你的工作更坚实、更持久,也更能经得起时间和同行检验。这,正是严谨科学的应有之义。
