模型在测试集上表现好,不代表在真实项目中有用。因为真实项目中面对的是「新骨架」与「未来的数据」,而随机划分的测试集两者都不是。评测设计的严格程度,直接决定了你对模型能力的判断是否准确。
划分方式的严格程度
| 划分方式 | 严格度 | 对应的真实场景 |
|---|---|---|
| 随机划分 | 最松 | 几乎不对应任何真实场景 |
| 骨架划分 | 中 | 遇到新的化学系列 |
| 严格骨架划分 | 较严 | 骨架 + 相似度双重约束 |
| 时间划分 | 最接近真实 | 用历史数据预测未来实验 |
| 聚类划分 | 较严 | 整个化学子空间未见过 |
| 跨项目划分 | 最严 | 模型迁移到新项目 |
时间划分是最诚实的评测:它完全模拟了模型在生产中的处境——用截至某个日期的数据训练,预测之后产生的实验结果。如果只能做一种评测,就做这个。
为什么随机划分严重高估性能
# 药物发现数据的特点:
# 分子不是独立采样的,而是【成系列产生的】
#
# 一个项目中,化学家围绕一个骨架
# 合成几十到几百个类似物
# → 这些分子彼此高度相似
#
# 随机划分的后果:
# 同一系列的分子被分散到训练集与测试集
# → 测试集中的每个分子,
# 训练集中都有它的「近亲」
# → 模型只需要「查表 + 插值」就能得高分
# → 【这不是泛化,是记忆】
#
# 典型的性能差距:
# 同一个模型,同一份数据:
# 随机划分 R² = 0.85
# 骨架划分 R² = 0.55
# 时间划分 R² = 0.40
# → 【差距巨大,且随机划分的数字毫无参考价值】
#
# 【一个诊断方法】:
# 计算测试集每个分子与训练集的最大相似度,
# 画分布图
# → 如果大量测试分子的最大相似度 > 0.7,
# 说明划分太松
import numpy as np
from rdkit import Chem, DataStructs
from rdkit.Chem import rdFingerprintGenerator
gen = rdFingerprintGenerator.GetMorganGenerator(radius=2, fpSize=2048)
def split_leakage_check(train_smiles, test_smiles):
tr_fps = [gen.GetFingerprint(Chem.MolFromSmiles(s))
for s in train_smiles if Chem.MolFromSmiles(s)]
max_sims = []
for s in test_smiles:
m = Chem.MolFromSmiles(s)
if m is None:
continue
sims = DataStructs.BulkTanimotoSimilarity(gen.GetFingerprint(m), tr_fps)
max_sims.append(max(sims))
max_sims = np.array(max_sims)
print(f"测试集与训练集的最大相似度:")
print(f" 中位数 {np.median(max_sims):.3f}")
print(f" > 0.7 的比例: {(max_sims > 0.7).mean():.1%} ← 【应该很低】")
print(f" > 0.5 的比例: {(max_sims > 0.5).mean():.1%}")
print(f" < 0.3 的比例: {(max_sims < 0.3).mean():.1%} ← 真正的外推")
return max_sims
各种划分的实现
from rdkit.Chem.Scaffolds import MurckoScaffold
from collections import defaultdict
import numpy as np
# ---- 骨架划分 ----
def scaffold_split(smiles, frac_train=0.8, generic=False):
groups = defaultdict(list)
for i, s in enumerate(smiles):
m = Chem.MolFromSmiles(s)
if m is None:
continue
scaf = MurckoScaffold.GetScaffoldForMol(m)
if generic:
# 【通用骨架】:忽略原子类型,只看拓扑
# 更严格 —— 苯环与吡啶环视为同一骨架
scaf = MurckoScaffold.MakeScaffoldGeneric(scaf)
groups[Chem.MolToSmiles(scaf)].append(i)
# 大骨架优先进训练集
order = sorted(groups.values(), key=len, reverse=True)
n_train = frac_train * len(smiles)
train, test = [], []
for g in order:
(train if len(train) + len(g) <= n_train else test).extend(g)
return train, test
# ---- 时间划分 ----
def temporal_split(df, date_col="date", cutoff="2024-01-01"):
train = df[df[date_col] < cutoff]
test = df[df[date_col] >= cutoff]
print(f"训练 {len(train)} (至 {cutoff}), 测试 {len(test)}")
return train, test
# 【最推荐】:需要数据有时间戳
# 如果内部数据没有记录合成/测定日期,
# 【现在就开始记录】—— 这是低成本高回报的数据治理
# ---- 聚类划分 ----
def cluster_split(smiles, frac_train=0.8, cutoff=0.6):
from rdkit.ML.Cluster import Butina
fps = [gen.GetFingerprint(Chem.MolFromSmiles(s)) for s in smiles]
dists = []
for i in range(1, len(fps)):
sims = DataStructs.BulkTanimotoSimilarity(fps[i], fps[:i])
dists.extend([1 - x for x in sims])
clusters = Butina.ClusterData(dists, len(fps), cutoff, isDistData=True)
clusters = sorted(clusters, key=len, reverse=True)
n_train = frac_train * len(smiles)
train, test = [], []
for c in clusters:
(train if len(train) + len(c) <= n_train else test).extend(c)
return train, test
# 【比骨架划分更严格】:即使骨架不同,
# 只要整体相似就分在同一边
外推的三个维度
- 结构外推:新的化学骨架——用骨架/聚类划分测试;
- 性质外推:预测超出训练集活性范围的值。模型几乎从不能外推到训练范围之外——如果训练集最高 pIC50 是 8,模型基本不会预测出 9.5。这个限制在先导优化后期尤其重要;
- 时间外推:化学空间随项目推进而漂移——用时间划分测试;
- 三者可以叠加:真实的部署场景通常同时面临这三种外推。
让模型更能外推的做法
# 1) 【更强的归纳偏置】
# 用图表示(保证价键规则)
# 用等变架构(保证对称性,见 160)
# 加入物化描述符(提供全局信息)
# → 【减少模型「记忆」的空间,迫使它学规律】
#
# 2) 【数据增强】
# SMILES 随机化(见 145)
# 构象采样
# → 让模型对表面变化不敏感
#
# 3) 【正则化】
# dropout、权重衰减、早停
# 集成
#
# 4) 【明确适用域】
# 与其追求「在所有分子上都准」,
# 不如【诚实地标注哪些分子不该预测】(见 166)
# → 这在实践中更有价值
#
# 5) 【持续更新】
# 定期用新数据重训练
# 监控性能漂移
#
# 6) 【降低期望】
# 对全新骨架,模型的作用是
# 「粗略排序」而非「精确预测」
# → 【承认能力边界,比夸大能力更有用】
# 【一个务实的评测报告应该包含】:
# - 随机划分的结果(作为上界参考)
# - 骨架划分的结果
# - 时间划分的结果(如果有时间戳)
# - 测试集与训练集的相似度分布
# - 分相似度区间的性能(相似度 > 0.6 / 0.4~0.6 / < 0.4)
# → 【最后一项最有信息量】:
# 它直接告诉你模型在什么距离上还可信
关键要点
- 随机划分几乎不对应任何真实场景——药物数据是成系列产生的,不是独立采样的;
- 时间划分最接近生产处境;如果内部数据没有时间戳,现在就开始记录;
- 模型几乎从不能外推到训练集的活性范围之外——先导优化后期尤其要注意;
- 最有信息量的报告是分相似度区间的性能,它告诉你模型在什么距离上还可信。
延伸资源
- 不确定性:166《不确定性估计》;Benchmark 陷阱:169《Benchmark 陷阱》;
- MoleculeNet:132《MoleculeNet 论文精读》;TDC:133《TDC 论文精读》;活性预测:155《AI 活性预测模型》。