Des partitions qui disent la vérité
Repère les images presque identiques qui se retrouvent à la fois dans l’entraînement et la validation, puis répartis les données selon l’unité qui sera nouvelle en déploiement.
Si un modèle obtient 95 % en validation et 60 % en production, c’est généralement que la partition a faussé les résultats. Des images presque identiques — des images vidéo consécutives, la même caméra le même jour — se sont retrouvées dans les deux ensembles ; la validation a donc testé la mémorisation plutôt que la capacité de généralisation. Tu trouveras exactement ce type de fuite dans un vrai jeu de données, puis tu la corrigeras en répartissant les données par groupes.
Pratique
Trois partitions, trois rôles#
| Partition | Qui l’utilise pour apprendre | Quand tu l’examines |
|---|---|---|
| Entraînement | Le modèle : les poids sont ajustés ici | À chaque époque |
| Validation | Toi : hyperparamètres, nombre d’époques, taille du modèle, seuils | Après chaque expérience |
| Test | Personne : il indique le résultat final | Une seule fois, une fois toutes les décisions prises |
Chaque décision que tu prends en examinant une partition fait fuiter un peu de ses données vers le modèle. C'est le rôle de l'ensemble de validation. Si tu ajustes le modèle sur l'ensemble de test, son score cesse d'être une estimation honnête : tu as surajusté le modèle à tes propres choix. Dans Ultralytics, model.val(split="test") calcule le score de la partition de test à partir du fichier YAML de ton jeu de données.
Repérer la fuite dans un vrai jeu de données#
Les images de médicaments sont nommées Frame_0.jpg, Frame_4.jpg, Frame_8.jpg : ce sont des images extraites d’une vidéo. C’est un signal d’alerte : des images consécutives sont presque identiques, et une partition aléatoire les répartit des deux côtés. Vérifie-le en comparant de minuscules empreintes en niveaux de gris de chaque image de validation avec chaque image d’entraînement :
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.8Une différence de 0,3 sur une échelle de 0 à 255 signifie que les deux images sont pratiquement identiques. Ouvre val/Frame_24.jpg et train/Frame_308.jpg côte à côte : tu verras les mêmes plaquettes sous le même angle. 17 des 23 images de validation ont un équivalent dans l’ensemble d’entraînement ; le score de validation mesure donc surtout la capacité du modèle à se souvenir d’images qu’il a déjà vues. C’est acceptable pour un jeu de données de démonstration rapide, mais c’est précisément ce qu’il ne faut pas mettre en production.

Le même piège existe dans tous les outils. La description de cette boîte de dialogue indique qu’elle réattribue aléatoirement chaque image : elle est donc idéale lorsque les images sont des photos indépendantes, mais crée une fuite lorsqu’il s’agit d’images vidéo. Effectue d’abord une vérification des quasi-doublons et conserve une part de test supérieure à 0 % si tu veux obtenir un résultat final sur lequel personne n’a effectué d’ajustements.
Effectuer une partition par groupe#
La règle : les partitions doivent être indépendantes selon l’unité qui sera nouvelle au moment du déploiement.
| Données | Partitionner par |
|---|---|
| Vidéo | Séquence ou session d’enregistrement |
| Parc de caméras fixes | Caméra ou site |
| Scènes qui évoluent au fil du temps | Jour ou semaine |
| Données médicales, sportives ou sur des personnes | Patient, athlète ou sujet |
| Rafales de photos | Événement de capture |
Les noms de fichiers contiennent souvent la clé de groupe. Cet exemple crée 900 images à partir de 6 caméras et compare les deux stratégies :
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']Avec la partition par image, la validation ne voit jamais de nouvelle caméra. Avec la partition par caméra, le score estime ce qui se passe avec une caméra que le modèle n’a jamais vue : c’est le résultat dont tu as besoin.
Pièges#
Symptôme : la validation donne de bien meilleurs résultats que le premier test sur le terrain. Solution : trouve la clé de groupe — souvent cachée dans les noms de fichiers ou les métadonnées de capture —, répartis les données selon cette clé et lance la vérification des quasi-doublons pour confirmer.
Symptôme : une classe rare sans AP, ou un AP calculé à partir de deux instances. Solution : après la séparation par groupes, compte chaque classe dans chaque sous-ensemble et échange des groupes entiers jusqu’à ce que chaque classe apparaisse dans des proportions similaires. sklearn.model_selection.StratifiedGroupKFold automatise cette opération.
Symptôme : le score de test augmente progressivement au fil des expériences. Solution : gèle le jeu de test, constitue-le à partir de conditions proches de la production et ne le mesure qu’une fois par expérience terminée. Pour les petits jeux de données, calcule la moyenne sur plusieurs séparations avec la validation croisée k-fold plutôt que de réutiliser une seule séparation.
Partitionner correctement le jeu de données explique les ratios et comment appliquer toutes ces règles à un jeu de données complet.
Afficher la solution
Une vérification des fuites qui fonctionne avec n’importe quelle liste de noms de fichiers, à condition de lui fournir une fonction qui extrait la clé de groupe :
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)
# Remplace ces listes par tes noms de fichiers, par exemple [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])) # regrouper par caméra['cam01']cam01 apparaît à la fois dans les ensembles d’entraînement et de validation : cette partition présente donc une fuite. Déplace chaque image cam01 dans une seule partition et relance la vérification jusqu’à ce qu’elle affiche []. Pour medical-pills, où les images d’une même vidéo montrent à nouveau les mêmes vues, les noms de fichiers ne fournissent pas de clé de groupe fiable : filme de nouvelles vidéos de validation montrant d’autres plaquettes, ou regroupe les images selon leur empreinte de quasi-doublon et place chaque groupe entier dans une seule partition.