> 技术文档 > YOLO模型魔改指南:从原理到实战,替换Backbone、Neck和Head(战损版)_yolo怎么修改模型

YOLO模型魔改指南:从原理到实战,替换Backbone、Neck和Head(战损版)_yolo怎么修改模型


前言

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/simplepip 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, args m = getattr(torch.nn, m[3:]) if \'nn.\' in m else globals()[m] # get module 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) ```* **修改后代码**: ```python is_backbone = False for 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 module except: 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) # module t = str(m)[8:-2].replace(\'__main__.\', \'\') # module type m_.np = sum(x.numel() for x in m_.parameters()) # number params m_.i, m_.f, m_.type = i, f, t # attach index, \'from\' index, type if verbose: LOGGER.info(f\'{i:>3}{str(f):>20}{n_:>3}{m_.np:10.0f} {t:<45}{str(args):<30}\') # print save.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标志位设为True is_backbone = True # m_ 直接就是我们实例化的主干对象 m (在之前的步骤中已经 m=m() ) m_ = m # 给模块实例动态添加一个 \'backbone\' 属性,值为 True。 # 目的:这是一个“标记”,为了在后续的前向传播 `_predict_once` 函数中识别它。 m_.backbone = True else: # 是标准层,按原逻辑构建 m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args) # module t = 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, type if 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 = [], [], [] # outputs for m in self.model: if m.f != -1: # if not from previous layer x = y[m.f] if isinstance(m.f, int) else [x if j == -1 else y[j] for j in m.f] # from earlier layers if profile: self._profile_one_layer(m, x, dt) x = m(x) # run y.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 = [], [], [] # outputs for m in self.model: if m.f != -1: # if not from previous layer x = y[m.f] if isinstance(m.f, int) else [x if j == -1 else y[j] for j in m.f] # from earlier layers if 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) # run y.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.0head_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] 变成了整数 256 args[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 classesdepth_multiple: 0.33 # model depth multiplewidth_multiple: 0.25 # layer channel multiple# anchorsanchors: - [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 backbonebackbone: # [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 headhead: - [-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 CBAMbackbone: # [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 indiceshead: - [-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 = 512c1 = ch[f]# 2. 获取输出通道数 c2# c2 = args[0] if isinstance(args[0], int) else ...# 这里的 args[0] 是 256, 是整数。所以 c2 被赋值为 256c2 = 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模型改造有所帮助,请考虑点赞、收藏或分享给更多有需要的朋友。您的支持是我持续创作优质内容的动力!欢迎在评论区交流讨论,共同进步。

猜谜语网