前言
机器人策略这几年进展很快,ACT、扩散策略这些方法把动作预测这件事往前推了一大步。但真要做成能泛化的系统,单靠一个 policy head 还不够,很多团队最后还是会把预训练视觉语言模型拉进来。RoboFlamingo 这类早期工作多用 LSTM 或 MLP 兜底,结构能跑,但离'能迁移到新任务'还有段距离。OpenVLA 算是更完整的一次尝试。
最近 π0 的出现也把通用机器人控制这条路讲得更清楚了:同一套策略算法,可以换着机械臂用。这个方向现在很明确,基本就是'先预训练,再针对机器人任务微调'。OpenVLA 出得更早,虽然不是最新的那个,但它把架构、数据和训练方式都讲得比较透,拿来拆一遍很有价值。
OpenVLA 的定位
2024 年 6 月,斯坦福大学、UC Berkeley、Google DeepMind 等机构发布了 OpenVLA,对应论文是《OpenVLA: An Open-Source Vision-Language-Action Model》。它的目标很直接:借用 CLIP、SigLIP、Llama 2 这些基础模型已经学到的通用表示,让机器人策略别只会死记训练集里的动作。
和闭源的 RT-2 比,OpenVLA 主要解决两件事。
一是结构和训练流程更透明,能看清楚它到底怎么做动作预测。二是更适合落到消费级硬件上,尤其是在换机器人、换数据集时,不用重新摸一套完全黑盒的配置。
它的思路其实不复杂:把动作也当成 token,让语言模型直接生成。这个做法看起来朴素,但好处是和 VLM 的训练范式接得很顺,扩展起来也省事。
模型结构
OpenVLA 建在 Prismatic-7B VLM 上,核心就三块。
- 视觉编码器:SigLIP 和 DinoV2。图像块先分别过两个编码器,再把特征在通道维拼起来。DinoV2 对空间推理确实有帮助,机器人控制里这点很实用。不过后面像 π0 只用 SigLIP 也跑得不错,所以它不是硬性依赖,更像是一个增益项。
- 投影器:两层小型 MLP,负责把视觉特征映射到语言模型的输入空间。
- 语言模型骨干:Llama 2 7B。
作者选 Prismatic,而不是 LLaVA 或 IDEFICS-1,原因也比较现实:SigLIP + DinoV2 的组合在空间理解上更稳,代码库本身也更模块化,后续改造成本低。
训练上有几个细节比较值得记一下。
- 输入分辨率用 224×224。384×384 没带来明显收益,训练时间却直接涨了三倍,这笔账不划算。
- 训练轮次跑得比较久,最后到了 27 个 epoch。VLA 这类任务通常就是要多看数据,动作 token 准确率超过 95% 后才开始稳定。
- 学习率固定在 2e-5,没做 warmup,效果反而最好。
动作离散化怎么做
OpenVLA 最关键的一步,是把连续动作变成离散 token。这样做的好处是,训练和推理都能直接复用 LLM 的 token 生成流程,不用再给动作分支单独设计一套输出头。
它参考 Brohan 等人的做法,把每个动作维度切成 256 个区间。问题在于,Llama 分词器里专门留给微调的特殊 token 只有 100 个,不够塞下 256 个动作 token。于是作者干脆用词表里频率最低的 256 个 token,也就是最后那一段 token,来承载动作。
区间边界不是按最小值和最大值直接拉满,而是取训练数据中第 1 和第 99 分位数之间的范围。这个选择很实用,能把异常动作甩出去,不然少数极端值会把离散区间拉得很宽,正常动作的分辨率就被稀释了。
数据集和训练
训练数据来自 Open X-Embodiment,里面有 70 多个机器人数据集,总共大约 200 万条轨迹。这个规模不小,但原始数据不能直接喂进去,还得先筛一遍。
OpenVLA 的筛选思路有两条。
首先,输入输出要尽量一致,所以它只保留至少有一个第三人称摄像机的操作数据集,并且只做单臂末端执行器控制。这样模型看到的视觉输入和动作空间不会太飘。
其次,要平衡不同 embodiment 的权重。这里借用了 Octo 的数据混合策略,简单说就是让低多样性数据少占点比重,把任务和场景更丰富的数据往前放。
DROID 数据集也试过接进来,但后期发现动作 token 准确率不太理想,最后还是移掉了。这个处理挺务实,训练里有些数据不是越多越好,混进去只会拖模型后腿。
基础设施方面,OpenVLA 在 64 张 A100 上训练了 14 天。推理时,bfloat16 精度大约占 15GB 显存,在一张 NVIDIA RTX 4090 上能跑到约 6Hz。再做量化,内存还能继续降,而且性能基本不受影响。
实验结果
和 RT-2 对比
在 BridgeData V2 和 Google Robot 上,OpenVLA 在大多数任务里都跑在前面。它的参数量只有 7B,而 RT-2-X 是 55B,差了一个数量级,但总体成功率反而更高。RT-2-X 在语义泛化上略占优势,这和它更大的互联网预训练规模有关,算是正常现象。
这里有个很具体的坑:BridgeData V2 原始数据里有不少全零动作转换,模型很容易学成'反正输出全零也不太错'。OpenVLA 的处理方式是过滤掉每个演示的第一个转换,先把这个偏置压下去;RT-2-X 则是评估时绕到第二高概率动作上。两个方案都不算优雅,但都有效。
和 Diffusion Policy 对比
单一指令任务里,Diffusion Policy 的轨迹通常更平滑,也更细腻。但一旦任务变成多对象、多步骤,还要靠语言去调节行为,预训练过的大模型策略就更占便宜。OpenVLA 在这种场景里会更稳,因为它不是只学动作,还学了语言和视觉的联合表征。
参数高效微调
LoRA 这部分的结果比较明确。
只调最后一层,或者把视觉编码器冻住,效果都不太行。Sandwich fine-tuning 的显存占用更低,但性能还是不如 LoRA。最后综合下来,LoRA 是最均衡的:只需要微调大约 1.4% 的参数,就能接近完整微调的效果。默认 rank r = 32 比较合适,单张 A100 上 10 到 15 小时就能做完。
Prismatic VLM 的设计取舍
OpenVLA 之所以能这样做,很大程度上是因为 Prismatic VLM 本身就把设计空间摸得比较细。相关工作《Prismatic VLMs: Investigating the Design Space of Visually-Conditioned Language Models》里,作者主要看了四件事:训练流程、图像处理、语言模型和扩展属性。
Prismatic 的基本结构和常规 VLM 差不多:视觉骨干提 patch 特征,投影器映射到 LM 的嵌入空间,最后由 LLM 生成输出。训练数据用了 LLaVA v1.5 混合集,差不多 100 万条图文对和纯文本样本。
几个结论挺实用。
- 两阶段训练并不是必须的。单阶段训练在性能基本不掉的情况下,能省掉 20% 到 25% 的计算成本。
- 特征融合确实有收益。把不同视觉骨干的表示拼在一起,比只用单一 backbone 更稳,SigLIP + DINOv2 是个比较强的组合。
- 图像处理要看 backbone。CLIP 更吃 naive resize;SigLIP 用 letterbox padding 也差不多。高分辨率,比如 336px 或 384px,提升明显,但算力开销也会跟着上去。
- 语言模型这块,基础模型和指令微调模型的差距没想象中大。反倒是后者更容易输出啰嗦内容,甚至开始幻觉。
- 多跑一轮训练、增加数据多样性,通常都能继续涨点性能,不是所有提升都得靠改结构。
源码怎么组织
OpenVLA 的代码库是标准的 PyTorch 风格,模块拆得比较清楚,既能单 GPU 微调,也能上多节点。
主要目录就两个。
vla-scripts/:训练、微调、部署脚本。prismatic/:模型加载、训练流程和数据预处理工具。
动作分词器
动作离散化的实现放在 prismatic/vla/action_tokenizer.py,核心类是 ActionTokenizer。它负责在连续动作和离散 token 之间来回转换。
初始化时,会先创建均匀分布的区间,再计算每个区间的中心值:
def __init__(self, tokenizer: PreTrainedTokenizerBase, bins: int = 256, min_action: float = -1, max_action: float = 1) -> None:
self.tokenizer, self.n_bins, self.min_action, self.max_action = tokenizer, bins, min_action, max_action
# 创建均匀分布的区间
self.bins = np.linspace(min_action, max_action, self.n_bins)
self.bin_centers = (self.bins[:-1] + self.bins[1:]) / 2.0
# 假设总是覆盖词汇表的最后 n_bins 个 token
self.action_token_begin_idx = int(self.tokenizer.vocab_size - (self.n_bins + 1))
调用时,先裁剪动作值,再把它离散到词表末尾那一段 token:
def __call__(self, action: np.ndarray) -> Union[str, List[str]]:
action = np.clip(action, a_min=float(self.min_action), a_max=float(self.max_action))
discretized_action = np.digitize(action, self.bins)
if len(discretized_action.shape) == 1:
return self.tokenizer.decode(list(self.tokenizer.vocab_size - discretized_action))
else:
return self.tokenizer.batch_decode((self.tokenizer.vocab_size - discretized_action).tolist())
反过来解码时,再把 token ID 映射回连续动作值:
def decode_token_ids_to_actions(self, action_token_ids: np.ndarray) -> np.ndarray:
# 映射到新索引范围
discretized_actions = self.tokenizer.vocab_size - action_token_ids
# 限制在有效区间
discretized_actions = np.clip(discretized_actions - 1, a_min=0, a_max=self.bin_centers.shape[0] - 1)
return self.bin_centers[discretized_actions]
这套实现的好处是,动作预测可以直接塞进标准 LLM 的训练和推理流程里,不需要额外设计一套专门的控制头。简单,但挺顺手。

