当前位置: 首页 > ai >正文

实例教学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 特征图

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到C4self.conv_to_c4 = nn.Sequential(nn.Conv2d(512, 1024, kernel_size=3, stride=2, padding=1),nn.ReLU())# 模拟从C4到C5self.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

2. 实现FPN

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,得到 P5p5 = self.lat_c5(c5)# 2. P5上采样,与处理后的C4融合,得到 P4p5_upsampled = F.interpolate(p5, scale_factor=2, mode='nearest')p4 = self.lat_c4(c4) + p5_upsampled # <- 核心的特征融合# 3. P4上采样,与处理后的C3融合,得到 P3p4_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

实例运行:

if __name__ == '__main__':# 假设输入一张 1x3x256x256 的图片 (Batch, Channel, Height, Width)dummy_input = torch.randn(1, 3, 256, 256)# 1. 通过骨干网络,得到C3, C4, C5backbone = 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, P5fpn_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 和上一节完全一样,这里省略以保持简洁:

import torch
import torch.nn as nn
import torch.nn.functional as Fclass 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卷积,一方面将翻倍的通道数降回原来的维度,另一方面更重要的是,通过可学习的权重,对来自不同源头的信息进行智能的、加权的融合,从而产生一个全新的、信息更丰富的特征图。

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)

前向传播:

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_upsampledp4_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 就是最终的 N3n3 = p3 # 从 N3 到 N4n3_downsampled = self.pan_downsample1(n3)# 拼接 P4 和下采样后的 N3n4_concatenated = torch.cat([n3_downsampled, p4], dim=1) # dim=1 是通道维度n4 = self.pan_fuse1(n4_concatenated)# 从 N4 到 N5n4_downsampled = self.pan_downsample2(n4)# 拼接 P5 和下采样后的 N4n5_concatenated = torch.cat([n4_downsampled, p5], dim=1)n5 = self.pan_fuse2(n5_concatenated)# 返回最终用于预测的特征图return n3, n4, n5

实例运行:

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现在包含了从底层'快速上报'的更精准的定位信息。")

http://www.xdnf.cn/news/16537.html

相关文章:

  • [leetcode] Z字型变换
  • dify离线插件打包步骤
  • 手撕设计模式——智能家居之外观模式
  • C++线程详解
  • C++11 std::function 详解:通用多态函数包装器
  • 从0开始学习R语言--Day62--RE插补
  • 【ssh】ubuntu服务器+本地windows主机,使用密钥对进行ssh链接
  • Linux常用基础命令
  • 反射核心:invoke与setAccessible方法详解
  • Git 从入门到精通
  • linux命令ps的实际应用
  • SQL注入SQLi-LABS 靶场less26-30详细通关攻略
  • 深入解析Java元注解与运行时处理
  • ​第七篇:Python数据库编程与ORM实践
  • 前缀和-974.和可被k整除的子数组-力扣(LeetCode)
  • [mcp: JSON-RPC 2.0 规范]
  • 机器学习之线性回归——小白教学
  • LRU(Least Recently Used)原理及算法实现
  • 最新优茗导航系统源码/全开源版本/精美UI/带后台/附教程
  • BreachForums 黑客论坛强势回归
  • sqLite 数据库 (2):如何复制一张表,事务,聚合函数,分组加过滤,列约束,多表查询,视图,触发器与日志管理,创建索引
  • JAVA_TWENTY—ONE_单元测试+注解+反射
  • 学习Python中Selenium模块的基本用法(3:下载浏览器驱动续)
  • Seq2Seq学习笔记
  • 前端优化之虚拟列表实现指南:从库集成到手动开发
  • 嵌入式学习日志————TIM定时中断之定时器定时中断
  • Python算法实战:从排序到B+树全解析
  • 算法精讲:二分查找(一)—— 基础原理与实现
  • 自学嵌入式 day37 HTML
  • 信号上升沿时间与频谱分量的关系