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_in
、ch_out
、kernel
、stride
、padding
、groups
、dilation
、activation
)来定义卷积层的行为。
- 在
Conv
类的初始化方法__init__
中,首先调用了父类nn.Module
的初始化方法super().__init__()
。 - 使用
nn.Conv2d
创建了一个卷积层self.conv
,其中包括输入通道数c1
、输出通道数c2
、卷积核大小k
、步长s
、填充p
、分组数g
、膨胀率d
、偏置bias
等参数。 - 创建了批归一化层
self.bn
,用于对卷积结果进行归一化处理。 - 根据
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 模块的主要功能如下:
- 通过一个 1x1 卷积层将输入特征的通道数进行压缩,将输入通道数减少到较小的值(c1 到 c_)。
- 经过一个具有指定内核大小(k[0])的卷积层对压缩后的特征进行卷积操作,用于提取特征信息。
- 经过一个具有指定内核大小(k[1])的卷积层将通道数扩展回原来的大小(c_ 到 c2)。
- 如果设置了残差连接(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层的结构。具体步骤如下:
- 使用
Conv
模块将输入特征图的通道数从c1
减半,得到隐藏通道数c_
。 - 定义第一个卷积层
cv1
,它将输入特征图的通道数从c1
减半到c_
。 - 定义第二个卷积层
cv2
,它将隐藏通道数c_
乘以4后,输出通道数为c2
。 - 定义最大池化层
m
,使用核大小为k
,步幅为1,填充为k // 2
。这个池化层用于进行空间金字塔池化。
在forward
方法中,执行前向传播操作。具体步骤如下:
- 将输入特征图
x
通过第一个卷积层cv1
进行处理。 - 对处理后的特征图
x
进行一次最大池化,得到y1
。 - 对
y1
进行第二次最大池化,得到y2
。 - 将
x
、y1
、y2
和y2
的第三次最大池化结果在通道维度上拼接起来。 - 将拼接后的结果通过第二个卷积层
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层的结构。具体步骤如下:
- 初始化一些参数,包括目标类别的数量
nc
,检测层的数量nl
,以及一些与Dense Fusion Layer(DFL)相关的参数。 - 定义了
cv2
和cv3
两个ModuleList,分别用于处理检测层的输出,将其转换为预测的边界框和类别概率。 - 如果存在DFL(即
reg_max > 1
),则初始化DFL模块。
在forward
方法中,首先将每个检测层的输出通过对应的卷积层进行处理,得到预测的边界框和类别概率。
- 如果处于训练模式,则直接返回预测结果。
- 如果处于动态模式或输入形状发生变化,则根据输入特征图的形状计算锚框和步长,并更新
self.anchors
和self.strides
。 - 将预测结果进行拼接和分割,得到边界框和类别概率。
- 将边界框进行转换(dist2bbox)并乘以步长,得到最终的边界框坐标。
- 将边界框坐标和类别概率进行拼接,并返回预测结果。
- 如果处于导出模式且输出格式为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
: 控制是否进行动态网格重构。当dynamic
为True
时,网格将根据输入图像的形状进行重构,以适应不同大小的输入。当dynamic
为False
时,网格将使用固定的大小,不会根据输入进行调整。export
: 控制是否处于导出模式。当export
为True
时,网络将生成适用于导出到其他平台或部署到生产环境的模型。在导出模式下,可能会应用一些特定的优化和约束。shape
: 记录输入图像的形状。在每次前向传播时,将检查输入图像的形状是否与存储在shape
中的形状相同。如果形状不同,可能会重新计算网格。anchors
: 存储锚框的张量。初始化时,anchors
被初始化为一个空的张量。在运行时,根据输入图像的形状和网络结构生成相应的锚框,并存储在anchors
中。strides
: 存储每个检测层的步长(stride)。初始化时,strides
被初始化为空张量。在网络构建过程中,根据网络结构的设计计算并存储每个检测层的步长。这些参数的设置和使用可以根据具体的应用和需求进行调整,以控制网络的行为和输出的形式。