165

主动学习 Active Learning:少做实验的前提是知道该问哪个问题

主动学习能显著减少实验次数,但前提是知道该问哪个问题。这篇讲清采集函数的选择、批量策略与实际约束。

主动学习的核心思想:不是随机做实验,而是让模型指出「做哪个实验最能减少不确定性」。在实验成本高、周期长的药物发现中,这个思路的价值很大——但它对模型的不确定性估计质量有很高要求

基本循环

# 1) 用已有数据训练模型
# 2) 用模型评估所有候选分子
# 3) 【用采集函数选出下一批要测试的分子】
# 4) 做实验,得到真实数据
# 5) 把新数据加入训练集,回到 1)
#
# 关键在第 3 步:怎么选
#
# 【核心权衡:探索 vs 利用】
#   利用(exploitation):选模型预测最好的
#     → 短期收益高,但可能陷在局部
#   探索(exploration):选模型最不确定的
#     → 改善模型,但当前批次的命中率可能低
#
# 在药物发现中,这个权衡有实际含义:
#   利用 = 「这一轮就要拿到活性分子」
#   探索 = 「让模型变好,为后续几轮服务」
#   → 【项目早期偏探索,后期偏利用】

常用的采集函数

采集函数 选择标准 倾向
贪心 预测值最高 纯利用
不确定性采样 预测方差最大 纯探索
UCB μ + κσ 可调平衡,简单可靠
期望改进(EI) 超过当前最好值的期望 自适应平衡
改进概率(PI) 超过阈值的概率 偏利用
Thompson 采样 从后验采样后取最优 天然平衡,易并行
多样性加权 + 与已选分子的差异 批量场景必需

批量选择:实际场景的关键

# 实验通常是【成批】做的(一次 96 孔板、一批合成)
# 而多数采集函数是为「一次选一个」设计的
#
# 【朴素做法的问题】:
#   直接选采集函数得分最高的 96 个
#   → 它们可能高度相似(都在同一个区域)
#   → 【实际获得的信息远少于 96 个独立实验】
#
# 正确做法:

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

gen = rdFingerprintGenerator.GetMorganGenerator(radius=2, fpSize=2048)

def batch_select(candidates_smiles, mu, sigma, batch_size=96,
                 kappa=2.0, diversity_weight=0.5):
    """UCB + 贪心多样性的批量选择"""
    fps = []
    for s in candidates_smiles:
        m = Chem.MolFromSmiles(s)
        fps.append(gen.GetFingerprint(m) if m else None)

    ucb = mu + kappa * sigma
    selected = []
    remaining = set(range(len(candidates_smiles)))

    while len(selected) < batch_size and remaining:
        best_i, best_score = None, -np.inf
        for i in remaining:
            if fps[i] is None:
                continue
            # 与已选分子的最大相似度(越小越多样)
            if selected:
                sims = DataStructs.BulkTanimotoSimilarity(
                    fps[i], [fps[j] for j in selected])
                penalty = max(sims)
            else:
                penalty = 0.0
            score = ucb[i] - diversity_weight * penalty
            if score > best_score:
                best_score, best_i = score, i
        if best_i is None:
            break
        selected.append(best_i)
        remaining.discard(best_i)
    return selected

# 其它批量策略:
#   - Thompson 采样:每个槽位独立从后验采样
#     → 天然产生多样性,且易并行
#   - 局部惩罚:选中一个后,降低其邻域的采集函数值
#   - 确定性点过程(DPP):显式建模多样性
#   - 【聚类后每簇选一个】:简单有效

成败取决于不确定性估计的质量

  • 这是主动学习最关键的前提:如果模型的不确定性估计不准,采集函数就是在瞎选(见 166《不确定性估计》);
  • 常见问题:神经网络往往过度自信——在自己完全不懂的区域也给出低不确定性;
  • 推荐的做法
    • 集成方法(多个种子训练,用预测方差)——简单可靠,实践中效果最稳定;
    • 高斯过程——不确定性有理论保证,但难以处理大数据;
    • MC Dropout——便宜但校准往往不好;
    • 加上适用域判断(与训练集的相似度)作为额外的保险。
  • 必须验证校准在开始主动学习前,先检查「模型说不确定的样本,是不是真的错得更多」——这个检查不做,整个循环可能是无效的。

实际约束

# 主动学习的理论很美,实际约束很多:
#
# 1) 【合成可行性】
#    模型选出的分子可能根本合成不了
#    → 【候选池应该是「可获得的分子」】:
#      商业可购买的库、
#      内部化合物库、
#      或有明确合成路线的分子
#    → 这个约束常常被忽略,导致选出的分子无法测试
#
# 2) 【实验周期】
#    一轮实验可能要 2~8 周
#    → 主动学习的轮数有限(一个项目可能只有 3~10 轮)
#    → 【每轮的批量要足够大】
#
# 3) 【实验成本与批量的经济性】
#    有时一次做 96 个与做 20 个成本差不多
#    → 批量大小由实验经济性决定,而非算法
#
# 4) 【化学家的意见】
#    模型选的分子,化学家可能认为没有价值
#    → 【应该把模型建议与专家判断结合】
#    → 实践中常见的做法:
#      模型选 200 个 → 化学家从中挑 96 个
#
# 5) 【多目标】
#    实际不只优化活性,还有选择性、ADMET
#    → 需要多目标的采集函数
#    → 或用帕累托前沿的思路
#
# 6) 【冷启动】
#    第一轮没有数据,模型无法工作
#    → 用多样性采样(覆盖化学空间)
#    → 或用文献数据做初始模型

评估主动学习的效果

# 【必须与随机基线比较】
#
# 回溯模拟(retrospective simulation):
#   用已完成项目的全部数据,
#   模拟「如果当初用主动学习会怎样」
#
#   做法:
#     1) 随机取 5% 作为初始训练集
#     2) 用主动学习策略选下一批
#     3) 「揭晓」这批的真实标签(数据已有)
#     4) 重复
#     5) 与「随机选择」对比
#
#   指标:
#     - 达到某个性能所需的样本数
#     - 前 N 轮累计发现的活性分子数
#     - 模型性能随轮次的提升曲线
#
#   【重复多次取平均】—— 单次模拟的方差很大

def simulate_active_learning(X, y, strategy, n_init=50, n_batch=32,
                             n_rounds=10, n_repeats=10):
    import numpy as np
    results = []
    for rep in range(n_repeats):
        rng = np.random.default_rng(rep)
        train_idx = list(rng.choice(len(X), n_init, replace=False))
        pool = [i for i in range(len(X)) if i not in set(train_idx)]
        found = []
        for r in range(n_rounds):
            model = fit(X[train_idx], y[train_idx])
            mu, sigma = model.predict_with_uncertainty(X[pool])
            picks = strategy(mu, sigma, n_batch)
            chosen = [pool[i] for i in picks]
            found.append(sum(y[i] >= ACTIVE_THRESHOLD for i in chosen))
            train_idx += chosen
            pool = [i for i in pool if i not in set(chosen)]
        results.append(np.cumsum(found))
    return np.array(results).mean(axis=0)

# 【与 random strategy 对比,差距才是主动学习的价值】

关键要点

  • 不确定性估计的质量决定成败——开始前必须验证「模型说不确定的,是否真的错得更多」;
  • 批量选择必须加多样性约束,否则选出的 96 个高度相似,信息量远小于 96 个独立实验;
  • 候选池必须是「可获得的分子」——选出合成不了的分子等于白做;
  • 用回溯模拟评估,必须与随机选择基线比较并重复多次取平均。

延伸资源