4. 分子特征化方法(上):ECFP、MACCS 指纹的生成与使用
做 ADMET 预测,说白了就是让模型“看懂”分子。但模型不认识 SMILES 字符串,也不懂原子坐标。你得把分子翻译成它吃得下的数字向量——这就是特征化。
我刚入行那会儿,还在用手算分子描述符,什么 LogP、分子量、氢键供受体……翻论文一个个列出来,再拿 Excel 拼特征矩阵。累吗?累。而且经常漏掉关键信息。后来接触 DeepChem,发现指纹这东西才是真正把“结构化学”和“机器学习”打通的桥。
这一讲,我们聚焦两种最常用的分子指纹:ECFP 和 MACCS。听名字挺唬人,其实理解起来很直观。
4.1 为什么需要分子指纹?
分子指纹是一种固定长度的二进制向量。每一位代表分子中是否存在某种子结构或拓扑特征。比如 ECFP 的某一位,可能对应一个半径内的原子环境。
好处很明显:
- 固定长度:不管分子大小,输出都是 1024 位或 166 位向量,适合直接喂给神经网络或随机森林。
- 模糊匹配:结构相似分子会得到相近的指纹向量,而非完全相同。这对 ADMET 预测特别重要——你总希望模型能泛化到未见过的类似分子。
- 可解释性:虽然比不上手工描述符那么直观,但至少可以反向追踪哪些子结构激活了高位特征。
你可能会问:那直接把原子类型 one‑hot 编码不行吗?嗯,理论上可以,但那种方式不考虑化学键和环境,丢掉太多信息。指纹恰恰保留了局部拓扑关系。
4.2 两张核心指纹:一张“圆形”一张“字典”
我们先快速对比一下,免得你混淆。
| 特性 | ECFP (Extended‑Connectivity Fingerprint) | MACCS (Molecular ACCess System) |
|---|---|---|
| 算法本质 | 圆形指纹(Morgan 算法) | 字典指纹(166 个预定义子结构) |
| 输出长度 | 可配置,常用 512/1024/2048 位 | 固定 166 位 |
| 是否需预定义 | 不需要——自动从局部环境哈希得到 | 需要——MDL 公司预先定义了 166 种结构模式 |
| 适用场景 | 聚类、相似性搜索、通用 QSAR | 快速筛选、已知子结构匹配、与 legacy 系统兼容 |
| 缺点 | 位长不够容易碰撞;对立体异构不敏感 | 覆盖有限,某些新颖结构可能没有匹配位 |
4.3 ECFP 的生成与使用
ECFP 的核心就是 Morgan 算法:从每个原子出发,逐层扩大半径,记录每个原子周围的拓扑环境,再哈希成二进制位。半径一般设为 2(对应 ECFP4,因为半径 2 实际上包含了 4 层化学键)。
用 DeepChem 生成 ECFP 非常直接。我个人习惯先指定半径和位长,然后用 CircularFingerprint 这个 featurizer。
import deepchem as dc
smiles = ["CC(=O)OC1=CC=CC=C1C(=O)O", # 阿司匹林
"CN1C=NC2=C1C(=O)N(C(=O)N2C)C"] # 咖啡因
featurizer = dc.feat.CircularFingerprint(radius=2, size=1024)
features = featurizer.featurize(smiles)
print(features.shape) # (2, 1024)
print(features[0][:10]) # 看看前10位
你会得到两个长度为 1024 的数组,里面全是 0 和 1。每个分子的每一位代表一个独特的局部结构环境。
我在一个项目中做过将 ECFP 位长从 1024 降到 512。当时数据量不大,想着节省内存。结果模型效果暴跌,很多原本可以区分的分子被挤到相似的指纹里。后来我把位长调回 1024,指标立刻恢复。所以要记住一个原则:位长宁大勿小,除非你有明确理由。
4.4 MACCS 指纹的生成与使用
MACCS 是 MDL 公司在 1990 年代推出的 166 位公共密钥。每个位对应一个特定的子结构模式,比如“是否存在一个丙基”、“是否有芳环”等。它不像 ECFP 那么“自动”,但胜在稳定、快速、结果可重现。
DeepChem 直接包装了 RDKit 的 MACCSKeysFingerprint 函数。用起来更简单:
featurizer_maccs = dc.feat.MACCSKeysFingerprint()
features_maccs = featurizer_maccs.featurize(smiles)
print(features_maccs.shape) # (2, 166)
print(features_maccs[0]) # 全部 166 位
你看,输出长度固定在 166,不用纠结参数。但我也踩过坑——有一次我用 MACCS 做 Tox21 毒性预测,发现模型一直欠拟合。查了半天,原来是某些含硫杂环的分子在 166 位中对应的位全是 0,因为它不属于预定义的任何子结构。换句话说,MACCS 对非常新颖的分子骨架可能会失效。
4.5 选择哪种指纹?
我个人的经验是:
- 如果你刚接触这个领域,从 ECFP 1024 位、半径 2 开始,它非常通用,几乎适用于所有 ADMET 任务。
- 如果项目需要与历史数据对齐(比如很多公开数据集已经用了 MACCS),那就上 MACCS,但最好配上 ECFP 做双通道输入。
- 当你想做可解释性分析,比如“究竟是哪个子结构导致毒性”,MACCS 的 166 位子结构定义可以直接查阅,比 ECFP 好理解。
下面这张图梳理了从 SMILES 到模型输入的整体流程:
图里表达了最常用的两条路径:你可以只选一条,或者把两个指纹直接拼接起来(比如变成一个 1190 维向量)。我在实际项目中经常用拼接的方式,因为既能享受 ECFP 的拓扑覆盖,又能利用 MACCS 的可解释子结构。
4.6 实战步骤总结
- 加载数据:准备好 SMILES 列表和对应标签(比如 hERG 阻断标签)。
- 选择 featurizer:根据任务决定用
CircularFingerprint还是MACCSKeysFingerprint,或者两者都用。 - 生成特征:调用
.featurize(),得到 numpy 数组。 - 训练模型:把特征数组直接喂给 DeepChem 的
dc.models(比如MultitaskClassifier)。
下面是一个完整的组合示例:
import deepchem as dc
import numpy as np
# 模拟数据:5个分子
smiles = ["CC(C)C1=CC=C(C=C1)C(C)C(=O)O", # 布洛芬
"COC1=CC=C2C(=C1)C=CC(=O)O2", # 羰基? 随便举个
"C1=CC=C2C(=C1)C(=O)NC2=O", # 邻苯二甲酰亚胺
"CCCC1=CC=CC=C1", # 丁基苯
"C1=CC=NC=C1"] # 吡啶
labels = np.random.randint(0, 2, (5, 2)) # 二分类任务
# 两个 featurizer
ecfp = dc.feat.CircularFingerprint(radius=2, size=1024)
maccs = dc.feat.MACCSKeysFingerprint()
X_ecfp = ecfp.featurize(smiles)
X_maccs = maccs.featurize(smiles)
X = np.hstack([X_ecfp, X_maccs]) # 拼接:1190 维
print("特征矩阵形状:", X.shape) # (5, 1190)
# 训练模型(快速演示)
model = dc.models.MultitaskClassifier(n_tasks=2, n_features=X.shape[1])
model.fit(X, labels, nb_epoch=10)
好了,以上就是 ECFP 和 MACCS 指纹从原理到代码的全部要点。下次遇到新的 ADMET 数据集,你应该能胸有成竹地选择合适的指纹方案了。