分子图表示基础:原子、键、图结构
好了,咱们正式开始第二讲。上一章我们聊了ADMET是什么,以及为什么图神经网络(GNN)特别适合干这活。这一章,我们来夯实最底层的地基——分子图到底是怎么表示的。
说白了,GNN 本质上是在图上做运算。你给模型输入的不是SMILES字符串,也不是分子指纹,而是一个图。这个图由什么构成?核心就是两点:节点(原子)和边(化学键)。如果你连这个结构都构造错了,后面模型性能再好也是空中楼阁。
2.1 原子特征:节点里到底放了什么信息
我在项目中遇到过不少新手,一上来就问:“原子特征是不是就写个原子序号?”嗯,没那么简单。一个原子在图里作为一个节点,它需要携带足够多的信息,让模型知道“我是什么、我在哪、我周围环境如何”。
常用的原子特征可以分为几大类。我习惯把它们归纳成一张表,方便你做特征工程时对照。
| 特征类型 | 具体属性 | 我的个人习惯 |
|---|---|---|
| 身份特征 | 原子种类(C、N、O……)、周期、族、原子质量 | 必须加。One-hot 编码或嵌入都行。 |
| 拓扑特征 | 度数(相连原子数)、隐式氢数、形式电荷 | 强相关。氢数别漏,很多ADMET性质跟氢键有关。 |
| 杂化与立体特征 | 杂化方式(sp³/sp²/sp)、手性标记、是否是芳香环原子 | 你想想看,芳香性对Caco-2渗透性影响多大?必须加。 |
| 化学环境特征 | 是否在环内、是否在侧链、与电负性原子的距离 | 高级特征,算力允许再上。我在做CYP450预测时加过。 |
代码实现上,我一般用 RDKit 来提取特征。RDKit 的 rdkit.Chem.rdchem.Atom 对象把大部分属性都封装好了。关键代码片段大概长这样:
from rdkit import Chem
# 以SMILES为例,解析分子
mol = Chem.MolFromSmiles("c1ccccc1O")
atom = mol.GetAtomWithIdx(0) # 获取第0号原子
# 核心特征提取
atomic_num = atom.GetAtomicNum() # 编号,如6表示C
degree = atom.GetDegree() # 度数
implicit_h = atom.GetNumImplicitHs() # 隐式氢数
hybridization = atom.GetHybridization() # 杂化状态
is_in_ring = atom.IsInRing() # 是否在环内
注意,上面这些只是初步特征。你还需要把这些数值转化成向量。比如原子序数6,你会做成一个长度为约30的one-hot向量;杂化状态是枚举值,也需要one-hot。我习惯把所有标量特征直接拼接进节点特征矩阵,维度通常在40~100之间,看你选了多少属性。
2.2 键的特征:边不仅仅是连接关系
原子之间的边,如果只用一个布尔信号(连/不连)那就太亏了。化学键本身有丰富的信息,比如键级(单键、双键、三键、芳香键)、是否共轭、是否在环内。这些信息同样需要编码成药。
你可能会问:“键的特征对ADMET预测有用吗?”太有用了。举个例子:共轭键会影响紫外吸收(导致光毒性),双键数量会影响氧化代谢的难易程度。我在做hERG活性预测时,把键级和芳香性加入边特征后,模型的AUC直接提升了3个百分点。嗯,这个坑我踩过,所以现在特别重视。
常用的边特征有这些:
- 键的类型:single、double、triple、aromatic(芳香键)。最好用one-hot。
- 是否共轭:布尔值,但很多预测任务中很重要。
- 是否在环内:环上的键通常刚性更强,影响分子构象。
- 立体化学:顺反异构(如果涉及,通常用于手性分子的ADMET差异)。
提取键特征的代码也很直接:
bond = mol.GetBondWithIdx(0) # 获取第0号键
bond_type = bond.GetBondType() # 键类型枚举
is_conj = bond.GetIsConjugated() # 共轭标志
is_ring_bond = bond.IsInRing() # 环内标志
有了节点和边的特征,图的基本骨架就有了。下一步,就是把这些拼成一个标准的图结构数据,喂给GNN。
2.3 图结构的构建:从分子到图的无缝转换
现在你手里有了原子列表和键列表,接下来要做的就是构造一个邻接矩阵或者边索引列表。在GNN里,我们更常用后者——边索引(edge index),也就是一个大小为 [2, num_edges] 的张量,每一列表示一条边从哪个节点到哪个节点。
令我印象深刻的是,有次我在处理一个大型数据集时,边的方向搞反了,导致消息传递时特征流串位。所以我建议:如果你用的是无向图(默认就是无向图),记得把边复制一份,源-目标互换,保证双向都有。大多数GNN框架(如PyG、DGL)都会自动处理,但自己手工构建时候得小心。
DataLoader会自动帮你完成,你只需要保证每个Data对象包含x(节点特征)、edge_index(边索引)、y(标签)即可。
下面这个代码片段,完整演示了如何用RDKit把一个SMILES分子转成PyG支持的图数据:
import torch
from torch_geometric.data import Data
from rdkit import Chem
def mol_to_graph(smiles: str):
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return None
# 1. 提取原子特征(假设有个函数 get_atom_features)
atom_features = []
for atom in mol.GetAtoms():
f = get_atom_features(atom)
atom_features.append(f)
x = torch.tensor(atom_features, dtype=torch.float)
# 2. 提取边索引和边特征
edge_index = []
edge_attr = []
for bond in mol.GetBonds():
i = bond.GetBeginAtomIdx()
j = bond.GetEndAtomIdx()
edge_index.append([i, j])
edge_index.append([j, i]) # 无向图双向
bond_feat = get_bond_features(bond)
# 边特征双向相同
edge_attr.append(bond_feat)
edge_attr.append(bond_feat)
edge_index = torch.tensor(edge_index, dtype=torch.long).t().contiguous()
edge_attr = torch.tensor(edge_attr, dtype=torch.float)
# 3. 组装Data对象
data = Data(x=x, edge_index=edge_index, edge_attr=edge_attr)
return data
嗯,这里要注意:edge_index 的shape必须是 [2, num_edges],PyG 对这个很严格。另外 edge_attr 如果是可选的,但加上远比不加好——能让消息传递层学到更丰富的键环境。
2.4 完整图结构示例与SVG
为了让你更直观地理解,我画了一张图来展示一个简单分子(对甲酚,HOC₆H₄CH₃)是如何表示成图数据的。SVG 里我把原子编号标出来了,每条边都有双向箭头,嗯,这就是模型看到的原始输入。
在图中,每一个原子都被赋予一个特征向量,每一条边也同样带着键特征。GNN 的“消息传递”就是沿着这些双向边,把邻居的特征传播到中心节点,然后更新节点表示。你想想,如果我们能让每个原子感知到它周围几层原子的信息(通过多层网络),那么整个分子的全局性质(比如 logD、溶解度、毒性)就能被模型捕捉到。
2.5 常用工具库与数据格式
做ADMET落地,你得熟悉几个主流工具库。我用得最多的组合是:RDKit + PyTorch Geometric (PyG) 或 DGL。RDKit 负责从 SMILES 或 SDF 里提取分子级信息,PyG/DGL 负责构造和训练图神经网络。
一个典型的分子数据格式是这样的——以 PyG 的 Data 对象为例:
Data(
x=[num_nodes, node_feat_dim], # 节点特征
edge_index=[2, num_edges], # 边索引(双向)
edge_attr=[num_edges, edge_feat_dim], # 边特征(可选)
y=[num_tasks] # 标签(如hERG值、Caco-2值)
)
如果我用DGL,图结构的表示稍有不同,但本质一样:
import dgl
g = dgl.graph((src_ids, dst_ids)) # 传入边源-目标节点编号
g.ndata['feat'] = node_features # 节点特征
g.edata['feat'] = edge_features # 边特征
scatter、segment)能大幅提升训练速度。我曾见过有团队强行用稠密矩阵存百万分子,内存直接爆了——这显然不是聪明的做法。
2.6 总结:这一章你需要记住的核心
- 分子图由原子节点和化学键边构成,两者都包含丰富的化学特征。
- 节点特征包括原子种类、度数、杂化、电荷、环信息等;边特征包括键级、共轭、环内标志等。
- 图必须表示为无向图,边索引双向,才能正确进行消息传递。
- 实际编码时,先用 RDKit 提取原子和键的原始信息,再转成 PyG/DGL 的图数据对象。
- 别忘了批处理——不同分子图节点数不同,利用框架的
DataLoader自动拼图。