目标检测neck经典算法之FPN的源码实现

发布于:2025-06-24 ⋅ 阅读:(18) ⋅ 点赞:(0)
     ┌────────────────────────────────────────────────────┐
     │          初始化构造 (__init__)                      │
     └────────────────────────────────────────────────────┘
                ↓
    【1】参数保存 + 基础配置断言
                ↓
    【2】判断使用哪些backbone层(start→end)
                ↓
    【3】判断是否添加额外输出(extra conv)
                ↓
    【4】构建 lateral convs(1×1 conv,统一通道)fpn convs(3×3 conv,用于输出)
                ↓
    【5】构建 extra convs(如 RetinaNet 的 P6/P7)

FPN构造阶段

【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')

参数名 含义
in_channels 主干输出的每层特征图的通道数列表,如 [256, 512, 1024, 2048]
out_channels 所有 FPN 输出层的统一通道数,典型值是 256
num_outs 最终 FPN 输出特征层数,≥ in_channels 个数
start_level 从哪个输入层开始构造 FPN,默认是 0(即从 C2 开始)
end_level 构造到哪个输入层结束(exclusive)。-1 表示一直到最后
add_extra_convs 是否添加额外层(如 P6、P7),可为 bool 或 str
relu_before_extra_convs 添加额外层前是否加 ReLU 激活
no_norm_on_lateral 横向连接的 1x1 卷积是否加 norm(BN、GN)
conv_cfg/norm_cfg/act_cfg 可选的 conv、norm、activation 配置
upsample_cfg 上采样的参数配置,默认最近邻插值
init_cfg 初始化配置,使用 Xavier 初始化 Conv2d 层
init_cfg = dict(
    type='Xavier', layer='Conv2d', distribution='uniform'
)
assert isinstance(in_channels, list)

初始化 BaseModule 的父类构造器,并传入权重初始化配置:
表示所有的 Conv2d 层都会用 Xavier(均匀分布)初始化权重
这符合多数检测模型中推荐的初始化方式
断言 in_channels 是列表,例如 [256, 512, 1024, 2048],即来自主干网络的多层特征图的通道数。
参数保存

self.in_channels = in_channels               # 输入通道数列表
self.out_channels = out_channels             # 输出通道数
self.num_ins = len(in_channels)              # 输入特征数量
self.num_outs = num_outs                     # 期望的输出数量

这些值将用于后续构建:

lateral_convs: 1x1 卷积,输入通道数由 in_channels 决定
fpn_convs: 3x3 卷积,输出通道数都为 out_channels

👇 额外功能配置

self.relu_before_extra_convs = relu_before_extra_convs     # ReLU 加在 extra convs 前
self.no_norm_on_lateral = no_norm_on_lateral               # 控制 lateral conv 是否加 norm
self.fp16_enabled = False                                  # 是否支持混合精度(保留)

relu_before_extra_convs:可提高非线性表达能力(如 RetinaNet 中默认开启)
no_norm_on_lateral:关闭 norm 通常用于节省资源或部署推理
fp16_enabled:暂未使用,框架中可能由 AMP 插件开启
👇 上采样方式配置

self.upsample_cfg = upsample_cfg.copy()

✅ 总结一句话:
这部分是 FPN 构建流程的“设置区”,所有后续模块的搭建都将以这些参数为基础,决定网络宽度、深度、融合方式与行为特性,是 FPN 构造逻辑的入口与地基。

输入: C2 C3 C4 C5
通道: 256 512 1024 2048 → self.in_channels
目标: 构建 P2~P5 或 P3~P7(num_outs = 4~5)
每层通道统一为 256 → self.out_channels
配置:

  • start_level = 1 → 从 C3 开始
  • end_level = -1 → 一直用到最后
  • add_extra_convs=True → P6、P7
    上采样方式: nearest → self.upsample_cfg

【2】确定使用哪些 backbone 层(start_level 和 end_level)

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:
    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
 

FPN 使用的层数 = self.backbone_end_level - self.start_level
如果 end_level = -1(默认) → 使用从 start 到最后的所有层
否则 → 精准地用 start~end_level(闭区间)
num_outs 决定最终输出多少层
- 必须 >= 使用的层数(如果你还想加 extra conv)
- 如果 end_level 被限定 → 不能加 extra conv

【3】判断是否添加额外输出(extra conv)

self.add_extra_convs = add_extra_convs
assert isinstance(add_extra_convs, (str, bool))
if isinstance(add_extra_convs, str):
    assert add_extra_convs in ('on_input', 'on_lateral', 'on_output')
elif add_extra_convs:  # True
    self.add_extra_convs = 'on_input'

False → 不加额外层
True → 加,默认用 ‘on_input’
‘on_input’, ‘on_lateral’, ‘on_output’ → 指定来源

【4】构建 lateral convs(1×1 conv,统一通道)

self.lateral_convs = nn.ModuleList()
self.fpn_convs = nn.ModuleList()

初始化两个用于保存卷积层的“有序列表容器”,用于搭建横向连接(lateral)和输出卷积(fpn)结构。

📦 nn.ModuleList() 是什么?
nn.ModuleList 是 PyTorch 提供的一种特殊列表容器,专门用于存放多个子模块(如多个 nn.Conv2d)。
🎯 作用:
能像 Python 列表一样逐个添加、访问模块
最重要的是:所有子模块会自动注册到整个模型里,参数能被 model.parameters() 正确获取
支持 .to(), .cuda(), .eval() 等模型操作

        for i in range(self.start_level, self.backbone_end_level):
            l_conv = ConvModule(
		    in_channels[i],       # 输入通道:来自 backbone 的这一层
		    out_channels,         # 输出通道:FPN 要统一为同一个通道
		    1,                    # 卷积核大小:1x1
		    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,                    # 卷积核大小:3x3
		    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)

🧠 意思是:
对 backbone 中从 start_level 到 backbone_end_level - 1 的每一层,都要创建两个卷积模块:

lateral_conv: 横向 1×1 卷积(通道变换)
使用 1×1 卷积,快速调整通道数
fpn_conv: 输出 3×3 卷积(特征增强)
用途:提取输出金字塔特征
每个融合后的 feature map(如 P5、P4、P3、P2)都需要进一步通过一个 3×3 卷积处理
这样可以补充一些局部上下文信息
🧰 ConvModule 是什么?
它是 mmcv 提供的封装类,包含:
Conv → Norm → Activation
所以上面两个模块实际是:
l_conv: Conv1x1 → (BN?) → (ReLU?)
fpn_conv: Conv3x3 → BN → ReLU
根据 norm_cfg 和 act_cfg 传什么,可以构造不同风格的 FPN(GroupNorm+ReLU)

【5】构建 extra convs(如 RetinaNet 的 P6/P7)

# 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)

✅ 总结:
这段代码实现的逻辑是:

计算还需要补充的额外层数(P6、P7),例如从 3 层(C3C5)扩展到 5 层(P3P7)。
根据 add_extra_convs 参数,选择从 原始特征(如 C5)还是 前一层输出(如 P5)开始构造新层。
使用 stride=2 的 3×3 卷积生成额外层(P6、P7),减少尺寸并保持通道一致。
将构造的卷积层存入 self.fpn_convs 列表中,便于后续 forward() 使用。
在这里插入图片描述

构建部分 描述
输入通道数 (in_channels) 记录 backbone 输出层的通道数(如 C3~C5)
输出通道数 (out_channels) 所有输出层的统一通道数,通常为 256
lateral_convs 1×1 卷积,用于统一每层的通道数
fpn_convs 3×3 卷积,用于对融合后的特征图进行增强与提取
extra_convs num_outs 大于 backbone 层数时,添加的扩展卷积(如 P6、P7)
upsample_cfg 上采样配置,控制如何调整不同尺度特征图的大小
初始化配置 (init_cfg) 控制卷积层的权重初始化方式,通常为 Xavier 初始化

FPN的前向传播阶段

【1】输入校验:

检查输入特征数量是否匹配:
assert len(inputs) == len(self.in_channels)

  • 通常 inputs 为来自 backbone 的 C3, C4, C5
    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)

【2】构建 lateral 特征(横向路径):

对每一层 inputs[i] 执行 1×1 卷积 → lateral[i]
-得到 lateral 特征列表:
laterals = [L3, L4, L5]

 # build laterals
        laterals = [
            lateral_conv(inputs[i + self.start_level])
            for i, lateral_conv in enumerate(self.lateral_convs)
        ]

🎯 这一段的目的是:
把从 backbone 传入的每一层特征图(如 C3、C4、C5)先经过一个 1×1 卷积,统一它们的通道数,变成 FPN 的 lateral feature(横向特征图),供后续 top-down 融合用。

🔧 每一步在干什么?
✅ inputs[i + self.start_level]
取的是 backbone 输出的第 start_level 层开始的特征

例如 start_level = 1,就从 C3 开始

如果你传入了 [C2, C3, C4, C5],那 inputs[1] = C3,依此类推

✅ self.lateral_convs 是什么?
init 构造阶段构建的 nn.ModuleList

里面放的是多个 ConvModule(1×1),用来统一通道数

例如:

self.lateral_convs = [
Conv1x1(512 → 256), # for C3
Conv1x1(1024 → 256), # for C4
Conv1x1(2048 → 256), # for C5
]
✅ 组合:生成 lateral[i]
每一层 lateral 是这样来的:

lateral[i] = self.lateral_convs[i](inputs[i + start_level])
🧠 为什么要这样处理?
Backbone 各层通道数不同(512、1024、2048)

但 FPN 要求统一为 out_channels = 256

所以要用 1×1 卷积做通道压缩

这样才能保证后续 “逐像素相加”(top-down 融合)是合法的。

🔚 输出结果是:

laterals = [L3, L4, L5] # 每个 shape 是 [B, 256, H, W]
例如:

L3 = Conv1x1(C3)
L4 = Conv1x1(C4)
L5 = Conv1x1(C5)
这些 laterals 就是 FPN 的 主干金字塔通道,接下来就会进入 top-down 融合流程了。

【3】自顶向下特征融合(Top-Down 路径)

从高层 L5 开始,依次向下融合:

  L4 ← L4 + upsample(L5)
  L3 ← L3 + upsample(L4)
  • 使用 F.interpolate 进行上采样(默认 scale_factor=2)
# 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)

【4】构建输出特征(FPN conv 输出层)

outs = [
    self.fpn_convs[i](laterals[i]) for i in range(used_backbone_levels)
]

【5】添加额外层输出(Extra Levels:P6 / P7 等)

# 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)

在这里插入图片描述
在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到