数据集质量控制
使用脚本审核标签、类别平衡、重复项和数据集划分覆盖情况;让模型指出可能缺失的标签,并在训练前修正发现的问题。
修正标签通常比调整超参数更能提升准确率。团队常常花几周时间调整设置,只为多获得一两个百分点的 mAP,结果在一个下午的审核中发现,某个类别的一部分数据一直被错误标注。质量控制能在训练将标签错误变成一个不可信的数字之前,找出标注指南遗漏的问题。
实践操作
七项检查#
在首次训练前以及每次重要标注批次完成后,运行全部七项检查。预处理已标注数据指南深入介绍了其中几项。
| # | 检查项 | 可发现的问题 | 查找方法 |
|---|---|---|---|
| 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 中,数据集的 图表选项卡会绘制数据集划分分布、类别数量和边界框统计信息(数据集文档)。这张截图已经显示出一项 QC 问题:类别列表中有两个类别,但 tablet 的 1,755 个标注中没有一个属于该类别。没有标签的类别无法被模型学会。请在训练前为该类别添加标签,或从类别列表中删除它。
按正确顺序解决类别不平衡问题#
forklift ████████████████████████ 8000 instances
person ████████████████ 5400
pallet ██ 700 ← red flag这样的差距会导致模型能自信识别叉车,却漏掉托盘。按以下顺序尝试三种解决方法:
- 收集并标注更多少数类数据。 这始终是最佳选择:目标是每个类别至少有 1,500 张图像和 10,000 个实例。
- 仅在 train 中过采样。 在训练文件列表中多次列出包含少数类的图像,让每个 epoch 更频繁地看到这些图像。不要将副本放入 val 和 test。
- 为损失加权。
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 笔记:
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,因为该类别的指标衡量的是模型从未有机会学会的内容。