┌────────────────────────────────────────────────────┐│ 初始化構造 (__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)