Ultralytics YOLO27: 加入候补名单。
Get Started
计算机视觉基础·查看指标·课程 8/10
课程初级

如实反映情况的数据集划分

找出训练集和验证集之间泄漏的近重复图像,并按部署时会出现新样本的单位进行划分。

如果模型在验证集上得分为 95%,在生产环境中却只有 60%,通常是划分方式有问题。近乎相同的图像——相邻的视频帧、同一天由同一台相机拍摄的图像——被分到了两边,因此验证测的是记忆而非泛化。你会在一个真实数据集中找到这样的泄漏,然后用分组划分来修复。

学习成果

检测不同数据集划分之间的近重复图像,按分组键划分,并确认每个组只出现在一个划分中。

快速通道
如果你已经熟悉相关内容,以下是简要版。
  1. 训练集用于拟合权重,验证集用于指导决策,测试集只用于最终评估一次。

  2. 在相信任何验证分数之前,先检查不同划分之间是否存在近乎相同的图像。

  3. 找出分组键:片段、相机、日期或对象。

  4. 随机打乱组而不是图像,然后检查每个类别是否都出现在每个划分中。

  5. 冻结测试集,每个实验完成后只评估一次。

实践操作

三种划分,各司其职#

划分模型从中学习的内容查看时机
训练集模型:在这里拟合权重每个 epoch
验证你: 超参数、epochs、模型大小、阈值每次实验后
测试没人:它只报告最终数值一次,在所有决策都已做出之后

你每次查看一个数据集划分并据此做出决策,都会有少量信息泄漏到模型中。这正是验证集的作用。在测试集上调参,测试结果就不再是诚实的估计——你已经对自己的选择过拟合了。在 Ultralytics 中,model.val(split="test") 根据你的数据集 YAML 对测试集划分进行评分。

在真实数据集中发现泄漏#

这些医药药片图像的文件名是 Frame_0.jpg、Frame_4.jpg、Frame_8.jpg——它们是视频帧。这是一个警示信号:相邻帧几乎完全相同,而随机划分会让它们出现在训练集和验证集两边。你可以比较每张验证图像与每张训练图像的微小灰度指纹来检查:

from pathlib import Path

import numpy as np
from PIL import Image
from ultralytics.data.utils import check_det_dataset

data = check_det_dataset("medical-pills.yaml")

def thumbnail(path):
    """A 32x18 grayscale fingerprint: enough to spot near-identical frames."""
    return np.asarray(Image.open(path).convert("L").resize((32, 18)), dtype=float)

train = {p.name: thumbnail(p) for p in sorted(Path(data["train"]).glob("*.jpg"))}
pairs = []
for p in sorted(Path(data["val"]).glob("*.jpg")):
    v = thumbnail(p)
    name, diff = min(((n, np.abs(v - t).mean()) for n, t in train.items()), key=lambda x: x[1])
    pairs.append((diff, p.name, name))

pairs.sort()
print(f"{sum(d < 5 for d, _, _ in pairs)} of {len(pairs)} val images have a near-twin in train (difference < 5)")
for diff, val_name, train_name in pairs[:3]:
    print(f"  {val_name} ~ {train_name}  difference {diff:.1f}")
17 of 23 val images have a near-twin in train (difference < 5)
  Frame_344.jpg ~ Frame_56.jpg  difference 0.3
  Frame_28.jpg ~ Frame_312.jpg  difference 0.5
  Frame_24.jpg ~ Frame_308.jpg  difference 1.8

在 0–255 的范围内,差值为 0.3 意味着两张图像几乎是同一张图片。把 val/Frame_24.jpg 和 train/Frame_308.jpg 并排打开,你会看到同一角度拍摄的相同泡罩包装。23 张验证图像中有 17 张在训练集中有对应的近似图像,因此这里的验证分数主要衡量模型对已见图像的记忆程度。这对于快速演示数据集来说没问题——但你绝不能用这样的数据集交付产品。

Ultralytics Platform 中医药药片数据集的重新分配划分对话框,设置为 80% 训练、20% 验证、0% 测试

任何工具里都可能出现同样的陷阱。这个对话框自己的说明写着,它会对每张图像进行随机重新分配,因此适用于图像彼此独立的照片——但用于视频帧时就会造成泄漏。先运行近似重复检查;如果你希望最终数值没有经过调参,就要确保测试集占比高于 0%。

按组划分#

规则:划分必须以部署时会遇到的新单位为基准,并确保各组相互独立。

数据划分依据
视频片段或录制会话
固定摄像头组摄像头或地点
随时间变化的场景日期或周次
医疗、体育或人员数据患者、运动员或受试者
连拍照片拍摄事件

文件名通常包含分组键。这个示例用 6 个摄像头拍摄的图像构建了一个包含 900 张图像的数据集,并比较两种策略:

import random
from collections import defaultdict

# File names carry the unit that must never straddle splits: camera_day_frame.jpg
images = [f"cam{c:02d}_day{d}_{f:04d}.jpg" for c in range(1, 7) for d in range(1, 4) for f in range(50)]

# WRONG: shuffle individual frames, so near-identical neighbors land in both splits
random.seed(0)
shuffled = random.sample(images, len(images))
train, val = shuffled[: int(0.8 * len(images))], shuffled[int(0.8 * len(images)) :]
shared = {i.split("_")[0] for i in train} & {i.split("_")[0] for i in val}
print(f"per-image split: {len(shared)} of 6 cameras appear in both train and val")

# RIGHT: shuffle whole cameras, then expand them back into images
groups = defaultdict(list)
for image in images:
    groups[image.split("_")[0]].append(image)
cameras = random.sample(sorted(groups), len(groups))
train = [i for cam in cameras[:5] for i in groups[cam]]
val = [i for cam in cameras[5:] for i in groups[cam]]
print(f"per-camera split: train {len(train)} images, val {len(val)} images, val cameras {cameras[5:]}")
per-image split: 6 of 6 cameras appear in both train and val
per-camera split: train 750 images, val 150 images, val cameras ['cam05']

按图像划分时,验证集从未包含新摄像头拍摄的图像。按摄像头划分时,分数估计的是模型在从未见过的摄像头上的表现——这才是你需要的数值。

常见问题#

对相关数据随机打乱

症状:验证结果远好于首次实地测试。解决方法:找出分组键——它通常藏在文件名或采集元数据中——按它划分数据,并运行近重复项检查进行确认。

验证集中缺少某个类别

症状:某个稀有类别没有 AP,或者 AP 只基于两个实例。解决方法:按组划分后,统计每个划分中的各类别数量,并交换完整分组,直到每个类别的占比都相近。sklearn.model_selection.StratifiedGroupKFold 可以自动完成这项工作。

在测试集上调参

症状:测试集数值在多次实验中不断上升。解决方法:冻结测试集,选择符合生产环境条件的数据,并在每个实验完成后只报告一次。对于小型数据集,请使用 K 折交叉验证对多个划分取平均,而不是重复使用同一个划分。

正确划分数据集介绍了比例设置,以及如何在完整数据集中落实所有这些做法。

试试看

在你的数据集(或 medical-pills)上运行近似重复检查。记录验证集中有多少张图像在训练集中存在近似图像。然后确定分组键,按分组键重新划分,并在新文件列表上运行解决方案中的泄漏检查。最终结果应包括重新划分前后的近似图像对数量,以及输出空列表的泄漏检查结果。

完成条件
满足以下所有条件,即表示你已完成本课程。
  • 你已统计出当前划分中的近似图像对数量。

  • 你已确定分组键,并按分组键重新划分。

  • 针对新划分运行的泄漏检查输出了空列表。

显示解答

只要提供一个提取分组键的函数,就能对任意文件名列表运行泄漏检查:

def leaked_groups(splits, key):
    """Return every group key that appears in more than one split."""
    owner, leaks = {}, set()
    for split, files in splits.items():
        for name in files:
            group = key(name)
            if owner.setdefault(group, split) != split:
                leaks.add(group)
    return sorted(leaks)

# 将这些列表替换为你的文件名,例如 [p.name for p in Path("images/train").glob("*.jpg")]
splits = {
    "train": ["cam01_day1_0001.jpg", "cam01_day1_0002.jpg", "cam02_day1_0001.jpg"],
    "val": ["cam03_day2_0001.jpg", "cam01_day1_0003.jpg"],
    "test": ["cam04_day3_0001.jpg"],
}
print(leaked_groups(splits, key=lambda name: name.split("_")[0]))  # 按摄像头分组
['cam01']

cam01 同时出现在 train 和 val 中,因此这个划分存在泄漏。将每张 cam01 图像移到同一个划分中,然后重新运行,直到输出 []。对于 medical-pills,来自同一视频的帧会反复拍到相同视角,文件名中没有合适的分组键:你可以录制不同药片包装的新验证视频,也可以根据近似重复指纹对图像进行聚类,并将整个簇分配到同一个划分中。

接下来做什么

完成诚实的数据划分后,接下来你将阅读这些划分生成的指标。

登录以保存学习进度