Link to this sectionНастройка Trainer#
Конвейер обучения Ultralytics построен на базе BaseTrainer и специализированных тренеров, таких как DetectionTrainer. Эти классы «из коробки» управляют циклом обучения, валидацией, созданием чекпоинтов и логированием. Когда тебе нужно больше контроля — например, для отслеживания пользовательских метрик, корректировки весов функций потерь или настройки расписания скорости обучения — ты можешь создать подкласс тренера и переопределить конкретные методы.
В этом руководстве рассмотрены семь распространенных способов настройки:
- Логирование пользовательских метрик (F1 score) в конце каждой эпохи
- Добавление весов классов для устранения дисбаланса классов
- Сохранение лучшей модели на основе другой метрики
- Замораживание бэкбона на первые N эпох с последующей разморозкой
- Настройка скорости обучения для каждого слоя
- Синхронизация BatchNorm между GPU для обучения на нескольких GPU
- Настройка градиентного клиппинга (gradient clipping) для обеспечения стабильности
Прежде чем изучать это руководство, убедись, что ты знаком с основами обучения моделей YOLO и страницей Расширенная настройка, где описывается архитектура BaseTrainer.
Link to this sectionКак работают пользовательские тренеры#
Класс модели YOLO принимает параметр trainer в методе train(). Это позволяет тебе передать собственный класс тренера, который расширяет поведение по умолчанию:
from ultralytics import YOLO
from ultralytics.models.yolo.detect import DetectionTrainer
class CustomTrainer(DetectionTrainer):
"""A custom trainer that extends DetectionTrainer with additional functionality."""
pass # Add your customizations here
model = YOLO("yolo26n.pt")
model.train(data="coco8.yaml", epochs=10, trainer=CustomTrainer)Твой пользовательский тренер наследует всю функциональность от DetectionTrainer, поэтому тебе нужно переопределить только те методы, которые хочешь изменить.
Link to this sectionЛогирование пользовательских метрик#
Шаг валидации вычисляет precision, recall и mAP. Если тебе нужны дополнительные метрики, такие как F1 score для каждого класса, переопредели метод 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
valid_f1 = f1_per_class[f1_per_class > 0]
mean_f1 = np.mean(valid_f1) if len(valid_f1) > 0 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) if f1_per_class[j] > 0
]
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)Этот код логирует средний F1 score по всем классам и подробную информацию по каждому классу после каждого цикла валидации.
Валидатор предоставляет доступ ко многим метрикам через self.validator.metrics.box:
| Атрибут | Описание |
|---|---|
f1 | F1 score по классам |
image_metrics | Словарь метрик для каждого изображения, включая precision, recall, F1, TP, FP и FN |
p | Precision по классам |
r | Recall по классам |
ap50 | AP при IoU 0.5 по классам |
ap | AP при IoU 0.5:0.95 по классам |
mp, mr | Средние значения precision и recall |
map50, map | Средние значения метрик AP |
Link to this sectionДобавление весов классов#
Если в твоем наборе данных есть дисбаланс классов (например, редкий дефект при промышленном контроле), ты можешь увеличить вес недопредставленных классов в функции потерь. Это заставит модель сильнее штрафовать ошибки классификации для редких классов.
Чтобы настроить функцию потерь, создай подклассы для классов потерь, модели и тренера:
import torch
from torch import nn
from ultralytics import YOLO
from ultralytics.models.yolo.detect import DetectionTrainer
from ultralytics.nn.tasks import DetectionModel
from ultralytics.utils import RANK
from ultralytics.utils.loss import E2ELoss, v8DetectionLoss
class WeightedDetectionLoss(v8DetectionLoss):
"""Detection loss with class weights applied to BCE classification loss."""
def __init__(self, model, class_weights=None, tal_topk=10, tal_topk2=None):
"""Initialize loss with optional per-class weights for BCE."""
super().__init__(model, tal_topk=tal_topk, tal_topk2=tal_topk2)
if class_weights is not None:
self.bce = nn.BCEWithLogitsLoss(
pos_weight=class_weights.to(self.device),
reduction="none",
)
class WeightedE2ELoss(E2ELoss):
"""E2E Loss with class weights for YOLO26."""
def __init__(self, model, class_weights=None):
"""Initialize E2E loss with weighted detection loss."""
def weighted_loss_fn(model, tal_topk=10, tal_topk2=None):
return WeightedDetectionLoss(model, class_weights=class_weights, tal_topk=tal_topk, tal_topk2=tal_topk2)
super().__init__(model, loss_fn=weighted_loss_fn)
class WeightedDetectionModel(DetectionModel):
"""Detection model that uses class-weighted loss."""
def init_criterion(self):
"""Initialize weighted loss criterion with per-class weights."""
class_weights = torch.ones(self.nc)
class_weights[0] = 2.0 # upweight class 0
class_weights[1] = 3.0 # upweight rare class 1
return WeightedE2ELoss(self, class_weights=class_weights)
class WeightedTrainer(DetectionTrainer):
"""Trainer that returns a WeightedDetectionModel."""
def get_model(self, cfg=None, weights=None, verbose=True):
"""Return a WeightedDetectionModel."""
model = WeightedDetectionModel(cfg, nc=self.data["nc"], verbose=verbose and RANK == -1)
if weights:
model.load(weights)
return model
model = YOLO("yolo26n.pt")
model.train(data="coco8.yaml", epochs=10, trainer=WeightedTrainer)Ты можешь автоматически вычислить веса классов на основе распределения меток в твоем датасете. Распространенный подход — взвешивание, обратно пропорциональное частоте встречаемости:
import numpy as np
# class_counts: number of instances per class
class_counts = np.array([5000, 200, 3000])
# Inverse frequency: rarer classes get higher weight
class_weights = max(class_counts) / class_counts
# Result: [1.0, 25.0, 1.67]Link to this sectionСохранение лучшей модели по пользовательской метрике#
Тренер сохраняет best.pt на основе fitness-показателя, который по умолчанию равен 0.9 × mAP@0.5:0.95 + 0.1 × mAP@0.5. Чтобы использовать другую метрику (например, mAP@0.5 или recall), переопредели validate() и верни выбранную метрику в качестве fitness-значения. Встроенный метод save_model() затем автоматически ее использует:
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."""
metrics, fitness = super().validate()
if metrics:
fitness = metrics.get("metrics/mAP50(B)", fitness)
if self.best_fitness is None or fitness > self.best_fitness:
self.best_fitness = fitness
return metrics, fitness
model = YOLO("yolo26n.pt")
model.train(data="coco8.yaml", epochs=20, trainer=CustomSaveTrainer)Распространенные метрики, доступные в self.metrics после валидации, включают:
| Ключ | Описание |
|---|---|
metrics/precision(B) | Precision |
metrics/recall(B) | Recall |
metrics/mAP50(B) | mAP при IoU 0.5 |
metrics/mAP50-95(B) | mAP при IoU 0.5:0.95 |
Link to this sectionЗамораживание и размораживание бэкбона#
Процессы transfer learning часто выигрывают от замораживания предобученного бэкбона на первые N эпох, что позволяет детектирующей голове адаптироваться перед fine-tuning всей сети. Ultralytics предоставляет параметр freeze для замораживания слоев в начале обучения, и ты можешь использовать callback, чтобы разморозить их после N эпох:
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 all layers after FREEZE_EPOCHS."""
if trainer.epoch == FREEZE_EPOCHS:
LOGGER.info(f"Epoch {trainer.epoch}: Unfreezing all layers for fine-tuning")
for name, param in trainer.model.named_parameters():
if not param.requires_grad:
param.requires_grad = True
LOGGER.info(f" Unfroze: {name}")
trainer.freeze_layer_names = [".dfl"]
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)Параметр freeze=10 замораживает первые 10 слоев (бэкбон) в начале обучения. Callback on_train_epoch_start срабатывает в начале каждой эпохи и размораживает все параметры, как только период заморозки завершается.
freeze=10замораживает первые 10 слоев (обычно бэкбон в архитектурах YOLO)freeze=[0, 1, 2, 3]замораживает указанные слои по индексу- Большие значения
FREEZE_EPOCHSдают голове больше времени на адаптацию до изменения бэкбона
Link to this sectionСкорость обучения для каждого слоя#
Различные части сети могут требовать разной скорости обучения. Распространенная стратегия — использовать более низкую скорость обучения для предобученного бэкбона, чтобы сохранить изученные признаки, позволяя при этом голове адаптироваться быстрее при более высокой скорости:
import torch
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."""
def build_optimizer(self, model, name="auto", lr=0.001, momentum=0.9, decay=1e-5, iterations=1e5):
"""Build optimizer with separate learning rates for backbone and head."""
backbone_params = []
head_params = []
for k, v in unwrap_model(model).named_parameters():
if not v.requires_grad:
continue
is_backbone = any(k.startswith(f"model.{i}.") for i in range(10))
if is_backbone:
backbone_params.append(v)
else:
head_params.append(v)
backbone_lr = lr * 0.1
optimizer = torch.optim.AdamW(
[
{"params": backbone_params, "lr": backbone_lr, "weight_decay": decay},
{"params": head_params, "lr": lr, "weight_decay": decay},
],
)
LOGGER.info(
f"PerLayerLR optimizer: backbone ({len(backbone_params)} params, lr={backbone_lr}) "
f"| head ({len(head_params)} params, lr={lr})"
)
return optimizer
model = YOLO("yolo26n.pt")
model.train(data="coco8.yaml", epochs=20, trainer=PerLayerLRTrainer)Link to this sectionВариант RT-DETR#
Для RT-DETR паттерн тот же, но с двумя уточнениями. Длина бэкбона считывается из model.yaml["backbone"], поэтому один и тот же тренер работает для разных вариантов RT-DETR (RT-DETR-L, RT-DETR-X, бэкбоны ResNet-50/101) без хардкодинга количества слоев. Параметры также разделены на группы весов, BatchNorm и bias в каждой секции, чтобы weight decay не применялся к параметрам BatchNorm и смещениям, что соответствует стандартной политике тренера. Это особенно полезно при дообучении RT-DETR, где голова декодера обычно инициализируется случайно, а бэкбон несет предобученные признаки, для которых полезна более низкая скорость обучения:
import torch
from torch import nn
from ultralytics import RTDETR
from ultralytics.models.rtdetr.train import RTDETRTrainer
from ultralytics.utils import LOGGER, colorstr
from ultralytics.utils.torch_utils import unwrap_model
class RTDETRBackboneLRTrainer(RTDETRTrainer):
"""RT-DETR trainer with a lower learning rate for backbone parameters."""
backbone_lr_ratio = 0.1 # backbone learning rate as a fraction of head learning rate
def build_optimizer(self, model, name="auto", lr=0.001, momentum=0.9, decay=1e-5, iterations=1e5):
"""Build an AdamW optimizer with six param groups: head and backbone x {weight, bn, bias}."""
# Resolve optimizer name; "auto" maps to AdamW with RT-DETR-style defaults
canonical = {"Adam", "Adamax", "AdamW", "NAdam", "RAdam", "auto"}
name = {x.lower(): x for x in canonical}.get(name.lower(), name)
if name == "auto":
name, lr, momentum = "AdamW", 1e-4, 0.9
self.args.warmup_bias_lr = 0.0 # RT-DETR warms biases from 0, unlike YOLO's 0.1
if name not in {"Adam", "Adamax", "AdamW", "NAdam", "RAdam"}:
raise NotImplementedError(f"This trainer only supports AdamW-family optimizers; got {name}")
# Identify backbone parameters from model.yaml and route each param into a (section, kind) group
unwrapped = unwrap_model(model)
backbone_len = len(unwrapped.yaml["backbone"])
norm_types = tuple(v for k, v in nn.__dict__.items() if "Norm" in k)
groups = {f"{s}_{k}": [] for s in ("head", "backbone") for k in ("weight", "bn", "bias")}
for module_name, module in unwrapped.named_modules():
for param_name, param in module.named_parameters(recurse=False):
if not param.requires_grad:
continue
fullname = f"{module_name}.{param_name}" if module_name else param_name
parts = fullname.split(".")
section = (
"backbone"
if len(parts) > 1 and parts[0] == "model" and parts[1].isdigit() and int(parts[1]) < backbone_len
else "head"
)
if "bias" in param_name:
kind = "bias"
elif isinstance(module, norm_types) or "logit_scale" in fullname:
kind = "bn"
else:
kind = "weight"
groups[f"{section}_{kind}"].append(param)
# Build the optimizer with per-group lr and weight decay; backbone groups use lr * backbone_lr_ratio
backbone_lr = lr * self.backbone_lr_ratio
param_groups = [
{"params": groups["head_weight"], "lr": lr, "weight_decay": decay, "param_group": "weight"},
{"params": groups["head_bn"], "lr": lr, "weight_decay": 0.0, "param_group": "bn"},
{"params": groups["head_bias"], "lr": lr, "weight_decay": 0.0, "param_group": "bias"},
{"params": groups["backbone_weight"], "lr": backbone_lr, "weight_decay": decay, "param_group": "weight"},
{"params": groups["backbone_bn"], "lr": backbone_lr, "weight_decay": 0.0, "param_group": "bn"},
{"params": groups["backbone_bias"], "lr": backbone_lr, "weight_decay": 0.0, "param_group": "bias"},
]
param_groups = [pg for pg in param_groups if pg["params"]] # drop empty groups
optimizer = getattr(torch.optim, name)(param_groups, betas=(momentum, 0.999))
LOGGER.info(
f"{colorstr('optimizer:')} {name}(lr={lr}, backbone_lr={backbone_lr}) with parameter groups\n"
f" Head: {len(groups['head_bn'])} bn, {len(groups['head_weight'])} weight(decay={decay}), "
f"{len(groups['head_bias'])} bias (lr={lr})\n"
f" Backbone: {len(groups['backbone_bn'])} bn, {len(groups['backbone_weight'])} weight(decay={decay}), "
f"{len(groups['backbone_bias'])} bias (lr={backbone_lr})"
)
return optimizer
model = RTDETR("rtdetr-l.pt")
model.train(data="coco8.yaml", epochs=20, trainer=RTDETRBackboneLRTrainer)Типичной отправной точкой является backbone_lr_ratio = 0.1, что соответствует оригинальной настройке RT-DETR с бэкбоном HGNetV2. Литература предполагает масштабирование коэффициента обратно пропорционально размеру бэкбона и масштабу данных предобучения: для больших бэкбонов, предобученных на очень больших датасетах (например, ViT-L/H, обученных с DINO, CLIP или MAE на сотнях миллионов изображений), обычно используются меньшие коэффициенты, например 0.01 или ниже, чтобы сохранить хорошо изученные признаки, в то время как меньшие бэкбоны с более легким предобучением допускают использование больших коэффициентов, например 0.5 и выше.
Встроенный планировщик скорости обучения (cosine или linear) по-прежнему применяется поверх базовых скоростей обучения для каждой группы. Скорости обучения как для бэкбона, так и для головы будут следовать одному и тому же графику затухания, поддерживая соотношение между ними на протяжении всего обучения.
Эти настройки можно объединить в один класс тренера, переопределив несколько методов и добавив необходимые callbacks.
Link to this sectionСинхронизированный BatchNorm для обучения на нескольких GPU#
При обучении на нескольких GPU с использованием DistributedDataParallel стандартные слои BatchNorm2d вычисляют статистику независимо на каждом GPU. Для дообучения RT-DETR и других сценариев с небольшим размером батча на GPU, статистика батча может быть зашумленной. SyncBatchNorm из PyTorch синхронизирует среднее значение и дисперсию по всем процессам (ranks) для получения глобальной статистики батча, что часто улучшает сходимость ценой небольших накладных расходов на коммуникацию между GPU.
Преобразование должно произойти после того, как модель перемещена на GPU, но до того, как DDP обернет её. Лучший способ для этого — хук set_model_attributes(), который BaseTrainer вызывает именно в этот момент:
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)Проверка world_size > 1 гарантирует, что тренер безопасно работает и при запуске на одном GPU; на одном GPU преобразование пропускается, и обучение продолжается с обычным BatchNorm2d. Тот же паттерн работает для YOLO при смене родительского класса на DetectionTrainer.
| Сценарий | Рекомендация |
|---|---|
| Обучение на нескольких GPU, малый батч на GPU (≤ 16) | Включить |
| Обучение на нескольких GPU, большой батч на GPU (≥ 32) | Опционально; небольшое преимущество |
| Обучение на одном GPU | Неприменимо (пропускается) |
Link to this sectionНастраиваемый градиентный клиппинг#
Стандартный тренер ограничивает градиенты до max_norm=10.0 в методе optimizer_step(). Это значение было подобрано для моделей YOLO, где градиенты редко превышают его. Детекторы семейства DETR (RT-DETR, DEIM, DINO) обычно используют гораздо более строгие значения, например 0.1, для стабилизации слоев cross-attention декодера, где значения градиентов могут резко возрастать. Чтобы переопределить значение клиппинга, создай подкласс тренера и переопредели optimizer_step():
import torch
from ultralytics import RTDETR
from ultralytics.models.rtdetr.train import RTDETRTrainer
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:
torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=self.clip_grad_norm)
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)Тот же тренер работает для YOLO при смене родительского класса на DetectionTrainer (from ultralytics.models.yolo.detect import DetectionTrainer) и загрузке чекпоинта YOLO с помощью YOLO("yolo26n.pt"). Содержимое optimizer_step остается без изменений.
| Семейство архитектур | Типичный max_norm |
|---|---|
| Семейство RT-DETR / DEIM / DETR | 0.1 |
| YOLO (по умолчанию в Ultralytics) | 10.0 |
| Отключить клиппинг | 0 |
Link to this sectionЧасто задаваемые вопросы (FAQ)#
Link to this sectionКак передать пользовательский тренер в YOLO?#
Передай класс своего пользовательского тренера (не экземпляр) в параметр trainer метода model.train():
from ultralytics import YOLO
model = YOLO("yolo26n.pt")
model.train(data="coco8.yaml", trainer=MyCustomTrainer)Класс YOLO инициализирует тренера самостоятельно. Подробности об архитектуре тренера смотри на странице Расширенная настройка.
Link to this sectionКакие методы BaseTrainer можно переопределить?#
Основные методы, доступные для настройки:
| Метод | Цель |
|---|---|
validate() | Запуск валидации и возврат метрик |
build_optimizer() | Создание оптимизатора |
save_model() | Сохраняй контрольные точки обучения |
get_model() | Возвращай экземпляр модели |
get_validator() | Возвращай экземпляр валидатора |
get_dataloader() | Создавай загрузчик данных |
preprocess_batch() | Предварительно обрабатывай входной батч |
label_loss_items() | Форматируй значения потерь для логирования |
Полное справочное руководство по API смотри в документации BaseTrainer.
Link to this sectionМогу ли я использовать колбэки вместо создания подкласса тренера?#
Да, для простых настроек колбэков часто бывает достаточно. Доступные события колбэков включают on_train_start, on_train_epoch_start, on_train_epoch_end, on_fit_epoch_end и on_model_save. Они позволяют тебе внедряться в цикл обучения без создания подкласса. Пример с заморозкой бэкбона выше демонстрирует этот подход.
Link to this sectionКак мне настроить функцию потерь без создания подкласса модели?#
Если твое изменение проще (например, корректировка весов потерь), ты можешь изменить гиперпараметры напрямую:
model.train(data="coco8.yaml", box=10.0, cls=1.5, dfl=2.0)Для структурных изменений в функциях потерь (например, добавление весов классов) тебе нужно создать подкласс функции потерь и модели, как показано в разделе о весах классов.