YOLOv8讲解

学习 · 2023-06-16 · 72 人浏览

YOLOv8讲解

模型结构

yolov8

scales: # model compound scaling constants, i.e. 'model=yolov8n.yaml' will call yolov8.yaml with scale 'n'
  # [depth, width, max_channels]
  n: [0.33, 0.25, 1024]  # YOLOv8n summary: 225 layers,  3157200 parameters,  3157184 gradients,   8.9 GFLOPs
  s: [0.33, 0.50, 1024]  # YOLOv8s summary: 225 layers, 11166560 parameters, 11166544 gradients,  28.8 GFLOPs
  m: [0.67, 0.75, 768]   # YOLOv8m summary: 295 layers, 25902640 parameters, 25902624 gradients,  79.3 GFLOPs
  l: [1.00, 1.00, 512]   # YOLOv8l summary: 365 layers, 43691520 parameters, 43691504 gradients, 165.7 GFLOPs
  x: [1.00, 1.25, 512]   # YOLOv8x summary: 365 layers, 68229648 parameters, 68229632 gradients, 258.5 GFLOPs

模型解析

这里只讲模型使用到的模块

autopad

  • 功能:返回pad的大小,使得padding后输出张量的大小不变。
  • 参数:

    • k:卷积核(kernel)的大小。类型可能是一个int也可能是一个序列
    • p: 填充(padding)的大小。默认为None
    • d: 扩张率(dilation rate)的大小, 默认为1 。普通卷积的扩张率为1,空洞卷积的扩张率大于1。

假设k为原始卷积核大小,d为卷积扩张率(dilation rate),加入空洞之后的实际卷积核尺寸与原始卷积核尺寸之间的关系:k =d(k-1)+1

通常,如果我们添加$p_h$行填充(大约一半在顶部,一半在底部)和$p_w$列填充(大约一半在左侧,一半在右侧),则输出的形状为$(n_h-k_h+p_h+1)\times(n_w−k_w+p_w+1)$

当设置$p_h=k_h-1$和$ p_w=k_w-1$时,输入和输出具有相同的高度和宽度。

假设p为填充(padding)的大小(通常,$p_h=p_w=\frac{p}{2}$)。一般来说$k_h=k_w=k$,且为奇数。

则当p=k//2时,padding后输出张量的大小不变。

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

Conv

  • 功能:标准卷积
  • 参数:输入通道数(c1), 输出通道数(c2), 卷积核大小(k,默认是1), 步长(s,默认是1), 填充(p,默认为None), 组(g, 默认为1), 扩张率(d,默认为1), 是否采用激活函数(act ,默认为True, 且采用SiLU为激活函数)
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):
        """Initialize Conv layer with given arguments including 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):
        """Apply convolution, batch normalization and activation to input tensor."""
        return self.act(self.bn(self.conv(x)))

    def forward_fuse(self, x):
        """Perform transposed convolution of 2D data."""
        return self.act(self.conv(x))

Conv 类,继承自 nn.Module。它实现了标准的卷积操作,具有一些参数(ch_inch_outkernelstridepaddinggroupsdilationactivation)来定义卷积层的行为。

  1. Conv 类的初始化方法 __init__ 中,首先调用了父类 nn.Module 的初始化方法 super().__init__()
  2. 使用 nn.Conv2d 创建了一个卷积层 self.conv,其中包括输入通道数 c1、输出通道数 c2、卷积核大小 k、步长 s、填充 p、分组数 g、膨胀率 d、偏置 bias 等参数。
  3. 创建了批归一化层 self.bn,用于对卷积结果进行归一化处理。
  4. 根据 act 参数的类型,确定激活函数 self.act,默认为 nn.SiLU()

在前向传播方法 forward 中,首先对输入张量 x 进行卷积操作 self.conv(x),然后对卷积结果进行批归一化 self.bn,最后使用激活函数 self.act 进行激活,并返回结果。

forward_fuse 方法用于执行转置卷积操作。它对输入张量 x 执行卷积操作 self.conv(x),然后使用激活函数 self.act 进行激活,并返回结果。

nn.SiLU() 是 PyTorch 中的一个激活函数,全称为 "Sigmoid-weighted Linear Unit"。SiLU 函数也被称为 Swish 函数,它是由 Google Brain 的研究人员提出的一种激活函数。

SiLU 函数的定义如下:

SiLU(x) = x * sigmoid(x)

SiLU 函数可以看作是将输入张量先经过 Sigmoid 函数,然后再与输入张量进行逐元素乘积的操作。SiLU 函数在很多情况下表现出比传统的激活函数如 ReLU 更好的性能和梯度传播特性。

在神经网络模型中,可以通过将 nn.SiLU() 作为激活函数的参数传递给卷积层、线性层或其他层,以应用 SiLU 激活函数。例如,在上述代码中的 Conv 类中,如果 act=True,则默认使用 nn.SiLU() 作为激活函数。

C2f 和 Bottleneck 和concat

C2f的每个Bottleneck的输出都会被Concat到一起。

  • 功能:构建深层特征提取网络
  • 参数:输入通道数 c1、输出通道数 c2、重复次数 n、是否使用 shortcut 连接 shortcut、分组卷积的组数 g、扩展因子 e 等参数。
class Bottleneck(nn.Module):
    """Standard bottleneck."""

    def __init__(self, c1, c2, shortcut=True, g=1, k=(3, 3), e=0.5):  # ch_in, ch_out, shortcut, groups, kernels, expand
        super().__init__()
        c_ = int(c2 * e)  # hidden channels
        self.cv1 = Conv(c1, c_, k[0], 1)
        self.cv2 = Conv(c_, c2, k[1], 1, g=g)
        self.add = shortcut and c1 == c2

    def forward(self, x):
        """'forward()' applies the YOLOv5 FPN to input data."""
        return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))

Bottleneck 是一种残差块(Residual Block)的变种。Bottleneck 的设计旨在提高网络的效率和表达能力,同时减少计算量。

该 Bottleneck 模块的主要功能如下:

  1. 通过一个 1x1 卷积层将输入特征的通道数进行压缩,将输入通道数减少到较小的值(c1 到 c_)。
  2. 经过一个具有指定内核大小(k[0])的卷积层对压缩后的特征进行卷积操作,用于提取特征信息。
  3. 经过一个具有指定内核大小(k[1])的卷积层将通道数扩展回原来的大小(c_ 到 c2)。
  4. 如果设置了残差连接(shortcut=True)且输入特征的通道数与输出特征的通道数相同(c1 == c2),则将输入特征与输出特征相加,实现残差连接。否则,直接输出卷积后的特征。

Bottleneck 模块的使用可以帮助网络进行深层的特征提取,并通过残差连接保持输入特征的重要信息,有助于提高网络的表达能力和优化训练过程。

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

    def __init__(self, c1, c2, n=1, shortcut=False, g=1, e=0.5):  # ch_in, ch_out, number, shortcut, groups, expansion
        super().__init__()
        self.c = int(c2 * e)  # hidden channels
        self.cv1 = Conv(c1, 2 * self.c, 1, 1)
        self.cv2 = Conv((2 + n) * self.c, c2, 1)  # optional act=FReLU(c2)
        self.m = nn.ModuleList(Bottleneck(self.c, self.c, shortcut, g, k=((3, 3), (3, 3)), e=1.0) for _ in range(n))

    def forward(self, x):
        """Forward pass through C2f layer."""
        y = list(self.cv1(x).chunk(2, 1))
        y.extend(m(y[-1]) for m in self.m)
        return self.cv2(torch.cat(y, 1))

    def forward_split(self, x):
        """Forward pass using split() instead of chunk()."""
        y = list(self.cv1(x).split((self.c, self.c), 1))
        y.extend(m(y[-1]) for m in self.m)
        return self.cv2(torch.cat(y, 1))

该模块包含了两个卷积层和一些 Bottleneck 模块的组合。

下面是该类的主要成员和功能:

  • __init__(self, c1, c2, n=1, shortcut=False, g=1, e=0.5):初始化函数,接受输入通道数 c1、输出通道数 c2、重复次数 n、是否使用 shortcut 连接 shortcut、分组卷积的组数 g、扩展因子 e 等参数。在初始化过程中创建了包含了两个卷积层和一些 Bottleneck 模块的组合。
  • forward(self, x):前向传播函数,接受输入张量 x。在前向传播过程中,首先通过一个卷积层 self.cv1 对输入进行卷积操作,然后将输出分成两部分。接下来,通过一系列的 Bottleneck 模块 self.m 对其中一部分进行处理,并将处理后的结果与另一部分进行拼接。最后,通过另一个卷积层 self.cv2 对拼接后的结果进行卷积操作,并返回输出张量。
  • forward_split(self, x):与 forward(self, x) 类似的前向传播函数,但在处理输入分成两部分时,使用了 split() 方法代替了 chunk() 方法。其余部分的功能与 forward(self, x) 相同。

这个模块类实现了一种特定的结构,用于构建神经网络中的某些层。在前向传播过程中,通过组合不同的卷积层和 Bottleneck 模块,实现了一种特定的特征提取和转换操作。具体的参数设置和设计取决于具体的应用和网络架构。

split() 是 PyTorch 中的一个张量操作函数,用于将张量沿指定维度进行分割。它可以将一个张量分割成多个子张量,并按照指定的尺寸或数量进行划分。

split() 函数的基本语法如下:

torch.split(tensor, split_size_or_sections, dim=0)

参数说明:

  • tensor:要分割的输入张量。
  • split_size_or_sections:指定分割的尺寸或分割的数量。可以是一个整数表示每个子张量的尺寸,也可以是一个列表或元组表示每个子张量的尺寸或分割的位置。
  • dim:指定分割的维度。

split() 函数的返回值是一个包含分割后的子张量的列表。

在 YOLOv8 的代码中,split() 函数用于将张量按照指定的尺寸或数量进行划分,例如:

y = list(self.cv1(x).split((self.c, self.c), 1))

这段代码是将经过 self.cv1(x) 的张量按照通道维度进行划分,划分为两个子张量,每个子张量的尺寸为 self.c。最后,将划分后的子张量存储在列表 y 中。

这种使用方式通常用于将张量在通道维度进行划分,以便在后续的操作中进行处理或连接。

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)

当dimension=1时,将多张相同尺寸的图像在通道维度第一个维度进行拼接

torch.cat(x, self.d) 是一个将张量列表 x 沿着指定的维度 self.d 进行拼接的操作。

具体而言,torch.cat 函数会将列表中的张量按照指定的维度进行连接,生成一个拼接后的张量。参数 x 是一个张量列表,表示需要拼接的张量序列,而 self.d 则表示指定的拼接维度。

例如,假设 x 是一个包含两个形状为 (3, 32, 32) 的张量的列表,那么调用 torch.cat(x, 1) 将会在第一个维度上进行拼接,生成一个形状为 (6, 32, 32) 的张量。注意,拼接的维度的大小需要保持一致,除了指定的拼接维度外,其他维度的大小必须完全相同。

总而言之,torch.cat(x, self.d) 可以将列表中的张量按照指定的维度进行拼接,生成一个拼接后的张量。

Upsample

[-1, 1, nn.Upsample, [None, 2, 'nearest']]

在YOLO中的上采样层通常使用的参数解释如下:

  • -1:表示上采样操作应用于输入特征图的维度或通道数。这个值通常是一个占位符,表示在实际使用时会根据输入特征图的维度进行动态计算。
  • 1:表示上采样的尺度因子或倍数。这个值指示输入特征图在每个维度上的放大倍数。例如,如果输入特征图的尺寸是 [C, H, W],则上采样后的尺寸将是 [C, H * 1, W * 1]
  • nn.Upsample:表示使用的上采样模块或函数。在YOLO中,通常使用PyTorch中的nn.Upsample模块来执行上采样操作。
  • [None, 2, 'nearest']:表示上采样操作的其他参数。这个列表中的参数用于配置nn.Upsample模块的行为。具体解释如下:

    • None:表示指定上采样操作的输出尺寸。在这里,None表示输出尺寸将根据输入特征图的尺寸和尺度因子进行自动计算。
    • 2:表示上采样操作使用的插值因子。这个值指示在上采样时使用的插值算法,例如双线性插值。
    • 'nearest':表示上采样操作使用的对齐方式。这个值指示上采样时每个输出像素对应的输入像素的对齐方式,例如最近邻对齐。

SPPF

这个是YOLOv5作者Glenn Jocher基于SPP提出的,速度较SPP快很多,所以叫SPP-Fast。

  • 功能:提取不同尺度的特征并进行融合
  • 参数:c1:输入特征图的通道数,c2:输出特征图的通道数,k:空间金字塔池化的核大小,表示使用不同尺度的池化核。
class SPPF(nn.Module):
    """Spatial Pyramid Pooling - Fast (SPPF) layer for YOLOv5 by Glenn Jocher."""

    def __init__(self, c1, c2, k=5):  # 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):
        """Forward pass through Ghost Convolution block."""
        x = self.cv1(x)
        y1 = self.m(x)
        y2 = self.m(y1)
        return self.cv2(torch.cat((x, y1, y2, self.m(y2)), 1))

__init__方法中,定义了SPPF层的结构。具体步骤如下:

  1. 使用Conv模块将输入特征图的通道数从c1减半,得到隐藏通道数c_
  2. 定义第一个卷积层cv1,它将输入特征图的通道数从c1减半到c_
  3. 定义第二个卷积层cv2,它将隐藏通道数c_乘以4后,输出通道数为c2
  4. 定义最大池化层m,使用核大小为k,步幅为1,填充为k // 2。这个池化层用于进行空间金字塔池化。

forward方法中,执行前向传播操作。具体步骤如下:

  1. 将输入特征图x通过第一个卷积层cv1进行处理。
  2. 对处理后的特征图x进行一次最大池化,得到y1
  3. y1进行第二次最大池化,得到y2
  4. xy1y2y2的第三次最大池化结果在通道维度上拼接起来。
  5. 将拼接后的结果通过第二个卷积层cv2进行处理,得到最终的输出特征图。

SPPF层的作用是在不同尺度上对输入特征图进行池化,从而捕捉多尺度的上下文信息,并将这些特征图进行融合,提供更丰富的特征表示给后续的网络层。

Detect

  • 功能:在检测模型中进行目标检测
  • 参数:nc:目标类别的数量,ch:每个检测层的通道数。
class Detect(nn.Module):
    """YOLOv8 Detect head for detection models."""
    dynamic = False  # force grid reconstruction
    export = False  # export mode
    shape = None
    anchors = torch.empty(0)  # init
    strides = torch.empty(0)  # init

    def __init__(self, nc=80, ch=()):  # detection layer
        super().__init__()
        self.nc = nc  # number of classes
        self.nl = len(ch)  # number of detection layers
        self.reg_max = 16  # DFL channels (ch[0] // 16 to scale 4/8/12/16/20 for n/s/m/l/x)
        self.no = nc + self.reg_max * 4  # number of outputs per anchor
        self.stride = torch.zeros(self.nl)  # strides computed during build
        c2, c3 = max((16, ch[0] // 4, self.reg_max * 4)), max(ch[0], self.nc)  # channels
        self.cv2 = nn.ModuleList(
            nn.Sequential(Conv(x, c2, 3), Conv(c2, c2, 3), nn.Conv2d(c2, 4 * self.reg_max, 1)) for x in ch)
        self.cv3 = nn.ModuleList(nn.Sequential(Conv(x, c3, 3), Conv(c3, c3, 3), nn.Conv2d(c3, self.nc, 1)) for x in ch)
        self.dfl = DFL(self.reg_max) if self.reg_max > 1 else nn.Identity()

    def forward(self, x):
        """Concatenates and returns predicted bounding boxes and class probabilities."""
        shape = x[0].shape  # BCHW
        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

        x_cat = torch.cat([xi.view(shape[0], self.no, -1) for xi in x], 2)
        if self.export and self.format in ('saved_model', 'pb', 'tflite', 'edgetpu', 'tfjs'):  # avoid TF FlexSplitV ops
            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)
        dbox = dist2bbox(self.dfl(box), self.anchors.unsqueeze(0), xywh=True, dim=1) * self.strides
        y = torch.cat((dbox, cls.sigmoid()), 1)
        return y if self.export else (y, x)

    def bias_init(self):
        """Initialize Detect() biases, WARNING: requires stride availability."""
        m = self  # self.model[-1]  # Detect() module
        # cf = torch.bincount(torch.tensor(np.concatenate(dataset.labels, 0)[:, 0]).long(), minlength=nc) + 1
        # ncf = math.log(0.6 / (m.nc - 0.999999)) if cf is None else torch.log(cf / cf.sum())  # nominal class frequency
        for a, b, s in zip(m.cv2, m.cv3, m.stride):  # from
            a[-1].bias.data[:] = 1.0  # box
            b[-1].bias.data[:m.nc] = math.log(5 / m.nc / (640 / s) ** 2)  # cls (.01 objects, 80 classes, 640 img)

__init__方法中,定义了Detect层的结构。具体步骤如下:

  1. 初始化一些参数,包括目标类别的数量nc,检测层的数量nl,以及一些与Dense Fusion Layer(DFL)相关的参数。
  2. 定义了cv2cv3两个ModuleList,分别用于处理检测层的输出,将其转换为预测的边界框和类别概率。
  3. 如果存在DFL(即reg_max > 1),则初始化DFL模块。

forward方法中,首先将每个检测层的输出通过对应的卷积层进行处理,得到预测的边界框和类别概率。

  1. 如果处于训练模式,则直接返回预测结果。
  2. 如果处于动态模式或输入形状发生变化,则根据输入特征图的形状计算锚框和步长,并更新self.anchorsself.strides
  3. 将预测结果进行拼接和分割,得到边界框和类别概率。
  4. 将边界框进行转换(dist2bbox)并乘以步长,得到最终的边界框坐标。
  5. 将边界框坐标和类别概率进行拼接,并返回预测结果。
  6. 如果处于导出模式且输出格式为TF相关格式,则返回拼接后的预测结果和原始的检测层输出。

bias_init方法用于初始化Detect层的偏置项,根据设置的步长进行初始化。

Detect层的作用是将检测层的输出转换为预测的边界框和类别概率,并进行后处理操作,如转换边界框坐标、应用DFL等。

Dense Fusion Layer(DFL)是YOLOv8中引入的一种特征融合机制,用于改进目标检测的性能。DFL主要用于处理检测头部(Detect)中生成的边界框的位置信息。

在YOLOv8中,每个边界框的位置信息通常由四个偏移量(dx, dy, dw, dh)表示,分别对应于边界框的中心坐标和宽度高度的缩放系数。然而,由于目标在不同尺度和长宽比下的变化,简单的线性回归方式可能无法准确地预测边界框的位置。

DFL通过引入一个全连接层(FC)来学习边界框位置的非线性映射,以提高位置预测的准确性。这个全连接层在Detect头部的最后一层卷积层之后,并且其输入维度为4 * reg_max,其中reg_max表示每个边界框位置的通道数。DFL的输出维度仍然为4,与原始边界框的位置偏移量相同。

在YOLOv8的训练过程中,DFL会与真实边界框位置之间的差异(即损失)被反向传播,用于优化DFL的参数,使得DFL能够更准确地预测边界框的位置。

通过引入DFL,YOLOv8能够更好地适应不同尺度和长宽比下的目标,并提高目标检测的精度和鲁棒性。

在YOLOv8中,有一些参数用于控制网络的行为和输出的形式。下面是这些参数的解释:

  • dynamic: 控制是否进行动态网格重构。当dynamicTrue时,网格将根据输入图像的形状进行重构,以适应不同大小的输入。当dynamicFalse时,网格将使用固定的大小,不会根据输入进行调整。
  • export: 控制是否处于导出模式。当exportTrue时,网络将生成适用于导出到其他平台或部署到生产环境的模型。在导出模式下,可能会应用一些特定的优化和约束。
  • shape: 记录输入图像的形状。在每次前向传播时,将检查输入图像的形状是否与存储在shape中的形状相同。如果形状不同,可能会重新计算网格。
  • anchors: 存储锚框的张量。初始化时,anchors被初始化为一个空的张量。在运行时,根据输入图像的形状和网络结构生成相应的锚框,并存储在anchors中。
  • strides: 存储每个检测层的步长(stride)。初始化时,strides被初始化为空张量。在网络构建过程中,根据网络结构的设计计算并存储每个检测层的步长。

这些参数的设置和使用可以根据具体的应用和需求进行调整,以控制网络的行为和输出的形式。

网络基本介绍 目标检测 深度学习 人工智能 YOLO
Theme Jasmine by Kent Liao