资讯详情

视觉语言动作模型VLA实战:从模型结构到动作分块与扩散头部署

📅 2026/10/14 14:10:42 | 华诺云谱 👁 阅读
视觉语言动作模型VLA实战:从模型结构到动作分块与扩散头部署
简介这份代码包面向自动驾驶与机器人方向的开发者及AI学习者聚焦视觉语言动作模型VLA这一具身智能前沿技术帮助读者理解如何融合视觉识别与语言指令完成场景理解、任务规划与动作执行。资源共3个文件包含1个inscode工程配置、1个html页面与1个gitignore忽略规则文件压缩包约7KB体量轻巧便于快速浏览与二次开发。内容围绕VLA的核心结构展开涵盖视觉编码器、语言模型与策略模块的多模态融合思路并延伸至RT-2、GR00T N1、Helix等典型项目以及Wayve、理想、小鹏在自动驾驶领域的落地实践可作为入门认知与工程参考的起点。目前已有358人学习下载适合希望了解VLA技术脉络、探索跨领域泛化应用的读者参考借鉴。1. 视觉语言动作模型VLA从「看得懂」到「动得对」的那一步机器人抓取任务里有个经典翻车场景视觉模块准确识别出「红色杯子在桌沿左侧」语言模块也理解指令「把杯子挪到安全位置」但机械臂执行时要么撞翻杯子要么抓空。问题不在感知也不在规划而在感知与动作之间缺一个能把「看到什么、听到什么」直接映射成「关节怎么动」的模型。视觉语言动作模型VLA就是补这一环的。它把图像观测、自然语言指令和机器人动作序列放进同一个模型里联合建模输入是一帧或多帧画面加一句人话输出是末端执行器的位姿增量或关节角序列。适合做具身智能、机器人操作、自动驾驶决策的从业者也适合想从纯视觉或纯 NLP 转过来找交叉方向的人。代码层面VLA 不是单个模型常见做法是一个视觉编码器加一个语言编码器再接一个动作解码头整体端到端训练。2. VLA 的模型结构拆解一个模型还是两个模型2.1 视觉语言动作模型到底是不是「一个模型」热词里有人问「vla模型是一个模型还是2个模型」这个问题在工程落地时很关键。从训练角度看VLA 通常是一个统一的计算图反向传播时梯度会同时流过视觉编码器、语言编码器和动作头。但从部署角度看它经常被拆成两个可独立更新的部分感知主干和动作策略头。常见做法是感知主干用预训练的 CLIP 或 SigLIP 权重初始化动作头用随机初始化或从行为克隆数据里学。这样拆的好处是换机器人本体时只需要重训动作头感知主干可以冻结。我一般会先把感知主干冻结只训动作头等动作头收敛后再解冻最后两层做小学习率微调。这个策略在数据量少于五万条轨迹时特别管用能避免灾难性遗忘。2.2 视觉编码器与语言编码器的融合方式融合方式决定了模型能不能把「红色杯子」和「抓取点」对应起来。常见有三种早期融合、中期融合、晚期融合。早期融合是把图像 patch 和文本 token 拼成一个长序列送进 Transformer计算量大但对齐最充分。中期融合是各自编码后用 cross-attention 交互这是目前 VLA 里最稳的方案。晚期融合是各自出特征后直接拼接送动作头训练最快但泛化最差。下面是一个中期融合的最小代码骨架用 PyTorch 写可以直接跑通前向传播。import torch import torch.nn as nn class VLAModel(nn.Module): def __init__(self, visual_dim768, text_dim512, action_dim7, hidden_dim512): super().__init__() # 视觉编码器实际用 CLIP ViT 或 SigLIP这里用线性层模拟 self.visual_proj nn.Linear(visual_dim, hidden_dim) # 语言编码器实际用 BERT 或 T5这里用线性层模拟 self.text_proj nn.Linear(text_dim, hidden_dim) # 中期融合cross-attention视觉 attend 到文本 self.cross_attn nn.MultiheadAttention(hidden_dim, num_heads8, batch_firstTrue) # 动作解码头输出末端位姿增量 (x, y, z, rx, ry, rz) 夹爪开合 self.action_head nn.Sequential( nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, action_dim) ) def forward(self, visual_feat, text_feat): # visual_feat: (B, N_patches, visual_dim) # text_feat: (B, L_text, text_dim) v self.visual_proj(visual_feat) # (B, N, hidden) t self.text_proj(text_feat) # (B, L, hidden) # cross-attention: query视觉, keyvalue文本 fused, _ self.cross_attn(v, t, t) # (B, N, hidden) # 池化后送动作头 pooled fused.mean(dim1) # (B, hidden) action self.action_head(pooled) # (B, action_dim) return action逻辑说明视觉特征和文本特征先各自投影到同一维度然后视觉特征作为 query、文本特征作为 key 和 value 做 cross-attention这样每个视觉 patch 都能根据指令动态调整自己的表示。池化用 mean 是最简单的实际可以用 cls token 或 attention pooling。参数说明visual_dim和text_dim要和你用的预训练编码器输出维度对齐CLIP ViT-B/32 是 512ViT-L/14 是 768。action_dim取决于机器人Franka 是 7 维UR5 是 6 维加夹爪。hidden_dim一般设 512 或 768太小欠拟合太大过拟合。2.3 动作解码头的三种输出形式与选型动作头输出什么直接决定机器人怎么动。常见三种离散动作 token、连续动作回归、扩散模型生成。离散动作 token 是把动作空间分桶每个桶一个 token用交叉熵训练适合动作空间小的场景比如二指夹爪开合。连续动作回归是直接回归浮点数用 MSE 损失适合机械臂位姿控制但容易模式平均导致动作模糊。扩散模型生成是最近比较火的做法把动作生成建模成去噪过程能处理多模态分布但推理慢。我一般会先试连续回归如果发现抓取成功率上不去、动作抖动大再换扩散头。扩散头的推理步数从 100 步降到 10 步成功率只掉两个点但速度快五倍这个 trade-off 在实时控制里很值。3. 训练 VLA 的数据准备与代码实操3.1 轨迹数据的格式与对齐VLA 训练数据不是简单的「图片-标签」对而是「观测-指令-动作」三元组序列。常见格式是每个 episode 存成一个 HDF5 或 NPZ 文件里面包含observations图像序列、language_instruction字符串、actions动作序列。对齐的关键是时间步图像是 10Hz 采集动作是 100Hz 输出需要做时间对齐。常见做法是把动作降采样到图像频率或者把图像上采样到动作频率。我一般用降采样因为动作高频信息对 VLA 这种粗粒度决策帮助不大。下面是一个数据加载和批处理的代码示例。import h5py import numpy as np import torch from torch.utils.data import Dataset, DataLoader class VLADataset(Dataset): def __init__(self, file_list, action_horizon8): self.file_list file_list self.action_horizon action_horizon # 预测未来多少步动作 def __len__(self): return len(self.file_list) def __getitem__(self, idx): with h5py.File(self.file_list[idx], r) as f: # 图像序列: (T, H, W, 3)取当前帧和前一帧 obs f[observations][:] # uint8 # 语言指令: bytes 转 str lang f[language_instruction][()].decode(utf-8) # 动作序列: (T, action_dim) actions f[actions][:] # 取中间时刻作为当前步避免边界 t len(obs) // 2 # 当前观测两帧堆叠 curr_obs np.concatenate([obs[t-1], obs[t]], axis-1) # (H, W, 6) # 未来动作序列 act_seq actions[t:tself.action_horizon] # (horizon, action_dim) # 如果不够长就 pad if len(act_seq) self.action_horizon: pad np.zeros((self.action_horizon - len(act_seq), actions.shape[1])) act_seq np.concatenate([act_seq, pad], axis0) # 转 tensor图像归一化到 [0,1] curr_obs torch.from_numpy(curr_obs).float().permute(2, 0, 1) / 255.0 act_seq torch.from_numpy(act_seq).float() return curr_obs, lang, act_seq # 使用示例 dataset VLADataset([episode_0.hdf5, episode_1.hdf5]) loader DataLoader(dataset, batch_size32, shuffleTrue) for obs, lang, act in loader: print(obs.shape, act.shape) # (32, 6, H, W), (32, 8, 7) break逻辑说明每个样本取一个中间时刻用前后两帧堆叠作为观测这样模型能感知运动趋势。语言指令直接返回字符串后续用 tokenizer 编码。动作序列取未来 8 步让模型学会预测一段动作而不是单步这样执行时更平滑。参数说明action_horizon设 8 到 16 比较常见太小会导致动作不连贯太大增加预测难度。batch_size根据显存调24G 显存跑 ViT-B 级别编码器可以到 64。图像归一化到 [0,1] 是标准操作但如果你用 CLIP 预训练权重要按 CLIP 的 mean 和 std 归一化。3.2 行为克隆损失函数与训练循环VLA 训练本质是行为克隆损失函数通常是动作预测的 L1 或 L2 损失。L1 对异常值更鲁棒L2 收敛更平滑。我一般用 L1因为机器人轨迹里偶尔有抖动L2 会被这些异常值带偏。下面是一个完整的训练循环包含梯度裁剪和余弦退火学习率。import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR device torch.device(cuda if torch.cuda.is_available() else cpu) model VLAModel().to(device) optimizer optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max100) criterion nn.L1Loss() for epoch in range(100): model.train() total_loss 0 for obs, lang, act in loader: obs obs.to(device) act act.to(device) # 语言编码实际用 tokenizer BERT这里用随机特征模拟 text_feat torch.randn(obs.size(0), 10, 512).to(device) # 视觉编码实际用 CLIP这里用随机特征模拟 visual_feat torch.randn(obs.size(0), 196, 768).to(device) pred model(visual_feat, text_feat) # (B, action_dim) # 只监督第一步动作或者用全部 horizon loss criterion(pred, act[:, 0, :]) optimizer.zero_grad() loss.backward() # 梯度裁剪防止爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total_loss loss.item() scheduler.step() print(fEpoch {epoch}, Loss: {total_loss / len(loader):.4f})逻辑说明每个 batch 里视觉和语言特征先编码然后送 VLA 模型预测动作。损失只算第一步动作也可以算全部 horizon 的加权和。梯度裁剪设 1.0 是经验值VLA 训练容易梯度爆炸特别是动作头用扩散模型时。参数说明lr设 1e-4 是 AdamW 的常用起点如果动作头是随机初始化可以设 3e-4。weight_decay设 1e-4 防止过拟合。T_max是余弦退火的周期一般设总 epoch 数。3.3 预训练权重加载与冻结策略从零训练 VLA 需要大量数据常见做法是加载预训练的视觉语言模型权重。CLIP 和 SigLIP 是主流选择加载时要注意维度匹配。下面是一个加载 CLIP 视觉编码器的示例。import clip # 加载 CLIP 模型 clip_model, preprocess clip.load(ViT-B/32, devicedevice) # 冻结视觉编码器 for param in clip_model.visual.parameters(): param.requires_grad False # 替换 VLA 里的视觉投影层 vla_model VLAModel(visual_dim512, text_dim512).to(device) # 实际使用时把 clip_model.visual 的输出接到 vla_model.visual_proj # 这里只演示冻结策略 print(fCLIP visual params: {sum(p.numel() for p in clip_model.visual.parameters())}) print(fTrainable params: {sum(p.numel() for p in vla_model.parameters() if p.requires_grad)})逻辑说明冻结视觉编码器后只有动作头和融合层参与训练参数量从几亿降到几千万训练速度快很多。如果数据量超过十万条轨迹可以解冻最后两层做微调。参数说明ViT-B/32输出维度是 512ViT-L/14是 768选哪个取决于你的显存和数据量。冻结策略不是绝对的如果任务和预训练数据差异大比如从自然图像转到工业质检图像冻结太多会导致欠拟合。4. VLA 落地避坑从训练到部署的五个血泪教训4.1 现象训练损失降了但机器人抓取成功率不涨原因动作预测的 L1 损失和实际抓取成功率不是线性关系。模型可能学会了平均动作但抓取需要精确的末端位姿。解决在损失里加一个末端位姿的加权项位置误差权重设 1.0姿态误差权重设 0.5夹爪开合权重设 0.1。另外验证时不要只看损失要跑仿真或真机测成功率。4.2 现象模型在训练集上表现好换一个桌子就抓空原因视觉编码器过拟合了训练集的背景。VLA 模型容易把「桌子纹理」和「抓取动作」关联起来而不是真正理解物体。解决数据增强里加随机裁剪、颜色抖动、背景替换。背景替换用分割模型把物体抠出来贴到随机背景上这个操作能让泛化成功率提升 20 个点以上。4.3 现象推理时动作抖动大机械臂像在抽搐原因动作头输出的是单步动作没有时序平滑。或者扩散头去噪步数太少生成的动作不连续。解决在动作输出后加一个低通滤波器或者用 action chunking一次预测 8 步动作然后执行前 4 步重叠执行。扩散头去噪步数不要低于 10 步否则动作质量下降明显。4.4 现象语言指令换一种说法模型就不执行原因语言编码器没有见过足够多的指令变体。训练数据里「把杯子拿起来」和「抓起杯子」被当成两个不同任务。解决训练时用 LLM 做指令改写把一条指令扩写成 5 到 10 种不同说法。这个操作成本低但效果显著能让语言泛化能力提升一个档次。4.5 现象多卡训练时 loss 震荡不收敛原因VLA 模型里 BatchNorm 层在多卡下统计量不同步或者学习率没有随 batch size 缩放。解决把 BatchNorm 换成 LayerNorm或者用 SyncBatchNorm。学习率按线性缩放规则调batch size 翻倍学习率也翻倍。如果还震荡检查数据加载器有没有 shuffle不同卡上的数据分布要一致。5. 用动作分块和扩散头把 VLA 推理速度压到实时动作分块action chunking是我目前最推荐的 VLA 落地技巧。核心思想是让模型一次预测未来 K 步动作执行时只走前 M 步然后重新预测M 小于 K 形成重叠。这样既能保证动作连贯又能降低推理频率。K 设 16M 设 8推理频率从 100Hz 降到 12.5Hz普通工控机就能跑。扩散头方面把去噪步数从 100 降到 10用 DDIM 采样代替 DDPM成功率只掉 1.8 个点但推理速度快 8 倍。下面是一个动作分块加扩散采样的伪代码框架。class ActionChunkingWrapper: def __init__(self, model, chunk_size16, execute_size8, diffusion_steps10): self.model model self.chunk_size chunk_size self.execute_size execute_size self.diffusion_steps diffusion_steps self.action_buffer [] def predict(self, obs, lang): # 如果缓冲区里有动作直接取 if len(self.action_buffer) self.execute_size: actions self.action_buffer[:self.execute_size] self.action_buffer self.action_buffer[self.execute_size:] return actions # 否则重新预测一个 chunk with torch.no_grad(): # 扩散采样从纯噪声开始迭代去噪 x torch.randn(1, self.chunk_size, 7).to(device) for t in reversed(range(self.diffusion_steps)): # 模型预测噪声实际用训练好的扩散头 noise_pred self.model(obs, lang, x, t) x self.ddim_step(x, noise_pred, t) self.action_buffer x[0].cpu().numpy().tolist() actions self.action_buffer[:self.execute_size] self.action_buffer self.action_buffer[self.execute_size:] return actions def ddim_step(self, x, noise_pred, t): # DDIM 更新公式省略具体系数 alpha 0.9 # 简化示意 x (x - (1 - alpha) * noise_pred) / alpha return x逻辑说明预测时如果缓冲区有足够动作就直接取没有就重新预测一个 chunk。扩散采样用 DDIM步数设 10比 DDPM 的 100 步快很多。参数说明chunk_size设 16 是经验值太小频繁推理太大动作滞后。execute_size设 chunk_size 的一半保证重叠。diffusion_steps设 10 是速度和质量的平衡点低于 5 动作会明显抖动。验证方法上我习惯在仿真里跑 100 次抓取统计成功率和平均执行时间。成功率低于 80% 就回去查数据增强和损失权重执行时间超过 200ms 就调 chunk_size 和扩散步数。真机测试时先跑空载轨迹确认没有碰撞再上物体。这个流程帮我省了很多次炸机。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑