3. 传统的分子特征编码方法:MACCS密钥、ECFP指纹、摩根指纹
聊到分子特征编码,很多刚接触这行的同学容易一头扎进图神经网络或者Transformer里。我的建议是,先别急。传统的分子指纹方法,到今天依然在生产环境中大量使用,尤其是做虚拟筛选或QSAR建模时,它们往往是baseline的标配。
这一章,我带大家把三种经典方法摸透:MACCS密钥、ECFP指纹、摩根指纹。它们背后的逻辑其实很朴素,你想想看,都是想把分子的结构信息“压缩”成一串固定长度的向量。只是压缩的方式不同罢了。
3.1 MACCS密钥:靠“模板”对号入座
MACCS(Molecular ACCess System)密钥,本质上是一组预定义的子结构模板集合。每个模板对应一个结构特征,比如“是否含有苯环”、“是否有羰基”、“是否带羧基”。计算时,把分子跟这166个模板逐一比对,匹配上了对应位就置1,否则置0。
这个方法的缺点很明显——它对未知结构的泛化能力弱。我记得早期做配体相似性搜索时,用MACCS筛出来的命中集,经常漏掉一些骨架新颖但活性不错的分子。说白了,模板覆盖不到的片段,它就是盲区。
不过优势也很突出:快速、可解释。每个bit对应什么化学含义,可以清清楚楚回溯。你怀疑某个官能团跟活性相关,直接看对应bit的贡献就好。
实现上,RDKit提供了现成接口。我个人习惯这么用:
from rdkit import Chem
from rdkit.Chem import MACCSkeys
mol = Chem.MolFromSmiles('CC(=O)OC1=CC=CC=C1C(=O)O')
maccs = MACCSkeys.GenMACCSKeys(mol)
# maccs是长度为167的位向量(第一位是dummy bit)
bit_vec = list(maccs.ToBinary())
active_bits = [i for i, v in enumerate(bit_vec) if v == 1]
print(f'命中bit数: {len(active_bits)}')
print(f'激活位索引: {active_bits[:10]}...')
输出里那些激活的索引,对应着RDKit内置的SMARTS模板。我曾经在一门课里让学生逐个解析这些模板,结果不少人发现,自己的分子明明有酚羟基,对应的模板位却没点亮。原因是对应SMARTS限定了特定的连接环境。嗯,这里要注意:MACCS的模板判定是严格的子图同构匹配,不是简单的子串匹配。
3.2 ECFP指纹:圆形扩展的“局部感受野”
ECFP(Extended-Connectivity Fingerprint)跟MACCS完全不同。它不从预定义模板出发,而是从每个原子出发,做迭代的圆形扩展。说白了吧,它是在模仿卷积神经网络里“局部感受野”的概念——每次扩展一层邻居,就把更大范围的拓扑信息揉到一起。
实现细节是酱紫的:
- 给每个原子初始化一个整数标识符(基于原子性质,比如原子序数、度、电荷等)。
- 迭代扩展:每轮,把当前原子及其半径r内的邻居信息,通过哈希函数融合成新标识符。
- 所有扩展过程中产生的标识符,全部收集起来,再哈希折叠到固定长度的bit位上。
这里有一个关键参数——直径(diameter)或者半径(radius)。ECFP通常用直径来命名,比如ECFP4,对应的radius=2。什么意思呢?就是说每个原子会考虑其周围2层以内的邻居。你调整这个参数,就等于在控制分子描述的感受野大小。
我做过对比实验:ECFP4在大多数小分子数据集上表现均衡,加到ECFP6后,虽然包含了更多环境信息,但噪声也跟着涨了,有些情况下反而是过拟合。所以我建议,在不清楚最优配置时,先从ECFP4开始试。
RDKit的计算示例:
from rdkit.Chem import AllChem
mol = Chem.MolFromSmiles('CC(=O)OC1=CC=CC=C1C(=O)O')
ecfp = AllChem.GetMorganFingerprintAsBitVect(mol, radius=2, nBits=2048)
# 或者获取原始整数标识符列表(不折叠)
ecfp_identifiers = list(AllChem.GetMorganFingerprint(mol, radius=2).GetNonzeroElements().keys())
print(f'唯一子结构标识符数量: {len(ecfp_identifiers)}')
看到那个nBits=2048了吧?这是折叠到2048维的位向量。如果不折叠,你会拿到一组稀疏的整数哈希值。这里我踩过一个坑:直接用不折叠的标识符做相似性计算,不同分子的标识符集可能大小差异巨大,导致Tanimoto系数失真。后来我统一先折叠到固定长度再比较,问题就解决了。
3.3 摩根指纹:跟ECFP是“表亲”
摩根指纹(Morgan Fingerprint)这个名字,听起来跟ECFP不是一回事,但实现上它们几乎是一模一样的算法——都基于摩根算法(Morgan algorithm)进行迭代圆扩展。差异主要出在细节上:
- 特征提取范围:ECFP更强调对原子连接环境的局部描述,摩根指纹默认会覆盖更多的原子特征(包括手性、环信息等)。
- 折叠方式:摩根指纹在RDKit里默认用
GetMorganFingerprintAsBitVect,折叠策略跟ECFP略有不同,但核心思想一致。 - 输出形式:两者都能输出位向量或者稀疏哈希列表。
我个人习惯:两者实际上是等效的。你在论文里看到ECFP4,用RDKit的GetMorganFingerprintAsBitVect(mol, radius=2)就能完美复现。很多研究者直接用“摩根指纹”来统称这类基于圆形扩展的算法。
来看一个对比表格,理清三者的关键差异:
| 特性 | MACCS | ECFP | 摩根指纹 |
|---|---|---|---|
| 特征来源 | 预定义子结构模板 | 原子局部环境迭代扩展 | 原子局部环境迭代扩展 |
| 固定长度 | 是(166位) | 可配置(默认2048) | 可配置(默认2048) |
| 可解释性 | 高(每个bit对应SMARTS) | 低(哈希值无直接化学含义) | 低(哈希值无直接化学含义) |
| 计算速度 | 快 | 中等 | 中等 |
| 感知范围 | 精确子图匹配 | 半径2~3层 | 同ECFP |
| 实战场景 | 快速过滤、规则检查 | 相似性搜索、QSAR | 同ECFP |
3.4 一张图看懂三种指纹的关系
为了让这些概念更直观,我画了一张比较图。你可以从上到下看到分子信息从“全局模板匹配”到“局部圆形扩展”再到“迭代哈希折叠”的变化线。
一句话总结:
- MACCS 是“查字典”式编码——有就写1,没有就是0,快但死板。
- ECFP 与摩根指纹是“画圈圈”式编码——从每个原子出发一圈圈往外扩展,最后哈希折叠成位向量,灵活但不好解释。
- 三者的输出最终都是固定长度的数值向量,可以直接喂给下游模型——包括我们后面要讲的Transformer。
3.5 实战避坑指南
最后分享几个我踩过的坑,希望对你有帮助。
避坑1:不要盲信折叠位数越大越好
有人觉得2048位不够,非要设到16384位。结果稀疏度太高,Tanimoto相似度几乎全降到0.2以下,失去排序意义。我建议从1024或者2048开始,做一次grid search。
技巧:组合使用指纹
我自己常在项目中把MACCS和ECFP拼接起来——一个提供可解释的官能团信息,一个提供拓扑环境信息。这样既保障了baseline性能,又能在分析时讲清楚哪些结构片段在驱动预测。
避坑2:ECFP半径的选择
radius=2(ECFP4)对小分子够用;如果分子比较长或者有较大取代基,可以试试radius=3。但再大就容易引入过多非直接相关的原子,反而稀释关键信号。我有一个失败案例是直接用radius=5,模型训练loss下降极慢,后来发现是分子整体特征被长距离噪声污染了。