yolov8源码学习

Backbone:主体是CSPDarkNet结构

img网络模型文件保存在models->v8->yolov8.yaml

# 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

**from:**输入。-1表示上层的输出作为本层的输入。

**repeats:**模块的重复次数。

**module:**使用的模块。

**args:**模块里面的参数

计算size的公式out_size =( in_size - k +2 * p)/ s + 1

1.1 卷积Conv

def autopad(k, p=None, d=1):  # kernel, padding, dilation
    """Pad to 'same' shape outputs."""
    # 当膨胀系数d>1时,计算实际卷积核尺寸(膨胀后的等效核尺寸)
    if d > 1:
        # 处理整数核尺寸或列表核尺寸的情况(例如3或[3,5])
        k = d * (k - 1) + 1 if isinstance(k, int) else [d * (x - 1) + 1 for x in k]  # 实际kernel尺寸
    
    # 自动计算padding值(当未指定p时)
    if p is None:
        # 对整数核或列表核分别处理,取核尺寸整除2的结果(保持输入输出尺寸一致)
        p = k // 2 if isinstance(k, int) else [x // 2 for x in k]  # 自动填充
    
    return p  # 返回计算得到的padding值


class Conv(nn. Module):
    """Standard convolution with args(ch_in, ch_out, kernel, stride, padding, groups, dilation, activation)."""
    default_act = nn. SiLU()  # 默认使用SiLU激活函数(可能存在拼写错误,应为nn.SiLU)

    def __init__(self, c1, c2, k=1, s=1, p=None, g=1, d=1, act=True):
        """初始化卷积层,参数包含激活函数配置"""
        super().__init__()  # 调用父类nn.Module的初始化
        
        # 创建卷积层:输入通道c1,输出通道c2,核尺寸k,步长s,自动计算padding
        # 分组数g,膨胀系数d,禁用偏置(与BatchNorm配合使用)
        self.conv = nn. Conv2d(c1, c2, k, s, autopad(k, p, d), groups=g, dilation=d, bias=False)
        
        # 添加批量归一化层
        self.bn = nn. BatchNorm2d(c2)
        
        # 配置激活函数:
        # act为True时使用默认激活函数
        # act为nn.Module实例时直接使用
        # 其他情况使用恒等映射(无激活)
        self.act = self.default_act if act is True else act if isinstance(act, nn. Module) else nn. Identity()

    def forward(self, x):
        """标准前向流程:卷积 -> 批归一化 -> 激活"""
        return self.act(self.bn(self.conv(x)))  # 注意执行顺序:conv -> bn -> act

    def forward_fuse(self, x):
        """融合前向流程(通常用于模型优化):卷积 -> 激活"""
        return self.act(self.conv(x))  # 跳过批归一化,直接执行conv -> act

conv(x)-> bn() -> SiLU()

1.1.1 Conv2d
class Conv2d(_ConvNd):  # 继承自PyTorch卷积基类_ConvNd,提供通用卷积操作实现
    def __init__(
        self,
        in_channels: int,          # 输入特征图的通道数(如RGB图像的3通道)
        out_channels: int,         # 输出特征图的通道数(卷积核的数量)
        kernel_size: _size_2_t,    # 卷积核尺寸(接受int或tuple,如3或(3,5))
        stride: _size_2_t = 1,     # 卷积步长,控制特征图下采样率
        padding: Union[str, _size_2_t] = 0,  # 填充方式(int/tuple或"same"/"valid"等字符串)
        dilation: _size_2_t = 1,   # 膨胀系数,扩大卷积核感受野(默认为1不膨胀)
        groups: int = 1,           # 分组卷积的组数(默认为1普通卷积,groups=in_channels时为深度可分离卷积)
        bias: bool = True,         # 是否添加可训练偏置参数
        padding_mode: str = "zeros",  # 填充模式("zeros"/"reflect"/"replicate"等)
        device=None,               # 指定计算设备(如"cuda:0")
        dtype=None,                # 指定权重数据类型(如torch.float16)
    ) -> None:
        # 创建包含设备和数据类型信息的字典,用于后续张量初始化
        factory_kwargs = {"device": device, "dtype": dtype}
        
        # 将卷积核尺寸转换为标准的二元组格式(例如3→(3,3))
        kernel_size_ = _pair(kernel_size)
        # 将步长转换为二元组格式(例如2→(2,2))
        stride_ = _pair(stride)
        # 处理padding参数:如果是字符串(如"same")直接使用,否则转为二元组
        padding_ = padding if isinstance(padding, str) else _pair(padding)
        # 将膨胀系数转换为二元组格式
        dilation_ = _pair(dilation)
        
        # 调用父类_ConvNd的构造函数,初始化卷积层核心参数
        super().__init__(
            in_channels,          # 输入通道数
            out_channels,         # 输出通道数
            kernel_size_,         # 处理后的卷积核尺寸
            stride_,              # 处理后的步长
            padding_,             # 处理后的填充参数
            dilation_,            # 处理后的膨胀系数
            False,                # 是否为转置卷积(此处为普通卷积,故为False)
            _pair(0),             # 输出填充(output_padding,转置卷积时使用,普通卷积设为0)
            groups,               # 分组数
            bias,                 # 是否使用偏置
            padding_mode,         # 填充模式
            **factory_kwargs,     # 传递设备和数据类型参数
        )
1.1.2 BatchNorm2d(c2)
def _check_input_dim(self, input):
    if input.dim() != 4:
        raise ValueError(f"expected 4D input (got {input.dim()}D input)")

代码功能

这是某个归一化层(如 BatchNorm2d)的内部方法,用于检查输入张量的维度是否符合要求

  • 预期输入:4D 张量(形状为 [batch_size, channels, height, width])。
  • 检查逻辑:如果输入维度不是4维(如3D或5D),抛出错误。
1.1.3 SiLU(Sigmoid Linear Unit)

激活函数的作用是对输入张量逐元素应用Sigmoid加权线性单元,结合了线性乘积和非线性Sigmoid门控机制,旨在增强神经网络的表达能力。以下是详细说明:

核心功能:

  1. 数学表达式

    SiLU(x)=xσ(x)

    其中,σ(x) 是Sigmoid函数
    σ(x)=11+e−x σ(x)=\frac{1}{1+e^{-x} } σ(x)=1+ex1
    。该函数将输入 x 与其Sigmoid值相乘,形成平滑的非线性激活。

  2. 特性

    • 门控机制:Sigmoid部分充当“软开关”,根据输入值动态调整输出权重。正数区域的输出接近 x(Sigmoid趋近于1),负数区域的输出趋近于0(Sigmoid趋近于0)。
    • 平滑梯度:相比ReLU,SiLU在负值区域具有非零梯度,可能缓解梯度消失问题。

核心代码:

class SiLU(Module): # 继承自PyTorch的Module基类,定义自定义神经网络模块

class SiLU(Module):  # 继承自PyTorch的`Module`基类,定义自定义神经网络模块

    __constants__ = ["inplace"]  # 将"inplace"标记为TorchScript常量
    inplace: bool   # 类型注解:声明inplace属性为布尔值

    def __init__(self, inplace: bool = False):  # 构造函数
        super().__init__()  # 调用父类Module的初始化方法
        self.inplace = inplace  # 将参数值赋给实例变量

    def forward(self, input: Tensor) -> Tensor:  # 定义前向传播
        return F.silu(input, inplace=self.inplace)  # 调用PyTorch函数式API实现

    def extra_repr(self) -> str:  # 用于模块的额外信息表示
        inplace_str = "inplace=True" if self.inplace else ""  # 生成状态描述字符串
        return inplace_str  # 返回可读性描述(影响print输出)

1.2 c2f

class C2f(nn.Module):
    """Faster Implementation of CSP Bottleneck with 2 convolutions."""

    def __init__(self, c1, c2, n=1, shortcut=False, g=1, e=0.5):
        """
        Initialize a CSP bottleneck with 2 convolutions.

        Args:
            c1 (int): Input channels.  # 输入特征图的通道数
            c2 (int): Output channels.  # 输出特征图的通道数
            n (int): Number of Bottleneck blocks.  # Bottleneck模块的重复次数
            shortcut (bool): Whether to use shortcut connections.  # 是否在Bottleneck中使用残差连接
            g (int): Groups for convolutions.  # 分组卷积的分组数
            e (float): Expansion ratio.  # 通道扩展系数,控制隐藏层通道数
        """
        super().__init__()
        self.c = int(c2 * e)  # 计算隐藏层通道数(实际处理通道数 = 输出通道数 * 扩展系数)
        self.cv1 = Conv(c1, 2 * self.c, 1, 1)  # 输入投影层:1x1卷积将c1通道扩展为2倍隐藏通道
        self.cv2 = Conv((2 + n) * self.c, c2, 1)  # 输出投影层:1x1卷积将融合特征压缩回c2通道
        # 创建n个Bottleneck模块组成的序列,每个模块处理self.c通道
        self.m = nn.ModuleList(
            Bottleneck(
                self.c,  # 输入通道
                self.c,  # 输出通道
                shortcut,  # 是否使用残差连接
                g,  # 分组数
                k=((3, 3), (3, 3)),  # 每个Bottleneck包含两个3x3卷积
                e=1.0  # Bottleneck内部通道扩展比例(保持输入输出通道一致)
            ) for _ in range(n)
        )

    def forward(self, x):
        """Forward pass through C2f layer."""
        # 将cv1的输出沿通道维度切分为两部分(各self.c通道)
        y = list(self.cv1(x).chunk(2, 1))  # chunk返回张量视图列表
        # 逐级处理:每个Bottleneck处理最后一个特征图,并将结果追加到列表
        y.extend(m(y[-1]) for m in self.m)  # y从2个元素扩展到2+n个元素
        # 沿通道维度拼接所有特征图,并通过cv2输出
        return self.cv2(torch.cat(y, 1))  # 最终通道数 = (2+n)*self.c

    def forward_split(self, x):
        """Forward pass using split() instead of chunk()."""
        # 替代实现:使用split进行张量分割(功能与chunk相同,底层实现不同)
        y = self.cv1(x).split((self.c, self.c), 1)  # 分割为两个self.c通道的张量
        y = [y[0], y[1]]  # 转换为列表形式
        # 后续处理与forward方法一致
        y.extend(m(y[-1]) for m in self.m)
        return self.cv2(torch.cat(y, 1))

🎯 核心设计解析

  1. CSP结构优化
    • 通道分割:将输入特征图分割为两部分,仅对其中一部分进行Bottleneck处理,减少计算量
    • 特征融合:通过torch.cat合并初始特征与处理后的多级特征,保留不同层次的特征信息
  2. Bottleneck堆叠
    每个Bottleneck包含两个3x3卷积(通过k=((3,3),(3,3))配置),形成更深的子网络结构,增强非线性表达能力
  3. 两种前向方法
    • forward():使用chunk进行快速分割(返回视图,无内存复制)
    • forward_split():使用split实现相同功能,适用于需要显式内存分配的场景

🌰 计算流程示例(设n=2)

  1. 输入x经过cv1后通道数变为 2*self.c
  2. 分割为两个self.c通道的特征图y1, y2
  3. y2通过第一个Bottleneck生成新特征y3(self.c通道)
  4. y3通过第二个Bottleneck生成新特征y4(self.c通道)
  5. 最终拼接[y1, y2, y3, y4]得到(2+2)*self.c通道
  6. cv2将总通道数压缩回c2输出

对应功能代码:

1. 输入x经过cv1卷积
y = list(self.cv1(x).chunk(2, 1))  # 对应步骤1
  • 代码操作self.cv1 是第一个 1x1 卷积层,将输入 x 的通道数从 c1 扩展为 2*self.c
  • 结果形状:假设输入 x(B, c1, H, W) → 输出变为 (B, 2*self.c, H, W)
2. 分割为两个部分 y1, y2
y = list(...)  # 对应步骤2
  • 代码操作:通过 chunk(2, 1)cv1 的输出沿通道维度(dim=1)切分为两个张量
  • 结果内容y = [y1, y2],每个张量形状为 (B, self.c, H, W)
3. y2 通过第一个 Bottleneck 生成 y3
y.extend(m(y[-1]) for m in self.m)  # 对应步骤3(第一次循环)
  • 代码细节:
    • self.m 是包含 2 个 Bottleneck 的模块列表(n=2
    • 第一次循环m = self.m[0],处理 y[-1](即 y2
    • 结果形状y3 = m(y2) → 输出形状仍为 (B, self.c, H, W)
  • 此时 y 的值y = [y1, y2, y3]
4. y3 通过第二个 Bottleneck 生成 y4
y.extend(...)  # 对应步骤4(第二次循环)
  • 第二次循环m = self.m[1],处理 y[-1](即 y3
  • 结果形状y4 = m(y3) → 输出形状仍为 (B, self.c, H, W)
  • 此时 y 的值y = [y1, y2, y3, y4]
5. 拼接所有特征图
return self.cv2(torch.cat(y, 1))  # 对应步骤5
  • 代码操作torch.cat(y, 1) 将列表 y 中的 4 个张量沿通道维度拼接
  • 拼接后形状(B, (2+2)*self.c, H, W) → 通道数为 4*self.c
6.通过 cv2 压缩通道
return self.cv2(...)  # 对应步骤6
  • 代码操作self.cv2 是第二个 1x1 卷积层,将通道数从 4*self.c 压缩回 c2
  • 最终输出形状(B, c2, H, W)

关键代码段解析

# 代码段:y.extend(m(y[-1]) for m in self.m)
for m in self.m:       # 遍历每个Bottleneck模块(n=2时循环两次)
    new_feature = m(y[-1])  # 总是处理当前最后一个特征图
    y.append(new_feature)   # 将结果追加到列表

动态更新逻辑

  • 初始 y = [y1, y2]
  • 第一次循环后 y = [y1, y2, y3]
  • 第二次循环后 y = [y1, y2, y3, y4]

可视化流程

输入 x
   │
   ↓
cv1(x) → [y1, y2] → y2 → Bottleneck1 → y3 → Bottleneck2 → y4
   │                      │                          │
   └──────────────────────┴──────────────────────────┘
最终拼接:y1 + y2 + y3 + y4 → cv2 → 输出

这种设计通过 逐步处理特征图并保留中间结果,实现了多级特征融合,同时保持了计算效率。

1.3 SPPF

class SPPF(nn.Module):
    """Spatial Pyramid Pooling - Fast (SPPF) layer for YOLOv5 by Glenn Jocher."""

    def __init__(self, c1, c2, k=5):
        """
        Initialize the SPPF layer with given input/output channels and kernel size.

        Args:
            c1 (int): Input channels.  # 输入特征图的通道数
            c2 (int): Output channels.  # 输出特征图的通道数
            k (int): Kernel size.  # 池化核尺寸(默认5x5)

        Notes:
            This module is equivalent to SPP(k=(5, 9, 13)).  # 等效于使用5,9,13三种核尺寸的SPP
        """
        super().__init__()
        c_ = c1 // 2  # 计算隐藏层通道数(输入通道数减半)
        # 第一个1x1卷积压缩通道数
        self.cv1 = Conv(c1, c_, 1, 1)  # 1x1卷积将c1通道压缩到c_(c1//2)通道
        # 最后一个1x1卷积恢复通道数
        self.cv2 = Conv(c_ * 4, c2, 1, 1)  # 1x1卷积将拼接后的4*c_通道扩展到c2通道
        # 定义最大池化层(核心实现)
        self.m = nn.MaxPool2d(kernel_size=k, stride=1, padding=k // 2)  # 固定步长1,padding=k//2保持特征图尺寸

    def forward(self, x):
        """Apply sequential pooling operations to input and return concatenated feature maps."""
        # 初始处理:通过第一个卷积层
        y = [self.cv1(x)]  # 对输入执行第一次卷积,结果作为第一个特征图
        # 连续三次最大池化操作(等效不同尺寸感受野)
        y.extend(self.m(y[-1]) for _ in range(3))  # 对前次结果重复池化3次,生成多尺度特征图
        """ 
        执行过程示例:
        y[0] = cv1(x)                  → 原始特征
        y[1] = m(y[0])                 → 5x5感受野
        y[2] = m(y[1]) = m(m(y[0]))    → 等效9x9感受野 
        y[3] = m(y[2]) = m(m(m(y[0]))) → 等效13x13感受野
        """
        # 拼接并输出最终结果
        return self.cv2(torch.cat(y, 1))  # 沿通道维度拼接4个特征图,最后通过卷积调整通道数

🎯 关键设计解析

  1. 等效多尺度感受野
    通过​​单一大尺寸池化核(5x5)的连续应用​​,实现与标准SPP(使用5x5,9x9,13x13池化核)相同的多尺度特征提取效果:
    • 第1次池化:5x5感受野
    • 第2次池化:5 + (5-1) = 9x9感受野
    • 第3次池化:9 + (5-1) = 13x13感受野
  2. 计算效率优化
    相比标准SPP需要三个独立池化层:
    • ✅ 参数共享:重复使用同一个池化层
    • ✅ 内存优化:只需存储一个池化核参数
    • ✅ 减少33%的池化计算量(实验证明精度无损)
  3. 通道压缩策略
    • cv1先将通道数减半(c1//2),降低后续池化计算量
    • 最终拼接后通过cv2恢复目标通道数(c2

Lout=⌊Lin+2×padding−dilation×(kernel_size−1)−1stride+1⌋ L_{\text{out}} = \left\lfloor \frac{L_{\text{in}} + 2 \times \text{padding} - \text{dilation} \times (\text{kernel\_size} - 1) - 1}{\text{stride}} + 1 \right\rfloor Lout=strideLin+2×paddingdilation×(kernel_size1)1+1

🌰 计算示例(假设输入形状为[1,64,640,640])

  1. cv1处理
    [1,64,640,640][1,32,640,640](通道减半)

  2. 三次池化结果

    步骤 操作 输出形状 等效感受野
    y[0] cv1(x) [1,32,640,640] -
    y[1] m(y[0]) [1,32,640,640] 5x5
    y[2] m(y[1]) [1,32,640,640] 9x9
    y[3] m(y[2]) [1,32,640,640] 13x13
  3. 最终拼接
    torch.cat(y,1)[1,128,640,640](4 * 32通道)
    cv2处理[1,c2,640,640](恢复目标通道)

1.4 总结

imput map channel:3 size:640*640

第0层(Conv[3,2,1]) channel:64 size:320*320

第1层(Conv[3,2,1]) channel:128 size:160*160

第2层(C2f)* 3 channel:128 size:160*160

第3层(Conv[3,2,1]) channel:256 size: 80*80

第4层(C2f)* 6 channel:256 size: 80*80

第5层(Conv[3,2,1]) channel:512 size: 40*40

第6层(C2f)* 6 channel:512 size: 40*40

第7层(Conv[3,2,1]) channel:1024 size: 20*20

第8层(C2f)* 3 channel:1024 size: 20*20

第9层(SPPF[k=5]) channel:1024 size: 20*20

二、Head

# 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)

(1)第10层:经过SSPF的模块,接着进入上采样upsample。
None:不指定输出尺寸。
2:输出的尺寸为输入尺寸的2倍。
nearest:使用邻近插值算法。经过上采样后尺寸从10242020 -> 10244040。
经过路线如图所示:

img

(2)第11层:Concat模块,与经过上采样的上一层和第六层(P4)concat。(代码在:ultralytics -> nn -> modules-> init.py)
[1]:在维度为1上拼接;

此时经过上采样的尺寸为10244040 + 第6层输出尺寸为5124040 = 15364040。

class Concat(nn.Module):
    """Concatenate a list of tensors along dimension."""
 
    def __init__(self, dimension=1):
        """Concatenates a list of tensors along a specified dimension."""
        super().__init__()
        self.d = dimension
 
    def forward(self, x):
        """Forward pass for the YOLOv8 mask Proto module."""
        return torch.cat(x, self.d)

(3)**第12层:**3C2f;通道数为512,不进行shortcut。此时尺寸从1536*40*40 -> 512*40*40
(4)**第13层:**upsample,第12层作为输入。与第10层原理一样。尺寸从512*40*40 -> 512*80*80
(5)**第14层:*Concat模块,与经过上采样的上一层(13层)和第四层(P3)连接。
此时经过上采样的尺寸为512*80*80 + 第4层输出尺寸为256*80*80 = 768*80*80。
(6)第15层:*
3*C2f;通道数为256,不进行shortcut。此时尺寸从768*80*80 -> 256*80*80。
(7)**第16层:**经过卷积Conv,通道256,k=3,s=2,计算公式上面介绍过。256*80*80 -> 256*40*40.
(8)**第17层:**Concat模块,与经过卷积的16和第12层连接。16层:256*40*40 + 第12层:512*40*40 = 768*40*40。
(9)**第18层:**3*C2f;通道数为512,不进行shortcut。此时尺寸从768
4040 -> 51240*40。
(10) **第19层:**经过卷积Conv,通道512,k=3,s=2,计算公式上面介绍过。512*40*40 -> 512*20*20.
(11)**第20层:**Concat模块,与经过卷积的19和第9层连接。19层:512*20*20 + 第9层:1024*20*20 = 1536*20*20。
(12)第21层:3*C2f;通道数为1024,不进行shortcut。尺寸变化:1536*20*20 -> 1024*20*20

三、Detect

def __init__(self, nc=80, ch=()):  # 初始化检测层
    super().__init__()
    self.nc = nc  # 类别数量(COCO默认80)
    self.nl = len(ch)  # 检测层数量(对应P3/P4/P5)
    self.reg_max = 16  # DFL回归参数最大值(用于边界框分布预测)
    self.no = nc + self.reg_max * 4  # 每个anchor的输出维度(4个坐标参数*reg_max + 类别数)
    self.stride = torch.zeros(self.nl)  # 各检测层的步幅
    
    # 通道数计算:c2用于回归分支,c3用于分类分支
    c2 = max((16, ch[0] // 4, self.reg_max * 4))  # 回归分支通道至少16/reg_max*4
    c3 = max(ch[0], self.nc)  # 分类分支通道至少等于类别数
    
    # 回归分支网络:3层卷积 -> 输出4*reg_max(坐标分布参数)
    self.cv2 = nn.ModuleList([
        nn.Sequential(
            Conv(x, c2, 3),     # 3x3卷积
            Conv(c2, c2, 3),    # 二次3x3卷积
            nn.Conv2d(c2, 4 * self.reg_max, 1)  # 1x1卷积输出分布参数
        ) for x in ch
    ])
    
    # 分类分支网络:3层卷积 -> 输出nc(类别概率)
    self.cv3 = nn.ModuleList([
        nn.Sequential(
            Conv(x, c3, 3),     # 3x3卷积
            Conv(c3, c3, 3),    # 二次3x3卷积
            nn.Conv2d(c3, self.nc, 1)  # 1x1卷积输出类别分数
        ) for x in ch
    ])
    
    # DFL模块:将分布参数转换为实际坐标(Distribution Focal Loss)
    self.dfl = DFL(self.reg_max) if self.reg_max > 1 else nn.Identity()

def forward(self, x):
    """前向传播过程:生成预测框和类别概率"""
    shape = x[0].shape  # 获取输入特征图形状[B,C,H,W]
    
    # 处理每个检测层的输出
    for i in range(self.nl):
        # 拼接回归分支和分类分支的输出
        x[i] = torch.cat((self.cv2[i](x[i]), self.cv3[i](x[i])), 1)
    
    # 训练模式直接返回原始输出
    if self.training:
        return x
    
    # 推理模式下的动态调整(首次推理或特征图形状变化时)
    elif self.dynamic or self.shape != shape:
        # 生成锚点框和步幅(基于特征图尺寸)
        self.anchors, self.strides = (x.transpose(0, 1) for x in make_anchors(x, self.stride, 0.5))
        self.shape = shape  # 缓存当前形状
    
    # 拼接所有检测层的输出 [B,no,anchors_num]
    x_cat = torch.cat([xi.view(shape[0], self.no, -1) for xi in x], 2)
    
    # 处理导出模式(避免某些框架不支持split操作)
    if self.export and self.format in ('saved_model', 'pb', 'tflite', 'edgetpu', 'tfjs'):
        box = x_cat[:, :self.reg_max * 4]  # 提取回归参数部分
        cls = x_cat[:, self.reg_max * 4:]  # 提取分类分数部分
    else:
        box, cls = x_cat.split((self.reg_max * 4, self.nc), 1)  # 分割回归和分类结果
    
    # 将分布参数转换为边界框坐标(使用DFL解码)
    dbox = dist2bbox(self.dfl(box), self.anchors.unsqueeze(0), xywh=True, dim=1) * self.strides
    # 拼接坐标和类别概率(sigmoid激活分类分数)
    y = torch.cat((dbox, cls.sigmoid()), 1)
    
    # 导出模式返回单张量,否则返回元组(包含中间结果)
    return y if self.export else (y, x)

def bias_init(self):
    """偏置初始化:需要stride参数可用时执行"""
    m = self  # 获取当前Detect模块
    
    # 遍历每个检测层的回归和分类分支
    for a, b, s in zip(m.cv2, m.cv3, m.stride):
        # 回归分支最后一层卷积偏置初始化为1.0(稳定初始预测)
        a[-1].bias.data[:] = 1.0  # box
        
        # 分类分支偏置初始化(基于类别频率的启发式初始化)
        # 公式解释:log(5/(nc*0.01*(640/s)^2)) -> 平衡不同尺度的类别预测
        b[-1].bias.data[:m.nc] = math.log(5 / m.nc / (640 / s) ** 2)

img

首先来看一下初始化的一些属性,

nc: 整数,表示图像分类问题中的类别数;
nl: 整数,表示检测模型中使用的检测层数;
reg_max: 整数,表示每个锚点输出的通道数;
no: 整数,表示每个锚点的输出数量,其中包括类别数和位置信息;
stride: 一个形状为(nl,)的张量,表示每个检测层的步长(stride);
cv2: 一个 nn.ModuleList 对象,包含多个卷积层,用于预测每个锚点的位置信息;
cv3: 一个 nn.ModuleList 对象,包含多个卷积层,用于预测每个锚点的类别信息;
dfl: DFL(Distribution Focal Loss)
shape属性表示模型期望的输入形状,如果模型只接受固定形状的输入,则 self.shape 存储该形状

在前向传播中,shape获取了输入张量x的形状,并保存在shape中。

    for i in range(self.nl):
        x[i] = torch.cat((self.cv2[i](x[i]), self.cv3[i](x[i])), 1)

这里我们可以print(x[i].size())看一下,发现是:1664040,1662020,1661010,因为我是两个类,所以66 = (2+416)这个416也就是self.no = nc + self.reg_max * 4。因为我输入尺寸是320320的,所以三个特征图是40,20,10,如果大家是640*640的,特征图应该是80,40,20。

这里同时可以打开onnx模型看一下,这一步是将cv2和cv3的输入进行concat,那么形状应该是这样:
img

接着如果是训练过程的话,这里的x就输出了。否则的话继续。

在这个代码片段中,self.dynamic和self.shape是两个属性,它们与输入张量的形状有关。如果 self.dynamic为真或者self.shape 与当前输入张量的形状不同,那么就会执行相应的操作。

self.dynamic属性通常用于指示模型是否支持动态形状输入。在 PyTorch 中,动态形状表示对形状进行推理,而不依赖于固定的形状尺寸。当使用动态形状时,模型可以处理任意形状的输入,并且可以通过在运行时计算形状信息来确定每个层的形状。如果模型支持动态形状输入,则 self.dynamic 属性通常设置为 True。

self.shape 属性通常用于存储模型所期望的输入形状。如果模型只接受固定形状的输入,则 self.shape 属性将存储该形状。在这种情况下,如果输入张量的形状与self.shape不匹配,则可能需要对输入进行重新调整,以适应模型的期望输入形状。

那么这个

self.anchors, self.strides = (x.transpose(0, 1) for x in make_anchors(x, self.stride, 0.5))

就需要看make_anchors了,这个方法在utils/tal.py中实现:

def make_anchors(feats, strides, grid_cell_offset=0.5):
    """Generate anchors from features."""
    anchor_points, stride_tensor = [], []
    assert feats is not None
    dtype, device = feats[0].dtype, feats[0].device
    for i, stride in enumerate(strides):
        _, _, h, w = feats[i].shape
        sx = torch.arange(end=w, device=device, dtype=dtype) + grid_cell_offset  # shift x
        sy = torch.arange(end=h, device=device, dtype=dtype) + grid_cell_offset  # shift y
        sy, sx = torch.meshgrid(sy, sx, indexing='ij') if TORCH_1_10 else torch.meshgrid(sy, sx)
        anchor_points.append(torch.stack((sx, sy), -1).view(-1, 2))
        stride_tensor.append(torch.full((h * w, 1), stride, dtype=dtype, device=device))
    return torch.cat(anchor_points), torch.cat(stride_tensor)

feats: 一个列表,包含多个特征图;
strides: 一个列表,包含多个步长;
grid_cell_offset: 一个浮点数,表示每个网格单元的偏移量,默认为 0.5。
在实现中,首先遍历输入的特征图和步长,并分别获取它们的高度、宽度和步长值。然后,使用 PyTorch 的 arange() 函数生成一组横向和纵向的位移值,并添加一个偏移量(即 grid_cell_offset)以将锚点的中心对准每个网格单元的中心。

接下来,使用 PyTorch 的 meshgrid() 函数生成所有可能的锚点位置,并将其保存在 anchor_points 列表中。其中,每个锚点的位置由两个坐标值表示,即 (x, y),并被转换为形状为 (n, 2) 的张量,其中 n 表示特征图上的像素点数量。

同时,在每个特征图上都需要保存相应的步长信息,以便后续计算。因此,使用 PyTorch 的 full() 函数创建一个形状为 (h*w, 1) 的张量,其中 h 和 w 分别表示特征图的高度和宽度,每个元素都被初始化为当前特征图的步长值。

最终,通过将所有锚点位置和步长信息连接起来,可以得到形状为 (nnl, 2) 和 (nnl, 1) 的张量,其中 nl 表示特征图的数量,n 表示每个特征图上的像素点数量。这些张量将被用于计算每个锚点的位置和预测信息,并生成最终的预测结果。

所以经过transpose后,得到的anchor应该是(2,2100),stride是(1,2100)

三个x[i]进行concat:

x_cat = torch.cat([xi.view(shape[0], self.no, -1) for xi in x], 2)

得到的x_cat的size()应该是1662100,因为xi.view(shape[0],self.no,-1)中的-1表示根据其他维度的值组合成一维,即4040=1600,2020=400,10*10=100。

下一步的if else其实输出是一样的(我是这么认为的,若有错误请指点)

都是将x_cat的第二个维度66分成box的64和cls的2,这里的box的第二个维度经过dfl的操作变成4维的142100,与升维后的anchors.unsqueeze(0)送入dist2bbox进行计算,得到xywh值。

这里贴一下dist2bbox的实现:

def dist2bbox(distance, anchor_points, xywh=True, dim=-1):
    """Transform distance(ltrb) to box(xywh or xyxy)."""
    lt, rb = distance.chunk(2, dim)
    #print('lt:',lt.size())
    #print('rb:',rb.size())
    x1y1 = anchor_points - lt
    x2y2 = anchor_points + rb
    if xywh:
        c_xy = (x1y1 + x2y2) / 2
        wh = x2y2 - x1y1
        return torch.cat((c_xy, wh), dim)  # xywh bbox
    return torch.cat((x1y1, x2y2), dim)  # xyxy bbox

这里lt和rb分别代表x y w h的偏移量。

紧接着y = torch.cat((dbox, cls.sigmoid()), 1),将xywh和经过sigmoid归一化后的2个cls在第二维度上进行组合,形成了最终的162100,也就是最终的output。

img

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐