YOLO Vision 2026:

自定义 Trainer#

Ultralytics 训练流程围绕 BaseTrainer 以及 DetectionTrainer 等任务专用 Trainer 构建。这些类开箱即用地处理训练循环、验证、检查点保存和日志记录。当你需要更多控制时——例如跟踪自定义指标、调整损失权重或实现学习率调度——可以继承 Trainer 并重写特定方法。

本指南将介绍七种常见的自定义方式:

  1. 在每个轮次结束时记录自定义指标(F1 分数)
  2. 添加类别权重以处理类别不平衡
  3. 根据不同指标保存最佳模型
  4. 前 N 个轮次冻结主干网络,然后取消冻结
  5. 指定逐层学习率
  6. 跨 GPU 同步 BatchNorm以进行多 GPU 训练
  7. 配置梯度裁剪以调节训练稳定性
前置条件

阅读本指南前,请确保你熟悉训练 YOLO 模型的基础知识,以及介绍 BaseTrainer 架构的高级自定义页面。

自定义 Trainer 的工作原理#

YOLO 模型类在 train() 方法中接受 trainer 参数。这样你就可以传入一个扩展默认行为的自定义 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)

你的自定义 Trainer 会继承 DetectionTrainer 的全部功能,因此只需重写你想要自定义的特定方法。

记录自定义指标#

验证步骤会计算精确率召回率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

            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)

这会在每次验证运行后记录验证集中所有类别的平均 F1 分数以及逐类别明细。

可用指标

验证器通过 self.validator.metrics.box 提供对许多指标的访问:

属性描述
f1逐类别 F1 分数
image_metrics逐图像指标字典,其中包含精确率、召回率、F1、TP、FP 和 FN
p逐类别精确率
r逐类别召回率
ap50IoU 为 0.5 时的逐类别 AP
apIoU 为 0.5:0.95 时的逐类别 AP
mpmr平均精确率和召回率
map50map平均 AP 指标

添加类别权重#

0.01.0 之间设置 cls_pw,即可将归一化的逆频率权重应用于分类损失。只有在需要手动指定比例时,才应重写现有的权重计算:

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() 会将这些值归一化为均值 1.0,并将其存储在模型上,由现有检测损失应用这些权重。上述索引要求数据集至少包含两个类别。

根据自定义指标保存最佳模型#

Trainer 根据适应度保存 best.pt;对于检测任务,适应度默认为 mAP@0.5:0.95(使用权重 [0.0, 0.0, 0.0, 1.0] 计算 [P, R, mAP@0.5, mAP@0.5:0.95])。要使用其他指标(例如 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."""
        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() 会使用默认指标更新 best_fitness,因此请在调用它之前保存其先前的值。

可用指标

验证后,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 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)

freeze=10 参数会在训练开始时冻结前 10 层(索引 0-9),这涵盖了 YOLO26 主干网络的大部分层。主干网络跨越第 0-10 层,因此 freeze=10 会保留最后的 C2PSA 模块(第 10 层)可训练;使用 freeze=11 可冻结整个主干网络。on_train_epoch_start 回调会在每个轮次开始时触发,并在冻结期结束后取消冻结指定层,同时保留永久冻结的 DFL 和蒸馏教师参数。

选择要冻结的内容
  • freeze=10 会冻结前 10 层,即索引 0-9(YOLO26 主干网络的大部分层;使用 freeze=11 可包含第 10 层的最后一个 C2PSA 模块)
  • freeze=[0, 1, 2, 3] 按索引冻结指定层
  • 较高的 FREEZE_EPOCHS 值会让检测头在主干网络发生变化前拥有更多适应时间

逐层学习率#

网络的不同部分可能适合使用不同的学习率。一种常见策略是为预训练主干网络使用较低的学习率,以保留已学习的特征,同时允许检测头使用较高的学习率更快地适应:

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 变体#

对于 RT-DETR,使用相同的重写方式,将 RTDETRTrainer 作为父类,并使用 RTDETR("rtdetr-l.pt") 加载检查点。

用于多 GPU 训练的同步 BatchNorm#

使用 DistributedDataParallel 在多个 GPU 上训练时,默认的 BatchNorm2d 层会在每个 GPU 上独立计算统计信息。对于 RT-DETR 微调以及其他使用较小单 GPU 批量大小的训练方案,单 GPU 批量统计可能会存在噪声。PyTorch 的 SyncBatchNorm 会跨所有 rank 同步均值和方差,从而获得单个全局批量统计值;这通常能改善收敛,但会增加少量 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 保护条件确保 Trainer 在单 GPU 运行中也能安全使用;在单 GPU 上会跳过转换,并使用常规的 BatchNorm2d 继续训练。将父类切换为 DetectionTrainer 后,同样的模式也适用于 YOLO。

何时使用 SyncBatchNorm
场景建议
多 GPU 训练,单 GPU 批量较小(≤ 16)启用
多 GPU 训练,单 GPU 批量较大(≥ 32)可选;收益较小
单 GPU 训练不适用(跳过)

可配置的梯度裁剪#

默认 Trainer 会在 optimizer_step() 中将梯度裁剪到 max_norm=10.0,这是针对 YOLO 模型调优的宽松值,因为梯度很少超过该值。DETR 系列检测器(RT-DETR、DEIM、DINO)通常使用 0.1 等更严格的值,以稳定解码器的交叉注意力层,因为这些层的梯度幅度可能会突然增大。要重写裁剪值,请继承 Trainer 并重写 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)

将父类切换为 DetectionTrainerfrom ultralytics.models.yolo.detect import DetectionTrainer)并使用 YOLO("yolo26n.pt") 加载 YOLO 检查点后,同一个 Trainer 也适用于 YOLO。optimizer_step 的主体无需更改。

典型的 `clip_grad_norm` 值
架构系列典型的 max_norm
RT-DETR / DEIM / DETR 系列0.1
YOLO(Ultralytics 默认值)10.0
禁用裁剪0

常见问题#

  • 将自定义 Trainer 类(而非实例)传入 model.train() 中的 trainer 参数:

    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)

    YOLO 类会在内部处理 Trainer 实例化。有关 Trainer 架构的更多详情,请参阅高级自定义页面。

  • 可用于自定义的关键方法:

    方法用途
    validate()运行验证并返回指标
    build_optimizer()构建优化器
    save_model()保存训练检查点
    get_model()返回模型实例
    get_validator()返回验证器实例
    get_dataloader()构建数据加载器
    preprocess_batch()预处理输入批次
    label_loss_items()格式化用于日志记录的损失项

    完整的 API 参考请参阅 BaseTrainer 文档

  • 可以。对于更简单的自定义,回调通常就足够了。可用的回调事件包括 on_train_starton_train_epoch_starton_train_epoch_endon_fit_epoch_endon_model_save。这些事件允许你接入训练循环,而无需继承 Trainer。上面的主干网络冻结示例展示了这种方法。

  • 如果改动较简单(例如调整损失增益),你可以直接修改超参数

    from ultralytics import YOLO
    
    model = YOLO("yolo26n.pt")
    model.train(data="coco8.yaml", box=10.0, cls=1.5, dfl=2.0)

    在 YOLO26 上,dfl 会缩放记录的 l1_loss,因为其检测头使用 reg_max: 1;对于使用 reg_max > 1 的模型,它会缩放 dfl_loss

评论