2026/9/26 2:23:28

训练集、验证集、测试集如何划分?原理与YOLO实战避坑指南

训练集、验证集、测试集如何划分?原理与YOLO实战避坑指南 1. 三个集合到底是什么从一次训练事故说起每次带新人做机器学习项目几乎都要先回答同一个问题训练集、验证集、测试集到底有什么区别很多人觉得这个太基础背个定义就过了但真正动手训练YOLO、训练UNet、跑mmrotate的时候栽跟头的往往不是模型结构而是数据划分。我自己就亲眼见过一个同事把验证集当测试集反复调参最后模型上线被客户数据和离线指标对不上排查了整整两天才发现是数据集划分出了问题。简单说训练集是让模型学习规律的数据验证集是用来调整模型结构、超参数和判断训练到什么程度该停下来的数据测试集是模型完全训练完成后用来做一次最终客观评估的数据。三者都来自于同一个真实数据分布但承担的任务完全不一样混用任何一个都会让模型评估结果失真。1.1 训练集让模型抄作业的本子训练集的任务最直接——喂给模型让模型计算损失、反向传播、更新权重。模型通过反复看训练集里的样本把输入和输出之间的映射关系记下来。你可以把它想象成学生的平时作业本每一道错题都是一个学习机会做错的题越多更正的机会也越多学到的规律就越扎实。训练集的规模通常最大一般占总数据的60%到80%。它决定了模型的上限因为模型能学到什么取决于它见过什么。训练集里没有的样本分布、没有的类别特征、没有的边界情况模型大概率学不出来。这也是为什么很多做YOLO训练自己的数据集的人第一步要疯狂扩充图像数量、做数据增强本质上就是在给训练集“加厚”。这里有个容易被忽略的点训练集不是越多越好而是越“对”越好。如果你的训练集里混了大量错误标注的样本模型会把错误标注当作正确规律去拟合训练损失怎么调都降不到理想值验证集指标也会变得忽高忽低。所以花时间清洗训练集永远比花时间调学习率更值得。1.2 验证集考前模拟卷验证集是用来做“模型选择”和“超参数决策”的。训练过程中模型在每个epoch都会更新权重但你不知道它到底学得好不好、有没有过拟合。这时候就需要一份没有被训练过的数据来模拟考试这份卷子就是验证集。实际项目里验证集的用途非常多。选择学习率大小、决定要不要早停、比较YOLOv8和YOLOv5哪个效果更好、判断增加一个数据增强策略有没有用都要用验证集来评估。验证集的指标就是你做决策的“裁判”“等”模型还没有真正被喂过所以给出的反馈相对公平。但要注意验证集的公平是相对的。它参与了你所有的决策过程你每一个决定都是基于验证集的反馈做出来的。做得多了验证集的信息就间接在影响模型选择了这时候验证集就不再是“未知数据”。打个比方学生如果反复做同一套模拟卷哪怕不是背答案也会对这套卷子的出题风格越来越熟悉最后的分数就不能真实反映对知识的掌握程度。所以验证集可以反复用但它的权威性会随着使用次数递减。项目做到后期验证集指标的轻微波动不要太当真真正的终审是测试集。1.3 测试集真正的期末考试测试集这辈子只被允许用一次至少在每个研究周期内只用一次。它是在所有训练和调参工作彻底结束后用来最终验证模型泛化能力的。测试集好比高考你平时怎么做模拟卷都行但真正决定你这段时间学习成果的是高考这一场考试。高考卷子不可能提前做一遍再来考试对模型来说测试集上的效果才是你对外宣称的“真实水平”。为什么测试集只能用一次因为模型和研究者都有“学习能力”。如果你用了测试集发现效果不好转头去调整参数再跑一次测试集这就等于把测试集的信息泄露到了模型选择过程中。跑两三次之后模型在测试集上的效果大概率会被“选中”得越来越好但换到真实场景的新数据上效果可能一塌糊涂因为模型已经把测试集的噪声也学进去了。我在项目里见过的最典型错误是一个人反复用测试集去对比不同模型的精度最后选了一个测试集分数最高的模型上线。结果到了生产环境新鲜数据完全不是那个分布精度比他的“对标模型”低了五个点。这就是测试集被污染之后评估结果失真带来的代价。2. 为什么非得分三份两份够用吗很多人会问那我把训练集跟验证集合起来一起训练只留一份测试集做评估不是也能用吗我以前刚入行的时候也这么干过觉得验证集就是浪费数据少喂一份数据模型不就少学一份规律吗后来被现实教育了几次才彻底明白这份冗余是必须的。2.1 验证集和测试集的分工差异如果只分两份把其中一份当成“评估集”那你的所有调参决策都只能基于这一份评估数据。问题在于调参是一个循环过程。你调一次学习率要看评估集结果换一个模型结构要看加一个数据增强要看改了N次之后评估集早就不是陌生数据了。这时候评估集的分数已经不能代表模型在新数据上的表现你等于在拿一张答案已经被翻烂的卷子反复考自己。验证集和测试集分家本质上是把“调参决策用的数据”和“最终审查用的数据”隔离开。验证集负责陪你走过整个开发过程用多少次都行测试集只在最后出场一次做一个中立、无偏的终审。这样既能让你调参时获得有效反馈又不至于所有数据都被你的决策过程“污染”。用两份数据还有一个实操层面的好处你在做模型选择的时候比如对比YOLOv8和YOLOv5在自己的数据集上的效果如果只在训练集上比较模型过拟合严重的时候两者可能都表现得“很好”完全看不出差距如果只在测试集上比较那测试集就被提前用掉了。有了验证集这些对比和选择统统都在验证集上做测试集始终是干净的留给最后一锤定音。2.2 两份数据集带来的日常翻车现场我遇到过不少只分两份数据的项目典型翻车现场如下。团队拿到一批业务数据把20%当测试集80%当训练集然后所有人都在训练集上训练每天用测试集做一轮评估来判断今天的改动有没有用。做了几天之后测试集指标越来越好看大家都很高兴。结果到了上线评审用业务方新导出的数据一测精度崩了。原因很简单团队成员每天都基于测试集的结果修改模型不知不觉已经“拟合”了测试集。还有一个更隐蔽的场景就是做早停。如果只看训练集损失模型会一直过拟合根本不知道什么时候该停。如果只用测试集做早停判断每停一次都消耗一次测试集的参考价值等早停判断过十几次之后测试集已经完全失效了。这个浪费是不可逆的——数据一旦参与了决策就永远失去了做“局外人”的资格。验证集的存在就是用来消耗的。早停看它、调超参看它、选模型看它、对比数据增强策略看它折腾多少次都不心疼。等所有实验做完了最后拿测试集做一次最终确认整个流程才说得上严谨。2.3 分布偏移与集合一致性问题这里还有个经常被忽略的原则训练集、验证集、测试集必须来自同一个分布。你拿一堆公园场景的照片训练YOLO模型验证集用的也是公园照片结果测试集全换成了停车场监控视角那训练得再好测试分数也高不了。数据划分要保证三个集合的样本都在描述同一个问题、同一个场景、同一种采集方式否则你评估的根本不是模型能力而是数据差异本身。实际操作中“同一分布”并不是那么理所当然。比如你收集数据时不同时间段采样的数据本身就有分布变化早期样本和近期样本在光照、设备、季节上都有差异。如果随机划分有可能训练集里全是早期样本测试集里全是近期样本导致测试集分数虚低。这种时候就不能纯随机切分得按照采集时间等比拆分让每个时间段的数据都按比例进入三个集合。这个问题在下一章我会详细展开。3. 数据怎么切比例、策略和那些隐藏的门道数据切分说起来就是按比例分一分但真正做起来有很多策略要选。切分方式直接决定了你后面所有实验的可靠性在这个环节敷衍后面就是在沙滩上盖楼。3.1 经典划分比例怎么来的最常见的划分比例是训练集验证集测试集 71.51.5 或者 811也有用到 622 的。这些数字并不是什么数学推导出来的精确结论而是在保证训练数据足够的前提下留出足够多的验证集和测试集来获得稳定可靠的评估指标。为什么训练集占大头因为深度学习模型参数动辄几百万上千万数据太少学不出有效特征模型就是欠拟合。验证集和测试集的大小则取决于你需要的评估精度——验证集和测试集规模越小指标的方差就越大你今天跑一个实验是72%的mAP明天加了20个样本重新跑一遍可能就变成76%你根本没法判断改动到底是好是坏。所以划分时要考虑的是验证集和测试集的样本量是否足够让指标保持稳定。如果整个数据集只有100张图片那111的划分意味着验证集只有33张算出来的mAP方差大到没法用。这种小数据集场景我更倾向于把验证集和测试集适当调大采用比如622的比例甚至用交叉验证来替代单一的验证集划分。我记得有个直白的经验验证集样本量至少得让每个类别出现过几十次测试集至少几十到一百个样本不然跑出来的指标连参考意义都存疑。图像分类任务里一个包含10000张图的数据集划分成80%训练、10%验证、10%测试验证集和测试集各有1000张指标通常就比较稳定了如果只有500张图那433这种划法反而更合理牺牲一点训练数据换来评估的可靠性这是值得的。3.2 分层抽样保证类别均衡数据集里类别不平衡是家常便饭很多时候某些类别的样本特别少。这时候最忌讳的就是不管三七二十一直接随机切分因为随机切分很容易把某个稀有类别样本全部切到训练集里验证集里一个都没有模型评估的时候这类别直接显示为0或者波动巨大。分层抽样stratified sampling就是为了解决这个问题。它保证每个集合中各类别所占的比例和原始数据集中保持一致。比如猫狗分类数据集里猫占90%狗占10%那么随机切分出来的训练集也约90%猫、10%狗验证集和测试集也约90%猫、10%狗这样评估结果才有代表性。做目标检测的时候分层抽样稍微麻烦一点。图像分类简单看文件名对应的标签就行目标检测数据集的“真实标签”是一张图里包含的多个目标类别你没法把一张图简单归类到单一类别。我自己在YOLO数据集上做分层划分时一般先解析每个标注文件里出现的类别列表针对稀有类别做约束——优先保证包含稀有类别目标的图片在三个集合中都有分布并且占比接近原始数据。这样做起来比较繁琐但能避免验证集里几乎没有稀有类别的尴尬。分层的另一种做法是对每个类别单独按比例切分再把各个类别的子集合并起来。比如先把包含“行人”的图片按71.51.5切再把包含“车辆”的图片同样切最后合并成训练集、验证集、测试集。注意一张图片可能包含多个类别会重复出现在多个类别子集中所以合并前要去重并且确定归属优先级。3.3 序列类数据按时间切分才靠谱如果你的数据本身带有时间先后顺序比如股票行情、传感器时序数据、监控视频流、用户行为日志就不能随机切分了。随机切分会把未来数据混进训练集模型在训练时已经“见过”未来测试时自然表现超预期上线后完全不是一回事。时间序列数据标准做法是按时间戳切分取前70%的时间段作为训练集随后的15%作为验证集最后15%作为测试集。这样严格保证了训练、验证、测试在时间轴上是从过去向未来推进的模拟了真实场景中“用历史数据预测未来”的工作方式。这里有个细节验证集和训练集之间要注意“间隔期”。模型在预测未来时最近几天的数据和训练集末尾的数据模式往往高度相关如果把验证集紧贴着训练集后面的日子模型可能靠“惯性复制”就能得到不错的结果。实际操作中我习惯在训练集和验证集之间留一段不参与任何集合的空白数据这个间隔期相当于让模型从“已知状态”过渡到“未知状态”测试出来更接近真实业务情况。3.4 小样本数据集交叉验证做替补数据集特别小的时候固定出一个验证集太奢侈了因为留出10%的数据可能只有几十条评估结果方差巨大。这时候我推荐用K折交叉验证把训练数据分成K份轮流取其中1份做验证集剩下K-1份训练得到K个模型和K个验证分数后取平均。K常取5或10。交叉验证的本质是把每一份数据都轮流当一次验证集既充分利用了有限数据又让评估结果更稳定。代价是训练K次模型计算成本高。如果你跑的是YOLO这种大模型动辄训练几个小时做5折交叉验证就是五倍时间要权衡。小数据集也可以用留一法LOO每次只留1个样本做验证。这个方式在小数据集上评估最充分但计算开销极大实际项目中我很少用一般只在几百到几千条样本的非深度学习任务上用。4. 实操用代码快速划分数据集概念讲完直接上代码。我平时做项目时最常用的是scikit-learn的train_test_split做随机划分处理YOLO这类目标检测数据集时会写一个自定义脚本保证图像和标注文件成对移动。这里给出几个可以直接改改就用的版本。4.1 用sklearn做基础划分from sklearn.model_selection import train_test_split # 假设你有 all_samples 列表每个元素是样本的路径 all_samples [fimage_{i}.jpg for i in range(1000)] # 先分出训练集和临时集验证测试的合并 train_list, temp_list train_test_split( all_samples, test_size0.3, # 训练集70% random_state42, # 固定随机种子保证结果可复现 shuffleTrue, # 默认就是True数据够乱才够随机 ) # 再把临时集按比例分成验证集和测试集 val_list, test_list train_test_split( temp_list, test_size0.5, # 临时集的50%即全量的15% random_state42, )注意第二次切分时test_size要基于临时集的比例来算。比如你希望整体是7:1.5:1.5第一次切出30%的临时集后第二次切50%出来当测试集这样测试集占全量的15%验证集也是15%。如果你直接把test_size0.15写进第二次切分它意思就是临时集的15%最终测试集只占全量的4.5%比例就错了。random_state这个参数极其重要。不固定随机种子的话你每次跑脚本划分结果都不一样昨天跑出来的模型A比模型B好0.5个点今天重新划分数据再跑一遍可能就反过来了实验根本没法比较。固定种子到42这种常数值保证每次切分结果完全一致别人复现你的实验也能拿到同样的划分。4.2 手动实现图像分类数据集的按比例复制如果不想把样本移动过来移动过去可以生成三个清单文件文件里每行是一个样本的路径。分类任务直接写import os import random from sklearn.model_selection import train_test_split random.seed(42) data_dir /path/to/dataset classes [d for d in os.listdir(data_dir) if os.path.isdir(os.path.join(data_dir, d))] train_lines, val_lines, test_lines [], [], [] for cls in classes: cls_dir os.path.join(data_dir, cls) imgs [os.path.join(cls, f) for f in os.listdir(cls_dir) if f.endswith((.jpg, .png))] train_t, temp_t train_test_split(imgs, test_size0.3, random_state42) val_t, test_t train_test_split(temp_t, test_size0.5, random_state42) train_lines.extend(train_t) val_lines.extend(val_t) test_lines.extend(test_t) # 写入清单 with open(train.txt, w) as f: f.write(\n.join(train_lines)) with open(val.txt, w) as f: f.write(\n.join(val_lines)) with open(test.txt, w) as f: f.write(\n.join(test_lines))这样每个类别在三个集合里的占比都和原始数据一致不用来回移动文件训练框架读清单文件就行省事又不容易出错。注意这里要保证写入清单的路径是相对路径还是绝对路径取决于框架怎么读取YOLO用相对路径加path前缀会更灵活。4.3 目标检测数据集成对划分脚本目标检测数据集不能只移动图片标注文件必须跟着图片走。YOLO格式的标注通常是和图片同名的.txt文件放在labels目录下。我习惯把图片和标注放在两个不同目录但文件名保持对应关系划分时按图片清单成对处理。import os import random import shutil random.seed(42) # 原始数据目录结构 img_dir /path/to/images # 所有图片 label_dir /path/to/labels # 所有同名txt标注 # 目标目录 out_base /path/to/dataset_split train_dir os.path.join(out_base, images/train) val_dir os.path.join(out_base, images/val) test_dir os.path.join(out_base, images/test) train_label_dir os.path.join(out_base, labels/train) val_label_dir os.path.join(out_base, labels/val) test_label_dir os.path.join(out_base, labels/test) for d in [train_dir, val_dir, test_dir, train_label_dir, val_label_dir, test_label_dir]: os.makedirs(d, exist_okTrue) # 收集所有图片文件 imgs [f for f in os.listdir(img_dir) if f.endswith((.jpg, .jpeg, .png))] # 按比例切分 random.shuffle(imgs) n_train int(len(imgs) * 0.7) n_val int(len(imgs) * 0.15) train_imgs imgs[:n_train] val_imgs imgs[n_train:n_train n_val] test_imgs imgs[n_train n_val:] # 复制成对文件 def copy_pairs(imgs_list, target_img_dir, target_label_dir): for img_name in imgs_list: label_name os.path.splitext(img_name)[0] .txt src_img os.path.join(img_dir, img_name) src_label os.path.join(label_dir, label_name) # 图片和标注都必须存在才复制 if os.path.exists(src_img) and os.path.exists(src_label): shutil.copy(src_img, os.path.join(target_img_dir, img_name)) shutil.copy(src_label, os.path.join(target_label_dir, label_name)) else: print(fWARNING: 缺少成对文件 {img_name} 或 {label_name}) copy_pairs(train_imgs, train_dir, train_label_dir) copy_pairs(val_imgs, val_dir, val_label_dir) copy_pairs(test_imgs, test_dir, test_label_dir) print(f训练集 {len(train_imgs)} 张验证集 {len(val_imgs)} 张测试集 {len(test_imgs)} 张)脚本里的os.path.splitext(img_name)[0] .txt是标的对应文件名的关键。YOLO数据集的标注文件与图片同名、扩展名不同实际操作中这个逻辑不会改唯一要注意的是有些数据集标注文件放在labels目录下且目录结构和images结构同样是逐级嵌套的这时候要保留相对路径之间的关系不能简单平铺复制。4.4 固定随机种子让你免掉90%的复现烦恼我再强调一遍随机种子因为这真的是新手最容易忽略的一环。不固定种子的时候网格搜索调参完全没法做——你今天调A参数得到验证集72%明天跑一遍基线突然变75%你以为是模型改动起了效果其实只是划分变了、样本换了纯属运气。固定随机种子之后所有实验都在同一份数据划分上进行不同实验之间的指标差异才能归因于模型或参数的变化。这是机器学习实验方法论里最底层的纪律比什么高级技术都重要。我的个人习惯是所有划分脚本、训练脚本、数据增强脚本里统一用同一个种子值比如42或者2024然后在项目文档里记清楚每个实验对应的种子和划分版本。这样就算过了半年再回来复现也能分毫不差地重新跑出来。5. 老手也会踩的坑信息泄漏与验证集复用数据集划分里最大的两个坑一个是数据泄漏data leakage一个是验证集反复复用。这两个问题隐蔽性都很强一旦发生模型离线评估虚高、上线崩溃就成了必然结果。5.1 数据泄漏的三种隐蔽形式标准化和归一化是最容易泄漏的地方。很多人在全量数据上计算均值和方差然后再切分数据集这就让验证集和测试集的信息通过全局均值和方差泄漏进了模型。正确的做法是只在训练集上计算均值和方差验证集和测试集在预测时使用训练集算出来的同一个统计值。这看起来是小事但对数据分布敏感的模型差别非常大。第二类是特征构造时不小心用了未来信息。比如做时间序列预测你用“当天的真实标签”来构造当天的特征训练时当然指标爆炸上线后没有标签可用模型直接瘫掉。这类泄漏往往在特征工程阶段就埋下了单纯靠划分数据集救不回来只能靠你对业务的理解去排查。第三类是重复数据和近重复数据。如果原始数据里有几十条几乎一模一样的样本随机划分可能会让同一份数据的复制品同时出现在训练集和验证集里模型在验证集上的效果自然好得离谱。我在处理爬虫数据的时候遇到过这类问题——同一篇文章被多个渠道转载内容高度相似随机划分后模型相当于拿着答案进考场。解决办法是训练前先做去重按内容哈希或者Embedding相似度把近重复样本归并掉再划分。5.2 反复用验证集做决策温水煮青蛙验证集本来就允许反复用但它也有上限。如果你每天跑十次实验每次都根据验证集结果调参连续调了两个月验证集的权威性会逐渐被消耗殆尽。模型结构、超参数、数据增强策略都在有意无意地向验证集分布倾斜这时候你在验证集上看到的高分已经不代表真实泛化能力。我判断验证集是否“用够了”有一个简单的经验同一个模型结构完全不改动连续在验证集上评估两三次指标差不多稳定但如果你对比很多个模型每个模型的最终选择都依赖验证集最高分那么再拿验证集去挑模型就是在挑噪声。正规做法是训练过程中把最优模型的选择交给验证集等到所有实验全部结束只运行一次测试集拿到最终数字对外汇报就是它。如果需要反复评估多个候选模型但测试集只能测一次可以考虑嵌套交叉验证或者把测试集再做分层多分几份、分批探查但这些都是变通做法核心原则依然是测试集的信息不能进入任何决策循环。5.3 切完数据先看分布别急着训练很多人辛辛苦苦写完划分脚本运行完就马上开训练省去了最关键的一步检查划分后的数据分布。我几乎每次都要先统计一下三个集合里类别的数量、图片尺寸的分布、标注框大小分布有没有明显偏差。这些检查花不了几分钟但能筛掉很多低级错误。最典型的例子是某类别在训练集里出现1000次验证集里只有50次测试集里却有300次。这样测试集分数天然就比验证集高不是因为模型变强了而是因为测试集里那个类别更多mAP计算时加权方式不同。你必须在训练之前就看到这些差异而不是等训练完才知道。具体操作上我写了个小函数读取图片对应的标注文件统计每个类别出现的次数然后和划分清单做关联分析。如果发现某个类别在某个集合里的占比偏离总体的20%以上我就会重新划分或者检查分层逻辑。别嫌麻烦这一步能救你至少一个调试周期。5.4 验证集和训练集“打架”的排查思路还有一种很让人抓狂的情况训练损失一直降验证损失先降后升你以为是过拟合了提前停掉结果测试集表现比验证集还好。这时候不要急着下结论先检查验证集是不是有和训练集风格差异很大的样本比如不同分辨率、不同光照条件、不同标注质量。模型在训练集上学到的视觉特征在分布漂移的验证集上自然表现不好但测试集如果更接近训练集分数反而高。遇到这种情况我建议先把三份数据各自的可视化图拉出来看一遍直观对照。YOLO训练自己的数据集时尤其要看图片里目标框的分布是否一致——如果训练集里的目标框普遍大而居中验证集里的目标框小而分散那验证集指标差根本不是模型问题是数据分布不一致。这类问题发生在切分策略上而不是模型结构上。6. YOLO项目中的训练/验证/测试划分细节如果你使用的是YOLOv5、YOLOv8、YOLO26这些框架训练自己的数据集或者用mmrotate训练DOTA这类旋转框检测数据集甚至用UNet训练自己的分割数据集数据划分的方式有一些共性也有一些框架特有的坑这里单独拎出来讲。6.1 图像与标注文件必须严格成对YOLO系列框架在读取数据时通常要求图片放在images目录标注文件放在labels目录而且文件名严格一一对应。图片a.jpg对应标注a.txt缺一个都会导致训练崩溃。最稳妥的划分方式是把每张图片当成一个整体图片复制到A集合标注文件同步复制到A集合的标注目录绝对不允许出现“图片在训练集、标注在测试集”这种情况。我自己的实操逻辑遵循前面代码示例里copy_pairs那套逻辑并且在事后多跑一步校验遍历两个目录的文件名取后缀前的部分做集合差输出找不到对应文件的列表。因为我在真实项目中遇到过数据集里有几百张图片没有标注文件的情况这种脏数据混入训练集轻则训练中断重则静默跳过导致训练分布失衡光靠肉眼根本发现不了。6.2 train.txt还是目录结构取决于你用的框架YOLOv5和YOLOv8支持两种数据组织方式。一种是直接把图片按训练、验证、测试放在不同子目录里比如images/train、images/val、images/test标注文件对应放在labels/train、labels/val、labels/test。另一种是提供三个清单文件每行写一个图片路径框架会自动推导标注路径。两种方式各有优劣。使用目录结构时YOLO的data.yaml写起来最简单path: /data/dataset_split train: images/train val: images/val test: images/test names: 0: person 1: car 2: bicycle这里names的类别编号顺序必须和标注文件里的第一个数字严格一致。如果你在标注文件里把person标成1、car标成2但在yaml里把索引0对应到person模型会把类别全部识别错而且训练时损失照样下降因为模型学到的是错乱的对应关系这种错误极其隐蔽。使用清单文件时data.yaml写成这样train: /data/train.txt val: /data/val.txt test: /data/test.txt清单文件里每行一个图片绝对路径或相对路径框架会在同样的相对路径下找标注文件的对应位置。我推荐新手优先用目录结构因为目录结构天然有序文件复制到位后不容易遗漏清单文件适合数据集已经很大、不想重复拷贝文件的场景生成一份文本清单成本最低。6.3 目标检测数据集的类别均衡划分要怎么做目标检测里一张图片可能有多个目标、多个类别与分类任务单标签分层的思路不同。比如一张街景图同时包含行人、自行车和车辆另一张只包含行人。类别数量相差悬殊时随机划分可能导致几乎所有包含罕见类别目标的图片都跑进训练集验证集和测试集里该类别数量为零或接近零。我给YOLO项目写过度量轻重更重重的新手方案先统计每张图片里出现过的类别集合找出整个数据集中出现次数最少的几个类别把这些图片单独拎出来按比例预先分配进训练、验证、测试然后再把剩余图片按普通随机方式划分。这样可以保证稀有类别在这三个集合里都有足够样本。实际操作时我用一个字典记录每张图片包含的类别集合然后定义“稀有类别”为出现次数低于总图片数5%的类别。包含这些类别的图片优先分配再分配其他图片。如果你在mmrotate里跑DOTA数据集DOTA本身类别相对均衡这个逻辑可能用不上但换到自己的业务数据时往往类别分布很不均衡这招就很关键。6.4 UNet和mmrotate的划分注意点UNet这类分割任务图片和Mask标注之间的对齐关系和YOLO是一致的划分时要保证原图与标签成对移动。另外分割任务里我还会检查标签的单像素区域面积分布如果一幅分割图里正样本的像素占比极低而它恰好被划进测试集模型在该样本上的IoU会剧烈拉低整体指标。所以UNet数据集划分后我会统计每张标签图的像素分布尽量保证三个集合的标签稠密度相对一致避免验证集和测试集里全是稀疏标签的“硬样本”。mmrotate训练DOTA数据集时要额外注意标注文件里不是普通的class_id cx cy w h而是旋转框的cx cy w h angle坐标格式。划分脚本如果直接复用YOLO的解析逻辑可能把旋转框的角度值误当作类别处理。我在简单处理这类数据集时划分阶段不管标注内容的具体格式唯一的原则就是“标注文件整体跟随图片走”不做任何一行的解析和改写。等到训练时再由mmrotate的数据加载模块去解析标注这样划分脚本的兼容性最强也最不容易出错。6.5 值班检查清单划分完之后马上看的五个指标我总结了一份划分后检查清单每次切完数据集都按顺序跑一遍不用花太久但能让后面省掉大半调试时间三份数据的数量是否和设定的比例一致差值不能超过样本总量的1%。每个类别在三个集合中的占比是否和原始数据集接近偏差超过20%就要重新检查分层逻辑。图片和标注文件是否严格成对运行一次文件名差集校验输出缺失列表。随机抽几张训练图片、验证图片、测试图片手动打开看一眼确认没有错乱文件比如把某个类的图片全放到了另一个类的目录下。类别名和类别编号的对应关系是不是和data.yaml里的names完全一致这一步最基础但最容易漏。我几乎每次都能靠这份清单在训练启动前发现至少一个问题要么是某类样本在测试集里缺失要么是标注文件命名后缀大小写不一致。你可以把这份清单贴在自己项目文档里当做一个标准流程来执行。我自己在实际项目里已经形成了肌肉记忆数据到手先统计分布再想怎么切切完一定要做校验最后固定随机种子、保存划分版本记录。这套流程也许不能保证模型效果一定好但至少能保证你评判模型效果的方式是可靠的。数据划分这件事做对了平平无奇做错了后患无穷值得多花那半小时。如果你用的是YOLO或者自己写训练脚本建议把上面那个copy_pairs脚本保存成一个常用工具把数据目录、输出目录、划分比例三个参数抽出来做成配置文件以后任何项目拿到新数据都能直接套用。练熟了之后你自然会理解训练集、验证集、测试集这三者之间的关系就是机器学习项目里那条看不见的安全绳。