Ferret: Refer and Ground Anything Anywhere at Any Granularity

Ferret 的输入指代与输出定位示例
Fig. 1 — Ferret 把空间理解的两半合到一个模型里。左(输入指代 Referring):用户用点/框/任意自由形状圈出 region0,问"这块区域里是什么动物"。右(输出定位 Grounding):Ferret 回答时把每个可定位名词后面紧跟坐标框 [box0]。下例还能基于圈出的食物区域做带定位的多步推理(turkey/cheese/knife 各自标框)。(论文 Fig. 1)

上一篇 LISA 解决的是输出:文本 → 掩码("想哪打哪")。Ferret 解决的是对偶的输入:区域 → 语义("圈哪问哪")。你终于可以在图上画一笔,问"这玩意儿是什么、怎么用"。

1. 出发点 (Motivation)

空间理解有两个互为对偶的能力:

论文的核心观察:这两件事需要的是同一种知识——空间与语义的对齐。可现有工作几乎都把它们分开学(指代分割一套、检测定位一套)。而人类能从一个任务学到的东西无缝迁移到另一个,还能把指代/定位自然糅进日常对话和推理里。

于是论文盯住三个问题:

  1. 能否在一个框架里统一指代和定位,且让两者互相增益?
  2. 怎么表示人类真正会用的"各种形状"的区域?——不只是点和框,还有涂鸦、划线、多边形、自由形状。
  3. 怎么让指代/定位是开放词表、可指令跟随、且鲁棒的?

痛点很具体:之前的 Kosmos-2、Shikra、GPT4RoI、PVIT 等只支持框(Shikra 多了点),都没法处理自由形状。而框有个根本缺陷——两个形状迥异的物体可能共享几乎相同的框:

论文 Fig. 2 的例子:一把手枪和一把叠放,二者的外接框几乎完全重合。只给框,模型根本分不清你指的是哪个。必须有一种能贴合任意形状的区域表示。

Ferret 就是冲着这三个问题来的,并且——按论文自己的说法——是第一个能处理自由形状区域输入的 MLLM

2. 方法 (Method)

Ferret 架构:混合区域表示 + 空间感知视觉采样器 + LLM
Fig. 2 — Ferret 架构。:混合区域表示 = 区域名 + [坐标] + <feature>,对点/框/自由形状统一适用;连续特征由空间感知视觉采样器产生(采样 → KNN 聚邻 → 融合 → 池化的点云式级联)。:图像编码器 + 采样器 + LLM 联合建模图像、文本、区域三种信息;输出时在名词后直接生成坐标 [80, 590, 450, 920] 实现定位。除图像编码器冻结外,其余全部可训。(论文 Fig. 3)

2.1 直觉:坐标负责"在哪",连续特征负责"长啥样"

只用坐标表示区域,就像用"经纬度"描述一个物体——能定位,但说不清形状(回到手枪/刀的例子)。只用连续特征(像 GPT4RoI 的 RoIAlign),又丢了精确的位置数值,且只能处理矩形框。

Ferret 的答案是两者都要——混合区域表示 (hybrid region representation):

\[\langle \text{region name} \rangle \;\; \langle \text{coordinates} \rangle \;\; \langle \text{SPE} \rangle\]

—— 翻译:例如 "a cat [100, 50, 200, 300] <SPE>"。前面是区域名(不知道就写 "region"/"area"),中间是离散坐标(负责"在哪"),最后一个占位 token <SPE> 会被一段连续视觉特征替换(负责"长啥样")。这样区域就能像普通词一样混进句子里。

两个关键设计:

2.2 空间感知视觉采样器:把"任意形状"当点云处理

怎么从一个任意形状的区域里抽特征?卷积、patch attention 这种网格化操作处理不了不规则形状。论文的灵感来自 3D 点云学习(PointNet++ / PointMLP)——点云本来就是不规则、稀疏度不均的。

给定图像特征图 \(Z \in \mathbb{R}^{H \times W \times C}\) 和区域二值掩码 \(M\),流程是:

  1. 在掩码 \(M\)随机采 \(N=512\) 个正点,每个点的特征用双线性插值得到;
  2. 级联两个 block,每个 block 三步——采样 → 聚集 → 池化:
    • 采样:用最远点采样 (FPS) 选 \(N_r\) 个点,保证覆盖均匀;
    • 聚集:对每个采样点 \(x_i\),找它的 \(k=24\) 个最近邻,按 PointMLP 方式融合邻居特征:
\[h_{ik} = \sigma\big([\,\theta([Z(x_{ik}) - Z(x_i);\, C(x_{ik}) - C(x_i)]);\; Z(x_i);\; C(x_i)\,]\big)\]

—— 翻译:对采样点 x_i 和它的某个邻居 x_ik,把"邻居相对它的特征差 Z(x_ik)−Z(x_i)"和"相对坐标差 C(x_ik)−C(x_i)"拼起来过一层 θ,再和中心点自身的特征 Z(x_i)、坐标 C(x_i) 拼起来过一层 σ。说白了:用"邻居相对中心的位置和特征差异"来刻画这块局部的几何。

\[h_i = \max_{k:\,(x_{ik}) \in \text{KNN of } x_i} h_{ik}\]

—— 翻译:把一个采样点周围 k 个邻居的特征汇成一个(论文公式写的是 max 池化)。两个 block 跑完,512 个点压成 32 个点,每个点都吸收了局部邻域的几何信息。

  1. 最后把 32 个点的特征 flatten 成一个向量,投影到 LLM 的 embedding 维度,替换掉 <SPE> 占位 token

这套采样器的代码就是标准的点云级联——FPS + KNN + 融合 + 池化:

repo/ferret/model/ferret_arch.py:L276-L317 — 空间感知视觉采样器的两段级联(对应公式 1、2)

for stage_i in range(len(self.num_sub_point)):
    cur_num_sub_point = self.num_sub_point[stage_i]
    cur_num_neighbor = self.num_neighbor[stage_i]
    fps_idx = farthest_point_sample(all_points, cur_num_sub_point).long()   # 采样
    new_points = index_points(all_points, fps_idx)
    new_points_fea = index_points(all_points_fea, fps_idx)
    idx = knn_point(cur_num_neighbor, all_points, new_points)               # 聚集邻居
    grouped_points = index_points(all_points, idx)
    grouped_points_fea = index_points(all_points_fea, idx)
    local_points_fea = torch.cat([grouped_points_fea, grouped_points], dim=-1)
    anchor_points_fea = torch.cat([new_points_fea, new_points], dim=-1).unsqueeze(-2)
    diff_points_fea = local_points_fea - anchor_points_fea                  # 相对差(公式1的核心)
    diff_points_fea = self.diff_projector_list[stage_i](diff_points_fea)
    gather_points_fea = torch.cat([diff_points_fea, anchor_points_fea.repeat(1,1,cur_num_neighbor,1)], dim=-1)
    ...
    gather_points_fea = self.pooler_list[stage_i](gather_points_fea)        # 池化(公式2)
    all_points, all_points_fea = new_points, gather_points_fea
x = all_points_fea.flatten(1, -1)
all_region_fea = self.dim_projector(self.flatten_projector(x))             # → LLM embedding 维度

2.3 输出端:名词后直接吐坐标

输入侧解决了,输出侧的定位更简单:Ferret 在生成文本时,在每个可定位名词后面直接生成框坐标文本,比如 "There is a dog [100, 150, 300, 200] in the figure."。模型隐式地学会"什么是可定位的、它在哪"。注意这里 Ferret 输出的是框坐标,不是 LISA 那种像素掩码——这正是后续 GLaMM / Osprey 要补的"输出掩码"那一步。

2.4 GRIT 数据集与 Ferret-Bench

GRIT 数据集的五类数据:物体、关系、区域、细节+推理、鲁棒性
Fig. 3 — GRIT 数据集概览。三大来源、五类数据:物体 / 关系 / 区域描述(前三行,由 Visual Genome、RefCOCO、Flickr30k 等公开数据用模板转成指令);细节+推理(第四行,GPT 生成);鲁棒性(末行,空间负样本挖掘——问图里有没有其实不存在的"cat",逼模型学会说"没有但有 dog [box0]")。每类都覆盖"区域进-文本出 / 文本进-区域出 / 图文区域混合"三种格式。(论文 Fig. 4)

3. 结论 (Key findings)

① 在"需要指代+推理"的对话任务上大幅领先。 Ferret-Bench(GPT-4 评判,越高越好):

模型 指代描述 指代推理 对话中定位 平均
LLaVA 41.4 31.7 28.8 34.0
Kosmos-2 51.8 33.7 48.4 44.6
Shikra-7B 46.0 41.6 50.1 45.9
Ferret-7B 68.7 67.3 57.5 64.5
Ferret-13B 70.6 68.7 59.7 66.3

比最强基线 Shikra 平均高 +20.4%(64.5 vs 45.9),指代推理一项几乎翻倍。

② 指代和定位真的互相增益(验证 Q1)。 消融(Table 8):去掉定位数据,指代精度从 Point 67.9→65.4、Box 79.4→75.6;去掉指代数据,定位 Flickr30k 80.4→79.8。两半合起来训,两边都更好——这是论文最重要的立论。

③ 采样器比 SEEM 的更适合自由形状(验证 Q2)。 Table 9:Ferret 的空间感知采样器在自由形状指代上 69.8,优于 SEEM 的采样器 68.9;点、框上也更好。

④ 显著缓解物体幻觉。 POPE 幻觉基准 accuracy:Ferret 90.24 vs Shikra 86.90 vs LLaVA 50.37。空间负样本挖掘起了作用——模型更敢说"图里没有"。

4. 实现细节 (Implementation notes)

把论文落到 apple/ml-ferret 代码,有几处只读论文看不到:

① 占位 token 论文叫 <SPE>,代码叫 <region_fea> 和 LISA 的 <SEG>/[SEG] 一模一样的对不上:

repo/ferret/model/ferret_arch.py:L24, L632-L633 — 占位 token 的真名与注册

DEFAULT_REGION_FEA_TOKEN = "<region_fea>"
...
num_region_fea_tokens = tokenizer.add_tokens([DEFAULT_REGION_FEA_TOKEN], special_tokens=True)
self.config.im_region_fea_token = tokenizer.convert_tokens_to_ids([DEFAULT_REGION_FEA_TOKEN])[0]

② "不引入新词表"这句话只对坐标成立,对连续特征不成立。 论文第 1 节脚注说 "no additional vocabulary or position encoders introduced"。严格讲这只对坐标为真(坐标是纯数字文本 token);上面 L632 明明给词表加了一个 <region_fea> 特殊 token。准确的说法是:坐标零新词表,但连续特征路径加了一个占位 token

③ 区域特征注入 = 输入侧的 <SEG> 镜像。 这是和 LISA 最值得对照的一行。LISA 在输出时取 <SEG> 的隐藏向量喂给 SAM;Ferret 在输入时,把采样器算出的区域特征写回 <region_fea> 占位 token 的 embedding:

repo/ferret/model/ferret_arch.py:L567-L572 — 用区域特征替换 <region_fea> 占位 token 的 embedding

if region_flag and region_features[batch_idx] is not None:
    region_embs = torch.zeros_like(text_input_embeds)
    region_replace_mask = (cur_input_ids == self.config.im_region_fea_token)
    region_embs[region_replace_mask] = region_features[batch_idx].to(text_input_embeds.dtype)
    text_input_embeds = text_input_embeds * (~region_replace_mask).to(text_input_embeds.dtype)[:, None] + region_embs

一句话对照:LISA 是"LLM 吐一个 token,解码成掩码";Ferret 是"把区域编码成一个 token,塞进 LLM"。 同一个"token 当桥"的思想,方向相反。

④ 论文公式 2 写 max 池化,代码默认却是 mean 池化。 GeoRegionSamplerpooler_mode 默认 'mean'(用 AvgPool1d),而论文公式 (2) 写的是 \(\max\)。这是一处 paper-vs-code 不一致——实际训练用的配置可能覆盖成别的,但默认实现与公式不符,复现时要留意:

repo/ferret/model/ferret_arch.py:L185, L202-L205 — 池化模式默认 mean,与论文公式 2 的 max 不符

def __init__(self, ..., pooler_mode='mean'):     # 默认 mean,非论文的 max
    ...
    if pooler_mode == 'mean':
        self.pooler_list.append(nn.AvgPool1d(kernel_size=num_neighbor[ii]))
    elif pooler_mode == 'max':
        self.pooler_list.append(nn.AdaptiveMaxPool1d(output_size=1))

⑤ 最远点采样 (FPS) 是纯 PyTorch 串行实现。 没有用 CUDA kernel,就是一个 for 循环逐点迭代选最远点——区域多、点多时会偏慢:

repo/ferret/model/ferret_arch.py:L73-L92 — FPS 的串行实现

def farthest_point_sample(xyz, npoint):
    B, N, C = xyz.shape
    centroids = torch.zeros(B, npoint, dtype=torch.long).to(device)
    distance = torch.ones(B, N).to(device) * 1e10
    farthest = torch.randint(0, N, (B,), dtype=torch.long).to(device)
    for i in range(npoint):
        centroids[:, i] = farthest
        centroid = xyz[batch_indices, farthest, :].view(B, 1, 2)
        dist = torch.sum((xyz - centroid) ** 2, -1)
        distance = torch.min(distance, dist)
        farthest = torch.max(distance, -1)[1]
    return centroids

⑥ 除图像编码器外全部可训。 CLIP-ViT-L/14 冻结,采样器、投影层、LLM(Vicuna)全量微调——注意这里不是 LISA 那种 LoRA,Ferret 是全量微调 LLM。

5. 批判性总结 (Critical assessment)

Strengths

Limitations / open questions

When to use / not use

后续工作谱系(输入侧 region grounding 的对偶线)

这是本文重点。把区域"喂进" MLLM,历史上有两条机制,Ferret 是它们的合流点;之后又长出 Apple 自己的高分辨率/UI 分支,以及"输入区域+输出掩码"的双向线。下表 arxiv id 均逐条核验。

两种区域表示机制,以及 Ferret 的位置

机制 区域 = 什么 代表
(a) 坐标即文本/token 一串数字写进文本流 Shikra(2306.15195,纯十进制文本)、Kosmos-2(2306.14824,离散 location token)
(b) 连续区域特征 一个池化/采样出的特征向量 GPT4RoI(2307.03601,RoIAlign)、PVIT(2308.13437,区域编码器)、RegionGPT(2403.02330,掩码池化)
(a)+(b) 混合 坐标 ∥ 连续特征,同一个区域 token 两者都给 Ferret——且唯一支持自由形状输入
(c) 视觉提示叠加 直接在像素上画框/箭头/涂鸦 ViP-LLaVA(2312.00784)
(d) 提议+索引 区域提议器出框,LLM 选 index Groma(2404.13013)、ChatRex(2411.18363)

A. Apple 直系

B. 输入区域 + 输出掩码(双向 grounding)——补 Ferret"只输出框"的短板

C. 同期输入侧家族(都只支持框/点,Ferret 用自由形状区分自己):GPT4RoI(RoIAlign 框)、Shikra(坐标文本,点+框)、Kosmos-2(location token,框)、PVIT(区域编码器,框)、ChatSpot(2307.09474,点/框提示)、Groma(提议+region token)、SPHINX(2311.07575,多任务混合)。

一句话趋势(2023→2026):区域表示从"坐标即文本"(Shikra/Kosmos-2,精度有天花板)→"连续区域特征"(GPT4RoI/PVIT,只到框)→ Ferret 的混合 + 自由形状(两条线合流)→ 输出端从框升级到掩码(Osprey/GLaMM/Sa2VA)、输入端升级到高分辨率与 UI(Ferret-v2/Ferret-UI)。到 2025,定位机制基本收敛到两个赢家:掩码 token + 基座分割器(LISA→GLaMM→Sa2VA)和提议+索引(Groma/ChatRex);而 Ferret 的"混合区域 token"则被直接继承进任意分辨率密集感知任意形状输入

Further reading

讨论 / Comments

评论托管在本仓库的 GitHub Discussions, 需 GitHub 账号。