外观
7.8 OpenVLA:开源视觉-语言-动作模型
在上一节中,我们见证了 RT-1、RT-2 以及 RT-X 如何开辟将大语言模型先验直接注入机器人动作空间的革命性道路。然而,RT-2 等工业界模型拥有数百亿庞大参数,且其核心权重与训练代码均未公开。对于广大学术研究者与机器人初创团队而言,在单一工控机或消费级 GPU 上微调和部署这类巨型闭源模型几乎是一项不可能完成的任务。
2024 年,由斯坦福大学、伯克利加州大学等多所顶尖高校联合推出了 OpenVLA(Open Vision-Language-Action Model)。OpenVLA 不仅完全开源了其 70 亿参数(7B)的模型权重与全套跨具身训练管线,更在视觉编码表征、稳健动作分词与参数高效微调(PEFT)三大关键维度上做出了极具启发性的架构创新。

图 7.8-1:OpenVLA 结合双视觉编码器与 Llama 2 骨干,支持多机器人平台与高效微调。 出处:OpenVLA: An Open-Source Vision-Language-Action Model,Moo Jin Kim et al.,2024。
7.8.1 物理与生理基石:人类双视觉通路与开源具身智能演进
要理解 OpenVLA 在视觉感知设计上的精妙之处,我们首先需要从人类双眼如何观察世界与引导双手的生物生理学机制讲起。
1. 生物视觉系统的双通路假说(Two-Streams Hypothesis)
当我们看着一个排球迎面飞来时,人类的大脑其实在以极高的速度同时解答两道截然不同的题目:
- 第一道是语文与常识识别题:“眼前这个物体是一个白红蓝相间的排球,而不是一个沉重的铅球或一只飞鸟”(负责回答**“它是什么”**);
- 第二道是立体几何与物理测距题:“这个球当前距离我的双手还有
,正以大约 的速度向斜下方飞行,我的双臂需要以 仰角并拢垫击”(负责回答**“它在哪里、该怎么动”**)。
在神经生物学中,这两道题目分别由大脑中两条平行的神经纤维回路分工解答:
- 腹侧通路(Ventral Stream,又称“What”通路):延伸至大脑颞叶皮层,专门负责高级物体的语义概念识别与分类;
- 背侧通路(Dorsal Stream,又称“Where/How”通路):延伸至大脑顶叶皮层,专门负责捕捉物体的三维空间位置、深度距离、几何轮廓与肌肉运动引导。
在过去的多模态大模型研究中,绝大多数视觉编码器(如著名的 CLIP)几乎把全部技能点都加在了“What”语义通路上——它们在数亿张网页图文上训练,非常擅长在看到一张照片时给出“这是一只猫”的文字判断;但如果你问它“这只猫的爪子距离桌角精确相差几毫米”,纯语义编码器就会彻底失灵。如果直接把这种“近视眼”编码器装在机器人上,机器人往往能够“认出杯子”,却总因为“看不清杯柄的精确深度”而频繁抓空。
2. Prismatic 双编码器互补机制
为了让机器人同时具备“博学的常识”与“精准的空间视力”,OpenVLA 提出了 Prismatic 双视觉融合架构:同时引入了两个互补的预训练视觉主干网络:
- SigLIP(充当“语义学者”,负责 What 通路):通过海量图文对比学习训练,赋予机器人识别成千上万种日常物体名称与功能的语义常识;
- DINOv2(充当“几何工匠”,负责 Where/How 通路):通过无文字标签的纯图像空间几何自监督训练,能够精确感知微观像素级的空间深度、边缘轮廓与机械臂夹爪的精确位置。

图 7.8-2:SigLIP 与 DINOv2 提取的特征在通道维度拼接后投影至 LLM 维度。
7.8.2 核心架构一:Prismatic 多源视觉融合机制
我们来一步步推演 Prismatic 架构如何将两种异构视觉特征无缝拼接并输入大语言模型。
设工作台摄像头输入的一张 RGB 彩色图片为
两路视觉编码器分别对每一个小方块提取特征:
- SigLIP 提取的语义特征向量:
(特征维度 ); - DINOv2 提取的空间几何特征向量:
(特征维度 )。
1. 特征通道拼接与多层感知机(MLP)投影
对于第
拼接后的总维度为
随后,通过一个带有非线性激活函数 GELU 的两层多层感知机(Projector),将融合特征线性转换至与大语言模型(如 Llama 2 7B)完全相同的词向量隐藏层维度
公式符号逐一拆解:
:第一层线性变换矩阵; :高斯误差线性单元激活函数( 为标准正态分布的累积概率函数); :第二层线性变换矩阵; :最终生成的第 个视觉词元向量,它现在可以像普通的文字单词一样直接输入给大语言模型了。
深入推导:自监督 DINOv2 空间补丁特征与对比学习 SigLIP 的特征正交性数学分析(点击展开查看完整推导)
考虑两个编码器在表示空间中的互信息(Mutual Information)与正交性。 SigLIP 优化的对比损失为全局图像-文本匹配:
这促使其特征向量主要分布在语义相关的低维流形上,而对高频空间纹理具有不变性。 DINOv2 则基于知识蒸馏自监督学习局部 Patch 对应关系:
DINOv2 的自注意力图(Self-Attention Maps)保留了精准的物体边界与深度几何。两者在流形切空间上的正交互补性满足:
拼接后的联合表征同时最大化了任务相关的语义互信息
7.8.3 核心架构二:分位数归一化动作量化(Quantile Action Tokenization)
在将连续物理动作划分为离散分桶时,我们在上一节采用了区间最大值
1. 为什么绝对极值归一化会失效?
在基础统计学中,我们知道平均值极易受到极端异常值(Outliers)的拉扯。
在长达数万小时的人工遥操作轨迹中,由于示教员偶发的误触开关或急停碰撞,数据集中不可避免地存在极少数异常外点(例如某一次意外碰撞导致机械臂瞬时读数达到了正常速度的 10 倍)。
如果直接取数据绝对最大值
原本跨度仅有
2. 稳健分位数归一化(Robust Quantile Normalization)
在大型统一考试与排位统计中,为了制定稳定的排位分数线,统计老师通常不会看极个别因为缺考或答题卡填错产生的 0 分,而是看“全校排名前 1% 的高分线”与“排名前 99% 的基础线”。
在数学统计中,这种把所有数据从小到大排序后处于特定百分比位置的数值被称为分位数(Quantiles)。
OpenVLA 采用数据集统计出的**第 1 百分位数(
- 边界截断(将极端外点收敛至分位数边界):
- 基于分位数的稳健线性归一化:
- 离散分桶:
手算代入算例: 设某机械臂
- 检查截断区间:
,数值在正常区间内; - 计算归一化比例:
- 映射为词元索引:
通过剔除首尾
深入推导:基于累积经验分布函数(ECDF)的稳健分位数估计与动作截断误差界(点击展开查看完整推导)
设离散动作样本为
分位数
由于外点概率测度极小(总和仅为
7.8.4 核心架构三:参数高效微调(LoRA)的低秩几何本质
拥有 70 亿参数的 OpenVLA 若直接进行全参数微调(Full Fine-Tuning),不仅需要数张高规格企业级显卡(单卡显存
OpenVLA 全面采用**低秩自适应(Low-Rank Adaptation, LoRA)**技术,使得普通研究者仅用一张消费级显卡(如 RTX 4090)即可在几小时内完成特定机器人任务的高效适配。

图 7.8-3:LoRA 用低秩矩阵分解参数更新,在冻结主权重时实现高效适配。 出处:LoRA: Low-Rank Adaptation of Large Language Models,Edward J. Hu et al.,2021。

图 7.8-4:新机器人平台上的适配任务比较全量与参数高效微调的实际效果。 出处:OpenVLA: An Open-Source Vision-Language-Action Model,Moo Jin Kim et al.,2024。
1. 矩阵分解的初等代数直觉
在初等代数中解多元线性方程组时,经常会发现如果第三个方程恰好是前两个方程相加得到的,那么第三个方程并没有提供新的信息,方程组的“有效独立未知数”其实减少了。
在线性代数中,一个庞大矩阵内部真正独立起作用的有效自由度,被称为矩阵的秩(Rank)。
LoRA 提出一个深刻的洞察:预训练大模型已经具备了极为强大的世界常识。为了让它学会‘在新的工作台上抓取特定的杯子’这一具体技能,其 70 亿参数需要的调整变化量

图 7.8-5:低秩支路先把 d_in 压到 r,再升回 d_out,与冻结基座输出相加;反向梯度只进入 A、B。
基于这一洞察,LoRA 将原本巨大的参数更新矩阵
其中:
:降维压缩矩阵,将输入特征从高维空间压缩到极低的秩 空间(例如取 或 ); :升维还原矩阵,将低维特征重新映射回原本的输出维度。
在前向传播过程中,庞大的原始预训练权重
其中
2. 算力与显存节省的惊人代数对比
设大语言模型中某一层全连接权重维度为
- 全参数微调:需要更新的参数量为
(约 个浮点数); - LoRA 低秩微调(取
):参数量为 (仅 个参数)!
需要更新的参数量仅仅是全量的
深入推导:LoRA 低秩矩阵分解的本征维度假说与奇异值分解(SVD)近似(点击展开查看完整推导)
根据矩阵奇异值分解(SVD)定理,任意秩为
根据 Eckart-Young-Mirsky 定理,对于任意设定的低秩阶数
在深度过参数化大模型中,由于参数间的高度共线性,奇异值谱
7.8.5 纯底层 PyTorch 代码实现:OpenVLA 核心组件与端到端前向推理
下面我们使用纯底层 PyTorch 算子手写实现 OpenVLA 的核心模块,包括分位数动作离散化器、Prismatic 双视觉投影层、LoRA 线性层以及微型 OpenVLA 主干网络。
python
import torch
import torch.nn as nn
import torch.nn.functional as F
class OpenVLAActionTokenizer:
"""
基于分位数统计的动作离散化与反量化器
"""
def __init__(self, num_bins: int = 256, q01: torch.Tensor = None, q99: torch.Tensor = None):
self.num_bins = num_bins
# 默认 7 维动作的分位数边界
self.q01 = q01 if q01 is not None else -torch.ones(7)
self.q99 = q99 if q99 is not None else torch.ones(7)
def tokenize(self, continuous_actions: torch.Tensor) -> torch.Tensor:
"""
连续动作 -> 稳健离散词元索引
:param continuous_actions: (B, 7)
:return: (B, 7) 整数张量
"""
self.q01 = self.q01.to(continuous_actions.device)
self.q99 = self.q99.to(continuous_actions.device)
# 1. 分位数截断
clamped = torch.max(torch.min(continuous_actions, self.q99), self.q01)
# 2. 稳健归一化到 [0, 1]
norm_actions = (clamped - self.q01) / (self.q99 - self.q01 + 1e-8)
# 3. 均匀分桶取整
tokens = torch.round(norm_actions * (self.num_bins - 1)).long()
return tokens
def detokenize(self, bin_indices: torch.Tensor) -> torch.Tensor:
"""
离散词元索引 -> 连续动作恢复
"""
self.q01 = self.q01.to(bin_indices.device)
self.q99 = self.q99.to(bin_indices.device)
norm_actions = bin_indices.float() / (self.num_bins - 1)
continuous_actions = norm_actions * (self.q99 - self.q01) + self.q01
return continuous_actions
class PrismaticProjector(nn.Module):
"""
Prismatic 双流视觉特征融合投影层
将 SigLIP (语义) 与 DINOv2 (空间几何) 特征通道拼接并投影至 LLM 维度
"""
def __init__(self, siglip_dim: int = 256, dinov2_dim: int = 256, llm_dim: int = 512):
super().__init__()
fused_dim = siglip_dim + dinov2_dim
self.mlp = nn.Sequential(
nn.Linear(fused_dim, llm_dim, bias=False),
nn.GELU(),
nn.Linear(llm_dim, llm_dim, bias=False)
)
def forward(self, siglip_feat: torch.Tensor, dinov2_feat: torch.Tensor) -> torch.Tensor:
"""
:param siglip_feat: (B, num_patches, siglip_dim)
:param dinov2_feat: (B, num_patches, dinov2_dim)
:return: (B, num_patches, llm_dim)
"""
# 通道维度拼接
fused = torch.cat([siglip_feat, dinov2_feat], dim=-1)
return self.mlp(fused)
class LoRALinear(nn.Module):
"""
手写底层 LoRA (Low-Rank Adaptation) 线性层
h = W_0 * x + (alpha / r) * B * A * x
"""
def __init__(self, base_linear: nn.Linear, r: int = 16, lora_alpha: float = 32.0):
super().__init__()
self.base_linear = base_linear
# 冻结原始基座权重
self.base_linear.weight.requires_grad = False
if self.base_linear.bias is not None:
self.base_linear.bias.requires_grad = False
in_dim = base_linear.in_features
out_dim = base_linear.out_features
self.r = r
self.scaling = lora_alpha / r
# 初始化 A 为高斯分布,B 为全 0
self.lora_A = nn.Parameter(torch.randn(r, in_dim) * (1.0 / r))
self.lora_B = nn.Parameter(torch.zeros(out_dim, r))
def forward(self, x: torch.Tensor) -> torch.Tensor:
base_out = self.base_linear(x)
# 低秩支路: x -> (x @ A^T) @ B^T
lora_out = (x @ self.lora_A.T @ self.lora_B.T) * self.scaling
return base_out + lora_out
class SimpleOpenVLA(nn.Module):
"""
微型 OpenVLA 演示模型
"""
def __init__(self, text_vocab_size: int = 32000, action_bins: int = 256, llm_dim: int = 512):
super().__init__()
self.projector = PrismaticProjector(siglip_dim=256, dinov2_dim=256, llm_dim=llm_dim)
self.total_vocab_size = text_vocab_size + action_bins
self.embedding = nn.Embedding(self.total_vocab_size, llm_dim)
decoder_layer = nn.TransformerEncoderLayer(
d_model=llm_dim, nhead=4, dim_feedforward=llm_dim * 2, batch_first=True
)
self.llm_backbone = nn.TransformerEncoder(decoder_layer, num_layers=2)
self.lm_head = nn.Linear(llm_dim, self.total_vocab_size, bias=False)
def forward(
self,
siglip_tokens: torch.Tensor,
dinov2_tokens: torch.Tensor,
text_action_tokens: torch.Tensor
) -> torch.Tensor:
"""
前向计算
"""
# 1. 视觉特征融合并投影
vis_emb = self.projector(siglip_tokens, dinov2_tokens) # (B, N_patches, llm_dim)
# 2. 文本/动作嵌入
tok_emb = self.embedding(text_action_tokens) # (B, seq_len, llm_dim)
# 3. 序列拼接
seq = torch.cat([vis_emb, tok_emb], dim=1) # (B, N_patches + seq_len, llm_dim)
# 4. 因果自回归掩码
seq_len = seq.size(1)
causal_mask = torch.triu(
torch.full((seq_len, seq_len), float("-inf"), device=seq.device), diagonal=1
)
hidden = self.llm_backbone(seq, mask=causal_mask)
logits = self.lm_head(hidden)
return logits
# ===================================================================
# 单元测试与低秩参数量校验
# ===================================================================
if __name__ == "__main__":
batch_size = 2
num_patches = 16
action_bins = 256
llm_dim = 512
# 1. 测试分位数动作分词器
q01 = torch.tensor([-0.50] * 7)
q99 = torch.tensor([0.50] * 7)
tokenizer = OpenVLAActionTokenizer(num_bins=action_bins, q01=q01, q99=q99)
dummy_act = torch.tensor([[0.25, -0.10, 0.00, 0.50, -0.60, 0.10, 0.45]])
toks = tokenizer.tokenize(dummy_act)
recon = tokenizer.detokenize(toks)
print(f"[OpenVLA Test] 输入动作: {dummy_act.numpy().round(3)}")
print(f"[OpenVLA Test] 分位数离散索引: {toks.numpy()}")
print(f"[OpenVLA Test] 最大重构误差: {(recon - dummy_act).abs().max().item():.6f}")
# 2. 测试 LoRA 模块与参数量统计
base_dense = nn.Linear(4096, 4096)
lora_dense = LoRALinear(base_dense, r=16, lora_alpha=32.0)
trainable_params = sum(p.numel() for p in lora_dense.parameters() if p.requires_grad)
frozen_params = sum(p.numel() for p in lora_dense.parameters() if not p.requires_grad)
print(f"[LoRA Test] 可训练参数量 (A + B): {trainable_params} ({trainable_params / frozen_params * 100:.2f}%)")
print(f"[LoRA Test] 冻结基座参数量 (W_0): {frozen_params}")
assert trainable_params == 16 * 4096 * 2, "LoRA 参数量计算不符!"
# 3. 测试 OpenVLA 前向推理
model = SimpleOpenVLA(text_vocab_size=32000, action_bins=action_bins, llm_dim=llm_dim)
model.eval()
dummy_siglip = torch.randn(batch_size, num_patches, 256)
dummy_dinov2 = torch.randn(batch_size, num_patches, 256)
dummy_input_tokens = torch.randint(0, 32000, (batch_size, 10))
with torch.no_grad():
out_logits = model(dummy_siglip, dummy_dinov2, dummy_input_tokens)
print(f"[OpenVLA Test] 前向推理输出 Logits 形状: {out_logits.shape}")
assert out_logits.shape == (batch_size, num_patches + 10, 32000 + action_bins)
print("✓ OpenVLA 双通路视觉与 LoRA 微调单测全部通过!")7.8.6 本节小结
回顾本节内容,我们建立了一条从生物视觉双通路走向现代开源大一统 VLA 模型的完整知识脉络:
- Prismatic 双视觉通路:融合语义导向的 SigLIP 与空间几何导向的 DINOv2,克服了单一 CLIP 编码器空间定位迟钝的缺陷;
- 稳健分位数动作量化:通过第 1 与第 99 分位数截断,规避了海量多源数据中的极端外点干扰,最大化保留了 256 桶离散化的物理控制分辨率;
- 低秩自适应(LoRA)的几何本质:基于下游任务适配的低秩子空间假说,将参数微调量严格限制在
左右,使大模型能够在低算力下敏捷迁移到新机器人形态。

