┌────────────────────────────────────────────────────┐│ 初始化构造 (__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_insassert num_outs >= self.num_ins - start_level
else:self.backbone_end_level = end_level + 1assert end_level < self.num_insassert 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: # Trueself.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, # 卷积核大小:1x1conv_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, # 卷积核大小:3x3padding=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_levelif 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_channelsextra_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, eachis 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 lateralslaterals = [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 pathused_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.10laterals[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 levelsif 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 NotImplementedErrorouts.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)