014、Anchor-Based vs Anchor-Free两种检测范式的历史演进与网格预测本质上周帮同事调一个Anchor-Free模型的收敛问题发现他在输出层直接用了Sigmoid激活结果训练到第50个epoch损失还在0.8附近震荡。我问他“你GT的center-ness怎么算的”他一脸茫然。这个场景让我想起三年前自己第一次写YOLOv3时把Anchor的宽高比设成了1:1、2:1、1:2结果小目标全漏检了。两种范式看似不同但底层都在做同一件事——把连续空间离散化成网格再让每个网格学会“负责”它该管的目标。从Anchor-Based说起YOLOv2的“顿悟时刻”YOLOv1的原始设计其实很暴力——直接把图像分成7×7网格每个网格预测2个框。但有个致命问题如果两个目标中心落在同一个网格里模型就“选择性失明”了。YOLOv2引入Anchor时Redmon在论文里写得很直白“We use anchor boxes to predict bounding boxes.” 但实际实现时有个关键细节常被忽略——Anchor不是直接预测坐标而是预测相对于网格的偏移量。看YOLOv2的源码我翻的是darknet的原始C代码后来移植到PyTorch时踩过坑# 假设特征图尺寸为13x13每个网格有5个Anchor# 网络输出维度: [batch, 5*(41num_classes), 13, 13]# 注意这里4个坐标是tx, ty, tw, th不是直接坐标defdecode_boxes(pred,anchors,grid_size13):# pred shape: [batch, 5*5, 13, 13] 假设5个anchor每个5个参数(tx,ty,tw,th,obj)# 别这样写直接sigmoid(tx) grid_x要先把grid坐标生成好batch,_,h,wpred.shape num_anchorslen(anchors)# 生成网格坐标这里踩过坑grid_x和grid_y必须用float否则后面加法会类型错误grid_y,grid_xtorch.meshgrid(torch.arange(h),torch.arange(w),indexingij)grid_xgrid_x.float().to(pred.device)grid_ygrid_y.float().to(pred.device)# 解析预测值predpred.view(batch,num_anchors,5,h,w)txtorch.sigmoid(pred[:,:,0])# 中心x偏移范围0~1tytorch.sigmoid(pred[:,:,1])# 中心y偏移twpred[:,:,2]# 宽高log尺度别用sigmoid这里很多人写错thpred[:,:,3]# 解码到特征图尺度bxtxgrid_x# 网格内偏移 网格索引bytygrid_y bwanchors[:,0].view(1,-1,1,1)*torch.exp(tw)# anchor宽 * exp(tw)bhanchors[:,1].view(1,-1,1,1)*torch.exp(th)returnbx,by,bw,bh注意tw和th没有经过Sigmoid因为宽高需要能放大到超过网格范围。我见过有人把tw也sigmoid了结果大目标永远预测不准——因为exp(sigmoid(x))最大才e倍而实际目标可能比anchor大10倍。Anchor-Free的崛起CenterNet和FCOS的“反叛”2019年FCOS出来时很多人觉得Anchor-Free要取代Anchor-Based了。但仔细看FCOS的论文它其实没有完全抛弃“网格”的概念——它只是把每个像素点当作一个“隐式Anchor”。FCOS的核心思想对于特征图上的每个位置如果它落在某个GT框内部就负责预测这个框。看FCOS的标签分配代码这里有个容易忽略的细节# 假设特征图stride8输入图像尺寸512x512# 特征图尺寸64x64每个位置对应原图的一个8x8区域deffcos_assign_gt(features,gt_boxes,gt_labels,stride8):# features shape: [batch, 64, 64]h,wfeatures.shape[-2:]# 生成所有像素点的坐标在原图尺度ys,xstorch.meshgrid(torch.arange(h),torch.arange(w),indexingij)pointstorch.stack([xs,ys],dim-1)*stridestride//2# 像素中心点# 对每个GT框计算哪些点在框内# 注意这里要处理边界情况点刚好在框边界上算不算FCOS论文说算lpoints[...,0]-gt_boxes[:,0]# 左边界距离tpoints[...,1]-gt_boxes[:,1]# 上边界距离rgt_boxes[:,2]-points[...,0]# 右边界距离bgt_boxes[:,3]-points[...,1]# 下边界距离# 所有距离必须为正表示点在框内insidetorch.stack([l,t,r,b],dim-1).min(dim-1).values0# 这里踩过坑如果多个GT框重叠一个点可能属于多个框# 解决方案选择面积最小的GT框因为小目标更需要精细监督# 别这样写直接取第一个匹配的GT会导致大目标覆盖小目标areas(gt_boxes[:,2]-gt_boxes[:,0])*(gt_boxes[:,3]-gt_boxes[:,1])# 对每个点找到所有匹配的GT中面积最小的那个# 实现略复杂但核心逻辑是小目标优先FCOS还引入了center-ness分支用来抑制低质量预测。这个分支的标签计算方式很巧妙——它让模型学习“我离目标中心有多远”。但很多人实现时直接算L2距离忽略了归一化# center-ness的正确计算方式defcompute_centerness(l,t,r,b):# l,t,r,b是点到GT框四条边的距离# 别这样写直接算 sqrt((l-r)^2 (t-b)^2)# 正确做法用min/max比值left_righttorch.stack([l,r],dim-1)top_bottomtorch.stack([t,b],dim-1)centerness(left_right.min(dim-1).values/left_right.max(dim-1).values)*\(top_bottom.min(dim-1).values/top_bottom.max(dim-1).values)# 开平方让分布更平滑centernesstorch.sqrt(centerness1e-8)returncenterness这个center-ness在推理时作为权重乘到分类得分上能有效过滤掉那些“框对了但位置偏”的预测。我见过有人直接去掉这个分支结果mAP掉了3个点。两种范式的本质网格预测的“变与不变”回到最底层Anchor-Based和Anchor-Free都在做同一件事把图像空间离散化成网格每个网格负责预测落在它“管辖范围”内的目标。区别在于Anchor-Based每个网格有K个预设的“形状模板”Anchor预测的是相对于模板的偏移。网格的“管辖范围”是固定的——中心点落在网格内的目标。Anchor-Free每个网格像素点没有预设形状预测的是到目标四条边的距离。网格的“管辖范围”是动态的——只要目标框覆盖了这个点它就负责。从数学上看YOLOv3的预测公式可以写成bx σ(tx) cx # cx是网格左上角x坐标 by σ(ty) cy bw pw * e^(tw) # pw是anchor宽度 bh ph * e^(th) # ph是anchor高度而FCOS的预测公式bx cx σ(tx) * stride # 其实也可以写成类似形式 by cy σ(ty) * stride bw l r # 左边界右边界 bh t b # 上边界下边界注意FCOS的bw和bh是直接预测绝对值而YOLOv3是相对于anchor的缩放。这就是为什么FCOS对大目标更友好——它不需要anchor来“兜底”尺寸范围。实际调试中的“坑”与“解”去年我在一个工业检测项目里尝试从YOLOv5切换到FCOS遇到了几个典型问题问题1小目标召回率骤降YOLOv5用Anchor时小目标有专门的Anchor尺寸比如4x4、8x8模型能“聚焦”在小尺度上。FCOS没有这个机制所有尺度共享同一个特征金字塔。解决方案在FCOS的FPN中给浅层特征图增加额外的可变形卷积让模型学会“放大”小目标的特征响应。问题2训练初期loss爆炸FCOS的回归分支直接预测绝对值l,t,r,b如果初始化不好预测值可能非常大。YOLOv3用exp(tw)虽然也会爆炸但因为有anchor的约束爆炸范围有限。解决方案对回归分支的输出做Normalization比如除以特征图尺寸让预测值落在合理范围内。问题3多尺度目标冲突在FCOS中一个点可能同时属于大目标和小目标比如人的身体和手上的手机。按照面积最小优先的规则手机被分配但人的身体就没人管了。解决方案引入ATSSAdaptive Training Sample Selection动态调整正负样本分配策略。个人经验什么时候选什么如果你在做一个新项目我的建议是选Anchor-Based的场景目标尺度分布非常集中比如都是行人宽高比1:2左右算力有限需要快速部署YOLOv8的Anchor-Free版本其实也很快但Anchor-Based的优化更成熟小目标占比高Anchor可以专门设计小尺度模板选Anchor-Free的场景目标尺度变化极大比如同时有汽车和交通标志目标形状不规则比如旋转目标、长条形目标不想手动调Anchor参数虽然现在有K-means自动聚类但总归多一步我的“真香”组合现在很多模型如YOLOX、YOLOv8都用了Anchor-Free的head但保留了Anchor-Based的标签分配策略。比如YOLOX的SimOTA本质上是用Anchor-Free的预测方式但用Anchor-Based的“每个网格预测多个框”的思路。这种混合范式在实际工程中往往效果最好——既避免了手动设计Anchor又保留了多预测头的优势。最后说一句别纠结于“哪个范式更好”而是理解它们都在解决同一个问题——如何让离散的网格学会预测连续的目标位置。Anchor-Based用“模板偏移”来近似Anchor-Free用“距离回归”来直接预测。两种思路在数学上是等价的只是工程实现上的trade-off不同。下次调参时多想想你的数据分布更适合哪种“离散化”方式比盲目跟风新模型更有用。
014、Anchor-Based vs Anchor-Free:两种检测范式的历史演进与网格预测本质
014、Anchor-Based vs Anchor-Free两种检测范式的历史演进与网格预测本质上周帮同事调一个Anchor-Free模型的收敛问题发现他在输出层直接用了Sigmoid激活结果训练到第50个epoch损失还在0.8附近震荡。我问他“你GT的center-ness怎么算的”他一脸茫然。这个场景让我想起三年前自己第一次写YOLOv3时把Anchor的宽高比设成了1:1、2:1、1:2结果小目标全漏检了。两种范式看似不同但底层都在做同一件事——把连续空间离散化成网格再让每个网格学会“负责”它该管的目标。从Anchor-Based说起YOLOv2的“顿悟时刻”YOLOv1的原始设计其实很暴力——直接把图像分成7×7网格每个网格预测2个框。但有个致命问题如果两个目标中心落在同一个网格里模型就“选择性失明”了。YOLOv2引入Anchor时Redmon在论文里写得很直白“We use anchor boxes to predict bounding boxes.” 但实际实现时有个关键细节常被忽略——Anchor不是直接预测坐标而是预测相对于网格的偏移量。看YOLOv2的源码我翻的是darknet的原始C代码后来移植到PyTorch时踩过坑# 假设特征图尺寸为13x13每个网格有5个Anchor# 网络输出维度: [batch, 5*(41num_classes), 13, 13]# 注意这里4个坐标是tx, ty, tw, th不是直接坐标defdecode_boxes(pred,anchors,grid_size13):# pred shape: [batch, 5*5, 13, 13] 假设5个anchor每个5个参数(tx,ty,tw,th,obj)# 别这样写直接sigmoid(tx) grid_x要先把grid坐标生成好batch,_,h,wpred.shape num_anchorslen(anchors)# 生成网格坐标这里踩过坑grid_x和grid_y必须用float否则后面加法会类型错误grid_y,grid_xtorch.meshgrid(torch.arange(h),torch.arange(w),indexingij)grid_xgrid_x.float().to(pred.device)grid_ygrid_y.float().to(pred.device)# 解析预测值predpred.view(batch,num_anchors,5,h,w)txtorch.sigmoid(pred[:,:,0])# 中心x偏移范围0~1tytorch.sigmoid(pred[:,:,1])# 中心y偏移twpred[:,:,2]# 宽高log尺度别用sigmoid这里很多人写错thpred[:,:,3]# 解码到特征图尺度bxtxgrid_x# 网格内偏移 网格索引bytygrid_y bwanchors[:,0].view(1,-1,1,1)*torch.exp(tw)# anchor宽 * exp(tw)bhanchors[:,1].view(1,-1,1,1)*torch.exp(th)returnbx,by,bw,bh注意tw和th没有经过Sigmoid因为宽高需要能放大到超过网格范围。我见过有人把tw也sigmoid了结果大目标永远预测不准——因为exp(sigmoid(x))最大才e倍而实际目标可能比anchor大10倍。Anchor-Free的崛起CenterNet和FCOS的“反叛”2019年FCOS出来时很多人觉得Anchor-Free要取代Anchor-Based了。但仔细看FCOS的论文它其实没有完全抛弃“网格”的概念——它只是把每个像素点当作一个“隐式Anchor”。FCOS的核心思想对于特征图上的每个位置如果它落在某个GT框内部就负责预测这个框。看FCOS的标签分配代码这里有个容易忽略的细节# 假设特征图stride8输入图像尺寸512x512# 特征图尺寸64x64每个位置对应原图的一个8x8区域deffcos_assign_gt(features,gt_boxes,gt_labels,stride8):# features shape: [batch, 64, 64]h,wfeatures.shape[-2:]# 生成所有像素点的坐标在原图尺度ys,xstorch.meshgrid(torch.arange(h),torch.arange(w),indexingij)pointstorch.stack([xs,ys],dim-1)*stridestride//2# 像素中心点# 对每个GT框计算哪些点在框内# 注意这里要处理边界情况点刚好在框边界上算不算FCOS论文说算lpoints[...,0]-gt_boxes[:,0]# 左边界距离tpoints[...,1]-gt_boxes[:,1]# 上边界距离rgt_boxes[:,2]-points[...,0]# 右边界距离bgt_boxes[:,3]-points[...,1]# 下边界距离# 所有距离必须为正表示点在框内insidetorch.stack([l,t,r,b],dim-1).min(dim-1).values0# 这里踩过坑如果多个GT框重叠一个点可能属于多个框# 解决方案选择面积最小的GT框因为小目标更需要精细监督# 别这样写直接取第一个匹配的GT会导致大目标覆盖小目标areas(gt_boxes[:,2]-gt_boxes[:,0])*(gt_boxes[:,3]-gt_boxes[:,1])# 对每个点找到所有匹配的GT中面积最小的那个# 实现略复杂但核心逻辑是小目标优先FCOS还引入了center-ness分支用来抑制低质量预测。这个分支的标签计算方式很巧妙——它让模型学习“我离目标中心有多远”。但很多人实现时直接算L2距离忽略了归一化# center-ness的正确计算方式defcompute_centerness(l,t,r,b):# l,t,r,b是点到GT框四条边的距离# 别这样写直接算 sqrt((l-r)^2 (t-b)^2)# 正确做法用min/max比值left_righttorch.stack([l,r],dim-1)top_bottomtorch.stack([t,b],dim-1)centerness(left_right.min(dim-1).values/left_right.max(dim-1).values)*\(top_bottom.min(dim-1).values/top_bottom.max(dim-1).values)# 开平方让分布更平滑centernesstorch.sqrt(centerness1e-8)returncenterness这个center-ness在推理时作为权重乘到分类得分上能有效过滤掉那些“框对了但位置偏”的预测。我见过有人直接去掉这个分支结果mAP掉了3个点。两种范式的本质网格预测的“变与不变”回到最底层Anchor-Based和Anchor-Free都在做同一件事把图像空间离散化成网格每个网格负责预测落在它“管辖范围”内的目标。区别在于Anchor-Based每个网格有K个预设的“形状模板”Anchor预测的是相对于模板的偏移。网格的“管辖范围”是固定的——中心点落在网格内的目标。Anchor-Free每个网格像素点没有预设形状预测的是到目标四条边的距离。网格的“管辖范围”是动态的——只要目标框覆盖了这个点它就负责。从数学上看YOLOv3的预测公式可以写成bx σ(tx) cx # cx是网格左上角x坐标 by σ(ty) cy bw pw * e^(tw) # pw是anchor宽度 bh ph * e^(th) # ph是anchor高度而FCOS的预测公式bx cx σ(tx) * stride # 其实也可以写成类似形式 by cy σ(ty) * stride bw l r # 左边界右边界 bh t b # 上边界下边界注意FCOS的bw和bh是直接预测绝对值而YOLOv3是相对于anchor的缩放。这就是为什么FCOS对大目标更友好——它不需要anchor来“兜底”尺寸范围。实际调试中的“坑”与“解”去年我在一个工业检测项目里尝试从YOLOv5切换到FCOS遇到了几个典型问题问题1小目标召回率骤降YOLOv5用Anchor时小目标有专门的Anchor尺寸比如4x4、8x8模型能“聚焦”在小尺度上。FCOS没有这个机制所有尺度共享同一个特征金字塔。解决方案在FCOS的FPN中给浅层特征图增加额外的可变形卷积让模型学会“放大”小目标的特征响应。问题2训练初期loss爆炸FCOS的回归分支直接预测绝对值l,t,r,b如果初始化不好预测值可能非常大。YOLOv3用exp(tw)虽然也会爆炸但因为有anchor的约束爆炸范围有限。解决方案对回归分支的输出做Normalization比如除以特征图尺寸让预测值落在合理范围内。问题3多尺度目标冲突在FCOS中一个点可能同时属于大目标和小目标比如人的身体和手上的手机。按照面积最小优先的规则手机被分配但人的身体就没人管了。解决方案引入ATSSAdaptive Training Sample Selection动态调整正负样本分配策略。个人经验什么时候选什么如果你在做一个新项目我的建议是选Anchor-Based的场景目标尺度分布非常集中比如都是行人宽高比1:2左右算力有限需要快速部署YOLOv8的Anchor-Free版本其实也很快但Anchor-Based的优化更成熟小目标占比高Anchor可以专门设计小尺度模板选Anchor-Free的场景目标尺度变化极大比如同时有汽车和交通标志目标形状不规则比如旋转目标、长条形目标不想手动调Anchor参数虽然现在有K-means自动聚类但总归多一步我的“真香”组合现在很多模型如YOLOX、YOLOv8都用了Anchor-Free的head但保留了Anchor-Based的标签分配策略。比如YOLOX的SimOTA本质上是用Anchor-Free的预测方式但用Anchor-Based的“每个网格预测多个框”的思路。这种混合范式在实际工程中往往效果最好——既避免了手动设计Anchor又保留了多预测头的优势。最后说一句别纠结于“哪个范式更好”而是理解它们都在解决同一个问题——如何让离散的网格学会预测连续的目标位置。Anchor-Based用“模板偏移”来近似Anchor-Free用“距离回归”来直接预测。两种思路在数学上是等价的只是工程实现上的trade-off不同。下次调参时多想想你的数据分布更适合哪种“离散化”方式比盲目跟风新模型更有用。