【无标题AGI关键拼图!(附实现代码)智驾传奇团队再出手:UniVLA 打造机器人通用行动指南】

智能体如何在复杂多变的环境中高效决策与行动,始终是研究者们不懈探索的核心命题。继智驾领域备受瞩目的 UniAD 将感知、预测、规划一网打尽,展现了强大的端到端能力后,李弘扬老师所领导的OpenDriveLab不久前又提出了机器人领域的“通用行动指南”——UniVLA。

©️【深蓝具身智能】

UniVLA 是一个统一的视觉-语言-行动框架,支持在不同环境下进行策略学习,以无监督的方式推导出以任务为中心的潜在动作,并利用来自任意实体和视角的数据(无需动作标签),在经过大规模视频预训练后,实现了一种跨实体的通用策略,在结合低成本的动作解码器,可以轻松地将UniVLA部署到各种机器人上。与OpenVLA相比,UniVLA在操作和导航任务上性能均有提升。

图片

本篇文章,我们将深度解析这一实现过程的三个关键阶段:潜在动作学习、通用策略预训练,以及部署后训练,并配合复现代码深入解读这一模型。

我们开设此账号,想要向各位对【具身智能】感兴趣的人传递最前沿最权威的知识讯息外,也想和大家一起见证它到底是泡沫还是又一场热浪?

欢迎关注【深蓝具身智能】

图片

方法说明

首先,介绍UniVLA模型的两个关键阶段。

 以任务为中心的潜在动作学习

图片

▲图1 | 潜在动作模型的两阶段训练流程©️【深蓝具身智能】编译

class DINO_LAM(LightningModule):    """    A latent action model operates at the DINO latent space    """    def __init__(            self,            ...    ) -> None:        super(DINO_LAM, self).__init__()        assert stage in ['stage-1', 'stage-2']         lam = UncontrolledDINOLatentActionModel if stage == 'stage-1' else ControllableDINOLatentActionModel
  • 代码2:

attention_mask是任务指令的码本,lang_embed是任务指令嵌入,将视频帧、指令嵌入、指令码本输入vq_encode进行VQ-VAE量化编码,然后通过decode解码后得到重建后的图像帧和潜在动作。

(这里以UncontrolledDINOLatentActionModel的forward进行说明,ControllableDINOLatentActionModel的forward大致相似)

class UncontrolledDINOLatentActionModel(nn.Module):    def forward(self, batch: Dict) -> Dict:        ...        lang_embed, attention_mask = self.encode_text(batch["task_instruction"])        lang_embed = self.lang_proj(lang_embed)        attention_mask = torch.cat([torch.ones((B, self.num_codes + (H // self.patch_size)**2)).to(self.device),                                    attention_mask],                                    dim = -1)         outputs = self.vq_encode(batch["videos"], repeat(lang_embed, 'b l d -> b T l d', T=T), attention_mask.repeat(T, 1))         video_patches = self.patch_up(outputs["patches"][:, :-1])        action_patches = self.action_up(outputs["z_q"])        video_action_patches = torch.cat([action_patches, video_patches], dim=2)         # Decode        video_recon = self.decoder(video_action_patches, outputs['lang_emb'], attention_mask)        video_recon = video_recon[:, :, self.num_codes: self.num_codes + video_patches.shape[2]]        ...    
  • 代码3:

outputs=self.lam(batch),这一句将拿到解码器重建后的结果,和真值进行比较,得到训练时的总损失。

class DINO_LAM(LightningModule):    def shared_step(self, batch: Dict) -> Tuple:        # batch: keys['videos', 'task_instruction', 'action', 'dataset_names']         outputs = self.lam(batch)        gt_future_frames = outputs["target"]         # Compute loss        mse_loss = ((gt_future_frames - outputs["recon"]) ** 2).mean()        q_loss = ((outputs["emb"].detach() - outputs["z"]) ** 2).mean()        commit_loss = ((outputs["emb"] - outputs["z"].detach()) ** 2).mean()         loss = mse_loss + q_loss + self.vq_beta * commit_loss        ...

 通用策略的预训练

图片

▲图2 | 通用策略的框架 ©️【深蓝具身智能】编译

  • 通用策略骨干网络:

使用Prismatic-7B(该架构集成了由SigLip和DINOv2融合的视觉编码器、视觉嵌入与语言模态对齐的投影层,以及LLaMA-2大型语言模型)

class PrismaticVLM(VLM):    def forward(        self,        ...    ) -> CausalLMOutputWithPast:        """Run a forward pass through the VLM, returning a CausalLMOutputWithPast instance (contains loss)."""        ...        # Run Visual Feature Extraction        with torch.set_grad_enabled(self.vision_backbone_requires_grad):            if isinstance(pixel_values, dict):                patch_features = self.vision_backbone({k: pixel_values[k][multimodal_indices] for k in pixel_values})            else:                patch_features = self.vision_backbone(pixel_values[multimodal_indices])         # Projection Logic :: [bsz, num_patches, llm_embed_dim] =>> num_patches = (2 *) (256 + 1) for ViT-L + CLS        projected_patch_embeddings = self.projector(patch_features)        projected_patch_attention_mask = None        if attention_mask is not None:            projected_patch_attention_mask = torch.full(                (projected_patch_embeddings.shape[0], projected_patch_embeddings.shape[1]),                True,                dtype=attention_mask.dtype,                device=attention_mask.device,            )         # Get Input Embeddings from LLM Backbone :: [bsz, input_seq_len, llm_embed_dim]        input_embeddings = self.llm_backbone.embed_input_ids(input_ids)         # Build Multimodal Embeddings (and build resulting attention mask)        multimodal_embeddings = torch.cat(            [                input_embeddings[multimodal_indices, :1, :],                projected_patch_embeddings,                input_embeddings[multimodal_indices, 1:, :],            ],            dim=1,        )        multimodal_attention_mask = None        if attention_mask is not None:            multimodal_attention_mask = torch.cat(                [                    attention_mask[multimodal_indices, :1],                    projected_patch_attention_mask,                    attention_mask[multimodal_indices, 1:],                ],                dim=1,            )         # [Contract] We assume the first token of `labels` (associated with <BOS>) is already marked as "IGNORE"        #   => We'll ignore the per-token outputs for each of the patch embeddings as well!        multimodal_labels = None        if labels is not None:            projected_patch_labels = torch.full(                (projected_patch_embeddings.shape[0], projected_patch_embeddings.shape[1]),                IGNORE_INDEX,                dtype=labels.dtype,                device=labels.device,            )            multimodal_labels = torch.cat(                [labels[multimodal_indices, :1], projected_patch_labels, labels[multimodal_indices, 1:]], dim=1            )         # === Add Unimodal Handling ===         # Create Fused Embeddings, Attention Mask, and Labels by Merging with "unimodal" Inputs (if applicable)        unimodal_indices = torch.tensor(            [idx for idx in range(len(input_ids)) if idx not in multimodal_indices],            dtype=torch.long,            device=multimodal_indices.device,        )         # No "unimodal" data --> Fused == Multimodal        if len(unimodal_indices) == 0:            fused_embeddings = multimodal_embeddings            fused_attention_mask = multimodal_attention_mask            fused_labels = multimodal_labels         else:            # Otherwise --> Merge w/ unimodal data             # This doesn't matter --> but in the "normal" case this is the embedding of the <PAD> token            #   => NOTE :: Verified that `zeros/randn/empty/<PAD> embedding` all return the same result!            unimodal_embeddings_pad = torch.zeros(                (len(unimodal_indices), projected_patch_embeddings.shape[1], input_embeddings.shape[2]),                dtype=input_embeddings.dtype,                device=input_embeddings.device,            )            unimodal_attention_pad = torch.full(                (len(unimodal_indices), projected_patch_embeddings.shape[1]),                False,                dtype=attention_mask.dtype,                device=attention_mask.device,            )            unimodal_labels_pad = torch.full(                (len(unimodal_indices), projected_patch_embeddings.shape[1]),                IGNORE_INDEX,                dtype=labels.dtype,                device=labels.device,            )             unimodal_embeddings = torch.cat([input_embeddings[unimodal_indices], unimodal_embeddings_pad], dim=1)            unimodal_attention_mask = torch.cat([attention_mask[unimodal_indices], unimodal_attention_pad], dim=1)            unimodal_labels = torch.cat([labels[unimodal_indices], unimodal_labels_pad], dim=1)             # Create "Fused" Tensors by Stacking Multimodal & Unimodal            fused_embeddings = torch.vstack([multimodal_embeddings, unimodal_embeddings])            fused_attention_mask = torch.vstack([multimodal_attention_mask, unimodal_attention_mask])            fused_labels = torch.vstack([multimodal_labels, unimodal_labels])         # Run LLM Forward --> returns CausalLMOutputWithPast!        return self.llm_backbone(            input_ids=None,            attention_mask=fused_attention_mask,            position_ids=None,            past_key_values=past_key_values,            inputs_embeds=fused_embeddings,            labels=fused_labels,            use_cache=use_cache,            output_attentions=output_attentions,            output_hidden_states=output_hidden_states,            return_dict=return_dict,        )

 部署后训练

  • 潜在动作解码器:

在下游任务的部署阶段,预训练的通用策略预测的下一个潜在动作是与实体无关的,为了弥合潜在动作与任务可执行行为之间的差距,需要额外运用一个动作解码器,即潜在动作解码,具体说明如下:

先通过多头注意力池化将视觉特征嵌入聚合成单一的标记,作为后续潜在动作查询的令牌。这一过程可以表示为:

  • 代码说明:潜在动作编码器相关代码实现

class ActionDecoder(torch.nn.Module):    def __init__(self, window_size = 12, hidden_dim = 512):        super().__init__()        self.latent_action_pool = MAPBlock(n_latents = 1, vis_dim = 4096, embed_dim = hidden_dim, n_heads = hidden_dim // 64)        self.visual_pool = MAPBlock(n_latents = 1, vis_dim = 4096, embed_dim = hidden_dim, n_heads = hidden_dim // 64)         self.proj = nn.Sequential(                                nn.Linear(hidden_dim, 7 * window_size),                                nn.Tanh(),                    )     def forward(self, latent_action_tokens, visual_embed):        visual_embed = self.visual_pool(visual_embed)        latent_action_tokens = latent_action_tokens[:, -4:]        action_token = self.latent_action_pool(latent_action_tokens, init_embed = visual_embed)         action = self.proj(action_token)         return action
  • 从历史输出中学习:

UniVLA提出利用历史潜在动作来促进机器人控制中的决策:即在每次时间步的输入提示中加入过去的动作。这为机器人策略建立了反馈循环,使策略能够从自己的决策中学习并适应动态环境。

为了实现这种方法,在使用潜在动作模型时,需要对历史动作进行标注。这些标注的动作随后被映射到LLaMA的分词词汇表中,并附加到任务指令上。

在后训练阶段,将历史动作输入整合到模型训练中,赋予模型上下文学习的能力。

在推理时,除了初始步骤外,每个步骤的输入提示中都加入前一步的历史潜在动作(编码为N=4的标记)。

  • 代码说明:如果想要喂进/预测多步action,可以修改window_size

vla_dataset = RLDSDataset(    cfg.data_root_dir,    cfg.dataset_name,    batch_transform,    resize_resolution=tuple(wrapped_model.module.vla.config.image_sizes),    shuffle_buffer_size=cfg.shuffle_buffer_size,    image_aug=cfg.image_aug,    window_size=cfg.window_size + 1,        # for constructing history latent actions    training_phase='post-training',)

图片

实验

  LIBERO基线测试

图片

▲表1 |  在四个评估套件中的LIBERO基准测试结果。©️【深蓝具身智能】编译

说明:在LIBERO-Spatial、LIBERO-Object、LIBERO-Goal、LIBERO-Long这4个基准测试上,将UniVLA和现有的基线方法(LAPA、Octo、MDT、OpenVLA、MaIL)进行比较。

可以发现,虽然UniVLA仅在有限数据集——Bridge-V2和无动作人体视频数据(分别表示为 “Bridge” 和 “Human”)上进行训练,但在所有基准测试任务中,UniVLA的性能表现都是最好的。

  VLN-CE R2R基线测试

图片

▲图3 |  成功率比较

说明:在VLN-CE R2R基准上进行测试,将UniVLA和现有的导航基线方法(Seq2Seq、CMA、LLaVA-Nav、OpenVLA、NaVid)进行比较。

可以发现,仅使用单帧 RGB输入,UniVLA的性能与整合了全部历史观测信息的导航模型NaVid相当,且成功率显著优于OpenVLA。

 真实机器人实验

图片

▲图4 | 真机实验

说明:设计了4种不同的任务:“存放螺丝刀”“清洁砧板”“折叠毛巾两次” 和 “搭建汉诺塔”,将UniVLA和现有的基线方法(Diffusion Policy、OpenVLA、LAPA)进行比较。

图片

可以发现,UniVLA 的表现优于先前的最先进方法,且平均成功率提升 36.7%,所有任务的平均得分达0.68。

  潜在动作实验

图片

▲表2 | 不同潜在动作的LIBERO性能

说明:在Ego4D数据集(该数据集包含多样化和任务无关的人类动作视频)上使用不同潜在动作进行预训练,然后在LIBERO上测试UniVLA效果。

可以发现,UniVLA通过任务中心的潜在动作建模,实现了跨领域语义一致性与动态解耦能力的平衡,从而提升了在未知场景中的泛化性。

  消融实验

图片

▲图4 | 数据效率

说明:图片展示了数据量对模型效果的影响;UniVLA在不同数据集比例(10%、20%、50% 和完整数据集)下的成功率。

相较于其他先进的基线方法,UniVLA不需要大量专家演示即可高度适应未知环境。

图片

▲表3 | 解码器的消融实验

说明:图片展示了解码器对模型效果的影响。

  • 自回归(Auto-regressive):采用OpenVLA和LAPA 的方法,以自回归方式在离散化动作区间上顺序预测动作。

  • 无视觉(w/o visual):指视觉嵌入未被用作解码潜在动作的查询输入。

实验表明,通过视觉特征增强的动作解码器效果最好。

图片

▲表4 | 历史动作的消融实验

说明:在提示中使用历史步骤的潜在动作,可以显著提升模型性能。

图片

结论

UniVLA 聚焦 “跨环境通用策略学习”,其核心创新在于通过无监督学习机制,从任意实体和视角的视频数据中(无需动作标签)推导出 “以任务为中心的潜在动作”。

区别于传统依赖大量标注数据的方法,UniVLA 通过大规模视频预训练,构建了跨实体的通用策略 —— 即无论场景是机器人操作、导航还是其他任务,模型能基于任务目标自主生成动作逻辑,再结合低成本动作解码器,实现对不同机器人硬件的轻量化部署。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值