YOLO Vision 2026 :

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 par défaut la boucle d'entraînement, la validation, la sauvegarde des checkpoints et la journalisation. Lorsque 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 planifications du 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 epoch
  2. Ajouter des poids de classes pour gérer le déséquilibre entre les classes
  3. Enregistrer le meilleur modèle selon une métrique différente
  4. Geler le backbone pendant les N premiers epochs, puis le dégeler
  5. Spécifier les taux d'apprentissage par couche
  6. Synchroniser BatchNorm entre les GPU pour l'entraînement multi-GPU
  7. Configurer le gradient clipping pour ajuster la stabilité
Prérequis

Avant de lire ce guide, assure-toi de maîtriser les bases de l'entraînement des modèles YOLO ainsi que 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(). Cela te permet de 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."""

    # Add your customizations here

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 dois donc uniquement remplacer 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 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)

Cette opération journalise le score F1 moyen pour toutes les classes représentées dans la 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 avec la précision, le rappel, le 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 classes#

Définis cls_pw entre 0.0 et 1.0 pour appliquer des poids normalisés inversement proportionnels à la fréquence à la loss de classification. Ne remplace le calcul des poids existant que lorsque 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 for the production loss owner."""
        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 dataset comportant au moins deux classes.

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

Le trainer enregistre best.pt selon la fitness, qui, pour la détection, correspond par défaut à mAP@0.5:0.95 (les poids [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 ta métrique comme valeur de fitness. 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; enregistre donc sa valeur précédente avant de l'appeler.

Métriques disponibles

Les métriques courantes disponibles dans self.metrics après la validation comprennent :

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 bénéficient souvent du gel du backbone préentraîné pendant les N premiers epochs, afin de permettre à la tête de détection de s'adapter avant l'ajustement fin de l'ensemble du réseau. Ultralytics fournit un paramètre freeze pour geler les couches au début de l'entraînement, et tu peux utiliser un callback pour les dégeler après N epochs :

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 s'étend sur 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 epoch et dégèle les couches demandées une fois la période de gel terminée, tout en conservant le gel permanent des paramètres DFL et du teacher de distillation.

Choisir les éléments à 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 bloc C2PSA final à la couche 10)
  • freeze=[0, 1, 2, 3] gèle des couches spécifiques par index
  • 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 bénéficier 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 checkpoint 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 leurs statistiques indépendamment sur chaque GPU. Pour l'ajustement fin de RT-DETR et les autres recettes qui utilisent de petites tailles de batch par GPU, les statistiques de batch par GPU peuvent être bruitées. Le SyncBatchNorm de PyTorch synchronise la moyenne et la variance entre tous les ranks afin d'obtenir une statistique globale pour un batch unique, ce qui améliore souvent la convergence au prix d'un léger surcoût de communication entre les GPU.

La conversion doit avoir lieu après le placement du modèle sur le GPU, mais avant son encapsulation par DDP. Le hook le plus propre pour cela est set_model_attributes(), que BaseTrainer appelle exactement à ce moment :

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 également être utilisé sans risque lors d'exécutions sur un seul GPU; sur un seul GPU, la conversion est ignorée et l'entraînement se poursuit avec le BatchNorm2d normal. Le même schéma fonctionne pour YOLO en remplaçant la classe parente par DetectionTrainer.

Quand utiliser SyncBatchNorm
ScénarioRecommandation
Entraînement multi-GPU, petit batch par GPU (≤ 16)Activer
Entraînement multi-GPU, grand batch par GPU (≥ 32)Facultatif; avantage limité
Entraînement sur un seul GPUNon applicable (ignoré)

Gradient clipping configurable#

Le trainer par défaut applique un clipping des 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 beaucoup plus strictes, comme 0.1, pour stabiliser les couches de cross-attention du décodeur, où l'amplitude des gradients peut augmenter fortement. Pour remplacer la valeur de clipping, crée une sous-classe du trainer 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  # 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:
            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 checkpoint YOLO avec YOLO("yolo26n.pt"). Le corps de optimizer_step reste inchangé.

Valeurs typiques de `clip_grad_norm`
Famille d'architecturesmax_norm typique
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 plus de détails sur l'architecture du trainer.

  • Principales méthodes disponibles pour la personnalisation :

    MéthodeObjectif
    validate()Exécuter la validation et renvoyer les métriques
    build_optimizer()Construire l'optimiseur
    save_model()Enregistrer les checkpoints d'entraînement
    get_model()Renvoyer l'instance du modèle
    get_validator()Renvoyer l'instance du validateur
    get_dataloader()Construire le dataloader
    preprocess_batch()Prétraiter le batch d'entrée
    label_loss_items()Formater les éléments de la loss pour la journalisation

    Pour consulter la référence complète de l'API, voir la documentation de BaseTrainer.

  • Oui, pour les 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'interfacer avec 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 gains de la 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 l1_loss journalisé, car sa tête de détection utilise reg_max: 1; sur les modèles dotés de reg_max > 1, il met à l'échelle dfl_loss.

Commentaires