FPN论文精读:利用内部金字塔结构

TL;DR · AI 摘要
FPN通过引入Neck结构融合多尺度特征,解决了目标检测中小物体识别难题。文章详解了Backbone-Neck-Head架构演进,并提供从零实现FPN及连接CNN与RPN的代码指南,是理解现代检测模型小目标优化机制的必读材料。
核心要点
- FPN作为Neck组件位于Backbone与Head之间,通过特征增强机制显著提升小物体检测精度。
- 深层特征图感受野大但空间信息丢失导致小目标漏检,FPN利用金字塔结构融合语义与位置信息。
- 文章提供FPN从零实现代码及与CNN/RPN对接方法,弥补了纯理论讲解缺乏工程落地细节的不足。
结构提纲
按章节快速跳转。
现代目标检测模型从Backbone-Head演变为Backbone-Neck-Head结构以提升特征质量。
Neck组件在骨干网络与检测头之间增强多尺度特征,使小物体检测成为可能。
深层网络感受野大但空间分辨率降低,导致无法精确定位小物体坐标。
FPN利用内部金字塔结构结合高层语义与低层空间细节,实现鲁棒的目标检测。
思维导图
用一张图看清主题之间的关系。
查看大纲文本(无障碍 / 无 JS 友好)
- FPN Architecture & Small Object Detection
- Model Components
- Backbone (Feature Extraction)
- Neck (Feature Enhancement/FPN)
- Head (Location & Class Prediction)
- Small Object Challenge
- Deep Layers: High Semantics, Low Resolution
- Shallow Layers: High Resolution, Low Semantics
金句 / Highlights
值得收藏与分享的关键句。
随着图像在网络中变深,空间维度收缩且像素信息混合,导致小物体坐标预测所需的细节丢失。
特征图的感受野大小与其包含的语义信息量呈正相关。
通过应用FPN,网络凭借Neck执行的特增强机制有望获得更高的准确率。
标题:FPN 论文解读:利用内部金字塔
URL 来源:https://towardsdatascience.com/fpn-paper-walkthrough-leveraging-the-internal-pyramid/
发布时间:2026-06-04T13:30:00+00:00
Markdown 内容: 我之前曾讨论过 YOLOv3 [1]。该版本之所以优于前代,其中一个关键因素在于其采用了类 FPN 的颈部(neck)结构,从而提升了小目标检测能力。遗憾的是,由于那篇文章的重点是 YOLOv3 本身,我对 FPN 的解释不够详尽。因此,本文我将专门围绕 FPN 的原始论文《_Feature Pyramid Networks for Object Detection_》[2] 展开,帮助大家更深入地理解其本质和工作原理。不仅如此,我还将演示如何从零开始实现 FPN,并将其与 CNN 主干网络(backbone)及 RPN 头部(head)进行连接。
- * *
主干网络、颈部与头部
在深入探讨 FPN 之前,我们首先需要了解目标检测模型与分类模型在结构上的差异,这种差异主要体现在最后一层。在典型的分类模型中,最后一层包含若干神经元,每个神经元对应数据集中的每一个类别。或者在二分类任务中,输出层仅由单个神经元组成,负责预测样本属于类别 0 还是类别 1。然而,这种输出层并不适用于检测任务,因为检测任务除了需要预测类别外,还需要专门的神经元来预测目标的位置和大小。
因此,为了使模型能够预测目标的位置和尺寸,我们需要将输出层(即_分类头_)替换为所谓的_检测头_。其余部分(除_头部_以外的所有层)通常被称为_主干网络_。YOLOv1 和 YOLOv2 就采用了这种结构,它们使用堆叠的卷积层作为_主干网络_,并使用特定的_头部_来预测图像中目标的位置、尺寸及其类别。
上述提到的早期目标检测模型(如 YOLOv1 和 YOLOv2)仅由_主干网络_和_头部_组成。随着研究的深入,研究人员发现这种结构仍不够理想,于是提出了一种新思路:增加一个名为_颈部_的新组件。顾名思义,它本质上位于_主干网络_和_头部_之间。而本文将要讨论的 FPN,正是最早为目标检测模型提出的_颈部_结构之一。请参见下方图 1,了解早期与现代目标检测模型的高层架构视图。

图 1. 目标检测模型的通用架构 [3]。
模型的_主干网络_主要负责特征提取,_颈部_用于增强特征质量,而_头部_则用于执行预测。基于这一概念,我们可以说,通过应用 FPN,网络能够借助_颈部_的特征增强机制,有望实现更高的精度。
多尺度检测机制的演进
前文提到,仅使用_主干网络_和检测_头部_而缺少_颈部_的结构并不够优化,尤其是在小目标检测能力方面。现在让我们看看下方的图 2。我前面提到的前两代 YOLO 版本采用的是图 (b) 所示的结构,即边界框和目标类别的预测完全依赖于主干网络最深层生成的特征图。这种方法虽然有效,但仅对大目标效果较好。原因很简单:随着图像在网络中逐层传递,空间维度不断缩小;更重要的是,深层特征图中的像素信息实际上是浅层特征图中多个相邻像素的综合表征,导致空间信息发生融合。这样一来,深层特征图获得了较大的感受野,使得大目标更容易被检测和识别。然而,随着网络加深,空间信息的退化阻碍了我们准确检测小目标,因为要精确预测目标的坐标,确实需要详细的像素位置信息。
此外,特征图的感受野大小与其包含的语义信息量呈正相关。在下图中,具有高语义信息的特征图用粗蓝色轮廓表示。这也解释了为什么图 (b) 中最深的特征图拥有最粗的轮廓。

图 2. 不同特征金字塔架构的比较 [2]。
让网络同时具备检测大小目标能力的最直接方法是使用_图像特征金字塔_(图 a)。该方法能够实现高精度,因为我们可以在不同的图像分辨率下进行预测。其核心做法是将输入图像缩放到多个尺度,在每个尺度上独立进行特征提取,并在生成的特征图上做出预测。较小的特征图负责检测大目标,而较大的特征图由于保留了详细的空间信息,专门用于检测小目标。然而,这种方法的计算成本很高,因为我们需要同时处理多个不同尺度的原始图像。
SSD(Single Shot Multibox Detector)的作者提出了另一种解决方案,即图 2 中被称为_金字塔特征层级_(c)的方法。因此,SSD 的作者不再向网络输入不同尺寸的同一图像,而是尝试仅使用最大尺寸的图像,并利用 CNN 主干网络内部的金字塔结构来进行多尺度预测。与方案 (a) 相比,这种方法在计算上更具可行性。然而,这实际上引入了类似 (b) 的权衡问题:较深层的特征图虽然包含丰富的语义信息,但空间信息极少;而较浅层的特征图虽然拥有大量空间信息,却缺乏足够的语义信息。值得注意的是,对于大目标而言,详细的空间信息可能并不那么关键,因为我们只需近似其大致形状即可。但对于小目标检测,空间信息和语义信息都不可或缺,因为模型不仅需要精确的坐标,还需要理解边界框内究竟是什么内容。因此,尽管方法 (c) 确实能够同时检测大小目标,但其对小目标的检测能力仍未达到最优。
FPN 正是为解决这一问题而生。观察图 2 中的 (d) 可以发现,预测是基于对应的、且均富含语义信息的特征图进行的。这从根本上保证了包括小目标在内的各种尺度目标都能被准确检测。我们将在下一节详细讨论 FPN 如何丰富特征图的细节。
- * *
FPN 的工作原理
FPN 的核心思想是将深层特征图的信息注入到浅层特征图中,从而使浅层特征图不仅保留高空间分辨率,还能获得来自网络深层的高语义信息。理论上,由于大尺寸特征图现在融合了丰富的语义信息,这应能提升小目标的检测精度。为实现这一目标,作者引入了所谓的_自顶向下路径_和_横向连接_。下图 3 展示了完整的 FPN 架构,它本质上是图 2 (d) 的详细版本。

图 3. 详细的 FPN 架构 [3]。
论文作者选择 ResNet-50 和 ResNet-101 作为主干网络。假设我们使用前者,根据图 4 中 ResNet 的架构细节,_conv2_、_conv3_、_conv4_ 和 _conv5_ 层将分别重复 3、4、6 和 3 次。_C2_、_C3_、_C4_ 和 _C5_ 是对应阶段最后一层输出的张量,它们将通过_横向连接_(即从主干网络引出的箭头)传递至_自顶向下路径_。

图 4. ResNet 架构 [3,4]。
_自顶向下路径_用于传递来自深层的语义信息,而_横向连接_则用于保留空间信息。我们通过逐元素相加来融合这两者,具体过程如图 5 所示。对于来自主干网络的张量(_C_),首先需要对其进行 1×1 卷积。该卷积层负责调整通道数,使其与来自自顶向下路径的张量相匹配。来自自顶向下路径的张量(_M+1_)本身则经过 2× 最近邻上采样。执行这些操作的根本原因是我们需要两个张量具有完全相同的维度,以便进行逐元素相加。相加完成后,得到的张量记为 _M_。由于之前的上采样操作,该张量会产生一些混叠效应,因此需要应用 3×3 卷积来减轻这种效应。最终,我们得到 _P_ 张量,它可以被送入检测头进行后续处理。

图 5. 横向连接 (C) 和自顶向下路径 (M+1) 的特征图融合方式 [3]。
请注意,上述图 5 中描述的所有流程仅适用于 _M2_、_M3_ 和 _M4_。_M5_ 的计算实际上要简单得多(见图 6),我们只需使用 1×1 卷积调整其通道数,使其与其他横向连接的张量保持一致即可。由于没有上采样机制,_M5_ 张量本身不需要再通过 3×3 卷积进行平滑处理。因此,我们可以认为 _P5_ 与 _M5_ 是完全相同的张量。

图 6. M5 和 P5 的计算方式与 M2-M4 及 P2-P4 略有不同 [3]。
至此,关于 FPN 背后的理论部分就介绍完了。在下一节中,我将通过使用 PyTorch 从零实现 FPN,带大家深入了解该架构的底层细节。
- * *
从零构建 FPN
CNN 主干网络
如下方代码块 1 所示,我们在代码中首先要做的就是导入所需的模块。
# Codeblock 1
import torch
import torch.nn as nn由于本文的重点是 FPN,为了简化说明,这里我将使用一个简易模型作为主干网络(backbone),而不是实际的 ResNet。不过,代码中的层命名仍然遵循图 3 和图 4:如下方代码块 2 所示,分别为 conv1、conv2、conv3、conv4 和 conv5。每个阶段的输出张量维度也按照原始 ResNet 架构进行设置。因此,尽管这个主干网络只是一个简单的基于 CNN 的模型,你可以将其视为一个标准的 ResNet。
接下来,我们在 forward() 方法中连接所有层。如果你仔细观察代码,会发现每个卷积层后面都跟着一个 ReLU 激活函数和一个最大池化层。最大池化层的步长(stride)设为 2,这有效地将特征图的空间尺寸减半。通过多次重复最大池化层,随着网络深度的增加,特征图会逐渐变小。这本质上在 CNN 主干网络中构建了一个金字塔结构,FPN 正是利用这一结构来实现对不同尺度目标的高精度检测。在 CNN 中,像这样降低空间维度是一种标准做法,旨在降低计算复杂度,以抵消通道数量增加带来的开销。
同样在 forward() 方法中,不要忘记克隆主 tensor x,如标记为 #(1)、#(2) 和 #(3) 的行所示。这些被复制的 tensor 分别命名为 c2、c3 和 c4,它们将与主流程输出的特征图 (c5) 一起作为 CNN 类的返回值 (#(4))。
# Codeblock 2
class CNN(nn.Module):
def __init__(self):
super().__init__()
self.relu = nn.ReLU()
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.conv1 = nn.Conv2d(in_channels=3, out_channels=64, kernel_size=3, padding=1)
self.conv2 = nn.Conv2d(in_channels=64, out_channels=256, kernel_size=3, padding=1)
self.conv3 = nn.Conv2d(in_channels=256, out_channels=512, kernel_size=3, padding=1)
self.conv4 = nn.Conv2d(in_channels=512, out_channels=1024, kernel_size=3, padding=1)
self.conv5 = nn.Conv2d(in_channels=1024, out_channels=2048, kernel_size=3, padding=1)
def forward(self, x):
print(f'original\t: {x.size()}\n')
x = self.relu(self.conv1(x))
print(f'after conv1\t: {x.size()}')
x = self.maxpool(x)
print(f'after maxpool\t: {x.size()}\n')
x = self.relu(self.conv2(x))
print(f'after conv2\t: {x.size()}')
x = self.maxpool(x)
print(f'after maxpool\t: {x.size()}\n')
c2 = x.clone() #(1)
x = self.relu(self.conv3(x))
print(f'after conv3\t: {x.size()}')
x = self.maxpool(x)
print(f'after maxpool\t: {x.size()}\n')
c3 = x.clone() #(2)
x = self.relu(self.conv4(x))
print(f'after conv4\t: {x.size()}')
x = self.maxpool(x)
print(f'after maxpool\t: {x.size()}\n')
c4 = x.clone() #(3)
x = self.relu(self.conv5(x))
print(f'after conv5\t: {x.size()}')
c5 = self.maxpool(x)
print(f'after maxpool\t: {c5.size()}\n')
return c2, c3, c4, c5 #(4)CNN 类构建完成后,我们现在尝试将一个尺寸为 224×224 的虚拟 RGB 图像输入网络。选择该张量维度是基于原始 ResNet 的输入形状。
# Codeblock 3
cnn = CNN()
x = torch.randn(1, 3, 224, 224)
out_cnn = cnn(x)输出结果如下所示。我们可以看到,每个卷积层后的通道数与图 4 中给出的 ResNet 结构完全一致。不仅如此,得益于最大池化层,我们的虚拟张量在每个阶段后空间尺寸都成功减半。这基本上表明,我们这个简单的 CNN 模型确实模拟了 ResNet 模型的通用结构。
# Codeblock 3 Output
original : torch.Size([1, 3, 224, 224])
after conv1 : torch.Size([1, 64, 224, 224])
after maxpool : torch.Size([1, 64, 112, 112])
after conv2 : torch.Size([1, 256, 112, 112])
after maxpool : torch.Size([1, 256, 56, 56])
after conv3 : torch.Size([1, 512, 56, 56])
after maxpool : torch.Size([1, 512, 28, 28])
after conv4 : torch.Size([1, 1024, 28, 28])
after maxpool : torch.Size([1, 1024, 14, 14])
after conv5 : torch.Size([1, 2048, 14, 14])
after maxpool : torch.Size([1, 2048, 7, 7])我们还可以通过运行下面的代码来查看返回的张量具体是什么样的。从输出结果可以看到,c2 张量的形状为 256×56×56,c3 的形状为 512×28×28,依此类推。顺便提一下,你可以忽略第 0 维的数字 1,因为它仅表示单个批次(batch)中传入的样本数量。
# Codeblock 4
c2, c3, c4, c5 = out_cnn
print(c2.shape)
print(c3.shape)
print(c4.shape)
print(c5.shape)# Codeblock 4 Output
torch.Size([1, 256, 56, 56])
torch.Size([1, 512, 28, 28])
torch.Size([1, 1024, 14, 14])
torch.Size([1, 2048, 7, 7])- * *
FPN Neck
CNN 主干网络构建完成后,现在让我们进入 FPN Neck 部分。在下方代码块 5 中,我们首先初始化上采样层(#(1)),每当需要将 _M_ 张量的空间尺寸放大一倍时,就会用到它。按照论文的建议,这里我将 mode 参数设置为 nearest,这是一种非常简单的插值方法,可以使处理过程更加快速。请参考图 7 以了解最近邻插值的效果。
# 代码块 5
class FPN(nn.Module):
def __init__(self):
super().__init__()
self.upsample = nn.Upsample(scale_factor=2, mode='nearest') #(1)
self.lateral_c5 = nn.Conv2d(in_channels=2048, out_channels=256, kernel_size=1)
self.lateral_c4 = nn.Conv2d(in_channels=1024, out_channels=256, kernel_size=1)
self.lateral_c3 = nn.Conv2d(in_channels=512, out_channels=256, kernel_size=1)
self.lateral_c2 = nn.Conv2d(in_channels=256, out_channels=256, kernel_size=1)
self.smooth_m4 = nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, padding=1)
self.smooth_m3 = nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, padding=1)
self.smooth_m2 = nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, padding=1)
def forward(self, c2, c3, c4, c5):
m5 = self.lateral_c5(c5)
p5 = m5
m4 = self.upsample(m5) + self.lateral_c4(c4)
p4 = self.smooth_m4(m4)
m3 = self.upsample(m4) + self.lateral_c3(c3)
p3 = self.smooth_m3(m3)
m2 = self.upsample(m3) + self.lateral_c2(c2)
p2 = self.smooth_m2(m2)
return p2, p3, p4, p5
图 7. 使用最近邻插值法进行 2 倍上采样的示例[3]。
回顾代码块 4,你会发现主干网络返回的 c{5,4,3,2} 张量具有不同的通道数。这正是我们初始化 lateral_c{5,4,3,2} 层来处理这些张量的原因,目的是使输出的通道数保持一致。根据论文,我们需要将这些卷积层的输出通道数设置为 256,这也是我们在 out_channels 参数中使用该数值的原因。
接下来,参考图 7,你可以想象特征图在上采样后会变得多么像素化。因此,我们需要使用 3×3 卷积层(我将其称为 smooth_m{4,3,2})对 m{4,3,2} 张量进行进一步处理。既然所有层都已初始化完毕,下一步就是按照我之前在图 3 中展示的结构,在 forward() 方法中将它们组装起来。
此外,论文还提到我们不需要在 FPN 内部实现任何非线性激活函数,这就是为什么上述 FPN 类中的所有卷积层后面都没有接 ReLU 的原因。现在,在下面的代码块 6 中,我尝试将之前获得的 _C_ 张量传入刚刚创建的 FPN 颈部网络。从输出结果可以看到,生成的张量具有不同的空间分辨率。稍后,p2 张量(尺寸为 56×56 的那个)将被送入检测头以检测小目标,而 p5(7×7 的张量)则负责检测大目标。
# 代码块 6
fpn = FPN()
out_fpn = fpn(c2, c3, c4, c5)
p2, p3, p4, p5 = out_fpn
print(p2.shape)
print(p3.shape)
print(p4.shape)
print(p5.shape)# 代码块 6 输出
torch.Size([1, 256, 56, 56])
torch.Size([1, 256, 28, 28])
torch.Size([1, 256, 14, 14])
torch.Size([1, 256, 7, 7])至此,我们已经完成了 FPN 部分的构建。请记住,FPN 只是检测模型的_颈部_(neck),这意味着此时我们尚未获得边界框预测结果。为了真正获取预测结果,我们需要将一个特定的_头部_(head)连接到 FPN,在本例中我将使用 RPN(Region Proposal Network,区域提议网络)头。
- * *
RPN 头
如果你还不熟悉 RPN,它本质上是目标检测模型中用于生成边界框的头部网络,最早在 Faster R-CNN 论文中被提出。需要注意的是,虽然在本演示中我们将 RPN 称为一个“头”,但它实际上并不是一个完整的检测头,因为它不具备对检测到的目标进行分类的能力。
从下方的 RPN 架构图中可以看到,它利用了所谓的_cls 层_和_reg 层_,分别生成_目标性_(objectness)分数和边界框坐标。目标性分数张量的长度为 2_k_,其中 _k_ 是预定义锚框的数量,2 代表对应锚框存在与否的概率。我们可以将其视为采用 one-hot 编码表示的二分类问题(目标/非目标)。同时,坐标张量长度 4_k_ 中的数字 4 对应于 _xywh_ 的预测值。

图 8. RPN 的结构[4]。
回到代码实现部分,在下面的代码块 7 中,我们在 RPN 类的 __init__() 方法里初始化了 intermediate、cls 和 reg 层。注意,只有中间层(intermediate)使用了 3×3 卷积,而 cls 和 reg 层均使用 1×1 卷积。关于通道数,intermediate 层将输入张量映射为 256 个通道,而 cls 和 reg 层则分别将其映射为 2_k_ 和 4_k_ 个通道。最后,我们只需在 forward() 方法中将这些层连接起来即可。
# 代码块 7
NUM_ANCHORS = 3
class RPN(nn.Module):
def __init__(self):
super().__init__()
self.intermediate = nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, padding=1)
self.cls = nn.Conv2d(in_channels=256, out_channels=NUM_ANCHORS*2, kernel_size=1)
self.reg = nn.Conv2d(in_channels=256, out_channels=NUM_ANCHORS*4, kernel_size=1)
def forward(self, x):
x = self.intermediate(x)
objectness_scores = self.cls(x)
bbox_regressions = self.reg(x)
return objectness_scores, bbox_regressions现在,让我们运行下面的代码块 8 来测试 RPN 类是否正常工作。这里我在代码块 6 中获得的 p2 特征图上对其进行测试。
# Codeblock 8
rpn = RPN()
p2_objectness, p2_bbox = rpn(p2)
print(p2_objectness.shape)
print(p2_bbox.shape)输出结果如下所示。可以看到,p2_objectness 是一个大小为 6×56×56 的张量,这意味着在 56×56 的空间维度中,每个像素都包含 6 个预测值:前 2 个值对应第一个锚框(anchor box),中间 2 个值对应第二个锚框,最后 2 个值对应第三个锚框。p2_bbox 张量的情况类似,在本例中它包含的是 _xywh_ 值。
# Codeblock 8 Output
torch.Size([1, 6, 56, 56])
torch.Size([1, 12, 56, 56])- * *
完整的检测模型
在下面的代码块 9 中,我们将构建完整的检测模型,以便你更好地理解 FPN 如何与其他组件协同工作。在 __init__() 方法中,我初始化了 CNN 主干网络(bottleneck)、FPN 颈部网络(neck)和 RPN 头部(head)。在 forward() 方法中,我们首先将图像张量传入 CNN(#(1))。该主干网络返回 4 个张量,这些张量可以通过横向连接(lateral connections)接入 FPN。接着,在 #(2) 处,我们将所有 _C_ 张量作为 FPN 的输入,生成 _P_ 张量。最后,我们将所有 _P_ 张量作为 RPN 的输入(#(3–4))。请记住,RPN 在所有检测头之间共享参数,因此我们只需初始化一次,即可将其用于所有不同尺度的特征图。
# Codeblock 9
class DetectionModel(nn.Module):
def __init__(self):
super().__init__()
self.cnn = CNN()
self.fpn = FPN()
self.rpn = RPN()
def forward(self, x):
c2, c3, c4, c5 = self.cnn(x) #(1)
p2, p3, p4, p5 = self.fpn(c2, c3, c4, c5) #(2)
p2_pred = self.rpn(p2) #(3)
p3_pred = self.rpn(p3)
p4_pred = self.rpn(p4)
p5_pred = self.rpn(p5) #(4)
return p2_pred, p3_pred, p4_pred, p5_pred现在检测头已经构建完成,我们可以使用下面的代码块 10 进行测试。这里我尝试传入一个大小为 1×3×224×224 的虚拟张量,模拟一张 224×224 大小的 RGB 图像(#(1))。接下来,将其传入 detection_model(#(2))并解包预测结果(#(3–4))。
# Codeblock 10
detection_model = DetectionModel()
x = torch.randn(1, 3, 224, 224) #(1)
p2_pred, p3_pred, p4_pred, p5_pred = detection_model(x) #(2)
p2_objectness, p2_bbox = p2_pred #(3)
p3_objectness, p3_bbox = p3_pred
p4_objectness, p4_bbox = p4_pred
p5_objectness, p5_bbox = p5_pred #(4)
print(p2_objectness.shape)
print(p3_objectness.shape)
print(p4_objectness.shape)
print(p5_objectness.shape)
print()
print(p2_bbox.shape)
print(p3_bbox.shape)
print(p4_bbox.shape)
print(p5_bbox.shape)输出结果如下所示。可以看到,生成的张量维度符合预期,每个网格单元对应的 objectness 和 bbox 张量分别包含 6 个和 12 个值。因此,我认为这个实现是正确的,可以用于训练目标检测任务。
# Codeblock 10 Output
torch.Size([1, 6, 56, 56])
torch.Size([1, 6, 28, 28])
torch.Size([1, 6, 14, 14])
torch.Size([1, 6, 7, 7])
torch.Size([1, 12, 56, 56])
torch.Size([1, 12, 28, 28])
torch.Size([1, 12, 14, 14])
torch.Size([1, 12, 7, 7])- * *
结语
关于 FPN 的底层理论和从零开始的实现,主要内容就是这些。在这里,我建议你尝试在真实的 ResNet 上实现 FPN,而不是像我上面演示的那样使用虚拟的 CNN 模型。我之前写过一篇关于 ResNet 的文章,你可以参考文献 [5]。当然,你也可以根据需要使用其他模型,如 VGG、ResNeXt、ConvNeXt 等,因为 FPN 基本上可以应用于任何基于 CNN 的主干网络。不仅如此,如果你能用 YOLO 风格的检测头来替代 RPN 就更好了,相关示例可以在我之前的文章中找到:YOLOv1 见参考文献 [6],YOLOv2 见参考文献 [7],YOLOv3 见参考文献 [1]。
如果文中或代码有任何错误,请告诉我。感谢阅读!另外,你可以在我的 GitHub 仓库 [8] 中找到本文使用的代码。
- * *
参考文献
[1] Muhammad Ardi. YOLOv3 Paper Walkthrough: Even Better, but Not That Much. Medium. https://ai.gopubby.com/yolov3-paper-walkthrough-even-better-but-not-that-much-4dc6c0c1b42c [Accessed June 1, 2026].
[2] Tsung-Yi Lin. Feature Pyramid Networks for Object Detection. Arxiv. https://arxiv.org/abs/1612.03144 [Accessed September 9, 2025].
[3] Image created originally by author.
[4] Shaoqing Ren. Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks. Arxiv. https://arxiv.org/abs/1506.01497 [Accessed September 9, 2025].
[5] Muhammad Ardi. Paper Walkthrough: Residual Network (ResNet). Python in Plain English. https://medium.com/python-in-plain-english/paper-walkthrough-residual-network-resnet-62af58d1c521 [Accessed September 9, 2025].
[6] Muhammad Ardi. YOLOv1 Paper Walkthrough: The Day YOLO First Saw the World. Medium. https://medium.com/ai-advances/yolov1-paper-walkthrough-the-day-yolo-first-saw-the-world-ccff8b60d84b [Accessed June 1, 2026].
[7] Muhammad Ardi。YOLOv2 与 YOLO9000 论文解读:更好、更快、更强。Medium。https://ai.gopubby.com/yolov2-yolo9000-paper-walkthrough-better-faster-stronger-c9906e0438a3 [访问日期:2026年6月1日]。
[8] MuhammadArdiPutra。FPN。GitHub。https://github.com/MuhammadArdiPutra/medium_articles/blob/main/FPN.ipynb [访问日期:2025年9月9日]。