002-YOLO11源码阅读-训练验证预测调用链
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 解析、网络实例化、数据集构建和验证器创建等工作。
这篇文章不逐行翻译整个项目,而是回答几个后续改进最常遇到的问题:
YOLO("yolo11n.pt")创建的到底是什么对象?.pt权重和.yaml配置的加载路径有什么区别?- 目标检测任务为什么会自动选择
DetectionTrainer? - YAML 中的模块名称在哪里转换成 Python 类?
- 训练、验证和预测分别由哪些文件负责?
- 添加新模块时,应该优先修改哪些位置?
本文固定使用 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.task 从 task_map 取出正确的类。
对应的选择动作由 engine/model.py 中的 _smart_load() 完成:
return self.task_map[self.task][key]
其中 key 可以是:
modeltrainervalidatorpredictor
所以目标检测调用 _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 中的 backbone 和 head,将每一行描述转换为实际的 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_modules 和 repeat_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
右侧参数优先级更高,因此本次调用传入的 epochs、batch、imgsz 等会覆盖默认配置。
参数整理完成后,程序通过 _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.50 到 0.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() 会读取 conf、iou、classes、agnostic_nms 和 max_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.toml 将 yolo 和 ultralytics 两个命令都注册到:
ultralytics.cfg:entrypoint
命令行参数在 ultralytics/cfg/__init__.py 中解析,随后创建 YOLO 对象,并根据 mode 执行:
getattr(model, mode)(**overrides)
当 mode=train 时,本质上还是调用 model.train();mode=val 和 mode=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__.py、nn/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 | 推理后处理相关代码 | conf、iou、输出格式与部署兼容性 |
| 修改数据增强 | 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() 注入的 c1、c2、n 顺序不一致。
**检查方式:**打印 YAML 当前行的 args,对照类的 __init__() 参数逐项检查。
4. 修改源码后运行结果没有变化
**可能原因:**Python 导入了其他环境中的 Ultralytics。
**检查方式:**使用前面的 inspect.getfile() 脚本确认真实加载路径。
5. CLI 能运行,Python 脚本却使用了不同配置
**可能原因:**两种入口传入的参数不同,或 Python 脚本继承了模型检查点中的 overrides。
**检查方式:**打印 model.overrides 和最终训练参数,重点比较 model、data、imgsz、batch、device 与 workers。
十三、把执行链压缩成一张定位表
| 操作 | 统一入口 | 检测任务实现 | 最终关注点 |
|---|---|---|---|
| 加载模型 | 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 根据任务选择实现,DetectionModel 和 parse_model() 负责把 YAML 变成网络,DetectionTrainer、DetectionValidator 与 DetectionPredictor 分别处理训练、验证和预测。
对于后续模型改进,最值得记住的不是某一个文件名,而是这条定位思路:
先判断修改属于模型、训练、数据还是后处理
-> 找到对应任务类
-> 确认 YAML 与构造参数
-> 验证实际导入路径
-> 先完成模型构建,再进行正式训练
本文只梳理源码执行链和验证方法,没有使用未经运行得到的精度或速度数据。下一篇将继续拆解 yolo11.yaml,重点说明层索引、特征尺度和通道数如何在网络中传递。
更多推荐




所有评论(0)