
1. 从一次误检排查说起为什么anchor generator值得单独拎出来讲刚接触faster RCNN那会儿我一度以为anchor generator就是个生成一堆框的工具函数调包跑通就行没什么好深究的。直到有一次在自建数据集上训练检测模型小目标召回率死活上不去mAP卡在一个尴尬的数字上不动排查了数据增强、学习率、backbone深度甚至怀疑过标注质量最后定位到的居然是anchor的尺度和长宽比配置跟数据分布严重不匹配。那次之后我才真正意识到anchor generator这个看起来不起眼的模块实际上是整个faster RCNN检测流程里定义什么叫候选目标的源头它决定了RPN在特征图上以什么样的先验假设去搜索物体。这篇内容就是把我对anchor generator的理解完整梳理一遍。核心围绕三件事展开anchor到底是怎么从特征图上长出来的、它的尺度和长宽比参数该怎么根据数据去调、以及在实际训练中围绕anchor有哪些容易踩的坑。关键词方面涉及faster RCNN、anchor generator、RPN、特征图、感受野、尺度、长宽比、IoU、正负样本划分这些概念我会在行文中自然带出来不会堆砌术语。适合谁看如果你已经跑通过faster RCNN的demo但对RPN里anchor的生成逻辑只有模糊印象或者正在自己数据集上训练检测模型、被小目标或密集目标折磨那这篇应该能帮你把这块知识补扎实。如果你是完全的新手建议先了解卷积、特征图、边界框回归这些基础概念再来看理解会更顺。下面我尽量用从业者之间聊天的口吻把原理、参数、实操和踩坑经验揉在一起讲清楚。2. anchor generator在faster RCNN里到底扮演什么角色2.1 先搞清楚RPN要解决的是什么问题faster RCNN相比它的前身fast RCNN最大的改进就是用一个RPNRegion Proposal Network网络替代了传统的选择性搜索来生成候选区域。选择性搜索那套东西慢且不可学习RPN则把找候选框这件事变成了一个可以端到端训练的卷积网络任务。而RPN要完成这个任务前提是它得有一个搜索起点——也就是在特征图的每个位置上预先放置一批不同大小、不同形状的参考框这些参考框就是anchor。所以anchor generator的职责非常明确给定特征图的尺寸、下采样倍数、一组尺度scale和一组长宽比aspect ratio生成出覆盖整张图的所有anchor框的坐标。这些anchor是先验框是假设是RPN用来做偏移量回归和前景背景分类的基准。RPN并不直接预测物体的绝对坐标而是预测相对于某个anchor真实框应该往哪个方向偏移多少、宽高缩放多少。理解了这一点你就明白为什么anchor的设计会直接影响检测性能——如果没有任何一个anchor跟真实目标在形状上接近那回归任务就会变得极其困难甚至根本学不出来。2.2 anchor不是随便撒的框它带着强烈的先验假设很多人把anchor理解成在图上均匀撒一堆框这个理解不算错但太浅了。anchor的本质是一组关于目标可能长什么样的先验假设。你配置了三种尺度比如128、256、512和三种长宽比比如0.5、1、2那你就等于告诉网络我认为这张图里的目标大小大概在这三个档位形状大概在这三种比例附近。这个假设如果跟真实数据分布吻合RPN学起来就轻松收敛快、召回高如果偏离很大网络就得花大量力气去纠正这些先验效果自然打折。我在一个工业质检场景里见过缺陷目标普遍是细长条状长宽比能到8:1甚至10:1但默认配置里最大长宽比才2结果就是大量缺陷目标根本找不到匹配的anchor召回率惨不忍睹。后来把长宽比加上4和8两档同样的网络结构召回直接涨了一大截。这就是anchor先验假设的威力。2.3 特征图上的一个点对应原图的一大片区域这里有个容易被忽略但很关键的点anchor是在特征图上生成的但它的坐标要映射回原图。特征图经过多次下采样一个像素点对应原图上的一个区域这个区域的大小就是感受野。比如常见的backbone下采样16倍特征图上每个点对应原图16×16的区域。anchor generator在特征图的每个位置生成anchor时是以这个位置映射回原图的中心点为基准然后按照配置的尺度和长宽比往外扩。这就带来一个直接后果特征图的分辨率决定了anchor的密度。特征图越稀疏下采样倍数越大anchor在原图上的间距就越大小目标就越容易被漏掉——因为可能两个相邻anchor之间正好夹着一个很小的目标谁都没覆盖到它。这也是为什么后来FPNFeature Pyramid Network会成为标配它用多个尺度的特征图分别生成anchor浅层特征图分辨率高、负责小目标深层特征图分辨率低、负责大目标本质上就是在解决单一特征图上anchor密度和尺度覆盖不足的问题。3. anchor的生成逻辑从配置参数到坐标矩阵的完整推导3.1 输入参数有哪些每个参数意味着什么要理解anchor生成先把输入参数列清楚。以常见的实现为例anchor generator的核心输入包括特征图尺寸通常是H, W比如经过backbone后得到60×40的特征图。下采样倍数stride特征图相对原图的缩放比例常见是16或32。它决定了anchor中心点在原图上的间距。尺度列表scales一组面积相关的参数常见如[8, 16, 32]这是相对特征图的尺度或[128, 256, 512]这是相对原图的尺度不同实现约定不同这点后面会专门讲坑。长宽比列表ratios一组宽高比常见如[0.5, 1.0, 2.0]。anchor中心点由特征图每个位置映射回原图得到。这里最容易混淆的就是scale到底是相对特征图还是相对原图。在torchvision的AnchorGenerator里scales是相对原图的比如scales(128, 256, 512)它直接就是anchor的基准边长而在一些从零实现的代码里scales可能是相对特征图的需要乘以stride才映射回原图。这个差异如果不注意anchor大小会差出十几倍训练直接崩掉。3.2 一个anchor的宽高是怎么算出来的给定一个基准尺度s和一个长宽比ranchor的宽高计算方式是w s * sqrt(1/r) h s * sqrt(r)这个公式的推导逻辑是让anchor的面积保持为s²同时宽高比等于r。因为w * h s²且w / h r联立就能解出上面两个式子。这样设计的好处是不同长宽比的anchor面积一致只有形状不同方便控制变量。举个例子s256r0.5时w 256 * sqrt(2) ≈ 362h 256 * sqrt(0.5) ≈ 181得到一个宽扁的框r2时反过来w≈181h≈362得到一个瘦高的框r1时就是256×256的正方形。三种尺度乘以三种长宽比每个位置就生成9个anchor这就是faster RCNN论文里的经典配置。3.3 从单个位置到整张特征图的坐标矩阵单个位置的anchor算出来后要把它平移到特征图的每个位置。假设特征图是H×W每个位置生成A个anchorA len(scales) × len(ratios)那总共就有H×W×A个anchor。这些anchor的坐标通常组织成一个形状为(H, W, A, 4)的张量最后一维是(x1, y1, x2, y2)或者中心点加宽高的形式。生成过程用伪代码表达大概是for i in range(H): for j in range(W): center_x j * stride stride / 2 center_y i * stride stride / 2 for scale in scales: for ratio in ratios: w scale * sqrt(1 / ratio) h scale * sqrt(ratio) x1 center_x - w / 2 y1 center_y - h / 2 x2 center_x w / 2 y2 center_y h / 2 anchors.append([x1, y1, x2, y2])实际实现当然会用向量化操作不会真的写双重循环但逻辑就是这样。注意中心点的计算用了j * stride stride / 2加半个stride是为了让anchor中心落在特征图每个像素对应的原图区域的正中间这个细节在从零实现时如果漏掉anchor会整体偏移半个stride虽然影响不算致命但会让坐标对齐变得别扭。3.4 生成之后的anchor还要经过哪些处理anchor生成出来只是第一步后面还有几道工序。首先是裁剪把超出原图边界的anchor裁到图像范围内避免无效框参与后续计算。然后是过滤把宽或高小于某个阈值的anchor丢掉这种框太小没有意义。接着在训练阶段还要根据anchor和真实框的IoU做正负样本划分IoU大于高阈值比如0.7的算正样本小于低阈值比如0.3的算负样本中间的忽略掉。这一步直接决定了RPN的学习信号质量。这里有个经验正负样本的比例很关键。如果负样本远多于正样本这几乎是必然的因为背景anchor占绝大多数需要做采样平衡常见做法是随机采样固定数量的正负样本比例控制在1:1到1:3之间。我试过完全不采样让所有anchor都参与损失计算结果就是负样本的梯度把正样本淹没了模型倾向于把所有框都预测成背景召回率极低。这个坑很隐蔽因为损失曲线看起来还在下降但实际学出来的东西没用。4. 尺度和长宽比怎么调从数据分布反推anchor配置4.1 先统计你的数据别拍脑袋设参数默认的scales(128, 256, 512)、ratios(0.5, 1, 2)是COCO和VOC这类通用数据集上调出来的经验值放到你自己的数据上未必合适。正确做法是先对训练集的标注框做统计分析把所有真实框的宽高统计出来画出宽高分布图看看目标主要集中在什么尺寸区间、什么长宽比区间。具体操作上可以写个脚本遍历标注文件收集每个框的宽高然后做几件事算宽高的分位数比如5%、50%、95%分位看目标尺寸的跨度算长宽比的分布看是偏正方形还是偏细长把宽高画成散点图直观感受分布形态。有了这些统计你就能有的放矢地设置scales和ratios而不是盲目套默认值。4.2 尺度设置覆盖目标尺寸的主要区间scales的设置原则是覆盖数据中目标尺寸的主要区间。假设你统计出来目标边长主要集中在40到300像素之间那scales可以设成[64, 128, 256]或者[48, 96, 192, 384]这样让anchor的尺度跨度能罩住大部分目标。注意anchor的尺度不是越多越好每增加一个尺度anchor数量线性增长计算量和显存占用都会上升而且过多的尺度会导致正负样本划分时匹配混乱。一般3到5个尺度是比较合理的范围。还有一个容易被忽略的点anchor的尺度应该跟特征图的感受野匹配。如果某个特征图的下采样倍数是16感受野大概是几十到上百像素那在这个特征图上生成一个512像素的大anchor它的实际覆盖范围远超感受野网络根本看不到这么大的区域这个anchor就是无效的。所以深层特征图配大anchor、浅层特征图配小anchor这是FPN的基本设计思想也是单特征图场景下需要权衡的地方。4.3 长宽比设置别让细长目标无处安放长宽比的设置同理要看数据里目标的形状分布。通用数据集里目标大多接近正方形或轻微长方形所以[0.5, 1, 2]够用。但如果你的数据里有大量细长目标比如电线杆、行人、某些工业零件就得把长宽比往两端扩展加上0.25、0.33、3、4甚至更大。这里有个实操技巧长宽比和尺度是可以组合出更多形状的。比如你设了scales[64, 128, 256]和ratios[0.25, 0.5, 1, 2, 4]那每个位置就有15个anchor覆盖的形状和大小组合相当丰富。但组合数太多也会带来问题后面讲计算开销时会细说。我的经验是先用统计结果确定一个粗略范围然后通过消融实验微调看哪个配置在验证集上mAP最高。4.4 一个真实场景的配置调整过程回到开头提到的工业质检场景。那个项目里缺陷目标的特点是尺寸偏小大多在30到80像素形状细长长宽比集中在3到8之间。初始配置用的是默认的scales(128, 256, 512)、ratios(0.5, 1, 2)结果小目标召回率只有40%多。调整过程分两步。第一步改scales把尺度整体下移改成(32, 64, 128)让anchor能覆盖到小目标第二步改ratios加上3和6两档覆盖细长形状。改完之后召回率涨到75%左右。后来又发现有些缺陷特别小20像素左右在原有特征图上anchor间距太大覆盖不到于是引入了更高分辨率的特征图专门负责这些小目标召回率最终稳定在85%以上。整个过程没有动网络结构纯粹是anchor配置和特征图选择的调整效果却非常明显。这个案例说明anchor配置不是玄学而是有明确数据依据的工程决策。你花半小时统计一下数据分布可能比调一周学习率都管用。5. 围绕anchor的那些坑从训练不收敛到显存爆炸5.1 坑一scale相对谁没搞清anchor大小差出天际前面提过不同实现里scale的参照系不一样。torchvision的AnchorGenerator里scales是相对原图的绝对尺寸而有些从零写的代码里scales是相对特征图的需要乘stride。我有一次参考某份开源实现改代码没注意这个差异把相对特征图的scale直接喂给了期望绝对尺度的接口结果anchor全变成了十几像素的小框训练时正样本几乎为零损失一直不降。排查了大半天才反应过来是单位问题。避坑方法很简单生成anchor后打印几个anchor的宽高看看跟你的预期对比。如果你设了scale256结果打印出来宽高是16左右那基本就是参照系搞错了。这个检查花不了一分钟能省下几小时排查时间。5.2 坑二anchor数量爆炸导致显存不够anchor的总数是H×W×A。假设特征图是60×40A9那就是21600个anchor如果A增加到15更多尺度和长宽比就变成36000个。这还只是一张图、一个特征图层级。如果用了FPN多个层级加起来anchor数量轻松上十万。训练时这些anchor都要参与IoU计算和损失计算显存和计算量都会飙升。我遇到过一次显存爆炸排查发现是ratios里手滑多写了两档A从9变成15显存直接多了近一倍。所以配置anchor时要有数量意识每增加一档scale或ratio都要想想是不是真的必要。另外训练时可以通过采样来控制参与损失的anchor数量不必让所有anchor都算损失这也是常规做法。5.3 坑三正负样本划分阈值设得不合理正样本的IoU阈值通常0.7和负样本阈值通常0.3这两个数不是随便定的。阈值太高正样本太少网络学不到东西阈值太低正样本里混入大量定位不准的框回归目标噪声大。负样本阈值同理。在一些特殊数据上默认阈值可能不合适。比如密集小目标场景anchor和真实框的IoU普遍偏低如果还用0.7做正样本阈值可能一个正样本都找不出来。这时候要么降低阈值要么改用其他匹配策略比如保证每个真实框至少匹配一个IoU最高的anchor。我见过有项目因为正样本为零导致RPN完全学不动最后就是靠每个真实框强制匹配一个anchor这个兜底策略救回来的。5.4 坑四忽略anchor裁剪和过滤带来的边界问题anchor生成后裁剪到图像边界这个操作本身没问题但裁剪会改变anchor的宽高比和面积。一个原本256×256的anchor如果中心在图像边缘裁剪后可能变成128×256形状完全变了。如果这种被裁剪的anchor还被划为正样本回归目标就会很奇怪。更稳妥的做法是在划分正负样本时把那些被严重裁剪的anchor比如裁剪后面积不到原来一半的直接排除不参与训练。另外过滤掉宽高过小的anchor也很重要这种框即使匹配上了真实框回归出来的结果也没意义。这些细节在标准实现里可能已经处理了但如果你自己改代码很容易漏掉。6. 把anchor generator用好的几个进阶思路6.1 用聚类代替手工设置尺度和长宽比手工统计加试错虽然有效但比较费时。一个更系统的做法是用k-means之类的聚类算法对所有真实框的宽高做聚类聚出k个簇心直接把簇心的宽高作为anchor的尺度和长宽比。YOLO系列里这套做法用得比较多faster RCNN里同样适用。聚类的k值就是anchor的种类数一般取5到9。这样得到的anchor是数据驱动的比手工拍脑袋更贴合实际分布。聚类的距离度量要注意不能用普通的欧氏距离因为大框的绝对差异天然比小框大会导致聚类偏向大框。常用的做法是用IoU作为相似度或者用宽高的比值作为距离。这块细节比较多如果要做建议先在小数据集上验证效果。6.2 多尺度特征图上的anchor分配策略用FPN之后anchor不再只在单一特征图上生成而是每个层级各生成一套。这时候就有一个分配问题一个真实框应该由哪个层级的anchor来负责常见策略是按目标尺寸分配小目标给浅层大目标给深层。具体阈值可以根据每层的anchor尺度来定比如某层的anchor尺度是64那尺寸在32到128之间的目标就归这层管。这个分配策略直接影响训练效果。如果分配不合理比如大目标被分到了浅层浅层的感受野不够回归会很难。我一般会打印每个层级负责的目标数量分布看看是否均衡如果某一层几乎没分到目标那这层的anchor配置可能就有问题。6.3 anchor-free思路对anchor generator的启发近几年anchor-free的检测方法越来越流行它们干脆不用anchor直接预测目标中心点和尺寸。这不代表anchor generator就没用了而是说anchor这套先验机制有它的局限性——它引入了额外的超参数增加了调参负担。理解anchor-free的思路反过来能帮你更好地理解anchor的价值和边界。比如anchor-free里的中心点采样本质上是在解决anchor正负样本划分不均衡的问题它的尺度预测本质上是在替代anchor的尺度先验。如果你把anchor generator理解透了再看anchor-free的方法会发现两者在解决同一批问题的不同侧面很多设计思想是相通的。6.4 调试anchor配置的实用检查清单最后给一份我在实际项目里常用的检查清单每次调整anchor配置后过一遍能避开大部分低级错误检查项检查方法预期结果anchor尺度是否正确打印前几个anchor的宽高与配置的scale量级一致anchor数量是否合理计算H×W×A单层不超过几万多层总量可控正样本比例是否正常统计IoU大于高阈值的anchor占比一般不低于千分之几负样本是否过多统计IoU小于低阈值的anchor占比通常占绝大多数需采样边界裁剪是否严重统计被裁剪anchor的比例比例过高需检查中心点计算各层级目标分配是否均衡统计每层负责的真实框数量不应有层级几乎为空这份清单看起来简单但每一条背后都对应着我踩过的坑。比如正样本比例这一条有一次我统计发现正样本占比不到万分之一排查发现是scale设得太大所有anchor都比目标大好几倍IoU自然上不去。如果没有这个检查可能又要花很久才能定位。anchor generator这个模块代码量不大但牵扯到数据分布、特征图特性、训练策略等多个方面。把它吃透你对整个检测流程的理解会上一个台阶。我在实际项目里的体会是与其花大量时间调网络结构和学习率不如先把anchor配置跟数据对齐这往往是性价比最高的优化手段。后续如果要做模型压缩或者部署anchor的数量和分布也会直接影响推理速度所以这块知识值得反复琢磨。