Personalizzazione del Trainer#
La pipeline di addestramento di Ultralytics è basata su BaseTrainer e su trainer specifici per attività come DetectionTrainer. Queste classi gestiscono autonomamente il ciclo di addestramento, la convalida, il salvataggio dei checkpoint e la registrazione. Quando ti serve maggiore controllo — tracciare metriche personalizzate, regolare la ponderazione delle perdite o implementare la pianificazione del tasso di apprendimento — puoi creare una sottoclasse del trainer e sovrascrivere metodi specifici.
Questa guida illustra sette personalizzazioni comuni:
- Registrazione di metriche personalizzate (punteggio F1) alla fine di ogni epoca
- Aggiunta di pesi di classe per gestire lo sbilanciamento delle classi
- Salvataggio del modello migliore in base a una metrica differente
- Congelamento del backbone per le prime N epoche, seguito dallo sblocco
- Specifica di tassi di apprendimento per livello
- Sincronizzazione di BatchNorm tra GPU per l'addestramento multi-GPU
- Configurazione del clipping dei gradienti per la regolazione della stabilità
Prima di leggere questa guida, assicurati di avere familiarità con le nozioni di base sull'addestramento di modelli YOLO e con la pagina di Personalizzazione avanzata, che tratta l'architettura BaseTrainer.
Come funzionano i trainer personalizzati#
La classe del modello YOLO accetta un parametro trainer nel metodo train(). Ciò ti consente di passare la tua classe trainer personalizzata 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 solo sovrascrivere i metodi specifici che desideri personalizzare.
Registrazione di metriche personalizzate#
Il passaggio di convalida 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
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 la media dell'F1 score su tutte le classi e un dettaglio per singola classe dopo ogni esecuzione della 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 | Precision e recall medie |
map50, map | Metriche mAP medie |
Aggiunta di pesi alle classi#
Se il tuo dataset presenta classi sbilanciate (ad esempio, un difetto raro nell'ispezione di produzione), puoi aumentare il peso delle classi sottorappresentate nella funzione di perdita. In questo modo il modello penalizza 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 i pesi delle classi automaticamente 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]Le classi personalizzate come WeightedDetectionModel vengono memorizzate nel checkpoint per riferimento. Quando definite in uno script di addestramento appartengono al modulo __main__, quindi il caricamento di best.pt da uno script differente genera AttributeError: Can't get attribute 'WeightedDetectionModel' on <module '__main__'>.
Definisci le classi personalizzate in un modulo dedicato in modo che rimangano importabili e assicurati che tale modulo si trovi sul tuo PYTHONPATH al momento del caricamento.
# weighted_model.py
from ultralytics.nn.tasks import DetectionModel
class WeightedDetectionModel(DetectionModel):
"""Detection model that uses class-weighted loss."""# inference script
from weighted_model import WeightedDetectionModel # noqa: F401 - must be importable at checkpoint load time
from ultralytics import YOLO
model = YOLO("runs/detect/train/weights/best.pt")
metrics = model.val()Salvataggio del modello migliore tramite metrica personalizzata#
Il trainer salva best.pt in base all'idoneità, che per il rilevamento è impostata per predefinizione su 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 utilizzare una metrica differente (come mAP@0.5 o il richiamo), sovrascrivi validate() e restituisci la metrica scelta come valore di idoneità. L'oggetto save_model() incorporato 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."""
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 convalida 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 |
Freezing e unfreezing della backbone#
I flussi di lavoro di apprendimento per trasferimento traggono spesso vantaggio dal congelamento del backbone preaddestrato per le prime N epoche, consentendo alla testa di rilevamento di adattarsi prima della messa a punto dell'intera rete. Ultralytics fornisce un parametro freeze per congelare i livelli 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 congela i primi 10 livelli (indici 0-9) all'inizio dell'addestramento, coprendo la maggior parte del backbone YOLO26. Il backbone si estende dai livelli 0 al 10, quindi freeze=10 lascia l'ultimo blocco C2PSA (livello 10) addestrabile; usa freeze=11 per congelare l'intero backbone. Il callback on_train_epoch_start si attiva all'inizio di ogni epoca e scongela tutti i parametri una volta completato il periodo di congelamento.
freeze=10congela i primi 10 livelli, indici 0-9 (la maggior parte del backbone YOLO26; usafreeze=11per includere l'ultimo blocco C2PSA al livello 10)freeze=[0, 1, 2, 3]congela livelli specifici per indice- Valori più alti di
FREEZE_EPOCHSdanno alla testa più tempo per adattarsi prima che il backbone cambi
Learning rate per livello#
Parti differenti 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 per preservare le caratteristiche apprese, consentendo al contempo alla testa 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 = []
unwrapped = unwrap_model(model)
backbone_len = len(unwrapped.yaml["backbone"]) # YOLO26 backbone spans layers 0-10 (C2PSA at layer 10)
for k, v in unwrapped.named_parameters():
if not v.requires_grad:
continue
is_backbone = any(k.startswith(f"model.{i}.") for i in range(backbone_len))
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)Variante RT-DETR#
Per RT-DETR lo schema è lo stesso con due perfezionamenti. La lunghezza del backbone viene letta da model.yaml["backbone"], così lo stesso trainer funziona su tutte le varianti di RT-DETR (backbone RT-DETR-L, RT-DETR-X, ResNet-50/101) senza dover inserire conteggi di livelli fissi. I parametri vengono inoltre suddivisi in gruppi di pesi, BatchNorm e bias all'interno di ciascuna sezione, in modo che il decadimento dei pesi sia escluso dai parametri BatchNorm e dai bias, rispettando la politica del trainer predefinito. Ciò è particolarmente utile per la messa a punto di RT-DETR, in cui la testa del decoder è solitamente inizializzata casualmente mentre il backbone trasporta caratteristiche preaddestrate che beneficiano di un tasso di apprendimento 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 rispetto alle dimensioni del backbone e alla scala dei dati di preaddestramento: backbone grandi preaddestrati su dataset molto ampi (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 inferiori per preservare le caratteristiche ben apprese, mentre backbone più piccoli con preaddestramento più leggero tollerano rapporti maggiori come 0.5 o superiori.
Il pianificatore del tasso di apprendimento incorporato (cosine o linear) si applica comunque sopra i tassi di apprendimento di base per gruppo. Sia i tassi di apprendimento del backbone che quelli della testa seguiranno lo stesso programma di decadimento, mantenendo il rapporto tra loro per tutto l'addestramento.
Queste personalizzazioni possono essere combinate in un'unica classe trainer sovrascrivendo più metodi e aggiungendo callback secondo necessità.
Synchronized BatchNorm per l'addestramento multi-GPU#
Quando si effettua l'addestramento su più GPU con DistributedDataParallel, i livelli predefiniti di BatchNorm2d calcolano le statistiche in modo indipendente su ciascuna GPU. Per la messa a punto di RT-DETR e altre ricette che utilizzano batch di dimensioni ridotte per GPU, le statistiche dei batch per GPU possono risultare rumorose. Il comando SyncBatchNorm di PyTorch sincronizza media e varianza tra tutti i ranghi per una statistica batch globale singola, il che spesso migliora la convergenza a scapito di un piccolo sovraccarico di comunicazione inter-GPU.
La conversione deve avvenire dopo che il modello si trova sulla GPU ma prima che DDP lo avvolga. Il gancio 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 GPU singola; 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 |
|---|---|
| Addestramento multi-GPU, batch ridotto per GPU (≤ 16) | Abilita |
| Addestramento multi-GPU, batch grande per GPU (≥ 32) | Opzionale; beneficio minimo |
| Addestramento su singola GPU | Non applicabile (saltato) |
Gradient clipping configurabile#
Il trainer predefinito ritaglia i gradienti a max_norm=10.0 in optimizer_step(), un valore flessibile ottimizzato per i modelli YOLO in cui 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 i livelli di attenzione incrociata del decoder, dove le grandezze dei gradienti possono subire picchi. Per sovrascrivere il valore di ritaglio, 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 | Tipico max_norm |
|---|---|
| Famiglia RT-DETR / DEIM / DETR | 0.1 |
| YOLO (Predefinito Ultralytics) | 10.0 |
| Disabilita clipping | 0 |
FAQ#
Passa la tua classe trainer personalizzata (non un'istanza) al parametro
trainerinmodel.train():from ultralytics import YOLO model = YOLO("yolo26n.pt") model.train(data="coco8.yaml", trainer=MyCustomTrainer)La classe
YOLOgestisce internamente l'istanziazione del trainer. Consulta la pagina di Personalizzazione avanzata per maggiori dettagli sull'architettura del trainer.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 dell'addestramento get_model()Restituisci l'istanza del modello get_validator()Restituisci l'istanza del validatore get_dataloader()Costruisci 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 dell'API, consulta la documentazione di
BaseTrainer.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_endeon_model_save. Questi ti consentono di collegarti al ciclo di addestramento senza creare sottoclassi. L'esempio di congelamento del backbone sopra illustra questo approccio.Se la tua modifica è più semplice (come la regolazione dei guadagni di perdita), puoi modificare direttamente gli iperparametri:
model.train(data="coco8.yaml", box=10.0, cls=1.5, dfl=2.0)Per modifiche strutturali alla perdita (come l'aggiunta di pesi di classe), devi creare una sottoclasse della perdita e del modello come mostrato nella sezione sui pesi di classe.