机器视觉 - YoloV8 的一些理解
理解权重和偏置是什么
- 全连接层: 输出=f(输入*权重+偏置), 权重是这个线性函数的一次性系数, 偏置是常数项.
- 卷积层: 输出=权重卷积核和输入做卷积运算, 这里的权重是卷积核的各个元素, 卷积核也经常被叫做过滤器filter或kernel. 传统的图像处理, 图像滤波器算子需要人工指定, 比如高斯滤波器, 在深度学习中, 卷积核是通过训练学习求得的.
- 权重和偏置的调整: 每个batch迭代完成后, 都会通过反向传播来更新权重和偏置, 权重更新由输入和梯度推导得到, 而偏置仅仅由梯度推导,不需要输入项. 更新都是以最小化损失函数为目标的.
理解batch
- batch size: yolo 每次会从整个数据集中随机选出一个batch的数据进行前向传播训练.
- 计算此batch上所有样本的损失函数值.
- 通过损失对各层参数计梯度求导, 计算出权重参数新值
- 将计算出的更新值应用到模型上, 完成参数的批量更新
- 重复采样下一个batch, 反复完成前向和后向传播, 直至完成一个全量的epoch
- 即yolov8采用的训练策略是batch 更新方式.
理解epoch
- epoch 是训练一个完整数据集的过程, 训练一个epoch后, 会自动完成一次 val, 我们可以观察收敛情况.
特征图数据的变化
- 输入图像为 640×640×3, 3为RGB通道数.
- 接着进入卷积层+池化处理.
- [卷积层+池化]首先通过一个 3×3x3 的卷积核进行卷积操作, 卷积核的通道数也为 3 , 生成一个 640x640x3 的特征图(feature map).
- [卷积层+池化]然后通过非线性激活函数ReLU处理后, 是的特征图的数值取值范围为0到正无穷大.
- [卷积层+池化]最后经过MaxPool操作, MaxPool操作相当于 2x2 滤波, 仅保留每个2x2区域中最大值, 特征图尺寸将减半到 320x320.
- 每次卷积层+池化处理, feature map的尺寸都会减半, 经过此次卷积层+池化处理, 就可以生成 80x80 这样小尺寸的特征图, 另外, yolov8还会生成 40x40 和 20x20 特征图.
- 最后将 80x80和 40x40 和 20x20 尺寸的特征图输入到head模块预测, 最终输出 26x26的识别矩阵.
卷积核尺寸
不同层通常使用的不同的尺寸的卷积核, 1x1 矩阵可以改变通道数.
- 底层次卷积层通常使用较大的卷积核 3x3 或 5x5, 用于提取低级特征, 如边缘和线条等.
- 中层次卷积层通常使用 3x3 或 1x1 小核, 进一步提取中间级别特征
- 高层次卷积层通常使用 1x1 小核, 用来改变通道数和融合特征.
- 最大池化层使用固定的 2x2 卷积核, 实现下采样.
- 上采样层通常使用固定的 2x2 卷积核, 实现上采样.
- 最后几层通常使用 1x1 小核进行预测.
识别结果矩阵的尺寸
- 矩阵通常是26x26 或者 13x13
- 通道数的公式为, 通道数 = cell目标个数x(类别数+1+4+2), 组成分别为:
. cell目标个数: 是指每个cell最多能预测几个目标, 通常是一个固定值, 比如5或其他.
. 类别数: 一般设置80个Coco数据集类别
. 每个目标准确度概率
. 每个目标坐标预测值, 左上和右下坐标
. 每个目标的宽高预测值
. 举例: 每个目标个数为5个, 通道数共计: 435=5x(80+1+4+2), 所以预测结果矩阵为 26x26x435
网络功能模块
- backbone 网络部分: 负责对输入图像进行特征提取, 输出底层和高层特征图(feature map).
- neck 网络部分:是将backbone输出的特征图进行融合, 输出集中的特征, 同时可以减少空间尺寸.
- head 网络: 基于neck输出的特征, 进行最终的检测任务, 通常包括两个head:
- 分类头 classification head, 预测每个区域中的物体类别
- 定位头 regression head, 用来预测每个区域的物体坐标或掩码
训练过程中如何进行val和如何结束训练
- YoloV8 在每个epoch完成后, 会使用val数据集进行验证, 来计算loss和mAP等指标
- 如果当前epoch的loss比之前最佳的loss还大, 就会增加一个early_stops计数, 如果比最佳loss小, 则early_stops清零. 如果early_stops计数大于阈值, 则进行早停(early_stopping)操作, 即整个训练不需要一定要完整预设的epoch次数.
- 早停机制是防止过拟合的重要手段.
- 我们可以通过 yolov8 的 patience 设置early_stops阈值
YoloV8的Fine-tuning和预训练和from scratch训练
yolo命令行model的参数的说明既可以选择 yolov8n.pt, 也可以选择 yolov8n.yaml, 区别是:
- model=yolov8n.pt, 即为Fine-tuning训练, yolov8n.pt 模型文件已经包含了 yolo v8网络结构、超参数、训练参数、 权重参数信息, 它是官方的pre-trained 模型文件, 官方基于大规模数据集(coco 数据集)的80个类别训练而成.
- model=yolov8n.yaml pretrained=yolov8n.pt, 这是一个全新模型配置文件 + 预训练模型文件的组合, 训练过程将使用yaml模型配置文件, 包含模型的网络结构, 超参数和训练参数等信息, 但初始权重使用预训练的yolov8n.pt, 这样收敛速度较快, 训练过程比完整的from scratch要会短一些.
- model=yolov8n.yaml pretrained=False ,这是from scratch 训练, yolov8n.yaml文件包含模型的网络结构, 超参数和训练参数等信息, 我们可以基于这样的模型定义+自定义数据集训练出自己的模型权重文件, 用于后续的预测. 训练耗时最长. 从一个资料中看到, 每个object需要训练2000次, yolov3 基于coco数据集共训练了5万多次, 由此可见from scratch的训练量是非常大的.
yolov8预训练模型是基于coco和imagenet大型数据集做的训练, 所以这样的预训练权重已经非常具备通用性了, 对于99%的情况都适用, 如果完成从0开始做预训练,初始权重太过随机,很难收敛,最终网路训练结果也不会太好.
如果我们修改了网络, 预训练权重基本上不能使用了, 需要从头开始训练网络, 从头训练网络需要有好的算力和大的数据集, 否则做不成.
深度学习通过冻结部分参数提升效率效率
- 深度学习模型通常包括通用特征提取(卷积+池化)和高层分类(卷积+池化)和结果输出(全连接层)三个阶段.
- 特征提取是通用特征, 这部分参数通常无需调整
- 所以, 可通过冻结特征提取参数来减少计算量, 提升fine-tuning的效率, 步骤一般为:
- (1)先冻结前面几层卷积和池化层, 仅仅更新后面负责高级特征处理的卷积和池化层(统称为later classifier)
- (2) 最后解冻全连接层, 完成分类任务的微调
cd myEnv\Scripts
# 基于预训练 yolov8n.pt 进行自有数据的training, 换句话说, 是针对自有数据进行模型的 fine-tuning, 训练耗时较短.
.\yolo task=detect mode=train data=coco8.yaml model=yolov8n.pt
# 使用 yolov8n.yaml 模型定义文件重新训练, 并使用预训练的 yolov8n.pt 作为初始权重值, 训练耗时较长.
# pretrained参数可以设置成bool值, 也可以设置为一个pt文件
.\yolo task=detect mode=train data=coco8.yaml model=yolov8n.yaml pretrained=yolov8n.pt
# 使用 yolov8n.yaml 模型定义文件进行完全重新训练, 训练耗时最长.
.\yolo task=detect mode=train data=coco8.yaml model=yolov8n.yaml pretrained=False
Yolo顶层逻辑伪代码结构
下面是伪代码, 仅用于理解yolo 训练过程的顶层逻辑
import torch
from torch.utils.tensorboard import SummaryWriter
from yolov8 import Darknet
# 初始化模型、优化器等
model = Darknet()
optimizer = SGD(model.params)
writer = SummaryWriter('logs/yolov8')
for epoch in range(epochs):
## 每个epoch 的 train 过程
model.train() #模型切换到训练状态
for batch_idx, (images, targets) in enumerate(train_loader):
# 前向后向传播
pred = model(images)
loss = calc_loss(pred, targets)
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 记录指标
writer.add_scalar('Loss/train', loss, global_step)
global_step += 1
## 每个epoch 的 validate
model.eval() #模型切换到val状态
val_loss = 0
for images, targets in val_loader:
pred = model(images)
loss = calc_loss(pred, targets)
val_loss += loss
val_loss /= len(val_loader)
writer.add_scalar('Loss/val', val_loss, global_step)
## 保存最优模型
if val_loss < best_loss:
torch.save(model)
writer.close()