1. 从VLA到在线强化学习:为什么需要RL Token
视觉-语言-动作模型(Vision-Language-Action,简称VLA)这两年在机器人操作领域的热度不用我多说,但凡关注过具身智能方向的朋友应该都有感知。从最早的RT-1、RT-2,到后来的OpenVLA、π0,VLA把视觉感知、语言理解和动作生成统一到一个模型里,确实解决了很多传统方法搞不定的泛化问题。但真正上手做过VLA微调的人都知道,这东西在真实场景里落地时有个绕不开的坎:离线模仿学习的天花板很低。
你拿一堆示教数据去训练VLA,模型能学会“看到什么做什么”,但它学到的本质是人类示教动作的统计分布。一旦环境有轻微变化——光照变了、物体位置偏了几厘米、背景多了一个干扰物——模型的表现就会明显下滑。更麻烦的是,VLA的输出是连续动作序列,动作空间维度高、时序依赖强,传统的强化学习算法直接往上套,训练极不稳定,reward稀疏的时候几乎学不动。
RL Token这套思路,就是在这个背景下被提出来的。它的核心想法很直接:不直接在高维动作空间上做强化学习,而是让VLA先学一个紧凑的“token表示”,在这个低维表示空间里做在线RL。你可以把它理解成给VLA加了一个“决策瓶颈层”——VLA负责把复杂的视觉-语言输入压缩成一组有语义的token,actor-critic在这个token空间里做探索和优化,最后再通过一个解码器把token映射回具体动作。
这个设计解决的核心问题是样本效率和训练稳定性。直接在原始动作空间做RL,一个七自由度机械臂的动作维度就是7维连续值,加上时序就是几十维,探索空间巨大,训练几百万步都不一定收敛。而RL Token把动作空间压缩到比如16维或32维的token空间,探索维度直接降了一个数量级,同时token本身携带了VLA预训练学到的语义先验,相当于站在巨人的肩膀上做RL。
适合读这篇内容的人,我大致分三类:一是已经在做VLA微调、想进一步提升模型在线适应能力的工程师;二是做机器人强化学习、想了解如何跟大模型结合的研究者;三是对具身智能感兴趣、想搞清楚VLA和RL怎么串起来的学生或爱好者。不管你属于哪一类,下面我会从设计思路、核心细节、实操流程到踩坑经验,把RL Token这套东西拆开讲清楚。
2. RL Token整体设计思路拆解
2.1 为什么不在原始动作空间直接做RL
先说一个我自己的真实经历。去年我尝试在一个六自由度机械臂的抓取任务上,直接用SAC算法在VLA输出的动作空间上做微调。结果跑了三天,reward曲线基本是一条水平线,偶尔波动一下又回去了。后来分析原因,主要有三个:
第一,动作空间维度太高。VLA输出的动作通常是末端执行器的位姿增量加上夹爪开合,6+1=7维连续动作,如果考虑时序窗口比如预测未来16步,那就是112维。SAC在高维连续空间里的探索效率是指数级下降的,这是维度灾难的典型表现。
第二,VLA的预训练知识被浪费了。VLA在预训练阶段已经学到了“看到杯子应该伸手”“看到抽屉应该拉”这类语义先验,这些知识编码在它的中间层表示里。但如果你直接在输出动作上加噪声做探索,相当于把VLA当成一个黑盒,它内部那些有用的表示完全没被RL利用。
第三,reward信号太稀疏。机器人任务里,成功抓取给1分、失败给0分,中间过程没有reward。这种稀疏reward下,随机探索几乎不可能碰到成功状态,critic根本学不出有效的价值函数。
RL Token的设计正是针对这三个痛点。它不让RL直接碰原始动作,而是让VLA先输出一组token,这组token是VLA对当前场景的“决策摘要”,维度低、语义强、连续可微。actor在这个token空间里输出动作,critic在这个空间里评估价值,探索效率自然就上去了。
2.2 RL Token的核心架构长什么样
整个架构可以分成三个模块:VLA编码器、RL Token头、actor-critic网络。
VLA编码器就是预训练好的VLA模型,比如OpenVLA或π0,它的参数在RL阶段通常是冻结的,或者只做很小的微调。输入是当前观测(RGB图像+语言指令),输出是中间层的特征表示。这个特征表示通常是几百维的向量序列,直接拿来做RL还是太大。
RL Token头是一个小的映射网络,通常就是两三层MLP,把VLA的中间特征压缩成K个token,每个token比如32维或64维。K一般取8到16,太小了表达力不够,太大了又失去了压缩的意义。这个Token头是RL阶段主要训练的部分之一。
actor-critic网络接在token后面。actor输入token,输出一个动作向量,这个动作向量再通过一个解码器映射回原始动作空间。critic输入token,输出一个标量价值估计。actor和critic都是小的MLP,参数量远小于VLA本身。
这里有个关键设计:token空间是连续的,不是离散的。有些工作用VQ-VAE把动作离散化成codebook,但离散化会带来梯度断裂的问题,RL训练时不好做。RL Token保持连续,actor输出的动作可以直接对token做加性噪声探索,梯度可以顺畅回传。
2.3 在线RL的闭环是怎么跑起来的
整个训练流程是一个在线闭环:机器人执行当前策略,收集transition(观测、token、动作、reward、下一观测),存进replay buffer,然后从buffer里采样batch更新actor和critic。
具体来说,每个step:
- 当前观测(图像+语言指令)送入VLA编码器,得到中间特征
- 特征送入RL Token头,得到K个token
- actor根据token输出动作,加探索噪声后解码成实际动作
- 机器人在环境中执行动作,得到reward和下一观测
- 把transition存入buffer
- 从buffer采样,更新actor和critic,同时可选地微调Token头
这个闭环的关键在于token头是可训练的。如果token头固定,那RL只能在一个固定的表示空间里优化,表达能力受限。让token头跟着RL一起更新,相当于让VLA的表示逐渐适应RL任务的需求,这是端到端优化的思路。
但这里有个坑:如果token头更新太快,VLA的预训练知识会被破坏,模型会遗忘之前学到的语义先验。所以实践中通常给token头设一个很小的学习率,比如VLA主干的十分之一,或者用LoRA这类参数高效微调方法。
3. 核心细节解析与实操要点
3.1 Token维度怎么选:不是越大越好
Token的维度和数量是两个需要仔细调的参数。我试过几组配置,分享下经验。
Token数量K,常见取值是8、16、32。K太小比如4,token表达力不够,复杂任务下actor学不动;K太大比如64,压缩效果不明显,RL探索效率下降。我的经验是:简单抓取任务K=8够用,多步骤操作任务K=16到32比较合适。
单个token的维度D,常见取值是32、64、128。D太小比如16,token信息容量不足;D太大比如256,跟直接用VLA特征差别不大。D=64是一个比较稳妥的起点。
总token空间维度就是K×D。K=16、D=64时,总维度1024,比原始动作空间的7维还是大很多。但注意,actor输出的动作维度不是K×D,而是直接输出一个动作向量,这个向量通过交叉注意力或MLP从token里聚合信息。所以实际探索维度是actor输出维度,通常就是原始动作维度,但actor的输入是token,它学的是“从token到动作”的映射,这个映射比“从原始观测到动作”简单得多。
3.2 actor-critic的具体实现细节
actor和critic的网络结构不用太复杂,两层MLP加ReLU就够了。actor输出用tanh激活,把动作限制在[-1,1],再根据实际动作范围做缩放。critic输出线性激活,不加限制。
损失函数方面,我用的是标准的SAC损失:
- critic损失:均方误差加上target网络的软更新
- actor损失:最大化Q值加上熵正则项
- 温度参数α:自动调节,目标熵设为-action_dim
这里有个细节:target网络的更新频率。SAC默认是每个step软更新一次,τ=0.005。但在RL Token场景下,因为token头也在更新,target网络如果更新太快,价值估计会不稳定。我的做法是τ降到0.001,或者每两步更新一次。
另一个细节是replay buffer的大小。在线RL样本宝贵,buffer不用太大,10万到50万条足够。太大反而会让旧数据稀释新数据,影响在线适应速度。
3.3 VLA主干冻结还是微调
这是个策略选择问题。三种方案我都试过:
| 方案 | VLA主干 | Token头 | actor-critic | 效果 | 训练速度 |
|---|---|---|---|---|---|
| 全冻结 | 冻结 | 训练 | 训练 | 稳定但上限低 | 快 |
| 部分微调 | LoRA | 训练 | 训练 | 平衡 | 中等 |
| 全微调 | 训练 | 训练 | 训练 | 上限高但不稳定 | 慢 |
全冻结方案下,VLA就是一个固定的特征提取器,RL只学token到动作的映射。优点是训练稳定,缺点是如果VLA预训练任务和当前任务差异大,特征质量不行,RL再怎么学也突破不了天花板。
全微调方案下,VLA参数也跟着RL梯度更新。理论上限最高,但实践中很容易崩——VLA参数量大,RL梯度噪声也大,两者一叠加,模型很快就发散。
我推荐的是LoRA微调方案。在VLA的注意力层加低秩适配器,只训练这些适配器参数,参数量只有原模型的1%到5%。这样既能让VLA特征适应当前任务,又不会破坏预训练知识,训练也稳定。LoRA的秩r一般取8到16,α取16到32。
3.4 探索噪声怎么加
在token空间做探索,噪声加在哪里很关键。两种做法:
一种是在actor输出上加噪声。actor输出动作向量a,然后a+ε,ε从高斯分布采样。这是标准做法,简单直接。
另一种是在token上加噪声。token t加上噪声后再送入actor。这种做法相当于在表示空间做探索,理论上探索范围更大,但容易破坏token的语义结构。
我两种都试过,在actor输出上加噪声更稳。token上加噪声虽然探索范围大,但经常导致actor收到无意义的token,输出乱动作,训练早期很容易崩。如果要用token噪声,建议噪声方差设得很小,比如0.01,而且只在训练早期用,后期退火到0。
噪声方差本身也需要调。SAC是自动调温度参数α,但α控制的是熵正则的强度,不是直接控制噪声方差。实践中我固定噪声方差为0.1到0.2,然后靠α自动调节探索强度。
4. 实操过程与核心环节实现
4.1 环境搭建与依赖安装
假设你已经有一个预训练好的VLA模型,比如OpenVLA的7B版本。硬件方面,训练需要至少一张24G显存的卡,推理可以量化到8bit跑在16G卡上。
依赖安装:
pip install torch==2.1.0 pip install transformers==4.40.0 pip install gymnasium==0.29.1 pip install stable-baselines3==2.3.0 pip install wandbstable-baselines3提供了SAC的实现,但我们需要改一下,把actor和critic的输入从原始观测改成token。所以实际用的时候,我是基于sb3的SAC源码改的,主要改predict和train两个方法。
4.2 VLA特征提取与Token头实现
先加载VLA模型,冻结参数:
from transformers import AutoModelForVision2Seq, AutoProcessor import torch import torch.nn as nn vla = AutoModelForVision2Seq.from_pretrained( "openvla/openvla-7b", torch_dtype=torch.bfloat16, device_map="auto" ) for param in vla.parameters(): param.requires_grad = False processor = AutoProcessor.from_pretrained("openvla/openvla-7b")Token头的实现:
class RLTokenHead(nn.Module): def __init__(self, input_dim=4096, num_tokens=16, token_dim=64): super().__init__() self.num_tokens = num_tokens self.token_dim = token_dim self.proj = nn.Sequential( nn.Linear(input_dim, 1024), nn.ReLU(), nn.Linear(1024, num_tokens * token_dim) ) def forward(self, vla_features): # vla_features: (batch, input_dim) x = self.proj(vla_features) x = x.view(-1, self.num_tokens, self.token_dim) return x # (batch, num_tokens, token_dim)这里input_dim=4096是OpenVLA的隐藏层维度,实际用的时候要根据你的VLA模型调整。num_tokens=16、token_dim=64是我常用的配置。
4.3 actor-critic网络与训练循环
actor和critic:
class Actor(nn.Module): def __init__(self, token_dim=64, num_tokens=16, action_dim=7): super().__init__() self.net = nn.Sequential( nn.Linear(num_tokens * token_dim, 256), nn.ReLU(), nn.Linear(256, 256), nn.ReLU(), nn.Linear(256, action_dim) ) def forward(self, tokens): x = tokens.view(tokens.size(0), -1) return torch.tanh(self.net(x)) class Critic(nn.Module): def __init__(self, token_dim=64, num_tokens=16, action_dim=7): super().__init__() self.net = nn.Sequential( nn.Linear(num_tokens * token_dim + action_dim, 256), nn.ReLU(), nn.Linear(256, 256), nn.ReLU(), nn.Linear(256, 1) ) def forward(self, tokens, actions): x = tokens.view(tokens.size(0), -1) x = torch.cat([x, actions], dim=-1) return self.net(x)训练循环的核心逻辑:
def train_step(batch, actor, critic, critic_target, token_head, log_alpha, optimizer): obs, tokens, actions, rewards, next_obs, dones = batch # 计算target Q值 with torch.no_grad(): next_tokens = token_head(vla_encode(next_obs)) next_actions, next_log_probs = actor.sample(next_tokens) q1_next, q2_next = critic_target(next_tokens, next_actions) q_next = torch.min(q1_next, q2_next) - log_alpha.exp() * next_log_probs target_q = rewards + (1 - dones) * 0.99 * q_next # 更新critic q1, q2 = critic(tokens, actions) critic_loss = F.mse_loss(q1, target_q) + F.mse_loss(q2, target_q) optimizer_critic.zero_grad() critic_loss.backward() optimizer_critic.step() # 更新actor new_actions, log_probs = actor.sample(tokens) q1_new, q2_new = critic(tokens, new_actions) q_new = torch.min(q1_new, q2_new) actor_loss = (log_alpha.exp() * log_probs - q_new).mean() optimizer_actor.zero_grad() actor_loss.backward() optimizer_actor.step() # 更新温度参数 alpha_loss = -(log_alpha * (log_probs + target_entropy).detach()).mean() optimizer_alpha.zero_grad() alpha_loss.backward() optimizer_alpha.step() # 软更新target网络 for param, target_param in zip(critic.parameters(), critic_target.parameters()): target_param.data.copy_(0.001 * param.data + 0.999 * target_param.data)注意这里token_head也在优化器里,但学习率设得很小,比如1e-5,而actor和critic的学习率是3e-4。
4.4 参数选择与计算过程
几个关键参数的计算依据:
折扣因子γ=0.99。机器人任务通常episode长度在100到500步,γ=0.99对应有效视野约100步,比较合适。如果任务更长比如1000步,γ可以调到0.995。
目标熵=-action_dim。SAC原论文的建议,action_dim=7时目标熵就是-7。这个值控制探索强度,目标熵越大探索越强。
学习率。actor和critic用3e-4,这是SAC的常用值。token头用1e-5,比主干小两个数量级,防止破坏VLA预训练知识。log_alpha用3e-4。
batch size=256。在线RL样本少,batch不用太大,256足够稳定。如果显存够,可以到512。
replay buffer=200000。大概能存几百个episode的数据,足够覆盖任务的各种状态。
预热步数=5000。前5000步只用随机策略收集数据,不更新网络。等buffer里有足够多样本后再开始训练,避免早期过拟合。
5. 常见问题与排查技巧实录
5.1 训练不收敛的几种典型表现
表现一:reward一直不涨,critic loss震荡。这通常是因为token头学习率太大,VLA特征被破坏。排查方法:把token头学习率降到1e-6,观察critic loss是否稳定。如果还是震荡,直接把token头冻结,只训练actor和critic,看能不能收敛。能收敛说明token头是问题所在。
表现二:reward涨了又掉,反复横跳。这是典型的灾难性遗忘。VLA预训练知识被RL梯度覆盖了,模型在新任务上过拟合,旧能力丢失。解决办法:加LoRA而不是全微调,或者给token头加L2正则,限制它偏离初始值太远。
表现三:actor输出饱和,动作全是±1。tanh激活在输入很大时会饱和,梯度消失。检查actor最后一层的输入范围,如果太大,在前面加LayerNorm。或者把tanh换成clipped ReLU,但SAC一般用tanh,所以还是加LayerNorm更稳妥。
5.2 样本效率低的优化方向
如果训练步数很多但效果不好,可以从这几个方向优化:
优先回放。给buffer里的transition加优先级,TD误差大的样本采样概率高。sb3的SAC默认不支持优先回放,需要自己改。我改过一版,样本效率大概提升30%。
n-step return。用n步回报代替单步回报,n取3到5。这能加速价值传播,尤其适合reward稀疏的任务。实现上就是在buffer里存n步的累积reward。
辅助任务。在token头上加一个辅助损失,比如重构VLA原始特征,或者预测下一帧的视觉特征。这能防止token头退化,保持表示质量。辅助损失权重一般取0.1到0.5。
课程学习。先从简单任务开始,比如物体位置固定、光照固定,等reward稳定了再逐渐增加难度。这能显著提升早期样本效率。
5.3 常见问题速查表
| 问题 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| reward不涨 | token头学习率太大 | 冻结token头看是否收敛 | 降学习率或冻结 |
| critic loss震荡 | target网络更新太快 | 降τ到0.001 | 降低软更新频率 |
| actor输出饱和 | 输入范围太大 | 检查actor输入统计 | 加LayerNorm |
| 灾难性遗忘 | VLA被过度微调 | 对比微调前后VLA输出 | 用LoRA或加正则 |
| 样本效率低 | 探索不足 | 看动作分布熵 | 提高目标熵或加噪声 |
| 训练后期崩 | 过拟合 | 看验证集reward | 加dropout或早停 |
5.4 几个我踩过的坑
坑一:token数量设成1。我一开始想,既然要压缩,那就压到极致,K=1。结果actor学不动,因为一个token表达力太有限,复杂任务下信息瓶颈太严重。后来改成K=16,效果立刻好了。所以token数量不能太少,8是下限。
坑二:忘了冻结VLA的BN层。VLA里如果有BatchNorm层,即使参数冻结,BN的running mean和var也会在forward时更新。这会导致VLA输出分布漂移,RL训练不稳定。解决办法:把所有BN层设成eval模式,或者换成LayerNorm。
坑三:replay buffer存了token而不是原始观测。token是VLA输出的,如果VLA更新了,旧token就失效了。所以buffer里要存原始观测,训练时重新过VLA算token。虽然计算量大一点,但保证了一致性。
坑四:探索噪声加在了错误的地方。我一开始在token上加噪声,结果actor收到的token经常是无意义的,输出乱动作。后来改成在actor输出上加噪声,训练立刻稳定了。所以噪声位置很关键,加在动作上比加在表示上更安全。
6. 这套方案还能怎么扩展
RL Token这套框架本身是一个通用思路,不局限于某个特定的VLA或某个特定的机器人任务。我目前看到几个比较有潜力的扩展方向。
多任务共享token空间。如果多个任务共享同一个VLA,可以让它们共享token头,但各自有独立的actor-critic。这样token头学到的是跨任务的通用表示,actor-critic学的是任务特定的策略。训练时多任务数据一起喂,token头的表示质量会更高。
离线到在线的平滑过渡。先用离线数据预训练token头和actor-critic,比如用IQL或CQL这类离线RL算法,然后再切换到在线微调。这样能避免在线RL早期的大量随机探索,样本效率更高。
token空间的可解释性分析。token是VLA特征的压缩表示,它到底编码了什么信息?可以用探针实验分析,比如用token预测物体位置、抓取点、运动方向等。这能帮我们理解RL到底在学什么,也能指导token头和actor的设计。
与模型预测控制结合。token空间可以做短时预测,用token预测未来几步的状态,然后做模型预测控制。这比直接在原始观测空间做MPC计算量小得多,实时性更好。
我个人在实际操作中的体会是,RL Token这套东西最大的价值不在于某个具体算法,而在于它提供了一种分层决策的思路:大模型负责感知和语义理解,小网络负责在线适应和精细控制。这个思路可以迁移到很多场景,不限于机器人操作。比如自动驾驶里,感知大模型输出token,RL在token空间做决策;比如游戏AI里,世界模型输出token,RL在token空间做动作选择。核心逻辑是一样的:让大模型做它擅长的事,让RL做它擅长的事,中间用一个紧凑的表示层连接起来。
最后再分享一个小技巧:如果你在调RL Token的时候发现训练不稳定,先别急着改算法,先把VLA的输出可视化出来看看。很多时候问题出在VLA特征本身——可能你的输入预处理有问题,可能语言指令没对齐,可能图像分辨率不对。VLA特征质量不行,后面RL再怎么调都是白搭。我踩过好几次这个坑,后来养成了习惯,每换一个VLA模型,先跑一遍特征可视化,确认特征合理了再上RL。这个习惯帮我省了很多时间。