Control de calidad del conjunto de datos
Audita las etiquetas, el equilibrio de clases, los duplicados y la cobertura de las particiones con un script; deja que un modelo señale las posibles etiquetas que faltan y corrige lo que encuentres antes de entrenar.
Corregir las etiquetas suele mejorar la precisión más que ajustar los hiperparámetros. Los equipos suelen dedicar semanas a configurar opciones para ganar uno o dos puntos de mAP y luego descubren, en una tarde de auditoría, que llevaban tiempo etiquetando mal parte de una clase. El control de calidad detecta lo que la guía de etiquetado pasó por alto antes de que una ejecución de entrenamiento convierta errores de etiquetado en una cifra en la que nadie debería confiar.
Práctico
Las siete comprobaciones#
Realiza las siete comprobaciones antes de la primera ejecución de entrenamiento y de nuevo después de cada lote importante de etiquetado. La guía sobre preprocesamiento de datos anotados explica varias de ellas en profundidad.
| # | Comprobación | Qué detecta | Cómo encontrarlo |
|---|---|---|---|
| 1 | Etiquetas ausentes | Objetos que los anotadores han pasado por alto | Revisión de casos sospechosos más abajo; muestra visual |
| 2 | Clases incorrectas o no válidas | Clases intercambiadas, ID fuera de la lista de clases | Script de auditoría; matriz de confusión tras una primera ejecución |
| 3 | Cuadros demasiado holgados | Cuadros con márgenes o bordes recortados | Muestra visual; avisos de cuadros diminutos de la auditoría |
| 4 | Duplicados | Copias exactas y fotogramas casi idénticos | Script de auditoría para las copias exactas; vista Clustering de Ultralytics Platform, coloreada por partición, para los casi duplicados |
| 5 | Desequilibrio de clases | Clases muy por debajo de los objetivos de la especificación | Script de auditoría; pestaña Charts de Platform |
| 6 | Cobertura de las particiones y fuga de datos | Clases ausentes de una partición; un mismo clip en dos particiones | Script de auditoría; particiones agrupadas (siguiente lección) |
| 7 | Proporción de fondos | Demasiadas o muy pocas imágenes vacías | Script de auditoría; objetivo: 0–10 % |
Automatiza el recuento#
Las comprobaciones 2, 4, 5, 6 y 7 son recuentos, así que automatízalas con un script. Esta auditoría lee un YAML de datos de detección cuyos valores train: y val: apuntan a carpetas de imágenes. Cuenta las instancias por clase y las imágenes de fondo de cada partición, señala los ID de clase no válidos, los cuadros diminutos, las filas de etiquetas duplicadas y las imágenes idénticas byte a byte, y enumera las clases que aparecen en val pero nunca en 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']La última línea es el hallazgo. person tiene 10 instancias en val y ninguna en train, así que val evalúa una clase con la que el modelo nunca se ha entrenado. COCO8 es un conjunto de prueba rápida de 8 imágenes, por lo que allí es algo esperable; en tu conjunto de datos significa que debes corregir la partición o recopilar más datos antes de entrenar. Si hay problemas, se muestran debajo de su partición como ! 000000000025.jpg: class 99 is not in names o ! 000000000009.jpg: duplicate label rows.
Deja que un modelo señale las etiquetas sospechosas#
Las etiquetas ausentes son el error más perjudicial y el más difícil de detectar al revisar las imágenes una por una. Un modelo preentrenado puede señalarlas: una detección de alta confianza sin ninguna etiqueta cerca puede ser una etiqueta ausente o un error del modelo, y merece la pena revisar cualquiera de los dos casos. Esto se ejecuta en 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.jpgDe 128 imágenes, el script reduce la revisión a tres. Al abrirlas, se ven ambos tipos de hallazgos:
000000000438.jpges un cuadro con una docena de donuts en el que dos detecciones de alta confianza no se superponen a ningún donut etiquetado: posibles etiquetas ausentes o mal colocadas.000000000531.jpgseñala una «persona» que en realidad es la sombra del fotógrafo en una pista de tenis. La etiqueta es correcta y el modelo se equivoca, justo por eso una persona debe revisar cada caso sospechoso en lugar de añadirlo automáticamente.
Reduce conf para ampliar la búsqueda y, cuando tengas un primer ajuste fino, sustitúyelo por tu propio modelo, ya que conoce tus clases.
Consulta los gráficos del conjunto de datos#

En Ultralytics Platform, la pestaña Charts del conjunto de datos representa gráficamente la distribución de las particiones, el recuento de clases y las estadísticas de los cuadros (documentación de conjuntos de datos). Esta captura ya muestra un hallazgo de control de calidad: la lista contiene dos clases, pero tablet tiene 0 de 1.755 anotaciones. No se puede aprender una clase sin etiquetas. Etiquétala o elimínala de la lista de clases antes de entrenar.
Corrige el desequilibrio de clases en el orden adecuado#
forklift ████████████████████████ 8000 instances
person ████████████████ 5400
pallet ██ 700 ← red flagUna diferencia como esta produce un modelo que detecta con confianza las carretillas elevadoras, pero no los palés. Hay tres soluciones, en este orden:
- Recopila y etiqueta más ejemplos de la clase minoritaria. Siempre es la mejor opción: intenta reunir ≥ 1.500 imágenes y ≥ 10.000 instancias de cada clase.
- Sobremuestrea solo en train. Incluye más de una vez en la lista de archivos de entrenamiento las imágenes que contienen la clase minoritaria para que cada época las vea con más frecuencia. No incluyas las copias en val ni en test.
- Pondera la función de pérdida. El argumento de entrenamiento
cls_pw(0.0 de forma predeterminada, hasta 1.0 para una ponderación inversa a la frecuencia completa) aumenta el peso de la pérdida de clasificación para las clases poco frecuentes. Recurre a esta opción solo después de aplicar las soluciones 1 y 2.
Eliminar de la validación imágenes borrosas, abarrotadas o confusas hace que suba el mAP y que el modelo no mejore, sino que sea menos honesto. Si una imagen es un fotograma real del despliegue, consérvala y corrige sus etiquetas. Elimina únicamente los valores atípicos verdaderos (error 9) y los duplicados exactos.
Escribe notas de control de calidad#
Bastan unas pocas líneas para responder a la pregunta «¿qué has comprobado?» cuando un cliente o auditor te lo pregunte meses después:
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: yesMostrar solución
Notas de control de calidad de las dos ejecuciones de demostración de esta lección:
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 firstFíjate en la última línea. Un conjunto de datos con una clase que solo aparece en val no supera el control de calidad, aunque todas las etiquetas individuales sean correctas, porque la métrica de esa clase mide algo que el modelo nunca tuvo la oportunidad de aprender.