【重温YOLOV5】第四章 检测头(Head)与损失计算
目录
第四章 检测头(Head)与损失计算
4.1 YOLOv5 Head 结构剖析
解耦头的雏形:1×1卷积的分类/定位分支
三个检测层的Anchor分配策略
输出张量解析
4.2 Anchor 机制与AutoAnchor
预设Anchor的尺寸设计逻辑
AutoAnchor算法:K-means聚类自适应数据集
Anchor与特征层匹配规则:IoU>0.25阈值机制
自定义数据集的Anchor重计算实战
4.3 损失函数深度拆解
4.4 标签分配与正负样本匹配
跨网格匹配策略:正样本扩展到相邻网格(+0.5阈值)
中心点偏移量(xy)与宽高(wh)的计算
多Anchor匹配:一个GT可匹配多个Anchor的规则
代码解读:build_targets() 函数实现
第四章 检测头(Head)与损失计算
4.1 YOLOv5 Head 结构剖析
检测头(Detection Head)作为YOLOv5网络的终端组件,负责将Neck网络融合后的多尺度特征转换为最终的目标检测结果。该模块在功能上实现了特征到预测参数的映射,在结构上体现了分类与定位任务的联合优化策略。
解耦头的雏形:1×1卷积的分类/定位分支
YOLOv5的检测头可视为解耦头(Decoupled Head)设计的早期形态。传统耦合检测头将分类与定位任务共享同一组卷积特征,通过单一输出层同时预测类别概率与边界框坐标。这种设计在计算效率上具有优势,但忽略了分类与定位任务在特征需求上的本质差异:分类任务关注物体的高级语义特征,对空间位置信息的敏感度较低;定位任务则依赖精确的空间边界信息,对语义抽象程度的要求相对较低。
YOLOv5通过1×1卷积层隐式实现了初步的特征分离。在每个检测层,融合后的特征首先经过1×1卷积调整通道数,随后分支为两个并行路径分别处理分类与定位预测。虽然这两个路径在物理上仍共享前层特征,但独立的1×1卷积层为各自任务提供了专属的特征变换空间,使网络能够学习到更适合特定任务的特征表示。
以COCO数据集为例,检测头的输出张量维度为(batch_size, 3, H, W, 85)。其中3表示每个网格单元预设的三个Anchor框,H和W表示特征图的空间维度(对于P3、P4、P5层分别为80、40、20),85表示每个Anchor的预测参数维度(5个边界框参数+80个类别概率)。
边界框参数包含:
中心点x偏移量(相对于网格单元左上角)
中心点y偏移量(相对于网格单元左上角)
宽度缩放系数(相对于Anchor宽度)
高度缩放系数(相对于Anchor高度)
目标置信度(表示该Anchor包含物体的概率)
三个检测层的Anchor分配策略
YOLOv5在三个检测层共预设9个Anchor框,每个检测层分配3个Anchor。这种分配策略基于感受野与目标尺寸的匹配原则:浅层特征图(P3层,80×80)具有较高的空间分辨率,负责检测小尺寸目标,因此分配尺寸较小的Anchor;深层特征图(P5层,20×20)具有较大的感受野,负责检测大尺寸目标,分配尺寸较大的Anchor。
COCO数据集上的预设Anchor尺寸如下:
P3层(80×80):[10,13], [16,30], [33,23] —— 负责小目标检测
P4层(40×40):[30,61], [62,45], [59,119] —— 负责中等目标检测
P5层(20×20):[116,90], [156,198], [373,326] —— 负责大目标检测
每个Anchor的尺寸表示为[width, height],单位是相对于输入图像尺寸的像素值。在640×640的输入分辨率下,P5层最大的Anchor[373,326]覆盖了约58%×51%的图像区域,足以检测占据图像大部分区域的大型物体。
输出张量解析
检测头的输出张量在不同层级具有不同的空间维度,但通道结构保持一致。以COCO数据集(80个类别)为例:
P3层输出维度:(batch, 3, 80, 80, 85)
总网格数:80×80 = 6,400
每个网格3个Anchor,共19,200个预测框
每个预测框85个参数:x, y, w, h, objectness + 80类概率
P4层输出维度:(batch, 3, 40, 40, 85)
总网格数:40×40 = 1,600
每个网格3个Anchor,共4,800个预测框
P5层输出维度:(batch, 3, 20, 20, 85)
总网格数:20×20 = 400
每个网格3个Anchor,共1,200个预测框
三层合计每个图像产生25,200个预测框,经过置信度阈值过滤与非极大值抑制(NMS)后,最终保留高质量的检测结果。
输出参数的物理意义与计算方式如下:
Python
def decode_predictions(raw_predictions, anchors, stride): """ 解码检测头原始输出为边界框坐标 raw_predictions: [batch, 3, H, W, 85] """ batch_size, num_anchors, height, width, num_params = raw_predictions.shape # 提取各维度预测值 x_pred = raw_predictions[:, :, :, :, 0] # 中心x偏移 y_pred = raw_predictions[:, :, :, :, 1] # 中心y偏移 w_pred = raw_predictions[:, :, :, :, 2] # 宽度缩放 h_pred = raw_predictions[:, :, :, :, 3] # 高度缩放 obj_pred = raw_predictions[:, :, :, :, 4] # 目标置信度 cls_pred = raw_predictions[:, :, :, :, 5:] # 类别概率 # 应用sigmoid激活将偏移量限制在0-1范围 x_offset = sigmoid(x_pred) y_offset = sigmoid(y_pred) # 计算实际中心坐标(相对于输入图像) # grid_x, grid_y表示网格单元的左上角坐标 center_x = (grid_x + x_offset) * stride center_y = (grid_y + y_offset) * stride # 计算实际宽高(相对于输入图像) anchor_w = anchors[:, 0].view(1, 3, 1, 1) anchor_h = anchors[:, 1].view(1, 3, 1, 1) box_w = exp(w_pred) * anchor_w box_h = exp(h_pred) * anchor_h # 目标置信度与类别概率 objectness = sigmoid(obj_pred) class_probs = sigmoid(cls_pred) return center_x, center_y, box_w, box_h, objectness, class_probs4.2 Anchor 机制与AutoAnchor
Anchor机制是YOLOv5实现多尺度目标检测的核心组件,通过预设一组具有不同长宽比的参考框,将边界框回归问题转换为相对于参考框的偏移量预测问题,显著降低了网络的学习难度。
预设Anchor的尺寸设计逻辑
YOLOv5的预设Anchor基于COCO数据集的边界框分布统计设计。设计过程遵循以下原则:Anchor的尺寸应覆盖数据集中绝大多数目标的尺寸分布,同时不同Anchor之间应具有足够的区分度以避免冗余匹配。
COCO数据集中的目标尺寸分布呈现明显的多峰特性:小目标(面积<32²像素)占比约41%,中等目标(32²-96²像素)占比约34%,大目标(面积>96²像素)占比约25%。预设Anchor通过K-means聚类算法在训练集边界框上自动学习得到,聚类距离采用IoU-based度量:
相较于欧氏距离,IoU距离对边界框尺寸不敏感,能够确保大尺寸与小尺寸的边界框在聚类过程中具有同等重要性。聚类结果生成9个Anchor,按面积从小到大排列后分配至三个检测层。
AutoAnchor算法:K-means聚类自适应数据集![]()
当YOLOv5应用于自定义数据集时,预设Anchor可能与目标尺寸分布不匹配,导致检测性能下降。AutoAnchor算法通过自适应聚类自动计算最优Anchor尺寸。
算法流程如下:
Python
def autoanchor_kmeans(dataset, k=9, max_iter=300): """ 基于K-means的Anchor自动聚类 """ # 提取数据集中所有边界框的宽高 boxes = extract_boxes(dataset) # [N, 2] - width, height # 随机初始化k个聚类中心 centroids = initialize_centroids(boxes, k) for iteration in range(max_iter): # 计算每个边界框到各聚类中心的IoU距离 distances = compute_iou_distance(boxes, centroids) # 分配边界框到最近的聚类中心 assignments = argmin(distances, axis=1) # 更新聚类中心为所属边界框的中位数 new_centroids = update_centroids(boxes, assignments, k) # 检查收敛 if converged(centroids, new_centroids): break centroids = new_centroids # 按面积排序并分配至检测层 sorted_anchors = sort_by_area(centroids) p3_anchors = sorted_anchors[0:3] # 小Anchor p4_anchors = sorted_anchors[3:6] # 中等Anchor p5_anchors = sorted_anchors[6:9] # 大Anchor return p3_anchors, p4_anchors, p5_anchors def compute_iou_distance(boxes, centroids): """ 计算IoU距离矩阵 boxes: [N, 2] centroids: [k, 2] """ # 计算两两之间的IoU iou_matrix = bbox_iou(boxes[:, None], centroids[None, :]) # 转换为距离 distance = 1 - iou_matrix return distance聚类质量通过平均IoU(Avg IoU)评估,即每个边界框与其最近聚类中心的IoU平均值。实验表明,当Avg IoU超过0.6时,Anchor与数据集的匹配度良好,模型收敛速度显著加快。
Anchor与特征层匹配规则:IoU>0.25阈值机制![]()
在训练阶段,每个真实边界框(Ground Truth)需要匹配到最适合的Anchor。YOLOv5采用基于IoU阈值的多对一匹配策略:
计算真实框与所有9个Anchor的IoU
选择IoU大于阈值(默认0.25)的所有Anchor作为正样本
若单个真实框匹配到多个Anchor,则这些Anchor均负责预测该目标
未匹配的Anchor标记为负样本,仅参与置信度损失计算
Python
复制
def match_anchors(gt_boxes, anchors, iou_threshold=0.25): """ 将真实框与Anchor匹配 gt_boxes: [num_gt, 4] - x, y, w, h anchors: [9, 2] - width, height """ num_gt = gt_boxes.shape[0] num_anchors = anchors.shape[0] # 计算每个真实框与每个Anchor的IoU ious = compute_anchor_iou(gt_boxes, anchors) # [num_gt, num_anchors] # 选择IoU大于阈值的匹配 positive_matches = ious > iou_threshold # 布尔矩阵 # 对于每个真实框,获取匹配的Anchor索引 matches = [] for gt_idx in range(num_gt): matched_anchors = where(positive_matches[gt_idx])[0] if len(matched_anchors) == 0: # 若无Anchor满足阈值,选择IoU最大的Anchor best_anchor = argmax(ious[gt_idx]) matched_anchors = [best_anchor] matches.append({ 'gt_idx': gt_idx, 'anchor_indices': matched_anchors, 'ious': ious[gt_idx, matched_anchors] }) return matches这种多对一匹配策略增加了正样本数量,缓解了正负样本不平衡问题。实验表明,相较于严格的一对一匹配(仅选择IoU最大的Anchor),IoU>0.25的宽松匹配策略可使正样本数量增加约30%,小目标检测精度提升约5%。
自定义数据集的Anchor重计算实战
对于自定义数据集,建议重新计算Anchor尺寸以获得最优检测性能。计算流程包括:
数据统计分析:统计数据集中所有边界框的宽高分布,计算长宽比、面积分布等统计量
聚类计算:运行K-means聚类(k=9),距离度量采用1-IoU
分层分配:将9个Anchor按面积从小到大分配至P3、P4、P5三层
配置更新:将计算得到的Anchor尺寸更新至模型配置文件(YAML)
以纺织品缺陷检测为例,由于缺陷区域通常呈现细长形态(长宽比1.2-2.5),重新聚类后的Anchor与默认COCO Anchor差异显著。实验表明,使用自定义Anchor后,平均IoU从58.3%提升至76.5%,模型收敛速度加快约40%。
4.3 损失函数深度拆解
YOLOv5的损失函数由三个组成部分构成,分别对应边界框回归、目标置信度预测与类别分类三个子任务:
Python
def ciou_loss(pred_boxes, target_boxes): """ 计算CIoU Loss pred_boxes: [N, 4] - x, y, w, h target_boxes: [N, 4] - x, y, w, h """ # 计算IoU iou = bbox_iou(pred_boxes, target_boxes, format='xywh') # 计算中心点距离 pred_center_x, pred_center_y = pred_boxes[:, 0], pred_boxes[:, 1] target_center_x, target_center_y = target_boxes[:, 0], target_boxes[:, 1] center_distance_sq = (pred_center_x - target_center_x)**2 + \ (pred_center_y - target_center_y)**2 # 计算最小闭包框的对角线长度平方 pred_left = pred_center_x - pred_boxes[:, 2] / 2 pred_right = pred_center_x + pred_boxes[:, 2] / 2 pred_top = pred_center_y - pred_boxes[:, 3] / 2 pred_bottom = pred_center_y + pred_boxes[:, 3] / 2 target_left = target_center_x - target_boxes[:, 2] / 2 target_right = target_center_x + target_boxes[:, 2] / 2 target_top = target_center_y - target_boxes[:, 3] / 2 target_bottom = target_center_y + target_boxes[:, 3] / 2 c_left = minimum(pred_left, target_left) c_right = maximum(pred_right, target_right) c_top = minimum(pred_top, target_top) c_bottom = maximum(pred_bottom, target_bottom) c_w = c_right - c_left c_h = c_bottom - c_top c_diag_sq = c_w**2 + c_h**2 # 中心点距离惩罚项 distance_penalty = center_distance_sq / (c_diag_sq + 1e-7) # 长宽比一致性 pred_w, pred_h = pred_boxes[:, 2], pred_boxes[:, 3] target_w, target_h = target_boxes[:, 2], target_boxes[:, 3] v = (4 / (math.pi**2)) * \ (torch.atan(target_w / (target_h + 1e-7)) - \ torch.atan(pred_w / (pred_h + 1e-7)))**2 alpha = v / ((1 - iou) + v + 1e-7) # CIoU Loss ciou = iou - distance_penalty - alpha * v loss = 1 - ciou return loss.mean()CIoU Loss的优势在于:
当预测框与真实框不重叠时(IoU=0),仍能提供有效的梯度信号,而原始IoU Loss在此情况下梯度消失
同时优化位置、尺寸与形状,实现更精确的框回归
收敛速度比GIoU和DIoU更快,最终检测精度更高
4.4 标签分配与正负样本匹配
标签分配(Label Assignment)是目标检测训练的核心环节,负责将真实边界框(Ground Truth)与预测Anchor匹配,生成监督信号。YOLOv5采用基于中心点与Anchor尺度的多层匹配策略,显著增加了正样本数量,改善了小目标检测性能。
跨网格匹配策略:正样本扩展到相邻网格(+0.5阈值)
传统YOLO仅将目标中心点所在的网格单元作为正样本,这种严格匹配导致正样本数量稀少(每个目标仅1个正样本)。YOLOv5引入跨网格匹配策略:若目标中心点距离相邻网格中心的偏移小于0.5个网格单元,则相邻网格也被标记为正样本,负责预测该目标。
这种策略的理论基础是:边界框回归允许预测中心点相对于网格单元左上角偏移0.5个单元(通过Sigmoid函数限制在0-1范围)。因此,当目标中心靠近网格边界时,相邻网格完全有能力准确预测该目标。
Python
复制
def assign_grid_targets(gt_boxes, grid_size, stride, center_radius=0.5): """ 跨网格标签分配 gt_boxes: [num_gt, 4] - x, y, w, h (图像坐标) grid_size: (H, W) - 特征图尺寸 """ num_gt = gt_boxes.shape[0] H, W = grid_size # 将边界框坐标转换为网格坐标 gt_x = gt_boxes[:, 0] / stride # 中心x(网格坐标) gt_y = gt_boxes[:, 1] / stride # 中心y(网格坐标) # 获取中心点所在的网格索引 gt_grid_x = floor(gt_x).long() gt_grid_y = floor(gt_y).long() # 计算相对于网格单元左上角的偏移 offset_x = gt_x - gt_grid_x.float() offset_y = gt_y - gt_grid_y.float() assigned_grids = [] for gt_idx in range(num_gt): cx, cy = gt_grid_x[gt_idx], gt_grid_y[gt_idx] ox, oy = offset_x[gt_idx], offset_y[gt_idx] # 当前网格始终为正样本 grids = [(cx, cy)] # 若x偏移 < 0.5,左侧相邻网格也为正样本 if ox < center_radius and cx > 0: grids.append((cx - 1, cy)) # 若x偏移 > 0.5,右侧相邻网格也为正样本 if ox > (1 - center_radius) and cx < W - 1: grids.append((cx + 1, cy)) # 若y偏移 < 0.5,上方相邻网格也为正样本 if oy < center_radius and cy > 0: grids.append((cx, cy - 1)) # 若y偏移 > 0.5,下方相邻网格也为正样本 if oy > (1 - center_radius) and cy < H - 1: grids.append((cx, cy + 1)) assigned_grids.append({ 'gt_idx': gt_idx, 'grids': grids, 'offsets': [(gt_x[gt_idx] - g[0], gt_y[gt_idx] - g[1]) for g in grids] }) return assigned_grids该策略使每个目标可匹配至最多3个网格单元(中心网格+相邻网格),正样本数量增加约200%,显著改善了小目标与密集目标的检测性能。
中心点偏移量(xy)与宽高(wh)的计算
预测目标相对于Anchor的参数化表示:
中心点偏移:
宽高缩放:
多Anchor匹配:一个GT可匹配多个Anchor的规则
在YOLOv5中,单个真实框可同时匹配多个Anchor(跨层与层内)。匹配规则如下:
层间匹配:计算真实框与所有9个Anchor的IoU,IoU>0.25的Anchor均参与匹配
层内匹配:在同一检测层,若多个Anchor均满足阈值,则这些Anchor均为正样本
网格扩展:每个匹配的Anchor在其负责的网格单元及相邻网格(若满足中心偏移条件)均生成正样本
这种宽松匹配策略导致:
单个真实框可能匹配6-9个正样本(3层×每层1-3个Anchor×每层1-3个网格)
正样本总数约为目标数量的8-10倍,有效缓解了正负样本不平衡
代码解读:build_targets() 函数实现
build_targets()函数是YOLOv5训练流程的核心,负责批量生成标签张量。其伪代码实现如下:
Python
def build_targets(preds, targets, anchors, strides): """ 构建训练目标 preds: 模型预测输出列表 [p3_pred, p4_pred, p5_pred] targets: 真实标签 [num_targets, 6] - img_idx, cls, x, y, w, h (归一化坐标) anchors: Anchor列表 [9, 2] strides: 步长列表 [8, 16, 32] """ num_layers = len(preds) # 3层 num_targets = targets.shape[0] # 初始化目标张量 tcls, tbox, indices, anch = [], [], [], [] # 将归一化坐标转换为绝对坐标(假设输入640x640) targets_abs = targets.clone() targets_abs[:, 2:] *= 640 # 计算目标与所有Anchor的IoU anchor_wh = anchors.view(1, 9, 2) # [1, 9, 2] gt_wh = targets_abs[:, 4:6].view(num_targets, 1, 2) # [N, 1, 2] # IoU计算(仅考虑宽高,忽略位置) inter_wh = minimum(gt_wh, anchor_wh) inter_area = inter_wh[:, :, 0] * inter_wh[:, :, 1] union_area = gt_wh[:, :, 0] * gt_wh[:, :, 1] + \ anchor_wh[:, :, 0] * anchor_wh[:, :, 1] - inter_area iou = inter_area / (union_area + 1e-16) # [num_targets, 9] # 选择最佳匹配的Anchor best_iou, best_anchor_idx = iou.max(dim=1) # [num_targets] for layer_idx in range(num_layers): # 获取当前层的Anchor索引 layer_anchor_start = layer_idx * 3 layer_anchor_end = layer_anchor_start + 3 # 筛选匹配到当前层的目标 layer_mask = (best_anchor_idx >= layer_anchor_start) & \ (best_anchor_idx < layer_anchor_end) & \ (best_iou > 0.25) if not layer_mask.any(): tcls.append(torch.zeros(0)) tbox.append(torch.zeros(0, 4)) indices.append((torch.zeros(0), torch.zeros(0), torch.zeros(0))) anch.append(torch.zeros(0, 2)) continue layer_targets = targets_abs[layer_mask] layer_anchors = anchors[best_anchor_idx[layer_mask]] # 计算网格坐标 stride = strides[layer_idx] grid_x = (layer_targets[:, 2] / stride).long() grid_y = (layer_targets[:, 3] / stride).long() # 跨网格扩展 expanded_targets = [] for gt_idx in range(len(layer_targets)): cx, cy = grid_x[gt_idx], grid_y[gt_idx] ox = layer_targets[gt_idx, 2] / stride - cx.float() oy = layer_targets[gt_idx, 3] / stride - cy.float() # 添加中心网格 expanded_targets.append((gt_idx, cx, cy)) # 添加相邻网格(若满足条件) if ox < 0.5 and cx > 0: expanded_targets.append((gt_idx, cx - 1, cy)) if ox > 0.5 and cx < preds[layer_idx].shape[3] - 1: expanded_targets.append((gt_idx, cx + 1, cy)) if oy < 0.5 and cy > 0: expanded_targets.append((gt_idx, cx, cy - 1)) if oy > 0.5 and cy < preds[layer_idx].shape[2] - 1: expanded_targets.append((gt_idx, cx, cy + 1)) # 构建最终目标张量 # ...(收集所有扩展目标,计算偏移量等) return tcls, tbox, indices, anch该函数的核心逻辑包括:
Anchor匹配:计算目标与所有Anchor的IoU,筛选IoU>0.25的匹配
层分配:根据最佳匹配Anchor将目标分配至对应检测层
网格分配:计算目标中心所在网格,并进行跨网格扩展
参数编码:将绝对坐标转换为中心偏移与宽高缩放系数
通过上述标签分配策略,YOLOv5实现了高效的正负样本匹配,为损失计算提供了高质量的监督信号,是模型达到优异检测性能的关键保障
