Personnalisation du Trainer#
Le pipeline d'entraînement Ultralytics s'articule autour de BaseTrainer et de trainers spécifiques aux tâches, comme DetectionTrainer. Ces classes gèrent directement la boucle d'entraînement, la validation, la sauvegarde des points de contrôle et la journalisation. Si tu as besoin de plus de contrôle — pour suivre des métriques personnalisées, ajuster la pondération de la fonction de perte ou mettre en œuvre des calendriers de taux d'apprentissage — tu peux créer une sous-classe du trainer et remplacer certaines méthodes.
Ce guide présente sept personnalisations courantes :
- Journaliser des métriques personnalisées (score F1) à la fin de chaque époque
- Ajouter des pondérations de classe pour gérer le déséquilibre des classes
- Enregistrer le meilleur modèle selon une métrique différente
- Geler le backbone pendant les N premières époques, puis le dégeler
- Définir des taux d’apprentissage par couche
- Synchroniser BatchNorm entre les GPU pour l’entraînement multi-GPU
- Configurer le clipping des gradients pour ajuster la stabilité
Avant de lire ce guide, assure-toi de connaître les bases de l’entraînement des modèles YOLO et la page Personnalisation avancée, qui présente l’architecture BaseTrainer.
Fonctionnement des entraîneurs personnalisés#
La classe de modèle YOLO accepte un paramètre trainer dans la méthode train(). Tu peux ainsi transmettre ta propre classe d’entraîneur, qui étend le comportement par défaut :
from ultralytics import YOLO
from ultralytics.models.yolo.detect import DetectionTrainer
class CustomTrainer(DetectionTrainer):
"""A custom trainer that extends DetectionTrainer with additional functionality."""
# Ajoute tes personnalisations ici
model = YOLO("yolo26n.pt")
model.train(data="coco8.yaml", epochs=10, trainer=CustomTrainer)Ton entraîneur personnalisé hérite de toutes les fonctionnalités de DetectionTrainer ; tu n’as donc besoin de remplacer que les méthodes que tu souhaites personnaliser.
Journalisation de métriques personnalisées#
L’étape de validation calcule la précision, le rappel et la mAP. Si tu as besoin de métriques supplémentaires, comme le score F1 par classe, remplace 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)Cela journalise le score F1 moyen de toutes les classes représentées dans les données de validation, ainsi qu’une ventilation par classe après chaque exécution de la validation.
Le validateur donne accès à de nombreuses métriques via self.validator.metrics.box :
| Attribut | Description |
|---|---|
f1 | Score F1 par classe |
image_metrics | Dictionnaire de métriques par image comprenant la précision, le rappel, le F1, les TP, FP et FN |
p | Précision par classe |
r | Rappel par classe |
ap50 | AP à IoU 0.5 par classe |
ap | AP à IoU 0.5:0.95 par classe |
mp, mr | Précision et rappel moyens |
map50, map | Métriques mAP moyennes |
Ajouter des poids aux classes#
Définis cls_pw entre 0.0 et 1.0 pour appliquer des poids normalisés, inversement proportionnels à la fréquence, à la perte de classification. Remplace le calcul de poids existant uniquement si tu as besoin de ratios définis manuellement :
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() normalise ces valeurs pour obtenir une moyenne de 1.0 et les stocke dans le modèle, où la perte de détection existante les applique. Les indices ci-dessus nécessitent un jeu de données comportant au moins deux classes.
Enregistrer le meilleur modèle selon une métrique personnalisée#
L’entraîneur enregistre best.pt selon le score d’aptitude, qui, pour la détection, correspond par défaut à mAP@0.5:0.95 (pondérations [0.0, 0.0, 0.0, 1.0] pour [P, R, mAP@0.5, mAP@0.5:0.95]). Pour utiliser une autre métrique (comme mAP@0.5 ou le rappel), remplace validate() et renvoie la métrique choisie comme valeur d’aptitude. save_model(), intégré, l’utilisera alors automatiquement :
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() met à jour best_fitness à l’aide de la métrique par défaut ; récupère donc sa valeur précédente avant de l’appeler.
Voici quelques métriques courantes disponibles dans self.metrics après la validation :
| Clé | Description |
|---|---|
metrics/precision(B) | Précision |
metrics/recall(B) | Rappel |
metrics/mAP50(B) | mAP à IoU 0.5 |
metrics/mAP50-95(B) | mAP à IoU 0.5:0.95 |
Geler et dégeler le backbone#
Les flux de travail d’apprentissage par transfert tirent souvent parti du gel du backbone préentraîné pendant les N premières époques, afin de laisser la tête de détection s’adapter avant le fine-tuning du réseau entier. Ultralytics fournit un paramètre freeze pour geler des couches au début de l’entraînement ; tu peux utiliser un callback pour les dégeler après N époques :
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)Le paramètre freeze=10 gèle les 10 premières couches (indices 0-9) au début de l’entraînement, ce qui couvre la majeure partie du backbone de YOLO26. Le backbone comprend les couches 0 à 10 ; freeze=10 laisse donc le dernier bloc C2PSA (couche 10) entraînable. Utilise freeze=11 pour geler tout le backbone. Le callback on_train_epoch_start est exécuté au début de chaque époque et dégèle les couches demandées une fois la période de gel terminée, tout en maintenant gelés les paramètres DFL et ceux de l’enseignant de distillation qui le sont de façon permanente.
freeze=10gèle les 10 premières couches, indices 0-9 (la majeure partie du backbone de YOLO26 ; utilisefreeze=11pour inclure le dernier bloc C2PSA, à la couche 10)freeze=[0, 1, 2, 3]gèle des couches spécifiques par indice- Des valeurs plus élevées de
FREEZE_EPOCHSdonnent à la tête plus de temps pour s’adapter avant que le backbone ne change
Taux d’apprentissage par couche#
Différentes parties du réseau peuvent tirer parti de taux d’apprentissage différents. Une stratégie courante consiste à utiliser un taux d’apprentissage plus faible pour le backbone préentraîné afin de préserver les caractéristiques apprises, tout en permettant à la tête de détection de s’adapter plus rapidement avec un taux plus élevé :
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#
Pour RT-DETR, utilise la même surcharge avec RTDETRTrainer comme classe parente et charge le point de contrôle avec RTDETR("rtdetr-l.pt").
BatchNorm synchronisé pour l’entraînement multi-GPU#
Lors d’un entraînement sur plusieurs GPU avec DistributedDataParallel, les couches BatchNorm2d par défaut calculent les statistiques indépendamment sur chaque GPU. Pour le fine-tuning de RT-DETR et d’autres recettes utilisant de petites tailles de lot par GPU, les statistiques de lot par GPU peuvent être bruitées. SyncBatchNorm de PyTorch synchronise la moyenne et la variance entre tous les rangs pour obtenir une statistique globale sur un seul lot, ce qui améliore souvent la convergence au prix d’une légère surcharge de communication entre GPU.
La conversion doit avoir lieu après le transfert du modèle sur le GPU, mais avant son encapsulation par DDP. Le point d’extension le plus simple est set_model_attributes(), que BaseTrainer appelle précisément à ce moment-là :
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 vérification world_size > 1 garantit que l’entraîneur fonctionne aussi avec un seul GPU ; dans ce cas, la conversion est ignorée et l’entraînement se poursuit avec BatchNorm2d standard. La même méthode fonctionne pour YOLO en remplaçant la classe parente par DetectionTrainer.
| Scénario | Recommandation |
|---|---|
| Entraînement multi-GPU, petit lot par GPU (≤ 16) | Activer |
| Entraînement multi-GPU, grand lot par GPU (≥ 32) | Facultatif ; bénéfice mineur |
| Entraînement sur un seul GPU | Non applicable (ignoré) |
Clipping des gradients configurable#
L’entraîneur par défaut limite les gradients à max_norm=10.0 dans optimizer_step(), une valeur permissive adaptée aux modèles YOLO, dont les gradients la dépassent rarement. Les détecteurs de la famille DETR (RT-DETR, DEIM, DINO) utilisent généralement des valeurs bien plus faibles, comme 0.1, pour stabiliser les couches d’attention croisée du décodeur, où l’amplitude des gradients peut fortement augmenter. Pour remplacer la valeur de clipping, dérive une classe de l’entraîneur et remplace 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 # norme maximale des gradients ; définis la valeur à 0 pour désactiver le 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)Le même entraîneur fonctionne pour YOLO en remplaçant la classe parente par DetectionTrainer (from ultralytics.models.yolo.detect import DetectionTrainer) et en chargeant un point de contrôle YOLO avec YOLO("yolo26n.pt"). Le corps de optimizer_step reste inchangé.
| Famille d’architectures | Valeur typique de max_norm |
|---|---|
| Famille RT-DETR / DEIM / DETR | 0.1 |
| YOLO (valeur par défaut d’Ultralytics) | 10.0 |
| Désactiver le clipping | 0 |
FAQ#
Transmets ta classe d’entraîneur personnalisée (et non une instance) au paramètre
trainerdansmodel.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
YOLOgère l’instanciation de l’entraîneur en interne. Consulte la page Personnalisation avancée pour en savoir plus sur l’architecture de l’entraîneur.Principales méthodes disponibles pour la personnalisation :
Méthode Rôle validate()Exécuter la validation et renvoyer les métriques build_optimizer()Construire l’optimiseur save_model()Enregistrer les points de contrôle de l’entraînement get_model()Renvoyer l’instance du modèle get_validator()Renvoyer l’instance du validateur get_dataloader()Construire le chargeur de données preprocess_batch()Prétraiter le lot d’entrée label_loss_items()Formater les éléments de perte pour la journalisation Pour consulter la référence complète de l’API, voir la documentation
BaseTrainer.Oui, pour des personnalisations plus simples, les callbacks suffisent souvent. Les événements de callback disponibles comprennent
on_train_start,on_train_epoch_start,on_train_epoch_end,on_fit_epoch_endeton_model_save. Ils te permettent de t’insérer dans la boucle d’entraînement sans dériver de classe. L’exemple de gel du backbone ci-dessus illustre cette méthode.Si ta modification est plus simple (par exemple, ajuster les coefficients de la perte), tu peux modifier directement les hyperparamètres :
from ultralytics import YOLO model = YOLO("yolo26n.pt") model.train(data="coco8.yaml", box=10.0, cls=1.5, dfl=2.0)Avec YOLO26,
dflmet à l’échelle la valeurl1_lossjournalisée, car sa tête de détection utilisereg_max: 1; sur les modèles avecreg_max > 1, ce paramètre met à l’échelledfl_loss.