YOLO26训练前必看:数据集划分方法、脚本与避坑指南(含校验) 📅 发布时间:2026/9/13 13:04:58 👁 浏览次数: 1. 为什么训练前必须自己动手划分数据集不只是走流程这么简单先说一个我自己踩过的坑。早几年刚接触目标检测时我图省事直接把网上下载的某个数据集原封不动丢进训练脚本结果验证集的mAP高得离谱我还以为模型训得不错。后来换了一批图片做实测效果立刻打回原形。排查了半天才发现问题出在数据集划分上——原始数据里同一个目标的图片被拆到了训练集和验证集模型在验证阶段等于开卷考试指标当然好看实际部署却完全不是一回事。这其实就是YOLO系列训练里最容易被忽略、却最能决定成败的一步。你后续所有改进backbone、调损失函数、做轻量化的工作全都建立在数据集划分是否合理之上。如果这一步糊弄过去后面花再多时间调参都是白费。虽然YOLO26这类新版本在训练脚本里通常会给出默认的数据集加载逻辑但它不会替你完成怎么把原始图片和标注文件科学地分成训练集、验证集必要时还有测试集这件事。原因也很简单划分策略和你的数据规模、类别分布、业务场景强相关没有任何一个通用框架能替你做出最优决策。这篇文章就是我基于自己多次实际训练YOLO26的经验把数据集划分这件事完整拆开来讲。核心内容包括三部分为什么划分不是随便按比例切一刀以及训练集、验证集、测试集各自扮演什么角色一份可以直接运行的完整划分脚本适配YOLO格式的txt标注文件和目录结构划分完成后必须做的一致性校验、常见错误排查以及针对小数据集和不平衡类别的进阶处理思路。不管你是第一次准备训练YOLO26的新手还是已经跑过不少实验、想规范流程的进阶玩家这篇文章都能给你一套可以直接抄作业的完整方案。2. 划分前必须想清楚的三件事比例、层级和随机种子2.1 训练集/验证集/测试集各管什么事用错了会怎样很多人上来就问按7:2:1还是8:1:1切但我觉得更核心的问题是你切出来的三个集合分别用来干什么它们的边界在哪里。训练集模型实际学习的样本。模型通过反复看这些图片和标注不断调整权重让预测结果逼近真实标注。验证集训练过程中用来阶段考的数据。每训练若干个epoch模型会在验证集上跑一次计算mAP、loss等指标。你要根据这些指标决定是否保存当前权重、是否调整学习率、是否早停。测试集训练全部结束后拿完全没参与过训练和验证调参的数据做一次期末考模拟模型在真实新数据上的表现。测试集不能在训练过程中被用来做任何决策。用生活类比来说训练集是学生平时做的练习题验证集是月考卷测试集是高考卷。你不可能拿月考卷里的题目当高考题不然高考成绩毫无参考意义。在YOLO26训练中val.py或训练时自动执行的验证环节用验证集算出的指标决定了你挑选哪个epoch的权重作为最终模型而test.py用测试集评估的是最终模型的泛化能力。两者混淆、或划分时泄漏最直接的后果就是你看到的指标虚高真实场景下一塌糊涂。2.2 目录结构和标注格式脚本适配的前提YOLO26沿用YOLO系列一贯的数据组织方式一般长这样dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ └── labels/ ├── train/ ├── val/ └── test/每张图片对应一个同名txt标注文件。比如images/train/000001.jpg对应labels/train/000001.txttxt里每一行是class_id x_center y_center width height注意x_center y_center width height都是归一化到0~1之间的相对坐标不是像素坐标。这一点在划分后校验时很重要后面会专门讲。如果你的数据是别的格式比如VOC的xml或者COCO的json建议先统一转成YOLO格式再走下面的脚本。YOLO26官方仓库里其实也带了格式转换的工具但那是题外话这篇文章聚焦在划分这一步。2.3 随机种子的重要性可复现实验的基石我自己第一次划分数据集时根本没设置随机种子结果每次运行脚本划分结果都不一样。后来训练时稍微调了一点数据增强参数模型指标涨了0.5个点我却没法判断到底是增强策略带来的提升还是数据划分变动带来的偶然波动。所以划分脚本里必须设置随机种子。random.seed(42)虽然简单但能保证你每次跑出来的划分结果完全一致这对后续做对比实验、调参复盘极其关键。用sklearn的train_test_split时同样可以传random_state参数。2.4 划分比例怎么定不只有7:2:1一种答案对于绝大多数场景7:2:1或者8:1:1是合理的起点。但实际怎么定取决于你的数据总量和类别分布。数据量很大比如每类上万张验证集和测试集各取5%~10%就足够稳定评估可以给训练集留出更多数据。数据量较小比如每类只有几十张建议适当调大验证集和测试集的比例否则验证集只有十几张图算出来的mAP方差极大完全没法反映模型真实水平。类别不平衡严重不能简单随机切最好按类别比例做分层采样。比如类别A有1000张、类别B有100张随机切的话B在验证集里可能只剩个位数模型训练和评估都不靠谱。分层采样的意义就是让每个集合里的类别比例尽量和全量数据一致。我在实际中常用sklearn的StratifiedShuffleSplit或者train_test_split配合stratify参数来实现分层划分。不过YOLO的标注文件里每张图可能同时包含多个类别分层时是按图片分层还是按实例分层需要想清楚。简单做法是以图片为单位给每张图片标注包含哪些类别然后根据多标签做分层复杂一点可以用迭代分层。对小数据集来说先按主类别或直接随机划分观察类别分布是否失衡再决定要不要上分层。3. 完整划分脚本适配YOLO26的train/val/test三步分离下面这份脚本我目前一直在用适配上面说的images/train、images/val、images/test和对应labels目录的结构。核心逻辑是扫描所有图片文件按设定比例划分再把对应的标注文件一并移动或复制过去。import os import random import shutil from collections import defaultdict random.seed(42) # 固定随机种子保证划分结果可复现 def split_dataset( source_images_dir: str, source_labels_dir: str, target_root: str, train_ratio: float 0.8, val_ratio: float 0.1, test_ratio: float 0.1, copy: bool True, ): 将YOLO格式数据集划分为train/val/test source_images_dir: 原始图片目录所有图片放在一起 source_labels_dir: 原始标签目录所有txt放在一起与图片同名 target_root: 输出根目录会自动创建 images/{train,val,test} 和 labels/{train,val,test} copy: True则复制原文件False则移动原文件 assert abs(train_ratio val_ratio test_ratio - 1.0) 1e-6, \ train_ratio val_ratio test_ratio 必须等于 1 image_paths [] for ext in (.jpg, .jpeg, .png, .bmp, .webp): image_paths.extend( os.path.join(source_images_dir, f) for f in os.listdir(source_images_dir) if f.lower().endswith(ext) ) image_paths.sort() assert len(image_paths) 0, 图片目录为空 # 检查标签是否齐全 missing [] for img_path in image_paths: basename os.path.splitext(os.path.basename(img_path))[0] label_path os.path.join(source_labels_dir, basename .txt) if not os.path.exists(label_path): missing.append(basename) if missing: raise RuntimeError(f以下图片缺少对应txt标注: {missing[:10]} ...) random.shuffle(image_paths) total len(image_paths) n_train int(total * train_ratio) n_val int(total * val_ratio) # test 用余下部分避免浮点累积误差 splits { train: image_paths[:n_train], val: image_paths[n_train:n_train n_val], test: image_paths[n_train n_val:], } for split_name, paths in splits.items(): out_img_dir os.path.join(target_root, images, split_name) out_lbl_dir os.path.join(target_root, labels, split_name) os.makedirs(out_img_dir, exist_okTrue) os.makedirs(out_lbl_dir, exist_okTrue) for img_path in paths: img_name os.path.basename(img_path) label_name os.path.splitext(img_name)[0] .txt src_label os.path.join(source_labels_dir, label_name) dst_img os.path.join(out_img_dir, img_name) dst_label os.path.join(out_lbl_dir, label_name) if copy: shutil.copy2(img_path, dst_img) shutil.copy2(src_label, dst_label) else: shutil.move(img_path, dst_img) shutil.move(src_label, dst_label) print(f{split_name} 数量: {len(paths)}) if __name__ __main__: split_dataset( source_images_dirraw_data/images, source_labels_dirraw_data/labels, target_rootdataset, train_ratio0.8, val_ratio0.1, test_ratio0.1, copyTrue, )这段脚本有几个设计点值得说明先检查标签再划分。如果标签缺失脚本直接报错并列出前10个缺失文件避免划分完成后训练到一半才发现某张图没有标注文件。test用余量计算。如果直接int(total * test_ratio)三个比例之和可能因为取整误差不等于总数的100%。用余量保证所有图片都被分到某个集合不会漏掉。copy和move可切换。默认复制这样原数据完好保留适合初次划分时反复调整比例确认没问题后再改成移动可以省一份磁盘空间。这个脚本默认是所有图片放在一个大目录的输入形态。如果你的原始数据已经是images/train、images/val都有内容只是想重新洗牌合并再划分可以先把所有图片路径收集起来再统一处理逻辑类似就不重复贴代码了。4. 划分后的必备校验图片、标签和类别分布一个都不能少脚本跑完第一件事不是急着写data.yaml开训而是做三组校验。这一步能省掉后续大量低级报错排查时间。4.1 图片与标签一一对应且数量一致我习惯用一行命令快速数数量find dataset/images/train -type f | wc -l find dataset/labels/train -type f | wc -l两个数字必须一致。如果图片多、标签少说明划分时漏拷贝了部分标注反过来也是问题。脚本里虽然做了源目录检查但目标目录生成后仍可能因为文件系统异常、中断执行等原因出现不一致所以校验不可省略。进一步检查同名对应而不是只数数量for img in dataset/images/train/*.jpg; do base$(basename $img .jpg) if [ ! -f dataset/labels/train/$base.txt ]; then echo MISSING: $base fi doneWindows下没有bash的话在Python里做同样的事即可。4.2 标注内容格式与坐标范围校验这一项很多人会忽略。YOLO格式要求坐标在0~1之间如果之前标注或转换出了问题坐标写成像素值训练时不会直接报错但loss会莫名其妙地降不下去。写一个小脚本检查所有txt的每行格式import os label_dirs [dataset/labels/train, dataset/labels/val, dataset/labels/test] invalid_count 0 for ld in label_dirs: for f in os.listdir(ld): if not f.endswith(.txt): continue with open(os.path.join(ld, f), r) as fp: for line in fp: parts line.strip().split() if len(parts) ! 5: print(f字段数错误: {ld}/{f}: {line.strip()}) invalid_count 1 continue try: cls float(parts[0]) vals list(map(float, parts[1:])) except ValueError: print(f非数字内容: {ld}/{f}: {line.strip()}) invalid_count 1 continue if cls 0 or any(v 0 or v 1 for v in vals): print(f坐标越界: {ld}/{f}: {line.strip()}) invalid_count 1 print(f非法标注行数: {invalid_count})另外还可以顺手统计每张图片的实例数是否异常为0。空的txt文件虽然属于合法格式但它表示这张图里没有任何目标训练时通常会被当作背景样本。如果空txt过多尤其是验证集里大量是空图mAP会偏低且抖动剧烈因为模型几乎没有任何正样本可评估。4.3 类别分布透视防止某个罕见类在验证集中蒸发划分完成后统计每个集合里的类别频次是判断划分是否合理的快速方法。可以用下面这段代码按实例数统计import os from collections import Counter class_names [class0, class1, class2] # 按你的data.yaml顺序填 for split_name in [train, val, test]: label_dir fdataset/labels/{split_name} counter Counter() instance_total 0 for f in os.listdir(label_dir): if not f.endswith(.txt): continue with open(os.path.join(label_dir, f), r) as fp: for line in fp: cls_id int(line.strip().split()[0]) counter[cls_id] 1 instance_total 1 print(f\n{split_name} 实例总数: {instance_total}) for cls_id in sorted(counter): name class_names[cls_id] if cls_id len(class_names) else cls_id print(f {name}: {counter[cls_id]})如果某个类别在训练集里有80%的比例验证集里却只剩1~2个实例这个验证集的评估结果基本没有参考价值需要重新划分或考虑分层采样。大量实际经验表明目标检测模型对罕见类本来就容易漏检如果罕见类在验证集里的样本还不够你连漏检这件事都测不出来这是划分阶段就需要规避的。5. 常见踩坑实录我自己走过的弯路你最好别走5.1 只划分图片忘划分标签训练一启动就报错这个错误我犯过不止一次。早期我为了省事只把图片复制到了images/train标签目录却忘了同步拆分结果训练初始化时报找不到标签文件。后来我总结了个习惯划分后第一时间做上面第4.1节的同名对应校验而不是直接开训。5.2 划分后忘记修改data.yaml里的路径和类别数YOLO26训练时读取的data.yaml长这样path: dataset train: images/train val: images/val test: images/test nc: 3 names: [class0, class1, class2]很多人划分完数据集data.yaml里还写着旧的路径。最典型的是train和val路径写成了绝对路径换机器后直接失效。我的建议是path字段用相对路径并且整个数据集目录和data.yaml放在同一个项目根下这样部署和复现都方便。nc和names的顺序必须和标注文件里的class_id一一对应。如果你在标注阶段改了类别顺序这里也要同步改否则训练时的loss会乱。5.3 工业场景下的数据泄漏同源图片同时出现在训练集和验证集再回到文章开头我说的那个坑。工业质检、安防监控这些场景里同一个设备或同一个目标往往会被连续拍摄多张高度相似的图片。如果随机划分时这些相似图片一部分进了训练集、一部分进了验证集模型在验证时其实就是见过类似内容的指标虚高。解决思路是按数据源比如按设备ID、按视频片段ID做分组划分保证同一个源的数据不被拆散。实践中我会在数据目录里给每组数据建子目录或者给文件名加上前缀然后按前缀分组采样。5.4 小数据集下验证集太小指标波动剧烈如果你一共只有200张图按8:1:1划分后验证集只有20张。每张图片对mAP的影响都很大很可能这轮epoch保存的权重比上轮高纯粹是因为验证集里恰好多了一两个简单样本。我的应对办法是适当提高验证集比例比如7:1.5:1.5如果还是太小可以用K折交叉验证的方式轮流做验证集最后综合各折的指标。虽然训练时间翻倍但评估结果要靠谱得多在训练脚本里设置patience时留出一定余量不要因为几个epoch的抖动就过早早停。5.5 把val当作test用最终模型性能评估失真训练过程中的权重挑选只能依赖验证集这是规则也是潜规则。可一旦你反复用验证集调参、挑选epoch验证集其实已经脏了——你是在用验证集上的表现做决策最终报告里再拿验证集数字当作最终泛化精度这就是典型的把月考当高考。所以务必留出测试集并且只在最终模型确定之后跑一次。如果你发现自己跑了三次测试集、改了三次超参那这个测试集事实上已经变成了第二个验证集。6. 进阶思路从能跑到可复现的流程固化数据集划分这件事本身不复杂但它和整个训练流程的质量管理体系强相关。最后分享几个让我训练效率明显提升的小习惯。6.1 把划分脚本和data.yaml一起版本管理不要只在命令行里跑一次脚本就完事。把划分脚本、data.yaml、类别列表、簇类分析结果如果需要anchors全部纳入git管理。这样你三个月后回头看还能准确知道某次实验用的数据是怎么划分出来的。6.2 记录划分日志随机种子、比例、文件数建议在脚本里加一行输出把这次划分的配置写入一个split_info.yamlrandom_seed: 42 train_ratio: 0.8 val_ratio: 0.1 test_ratio: 0.1 train_images: 1200 val_images: 150 test_images: 150以后做实验复盘时这份日志能帮你快速定位指标变化到底是数据变了还是模型变了。6.3 划分后先跑通一个迷你实验再全量训练哪怕数据集划分没有任何问题也建议先拿划分后的一小部分数据比如每类50张快速启动一个只跑几个epoch的YOLO26训练。这个步骤不是浪费算力而是验证环境、路径、标注读取、数据增强整个链路是否能串起来。我通常会在一个debug模式下跑5个epoch确认loss正常下降后再启动全量训练。很多环境层面的问题与其在全量训练卡到一半再排查不如在迷你实验里提前暴露。7. 写在最后划分做得好YOLO26训练就成功了一半坦白说数据集划分不是多高深的技术但它是整个目标检测实验链条里最容易埋雷的一环。比例不科学、随机种子不固定、标签漏拷贝、验证集泄漏、类别分布失衡——任何一个问题都会让后续的模型改进、损失函数调整、轻量化部署全部建立在不可靠的数据地基上。我在实际项目中养成的固定流程是原始数据整理 → 划分 → 三重校验数量对应、格式合法、类别分布→ 迷你实验 → 全量训练。每一步都走得机械而枯燥但恰恰是这种枯燥让后续的实验结果可追溯、可对比、可复现。YOLO26本身的网络结构、损失函数、改进技巧当然值得研究可如果你连模型到底在什么数据上学的、在什么数据上考的都没搞清楚那些上游研究做得再花哨也很难转换成真实场景里可靠的效果。如果你按照这篇文章的脚本和校验流程走一遍应该能避免我当年踩过的大部分坑。如果你在实际划分过程中遇到脚本报错或类别分布问题欢迎在评论区把具体情况贴出来我看到了会尽量回复。毕竟这类问题往往很吃具体数据一个细节不同解决方案就完全不一样。