Controllo qualità del dataset
Verifica le etichette, il bilanciamento delle classi, i duplicati e la copertura degli split con uno script; chiedi a un modello di segnalare le probabili etichette mancanti e correggi i problemi prima del training.
Correggere le etichette di solito aumenta la precisione più che ottimizzare gli iperparametri. Spesso i team trascorrono settimane a regolare le impostazioni per guadagnare uno o due punti di mAP, per poi scoprire durante un pomeriggio di verifica che una parte di una classe era stata etichettata in modo errato fin dall'inizio. Il controllo qualità rileva ciò che la guida per l'annotazione non ha individuato, prima che un'esecuzione di training trasformi gli errori nelle etichette in un numero di cui nessuno dovrebbe fidarsi.
Pratico
I sette controlli#
Esegui tutti e sette i controlli prima della prima esecuzione di training e di nuovo dopo ogni lotto di annotazione importante. La guida alla preelaborazione dei dati annotati ne approfondisce alcuni.
| # | Controllo | Cosa rileva | Come trovarlo |
|---|---|---|---|
| 1 | Etichette mancanti | Oggetti ignorati da chi ha annotato | Revisione degli elementi sospetti qui sotto; campione visivo |
| 2 | Classi errate o non valide | Classi scambiate, ID non presenti nell'elenco delle classi | Script di verifica; matrice di confusione dopo una prima esecuzione |
| 3 | Riquadri troppo larghi | Riquadri con margini o bordi tagliati | Campione visivo; segnalazioni di riquadri troppo piccoli dallo script di verifica |
| 4 | Duplicati | Copie esatte e fotogrammi quasi identici | Script di verifica per le copie esatte; vista Clustering di Ultralytics Platform, colorata in base allo split, per i quasi duplicati |
| 5 | Sbilanciamento delle classi | Classi molto al di sotto degli obiettivi indicati nelle specifiche | Script di verifica; scheda Grafici della Platform |
| 6 | Copertura degli split e data leakage | Classi assenti da uno split; una clip presente in due split | Script di verifica; split raggruppati (lezione successiva) |
| 7 | Percentuale di immagini di sfondo | Troppe poche o troppe immagini vuote | Script di verifica; obiettivo: 0–10% |
Automatizza il conteggio#
I controlli 2, 4, 5, 6 e 7 riguardano conteggi, quindi automatizzali con uno script. Questo controllo legge un file YAML per i dati di rilevamento, i cui train: e val: indicano le cartelle delle immagini. Conta le istanze per classe e le immagini di sfondo in ogni split, segnala gli ID di classe non validi, i riquadri troppo piccoli, le righe delle etichette duplicate e le immagini identiche byte per byte, ed elenca le classi presenti in val ma mai in 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']L'ultima riga è il risultato del controllo. person ha 10 istanze in val e nessuna in train, quindi val valuta una classe su cui il modello non è mai stato addestrato. COCO8 è un set di 8 immagini per i test rapidi, quindi in questo caso è previsto; nel tuo dataset significa che devi correggere lo split o raccogliere altri dati prima del training. Quando ci sono problemi, vengono stampati sotto il rispettivo split come ! 000000000025.jpg: class 99 is not in names o ! 000000000009.jpg: duplicate label rows.
Chiedi a un modello di segnalare le etichette sospette#
Le etichette mancanti sono l'errore più dannoso e il più difficile da individuare scorrendo le immagini. Un modello preaddestrato può segnalarle: una rilevazione sicura senza alcuna etichetta nelle vicinanze indica un'etichetta mancante o un errore del modello, e vale la pena controllare entrambi i casi. Questo script funziona su 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.jpgSu 128 immagini, lo script riduce il controllo a tre. Aprendole si trovano entrambi i tipi di risultati:
000000000438.jpgmostra un riquadro con una dozzina di ciambelle, in cui due rilevazioni sicure non si sovrappongono ad alcuna ciambella etichettata: possibili etichette mancanti o posizionate male.000000000531.jpgsegnala una «persona» che in realtà è l'ombra del fotografo su un campo da tennis. L'etichetta è corretta e il modello ha sbagliato; è proprio per questo che una persona deve controllare ogni elemento sospetto, invece di aggiungerlo automaticamente.
Abbassa conf per ampliare la ricerca e, quando ne avrai uno, usa il tuo primo modello ottimizzato, perché conosce le tue classi.
Leggi i grafici del dataset#

Su Ultralytics Platform, la scheda Grafici del dataset visualizza la distribuzione degli split, il conteggio delle classi e le statistiche dei riquadri (documentazione dei dataset). Questa schermata mostra già un problema rilevato dal controllo qualità: l'elenco contiene due classi, ma tablet ha 0 annotazioni su 1,755. Una classe senza etichette non può essere appresa. Etichettala oppure rimuovila dall'elenco delle classi prima del training.
Correggi lo sbilanciamento delle classi nell'ordine giusto#
forklift ████████████████████████ 8000 instances
person ████████████████ 5400
pallet ██ 700 ← red flagUna differenza come questa produce un modello sicuro nelle rilevazioni dei carrelli elevatori, ma che non rileva i pallet. Tre soluzioni, in ordine:
- Raccogli e annota più dati della classe minoritaria. È sempre la soluzione migliore: punta ad almeno 1.500 immagini e 10.000 istanze per ogni classe.
- Aumenta il sovracampionamento solo nel train. Inserisci più volte nell'elenco dei file di train le immagini che contengono la classe minoritaria, così ogni epoca le vede più spesso. Non includere le copie in val e test.
- Applica pesi alla loss. L'argomento di training
cls_pw(0.0 per impostazione predefinita, fino a 1.0 per una ponderazione completa inversamente proporzionale alla frequenza) aumenta il peso della loss di classificazione per le classi rare. Ricorri a questa soluzione solo dopo i punti 1 e 2.
Rimuovere dal set di validazione immagini sfocate, affollate o ambigue fa aumentare mAP senza migliorare il modello: lo rende solo meno affidabile. Se un'immagine è stata davvero acquisita durante la produzione, tienila e correggine le etichette. Rimuovi solo i veri valori anomali (errore 9) e i duplicati esatti.
Scrivi note di QC#
Bastano poche righe per rispondere alla domanda «che cosa hai controllato?» quando, mesi dopo, te lo chiederanno un cliente o un auditor:
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: yesMostra la soluzione
Note di QC per le due esecuzioni dimostrative di questa lezione:
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 firstNota l'ultima riga. Un dataset in cui una classe compare solo in val non supera il controllo qualità, anche se ogni singola etichetta è corretta, perché la metrica di quella classe misura qualcosa che il modello non ha mai avuto la possibilità di apprendere.