做机器学习项目,很多人一上来就急着调参、换模型、看炼丹效果,结果模型在训练集上神挡杀神、在测试集上瞬间垮掉,最后只能对着过拟合曲线发愁。其实这些问题的根源,多半在第一天准备数据时就埋下了——数据集划分这件事,看着简单,却是整个机器学习流程中最容易被低估、也最影响结果可靠性的一步。
这篇文章聊的就是机器学习里的数据集划分:为什么必须划分、怎么划分才算科学、背后有哪些容易踩的坑。我尽量用自己实际跑项目时的习惯来讲,不堆概念,给可以直接抄的步骤和代码。无论你是刚学机器学习的本科生、在准备课程设计或者考研复试,还是已经用自己的数据集训练过模型但总觉得结果不靠谱的开发者,这篇文章应该都能帮你把这块地基打稳。
在动手之前,先把一个底层观念摆清楚:数据划分不是"随便拿一块做测试"这么简单,它决定了你对模型的所有评价是否可信。网上经常看到各种公开数据集的使用教程,从Imagenet1K、HRSC2016、CCPD车牌数据集到作物病害图像、PHM2012轴承数据,大家下载完数据后第一件事都是划分。但这些教程往往只告诉你"怎么分",很少说清楚"为什么这么分",以及"哪些划分方式是错的"。这篇文章补上这一课。
1. 为什么要单独把数据集划分拎出来讲
1.1 模型评估的可信度,全看划分这一关
很多机器学习新手有个特自然的想法:把所有数据都拿去训练,模型见多识广,效果肯定更好。这个想法在直觉上没错,但它忽略了一个致命问题——机器学习的目的是泛化,不是背诵。
我习惯用一个比方来解释:训练模型就像学生准备考试,训练集是平时做的练习题,测试集是期末考试卷。如果这个学生考试前已经把答案背过了,平时成绩再好看也没用,因为真到了考场,面对没见过的题照样懵。模型如果见过所有数据,它在这些数据上的表现只能说明它"记性好",不能说明它"学会了规律"。
所以划分数据集的核心目的,是模拟"没见过的数据"场景,真实考验模型的能力。测试集必须从训练过程中彻底隔离,模型在训练时绝对不能接触测试集里的任何信息。
另一个容易忽略的点是数据集划分直接决定了你调参的方向。如果划分不合理,哪怕你后面用再先进的调参工具、再精细的损失函数,模型依然可能在真实场景中翻车。因为所有基于错误划分得到的结果,本质上都是一堆不可靠的"虚假好评"。
1.2 从热词看,划分数据集是绕不开的共同入口
这几年在各类论坛和社区里,跟数据集划分相关的问题常年霸榜。从CCPD车牌识别、HRSC2016遥感船舶检测,到叶片病害图像分类、作物病害数据集处理,再到ReID行人重识别、CrowdHuman人群检测、BDD100K自动驾驶数据,这些数据集在公开后,官方往往只提供原始图片和标注文件,不帮你分好训练测试集。
于是每个使用者都要自己做划分。同一个数据集,不同人划分方式五花八门,有人随机切、有人按目录切、还有人干脆不切直接用全部数据训练。结果就是论文里的精度指标大家千差万别,复现的时候对不上号,根源往往不是模型结构不同,而是数据划分的随机性不同。
学术界其实早就意识到了这个问题,所以很多数据集官方会给出标准划分,比如Imagenet1K的train/val拆分、PHM2012的官方训练测试方案,就是为了让大家在同一个基准下比较算法。但换到自己的数据集上,怎么划分就成了每个机器学习从业者必须独立完成的基础操作。
2. 主流划分方法:三种策略和它们的使用场景
2.1 留出法:最基础的七三/八二开
留出法是最简单也最常用的一种划分方式。做法很直接:把数据集按一定比例随机分成两份,比如70%做训练、30%做测试,或者80%训练、20%测试。
这个方法适合数据量充足、类别分布相对均匀的场景。我自己的经验是,当数据量达到几千条以上,留出法完全够用,没必要把简单问题复杂化。划分时要注意的是:
- 类别分布要保持与原数据集大致一致。比如一个二分类问题,原始数据里正例占60%、负例占40%,划分后的训练集和测试集也应该大致保持这个比例,否则模型评估会出现偏差。这就叫分层抽样。
- 比例选择要结合数据量。如果只有200条数据,你硬切70/30,测试集只有60条,统计意义太弱,模型性能的波动会很大。此时可以考虑留出法搭配多次重复,或者换交叉验证。
这里给一个实操小建议:做好划分后,务必检查一下划分后的训练集和测试集中,每个类别的样本数量是否合理。有些人分完就训练,结果测试集里某个类别只有三五条样本,最后模型的准确率看着还行,但召回率忽高忽低,原因就在这里。
2.2 K折交叉验证:小数据量时的救命稻草
K折交叉验证的原理是把数据集分成K份,每次取其中1份做测试、剩下K-1份做训练,如此轮流K次,最后把K次结果取平均。常用的是5折或10折。
这方法最典型的应用场景是数据量不大,比如几百到几千条样本时。留出法切一次,训练集和测试集的比例固定,模型性能评估受切分随机性影响很大;但交叉验证相当于让每条数据都有机会当一次"测试数据",评估结果更稳定、更可信。
实操上的细节是:K折交叉验证的K值怎么选。
- K=5时,每次用80%数据训练,20%测试,计算量适中
- K=10时,每次用90%数据训练,10%测试,评估更稳定,但计算量翻倍
- 如果数据集特别小,可以考虑留一法(LOOCV),每轮只留1条做测试,但计算成本极高,只适合小样本场景
K折交叉验证还有个隐藏好处——能辅助调参。你可以把K折中的每一折都当作一次完整训练,观察模型在不同数据子集上的表现差异。如果某几折效果明显差于其他折,说明数据里可能存在比较难学习的子集,或者数据分布有异常。
2.3 自助采样法:处理小样本的另一种思路
自助采样法(Bootstrap)在机器学习里也有它的位置。做法是从原始数据中有放回地随机抽样,抽出的样本做训练集,没被抽到的样本做测试集。当数据量很大时,大约会有36.8%的样本没被抽到,这些被称为"袋外样本"。
这个方法在处理极小数据集时比较好用。比如只有几十条样本,切分一次训练集就太小了,训练集没办法覆盖足够的数据分布,模型收敛都困难。这时候用自助法,可以一次性生成足够大的训练集。
不过自助法的缺点是训练集中会有重复样本,会让模型对某些样本产生偏向。在深度学习时代,除非极端小样本场景,否则一般很少用它做常规的数据集划分。但在集成学习、随机森林这类基于Bagging思想的算法中,自助采样是它们的内在机制。
2.4 三种方法怎么选,一张表讲明白
| 方法 | 适用场景 | 优点 | 缺点 | 典型应用 |
|---|---|---|---|---|
| 留出法 | 数据量充足 | 简单、直观、计算快 | 结果受单次随机切分影响大 | 几千条以上常规分类回归 |
| K折交叉验证 | 数据量中等或偏小 | 评估稳定、充分利用数据 | 计算成本高、代码稍复杂 | Kaggle比赛、学术论文实验 |
| 自助采样法 | 极小样本量 | 缓解训练数据不足 | 有重复抽样,分布有偏 | 集成学习内部机制、几十条小样本 |
我个人的使用习惯是:数据量大了就留出法,数据量小了就K折交叉验证。千万别在数据量很少的时候硬用留出法,也别在几千条数据上无脑跑10折交叉验证把时间浪费在重复训练上。
3. 动手实操:从零开始划分你的第一个数据集
3.1 环境准备与工具选型
做数据集划分其实不需要什么重型工具,Python环境加一个scikit-learn基本就够了。我自己平时就是用的train_test_split加上StratifiedKFold,这套工具足够覆盖绝大多数需求。
以鸢尾花数据集为例,这是机器学习领域最经典的开胃菜,用来演示划分方法再合适不过。先看一下基本流程:
import numpy as np import pandas as pd from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split # 加载鸢尾花数据集 iris = load_iris() X = iris.data y = iris.target # 划分训练集和测试集,测试集占比30% X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y ) print(f"训练集样本数: {X_train.shape[0]}, 测试集样本数: {X_test.shape[0]}")这一小段代码就把留出法的核心流程跑通了。但里面有几个参数值得拆开讲清楚,因为它们直接影响划分质量。
3.2 关键参数逐个拆解:为什么必须这么设
首先是test_size,也就是测试集比例。0.3是常见的配置,但到底该设多少,要看你的总数据量。如果数据只有1000条,设0.2意味着测试集200条,还能接受;如果数据只有100条,设0.2测试集只有20条,评估结果波动会非常大。我的原则是:在保证训练集足够的前提下,测试集尽量多留一些,这样评估结果更可靠。
接下来是random_state。很多初学者不理解为什么每次划分都要设一个固定值,不设不行吗?不设也能运行,但每次运行代码,数据都会被重新随机打乱一次。你今天训练跑出的准确率是90%,明天再跑变成85%,你根本不知道是模型的问题还是数据划分的问题。设成固定值,比如42,意思是"用这个随机种子来生成随机过程",保证每次划分结果完全一样,实验才能复现。这一点在做课程实验、写论文、做比赛提交时尤其重要。
然后是stratify参数。这个参数的作用是保持划分前后类别比例一致。鸢尾花数据集有三个类别,每类50条,如果不加stratify,随机划分后很有可能出现某个类别在测试集里多几条、另一个类别少几条的情况。加了stratify=y后,训练集和测试集都保持着每类约各占1/3的比例。
3.3 分层抽样时的具体操作
分层抽样听起来玄乎,其实就是在随机划分时,先按类别分组,然后在每个组内独立随机抽样。sklearn的train_test_split加一个stratify参数就解决了,但如果是自己写代码处理,要学会手动实现一遍,加深理解。
有一个非常经典的错误示范:有人面对图像分类任务,直接把图片文件名乱序后切一刀,前80%做训练,后20%做测试。看着是随机了,但如果这个数据集本身是按类别排序存放的,比如前面全是猫、后面全是狗,这么切出来的训练集里全是猫,测试集全是狗,模型直接废掉。
我实际处理图像数据时,习惯先给每个样本打上类别标签,用pandas组织成一个DataFrame,然后按类别做分组抽样:
import pandas as pd from sklearn.model_selection import train_test_split # 假设df包含两列:image_path和label df = pd.DataFrame({ 'image_path': ['img_001.jpg', 'img_002.jpg', ...], 'label': ['cat', 'dog', 'cat', ...] }) # 按标签分层划分 train_df, test_df = train_test_split( df, test_size=0.2, random_state=42, stratify=df['label'] )这样划分的结果是,无论原始数据排列是否有规律,训练集和测试集中猫狗比例都保持一致。对于图像数据集,比如从网上爬来的叶片病害图片、自己的手机拍摄数据,这一步尤其重要,因为真实采集的数据往往会有类别不均衡的问题。
3.4 验证集:容易被忽略的第三份数据
很多初学者以为数据集只分两份就够了:训练集和测试集。但这会在调参时引入一个大坑——你用测试集反复验证效果、调整参数,测试集就不再是"没见过的数据"了。
打个比方,你把期末考试的题目提前给学生当模拟题练了一遍,然后又用同一套题去考他,考出来的分数能说明他的真实水平吗?显然不能。测试集一旦被反复用来指导调参,模型就会在测试集上产生过拟合,测试精度虚高,真实场景效果却一塌糊涂。
所以标准做法是把数据分成三份:训练集、验证集、测试集。训练集用来训模型,验证集用来选参数和模型结构,测试集只在最终评估时用一次。三份的常见比例是70%、15%、15%,或者60%、20%、20%。
实操中验证集确实会占走一部分数据,数据少的时候有点肉疼。这时候可以把验证集直接和K折交叉验证结合——用训练集跑K折,每折里再细分出一个验证折,测试集始终不动,留到最后。这个套路在Kaggle比赛里非常常见,也是保证模型评估可信的关键手段。
4. 特殊场景下的划分难题与实战对策
4.1 时间序列数据:严禁随机打乱
时间序列预测(股票价格、风力发电功率、设备剩余寿命预测)是机器学习里一类非常特殊的问题。很多人在划分时间序列数据集时,顺手就用了随机切分,这是教科书级别的错误。
原因很直观:模型训练只能用过去的样本预测未来的样本,这是时间序列建模的基本假设。如果你把未来的数据混进训练集,让模型"偷看"了未来信息,它在测试集上的表现会虚高得离谱。这就是典型的信息泄漏。
时间序列数据划分有专门的做法,我总结为三个原则:
- 按时间顺序切分,前一部分做训练,后一部分做测试,严禁打乱
- 训练集和测试集之间要留出间隔,这是为了防止序列自相关性导致的信息穿越
- 如果要评估模型在极端情况下的表现,还可以考虑滚动预测方式,让模型逐步预测未来一个时间窗
PHM2012轴承加速寿命实验数据集就是典型例子。这种数据集通常包含多组轴承从正常到失效的全生命周期振动信号,如果随机划分数据,同一个轴承不同寿命阶段的数据会同时出现在训练集和测试集里,模型等于提前知道了轴承的退化规律,测试结果的真实性大打折扣。正确的做法是按传感器编号或实验批次进行划分,至少保证同一组件的不同时间片段不会被拆分进两个集合。
4.2 图像数据集划分:避免同源数据泄漏
做目标检测、图像分类的任务时,除了按样本划分,还要特别注意数据同源性的问题。比如CCPD车牌数据集里有大量来自同一辆车不同角度的照片,CrowdHuman数据集里同一场景可能包含多个不同的人,这些数据之间存在强相关性。
如果随机划分,同一条车辆记录可能在训练集和测试集各出现一半图片,模型在训练时见过这个车的特征,测试时认出来就很容易,评估结果虚高。更严格的做法是在更粗的粒度上划分,比如按拍摄视频片段划分,一个视频片段里的所有帧要么全部进训练集,要么全部进测试集。
这也是为什么很多自动驾驶数据集、行人重识别数据集的建设会投入大量精力做身份级别的划分。ReID数据集尤其典型,同一行人的多张照片必须放在同一个集合里,不能在训练和测试中重复出现。否则模型可以直接靠记忆外观特征,而不是学习泛化能力。
4.3 类别极不均衡时,光分层还不够
当类别极不均衡时(比如99%正常样本,1%故障样本),分层抽样虽然能保持比例一致,但测试集中的少数类可能只有几条样本,评估结果依然不可靠。这时候通常有两条路:
一是用交叉验证替代留出法。交叉验证能让每个类的样本都有充分机会出现在测试集中,至少评估结果更平滑。二是评估指标要换。纯准确率在这种场景下毫无意义,一个把所有样本都预测为正常类的模型,准确率也能到99%。这种做法看着精度高,实际根本没用。正确评估要看精确率(Precision)、召回率(Recall)、F1分数或PR曲线。
4.4 数据泄漏:划分后最容易犯的高级错误
数据泄漏是个听到的人多、真正理解的人少的问题。它指的是测试集的信息在训练过程中被模型接触到了。除了刚才说的时间序列随机切分,数据泄漏还经常发生在一个意料之外的地方——数据预处理阶段。
举个例子,如果用全量数据计算均值和标准差做标准化,然后再划分训练集和测试集,这就是一种泄漏。因为标准化使用的均值是包含测试集信息算出来的,模型间接接触了测试集分布。正确流程是:先划分数据,再单独对训练集拟合标准化参数,然后用同一套参数去转换测试集。
from sklearn.preprocessing import StandardScaler # 错误示范:先标准化再划分 # scaler = StandardScaler().fit(X) # X_scaled = scaler.transform(X) # X_train, X_test = train_test_split(X_scaled, ...) # 正确示范:先划分再标准化 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) scaler = StandardScaler().fit(X_train) # 只用训练集拟合 X_train_scaled = scaler.transform(X_train) X_test_scaled = scaler.transform(X_test) # 用训练集的参数转换测试集同理,特征选择、PCA降维、缺失值填充等操作,凡是涉及统计量计算的,都应该先只对训练集做拟合,再应用到测试集。这个习惯要尽早养成,否则后面做复杂项目时很容易在不知不觉间把测试集的信息学进模型里。
5. 常见问题与排查技巧实录
5.1 快速排查清单:划分完先自查这4件事
每次划分完数据集,我都会先做一轮自查,把这几项检查完了才会开始训练。照着做,能省掉后面成堆的调试时间。
- 各类别在划分前后的占比是否大致一致(分类问题时)
- 训练集、验证集、测试集的样本量是否足够支撑后续评估
- 测试集的样本是否被用于任何训练步骤,包括预处理拟合
- random_state是否固定,保证实验可复现
如果自查发现某一项不通过,别含糊,直接重新划分来得更快。
5.2 划分比例到底怎么定,别死记硬背
关于比例,网上最常见的说法是"训练集70%、测试集30%"或者"80/20"。但实际项目中,这个比例从来不是死公式。我给一个更实用的思路:
- 先看总量。总量几万条,测试集留20%完全够;总量几百条,测试集留30%甚至更多,但此时建议上交叉验证
- 再看任务难度。任务难度高、数据分布复杂,需要更多数据做训练,测试集比例可以适当降低
- 最后看评估需求。如果要做模型选择或者调参,验证集必须有;如果只是训练完毕做一次简单汇报,那训练测试两分法也能起步
实际工程里,我见过很多团队对靠谱实验结果要求比较高的情况,这时候用"训练+验证+测试"三分法,并配合交叉验证做模型选择,是最稳妥的一套路子。
5.3 怎么判断自己的划分到底合不合理
有一种很朴素但有用的验算方法:基线模型测试。拿一个非常简单的模型(比如逻辑回归、决策树桩)跑一遍流程,看它在测试集上的表现是否合理。如果基线模型的表现都好得离谱,大概率数据泄漏了;如果强模型的表现反而不如基线模型,更大概率是划分出了问题。
还可以做一个更直接的检查:把模型在训练集、验证集、测试集上的表现打印出来对比。正常情况应该是训练集最好、验证集略差、测试集最差但差距不大。如果测试集表现和训练集一样好,先说恭喜,但也要警惕是否泄漏;如果验证集远差于训练集,说明泛化能力有硬伤。
5.4 文件目录级别的划分小技巧
处理图像数据集时,很多人习惯把所有图片放在同一个文件夹里,然后用一个CSV或TXT文件记录标签。但这种做法的缺点是,划分完数据后还得维护标签文件的一致性,一旦手动移动文件,容易出错。
我更推荐的数据组织方式是这样:
dataset/ train/ cat/ cat_001.jpg cat_002.jpg dog/ dog_001.jpg val/ cat/ cat_101.jpg dog/ dog_101.jpg test/ cat/ cat_201.jpg dog/ dog_201.jpg按类别建文件夹,训练、验证、测试各建一份。划分时,读取类别文件夹中的图片路径列表,按比例随机划分,然后把文件复制或移动到对应目录。这样做的好处是后续训练代码可以直接按目录读取,简单直接,而且不会因为标签文件错行导致错位。
如果是大规模数据集,比如Imagenet1K这样有几十万张图的场景,用shutil.copy移动文件会比较慢,可以先复制再在空闲时校验文件数,保证划分结果可靠。
6. 从划分数据开始,养成机器学习的工程习惯
数据集划分是机器学习项目中很小的一个环节,但它像盖楼的地基——看不见、却一直在承重。很多同学调模型调得头头是道,但提交实验报告时却连数据划分的细节都写不清楚,这种实验的复现性就存在先天不足。
我在实际带项目和辅导课程设计的过程中发现,最终结果稳定、论文逻辑顺畅、答辩经得起追问的团队,几乎都有一个共性:他们在数据划分阶段就投入了认真思考,划分方案写得清清楚楚,包括方法选择、比例设置、随机种子、为了防止数据泄漏做了哪些控制。反观那些效果忽好忽坏、结果无法复现的团队,大多是在这个环节埋下了隐患。
如果你想从今天开始建立一个好习惯,我建议先做一件事:把你手头数据集划分的方案完整写下来,哪怕只是几行字——数据来源、样本总量、类别分布、划分方法、比例、随机种子、验证集策略、防止泄漏的措施。下次跑模型前先把这个文档打开看一眼,如果哪一项写不出来,就先补上再继续。
这听起来很繁琐,但根据我的亲身体会,这个习惯会帮你在后面节省大量返工时间。毕竟机器学习这条路上,真正决定一个项目能走多远的,往往不是最光鲜的那个模型结构,而是底下那些看似基础、实则关键的准备步骤。