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

发布于:2025-07-10 ⋅ 阅读:(29) ⋅ 点赞:(0)


前言

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, 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.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] 变成了整数 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.yaml

nc: 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模型改造有所帮助,请考虑点赞、收藏或分享给更多有需要的朋友。您的支持是我持续创作优质内容的动力!欢迎在评论区交流讨论,共同进步。


网站公告

今日签到

点亮在社区的每一天
去签到