Aufteilungen, die die Wahrheit zeigen
Finde nahezu identische Bilder, die zwischen Trainings- und Validierungsdaten durchsickern, und teile die Daten nach der Einheit auf, die beim Einsatz neu sein wird.
Wenn ein Modell bei der Validierung 95 % und im Produktivbetrieb 60 % erreicht, war die Aufteilung meist irreführend. Nahezu identische Bilder – benachbarte Videobilder oder Aufnahmen derselben Kamera am selben Tag – landeten auf beiden Seiten. Dadurch prüfte die Validierung das Erinnerungsvermögen statt der Generalisierung. Du findest genau dieses Datenleck in einem echten Datensatz und behebst es anschließend mit einer gruppenbasierten Aufteilung.
Praxisnah
Drei Teilmengen, drei Aufgaben#
| Teilmenge | Wer daraus lernt | Wann du sie dir ansiehst |
|---|---|---|
| Training | Das Modell: Hier werden die Gewichte angepasst | Jede Epoche |
| Validierung | Du: Hyperparameter, Epochen, Modellgröße, Schwellenwerte | Nach jedem Experiment |
| Test | Niemand: Es meldet die endgültige Zahl | Einmal, nachdem alle Entscheidungen getroffen wurden |
Jede Entscheidung, die du anhand eines Splits triffst, lässt ein wenig davon in das Modell einfließen. Genau dafür ist der Validierungssatz da. Wenn du den Testsatz zum Abstimmen verwendest, ist dessen Ergebnis keine ehrliche Schätzung mehr – du hast dich auf deine eigenen Entscheidungen überangepasst. In Ultralytics wertet model.val(split="test") den Testsplit aus deiner Dataset-YAML-Datei aus.
Finde das Datenleck in einem echten Dataset#
Die Bilder der Medikamentenblister heißen Frame_0.jpg, Frame_4.jpg, Frame_8.jpg – Einzelbilder aus einem Video. Das ist ein Warnzeichen: Benachbarte Einzelbilder sind nahezu identisch, und bei einer zufälligen Aufteilung landen sie auf beiden Seiten. Prüfe das, indem du winzige Graustufen-Fingerabdrücke jedes Validierungsbilds mit jedem Trainingsbild vergleichst:
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.8Eine Abweichung von 0.3 auf einer Skala von 0–255 bedeutet, dass die beiden Bilder praktisch dasselbe Motiv zeigen. Öffne val/Frame_24.jpg und train/Frame_308.jpg nebeneinander, dann siehst du dieselben Blisterpackungen aus derselben Perspektive. 17 der 23 Validierungsbilder haben ein Gegenstück im Trainingssatz. Der Validierungswert misst hier also größtenteils, wie gut sich das Modell an Bilder erinnert, die es bereits gesehen hat. Für ein schnelles Demo-Dataset ist das in Ordnung – aber genau so etwas darfst du nicht ausliefern.

Dieselbe Falle gibt es in jedem Tool. In der Beschreibung dieses Dialogfelds steht, dass jedes Bild zufällig neu zugewiesen wird. Das ist ideal, wenn die Bilder unabhängige Fotos sind – bei Einzelbildern aus Videos führt es jedoch zu Datenlecks. Prüfe zuerst auf nahezu identische Bilder und behalte einen Testanteil von über 0 %, wenn du ein endgültiges Ergebnis möchtest, auf das niemand abgestimmt hat.
Nach Gruppen aufteilen#
Die Regel: Die Splits müssen hinsichtlich der Einheit unabhängig sein, die beim Einsatz neu sein wird.
| Daten | Aufteilen nach |
|---|---|
| Video | Videoclip oder Aufnahmesitzung |
| Fester Kamerabestand | Kamera oder Standort |
| Szenen, die sich im Laufe der Zeit verändern | Tag oder Woche |
| Medizinische Daten, Sportdaten oder Personendaten | Patient, Sportler oder Person |
| Fotoserien | Aufnahmeereignis |
Dateinamen enthalten oft den Gruppenschlüssel. Dieses Beispiel erstellt 900 Bilder von 6 Kameras und vergleicht die beiden Strategien:
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']Bei der Aufteilung pro Bild sieht die Validierung nie eine neue Kamera. Bei der Aufteilung pro Kamera schätzt der Wert, wie das Modell mit einer Kamera abschneidet, die es noch nie gesehen hat – genau die Zahl, die du brauchst.
Stolperfallen#
Symptom: Die Validierung fällt deutlich besser aus als der erste Test im Einsatz. Lösung: Finde den Gruppenschlüssel – oft in Dateinamen oder Aufnahmemetadaten verborgen –, teile anhand dieses Schlüssels auf und prüfe die Aufteilung auf nahezu identische Bilder.
Symptom: Eine seltene Klasse ohne AP oder AP auf Grundlage von nur zwei Instanzen. Lösung: Zähle nach der Aufteilung nach Gruppen die Instanzen jeder Klasse pro Teilmenge und tausche ganze Gruppen aus, bis jede Klasse in ähnlichen Anteilen vorkommt. sklearn.model_selection.StratifiedGroupKFold automatisiert das.
Symptom: Der Testwert steigt über mehrere Experimente hinweg langsam an. Lösung: Fixiere den Testsatz, stelle ihn unter produktionsähnlichen Bedingungen zusammen und werte ihn pro abgeschlossenem Experiment nur einmal aus. Bei kleinen Datensätzen solltest du statt denselben Testsatz wiederzuverwenden den Durchschnitt über mehrere Aufteilungen mit k-facher Kreuzvalidierung bilden.
Dataset richtig aufteilen behandelt Aufteilungsverhältnisse und zeigt, wie du all das auf ein vollständiges Dataset anwendest.
Lösung anzeigen
Eine Leckprüfung, die mit jeder Dateinamensliste funktioniert, wenn eine Funktion zum Extrahieren des Gruppenschlüssels übergeben wird:
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)
# Ersetze diese Listen durch deine Dateinamen, z. B. [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])) # Nach Kamera gruppieren['cam01']cam01 kommt sowohl in train als auch in val vor, also führt dieser Split zu Datenlecks. Verschiebe alle cam01-Bilder in einen Split und führe die Prüfung erneut aus, bis [] ausgegeben wird. Bei medical-pills, wo Einzelbilder aus einem Video dieselbe Ansicht wiederholen, gibt es in den Dateinamen keinen eindeutigen Gruppenschlüssel: Nimm entweder neue Validierungsvideos mit anderen Packungen auf oder gruppiere die Bilder anhand des Fingerabdrucks für nahezu identische Bilder und weise ganze Gruppen einem Split zu.