资讯动态

视觉语言动作模型VLA实战:从模型选型到真机抓取成功率提升

发布时间:2026/10/14 14:10:53 来源:尧图企业网站定制
简介这份代码包面向自动驾驶与机器人方向的开发者及AI学习者聚焦视觉语言动作模型VLA这一具身智能前沿技术帮助读者理解如何融合视觉识别与语言指令完成场景理解、任务规划与动作执行。资源共3个文件包含1个inscode工程配置、1个html页面与1个gitignore忽略规则压缩包约7KB体量轻巧便于快速导入查看与二次修改。内容围绕VLA的典型结构展开涉及视觉编码器、语言模型与策略模块的多模态融合思路并关联RT-2、GR00T N1、Helix等代表性项目以及Wayve、理想、小鹏在自动驾驶领域的落地探索可作为理解端到端统一训练与跨领域泛化的入门参考。目前已有358人学习下载适合希望快速建立VLA整体认知、梳理技术脉络并动手实践的中高级开发者。1. 视觉语言动作模型VLA到底在解决什么从「看得懂」到「动得了」的那一步机械臂抓取任务里有个经典翻车场景视觉模型准确识别出桌面上有个红色杯子语言指令也解析成「把杯子拿起来」但机械臂伸过去的角度偏了 3 厘米夹爪直接撞翻杯子。问题不在感知也不在理解而在从「知道要做什么」到「知道每个关节该转多少度」之间缺了一层能把视觉、语言、动作统一到同一个表征空间的模型。视觉语言动作模型VLA就是补这一层的。它把图像观测、自然语言指令和机器人动作序列放进同一个 Transformer 里训练输出的是末端执行器的位姿增量或关节角度而不是一句「我看到了杯子」。适合谁看做机器人抓取、具身智能、多模态控制的工程师以及想把大模型能力落到真实硬件上的开发者。这一章先把 VLA 的定位讲清楚后面几章直接上代码和参数。2. VLA 的模型结构选型一个模型还是两个模型代码里怎么拆2.1 视觉语言动作模型是一个模型还是两个模型这是被问得最多的问题答案取决于你选的架构路线。目前主流做法分两类第一类是端到端单模型。视觉编码器ViT 或 ResNet、语言编码器T5 或 LLaMA 的 tokenizer embedding、动作解码头全部塞进一个 Transformer用动作 token 或连续回归头输出。RT-2 走的就是这条路把动作离散化成 token和文本 token 一起做自回归预测。优点是视觉、语言、动作的注意力可以跨模态交互指令跟随能力强缺点是训练数据量大动作 token 的离散化会损失精度。第二类是双阶段松耦合。先用一个视觉语言模型VLM做场景理解和目标定位输出目标物体的 6D 位姿或抓取点再用一个独立的动作策略网络比如 ACT、Diffusion Policy根据位姿生成动作序列。优点是每个模块可以单独调试和替换VLM 用现成的开源权重就行缺点是两阶段之间有信息损失VLM 输出的位姿误差会直接传给策略网络。我一般会这样选如果你的任务指令变化多、需要理解「把左边那个比杯子大的东西拿过来」这种复杂语义走端到端如果任务固定、只是抓取点变化双阶段更快落地。代码上端到端的关键是把动作序列对齐到 token 维度双阶段的关键是定义好位姿接口。2.2 用 PyTorch 搭一个最小 VLA 前向通路下面这段代码展示端到端 VLA 的核心前向逻辑图像过 ViT 得到 patch embedding语言指令过 tokenizer 和 embedding 层两者拼接后送入 Transformer最后用动作头回归 7 维动作3 维位置增量 3 维旋转增量 1 维夹爪开合。import torch import torch.nn as nn from transformers import ViTModel, T5Tokenizer, T5EncoderModel class MinimalVLA(nn.Module): def __init__(self, action_dim7, hidden_dim512): super().__init__() # 视觉编码器ViT 输出 768 维 patch 特征 self.vision_encoder ViTModel.from_pretrained(google/vit-base-patch16-224) # 语言编码器T5 encoder 输出 512 维 token 特征 self.tokenizer T5Tokenizer.from_pretrained(t5-small) self.lang_encoder T5EncoderModel.from_pretrained(t5-small) # 跨模态融合把视觉和语言特征投影到同一维度 self.vision_proj nn.Linear(768, hidden_dim) self.lang_proj nn.Linear(512, hidden_dim) # 动作解码头Transformer 编码器 线性回归 encoder_layer nn.TransformerEncoderLayer(d_modelhidden_dim, nhead8, batch_firstTrue) self.fusion nn.TransformerEncoder(encoder_layer, num_layers4) self.action_head nn.Sequential( nn.Linear(hidden_dim, 256), nn.ReLU(), nn.Linear(256, action_dim) # 输出 7 维动作 ) def forward(self, images, instructions): # images: [B, 3, 224, 224] # instructions: list of str, 长度 B vision_feat self.vision_encoder(images).last_hidden_state # [B, 197, 768] vision_feat self.vision_proj(vision_feat) # [B, 197, 512] tokens self.tokenizer(instructions, return_tensorspt, paddingTrue, truncationTrue) lang_feat self.lang_encoder(**tokens).last_hidden_state # [B, L, 512] lang_feat self.lang_proj(lang_feat) # [B, L, 512] # 拼接视觉和语言 token送入融合 Transformer fused torch.cat([vision_feat, lang_feat], dim1) # [B, 197L, 512] fused self.fusion(fused) # [B, 197L, 512] # 取平均池化作为全局表征 global_feat fused.mean(dim1) # [B, 512] action self.action_head(global_feat) # [B, 7] return action逻辑说明视觉编码器输出 197 个 token1 个 cls 196 个 patch语言编码器输出 L 个 token拼接后总序列长度 197L。融合 Transformer 让视觉 token 和语言 token 做 cross-attention动作头从全局池化特征回归动作。参数说明action_dim7对应末端执行器的 6D 位姿增量加夹爪开合hidden_dim512是融合层的统一维度显存不够可以降到 256num_layers4是融合层数层数越多跨模态交互越充分但训练越慢。注意这个最小实现没有做动作序列的时序建模实际部署时需要在动作头前加 LSTM 或 Transformer decoder 来输出未来 T 步的动作序列。2.3 双阶段方案的接口定义与代码骨架如果你选双阶段VLM 部分可以直接调现成的视觉语言模型关键是定义好输出接口。下面是一个双阶段骨架VLM 输出目标物体的 6D 位姿策略网络根据位姿和当前关节状态生成动作。class TwoStageVLA: def __init__(self, vlm_model, policy_model): self.vlm vlm_model # 视觉语言模型输出目标位姿 self.policy policy_model # 动作策略网络输入位姿关节状态 def predict_pose(self, image, instruction): # VLM 前向输出目标物体的 6D 位姿 [x, y, z, rx, ry, rz] pose self.vlm(image, instruction) # [B, 6] return pose def predict_action(self, pose, joint_state): # 策略网络前向输入目标位姿和当前关节状态输出动作序列 # joint_state: [B, 7] 当前 7 个关节角度 action_seq self.policy(pose, joint_state) # [B, T, 7] return action_seq逻辑说明predict_pose负责「看懂并定位」predict_action负责「动得了」。两个方法之间的接口就是 6 维位姿向量。参数说明pose的 6 维分别是世界坐标系下的 xyz 和欧拉角joint_state是当前机械臂的关节角度7 自由度机械臂就是 7 维action_seq的 T 是预测的未来步数一般取 8 到 16 步。注意双阶段方案里 VLM 的位姿精度直接决定最终抓取成功率建议在 VLM 输出后加一个位姿精修网络用点云做 ICP 对齐。3. VLA 训练数据准备从遥操作采集到 HDF5 格式转换3.1 数据采集的三种方式和适用场景VLA 训练数据不是随便拍几张照片就行需要「图像-指令-动作」三元组对齐。常见采集方式有三种第一种是遥操作采集。操作员用主手或 VR 手柄控制从手系统同步记录相机图像、操作员语音指令和从手关节角度。这种方式数据质量最高因为动作是真实执行出来的但采集速度慢一小时大概能采 50 到 100 条轨迹。第二种是仿真环境生成。在 MuJoCo 或 Isaac Sim 里用脚本控制机械臂执行任务自动记录图像和动作。优点是速度快、成本低缺点是仿真到现实的迁移有 gap需要做域随机化。第三种是人类视频标注。从公开视频里提取手部动作映射到机械臂关节空间。这种方式数据量大但噪声也大适合做预训练不适合做精细抓取。我一般会这样组合仿真数据做预训练遥操作数据做微调。仿真数据量可以是遥操作的 10 倍以上但微调阶段必须用真实数据。3.2 把采集数据转成 HDF5 格式的完整脚本VLA 训练框架比如 ACT、Diffusion Policy通常要求 HDF5 格式每个 episode 存成一个 group里面包含observations/images、observations/qpos、action三个数据集。下面是把原始采集数据转成 HDF5 的脚本import h5py import numpy as np import cv2 import os def convert_to_hdf5(raw_data_dir, output_path, num_episodes): raw_data_dir: 原始数据目录每个 episode 一个子文件夹 output_path: 输出的 hdf5 文件路径 num_episodes: episode 数量 with h5py.File(output_path, w) as f: for ep in range(num_episodes): ep_dir os.path.join(raw_data_dir, fepisode_{ep:04d}) # 读取图像序列每帧一个 jpg 文件 img_files sorted([x for x in os.listdir(ep_dir) if x.endswith(.jpg)]) images [] for img_file in img_files: img cv2.imread(os.path.join(ep_dir, img_file)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, (224, 224)) # 统一分辨率 images.append(img) images np.array(images, dtypenp.uint8) # [T, 224, 224, 3] # 读取关节角度和动作 qpos np.load(os.path.join(ep_dir, qpos.npy)) # [T, 7] action np.load(os.path.join(ep_dir, action.npy)) # [T, 7] # 写入 hdf5 grp f.create_group(fepisode_{ep:04d}) grp.create_dataset(observations/images, dataimages, compressiongzip) grp.create_dataset(observations/qpos, dataqpos) grp.create_dataset(action, dataaction) # 存储语言指令 with open(os.path.join(ep_dir, instruction.txt), r) as f_in: instruction f_in.read().strip() grp.attrs[instruction] instruction print(f转换完成共 {num_episodes} 个 episode输出到 {output_path}) # 调用示例 convert_to_hdf5(./raw_data, ./vla_dataset.hdf5, num_episodes100)逻辑说明每个 episode 的图像序列按帧读取、统一 resize 到 224x224、转成 RGB 通道顺序。qpos是当前关节角度action是目标关节角度或位姿增量两者维度必须一致。语言指令存在 group 的 attribute 里训练时按 episode 读取。参数说明compressiongzip可以压缩图像数据100 个 episode 大约能从 2GB 压到 800MBnum_episodes要和实际子文件夹数量一致否则会报 FileNotFoundError。注意图像 resize 时保持宽高比一致否则视觉编码器的位置编码会错位如果原始图像不是正方形建议先中心裁剪再 resize。3.3 数据增强的三个必调参数VLA 训练数据通常不够需要做增强。但机器人数据的增强和图像分类不一样动作标签必须跟着图像变换一起变。常见做法是随机裁剪裁剪比例 0.8 到 1.0裁剪后 resize 回 224。动作标签不需要变因为裁剪不改变空间位置关系。颜色抖动亮度、对比度、饱和度各扰动 0.2。动作标签不变。随机旋转旋转角度 -5 到 5 度。动作标签需要做对应的旋转矩阵变换否则抓取角度会偏。我一般会把颜色抖动和随机裁剪打开随机旋转只在抓取圆形物体时用。旋转增强如果没做动作标签变换训练出来的模型抓取角度会系统性偏移这是血泪经验。4. VLA 训练与推理的避坑排查从 loss 不降到真机抓空4.1 训练 loss 不下降的四个原因现象训练了 50 个 epoch动作回归的 MSE loss 一直在 0.1 附近震荡不往下走。原因一动作标签没有归一化。关节角度范围可能是 -180 到 180 度而位置增量是 -0.01 到 0.01 米量纲差了两个数量级MSE 会被大数值主导。解决对每个动作维度做 min-max 归一化到 [-1, 1]推理时再反归一化。原因二视觉编码器没有冻结。ViT 的预训练权重在机器人数据上微调时如果学习率设得和动作头一样大视觉特征会被破坏。解决视觉编码器学习率设为动作头的 1/10或者前 10 个 epoch 直接冻结。原因三语言指令没有区分度。如果所有训练数据的指令都是「抓取物体」语言编码器学不到任何有用信息反而引入噪声。解决确保指令有多样性至少包含物体颜色、位置、形状的描述。原因四batch size 太小。VLA 的跨模态对齐需要足够的负样本batch size 小于 16 时对比学习效果很差。解决用梯度累积模拟大 batch或者换更大的显存。4.2 真机推理时抓空的三个排查方向现象仿真环境里成功率 90%部署到真机上抓取成功率不到 30%机械臂经常伸到目标旁边就闭合夹爪。原因一相机标定误差。仿真里相机位姿是精确已知的真机上相机外参有标定误差导致视觉定位偏移。解决用棋盘格重新标定相机外参标定后做一次手眼协调验证让机械臂末端去碰一个已知位置的标记点看误差是否在 5mm 以内。原因二动作频率不匹配。训练时动作是 10Hz 采集的推理时控制循环跑在 50Hz动作被重复执行了 5 次导致机械臂走过头。解决推理时加一个动作队列按训练频率出队执行或者把推理频率对齐到训练频率。原因三光照变化。训练数据是在固定灯光下采集的真机部署时环境光变了视觉编码器提取的特征漂移。解决训练时加颜色抖动增强推理时如果环境光不可控加一个直方图均衡化预处理。4.3 动作输出抖动导致机械臂震颤现象机械臂在执行 VLA 输出的动作时高频抖动像在「抽搐」。原因动作头输出的是逐帧独立预测相邻两帧的动作没有时序平滑噪声被放大。解决在动作头后加一个低通滤波器或者用 ACT 的 action chunking 机制一次预测未来 16 步动作取前 8 步执行后 8 步丢弃下次推理时重叠 8 步。这样动作序列天然平滑。参数上低通滤波器的截止频率设成控制频率的 1/5 到 1/3比如 50Hz 控制循环截止频率设 10 到 15Hz。5. VLA 进阶用 action chunking 和时序集成把抓取成功率从 60% 拉到 85%5.1 action chunking 的实现和参数选择action chunking 的核心思想是一次预测多步动作而不是逐帧预测。这样做有两个好处一是减少推理次数降低延迟二是动作序列内部有时序一致性不会逐帧抖动。实现上把动作头的输出维度从action_dim改成action_dim * chunk_size推理时 reshape 成[B, chunk_size, action_dim]。class ChunkedActionHead(nn.Module): def __init__(self, hidden_dim512, action_dim7, chunk_size16): super().__init__() self.chunk_size chunk_size self.action_dim action_dim self.head nn.Sequential( nn.Linear(hidden_dim, 512), nn.ReLU(), nn.Linear(512, action_dim * chunk_size) # 一次输出 chunk_size 步动作 ) def forward(self, global_feat): # global_feat: [B, 512] out self.head(global_feat) # [B, action_dim * chunk_size] out out.view(-1, self.chunk_size, self.action_dim) # [B, chunk_size, action_dim] return out逻辑说明chunk_size16表示一次预测未来 16 步动作。推理时取前 8 步执行后 8 步作为下一次推理的参考。参数说明chunk_size太小小于 4起不到平滑作用太大大于 32会导致动作更新不及时一般取 8 到 16。注意训练时动作标签也要按 chunk 切分每个样本的标签是未来 16 步的动作序列不是单步动作。5.2 时序集成temporal ensemble的加权策略action chunking 有个问题相邻两次推理的重叠部分动作可能不一致直接切换会导致机械臂顿挫。时序集成用加权平均来平滑重叠部分的动作。具体做法是维护一个动作缓冲区每次推理输出 16 步动作后按指数衰减权重叠加到缓冲区里。import numpy as np class TemporalEnsemble: def __init__(self, chunk_size16, decay0.5): self.chunk_size chunk_size self.decay decay self.buffer None # 动作缓冲区 def update(self, new_chunk): # new_chunk: [chunk_size, action_dim] if self.buffer is None: self.buffer new_chunk.copy() else: # 指数衰减权重越靠前的动作权重越高 weights np.array([self.decay ** i for i in range(self.chunk_size)]) weights weights / weights.sum() # 重叠部分加权平均 overlap min(len(self.buffer), self.chunk_size) self.buffer[:overlap] ( weights[:overlap, None] * new_chunk[:overlap] (1 - weights[:overlap, None]) * self.buffer[:overlap] ) # 追加新动作 if len(self.buffer) self.chunk_size: self.buffer np.concatenate([self.buffer, new_chunk[len(self.buffer):]], axis0) return self.buffer[0] # 返回当前步动作 def step(self): # 每执行一步缓冲区前移 if self.buffer is not None and len(self.buffer) 1: self.buffer self.buffer[1:]逻辑说明update方法把新预测的动作块和缓冲区里的旧动作做加权平均decay0.5表示越靠前的动作权重越高。step方法每执行一步就把缓冲区前移一位。参数说明decay越大新动作权重越高响应越快但平滑效果越弱decay越小平滑效果越强但动作滞后越明显。我一般取 0.3 到 0.5 之间抓取任务取 0.4 比较稳。5.3 验证方法用成功率曲线判断是否值得继续投入部署 VLA 后不要只看单次抓取成功与否要跑成功率曲线。具体做法固定 20 个物体位置每个位置抓 5 次记录成功次数画成柱状图。如果成功率低于 60%先排查第 4 章的避坑点如果在 60% 到 75% 之间加 action chunking 和时序集成如果超过 75%可以尝试换更大的视觉编码器或增加训练数据。我自己的经验是action chunking 加时序集成能把成功率从 60% 拉到 85% 左右再往上就要靠数据质量和模型规模了。这套方案值不值得做取决于你的任务容错率如果抓空一次成本很低60% 成功率就能上线如果抓空会损坏物体至少要跑到 90% 以上。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑