介绍
YOLOv8 算法的核心特性可以归结为如下:
- Backbone:使用的依旧是CSP的思想,将YOLOv5中的C3模块被替换成了C2f模块,实现了进一步的轻量化,同时YOLOv8依旧使用了YOLOv5等架构中使用的SPPF模块;
- Head: Head部分较YOLOv5而言有两大改进:1)换成了目前主流的解耦头结构(Decoupled-Head),将分类和检测头分离 2)同时也从 Anchor-Based 换成了 Anchor-Free;
- Loss :YOLOv8抛弃了以往的IOU匹配或者单边比例的分配方式,而是使用了Task-Aligned Assigner正负样本匹配方式。并引入了 Distribution Focal Loss(DFL),整体使用DFL Loss+CIOU Loss作为分类损失;
-
Anchor-Free : YOLOv8 抛弃了以往的 Anchor-Base ,使用了 Anchor-Free 的思想;
- Train: 训练的数据增强部分引入了 YOLOX 中的最后 10 epoch 关闭 Mosiac 增强的操作,可以有效地提升精度;
模型部分解析
C3与C2F
class C3(nn.Module):
# CSP Bottleneck with 3 convolutions
def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): # ch_in,
ch_out, number, shortcut, groups, 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, k=((1, 1), (3,
3)), e=1.0) for _ in range(n)))
def forward(self, x):
return self.cv3(torch.cat((self.m(self.cv1(x)), self.cv2(x)), 1))
接着我们讲解C2F模块,C2F模块旨在从不同层次的特征图中提取信息,并将这些信息进行融合,以便提高目标检测的性能。具体来说,C2F模块包含一个跨阶段的特征融合操作,通过将来自不同层的特征图相加,实现特征的融合。这有助于网络更好地理解不同尺度的特征,并提高目标检测的准确性和鲁棒性,其结构图如下图所示,这个过程的描述如下:首先,输入特征经过一个卷积层,然后使用chunk函数将输出特征平均拆分成两个向量,并将它们保存到一个列表中。接着,列表中的后半部分向量被输入到一个Bottleneck Block中,该Block包含n个Bottleneck模块。每个Bottleneck模块的输出都被追加到列表中,最终列表中包含n个Bottleneck模块的输出。这些输出被拼接起来,形成一个输出向量。最后,输出向量经过一个卷积层,输出大小为h×w×c_out。
autopad
autopad函数用于计算填充的大小,以使得经过填充后的输出张量大小保持不变。该函数接受三个参数:
- k:卷积核的大小,可以是一个整数或一个序列。
p
:填充的大小,默认为None。d
:扩张率的大小,默认为1。普通卷积的扩张率为1,而空洞卷积的扩张率大于1。
autopad函数代码如下所示:
def autopad(k, p=None, d=1): # kernel(卷积核), padding(填充), dilation(扩张)
# 返回pad的大小,使得padding后输出张量的shape不变
if d > 1: # 如果采用扩张卷积,则计算扩张后实际的kernel大小
k = d * (k - 1) + 1 if isinstance(k, int) else [d * (x - 1) + 1 for x in k] #
if p is None:
p = k // 2 if isinstance(k, int) else [x // 2 for x in k] # 自动pad
return p
Conv
在YOLOv8中,Conv作为卷积层(Convolutional Layer)是通过卷积操作对输入特征图进行处理的一种神经网络层。卷积操作可以看作是一个滤波器,在特征图上滑动并对每个位置的像素值进行加权求和,从而生成输出特征图。卷积操作可以有效地提取图像中的局部特征,例如边缘、纹理等,从而帮助网络理解图像内容,具体代码如下所示:
class Conv(nn.Module):
# 标准的卷积 参数(输入通道数, 输出通道数, 卷积核大小, 步长, 填充, 组, 扩张, 激活函数)
default_act = nn.SiLU() # 默认的激活函数
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) # 2维卷积,其中采用了自动填充函数。
self.bn = nn.BatchNorm2d(c2) # 使得每一个batch的特征图均满足均值为0,方差为1的分布规律
# 如果act=True 则采用默认的激活函数SiLU;如果act的类型是nn.Module,则采用传入的act; 否则不采取任何动作 (nn.Identity函数相当于f(x)=x,只用做占位,返回原始的输入)。
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))) # 采用BatchNorm
def forward_fuse(self, x): # 用于Model类的fuse函数融合 Conv + BN 加速推理,一般用于测试/验证阶段
return self.act(self.conv(x)) # 不采用BatchNorm
ConvTranspose
ConvTranspose(转置卷积)层是卷积神经网络中的一种操作,也称为反卷积。它与普通的卷积操作相反,用于将输入特征图放大(上采样)并生成更大尺寸的输出特征图。转置卷积通过学习可训练的参数(称为卷积核或滤波器)来实现这一目的。转置卷积的工作原理是在输入特征图中的每个像素周围填充零,并对其进行卷积操作。这使得输出特征图的尺寸比输入特征图大,其代码如下所示:
class ConvTranspose(nn.Module):
# Convolution transpose 2d layer
default_act = nn.SiLU() # default activation
def __init__(self, c1, c2, k=2, s=2, p=0, bn=True, act=True):
super().__init__()
self.conv_transpose = nn.ConvTranspose2d(c1, c2, k, s, p, bias=not bn)
self.bn = nn.BatchNorm2d(c2) if bn else nn.Identity()
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_transpose(x)))
DWConv
DWConv指深度可分离卷积(Depthwise Separable Convolution),是一种轻量级的卷积操作,常用于深度学习模型中以减少参数数量和计算量。深度可分离卷积由两部分组成:深度卷积(Depthwise Convolution)和逐点卷积(Pointwise Convolution)。
-
深度卷积:对输入的每个通道分别应用一个独立的卷积核。如果输入有C个通道,那么将会有C个卷积核,每个卷积核只与对应通道的数据进行卷积操作。
-
逐点卷积:对深度卷积的输出应用1x1的卷积核,目的是将通道数减少到所需的数量,以减少计算量。
详细代码如下所示:
class DWConv(Conv):
# 深度可分离卷积
def __init__(self, c1, c2, k=1, s=1, d=1, act=True): # ch_in, ch_out, kernel, stride, dilation, activation
super().__init__(c1, c2, k, s, g=math.gcd(c1, c2), d=d, act=act)
DWConvTranspose2d
DWConvTranspose2d指深度可分离转置卷积(Depthwise Separable Transpose Convolution),用于上采样(放大)特征图。深度可分离转置卷积结合了深度可分离卷积和转置卷积的特性,以减少参数数量和计算量。
深度可分离转置卷积包括两个步骤:
-
深度可分离卷积(Depthwise Separable Convolution):首先,对输入的每个通道分别应用一个独立的卷积核(深度卷积)。这一步与常规的深度可分离卷积操作相同。
-
转置卷积(Transpose Convolution):然后,对深度可分离卷积的输出进行逐点卷积(逐点卷积)。逐点卷积使用1x1的卷积核对每个通道的输出进行线性变换和融合,以恢复输出特征图的尺寸。
详细代码如下所示:
class DWConvTranspose2d(nn.ConvTranspose2d):
# Depth-wise transpose convolution
def __init__(self, c1, c2, k=1, s=1, p1=0, p2=0): # 输入通道, 输出通道, 卷积核大小, 步长, padding, padding_out
super().__init__(c1, c2, k, s, p1, p2, groups=math.gcd(c1, c2))
Bottleneck
Bottleneck的设计旨在减少模型的参数数量和计算量,同时提高网络的表达能力,先使用 3x3 卷积降维,剔除冗余信息,再使用 3×3 卷积升维,其具体代码如下:
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) # 输入通道: c1, 输出通道:c_ , 卷积核:3x3, 步长1
self.cv2 = Conv(c_, c2, k[1], 1, g=g) # 输入通道:c_ , 输出通道c2, 卷积核:3x3, 步长1
self.add = shortcut and c1 == c2 # 当传入的shortcut参数为true,且c1和c2相等时,则使用残差连接。
def forward(self, x):
return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))
BottleneckCSP
BottleneckCSP结合了Bottleneck和Cross Stage Partial连接(CSP),使得其可以在保持模型轻量化的同时,提高网络的感知能力和特征表达能力,代码如下:
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): # ch_in, ch_out, number, shortcut, groups, expansion
super().__init__()
c_ = int(c2 * e) # hidden channels
# 输出x的大小是(b,c1,w,h)
self.cv1 = Conv(c1, c_, 1, 1) # cv1的大小为(b,c_,w,h)
self.cv2 = nn.Conv2d(c1, c_, 1, 1, bias=False) # cv2的大小为(b,c_,w,h)
self.cv3 = nn.Conv2d(c_, c_, 1, 1, bias=False) # m通过Conv2d,变成cv3,大小是(b,c_,w,h)
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)))
# cv1通过n个串联的bottleneck,变成m,大小为(b,c_,w,h)
def forward(self, x):
y1 = self.cv3(self.m(self.cv1(x))) # (b,c_,w,h)
y2 = self.cv2(x) # (b,c_,w,h)
return self.cv4(self.act(self.bn(torch.cat((y1, y2), 1))))
# cat后:(b,2*c_,w,h) 返回cv4: (b,c2,w,h)
CBAM
CBAM模块通常由两个子模块组成:通道注意力模块(Channel Attention Module)和空间注意力模块(Spatial Attention Module)。
-
通道注意力模块:用于对每个通道的特征图进行加权,以提取重要的通道信息。通道注意力模块通过计算每个通道的全局平均池化和全局最大池化,然后经过一系列全连接层和激活函数,最终产生一个通道注意力权重向量,用于加权每个通道的特征图。
-
空间注意力模块:用于对特征图的空间位置进行加权,以提取重要的空间信息。空间注意力模块通过对特征图进行一系列卷积操作和激活函数,最终产生一个空间注意力权重图,用于加权特征图的每个空间位置。
CBAM模块将通道注意力和空间注意力结合起来,通过对特征图的通道和空间位置进行加权,提高了网络对重要特征的关注程度,从而增强了网络的表达能力和泛化能力,代码如下:
class CBAM(nn.Module):
# Convolutional Block Attention Module
def __init__(self, c1, kernel_size=7): # ch_in, kernels
super().__init__()
self.channel_attention = ChannelAttention(c1)
self.spatial_attention = SpatialAttention(kernel_size)
def forward(self, x):
return self.spatial_attention(self.channel_attention(x))
结语
后期将会对这篇文章进行进一步完善,同时更新YOLOv8实现教程。