OpenVLA 核心原理深度解析
这是一份"脱离实现细节也能读懂"的原理说明。
它不按工程结构讲,而按一个 Vision-Language-Action 系统为什么这样设计、数据如何流动、模型到底学习什么、推理时如何把 token 变回机器人动作来讲。
目录
- 先用一句话建立直觉
- [OpenVLA 到底在解决什么问题](#OpenVLA 到底在解决什么问题)
- [核心思想:把机器人控制改写成 token 生成](#核心思想:把机器人控制改写成 token 生成)
- [从机器人动作到语言模型 token](#从机器人动作到语言模型 token)
- [从图像到语言模型可理解的 embedding](#从图像到语言模型可理解的 embedding)
- 训练样本如何构造
- 模型训练时到底在优化什么
- 推理时如何从图片和指令得到真实动作
- 多机器人数据集为什么需要标准化
- 两种训练范式:全量训练与参数高效微调
- 部署服务的抽象链路
- 核心伪代码
- 关键设计取舍
- 常见误解
- 如果要改造系统,应优先理解哪些抽象接口
- 最终总结
先用一句话建立直觉
OpenVLA 的本质是:
text
把"机器人看到什么、听到什么指令、下一步怎么动"这个控制问题,
改写成"多模态语言模型在图像条件下生成一串动作 token"的问题。
普通视觉语言模型通常是:
text
图片 + 问题 -> 文本回答
OpenVLA 是:
text
图片 + 任务指令 -> 动作 token -> 连续机器人动作
类比一下:
一个传统机器人策略像"直接输出方向盘角度和油门大小"的驾驶控制器。
OpenVLA 更像是先发明一套"动作密码本",把方向盘角度和油门大小编码成若干特殊词,然后让一个会看图的语言模型根据路况和指令生成这些特殊词。最后系统再把特殊词翻译回真实控制量。
这个思路看起来绕,但它把机器人动作预测接入了语言模型最擅长的能力:
text
根据上下文生成下一个 token。
OpenVLA 到底在解决什么问题
机器人模仿学习里,一条训练数据大致包含三类信息:
text
1. 观测
- 相机图像
- 可选的本体状态,例如关节角、末端位置、夹爪状态
2. 任务
- 人类语言指令,例如"把胡萝卜放进碗里"
3. 动作
- 机器人专家在这个状态下执行的动作
- 常见形式是连续向量,例如:
[x方向移动, y方向移动, z方向移动, 旋转1, 旋转2, 旋转3, 夹爪开合]
模型要学的是一个条件策略:
text
policy(image, instruction) -> action
也就是:
text
当前画面 + 当前任务 -> 下一步机器人动作
传统做法可能直接训练一个神经网络回归动作:
text
图片特征 + 语言特征 -> MLP -> 连续动作向量
OpenVLA 选择了另一条路:
text
图片特征 + 语言 token -> LLM -> 动作 token -> 连续动作向量
这样做的关键收益是:
- 可以复用大型语言模型的自回归生成框架。
- 可以复用视觉语言模型的图文对齐能力。
- 可以把"动作预测"纳入通用 token 预测范式。
- 可以用语言指令自然地控制机器人行为。
核心思想:把机器人控制改写成 token 生成
语言模型最擅长的问题是:
text
给定前文,预测下一个 token。
例如:
text
输入: The robot should pick up the
输出: cup
OpenVLA 想让语言模型做:
text
输入: 图像 + "机器人应该如何执行:把杯子拿起来?"
输出: 动作 token 1, 动作 token 2, ..., 动作 token N
这里的动作 token 不是普通词语,而是人为约定的"动作编码符号"。
如果动作是 7 维:
text
action = [a1, a2, a3, a4, a5, a6, a7]
模型就可以生成 7 个动作 token:
text
tokens = [t1, t2, t3, t4, t5, t6, t7]
每个 token 对应一个动作维度的离散值。
于是原问题:
text
回归一个连续向量
被改写成:
text
生成一串离散 token
这一步是 OpenVLA 的思想核心。
从机器人动作到语言模型 token
机器人动作通常是连续数值。比如机械臂末端移动动作:
text
dx = 0.021
dy = -0.014
dz = 0.007
语言模型不能天然生成"高精度浮点控制量"。它生成的是 token。因此需要一个动作编码器。
第一步:归一化动作
不同机器人、不同数据集的动作尺度不一样。
例如:
text
机器人 A:
dx 的合理范围可能是 [-0.05, 0.05]
机器人 B:
dx 的合理范围可能是 [-0.5, 0.5]
如果直接混在一起训练,模型会很混乱。
所以先把动作统一映射到一个标准区间,通常是:
text
[-1, 1]
一种常见的分位数归一化方式是:
text
normalized = 2 * (raw - q01) / (q99 - q01) - 1
其中:
text
q01 = 这个动作维度在数据集中的 1% 分位数
q99 = 这个动作维度在数据集中的 99% 分位数
使用分位数而不是最小值/最大值,是为了降低异常动作值的影响。
q01 和 q99 到底是什么意思
分位数可以理解成:
text
把一堆数从小到大排序后,位于某个百分比位置的值。
假设某个数据集里,dx 这个动作维度一共有 10000 个样本。把所有 dx 从小到大排序:
text
dx_sorted = [
最小值,
第二小,
第三小,
...
最大值
]
那么:
text
q01 = 排在 1% 位置附近的值
q99 = 排在 99% 位置附近的值
更白话地说:
text
q01 ≈ "正常小值边界"
q99 ≈ "正常大值边界"
它们不是最小值和最大值,而是把最极端的少量异常值排除掉之后的范围。
举个具体例子。假设某个机器人数据集里,dx 的一批值从小到大大概是:
text
[-10.0, -0.06, -0.05, -0.04, -0.03,
-0.02, -0.01, 0.00, 0.01, 0.02,
0.03, 0.04, 0.05, 0.06, 0.07,
0.08, 0.09, 0.10, 0.11, 8.0]
这里 -10.0 和 8.0 很可能是异常值,例如传感器错误、数据记录异常、碰撞瞬间动作等。
如果用最小值/最大值归一化:
text
min = -10.0
max = 8.0
那么正常动作值,比如 0.05,会被压到非常靠近中间的位置。模型会把大量正常动作都挤在很窄的归一化区间里,难以区分细微差别。
如果用分位数,系统会更关注大多数正常数据。例如可以近似认为:
text
q01 ≈ -0.06
q99 ≈ 0.11
这表示:
text
绝大多数正常 dx 动作大概落在 [-0.06, 0.11] 之间。
然后再把这个正常范围映射到 [-1, 1]。
例如:
text
q01 = -0.06
q99 = 0.11
raw = 0.025
代入公式:
text
normalized = 2 * (raw - q01) / (q99 - q01) - 1
= 2 * (0.025 - (-0.06)) / (0.11 - (-0.06)) - 1
= 2 * 0.085 / 0.17 - 1
= 0
所以 raw = 0.025 位于正常范围中间,会被映射成 0。
边界也很好理解:
text
raw = -0.06 -> normalized = -1
raw = 0.11 -> normalized = 1
因此,使用 q01/q99 的含义是:
text
不要让最极端的 1% 小值和最极端的 1% 大值决定整个动作范围;
用中间 98% 的主要数据范围来定义"正常动作尺度"。
这能让模型更关注真实控制中常见的动作变化,而不是被少量异常值带偏。
第二步:离散化
归一化后的动作在 [-1, 1] 之间。
OpenVLA 风格的离散化会把这个区间切成固定数量的桶,例如 256 个桶。
text
[-1, 1] -> 256 个离散区间
于是:
text
-1.0 附近 -> 第 1 桶
0.0 附近 -> 中间桶
1.0 附近 -> 第 256 桶
类比:
连续动作像一把可以无级调节的旋钮;
离散化像把旋钮改成 256 档。
精度少了一点,但模型从"预测小数"变成了"选择档位"。
第三步:把桶映射到 token
离散桶还不是语言模型 token。
还需要一个映射:
text
桶编号 -> token id
一种简单策略是使用语言模型词表中很少用于自然语言的末尾 token。
假设词表大小是:
text
vocab_size = 32000
动作桶数量是:
text
n_bins = 256
那么可以约定:
text
词表最后 256 个 token 专门表示动作桶
所以:
text
第 1 桶 -> 某个靠近词表末尾的 token
第 128 桶 -> 另一个动作 token
第 256 桶 -> 另一个动作 token
这样,语言模型不需要新增一个动作输出头。它仍然输出整个词表上的 logits,只不过训练会让它在动作位置生成这些动作 token。
从图像到语言模型可理解的 embedding
语言模型原本只理解 token embedding,不理解图片像素。
OpenVLA 需要把图片变成可以插入语言模型上下文的向量。
整体过程是:
text
图片
-> 视觉编码器
-> 一串图像 patch 特征
-> 投影器
-> 与 LLM hidden size 匹配的图像 embedding
-> 插入语言模型输入序列
视觉编码器
视觉编码器通常是 ViT 类模型。
它会把图片切成 patch,每个 patch 变成一个向量。
text
图片 -> [patch_1, patch_2, ..., patch_M]
每个 patch 向量表示图片某个区域的视觉语义。
可以类比成:
人看一张桌面图片,会注意到"左边有杯子""中间有碗""右侧有夹爪"。
ViT patch 特征就是让模型用一堆局部视觉向量表达这些区域信息。
投影器
视觉编码器输出的向量维度通常和语言模型 hidden size 不一致。
例如:
text
视觉特征维度 = 1024
语言模型 hidden size = 4096
不能直接拼接。
因此需要一个投影器:
text
projector: vision_dim -> llm_hidden_dim
投影器可以是线性层,也可以是小 MLP。
它的角色像翻译器:
text
视觉编码器说"视觉向量语言"
语言模型说"token embedding 语言"
projector 负责把前者翻译成后者
插入语言模型上下文
处理文本时,语言模型本来会得到:
text
[BOS], [In], [:], [What], [action], ...
每个 token 会变成 embedding:
text
[text_emb_0, text_emb_1, text_emb_2, ...]
OpenVLA 会把图片 patch embedding 插进去:
text
[BOS],
[image_emb_1], [image_emb_2], ..., [image_emb_M],
[In], [:], [What], [action], ...
注意:图片没有被转换成自然语言描述。
它是以 embedding 形式进入语言模型上下文的。
训练样本如何构造
一条机器人数据:
text
图像: 当前相机画面
指令: "put the carrot in the bowl"
动作: [0.1, -0.2, 0.0, ...]
会被改写成类似对话的格式:
text
输入部分:
In: What action should the robot take to put the carrot in the bowl?
Out:
输出部分:
<动作token1><动作token2>...<动作tokenN>
拼起来是:
text
In: What action should the robot take to put the carrot in the bowl?
Out: <动作token1><动作token2>...<动作tokenN>
然后整段文本被 tokenizer 编码成:
text
input_ids = [prompt tokens..., action tokens...]
训练时,labels 通常从 input_ids 复制而来,但 prompt 部分会被屏蔽:
text
labels = [-100, -100, ..., -100, action_token_1, action_token_2, ...]
其中 -100 是常见的 ignore index,表示这些位置不参与 loss。
这非常重要。
模型不是被训练去复述:
text
In: What action should...
模型只在动作 token 位置被监督。
也就是说,训练目标是:
text
看到图像和指令后,把动作 token 预测对。
模型训练时到底在优化什么
虽然 OpenVLA 是机器人模型,但它的训练目标本质上仍是语言模型的 next-token prediction。
只不过预测目标换成了动作 token。
训练时模型看到:
text
图像 embedding + prompt token
它要生成:
text
动作 token 1
动作 token 2
...
动作 token N
损失函数
语言模型通常输出:
text
每个位置对整个词表的 logits
在动作位置上,它会对所有 token 给出概率。
训练目标要求:
text
正确动作 token 的概率尽可能高。
所以 loss 是交叉熵:
text
loss = cross_entropy(predicted_token_distribution, target_action_token)
因为 prompt 和图片 patch 的 label 都被忽略,所以 loss 主要来自:
text
动作 token 位置
训练指标
除了 loss,常见还会看两个动作相关指标。
动作 token accuracy
看模型生成的动作 token 是否和真实动作 token 完全一致。
text
预测 token == 真实 token
这个指标衡量的是离散动作桶分类是否准确。
连续动作 L1 误差
把预测 token 和真实 token 都解码回连续动作,然后计算绝对误差。
text
L1 = mean(abs(predicted_action - target_action))
这个指标更接近机器人控制意义上的误差。
举例:
text
真实动作桶 = 128
预测动作桶 = 129
token accuracy 会认为错了,但连续值可能只差一点点。
所以 L1 误差可以补充 token accuracy 的局限。
推理时如何从图片和指令得到真实动作
推理时没有真实动作,只有:
text
当前图片
任务指令
系统要输出:
text
真实机器人动作向量
完整链路如下:
text
1. 构造 prompt
2. 处理图片和文本
3. 图片变成视觉 embedding
4. 文本变成 token embedding
5. 图像 embedding 插入语言模型上下文
6. 语言模型 generate 动作 token
7. 动作 token 解码成 [-1, 1] 范围内的归一化动作
8. 用数据集统计量反归一化
9. 输出机器人真实动作
为什么需要反归一化
模型内部动作空间是统一的:
text
[-1, 1]
但真实机器人控制空间不是。
所以需要把:
text
normalized_action
恢复成:
text
real_action
常见反归一化公式:
text
real = 0.5 * (normalized + 1) * (q99 - q01) + q01
其中 q01 和 q99 来自训练数据统计。
如果模型混合训练了多个机器人数据集,就必须知道当前要使用哪套统计量。
可以类比成:
模型说的是"标准动作语言":向右移动 0.3 个标准单位。
但不同机器人 0.3 个标准单位对应的真实距离不同。
反归一化就是把标准单位换算成当前机器人实际控制单位。
多机器人数据集为什么需要标准化
OpenVLA 通常希望利用大量机器人数据。
问题是,不同数据集差异很大:
text
数据集 A:
图片字段叫 camera_image
指令字段叫 instruction
动作是 7 维
数据集 B:
图片字段叫 image_primary
指令字段叫 language_instruction
动作是 10 维
数据集 C:
有腕部相机
有深度图
gripper 表示方式也不同
如果训练代码直接适配每个数据集,会变得不可维护。
所以系统需要一个统一数据协议。
抽象后的样本应该长这样:
text
sample:
image_primary:
主视角 RGB 图像
language_instruction:
自然语言任务
action:
统一语义下的动作向量
dataset_name:
样本来自哪个数据集
statistics:
动作归一化和反归一化需要的统计量
数据标准化做什么
数据标准化主要解决四类问题。
字段名统一
把不同数据集中的图像、语言、动作字段映射到统一名字。
text
workspace_image / camera_rgb / image -> image_primary
task_text / instruction / language -> language_instruction
动作语义统一
不同机器人动作可能含义不同。
例如:
text
前 3 维是末端平移
中间 3 维是末端旋转
最后 1 维是夹爪
系统需要明确每个维度的语义,尤其是:
text
哪些维度是相对动作
哪些维度是绝对动作
哪些维度需要归一化
哪些维度不该归一化
夹爪维度经常比较特殊,因为它可能是二值开合,不适合像连续位移那样归一化。
数值尺度统一
动作要归一化到模型内部空间。
这一步让多机器人数据能共享一个动作 token 体系。
数据混合采样
多数据集训练时,需要决定每个数据集被采样的比例。
抽象地说:
text
mixed_dataset = sample_from([
dataset_A with weight_A,
dataset_B with weight_B,
dataset_C with weight_C,
])
这样模型训练时会看到来自多个机器人和任务的数据。
两种训练范式:全量训练与参数高效微调
OpenVLA 类系统一般支持两类训练。
全量训练
全量训练会更新大量参数,可能包括:
text
视觉编码器
投影器
语言模型
优点:
text
表达能力强
适合大规模预训练
能深度适配机器人动作生成
缺点:
text
显存需求高
训练成本高
分布式工程复杂
常见策略还会选择冻结一部分模块:
text
冻结视觉编码器,只训练投影器和语言模型
冻结大部分语言模型,只训练最后几层
全部解冻,端到端训练
参数高效微调
参数高效微调不直接更新全部大模型参数,而是在模型中插入少量可训练参数。
常见方式是 LoRA。
它的直觉是:
不改整本书,只在关键页贴便签;
便签数量很少,但足以让模型适配新任务。
优点:
text
显存需求低
训练更快
适合新任务、新机器人快速适配
缺点:
text
适配能力可能不如全量训练
如果新机器人分布差异太大,可能不够
两种训练范式的共同核心不变:
text
图像 + 指令 -> 动作 token
部署服务的抽象链路
部署时,OpenVLA 通常作为一个远程策略服务。
机器人控制端发送:
text
当前相机图片
当前任务指令
可选的动作反归一化配置名
模型服务返回:
text
动作向量
抽象服务流程:
text
1. 接收请求
2. 解码图片
3. 构造 prompt
4. 图片和文本预处理
5. 模型生成动作 token
6. token 解码成动作
7. 动作反归一化
8. 返回动作
这个服务层本身很薄。
真正重要的是:
text
processor + multimodal model + action decoder + unnormalizer
核心伪代码
下面伪代码只表达 OpenVLA 的核心机制,不绑定任何具体工程结构。每个关键步骤都包含中文注释。
伪代码 1:动作归一化
python
def normalize_action(raw_action, statistics, mask):
# raw_action:
# 真实机器人动作,例如机械臂末端位移、旋转、夹爪开合。
#
# statistics:
# 从训练数据中统计出的每个动作维度范围。
# 这里用 q01/q99 表示 1% 和 99% 分位数,用于减少异常值影响。
#
# mask:
# 指示哪些动作维度需要归一化。
# 例如夹爪维度可能已经是二值信号,不一定适合连续归一化。
low = statistics.q01
high = statistics.q99
# 将动作线性映射到 [-1, 1]。
# 当 raw_action == low 时,normalized 约等于 -1;
# 当 raw_action == high 时,normalized 约等于 1。
normalized = 2 * (raw_action - low) / (high - low + 1e-8) - 1
# 将极端值裁剪到 [-1, 1],避免离散化时越界。
normalized = clip(normalized, -1, 1)
# 对不需要归一化的维度,保留原始值。
# 这样可以避免破坏夹爪等特殊动作维度的语义。
normalized = where(mask, normalized, raw_action)
return normalized
伪代码 2:连续动作编码为动作 token
python
def encode_action_as_tokens(normalized_action, tokenizer, n_bins=256):
# normalized_action:
# 已归一化到 [-1, 1] 附近的动作向量。
#
# tokenizer:
# 语言模型使用的 tokenizer。
#
# n_bins:
# 每个动作维度被切成多少个离散桶。
# 创建离散桶边界。
# 例如 n_bins=256 时,[-1, 1] 会被切成 256 个刻度。
bin_edges = linspace(-1, 1, n_bins)
# 先裁剪,确保所有动作值都在可表示范围内。
clipped_action = clip(normalized_action, -1, 1)
# 找到每个动作维度落在哪个桶中。
# 这一步把连续控制值变成离散类别编号。
bin_ids = digitize(clipped_action, bin_edges)
# 将离散桶编号映射到语言模型词表中的动作 token id。
# 一种常见做法是使用词表尾部 token 作为动作 token。
action_token_ids = tokenizer.vocab_size - bin_ids
# 返回 token id 序列。
# 每个动作维度对应一个 token。
return action_token_ids
伪代码 3:构造训练样本
python
def build_training_example(image, instruction, raw_action, statistics, tokenizer):
# image:
# 当前机器人相机图像。
#
# instruction:
# 人类语言任务,例如"把杯子放到盘子上"。
#
# raw_action:
# 专家示范中的真实动作。
#
# statistics:
# 用于动作归一化的数据集统计量。
# 1. 将真实动作归一化到标准动作空间。
# 这样不同机器人或不同数据集的动作可以共用一套离散 token 体系。
normalized_action = normalize_action(
raw_action,
statistics=statistics.action,
mask=statistics.action_mask,
)
# 2. 将归一化动作编码成动作 token。
# 从这里开始,机器人动作就被转成语言模型可以预测的 token 序列。
action_token_ids = encode_action_as_tokens(
normalized_action,
tokenizer=tokenizer,
)
# 3. 构造自然语言 prompt。
# 这一步把机器人控制任务包装成语言模型熟悉的问答格式。
prompt = "In: What action should the robot take to " + instruction.lower() + "?\nOut:"
# 4. 将 prompt 编码为文本 token。
# 这些 token 提供任务语义,但不作为训练目标。
prompt_token_ids = tokenizer.encode(prompt)
# 5. 拼接完整输入。
# 语言模型看到 prompt,并在后续位置学习生成动作 token。
input_ids = concat(prompt_token_ids, action_token_ids, [EOS_TOKEN])
# 6. 构造 labels。
# labels 与 input_ids 对齐,但 prompt 部分设置为 IGNORE_INDEX。
labels = copy(input_ids)
# 7. 屏蔽 prompt 部分。
# 这表示模型不需要学习复述问题,只需要学习预测动作。
labels[0 : len(prompt_token_ids)] = IGNORE_INDEX
# 8. 图像预处理。
# 将原始 RGB 图像 resize、归一化,并变成视觉编码器可接收的张量。
pixel_values = preprocess_image(image)
# 9. 返回一个训练样本。
# 后续 batch collator 会负责 padding 和堆叠。
return {
"pixel_values": pixel_values,
"input_ids": input_ids,
"labels": labels,
}
伪代码 4:多模态前向传播
python
def multimodal_forward(model, pixel_values, input_ids, attention_mask, labels):
# pixel_values:
# 已预处理的图片张量。
#
# input_ids:
# prompt token + 动作 token 的输入序列。
#
# labels:
# prompt 部分被 IGNORE_INDEX 屏蔽,只监督动作 token。
# 1. 图片进入视觉编码器,得到 patch 级视觉特征。
# 每个 patch 表示图片中的一个局部区域。
image_patch_features = model.vision_encoder(pixel_values)
# 2. 用投影器把视觉特征转换到语言模型 hidden size。
# 只有维度一致,图像 embedding 才能和文本 embedding 拼接。
image_embeddings = model.projector(image_patch_features)
# 3. 文本 token id 进入语言模型 embedding table。
# 这得到 prompt 和动作 token 的文本 embedding。
text_embeddings = model.language_model.embed_tokens(input_ids)
# 4. 将图像 embedding 插入文本 embedding 序列。
# 常见方式是插到 BOS 之后。
multimodal_embeddings = concat(
text_embeddings[:, :1, :],
image_embeddings,
text_embeddings[:, 1:, :],
dim=1,
)
# 5. 为图像 patch 构造 attention mask。
# 图像 patch 是有效上下文,所以 mask 为 1。
image_attention_mask = ones(batch_size, num_image_patches)
# 6. 拼接新的 attention mask。
# 新 mask 必须和 multimodal_embeddings 的序列长度一致。
multimodal_attention_mask = concat(
attention_mask[:, :1],
image_attention_mask,
attention_mask[:, 1:],
dim=1,
)
# 7. 为图像 patch 构造 label。
# 图像 patch 不是要生成的 token,所以它们的 label 必须忽略。
image_labels = full(
shape=(batch_size, num_image_patches),
value=IGNORE_INDEX,
)
# 8. 拼接新的 labels。
# 最终 loss 只会在动作 token 位置产生。
multimodal_labels = concat(
labels[:, :1],
image_labels,
labels[:, 1:],
dim=1,
)
# 9. 调用语言模型。
# 因为序列中包含图像 embedding,它们没有 token id,
# 所以这里传入 inputs_embeds,而不是只传 input_ids。
output = model.language_model(
inputs_embeds=multimodal_embeddings,
attention_mask=multimodal_attention_mask,
labels=multimodal_labels,
)
# 10. 输出包含 loss 和 logits。
# loss 用于训练,logits 可用于计算动作 token accuracy。
return output
伪代码 5:训练循环
python
def train_openvla(model, dataset, optimizer):
# model:
# 由视觉编码器、投影器、语言模型组成的多模态动作生成模型。
#
# dataset:
# 已标准化的机器人数据流,每条样本包含图片、指令和专家动作。
for raw_batch in dataset:
# 1. 将原始机器人数据转成模型训练 batch。
# 包括动作归一化、动作 token 化、prompt 构造、图片预处理和 label mask。
batch = build_batch(raw_batch)
# 2. 多模态前向传播。
# 模型会把图片 embedding 插入语言模型上下文,并预测动作 token。
output = multimodal_forward(
model=model,
pixel_values=batch.pixel_values,
input_ids=batch.input_ids,
attention_mask=batch.attention_mask,
labels=batch.labels,
)
# 3. 读取训练 loss。
# 因为 prompt 和图像 patch 都被忽略,所以这个 loss 主要来自动作 token。
loss = output.loss
# 4. 反向传播。
# 梯度会更新所有被设置为可训练的参数。
loss.backward()
# 5. 可选梯度裁剪。
# 大模型训练中,梯度裁剪能减少不稳定更新。
clip_grad_norm(model.trainable_parameters)
# 6. 参数更新。
optimizer.step()
# 7. 清空梯度,准备下一步训练。
optimizer.zero_grad()
# 8. 计算动作 token 准确率。
# 这是离散 token 层面的监控指标。
token_accuracy = compute_action_token_accuracy(
logits=output.logits,
labels=batch.labels,
)
# 9. 计算连续动作误差。
# 把预测 token 解码回连续动作,再和真实动作比较。
action_l1 = compute_continuous_action_l1(
logits=output.logits,
labels=batch.labels,
)
# 10. 记录指标。
log({
"loss": loss,
"token_accuracy": token_accuracy,
"action_l1": action_l1,
})
伪代码 6:动作 token 解码为归一化动作
python
def decode_tokens_to_normalized_action(action_token_ids, tokenizer, n_bins=256):
# action_token_ids:
# 模型生成的动作 token id 序列。
#
# tokenizer:
# 用于知道词表大小,从而反推出桶编号。
# 1. 重建桶中心。
# 每个桶中心代表该离散 token 对应的连续动作近似值。
bin_edges = linspace(-1, 1, n_bins)
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
# 2. 根据 token id 反推桶编号。
# 编码时使用词表尾部 token,因此解码时反向计算。
bin_ids = tokenizer.vocab_size - action_token_ids
# 3. 将桶编号转换为数组索引。
# 通常 digitize 的桶编号从 1 开始,而数组索引从 0 开始。
bin_indices = bin_ids - 1
# 4. 裁剪索引,避免边界情况下越界。
bin_indices = clip(bin_indices, 0, len(bin_centers) - 1)
# 5. 查表得到归一化动作。
# 此时动作仍在模型内部标准空间,不可直接发给机器人。
normalized_action = bin_centers[bin_indices]
return normalized_action
伪代码 7:动作反归一化
python
def unnormalize_action(normalized_action, statistics, mask):
# normalized_action:
# 模型输出并解码后的 [-1, 1] 动作。
#
# statistics:
# 当前机器人或当前数据集对应的动作统计量。
#
# mask:
# 指示哪些维度需要从标准空间还原到真实动作空间。
low = statistics.q01
high = statistics.q99
# 将 [-1, 1] 线性映射回真实动作范围。
real_action = 0.5 * (normalized_action + 1) * (high - low) + low
# 对不需要反归一化的维度,保留 normalized_action。
# 这通常用于夹爪等特殊维度。
real_action = where(mask, real_action, normalized_action)
return real_action
伪代码 8:推理流程
python
def predict_action(model, image, instruction, statistics_key):
# model:
# 已训练好的 OpenVLA 风格模型。
#
# image:
# 当前相机图像。
#
# instruction:
# 当前任务语言。
#
# statistics_key:
# 指定当前应使用哪套动作统计量。
# 多机器人或多数据集模型必须知道这一点。
# 1. 构造 prompt。
# 使用和训练时一致的问答格式,避免训练/推理分布不一致。
prompt = "In: What action should the robot take to " + instruction.lower() + "?\nOut:"
# 2. 编码文本。
# 得到 prompt token ids 和 attention mask。
text_inputs = tokenize(prompt)
# 3. 预处理图片。
# 得到视觉编码器需要的 pixel_values。
pixel_values = preprocess_image(image)
# 4. 根据当前动作空间维度决定生成多少个 token。
# 7 维动作生成 7 个 token,10 维动作生成 10 个 token。
action_dim = model.get_action_dim(statistics_key)
# 5. 自回归生成动作 token。
# 这里通常使用 deterministic decoding,例如 do_sample=False。
generated_token_ids = model.generate(
input_ids=text_inputs.input_ids,
attention_mask=text_inputs.attention_mask,
pixel_values=pixel_values,
max_new_tokens=action_dim,
)
# 6. 取最后 action_dim 个 token。
# 这些就是模型为每个动作维度生成的动作 token。
action_token_ids = generated_token_ids[-action_dim:]
# 7. 将动作 token 解码为归一化动作。
normalized_action = decode_tokens_to_normalized_action(
action_token_ids,
tokenizer=model.tokenizer,
)
# 8. 读取当前机器人或数据集的统计量。
statistics = model.action_statistics[statistics_key]
# 9. 将归一化动作还原为真实机器人动作。
real_action = unnormalize_action(
normalized_action,
statistics=statistics,
mask=statistics.action_mask,
)
# 10. 返回动作给机器人控制器。
return real_action
伪代码 9:多数据集混合训练数据流
python
def build_mixed_robot_dataset(datasets, sampling_weights):
# datasets:
# 多个机器人数据集。
#
# sampling_weights:
# 每个数据集被采样的相对概率。
standardized_datasets = []
for dataset in datasets:
# 1. 字段标准化。
# 将不同数据集的图片、语言、动作字段映射到统一协议。
dataset = standardize_fields(dataset)
# 2. 动作语义标准化。
# 确保动作维度含义一致,例如前几维是位移、后几维是旋转和夹爪。
dataset = standardize_action_semantics(dataset)
# 3. 计算动作统计量。
# 这些统计量用于训练时归一化,以及推理时反归一化。
dataset.statistics = compute_action_statistics(dataset)
# 4. 过滤无效样本。
# 例如没有语言指令、动作异常、图片缺失的样本。
dataset = filter_invalid_examples(dataset)
# 5. 将轨迹展开成训练帧。
# 如果模型预测单步动作,每个时间步都可以变成一条训练样本。
dataset = flatten_trajectories_to_frames(dataset)
standardized_datasets.append(dataset)
# 6. 按权重混合采样。
# 训练时模型会以指定比例看到不同机器人和任务的数据。
mixed_dataset = weighted_sample(
standardized_datasets,
weights=sampling_weights,
)
# 7. 打乱样本。
# 避免训练连续看到同一轨迹或同一数据集的局部相关样本。
mixed_dataset = shuffle(mixed_dataset)
return mixed_dataset
关键设计取舍
为什么不直接回归动作
直接回归动作当然可以,但 OpenVLA 选择 token 化动作有几个好处:
- 复用语言模型原生输出头。
- 复用自回归生成机制。
- 统一文本生成和动作生成形式。
- 让动作预测变成分类问题,训练稳定性往往更好。
代价是:
- 连续动作会有量化误差。
- 推理需要 token 解码和反归一化。
- 动作维度越多,需要生成的 token 越多。
为什么图片要作为 embedding 插入,而不是生成图像描述
图像描述会丢失细节。
机器人控制需要非常具体的空间信息:
text
杯子离夹爪多远
物体在图像哪个区域
夹爪是否已经对齐
目标物是否被遮挡
这些信息很难通过一句 caption 完整表达。
直接插入视觉 embedding 可以保留更多空间和视觉细节。
为什么只监督动作 token
如果 prompt 也参与 loss,模型会花能力学习复述固定模板。
而机器人控制真正需要的是:
text
根据图像和任务预测动作
所以 prompt 和图像 patch 位置都应该被忽略,loss 集中到动作 token。
为什么需要数据集统计量
模型内部只能统一学习标准动作空间。
但真实机器人动作尺度因机器人和数据集而异。
数据集统计量承担了单位换算功能:
text
标准动作单位 -> 当前机器人真实动作单位
没有这一步,模型输出的动作 token 即使语义正确,也可能无法驱动真实机器人。
常见误解
误解一:OpenVLA 是普通动作回归模型
不是。
它的输出首先是 token。连续动作是 token 解码和反归一化之后得到的。
误解二:动作 token 是自然语言词语
不是。
动作 token 通常不可读。它们只是借用了语言模型词表中的 token id 来表示动作桶。
误解三:图片先被描述成文字再给语言模型
不是。
图片通过视觉编码器变成 embedding,然后直接插入语言模型上下文。
误解四:只要 token accuracy 高,机器人效果就一定好
不一定。
机器人效果还取决于:
text
动作反归一化是否正确
图像预处理是否和训练一致
控制频率是否匹配
机器人硬件误差
任务是否需要长期闭环纠错
误解五:多数据集训练只需要把数据拼起来
不够。
必须统一:
text
字段格式
动作语义
动作尺度
采样权重
统计量保存
否则模型学到的是混乱的动作语言。
如果要改造系统应优先理解哪些抽象接口
新增机器人数据集
你需要定义:
text
原始图像字段 -> 标准图像字段
原始语言字段 -> 标准语言字段
原始动作字段 -> 标准动作向量
动作维度语义
哪些维度需要归一化
数据集统计量如何计算
改动作表示
你需要同时考虑:
text
动作归一化方式
离散桶数量
桶到 token 的映射
token 到连续动作的解码
反归一化公式
训练指标如何计算
推理生成 token 数量
这些必须保持一致。
只改编码不改解码,系统会失效。
改视觉编码器
你需要保证:
text
图片预处理匹配视觉编码器
视觉输出是 patch 级特征
视觉特征维度已知
投影器能映射到语言模型 hidden size
图像 patch 数量和 attention mask / label mask 对齐
改语言模型
你需要注意:
text
tokenizer 词表大小
特殊 token 规则
prompt 格式
上下文长度
动作 token 是否仍能映射到词表尾部
generate 行为是否和训练格式一致
语言模型变了,动作 token 的含义也可能要重新校准。
支持动作 chunk
如果想一次生成多个未来动作,而不是单步动作,需要重新定义:
text
动作序列如何展开成 token
max_new_tokens 应该是多少
解码后如何 reshape 成 [chunk_len, action_dim]
训练 labels 如何屏蔽
机器人控制器如何执行动作 chunk
最终总结
OpenVLA 的核心不是某个具体工程实现,而是一套建模思想:
text
1. 把不同机器人动作统一到标准动作空间
2. 把连续动作离散成动作桶
3. 把动作桶映射成语言模型 token
4. 把图片变成视觉 embedding
5. 把视觉 embedding 插入语言模型上下文
6. 让语言模型在图像和指令条件下生成动作 token
7. 把动作 token 解码成归一化动作
8. 用数据集统计量反归一化成真实机器人动作
用最短的话说:
OpenVLA 把机器人控制变成了多模态 token 生成。
它的强大之处在于,机器人动作不再被看作一个孤立的回归头输出,而是被纳入了语言模型统一的生成范式。
它的关键工程闭环是:
text
机器人数据
-> 标准化
-> 动作归一化
-> 动作 token 化
-> 图像 embedding 注入
-> 语言模型生成
-> token 解码
-> 动作反归一化
-> 机器人执行