简介:这份代码包面向自动驾驶与机器人方向的开发者及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_dim=7, hidden_dim=512): 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_model=hidden_dim, nhead=8, batch_first=True) self.fusion = nn.TransformerEncoder(encoder_layer, num_layers=4) 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_tensors="pt", padding=True, truncation=True) 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], dim=1) # [B, 197+L, 512] fused = self.fusion(fused) # [B, 197+L, 512] # 取平均池化作为全局表征 global_feat = fused.mean(dim=1) # [B, 512] action = self.action_head(global_feat) # [B, 7] return action逻辑说明:视觉编码器输出 197 个 token(1 个 cls + 196 个 patch),语言编码器输出 L 个 token,拼接后总序列长度 197+L。融合 Transformer 让视觉 token 和语言 token 做 cross-attention,动作头从全局池化特征回归动作。参数说明:action_dim=7对应末端执行器的 6D 位姿增量加夹爪开合;hidden_dim=512是融合层的统一维度,显存不够可以降到 256;num_layers=4是融合层数,层数越多跨模态交互越充分但训练越慢。注意:这个最小实现没有做动作序列的时序建模,实际部署时需要在动作头前加 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, f"episode_{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, dtype=np.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(f"episode_{ep:04d}") grp.create_dataset("observations/images", data=images, compression="gzip") grp.create_dataset("observations/qpos", data=qpos) grp.create_dataset("action", data=action) # 存储语言指令 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_episodes=100)逻辑说明:每个 episode 的图像序列按帧读取、统一 resize 到 224x224、转成 RGB 通道顺序。qpos是当前关节角度,action是目标关节角度或位姿增量,两者维度必须一致。语言指令存在 group 的 attribute 里,训练时按 episode 读取。参数说明:compression="gzip"可以压缩图像数据,100 个 episode 大约能从 2GB 压到 800MB;num_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_dim=512, action_dim=7, chunk_size=16): 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_size=16表示一次预测未来 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_size=16, decay=0.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):]], axis=0) return self.buffer[0] # 返回当前步动作 def step(self): # 每执行一步,缓冲区前移 if self.buffer is not None and len(self.buffer) > 1: self.buffer = self.buffer[1:]逻辑说明:update方法把新预测的动作块和缓冲区里的旧动作做加权平均,decay=0.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% 以上。希望帮到你。
本文还有配套的精品资源,点击获取