做活性预测时,最常见的错误不是选错模型,而是没想清楚这个模型要支持什么决策。先定义决策,再选择建模方式——这个顺序颠倒了,后面的所有工作都会偏离。
从决策倒推建模方式
| 要支持的决策 | 合适的建模方式 | 关键指标 |
|---|---|---|
| 从百万库中选一万个去筛 | 排序 / 分类 | 富集因子、命中率 |
| 判断某个分子值不值得合成 | 回归 + 不确定性 | 校准的预测区间 |
| 在一个系列内排优先级 | 排序 | Spearman、Kendall tau |
| 预测具体的 IC50 数值 | 回归 | RMSE、MAE |
| 判断是否达到某个活性阈值 | 分类 | 精确率、召回率、PR-AUC |
| 决定下一批做什么实验 | 主动学习(见 165《主动学习 Active Learning》) | 信息增益 |
最常见的错配:用 RMSE 优化的回归模型去做虚拟筛选。虚拟筛选只关心「前 1% 里有多少真阳性」,而 RMSE 是对所有样本的平均误差——模型可能在低活性区域表现很好(那里样本多),而在我们真正关心的高活性区域很差。
各建模方式的要点
# ---- 分类 ----
# 关键:阈值怎么定
#
# 常见做法:IC50 < 1 μM 算活性
# 问题:
# - 阈值附近的分子被硬性二分,信息损失
# - 不同项目的阈值不同
# - 【类别不平衡严重】:
# 真实筛选中活性分子可能只占 0.1%
#
# 应对:
# - 用 PR-AUC 而非 ROC-AUC
# 【ROC-AUC 在极度不平衡时会给出误导性的高值】
# - 报告不同阈值下的精确率与召回率
# - 考虑用多个阈值做有序分类
#
# ---- 回归 ----
# 关键:预测什么尺度
#
# 【必须用 pIC50 / pKi(对数尺度)而非原始浓度】
# 原因:
# - 活性跨越多个数量级
# - 对数尺度下误差分布更接近正态
# - 与结合自由能线性相关
# - 实验误差在对数尺度上更均匀
#
# 注意「删失数据」:
# 很多数据是「> 10 μM」(未测到活性)
# → 【直接当作 10 μM 会引入偏差】
# → 正确做法:用删失回归(Tobit 模型)
# 或至少标记出来单独处理
#
# ---- 排序 ----
# 关键:在什么范围内排序
#
# 全局排序 vs 组内排序
# → 实际决策通常是【组内】的:
# 「这 20 个类似物哪个先合成」
# → 应该用组内的排序指标评价
#
# 学习排序方法:
# pairwise(如 RankNet)、listwise(如 LambdaRank)
# → 直接优化排序目标
# → 【在样本少时常优于回归后排序】
评价指标的选择
import numpy as np
from scipy import stats
def enrichment_factor(y_true, y_score, fraction=0.01, threshold=6.0):
"""富集因子:虚拟筛选最重要的指标"""
n = len(y_true)
n_top = max(1, int(n * fraction))
order = np.argsort(-np.asarray(y_score))
actives = np.asarray(y_true) >= threshold
hits_top = actives[order[:n_top]].sum()
rate_top = hits_top / n_top
rate_all = actives.mean()
return rate_top / rate_all if rate_all > 0 else np.nan
def bedroc(y_true, y_score, alpha=20.0, threshold=6.0):
"""BEDROC:对排名靠前的正例给更高权重"""
from rdkit.ML.Scoring import Scoring
order = np.argsort(-np.asarray(y_score))
labels = [[1 if y_true[i] >= threshold else 0] for i in order]
return Scoring.CalcBEDROC(labels, 0, alpha)
def per_group_spearman(df, group_col="series", true_col="y", pred_col="pred"):
"""分组计算排序相关性 —— 更贴近实际决策"""
out = {}
for g, sub in df.groupby(group_col):
if len(sub) >= 5:
out[g] = stats.spearmanr(sub[true_col], sub[pred_col]).correlation
vals = np.array(list(out.values()))
print(f"分组 Spearman: 中位数 {np.median(vals):.3f}, "
f"范围 [{vals.min():.3f}, {vals.max():.3f}]")
# 【整体相关性高但分组内很差,是常见情况】
return out
# 【报告建议】:
# 虚拟筛选场景:EF@1%、EF@0.1%、BEDROC
# 先导优化场景:分组 Spearman 的分布
# 数值预测场景:RMSE + 预测区间的覆盖率
# 任何场景:都要报告多种子的均值与标准差
数据准备中的关键问题
- 不同测定方法的数据不能直接混用。这是最常见的数据问题。同一个靶点的 IC50,用不同的酶浓度、底物浓度、孵育时间测出来可能差几倍——混在一起训练会让模型学习噪声;
- 处理重复测量:同一分子多次测定,取中位数比取平均更稳健(对异常值不敏感);
- 删失数据:「> 10 μM」的信息不能丢,也不能当作精确值;
- 活性悬崖:结构相似但活性差异巨大的分子对——这些是模型最容易错的地方,也是最有信息量的。应该单独评估模型在活性悬崖上的表现;
- 时间划分:用历史数据训练、预测后续实验,是最诚实的评测(见 167《模型外推性》)。
模型输出必须配套的东西
# 一个只给出预测值的模型,在实际决策中价值有限
#
# 必须配套:
#
# 1) 【不确定性估计】(见 166)
# 预测值 ± 置信区间
# → 高不确定的预测不应驱动昂贵的决策
#
# 2) 【适用域判断】
# 这个分子是否在模型的能力范围内?
# 简单做法:与训练集的最大 Tanimoto 相似度
# > 0.6 可能可靠
# 0.4~0.6 谨慎
# < 0.4 【预测基本不可信】
#
# 3) 【与最近邻的对比】
# 展示训练集中最相似的几个分子及其实测活性
# → 【化学家能立刻判断预测是否合理】
# → 这个简单的功能极大提高了模型的可用性
#
# 4) 【解释】
# 哪些结构特征驱动了这个预测?(见 168)
# → 但要注意解释的可靠性
#
# 5) 【模型的历史表现】
# 在类似分子上,这个模型过去准不准?
def prediction_report(mol_smiles, model, train_data, k=5):
"""给出预测 + 上下文,而非孤立的数字"""
pred, unc = model.predict_with_uncertainty(mol_smiles)
neighbors = find_nearest(mol_smiles, train_data, k=k)
max_sim = neighbors[0]["similarity"]
domain = ("可靠" if max_sim > 0.6 else
"谨慎" if max_sim > 0.4 else "超出适用域")
return {
"prediction": pred,
"uncertainty": unc,
"applicability": domain,
"max_train_similarity": max_sim,
"nearest_neighbors": neighbors, # 【最有用的部分】
}
关键要点
- 先定义决策再选建模方式——用 RMSE 优化的回归做虚拟筛选是最常见的错配;
- 类别极度不平衡时用 PR-AUC 而非 ROC-AUC,后者会给出误导性的高值;
- 删失数据(「> 10 μM」)不能当作精确值,也不能丢弃;
- 模型输出必须配套不确定性、适用域判断与最近邻对比,孤立的数字价值有限。
延伸资源
- ADMET 预测:156《AI ADMET 预测模型》;DTI 模型:157《AI DTI 预测模型》;不确定性:166《不确定性估计》;
- 主动学习:165《主动学习 Active Learning》;模型外推性:167《模型外推性》;Chemprop:131《Chemprop / D-MPNN 论文精读》。