FPN这篇论文Feature Pyramid Networks for Object Detection是我反复读了很多遍的目标检测经典。前阵子做工业质检项目场景里既有几千像素的大零件又有几十像素的划痕缺陷用常规检测框架折腾了快两周小目标AP始终上不去。后来把骨干特征提取改成FPN结构重跑小目标AP直接抬了几个点很多之前完全漏掉的缺陷开始能被检出来。这篇笔记把我从这篇论文里沉淀下来的东西完整整理一遍它到底解决了什么问题、每个模块为什么这么设计、消融实验证明了什么以及工程落地时容易踩的坑。1. 多尺度检测的困局FPN到底在解决什么问题1.1 尺度差异为什么是检测框架的“心腹之患”做目标检测的人应该都有体会一张工业质检图里大零件能占半个画面旁边一道划痕可能只有三四十个像素宽。常规的CNN分类网络为了提取高层语义会反复下采样原图里30像素的目标经过4次2倍降采样之后只剩下不到2个像素的信息已经很难在特征图上留下可靠的响应。早期Faster R-CNN这类框架就直接用骨干网络最后一层特征做区域建议和分类。最后一层语义信息最丰富但分辨率最低小目标的细节基本被“压扁”了。COCO这种数据里小目标占比又很高所以单尺度高层特征的框架在小目标上普遍拉胯这也是当时社区里一个公认痛点。另一个让人头疼的点是不同尺度的目标适合它们的特征层级其实完全不同。小目标需要高分辨率、细节丰富的浅层特征大目标需要语义充分、感受野大的深层特征。过去大多数检测器把所有目标都粗暴地丢到同一个特征金字塔层级上处理等于让一家公司同时服务大客户和小客户却只开了一个业务窗口。1.2 四种多尺度方案的真实对比在FPN之前业界已经有几种多尺度处理的思路但它们各有各的问题。我把常见的四种方案放在一起对比能更清楚看出FPN的定位方案核心做法主要代价实际效果图像金字塔把输入图缩放到多个尺寸每个尺寸分别过一遍CNN计算量成倍增加训练和推理都撑不住效果上限高但工程上几乎无法落地CNN金字塔特征逐层预测用骨干网络不同stage的输出分别做预测比如SSD浅层语义不足深层分辨率不够层级间特征质量不齐能改善多尺度但各层预测能力差异很大只用单一高层特征只在骨干网络最后一层做分类和回归比如早期Faster R-CNN小目标信息已经被严重稀释大目标还行小目标AP很难看FPN自顶向下融合把深层语义逐级“往回传”与浅层细节逐层融合增加一部分计算量但远小于图像金字塔小目标AP提升明显各尺度均衡FPN真正聪明的地方在于它没有重新设计一个特征提取网络而是把骨干网络原本就天然存在的多尺度金字塔结构利用起来再通过一条自顶向下的路径把高层语义广播下去让金字塔每一层都同时拥有“高分辨率”和“强语义”。这是一种结构上的重新组织而不是暴力堆计算量。这个思路听起来简单但落地细节不少。论文里每一个改动背后都有对应实验支撑这也是为什么它比一般的“炼丹技巧”更值得反复读。2. FPN结构逐层拆解自顶向下路径和横向连接的设计逻辑2.1 自底向上路径为什么只挑C2到C5FPN的骨架依然是一个普通的CNN前向传播过程中特征图尺寸每缩小一半就算是金字塔的一级。论文拿ResNet这类残差网络举例把每个stage输出的最后一组特征分别叫作C2、C3、C4、C5对应原图的4倍、8倍、16倍、32倍下采样。这里有个值得琢磨的细节为什么不从C1就开始做金字塔C1的分辨率最高包含的细节最原始但它的分辨率太高、通道数多直接把C1拉进来会吃掉大量显存和计算量而且C1距离语义信息太远即使融合了高层语义提升也有限。工程上更合理的起点是C2。我自己做项目时也验证过把C2换成C1参与金字塔显存占用明显上升AP收益却很微小属于典型的“性价比为负”的改动。另一个问题是为什么金字塔只到C5为止因为再往下走特征图已经缩小到原图的32倍更大尺度的目标并不是常见场景继续下采样不仅丢细节还浪费算力。FPN论文里会额外构造一个P6主要用于区域建议网络去覆盖更大的目标但参与分类回归的金字塔层级实际还是P2到P5为主。2.2 横向连接1x1卷积做的事情比“对齐维度”多横向连接是FPN容易被忽视但极其关键的一环。每个自底向上的特征层C2到C5通道数各不相同。拿ResNet-50举例C2到C5的输出通道数分别是256、512、1024、2048。如果不做任何处理就把这些特征和上采样后的高层特征直接相加维度对不上统计分布差异也会很大融合结果基本不可用。论文的做法是在每一层先接一个1x1卷积把通道统一到一个固定值论文里用的是256。1x1卷积在这里不只是对齐维度它也相当于对每一层特征做一次线性投影让不同层级的特征有相近的分布之后相加才不会被某一层的高响应值带偏。为什么不用3x3卷积来做横向连接因为3x3卷积的感受野大特征已经被高层语义“污染”了一部分再去做横向融合反而会混入本层不需要的信息。1x1卷积只做通道级重投影空间信息保持得最干净。这个细节是我一开始读的时候忽略掉的后来自己做对比实验把1x1换成3x3AP反而小幅下降也印证了论文里这个选择的合理性。2.3 自顶向下路径最近邻上采样、逐元素相加与3x3平滑自顶向下路径的每一级操作是固定的三步先对上一层特征做2倍最近邻上采样再和当前层的横向连接输出做逐元素相加最后过一层3x3卷积生成最终的特征图P2到P5。上采样为什么用最近邻而不是转置卷积这一点我最早也有疑惑。转置卷积能学参数理论上有更强的表达能力。但在FPN这个场景里我们需要的只是把特征图尺寸翻倍以便和下一层对齐并不需要引入新的可学习映射。转置卷积在很多任务里容易带来棋盘伪影训练也不够稳定参数还白占显存。论文的实验也表明在特征金字塔的融合场景里最近邻上采样已经足够。工程上它不是“最牛的选择”而是“不会被干扰的选择”。逐元素相加而不是通道拼接也是一个值得品味的决定。拼接会保留两组特征的独立信息理论上信息量更大但代价是通道数翻倍、后续计算量上升。相加相当于默认两组特征在相同空间位置上共享语义这会让融合后的特征更紧凑。论文用实验说明在这个框架里相加带来的简洁性大于拼接带来的额外信息。最后的3x3卷积常被忽略但它负责一个很实际的问题上采样会带来混叠效应特征图在空间上出现不自然的重复块3x3卷积能起到平滑作用同时把融合后的特征做一次局部重编码。论文里管这一步叫“减少上采样的混叠效应”它不是可有可无的装饰。2.4 一个最小的FPN模块长什么样把上面这些设计落成代码其实非常短。下面是一个我经常在原型里用的最小PyTorch实现思路和论文保持一致import torch import torch.nn as nn import torch.nn.functional as F class FPN(nn.Module): def __init__(self, in_channels(256, 512, 1024, 2048), out_channels256): super().__init__() # 横向连接统一通道数 self.lateral_convs nn.ModuleList([ nn.Conv2d(ic, out_channels, 1) for ic in in_channels ]) # 3x3平滑卷积减少上采样混叠 self.smooth_convs nn.ModuleList([ nn.Conv2d(out_channels, out_channels, 3, padding1) for _ in in_channels ]) def forward(self, c2, c3, c4, c5): # 自顶向下路径 m5 self.lateral_convs[3](c5) m4 self.lateral_convs[2](c4) F.interpolate(m5, scale_factor2, modenearest) m3 self.lateral_convs[1](c3) F.interpolate(m4, scale_factor2, modenearest) m2 self.lateral_convs[0](c2) F.interpolate(m3, scale_factor2, modenearest) # 平滑后输出 p5 self.smooth_convs[3](m5) p4 self.smooth_convs[2](m4) p3 self.smooth_convs[1](m3) p2 self.smooth_convs[0](m2) return p2, p3, p4, p5这段代码去掉注释大概二十多行却把一个多尺度检测框架的核心结构完整表达出来了。这也是我推荐所有做检测的人都手动推一遍的原因结构上的理解一旦到位换成其他backbone或检测头都很快。3. 把FPN接到Faster R-CNN上RPN和ROI分支的适配细节3.1 RPN改造每层只有一个anchor尺度的滑窗原来的RPN在单层特征图上用3x3滑窗每种位置同时预测多个尺度和多个长宽比的anchor。FPN接进来之后RPN的改造比想象中更直接每个金字塔层级P2到P5上仍然跑3x3滑窗但每一层只负责一种anchor尺度。具体来说P2到P5对应的anchor边长基准分别是32、64、128、256像素再配合三种长宽比每层就是3个anchor。因为金字塔本身已经覆盖了尺度维度所以单层不再需要多个尺度所有层级合起来的总覆盖范围反而比原来更大。这里有一个容易踩坑的地方RPN的候选框来自多个不同分辨率的层级最后要合并在一起做跨层非极大值抑制。不同层级的输出在空间位置和尺度上天然错开如果只在单层内做NMS两个重叠度很高的proposal可能会被重复保留。工程上一般把各层RPN输出合并后再统一做NMS阈值可以按实际数据微调对最终检测精度影响很大。3.2 ROI按尺寸分配金字塔层公式背后的直觉区域建议网络产出的proposal尺度不一Fast R-CNN分支需要从不同金字塔层级上取特征来做ROI Pooling。FPN论文给了一个非常优雅的分配规则用ROI的宽高信息决定它应该从哪一层取特征k floor(k0 log2(√(wh) / 224))这里的wh是ROI的真实宽高√(wh)相当于一个几何平均边长k0默认取4224是ImageNet预训练时常用的输入尺寸。ROI平均边长接近224像素时k4对应P4边长缩小一半k就减1对应到更低层的P3边长翻倍k加1对应更高层的P5。算出来的k会被限制在2到5之间。这个公式背后的直觉是大目标在深层特征上语义充分不需要太高分辨率小目标在浅层特征上才能保留足够的细节。用一句话概括就是“人尽其才尺尽其用”。我把这个逻辑翻译成一个可以直接用的函数def assign_fpn_level(roi_w, roi_h, k04, canonical_size224): # roi_w, roi_h: 目标在原始图像上的宽高 roi_size torch.sqrt(roi_w * roi_h) k torch.floor(k0 torch.log2(roi_size / canonical_size)) return k.clamp(min2, max5)一个很容易忽略的点是ROI在某个层上被采样之后这一层的特征已经经过FPN融合包含了更丰富的信息。所以ROI分配更多的是决定“从哪一层下手”而不是决定“只能看到哪一层的信息”。这也是FPN和普通多尺度特征最本质的区别。3.3 跨层共享参数的坑与收益FPN里不同金字塔层级的预测头是共享参数的。也就是说P2上的分类头和P5上的分类头用的是同一套卷积和全连接权重。这样设计最直接的收益是参数量不会随金字塔层数线性膨胀。共享参数在理论上也站得住脚FPN已经把各层特征做了一定程度的对齐和融合不同层级的输出具备相近的语义表达能力这时候共享预测头不会明显拖累精度。论文里也通过实验对比过共享与不共享结论是共享参数几乎不损失性能收益却是参数和显存的大幅下降。我自己在复现时发现共享参数真正需要注意的是初始化尺度。预测头如果初始化过大低层特征上的梯度容易把参数带偏。实操时给新加的预测头设置一个较小的标准差比如0.01训练会稳定很多。4. 消融实验教我的事哪些设计改动是真金白银4.1 横向连接与自顶向下路径各自的贡献FPN论文里最值得反复读的是消融实验。它把结构拆开来验证每一部分的必要性结果很有启示意义。只去掉横向连接保留自顶向下路径时高层特征被直接上采样并逐级相加但每一层原有的自身特征没有参与融合。这时候深层语义虽然被传播下去了但和浅层细节没有进行校准特征在每个层级上都有一种说不上来的“错位感”AP下降幅度非常明显。这说明横向连接不是单纯为了对齐通道而是让高层语义和浅层细节在空间上产生真正的交互。只去掉自顶向下路径保留横向连接时就退化成了一种多尺度独立预测的结构每个层级直接用自己原有的特征做预测。这比单层特征好一些但问题的根源还在浅层特征仍然缺乏全局语义深层特征仍然分辨率不足。两条路径同时存在时AP是最高的。对做检测的人来说这个实验的教训很直接不要在结构上做减法减到“看起来差不多”差的那一点往往就是小目标和高层语义之间的鸿沟。4.2 去掉P2带来的AP_s曲线变化P2是金字塔里分辨率最高的一层对应原图4倍下采样专门负责小目标。论文的消融里有一项是看顶层的覆盖范围从P2到P5改成从P3到P5会怎样相当于砍掉P2。砍掉P2之后整体AP肯定下降其中小目标AP的下降尤其显著。原因很好理解P2的分辨率最高那些在P3上只有几个像素响应的目标在P2上还能保有相对完整的空间信息。P2也被认为是为检测器开启“高分辨率窗口”的关键层级。这对工程选型很有参考价值。我的实际经验是如果显存有限或者目标平台速度要求很苛刻可以先砍P2换速度。但在目标普遍偏小的业务里砍P2等于自断一臂。如果既想要P2又不想增加过多计算可以考虑把输入分辨率适当提高让目标在产生金字塔特征之前就已经有足够像素保底。4.3 换backbone和任务的泛化表现FPN论文不仅把结构用在Faster R-CNN的检测分支上还在语义分割和实例分割任务里做了验证。它是少数在检测、分割两开花的结构设计。后续很多工作都以它为底座比如经典的实例分割框架里的特征提取部分基本就是FPN的直系后代。换backbone时FPN的表现也相当稳。用更深的残差网络时FPN带来的提升依然在这是因为结构本身与backbone深度解耦它只依赖“金字塔各级特征都存在”这个前提。我在不同业务里试过多种轻量backbone只要骨干网络还保留了多级下采样的结构FPN都能稳定带来提升区别只是幅度大小。这个泛化能力很关键。它说明FPN抓住了多尺度问题里一个更普遍的结构性规律而不是针对某个具体网络修的补丁。做工程选型时FPN基本可以当作一个“默认存在”的模块除非场景本身有强烈的单尺度性质否则加了通常都是赚的。5. 工程落地的几个硬坑上采样、BN、小目标排查5.1 上采样方式工程选择不是越高级越好很多初学者会在FPN里用带学习的上采样比如转置卷积或者像素重排。我从实际经验出发的建议是不要跟风换。最近邻上采样在FPN里已经够用而且没有任何参数、不会引入棋盘伪影、训练时梯度传播路径更短更稳。我做过一组对比把最近邻换成双线性插值效果几乎没差别但速度略慢换成转置卷积训练波动明显变大最终AP也没有变好。FPN里每个阶段都要做一次上采样任何一个环节的不稳定都会在融合中放大。跑这类实验时建议固定随机种子多做几次对比再下结论否则很容易被单次run的噪声误导。5.2 batch size很小的时候BN千万别随便动FPN一般接在pretrained backbone后面微调这个阶段最容易被忽视的问题是BatchNorm的状态。如果batch size只有2或者4在新加的FPN分支里开BN反向传播更新统计量的震荡会非常剧烈因为每一层金字塔拿到的有效样本数量本来就少再除以小batch很容易把训练搞出不稳定的损失曲线。我的做法是在复现阶段把backbone里的BN统计量冻结只让它在前向传播中做推理FPN新分支如果加了BN也建议先用同步BN或者干脆用GroupNorm替代。GroupNorm在小batch下表现得比BN稳很多这是我调FPN时学到的最实用的一招。冻结BN还有一个好处是训练能更快进入稳定状态因为一开始梯度信号不会被BN统计量的波动淹没。等loss明显下降之后再决定要不要解冻BN做精细微调这个顺序比较安全。5.3 小目标漏检先从这三个环节排查如果落地一个FPN检测器后小目标依然漏检我的排查顺序是固定的。第一步看输入分辨率。如果原始图像里小目标只有几十像素预处理阶段又把整图缩到五六百像素那FPN结构再强也救不回来。输入分辨率是最容易被忽视的上限。第二步看ROI分配公式。如果业务里小目标特别密集可以把公式里的k0微调或者把ROI分配范围下限改成2让更多中小目标流向P2和P3。实际上就是给公式加一点“偏小目标”的倾向。第三步看anchor设置。P2层的anchor边长默认是32如果业务里的目标全是十几二十像素的车牌、缺陷、码点这个默认值完全不够用。可以往下降或者引入更小的自定义anchor集合FPN把尺度维度拆到层上之后anchor设计变得非常直观。这三个环节检查完大部分小目标漏检问题都能找到明确方向。我自己碰到的案例里有七成是卡在输入分辨率上剩下三成才是结构或参数问题。5.4 从零训练比微调更容易踩的梯度雷FPN论文场景下大家都默认用pretrained backbone微调但实际有一些业务数据分布偏离ImageNet很远可能要从头训练。这种情况下的FPN有自己特有的麻烦。从零训练时骨干网络的浅层和深层在同一时期都处于剧烈变化状态FPN融合机制会把这种变化快速传播到所有层级。训练初期经常出现loss飙升或震荡。我的对策是加长warmup把头几千个iterations的学习率压到很低同时把新加的横向连接和预测头的初始化scale调小给结构一个先稳定、再学习的缓冲期。另一个容易被忽略的点是FPN分支的学习率。微调阶段通常整个网络用一个学习率就能稳定但从零训练时FPN新分支产生的梯度往往比骨干网络更深处的梯度更剧烈可以给它单独设一个较小倍率。这个细节在主流开源框架里没有统一处理需要自己根据loss曲线来调。6. 从FPN到后续架构它如何改变了检测器的设计习惯6.1 PANet、BiFPN到底改了哪根弦FPN之后特征金字塔这块很快成了一片试验田。理解这些后续工作反过来能看出FPN的设计边界在哪里。PANet做的改造是在自顶向下路径之后又加了一条自底向上的增强路径。FPN把高层语义传给了浅层但浅层的精确空间信息并没有很好地回流到高层。PANet相当于补了一条“从底层向高层汇报”的通路让大目标也能拿到更精确的位置细节。BiFPN则更进一步它删掉了部分冗余连接只保留关键跨尺度连接并且给每条连接分配一个可学习的权重让网络自己决定哪些尺度信息在融合时更重要。这让融合过程从“所有人平等发言”变成了“重要角色多发言”。我用一个生活化的类比FPN像组织内部单向传达上级精神PANet像上下级双向沟通BiFPN则是开会时每个人带着自己的权重话语权由网络学出来。这些演进本质上都在围绕同一件事做文章跨尺度信息怎么融合才是最有效率的。FPN提供了第一版可靠答案后续工作是把它向更多方向和更深层次推。6.2 什么时候其实可以不用FPN作为一个FPN的忠实用户我也得诚实说一句不是所有场景都非它不可。如果任务里目标的尺度分布非常集中比如固定距离的文档OCR、车牌识别、流水线上的标准件检测一张图里所有目标都在很窄的尺寸范围内这时候单层或两三层特征通常够用FPN带来的收益可能只有零点几个点却白白增加了几十毫秒的推理耗时和显存占用。另外在移动端或者边缘设备上算力本身就紧张FPN的融合计算带来的额外开销需要仔细权衡。这种情况下可以考虑简化版金字塔比如只从C3、C4、C5做三层或者去掉P2用速度换回可接受的精度。要不要上FPN本质上是先看数据再看算力。数据没有明显多尺度问题就物理上不需要多尺度结构数据有极端尺度跨度再贵也得上。这个判断顺序是我做过多个落地项目之后最深刻的体会。我自己的第一习惯是把FPN当作一个默认思考的起点但绝不会把默认当作真理。每个检测任务的合理结构最终都是通过数据和实验说出来的论文给了我们可靠的地基而我们真正要做的是在上面盖出适合自己的房子。