第四章 分子指纹:MACCS keys 与 Morgan 指纹 (ECFP)

分子指纹,说白了就是把一个分子的结构,转成一串固定长度的数字或二进制位串。

为什么要这么做?因为神经网络也好,随机森林也好,都听不懂“苯环”、“羰基”这些化学结构。你得把结构编码成它们能懂的“语言”——向量。指纹,就是这种语言。

我记得刚开始搞ADMET预测时,总觉得这步多余。直接算描述符不就完了吗?后来发现很多结构特征,比如局部取代模式,常规描述符根本抓不住。指纹正好解决这个问题。

分子指纹:从结构到特征向量的核心流程 分子(SMILES / Mol对象) MACCS keys(固定166位) 子结构模板匹配 每个位对应一个SMARTS模式 结果:二进制位向量 (0/1) Morgan指纹(ECFP,可变位数) 各原子为中心,半径R扩展 哈希映射到固定长度 结果:整数/位向量(可解释性低)

MACCS Keys:白盒指纹

MACCS keys,全称是 MACCS Structural Keys。这东西很老,但特别稳。RDKit里直接调用,生成的是 166位的二进制向量。每一位代表一个特定的子结构。

说白了,就是拿着166个模板去分子里“做匹配”。匹配上了,那一位就是1,否则就是0。

我曾经在某次做分类任务时,发现模型效果差。查来查去,发现是分子指纹位数太多了,模型过拟合。后来换成MACCS keys,效果反而好了不少。为什么?因为它的特征是确定的,不含糊。每个位都对应具体的化学含义。

适用场景:
  • 你需要可解释性——能追溯到哪个子结构在起作用
  • 数据量不大(几百到几千个分子),用MACCS更稳
  • 做药化任务,结构枚举做对比,MACCS是标准配置

Morgan 指纹(ECFP):黑盒指纹

Morgan指纹是另一类。它不依赖预定义模板,而是以每个原子为中心,往外扩展。

半径R=0时,只考虑中心原子;R=1时,考虑原子及其相邻的一个键;R=2时,扩展两层键……然后把每个子结构哈希映射到固定长度的位上。

RDKit默认的Morgan指纹是 2048位、半径=2,也就是常说的ECFP4(因为直径是4,所以叫ECFP4)。

我个人习惯在虚拟筛选任务上用它。你想,MACCS keys只有166个模板,很多结构模式根本覆盖不到。Morgan可以生成几乎无限的子结构,灵活性高得多。

避坑指南: 我曾经在计算大量分子的Morgan指纹时,直接用的默认2048位哈希。结果跑完一看,所有分子的指纹都极其相似——几乎全是1,没有区分度。
后来发现是键位太多、哈希冲突严重。解决方案:把指纹位数改大,比如4096或8192位。或者改用整数列表形式(不哈希),保留每个子结构信息。

特征提取:RDKit 代码实战

看代码,最直接。

1. MACCS Keys 提取

from rdkit import Chem
from rdkit.Chem import MACCSkeys

smiles = ["c1ccccc1", "CC(=O)Oc1ccccc1C(=O)O"]
mols = [Chem.MolFromSmiles(s) for s in smiles]

maccs = [MACCSkeys.GenMACCSKeys(m) for m in mols]
print(maccs[0])          # 位向量对象
print(maccs[0].ToBitString())  # 二进制字符串
print(len(maccs[0]))     # 固定 166 位

输出长这样:

<rdkit.DataStructs.cDataStructs.ExplicitBitVect at 0x...>
0000000000000000000000000000000000001000000000000000000000000010000000...
166

注意,前几位基本都是0,因为一些模板在简单分子里匹配不上。正常。

2. Morgan 指纹提取

from rdkit.Chem import AllChem

# 默认参数:半径=2, 位数=2048
fp1 = AllChem.GetMorganFingerprintAsBitVect(mols[0], radius=2, nBits=2048)

# 换成整数列表模式(不哈希)
fp2_list = AllChem.GetMorganFingerprintAsIntVect(mols[0], radius=2)
print(len(fp1))
print(fp1.ToBitString()[:80])  # 看前80位
# 整数列表模式可以看哪些子结构出现
print(len(fp2_list.GetNonzeroElements()))  # 非零键数量

可视化:指纹也有“画像”

一张指纹向量,密密麻麻成千上万位,你直接看字符串能看出什么来?什么也看不出。

我习惯用 t-SNE 或者 PCA 降维,把分子指纹降到2维或3维,看一眼聚类效果。

为什么不直接算相似度?相似度矩阵你要全部分子两两算一遍,几百个还行,几万个直接炸内存。降维可视化是快速检验指纹质量的好方法。

import numpy as np
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt

# 假设有500个分子的MACCS指纹
maccs_list = [MACCSkeys.GenMACCSKeys(m) for m in mols]
# 转为numpy数组
X = np.array([list(fp) for fp in maccs_list])

pca = PCA(n_components=2)
X_pca = pca.fit_transform(X)

plt.figure(figsize=(8,6))
plt.scatter(X_pca[:,0], X_pca[:,1], c=labels, cmap='viridis', s=10)
plt.title("MACCS Keys PCA 投影")
plt.xlabel("PC1")
plt.ylabel("PC2")
plt.colorbar()
关键指标对比:
特性 MACCS Keys Morgan指纹 (ECFP)
维度 166 位固定 可配置(默认2048)
编码方式 子结构模板匹配 原子扩展 → 哈希
可解释性 高(每位有化学定义) 低(哈希后无法反推)
灵活性 低(只覆盖预定义模板) 高(半径/位数可调)
常见场景 分类、规则过滤、可解释模型 虚拟筛选、相似度搜索、深度学习输入

关于特征标准化

指纹向量的每一位已经是0/1了,不需要标准化。但要注意:如果你把指纹和其他描述符拼接在一起,其他描述符是连续数值(比如分子量、LogP),那最好先把它们归一化或者标准化,否则量级差异会让模型忽略指纹位。

我踩过这个坑。某次把2048位Morgan指纹和3个理化描述符直接拼接送进XGBoost,结果模型基本只用了理化描述符,指纹几乎没权重。后来把理化描述符做Z-score归一化,两者就能平等参与训练了。

小技巧:
  • 位向量可以用 Jaccard 距离做相似度计算
  • 整数列表的Morgan指纹更适合做核方法,比如SVM + Tanimoto核
  • 大分子集合建议先抽一份子集试试指纹维度和半径,别一上来就跑全部

小结:什么时候用什么

说实话,没有绝对的好坏。我自己的经验是:

  • 数据量 < 2000,需要解释结果 → MACCS keys
  • 数据量 > 10000,画结构相似性图 → Morgan指纹 ECFP4
  • 做深度学习,输入是一维向量 → Morgan指纹(整数列表或2048位)
  • 做规则过滤或排除PAINS结构 → MACCS keys + 自定义SMARTS

嗯,多试试,看结果。指纹这件事,数据说了算。我见过Morgan搞不定的任务,换MACCS反而更好。也见过相反的。

别偷懒,跑几个实验,看看可视化出来的聚类好不好,交叉验证的分数高不高。数据永远不会骗你。