☰
VLA视觉语言动作模型:从端到端训练到机械臂部署的完整指南
2026/10/9 21:08:43 网站建设 项目流程

简介:视觉语言动作模型(VLA)是当前具身智能领域的重要技术方向,通过融合视觉信息与语言指令,驱动机器人和自动驾驶系统完成场景感知、任务规划与动作执行。代码包面向AI开发者、机器人研究者及自动驾驶从业者,提供一套轻量级的VLA应用参考实现,便于快速理解视觉编码器、语言模型与策略模块之间的衔接,以及多模态融合和端到端训练的基本思路。压缩包共3个文件,主要包含HTML演示页面、.gitignore版本管理配置和.inscode在线编码环境配置,整体仅7KB,结构简洁,适合入门研读、运行和二次扩展。目前已有378人学习,读者可在浏览器中直接查看页面效果,对照描述中的技术脉络梳理VLA的完整流程,并以该代码为基础快速搭建自己的演示原型,为后续在真实机器人或自动驾驶场景中的部署与调参提供起点。

1. VLA 到底是什么:一条把视觉、语言直接映射成机械臂动作的路线

有个做自动化集成的朋友找我排查产线上的视觉分拣问题。他们的方案是“目标检测 + 机械臂规划”:相机识别物料类别和位置,再交给运动规划器算轨迹。每个新产品上线,工程师要重新标数据、调检测阈值、改抓取位姿,一套流程下来两到三周。我问他为什么不试试 VLA,他说听过这个词,但一直觉得是学术 Demo,不敢往产线上放。后来我们花了两周,用一条端到端的视觉语言动作模型把“识别、决策、控制”三个环节合成一步,新任务只改一句自然语言指令,不再改代码。

VLA(Vision-Language-Action Model)做的事情,就是把相机画面和一条操作指令同时送进同一个模型,模型直接输出机械臂动作——末端位姿增量、关节角或者夹爪开合量。它和传统方案的本质区别不是“精度更好”,而是“泛化方式变了”:传统方案用规则和坐标传递语义,VLA 用语言做中间层。适合读这篇笔记的人,是手里有示教数据、想用一套模型覆盖多种操作任务、又不想每次重新写视觉规则的团队。带 [代码] 就是因为下面所有步骤都能直接落到脚本里跑,不是停留在概念讲解。

2. VLA 模型结构拆解:VLM 底座、动作头与动作表示怎么选

VLA 不是什么全新的网络结构,它是把两条成熟技术路线拼在一起:一条是视觉语言模型(VLM),一条是动作解码器。搞清楚这条拼接线,很多配置上的困惑会瞬间解开。

2.1 为什么不是“视觉模型 + 规划器”:差在泛化方式

传统操作流程里,感知和规划是两套独立系统。感知系统输出物体的类别、坐标、姿态,规划系统把这些结构化信息翻译成轨迹。这套流程在固定场景里很稳,但一旦任务变化,感知接口和规划规则都要跟着改。比如原来抓“红色方块”,现在要“把红色方块放到左侧托盘”,传统方案需要额外实现托盘定位和放置点计算,这些逻辑散落在不同模块里。

VLA 把整个映射过程压进一个网络。输入是图像和文本,输出是动作,中间的“该看哪里、该往哪动”全部由模型自己决定。训练数据是“图像 + 指令 + 动作”的三元组,模型学到的是语义层面的行为映射,而不是某个物体的坐标。所以换新任务时,指令变了,输出就跟着变。代价是:模型变大了,数据要求变高了,推理变慢了。选择 VLA 的前提是你能接受 7B 级模型带来的部署成本,换来的是任务迁移成本几乎归零。

2.2 三个关键组件:视觉编码器、VLM 底座、动作头

一个可落地的 VLA 通常由三部分组成。首先是视觉编码器,负责把原始图像变成视觉 token。常见实现是用 CLIP 风格的编码器,输入分辨率一般取 224 到 512 之间。分辨率越高,细节保留越好,但 token 数量翻倍,推理时间和显存占用都涨得很快。其次是 VLM 底座,负责融合视觉和语言信息。现在主流方案是拿 7B 量级的开源语言模型做底座,把图像 token 和文本 token 拼在同一个序列里,让模型在自回归过程中同时“看到”画面和指令。

第三部分是动作头,也是最容易被忽略的部分。VLM 底座输出的是语义向量,不是动作。动作头负责把最后一层隐含状态解码成具体的动作数值。这里的选择会直接影响训练难度和最终控制精度。我在最开始做 VLA 实验时,把大部分时间花在调视觉编码器和底座上,后来发现动作头选型错了,损失函数怎么调都收不住。建议先固定视觉语言部分,把动作头确定下来再谈其他。

2.3 动作表示:连续回归与离散步进 token 的取舍

动作头怎么做,业界有两条路线。第一条是离散化:把每个动作维度切分成固定数量的 bin,例如把末端位移映射到 256 个离散区间,模型用分类损失去预测每个区间的编号。这条路线的好处是能直接复用语言模型的交叉熵训练方式,实现简单,稳定性高。代价是精度受 bin 数量限制,而且动作维数多了之后,离散空间会迅速膨胀。对于夹爪开合这种两态输出,离散化很自然;对于连续位姿,离散化会让动作看起来有“阶梯感”。

第二条是连续回归:动作头输出动作分布的均值(有时也预测方差),配合 L2 或者负对数似然损失。精度上比离散化好,但训练容易不稳定,尤其当数据里存在少量异常轨迹时,损失会被极端值拉爆。近两年常见的做法是引入扩散模型做动作头,把“从噪声中逐步去噪得到动作”当成生成任务。扩散头效果更稳,对多模态动作分布(同一个指令存在多种合理走法)表达能力更强,缺点是训练步数和推理时延都相应增加。我的建议是:数据量少于 2 万条,先走离散化;数据超过 5 万条且任务包含多种操作风格,直接上扩散头。

2.4 LoRA 微调:为什么不能冻结 VLM 底座

拿到了预训练的 VLM 底座之后,一个很自然的想法是冻结它,只训练动作头。这样做训练成本低,但效果往往不好。原因是底座里的语义表征并不是为动作任务优化的,冻结后模型会“看得懂指令但不知道手往哪放”。会损失一部分语言理解能力,也会损失对细微视觉差异的敏感度。常见做法是用 LoRA 做参数高效微调,只更新一部分低秩矩阵,既保住底座能力,又让语义表征向动作任务对齐。

用 LoRA 时有两个坑必须注意。第一,如果只把 LoRA 挂在语言部分的注意力矩阵上,视觉编码器仍然处于冻结状态,模型对光照、物体纹理的适应能力会偏弱,后面我会在避坑章节具体展开。第二,LoRA 的 rank 不是越大越好。我在一个抓取项目里把 rank 从 16 调到 64,效果确实涨了一点,但显存占用涨了将近一倍,训练速度也明显变慢,最终在 rank=48 左右达到性能拐点。参数选择上没有统一最优解,但按 rank 32~64、alpha 取 rank 的 2 倍起步,是一个不会翻车的起始区间。

3. 最小可复现 VLA 训练:数据组织、训练脚本与四个关键参数

没有开源权重做底座就不要从零训练 VLA,那是另一条成本完全不同的路线。我先讲数据怎么组织,再给一份能直接改路径运行的训练脚本,最后把四个必调参数拆开讲。

3.1 训练数据长什么样:图像、指令、动作三元组

VLA 训练集的最小单元是一条“指令 + 画面 + 动作”记录。画面通常是单个视角的 RGB 图,指令是自然语言句子,动作是一个浮点数组。动作维度取决于机器人构型:常见的六轴机械臂加夹爪,动作是 7 维——末端 x/y/z 平移增量、绕 x/y/z 轴的旋转增量、夹爪开合量。如果只做平面抓取,动作可以降到 4 维(x/y/z 加夹爪);如果做移动抓取,还要加上底盘线速度和角速度。

数据里有几点必须统一。第一是坐标系,所有动作必须定义在同一个基准坐标系下,不能一部分是基座坐标、一部分是相机坐标。第二是单位,平移用毫米还是米,旋转用弧度还是角度,一旦混了,loss 会表现出“先降后横盘”的怪状。第三是采样频率,动作标签的时间间隔必须和真机控制频率一致,比如示教时 10Hz 采一条,训练时模型输出的动作就是按 10Hz 的执行周期来理解的。数据采集多花一周时间统一这些元信息,后面训练和部署会省两周不止。

3.2 数据加载与预处理脚本:先统一图像和动作空间

import torch from datasets import load_dataset from torch.utils.data import Dataset ACTION_DIM = 7 # 末端6自由度量 + 夹爪开合 class VLADataset(Dataset): def __init__(self, data_path, image_size=224): # jsonl: 每行 {"instruction": "...", "image_path": "...", "action": [...]} self.data = load_dataset("json", data_files=data_path)["train"] self.image_size = image_size def __len__(self): return len(self.data) def __getitem__(self, idx): item = self.data[idx] # 1) 统一图像格式:RGB + 固定尺寸,避免换相机后翻车 image = item["image"].convert("RGB").resize((self.image_size, self.image_size)) # 2) 动作归一化到 [-1, 1],训练更稳,推理时再反算回真实量程 action = torch.tensor(item["action"], dtype=torch.float32) action_min = torch.tensor(item["action_min"], dtype=torch.float32) action_max = torch.tensor(item["action_max"], dtype=torch.float32) action_norm = 2.0 * (action - action_min) / (action_max - action_min) - 1.0 return {"image": image, "instruction": item["instruction"], "action": action_norm}

这段代码做了三件事:把 PIL 图像统一成 RGB 并缩放到固定尺寸;将动作向量归一化到 [-1, 1] 区间;返回与训练框架对接的标准字典。归一化这一步不能省,未归一化的动作会让模型在训练初期把大量容量浪费在“适应数值尺度”上,表现为 loss 掉得很慢。动作的 min/max 值必须来自全部训练数据,而不是单条数据,并且要在部署时把同一组 min/max 存下来,推理端反归一化要用它。很多人训练和部署各存了一份归一化参数,结果部署出来的动作幅度差两倍,这类问题我会在避坑章节再强调一次。

3.3 LoRA 微调训练脚本:可直接替换路径运行

from transformers import Trainer, TrainingArguments, AutoProcessor from peft import LoraConfig, get_peft_model # 以 7B 级开源 VLM 底座为例,实际使用时替换为你的底座路径 model_path = "vlm-backbone-7b" # 1) 加载底座模型 + LoRA 配置 model = AutoModelForVision2Seq.from_pretrained(model_path) lora_config = LoraConfig( r=48, lora_alpha=96, target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], lora_dropout=0.05, ) model = get_peft_model(model, lora_config) # 2) 训练参数:8卡 batch 总大小 8*4=32 training_args = TrainingArguments( output_dir="./vla-runs", per_device_train_batch_size=8, gradient_accumulation_steps=4, learning_rate=1e-4, num_train_epochs=3, warmup_ratio=0.03, logging_steps=50, save_steps=500, bf16=True, ) trainer = Trainer( model=model, args=training_args, train_dataset=VLADataset("data/train.jsonl"), ) trainer.train()

核心逻辑是两条:AutoModelForVision2Seq负责加载视觉语言底座,get_peft_model套上 LoRA。训练时只更新 LoRA 参数和动作头,底座的原始权重不动。这个脚本默认把 LoRA 挂在注意力层上,如果你的底座用了不同的模块命名,需要先打印模型结构,把target_modules改对,否则 LoRA 根本没生效,训练半天 loss 纹丝不动。

训练参数里有三个最容易影响结果的:learning_rate从 1e-4 起步,如果 loss 前两百步不降,降到 3e-5;per_device_train_batch_size受显存限制,不够时用gradient_accumulation_steps凑总 batch,总 batch 太小会让 VLA 训练很不稳,建议总 batch 保持在 32 以上;bf16=True只在支持 BF16 的 GPU 上开,老卡改成fp16=True。最后是save_steps,我习惯设 500 步存一次 checkpoint,因为 VLA 训练经常在第 1000 步附近出现 loss 突然跳变,多几个存档点才有后悔药吃。

3.4 四个关键参数:lr、rank、图像分辨率、动作损失权重

第一个是学习率。VLA 是在底座之上做微调,学习率太高会把底座原有语义冲坏,太低动作头又学不动。一个稳妥做法是给动作头单独设更高的学习率,给 LoRA 层用低学习率,两个学习率差 3~5 倍。第二个是 LoRA rank,我在 2.4 里已经提过,rank 从 32 起步,数据量大就升到 64,显存不够就降到 16 并接受一点性能损失。

第三个是图像分辨率。训练时用 224 还是 384,直接决定模型能不能看清小目标。之前做某个插座插拔项目,224 分辨率下模型根本分不清插孔朝向,换到 384 后成功率直接翻倍。但分辨率从 224 提高到 384,视觉 token 数量差不多是原来的三倍,训练时间和显存都会明显上涨。第四个是动作损失权重。如果整体 loss 里语言建模损失占主导,动作部分会被“淹没”,模型会变成“能看懂指令但动作幅度很小”的状态。常见做法是把动作损失权重单独调到 1.5~2.5,保持语言损失在辅助位置。

4. VLA 部署到真机:量化、延迟预算与推理服务

训练跑通只是第一步,VLA 模型从 checkpoint 变成产线上能用的控制器,中间还隔着推理延迟、显存占用和接口稳定性三道坎。这一章我按实际部署顺序来讲:先定延迟预算,再决定量化和硬件,最后写推理服务。

4.1 先算延迟预算,再选硬件

机械臂控制周期决定了 VLA 推理必须做多快。常见工业机械臂的控制频率在 10Hz 到 30Hz 之间,但 VLA 模型的推理很少能跑进这个周期。一个常见的部署方案是异步执行:相机以固定频率采集图像,模型持续推理,最新一帧画面产生的动作直接覆盖上一帧动作,而不是等待当前动作执行完。这样机械臂的执行频率是固定的,模型只是不断“刷新”目标动作,整体表现取决于模型平均推理延迟而不是单次上限。

先根据你的算力量级定一个延迟预算表,再决定要不要上量化和裁剪。下面是我在几个项目里积累的典型参考值:

部署形态硬件类型量化位宽典型推理延迟
工作站中端单卡 GPUFP1630~80ms
工作站中端单卡 GPUINT820~50ms
边缘盒嵌入式 GPUINT8/INT4100~250ms
纯 CPU 工控机普通多核INT4500ms 以上

这张表的意义是帮你先做乘法:如果机械臂控制频率是 10Hz,即每 100ms 需要一次新动作,那嵌入式边缘盒勉强够;如果是 20Hz,就必须上工作站 GPU。很多人项目翻车不是因为模型精度不够,而是部署前没算延迟预算,等到模型上了真机才发现跑不满控制频率,整个系统一抖一抖的。

4.2 量化方案:压视觉语言部分,动作头尽量保持原精度

7B 模型正常人手里都没有无限显存,量化基本是必选项。常见做法是把底座权重量化到 INT8 或者 INT4,动作头保持更高精度。原因是动作头对数值误差更敏感,它输出的动作数值直接进机械臂控制器,哪怕差 2% 都会表现为末端位置的毫米级偏移,在精密装配场景里这是致命的。

量化用现成库就可以了,加载时传一个量化配置,模型权重在内存里以低精度存储,计算时反量化回浮点。INT8 通常不掉点,INT4 会有一点性能损失,但对操作类任务往往在可接受范围内。如果你发现 INT4 之后动作明显抖动,有一个折中方案:底座用 INT4,动作头单独保持 FP16。这个混合精度方案在不少开源部署框架里都有支持,效果比整体 INT4 好很多,部署成本也没涨太多。

4.3 一个能直接改的推理服务骨架

import torch from transformers import AutoModelForVision2Seq, AutoProcessor class VLARunner: def __init__(self, ckpt_path, action_min, action_max, quantize_bits=4): # 加载处理器和模型,量化底座权重 self.processor = AutoProcessor.from_pretrained(ckpt_path) if quantize_bits == 4: model = AutoModelForVision2Seq.from_pretrained( ckpt_path, load_in_4bit=True, device_map="auto" ) else: model = AutoModelForVision2Seq.from_pretrained(ckpt_path, device_map="auto") self.model = model # 训练时用过的归一化参数,推理端必须和训练端完全一致 self.action_min = torch.tensor(action_min, dtype=torch.float32) self.action_max = torch.tensor(action_max, dtype=torch.float32) @torch.inference_mode() def predict(self, frame, instruction): # 图像预处理与训练保持一致:RGB + 224x224 inputs = self.processor( text=instruction, images=frame.convert("RGB"), return_tensors="pt", ).to(self.model.device) # 模型输出的是归一化动作,反算回真实量程 action_norm = self.model.generate(**inputs, max_new_tokens=ACTION_DIM) action = (action_norm + 1) / 2 * (self.action_max - self.action_min) + self.action_min return action.cpu().numpy()

这段代码的骨架可以直接抄进项目里,但有几个点要格外注意。self.processor的文本和图像预处理参数必须和训练时一致,否则模型看到的是“另一种格式的画面”,表现会莫名下降。max_new_tokens=ACTION_DIM要等于你的动作维度,这个值设大了模型会继续生成无意义内容,设小了动作被截断。最后,action_min和action_max必须是从训练数据统计来的同一组值,不能用推理时手动指定的范围。

延迟优化方面,有一个低成本技巧值得先试:把推理输入图像的动态尺寸改成固定尺寸。有些开源实现默认按最长边 512 缩放,不同帧的 token 数量不同,GPU 每次都要重新构图,延迟波动很大。固定到 224 或 320 之后,延迟能减少 20% 左右,而且波动变小。另外,torch.inference_mode()要包住整个推理路径,它能避免 PyTorch 在推理时保存中间变量用于自动求导,这部分能省下不少显存和计算。

5. VLA 部署避坑:五个高频故障的现象、原因与解法

VLA 项目的问题往往不在模型结构,而在数据管线、部署环境和接口对接。下面五条是我在实际项目中反复遇到的,每条都按现象、原因、解决的顺序写,方便你对照排查。

5.1 换了一台相机,模型动作整体漂移

现象:同一个模型,训练时用的相机和部署时用的相机不是同一型号,结果机械臂的动作轨迹整体偏移,抓取位置明显偏离物体中心。模型在旧相机数据上测试一切正常。

原因:VLA 对图像低级特征的敏感度超出预期。换相机带来的变化不只是分辨率,还有色彩空间、白平衡、镜头畸变、自动曝光曲线。视觉编码器提取的特征分布发生变化,动作输出自然跟着漂移。

解决:推理端和训练端统一图像预处理是最低要求,但往往还不够。我后来总结了一条可复用的习惯:在训练数据里混入多种相机的采集画面,或者在推理端对图像叠加轻微的颜色扰动(亮度 ±10%、对比度 ±5%),能显著提升跨相机的鲁棒性。如果项目允许,尽量用训练数据同款相机做部署,这是最省事的话。

5.2 动作数据混用关节角和末端位姿,loss 降不下去

现象:训练集一部分数据来自遥操作示教,存的是关节角;另一部分来自规划器生成,存的是末端位姿。混合训练后 loss 一直横盘,或者不断出现尖峰。

原因:关节角和末端位姿的量纲、数值范围完全不同。关节角在 0~2π 之间,末端平移可能到几百毫米,旋转又是弧度,三种数值混在一起,模型相当于在同一个输出空间里学两种完全不同的语义,很难收敛。

解决:统一动作空间。最稳妥的是全部转成末端位姿,因为规划器和大多数真机控制接口都用这个格式。统一后重新统计 action_min 和 action_max,再跑训练。如果某些数据源确实只能提供关节角,需要先用运动学正解转一遍,而不是留着一个混合空间让模型自己学。

5.3 推理延迟忽高忽低,机械臂高频抖动

现象:模型平均推理延迟在 80ms 左右,但偶尔会跳到 300ms。机械臂执行时出现高频抖动,看起来像控制不稳定,调 PID 参数也没用。

原因:延迟波动造成的动作刷新间隔不稳定,比“慢但稳定”更影响控制稳定性。产生波动的源头可能是系统里其他进程抢占 GPU、显存不足导致的动态换页,或者推理输入尺寸不固定导致的构图耗时差异。

解决:先固定输入尺寸,消除构图波动。再给推理服务设置延迟告警,记录推理耗时分布,当 P95 延迟超过预算的 1.5 倍时主动告警。如果多次出现尖峰,检查显存占用和并行任务,优先保证推理进程独占 GPU。也可以在服务里加一个“过期动作丢弃”逻辑:如果新动作推理时间超过控制周期,就沿用上一帧动作,而不是插入一个过期的中间动作。

5.4 视觉编码器冻结,模型看不见小目标

现象:抓取大物件时成功率不错,换成小尺寸目标(比如螺丝、小插头)时成功率骤降。训练 loss 正常,但真机能感觉到“眼神不好”。

原因:4.3 节里我用 LoRA 只挂了注意力层,视觉编码器处于冻结状态。底座预训练时的视觉特征是为图文理解优化的,对细小物体的边缘、纹理表达能力不够,而 LoRA 没覆盖到这一层。

解决:把视觉编码器也纳入微调范围。一种做法是单独为视觉层配置更低的 LoRA rank 和更小的学习率,让它只做轻微调整;另一种做法是解冻视觉编码器的最后几层,配合低学习率训练。两个方法我都试过,效果接近,但前者显存开销更小。注意:视觉编码器学习率要设在底座 LoRA 的 1/5 以下,否则容易把预训练视觉特征冲掉,表现为训练集上动作稳了,换场景效果反而更差。

5.5 轨迹数据被随机打散,连续动作“接不上”

现象:训练 loss 正常,模型单步动作预测准确率也高,但真机上连续执行一个多步任务时,动作在步骤切换处断档,比如从“靠近物体”到“抓取”之间会停顿甚至倒退。

原因:训练时把同一条示教轨迹里的连续帧当成独立样本随机洗牌了。模型在训练时看到的画面和动作是“跳着”的,没有形成连续运动的上下文,输出动作自然只针对单帧,不针对时序。

解决:按轨迹分组训练。同一条示教轨迹的样本,在同一个 batch 内保持时序顺序,不同轨迹之间才允许随机洗牌。实现上可以在数据集的样本里额外存一个traj_id字段,采样时按traj_id分组,组内顺序不打乱。这个问题在视觉动作模型里很常见,但很少被人主动提,等真机连续任务跑不通时才暴露出来。

6. 进阶:先在仿真基准上打一次分,再谈真机迁移

VLA 项目做到后面,你会发现最贵的不是训练,而是“评估”。真机试错一次要花几分钟,还有安全风险,根本不可能频繁迭代。我现在的习惯是:所有改动先在仿真基准上打分,分数稳定之后,再挑其中几条任务上真机验证。仿真不能完全替代真机,但能把迭代周期从一天一次压缩到一小时一次。

具体做法是:找一个开源的机器人操作仿真包,里面固定了 100 条左右的操作任务,每条任务对应一个自然语言指令和初始场景配置。模型在仿真里跑完所有任务,统计一个“指令成功率”作为统一指标。训练过程中每隔几百步就评估一次,看曲线走势。在这个阶段,我通常还会同时记录两个诊断指标:平均步数越少越好,失败模式集中在哪几类指令。如果所有失败都集中在“夹取细长物体”这类任务上,说明图像分辨率不够;如果集中在“把物体放到指定位置”上,说明动作回归精度不够,优先调动作头而不是盲目加数据。

真机迁移前有一组对齐清单值得固化下来,每次训完新模型先自查一遍:图像尺寸和色彩空间是否与训练一致;动作归一化参数是否用了同一组;机械臂坐标基准是否和训练动作定义一致;控制频率是否符合推理延迟预算。这四项里任何一项不一致,模型再准都是白搭。我做过一个项目,仿真成功率到 87%,上真机只有 40%,排查到最后是部署端的图像格式从 RGB 变成了 BGR,一次颜色通道翻转直接掉了一半分。

最后一个我亲测有效的技巧:在真机首次运行前,把模型在验证集上的“动作输出分布”画出来,和训练集动作分布叠在一张图上。如果输出分布明显偏离训练分布,比如动作幅度整体偏小,说明模型没有真正学会动作空间,只是记住了“图像到指令”的表面对应。这时候直接上真机就是撞运气。这个检查只需要十分钟,能帮你省下至少一个下午的现场调试。

我自己也翻过好几次车,每次都是换相机、改频率、动归一化参数这三件事里栽的。后来把这些检查项做成一个固定流程,迁移新任务时先跑一遍,再谈调优。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询