前言

Hello,大家好,我是GISer Liu😁,一名热爱AI技术的GIS开发者。本系列是作者参加DataWhale 2025年6月份Yolo原理组队学习的技术笔记文档,这里整理为博客,希望能帮助Yolo的开发者少走弯路!

🚀 欢迎来到YOLO进阶系列教程的核心,也是最后一篇文章——模型“魔改”!在目标检测领域,YOLO系列凭借其卓越的速效平衡成为了标杆。然而,无论是为了发表学术论文,还是应对复杂多变的业务场景,仅仅满足于使用官方模型、调整参数是远远不够的。我们需要的,是突破“调参工程师”的局限,真正深入模型内部,进行结构级的创新。

这正是Datawhale YOLO Master项目的初衷。它提供了一套即插即用的先进模块和一套系统性的魔改方法论,旨在帮助开发者:

  1. 系统性理解YOLO架构:拆解模型为Backbone、Neck、Head等核心组件。
  2. 掌握模块化创新:像搭乐高一样,将前沿的模块(如SwinTransformer, CBAM等)无缝集成到YOLOv8/v10/v11中。
  3. 提升工程与科研能力:从源码层面理解并改造SOTA模型,为自己的项目或研究注入创新力。

本教程将手把手带你走完从环境准备到模型改造、训练的全过程。无论你是希望在CV领域深造的大学生,还是寻求技术突破的开发者,相信本教程都能为你提供坚实的起点。OK,让我们开始“造”自己的YOLO吧!


更新记录:(本文随时更新)

20250709:本文当前只是理论的堆砌,阅读感觉并不好;不够直观;后续我作者通过一个具体的需求,例如遥感影像目标识别去魔改我们的yolo模型结构;测试性能的变化;


一、YOLO“魔改”:从“调参”到“改结构”

1. 为什么要“魔改”YOLO?

标准的YOLO模型虽然强大,但在特定任务上未必是最优解。例如,在遥感影像中检测微小目标,或是在工业流水线上识别密集物体,都对模型的特征提取能力、多尺度融合等方面提出了更高的要求。此时,仅仅调整学习率、优化器等超参数,带来的性能提升是有限的。

真正的突破来自于对网络结构的创新——也就是我们常说的“魔改”。这好比我们不是简单地调整一辆车的悬挂软硬(调参),而是给它换上一台更强劲的发动机或者更先进的空气动力学套件(魔改)。

模型魔改是网络结构上的修改和替换,而非简单调参;这需要开发者对模型组成和原理有深刻的理解🤔

2. “魔改”的哲学:像搭乐高一样构建网络

YOLO Master项目的核心思想是将复杂的神经网络解构成一系列可插拔的、标准化的“积木块”。YOLO模型经典的三段式结构(主干、颈部、头部)为这种模块化改造提供了完美的框架。

mermaid

  • 主干网络 (Backbone):负责从输入图像中提取基础特征,是模型的“地基”。我们可以将其替换为更先进的结构,如SwinTransformerConvNeXtV2等,以获取更强的特征表达能力。
  • 颈部网络 (Neck):负责融合主干网络在不同阶段提取出的特征图,增强模型对不同尺寸目标的感知能力。可以引入GFPN等结构进行优化。
  • 检测头 (Head):根据融合后的特征进行最终的边界框回归和类别预测。我们可以尝试DyHead等动态头部来提升检测性能。
  • 注意力机制 (Attention):像“插件”一样,可以插入到网络中的任何位置,让模型“关注”到最重要的特征区域。CBAMSE是常用的选择。

二、准备工作:搭建你的“魔改”基础环境

在开始之前,我们需要准备好两个核心的代码库:ultralytics官方库和yolo-master魔改项目库。

1. 克隆项目仓库

我们提供三种下载方式,推荐使用git clone,如果遇到网络问题,可以尝试国内的GitCode镜像。

① 下载 ultralytics 源码
# 方法一:从GitHub直接克隆 (推荐)
git clone https://github.com/ultralytics/ultralytics.git# 方法二:从国内镜像GitCode克隆
git clone https://gitcode.com/gh_mirrors/ul/ultralytics.git

file_catalog

其中,我们要对ultralytics文档目录结构有个相对完整的了解:

ultralytics/​​​​
​assets:静态资源:测试图像、预训练模型等示例文件
cfg:配置文件中心​​
- datasets/:数据集定义(路径、类别、预处理)
- models/:模型架构配置(YOLOv8n/v8s/v8m等)
- trackers/:跟踪算法参数
- default.yaml:全局默认配置(训练/推理/导出参数)
​data/:数据预处理与增强逻辑
engine/:核心功能引擎
- exporter.py:模型导出(ONNX/TensorRT等)
- model.py:模型生命周期管理
- predictor.py:推理接口
- trainer.py:训练流程控制
- validator.py:验证指标计算
​hub/​​:PyTorch Hub 集成接口
models/​​:模型架构实现
- yolo/YOLO系列主代码
- detect/:检测任务
- segment/:分割任务
- pose/:姿态估计
- classify/:分类任务
- model.py:模型构建核心
- rtdetr/:实时DETR架构
- nas/:神经架构搜索
- sam/SAM优化策略
- fastsam/:快速分割模型
​nn/​​:神经网络组件:自定义层/激活函数等
​solutions/​​:高级应用模块:线计数/热力图生成等场景化解决方案
trackers/​​:多目标跟踪(MOT)算法实现
utils/​​:工具库
- callbacks/:训练回调函数
- metrics.py:性能评估指标
- plotting.py:可视化工具
- torch_utils.py:PyTorch扩展功能
- downloads.py:资源下载管理
- ops.py:张量操作扩展
​init.py​:包初始化入口(版本/API暴露)
② 下载 yolo-master 魔改教程源码
git clone https://github.com/datawhalechina/yolo-master.git

2. 环境配置

进入yolo-master目录,其中包含了requirements.txt文件,我们可以使用pip进行安装。为了加速下载,建议使用国内的清华镜像。

# 进入yolo-master项目目录
cd yolo-master# 安装依赖,-e . 表示以可编辑模式安装ultralytics
# 假设你的ultralytics目录与yolo-master在同一级
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install -e ../ultralytics

pip install -e .-e 代表 “editable”(可编辑)。这种方式安装后,你对 ultralytics 源码的任何修改(比如我们后续的“魔改”操作)会立刻生效,而无需重新安装。这对于模型开发和调试至关重要。

config


三、主干(BackBone)的替换

要实现真正的“即插即用”,我们需要对ultralytics的源码进行一些通用性的改造,让它能够识别并正确处理我们添加的自定义模块,特别是复杂的主干网络。修改的核心位于ultralytics/nn/tasks.py文件中,这个文件负责解析YAML配置文件并构建整个神经网络模型。

1. 魔改的挑战与思路

挑战ultralytics原生的parse_model函数设计时,主要考虑的是构建由一系列“标准”层(如ConvC2fConcat)组成的网络。这些层有一个共同点:输入一个张量,输出一个张量。而我们想替换的先进主干网络(如RepViT, SwinTransformer等)通常是作为一个整体模块,它输入一个图像张量,一次性输出多个不同尺度的特征图(例如,同时输出P3, P4, P5三个层级的特征)。原生的解析和前向传播逻辑无法直接处理这种“一对多”的复杂模块。

解决思路:我们的魔改思路可以分为两步,就像进行一次精密的“外科手术”:

  1. 改造parse_model函数(模型构建阶段):让它在解析YAML时,能够“识别”出我们自定义的、作为整体的主干网络。识别后,它需要特殊处理:不再将它看作一个普通层,而是作为一个特殊的“多输出模块”,并正确记录下它所有输出头的通道信息。
  2. 改造_predict_once函数(模型推理阶段):让它在前向传播时,如果遇到这个被标记过的特殊主干网络,就执行特殊的传播逻辑。这个逻辑会一次性接收主干网络输出的多个特征图,并将它们正确地存放到一个列表中(y列表),以供后续的Neck网络层使用。

2. 实战:一步步改造YOLOv8主干

主干网络是决定模型性能的基石。YOLO Master提供了大量先进的Backbone供我们选择。

可选主干网络核心思想
RepViT融合CNN的效率和ViT的性能
StarNet轻量级、高效的星状结构
EfficientViT高效的Vision Transformer变体
FasterNet极速推理,专注于硬件友好
ConvNeXtV2现代化的纯卷积网络,性能媲美Transformer
SwinTransformer经典的层级化窗口注意力Transformer
VanillaNet极简主义设计,返璞归真但效果强大
(还有更多)

让我们以RepViT为例,演示完整的替换步骤。
接下来,我们将以RepViT为例,完整地展示如何实现这一“手术”。这个方法具有普适性,适用于本文中介绍的所有主干网络。

① 准备模块代码和配置文件
  • 第一步:安放模块代码
    ultralytics/ultralytics/nn/目录下,创建一个新文件夹new_modules。然后,将yolo-master项目中的Backbone_RepViT.py文件复制到这个新文件夹中,并重命名为repvit.py
    在这里插入图片描述
    在这里插入图片描述

    • 目的:将我们自定义的模块代码与ultralytics的官方代码分离开,便于管理和维护。
  • 第二步:安放YAML文件
    yolo-master中的RepViT-P345.yaml文件复制到ultralytics/cfg/models/v8/(或者你指定的其他版本目录)下。这个YAML文件描述了使用RepViT作为主干的网络结构。
    在这里插入图片描述

    • 目的:让YOLO的训练引擎能够找到并加载我们的新模型定义。
② 引用新模块

打开ultralytics/nn/tasks.py文件,在文件的开头部分,找到导入模块的区域,添加以下代码:

# ultralytics/nn/tasks.py# ... 其他 import 语句 ...
from ultralytics.utils.torch_utils import (fuse_conv_and_bn, fuse_deconv_and_bn, initialize_weights, intersect_dicts,make_divisible, model_info, scale_img, time_sync)# ==================== 在这里添加我们的新模块导入 ====================
from .new_modules.repvit import *
from .new_modules.starnet import * # 为后续其他模块预留
from .new_modules.efficientvit import * # 为后续其他模块预留
# ... 你可以继续添加其他自定义模块的导入 ...
# ===================================================================#LOGGER = logging.getLogger(__name__)
# ... 文件后续内容 ...

import

  • 目的:将repvit.py中定义的所有类(如repvit_m2_3)导入到tasks.py的全局命名空间中,这样parse_model函数在解析YAML时才能通过字符串名字找到并实例化它们。
③ 核心改造一:parse_model函数

tasks.py中,使用Ctrl+F找到parse_model函数。这是整个魔改工程的核心。我们将分三部分进行修改。

i) 修改读取模型参数部分(增强解析器的灵活性)

这部分修改的目的是让解析器更加健壮和灵活,能够处理更复杂的参数和模块名,为后续所有类型的魔改(包括主干、颈部等)打下基础。
原代码:

 for i, (f, n, m, args) in enumerate(d['backbone'] + d['head']):  # from, number, module, argsm = getattr(torch.nn, m[3:]) if 'nn.' in m else globals()[m]  # get modulefor 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)```* **修改后代码**:```pythonis_backbone = Falsefor i, (f, n, m, args) in enumerate(d['backbone'] + d['head']):  # from, number, module, args# ==================== 增强的模块获取逻辑 ====================# 原理:使用 try-except 块来优雅地处理模块查找。# 原生代码直接使用 globals()[m] 查找,如果m不是一个已知的模块名,程序会报错退出。# 修改后,我们尝试获取模块,如果失败(比如m是一个我们后续要特殊处理的字符串),# 就暂时跳过,给予后续代码处理它的机会。try:if m == 'node_mode':  # 为更复杂的颈部(如GFPN)预留的逻辑m = d[m]if len(args) > 0:if args[0] == 'head_channel':args[0] = int(d[args[0]])t = m # 临时保存模块名字符串,用于打印日志m = getattr(torch.nn, m[3:]) if 'nn.' in m else globals()[m]  # get moduleexcept:pass # 如果在globals()中找不到模块名,暂时忽略# ==========================================================# ==================== 增强的参数解析逻辑 ====================# 原理:同样使用 try-except 块增强鲁棒性。# 有些参数可能是字符串(如路径),ast.literal_eval 会解析失败。# 修改后,如果解析失败,就保持其原始的字符串类型。for j, a in enumerate(args):if isinstance(a, str):with contextlib.suppress(ValueError):try:args[j] = locals()[a] if a in locals() else ast.literal_eval(a)except:args[j] = a # 解析失败时,保留为字符串# ==========================================================
ii) 添加自定义主干的参数接收逻辑

这是识别我们自定义主干的“秘密握手”。

  • 实现思路:我们在parse_model函数的循环内部,计算每个模块的输出通道数c2之后,添加一个判断。如果m是我们自定义的主干网络类(如repvit_m2_3),我们就调用它特有的一个方法(我们约定所有自定义主干都实现一个名为channel的属性或方法)来获取它所有输出层的通道列表。

c1, c2 = ch[f], args[0]这行代码之后,添加如下逻辑:

# ... 在 parse_model 函数的循环内 ...if m in (Classify, Detect, RTDETR, Segment):# ... 省略 ...elif m is nn.BatchNorm2d:# ... 省略 ...else:c2 = ch[f] if c2 == -1 else c2# ==================== 自定义主干接收参数部分 ====================# 原理:这是识别自定义主干的核心。# 我们约定,所有即插即用的主干网络模块,都会有一个名为 `channel` 的属性,# 这个属性返回一个列表,包含了它所有输出特征图的通道数。# 通过检查模块 m 是否有 'channel' 属性,我们就能识别出它。if hasattr(globals()[t], 'channel'): # 使用临时变量t(模块名字符串)来检查# 实例化主干网络。注意,这里的m是模块的类本身。m = m()# 获取输出通道列表,例如 [128, 256, 512]c2 = m.channel# ==============================================================m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args)  # module# ... 后续代码 ...
iii) 修改模型实例化部分

这部分是整个改造的“执行”阶段。在这里,我们将真正地区分处理标准层和我们的自定义主干。

原代码:

    # 原代码部分一m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args)  # modulet = str(m)[8:-2].replace('__main__.', '')  # module typem_.np = sum(x.numel() for x in m_.parameters())  # number paramsm_.i, m_.f, m_.type = i, f, t  # attach index, 'from' index, typeif verbose:LOGGER.info(f'{i:>3}{str(f):>20}{n_:>3}{m_.np:10.0f}  {t:<45}{str(args):<30}')  # printsave.extend(x % i for x in ([f] if isinstance(f, int) else f) if x != -1)  # append to savelist# 原代码部分二if i == 0:ch = []ch.append(c2)

修改后代码 (整合了ii和iii的逻辑)

    # ... 在 c1, c2 = ch[f], args[0] 之后 ...# 这是更完整的逻辑,取代了ii)中的简单判断if m in (Conv, ConvTranspose, GhostConv, Bottleneck, GhostBottleneck, SPP, SPPF, DWConv, Focus,BottleneckCSP, C1, C2, C2f, C3, C3TR, C3Ghost, nn.Conv2d, DWConvTranspose, C3x, RepC3):c2 = m.get_nc(ch, f, args)  # get c2 output channels# ... (省略其他elif)# ==================== 修改后的模型实例化 ====================# --- 步骤 1: 实例化与识别 ---# 巧妙的识别方法:我们先按常规方式实例化模块。# 如果 m 是我们的自定义主干,它没有输入参数,直接 m() 即可。# 然后,我们检查它的 `channel` 属性,得到 `c2`。# 如果 `c2` 是一个列表,就说明这是一个自定义主干!if isinstance(c2, list):# 是自定义主干,is_backbone标志位设为Trueis_backbone = True# m_ 直接就是我们实例化的主干对象 m (在之前的步骤中已经 m=m() )m_ = m# 给模块实例动态添加一个 'backbone' 属性,值为 True。# 目的:这是一个“标记”,为了在后续的前向传播 `_predict_once` 函数中识别它。m_.backbone = Trueelse:# 是标准层,按原逻辑构建m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args)  # modulet = str(m)[8:-2].replace('__main__.', '')  # module type# --- 步骤 2: 计算参数并附加信息 ---# 计算参数量m_.np = sum(x.numel() for x in m_.parameters())  # number params# 附加索引信息。注意这里的 `i+4`# 原理:我们的自定义主干会输出多个特征图,为了给这些特征图在内部留出索引位置(0,1,2,3),# 我们将主干模块本身的索引号人为地增加,例如 `0 -> 4`。# 这样,后续Neck部分的层索引就不会与Backbone的输出索引冲突。# 4是一个经验值,通常主干输出P2-P5四层特征,但只要比主干输出层数多即可。m_.i, m_.f, m_.type = i + 4 if is_backbone else i, f, t  # attach index, 'from' index, typeif verbose:LOGGER.info(f'{i:>3}{str(f):>20}{n_:>3}{m_.np:10.0f}  {t:<45}{str(args):<30}')  # print# 将需要保存的层的索引添加到 savelist。同样,对主干索引进行偏移。save.extend(x % (i + 4 if is_backbone else i) for x in ([f] if isinstance(f, int) else f) if x != -1)# --- 步骤 3: 追踪通道数 ---if i == 0:ch = []# 关键修改!if isinstance(c2, list):# 如果 c2 是列表 (我们的主干),则用 extend 将所有输出通道加入 ch 列表ch.extend(c2)# 补位操作:确保 ch 列表的长度至少为5。# 目的:为了与 `_predict_once` 中的逻辑对齐,方便通过索引访问不同尺度的特征。# 即使某个主干不输出P1, P2层,也用0占位,避免索引错误。for _ in range(5 - len(ch)):ch.insert(0, 0)else:# 如果是标准层,按原逻辑 append 单个输出通道ch.append(c2)# ==============================================================
④ 核心改造二:_predict_once函数

这个函数负责执行模型的前向传播。我们需要修改它,以便能正确处理我们标记了backbone=True的特殊模块。

原代码:

    def _predict_once(self, x, profile=False, visualize=False, embed=None):y, dt, embeddings = [], [], []  # outputsfor m in self.model:if m.f != -1:  # if not from previous layerx = y[m.f] if isinstance(m.f, int) else [x if j == -1 else y[j] for j in m.f]  # from earlier layersif profile:self._profile_one_layer(m, x, dt)x = m(x)  # runy.append(x if m.i in self.save else None)  # save output# ... 省略 visualize 和 embed 的代码 ...return x

修改后代码

    def _predict_once(self, x, profile=False, visualize=False, embed=None):y, dt, embeddings = [], [], []  # outputsfor m in self.model:if m.f != -1:  # if not from previous layerx = y[m.f] if isinstance(m.f, int) else [x if j == -1 else y[j] for j in m.f]  # from earlier layersif profile:self._profile_one_layer(m, x, dt)# ==================== 自定义主干前向传播逻辑 ====================# 原理:检查在 parse_model 中添加的 'backbone' 标记。if hasattr(m, 'backbone'):# 如果是主干模块,直接调用它,它会返回一个特征图列表x = m(x)# 补位操作,与 parse_model 中的逻辑对应。# 目的:确保输出列表 x 的长度固定,即使主干输出的特征图数量不同,# 后续的层可以通过固定的索引(如 y[4])来获取特征。for _ in range(5 - len(x)):x.insert(0, None) # 用 None 填充不存在的低层特征# 遍历主干输出的每一层特征图for i_idx, i in enumerate(x):# 根据 savelist 判断这一层是否需要保存给后续层使用if i_idx in self.save:y.append(i)else:y.append(None) # 如果不需要,用 None 占位# 将主干的最后一层输出作为下一个模块的输入x = x[-1]else:# 如果是标准层,执行原有的逻辑x = m(x)  # runy.append(x if m.i in self.save else None)  # save output# ==============================================================if visualize:feature_visualization(x, m.type, m.i, save_dir=visualize)if embed and m.i in embed:# ... (省略)return x

如此修改后,模型的网络结构就会发生变化:
new_networkl


四、颈部(Neck)的替换

我们已经成功地改造了tasks.py,建立了一个强大的、可兼容自定义主干的框架。现在,我们将利用这个框架,对模型的“颈部”进行替换。

1-GFPN (GiraffeDet FPN)

GFPN(长颈鹿特征金字塔网络)通过其独特的、类似长颈鹿脖子的交错连接方式,高效地融合深层语义信息和浅层空间信息。我们将用它来替换YOLOv8原生的PANet结构。

① 文件准备与引用(此步骤与之前一致)
  1. 代码Neck-GFPN.py -> new_modules/GFPN.py
  2. 配置GFPN-P345.yaml -> cfg/models/v8/
  3. 引用:在 tasks.py 中添加 from .new_modules.GFPN import *
② GFPN的YAML配置与parse_model的联动

思考:GFPN是如何被我们的新parse_model函数解析的?

让我们深入GFPN-P345.yamltasks.py的代码;。

  • 第一步:分析GFPN-P345.yaml的配置

    打开GFPN-P345.yaml,你会发现它巧妙地使用变量来定义网络结构,这是一种非常优雅的工程实践。

    # ultralytics/cfg/models/v8/GFPN-P345.yaml (内容示例)# ------------------ YAML 顶层参数定义 ------------------
    # 定义了颈部和头部的默认通道数
    widen_factor: 1.0
    head_channel: 256# 核心:定义了颈部中重复使用的核心模块的名称
    # 这样做的好处是,如果想把所有 CSPStage 换成其他模块,
    # 只需修改下面这一行,无需改动 head 中的每一处。
    node_mode: CSPStage# ... (nc, depth_multiple, width_multiple 等定义)# ------------------ Backbone (主干) ------------------
    backbone:- [-1, 1, Conv, [64, 3, 2]]              # 0-P1/2# ... (主干网络定义,这里可能是标准的YOLOv8主干,也可能是我们之前替换的RepViT等)# 假设主干的第4、6、9层分别输出P3, P4, P5 特征# ------------------ Head (颈部 + 头部) ------------------
    head:# in_channels: [256, 512, 1024] # 来自Backbone的 P3, P4, P5# out_channels: [256, 512, 1024]# --- GFPN 颈部结构 ---- [-1, 1, Conv, [256, 1, 1]]              # 10- [-1, 1, nn.Upsample, [None, 2, 'nearest']] # 11- [[-1, 6], 1, Concat, [1]]               # 12 (Concat P4)- [-1, 3, node_mode, [head_channel, 3]]   # 13 <--- 关键!使用了node_mode- [-1, 1, Conv, [256, 1, 1]]              # 14- [-1, 1, nn.Upsample, [None, 2, 'nearest']] # 15- [[-1, 4], 1, Concat, [1]]               # 16 (Concat P3)- [-1, 3, node_mode, [head_channel, 3]]   # 17 <--- 关键!使用了node_mode- [-1, 1, Conv, [256, 3, 2]]              # 18- [[-1, 14], 1, Concat, [1]]              # 19- [-1, 3, node_mode, [head_channel, 3]]   # 20 <--- 关键!使用了node_mode- [-1, 1, Conv, [256, 3, 2]]              # 21- [[-1, 10], 1, Concat, [1]]              # 22- [-1, 3, node_mode, [head_channel, 3]]   # 23 <--- 关键!使用了node_mode# --- Detect Head ---- [[17, 20, 23], 1, Detect, [nc]]        # Detect(P3, P4, P5)
    
  • 第二步:追踪parse_model的执行流程

    parse_model函数解析到第13层 [-1, 3, node_mode, [head_channel, 3]] 时,我们之前修改的代码开始发挥作用:

    # 在 ultralytics/nn/tasks.py 的 parse_model 中# 此时,循环变量的值为:
    # i = 13, f = -1, n = 3, m = 'node_mode', args = ['head_channel', 3]try:# 1. 检查到 m == 'node_mode',条件成立if m == 'node_mode':# 2. 将 m 的值从字符串 'node_mode' 替换为 YAML 顶层定义的实际值# m = d['node_mode']  -->  m 变成了 'CSPStage'm = d[m]# 3. 检查 args 列表if len(args) > 0: # len(['head_channel', 3]) > 0, 成立# 4. 检查第一个参数是否为 'head_channel'if args[0] == 'head_channel': # 成立# 5. 将 args[0] 从字符串 'head_channel' 替换为 YAML 顶层定义的实际值# args[0] = int(d['head_channel']) --> args[0] 变成了整数 256args[0] = int(d[args[0]])# 经过处理后,模块定义从 'node_mode', ['head_channel', 3]# 变成了实际的 'CSPStage', [256, 3]t = m # t 被赋值为 'CSPStage'# 6. 最后,通过 globals()['CSPStage'] 找到我们导入的 CSPStage 类m = getattr(torch.nn, m[3:]) if 'nn.' in m else globals()[m]
    except:pass
    # ... 后续代码将使用 m = CSPStage 类, args = [256, 3] 来实例化模块
    

    结论:我们为parse_model添加的try-exceptif m == 'node_mode'逻辑,本质上是创建了一个“宏替换”机制。它使得YAML的编写者可以像定义宏一样预设模块名和参数,极大地增强了配置文件的灵活性和复用性。

③ 魔改前后模型参数对比

我们可以通过打印模型结构来直观地看到变化。

魔改前模型结构 (标准 YOLOv8 Neck)

    # yolov8.yaml summary: 225 layers, 3157200 parameters, 3157184 gradients, 8.9 GFLOPs... (backbone)10  -1  1  ultralytics.nn.modules.conv.Conv         [512, 1, 1]11  -1  1  torch.nn.modules.upsampling.Upsample     [None, 2, 'nearest']12  [-1, 6]  1  ultralytics.nn.modules.container.Concat  [1]13  -1  3  ultralytics.nn.modules.block.C2f         [512, True]14  -1  1  ultralytics.nn.modules.conv.Conv         [256, 1, 1]15  -1  1  torch.nn.modules.upsampling.Upsample     [None, 2, 'nearest']16  [-1, 4]  1  ultralytics.nn.modules.container.Concat  [1]17  -1  3  ultralytics.nn.modules.block.C2f         [256, True]18  -1  1  ultralytics.nn.modules.conv.Conv         [256, 3, 2]19  [-1, 14] 1  ultralytics.nn.modules.container.Concat  [1]20  -1  3  ultralytics.nn.modules.block.C2f         [512, True]21  -1  1  ultralytics.nn.modules.conv.Conv         [512, 3, 2]22  [-1, 10] 1  ultralytics.nn.modules.container.Concat  [1]23  -1  3  ultralytics.nn.modules.block.C2f         [1024, True]24  [17, 20, 23] 1 ultralytics.nn.modules.head.Detect [80]

魔改后模型结构 (GFPN-P345 Neck)

    # GFPN-P345.yaml summary: 237 layers, 3348644 parameters, 3348628 gradients, 9.2 GFLOPs... (backbone)10  -1  1  ultralytics.nn.modules.conv.Conv         [256, 1, 1]11  -1  1  torch.nn.modules.upsampling.Upsample     [None, 2, 'nearest']12  [-1, 6]  1  ultralytics.nn.modules.container.Concat  [1]13  -1  3  new_modules.GFPN.CSPStage                [256, 3]  # <-- 核心模块被替换14  -1  1  ultralytics.nn.modules.conv.Conv         [256, 1, 1]15  -1  1  torch.nn.modules.upsampling.Upsample     [None, 2, 'nearest']16  [-1, 4]  1  ultralytics.nn.modules.container.Concat  [1]17  -1  3  new_modules.GFPN.CSPStage                [256, 3]  # <-- 核心模块被替换18  -1  1  ultralytics.nn.modules.conv.Conv         [256, 3, 2]19  [-1, 14] 1  ultralytics.nn.modules.container.Concat  [1]20  -1  3  new_modules.GFPN.CSPStage                [512, 3]  # <-- 核心模块被替换21  -1  1  ultralytics.nn.modules.conv.Conv         [512, 3, 2]22  [-1, 10] 1  ultralytics.nn.modules.container.Concat  [1]23  -1  3  new_modules.GFPN.CSPStage                [1024, 3] # <-- 核心模块被替换24  [17, 20, 23] 1 ultralytics.nn.modules.head.Detect [80]

通过对比可以清晰地看到,原生的C2f模块被我们自定义的CSPStage(来自GFPN.py)所取代,这证明我们的魔改成功了。


五、头部(Head)的革新

1-DyHead (Dynamic Head)

DyHead通过统一的注意力机制,动态地选择最重要的特征,极大地提升了检测头的表征能力。

① 文件准备与引用(同上)
  1. 代码: Head-DyHead.py -> new_modules/DyHead.py
  2. 配置: DyHead-P345.yaml -> cfg/models/v8/
  3. 引用: tasks.py 中添加 from .new_modules.DyHead import *
② 核心解析:DyHead的“无缝”集成

思考:DyHead为什么不需要对tasks.py做任何新的修改?

答案在于,DyHead的设计模式与YOLO原生Detect头高度兼容,并且我们之前对parse_model的通用化改造已经能够处理它。

第一步:分析DyHead-P345.yaml的结构
DyHead模块将替换掉原Detect层以及之前的一些卷积层。

    # DyHead-P345.yaml (head部分示例)head:# ... (颈部融合层)# 假设颈部输出三层特征分别在索引 17, 20, 23# 原生YOLOv8中,这里会有一系列解耦的卷积层,最后连接Detect层# 使用DyHead后,直接将三层特征输入给DyHead模块- [[17, 20, 23], 1, DyHead, [nc]] # <--- 直接替换

第二步:追踪_predict_once的执行流程
当模型前向传播到DyHead层时:

    # 在 ultralytics/nn/tasks.py 的 _predict_once 中# 此时,m 是实例化的 DyHead 对象,m.f 是 [17, 20, 23]# 1. 进入获取输入的逻辑if m.f != -1:  # 成立# 2. m.f 是一个列表, 执行 else 分支#    这行代码会从 y 列表中,根据索引 17, 20, 23,#    取出对应的三层特征图张量,并打包成一个新的列表 `x`#    x = [y[17], y[20], y[23]]x = y[m.f] if isinstance(m.f, int) else [x if j == -1 else y[j] for j in m.f]# 3. 检查 'backbone' 属性, DyHead没有这个标记, 跳过if hasattr(m, 'backbone'):...else:# 4. 执行常规的前向传播#    将包含三个特征图的列表 x, 整体传递给 DyHead 模块#    x = m(x)  等价于  outputs = dyhead_instance([feature_p3, feature_p4, feature_p5])x = m(x)y.append(x if m.i in self.save else None)

结论DyHead模块本身被设计为接收一个特征图列表作为输入。而YOLO的_predict_once函数中,处理多输入的from(如[-1, 6][17, 20, 23])的逻辑,天然地就会将多个来源的特征图打包成一个列表。两者一拍即合,实现了无缝对接。我们不需要为DyHead编写任何特殊的解析或执行代码。


六、注意力机制(Attention)的融合

欢迎来到“魔改”系列中最灵活、最有趣的部分——集成注意力机制。

核心思想与类比:想象一下,当您在一张杂乱的桌面上寻找钥匙时,您的大脑并不会平均地扫描每一个平方厘米。相反,您的目光会自动聚焦于桌面上的高亮区域,比如金属反光处、颜色鲜艳的物体旁。注意力机制(Attention Mechanism)赋予了神经网络类似的能力。它让模型在处理海量信息时,能够智能地“聚焦”于最关键的特征,并“忽略”次要或无关的背景,从而用有限的计算资源做出更精准的判断。

本节,我们将以CBAM (Convolutional Block Attention Module) 为例,进行一次完整、详尽的“即插即用”式集成。我们将一起分析其代码原理,选择合适的插入位置,完成一次无死角的YAML文件修改,并最终验证我们的工作。

1. 深入理解CBAM模块

在动手之前,我们先快速理解CBAM的工作原理,这将有助于我们决定将它放在网络中的哪个位置。

CBAM由两个串联的子模块组成:

  1. 通道注意力模块 (Channel Attention):它回答的问题是“什么特征更重要?”。比如,在一个人像识别任务中,包含“眼睛”、“鼻子”等信息的特征通道,其重要性就应该高于包含“背景墙壁”信息的通道。该模块会学习一个权重,对各个通道进行加权,增强重要特征,抑制次要特征。
  2. 空间注意力模块 (Spatial Attention):它回答的问题是“特征图的哪个位置更重要?”。在识别出一张人脸后,其五官所在的位置显然比背景区域更关键。该模块会生成一个空间“热力图”,告诉网络应该重点关注特征图上的哪些像素区域。

最重要的特性:CBAM模块的forward函数接收一个张量x,经过内部一系列计算后,输出一个与x 尺寸完全相同 的张量。这正是它能被“即插即用”的关键。

2. 集成CBAM

① 准备工作
  1. 代码: 将 yolo_master/.../Attention-CBAM.py 复制到 ultralytics/ultralytics/nn/new_modules/ 并重命名为 CBAM.py
  2. 引用: 在 ultralytics/nn/tasks.py 的顶部添加 from .new_modules.CBAM import *
② CBAM应该放在哪里?

这是一个开放性问题,但有一些常用的策略:

  • 放在特征提取之后:通常将注意力模块放置在核心特征提取块(如C2f)之后。这样做的好处是,C2f已经产生了丰富的特征组合,此时使用CBAM可以立刻对这些新特征进行“精炼”和“筛选”,让最有用的信息传递给下一层。
  • 放在下采样之前:在网络通过步进卷积(Conv)进行下采样、缩小特征图尺寸之前,使用CBAM可以确保在信息被压缩前,关键特征已经被充分“关注”,减少重要信息的丢失。

本教程决策:我们将遵循以上策略,选择在Backbone的第4个模块(一个C2f层)之后,第5个模块(一个Conv下采样层)之前插入CBAM。这个位置非常理想,它能精炼P3级别的特征,再将其传递给更深的网络。

③ YAML修改

这是最关键的一步。我们将以yolov8n.yaml为蓝本,创建yolov8n-CBAM.yaml

  • 第一步:复制并重命名yolov8n.yamlyolov8n-CBAM.yaml

  • 第二步:进行修改。 下面是完整的修改前后对比,所有改动都用注释明确标出。

yolov8n.yaml (原始文件)

# ultralytics/cfg/models/v8/yolov8n.yamlnc: 80  # number of classes
depth_multiple: 0.33  # model depth multiple
width_multiple: 0.25  # layer channel multiple# anchors
anchors:- [10,13, 16,30, 33,23]  # P3/8- [30,61, 62,45, 59,119]  # P4/16- [116,90, 156,198, 373,326]  # P5/32# YOLOv8.0n backbone
backbone:# [from, number, module, args]- [-1, 1, Conv, [64, 3, 2]]  # 0-P1/2- [-1, 1, Conv, [128, 3, 2]]  # 1-P2/4- [-1, 3, C2f, [128, True]]  # 2- [-1, 1, Conv, [256, 3, 2]]  # 3-P3/8- [-1, 6, C2f, [256, True]]  # 4- [-1, 1, Conv, [512, 3, 2]]  # 5-P4/16- [-1, 6, C2f, [512, True]]  # 6- [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32- [-1, 3, C2f, [1024, True]] # 8- [-1, 1, SPPF, [1024, 5]]   # 9# YOLOv8.0n head
head:- [-1, 1, nn.Upsample, [None, 2, 'nearest']] # 10- [[-1, 6], 1, Concat, [1]]  # 11- [-1, 3, C2f, [512, False]] # 12- [-1, 1, nn.Upsample, [None, 2, 'nearest']] # 13- [[-1, 4], 1, Concat, [1]]  # 14- [-1, 3, C2f, [256, False]] # 15- [-1, 1, Conv, [256, 3, 2]] # 16- [[-1, 12], 1, Concat, [1]] # 17- [-1, 3, C2f, [512, False]] # 18- [-1, 1, Conv, [512, 3, 2]] # 19- [[-1, 9], 1, Concat, [1]]  # 20- [-1, 3, C2f, [1024, False]]# 21- [[15, 18, 21], 1, Detect, [nc]]  # Detect(P3, P4, P5)

yolov8n-CBAM.yaml (修改后的文件)

# ultralytics/cfg/models/v8/yolov8n-CBAM.yaml# ... (nc, depth_multiple, width_multiple, anchors 定义与上面完全相同) ...# YOLOv8.0n backbone with CBAM
backbone:# [from, number, module, args]- [-1, 1, Conv, [64, 3, 2]]      # 0- [-1, 1, Conv, [128, 3, 2]]     # 1- [-1, 3, C2f, [128, True]]      # 2- [-1, 1, Conv, [256, 3, 2]]     # 3- [-1, 6, C2f, [256, True]]      # 4- [-1, 1, CBAM, [256]]           # 5  <--- 新增CBAM层. 它接收第4层的256通道输出- [-1, 1, Conv, [512, 3, 2]]     # 6 (原索引为5)- [-1, 6, C2f, [512, True]]      # 7 (原索引为6)- [-1, 1, Conv, [1024, 3, 2]]    # 8 (原索引为7)- [-1, 3, C2f, [1024, True]]     # 9 (原索引为8)- [-1, 1, SPPF, [1024, 5]]       # 10 (原索引为9)# YOLOv8.0n head with updated indices
head:- [-1, 1, nn.Upsample, [None, 2, 'nearest']] # 11# Concat 融合 Neck P5 和 Backbone P4. Backbone P4 现在是第7层- [[-1, 7], 1, Concat, [1]]      # 12 (原为[-1, 6]) <--- 索引更新!- [-1, 3, C2f, [512, False]]     # 13- [-1, 1, nn.Upsample, [None, 2, 'nearest']] # 14# Concat 融合 Neck P4 和 Backbone P3. Backbone P3 的输出现在经过了第4层的C2f和第5层的CBAM,所以我们从第5层引出- [[-1, 5], 1, Concat, [1]]      # 15 (原为[-1, 4]) <--- 索引更新!- [-1, 3, C2f, [256, False]]     # 16- [-1, 1, Conv, [256, 3, 2]]     # 17# Concat 融合 Neck P3 和 Neck P4(第13层)- [[-1, 13], 1, Concat, [1]]     # 18 (原为[-1, 12]) <--- 索引更新!- [-1, 3, C2f, [512, False]]     # 19- [-1, 1, Conv, [512, 3, 2]]     # 20# Concat 融合 Neck P4 和 Backbone P5(第10层)- [[-1, 10], 1, Concat, [1]]     # 21 (原为[-1, 9]) <--- 索引更新!- [-1, 3, C2f, [1024, False]]    # 22# Detect 层的输入来自第16, 19, 22层- [[16, 19, 22], 1, Detect, [nc]] # Detect(P3, P4, P5) (原为[15, 18, 21]) <--- 索引更新!
(4) 追踪parse_model如何处理CBAM

让我们看看parse_model解析新增的第5层[-1, 1, CBAM, [256]]时,发生了什么。

  1. 确定输入通道 c1from-1,所以 c1 来自上一层(第4层)的输出。我们知道第4层C2f的输出通道是256,所以 c1 = 256
  2. 确定模块 mm 是我们导入的 CBAM 类。
  3. 确定参数 argsargs 是列表 [256]
  4. 实例化模块 m_:对于CBAM这样的通用模块,parse_model会执行 m(c1, *args) 来实例化。这会调用 CBAM(256, 256)
  5. 确定输出通道 c2:对于通用模块,parse_model会默认将args[0]作为输出通道数,即c2 = 256
  6. 更新通道列表 ch:执行 ch.append(c2),将256添加到通道列表中。

结论:由于CBAM输入和输出通道数相同(c1=c2=256),它完美地融入了网络的数据流,对后续层的通道数计算没有任何影响。唯一的、也是最容易出错的复杂性,在于手动更新所有后续层(尤其是ConcatDetect层)的from索引。

(5) 可视化对比:魔改前后的模型结构

下面是模拟model.info()命令输出的文本,可以清晰地看到变化。
魔改前模型结构 (yolov8n.yaml)

      # ... (层 0-3)4  -1  6       ultralytics.nn.modules.block.C2f  119296    (256, 128, 6, True, False, 1, 0.5)5  -1  1       ultralytics.nn.modules.conv.Conv  147968    (512, 256, 3, 2)6  -1  6       ultralytics.nn.modules.block.C2f  476160    (512, 256, 6, True, False, 1, 0.5)# ...11  [-1, 6]  1  ultralytics.nn.modules.container.Concat  0         (1)# ...14  [-1, 4]  1  ultralytics.nn.modules.container.Concat  0         (1)# ...22  [15, 18, 21]  1  ultralytics.nn.modules.head.Detect  649920    (80, (256, 512, 1024))

魔改后模型结构 (yolov8n-CBAM.yaml)

      # ... (层 0-3)4  -1  6       ultralytics.nn.modules.block.C2f  119296    (256, 128, 6, True, False, 1, 0.5)+  5  -1  1            new_modules.CBAM.CBAM  704       (256, None) # <--- 新增层,参数量极小6  -1  1       ultralytics.nn.modules.conv.Conv  147968    (512, 256, 3, 2)7  -1  6       ultralytics.nn.modules.block.C2f  476160    (512, 256, 6, True, False, 1, 0.5)# ...- 11  [-1, 6]  1  ultralytics.nn.modules.container.Concat  0         (1)+ 12  [-1, 7]  1  ultralytics.nn.modules.container.Concat  0         (1) # <--- 索引更新# ...- 14  [-1, 4]  1  ultralytics.nn.modules.container.Concat  0         (1)+ 15  [-1, 5]  1  ultralytics.nn.modules.container.Concat  0         (1) # <--- 索引更新# ...- 22  [15, 18, 21]  1  ultralytics.nn.modules.head.Detect  649920    (80, (256, 512, 1024))+ 23  [16, 19, 22]  1  ultralytics.nn.modules.head.Detect  649920    (80, (256, 512, 1024)) # <--- 索引更新

通过这个对比,我们可以百分之百地确认,我们的CBAM模块已成功插入,并且整个模型的后续连接也已正确更新。对于SE模块的集成,过程与此完全相同,不再赘述。


七、核心组件的优化

在掌握了对Backbone、Neck、Head三大件的“大刀阔斧”式改造后,我们再来学习如何对网络中的基础“零件”——如上下采样和卷积模块——进行“精雕细琢”的单元。这要求我们更深入地理解YOLO的parse_model函数是如何处理标准模块的。

1. 上下采样模块:EUCB (Efficient Up-sampling with Channel Balancing)

背景与目的:在特征金字塔网络(FPN)中,上采样负责将高层(小尺寸、强语义)的特征图放大,以便与低层(大尺寸、强细节)的特征图融合。YOLOv8默认使用的nn.Upsample(配合mode='nearest')虽然速度极快,但它是一种固定的、非学习性的插值方法,仅仅是简单地复制像素,可能会在放大过程中产生伪影或丢失细节。

EUCB (Efficient Up-sampling with Channel Balancing) 旨在解决这个问题。它是一种可学习的上采样模块。这意味着网络可以通过反向传播,学习到如何以最优的方式从低分辨率特征中“生成”高分辨率特征,同时还能调整通道数量,从而可能带来更平滑、更有效的特征融合效果。

① 集成步骤
  1. 代码: 将 yolo_master/.../Upsample-EUCB.py 复制到 ultralytics/ultralytics/nn/new_modules/ 并重命名为 EUCB.py
  2. 引用: 在tasks.py的顶部添加 from .new_modules.EUCB import *
② 核心解析:EUCB的参数与parse_model的自动适配

思考:nn.UpsampleEUCB 在参数上有何不同?parse_model如何处理这种不同?

  • 第一步:分析模块的__init__签名

    • torch.nn.Upsample的定义很简单,它只关心缩放因子和模式,不改变通道数。其YAML中的args[None, 2, 'nearest']None代表输出尺寸(由scale_factor=2决定),通道数保持不变。

    • 我们打开new_modules/EUCB.py文件(或根据其用法推断),可以发现EUCB模块的定义更像一个卷积层。一个合理的__init__签名应该是:def __init__(self, c1, c2, scale_factor=2):

      • c1: 输入通道数。
      • c2: 输出通道数。这是与nn.Upsample最大的不同。
      • scale_factor: 缩放因子。
  • 第二步:修改YAML文件并追踪解析过程

    现在,我们在yolov8.yamlhead部分进行替换。

    # yolov8-EUCB.yaml (head 部分示例)head:# ...# 假设解析到第14层,其输入来自第13层(C2f),输出通道数为512# ch = [..., 512]# 第15层: 上采样层# --- 原代码 ---# - [-1, 1, nn.Upsample, [None, 2, 'nearest']] # 15# --- 修改后 ---# 我们希望上采样后,通道数从512变为256- [-1, 1, EUCB, [256, 2]] # 15. args为[输出通道数, 缩放因子]# 第16层: Concat层# 它的输入来自上一层(第15层)和主干的第4层 (假设通道数为256)- [[-1, 4], 1, Concat, [1]] # 16# 第17层: C2f层# 它的输入来自第16层的Concat。Concat后的通道数 = 256(来自EUCB) + 256(来自Backbone) = 512- [-1, 3, C2f, [256, False]] # 17. 该C2f层输出通道数为256# ...
    

    parse_model解析到我们修改的第15层 [-1, 1, EUCB, [256, 2]] 时,其标准模块处理逻辑会执行以下操作:

    # 在 ultralytics/nn/tasks.py 的 parse_model 中# 此时: m = EUCB 类, args = [256, 2]
    # 假设上一层的输出通道数 ch[-1] 是 512# 1. 获取输入通道数 c1
    # c1 = ch[f] --> c1 = ch[-1] --> c1 = 512
    c1 = ch[f]# 2. 获取输出通道数 c2
    # c2 = args[0] if isinstance(args[0], int) else ...
    # 这里的 args[0] 是 256, 是整数。所以 c2 被赋值为 256
    c2 = args[0] # 3. 实例化模块
    # m_ = m(*args) 等价于 m_ = EUCB(512, 256, 2)
    # 注意!这里的`*args`展开会把所有参数传进去,所以我们的模块定义要与之匹配
    # 一个更严谨的写法是在YAML中只写输出通道数,让模块内部处理
    # 假设YAML为 [-1, 1, EUCB, [256]]
    # 那么实例化将是 m_ = EUCB(c1, *args) --> EUCB(512, 256)
    m_ = nn.Sequential(...) if n > 1 else m(c1, *args) # 注意这里隐式的 c1 参数# 4. 更新通道列表 ch
    # ch.append(c2) --> ch.append(256)
    # 现在 ch 列表的最后一个元素是 256,供下一层使用
    ch.append(c2)
    

    结论:我们不需要为EUCB编写任何特殊解析代码。只要一个模块遵循“接收输入通道c1,并通过args接收其他参数(包括输出通道c2)”这一标准模式,parse_model的通用逻辑就能自动完成输入/输出通道的推断和模块的正确实例化。

③ 魔改前后模型结构对比

魔改前 (使用 nn.Upsample)

    # ...# 假设第14层输出512通道14 ... [..., 512, True]15  -1  1  torch.nn.modules.upsampling.Upsample     [None, 2, 'nearest']# 第15层输出通道仍为512# ...

魔改后 (使用 EUCB)

    # ...# 第14层输出512通道14 ... [..., 512, True]15  -1  1  new_modules.EUCB.EUCB                    [512, 256, 2]# 第15层输出通道变为256,参数量增加,因为它有可学习的权重# ...

2. 卷积模块:C2f_CMUNeXtBlock

这是对YOLOv8中最重要的特征提取单元C2f的直接替换。CMUNeXtBlock可能借鉴了ConvNeXt的设计,例如使用更大的卷积核、深度可分离卷积等,旨在用相似的参数量换取更强的特征表达能力。

① 集成步骤(同上)
  1. 代码: C2f_CMUNeXtBlock.py -> new_modules/C2f_CMUNeXtBlock.py
  2. 引用: 在 tasks.py 中添加 from .new_modules.C2f_CMUNeXtBlock import *
② 实现“无痛”替换

思考:为什么C2f_CMUNeXtBlock可以如此轻易地替换C2f

答案在于接口兼容性C2f_CMUNeXtBlock在设计时,刻意模仿了C2f__init__参数签名,使得它可以直接使用C2f在YAML文件中的参数定义。

  • 第一步:对比__init__签名

    • C2f的签名(简化后):__init__(self, c1, c2, n=1, shortcut=False, ...)
    • C2f_CMUNeXtBlock的签名(推断):__init__(self, c1, c2, n=1, shortcut=False, ...)

    只要两者都接收相同的核心参数(输入通道c1,输出通道c2,重复次数n,快捷连接shortcut),它们在YAML层面就是可互换的。

  • 第二步:修改YAML
    这个修改是最简单的,只需更换模块名即可。

    # yolov8-CMUNeXt.yaml (backbone 部分示例)backbone:- [-1, 1, Conv, [64, 3, 2]]  # 0- [-1, 1, Conv, [128, 3, 2]]  # 1# --- 原代码 ---# - [-1, 3, C2f, [128, True]]  # 2# --- 修改后 ---- [-1, 3, C2f_CMUNeXtBlock, [128, True]] # 2. 参数完全相同!
    
  • 第三步:追踪parse_model的执行流程
    这个流程与解析C2f时完全一样。

    1. parse_model获取模块名C2f_CMUNeXtBlock
    2. 它从ch列表获取输入通道c1
    3. 它从args[128, True])中获取输出通道c2=128以及其他参数。
    4. 它根据n=3,循环3次来实例化模块。
    5. 它将输出通道c2=128添加到ch列表中。
      整个过程行云流水,因为C2f_CMUNeXtBlock完美地扮演了C2f的角色。

结论:对于想要替换现有标准模块的自定义模块,最佳实践就是让新模块的参数接口与旧模块保持高度一致。这样就能实现“无痛”替换,将修改成本降至最低,仅需更改YAML中的一个字符串。


OK,今天我们就学习到这里🏆🎉👌!


文章参考

  • YOLO系列官方论文
    • YOLOv8 by Ultralytics
    • YOLOv1: You Only Look Once: Unified, Real-Time Object Detection
  • 核心项目与代码
    • YOLO Master GitHub by DataWhale
    • Ultralytics YOLOv8 Documentation

拓展阅读

  • Ultralytics GitHub 仓库 (YOLOv3/v5/v8 的主流实现)
  • 作者的算法专栏 (包含更多YOLO技术文章)

💖 感谢您的耐心阅读!

如果您觉得本文对您理解和实践YOLO模型改造有所帮助,请考虑点赞、收藏或分享给更多有需要的朋友。您的支持是我持续创作优质内容的动力!欢迎在评论区交流讨论,共同进步。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/88341.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/88341.shtml
英文地址,请注明出处:http://en.pswp.cn/web/88341.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Swift 图论实战:DFS 算法解锁 LeetCode 323 连通分量个数

文章目录摘要描述示例题解答案DFS 遍历每个连通区域Union-Find&#xff08;并查集&#xff09;题解代码分析&#xff08;Swift 实现&#xff1a;DFS&#xff09;题解代码详解构建邻接表DFS 深度优先搜索遍历所有节点示例测试及结果示例 1示例 2示例 3时间复杂度分析空间复杂度分…

【剑指offer】栈 队列

&#x1f4c1; JZ9 用两个栈实现队列一个栈in用作进元素&#xff0c;一个栈out用于出元素。当栈out没有元素时&#xff0c;从in栈获取数据&#xff0c;根据栈的特性&#xff0c;栈out的top元素一定是先进入的元素&#xff0c;因此当栈out使用pop操作时&#xff0c;一定时满足队…

GoView 低代码数据可视化

纯前端 分支&#xff1a; master &#x1f47b; 携带 后端 请求分支: master-fetch &#x1f4da; GoView 文档 地址&#xff1a;https://www.mtruning.club/ 项目纯前端-Demo 地址&#xff1a;https://vue.mtruning.club/ 项目带后端-Demo 地址&#xff1a;https://demo.mtrun…

Spring Boot返回前端Long型丢失精度 后两位 变成00

文章目录一、前言二、问题描述2.1、问题背景2.2、问题示例三、解决方法3.1、将ID转换为字符串3.2、使用JsonSerialize注解3.3、使用JsonFormat注解一、前言 在后端开发中&#xff0c;我们经常会遇到需要将ID作为标识符传递给前端的情况。当ID为long类型时&#xff0c;如果该ID…

计算机网络实验——无线局域网安全实验

实验1. WEP和WPA2-PSK实验一、实验目的验证AP和终端与实现WEP安全机制相关的参数的配置过程。验证AP和终端与实现WPA2-PSK安全机制相关的参数的配置过程。验证终端与AP之间建立关联的过程。验证关闭端口的重新开启过程。验证属于不同BSS的终端之间的数据传输过程。二、实验任务…

【从零开始学Dify】大模型应用开发平台Dify本地化部署

目录Dify一、本地化部署1、安装docker2、安装Dify&#xff08;1&#xff09;拉取代码到本地&#xff08;2&#xff09;docker部署&#xff08;3&#xff09;查看服务状态&#xff08;4&#xff09;web端部署&#xff08;5&#xff09;登录二、可能会出现的问题&#xff08;1&am…

LVGL应用和部署(和物理按键交互)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】屏幕除了显示部分&#xff0c;还要去和其他外设进行交互&#xff0c;这是非常重要的一个处理方法。我们知道&#xff0c;不管是mcu&#xff0c;还是…

限流式保护器如何筑牢无人驾驶汽车充电站的安全防线

摘要&#xff1a; 随着新能源汽车&#xff0c;尤其是无人驾驶车队的快速发展&#xff0c;充电设施的安全可靠性至关重要。交流充电桩&#xff08;俗称“慢充桩”&#xff09;作为重要的充电基础设施&#xff0c;其末端回路的安全保护需满足国家标准GB51348-2019的严格要求&…

专题:2025母婴行业洞察报告|附60+份报告PDF汇总下载

原文链接&#xff1a;https://tecdat.cn/?p42908 全球母婴市场正经历结构性增长&#xff0c;一面是欧美成熟市场的品质消费升级&#xff0c;一面是东南亚、中东等新兴市场的人口红利释放。2020至2026年&#xff0c;全球母婴市场规模将从1859亿美元增至3084亿美元&#xff0c;年…

从零搭建多商户商城系统源码:技术栈、数据库设计与接口规划详解

如今&#xff0c;多商户商城系统已成为传统零售转型与新型电商平台构建的关键利器。无论是打造像某宝、某东这样的综合型平台&#xff0c;还是服务于垂直行业的独立电商&#xff0c;一套高效、可扩展的多商户商城系统源码&#xff0c;往往决定着平台的成败。 今天&#xff0c;小…

在Docker中运行macOS的超方便体验!

在数字化和开发人员快速迭代的今日&#xff0c;拥有一个便捷、高效的开发环境成为每个开发者梦寐以求的事情。特别是在需要操作多个系统、开发跨平台应用时&#xff0c;调试和测试的便利性显得尤为重要。今天为大家介绍的这款开源项目&#xff0c;正是一个解决此类问题的利器—…

Kettle导入Excel文件进数据库时,数值发生错误的一种原因

1、问题描述及原因 在使用kettle读取Excel文件、并导入数据库时&#xff0c;需要读取Excel中的数值、日期(或日期时间、时间)、文本这三种类型的列进来&#xff0c;发现读取其中的数值时&#xff0c;读取的数字就不对。 经调查&#xff0c;原因是&#xff0c;在“导出数据为E…

Windows安装DevEco Studio

1. 概述 DevEco Studio是华为基于IDEA Community开源工具开发的一站式HarmonyOS应用及元服务开发平台&#xff0c;为开发者提供代码开发、编译构建以及调测等功能 2. 运行环境要求 操作系统&#xff1a;Windows10 64位、Windows11 64位 内存&#xff1a;16GB及以上 硬盘&…

PLC框架-1.3.2 报文750控制汇川伺服的转矩上下限

本文介绍1200PLC如何使用750报文设定伺服转矩的上下限。 750号报文 PLC---->伺服 (控制) 伺服--->PLC (状态) PZD1

Redis知识集合---思维导图(持续更新中)

一、Redis中常见的数据类型有哪些&#xff1f;二、Redis为什么这么快&#xff1f;三、为什么Redis设计为单线程&#xff1f;6.0版本为何引入多线程&#xff1f;四、

mac m1安装大模型工具vllm

1 更新系统环境 参考vllm官网文档&#xff0c;vllm对apple m1平台mac os, xcoder, clang有如下要求 OS: macOS Sonoma or later SDK: XCode 15.4 or later with Command Line Tools Compiler: Apple Clang > 15.0.0 在App Store更新macOS和XCoder&#xff0c;依据XCoder版本…

解锁localtime:使用技巧与避坑指南

目录 一、引言 1.1 背景与目的 1.2 localtime 函数简介 二、localtime 函数详解 2.1 函数原型与参数 2.2 返回值与 tm 结构体 2.3 基本使用示例 三、localtime 函数的缺陷剖析 3.1 多次调用同一共享区间导致错误 3.1.1 问题现象展示 3.1.2 原因深入分析 3.1.3 实际影…

郑州机械设计研究所 -PHM产品序列概览

1.设备状态监测系统 动态信号监测很像是三个独立通道&#xff0c;振动&#xff0c;转速&#xff0c;然后高频的某个频带。或者是同一个振动信号做的低频和高频两个带通&#xff0c;时域和频域组图。实时检测&#xff0c;很明显是24个时 -频指标。 动态分析看起来像趋势图。 2.…

《棒垒球知道》奥运会的吉祥物是什么·棒球1号位

Olympic Mascots & Baseball/Softball Games History ⚾&#xff08;奥运吉祥物与棒垒球赛事全科普&#xff09;1984洛杉矶奥运会 / Los Angeles 1984Mascot: Sam the Eagle&#xff08;山姆鹰&#xff09;美国精神象征&#xff0c;红白蓝配色超吸睛&#xff01;Baseball/S…

【提高篇-基础知识与编程环境:1、Linux系统终端中常用的文件与目录操作命令】

Linux终端提供了丰富的命令来操作文件和目录&#xff0c;以下简单介绍一些常用的命令&#xff1a; 一、目录操作命令 pwd - 显示当前工作目录 pwd #输出当前所在目录的绝对路径 cd - 切换目录 cd /path/to/directory # 切换到指定目录 cd … # …