本文旨在通过实例数据,详细解读YOLOv8检测头的网络结构及其代码实现。首先将从检测头的网络架构开始讲解,涵盖代码与网络结构图的对比分析。关键在于深入探讨检测头的输出结果,因为这些输出将直接用于损失函数的计算。由于在不同阶段,检测头的输出有所不同,因此在讲解损失函数的计算之前,我们需要先理解检测头的输出内容以及相关参数的定义。
代码的讲解及数据变换在注释中。
一、检测头/解耦头
2.1 理论框图
在框图中,我将检测头部分单独截取如下:
其中包含三个结构,任何一个被选中都将作为我们的解耦头(即分别计算BboxLoss和CIsLoss)。
这三个部分,仅是输入的特征图大小和通道数存在差异,这也是我们所说的大目标检测头和小目标检测头的区别(即20x20和80x80的区别)。因此,大目标检测头和小目标检测头实际上调用的是同一段代码,只是从Neck部分输入给检测头的特征图不同,从而产生了大目标和小目标检测头的定义。
2.2 YOLOv8检测头代码分析
YOLOv8检测头的代码位于项目仓库的'ultralytics/nn/modules/head.py'路径下。虽然现在代码已经更新,但核心内容保持不变,只是新增了一些无关紧要的小功能。
class Detect(nn.Module):
"""YOLO Detect head for detection models.
检测头模块,负责将骨干网络提取的特征转换为检测预测结果
网络结构组成:
- 回归分支(cv2):预测边界框的分布参数
- 分类分支(cv3):预测类别置信度
- DFL模块:将分布参数转换为坐标偏移量
- 解码模块:将偏移量转换为实际坐标
输入特征图示例:
layer1: (1, 128, 80, 80) # 高分辨率特征图,检测小物体
layer2: (1, 256, 40, 40) # 中分辨率特征图
layer3: (1, 512, 20, 20) # 低分辨率特征图,检测大物体
Args:
nc (int): 类别数
ch (list): 输入通道数列表,例如[128, 256, 512]表示三个检测层的输入通道数
"""
dynamic = False # 是否动态重建网格(通常用于动态输入尺寸)
export = False # 导出模式(影响后处理方式)
format = None # 导出格式(tflite/edgetpu等)
end2end = False # 是否端到端模式
max_det = 300 # 每张图最大检测数
shape = None # 输入特征图形状缓存
anchors = torch.empty(0) # 初始化锚点
strides = torch.empty(0) # 各检测层的步长
def __init__(self, nc=80, ch=()):
super().__init__()
self.nc = nc # 检测的类别数量
self.nl = len(ch) # 检测层(输出特征层)的数量,例如 YOLOv8 有3个输出层
self.reg_max = 16 # DFL(Distribution Focal Loss)使用的分布最大值,表示每个坐标回归预测分布的离散区间数(0~15)
self.no = nc + self.reg_max * 4 # 每个“anchor”位置输出的通道数:nc 个分类分数 + 4 个坐标回归分布值(每个坐标 reg_max 个分布值,4 个坐标)
self.stride = torch.zeros(self.nl) # 存储每个检测层的下采样步幅(stride),将在模型构建时赋值,例如[8.0, 16.0, 32.0]
# 构建回归分支和分类分支
# 根据输入通道数计算头部卷积的中间通道数:
# c2 用于回归分支的卷积通道数,至少为16,并取输入通道的1/4与 reg_max*4 之间的较大值,确保足够的容量预测坐标
# c3 用于分类分支的卷积通道数,取输入通道和 min(nc,100) 之间的较大值,避免类别数很多时通道过少(上限100)
c2 = max(16, ch[0] // 4, self.reg_max * 4)
c3 = max(ch[0], min(self.nc, 100))
# 回归分支(预测边界框):Conv -> Conv -> 输出reg_max*4通道
# 例如:输入ch[0]=256,经过两个Conv后输出64通道(16*4)
# 最终 1x1 卷积将通道压缩到 4*reg_max(例如 reg_max=16 时输出64通道),表示4个坐标的分布预测
self.cv2 = nn.ModuleList(
nn.Sequential(
Conv(in_channels, c2, k=3), # 第一个卷积模块,提取特征,k=3表示3x3卷积
Conv(c2, c2, k=3), # 第二个卷积模块,继续提取特征
nn.Conv2d(c2, 4 * self.reg_max, 1) # 最终1x1卷积,输出4*reg_max个通道(每个坐标 reg_max 个分布值)
) for in_channels in ch
)
# 分类分支(预测类别):若使用 legacy 模式则用简单的两个卷积,否则采用深度可分离卷积结构提高效率
# 例如:输入ch[0]=256,最终输出80通道(对应80类)
self.cv3 = (
nn.ModuleList(nn.Sequential(Conv(x, c3, 3), Conv(c3, c3, 3), nn.Con