Divisiones que reflejan la realidad
Detecta imágenes casi duplicadas que se filtren entre entrenamiento y validación, y divide los datos según la unidad que será nueva durante el despliegue.
Si un modelo obtiene un 95 % en validación y un 60 % en producción, normalmente es porque la división engañaba. Imágenes casi idénticas —fotogramas de vídeo contiguos, la misma cámara el mismo día— acabaron en ambos conjuntos, así que la validación puso a prueba la memoria en lugar de la capacidad de generalización. Encontrarás exactamente esa filtración en un conjunto de datos real y después la corregirás con una división por grupos.
Práctico
Tres divisiones, tres funciones#
| División | Quién aprende de ella | Cuándo la consultas |
|---|---|---|
| Entrenamiento | El modelo: aquí se ajustan los pesos | Cada época |
| Validación | Tú: hiperparámetros, épocas, tamaño del modelo, umbrales | Después de cada experimento |
| Prueba | Nadie: informa del número final | Una vez, después de tomar todas las decisiones |
Cada decisión que tomas al observar una partición filtra un poco de información al modelo. Para eso sirve el conjunto de validación. Si ajustas el modelo con el conjunto de prueba, su cifra deja de ser una estimación honesta: habrás sobreajustado tus propias decisiones. En Ultralytics, model.val(split="test") evalúa la partición de prueba indicada en el YAML de tu conjunto de datos.
Encuentra la filtración en un conjunto de datos real#
Las imágenes de medical-pills se llaman Frame_0.jpg, Frame_4.jpg, Frame_8.jpg: son fotogramas de un vídeo. Es una señal de alerta: los fotogramas contiguos son casi idénticos, y una partición aleatoria los coloca a ambos lados. Compruébalo comparando pequeñas huellas en escala de grises de cada imagen de validación con cada imagen de entrenamiento:
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 diferencia de 0,3 en una escala de 0–255 significa que las dos imágenes son prácticamente la misma. Abre val/Frame_24.jpg y train/Frame_308.jpg una al lado de la otra y verás los mismos blísteres desde el mismo ángulo. 17 de las 23 imágenes de validación tienen una gemela en el conjunto de entrenamiento, así que la puntuación de validación mide sobre todo lo bien que el modelo recuerda imágenes que ya ha visto. Está bien para una demostración rápida con un conjunto de datos, pero es precisamente lo que no debes llevar a producción.

La misma trampa existe en cualquier herramienta. La propia descripción de este cuadro de diálogo indica que reasigna aleatoriamente todas las imágenes, así que es ideal si las imágenes son fotos independientes, pero provoca filtraciones si son fotogramas de vídeo. Ejecuta primero una comprobación de duplicados casi idénticos y mantén la proporción de prueba por encima del 0 % si quieres obtener una cifra final que nadie haya usado para ajustar el modelo.
Divide por grupos#
La regla: las particiones deben ser independientes respecto a la unidad que será nueva durante el despliegue.
| Datos | Dividir por |
|---|---|
| Vídeo | Clip o sesión de grabación |
| Flota de cámaras fijas | Cámara o ubicación |
| Escenas que cambian con el tiempo | Día o semana |
| Datos médicos, deportivos o de personas | Paciente, deportista o sujeto |
| Ráfagas de fotos | Evento de captura |
Los nombres de archivo suelen contener la clave del grupo. Este ejemplo crea 900 imágenes a partir de 6 cámaras y compara las dos estrategias:
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 la partición por imagen, la validación nunca incluye una cámara nueva. Con la partición por cámara, la puntuación estima qué ocurre con una cámara que el modelo nunca ha visto: justo la cifra que necesitas.
Errores habituales#
Síntoma: la validación da resultados mucho mejores que la primera prueba en campo. Solución: encuentra la clave de grupo —a menudo oculta en los nombres de archivo o en los metadatos de captura—, divide los datos según ella y ejecuta la comprobación de casi duplicados para confirmarlo.
Síntoma: una clase poco frecuente sin AP o con AP calculada a partir de dos instancias. Solución: después de dividir por grupos, cuenta cada clase en cada partición e intercambia grupos completos hasta que todas las clases aparezcan en proporciones similares. sklearn.model_selection.StratifiedGroupKFold lo automatiza.
Síntoma: la puntuación de prueba sube poco a poco entre experimentos. Solución: congela el conjunto de prueba, extráelo de condiciones similares a las de producción e informa de sus resultados una vez por experimento finalizado. En conjuntos de datos pequeños, calcula la media de varias particiones mediante validación cruzada k-fold en lugar de reutilizar una sola.
Divide correctamente el conjunto de datos explica las proporciones y cómo aplicar todo esto a un conjunto de datos completo.
Mostrar solución
Una comprobación de filtraciones que funciona con cualquier lista de nombres de archivo, si se le proporciona una función que extraiga la clave del grupo:
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)
# Sustituye estas listas por tus nombres de archivo, p. ej., [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])) # agrupa por cámara['cam01']cam01 aparece tanto en entrenamiento como en validación, así que esta partición tiene filtraciones. Mueve todas las imágenes cam01 a una misma partición y vuelve a ejecutarlo hasta que imprima []. En medical-pills, donde los fotogramas de un mismo vídeo vuelven a mostrar la misma vista, los nombres de archivo no contienen una clave de grupo clara: graba vídeos de validación nuevos con blísteres distintos o agrupa las imágenes por su huella de duplicados casi idénticos y asigna cada grupo completo a una sola partición.