一句话总结

Lift3D-VLA 通过三项创新让 VLA 模型真正理解 3D 几何与物理动态:将点云对齐到预训练 2D 位置嵌入、用掩码自编码同时学习当前结构与未来演变、用 LLM 多层隐状态协同预测时序连贯的动作序列。

为什么这个问题重要?

机器人抓取一个杯子,看似简单,实则需要精确的几何感知:杯子在哪里?距离多远?手指该从哪个角度接近?这些问题在 2D 图像中永远是欠定的。

现有 VLA 方法的瓶颈

当前主流 VLA 方法(如 RT-2、OpenVLA)把 RGB 图像喂给视觉编码器,再通过 LLM 生成动作。但 RGB 图像有两个根本性缺陷:

  • 深度歧义:同一 2D 像素可能对应不同深度,抓取时差 1cm 就会失败
  • 视角依赖:换个相机角度,同一场景”看起来”完全不同

3D 点云提供了明确的空间坐标,但直接用 3D 编码器有另一个问题:数据稀缺。3D 操作数据比 2D 图像少好几个数量级,从头训练 3D 编码器效果很差。

Lift3D-VLA 的核心思路

不从头训练 3D 编码器,而是把预训练的 2D 视觉编码器”升维”——让它直接处理点云,同时保留其丰富的预训练语义知识。

背景知识

VLA 架构简介

RGB图像 ──→ 视觉编码器(ViT) ──┐
                              ├──→ LLM ──→ 动作预测头 ──→ 机器人控制
语言指令 ──→ 文本嵌入         ──┘

ViT 把图像分成 $16 \times 16$ 的 patch,每个 patch 加上位置嵌入(告诉模型这个 patch 在图像哪个位置)后送入 Transformer。Lift3D-VLA 的关键洞察在于:这套位置嵌入机制可以被几何对齐到 3D 空间。

为什么点云适合操作任务?

表示方式 深度信息 数据获取 计算量
RGB 图像 无(需推断) 极易
深度图 有,但视角固定 较易
点云 有,视角无关 需 RGBD 相机
体素网格 较难

点云是最佳折衷:一台 RealSense 或 Azure Kinect 即可采集,天然视角无关,且不需要庞大存储。

核心方法

创新一:Lift3D 几何对齐策略

直觉:ViT 的位置嵌入本质上是”这个特征来自图像的哪个位置”。3D 点云的每个点也有位置——三维坐标。如果能学一个映射 $f_\theta: (x,y,z) \to (u,v)$,把 3D 坐标对应到 2D 位置嵌入空间,就可以直接复用预训练的 ViT!

\[\mathbf{e}_{pos}(\mathbf{p}) = \text{Interp}\bigl(\mathbf{E}_{2D},\ f_\theta(x, y, z)\bigr)\]

其中 $\mathbf{E}_{2D} \in \mathbb{R}^{H/16 \times W/16 \times d}$ 是预训练 ViT 的 2D 位置嵌入表,$\text{Interp}$ 为双线性插值。

import torch
import torch.nn as nn
import torch.nn.functional as F

class Lift3DPositionalEncoder(nn.Module):
    """将3D点坐标几何对齐到预训练ViT的2D位置嵌入空间"""

    def __init__(self, grid_size=14, d_model=768):
        super().__init__()
        self.grid_size = grid_size  # ViT patch网格大小(ViT-B/16 为14×14)

        # 可学习的3D→2D几何投影:输入归一化坐标,输出patch平面坐标
        self.geo_projector = nn.Sequential(
            nn.Linear(3, 64), nn.GELU(), nn.Linear(64, 2)
        )

        # 预训练ViT的2D位置嵌入(实际使用时从checkpoint加载)
        self.pos_embed_2d = nn.Parameter(
            torch.randn(1, grid_size, grid_size, d_model) * 0.02
        )

    def forward(self, points_3d, point_features):
        """
        points_3d:      (B, N, 3) 点云坐标,已归一化到 [-1, 1]
        point_features: (B, N, C) 来自点云主干网络的特征
        """
        B = points_3d.shape[0]

        # 3D坐标 → 归一化2D坐标 (u,v) ∈ [-1,1],与 grid_sample 约定一致
        uv = torch.tanh(self.geo_projector(points_3d))    # (B, N, 2)

        # 将2D位置嵌入图转为 (B, d, H, W),便于 grid_sample 采样
        pos_map = self.pos_embed_2d.permute(0, 3, 1, 2).expand(B, -1, -1, -1)

        # 双线性插值:为每个3D点采样其几何对应的2D位置嵌入
        sampled = F.grid_sample(
            pos_map, uv.unsqueeze(1),                     # grid: (B,1,N,2)
            mode='bilinear', align_corners=True
        )                                                  # (B, d, 1, N)
        lifted_pos = sampled.squeeze(2).permute(0, 2, 1)  # (B, N, d)

        return point_features + lifted_pos                 # 加性融合

创新二:GC-MAE 几何中心掩码自编码

标准 MAE 掩掉图像 patch 然后重建。GC-MAE 对点云做同样的事,但增加了第二个目标:预测点云的未来几何状态,让模型内化物理动态。

\[\mathcal{L}_{GC\text{-}MAE} = \underbrace{\frac{1}{|M|}\sum_{i \in M}\|\mathbf{p}_i - \hat{\mathbf{p}}_i\|^2}_{\text{当前重建}} + \lambda\underbrace{\frac{1}{|M|}\sum_{i \in M}\|\Delta\mathbf{p}_i - \widehat{\Delta\mathbf{p}}_i\|^2}_{\text{未来演变预测}}\]

$\Delta\mathbf{p}_i = \mathbf{p}_i^{t+1} - \mathbf{p}_i^t$ 是点的帧间位移向量,$M$ 是被掩码的点集。

class GCMAEPretrainer(nn.Module):
    """双目标自监督:同时重建当前点云并预测未来几何位移"""

    def __init__(self, encoder, d_model=768, mask_ratio=0.75, lam=1.0):
        super().__init__()
        self.encoder = encoder
        self.mask_ratio = mask_ratio
        self.lam = lam

        # 解码器1:重建当前帧被掩掉点的坐标
        self.recon_head = nn.Sequential(
            nn.Linear(d_model, 256), nn.GELU(), nn.Linear(256, 3)
        )
        # 解码器2:预测每个点的未来位移 Δxyz(捕捉操作动态)
        self.dynamics_head = nn.Sequential(
            nn.Linear(d_model, 256), nn.GELU(), nn.Linear(256, 3)
        )

    def forward(self, pts_t, pts_t1):
        """pts_t: 当前帧点云 (B,N,3),pts_t1: 下一帧点云 (B,N,3)"""
        B, N, _ = pts_t.shape
        num_mask = int(N * self.mask_ratio)

        # 随机掩码:shuffle后取前num_mask个作为被掩点,其余可见
        perm        = torch.rand(B, N, device=pts_t.device).argsort(dim=1)
        visible_idx = perm[:, num_mask:]
        idx3        = visible_idx.unsqueeze(-1).expand(-1, -1, 3)

        pts_visible = pts_t.gather(1, idx3)
        feats = self.encoder(pts_visible)              # (B, N_vis, d_model)

        # 目标1:重建可见点坐标(验证编码器的几何理解)
        pred_curr = self.recon_head(feats)
        loss_recon = F.mse_loss(pred_curr, pts_t.gather(1, idx3))

        # 目标2:预测可见点的帧间位移(学习场景动力学)
        delta    = pts_t1 - pts_t
        pred_dyn = self.dynamics_head(feats)
        loss_dyn = F.mse_loss(pred_dyn, delta.gather(1, idx3))

        return loss_recon + self.lam * loss_dyn

创新三:层级时序动作建模

传统 VLA 只用 LLM 最后一层输出预测动作。Lift3D-VLA 认为:LLM 的不同层捕捉了不同时间尺度的语义,应协同使用。

\[\hat{\mathbf{a}}_{1:T} = \sum_{l=1}^{L} w_l \cdot g_l\!\left(\mathbf{h}_l\right), \quad \mathbf{w} = \text{softmax}(\mathbf{v})\]

浅层 $\mathbf{h}_1, \mathbf{h}_2$ 更关注局部几何(如避开障碍),深层 $\mathbf{h}_L$ 更关注语言语义(如”把杯子放到盘子左边”)。

class LayerWiseTemporalActionHead(nn.Module):
    """用LLM各层隐状态加权融合,协同预测动作序列块"""

    def __init__(self, num_layers=24, d_model=4096, action_dim=7, chunk_size=10):
        super().__init__()
        self.chunk_size = chunk_size  # 一次性预测未来T步(Action Chunking)

        # 每层一个轻量线性头,开销极小
        self.layer_heads = nn.ModuleList([
            nn.Linear(d_model, action_dim * chunk_size)
            for _ in range(num_layers)
        ])
        # 可学习的层间融合权重(初始均匀)
        self.layer_logits = nn.Parameter(torch.zeros(num_layers))

    def forward(self, hidden_states_per_layer):
        """
        hidden_states_per_layer: list[Tensor(B, seq_len, d_model)]
            LLM每层的输出隐状态
        返回: (B, chunk_size, action_dim)
        """
        B = hidden_states_per_layer[0].shape[0]
        weights = torch.softmax(self.layer_logits, dim=0)

        total = 0
        for w, hidden, head in zip(weights, hidden_states_per_layer, self.layer_heads):
            ctx  = hidden.mean(dim=1)                        # (B, d_model) 全局上下文
            pred = head(ctx).reshape(B, self.chunk_size, -1) # (B, T, action_dim)
            total = total + w * pred

        return total

Pipeline 全貌

RGB-D 图像
    ↓ 逐像素反投影
点云 (x,y,z,r,g,b)
    ↓
Lift3D Encoder(几何对齐到预训练ViT位置嵌入)
    ↓
3D感知视觉特征 + 语言嵌入
    ↓
LLM(多层,同时输出各层隐状态)
    ↓
层级时序动作头(层间加权融合)
    ↓
动作序列块 a_{1:T} → 机器人执行

实验

数据集说明

  • MetaWorld:50 类模拟操作任务(开抽屉、按按钮、推物体等),评估泛化宽度
  • RLBench:100+ 任务,多步长程规划,难度更高
  • 真实世界:桌面操作,RealSense D435 采集 RGBD 数据,8 类任务

定量评估

方法 MetaWorld 成功率 RLBench 成功率 真实世界成功率
RT-2 61.3% 55.2% 62.0%
OpenVLA 65.7% 58.4% 65.5%
3D-VLA 67.2% 60.1% 68.0%
Lift3D-VLA 76.0% 71.2% 72.0%

Lift3D-VLA 在三个测试集上分别比最强 baseline 高出 10.8%、11.1%、4%,在 out-of-distribution 扰动(相机位置偏移、光照变化、新物体)下优势更显著。

可视化:RGB-D → 点云 + 动作轨迹

import open3d as o3d
import numpy as np

def visualize_prediction(rgb, depth, action_traj, K):
    """可视化操作场景点云与预测动作轨迹"""
    fx, fy, cx, cy = K[0,0], K[1,1], K[0,2], K[1,2]
    H, W = depth.shape
    u, v = np.meshgrid(np.arange(W), np.arange(H))

    # 深度图反投影 → 点云
    z   = depth
    pts = np.stack([(u-cx)*z/fx, (v-cy)*z/fy, z], axis=-1).reshape(-1, 3)
    cols = rgb.reshape(-1, 3) / 255.0
    valid = (pts[:, 2] > 0.1) & (pts[:, 2] < 1.5)

    pcd = o3d.geometry.PointCloud()
    pcd.points = o3d.utility.Vector3dVector(pts[valid])
    pcd.colors = o3d.utility.Vector3dVector(cols[valid])

    # 预测动作轨迹(前3维为末端执行器xyz)可视化为红色线段
    traj_xyz = action_traj[:, :3]
    lines    = [[i, i+1] for i in range(len(traj_xyz) - 1)]
    traj_vis = o3d.geometry.LineSet(
        points=o3d.utility.Vector3dVector(traj_xyz),
        lines=o3d.utility.Vector2iVector(lines)
    )
    traj_vis.paint_uniform_color([1, 0, 0])

    o3d.visualization.draw_geometries([pcd, traj_vis],
                                       window_name="Lift3D-VLA 预测轨迹")

预期输出:蓝色桌面点云上叠加一条红色曲线,从机器人当前位置延伸到目标物体,可直观判断动作预测的空间合理性。

工程实践

实际部署考虑

硬件需求(以 7B 参数基座为例)

场景 GPU 推理延迟 显存占用
开发调试 RTX 3090 (24GB) ~80ms/步 ~18GB
实时部署 A100 (80GB) ~40ms/步 ~35GB
边缘端 Jetson Orin 不推荐 OOM

量化到 INT4 可以在 RTX 3090 上跑通,但操作成功率会下降约 3-5 个百分点。

点云质量是最大的坑

RGBD 相机在反光表面、玻璃、强光下会产生噪声点云,严重影响模型表现:

def filter_point_cloud(points, min_z=0.05, max_z=1.5, voxel_size=0.005):
    """实际部署必做的点云预处理,不做这步精度会显著下降"""
    pcd = o3d.geometry.PointCloud()
    pcd.points = o3d.utility.Vector3dVector(points)

    # 去除统计离群点(玻璃、金属反光造成的飞点)
    pcd, _ = pcd.remove_statistical_outlier(nb_neighbors=20, std_ratio=2.0)

    # 深度截断:过近或过远的点深度值不可靠
    pts  = np.asarray(pcd.points)
    mask = (pts[:, 2] > min_z) & (pts[:, 2] < max_z)
    pcd.points = o3d.utility.Vector3dVector(pts[mask])

    # 体素下采样:均匀化点密度,防止近处点过密远处过稀
    return np.asarray(pcd.voxel_down_sample(voxel_size).points)

常见坑

坑1:相机外参不准导致点云坐标系错位

模型在模拟器效果很好,上真机一塌糊涂。根本原因通常是手眼标定误差——相机坐标系和机器人末端执行器坐标系没对齐。需用专用标定工具将误差控制在 1mm 以内。

坑2:动作序列执行抖动

动作块中相邻步之间有跳变,手臂抖动甚至触发急停。

def smooth_action_chunk(action_chunk, alpha=0.3):
    """指数移动平均平滑,消除预测跳变"""
    smoothed = [action_chunk[0]]
    for t in range(1, len(action_chunk)):
        smoothed.append(alpha * action_chunk[t] + (1 - alpha) * smoothed[-1])
    return np.stack(smoothed)

坑3:随机下采样丢失小物体

固定 $N=8192$ 随机采样时,桌面小物体(螺丝、笔帽)可能完全丢失。用 FPS(最远点采样)代替随机采样,确保空间均匀覆盖。

什么时候用 / 不用?

适用场景 不适用场景
静态或准静态桌面操作 高速动态场景(>1m/s)
有 RGBD 相机的机器人平台 仅单目 RGB 相机
需要精确位置控制(±5mm) 粗粒度室内导航
GPU 资源充足(≥24GB) 边缘嵌入式部署
需要泛化到新物体、新场景 固定物体的重复性工业任务

与其他方法对比

方法 3D输入 动态建模 语言理解 推理速度
RT-2
SpatialVLA 部分
3D-VLA
Lift3D-VLA

Lift3D-VLA 是目前少数能同时做到”强语言理解 + 真 3D 输入 + 动态预测”的方法,代价是推理延迟偏高(40-80ms),不适合需要 >15Hz 控制频率的精密装配任务。

我的观点

Lift3D-VLA 的核心思路清晰:不要从头构建 3D 理解,而是把现有 2D 大模型的能力”嫁接”到 3D 上。这和 LoRA、Adapter 的逻辑如出一辙——数据有限时,复用预训练权重永远比从头训练聪明。

GC-MAE 的双目标设计(重建当前 + 预测未来)让我想起 DreamerV3 的世界模型。机器人要能”想象”动作的后果,才能做到灵活泛化。Lift3D-VLA 用自监督方式隐式学习了这种物理直觉,避免了显式动态模型的高昂标注成本。

离实用还有多远? 差不多到了”实验室可以落地”的阶段。真实世界 4 个百分点的提升听起来不多,但操作成功率从 68% 到 72% 意味着故障率下降 12.5%,在长时间自主运行场景中是有实际意义的。

当前主要瓶颈:

  1. 成对时序点云数据难采集:GC-MAE 需要 $(\mathbf{p}^t, \mathbf{p}^{t+1})$ 配对,比单帧数据采集难度高一个量级
  2. 推理延迟限制控制频率:40-80ms 只支持低频控制,精密装配、柔顺控制暂时没戏
  3. 深度相机环境敏感性:强光、透明物体、镜面物体对点云质量破坏严重

值得关注的开放问题:如何把这套 3D 感知框架延伸到双臂协作和更长程任务规划?以及 GC-MAE 的动态预测能力能否迁移到更快速的操作场景?这两个方向都有很大空间。