155

AI 活性预测模型:先定义决策,再选择分类、回归或排序

活性预测模型该做分类、回归还是排序,取决于你要支持什么决策。这篇给出从决策倒推建模方式的完整框架。

做活性预测时,最常见的错误不是选错模型,而是没想清楚这个模型要支持什么决策先定义决策,再选择建模方式——这个顺序颠倒了,后面的所有工作都会偏离。

从决策倒推建模方式

要支持的决策 合适的建模方式 关键指标
从百万库中选一万个去筛 排序 / 分类 富集因子、命中率
判断某个分子值不值得合成 回归 + 不确定性 校准的预测区间
在一个系列内排优先级 排序 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」)不能当作精确值,也不能丢弃;
  • 模型输出必须配套不确定性、适用域判断与最近邻对比,孤立的数字价值有限。

延伸资源