AI2026年7月10日· 约 49 分钟

3D-VLA 方法原理详解 - S-X

#3D视觉#语言模型#动作预测#目标状态#机器人控制
Twitter 微博

3D-VLA 方法原理详解 - S-X

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 序列来表示一串机器人控制命令。

整体数据流如下:

当前多视角/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
            解码成机器人动作

为了避免这个流程过于抽象,下面用一个具体任务走一遍。

示例任务:

用户指令: close bottom drawer
当前观察: 机器人看到一个柜子,底层抽屉是打开的;系统已有当前场景点云 start_pc
目标结果: 底层抽屉应该被关上,生成对应目标点云 goal_pc,并进一步产生关抽��动作

对应的数据流可以展开成:

1. 传感器/数据集给出当前状态

RGB-D / 多视角图像 / 点云 start_pc = [N, 6] 每个点包含 [x, y, z, r, g, b]

    |
    v
  1. 构建 3D 场景表示

    从当前状态中采样固定数量的点,例如 2048 或 8192 个点。 坐标做归一化,颜色/语义特征保留。 如果走 3D-VLA LLM,则还会有 pc_feat = [N, 1408] 的点级特征。

     |
     v
    
  2. Q-Former 把大量 3D 点压缩成 scene tokens

    输入: pc_feat: [1, T, N, 1408] pc: [1, T, N, 3]

    输出: scene_tokens: [1, T, 32, hidden_dim]

    这些 scene tokens 会插入到文本里的 <scene> 位置。

     |
     v
    
  3. 3D-VLA LLM 理解任务并产生中间意图

    输入文本可以理解为: "The initial scene is <scene></scene>. close bottom drawer"

    插入 scene tokens 后,LLM 看到的不是纯文本,而是: 文本 token + 当前 3D 场景 embedding

    方法级输出可能是: "I should <pcd> close bottom drawer </pcd>"

    这表示模型决定先生成一个“抽屉关闭后”的目标点云。

     |
     v
    
  4. Goal Point Cloud Diffusion 生成目标状态

    输入: start_pc: 当前点云 [1, 6, N] instruction: "close bottom drawer" x_T: 随机噪声目标点云 [1, 6, N]

    扩散模型每一步预测噪声: model_input = concat(x_t, start_pc) # [1, 12, N] noise_pred = transformer(model_input, timestep, text="close bottom drawer") x_{t-1} = scheduler.step(noise_pred, x_t)

    输出: goal_pc: 预测的关闭抽屉后点云 [1, 6, N]

     |
     v
    
  5. 目标状态回到控制阶段

    系统把 goal_pc 或由 goal_pc 重新编码得到的 scene tokens 作为目标上下文。 LLM 接收类似: "<scene></scene> Execute now."

    输出动作 token: <aloc...><aloc...><aloc...><arot...><arot...><arot...><gripper...><ACT_SEP>...

     |
     v
    
  6. 动作 token 解码成机器人控制量

    <aloc*> -> 末端执行器位置 <arot*> -> 末端执行器姿态 <gripper*> -> 夹爪开合 <ACT_SEP> -> 动作步之间的分隔

    最终得到一段关上底层抽屉的控制轨迹。

如果走 goal image 分支,步骤 5 换成:

输入:
  当前图像 image
  指令 "close bottom drawer"
  随机图像 latent x_T

扩散过程: model_input = concat(noisy_goal_image_latent, current_image_latent) noise_pred = UNet(model_input, timestep, text="close bottom drawer")

输出: goal_image = 关闭底层抽屉后的目标图像

所以“整体数据流”真正表达的是:3D-VLA 先把当前世界编码成 LLM 能读的 scene tokens;LLM 决定任务需要什么目标状态;扩散模型生成这个目标状态;最后再把目标状态转成动作 token。它不是一条单纯的图像到动作流水线,而是一条带有“目标想象”中间环节的世界模型流水线。

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

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

3.1 原始观察#

方法层面支持以下输入:

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

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

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,说明方法中也把位置作为可离散表达的语言符号。

可以把它理解为:

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

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

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

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

Q-Former 的输入是:

pc_feat_with_position: [B, T, N, 1408]

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

也就是说:

N个3D点/体素特征  -->  32个scene embeddings

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

伪代码如下:

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, class="hljs-number">0, class="hljs-number">255))
point_tokens = pc_feat + class="hljs-number">0.1 * pos_emb

scene_tokens = []
for each_state in range(T):
    query = learnable_query_tokens(num=class="hljs-number">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  class=class="hljs-string">"hljs-comment"># [B, T, class="hljs-number">32, t5_hidden_dim]

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

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

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

输入文本中会出现类似:

The initial scene is <scene></scene>. Find some snacks for me.

模型在 tokenizer 层面加入了:

<scene>, </scene>

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

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

伪代码如下:

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 = class="hljs-number">0

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

    if token == class="hljs-string">"&lt;scene&gt;":
        output_embeddings.extend(scene_embeddings[scene_index])
        scene_index += class="hljs-number">1

return output_embeddings

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

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

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

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

位置:
<loc0> ... <loc255>

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

物体: <obj> </obj>

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

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

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

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

Sure, I should <pcd> pick up <obj> the chip bag </obj> <loc...> </pcd>

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

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

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

<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 部分本质上是条件文本生成:

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

训练时使用标准 teacher forcing:

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[:-class="hljs-number">1]
)

loss = cross_entropy(logits, target_tokens[class="hljs-number">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 用来回答:

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

训练样本形式是:

当前图像 input_image
语言指令 edit_prompt
目标图像 edited_image
可选当前深度 input_depth
可选目标深度 edited_depth

例如:

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

8.1 它为什么基于 InstructPix2Pix#

InstructPix2Pix 的任务是:

输入原图 + 编辑指令 -> 输出编辑后的图

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

当前状态图像 + 任务指令 -> 完成任务后的目标状态图像

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

8.2 RGB 版本的训练过程#

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

目标图像 edited_image -> target_latent
当前图像 input_image -> condition_latent
语言指令 edit_prompt -> text_embedding

训练时对目标 latent 加噪:

noisy_target_latent = add_noise(target_latent, noise, timestep)

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

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

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

noisy目标latent: 4 channels
当前图像condition latent: 4 channels
UNet输入: 8 channels
UNet输出: 4 channels

伪代码如下:

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 通道上拼接:

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 开始,逐步去噪:

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,每个样本包含:

start_pc:    初始点云
end_pc:      目标点云
instruction: 任务文字

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

[x, y, z, r, g, b]

9.1 点云预处理#

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

  1. 采样固定数量的点。

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

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

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

  3. 归一化坐标。

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

伪代码:

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[:, :class="hljs-number">3], end_pc[:, :class="hljs-number">3])
center = mean(xyz_all)
radius = max_norm(xyz_all - center)

start_pc[:, :class="hljs-number">3] = (start_pc[:, :class="hljs-number">3] - center) / radius
end_pc[:, :class="hljs-number">3] = (end_pc[:, :class="hljs-number">3] - center) / radius

return start_pc, end_pc

9.2 为什么改造 Point-E#

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

当前点云 + 语言指令 -> 目标点云

所以它把模型输入改成:

concat(noisy_goal_pc, start_pc)

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

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。

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)  class=class="hljs-string">"hljs-comment"># [B, class="hljs-number">6, N]
goal_pc = transpose_to_channels_first(goal_pc)    class=class="hljs-string">"hljs-comment"># [B, class="hljs-number">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)  class=class="hljs-string">"hljs-comment"># [B, class="hljs-number">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 个通道,起始点云条件保持不变。

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)  class=class="hljs-string">"hljs-comment"># [class="hljs-number">1, class="hljs-number">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[:, :class="hljs-number">6] = scheduler.step(pred_noise, t, x[:, :class="hljs-number">6])

return x[:, :class="hljs-number">6]  class=class="hljs-string">"hljs-comment"># predicted goal point cloud

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

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

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

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

阶段一:Goal Imagination#

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

User: The initial scene is <scene>[embed]</scene>. Find some snacks for me.

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

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,提示执行:

User: <scene>[current_or_goal_embed]</scene> Execute now.

模型输出动作 token:

Robot: Actions are: [action tokens]

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

高层伪代码:

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=fclass="hljs-string">"The initial scene is &lt;scene&gt;&lt;/scene&gt;. {user_instruction}",
    scene_tokens=[current_scene_tokens]
)

if contains_goal_image_request(reasoning_text):
    goal_prompt = parse_between(reasoning_text, class="hljs-string">"&lt;image&gt;", class="hljs-string">"&lt;/image&gt;")
    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, class="hljs-string">"&lt;pcd&gt;", class="hljs-string">"&lt;/pcd&gt;")
    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=class="hljs-string">"&lt;scene&gt;&lt;/scene&gt; 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:

<aloc0> ... <aloc255>
<arot0> ... <arot255>
<gripper0> <gripper1>
<ACT_SEP>

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

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

一种典型解码方式是:

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(class="hljs-string">"&lt;arot*&gt;", tokens[class="hljs-number">3])
r2_bin = parse(class="hljs-string">"&lt;arot*&gt;", tokens[class="hljs-number">4])
r3_bin = parse(class="hljs-string">"&lt;arot*&gt;", tokens[class="hljs-number">5])

gripper = class="hljs-number">1 if tokens[class="hljs-number">6] == class="hljs-string">"&lt;gripper1&gt;" else class="hljs-number">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> 分隔:

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。

输入:

3D场景特征 + 语言问题/指令

输出:

答案 / 目标生成token / 动作token

损失:

token cross entropy

12.2 Goal Image LDM 训练#

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

输入:

当前RGB/RGB-D + 指令

输出:

目标RGB/RGB-D

损失:

diffusion noise prediction MSE

12.3 Goal Point Cloud Diffusion 训练#

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

输入:

当前点云 + 指令

输出:

目标点云

损失:

diffusion noise prediction MSE

三者的关系可以理解为:

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

13. 关键设计为什么有效#

13.1 显式引入目标状态#

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

当前状态 -> 目标状态 -> 动作

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

当前状态 -> ���标想象 -> 动作

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

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

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=class="hljs-string">"The initial scene is &lt;scene&gt;&lt;/scene&gt;. " + instruction,
        scene_tokens=[scene_tokens],
    )

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

    if has_tag(response, class="hljs-string">"pcd"):
        goal_instruction = extract_tag(response, class="hljs-string">"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=class="hljs-string">"&lt;scene&gt;&lt;/scene&gt; 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 的交互框架里,因此能更显式地处理空间关系、目标状态和长时程任务。


原文链接:https://www.cnblogs.com/sxq-blog/p/21326505

评论

© 2026 松岛川树