Contrôle qualité du jeu de données
Audite les étiquettes, l’équilibre des classes, les doublons et la couverture des partitions avec un script, demande à un modèle de repérer les étiquettes probablement manquantes, puis corrige les problèmes avant l’entraînement.
Corriger les étiquettes améliore généralement davantage la précision que régler les hyperparamètres. Les équipes passent souvent des semaines à ajuster des paramètres pour gagner un ou deux points de mAP, puis découvrent en une après-midi d’audit qu’une partie d’une classe était mal étiquetée depuis le début. Le contrôle qualité repère ce que le guide d’annotation a oublié avant qu’un entraînement ne transforme les erreurs d’étiquetage en un chiffre auquel personne ne devrait se fier.
Pratique
Les sept vérifications#
Effectue les sept vérifications avant le premier entraînement, puis après chaque lot important d’annotations. Le guide sur le prétraitement des données annotées en détaille plusieurs.
| # | Vérification | Problème détecté | Méthode de détection |
|---|---|---|---|
| 1 | Étiquettes manquantes | Objets ignorés par les annotateurs | Vérification des cas suspects ci-dessous ; échantillon visuel |
| 2 | Classes incorrectes ou non valides | Classes inversées, ID absents de la liste des classes | Script d’audit ; matrice de confusion après un premier entraînement |
| 3 | Boîtes trop larges | Boîtes avec des marges ou des bords tronqués | Échantillon visuel ; signalement des petites boîtes par l’audit |
| 4 | Doublons | Copies exactes et images presque identiques | Script d’audit pour les copies exactes ; vue Clustering d’Ultralytics Platform, avec une couleur par partition, pour les quasi-doublons |
| 5 | Déséquilibre des classes | Classes bien en dessous des objectifs définis dans les spécifications | Script d’audit ; onglet Charts de Platform |
| 6 | Couverture des partitions et fuite de données | Classes absentes d’une partition ; même séquence vidéo dans deux partitions | Script d’audit ; partitions regroupées (leçon suivante) |
| 7 | Proportion d’images d’arrière-plan | Trop ou pas assez d’images vides | Script d’audit ; objectif : 0–10 % |
Automatise le comptage#
Les vérifications 2, 4, 5, 6 et 7 portent sur le comptage : automatise-les avec un script. Cet audit lit un fichier YAML de données de détection dont train: et val: pointent vers les dossiers d’images. Il compte les instances par classe et les images d’arrière-plan dans chaque partition, signale les ID de classe non valides, les petites boîtes, les lignes d’étiquettes en double et les images identiques octet par octet, puis liste les classes présentes dans val mais absentes de 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']La dernière ligne indique le problème. person a 10 instances dans val et aucune dans train : val évalue donc une classe sur laquelle le modèle n’a jamais été entraîné. COCO8 est un jeu de données de test rapide de 8 images, c’est donc normal dans ce cas ; dans ton jeu de données, cela signifie qu’il faut corriger la partition ou collecter davantage de données avant l’entraînement. En présence de problèmes, ils s’affichent sous leur partition sous la forme ! 000000000025.jpg: class 99 is not in names ou ! 000000000009.jpg: duplicate label rows.
Demande à un modèle de repérer les étiquettes suspectes#
Les étiquettes manquantes sont l’erreur la plus dommageable et la plus difficile à repérer en faisant défiler les images. Un modèle préentraîné peut les signaler : une détection fiable sans étiquette à proximité correspond soit à une étiquette manquante, soit à une erreur du modèle, et mérite dans les deux cas d’être examinée. Voici le script exécuté sur 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.jpgSur 128 images, le script réduit la vérification à trois images. Leur ouverture révèle les deux types de constat :
000000000438.jpgmontre une boîte contenant une douzaine de beignets, où deux détections fiables ne chevauchent aucun beignet étiqueté : des étiquettes manquantes ou mal placées sont possibles.000000000531.jpgsignale une « personne » qui est en fait l’ombre du photographe sur un court de tennis. L’étiquette est correcte et le modèle se trompe : c’est précisément pourquoi une personne doit examiner chaque cas suspect au lieu de l’ajouter automatiquement.
Diminue conf pour ratisser plus large, puis remplace-le par ton premier modèle affiné dès que tu en as un, puisqu’il connaît tes classes.
Consulte les graphiques du jeu de données#

Sur Ultralytics Platform, l’onglet Charts du jeu de données trace la distribution des partitions, le nombre d’instances par classe et les statistiques des boîtes (documentation des jeux de données). Cette capture révèle déjà un problème de contrôle qualité : la liste comprend deux classes, mais tablet compte 0 annotation sur 1 755. Une classe sans étiquette ne peut pas être apprise. Étiquette-la ou retire-la de la liste des classes avant l’entraînement.
Corrige le déséquilibre des classes dans le bon ordre#
forklift ████████████████████████ 8000 instances
person ████████████████ 5400
pallet ██ 700 ← red flagUn tel écart produit un modèle fiable sur les chariots élévateurs, mais qui ne détecte pas les palettes. Trois solutions, dans l’ordre :
- Collecte et annote davantage d’exemples de la classe minoritaire. C’est toujours la meilleure solution : vise au moins 1 500 images et 10 000 instances pour chaque classe.
- Suréchantillonne uniquement dans train. Dans la liste des fichiers d’entraînement, indique plusieurs fois les images qui contiennent la classe minoritaire afin qu’elles soient vues plus souvent à chaque époque. N’inclus pas les copies dans val et test.
- Pondère la fonction de perte. L’argument d’entraînement
cls_pw(0,0 par défaut, jusqu’à 1,0 pour une pondération inversement proportionnelle à la fréquence) augmente le poids de la perte de classification des classes rares. N’y recours qu’après les solutions 1 et 2.
Supprimer de la validation les images floues, encombrées ou difficiles à interpréter fait monter le mAP sans améliorer le modèle, seulement en le rendant moins honnête. Si une image provient réellement du déploiement, conserve-la et corrige plutôt ses étiquettes. Supprime uniquement les vrais cas aberrants (erreur 9) et les doublons exacts.
Rédige des notes de contrôle qualité#
Quelques lignes suffisent pour répondre à la question « qu’as-tu vérifié ? » lorsqu’un client ou un auditeur la pose des mois plus tard :
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: yesAfficher la solution
Notes de contrôle qualité pour les deux exécutions de démonstration de cette leçon :
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 firstNote la dernière ligne. Un jeu de données dont une classe apparaît uniquement dans val échoue au contrôle qualité même si chaque étiquette est correcte, car la métrique de cette classe mesure quelque chose que le modèle n’a jamais eu l’occasion d’apprendre.