Skip to content

视觉-语言-行动模型

视觉-语言-行动模型将看见、理解语言和行动统一到一个神经网络中。本章涵盖 VLA 架构、动作标记化、RT‑2、Octo、OpenVLA、预训练策略、泛化、与具体硬件无关的模型以及基准测试。

  • 在前面的章节中,我们学习了感知(感知世界)和机器人学习(控制身体)。传统上,这两者是分离的流程:一个感知模块检测物体,一个语言模块解析指令,一个控制模块生成动作。每个模块独立设计、训练和调试。

  • 视觉-语言-行动模型 将这一流程压缩成一个单一的神经网络。模型接收图像(视觉)和自然语言指令(语言),输出电机命令(行动)。一个模型,端到端。

  • 这遵循了我们在第10章中看到的统一趋势:就像多模态模型将视觉和语言理解合并到一个架构中一样,VLA 将这一趋势扩展到物理行动。其核心见解在于,语言为指定任务提供了一个自然、灵活的接口(“拿起红色杯子,把它放在架子上”),而大型预训练的视觉-语言模型已经能够理解图像和指令。

从视觉-语言到行动

  • 回顾第10章,像 LLaVA 和 Flamingo 这样的视觉-语言模型以图像和文本作为输入,输出文本。它们理解场景、回答问题、跟随指令,全部通过语言完成。

  • VLA 提出的问题是:如果输出不是文本而是机器人动作会怎样?模型不再生成“红色杯子在桌子左边”,而是生成一系列电机命令,使手臂移动去抓起那个杯子。

  • 关键的架构洞见是:动作可以像单词一样被表示为标记。如果一个 VLM 使用下一标记预测逐个生成语言标记,那么一个 VLA 就可以用同样的方式生成动作标记。Transformer 本质上并不关心输出标记代表“杯子”还是“将夹爪向前移动 2 厘米”。

  • 这重新将机器人控制定义为一个序列建模问题,而 Transformer 正擅长于此(第7章)。模型学习从(图像观测,语言指令)到(动作标记序列)的映射。

VLA 架构

VLA 架构:相机图像和语言指令被编码为标记,经 LLM 主干处理,再解码为机器人动作

  • 典型的 VLA 包含三个组件:

    • 视觉编码器:将相机图像处理为视觉标记。通常使用预训练的 ViT(第8章)或 SigLIP 编码器(第10章)。图像被分割为小块,每个小块嵌入为一个标记,与标准视觉 Transformer 完全相同。

    • 语言模型主干:一个预训练的 LLM(例如 LLaMA、PaLM),处理交错的视觉标记和语言标记序列。这是推理发生的地方:模型通过同时关注指令和视觉特征来理解“拿起红色杯子”。

    • 动作头:将 LLM 的输出映射为机器人动作。可以是一个简单的 MLP,将最后的隐藏状态映射为连续的动作值;也可以采用某种标记化方案,将动作转换为离散标记,由 LLM 的现有词汇表来预测。

  • 架构形式如下:

\[\text{图像} \xrightarrow{\text{ViT}} \text{视觉标记} \quad + \quad \text{指令} \xrightarrow{\text{分词器}} \text{语言标记} \quad \xrightarrow{\text{LLM}} \quad \text{动作标记}\]
  • 视觉标记和语言标记被拼接(或交错)后馈入 Transformer 主干,主干自回归地生成动作标记。这与 VLM 的架构相同(第10章),只是输出模态从文本变成了动作。

动作标记化

  • 机器人动作是连续的:关节速度、末端执行器位置、夹爪宽度。这些连续值必须转换为离散的标记,以便 LLM 生成。

动作标记化:连续的动作值被分箱成离散的索引,LLM 将这些索引生成为标记

  • 最简单的方法是均匀离散化。每个动作维度被划分为 \(N\) 个区间,覆盖有效值的范围。例如,如果 x 方向速度的范围是 -0.1 到 0.1 m/s,我们使用 256 个区间,那么每个区间代表 \(\frac{0.2}{256} \approx 0.8\) mm/s。一个动作值被映射到最近的区间索引,该索引就成为一个标记。

  • 如果有 7 个动作维度(6 自由度 + 夹爪),每个维度 256 个区间,则动作词汇表共有 \(7 \times 256 = 1792\) 个标记。这些标记被添加到 LLM 原有的文本词汇表中。模型自回归地为每个维度生成一个动作标记,就像生成单词一样。

  • 动作块 一次性预测未来多个时间步,而不是单个动作。如果块大小为 \(H\),模型输出 \(H \times d\) 个标记(\(d\) 是动作维度)。这对于产生平滑、时间上连贯的运动至关重要。单步预测可能导致抖动,因为每一步的预测是独立的。分块迫使模型规划一个短轨迹,从而捕捉时间结构。

  • 更复杂的方法使用可学习的标记化,通过 VQ‑VAE(第10章)实现。VQ‑VAE 编码器将连续动作序列映射为离散码本索引序列,解码器再从这些索引重建连续动作。LLM 随后生成码本索引,而不是均匀分箱的值。这类似于图像分词器(第10章)将视觉信息压缩为紧凑的离散码的方式。

关键的 VLA 模型

  • RT‑2(Robotic Transformer 2,Google DeepMind)是第一个大规模 VLA。它采用预训练的 VLM(PaLM‑E 或 PaLI‑X,参数最多 55B),并在机器人演示数据上微调。动作被表示为文本字符串:例如标记序列“1 128 91 241 5 101 127”编码了一个 7 维动作(每个数字是一个区间索引)。

  • RT‑2 展示了一个显著的特性:涌现能力从 VLM 主干迁移到了机器人领域。模型能够遵循从未在机器人数据中见过的概念指令(例如“把香蕉移到以 A 开头的国家”),这需要视觉物体识别 + 世界知识 + 行动。VLM 的语言理解和视觉推理能力“免费”获得。

  • RT‑2 的局限性在于,它仅用单个机器人实体(一个特定夹爪的特定手臂)的数据训练,无法泛化到不同的机器人。

  • Octo(UC Berkeley)是一个开源的、与具体硬件无关的 VLA,旨在跨不同机器人平台工作。其关键创新包括:

    • 扩散动作头,而不是自回归标记预测。动作头接收 Transformer 的输出,通过去噪扩散过程(第8章)生成动作。这天然地处理了多模态动作分布(见下图),即完成任务存在多种有效方式。

多模态动作分布:回归会将两条有效路径平均成一条穿过障碍物的无效路径

- **灵活的观测和动作空间**:Octo 为不同的机器人配置使用任务特定的分词器。它在 Open X‑Embodiment 数据集上预训练,该数据集包含来自 22 种不同机器人实体的演示。

- **高效微调**:Octo 可以用少至 100 次演示微调到新机器人,使其在数据有限的实验室中也很实用。
  • OpenVLA(Stanford, UC Berkeley)采用的方法是对现有的开源 VLM(基于 Llama)进行机器人领域的微调。它使用 70 亿参数的主干、均匀动作标记化(每维度 256 个区间),并在 Open X‑Embodiment 数据上训练。其优势在于简单:架构是标准的 VLM,只是将动作标记添加到词汇表中,因此可以利用现有的 LLM 基础设施轻松训练和部署。

  • \(\pi_0\)(Physical Intelligence)代表了当前的最先进水平。它使用预训练的 VLM 主干和流匹配动作头(第8章)。流匹配通过学习一个将噪声传输到动作分布的向量场来生成动作,产生平滑、时间上连贯的动作轨迹。\(\pi_0\) 展现了卓越的通用性,能够在多种机器人实体上执行任务,包括双臂操纵和灵巧手控制。

预训练策略

  • VLA 极大地受益于预训练的 VLM 主干,后者已经理解了视觉场景和语言。典型的训练流程包括以下几个阶段:

    1. VLM 预训练:在数十亿对互联网图文数据上训练(或使用现成的)一个视觉-语言模型(CLIP、SigLIP、LLaVA 风格的训练,如第10章所述)。

    2. 机器人数据协同训练:在互联网数据和机器人演示数据的混合体上微调 VLM。互联网数据防止视觉和语言理解的灾难性遗忘,而机器人数据教会动作生成。两者混合的比例至关重要:机器人数据太多会损害语言理解,太少则无法学会动作。

    3. 特定任务微调:可选步骤,在特定任务或特定机器人的演示上进行微调,通常使用 LoRA(第10章)来保持可训练参数的数量较少。

  • 机器人数据的数量比互联网数据少几个数量级。一个 VLM 可能在数十亿张图像上预训练,但最大的机器人数据集(Open X‑Embodiment)也仅包含数百万帧(跨所有实体)。数据稀缺正是从预训练 VLM 开始至关重要的原因:视觉和语言表示可以迁移,只需要从有限的机器人数据中学习动作映射。

泛化

  • VLA 的承诺在于泛化:执行训练期间未见的任务,操作未见过的物体,处于未见过的环境,遵循未见过的指令。

  • VLA 沿着多个轴进行泛化:

    • 新物体:VLM 主干通过互联网预训练识别物体。如果模型从网络图像中知道“螺丝刀”长什么样,那么即使没有一次机器人演示包含过螺丝刀,它也能操作螺丝刀。

    • 新指令:组合式语言理解使模型能够遵循已知概念的新组合。“把蓝色方块叠到绿色方块上”即使训练中只展示了叠红色方块也能工作,因为模型从语言预训练中理解了颜色形容词。

    • 新环境:在一定程度上,VLA 能够跨视觉领域(不同的桌子、光照、背景)迁移,因为视觉编码器是在多样的网络图像上预训练的。但这也有局限:在实验室训练的机器人可能会在杂乱厨房中挣扎。

    • 新实体:这是最困难的轴。不同的机器人有不同的动作空间(关节角度 vs. 末端执行器速度)、不同的传感器(腕部相机 vs. 顶视相机)以及不同的物理能力。像 Octo 和 \(\pi_0\) 这样的与实体无关的模型利用灵活的分词器和跨多种机器人类型的预训练来解决这一问题。

  • 泛化通过保留任务进行评估:要求机器人执行它从未被训练过的任务。对于分布内的任务,成功率通常 >90%;在新任务上,50–80% 的成功率被认为是强结果。随着模型规模扩大和机器人数据集增长,这一差距正在缩小。

与具体硬件无关的模型

  • 该领域正朝着“一个模型,多种机器人”的方向发展。不再为每个机器人分别训练一个策略,而是让一个 VLA 处理多种实体。

  • 这需要解决动作空间不匹配问题。一个带平行夹爪的 7 自由度手臂有 7 个动作维度;一个双臂装置有 14 个;四足机器人有 12 个;人形机器人有 30 个以上。动作标记化必须足够灵活,以处理所有这些情况。

  • 解决方案包括:

    • 填充动作向量:使用最大的动作空间,较小的空间用零填充。
    • 每个实体的动作头:一个共享的 Transformer 主干,加上为每种机器人类型单独的一个小 MLP。
    • 归一化的动作表示:将所有动作表达在一个公共坐标系中(例如,在世界坐标系中的末端执行器速度),使产生相似末端执行器运动的不同机器人共享相同的动作标记。
  • 共享主干学习通用的视觉和语言理解,以及常见操作策略(从上方向下接近、与物体对齐、闭合夹爪)。实体特定的组件只需将这些高层策略转化为具体的电机指令。

基准测试与评估

  • 评估 VLA 特别具有挑战性,因为它需要真实的物理机器人实验(或高保真仿真)。

  • SIMPLER(Simulated Manipulation Policy Evaluation for Robot learning)提供了标准化的仿真环境,无需物理硬件即可比较 VLA 性能。它与真实世界的成功率有很好的相关性,并支持可复现的基准测试。

  • 真实世界评估 仍然是黄金标准。典型流程包括:

    1. 定义一组具有明确成功标准的任务(物体到达目标位置、选择正确物体、在规定时间内完成任务)。
    2. 每个任务运行 \(N\) 次试验(通常为 10–50 次)。
    3. 报告成功率及置信区间。
    4. 包括保留任务(从未训练过的任务)以衡量泛化能力。
  • Open X‑Embodiment 数据集和基准汇总了来自 22 家机构的、跨多个机器人平台的数据。它提供了标准化的演示共享格式和一套通用的评估套件,用于跨实体迁移。

编程任务(使用 CoLab 或 notebook)

  1. 实现动作标记化:将连续动作离散化为区间并重建。观察量化误差随区间数量的变化。

    import jax.numpy as jnp
    
    # 连续动作:7 维(6 自由度 + 夹爪)
    action_true = jnp.array([0.023, -0.051, 0.012, 0.1, -0.03, 0.005, 0.8])
    action_min = jnp.array([-0.1, -0.1, -0.1, -0.5, -0.5, -0.5, 0.0])
    action_max = jnp.array([ 0.1,  0.1,  0.1,  0.5,  0.5,  0.5, 1.0])
    
    for n_bins in [16, 64, 256, 1024]:
        # 标记化:将连续值映射到区间索引
        normalised = (action_true - action_min) / (action_max - action_min)
        tokens = jnp.clip((normalised * n_bins).astype(int), 0, n_bins - 1)
    
        # 去标记化:将区间索引映射回连续值
        reconstructed = (tokens + 0.5) / n_bins * (action_max - action_min) + action_min
    
        error = jnp.linalg.norm(action_true - reconstructed)
        print(f"区间数={n_bins:4d}  tokens={tokens}  误差={error:.6f}")
    

  2. 模拟动作块 vs. 单步预测。生成一条平滑轨迹,给单步预测添加噪声,并与基于块的预测比较。

    import jax
    import jax.numpy as jnp
    import matplotlib.pyplot as plt
    
    # 真实平滑轨迹(例如,伸手运动)
    t = jnp.linspace(0, 2 * jnp.pi, 100)
    gt_x = jnp.sin(t)
    gt_y = 1 - jnp.cos(t)
    
    # 单步预测:每一步都有独立噪声
    rng = jax.random.PRNGKey(42)
    noise_ss = jax.random.normal(rng, (100, 2)) * 0.05
    single_step = jnp.stack([gt_x, gt_y], axis=1) + noise_ss
    # 单步误差累积导致的漂移
    single_step_cumulative = jnp.cumsum(noise_ss, axis=0) * 0.3 + jnp.stack([gt_x, gt_y], axis=1)
    
    # 块预测(块大小=10):块内噪声相关,更平滑
    chunk_size = 10
    rng2 = jax.random.PRNGKey(7)
    chunks = []
    for i in range(0, 100, chunk_size):
        chunk_noise = jax.random.normal(jax.random.fold_in(rng2, i), (2,)) * 0.05
        chunk = jnp.stack([gt_x[i:i+chunk_size], gt_y[i:i+chunk_size]], axis=1)
        chunks.append(chunk + chunk_noise)
    chunked = jnp.concatenate(chunks, axis=0)
    
    plt.figure(figsize=(8, 4))
    plt.plot(gt_x, gt_y, "k-", linewidth=2, label="真实轨迹")
    plt.plot(single_step_cumulative[:, 0], single_step_cumulative[:, 1],
             "r-", alpha=0.7, label="单步预测(漂移)")
    plt.plot(chunked[:, 0], chunked[:, 1], "b-", alpha=0.7, label="分块预测(稳定)")
    plt.legend(); plt.axis("equal"); plt.grid(True)
    plt.title("动作块 vs. 单步预测")
    plt.show()
    

  3. 可视化 VLA 的动作分布如何呈现出多模态。使用一个简单的二维高斯混合模型,说明为什么扩散/流匹配动作头优于回归。

    import jax
    import jax.numpy as jnp
    import matplotlib.pyplot as plt
    
    # 绕过障碍物有两种有效方式:向左或向右
    rng = jax.random.PRNGKey(0)
    k1, k2 = jax.random.split(rng)
    
    mode1 = jax.random.normal(k1, (200, 2)) * 0.15 + jnp.array([-1.0, 0.5])
    mode2 = jax.random.normal(k2, (200, 2)) * 0.15 + jnp.array([ 1.0, 0.5])
    samples = jnp.concatenate([mode1, mode2])
    
    # 回归会预测均值 = 两种模式的平均(无效!)
    mean_pred = samples.mean(axis=0)
    
    plt.figure(figsize=(6, 5))
    plt.scatter(samples[:, 0], samples[:, 1], s=5, alpha=0.5, label="真实的动作分布")
    plt.plot(*mean_pred, "rx", markersize=15, markeredgewidth=3, label="回归均值(无效!)")
    plt.plot(-1, 0.5, "g^", markersize=12, label="模式1(向左)")
    plt.plot(1, 0.5, "b^", markersize=12, label="模式2(向右)")
    plt.legend(); plt.grid(True)
    plt.title("多模态动作:为什么回归会失败")
    plt.xlabel("动作维度 1"); plt.ylabel("动作维度 2")
    plt.show()