基础回顾(一):分子结构与图表示
做药物发现的朋友都知道,分子本质上就是一个图。你想想看,原子是节点,化学键是边——这不就是天然的图结构吗?我刚开始接触这个领域的时候,觉得把分子画成图是多此一举。后来踩了不少坑才明白,图的视角能帮我们用数学语言描述分子,这才是GNN能干QSAR的关键。
分子图构建的基本概念
一个分子图包含两个核心元素:节点和边。节点就是原子,边就是化学键。但这里有个细节要注意:不是所有键都一样。单键、双键、三键、芳香键,它们的性质完全不同。
我在项目中遇到过这样的情况:直接用0/1表示有没有键,结果模型死活学不好。后来加上了键的类型编码,准确率直接提升了8%。所以说,特征编码不是小事。
| 概念 | 定义 | 我的备注 |
|---|---|---|
| 节点(原子) | 分子中的每个原子 | 注意区分重原子和氢,氢原子经常被省略 |
| 边(化学键) | 原子间的连接关系 | 单键、双键、三键、芳香键要分开编码 |
| 邻接矩阵 | 描述节点连接关系的矩阵 | 对称矩阵,对角线为0 |
| 特征矩阵 | 每个节点的属性向量 | 原子类型、杂化方式、电荷等 |
为什么要构建邻接矩阵?因为GNN的传播机制就是靠它来聚合邻居信息的。没有邻接矩阵,图卷积就没法做。嗯,这里要注意邻接矩阵的稀疏性——一个分子通常只有几十个原子,但完全连接的矩阵会有大量0。用稀疏矩阵存储是基本操作。
原子与键的特征编码
特征编码,说白了就是把原子和键的各种性质转化为向量。我常用的原子特征包括:
- 原子类型:C、N、O、S、F等,一般用one-hot编码
- 原子度数:连接的邻居数量
- 氢原子数:隐式或显式氢的数量
- 轨道杂化:sp、sp2、sp3等
- 芳香性:原子是否在芳香环中
- 形式电荷:原子的净电荷
- 原子质量:整数原子质量
键的特征相对简单一些:
- 键的类型:单键(1)、双键(2)、三键(3)、芳香键(1.5),我习惯用one-hot
- 共轭性:键是否在共轭体系中
- 环状性:键是否属于环
- 立体构型:Z/E构型等
你可能会问:这些特征怎么提取?用RDKit就行。
from rdkit import Chem
from rdkit.Chem import AllChem
mol = Chem.MolFromSmiles('c1ccccc1C(=O)O')
# 获取原子特征
for atom in mol.GetAtoms():
features = [
atom.GetAtomicNum(), # 原子序数
atom.GetDegree(), # 度数
atom.GetTotalNumHs(), # 氢原子数
atom.GetHybridization(), # 杂化方式
atom.GetIsAromatic(), # 芳香性
atom.GetFormalCharge(), # 形式电荷
atom.GetMass() # 原子质量
]
print(features)
# 获取键特征
for bond in mol.GetBonds():
bond_type = bond.GetBondType()
features = [
bond.GetBeginAtomIdx(),
bond.GetEndAtomIdx(),
int(bond_type), # 键类型
bond.GetIsConjugated(), # 共轭性
bond.IsInRing() # 环状性
]
print(features)
这里要注意:每种特征最好归一化到0-1范围。我曾经在特征编码这一步偷懒,直接把原子序数当特征丢进去...结果模型完全收敛不了。为什么呢?因为原子序数差的量级太大(1到80+),而度数只有1到6,模型肯定学偏。
经验分享:我常用的原子特征维度是74维(RDKit默认的摩根特征)+ 自定义的原子属性。代码中用
表示,其实是对特征维度的标准化处理。你可以在预处理阶段统一一下。
小技巧:用RDKit的GetMorganFingerprintAsBitVect可以直接拿到固定长度的特征向量,省去手动编码的麻烦。不过注意,摩根指纹是全局特征,不是节点级别的特征。如果你要每个原子的特征,还是得用上面的方法逐原子提取。
避坑指南:我曾经在一个溶解度预测任务里,没处理好键的类型编码,结果模型把单键和双键当成一样的。后来分析发现,模型根本学不到π电子云的信息。从此以后,我对键特征特别小心——one-hot和数值混合编码是最稳妥的方案。
从分子到图的完整映射
我们来看一个实际例子。水分子H₂O:
# 水分子示例
smiles = 'O'
mol = Chem.MolFromSmiles(smiles)
# 节点:3个原子(1个O,2个H)
# 节点特征矩阵 shape: (3, feature_dim)
# 比如氧的原子序数=8,氢的原子序数=1
# 边:2个O-H单键
# 邻接矩阵 shape: (3, 3)
# [[0, 1, 1],
# [1, 0, 0],
# [1, 0, 0]]
# 边特征矩阵 shape: (2, edge_feature_dim)
# 两个键都是单键,one-hot编码为 [1,0,0,0]
这个映射过程看起来简单,但扩展到几十个原子的分子就不一样了。我在做活性预测项目时,经常处理500+原子的分子。邻接矩阵直接变成500x500,特征矩阵也大得吓人。这时候必须用批量处理和填充(padding)技术。
总结一下,分子图构建就是两件事:提取原子特征、提取键特征、拼成邻接矩阵和特征矩阵。这两件事做好了,GNN才能发挥作用。我在做QSAR项目时,光是特征编码就迭代了5个版本,才找到通用性最强的那一套。
嗯,这里要强调一点:特征编码没有银弹。不同的任务(溶解度 vs 毒性 vs 活性)对特征的敏感度不一样。我的建议是:先跑一个基线模型,然后再逐步加特征,看看每个特征对性能的影响。