トレーナーのカスタマイズ#
Ultralytics のトレーニングパイプラインは、BaseTrainer や DetectionTrainer のようなタスク固有のトレーナーを中心に構築されています。これらのクラスは、トレーニングループ、検証、チェックポイントの保存、およびロギングをデフォルトで処理します。カスタムメトリクスのトラッキング、損失の重みの調整、学習率スケジュールの実装など、より高度な制御が必要な場合は、トレーナーをサブクラス化して特定のメソッドをオーバーライドできます。
本ガイドでは、一般的によく行われる7つのカスタマイズについて解説します。
- 各 epoch の終了時に カスタム メトリクス (F1 score) をログに記録します
- クラスの不均衡に対処するためのクラス重みの追加
- 異なるメトリクスに基づく最良モデルの保存
- 最初のNエポックでバックボーンを凍結し、その後凍結を解除する処理
- レイヤーごとの学習率の指定
- マルチGPUトレーニングのためのGPU間での BatchNorm の同期
- 安定性チューニングのための勾配クリッピングの設定
このガイドを読む前に、training YOLO models の基礎および BaseTrainer アーキテクチャをカバーする Advanced Customization ページを十分に理解していることを確認してください。
カスタムトレーナーの仕組み#
YOLO モデルクラスは、train() メソッド内で trainer パラメータを受け入れます。これにより、デフォルトの動作を拡張する独自のトレーナー クラスを渡すことができます:
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)カスタムトレーナーは DetectionTrainer からすべての機能を継承するため、カスタマイズしたい特定のメソッドのみをオーバーライドすれば十分です。
カスタムメトリクスのログ記録#
検証ステップでは、適合率(precision)、再現率(recall)、および mAP を計算します。クラスごとの F1スコア などの追加メトリクスが必要な場合は、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スコアとクラスごとの内訳が記録されます。
バリデータは、self.validator.metrics.box を通じて多くのメトリクスへのアクセスを提供します。
| 属性 | 説明 |
|---|---|
f1 | クラスごとのF1スコア |
image_metrics | 適合率、再現率、F1、TP、FP、FNを含む画像ごとのメトリクス辞書 |
p | クラスごとの適合率 |
r | クラスごとの再現率 |
ap50 | クラスごとのIoU 0.5におけるAP |
ap | クラスごとのIoU 0.5:0.95におけるAP |
mp、mr | 平均適合率と平均再現率 |
map50、map | 平均APメトリクス |
クラス重みの追加#
データセットのクラスに偏りがある場合(例:製造検査における稀な欠陥など)、損失関数において表現が不足しているクラスの重みを大きくすることができます。これにより、モデルは稀なクラスの誤分類に対してより強くペナルティを課すようになります。
損失関数をカスタマイズするには、損失クラス、モデル、およびトレーナーをサブクラス化します。
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]WeightedDetectionModel などのカスタムクラスは、参照によってチェックポイントに保存されます。トレーニングスクリプト内で定義された場合、それらは __main__ モジュールに属するため、別のスクリプトから best.pt をロードすると AttributeError: Can't get attribute 'WeightedDetectionModel' on <module '__main__'> が発生します。
インポート可能な状態を維持するためにカスタムクラスは専用のモジュール内で定義し、ロード時にそのモジュールが PYTHONPATH に含まれていることを確認してください。
# weighted_model.py
from ultralytics.nn.tasks import DetectionModel
class WeightedDetectionModel(DetectionModel):
"""Detection model that uses class-weighted loss."""# inference script
from weighted_model import WeightedDetectionModel # noqa: F401 - must be importable at checkpoint load time
from ultralytics import YOLO
model = YOLO("runs/detect/train/weights/best.pt")
metrics = model.val()カスタムメトリクスによる最適なモデルの保存#
トレーナーは適応度(fitness)に基づいて best.pt を保存します。検出の場合、デフォルトは mAP@0.5:0.95 です([P, R, mAP@0.5, mAP@0.5:0.95] に対して [0.0, 0.0, 0.0, 1.0] の重みが与えられます)。mAP@0.5 や再現率などの別のメトリクスを使用するには、validate() をオーバーライドし、選択したメトリクスを適応度の値として返します。その後、組み込みの 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) | 適合率 |
metrics/recall(B) | 再現率 |
metrics/mAP50(B) | IoU 0.5におけるmAP |
metrics/mAP50-95(B) | IoU 0.5:0.95におけるmAP |
バックボーンのフリーズと解除#
転移学習のワークフローでは、最初のNエポックの間は事前学習済みのバックボーンを凍結し、ネットワーク全体をファインチューニングする前に検出ヘッドを適応させると効果的です。Ultralytics はトレーニング開始時にレイヤーを凍結する freeze パラメータを提供しており、コールバックを使用して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レイヤー(インデックス0〜9)を凍結します。これは YOLO26 バックボーンの大部分をカバーしています。バックボーンはレイヤー0〜10に及ぶため、freeze=10 を使用すると最後の C2PSA ブロック(レイヤー10)がトレーニング可能な状態で残ります。バックボーン全体を凍結するには freeze=11 を使用してください。on_train_epoch_start コールバックは各エポックの開始時に実行され、凍結期間が完了するとすべてのパラメータの凍結を解除します。
freeze=10は、最初の10レイヤー(インデックス0〜9、YOLO26 バックボーンの大半)を凍結します(レイヤー10にある最後の C2PSA ブロックを含めるにはfreeze=11を使用してください)freeze=[0, 1, 2, 3]は特定のレイヤーをインデックスで凍結しますFREEZE_EPOCHSの値を大きくすると、バックボーンが変更される前にヘッドが適応するための時間を増やすことができます
レイヤーごとの学習率#
ネットワークの異なる部分は、異なる学習率から恩恵を受けることができます。一般的な戦略は、学習済みの特徴を保持するために事前学習済みのバックボーンには低い学習率を使用し、より高い学習率によって検出ヘッドを迅速に適応させることです。
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 = []
unwrapped = unwrap_model(model)
backbone_len = len(unwrapped.yaml["backbone"]) # YOLO26 backbone spans layers 0-10 (C2PSA at layer 10)
for k, v in unwrapped.named_parameters():
if not v.requires_grad:
continue
is_backbone = any(k.startswith(f"model.{i}.") for i in range(backbone_len))
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)RT-DETRバリエーション#
RT-DETR の場合も、2つの調整点を除けばパターンは同じです。バックボーンの長さは model.yaml["backbone"] から読み取られるため、レイヤー数をハードコーディングすることなく、同じトレーナーを RT-DETR のバリエーション(RT-DETR-L、RT-DETR-X、ResNet-50/101 バックボーン)全体で利用できます。また、各セクション内でパラメータは重み、BatchNorm、バイアスグループに分割され、デフォルトのトレーナーの方針に合わせて 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 であり、これは HGNetV2 バックボーンを持つオリジナルの RT-DETR の設定と一致します。文献によると、比率はバックボーンのサイズと事前学習データの規模に反比例してスケーリングすることが推奨されています。非常に大規模なデータセットで事前学習された大規模なバックボーン(例えば、数億枚の画像で DINO、CLIP、または MAE を用いて訓練された ViT-L/H)では、十分に学習された特徴を維持するために 0.01 以下のような小さな比率が通常使用されます。一方、軽量な事前学習を持つ小型のバックボーンは、0.5 以上の大きな比率を許容します。
組み込みの学習率スケジューラー(cosine または linear)は、グループごとのベース学習率に対して引き続き適用されます。バックボーンとヘッドの両方の学習率は同じ減衰スケジュールに従い、トレーニング中を通じてそれらの間の比率が維持されます。
これらのカスタマイズは、複数のメソッドをオーバーライドし、必要に応じてコールバックを追加することで、単一のトレーナークラスにまとめることができます。
マルチGPUトレーニングのための同期BatchNorm#
DistributedDataParallel を使用して複数のGPUでトレーニングする場合、デフォルトの BatchNorm2d レイヤーは各GPU上で独立して統計情報を計算します。RT-DETR のファインチューニングや、GPUあたりのバッチサイズが小さいその他のレシピでは、GPUごとのバッチ統計情報にノイズが含まれることがあります。PyTorch の SyncBatchNorm は、すべてのランク間で平均と分散を同期させて単一のグローバルなバッチ統計情報を算出するため、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 でトレーニングが続行されます。親クラスを DetectionTrainer に切り替えることで、YOLO でも同様のパターンが機能します。
| シナリオ | 推奨 |
|---|---|
| マルチGPUトレーニング、GPUあたりバッチサイズが小さい (≤ 16) | 有効化 |
| マルチGPUトレーニング、GPUあたりバッチサイズが大きい (≥ 32) | オプション(微小なメリット) |
| 単一GPUトレーニング | 該当なし(スキップ) |
設定可能な勾配クリッピング#
デフォルトのトレーナーは、optimizer_step() 内で勾配を max_norm=10.0 にクリップします。これは、YOLO モデルでは勾配がそれを超えることがめったにないため調整された緩い値です。RT-DETR、DEIM、DINO などの DETR ファミリーの検出器は通常、勾配の大きさが急上昇する可能性のあるデコーダーのクロスアテンション層を安定させるために、0.1 のようなはるかに厳しい値を使用します。クリップ値を上書きするには、トレーナーをサブクラス化し、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)親クラスを DetectionTrainer(from ultralytics.models.yolo.detect import DetectionTrainer)に切り替え、YOLO("yolo26n.pt") を使用して YOLO チェックポイントをロードすることで、YOLO でも同じトレーナーが機能します。optimizer_step の本体は変更されません。
| アーキテクチャファミリー | 一般的な max_norm |
|---|---|
| RT-DETR / DEIM / DETRファミリー | 0.1 |
| YOLO (Ultralyticsデフォルト) | 10.0 |
| クリッピングを無効化 | 0 |
よくある質問 (FAQ)#
カスタム トレーナー クラス (インスタンスではなく) を
model.train()内のtrainerパラメータに渡します:from ultralytics import YOLO model = YOLO("yolo26n.pt") model.train(data="coco8.yaml", trainer=MyCustomTrainer)YOLOクラスは、内部でトレーナーのインスタンス化を処理します。トレーナーアーキテクチャの詳細については、高度なカスタマイズページを参照してください。カスタマイズ可能な主要メソッド:
メソッド 目的 validate()バリデーションを実行し、メトリクスを返します build_optimizer()オプティマイザを構築します save_model()トレーニングのチェックポイントを保存します get_model()モデルインスタンスを返します get_validator()バリデータインスタンスを返します get_dataloader()データローダを構築します preprocess_batch()入力バッチを前処理します label_loss_items()ログ出力用にロス項目をフォーマットします 完全な API リファレンスについては、
BaseTrainerのドキュメントを参照してください。はい、より簡単なカスタマイズであれば、コールバックで十分に対応できることがよくあります。利用可能なコールバックイベントには、
on_train_start、on_train_epoch_start、on_train_epoch_end、on_fit_epoch_end、およびon_model_saveが含まれます。これらを使用することで、サブクラス化を行わずにトレーニングループにフックインできます。上記のバックボーン凍結の例はこのアプローチを示しています。損失ゲインの調整など、変更がよりシンプルな場合は、ハイパーパラメータを直接変更できます。
model.train(data="coco8.yaml", box=10.0, cls=1.5, dfl=2.0)クラスの重みの追加など、損失に対する構造的な変更を行うには、クラスの重みのセクションに示されているように、損失とモデルをサブクラス化する必要があります。