1. 从VLA到在线强化学习:为什么需要RL Token
视觉-语言-动作模型(Vision-Language-Action,简称VLA)这两年在机器人操作领域的热度不用我多说,从RT-2到OpenVLA再到各种开源复现版本,大家基本形成了一个共识:把视觉感知、语言指令和动作输出统一到一个大模型里,泛化能力确实比传统模仿学习强不少。但真正上手做过VLA微调的人都知道,模仿学习有个绕不开的天花板——它只能学到示范数据里出现过的行为分布,一旦遇到训练集没覆盖的状态,模型的表现就会断崖式下跌。
在线强化学习(Online RL)本来是解决这个问题的正解:让机器人在真实环境里自己试错,用奖励信号去引导策略改进。但直接把在线RL套到VLA上,问题立刻就来了。VLA的参数量动辄几十亿,动作空间是连续的高维向量,用传统的actor-critic框架去训练,光是critic网络的估计方差就足以让整个训练过程崩掉。更别说在线采样本身的成本——机器人每走一步都要真实执行,样本效率低得让人心疼。
RL Token这个思路,本质上是在VLA和在线RL之间架了一层"适配器"。它不去动VLA的主干权重,而是把VLA中间层的特征拿出来,压缩成一个紧凑的token表示,然后在这个token空间上做actor-critic的在线优化。这样做的好处很直接:一方面保留了VLA预训练学到的视觉-语言对齐能力,另一方面把RL的搜索空间从几十亿参数降到了几百万甚至几十万参数的级别,训练稳定性和样本效率都能兼顾。
我第一次看到这个方向的时候,第一反应是"这不就是feature-based RL的老套路吗"。但仔细想过之后发现,关键差异在于VLA的特征本身已经包含了丰富的语义和空间信息,比传统CNN特征强太多,而且token化的表示天然适合和Transformer架构对接。所以RL Token不是简单的特征复用,而是一种有明确设计意图的架构选择。
这篇文章我会从设计思路、核心细节、实操流程到问题排查,把RL Token引导在线RL的完整链路拆开讲一遍。适合已经跑通过VLA推理、想进一步做在线微调的读者,也适合对actor-critic有基础但没在VLA上实践过的朋友。代码层面我会给出关键模块的实现参考,参数选择会解释清楚背后的计算逻辑,踩过的坑也会如实交代。
2. RL Token的整体设计与方案选型
2.1 为什么不在VLA主干上直接做RL
先说清楚一个常见误区:很多人觉得既然VLA已经是个大模型了,直接把它当actor,输出动作分布,然后接一个critic不就行了。理论上没错,但实操上几乎不可行,原因有三个。
第一是梯度传播路径太长。VLA主干通常有几十层Transformer,动作头的梯度要回传到最底层的视觉编码器,中间经过注意力层、FFN层、归一化层,梯度消失和爆炸的风险极高。即使加了gradient checkpointing,训练速度也会慢到无法接受。
第二是critic网络的匹配问题。在线RL里critic要估计状态价值,如果状态表示就是VLA的隐藏层输出,维度可能是4096甚至更高,critic本身也得是个大网络,训练难度和actor不相上下。而且VLA的特征是预训练出来的,不一定适合价值估计,强行用会导致critic收敛极慢。
第三是灾难性遗忘。在线RL的奖励信号往往很稀疏,如果直接更新VLA主干,很容易把预训练学到的通用能力覆盖掉,最后模型在新任务上过拟合,在老任务上完全失效。
RL Token的设计正是针对这三点:把特征抽取和价值估计解耦,让RL只在一个低维、专门优化的空间里进行。
2.2 RL Token的核心结构拆解
RL Token的完整链路可以分成四个模块,我用一个表格先做个总览,后面逐个展开。
| 模块 | 输入 | 输出 | 参数量级 | 是否训练 |
|---|---|---|---|---|
| VLA特征提取器 | 图像+语言指令 | 中间层特征 | 数十亿 | 冻结 |
| Token压缩器 | VLA中间特征 | RL Token (d=256) | 约1M | 训练 |
| Actor头 | RL Token | 动作分布 | 约0.5M | 训练 |
| Critic头 | RL Token | 状态价值 | 约0.5M | 训练 |
VLA特征提取器保持冻结,这是整个方案能跑起来的前提。Token压缩器通常是一个小的Transformer encoder或者MLP,把VLA某一层(一般是倒数第二层或倒数第三层)的输出投影到固定维度的token空间。Actor和Critic都是轻量级MLP,输入是RL Token,输出分别是动作分布的参数(均值和方差)以及标量价值。
这里有个设计细节值得说:Token压缩器不是简单地把特征降维,而是要做时序聚合。因为机器人操作是序列决策问题,单帧特征不足以表达动态信息。常见做法是把最近K帧的VLA特征拼起来,用一个小的注意力模块做聚合,输出一个综合的RL Token。K一般取4到8,太小了信息不够,太大了计算量上去了而且冗余。
2.3 在线RL算法的选择逻辑
RL Token空间上跑什么算法,这个选择直接影响训练稳定性。我试过三种方案,对比如下。
PPO(Proximal Policy Optimization)是最稳的选择。它的clip机制天然适合小批量在线更新,对超参不敏感,在RL Token这种低维空间上收敛很快。缺点是样本效率一般,需要比较多的环境交互。
SAC(Soft Actor-Critic)样本效率高,适合真实机器人场景,因为真实采样成本高。但SAC对critic的估计精度要求高,如果RL Token的表示质量不够好,容易发散。我一般会先用PPO跑通流程,再切SAC做精细调优。
TD3(Twin Delayed DDPG)在连续动作空间上表现不错,但确定性策略在探索方面弱一些,对于需要多样探索的操作任务不太合适。
综合下来,我的推荐是PPO起步,SAC进阶。PPO的clip范围设0.2,GAE的lambda设0.95,这些是经过大量实践验证的默认值,先不要动。等流程跑通了,再根据具体任务调整。
2.4 奖励设计与信号整形
在线RL能不能work,奖励设计占一半功劳。VLA场景下的奖励通常来自任务成功判定,比如抓取成功给1,失败给0,这是稀疏奖励,直接拿来做RL基本学不动。
我的做法是分三层整形。第一层是稠密塑形奖励,比如末端执行器到目标点的距离负值,这个信号连续且容易计算。第二层是阶段奖励,把任务拆成接近、对准、抓取、放置几个阶段,每完成一个阶段给一个中间奖励。第三层才是稀疏成功奖励,只在任务真正完成时给。
三层奖励的权重需要调,我的经验是塑形奖励权重0.1,阶段奖励权重0.3,成功奖励权重1.0。这个比例不是固定的,如果发现策略一直在原地打转,说明塑形奖励太强了,要降;如果策略迟迟不完成最终目标,说明成功奖励不够突出,要升。
注意:奖励整形有个大坑,就是塑形奖励如果设计不当,策略会学会"刷奖励"而不是真正完成任务。比如距离奖励,如果只给负距离,策略可能学会停在距离目标不远的地方不动,因为再靠近可能触发碰撞惩罚。解决办法是加一个时间惩罚项,每步给一个小的负奖励,逼策略尽快完成。
3. 核心细节解析与实操要点
3.1 VLA特征层的选择与验证
VLA模型有很多层,选哪一层的输出作为RL Token的输入,这个不是拍脑袋决定的。我的做法是做一个线性探测实验:冻结VLA,在每一层的特征上训一个小的线性分类器,预测当前任务阶段或者动作方向,看哪一层的准确率最高。
以OpenVLA为例,我实测下来倒数第三层的特征在动作预测任务上表现最好,准确率比最后一层高约8个百分点。原因是最后一层已经过度特化到预训练任务的动作输出上了,反而丢失了一些通用的空间信息。倒数第三层还保留着比较丰富的中间表示。
选定层之后,还要确定特征维度。VLA的隐藏层维度通常是4096,直接拿来做RL Token太大了。Token压缩器一般会把它降到256或512。我推荐256,因为actor和critic的参数量能控制在百万级别,训练快且不容易过拟合。如果任务特别复杂,可以升到512,但再大就没必要了。
3.2 Token压缩器的实现细节
Token压缩器的结构我试过两种:纯MLP和轻量Transformer。MLP版本简单,两层全连接加LayerNorm加GELU,参数量小,训练快。Transformer版本多了一个自注意力层,能更好地做时序聚合,但参数量翻倍。
我的建议是:如果K(时序窗口)小于等于4,用MLP就够了;如果K大于4,用Transformer。因为窗口大了之后,简单的拼接+MLP很难捕捉帧间的依赖关系。
代码层面,MLP版本的压缩器大概长这样:
import torch import torch.nn as nn class MLPTokenCompressor(nn.Module): def __init__(self, input_dim=4096, token_dim=256, window_size=4): super().__init__() self.window_size = window_size self.proj = nn.Sequential( nn.Linear(input_dim * window_size, 1024), nn.LayerNorm(1024), nn.GELU(), nn.Linear(1024, token_dim), nn.LayerNorm(token_dim) ) def forward(self, features): # features: (batch, window_size, input_dim) batch = features.shape[0] flat = features.reshape(batch, -1) return self.proj(flat)这里有个细节:LayerNorm放在最后很重要,因为RL Token要送进actor和critic,如果分布不稳定,价值估计会抖得厉害。我一开始没加最后的LayerNorm,critic的loss震荡幅度是加了之后的3倍多。
3.3 Actor与Critic的网络设计
Actor输出的是连续动作的均值和标准差。均值用tanh激活限制在[-1, 1],然后根据实际动作范围做缩放。标准差用softplus激活保证正值,再加一个最小值裁剪防止方差塌缩。
class Actor(nn.Module): def __init__(self, token_dim=256, action_dim=7): super().__init__() self.shared = nn.Sequential( nn.Linear(token_dim, 256), nn.GELU(), nn.Linear(256, 256), nn.GELU() ) self.mean_head = nn.Linear(256, action_dim) self.log_std_head = nn.Linear(256, action_dim) def forward(self, token): h = self.shared(token) mean = torch.tanh(self.mean_head(h)) log_std = torch.clamp(self.log_std_head(h), -5, 2) return mean, log_stdCritic的结构类似,但输出是标量。我一般会用双Q网络(Twin Critic),取两个Q值的最小值作为估计,这样能缓解过估计问题。虽然PPO本身对Q值估计的依赖不如SAC那么强,但双Q网络在实操中确实更稳。
实操心得:actor和critic的共享层不要太多。我试过让它们共享前两层,结果actor的策略更新会干扰critic的价值估计,训练曲线明显更抖。后来改成完全独立的两个网络,虽然参数量多了一点,但稳定性提升很明显。
3.4 训练循环与数据流
完整的训练循环是这样的:机器人执行当前策略,采集一条轨迹,把每步的VLA特征存下来,然后用Token压缩器转成RL Token,送进actor和critic做更新。这里的关键是特征缓存,因为VLA推理很慢,如果每次更新都重新跑VLA,训练速度会慢到无法接受。
我的做法是采集阶段就把VLA特征存到replay buffer里,更新阶段直接用缓存的特征。这样VLA只在采样时跑一次,更新时完全不碰。代价是buffer占用会大一些,4096维的特征存10万条大概是1.5GB,可以接受。
训练循环的伪代码:
for iteration in range(num_iterations): # 采样阶段 trajectories = [] for _ in range(num_envs): obs = env.reset() traj = [] for step in range(max_steps): vla_feat = vla.extract_features(obs) # 冻结,不计算梯度 token = compressor(vla_feat) action = actor.sample(token) next_obs, reward, done = env.step(action) traj.append((vla_feat, action, reward, done)) obs = next_obs trajectories.append(traj) # 更新阶段 buffer.add(trajectories) for epoch in range(update_epochs): batch = buffer.sample(batch_size) tokens = compressor(batch.features) loss = ppo_loss(actor, critic, tokens, batch) optimizer.zero_grad() loss.backward() optimizer.step()注意VLA特征提取那一步要加torch.no_grad(),不然显存直接爆掉。
4. 实操过程与核心环节实现
4.1 环境搭建与依赖版本
我用的环境是Python 3.10,PyTorch 2.1,CUDA 12.1。VLA部分用的是OpenVLA的7B版本,跑在一张A100 80G上。Token压缩器和actor-critic都很小,显存占用可以忽略。
依赖清单:
pip install torch==2.1.0 torchvision==0.16.0 pip install transformers==4.38.0 pip install gymnasium==0.29.1 pip install numpy==1.24.0 pip install wandb # 用于训练曲线记录有个坑要提前说:OpenVLA的默认加载方式是float16,但特征提取的时候如果也用float16,数值稳定性会有问题。我的做法是VLA推理用float16加速,但特征输出转成float32再送进压缩器。这样既保证了速度,又保证了RL训练的数值精度。
4.2 VLA特征提取的工程优化
VLA推理是整条链路里最慢的环节,7B模型单次前向大概要200ms。如果控制频率要求10Hz,这个延迟是不可接受的。我的优化方案是异步推理:用一个单独的进程跑VLA,主进程只管控制和RL更新,两者通过共享内存通信。
具体实现上,VLA进程持续接收最新的观测,输出特征写到共享buffer;控制进程从buffer读最新特征,如果特征还没更新就用上一帧的。这样控制频率能跑到50Hz以上,代价是特征有最多一帧的延迟。对于大多数操作任务,这个延迟是可以接受的。
另一个优化是特征降维缓存。4096维的特征存起来太占空间,我会在存储前先用一个固定的随机投影矩阵降到1024维,用的时候再投影回4096。随机投影保持距离的性质(Johnson-Lindenstrauss引理),对RL Token的质量影响很小,但存储能省75%。
4.3 奖励函数的代码实现
奖励函数我封装成了一个类,方便不同任务复用:
class TaskReward: def __init__(self, target_pos, stage_thresholds): self.target_pos = target_pos self.stage_thresholds = stage_thresholds self.current_stage = 0 def compute(self, ee_pos, gripper_state, object_pos, success): reward = 0.0 # 塑形奖励:末端到目标的距离 dist = np.linalg.norm(ee_pos - self.target_pos) reward += 0.1 * (-dist) # 阶段奖励 if self.current_stage == 0 and dist < self.stage_thresholds[0]: reward += 0.3 self.current_stage = 1 elif self.current_stage == 1 and gripper_state > 0.5: reward += 0.3 self.current_stage = 2 # 成功奖励 if success: reward += 1.0 # 时间惩罚 reward -= 0.01 return reward阶段阈值的设定要根据实际任务调。我一般会先跑几次人工演示,记录下每个阶段的距离分布,取中位数作为阈值。
4.4 PPO更新的关键参数
PPO的超参我调过很多轮,下面这套是在VLA+RL Token场景下比较稳的配置:
| 参数 | 值 | 说明 |
|---|---|---|
| clip_range | 0.2 | 策略更新幅度限制 |
| gamma | 0.99 | 折扣因子 |
| gae_lambda | 0.95 | GAE优势估计的平滑系数 |
| learning_rate | 3e-4 | actor和critic共用 |
| batch_size | 256 | 每次更新的样本数 |
| update_epochs | 10 | 每批数据的复用次数 |
| entropy_coef | 0.01 | 熵正则系数 |
| value_coef | 0.5 | 价值损失的权重 |
gamma设0.99是因为操作任务通常有几十步的horizon,折扣太狠会导致远期奖励被忽略。entropy_coef设0.01是为了保持一定的探索性,太小了策略会过早收敛到局部最优,太大了动作会太随机。
注意:learning_rate不要设太大。我试过1e-3,结果actor的KL散度经常超过clip范围,训练极不稳定。3e-4是经过验证的安全值,如果发现收敛太慢,可以降到1e-4再试。
4.5 训练监控与日志
训练过程中我重点看四个指标:策略熵、价值损失、KL散度、平均回报。
策略熵应该缓慢下降,如果下降太快说明探索不足,要加entropy_coef;如果一直不降说明策略没学到东西,要检查奖励设计。价值损失应该震荡下降,如果一直很大说明critic拟合不好,可能是RL Token的表示质量不够。KL散度要控制在clip_range以内,超过说明学习率太大。平均回报是最直接的指标,但要注意看的是滑动平均,单次回报波动很大。
我用wandb记录这些曲线,每1000步打一次日志。如果发现异常,能及时回滚到之前的checkpoint。
5. 常见问题与排查技巧实录
5.1 训练不收敛的排查路径
训练不收敛是最常见的问题,我整理了一个排查顺序,按这个顺序走基本能定位到原因。
| 排查项 | 检查方法 | 常见问题 | 解决方式 |
|---|---|---|---|
| 奖励信号 | 打印每步奖励 | 奖励全为0或全为1 | 调整奖励整形权重 |
| RL Token质量 | 线性探测准确率 | 准确率接近随机 | 换VLA特征层 |
| 动作范围 | 打印动作统计 | 动作饱和在边界 | 调整动作缩放系数 |
| 学习率 | 看KL散度 | KL持续超clip | 降低学习率 |
| Critic拟合 | 看价值损失 | 损失不下降 | 增大critic网络或降token维度 |
| 数据分布 | 看状态覆盖 | 状态集中 | 增加探索噪声 |
我遇到最多的是奖励信号问题。有一次任务成功奖励设成了1,但塑形奖励的系数设成了1,结果策略学会了在目标附近来回晃悠刷塑形奖励,根本不完成最终任务。后来把塑形奖励降到0.1,问题就解决了。
5.2 RL Token表示坍塌的处理
表示坍塌是指Token压缩器输出的token在训练过程中逐渐变得相似,失去了区分不同状态的能力。表现是critic的价值估计对所有状态都给出差不多的值,策略梯度信号消失。
检测方法是计算一个batch内token的方差,如果方差持续下降,就是坍塌了。解决办法有两个:一是加一个对比损失,拉大不同状态token之间的距离;二是降低压缩器的学习率,让它不要更新太快。
我一般用第一种,对比损失的实现是在batch内随机采样正负对,正对是时间上相邻的token,负对是随机采的token,用InfoNCE损失训练。加了之后token方差能稳定在一个合理范围。
5.3 真实机器人上的延迟问题
仿真环境里跑通了不代表真机上能用。真机上最大的问题是推理延迟和控制频率不匹配。VLA推理200ms,控制周期20ms,差了10倍。
我的解决方案是动作分块(Action Chunking):VLA一次推理输出未来8步的动作序列,控制进程按顺序执行,执行完了再请求下一次推理。这样VLA推理频率降到1.25Hz,控制频率还是50Hz。动作分块还有个好处是动作更平滑,因为序列内的动作是一起生成的,天然有连续性。
但动作分块和在线RL结合有个矛盾:RL更新需要每步的奖励和状态,如果一次执行8步,中间的状态就采不到了。我的做法是执行分块动作的同时,仍然每步记录状态和奖励,更新的时候把分块内的步骤展开。这样既保证了控制频率,又保证了RL的数据密度。
5.4 灾难性遗忘的缓解
虽然RL Token方案冻结了VLA主干,理论上不会遗忘,但Token压缩器和actor-critic是在线更新的,如果任务分布偏移太大,压缩器可能会学偏,导致VLA的通用特征被扭曲。
缓解方法是加一个正则项,约束压缩器的输出不要偏离初始值太远。具体做法是保存一份压缩器的初始权重,每次更新时加一个L2正则,权重系数设0.01。这样压缩器能适应新任务,但不会跑得太偏。
另一个做法是经验回放,把之前任务的数据留一部分在buffer里,更新时混合采样。这样能保持策略在旧任务上的性能。我一般会保留20%的旧数据,效果比较明显。
5.5 超参调整的经验法则
最后分享几条超参调整的经验,都是踩坑踩出来的。
学习率:actor和critic可以用不同的学习率,critic可以稍大一点,因为它拟合的是标量,任务更简单。我一般actor用3e-4,critic用1e-3。
batch_size:不要太小,256是下限。太小了梯度方差大,训练抖。如果显存够,可以上512。
update_epochs:10是安全值,不要超过20。太多了会过拟合当前batch的数据,策略更新过头。
entropy_coef:从0.01开始调,如果策略太随机就降到0.005,如果太早收敛就升到0.02。
window_size:时序窗口K从4开始试,如果任务动态性强(比如需要预测物体运动),可以升到8。
这些值不是绝对的,不同任务需要微调,但作为起点能省很多试错时间。
6. 扩展方向与个人实践体会
RL Token这套框架跑通之后,我陆续试了几个扩展方向,有些效果不错,有些还在探索。
多任务共享Token空间:如果多个任务的VLA特征分布接近,可以共享一个Token压缩器,只是actor和critic分开。这样压缩器能看到更多数据,表示质量更高。我试过在三个抓取任务上共享,压缩器的收敛速度比单任务快了约40%。
离线预训练+在线微调:先用离线数据训一个初始的actor-critic,再上线做在线微调。这样在线阶段只需要少量交互就能达到不错的效果。离线数据可以用人工演示或者之前的策略采集,关键是覆盖足够多的状态。
Token空间的可解释性分析:我做过一个实验,把RL Token做PCA降到2维可视化,发现不同任务阶段的token确实聚成了不同的簇。这说明Token压缩器学到了有意义的状态表示,不是单纯的降维。这个分析对调试很有帮助,如果token可视化是一团乱麻,说明压缩器没学好。
和扩散策略的结合:最近在试把actor换成扩散模型,在Token空间上做去噪生成动作。初步结果是动作更平滑,但训练慢了不少。这个方向还在探索,等有稳定结论再分享。
我个人在实际操作中的体会是,RL Token这套方案最大的价值不是某个具体的技术点,而是它提供了一种分层解耦的思路:预训练的通用能力冻结住,任务特定的优化放在小模块上做。这个思路不仅适用于VLA+RL,其他大模型+在线优化的场景也能借鉴。关键是要找到合适的中间表示层,既保留足够的语义信息,又足够紧凑以便高效优化。
最后再分享一个小技巧:训练初期先用一个固定的随机策略采集一批数据,把critic预热一下,再开始正式的actor-critic更新。这样能避免critic在初期给出完全错误的估计,导致actor被带偏。预热数据量不用多,几千步就够,但效果很明显,训练曲线会平滑很多。