分子图构建原理
说实话,分子图这个概念我第一次听的时候觉得挺玄乎的。图?不就是原子连原子嘛。但真正动手把分子转成 GNN 能吃的数据时,才发现里头有不少细节讲究。我个人习惯先把分子拆成两个最核心的东西:原子(节点)和化学键(边)。然后呢,再分别给它们配上对应的矩阵——特征矩阵和邻接矩阵。
原子作为节点——节点特征怎么定?
每个原子就是一个节点。咱们写个分子图构建函数,第一步就是拿原子序号和对应的属性。举个最简单的例子,甲烷 CH₄:一个碳连四个氢。碳和氢都是节点,总共 5 个节点。
那节点特征是什么?我早期踩过一个坑:只把原子序数塞进去,结果模型完全分不清同分异构体。后来我加了电荷、手性标签、杂化类型、度(连接的键数)这些特征,效果立马不一样。
节点特征常用字段
- 原子序数(整数,1~118)
- 形式电荷(默认 0)
- 手性中心(True/False 或 CHI_TETRAHEDRAL_CCW 等)
- 杂化类型(SP, SP², SP³ …)
- 在环中(True/False)
- 连接的氢原子数(隐含氢)
构建特征矩阵的代码片段大概长这样:
import numpy as np
from rdkit import Chem
def atom_feature_vector(atom):
features = [
atom.GetAtomicNum(),
atom.GetFormalCharge(),
int(atom.GetHybridization()),
atom.GetTotalDegree(),
int(atom.IsInRing())
]
return np.array(features, dtype=np.float32)
# 对甲烷分子
mol = Chem.MolFromSmiles('C')
atom_features = [atom_feature_vector(atom) for atom in mol.GetAtoms()]
X = np.stack(atom_features) # shape: (num_atoms, feature_dim)
你看,每个原子变成一个固定长度的向量。不同分子的原子数不一样,特征矩阵的行数也就不一样——这就是 GNN 能处理变长输入的基础。
化学键作为边——邻接矩阵藏着的信息
原子之间靠化学键相连。在分子图中,每条化学键就是一条边。我习惯用无向图表示,因为键没有方向性。邻接矩阵 A 是一个 N×N 的 0-1 矩阵,A[i][j]=1 表示原子 i 和原子 j 之间有一条键。
不过有一点很重要:邻接矩阵默认只告诉你“有没有键”,但不告诉你“什么键”。我会在边特征里补充键的类型(单键、双键、芳香键……),但本章先只讲基础。
def build_adjacency_matrix(mol):
n = mol.GetNumAtoms()
A = np.zeros((n, n), dtype=np.int8)
for bond in mol.GetBonds():
i = bond.GetBeginAtomIdx()
j = bond.GetEndAtomIdx()
A[i, j] = 1
A[j, i] = 1 # 无向图
return A
A = build_adjacency_matrix(mol)
嗯,这里要注意:如果分子有环,邻接矩阵里就会出现环。比如苯环的邻接矩阵是六边形结构,对角线全是 0(自己连自己算自环,通常不加)。
特征矩阵与邻接矩阵——GNN 的两个输入基石
把前面两个矩阵合在一起,就构成了 GNN 的输入图。我一个标准的分子图可以写成:
- 节点特征矩阵 X ∈ ℝN×F,F 是每个原子的特征维度。
- 邻接矩阵 A ∈ {0,1}N×N,通常加上自环(A + I)以便消息传递时保留自身信息。
下表列出了几个常见分子的维度对比:
| 分子 | SMILES | 原子数 N | 特征维度 F | 邻接矩阵非零数 |
|---|---|---|---|---|
| 甲烷 | C | 5 | 5 | 4 |
| 乙醇 | CCO | 9 | 5 | 8 |
| 苯 | c1ccccc1 | 12 | 5 | 12 |
| 咖啡因 | CN1C=NC2=C1C(=O)N(C(=O)N2C)C | 24 | 5 | 26 |
你看,分子图的大小完全不一样。GNN 的好处就是甭管 N 是 5 还是 24,消息传递照样跑。
我的一个小建议
构建特征矩阵时,尽量把每个维度归一化到 [-1,1] 或 [0,1] 区间。原子序数从 1 到 118,直接扔进去权重会畸形。我习惯用 one-hot 对前 30 号元素编码,其余统一归为 “other”。
分子图构建流程全景
下面这张图总结了从分子结构到 GNN 输入的完整路线。我画了四个主要步骤:
我曾经踩过的坑
用 RDKit 构造邻接矩阵时,如果直接遍历原子序号,别忘了跳过 mol.GetBonds() 返回的 Bond 对象里的 BeginAtomIdx 和 EndAtomIdx 都是基于当前分子对象的索引,不是全局索引。同一原子在不同分子里索引肯定不一样,这没问题,但跨分子做 embedding 时要小心映射对齐。
做完这一步,你的分子图就齐活了。特征矩阵 X 和邻接矩阵 A 是 GNN 计算最原始的输入。后面所有的消息传递、图卷积、池化操作都在这个基础上构建。