训练集与测试集划分:机器学习评估的基石与避坑指南

发布时间:2026/10/27 22:08:22
训练集与测试集划分:机器学习评估的基石与避坑指南

做机器学习项目,很多人把八成精力砸在模型选型和调参上,结果模型在本地跑出漂亮指标,一上线就拉胯。我见过太多这样的案例,回头一查,问题往往出在最不起眼的一步:训练集和测试集到底怎么分的。这个环节看起来就是调个库函数、写一行train_test_split的事,但它直接决定了你评估出来的指标到底可不可信。分得好,模型评估结果接近真实表现;分得随意,你拿到的可能只是一组自我安慰的数字。

这篇内容我想把“训练集与测试集划分”这件事彻底讲透。它适合刚入门机器学习、正在跑第一个分类或回归项目的朋友,也适合已经做过几个项目、但评估结果总是不稳定的从业者。我会从划分背后的设计逻辑讲起,拆解几种常见划分方式的适用场景和取舍理由,再落到具体代码和参数选择,最后把我自己踩过的坑和排查经验整理出来。核心关键词就一个:训练集和测试集的划分,但围绕它展开的细节,远比一行代码丰富。

1. 划分这件事到底在解决什么问题

1.1 从“背题”和“考试”的类比说起

理解划分的意义,最直观的类比就是学生备考。训练集是学生平时做的练习题,测试集是考场上从没见过的卷子。如果一个学生把练习册的答案全背下来了,练习题做得飞快,但一上考场遇到新题就懵,这就是典型的“过拟合”——模型在训练数据上表现极好,在未见过的数据上表现很差。划分训练集和测试集的根本目的,就是人为制造一个“没见过的卷子”,用来检验模型是真的学到了规律,还是只是记住了训练数据的噪声。

这里有个关键点容易被忽略:测试集必须在整个建模过程中保持“不可见”。很多人在调参时反复用测试集评估,看到指标不好就改参数再测,改来改去,测试集的信息就一点点泄漏进了模型选择过程。这时候测试集已经不再是“没见过的卷子”,而是变成了“你反复研究过的模拟题”,评估结果自然虚高。所以划分不只是切一刀的事,还包含一整套“测试集隔离”的纪律。

1.2 训练集、验证集、测试集三者的分工

严格来说,一个规范的机器学习流程涉及三个数据集,而不是两个。训练集用来拟合模型参数;验证集用来做模型选择和超参数调优;测试集只在最后用一次,给出对泛化能力的最终估计。很多人把验证集和测试集混为一谈,或者干脆只用训练集和测试集两分法,然后在测试集上反复调参,这是评估失真的重灾区。

三分法的逻辑是:调参阶段你允许自己“看”验证集的表现来做出决策,但测试集要留到模型完全定稿后再碰。这样测试集给出的指标才是对未知数据表现的无偏估计。当然,如果数据量很小,三分法会让每份数据都太少,这时候可以用交叉验证来替代单独的验证集,但测试集依然要单独留出来。这个取舍后面会详细讲。

1.3 划分比例背后的权衡

常见的划分比例是训练集占70%到80%,测试集占20%到30%。这个范围不是拍脑袋定的,背后是偏差和方差之间的权衡。训练集越大,模型学到的规律越充分,参数估计越稳定,但测试集越小,评估结果的方差就越大——可能你换一个随机种子,测试指标就波动好几个百分点。反过来,测试集留得多,评估更稳定,但训练数据不够,模型欠拟合的风险上升。

我的经验是:数据量在万级以上时,测试集留20%甚至10%都够用,因为绝对样本数足够多,评估方差可控;数据量在千级以下时,测试集比例可以适当提高到25%到30%,保证评估有一定统计意义,同时配合交叉验证来充分利用数据。如果数据量只有几百条,那单次划分的评估结果基本不可信,必须上交叉验证,甚至要考虑用分层抽样保证类别分布。

2. 几种划分方式的适用场景与取舍

2.1 简单随机划分:最常用但不总是最合适

简单随机划分就是打乱数据后按比例切分,train_test_split默认干的就是这件事。它的优点是实现简单、无偏,适合样本独立同分布的场景,比如一堆用户行为记录、一批传感器读数。但它有个隐含假设:样本之间相互独立。一旦数据存在时间顺序、分组结构或者类别不平衡,简单随机划分就会出问题。

举个典型场景:你做的是用户流失预测,同一个用户有多条行为记录。如果随机划分,同一个用户的不同记录可能同时出现在训练集和测试集里,模型在训练时已经“见过”这个用户的行为模式,测试时自然表现好,但这个好成绩是虚的,因为真实场景下你要预测的是全新用户。这就是数据泄漏的一种形式,叫分组泄漏。遇到这种情况,必须按用户ID分组划分,保证同一个用户的所有记录只出现在一边。

2.2 分层抽样:类别不平衡时的必备手段

当你的分类任务类别分布不均衡时,比如正样本只占5%,简单随机划分很可能导致训练集或测试集里正样本比例偏离总体,甚至某一类样本在测试集里几乎没有。这会带来两个问题:训练时模型见不到足够的少数类样本,测试时评估指标波动极大。分层抽样(stratified sampling)就是为解决这个问题设计的,它保证划分后各子集的类别比例与原始数据一致。

在train_test_split里加一个stratify=y参数就能实现。这个参数看起来不起眼,但在不平衡数据上效果立竿见影。我做过一个欺诈检测的小项目,正样本占比不到3%,不加分层时测试集里正样本数量忽多忽少,召回率指标每次跑都不一样;加上分层后,每次划分的类别分布稳定,评估结果的可比性大大提升。需要注意的是,分层抽样要求每个类别至少有足够样本,如果某个类别只有个位数样本,分层也可能失败,这时候要考虑合并稀有类别或者用其他策略。

2.3 时间序列划分:绝不能随机打乱

时间序列数据是划分规则最特殊的一类。股票价格、销量预测、设备运行状态监测,这些数据都有时间先后顺序,样本之间不独立。如果你随机打乱后划分,等于用未来的数据预测过去,模型在测试集上的表现会好得不真实,但上线后完全不能用。正确做法是按时间切分:用较早时间段的数据做训练,较晚时间段的数据做测试,中间可以留一段做验证。

具体操作上,可以按时间戳排序后取前80%做训练、后20%做测试,或者用时间序列交叉验证(如TimeSeriesSplit),每次用前面的窗口训练、紧接着的窗口验证,滚动前进。这里有个细节:训练集和测试集之间最好留一个“间隔期”(gap),避免特征工程中用到未来信息时产生边界泄漏。比如你做的是未来7天销量预测,训练集最后一天和测试集第一天之间至少要隔7天,否则测试集的特征窗口会碰到训练集标签覆盖的时间段。

2.4 交叉验证:小数据集的评估利器

当数据量不足以支撑一次可靠的留出法评估时,交叉验证是更好的选择。K折交叉验证把数据分成K份,每次用K-1份训练、1份验证,轮换K次后取平均指标。这样每个样本都参与过训练和验证,数据利用率高,评估结果也更稳定。常见的K取5或10,K越大计算成本越高,但偏差越小。

交叉验证和留出法不是互斥的。规范做法是:先用交叉验证在训练集上做模型选择和调参,定稿后再用独立测试集做最终评估。很多人图省事,直接在全部数据上做交叉验证然后报告平均指标,这其实高估了模型表现,因为交叉验证过程中你已经根据验证结果做了决策,信息已经泄漏。交叉验证的定位是“训练阶段的评估工具”,不是“最终成绩单”。

3. 实操过程与关键参数落地

3.1 用train_test_split完成一次规范划分

先看最基础的代码骨架。假设特征矩阵是X,标签是y,要做一次带分层的划分:

from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y, shuffle=True )

逐参数说清楚。test_size=0.2表示测试集占20%,也可以传整数表示测试集绝对样本数。random_state=42固定随机种子,保证每次划分结果可复现——这一点在团队协作和结果对比时极其重要,没有固定种子,你连“这次调参比上次好”都说不清楚。stratify=y开启分层,前面讲过它的价值。shuffle=True是默认值,表示划分前打乱数据,时间序列场景要设为False并配合其他处理。

划分完成后,第一件事不是急着训练模型,而是检查两个子集的分布是否一致。可以对比训练集和测试集的标签均值、关键特征的均值方差,如果差异明显,说明划分可能有问题。这个检查步骤很多人跳过,但它能在早期发现数据泄漏或抽样偏差。

3.2 分组划分的代码实现

按用户ID分组划分,需要用到GroupShuffleSplit:

from sklearn.model_selection import GroupShuffleSplit gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, test_idx = next(gss.split(X, y, groups=user_ids)) X_train, X_test = X[train_idx], X[test_idx] y_train, y_test = y[train_idx], y[test_idx]

关键在groups参数,它告诉划分器哪些样本属于同一组,同一组样本只会整体进入训练集或测试集。这个方式在医疗数据(同一患者多条记录)、推荐系统(同一用户多次交互)、设备监测(同一设备多时段数据)里都是标配。我见过有人用简单随机划分处理患者数据,结果同一患者的记录横跨训练测试,模型评估虚高十几个百分点,这种错误在评审时是硬伤。

3.3 时间序列划分的具体操作

时间序列划分用TimeSeriesSplit:

from sklearn.model_selection import TimeSeriesSplit tscv = TimeSeriesSplit(n_splits=5, gap=7) for train_idx, test_idx in tscv.split(X): X_train, X_test = X[train_idx], X[test_idx] y_train, y_test = y[train_idx], y[test_idx] # 训练与评估

n_splits=5表示切5折,gap=7表示训练集和测试集之间留7个时间单位的间隔,防止边界泄漏。注意TimeSeriesSplit的测试窗口是顺序向前的,不会打乱时间顺序。如果你的数据有明确的时间戳,划分前务必按时间排序,否则索引顺序错乱会导致训练集里混入未来数据。

3.4 划分后的分布校验清单

划分完成后的校验,我习惯做成一个固定清单,每次项目都过一遍:

校验项检查方法异常信号
类别比例对比训练集与测试集各类占比差异超过3个百分点需警惕
样本量确认测试集绝对样本数少于100条时评估方差偏大
特征分布对比关键特征均值与方差明显偏移提示抽样偏差
分组完整性检查同一组是否跨集跨集即存在分组泄漏
时间顺序确认训练集时间早于测试集出现未来数据即泄漏

这张表看着简单,但每一条都对应过真实翻车案例。尤其是分组完整性和时间顺序两项,一旦出问题,模型指标再好看也没有参考价值。

4. 常见问题与排查技巧实录

4.1 测试指标忽高忽低怎么办

这是最常见的困惑:同样的模型,换个随机种子重跑,测试准确率能从0.82跳到0.76。原因通常是测试集太小或者划分不稳定。排查顺序是:先看测试集绝对样本数,如果只有几十条,波动大是必然的,考虑增大测试集比例或改用交叉验证;再看是否开了分层,类别不平衡时不分层会导致每次划分的类别构成差异大;最后检查数据本身是否有重复样本或近似重复样本,重复样本随机落入不同子集会放大波动。

4.2 训练指标很高但测试指标很差

这是过拟合的典型信号,但先别急着加正则化,先排查划分是否泄漏。检查清单:特征里是否包含了标签的未来信息(比如用“最终是否流失”这个字段去预测“是否流失”);分组数据是否被随机划分导致同组泄漏;时间序列是否被打乱。我遇到过一个案例,特征工程时把全量数据的统计量(如全局均值)算进了特征,这个统计量包含了测试集信息,导致测试指标虚高,修正后指标回落但更真实。这类泄漏隐蔽性强,排查时要逐个特征追溯来源。

4.3 小样本数据怎么划分才靠谱

数据只有几百条时,单次留出法评估基本不可信。我的做法是:用5折交叉验证在训练集上评估和调参,同时保留一个占15%到20%的独立测试集做最终验证。如果数据少到连独立测试集都留不出,那就用嵌套交叉验证——外层交叉验证做评估,内层交叉验证做调参,虽然计算成本高,但评估结果最可靠。另一个技巧是重复多次随机划分取平均,比如重复10次不同种子的划分,报告指标的均值和标准差,比单次结果稳健得多。

4.4 划分比例到底怎么定

没有万能比例,但有个决策思路:先看数据量级,万级以上测试集10%到20%足够;千级左右用20%到25%;百级以下必须交叉验证。再看任务类型,分类任务测试集要保证每个类别有足够样本,必要时提高测试集比例;回归任务对测试集绝对数量要求相对低一些。最后看评估指标的稳定性要求,如果业务对指标波动敏感,宁可多留测试数据换取评估稳定性。这个决策没有标准答案,但把数据量、类别分布、业务容忍度三个因素摆出来,比例自然就清楚了。

4.5 划分相关的常见误区速查

误区后果正确做法
在测试集上反复调参评估虚高,上线翻车测试集只在定稿后用一次
时间序列随机打乱用未来预测过去按时间顺序切分并留gap
分组数据随机划分同组泄漏,指标虚高用GroupShuffleSplit按组划分
不平衡数据不分层类别分布漂移加stratify参数
划分后不校验分布隐藏抽样偏差对比子集统计量
小数据单次划分定结论评估方差过大交叉验证加重复划分

这些误区我在不同项目里几乎都见过,其中“测试集反复调参”和“时间序列打乱”是最伤的两条,因为它们制造的虚高指标会让人误以为模型已经可用,直到上线才暴露问题,返工成本极高。

5. 我个人的几条实操心得

第一条心得是:划分代码要写成可复用的函数,把随机种子、分层、分组这些参数显式暴露出来,而不是散落在各个脚本里。项目一多,你根本记不住哪个脚本用了什么划分方式,统一封装能避免很多低级错误。第二条是:每次划分后把子集的统计摘要打印出来,养成习惯,这比事后排查泄漏省事得多。第三条是:测试集的指标只信第一次,后续任何调参后的测试结果都要打折扣看,因为你的决策已经污染了它。

还有一点值得强调:划分方式的选择应该在项目开始时就定下来,写进实验记录,而不是中途随意更换。我见过有人调参效果不好就换一种划分方式重跑,这本质上是在用测试集做选择,评估结果失去意义。划分方案是评估的地基,地基不能边盖楼边改。

最后分享一个实用技巧:如果你的数据同时存在分组和时间属性,比如同一设备多个时间点的监测数据,划分时要先按时间切分,再在时间切分的基础上按组划分,两个约束叠加。这种复合场景没有现成的一行代码,需要自己写索引逻辑,但原则不变——任何可能让测试集信息泄漏到训练过程的路径,都要在划分阶段堵死。