《深入解析 MMDetection 中的 Feature Pyramid Network(FPN)实现》
在目标检测任务中,特征金字塔网络(FPN)是一种极为重要的结构,它能够有效地融合不同尺度的特征,为目标检测模型提供丰富的多尺度信息。本文将深入剖析 MMDetection 框架中mmdet/models/necks/fpn.py
文件的实现。
一、模块导入与注册
from typing import List, Tuple, Union
import torch.nn as nn
import torch.nn.functional as F
from mmcv.cnn import ConvModule
from mmengine.model import BaseModule
from torch import Tensor
from mmdet.registry import MODELS
from mmdet.utils import ConfigType, MultiConfig, OptConfigType
typing
模块的导入用于明确函数和方法的参数及返回值类型,提高代码的可读性和可维护性。torch.nn
和torch.nn.functional
提供了构建神经网络所需的各种层和函数。mmcv.cnn.ConvModule
是一个方便的卷积模块包装,通常包含卷积层、归一化层和激活函数,简化了卷积操作的定义。mmengine.model.BaseModule
是 MMDetection 中模型的基类,为模型提供了一些通用的功能和接口。torch.Tensor
是 PyTorch 中用于表示张量的数据结构。mmdet.registry.MODELS
是 MMDetection 中的模型注册器,用于注册各种模型组件,方便在配置文件中进行配置和调用。mmdet.utils.ConfigType
、MultiConfig
和OptConfigType
用于定义各种配置的类型,使得配置可以灵活地传递给不同的模块。
二、FPN 类定义
@MODELS.register_module()
class FPN(BaseModule):
r"""Feature Pyramid Network.
This is an implementation of paper `Feature Pyramid Networks for Object
Detection <https://arxiv.org/pdf/1612.03144>`_.
通过@MODELS.register_module()
装饰器将FPN
类注册到 MMDetection 的模型注册器中,使得该类可以通过配置文件进行实例化。
1. 构造函数参数
def __init__(
self,
in_channels: List[int],
out_channels: int,
num_outs: int,
start_level: int = 0,
end_level: int = -1,
add_extra_convs: Union[bool, str] = False,
relu_before_extra_convs: bool = False,
no_norm_on_lateral: bool = False,
conv_cfg: OptConfigType = None,
norm_cfg: OptConfigType = None,
act_cfg: OptConfigType = None,
upsample_cfg: ConfigType = dict(mode='nearest'),
init_cfg: MultiConfig = dict(
type='Xavier', layer='Conv2d', distribution='uniform')
) -> None:
in_channels
:输入特征图的通道数列表,每个元素对应一个输入尺度的通道数。这反映了上游网络(通常是骨干网络)输出的不同层级特征图的通道情况。out_channels
:输出特征图的通道数,所有输出尺度的特征图都具有相同的通道数,用于统一特征表示。num_outs
:指定输出特征图的数量,决定了 FPN 最终会输出多少个不同尺度的特征图,以满足不同检测任务对多尺度特征的需求。start_level
:从骨干网络的哪个层级开始构建特征金字塔,默认从第一个层级开始。end_level
:从骨干网络的哪个层级结束构建特征金字塔,默认取最后一个层级。如果设置为 -1,则表示取最后一个层级。add_extra_convs
:决定是否添加额外的卷积层,可以是布尔值或特定的字符串选项。如果为布尔值 True,则相当于add_extra_convs='on_input'
;如果是字符串,有三种选项:'on_input’表示在输入特征图上添加额外卷积层;'on_lateral’表示在侧向特征图上添加额外卷积层;'on_output’表示在最终输出特征图上添加额外卷积层。relu_before_extra_convs
:控制在添加额外卷积层之前是否应用 ReLU 激活函数,影响网络的非线性表达能力。no_norm_on_lateral
:决定是否在侧向卷积上不应用归一化操作,归一化可以帮助稳定训练和提高模型性能。conv_cfg
、norm_cfg
和act_cfg
:分别是卷积层、归一化层和激活层的配置字典,可以自定义不同层的具体配置,例如选择不同的卷积类型、归一化方法和激活函数。upsample_cfg
:上采样的配置字典,默认使用最近邻插值方式进行上采样,用于融合不同尺度的特征图。init_cfg
:初始化配置字典,指定模型参数的初始化方式,对模型的训练和性能有重要影响。
2. 初始化过程
super().__init__(init_cfg=init_cfg)
assert isinstance(in_channels, list)
self.in_channels = in_channels
self.out_channels = out_channels
self.num_ins = len(in_channels)
self.num_outs = num_outs
self.relu_before_extra_convs = relu_before_extra_convs
self.no_norm_on_lateral = no_norm_on_lateral
self.fp16_enabled = False
self.upsample_cfg = upsample_cfg.copy()
- 首先调用父类
BaseModule
的初始化方法,传入init_cfg
进行模型参数的初始化。 - 断言确保
in_channels
是一个列表,以保证输入参数的正确性。 - 初始化多个属性,包括输入通道数列表
in_channels
、输出通道数out_channels
、输入层级数量num_ins
(等于in_channels
的长度)、输出尺度数量num_outs
、是否在额外卷积前应用 ReLU 的标志relu_before_extra_convs
、是否在侧向卷积上不应用归一化的标志no_norm_on_lateral
、是否支持半精度浮点数(默认为 False)以及复制上采样配置字典upsample_cfg
。
if end_level == -1 or end_level == self.num_ins - 1:
self.backbone_end_level = self.num_ins
assert num_outs >= self.num_ins - start_level
else:
# if end_level is not the last level, no extra level is allowed
self.backbone_end_level = end_level + 1
assert end_level < self.num_ins
assert num_outs == end_level - start_level + 1
self.start_level = start_level
self.end_level = end_level
self.add_extra_convs = add_extra_convs
assert isinstance(add_extra_convs, (str, bool))
if isinstance(add_extra_convs, str):
# Extra_convs_source choices: 'on_input', 'on_lateral', 'on_output'
assert add_extra_convs in ('on_input', 'on_lateral', 'on_output')
elif add_extra_convs: # True
self.add_extra_convs = 'on_input'
- 根据
end_level
的值确定 backbone 的结束层级。如果end_level
为 -1 或等于输入层级数量减 1,则将backbone_end_level
设置为输入层级数量,表示使用整个骨干网络的层级。同时,断言确保输出尺度数量至少等于输入层级数量减去起始层级。 - 如果
end_level
不是最后一个层级,则将backbone_end_level
设置为end_level + 1
,并进行一些断言以确保参数的合理性。 - 初始化起始层级
start_level
、结束层级end_level
和是否添加额外卷积的标志add_extra_convs
。如果add_extra_convs
是字符串类型,断言确保其值在合法的选项范围内。如果add_extra_convs
为 True,则将其设置为'on_input'
。
self.lateral_convs = nn.ModuleList()
self.fpn_convs = nn.ModuleList()
for i in range(self.start_level, self.backbone_end_level):
l_conv = ConvModule(
in_channels[i],
out_channels,
1,
conv_cfg=conv_cfg,
norm_cfg=norm_cfg if not self.no_norm_on_lateral else None,
act_cfg=act_cfg,
inplace=False)
fpn_conv = ConvModule(
out_channels,
out_channels,
3,
padding=1,
conv_cfg=conv_cfg,
norm_cfg=norm_cfg,
act_cfg=act_cfg,
inplace=False)
self.lateral_convs.append(l_conv)
self.fpn_convs.append(fpn_conv)
- 创建两个
nn.ModuleList
,分别用于存储侧向卷积和 FPN 卷积。 - 对于每个输入层级,从起始层级到结束层级,创建对应的侧向卷积和 FPN 卷积。侧向卷积将输入特征图的通道数转换为输出通道数,通常使用 1x1 卷积。FPN 卷积进一步处理侧向特征图,通常使用 3x3 卷积。将这些卷积添加到相应的列表中,以便在后续的前向传播中使用。
# add extra conv layers (e.g., RetinaNet)
extra_levels = num_outs - self.backbone_end_level + self.start_level
if self.add_extra_convs and extra_levels >= 1:
for i in range(extra_levels):
if i == 0 and self.add_extra_convs == 'on_input':
in_channels = self.in_channels[self.backbone_end_level - 1]
else:
in_channels = out_channels
extra_fpn_conv = ConvModule(
in_channels,
out_channels,
3,
stride=2,
padding=1,
conv_cfg=conv_cfg,
norm_cfg=norm_cfg,
act_cfg=act_cfg,
inplace=False)
self.fpn_convs.append(extra_fpn_conv)
- 如果需要添加额外的卷积层(例如在 RetinaNet 中),计算需要添加的额外层级数量
extra_levels
。如果add_extra_convs
为 True 且额外层级数量大于等于 1,则创建额外的 FPN 卷积。 - 对于第一个额外卷积层,如果
add_extra_convs
为'on_input'
,则输入通道数为骨干网络结束层级的输入通道数;否则,输入通道数为输出通道数。 - 创建额外的 FPN 卷积,使用 3x3 卷积,步长为 2,以进一步提取特征。将这些额外的卷积添加到
fpn_convs
列表中。
三、forward 方法
def forward(self, inputs: Tuple[Tensor]) -> tuple:
"""Forward function.
Args:
inputs (tuple[Tensor]): Features from the upstream network, each
is a 4D-tensor.
Returns:
tuple: Feature maps, each is a 4D-tensor.
"""
assert len(inputs) == len(self.in_channels)
# build laterals
laterals = [
lateral_conv(inputs[i + self.start_level])
for i, lateral_conv in enumerate(self.lateral_convs)
]
-
参数说明:
inputs
是一个张量元组,每个张量代表上游网络(通常是骨干网络)输出的不同层级的特征图。每个张量是 4D 的,通常具有形状(batch_size, channels, height, width)
。self.in_channels
是在构造函数中初始化的列表,存储了每个输入层级特征图的通道数。
-
断言检查:
assert len(inputs) == len(self.in_channels)
这行代码确保输入的特征图数量与在构造函数中指定的输入通道数列表的长度一致。这是为了保证输入数据的正确性,防止出现不匹配的情况。
-
构建侧向特征图:
- 通过列表推导式构建侧向特征图
laterals
。对于每个侧向卷积lateral_conv
和对应的输入特征图,使用侧向卷积处理输入特征图,得到侧向特征图。 - 侧向卷积通常是 1x1 的卷积操作,用于将不同通道数的输入特征图转换为统一的输出通道数(由构造函数中的
out_channels
指定)。
例如,假设有三个输入层级,通道数分别为[64, 128, 256],输出通道数为 256。对于第一个输入层级,侧向卷积将 64 通道的特征图转换为 256 通道的侧向特征图。
- 通过列表推导式构建侧向特征图
# build top-down path
used_backbone_levels = len(laterals)
for i in range(used_backbone_levels - 1, 0, -1):
# In some cases, fixing `scale factor` (e.g. 2) is preferred, but
# it cannot co-exist with `size` in `F.interpolate`.
if 'scale_factor' in self.upsample_cfg:
# fix runtime error of "+=" inplace operation in PyTorch 1.10
laterals[i - 1] = laterals[i - 1] + F.interpolate(
laterals[i], **self.upsample_cfg)
else:
prev_shape = laterals[i - 1].shape[2:]
laterals[i - 1] = laterals[i - 1] + F.interpolate(
laterals[i], size=prev_shape, **self.upsample_cfg)
-
确定使用的骨干网络层级数量:
used_backbone_levels = len(laterals)
这行代码确定了使用的骨干网络层级数量,即侧向特征图的数量。
-
构建自顶向下的融合路径:
- 通过循环从最高层级开始,依次向下遍历层级,通过上采样将高层级的特征图调整到与低一层级相同的尺寸,然后与低一层级的特征图相加,实现特征的融合。
- 如果上采样配置字典中包含
scale_factor
,则使用该比例因子进行上采样,并通过特殊处理避免 PyTorch 1.10 中的运行时错误。 - 否则,根据低一层级特征图的尺寸进行上采样,并与低一层级特征图相加。
例如,假设有三个侧向特征图,分别对应三个层级。首先,将最高层级的特征图上采样到与中间层级相同的尺寸,然后与中间层级的特征图相加。接着,将融合后的中间层级特征图上采样到与最低层级相同的尺寸,再与最低层级的特征图相加,完成自顶向下的融合。
# build outputs
# part 1: from original levels
outs = [
self.fpn_convs[i](laterals[i]) for i in range(used_backbone_levels)
]
# part 2: add extra levels
if self.num_outs > len(outs):
# use max pool to get more levels on top of outputs
# (e.g., Faster R-CNN, Mask R-CNN)
if not self.add_extra_convs:
for i in range(self.num_outs - used_backbone_levels):
outs.append(F.max_pool2d(outs[-1], 1, stride=2))
# add conv layers on top of original feature maps (RetinaNet)
else:
if self.add_extra_convs == 'on_input':
extra_source = inputs[self.backbone_end_level - 1]
elif self.add_extra_convs == 'on_lateral':
extra_source = laterals[-1]
elif self.add_extra_convs == 'on_output':
extra_source = outs[-1]
else:
raise NotImplementedError
outs.append(self.fpn_convs[used_backbone_levels](extra_source))
for i in range(used_backbone_levels + 1, self.num_outs):
if self.relu_before_extra_convs:
outs.append(self.fpn_convs[i](F.relu(outs[-1])))
else:
outs.append(self.fpn_convs[i](outs[-1]))
return tuple(outs)
-
构建输出特征图的第一部分:
- 从原始层级构建输出特征图
outs
。通过对每个侧向特征图应用对应的 FPN 卷积,得到输出特征图的第一部分。 - FPN 卷积通常是 3x3 的卷积操作,用于进一步处理侧向特征图,以提取更丰富的特征。
- 从原始层级构建输出特征图
-
添加额外层级(如果需要):
- 如果输出尺度数量
self.num_outs
大于原始层级得到的输出数量len(outs)
,则需要添加额外层级。 - 如果不需要添加额外卷积层(
not self.add_extra_convs
),则通过对最后一个输出特征图进行最大池化操作,逐步得到更多的输出层级。 - 如果需要添加额外卷积层,则根据
add_extra_convs
的选项确定额外特征图的来源。可以是输入特征图、侧向特征图或输出特征图。然后,对额外特征图应用额外的 FPN 卷积得到新的输出特征图,并根据是否在额外卷积前应用 ReLU 进行处理。
例如,假设需要输出五个尺度的特征图,但原始层级只提供了三个尺度的特征图。如果不需要添加额外卷积层,则通过对第三个输出特征图进行最大池化操作,得到第四和第五个尺度的特征图。如果需要添加额外卷积层,并且
add_extra_convs
为'on_output'
,则将第三个输出特征图作为额外特征图的来源,经过额外的 FPN 卷积和可能的 ReLU 处理,得到第四和第五个尺度的特征图。 - 如果输出尺度数量
综上所述,mmdet/models/necks/fpn.py
文件实现了特征金字塔网络(FPN),通过精心设计的构造函数和前向传播方法,能够有效地融合不同尺度的特征,为目标检测任务提供强大的多尺度特征表示。