Trainer anpassen#
Die Trainingspipeline von Ultralytics basiert auf BaseTrainer und aufgabenspezifischen Trainern wie DetectionTrainer. Diese Klassen übernehmen standardmäßig die Trainingsschleife, die Validierung, das Speichern von Prüfpunkten und die Protokollierung. Wenn du mehr Kontrolle benötigst – etwa um eigene Metriken zu erfassen, die Gewichtung des Verlusts anzupassen oder Lernratenpläne zu implementieren –, kannst du vom Trainer eine Unterklasse ableiten und bestimmte Methoden überschreiben.
Diese Anleitung führt durch sieben gängige Anpassungen:
- Eigene Metriken protokollieren (F1-Wert) am Ende jeder Epoche
- Klassengewichte hinzufügen, um eine Klassenungleichverteilung auszugleichen
- Das beste Modell speichern, basierend auf einer anderen Metrik
- Das Backbone einfrieren während der ersten N Epochen und anschließend wieder freigeben
- Lernraten pro Schicht festlegen
- BatchNorm über GPUs synchronisieren, um das Training mit mehreren GPUs zu ermöglichen
- Gradient Clipping konfigurieren, um die Stabilität einzustellen
Bevor du diese Anleitung liest, solltest du mit den Grundlagen des Trainings von YOLO-Modellen und der Seite Erweiterte Anpassung vertraut sein, die die Architektur von BaseTrainer behandelt.
Funktionsweise benutzerdefinierter Trainer#
Die Modellklasse YOLO akzeptiert einen Parameter trainer in der Methode train(). Damit kannst du deine eigene Trainerklasse übergeben, die das Standardverhalten erweitert:
from ultralytics import YOLO
from ultralytics.models.yolo.detect import DetectionTrainer
class CustomTrainer(DetectionTrainer):
"""A custom trainer that extends DetectionTrainer with additional functionality."""
# Füge hier deine Anpassungen hinzu
model = YOLO("yolo26n.pt")
model.train(data="coco8.yaml", epochs=10, trainer=CustomTrainer)Dein benutzerdefinierter Trainer erbt sämtliche Funktionen von DetectionTrainer. Du musst daher nur die Methoden überschreiben, die du anpassen möchtest.
Eigene Metriken protokollieren#
Der Schritt der Validierung berechnet Präzision, Trefferquote und mAP. Wenn du zusätzliche Metriken wie den F1-Wert pro Klasse benötigst, überschreibe 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)Nach jedem Validierungslauf werden hier der mittlere F1-Wert über alle in der Validierung vertretenen Klassen sowie eine Aufschlüsselung nach Klassen protokolliert.
Der Validator bietet über self.validator.metrics.box Zugriff auf zahlreiche Metriken:
| Attribut | Beschreibung |
|---|---|
f1 | F1-Wert pro Klasse |
image_metrics | Metrikverzeichnis pro Bild mit Präzision, Trefferquote, F1, TP, FP und FN |
p | Präzision pro Klasse |
r | Trefferquote pro Klasse |
ap50 | AP bei IoU 0.5 pro Klasse |
ap | AP bei IoU 0.5:0.95 pro Klasse |
mp, mr | Mittlere Präzision und Trefferquote |
map50, map | Mittlere AP-Metriken |
Klassengewichte hinzufügen#
Lege cls_pw zwischen 0.0 und 1.0 fest, um normalisierte Gewichte auf Basis der inversen Häufigkeit auf den Klassifikationsverlust anzuwenden. Überschreibe die vorhandene Gewichtsberechnung nur, wenn du manuell festgelegte Verhältnisse benötigst:
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() normalisiert diese Werte auf einen Mittelwert von 1.0 und speichert sie im Modell, wo der vorhandene Erkennungsverlust sie anwendet. Die oben genannten Indizes setzen einen Datensatz mit mindestens zwei Klassen voraus.
Das beste Modell anhand einer benutzerdefinierten Metrik speichern#
Der Trainer speichert best.pt anhand des Fitnesswerts. Bei der Erkennung ist dieser standardmäßig mAP@0.5:0.95 (Gewichte [0.0, 0.0, 0.0, 1.0] für [P, R, mAP@0.5, mAP@0.5:0.95]). Wenn du eine andere Metrik verwenden möchtest, etwa mAP@0.5 oder die Trefferquote, überschreibe validate() und gib die gewählte Metrik als Fitnesswert zurück. Die integrierte Methode save_model() verwendet diesen Wert dann automatisch:
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() aktualisiert best_fitness anhand der Standardmetrik. Sichere daher den vorherigen Wert, bevor du die Methode aufrufst.
Zu den gängigen Metriken, die nach der Validierung in self.metrics verfügbar sind, gehören:
| Schlüssel | Beschreibung |
|---|---|
metrics/precision(B) | Präzision |
metrics/recall(B) | Trefferquote |
metrics/mAP50(B) | mAP bei IoU 0.5 |
metrics/mAP50-95(B) | mAP bei IoU 0.5:0.95 |
Backbone einfrieren und wieder freigeben#
Bei Workflows zum Transferlernen ist es oft vorteilhaft, das vortrainierte Backbone während der ersten N Epochen einzufrieren. So kann sich der Erkennungskopf anpassen, bevor das gesamte Netzwerk feinabgestimmt wird. Ultralytics stellt den Parameter freeze bereit, mit dem du zu Beginn des Trainings Schichten einfrieren kannst. Mit einem Callback kannst du sie nach N Epochen wieder freigeben:
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)Der Parameter freeze=10 friert zu Beginn des Trainings die ersten 10 Schichten (Indizes 0–9) ein, die den größten Teil des YOLO26-Backbones abdecken. Das Backbone umfasst die Schichten 0–10. Mit freeze=10 bleibt daher der letzte C2PSA-Block (Schicht 10) trainierbar; mit freeze=11 frierst du das gesamte Backbone ein. Der Callback on_train_epoch_start wird zu Beginn jeder Epoche ausgeführt und gibt die angeforderten Schichten nach Ablauf des Einfrierzeitraums wieder frei. Dauerhaft eingefrorene DFL- und Distillations-Lehrerparameter bleiben dabei unverändert.
freeze=10friert die ersten 10 Schichten mit den Indizes 0–9 ein (den größten Teil des YOLO26-Backbones; verwendefreeze=11, um auch den letzten C2PSA-Block in Schicht 10 einzuschließen).freeze=[0, 1, 2, 3]friert bestimmte Schichten anhand ihres Index ein- Höhere Werte für
FREEZE_EPOCHSgeben dem Kopf mehr Zeit zur Anpassung, bevor sich das Backbone verändert
Lernraten pro Schicht#
Verschiedene Teile des Netzwerks können von unterschiedlichen Lernraten profitieren. Eine gängige Strategie besteht darin, für das vortrainierte Backbone eine niedrigere Lernrate zu verwenden, damit die erlernten Merkmale erhalten bleiben, während sich der Erkennungskopf mit einer höheren Lernrate schneller anpassen kann:
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)RT-DETR-Variante#
Verwende für RT-DETR dieselbe Überschreibung mit RTDETRTrainer als übergeordneter Klasse und lade den Prüfpunkt mit RTDETR("rtdetr-l.pt").
Synchronisiertes BatchNorm für das Training mit mehreren GPUs#
Beim Training mit mehreren GPUs und DistributedDataParallel berechnen die standardmäßigen BatchNorm2d-Schichten die Statistiken unabhängig auf jeder GPU. Beim Feinabstimmen von RT-DETR und bei anderen Rezepten mit kleinen Stapelgrößen pro GPU können die Statistiken einzelner GPUs verrauscht sein. PyTorchs SyncBatchNorm synchronisiert Mittelwert und Varianz über alle Ränge hinweg, um eine globale Stapelstatistik zu berechnen. Das verbessert oft die Konvergenz, verursacht jedoch einen geringen Kommunikationsaufwand zwischen den GPUs.
Die Umwandlung muss erfolgen, nachdem das Modell auf die GPU verschoben wurde, aber bevor DDP es umschließt. Der geeignete Hook dafür ist set_model_attributes(), den BaseTrainer genau in diesem Zeitfenster aufruft:
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)Die Abfrage world_size > 1 stellt sicher, dass der Trainer auch bei Läufen mit einer GPU sicher verwendet werden kann. Bei nur einer GPU wird die Umwandlung übersprungen und das Training mit dem regulären BatchNorm2d fortgesetzt. Dasselbe Muster funktioniert für YOLO, wenn du die übergeordnete Klasse durch DetectionTrainer ersetzt.
| Szenario | Empfehlung |
|---|---|
| Training mit mehreren GPUs, kleiner Stapel pro GPU (≤ 16) | Aktivieren |
| Training mit mehreren GPUs, großer Stapel pro GPU (≥ 32) | Optional; geringer Vorteil |
| Training mit einer GPU | Nicht zutreffend (übersprungen) |
Gradient Clipping konfigurieren#
Der Standardtrainer begrenzt die Gradienten in optimizer_step() auf max_norm=10.0. Dieser großzügige Wert ist auf YOLO-Modelle abgestimmt, deren Gradienten ihn nur selten überschreiten. Detektoren der DETR-Familie (RT-DETR, DEIM, DINO) verwenden typischerweise deutlich niedrigere Werte wie 0.1, um die Kreuzaufmerksamkeitsschichten des Decoders zu stabilisieren, in denen die Gradientenbeträge stark ansteigen können. Um den Grenzwert zu ändern, leite eine Unterklasse vom Trainer ab und überschreibe 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 # maximale Gradienten-Norm; auf 0 setzen, um das Clipping zu deaktivieren
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)Derselbe Trainer funktioniert auch für YOLO, wenn du die übergeordnete Klasse durch DetectionTrainer (from ultralytics.models.yolo.detect import DetectionTrainer) ersetzt und einen YOLO-Prüfpunkt mit YOLO("yolo26n.pt") lädst. Der Inhalt von optimizer_step bleibt unverändert.
| Architekturfamilie | Typischer Wert für max_norm |
|---|---|
| RT-DETR- / DEIM- / DETR-Familie | 0.1 |
| YOLO (Ultralytics-Standard) | 10.0 |
| Clipping deaktivieren | 0 |
Häufig gestellte Fragen#
Übergib deine benutzerdefinierte Trainerklasse (keine Instanz) an den Parameter
trainerinmodel.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)Die Klasse
YOLOübernimmt intern die Instanziierung des Trainers. Weitere Informationen zur Trainerarchitektur findest du auf der Seite Erweiterte Anpassung.Wichtige Methoden zur Anpassung:
Methode Zweck validate()Validierung ausführen und Metriken zurückgeben build_optimizer()Den Optimierer erstellen save_model()Prüfpunkte des Trainings speichern get_model()Die Modellinstanz zurückgeben get_validator()Die Validatorinstanz zurückgeben get_dataloader()Den Datenlader erstellen preprocess_batch()Den Eingabestapel vorverarbeiten label_loss_items()Verlustwerte für die Protokollierung formatieren Die vollständige API-Referenz findest du in der Dokumentation zu
BaseTrainer.Ja, für einfachere Anpassungen reichen Callbacks oft aus. Zu den verfügbaren Callback-Ereignissen gehören
on_train_start,on_train_epoch_start,on_train_epoch_end,on_fit_epoch_endundon_model_save. Damit kannst du dich ohne Ableitung einer Unterklasse in die Trainingsschleife einklinken. Das obige Beispiel zum Einfrieren des Backbones zeigt diesen Ansatz.Wenn deine Änderung einfacher ist, etwa das Anpassen der Verlustgewichtungen, kannst du die Hyperparameter direkt ändern:
from ultralytics import YOLO model = YOLO("yolo26n.pt") model.train(data="coco8.yaml", box=10.0, cls=1.5, dfl=2.0)Bei YOLO26 skaliert
dflden protokollierten Wertl1_loss, da der Erkennungskopfreg_max: 1verwendet. Bei Modellen mitreg_max > 1skaliert die Methodedfl_loss.