Personalizzazione del trainer#
La pipeline di training di Ultralytics si basa su BaseTrainer e su trainer specifici per task, come DetectionTrainer. Queste classi gestiscono automaticamente il ciclo di training, la validazione, la creazione di checkpoint e la registrazione dei log. Quando hai bisogno di un maggiore controllo — per monitorare metriche personalizzate, modificare i pesi delle loss o implementare pianificazioni del learning rate — puoi creare una sottoclasse del trainer e sovrascrivere 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 delle classi
- Salvare il modello migliore in base a una metrica diversa
- Congelare il backbone per le prime N epoche, quindi scongelarlo
- Specificare i tassi di apprendimento per ogni layer
- Sincronizzare BatchNorm tra GPU per l'addestramento multi-GPU
- Configurare il clipping del gradiente per regolare la stabilità
Prima di leggere questa guida, assicurati di conoscere le nozioni di base 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(). In questo modo puoi passare la 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."""
# Aggiungi qui le tue personalizzazioni
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 sovrascrivere solo i metodi specifici che vuoi personalizzare.
Registrare metriche personalizzate#
La fase di validazione calcola precisione, richiamo e mAP. Se ti servono metriche aggiuntive, come il punteggio F1 per classe, sovrascrivi 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)Dopo ogni esecuzione della validazione, questa operazione registra il punteggio F1 medio di tutte le classi rappresentate nella validazione e un dettaglio per classe.
Il validatore fornisce accesso a molte metriche tramite self.validator.metrics.box:
| Attributo | Descrizione |
|---|---|
f1 | Punteggio F1 per classe |
image_metrics | Dizionario delle metriche per immagine con precisione, richiamo, F1, TP, FP e FN |
p | Precisione per classe |
r | Richiamo per classe |
ap50 | AP a IoU 0.5 per classe |
ap | AP a IoU 0.5:0.95 per classe |
mp, mr | Precisione e richiamo medi |
map50, map | Metriche mAP medie |
Aggiungere pesi alle classi#
Imposta cls_pw tra 0.0 e 1.0 per applicare alla loss di classificazione pesi normalizzati inversamente proporzionali alla frequenza. Sovrascrivi il calcolo dei pesi esistente solo se 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 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() normalizza questi valori a una media di 1.0 e li salva nel modello, dove vengono applicati dalla loss di rilevamento esistente. Gli indici sopra richiedono un dataset con almeno due classi.
Salvare il modello migliore in base a una metrica personalizzata#
Il trainer salva best.pt in base al fitness, che per il rilevamento corrisponde per impostazione predefinita a mAP@0.5:0.95 (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 richiamo), sovrascrivi validate() e restituisci la metrica scelta come valore di fitness. Il metodo integrato save_model() la userà 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 acquisiscine il valore precedente prima di chiamarlo.
Tra le metriche comuni disponibili in self.metrics dopo la validazione ci sono:
| Chiave | Descrizione |
|---|---|
metrics/precision(B) | Precisione |
metrics/recall(B) | Richiamo |
metrics/mAP50(B) | mAP a IoU 0.5 |
metrics/mAP50-95(B) | mAP a IoU 0.5:0.95 |
Congelare e scongelare il backbone#
I flussi di lavoro di transfer learning traggono spesso vantaggio dal congelamento del backbone preaddestrato per le prime N epoche, consentendo alla testa di rilevamento di adattarsi prima di eseguire il fine-tuning dell'intera rete. Ultralytics offre un parametro freeze per congelare i layer all'inizio dell'addestramento; 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 gran 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 al termine del periodo di congelamento, mantenendo congelati in modo permanente i parametri DFL e quelli dell'insegnante di distillazione.
freeze=10congela i primi 10 layer, indici 0-9 (gran 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 testa più tempo per adattarsi prima che il backbone cambi
Tassi di apprendimento per layer#
Parti diverse della rete possono trarre vantaggio da tassi di apprendimento differenti. Una strategia comune consiste nell'usare un tasso di apprendimento inferiore per il backbone preaddestrato, così da preservare le caratteristiche apprese, consentendo al contempo alla testa di rilevamento di adattarsi più rapidamente con un tasso 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 la stessa sovrascrittura con RTDETRTrainer come classe genitore e carica il checkpoint con RTDETR("rtdetr-l.pt").
BatchNorm sincronizzata per l'addestramento multi-GPU#
Quando ti addestri su più GPU con DistributedDataParallel, i layer BatchNorm2d predefiniti calcolano le statistiche separatamente su ogni GPU. Nel fine-tuning di RT-DETR e in altre configurazioni che usano batch di piccole dimensioni per GPU, le statistiche dei batch per GPU possono essere rumorose. SyncBatchNorm di PyTorch sincronizza la media e la varianza tra tutti i rank per ottenere una singola statistica globale del batch, migliorando spesso la convergenza al costo di un lieve sovraccarico 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ù appropriato a questo scopo è set_model_attributes(), che BaseTrainer chiama esattamente in questa fase:
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 con una sola GPU; in questo caso la conversione viene saltata e l'addestramento prosegue con il normale BatchNorm2d. Lo stesso schema funziona per YOLO sostituendo la classe genitore con DetectionTrainer.
| Scenario | Consiglio |
|---|---|
| Addestramento multi-GPU, batch piccolo per GPU (≤ 16) | Attiva |
| Addestramento multi-GPU, batch grande per GPU (≥ 32) | Facoltativo; vantaggio modesto |
| Addestramento con una sola GPU | Non applicabile (ignorato) |
Clipping del gradiente configurabile#
Il trainer predefinito limita i gradienti a max_norm=10.0 in optimizer_step(), un valore permissivo calibrato per i modelli YOLO, i cui gradienti raramente lo superano. I rilevatori 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 bruscamente. Per sostituire il valore di clipping, crea una sottoclasse del trainer e sovrascrivi 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 massima del gradiente; imposta a 0 per disabilitare il 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 rimane invariato.
| Famiglia di architetture | Valore tipico di max_norm |
|---|---|
| Famiglia RT-DETR / DEIM / DETR | 0.1 |
| YOLO (impostazione predefinita di Ultralytics) | 10.0 |
| Disabilita il clipping | 0 |
Domande frequenti#
Passa la classe del tuo trainer personalizzato (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'istanza del trainer. Per maggiori dettagli sull'architettura del trainer, consulta la pagina Personalizzazione avanzata.Metodi principali disponibili per la personalizzazione:
Metodo Scopo validate()Eseguire la validazione e restituire le metriche build_optimizer()Creare l'ottimizzatore save_model()Salvare i checkpoint di addestramento get_model()Restituire l'istanza del modello get_validator()Restituire l'istanza del validatore 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 spesso sono sufficienti le callback. Gli eventi di callback disponibili includono
on_train_start,on_train_epoch_start,on_train_epoch_end,on_fit_epoch_endeon_model_save. Ti consentono di agganciarti al ciclo di addestramento senza creare una sottoclasse. L'esempio precedente sul congelamento del backbone illustra questo approccio.Se la modifica è più semplice (ad esempio, regolare i pesi 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)In YOLO26,
dflscalal1_lossregistrato, perché la sua testa di rilevamento usareg_max: 1; nei modelli conreg_max > 1, scaladfl_loss.