训练集、验证集、测试集怎么划分?避开数据泄漏的坑

发布时间:2026/9/18 10:59:14
训练集、验证集、测试集怎么划分?避开数据泄漏的坑 刚开始接触机器学习或者已经用YOLO这类框架跑过几个模型的朋友对“训练集、测试集、验证集”这三个词肯定不陌生。但说实话很多人对它们的理解停留在“训练集用来学测试集用来考”这个层面真到自己划分数据、调参、评估模型的时候才发现里面门道挺多。我见过太多新手把验证集和测试集混为一谈或者划分数据时图省事直接随机乱切结果模型训练时指标飘得厉害上线一测立刻现原形。这篇内容我不打算照本宣科讲定义而是结合我自己这些年做目标检测、图像分类项目的经验把这三大数据集的本质、划分策略、实操中容易踩的坑一次讲透。不管你是准备训练YOLOv8自己的数据集还是在折腾mmrotate处理DOTA这类旋转框数据理解清楚这三者的关系比调任何参数都重要。1. 拆解三大数据集它们不是“训练”和“测试”那么简单很多教材把这个问题讲得太粗了会说训练集就是给模型学习的测试集就是看模型学得咋样的然后验证集似乎可有可无。真实项目里这套理解会出大问题。1.1 训练集模型的“课本”与“习题册”训练集是模型唯一直接学习的对象。模型通过反复看训练集里的样本不断调整网络权重让预测结果和真实标注越来越接近。用一个不太恰当但很贴切的类比训练集既是课本也是习题册模型不仅从里面学知识还要做大量重复练习来巩固。但这里有个关键细节训练集不是越大越好而是越“全面”越好。如果训练集里全是同一个角度、同一种光照下的目标模型学出来的特征就是偏的。我记得之前做工业质检项目训练集里全是正光位的产品图模型检测精度在验证集上刷到99%结果现场一装上去侧光位、背光位的产品漏检率直接飙到30%以上。后来重新采集数据把光照角度、遮挡情况、目标尺度都均匀分布到训练集里模型才真正“见过世面”。1.2 验证集训练过程中的“模拟考”验证集不参与权重更新但它参与了训练过程。每训练完一个epoch或几个epoch模型会在验证集上跑一遍算一下当前的精度、召回率、loss值。它的作用是帮助我们判断模型当前状态——是欠拟合了、过拟合了、还是正在正常收敛。在训练过程中很多策略都依赖验证集比如早停法、学习率衰减、模型选择保存。我用YOLOv5训练自己的数据集时训练脚本里默认会从训练集中再切出一部分作为验证集每轮epoch结束后在验证集上计算mAP。如果验证集mAP连续好几个epoch不涨了就可以触发早停省时省力。验证集还有一个容易忽略的作用防止数据泄漏。如果我们把验证集的信息混进训练过程比如根据验证集的表现反复手工修改网络结构或者数据增强策略那验证集就不再“客观”了。这就像模拟考做了很多次学生把模拟考题都背下来了可真正高考成绩还是暴露真实水平。1.3 测试集最终的“高考”测试集在全流程中只能用一次——在模型训练完全结束后用来做最终评估。它不能参与训练也不能参与任何调参或模型选择过程否则就丧失了公平评估的意义。实操中很多人会犯一个错误用验证集的结果反复“挑模型”挑完再在测试集上测一下发现不行回去再调参、再训练、再在验证集上比对等模型在验证集上刷高了再去测试集上试。这样搞几轮下来虽然测试集本身没参与训练但我们的决策过程已经间接参考了测试集的信息测试集就“脏”了。我自己的习惯是划分完数据之后测试集直接封存到一个独立文件夹不到最终评估绝不去碰。这样可以保证模型的泛化能力度量是可信的。1.4 一张表说清三者的核心区别维度训练集验证集测试集使用阶段训练全程训练过程中周期性评估模型定稿后一次性评估参与权重更新是否否是否参与调参否仅提供梯度是辅助选择超参数否评估作用学习数据分布特征监控训练状态、防过拟合、早停、模型选择衡量最终泛化性能使用次数不限可多次尽可能一次可类比场景教材习题册模拟考高考2. 数据划分的正确姿势比例、随机性与分层策略确定了三个数据集各自的职责之后最现实的实操问题就来了数据到底怎么切2.1 比例分配不是数学题是权衡题我经常看到有人问“训练集、验证集、测试集按7:2:1还是8:1:1好”这个问题其实没有标准答案它取决于数据总量、任务复杂度和样本分布情况。数据量大的时候比如有几万张图按98:1:1划分完全没问题验证和测试各留几百张就足够评估了。但如果数据总量只有几百上千张就得精打细算。我记得自己刚接触深度学习时手里数据只有500张左右按照老教程硬套7:2:1测试集才50张评估结果的波动区间大到无法接受。这时更好的选择是采用K折交叉验证把数据分成K份通常K5或10每次取一份当验证集其余K-1份当训练集轮流做K次最后把K次的结果取平均。这样每个样本都能轮上做验证评估更稳定。但代价是训练K次模型耗时翻倍。数据量稍大或者训练时间很长的任务老老实实留出法合理的验证集比例就够了。2.2 随机划分是默认项但有例外默认情况下数据划分是随机进行的。但有几个场景随机划分会出问题第一种是类别极度不平衡的数据。比如目标检测里常见的问题某个类别占80%的样本另一个类别只有5%。随机划分很可能导致验证集或测试集里小类别样本数量寥寥无几评估指标失真。这时要用分层采样保证每个集合里各类别比例和全集一致。sklearn里的train_test_split支持stratify参数PyTorch和TensorFlow生态里也有相应的分层采样器。第二种是时间序列或者空间序列数据。比如用视频抽取帧做目标检测相邻帧之间高度相关。如果随机划分很可能训练集和验证集里出现了同一段视频的相似帧验证集就没法真实反映模型在陌生场景下的表现。这种情况必须按时间或视频ID来划分确保一个视频的所有帧只出现在一个集合里。第三种是同一个目标物体的多次拍摄。拿DOTA这类遥感数据集举例同一栋建筑可能在多张图像切片中出现如果随机划分模型相当于“见过”验证集里的目标了测试结果虚高。合理做法是先按目标或图像ID分组再在组级别做划分。2.3 数据划分的实操流程参考以我处理图像类项目为例通常按以下步骤操作先把原始数据整理成统一结构所有图片和对应的标注文件放在一起统一命名。统计全量数据的类别分布和样本量确认数据基本盘。如果样本量充足且分布均衡直接用随机打乱加比例切分。如果类别不均衡先按类别分组再在各组内按比例随机抽样拼装成训练、验证、测试三个集合。检查切分后的三个集合保证类别覆盖完整且分布与全集基本一致。把测试集单独封存训练阶段只接触训练集和验证集。这套流程我会在后面的章节里结合目标检测数据给一个更具体的例子。3. 结合YOLO、DOTA等场景训练你自己的数据集时怎么切现在前文提到的一大堆热词都和“训练自己的数据集”有关YOLOv5、YOLOv8、YOLOv11、mmrotate做DOTA数据集训练这些项目里数据集划分的细节直接决定训练效率和最终精度必须单独拿出来说。3.1 YOLO系列训练自己的数据集目录结构就是隐形的划分逻辑YOLO系列框架对数据集的组织方式非常明确一般要求或者建议你将图片和标签放在同一级目录下并且在配置文件中通过train、val、test三个参数指定图片路径。比如你最常看到的目录结构往往长这样dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yamldata.yaml里大致配置train: dataset/images/train val: dataset/images/val test: dataset/images/test nc: 80 names: [person, bicycle, car, ...]实操中很多人的困惑是框架里明明只分了train和val没有test该怎么处理其实YOLOv5、YOLOv8训练完默认在val上做最终评估打印出各类别mAP。如果你的目标是发论文或者做严谨的横向对比最好自行把测试集路径填进test字段训练时不用它训练结束后再单独对test集做一次评估。另外有个细节像YOLO这类检测框架训练时标注文件和图片严格一一对应。划分数据时必须同时移动或复制对应的label文件不能只挪图片。很多人直接手动拖拽图片漏掉了labels结果训练时报错“no labels found”。建议写个小工具脚本自动处理。3.2 mmrotate与DOTA数据集切割、划分与跨图目标DOTA数据集是遥感目标检测的常用基准数据原始图像非常大一般都切成小块patch再进入训练流程。这时候数据划分必须在切割之前就确定好归属。把一张原始大图切成了若干patch如果先切割再划分同一个大图的不同patch可能同时出现在train和val里相当于模型在训练时“偷看”过验证集的地物特征评估结果会虚高。正确做法是先按大图ID把完整大图划分到train、val、test然后再对每个集合内的图做切割。我用mmrotate训练DOTA数据集时踩过类似坑。一开始图省事直接把所有patch混在一起随机划分训练完mAP看着很不错后来换了完全陌生的遥感图一测精度掉了一大截。根本原因就是patch级别的划分泄漏了同源信息。3.3 目标检测数据集划分的自检清单处理检测类数据时我每次划分完都会过一遍自检清单图片和标签文件数量是否一一对应有没有孤儿文件每个集合里是否都覆盖了全部类别必要时打印每个集合的类别分布。同一来源视频序列、同一大图、同一目标多次拍摄的样本是否保证不会被分到不同集合验证集和测试集样本量是否足够支撑可靠评估类别少的确认每个类别至少有一定数量。标注框的尺寸和分布在各集合间是否大致一致避免训练集全是小目标、验证集全是大目标的情况。4. 数据划分与数据泄漏那些你看不见的“作弊”数据泄漏是模型评估中最隐蔽的问题它的本质是验证集或测试集里包含了不该有的信息导致模型评估结果比真实部署场景好得多。很多人天真地以为自己只要按7:2:1切完就万事大吉了实际上泄漏的途径远远超出你的想象。4.1 标签泄漏与特征泄漏模型“偷看”了答案特征泄漏指的是模型在训练时用到了测试阶段拿不到的信息。举个例子你做一个电商点击率预测如果把未来时间段的用户行为特征拼进训练样本模型在验证集上效果可能爆炸但真上线后这些特征根本不存在。表现在数据划分上就是时间切分必须严格以训练数据的截止时间为界验证和测试数据的特征计算不能借用训练集的全局统计量。标签泄漏更隐蔽常见于数据预处理环节。假设你在做数据标准化用全量数据的均值和方差来缩放特征然后才划分训练/验证集。这一操作意味着验证集和测试集的分布信息已经被我用到了训练过程中相当于考试前先偷看了出题老师的标准答案分布。正确做法是只在训练集上计算标准化参数再直接套用到验证集和测试集上sklearn里的StandardScaler就要求先fit训练集再transform所有集合。4.2 数据增强与预处理顺序图像领域也存在类似问题只是表现形态不同。比如做图像分类有些人在划分数据集之前先对所有图像做了整体归一化这还好一些因为图像均值方差基本在百万像素级别上比较稳定。但如果你用了基于整份数据学习的数据增强比如自动增强AutoAugment的某些策略它需要统计全量数据来学习增强策略就可能间接把验证集的信息带进训练。更常见的坑是不少人做目标检测时训练前手动统一压缩了所有图片的尺寸然后在训练脚本里又做一次随机缩放。如果压缩和随机缩放产生的插值污染了验证集的原始信息评估出来的精度自然会偏高一点。实际影响不算大但严谨起见预处理流程应该是先划分数据再对训练集单独做增强验证集和测试集只做必要的尺寸调整尽量不要使用任何随机性变换。4.3 时间序列与业务逻辑的泄漏做业务数据分析时这个问题更要命。举个例子你要预测某商品未来一周的销量按正常逻辑应该按时间排序用前几个月做训练中间的时间段做验证最后一个月做测试。有人图方便直接全部随机打乱结果训练集里混进了“未来”的数据测试集里出现了“历史”数据。虽然从纯数据角度看分布一致性没问题但从业务逻辑看模型已经见过未来的走势预测精度虚高。我在这个坑上栽过跟头。当时做一个销量预测模型随机划分下指标不错老板一高兴直接拿上个月的数据做“盲测”结果一塌糊涂。排查后才发现根因我的验证集和训练集之间存在时间上的重叠模型其实在验证集里学到了那些意外波动的模式。4.4 数据泄漏自查办法如果你怀疑自己的数据划分存在泄漏有几个自查手段画一个数据时间线图查看每个集合覆盖的时间区间是否完全互斥。检查文件名、视频ID、图像ID、目标ID等高层唯一标识是否有跨集合重叠。对图像数据可以计算训练集、验证集、测试集之间的图像相似度矩阵看是否存在高度相似的图像对。观察训练曲线如果验证loss一开始就非常低低得反常大概率有泄漏。5. 实操中的训练、验证与评估闭环讲了这么多概念和坑最后给一个相对完整的实操闭环帮你把前面说的串起来。无论你用YOLOv8训练自己的数据集还是用mmrotate训练DOTA这套流程都适用。5.1 数据划分落地脚本示例我习惯用Python写一个划分脚本支持随机划分和分组划分。这里给一个简单但完整的参考实现import os import random import shutil from collections import defaultdict def split_dataset(image_dir, label_dir, train_ratio0.7, val_ratio0.2, group_by_videoTrue): # 获取所有图片文件 all_images [f for f in os.listdir(image_dir) if f.endswith((.jpg, .png))] images_with_labels [] for img in all_images: label_path os.path.join(label_dir, os.path.splitext(img)[0] .txt) if os.path.exists(label_path): images_with_labels.append(img) else: print(f跳过缺失标注的图片: {img}) if group_by_video: # 按视频ID分组确保同一视频帧不会被拆分到不同集合 groups defaultdict(list) for img in images_with_labels: video_id img.split(_)[0] # 假设文件名以videoID_frameID命名 groups[video_id].append(img) group_list list(groups.values()) random.shuffle(group_list) train_groups group_list[:int(len(group_list) * train_ratio)] val_groups group_list[int(len(group_list) * train_ratio):int(len(group_list) * (train_ratio val_ratio))] test_groups group_list[int(len(group_list) * (train_ratio val_ratio)):] train_images [img for g in train_groups for img in g] val_images [img for g in val_groups for img in g] test_images [img for g in test_groups for img in g] else: random.shuffle(images_with_labels) train_images images_with_labels[:int(len(images_with_labels) * train_ratio)] val_images images_with_labels[int(len(images_with_labels) * train_ratio):int(len(images_with_labels) * (train_ratio val_ratio))] test_images images_with_labels[int(len(images_with_labels) * (train_ratio val_ratio)):] # 复制到目标目录 for subset, img_list in zip([train, val, test], [train_images, val_images, test_images]): os.makedirs(fout/images/{subset}, exist_okTrue) os.makedirs(fout/labels/{subset}, exist_okTrue) for img in img_list: shutil.copy2(os.path.join(image_dir, img), fout/images/{subset}/{img}) label_name os.path.splitext(img)[0] .txt shutil.copy2(os.path.join(label_dir, label_name), fout/labels/{subset}/{label_name}) print(f训练集: {len(train_images)} 张) print(f验证集: {len(val_images)} 张) print(f测试集: {len(test_images)} 张)这段脚本的重点是支持分组划分。在视频序列或同一场景多帧拍摄时按组划分能有效避免数据泄漏。分组ID的提取逻辑要根据自己数据命名规则调整比如DOTA原始图像可以按大图ID分组船舶视频检测可以按视频文件名分组。5.2 训练过程中的验证集监控要点训练时验证集的价值体现在几个关键监控指标上训练loss与验证loss的gap。如果训练loss不断下降验证loss不降反升说明过拟合已经开始了。这时可以降低模型复杂度、增加正则化、增大数据增强强度。验证集mAP的早停与模型选择。YOLO训练中框架默认保存验证集mAP最高的权重作为best.pt。这个机制建立在验证集正确划分的前提下。如果验证集本身划分有误best.pt选出来的模型可能并不是真正最好的。学习率调度器。很多调度器依赖验证集loss比如ReduceLROnPlateau如果验证集分布不均匀学习率调整节奏也会乱。我自己习惯在训练中盯两件事一是每个epoch结束后看验证集mAP变化是否平稳二是看最终best.pt在测试集上的表现和val集上的表现差多少。如果差距在2-3个点以内说明泛化性良好如果差距超过5个点通常说明训练集和验证集分布存在偏差或者模型过拟合了。5.3 测试集评估与可解释性分析训练结束模型定稿这时才轮到测试集登场。我建议不只打印总的mAP还要逐类别查看精度和召回率。之前做DOTA训练时模型整体mAP看起来不错但逐类别一看小目标类别如“small-vehicle”和“storage-tank”的召回率远低于均值。这种问题只有细看每个类别的指标才能暴露。同时把测试集上预测错误的样本可视化出来逐个观察错误形态。比如误检是因为遮挡、光照还是目标尺度太小这些信息直接决定了下一步是扩充数据还是调整模型结构。没有测试集的系统性排查优化方向就是盲目的。6. 进阶话题当数据量不足时的划分与验证策略很多个人项目和中小企业项目数据量都很有限几百张图、几千条记录硬切出测试集后样本就更少了。这种情况怎么处理我分享几个实践经验。6.1 不确定性评估代替绝对指标当测试集只有几十张时评估指标的置信区间非常大。比如mAP在0.6还是0.75之间波动可能纯粹是测试集样本抽取偏差导致的。这时我通常会在不同随机种子下重复多次划分和训练观察指标均值和方差。如果方差过大说明评估结论本身不可靠这时候更应该关注验证集上的相对对比而不是死盯着绝对数值。6.2 留一法与K折在小样本中的应用数据量到了100张以下常规划分已经很难玩出花来。K折交叉验证是更严谨的选择。虽然训练多次耗时但至少每次的验证集小但有效最终评估的置信度更高。目标检测任务里如果每折训练时间太长可以选择K3或者只对验证集做一次牺牲一点可靠性换取时间成本的可接受。让我印象最深的一次是做一个安防场景下的行人检测项目有效标注数据只有200多张。如果用7:2:1划分验证集才40多张测试集20多张调参完全靠感觉。后来改成了5折交叉验证每折用160张训练40张验证训练5次取平均mAP。虽然总训练时间是原来的5倍但最终模型上线后的表现确实和评估结果对得上这个代价是值得的。6.3 弱监督与伪标签场景下的划分陷阱现在很多人会用预训练模型给无标注数据打伪标签扩充训练集。这块操作要格外小心。伪标签数据不能直接混入测试集或验证集因为它们可能包含大量错误标注会污染评估结果。我自己通常在伪标签扩充时只扩充训练集验证集和测试集严格保留人工标注的干净数据。还有一个细节如果伪标签数据来自和测试集相同的数据分布比如同一个摄像头不同时间拍的画面那模型可能在伪标签训练中“见过”测试场景评估结果依然有虚高风险。严谨一点的做法伪标签数据最好来自和真实部署环境相近但不同源的场景。7. 一些散落但重要的实操心得最后分享几个零散经验每一条都是我在实际项目中用代价换来的。7.1 数据集版本管理很有必要数据划分完成后最好给每个版本做个快照记录划分的随机种子、划分脚本、文件列表和统计信息。我见过太多项目改了几次数据后自己都不记得当前模型到底是在哪份数据上训练的。没有版本管理复现实验就成了一场灾难。这个领域的工具有不少DVC就是专门做数据和模型版本管理的熟悉Git的话上手很快。如果只是个人项目也可以简单建一个CSV清单记录每张图片属于哪个集合放到项目目录里留底。别嫌这一步麻烦三个月后你就会感谢当时的自己。7.2 别迷信“自动划分”动手检查永远不过时现在很多框架和工具提供了自动划分功能点一下就把数据切好了。但自动划分只解决“切了”这个问题不保证“切得对”。切完之后一定要动手检查看一下train里图片是否正常、val里每个类别是否都有、test集里有没有重复文件。拿我自己的习惯来说每次划分后要跑一个小脚本输出每个集合的类别数量柱状图看到分布不均立刻处理。7.3 测试集该“用完”还是“反复用”有句话说得好测试集是“一次性”的。如果反复用测试集调参测试集就变成了第二个验证集。但真实的工程环境里我们往往做不到只用一次因为产品迭代需要不断验证。我的妥协方案是真正干净的测试集留一份交给不参与训练和调参的同事或独立流程保管平时开发用自己的验证集做对比。等模型上线前再拿这份干净的测试集做一次终极评估。这种分工虽然操作上多一点约束但能保证每次评估结果都有公信力。7.4 给新手的落地行动清单如果这篇文章你只能记住一部分内容那请记住这五条先把测试集单独封存不要碰它。划分数据时先查同名泄漏再谈比例。类别不均衡时优先使用分层采样。同一场景、同一视频、同一目标的样本避免跨集合出现。训练过程中只看训练loss和验证集指标最终评估才碰测试集。数据划分看似基础实际上直接决定了一个模型项目的成败。我自己见过太多项目栽在这上面不是在模型结构上而是在最不起眼的数据划分环节。希望这篇文章能帮你少走这些弯路把精力真正花在值得的地方。