Lift3D-VLA:让机器人"看懂"三维空间的视觉-语言-动作模型
一句话总结
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%,在长时间自主运行场景中是有实际意义的。
当前主要瓶颈:
- 成对时序点云数据难采集:GC-MAE 需要 $(\mathbf{p}^t, \mathbf{p}^{t+1})$ 配对,比单帧数据采集难度高一个量级
- 推理延迟限制控制频率:40-80ms 只支持低频控制,精密装配、柔顺控制暂时没戏
- 深度相机环境敏感性:强光、透明物体、镜面物体对点云质量破坏严重
值得关注的开放问题:如何把这套 3D 感知框架延伸到双臂协作和更长程任务规划?以及 GC-MAE 的动态预测能力能否迁移到更快速的操作场景?这两个方向都有很大空间。
Comments