330

计算 ECFP4 Fingerprint:传统特征仍然有价值

ECFP4 指纹至今是最可靠的分子特征基线。这篇给出正确的计算方式、参数含义、常见误用,以及它为什么仍然难以被超越。

ECFP4(扩展连通性指纹,RDKit 中称 Morgan 指纹)是分子机器学习使用最广的特征。在深度学习盛行的今天它依然重要——大量对比研究发现,在几百到几千个分子的典型药物项目数据上,ECFP4 + 梯度提升/随机森林常常打平甚至优于复杂的图神经网络,而且快几个数量级、可解释性好得多。

正确的计算方式

from rdkit import Chem
from rdkit.Chem import rdFingerprintGenerator, DataStructs
import numpy as np

# 新接口(推荐)——旧的 AllChem.GetMorganFingerprintAsBitVect 已弃用
gen = rdFingerprintGenerator.GetMorganGenerator(
    radius=2,          # radius=2 对应 ECFP4(直径=2×半径)
    fpSize=2048,       # 位数
    includeChirality=False,
)

def smiles_to_fp_array(smiles_list, gen):
    X = np.zeros((len(smiles_list), gen.GetOptions().fpSize), dtype=np.uint8)
    ok = np.ones(len(smiles_list), dtype=bool)
    for i, smi in enumerate(smiles_list):
        mol = Chem.MolFromSmiles(smi)
        if mol is None:
            ok[i] = False
            continue
        fp = gen.GetFingerprint(mol)
        DataStructs.ConvertToNumpyArray(fp, X[i])
    return X, ok

X, ok = smiles_to_fp_array(df["clean_smiles"].tolist(), gen)
print(X.shape, "有效:", ok.sum())

参数含义与选择

参数 含义 建议
radius 从每个原子向外扩展的键数 2 = ECFP4(最常用);3 = ECFP6
fpSize 折叠后的位数 2048 常用;数据多可用 4096 减少冲突
includeChirality 是否区分立体化学 活性受立体化学影响时开启
countSimulation 用计数而非二值 某些任务上略有提升

「ECFP4 对应 radius=2」是最常见的困惑:ECFP 的数字指的是直径(diameter),而 RDKit 参数是半径(radius)。ECFP4 = 直径 4 = 半径 2。写错会导致与文献结果不可比。

计数指纹与稀疏表示

# 计数版本(保留子结构出现次数)
gen_count = rdFingerprintGenerator.GetMorganGenerator(radius=2, fpSize=2048)
fp_count = gen_count.GetCountFingerprint(mol)
count_array = np.zeros(2048, dtype=np.int32)
DataStructs.ConvertToNumpyArray(fp_count, count_array)

# 未折叠的稀疏指纹(无哈希冲突,适合相似性检索)
sparse = gen.GetSparseCountFingerprint(mol)
print("非零位数:", len(sparse.GetNonzeroElements()))

# 折叠会造成哈希冲突:不同子结构映射到同一位
# fpSize 越小冲突越多。2048 位对多数任务够用,
# 但做精确相似性检索时建议用稀疏版本

可解释性:ECFP 的重要优势

# 找出哪些子结构对应哪些位 —— 这是深度模型给不了的
from rdkit.Chem import Draw

ao = rdFingerprintGenerator.AdditionalOutput()
ao.AllocateBitInfoMap()
fp = gen.GetFingerprint(mol, additionalOutput=ao)
bit_info = ao.GetBitInfoMap()

# bit_info: {bit_id: ((atom_idx, radius), ...)}
for bit, envs in list(bit_info.items())[:5]:
    atom_idx, rad = envs[0]
    print(f"位 {bit}: 以原子 {atom_idx} 为中心、半径 {rad} 的环境")

# 可视化重要位对应的子结构
Draw.DrawMorganBit(mol, bit, bit_info)

# 结合模型的特征重要性,就能回答
# 「模型认为哪个子结构与活性相关」——
# 这对说服药化同事至关重要

建立基线模型

from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import cross_val_score
import lightgbm as lgb

# 基线一:随机森林
rf = RandomForestRegressor(n_estimators=500, n_jobs=-1, random_state=0)

# 基线二:梯度提升(通常更强)
gbm = lgb.LGBMRegressor(n_estimators=1000, learning_rate=0.05,
                        num_leaves=31, subsample=0.8,
                        colsample_bytree=0.6, random_state=0)

# 注意:必须用骨架划分而非随机划分(见 332)
from sklearn.model_selection import cross_val_predict
# scores = cross_val_score(gbm, X, y, cv=scaffold_cv, scoring="neg_root_mean_squared_error")

这个基线是后续所有工作的参照系。如果 Chemprop 或图神经网络赢不过它,说明问题出在数据量或标签质量上,换更复杂的模型没有意义——应该回头查数据。

常见误用

  • 没有标准化就算指纹:盐型、互变异构不同的同一化合物会得到不同指纹(见 328《RDKit 标准化分子》)。
  • 用错半径:ECFP4 = radius 2,不是 4。
  • 忽略哈希冲突:2048 位下不同子结构会碰撞,精确检索应用稀疏版本。
  • 用相似性代替模型:指纹相似不代表活性相似(活性悬崖普遍存在)。
  • 只用指纹不试描述符ECFP + 物化描述符拼接通常比单用任一种更稳,成本也很低。

常见坑与提示

  • ECFP4 = radius 2(直径 vs 半径),写错则与文献不可比;
  • 用新接口 rdFingerprintGenerator,旧接口已弃用;
  • 算指纹前必须先做分子标准化;
  • 它是必须先打的基线——复杂模型赢不过它,说明问题在数据不在模型。

延伸资源