Link to this sectionPersonalizzazione del trainer#
La pipeline di training di Ultralytics è costruita attorno a BaseTrainer e a trainer specifici per attività come DetectionTrainer. Queste classi gestiscono il ciclo di addestramento, la validazione, il checkpoint e il logging in modo nativo. Quando hai bisogno di maggiore controllo — per monitorare metriche personalizzate, regolare la ponderazione della loss o implementare scheduler del learning rate — puoi creare una sottoclasse del trainer e sovrascrivere metodi specifici.
Questa guida illustra sette personalizzazioni comuni:
- Logging di metriche personalizzate (F1 score) alla fine di ogni epoca
- Aggiunta di pesi alle classi per gestire lo sbilanciamento delle classi
- Salvataggio del modello migliore in base a una metrica differente
- Freeze del backbone per le prime N epoche, quindi unfreezing
- Specificazione di learning rate per strato
- Sincronizzazione di BatchNorm tra GPU per training multi-GPU
- Configurazione del gradient clipping per la regolazione della stabilità
Prima di leggere questa guida, assicurati di avere familiarità con le basi dell'addestramento dei modelli YOLO e con la pagina Personalizzazione avanzata, che copre l'architettura di BaseTrainer.
Link to this sectionCome funzionano i trainer personalizzati#
La classe del modello YOLO accetta un parametro trainer nel metodo train(). Questo ti permette di 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."""
pass # 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 solo sovrascrivere i metodi specifici che desideri personalizzare.
Link to this sectionLogging di metriche personalizzate#
Il passaggio di validazione calcola precision, recall e mAP. Se hai bisogno di metriche aggiuntive come lo F1 score 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
valid_f1 = f1_per_class[f1_per_class > 0]
mean_f1 = np.mean(valid_f1) if len(valid_f1) > 0 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) if f1_per_class[j] > 0
]
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)Questo registra lo F1 score medio su tutte le classi e un dettaglio per classe dopo ogni esecuzione di validazione.
Il validatore fornisce accesso a molte metriche tramite self.validator.metrics.box:
| Attributo | Descrizione |
|---|---|
f1 | F1 score 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 | Mean precision e recall |
map50, map | Metriche di mean AP |
Link to this sectionAggiunta di pesi alle classi#
Se il tuo dataset ha classi sbilanciate (ad esempio, un difetto raro nell'ispezione di produzione), puoi aumentare il peso delle classi sottorappresentate nella funzione di loss. Ciò fa sì che il modello penalizzi più pesantemente le classificazioni errate sulle classi rare.
Per personalizzare la loss, crea una sottoclasse delle classi di loss, del modello e del trainer:
import torch
from torch import nn
from ultralytics import YOLO
from ultralytics.models.yolo.detect import DetectionTrainer
from ultralytics.nn.tasks import DetectionModel
from ultralytics.utils import RANK
from ultralytics.utils.loss import E2ELoss, v8DetectionLoss
class WeightedDetectionLoss(v8DetectionLoss):
"""Detection loss with class weights applied to BCE classification loss."""
def __init__(self, model, class_weights=None, tal_topk=10, tal_topk2=None):
"""Initialize loss with optional per-class weights for BCE."""
super().__init__(model, tal_topk=tal_topk, tal_topk2=tal_topk2)
if class_weights is not None:
self.bce = nn.BCEWithLogitsLoss(
pos_weight=class_weights.to(self.device),
reduction="none",
)
class WeightedE2ELoss(E2ELoss):
"""E2E Loss with class weights for YOLO26."""
def __init__(self, model, class_weights=None):
"""Initialize E2E loss with weighted detection loss."""
def weighted_loss_fn(model, tal_topk=10, tal_topk2=None):
return WeightedDetectionLoss(model, class_weights=class_weights, tal_topk=tal_topk, tal_topk2=tal_topk2)
super().__init__(model, loss_fn=weighted_loss_fn)
class WeightedDetectionModel(DetectionModel):
"""Detection model that uses class-weighted loss."""
def init_criterion(self):
"""Initialize weighted loss criterion with per-class weights."""
class_weights = torch.ones(self.nc)
class_weights[0] = 2.0 # upweight class 0
class_weights[1] = 3.0 # upweight rare class 1
return WeightedE2ELoss(self, class_weights=class_weights)
class WeightedTrainer(DetectionTrainer):
"""Trainer that returns a WeightedDetectionModel."""
def get_model(self, cfg=None, weights=None, verbose=True):
"""Return a WeightedDetectionModel."""
model = WeightedDetectionModel(cfg, nc=self.data["nc"], verbose=verbose and RANK == -1)
if weights:
model.load(weights)
return model
model = YOLO("yolo26n.pt")
model.train(data="coco8.yaml", epochs=10, trainer=WeightedTrainer)Puoi calcolare automaticamente i pesi delle classi dalla distribuzione delle etichette del tuo dataset. Un approccio comune è la ponderazione per frequenza inversa:
import numpy as np
# class_counts: number of instances per class
class_counts = np.array([5000, 200, 3000])
# Inverse frequency: rarer classes get higher weight
class_weights = max(class_counts) / class_counts
# Result: [1.0, 25.0, 1.67]Link to this sectionSalvataggio del modello migliore tramite metrica personalizzata#
Il trainer salva best.pt in base alla fitness, che di default è 0.9 × mAP@0.5:0.95 + 0.1 × mAP@0.5. Per utilizzare una metrica diversa (come mAP@0.5 o recall), sovrascrivi validate() e restituisci la metrica scelta come valore di fitness. Il metodo integrato save_model() lo 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."""
metrics, fitness = super().validate()
if metrics:
fitness = metrics.get("metrics/mAP50(B)", fitness)
if self.best_fitness is None or fitness > self.best_fitness:
self.best_fitness = fitness
return metrics, fitness
model = YOLO("yolo26n.pt")
model.train(data="coco8.yaml", epochs=20, trainer=CustomSaveTrainer)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 |
Link to this sectionFreeze e unfreezing del backbone#
I workflow di transfer learning beneficiano spesso del freeze del backbone pre-addestrato per le prime N epoche, permettendo all'head di rilevamento di adattarsi prima del fine-tuning dell'intera rete. Ultralytics fornisce un parametro freeze per bloccare gli strati all'inizio dell'addestramento e puoi utilizzare un callback per sbloccarli 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 all layers after FREEZE_EPOCHS."""
if trainer.epoch == FREEZE_EPOCHS:
LOGGER.info(f"Epoch {trainer.epoch}: Unfreezing all layers for fine-tuning")
for name, param in trainer.model.named_parameters():
if not param.requires_grad:
param.requires_grad = True
LOGGER.info(f" Unfroze: {name}")
trainer.freeze_layer_names = [".dfl"]
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 blocca i primi 10 strati (il backbone) all'inizio dell'addestramento. Il callback on_train_epoch_start viene attivato all'inizio di ogni epoca e sblocca tutti i parametri una volta terminato il periodo di freeze.
freeze=10blocca i primi 10 strati (tipicamente il backbone nelle architetture YOLO)freeze=[0, 1, 2, 3]blocca strati specifici per indice- Valori più alti di
FREEZE_EPOCHSdanno all'head più tempo per adattarsi prima che il backbone cambi
Link to this sectionLearning rate per strato#
Diverse parti della rete possono beneficiare di learning rate differenti. Una strategia comune è usare un learning rate inferiore per il backbone pre-addestrato al fine di preservare le feature apprese, consentendo contemporaneamente all'head di rilevamento di adattarsi più rapidamente con un tasso superiore:
import torch
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."""
def build_optimizer(self, model, name="auto", lr=0.001, momentum=0.9, decay=1e-5, iterations=1e5):
"""Build optimizer with separate learning rates for backbone and head."""
backbone_params = []
head_params = []
for k, v in unwrap_model(model).named_parameters():
if not v.requires_grad:
continue
is_backbone = any(k.startswith(f"model.{i}.") for i in range(10))
if is_backbone:
backbone_params.append(v)
else:
head_params.append(v)
backbone_lr = lr * 0.1
optimizer = torch.optim.AdamW(
[
{"params": backbone_params, "lr": backbone_lr, "weight_decay": decay},
{"params": head_params, "lr": lr, "weight_decay": decay},
],
)
LOGGER.info(
f"PerLayerLR optimizer: backbone ({len(backbone_params)} params, lr={backbone_lr}) "
f"| head ({len(head_params)} params, lr={lr})"
)
return optimizer
model = YOLO("yolo26n.pt")
model.train(data="coco8.yaml", epochs=20, trainer=PerLayerLRTrainer)Link to this sectionVariante RT-DETR#
Per RT-DETR lo schema è lo stesso con due affinamenti. La lunghezza del backbone viene letta da model.yaml["backbone"] così lo stesso trainer funziona tra le varianti RT-DETR (RT-DETR-L, RT-DETR-X, backbone ResNet-50/101) senza dover scrivere i conteggi degli strati. I parametri sono anche suddivisi in gruppi weight, BatchNorm e bias all'interno di ogni sezione in modo che il weight decay sia escluso dai parametri BatchNorm e dai bias, rispettando la policy del trainer predefinito. Questo è particolarmente utile per il fine-tuning di RT-DETR, dove l'head del decoder è solitamente inizializzato casualmente mentre il backbone trasporta feature pre-addestrate che beneficiano di un learning rate inferiore:
import torch
from torch import nn
from ultralytics import RTDETR
from ultralytics.models.rtdetr.train import RTDETRTrainer
from ultralytics.utils import LOGGER, colorstr
from ultralytics.utils.torch_utils import unwrap_model
class RTDETRBackboneLRTrainer(RTDETRTrainer):
"""RT-DETR trainer with a lower learning rate for backbone parameters."""
backbone_lr_ratio = 0.1 # backbone learning rate as a fraction of head learning rate
def build_optimizer(self, model, name="auto", lr=0.001, momentum=0.9, decay=1e-5, iterations=1e5):
"""Build an AdamW optimizer with six param groups: head and backbone x {weight, bn, bias}."""
# Resolve optimizer name; "auto" maps to AdamW with RT-DETR-style defaults
canonical = {"Adam", "Adamax", "AdamW", "NAdam", "RAdam", "auto"}
name = {x.lower(): x for x in canonical}.get(name.lower(), name)
if name == "auto":
name, lr, momentum = "AdamW", 1e-4, 0.9
self.args.warmup_bias_lr = 0.0 # RT-DETR warms biases from 0, unlike YOLO's 0.1
if name not in {"Adam", "Adamax", "AdamW", "NAdam", "RAdam"}:
raise NotImplementedError(f"This trainer only supports AdamW-family optimizers; got {name}")
# Identify backbone parameters from model.yaml and route each param into a (section, kind) group
unwrapped = unwrap_model(model)
backbone_len = len(unwrapped.yaml["backbone"])
norm_types = tuple(v for k, v in nn.__dict__.items() if "Norm" in k)
groups = {f"{s}_{k}": [] for s in ("head", "backbone") for k in ("weight", "bn", "bias")}
for module_name, module in unwrapped.named_modules():
for param_name, param in module.named_parameters(recurse=False):
if not param.requires_grad:
continue
fullname = f"{module_name}.{param_name}" if module_name else param_name
parts = fullname.split(".")
section = (
"backbone"
if len(parts) > 1 and parts[0] == "model" and parts[1].isdigit() and int(parts[1]) < backbone_len
else "head"
)
if "bias" in param_name:
kind = "bias"
elif isinstance(module, norm_types) or "logit_scale" in fullname:
kind = "bn"
else:
kind = "weight"
groups[f"{section}_{kind}"].append(param)
# Build the optimizer with per-group lr and weight decay; backbone groups use lr * backbone_lr_ratio
backbone_lr = lr * self.backbone_lr_ratio
param_groups = [
{"params": groups["head_weight"], "lr": lr, "weight_decay": decay, "param_group": "weight"},
{"params": groups["head_bn"], "lr": lr, "weight_decay": 0.0, "param_group": "bn"},
{"params": groups["head_bias"], "lr": lr, "weight_decay": 0.0, "param_group": "bias"},
{"params": groups["backbone_weight"], "lr": backbone_lr, "weight_decay": decay, "param_group": "weight"},
{"params": groups["backbone_bn"], "lr": backbone_lr, "weight_decay": 0.0, "param_group": "bn"},
{"params": groups["backbone_bias"], "lr": backbone_lr, "weight_decay": 0.0, "param_group": "bias"},
]
param_groups = [pg for pg in param_groups if pg["params"]] # drop empty groups
optimizer = getattr(torch.optim, name)(param_groups, betas=(momentum, 0.999))
LOGGER.info(
f"{colorstr('optimizer:')} {name}(lr={lr}, backbone_lr={backbone_lr}) with parameter groups\n"
f" Head: {len(groups['head_bn'])} bn, {len(groups['head_weight'])} weight(decay={decay}), "
f"{len(groups['head_bias'])} bias (lr={lr})\n"
f" Backbone: {len(groups['backbone_bn'])} bn, {len(groups['backbone_weight'])} weight(decay={decay}), "
f"{len(groups['backbone_bias'])} bias (lr={backbone_lr})"
)
return optimizer
model = RTDETR("rtdetr-l.pt")
model.train(data="coco8.yaml", epochs=20, trainer=RTDETRBackboneLRTrainer)Un punto di partenza comune è backbone_lr_ratio = 0.1, che corrisponde alla configurazione originale di RT-DETR con il suo backbone HGNetV2. La letteratura suggerisce di scalare il rapporto inversamente con la dimensione del backbone e la scala dei dati di pre-addestramento: backbone grandi pre-addestrati su dataset molto vasti (ad esempio ViT-L/H addestrati con DINO, CLIP o MAE su centinaia di milioni di immagini) utilizzano tipicamente rapporti inferiori come 0.01 o meno per preservare feature ben apprese, mentre backbone più piccoli con pre-addestramento più leggero tollerano rapporti maggiori come 0.5 o superiori.
Lo scheduler del learning rate integrato (cosine o linear) si applica comunque sopra i learning rate di base per gruppo. Sia il learning rate del backbone che quello dell'head seguiranno lo stesso programma di decadimento, mantenendo costante il rapporto tra loro durante tutto l'addestramento.
Queste personalizzazioni possono essere combinate in un'unica classe trainer sovrascrivendo più metodi e aggiungendo callback secondo necessità.
Link to this sectionSynchronized BatchNorm per training multi-GPU#
Durante l'addestramento su più GPU con DistributedDataParallel, gli strati BatchNorm2d predefiniti calcolano le statistiche indipendentemente su ogni GPU. Per il fine-tuning di RT-DETR e altre ricette che utilizzano batch size ridotti per GPU, le statistiche batch per GPU possono essere rumorose. SyncBatchNorm di PyTorch sincronizza media e varianza su tutti i rank per una singola statistica batch globale, il che spesso migliora la convergenza a costo di un piccolo sovraccarico di comunicazione inter-GPU.
La conversione deve avvenire dopo che il modello è sulla GPU ma prima che DDP lo avvolga. L'hook più pulito per questo è 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 protezione world_size > 1 garantisce che il trainer sia sicuro da usare anche in esecuzioni su singola GPU; su una singola GPU la conversione viene saltata e l'addestramento procede con il normale BatchNorm2d. Lo stesso schema funziona per YOLO passando la classe genitore a DetectionTrainer.
| Scenario | Raccomandazione |
|---|---|
| Training multi-GPU, batch piccolo per GPU (≤ 16) | Abilita |
| Training multi-GPU, batch grande per GPU (≥ 32) | Opzionale; beneficio minimo |
| Training su singola GPU | Non applicabile (saltato) |
Link to this sectionGradient clipping configurabile#
Il trainer predefinito taglia i gradienti a max_norm=10.0 in optimizer_step(), un valore approssimativo ottimizzato per modelli YOLO dove i gradienti raramente lo superano. I rilevatori della famiglia DETR (RT-DETR, DEIM, DINO) utilizzano solitamente valori molto più ristretti come 0.1 per stabilizzare gli strati di cross-attention del decoder, dove le grandezze dei gradienti possono piccare. Per sovrascrivere il valore di clip, crea una sottoclasse del trainer e sovrascrivi optimizer_step():
import torch
from ultralytics import RTDETR
from ultralytics.models.rtdetr.train import RTDETRTrainer
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:
torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=self.clip_grad_norm)
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 passando la classe genitore a 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 (default di Ultralytics) | 10.0 |
| Disabilita il clipping | 0 |
Link to this sectionFAQ#
Link to this sectionCome passo un trainer personalizzato a YOLO?#
Passa la tua classe trainer personalizzata (non un'istanza) al parametro trainer in model.train():
from ultralytics import YOLO
model = YOLO("yolo26n.pt")
model.train(data="coco8.yaml", trainer=MyCustomTrainer)La classe YOLO gestisce l'istanziazione del trainer internamente. Consulta la pagina Personalizzazione avanzata per maggiori dettagli sull'architettura del trainer.
Link to this sectionQuali metodi di BaseTrainer posso sovrascrivere?#
Metodi chiave disponibili per la personalizzazione:
| Metodo | Scopo |
|---|---|
validate() | Esegui la validazione e restituisci le metriche |
build_optimizer() | Costruisci l'ottimizzatore |
save_model() | Salva i checkpoint di addestramento |
get_model() | Restituisci l'istanza del modello |
get_validator() | Restituisci l'istanza del validatore |
get_dataloader() | Crea il dataloader |
preprocess_batch() | Pre-elabora il batch di input |
label_loss_items() | Formatta gli elementi della loss per il logging |
Per il riferimento completo alle API, consulta la documentazione di BaseTrainer.
Link to this sectionPosso usare i callback invece di creare una sottoclasse del trainer?#
Sì, per personalizzazioni più semplici, i callback sono spesso sufficienti. Gli eventi di 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 integrarti nel ciclo di addestramento senza creare sottoclassi. L'esempio del freezing della backbone sopra riportato dimostra questo approccio.
Link to this sectionCome posso personalizzare la funzione di loss senza creare una sottoclasse del modello?#
Se la tua modifica è più semplice (come la regolazione dei guadagni della loss), puoi modificare direttamente gli iperparametri:
model.train(data="coco8.yaml", box=10.0, cls=1.5, dfl=2.0)Per modifiche strutturali alla loss (come l'aggiunta di pesi alle classi), devi creare una sottoclasse della loss e del modello come mostrato nella sezione sui pesi delle classi.