YOLO
作为目标检测领域的常青树,如今以及更新到了YOLOv10
,并且还有YOLOX
、YOLOS
等变体,可以说该系列已经在目标检测领域占据了半壁江山,如今,YOLOv8
的发行者ultralytics
竟有一统江山之意,其在提出的框架中不但集成了v3
到v10
的YOLO
目标检测模型,还包揽了分类,语义分割、目标追踪和姿态估计等计算机视觉任务。
那么,今天我们就来看看YOLOv8
是如何将这些计算机视觉任务融合在一起吧
其实从思路上很简单,YOLOv8
继续沿用了YOLO
的基本架构,即将整个模型分为特征提取骨干网络(Backbone
),用于进行特征提取,特征融合网络(Neck
),用于融合提取的特征信息以及最后的检测输出模块(Head
),由于前面的Backbone
与Neck
已经完成了特提取与特征融合的功能,而最后的输出头其实就是根据不同任务所设计的,因此,要想让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_grad
为false
,得到的这个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推断结果可视化