Ultralytics 框架将一份声明式的 YAML 配置文件,逐步转化为一个完整的、可直接训练的 PyTorch 神经网络模型。我们将从 YAML 规范出发,依次剖析配置加载、模块解析、通道推演、尺度缩放、跳跃连接构建以及模型实例化等关键环节,帮助读者理解整个声明式模型构建系统的内部运作机制。

Sources: tasks.py

YAML 配置:声明式模型蓝图的语法规范

Ultralytics 采用 YAML 作为模型架构的声明式描述语言。每一份模型配置文件由元参数主干网络检测头三大段组成。以 yolo11.yaml 为典型范例:

# 元参数
nc: 80                          # 类别数
scales:                         # 复合缩放常量 [depth, width, max_channels]
  n: [0.50, 0.25, 1024]
  s: [0.50, 0.50, 1024]
  m: [0.50, 1.00, 512]
  l: [1.00, 1.00, 512]
  x: [1.00, 1.50, 512]

# 主干网络
backbone:
  - [-1, 1, Conv, [64, 3, 2]]         # 0-P1/2
  - [-1, 1, Conv, [128, 3, 2]]        # 1-P2/4
  - [-1, 2, C3k2, [256, False, 0.25]]
  ...

# 检测头
head:
  - [-1, 1, nn.Upsample, [None, 2, "nearest"]]
  - [[-1, 6], 1, Concat, [1]]         # cat backbone P4
  ...
  - [[16, 19, 22], 1, Detect, [nc]]   # Detect(P3, P4, P5)

每一行的格式为 [from, repeats, module, args],其中:

字段 含义 示例
from 输入来源索引,-1 表示上一层,列表表示多层拼接 -1, [-1, 6], [16, 19, 22]
repeats 模块重复次数(会被 depth_multiple 缩放) 1, 2, 3
module 模块类名,通过 globals()torch.nn 解析 Conv, C3k2, Detect, nn.Upsample
args 模块构造参数列表 [64, 3, 2], [nc]

元参数字段定义了模型的全局属性:nc 为类别数,scales 为不同尺寸变体(n/s/m/l/x)提供 [depth, width, max_channels] 三元组,activation 可覆盖默认激活函数,reg_max 控制 DFL 通道数,kpt_shape 在姿态估计中定义关键点形状,end2end 标记端到端检测模式。

Sources: yolo11.yaml, yolo26.yaml

配置加载与尺度推断:yaml_model_load 的路径解析

当用户传入如 "yolo11n.yaml" 的配置路径时,yaml_model_load 函数执行两步关键操作:路径归一化尺度推断

用户输入: yolo11n.yaml

路径归一化
yolo11n.yaml → yolo11.yaml

加载统一 YAML
check_yaml(unified_path)

正则提取 scale 字符
guess_model_scale('yolo11n') → 'n'

写入 d['scale'] = 'n'

返回完整配置字典

路径归一化通过正则 r"(\d+)([nslmx])(.+)?$" 将尺寸后缀剥离,使 yolo11n.yaml 映射到基础模板 yolo11.yaml。这意味着所有尺寸变体共享同一份架构定义,仅通过 scales 字典中的三元组来区分参数规模。guess_model_scale 通过正则 r"yolo(e-)?[v]?\d+([nslmx])" 从文件名中提取尺寸字符,最终写入配置字典的 scale 键。

Sources: tasks.py

模型类层次:从 DetectionModel 到任务特化模型

Ultralytics 定义了一套层次化的模型类体系,每个任务对应一个模型类,它们最终都汇聚到 parse_model 这个统一的解析函数:

BaseModel

+forward(x)

+predict(x)

+fuse()

+load(weights)

+loss(batch)

+info()

_predict_once(x)

DetectionModel

+stride: Tensor

+names: dict

+end2end: bool

+init(cfg, ch, nc)

+init_criterion()

_predict_augment(x)

OBBModel

+init_criterion()

SegmentationModel

+init_criterion()

PoseModel

+kpt_shape: tuple

+init_criterion()

ClassificationModel

+init(cfg, ch, nc)

_from_yaml(cfg, ch, nc)

reshape_outputs(model, nc)

+init_criterion()

RTDETRDetectionModel

+init_criterion()

+loss(batch)

+predict(x)

DetectionModel 是检测、分割、姿态和 OBB 任务的基础。其 __init__ 方法(第 370–423 行)执行以下初始化序列:① 加载 YAML 配置;② 调用 parse_model 构建 nn.Sequential 模型;③ 通过一次前向传播计算 stride;④ 调用 bias_init() 初始化检测头偏置;⑤ 执行权重初始化。ClassificationModel 则直接继承 BaseModel,其内部调用 _from_yaml 方法完成类似流程,但不需要 stride 计算。

Sources: tasks.py, tasks.py

parse_model:核心解析引擎

parse_model 是整个声明式构建系统的核心引擎,它将 YAML 字典转化为 torch.nn.Sequential 模型和保存列表。以下是其完整的处理流程:

nn.xxx

torchvision.ops.xxx

自定义模块

base_modules

Detect/Segment/Pose/OBB

Concat

nn.BatchNorm2d

其他

遍历完毕

提取全局参数
nc, scales, depth, width, max_channels

遍历 backbone + head 列表

解析模块类名
m 的来源

getattr(torch.nn, m)

getattr(torchvision.ops, m)

globals()[m]

解析 args 中的字符串字面量

计算 depth gain
n = max(round(n × depth), 1)

模块类型分支

c1=ch[from], c2 缩放
args=[c1, c2, *rest]

追加 reg_max, end2end, ch_list

c2 = sum(ch[x] for x in f)

args=[ch[from]]

c2 = ch[from]

实例化模块
nn.Sequential(*[m(*args)] * n)

附加元信息 i, f, type

更新 save 列表

追加到 layers, 记录 c2

返回 nn.Sequential(*layers), sorted(save)

4.1 缩放机制:depth、width 与 max_channels 的三重增益

scales 字典为每种模型尺寸提供三元组 [depth_multiple, width_multiple, max_channels],在解析过程中分别控制三个维度:

  • depth 增益n = max(round(n * depth), 1),将 YAML 中的 repeats 值按比例缩放。例如 yolo11n.yaml 中 C3k2 的 repeats=2,乘以 depth=0.50 后实际重复 1 次。
  • width 增益:对 base_modules 的输出通道执行 c2 = make_divisible(min(c2, max_channels) * width, 8),将通道数按比例缩放并保证可被 8 整除。例如 YAML 中的 c2=256,乘以 width=0.25 后得到 64。
  • max_channels:通道数的硬上限。当 max_channels=512 时,即使 c2 * width 超过 512 也会被截断。
模型 depth width max_channels 参数量
YOLO11n 0.50 0.25 1024 2.6M
YOLO11s 0.50 0.50 1024 9.5M
YOLO11m 0.50 1.00 512 20.1M
YOLO11l 1.00 1.00 512 25.4M
YOLO11x 1.00 1.50 512 57.0M

Sources: tasks.py, yolo11.yaml

4.2 模块解析:名称到类的三路分发

模块名称的解析遵循优先级顺序(第 1633–1639 行):

m = (
    getattr(torch.nn, m[3:])              # "nn.Upsample" → torch.nn.Upsample
    if "nn." in m
    else getattr(__import__("torchvision").ops, m[16:])  # "torchvision.ops.xxx"
    if "torchvision.ops." in m
    else globals()[m]                      # "Conv" → ultralytics.nn.tasks 中的 Conv
)

这里的关键设计是 globals()[m]——所有从 ultralytics.nn.modules 导入的模块类(如 Conv, C3k2, Detect, SPPF 等)都在 tasks.py 的全局命名空间中可用。这意味着框架在解析 YAML 时不需要维护一个注册表,而是直接利用 Python 模块系统的全局符号解析。

Sources: tasks.py

4.3 通道推演与模块分类处理

parse_model 维护一个通道列表 ch,记录每一层的输出通道数。模块的输入通道 c1 来自 ch[f]ffrom 索引),输出通道 c2 根据模块类型以不同方式确定:

模块类别 代表模块 通道计算逻辑
base_modules Conv, C3k2, C2f, SPPF c2 = args[0],经过 width 增益缩放
repeat_modules C2f, C3k2, C3, C2PSA args 中插入重复次数 nn 重置为 1
Detect 系列 Detect, Segment, Pose, OBB 追加 reg_max, end2end, [ch[x] for x in f]
Concat Concat c2 = sum(ch[x] for x in f)
nn.BatchNorm2d nn.BatchNorm2d args = [ch[f]]
HGBlock/HGStem HGBlock, HGStem 三通道格式 c1, cm, c2
RTDETRDecoder RTDETRDecoder 在 index 1 插入通道列表
其他 nn.Upsample c2 = ch[f],通道不变

对于 Detect 系列头部第 1681–1700 行),parse_model 会将 reg_maxend2end 标志和来自多尺度特征图的通道列表 [ch[x] for x in f] 附加到构造参数中。以 Detect 头为例:

# YAML: [[16, 19, 22], 1, Detect, [nc]]
# 解析后: Detect(nc=80, reg_max=16, end2end=False, ch=(ch_16, ch_19, ch_22))

Segment 头额外处理 nm(掩码数)的 width 增益缩放:args[2] = make_divisible(min(args[2], max_channels) * width, 8)

Sources: tasks.py

4.4 跳跃连接与保存列表

YOLO 架构中的 FPN/PAN 结构依赖跨层连接(如 Concat 操作)。parse_model 通过 save 列表追踪哪些层的输出需要被保留:

save.extend(x % i for x in ([f] if isinstance(f, int) else f) if x != -1)

在运行时,BaseModel._predict_once 通过以下逻辑实现跳跃连接:

for m in self.model:
    if m.f != -1:  # 不是来自上一层
        x = y[m.f] if isinstance(m.f, int) else [x if j == -1 else y[j] for j in m.f]
    x = m(x)
    y.append(x if m.i in self.save else None)  # 仅保存需要的层

每个模块实例被附加了三个属性:m.i(全局层索引)、m.f(from 索引)和 m.type(类型字符串)。y 列表存储历史输出,仅在 m.i 属于 save 列表时保留,否则存入 None 以节省内存。

Sources: tasks.py, tasks.py

4.5 字符串参数的动态求值

YAML 中的 args 可能包含字符串形式的变量引用或字面量。第 1640–1643 行的处理逻辑是:

for j, a in enumerate(args):
    if isinstance(a, str):
        with contextlib.suppress(ValueError):
            args[j] = locals()[a] if a in locals() else ast.literal_eval(a)

这使得 YAML 中可以使用 nckpt_shape 等配置字典中的变量名。例如 Detect, [nc] 中的 nc 会被解析为 80(实际类别数),Pose, [nc, kpt_shape] 中的 kpt_shape 被解析为 (17, 3)

Sources: tasks.py

Stride 计算与偏置初始化:模型就绪的最后一步

模型构建完成后,DetectionModel.init 执行两个关键初始化步骤:

Stride 计算第 400–417 行):通过向模型输入一个 256×256 的零张量,计算每个检测层的下采样倍率:

s = 256  # 2x min stride
m.stride = torch.tensor([s / x.shape[-2] for x in _forward(torch.zeros(1, ch, s, s))])

对于标准的 P3/P4/P5 三尺度输出,stride 通常为 [8, 16, 32]。对于不使用 Detect 头的模型(如 RTDETR),stride 默认为 [32]

偏置初始化Detect.bias_init):根据 stride 为检测头的回归和分类分支设置合理的初始偏置值,确保训练初期输出分布合理:

a[-1].bias.data[:] = 2.0  # box 偏置
b[-1].bias.data[: self.nc] = math.log(5 / self.nc / (640 / self.stride[i]) ** 2)  # cls 偏置

最后,initialize_weights 函数对模型中所有的 Conv2d、BatchNorm2d 和 Linear 层执行标准初始化(Kaiming 初始化、恒等初始化等)。

Sources: tasks.py, head.py

任务特化:不同头部的 YAML 差异与参数传递

虽然不同任务(检测、分割、姿态、OBB、分类)共享相同的 backbone 架构,但头部模块的构造参数存在显著差异:

任务 头部模块 YAML 参数 parse_model 追加参数 特殊处理
检测 Detect [nc] reg_max, end2end, ch_list 设置 m.legacy
分割 Segment [nc, 32, 256] 同上 + nm 经 width 缩放 args[2] 缩放
姿态 Pose [nc, kpt_shape] 同上 kpt_shape 从 YAML 传入
OBB OBB [nc, 1] 同上 ne=1 表示角度参数
分类 Classify [nc] 无追加 仅有 c1, c2 两参数
RTDETR RTDETRDecoder [nc] 在 index 1 插入 ch_list Transformer 解码器

yolo11-seg.yaml 为例,最后一行 [[16, 19, 22], 1, Segment, [nc, 32, 256]] 声明了分割头。在 parse_model 中,nm=32(掩码数)经过 width 增益缩放,npr=256(原型数)同样被缩放,然后 reg_maxend2end 和通道列表被追加到参数末尾,最终构造为 Segment(nc=80, nm=32, npr=256, reg_max=16, end2end=False, ch=(ch_16, ch_19, ch_22))

Sources: tasks.py, yolo11-seg.yaml, yolo11-pose.yaml

模块库架构:从 YAML 名称到 PyTorch 类的映射

整个解析系统依赖一个清晰的三级模块导入链:

ultralytics/nn/modules/

block.py
C2f, C3k2, SPPF, PSA, ...

conv.py
Conv, DWConv, Concat, ...

head.py
Detect, Segment, Pose, OBB, Classify, ...

transformer.py
AIFI, TransformerBlock, ...

__init__.py
统一导出所有模块

tasks.py
from ultralytics.nn.modules import *
globals()[m] 解析

ultralytics/nn/modules/__init__.py 作为统一的导出入口,从四个子模块中收集所有公共类。tasks.py 在文件顶部通过 from ultralytics.nn.modules import (Conv, C3k2, Detect, ...) 将这些类导入全局命名空间。parse_model 中的 globals()[m] 正是依赖这一导入关系来完成从字符串到 Python 类的映射。

Sources: __init__.py, tasks.py

完整构建流程回顾

将以上所有环节串联,从用户调用 YOLO("yolo11n.pt") 到获得可训练模型的完整路径如下:

detect

segment

classify

pose

obb

YOLO('yolo11n.pt')

guess_model_task
推断任务类型

任务类型

DetectionModel(cfg, ch=3, nc=80)

SegmentationModel(cfg, ch=3, nc=80)

ClassificationModel(cfg, ch=3, nc=1000)

PoseModel(cfg, ch=3, nc=80)

OBBModel(cfg, ch=3, nc=80)

yaml_model_load('yolo11n.yaml')
归一化路径 → yolo11.yaml
提取 scale='n'

parse_model(d, ch=3)
depth=0.50, width=0.25, max_ch=1024

逐层遍历 backbone + head
解析模块、计算通道、构建跳跃连接

nn.Sequential(*layers)
+ sorted(save)

stride 计算
前向传播 zeros(1,3,256,256)

bias_init()
初始化检测头偏置

initialize_weights()
全局权重初始化

可训练的 PyTorch 模型

理解这一构建流程对于自定义模型架构至关重要——开发者只需编写符合 [from, repeats, module, args] 规范的 YAML 文件,并确保所引用的模块类已在 ultralytics/nn/modules/ 中定义并在 tasks.py 中导入,即可让框架自动完成从声明到实例化的全部工作。

如需深入了解可用的神经网络模块及其实现细节,请参阅 神经网络模块库:卷积、注意力、检测头等核心算子;如需了解训练后模型的多种推理格式适配,请参阅 推理后端抽象层 AutoBackend:多格式统一调度

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐