在深度学习和计算机视觉领域,架构创新在推动技术进步中发挥了重要作用。在这些创新中,特征金字塔网络(Feature Pyramid Networks, FPN)脱颖而出,成为革命性的基础构建模块,彻底改变了我们在神经网络中处理多尺度特征表示的方式。本文将深入探讨FPN。
一、现代神经网络的三个主要组成部分(骨干网络、颈部网络和头部网络)
在深入FPN之前,了解现代计算机视觉神经网络的三个主要组成部分非常重要:
1. 骨干网络(Backbone)
骨干网络通常是一个卷积神经网络(如ResNet或VGG),作为主要特征提取器。它处理原始输入图像,并生成不同尺度的层次化特征表示。可以将其视为捕捉从边缘、纹理到高级语义信息的基础。
2. 颈部网络(Neck)
颈部网络是骨干网络和头部网络之间的特征融合和增强模块。其主要目的是处理和组合来自骨干网络不同尺度或阶段的特征,以生成更具判别性的特征表示。可以将其视为一个加工厂,将来自不同来源的原材料(特征)提炼成更有用的产品。
颈部网络可以执行多种操作,例如:
- 跨不同尺度的特征融合
- 通过额外卷积增强特征
- 管理不同网络层级之间的信息流
特征金字塔网络是颈部网络架构的一种流行实现,但还有其他实现,如路径聚合网络(PANet)和高分辨率网络(HRNet)。
3. 头部网络(Head)
头部网络是任务特定的组件,使用经过优化的特征进行最终预测。不同任务(检测、分割、分类)需要不同的头部架构,但它们都受益于颈部网络提供的经过良好处理的特征。
二、为什么需要特征金字塔网络?
计算机视觉中的多尺度挑战源于传统CNN架构的多个基本限制:
1. 特征层次问题
随着CNN的深入,空间分辨率降低,而语义层次提高。例如,在典型的ResNet中:
- 早期层(如Conv1)具有1/2分辨率,捕捉基本特征(边缘、纹理)
- 中间层(如Conv3)具有1/8分辨率,捕捉中级特征(部件、模式)
- 深层(如Conv5)具有1/32分辨率,捕捉高级特征(物体、场景)
2. 尺度变化
自然图像中的物体以不同的尺度出现。例如,在自动驾驶中:
- 附近的行人可能占据300x600像素
- 远处的车辆可能仅占据30x60像素
- 交通标志可能以任何大小出现
3. 信息丢失
传统的特征金字塔(如图像金字塔)保持了空间分辨率,但在较低层次缺乏语义强度,使其在现代深度学习中效率低下。这些问题的结合构成了计算机视觉中的重大挑战。
现实世界的例子:想象一辆自动驾驶汽车试图检测街道上的物体。 摄像头看到的物体距离不同,有些近,有些远。 要检测远处的行人,系统需要处理高分辨率(详细)图像以捕捉小细节。 但问题在于:处理这些详细图像的早期网络层并不擅长理解内容。它们可能看到人的基本形状,但无法区分是行人还是路灯杆,因为它们缺乏深层次的理解。
为什么不使用传统方法(如图像金字塔)? 这种方法提取的特征信息不够丰富,无法真正用于现代深度学习。
我们陷入了两难选择: 要么获得良好的细节但理解力差,要么获得良好的理解力但细节差。这就像在放大镜(能看清细节但无法识别物体)和模糊眼镜(能识别物体但看不清细节)之间做出选择。 这种“看清”与“理解”之间的权衡正是研究人员开发特征金字塔网络的原因——最终解决这一困境。
三、特征金字塔网络(FPN)
图片、、
FPN通过三个关键组件结合了低层次和高层次特征:
(1) 自下而上路径(骨干网络)
- 这是常规的卷积神经网络前向传播。
- 特征逐渐变得更语义化,但空间分辨率降低。
- 每个阶段输出不同尺度的特征图(C₂, C₃, C₄, C₅)。
(2) 自上而下路径
- 从最深层开始,逐步上采样空间较粗糙但语义较强的特征。
- 创建更高分辨率的特征(P₅, P₄, P₃, P₂)。
- 使用最近邻上采样来增加分辨率。
(3) 横向连接
- 1x1卷积减少骨干网络特征的通道维度。
- 逐元素加法合并自下而上和自上而下的特征。
- 3x3卷积平滑合并后的特征。
1. 技术过程
- 提取自下而上的特征 {C₂, C₃, C₄, C₅}
- 通过1x1卷积处理顶层特征C₅以创建P₅
- 上采样P₅并与处理后的C₄合并以创建P₄
- 此过程持续到P₂
- 最终金字塔的每个层级 {P₂, P₃, P₄, P₅} 包含丰富的语义信息,同时保持适当的空间分辨率
2. 示例代码
以下是一个使用ResNet-18骨干网络实现FPN进行图像分类的示例代码。
import torch
import torch.nn as nn
import torchvision.models as models
class FPNNeck(nn.Module):
def __init__(self, in_channels_list, out_channels):
super(FPNNeck, self).__init__()
# Lateral connections (1x1 convolutions)
self.lateral_convs = nn.ModuleList([
nn.Conv2d(in_channels, out_channels, 1)
for in_channels in in_channels_list
])
# Top-down pathway (upsampling + smoothing)
self.fpn_convs = nn.ModuleList([
nn.Conv2d(out_channels, out_channels, 3, padding=1)
for _ in range(len(in_channels_list))
])
def forward(self, features):
# features should be ordered from highest resolution to lowest
laterals = [conv(feature) for feature, conv in zip(features, self.lateral_convs)]
# Top-down pathway
for i in range(len(laterals)-1, 0, -1):
laterals[i-1] += nn.functional.interpolate(
laterals[i], size=laterals[i-1].shape[-2:], mode='nearest'
)
# Smoothing
outputs = [conv(lateral) for lateral, conv in zip(laterals, self.fpn_convs)]
return outputs
class ResNetFPN(nn.Module):
def __init__(self, num_classes):
super(ResNetFPN, self).__init__()
# Load pretrained ResNet-18 as backbone
resnet = models.resnet18(pretrained=True)
self.backbone_layers = nn.ModuleList([
nn.Sequential(resnet.conv1, resnet.bn1, resnet.relu, resnet.maxpool, resnet.layer1),
resnet.layer2,
resnet.layer3,
resnet.layer4
])
# FPN neck
in_channels_list = [64, 128, 256, 512] # ResNet-18 output channels
self.fpn = FPNNeck(in_channels_list, out_channels=256)
# Classification head
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(256 * 4, num_classes) # 4 feature maps from FPN
def forward(self, x):
# Extract features from backbone
features = []
for layer in self.backbone_layers:
x = layer(x)
features.append(x)
# FPN forward pass
fpn_features = self.fpn(features)
# Global average pooling on each FPN level
pooled_features = []
for feature in fpn_features:
pooled = self.avgpool(feature)
pooled_features.append(pooled.flatten(1))
# Concatenate all pooled features
x = torch.cat(pooled_features, dim=1)
x = self.fc(x)
return x
import torch
import torch.nn as nn
import torchvision.models as models
class FPNNeck(nn.Module):
def __init__(self, in_channels_list, out_channels):
super(FPNNeck, self).__init__()
# Lateral connections (1x1 convolutions)
self.lateral_convs = nn.ModuleList([
nn.Conv2d(in_channels, out_channels, 1)
for in_channels in in_channels_list
])
# Top-down pathway (upsampling + smoothing)
self.fpn_convs = nn.ModuleList([
nn.Conv2d(out_channels, out_channels, 3, padding=1)
for _ in range(len(in_channels_list))
])
FPNNeck类实现了FPN的核心架构:
- `lateral_convs` 创建1x1卷积,减少来自不同层级骨干网络特征的通道维度。
- `fpn_convs` 是3x3卷积,用于平滑合并后的特征。
def forward(self, features):
# features should be ordered from highest resolution to lowest
laterals = [conv(feature) for feature, conv in zip(features, self.lateral_convs)]
# Top-down pathway
for i in range(len(laterals)-1, 0, -1):
laterals[i-1] += nn.functional.interpolate(
laterals[i], size=laterals[i-1].shape[-2:], mode='nearest'
)
# Smoothing
outputs = [conv(lateral) for lateral, conv in zip(laterals, self.fpn_convs)]
return outputs
前向传播展示了FPN如何处理特征:
- 它对所有特征层级应用横向卷积。
- 它实现自上而下的路径:从最深层开始,上采样特征并将其添加到上一层级。
- 它对所有层级应用平滑卷积。
class ResNetFPN(nn.Module):
def __init__(self, num_classes):
super(ResNetFPN, self).__init__()
# Load pretrained ResNet-18 as backbone
resnet = models.resnet18(pretrained=True)
self.backbone_layers = nn.ModuleList([
nn.Sequential(resnet.conv1, resnet.bn1, resnet.relu, resnet.maxpool, resnet.layer1),
resnet.layer2,
resnet.layer3,
resnet.layer4
])
ResNetFPN类组合所有内容:
- 使用预训练的ResNet-18作为骨干网络。
- 添加FPN颈部网络以处理来自ResNet四个阶段的特征。
- 添加一个简单的分类头,用于池化每个FPN层级的特征并最终分类。
# FPN neck
in_channels_list = [64, 128, 256, 512] # ResNet-18 output channels
self.fpn = FPNNeck(in_channels_list, out_channels=256)
# Classification head
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(256 * 4, num_classes) # 4 feature maps from FPN
对于分类任务:我们创建FPN颈部网络,用于处理来自ResNet四个阶段的特征。我们添加一个简单的分类头,其功能包括:
- 对每个FPN层级的特征进行池化
- 将它们连接在一起
- 进行最终的分类
def forward(self, x):
# Extract features from backbone
features = []
for layer in self.backbone_layers:
x = layer(x)
features.append(x)
# FPN forward pass
fpn_features = self.fpn(features)
# Global average pooling on each FPN level
pooled_features = []
for feature in fpn_features:
pooled = self.avgpool(feature)
pooled_features.append(pooled.flatten(1))
# Concatenate all pooled features
x = torch.cat(pooled_features, dim=1)
x = self.fc(x)
return x
前向传播将所有内容结合在一起:
- 输入图像通过ResNet骨干网络,收集每个阶段的特征。
- 这些特征通过FPN颈部网络,生成特征金字塔。
- 我们对金字塔的每个层级的特征进行池化。
- 最后,我们结合所有这些特征以进行最终的分类预测。
四、变体(特征金字塔网络的演进)
自FPN诞生以来,研究人员对其进行了多种改进。以下是一些常见变体:
1. PANet(路径聚合网络)
- 通过添加额外的自下而上路径增强信息流。
- 用于实例分割的Mask Scoring R-CNN和实时目标检测的Thunder-Net。
2. BiFPN(双向FPN)
- 引入加权双向跨尺度连接。
- 用于EfficientDet系列目标检测器。
3. 最新模型(2023-2024)
- RT-DETR:使用基于变形Transformer的FPN变体。
- DINO-V2:实现混合FPN-Transformer颈部网络。
- YOLOv8:采用受FPN启发的改进CSP-PAN颈部网络。
五、结论
特征金字塔网络代表了计算机视觉架构设计的一项重大成就。其有效处理多尺度特征表示并保持计算效率的能力,使其成为现代计算机视觉系统中不可或缺的组成部分。随着领域的不断发展,FPN的影响可以在新架构中看到,其设计原则继续激发神经网络设计的创新。