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:
- Registrazione di metriche personalizzate (punteggio F1) al termine di ogni epoca
- Aggiunta di pesi alle classi per gestire lo sbilanciamento tra le classi
- Salvataggio del modello migliore in base a una metrica diversa
- Congelamento del backbone per le prime N epoche, quindi scongelamento
- Specificazione dei learning rate per layer
- Sincronizzazione di BatchNorm tra le GPU per l'addestramento multi-GPU
- Configurazione del gradient clipping per la regolazione della stabilità
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.
Il validator fornisce accesso a numerose metriche tramite self.validator.metrics.box:
| Attributo | Descrizione |
|---|---|
f1 | Punteggio F1 per classe |
image_metrics | Dizionario delle metriche per immagine con precision, recall, F1, TP, FP e FN |
p | Precision per classe |
r | Recall per classe |
ap50 | AP a IoU 0.5 per classe |
ap | AP a IoU 0.5:0.95 per classe |
mp, mr | Precision e recall medie |
map50, map | Metriche 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.
Le metriche comuni disponibili in self.metrics dopo la validazione includono:
| Chiave | Descrizione |
|---|---|
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.
freeze=10congela i primi 10 layer, indici 0-9 (la maggior parte del backbone di YOLO26; usafreeze=11per 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_EPOCHSdanno 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.
| Scenario | Raccomandazione |
|---|---|
| Addestramento multi-GPU, batch ridotto per GPU (≤ 16) | Abilita |
| Addestramento multi-GPU, batch ampio per GPU (≥ 32) | Facoltativo; vantaggio minimo |
| Addestramento su una singola GPU | Non 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.
| Famiglia di architetture | max_norm tipico |
|---|---|
| Famiglia RT-DETR / DEIM / DETR | 0.1 |
| YOLO (predefinito Ultralytics) | 10.0 |
| Disabilita il clipping | 0 |
FAQ#
Passa la tua classe trainer personalizzata (non un'istanza) al parametro
trainerinmodel.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
YOLOgestisce internamente l'istanziazione del trainer. Consulta la pagina Personalizzazione avanzata per maggiori dettagli sull'architettura del trainer.Metodi principali disponibili per la personalizzazione:
Metodo Scopo 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_endeon_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,
dflscala lal1_lossregistrata perché la sua detection head usareg_max: 1; sui modelli conreg_max > 1scaladfl_loss.