VLA 架构深度剖析:视觉语言动作模型与机器人操控

8259 字
41 分钟
VLA 架构深度剖析:视觉语言动作模型与机器人操控

1. VLA 的诞生:为什么需要视觉语言动作模型?#

1.1 从 VLM 到 VLA#

视觉语言模型(VLM) 能回答”图片里有什么”,但无法控制机器人执行物理操作。VLM 的输出是文本,而机器人需要的是动作指令——关节角度、末端执行器位姿、夹爪开闭。

VLA = Vision + Language + Action。VLA 在 VLM 的基础上增加了一个”动作头”,把视觉语言理解能力直接映射为机器人动作序列。

核心挑战是:动作模态与文本模态的本质不同。

VLM 输出: 文本 token 序列 (离散、变长、可自回归生成)
VLA 输出: 动作向量序列 (连续、高频、时序平滑)
文本: "请把红色积木放到蓝色积木上面"
动作: [joint_angles(7D), ee_pose(6D), gripper(1D)] @ 50Hz
特点对比:
文本: 可以一个 token 一个 token 自回归生成
动作: 必须一次性预测一个动作块 (chunk),保证时序平滑

1.2 VLA 的演进时间线#

2017 - Attention Is All You Need (Transformer)
2021 - CLIP (Radford et al.) — 视觉语言预训练
2022 - RT-1 (Brohan et al.) — 机器人 Transformer (350K 演示数据)
2023 - RT-2 (Brohan et al.) — VLA 开山之作 ★
PaLM-E (Driess et al.) — 具身多模态 LLM
RoboGPT (Kumar et al.) — LLM 作为机器人控制器
HiveFormer — 多机器人协作
2024 - OpenVLA (Zhao et al.) — 开源 VLA (7B, 97K 演示数据) ★
π₀ (Blackwell et al.) — 动作先验 + 完整机器人策略
Act (Zhang et al.) — 扩散策略 + VLM 编码器
RDT (Peng et al.) — 扩散 Transformer + 物理引擎数据
VoxPoser (Huang et al.) — LLM 编程机器人
UniARM — 通用机械臂策略
2025 - π₀.5 (Physical Intelligence) — 双手多指操控
OpenHands-VLA (Ji et al.) — 开放式手动物体操控
Helix — 完整 7 轴全身 VLA (Physical Intelligence)
GR00T (NVIDIA) — 机器人基础模型

1.3 一句话概括 VLA#

VLA 的核心是把”看得懂场景、说得清任务”的视觉语言模型,通过一个动作预测头,映射为高频、连续、平滑的机器人动作序列——让机器人像人类一样,从语言指令和视觉观察中直接输出肌肉控制信号。

2. 动作空间:VLA 的核心挑战#

2.1 动作空间的分类#

class ActionSpace:
"""
机器人动作空间的三种主要类型。
"""
DISCRETE = "discrete"
CONTINUOUS = "continuous"
HYBRID = "hybrid"
@dataclass
class ActionSpec:
"""
动作规范。
"""
space: str
action_dim: int
frequency_hz: float
chunk_size: int # 每次预测的动作步数
类型维度特点典型模型
离散动作低 (~几十)直接预测动作 token IDRT-2, PaLM-E
连续动作高 (~7-14D)回归预测实数值OpenVLA, π₀
混合中等离散 + 连续组合Helix

2.2 离散动作空间(RT-2 方案)#

RT-2 把动作空间量化为离散 token:

原始连续动作: joint_angles ∈ ℝ⁷, ee_pose ∈ ℝ⁶, gripper ∈ ℝ¹
量化: 离散 bin 化 → 32K 个动作 token
动作 token = action_token_id ∈ {1, 2, ..., 32768}
class DiscreteActionTokenizer:
"""
RT-2 的离散动作 tokenizer。
把连续动作量化成 token ID。
"""
def __init__(self, num_bins=2**15):
self.num_bins = num_bins # 32768 个离散 bin
self.action_dim = 14 # 7 joint + 6 ee + 1 gripper
def tokenize_action(self, action):
"""
连续动作 → 离散 token ID。
参数:
action: (action_dim,) 连续动作向量
返回:
token_ids: (action_dim,) 离散 token ID 列表
"""
# 每个维度独立量化
token_ids = []
for a in action:
# 归一化到 [-1, 1] (数据集内)
norm_a = (a - self.action_mean) / (self.action_std + 1e-8)
# 映射到 [0, num_bins)
bin_id = int((norm_a + 1) / 2 * (self.num_bins - 1))
bin_id = max(0, min(self.num_bins - 1, bin_id))
token_ids.append(bin_id)
return token_ids
def detokenize_action(self, token_ids):
"""
离散 token ID → 连续动作。
"""
actions = []
for tid in token_ids:
norm_a = (tid / (self.num_bins - 1)) * 2 - 1
a = norm_a * self.action_std + self.action_mean
actions.append(a)
return torch.tensor(actions)
def forward(self, action_sequence):
"""
动作序列 → token 序列。
用于训练时的标签构建。
"""
tokens = []
for step in action_sequence:
tokens.extend(self.tokenize_action(step))
return tokens # 14 × T 个 token IDs

优点

  • 与语言 token 共用词汇表,自然复用 LLM 的预训练分布
  • 可以直接用语言模型的预训练权重初始化
  • 推理时自回归生成,与 LLM 完全相同

缺点

  • 量化误差:32768 个 bin 对 14 维动作仍然粗糙
  • 高频控制困难:每个动作步需要一个自回归 step(慢)
  • 动作精度受限于 bin 数量

2.3 连续动作空间(OpenVLA 方案)#

OpenVLA 直接回归连续动作值:

class ContinuousActionHead(nn.Module):
"""
OpenVLA 的连续动作头。
输出: 每个动作维度的均值和方差(用于采样)。
"""
def __init__(self, llm_dim, action_dim, chunk_size=8):
super().__init__()
self.chunk_size = chunk_size
self.action_dim = action_dim
self.action_head = nn.Sequential(
nn.Linear(llm_dim, llm_dim // 2),
nn.ReLU(),
nn.Linear(llm_dim // 2, chunk_size * action_dim * 2), # mean + log_std
)
def forward(self, llm_output):
"""
llm_output: (B, llm_dim) LLM 的最后一层 hidden state
返回:
action_chunk: (B, chunk_size, action_dim) 预测的动作块
"""
raw = self.action_head(llm_output) # (B, chunk_size*action_dim*2)
mean, log_std = raw.chunk(2, dim=-1) # 各 (B, chunk_size*action_dim)
mean = mean.view(-1, self.chunk_size, self.action_dim)
log_std = log_std.view(-1, self.chunk_size, self.action_dim)
std = (log_std / 2).exp() + 1e-6 # 确保标准差为正
# 训练时:直接预测均值(简化版)
action_chunk = torch.tanh(mean) # 动作通常在 [-1, 1] 范围
return action_chunk

优点

  • 无量化误差,精度高
  • 可以预测动作块(chunk),支持高频控制
  • 与扩散模型结合(π₀ 用扩散头)

缺点

  • 需要额外的动作头(不能直接复用 LLM 的语言头)
  • 动作空间需要归一化(数据集之间差异大)

2.4 动作分块(Action Chunking)#

动作分块是 VLA 高频控制的关键技术:

不用 Action Chunking:
每步推理 → 预测 1 个动作 → 执行 → 等待 → 再推理
问题: 推理延迟 >> 控制周期,机器人抖动
用 Action Chunking:
一次推理 → 预测 T 个动作 [a_1, a_2, ..., a_T]
→ 执行 a_1
→ 后台并行预测下一步 [a_{T+1}, ..., a_{2T}]
优点: 推理频率要求降低 T 倍,控制平滑
@dataclass
class ActionChunkConfig:
"""动作分块配置。"""
chunk_size: int = 8 # 每次预测 8 步
execution_freq_hz: float = 50.0 # 机器人控制频率
lookahead_seconds: float = chunk_size / execution_freq_hz # 0.16 秒
# 置信度衰减
use_ema_decay: bool = True # 越远的动作权重越低
ema_gamma: float = 0.99
# 安全约束
clip_actions: bool = True
action_min: float = -1.0
action_max: float = 1.0
def predict_with_chunking(model, observation, config):
"""
用动作分块进行推理。
"""
action_chunk = model(observation) # (chunk_size, action_dim)
if config.use_ema_decay:
# 对远处的动作施加置信度衰减
decays = config.ema_gamma ** torch.arange(config.chunk_size)
action_chunk = action_chunk * decays.view(-1, 1)
# 执行第一步
action = action_chunk[0].cpu().numpy()
# 安全约束
if config.clip_actions:
action = np.clip(action, config.action_min, config.action_max)
return action, action_chunk[1:] # 返回剩余 chunk 用于下次执行

2.5 动作归一化#

VLA 训练数据来自不同机器人平台,动作空间差异巨大:

class ActionNormalizer:
"""
动作归一化器。
每个数据集的动作有不同的均值和方差。
"""
def __init__(self, action_dim):
self.action_dim = action_dim
self.register_buffer("mean", torch.zeros(action_dim))
self.register_buffer("std", torch.ones(action_dim))
def normalize(self, action):
"""连续动作 → [-1, 1] 范围。"""
return (action - self.mean) / (self.std + 1e-8)
def denormalize(self, action_normed):
"""[-1, 1] → 原始物理单位。"""
return action_normed * self.std + self.mean
def compute_stats(self, dataset):
"""从数据集计算归一化统计量。"""
all_actions = []
for traj in dataset.trajectories:
all_actions.append(traj.actions) # (T, action_dim)
all_actions = torch.cat(all_actions, dim=0) # (N_total, action_dim)
self.mean = all_actions.mean(dim=0)
self.std = all_actions.std(dim=0)
@classmethod
def from_dataset_config(cls, dataset_name):
"""预定义数据集的统计量。"""
stats = {
"rt-1": {
"action_dim": 7, # 7 自由度臂
"mean": torch.zeros(7),
"std": torch.ones(7),
},
"bridge": {
"action_dim": 8, # 7DOF + gripper
"mean": torch.zeros(8),
"std": torch.ones(8),
},
"open-x-embodiment": {
"action_dim": 14, # 7 joint + 6 ee + 1 gripper
"mean": torch.zeros(14),
"std": torch.ones(14),
},
}
return stats.get(dataset_name)

3. VLA 架构:三种主流设计#

3.1 架构概览#

┌─────────────────────────────────────────────────────────────┐
│ VLA = 视觉编码器 + LLM 骨干 + 动作头 │
├─────────────────────────────────────────────────────────────┤
│ │
│ [图像/视频] → 视觉编码器 → 视觉 tokens │
│ ↓ │
│ [语言指令] → LLM Tokenizer → 语言 tokens │
│ ↓ │
│ [视觉 tokens + 语言 tokens] → LLM Transformer ★ │
│ ↓ │
│ LLM 输出 hidden state → 动作头 → [动作块] │
│ │
└─────────────────────────────────────────────────────────────┘
三种设计模式:
A. VLA-as-VLM (RT-2): 把动作当作"超级 token",用语言头预测
B. VLM + Action Head (OpenVLA): 冻住 VLM,加专用动作头
C. Diffusion Action Head (π₀): 动作头用扩散模型

3.2 RT-2:VLA-as-VLM#

RT-2(Robotics Transformer 2)是 VLA 的开山之作,核心思想是把动作当作超级 token

RT-2 架构:
输入: 视觉 tokens + 语言指令 tokens
输出: 动作 tokens (和语言 tokens 同一套 vocabulary)
关键设计:
1. 预训练的 PaLM-E (540B) 或 PaLI-X (5B) 作为骨干
2. 动作被离散化为 32768 个 bin
3. 动作 token 和语言 token 混合输入
4. 自回归生成动作 token
5. 推理时,把动作 token 解码为连续动作
训练数据: 130K episodes (RT-1 数据 + 模拟数据 +网络数据)

为什么 RT-2 有效?

CLIP 预训练的视觉编码器学到了丰富的视觉概念。
LLM 预训练学到了语言理解和推理能力。
动作离散化后,动作空间可以被视为"另一个语言"。
→ VLM 的"语言先验"迁移到了机器人控制!
→ 能泛化到训练数据中从未见过的指令(如"把苹果放到有图案的碗里")
class RT2LikeVLA(nn.Module):
"""
RT-2 风格的 VLA(离散动作版)。
"""
def __init__(self, llm_name="google/palm2-e"):
super().__init__()
# 1) 视觉编码器 (冻结,用于预训练)
self.vision_encoder = AutoModel.from_pretrained("google/pali-x")
# 2) LLM 骨干
self.llm = AutoModelForCausalLM.from_pretrained(llm_name)
# 3) 动作 tokenizer(与语言 tokenizer 共用)
self.action_tokenizer = DiscreteActionTokenizer(num_bins=2**15)
# 4) 动作嵌入
self.action_embed = nn.Embedding(2**15, self.llm.config.hidden_size)
def forward(self, image, instruction, action_sequence=None):
"""
训练时的前向传播。
参数:
image: (B, C, H, W) 视觉输入
instruction: str 或 token IDs 语言指令
action_sequence: (B, T, action_dim) 动作序列(标签)
"""
# 视觉编码
vision_tokens = self.vision_encoder(image) # (B, N_v, D_v)
# 语言编码
if isinstance(instruction, str):
instruction_ids = self.tokenizer(instruction, return_tensors="pt").to(image.device)
else:
instruction_ids = instruction
lang_embeds = self.llm.get_input_embeddings()(instruction_ids["input_ids"])
# 融合
multimodal_embeds = torch.cat([vision_tokens, lang_embeds], dim=1)
if action_sequence is not None:
# 训练: 预测动作 tokens
action_tokens = self.action_tokenizer.forward(action_sequence)
action_embeds = self.action_embed(torch.tensor(action_tokens).to(image.device))
multimodal_embeds = torch.cat([multimodal_embeds, action_embeds], dim=1)
labels = instruction_ids["input_ids"].clone()
labels[labels == self.tokenizer.pad_token_id] = -100 # 忽略 padding
outputs = self.llm(inputs_embeds=multimodal_embeds, labels=labels)
return outputs
else:
# 推理: 自回归生成动作
outputs = self.llm.generate(
inputs_embeds=multimodal_embeds,
max_new_tokens=max_action_tokens,
)
action_tokens = outputs[0, -num_action_tokens:]
actions = self.action_tokenizer.detokenize(action_tokens)
return actions

3.3 OpenVLA:VLM + Action Head#

OpenVLA 是目前最流行的开源 VLA(Apache 2.0 许可),核心设计是冻结 VLM 骨干 + 训练专用动作头

OpenVLA 架构:
视觉编码器: SigLIP-So400m / EVA-CLIP-2B (冻结 ❄️)
LLM 骨干: Llama-3 8B (冻结 ❄️,来自 VLM 预训练)
动作头: MLP 预测头 (训练 🔥)
动作空间: 连续,chunk_size=8
关键创新:
1. 使用 SigLIP/EVA 等大规模视觉编码器(远超 CLIP)
2. 97K 小时的真实机器人演示数据训练
3. 14 亿参数(其中 7B LLM + 400M 视觉编码器)
4. 开源 + 可微调 → 社区广泛使用
训练: 模仿学习(行为克隆),用 MSE 损失预测动作
class OpenVLAConfig:
"""OpenVLA 配置。"""
vision_encoder: str = "蒲公英/siglip-so400m-224" # SigLIP-So400m
llm_model: str = "meta-llama/Meta-Llama-3-8B"
action_dim: int = 7 # 7 自由度
chunk_size: int = 8 # 每次预测 8 步
norm_stats: str = "openx_embod" # 动作归一化统计
class OpenVLA(nn.Module):
"""
OpenVLA 架构。
论文: "OpenVLA: An Open-Source Vision-Language-Action Model"
"""
def __init__(self, config: OpenVLAConfig):
super().__init__()
# 1) 视觉编码器 (冻结)
self.vision_encoder = AutoModel.from_pretrained(config.vision_encoder)
self.vision_encoder.requires_grad_(False)
# 2) 模态投影器 (训练)
vision_dim = self.vision_encoder.config.hidden_size
lang_dim = 4096 # Llama-3 8B
self.projector = nn.Sequential(
nn.Linear(vision_dim, lang_dim),
nn.GELU(),
nn.Linear(lang_dim, lang_dim),
)
# 3) LLM (冻结)
self.llm = AutoModelForCausalLM.from_pretrained(
config.llm_model,
torch_dtype=torch.float16,
device_map="auto",
)
self.llm.requires_grad_(False)
# 4) 动作头 (训练)
self.action_head = nn.Sequential(
nn.Linear(lang_dim, lang_dim // 2),
nn.ReLU(),
nn.Linear(lang_dim // 2, config.chunk_size * config.action_dim),
)
# 5) 动作归一化
self.normalizer = ActionNormalizer(config.action_dim)
self.normalizer.load_stats(config.norm_stats)
# 6) 特殊 token
self.tokenizer = AutoTokenizer.from_pretrained(config.llm_model)
obs_tokens = ["[ OBS ]", "[ / OBS ]"]
self.tokenizer.add_tokens(obs_tokens, special_tokens=True)
self.obs_start = self.tokenizer("[ OBS ]", add_special_tokens=False).input_ids[0]
self.obs_end = self.tokenizer("[ / OBS ]", add_special_tokens=False).input_ids[0]
def encode_observation(self, image):
"""编码图像观察。"""
with torch.no_grad():
vision_out = self.vision_encoder(image)
vision_embeds = vision_out.last_hidden_state # (B, N+1, D_v)
vision_embeds = vision_embeds[:, 1:, :] # 去掉 [CLS]
return self.projector(vision_embeds) # (B, N, D_l)
def forward(self, image, instruction, action=None):
"""
完整前向传播。
"""
# 编码观察
vision_embeds = self.encode_observation(image) # (B, N, D_l)
# 编码指令
instr_ids = self.tokenizer(instruction, return_tensors="pt", padding=True,
truncation=True, max_length=512).to(image.device)
lang_embeds = self.llm.get_input_embeddings()(instr_ids["input_ids"])
# 拼接: [OBS] + 视觉 tokens + [OBS] + 语言指令
B = image.shape[0]
obs_start_emb = self.llm.get_input_embeddings()(
torch.tensor([self.obs_start]).to(image.device)
).expand(B, 1, -1)
obs_end_emb = self.llm.get_input_embeddings()(
torch.tensor([self.obs_end]).to(image.device)
).expand(B, 1, -1)
multimodal_embeds = torch.cat([obs_start_emb, vision_embeds,
obs_end_emb, lang_embeds], dim=1)
# LLM 前向(冻结权重,但用于特征提取)
with torch.no_grad():
llm_out = self.llm(inputs_embeds=multimodal_embeds,
attention_mask=create_attention_mask(multimodal_embeds))
# 动作预测(训练)
last_hidden = llm_out.last_hidden_state[:, -1, :] # (B, D_l)
action_flat = self.action_head(last_hidden) # (B, chunk*action_dim)
action_chunk = action_flat.view(-1, self.chunk_size, self.action_dim)
action_chunk = torch.tanh(action_chunk) # 限制在 [-1, 1]
if action is not None:
# 训练模式: 计算 MSE 损失
action_normed = self.normalizer.normalize(action) # (B, T, action_dim)
# 取前 chunk_size 步作为标签
loss = F.mse_loss(action_chunk, action_normed[:, :self.chunk_size, :])
return {"loss": loss, "action_pred": action_chunk}
return action_chunk

3.4 π₀:扩散动作头#

π₀(Physical Intelligence 2024)使用扩散模型作为动作头,是当前精度最高的 VLA 方案:

π₀ 核心思想:
动作空间太复杂(连续、高维、时序依赖),简单回归不够
→ 用扩散模型建模动作分布
架构:
视觉编码器: 来自 VLM (ViT-L)
LLM 骨干: 7B LLM (冻结 ❄️)
动作先验: 扩散 Transformer (DDPM/DIT 架构) (训练 🔥)
关键创新:
1. 动作先验:独立的扩散模型学习"什么动作是合理的"
2. 完整机器人:7 自由度臂 + 夹爪 + 躯干 + 视觉
3. 条件生成:文本指令 + 视觉观察 → 条件扩散过程
4. 两个流:双臂分别预测(π₀.5)
class DiffusionActionPrior(nn.Module):
"""
π₀ 的扩散动作先验。
条件扩散模型: noise → action_chunk
"""
def __init__(self, lang_dim, action_dim, chunk_size, hidden_dim=512):
super().__init__()
self.action_dim = action_dim
self.chunk_size = chunk_size
self.noise_steps = 100
# 时间步嵌入
self.time_embed = nn.Sequential(
nn.Linear(1, hidden_dim),
nn.SiLU(),
nn.Linear(hidden_dim, hidden_dim),
)
# 条件: 语言 + 视觉
self.cond_proj = nn.Linear(lang_dim, hidden_dim)
# U-Net 风格的 denoiser
self.net = nn.Sequential(
nn.Linear(chunk_size * action_dim + hidden_dim, hidden_dim * 4),
nn.SiLU(),
nn.Linear(hidden_dim * 4, hidden_dim * 4),
nn.SiLU(),
nn.Linear(hidden_dim * 4, chunk_size * action_dim),
)
# 噪声调度
self.register_buffer("betas", self.cosine_beta_schedule())
def cosine_beta_schedule(self):
"""余弦噪声调度。"""
steps = self.noise_steps + 1
s = 0.008
x = torch.linspace(0, self.noise_steps, steps)
alphas_cumprod = torch.cos((x / self.noise_steps + s) / (1 + s) * torch.pi * 0.5) ** 2
alphas_cumprod = alphas_cumprod / alphas_cumprod[0]
betas = 1 - alphas_cumprod[1:] / alphas_cumprod[:-1]
return torch.clamp(betas, 0.0001, 0.9999)
def forward(self, x_t, t, cond):
"""
Denoiser: 从噪声 x_t 中预测 noise (DDPM 目标)。
参数:
x_t: (B, chunk_size, action_dim) 带噪声的动作
t: (B,) 时间步
cond: (B, lang_dim) 条件(语言+视觉)
返回:
noise_pred: (B, chunk_size, action_dim) 预测的噪声
"""
t_embed = self.time_embed(t.float().unsqueeze(-1) / self.noise_steps)
cond_embed = self.cond_proj(cond)
x_flat = x_t.flatten(1) # (B, chunk*action_dim)
x_cat = torch.cat([x_flat, t_embed + cond_embed], dim=-1) # (B, chunk*action_dim + hidden_dim)
noise_pred = self.net(x_cat)
noise_pred = noise_pred.view(-1, self.chunk_size, self.action_dim)
return noise_pred
@torch.no_grad()
def sample(self, cond, num_inference_steps=50):
"""
DDIM 采样:从条件生成动作块。
参数:
cond: (B, lang_dim) 条件
num_inference_steps: 推理步数
返回:
action_chunk: (B, chunk_size, action_dim)
"""
B = cond.shape[0]
# 从纯噪声开始
x = torch.randn(B, self.chunk_size, self.action_dim, device=cond.device)
# DDIM 时间步
step_ratio = self.noise_steps // num_inference_steps
timesteps = (torch.arange(0, num_inference_steps) * step_ratio).flip(0)
for i, t in enumerate(timesteps):
t_tensor = torch.full((B,), t, device=cond.device, dtype=torch.long)
# 预测噪声
noise_pred = self.forward(x, t_tensor, cond)
# DDIM 采样公式
alpha_prod_t = self.alphas_cumprod[t]
alpha_prod_t_prev = self.alphas_cumprod[t - step_ratio] if t > 0 else torch.tensor(1.0)
# 去噪
x0_pred = (x - (1 - alpha_prod_t).sqrt() * noise_pred) / alpha_prod_t.sqrt()
# 估计 x_{t-1}
x = alpha_prod_t_prev.sqrt() * x0_pred + (1 - alpha_prod_t_prev).sqrt() * noise_pred
return x # (B, chunk_size, action_dim)
class PiZero(nn.Module):
"""
π₀ 完整架构。
"""
def __init__(self):
super().__init__()
# 1) VLM 骨干 (冻结)
self.vlm = load_pretrained_vlm() # e.g., CLIP ViT-L + Llama-3
# 2) π₀ 扩散动作先验
lang_dim = 4096
action_dim = 7
chunk_size = 16
self.diffusion_prior = DiffusionActionPrior(lang_dim, action_dim, chunk_size)
def forward(self, image, instruction, action=None):
"""
完整前向。
"""
# VLM 提取特征
vision_embeds, lang_embeds = self.vlm(image, instruction)
cond = vision_embeds + lang_embeds # (B, lang_dim)
if action is not None:
# 训练: 加噪,计算噪声预测损失
B, T, A = action.shape
if T > self.diffusion_prior.chunk_size:
action = action[:, :self.diffusion_prior.chunk_size]
elif T < self.diffusion_prior.chunk_size:
pad = torch.zeros(B, self.diffusion_prior.chunk_size - T, A, device=action.device)
action = torch.cat([action, pad], dim=1)
t = torch.randint(0, self.diffusion_prior.noise_steps, (B,), device=action.device)
noise = torch.randn_like(action)
alpha_prod = self.diffusion_prior.alphas_cumprod[t].view(B, 1, 1)
noisy_action = alpha_prod.sqrt() * action + (1 - alpha_prod).sqrt() * noise
noise_pred = self.diffusion_prior(noisy_action, t, cond)
loss = F.mse_loss(noise_pred, noise)
return {"loss": loss}
else:
# 推理: 扩散采样
action_chunk = self.diffusion_prior.sample(cond)
return action_chunk

3.5 三种架构对比#

维度RT-2OpenVLAπ₀
动作表示离散 token (32K bin)连续回归连续扩散
LLM 骨干PaLM-E / PaLI-XLlama-3 8BLlama-3 7B
视觉编码器ViT + PerceiverSigLIP-So400mViT-L
动作头语言头(共享)MLP扩散 U-Net
动作块1(自回归)816
推理速度慢(逐 token 自回归)中(DDIM 50步)
动作质量中等良好优秀
开源是 (Apache 2.0)
训练数据130K episodes97K hours私有大规模
可微调性高(冻结骨干)

4. 训练范式:从模仿学习到强化微调#

4.1 三阶段训练流程#

┌─────────────────────────────────────────────────────────────┐
│ 阶段 1: 视觉语言预训练 (与 VLM 相同) │
│ 目标: 让视觉编码器和 LLM 学到强大的语义理解 │
│ 数据: 图文对、网页文本、数万亿 token │
│ LLM: 训练 🔥 | 视觉编码器: 训练 🔥 │
├─────────────────────────────────────────────────────────────┤
│ 阶段 2: 机器人动作预训练 (Imitation Learning) │
│ 目标: 让 VLM 的语义能力迁移到动作预测 │
│ 数据: 示教数据 (demonstration data) │
│ - Open X-Embodiment: 100+ 机器人, 100K+ episodes │
│ - Bridge Dataset: 家庭场景 7DoF 臂 │
│ - RT-1/RT-2 数据: Google 机器人 │
│ LLM: 解冻 🔥 | 视觉编码器: 解冻 🔥 | 动作头: 训练 🔥 │
├─────────────────────────────────────────────────────────────┤
│ 阶段 3: 任务特定微调 (可选) │
│ 目标: 在目标任务上进一步优化 │
│ 数据: 目标场景的少量示教数据 │
│ 方法: 行为克隆 / DAPO / PPO │
│ 特点: 可以用 LoRA 高效微调 │
└─────────────────────────────────────────────────────────────┘

4.2 阶段 2:示教学习(Imitation Learning)#

class RobotDataset(Dataset):
"""
机器人示教数据集。
"""
def __init__(self, dataset_paths, chunk_size=8):
self.trajectories = []
for path in dataset_paths:
self.trajectories.extend(self.load(path))
self.chunk_size = chunk_size
self.normalizer = ActionNormalizer(action_dim)
def load(self, path):
"""加载轨迹数据。"""
if path.endswith(".hdf5"):
return self.load_hdf5(path)
elif path.endswith(".zarr"):
return self.load_zarr(path)
def __getitem__(self, idx):
traj = self.trajectories[idx]
# 随机采样一个起始位置
T = len(traj["observations"])
start = np.random.randint(0, max(1, T - self.chunk_size))
end = min(start + self.chunk_size, T)
obs = traj["observations"][start:end] # (T, obs_dim)
actions = traj["actions"][start:end] # (T, action_dim)
instruction = traj["language_instruction"]
# 图像处理(可能多视角)
images = [obs[i]["image"] for i in range(start, end)]
# 多视角 concat 或选择主视角
image = np.concatenate(images, axis=-1) if len(images) > 1 else images[0]
return {
"image": self.preprocess_image(image),
"instruction": instruction,
"action": torch.FloatTensor(actions),
}
def preprocess_image(self, image):
# 调整大小到 [224, 224]
# 归一化到 [0, 1]
return torch.FloatTensor(image).permute(2, 0, 1) / 255.0
def train_openx_vla(model, dataset_paths, num_epochs=10, lr=1e-4):
"""
在 Open X-Embodiment 数据集上训练 VLA。
"""
dataset = RobotDataset(dataset_paths)
dataloader = DataLoader(dataset, batch_size=32, shuffle=True, num_workers=8)
optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=0.01)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=num_epochs)
model.train()
for epoch in range(num_epochs):
epoch_loss = 0.0
for batch in tqdm(dataloader):
images = batch["image"].to(device) # (B, C, H, W)
instructions = batch["instruction"]
actions = batch["action"].to(device) # (B, T, A)
optimizer.zero_grad()
output = model(images, instructions, actions)
loss = output["loss"]
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
epoch_loss += loss.item()
scheduler.step()
print(f"Epoch {epoch}: loss={epoch_loss / len(dataloader):.4f}")
# 保存
torch.save(model.state_dict(), "openx_vla.pt")

4.3 阶段 3:强化微调(DAPO/PPO)#

模仿学习后用 RL 进一步优化:

def dapo_finetune(model, env, num_episodes=1000):
"""
DAPO (Diffusion Augmented Policy Optimization) 微调。
在模仿学习的基础上,用 RL 进一步优化策略。
"""
from torch.distributions import Normal
optimizer = torch.optim.AdamW(model.action_head.parameters(), lr=1e-5)
for episode in range(num_episodes):
# 收集轨迹
obs = env.reset()
done = False
rewards = []
states = []
while not done:
with torch.no_grad():
action = model.predict(obs) # (action_dim,)
next_obs, reward, done, info = env.step(action)
rewards.append(reward)
states.append((obs, action, reward, next_obs, done))
obs = next_obs
# 计算 GAE
returns = compute_gae(rewards, gamma=0.99, lam=0.95)
# 策略梯度更新
optimizer.zero_grad()
total_loss = 0.0
for (obs, action, reward, next_obs, done), ret in zip(states, returns):
output = model(obs["image"], obs["instruction"])
action_pred = output["action_pred"][0] # (action_dim,)
# 负对数似然(假设高斯策略)
log_prob = Normal(action_pred, std=0.1).log_prob(torch.FloatTensor(action)).mean()
loss = -log_prob * ret # 策略梯度
total_loss += loss
total_loss.backward()
optimizer.step()
if episode % 100 == 0:
print(f"Episode {episode}: avg_reward={np.mean(rewards):.2f}")

4.4 动作空间的领域随机化#

不同机器人平台的关节限制不同:

class DomainRandomization:
"""
领域随机化:增强 VLA 的跨机器人泛化能力。
"""
def __init__(self):
self.robot_configs = {
"panda": {"action_dim": 7, "limits": [(-2.9,2.9)]*7},
" WidowX": {"action_dim": 5, "limits": [(-1.6,1.6)]*5},
"UR5e": {"action_dim": 6, "limits": [(-np.pi,np.pi)]*6},
"xArm7": {"action_dim": 7, "limits": [(-2.0,2.0)]*7},
}
def sample_robot_config(self):
"""随机选择一个机器人配置。"""
import random
name = random.choice(list(self.robot_configs.keys()))
return name, self.robot_configs[name]
def adapt_action(self, action, source_robot, target_robot):
"""
把源机器人的动作映射到目标机器人。
"""
src_cfg = self.robot_configs[source_robot]
tgt_cfg = self.robot_configs[target_robot]
# 简单的维度映射(实际中更复杂)
min_dim = min(src_cfg["action_dim"], tgt_cfg["action_dim"])
adapted = action[:min_dim]
# 重新缩放到目标机器人的范围
for i in range(min_dim):
src_lo, src_hi = src_cfg["limits"][i]
tgt_lo, tgt_hi = tgt_cfg["limits"][i]
normalized = (adapted[i] - src_lo) / (src_hi - src_lo)
adapted[i] = normalized * (tgt_hi - tgt_lo) + tgt_lo
return adapted

5. 关键训练技术#

5.1 动作空间设计的工程细节#

@dataclass
class ActionSpaceConfig:
"""
VLA 动作空间的完整配置。
"""
# 关节控制 vs 末端执行器控制
control_mode: str = "joint" # "joint" | "ee_pose" | "ee_delta"
# 动作分块
chunk_size: int = 8 # 每次预测的动作步数
execution_freq_hz: float = 50.0
# 动作表示
representation: str = "continuous" # "continuous" | "discrete" | "diffusion"
# 连续动作的约束
use_tanh_squash: bool = True # tanh 把输出压缩到 [-1, 1]
use_clipping: bool = True
action_bound: float = 1.0
# 速度/加速度/位置
action_type: str = "delta" # "position" | "delta" | "velocity"
# 多臂协调 (π₀.5)
num_arms: int = 1
shared_base: bool = False # 双臂是否共享底座
def apply_action_constraints(action, config: ActionSpaceConfig):
"""
应用动作约束。
"""
if config.use_tanh_squash:
# tanh 压缩:输出在 (-1, 1),然后缩放到实际范围
action = config.action_bound * torch.tanh(action)
if config.use_clipping:
action = torch.clamp(action, -config.action_bound, config.action_bound)
return action

5.2 视觉观察处理#

class MultiViewObservation:
"""
多视角图像处理。
真实机器人通常有多个相机。
"""
def __init__(self, camera_names):
self.camera_names = camera_names
def process(self, obs_dict):
"""
处理多视角观察。
返回: 拼接后的图像 tensor (3*C, H, W)
"""
images = []
for cam_name in self.camera_names:
img = obs_dict[cam_name] # (H, W, 3)
img_tensor = torch.from_numpy(img).permute(2, 0, 1).float() / 255.0
images.append(img_tensor)
# 拼接 RGB 通道
concat = torch.cat(images, dim=0) # (3*num_cameras, H, W)
return concat
class ProprioceptiveState:
"""
本体感觉状态:关节角度、末端执行器位置、夹爪状态。
"""
def __init__(self, robot_type):
self.robot_type = robot_type
self.proprio_dims = {
"panda": 7 + 1, # 7 joints + gripper
"widowx": 5 + 1, # 5 joints + gripper
"ur5": 6 + 1, # 6 joints + gripper
}
self.dim = self.proprio_dims.get(robot_type, 7)
def encode(self, obs_dict):
"""
编码本体感觉状态为 embedding。
"""
joint_pos = torch.FloatTensor(obs_dict["joint_positions"]) # (7,)
gripper = torch.FloatTensor([obs_dict["gripper_position"]]) # (1,)
proprio = torch.cat([joint_pos, gripper]) # (8,)
# 归一化到 [-1, 1]
proprio = (proprio - self.mean) / (self.std + 1e-8)
# 投影到 LLM 维度
return self.proprio_proj(proprio) # (llm_dim,)

5.3 语言指令的格式化#

class InstructionFormatter:
"""
格式化语言指令以适配 VLA。
"""
def __init__(self, llm_name):
self.templates = {
"short": "{task}", # "pick up the cup"
"detailed": "Robot, please {task}.", # 详细描述
"goal": "Task: {task}. Observations: {obs}", # 带观察描述
"step": "Step {n}/{N}: {step}", # 分步指令
}
def format(self, instruction, template="detailed", **kwargs):
"""格式化指令。"""
tmpl = self.templates.get(template, self.templates["detailed"])
# 填入参数
formatted = tmpl.format(instruction, **kwargs)
# 特殊 token
formatted = f"<|start_of_text|><|vision|><|observation|>{formatted}<|end_of_text|>"
return formatted
def make_system_prompt(self):
"""构建系统提示(用于引导 VLA 的行为)。"""
return (
"You are a helpful robot assistant. "
"Given a visual observation and a language instruction, "
"predict the robot's next action. "
"Actions are continuous control signals for the robot arm and gripper."
)

6. 主流 VLA 模型深度对比#

6.1 RT-2 家族#

RT-2 (2023):
- 540B PaLM-E 或 5B PaLI-X 骨干
- 离散动作 (32K bins)
- 自回归生成(慢,但泛化能力强)
- 130K episodes 训练数据
- 泛化到训练中未见过的指令
RT-2-X (2023):
- 在 RT-2 基础上增加模拟数据
- 在 Sim-to-Real 迁移上表现更好
局限性:
- 推理速度慢(~1Hz,机器人控制需要 50Hz)
- 量化误差大
- 不开源

6.2 OpenVLA#

OpenVLA (2024):
- SigLIP-So400m 视觉编码器
- Llama-3 8B LLM 骨干
- 连续动作 + MLP 头
- chunk_size=8 → 支持 6.25Hz 控制
- 97K 小时真实机器人数据
关键数据 (LIBERO 基准):
- LIBERO-Spatial: 82.3%
- LIBERO-Object: 77.8%
- LIBERO-Social: 78.1%
- LIBERO-Long-Horizon: 65.4%
优势:
- 完全开源 (Apache 2.0)
- 可微调(LoRA/QLoRA)
- 推理速度快(Llama-3 8B)

6.3 π₀ / π₀.5#

π₀ (2024, Physical Intelligence):
- ViT-L 视觉编码器 + Llama-3 7B
- 扩散动作先验 (Diffusion Transformer)
- chunk_size=16 → 支持高频控制
- 家庭、厨房、办公场景
- 双臂协调(π₀.5)
关键创新:
1. 扩散模型处理复杂的动作分布
2. 两个扩散流分别处理双臂(π₀.5)
3. 可泛化到新物体和场景
4. 处理部分可观测性(遮挡物体)
π₀.5 扩展:
- 多指灵巧手控制
- 复杂物体操作(拧螺丝、折叠衣物)
- 全身控制(躯干 + 双臂)

6.4 Helix:全身 VLA#

Helix (Physical Intelligence, 2025):
- 完整 7 轴全身控制:躯干 + 双臂 + 夹爪
- 单一模型处理所有自由度
- 400K 小时数据训练
- 隐式动作表示(用 SDF/3D 点云)
关键数据:
- 厨房场景: 90%+ 成功率
- 开放世界泛化: 训练数据中未见过的物体

6.5 OpenHands-VLA#

OpenHands-VLA (2025):
- 专注于手部精细操作
- 全模态: 视觉 + 触觉 + 力反馈
- 开放式物体操控(不预设物体类别)
核心能力:
1. 拿起任意物体(未知形状/材质)
2. 精细操作(拧瓶盖、使用工具)
3. 多步骤任务(拿起→移动→放下)

6.6 模型对比总表#

模型骨干动作表示开源双手泛化能力推理速度
RT-2PaLM-E 540B离散 32K极强
OpenVLALlama-3 8B连续 MLP
π₀Llama-3 7B扩散极强
π₀.5Llama-3 7B扩散极强
Helix定制连续极强
OpenHands定制连续部分
GR00TNVIDIA扩散可扩展

7. Sim-to-Real:模拟到真实迁移#

7.1 域随机化#

class DomainRandomization:
"""
域随机化:让 VLA 学会适应不同物理参数。
"""
def __init__(self):
self.params = {
"friction": (0.3, 1.5), # 摩擦系数
"mass_scale": (0.5, 2.0), # 质量缩放
"gravity": (9.0, 10.0), # 重力
"joint_limits": (0.9, 1.1), # 关节限幅
"camera_noise": (0.0, 0.05), # 相机噪声
"lighting": (0.5, 1.5), # 光照变化
"object_color": (0.0, 1.0), # 物体颜色
}
def sample(self):
"""随机采样一组物理参数。"""
config = {}
for name, (lo, hi) in self.params.items():
config[name] = np.random.uniform(lo, hi)
return config
def apply_to_sim(self, sim_env, config):
"""把随机化参数应用到模拟环境。"""
sim_env.set_friction(config["friction"])
sim_env.set_gravity(config["gravity"])
sim_env.set_camera_noise(config["camera_noise"])

7.2 视觉域随机化#

class VisualDomainRandomization:
"""
视觉域随机化:改变模拟器的渲染外观。
让 VLA 不过度依赖特定的视觉特征。
"""
def __init__(self):
self.bg_colors = ["white", "black", "gray", "blue", "green"]
self.textures = ["smooth", "rough", "metallic"]
self.light_positions = [
[2, 2, 3], [2, -2, 3], [-2, 2, 3], [-2, -2, 3],
[0, 0, 4], [0, 2, 3], [0, -2, 3],
]
def randomize(self, renderer):
"""随机化渲染参数。"""
# 背景颜色
bg = random.choice(self.bg_colors)
renderer.set_background(bg)
# 光照
light_pos = random.choice(self.light_positions)
renderer.set_light_position(light_pos)
# 纹理
texture = random.choice(self.textures)
renderer.set_object_texture(texture)
# 相机内参
fx = np.random.uniform(500, 700)
fy = np.random.uniform(500, 700)
renderer.set_camera_intrinsics(fx, fy)

8. 基准测试与评估#

8.1 主要评估基准#

def benchmark_descriptions():
"""
VLA 评估基准说明。
"""
benchmarks = {
"LIBERO": {
"描述": "4 个子任务:Spatial, Object, Social, Long-Horizon",
"特点": "语言指令逐步复杂,测试泛化能力",
"场景": "厨房/办公室模拟器",
"成功率指标": "Task Success Rate",
},
"CALVIN": {
"描述": "4 个语言条件操控任务,ABCD 四个任务逐级加难",
"特点": "测试长-horizon 指令跟随",
"场景": "单臂机械臂模拟",
"成功率指标": "平均任务完成数",
},
"RT-1 Benchmark": {
"描述": "真实机器人 37 项任务评估",
"特点": "真实硬件,测试 sim-to-real",
"场景": "真实 7DoF 臂",
"成功率指标": "Task Success Rate",
},
"Open X-Embodiment": {
"描述": "100+ 机器人数据集的大规模评估",
"特点": "跨机器人泛化",
"场景": "模拟 + 真实",
"成功率指标": "多任务平均",
},
"Franka Kitchen": {
"描述": "厨房多步任务",
"特点": "长期规划能力",
"场景": "模拟真实厨房",
"成功率指标": "子任务完成率",
},
}
return benchmarks

8.2 评估指标详解#

def evaluation_metrics():
"""
VLA 评估指标。
"""
metrics = {
"成功率 (Success Rate)": "任务是否完成的二值指标",
"平均步数 (Avg Steps)": "完成任务所需的平均步数,越少越好",
"到达误差 (Final Distance)": "末端执行器到目标位置的最终距离",
"碰撞率 (Collision Rate)": "执行过程中发生碰撞的比率",
"动作平滑度 (Action Smoothness)": "相邻动作的一阶差分均方根",
"力反馈峰值 (Force Peak)": "最大接触力(安全指标)",
"长-horizon 任务数 (Tasks Completed)": "连续 N 个子任务完成的数量",
"CLIP Score (图像描述)": "生成动作对应视觉描述的 CLIP 相似度",
}
def action_smoothness(actions):
"""
计算动作平滑度。
"""
diffs = torch.diff(actions, dim=0) # (T-1, action_dim)
smoothness = diffs.norm(dim=-1).mean()
return smoothness.item()
return metrics

9. 完整实现:基于 OpenVLA 风格的 VLA#

import torch
import torch.nn as nn
import torch.nn.functional as F
from dataclasses import dataclass
@dataclass
class VLAConfig:
"""VLA 配置。"""
vision_model: str = "openai/clip-vit-large-patch14-336"
llm_model: str = "meta-llama/Meta-Llama-3-8B"
action_dim: int = 7
chunk_size: int = 8
lang_dim: int = 4096
vision_hidden_size: int = 1024
class VLAMLPProjector(nn.Module):
"""VLM 的模态投影器。"""
def __init__(self, vision_dim, lang_dim):
super().__init__()
self.proj = nn.Sequential(
nn.Linear(vision_dim, lang_dim),
nn.GELU(),
nn.Linear(lang_dim, lang_dim),
nn.LayerNorm(lang_dim),
)
def forward(self, x):
return self.proj(x)
class VLAActionHead(nn.Module):
"""VLA 的动作预测头(连续动作)。"""
def __init__(self, lang_dim, action_dim, chunk_size):
super().__init__()
self.chunk_size = chunk_size
self.action_dim = action_dim
self.net = nn.Sequential(
nn.Linear(lang_dim, lang_dim // 2),
nn.ReLU(),
nn.Linear(lang_dim // 2, chunk_size * action_dim),
)
def forward(self, llm_hidden):
"""
llm_hidden: (B, lang_dim) LLM 最后一层 hidden state
返回: (B, chunk_size, action_dim) 动作块
"""
out = self.net(llm_hidden) # (B, chunk_size * action_dim)
chunk = out.view(-1, self.chunk_size, self.action_dim)
return torch.tanh(chunk) # 限制在 [-1, 1]
class VLA(nn.Module):
"""
VLA 完整架构(OpenVLA 风格)。
支持图像 + 语言 → 动作块预测。
"""
def __init__(self, config: VLAConfig):
super().__init__()
self.config = config
# 1) 视觉编码器 (冻结)
from transformers import AutoModel, AutoImageProcessor
self.vision_encoder = AutoModel.from_pretrained(config.vision_model)
self.vision_encoder.requires_grad_(False)
self.image_processor = AutoImageProcessor.from_pretrained(config.vision_model)
# 2) 模态投影器 (训练)
self.projector = VLAMLPProjector(
config.vision_hidden_size,
config.lang_dim,
)
# 3) LLM 骨干 (冻结)
from transformers import AutoModelForCausalLM, AutoTokenizer
self.llm = AutoModelForCausalLM.from_pretrained(
config.llm_model,
torch_dtype=torch.float16,
device_map="auto",
trust_remote_code=True,
)
self.llm.requires_grad_(False)
self.tokenizer = AutoTokenizer.from_pretrained(config.llm_model, trust_remote_code=True)
# 4) 动作头 (训练)
self.action_head = VLAActionHead(
config.lang_dim,
config.action_dim,
config.chunk_size,
)
# 5) 特殊 token
obs_tokens = ["[ OBS ]", "[ / OBS ]"]
self.tokenizer.add_tokens(obs_tokens, special_tokens=True)
self.obs_start_id = self.tokenizer("[ OBS ]", add_special_tokens=False).input_ids[0]
self.obs_end_id = self.tokenizer("[ / OBS ]", add_special_tokens=False).input_ids[0]
# 6) 动作归一化器
self.normalizer = ActionNormalizer(config.action_dim)
def encode_image(self, image):
"""编码图像。"""
if not isinstance(image, torch.Tensor):
inputs = self.image_processor(image, return_tensors="pt")
else:
inputs = {"pixel_values": image}
inputs = {k: v.to(self.llm.device) for k, v in inputs.items()}
with torch.no_grad():
outputs = self.vision_encoder(**inputs)
vis_embeds = outputs.last_hidden_state[:, 1:, :] # 去掉 [CLS]
return self.projector(vis_embeds) # (B, N, lang_dim)
def build_multimodal_input(self, image, instruction):
"""构建多模态输入序列。"""
# 图像 tokens
vis_embeds = self.encode_image(image) # (B, N, lang_dim)
# 语言 tokens
instr_tokens = self.tokenizer(
instruction, return_tensors="pt", padding=True, truncation=True, max_length=512
).to(image.device)
lang_embeds = self.llm.get_input_embeddings()(instr_tokens["input_ids"])
# 构建序列: [OBS] + 视觉 + [/OBS] + 语言
B = image.shape[0]
obs_start = self.llm.get_input_embeddings()(
torch.tensor([self.obs_start_id]).to(image.device)
).expand(B, 1, -1)
obs_end = self.llm.get_input_embeddings()(
torch.tensor([self.obs_end_id]).to(image.device)
).expand(B, 1, -1)
multimodal_embeds = torch.cat([obs_start, vis_embeds, obs_end, lang_embeds], dim=1)
return multimodal_embeds, instr_tokens["attention_mask"]
def forward(self, image, instruction, action=None):
"""
完整前向。
参数:
image: (B, C, H, W) 图像
instruction: str 或 list[str] 语言指令
action: (B, T, action_dim) 动作序列(标签,可选)
"""
# 构建多模态输入
multimodal_embeds, attn_mask = self.build_multimodal_input(image, instruction)
# LLM 前向
with torch.no_grad():
llm_out = self.llm(
inputs_embeds=multimodal_embeds,
attention_mask=attn_mask,
output_hidden_states=True,
)
# 取最后一个 token 的 hidden state
last_hidden = llm_out.hidden_states[-1][:, -1, :] # (B, lang_dim)
# 动作预测
action_chunk = self.action_head(last_hidden) # (B, chunk, action_dim)
if action is not None:
# 训练模式
action_normed = self.normalizer.normalize(action)
# 取前 chunk_size 步
T = min(self.config.chunk_size, action_normed.shape[1])
loss = F.mse_loss(action_chunk[:, :T, :], action_normed[:, :T, :])
return {"loss": loss, "action_pred": action_chunk}
return action_chunk
@torch.no_grad()
def predict(self, image, instruction, temperature=0.0):
"""
推理:预测动作块。
"""
self.eval()
action_chunk = self.forward(image, instruction)
# 反归一化
action_denorm = self.normalizer.denormalize(action_chunk[0, 0])
# 应用安全约束
action_denorm = torch.clamp(action_denorm, -1.0, 1.0)
return action_denorm.cpu().numpy()

10. 微调实践:LoRA 高效训练#

def finetune_vla_with_lora(vla_model, dataset_path, lora_rank=64):
"""
用 LoRA 高效微调 VLA。
"""
from peft import LoraConfig, get_peft_model
# 只微调动作头和投影器
lora_config = LoraConfig(
r=lora_rank,
lora_alpha=16,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
)
# 给投影器和动作头也加 LoRA
vla_model.projector = get_peft_model(vla_model.projector, lora_config)
vla_model.action_head = get_peft_model(vla_model.action_head, lora_config)
# 冻结其他
vla_model.vision_encoder.requires_grad_(False)
vla_model.llm.requires_grad_(False)
# 训练
optimizer = torch.optim.AdamW(
list(vla_model.projector.parameters()) +
list(vla_model.action_head.parameters()),
lr=1e-4, weight_decay=0.01,
)
dataset = RobotDataset([dataset_path])
dataloader = DataLoader(dataset, batch_size=16, shuffle=True)
for epoch in range(5):
for batch in dataloader:
images = batch["image"].to(device)
instructions = batch["instruction"]
actions = batch["action"].to(device)
output = vla_model(images, instructions, actions)
loss = output["loss"]
loss.backward()
torch.nn.utils.clip_grad_norm_(vla_model.parameters(), max_norm=1.0)
optimizer.step()
optimizer.zero_grad()
print(f"Epoch {epoch}: loss={loss.item():.4f}")
# 保存 LoRA 权重
vla_model.save_pretrained("vla_lora_checkpoints")

11. VLA 的挑战与未来#

11.1 当前挑战#

def current_challenges():
"""
VLA 当前面临的主要挑战。
"""
challenges = {
"高频控制": {
"问题": "LLM 自回归推理 ~1Hz,机器人需要 50Hz",
"方案": "动作分块 (chunking) + 异步推理",
},
"Sim-to-Real 差距": {
"问题": "模拟器渲染与真实场景差异大",
"方案": "域随机化 + 视觉重照 + 课程学习",
},
"长-horizon 任务": {
"问题": "行为克隆的复合误差累积",
"方案": "分层策略 + 任务规划 + 视觉反馈修正",
},
"双手协调": {
"问题": "双机械臂的协调操控极其复杂",
"方案": "双流扩散 (π₀.5) + 接触力学建模",
},
"安全保证": {
"问题": "神经网络策略缺乏安全性证明",
"方案": "安全层 (safety layer) + 力量限制 + 碰撞检测",
},
"数据规模": {
"问题": "高质量示教数据昂贵且稀缺",
"方案": "视频学习 + 模仿 + 合成数据 + 互联网上的人类视频",
},
}
return challenges

11.2 未来趋势#

2025-2026 VLA 趋势:
1. 更大规模的预训练
- 100K+ 机器人小时数据
- 互联网视频预训练(学习人类动作)
- 合成物理引擎数据
2. 更好的泛化
- 零样本新任务泛化
- 跨机器人平台迁移
- 开放世界物体操作
3. 更快的推理
- 量化 (INT4/INT8)
- 蒸馏 (大 VLA → 小 VLA)
- 异步并行(视觉处理 + 动作生成)
4. 多模态输入
- 视觉 + 触觉 + 力反馈
- 3D 点云(Voxel/NeRF)
- 语音指令
5. 与世界模型结合
- VLA + World Model = 规划 + 执行
- 机器人作为 Agent:感知→规划→执行循环

12. 总结#

12.1 核心要点#

维度关键要点
动作空间离散(RT-2)→ 连续回归(OpenVLA)→ 扩散(π₀)
动作分块chunk_size=8-16 解决高频控制与推理延迟的矛盾
架构设计VLM 骨干 + 专用动作头(MLP/扩散)
训练范式模仿学习(主体)+ RL 微调(精修)
Sim-to-Real域随机化 + 视觉重照化
核心挑战高频控制、长-horizon、双手协调、安全保证
开源模型OpenVLA(最流行)、GR00T(Nvidia)
闭源模型π₀ / π₀.5 / Helix(Physical Intelligence,最先进)

12.2 与相关模型的关系#

VLM (视觉语言模型):
输入: 图像 + 文本 → 输出: 文本
任务: VQA, 图像描述, OCR
VLA (视觉语言动作模型):
输入: 图像 + 文本 → 输出: 动作序列
任务: 机器人操控, 物体抓取, 任务执行
VLM + 动作头 = VLA
VLM 赋予"感知和理解"能力
动作头赋予"物理执行"能力
VLA 与具身 Agent 的关系:
VLA 是具身 Agent 的"动作生成器"组件
完整 Agent = VLA (动作) + 世界模型 (预测) + 规划器 (决策)

12.3 一句话总结#

VLA 的本质是把”理解场景”和”执行动作”这两个能力统一到一个模型里——通过视觉编码器理解图像,通过语言模型理解任务,通过动作头输出高频、连续、平滑的机器人控制信号。从 RT-2 的离散 token 到 π₀ 的扩散先验,VLA 正在从”能用”走向”好用”,成为具身智能的核心组件。

12.4 推荐资源#

论文:
- RT-2 (Brohan et al., 2023): "RT-2: Vision-Language-Action Models"
- PaLM-E (Driess et al., 2023): "PaLM-E: An Embodied Multimodal Language Model"
- OpenVLA (Zhao et al., 2024): "OpenVLA: Open-Source Vision-Language-Action Model"
- π₀ (Blackwell et al., 2024): "π₀: A Vision-Language-Action Flow Model"
- Helix (Physical Intelligence, 2025): "System 2"
代码:
- openvla/openvla (OpenVLA 官方实现)
- physical-intelligence/PiZero (π₀)
- NVIDIA/GR00T (NVIDIA 机器人基础模型)
数据集:
- Open X-Embodiment (100+ 机器人, 1M+ episodes)
- Bridge Dataset (家庭场景)
- RT-1/RT-2 数据 (Google 机器人)
- LIBERO (语言条件操控)
- CALVIN (长-horizon 任务)

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

VLA 架构深度剖析:视觉语言动作模型与机器人操控
https://aiattnstudio.link/posts/vision-language-action/
作者
Federico
发布于
2026-07-16
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author

Federico

AI Research Lab

Hello, I'm Federico.

关于实验室 / About
公告

欢迎来到Federico的个人博客

分类
标签
站点统计
57文章
7分类
404标签