CV党福音:YOLOv8实现分类

YOLO作为目标检测领域的常青树,如今以及更新到了YOLOv10,并且还有YOLOXYOLOS等变体,可以说该系列已经在目标检测领域占据了半壁江山,如今,YOLOv8的发行者ultralytics竟有一统江山之意,其在提出的框架中不但集成了v3v10YOLO目标检测模型,还包揽了分类,语义分割、目标追踪和姿态估计等计算机视觉任务。

那么,今天我们就来看看YOLOv8是如何将这些计算机视觉任务融合在一起吧

其实从思路上很简单,YOLOv8继续沿用了YOLO的基本架构,即将整个模型分为特征提取骨干网络(Backbone),用于进行特征提取,特征融合网络(Neck),用于融合提取的特征信息以及最后的检测输出模块(Head),由于前面的BackboneNeck已经完成了特提取与特征融合的功能,而最后的输出头其实就是根据不同任务所设计的,因此,要想让YOLOv8具备分类、分割以及姿态估计的功能,只需要将我们的检测头(Detect)替换为相应的分类头、分割头以及姿态估计头即可,当然,相应的还要替换损失函数与数据集标签。

YOLOv8模型结构如下:

目标检测模型

那么,我们接下来便来看看YOLOv8是如何去替换的:

首先是模型结构,博主以分类为例,要修改模型结构,只需要修改对应的yaml文件即可,这里博主推荐可以参考这篇文章:YOLOv8模型yaml结构图理解(逐层分析)

YOLOv8的模型结构如下:

python 复制代码
# Ultralytics YOLO 🚀, AGPL-3.0 license
# YOLOv8 object detection model with P3-P5 outputs. For Usage examples see https://docs.ultralytics.com/tasks/detect
# Parameters
nc: 80  # 类别数目,nc代表"number of classes",即模型用于检测的对象类别总数。
scales: # 模型复合缩放常数,例如 'model=yolov8n.yaml' 将调用带有 'n' 缩放的 yolov8.yaml
  # [depth, width, max_channels]
  n: [0.33, 0.25, 1024]  # YOLOv8n概览:225层, 3157200参数, 3157184梯度, 8.9 GFLOPs
  s: [0.33, 0.50, 1024]  # YOLOv8s概览:225层, 11166560参数, 11166544梯度, 28.8 GFLOPs
  m: [0.67, 0.75, 768]   # YOLOv8m概览:295层, 25902640参数, 25902624梯度, 79.3 GFLOPs
  l: [1.00, 1.00, 512]   # YOLOv8l概览:365层, 43691520参数, 43691504梯度, 165.7 GFLOPs
  x: [1.00, 1.25, 512]   # YOLOv8x概览:365层, 68229648参数, 68229632梯度, 258.5 GFLOPs
# YOLOv8.0n backbone 骨干层
backbone:
  # [from, repeats, module, args]
  - [-1, 1, Conv, [64, 3, 2]]  # 0-P1/2 第0层,-1代表将上层的输入作为本层的输入。第0层的输入是640*640*3的图像。Conv代表卷积层,相应的参数:64代表输出通道数,3代表卷积核大小k,2代表stride步长。
  - [-1, 1, Conv, [128, 3, 2]]  # 1-P2/4 第1层,本层和上一层是一样的操作(128代表输出通道数,3代表卷积核大小k,2代表stride步长)
  - [-1, 3, C2f, [128, True]] # 第2层,本层是C2f模块,3代表本层重复3次。128代表输出通道数,True表示Bottleneck有shortcut。
  - [-1, 1, Conv, [256, 3, 2]]  # 3-P3/8 第3层,进行卷积操作(256代表输出通道数,3代表卷积核大小k,2代表stride步长),输出特征图尺寸为80*80*256(卷积的参数都没变,所以都是长宽变成原来的1/2,和之前一样),特征图的长宽已经变成输入图像的1/8。
  - [-1, 6, C2f, [256, True]] # 第4层,本层是C2f模块,可以参考第2层的讲解。6代表本层重复6次。256代表输出通道数,True表示Bottleneck有shortcut。经过这层之后,特征图尺寸依旧是80*80*256。
  - [-1, 1, Conv, [512, 3, 2]]  # 5-P4/16 第5层,进行卷积操作(512代表输出通道数,3代表卷积核大小k,2代表stride步长),输出特征图尺寸为40*40*512(卷积的参数都没变,所以都是长宽变成原来的1/2,和之前一样),特征图的长宽已经变成输入图像的1/16。
  - [-1, 6, C2f, [512, True]] # 第6层,本层是C2f模块,可以参考第2层的讲解。6代表本层重复6次。512代表输出通道数,True表示Bottleneck有shortcut。经过这层之后,特征图尺寸依旧是40*40*512。
  - [-1, 1, Conv, [1024, 3, 2]]  # 7-P5/32 第7层,进行卷积操作(1024代表输出通道数,3代表卷积核大小k,2代表stride步长),输出特征图尺寸为20*20*1024(卷积的参数都没变,所以都是长宽变成原来的1/2,和之前一样),特征图的长宽已经变成输入图像的1/32。
  - [-1, 3, C2f, [1024, True]] #第8层,本层是C2f模块,可以参考第2层的讲解。3代表本层重复3次。1024代表输出通道数,True表示Bottleneck有shortcut。经过这层之后,特征图尺寸依旧是20*20*1024。
  - [-1, 1, SPPF, [1024, 5]]  # 9 第9层,本层是快速空间金字塔池化层(SPPF)。1024代表输出通道数,5代表池化核大小k。结合模块结构图和代码可以看出,最后concat得到的特征图尺寸是20*20*(512*4),经过一次Conv得到20*20*1024。
# YOLOv8.0n head 头部层
head:
  - [-1, 1, nn.Upsample, [None, 2, 'nearest']] # 第10层,本层是上采样层。-1代表将上层的输出作为本层的输入。None代表上采样的size(输出尺寸)不指定。2代表scale_factor=2,表示输出的尺寸是输入尺寸的2倍。nearest代表使用的上采样算法为最近邻插值算法。经过这层之后,特征图的长和宽变成原来的两倍,通道数不变,所以最终尺寸为40*40*1024。
  - [[-1, 6], 1, Concat, [1]]  # cat backbone P4 第11层,本层是concat层,[-1, 6]代表将上层和第6层的输出作为本层的输入。[1]代表concat拼接的维度是1。从上面的分析可知,上层的输出尺寸是40*40*1024,第6层的输出是40*40*512,最终本层的输出尺寸为40*40*1536。
  - [-1, 3, C2f, [512]]  # 12 第12层,本层是C2f模块,可以参考第2层的讲解。3代表本层重复3次。512代表输出通道数。与Backbone中C2f不同的是,此处的C2f的bottleneck模块的shortcut=False。
  - [-1, 1, nn.Upsample, [None, 2, 'nearest']] # 第13层,本层也是上采样层(参考第10层)。经过这层之后,特征图的长和宽变成原来的两倍,通道数不变,所以最终尺寸为80*80*512。
  - [[-1, 4], 1, Concat, [1]]  # cat backbone P3 第14层,本层是concat层,[-1, 4]代表将上层和第4层的输出作为本层的输入。[1]代表concat拼接的维度是1。从上面的分析可知,上层的输出尺寸是80*80*512,第6层的输出是80*80*256,最终本层的输出尺寸为80*80*768。
  - [-1, 3, C2f, [256]]  # 15 (P3/8-small) 第15层,本层是C2f模块,可以参考第2层的讲解。3代表本层重复3次。256代表输出通道数。经过这层之后,特征图尺寸变为80*80*256,特征图的长宽已经变成输入图像的1/8。
  - [-1, 1, Conv, [256, 3, 2]] # 第16层,进行卷积操作(256代表输出通道数,3代表卷积核大小k,2代表stride步长),输出特征图尺寸为40*40*256(卷积的参数都没变,所以都是长宽变成原来的1/2,和之前一样)。
  - [[-1, 12], 1, Concat, [1]]  # cat head P4 第17层,本层是concat层,[-1, 12]代表将上层和第12层的输出作为本层的输入。[1]代表concat拼接的维度是1。从上面的分析可知,上层的输出尺寸是40*40*256,第12层的输出是40*40*512,最终本层的输出尺寸为40*40*768。
  - [-1, 3, C2f, [512]]  # 18 (P4/16-medium) 第18层,本层是C2f模块,可以参考第2层的讲解。3代表本层重复3次。512代表输出通道数。经过这层之后,特征图尺寸变为40*40*512,特征图的长宽已经变成输入图像的1/16。
  - [-1, 1, Conv, [512, 3, 2]] # 第19层,进行卷积操作(512代表输出通道数,3代表卷积核大小k,2代表stride步长),输出特征图尺寸为20*20*512(卷积的参数都没变,所以都是长宽变成原来的1/2,和之前一样)。
  - [[-1, 9], 1, Concat, [1]]  # cat head P5 第20层,本层是concat层,[-1, 9]代表将上层和第9层的输出作为本层的输入。[1]代表concat拼接的维度是1。从上面的分析可知,上层的输出尺寸是20*20*512,第9层的输出是20*20*1024,最终本层的输出尺寸为20*20*1536。
  - [-1, 3, C2f, [1024]]  # 21 (P5/32-large) 第21层,本层是C2f模块,可以参考第2层的讲解。3代表本层重复3次。1024代表输出通道数。经过这层之后,特征图尺寸变为20*20*1024,特征图的长宽已经变成输入图像的1/32。
  - [[15, 18, 21], 1, Detect, [nc]]  # Detect(P3, P4, P5) 第20层,本层是Detect层,[15, 18, 21]代表将第15、18、21层的输出(分别是80*80*256、40*40*512、20*20*1024)作为本层的输入。nc是数据集的类别数。

分类模型结构

接下来,我们看一下YOLOv8用于分类的模型结构,可以看到,除了最后的Head模块发生了改变,Backbone部分并没有明显改动(分类的Backbone还是去掉了SPPF模块的),并且,分类直接去掉了特征融合模块(Neck),直接连接了分类头,这说明分类相较于检测更简单些。

python 复制代码
nc: 1000 # number of classes
scales: # model compound scaling constants, i.e. 'model=yolov8n-cls.yaml' will call yolov8-cls.yaml with scale 'n'
  # [depth, width, max_channels]
  n: [0.33, 0.25, 1024]
  s: [0.33, 0.50, 1024]
  m: [0.67, 0.75, 1024]
  l: [1.00, 1.00, 1024]
  x: [1.00, 1.25, 1024]

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

# YOLOv8.0n head
head:
  - [-1, 1, Classify, [nc]] # Classify

具体的,可以通过分类头的代码来查看其输出的结果:\ultralytics\nn\modules\head.py

python 复制代码
class Classify(nn.Module):
    """YOLOv8 classification head, i.e. x(b,c1,20,20) to x(b,c2)."""

    def __init__(self, c1, c2, k=1, s=1, p=None, g=1):
        """Initializes YOLOv8 classification head with specified input and output channels, kernel size, stride,
        padding, and groups.
        """
        super().__init__()
        c_ = 1280  # efficientnet_b0 size
        self.conv = Conv(c1, c_, k, s, p, g)
        self.pool = nn.AdaptiveAvgPool2d(1)  # to x(b,c_,1,1)
        self.drop = nn.Dropout(p=0.0, inplace=True)
        self.linear = nn.Linear(c_, c2)  # to x(b,c2)

    def forward(self, x):
        """Performs a forward pass of the YOLO model on input image data."""
        if isinstance(x, list):
            x = torch.cat(x, 1)
        x = self.linear(self.drop(self.pool(self.conv(x)).flatten(1)))
        return x if self.training else x.softmax(1)

Debug可知,其输入到分类头的数据维度为torch.Size([1, 256, 7, 7]),分类头结构如下,可以看到,其最终输出结果的维度为(1,1000),对应1000个类别。

python 复制代码
Classify(
  (conv): Conv(
    (conv): Conv2d(256, 1280, kernel_size=(1, 1), stride=(1, 1))
    (act): SiLU(inplace=True)
  )
  (pool): AdaptiveAvgPool2d(output_size=1)
  (drop): Dropout(p=0.0, inplace=True)
  (linear): Linear(in_features=1280, out_features=1000, bias=True)
)

分类推理

python 复制代码
from ultralytics import YOLO
model = YOLO("yolov8n-cls.pt")  # load an official model
model.predict("image.jpg",save=True)

YOLO模型能够根据pt文件来获取当然模型所进行的任务,pt文件中包含任务类型task,模型的yaml文件,predict方法也因此可以根据其任务类型选择不同的推理形式,结果如下:其分类为泰迪,应该是泰迪熊的意思,当然它应该是分错的,人家明明是猫好吧。

分类训练

分类开始训练代码如下,当然这里可以直接传入YOLO8n-cls的预训练模型,因为pt文件包含这些yaml文件内容的

python 复制代码
from ultralytics import YOLO
if __name__ == '__main__':
# 代码
    model = YOLO("ultralytics\cfg\models/v8\yolov8-cls.yaml").load("yolov8n-cls.pt")  # build from YAML and transfer weights
    results = model.train(data="imagenette160", epochs=100, imgsz=64)

此外,需要更改的便是训练过程中的数据集与损失函数了

分类损失定义在:\ultralytics\utils\loss.py

python 复制代码
class v8ClassificationLoss:
    """Criterion class for computing training losses."""
    def __call__(self, preds, batch):
        """Compute the classification loss between predictions and true labels."""
        loss = F.cross_entropy(preds, batch["cls"], reduction="mean")
        loss_items = loss.detach()
        return loss, loss_items

batch即真实类别标签,由于batch=16,因此共有16个标签,数字即对应的类别索引,preds为预测结果,维度为(16,10),即有16个预测结果,10为类别格式,其结果为16张图像的各个类别得分。

loss.detach()返回一个新的tensor,从当前计算图中分离下来的,但是仍指向原变量的存放位置,不同之处只是requires_gradfalse,得到的这个tensor永远不需要计算其梯度,不具有grad

最终的分类训练结果

F.cross_entropy函数是torch提供的用于求交叉熵损失函数的工具包,其用法为:F.cross_entropy(input, target)

交叉熵公式:

其中P为真实值,Q 为预测值。
计算交叉熵的详细步骤:

①将predict_scores进行softmax运算,将运算结果记为pred_scores_soft

②将pred_scores_soft进行log运算,将运算结果记为pred_scores_soft_log

③将pred_scores_soft_log与真实值进行计算处理。

思路即:

scores→softmax→log→compute

计算案例如下:

分类评价指标

accuracy_top-1

就是你预测的label取最后概率向量里面最大的那一个作为预测结果,如果你的预测结果中概率最大的那个分类正确,则预测正确。否则预测错误
accuracy_top-5

就是最后概率向量最大的前五名中,只要出现了正确概率即为预测正确。否则预测错误。

由此可以看出,top5一般比top1

与目标检测一样,YOLOv8的分类结果也会被保存,如下:


混淆矩阵

在机器学习领域,混淆矩阵(Confusion Matrix),又称为可能性矩阵或错误矩阵。混淆矩阵是可视化工具,特别用于监督学习,在无监督学习一般叫做匹配矩阵。在图像精度评价中,主要用于比较分类结果和实际测得值,可以把分类结果的精度显示在一个混淆矩阵里面。

混淆矩阵要表达的含义:

混淆矩阵的每一列代表了预测类别,每一列的总数表示预测为该类别的数据的数目;

每一行代表了数据的真实归属类别,每一行的数据总数表示该类别的数据实例的数目;每一列中的数值表示真实数据被预测为该类的数目。


batch推断结果可视化

相关推荐
Kenneth風车2 小时前
【机器学习(九)】分类和回归任务-多层感知机(Multilayer Perceptron,MLP)算法-Sentosa_DSML社区版 (1)111
算法·机器学习·分类
LL.。6 小时前
目标检测——基于yolov8和pyqt的螺栓松动检测系统
yolo·目标检测·pyqt
沅_Yuan12 小时前
基于GRU门控循环神经网络的多分类预测【MATLAB】
matlab·分类·gru
IT古董13 小时前
【机器学习】机器学习的基本分类-强化学习-Actor-Critic 方法
人工智能·机器学习·分类
martian66513 小时前
【人工智能数学基础】——深入详解贝叶斯理论:掌握贝叶斯定理及其在分类和预测中的应用
人工智能·数学·分类·数据挖掘·贝叶斯
Dneccc13 小时前
YOLO模型格式转换:pt -> onnx -> rknn
yolo
CountingStars6192 天前
目标检测常用评估指标(metrics)
人工智能·目标检测·目标跟踪
今天炼丹了吗2 天前
YOLOv11融合[ECCV2024]FADformer中的FFCM模块
yolo
醒了就刷牙2 天前
transformer用作分类任务
深度学习·分类·transformer
四口鲸鱼爱吃盐2 天前
Pytorch | 从零构建ParNet/Non-Deep Networks对CIFAR10进行分类
人工智能·pytorch·分类