Ultralytics:解读Conv2模块

在这里插入图片描述

前言

相关介绍

Ultralytics 简介

Ultralytics 基于多年的计算机视觉和人工智能基础研究,创建了最先进的 (SOTA) YOLO 模型。我们的模型不断更新性能和灵活性,快速、准确且易于使用。他们擅长对象检测、跟踪、实例分割、语义分割、图像分类和姿势估计任务。

前提条件

  • 熟悉Python、Pytorch

实验环境

Package                  Version
------------------------ ------------
Python                   3.11.8
absl-py                  2.4.0
accelerate               1.13.0
annotated-doc            0.0.4
anyio                    4.13.0
calflops                 0.3.2
certifi                  2026.4.22
charset-normalizer       3.4.7
click                    8.3.3
colorama                 0.4.6
contourpy                1.3.3
cycler                   0.12.1
filelock                 3.29.0
flatbuffers              25.12.19
fonttools                4.62.1
fsspec                   2026.4.0
grpcio                   1.80.0
h11                      0.16.0
hf-xet                   1.5.0
httpcore                 1.0.9
httpx                    0.28.1
huggingface_hub          1.14.0
idna                     3.15
Jinja2                   3.1.6
kiwisolver               1.5.0
Markdown                 3.10.2
markdown-it-py           4.2.0
MarkupSafe               3.0.3
matplotlib               3.10.9
mdurl                    0.1.2
ml_dtypes                0.5.0
mpmath                   1.3.0
networkx                 3.6.1
numpy                    1.26.4
nvidia-cublas-cu12       12.8.3.14
nvidia-cuda-cupti-cu12   12.8.57
nvidia-cuda-nvrtc-cu12   12.8.61
nvidia-cuda-runtime-cu12 12.8.57
nvidia-cudnn-cu12        9.7.1.26
nvidia-cufft-cu12        11.3.3.41
nvidia-cufile-cu12       1.13.0.11
nvidia-curand-cu12       10.3.9.55
nvidia-cusolver-cu12     11.7.2.55
nvidia-cusparse-cu12     12.5.7.53
nvidia-cusparselt-cu12   0.6.3
nvidia-nccl-cu12         2.26.2
nvidia-nvjitlink-cu12    12.8.61
nvidia-nvtx-cu12         12.8.55
onnx                     1.19.0
onnxruntime-gpu          1.26.0
onnxslim                 0.1.94
opencv-python            4.6.0.66
packaging                26.2
pillow                   12.2.0
pip                      24.0
polars                   1.40.1
polars-runtime-32        1.40.1
protobuf                 7.34.1
psutil                   7.2.2
pycocotools              2.0.11
Pygments                 2.20.0
pyparsing                3.3.2
python-dateutil          2.9.0.post0
PyYAML                   6.0.3
regex                    2026.5.9
requests                 2.34.1
rich                     15.0.0
safetensors              0.7.0
scipy                    1.16.0
setuptools               65.5.0
shellingham              1.5.4
six                      1.17.0
sympy                    1.14.0
tabulate                 0.10.0
tensorboard              2.20.0
tensorboard-data-server  0.7.2
tokenizers               0.22.2
torch                    2.7.1+cu128
torchaudio               2.7.1+cu128
torchvision              0.22.1+cu128
tqdm                     4.67.3
transformers             5.8.1
triton                   3.3.1
typer                    0.25.1
typing_extensions        4.15.0
ultralytics              8.4.58
ultralytics-thop         2.0.19
urllib3                  2.7.0
Werkzeug                 3.1.8

Conv2

import cv2
import math
import torch
import numpy as np
import matplotlib.pyplot as plt
from torch import nn


def autopad(k, p=None, d=1):  # kernel, padding, dilation
    """Pad to 'same' shape outputs."""
    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


class Conv(nn.Module):
    """Standard convolution module with batch normalization and activation.

    Attributes:
        conv (nn.Conv2d): Convolutional layer.
        bn (nn.BatchNorm2d): Batch normalization layer.
        act (nn.Module): Activation function layer.
        default_act (nn.Module): Default activation function (SiLU).
    """

    default_act = nn.SiLU()  # default activation

    def __init__(self, c1, c2, k=1, s=1, p=None, g=1, d=1, act=True):
        """Initialize Conv layer with given parameters.

        Args:
            c1 (int): Number of input channels.
            c2 (int): Number of output channels.
            k (int): Kernel size.
            s (int): Stride.
            p (int, optional): Padding.
            g (int): Groups.
            d (int): Dilation.
            act (bool | nn.Module): Activation function.
        """
        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):
        """Apply convolution, batch normalization and activation to input tensor.

        Args:
            x (torch.Tensor): Input tensor.

        Returns:
            (torch.Tensor): Output tensor.
        """
        return self.act(self.bn(self.conv(x)))

    def forward_fuse(self, x):
        """Apply convolution and activation without batch normalization.

        Args:
            x (torch.Tensor): Input tensor.

        Returns:
            (torch.Tensor): Output tensor.
        """
        return self.act(self.conv(x))


class Conv2(Conv):
    """Simplified RepConv module with Conv fusing.

    Attributes:
        conv (nn.Conv2d): Main 3x3 convolutional layer.
        cv2 (nn.Conv2d): Additional 1x1 convolutional layer.
        bn (nn.BatchNorm2d): Batch normalization layer.
        act (nn.Module): Activation function layer.
    """

    def __init__(self, c1, c2, k=3, s=1, p=None, g=1, d=1, act=True):
        """Initialize Conv2 layer with given parameters.

        Args:
            c1 (int): Number of input channels.
            c2 (int): Number of output channels.
            k (int): Kernel size.
            s (int): Stride.
            p (int, optional): Padding.
            g (int): Groups.
            d (int): Dilation.
            act (bool | nn.Module): Activation function.
        """
        super().__init__(c1, c2, k, s, p, g=g, d=d, act=act)
        self.cv2 = nn.Conv2d(c1, c2, 1, s, autopad(1, p, d), groups=g, dilation=d, bias=False)  # add 1x1 conv

    def forward(self, x):
        """Apply convolution, batch normalization and activation to input tensor.

        Args:
            x (torch.Tensor): Input tensor.

        Returns:
            (torch.Tensor): Output tensor.
        """
        return self.act(self.bn(self.conv(x) + self.cv2(x)))

    def forward_fuse(self, x):
        """Apply fused convolution, batch normalization and activation to input tensor.

        Args:
            x (torch.Tensor): Input tensor.

        Returns:
            (torch.Tensor): Output tensor.
        """
        return self.act(self.bn(self.conv(x)))

    def fuse_convs(self):
        """Fuse parallel convolutions."""
        w = torch.zeros_like(self.conv.weight.data)
        i = [x // 2 for x in w.shape[2:]]
        w[:, :, i[0] : i[0] + 1, i[1] : i[1] + 1] = self.cv2.weight.data.clone()
        self.conv.weight.data += w
        self.__delattr__("cv2")
        self.forward = self.forward_fuse

使用示例

在这里插入图片描述

if __name__ == '__main__':
    # 1. 读取图像(请将路径改为您自己的图片)
    img_path = "cat_640x640.png"  # 或者使用绝对路径
    img_bgr = cv2.imread(img_path)
    if img_bgr is None:
        raise FileNotFoundError(f"图片 {img_path} 不存在!请检查路径。")

    # 2. 转换为 RGB,并转为 PyTorch 张量 (H,W,C) -> (C,H,W) -> (1,C,H,W)
    img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
    img_tensor = torch.from_numpy(img_rgb).float()          # (H, W, C)
    img_tensor = img_tensor.permute(2, 0, 1).unsqueeze(0)   # (1, C, H, W)

    # 3. 创建 Conv2 层:输入3通道,输出16通道,主核3×3,步长2(自动same padding)
    conv2_layer = Conv2(c1=3, c2=16, k=3, s=2)

    # 4. 前向传播(未融合,双分支)
    with torch.no_grad():
        out_before = conv2_layer(img_tensor)
    print("未融合输出形状:", out_before.shape)   # torch.Size([1, 16, 320, 320])

    # 5. 可视化未融合的特征图(第一个通道)
    feat_map_before = out_before[0, 0, :, :].cpu().numpy()
    feat_map_before = (feat_map_before - feat_map_before.min()) / (feat_map_before.max() - feat_map_before.min() + 1e-8)
    feat_map_before = (feat_map_before * 255).astype(np.uint8)

    # 6. 融合分支(将1x1卷积分支合并到3x3主卷积)
    conv2_layer.fuse_convs()
    print("融合后,cv2 属性已删除,forward 已切换为 forward_fuse")

    # 7. 融合后的前向传播(单分支)
    with torch.no_grad():
        out_after = conv2_layer(img_tensor)
    print("融合后输出形状:", out_after.shape)   # 同样为 torch.Size([1, 16, 320, 320])

    # 8. 验证融合前后输出是否一致(数值误差应接近于0)
    diff = torch.abs(out_before - out_after).max().item()
    print(f"融合前后最大绝对差异: {diff:.6f}")   # 通常 < 1e-6

    # 9. 可视化融合后的特征图(第一个通道),并与融合前对比
    feat_map_after = out_after[0, 0, :, :].cpu().numpy()
    feat_map_after = (feat_map_after - feat_map_after.min()) / (feat_map_after.max() - feat_map_after.min() + 1e-8)
    feat_map_after = (feat_map_after * 255).astype(np.uint8)

    # 10. 绘制对比图(原图、融合前特征图、融合后特征图、差异图)
    plt.figure(figsize=(15, 5))

    plt.subplot(1, 4, 1)
    plt.imshow(cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB))
    plt.title("Original Image")
    plt.axis("off")

    plt.subplot(1, 4, 2)
    plt.imshow(feat_map_before, cmap='gray')
    plt.title("Before Fuse (Ch0)")
    plt.axis("off")

    plt.subplot(1, 4, 3)
    plt.imshow(feat_map_after, cmap='gray')
    plt.title("After Fuse (Ch0)")
    plt.axis("off")

    # 差异图(放大显示差异)
    diff_map = np.abs(feat_map_before.astype(np.float32) - feat_map_after.astype(np.float32))
    plt.subplot(1, 4, 4)
    plt.imshow(diff_map, cmap='hot')
    plt.title("Difference (×10)")
    plt.axis("off")

    plt.tight_layout()
    plt.savefig("conv2_output_visualization.png", dpi=150)
    # 若有图形界面,可取消下面一行的注释以显示窗口
    # plt.show()
    print("可视化图片已保存为 conv2_output_visualization.png")

在这里插入图片描述

未融合输出形状: torch.Size([1, 16, 320, 320])
融合后,cv2 属性已删除,forward 已切换为 forward_fuse
融合后输出形状: torch.Size([1, 16, 320, 320])
融合前后最大绝对差异: 0.000006
可视化图片已保存为 conv2_output_visualization.png

流程示意图

在这里插入图片描述

代码解读

Conv2(简化版 RepConv)

Conv2Conv 的增强版本,它在标准 3×3 卷积的基础上,并行添加了一个 1×1 卷积分支,并将两者输出相加后再过 BN 和激活函数。这种设计借鉴了 RepVGG 的思想,能在训练时提高模型表达能力,而在推理时通过“融合”将两个分支合并为单个 3×3 卷积,从而不增加额外计算量。

功能

  • 训练阶段:采用 3×3 卷积 + 1×1 卷积 并行结构,两者输出相加后通过 BN 和激活函数,增强特征表达能力。
  • 推理阶段:通过 fuse_convs() 将 1×1 分支的权重叠加到 3×3 卷积核的中心位置,从而将双分支合并为单个 3×3 卷积,且无需增加额外计算量,实现无损加速

初始化参数

Conv 完全相同,但额外创建了一个 self.cv2(1×1 卷积),其参数(输入/输出通道、步长、分组、膨胀等)与主卷积保持一致。

参数说明
c1输入通道数
c2输出通道数
k主卷积核大小(默认 3)
s步长(两个分支共用)
p填充(若为 None 自动计算)
g分组数(两个分支共用)
d膨胀率(两个分支共用)
act激活函数(与 Conv 一致)

关键属性

  • self.conv:继承自父类的 3×3(或指定大小)主卷积。
  • self.cv2:新增的 1×1 卷积分支(无 BN,因为它与主分支共享同一个 BN)。
  • self.bn:共享的批归一化层(对两个分支的和进行归一化)。
  • self.act:共享的激活函数。

前向方法

方法流程用途
forward(x)act(bn(conv(x) + cv2(x)))训练或未融合推理(双分支)
forward_fuse(x)act(bn(conv(x)))融合后仅使用主卷积(已剥离 cv2)

融合机制(fuse_convs 详解)

数学原理

设主卷积核为 W 3 ∈ R C o u t × C i n × 3 × 3 W_3 \in \mathbb{R}^{C_{out} \times C_{in} \times 3 \times 3} W3RCout×Cin×3×3,1×1 卷积核为 W 1 ∈ R C o u t × C i n × 1 × 1 W_1 \in \mathbb{R}^{C_{out} \times C_{in} \times 1 \times 1} W1RCout×Cin×1×1

由于 1×1 卷积等价于 3×3 卷积核中心位置的权重,其余位置为 0,因此可将 W 1 W_1 W1 填充到 3×3 尺寸:

W f u s e d = W 3 + pad_to_3x3 ( W 1 ) W_{fused} = W_3 + \text{pad\_to\_3x3}(W_1) Wfused=W3+pad_to_3x3(W1)
其中填充操作将 W 1 W_1 W1 放在 3×3 核的正中央(索引 [1,1] 处)。

实现步骤
  1. 创建一个与主卷积权重形状相同的零张量 w
  2. 计算中心位置索引 i = [kh//2, kw//2](假设核大小为 3,则 i=[1,1])。
  3. self.cv2.weight 克隆后赋值到 w[:, :, i[0]:i[0]+1, i[1]:i[1]+1]
  4. w 加到主卷积权重上:self.conv.weight.data += w
  5. 删除 self.cv2 属性(释放内存)。
  6. forward 方法重定向为 forward_fuse,使后续前向只走单分支。

注意:由于两个分支共享同一个 BN,融合后 BN 参数不变(因为卷积层输出相加后归一化等价于融合后卷积再归一化,两者数学上完全等价)。


设计意图与优势

  • 训练增益:多分支结构提供更多梯度路径,有助于优化,提升模型精度。
  • 推理无损:融合后等价于单卷积,推理速度与 Conv 相同,无额外开销。
  • 简化实现:相比完整的 RepVGG(包含 3×3、1×1、identity 三支),Conv2 去除了 identity 分支(等价于 1×1 的特殊情况),实现更简洁,但仍具备核心的融合能力。

注意事项

  1. 膨胀卷积的支持:若 d > 1,1×1 卷积的膨胀率设置为 d 虽然无实际意义(因为核大小为1),但为保持一致性,代码仍传入了 d,不影响结果。
  2. 分组卷积g 参数同时作用于两个分支,分组数必须能整除 c1c2,否则会报错。
  3. 融合时机:应在推理前调用 fuse_convs(),且不要再进行训练,因为融合后 cv2 被删除,无法反向传播更新其梯度。
  4. 与父类 Conv 的关系Conv2 在初始化时调用了 super().__init__(...),因此继承了 convbnact 等属性,并额外添加 cv2

在 YOLOv8 中的应用

Conv2 并非 Ultralytics 官方 YOLOv8 的默认组件(官方使用标准 Conv),但它可用于自定义网络结构,例如在骨干网络中加入 RepVGG 风格模块,以提升精度-速度权衡。您可以将其替换 YAML 配置中的 Conv 项,或作为独立模块插入到 C2f 等容器中。

优缺点

优点

  1. 训练时表达能力强
    并行使用 3×3 和 1×1 双分支,梯度流更丰富,相当于隐式集成,有助于提升模型精度,可视为一种结构重参数化(RepVGG 思想)。

  2. 推理无额外开销
    通过 fuse_convs() 可将两个分支无损融合为单个 3×3 卷积,推理时速度与标准卷积完全一致,不增加任何计算或显存成本。

  3. 灵活性高
    可随时按需融合或保持双分支,方便在训练和部署之间切换,适应不同场景(如训练时用多分支,推理时用单分支)。

  4. 兼容性强
    继承自 Conv,可以无缝替换原有模块,无需改动网络结构,即插即用。

缺点

  1. 训练时显存和计算翻倍
    前向和反向传播需同时计算两个卷积,训练时 FLOPs 约增加 1.1 倍(3×3 + 1×1),显存占用也相应增加,对资源有一定要求。

  2. 实现复杂度较高
    融合逻辑涉及权重拷贝和 __delattr__ 操作,可能引发代码维护难度;若操作不当(如在融合后继续训练)会导致梯度计算错误。

  3. 仅适用于特定场合
    当步长 s>1 或采用膨胀卷积时,1×1 分支的“对齐”方式需额外注意,融合公式略有不同(但代码已考虑),但实际应用时需验证等效性。

  4. 对 BN 层共享依赖
    两个分支共用同一个 BN,融合后 BN 统计量需保持不变,这种设计虽然简洁,但限制了分支数(如不能轻易加入 identity 分支)。

参考文献

[1] https://docs.ultralytics.com/
[2] https://github.com/ultralytics/ultralytics.git

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FriendshipT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值