YOLO11 源码阅读实战:从 YOLO() 到训练、验证和预测的完整调用链

文章摘要

使用 YOLO11 训练数据集并不难,真正开始修改网络结构后,很多问题却不是一条训练命令能够解释的。例如:模型 YAML 在哪里被解析、自定义模块为什么提示找不到、model.train() 调用了哪个训练器、预测结果又是在哪一步经过 NMS。本篇基于 Ultralytics 8.3.253 源码,从 YOLO() 入口出发,梳理目标检测任务的模型加载、网络构建、数据读取、训练、验证和预测调用链,为后续添加注意力、替换 Neck、修改检测头与损失函数建立统一的源码定位方法。

CSDN 标签

  • YOLO11
  • Ultralytics
  • PyTorch
  • 目标检测
  • 源码分析
  • 深度学习
  • Python
  • 模型训练

前言

上一篇已经把 YOLO11 自定义数据集从准备、训练到导出的流程跑通。接下来如果要正式进入模型改进,仅仅会写下面两行代码还不够:

from ultralytics import YOLO

model = YOLO("yolo11n.pt")
model.train(data="dataset.yaml", epochs=100)

表面上看,训练从 model.train() 开始;实际上,程序还要完成任务识别、权重加载、训练器选择、YAML 解析、网络实例化、数据集构建和验证器创建等工作。

这篇文章不逐行翻译整个项目,而是回答几个后续改进最常遇到的问题:

  1. YOLO("yolo11n.pt") 创建的到底是什么对象?
  2. .pt 权重和 .yaml 配置的加载路径有什么区别?
  3. 目标检测任务为什么会自动选择 DetectionTrainer
  4. YAML 中的模块名称在哪里转换成 Python 类?
  5. 训练、验证和预测分别由哪些文件负责?
  6. 添加新模块时,应该优先修改哪些位置?

本文固定使用 Ultralytics 8.3.253 源码。不同版本的目录和解析逻辑可能发生变化,后续文章也继续以这个版本为准。

一、先认识真正需要关注的源码目录

Ultralytics 项目文件很多,但目标检测改进并不需要从头读到尾。先把与执行链直接相关的目录挑出来:

ultralytics/
├── cfg/
│   ├── __init__.py          # yolo 命令入口与参数解析
│   ├── default.yaml         # 默认训练、验证、预测参数
│   └── models/11/           # YOLO11 模型 YAML
├── data/
│   ├── build.py             # 创建 Dataset 和 DataLoader
│   ├── dataset.py           # YOLODataset
│   └── augment.py           # 数据增强流程
├── engine/
│   ├── model.py             # 统一的 train、val、predict、export 接口
│   ├── trainer.py           # 通用训练循环
│   ├── validator.py         # 通用验证流程
│   └── predictor.py         # 通用推理流程
├── models/yolo/
│   ├── model.py             # YOLO 类和 task_map
│   └── detect/
│       ├── train.py         # DetectionTrainer
│       ├── val.py           # DetectionValidator
│       └── predict.py       # DetectionPredictor
└── nn/
    ├── modules/             # Conv、C3k2、C2PSA、Detect 等模块
    └── tasks.py             # DetectionModel、parse_model

可以把这些文件分成三层:

  • engine/:提供所有任务共用的训练、验证和预测框架。
  • models/yolo/detect/:补充目标检测任务特有的实现。
  • nn/:负责神经网络模块和 YAML 到模型的构建过程。

后续改进时,先判断修改属于哪一层,可以少走很多弯路。例如添加卷积模块通常不需要重写训练循环,而替换损失函数也不应该只盯着模型 YAML。

二、Python API 的第一站:YOLO 类

我们通常从下面这句代码开始:

model = YOLO("yolo11n.pt")

YOLO 类位于:

ultralytics/models/yolo/model.py

在 8.3.253 中,YOLO 继承自 ultralytics.engine.model.Model。它会先检查模型名称是否属于 YOLO-World、YOLOE 等特殊类型;普通的 yolo11n.pt 会继续进入父类初始化。

可以把这一层理解为“任务总入口”。它本身不负责写完整训练循环,而是根据当前任务,把请求交给对应的模型、训练器、验证器和预测器。

目标检测任务在 task_map 中的对应关系为:

"detect": {
    "model": DetectionModel,
    "trainer": yolo.detect.DetectionTrainer,
    "validator": yolo.detect.DetectionValidator,
    "predictor": yolo.detect.DetectionPredictor,
}

这段映射非常关键。调用 train()val()predict() 时,框架不需要在各处写一长串任务判断,而是根据 self.tasktask_map 取出正确的类。

对应的选择动作由 engine/model.py 中的 _smart_load() 完成:

return self.task_map[self.task][key]

其中 key 可以是:

  • model
  • trainer
  • validator
  • predictor

所以目标检测调用 _smart_load("trainer") 时,得到的是 DetectionTrainer;分割任务执行相同接口时,得到的则是 SegmentationTrainer

三、加载 .pt 和读取 .yaml 不是同一条路

理解这一点,对后面修改 YAML 特别重要。

1. 加载预训练权重

model = YOLO("yolo11n.pt")

当输入以 .pt 结尾时,Model 会进入 _load(),通过 load_checkpoint() 恢复权重及其模型信息。任务类型、模型参数和原始 YAML 信息也会从检查点中读取。

这条路径的重点是“恢复已经存在的模型”。

2. 根据 YAML 新建模型

model = YOLO("yolo11n.yaml")

当输入是模型配置文件时,程序进入 _new()

读取 YAML
  -> 推断任务类型
  -> 从 task_map 取得 DetectionModel
  -> 根据 YAML 构建新网络

这条路径只定义网络结构,并不会自动获得预训练参数。如果希望使用自定义 YAML 并加载预训练权重,可以再调用 load(),或者让训练入口将兼容的权重加载到新模型中。

因此,测试自定义结构时要先确认自己用的是 .yaml 还是 .pt。如果一直加载旧的 .pt,新写的 YAML 很可能根本没有进入构建流程。

四、YAML 如何变成 PyTorch 网络

目标检测网络类位于:

ultralytics/nn/tasks.py

DetectionModel 初始化时会读取配置,并执行:

self.model, self.save = parse_model(
    deepcopy(self.yaml),
    ch=ch,
    verbose=verbose,
)

这里的 parse_model() 是后续改进必须理解的核心函数。它遍历 YAML 中的 backbonehead,将每一行描述转换为实际的 nn.Module

YOLO 模型 YAML 的基本行结构是:

[from, repeats, module, args]

例如:

- [-1, 2, C3k2, [256, false, 0.25]]

四个字段分别表示:

  • from:当前层读取哪一层的输出。
  • repeats:模块重复次数,会受到模型深度缩放影响。
  • module:模块类名称。
  • args:创建模块时使用的参数。

在 8.3.253 中,parse_model() 先解析模块名称:

m = (
    getattr(torch.nn, m[3:])
    if "nn." in m
    else getattr(__import__("torchvision").ops, m[16:])
    if "torchvision.ops." in m
    else globals()[m]
)

普通自定义模块通常会走到 globals()[m]。如果类没有正确导入 tasks.py 的全局命名空间,最常见的结果就是 KeyError

随后,base_modulesrepeat_modules 决定通道参数与重复次数如何插入:

base_modules
  负责处理常见的输入通道 c1 和输出通道 c2

repeat_modules
  在构造参数中插入内部重复次数 n

这也是为什么旧教程只写“导入模块并修改 YAML”,放到新版本中有时仍然不能运行。模块构造函数的参数约定如果与解析规则不一致,就会出现参数数量错误或通道不匹配。

五、model.train() 背后的训练调用链

现在回到最常用的训练代码:

from ultralytics import YOLO

model = YOLO("yolo11n.pt")
model.train(
    data="D:/datasets/SafetyHelmet/helmet.yaml",
    epochs=100,
    imgsz=640,
    batch=16,
    device=0,
)

从源码角度,可以把调用过程整理成下面这条主线:

YOLO("yolo11n.pt")
  -> Model._load()
  -> Model.train()
  -> _smart_load("trainer")
  -> DetectionTrainer
  -> DetectionTrainer.get_model()
  -> DetectionModel
  -> parse_model()
  -> DetectionTrainer.get_dataloader()
  -> build_yolo_dataset()
  -> BaseTrainer.train()
  -> BaseTrainer._do_train()

1. Model.train() 负责整理参数

engine/model.py 中的 train() 会合并多个参数来源:

模型已有 overrides
  + 方法默认值
  + 用户本次传入的 kwargs

右侧参数优先级更高,因此本次调用传入的 epochsbatchimgsz 等会覆盖默认配置。

参数整理完成后,程序通过 _smart_load("trainer") 选择 DetectionTrainer,再调用 trainer.get_model() 构建检测模型。训练结束后,框架会重新加载 best.pt;如果没有 best,则使用 last.pt,并更新当前 YOLO 对象中的模型和指标。

2. DetectionTrainer 负责检测任务细节

目标检测训练器位于:

ultralytics/models/yolo/detect/train.py

它继承通用的 BaseTrainer,主要补充检测任务需要的内容:

  • 创建 DetectionModel
  • 构建 YOLO 检测数据集。
  • 创建训练和验证 DataLoader。
  • 设置类别数量与类别名称。
  • 创建检测验证器。
  • 组织损失名称和结果绘图。

get_model() 中会使用数据集里的类别数覆盖模型默认类别数:

model = DetectionModel(
    cfg,
    nc=self.data["nc"],
    ch=self.data["channels"],
    verbose=verbose,
)

这说明模型输出类别数最终应以数据集配置为准。修改检测头后如果输出维度不正确,要同时检查 YAML、数据集 names 和 Detect 模块,而不是只改其中一个位置。

3. BaseTrainer 负责通用训练循环

ultralytics/engine/trainer.py 处理设备、优化器、学习率、混合精度、分布式训练、保存权重和验证调度等通用逻辑。

单卡或 CPU 训练会直接进入 _do_train();多卡模式则生成 DDP 命令并启动分布式进程。

所以添加一个即插即用注意力模块时,通常不需要修改 BaseTrainer。只有当训练策略本身发生变化,例如新增辅助损失、特殊数据输入或额外优化步骤时,才需要继续深入训练器。

六、数据集从哪里进入训练循环

DetectionTrainer.get_dataloader() 会先调用 build_dataset(),再创建 DataLoader:

DetectionTrainer.get_dataloader()
  -> DetectionTrainer.build_dataset()
  -> build_yolo_dataset()
  -> YOLODataset
  -> build_dataloader()

训练集和验证集在这里会采用不同策略:

  • train 默认允许打乱顺序,并启用训练增强。
  • val 使用验证模式,并按验证要求组织矩形批次等设置。
  • 分布式训练会确保缓存文件不会被多个进程同时重复创建。

如果后续需要修改 Mosaic、MixUp、颜色增强或自定义样本读取,应该重点查看 ultralytics/data/,而不是把逻辑塞进模型模块。

七、model.val() 如何选择验证器

验证入口仍然在 engine/model.py

metrics = model.val(
    data="D:/datasets/SafetyHelmet/helmet.yaml",
    imgsz=640,
    device=0,
)

Model.val() 通过 _smart_load("validator") 得到 DetectionValidator,然后执行验证并返回指标对象。

目标检测验证器位于:

ultralytics/models/yolo/detect/val.py

8.3.253 中,检测验证器使用从 0.500.95 的 10 个 IoU 阈值:

self.iouv = torch.linspace(0.5, 0.95, 10)

因此两个常见指标必须分开理解:

  • metrics.box.map50:IoU=0.50 时的 mAP。
  • metrics.box.map:IoU 0.50:0.95 范围取平均后的 mAP50-95。

不要把两者都写成“模型准确率”。验证器还负责混淆矩阵、类别统计、预测匹配和结果绘图,它输出的是一组检测评价指标,而不是单一准确率。

八、model.predict() 如何得到 Results

预测代码示例:

results = model.predict(
    source="test.jpg",
    conf=0.25,
    iou=0.7,
    save=True,
)

其调用链可以简化为:

Model.predict()
  -> _smart_load("predictor")
  -> DetectionPredictor
  -> 输入预处理
  -> 模型前向传播
  -> DetectionPredictor.postprocess()
  -> non_max_suppression()
  -> Results

DetectionPredictor.postprocess() 会读取 confiouclassesagnostic_nmsmax_det 等参数,对原始预测执行 NMS,最后把检测框、类别和置信度封装为 Results

这说明更换 NMS 与更换 Detect 检测头不是同一件事:

  • 检测头决定网络输出什么。
  • Predictor 的后处理决定如何筛选输出。

后续修改 Soft-NMS、DIoU-NMS 等策略时,需要明确改的是推理后处理,而不是训练网络主体。

九、yolo 命令和 Python API 最终走到同一套接口

源码安装后可以直接执行:

yolo detect train model=yolo11n.pt data=dataset.yaml epochs=100

pyproject.tomlyoloultralytics 两个命令都注册到:

ultralytics.cfg:entrypoint

命令行参数在 ultralytics/cfg/__init__.py 中解析,随后创建 YOLO 对象,并根据 mode 执行:

getattr(model, mode)(**overrides)

mode=train 时,本质上还是调用 model.train()mode=valmode=predict 也是同样的逻辑。

所以 CLI 和 Python API 不是两套独立训练框架。它们的主要区别是参数从哪里传入,底层最终进入相同的 Model 接口。

十、用 inspect 确认自己修改的是哪份源码

Windows 下很常见的情况是:明明修改了源码,训练结果却完全没有变化。原因可能不是代码失效,而是当前 Python 环境导入了另一份 ultralytics

可以新建一个检查脚本:

import inspect

import ultralytics
from ultralytics import YOLO
from ultralytics.engine.model import Model
from ultralytics.models.yolo.detect import DetectionTrainer
from ultralytics.nn.tasks import DetectionModel, parse_model


print("Ultralytics version:", ultralytics.__version__)
print("Ultralytics package:", ultralytics.__file__)
print("YOLO class:", inspect.getfile(YOLO))
print("Model class:", inspect.getfile(Model))
print("DetectionTrainer:", inspect.getfile(DetectionTrainer))
print("DetectionModel:", inspect.getfile(DetectionModel))
print("parse_model:", inspect.getfile(parse_model))

这段脚本没有修改模型,它只负责打印当前环境实际导入的文件路径。

如果路径指向:

.../site-packages/ultralytics/

而不是准备修改的源码目录,说明当前环境没有正确使用可编辑安装。可以进入 8.3.253 源码根目录重新执行:

pip uninstall ultralytics -y
pip install -e .

重新运行检查脚本,确认版本为 8.3.253,并且导入路径指向当前源码工程。

十一、不同改进分别应该关注哪些文件

改进方向 主要位置 通常还要检查
添加注意力或卷积模块 ultralytics/nn/modules/ modules/__init__.pynn/tasks.py、模型 YAML
修改 C3k2、C2PSA ultralytics/nn/modules/block.py 构造参数、重复次数、预训练权重兼容性
更换 Backbone 自定义模块文件与模型 YAML 多尺度输出、通道数、层索引
修改 Neck 模型 YAML 与融合模块 Concat 输入、上采样尺寸、通道对齐
修改 Detect Head ultralytics/nn/modules/head.py tasks.py 特殊分支、stride、输出格式
替换损失函数 ultralytics/utils/loss.py DetectionModel 的 criterion、训练日志字段
修改 NMS 推理后处理相关代码 confiou、输出格式与部署兼容性
修改数据增强 ultralytics/data/augment.py 标签同步变换、验证集是否误用增强

最重要的判断原则是:先找到数据真正经过的位置,再修改代码。不要因为某个文件名看起来像训练入口,就把所有功能都写进去。

十二、常见问题排查

1. 修改 YAML 后,模型结构没有变化

**可能原因:**训练时仍然加载旧 .pt,或者命令中的 model 指向了另一份 YAML。

**检查方式:**打印 model.model.yaml,同时查看训练开始时输出的模型层表。

2. 自定义模块报 KeyError

**可能原因:**YAML 中写了模块名称,但该类没有进入 tasks.py 可访问的全局命名空间。

**检查方式:**确认模块文件存在、modules/__init__.py 已导出,并且 tasks.py 已导入该类。

3. 报错缺少位置参数或通道不一致

**可能原因:**自定义类的构造函数与 parse_model() 注入的 c1c2n 顺序不一致。

**检查方式:**打印 YAML 当前行的 args,对照类的 __init__() 参数逐项检查。

4. 修改源码后运行结果没有变化

**可能原因:**Python 导入了其他环境中的 Ultralytics。

**检查方式:**使用前面的 inspect.getfile() 脚本确认真实加载路径。

5. CLI 能运行,Python 脚本却使用了不同配置

**可能原因:**两种入口传入的参数不同,或 Python 脚本继承了模型检查点中的 overrides。

**检查方式:**打印 model.overrides 和最终训练参数,重点比较 modeldataimgszbatchdeviceworkers

十三、把执行链压缩成一张定位表

操作 统一入口 检测任务实现 最终关注点
加载模型 engine/model.py models/yolo/model.py _load()_new()task_map
构建网络 nn/tasks.py DetectionModel parse_model()、通道与重复次数
训练 Model.train() DetectionTrainer 数据集、模型、损失和训练循环
验证 Model.val() DetectionValidator IoU 阈值、指标统计和绘图
预测 Model.predict() DetectionPredictor 预处理、NMS 和 Results
CLI cfg/__init__.py 仍调用 Model 接口 参数解析和 overrides

以后遇到报错时,可以先判断它属于哪一行,再进入对应文件排查。这样比全局搜索某个关键词更快,也更不容易误改无关代码。

十四、总结

Ultralytics 将 YOLO11 的执行流程拆成了清晰的层次:YOLO 提供统一入口,task_map 根据任务选择实现,DetectionModelparse_model() 负责把 YAML 变成网络,DetectionTrainerDetectionValidatorDetectionPredictor 分别处理训练、验证和预测。

对于后续模型改进,最值得记住的不是某一个文件名,而是这条定位思路:

先判断修改属于模型、训练、数据还是后处理
  -> 找到对应任务类
  -> 确认 YAML 与构造参数
  -> 验证实际导入路径
  -> 先完成模型构建,再进行正式训练

本文只梳理源码执行链和验证方法,没有使用未经运行得到的精度或速度数据。下一篇将继续拆解 yolo11.yaml,重点说明层索引、特征尺度和通道数如何在网络中传递。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐