Personalizar el entrenador#
El flujo de entrenamiento de Ultralytics se basa en BaseTrainer y en entrenadores específicos para cada tarea, como DetectionTrainer. Estas clases se encargan de forma predeterminada del bucle de entrenamiento, la validación, los puntos de control y el registro. Si necesitas más control —por ejemplo, para hacer un seguimiento de métricas personalizadas, ajustar la ponderación de la pérdida o implementar programas de tasa de aprendizaje—, puedes crear una subclase del entrenador y sobrescribir métodos específicos.
Esta guía explica siete personalizaciones habituales:
- Registrar métricas personalizadas (puntuación F1) al final de cada época
- Añadir pesos por clase para gestionar el desequilibrio entre clases
- Guardar el mejor modelo según una métrica distinta
- Congelar la red troncal durante las primeras N épocas y descongelarla después
- Especificar tasas de aprendizaje por capa
- Sincronizar BatchNorm entre GPU para entrenar con varias GPU
- Configurar el recorte del gradiente para ajustar la estabilidad
Antes de leer esta guía, asegúrate de conocer los fundamentos del entrenamiento de modelos YOLO y de la página Personalización avanzada, que explica la arquitectura BaseTrainer.
Cómo funcionan los entrenadores personalizados#
La clase de modelo YOLO acepta un parámetro trainer en el método train(). Esto te permite pasar tu propia clase de entrenador, que amplía el comportamiento predeterminado:
from ultralytics import YOLO
from ultralytics.models.yolo.detect import DetectionTrainer
class CustomTrainer(DetectionTrainer):
"""A custom trainer that extends DetectionTrainer with additional functionality."""
# Añade aquí tus personalizaciones
model = YOLO("yolo26n.pt")
model.train(data="coco8.yaml", epochs=10, trainer=CustomTrainer)Tu entrenador personalizado hereda todas las funciones de DetectionTrainer, así que solo tienes que sobrescribir los métodos que quieras personalizar.
Registrar métricas personalizadas#
El paso de validación calcula la precisión, la exhaustividad y el mAP. Si necesitas métricas adicionales, como la puntuación F1 por clase, sobrescribe validate():
import numpy as np
from ultralytics import YOLO
from ultralytics.models.yolo.detect import DetectionTrainer
from ultralytics.utils import LOGGER
class MetricsTrainer(DetectionTrainer):
"""Custom trainer that computes and logs F1 score at the end of each epoch."""
def validate(self):
"""Run validation and compute per-class F1 scores."""
metrics, fitness = super().validate()
if metrics is None:
return metrics, fitness
if hasattr(self.validator, "metrics") and hasattr(self.validator.metrics, "box"):
box = self.validator.metrics.box
f1_per_class = box.f1
class_indices = box.ap_class_index
names = self.validator.names
mean_f1 = float(np.mean(f1_per_class)) if len(f1_per_class) else 0.0
LOGGER.info(f"Mean F1 Score: {mean_f1:.4f}")
per_class_str = [f"{names[i]}: {f1_per_class[j]:.3f}" for j, i in enumerate(class_indices)]
LOGGER.info(f"Per-class F1: {per_class_str}")
return metrics, fitness
model = YOLO("yolo26n.pt")
model.train(data="coco8.yaml", epochs=5, trainer=MetricsTrainer)Esto registra la puntuación F1 media de todas las clases representadas en la validación y un desglose por clase después de cada ejecución de validación.
El validador proporciona acceso a numerosas métricas mediante self.validator.metrics.box:
| Atributo | Descripción |
|---|---|
f1 | Puntuación F1 por clase |
image_metrics | Diccionario de métricas por imagen con precisión, exhaustividad, F1, TP, FP y FN |
p | Precisión por clase |
r | Exhaustividad por clase |
ap50 | AP con IoU de 0.5 por clase |
ap | AP con IoU de 0.5:0.95 por clase |
mp, mr | Precisión y exhaustividad medias |
map50, map | Métricas mAP medias |
Añadir pesos por clase#
Establece cls_pw entre 0.0 y 1.0 para aplicar pesos normalizados de frecuencia inversa a la pérdida de clasificación. Sobrescribe el cálculo de pesos existente solo si necesitas proporciones definidas manualmente:
import numpy as np
from ultralytics import YOLO
from ultralytics.models.yolo.detect import DetectionTrainer
class WeightedTrainer(DetectionTrainer):
"""Detection trainer with hand-picked class-weight ratios."""
def compute_class_weights(self, class_counts):
"""Return custom per-class weights that the detection loss applies."""
weights = np.ones_like(class_counts)
weights[0] = 2.0
weights[1] = 3.0
return weights
model = YOLO("yolo26n.pt")
model.train(data="custom.yaml", epochs=10, cls_pw=1.0, trainer=WeightedTrainer)set_class_weights() normaliza estos valores para que su media sea 1.0 y los guarda en el modelo, donde la pérdida de detección existente los aplica. Los índices anteriores requieren un conjunto de datos con al menos dos clases.
Guardar el mejor modelo según una métrica personalizada#
El entrenador guarda best.pt según el valor de aptitud, que para la detección es mAP@0.5:0.95 de forma predeterminada (pondera [0.0, 0.0, 0.0, 1.0] para [P, R, mAP@0.5, mAP@0.5:0.95]). Para usar otra métrica (como mAP@0.5 o la exhaustividad), sobrescribe validate() y devuelve la métrica elegida como valor de aptitud. El método integrado save_model() la usará automáticamente:
from ultralytics import YOLO
from ultralytics.models.yolo.detect import DetectionTrainer
class CustomSaveTrainer(DetectionTrainer):
"""Trainer that saves the best model based on mAP@0.5 instead of default fitness."""
def validate(self):
"""Override fitness to use mAP@0.5 for best model selection."""
previous_best = self.best_fitness
metrics, fitness = super().validate()
if metrics is None:
return metrics, fitness
fitness = metrics["metrics/mAP50(B)"]
self.best_fitness = fitness if previous_best is None else max(previous_best, fitness)
return metrics, fitness
model = YOLO("yolo26n.pt")
model.train(data="coco8.yaml", epochs=20, trainer=CustomSaveTrainer)BaseTrainer.validate() actualiza best_fitness usando la métrica predeterminada, así que captura su valor anterior antes de llamarlo.
Entre las métricas habituales disponibles en self.metrics después de la validación se incluyen:
| Clave | Descripción |
|---|---|
metrics/precision(B) | Precisión |
metrics/recall(B) | Exhaustividad |
metrics/mAP50(B) | mAP con IoU de 0.5 |
metrics/mAP50-95(B) | mAP con IoU de 0.5:0.95 |
Congelar y descongelar la red troncal#
Los flujos de trabajo de aprendizaje por transferencia suelen beneficiarse de congelar la red troncal preentrenada durante las primeras N épocas, para que la cabeza de detección pueda adaptarse antes de ajustar toda la red. Ultralytics proporciona el parámetro freeze para congelar capas al inicio del entrenamiento; también puedes usar una función de devolución de llamada para descongelarlas después de N épocas:
from ultralytics import YOLO
from ultralytics.models.yolo.detect import DetectionTrainer
from ultralytics.utils import LOGGER
FREEZE_EPOCHS = 5
def unfreeze_backbone(trainer):
"""Callback to unfreeze the user-requested layers after FREEZE_EPOCHS."""
if trainer.epoch == FREEZE_EPOCHS:
user_freeze = [x for x in trainer.freeze_layer_names if x not in {".dfl", "teacher_model."}]
LOGGER.info(f"Epoch {trainer.epoch}: Unfreezing requested layers for fine-tuning")
for name, param in trainer.model.named_parameters():
if (
not param.requires_grad
and ".dfl" not in name
and "teacher_model." not in name
and any(x in name for x in user_freeze)
):
param.requires_grad = True
LOGGER.info(f" Unfroze: {name}")
trainer.freeze_layer_names = [x for x in trainer.freeze_layer_names if x not in user_freeze]
class FreezingTrainer(DetectionTrainer):
"""Trainer with backbone freezing for first N epochs."""
def __init__(self, *args, **kwargs):
"""Initialize and register the unfreeze callback."""
super().__init__(*args, **kwargs)
self.add_callback("on_train_epoch_start", unfreeze_backbone)
model = YOLO("yolo26n.pt")
model.train(data="coco8.yaml", epochs=20, freeze=10, trainer=FreezingTrainer)El parámetro freeze=10 congela las 10 primeras capas (índices 0-9) al inicio del entrenamiento, que abarcan la mayor parte de la red troncal de YOLO26. La red troncal comprende las capas 0-10, por lo que freeze=10 deja entrenable el bloque C2PSA final (capa 10); usa freeze=11 para congelar toda la red troncal. La función de devolución de llamada on_train_epoch_start se ejecuta al principio de cada época y descongela las capas indicadas cuando termina el periodo de congelación, a la vez que mantiene congelados permanentemente los parámetros DFL y del profesor de destilación.
freeze=10congela las 10 primeras capas, con índices 0-9 (la mayor parte de la red troncal de YOLO26; usafreeze=11para incluir el bloque C2PSA final, en la capa 10)freeze=[0, 1, 2, 3]congela capas concretas según su índice- Los valores más altos de
FREEZE_EPOCHSdan a la cabeza más tiempo para adaptarse antes de que cambie la red troncal
Tasas de aprendizaje por capa#
Las distintas partes de la red pueden beneficiarse de diferentes tasas de aprendizaje. Una estrategia habitual consiste en usar una tasa de aprendizaje más baja para la red troncal preentrenada, a fin de conservar las características aprendidas, y una tasa más alta para que la cabeza de detección se adapte con mayor rapidez:
from ultralytics import YOLO
from ultralytics.models.yolo.detect import DetectionTrainer
from ultralytics.utils import LOGGER
from ultralytics.utils.torch_utils import unwrap_model
class PerLayerLRTrainer(DetectionTrainer):
"""Trainer with different learning rates for backbone and head."""
backbone_lr_ratio = 0.1
def build_optimizer(self, model, name="auto", lr=0.001, momentum=0.9, decay=1e-5, iterations=1e5):
"""Reuse the trainer optimizer and lower its backbone parameter-group rates."""
optimizer = super().build_optimizer(model, name, lr, momentum, decay, iterations)
unwrapped = unwrap_model(model)
backbone_len = len(unwrapped.yaml["backbone"])
backbone = {
id(p)
for name, p in unwrapped.named_parameters()
if any(name.startswith(f"model.{i}.") for i in range(backbone_len))
}
groups = []
for group in optimizer.param_groups:
head_params = [p for p in group["params"] if id(p) not in backbone]
backbone_params = [p for p in group["params"] if id(p) in backbone]
if head_params:
groups.append({**group, "params": head_params})
if backbone_params:
groups.append({**group, "params": backbone_params, "lr": group["lr"] * self.backbone_lr_ratio})
optimizer.param_groups = groups
LOGGER.info(f"PerLayerLR: {len(backbone)} backbone params at {self.backbone_lr_ratio}x the head rate")
return optimizer
model = YOLO("yolo26n.pt")
model.train(data="coco8.yaml", epochs=20, trainer=PerLayerLRTrainer)Variante de RT-DETR#
Para RT-DETR, usa la misma sobrescritura con RTDETRTrainer como clase base y carga el punto de control con RTDETR("rtdetr-l.pt").
BatchNorm sincronizado para entrenar con varias GPU#
Al entrenar con varias GPU mediante DistributedDataParallel, las capas predeterminadas BatchNorm2d calculan las estadísticas de forma independiente en cada GPU. En el ajuste fino de RT-DETR y otras recetas que usan tamaños de lote pequeños por GPU, las estadísticas de cada GPU pueden ser ruidosas. SyncBatchNorm de PyTorch sincroniza la media y la varianza entre todos los rangos para obtener una única estadística global del lote, lo que suele mejorar la convergencia a cambio de una pequeña sobrecarga de comunicación entre GPU.
La conversión debe realizarse después de colocar el modelo en la GPU, pero antes de que DDP lo envuelva. El punto de enganche más sencillo es set_model_attributes(), que BaseTrainer llama precisamente en ese momento:
from torch import nn
from ultralytics import RTDETR
from ultralytics.models.rtdetr.train import RTDETRTrainer
class SyncBNTrainer(RTDETRTrainer):
"""RT-DETR trainer that converts BatchNorm to SyncBatchNorm for multi-GPU training."""
def set_model_attributes(self):
"""Run the parent setup, then convert BN to SyncBatchNorm when training on multiple GPUs."""
super().set_model_attributes()
if self.world_size > 1:
self.model = nn.SyncBatchNorm.convert_sync_batchnorm(self.model)
model = RTDETR("rtdetr-l.pt")
model.train(data="coco8.yaml", epochs=20, device=[0, 1], trainer=SyncBNTrainer)La comprobación world_size > 1 garantiza que el entrenador también se pueda usar con seguridad en ejecuciones con una sola GPU; en ese caso, se omite la conversión y el entrenamiento continúa con BatchNorm2d normal. El mismo patrón funciona con YOLO si cambias la clase base por DetectionTrainer.
| Situación | Recomendación |
|---|---|
| Entrenamiento con varias GPU y lote pequeño por GPU (≤ 16) | Activar |
| Entrenamiento con varias GPU y lote grande por GPU (≥ 32) | Opcional; beneficio menor |
| Entrenamiento con una sola GPU | No aplicable (se omite) |
Recorte configurable del gradiente#
El entrenador predeterminado recorta los gradientes a max_norm=10.0 en optimizer_step(), un valor holgado ajustado para los modelos YOLO, cuyos gradientes rara vez lo superan. Los detectores de la familia DETR (RT-DETR, DEIM, DINO) suelen usar valores mucho más estrictos, como 0.1, para estabilizar las capas de atención cruzada del decodificador, donde la magnitud del gradiente puede dispararse. Para cambiar el valor de recorte, crea una subclase del entrenador y sobrescribe optimizer_step():
import torch
from ultralytics import RTDETR
from ultralytics.models.rtdetr.train import RTDETRTrainer
from ultralytics.utils.torch_utils import TORCH_2_0
class CustomClipTrainer(RTDETRTrainer):
"""RT-DETR trainer with configurable gradient clipping."""
clip_grad_norm = 0.1 # norma máxima del gradiente; establece el valor en 0 para desactivar el recorte
def optimizer_step(self):
"""Run an optimizer step with a configurable gradient-norm clip."""
self.scaler.unscale_(self.optimizer)
if self.clip_grad_norm > 0:
kwargs = {"foreach": False} if self.device.type == "npu" and TORCH_2_0 else {}
torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=self.clip_grad_norm, **kwargs)
self.scaler.step(self.optimizer)
self.scaler.update()
self.optimizer.zero_grad()
if self.ema:
self.ema.update(self.model)
model = RTDETR("rtdetr-l.pt")
model.train(data="coco8.yaml", epochs=20, trainer=CustomClipTrainer)El mismo entrenador funciona con YOLO si cambias la clase base por DetectionTrainer (from ultralytics.models.yolo.detect import DetectionTrainer) y cargas un punto de control de YOLO con YOLO("yolo26n.pt"). El cuerpo de optimizer_step no cambia.
| Familia de arquitecturas | Valor habitual de max_norm |
|---|---|
| Familia RT-DETR / DEIM / DETR | 0.1 |
| YOLO (valor predeterminado de Ultralytics) | 10.0 |
| Desactivar el recorte | 0 |
Preguntas frecuentes#
Pasa tu clase de entrenador personalizada (no una instancia) al parámetro
trainerdemodel.train():from ultralytics import YOLO from ultralytics.models.yolo.detect import DetectionTrainer class MyCustomTrainer(DetectionTrainer): """A custom trainer that extends DetectionTrainer.""" model = YOLO("yolo26n.pt") model.train(data="coco8.yaml", trainer=MyCustomTrainer)La clase
YOLOse encarga internamente de crear una instancia del entrenador. Consulta la página Personalización avanzada para obtener más información sobre la arquitectura del entrenador.Métodos clave disponibles para la personalización:
Método Finalidad validate()Ejecutar la validación y devolver las métricas build_optimizer()Crear el optimizador save_model()Guardar los puntos de control del entrenamiento get_model()Devolver la instancia del modelo get_validator()Devolver la instancia del validador get_dataloader()Crear el cargador de datos preprocess_batch()Preprocesar el lote de entrada label_loss_items()Formatear los elementos de pérdida para el registro Para consultar la referencia completa de la API, consulta la documentación de
BaseTrainer.Sí. Para personalizaciones más sencillas, las funciones de devolución de llamada suelen ser suficientes. Entre los eventos de devolución de llamada disponibles se incluyen
on_train_start,on_train_epoch_start,on_train_epoch_end,on_fit_epoch_endyon_model_save. Te permiten añadir código al bucle de entrenamiento sin crear una subclase. El ejemplo anterior sobre cómo congelar la red troncal muestra este método.Si el cambio es sencillo (por ejemplo, ajustar los factores de pérdida), puedes modificar directamente los hiperparámetros:
from ultralytics import YOLO model = YOLO("yolo26n.pt") model.train(data="coco8.yaml", box=10.0, cls=1.5, dfl=2.0)En YOLO26,
dflescala el valor registrado del1_lossporque su cabeza de detección usareg_max: 1; en los modelos conreg_max > 1, escaladfl_loss.