三、分子描述符与指纹:给分子拍“数字证件照”

同学们好,咱们今天聊点实在的。SMILES 字符串虽然能表示分子结构,但计算机没法直接拿字符串做相似性比较、训练模型,对吧?你想想看,CCO 和 C1CC1 在字符串层面差得很远,但一个是乙醇,一个是环丙烷,性质完全不同。这里面需要一层转化——把分子变成计算机能“理解”的数字向量。

这就是分子描述符与指纹要做的事。说白了,就是给每个分子拍一张“数字证件照”,把它的物理化学性质、结构特征、拓扑信息全部编码成一串固定的数值或比特位。

我刚开始做药物筛选那会儿,就觉得这步可简单了,无非算几个物理化学性质呗。结果有一次做虚拟筛选,由于指纹选错,模型AUC直接掉到0.55,跟抛硬币差不多。从那以后,我再也不敢随便选指纹了。

3.1 分子描述符:分子的“体检报告”

分子描述符是个比较宽的概念。任何通过分子结构计算出来的数值,都可以叫描述符。比如分子量、LogP、氢键供体/受体数量、拓扑极性表面积(TPSA)等。一个分子能算成百上千个描述符,但常用的也就是那几个经典的。

我个人习惯把描述符分成三类,用表格看一眼就清楚了:

类型 代表描述符 典型应用
物理化学 分子量、LogP、TPSA、HBA、HBD 类药性评估(Lipinski五规则)
拓扑/几何 Balaban指数、Wiener指数、χ(Chi)指数 结构相似性、分子稳定性预测
量子化学 HOMO能量、LUMO能量、电负性 反应活性、带隙预测

代码实现上,RDKit 提供了非常成熟的接口。我习惯直接用 Descriptors 模块,一行代码就能拿到200多个描述符:

from rdkit import Chem
from rdkit.Chem import Descriptors

mol = Chem.MolFromSmiles('CCO')
# 计算单个描述符
mw = Descriptors.MolWt(mol)
logp = Descriptors.MolLogP(mol)
hbond_donor = Descriptors.NumHDonors(mol)
print(f"分子量: {mw:.2f}, LogP: {logp:.2f}, HBD: {hbond_donor}")

嗯,这里要注意:描述符的数值范围差异很大。分子量可能几百,LogP 可能是负数或正数,直接扔进模型会出问题。记得做标准化或归一化。

小技巧: 如果你只是想快速算一堆分子的描述符,可以用 Descriptors.CalcAllDescriptors() 函数,它会返回一个字典,包含全部可用描述符。

3.2 分子指纹:结构模式的“比特化”

描述符是连续数值,指纹则是二进制向量。为什么要比特化?因为很多场景——比如相似性搜索、子结构匹配——只需要知道“结构片段是否存在”,而不需要知道“值是多少”。指纹本质上是一种哈希映射:把分子中的特定子结构映射到比特位上。

常见的三种指纹,我分别说说:

3.2.1 ECFP(扩展连通性指纹)

ECFP 是目前应用最广的指纹,尤其在深度学习领域。它的核心思想是对每个原子,基于其邻居信息迭代生成固定长度的“环境描述”,然后哈希成比特位。

ECFP 有几个版本:ECFP4 对应2层半径(考虑2个化学键范围内的邻居),ECFP6 对应3层半径。半径越大,感受野越大,但计算量也上去了。

为什么选择 ECFP? 因为它能捕捉局部拓扑结构,对分子骨架变化非常敏感。我曾在配体相似性搜索项目中用过ECFP4,top-10命中率比MACCS高15%左右。

看一下 RDKit 的实现:

from rdkit import Chem
from rdkit.Chem import AllChem

mol = Chem.MolFromSmiles('c1ccccc1C(=O)O')
# ECFP4,固定1024位
ecfp = AllChem.GetMorganFingerprintAsBitVect(mol, radius=2, nBits=1024)
print(type(ecfp))        # <class 'rdkit.DataStructs.cDataStructs.ExplicitBitVect'>
print(ecfp.ToBitString()[:64])   # 打印前64位看看
避坑指南: 我曾经在训练图神经网络时,直接把ECFP当作特征输入,结果模型死活不收敛。后来发现是ECFP的哈希碰撞比较严重,导致很多不同结构映射到同一比特位。建议在作为模型输入前,先用PCA或自编码器降维一下。

3.2.2 MACCS 指纹

MACCS 是相对“老牌”的指纹,只有166个预定义的SMARTS子结构模式。每个模式就像一个“是非题”:分子包含这个子结构就置1,否则置0。正因为是预定义的,它比较稳定,迁移性好,但表达能力有限。

我一般在做分子聚类时先用MACCS打个底,速度快,结果也稳定。要算MACCS,RDKit 同样提供接口:

from rdkit.Chem import MACCSkeys

mol = Chem.MolFromSmiles('CC(=O)Oc1ccccc1C(=O)O')  # 阿司匹林
maccs = MACCSkeys.GenMACCSKeys(mol)
print(f"MACCS长度: {maccs.GetNumBits()}")
print(maccs.ToBitString()[:80])

3.2.3 其他指纹

除了 ECFP 和 MACCS,还有一些特定场景下的指纹,比如:

  • RDKit 指纹:基于子结构路径的标准指纹,可以自定义参数。
  • AtomPair 指纹:编码原子对之间的拓扑距离关系。
  • Topological Torsion 指纹:编码四个原子之间的扭转角度模式。

我个人的选型原则是这样的:做通用相似性搜索,优先 ECFP4;做子结构过滤或快速聚类,用 MACCS;做反应预测或构象分析,试试 AtomPair 或 Topological Torsion。

3.3 实际应用:基于指纹的分子相似性搜索

讲完理论,我们把它用在真实场景里。假设你从高通量筛选拿到一个先导化合物,想找数据库里跟它结构相似的其他分子。基于指纹的 Tanimoto 相似度是最直接的方法:

from rdkit import Chem
from rdkit.DataStructs import TanimotoSimilarity

query = Chem.MolFromSmiles('c1ccccc1')
ref   = Chem.MolFromSmiles('c1ccccc1C(=O)O')

fp_q = AllChem.GetMorganFingerprintAsBitVect(query, radius=2, nBits=1024)
fp_r = AllChem.GetMorganFingerprintAsBitVect(ref, radius=2, nBits=1024)

sim = TanimotoSimilarity(fp_q, fp_r)
print(f"Tanimoto相似度: {sim:.3f}")
重点: Tanimoto 相似度范围是[0,1],一般大于0.6就可以认为两个分子在结构上“比较接近”。我在做数据清洗时,经常用这个阈值去重。

下面这张图总结了从SMILES到描述符/指纹的完整转化路径:

分子描述符与指纹计算流程 SMILES 分子输入 物理化学描述符 分子指纹(ECFP) MACCS 指纹 数值向量(连续值) 比特位向量(0/1) 166位比特向量 分子数值表示空间 物理化学描述符:连续值,适合回归/分类任务

从这张图里能看到,同样的SMILES输入,可以同时产生连续描述符和离散指纹。我个人建议:如果你要做相似性搜索或聚类,优先用指纹;如果你要做定量预测(比如活性值、毒性),用描述符更直觉。

3.4 选型对比与避坑

最后,把三种指纹的主要差异列成一张表,方便你快速决策:

指纹类型 位数 可解释性 计算速度 适用场景
ECFP4 自定义(常用1024/2048位) 中等,依赖哈希理解 中等 相似性搜索、QSAR模型输入
MACCS 166位固定 高,子结构模式清晰 快 快速聚类、子结构过滤
AtomPair 自定义 低,基于拓扑距离 慢 构象感知的相似性
曾经踩过的坑: 我在做分子生成模型的评估时,直接用MACCS计算Tanimoto相似度,结果大量生成分子相似度接近0.9,但一看结构完全不同。后来发现MACCS位太少,碰撞太严重。幸好换成ECFP后问题解决。

这里再补充一个实战技巧:指纹可以拼接使用。 比如同时计算ECFP和MACCS,然后拼接成 nBits+166 维的向量。我在一个配体-蛋白结合模式预测项目中就这么干过,效果比单用任何一种都好,你不妨也试试。