Ultralytics YOLO27:

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:

  1. Registrar métricas personalizadas (puntuación F1) al final de cada época
  2. Añadir pesos por clase para gestionar el desequilibrio entre clases
  3. Guardar el mejor modelo según una métrica distinta
  4. Congelar la red troncal durante las primeras N épocas y descongelarla después
  5. Especificar tasas de aprendizaje por capa
  6. Sincronizar BatchNorm entre GPU para entrenar con varias GPU
  7. Configurar el recorte del gradiente para ajustar la estabilidad
Requisitos previos

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.

Métricas disponibles

El validador proporciona acceso a numerosas métricas mediante self.validator.metrics.box:

AtributoDescripción
f1Puntuación F1 por clase
image_metricsDiccionario de métricas por imagen con precisión, exhaustividad, F1, TP, FP y FN
pPrecisión por clase
rExhaustividad por clase
ap50AP con IoU de 0.5 por clase
apAP con IoU de 0.5:0.95 por clase
mp, mrPrecisión y exhaustividad medias
map50, mapMé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.

Métricas disponibles

Entre las métricas habituales disponibles en self.metrics después de la validación se incluyen:

ClaveDescripció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.

Elegir qué congelar
  • freeze=10 congela las 10 primeras capas, con índices 0-9 (la mayor parte de la red troncal de YOLO26; usa freeze=11 para 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_EPOCHS dan 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.

Cuándo usar SyncBatchNorm
SituaciónRecomendació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 GPUNo 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.

Valores habituales de `clip_grad_norm`
Familia de arquitecturasValor habitual de max_norm
Familia RT-DETR / DEIM / DETR0.1
YOLO (valor predeterminado de Ultralytics)10.0
Desactivar el recorte0

Preguntas frecuentes#

  • Pasa tu clase de entrenador personalizada (no una instancia) al parámetro trainer de model.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 YOLO se 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étodoFinalidad
    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_end y on_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, dfl escala el valor registrado de l1_loss porque su cabeza de detección usa reg_max: 1; en los modelos con reg_max > 1, escala dfl_loss.

Comentarios