深入浅出之FPN (Feature Pyramid Networks for Object Detection)
1. 动机 (Motivation)
在计算机视觉任务中,尤其是目标检测和实例分割,识别不同尺度的目标是一个核心挑战。传统的卷积神经网络(CNN)通常在最后几层产生具有强语义信息的特征图,但这些特征图的空间分辨率较低,不利于检测小目标。而较浅层的特征图虽然分辨率高,细节丰富,但语义信息较弱,不利于分类。
为了解决这个问题,有几种常见方法:
- 图像金字塔 (Image Pyramid): 将输入图像缩放到不同尺寸,分别送入网络。这种方法效果好,但计算量和内存开销巨大。
- 单层特征图 (Single Feature Map): 只使用 CNN 某一层的特征图进行预测(如 Faster R-CNN 的早期版本使用 VGG 的
conv5
)。这对于尺度变化大的目标效果不佳。 - CNN 内建金字塔 (Pyramidal Feature Hierarchy): 利用 CNN 不同层天然产生的多尺度特征图(如 SSD)。但浅层特征语义不足的问题仍然存在。
FPN 的提出旨在构建一个在网络内部就能生成具有强语义信息的多尺度特征金字塔,且计算开销相对较小。它的核心思想是结合低分辨率、强语义的特征和高分辨率、弱语义的特征,使得金字塔的每一层都具有丰富的语义信息。
2. FPN 结构 (Architecture)
FPN 主要由三个部分组成:
- 自底向上路径 (Bottom-up Pathway)
- 自顶向下路径 (Top-down Pathway)
- 横向连接 (Lateral Connections)
我们以 ResNet 作为典型的骨干网络(Backbone)为例来解释:
(a) 自底向上路径 (Bottom-up Pathway)
- 这就是标准的前馈 CNN 计算过程。输入图像经过骨干网络(如 ResNet)的不同阶段(stage),产生一系列特征图。
- 通常,每个阶段的最后一个卷积层的输出被视为一个特征层级。例如,在 ResNet 中,我们通常关注
conv2
,conv3
,conv4
,conv5
的输出(记为 C2, C3, C4, C5)。 - 随着层级加深(从 C2 到 C5),特征图的空间分辨率逐渐降低(通常是 1/4, 1/8, 1/16, 1/32 的输入图像尺寸),而语义信息越来越强。
(b) 自顶向下路径 (Top-down Pathway)
- 这个路径的目标是将高层(语义强但分辨率低)的特征信息传递给低层。
- 它从骨干网络最高层的输出(如 C5)开始。
- 首先,对 C5 应用一个 1×1 卷积(主要是为了减少通道数,例如统一减少到 256 维),得到 M5。
- 然后,对 M5 进行上采样(通常是 2 倍,使用最近邻插值或双线性插值),使其空间分辨率与下一层(C4)匹配。
- 这个过程逐层向下进行:将上采样后的高层特征与经过横向连接处理的对应底层特征合并,然后再次上采样,传递给更低层。
© 横向连接 (Lateral Connections)
- 横向连接的作用是融合自顶向下路径传递过来的特征和自底向上路径的原始特征。
- 对于自底向上路径的每一层(如 C4),先用一个 1×1 卷积进行处理(同样是为了匹配通道数,例如统一到 256 维,并增强特征表示)。
- 然后,将这个处理后的特征图(来自 C4)与从上一层(M5)上采样过来的特征图进行逐元素相加 (element-wise addition)。
- 这种加法操作使得来自高层的强语义信息能够与来自底层的精确定位信息相结合。
(d) 生成最终特征图 (P-layers)
- 合并后的特征图(例如 C4 和上采样的 M5 相加的结果)会再经过一个 3×3 卷积层进行处理。
- 这个 3×3 卷积的作用是为了消除上采样可能带来的混叠效应(aliasing effect),并生成最终的、更平滑、更鲁棒的特征图。
- 经过 3×3 卷积后得到的特征图就是 FPN 输出的特征金字塔层级,通常表示为 P2, P3, P4, P5(对应于 C2, C3, C4, C5)。它们的空间分辨率分别是输入图像的 1/4,1/8,1/16,1/32。
- 有时,还会通过对 P5 进行最大池化(或步长为 2 的卷积)来生成 P6,用于检测非常大的目标。
总结流程:
- Bottom-up: 计算 C2, C3, C4, C5。
- Top-down & Lateral:
- M5 = 1×1 Conv(C5)
- P5 = 3×3 Conv(M5)
- M4 = (1×1 Conv(C4)) + Upsample(M5)
- P4 = 3×3 Conv(M4)
- M3 = (1×1 Conv(C3)) + Upsample(M4)
- P3 = 3×3 Conv(M3)
- M2 = (1×1 Conv(C2)) + Upsample(M3)
- P2 = 3×3 Conv(M2)
- (Optional) P6: P6 = MaxPool(P5) or Conv(P5)
最终得到的 P2, P3, P4, P5 (以及可能的 P6) 构成了特征金字塔,每一层都融合了高分辨率和强语义信息,可以直接用于后续的任务(如目标检测的 RPN 和 RoI head)。
3. 优点 (Advantages)
- 多尺度检测/分割能力强: FPN 能有效处理不同尺度的目标。
- 端到端训练: FPN 可以作为标准 CNN 的一部分,进行端到端的训练。
- 计算高效: 相比图像金字塔,FPN 利用了 CNN 计算过程中产生的特征,增加了较少的额外计算量。
4. 应用 (Applications)
FPN 已经成为许多先进的目标检测(如 Faster R-CNN 的后续改进、RetinaNet、Mask R-CNN)和实例分割框架的标准组件。
5. 代码示例 (PyTorch)
下面是一个简化的 FPN 模块的 PyTorch 实现,假设我们已经有了骨干网络提取的 C2, C3, C4, C5 特征图:
import torch
import torch.nn as nn
import torch.nn.functional as Fclass FPN(nn.Module):"""一个简化的 FPN 实现。假设输入是来自骨干网络不同阶段的特征图列表。"""def __init__(self, in_channels_list, out_channels):"""Args:in_channels_list (list[int]): 输入特征图的通道数列表,例如 [256, 512, 1024, 2048] (对应 ResNet C2, C3, C4, C5)out_channels (int): FPN 输出特征图的统一通道数,例如 256。"""super(FPN, self).__init__()self.lateral_convs = nn.ModuleList() # 横向连接的 1x1 卷积self.output_convs = nn.ModuleList() # 输出的 3x3 卷积# 创建横向连接层 (1x1 conv)# 从 C2 到 C5for in_channels in in_channels_list:# 这里假设输入通道数不一定等于 out_channelsself.lateral_convs.append(nn.Conv2d(in_channels, out_channels, kernel_size=1))# 创建输出卷积层 (3x3 conv)# FPN 会为每个输入的 C 层级生成一个 P 层级for _ in range(len(in_channels_list)):self.output_convs.append(nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1))# 初始化权重 (可选但推荐)for m in self.modules():if isinstance(m, nn.Conv2d):nn.init.kaiming_uniform_(m.weight, a=1)if m.bias is not None:nn.init.constant_(m.bias, 0)def forward(self, features):"""Args:features (list[Tensor]): 来自骨干网络的特征图列表,例如 [c2, c3, c4, c5]顺序是从浅到深 (分辨率从高到低)。Returns:list[Tensor]: FPN 输出的特征金字塔列表,例如 [p2, p3, p4, p5]顺序也是从浅到深。"""# features = [c2, c3, c4, c5]c2, c3, c4, c5 = features# 1. 自顶向下路径与横向连接# 处理最高层 C5p5_in = self.lateral_convs[3](c5) # 对应 C5 的 1x1 卷积p5 = self.output_convs[3](p5_in) # 对应 P5 的 3x3 卷积# 处理 C4p4_in_lat = self.lateral_convs[2](c4) # 对应 C4 的 1x1 卷积p5_upsampled = F.interpolate(p5_in, size=p4_in_lat.shape[2:], mode='nearest') # 上采样 p5_in (注意是 1x1 卷积后的结果)p4_in = p4_in_lat + p5_upsampled # 逐元素相加p4 = self.output_convs[2](p4_in) # 对应 P4 的 3x3 卷积# 处理 C3p3_in_lat = self.lateral_convs[1](c3) # 对应 C3 的 1x1 卷积p4_upsampled = F.interpolate(p4_in, size=p3_in_lat.shape[2:], mode='nearest') # 上采样 p4_inp3_in = p3_in_lat + p4_upsampled # 逐元素相加p3 = self.output_convs[1](p3_in) # 对应 P3 的 3x3 卷积# 处理 C2p2_in_lat = self.lateral_convs[0](c2) # 对应 C2 的 1x1 卷积p3_upsampled = F.interpolate(p3_in, size=p2_in_lat.shape[2:], mode='nearest') # 上采样 p3_inp2_in = p2_in_lat + p3_upsampled # 逐元素相加p2 = self.output_convs[0](p2_in) # 对应 P2 的 3x3 卷积# 返回 FPN 特征金字塔 P2, P3, P4, P5# 通常按 P2, P3, P4, P5 (从浅到深) 的顺序返回return [p2, p3, p4, p5]# --- 使用示例 ---
if __name__ == '__main__':# 假设的骨干网络输出通道数 (类似 ResNet-50)# C2: 256 channels, C3: 512, C4: 1024, C5: 2048in_channels_list = [256, 512, 1024, 2048]fpn_out_channels = 256 # FPN 输出通道数# 创建 FPN 模型fpn_model = FPN(in_channels_list, fpn_out_channels)# 创建假的输入特征图 (Batch size = 1)# 尺寸需要符合 CNN 下采样规则, 例如 H/4, H/8, H/16, H/32H, W = 256, 256 # 假设输入图像是 256x256c2 = torch.randn(1, in_channels_list[0], H // 4, W // 4) # 64x64c3 = torch.randn(1, in_channels_list[1], H // 8, W // 8) # 32x32c4 = torch.randn(1, in_channels_list[2], H // 16, W // 16) # 16x16c5 = torch.randn(1, in_channels_list[3], H // 32, W // 32) # 8x8input_features = [c2, c3, c4, c5]# 前向传播output_pyramid = fpn_model(input_features) # [p2, p3, p4, p5]# 打印输出特征图的尺寸和通道数print("FPN Output Shapes:")for i, p in enumerate(output_pyramid):print(f"P{i+2}: {p.shape}") # P2, P3, P4, P5# 预期输出 (通道数都是 fpn_out_channels=256):# P2: torch.Size([1, 256, 64, 64])# P3: torch.Size([1, 256, 32, 32])# P4: torch.Size([1, 256, 16, 16])# P5: torch.Size([1, 256, 8, 8])
代码讲解:
__init__
:- 接收骨干网络各层输出的通道数列表
in_channels_list
和 FPN 期望输出的统一通道数out_channels
。 - 创建了两个
nn.ModuleList
:lateral_convs
用于存放所有横向连接的 1×1 卷积层,output_convs
用于存放所有最终的 3×3 卷积层。 - 循环创建这些卷积层,确保每个输入的 C 层级都有对应的 1×1 和 3×3 卷积。
- 进行了简单的权重初始化(Kaiming 初始化)。
- 接收骨干网络各层输出的通道数列表
forward
:- 接收一个包含 C2, C3, C4, C5 特征图的列表
features
。 - 自顶向下计算:
- 从最高层 C5 开始,先通过对应的
lateral_convs
(1×1 卷积) 处理 C5,得到p5_in
。注意,原始 FPN 论文中 M5 就是 1×1 卷积的结果,这里直接用p5_in
表示中间结果。 - 然后
p5_in
经过output_convs
(3×3 卷积) 得到最终的 P5。 - 对于下一层 C4,先用其对应的
lateral_convs
处理得到p4_in_lat
。 - 将上一层的中间结果 (
p5_in
) 通过F.interpolate
进行上采样,使其尺寸与p4_in_lat
匹配。 - 将上采样结果
p5_upsampled
与p4_in_lat
逐元素相加,得到p4_in
。 p4_in
经过对应的output_convs
(3×3 卷积) 得到最终的 P4。- 这个过程(
lateral_convs
-> 上采样 -> 相加 ->output_convs
)依次对 C3 和 C2 重复,得到 P3 和 P2。
- 从最高层 C5 开始,先通过对应的
- 返回: 将计算得到的 P2, P3, P4, P5 作为一个列表返回。
- 接收一个包含 C2, C3, C4, C5 特征图的列表
这个代码清晰地展示了 FPN 的核心逻辑:横向连接 (1×1 卷积)、自顶向下的上采样、特征融合(加法)以及最后的 3×3 卷积处理。