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 系统兼容
缺点位长不够容易碰撞;对立体异构不敏感覆盖有限,某些新颖结构可能没有匹配位
一句话总结:ECFP 像“盲人摸象”自动学习局部环境,MACCS 像“按图索骥”对照预设的模板。

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,指标立刻恢复。所以要记住一个原则:位长宁大勿小,除非你有明确理由。

小技巧:如果你的分子数量很大(几十万以上),可以适当减小位长到 512 以平衡速度与精度,但务必在验证集上确认效果。

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 对非常新颖的分子骨架可能会失效。

避坑指南:我曾经在筛选一个含有 B – N 配合物的数据集时,MACCS 指纹全部为 0,模型根本无法学习。解决方案是同时拼接 ECFP 作为补充特征。

4.5 选择哪种指纹?

我个人的经验是:

  • 如果你刚接触这个领域,从 ECFP 1024 位、半径 2 开始,它非常通用,几乎适用于所有 ADMET 任务。
  • 如果项目需要与历史数据对齐(比如很多公开数据集已经用了 MACCS),那就上 MACCS,但最好配上 ECFP 做双通道输入。
  • 当你想做可解释性分析,比如“究竟是哪个子结构导致毒性”,MACCS 的 166 位子结构定义可以直接查阅,比 ECFP 好理解。

下面这张图梳理了从 SMILES 到模型输入的整体流程:

分子特征化流程:SMILES → 指纹 → 模型 SMILES 字符串 ECFP 生成 Morgan 算法, r=2, 1024 位 MACCS 生成 166 位公共密钥 特征向量 1024 / 166 / 拼接 建议 分类任务:ECFP 优先 可解释需求:MACCS 优先

图里表达了最常用的两条路径:你可以只选一条,或者把两个指纹直接拼接起来(比如变成一个 1190 维向量)。我在实际项目中经常用拼接的方式,因为既能享受 ECFP 的拓扑覆盖,又能利用 MACCS 的可解释子结构。

4.6 实战步骤总结

  1. 加载数据:准备好 SMILES 列表和对应标签(比如 hERG 阻断标签)。
  2. 选择 featurizer:根据任务决定用 CircularFingerprint 还是 MACCSKeysFingerprint,或者两者都用。
  3. 生成特征:调用 .featurize(),得到 numpy 数组。
  4. 训练模型:把特征数组直接喂给 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 的 512 位版本快速预览,确认模型能收敛后再扩大到位长 2048 做最终训练。这一步能省下不少算力。

好了,以上就是 ECFP 和 MACCS 指纹从原理到代码的全部要点。下次遇到新的 ADMET 数据集,你应该能胸有成竹地选择合适的指纹方案了。