自定义 Trainer#
Ultralytics 训练流程以 BaseTrainer 为核心,并配有 DetectionTrainer 等任务专用 Trainer。这些类开箱即用地处理训练循环、验证、检查点保存和日志记录。如果你需要更多控制权——例如跟踪自定义指标、调整损失权重或实现学习率计划——可以继承 Trainer 类并重写特定方法。
本指南将介绍七种常见的自定义方法:
- 在每个轮次结束时记录自定义指标(F1 分数)
- 添加类别权重以处理类别不平衡
- 基于不同指标保存最佳模型
- 冻结骨干网络前 N 个 epoch,然后解冻
- 指定各层的学习率
- 在多个 GPU 间同步 BatchNorm,用于多 GPU 训练
- 配置梯度裁剪,用于稳定性调优
阅读本指南前,请确保你熟悉训练 YOLO 模型的基础知识,以及介绍 BaseTrainer 架构的高级自定义页面。
自定义训练器的工作原理#
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."""
# 在此处添加你的自定义内容
model = YOLO("yolo26n.pt")
model.train(data="coco8.yaml", epochs=10, trainer=CustomTrainer)你的自定义训练器继承了 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 | 各类别的召回率 |
ap50 | 各类别在 IoU 为 0.5 时的 AP |
ap | 各类别在 IoU 为 0.5:0.95 时的 AP |
mp, mr | 平均精确率和召回率 |
map50, map | 平均 AP 指标 |
添加类别权重#
将 cls_pw 设置在 0.0 和 1.0 之间,即可为分类损失应用归一化的逆频率权重。只有在需要手动指定比例时,才重写现有的权重计算:
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() 会将这些值归一化为均值 1.0,并将其存储在模型上,现有的检测损失会应用这些值。上述索引要求数据集至少包含两个类别。
按自定义指标保存最佳模型#
训练器根据适应度保存 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."""
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 个 epoch 冻结预训练骨干网络,让检测头先进行适配,然后再对整个网络进行微调。Ultralytics 提供了 freeze 参数,可在训练开始时冻结层;你还可以使用回调在 N 个 epoch 后解冻这些层:
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 回调会在每个 epoch 开始时触发,并在冻结期结束后解冻指定的层,同时保持 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 守卫可确保训练器在单 GPU 运行时也能安全使用;在单 GPU 上会跳过转换,并使用常规的 BatchNorm2d 继续训练。将父类切换为 DetectionTrainer 后,同样的做法也适用于 YOLO。
| 场景 | 建议 |
|---|---|
| 多 GPU 训练,每个 GPU 的批次较小(≤ 16) | 启用 |
| 多 GPU 训练,每个 GPU 的批次较大(≥ 32) | 可选;收益较小 |
| 单 GPU 训练 | 不适用(跳过) |
可配置的梯度裁剪#
默认训练器会在 optimizer_step() 中将梯度裁剪到 max_norm=10.0;这是针对 YOLO 模型调优的宽松阈值,梯度很少会超过该值。DETR 系列检测器(RT-DETR、DEIM、DINO)通常使用更严格的阈值,例如 0.1,以稳定解码器的交叉注意力层,因为这些层的梯度幅值可能会突然增大。若要覆盖裁剪值,请创建训练器子类并重写 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 # 最大梯度范数;设为 0 可禁用裁剪
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)将父类切换为 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 |
常见问题#
将自定义训练器类(而非实例)作为
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类会在内部处理训练器实例化。有关训练器架构的更多详情,请参阅高级自定义页面。可用于自定义的关键方法:
方法 用途 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。这些事件让你无需创建训练器子类,就能接入训练循环。上面的骨干网络冻结示例展示了这种做法。如果你的更改较简单(例如调整损失增益),可以直接修改超参数:
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。