1. 分子图模型工程导论
分子图模型工程,是把分子当作图结构来处理,并围绕“图神经网络训练—验证—部署”建立一套可复现、可度量、可迭代的工程体系。它与传统基于分子指纹(如 Morgan 指纹)加梯度提升树的流程最大的区别在于:模型不再依赖人工设计的固定特征,而是直接在原子与化学键构成的图上做消息传递,让模型自己学习局部化学环境与全局性质之间的映射关系。
一、分子图表示的核心概念
一个分子可以形式化地写成图 G = (V, E)。其中节点集合 V 对应原子,边集合 E 对应化学键。工程实现时,通常把它展开成三组张量:
- 节点特征矩阵 X:形状
[N, d_node],N 为原子数。每一行描述一个原子,常用特征包括原子序数、元素类型 one-hot、杂化方式(sp/sp²/sp³)、形式电荷、是否在环上、手性标签、隐式氢数量等。 - 边索引 edge_index:形状
[2, E],用两行分别存“起点原子下标”和“终点原子下标”。无向键通常写成双向边,即一条键在 edge_index 中出现两次。 - 边特征矩阵 E_feat:形状
[E, d_edge],描述键的类型(单键/双键/三键/芳香键)、是否共轭、是否在环内、立体化学标记等。
此外还有两个常被忽略但很关键的输入:全局特征向量 u(如分子量、可旋转键数、拓扑极性表面积)和批次向量 batch(把多个小分子拼成一张大图时,标记每个原子属于哪个分子)。以阿司匹林(C₉H₈O₄)为例,若只保留重原子,N = 13,E(双向计)约为 26,节点特征维度取 32,则 X 为 [13, 32],edge_index 为 [2, 26]。这个规模说明:分子图普遍很小,但数量极大,因此工程瓶颈往往不在单图计算,而在数据吞吐与批处理效率。
二、训练与验证工程的整体流程
从原始 SMILES 到可交付模型,工程链路可以拆成六个环节,每个环节都有明确的输入输出与失败模式:
- 数据获取与清洗:从 ChEMBL、PubChem、ZINC 或内部实验记录读取 SMILES 与标签。需要处理盐离子剥离、互变异构体标准化、重复条目去重、标签单位统一(如 IC₅₀ 统一为 nM 并取 −log 转换)。
- 图构建与特征化:用 RDKit 解析 SMILES,生成原子/键特征,输出 PyG 的
Data对象或 DGL 图。此步要固定特征字典顺序,否则训练与推理会错位。 - 数据划分:随机划分在分子性质预测中容易高估性能,因为相似骨架会同时落入训练与测试集。更严谨的做法是 Scaffold 划分(按 Murcko 骨架分组)或时间划分。
- 模型训练:选择 GIN、GCN、MPNN 或 AttentiveFP 等消息传递骨架,配置层数、隐藏维度、readout 方式、学习率与早停策略。
- 验证与评估:回归任务看 RMSE、MAE、R²;分类任务看 ROC-AUC、PR-AUC、F1。同时必须报告划分方式与随机种子,否则指标不可比。
- 导出与推理:固化特征化代码与权重,保证线上推理与训练时的图构建逻辑完全一致。
下面这张图把上述链路与每步的关键产物、判断依据放在一起,便于对照理解各环节之间的依赖关系。
三、典型应用场景
分子图模型并非只用于“预测一个数”。按任务形态大致可分四类:
- 分子性质回归:水溶性(logS)、脂水分配系数(logP)、毒性(LD₅₀)、ADMET 端点。输出是连续值,损失常用 MSE 或 Huber。
- 活性分类:某靶点是否活性、是否 hERG 阻断。输出是概率,损失用 BCE,评估看 ROC-AUC。
- 分子生成与优化:以图生成为核心,结合强化学习或条件 VAE,在满足约束下优化性质。
- 反应预测:把反应物与产物建模为图到图的映射,用于逆合成与产率预测。
不同场景对工程的要求差异明显:回归任务更关注标签噪声与划分方式,分类任务更关注类别不平衡与阈值选择,生成任务则更关注采样效率与化学有效性。
四、工具链概览
当前主流工具链可以按职责分层理解:
| 层级 | 常用工具 | 职责 |
|---|---|---|
| 化学信息学 | RDKit、Open Babel | SMILES 解析、标准化、特征生成 |
| 图深度学习框架 | PyTorch Geometric、DGL | 图数据结构、消息传递层、批处理 |
| 训练与实验管理 | PyTorch Lightning、Weights & Biases | 训练循环、日志、超参记录 |
| 分子专用库 | DeepChem、Chemprop | 预置模型、数据加载、基准评测 |
| 部署 | ONNX Runtime、TorchServe | 模型导出与在线推理 |
一个最小可运行示例(PyG 风格)如下,用于说明图构建与训练入口的衔接方式:
from rdkit import Chem
import torch
from torch_geometric.data import Data
def mol_to_graph(smiles):
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return None
atom_feats, edge_index, edge_feats = [], [], []
for atom in mol.GetAtoms():
atom_feats.append([
atom.GetAtomicNum(),
atom.GetDegree(),
int(atom.GetIsAromatic()),
atom.GetFormalCharge(),
])
for bond in mol.GetBonds():
i, j = bond.GetBeginAtomIdx(), bond.GetEndAtomIdx()
edge_index += [[i, j], [j, i]]
bt = bond.GetBondTypeAsDouble()
edge_feats += [[bt], [bt]]
x = torch.tensor(atom_feats, dtype=torch.float)
ei = torch.tensor(edge_index, dtype=torch.long).t().contiguous()
ea = torch.tensor(edge_feats, dtype=torch.float)
return Data(x=x, edge_index=ei, edge_attr=ea)
g = mol_to_graph("CC(=O)Oc1ccccc1C(=O)O") # 阿司匹林
print(g)
# Data(x=[13, 4], edge_index=[2, 26], edge_attr=[26, 1])
这段代码的关键点在于:特征字典顺序必须固定,且无向键要双向登记,否则消息传递时会出现信息不对称。打印出的 x=[13, 4] 与 edge_index=[2, 26] 正是前文阿司匹林示例的规模,可作为图构建是否正确的第一道自检。