定制训练师
Ultralytics 管道围绕以下内容构建: BaseTrainer 以及任务特定的训练师,例如 DetectionTrainer这些类开箱即用即可处理训练循环、验证、检查点保存和日志记录。当您需要更多控制权——例如追踪自定义指标、调整损失权重或实现学习率调度——可以继承训练器并重写特定方法。
本指南将逐步介绍五种常见的自定义操作:
定制训练师如何运作
字段 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因此,您只需重写需要自定义的特定方法即可。
记录自定义指标
字段 验证 步长计算 精度, 召回率和 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分数 |
p | 每类精度 |
r | 每班召回 |
ap50 | AP 课程AP IoU .5 |
ap | IoU .AP :每类0.95 |
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]
通过自定义指标保存最佳模型
教练保存 best.pt 基于健身水平,默认值为 0.9 × mAP@0.5:0.95 + 0.1 × mAP@0.5. 使用另一种度量标准(例如 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 .5时 |
metrics/mAP50-95(B) | IoU .5时:0.95 |
冻结与解冻主干
迁移学习 工作流通常受益于在前N个 epoch 冻结预训练的主体网络,使检测头能够在后续阶段进行适应性调整。 微调 整个网络。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层(主干)。 on_train_epoch_start 回调函数在每个训练 epoch 的开始时触发,并在冻结期结束后解冻所有参数。
选择冷冻什么
freeze=10冻结前10层(通常是YOLO 中的主干网络)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 = []
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)
学习率调度器
内置的学习率调度器(cosine 或 linear在每组基础学习率之上,主干网络和头部网络的学习率仍将适用。两者的学习率将遵循相同的衰减计划,在整个训练过程中保持它们之间的比例关系。
技术组合
这些定制功能可通过重写多个方法并按需添加回调函数,整合为单一的训练器类。
常见问题
如何YOLO传递自定义训练器?
将您的自定义训练器类(非实例)传递给 trainer 参数中指定。 model.train():
from ultralytics import YOLO
model = YOLO("yolo26n.pt")
model.train(data="coco8.yaml", trainer=MyCustomTrainer)
字段 YOLO 类内部处理训练器的实例化。参见 高级自定义 有关训练器架构的更多详细信息,请参阅相关页面。
我可以重写哪些 BaseTrainer 方法?
可用的主要定制方法:
| 方法 | 目的 |
|---|---|
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)
若需对损失函数进行结构性修改(例如添加类权重),则需参照类权重章节所述方式,对损失函数和模型进行子类化操作。