为YOLOv11引入Anchor-Free分支(SimOTA标签分配)

张开发
2026/4/7 21:43:16 15 分钟阅读

分享文章

为YOLOv11引入Anchor-Free分支(SimOTA标签分配)
上周在部署YOLOv11到边缘设备时遇到个头疼问题同一批训练出来的模型在测试集上mAP差不太多但上线后某些场景的漏检率突然飙升。查了三天数据发现是anchor设置和实际目标分布不匹配——那些漏检的目标宽高比都比较极端anchor框根本覆盖不到。这让我下定决心给YOLOv11加个Anchor-Free分支用SimOTA做动态标签分配今天把改造过程记下来。为什么需要Anchor-Free分支YOLO系列从v2开始用anchor本质是在预设的框里做微调。但预设就有局限数据集目标尺度变化大时要么增加anchor数量计算量上去要么接受某些目标匹配不好。我们项目里要检测的物体从细长的电缆到接近正方形的设备都有固定anchor怎么调都别扭。Anchor-Free的思路是直接预测目标中心点和宽高不用预设anchor。但纯Anchor-Free容易训练不稳定所以保留原来的anchor-based分支做双保险两个分支结果后期融合。这种混合结构在复杂场景里特别实用——anchor分支抓常规目标anchor-free分支补漏那些形状特殊的。SimOTA标签分配的核心逻辑之前YOLOv11用的标签分配还是静态的主要看IOU。SimOTASimplified Optimal Transport Assignment不一样它是动态分配每个训练迭代都会根据当前网络预测质量来匹配。简单说就是不再让一个gt框只配几个正样本而是让网络自己决定哪些预测框最适合这个gt同时考虑分类置信度和位置精度。这里有个关键点SimOTA会限制每个gt框匹配的预测框数量topk避免某个gt框占用太多正样本导致其他gt框分不到。这个数量不是固定的会根据gt框大小自适应调整——大目标多匹配几个小目标少匹配几个很符合直觉。代码改造实战在models/yolo.py里我们先加个anchor-free检测头。注意这里不用anchor相关的参数输出通道数直接是(x, y, w, h, obj, cls)classAnchorFreeHead(nn.Module):def__init__(self,in_channels,num_classes):super().__init__()# 每个位置预测3个框和原来保持一致self.num_outputs3self.reg_convsnn.Sequential(Conv(in_channels,in_channels//2,3),Conv(in_channels//2,in_channels//4,3),nn.Conv2d(in_channels//4,4*self.num_outputs,1)# 直接预测坐标偏移)self.cls_convsnn.Sequential(Conv(in_channels,in_channels//2,3),Conv(in_channels//2,in_channels//4,3),nn.Conv2d(in_channels//4,(1num_classes)*self.num_outputs,1))defforward(self,x):# 输出shape: (batch, num_anchors*(41num_classes), height, width)reg_outself.reg_convs(x)cls_outself.cls_convs(x)returntorch.cat([reg_out,cls_out],dim1)标签分配部分重头戏在loss.py里。我们新增SimOTA匹配函数defsimota_matching(predictions,targets,num_classes,fg_iou_threshold0.5): predictions: [batch, num_pred, 41num_classes] targets: [batch, num_gt, 41] (最后一维是class_label) 返回匹配矩阵和分配数量 batch_sizepredictions.shape[0]matching_matrixtorch.zeros(batch_size,predictions.shape[1],targets.shape[1])foriinrange(batch_size):pred_ipredictions[i]# [num_pred, 85]target_itargets[i]# [num_gt, 5]iflen(target_i)0:continue# 计算预测框和gt框的代价costiou_matrixbox_iou(pred_i[:,:4],target_i[:,:4])cls_matrixpred_i[:,5:].sigmoid()[:,target_i[:,4].long()].t()# 这里踩过坑iou和cls的权重需要调默认各0.5不一定最优cost_matrix-(iou_matrix*cls_matrix**0.5)# 负号因为后面用最小化# 动态topkgt面积越大匹配的预测框越多gt_areas(target_i[:,2]-target_i[:,0])*(target_i[:,3]-target_i[:,1])topk_valuestorch.clamp(gt_areas.sqrt().int(),min1,max10)# 为每个gt选topk个代价最小的预测框forgt_idxinrange(len(target_i)):_,topk_indicestorch.topk(cost_matrix[gt_idx],kmin(topk_values[gt_idx],len(pred_i)),largestFalse)matching_matrix[i,topk_indices,gt_idx]1returnmatching_matrix训练时两个分支的loss要加权融合。我的经验是前期让anchor-based权重高些0.7后期逐渐平衡到0.5/0.5这样训练更稳# 在compute_loss函数里anchor_based_lossoriginal_yolo_loss(pred_anchor,targets)anchor_free_losscompute_anchor_free_loss(pred_af,targets,simota_matrix)total_loss0.7*anchor_based_loss0.3*anchor_free_loss# 前期比例调试遇到的坑第一次跑通后mAP反而降了2个点。排查发现是坐标转换问题——anchor-free分支预测的是相对grid cell的偏移但我在loss计算时误用了绝对坐标。改完这个bug后涨点1.5%。第二个坑在推理阶段。两个分支的输出需要做加权融合但直接相加会导致重复检测。后来改成用anchor-free分支的结果去修正低置信度的anchor-based预测# 推理时融合策略defmerge_predictions(anchor_pred,af_pred,weight0.3):# anchor_pred: [N, 6], af_pred: [M, 6]# 优先用anchor-based结果mergedanchor_pred.clone()# 对anchor-based里置信度低于0.3的预测用anchor-free结果补充low_conf_maskmerged[:,4]0.3iflow_conf_mask.any():# 这里可以加个NMS去重我偷懒没写实际部署要加上mergedtorch.cat([merged,af_pred[af_pred[:,4]0.5]])returnmerged经验建议不要一上来就全量训练先用小数据集跑几个epoch看两个分支的loss是否都正常下降。如果anchor-free分支loss震荡太大调低它的权重到0.2慢慢往上加。SimOTA的topk设置要灵活我试过固定topk5效果不如动态调整。大目标给5-10个正样本小目标给1-3个这个策略在无人机航拍数据集上特别有效。部署时考虑计算量anchor-free分支会增加约15%的推理时间。如果端侧设备资源紧张可以只在训练时用这个分支导出模型时保留权重但做分支剪枝——不过这样会损失部分精度需要权衡。可视化中间结果训练时每隔几个epoch就把两个分支的预测结果画出来对比能直观看出anchor-free分支补漏了哪些目标。我就是在可视化时发现它特别擅长检测长宽比大于5:1的物体。这个改造在工业缺陷检测项目上最终提升了3.2%的mAP主要提升都在那些形状不规则的缺陷上。anchor机制就像固定网眼的渔网总能漏掉一些鱼加个anchor-free分支相当于多了套自适应网眼虽然重了点但捞得全。

更多文章