交叉验证基础概念:训练集、验证集、测试集

各位,咱们先把基本功打扎实。做 ADMET预测 的交叉验证,如果连训练集、验证集、测试集都分不清楚,那后面的策略全是空中楼阁。我最早入行时也犯过这种糊涂,有一回把测试集的数据偷偷用到了模型调参里,结果上线后预测准确率直接崩了30%。

说白了,这三者的划分就是机器学习的「铁三角」。你想想看,模型需要一个学习的样本、需要一个调整方向的参考、还需要一个最终的考试。这三份数据一旦混用,你的模型成绩就是作弊得来的,根本没法用。

1. 三个集合的准确定义

先给一个简洁的归纳,我习惯这么记:

核心记忆口诀:
训练集(Training Set)→ 让模型学知识
验证集(Validation Set)→ 帮模型调参数(防止一意孤行)
测试集(Test Set)→ 最终打分,不参与任何决策

很多教科书讲得太文绉绉了。用ADMET预测的场景解释一下:

  • 训练集: 就是你的「课本」。里面包含化合物结构和对应的ADMET标签(比如是否具有肝毒性)。模型一遍遍看这些例题,记住规律。
  • 验证集: 相当于「模拟考」。模型学完一部分内容后,用这份没见过的题来测一下,看看是不是学偏了。根据模拟考成绩,你决定要不要调整学习率、正则化系数等超参数。
  • 测试集: 那是「高考」。模型所有参数都定下来以后,最后用它来评估模型的真实泛化能力。这份数据在训练过程中绝对不能碰。

2. 三者之间的「防火墙」规则

这里我要强调一个血的教训。我曾经接手过一个ADMET项目,某位同事把验证集效果不好的数据直接加回了训练集重新训练。看起来模型验证集准确率飙升,但一到测试集就原形毕露。这就是典型的数据泄露(Data Leakage)。

防火墙铁律:
  1. 测试集只能在最后用一次。
  2. 验证集可以多次使用,但不能参与模型参数的学习。
  3. 训练集不能包含任何验证集/测试集的信息,包括统计分布特征。
  4. 数据分割必须在数据预处理之前完成,防止预处理时「偷看」全局信息。

举个例子,如果你先对整个数据集做了标准化(StandardScaler),然后再划分训练集和测试集,这就有问题。因为标准化计算了全局均值和方差,测试集的信息已经渗透到训练过程中了。正确做法是:先划分,再用训练集的统计量去标准化验证集和测试集。

3. 常见的划分比例与工程陷阱

划分比例没有铁律,我根据自己的经验给出一张常用参考表:

数据集规模 训练集 验证集 测试集
小规模(< 1万条) 60%-70% 15%-20% 15%-20%
中等规模(1万-10万条) 70%-80% 10%-15% 10%-15%
大规模(> 10万条) 80%-90% 5%-10% 5%-10%

但这里有个坑——ADMET数据往往极度不平衡。比如阳性样本(有毒化合物)可能只占5%。如果简单随机划分,很可能验证集或者测试集里一个阳性样本都没有。那就废了。

我建议的做法是:

  • 使用 分层抽样(Stratified Split),确保每个集合中阳性/阴性比例与原始数据一致。
  • 对于ADMET中的多分类任务(比如CYP抑制活性分4级),按类别比例分层。
  • 如果数据量极少(比如只有几百条),干脆用留一法(LOO)代替固定划分,这点后面章节会细讲。

4. 代码示例:用Python实现严谨分割

这里展示我一直用的一个分割方案,用的是scikit-learn。记住,先分割,再预处理:

from sklearn.model_selection import train_test_split
import numpy as np

# X: 分子指纹特征 (假设已计算完毕)
# y: ADMET标签 (0: 无毒, 1: 有毒)
X = np.random.rand(10000, 2048)  # 模拟10000个分子
y = np.random.randint(0, 2, 10000)

# 第一步:分割出测试集 (占20%)
X_temp, X_test, y_temp, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# 第二步:从剩余数据中分割出验证集 (占原数据的20%)
X_train, X_val, y_train, y_val = train_test_split(
    X_temp, y_temp, test_size=0.25, random_state=42, stratify=y_temp
)

# 验证比例
print(f"训练集: {len(X_train)}, 验证集: {len(X_val)}, 测试集: {len(X_test)}")
print(f"训练集阳性比例: {y_train.mean():.3f}")
print(f"验证集阳性比例: {y_val.mean():.3f}")
print(f"测试集阳性比例: {y_test.mean():.3f}")
我的小技巧: random_state固定为同一个值(我用42用了好多年),这样每次分割结果可复现。团队协作时,这个问题经常引发争议,统一种子值是良好的工程规范。

5. 知识框架图:数据集划分的逻辑脉络

下面这张图我用来给团队新人做培训的。它把ADMET预测中的整个数据流和防火墙规则一次性说清楚了:

ADMET预测数据集划分与防火墙规则 全量标注数据 训练集 (80%) 学习分子-活性规律 验证集 (10%) 调整超参数和模型结构 测试集 (10%) 最终泛化评估 防火墙1:不可反馈 防火墙2:仅一次 模型训练 权重更新 验证集评估 & 超参调优 迭代N次 测试集评估 最终报告 反馈调整超参数,重新训练 注意:测试集在最终评估前不可见,验证集可多次使用但不能参与权重学习

这张图的精髓在于两道红色虚线防火墙。验证集的数据可以多次用于超参调优,但绝对不能进入训练集做权重更新;测试集更是连看都不能多看一眼。

6. ADMET场景下的特殊注意事项

最后聊点实际的。大多数公开ADMET数据集都有分子骨架相似性高的问题。比如同一个化学系列(scaffold)的化合物可能全部集中在训练集,而测试集里完全是新骨架。这种情况下,常规的随机划分会严重高估模型性能。

我的处理方案是:

  • 基于分子骨架的划分: 先对分子做 Murcko Scaffold 聚类,然后再按骨架类别划分。确保训练集和测试集没有共享骨架。
  • 时间序列分割: 如果ADMET数据带有时间戳(比如不同年份的化合物),用早期数据训练,晚期数据测试,更贴近真实研发流程。
  • 化合物相似度过滤: 在划分前,计算所有分子的 Tanimoto 相似度矩阵,确保验证集/测试集中没有与训练集高度相似的样本(阈值通常设为 0.85)。
一句话总结:
训练集是课本,验证集是模考,测试集是高考。
模考可以反复练,但高考不能提前看答案。
在ADMET预测里,还得加上「骨架隔离」这条强制措施。

好了,这三个集合的概念就讲到这里。记住它们的分工和边界,后面再深入各种交叉验证策略时,你才不会乱了阵脚。