YOLOV5入门教学-common.py文件

在 YOLOv5 框架中,common.py 文件是一个核心组件,负责定义深度学习模型的基础模块和常用操作。无论是卷积层、激活函数、特征融合还是其他复杂的模型结构,common.py 都提供了灵活且高效的实现。在这篇文章中,我们将深入解析 common.py 的设计思想、各个模块的功能以及它在 YOLOv5 中的应用。通过理解该文件的实现细节,不仅可以帮助我们更好地掌握 YOLOv5 的内部结构,还能为构建自定义模型提供启发。附录一张上面yolov5s.yaml结构图

 一、导入包及其配置

1.1标准库导入

import ast
import contextlib
import json
import math
import platform
import warnings
import zipfile
from collections import OrderedDict, namedtuple
from copy import copy
from pathlib import Path
from urllib.parse import urlparse

import cv2
import numpy as np
import pandas as pd
import requests
import torch
import torch.nn as nn
from PIL import Image
from torch.cuda import amp

1. 标准库导入

  • ast:用于解析和处理 Python 抽象语法树(Abstract Syntax Tree)。通常用于将字符串形式的 Python 表达式转换为实际对象,或者反向操作。
  • contextlib:提供了简化上下文管理的工具,例如使用 with 语句的上下文管理器。
  • json:用于 JSON 数据的编码和解码。适用于与 API 交互或处理 JSON 格式的配置文件。
  • math:包含基础的数学运算函数,例如 sqrtlogcos 等,用于执行常见的数学计算。
  • platform:用于访问操作系统平台相关的信息,比如判断当前系统是 Windows 还是 Linux。
  • warnings:用于发出警告消息,帮助开发者在代码运行时了解潜在的问题。
  • zipfile:用于压缩和解压缩 .zip 文件,通常在处理文件存储或传输时使用。
  • collections.OrderedDict 和 namedtuple
    • OrderedDict:维护字典插入元素的顺序。
    • namedtuple:创建具名元组,类似于一个轻量级的类,具有可访问的字段。
  • copy:提供浅复制(copy.copy)和深复制(copy.deepcopy)的功能。
  • Pathpathlib.Path):用于简化文件和目录路径的操作,比传统的 os.path 更直观。
  • urlparseurllib.parse.urlparse):用于解析 URL,分解出协议、主机名、路径等。

2. 第三方库导入

  • cv2(OpenCV)

    • OpenCV 是一个广泛使用的计算机视觉库,用于图像和视频处理。
    • 在 YOLOv5 中,cv2 通常用于图像的预处理和后处理操作,例如图像读取、显示、缩放、裁剪等。
  • numpy

    • NumPy 是 Python 科学计算的基础库,主要用于处理多维数组。
    • 在 YOLOv5 中,numpy 常用于图像和矩阵运算,比如将图像从张量转换为 NumPy 数组用于可视化等。
  • pandas

    • Pandas 是数据分析的核心库,主要用于处理数据表格、时间序列等结构化数据。
    • 在 YOLOv5 中,pandas 可能用于结果保存和数据分析,尤其是处理 CSV 文件时。
  • requests

    • requests 是一个用于发起 HTTP 请求的库,通常用于从网络下载文件或进行 API 调用。
  • torch(PyTorch)

    • PyTorch 是深度学习的主要框架之一,用于构建、训练和推理深度神经网络。
    • YOLOv5 使用 PyTorch 作为模型的框架来定义网络结构、执行前向传播、计算损失和进行反向传播训练。
  • torch.nn

    • PyTorch 的神经网络模块,包含常见的层定义(如卷积层、全连接层等),用于构建深度学习模型。
  • amp(Automatic Mixed Precision,自动混合精度)

    • torch.cuda.amp 是 PyTorch 用于加速训练的工具,它通过自动切换精度(如使用 FP16 和 FP32)来加速模型训练,同时减少显存占用。

3. 其他导入

  • PIL.Image(Pillow 库)
    • Pillow 是 Python 的图像处理库,用于图像的加载、处理和保存。
    • 在 YOLOv5 中,PIL 通常用于图像的加载和转换操作,比如将图像文件转换为张量。

这段代码引入的库涵盖了深度学习中的各个方面:

  1. 数据处理:使用 numpy 和 pandas 处理数组和结构化数据。
  2. 图像处理cv2 和 PIL 负责图像的读取、预处理和保存。
  3. 深度学习torch 和 torch.nn 用于定义和训练神经网络,amp 用于加速训练。
  4. 文件和网络操作requestszipfile 和 urlparse 提供了下载和解压等功能。

在 YOLOv5 模型中,这些工具被整合在一起,用于高效的图像处理、深度学习推理和训练任务。

1.2 自动安装模块

# Import 'ultralytics' package or install if missing
try:
    import ultralytics

    assert hasattr(ultralytics, "__version__")  # verify package is not directory
except (ImportError, AssertionError):
    import os

    os.system("pip install -U ultralytics")
    import ultralytics


详细流程解析

  1. 尝试导入 ultralytics

    import ultralytics
    • 这一步尝试导入 ultralytics 包。ultralytics 是由 YOLOv5 开发团队维护的库,可能用于计算机视觉任务,例如目标检测、分割等。
  2. 检查 ultralytics 的有效性

    assert hasattr(ultralytics, "__version__")
    • 通过 hasattr(ultralytics, "__version__") 检查是否成功导入了 ultralytics 库。如果导入的是一个目录或者该库没有 __version__ 属性,代码会抛出 AssertionError 异常,表明导入失败或库不正确。
  3. 异常处理(安装 ultralytics 库)

     

    如果 ImportErrorAssertionError 被捕获,则意味着库未安装或导入有问题。此时代码会自动执行以下步骤:

    import os
    os.system("pip install -U ultralytics")
    • 通过 os.system 执行命令行指令,使用 pip install -U ultralytics 命令安装或更新 ultralytics 库。
    • -U 选项表示更新,如果已经安装了该库,会尝试更新到最新版本。
  4. 重新导入 ultralytics

     

    安装完成后,再次导入 ultralytics

    import ultralytics
    • 在成功安装或更新之后,再次导入该库,确保可以在后续代码中正常使用。

关键点总结

  • 自动安装依赖:该代码可以在库未安装或导入失败时自动安装所需的依赖。
  • 异常捕获与处理
    • 使用 try-except 结构捕获 ImportError 和 AssertionError,确保在库未安装或导入失败时执行安装操作。
  • 动态环境适应性:这种动态检测和安装库的方式使得代码能够在多种环境中运行,而不必担心库缺失的问题。

1.3 自定义模块


from ultralytics.utils.plotting import Annotator, colors, save_one_box

from utils import TryExcept
from utils.dataloaders import exif_transpose, letterbox
from utils.general import (
    LOGGER,
    ROOT,
    Profile,
    check_requirements,
    check_suffix,
    check_version,
    colorstr,
    increment_path,
    is_jupyter,
    make_divisible,
    non_max_suppression,
    scale_boxes,
    xywh2xyxy,
    xyxy2xywh,
    yaml_load,
)
from utils.torch_utils import copy_attr, smart_inference_mode

1. 导入模块

代码通过从不同的模块导入函数和类,这些工具帮助 YOLOv5 在推理过程中进行数据处理、绘图和性能优化。下面逐一解读这些导入。

1.1 ultralytics.utils.plotting 导入的工具
from ultralytics.utils.plotting import Annotator, colors, save_one_box

这些导入主要与 YOLOv5 的检测结果可视化相关:

  • Annotator: 用于图像上添加检测框、标签和其他注释的类。它负责将 YOLO 模型的输出绘制到图像上。
  • colors: 颜色工具,通常用于为不同类别的物体分配不同的颜色,便于在检测结果中区分。
  • save_one_box: 一个用于保存单个检测框中的目标图像的函数。这有助于保存感兴趣区域的裁剪图像,方便后续分析。
1.2 utils 模块导入的工具
from utils import TryExcept
from utils.dataloaders import exif_transpose, letterbox
  • TryExcept: 一个工具类,用于处理错误异常。它的设计目的是简化异常捕获机制,确保代码的健壮性。
  • exif_transpose: 用于处理图像的 EXIF 元数据,尤其是方向信息。这是为了保证在推理时图像方向一致。
  • letterbox: 图像预处理函数,用于调整图像大小(如将图像调整到 YOLO 模型需要的尺寸),同时保持原始的宽高比并在图像的空白区域填充像素。
1.3 utils.general 模块导入的工具
from utils.general import (
    LOGGER,
    ROOT,
    Profile,
    check_requirements,
    check_suffix,
    check_version,
    colorstr,
    increment_path,
    is_jupyter,
    make_divisible,
    non_max_suppression,
    scale_boxes,
    xywh2xyxy,
    xyxy2xywh,
    yaml_load,
)

python

这些函数主要涉及日志记录、文件处理、版本检测、颜色处理、文件路径管理、图像检测的后处理、以及坐标转换等任务。让我们简单介绍其中的一些关键工具:

  • LOGGER: 一个日志记录器,用于打印调试信息、模型状态或错误消息。
  • ROOT: 代码或项目的根目录路径,方便统一管理文件路径。
  • Profile: 用于性能分析的类,帮助计算特定代码片段的执行时间,方便优化推理速度。
  • check_requirements: 检查项目依赖是否满足,确保 YOLOv5 能够正常运行。
  • colorstr: 将文本着色的工具,通常用于终端输出时美化文本。
  • non_max_suppression (NMS): 非极大值抑制算法,YOLOv5 用于后处理的关键步骤之一,目的是去除冗余的边界框,保留最优的检测框。
  • scale_boxes: 用于缩放检测框的函数,通常在图像尺寸发生变化时用于调整检测结果。
  • xywh2xyxyxyxy2xywh: 坐标转换工具,用于在 (x, y, w, h) 和 (x1, y1, x2, y2) 之间相互转换。
1.4 utils.torch_utils 导入的工具
from utils.torch_utils import copy_attr, smart_inference_mode
  • copy_attr: 用于复制属性的工具,帮助在模型中高效复制参数或配置。
  • smart_inference_mode: 智能推理模式,用于在推理时优化性能,比如在推理时关闭梯度计算以节省内存和提高速度。

二、基础组件

2.1 autopad

def autopad(k, p=None, d=1):
    """
    Pads kernel to 'same' output shape, adjusting for optional dilation; returns padding size.

    `k`: kernel, `p`: padding, `d`: dilation.
    """
    if d > 1:
        k = d * (k - 1) + 1 if isinstance(k, int) else [d * (x - 1) + 1 for x in k]  # actual kernel-size
    if p is None:
        p = k // 2 if isinstance(k, int) else [x // 2 for x in k]  # auto-pad
    return p

autopad 函数的主要作用是根据卷积核的大小和膨胀系数(dilation)来计算合适的填充大小(padding),从而实现输出形状与输入形状相同的“same”卷积操作。函数的目的是通过自动填充来简化卷积层的配置。让我们详细分析一下这个函数的工作原理:

函数参数

  • k:卷积核大小(kernel size),可以是一个整数(单维卷积核)或列表(多维卷积核,比如二维卷积核 [k_height, k_width])。
  • p:填充大小(padding size),默认为 None,即自动计算填充。如果指定了 p,则直接返回这个填充值。
  • d:膨胀系数(dilation),默认值为 1,表示没有膨胀。膨胀系数用于扩展卷积核的感受野。

核心逻辑

  1. 膨胀调整:

    if d > 1:
        k = d * (k - 1) + 1 if isinstance(k, int) else [d * (x - 1) + 1 for x in k]

    如果膨胀系数 d 大于 1,那么需要对卷积核的大小进行调整。膨胀卷积(dilated convolution)会在卷积核元素之间插入空隙,使得感受野变大。调整后的卷积核大小为:

    • 对于整数卷积核大小 k,新的大小为 d * (k - 1) + 1
    • 对于列表形式的多维卷积核,调整每个维度的大小为 d * (x - 1) + 1
  2. 自动填充计算:

    if p is None:
        p = k // 2 if isinstance(k, int) else [x // 2 for x in k]

    如果用户没有提供 p,即 pNone,函数会自动计算填充大小。填充大小为卷积核大小的一半(取整),这是经典的 same 填充策略,保证卷积操作后输出的大小与输入相同。

    • 如果卷积核大小是整数,填充大小为 k // 2
    • 如果卷积核大小是列表,填充大小为 [x // 2 for x in k],即每个维度的填充大小是该维度卷积核大小的一半。
  3. 返回结果:

    return p

    最终返回计算好的填充大小 p

举例说明

  1. 没有膨胀,使用自动填充:

    autopad(3)  # 返回 1
    • 输入卷积核大小为 3,膨胀系数 d=1,自动填充为 3 // 2 = 1,即输出填充大小为 1。
  2. 二维卷积核,使用自动填充:

    autopad([5, 5])  # 返回 [2, 2]
    • 输入卷积核大小为 [5, 5],自动填充大小为 [5 // 2, 5 // 2] = [2, 2]
  3. 有膨胀,自动计算填充:

    autopad(3, d=2)  # 返回 2
    • 输入卷积核大小为 3,膨胀系数 d=2,首先计算膨胀后的卷积核大小为 2 * (3 - 1) + 1 = 5,然后自动填充为 5 // 2 = 2

2.2 Conv

class Conv(nn.Module):
    # Standard convolution with args(ch_in, ch_out, kernel, stride, padding, groups, dilation, activation)
    default_act = nn.SiLU()  # default activation

    def __init__(self, c1, c2, k=1, s=1, p=None, g=1, d=1, act=True):
        """Initializes a standard convolution layer with optional batch normalization and activation."""
        super().__init__()
        self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p, d), groups=g, dilation=d, bias=False)
        self.bn = nn.BatchNorm2d(c2)
        self.act = self.default_act if act is True else act if isinstance(act, nn.Module) else nn.Identity()

    def forward(self, x):
        """Applies a convolution followed by batch normalization and an activation function to the input tensor `x`."""
        return self.act(self.bn(self.conv(x)))

    def forward_fuse(self, x):
        """Applies a fused convolution and activation function to the input tensor `x`."""
        return self.act(self.conv(x))

1. 类定义与默认激活函数

class Conv(nn.Module):
    default_act = nn.SiLU()  # default activation
  • Conv 类继承自 PyTorch 的 nn.Module,它实现了标准的卷积层。
  • default_act 是默认的激活函数,使用的是 SiLU()(Sigmoid-Weighted Linear Unit),也叫做 Swish 激活函数。这个激活函数在许多现代模型中表现良好。

2. 初始化方法 __init__

def __init__(self, c1, c2, k=1, s=1, p=None, g=1, d=1, act=True):
    super().__init__()
    self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p, d), groups=g, dilation=d, bias=False)
    self.bn = nn.BatchNorm2d(c2)
    self.act = self.default_act if act is True else act if isinstance(act, nn.Module) else nn.Identity()

Conv 是标准卷积层函数,是整个网络中最核心的模块,由卷积层 + BN 层 + 激活函数组成。主要作用是将输入特征经过卷积层、激活函数、归一化层,得到输出层。同时可以指定是否使用归一化层。

参数说明:
  • c1: 输入通道数(输入的特征图通道数)。
  • c2: 输出通道数(卷积输出的特征图通道数)。
  • k: 卷积核大小(kernel size),默认为 1。
  • s: 卷积步长(stride),默认为 1。
  • p: 填充大小(padding),默认为 None,由 autopad() 函数自动计算合适的填充值。
  • g: 卷积组(groups),默认为 1。它用于组卷积(group convolution),例如在深度可分离卷积中使用。
  • d: 膨胀系数(dilation),默认为 1,用于膨胀卷积。
  • act: 激活函数。如果 act=True,使用默认的激活函数 nn.SiLU();如果 act 是 nn.Module 的实例,则使用自定义激活函数;否则使用 nn.Identity(),即无激活函数。
主要组件:
  1. self.conv: 标准的 2D 卷积层,由 nn.Conv2d 实现。它使用 autopad() 函数自动计算填充大小,确保输出尺寸与输入的设定相符。
  2. self.bn: 批归一化层,使用 nn.BatchNorm2d 对卷积后的输出进行归一化,帮助稳定训练过程。
  3. self.act: 激活函数。根据 act 参数设置,可能是默认的 SiLU(),或者是用户传入的自定义激活函数,或没有激活函数(nn.Identity())。

3. 前向传播 forward

def forward(self, x):
    return self.act(self.bn(self.conv(x)))

这个函数定义了卷积层的前向传播过程:

  • 首先,对输入的张量 x 进行卷积操作(self.conv(x))。
  • 接着,对卷积结果进行批归一化(self.bn())。
  • 最后,应用激活函数(self.act())。

完整的顺序为 卷积 -> 批归一化 -> 激活。这是卷积神经网络中常见的标准模式。

4. 融合前向传播 forward_fuse

def forward_fuse(self, x):
    return self.act(self.conv(x))

forward_fuse 是为推理优化的前向传播方法,它只应用卷积和激活函数,跳过了批归一化。这在推理阶段非常有用,因为在模型训练完成后,批归一化的均值和方差已经被学习到,因此可以直接与卷积权重融合,从而提高推理效率。

torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True, padding_mode='zeros')

nn.Conv2d 是 PyTorch 中用于实现二维卷积的函数,用于处理图像等二维输入数据。二维卷积是卷积神经网络(CNN)的核心操作,能够对图像或特征图进行特征提取。

1. 参数解释

  • in_channels(输入通道数):

    • 输入数据的通道数(即输入特征图的深度),对于 RGB 图像,in_channels=3,而对于灰度图像,in_channels=1
  • out_channels(输出通道数):

    • 卷积操作后生成的特征图的通道数。该值决定了卷积核的数量,每个卷积核会输出一个特征图。
  • kernel_size(卷积核大小):

    • 卷积核的大小,可以是一个整数(表示高和宽相等)或一个元组(如 (height, width)),用于指定卷积核的尺寸。
  • stride(步长,默认为 1):

    • 控制卷积核在输入数据上滑动的步长,默认为 1,即每次卷积核移动 1 个像素。如果设为 2,卷积核每次移动 2 个像素。
  • padding(填充,默认为 0):

    • 控制在输入数据的边缘添加的像素数量,填充用于控制输出特征图的空间大小。padding 可以是:
      • 整数(例如 1 表示在每个边界添加 1 个像素)。
      • 元组(例如 (1, 2) 表示在高和宽方向上分别添加不同数量的像素)。
      • 通常使用 autopad 函数自动计算。
  • dilation(膨胀,默认为 1):

    • 控制卷积核元素之间的间距,默认为 1(标准卷积),如果大于 1,则会在卷积核元素之间插入空洞,形成 膨胀卷积,从而扩大卷积核的感受野。
  • groups(组卷积,默认为 1):

    • 控制输入和输出通道之间的连接方式。默认为 1,即标准卷积,所有的输入通道与输出通道相连。如果 groups=输入通道数,则为 深度可分离卷积,每个输入通道只与对应的输出通道进行卷积。
  • bias(偏置项,默认为 True):

    • 是否在卷积层中添加偏置项。默认值为 True,即每个输出通道都有一个可学习的偏置。如果设置为 False,则不使用偏置项。
  • padding_mode(填充模式,默认为 'zeros'):

    • 指定填充的方式,默认是 'zeros'(用 0 进行填充),其他选项包括 'reflect'(反射填充)、'replicate'(复制边缘像素填充)等。

2. 卷积输出形状计算公式

对于二维卷积,输出特征图的高度和宽度可以通过以下公式计算:

output_size=(strideinput_size+2×padding−dilation×(kernel_size−1)−1​)+1

其中:

  • input_size 是输入图像的高度或宽度。
  • kernel_size 是卷积核的大小。
  • padding 是填充大小。
  • stride 是步长。
  • dilation 是膨胀系数。

3. 例子

示例 1:标准卷积
import torch
import torch.nn as nn

# 输入通道为3(RGB图像),输出通道为16,卷积核大小为3x3,步长为1,填充为1
conv_layer = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3, stride=1, padding=1)

# 输入为一个大小为(1, 3, 32, 32)的张量(batch_size=1, channels=3, height=32, width=32)
input_tensor = torch.randn(1, 3, 32, 32)

# 经过卷积层后,输出的形状为 (1, 16, 32, 32)
output_tensor = conv_layer(input_tensor)
print(output_tensor.shape)

在这个例子中:

  • 输入的通道数是 3(例如 RGB 图像)。
  • 卷积核大小为 3x3,步长为 1,填充为 1,这样卷积后的输出大小与输入大小保持一致(32x32)。
  • 输出的通道数为 16,这意味着我们对输入应用了 16 个卷积核,得到 16 张输出特征图。
示例 2:使用膨胀卷积和组卷积
# 输入通道为3,输出通道为6,卷积核为3x3,步长为1,填充为1,膨胀系数为2,组卷积为3
conv_layer = nn.Conv2d(in_channels=3, out_channels=6, kernel_size=3, stride=1, padding=1, dilation=2, groups=3)

# 输入张量大小为(1, 3, 32, 32)
input_tensor = torch.randn(1, 3, 32, 32)

# 输出张量的形状
output_tensor = conv_layer(input_tensor)
print(output_tensor.shape)

在这个例子中:

  • 膨胀卷积的膨胀系数为 2,卷积核元素之间有间隔,感受野更大。
  • groups=3 表示进行组卷积,每个输入通道只与相应的输出通道进行卷积。这样可以减少参数和计算量。

4. 关键点总结

  • nn.Conv2d 是二维卷积的核心操作:它可以在图像或特征图中提取空间信息,如边缘、纹理等。
  • 卷积参数的灵活性:通过 kernel_sizestridepaddingdilation 和 groups 等参数,你可以控制卷积的行为,比如输出特征图的大小、感受野、计算复杂度等。
  • 自动填充:通过 padding 参数或使用 autopad 函数,可以保证输出与输入的大小一致。
  • 推理和训练的灵活性:卷积层在训练和推理过程中表现优异,可以高效提取特征,同时能够与批归一化(BatchNorm2d)、激活函数等操作组合使用,形成标准的 CNN 结构。

2.3 DWConv

class DWConv(Conv):
    # Depth-wise convolution
    def __init__(self, c1, c2, k=1, s=1, d=1, act=True):
        """Initializes a depth-wise convolution layer with optional activation; args: input channels (c1), output
        channels (c2), kernel size (k), stride (s), dilation (d), and activation flag (act).
        """
        super().__init__(c1, c2, k, s, g=math.gcd(c1, c2), d=d, act=act)

DWConv 是深度可分离卷积(DepthWise Convolution)的实现,它继承了标准的卷积层类 Conv,并对其进行了一些修改,使其能够执行深度可分离卷积操作。深度可分离卷积是一种优化卷积计算的方式,它将标准卷积操作分解为深度卷积(Depthwise Convolution)和逐点卷积(Pointwise Convolution)。

深度可分离卷积(DepthWise Convolution)概述:

  • 标准卷积:每个输出通道与输入的所有通道进行卷积操作,计算复杂度较高。
  • 深度卷积:每个输入通道仅与其对应的卷积核做卷积,互不干涉,之后通过逐点卷积(1x1 卷积)将这些特征进行线性组合。

通过这种分离,深度可分离卷积极大地减少了卷积操作中的计算量和参数数量。

1. 参数介绍

  • c1(输入通道数):输入特征图的通道数。
  • c2(输出通道数):输出特征图的通道数。
  • k(卷积核大小):卷积核的尺寸,默认是 1
  • s(步长):卷积核在输入特征图上的滑动步长,默认是 1
  • d(膨胀系数):卷积核元素之间的间隔,默认为 1,即普通卷积。
  • act(激活函数):是否使用激活函数,默认为 True 使用默认的 SiLU 激活函数,也可以传入自定义的激活函数。

2. 深度可分离卷积关键点:g=math.gcd(c1, c2)

  • groups=g=math.gcd(c1, c2)
    • 这里的 g 参数表示卷积组数,使用 math.gcd(c1, c2) 来自动确定。
    • 如果输入通道数 c1 与输出通道数 c2 的最大公约数为 c1,那么这就是典型的 深度卷积,每个输入通道只与对应的卷积核做卷积,即每个卷积组只处理一个输入通道。
    • 如果 g=1,则是标准卷积。
    • 通过这种设置,代码能够根据输入和输出通道数动态调整卷积的类型和计算复杂度。

3. 继承的 Conv

DWConv 继承自 Conv,因此除了深度卷积外,它仍然保留了 Conv 中的功能,包括:

  • 卷积层(通过 super().__init__() 实现)。
  • 批归一化层(Batch Normalization)。
  • 激活函数
Conv 类中使用的 super().__init__(c1, c2, k, s, g=..., d=d, act=act)

Conv 类中的初始化函数调用了 nn.Conv2d,并且设置了 groups=g 参数,这意味着在 DWConv 中,groups 将决定是标准卷积还是深度卷积。

4. 深度可分离卷积的优势

  • 计算效率:深度卷积相比标准卷积大大减少了计算量,因为它只对每个输入通道进行独立卷积操作,而不是跨所有通道进行卷积。
  • 参数减少:通过将标准卷积分为深度卷积和逐点卷积,参数量减少了近乎线性级别,因此可以加速推理速度。
  • 感受野:深度卷积可以扩展卷积核的感受野,尤其是在配合膨胀卷积(dilated convolution)时,可以在不增加计算量的情况下捕捉更大的上下文信息。

6. 图示

类似参考图片中的结构,深度可分离卷积通常被分为两个阶段:

图源Depthwise卷积与Pointwise卷积-CSDN博客

在深度可分离卷积(Depthwise Separable Convolution)中,正如你所提到的,第一部分是深度卷积(Depthwise Convolution),它对每个输入通道单独进行卷积,但由于每个通道是独立处理的,所以没有充分利用空间上不同通道之间的关联信息。为了弥补这一不足,第二部分是逐点卷积(Pointwise Convolution),它通过 1x1 的卷积核将不同通道的信息结合起来,生成新的特征图。

第二部分:逐点卷积(Pointwise Convolution)

逐点卷积的核心思想是通过 1x1 卷积来融合在 深度卷积 阶段生成的每个通道的特征图。具体地,它将所有通道(feature map)整合成一个新的特征图,用来提取各通道间的交互信息。

1. Pointwise Convolution 的操作过程
  • 卷积核大小:1x1,这意味着它不会改变空间维度(高度和宽度保持不变)。
  • 作用:其作用是将不同的通道之间的特征进行组合,从而生成新的通道表示。
  • 通道数:输出通道数等于指定的输出通道数,而输入通道数等于深度卷积阶段的输出通道数。
  • 计算效率:1x1 卷积的计算量相比普通卷积要少得多,因此它可以高效地进行通道间的信息融合。
2. 逐点卷积的原理

在逐点卷积中,每个位置的特征点仅受到同一位置上所有通道的特征点的影响,不会改变空间位置。它的核心作用是结合深度卷积输出的多个通道上的信息,从而生成更丰富的特征。

逐点卷积示意图:

输入特征图 (Input)
    ↓
深度卷积 (Depthwise Conv) - 逐个通道卷积,通道数不变,特征提取
    ↓
逐点卷积 (Pointwise Conv) - 1x1卷积,融合通道之间的信息
    ↓
输出特征图 (Output)

逐点卷积的卷积核大小为 1x1,意味着它不会改变特征图的空间大小,而是仅在通道维度上进行信息的组合与变化。这使得模型能够在通道维度上进行特征融合,以形成更丰富的特征表示。

3. 逐点卷积的代码示例

在深度可分离卷积中,逐点卷积通常与深度卷积结合使用。下面是一个 PyTorch 中实现深度可分离卷积的示例,展示了如何使用 1x1 的卷积核来实现逐点卷积:

import torch
import torch.nn as nn

class DepthwiseSeparableConv(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size=3, stride=1, padding=1, dilation=1):
        super(DepthwiseSeparableConv, self).__init__()
        # 深度卷积:每个输入通道对应一个独立的卷积核
        self.depthwise = nn.Conv2d(in_channels, in_channels, kernel_size, stride, padding, dilation, groups=in_channels, bias=False)
        # 逐点卷积:1x1卷积,用于将不同通道的特征组合
        self.pointwise = nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False)

    def forward(self, x):
        x = self.depthwise(x)
        x = self.pointwise(x)
        return x

# 示例
input_tensor = torch.randn(1, 32, 64, 64)  # Batch size 1, 32个通道,64x64大小的图像
model = DepthwiseSeparableConv(in_channels=32, out_channels=64, kernel_size=3, stride=1, padding=1)
output_tensor = model(input_tensor)
print(output_tensor.shape)  # 输出的形状将是 (1, 64, 64, 64)

4. 逐点卷积的优点

  • 减少计算量:相比标准卷积,深度可分离卷积将卷积的计算分为两个阶段,先提取每个通道的特征,再用 1x1 卷积进行特征组合。计算量显著减少,尤其在输入通道数较多时。
  • 保持特征的空间分辨率:1x1 的卷积不会改变输入特征图的空间分辨率,但可以通过增加或减少通道数来调整输出特征图的深度。
  • 提高模型效率:深度卷积和逐点卷积的分离方式大大减少了参数数量,尤其是在处理大尺寸输入图像时。

5. 逐点卷积的作用总结

逐点卷积在深度可分离卷积中的作用就是结合每个输入通道的独立特征,将其重新整合到新的通道上,这样可以确保空间信息被提取后,各个通道之间的依赖关系也能够被充分利用。这个过程让模型能够更有效地捕捉复杂的特征模式。

逐点卷积的主要特点是:

  • 只关注通道维度上的特征组合。
  • 它通常紧跟在深度卷积后,用于将独立通道的特征重新融合为输出通道。
  • 它极大地减少了卷积运算的参数和计算量。

总结

深度可分离卷积将卷积操作分成两部分:

  1. 深度卷积(Depthwise Convolution):独立对每个通道进行卷积操作,提取空间特征。
  2. 逐点卷积(Pointwise Convolution):通过 1x1 的卷积核将各个通道的信息进行组合,生成新的特征图。

这种方式不仅大大减少了卷积层的计算复杂度,而且能够保持高效的特征提取能力,特别适用于轻量级模型(如 MobileNet 等)。

class DWConvTranspose2d(nn.ConvTranspose2d):
    # Depth-wise transpose convolution
    def __init__(self, c1, c2, k=1, s=1, p1=0, p2=0):
        """Initializes a depth-wise transpose convolutional layer for YOLOv5; args: input channels (c1), output channels
        (c2), kernel size (k), stride (s), input padding (p1), output padding (p2).
        """
        super().__init__(c1, c2, k, s, p1, p2, groups=math.gcd(c1, c2))

DWConvTranspose2d 是一个深度卷积转置(反卷积)层的实现,它继承了 PyTorch 的 nn.ConvTranspose2d,并且使用了深度卷积的概念,通过将卷积的 groups 参数设置为 math.gcd(c1, c2) 来实现组卷积,允许每个输入通道与对应的卷积核独立进行反卷积操作。

1. 反卷积(Transpose Convolution)

反卷积(也称为转置卷积,Deconvolution)与标准卷积的方向相反,它的作用是将输入的特征图放大到更高的空间分辨率。这在生成模型(如生成对抗网络)或者图像上采样(如 YOLOv5 的解码阶段)中非常有用。反卷积允许我们将低分辨率的特征图还原到高分辨率,从而恢复空间信息。

2. 代码分析

DWConvTranspose2d 类实现了一个带组卷积的反卷积操作,它通过继承 nn.ConvTranspose2d 并使用 groups=math.gcd(c1, c2) 来支持深度可分离的卷积操作。

主要参数解释:
  • c1(输入通道数):输入特征图的通道数。
  • c2(输出通道数):反卷积后输出特征图的通道数。
  • k(卷积核大小):反卷积核的尺寸,默认为 1
  • s(步长):反卷积操作的步长,默认为 1。步长可以控制特征图的上采样倍率(例如 s=2 会将特征图的空间尺寸放大两倍)。
  • p1(输入填充):输入特征图的填充数,控制输入图边界的填充方式。
  • p2(输出填充):反卷积后输出特征图的额外填充量。输出填充用来控制输出大小的精细调节。
  • groups=math.gcd(c1, c2):使用输入通道数和输出通道数的最大公约数来设置组卷积(group convolution)。当 groups=c1 时,执行的是深度卷积转置,每个输入通道独立进行反卷积操作。

3. 反卷积 vs. 标准卷积

标准卷积(卷积操作):
  • 在输入特征图上应用卷积核,并且通常会减小特征图的空间维度(通过步长、填充等)。
  • 主要用于提取空间特征。
反卷积(转置卷积操作):
  • 与标准卷积相反,它用于放大特征图的空间维度,通常用于上采样操作。
  • 主要用于恢复分辨率或者生成高分辨率的输出(如目标检测、语义分割等任务中的解码阶段)。

4. 深度卷积转置的优势

DWConvTranspose2d 中,使用了组卷积或深度卷积的形式(通过 groups 参数实现),这意味着:

  • 计算效率高:深度卷积允许每个输入通道独立进行卷积运算,因此相较于标准卷积,计算量大大减少。尤其是当 groups=c1 时,模型的计算复杂度进一步降低。
  • 参数减少:相比标准卷积,使用深度卷积和组卷积会减少卷积核的参数量。
  • 应用于轻量级模型:这种结构特别适用于轻量级的网络架构,如 YOLOv5 中的解码阶段,在需要高效、快速推理的场景中深度卷积是一种常见的设计选择。

5. 示例用法

为了更好地理解这个类如何工作,我们可以编写一个简单的例子,展示如何使用 DWConvTranspose2d 来对输入特征图进行上采样(放大):

import torch
import torch.nn as nn
import math

class DWConvTranspose2d(nn.ConvTranspose2d):
    def __init__(self, c1, c2, k=1, s=1, p1=0, p2=0):
        """深度卷积转置的初始化;参数:输入通道数 (c1),输出通道数 (c2),卷积核大小 (k),步长 (s),输入填充 (p1),输出填充 (p2)。"""
        super().__init__(c1, c2, k, s, p1, p2, groups=math.gcd(c1, c2))

# 示例使用
input_tensor = torch.randn(1, 16, 32, 32)  # Batch size 1, 16通道, 32x32尺寸的特征图
model = DWConvTranspose2d(c1=16, c2=32, k=3, s=2, p1=1, p2=1)  # 使用3x3卷积核,步长为2,上采样
output_tensor = model(input_tensor)
print(output_tensor.shape)  # 输出的形状将是 (1, 32, 64, 64),即特征图尺寸被放大了两倍

在这个例子中:

  • 输入的特征图大小为 32x32,通道数为 16。
  • 通过深度卷积转置层,使用 3x3 的卷积核和步长为 2,对输入特征图进行上采样,输出的特征图大小变为 64x64,通道数变为 32。

6. 应用场景

DWConvTranspose2d 可以在许多应用中使用,尤其是在需要上采样的场景中,例如:

  • 生成对抗网络(GAN):生成器网络中通常使用反卷积操作来逐步放大特征图。
  • 目标检测与语义分割:在 YOLOv5 等检测网络中,反卷积用于解码阶段,以便逐步恢复原始图像的分辨率。
  • 超分辨率任务:通过反卷积,可以将低分辨率的特征图放大为高分辨率,从而恢复更多的细节。

2.4 Bottleneck

class Bottleneck(nn.Module):
    # Standard bottleneck
    def __init__(self, c1, c2, shortcut=True, g=1, e=0.5):
        """Initializes a standard bottleneck layer with optional shortcut and group convolution, supporting channel
        expansion.
        """
        super().__init__()
        c_ = int(c2 * e)  # hidden channels
        self.cv1 = Conv(c1, c_, 1, 1)
        self.cv2 = Conv(c_, c2, 3, 1, g=g)
        self.add = shortcut and c1 == c2

    def forward(self, x):
        """Processes input through two convolutions, optionally adds shortcut if channel dimensions match; input is a
        tensor.
        """
        return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))

Bottleneck 类是深度学习中非常常见的一种模块,它使用了瓶颈结构(Bottleneck Structure),目的是通过减少通道数来降低计算量,然后再恢复原始通道数,从而达到高效提取特征的目的。这个结构最早是在 ResNet 中提出的,用于设计深度残差网络(Residual Network),并且在 YOLOv5 这样的目标检测模型中也非常常见。

1. Bottleneck 结构概述

Bottleneck 模块通常由以下几个部分组成:

  • 1x1 卷积:用于减少通道数,从而降低计算复杂度(即降维操作)。
  • 3x3 卷积:用来在降低后的通道上进行特征提取(主要卷积操作)。
  • 残差连接(Shortcut):如果输入和输出的维度一致,则使用残差连接,将输入直接加到输出上,形成一个 "shortcut"。

2. 参数解释

  • c1(输入通道数):输入特征图的通道数。
  • c2(输出通道数):经过瓶颈结构后的输出通道数。
  • shortcut(是否使用残差连接):当为 True 时,如果输入和输出通道数相同,则会使用残差连接。
  • g(组卷积参数):控制 3x3 卷积的组卷积,默认是 1,即标准卷积。通过调整 g,可以让不同通道组独立进行卷积计算。
  • e(扩展系数):控制 1x1 卷积后的中间通道数。e=0.5 表示中间通道数是输出通道数的一半,从而起到降维的作用。

3. Bottleneck 结构解读

步骤:
  1. 通道降维:首先,使用 1x1 卷积将输入的通道数从 c1 减少到 c_,这个 c_c2 和扩展系数 e 的乘积。该步骤减少了计算量,并且降低了特征维度。

  2. 卷积提取特征:接下来,使用 3x3 卷积在降维后的特征图上进行卷积操作,用于提取空间特征。这里可以选择是否使用组卷积。

  3. 残差连接:如果输入和输出的通道数一致(c1 == c2),并且 shortcut=True,则通过残差连接将输入特征与卷积后的特征相加,这样可以保持梯度传递的稳定性,并避免特征信息的丢失。

分支选择:
  • 残差连接self.add 是 True 时,表示输入通道数 c1 和输出通道数 c2 相同,使用了残差连接(shortcut)。
  • 无残差连接:如果 c1 != c2 或者 shortcut=False,则不使用残差连接,直接返回经过两个卷积层处理后的输出。

4. Bottleneck 的优势

  • 计算效率高:通过 1x1 卷积先降维,再使用 3x3 卷积进行特征提取,可以大幅减少计算量和参数量,尤其是在深层网络中表现尤为突出。
  • 残差连接:引入残差连接后,即使网络变得非常深,也能够避免梯度消失问题,使得网络更容易训练。
  • 灵活性:通过参数 g 控制组卷积,适合各种不同需求的网络设计;通过 e 控制降维的比例,可以自由调整计算复杂度和特征提取能力。

5. 工作流程

  1. 输入:输入的特征图大小为 [batch_size, c1, h, w],其中 c1 为通道数,hw 分别为特征图的高度和宽度。

  2. 卷积 1:先经过 1x1 卷积层,将通道数从 c1 降维到 c_。输出的形状为 [batch_size, c_, h, w]

  3. 卷积 2:然后经过 3x3 卷积层,将通道数恢复到 c2。输出的形状为 [batch_size, c2, h, w]

  4. 残差连接:如果 shortcut=Truec1 == c2,则将输入特征图与卷积后的输出相加(残差连接)。

  5. 输出:最终输出的特征图大小为 [batch_size, c2, h, w]

6. 示例

假设我们有一个输入特征图,通道数为 64,经过 Bottleneck 模块后通道数保持不变,使用残差连接。我们可以通过以下代码演示:

import torch
import torch.nn as nn

# 假设输入为 64 通道,输出也为 64 通道
input_tensor = torch.randn(1, 64, 32, 32)  # [batch_size, channels, height, width]

# 初始化 Bottleneck 模块
bottleneck_layer = Bottleneck(64, 64, shortcut=True)

# 进行前向传播
output_tensor = bottleneck_layer(input_tensor)

print(output_tensor.shape)  # 输出形状为 [1, 64, 32, 32],与输入形状一致

8. 应用场景

  • YOLOv5:在 YOLOv5 的架构中,Bottleneck 模块用于提取特征,并在网络的浅层和深层之间传递信息,以保证目标检测任务中的精确度。
  • ResNet:ResNet 网络中,Bottleneck 模块被广泛应用于残差块(Residual Block)中,通过残差连接和降维操作,网络能够变得非常深,并且易于训练。

9. 总结

Bottleneck 模块通过两层卷积和残差连接的结构,显著提高了网络的计算效率,同时保持了较强的特征提取能力。它的核心思想是通过降维操作减少计算量,再通过 3x3 卷积提取特征,最后通过残差连接增强梯度流动,避免梯度消失问题。

2.5 BottleneckCSP

class BottleneckCSP(nn.Module):
    # CSP Bottleneck https://github.com/WongKinYiu/CrossStagePartialNetworks
    def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):
        """Initializes CSP bottleneck with optional shortcuts; args: ch_in, ch_out, number of repeats, shortcut bool,
        groups, expansion.
        """
        super().__init__()
        c_ = int(c2 * e)  # hidden channels
        self.cv1 = Conv(c1, c_, 1, 1)
        self.cv2 = nn.Conv2d(c1, c_, 1, 1, bias=False)
        self.cv3 = nn.Conv2d(c_, c_, 1, 1, bias=False)
        self.cv4 = Conv(2 * c_, c2, 1, 1)
        self.bn = nn.BatchNorm2d(2 * c_)  # applied to cat(cv2, cv3)
        self.act = nn.SiLU()
        self.m = nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)))

    def forward(self, x):
        """Performs forward pass by applying layers, activation, and concatenation on input x, returning feature-
        enhanced output.
        """
        y1 = self.cv3(self.m(self.cv1(x)))
        y2 = self.cv2(x)
        return self.cv4(self.act(self.bn(torch.cat((y1, y2), 1))))

构造函数 __init__

def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):
    ...
  • c1: 输入通道数。
  • c2: 输出通道数。
  • nBottleneck 块的重复次数。
  • shortcut: 是否使用快捷连接(shortcut connections)。
  • g: 组卷积的组数,默认为 1,即普通卷积。
  • e: 通道扩展的系数,决定隐藏层的通道数。

隐藏通道数计算

c_ = int(c2 * e)  # hidden channels
  • c_ 是隐藏层的通道数,通过输出通道数 c2 乘以扩展系数 e 来计算。

各层定义

self.cv1 = Conv(c1, c_, 1, 1)
self.cv2 = nn.Conv2d(c1, c_, 1, 1, bias=False)
self.cv3 = nn.Conv2d(c_, c_, 1, 1, bias=False)
self.cv4 = Conv(2 * c_, c2, 1, 1)
self.bn = nn.BatchNorm2d(2 * c_)
self.act = nn.SiLU()
self.m = nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)))
  • self.cv1 和 self.cv2:1x1卷积层。cv1 负责将输入的 c1 个通道转换为 c_ 个隐藏通道,cv2 直接作用于输入。
  • self.cv3:也是一个 1x1 卷积层,用于将通过 self.m 模块处理过的特征再次处理。
  • self.cv4:1x1卷积层,负责将 concat 之后的特征映射回 c2 个通道。
  • self.bn:批归一化(Batch Normalization)层,用于归一化连接后的特征图。
  • self.act:激活函数,使用 SiLU (Swish) 激活函数。
  • self.m:由 Bottleneck 组成的顺序模块,内部包含 n 个 Bottleneck 块,Bottleneck 是一种常见的残差块。

前向传播 forward

def forward(self, x):
    y1 = self.cv3(self.m(self.cv1(x)))
    y2 = self.cv2(x)
    return self.cv4(self.act(self.bn(torch.cat((y1, y2), 1))))
  • x 是输入张量,首先通过 cv1 层得到特征图,然后通过 self.m(包含多个 Bottleneck 块)进一步处理,最后通过 cv3 层得到 y1
  • y2 是输入直接通过 cv2 处理后的结果。
  • y1 和 y2 通过 torch.cat 在通道维度上进行拼接,然后通过批归一化和激活函数处理,最后通过 cv4 输出最终的结果。

2.6 CrossConv

class CrossConv(nn.Module):
    # Cross Convolution Downsample
    def __init__(self, c1, c2, k=3, s=1, g=1, e=1.0, shortcut=False):
        """
        Initializes CrossConv with downsampling, expanding, and optionally shortcutting; `c1` input, `c2` output
        channels.

        Inputs are ch_in, ch_out, kernel, stride, groups, expansion, shortcut.
        """
        super().__init__()
        c_ = int(c2 * e)  # hidden channels
        self.cv1 = Conv(c1, c_, (1, k), (1, s))
        self.cv2 = Conv(c_, c2, (k, 1), (s, 1), g=g)
        self.add = shortcut and c1 == c2

    def forward(self, x):
        """Performs feature sampling, expanding, and applies shortcut if channels match; expects `x` input tensor."""
        return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))

构造函数 __init__

def __init__(self, c1, c2, k=3, s=1, g=1, e=1.0, shortcut=False):
    ...
  • c1: 输入通道数。
  • c2: 输出通道数。
  • k: 卷积核的大小,默认为 3。
  • s: 步幅 (stride),决定下采样的程度,默认为 1。
  • g: 组卷积的组数,默认为 1,即普通卷积。
  • e: 通道扩展的系数,决定隐藏层的通道数,默认为 1.0。
  • shortcut: 布尔值,是否使用快捷连接。

隐藏通道数计算

c_ = int(c2 * e)  # hidden channels
  • c_ 是隐藏通道数,通过输出通道数 c2 乘以扩展系数 e 计算得到。

各层定义

self.cv1 = Conv(c1, c_, (1, k), (1, s))
self.cv2 = Conv(c_, c2, (k, 1), (s, 1), g=g)
self.add = shortcut and c1 == c2
  • self.cv1:第一个卷积层,卷积核大小为 (1, k),步幅为 (1, s)。这个卷积层沿着宽度方向(水平)进行卷积,保持高度不变。
  • self.cv2:第二个卷积层,卷积核大小为 (k, 1),步幅为 (s, 1),并且支持组卷积(group convolution)。这个卷积层沿着高度方向(垂直)进行卷积,保持宽度不变。
  • self.add:布尔值,如果 shortcut 为 True 且 c1 等于 c2,则表示可以进行快捷连接(即输入和输出通道数相同)。

前向传播 forward

def forward(self, x):
    """Performs feature sampling, expanding, and applies shortcut if channels match; expects `x` input tensor."""
    return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))
  • 输入 x 首先通过 cv1 卷积层,然后再通过 cv2 卷积层。
  • 如果 self.add 为 True,即允许快捷连接并且输入通道数等于输出通道数,那么结果是输入 x 加上卷积处理后的结果 (x + self.cv2(self.cv1(x)))。
  • 如果 self.add 为 False,则直接返回卷积后的结果 (self.cv2(self.cv1(x)))。

设计思想

CrossConv 通过两个交叉方向的卷积操作(一个沿宽度方向,另一个沿高度方向)来增强特征提取能力。交叉卷积可以捕捉输入张量中更加细粒度的特征,同时保持相对较低的计算成本。

这个模块还提供了可选的快捷连接,类似于 ResNet 中的残差连接,可以帮助缓解梯度消失问题,特别是在网络较深的情况下。

2.7 C3

class C3(nn.Module):
    # CSP Bottleneck with 3 convolutions
    def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):
        """Initializes C3 module with options for channel count, bottleneck repetition, shortcut usage, group
        convolutions, and expansion.
        """
        super().__init__()
        c_ = int(c2 * e)  # hidden channels
        self.cv1 = Conv(c1, c_, 1, 1)
        self.cv2 = Conv(c1, c_, 1, 1)
        self.cv3 = Conv(2 * c_, c2, 1)  # optional act=FReLU(c2)
        self.m = nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)))

    def forward(self, x):
        """Performs forward propagation using concatenated outputs from two convolutions and a Bottleneck sequence."""
        return self.cv3(torch.cat((self.m(self.cv1(x)), self.cv2(x)), 1))

构造函数 __init__

def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):
    ...
  • c1: 输入通道数。
  • c2: 输出通道数。
  • nBottleneck 块的重复次数。
  • shortcut: 是否使用快捷连接,默认为 True
  • g: 组卷积的组数,默认为 1。
  • e: 通道扩展的系数,决定隐藏层的通道数。

隐藏通道数计算

c_ = int(c2 * e)  # hidden channels
  • c_ 是隐藏通道数,通过输出通道数 c2 乘以扩展系数 e 计算得到。

各层定义

self.cv1 = Conv(c1, c_, 1, 1)
self.cv2 = Conv(c1, c_, 1, 1)
self.cv3 = Conv(2 * c_, c2, 1)  # optional act=FReLU(c2)
self.m = nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)))
  • self.cv1: 一个 1x1 卷积层,将输入的 c1 个通道映射到 c_ 个隐藏通道。
  • self.cv2: 另一个 1x1 卷积层,同样将输入的 c1 个通道映射到 c_ 个隐藏通道。与 cv1 并行处理。
  • self.cv3: 最终的 1x1 卷积层,将拼接后的特征映射回 c2 个通道。注意,代码注释中提到可以选择使用 FReLU(c2) 作为激活函数,但默认未使用。
  • self.m: 包含 n 个 Bottleneck 块的顺序模块,每个 Bottleneck 块进一步处理通过 cv1 层的特征。

前向传播 forward

def forward(self, x):
    """Performs forward propagation using concatenated outputs from two convolutions and a Bottleneck sequence."""
    return self.cv3(torch.cat((self.m(self.cv1(x)), self.cv2(x)), 1))
  • 输入 x 首先通过 cv1 和 cv2 两个 1x1 卷积层分别得到两个特征图。
  • cv1 的输出进一步通过包含 n 个 Bottleneck 块的顺序模块 self.m 进行处理。
  • 将 self.m 处理后的输出与 cv2 的输出在通道维度上拼接(torch.cat)。
  • 最后,拼接后的结果通过 cv3 卷积层得到最终输出。

设计思想

C3 模块简化了 BottleneckCSP 的设计,通过去除一个卷积层,降低了计算复杂度。这种设计依然保留了核心的特征融合机制,即通过在两个不同路径上处理输入特征,再将它们拼接在一起,来增强特征的表达能力。

这种模块在 YOLOv5 等模型中广泛应用,用于特征提取部分,能够有效地在降低计算开销的同时,保持甚至提升模型的性能。

总结

C3 模块是 BottleneckCSP 的简化版本,它通过减少卷积层数来提高计算效率,但保留了通过并行路径进行特征提取和融合的关键设计。这使得 C3 模块在需要快速推理的应用中非常实用,同时也能保持较好的性能。

2.7.1 C3x

class C3x(C3):
    # C3 module with cross-convolutions
    def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):
        """Initializes C3x module with cross-convolutions, extending C3 with customizable channel dimensions, groups,
        and expansion.
        """
        super().__init__(c1, c2, n, shortcut, g, e)
        c_ = int(c2 * e)
        self.m = nn.Sequential(*(CrossConv(c_, c_, 3, 1, g, 1.0, shortcut) for _ in range(n)))
  • 继承自C3
  • 主要功能: 使用 CrossConv 替换原有的 Bottleneck 模块,保留了 C3 模块的基本架构。
  • CrossConv 模块: 通过交叉卷积(Cross Convolutions)进行特征提取,相比普通卷积可以更高效地捕捉跨通道和空间维度的特征。
  • 用途: 适用于需要更精细特征提取的场景。

2.7.2 C3TR

class C3TR(C3):
    # C3 module with TransformerBlock()
    def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):
        """Initializes C3 module with TransformerBlock for enhanced feature extraction, accepts channel sizes, shortcut
        config, group, and expansion.
        """
        super().__init__(c1, c2, n, shortcut, g, e)
        c_ = int(c2 * e)
        self.m = TransformerBlock(c_, c_, 4, n)
  • 继承自C3
  • 主要功能: 使用 TransformerBlock 替换 C3 模块中的 Bottleneck 模块。
  • TransformerBlock 模块: 通过自注意力机制(self-attention)捕捉全局特征,适合处理长距离依赖关系的任务。
  • 用途: 适用于需要全局特征建模的任务,如图像分类中的注意力机制。

2.7.3 C3SPP

class C3SPP(C3):
    # C3 module with SPP()
    def __init__(self, c1, c2, k=(5, 9, 13), n=1, shortcut=True, g=1, e=0.5):
        """Initializes a C3 module with SPP layer for advanced spatial feature extraction, given channel sizes, kernel
        sizes, shortcut, group, and expansion ratio.
        """
        super().__init__(c1, c2, n, shortcut, g, e)
        c_ = int(c2 * e)
        self.m = SPP(c_, c_, k)
  • 继承自C3
  • 主要功能: 使用空间金字塔池化(SPP, Spatial Pyramid Pooling)替换 C3 模块中的 Bottleneck 模块。
  • SPP 模块: 通过多尺度池化操作来提取空间特征,从而增强模型对不同尺度特征的捕捉能力。
  • 用途: 适用于目标检测和分割等需要处理多尺度特征的任务。

2.7.4 C3Ghost

class C3Ghost(C3):
    # C3 module with GhostBottleneck()
    def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):
        """Initializes YOLOv5's C3 module with Ghost Bottlenecks for efficient feature extraction."""
        super().__init__(c1, c2, n, shortcut, g, e)
        c_ = int(c2 * e)  # hidden channels
        self.m = nn.Sequential(*(GhostBottleneck(c_, c_) for _ in range(n)))
  • 继承自C3
  • 主要功能: 使用 GhostBottleneck 替换 C3 模块中的 Bottleneck 模块。
  • GhostBottleneck 模块: 通过 Ghost 模块减少冗余计算,从而提高计算效率,尤其在轻量级模型中表现突出。
  • 用途: 适用于需要在低计算资源下保持高效特征提取的任务,如移动端设备上的计算机视觉任务。

2.8 SPP

class SPP(nn.Module):
    # Spatial Pyramid Pooling (SPP) layer https://arxiv.org/abs/1406.4729
    def __init__(self, c1, c2, k=(5, 9, 13)):
        """Initializes SPP layer with Spatial Pyramid Pooling, ref: https://arxiv.org/abs/1406.4729, args: c1 (input channels), c2 (output channels), k (kernel sizes)."""
        super().__init__()
        c_ = c1 // 2  # hidden channels
        self.cv1 = Conv(c1, c_, 1, 1)
        self.cv2 = Conv(c_ * (len(k) + 1), c2, 1, 1)
        self.m = nn.ModuleList([nn.MaxPool2d(kernel_size=x, stride=1, padding=x // 2) for x in k])

    def forward(self, x):
        """Applies convolution and max pooling layers to the input tensor `x`, concatenates results, and returns output
        tensor.
        """
        x = self.cv1(x)
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")  # suppress torch 1.9.0 max_pool2d() warning
            return self.cv2(torch.cat([x] + [m(x) for m in self.m], 1))

  SPP是在Feature Map经过不同尺度的池化后,将特征图们重新cat起来传到下一层侦测网络中,其网络结构如下图所示。这样SPP就融合了局部与全局特征,同时还兼顾了避免裁剪图像的作用。

2.8.2 SPPF

class SPPF(nn.Module):
    # Spatial Pyramid Pooling - Fast (SPPF) layer for YOLOv5 by Glenn Jocher
    def __init__(self, c1, c2, k=5):
        """
        Initializes YOLOv5 SPPF layer with given channels and kernel size for YOLOv5 model, combining convolution and
        max pooling.

        Equivalent to SPP(k=(5, 9, 13)).
        """
        super().__init__()
        c_ = c1 // 2  # hidden channels
        self.cv1 = Conv(c1, c_, 1, 1)
        self.cv2 = Conv(c_ * 4, c2, 1, 1)
        self.m = nn.MaxPool2d(kernel_size=k, stride=1, padding=k // 2)

    def forward(self, x):
        """Processes input through a series of convolutions and max pooling operations for feature extraction."""
        x = self.cv1(x)
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")  # suppress torch 1.9.0 max_pool2d() warning
            y1 = self.m(x)
            y2 = self.m(y1)
            return self.cv2(torch.cat((x, y1, y2, self.m(y2)), 1))
  • x = self.cv1(x):

    • 输入 x 先通过 cv1 进行 1x1 卷积,得到降维后的特征图。
  • y1 = self.m(x):

    • 对降维后的特征图进行一次池化操作,得到 y1
  • y2 = self.m(y1):

    • 对 y1 再进行一次池化操作,得到 y2
  • torch.cat((x, y1, y2, self.m(y2)), 1):

    • 将原始降维后的特征图 x,第一次池化的结果 y1,第二次池化的结果 y2,以及第三次池化的结果(self.m(y2))在通道维度上拼接。
  • self.cv2(...):

    • 最后,将拼接后的特征图通过 cv2 进行 1x1 卷积,得到最终输出。

2.9 Focus

class Focus(nn.Module):
    # Focus wh information into c-space
    def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True):
        """Initializes Focus module to concentrate width-height info into channel space with configurable convolution
        parameters.
        """
        super().__init__()
        self.conv = Conv(c1 * 4, c2, k, s, p, g, act=act)
        # self.contract = Contract(gain=2)

    def forward(self, x):
        """Processes input through Focus mechanism, reshaping (b,c,w,h) to (b,4c,w/2,h/2) then applies convolution."""
        return self.conv(torch.cat((x[..., ::2, ::2], x[..., 1::2, ::2], x[..., ::2, 1::2], x[..., 1::2, 1::2]), 1))
        # return self.conv(self.contract(x))

1. 初始化方法 (__init__):
  • 该类继承自 torch.nn.Module,因此它是一个标准的 PyTorch 模块。
  • 初始化方法接受以下参数:
    • c1: 输入通道数。
    • c2: 输出通道数。
    • k: 卷积核的大小(默认值为 1)。
    • s: 卷积步幅(默认值为 1)。
    • p: 卷积的填充(默认值为 None)。
    • g: 卷积的分组数(默认值为 1,不使用分组)。
    • act: 是否使用激活函数(默认值为 True)。
  • self.conv 是一个卷积层,由一个单独的 Conv 模块执行卷积操作。它的输入通道数为 c1 * 4(因为输入张量会进行重新排列,通道数增加4倍),输出通道数为 c2

注意: 注释掉的代码 # self.contract = Contract(gain=2) 暗示了一种替代的下采样方式,使用 Contract 操作来压缩输入,但在当前版本中没有使用。

2. 前向传播方法 (forward):
  • 输入 x 假设形状为 (batch_size, channels, width, height)
  • 使用 torch.cat 对输入张量进行切片,将输入张量分成 4 部分,分别选取宽高方向上每隔一个像素的元素:
    • x[..., ::2, ::2]: 选取从 (0,0) 开始的每隔一个像素。
    • x[..., 1::2, ::2]: 选取从 (1,0) 开始的每隔一个像素。
    • x[..., ::2, 1::2]: 选取从 (0,1) 开始的每隔一个像素。
    • x[..., 1::2, 1::2]: 选取从 (1,1) 开始的每隔一个像素。
  • 这 4 个张量在通道维度上进行拼接,使得输入的形状从 (b, c, w, h) 变为 (b, 4c, w/2, h/2),也就是将空间信息压缩到通道维度中。
  • 拼接后的张量传入卷积层 self.conv 进行进一步处理。

关键概念:Focus 层

Focus 层本质上执行了一种下采样操作,将输入的空间分辨率(宽度和高度减半)压缩,同时将通道数增加 4 倍。这种操作保留了空间信息,但将其压缩到了通道维度。这在目标检测等任务中非常有用,因为它可以在不同的分辨率下保留特征信息。

2.10 Contract

class Contract(nn.Module):
    # Contract width-height into channels, i.e. x(1,64,80,80) to x(1,256,40,40)
    def __init__(self, gain=2):
        """Initializes a layer to contract spatial dimensions (width-height) into channels, e.g., input shape
        (1,64,80,80) to (1,256,40,40).
        """
        super().__init__()
        self.gain = gain

    def forward(self, x):
        """Processes input tensor to expand channel dimensions by contracting spatial dimensions, yielding output shape
        `(b, c*s*s, h//s, w//s)`.
        """
        b, c, h, w = x.size()  # assert (h / s == 0) and (W / s == 0), 'Indivisible gain'
        s = self.gain
        x = x.view(b, c, h // s, s, w // s, s)  # x(1,64,40,2,40,2)
        x = x.permute(0, 3, 5, 1, 2, 4).contiguous()  # x(1,2,2,64,40,40)
        return x.view(b, c * s * s, h // s, w // s)  # x(1,256,40,40)

这个 Contract 类是一个自定义的 PyTorch 模块,用于通过减少输入张量的空间维度(宽度和高度),将这些信息“收缩”到通道维度中,从而增加通道数。这种操作与下采样类似,但其目标是将空间信息编码到更多的通道中,而不是简单地减少分辨率。

功能说明:

  • 目标: 将输入张量的空间维度(宽度和高度)缩小,同时增加通道数。比如输入的形状从 (1, 64, 80, 80) 转变为 (1, 256, 40, 40),其中 64 * 4 = 256 是通过减少空间维度得到的通道扩展。
  • 实现: 利用 view() 和 permute() 方法重新排列张量。

详细解析:

1. 初始化方法 (__init__):
  • gain: 控制收缩的尺度,即宽度和高度将被缩小 gain 倍,而通道数将被增加 gain^2 倍。默认为 2,表示每次操作将宽度和高度缩小 2 倍,同时通道数增加 4 倍(2 * 2)。
2. 前向传播方法 (forward):
  • 输入张量 x 的形状为 (b, c, h, w),其中:

    • b: 批次大小。
    • c: 输入通道数。
    • h: 高度。
    • w: 宽度。

    函数执行以下步骤:

    1. 重塑张量 (view): 将输入张量按照 gain 的大小重新排列。具体来说,将空间维度 hw 分割成两部分,使得它们可以按照 gain 的大小进行分块:

      x = x.view(b, c, h // s, s, w // s, s)

      此时,张量的形状变为 (b, c, h//s, s, w//s, s),即将宽度和高度分别拆分为 h//sw//s 两个部分,同时额外引入 ss 的维度。

    2. 维度交换 (permute): 使用 permute() 函数将新引入的维度 ss 移动到通道维度的位置。这一步操作将张量的形状变为 (b, s, s, c, h//s, w//s),将空间信息“编码”到新的通道中。

      x = x.permute(0, 3, 5, 1, 2, 4).contiguous()

    3. 再次重塑张量 (view): 最后,通过 view() 将所有维度压平到通道维度,将张量形状变为 (b, c * s * s, h//s, w//s)。这意味着原始的通道数 c 变为 c * s * s,宽度和高度都缩小 s 倍。

      return x.view(b, c * s * s, h // s, w // s)

工作原理示例:

假设输入张量的形状是 (1, 64, 80, 80),且 gain = 2

  1. 首先,view() 将张量转换为形状 (1, 64, 40, 2, 40, 2)
  2. permute() 将其转换为形状 (1, 2, 2, 64, 40, 40),即将 2x2 的块移到了通道维度上。
  3. 最后,通过 view(),张量的形状变为 (1, 256, 40, 40),通道数增加到了 64 * 2 * 2 = 256,而宽度和高度都减少为 40

总结:

  • Contract 层通过减少空间分辨率并增加通道数来将空间信息编码到通道维度中。
  • 它适用于某些任务中,尤其是需要在较低分辨率下保留更多特征信息的场景,例如目标检测或图像分割。

2.11 Expand

class Expand(nn.Module):
    # Expand channels into width-height, i.e. x(1,64,80,80) to x(1,16,160,160)
    def __init__(self, gain=2):
        """
        Initializes the Expand module to increase spatial dimensions by redistributing channels, with an optional gain
        factor.

        Example: x(1,64,80,80) to x(1,16,160,160).
        """
        super().__init__()
        self.gain = gain

    def forward(self, x):
        """Processes input tensor x to expand spatial dimensions by redistributing channels, requiring C / gain^2 ==
        0.
        """
        b, c, h, w = x.size()  # assert C / s ** 2 == 0, 'Indivisible gain'
        s = self.gain
        x = x.view(b, s, s, c // s**2, h, w)  # x(1,2,2,16,80,80)
        x = x.permute(0, 3, 4, 1, 5, 2).contiguous()  # x(1,16,80,2,80,2)
        return x.view(b, c // s**2, h * s, w * s)  # x(1,16,160,160)

这个 Expand 类是 Contract 类的逆操作,目的是通过减少通道数并扩展空间维度(宽度和高度),从而将特征图像“展开”,也就是增加分辨率。这种操作适用于需要在保留通道数信息的前提下,还原或增加空间分辨率的场景。

功能说明:

  • 目标: 通过减少通道维度,将其重新分配到空间维度(宽度和高度),从而增加空间分辨率。例如,输入形状从 (1, 64, 80, 80) 转变为 (1, 16, 160, 160),其中通道数从 64 变为 16,宽度和高度从 80 增加到 160
  • 实现: 通过 view() 和 permute() 操作重新排列张量。

详细解析:

1. 初始化方法 (__init__):
  • gain: 控制展开的尺度,也就是空间维度扩大的倍数,默认值为 2。例如,当 gain=2 时,宽度和高度将增加 2 倍,通道数将减少 gain^2 倍。
2. 前向传播方法 (forward):
  • 输入张量 x 的形状为 (b, c, h, w),其中:
    • b: 批次大小。
    • c: 输入通道数。
    • h: 高度。
    • w: 宽度。

该方法执行以下步骤:

  1. 重塑张量 (view): 首先,将通道维度分割成多个部分,以便将其重新分配给宽度和高度。具体地:

    x = x.view(b, s, s, c // s**2, h, w)

    这一步操作将张量形状转换为 (b, s, s, c // s**2, h, w)。这里,sgain,即扩展倍数。举例来说,如果 gain=2,那么原始的通道 c 被分割成 s * s 块,每块的通道数变为 c // s^2

  2. 维度交换 (permute): 使用 permute() 将新引入的 s 维度移动到空间维度上,这样可以将原来的通道信息重新映射到空间维度上。即:

    x = x.permute(0, 3, 4, 1, 5, 2).contiguous()

    在这一步,张量的形状变为 (b, c // s^2, h, s, w, s),其中 s 已被分配给空间维度。

  3. 再次重塑张量 (view): 最后,将分配后的空间维度展开,转换为新的空间维度。最终输出形状为 (b, c // s^2, h * s, w * s),即通道数变为 c // s^2,宽度和高度扩展为 h * sw * s

    return x.view(b, c // s**2, h * s, w * s)

工作原理示例:

假设输入张量形状为 (1, 64, 80, 80),且 gain = 2

  1. view() 操作将其转换为 (1, 2, 2, 16, 80, 80),即将通道维度拆分成 4 个部分(2 * 2),每个部分有 16 个通道。
  2. permute() 操作将形状转换为 (1, 16, 80, 2, 80, 2),即将新引入的 2x2 块分配给空间维度。
  3. 最后,通过 view() 操作,将形状转换为 (1, 16, 160, 160),其中宽度和高度都扩展为 160,通道数减少到 16。

总结:

  • Expand 模块通过减少通道维度并增加空间维度,将图像的分辨率扩大。这与 Contract 模块相反,Contract 是通过减少空间维度并增加通道数来缩小图像。
  • 这种操作通常用于需要在某些阶段恢复或提升空间分辨率的场景,如图像生成或超分辨率任务。

2.12 Concat

class Concat(nn.Module):
    # Concatenate a list of tensors along dimension
    def __init__(self, dimension=1):
        """Initializes a Concat module to concatenate tensors along a specified dimension."""
        super().__init__()
        self.d = dimension

    def forward(self, x):
        """Concatenates a list of tensors along a specified dimension; `x` is a list of tensors, `dimension` is an
        int.
        """
        return torch.cat(x, self.d)

这个 Concat 类是一个自定义的 PyTorch 模块,用于沿着指定的维度对多个张量进行拼接操作。类似于 PyTorch 的 torch.cat() 函数,这个类主要用于将输入的张量列表沿特定的维度进行连接。通常用于将多个特征图合并在一起,比如在神经网络结构中,通过拼接不同层输出的特征图,进行信息融合。

功能说明:

  • 目标: 沿指定维度对输入的张量列表进行拼接操作。例如,将来自不同卷积层的特征图在通道维度上拼接。
  • 实现: 使用 PyTorch 的 torch.cat() 函数来实现张量拼接。

详细解析:

1. 初始化方法 (__init__):
  • dimension: 指定沿哪个维度进行拼接,默认值为 1。通常情况下,dimension=1 表示在通道维度上进行拼接,这是卷积神经网络中特征融合的常见操作。
    • 例子: 如果输入的特征图的形状为 (batch_size, channels, height, width),那么 dimension=1 就是沿着 channels 维度进行拼接。
2. 前向传播方法 (forward):
  • 输入 x 是一个张量列表,假设列表中的每个张量形状相同(除了要拼接的维度)。
  • 使用 torch.cat(x, self.d) 来进行拼接,其中 self.d 是指定的拼接维度。
    return torch.cat(x, self.d)

使用示例:

假设输入的两个张量分别为 (batch_size, 64, 80, 80)(batch_size, 64, 80, 80),那么:

  • 如果 dimension=1,拼接后输出的张量形状将变为 (batch_size, 128, 80, 80),即在通道维度上拼接。
  • 如果 dimension=2,拼接后输出的张量形状将变为 (batch_size, 64, 160, 80),即在高度维度上拼接。

应用场景:

  • 这种拼接操作通常用于将神经网络中不同层的特征图进行融合。例如,在目标检测任务(如 YOLO 系列模型)中,可能需要将来自不同分辨率层的特征图进行拼接,以便更好地捕获多尺度信息。
  • 这种拼接方式也常用于残差连接或特征图融合等场景。

三、模型扩展模块

3.1 AutoShape

class AutoShape(nn.Module):
    # YOLOv5 input-robust model wrapper for passing cv2/np/PIL/torch inputs. Includes preprocessing, inference and NMS
    conf = 0.25  # NMS confidence threshold
    iou = 0.45  # NMS IoU threshold
    agnostic = False  # NMS class-agnostic
    multi_label = False  # NMS multiple labels per box
    classes = None  # (optional list) filter by class, i.e. = [0, 15, 16] for COCO persons, cats and dogs
    max_det = 1000  # maximum number of detections per image
    amp = False  # Automatic Mixed Precision (AMP) inference

    def __init__(self, model, verbose=True):
        """Initializes YOLOv5 model for inference, setting up attributes and preparing model for evaluation."""
        super().__init__()
        if verbose:
            LOGGER.info("Adding AutoShape... ")
        copy_attr(self, model, include=("yaml", "nc", "hyp", "names", "stride", "abc"), exclude=())  # copy attributes
        self.dmb = isinstance(model, DetectMultiBackend)  # DetectMultiBackend() instance
        self.pt = not self.dmb or model.pt  # PyTorch model
        self.model = model.eval()
        if self.pt:
            m = self.model.model.model[-1] if self.dmb else self.model.model[-1]  # Detect()
            m.inplace = False  # Detect.inplace=False for safe multithread inference
            m.export = True  # do not output loss values

    def _apply(self, fn):
        """
        Applies to(), cpu(), cuda(), half() etc.

        to model tensors excluding parameters or registered buffers.
        """
        self = super()._apply(fn)
        if self.pt:
            m = self.model.model.model[-1] if self.dmb else self.model.model[-1]  # Detect()
            m.stride = fn(m.stride)
            m.grid = list(map(fn, m.grid))
            if isinstance(m.anchor_grid, list):
                m.anchor_grid = list(map(fn, m.anchor_grid))
        return self

    @smart_inference_mode()
    def forward(self, ims, size=640, augment=False, profile=False):
        """
        Performs inference on inputs with optional augment & profiling.

        Supports various formats including file, URI, OpenCV, PIL, numpy, torch.
        """
        # For size(height=640, width=1280), RGB images example inputs are:
        #   file:        ims = 'data/images/zidane.jpg'  # str or PosixPath
        #   URI:             = 'https://ultralytics.com/images/zidane.jpg'
        #   OpenCV:          = cv2.imread('image.jpg')[:,:,::-1]  # HWC BGR to RGB x(640,1280,3)
        #   PIL:             = Image.open('image.jpg') or ImageGrab.grab()  # HWC x(640,1280,3)
        #   numpy:           = np.zeros((640,1280,3))  # HWC
        #   torch:           = torch.zeros(16,3,320,640)  # BCHW (scaled to size=640, 0-1 values)
        #   multiple:        = [Image.open('image1.jpg'), Image.open('image2.jpg'), ...]  # list of images

        dt = (Profile(), Profile(), Profile())
        with dt[0]:
            if isinstance(size, int):  # expand
                size = (size, size)
            p = next(self.model.parameters()) if self.pt else torch.empty(1, device=self.model.device)  # param
            autocast = self.amp and (p.device.type != "cpu")  # Automatic Mixed Precision (AMP) inference
            if isinstance(ims, torch.Tensor):  # torch
                with amp.autocast(autocast):
                    return self.model(ims.to(p.device).type_as(p), augment=augment)  # inference

            # Pre-process
            n, ims = (len(ims), list(ims)) if isinstance(ims, (list, tuple)) else (1, [ims])  # number, list of images
            shape0, shape1, files = [], [], []  # image and inference shapes, filenames
            for i, im in enumerate(ims):
                f = f"image{i}"  # filename
                if isinstance(im, (str, Path)):  # filename or uri
                    im, f = Image.open(requests.get(im, stream=True).raw if str(im).startswith("http") else im), im
                    im = np.asarray(exif_transpose(im))
                elif isinstance(im, Image.Image):  # PIL Image
                    im, f = np.asarray(exif_transpose(im)), getattr(im, "filename", f) or f
                files.append(Path(f).with_suffix(".jpg").name)
                if im.shape[0] < 5:  # image in CHW
                    im = im.transpose((1, 2, 0))  # reverse dataloader .transpose(2, 0, 1)
                im = im[..., :3] if im.ndim == 3 else cv2.cvtColor(im, cv2.COLOR_GRAY2BGR)  # enforce 3ch input
                s = im.shape[:2]  # HWC
                shape0.append(s)  # image shape
                g = max(size) / max(s)  # gain
                shape1.append([int(y * g) for y in s])
                ims[i] = im if im.data.contiguous else np.ascontiguousarray(im)  # update
            shape1 = [make_divisible(x, self.stride) for x in np.array(shape1).max(0)]  # inf shape
            x = [letterbox(im, shape1, auto=False)[0] for im in ims]  # pad
            x = np.ascontiguousarray(np.array(x).transpose((0, 3, 1, 2)))  # stack and BHWC to BCHW
            x = torch.from_numpy(x).to(p.device).type_as(p) / 255  # uint8 to fp16/32

        with amp.autocast(autocast):
            # Inference
            with dt[1]:
                y = self.model(x, augment=augment)  # forward

            # Post-process
            with dt[2]:
                y = non_max_suppression(
                    y if self.dmb else y[0],
                    self.conf,
                    self.iou,
                    self.classes,
                    self.agnostic,
                    self.multi_label,
                    max_det=self.max_det,
                )  # NMS
                for i in range(n):
                    scale_boxes(shape1, y[i][:, :4], shape0[i])

            return Detections(ims, y, files, dt, self.names, x.shape)

这个 AutoShape 类是一个用于 YOLOv5 模型的包装器类,主要用于处理多种格式的输入(如 cv2numpyPILtorch),进行预处理、推理和非极大值抑制(NMS)。它封装了 YOLOv5 的推理逻辑,并简化了输入数据的处理,使得 YOLOv5 模型能够处理各种类型的图像输入。

功能说明:

  • 目标: 该类的核心功能是自动适配不同格式的输入数据,并对其进行推理。它支持常见的图像格式,如文件路径、OpenCV 图像、PIL 图像、Numpy 数组和 PyTorch 张量。同时,该类还会自动执行推理前的预处理操作,并在推理后进行非极大值抑制(NMS)来过滤冗余框。
  • 结构: 主要分为初始化、推理、预处理、推理后的后处理等几个关键步骤。

详细解析:

1. 类属性:

这些属性是 NMS(非极大值抑制)的参数控制:

  • conf: NMS 的置信度阈值,默认值为 0.25。
  • iou: NMS 的 IoU 阈值,默认值为 0.45。
  • agnostic: 是否进行类别无关的 NMS。
  • multi_label: 是否允许每个框有多个标签。
  • classes: 可选的类别列表,用于过滤目标类别。
  • max_det: 每张图像最多保留的目标数。
  • amp: 是否使用自动混合精度(AMP)推理,以提高推理效率。
2. 初始化方法 (__init__):
  • model: 这是 YOLOv5 模型。
  • verbose: 是否打印日志信息,默认为 True

初始化方法会:

  1. 通过 copy_attr 方法将 model 中的属性复制到 AutoShape 中。
  2. 检查模型是否是 DetectMultiBackend 实例,并设置 PyTorch 模型相关属性。
  3. 将模型设置为评估模式(eval()),并禁用 Detect() 层的 inplace 操作以确保安全的多线程推理。
3. _apply 方法:

这是 PyTorch 自带的应用方法,用于将某个函数 fn 应用到模型的所有参数或属性上(如 to()cpu()cuda()half() 等操作)。_apply 方法会确保这些函数也应用到检测层的相关属性(如 stride 和 anchor grid)上。

4. forward 方法:

这是核心推理逻辑,执行 YOLOv5 推理并进行后处理。支持多种输入格式,并包含了数据预处理、推理和 NMS 后处理的逻辑。

  • 输入格式支持: 该方法支持文件路径、URI、OpenCV 图像、PIL 图像、Numpy 数组和 PyTorch 张量等格式。

    • 文件路径和 URI 会先通过 PIL 打开。
    • OpenCV 图像会先从 BGR 转换为 RGB。
    • PIL 图像则通过 exif_transpose 函数调整方向。
  • 预处理:

    1. 首先对输入图像进行尺寸调整,使其适应 YOLOv5 模型的输入尺寸。
    2. 然后通过 letterbox 函数对图像进行缩放和填充,确保输入图像的长宽符合 YOLOv5 的要求。
    3. 最后将图像从 BHWC 格式转换为 BCHW 格式,并归一化到 [0, 1] 范围。
  • 推理:

    • 如果输入是 PyTorch 张量,则直接调用模型进行推理。
    • 如果输入是其他格式,首先将其转换为 PyTorch 张量,然后执行推理。
  • 后处理:

    • 调用 non_max_suppression 函数执行 NMS,过滤掉多余的框。
    • 使用 scale_boxes 函数将预测框从模型输入的缩放尺寸恢复到原始输入图像的尺寸。
  • Profiling: 使用了 Profile() 来记录预处理、推理和后处理的耗时。

工作流程:

  1. 预处理:将输入图像转换为模型可以处理的格式,包括调整尺寸、归一化等。
  2. 推理:在模型上执行前向传播,得到预测结果。
  3. 后处理:应用非极大值抑制(NMS),过滤掉重叠的框,并将预测框的尺寸映射回原始图像。

3.2 Detections

class Detections:
    # YOLOv5 detections class for inference results
    def __init__(self, ims, pred, files, times=(0, 0, 0), names=None, shape=None):
        """Initializes the YOLOv5 Detections class with image info, predictions, filenames, timing and normalization."""
        super().__init__()
        d = pred[0].device  # device
        gn = [torch.tensor([*(im.shape[i] for i in [1, 0, 1, 0]), 1, 1], device=d) for im in ims]  # normalizations
        self.ims = ims  # list of images as numpy arrays
        self.pred = pred  # list of tensors pred[0] = (xyxy, conf, cls)
        self.names = names  # class names
        self.files = files  # image filenames
        self.times = times  # profiling times
        self.xyxy = pred  # xyxy pixels
        self.xywh = [xyxy2xywh(x) for x in pred]  # xywh pixels
        self.xyxyn = [x / g for x, g in zip(self.xyxy, gn)]  # xyxy normalized
        self.xywhn = [x / g for x, g in zip(self.xywh, gn)]  # xywh normalized
        self.n = len(self.pred)  # number of images (batch size)
        self.t = tuple(x.t / self.n * 1e3 for x in times)  # timestamps (ms)
        self.s = tuple(shape)  # inference BCHW shape

    def _run(self, pprint=False, show=False, save=False, crop=False, render=False, labels=True, save_dir=Path("")):
        """Executes model predictions, displaying and/or saving outputs with optional crops and labels."""
        s, crops = "", []
        for i, (im, pred) in enumerate(zip(self.ims, self.pred)):
            s += f"\nimage {i + 1}/{len(self.pred)}: {im.shape[0]}x{im.shape[1]} "  # string
            if pred.shape[0]:
                for c in pred[:, -1].unique():
                    n = (pred[:, -1] == c).sum()  # detections per class
                    s += f"{n} {self.names[int(c)]}{'s' * (n > 1)}, "  # add to string
                s = s.rstrip(", ")
                if show or save or render or crop:
                    annotator = Annotator(im, example=str(self.names))
                    for *box, conf, cls in reversed(pred):  # xyxy, confidence, class
                        label = f"{self.names[int(cls)]} {conf:.2f}"
                        if crop:
                            file = save_dir / "crops" / self.names[int(cls)] / self.files[i] if save else None
                            crops.append(
                                {
                                    "box": box,
                                    "conf": conf,
                                    "cls": cls,
                                    "label": label,
                                    "im": save_one_box(box, im, file=file, save=save),
                                }
                            )
                        else:  # all others
                            annotator.box_label(box, label if labels else "", color=colors(cls))
                    im = annotator.im
            else:
                s += "(no detections)"

            im = Image.fromarray(im.astype(np.uint8)) if isinstance(im, np.ndarray) else im  # from np
            if show:
                if is_jupyter():
                    from IPython.display import display

                    display(im)
                else:
                    im.show(self.files[i])
            if save:
                f = self.files[i]
                im.save(save_dir / f)  # save
                if i == self.n - 1:
                    LOGGER.info(f"Saved {self.n} image{'s' * (self.n > 1)} to {colorstr('bold', save_dir)}")
            if render:
                self.ims[i] = np.asarray(im)
        if pprint:
            s = s.lstrip("\n")
            return f"{s}\nSpeed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {self.s}" % self.t
        if crop:
            if save:
                LOGGER.info(f"Saved results to {save_dir}\n")
            return crops

    @TryExcept("Showing images is not supported in this environment")
    def show(self, labels=True):
        """
        Displays detection results with optional labels.

        Usage: show(labels=True)
        """
        self._run(show=True, labels=labels)  # show results

    def save(self, labels=True, save_dir="runs/detect/exp", exist_ok=False):
        """
        Saves detection results with optional labels to a specified directory.

        Usage: save(labels=True, save_dir='runs/detect/exp', exist_ok=False)
        """
        save_dir = increment_path(save_dir, exist_ok, mkdir=True)  # increment save_dir
        self._run(save=True, labels=labels, save_dir=save_dir)  # save results

    def crop(self, save=True, save_dir="runs/detect/exp", exist_ok=False):
        """
        Crops detection results, optionally saves them to a directory.

        Args: save (bool), save_dir (str), exist_ok (bool).
        """
        save_dir = increment_path(save_dir, exist_ok, mkdir=True) if save else None
        return self._run(crop=True, save=save, save_dir=save_dir)  # crop results

    def render(self, labels=True):
        """Renders detection results with optional labels on images; args: labels (bool) indicating label inclusion."""
        self._run(render=True, labels=labels)  # render results
        return self.ims

    def pandas(self):
        """
        Returns detections as pandas DataFrames for various box formats (xyxy, xyxyn, xywh, xywhn).

        Example: print(results.pandas().xyxy[0]).
        """
        new = copy(self)  # return copy
        ca = "xmin", "ymin", "xmax", "ymax", "confidence", "class", "name"  # xyxy columns
        cb = "xcenter", "ycenter", "width", "height", "confidence", "class", "name"  # xywh columns
        for k, c in zip(["xyxy", "xyxyn", "xywh", "xywhn"], [ca, ca, cb, cb]):
            a = [[x[:5] + [int(x[5]), self.names[int(x[5])]] for x in x.tolist()] for x in getattr(self, k)]  # update
            setattr(new, k, [pd.DataFrame(x, columns=c) for x in a])
        return new

    def tolist(self):
        """
        Converts a Detections object into a list of individual detection results for iteration.

        Example: for result in results.tolist():
        """
        r = range(self.n)  # iterable
        return [
            Detections(
                [self.ims[i]],
                [self.pred[i]],
                [self.files[i]],
                self.times,
                self.names,
                self.s,
            )
            for i in r
        ]

    def print(self):
        """Logs the string representation of the current object's state via the LOGGER."""
        LOGGER.info(self.__str__())

    def __len__(self):
        """Returns the number of results stored, overrides the default len(results)."""
        return self.n

    def __str__(self):
        """Returns a string representation of the model's results, suitable for printing, overrides default
        print(results).
        """
        return self._run(pprint=True)  # print results

    def __repr__(self):
        """Returns a string representation of the YOLOv5 object, including its class and formatted results."""
        return f"YOLOv5 {self.__class__} instance\n" + self.__str__()

这个 Detections 类是专门为 YOLOv5 推理结果设计的,它封装了推理过程中的输出结果,并提供了一系列方法来处理、展示、保存和转换检测结果。它包括了检测框的处理、结果展示和保存等功能,可以很方便地与 YOLOv5 的输出集成。

功能说明:

  • 目标: 封装 YOLOv5 的推理结果,并提供多种方式对结果进行操作,如展示检测结果、保存检测结果、裁剪检测框、渲染标签等。同时还提供了数据转换方法,例如将检测结果转化为 pandas DataFrame 格式。
  • 输出格式: YOLOv5 的检测结果通常包括边界框(xyxy 或 xywh 格式)、置信度和类别。

详细解析:

1. 初始化方法 (__init__):

该方法初始化了类的多个属性,负责将推理结果存储起来:

  • ims: 输入的图像,通常为一个 numpy 数组的列表。
  • pred: 推理的结果,通常是一个包含多个张量的列表,每个张量包含 xyxy 边界框、置信度和类别。
  • files: 图像文件名。
  • times: 推理的时间信息,用于显示推理速度。
  • names: 类别名称。
  • shape: 推理的图像尺寸(BCHW 格式)。

该方法还会将推理结果转换为不同格式:

  • xyxy: 使用像素表示的 xyxy 边界框。
  • xywh: 使用像素表示的 xywh 边界框。
  • xyxyn: 使用归一化表示的 xyxy 边界框。
  • xywhn: 使用归一化表示的 xywh 边界框。
2. _run 方法:

这是一个内部方法,用于执行多种操作:显示、保存、裁剪和渲染检测结果。该方法提供了对每个检测结果的灵活处理,包括:

  • 显示图像及检测结果(show)。
  • 保存图像及检测结果(save)。
  • 将检测框裁剪出来,并可选地保存裁剪结果(crop)。
  • 渲染带有标签的检测结果(render)。

如果传入 pprint=True,则返回处理结果的字符串形式(通常用于打印输出),如检测到的类别及其数量等。

3. show 方法:

这个方法用于在屏幕上显示检测结果,使用图像库来展示图像,支持添加标签。

self._run(show=True, labels=labels)
4. save 方法:

该方法用于将检测结果保存到指定的文件夹。每张图像会保存为 .jpg 文件,并包含检测框和标签的可视化结果。

self._run(save=True, labels=labels, save_dir=save_dir)
5. crop 方法:

该方法用于裁剪检测框并保存裁剪的结果。裁剪的结果包含边界框信息、置信度、类别和裁剪后的图像。

self._run(crop=True, save=save, save_dir=save_dir)
6. render 方法:

这个方法用于渲染检测结果,渲染后的图像可以用于进一步处理或保存。

self._run(render=True, labels=labels)
7. pandas 方法:

将检测结果转化为 pandas DataFrame,便于进行数据分析或操作。返回的 DataFrame 包含边界框信息、置信度和类别名称。

new = copy(self)
8. tolist 方法:

将检测结果转化为一个列表,每个列表项是一个 Detections 实例。便于逐个处理每个图像的检测结果。

return [Detections([self.ims[i]], [self.pred[i]], [self.files[i]], self.times, self.names, self.s) for i in range(self.n)]
9. __len____str__ 方法:
  • __len__: 返回检测到的图像数量。
  • __str__: 返回检测结果的字符串表示,便于输出推理结果。

总结:

Detections 类封装了 YOLOv5 的推理结果,提供了从数据处理、结果展示、裁剪、保存到渲染等一系列功能。它为使用者提供了灵活的接口,便于展示或保存推理结果,并支持将检测结果转换为更易于分析的格式(如 pandas DataFrame)。

3.3 Classify

class Classify(nn.Module):
    # YOLOv5 classification head, i.e. x(b,c1,20,20) to x(b,c2)
    def __init__(
        self, c1, c2, k=1, s=1, p=None, g=1, dropout_p=0.0
    ):  # ch_in, ch_out, kernel, stride, padding, groups, dropout probability
        """Initializes YOLOv5 classification head with convolution, pooling, and dropout layers for input to output
        channel transformation.
        """
        super().__init__()
        c_ = 1280  # efficientnet_b0 size
        self.conv = Conv(c1, c_, k, s, autopad(k, p), g)
        self.pool = nn.AdaptiveAvgPool2d(1)  # to x(b,c_,1,1)
        self.drop = nn.Dropout(p=dropout_p, inplace=True)
        self.linear = nn.Linear(c_, c2)  # to x(b,c2)

    def forward(self, x):
        """Processes input through conv, pool, drop, and linear layers; supports list concatenation input."""
        if isinstance(x, list):
            x = torch.cat(x, 1)
        return self.linear(self.drop(self.pool(self.conv(x)).flatten(1)))

这个 Classify 类是一个用于 YOLOv5 分类任务的头部模块。它通过卷积、池化、丢弃(dropout)和全连接层,将输入的特征图转换为分类结果。这个模块适用于从特征图输出到类别预测的流程,可以直接将 YOLOv5 的特征图用于图像分类。

功能说明:

  • 目标: 将输入的特征图经过卷积、全局池化、丢弃层以及线性层,最终输出分类结果。输入的特征图大小是 (b, c1, h, w),输出是 (b, c2),其中 c2 是分类任务中的类别数。

详细解析:

1. 初始化方法 (__init__):
  • c1: 输入通道数,通常是来自上一层特征提取的输出通道数。
  • c2: 输出通道数,通常对应于分类任务中的类别数。
  • k: 卷积核大小,默认为 1。
  • s: 卷积步幅,默认为 1。
  • p: 卷积的填充,默认为 None,会通过 autopad 自动计算。
  • g: 卷积的分组数,默认为 1。
  • dropout_p: dropout 概率,默认为 0.0,即不执行 dropout。

内部有以下几个主要组件:

  • self.conv: 一个卷积层,用于对输入的特征图进行卷积操作,将通道数从 c1 转换为 c_c_ 的默认值是 1280(基于 EfficientNet 的大小)。
  • self.pool: 一个自适应平均池化层,将特征图的空间维度 (h, w) 缩小到 (1, 1),即进行全局平均池化。
  • self.drop: 一个 dropout 层,用于在训练期间随机丢弃一部分神经元,以防止过拟合。
  • self.linear: 一个全连接层,将池化后的特征通过线性层输出到 c2 个类别。
2. 前向传播方法 (forward):

该方法定义了分类模型的前向传播过程,分为几个步骤:

  1. 卷积 (self.conv): 首先对输入特征图执行卷积操作,将输入通道数 c1 转换为中间通道数 c_
  2. 池化 (self.pool): 对卷积后的特征图进行全局平均池化,输出形状为 (b, c_, 1, 1),即将每个通道的空间维度缩小为 1x1
  3. 丢弃 (self.drop): 对池化后的特征图执行 dropout 操作,以防止过拟合。
  4. 展平 (flatten): 将输出展平为 (b, c_) 的形状,以便能够传入全连接层。
  5. 线性层 (self.linear): 通过全连接层,将中间通道数 c_ 映射到最终输出的类别数 c2,输出形状为 (b, c2)

如果输入是一个列表,首先将其在通道维度上拼接(使用 torch.cat(x, 1)),然后再传入网络进行后续处理。

工作流程示例:

假设输入的特征图大小为 (batch_size, 512, 20, 20),分类任务有 10 个类别,步骤如下:

  1. 卷积将通道数从 512 转换为 1280
  2. 全局平均池化将空间维度从 (20, 20) 变为 (1, 1),输出形状变为 (batch_size, 1280, 1, 1)
  3. Dropout 随机丢弃部分神经元(训练时生效)。
  4. 展平为 (batch_size, 1280)
  5. 全连接层将其转换为 (batch_size, 10),输出分类结果。

总结:

  • Classify 类是一个简单而高效的分类头部,用于从特征图生成类别预测。它通过卷积、全局平均池化、dropout 和全连接层将输入特征图转化为分类输出。
  • 该模块可以直接用于图像分类任务,特别是当输入特征图是来自卷积神经网络的输出时。

3.4 Proto

class Proto(nn.Module):
    # YOLOv5 mask Proto module for segmentation models
    def __init__(self, c1, c_=256, c2=32):
        """Initializes YOLOv5 Proto module for segmentation with input, proto, and mask channels configuration."""
        super().__init__()
        self.cv1 = Conv(c1, c_, k=3)
        self.upsample = nn.Upsample(scale_factor=2, mode="nearest")
        self.cv2 = Conv(c_, c_, k=3)
        self.cv3 = Conv(c_, c2)

    def forward(self, x):
        """Performs a forward pass using convolutional layers and upsampling on input tensor `x`."""
        return self.cv3(self.cv2(self.upsample(self.cv1(x))))

Proto 类是 YOLOv5 中用于实例分割模型的掩码生成模块。这个模块通过卷积和上采样操作将输入特征图生成用于分割任务的掩码原型(prototypes),并最终输出适合于分割的特征图。这在 YOLOv5 的分割模型中用于预测掩码区域。

功能说明:

  • 目标: 将输入特征图通过一系列卷积和上采样操作生成掩码原型,这些原型可以进一步用于生成分割掩码。
  • 实现: 使用三个卷积层和一次上采样操作,将输入特征图处理为掩码原型。

详细解析:

1. 初始化方法 (__init__):

该模块有三个主要参数:

  • c1: 输入通道数,即从上一层输入的特征图的通道数。
  • c_: 中间层的通道数,默认为 256。
  • c2: 输出通道数,默认为 32,通常用于生成掩码原型。

这个模块包括以下层:

  1. self.cv1: 一个 3x3 的卷积层,将输入通道数 c1 转换为中间通道数 c_(默认为 256)。
  2. self.upsample: 一个上采样层,将特征图的空间尺寸扩大一倍,使用 nearest 插值法。上采样的倍数由 scale_factor=2 指定。
  3. self.cv2: 另一个 3x3 的卷积层,保持通道数为 c_,进一步处理特征。
  4. self.cv3: 最后一个卷积层,将通道数从 c_ 转换为输出通道数 c2(默认为 32),输出的特征图将用于分割任务。
2. 前向传播方法 (forward):

该方法定义了 Proto 模块的前向传播过程,分为几个步骤:

  1. 卷积 (self.cv1): 输入特征图通过第一个卷积层,通道数从 c1 转换为 c_
  2. 上采样 (self.upsample): 将卷积后的特征图进行上采样,空间尺寸扩大一倍。
  3. 卷积 (self.cv2): 上采样后的特征图再经过第二个卷积层,保持通道数不变(c_)。
  4. 卷积 (self.cv3): 最后,通过第三个卷积层将通道数从 c_ 变为 c2,输出最终的掩码原型。

最终输出的形状为 (batch_size, c2, H*2, W*2),即输入的特征图在通道数缩减到 c2 的同时,空间尺寸扩大一倍。

工作流程示例:

假设输入特征图的形状为 (batch_size, 512, 40, 40),参数 c_=256c2=32,则工作流程如下:

  1. self.cv1: 卷积将输入的通道数从 512 转换为 256,输出形状为 (batch_size, 256, 40, 40)
  2. self.upsample: 上采样将空间尺寸从 40x40 扩大到 80x80,输出形状为 (batch_size, 256, 80, 80)
  3. self.cv2: 卷积保持通道数不变,输出形状仍为 (batch_size, 256, 80, 80)
  4. self.cv3: 卷积将通道数从 256 变为 32,最终输出形状为 (batch_size, 32, 80, 80)

总结:

  • Proto 模块主要用于 YOLOv5 的实例分割模型,通过卷积和上采样操作生成分割所需的掩码原型(prototypes)。
  • 它通过一系列的卷积操作提取掩码特征,同时通过上采样增加分辨率,最终输出的掩码原型可以用于生成具体的分割掩码。

四、幻象模块

4.1 GhostConv

class GhostConv(nn.Module):
    # Ghost Convolution https://github.com/huawei-noah/ghostnet
    def __init__(self, c1, c2, k=1, s=1, g=1, act=True):
        """Initializes GhostConv with in/out channels, kernel size, stride, groups, and activation; halves out channels
        for efficiency.
        """
        super().__init__()
        c_ = c2 // 2  # hidden channels
        self.cv1 = Conv(c1, c_, k, s, None, g, act=act)
        self.cv2 = Conv(c_, c_, 5, 1, None, c_, act=act)

    def forward(self, x):
        """Performs forward pass, concatenating outputs of two convolutions on input `x`: shape (B,C,H,W)."""
        y = self.cv1(x)
        return torch.cat((y, self.cv2(y)), 1)

GhostConv 是一种轻量化卷积模块,属于 GhostNet 的一部分,旨在通过减少冗余计算来降低模型的计算复杂度。GhostNet 模块通过生成少量的特征图,然后通过更轻量的操作生成“冗余”特征图,从而减少计算量。

从图和代码来看,GhostConv 主要使用两步来进行卷积:

  1. 主卷积操作:生成一部分主特征图。
  2. 轻量卷积操作:通过轻量的操作生成额外的冗余特征图。

具体模块解析:

1. 初始化方法 (__init__):
  • 参数

    • c1: 输入通道数。
    • c2: 输出通道数。
    • k: 卷积核大小(默认为 1)。
    • s: 卷积步幅(默认为 1)。
    • g: 分组卷积的组数(默认为 1)。
    • act: 激活函数,默认为 True,表示使用激活函数。
  • 内部结构

    1. self.cv1:第一个卷积层,执行标准卷积,将输入的 c1 个通道映射到 c2 // 2 个通道。这一步可以理解为主卷积操作,输出少量的特征图。
    2. self.cv2:第二个卷积层,执行一个 5x5 的深度卷积,使用较大卷积核处理由 cv1 生成的特征图,生成一些冗余特征图。这是 Ghost 模块的核心部分,使用便宜的操作生成更多特征。
2. 前向传播方法 (forward):
  • 输入:形状为 (B, C, H, W) 的输入张量 x

  • 步骤

    1. y = self.cv1(x):第一个卷积操作,生成一部分主特征图。
    2. self.cv2(y):在生成的主特征图 y 上使用深度卷积生成冗余特征图。
    3. torch.cat((y, self.cv2(y)), 1):将主特征图 y 和冗余特征图 self.cv2(y) 在通道维度上拼接,生成最终的输出。
  • 输出:拼接后的特征图,通道数为 c2,其中一半是主卷积生成的特征,另一半是通过轻量卷积生成的冗余特征。

设计思想:

GhostNet 模块通过减少常规卷积层的计算复杂度来提高模型的效率。相比于传统的卷积操作,Ghost 模块只计算一部分主特征图,其他冗余特征图是通过轻量级的操作生成的,这样可以减少计算量,同时保持模型的表达能力。

总结:

  • GhostConv 通过将 c2 分成两部分,使用两个卷积操作生成不同的特征。第一部分使用较少的计算资源生成主特征,第二部分通过轻量级卷积生成冗余特征。
  • 这种方法极大减少了模型的计算量(即 FLOPs),但仍然保持了足够的模型表现力。GhostConv 可以在一些不重要的位置替代标准卷积(如 Conv),从而减少模型的计算开销。

4.2 GhostBottleneck

class GhostBottleneck(nn.Module):
    # Ghost Bottleneck https://github.com/huawei-noah/ghostnet
    def __init__(self, c1, c2, k=3, s=1):
        """Initializes GhostBottleneck with ch_in `c1`, ch_out `c2`, kernel size `k`, stride `s`; see https://github.com/huawei-noah/ghostnet."""
        super().__init__()
        c_ = c2 // 2
        self.conv = nn.Sequential(
            GhostConv(c1, c_, 1, 1),  # pw
            DWConv(c_, c_, k, s, act=False) if s == 2 else nn.Identity(),  # dw
            GhostConv(c_, c2, 1, 1, act=False),
        )  # pw-linear
        self.shortcut = (
            nn.Sequential(DWConv(c1, c1, k, s, act=False), Conv(c1, c2, 1, 1, act=False)) if s == 2 else nn.Identity()
        )

    def forward(self, x):
        """Processes input through conv and shortcut layers, returning their summed output."""
        return self.conv(x) + self.shortcut(x)

GhostBottleneck 是 GhostNet 架构中的一种轻量化瓶颈结构,类似于传统的残差块(ResNet中的Bottleneck),但使用了 GhostConv 来减少冗余计算。这种模块通过 Ghost 卷积和深度卷积来进一步优化计算效率,并在特定情况下使用 shortcut(跳跃连接)来保留输入信息,增强网络的表现力。

功能说明:

  • 目标: GhostBottleneck 模块将输入特征图通过 Ghost 卷积和深度卷积进行处理,并通过残差连接保留输入特征,从而增强特征提取的效率和表达能力。
  • 实现: 该模块使用 GhostConv 替代常规卷积,并在通道数扩展过程中保持低计算量。

详细解析:

1. 初始化方法 (__init__):
  • 参数:

    • c1: 输入通道数。
    • c2: 输出通道数。
    • k: 卷积核大小,默认为 3。
    • s: 步幅,默认为 1。
  • 模块内部结构:

    • c_ = c2 // 2:中间通道数。这个值是输出通道数的一半,用于第一个 Ghost 卷积。

    • self.conv:核心卷积序列,包括:

      1. GhostConv(c1, c_, 1, 1):第一个 Ghost 卷积,使用 1x1 卷积将输入的 c1 个通道降维到 c_,即 c2 // 2 个通道。
      2. DWConv(c_, c_, k, s):一个深度卷积层,只有当步幅 s=2 时才会启用,用于执行下采样。如果步幅为 1,则使用 nn.Identity() 跳过这一步。
      3. GhostConv(c_, c2, 1, 1, act=False):第二个 Ghost 卷积,将中间通道数 c_ 扩展到最终的输出通道数 c2
    • self.shortcut:用于残差连接的跳跃分支。只有当步幅 s=2 时才启用 shortcut:

      1. DWConv(c1, c1, k, s):深度卷积进行下采样。
      2. Conv(c1, c2, 1, 1):1x1 卷积将输入通道数 c1 转换为输出通道数 c2,以保证与主路径的通道数相匹配。

      如果步幅 s=1,则使用 nn.Identity(),即不改变输入张量,直接跳过。

2. 前向传播方法 (forward):

该方法定义了 GhostBottleneck 模块的前向传播过程:

  1. self.conv(x):通过 self.conv 主路径对输入进行处理,主要包括 Ghost 卷积和(可选的)深度卷积。
  2. self.shortcut(x):如果步幅为 2,则通过 shortcut 对输入进行下采样和卷积,以便与主路径的输出通道数匹配;如果步幅为 1,则保持输入不变。
  3. self.conv(x) + self.shortcut(x):将主路径和 shortcut 路径的输出相加,实现残差连接。

最终输出是主路径和 shortcut 路径的加和,输出形状为 (batch_size, c2, h, w),其中 hw 取决于是否进行下采样(如果步幅为 2,空间尺寸减半)。

工作流程示例:

假设输入特征图形状为 (batch_size, 32, 56, 56)c1=32c2=64s=2,工作流程如下:

  1. 主路径

    • 第一个 Ghost 卷积将通道数从 32 变为 32c2 // 2)。
    • 深度卷积进行下采样,将空间尺寸从 56x56 变为 28x28
    • 第二个 Ghost 卷积将通道数从 32 扩展到 64
  2. shortcut 路径

    • 深度卷积对输入进行下采样,将空间尺寸从 56x56 变为 28x28
    • 1x1 卷积将通道数从 32 转换为 64
  3. 残差连接:将主路径和 shortcut 的输出相加,输出形状为 (batch_size, 64, 28, 28)

设计思想:

  • GhostBottleneck 通过 Ghost 卷积减少冗余计算,而保留了标准卷积的特性,从而提高了计算效率。
  • 使用 shortcut 保留输入信息,使得信息流更加顺畅,尤其是在网络加深的情况下,残差连接有助于防止梯度消失问题。

总结:

  • GhostBottleneck 是 GhostNet 架构中的轻量化瓶颈模块,通过 Ghost 卷积和残差连接来实现高效的特征提取和信息流传递。
  • 它比标准的卷积模块计算量更少,但仍然可以保持良好的特征提取能力,适合用于需要高效推理的场景,如移动设备上的深度学习模型。

  • 11
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值