ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

多尺度锚框机制详解:从公式推导到工程实践

多尺度锚框机制详解:从公式推导到工程实践

做目标检测绕不开的基础模块里,多尺度锚框绝对算一个。很多人第一次听到这个词,觉得它高深,其实拆开看就两件事:什么是锚框,为什么要多个尺度。如果你已经能跑通图像分类,正打算往检测方向走,这个节点值得静下心来推一遍。主流检测框架基本都依赖锚框机制,候选框生成、正负样本分配、边框回归,全是从这条线延伸出去的。这篇文章会把多尺度锚框的来龙去脉、背后的几何原理、代码实现和训练时的常见坑一次性讲透,适合刚入门的初学者,也适合想回头补原理细节的调参选手。

1. 多尺度锚框到底在解决什么问题

1.1 目标检测的“找”和“认”是两件事

图像分类任务只需要回答“这是什么”,模型看完整张图给出一个类别概率就行。目标检测要再多回答一个问题:“在哪”。这就涉及一个根本性的困境——计算机并不知道目标在图像的哪个位置,需要先有一堆“候选位置”,再判断每个位置里有没有目标、是什么目标。

早期做法是穷举式滑窗:用不同尺寸、不同宽高比的窗口在整张图上滑一遍,每个窗口都送进分类器判断。这个思路直白,但代价大得离谱。一张 512×512 的图,如果窗口尺寸有 20 种、宽高比有 5 种,每个位置还要滑好几个偏移,算下来窗口数量轻松破百万。纯滑窗方案只适合目标尺度固定、场景简单的任务,没法直接用于通用检测。

锚框的出现就是把“穷举”变成了“带先验的取样”。它不再沿图像逐个滑窗,而是预定义一组固定位置、固定尺寸、固定宽高比的矩形框,均匀撒在图上,像在一张地图上提前布好网格。模型后续要做的,不是从零找目标,而是在这些预先撒好的框上做两件事:判断框里有没有目标(分类),以及把框的位置和大小修正到贴近真实目标(回归)。

1.2 只用一个网格为什么不够用

如果只用一种固定尺寸的网格去覆盖目标,会立刻撞上现实问题:目标尺寸差异太大了。远处的一个交通标志可能只有 40×40 像素,近处的卡车却能占据整张图的一半。两者在归一化坐标系里可能相差二十倍,单一尺寸的先验框完全没有能力同时覆盖这两个量级。

用小锚框去匹配大目标,最直接的后果是每个锚框只覆盖了卡车的一个角,IoU(交并比)只有零点零几,模型永远无法把这样的框修正到完整覆盖卡车。反过来,用大锚框去匹配小目标,也会因为框体太大、背景占比过高而无法精准定位。

更麻烦的是,只在一个特征图层面上生成锚框,模型的“视野”也是固定的。浅层特征图分辨率高、保留着小目标的边缘和纹理,但语义信息弱,没法判断“这个像素区域是车还是人”;深层特征图语义信息强,但分辨率低,小目标信息早就被下采样抹掉了。视觉任务讲究“既能看清细节,又能看懂全貌”,单尺度锚框天然做不到这一点。

1.3 锚框的本质:模型预置的“扫描习惯”

把锚框机制理解成“先验扫描习惯”,很多设计上的疑问会豁然开朗。人眼看一幅图时,从来不会用单一密度的格子扫描画面——你会先扫一眼整幅画面的构图,再聚焦到某个小物体上去看细节。这种“由粗到细、由大到小”的扫描策略,翻译到检测模型里,就是多尺度锚框。

每一组锚框其实代表模型对“目标大概会出现在什么位置、大概有多大”的一种先验假设。比如在浅层高分辨率特征图上设置一批小锚框,等于告诉模型:“我预期小目标会出现在这些位置附近,请优先关注它们。”在深层低分辨率特征图上设置大锚框,等于说:“尺寸较大的目标由这边负责。”

所以多尺度锚框的使命不是“让模型更聪明”,而是先把搜索范围剪枝到合理空间,把困难的位置搜索问题拆成多个尺度上的局部修正问题。锚框本身不是预测结果,它只是一个待修正的候选起点。理解这一点,后面看公式和代码时才不会迷失。

2. 单尺度锚框生成:先把公式推明白

2.1 一个像素中心如何发展出多个框

先从最简单的单尺度情况说起。假设输入图像宽度为 W、高度为 H,我们要在一个大小为 fh×fw 的特征图上生成锚框。常规做法是取特征图的每个单元格中心作为锚框中心,该中心映射回原图的归一化坐标就是:

cx = (i + 0.5) / fw
cy = (j + 0.5) / fh

这里 i 和 j 分别是特征图的横纵索引。之所以加 0.5,是因为我们把单元格的中心当作锚点,而不是左上角,这样生成的框能以目标为中心均匀展开,不会系统性偏向某个角落。

确定中心后,每个位置会生成多组框。框的尺寸由一个尺寸集合和一个宽高比集合排列组合决定。尺寸集合记为 sizes,例如 [0.2, 0.35, 0.5],表示框的归一化边长在这三个取值之间变化;宽高比集合记为 ratios,例如 [1, 2, 0.5],表示框的宽高比例。排列组合下来,每个中心点会生成 3×3=9 个候选框。

具体每个框的宽高按如下公式计算:

框宽 = s × sqrt(r)
框高 = s / sqrt(r)

其中 s 是尺寸集合中的某个值,r 是宽高比集合中的某个值。这个公式的精妙之处在于,它能保证框的面积约等于 s² 乘以固定系数,不会因为宽高比改变而出现面积忽大忽小的情况。比如 s=0.4,r=2 时,框宽约 0.566、框高约 0.283,面积约 0.16;r=0.5 时,框宽约 0.283、框高约 0.566,面积同样是约 0.16。两种宽高比覆盖了横向目标和纵向目标,面积却保持一致,这让模型对不同形状物体的匹配难度更均衡。

如果用 unnormalized 像素值表示,直接乘上 W 和 H 即可:

框宽像素 = W × s × sqrt(r)
框高像素 = H × s / sqrt(r)

有了中心和宽高,锚框的归一化坐标就可以写成 xmin=cx-w/2、ymin=cy-h/2、xmax=cx+w/2、ymax=cy+h/2 的形式。整个特征图上的锚框总数是 fh × fw × len(sizes) × len(ratios),以 64×64 特征图、3 种尺寸、3 种宽高比为例,算下来就有 36864 个框,可见锚框的数量规模从一开始就不小。

2.2 交并比 IoU:锚框与真实框的“贴合度量”

生成这么多候选框之后,训练时首先要回答一个问题:哪些锚框是正样本、哪些是负样本?答案由 IoU 决定。IoU 全称 Intersection over Union,计算两个框交集面积与并集面积的比值,取值范围在 0 到 1 之间。IoU 越接近 1,说明两个框重叠程度越高,越可能指向同一个目标。

定义一个锚框 A 与真实目标框 G 的 IoU:

IoU = (A 与 G 交集面积) / (A 面积 + G 面积 - A 与 G 交集面积)

如果锚框完全包裹了真实框,IoU 有可能较低;如果两个框完全重合,IoU 等于 1。用代码实现时,最方便的是向量化计算,一次算完所有锚框与所有真实框的 IoU 矩阵。假设我们已经有了 N 个锚框和 M 个真实框,可以用如下方式:

def box_iou(boxes1, boxes2): # boxes1, boxes2: (N, 4), (M, 4),坐标是 [xmin, ymin, xmax, ymax] area1 = (boxes1[:, 2] - boxes1[:, 0]) * (boxes1[:, 3] - boxes1[:, 1]) area2 = (boxes2[:, 2] - boxes2[:, 0]) * (boxes2[:, 3] - boxes2[:, 1]) lt = torch.max(boxes1[:, None, :2], boxes2[None, :, :2]) rb = torch.min(boxes1[:, None, 2:], boxes2[None, :, 2:]) wh = (rb - lt).clamp(min=0) inter = wh[:, :, 0] * wh[:, :, 1] union = area1[:, None] + area2[None, :] - inter return inter / union

这段代码一次返回 N×M 的 IoU 矩阵,每一行对应一个锚框,每一列对应一个真实框。实际训练中会以这个矩阵为基础做正负样本分配。一个常见的匹配策略分两步:先让每个真实框与所有锚框计算 IoU,挑选出 IoU 最大的锚框作为它的“专属正样本”,保证每个真实目标至少有一个锚框负责预测;随后再遍历所有锚框,凡是与任意真实框的 IoU 超过阈值的,也算作正样本。

2.3 分类分支和回归分支各学什么

锚框参与训练时,每一组锚框都会对应两个学习目标:分类和回归。分类分支学习的是“这个锚框区域内有没有目标、是什么类别”,负样本的类别通常标为背景;回归分支学习的是“锚框需要怎么平移、怎么缩放才能贴合真实框”。

回归目标不是直接回归绝对坐标,而是回归相对偏移量。常见的编码方式是这样的:

dx = (gt_cx - anchor_cx) / anchor_w
dy = (gt_cy - anchor_cy) / anchor_h
dw = log(gt_w / anchor_w)
dh = log(gt_h / anchor_h)

其中 gt_cx、gt_cy、gt_w、gt_h 是真实框的中心与宽高,anchor_* 是对应锚框的量。把偏移量设计成相对量而不是绝对量,是因为不同尺度的锚框本身就有不同量级,直接用绝对像素偏移会让小锚框的回归任务显得“变化很大”,大锚框又显得“变化很小”,不利于统一优化。

到了多尺度阶段,锚框的生成逻辑和训练逻辑都不会改变,改变的是“在不同特征图层面上放不同尺度的框”。所以单尺度的公式必须吃透,因为它是多尺度体系中的最小单元。

3. 多尺度的本质:在不同特征层上分配不同先验

3.1 特征图本身就带着尺度属性

深度卷积网络从输入到输出,特征图分辨率逐层下降,但每个位置的感受野却在逐层扩大。浅层特征图(比如输入下采样 4 倍得到的特征图)保留了比较多的空间细节,适合定位小目标;深层特征图(比如下采样 32 倍得到的特征图)每个位置都看到了输入图上很大一块区域,包含更抽象的语义信息,适合检测占据画面大部分的大目标。

如果只拿某一层特征图来生成锚框,等于强迫这层特征既要做小目标的细节定位,又要做大目标的语义识别,显然超出它的能力范围。多尺度方案的核心思路,就是“让每层特征图负责它擅长的目标尺度,并且把这些不同尺度的预测结果汇总起来”。

举个直观例子:输入 512×512 的图像,如果只在 16×16 的特征图上生成锚框,每个锚框中心之间的间隔就是 32 像素,锚框本身的尺度若设计得较小,感受野却已经覆盖了原图很大一块区域,小目标的信息很难从这层特征图里找回。这就是为什么必须让更浅、分辨率更高的特征图也参与小目标检测。

3.2 特征金字塔与多层锚框的对齐方式

要在多个特征层上做预测,最简单的做法是直接把骨干网络的不同阶段特征拿来做预测,但这样会存在一个明显问题:浅层特征语义信息不足。于是就有了特征金字塔结构(FPN)的思路——高层特征通过上采样和侧向连接,把语义信息逐层传递到浅层,让每一层都兼具高分辨率和较强的语义表达能力。

以典型的特征金字塔为例,假设骨干网络输出四层特征图,分别记为 P2、P3、P4、P5,它们相对输入图的下采样倍数依次是 4、8、16、32。多尺度锚框就会这样配置:P2 层设置一批小锚框(覆盖几十像素的小目标),P3 层设置中等偏小的锚框,P4 层设置中等偏大的锚框,P5 层设置大锚框(覆盖数百像素的大目标)。每一层使用的锚框宽高比一般保持一致,以保证不同尺度下各种形状的目标都被照顾到。

这种“金字塔层级与锚框尺度一一对应”的设计,结构上非常干净。模型推理时,不同层各司其职,最后把各层的检测结果做非极大值抑制(NMS),合并成最终输出。

3.3 锚框尺度的工程取值经验

锚框尺度怎么定,没有一个万能参数,但工程上有比较可靠的参考经验:相邻特征层之间的锚框面积通常按 2 到 4 倍的倍率递增,换算成边长就是约 1.4 到 2 倍。这样能保证各层的召回范围平滑衔接,不会出现大目标和小目标之间的“真空地带”。

以输入 512×512 为例,可以按下面的示意配置来理解各层锚框尺度:

特征图层级下采样倍数特征图尺寸示例锚框边长范围(像素)主要检测目标
P24128×12832 ~ 64小目标
P3864×6464 ~ 128中偏小目标
P41632×32128 ~ 256中偏大目标
P53216×16256 ~ 512大目标

这个表格只是示意,不是必须照抄的参数。实际项目里更科学的做法是先统计自己数据集里所有标注框的尺寸分布,看看目标主要集中在哪些像素范围,再决定每个特征层放多大范围的锚框。如果数据集里小目标比例很高,P2 层就要额外加密;如果大目标居多,P5 层就要多塞几组更大的锚框。

宽高比方面,一般默认包含 1:1、2:1、1:2 这组基础比例,处理横竖形目标。如果数据集中有大量极端细长物体(比如桥梁、电线杆),可以再加入 3:1、1:3 甚至更极端的比例。每层宽高比数量建议控制在 3 到 5 个,不是越多越好,多一个 ratio 就多好几万个候选框,训练匹配的计算量和显存占用都会上升。

4. 直接动手:实现一个多尺度锚框生成器

4.1 准备工作与完整代码框架

动手环节我用的环境是 PyTorch,CPU 就能跑通整个流程,不需要 GPU。核心要写两个函数:一个是锚框生成函数,输入特征图尺寸、尺寸集合和宽高比集合,输出该特征图对应位置的全部锚框;另一个是 IoU 计算函数,用于验证生成的锚框能否匹配到真实目标。

生成锚框的函数可以这样实现:

import torch def generate_anchors(feat_h, feat_w, img_h, img_w, sizes, ratios): """ 在大小为 feat_h x feat_w 的特征图上生成锚框。 坐标归一化到 [0,1],格式为 [xmin, ymin, xmax, ymax]。 sizes: 归一化边长集合,例如 [0.1, 0.2, 0.4] ratios: 宽高比集合,例如 [0.5, 1, 2] """ anchors = [] for j in range(feat_h): for i in range(feat_w): cx = (i + 0.5) / feat_w cy = (j + 0.5) / feat_h for s in sizes: for r in ratios: w = s * (r ** 0.5) h = s / (r ** 0.5) anchors.append([cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2]) return torch.tensor(anchors)

这里有个细节值得注意:代码里生成的锚框宽高是归一化值,没有直接乘以 img_w 和 img_h。这是因为中心坐标已经归一化到 [0,1],锚框的宽高同样归一化到 [0,1] 才方便与分类、回归目标统一处理。如果后续需要像素坐标,只需把四个值分别乘以输入图的宽高即可。

IoU 计算函数上面已经给出,这里把二者串起来用。假设输入图是 512×512,我们有四层特征图,尺寸分别为 64×64、32×32、16×16、8×8,每一层采用不同的尺寸集合,宽高比统一用 [0.7, 1, 1.4]。

4.2 多尺度调用与数量统计

实际调用时,各层的尺寸集合需要按前面说的“面积递增”原则来配置。例如:

sizes_p2 = [0.06, 0.09, 0.14] # 约 30~70 像素 sizes_p3 = [0.12, 0.2, 0.32] # 约 60~160 像素 sizes_p4 = [0.24, 0.4, 0.6] # 约 120~300 像素 sizes_p5 = [0.5, 0.7, 0.9] # 约 250~450 像素 anchors_p2 = generate_anchors(64, 64, 512, 512, sizes_p2, [0.7, 1, 1.4]) anchors_p3 = generate_anchors(32, 32, 512, 512, sizes_p3, [0.7, 1, 1.4]) anchors_p4 = generate_anchors(16, 16, 512, 512, sizes_p4, [0.7, 1, 1.4]) anchors_p5 = generate_anchors(8, 8, 512, 512, sizes_p5, [0.7, 1, 1.4])

每层的锚框数量分别是 64×64×9、32×32×9、16×16×9、8×8×9,也就是 36864、9216、2304、576 个,四层加起来约 49000 个候选框。这个数量是可控的,单卡训练没问题。如果每层再塞 5 个宽高比,数量会涨到八万多个,匹配和回归的计算量都会明显上升,所以宽高比的选择要克制。

有了这四层锚框,就可以定义几个模拟真实目标,验证锚框的覆盖效果。假设我们手工标注三个目标框,分别是一个小目标、一个中等目标、一个大目标,计算每个目标与四层锚框的最大 IoU,看看它们分别被哪一层“接住”。

4.3 验证覆盖效果:为什么多尺度真的有用

写一段简短的验证脚本,定义三个真实框,分别模拟小、中、大目标:

gt_boxes = torch.tensor([ [0.1, 0.15, 0.2, 0.28], # 小目标,约 50x65 像素 [0.35, 0.3, 0.55, 0.62], # 中等目标,约 100x160 像素 [0.6, 0.1, 0.95, 0.5] # 大目标,约 180x205 像素 ])

然后分别把每个真实框与四层锚框计算 IoU,记录最大 IoU。你会发现小目标几乎只能被 P2 层的锚框以较高 IoU 匹配到,P5 层的最大 IoU 可能只有 0.15;大目标则正好相反。这说明如果只保留高层特征图做预测,小目标在匹配阶段就被漏掉了;只有多尺度组合起来,每个尺度的目标才都有对应的锚框兜底。

实际训练时还会对锚框做中心位置约束:超出图像边界的锚框会被裁剪,或者直接丢弃“出界”的锚框来减少无效匹配。简单 demo 里可以忽略这个细节,但完整训练代码里建议加上边界检查,这部分在常见问题里还会说到。

5. 训练多尺度锚框时最常踩的坑

5.1 正负样本比例失衡

几万甚至几十万个候选锚框里,真正与真实目标重叠足够的正样本往往只有上百个,正负比例可能达到 1:200。如果不加控制,模型会学到“无脑输出背景”就能把 loss 压得很低,检测框一个都出不来。

解决思路一般有三条。第一条是限制训练时每张图参与 loss 的锚框数量,比如正样本保留全部,负样本随机采样到正样本的三倍,保证 batch 内正负比例在 1:3 左右。第二条是使用难例挖掘策略(OHEM),把 loss 最高的那些负样本挑出来重点学习,让模型把更多注意力放在容易误判的背景区域。第三条是用 Focal Loss 这类改进损失函数,降低大量简单负样本的权重,让模型更关注难分类的样本。我在实际项目中通常先用第一种,稳定复现后再试第三种。

5.2 IoU 阈值选多少才合适

匹配真实框时,IoU 阈值的设定直接决定正样本质量和数量。阈值设 0.5,比较宽松,能捞到不少中低质量的锚框作为正样本,训练早期收敛快,但框的定位精度上限不高;阈值设 0.7,正样本更少、质量更高,模型学到的定位能力更强,但训练可能因为正样本不足而波动。

经验做法是训练早期用 0.5 甚至更低一点,让模型先把“大概在哪”学出来;训练后期,如果想进一步提升定位精度,再把阈值提到 0.6 或 0.7 微调。有些改进方案直接在匹配时同时用多个阈值,比如 0.5 和 0.7 双分支,复杂度更高,但对于从小目标到大目标分布极不均匀的数据集效果确实更好。

5.3 锚框数量爆炸,显存和耗时双双爆表

多尺度锚框把几个特征层的候选框加起来,数量会非常可观。如果输入图很大、特征图又下采样不够,再加 5 个宽高比,显存很容易吃满。我见过有人把输入图设置成 1280×1280,还用四层特征每层 5 个 ratio,锚框数量直接逼近三十万,单卡连前向传播都不稳。

缓解方法很直接:限制每层宽高比数量、控制参与训练的最大锚框数、对大分辨率输入做轻微下采样;另外也可以在匹配时只保留与真实框中心距离在一定范围内的锚框,减少无效计算。训练时醒着的显存如果长期超过九成,优先检查锚框数量是不是失控了。

5.4 回归目标发散导致训练崩溃

锚框回归的 dw、dh 因为是 log 形式,一旦真实框与锚框差得离谱,比如锚框很小却要去拟合一个巨大目标,log 值就会变得很大,梯度也容易爆炸。很多人训练多尺度锚框模型时遇到 loss 突然变成 nan,查半天发现是某些极端匹配对产生了超大回归值。

对策有两个:一是把回归目标的 dw、dh 裁剪到合理范围,比如 [-2.5, 2.5],超出部分截断;二是让匹配器优先为每个真实目标挑选尺度更接近的锚框,而不是单纯按 IoU 排序。后者在多尺度场景里尤其重要,因为不同特征层的锚框尺度差异很大,IoU 高不代表尺度匹配,尺度差太远时即使 IoU 达标,回归难度也很大。

5.5 常见问题速查表

表现可能原因常用解决方法
训练 loss 很低但没有检出框正样本太少,模型全部预测为背景限制正负样本比例,检查 IoU 阈值是否过高
小目标漏检严重小尺度锚框数量不足或浅层特征未参与预测增加 P2/P3 层小锚框数量,检查下采样倍数
大目标框偏小或定位不稳高层特征锚框尺度不够大增大深层锚框边长,调整面积递增倍率
显存溢出锚框总量过大减少 ratios,限制每层锚框数,缩小输入分辨率
loss 出现 nan回归目标过大或学习率偏高裁剪 dw/dh,降低学习率,检查梯度

6. 最后说说我的调参习惯

多尺度锚框这套体系,我前前后后在不同项目里调了很多次,最大的体会是:不要在拿到代码后凭感觉写死一组锚框参数,而是先量化分析数据集里标注框的尺寸分布。把每个真实框的宽和高统计出来,画一张二维分布图,看看目标主要集中在哪个像素区间,然后反推每层特征图应该配置多大的锚框。这个习惯帮我少走了非常多次弯路,比套用任何经典模型的默认参数都管用。

另外,新手阶段强烈建议先在一组小图上做多尺度锚框的可视化实验,把每层锚框画出来、模拟几个真实目标看匹配结果,再动手训练完整检测模型。原因很简单:锚框匹配是目标检测一切逻辑的地基,地基没搭实,后面调什么都像在雾里找方向。等你能很自然地解释“为什么这个小目标会被 P2 层的一个 1:1.7 锚框接住”时,再进训练调参阶段,心态会完全不一样。

多尺度锚框这块内容,后续还可以往两个方向继续拓展:一是自适应锚框,让网络根据数据分布自己学习锚框尺寸,省去手动统计的麻烦;二是无锚框检测方法,舍弃预置锚框,直接从特征图每个位置中心做预测。把多尺度锚框的原理吃透,再去看这些演进方案,你会发现它们其实都是同一个问题的不同解法。

返回列表