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 MolAtom 对象Bond 对象解析、清洗、特征抽取
PyG Datax: [N, d]edge_index: [2, M], edge_attr: [M, d]GNN 训练
DGL DGLGraphndata['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 → 分子对象 → 节点/边特征 → 邻接矩阵与边索引 → 框架张量”的转换链路,以及各环节的关键字段并列展示,便于对照实现:

分子图构建链路:从 SMILES 到框架张量 ① 原始输入 SMILES / SDF 例:CCO ② 化学解析 RDKit Mol Atom / Bond 对象 ③ 特征抽取 节点特征 X [N,5] 边特征 E [M,4] ④ 图结构组装 邻接矩阵 A [N,N] edge_index [2,M] 乙醇 CCO 的图表示(N=3,M=4 有向边) C₀ C₁ O₂ 单键 单键 邻接矩阵 A 0 1 0 1 0 1 0 1 0 edge_index(双向) [0,1,1,2] [1,0,2,1] 源节点 / 目标节点 PyG Data 对象 x: [3, 5] edge_index: [2, 4] edge_attr: [4, 4] 关键一致性:氢处理方式、芳香键表示、边方向约定,三者需在全流程统一

把这张图与前面的代码对照阅读,可以看到每个数组的形状与来源。实际搭建数据管线时,建议先用 2~3 个已知分子(如乙醇、苯、乙酸)手工核对节点数、边数与邻接矩阵,确认无误后再批量处理数据集。这一步的验证成本很低,却能避免后续训练中大量难以定位的维度错误。