ultralytics/nn/tasks.py源码学习笔记——核心函数parse_model

在为yolo模型添加模块时,总是会涉及ultralytics/nn/tasks.py源码的修改。但一般是直接照搬的人家的工作,一直不清晰到底是咋修改的。这里记录一下对tasks.py源码中parse_model这个重要函数的学习,以便后续新增模块时能实现自主修改。

核心函数parse_model

输入参数

def parse_model(d, ch, verbose=True):  # model_dict, input_channels(3)

d (dict): 一个包含模型结构定义的字典。通常是从 yolov8n.yaml 这样的 YAML 配置文件读取并解析得到的。
ch (int): 输入通道数(Input Channels)。通常是 3(RGB 图像)。
verbose (bool): 是否打印模型的详细信息(如每层的输出形状),用于调试。

返回值

return torch.nn.Sequential(*layers), sorted(save)

torch.nn.Sequential(*layers): 一个 torch.nn.Sequential 对象,它包含了整个神经网络的所有层,按顺序堆叠。
sorted(save): 输出层排序列表,包含所有需要保存其输出的层的索引(从 0 开始)。这些层的输出通常会被后续的 Neck(如 PAN-FPN)或 Head 使用。

核心功能

  1. 读取配置:
    从字典 d 中提取关键信息,如:
    d[‘backbone’]: 主干网络的层定义。
    d[‘head’]: 检测头的层定义。
    d[‘anchors’]: 锚框设置(如果使用)。
    d[‘nc’]: 类别数量。
  2. 初始化:
    创建一个空的 nn.Sequential 容器 model。
    创建一个列表 save 用于记录哪些层的输出需要被保存(供后续层使用)。
    创建一个列表 c2 用于记录每一层的输出通道数,方便下一层配置。
  3. 逐层构建网络:
    函数会遍历 backbone 和 head 中定义的每一层。
    对于每一层定义(如 [-1, 1, Conv, [64, 3, 2]]),它会:
    [-1, …]: 表示该层的输入来自前一层(-1 表示上一层,-2 表示上上层,6 表示第 6 层的输出)。
    1: 表示该模块(如 Conv)需要重复的次数。
    Conv: 表示模块的类名(在代码中会映射到 nn.Conv2d 或自定义的 Conv 模块)。
    [64, 3, 2]: 传递给 Conv 模块的参数,如 [out_channels, kernel_size, stride]。
  4. 动态计算输入通道:
    对于每一层,函数会根据其输入来源(f)查找之前层的输出通道数 c2[f],作为当前层的输入通道 c1。
  5. 实例化模块并添加到模型:
    根据模块名(如 Conv, Bottleneck, SPPF)和参数,动态创建 PyTorch 模块实例。
    将该实例添加到 model 这个 Sequential 容器中。
  6. 记录需要保存输出的层:
    如果某一层的索引出现在后续层的输入定义中(如 f in [4, 6]),则将其索引添加到 save 列表中。
    最终 save 会被排序并去重。
  7. 打印模型信息(如果 verbose=True):
    打印每层的序号、输出形状、参数数量等,方便用户检查模型结构。
parse_model 函数就像是一个 “模型装配工厂”:

输入: 一份“设计图纸”(model.yaml 解析成的字典 d)。
过程: 按照图纸上的指令,一层一层地创建 PyTorch 模块,并将它们连接起来。
输出: 一个完整的、可训练的 PyTorch 神经网络模型(model)和一个记录关键节点的列表(save)。
它是 YOLO 实现高度模块化和可配置化的关键,允许用户通过修改简单的 YAML 文件来定义复杂的网络结构。

示例

假设 d[‘backbone’] 中有一行:

[[-1, 1, Conv, [64, 3, 2]]]

parse_model 会:

  1. 读取这一行。
  2. 发现输入来自 -1(上一层)。
  3. 获取上一层的输出通道数作为 c1。
  4. 创建一个 Conv 模块,参数为 [64, 3, 2](即输出 64 通道,3x3 卷积核,stride=2)。
  5. 将这个 Conv 层添加到 model 中。
  6. 记录当前层的输出通道数 c2 = 64。
  7. 如果后续层要用到这一层的输出,就将其索引加入 save。

函数详解

    import ast
    # Args
    # 定义一个布尔变量 legacy,用于控制是否启用兼容旧版本模式,不同版本的 YOLO 可能在某些模块(如 BottleneckCSP)的实现上有细微差别。设置 legacy=True 可以确保旧模型能正确加载。
    legacy = True  # backward compatibility for v3/v5/v8/v9 models
    # 定义一个变量 max_channels,表示通道数的最大限制。
    max_channels = float("inf")
    # 从配置字典 d 中提取三个关键参数:类别数、激活函数 和 尺度
    nc, act, scales = (d.get(x) for x in ("nc", "activation", "scales"))
    # 从配置字典 d 中提取三个缩放因子:深度倍率、宽度倍率 和 关键点形状。与上面不同,这里提供了默认值 1.0
    depth, width, kpt_shape = (d.get(x, 1.0) for x in ("depth_multiple", "width_multiple", "kpt_shape"))

在这里插入图片描述

depth_multiple (深度倍率):

控制模型中重复模块的数量。
例如,Bottleneck 模块在 yolov8n.yaml 中可能重复 1 次,在 yolov8x.yaml 中可能重复 3 次。depth = 0.33 或 depth = 1.0 就是这个倍率。

width_multiple (宽度倍率):

控制模型中每一层的通道数。
例如,yolov8n 的 width=0.25,表示所有卷积层的通道数都乘以 0.25,使模型更小更快。

kpt_shape (关键点形状):

用于姿态估计(Pose Estimation) 模型。
表示每个对象的关键点数量和维度(如 [17, 3] 表示 17 个关键点,每个点有 x, y, visible 三个值)。
默认 1.0 表示不是姿态估计模型。

# scales 通常是一个字典,定义了不同模型尺寸(如 n, s, m, l, x)对应的 depth, width, max_channels.
# 这里首先检查之前从配置字典 d 中提取的 scales 变量是否存在且不为空。
    if scales:
        scale = d.get("scale")
		# 如果用户没有指定scale(即 scale 为 None, "", 或不存在),则自动选择一个默认的 scale。
        if not scale:
        	# 从 scales 字典的所有键(如 ['n', 's', 'm', 'l', 'x'])中,取第一个作为默认的 scale。
			# 为什么是第一个? 通常排序是 n < s < m < l < x,取最小的 n 作为默认值比较安全。
			# 示例: 如果 scales.keys() = ['n', 's', 'm'],则 scale = 'n'。
            scale = tuple(scales.keys())[0]
            LOGGER.warning(f"WARNING ⚠️ no model scale passed. Assuming scale='{scale}'.")
        depth, width, max_channels = scales[scale]
	
# 作用: 检查之前提取的 act 变量是否存在(即配置文件中指定了激活函数)
# act 的值: 通常是字符串,如 "silu", "relu", "leaky_relu"。
    if act:
    	# 重新定义Conv 模块的默认激活函数
        Conv.default_act = eval(act)  # redefine default activation, i.e. Conv.default_act = torch.nn.SiLU()
        # 如果 verbose=True,则打印当前使用的激活函数。
        if verbose:
            LOGGER.info(f"{colorstr('activation:')} {act}")  # print
# 如果 verbose=True,打印一个表头,用于后续显示每一层的详细信息
    if verbose:
        LOGGER.info(f"\n{'':>3}{'from':>20}{'n':>3}{'params':>10}  {'module':<45}{'arguments':<30}")

这段代码完成了以下关键任务:

模型缩放 (Scales):

根据用户指定或默认的 scale(如 ‘n’),从 scales 字典中加载对应的 depth, width, max_channels。
这是 YOLO 实现 n/s/m/l/x 多尺寸模型的核心机制。

激活函数 (Activation):

动态设置 Conv 模块的默认激活函数(如 SiLU)。
使模型配置更加灵活。

日志输出:

在 verbose 模式下,打印激活函数信息和一个清晰的表头,为后续逐层打印模型结构做准备。
这三步确保了模型能够根据配置文件灵活地调整大小和行为,并提供清晰的构建过程日志,是 parse_model 函数中承上启下的关键部分。

	# ch 变成了一个列表,用于记录每层的输出通道数。
    ch = [ch]
    # layers 用于收集所有模块实例
	# save 用于记录需要保存输出的层索引
	# c2 初始化为输入通道数
    layers, save, c2 = [], [], ch[-1]  # layers, savelist, ch out
    # 所有基础模块(Conv, Bottleneck, C2f, Detect, Segment 等)用于识别“标准模块”,可能采用通用创建流程
    # 与普通 set 不同,frozenset 一旦创建就不能修改(不能添加或删除元素)。
    base_modules = frozenset(
        {
            Classify,	# YOLO 中用于图像分类任务的模块
            ...     
        }
    )
    # 支持重复参数的模块(BottleneckCSP, C2f, C3 等)用于识别“可重复模块”,需要根据 n 参数创建多个实例
    repeat_modules = frozenset(  # modules with 'repeat' arguments
        {
            BottleneckCSP,	# YOLOv5 中的一个关键模块
     		...
        }
    )

在这里插入图片描述
下面我们来深入解析 parse_model 函数的核心部分——这个庞大的 for 循环。它是整个函数的“引擎”,负责逐层解析配置、创建模块、计算通道数,最终构建出完整的 PyTorch 模型。

# d["backbone"] + d["head"]: 将主干网络和检测头的配置拼接成一个长列表
# enumerate(...): 遍历这个列表,同时获取索引 i 和每一层的定义 (f, n, m, args)
# f (from): 输入来源层的索引。-1 表示上一层,6 表示第 6 层
# n (number): 该模块重复的次数
# m (module): 模块的类名(字符串或类对象)
# args (arguments): 传递给模块构造函数的参数列表
    for i, (f, n, m, args) in enumerate(d["backbone"] + d["head"]):  # from, number, module, args
    	# 这里将 m(可能是字符串)转换为真正的 Python 类对象。 m 现在是一个可以调用的类(如 torch.nn.Conv2d)
    	# 三种情况:1."nn.Conv2d": 提取 "Conv2d",从 torch.nn 中获取类。	2."torchvision.ops.DeformConv2d": 提取 "DeformConv2d",从 torchvision.ops 中获取类。	3."Conv": 直接从当前模块的全局命名空间 globals() 中查找 Conv 类。
        m = (
            getattr(torch.nn, m[3:])
            if "nn." in m
            else getattr(__import__("torchvision").ops, m[16:])
            if "torchvision.ops." in m
            else globals()[m]
        )  # get module
        # 将 args 列表中的字符串参数转换为实际的 Python 对象
        for j, a in enumerate(args):
            if isinstance(a, str):
            	# contextlib.suppress(ValueError): 安静地忽略 ast.literal_eval 可能抛出的 ValueError(如果字符串无法解析)
                with contextlib.suppress(ValueError):
               	 	# 示例: 
				 	# args = [64, 'k'],如果 k = 3,则 'k' 会被替换为 3。
 					# args = [64, '[3, 3]'],'[3, 3]' 会被 ast.literal_eval 解析为列表 [3, 3]。
                    args[j] = locals()[a] if a in locals() else ast.literal_eval(a)
        # 根据全局的 depth_multiple 因子调整模块的重复次数。	n_: 用于打印,记录原始的 n(缩放前)。	if n > 1: 只有当 n > 1 时才应用缩放,避免对单次模块(如 n=1 的 Conv)进行不必要的计算。
        # 示例: 
		# 配置中 n=3,depth=0.33 → n = round(3 * 0.33) = 1
		# 配置中 n=6,depth=1.0 → n = 6
        n = n_ = max(round(n * depth), 1) if n > 1 else n  # depth gain
        ########################## 核心逻辑:根据不同模块类型处理 args 和 c2 ##########################################
        # 这是循环中最复杂、最关键的 if-elif 链,根据不同模块类型动态计算输入/输出通道数 c1/c2 并调整参数 args
        # 情况 1: m in base_modules (如 Conv, C2f, BottleneckCSP)
        if m in base_modules:
        	# c1 = ch[f]: 输入通道数来自输入来源层 f 的输出通道。	
        	# c2 = args[0]: 原始配置中的输出通道数
            c1, c2 = ch[f], args[0]
            if c2 != nc:  # if c2 not equal to number of classes (i.e. for Classify() output)		# # 如果不是分类头输出
            	# make_divisible(..., 8): 确保通道数是 8 的倍数(有利于 GPU 计算效率)
				# width: 宽度倍率,用于缩放通道数(如 width=0.25 → 通道数 × 0.25)。
				# args.insert(2, n): 对于 repeat_modules,将 n 作为模块的第三个参数传入(如 C2f(c1, c2, n=3, ...))。
                c2 = make_divisible(min(c2, max_channels) * width, 8)		
            # 其他特殊模块处理
            if m is C2fAttn:  # set 1) embed channels and 2) num heads
                args[1] = make_divisible(min(args[1], max_channels // 2) * width, 8)
                args[2] = int(max(round(min(args[2], max_channels // 2 // 32)) * width, 1) if args[2] > 1 else args[2])
            args = [c1, c2, *args[1:]]		# 插入 c1, c2 作为前两个参数
            if m in repeat_modules:		# 如 BottleneckCSP, C2f
                args.insert(2, n)  # number of repeats		# 将重复次数 n 插入到 args[2]
                n = 1			# 重置 n,因为模块内部已处理重复
            if m is C3k2:  # for M/L/X sizes
                legacy = False
                if scale in "mlx":
                    args[3] = True
            if m is A2C2f:
                legacy = False
                if scale in "lx":  # for L/X sizes
                    args.extend((True, 1.2))
        # 情况 2: m is AIFI (RT-DETR 的注意力模块)
        elif m is AIFI:
            args = [ch[f], *args]		## 输入通道作为第一个参数
        # 情况 3: m in {HGStem, HGBlock} (Hourglass 模块)
        elif m in frozenset({HGStem, HGBlock}):
            c1, cm, c2 = ch[f], args[0], args[1]		# 多个通道参数
            args = [c1, cm, c2, *args[2:]]
            if m is HGBlock:
                args.insert(4, n)  # number of repeats		# n 作为第 5 个参数
                n = 1
        # 情况 4: m is ResNetLayer (ResNet 风格模块)
        elif m is ResNetLayer:
            c2 = args[1] if args[3] else args[1] * 4		# bottleneck 结构输出通道翻 4 倍
        # 情况 5: m is torch.nn.BatchNorm2d
        elif m is torch.nn.BatchNorm2d:
            args = [ch[f]]		# BN 层只需要输入通道数
        # 情况 6: m is Concat (拼接层)
        elif m is Concat:
            c2 = sum(ch[x] for x in f)		# 输出通道 = 所有输入层通道之和
        # 情况 7: m in {Detect, Segment, Pose, ...} (检测/分割头)
        elif m in frozenset({Detect, WorldDetect, Segment, Pose, OBB, ImagePoolingAttn, v10Detect}):
            args.append([ch[x] for x in f])		# 将所有输入层的通道数列表作为最后一个参数
            if m is Segment:
                args[2] = make_divisible(min(args[2], max_channels) * width, 8)		 # 缩放内部通道
            if m in {Detect, Segment, Pose, OBB}:		
                m.legacy = legacy		 # 设置 legacy 属性
        # 情况 8: m is RTDETRDecoder
        elif m is RTDETRDecoder:  # special case, channels arg must be passed in index 1
            args.insert(1, [ch[x] for x in f])		# 通道列表作为第 2 个参数
        elif m is CBLinear:
            c2 = args[0]
            c1 = ch[f]
            args = [c1, c2, *args[1:]]
        elif m is CBFuse:
            c2 = ch[f[-1]]
        elif m in frozenset({TorchVision, Index}):
            c2 = args[0]
            c1 = ch[f]
            args = [*args[1:]]
        # 默认情况,c2 = ch[f](输出通道等于输入通道,如 nn.Identity)
        else:
            c2 = ch[f]
		# 目的: 实例化模块。n > 1: 创建一个包含 n 个 m(*args) 实例的 Sequential。 n == 1: 直接创建单个模块 m(*args)。 m_: 最终的模块实例。
		# 为模块实例 m_ 添加自定义属性,便于后续调试和分析。
        m_ = torch.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
        # 将输入来源层 f 的索引加入 save 列表(排除 -1),表示这些层的输出需要被保存。
        save.extend(x % i for x in ([f] if isinstance(f, int) else f) if x != -1)  # append to savelist
        layers.append(m_)		# 将创建好的模块 m_ 加入 layers 列表
        if i == 0:
            ch = []
        ch.append(c2)		# 将当前层的输出通道数 c2 加入 ch 列表,供下一层使用。
    return torch.nn.Sequential(*layers), sorted(save)		# 最终返回	
	#torch.nn.Sequential(*layers): 将所有模块按顺序组合成一个完整的模型。
	#sorted(save): 返回需要保存输出的层索引列表,用于后续的特征融合(如 PAN-FPN)

这个 for 循环是 parse_model 的心脏,它实现了:

  • 通用性: 通过 if-elif 链处理数十种不同类型的模块。
  • 可配置性: 支持 depth_multiple 和 width_multiple 进行模型缩放。
  • 自动化: 自动推断输入通道数,无需在配置中显式写出。
  • 灵活性: 支持复杂的连接方式(如多输入 Concat、跨层连接)。
  • 可读性: 在 verbose 模式下打印详细的模型结构表。

正是这个循环,使得 YOLO 能够通过简单的 YAML 配置文件,灵活地构建出从轻量级 yolov8n 到大型 yolov8x 的各种模型。

怎么实现yolov8中将cbam和Repulsion Loss损失函数两个模块融合起来,以下分别是我的CBAM模块、CBAM的yaml、Repulsion Loss模块、Repulsion Loss的yaml以及我的训练脚本。CBAM模块:import numpy as np import torch from torch import nn from torch.nn import init class ChannelAttentionModule(nn.Module): def __init__(self, c1, reduction=16): super(ChannelAttentionModule, self).__init__() mid_channel = c1 // reduction self.avg_pool = nn.AdaptiveAvgPool2d(1) self.max_pool = nn.AdaptiveMaxPool2d(1) self.shared_MLP = nn.Sequential( nn.Linear(in_features=c1, out_features=mid_channel), nn.LeakyReLU(0.1, inplace=True), nn.Linear(in_features=mid_channel, out_features=c1) ) self.act = nn.Sigmoid() #self.act=nn.SiLU() def forward(self, x): avgout = self.shared_MLP(self.avg_pool(x).view(x.size(0),-1)).unsqueeze(2).unsqueeze(3) maxout = self.shared_MLP(self.max_pool(x).view(x.size(0),-1)).unsqueeze(2).unsqueeze(3) return self.act(avgout + maxout) class SpatialAttentionModule(nn.Module): def __init__(self): super(SpatialAttentionModule, self).__init__() self.conv2d = nn.Conv2d(in_channels=2, out_channels=1, kernel_size=7, stride=1, padding=3) self.act = nn.Sigmoid() def forward(self, x): avgout = torch.mean(x, dim=1, keepdim=True) maxout, _ = torch.max(x, dim=1, keepdim=True) out = torch.cat([avgout, maxout], dim=1) out = self.act(self.conv2d(out)) return out class CBAM(nn.Module): def __init__(self, c1,c2): super(CBAM, self).__init__() self.channel_attention = ChannelAttentionModule(c1) self.spatial_attention = SpatialAttentionModule() def forward(self, x): out = self.channel_attention(x) * x out = self.spatial_attention(out) * out return out CBAM的yaml:# Ultralytics YOLO 🚀, AGPL-3.0 license # YOLOv8 object detection model with P3-P5 outputs. For Usage examples see https://docs.ultralytics.com/tasks/detect # Parameters nc: 2 # number of classes loss: 'RepulsionLoss' # 关键修改:指定使用Repulsion Loss 2025/7/19改 scales: # model compound scaling constants, i.e. 'model=yolov8n.yaml' will call yolov8.yaml with scale 'n' # [depth, width, max_channels] n: [0.33, 0.25, 1024] # YOLOv8n summary: 225 layers, 3157200 parameters, 3157184 gradients, 8.9 GFLOPs s: [0.33, 0.50, 1024] # YOLOv8s summary: 225 layers, 11166560 parameters, 11166544 gradients, 28.8 GFLOPs m: [0.67, 0.75, 768] # YOLOv8m summary: 295 layers, 25902640 parameters, 25902624 gradients, 79.3 GFLOPs l: [1.00, 1.00, 512] # YOLOv8l summary: 365 layers, 43691520 parameters, 43691504 gradients, 165.7 GFLOPs x: [1.00, 1.25, 512] # YOLOv8x summary: 365 layers, 68229648 parameters, 68229632 gradients, 258.5 GFLOPs # YOLOv8.0n backbone backbone: # [from, repeats, 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]] - [-1, 1, Conv, [256, 3, 2]] # 3-P3/8 - [-1, 6, C2f, [256, True]] - [-1, 1, Conv, [512, 3, 2]] # 5-P4/16 - [-1, 6, C2f, [512, True]] - [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32 - [-1, 3, C2f, [1024, True]] - [-1, 3, CBAM, [1024]] - [-1, 1, SPPF, [1024, 5]] # 9 # YOLOv8.0n head head: - [-1, 1, nn.Upsample, [None, 2, 'nearest']] - [[-1, 6], 1, Concat, [1]] # cat backbone P4 - [-1, 3, C2f, [512]] # 12 - [-1, 1, nn.Upsample, [None, 2, 'nearest']] - [[-1, 4], 1, Concat, [1]] # cat backbone P3 - [-1, 3, C2f, [256]] # 15 (P3/8-small) - [-1, 1, Conv, [256, 3, 2]] - [[-1, 13], 1, Concat, [1]] # cat head P4 - [-1, 3, C2f, [512]] # 18 (P4/16-medium) - [-1, 1, Conv, [512, 3, 2]] - [[-1, 10], 1, Concat, [1]] # cat head P5 - [-1, 3, C2f, [1024]] # 21 (P5/32-large) - [[16, 19, 22], 1, Detect, [nc]] # Detect(P3, P4, P5) Repulsionloss模块:import torch import numpy as np def pairwise_bbox_iou(box1, box2, box_format='xywh'): if box_format == 'xyxy': lt = torch.max(box1[:, None, :2], box2[:, :2]) rb = torch.min(box1[:, None, 2:], box2[:, 2:]) area_1 = torch.prod(box1[:, 2:] - box1[:, :2], 1) area_2 = torch.prod(box2[:, 2:] - box2[:, :2], 1) elif box_format == 'xywh': lt = torch.max( (box1[:, None, :2] - box1[:, None, 2:] / 2), (box2[:, :2] - box2[:, 2:] / 2), ) rb = torch.min( (box1[:, None, :2] + box1[:, None, 2:] / 2), (box2[:, :2] + box2[:, 2:] / 2), ) area_1 = torch.prod(box1[:, 2:], 1) area_2 = torch.prod(box2[:, 2:], 1) valid = (lt < rb).type(lt.type()).prod(dim=2) inter = torch.prod(rb - lt, 2) * valid return inter / (area_1[:, None] + area_2 - inter) def IoG(gt_box, pred_box): inter_xmin = torch.max(gt_box[:, 0], pred_box[:, 0]) inter_ymin = torch.max(gt_box[:, 1], pred_box[:, 1]) inter_xmax = torch.min(gt_box[:, 2], pred_box[:, 2]) inter_ymax = torch.min(gt_box[:, 3], pred_box[:, 3]) Iw = torch.clamp(inter_xmax - inter_xmin, min=0) Ih = torch.clamp(inter_ymax - inter_ymin, min=0) I = Iw * Ih G = ((gt_box[:, 2] - gt_box[:, 0]) * (gt_box[:, 3] - gt_box[:, 1])).clamp(1e-6) return I / G def smooth_ln(x, sigma=0.5): return torch.where( torch.le(x, sigma), -torch.log(1 - x), ((x - sigma) / (1 - sigma)) - np.log(1 - sigma) ) def repulsion_loss(pbox, gtbox, fg_mask, sigma_repgt=0.9, sigma_repbox=0, pnms=0, gtnms=0): # nms=0 loss_repgt = torch.zeros(1).to(pbox.device) loss_repbox = torch.zeros(1).to(pbox.device) bbox_mask = fg_mask.unsqueeze(-1).repeat([1, 1, 4]) bs = 0 pbox = pbox.detach() gtbox = gtbox.detach() for idx in range(pbox.shape[0]): num_pos = bbox_mask[idx].sum() if num_pos <= 0: continue _pbox_pos = torch.masked_select(pbox[idx], bbox_mask[idx]).reshape([-1, 4]) _gtbox_pos = torch.masked_select(gtbox[idx], bbox_mask[idx]).reshape([-1, 4]) bs += 1 pgiou = pairwise_bbox_iou(_pbox_pos, _gtbox_pos, box_format='xyxy') ppiou = pairwise_bbox_iou(_pbox_pos, _pbox_pos, box_format='xyxy') pgiou = pgiou.cuda().data.cpu().numpy() ppiou = ppiou.cuda().data.cpu().numpy() _gtbox_pos_cpu = _gtbox_pos.cuda().data.cpu().numpy() for j in range(pgiou.shape[0]): for z in range(j, pgiou.shape[0]): ppiou[j, z] = 0 if (_gtbox_pos_cpu[j][0] == _gtbox_pos_cpu[z][0]) and (_gtbox_pos_cpu[j][1] == _gtbox_pos_cpu[z][1]) \ and (_gtbox_pos_cpu[j][2] == _gtbox_pos_cpu[z][2]) and ( _gtbox_pos_cpu[j][3] == _gtbox_pos_cpu[z][3]): pgiou[j, z] = 0 pgiou[z, j] = 0 ppiou[z, j] = 0 pgiou = torch.from_numpy(pgiou).to(pbox.device).cuda().detach() ppiou = torch.from_numpy(ppiou).to(pbox.device).cuda().detach() max_iou, _ = torch.max(pgiou, 1) pg_mask = torch.gt(max_iou, gtnms) num_repgt = pg_mask.sum() if num_repgt > 0: pgiou_pos = pgiou[pg_mask, :] _, argmax_iou_sec = torch.max(pgiou_pos, 1) pbox_sec = _pbox_pos[pg_mask, :] gtbox_sec = _gtbox_pos[argmax_iou_sec, :] IOG = IoG(gtbox_sec, pbox_sec) loss_repgt += smooth_ln(IOG, sigma_repgt).mean() pp_mask = torch.gt(ppiou, pnms) num_pbox = pp_mask.sum() if num_pbox > 0: loss_repbox += smooth_ln(ppiou, sigma_repbox).mean() loss_repgt /= bs loss_repbox /= bs torch.cuda.empty_cache() return loss_repgt.squeeze(0), loss_repbox.squeeze(0) Repulsionloss的yaml:# Ultralytics YOLO 🚀, AGPL-3.0 license # YOLOv8 object detection model. More improvement points for YOLOv8, please see https://github.com/iscyy/ultralyticsPro # Parameters nc: 80 # number of classes scales: # model compound scaling constants, i.e. 'model=yolov8n.yaml' will call yolov8.yaml with scale 'n' # [depth, width, max_channels] n: [0.33, 0.25, 1024] # YOLOv8n summary: 225 layers, 3157200 parameters, 3157184 gradients, 8.9 GFLOPs s: [0.33, 0.50, 1024] # YOLOv8s summary: 225 layers, 11166560 parameters, 11166544 gradients, 28.8 GFLOPs m: [0.67, 0.75, 768] # YOLOv8m summary: 295 layers, 25902640 parameters, 25902624 gradients, 79.3 GFLOPs l: [1.00, 1.00, 512] # YOLOv8l summary: 365 layers, 43691520 parameters, 43691504 gradients, 165.7 GFLOPs x: [1.00, 1.25, 512] # YOLOv8x summary: 365 layers, 68229648 parameters, 68229632 gradients, 258.5 GFLOPs loss: 'RepulsionLoss' # 举例,如果使用 RepulsionLoss 损失函数的话, 即修改对应的名称 # YOLOv8.0n backbone backbone: # [from, repeats, 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]] - [-1, 1, Conv, [256, 3, 2]] # 3-P3/8 - [-1, 6, C2f, [256, True]] - [-1, 1, Conv, [512, 3, 2]] # 5-P4/16 - [-1, 6, C2f, [512, True]] - [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32 - [-1, 3, C2f, [1024, True]] - [-1, 1, SPPF, [1024, 5]] # 9 # YOLOv8.0n head head: - [-1, 1, nn.Upsample, [None, 2, 'nearest']] - [[-1, 6], 1, Concat, [1]] # cat backbone P4 - [-1, 3, C2f, [512]] # 12 - [-1, 1, nn.Upsample, [None, 2, 'nearest']] - [[-1, 4], 1, Concat, [1]] # cat backbone P3 - [-1, 3, C2f, [256]] # 15 (P3/8-small) - [-1, 1, Conv, [256, 3, 2]] - [[-1, 12], 1, Concat, [1]] # cat head P4 - [-1, 3, C2f, [512]] # 18 (P4/16-medium) - [-1, 1, Conv, [512, 3, 2]] - [[-1, 9], 1, Concat, [1]] # cat head P5 - [-1, 3, C2f, [1024]] # 21 (P5/32-large) - [[15, 18, 21], 1, Detect, [nc]] # Detect(P3, P4, P5) 训练脚本.py:import warnings import envs warnings.filterwarnings('ignore') from ultralytics import YOLO if __name__ == '__main__': model = YOLO(r'F:\Anaconda\anaconda\envs\yolov8_pytorch\yolov8_CBAM.yaml').load(r'F:\Anaconda\anaconda\envs\yolov8_pytorch\测试\yolov8n.pt') model.train( data=r'F:\Anaconda\anaconda\envs\yolov8_pytorch\xunlian2\data.yaml', device="cuda", # 使用GPU(等效于 device=0) epochs=200, # 训练轮次 batch=16, # 根据GPU内存调整(4060笔记本GPU建议8-16) imgsz=640, # 输入图像尺寸 workers=4, # 数据加载线程数 optimizer="auto", # 自动选择优化器 lr0=0.01, # 初始学习率 name='yolov8_cbam_exp6'# 实验名称(可选) ) # 3. 验证(训练完成后自动执行) metrics = model.val() # 在验证集上评估 print(f"mAP@0.5: {metrics.box.map}") # 输出精度指标 print('模型训练完毕')
最新发布
08-05
Traceback (most recent call last): File "/root/ultralytics/yolov8-seg/yolov8-MobileNetv3-CA-train.py", line 3, in <module> model=YOLO("/root/ultralytics/ultralytics/cfg/models/v8/yolov8-MobileNetv3-CA.yaml",verbose=True) File "/root/ultralytics/ultralytics/models/yolo/model.py", line 79, in __init__ super().__init__(model=model, task=task, verbose=verbose) File "/root/ultralytics/ultralytics/engine/model.py", line 149, in __init__ self._new(model, task=task, verbose=verbose) File "/root/ultralytics/ultralytics/engine/model.py", line 261, in _new self.model = (model or self._smart_load("model"))(cfg_dict, verbose=verbose and RANK == -1) # build model File "/root/ultralytics/ultralytics/nn/tasks.py", line 419, in __init__ m.stride = torch.tensor([s / x.shape[-2] for x in _forward(torch.zeros(1, ch, s, s))]) # forward File "/root/ultralytics/ultralytics/nn/tasks.py", line 415, in _forward return self.forward(x)[0] if isinstance(m, (Segment, YOLOESegment, Pose, OBB)) else self.forward(x) File "/root/ultralytics/ultralytics/nn/tasks.py", line 141, in forward return self.predict(x, *args, **kwargs) File "/root/ultralytics/ultralytics/nn/tasks.py", line 159, in predict return self._predict_once(x, profile, visualize, embed) File "/root/ultralytics/ultralytics/nn/tasks.py", line 182, in _predict_once x = m(x) # run File "/root/miniconda3/lib/python3.8/site-packages/torch/nn/modules/module.py", line 1110, in _call_impl return forward_call(*input, **kwargs) File "/root/ultralytics/ultralytics/nn/modules/conv.py", line 848, in forward out = identity * a_w * a_h RuntimeError: The size of tensor a (256) must match the size of tensor b (8) at non-singleton dimension 1
06-19
<think>我们面对的问题是在YOLOv8中使用MobileNetv3-CA进行训练时出现的RuntimeError,错误信息为:'sizeoftensora(256)mustmatchthesizeoftensorb(8)'这个错误通常发生在两个张量进行运算(如加法、乘法或连接)时,它们的形状不匹配。具体来说,错误信息指出在某个维度上,一个张量的大小为256,另一个为8,而在这个维度上它们必须相同(除非是单例维度,但这里不是)。根据引用[1]和引用[4]中的类似问题,我们可以知道这类错误通常是由于网络结构设计或数据流经网络时产生的形状不匹配引起的。解决步骤:1.**理解错误来源**:错误发生在某个操作(可能是矩阵乘法、逐元素运算或连接操作)中,两个参与运算的张量在某一维度上大小不同。这里,tensora的某一维大小为256,tensorb的同一维大小为8。2.**检查网络结构**:由于我们使用了MobileNetv3-CA作为YOLOv8的主干网络,需要确保MobileNetv3-CA的输出与YOLOv8的头部(head)网络输入兼容。YOLOv8的头部通常期望特定维度的特征图。3.**检查修改点**:如果我们对MobileNetv3-CA或YOLOv8的头部进行了修改,那么很可能是这些修改导致了特征图尺寸的变化。特别要注意的是,CA(CoordinateAttention)模块的加入可能会改变特征图的通道数,但通常不会改变空间尺寸(高和宽)。然而,错误中提到的维度可能是通道维(通常为1)或空间维(如果错误发生在空间位置相关的操作中)。4.**分析错误发生的位置**:错误信息没有直接给出是哪一行代码,但我们可以通过堆栈跟踪找到具体位置。如果没有堆栈跟踪,我们需要逐步检查网络的前向传播过程。5.**常见原因**:-在连接(concat)操作中,两个特征图除了连接的维度外,其他维度必须相同。例如,在YOLO的头部,可能会将不同层的特征图进行连接,如果它们的空间尺寸或通道数不匹配,就会出错。-在注意力机制(如CA)中,如果注意力图的生成与原始特征图的空间尺寸不一致,可能导致后续运算出错。-上采样或下采样操作后特征图尺寸计算错误。6.**解决方案**:a.**检查特征图尺寸**:在MobileNetv3-CA的输出层以及YOLOv8头部每一层的输入和输出处打印特征图的尺寸,观察在哪一层出现了尺寸异常。b.**调整通道数**:如果是因为CA模块改变了通道数,我们需要在后续层(如YOLO头部)调整通道数,使其匹配。可以通过添加1x1卷积调整通道数。c.**调整空间尺寸**:如果空间尺寸不匹配,需要检查上采样或下采样的倍数是否正确,或者检查步长设置。d.**检查注意力图**:CA模块会生成注意力图,该注意力图需要与输入特征图进行逐元素乘法。因此,注意力图必须与输入特征图具有相同的形状(包括通道数、高度和宽度)。如果这里不匹配,就会出错。7.**具体操作建议**:-在CA模块的输出后打印特征图尺寸,确保其与预期一致。-检查YOLOv8头部网络的第一层输入特征图尺寸是否与主干网络的输出特征图尺寸匹配。-如果使用了特征金字塔(FPN)或路径聚合网络(PAN),需要检查多尺度特征融合时的尺寸匹配。由于错误信息中出现了256和8,我们可以推测:-256可能是某个特征图的通道数,而8可能是另一个特征图的通道数,或者是空间维度(如高度或宽度)的大小。但通常,在通道维度上不匹配会报错通道数,而空间维度上不匹配也会报错对应的维度大小。8.**参考引用**:引用[2]中提到了`expand`函数的使用,但我们的错误可能不是由`expand`函数直接引起的,不过可以借鉴张量扩展时维度匹配的重要性。引用[4]中提到了在修改网络结构后出现类似错误,解决方法是检查网络结构修改导致的尺寸不匹配。因此,重点检查我们修改的部分。9.**调试步骤**:我们可以通过以下代码片段在关键位置打印形状(以PyTorch为例):```pythonprint(f"特征图形状:{x.shape}")```将这样的语句插入到主干网络末尾、头部网络开始以及头部网络中的每一层之前,直到找到形状从正确变为错误的位置。10.**可能的修改**:如果发现是CA模块的输出通道数与后续层期望的输入通道数不一致,我们可以在CA模块后添加一个卷积层来调整通道数。例如,如果我们期望后续层输入通道为C,而CA模块输出通道为C'(不等于C),则可以添加:```pythonnn.Conv2d(C',C,kernel_size=1)```11.**注意**:在YOLOv8中,通常会有多个输出层(如三个不同尺度的输出),因此要确保每个分支的输入特征图尺寸都正确。由于我们没有具体的网络结构代码,以上是通用的解决思路。下面我们给出一个可能的解决方案步骤:解决方案:步骤1:在训练脚本中启用更详细的错误跟踪,找到错误发生的具体位置。步骤2:检查该位置两个张量的形状,确定是哪个层的输出出了问题。步骤3:回溯网络结构,确保每一层的输入输出尺寸匹配。如果问题依然存在,我们可以考虑以下代码层面的调整:假设错误发生在CA模块与后续层的连接处,我们可以尝试修改CA模块的输出通道数,或者修改后续层的输入通道数。例如,在MobileNetv3中嵌入CA模块时,我们可能会这样插入:```pythonclassMobileNetV3_CA(nn.Module):#...其他部分defforward(self,x):#...经过MobileNetV3的若干层x=self.ca(x)#加入CA模块returnx```我们需要确保`self.ca`模块的输出通道数与原始MobileNetv3该层的输出通道数一致,并且空间尺寸不变。如果CA模块改变了通道数(例如,原始通道数为C,CA模块输出为C'),那么我们需要在CA模块后添加一个调整通道的卷积:```pythonself.adjust=nn.Conv2d(C',C,kernel_size=1)#如果CA模块改变了通道数```然后在forward中:```pythonx=self.ca(x)x=self.adjust(x)```这样就能恢复原始通道数,保证后续网络的结构匹配。另外,注意CA模块的实现中,注意力图的生成是否正确?CA模块通常不会改变特征图的形状,但需要确认其实现是否正确。如果错误发生在YOLO头部,则可能需要调整头部的通道数设置。YOLOv8的头部通常通过配置文件(如yaml)定义,我们可以检查配置文件中每一层的通道数设置是否与主干输出匹配。最后,如果上述方法不能解决问题,我们可以尝试将问题缩小,例如:-单独测试MobileNetv3-CA主干网络,输入一个固定尺寸的图片,检查输出特征图的尺寸。-单独测试YOLOv8头部,输入一个与主干输出相同尺寸的特征图,检查是否出错。通过逐步隔离,可以定位问题所在。由于具体错误信息有限,以上提供的是通用解决方案。如果用户能提供更多信息(如堆栈跟踪或修改的代码部分),可以更精确地定位问题。相关问题:1.如何调试PyTorch中的张量形状不匹配错误?2.YOLO模型中如何正确设计注意力模块?3.MobileNetv3的结构特点及其与YOLO头部整合的注意事项?4.如何修改YOLOv8的配置文件以适应不同的主干网络?5.在深度学习模型中,特征图尺寸匹配的常见解决方案有哪些?</think>### YOLOv8 MobileNetv3-CA训练中RuntimeError 'size of tensor a (256) must match the size of tensor b (8)' 解决方案 该错误发生在两个张量进行运算时维度不匹配,具体是第1维度上一个张量大小为256,另一个为8。常见原因和解决方案如下: #### 主要原因分析 1. **注意力模块输出维度不匹配** CA(Coordinate Attention)模块的输出通道数与YOLO头部期望的输入通道数不一致。CA模块可能输出8通道特征图,而YOLO头部预期256通道[^4]。 2. **特征金字塔尺寸冲突** 当MobileNetv3的多尺度特征图(如C3/C4/C5层)与YOLO的FPN/PAN结构连接时,空间尺寸或通道数未对齐[^4]。 3. **上采样/下采样比例错误** 转置卷积或插值上采样操作中,输出尺寸计算错误导致后续张量运算维度冲突[^1]。 #### 解决方案步骤 ##### 1. 验证CA模块维度兼容性 在CA模块后添加卷积层统一通道数: ```python from torch import nn class CABlock(nn.Module): def __init__(self, in_channels): super().__init__() self.ca = CoordinateAttention() # 假设输出8通道 self.conv = nn.Conv2d(8, 256, 1) # 1x1卷积统一通道数 def forward(self, x): x = self.ca(x) return self.conv(x) # 输出256通道 ``` ##### 2. 调整特征金字塔通道映射 在`yolov8.yaml`配置文件中修改通道数: ```yaml head: - [-1, 1, Conv, [256, 1, 1]] # 确保输入通道匹配 - [-1, 1, nn.Upsample, [None, 2, 'nearest']] - [[-1, 6], 1, Concat, [1]] # 拼接层需通道一致 ``` ##### 3. 调试维度不匹配位置 添加张量形状日志定位问题层: ```python # 在models/yolo/detect.py的forward函数中添加: print(f"特征图形状: {x.shape}") # 打印每层输出形状 # 当出现维度操作(如cat/add)前添加检查: if x1.shape != x2.shape: raise RuntimeError(f"形状不匹配: {x1.shape} vs {x2.shape}") ``` ##### 4. 修改上采样比例 在FPN结构中显式指定上采样倍数: ```python # 原代码可能使用默认上采样 self.upsample = nn.Upsample(scale_factor=2) # 改为指定输出尺寸避免歧义 self.upsample = nn.Upsample(size=(h*2, w*2)) ``` #### 关键验证点 1. **主干网络输出** 确保MobileNetv3最终输出特征图尺寸为输入图像的1/32(如输入640x640,输出应为20x20) 2. **CA模块位置** 将CA置于浅层(如stride=8的层),避免深层特征图尺寸过小导致计算错误 3. **拼接层对齐** 所有输入`torch.cat()`的张量必须在非拼接维度上完全一致: ```python # 正确示例: 通道数相同(256), 空间尺寸相同 torch.cat([tensor1, tensor2], dim=1) # 要求 tensor1.shape=(b,256,40,40), tensor2.shape=(b,256,40,40) ``` > **调试建议**:使用`torchsummary`库可视化每层输出尺寸: > ```python > from torchsummary import summary > model = YOLO('yolov8n.yaml').model > summary(model, input_size=(3, 640, 640)) > ``` ### 相关问题 1. 如何在YOLOv8中正确集成注意力机制(如CA、SE、CBAM)? 2. 修改YOLO网络结构时如何避免特征图尺寸不匹配? 3. PyTorch中张量形状调试有哪些实用技巧? 4. MobileNetv3作为YOLOv8主干网络需要哪些结构调整? 5. 如何解读RuntimeError中维度不匹配的具体位置信息? [^1]: 张量维度不匹配通常由网络层间输出/输入尺寸计算错误引起 [^2]: 使用1x1卷积可灵活调整通道数而不改变空间尺寸 [^4]: 特征金字塔中的拼接/相加操作要求所有输入张量在非操作维度上完全一致
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值