
在计算化学领域里onepot-Bench 0: towards lab-aware in silico chemistry benchmarks这个标题看起来像是一个简单的数据集描述实际上它同时提出了三个问题一锅法反应onepot为什么值得单独建基准普通 in silico 基准为什么不够用Lab-aware 到底是哪里“感知”了实验室这篇文章会从标题拆解开始带你把这条技术主线走通先理解 onepot-Bench 0 要解决什么问题再搭一个最小复现环境写一条可以跑通的数据加载、特征化、分组划分、模型训练与评估流水线最后给出可用的排查清单和扩展方向。目标是让你读完以后不只是知道这个基准名字而是能把一类“实验室感知型”计算化学基准在本地复现出来。1. 先理解 onepot-Bench 0 解决什么问题1.1 一锅法反应是合成化学里的“连续任务”一锅法one-pot反应指的是多个化学转化在同一个反应容器里连续完成中间体不需要分离纯化。这样可以减少操作步骤、降低物料损耗、提高合成效率在药物中间体和精细化工里非常常见。但对计算化学来说一锅法带来了一个特殊挑战模型要预测的不是单个反应步骤而是一组反应条件叠加在底物序列上的最终结果。如果你把一锅法反应拆成多个独立的单步反应分别建模就会丢失每一步之间的相互影响。比如第一步生成的中间体可能在第二步被消耗掉也可能和溶剂、催化剂、温度变化一起影响最终产物分布。这就是 onepot-Bench 0 背后最重要的动机为“多步反应在一个条件下串联”这类任务提供一个可比较的基准。从数据形态看一条一锅法反应记录通常包含底物 SMILES、试剂、催化剂、溶剂、温度、反应时间、反应类型和最终产率。它和普通单步反应数据集的最大区别在于整条反应序列作为一个样本而不是每一步作为一个样本。1.2 为什么普通 in silico 基准不够用所谓 in silico chemistry benchmarks就是用计算手段评估机器学习模型能否从分子结构、反应条件中预测化学结果。传统基准大多把数据集随机切分成训练集和测试集然后直接比较 MAE、R2、AUC 等指标。但这类基准在真实实验室场景里经常失真原因主要有三个。第一随机划分会造成数据泄漏。同一个底物在不同反应条件下可能出现在训练集和测试集里模型其实是在记忆分子骨架而不是学习反应规律。第二随机划分忽略了试剂批次、实验日期、操作人员等实验因素。同一组反应在同一天、同一台设备上采集的数据天然存在批次相关随机划分会让模型悄悄利用这批“实验室指纹”。第三真实使用场景通常是预测“一个新底物、新配方”的产率或条件是否可行而不是在已知底物里插入缺失值。lab-aware 的核心思想就是让数据划分“感知”这些实验结构按实验室批次、反应类型、底物骨架或条件模板分组保证评估组在某个维度上对模型是全新的。onepot-Bench 0 这类基准的价值正在于把这种划分意识变成可执行的评估协议。1.3 onepot-Bench 0 的定位最小可用基准标题里的 0通常表示这是第 0 版也就是最小可用版本。它未必提供完备的数据平台而是先定义三样东西数据格式、任务定义、评估协议。这样后续版本才能在此基础上增加更多反应类型、更多数据规模、更多模型接口。对复现者来说第 0 版的实际意义是你不需要先搭建一个复杂的 Web 平台也不需要引入大规模分布式训练只要按它的划分逻辑跑通一个最小模型就能得到可对齐的指标。本文后面的复现也会按“最小可用”原则组织先解决能不能跑再考虑准不准。2. 从标题拆解基准结构协议、任务与数据格式2.1 标题里的每个词分别对应什么工程模块把 onepot-Bench 0 拆开看每个词都对应基准的一项设计决策而不是单纯的关键词堆叠。标题词汇在基准里的含义工程映射onepot面向一锅法反应序列数据按反应序列组织保留多步上下文Bench可复现、可比较的基准固定数据划分、固定指标、固定输入输出格式0第 0 版最小可用先做最小数据集和最小评估协议lab-aware划分时感知实验分组按批次、反应类型、底物骨架分组in silico计算模拟驱动用分子描述符和算法模型完成预测这五个词组合起来等于定义了一个“实验室条件感知的一锅法合成预测基准协议”。如果实际复现时拿到的原始材料里没有统一格式你也应该按这个思路去整理数据。2.2 一个最小数据表格为了让后续代码可以运行这里先定义一种最小的 CSV 格式。不要把这个格式当成 onepot-Bench 0 的官方结构而应理解成“为了说明基准流程而构造的通用样例结构”。字段名示例值含义reaction_idrxn_0001反应唯一编号substrate_smilesCC(O)Oc1ccccc1底物分子结构reagent_smilesOC(O)C1CCCCC1参与反应的试剂结构solventDMF溶剂名称或 SMILEStemperature_c120反应温度摄氏度time_h8反应时长小时reaction_typeesterification反应类型标签lab_batchbatch_2024_06_12实验批次编号product_smilesCC(O)OC2CCCCC2产物分子结构yield85.5实际产率目标值这样的表结构包含了两类信息一类是分子结构和反应条件用于生成特征另一类是 lab_batch 和 reaction_type用于分组评估。重要的是lab_batch 不应该参与模型特征它只在划分阶段发挥作用。2.3 核心任务类型onepot-Bench 0 可以包含多种预测任务但最核心的是以下三类。第一类是产率回归输入底物和条件输出一个连续的产率数值评价指标用 MAE 和 R2。第二类是条件推荐排序给定底物和候选条件集合模型给每个候选条件打分评价指标用 Top-1 命中率或 Top-3 命中率。第三类是中间体存在性判断判断在多步一锅法条件下某个中间体能否被检测到这是一个二分类问题评价指标用 ROC-AUC。在实际复现里建议先从产率回归入手因为它的指标解释成本最低也最容易做损失分析。等回归任务跑通再往排序和分类扩展。3. 搭建本地复现环境3.1 运行时和依赖复现 onepot-Bench 0 最小流水线建议使用 Python 3.10 或 3.11。依赖按用途可以分成数据处理、分子特征化、机器学习三组。依赖版本建议用途Python3.10 或 3.11运行环境pandas2.x数据加载与清洗numpy1.26 或 2.x数值计算scikit-learn1.3 以上分组划分、模型训练、评估rdkit2023.9 或 2024.3SMILES 解析、分子指纹最推荐的方式是用 conda 建立独立环境避免系统 Python 被污染。conda create -n onepot-bench-0 python3.10 conda activate onepot-bench-0 conda install -c conda-forge rdkit pandas numpy scikit-learn这样安装的 RDKit 会预编译好不需要单独处理 boost 依赖。如果使用 pip 安装建议这样写pip install rdkit pandas numpy scikit-learnRDKit 通过 pip 安装时依赖版本可能与 conda 不同如果遇到二进制兼容报错优先退回 conda-forge 通道。3.2 目录结构复现工程建议保持简洁方便后续替换成真实数据集。onepot-bench-0/ ├── data/ │ └── onepot_demo.csv ├── src/ │ ├── load_data.py │ ├── features.py │ ├── split.py │ └── train_eval.py ├── output/ │ ├── predictions.csv │ └── metrics.json └── run.sh这个结构的作用是把“加载数据”“生成特征”“分组划分”“训练评估”四个环节分开。原因是这四个环节各自有独立调试点数据加载阶段查缺失值特征化阶段查 SMILES 解析划分阶段查分组结构训练阶段查指标。3.3 生成模拟数据由于无法确认 onepot-Bench 0 原始数据集中是否包含真实反应记录这里提供一个“用于验证流水线”的模拟数据生成器。它不用于得出化学结论只用来保证代码可运行、可验证。import pandas as pd import numpy as np np.random.seed(42) substrates [ CC(O)Oc1ccccc1, CCO, c1ccc(cc1)CO, N#CCc1ccccc1, ] reagents [ OC(O)C1CCCCC1, OS(O)(O)O, CC(O)Cl, ] solvents [DMF, THF, toluene] reaction_types [esterification, hydrolysis, alkylation] batches [batch_2024_06_10, batch_2024_06_11, batch_2024_06_12] rows [] for i in range(200): substrate np.random.choice(substrates) reagent np.random.choice(reagents) solvent np.random.choice(solvents) reaction_type np.random.choice(reaction_types) batch np.random.choice(batches) temp float(np.random.randint(80, 160)) time_h float(np.random.randint(2, 20)) # 模拟产率包含条件效应和随机噪声 yield_val 50.0 yield_val 0.2 * temp yield_val - 1.5 * time_h yield_val np.random.normal(0, 8.0) yield_val float(np.clip(yield_val, 5.0, 99.0)) rows.append({ reaction_id: frxn_{i:04d}, substrate_smiles: substrate, reagent_smiles: reagent, solvent: solvent, temperature_c: temp, time_h: time_h, reaction_type: reaction_type, lab_batch: batch, product_smiles: substrate, yield: round(yield_val, 2), }) df pd.DataFrame(rows) df.to_csv(data/onepot_demo.csv, indexFalse) print(df.head())这段代码生成了 200 条反应记录。product_smiles 直接借用了 substrate只是为了演示流程不是化学上正确的产物写法。真实复现时必须使用实验数据不能用模拟值评估模型效果。4. 用 Python 实现最小复现流水线4.1 数据加载与清洗数据加载要做三件事读取 CSV、检查缺失值、过滤不可解析的 SMILES。SMILES 是分子结构的线性编码解析失败通常意味着字符串本身有问题比如括号不匹配、原子符号错误、盐类写法不规范。# src/load_data.py import pandas as pd from rdkit import Chem SMILES_COLUMNS [substrate_smiles, reagent_smiles, product_smiles] def load_data(path: str) - pd.DataFrame: df pd.read_csv(path) print(f原始样本数: {len(df)}) missing df.isnull().sum() print(f缺失值统计:\n{missing[missing 0]}) for col in SMILES_COLUMNS: if col in df.columns: df df[df[col].notna()] df df[df[col].apply(lambda s: Chem.MolFromSmiles(s) is not None)] print(f清洗后样本数: {len(df)}) return df.reset_index(dropTrue)这里有三个关键点。第一过滤 SMILES 解析失败必须在特征化之前完成否则后面生成指纹时会反复处理无效对象。第二不要直接删除整行而是把原始样本数和清洗后样本数都打印出来这样能快速判断是数据源本身有问题还是清洗逻辑写得太严。第三真实数据里如果 product_smiles 缺失可以单独处理而不是直接丢弃因为有些反应任务只需要预测产率不需要产物结构。4.2 分子特征化特征化的目标是把 SMILES 变成模型能读的数字向量。这里使用 RDKit 的 Morgan 指纹也就是 ECFP 的一种实现。它会枚举分子中每个原子周围的环境再哈希成固定长度的比特向量。# src/features.py import numpy as np import pandas as pd from rdkit import Chem from rdkit.Chem import AllChem, Descriptors FP_SIZE 2048 RADIUS 2 def mol_fingerprint(smiles: str) - np.ndarray: mol Chem.MolFromSmiles(smiles) if mol is None: return np.zeros(FP_SIZE) fp AllChem.GetMorganFingerprintAsBitVect(mol, RADIUS, nBitsFP_SIZE) return np.array(fp, dtypenp.float32) def make_features(df: pd.DataFrame) - pd.DataFrame: # 分子结构指纹 sub_fp np.vstack(df[substrate_smiles].apply(mol_fingerprint)) reagent_fp np.vstack(df[reagent_smiles].apply(mol_fingerprint)) # 反应条件数值特征 condition_features df[[temperature_c, time_h]].values.astype(np.float32) # 溶剂和反应类型做 one-hot 编码 solvent_oh pd.get_dummies(df[solvent], prefixsolvent).values type_oh pd.get_dummies(df[reaction_type], prefixtype).values X np.hstack([sub_fp, reagent_fp, condition_features, solvent_oh, type_oh]) return pd.DataFrame(X, columns[ff_{i} for i in range(X.shape[1])])这种做法有几个工程上的取舍。第一FP_SIZE 取 2048RADIUS 取 2这是化学反应筛选任务里比较常见的默认配置。FP_SIZE 太小会碰撞严重太大则训练较慢且稀疏RADIUS 增大虽然能捕捉更大原子环境但特征维度和稀疏性都会上升。第二温度和时间需要归一化但这里先保留原始值因为随机森林对数值尺度不敏感如果换成神经网络就必须做标准化。第三溶剂和反应类型不适合直接编码成整数否则模型会误认为 2 和 3 之间的距离比 1 和 3 更远one-hot 更合适。4.3 lab-aware 组划分这正是 onepot-Bench 0 最核心的部分。普通随机划分会把同一个批次、同一个底物的样本同时放进训练集和测试集测试成绩会虚高。lab-aware 划分要求测试集中的样本来自模型训练时完全没有接触过的实验组。# src/split.py import numpy as np from sklearn.model_selection import GroupShuffleSplit def lab_aware_split(df: pd.DataFrame, group_col: str lab_batch, test_size: float 0.2, seed: int 0): groups df[group_col].values splitter GroupShuffleSplit(n_splits1, test_sizetest_size, random_stateseed) train_idx, test_idx next(splitter.split(df, groupsgroups)) return train_idx, test_idx def random_split(df: pd.DataFrame, test_size: float 0.2, seed: int 0): from sklearn.model_selection import train_test_split return train_test_split(np.arange(len(df)), test_sizetest_size, random_stateseed)这里有两个函数lab_aware_split 和 random_split。同时保留两者的目的是做对照实验一个模型在随机划分和 lab-aware 划分上的指标差异能直接说明数据泄漏程度。解释一下 GroupShuffleSplit 的工作原理它先根据 group_col 找出所有不同的 lab_batch然后在这些批次上做随机划分再把批次映射回样本。这样测试集中的样本其 lab_batch 一定不会出现在训练集里。另一种更严格的分组方式是按 reaction_type 分组让测试集只包含模型从未见过的反应类型。比如训练集只有 esterification 和小部分 hydrolysis测试集全部是 alkylation。GroupShuffleSplit 同样支持这种用法把 group_col 换成 reaction_type 即可。4.4 基准模型与评估最小复现模型使用随机森林回归。原因很简单它对高维稀疏特征很稳定不需要特别复杂的调参而且能够给出特征重要性方便后续做特征分析。神经网络和 GNN 可以放在这个基线之后。# src/train_eval.py import json import numpy as np import pandas as pd from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import r2_score, mean_absolute_error def top_k_hit_rate(y_true, y_pred, k3): y_true np.asarray(y_true) y_pred np.asarray(y_pred) order np.argsort(-y_pred) top_k_true y_true[order[:k]] best_true np.max(y_true) return float(np.sum(top_k_true best_true - 1e-6) / min(len(y_true), k)) def run_training(X_train, X_test, y_train, y_test): model RandomForestRegressor( n_estimators300, max_depth20, min_samples_leaf2, random_state0, n_jobs-1, ) model.fit(X_train, y_train) pred model.predict(X_test) metrics { mae: float(mean_absolute_error(y_test, pred)), r2: float(r2_score(y_test, pred)), top3_hit_rate: top_k_hit_rate(y_test, pred, k3), } return model, pred, metrics评估指标为什么选这三个要逐一解释。MAE 直接反映产率预测的平均绝对误差单位是百分比实验室里容易理解。R2 衡量模型相对“永远预测平均值”的改进程度取值范围越接近 1 越好但化学数据普遍噪声大R2 在 0.3 到 0.6 也很常见。Top-k 命中率模拟的是“条件初筛”场景如果模型给 10 个候选条件打分真实产率最高的条件是否落在预测最高的前 3 个里。这个指标比 MAE 更贴近实际实验筛选逻辑。5. 运行验证与结果分析5.1 组织主流程把所有模块组合成一条流水线再通过 run.sh 一键执行。这个阶段不要直接追求模型精度先确认每个模块能正常衔接。# run.sh set -e python src/load_data.py python src/features.py python src/train_eval.py为了减少中间文件管理可以在一个入口文件里调用前面函数例如新建src/main.py。# src/main.py from load_data import load_data from features import make_features from split import lab_aware_split, random_split from train_eval import run_training import pandas as pd df load_data(data/onepot_demo.csv) X make_features(df) y df[yield].values.astype(np.float32) # lab-aware 划分 train_idx, test_idx lab_aware_split(df, group_collab_batch) X_train, X_test X.iloc[train_idx], X.iloc[test_idx] y_train, y_test y[train_idx], y[test_idx] model, pred, metrics run_training(X_train, X_test, y_train, y_test) print(lab-aware 划分指标:, metrics) # 对照随机划分 train_idx_r, test_idx_r random_split(df) X_train_r, X_test_r X.iloc[train_idx_r], X.iloc[test_idx_r] y_train_r, y_test_r y[train_idx_r], y[test_idx_r] _, _, metrics_random run_training(X_train_r, X_test_r, y_train_r, y_test_r) print(随机划分指标:, metrics_random) # 保存结果 pd.DataFrame({test_idx: test_idx, y_test: y_test, y_pred: model.predict(X_test)}).to_csv(output/predictions.csv, indexFalse) with open(output/metrics.json, w) as f: json.dump({lab_aware: metrics, random: metrics_random}, f, indent2)运行方式mkdir -p output python src/main.py预期输出类似下面这样具体数值会随模拟数据和模型波动。原始样本数: 200 缺失值统计: Series([], dtype: int64) 清洗后样本数: 200 lab-aware 划分指标: {mae: 12.3, r2: 0.21, top3_hit_rate: 0.65} 随机划分指标: {mae: 9.1, r2: 0.43, top3_hit_rate: 0.78}5.2 怎样判断基准跑得有效不要把“能运行”当成“跑通了”。要判断 onepot-Bench 0 复现是否有效至少要看四点。第一数据量是否足够支持分组。如果 sim 数据里 lab_batch 只有 3 个train 和 test 都只有几十个样本指标随机性会很大。第二清洗前后样本数变化是否合理。如果一半以上被过滤掉说明 SMILES 格式和 RDKit 的兼容性要先解决。第三lab-aware 和 random 的指标差异是否符合预期。通常 lab-aware 更严格MAE 会更高R2 会更低如果两者几乎一致可能是分组字段选得不好或者样本本身泄漏不严重。第四输出文件是否可复核。predictions.csv 应该保留 test_idx、真实值和预测值metrics.json 应该固化协议版本和划分方式。{ lab_aware: {mae: 12.3, r2: 0.21, top3_hit_rate: 0.65}, random: {mae: 9.1, r2: 0.43, top3_hit_rate: 0.78}, note: simulated data, used for pipeline verification only }5.3 随机划分与 lab-aware 划分的预期差异指标随机划分预期lab-aware 划分预期说明MAE偏低偏高随机划分存在信息泄漏R2偏高偏低测试集更难模型泛化压力更大Top-3 命中率偏高中等条件筛选困难度上升方差小大测试组少时结果不稳定如果 lab-aware 指标远差于随机不等于模型失败而是说明原数据集里“实验分组结构”对预测影响很大。这也是 onepot-Bench 0 要从随机划分转向 lab-aware 划分的根本原因。6. 常见问题排查6.1 按现象定位的排查表问题现象可能原因检查方式处理建议RDKit 安装失败系统缺少编译依赖或 pip 和 conda 环境混用python -c from rdkit import Chem; print(Chem.__version__)统一使用 conda-forge 安装不要在原环境里重复安装SMILES 解析出 None分子式写法不规范括号或原子符号错误单独取出 SMILES调用Chem.MolFromSmiles(s)检查加上解析失败日志输出具体行号和原始字符串分组划分报“group 数量不够”GroupShuffleSplit 要求至少分成两组但 lab_batch 只有一个打印df[lab_batch].nunique()改用更细的分组字段比如批次加反应类型组合lab-aware 和 random 指标几乎一样group_col 选取不合理或模拟数据里分组字段和 y 无关检查每个批次的产率均值方差分布更换分组字段如 substrate_smiles、reaction_type内存溢出2048 维指纹叠加大批量数据观察训练时内存占用或先跑 1000 条测试降低 FP_SIZE使用稀疏矩阵分批训练模型指标波动大测试集合太小或模型随机性多次设置不同 random_state使用 K-fold 分组交叉验证报告均值与标准差6.2 排查顺序实际排查时按下面顺序走最省时间。先检查输入数据。用df.info()、df.isnull().sum()确认列名和缺失值不要等到特征化之后才发现列名不对。再检查 SMILES 解析率。可以对每个 SMILES 列单独跑一遍解析统计成功比例低于 90% 时要优先处理。然后检查特征维度。打印 X.shape确认特征拼接逻辑没有把指纹和条件特征维度搞错。接下来检查划分逻辑。打印 train_idx 和 test_idx 对应的 lab_batch 集合确认有没有交叉。最后再检查模型和指标。如果模型在训练集上表现很好、测试集上很差首先要怀疑划分泄漏不要急着改模型结构。7. lab-aware 基准的设计实践与扩展方向7.1 学习环境与生产环境的差异事项本地学习环境真实科研生产环境数据来源模拟数据或公开小样本实验记录系统、实验室电子笔记本数据规模几百条数万条目特征化单机 RDKit并发指纹计算、缓存指纹库模型复杂度随机森林GNN、Transformer 或集成模型评估协议单次划分多折分组交叉验证可复现要求固定 seed 即可固定版本、提交数据 Hash、记录平台依赖化学校验忽略需要由化学家审阅预测结果在真实生产环境里onepot-Bench 0 的协议还应该包含“化学规则过滤”。比如模型预测出来的产物 SMILES 在热力学上不稳定或反应条件组合超出安全范围这些都不应该直接进入候选列表。7.2 可复用清单lab-aware 划分检查清单设计 lab-aware 基准时可以参考下面这张清单逐项核对。数据集里是否存在实验批次、日期、操作人员、设备编号等元数据字段。是否存在底物骨架、反应类型、配体结构等化学分组字段。分组字段是否与预测目标存在潜在关联比如同一个批次的产率整体偏高。是否同时实现了随机划分作为对照实验。测试集包含的分组数量是否足够支撑指标稳定。是否打印训练集和测试集的分组交集确认没有泄漏。是否保存了完整的划分索引、数据版本、依赖版本。是否在报告里同时给出 lab-aware 和 random 的指标而不是只报更好看的那一组。是否对预测结果做了化学合理性校验。7.3 从 onepot-Bench 0 扩展出去的方向第 0 版跑通后可以考虑四个扩展方向。第一多维分组划分。不再只按 lab_batch 分组而是同时考虑 lab_batch 和 substrate 骨架要求测试组在这两个维度上都是新的。这会更接近合成化学家“换一个全新骨架”的真实需求。第二条件编码增强。现在用的是溶剂、温度、时间后续可以把催化剂结构、浓度、加料顺序也作为特征甚至可以按 SMILES 编码催化剂。第三不确定性估计。随机森林可以输出单棵树的预测分布或者用分位数回归森林让模型在给出产率估计的同时给出置信区间。第四图神经网络。RDKit 指纹只是一袋子特征GNN 可以学习原子连接关系在信息传递中保留结构拓扑更有可能抓取一锅法串联反应里的中间产物敏感关系。从工程角度看更重要的是把当前流水线固化成可复用模板数据加载、特征化、划分、训练、评估五个模块都保留独立接口。这样后续换真实数据、换模型、换评估指标都只需要替换对应模块不破坏整体链路。回到最初的问题onepot-Bench 0 为什么值得关注因为它把化学基准的注意力从“全局指标是否好看”拉回“模型在真实实验分组下是否可靠”。当你用 lab-aware 划分替换随机划分的那一刻你会看到很多模型漂亮的指标开始缩水但这恰恰是它们进入实验室前该补的课。实际项目里最该记住的一句话是不要先用随机划分调模型再用 lab-aware 划分做终评。两个阶段的划分逻辑必须完全一致否则你优化的其实是一套错误目标。