4. 数据集划分与组织:训练集/验证集/测试集划分原则、目录结构与配置文件
数据集做好了,标注也完成了。接下来这一步,很多人觉得简单——不就是把文件分几个文件夹吗?
但我见过太多项目,因为这一步没做好,后面训练出来的模型要么过拟合,要么评估结果虚高,根本没法用。说白了,数据集划分和组织,决定了你后面所有工作的可信度。
4.1 训练集 / 验证集 / 测试集:各自干什么用
先搞清楚三者的角色。我习惯用一个比喻:
- 训练集——学生的课本。模型从这里学知识。
- 验证集——随堂测验。每次学完一个阶段,测一下学得怎么样,用来调超参数、选模型。
- 测试集——期末考试。模型全部训练完成后,最后考一次,看看真实水平。
注意,测试集在整个训练过程中都不能碰。我见过有人用测试集的结果反复调参,那等于把期末考试答案提前给学生看了,分数再高也没意义。
核心原则:测试集只能用于最终评估,不能参与任何训练决策。
4.2 划分比例:没有万能公式,但有经验值
数据集大小不同,划分比例也不一样。我列一个常用的参考表:
| 数据集规模 | 训练集 | 验证集 | 测试集 | 我的建议 |
|---|---|---|---|---|
| 小(< 1,000 张) | 70% | 20% | 10% | 尽量用 K 折交叉验证 |
| 中(1,000 ~ 10,000 张) | 80% | 10% | 10% | 最常用的比例 |
| 大(> 10,000 张) | 90% | 5% | 5% | 验证集够用就行 |
为什么小数据集要留更多验证集?因为样本少,验证集太小的话,评估结果波动会很大,你根本分不清是模型变好了还是随机运气。
我的经验:如果数据集少于 500 张,我建议直接用 K 折交叉验证(比如 5 折),而不是固定划分。这样每个样本都有机会做验证,评估更稳定。
4.3 划分方式:随机 vs 分层 vs 按场景
光有比例还不够,怎么分也很关键。三种常见方式:
- 随机划分:最简单,适合数据分布均匀的情况。但万一随机抽到的验证集全是某个类别,那就麻烦了。
- 分层划分:保证每个类别在训练/验证/测试集中的比例一致。我强烈推荐这种方式,尤其是类别不平衡的时候。
- 按场景划分:比如按视频序列、按拍摄时间、按地点。防止同一场景的相似图片同时出现在训练集和测试集里,造成评估虚高。
我曾经做过一个项目,用随机划分,测试集准确率 92%。结果一上线,直接掉到 70%。为什么?因为训练集和测试集里有很多同一段视频的连续帧,模型其实是在「记住」场景,而不是「学会」检测。后来改成按视频序列划分,才拿到真实指标。
避坑:如果你的数据来自视频抽帧,一定要按视频 ID 划分,不能随机分帧。否则测试集里很可能出现训练集的「邻居」,评估结果会虚高 10%~20%。
4.4 目录结构:YOLO 的标准组织方式
YOLO 对数据集目录有约定俗成的结构。我个人习惯这样组织:
dataset/
├── images/
│ ├── train/ # 训练集图片
│ ├── val/ # 验证集图片
│ └── test/ # 测试集图片
├── labels/
│ ├── train/ # 训练集标注
│ ├── val/ # 验证集标注
│ └── test/ # 测试集标注
└── data.yaml # 数据集配置文件
注意几个关键点:
- 图片和标注文件名必须一一对应。比如
img_001.jpg对应img_001.txt,扩展名不同但主文件名完全一致。 - 标注文件是 .txt 格式,每行一个目标:
class_id x_center y_center width height,所有坐标都是归一化的(0~1)。 - 图片格式建议统一。我一般用 .jpg,压缩率适中,读取速度快。
小技巧:用 Python 的 os.symlink() 创建软链接,而不是复制文件。尤其是图片很大的时候,能省大量磁盘空间。
4.5 数据集配置文件:data.yaml
YOLO 通过一个 YAML 文件来告诉训练脚本:数据集在哪、有多少类别、类别名是什么。
一个标准的 data.yaml 长这样:
# 数据集路径(相对或路径均可)
path: ../dataset
# 各子集路径(相对于 path)
train: images/train
val: images/val
test: images/test # 可选,训练时用不到
# 类别数量
nc: 3
# 类别名称(索引从 0 开始)
names:
0: person
1: car
2: bicycle
几个容易踩坑的地方:
- 路径不要写错。YOLO 会拼接
path + train得到完整路径。如果path是相对路径,注意当前工作目录。 nc必须和names列表长度一致。我见过有人改了names忘了改nc,训练直接报错。test字段是可选的。训练时 YOLO 只读train和val,test是留给后续评估用的。
注意:YOLOv5/v8 对 YAML 的缩进非常敏感。不要用 Tab,统一用两个空格缩进。我曾经因为编辑器自动插入了 Tab,排查了半小时才找到原因。
4.6 自动化划分脚本
手动分文件太累了,尤其数据集大了以后。我写了一个简单的 Python 脚本,每次做新项目都直接复用:
import os
import random
import shutil
from sklearn.model_selection import train_test_split
# 配置
image_dir = 'raw_images' # 原始图片目录
label_dir = 'raw_labels' # 原始标注目录
output_dir = 'dataset' # 输出目录
train_ratio = 0.8
val_ratio = 0.1
test_ratio = 0.1
# 获取所有图片文件
images = [f for f in os.listdir(image_dir) if f.endswith('.jpg')]
# 分层划分(按类别比例)
# 这里简化处理,实际项目中建议用 StratifiedShuffleSplit
train_val, test = train_test_split(
images, test_size=test_ratio, random_state=42
)
train, val = train_test_split(
train_val, test_size=val_ratio/(train_ratio+val_ratio), random_state=42
)
# 创建目录结构
for split in ['train', 'val', 'test']:
os.makedirs(f'{output_dir}/images/{split}', exist_ok=True)
os.makedirs(f'{output_dir}/labels/{split}', exist_ok=True)
# 复制文件
def copy_files(file_list, split):
for img_file in file_list:
# 复制图片
shutil.copy(
f'{image_dir}/{img_file}',
f'{output_dir}/images/{split}/{img_file}'
)
# 复制对应的标注文件(.jpg -> .txt)
label_file = img_file.replace('.jpg', '.txt')
if os.path.exists(f'{label_dir}/{label_file}'):
shutil.copy(
f'{label_dir}/{label_file}',
f'{output_dir}/labels/{split}/{label_file}'
)
copy_files(train, 'train')
copy_files(val, 'val')
copy_files(test, 'test')
print(f'划分完成:训练集 {len(train)},验证集 {len(val)},测试集 {len(test)}')
这个脚本做了几件事:
- 读取原始图片列表
- 按比例随机划分(实际项目建议改成按类别分层)
- 自动创建 YOLO 标准目录结构
- 同时复制图片和对应的标注文件
改进建议:如果数据来自多个视频,建议先按视频 ID 分组,再以组为单位划分。这样能避免前面提到的「场景泄露」问题。
4.7 整体流程:一张图看懂
下面这张 SVG 图,把数据集划分与组织的完整流程串起来了。你可以把它当作操作清单:
从原始数据开始,先选划分策略,再分成三份,然后按标准目录结构组织,最后写一个 data.yaml 配置文件。每一步都有坑,但每一步也都有固定的解法。
4.8 验证划分结果
划分完了,别急着训练。先做两件事:
- 检查文件数量。训练集、验证集、测试集的图片数量和标注文件数量是否一致?有没有漏标的?
- 检查类别分布。每个集合里的类别比例是否接近?如果验证集里某个类别只有一两张,评估结果会很不稳定。
我习惯写一个简单的统计脚本:
import os
def check_dataset(data_path):
for split in ['train', 'val', 'test']:
img_dir = f'{data_path}/images/{split}'
label_dir = f'{data_path}/labels/{split}'
imgs = set(os.listdir(img_dir))
labels = set(os.listdir(label_dir))
# 检查文件名是否对应
img_names = {f.replace('.jpg', '') for f in imgs}
label_names = {f.replace('.txt', '') for f in labels}
missing_labels = img_names - label_names
missing_imgs = label_names - img_names
print(f'{split}:')
print(f' 图片 {len(imgs)} 张,标注 {len(labels)} 个')
if missing_labels:
print(f' 缺少标注的图片: {missing_labels}')
if missing_imgs:
print(f' 缺少图片的标注: {missing_imgs}')
check_dataset('dataset')
这一步花不了几分钟,但能避免后面训练时各种莫名其妙的报错。
总结一下:数据集划分不是简单的文件搬运。划分策略、目录结构、配置文件,每一步都影响最终模型的可信度。花 10 分钟把这一步做好,后面能省 10 小时的排查时间。