ARTICLE

从 FPN 到 SAM 2:理解特征金字塔网络

最近看 SAM 2 的论文和代码时,我在图像编码器里遇到了 FpnNeck。一开始看到“特征金字塔网络”这个名字,我以为它只是把不同分辨率的特征图放在一起;继续读 FPN 原论文和 SAM 2 的实现后才发现,真正关键的并不是“有很多层”,而是怎样让高分辨率特征也拥有足够强的语义,同时保留它精确的空间定位能力

在进入公式之前,先确定它在整个视觉模型里的位置:

输入图像
  ↓
Backbone:逐层提取 C2、C3、C4、C5
  ↓
FPN / Neck:对齐并融合不同尺度,得到 P2、P3、P4、P5
  ↓
Task Head:完成分类、检测框、关键点或掩码预测

C 表示 backbone 的原始 stage 输出,P 表示经过金字塔融合、准备交给任务头的输出。FPN 不是 backbone,也不直接预测类别或掩码;它是夹在 backbone 和 head 之间的 neck,负责把原本语义强弱不一的多层特征整理成更好用的多尺度接口,我的理解是作为一个多尺度融合的方法。

1. 为什么视觉模型需要多尺度特征

同一种物体在图片中可能只有十几个像素,也可能占据大半张图。对检测和分割来说,这不是简单的缩放问题:物体大小会直接决定它在特征图上还剩多少空间信息。

假设 backbone 每经过一个 stage 就把高、宽缩小一半:

特征层 相对输入的步长 1024×1024 输入对应尺寸 典型特点
$C_2$ 4 256×256 定位细,但语义较浅
$C_3$ 8 128×128 细节与语义开始折中
$C_4$ 16 64×64 语义强,空间较粗
$C_5$ 32 32×32 感受野大,但小目标容易消失

这里存在一个天然矛盾:

  • 浅层特征分辨率高,边缘、纹理和位置清楚,但还不太知道“这是什么”;
  • 深层特征语义强、感受野大,却经过多次下采样,不太知道“它精确在哪里”。

大目标即使落到低分辨率特征图上仍然占据很多格子,深层语义足以识别;小目标在 stride 32 的特征图上可能只剩一个点,甚至在下采样中完全消失。多尺度问题的本质,是语义强度与空间分辨率分布在网络的不同深度。

图像金字塔、单层深特征、直接使用骨干层级和 FPN 四种多尺度方案的对比;FPN 只运行一次骨干网络,同时让各分辨率输出都具有较强语义
过去的方案要么对多种输入尺寸重复运行 backbone,要么只用一张深层特征图,要么直接使用语义强弱不一的骨干层级。FPN 的目标是一次前向计算得到语义一致的多尺度输出。

2. FPN 之前的几种办法分别缺什么

2.1 图像金字塔:准确,但计算昂贵

最直接的方法是把同一张图片缩放成多个尺寸,每个尺寸都单独运行一遍完整网络。小物体在放大的图片中变得容易识别,大物体则可以在缩小的图片中处理。

这种方法的优点是每个尺度都能获得完整的深层语义;缺点也很明显:backbone 被重复计算多次,推理时间和显存开销都很大。

2.2 单张深层特征图:速度快,但小目标吃亏

Faster R-CNN 等系统为了效率,通常只在一张深层特征图上做预测。深层特征适合分类,却已经丢掉大量高分辨率信息。给检测头准备不同大小的 anchor,并不能恢复 backbone 下采样时已经消失的细节。

2.3 直接使用 backbone 的多层输出:尺度有了,语义不齐

卷积网络本来就会产生由高分辨率到低分辨率的层级,因此也可以直接在多个 stage 上预测。问题是浅层与深层的语义差距很大:越靠近输入的层越擅长描述纹理和边缘,却未必能稳定表示目标类别。

所以 FPN 要解决的不是“怎样制造不同大小的特征图”——backbone 早就有这些特征图——而是:

能否只计算一次 backbone,再把深层的强语义传回高分辨率层,使每一个尺度都适合做预测?

3. FPN 的核心贡献究竟是什么

FPN 的核心设计由三部分组成:

  1. Bottom-up pathway:正常运行 backbone,获得 $C_2,C_3,C_4,C_5$;
  2. Top-down pathway:从最深层开始,逐级上采样,把强语义送向更高分辨率;
  3. Lateral connection:把上采样结果与同尺寸的 backbone 特征横向融合。

它的改变很克制:没有重新设计复杂 backbone,也没有对多个输入尺度重复计算。它只是重新组织 backbone 已经计算出的层级,让“深层语义”和“浅层定位”在每个尺度相遇。

这也是 FPN 论文最重要的贡献:用很小的额外开销,在单尺度输入、单次 backbone 前向中,构造出所有层都具有强语义的特征金字塔。原论文进一步强调,这些金字塔层不是只为恢复一张最高分辨率输出,而是可以分别承担不同尺度的预测任务。

backbone 自下而上生成 C2 到 C5;FPN 从 C5 开始向上逐级放大,与经过一乘一卷积的同尺度特征相加,再经三乘三卷积得到 P2 到 P5
Bottom-up 负责逐步扩大感受野,top-down 把深层语义送回高分辨率,lateral connection 则补回当前尺度的定位细节。输出 P2–P5 空间尺寸不同,但通道数和语义层级更一致。

4. 标准 FPN 的完整工作流程

以 ResNet backbone 为例,FPN 取每个 stage 最后一个 block 的输出:

\[\{C_2,C_3,C_4,C_5\},\qquad \operatorname{stride}=\{4,8,16,32\}\]

4.1 Bottom-up:backbone 先产生天然的尺度层级

以 ResNet-50 为例,四个 stage 的输出可以写成:

特征 PyTorch 形状 stride 每个网格大致对应的输入区域
$C_2$ $[B,256,H/4,W/4]$ 4 细,适合定位
$C_3$ $[B,512,H/8,W/8]$ 8 开始形成局部结构
$C_4$ $[B,1024,H/16,W/16]$ 16 语义较强
$C_5$ $[B,2048,H/32,W/32]$ 32 感受野最大、空间最粗

这条从 $C_2$ 走到 $C_5$ 的普通前向过程,就是论文所说的 bottom-up pathway。它没有增加额外结构,只是在命名 backbone 已经存在的计算路径。

这里有两个不匹配:相邻层高、宽不同,通道数也不同。例如 $C_4$ 是 1024 通道,而 $C_5$ 是 2048 通道;如果想让两层逐元素相加,必须同时完成空间和通道对齐。

4.2 Lateral projection:用 1×1 卷积统一通道

不同 stage 的通道数通常不同,例如 ResNet-50 的 $C_2$ 到 $C_5$ 可以是 256、512、1024、2048 通道。逐元素相加要求通道一一对应,所以先做横向投影:

\[L_i=\operatorname{Conv}_{1\times1}(C_i),qquad L_i\in\mathbb{R}^{H_i\times W_i\times d}\]

原始 FPN 将所有层统一为 $d=256$。这里的 1×1 卷积不是简单地“截掉多余通道”。在任意空间位置 $(x,y)$,它都在做一个可学习的线性投影:

\[\mathbf l_i(x,y)=W_i\mathbf c_i(x,y)+\mathbf b_i\]

如果输入是 1024 通道、输出是 256 通道,那么 $W_i\in\mathbb R^{256\times1024}$。它学习怎样把每个 stage 自己的通道语义翻译到统一的 FPN 特征空间,同时不改变高和宽。

所以 1×1 卷积有两个作用:

  1. 让横向分支与 top-down 分支都变成 256 通道,满足逐元素相加的形状要求;
  2. 对 backbone 特征重新组合,而不是把两套含义完全不同的通道生硬地对齐。

4.3 Top-down:从最深层开始逐级上采样

最粗的一层直接由 $C_5$ 投影得到:

\[M_5=L_5\]

然后递归计算:

\[M_i=L_i+\operatorname{Up}_2(M_{i+1}),qquad i=4,3,2\]

$\operatorname{Up}_2$ 把高、宽各放大 2 倍,使它与 $L_i$ 的空间尺寸一致。原始论文为了简单使用最近邻插值,再做逐元素相加。

需要特别注意:上采样只扩大网格,不会凭空恢复细节。一张 32×32 的深层特征被复制成 64×64 后,空间坐标变多了,但其中并没有重新出现下采样时丢失的边缘。它更像把一张“语义明确但模糊”的小图放大。

真正补回当前尺度空间信息的是横向连接 $L_i$。因此:

\[M_i(x,y)= \underbrace{L_i(x,y)}_{\text{当前尺度的空间线索}} + \underbrace{\operatorname{Up}_2(M_{i+1})(x,y)}_{\text{更深层传来的语义}}\]

top-down 负责把“它是什么”送回来,lateral connection 负责告诉它“更准确地在哪里”。两条路径缺一不可:只上采样会得到放大的粗特征,只用 lateral features 又会回到浅层语义不足的问题。

为什么是相加而不是拼接?相加计算简单、内存开销小,而且两边已经通过 1×1 卷积对齐到相同通道。它迫使每个输出通道同时综合“当前尺度的定位信息”和“更深层传来的语义信息”。拼接当然也能实现融合,但会增加通道和后续计算,并不是原始 FPN 的设计。

4.4 为什么经典 FPN 选择相加而不是拼接

假设 lateral 与 top-down 分支都是 256 通道:

  • 相加后仍是 256 通道,不需要额外扩大内存和预测头;
  • 拼接后会变成 512 通道,通常还要再加卷积压回 256 通道;
  • 相加要求两边在同一通道空间表达可兼容的信息,这也是前面 1×1 投影存在的意义。

相加并不意味着两边在概念上永远同等重要,只表示经典 FPN 使用了最简洁的无参数融合。后来的 BiFPN 会给不同输入增加可学习权重,正是在放宽这一假设。

4.5 用 3×3 卷积生成最终输出

原始 FPN 在每个融合结果后再接一层 3×3 卷积:

\[P_i=\operatorname{Conv}_{3\times3}(M_i)\]

论文给出的目的,是减轻上采样带来的混叠效应。逐元素相加只融合同一个坐标的两个向量,而 3×3 卷积还会重新观察它周围的 3×3 邻域,因此也能整理最近邻复制形成的块状模式。

原论文还特意说明,在这些额外卷积层后没有加入非线性激活。FPN 在这里更像一个保持信息连续的特征重组器:先让横向特征与 top-down 特征直接相加,再交给下游任务头学习如何使用。

最后得到 $P_2,P_3,P_4,P_5$:它们分别与 $C_2,C_3,C_4,C_5$ 同尺寸,但都具有统一的 256 通道和更接近的语义强度。这里最好区分两个符号:

  • $M_i$:top-down 递归中的中间融合特征,还要继续向更高分辨率传播;
  • $P_i$:经过 3×3 卷积整理后,真正交给任务头的金字塔输出。

有些实现会复用变量名,看起来没有区分 $M_i$ 和 $P_i$,但理解论文时把两者分开更清楚。

单个 FPN 融合单元中,深层特征先放大两倍,当前骨干特征用一乘一卷积统一通道,两者逐元素相加后由三乘三卷积平滑,得到当前金字塔输出
一个 FPN 单元只做四件事:上采样深层语义、1×1 对齐当前层、逐元素相加、3×3 平滑输出。空间尺寸由当前层决定,语义则从更深层逐级传来。

4.6 用 1024×1024 输入完整走一遍

假设输入为 1024×1024,ResNet backbone 产生:

C2: [B,  256, 256, 256]
C3: [B,  512, 128, 128]
C4: [B, 1024,  64,  64]
C5: [B, 2048,  32,  32]

从最深层开始:

M5 = Conv1×1(C5)                         → [B, 256,  32,  32]
M4 = Conv1×1(C4) + Up(M5, 64×64)         → [B, 256,  64,  64]
M3 = Conv1×1(C3) + Up(M4, 128×128)       → [B, 256, 128, 128]
M2 = Conv1×1(C2) + Up(M3, 256×256)       → [B, 256, 256, 256]

最后分别施加 3×3 卷积:

P2: [B, 256, 256, 256]   stride 4
P3: [B, 256, 128, 128]   stride 8
P4: [B, 256,  64,  64]   stride 16
P5: [B, 256,  32,  32]   stride 32

这里“金字塔”指的是空间尺寸逐级减半,而不是通道数逐级减少。恰恰相反,FPN 通常把每层通道统一,方便同一个预测头跨尺度复用。

工程实现里,最好把上采样目标直接写成 lateral feature 的实际尺寸,而不是永远假设 scale_factor=2。输入尺寸不是 32 的整数倍时,连续下采样可能出现 25→13 这样的奇数尺寸;13×2 得到 26,却未必能与 25×25 的横向特征相加。显式指定 size=lateral.shape[-2:] 更稳妥。

4.7 为什么最后连 $P_2$ 也有较强语义

把递归关系展开就能看见答案。记 $L_i=\operatorname{Conv}_{1\times1}(C_i)$,忽略书写中的重复上采样:

\[M_2=L_2+\operatorname{Up}\left( L_3+\operatorname{Up}\left( L_4+\operatorname{Up}(L_5) \right)\right)\]

所以 $P_2$ 并不是把浅层 $C_2$ 单独拿来预测。它直接保留 $C_2$ 的高分辨率线索,又间接接收了 $C_3,C_4,C_5$ 的信息。所谓“高分辨率层拥有强语义”,说的正是这条递归的信息路径,而不是上采样本身产生了新语义。

同样也不要把它机械地理解成“浅层只提供细节、深层只提供类别”。所有分支传递的都是学习到的特征向量;“定位”和“语义”只是对它们统计倾向的解释,最终哪些通道被保留、怎样相加,都由训练目标共同决定。

4.8 把公式对应到 PyTorch 前向过程

忽略模块初始化,标准 FPN 的 forward 基本就是公式的逐行翻译:

def forward(self, c2, c3, c4, c5):
    # lateral2...5 都是 1×1 卷积,输出统一为 256 通道
    m5 = self.lateral5(c5)
    m4 = self.lateral4(c4) + F.interpolate(
        m5, size=c4.shape[-2:], mode="nearest"
    )
    m3 = self.lateral3(c3) + F.interpolate(
        m4, size=c3.shape[-2:], mode="nearest"
    )
    m2 = self.lateral2(c2) + F.interpolate(
        m3, size=c2.shape[-2:], mode="nearest"
    )

    # output2...5 是带 padding=1 的 3×3 卷积,不改变 H、W
    return {
        "p2": self.output2(m2),
        "p3": self.output3(m3),
        "p4": self.output4(m4),
        "p5": self.output5(m5),
    }

这段代码里最容易写错的并不是卷积,而是上采样尺寸。使用下一层 lateral feature 的 shape[-2:] 作为目标,比连续假设恰好放大两倍更能适应奇数尺寸输入。

5. 金字塔输出怎样解决大小目标

FPN 让不同大小的目标在不同层上接受预测:

  • $P_2$ 分辨率最高,适合小目标和精细边界;
  • $P_3$、$P_4$ 处理逐渐变大的目标;
  • $P_5$ 感受野最大,更适合大目标;
  • RPN 中还可以从 $P_5$ 下采样得到 $P_6$,覆盖更大的 anchor。

在原论文的 RPN 实验中,不同 pyramid level 分别使用 32、64、128、256、512 像素尺度的 anchor,预测头参数在各层共享。共享参数能够工作,本身也说明 FPN 把不同层拉到了较接近的语义空间。

5.1 RPN:每一层在自己的网格上寻找候选框

在 Faster R-CNN + FPN 中,RPN 会分别扫过 $P_2$ 到 $P_6$。每个位置都预测两件事:这里有没有目标,以及 anchor 应怎样平移和缩放。小 anchor 放在稠密的高分辨率层,大 anchor 放在稀疏的低分辨率层:

金字塔层 stride 论文中的 anchor 尺度
$P_2$ 4 32 像素
$P_3$ 8 64 像素
$P_4$ 16 128 像素
$P_5$ 32 256 像素
$P_6$ 64 512 像素

这里的关键不是“每层只认识一种尺寸”,而是让目标在负责它的特征图上占据合理数量的网格。20×20 的小目标放到 stride 32 层,宽高都不足一个网格;放到 stride 4 层,大约还能覆盖 5×5 个位置,结构信息明显更完整。

5.2 RoI Head:根据候选框面积选择读取哪一层

在 Fast R-CNN 中,宽为 $w$、高为 $h$ 的 RoI 会按照面积分配到合适层级:

\[k=\left\lfloor k_0+\log_2\left(\frac{\sqrt{wh}}{224}\right)\right\rfloor\]

直觉上,目标缩小一半,就转到分辨率高一倍的金字塔层。这样每个目标在对应特征图上都能占据比较合适的面积。

层级确定后,RoIAlign 只从对应的 $P_k$ 提取固定尺寸特征,再交给分类和边框回归 head。也就是说,FPN 不负责决定最终类别;它负责给每个尺寸的目标准备合适的“观察底图”。

5.3 单阶段检测器:直接在每层做密集预测

RetinaNet、FCOS 这类单阶段检测器不先生成 RoI,而是直接在每个金字塔层的所有位置预测分类和边框。不同实现会用 anchor 尺度或回归距离区间把目标分配到不同层,但原则相同:

高分辨率层用更多网格描述小目标,低分辨率层用更大感受野描述大目标。

这也解释了 FPN 为什么必须保留一整组 $P_i$,而不是最终只输出分辨率最高的 $P_2$。

6. 这项工作的贡献和边界

FPN 论文并不是第一个使用多尺度、上采样或 skip connection 的工作。它真正把几件事组合成了一个简洁而通用的方案:

  • 计算复用:只输入一个图像尺度,复用 backbone 内部天然产生的层级;
  • 语义回流:通过 top-down pathway,把深层语义逐级传播到高分辨率层;
  • 定位补充:通过 lateral connection,保留浅层精确的空间结构;
  • 多层预测:不是只恢复一张精细特征图,而是在所有 pyramid level 上分别服务不同尺度目标;
  • 接口统一:各层使用相同通道数,使检测头可以共享。

原论文在强单尺度 Faster R-CNN baseline 上报告:RPN 的 AR 提升 8.0 个点,小目标 AR 提升 12.9 个点,目标检测的 COCO AP 提升 2.3 个点。这些消融结果说明,收益不只是来自“多放几层特征”,top-down 的语义增强尤其重要。

FPN 也不是凭空恢复细节。若一个极小目标在 backbone 的浅层就已经不可分辨,后续融合无法创造不存在的信息;上采样本身也只是扩大网格,不会自动增加真实分辨率。FPN 的能力来自保留尚未丢失的高分辨率线索,并用深层语义解释这些线索

经典 FPN 还有几个明确的局限:

  1. 信息主要单向传播:语义从 $P_5$ 向 $P_2$ 传播,但低层定位信息没有一条同样直接的路径重新回到高层;
  2. 融合默认等权:$L_i+\operatorname{Up}(M_{i+1})$ 没有显式学习两条分支各占多大权重;
  3. 跨层对齐很粗:最近邻上采样后直接相加,默认同一网格坐标能够自然对应;
  4. 无法挽救已消失的小目标:如果早期下采样已经抹掉目标,后面的 top-down pathway 只能传语义,不能重建原始证据。

后续工作大多围绕这些边界继续改造:PANet/PAFPN 增加 bottom-up 路径增强,让定位信息更快返回深层;BiFPN 使用双向连接和可学习融合权重,并允许多次堆叠;NAS-FPN 则通过架构搜索寻找更复杂的跨尺度连接。它们改变的是“怎样融合”,而 FPN 奠定的 backbone→neck→多尺度 head 接口仍然保留下来。

8. SAM 2 中的 FPN 是什么样的

SAM 2 的图像编码器可以概括为:

图像 → Hiera trunk → FpnNeck → 图像特征 / 高分辨率特征

Hiera 是一个分层视觉 Transformer。与保持单一 patch 分辨率的普通 ViT 不同,它像 CNN 一样分 stage 降低空间分辨率、增加通道,因此天然输出 stride 4、8、16、32 的四级特征。这正好给 FPN neck 提供了多尺度输入。

以 SAM 2.1 的 Hiera-L 和 1024×1024 输入为例,trunk 送给 neck 的四层大致是:

Hiera 输出 原始通道 空间尺寸 stride
Stage 1 144 256×256 4
Stage 2 288 128×128 8
Stage 3 576 64×64 16
Stage 4 1152 32×32 32

这和 ResNet 的 $C_2$ 到 $C_5$ 扮演相似角色,只是特征提取单元从卷积 block 换成了分层 Transformer block。FpnNeck 首先用四个独立的 1×1 卷积把 144、288、576、1152 通道都投影为 d_model=256

但 SAM 2 没有照搬标准 FPN 的 P2–P5 全层 top-down 融合,也没有在每个尺度上放检测头。论文和官方配置显示,它按下列方式使用这些特征:

  1. Hiera Stage 4 的 stride 32 深层特征先投影到 256 通道;
  2. 将它上采样 2 倍,与 Stage 3 的 stride 16 横向特征相加;
  3. 得到的 stride 16、256 通道特征成为主要 image embedding,进入 memory attention;
  4. Stage 1 和 Stage 2 的 stride 4、8 特征只做横向通道投影,不接收 top-down 融合;
  5. 这两张高分辨率特征图作为 skip features 加入 mask decoder 的上采样层,帮助恢复边界细节;
  6. stride 32 的输出完成融合后被丢弃,不作为最终 backbone_fpn 输出。

因此它真正的 top-down 融合只有一跳:

\[F_{16}=\operatorname{Conv}_{1\times1}(C_{16})+ \operatorname{Up}_2\left(\operatorname{Conv}_{1\times1}(C_{32})\right)\]

而两个高分辨率旁路是:

\[F_8=\operatorname{Conv}_{1\times1}(C_8),\qquad F_4=\operatorname{Conv}_{1\times1}(C_4)\]

它们没有继续与 $F_{16}$ 相加。这个区别非常重要:如果只看类名 FpnNeck,很容易误以为 stride 32 的语义会像经典 FPN 一样一路传到 stride 4;官方循环中的重置逻辑明确阻止了这件事。

SAM 2 的 Hiera 产生 stride 4、8、16、32 四级特征;FPN neck 只把 stride 32 上采样并融合到 stride 16,主特征进入 memory attention,而 stride 4 和 8 经投影后直接送给 mask decoder 补充高分辨率细节
SAM 2 的 FPN 更像任务定制的 neck:stride 16/32 负责形成语义强的主图像特征,stride 4/8 不参与记忆注意力,而是作为高分辨率旁路帮助 mask decoder 还原精细边界。

9. 对照 SAM 2 官方代码

在 SAM 2.1 Hiera-L 配置中,可以看到:

neck:
  d_model: 256
  backbone_channel_list: [1152, 576, 288, 144]
  fpn_top_down_levels: [2, 3]
  fpn_interp_model: nearest

image_encoder:
  scalp: 1

backbone_channel_list 按 top-down 处理顺序列出 Hiera-L 四个 stage 的通道数。每一级先用 1×1 卷积变成 d_model=256fpn_top_down_levels: [2, 3] 表示只有对应 stride 16 和 32 的层参与 top-down 传播;stride 4 和 8 只保留各自的 lateral features。

这里的 level 编号仍按 Hiera 正向输出列表计算:0、1、2、3 分别对应 stride 4、8、16、32;代码只是通过 reversed(range(len(xs))) 从 level 3 倒着处理。第一次处理 level 3 时还没有 prev,所以只是建立 stride 32 的起点;处理 level 2 时才真正发生一次“上采样 + 相加”。

官方 FpnNeck.forward 的核心逻辑可以简化为:

prev = None
for level in reversed(levels):          # 从低分辨率走向高分辨率
    lateral = conv1x1(backbone[level])  # 统一到 d_model

    if level in top_down_levels and prev is not None:
        prev = lateral + interpolate(prev, scale_factor=2)
    else:
        prev = lateral

    output[level] = prev

这个 else 很关键。当循环走到 stride 8 和 stride 4 时,prev 会重新变成当前层的 lateral feature,而不是继续把 stride 16 的语义一路向上传。因此,SAM 2 的高分辨率旁路并不是经典 FPN 那种“所有层都被深层语义增强”的 P2、P3。

把循环按实际形状展开,会更直观:

level 3 / stride 32:
  1152×32×32 --1×1--> 256×32×32
  prev 为空,不融合

level 2 / stride 16:
  576×64×64  --1×1--> 256×64×64
  prev: 256×32×32 --nearest up--> 256×64×64
  两者相加 → 256×64×64 主 image embedding

level 1 / stride 8:
  288×128×128 --1×1--> 256×128×128
  不在继续融合的路径上,prev 重置为当前 lateral feature

level 0 / stride 4:
  144×256×256 --1×1--> 256×256×256
  同样只保留当前 lateral feature

配置中的 scalp: 1 会删除最低分辨率的最后一级输出。对于 1024×1024 输入,最终主要保留:

stride 4 : [B, 256, 256, 256] → mask decoder 高分辨率旁路
stride 8 : [B, 256, 128, 128] → mask decoder 高分辨率旁路
stride 16: [B, 256,  64,  64] → image embedding / memory attention

SAM 2 的 FpnNeck 还与原始 FPN 有两个实现差异:代码注释明确写着去掉了各层最后的 output convolution,也就是经典 FPN 的 3×3 平滑卷积;SAM 2.1 配置使用最近邻插值,并为每个输出特征生成正弦位置编码。

随后,SAM2Base.forward_image 会把 stride 4 和 stride 8 两层预先投影成 mask decoder 需要的通道数,并把它们作为 high_res_features 保存;stride 16 则成为视觉主干特征,展平后进入 memory attention,与当前提示和历史视频记忆交互。也就是说,neck 输出的三层并不是交给三个相同的预测头,而是被分流到两个完全不同的计算预算中。

10. 为什么 SAM 2 要这样改

SAM 2 的目标不是在四个尺度上分别检测大小目标,而是同时满足两种下游需求:

  • 记忆与语义建模需要紧凑特征:stride 16 的 64×64 特征足够保留空间结构,又能控制视频 memory attention 的 token 数量;
  • 掩码边界需要高分辨率细节:stride 4 和 8 特征直接送入 mask decoder,可以在上采样时补回轮廓、细小结构和局部定位信息。

如果把 stride 4 特征全部送进视频记忆注意力,token 数会比 stride 16 多 16 倍,时间和显存代价会非常高;如果只保留 stride 16,又容易让最终掩码边缘过粗。SAM 2 的设计因此把“深层语义与时序记忆”和“高分辨率空间细节”分给不同路径处理。

从这个角度看,SAM 2 使用 FPN 思想的地方,不只是出现了上采样和横向相加,而是延续了 FPN 最核心的原则:不同深度的特征各有所长,neck 的任务是按照下游需求重新分配语义与分辨率。

11. 最后总结

现在再看 FPN,我会把它概括成下面几句话:

  1. backbone 天然产生多尺度特征,但浅层定位准、语义弱,深层语义强、定位粗;
  2. FPN 用 top-down pathway 传递深层语义,用 lateral connection 保留当前尺度细节;
  3. 1×1 卷积负责通道对齐,2× 上采样负责空间对齐,相加完成融合,原始 FPN 再用 3×3 卷积平滑;
  4. 它的贡献不是“发明金字塔”,而是以很小额外成本构造语义一致、可分别预测的多尺度特征;
  5. SAM 2 只在 stride 32→16 之间做 top-down 融合,stride 4/8 则作为 mask decoder 的高分辨率旁路;
  6. 所以 SAM 2 的 FpnNeck 是对 FPN 原理的任务化改造,而不是经典检测 FPN 的原样复制。

参考资料