3. 分子特征表示方法概览

做 ADMET 预测,第一关就是把分子喂给模型。但分子不是一张图片,也不是一段文本,它是一团原子和化学键。怎么把这个三维世界里的东西,变成模型能理解的数字?这就是我们今天要聊的——分子特征表示。

我最早接触这个领域时,踩过一个坑。当时以为直接把 SMILES 字符串扔进 LSTM 就行,结果模型收敛奇慢,预测结果一塌糊涂。后来才明白,不同的表示方法,背后藏着完全不同的归纳偏置。选错了,模型再强也白搭。

核心观点: 分子表示是特征工程的起点。你用什么视角看分子,模型就只能学到什么。

3.1 四种主流表示方法

目前业界常用的分子表示方法,大致可以分成四类:传统描述符、图表示、序列表示、3D 构象。每个家族都有自己的优势和短板。

方法 核心思路 典型工具 优点 缺点
传统描述符 预定义规则计算分子属性 RDKit、Mordred 成熟、可解释、低计算量 丢失拓扑/构象信息,表达力有限
图表示 原子→节点,键→边 PyG、DGL、RDKit 保留局部结构,适合 GNN 图同构问题,需要消息传递
序列表示 分子编码为字符串 SMILES、InChI、SELFIES 方便与 NLP 模型对接 语法敏感,空间信息丢失
3D 构象 三维坐标 + 原子类型 RDKit + ETKDG、Open Babel 包含空间信息,适合力场 计算贵,需要构象采样

3.2 传统描述符——老牌稳军

说白了,就是用手工设计的公式去算分子的各种物理化学性质。比如分子量、logP、氢键供体数、拓扑极性表面积(TPSA)等等。RDKit 里内置了 200 多个这样的描述符,我平时做快速基线时最喜欢用它。

举个例子,你要算一个分子的 logP 和 TPSA:

from rdkit import Chem
from rdkit.Chem import Descriptors, Lipinski

mol = Chem.MolFromSmiles('CC(=O)OC1=CC=CC=C1C(=O)O')
logp = Descriptors.MolLogP(mol)
tpsa = Descriptors.TPSA(mol)
hbond_donor = Lipinski.NumHDonors(mol)
print(f"logP: {logp:.2f}, TPSA: {tpsa:.2f}, HBD: {hbond_donor}")

我踩过的第一个坑:描述符之间往往有强烈的相关性。比如 logP 和分子量,在脂肪族化合物里常常正相关。如果你直接把几百个描述符扔进线性模型,多重共线性会让你头大。我的做法是先做特征筛选,或者用 PCA 降维。

小技巧: 不要盲目堆描述符。我个人习惯先用 RDKit 的 Descriptors.descList 查看所有描述符列表,然后结合领域知识选 10-20 个关键特征,效果往往比全量要好。

3.3 图表示——分子的自然语言

分子的本质就是一张图。原子是节点,化学键是边。如果把每个原子用一个特征向量表示(原子类型、度数、电荷等),把键也编码,就得到了分子图。

用图的好处是保留了局部连通性。比如苯环的共轭结构,在图里就是 6 个节点和 6 条边构成的一个环。GNN 通过消息传递机制,能让邻居原子的信息逐步聚合到中心节点上。我做 ADMET 毒副作用预测时,发现图表示对局部的反应性位点非常敏感,比如某个芳环上的取代基是代谢活性的关键。

下面是把一个分子转成 PyG 图数据对象的示例:

from rdkit import Chem
from torch_geometric.data import Data
import torch

def mol_to_graph(smiles):
    mol = Chem.MolFromSmiles(smiles)
    # 原子特征:one-hot 编码原子类型 + 度数
    atom_features = []
    for atom in mol.GetAtoms():
        feat = [atom.GetAtomicNum(), atom.GetDegree(), ...]
        atom_features.append(feat)
    x = torch.tensor(atom_features, dtype=torch.float)
    
    # 边索引
    edges = []
    for bond in mol.GetBonds():
        i, j = bond.GetBeginAtomIdx(), bond.GetEndAtomIdx()
        edges.append([i, j])
        edges.append([j, i])  # 无向图
    edge_index = torch.tensor(edges, dtype=torch.long).t().contiguous()
    
    return Data(x=x, edge_index=edge_index)
注意: 图同构问题!如果两个分子的图完全一样但原子编号不同,GNN 可能把它俩当成不同的。解决办法是用规范化的分子图(RDKit 的 CanonicalRankAtoms)或在数据增强时做随机扰动。我曾因为这个在验证集上得到两个不同的预测,排查了好久才发现是编号不一致惹的祸。

3.4 序列表示——分子的语言模型

SMILES 是最常用的分子序列,它用符号和括号表示分支、环和立体化学。比如苯酚就是 Oc1ccccc1。InChI 是另一种标准,它更唯一但不易读。

为什么有人用序列?因为你可以直接用 NLP 的那一套——Transformer、BERT、GPT——来处理分子。我自己做过一个实验:用 100 万 SMILES 预训练一个 BERT 模型,然后在下游 ADMET 任务上微调,效果竟然比随机初始化的 GNN 好一点点。但别高兴太早,SMILES 有个大毛病:同一个分子可以写出多个合法 SMILES(除非用规范化),而且正则化的 SMILES 依赖于 RDKit 的规范算法。

下面是一个 SMILES 解析与规范化的例子:

from rdkit import Chem

smiles = "Oc1ccccc1"  # 苯酚的简单表示
mol = Chem.MolFromSmiles(smiles)
canonical_smiles = Chem.MolToSmiles(mol, canonical=True)
print(canonical_smiles)  # 输出: c1ccc(cc1)O

嗯,这里要注意:SMILES 的长度差异很大。一个小分子只有 10 个字符,大分子可能有几百。如果你的序列模型需要固定长度,会浪费大量算力在 padding 上。我通常用 SELFIES 替代 SMILES,因为它天生是任务无关的,且对非法字符鲁棒。

经验之谈: 如果你的 ADMET 任务涉及到手性或者 E/Z 异构,一定要用包含立体信息的 InChI 或规范 SMILES。我见过一个团队用普通 SMILES 训练 CYP450 代谢预测,结果对(R)-和(S)-构型完全没区分能力,模型变成了瞎子。

3.5 3D 构象——加入空间维度

2D 表示(图、序列)忽略了原子的三维排布。而很多 ADMET 性质,比如 CYP450 的代谢位点、穿过细胞膜的难易程度,都和分子的三维形状直接相关。3D 构象表示就是给每个原子分配一个三维坐标(x, y, z)。

获取 3D 构象最简单的方式是用 RDKit 的力场或者 ETKDG 算法生成一个低能构象:

from rdkit import Chem
from rdkit.Chem import AllChem

mol = Chem.MolFromSmiles('CCO')  # 乙醇
mol = Chem.AddHs(mol)  # 加氢
AllChem.EmbedMolecule(mol, randomSeed=42)  # 生成 3D 坐标
AllChem.MMFFOptimizeMolecule(mol)  # 力场优化
conf = mol.GetConformer()

# 获取原子坐标
pos = conf.GetPositions()
print(pos.shape)  # (9, 3)  # 9个原子,xyz

我踩过的坑:同一个分子的不同构象,能量可能差别很大。如果只用一个构象,相当于忽略了分子的柔性。我的解决方法是采样多个构象(比如 10 个低能构象),然后用图神经网络对每个构象分别编码,最后做池化。这在某些 ADMET 任务上能提高 3-5% 的准确率。

避坑指南: 我曾经在训练溶解度预测模型时,直接用优化后的最低能量构象。结果模型对溶剂效应完全没反应——因为气相构象和溶液中的构象差很多。后来我改用分子动力学采样或者隐式溶剂模型(如 PCM)来生成更真实的构象,效果才上去。

3.6 四种方法的协同与选择

下面这张图梳理了四种表示方法在整个 ADMET 工作流中的位置和互补关系。每种方法都不是万能的,但它们组合在一起就能覆盖大部分场景。

分子特征表示方法框架 分子结构 (3D 原子坐标) 传统描述符 RDKit / Mordred 图表示 PyG / DGL 序列表示 SMILES / InChI 3D 构象 RDKit ETKDG / Open Babel 选择建议:2D 图 + 3D 构象联合使用是当前 ADMET 自监督学习的主流策略

我个人在实际项目中的选型策略:如果 data 量少(<1 万),用传统描述符做 baseline 最快;如果 data 量大(>10 万),优先尝试图 Transformer 或序列 BERT;如果任务对立体化学敏感(比如 hERG 钾通道抑制),一定要加上 3D 构象表示。组合使用往往比单打独斗强 10-15%。

总结: 没有最好的表示,只有最合适的表示。理解每种方法的原理和局限,才能在 ADMET 预测中做出合理选择。