2. 分子图表示基础
在分子性质预测任务中,图神经网络之所以能替代传统指纹方法,核心在于它把分子显式地建模为一张图。理解这张图的构成,是后续所有模型设计、特征工程与数据管线搭建的前提。本节围绕“分子图到底由什么组成、如何用代码把它构造出来、以及不同工具与数据格式之间如何对应”展开。
一、分子图的四个基本要素
一张分子图 G = (V, E) 由节点集合 V 和边集合 E 构成。在分子语境下,约定如下:
- 节点(Node):对应一个重原子(通常为 C、N、O、S、P、卤素等),氢原子默认被隐式处理,不单独建节点。
- 边(Edge):对应一条化学键。单键、双键、三键、芳香键在图中都是边,但通过边特征区分键级。
- 节点特征(Node Feature):描述该原子的属性,如元素种类、原子序数、度、形式电荷、杂化方式、是否在环上、是否芳香、手性标签等。
- 边特征(Edge Feature):描述该键的属性,如键类型(单/双/三/芳香)、是否共轭、是否在环上、立体化学标记等。
需要特别强调的是:分子图是无向图,但为了便于消息传递实现,通常存储为双向边——即一条 C–O 键会同时写入 (C→O) 和 (O→C) 两条有向边,二者共享同一组边特征。这一点在后续实现消息传递时必须保持一致,否则会出现信息只沿一个方向流动的错误。
二、邻接矩阵与特征矩阵
把图写成矩阵形式,是接入深度学习框架的关键一步。设分子有 N 个重原子:
- 邻接矩阵 A:形状
N×N。若原子 i 与 j 之间有键,则A[i][j] = 1,否则为 0。若需要区分键级,可扩展为带权邻接矩阵,用 1/2/3/1.5 表示单/双/三/芳香键。 - 节点特征矩阵 X:形状
N×d_node,第 i 行是原子 i 的特征向量。 - 边特征矩阵 E:形状
M×d_edge,M 为有向边数,每行对应一条边的特征。
以乙醇 CCO 为例,3 个重原子,2 条化学键。邻接矩阵为:
A = [[0, 1, 0],
[1, 0, 1],
[0, 1, 0]]
可以看到 A 是对称的,且对角线为 0(无自环)。若采用双向边存储,边列表长度为 4,即 (0→1)、(1→0)、(1→2)、(2→1)。
三、从 SMILES 到分子图的构建流程
实际工程中最常见的输入是 SMILES 字符串。构建分子图的标准流程是:先用化学信息学工具解析 SMILES 得到带显式键级与原子属性的分子对象,再遍历原子与键,抽取特征并组装成图结构。下面给出一个基于 RDKit 的最小可运行示例,节点特征取原子序数、度、形式电荷、是否芳香、杂化方式共 5 维,边特征取键类型 one-hot 共 4 维:
from rdkit import Chem
import numpy as np
def mol_to_graph(smiles):
mol = Chem.MolFromSmiles(smiles)
if mol is None:
raise ValueError("invalid SMILES")
# 节点特征
node_feats = []
for atom in mol.GetAtoms():
node_feats.append([
atom.GetAtomicNum(),
atom.GetDegree(),
atom.GetFormalCharge(),
int(atom.GetIsAromatic()),
int(atom.GetHybridization()),
])
# 边特征:单/双/三/芳香
bond_map = {Chem.BondType.SINGLE:0, Chem.BondType.DOUBLE:1,
Chem.BondType.TRIPLE:2, Chem.BondType.AROMATIC:3}
edge_index, edge_feats = [], []
for bond in mol.GetBonds():
i, j = bond.GetBeginAtomIdx(), bond.GetEndAtomIdx()
f = [0,0,0,0]
f[bond_map[bond.GetBondType()]] = 1
edge_index += [[i,j],[j,i]]
edge_feats += [f, f]
return np.array(node_feats), np.array(edge_index).T, np.array(edge_feats)
对 "CCO" 调用该函数,会得到 node_feats 形状 (3,5)、edge_index 形状 (2,4)、edge_feats 形状 (4,4)。这三个数组就是后续 PyTorch Geometric 或 DGL 中 Data 对象的核心字段。
四、常见工具与数据格式对照
不同工具对同一张分子图的表达方式不同,但语义一致。下表给出常见组合的对应关系,便于在管线中做转换:
| 工具/格式 | 节点表示 | 边表示 | 典型用途 |
|---|---|---|---|
| RDKit Mol | Atom 对象 | Bond 对象 | 解析、清洗、特征抽取 |
| PyG Data | x: [N, d] | edge_index: [2, M], edge_attr: [M, d] | GNN 训练 |
| DGL DGLGraph | ndata['h'] | edata['e'], 双向边 | GNN 训练 |
| NetworkX Graph | 节点属性 dict | 边属性 dict | 可视化、图算法 |
| SMILES | 隐式 | 隐式 | 存储、交换 |
| MOL / SDF | 原子块 | 键块 | 含 3D 坐标的交换格式 |
工程上常见的做法是:以 SMILES 或 SDF 作为原始存储,用 RDKit 做解析与特征抽取,再转换为 PyG 的 Data 对象缓存为 .pt 文件,训练时直接加载。这样既保留了化学语义,又避免了每次训练重复解析。
五、构建时的常见陷阱
陷阱一:氢原子处理不一致。有的工具默认隐式氢,有的显式加氢。若训练集与推理集处理方式不同,节点数会变化,模型输入维度对不上。建议统一使用隐式氢,并在文档中明确记录。
陷阱二:芳香键与凯库勒式混用。RDKit 默认会把苯环解析为芳香键,但若先 Kekulize 再取键级,会得到交替的单双键。两种表示对模型是不同输入,必须全流程统一。
陷阱三:边方向重复计数。若同时存了 (i→j) 和 (j→i),在聚合时要注意不要重复累加,或在消息函数中除以 2。PyG 的 edge_index 默认就是双向的,需在模型侧确认。
下面这张图把“SMILES → 分子对象 → 节点/边特征 → 邻接矩阵与边索引 → 框架张量”的转换链路,以及各环节的关键字段并列展示,便于对照实现:
把这张图与前面的代码对照阅读,可以看到每个数组的形状与来源。实际搭建数据管线时,建议先用 2~3 个已知分子(如乙醇、苯、乙酸)手工核对节点数、边数与邻接矩阵,确认无误后再批量处理数据集。这一步的验证成本很低,却能避免后续训练中大量难以定位的维度错误。