3. 特征工程:分子描述符、指纹、图特征

老实说,特征工程才是ADMET预测模型里最见功力的环节。模型架构可以抄,超参数可以调,但特征选不好,数据质量差,后面再怎么折腾也是白搭。我在这个领域摸爬滚打了好几年,踩过的坑比吃过的盐还多,今天就把这些经验掰开了揉碎了讲给你听。

3.1 分子描述符:传统但不过时

分子描述符是什么?说白了,就是把分子的化学信息转化成数字。比如分子量、LogP、氢键供体/受体个数、可旋转键数等等。这些数字听着简单,但它们直接反映了分子的基本物理化学性质。

核心要点: 描述符的质量决定了模型性能的天花板。数据再干净,模型再复杂,如果描述符选得不对,结果也不会好。

我记得刚入行时做过一个Caco-2渗透性预测项目。当时选了一堆描述符,什么拓扑极性表面积、摩尔折射率,一股脑全扔进模型。结果模型在训练集上表现还不错,一到验证集就崩。后来发现是描述符之间存在严重的共线性,比如分子量和体积高度相关,导致模型过拟合。

常见的描述符可以分为几类:

描述符类型 例子 特点
物理化学性质 LogP, 分子量, TPSA 直观, 易解释, 但信息量有限
拓扑描述符 Zagreb指数, Wiener指数 反映分子骨架结构, 计算快
量子化学描述符 HOMO/LUMO能级, 偶极矩 电子层面信息丰富, 但计算昂贵
3D描述符 分子体积, 溶剂可及表面积 考虑构象, 对结合模式敏感

我个人习惯用RDKit来计算描述符。它开源、高效,而且社区活跃。下面是一段常用代码:

from rdkit import Chem
from rdkit.Chem import Descriptors

mol = Chem.MolFromSmiles('CCO')
mw = Descriptors.MolWt(mol)
logp = Descriptors.MolLogP(mol)
hbd = Descriptors.NumHDonors(mol)
hba = Descriptors.NumHAcceptors(mol)
tpsa = Descriptors.TPSA(mol)

print(f"MW: {mw:.2f}, LogP: {logp:.2f}, HBD: {hbd}, HBA: {hba}, TPSA: {tpsa}")
小建议: 描述符的标准化(归一化)很重要。我习惯用StandardScaler或MinMaxScaler,特别是对SVM、神经网络这类对特征尺度敏感的模型。

3.2 分子指纹:降维打击式的表达

分子指纹可以看成是分子结构的「数字化编码」。它把分子映射成一个固定长度的二进制向量,每个bit代表一个子结构是否存在。为什么说它是降维打击?因为指纹可以自动捕捉大量的局部特征,而且计算效率极高。

有次我在做hERG毒性预测,用了简单的MACCS指纹。你猜怎么着?模型AUC直接提升了0.08。原因很简单——指纹能捕捉到那些微小但关键的毒性基团,比如叔胺、芳环上的特定取代模式。而这些信息,用传统的1024个描述符根本抓不住。

常见的指纹类型包括:

  • MACCS keys: 166个预定义子结构,简单粗暴但很有效
  • Morgan/Circular指纹: 基于ECFP,半径可调,能捕捉更大范围的局部环境
  • Atom pair指纹: 编码原子间距离关系,对分子大小不敏感
  • Topological torsion指纹: 编码路径上的原子类型序列

我个人建议多试试Morgan指纹,特别是ECFP4。它在很多ADMET任务上表现都很稳定。下面是如何生成指纹的代码:

from rdkit import Chem
from rdkit.Chem import AllChem

mol = Chem.MolFromSmiles('CCO')

# MACCS keys
maccs = AllChem.GetMACCSKeysFingerprint(mol)

# ECFP4 (Morgan radius=2)
ecfp4 = AllChem.GetMorganFingerprintAsBitVect(mol, radius=2, nBits=2048)

print(f"MACCS: {maccs.ToBitString()[:50]}...")
print(f"ECFP4: {ecfp4.ToBitString()[:50]}...")
避坑提醒: 指纹的bits数是关键超参数。bits太少(如512)容易碰撞,bits太多(如4096)容易过拟合且存储开销大。我一般从2048开始调,根据模型效果和数据集规模调整。

3.3 图特征:分子的自然语言

其实分子本身就是一个图结构——原子是节点,化学键是边。用图特征来建模,某种程度上是「让模型学会看懂分子的本来面貌」。我觉得这是近年来ADMET预测领域最激动人心的进展之一。

为什么要用图特征?举个例子:传统描述符和指纹都无法区分R-和S-对映体,但图神经网络可以,因为图可以编码原子的立体构型。有次我做一个CYP酶抑制预测,用GCN直接在图结构上做消息传递,效果比用ECFP4加XGBoost还好。

常见的图特征方法:

  1. 节点特征: 原子类型、杂化状态、形式电荷、是否为芳香原子等
  2. 边特征: 键类型、是否在环中、是否共轭等
  3. 图池化: 全局加和/均值/注意力,把节点特征聚合成整图表示
  4. GNN变体: GCN、GAT、GIN、MPNN等,每种有各自的聚合策略
import torch
from torch_geometric.data import Data
from rdkit import Chem

mol = Chem.MolFromSmiles('CCO')

# 构造节点特征
atom_features = []
for atom in mol.GetAtoms():
    feat = [
        atom.GetAtomicNum(),
        atom.GetDegree(),
        atom.GetFormalCharge(),
        1 if atom.GetIsAromatic() else 0,
    ]
    atom_features.append(feat)
x = torch.tensor(atom_features, dtype=torch.float)

# 构造边索引
edge_index = []
for bond in mol.GetBonds():
    i = bond.GetBeginAtomIdx()
    j = bond.GetEndAtomIdx()
    edge_index.append([i, j])
    edge_index.append([j, i])
edge_index = torch.tensor(edge_index, dtype=torch.long).t().contiguous()

data = Data(x=x, edge_index=edge_index)
print(data)
我的经验: 图特征虽然强大,但对数据量要求也高。如果你的训练集不到1000个分子,建议先从描述符+指纹的组合开始。我曾经在一个只有500个样本的P-gp抑制项目里硬上GNN,结果验证集AUC还不如随机森林。数据量才是硬道理。

3.4 三种特征方法如何选?

讲完了三种方法,怎么选?为什么要这么麻烦?其实没有绝对的「最优特征」,只有「最适合当前任务的特征」。

下面这张图是我自己总结的特征选择路径,做完几十个项目后慢慢形成的判断框架:

特征工程选择框架 输入分子结构 数据量 < 1000 1000 ≤ 数据量 ≤ 10000 数据量 > 10000 描述符 + MACCS指纹 (简单模型优先) 描述符 + ECFP4指纹 (XGBoost / LightGBM) 图神经网络 (GCN / GAT / MPNN) 验证策略:特征重要性分析 + 交叉验证 发现过拟合或不足时,回溯调整特征组合

你可能会问:这三种特征能组合使用吗?当然可以。我自己的经验是,把描述符和指纹拼接起来(concatenate),然后一起喂给树模型,在很多ADMET任务上都是「性价比最高」的方案。图特征目前更适合那些数据量大、任务本身对分子拓扑敏感的场景。

总结一下我的惯用策略:
1. 新任务先用描述符+MACCS打底,快速了解数据情况
2. 然后加上ECFP4,看效果有没有明显提升
3. 如果数据超过5000,尝试GIN或MPNN
4. 所有选择都要用五折交叉验证来拍板

说到底,特征工程没有银弹。我见过不少团队一上来就上GNN,结果还不如用LightGBM跑描述符。你想想看,同样的时间,花在理解数据、清洗数据、做特征分析上,回报率其实更高。嗯,这里要注意,别被「新技术」的炫目感带偏了,打牢基础比什么都重要。