实例教学FPN原理与PANet,Pytorch逐行精讲实现

第一部分:FPN实例教学

1. 问题:鱼与熊掌不可兼得

现在我们有一个普通的卷积神经网络(像ResNet),我们用它来处理一张图片。这张图片里有一辆很大的卡车,还有一个很小的行⼈。

网络在处理图片时,会产生不同层次的"特征图":

浅层特征图 (e.g., C3) :尺寸很大 (比如 32x32),保留了很多细节信息,比如物体的边缘、颜色、纹理。它的优点是定位精准,知道行人在哪个像素点附近。但缺点是它不知道这个纹理组合起来是个"行人"。

深层特征图 (e.g., C5) :尺寸很小 (比如 8x8),经过了多次压缩,细节信息丢失严重。但优点是,它具有高度浓缩的语义信息,能认出"这是一辆卡车"和"这是一个行人"。但缺点是定位粗糙,它只知道行人大致在左上角那块区域,具体在哪说不清。

负责定位的浅层特征,不认识物体。负责识别的深层特征,定不准物体。

这导致检测小物体(如那个行人)时非常困难。我们需要一种方法,让浅层特征图,也能获得深层特征图的智慧。FPN就是解决这个问题的

2. FPN的解决方案:自顶向下的信息传递

FPN通过一个"自顶向下"的路径,让深层特征把智慧传递给浅层特征。

实例:

输入图片 256x256

Backbone网络产生了三层特征图:

C3 : 尺寸 32x32,通道数 512

C4 : 尺寸 16x16,通道数 1024

C5 : 尺寸 8x8,通道数 2048

融合步骤 :

步骤一:先整理C5

C5虽然智慧(语义信息强),但它的信息太多(通道数2048)。我们先用一个1x1卷积给它"降维",把通道数从 2048 减少到 256

这个降维后的特征图,我们称之为 P5 。现在 P5 的尺寸是 8x8x256。它是我们金字塔的顶层,专门用来检测大物体(比如那辆卡车)。

步骤二:C5向C4传递信息

上采样 (Upsample): C5的 P5 要和 C4 联系融合到一起,得先把自己的尺寸变得和C4一样大。我们通过上采样操作,将 P5 从 8x8 放大到 16x16

C4整理: C4自己也要用一个1x1卷积,把通道数从 1024 降到 256,方便待会儿和 P5 融合

融合 (Fusion): 现在,上采样后的 P5 和降维后的 C4 尺寸完全一样了(都是16x16x256)。我们把它们按元素相加 (Element-wise Addition)。

这"相加"的一步,就是特征融合的核心,它意味着C4的特征图,每一个点都融入了来自C5的更高级的语义信息。融合后的结果,我们称之为 P4。

现在 P4 (16x16x256) 既有C4的较好定位能力,又有C5的强大识别能力。它很适合用来检测中等大小的物体。

步骤三:C4向C3传递信息

这个过程完全一样:

将刚刚生成的新P4上采样,从 16x16 放大到 32x32

对原始的 C3 进行1x1卷积,通道数从 512 降到 256

把两者相加,得到 P3 (32x32x256)。

最终成果: 我们得到了一个新的特征金字塔:P3, P4, P5

P5 (8x8): 主要来自C5,负责检测大物体。

P4 (16x16): 融合了C5和C4,负责检测中等物体。

P3 (32x32): 融合了P4和C3(间接也融合了C5),它既有C3本身超强的细节定位能力,又被赋予了来自高层的识别能力。因此,它现在能够轻松地识别并定位出那个很小的行人!

这就是FPN特征融合的魅力所在。

第二部分:FPN代码教学

下面我们用 PyTorch 来实现上面描述的整个过程。我会写一个非常简化的例子,让读者看到数据的维度是如何变化的。

  1. 模拟一个骨干网络 (Backbone) 真实的骨干网络会是ResNet等,这里我们用几个简单的卷积层来模拟,它的作用是输入一张图片,输出我们在实例中提到的 C3, C4, C5 特征图
python 复制代码
import torch
import torch.nn as nn
import torch.nn.functional as F
class ToyBackbone(nn.Module):
    def __init__(self):
        super().__init__()
        # 模拟从图片到C3的过程
        self.conv_to_c3 = nn.Sequential(
            nn.Conv2d(3, 128, kernel_size=3, stride=2, padding=1),
            nn.ReLU(),
            nn.Conv2d(128, 256, kernel_size=3, stride=2, padding=1),
            nn.ReLU(),
            nn.Conv2d(256, 512, kernel_size=3, stride=2, padding=1),
            nn.ReLU()
        )
        # 模拟从C3到C4
        self.conv_to_c4 = nn.Sequential(
            nn.Conv2d(512, 1024, kernel_size=3, stride=2, padding=1),
            nn.ReLU()
        )
        # 模拟从C4到C5
        self.conv_to_c5 = nn.Sequential(
            nn.Conv2d(1024, 2048, kernel_size=3, stride=2, padding=1),
            nn.ReLU()
        )
    def forward(self, x):
        c3 = self.conv_to_c3(x)
        c4 = self.conv_to_c4(c3)
        c5 = self.conv_to_c5(c4)
        return c3, c4, c5
  1. 实现FPN
python 复制代码
class FPN(nn.Module):
    def __init__(self, c3_channels, c4_channels, c5_channels, out_channels=256):
        super().__init__()
        self.out_channels = out_channels

        # 建立横向连接 (Lateral Connection)
        # 这是给C3, C4, C5整理的1x1卷积
        self.lat_c5 = nn.Conv2d(c5_channels, self.out_channels, kernel_size=1)
        self.lat_c4 = nn.Conv2d(c4_channels, self.out_channels, kernel_size=1)
        self.lat_c3 = nn.Conv2d(c3_channels, self.out_channels, kernel_size=1)

        # 建立平滑层
        # 融合后的特征图可以用一个3x3卷积来消除上采样带来的混叠效应
        self.smooth = nn.Conv2d(self.out_channels, self.out_channels, kernel_size=3, padding=1)

    def forward(self, c3, c4, c5):
        # 自顶向下 (Top-Down) 的路径

        # 1. 处理最高层 C5,得到 P5
        p5 = self.lat_c5(c5)

        # 2. P5上采样,与处理后的C4融合,得到 P4
        p5_upsampled = F.interpolate(p5, scale_factor=2, mode='nearest')
        p4 = self.lat_c4(c4) + p5_upsampled # <- 核心的特征融合

        # 3. P4上采样,与处理后的C3融合,得到 P3
        p4_upsampled = F.interpolate(p4, scale_factor=2, mode='nearest')
        p3 = self.lat_c3(c3) + p4_upsampled # <- 核心的特征融合

        # 对最终的金字塔层进行平滑处理,得到最终输出
        p3 = self.smooth(p3)
        p4 = self.smooth(p4)
        # p5也经过一次平滑,保持一致性
        p5 = self.smooth(p5)

        return p3, p4, p5

实例运行:

python 复制代码
if __name__ == '__main__':
    # 假设输入一张 1x3x256x256 的图片 (Batch, Channel, Height, Width)
    dummy_input = torch.randn(1, 3, 256, 256)

    # 1. 通过骨干网络,得到C3, C4, C5
    backbone = ToyBackbone()
    c3, c4, c5 = backbone(dummy_input)
    print("--- Backbone输出 ---")
    print(f"C3 shape: {c3.shape}") # 预期: [1, 512, 32, 32]
    print(f"C4 shape: {c4.shape}") # 预期: [1, 1024, 16, 16]
    print(f"C5 shape: {c5.shape}") # 预期: [1, 2048, 8, 8]
    print("-" * 20)

    # 2. 将C3, C4, C5送入FPN,得到新的特征金字塔 P3, P4, P5
    fpn_model = FPN(c3_channels=512, c4_channels=1024, c5_channels=2048, out_channels=256)
    p3, p4, p5 = fpn_model(c3, c4, c5)
    print("--- FPN输出 ---")
    print(f"P3 shape: {p3.shape}") # 预期: [1, 256, 32, 32]
    print(f"P4 shape: {p4.shape}") # 预期: [1, 256, 16, 16]
    print(f"P5 shape: {p5.shape}") # 预期: [1, 256, 8, 8]
    print("-" * 20)
    print("可以看到,所有输出特征图的通道数都统一为了256,且尺寸与输入对应层相同。")
    print("这些P3, P4, P5就是被送去预测的、融合了多尺度信息的全新特征图。")

第三部分:为什么还需要"自底向上?

我们先回顾一下FPN(自顶向下)做了什么:它让具有高层语义信息的特征图,去"帮助"具有高分辨率的低层特征图,解决了"低层特征不认识物体"的问题

但是,这里面隐藏着一个信息传递的"小瑕疵":

信息路径过长 :我们想让最底层的特征C3(32x32,定位最准)的精确位置信息,去帮助最高层的预测P5(8x8)。在FPN架构中,这个信息需要先经过整个Backbone一路向上到C5,再经过FPN的路径一路向下传回到P3,路径非常长,途中的信息可能会丢失。

FPN的"自顶向下"C5把信息传达给C3。但是,C3发现了一个非常紧急、非常具体的本地情报,比如一个精确的像素级边缘,他需要一个快速上报通道"直接反馈给C5

"自底向上"的路径,就是这个为"定位信息"建立的快速上报通道。

第四部分:PANet - 实例教学

这个新增的路径,其思想主要来源于 PANet (Path Aggregation Network)。它在FPN生成的特征金字塔(P3, P4, P5)的基础上,立即开始工作。

FPN已经为我们生成了融合后的特征金字塔:

P3 : 尺寸 32x32x256

P4 : 尺寸 16x16x256

P5 : 尺寸 8x8x256

新增的"自底向上"步骤

步骤一:P3向P4"上报"信息

下采样 (Downsample): 我们从最底层的P3开始,因为它包含了最丰富的定位信息。我们通过一个步长为2的3x3卷积,将P3的尺寸从32x32缩小到16x16

融合 (Fusion): 将下采样后的P3,与FPN已经生成的P4进行拼接 (Concatenate)

注意: 这里的融合方式通常是拼接 ,而不是相加。拼接可以看作是把两个信息渠道"并排"放在一起,保留了各自更完整的信息,然后再通过一个1x1卷积进行降维和真正的融合。

融合后的结果,我们称之为N4 。现在 N4 (16x16x256) 不仅拥有来自高层的语义信息(继承自P4),还拥有了从底层P3直接传递过来的精确定位信息。它比原来的P4更强大。

步骤二:新N4向P5"上报"信息

过程完全一样:

  1. 将刚刚生成的新N4通过一个步长为2的卷积,从16x16缩小到8x8

  2. 将下采样后的N4,与FPN生成的P5进行拼接和融合。

  3. 最终得到的结果,我们称之为N5 (8x8x256)。

最终成果: 经过"自顶向下(FPN)" + "自底向上(PANet)"这一个来回,我们得到了一套全新的、用于最终预测的特征金字塔:N3, N4, N5

N3: 就是原始的P3。

N4: 是P4融合了来自N3的定位信息。

N5: 是P5融合了来自N4的(间接也包含了N3的)定位信息。

核心优势: 这条新增的路径,极大地缩短了精确定位信息(来自底层)到高层语义特征的传递路径。现在,无论是负责检测大、中、小物体的哪个预测头,都能同时享受到最好的语义信息和定位信息。

完整的颈部 (Neck) 流程如下: Backbone -> FPN (自顶向下) -> PANet (自底向上) -> 最终的特征金字塔 (N3, N4, N5)

第三部分:PAnet代码讲解

我们在上一节的代码基础上,扩展FPN类,让它完整地包含PANet的路径。

骨干网络 ToyBackbone 和上一节完全一样,这里省略以保持简洁:

python 复制代码
import torch
import torch.nn as nn
import torch.nn.functional as F

class ToyBackbone(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv_to_c3 = nn.Sequential(nn.Conv2d(3, 512, kernel_size=8, stride=8))
        self.conv_to_c4 = nn.Sequential(nn.Conv2d(512, 1024, kernel_size=2, stride=2))
        self.conv_to_c5 = nn.Sequential(nn.Conv2d(1024, 2048, kernel_size=2, stride=2))
    def forward(self, x):
        c3 = self.conv_to_c3(x); c4 = self.conv_to_c4(c3); c5 = self.conv_to_c5(c4)
        return c3, c4, c5

实现一个包含 FPN + PANet 的完整颈部:

通过torch.cat将两个特征图在通道维度上堆叠起来,然后立即使用一个1x1卷积,一方面将翻倍的通道数降回原来的维度,另一方面更重要的是,通过可学习的权重,对来自不同源头的信息进行智能的、加权的融合,从而产生一个全新的、信息更丰富的特征图。

python 复制代码
class FPN_PANet_Neck(nn.Module):
    def __init__(self, c3_channels, c4_channels, c5_channels, out_channels=256):
        super().__init__()
        self.out_channels = out_channels

        # --- FPN (自顶向下) 部分的层 ---
        self.lat_c5 = nn.Conv2d(c5_channels, self.out_channels, kernel_size=1)
        self.lat_c4 = nn.Conv2d(c4_channels, self.out_channels, kernel_size=1)
        self.lat_c3 = nn.Conv2d(c3_channels, self.out_channels, kernel_size=1)
        self.fpn_smooth = nn.Conv2d(self.out_channels, self.out_channels, kernel_size=3, padding=1)


        # --- PANet (自底向上) 部分的层 ---
        # 用于下采样的卷积
        self.pan_downsample1 = nn.Conv2d(self.out_channels, self.out_channels, kernel_size=3, stride=2, padding=1)
        self.pan_downsample2 = nn.Conv2d(self.out_channels, self.out_channels, kernel_size=3, stride=2, padding=1)

        # 拼接后用于融合的卷积
        self.pan_fuse1 = nn.Conv2d(self.out_channels * 2, self.out_channels, kernel_size=1)
        self.pan_fuse2 = nn.Conv2d(self.out_channels * 2, self.out_channels, kernel_size=1)

前向传播:

python 复制代码
def forward(self, c3, c4, c5):
        # --- 1. FPN: 自顶向下路径 ---
        p5 = self.lat_c5(c5)
        p5_upsampled = F.interpolate(p5, scale_factor=2, mode='nearest')
        p4 = self.lat_c4(c4) + p5_upsampled
        p4_upsampled = F.interpolate(p4, scale_factor=2, mode='nearest')
        p3 = self.lat_c3(c3) + p4_upsampled
        
        # FPN 路径的输出(经过平滑)
        p3 = self.fpn_smooth(p3)
        p4 = self.fpn_smooth(p4)
        p5 = self.fpn_smooth(p5)

        # --- 2. PANet: 自底向上路径 ---
        # P3 就是最终的 N3
        n3 = p3 
        
        # 从 N3 到 N4
        n3_downsampled = self.pan_downsample1(n3)
        # 拼接 P4 和下采样后的 N3
        n4_concatenated = torch.cat([n3_downsampled, p4], dim=1) # dim=1 是通道维度
        n4 = self.pan_fuse1(n4_concatenated)

        # 从 N4 到 N5
        n4_downsampled = self.pan_downsample2(n4)
        # 拼接 P5 和下采样后的 N4
        n5_concatenated = torch.cat([n4_downsampled, p5], dim=1)
        n5 = self.pan_fuse2(n5_concatenated)

        # 返回最终用于预测的特征图
        return n3, n4, n5

实例运行:

python 复制代码
if __name__ == '__main__':
    dummy_input = torch.randn(1, 3, 256, 256)
    backbone = ToyBackbone()
    c3, c4, c5 = backbone(dummy_input)
    
    print("--- Backbone输出 ---")
    print(f"C3 shape: {c3.shape}")
    print(f"C4 shape: {c4.shape}")
    print(f"C5 shape: {c5.shape}")
    print("-" * 30)

    # 实例化完整的颈部
    neck = FPN_PANet_Neck(c3_channels=512, c4_channels=1024, c5_channels=2048, out_channels=256)
    n3, n4, n5 = neck(c3, c4, c5)

    print("--- FPN + PANet Neck 输出 ---")
    print("这些是最终送入预测头的特征图:")
    print(f"N3 (for small objects) shape: {n3.shape}")
    print(f"N4 (for medium objects) shape: {n4.shape}")
    print(f"N5 (for large objects) shape: {n5.shape}")
    print("-" * 30)
    print("虽然维度和单独使用FPN时一样,但N4和N5现在包含了从底层'快速上报'的更精准的定位信息。")
相关推荐
Wendy14411 小时前
【边缘填充】——图像预处理(OpenCV)
人工智能·opencv·计算机视觉
钱彬 (Qian Bin)1 小时前
《使用Qt Quick从零构建AI螺丝瑕疵检测系统》——8. AI赋能(下):在Qt中部署YOLOv8模型
人工智能·qt·yolo·qml·qt quick·工业质检·螺丝瑕疵检测
星月昭铭2 小时前
Spring AI调用Embedding模型返回HTTP 400:Invalid HTTP request received分析处理
人工智能·spring boot·python·spring·ai·embedding
大千AI助手3 小时前
直接偏好优化(DPO):原理、演进与大模型对齐新范式
人工智能·神经网络·算法·机器学习·dpo·大模型对齐·直接偏好优化
ReinaXue3 小时前
大模型【进阶】(四)QWen模型架构的解读
人工智能·神经网络·语言模型·transformer·语音识别·迁移学习·audiolm
静心问道3 小时前
Deja Vu: 利用上下文稀疏性提升大语言模型推理效率
人工智能·模型加速·ai技术应用
小妖同学学AI3 小时前
deepseek+飞书多维表格 打造小红书矩阵
人工智能·矩阵·飞书
阿明观察3 小时前
再谈亚马逊云科技(AWS)上海AI研究院7月22日关闭事件
人工智能
zzywxc7874 小时前
AI 驱动的软件测试革新:框架、检测与优化实践
人工智能·深度学习·机器学习·数据挖掘·数据分析
WSSWWWSSW4 小时前
华为昇腾NPU卡 文生视频[T2V]大模型WAN2.1模型推理使用
人工智能·大模型·音视频·显卡·文生视频·文生音频·文生音乐