Ultralytics YOLO27: 加入候补名单。
Get Started
构建高性能 YOLO 数据集·标注·课程 5/10
课程初级

数据集质量控制

使用脚本审核标签、类别平衡、重复项和数据集划分覆盖情况;让模型指出可能缺失的标签,并在训练前修正发现的问题。

修正标签通常比调整超参数更能提升准确率。团队常常花几周时间调整设置,只为多获得一两个百分点的 mAP,结果在一个下午的审核中发现,某个类别的一部分数据一直被错误标注。质量控制能在训练将标签错误变成一个不可信的数字之前,找出标注指南遗漏的问题。

学习成果

运行自动标签审核和模型辅助的可疑项检查,修正发现的问题,并在简短的 QC 笔记中记录结果。

快速通道
如果你已经熟悉相关内容,以下是简要版。
  1. 用脚本统计数量:每个类别的实例数、背景图像占比、无效类别、重复项,以及训练集中缺失的类别。

  2. 让预训练模型标记没有匹配标签的高置信度检测结果,再逐一目视检查。

  3. 解决类别不平衡问题的顺序是:先收集数据,再过采样,最后才考虑损失加权。

  4. 记录你检查和修正的内容。

实践操作

七项检查#

在首次训练前以及每次重要标注批次完成后,运行全部七项检查。预处理已标注数据指南深入介绍了其中几项。

#检查项可发现的问题查找方法
1标签缺失标注人员漏掉的物体下文的可疑项检查;目视抽样
2类别错误或无效类别互换、ID 超出类别列表范围审核脚本;首次训练后的混淆矩阵
3边界框过松带有多余边缘空间或边缘被裁切的边界框目视抽样;审核脚本标记的小边界框
4重复项完全相同的副本和近乎相同的帧审核脚本查找完全相同的副本;使用按数据集划分着色的 Ultralytics Platform 聚类视图查找近重复项
5类别不平衡远低于规范目标数量的类别审核脚本;Platform 的 图表选项卡
6数据集划分覆盖与泄漏某个划分中缺少类别;同一片段出现在两个划分中审核脚本;分组划分(下一课)
7背景占比空图像过少或过多审核脚本;目标为 0–10%

自动统计#

第 2、4、5、6 和 7 项都属于统计问题,因此应编写脚本自动检查。此审核脚本会读取检测数据 YAML 文件,其中的 train: 和 val: 指向图像文件夹。脚本会统计每个数据集划分中各类别的实例数和背景图像数,标记无效类别 ID、过小的边界框、重复标签行和字节完全相同的图像,并列出在 val 中出现但在 train 中从未出现的类别:

import hashlib
from collections import Counter
from pathlib import Path

from ultralytics.data.utils import IMG_FORMATS, check_det_dataset, img2label_paths

data = check_det_dataset("coco8.yaml")  # downloads COCO8 on first run; swap in your data YAML
names, seen = data["names"], {}

for split in ("train", "val"):
    images = sorted(p for p in Path(data[split]).rglob("*.*") if p.suffix[1:].lower() in IMG_FORMATS)
    instances, hashes, problems, background = Counter(), Counter(), [], 0
    for img, label in zip(images, img2label_paths([str(p) for p in images])):
        hashes[hashlib.md5(img.read_bytes()).hexdigest()] += 1
        rows = [r.split() for r in Path(label).read_text().splitlines() if r.strip()] if Path(label).exists() else []
        background += not rows
        if len(rows) != len({tuple(r) for r in rows}):
            problems.append(f"{img.name}: duplicate label rows")
        for r in rows:
            cls, w, h = int(r[0]), float(r[3]), float(r[4])
            if cls not in names:
                problems.append(f"{img.name}: class {cls} is not in names")
            elif w * h < 1e-4:
                problems.append(f"{img.name}: tiny {names[cls]} box")
            instances[cls] += 1
    seen[split] = set(instances)
    duplicates = sum(n - 1 for n in hashes.values())
    print(f"{split}: {len(images)} images, {background} background, {duplicates} duplicate images")
    for cls, n in instances.most_common():
        print(f"  {names.get(cls, cls):>12}  {n}")
    for problem in problems:
        print("  !", problem)

print("in val but never in train:", sorted(names[c] for c in seen["val"] - seen["train"] if c in names))
train: 4 images, 0 background, 0 duplicate images
        orange  4
          bowl  3
       giraffe  2
...
val: 4 images, 0 background, 0 duplicate images
        person  10
         horse  2
...
in val but never in train: ['dog', 'elephant', 'horse', 'person', 'umbrella']

最后一行就是发现的问题。person 在 val 中有 10 个实例,在 train 中却没有,因此 val 正在评估模型从未训练过的类别。COCO8 是一个包含 8 张图像的冒烟测试集,因此出现这种情况是预料之中的;如果这是你的数据集,就意味着你需要在训练前调整数据集划分或收集更多数据。有问题时,脚本会在相应的数据集划分下输出结果,例如 ! 000000000025.jpg: class 99 is not in names 或 ! 000000000009.jpg: duplicate label rows。

让模型指出可疑标签#

标签缺失是危害最大的错误,也是最难通过滚动浏览发现的问题。预训练模型可以帮你找到这些问题:如果高置信度检测结果附近完全没有标签,它要么是标签缺失,要么是模型错误;无论哪种情况都值得检查。此脚本在 COCO128 上运行:

from pathlib import Path

import torch

from ultralytics import YOLO
from ultralytics.data.utils import check_det_dataset, img2label_paths
from ultralytics.utils.metrics import box_iou
from ultralytics.utils.ops import xywh2xyxy

data = check_det_dataset("coco128.yaml")  # swap in your data YAML
model = YOLO("yolo26n.pt")  # a pretrained model, or your own first fine-tune
images = sorted(Path(data["train"]).glob("*.jpg"))

suspects = []
for result, label in zip(model(images, conf=0.6, stream=True, verbose=False), img2label_paths(map(str, images))):
    rows = [r.split() for r in Path(label).read_text().splitlines() if r.strip()] if Path(label).exists() else []
    boxes = xywh2xyxy(torch.tensor([[float(v) for v in r[1:5]] for r in rows]).reshape(-1, 4))
    iou = box_iou(result.boxes.xyxyn, boxes)  # model boxes vs labeled boxes
    for j, (cls, conf) in enumerate(zip(result.boxes.cls.int().tolist(), result.boxes.conf.tolist())):
        if not rows or iou[j].max() < 0.3:  # confident detection with no label anywhere near it
            suspects.append((conf, Path(result.path).name, model.names[cls]))

print(f"{len(suspects)} confident detections with no matching label; top 5:")
for conf, name, cls in sorted(suspects, reverse=True)[:5]:
    print(f"  {conf:.2f}  {cls:<12} {name}")
4 confident detections with no matching label; top 5:
  0.77  donut        000000000438.jpg
  0.75  person       000000000531.jpg
  0.71  donut        000000000438.jpg
  0.66  bottle       000000000436.jpg

在 128 张图像中,脚本将审核范围缩小到三张。打开图像后会发现两种情况:

  • 000000000438.jpg 是一盒十几个甜甜圈,其中两个高置信度检测结果没有与任何已标注的甜甜圈重叠:它们可能是缺失或错位的标签。
  • 000000000531.jpg 标记了一个“person”,但它其实是网球场上摄影师的影子。标签是正确的,模型错了;这正说明为什么每个可疑项都应由人工审核,而不是自动添加标签。

降低 conf 可以扩大筛查范围;如果你已经完成首次微调,则应换用自己的模型,因为它了解你的类别。

查看数据集图表#

医疗药丸数据集的 Ultralytics Platform Charts 选项卡:Split Distribution 显示 80 张训练图像和 20 张验证图像,Top Classes 显示 pill 有 1,755 个标注,tablet 有 0 个

在 Ultralytics Platform 中,数据集的 图表选项卡会绘制数据集划分分布、类别数量和边界框统计信息(数据集文档)。这张截图已经显示出一项 QC 问题:类别列表中有两个类别,但 tablet 的 1,755 个标注中没有一个属于该类别。没有标签的类别无法被模型学会。请在训练前为该类别添加标签,或从类别列表中删除它。

按正确顺序解决类别不平衡问题#

   forklift  ████████████████████████ 8000 instances
   person    ████████████████ 5400
   pallet    ██ 700                              ← red flag

这样的差距会导致模型能自信识别叉车,却漏掉托盘。按以下顺序尝试三种解决方法:

  1. 收集并标注更多少数类数据。 这始终是最佳选择:目标是每个类别至少有 1,500 张图像和 10,000 个实例。
  2. 仅在 train 中过采样。 在训练文件列表中多次列出包含少数类的图像,让每个 epoch 更频繁地看到这些图像。不要将副本放入 val 和 test。
  3. 为损失加权。 cls_pw 训练参数(默认值为 0.0,最高为 1.0,表示完全按类别频率倒数加权)会提高稀有类别的分类损失权重。只有在尝试了第 1 和第 2 种方法后,才考虑使用它。
绝不要为了提高分数而删除难例图像

从验证集中删除模糊、拥挤或难以判断的图像会提高 mAP,让模型看起来没有变差,却使评估变得不诚实。如果某张图像是真实的部署画面,就应保留它;修正标签即可。只删除真正的异常值(错误 9)和完全重复的图像。

记录 QC 笔记#

几行内容就够了;几个月后客户或审核员询问“你检查了什么?”时,这些记录就能给出答案:

Dataset: forklift-v1   Audit date: 2026-05-09
Images: 4,820   Labels: 18,442   Background: 380 (7.9%)
Audit script: 3 invalid class IDs fixed, 41 duplicate label rows removed
Suspect review: 62 flagged, 19 missing labels added, 43 model errors ignored
Visual sample: 120 images, 4 mislabeled (3.3%), fixed
Split check: grouped by camera-day, every class in every split
Imbalance: pallet 2.5x below person; collecting more next round
Ready to train: yes
试试看

将 qc_audit.py 和 suspect_labels.py 指向你自己的数据 YAML(或 COCO8 和 COCO128),然后运行脚本。修复所有报告的问题,打开每张可疑图像并判断是标签错误还是模型错误,然后重新运行两个脚本。交付内容:qc-notes.md,记录修复前后的统计数量。

完成条件
满足以下所有条件,即表示你已完成本课程。
  • 审核脚本报告没有无效类别、重复标签行或训练集中缺失的类别。

  • 模型辅助检查中的每个可疑项都已打开查看,并作为标签修正或模型错误得到处理。

  • QC 笔记记录了检查内容、发现的问题和已修复的内容。

显示解答

本课两次演示运行的 QC 笔记:

Dataset: COCO8 + COCO128 demo   Audit date: 2026-05-09
qc_audit.py (COCO8): 0 invalid classes, 0 duplicate rows, 0 duplicate images
  FINDING: dog, elephant, horse, person, umbrella appear in val only
  ACTION: re-split by group or collect these classes into train before training
suspect_labels.py (COCO128, conf 0.6): 4 suspects in 3 images
  000000000438.jpg: 2 donuts without matching labels -> relabel the donut box
  000000000531.jpg: "person" is the photographer's shadow -> model error, label kept
  000000000436.jpg: bottle -> open and decide
Ready to train: no, fix the val-only classes first

注意最后一行。即使每个标签都正确,只要某个类别仅出现在 val 中,数据集就未通过 QC,因为该类别的指标衡量的是模型从未有机会学会的内容。

接下来做什么

标签清理完成后,下一课将划分数据集,让验证分数反映实际部署表现,而不是模型对训练数据的记忆。

登录以保存学习进度