0x00 概要
SERL 的核心使命是:在真实世界中,让机器人在 20-40 分钟内学会高精度的机械操作。它通过集成 SAC、RLPD、DrQ 和 VICE,将原本需要数百万次尝试的 RL,压缩到了人类演示水平的量级。
RLPD(Reinforcement Learning with Prior Data)是一种基于 off-policy 的 actor-critic强化学习算法,借鉴了 soft-actor-critic 等时序差分算法的成功经验,但为满足上述需求做出了一些关键修改,它其实就是 SAC + 之前的数据(Prior Data)+ 极高的更新频率(High UTD)。
注:
- 本系列的最终目标是“通过一系列相关项目/算法的解读,来深入学习/分析/反推 LWD(Learning while Deploying)这篇论文的机理和可能实现”。之所以从SERL入手,是因为 SERL,HIL-SERL,SOP(没有开源)都是罗剑岚博士的一系列论文,可以从中管窥作者的思路脉络。
- 本文依然是从工程/论文进行反推,还请读者不吝指出问题,多谢。
0x01 基础 & 背景
1.1 总体流程图
SERL 的总体流程图如下,其中:
- SAC(Soft Actor-Critic)是算法底座 ,是整个系统的"引擎"。
- RLPD (RL with Prior Data) 是性能加速器,是 SERL 实现"20 分钟学会"的关键,它通过"暴力更新"和"不忘初心"来榨取每一条数据的价值。
- High UTD:迫使模型对有限的样本进行 "深度研读",从而在极短的物理时间内捕捉到成功的信号。
1.2 面临的问题
在许多场景中,强化学习的优异表现依赖于与环境进行大量的在线交互,这通常通过使用模拟器来实现。然而,在实际问题中,常常面临样本获取成本高昂的情况。此外,奖励信号稀疏,且高维的状态和动作空间往往使这一问题更加严重。
一些先前的研究致力于通过预训练利用这些数据,而其他方法则在在线训练时引入约束,以应对分布转移问题。然而,每种方法都有其缺点,例如需要额外的训练时间和超参数,或者在行为策略之外的提升有限。
0x02 RLPD 基础
RLPD关注的是,是否可以在 在线学习时,直接应用现有的离策略方法以充分利用离线数据。在每一步训练中,RLPD 在先验(离线)数据和on-policy数据之间等概率采样,以形成一个训练批次,即“对称采样”,即每个批次有50%的数据来自(在线)回放缓冲区,另外50%来自离线数据缓冲区(先验数据)。
我们对比如下:
原生:纯靠自己试
SERL:在 SAC 的 Batch 里塞进人类演示数据,相当于给 SAC 考试时递了一张带有参考答案的纸,让它不用从头瞎猜
那"RLPD"这四个字母贵在哪里?秘密不在"怎么算"(Loss Function),而在"喂什么"(Batch Composition)。RLPD 50/50 抽样 强行保证每个 Batch 都有 128 条 Demo。这相当于给机器人装了一个"强制记忆模块",让它每一秒钟都在看正确答案。
2.1 RLPD 的三大支柱
- High UTD(Update-to-Data):
- SERL 每跑一步环境,就进行 20 次网络更新。这种"暴力刷题"让每一条物理数据都被反复压榨,是 20 分钟收敛的硬件级保证。
- Layer Normalization(LN):
- 问题:在普通的 SAC 中, 网络通常就是纯 MLP。但在 RLPD 中, 由于我们要做 High UTD (高频更新), Q 值的估算非常容易发散。
- 解决方案:因此,RLPD 会在 Critic 网络 (甚至 Actor 网络) 的每一个隐藏层之后, 都加上 LayerNorm。
- 直观理解:高 UTD 会产生巨大的梯度冲击。LN 就给高速赛车装上强力悬挂,确保每一层神经元的输出在 20 倍更新频率下依然稳定,不至于"飞出去"。
- 50/50 混合采样:
- 做法:在 update 过程中, 我们不再只从 replay_buffer 抽数据, 而是将演示数据(Demo)与在线数据(Online)强行等比例混合,确保智能体在探索新坑时,始终能看到正确路标。
- 抽 128 个在线数据 (自己跑出来的)。
- 抽 128 个先验数据 (人类演示的 Demo)。
- 把它们拼成一个 256 的 Batch 进行训练。
- 为什么有效?:演示数据告诉智能体"正确的路长啥样", 在线数据告诉智能体"这里的坑别踩"。两者结合, 收敛速度能提升数倍。
2.2 Prior Data 策略
Prior Data 策略决定了演示数据与在线数据的黄金采样配比。
真机强化学习最怕"冷启动"— 机器人像没头苍蝇一样乱撞。SERL 引入了 RLPD(Reinforcement Learning with Prior Data)机制:
数据混洗:在训练的每一个 Batch 中,系统会强制性地混合:
- 50% 的演示数据:这是由人类老师录制好的"标准答案"
- 50% 的在线数据:这是机器人自己折腾出来的"实战记录"
算法价值:这种混合采样确保了模型在进化的每一秒,都在不断对比"正确答案"与"自己的尝试"。它解决了强化学习初期的探索困境,让机器人即使在完全没拿高分的情况下,也能通过模仿专家数据来迅速建立起任务的初步认知。
我们可以这样理解:
- prior data 像"老师给的标准答案"
- online replay 像"学生自己的练习记录"
- 每次训练都同时看标准答案和自己的错题,策略就不会偏离任务太远
这也是 SERL 样本效率高的关键之一。它不是让机器人从零开始乱试,而是在 demonstrations 的引导下进行强化学习微调。论文中明确写到,每次更新使用 sample-based approximation,其中 half of the samples drawn from prior data,half drawn from replay buffer。
2.3 算法流程图
下面是根据 rlpd.py 的代码逻辑整理的 RLPD 训练流程逻辑图。这个图展示了从数据采样到网络更新的完整路径, 特别标注了 RLPD 相对于普通 SAC 的核心改进点 (如 BC Loss 和 Pessimistic Backup)。
2.4 代码细节
RLPD 关键组件说明
- Ensemble Q (10 Qs): 使用 10 个 Q 网络而非 2 个, 增加评估的多样性。
- rho (Pessimism): 通过 "均值 - rho * 标准差" 来实现对不确定区域的惩罚。
- BC Loss: 在 Actor 更新中加入行为克隆, 强迫智能体初始阶段不要偏离演示数据。
- High UTD (Scan): JAX 的
lax.scan允许在一个硬件循环内执行 20 次上述流程。 - LayerNorm (代码内部) 在所有隐藏层后强制执行归一化, 支撑高频更新。
结合 rlpd.py 代码的深度解读:
关于 rho 的计算:
next_qs = self.network.select('target_critic')(batch['next_observations'][..., -1, :], next_actions) next_q = next_qs.mean(axis=0) - next_qs.std(axis=0) * self.config["rho"]这是 RLPD 的精髓。普通的 SAC 是 min(Q1, Q2), 而这里是用标准差来量化"不确定性"。如果 10 个 Q 网络对某个状态动作意见不统一 (std 大), next_q 就会被压得很低。
关于 BC Loss:
bc_loss = -(dist.log_prob(jnp.clip(batch_actions, -1 + 1e-5, 1 - 1e-5)) * batch["valid"][..., -1]).mean() * self.config["bc_alpha"]这行代码在告诉 Actor: "不管 Q 值怎么说, 你输出的动作最好和 Buffer 里的真实动作 (演示数据) 接近一些"。这对机器人任务极其关键, 因为它防止了机器人在训练初期因为乱甩而撞坏硬件。
关于 High UTD:
@jax.jit def batch_update(self, batch): agent, infos = jax.lax.scan(self._update, self, batch)这里使用了 JAX 的 scan 原语。这比 Python 的 for 循环快得多, 它能把 20 次更新编译成一个高效的 GPU 算子。
演示数据(Prior Data):
SERL 所基于的 RLPD 算法中,作者发现最简单、最有效的办法就是一视同仁,比如:- 从 Replay Buffer(自己跑的数据)取 128 个。
- 从 Demo Buffer(演示数据)取 128 个。
- 凑成一个 256 的 Batch,直接丢进 Loss 函数。
- 意义:不需要复杂的权重计算,这种简单的"对半开"采样就能极大地提升效率。
"看未来"与"看现在"的逻辑
rlpd.py 中计算 next_q 用的是 target_critic, 而计算 actor_loss 时用的是 critic (当前网络)。
Target Critic (看未来): 用于计算
r + γ Q_{target}。由于 Q_{target} 更新得很慢 (Soft Update), 它提供了一个稳定的地基, 防止 Q 值计算产生正反馈螺旋 (即自己把自己估高)。Critic (看现在): 用于 Actor 的更新。Actor 问: "我现在的动作好吗? "。由于 Critic 正在被最快地训练, 它能给 Actor 提供最及时的反馈。
矛盾解决: 这就是"评估要稳 (Target), 改进要快 (Current)"的权衡。
0x03 BC
在 SERL 的复现中, 通常的步骤是:
- 收集 20 个 Demo。
- 跑 bc.py 进行预训练, 让机器人学会"手往哪放"。
- 跑 rlpd.py 进行正式训练, 利用 BC 训练好的模型作为起点, 通过 50/50 采样快速进化。
在 SERL 的完整流程中,bc.py 的代码非常关键, 它揭示了 SERL 系统中 Behavioral Cloning (BC) 环节是如何运作的。
3.1 BCAgent
BCAgent 负责预训练冷启动,是纯监督学习的行为克隆实现,架构最为简洁。SERL先用 BC 模仿 Demo,让机器人学会"手往哪放",再开启 RL 寻找"怎么抓取"。
3.1.1 BCAgent 在SERL中的作用
- 冷启动:为RL算法提供初始策略
- 演示数据利用:从专家演示中学习
- 安全基线:在RL训练初期提供安全策略
核心优势:BCAgent 的简洁性使其成为从演示到强化学习的理想桥梁,通过监督学习快速获得可用的策略,然后可以在此基础上进行RL微调。
BCAgent 的特性如下:
| 特性 | BCAgent | 说明 |
|---|---|---|
| 网络数量 | 仅1个Policy网络 | 无Critic,无Temperature |
| tanh_squash | False | 不使用tanh压缩 |
| 输出分布 | MultivariateNormalDiag | 标准高斯分布 |
| 训练目标 | 最小化MSE + 负对数似然 | 监督学习 |
3.1.2 BCAgent 核心组件
唯一的 Policy 网络
network_kwargs["activate_final"] = True networks = { "actor": Policy( encoder_def, # 视觉编码器 MLP(**network_kwargs), # 默认 [256, 256] action_dim=actions.shape[-1], tanh_squash_distribution=False, # 关键差异 ) }| 组件 | 输入 | 网络结构 | 输出 | 特点 |
|---|---|---|---|---|
| Policy | 图像观测 | 编码器+MLP[256,256] | 动作分布(μ,σ) | 纯监督学习 |
3.1.3 BCAgent 编码器架构
"small" 编码器:
encoders = { image_key: SmallEncoder( features=(32, 64, 128, 256), kernel_sizes=(3, 3, 3, 3), strides=(2, 2, 2, 2), padding="VALID", pool_method="avg", bottleneck_dim=256, spatial_block_size=8, ) }"resnet" 编码器:
encoders = { image_key: resnetv1_configs["resnetv1-10"]( pooling_method="spatial_learned_embeddings", num_spatial_blocks=8, bottleneck_dim=256, ) }"resnet-pretrained" 编码器:
pretrained_encoder = resnetv1_configs["resnetv1-10-frozen"]( pre_pooling=True, ) encoders = { image_key: PreTrainedResNetEncoder( pooling_method="spatial_learned_embeddings", num_spatial_blocks=8, bottleneck_dim=256, pretrained_encoder=pretrained_encoder, ) }3.1.4 BCAgent 损失函数
def loss_fn(params, rng): # 前向传播 dist = self.state.apply_fn( {"params": params}, batch["observations"], temperature=1.0, train=True, rngs={"dropout": key}, name="actor", ) pi_actions = dist.mode() # 预测动作 log_probs = dist.log_prob(batch["actions"]) # 对数概率 # 多重损失 mse = ((pi_actions - batch["actions"]) ** 2).sum(-1) # MSE损失 actor_loss = -(log_probs).mean() # 负对数似然 return actor_loss, { "actor_loss": actor_loss, "mse": mse.mean(), }3.1.5 BCAgent 动作采样
def sample_actions(self, observations, seed=None, temperature=1.0, argmax=False): dist = self.state.apply_fn( {"params": self.state.params}, observations, temperature=temperature, name="actor", ) if argmax: actions = dist.mode() # 确定性采样 else: actions = dist.sample(seed=seed) # 随机采样 return actions3.2 bc.py 流程图
BC Agent (模仿学习) 核心流程图如下。BC 关键组件说明:
- ResNet-10: SERL 的默认视觉骨架,用于从原始像素中提取物理特征。
- Random Crop: 极其重要的 Trick,通过对画面进行 ±4 像素的裁剪来模拟环境扰动。
- TanhNormal: 动作分布模型,确保输出的动作符合机械臂的物理范围。
- Pre-training: BC 在 SERL 中扮演“冷启动”的角色,将 RL 的搜索空间缩减到目标附近。
3.3 bc.py 技术细节
核心逻辑: update 函数
- 不仅仅是回归: 虽然它计算了 mse, 但实际更新用的是 actor_loss = -log_probs.mean()。这是一种概率视角下的模仿: 让策略在演示数据给出的状态下, 输出演示动作的概率尽可能大。
- 没有 Critic:这里完全没有 Q 网络。BC 纯粹是"看着答案抄答案", 不需要奖励信号。
视觉处理: 数据增强 (Data Augmentation) data_augmentation_fn:
- 这是 DrQ-v2 风格的随机裁剪。
- 为什么重要?: 在机器人任务中, 摄像头画面可能会有轻微抖动。通过对演示图片进行随机裁剪, 可以让策略学会"忽略"这种位移, 提高鲁棒性。这也是 SERL 能在 20 分钟内学会任务的秘诀之一。
网络架构: ResNet-10
- 在 create 方法中, 它定义了三种编码器。SERL 默认推荐的是 resnet:
elif encoder_type == "resnet": encoders = { image_key: resnetv1_configs["resnetv1-10"](...) }- ResNet-10 比传统的 CNN 效果好得多, 因为它能提取更深层的特征, 同时又不像 ResNet-50 那样运算缓慢。
为什么会有 mse 却不用它更新?
- 在 update 函数的 loss_fn 中, 作者计算了 mse = ((pi_actions - batch["actions"]) ** 2).sum(-1), 但返回值的第一项 (真正的 Loss) 是 actor_loss = -(log_probs).mean()。
- 原因: MSE 只关心均值对不对, 而 log_prob 关心的是整个概率分布。如果人类演示同一个动作时有微小的偏差, log_prob 能更好地捕获这种"容错性"。
EncodingWrapper 的作用
这个包装器能把视觉图像和机械臂自身的状态 (关节角度、末端坐标) 揉在一起。这意味着机器人不仅知道自己"看到了什么", 还知道自己"现在手在哪"。
encoder_def = EncodingWrapper(..., use_proprio=use_proprio, enable_stacking=True, ...)冷启动与热切换:如果复现SERL,一般会把 bc.py 练出来的模型会作为 RLPDAgent.create 时的初始参数 (或权重)。这相当于把原本需要几百万次尝试才能学会的动作, 压缩成了几千步的模仿。
bc_loss 的数据生效为(padding="VALID")。这是因为不能对在线数据做 BC Loss?
- 在线数据 (智能体自己乱跑出来的) 非常"乱"。
- 物理后果: 如果强迫智能体去模仿这些乱七八糟的动作, 就像是让一个正在学走路的孩子去模仿自己摔跤的动作。这会导致策略陷入低水平的循环。
0x04 High UTD & 稳定性机制
High UTD 的意义:它强迫神经网络在极短的时间内"吃透"每一张图片。
4.1 High UTD:把每条真机样本反复研读
4.1.1 理解 UTD(Update-to-Data Ratio)
UTD(Update-to-Data Ratio)表示每采集一条环境数据,算法进行多少次梯度更新。
传统 RL 常用 UTD=1:采一步,训一步。
SERL / RLPD 使用更高 UTD(通常为 20 甚至更高):采集一条昂贵的真机数据后,learner 会多次从 buffer 中采样并更新网络。
4.1.2 为什么 SERL 需要高 UTD?
我们可以把 High UTD 理解成:真机数据太贵,所以每一帧都要反复研读,不能看一遍就扔。
没有 UTD 的后果:普通的 SAC 每采样一个数据才更新一次。对于机器人这种高维度(ResNet 图像)且数据量极小(只有 2.5 小时数据)的任务:
- 收敛太慢:你可能需要练上 10 天半个月。
- 不稳定性:由于视觉特征(ResNet)需要海量更新才能稳定,如果更新频率太低,视觉头会一直处于"模糊"状态,无法提取有效的位姿信息。
4.1.3 极致的采样效率
High UTD 将数据的价值榨取到了极致:
- 20x 的复习强度: 利用 High UTD(Update-to-Data)策略,机器人每在现实中走一步,后台 Learner 就会对现有数据进行 20 到 40 次的高频更新。
- REDQ 保驾护航: 为了防止这种高强度学习产生幻觉,系统利用 10–20 个 Critic 组成 "陪审团(Ensemble)",通过取最小值的方式压制过估计偏差。
- 成果:
- 数据压榨逻辑:真机采集的数据中隐藏着极其细微的物理交互特征(如手爪与工件的摩擦)。通过 High UTD,模型被迫对有限的样本进行"深度研读",从而在极短的物理时间内捕捉到成功的信号。
- 这让原本需要几周的训练过程,被压缩到了喝杯咖啡的时间。
- 工程代价:High UTD 对 Learner 的算力提出了严苛要求。这要求 JAX 必须在毫秒级时间内完成多轮反向传播,以确保学习速度始终领先于采样速度。
4.1.4 实现
@partial(jax.jit, static_argnames=("utd_ratio", "pmap_axis")) def update_high_utd( self, batch: Batch, *, utd_ratio: int, pmap_axis: Optional[str] = None, ) -> Tuple["SACAgent", dict]: """ Fast JITted high-UTD version of `.update`. Splits the batch into minibatches, performs `utd_ratio` critic (and target) updates, and then one actor/temperature update. Batch dimension must be divisible by `utd_ratio`. """ batch_size = batch["rewards"].shape[0] assert ( batch_size % utd_ratio == 0 ), f"Batch size {batch_size} must be divisible by UTD ratio {utd_ratio}" minibatch_size = batch_size // utd_ratio chex.assert_tree_shape_prefix(batch, (batch_size,)) def scan_body(carry: Tuple[SACAgent], data: Tuple[Batch]): (agent,) = carry (minibatch,) = data agent, info = agent.update( minibatch, pmap_axis=pmap_axis, networks_to_update=frozenset({"critic"}) ) return (agent,), info def make_minibatch(data: jnp.ndarray):