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,旧接口已弃用; - 算指纹前必须先做分子标准化;
- 它是必须先打的基线——复杂模型赢不过它,说明问题在数据不在模型。