☰
重建式VLA源码解析:架构、训练与推理全拆解
2026/10/19 13:47:27 网站建设 项目流程

这几年机器人学习领域最绕不开的一个词,大概就是 VLA。Vision-Language-Action,把视觉、语言、动作三个模态揉进同一个模型里,让机器人听一句人话就能在真实环境里干活。但源码读多了你会发现,真正拉开差距的往往不是“多模态 prompt 怎么拼”,而是训练目标怎么定。有一类工作做得特别激进:除了预测动作,还把整个场景“重建”出来,这类模型也被叫成重建式 VLA。

能拿下顶会最佳论文,说明这套源码不只在刷榜,工程完成度通常也很高。我前后把它完整读过两遍,也照着思路自己复现过一版,中间的坑不少。这篇我打算从源码角度把重建式 VLA 的架构选型、数据管线、重建头实现、训练细节和推理过程全部拆开讲,顺着代码逻辑走一遍。适合两类人看:一类是已经在跑策略学习、想搞清楚 VLA 内部到底发生了什么的人;另一类是准备自己训练机器人基座模型、但担心上来就被各种细节卡住的开发者。我不讲虚的,尽量把每个模块为什么这么设计都说清楚。

1. 重建式VLA到底在解决什么问题

1.1 难点为什么不在模型,而在数据的“信息密度”

很多人第一次接触 VLA 时会以为难点在模型结构。实际上,模型结构只要照着成熟的视觉语言模型改一改就能用,最麻烦的是数据。机器人领域和 NLP、CV 不一样,数据里动作标签经常有噪声,语言指令也存在歧义,比如“把杯子放到桌上”,这句话并没有说放在桌子的哪个具体位置,也没说目标是哪个颜色相近的杯子。

过去主流做法是直接把动作作为文本 token 输出,让 VLM 预测一串类似“move to coordinate (x, y)”的文本。这么做有一个隐含风险:模型只看文本动作标签,很容易学会“抄答案”,而不是学会理解场景。只要换一个背景、换一个光照,策略就会崩。原因很简单——文本动作本身就是人类事后标注的低维信息,信息密度太低,模型根本没有被逼着去注意物体形状、边缘、空间位置这些细节。

重建式 VLA 的思路不同。它在训练时额外要求模型把当前场景、甚至未来几帧场景重建出来。相当于你带一个新员工,不只要他记住“把杯子放回桌子”这句话,还要他先仔细辨认桌子长什么样、杯子抓在哪个位置、路径上有没有障碍,然后再把动作决定写下来。信息的使用方式完全不一样。

这也是源码里最值得关注的第一点:它不是在原来 VLM 后面简单挂一个动作 head,而是通过重建目标倒逼视觉编码器保存更丰富的空间细节。源码里大量细节,包括 tokenizer 的选择、重建头的输入输出、损失函数权重,都是围绕这个目标服务的。

1.2 重建目标为什么能改变策略学习的上限

从优化角度来看,纯行为克隆的损失函数通常长这样:

loss_action = MSE(action_pred, action_gt)

这个损失在动作标签准确的前提下没问题,但它的梯度信息非常稀疏。一张 256×256 的图,最后只得到一个 7 维动作向量,中间绝大部分像素其实没有参与监督。模型自然倾向于只关注少数显著区域,一旦场景出现变化就翻车。

重建式 VLA 在损失函数里加了一项:

loss = loss_action + lambda * loss_reconstruction

重建损失提供的是稠密梯度。每个像素、每个 latent token 都可能对模型产生监督,模型被迫理解“杯子是圆形的”“把手在杯子侧面”“桌子台面是平的”这些底层结构。它不依赖动作标签的质量,能直接从图像本身吸收可迁移的视觉语义。

源码里最常见的做法不是直接重建 RGB 像素,而是重建离散视觉 token 或低维 latent。原因是纯像素重建训练不稳定、计算开销高,而且容易产生模糊结果。通过一个预训练的 VQ tokenizer,把图像压缩成离散 token 序列,再让模型去预测这些 token,既保留了空间信息,又把任务变成一个更规整的分类问题。

在真实复现中,这种重建分支对模型的泛化提升非常明显。我做过一组对比实验:同样一个模型,去掉重建头之后,在已见过的训练场景里表现几乎一样;但一旦换到新场景、新物体位置,带重建头的模型成功率高出不少。原因是它学会了“场景几何”,而不是单纯记住了动作映射表。

2. 源码整体设计与模块拆解

2.1 架构总览:一个视觉语言骨干,两个输出头

那套开源实现的整体架构并不复杂,概括下来就是:一个视觉语言骨干,两个输出头。

视觉语言骨干负责把图像和文本指令编码成语义一致的隐状态。图像先被视觉编码器转成 patch embedding,文本指令经过分词和 embedding 后,拼接成同一个 token 序列。这个序列送入多层 Transformer 骨干做交叉建模。骨干的输出隐藏状态,同时被两个 head 使用:

  • 动作头(Action Head):从序列中特定位置取 token 状态,回归出连续动作向量,或分类出离散动作 token。
  • 重建头(Reconstruction Head):把隐藏状态映射回视觉 token 空间,重建当前帧或未来帧的 latent 表示。

这个双头结构很关键。如果没有重建头,模型只在动作监督下工作,骨干很容易退化成“一个只认颜色和粗糙位置的特征提取器”。有了重建头,骨干除了要回答“该干什么”,还要回答“场景到底长什么样”。这就相当于多任务学习,视觉语言骨干被迫学习一个更完整、更通用的中间表征。

我在源码里注意到一个细节:两个 head 不是简单并联,而是共用了最后一层 Transformer 的隐藏状态。这意味着重建任务和动作任务在底层是真正被联合处理的,不是各学各的。重建任务提供的梯度会直接影响骨干参数,这就是它和“先训练一个视觉编码器、再冻结做策略”的最大区别。后者只把重建当成离线前置,完全起不到联合正则化的作用。

2.2 数据管线:从一堆原始数据集到可训练样本

源码里最反直觉的部分往往不在模型定义,而在数据管线。一个真正可用的 VLA 训练管线,需要处理的数据种类太多了:单臂操作、双臂操作、移动操作、仿真数据、遥操作数据。它们动作维度不同、相机视角不同、指令粒度不同,直接拼在一起会让模型学疯。

源码的做法是先做统一的中间格式。每条样本包含这样几个字段:

  • 观测图像序列:通常是最近 3 到 6 帧,按时间顺序堆叠,用于感知物体运动和当前状态。
  • 语言指令:统一模板化,最简单就是“把 [目标物体] 放到 [目标位置]”。
  • 动作序列:连续动作向量,归一化到固定范围。
  • 时间戳和相机参数:记录动作对应的时间点、相机内参外参,用于后续视角对齐。

数据混合的时候,源码没有简单随机拼接,而是用了“任务级加权采样”。高难度任务、多样化场景的样本会被提高采样权重,单一重复的简单样本会被主动降权。这个细节直接影响模型在复杂任务上的表现,我在复现时一开始没有注意,导致模型在简单场景上很准,一到长 horizon 任务就彻底迷路。

还有一个细节是动作分块(Action Chunking)。源码不是每一步预测一个动作,而是每来一次观测,直接预测未来 16 步动作序列。这么做的好处有二:一是让动作轨迹更平滑,避免高频抖动;二是减少了策略对于“观测噪声”的敏感度。训练时这 16 步动作要对应样本里连续 16 个时间步,所以数据管线里会有专门的 chunk 切片逻辑,不能随便跳过帧。

2.3 重建头设计:像素级重建和隐空间重建的取舍

重建头是整个源码里最值得琢磨的部分,它决定了模型能不能真正理解场景。

像素级重建最直观,把图像送进解码器,输出重建后的 RGB 图,用 MSE 或 L1 做损失。但实际做的时候会发现两个问题。第一个是计算量太大,在 VLM 骨干后面再挂一个像素级解码器,显存直接多出几个 GB;第二个是像素损失容易产生模糊结果,模型特别容易学出一个“平均图像”,对细节完全无感。真机部署时,这种模糊反馈在特征空间里会产生误导。

源码里更常见的做法是隐空间重建,分三步:

  1. 用训练集训练一个离散视觉 tokenizer,把 256×256 图像压缩成若干离散 token。
  2. 在 VLA 训练时,把真实图像通过 tokenizer 编码得到目标 token 序列。
  3. 重建头输出每个位置的 token 概率分布,用交叉熵损失和真实 token 序列做监督。

这样重建任务从“预测像素”变成“预测离散 token 类别”,模型要准确回答这个 patch 属于哪个语义类别,比预测颜色更抽象,也更符合策略学习的需求。这个思路跟视觉语言模型中用图像 tokenizer 做对齐的做法一脉相承。

选择隐空间重建还有一个额外好处:可以和未来预测结合起来。如果训练样本里包含未来帧,重建目标不恢复当前帧,而是恢复未来 1 到 3 帧的 latent,模型就变成了一个小型世界模型。它必须估计出“物体下一步会以什么状态出现在什么位置”。这对长程任务特别有价值。源码里把“重建当前场景”和“重建未来场景”做成可配置选项,我在实验时发现,未来重建对动作成功率的提升比当前重建明显很多。

3. 核心实现与训练细节

3.1 从配置文件到训练脚本

拿到源码第一步不是读模型代码,而是先看配置目录。这套实现的配置组织得很清楚,基本可以分成三个部分:模型配置、数据配置、训练配置。目录结构类似下面这样:

- configs/ - train.yaml - model/vla.yaml - model/backbone.yaml - data/open_source_mix.yaml - vla/ - model.py - heads/ - action_head.py - reconstruction_head.py - tokenizer/ - vq_tokenizer.py - datasets/ - base_dataset.py - mixed_dataset.py - trainer.py - inference.py

train.yaml是整个流程的入口,决定模型用哪个 backbone、数据用哪个 mix、损失权重是多少。model.py负责把 backbone、动作头、重建头拼起来;mixed_dataset.py负责把多份数据集统一成训练样本。这个层次划分对复现很友好,改动某个部分不需要牵动全局。

训练入口代码通常长这样:

model = ReconstructiveVLA( backbone_cfg=cfg.model.backbone, action_dim=cfg.model.action_dim, chunk_size=cfg.model.chunk_size, reconstruction_head_cfg=cfg.model.reconstruction_head, )

虽然只是寥寥几行,但背后有一个容易忽略的点:chunk_size被放在模型配置里,而不是数据配置里。这个设计不是随意的。chunk size 决定动作头输出多少个未来动作,它影响重建头的对齐方式,也影响推理策略,所以它本质上是模型结构的一部分,不是数据预处理的参数。我自己第一版把 chunk size 放在数据配置里,结果数据切分和模型输出对不上,排查了很久才发现是配置层级的问题。

3.2 损失计算、梯度裁剪与优化器配置

模型前向的核心部分,简化后可以写成这样:

class ReconstructiveVLA(nn.Module): def __init__(self, encoder, decoder, action_head, recon_head): super().__init__() self.encoder = encoder self.decoder = decoder self.action_head = action_head self.recon_head = recon_head def forward(self, image, instruction, action=None, visual_token_target=None): # 1. 图像和指令一起编码 hidden = self.encoder(image, instruction) # 2. 动作头读取动作 token 位置的隐状态 action_pred = self.action_head(hidden) # 3. 重建头把隐状态映射回视觉 token 空间 recon_logits = self.recon_head(hidden) if self.training: loss_action = F.mse_loss(action_pred, action) loss_recon = F.cross_entropy( recon_logits.permute(0, 2, 1), visual_token_target, ignore_index=-100 ) return loss_action + self.lambda_recon * loss_recon return action_pred

真实的训练循环会在这个基础上加不少东西。首先是梯度裁剪,源码里普遍会把 grad norm 限制在 1.0 左右。VLA 是视觉语言骨干和策略头的联合训练,两个任务梯度方向经常不完全一致,不裁剪的话经常训练两三千步就出现 loss 突刺。其次是优化器,很多实现直接沿用了 LLM 的 AdamW + warmup + cosine decay 配置。我的经验是 warmup 至少要给 1000 步,千万不能省。VLM 骨干加载预训练权重之后,如果一开始就用大学习率激活全参数,重建分支的随机初始化会使总损失剧烈震荡。

还有一个小但非常重要的细节:EMA(指数移动平均)策略。源码里动作头和重建头通常各自保留一份 EMA 参数副本,在验证和推理时用 EMA 参数,而不是用实时参数。这样做对稳定策略输出很有帮助,相当于把训练过程中震动的权重做了一个平滑。我在复现时不加 EMA,实机测试时动作轨迹明显比加 EMA 要“毛躁”。

3.3 推理时策略怎么把“动作”抠出来

训练和推理的一个关键差异,在推理阶段模型不再有重建目标。重建头只在训练时参与,推理时把它从计算图里摘掉。源码在写推理逻辑时通常会有类似model.eval()加inference_mode的双重包装,避免重建头白白消耗显存和计算时间。

推理阶段真正的核心问题是怎么从骨干输出里抽取动作。如果动作头是连续回归式,直接取动作 token 对应位置的隐藏状态,过一个多层感知机输出连续向量就行。如果动作是离散式,则需要从隐藏状态生成动作 token,再通过逆映射得到具体数值,解码时还需要在动作 token 集合上做一遍 softmax 采样。

源码里一般会单独列一份逆归一化配置,把训练前算好的均值、标准差、最大值最小值存下来,推理时做逆变换。这个步骤看着简单,却是最容易出 bug 的地方。我踩过的一个坑是:训练用的动作归一化统计来自全部数据集,但推理时没有加载这份统计,直接把模型输出发到底层控制器。结果就是模型动作看起来合理,但幅度全部偏大,机器人在真实环境里“甩手”。正确做法是把这些统计参数序列化保存,推理时和模型权重一起加载。

如果是连续动作头,还有一个采样策略问题。最简单的方式是直接用均值输出、不做采样,这样动作最稳定。但代价是有时会陷入局部平庸,比如多峰任务里均值动作会打在两条可行轨迹之间。更稳的做法是预测 K 个候选动作,在推理时用某种打分函数选最优的一个。源码里通常用轨迹成本函数、重建误差或者简单的动作自洽性打分来选。

3.4 多卡训练、混合精度与显存策略

VLA 模型规模通常不小,骨干参数几百亿级别的也有,但开源复现一般用几十亿或几亿规模的模型。不管哪种,多卡训练都是避不开的。源码里常用的是 FSDP 或 ZeRO,把模型参数、梯度和优化器状态切到多卡上。用 FSDP 时有个配置要特别注意:重建头通常不参与 FSDP 的参数切分,因为重建头本身不大,切分反而增加通信开销。

混合精度方面,大部分实现用 bf16。bf16 的指数范围和 fp32 一样,训练起来比 fp16 稳定很多,不需要手动做 loss scaling。但要注意,批量归一化或者某些自定义算子对精度比较敏感,我发现重建头的交叉熵损失在 bf16 下容易出现数值不稳定,给它单独保留 fp32 计算会更稳妥。

还有一个很多人容易忽略的加速技巧:离线缓存视觉编码器的输出。如果视觉编码器在训练时是冻结的,那么同一张图像每次迭代都会被重复编码,非常浪费算力。源码里很多实现会提前把图像编码成视觉 embedding 存到磁盘或内存,训练时直接加载 embedding,而不是加载原始图像。这样可以省下大量视觉编码时间。代价是数据增强受限,一旦冻结视觉编码器,就不能在训练时做在线 color jitter 这类增强。我在复现时用了离线缓存,训练速度差不多快了 1.5 到 2 倍,所以这是一个非常值得投入的优化。

4. 复现过程中最常见的坑与排查

4.1 显存直接爆掉

第一次按默认配置跑这套源码,十有八九会碰见显存溢出。原因往往不是骨干模型太大,而是重建头和长序列同时存在。图像经过视觉编码器后 token 数量本来就多,如果重建头要在每个 token 位置输出一份分类 logits,那 logits 的量级会非常夸张。

排查顺序建议是这样的:先关掉重建头,只看动作头的显存占用;如果这个占用你还能接受,那问题出在重建头。解决手段有几个,可以叠加使用:

  • 降低重建目标的空间分辨率,比如让重建头只恢复 32×32 的 latent,而不是 128×128。
  • 对重建头使用 block-wise 计算,分块计算 logits,减少一次性显存峰值。
  • 启用梯度检查点,用少量计算换显存。
  • 如果骨干够大,还可以直接用 LoRA 之类的参数高效方案冻结大部分骨干参数。

我在复现时按这个顺序操作,显存从爆掉降到大概占了原来的六成。不要一上来就换小模型,那样会把实验结论也一起带偏。

4.2 两个损失为什么总是打架

重建损失和动作损失在训练初期经常出现一边快速下降、一边原地不动的情况。这不是代码 bug,而是两个任务的学习难度和梯度尺度差异太大。动作损失是低维回归,量级通常非常小;重建损失是大量 token 的分类损失,累加之后数值会高出好几个量级。

源码一般会给lambda_recon一个固定权重,但这个权重在不同数据集上不是普适的。我建议训练前先不看最终 loss,而是分别打印两个独立 loss 的初始值。然后根据初始值选 lambda,让两者在数量级上差不多。比如动作 loss 初始 1.2,重建 loss 初始 0.8,lambda 可以设 1.5 左右。如果重建 loss 初始就比动作 loss 大十倍,那 lambda 反而要小于 1,不要无脑调大重建权重。

还有一种更细腻的做法是给重建损失加一个梯度阻塞:前几百步只更新动作头和相关骨干,重建头先什么都不干,等动作主任务稳住了再放开。这样能避免两个任务在初期抢梯度。源码里未必会写,但实测下来确实管用。

4.3 机器人动作在实机上“发抖”

模型在仿真里跑得好好的,一上真机就抖,这是很典型的问题。通常不是模型本身错乱了,而是推理输出不稳定。VLA 推理频率一般不会太高,几赫兹到十几赫兹都有,但底层控制器往往需要更高频的指令。如果每次推理输出之间出现微小跳变,底层执行器就会表现出震颤。

我的经验是几件事一起做:

  • 使用动作分块,推理时输出未来 16 步,但只取第一帧动作下发,后面 15 帧作为平滑缓冲。
  • 对动作头输出做 EMA 平滑,公式大概是action_smooth = alpha * action_pred + (1 - alpha) * action_smooth。
  • 限制单步动作的最大变化量,超过阈值就按阈值截断。

还有一个容易忽略的因素是观测堆叠。我只用单帧图像输入的时候抖动最明显,后来把最近到 3 到 4 帧堆叠起来,模型对瞬时噪声不那么敏感了,动作稳定度明显提升。图像帧率不要随意调整,源码里的频率预设是一个固定值,改变帧率等价于改变动作的速度语义。

4.4 指令和视觉对不上,模型看字不看图

如果训练数据里的指令和图像内容匹配度不高,模型会学到很偷懒的策略:只根据语言指令做平均动作,完全忽略图像。表现就是换一个场景、换一个物体颜色,模型照样按旧动作执行。

排查时可以做一个简单的 checker:固定同一句指令,换不同背景的图片做推理。如果输出动作完全不变,基本可以确认模型被语言指令主导了。修复手段主要是数据清洗和指令模板统一。源码里对指令文本有很严格的模板要求,不鼓励在指令里写多余信息。比如“把红色的杯子放到托盘上”,模型默认只需要关注“红色杯子”和“托盘”,其他描述词都会干扰特征对齐。

如果数据本身没有问题,还有可能是重建头把大部分语义责任揽过去了,导致视觉语言骨干在文本侧过度学习。这时候可以尝试降低重建头的层数,或者砍掉一部分重建 token 数量,逼迫模型更依赖原始多模态输入。这个调参方向在实际排查中经常有效。

5. 复现经验留档与后续改造建议

关于损失权重,我最终选择的是让重建损失占总损失的三成左右,这个配置在中等规模数据下收敛最快,但每个数据集都要重新标定。如果你在复现时发现训练 loss 下降很快、评测却不行,大概率就是动作任务被重建任务过度压制了。

如果你是准备把重建式 VLA 用到自己的机器人项目里,我的建议是先别急着从头训练。挑一个开源预训练好的视觉语言骨干,冻结住,只在上面训练重建头和动作头,先把 pipeline 跑通。等整体训练、部署、实机的闭环稳定了,再考虑解冻骨干做全参数微调。这样能把变量控制到最小,排查问题也更容易定位。

另外一个小技巧:在训练早期只看重建头输出的可视化结果,不看动作指标。如果重建结果在训练集上都模糊不清,说明骨干没有学到足够好的中间表征,这时候做任何策略优化都是在沙地上盖楼。重建可视化是判断模型是否“理解”场景的最快标尺。

我个人在踩过这么多坑之后,如果只能留一条经验,那就是:重建式 VLA 的价值不在那个可爱的重建分支本身,而在于它迫使整个模型去学习场景的结构化表示。源码里的一切细节,数据对齐、tokenizer 选择、损失配比、EMA 平滑,都是在为这个目标服务。真正读懂这条主线,后面无论换骨干还是换下游任务,你都知道该动哪里。

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

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

立即咨询