3D-VLA 方法原理详解

3D-VLA 方法原理详解

1. 方法想解决什么问题

传统视觉语言动作模型常见做法是:

  1. 输入当前图像和语言指令。
  2. 模型直接输出动作。
  3. 机器人按动作执行。

这种方式有一个核心缺陷:模型容易只在二维图像空间里"看起来懂了任务",但并没有显式建立三维物理世界的状态变化。例如"把薯片袋拿出来""关上抽屉""把钱放进保险柜"这些任务,不只是识别物体,还需要理解:

  • 物体在三维空间中的位置。
  • 当前状态和目标状态有什么差别。
  • 哪些物体会移动,哪些环境结构要保持。
  • 动作执行后世界应该变成什么样。

3D-VLA 的核心思想是:不要让模型直接从当前观察跳到动作,而是先让模型具备"想象目标状态"的能力。也就是先回答:

如果这个指令完成了,世界应该长什么样?

这个"目标世界"可以是目标 RGB 图像、目标 RGB-D 图像,也可以是目标点云。然后再基于当前状态和目标状态生成机器人动作。

因此 3D-VLA 的方法可以概括为:

用 3D 场景表示作为语言模型的输入,让语言模型在文本 token、3D token、目标生成 token 和动作 token 之间推理;当它需要预测未来状态时,调用 embodied diffusion model 生成目标图像或目标点云;最终将当前状态、目标状态和任务意图转化为机器人动作 token。

2. 整体框架

3D-VLA 由三类能力组成:

  1. 3D Vision-Language-Action Model

    这是中枢模型。它基于 3D-LLM / BLIP2 / Flan-T5 的思想,把三维场景特征转成语言模型能读的 token embedding,并输出自然语言、目标生成请求或动作 token。

  2. Embodied Diffusion Models

    这是"目标想象器"。输入当前状态和语言指令,生成任务完成后的目标状态。仓库中包含两种:

    • Goal Image LDM:生成目标 RGB 或 RGB-D 图像。
    • Goal Point Cloud Diffusion Model:生成目标点云。
  3. Robot Control Tokenization

    这是"动作接口"。机器人动作被离散成语言模型能输出的特殊 token,例如位置 token、旋转 token、夹爪开合 token 和动作分隔 token。语言模型通过生成 token 序列来表示一串机器人控制命令。

整体数据流如下:

text 复制代码
当前多视角/RGB-D观察
        |
        v
3D特征提取与点云/体素表示
        |
        v
Q-Former压缩为少量3D scene tokens
        |
        v
3D Vision-Language-Action LLM
        |
        +--> 输出目标生成请求 token
        |         |
        |         v
        |   Goal Image / Point Cloud Diffusion Model
        |         |
        |         v
        |   生成目标图像或目标点云
        |
        +--> 输出动作 token
                  |
                  v
            解码成机器人动作

3. 输入表示:如何把 3D 世界喂给语言模型

语言模型本身只能处理 token embedding,不能直接理解点云或三维坐标。因此 3D-VLA 需要先把环境观察变成"语言模型可消费"的形式。

3.1 原始观察

方法层面支持以下输入:

  • 单视角或多视角 RGB 图像。
  • 深度图或 RGB-D 图像。
  • 由 RGB-D / 多视角重建得到的点云。
  • 每个点或体素对应的视觉语义特征。
  • 用户语言指令。

仓库中的 3D-VQA 数据格式可以抽象为:

text 复制代码
sample = {
    pc_feat: [T, N, 1408],  # 每个时间/状态的N个3D点特征
    pc:      [T, N, 3],     # 每个点的三维坐标或离散坐标
    text:    instruction_or_question,
    answer:  target_text_or_action_tokens
}

其中:

  • T 表示一个样本中可以包含多个 3D 状态,仓库数据集中常见最大值是 2,例如当前状态和目标状态。
  • N 是采样后的点数。
  • 1408 是每个点/体素的视觉特征维度。

这些 3D 特征通常不是语言模型直接从原图端到端学出来的,而是由外部视觉/3D工具预处理得到。README 中也说明数据处理借助了 SAM、ConceptFusion、3D-CLR 等资源。

3.2 坐标和视觉特征一起使用

只给每个点的语义特征不够,因为机器人任务强依赖空间位置。例如"底层抽屉""杯子旁边""把钱放进保险柜"都需要坐标。

所以 3D-VLA 同时使用:

  • 点/体素的视觉特征 pc_feat
  • 点/体素的三维位置 pc

源码确认:BLIP2/T5 模型中会对 3D 坐标加入位置编码。具体做法是把 x/y/z 三个坐标 clamp 到 0 到 255 的范围,分别查 positional embedding,再拼接成接近 1408 维的坐标编码,加到点特征上。模型还向 tokenizer 加入了 <loc0><loc255> 这 256 个位置 token,说明方法中也把位置作为可离散表达的语言符号。

可以把它理解为:

text 复制代码
每个3D点的最终输入特征 = 视觉语义特征 + 位置编码

这样语言模型接收到的不是"一个物体类别",而是"某个空间位置上的语义实体"。

4. Q-Former:把大量 3D 点压缩成少量 scene tokens

点云或体素特征数量很多,不能直接全部塞进大语言模型。3D-VLA 使用 BLIP2 风格的 Q-Former 做压缩。

Q-Former 的输入是:

text 复制代码
pc_feat_with_position: [B, T, N, 1408]

Q-Former 内部有固定数量的 learnable query tokens。仓库中默认是 32 个 query token。每个 query token 通过 cross-attention 从所有 3D 点特征中抽取信息,最终每个 3D 状态被压缩成 32 个语言模型尺度的 embedding。

也就是说:

text 复制代码
N个3D点/体素特征  -->  32个scene embeddings

这些 scene embeddings 再经过线性投影,变成 Flan-T5 可以接收的 hidden size。

伪代码如下:

python 复制代码
def encode_3d_scene(pc_feat, pc_coord):
    # pc_feat: [B, T, N, 1408]
    # pc_coord: [B, T, N, 3]

    pos_emb = positional_encoding(clamp(pc_coord, 0, 255))
    point_tokens = pc_feat + 0.1 * pos_emb

    scene_tokens = []
    for each_state in range(T):
        query = learnable_query_tokens(num=32)
        compressed = q_former_cross_attention(
            query=query,
            key_value=point_tokens[:, each_state]
        )
        scene_tokens.append(linear_project_to_t5(compressed))

    return scene_tokens  # [B, T, 32, t5_hidden_dim]

关键点是:Q-Former 不是把点云转成文字,而是把 3D 场景转成一组软 token embedding。这些 token 对语言模型来说就像"插入到句子里的视觉上下文"。

5. <scene> token:把 3D embedding 插进语言上下文

3D-VLA 不是简单地把 scene embeddings 接在文本前面,而是使用特殊 token 作为插入位置。

输入文本中会出现类似:

text 复制代码
The initial scene is <scene></scene>. Find some snacks for me.

模型在 tokenizer 层面加入了:

text 复制代码
<scene>, </scene>

当输入文本 token 中遇到 <scene> 时,模型会在这个位置后面插入对应的 32 个 3D scene embeddings。

如果一个样本有多个 3D 状态,就可以有多个 <scene> 占位符,每个占位符对应一个 3D 特征块。

伪代码如下:

python 复制代码
def insert_scene_embeddings(text_tokens, scene_embeddings):
    # text_tokens: tokenized text containing <scene>
    # scene_embeddings: [num_scenes, 32, hidden_dim]

    output_embeddings = []
    scene_index = 0

    for token in text_tokens:
        output_embeddings.append(embed_text_token(token))

        if token == "<scene>":
            output_embeddings.extend(scene_embeddings[scene_index])
            scene_index += 1

    return output_embeddings

这种设计的好处是:语言可以显式指代 3D 场景。模型不需要猜"这批视觉 embedding 是什么",因为输入句子已经告诉它:这里是当前场景、这里是目标场景、这里是执行环境。

6. Interaction tokens:把语言、目标生成和动作统一成 token 接口

3D-VLA 的一个重要设计是把不同模态的交互都包装成特殊 token。

仓库中加入的特殊 token 包括:

text 复制代码
位置:
<loc0> ... <loc255>

3D场景:
<scene> </scene>

物体:
<obj> </obj>

目标图像:
<image> </image>

目标点云:
<pcd> </pcd>

动作:
<aloc0> ... <aloc255>
<arot0> ... <arot255>
<gripper0> <gripper1>
<ACT_SEP>

这使得语言模型可以输出一种"混合程序":

text 复制代码
Sure, I should <pcd> pick up <obj> the chip bag </obj> <loc...> </pcd>

含义不是普通自然语言,而是:

  • <pcd> 表示接下来要生成目标点云。
  • <obj>...</obj> 标记目标物体。
  • <loc...> 标记目标相关空间位置。
  • </pcd> 表示目标点云请求结束。

对于机器人控制,输出可能类似:

text 复制代码
<aloc12><aloc98><aloc43><arot7><arot200><arot15><gripper1><ACT_SEP>...

这表示一串离散化后的动作参数。模型不是直接输出浮点控制量,而是像生成文本一样生成动作 token,再由后处理把 token 解码回连续动作空间。

为什么要用 token 统一接口

这样做有三个好处:

  1. 统一学习目标

    无论是回答问题、请求目标生成、描述物体位置,还是输出动作,本质上都变成 sequence-to-sequence 的 token 预测。

  2. 可组合

    模型可以先生成"我需要一个目标点云",再由扩散模型生成目标状态,之后再基于目标状态生成动作。

  3. 可解释

    中间输出不是黑盒向量,而是带有结构的 token 序列。人可以看出模型正在想象图像、点云,还是直接控制机器人。

7. 3D-VLA LLM 的训练目标

3D-VLA 的 LLM 部分本质上是条件文本生成:

text 复制代码
输入:  3D场景 + 用户指令/问题
输出:  答案、目标生成请求、目标描述或动作token序列

训练时使用标准 teacher forcing:

python 复制代码
def train_3d_vla_llm(sample):
    scene_tokens = encode_3d_scene(sample.pc_feat, sample.pc)
    input_embeddings = insert_scene_embeddings(
        tokenize(sample.text_input),
        scene_tokens
    )

    target_tokens = tokenize(sample.answer)

    logits = t5(
        encoder_inputs=input_embeddings,
        decoder_inputs=target_tokens[:-1]
    )

    loss = cross_entropy(logits, target_tokens[1:])
    update_trainable_parameters(loss)

源码确认的训练细节:

  • 语言模型基于 Flan-T5。
  • T5 主体参数被冻结。
  • T5 的输入/输出 embedding 会训练,因为新增了大量特殊 token。
  • Q-Former 和从 Q-Former 到 T5 hidden size 的投影层承担 3D 对齐。
  • 训练样本的答案字段可以有多个 answer,训练时会展开为多个目标序列。

这种设计不是从零训练一个大模型,而是把预训练语言模型作为推理核心,然后学习如何把 3D 场景嵌入到它能理解的 token 空间里。

8. Goal Image / RGB-D Latent Diffusion Model

Goal Image LDM 用来回答:

给定当前图像和语言指令,任务完成后的图像应该是什么样?

训练样本形式是:

text 复制代码
当前图像 input_image
语言指令 edit_prompt
目标图像 edited_image
可选当前深度 input_depth
可选目标深度 edited_depth

例如:

text 复制代码
input_image:  桌上有瓶子和海绵
edit_prompt:  move water bottle near sponge
edited_image: 瓶子移动到海绵附近后的图像

8.1 它为什么基于 InstructPix2Pix

InstructPix2Pix 的任务是:

text 复制代码
输入原图 + 编辑指令 -> 输出编辑后的图

机器人目标图像生成也可以看成一种编辑:

text 复制代码
当前状态图像 + 任务指令 -> 完成任务后的目标状态图像

所以 3D-VLA 直接使用 Stable Diffusion / InstructPix2Pix 风格的 latent diffusion training。

8.2 RGB 版本的训练过程

RGB 版本使用 VAE 把图像压到 latent 空间:

text 复制代码
目标图像 edited_image -> target_latent
当前图像 input_image -> condition_latent
语言指令 edit_prompt -> text_embedding

训练时对目标 latent 加噪:

text 复制代码
noisy_target_latent = add_noise(target_latent, noise, timestep)

然后把 noisy target latent 和 condition latent 在通道维拼接,送入 UNet:

text 复制代码
UNet输入 = concat(noisy_target_latent, condition_latent)
UNet条件 = text_embedding + timestep
UNet输出 = predicted_noise
loss = MSE(predicted_noise, true_noise)

RGB 图像的 latent 通常是 4 通道,因此:

text 复制代码
noisy目标latent: 4 channels
当前图像condition latent: 4 channels
UNet输入: 8 channels
UNet输出: 4 channels

伪代码如下:

python 复制代码
def train_goal_image_ldm(batch):
    input_image = batch["original_pixel_values"]
    goal_image = batch["edited_pixel_values"]
    instruction = batch["input_ids"]

    goal_latent = vae.encode(goal_image).sample() * vae_scaling
    input_latent = vae.encode(input_image).mode()

    t = sample_random_timestep()
    noise = randn_like(goal_latent)
    noisy_goal = scheduler.add_noise(goal_latent, noise, t)

    text_cond = clip_text_encoder(instruction)
    model_input = concat_channels(noisy_goal, input_latent)

    pred_noise = unet(model_input, t, text_cond)
    loss = mse(pred_noise, noise)

    update_unet(loss)

8.3 RGB-D 版本如何扩展

RGB-D 版本把 RGB 和 depth 分别经过 VAE 编码,然后在 latent 通道上拼接:

text 复制代码
RGB目标latent:   4 channels
Depth目标latent: 4 channels
目标latent合计:  8 channels

RGB当前latent:   4 channels
Depth当前latent: 4 channels
当前condition:   8 channels

UNet输入: 16 channels
UNet输出: 8 channels

源码确认:训练脚本会修改 UNet 的输入卷积和输出卷积,使其支持 RGB-D 的 16 通道输入和 8 通道输出。新增通道部分从预训练权重中尽量复制已有通道,其余初始化为零或新权重。

8.4 推理过程

推理时没有目标图像。模型从随机噪声 latent 开始,逐步去噪:

python 复制代码
def infer_goal_image(input_image, instruction):
    input_latent = vae.encode(input_image).mode()
    text_cond = clip_text_encoder(instruction)

    x = random_latent()

    for t in scheduler.timesteps:
        model_input = concat_channels(x, input_latent)
        pred_noise = unet(model_input, t, text_cond)
        x = scheduler.step(pred_noise, t, x)

    goal_image = vae.decode(x)
    return goal_image

仓库推理默认使用:

  • 50 个 denoising steps。
  • text guidance scale。
  • image guidance scale。
  • 如果是 RGB-D,则分别 decode RGB latent 和 depth latent。

9. Goal Point Cloud Diffusion Model

Goal Point Cloud Diffusion Model 用来回答:

给定当前点云和语言指令,任务完成后的目标点云应该是什么样?

训练样本来自 RLBench,每个样本包含:

text 复制代码
start_pc:    初始点云
end_pc:      目标点云
instruction: 任务文字

每个点通常包含 6 个通道:

text 复制代码
[x, y, z, r, g, b]

9.1 点云预处理

训练时会做几个关键预处理:

  1. 采样固定数量的点。

    仓库训练默认是 2048 点;推理配置中可以用 8192 点。

  2. 对起始点云和目标点云使用同一套采样索引。

    这样第 i 个起始点和第 i 个目标点在训练中形成对应关系,扩散模型学习的是整组点的状态变化。

  3. 归一化坐标。

    把起始点云和目标点云合在一起计算中心和半径,再把坐标归一化到单位球附近。这样不同场景尺度不会让模型训练不稳定。

伪代码:

python 复制代码
def preprocess_pointcloud_pair(start_pc, end_pc, sample_size):
    idx = random_sample_indices(len(start_pc), sample_size)
    start_pc = start_pc[idx]
    end_pc = end_pc[idx]

    xyz_all = concat(start_pc[:, :3], end_pc[:, :3])
    center = mean(xyz_all)
    radius = max_norm(xyz_all - center)

    start_pc[:, :3] = (start_pc[:, :3] - center) / radius
    end_pc[:, :3] = (end_pc[:, :3] - center) / radius

    return start_pc, end_pc

9.2 为什么改造 Point-E

Point-E 原本是文本/图像条件点云生成模型。3D-VLA 需要的不是"从零生成一个物体点云",而是:

text 复制代码
当前点云 + 语言指令 -> 目标点云

所以它把模型输入改成:

text 复制代码
concat(noisy_goal_pc, start_pc)

如果每个点云是 6 通道,那么拼接后是 12 通道:

text 复制代码
noisy_goal_pc: 6 channels
start_pc:      6 channels
model_input:  12 channels
model_output: 6 channels predicted noise

源码确认:GoalPointDiffusionTransformer.modify_layer() 会把输入线性层改为 12 通道,把输出线性层改为 6 通道,并尽量继承 Point-E 预训练权重。

9.3 训练过程

训练目标和图像扩散类似,只是对象从 image latent 换成 point cloud tensor。

python 复制代码
def train_goal_pointcloud_diffusion(batch):
    start_pc = batch["start_pc"]   # [B, N, 6]
    goal_pc = batch["end_pc"]      # [B, N, 6]
    text = batch["instruction"]

    start_pc = transpose_to_channels_first(start_pc)  # [B, 6, N]
    goal_pc = transpose_to_channels_first(goal_pc)    # [B, 6, N]

    t = sample_random_timestep()
    noise = randn_like(goal_pc)
    noisy_goal = scheduler.add_noise(goal_pc, noise, t)

    model_input = concat_channels(noisy_goal, start_pc)  # [B, 12, N]

    pred_noise = point_transformer(
        x=model_input,
        timestep=t,
        input_pointcloud=start_pc,
        texts=text
    )

    loss = mse(pred_noise, noise)
    update_model(loss)

模型内部使用:

  • timestep embedding 表示当前扩散步。
  • CLIP text embedding 表示语言指令。
  • transformer backbone 建模点之间的全局关系。
  • conditioning dropout 支持 classifier-free guidance。

9.4 推理过程

推理时从随机点云噪声开始,在每一步只更新目标点云那 6 个通道,起始点云条件保持不变。

python 复制代码
def infer_goal_pointcloud(start_pc, instruction):
    start_pc = normalize_and_sample(start_pc)
    x_goal = randn([1, 6, N])

    x = concat_channels(x_goal, start_pc)  # [1, 12, N]

    for t in scheduler.timesteps:
        pred_noise_cond = model(x, t, start_pc, texts=[instruction])

        if use_classifier_free_guidance:
            pred_noise_uncond = model(x, t, start_pc, texts=[None])
            pred_noise = pred_noise_uncond + scale * (
                pred_noise_cond - pred_noise_uncond
            )
        else:
            pred_noise = pred_noise_cond

        x[:, :6] = scheduler.step(pred_noise, t, x[:, :6])

    return x[:, :6]  # predicted goal point cloud

仓库推理默认使用 64 个 reverse diffusion steps,classifier-free guidance scale 为 2.0。

10. 目标想象如何服务机器人控制

3D-VLA 最重要的思想不是"额外训练一个生成模型",而是把目标生成变成动作规划前的中间世界模型。

完整方法可以分成两阶段:

阶段一:Goal Imagination

输入当前场景和用户指令:

text 复制代码
User: The initial scene is <scene>[embed]</scene>. Find some snacks for me.

3D-VLA LLM 生成带结构的响应:

text 复制代码
Robot: Sure, I should <pcd> pick up <obj> the chip bag </obj> [loc tokens] </pcd>

这表示模型判断:为了完成任务,应该在目标点云空间中想象"薯片袋被拿起"后的状态。

随后系统把 <pcd>...</pcd> 中的语义、目标物体和位置 token 交给 point cloud diffusion model,生成目标点云。

如果输出的是 <image>...</image>,则调用 goal image diffusion model,生成目标图像或 RGB-D 图像。

阶段二:Robot Control

得到目标状态后,系统再把当前场景和目标状态送回 3D-VLA LLM,提示执行:

text 复制代码
User: <scene>[current_or_goal_embed]</scene> Execute now.

模型输出动作 token:

text 复制代码
Robot: Actions are: [action tokens]

这些 action tokens 再被解码为机器人轨迹或离散控制序列。

高层伪代码:

python 复制代码
def run_3dvla_policy(observation, user_instruction):
    current_scene = build_3d_scene(observation)
    current_scene_tokens = encode_3d_scene(
        current_scene.features,
        current_scene.coordinates
    )

    reasoning_text = llm_generate(
        text=f"The initial scene is <scene></scene>. {user_instruction}",
        scene_tokens=[current_scene_tokens]
    )

    if contains_goal_image_request(reasoning_text):
        goal_prompt = parse_between(reasoning_text, "<image>", "</image>")
        goal_state = goal_image_ldm.generate(
            input_image=observation.rgb_or_rgbd,
            instruction=goal_prompt
        )
        goal_scene_tokens = encode_goal_state(goal_state)

    elif contains_goal_pointcloud_request(reasoning_text):
        goal_prompt = parse_between(reasoning_text, "<pcd>", "</pcd>")
        goal_state = goal_pcd_diffusion.generate(
            input_pointcloud=current_scene.pointcloud,
            instruction=goal_prompt
        )
        goal_scene_tokens = encode_goal_state(goal_state)

    else:
        goal_scene_tokens = None

    action_text = llm_generate(
        text="<scene></scene> Execute now.",
        scene_tokens=[goal_scene_tokens or current_scene_tokens]
    )

    action_tokens = parse_action_tokens(action_text)
    robot_actions = decode_action_tokens(action_tokens)
    return robot_actions

这里的核心是:LLM 不只是"看图说动作",而是可以把任务拆成"当前状态理解 -> 目标状态想象 -> 动作生成"。

11. 动作 token 如何表达机器人控制

仓库中加入了三类动作 token:

text 复制代码
<aloc0> ... <aloc255>
<arot0> ... <arot255>
<gripper0> <gripper1>
<ACT_SEP>

它们可以表达一个 7D end-effector action:

text 复制代码
位置: x, y, z
旋转: roll, pitch, yaw 或其他旋转参数化
夹爪: open / close

一种典型解码方式是:

python 复制代码
def decode_action_step(tokens):
    x_bin = parse("<aloc*>", tokens[0])
    y_bin = parse("<aloc*>", tokens[1])
    z_bin = parse("<aloc*>", tokens[2])

    r1_bin = parse("<arot*>", tokens[3])
    r2_bin = parse("<arot*>", tokens[4])
    r3_bin = parse("<arot*>", tokens[5])

    gripper = 1 if tokens[6] == "<gripper1>" else 0

    xyz = dequantize_position(x_bin, y_bin, z_bin)
    rot = dequantize_rotation(r1_bin, r2_bin, r3_bin)

    return Action(xyz=xyz, rotation=rot, gripper=gripper)

多步动作之间用 <ACT_SEP> 分隔:

python 复制代码
def decode_action_sequence(token_sequence):
    chunks = split_by_token(token_sequence, "<ACT_SEP>")
    return [decode_action_step(chunk) for chunk in chunks]

源码确认:当前公开代码中定义了这些 action special tokens,但没有完整释放 action token 到真实机器人控制器的后处理实现。因此本文对 7D 动作解码细节属于方法层解释,而不是公开仓库中已完整实现的控制栈。

12. 三类训练任务之间的关系

3D-VLA 不是只训练一个模型,而是多种任务共同形成能力。

12.1 3D-VLA LLM 对齐训练

目标:让语言模型读懂 3D 场景,并输出正确文本、目标请求或动作 token。

输入:

text 复制代码
3D场景特征 + 语言问题/指令

输出:

text 复制代码
答案 / 目标生成token / 动作token

损失:

text 复制代码
token cross entropy

12.2 Goal Image LDM 训练

目标:学习当前图像到目标图像的任务条件变化。

输入:

text 复制代码
当前RGB/RGB-D + 指令

输出:

text 复制代码
目标RGB/RGB-D

损失:

text 复制代码
diffusion noise prediction MSE

12.3 Goal Point Cloud Diffusion 训练

目标:学习当前点云到目标点云的任务条件变化。

输入:

text 复制代码
当前点云 + 指令

输出:

text 复制代码
目标点云

损失:

text 复制代码
diffusion noise prediction MSE

三者的关系可以理解为:

text 复制代码
LLM负责决定"应该想象什么/应该执行什么"
Diffusion负责生成"完成任务后的世界状态"
Action tokens负责把目标状态落实为控制命令

13. 关键设计为什么有效

13.1 显式引入目标状态

如果直接输出动作,模型必须隐式学会:

text 复制代码
当前状态 -> 目标状态 -> 动作

3D-VLA 把中间的目标状态显式化:

text 复制代码
当前状态 -> 目标想象 -> 动作

这让模型更容易处理长时程任务,因为目标状态是一个可观察、可训练、可检查的中间变量。

13.2 使用 3D 表示而不是只用 2D 图像

二维图像容易受到视角、遮挡和尺度影响。3D 点云/体素表示能更直接表达:

  • 物体的真实空间位置。
  • 操作目标和障碍物之间的距离。
  • 抽屉、柜子、桌面等结构的三维关系。
  • 机器人末端执行器应该到达的位置。

13.3 用 token 统一多模态接口

把位置、物体、图像请求、点云请求和动作全部做成 token,使得系统可以复用大语言模型的序列生成能力,而不需要为每种输出设计完全不同的 head。

13.4 用扩散模型建模多解目标

同一个任务可能有多个合理目标状态。例如"把杯子移到海绵附近",杯子可以放在海绵左边、右边或前面。扩散模型天然适合生成多模态分布,而不是只回归一个平均结果。

13.5 冻结大语言模型主体,训练对齐层和特殊 token

冻结 Flan-T5 主体可以保留语言模型已有的推理和泛化能力,同时降低训练成本。训练 Q-Former、投影层和新增 token embedding,让模型学会把 3D 世界接入语言空间。

14. 方法与当前仓库实现的对应关系

这部分不是源码导读,只是帮助读者理解当前仓库到底实现了方法的哪些组件。

源码确认:

  • train.py 训练 3D-VLA 的 BLIP2/T5 语言模型骨架。
  • lavis/models/blip2_models/blip2_t5.py 定义 3D scene token 插入、Q-Former、Flan-T5、特殊 token 和训练前向过程。
  • lavis/datasets/datasets/threedvqa_datasets.py 读取 3D 特征、点坐标、文本输入和答案。
  • train_ldm_goal_image.py 训练 goal image / RGB-D latent diffusion model。
  • inference_ldm_goal_image.py 推理目标图像或目标 RGB-D。
  • train_pe_goal_pcd.py 训练 goal point cloud diffusion model。
  • inference_pe_goal_pcd.py 推理目标点云。
  • lavis/models/pointe/transformer.py 定义 Point-E 风格 transformer 和 3D-VLA 改造后的 GoalPointDiffusionTransformer

合理推断:

  • 当前公开仓库主要是子模块级实现,并没有完整发布一个端到端机器人闭环执行脚本。
  • 方法图中的 projector / LLM-to-diffusion 调用链路在概念上是完整方法的一部分,但公开代码中更明显的是分别训练和调用 image / point cloud diffusion model。
  • action special tokens 已经在 tokenizer 中定义,但动作 token 到机器人控制器的完整落地流程没有在公开仓库中完整展开。

15. 最简洁的完整伪代码

下面是一份把 3D-VLA 方法串起来的端到端伪代码。它表达的是论文方法的完整运行逻辑;当前公开仓库没有把这段闭环控制流程封装成同一个可直接运行的入口。

python 复制代码
class ThreeDVLA:
    def __init__(self):
        self.scene_encoder = QFormer3DEncoder()
        self.llm = FlanT5WithSpecialTokens()
        self.goal_image_model = GoalImageLatentDiffusion()
        self.goal_pcd_model = GoalPointCloudDiffusion()

    def encode_scene(self, observation):
        pc_feat, pc_coord = build_3d_features(observation)
        return self.scene_encoder(pc_feat, pc_coord)

    def imagine_goal(self, observation, instruction):
        scene_tokens = self.encode_scene(observation)

        response = self.llm.generate(
            text="The initial scene is <scene></scene>. " + instruction,
            scene_tokens=[scene_tokens],
        )

        if has_tag(response, "image"):
            goal_instruction = extract_tag(response, "image")
            return self.goal_image_model.generate(
                input_image=observation.rgb_or_rgbd,
                instruction=goal_instruction,
            )

        if has_tag(response, "pcd"):
            goal_instruction = extract_tag(response, "pcd")
            return self.goal_pcd_model.generate(
                input_pointcloud=observation.pointcloud,
                instruction=goal_instruction,
            )

        return None

    def act(self, observation, instruction):
        goal_state = self.imagine_goal(observation, instruction)

        if goal_state is not None:
            control_scene = self.encode_scene(goal_state)
        else:
            control_scene = self.encode_scene(observation)

        action_text = self.llm.generate(
            text="<scene></scene> Execute now.",
            scene_tokens=[control_scene],
        )

        action_tokens = parse_action_tokens(action_text)
        return decode_robot_actions(action_tokens)

16. 一句话总结

3D-VLA 的本质是把机器人任务从"看当前图像直接输出动作"改成"理解 3D 当前世界、用语言模型决定需要怎样的目标世界、用扩散模型生成目标图像或目标点云、再把目标世界转成动作 token"。它把 3D 表示、语言推理、生成式世界模型和机器人控制统一到 token-based 的交互框架里,因此能更显式地处理空间关系、目标状态和长时程任务。