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"
@dataclassclass ActionSpec: """ 动作规范。 """ space: str action_dim: int frequency_hz: float chunk_size: int # 每次预测的动作步数| 类型 | 维度 | 特点 | 典型模型 |
|---|---|---|---|
| 离散动作 | 低 (~几十) | 直接预测动作 token ID | RT-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 倍,控制平滑@dataclassclass 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 actions3.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_chunk3.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_chunk3.5 三种架构对比
| 维度 | RT-2 | OpenVLA | π₀ |
|---|---|---|---|
| 动作表示 | 离散 token (32K bin) | 连续回归 | 连续扩散 |
| LLM 骨干 | PaLM-E / PaLI-X | Llama-3 8B | Llama-3 7B |
| 视觉编码器 | ViT + Perceiver | SigLIP-So400m | ViT-L |
| 动作头 | 语言头(共享) | MLP | 扩散 U-Net |
| 动作块 | 1(自回归) | 8 | 16 |
| 推理速度 | 慢(逐 token 自回归) | 快 | 中(DDIM 50步) |
| 动作质量 | 中等 | 良好 | 优秀 |
| 开源 | 否 | 是 (Apache 2.0) | 否 |
| 训练数据 | 130K episodes | 97K 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 adapted5. 关键训练技术
5.1 动作空间设计的工程细节
@dataclassclass 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 action5.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-2 | PaLM-E 540B | 离散 32K | 否 | 否 | 极强 | 慢 |
| OpenVLA | Llama-3 8B | 连续 MLP | 是 | 否 | 强 | 快 |
| π₀ | Llama-3 7B | 扩散 | 否 | 否 | 极强 | 中 |
| π₀.5 | Llama-3 7B | 扩散 | 否 | 是 | 极强 | 中 |
| Helix | 定制 | 连续 | 否 | 是 | 极强 | 快 |
| OpenHands | 定制 | 连续 | 部分 | 是 | 强 | 中 |
| GR00T | NVIDIA | 扩散 | 是 | 可扩展 | 强 | 中 |
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 benchmarks8.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 metrics9. 完整实现:基于 OpenVLA 风格的 VLA
import torchimport torch.nn as nnimport torch.nn.functional as Ffrom dataclasses import dataclass
@dataclassclass 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 challenges11.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 + 动作头 = VLAVLM 赋予"感知和理解"能力动作头赋予"物理执行"能力
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 任务)文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!

