一、引言
目标检测是计算机视觉领域的重要任务之一,在众多目标检测算法中,Yolov5 以其高效、准确的特点受到了广泛关注。我以 Yolov5 的模型结构为例,研究其 Backbone、Neck、Head 等各个部分的详细内容,为日后优化模型做示例。
二、Yolov5 模型结构之概述
(一)Yolov5 模型的整体架构
Yolov5 模型主要由 Backbone、Neck 和 Head 三大部分组成,这三个部分协同工作,共同完成目标检测任务。Backbone 负责提取图像的特征,Neck 对特征进行融合和增强,Head 则根据融合后的特征进行目标的预测和分类,一个模型若想得到优化,需要从以下三点着手:Backbone 、Neck 和Head。
(二)Yolov5 模型结构的优势与特点
- 高效性:Yolov5 在模型结构和算法优化上进行了诸多改进,使得模型在保持较高检测精度的同时,具有更快的检测速度,能够满足实时目标检测的需求。
- 灵活性:官网提供了不同大小的模型版本(如 s、m、l、x 等),我们可以根据实际应用场景和硬件资源选择合适的模型,方便在不同设备上部署和运行。
- 易用性:代码实现相对简洁,易于理解和修改,同时社区支持丰富,有大量的教程和示例可供参考,降低了初学者的入门门槛。
三、Backbone 之 CSP(Cross Stage Partial DenseNet)
(一)CSP 结构的基本原理
CSP 是 Yolov5 Backbone 的核心结构,它将基础层分为两部分,一部分直接连接到 Transition 层,另一部分则经过 Dense Block 后再连接到 Transition 层。这种设计有效地减少了计算量,同时缓解了模型的过拟合问题,提高了特征提取的效率和质量。
(二)CSP 与传统 DenseNet 的对比
- 传统 DenseNet:通过密集连接的方式,使每一层都能直接与前面的所有层相连,实现了特征的充分复用,但也带来了计算量过大和容易过拟合的问题。
- CSP 的改进:在保留 DenseNet 特征复用优点的基础上,通过跨阶段的部分连接,降低了模型的复杂度和计算量,并且能够更好地平衡模型的精度和速度。
(三)CSP 模块的代码实现(以 PyTorch 为例)
import torch
import torch.nn as nn
class CSPBlock(nn.Module):
def __init__(self, in_channels, out_channels, num_blocks):
super(CSPBlock, self).__init__()
self.part1_conv = nn.Conv2d(in_channels, out_channels // 2, kernel_size=1)
self.part2_conv1 = nn.Conv2d(in_channels, out_channels // 2, kernel_size=1)
self.part2_bn1 = nn.BatchNorm2d(out_channels // 2)
self.part2_relu1 = nn.ReLU(inplace=True)
self.part2_blocks = nn.Sequential(*[BasicBlock(out_channels // 2) for _ in range(num_blocks)])
self.part2_conv2 = nn.Conv2d(out_channels // 2, out_channels // 2, kernel_size=1)
self.part2_bn2 = nn.BatchNorm2d(out_channels // 2)
self.part2_relu2 = nn.ReLU(inplace=True)
self.transition_conv = nn.Conv2d(out_channels, out_channels, kernel_size=1)
self.transition_bn = nn.BatchNorm2d(out_channels)
self.transition_relu = nn.ReLU(inplace=True)
def forward(self, x):
x1 = self.part1_conv(x)
x2 = self.part2_conv1(x)
x2 = self.part2_bn1(x2)
x2 = self.part2_relu1(x2)
x2 = self.part2_blocks(x2)
x2 = self.part2_conv2(x2)
x2 = self.part2_bn2(x2)
x2 = self.part2_relu2(x2)
out = torch.cat([x1, x2], dim=1)
out = self.transition_conv(out)
out = self.transition_bn(out)
out = self.transition_relu(out)
return out
class BasicBlock(nn.Module):
def __init__(self, in_channels):
super(BasicBlock, self).__init__()
self.conv1 = nn.Conv2d(in_channels, in_channels, kernel_size=3, padding=1)
self.bn1 = nn.BatchNorm2d(in_channels)
self.relu = nn.ReLU(inplace=True)
self.conv2 = nn.Conv2d(in_channels, in_channels, kernel_size=3, padding=1)
self.bn2 = nn.BatchNorm2d(in_channels)
def forward(self, x):
residual = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out += residual
out = self.relu(out)
return out
在上述代码中,CSPBlock
类实现了 CSP 模块的结构。__init__
方法中定义了各个卷积层、批归一化层和 ReLU 激活函数等组件。forward
方法中,输入x
被分成两部分,分别经过不同的处理路径后再进行拼接和进一步的处理,最终输出融合后的特征。BasicBlock
类则定义了 CSP 模块中使用的基本残差块结构。
四、Neck 之 PANet(Path Aggregation Network)
(一)PANet 在 Yolov5 中的作用
PANet 在 Yolov5 中作为 Neck 部分,主要用于对 Backbone 提取的特征进行融合和增强。它在 FPN(Feature Pyramid Network)的基础上进行了改进,增加了自底向上的路径,形成了一个双向的特征融合网络,使得不同尺度的特征能够更好地交互和融合,从而提高模型对不同大小目标的检测能力。
(二)PANet 与 FPN 的区别
- FPN 结构:通过自顶向下的方式,将高层的语义强但分辨率低的特征与底层的语义弱但分辨率高的特征进行融合,构建了一个特征金字塔。然而,底层特征向高层传递的路径较长,可能会导致信息丢失。
- PANet 的改进:除了自顶向下的路径外,还增加了自底向上的路径,底层特征可以更快速地传递到高层,高层特征也能更好地指导底层特征的学习,进一步增强了特征的表达能力和模型的检测性能。
(三)PANet 的代码实现
class PANet(nn.Module):
def __init__(self, in_channels_list, out_channels):
super(PANet, self).__init__()
self.lateral_convs = nn.ModuleList()
self.top_down_convs = nn.ModuleList()
self.bottom_up_convs = nn.ModuleList()
for in_channels in in_channels_list:
self.lateral_convs.append(nn.Conv2d(in_channels, out_channels, kernel_size=1))
self.top_down_convs.append(nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1))
self.bottom_up_convs.append(nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1))
def forward(self, x):
# 自顶向下的路径
c = len(x)
top_down_feats = [self.lateral_convs[-1](x[-1])]
for i in range(c - 2, -1, -1):
top_down_feat = F.interpolate(top_down_feats[-1], scale_factor=2, mode='nearest')
top_down_feat += self.lateral_convs[i](x[i])
top_down_feats.append(self.top_down_convs[i](top_down_feat))
# 自底向上的路径
bottom_up_feats = [top_down_feats[-1]]
for i in range(c - 1):
bottom_up_feat = F.max_pool2d(bottom_up_feats[-1], kernel_size=2, stride=2)
bottom_up_feat += top_down_feats[i]
bottom_up_feats.append(self.bottom_up_convs[i](bottom_up_feat))
return bottom_up_feats
在上述PANet
类的代码中,__init__
方法初始化了侧向连接卷积层、自顶向下卷积层和自底向上卷积层。forward
方法中,首先通过自顶向下的路径将高层特征与底层特征进行融合,然后再通过自底向上的路径进一步增强特征,最终返回融合后的特征列表,这些特征将被传递到 Head 部分进行目标检测。
五、Head 之 output(Dense Prediction)
(一)Yolov5 Head 的输出特点
- 与 YOLOv3、YOLOv4 的相似性:Yolov5 的 Head 输出与 YOLOv3、YOLOv4 在某些方面具有相似性,例如输出的格式和处理方式,这使得熟悉前几代 YOLO 模型的开发者能够更快地理解和上手 Yolov5。
- 三个输出层:对应不同的分辨率,分别用于检测不同尺度的目标。这种多尺度输出的设计能够更好地适应目标在图像中大小的变化,提高模型对各种尺度目标的检测精度和召回率。
(二)输出信息的详细解释
输出信息的格式为[NxCxHxWx(num + 5)]
,其中:
N
:表示批量大小,即一次处理的图像数量。在实际应用中,可以根据硬件资源和任务需求设置合适的批量大小。C
:通常与输入图像的通道数相关,一般为 3(RGB 图像)。H
和W
:分别表示输出特征图的高度和宽度,不同的输出层具有不同的H
和W
值,以适应不同尺度的目标检测。例如,对于输入为640x640
的图像,可能会得到三个输出层大小分别为20
、40
、80
。num
:表示检测目标的类别数,num + 5
则表示每个检测框的相关信息,包括目标的类别概率、边界框的位置信息(如中心坐标cx, cy
、宽度w
和高度h
等)以及目标的置信度得分score
。- 一般需要使用tensor的方式将NCWH转换为NCHW,将数据以 NCHW 的方式组织 tensor 在深度学习中具有诸多优势,包括硬件加速友好、深度学习框架的优化支持以及算法实现的便利性等,这些因素共同促使 NCHW 成为深度学习中常用的数据格式。
(三)输出层演化(以 Yolov5 版本更新为例)
- 早期版本:输出层较为复杂,可能有多个不同名称和格式的输出,例如
name: 397
,type: float32[1,3,80,80,85]
等,这些输出分别对应不同分辨率的特征图和相关信息,需要进行较为繁琐的解析和处理。 - 高版本(如 6.x):进行了简化,只需要解析最后一个输出层即可,输出层格式为
cx, cy, w, h, score
后面跟着num
个类别得分(例如80
个MSCOCO
的分类得分)。这种简化使得模型的后处理更加方便和高效,同时也减少了计算量和存储空间的需求。
以下是一个简单的输出解析示例代码(假设输出为output
张量):
def parse_output(output, num_classes):
batch_size, _, height, width = output.shape
predictions = []
for b in range(batch_size):
for h in range(height):
for w in range(width):
values = output[b, :, h, w]
obj_score = values[4]
if obj_score > 0.5: # 设定置信度阈值
class_scores = values[5:]
class_id = torch.argmax(class_scores)
class_prob = class_scores[class_id]
if class_prob > 0.5: # 设定类别概率阈值
cx = w + values[0]
cy = h + values[1]
w_box = values[2]
h_box = values[3]
prediction = {
'class_id': class_id.item(),
'class_prob': class_prob.item(),
'bbox': [cx.item(), cy.item(), w_box.item(), h_box.item()]
}
predictions.append(prediction)
return predictions
在上述代码中,parse_output
函数用于解析模型的输出。首先,获取输出张量的维度信息。然后,通过遍历输出张量的每个元素,根据置信度阈值和类别概率阈值筛选出有效的检测结果,并将检测到的目标类别、概率以及边界框信息存储在predictions
列表中返回。
六、模型结构之输入
(一)输入图像的格式和要求
Yolov5 模型的输入图像格式为[1xNxHxW] = [1x3x640x640]
,即NCHW,其中:
1
:表示批量大小为 1,即一次处理一张图像。在实际应用中,可以根据需要调整批量大小,但需要注意硬件的内存限制。3
:表示图像的通道数,通常为 RGB 三通道彩色图像。640x640
:表示输入图像的高度和宽度,将图像统一 resize 到这个尺寸可以方便模型进行处理和计算,利用Opencv的resize即可达到此效果,同时也能保证模型在不同大小的输入图像上具有一定的稳定性和一致性。
(二)输入图像预处理的重要性和方法
输入图像预处理是模型训练和推理过程中的重要环节,它可以提高模型的性能和稳定性。以下是一些常见的预处理方法及其代码示例:
1. 图像缩放与裁剪
将图像 resize 到指定的尺寸(如640x640
),可以使用 OpenCV 库实现:
import cv2
def resize_image(image_path, target_size=(640, 640)):
image = cv2.imread(image_path)
resized_image = cv2.resize(image, target_size)
return resized_image
在上述代码中,resize_image
函数读取指定路径的图像,并将其 resize 到target_size
指定的尺寸。
2. 图像归一化
将图像的像素值归一化到一个较小的范围内,如[0, 1]
或[-1, 1]
,可以加快模型的收敛速度,提高训练的稳定性和效果。以下是将图像像素值归一化到[0, 1]
的示例代码:
import numpy as np
def normalize_image(image):
return image / 255.0
在上述代码中,normalize_image
函数将输入图像的像素值除以 255,像素值最大就是255,因此此操作可以将其归一化到[0, 1]
范围内。
3. 数据增强
通过对输入图像进行随机裁剪、翻转、缩放、旋转等操作,可以增加训练数据的多样性,提高模型的泛化能力。以下是使用torchvision.transforms
实现数据增强的示例代码:
import torchvision.transforms as transforms
transform = transforms.Compose([
transforms.RandomResizedCrop((640, 640)),
transforms.RandomHorizontalFlip(),
transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
def augment_image(image):
augmented_image = transform(image)
return augmented_image
在上述代码中,transform
定义了一系列的数据增强操作,包括随机裁剪、随机水平翻转、颜色抖动、转换为张量以及归一化等。augment_image
函数将输入图像应用这些数据增强操作后返回增强后的图像。
七、Yolov5 模型的训练与优化
(一)训练数据集的准备
训练一个高质量的 Yolov5 模型需要大量的标注数据。数据集的标注通常包括目标的类别和边界框信息。可以使用一些开源的标注工具,如 LabelImg 等,来标注图像数据。标注完成后,将数据集按照一定的比例划分为训练集、验证集和测试集,以便在训练过程中进行模型评估和调优。
(二)训练参数的设置
- 学习率:学习率是影响模型训练速度和效果的重要参数。通常可以采用学习率衰减策略,在训练初期使用较大的学习率,随着训练的进行逐渐减小学习率,以帮助模型更好地收敛。
- 批次大小:批次大小决定了一次训练中处理的图像数量。较大的批次大小可以提高 GPU 的利用率,但可能会导致内存不足;较小的批次大小则可以使模型训练更加稳定,但训练速度可能会较慢。需要根据硬件资源和数据集大小进行合理设置。
- 训练轮数:训练轮数表示模型遍历整个训练数据集的次数。过多的训练轮数可能会导致过拟合,而过少的训练轮数则可能使模型无法充分学习数据的特征。可以通过观察验证集上的损失函数和准确率等。