👑 欢迎大家订阅我的专栏一起学习YOLO 👑
一、本文介绍
本文给大家带来的是 YOLOv11项目的解读 ,之前给大家分析了YOLOv11的项目文件分析,这一篇文章给大家带来的是 模型训练 从我们的yaml文件定义到模型的定义部分的讲解,我们一般只知道如何去训练模型,和配置yaml文件,但是对于yaml文件是如何输入到模型里,模型如何将yaml文件解析出来的确是不知道的,本文的内容接上一篇的代码逐行解析(二) ,本文对于小白来说非常友好,非常推荐大家进行阅读,深度的了解模型的工作原理已经流程,下面我们从yaml文件来讲解。
本文的讲解全部在代码的对应位置进行注释介绍非常详细, 以下为部分内容的截图。
二、yaml文件的定义
我们训练模型的第一步是需要配置yaml文件,我们的讲解第一步也从yaml文件来开始讲解,YOLOv11的yaml文件存放在我们的如下目录内' ultralytics /cfg/models/v11,在其中我们可以定义各种模型配置的文件组合 不同的 模块,我们拿最基础的YOLOv11yaml文件来讲解一下。
注释部分的内容我就不介绍了,我只介绍一下其中有用的部分,我已经在代码中对应的位置注释上了解释,大家可以看这样看起来也直观一些。
- # Ultralytics YOLO 🚀, AGPL-3.0 license
- # YOLO11 object detection model with P3-P5 outputs. For Usage examples see https://docs.ultralytics.com/tasks/detect
- # Parameters
- nc: 80 # number of classes
- # 数据集的类别数,我们默认的数据COCO是80类别(YOLOv8提供的权重也是由此数据集训练出来的),有的读者喜欢修改nc此处其实不需要修改,
- # 模型会自动根据我们数据集的yaml文件获取此处的数量,同时我们8.1版本之前的ultralytics仓库打印两边的网络结构,唯一的区别就是nc的数量不一样(实际运行的是第二遍的网络结构)。
- scales: # model compound scaling constants, i.e. 'model=yolo11n.yaml' will call yolo11.yaml with scale 'n'
- # 此处的含义大概就是如果我们在训练的指令时候使用model=yolov10.yaml 则对应的是v10n,如果使用model=yolov10s.yaml则对应的是v8s
- # 当然了大家如果不想使用上面的方式指定模型,我们只需要将下面想要使用的模型移到最前端即可,或者将其余不想用的注释掉都可以。
- # [depth, width, max_channels]
- # depth 模型的深度 width 模型的宽度 max_channels模型最大的通道数
- # 这三个参数共同决定模型的大小,其中depth主要是和下面配置文件部分的repeats进行一个计算来确定其中模块的串联数量
- # width 主要是 和args的第一个参数通道数进行计算来算出模型不同层的通道数, 但是通道数不能超过max_channels.
- # 大概介绍一下通道数是什么, 我们输入模型的tensor形状为[batch_size, 通道数, 图片宽, 图片高]
- # 通道数就是tensor的第二个参数,如果我们忽略batch_size, 那么可以理解为通道数张二维图片堆叠在一起, batch_size为多少堆 每一堆有通道数张二维图片堆叠在一起.
- # 每一个版本后面有每一个版本的层数 参数量等指标
- n: [0.50, 0.25, 1024] # summary: 319 layers, 2624080 parameters, 2624064 gradients, 6.6 GFLOPs
- s: [0.50, 0.50, 1024] # summary: 319 layers, 9458752 parameters, 9458736 gradients, 21.7 GFLOPs
- m: [0.50, 1.00, 512] # summary: 409 layers, 20114688 parameters, 20114672 gradients, 68.5 GFLOPs
- l: [1.00, 1.00, 512] # summary: 631 layers, 25372160 parameters, 25372144 gradients, 87.6 GFLOPs
- x: [1.00, 1.50, 512] # summary: 631 layers, 56966176 parameters, 56966160 gradients, 196.0 GFLOPs
- # YOLO11n backbone
- backbone:
- # 这里需要多介绍一下,from, repeats, module, args
- # from 此处有三种可能的值分别是 -1、具体的数值、list存放数值。分别含义如下 (1)、-1的含义就是代表此层的输入就是上一层的输出,
- # (2)、如果是具体的某个数字比如4那么则代表本层的输入来自于模型的第四层,
- # (3)、有的层是list存放两个值也可能是多个值,则代表对应两个值的输出为本层的输入
- # repeats 这个参数是为了C2f设置的其它的模块都用不到,代表着C2f当中Bottleneck重复的次数,比如当我们的模型用的是l的时候,那么repeats=2那么则代表C3k2当中的Bottleneck串行2个。
- # module 此处则代表模型的名称
- # args 此处代表输入到对应模块的参数,此处和parse_model函数中的定义方法有关,对于C3k2来说传入的参数->第一个参数是上一个模型的输出通道数,第二个参数就是args的第一个参数,然后以此类推。
- # [from, repeats, module, args]
- - [-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]]
- - [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
- - [-1, 2, C3k2, [512, False, 0.25]]
- - [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
- - [-1, 2, C3k2, [512, True]]
- - [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
- - [-1, 2, C3k2, [1024, True]]
- - [-1, 1, SPPF, [1024, 5]] # 9
- - [-1, 2, C2PSA, [1024]] # 10
- # YOLO11n head
- head:
- - [-1, 1, nn.Upsample, [None, 2, "nearest"]]
- - [[-1, 6], 1, Concat, [1]] # cat backbone P4
- - [-1, 2, C3k2, [512, False]] # 13
- - [-1, 1, nn.Upsample, [None, 2, "nearest"]]
- - [[-1, 4], 1, Concat, [1]] # cat backbone P3
- - [-1, 2, C3k2, [256, False]] # 16 (P3/8-small)
- - [-1, 1, Conv, [256, 3, 2]]
- - [[-1, 13], 1, Concat, [1]] # cat head P4
- - [-1, 2, C3k2, [512, False]] # 19 (P4/16-medium)
- - [-1, 1, Conv, [512, 3, 2]]
- - [[-1, 10], 1, Concat, [1]] # cat head P5
- - [-1, 2, C3k2, [1024, True]] # 22 (P5/32-large)
- - [[16, 19, 22], 1, Detect, [nc]] # Detect(P3, P4, P5)
上面我们讲了yaml文件的大概含义,下面我们来解析其中各个模块.
下面的图片为YOLOv11的网络结构图。
其中主要创新点可以总结如下->
1. 提出C3k2机制,其中C3k2有参数为c3k,其中在网络的浅层c3k设置为False(下图中可以看到c3k2第二个参数被设置为False,就是对应的c3k参数)。
此时所谓的C3k2就相当于YOLOv8中的C2f,其网络结构为一致的,其中的C3k机制的网络结构图如下图所示 (为什么叫C3k2,我个人理解是因为C3k的调用时C3k其中的参数N固定设置为2的原因,个人理解不一定对 )。
2. 第二个创新点是提出C2PSA机制,这是一个C2(C2f的前身)机制内部嵌入了一个多头注意力机制,在这个过程中我还发现作者尝试了C2fPSA机制但是估计效果不如C2PSA,有的时候机制有没有效果理论上真的很难解释通,下图为C2PSA机制的原理图,仔细观察把Attention哪里去掉则C2PSA机制就变为了C2所以我上面说C2PSA就是C2里面嵌入了一个PSA机制。
3. 第三个创新点可以说是原先的解耦头中的分类检测头增加了两个DWConv,具体的对比大家可以看下面两个图下面的是YOLOv11的解耦头,上面的是YOLOv8的解耦头.
我们上面看到了在分类检测头中YOLOv11插入了两个DWConv这样的做法可以大幅度减少参数量和计算量(原先两个普通的Conv大家要注意到卷积和是由3变为了1的,这是形成了两个深度可分离Conv),大家可能不太理解为什么加入了两个DWConv还能够减少计算量,以及什么是深度可分离Conv,下面我来解释一下。
DWConv代表 Depthwise Convolution(深度卷积) , 是一种在 卷积神经网络 中常用的高效卷积操作。它主要用于减少计算复杂度和参数量,尤其在移动端或轻量化网络(如 MobileNet)中十分常见。1. 标准卷积的计算过程
在标准卷积操作中,对于一个输入张量(通常是一个多通道的特征图),卷积核的尺寸是
(h, w, C_in),其中h和w是卷积核的空间尺寸,C_in是输入通道的数量。而卷积核与输入张量做的是完整的卷积运算,每个输出通道都与所有输入通道相连并参与卷积操作,导致计算量比较大。标准卷积的计算过程是这样的:
- 每个输出通道是所有输入通道的组合(加权求和),卷积核在每个位置都会计算与所有输入通道的点积。
- 假设有
C_in个输入通道和C_out个输出通道,那么卷积核的总参数量是C_in * C_out * h * w。2. Depthwise Convolution(DWConv)
与标准卷积不同, 深度卷积 将输入的每个通道单独处理,即 每个通道都有自己的卷积核进行卷积 ,不与其他通道进行交互。它可以被看作是标准卷积的一部分,专注于空间维度上的卷积运算。
深度卷积的计算过程:
- 假设输入张量有
C_in个通道,每个通道会使用一个h × w的卷积核进行卷积操作。这个过程称为“深度卷积”,因为每个通道独立进行卷积运算。- 输出的通道数与输入通道数一致,每个输出通道只和对应的输入通道进行卷积,没有跨通道的组合。
- 参数量和计算量相比标准卷积大大减少,卷积核的参数量是
C_in * h * w。深度卷积的优点:
- 计算效率高 :相对于标准卷积,深度卷积显著减少了计算量。它只处理空间维度上的卷积,不再处理通道间的卷积。
- 参数量减少 :由于每个卷积核只对单个通道进行卷积,参数量大幅减少。例如,标准卷积的参数量为
C_in * C_out * h * w,而深度卷积的参数量为C_in * h * w。- 结合点卷积可提升效果 :为了弥补深度卷积缺乏跨通道信息整合的问题,通常深度卷积后会配合
1x1的点卷积(Pointwise Convolution)使用,通过1x1的卷积核整合跨通道的信息。这种组合被称为 深度可分离卷积 (Depthwise Separable Convolution) | 这也是我们本文YOLOv11中的做法 。3. 深度卷积与标准卷积的区别
操作类型 卷积核大小 输入通道数 输出通道数 参数量 标准卷积 h × wC_inC_outC_in * C_out * h * w深度卷积(DWConv) h × wC_inC_inC_in * h * w可以看出,深度卷积在相同的卷积核大小下,参数量减少了约
C_out倍 (细心的人可以发现用最新版本的ultralytics仓库运行YOLOv8参数量相比于之前的YOLOv8以及大幅度减少了这就是因为检测头改了的原因但是名字还是Detect,所以如果你想继续用YOLOv8发表论文做实验那么不要更新最近的ultralytics仓库)。4. 深度可分离卷积 (Depthwise Separable Convolution)
深度卷积常与
1x1的点卷积配合使用,这称为深度可分离卷积。其过程如下:
- 先对输入张量进行深度卷积,对每个通道独立进行空间卷积。
- 然后通过
1x1点卷积,对通道维度进行混合,整合不同通道的信息。这样既可以保证计算量的减少,又可以保持跨通道的信息流动。
5. 总结
DWConv是一种高效的卷积方式,通过单独处理每个通道来减少计算量,结合1x1的点卷积,形成深度可分离卷积,可以在保持网络性能的同时极大地减少模型的计算复杂度和参数量。
看到这里大家应该明白了为什么加入了两个DWConv还能减少参数量以及YOLOv11的检测头创新点在哪里。
4.YOLOv11和YOLOv8还有一个不同的点就是其各个版本的模型(N - S - M- L - X)网络深度和宽度变了
可以看到在深度(depth)和宽度 (width)两个地方YOLOv8和YOLOv11是基本上完全不同了,这里我理解这么做的含义就是模型网络变小了,所以需要加深一些模型的放缩倍数来弥补模型之前丧失的能力从而来达到一个平衡。
本章总结: YOLOv11的改进点其实并不多更多的都是一些小的结构上的创新,相对于之前的 YOLOv5 到YOLOv8的创新,其实YOLOv11的创新点不算多,但是其是ultralytics公司的出品,同时ultralytics仓库的使用量是非常多的(不像YOLOv9和YOLOv10)所以在未来的很长一段时间内其实YOLO系列估计不会再更新了,YOLOv11作为最新的SOTA肯定是十分适合大家来发表论文和创新的。
三、yaml文件的输入
上面我们解释了yaml文件中的参数含义,然后提供了一个结构图(其中能够获取到每个模块的详细结构,该结构图来源于官方)。然后我们下一步介绍当定义好了一个ymal文件其是如何传入到模型的内部的,模型的开始在哪里。
3.1 模型的定义
我们通过命令行的命令或者创建py文件运行模型之后,模型最开始的工作是模型的定义操作。模型存放于文件'ultralytics/engine/model.py'内部,首先需要通过'__init__'来定义模型的一些变量。
此处我将模型的定义部分的代码解释了一下,大家有兴趣的可以和自己的文件对比着看。
- class Model(nn.Module):
- import torch.nn as nn
- class Model(nn.Module):
- """
- 一个统一所有模型API的基类。
- 参数:
- model (str, Path): 要加载或创建的模型文件的路径。
- task (Any, 可选): YOLO模型的任务类型。默认为None。
- 属性:
- predictor (Any): 预测器对象。
- model (Any): 模型对象。
- trainer (Any): 训练器对象。
- task (str): 模型任务类型。
- ckpt (Any): 如果从*.pt文件加载的模型,则为检查点对象。
- cfg (str): 如果从*.yaml文件加载的模型,则为模型配置。
- ckpt_path (str): 检查点文件路径。
- overrides (dict): 训练器对象的覆盖。
- metrics (Any): 用于度量的数据。
- 方法:
- __call__(source=None, stream=False, **kwargs):
- 预测方法的别名。
- _new(cfg:str, verbose:bool=True) -> None:
- 初始化一个新模型,并从模型定义中推断任务类型。
- _load(weights:str, task:str='') -> None:
- 初始化一个新模型,并从模型头中推断任务类型。
- _check_is_pytorch_model() -> None:
- 如果模型不是PyTorch模型,则引发TypeError。
- reset() -> None:
- 重置模型模块。
- info(verbose:bool=False) -> None:
- 记录模型信息。
- fuse() -> None:
- 为了更快的推断,融合模型。
- predict(source=None, stream=False, **kwargs) -> List[ultralytics.engine.results.Results]:
- 使用YOLO模型进行预测。
- 返回:
- list(ultralytics.engine.results.Results): 预测结果。
- """
- def __init__(self, model: Union[str, Path] = "yolov8n.pt", task=None, verbose=False) -> None:
- """
- Initializes the YOLO model.
- Args:
- model (Union[str, Path], optional): Path or name of the model to load or create. Defaults to 'yolov8n.pt'.
- task (Any, optional): Task type for the YOLO model. Defaults to None.
- verbose (bool, optional): Whether to enable verbose mode.
- """
- """
- 此处为上面的解释
- 初始化 YOLO 模型。
- 参数:
- model (Union[str, Path], 可选): 要加载或创建的模型的路径或名称。默认为'yolov8n.pt'。
- task (Any, 可选): YOLO 模型的任务类型。默认为 None。
- verbose (bool, 可选): 是否启用详细模式。
- """
- super().__init__()
- """此处就是读取我们的yaml文件的地方,callbacks.get_default_callbacks()会将我们的yaml文件进行解析然后将名称返回回来存放在self.callbacks中"""
- self.callbacks = callbacks.get_default_callbacks()
- """ 下面的部分就是一些模型的参数定义,我大概解释了一下,大家其实也不用太了解,一篇文章也介绍不了太多"""
- self.predictor = None # 重用预测器
- self.model = None # 模型对象
- self.trainer = None # 训练器对象
- self.ckpt = None # 如果从*.pt文件加载的检查点对象
- self.cfg = None # 如果从*.yaml文件加载的模型配置
- self.ckpt_path = None # 检查点文件路径
- self.overrides = {} # 训练器对象的覆盖设置
- self.metrics = None # 验证/训练指标
- self.session = None # HUB 会话
- self.task = task # 任务类型
- self.model_name = model = str(model).strip() # 去除空格
- # 检查是否为来自 https://hub.ultralytics.com 的 Ultralytics HUB 模型
- if self.is_hub_model(model):
- # 从 HUB 获取模型
- checks.check_requirements("hub-sdk>0.0.2")
- self.session = self._get_hub_session(model)
- model = self.session.model_file
- # 检查是否为 Triton 服务器模型
- elif self.is_triton_model(model):
- self.model = model
- self.task = task
- return
- # 加载或创建新的 YOLO 模型
- model = checks.check_model_file_from_stem(model) # 添加后缀,例如 yolov8n -> yolov8n.pt
- """ 此处比较重要,如果我们没有指定模型的权重.pt那么模型会根据yaml文件创建一个新的模型,如果指定了权重那么模型这回加载pt文件中的模型"""
- if Path(model).suffix in (".yaml", ".yml"):
- self._new(model, task=task, verbose=verbose)
- else:
- self._load(model, task=task)
- self.model_name = model # 返回的模型则保存在self.model_name中
3.2 模型的训练
我们上面讲完了模型的定义,然后模型就会根据你指定的参数来进行调用对应的 函数 ,比如我这里指定的是detect,和train,如下图所示,然后模型就会根据指定的参数进行对应任务的训练。
图片来源于文件'ultralytics/cfg/default.yaml' 截图。
此处执行的是ultralytics/engine/model.py'文件中class Model(nn.Module):类别的def train(self, trainer=None, **kwargs):函数,具体的解释我已经在代码中标记了。
- def train(self, trainer=None, **kwargs):
- """
- 在给定的数据集上训练模型。
- 参数:
- trainer (BaseTrainer, 可选): 自定义的训练器。
- **kwargs (Any): 表示训练配置的任意数量的参数。
- """
- self._check_is_pytorch_model() # 检查模型是否为 PyTorch 模型
- if hasattr(self.session, "model") and self.session.model.id: # Ultralytics HUB session with loaded model
- if any(kwargs):
- LOGGER.warning("WARNING ⚠️ 使用 HUB 训练参数,忽略本地训练参数。")
- kwargs = self.session.train_args # 覆盖 kwargs
- checks.check_pip_update_available() # 检查 pip 是否有更新
- overrides = yaml_load(checks.check_yaml(kwargs["cfg"])) if kwargs.get("cfg") else self.overrides
- custom = {"data": DEFAULT_CFG_DICT["data"] or TASK2DATA[self.task]} # 方法的默认设置
- args = {**overrides, **custom, **kwargs, "mode": "train"} # 最高优先级的参数在右侧
- if args.get("resume"):
- args["resume"] = self.ckpt_path
- # 实例化或加载训练器
- """ 此处将一些参数加载到模型的内部"""
- self.trainer = (trainer or self._smart_load("trainer"))(overrides=args, _callbacks=self.callbacks)
- if not args.get("resume"): # 仅在不续训的时候手动设置模型
- # 获取模型并设置训练器
- """
- 此处比较重要,为开始定义我们的对应任务的模型了比如我这里task设置的为Detect,那么此处会实例化DetectModel模型。
- 模型存放在ultralytics/nn/tasks.py内(就是我们修改模型时候的用到的那个task.py文件)
- 此处就会跳转到'ultralytics/nn/tasks.py'文化内的class DetectionModel(BaseModel):类中进行初始化和模型的定义工作
- """
- self.trainer.model = self.trainer.get_model(weights=self.model if self.ckpt else None, cfg=self.model.yaml)
- self.model = self.trainer.model
- if SETTINGS["hub"] is True and not self.session:
- # 如果开启了 HUB 并且没有 HUB 会话
- try:
- # 创建一个 HUB 中的模型
- self.session = self._get_hub_session(self.model_name)
- if self.session:
- self.session.create_model(args)
- # 检查模型是否创建成功
- if not getattr(self.session.model, "id", None):
- self.session = None
- except (PermissionError, ModuleNotFoundError):
- # 忽略 PermissionError 和 ModuleNotFoundError,表示 hub-sdk 未安装
- pass
- # 将可选的 HUB 会话附加到训练器
- self.trainer.hub_session = self.session
- # 进行模型训练
- self.trainer.train()
- # 训练结束后更新模型和配置信息
- if RANK in (-1, 0):
- ckpt = self.trainer.best if self.trainer.best.exists() else self.trainer.last
- self.model, _ = attempt_load_one_weight(ckpt)
- self.overrides = self.model.args
- self.metrics = getattr(self.trainer.validator, "metrics", None) # TODO: DDP 模式下没有返回指标
- return self.metrics
3.3 模型的网络结构打印
第三步比较重要的就是来到了'ultralytics/nn/tasks.py'(就是我们改进模型时候的那个文件)文化内的class DetectionModel(BaseModel):类中进行初始化和模型的定义工作。
这里涉及到了模型的定义和校验工作(在模型的正式开始训练之前检测模型是否能够运行的工作!)。
- class DetectionModel(BaseModel):
- """YOLOv8 目标检测模型。"""
- def __init__(self, cfg="yolov8n.yaml", ch=3, nc=None, verbose=True): # model, input channels, number of classes
- """使用给定的配置和参数初始化 YOLOv8 目标检测模型。"""
- super().__init__()
- self.yaml = cfg if isinstance(cfg, dict) else yaml_model_load(cfg) # cfg 字典
- # 定义模型
- ch = self.yaml["ch"] = self.yaml.get("ch", ch) # 输入通道数
- if nc and nc != self.yaml["nc"]:
- LOGGER.info(f"覆盖 model.yaml nc={self.yaml['nc']} 为 nc={nc}")
- self.yaml["nc"] = nc # 覆盖 YAML 中的值
- """ 此处最为重要,涉及到了我们修改模型的配置的那个函数parse_model,
- 这里返回了我们的每一个模块的定义,也就是self.model保存了我们的ymal文件所有模块的实例化模型
- self.save保存列表 | 也就是除了from部分为-1的部分比如from为4那么就将第四层的索引保存这里留着后面备用,
- """
- self.model, self.save = parse_model(deepcopy(self.yaml), ch=ch, verbose=verbose) # 模型,保存列表
- self.names = {i: f"{i}" for i in range(self.yaml["nc"])} # 默认名称字典
- self.inplace = self.yaml.get("inplace", True)
- # 构建步长
- m = self.model[-1] # Detect()
- if isinstance(m, (Detect, Segment, Pose, Detect_AFPN4, Detect_AFPN3, Detect_ASFF, Detect_FRM, Detect_dyhead,
- CLLAHead, Detect_dyhead3, Detect_DySnakeConv, Segment_DySnakeConv,
- Segment_DBB, Detect_DBB, Pose_DBB, OBB, Detect_FASFF)):
- s = 640 # 2x 最小步长
- m.inplace = self.inplace
- forward = lambda x: self.forward(x)[0] if isinstance(m, (Segment, Segment_DySnakeConv, Pose, Pose_DBB, Segment_DBB, OBB)) else self.forward(x)
- try:
- m.stride = torch.tensor([s / x.shape[-2] for x in forward(torch.zeros(1, ch, s, s))]) # 在 CPU 上进行前向传播
- except RuntimeError:
- try:
- self.model.to(torch.device('cuda'))
- m.stride = torch.tensor([s / x.shape[-2] for x in forward(
- torch.zeros(1, ch, s, s).to(torch.device('cuda')))]) # 在 CUDA 上进行前向传播
- except RuntimeError as error:
- raise error
- self.stride = m.stride
- m.bias_init() # 仅运行一次
- else:
- self.stride = torch.Tensor([32]) # 默认步长,例如 RTDETR
- # 初始化权重和偏置
- initialize_weights(self)
- if verbose: # 此处为获取模型参数量和打印的地方。
- self.info()
- LOGGER.info("")
3.4 parse_model的解析
这里涉及到yaml文件中模块的定义和,通道数放缩的地方,此处大家可以仔细看看比较重要涉及到模块的改动。
- def parse_model(d, ch, verbose=True): # model_dict, input_channels(3)
- """解析 YOLO 模型.yaml 字典为 PyTorch 模型。"""
- import ast
- # 参数设置
- max_channels = float("inf") # 设置一个最大的通道数inf,防止后面的通道数有的超出了范围,没什么作用其实。
- """下面一行代码比较重要,为获取我们yaml文件中的参数,nc=类别数(前面解释过了) act=激活函数, scales=模型的大小"""
- nc, act, scales = (d.get(x) for x in ("nc", "activation", "scales"))
- """此处为获取模型的通道数放缩比例假如 n: [0.33, 0.25, 1024] # YOLOv8n summary: 225 layers, 3157200 parameters, 3157184 gradients, 8.9 GFLOPs"""
- """那么此处对应的就是 0.33 , 0.25, 1024"""
- depth, width, kpt_shape = (d.get(x, 1.0) for x in ("depth_multiple", "width_multiple", "kpt_shape"))
- """下面这个判断主要的功能就是我们指定yaml文件的时候如果不指定n或者其它模型尺度则默认用n然后提出一个警告,细心的读者应该会遇到过这个警告,群里也有人问过"""
- if scales:
- scale = d.get("scale")
- if not scale:
- scale = tuple(scales.keys())[0]
- LOGGER.warning(f"WARNING ⚠️ 没有传递模型比例。假定 scale='{scale}'。")
- depth, width, max_channels = scales[scale]
- if act:
- Conv.default_act = eval(act) # 重新定义默认激活函数,例如 Conv.default_act = nn.SiLU()
- if verbose:
- LOGGER.info(f"{colorstr('activation:')} {act}") # 打印
- if verbose:
- LOGGER.info(f"\n{'':>3}{'from':>20}{'n':>3}{'params':>10} {'module':<45}{'arguments':<30}")
- ch = [ch] # 存放第一个输入的通道数,这个ch后面会存放所有层的通道数,第一层为通道数是ch=3也就是对应我们一张图片的RGB图片的三基色三个通道,分别对应红绿蓝!
- layers, save, c2 = [], [], ch[-1] # 提前定义一些之后存放的容器分别为,模型层,保存列表,输出通道数
- """下面开始正式解析模型的yaml文件然后进行定义的操作用for训练便利yaml文件"""
- for i, (f, n, m, args) in enumerate(d["backbone"] + d["head"]): # from, number, module, args
- m = getattr(torch.nn, m[3:]) if "nn." in m else globals()[m] # 获取模块
- 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)
- """ 此处为repeat那个参数的放缩操作,不过多解释了,最小的n是1(就是是说你yaml文件里定义的是3,然后和放缩系数相乘然后和1比那个小取那个)"""
- n = n_ = max(round(n * depth), 1) if n > 1 else n
- """下面是一些具体模块的定义操作了"""
- if m in (Classify, Conv, ConvTranspose, GhostConv, Bottleneck, GhostBottleneck, SPP, SPPF, DWConv, Focus,
- BottleneckCSP, C1, C2, C2f, C2fAttn, C3, C3TR, C3Ghost, nn.ConvTranspose2d, DWConvTranspose2d, C3x, RepC3):
- c1, c2 = ch[f], args[0]
- if c2 != nc: # 如果 c2 不等于类别数(即 Classify() 输出)
- """ 绝大多数情况下都不等,我们放缩通道数,也就是为什么不同大小的模型参数量不一致的地方因为参数量主要由通道数决定,GFLOPs主要有图像的宽和高决定"""
- c2 = make_divisible(min(c2, max_channels) * width, 8)
- if m is C2fAttn:
- args[1] = make_divisible(min(args[1], max_channels // 2) * width, 8) # 嵌入通道数
- args[2] = int(
- max(round(min(args[2], max_channels // 2 // 32)) * width, 1) if args[2] > 1 else args[2]
- ) # 头部数量
- """此处需要解释一下,大家需要仔细注意此处"""
- """ 这个args就是传入到我们模型的参数,C1就是上一层的或者指定层的输出的通道数,C2就是本层的输出通道数, *args[1:]就是其它的一些参数比如卷积核步长什么的"""
- """ 此处和注意力机制不同的是,为什么注意力机制不在此处添加因为注意力机制不改变模型的维度,所以一般只需要指定一个输入通道数就行,
- 所以这也是为什么我们在后面定义注意力需要额外添加代码的原因有兴趣的读者可以对比一下"""
- args = [c1, c2, *args[1:]]
- """ 此处就是涉及的上面求出的实际的n然后插入的参数列表中去,然后准备在最下面进行传参"""
- if m in (BottleneckCSP, C1, C2, C2f, C2fAttn, C3, C3TR, C3Ghost, C3x, RepC3):
- args.insert(2, n) # 重复次数
- n = 1
- """这些都是一些具体的模块定义的方法,不多解释了"""
- elif m is AIFI:
- args = [ch[f], *args]
- elif m in (HGStem, HGBlock):
- c1, cm, c2 = ch[f], args[0], args[1]
- args = [c1, cm, c2, *args[2:]]
- if m is HGBlock:
- args.insert(4, n) # 重复次数
- n = 1
- elif m is ResNetLayer:
- c2 = args[1] if args[3] else args[1] * 4
- elif m is nn.BatchNorm2d:
- args = [ch[f]]
- elif m is Concat:
- c2 = sum(ch[x] for x in f)
- elif m in (Detect, WorldDetect, Segment, Pose, OBB, ImagePoolingAttn):
- args.append([ch[x] for x in f])
- if m is Segment:
- args[2] = make_divisible(min(args[2], max_channels) * width, 8)
- elif m is RTDETRDecoder: # 特殊情况,channels 参数必须在索引 1 中传递
- args.insert(1, [ch[x] for x in f])
- else:
- c2 = ch[f]
- """此处就是模型的正式定义和传参的操作"""
- m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args) # 模块
- t = str(m)[8:-2].replace("__main__.", "") # 模块类型
- m.np = sum(x.numel() for x in m_.parameters()) # 参数数量
- m_.i, m_.f, m_.type = i, f, t # 附加索引,'from' 索引,类型
- if verbose:
- LOGGER.info(f"{i:>3}{str(f):>20}{n_:>3}{m.np:10.0f} {t:<45}{str(args):<30}") # 打印
- """此处就是保存一些索引通道数涉及到from的部分,此处文字很难解释的清楚有兴趣可以自己debug看一下就明白了"""
- save.extend(x % i for x in ([f] if isinstance(f, int) else f) if x != -1) # 添加到保存列表
- layers.append(m_)
- if i == 0:
- ch = []
- ch.append(c2)
- return nn.Sequential(*layers), sorted(save)
四、模型的结构打印
经过上面的分析之后,我们就会打印了模型的结构,图片如下所示,然后到此本篇文章的分析就到这里了,剩下的下一篇文章讲解。
(需要注意的是上面的讲解整体是按照顺序但是是以递归的形式介绍,比如3.2是3.1当中的某一行代码的功能而不是结束之后才允许的3.2,而是3.1运行的过程中运行了3.2。)
五、本文总结
到此本文的正式分享内容就结束了,在这里给大家推荐我的YOLOv11改进有效涨点专栏,本专栏目前为新开的平均质量分98分,后期我会根据各种最新的前沿顶会进行论文复现,也会对一些老的改进机制进行补充,如果大家觉得本文帮助到你了,订阅本专栏,关注后续更多的更新~