PyTorch机器学习(九)—— 从锚框到预测框:YOLO候选区域生成与标注的数学原理与代码实现
1. 目标检测中的锚框与预测框基础在目标检测任务中锚框Anchor Box和预测框Prediction Box是两个核心概念。简单来说锚框是我们预先设定好的一组参考框它们像模板一样覆盖在图像的各个位置而预测框则是模型根据这些锚框调整后得到的最终检测结果。这就好比装修房子时先搭好脚手架锚框再根据实际需求调整成最终的结构预测框。边界框的两种表示形式在目标检测中非常常见xyxy格式用左上角坐标(x1,y1)和右下角坐标(x2,y2)表示一个矩形框xywh格式用中心点坐标(x,y)和宽高(w,h)表示YOLO系列采用的就是这种形式我在实际项目中发现xywh格式在处理坐标变换时更方便特别是当需要计算框的中心点偏移时。举个例子假设有个物体的真实框是[0.4, 0.5, 0.2, 0.3]表示中心点在图像40%宽度和50%高度的位置宽高分别是图像尺寸的20%和30%。2. 锚框的生成原理与策略2.1 锚框的生成方式YOLO中锚框的生成规则很有意思先把图片划分成m×n的网格然后在每个网格中心生成一系列不同长宽比的锚框。这就好比用棋盘格的方式扫描整张图片确保不遗漏任何可能的物体位置。在YOLOv5中锚框的尺寸是通过k-means聚类遗传算法计算得到的。具体来说对训练集中所有真实框的宽高进行k-means聚类用遗传算法优化初始聚类中心最终得到9组典型的宽高组合3种尺度×3种长宽比# 示例YOLOv5默认的锚框尺寸针对COCO数据集 anchors [ [10,13, 16,30, 33,23], # P3/8 小尺度锚框 [30,61, 62,45, 59,119], # P4/16 中尺度锚框 [116,90, 156,198, 373,326] # P5/32 大尺度锚框 ]2.2 锚框与特征图的对应关系这里有个关键点不同尺度的锚框对应不同层级的特征图。在YOLOv5中小尺度锚框如10×13负责检测小物体对应高分辨率的浅层特征中尺度锚框如30×61对应中层特征大尺度锚框如116×90负责大物体对应低分辨率的深层特征这种设计让模型能够同时检测不同大小的物体我在处理无人机航拍图像时就深有体会——小尺度锚框对检测远处的小车辆特别有效。3. 从锚框到预测框的数学变换3.1 中心坐标的预测YOLO中最精妙的部分就是锚框到预测框的转换公式。对于中心坐标的预测bx σ(tx) cx by σ(ty) cy其中(cx,cy)是网格左上角坐标σ是sigmoid函数确保预测的偏移量在0到1之间(tx,ty)是网络预测的偏移量这样设计的好处是预测框的中心不会跳出当前网格避免了预测框乱跑的情况。我在调试模型时发现如果没有这个约束训练初期会非常不稳定。3.2 宽高的预测宽高的预测采用指数变换bw pw × e^tw bh ph × e^th这里(pw,ph)是锚框的原始宽高(tw,th)是网络预测的缩放系数。指数函数保证了宽高总是正数而且可以放大或缩小锚框尺寸。为什么使用指数函数我在实验中发现如果用线性变换当预测值为负时宽高会变成负数导致训练崩溃。而指数变换不仅解决了这个问题还能更好地处理不同尺度的物体——小物体需要精细调整tw接近0大物体可能需要较大变化tw绝对值较大。4. 候选区域的标注策略4.1 正负样本的分配一张图片可能生成成千上万个锚框但只有少数会包含物体。如何筛选这些有用的锚框YOLO采用以下策略计算所有锚框与真实框的IoU交并比对每个真实框选择IoU最大的锚框作为正样本同时IoU超过阈值如0.7的锚框也作为正样本IoU低于阈值如0.3的标记为负样本介于中间的样本忽略不参与训练def compute_iou(box1, box2): # 计算两个框的交并比 # box格式为[x_center, y_center, width, height] ...4.2 三种关键标签每个正样本锚框需要标注三种信息Objectness标签二分类1表示包含物体0表示背景Location标签4个值(tx*,ty*,tw*,th*)表示预测框与真实框的偏移Label标签物体的类别用one-hot编码表示在实际项目中我发现location标签的数值范围对训练效果影响很大。通常会对偏移量做归一化处理比如将tx,ty除以网格大小tw,th取对数后再除以预设的方差。5. PyTorch代码实现详解5.1 锚框生成代码def generate_anchors(feature_map_size, anchor_sizes, anchor_ratios): 生成锚框坐标 参数 feature_map_size: 特征图尺寸 (h,w) anchor_sizes: 基础尺寸列表 anchor_ratios: 宽高比列表 返回 anchors: 生成的锚框 [x_center, y_center, width, height] anchors [] for i in range(feature_map_size[0]): for j in range(feature_map_size[1]): # 计算网格中心坐标 cx (j 0.5) / feature_map_size[1] cy (i 0.5) / feature_map_size[0] for size in anchor_sizes: for ratio in anchor_ratios: # 计算锚框宽高 w size * math.sqrt(ratio) h size / math.sqrt(ratio) anchors.append([cx, cy, w, h]) return torch.tensor(anchors)5.2 标签生成完整实现def get_targets(annotations, anchors, num_classes, img_size, iou_threshold0.5): 生成训练标签 参数 annotations: 标注信息 [class_id, x_center, y_center, width, height] anchors: 预生成的锚框 num_classes: 类别数 img_size: 图像尺寸 (h,w) iou_threshold: IoU阈值 返回 obj_mask: 物体掩码 [n_anchors] loc_targets: 位置目标 [n_anchors, 4] cls_targets: 类别目标 [n_anchors, n_classes] n_anchors len(anchors) obj_mask torch.zeros(n_anchors) loc_targets torch.zeros((n_anchors, 4)) cls_targets torch.zeros((n_anchors, num_classes)) # 如果没有标注则全部为负样本 if len(annotations) 0: return obj_mask, loc_targets, cls_targets # 计算所有锚框与真实框的IoU ious [] for ann in annotations: gt_box ann[1:].unsqueeze(0) # 取xywh iou compute_iou(anchors[:, 1:], gt_box) # 忽略中心点只比较宽高 ious.append(iou) ious torch.cat(ious, dim1) # 为每个真实框分配最佳锚框 best_anchor_idx ious.argmax(dim0) best_iou ious.max(dim0)[0] # 设置正样本 for i, idx in enumerate(best_anchor_idx): if best_iou[i] 0: obj_mask[idx] 1 # 计算位置目标 gt_box annotations[i][1:] anchor_box anchors[idx] loc_targets[idx, 0] gt_box[0] * img_size[1] - anchor_box[0] # tx loc_targets[idx, 1] gt_box[1] * img_size[0] - anchor_box[1] # ty loc_targets[idx, 2] torch.log(gt_box[2] / anchor_box[2]) # tw loc_targets[idx, 3] torch.log(gt_box[3] / anchor_box[3]) # th # 设置类别 cls_id int(annotations[i][0]) cls_targets[idx, cls_id] 1 # 设置高IoU的负样本 max_iou, _ ious.max(dim1) obj_mask[max_iou 0.3] -1 # 忽略样本 return obj_mask, loc_targets, cls_targets6. 实战经验与调优技巧在多个实际项目中我总结了以下优化锚框策略的经验锚框尺寸定制针对特定场景如人脸检测或文字检测重新聚类计算锚框尺寸能显著提升效果。我曾经在一个工业零件检测项目中通过调整锚框尺寸使mAP提升了8%。多尺度训练YOLOv5默认使用3种尺度的特征图但对于小物体检测可以增加更浅层的特征图。我在一个无人机图像项目中增加了第4个检测头小物体召回率提高了15%。正样本扩充除了IoU最大的锚框还可以为每个真实框分配多个正样本。YOLOv5中采用的做法是对每个真实框选择所有IoU0.2的锚框作为正样本。损失函数调优位置损失通常使用CIoU Loss它比传统的L1/L2损失更能反映框的重叠情况。我在一个项目中对比发现CIoU Loss比MSE Loss使定位精度提高了约5%。# CIoU损失计算示例 def ciou_loss(pred_boxes, target_boxes): # 计算中心点距离 center_loss (pred_boxes[:, :2] - target_boxes[:, :2]).pow(2).sum(dim1) # 计算宽高差异 wh_loss ((pred_boxes[:, 2:] - target_boxes[:, 2:]) / target_boxes[:, 2:]).pow(2).sum(dim1) # 计算IoU和其他项 iou compute_iou(pred_boxes, target_boxes) v (4 / (math.pi ** 2)) * ((pred_boxes[:, 2] / pred_boxes[:, 3]).atan() - (target_boxes[:, 2] / target_boxes[:, 3]).atan()).pow(2) alpha v / (1 - iou v 1e-7) return 1 - iou center_loss wh_loss alpha * v7. 常见问题排查在实现锚框机制时经常会遇到以下问题预测框跑出图像边界这是因为没有对预测坐标做限制。解决方法是在输出层添加sigmoid激活或者在后处理时做clip操作。大量锚框被标记为忽略样本检查IoU阈值设置是否合理。对于密集小物体场景可以适当降低负样本阈值。某些类别检测效果差可能是锚框尺寸不适合该类物体。可以通过分析该类物体的宽高分布调整对应的锚框尺寸。训练初期loss震荡大可能是因为位置预测值范围太大。可以尝试对定位损失乘以一个较小的权重如0.1等训练稳定后再恢复。显存不足减少每张图像的锚框数量或者使用更小的特征图。YOLOv5中可以通过调整anchor_t参数来控制正样本数量。