2、分子图的表示与构建
说到分子图,说白了就是把分子结构翻译成图神经网络能啃的数据格式。这个过程听起来直白,但里头的门道不少。我刚开始做分子图项目时,觉得不就是把原子当节点、键当边嘛,结果特征编码没做对,模型训了好几天都不收敛——后来才发现是手性特征漏了。这一章我们就聊聊分子图的数据结构、邻接矩阵,还有节点和边特征到底该怎么搞。
2.1 分子图数据结构
图数据结构是GNN的基石。一个分子图定义为 G = (V, E),其中 V 是节点集合(原子),E 是边集合(化学键)。我习惯用 N 表示原子数,M 表示键数。每个节点和边都可以携带特征向量。
你想想看,一个水分子 H₂O,3个原子、2条键,在图上就是3个节点、2条边。数据结构上,我们需要同时存三样东西:节点特征矩阵、邻接矩阵、边特征矩阵。缺一不可。
我在项目中踩过坑——只用邻接矩阵,忽略了边特征,模型死活学不到键级信息。后来加上键特征,效果直接提了十几个点。所以别偷懒,边特征一定要带上。
分子图数据结构三要素
| 组成部分 | 物理意义 | 数学表示 | 维度说明 |
|---|---|---|---|
| 节点集 V | 原子 | 节点特征矩阵 X | N × d_node |
| 边集 E | 化学键 | 邻接矩阵 A + 边特征矩阵 E | N×N, M×d_edge |
| 全局属性(可选) | 分子整体性质 | 全局特征向量 u | 1 × d_global |
具体到代码实现,我常用 Python + NumPy 或者 PyTorch Geometric 来组织数据。下面是一个最简的分子图数据结构示意:
# 甲烷 CH4:5个原子,4条 C-H 键
N = 5
M = 4
d_node = 4 # 原子特征维度
d_edge = 2 # 键特征维度
# 节点特征矩阵 shape (5, 4)
X = np.array([
[6, 4, 0, 0], # C
[1, 1, 0, 0], # H1
[1, 1, 0, 0], # H2
[1, 1, 0, 0], # H3
[1, 1, 0, 0], # H4
])
# 邻接矩阵 shape (5, 5) 对称
A = np.array([
[0, 1, 1, 1, 1],
[1, 0, 0, 0, 0],
[1, 0, 0, 0, 0],
[1, 0, 0, 0, 0],
[1, 0, 0, 0, 0],
])
# 边特征矩阵 shape (4, 2) 按 (u,v) 顺序排列
E = np.array([
[1, 0], # C-H1 单键
[1, 0], # C-H2 单键
[1, 0], # C-H3 单键
[1, 0], # C-H4 单键
])
2.2 邻接矩阵
邻接矩阵 A 是分子拓扑结构的数学化身。A[i][j] = 1 表示原子 i 和 j 之间有化学键,0 表示没有。对于无向分子图,A 是对称矩阵。
但这里有个实际问题——分子图通常很稀疏。一个分子几百个原子,邻接矩阵的稠密度可能不到 1%。我遇到过有人直接把稠密矩阵扔进 GPU,显存直接爆了。所以 稀疏邻接矩阵 是必选项。
常见的稀疏存储格式有 COO(坐标列表)和 CSR(压缩行存储)。在 GNN 框架里,PyG 默认用 COO 格式:
# COO 格式存储邻接矩阵
# 只存非零元素的 (行, 列) 对
edge_index = [
[0, 0, 0, 0, 1, 2, 3, 4], # 源节点
[1, 2, 3, 4, 0, 0, 0, 0], # 目标节点
]
# shape (2, 8) 表示 8 条有向边(无向边存双向)
为什么存双向?因为 GAT 注意力需要每个节点能看到邻居,单向边会让信息流断掉。这是我做第一个分子模型时踩的坑——只存了单向边,注意力权重怎么算都不对。
邻接矩阵必须对称存储。分子键是无向的,但消息传递需要双向流动。请确保 edge_index 中每条无向边都存了 (u,v) 和 (v,u) 两个方向。
另外要注意自环(self-loop)。大部分 GNN 模型会在消息聚合时把节点自身的信息也加进来,所以邻接矩阵对角线通常设为 1。不过这事取决于具体模型,GAT 有的版本加自环,有的不加,你得看实现。
2.3 节点与边特征
特征工程在分子图里比图像领域更讲究。原子和键的特征选得好不好,直接决定模型能不能学到物理化学规律。
节点特征(原子特征),我个人常用这几种:
- 原子类型:C、H、O、N、S 等,用 one-hot 编码
- 原子序数:整数,6 号 C、8 号 O,直接作为数值特征
- 价电子数:4、1、6 等,和化学性质强相关
- 形式电荷:-1、0、+1,对离子型分子很重要
- 手性标记:R/S 或无,药物分子必须带这个
- 杂化方式:sp、sp²、sp³,影响分子空间构型
- 氢键供体/受体:0 或 1,生物活性预测常用
边特征(键特征),我一般至少包含这些:
- 键型:单键、双键、三键、芳环键,one-hot 编码
- 共轭标记:是否在共轭体系内,0/1
- 环内标记:键是否在环上,0/1
- 键级:1.0、1.5、2.0 等,浮点数
我记得有一次做毒性预测,模型在含芳环的分子上表现很差。排查后发现是边特征没区分芳环键和普通双键。加上芳环标记后,准确率从 82% 提到 89%。所以特征维度不要省,该加的都得加。
特征归一化。数值型特征(原子序数、价电子数)最好缩放到 [0,1] 或标准化,避免大数值特征 dominate 梯度。我习惯用 Min-Max 归一化,简单稳定。
下面是一个特征编码的代码片段,展示了如何把原子类型转为 one-hot:
# 原子类型 one-hot 编码示例
atom_types = ['C', 'H', 'O', 'N', 'S', 'P', 'F', 'Cl', 'Br', 'I']
type_to_idx = {t: i for i, t in enumerate(atom_types)}
def encode_atom_type(atom_symbol):
"""将原子符号转为 one-hot 向量"""
vec = [0.0] * len(atom_types)
idx = type_to_idx.get(atom_symbol, -1)
if idx >= 0:
vec[idx] = 1.0
return vec
# 示例:水分子 O 和 H
print(encode_atom_type('O')) # [0,0,1,0,0,0,0,0,0,0]
print(encode_atom_type('H')) # [0,1,0,0,0,0,0,0,0,0]
全部特征拼起来,每个节点得到一个向量(比如 20 维),每条边一个向量(比如 6 维)。把这些堆成矩阵 X 和 E,就得到了 GNN 的完整输入。
最后我想说一句:分子图的表示没有绝对的标准答案。不同任务侧重点不同,特征选择也要灵活调整。我建议你从一个小而全的特征集开始,逐步剪枝,找到最适合你任务的那一套。