YOLO Vision 2026:

Personalizzazione del Trainer#

La pipeline di addestramento di Ultralytics è basata su BaseTrainer e su trainer specifici per attività come DetectionTrainer. Queste classi gestiscono automaticamente il ciclo di addestramento, la validazione, il salvataggio dei checkpoint e la registrazione dei log. Quando hai bisogno di un maggiore controllo — per monitorare metriche personalizzate, modificare i pesi della loss o implementare scheduler del learning rate — puoi creare una sottoclasse del trainer ed eseguire l'override di metodi specifici.

Questa guida illustra sette personalizzazioni comuni:

  1. Registrazione di metriche personalizzate (punteggio F1) al termine di ogni epoca
  2. Aggiunta di pesi alle classi per gestire lo sbilanciamento tra le classi
  3. Salvataggio del modello migliore in base a una metrica diversa
  4. Congelamento del backbone per le prime N epoche, quindi scongelamento
  5. Specificazione dei learning rate per layer
  6. Sincronizzazione di BatchNorm tra le GPU per l'addestramento multi-GPU
  7. Configurazione del gradient clipping per la regolazione della stabilità
Prerequisiti

Prima di leggere questa guida, assicurati di conoscere le basi dell'addestramento dei modelli YOLO e la pagina Personalizzazione avanzata, che illustra l'architettura BaseTrainer.

Come funzionano i trainer personalizzati#

La classe del modello YOLO accetta un parametro trainer nel metodo train(). Questo ti permette di passare una tua classe trainer che estende il comportamento predefinito:

from ultralytics import YOLO
from ultralytics.models.yolo.detect import DetectionTrainer

class CustomTrainer(DetectionTrainer):
    """A custom trainer that extends DetectionTrainer with additional functionality."""

    # Add your customizations here

model = YOLO("yolo26n.pt")
model.train(data="coco8.yaml", epochs=10, trainer=CustomTrainer)

Il tuo trainer personalizzato eredita tutte le funzionalità da DetectionTrainer, quindi devi eseguire l'override solo dei metodi specifici che vuoi personalizzare.

Registrazione di metriche personalizzate#

Il passaggio di validazione calcola precision, recall e mAP. Se ti servono metriche aggiuntive come il punteggio F1 per classe, esegui l'override di 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)

In questo modo vengono registrati il punteggio F1 medio tra tutte le classi rappresentate nella validazione e un dettaglio per classe dopo ogni esecuzione della validazione.

Metriche disponibili

Il validator fornisce accesso a numerose metriche tramite self.validator.metrics.box:

AttributoDescrizione
f1Punteggio F1 per classe
image_metricsDizionario delle metriche per immagine con precision, recall, F1, TP, FP e FN
pPrecision per classe
rRecall per classe
ap50AP a IoU 0.5 per classe
apAP a IoU 0.5:0.95 per classe
mp, mrPrecision e recall medie
map50, mapMetriche mAP medie

Aggiunta dei pesi alle classi#

Imposta cls_pw tra 0.0 e 1.0 per applicare pesi normalizzati inversamente proporzionali alla frequenza nella loss di classificazione. Esegui l'override del calcolo dei pesi esistente solo quando ti servono rapporti scelti 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 for the production loss owner."""
        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() normalizza questi valori a una media di 1.0 e li memorizza nel modello, dove la loss di rilevamento esistente li applica. Gli indici sopra richiedono un dataset con almeno due classi.

Salvataggio del modello migliore in base a una metrica personalizzata#

Il trainer salva best.pt in base alla fitness, che per il rilevamento è mAP@0.5:0.95 per impostazione predefinita (i pesi [0.0, 0.0, 0.0, 1.0] per [P, R, mAP@0.5, mAP@0.5:0.95]). Per usare una metrica diversa (come mAP@0.5 o il recall), esegui l'override di validate() e restituisci la metrica scelta come valore di fitness. Il save_model() integrato la utilizzerà quindi automaticamente:

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() aggiorna best_fitness usando la metrica predefinita, quindi salva il valore precedente prima di chiamarlo.

Metriche disponibili

Le metriche comuni disponibili in self.metrics dopo la validazione includono:

ChiaveDescrizione
metrics/precision(B)Precision
metrics/recall(B)Recall
metrics/mAP50(B)mAP a IoU 0.5
metrics/mAP50-95(B)mAP a IoU 0.5:0.95

Congelamento e scongelamento del backbone#

I workflow di transfer learning traggono spesso vantaggio dal congelamento del backbone preaddestrato per le prime N epoche, consentendo alla detection head di adattarsi prima di eseguire il fine-tuning dell'intera rete. Ultralytics fornisce un parametro freeze per congelare i layer all'inizio dell'addestramento e puoi usare una callback per scongelarli dopo N epoche:

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)

Il parametro freeze=10 congela i primi 10 layer (indici 0-9) all'inizio dell'addestramento, coprendo la maggior parte del backbone di YOLO26. Il backbone comprende i layer 0-10, quindi freeze=10 lascia addestrabile il blocco C2PSA finale (layer 10); usa freeze=11 per congelare l'intero backbone. La callback on_train_epoch_start viene eseguita all'inizio di ogni epoca e scongela i layer richiesti una volta terminato il periodo di congelamento, mantenendo congelati in modo permanente i parametri DFL e del teacher di distillazione.

Scelta degli elementi da congelare
  • freeze=10 congela i primi 10 layer, indici 0-9 (la maggior parte del backbone di YOLO26; usa freeze=11 per includere il blocco C2PSA finale al layer 10)
  • freeze=[0, 1, 2, 3] congela layer specifici in base all'indice
  • Valori più alti di FREEZE_EPOCHS danno alla head più tempo per adattarsi prima che il backbone cambi

Learning rate per layer#

Parti diverse della rete possono trarre vantaggio da learning rate differenti. Una strategia comune consiste nell'usare un learning rate più basso per il backbone preaddestrato, così da preservare le feature apprese, consentendo al contempo alla detection head di adattarsi più rapidamente con un rate più alto:

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 RT-DETR#

Per RT-DETR, usa lo stesso override con RTDETRTrainer come classe genitore e carica il checkpoint con RTDETR("rtdetr-l.pt").

BatchNorm sincronizzato per l'addestramento multi-GPU#

Quando esegui l'addestramento su più GPU con DistributedDataParallel, i layer BatchNorm2d predefiniti calcolano le statistiche in modo indipendente su ciascuna GPU. Per il fine-tuning di RT-DETR e altre ricette che usano batch di piccole dimensioni per GPU, le statistiche del batch per GPU possono essere rumorose. SyncBatchNorm di PyTorch sincronizza media e varianza tra tutti i rank per ottenere un'unica statistica globale del batch, migliorando spesso la convergenza al costo di un lieve overhead di comunicazione tra GPU.

La conversione deve avvenire dopo che il modello è stato spostato sulla GPU ma prima che DDP lo avvolga. L'hook più pulito per farlo è set_model_attributes(), che BaseTrainer chiama esattamente in quella finestra:

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 condizione world_size > 1 garantisce che il trainer sia sicuro da usare anche nelle esecuzioni su una singola GPU; su una singola GPU la conversione viene ignorata e l'addestramento procede con il normale BatchNorm2d. Lo stesso schema funziona per YOLO sostituendo la classe genitore con DetectionTrainer.

Quando usare SyncBatchNorm
ScenarioRaccomandazione
Addestramento multi-GPU, batch ridotto per GPU (≤ 16)Abilita
Addestramento multi-GPU, batch ampio per GPU (≥ 32)Facoltativo; vantaggio minimo
Addestramento su una singola GPUNon applicabile (ignorato)

Gradient clipping configurabile#

Il trainer predefinito limita i gradienti a max_norm=10.0 in optimizer_step(), un valore permissivo ottimizzato per i modelli YOLO, nei quali i gradienti lo superano raramente. I detector della famiglia DETR (RT-DETR, DEIM, DINO) usano in genere valori molto più restrittivi, come 0.1, per stabilizzare i layer di cross-attention del decoder, dove l'ampiezza dei gradienti può aumentare improvvisamente. Per eseguire l'override del valore di clipping, crea una sottoclasse del trainer ed esegui l'override di 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  # max gradient norm; set to 0 to disable clipping

    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)

Lo stesso trainer funziona per YOLO sostituendo la classe genitore con DetectionTrainer (from ultralytics.models.yolo.detect import DetectionTrainer) e caricando un checkpoint YOLO con YOLO("yolo26n.pt"). Il corpo di optimizer_step non cambia.

Valori tipici di `clip_grad_norm`
Famiglia di architetturemax_norm tipico
Famiglia RT-DETR / DEIM / DETR0.1
YOLO (predefinito Ultralytics)10.0
Disabilita il clipping0

FAQ#

  • Passa la tua classe trainer personalizzata (non un'istanza) al parametro trainer in 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 classe YOLO gestisce internamente l'istanziazione del trainer. Consulta la pagina Personalizzazione avanzata per maggiori dettagli sull'architettura del trainer.

  • Metodi principali disponibili per la personalizzazione:

    MetodoScopo
    validate()Eseguire la validazione e restituire le metriche
    build_optimizer()Costruire l'optimizer
    save_model()Salvare i checkpoint dell'addestramento
    get_model()Restituire l'istanza del modello
    get_validator()Restituire l'istanza del validator
    get_dataloader()Creare il dataloader
    preprocess_batch()Preelaborare il batch di input
    label_loss_items()Formattare gli elementi della loss per la registrazione

    Per il riferimento completo dell'API, consulta la documentazione di BaseTrainer.

  • Sì, per personalizzazioni più semplici, le callback sono spesso sufficienti. Gli eventi callback disponibili includono on_train_start, on_train_epoch_start, on_train_epoch_end, on_fit_epoch_end e on_model_save. Questi ti permettono di inserirti nel ciclo di addestramento senza creare una sottoclasse. L'esempio sul congelamento del backbone riportato sopra illustra questo approccio.

  • Se la modifica è più semplice (ad esempio, la regolazione dei gain della loss), puoi modificare direttamente gli iperparametri:

    from ultralytics import YOLO
    
    model = YOLO("yolo26n.pt")
    model.train(data="coco8.yaml", box=10.0, cls=1.5, dfl=2.0)

    Su YOLO26, dfl scala la l1_loss registrata perché la sua detection head usa reg_max: 1; sui modelli con reg_max > 1 scala dfl_loss.

Commenti