科学智能(AI for Science)正在从论坛上的概念热词,变成需要动手解决的真实工程问题。很多技术团队和科研院所开始用实训营的方式推动这个方向落地,9 月开营的磐石·科学智能实训营就是一类典型例子。这个信号值得注意:科学智能不再只是“用神经网络拟合几条曲线”,而是要把数据预处理、物理约束、模型训练、实验验证和结果解释串成一条完整链路。对于想进入这个方向的开发者来说,真正的难点不是装一个深度学习框架,而是理解科学数据长什么样、模型要学到什么规律、以及实验结果如何复现。
在动手写代码之前,我会用一个小型分子性质预测案例,把科学智能项目的完整流程拆开讲清楚。你会看到这一类项目的环境怎么搭、代码怎么组织、指标怎么验证,也会看到最容易出问题的数据泄漏、特征尺度、偶然相关等问题。整个项目用 Python 和常见机器学习库就能跑通,适合作为第一个科学智能练习。
1. 科学智能不是简单套用深度学习,它改变的是研究闭环
1.1 先理解科学计算与数据驱动的边界
传统科学研究的常用路径是“猜想、建模、仿真、实验验证”。科学家先提出理论方程,再用计算软件求解,最后用实验数据修正参数。这个过程可靠,但缺点是模型更新慢,计算复杂时很难兼顾多尺度问题。
科学智能做的事情,是用机器学习模型去拟合那些无法轻易写成显式方程的关系。它不排斥物理方程,而是在数据驱动和物理规律之间取一个结合点。比如在分子性质预测里,分子结构和溶解度之间确实存在物理化学规律,但规律太复杂,很难用统一公式表达。机器学习可以把大量分子结构映射成特征,再从实验数据里学出统计规律。
所以,科学智能的核心不是“有没有用深度学习”,而是“能不能把科学知识的归纳和验证过程自动化”。这也决定了科学智能项目的评价标准与传统互联网推荐项目不同。推荐系统看点击率,科学智能还要看预测结果是否具备物理合理性、是否能在新实验里复现。
1.2 科学智能的几条主流技术路线
把当前常见项目放在一起看,大致能分成三类。
第一类是分子和材料性质预测。输入是分子结构、晶体结构、原子坐标,输出是溶解度、毒性、带隙、力学强度等性质。常用做法包括分子指纹加传统机器学习,以及图神经网络、Transformer 等深度模型。这类任务最容易让初学者上手,因为它和标准机器学习项目相似,难点在于特征构造和实验数据质量。
第二类是物理场和微分方程求解。很多物理过程可以用偏微分方程描述,但方程复杂、边界条件复杂时,传统求解器会很慢。神经网络算子可以学习从初始条件到解的映射,训练完成后推理速度远快于数值求解。这类任务难度更高,需要理解方程本身的数学含义。
第三类是科学实验优化。在材料合成、药物研发里,实验成本很高,不能盲目尝试所有组合。算法需要根据已有实验数据推荐下一组实验参数,把主动学习和贝叶斯优化用在实验设计中。这类任务讲究“少做实验、多做判断”,工程上还需要和自动化实验设备对接。
1.3 为什么科学智能工程的调试方向不同
传统开发调试时,大多数问题集中在接口返回、并发、数据库状态。科学智能项目里,问题往往藏在数据和特征层。
例如模型预测结果很好,但换一批分子就不行,可能是训练数据分布覆盖面不够;模型损失下降很慢,可能是特征没有做归一化;测试集分数异常高,可能是同一批分子在切分时泄漏到了训练集。这些问题的排查路径和普通 Web 服务完全不同,也更依赖实验记录和版本管理。
做科学智能的第一步,不是急着学某个最新模型,而是先建立一套能记录数据版本、特征版本、模型版本和实验指标的工程习惯。后面的代码示例也会围绕这个习惯展开。
2. 搭建一个可复现的科学 AI 环境,再开始写模型
2.1 最小运行环境要求
科学智能项目依赖较多,建议先用虚拟环境隔离,不要直接装在系统 Python 里。下面是我常用的环境组合。
| 项目 | 推荐配置 | 备注 |
|---|---|---|
| 操作系统 | Ubuntu 20.04/22.04,macOS 也可 | Windows 需要额外处理 RDKit 依赖 |
| Python | 3.9 或 3.10 | 部分科学计算库对 3.11+ 支持滞后 |
| 包管理 | conda 或 venv + pip | RDKit 建议用 conda 安装 |
| GPU | 可选,先不依赖 | 小规模案例用 CPU 足够 |
| CUDA | 11.7 以上 | 只在需要训练 GNN 时准备 |
如果电脑上已经有 conda,创建环境使用下面命令。
conda create -n ai4s python=3.9 -y conda activate ai4s2.2 安装依赖:RDKit、scikit-learn、pandas
分子性质预测需要 RDKit 处理分子结构。RDKit 安装比较特殊,直接pip install rdkit在某些环境会失败,建议用 conda 安装。
conda install -c conda-forge rdkit -y pip install pandas scikit-learn numpy如果后续要扩展到深度模型,再安装 PyTorch。
pip install torch --index-url https://download.pytorch.org/whl/cpu这里重点解释一下 RDKit 的作用。RDKit 能把 SMILES 字符串解析成真正的分子对象,然后计算分子量、脂水分配系数、氢键供体数量等描述符。这些描述符是模型输入的重要特征,比直接把字符塞给模型要可靠得多。
2.3 项目目录结构
科学计算项目很容易变成一堆不可复现的脚本,建议从第一个项目开始就按层次分目录。
ai4s-solubility/ ├── data/ # 原始数据,尽量只读 ├── notebooks/ # 探索性分析 ├── src/ # 正式代码 │ ├── features.py # 特征工程 │ ├── model.py # 模型训练和评估 │ └── inference.py # 推理脚本 ├── models/ # 模型产物 ├── results/ # 指标和图表 ├── requirements.txt └── README.md这里的关键约束是data目录只存放原始数据,所有中间结果都写到results,预防不小心修改原数据。真实研究中,这个目录还会增加configs保存实验配置。
2.4 环境检查清单
进入正式实现前,先检查环境是否满足下面条件。
python --version输出 3.9 或 3.10。conda list | grep rdkit能看到 RDKit 版本。python -c "from rdkit import Chem; print(Chem.MolFromSmiles('CCO'))"输出一个分子对象而不是报错。python -c "import sklearn; print(sklearn.__version__)"正常输出版本号。
这些检查只需要一分钟,但能避免后面把时间浪费在依赖冲突上。如果环境反复装不上 RDKit,优先检查 conda 源和 Python 版本。
3. 用最小案例跑通分子溶解度预测闭环
3.1 案例目标:从分子结构预测水溶解度
在药物研发和材料筛选里,水溶解度影响化合物的吸收、分布和实验方案设计。这个任务非常适合作为科学智能入门案例,因为数据可以表示成 SMILES 字符串,特征可以用 RDKit 计算,模型也可以从最简单的随机森林开始。
这个案例的目标是输入一个分子结构,输出预测的溶解度数值。为了跑通流程,我先造一个演示数据集,实际项目要换成经过实验验证的公开数据集或自有数据。
import random import pandas as pd from rdkit import Chem random.seed(42) smi_pool = [ "CCO", "CCN", "CCC", "CCCC", "c1ccccc1", "CC(=O)O", "CCOC", "CC(C)C", "c1ccccc1O", "CCCl" ] rows = [] for i in range(200): smi = random.choice(smi_pool) mol = Chem.MolFromSmiles(smi) if mol is None: continue logp = Chem.Descriptors.MolLogP(mol) # 这里是演示数据生成逻辑,不是真实实验值 sol = -1.5 * logp + random.uniform(-0.5, 0.5) rows.append({"smiles": smi, "solubility": round(sol, 3)}) df = pd.DataFrame(rows) df.to_csv("data/solubility_demo.csv", index=False)这段演示代码生成 200 条包含smiles和solubility两列的记录。注意溶解度是模拟值,只是为了演示完整链路,不能拿去做真实科研判断。拿到真实数据集以后,只需保证 CSV 里保留这两列,后续代码可以复用。
3.2 数据加载和描述符计算
从 CSV 加载数据后,下一步是把 SMILES 转成 RDKit 分子对象,再计算分子描述符。
import pandas as pd from rdkit import Chem from rdkit.Chem import Descriptors from rdkit.ML.Descriptors import MoleculeDescriptors df = pd.read_csv("data/solubility_demo.csv") descriptor_names = ["MolWt", "MolLogP", "NumHDonors", "NumHAcceptors", "TPSA"] calculator = MoleculeDescriptors.MolecularDescriptorCalculator(descriptor_names) def smiles_to_features(smiles): mol = Chem.MolFromSmiles(smiles) if mol is None: return None values = calculator.CalcDescriptors(mol) return list(values) feature_list = [] valid_idx = [] for i, row in df.iterrows(): feats = smiles_to_features(row["smiles"]) if feats is not None: feature_list.append(feats) valid_idx.append(i) X = pd.DataFrame(feature_list, columns=descriptor_names) y = df.loc[valid_idx, "solubility"]这里选择 5 个描述符作为演示特征,原因有两个:一是它们和分子疏水性、分子大小、氢键能力直接相关,对溶解度有较强解释力;二是数量少,方便排查特征问题。真实项目里可以根据领域知识增加描述符或改用分子指纹。
3.3 切分训练集和测试集,关键是不能随机采样
分子性质预测里,数据切分需要特别小心。如果只是随机切分,同一个分子的不同记录可能同时出现在训练集和测试集,导致测试指标虚高。更贴近真实应用的做法是使用 ScaffoldSplitter,按分子骨架切分。
from rdkit import Chem from rdkit.Chem.Scaffolds import MurckoScaffold from sklearn.model_selection import train_test_split scaffolds = [] smiles_list = df.loc[valid_idx, "smiles"].tolist() for smi in smiles_list: mol = Chem.MolFromSmiles(smi) scaffold = MurckoScaffold.MurckoScaffoldSmiles(mol=mol) scaffolds.append(scaffold) scaffold_df = pd.DataFrame({"scaffold": scaffolds}) train_idx, test_idx = train_test_split( range(len(scaffold_df)), test_size=0.2, stratify=scaffold_df["scaffold"], random_state=42 ) X_train, X_test = X.iloc[train_idx], X.iloc[test_idx] y_train, y_test = y.iloc[train_idx], y.iloc[test_idx]这段代码先把分子转成骨架,再按骨架做分层切分,保证训练集和测试集里的分子骨架不重叠。这样验证结果更能反映模型遇到新结构时的表现。
3.4 训练随机森林回归模型
特征准备好以后,模型部分可以用 scikit-learn 的随机森林。随机森林对特征尺度不敏感,适合作为科学智能项目的基线模型。
from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score model = RandomForestRegressor( n_estimators=200, max_depth=10, min_samples_leaf=2, random_state=42 ) model.fit(X_train, y_train) y_pred = model.predict(X_test) print("R2:", r2_score(y_test, y_pred)) print("MAE:", mean_absolute_error(y_test, y_pred)) print("RMSE:", mean_squared_error(y_test, y_pred, squared=False))训练完成后输出三个指标。R2 表示模型解释了多少方差,MAE 表示平均绝对误差,RMSE 表示大误差样本对结果的惩罚。只看 R2 不够,还要结合 MAE 和 RMSE 判断模型是否会在大误差样本上表现很差。
3.5 保存模型和推理脚本
训练完成后,把模型和特征列名保存下来,方便后续推理。
import joblib joblib.dump(model, "models/solubility_rf.joblib") joblib.dump(descriptor_names, "models/descriptor_names.joblib")推理脚本读取模型,输入 SMILES,输出预测值。
import joblib from rdkit.Chem import Descriptors from rdkit.ML.Descriptors import MoleculeDescriptors model = joblib.load("models/solubility_rf.joblib") descriptor_names = joblib.load("models/descriptor_names.joblib") calculator = MoleculeDescriptors.MolecularDescriptorCalculator(descriptor_names) def predict_solubility(smiles): mol = Chem.MolFromSmiles(smiles) if mol is None: raise ValueError("Invalid SMILES") descs = calculator.CalcDescriptors(mol) pred = model.predict([list(descs)])[0] return pred print(predict_solubility("CCO"))到这里,一个最小科学智能项目已经形成闭环:原始分子结构进入系统,模型输出性质预测。这个闭环虽然简单,却是后面所有复杂方案的地基。
4. 运行验证不是只看分数,要回到任务理解结果
4.1 训练时的预期输出
运行上面对应脚本后,应该看到类似下面的输出,实际数值会因为演示数据和随机种子略有不同。
R2: 0.87 MAE: 0.31 RMSE: 0.42如果脚本报错,先对照环境检查清单确认 RDKit 和 scikit-learn 是否安装成功。如果 R2 为负值,说明模型比直接取平均值还差,需要检查特征构造和标签方向。
4.2 从指标判断模型状态
科学智能项目里,指标要结合任务背景解读。R2 在 0.8 以上对这个演示任务算不错,但真实实验数据通常噪声更大,R2 反而可能只有 0.5 到 0.7。这不一定代表代码写错,也可能是实验误差和特征信息量不足。
MAE 和 RMSE 的差距也能说明问题。两者接近,说明误差分布均匀;RMSE 明显大于 MAE,说明存在少数预测误差很大的样本。遇到这种情况,要回到训练数据里找这些样本,检查是否存在分子结构特殊或标签记录异常的情况。
4.3 模型推理结果是否合理
把训练集里出现过的分子和新分子分别输入推理脚本,检查输出。
for smi in ["CCO", "CCCC", "c1ccccc1", "O=C(O)C"]: print(smi, predict_solubility(smi))预期结果是:碳链越长、脂溶性越强的分子,溶解度数值越低;带羟基、羧基的分子溶解度更高。如果推理结果明显违背化学常识,那么特征计算或训练数据大概率有问题。
4.4 参数调整会影响什么
随机森林里有几个参数对科学智能项目影响较大,整理成表格便于对照。
| 参数 | 默认值 | 调大影响 | 调小影响 | 适用场景 |
|---|---|---|---|---|
| n_estimators | 100 | 模型更稳定,但训练更慢 | 训练快,易波动 | 数据量大时 300 左右足够 |
| max_depth | None | 容易过拟合小数据 | 偏差变大 | 小数据限制为 5 到 15 |
| min_samples_leaf | 1 | 叶子样本多,更稳健 | 过拟合 | 数据量小时设为 2 到 5 |
| max_features | 1.0 | 每棵树更相似 | 树之间差异大 | 特征多时设为 sqrt |
科学智能里没有一套万能参数,更实际的做法是先把默认参数跑通,再用交叉验证对比参数组合。不要一开始就追求复杂模型,基线模型的结果就是后续优化的参照线。
5. 科学智能项目中最常见的坑和排查路径
5.1 数据泄漏:测试集里混入了训练集信息
这是科学智能项目最隐蔽的问题。现象是训练时 R2 很高,线下评估也很高,但一到实际新样本预测就崩。
检查方式有几个。第一,查看训练集和测试集的 SMILES 是否有交集。第二,检查切分方式是否按分子骨架而不是普通随机切分。第三,检查特征里是否包含实验条件等测试时拿不到的信息。
解决方法是统一使用基于分子结构的切分策略,并且在数据预处理阶段就把同一分子的所有记录放进同一集合。这种问题一旦出现,后面的模型再复杂都没有意义。
5.2 RDKit 解析大量 SMILES 时静默失败
RDKit 遇到非法 SMILES 时,MolFromSmiles会返回 None,而不是抛出异常。很多初学者继续往下计算特征,结果矩阵里混进空值。
def smiles_to_features_safe(smiles): mol = Chem.MolFromSmiles(smiles) if mol is None: raise ValueError(f"Unable to parse SMILES: {smiles}") return list(calculator.CalcDescriptors(mol))在预处理阶段应该主动拦截非法分子,记录原始 SMILES,排查是否来自数据清洗遗漏。
5.3 小数据集过拟合
科学实验数据量通常很小,几百条到几千条都很常见。模型很容易在训练集上记住个别样本,测试集分数很高但真实预测不稳。
处理方式是限制模型复杂度、增加交叉验证、使用简单模型作为基线。随机森林、线性回归在这种场景下往往比深度模型更实用。等基线稳定后再尝试图神经网络。
5.4 结果无法复现
实验做完,同事却复现不出同样的分数,常见原因是随机种子没有固定、依赖版本不一致、数据切分顺序不同。
训练脚本里要固定 Python 的random、NumPy 和 scikit-learn 的随机种子。
import random import numpy as np random.seed(42) np.random.seed(42)同时把依赖版本写入requirements.txt,把数据哈希记录到实验配置文件里。科学计算的复现要求比普通项目高得多,这一点要尽早养成习惯。
5.5 分子指纹维度过大导致内存问题
真实项目中经常用 RDKit 的摩根指纹替代少量描述符,维度可能到 2048 或 4096。这时数据量一大,稀疏矩阵直接转成密集矩阵会让内存暴涨。
处理方案是使用稀疏格式。
from rdkit.Chem import AllChem def mol_to_fingerprint(smiles): mol = Chem.MolFromSmiles(smiles) if mol is None: return None fp = AllChem.GetMorganFingerprintAsBitVect(mol, radius=2, nBits=1024) return fp这类指纹向量只有在位为 1 的位置才参与计算,保存和读取时都要注意保持稀疏结构。
下面把常见问题整理成一张排错速查表。
| 现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 测试分数很高但真实预测差 | 数据泄漏 | 检查 SMILES 交集和切分方式 | 改用骨架切分,特征去除实验条件 |
| 训练 loss 正常,测试 loss 高 | 过拟合 | 查看训练和测试指标差距 | 减小模型深度,增加数据量 |
| 某些分子特征为空 | SMILES 非法 | 打印MolFromSmiles返回值 | 清洗数据,记录原始 SMILES |
| 换机器后结果不同 | 随机种子或版本不一致 | 检查依赖版本 | 固定 seed,锁定 requirements |
| 程序内存溢出 | 指纹维度或批量过大 | 查看数据大小 | 使用稀疏矩阵,减小 nBits |
6. 从最小闭环走向真实科学工作流的建议
6.1 扩展数据流水线
演示项目只用了 5 个描述符和 200 条数据,真实场景里数据规模、来源、质量都要复杂得多。建议从三个方面扩展。
一是数据标准化。SMILES 字符串有多种写法,同一分子可以表示成不同顺序,要使用 RDKit 的标准化函数统一分子表示。二是数据关联。科学数据经常分散在不同实验记录里,建模前要明确样本的唯一标识,防止不同来源的数据错位。三是数据版本管理。原始数据一旦更新,历史实验结果就要重新评估,最好用 DVC 或简单哈希记录每次数据变更。
6.2 增加模型可解释性和不确定性评估
科学智能的落地不只看预测精度,还看研究人员是否敢用预测结果做决策。这个环节建议关注两方面。
可解释性可以用 SHAP 值分析特征贡献,看模型是否依赖了有物理意义的特征。如果模型只靠一个过拟合噪声特征,说明数据或特征选择有问题。不确定性估计也很重要,模型应该知道自己对陌生分子不太确定。简单做法是使用随机森林每棵树的预测方差,更复杂做法是使用贝叶斯神经网络或集成方法。
6.3 与实验闭环结合
科学研究对模型的期望不是“替代实验”,而是“减少实验次数”。实践中可以把模型嵌入主动学习循环:先训练初版模型,用模型挑出最不确定且最可能有价值的候选样本,再让实验人员验证,把新数据加回训练集。
这个闭环对工程的要求是接口清晰。模型输入输出要封装成函数,实验记录要能自动回填数据库。演示项目里的predict_solubility(smiles)就是一个最小接口,扩展成远程服务时同样要保持这种函数式语义。
6.4 工程化最佳实践清单
从最小项目走向真实项目时,下面这份清单可以直接使用。
- 数据文件只读,所有中间结果写入独立目录。
- SMILES 解析失败时主动报错,不静默跳过。
- 模型训练前先固定全局随机种子。
- 用骨架切分替代普通随机切分,避免数据泄漏。
- 记录每个实验的数据版本、依赖版本、模型参数和指标。
- 保存推理脚本和特征列名,避免上线时特征顺序不一致。
- 用 SHAP 或特征重要性检查模型依据。
- 基线模型使用简单算法,深度模型作为后续优化方向。
- 所有结果保存到带时间和实验编号的目录,不要覆盖历史实验。
- 上线前用一组人工标注的关键样本做回归测试。
科学智能的学习路线和传统软件工程不太一样。真正值钱的不是背熟某个模型的 API,而是能在数据、特征、模型、验证之间建立可追踪的闭环。把一个最小分子性质预测项目从头到尾跑通、记录清楚、排查出问题,比起一次性堆很多高级模型,更容易帮你建立起科学智能项目的全局观。后面再进入图神经网络、物理约束建模、实验优化时,就可以沿着这套闭环逐层扩展。