Ultralytics YOLO27 : Rejoins la liste d’attente.
Get Started

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 :

  1. Journaliser des métriques personnalisées (score F1) à la fin de chaque époque
  2. Ajouter des pondérations de classe pour gérer le déséquilibre des classes
  3. Enregistrer le meilleur modèle selon une métrique différente
  4. Geler le backbone pendant les N premières époques, puis le dégeler
  5. Définir des taux d’apprentissage par couche
  6. Synchroniser BatchNorm entre les GPU pour l’entraînement multi-GPU
  7. Configurer le clipping des gradients pour ajuster la stabilité
Prérequis

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.

Métriques disponibles

Le validateur donne accès à de nombreuses métriques via self.validator.metrics.box :

AttributDescription
f1Score F1 par classe
image_metricsDictionnaire de métriques par image comprenant la précision, le rappel, le F1, les TP, FP et FN
pPrécision par classe
rRappel par classe
ap50AP à IoU 0.5 par classe
apAP à IoU 0.5:0.95 par classe
mp, mrPrécision et rappel moyens
map50, mapMé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.

Métriques disponibles

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.

Choisir les couches à geler
  • freeze=10 gèle les 10 premières couches, indices 0-9 (la majeure partie du backbone de YOLO26 ; utilise freeze=11 pour 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_EPOCHS donnent à 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.

Quand utiliser SyncBatchNorm
ScénarioRecommandation
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 GPUNon 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é.

Valeurs typiques de `clip_grad_norm`
Famille d’architecturesValeur typique de max_norm
Famille RT-DETR / DEIM / DETR0.1
YOLO (valeur par défaut d’Ultralytics)10.0
Désactiver le clipping0

FAQ#

  • Transmets ta classe d’entraîneur personnalisée (et non une instance) au paramètre trainer dans model.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 YOLO gè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éthodeRô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_end et on_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, dfl met à l’échelle la valeur l1_loss journalisée, car sa tête de détection utilise reg_max: 1 ; sur les modèles avec reg_max > 1, ce paramètre met à l’échelle dfl_loss.

Commentaires