RynnVLA-002 方法原理详解
VLA action model:输入文本指令、当前或历史图像、可选机器人状态,输出未来一段机器人动作。
World model:输入历史图像和动作,输出下一帧或下一组图像,用来预测动作执行后的视觉结果。
RynnVLA-002 的关键点不是简单地把两个模型放在一起,而是把“动作生成”和“图像生成”都写成多模态对话序列预测问题,让同一个 Transformer 学习机器人交互中的条件生成规律。同时,它在离散动作 token 之外增加了连续动作头,用连续 L1 回归提升动作精度。
- 任务定义
2.1 VLA 动作生成
给定:
语言任务指令,例如 “put the bowl on top of the cabinet”。
当前或若干历史第三视角图像。
当前或若干历史腕部相机图像。
可选机器人本体状态,通常包含末端执行器位置、姿态和夹爪状态。
输出:
一个动作 chunk,即未来 T 步动作。
每步动作通常是 7 维:三维平移、三维旋转、夹爪控制。
形式化表示:
a_{t:t+T-1} = pi(instruction, state_t, image_history, wrist_image_history)
其中 T 在 LIBERO 配置里常见为 5,在 LeRobot 实验里可以是 20。
2.2 World model 图像生成
给定:
历史第三视角图像。
历史腕部图像。
与历史转移对应的动作。
输出:
下一时刻第三视角图像。
下一时刻腕部图像。
形式化表示:
(I_{t+1}^{front}, I_{t+1}^{wrist}) =
W(I_{t}^{front}, I_{t}^{wrist}, a_t)
在实现中,这个图像生成不是像扩散模型那样逐步去噪,而是像语言模型一样逐 token 生成图像 token,再通过 Chameleon 的图像 tokenizer/decoder 还原为图片。
- 统一序列表示
RynnVLA-002 的核心设计是把不同模态都变成 token,并放入同一个自回归序列。
3.1 文本 token
任务指令和 prompt 使用文本 tokenizer 编码。例如动作任务的自然语言提示大致是:
What action should the robot take to {task_description}?
world model 的提示大致是:
Generate the next image based on the provided sequence of historical images and corresponding actions.
3.2 图像 token
图像使用 Chameleon VQGAN tokenizer 编码。流程是:
输入图像被 resize/crop 到目标分辨率相关的网格尺寸。
图像进入 VQGAN tokenizer,变成离散图像 token。
图像 token 被组织成二维 latent 网格。
每行末尾插入换行类 reserved token。
图像块外面包上图像开始 token、网格高宽 token、图像结束 token。
概念上,一个图像块长这样:
<image_start> <h_grid> <w_grid>
image_token_1 image_token_2 …
image_token_… …
<image_end>
这使得图像能够像文本一样出现在自回归上下文中,也能够作为模型要生成的目标。
3.3 动作 token
动作有两种表示方式。
第一种是离散动作 token。连续动作先按每个维度的统计范围归一化到 [-1, 1],再映射到 256 个均匀 bin,最后偏移到 reserved token 区间。动作块结构是:
<action_start> action_dim_1_token … action_dim_7_token <action_end>
对于一个动作 chunk,目标序列里会重复多个 <|action|> 占位符;每个占位符在 tokenization 时被展开成一个完整动作块。
第二种是连续动作回归。模型仍然先生成或定位 <action_start> 这样的动作起始语义位置,但真正输出动作时,不只依赖离散 token,而是使用 Transformer 隐藏状态接一个连续动作头,直接回归 T x action_dim 的连续动作。
3.4 状态 token
状态也被归一化到 [-1, 1],再离散化到 reserved token 区间。状态块结构类似动作:
<state_start> state_dim_1_token … state_dim_N_token <state_end>
LIBERO 里的状态通常由末端执行器状态和夹爪状态拼接得到。源码里对应的状态维度是 8 维。
- 把机器人数据改写成“多模态对话”
训练数据不是直接喂 (obs, action) 元组,而是被改写成对话格式。这样做有两个好处:
可以复用大语言模型/多模态语言模型的 instruction-following 形式。
动作、图像、状态都通过占位符插入对话,最终统一展开成 token 序列。
4.1 VLA 对话样本
动作生成样本形如:
{
“conversations”: [
{
“from”: “human”,
“value”: “What action should the robot take to open the drawer?<|state|><|image|><|image|><|image|><|image|>”
},
{
“from”: “gpt”,
“value”: “<|action|><|action|><|action|><|action|><|action|>”
}
],
“image”: [
“front_t-1.png”,
“wrist_t-1.png”,
“front_t.png”,
“wrist_t.png”
],
“state”: [“state_t.npy”],
“action”: [
“action_t.npy”,
“action_t+1.npy”,
“action_t+2.npy”,
“action_t+3.npy”,
“action_t+4.npy”
]
}
这里的 <|image|>、<|state|>、<|action|> 只是占位符。tokenization 时,processor 会依次取 image、state、action 字段里的真实数据,把它们展开成对应 token 块。
4.2 World model 对话样本
世界模型样本形如:
{
“conversations”: [
{
“from”: “human”,
“value”: “Generate the next image based on the provided sequence of historical images and corresponding actions.<|image|><|image|><|action|>”
},
{
“from”: “gpt”,
“value”: “<|image|><|image|>”
}
],
“image”: [
“front_t.png”,
“wrist_t.png”,
“front_t+1.png”,
“wrist_t+1.png”
],
“action”: [“action_t.npy”]
}
这个样本让模型学习:看到当前 front/wrist 图像和动作后,生成下一时刻 front/wrist 图像。
4.3 VLA 样本和 WM 样本如何一起训练
VLA 样本和 world model 样本不是在同一个轨迹时刻里强制成对做两次 forward,也不是每个 step 同时优化一个 VLA batch 和一个 WM batch。实际训练方式是把两类样本合并到同一个训练数据集中,再由 dataloader/sampler 像普通监督数据一样取 batch。
也就是说,训练集概念上是:
train_dataset = action_samples + world_model_samples
每个 batch 里可能包含 VLA/action 样本,也可能包含 world model 样本,具体取决于合并后的样本顺序、shuffle 和 sampler。源码确认:pretokenize 流程会把 action 数据 record 和 world model 数据 record concat 成一个统一 record;no-pretokenize 流程里,dataset 构造时也会分别 append action sample 和 world sample。
这意味着:
抽到 VLA/action 样本时,目标主要是动作 token,连续动作头也有有效的 L1 监督。
抽到 world model 样本时,目标主要是未来图像 token,通常没有连续动作头监督。
两类任务的训练比例主要由合并进去的样本数量决定;当前实现里没有看到额外的任务均衡采样器。
4.4 训练标签 mask
训练时,prompt 部分只作为条件,不计算 loss;assistant/gpt 部分作为目标,计算 loss。也就是说:
human prompt tokens -> label = -100
gpt target image/action -> label = token_id
padding -> label = -100
这和语言模型监督微调一致,只是目标 token 可能是文本、图像 token、动作 token 或状态 token。
- 模型结构
RynnVLA-002 的主干是 Chameleon 条件生成模型,外面加了机器人任务相关的配置和动作头。
5.1 多模态自回归 Transformer
主干 Transformer 接收统一 token 序列:
x = [text tokens, state tokens, image tokens, action tokens, …]
训练时,它做标准 causal language modeling:
P(x) = product_i P(x_i | x_<i)
因此,只要把目标动作或目标图像放在序列后半段,模型就会学习在前文条件下生成它们。
5.2 动作离散生成分支
离散动作分支完全复用语言模型的 token logits:
模型预测下一个 token 分布。
在动作位置上,目标 token 是动作 bin 对应的 reserved token。
推理时生成动作 token 序列。
将动作 token 反量化回 [-1, 1] 连续动作。
再按数据集 min/max 反归一化回机器人环境动作空间。
这种方式简单统一,但每个动作维度只能落在离散 bin 中,精度受量化限制。
5.3 连续动作头
连续动作头解决动作量化精度问题。它的思路是:让大 Transformer 负责理解语言、图像、状态上下文,再用一个专门的小网络从 hidden state 回归连续动作。
连续动作头内部流程:
找到序列中第一个动作起始 token <action_start> 的位置。
取该位置之前的所有 Transformer hidden states 作为上下文。
拼接一组可学习的 action query token。数量为 time_horizon * action_dim。
将上下文和 action query 一起送入一个轻量 Transformer Encoder。
取 action query 对应的输出。
用 MLP-ResNet 投影为连续动作。
reshape 成 [time_horizon, action_dim]。
直观地说,连续动作头不是让语言模型一个 token 一个 token 猜动作数值,而是在“我要开始输出动作”的位置,对上下文做一次条件读取,然后并行解码未来动作 chunk。
- 训练目标
训练总 loss 由几部分组成。
6.1 token 交叉熵
主 loss 是标准 token-level cross entropy:
L_ce = CE(logits, labels)
它会监督:
world model 的图像 token 生成。
离散动作 token 生成。
其他 assistant 目标 token。
6.2 连续动作 L1 loss
当启用连续动作头时,模型从 labels 中找到动作 token 块,把离散动作 token 反解码成归一化连续动作,作为连续动作头的监督目标。
L_ct = mean_absolute_error(predicted_action, decoded_label_action)
源码确认:训练中默认总 loss 是:
L = L_ce + loss_ct_weights * L_ct + optional_z_loss
其中 loss_ct_weights 默认是 10。
6.3 图像 token loss 权重
在 action-world-model 联合训练的某些路径里,图像 token 的交叉熵权重会被调小。源码中对 token id [3, 8195) 这段图像 token 区间设置 loss_img_weights,默认值是 0.04。
这背后的动机是:图像 token 数量远多于动作 token。如果不降权,训练目标很容易被图像重建主导,动作能力被稀释。
6.4 可选 z-loss
如果配置 z_loss_weight > 0,会对 logits 的 log-sum-exp 加一个正则项:
L_z = mean(logsumexp(logits)^2)
它用于稳定大词表 logits,但默认可以为 0。
- 注意力机制细节
模型使用 causal attention,也就是每个位置只能看自己之前的 token。
源码里还针对图像块和动作块构造了额外 attention mask。核心意图是:在包含多个动作块和图像块的序列中,控制某些动作块之间的可见性,避免当前要预测的动作块不恰当地看到后续或不该参考的动作信息。
可以把它理解为:统一序列虽然把所有模态拼在一起,但训练时仍需要保证“条件”和“目标”的时间方向正确。
7.1 动作 chunk 内不同动作块的 mask
动作 chunk 在离散 token 序列里通常表现为多个连续的 action block:
condition_tokens
<action_start> action_t_dim1 … action_t_dim7 <action_end>
<action_start> action_t+1_dim1 … action_t+1_dim7 <action_end>
<action_start> action_t+2_dim1 … action_t+2_dim7 <action_end>
…
如果只使用标准 causal mask,那么后面的动作块会看到前面的动作块。例如 action_t+1 的 token 可以 attend 到 action_t,action_t+2 又可以 attend 到 action_t 和 action_t+1。这会让 chunk 内动作形成自回归依赖:
action_t -> action_t+1 -> action_t+2
当前实现额外做了一步:对位于最后一个 image block 之后的 action block,把“当前动作块看之前动作块”的 attention 置 0。伪代码可以理解为:
初始 mask 是标准 causal mask:后面的 token 默认可以看前面的 token。
mask = causal_lower_triangular_mask(seq_len)
找到序列中的所有图像块和动作块。
image_blocks = find_blocks(input_ids, IMAGE_START_ID, IMAGE_END_ID)
action_blocks = find_blocks(input_ids, ACTION_START_ID, ACTION_END_ID)
只处理位于最后一个图像块之后的动作目标块。
last_image_end = image_blocks[-1].end
for current_action in action_blocks:
if current_action.start > last_image_end:
previous_actions = [
block for block in action_blocks
if block.end < current_action.start
]
for prev_action in previous_actions:
# 禁止当前动作块 attend 到更早的动作块。
# 这样 action_t+1 不再直接依赖 action_t 的 token,
# action_t+2 也不再直接依赖 action_t/action_t+1 的 token。
mask[current_action.start : current_action.end + 1,
prev_action.start : prev_action.end + 1] = 0
因此,离散动作 chunk 的训练更接近:
action_t = f(condition_tokens)
action_t+1 = f(condition_tokens)
action_t+2 = f(condition_tokens)
…
而不是严格的:
action_t+1 = f(condition_tokens, action_t)
action_t+2 = f(condition_tokens, action_t, action_t+1)
这里有一个细节:这个 mask 切断的是“不同 action block 之间”的依赖,不是切断同一个 action block 内部的维度依赖。一个动作块内部仍然是 causal 的:
<action_start> dim1 -> dim2 -> … -> dim7 -> <action_end>
所以离散动作 token 分支可以概括为:chunk 内不同未来动作块主要共同依赖观测/语言/状态上下文;但每个动作块内部的 7 个维度仍按 token 顺序生成。
连续动作头更进一步:它不按动作 token 逐步生成 chunk,而是在动作开始位置前取上下文 hidden states,拼接 time_horizon * action_dim 个 action query,一次性回归整个动作 chunk。因此连续动作头本身就是并行 chunk 预测:
[action_t, action_t+1, …, action_t+T-1] = ActionHead(condition_hidden_states)
8. 训练流程伪代码
8.1 数据构造
遍历 LIBERO 里的任务集合。每个 task 通常对应一个自然语言目标和若干演示轨迹。
for task in LIBERO_tasks:
trajectories = load_trajectories(task)
# 一条轨迹包含按时间排列的图像、状态和动作。
for traj in trajectories:
images_front = traj.front_images
images_wrist = traj.wrist_images
states = traj.robot_states
actions = traj.actions
# 对轨迹中的每个时间步 t 都尝试构造训练样本。
for t in range(len(actions)):
# 构造 VLA/action 样本:给定当前/历史观测,预测未来一段动作 chunk。
# 只有当 t 后面还剩足够 time_horizon 步动作时,样本才有效。
if t + time_horizon <= len(actions):
# 收集历史图像。若使用双视角,则每个时间步会放 front 和 wrist 两张图。
obs_images = collect_history(
images_front, images_wrist, end=t, history=his
)
# 取当前机器人本体状态,例如末端执行器位姿和夹爪状态。
state_t = states[t]
# 监督目标是从 t 开始的未来 time_horizon 步动作。
future_actions = actions[t : t + time_horizon]
add_sample({
# human 侧是条件:语言指令 + 状态占位符 + 图像占位符。
# 这些占位符之后会被展开成真正的 state/image token。
"human": instruction(task) + "<|state|>" + "<|image|>" * len(obs_images),
# gpt 侧是目标:每个 <|action|> 会展开成一个完整动作 token 块。
"gpt": "<|action|>" * time_horizon,
# image/state/action 字段按占位符出现顺序提供真实数据。
"image": obs_images,
"state": [state_t],
"action": future_actions,
})
# 构造 world model 样本:给定当前图像和动作,预测下一帧图像。
# 只有当 t+1 的未来图像存在时,样本才有效。
if t + 1 < len(images_front):
# 条件图像:当前 front 和 wrist。
hist_images = [images_front[t], images_wrist[t]]
# 目标图像:下一时刻 front 和 wrist。
next_images = [images_front[t + 1], images_wrist[t + 1]]
# 条件动作:从当前状态执行的动作。
action_t = actions[t]
add_sample({
# human 侧描述 world model 任务,并给出当前双视角图像和动作。
"human": world_prompt + "<|image|><|image|><|action|>",
# gpt 侧要求模型生成下一时刻双视角图像。
"gpt": "<|image|><|image|>",
"image": hist_images + next_images,
"action": [action_t],
})
训练时不是分别跑两个 dataloader,而是把 action 样本和 world model 样本合并。
shuffle/sampler 会决定每个 batch 抽到哪类样本,比例主要由两类样本数量决定。
train_dataset = shuffle(action_samples + world_model_samples)
8.2 多模态 tokenization
def tokenize_conversation(sample, training=True):
# output_tokens 是模型实际输入的完整 token 序列。
output_tokens = []
# labels 与 output_tokens 等长;label=-100 的位置不参与 CE loss。
labels = []
# 对话模板会把 human/gpt 两侧展开成一段段文本或模态占位符。
# human 通常是条件,gpt 通常是训练目标。
for segment in render_conversation_template(sample["conversations"]):
if segment.is_text:
# 普通文本直接用文本 tokenizer 编码。
ids = text_tokenizer(segment.text)
output_tokens.extend(ids)
# 如果这段文本属于目标侧,就监督预测;如果属于 prompt 条件侧,就 mask 掉。
labels.extend(ids if segment.is_target else [-100] * len(ids))
elif segment.placeholder == "<|image|>":
# 每遇到一个 <|image|>,就按顺序取一个真实图像。
image = pop_next(sample["image"])
# 图像通过 Chameleon VQGAN tokenizer 变成离散 image token 块。
ids = encode_image_as_chameleon_tokens(image)
output_tokens.extend(ids)
# 条件图像只作为输入;目标图像才计算 CE。
labels.extend(ids if segment.is_target else [-100] * len(ids))
elif segment.placeholder == "<|action|>":
# 每遇到一个 <|action|>,就按顺序取一条真实动作。
action = pop_next(sample["action"])
# 动作先归一化到 [-1, 1],再量化成 reserved token。
ids = encode_action_as_reserved_tokens(action)
output_tokens.extend(ids)
# VLA 样本的目标动作 token 会参与 CE;历史动作条件会被 mask。
labels.extend(ids if segment.is_target else [-100] * len(ids))
elif segment.placeholder == "<|state|>":
# 状态通常是当前机器人 proprioception,例如 ee pose + gripper。
state = pop_next(sample["state"])
# 状态和动作类似,也会被归一化并离散化到 reserved token。
ids = encode_state_as_reserved_tokens(state)
output_tokens.extend(ids)
# 当前实现里 state 通常作为条件输入,因此大多会被 mask。
labels.extend(ids if segment.is_target else [-100] * len(ids))
# 返回统一序列和监督标签;后续训练只需要这两个对象即可计算 CE。
return output_tokens, labels
8.3 主训练循环
for batch in dataloader:
# batch 可能包含 VLA/action 样本,也可能包含 world model 样本。
# collate 会把不同长度的 token 序列 pad 到同一长度。
token_batch, label_batch = collate_and_pad(batch)
# 主模型做一次完整 forward:
# - logits 用于 token 级 CE,包括图像 token 和离散动作 token。
# - hidden_states 会被连续动作头读取。
outputs = model(
input_ids=token_batch,
labels=label_batch,
output_hidden_states=True,
training=True,
)
# c_loss:自回归 token 预测损失。
# 对 VLA 样本,它监督动作 token;对 WM 样本,它监督未来图像 token。
ce_loss = outputs.cross_entropy_loss
# 最后一层 hidden states 作为连续动作头的输入上下文。
hidden_states = outputs.last_hidden_states
# 连续动作头会找到 ACTION_START_ID 之前的上下文,并回归动作 chunk。
predicted_actions = action_head(
hidden_states=hidden_states,
input_ids=token_batch,
target_token_id=ACTION_START_ID,
)
# 从 label 里找到真实动作 token 块。
# 注意:当前实现用 label 里的离散动作 token 反解码连续监督目标。
label_action_tokens = find_action_blocks(label_batch)
target_actions = decode_action_tokens(label_action_tokens)
# 连续动作头的监督是 L1 回归损失。
# 如果 batch 中没有有效动作目标,该项应为 0 或被跳过。
continuous_loss = l1(predicted_actions, target_actions)
# 总损失 = token CE + 连续动作 L1 的加权和。
# 源码默认 loss_ct_weight 为 10。
total_loss = ce_loss + loss_ct_weight * continuous_loss
# 可选 logits 稳定正则。
if z_loss_enabled:
total_loss += z_loss_weight * z_loss(outputs.logits)
# 反向传播会同时更新主 Transformer 可训练参数和连续动作头参数。
total_loss.backward()
optimizer.step()
8.4 连续动作头伪代码
def continuous_action_head(hidden_states, input_ids):
# hidden_states: [B, L, H]
# B 是 batch size,L 是序列长度,H 是主模型 hidden size。
# input_ids: [B, L]
# 与 hidden_states 对齐,用来定位动作开始 token。
contexts = []
for b in range(B):
# 找到当前样本里第一个动作开始 token。
# 这个位置表示“从这里开始要输出动作”。
action_start = first_position(input_ids[b] == ACTION_START_ID)
# 取动作开始 token 之前的所有 hidden states 作为条件上下文。
# 上下文中已经融合了语言、状态、图像等信息。
contexts.append(hidden_states[b, :action_start])
# 创建一组可学习 action query。
# 数量等于 time_horizon * action_dim,例如 5 步 * 7 维 = 35 个 query。
action_queries = learned_embedding(
shape=[time_horizon * action_dim, hidden_size]
)
sequences = []
for context in contexts:
# 把上下文和 action query 拼在一起。
# query 会在后面的 Transformer Encoder 中通过 attention 读取上下文。
sequences.append(concat(context, action_queries))
# 不同样本的上下文长度可能不同,需要 pad 成同一长度。
# mask 用来告诉 Transformer 哪些位置是 padding。
padded_sequences, mask = pad_to_same_length(sequences)
# 先把主模型 hidden size 投影到动作头内部较小的维度。
z = linear_projection(padded_sequences)
# 用轻量 Transformer Encoder 做条件读取。
# 这里不是 causal LM,而是让 action query 充分 attend 到上下文。
z = transformer_encoder(z, padding_mask=mask)
# 取拼接序列末尾的 action query 输出。
# 这些输出就是每个未来动作维度的条件表示。
query_outputs = take_last_query_outputs(
z, num_queries=time_horizon * action_dim
)
# MLP-ResNet 把 query 表示映射成连续动作数值。
actions = mlp_resnet(query_outputs)
# 整理成 [batch, 未来步数, 每步动作维度]。
actions = reshape(actions, [B, time_horizon, action_dim])
return actions
- 推理流程
9.1 连续动作推理
在 LIBERO 连续动作评估中,推理闭环大致如下:
环境 reset,并执行若干步 dummy action,让模拟器稳定。
读取当前第三视角图像、腕部图像和机器人状态。
如果动作缓存为空,就构造 VLA prompt。
prompt 经过 item processor 展开成 token。
模型先生成一个动作起始 token。
连续动作头根据 hidden states 输出一个动作 chunk。
将归一化动作反归一化回环境动作空间。
逐步执行 chunk 中的动作。
更新历史图像、腕部图像和历史动作。
重复直到成功或达到最大步数。
伪代码:
动作缓存用于保存一次模型调用生成的 action chunk。
环境每 step 消耗一个动作,缓存空了才重新查询大模型。
actions_cache = []
历史信息会作为下一次决策的上下文。
history_front = []
history_wrist = []
history_actions = []
while not done and t < max_steps:
# 从环境中读取当前观测。
obs = env.current_observation()
# 提取并预处理第三视角图像、腕部图像和机器人状态。
front = preprocess_front_image(obs)
wrist = preprocess_wrist_image(obs)
state = get_robot_state(obs)
# 只有缓存为空时才调用模型,避免每个仿真 step 都跑一次大模型。
if len(actions_cache) == 0:
# 构造 VLA 推理 prompt。
# 其中包含任务文本、当前状态、当前双视角图像,以及可选历史图像/动作。
sample = build_vla_prompt(
task_description=task,
state=state,
current_front=front,
current_wrist=wrist,
history_front=history_front,
history_wrist=history_wrist,
history_actions=history_actions,
)
# 把 prompt 展开成模型输入 token;推理时不需要 labels。
input_ids = tokenize_conversation(sample, training=False)
# 连续动作头输出一个动作 chunk,形状近似 [time_horizon, action_dim]。
action_chunk = model.generate_action_head(input_ids)
# 将 chunk 放入缓存,后续环境 step 逐个取出执行。
actions_cache.extend(action_chunk)
# 取出当前要执行的一步归一化动作。
normalized_action = actions_cache.pop(0)
# 将 [-1, 1] 归一化动作映射回 LIBERO 环境需要的真实动作范围。
env_action = unnormalize_action(normalized_action)
# 在环境中执行动作。
obs, reward, done, info = env.step(env_action)
# 更新历史上下文,供下一次模型查询使用。
history_front.append(front)
history_wrist.append(wrist)
history_actions.append(normalized_action)
9.2 离散动作推理
离散动作推理不走连续动作头,而是直接让语言模型生成动作 token:
离散动作推理只依赖 LM head 生成 token,不调用连续动作头。
input_ids = tokenize_vla_prompt(…)
生成若干 reserved action token;每个动作块包含 start、各维动作 token、end。
generated_tokens = model.generate(input_ids, max_new_tokens=action_steps * tokens_per_action)
从生成结果中解析出一个或多个动作块。
action_blocks = parse_action_blocks(generated_tokens)
将动作 token 反量化成 [-1, 1] 的连续动作。
actions = decode_action_tokens(action_blocks)
再映射回环境动作空间。
env_actions = unnormalize_actions(actions)
优点是与自回归训练目标完全一致;缺点是动作被 256-bin 量化。
9.3 World model 推理
world model 推理是图像 token 生成:
构造 world model 推理样本:
条件是当前 front/wrist 图像和动作,目标是让模型继续生成图像 token。
sample = {
“human”: world_prompt + “<|image|><|image|><|action|>”,
“image”: [front_t, wrist_t],
“action”: [action_t],
}
推理时只需要输入 token,不需要 labels。
input_ids = tokenize_conversation(sample, training=False)
自回归生成未来图像 token。
generated_image_tokens = model.generate_img(input_ids)
将 Chameleon 图像 token 解码回 RGB 图像。
front_next, wrist_next = decode_chameleon_images(generated_image_tokens)
生成图像可以用于离线评估,也可以作为 action-world-model 的一部分,帮助模型学习动作造成的视觉变化。
- 为什么这种统一方法有效
10.1 动作和世界变化共享机器人语义
动作模型学的是“看到状态后该怎么动”,world model 学的是“这样动之后世界会怎么变”。这两件事本质上依赖同一套视觉、语言和物理语义。统一模型让二者共享表示,而不是各自训练孤立网络。
10.2 图像 token 让未来视觉预测变成语言模型式生成
Chameleon 的图像 tokenizer 把图像离散化后,图像预测就可以用 next-token prediction 处理。这让 world model 不需要单独设计 diffusion decoder 或 video model,而是和文本、动作共用同一个自回归建模范式。
10.3 动作 token 让机器人控制进入语言模型词表
动作离散化后,机器人控制变成了 token 预测问题。模型可以像生成文本一样生成动作序列,从而把 VLA 任务纳入同一个因果序列建模框架。
10.4 连续动作头弥补离散化精度
纯 token 动作会受到 bin 数量限制。连续动作头利用大模型上下文 hidden state 直接回归动作,保留统一表示的优点,同时改善控制精度。
10.5 action chunk 提高闭环控制效率
一次预测多个未来动作,可以减少每一步都调用大模型的开销,并让模型学习短时动作连贯性。执行时通过缓存逐步消费 chunk,再根据新观测重新规划。
- 重要超参数和约定
项目 含义
his 使用多少历史帧作为观测上下文
time_horizon / len_action 一次预测多少未来动作
action_dim 每步动作维度,LIBERO 中通常为 7
resolution 图像训练/推理分辨率,VLA 常见 256,world model 常见 512
loss_ct_weights 连续动作 L1 loss 权重,默认 10
loss_img_weights 图像 token loss 降权,默认 0.04
ACTION_START_ID 动作开始 token id,源码中使用 10004
ACTION_END_ID 动作结束 token id,源码中使用 15004 - 方法边界和容易误解的地方
RynnVLA-002 不是只训练一个 action policy。它同时包含 action model 和 world model 的统一训练思路。
world model 生成的是图像 token,再解码成图片,不是直接回归 RGB。
离散动作和连续动作头是两套动作输出机制,可以分别用于评估;连续动作头不是简单地读取最后一个 token logits。
<|image|>、<|action|>、<|state|> 是数据构造里的占位符,不是最终模型直接看到的单个普通 token;它们会展开成完整模态 token 块。
prompt 部分不计算监督 loss,只有 assistant/gpt 目标部分计算 loss。
图像 token 数量很多,联合训练时需要权重平衡,否则动作学习容易被图像生成目标压过。
推理时 action chunk 会被缓存并逐步执行,因此模型查询频率低于环境 step 频率。
动作 chunk 不是简单的逐动作自回归链。离散动作 token 分支通过 mask 切断不同 action block 之间的直接 attention;连续动作头则一次性并行回归整个 chunk。不过,离散动作块内部的动作维度仍保留 causal 顺序。 - 最小可复现的概念版本
如果要从零复现这个方法的核心,不必先复刻全部工程细节,可以按下面的最小版本理解:
1. 准备机器人轨迹
一条轨迹是按时间排列的多模态机器人交互数据。
trajectory = {
“instruction”: text,
“front_images”: […],
“wrist_images”: […],
“states”: […],
“actions”: […],
}
2. 构造两种样本,并合并成混合多任务训练集
VLA 样本学“看到观测后该怎么动”。
vla_samples = make_action_prediction_samples(trajectory)
World model 样本学“执行动作后视觉世界怎么变化”。
world_samples = make_next_image_prediction_samples(trajectory)
两类样本合并训练;不是每一步固定同时训练两类 batch。
dataset = shuffle(vla_samples + world_samples)
3. 把所有模态变成 token
for sample in dataset:
# input_ids 是统一多模态序列;labels 标记哪些位置需要计算 CE。
input_ids, labels = multimodal_tokenize(sample)
4. 训练统一自回归模型
logits 用于离散 token 预测;hidden_states 用于连续动作头。
logits, hidden_states = transformer(input_ids)
CE 同时覆盖离散动作 token 和未来图像 token。
loss_ce = cross_entropy(logits, labels)
5. 额外训练连续动作头
找到动作开始 token 前的上下文 hidden states。
action_context = hidden_states_before_action_start(hidden_states, input_ids)
连续动作头根据上下文回归未来动作 chunk。
pred_action = continuous_action_head(action_context)
当前实现从 label 中的离散动作 token 反解码连续监督目标。
如果额外保存原始归一化 GT action,也可以直接用原始连续 GT 监督。
target_action = decode_action_labels(labels)
连续动作头使用 L1 损失。
loss_ct = l1(pred_action, target_action)
联合优化:主 token CE + 连续动作 L1。
loss = loss_ce + lambda_ct * loss_ct
loss.backward()
6. 部署时闭环控制
while not task_done:
# 读取当前机器人观测。
obs = get_robot_observation()
# 根据当前观测和历史上下文构造 VLA prompt。
prompt = build_action_prompt(obs, instruction, history)
# 可以选择连续动作头或离散动作 token 推理。
action_chunk = model.generate_actions(prompt)
# 执行动作 chunk;缓存为空或需要重新规划时再查询模型。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)