IsaacLab 训练范式探索(一):让机器人拥有“记忆”的 RNN 策略

IsaacLab 训练范式探索(一):让机器人拥有“记忆”的 RNN 策略 在 IsaacLab 基于 RSL-RL 库的强化学习RLDemo 中我们最常接触到的往往是默认的 MLP多层感知机策略范式。然而在探索机器人复杂步态和真实世界部署时除了标准的RslRlPpoActorCriticCfg之外RSL-RL 其实还为我们提供了更为强大的循环策略Recurrent Policy和蒸馏学习Distillation机制。今天作为本系列笔记的第一篇我们就来深入聊聊如何通过 RNN循环神经网络让你的机器人拥有“记忆”。我们将从需求背景出发剖析代码配置深入底层原理最后分享实机部署的干货与避坑指南。一、 需求背景为什么我们需要“记忆”在标准的强化学习设定中我们通常假设环境是一个马尔可夫决策过程MDP。这意味着机器人当前状态State包含了做出最优决策所需的所有信息。如果是基于这种完美假设一个普通的 MLP 网络足以建立从观测Observation到动作Action的映射。然而现实世界是残酷的。真实部署的机器人往往处于部分可观测马尔可夫决策过程POMDP中。举个最直接的例子你无法仅凭某一瞬间的关节位置和 IMU 姿态就准确推断出机器人此时的速度、外部的推力大小或者脚下地面的摩擦力系数。为了解决“信息缺失”的问题传统的做法是在观测项中加入历史帧History Frames比如把过去 5 帧的观测拼接在一起喂给网络让它自己去推导变化率。但这种做法会导致输入维度爆炸且对远期特征的捕捉能力极其有限。这时候RNN循环神经网络就该登场了。RNN 天生具备“记忆”能力它能在内部维护一个隐藏状态Hidden State通过不断吸收当前观测来更新自己对世界整体状态的“内部认知”。有了这种隐式记忆机器人面对复杂地形和外部干扰时表现会更加鲁棒和从容。二、 默认配置回顾基础的 PPO 策略在深入 RNN 之前我们先来回顾一下 Lab 里 Demo 中最常见的超参数配置。相信各位炼丹师对这套基于标准RslRlOnPolicyRunnerCfg和RslRlPpoAlgorithmCfg的配置已经毫不陌生configclassclassG1RoughPPORunnerCfg(RslRlOnPolicyRunnerCfg):num_steps_per_env24max_iterations3000save_interval50experiment_nameg1_rough# 默认的 MLP 策略配置policyRslRlPpoActorCriticCfg(init_noise_std1.0,actor_obs_normalizationFalse,critic_obs_normalizationFalse,actor_hidden_dims[512,256,128],critic_hidden_dims[512,256,128],activationelu,)algorithmRslRlPpoAlgorithmCfg(value_loss_coef1.0,use_clipped_value_lossTrue,clip_param0.2,entropy_coef0.008,num_learning_epochs5,num_mini_batches4,learning_rate1.0e-3,scheduleadaptive,gamma0.99,lam0.95,desired_kl0.01,max_grad_norm1.0,)在上面的代码中网络仅仅是一个纯前馈的 MLP ([512, 256, 128])。每一帧的观测进去每一帧的动作出来它们之间毫无羁绊犹如一个患有“重度失忆症”的特工。三、 策略配置探索一键切换 RNN 范式在source/isaaclab_rl/isaaclab_rl/rsl_rl目录下我们可以看到框架为我们封装了不同的配置类文件主要是rl_cfg.py和distillation_cfg.py。当我们翻阅基础的rl_cfg.py时会发现对于Runner和Algorithm官方并没有提供太多花里胡哨的可选项基本就是沿用标准的RslRlOnPolicyRunnerCfg和RslRlPpoAlgorithmCfg。但是玄机藏在 Policy 的配置上。除了刚才提到的标准RslRlPpoActorCriticCfg之外这里还有一个名为RslRlPpoActorCriticRecurrentCfg的配置类可以直接调用。顾名思义这就是为循环神经网络准备的。要想让你的机器人长出脑子记忆只需在配置中指定rnn_type及其网络维度即可。你可以将 Demo 里的 Policy 部分直接替换为如下结构其他参数原封不动即可开启 RNN 训练范式policyRslRlPpoActorCriticRecurrentCfg(init_noise_std1.0,actor_obs_normalizationFalse,critic_obs_normalizationFalse,actor_hidden_dims[512,256,128],critic_hidden_dims[512,256,128],activationelu,# # 以下为 RNN 专属附加配置# rnn_typelstm,# 循环网络类型通常为 lstm 或 grurnn_hidden_dim256,# 隐藏层维度rnn_num_layers2,# 循环网络的层数)就这么简单框架在底层已经为你处理好了所有繁杂的数据流。四、 深度解析到底什么是 RNN虽然配置很简单但作为专业工程师我们必须做到“知其然更知其所以然”。RNNRecurrent Neural Network循环神经网络与传统前馈神经网络MLP/CNN最大的区别在于它的网络节点之间存在环状Recurrent连接。打个直白的物理比方MLP 就像是一个流水线上的质检员看一个零件观测敲一个章动作零件之间毫不相干而 RNN 就像是一个在读连载小说的读者他在读当前这一页当前帧观测的时候脑子里还保留着上一页的剧情记忆Hidden State, 隐藏状态。在数学上RNN 的当前输出不仅取决于当前的输入xtx_txt​还取决于上一个时间步传过来的隐藏状态ht−1h_{t-1}ht−1​。正是这种机制使得 RNN 拥有了处理时间序列数据的天然优势。对于机器人而言这意味着它可以隐式地从时序序列中“感受”到速度、加速度、外力甚至是不可见的摩擦力。五、 LSTM 与 GRU 的恩怨情仇在我们的配置项中rnn_type有两个常客LSTM和GRU。这两者都是为了解决基础 RNN 中臭名昭著的“梯度消失”和“梯度爆炸”问题而诞生的变体。它们各自有什么门道呢1. LSTM (Long Short-Term Memory长短期记忆网络)LSTM 堪称循环神经网络界的老大哥。它设计了一个非常精妙的“细胞状态Cell State,CtC_tCt​”就像一条贯穿整个时间链条的传送带信息可以很容易地在上面无损传递。为了控制信息的增删LSTM 引入了三个“门Gates”遗忘门Forget Gate决定上一时刻的记忆有哪些已经没用了需要丢弃。输入门Input Gate决定当前时刻的新观测中有哪些是有价值的需要被记录到细胞状态中。输出门Output Gate根据更新后的细胞状态决定当前时刻要对外输出什么隐藏状态hth_tht​。在强化学习部署中LSTM 需要同时维护传递两个状态矩阵h_in隐藏状态和c_in细胞状态。2. GRU (Gated Recurrent Unit门控循环单元)GRU 是 LSTM 的年轻后辈主打一个“轻量化与高效”。研究人员发现 LSTM 虽然强大但三个门加上两个状态向量计算开销有点大。于是 GRU 进行了大刀阔斧的合并状态合并它将细胞状态Cell State和隐藏状态Hidden State合并为了单一的隐藏状态hth_tht​。门控合并它只有两个门——重置门Reset Gate和更新门Update Gate。重置门决定如何将新输入与之前的记忆结合忽略多少过去的记忆。更新门则直接取代了 LSTM 中遗忘门和输入门的作用一揽子决定保留多少旧记忆、吸纳多少新信息。区别与选择从参数量上看GRU 因为少了一个门参数比 LSTM 少约 25%训练速度更快推理时的计算延迟也更低。在很多机器人的强化学习任务中GRU 和 LSTM 的最终表现往往不相上下。但在某些需要捕捉极长时间跨度依赖的任务中LSTM 的表现通常更加稳定。在 RSL-RL 中lstm通常是作为默认和首选的稳妥方案。六、 源码实现机制大揭秘当我们选用RslRlPpoActorCriticRecurrentCfg后RSL-RL 在底层其实是实例化了一个带有Memory模块的网络架构比如ActorCriticRecurrent类。在它的内部机制中环境送来的obs无论 Actor 还是 Critic首先会被送入一个专门处理时序的Memory模块底层的 PyTorchnn.LSTM或nn.GRU。Memory模块在处理当前帧时会自动提取保存在内部的hidden_states进行计算并将输出的特征向量映射到配置的rnn_hidden_dim维度例如 256 维。随后这个 256 维的“浓缩记忆特征”才会被送入后续的 MLP如[512, 256, 128]计算最终输出动作Action或价值评估Value。在 PPO 更新时BPTT需要特别注意的是带有 RNN 的 PPO 训练比普通 PPO 要复杂得多。普通的 PPO 可以把收集到的经验打乱Shuffle后直接塞进网络但 RNN 必须保证时序的连贯性。因此RSL-RL 底层会将数据按照轨迹Trajectory切分成一个个固定长度的序列块使用沿时间反向传播BPTT, Backpropagation Through Time来计算梯度。这也是为什么使用 RNN 往往会稍微增加训练时的显存和时间开销的原因。七、 实机部署指南C 工程师必读当你在仿真中大获成功高高兴兴把模型导出为 ONNX 格式交给下游的 C 部署工程师时他们可能会满头大汗地跑来找你“这模型不对啊怎么要求这么多输入”没错普通的 MLP 模型输入只有obs输出只有actions。但如果你的rnn_typelstm且rnn_num_layers2你的 ONNX 模型图将会发生本质改变。输入节点要求obs当前帧的观测向量。h_in输入的隐藏状态维度通常为[num_layers, batch_size, rnn_hidden_dim]在你的配置下就是[2, 1, 256]。c_in输入的细胞状态维度同上[2, 1, 256]。注如果是 GRU则只有h_in没有c_in。输出节点要求actions网络给出的动作指令。h_out更新后的隐藏状态。c_out更新后的细胞状态。部署逻辑避坑伪代码在 C 的推理循环中必须要妥善管理好这些记忆张量// 1. 初始化阶段必须分配全 0 的内存给 h 和 cfloath_state[2][1][256]{0.0f};floatc_state[2][1][256]{0.0f};// 2. 控制循环while(robot_is_running){floatobs[OBS_DIM]get_sensors();// 准备输入字典inputs{obs:obs,h_in:h_state,c_in:c_state};// 运行 ONNX 推理outputsonnx_session.run(inputs);// 提取动作并发送给底层控制器send_to_motors(outputs[actions]);// 3. 关键步骤用 h_out 和 c_out 覆盖旧的 h_state 和 c_state// 为下一帧的推理做准备h_stateoutputs[h_out];c_stateoutputs[c_out];}// 4. 注意事项如果机器人跌倒重置必须把 h_state 和 c_state 重新清零八、 避坑指南RNN 与“观测历史帧”的取舍这是许多新手在配置环境时最容易踩进去的深坑在引入 RNN 的同时依然在观测组里保留了历史帧History Frames。请各位炼丹师千万注意既然已经是带记忆的循环网络了观测项里就不需要再配置历史帧了在普通 MLP 训练中因为网络没有记忆我们往往需要把ttt,t−1t-1t−1,t−2t-2t−2… 的观测堆叠起来比如 5 帧历史喂给网络这是在人为制造“伪记忆”。但当你切换到 RNN 时网络本身就已经在时间维度上对特征进行了提炼。此时再塞入 5 帧历史观测不仅构成了严重的信息冗余还会直接导致 RNN 输入维度暴增增加模型拟合的难度。实验数据打脸现场经过亲测对比在完全相同的环境中黑色曲线纯当前帧观测无历史帧 RNN 策略。蓝色曲线5 帧观测历史拼接 RNN 策略。我们发现加入历史帧后蓝色线训练前期的收敛速度明显变慢。网络需要在庞杂且冗余的数据中苦苦搜寻真正的有效特征导致样本效率下降。虽然最终经过漫长的训练综合奖励和无历史项基本持平但浪费了大量算力。更致命的是在实机部署测试中由于加入了历史序列模型对传感器的噪声累积和通信延迟变得更加敏感部署效果略差于无历史帧的纯净版本。一句话总结用 RNN请务必去掉环境观测中的history_length配置让 RNN 做它该做的事。九、 总结与预告经过大量综合测试与实机验证在相同的训练环境配置和难度下我们明确发现RNN 范式循环策略的整体表现是显著优于普通 PPOMLP配置的。它展现出了更强大的地形适应性、更丝滑的步态过渡以及在面对未建模外部干扰时惊人的鲁棒性。当然RNN 也带来了模型体积变大、推理有极小延迟增加、部署逻辑变复杂等轻微副作用但对于四足/双足机器人这种高度非线性的动态系统而言这点代价绝对是物超所值的。