Suddivisioni che dicono la verità
Individua le immagini quasi duplicate che si infiltrano tra train e validation e suddividi i dati in base all'unità che sarà nuova in fase di distribuzione.
Se un modello ottiene il 95% in validation e il 60% in produzione, di solito è perché la suddivisione ha fornito un quadro ingannevole. Immagini quasi identiche — fotogrammi adiacenti di un video, ripresi dalla stessa fotocamera nello stesso giorno — sono finite da entrambe le parti, perciò la validation ha misurato la memorizzazione, non la capacità di generalizzare. Individuerai proprio questa fuga di dati in un dataset reale e poi la correggerai suddividendo per gruppi.
Pratico
Tre suddivisioni, tre funzioni#
| Suddivisione | Chi apprende da essa | Quando la esamini |
|---|---|---|
| Train | Il modello: qui vengono adattati i pesi | A ogni epoca |
| Validazione | Tu: iperparametri, epoche, dimensioni del modello, soglie | Dopo ogni esperimento |
| Test | Nessuno: riporta il numero finale | Una sola volta, dopo aver preso ogni decisione |
Ogni decisione che prendi osservando uno split ne fa trapelare una piccola parte nel modello. È questo il compito del set di validazione. Se ottimizzi sul set di test, il suo numero smette di essere una stima onesta: hai eseguito l'overfitting sulle tue stesse scelte. In Ultralytics, model.val(split="test") valuta lo split di test del tuo dataset YAML.
Trova la fuga di dati in un dataset reale#
Le immagini di medical-pills si chiamano Frame_0.jpg, Frame_4.jpg, Frame_8.jpg: sono fotogrammi di un video. È un campanello d'allarme: i fotogrammi consecutivi sono quasi identici e uno split casuale li distribuisce in entrambi i set. Verificalo confrontando le impronte in scala di grigi, di dimensioni ridotte, di ogni immagine di validazione con quelle di ogni immagine di training:
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.8Una differenza di 0.3 su una scala da 0 a 255 significa che le due immagini sono praticamente identiche. Apri val/Frame_24.jpg e train/Frame_308.jpg affiancate e vedrai le stesse confezioni blister dalla stessa angolazione. 17 delle 23 immagini di validazione hanno un gemello nel set di training, quindi il punteggio di validazione misura soprattutto quanto bene il modello ricorda immagini già viste. Va bene per un rapido dataset dimostrativo, ma è esattamente ciò che non devi distribuire.

La stessa trappola esiste in qualsiasi strumento. La descrizione della finestra di dialogo dice che riassegna casualmente ogni immagine: è quindi ideale quando le immagini sono fotografie indipendenti, ma causa fughe di dati quando sono fotogrammi video. Esegui prima un controllo dei quasi-duplicati e mantieni la quota del test sopra lo 0% se vuoi un numero finale su cui nessuno abbia ottimizzato.
Dividi per gruppi#
La regola: gli split devono essere indipendenti rispetto all'unità che sarà nuova in fase di deployment.
| Dati | Dividi per |
|---|---|
| Video | Clip o sessione di registrazione |
| Parco di telecamere fisse | Telecamera o sito |
| Scene che cambiano nel tempo | Giorno o settimana |
| Dati medici, sportivi o relativi a persone | Paziente, atleta o soggetto |
| Raffiche di foto | Evento di acquisizione |
I nomi dei file spesso contengono la chiave del gruppo. Questo esempio crea 900 immagini da 6 telecamere e confronta le due strategie:
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']Con lo split per immagine, la validazione non vede mai una telecamera nuova. Con lo split per telecamera, il punteggio stima cosa accade con una telecamera mai vista dal modello: il numero che ti serve.
Insidie#
Sintomo: la validazione è molto migliore del primo test sul campo. Soluzione: individua la chiave di raggruppamento, spesso nascosta nei nomi dei file o nei metadati di acquisizione, suddividi i dati in base a essa ed esegui il controllo dei quasi duplicati per confermare.
Sintomo: una classe rara senza AP, oppure AP calcolata su due istanze. Soluzione: dopo la suddivisione in gruppi, conta le istanze di ogni classe in ciascuna suddivisione e scambia gruppi interi finché tutte le classi non compaiono in proporzioni simili. sklearn.model_selection.StratifiedGroupKFold automatizza questa procedura.
Sintomo: il punteggio del test aumenta a poco a poco con ogni esperimento. Soluzione: congela il set di test, ricavalo da condizioni simili a quelle di produzione e riportane i risultati una volta per ogni esperimento completato. Con dataset piccoli, calcola la media su più suddivisioni usando la convalida incrociata k-fold invece di riutilizzare sempre la stessa.
Dividere correttamente il dataset tratta i rapporti e come applicare tutto questo a un dataset completo.
Mostra la soluzione
Un controllo delle fughe di dati che funziona con qualsiasi elenco di nomi di file, data una funzione che estrae la chiave del gruppo:
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)
# Sostituisci questi elenchi con i nomi dei tuoi file, ad es. [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])) # raggruppa per telecamera['cam01']cam01 compare sia in train sia in val, quindi questo split presenta una fuga di dati. Sposta tutte le immagini cam01 in un unico split e ripeti finché non viene stampato []. Per medical-pills, dove i fotogrammi di uno stesso video riprendono la stessa inquadratura, i nomi dei file non contengono una chiave di gruppo affidabile: registra nuovi video di validazione con confezioni diverse oppure raggruppa le immagini in base all'impronta dei quasi-duplicati e assegna interi gruppi a un singolo split.