三、分子描述符与指纹:给分子拍“数字证件照”
同学们好,咱们今天聊点实在的。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位看看
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}")
下面这张图总结了从SMILES到描述符/指纹的完整转化路径:
从这张图里能看到,同样的SMILES输入,可以同时产生连续描述符和离散指纹。我个人建议:如果你要做相似性搜索或聚类,优先用指纹;如果你要做定量预测(比如活性值、毒性),用描述符更直觉。
3.4 选型对比与避坑
最后,把三种指纹的主要差异列成一张表,方便你快速决策:
| 指纹类型 | 位数 | 可解释性 | 计算速度 | 适用场景 |
|---|---|---|---|---|
| ECFP4 | 自定义(常用1024/2048位) | 中等,依赖哈希理解 | 中等 | 相似性搜索、QSAR模型输入 |
| MACCS | 166位固定 | 高,子结构模式清晰 | 快 | 快速聚类、子结构过滤 |
| AtomPair | 自定义 | 低,基于拓扑距离 | 慢 | 构象感知的相似性 |
这里再补充一个实战技巧:指纹可以拼接使用。 比如同时计算ECFP和MACCS,然后拼接成 nBits+166 维的向量。我在一个配体-蛋白结合模式预测项目中就这么干过,效果比单用任何一种都好,你不妨也试试。