第4章 分子特征工程:原子类型、键类型、手性、电荷等特征
好,到了本章,我们正式进入「把手弄脏」的阶段。
很多同学刚开始接触图神经网络分子建模时,总觉得模型够「深」就够了。但实际上,我见过太多项目——包括我自己早期踩过的坑——模型死活不收敛,最后发现不是网络结构的问题,而是输入特征根本没表达清楚分子信息。
说白了,分子的图结构只是骨架。真正让GNN理解化学的,是每个节点和每条边上的特征向量。这一章我们就来聊这个。
4.1 原子特征:拿什么描述一个原子?
我先问个问题。如果让你给一个碳原子编码,你会写哪些属性?
元素种类?肯定要。但只有这个远远不够。在分子里,同一个碳原子可以连四个单键,也可以连一个双键。它可以是sp³杂化,也可以是sp²。这些信息如果不喂给网络,模型就只能瞎猜。
现在行业内一般推荐的特征组合如下:
| 特征类别 | 具体项 | 编码方式 | 维度 |
|---|---|---|---|
| 元素类型 | H, C, N, O, F, P, S, Cl, Br, I 等 | One-hot | 通常 12~20 |
| 原子度 | 连接的原子数(0~6) | One-hot | 7 |
| 形式电荷 | -1, 0, +1 等 | One-hot | 3~5 |
| 自由基电子数 | 0, 1, 2 | One-hot | 3 |
| 杂化状态 | sp, sp², sp³ | One-hot | 3 |
| 芳香性 | 是否在芳香环中 | 二进制 | 1 |
| 氢原子数 | 连接的氢个数(0~4) | One-hot | 5 |
| 手性标记 | R/S 或无 | One-hot | 3 |
| 在环中 | 是否在环上 | 二进制 | 1 |
你想想看,这加起来也就四十多维度。用RDKit几行代码就能全部算出来。
from rdkit import Chem
from rdkit.Chem import rdMolDescriptors
mol = Chem.MolFromSmiles('CC(=O)O') # 乙酸
for atom in mol.GetAtoms():
feats = []
feats.append(atom.GetAtomicNum()) # 元素序号
feats.append(atom.GetDegree()) # 度
feats.append(atom.GetFormalCharge()) # 形式电荷
feats.append(atom.GetNumRadicalElectrons())# 自由基电子
hybrid = str(atom.GetHybridization()) # 杂化
feats.append(1 if 'SP2' in hybrid else 0)
feats.append(1 if atom.GetIsAromatic() else 0) # 芳香性
print(f"原子 {atom.GetIdx()}: {feats}")
4.2 键特征:边信息不能丢
图神经网络里,节点之间的边不只是「连或不连」。键的类型、是否共轭、是否在环里——这些都是决定性信息。
拿单键和双键来说。在分子里,C-C单键可以自由旋转,C=C双键不行。这个差异,在力场计算和构象预测里是致命的。
我一般用下面这组键特征:
- 键类型:单键、双键、三键、芳香键,one-hot编码,4维
- 共轭:是否共轭,二进制,1维
- 环结构:键是否在环中,二进制,1维
- 立体构型:顺/反/无,one-hot,3维
加起来就9维。轻量,但够用。
RDKit的提取方式也很直接:
for bond in mol.GetBonds():
btype = bond.GetBondTypeAsDouble()
# 1.0 单键, 2.0 双键, 3.0 三键, 1.5 芳香键
feats = []
feats.append(1.0 if btype == 1.0 else 0)
feats.append(1.0 if btype == 2.0 else 0)
feats.append(1.0 if btype == 3.0 else 0)
feats.append(1.0 if btype == 1.5 else 0)
feats.append(1.0 if bond.GetIsConjugated() else 0)
feats.append(1.0 if bond.IsInRing() else 0)
# 立体
stereo = bond.GetStereo()
feats.append(1.0 if stereo == Chem.rdchem.BondStereo.STEREOZ else 0)
feats.append(1.0 if stereo == Chem.rdchem.BondStereo.STEREOE else 0)
print(f"键 {bond.GetIdx()}: {feats}")
4.3 手性特征:R/S构型怎么编码?
做药物设计项目时,我吃过手性的亏。
有一次模型预测某个分子的生物活性,结果总是比实验值低一个数量级。查了好久才发现,RDKit读入的SMILES不带立体信息,所有手性中心全被当成了平面结构。我赶紧补上了CIP标记,模型才恢复正常。
手性标记通常分成两级:
- 是否存在手性中心:二进制标记,1维
- R/S或None:one-hot编码,3维
在RDKit中:
from rdkit.Chem import rdMolDescriptors
mol = Chem.MolFromSmiles('C[C@H](O)CO')
for atom in mol.GetAtoms():
chiral_tag = atom.GetChiralTag()
if chiral_tag == Chem.rdchem.ChiralType.CHI_TETRAHEDRAL_CW:
print(f"原子 {atom.GetIdx()}: 手性 R")
elif chiral_tag == Chem.rdchem.ChiralType.CHI_TETRAHEDRAL_CCW:
print(f"原子 {atom.GetIdx()}: 手性 S")
4.4 电荷与其它重要特征
形式电荷这个东西,我建议分两种情况来看:
- 如果分子是中性,形式电荷基本都是0,可以不做特征
- 如果分子是离子(比如[NH4]+、[CH3COO]-),那形式电荷就是非常强的信号
另外,还有一个容易被忽略的特征——隐式氢数。一个碳连三个氢还是连一个氢,完全决定了它的反应活性。
我习惯把隐式氢数当作单独一组特征,而不是直接用原子序数推导:
total_h = atom.GetTotalNumHs() # 隐式 + 显式氢
# 输出 0~4,做 one-hot
4.5 特征组合的工程实践
说了这么多,最终的特征向量长什么样?
举一个具体的例子——苯环上的碳原子:
原子特征 (43维):
[C one-hot: 0,1,0,0,0,...]
[度: 0,0,1,0,0,0,0] # degree=3
[形式电荷: 0,1,0]
[自由基: 1,0,0]
[杂化: 0,1,0] # sp²
[芳香性: 1]
[隐式氢: 0,1,0,0,0]
[手性: 1,0,0,0]
[在环中: 1]
键特征 (9维):
[单,双,三,芳香: 0,0,0,1]
[共轭: 1]
[在环中: 1]
[顺/反/无: 0,0,1]
整个分子打包成一个异构图,节点特征矩阵 shape 为 (N_atoms, 43),边特征矩阵为 (N_bonds, 9)。
4.6 本章知识结构图
4.7 特征预处理的一些坑
最后聊几个实战中容易翻车的地方: