Ultralytics YOLO27 :

Personnaliser le 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 d’emblée la boucle d’entraînement, la validation, la sauvegarde des points de contrôle et la journalisation. Si tu as besoin de davantage de contrôle — pour suivre des métriques personnalisées, ajuster la pondération de la loss ou implémenter des schedulers de taux d’apprentissage — tu peux créer une sous-classe du trainer et redéfinir des méthodes spécifiques.

Ce guide présente sept personnalisations courantes :

  1. Journaliser des métriques personnalisées (score F1) à la fin de chaque époque
  2. Ajouter des poids de classe pour gérer le déséquilibre des classes
  3. Enregistrer le meilleur modèle en fonction d’une autre métrique
  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 maîtriser les bases de l’entraînement de modèles YOLO et de consulter la page Personnalisation avancée, qui présente l’architecture BaseTrainer.

Fonctionnement des trainers personnalisés#

La classe de modèle YOLO accepte un paramètre trainer dans la méthode train(). Tu peux ainsi lui transmettre ta propre classe de trainer 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 trainer personnalisé hérite de toutes les fonctionnalités de DetectionTrainer ; tu n’as donc qu’à redéfinir les méthodes spécifiques que tu souhaites personnaliser.

Journaliser des métriques personnalisées#

L’étape de validation calcule la précision, le rappel et le mAP. Si tu as besoin de métriques supplémentaires, comme le score F1 par classe, redéfinis 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 pour toutes les classes représentées dans les données de validation, ainsi que le détail 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 des métriques par image, avec la précision, le rappel, le score F1, 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 AP moyennes

Ajouter des poids de classe#

Définis cls_pw entre 0.0 et 1.0 pour appliquer des poids normalisés, inversement proportionnels aux fréquences, à la loss de classification. Redéfinis le calcul des 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 loss de détection existante les applique. Les indices ci-dessus nécessitent un jeu de données comprenant au moins deux classes.

Enregistrer le meilleur modèle selon une métrique personnalisée#

Le trainer enregistre best.pt en fonction du 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), redéfinis validate() et renvoie la métrique de ton choix comme valeur d’aptitude. Le 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 ; capture 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 workflows d’apprentissage par transfert tirent souvent profit du gel du backbone préentraîné pendant les N premières époques, ce qui permet à la tête de détection de s’adapter avant l’ajustement fin 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 YOLO26. Le backbone comprend les couches 0-10 ; freeze=10 laisse donc le bloc C2PSA final (couche 10) entraînable. Utilise freeze=11 pour geler l’intégralité du backbone. Le callback on_train_epoch_start s’exécute 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 laissant gelés les paramètres DFL et du teacher de distillation, qui le sont en permanence.

Choisir les couches à geler
  • freeze=10 gèle les 10 premières couches, indices 0-9 (la majeure partie du backbone YOLO26 ; utilise freeze=11 pour inclure le bloc C2PSA final, à la couche 10)
  • freeze=[0, 1, 2, 3] gèle des couches spécifiques en fonction de leur indice
  • Des valeurs plus élevées de FREEZE_EPOCHS donnent à la tête davantage de temps pour s’adapter avant que le backbone ne change

Taux d’apprentissage par couche#

Différentes parties du réseau peuvent tirer profit de taux d’apprentissage différents. Une stratégie courante consiste à utiliser un taux 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 redéfinition avec RTDETRTrainer comme classe parente et charge le point de contrôle avec RTDETR("rtdetr-l.pt").

BatchNorm synchronisée 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 l’ajustement fin de RT-DETR et d’autres recettes utilisant de petites tailles de lot par GPU, ces statistiques peuvent être bruitées. SyncBatchNorm de PyTorch synchronise la moyenne et la variance sur tous les rangs afin d’obtenir une statistique globale pour 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 hook le plus simple pour cela 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 condition world_size > 1 garantit que le trainer peut aussi être utilisé sans risque avec un seul GPU ; dans ce cas, la conversion est ignorée et l’entraînement se poursuit avec BatchNorm2d standard. Le même modèle 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 ; avantage mineur
Entraînement sur un seul GPUNon applicable (ignoré)

Clipping des gradients configurable#

Le trainer par défaut ramène les gradients à max_norm=10.0 dans optimizer_step(), une valeur élevée 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 de cross-attention du décodeur, où l’amplitude des gradients peut fortement augmenter. Pour remplacer la valeur de clipping, crée une sous-classe du trainer et redéfinis 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 sur 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 trainer 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 de trainer 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 du trainer en interne. Consulte la page Personnalisation avancée pour en savoir plus sur l’architecture du trainer.

  • Principales méthodes personnalisables :

    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 loss pour la journalisation

    Pour consulter la référence complète de l’API, reporte-toi à la documentation BaseTrainer.

  • Oui, les callbacks suffisent souvent pour les personnalisations simples. Parmi les événements de callback disponibles, on trouve on_train_start, on_train_epoch_start, on_train_epoch_end, on_fit_epoch_end et on_model_save. Ils te permettent d’intervenir dans la boucle d’entraînement sans créer de sous-classe. L’exemple de gel du backbone ci-dessus illustre cette approche.

  • Si ta modification est plus simple (par exemple, ajuster les coefficients de loss), 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)

    Sur 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, il met à l’échelle dfl_loss.

Commentaires