Ultralytics YOLO27: Tham gia danh sách chờ.
Get Started

Tùy chỉnh Trainer#

Pipeline huấn luyện của Ultralytics được xây dựng xoay quanh BaseTrainer và các trainer dành riêng cho từng tác vụ như DetectionTrainer. Các class này xử lý sẵn vòng lặp huấn luyện, validation, lưu checkpoint và ghi log. Khi cần kiểm soát nhiều hơn — theo dõi metric tùy chỉnh, điều chỉnh trọng số loss hoặc triển khai lịch learning rate — bạn có thể kế thừa trainer và ghi đè các phương thức cụ thể.

Hướng dẫn này trình bày bảy cách tùy chỉnh phổ biến:

  1. Ghi log metric tùy chỉnh (điểm F1) vào cuối mỗi epoch
  2. Thêm trọng số class để xử lý tình trạng mất cân bằng class
  3. Lưu model tốt nhất dựa trên một metric khác
  4. Đóng băng backbone trong N epoch đầu, sau đó mở băng
  5. Chỉ định learning rate cho từng layer
  6. Đồng bộ BatchNorm trên các GPU để huấn luyện đa GPU
  7. Cấu hình gradient clipping để tinh chỉnh độ ổn định
Điều kiện tiên quyết

Trước khi đọc hướng dẫn này, hãy đảm bảo bạn nắm được kiến thức cơ bản về huấn luyện model YOLO và trang Tùy chỉnh nâng cao, trong đó trình bày kiến trúc BaseTrainer.

Cách hoạt động của Custom Trainer#

Lớp model YOLO nhận tham số trainer trong phương thức train(). Điều này cho phép bạn truyền vào lớp trainer riêng kế thừa hành vi mặc định:

from ultralytics import YOLO
from ultralytics.models.yolo.detect import DetectionTrainer

class CustomTrainer(DetectionTrainer):
    """A custom trainer that extends DetectionTrainer with additional functionality."""

    # Thêm các tùy chỉnh của bạn tại đây

model = YOLO("yolo26n.pt")
model.train(data="coco8.yaml", epochs=10, trainer=CustomTrainer)

Custom trainer của bạn kế thừa toàn bộ chức năng từ DetectionTrainer, vì vậy bạn chỉ cần ghi đè các phương thức cụ thể muốn tùy chỉnh.

Ghi log metric tùy chỉnh#

Bước validation tính toán precision, recall và mAP. Nếu cần các metric bổ sung như F1 score theo từng class, hãy ghi đè 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)

Sau mỗi lần validation, thao tác này ghi log F1 score trung bình trên tất cả class xuất hiện trong tập validation và bảng phân tích theo từng class.

Các metric hiện có

Validator cung cấp quyền truy cập vào nhiều metric thông qua self.validator.metrics.box:

Thuộc tínhMô tả
f1F1 score cho từng class
image_metricsDictionary metric cho từng ảnh gồm precision, recall, F1, TP, FP và FN
pPrecision cho từng class
rRecall cho từng class
ap50AP tại IoU 0.5 cho từng class
apAP tại IoU 0.5:0.95 cho từng class
mp, mrPrecision và recall trung bình
map50, mapCác metric mAP trung bình

Thêm trọng số cho class#

Đặt cls_pw trong khoảng từ 0.0 đến 1.0 để áp dụng trọng số nghịch đảo tần suất đã chuẩn hóa cho classification loss. Chỉ ghi đè phép tính trọng số hiện có khi bạn cần các tỷ lệ được chỉ định thủ công:

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() chuẩn hóa các giá trị này về giá trị trung bình 1.0 và lưu chúng trong model, nơi detection loss hiện có sẽ áp dụng. Các chỉ số ở trên yêu cầu dataset có ít nhất hai class.

Lưu model tốt nhất theo metric tùy chỉnh#

Trainer lưu best.pt dựa trên fitness; với detection, mặc định là mAP@0.5:0.95 (trọng số [0.0, 0.0, 0.0, 1.0] cho [P, R, mAP@0.5, mAP@0.5:0.95]). Để sử dụng metric khác (chẳng hạn mAP@0.5 hoặc recall), hãy ghi đè validate() và trả về metric đã chọn làm giá trị fitness. Sau đó, save_model() tích hợp sẵn sẽ tự động sử dụng giá trị này:

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() cập nhật best_fitness bằng metric mặc định, vì vậy hãy lưu lại giá trị trước đó trước khi gọi phương thức này.

Các metric hiện có

Các metric phổ biến có trong self.metrics sau validation gồm:

KhóaMô tả
metrics/precision(B)Precision
metrics/recall(B)Recall
metrics/mAP50(B)mAP tại IoU 0.5
metrics/mAP50-95(B)mAP tại IoU 0.5:0.95

Đóng băng và mở băng backbone#

Các quy trình transfer learning thường được hưởng lợi từ việc đóng băng backbone đã pretrained trong N epoch đầu, cho phép detection head thích nghi trước khi fine-tuning toàn bộ mạng. Ultralytics cung cấp tham số freeze để đóng băng các layer khi bắt đầu huấn luyện; bạn có thể dùng callback để mở băng chúng sau 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)

Tham số freeze=10 đóng băng 10 layer đầu tiên (chỉ số 0-9) khi bắt đầu huấn luyện, bao gồm phần lớn backbone YOLO26. Backbone trải dài từ layer 0-10, vì vậy freeze=10 giữ block C2PSA cuối cùng (layer 10) ở trạng thái có thể huấn luyện; dùng freeze=11 để đóng băng toàn bộ backbone. Callback on_train_epoch_start được gọi vào đầu mỗi epoch và mở băng các layer đã chỉ định sau khi hết thời gian đóng băng, đồng thời giữ nguyên các tham số DFL và distillation-teacher được đóng băng vĩnh viễn.

Chọn layer cần đóng băng
  • freeze=10 đóng băng 10 layer đầu tiên, chỉ số 0-9 (phần lớn backbone YOLO26; dùng freeze=11 để bao gồm block C2PSA cuối cùng ở layer 10)
  • freeze=[0, 1, 2, 3] đóng băng các layer cụ thể theo chỉ số
  • Giá trị FREEZE_EPOCHS cao hơn giúp head có thêm thời gian thích nghi trước khi backbone thay đổi

Learning rate theo từng layer#

Các phần khác nhau của mạng có thể đạt hiệu quả tốt hơn với learning rate khác nhau. Một chiến lược phổ biến là dùng learning rate thấp hơn cho backbone đã pretrained để bảo toàn các feature đã học, đồng thời cho phép detection head thích nghi nhanh hơn với learning rate cao hơn:

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)

Biến thể RT-DETR#

Với RT-DETR, hãy dùng cùng cách ghi đè với RTDETRTrainer làm lớp cha và tải checkpoint bằng RTDETR("rtdetr-l.pt").

BatchNorm đồng bộ khi huấn luyện đa GPU#

Khi huấn luyện trên nhiều GPU bằng DistributedDataParallel, các layer BatchNorm2d mặc định tính toán thống kê độc lập trên từng GPU. Với fine-tuning RT-DETR và các cấu hình khác dùng batch size nhỏ trên mỗi GPU, thống kê theo từng GPU có thể nhiễu. SyncBatchNorm của PyTorch đồng bộ mean và variance trên tất cả rank để tạo một thống kê batch toàn cục, thường giúp cải thiện hội tụ với chi phí là một lượng nhỏ overhead giao tiếp giữa các GPU.

Việc chuyển đổi phải diễn ra sau khi model được đưa lên GPU nhưng trước khi DDP bọc model. Hook phù hợp nhất cho việc này là set_model_attributes(), được BaseTrainer gọi đúng trong khoảng thời gian đó:

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)

Điều kiện world_size > 1 đảm bảo trainer cũng an toàn khi dùng trong các lần chạy một GPU; trên một GPU, thao tác chuyển đổi được bỏ qua và quá trình huấn luyện tiếp tục với BatchNorm2d thông thường. Có thể áp dụng cách tương tự cho YOLO bằng cách đổi lớp cha thành DetectionTrainer.

Khi nào nên dùng SyncBatchNorm
Kịch bảnKhuyến nghị
Huấn luyện đa GPU, batch size nhỏ trên mỗi GPU (≤ 16)Bật
Huấn luyện đa GPU, batch size lớn trên mỗi GPU (≥ 32)Không bắt buộc; lợi ích nhỏ
Huấn luyện một GPUKhông áp dụng (bỏ qua)

Gradient clipping có thể cấu hình#

Trainer mặc định clip gradient ở mức max_norm=10.0 trong optimizer_step(), một giá trị khá rộng được tinh chỉnh cho các model YOLO, vốn hiếm khi có gradient vượt mức này. Các detector thuộc họ DETR (RT-DETR, DEIM, DINO) thường dùng giá trị chặt hơn nhiều, chẳng hạn 0.1, để ổn định các layer cross-attention của decoder, nơi độ lớn gradient có thể tăng đột biến. Để ghi đè giá trị clip, hãy tạo subclass của trainer và ghi đè 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  # norm gradient tối đa; đặt thành 0 để tắt 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)

Trainer này cũng dùng được cho YOLO bằng cách đổi lớp cha thành DetectionTrainer (from ultralytics.models.yolo.detect import DetectionTrainer) và tải checkpoint YOLO bằng YOLO("yolo26n.pt"). Phần thân optimizer_step không thay đổi.

Các giá trị `clip_grad_norm` thường dùng
Họ kiến trúcGiá trị max_norm thường dùng
Họ RT-DETR / DEIM / DETR0.1
YOLO (mặc định của Ultralytics)10.0
Tắt clipping0

Câu hỏi thường gặp#

  • Truyền lớp custom trainer của bạn (không phải một instance) vào tham số trainer trong model.train():

    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)

    Lớp YOLO tự xử lý việc khởi tạo trainer. Xem trang Tùy chỉnh nâng cao để biết thêm chi tiết về kiến trúc trainer.

  • Các phương thức chính có thể tùy chỉnh:

    Phương thứcMục đích
    validate()Chạy validation và trả về metric
    build_optimizer()Khởi tạo optimizer
    save_model()Lưu checkpoint huấn luyện
    get_model()Trả về instance của model
    get_validator()Trả về instance của validator
    get_dataloader()Xây dựng dataloader
    preprocess_batch()Tiền xử lý batch đầu vào
    label_loss_items()Định dạng các thành phần loss để ghi log

    Để xem tài liệu tham khảo API đầy đủ, hãy xem tài liệu BaseTrainer.

  • Có, với các tùy chỉnh đơn giản hơn, callback thường là đủ. Các event callback hiện có gồm on_train_start, on_train_epoch_start, on_train_epoch_end, on_fit_epoch_end và on_model_save. Những event này cho phép bạn móc nối vào vòng lặp huấn luyện mà không cần tạo subclass. Ví dụ đóng băng backbone ở trên minh họa cách tiếp cận này.

  • Nếu thay đổi của bạn đơn giản hơn (chẳng hạn điều chỉnh các hệ số loss), bạn có thể sửa trực tiếp hyperparameter:

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

    Trên YOLO26, dfl scale l1_loss được ghi log vì detection head sử dụng reg_max: 1; trên các model có reg_max > 1, tham số này scale dfl_loss.

Bình luận