011

Practical Cheminformatics 博客:药物化学家的计算入门指南

Practical Cheminformatics 提供了实践视角与批判性思维。这篇讲清它的价值、核心主题与学习方式。

Practical Cheminformatics(Pat Walters 的博客)在 AI 制药的学习资源中位置特殊:它不教方法,而是教「什么时候不该相信方法」。这种基于长期工业实践的批判性视角,是教科书与论文里学不到的。

反复出现的核心主题

主题 核心观点
基线的重要性 先跑简单基线,很多复杂方法打不过它
数据划分 随机划分会严重高估性能(见 051《Scaffold Split》
统计严谨性 报告置信区间;单次运行不足信
数据质量 清洗与标准化的价值高于换模型
评价指标 指标要对应实际决策(见 155《AI 活性预测模型》
论文的批判性阅读 识别不公平比较与夸大宣称(见 169《Benchmark 陷阱》
可视化 看数据比看指标更能发现问题
化学直觉 模型输出必须经得起化学检验

「先跑基线」的具体含义

# 【这是整个博客最核心的实践建议】
#
# 在评估任何新方法之前,先建立基线:

import numpy as np
import lightgbm as lgb
from rdkit import Chem
from rdkit.Chem import Descriptors, rdFingerprintGenerator

gen = rdFingerprintGenerator.GetMorganGenerator(radius=2, fpSize=2048)
DESC_NAMES = [d[0] for d in Descriptors._descList]

def baseline_features(smiles_list, n_desc=50):
    """ECFP + 常用描述符"""
    X = []
    for smi in smiles_list:
        m = Chem.MolFromSmiles(smi)
        if m is None:
            X.append(np.zeros(2048 + n_desc))
            continue
        fp = np.array(gen.GetFingerprint(m))
        desc = []
        for name in DESC_NAMES[:n_desc]:
            try:
                desc.append(getattr(Descriptors, name)(m))
            except Exception:
                desc.append(0.0)
        X.append(np.concatenate([fp, np.nan_to_num(desc)]))
    return np.array(X)

def baseline_model(X, y, seeds=range(5)):
    """多种子训练,返回均值与标准差"""
    return [lgb.LGBMRegressor(n_estimators=800, learning_rate=0.05,
                              num_leaves=31, subsample=0.8,
                              colsample_bytree=0.8, random_state=s,
                              verbose=-1).fit(X, y)
            for s in seeds]

# 【这个基线的特点】:
#   - 训练几分钟
#   - 不需要 GPU
#   - 部署简单(一个 pickle 文件)
#   - 结果稳定可复现
#
# 【判断标准】:
#   新方法必须【显著】超过它才值得采用
#   「显著」= 提升 > 2 倍种子间标准差
#   且要考虑增加的复杂度与部署成本

统计严谨性:常被忽略的部分

# 【博客反复强调的一点】:
#   分子数据集小,结果的方差大
#   → 【不报告不确定性的结果没有意义】

import numpy as np
from scipy import stats

def bootstrap_ci(y_true, y_pred, metric_fn, n_boot=1000, alpha=0.05, seed=0):
    """用自助法给指标算置信区间"""
    rng = np.random.default_rng(seed)
    n = len(y_true)
    scores = []
    for _ in range(n_boot):
        idx = rng.integers(0, n, n)
        try:
            scores.append(metric_fn(y_true[idx], y_pred[idx]))
        except Exception:
            continue
    lo, hi = np.percentile(scores, [100 * alpha / 2, 100 * (1 - alpha / 2)])
    return float(np.mean(scores)), float(lo), float(hi)

def compare_models(y_true, pred_a, pred_b, metric_fn, n_boot=1000, seed=0):
    """比较两个模型:差值的置信区间是否包含 0"""
    rng = np.random.default_rng(seed)
    n = len(y_true)
    diffs = []
    for _ in range(n_boot):
        idx = rng.integers(0, n, n)
        diffs.append(metric_fn(y_true[idx], pred_a[idx])
                     - metric_fn(y_true[idx], pred_b[idx]))
    lo, hi = np.percentile(diffs, [2.5, 97.5])
    significant = not (lo <= 0 <= hi)
    print(f"差值 {np.mean(diffs):.4f} [{lo:.4f}, {hi:.4f}] "
          f"{'显著' if significant else '【不显著】'}")
    return significant

# 【实际经验】:
#   很多论文报告的「提升」,
#   用这个方法检验后是不显著的
#   → 【在自己的工作中先做这个检验,
#     能避免向团队汇报不存在的提升】

「看数据」的重要性

  • 博客的一个反复建议:不要只看汇总指标,要看具体的分子
  • 具体做法
    • 把预测误差最大的 20 个分子画出来看——常常能发现数据问题(错误的结构、异常的标签)
    • 画预测值 vs 实测值的散点图,而非只看 R²;
    • 按化学系列分组看性能,而非只看整体;
    • 用 mols2grid 等工具批量查看分子(见 217《Mols2grid》)。
  • 为什么有效汇总指标会掩盖系统性问题。R² = 0.7 可能是「大部分预测得不错」,也可能是「一个系列很准、另一个系列完全错」——后者在实际使用中是灾难性的
from rdkit import Chem
from rdkit.Chem import Draw
import numpy as np

def inspect_worst_predictions(smiles, y_true, y_pred, n=20):
    """看看模型错得最离谱的分子"""
    errors = np.abs(np.asarray(y_true) - np.asarray(y_pred))
    worst = np.argsort(-errors)[:n]
    mols, legends = [], []
    for i in worst:
        m = Chem.MolFromSmiles(smiles[i])
        if m is None:
            print(f"【无效结构】: {smiles[i]}")   # ← 常常在这里发现问题
            continue
        mols.append(m)
        legends.append(f"真值 {y_true[i]:.2f} 预测 {y_pred[i]:.2f}")
    return Draw.MolsToGridImage(mols, molsPerRow=4, legends=legends,
                                subImgSize=(280, 220))

# 【常见的发现】:
#   - 结构解析失败的分子
#   - 标签明显异常的数据点(录入错误)
#   - 某一类化学结构系统性地预测偏差
#   - 训练集中根本没有类似结构(适用域外,见 166)

学习方式建议

做法 说明
跟着跑 Notebook 博客配有可运行的代码
在自己的数据上重做 这是真正的学习
对照检查自己的项目 用博客提出的问题自查
读它对具体论文的评论 学习批判性阅读的方法
关注方法学的讨论 而非只看结论

它能补上的认知空白

  • 教科书不讲的实践陷阱:数据泄漏、不公平基线、指标误用;
  • 论文不会说的局限:作者通常不会强调自己方法的弱点;
  • 工业与学术的差异:学术追求指标提升,工业关心的是「这能不能改变决策」
  • 化学与计算的结合:作者兼具药化与计算背景,能指出纯计算背景的人看不到的问题
  • 合理的怀疑态度对领域内的热门宣称保持审慎,而非全盘接受或全盘否定

关键要点

  • 核心建议是先跑简单基线——ECFP + LightGBM,几分钟、无需 GPU、结果稳定;
  • 不报告不确定性的结果没有意义——用自助法给指标算置信区间;
  • 看具体的分子而非只看汇总指标——误差最大的 20 个分子常常暴露数据问题;
  • 它补的是「什么时候不该相信方法」,这是教科书与论文里学不到的。

延伸资源