1. 从"仿真"到"真机"之间,Microduck 想解决的是什么问题
搞过机器人强化学习的人大概都有过这种体验:在仿真环境里训练出来的策略跑得飞起,奖励曲线一路向上,可视化窗口里的小机器人翻跟头、走平衡木样样精通。可一旦把模型搬到真机上,动作就开始抽搐,要么原地抖动,要么直接趴窝。这个落差不是调参能解决的,它背后是仿真与真实世界之间那条看不见的鸿沟——物理参数不准、传感器噪声、执行器延迟、接触模型简化,每一项都在悄悄吃掉策略的鲁棒性。
Microduck 这个项目,本质上就是围绕"仿真强化学习流程"搭一套可复现、可迁移、可部署的完整链路。它不是一个单点算法,而是一条从环境搭建、策略训练、模型导出到端侧推理的流水线。关键词里出现的 PPO、rsl_rl、ONNX,基本勾勒出了这条链路的技术骨架:用 PPO 做策略优化,用 rsl_rl 这套专为足式机器人设计的强化学习库做训练框架,最后把 PyTorch 训练出来的策略网络导出成 ONNX,方便在推理引擎上跑起来。
为什么这套组合值得单独拿出来讲?因为足式机器人的强化学习跟打游戏、下棋那种纯虚拟任务完全不是一回事。游戏里你输一局重开就行,机器人摔一跤可能就是几千块的维修费。所以整个流程的设计重心,不是"训得多快",而是"训出来的东西能不能安全地落到真机上"。这就决定了 Microduck 这类项目必须在几个地方下功夫:仿真环境的物理保真度、训练时的域随机化、策略网络的轻量化、以及导出部署时的数值一致性。
我见过太多人卡在最后一步——训练好的模型导出 ONNX 之后,推理结果和 PyTorch 里对不上,动作偏差一点点,累积起来就是灾难。这类问题在文档里往往一笔带过,但实际排查起来能耗掉一整天。所以这篇内容我会把重点放在"流程中那些容易被忽略但会要命的细节"上,而不是泛泛地讲 PPO 的原理。适合已经有一定强化学习基础、正在或者准备做仿真到真机迁移的读者,也适合想了解 rsl_rl 和 ONNX 部署链路具体怎么串起来的人。
2. 为什么是 PPO + rsl_rl 这套组合,而不是别的
2.1 PPO 在连续控制任务里的实际优势
PPO(Proximal Policy Optimization)这几年几乎成了机器人强化学习的默认选择,但它并不是唯一选项。SAC、TD3 在样本效率上往往更好,DDPG 更早、更直接。那为什么足式机器人社区普遍押注 PPO?
核心原因在于训练稳定性和并行采样友好度。足式机器人的仿真环境通常跑在 GPU 上,可以同时开几千个并行环境采样。PPO 是 on-policy 算法,虽然样本效率不如 off-policy,但它对经验回放池没有依赖,天然适合大规模并行采样——几千个环境同时跑,采一批数据就更新一次,数据新鲜、分布一致,训练曲线非常平滑。SAC 这类 off-policy 算法需要维护 replay buffer,在几千个并行环境的情况下,buffer 管理和采样优先级的设计会变得很复杂,而且 off-policy 的 bootstrapping 在接触丰富的任务里容易发散。
另一个关键点是 PPO 的 clip 机制。它通过限制新旧策略的比率变化幅度,避免了策略更新过猛导致的崩溃。对于机器人这种"一步错步步错"的任务,这种保守的更新方式反而更安全。我在实际训练四足机器人步态时对比过 PPO 和 SAC,SAC 前期收敛确实快,但到了后期经常出现策略突然退化、机器人开始乱蹬的情况,而 PPO 的曲线虽然爬得慢一点,但很少出现断崖式下跌。
2.2 rsl_rl 到底帮你省了哪些事
rsl_rl 是 ETH 苏黎世那边开源的强化学习库,专门为足式机器人设计。它不是一个通用 RL 框架,而是把机器人训练里那些重复造轮子的部分都封装好了。你如果自己从零写 PPO,需要处理的东西包括:并行环境的数据收集与拼接、优势函数估计(GAE)、策略网络的初始化与正交初始化、观测归一化、学习率调度、checkpoint 保存与恢复。这些每一项都有坑,rsl_rl 把它们都做成了默认配置。
它最值得说的两个设计,一个是观测归一化,一个是actor-critic 网络结构。观测归一化在机器人任务里极其重要,因为观测向量里可能同时包含关节角度(量级 0.1~1)、角速度(量级 1~10)、命令速度(量级 0.1~2),如果不做归一化,网络训练会非常不稳定。rsl_rl 默认用 running mean/std 做在线归一化,并且把归一化参数一起存进 checkpoint,导出时也能保持一致。
网络结构上,rsl_rl 默认的 actor 和 critic 是分开的两套 MLP,隐藏层通常是 [512, 256, 128] 这种逐层减半的结构。这个设计不是随便定的——足式机器人的观测维度通常在 40~60 之间,动作维度在 12 左右(四足 12 个关节),这种规模用三层 MLP 足够表达,再深反而容易过拟合仿真环境。而且 actor 和 critic 分开可以让 critic 用更大的网络、actor 保持轻量,方便后续部署。
2.3 从训练到部署,ONNX 在中间扮演什么角色
训练框架是 PyTorch,但真机上的推理引擎往往不是。机器人主控可能跑的是 TensorRT、ONNX Runtime,甚至是嵌入式 NPU 上的专用推理框架。ONNX 就是中间的通用交换格式。
把 PyTorch 模型导出成 ONNX,表面上是torch.onnx.export一行代码的事,但实际做起来,足式机器人的策略网络导出有几个特有的坑。第一,rsl_rl 的 actor 网络里包含了观测归一化的逻辑,这部分如果不在导出时固化进去,真机推理时就得自己手动做归一化,容易出错。第二,策略网络输出的是动作的均值,实际执行时还需要采样或者加噪声,导出时通常只导出均值部分,把采样逻辑留在外面。第三,PyTorch 和 ONNX 在某些算子上的数值精度有细微差异,尤其是涉及除法、指数运算的地方,累积起来可能导致动作偏差。
我个人的做法是:导出 ONNX 之后,一定要用同一批观测数据,分别在 PyTorch 和 ONNX Runtime 里跑一遍,逐元素对比输出差异。如果最大绝对误差超过 1e-4,就要检查是不是有算子被降精度了。这个验证步骤看起来多余,但它能帮你提前发现 90% 的部署问题。
3. 仿真环境搭建里那些"看起来不重要"的配置
3.1 物理步长与控制频率的匹配
仿真环境里有两个频率概念容易混淆:物理仿真步长(simulation dt)和控制频率(control decimation)。物理步长决定了仿真器多久算一次物理,控制频率决定了策略多久输出一次动作。在 Isaac Gym 或 MuJoCo 里,通常物理步长设成 1/200 秒甚至更小,而策略控制频率设成 50Hz 左右,也就是每 4 个物理步执行一次策略输出。
这个比例不是随便定的。物理步长太小,仿真慢;太大,接触求解不稳定,机器人会穿模或者抖动。控制频率太高,策略来不及响应,而且真机上执行器也跟不上;太低,机器人动作会显得迟钝、不连贯。四足机器人常用的组合是物理步长 1/200、控制频率 50Hz,也就是 decimation=4。这个配置在仿真里跑出来的步态,搬到真机上通常不会太突兀。
注意:如果你在仿真里用了 100Hz 的控制频率,训练出来的策略动作会非常急促,真机执行器带宽不够,表现会差很多。控制频率一定要参考真机执行器的实际响应能力来定。
3.2 域随机化的参数范围怎么定
域随机化是缩小 sim-to-real gap 的核心手段,但随机化范围定得不对,反而会害了你。范围太小,策略没见过真实世界的多样性,迁移失败;范围太大,策略为了适应所有情况变得过于保守,动作软绵绵的,真机上表现平庸。
我的经验是分阶段调整。第一阶段训练基础步态时,随机化范围设小一点,比如地面摩擦系数 0.8~1.2,机器人质量 ±5%,关节 PD 增益 ±10%。这个阶段的目标是让策略先学会走路。第二阶段做鲁棒性训练时,再把范围拉大:摩擦 0.4~1.6,质量 ±15%,增益 ±30%,再加上观测噪声和动作延迟。这样分阶段训练出来的策略,既有基本能力,又有鲁棒性。
还有一个容易被忽略的随机化项是初始状态。如果每次都从完全相同的初始姿态开始训练,策略会过拟合这个初始状态,真机上稍微有点偏差就站不稳。建议在初始关节角度上加 ±0.1 rad 的噪声,初始基座高度也做小幅随机。
3.3 奖励函数设计:别让机器人学会"作弊"
奖励函数设计是强化学习里最像"玄学"的部分,但其实有章可循。足式机器人的奖励通常由几部分组成:跟踪命令速度的奖励、保持直立姿态的奖励、关节力矩惩罚、动作平滑度惩罚、足端接触奖励等。
这里最大的坑是奖励作弊。比如你给"保持基座高度"很高的奖励,机器人可能学会把腿缩起来趴着不动,因为这样基座高度最稳定。你给"足端接触"奖励,机器人可能学会高频抖动腿来骗接触信号。避免作弊的方法,一是奖励项要互相制衡,二是要观察训练出来的步态是否自然。
我习惯在训练中期把策略可视化出来看,如果发现动作很怪异但奖励很高,那基本就是作弊了。这时候要调整奖励权重,或者增加惩罚项。比如加一个"关节速度过大"的惩罚,能有效抑制高频抖动。
4. 训练过程中的监控与调参实战
4.1 该盯哪些指标,不该盯哪些
训练强化学习模型时,很多人只盯着平均奖励曲线,这是不够的。平均奖励高不代表策略好,可能只是某个奖励项被刷高了。我通常会同时监控这几组指标:
| 指标类别 | 具体指标 | 健康表现 |
|---|---|---|
| 奖励分解 | 各奖励项分别的均值 | 没有单项异常突出 |
| 策略统计 | 动作均值、动作标准差 | 均值接近 0,标准差稳定 |
| 价值函数 | explained variance | 逐渐上升到 0.8 以上 |
| 训练稳定性 | KL 散度 | 保持在 0.01~0.02 之间 |
| 物理合理性 | 关节力矩、接触力 | 无异常尖峰 |
其中explained variance是最容易被忽略但最有用的指标。它衡量 critic 网络对回报的预测能力,如果这个值一直上不去(比如低于 0.5),说明 critic 没学好,优势函数估计不准,策略更新方向就是错的。这时候要检查 critic 的学习率是不是太大,或者网络容量不够。
KL 散度反映策略更新的幅度。PPO 的 clip 机制会让 KL 散度保持在一个范围内,如果 KL 突然飙升,说明策略在剧烈变化,通常是要崩的前兆。我遇到过几次 KL 突然从 0.015 跳到 0.1 的情况,紧接着奖励就断崖下跌,只能回滚到上一个 checkpoint。
4.2 学习率和 clip 系数的联动调整
PPO 有两个核心超参:学习率和 clip 系数。这两个参数不是独立的,它们共同决定了每次策略更新的幅度。学习率大、clip 系数大,更新猛,容易崩;学习率小、clip 系数小,更新慢,训练时间长。
rsl_rl 的默认配置通常是学习率 1e-3、clip 系数 0.2。这个配置在大多数足式任务里能跑通,但不是最优。我的调参经验是:先用默认配置跑一遍,观察 KL 散度的表现。如果 KL 长期低于 0.005,说明更新太保守,可以把学习率提到 2e-3 或者 3e-3;如果 KL 经常超过 0.03,说明更新太激进,把学习率降到 5e-4。
还有一个技巧是学习率衰减。训练后期策略已经接近收敛,这时候降低学习率能让策略更稳定。rsl_rl 支持自适应学习率,根据 KL 散度自动调整,但我个人更倾向于手动设置分段衰减,比如训练到 60% 的时候把学习率减半,这样可控性更强。
4.3 checkpoint 管理与回滚策略
训练足式机器人策略,checkpoint 管理是保命的东西。你永远不知道下一次更新会不会把策略搞崩,所以必须能随时回滚。
我的做法是:每 50 个 iteration 存一次 checkpoint,同时保留最近 5 个。另外,每次奖励创新高的时候单独存一个"best" checkpoint。这样即使训练崩了,也能从最近的好状态恢复,不用从头再来。
回滚的时机判断也很重要。如果奖励连续 100 个 iteration 没有提升,而且 KL 散度开始异常,那就该考虑回滚了。不要抱着"再等等说不定就好了"的心态,强化学习训练崩了之后很少能自己恢复,越等越糟。
5. 从 PyTorch 到 ONNX:导出环节的完整操作与验证
5.1 导出前的网络结构确认
在导出之前,必须先确认 actor 网络的结构。rsl_rl 的 actor 通常是一个 MLP,输入是观测向量,输出是动作均值。但要注意,有些配置里 actor 会包含一个可学习的对数标准差(log_std),这部分在部署时是不需要的,因为真机上我们通常直接用均值作为动作,或者在外面加固定的探索噪声。
导出时要把网络设成 eval 模式,这很重要。训练模式下的 dropout、batch norm 等层在推理时行为不同,如果不切换,导出的模型会有随机性。rsl_rl 的网络一般没有 dropout,但养成 eval 的习惯没坏处。
另外,观测归一化的参数(running mean 和 running std)要一起导出。有两种做法:一种是把归一化逻辑写进网络的前向计算里,这样导出的 ONNX 直接接受原始观测;另一种是导出归一化参数,在推理端手动做归一化。我更推荐第一种,因为这样部署端只需要喂原始观测,不容易出错。
5.2 导出脚本的关键参数
下面是一个典型的导出脚本骨架,我把它拆开讲每个参数的意义:
import torch import torch.onnx # 假设 actor 已经加载好权重并设为 eval 模式 actor.eval() # 构造一个示例输入,维度要和真实观测一致 dummy_input = torch.randn(1, obs_dim) torch.onnx.export( actor, dummy_input, "policy.onnx", export_params=True, # 把权重一起导出 opset_version=11, # ONNX 算子集版本 do_constant_folding=True, # 常量折叠优化 input_names=["obs"], # 输入名,部署时要对应 output_names=["action"], # 输出名 dynamic_axes={ # 动态维度,支持变 batch "obs": {0: "batch_size"}, "action": {0: "batch_size"} } )opset_version的选择有讲究。版本太低,某些算子不支持;版本太高,部署端的推理引擎可能还没适配。目前比较稳妥的是 11 或 12。如果你要用 TensorRT 部署,建议先查一下目标 TensorRT 版本支持的 opset 范围。
dynamic_axes是为了支持变 batch size。真机推理通常是 batch=1,但如果你要做批量测试或者多机器人并行,就需要动态 batch。设了动态维度之后,ONNX 模型会更灵活,但某些推理引擎对动态维度的优化不如静态维度好,这个要权衡。
5.3 导出后的数值一致性验证
这一步是重中之重,绝对不能跳过。验证方法是:准备一批观测数据(可以从训练时的 replay 里采样,也可以随机生成),分别用 PyTorch 模型和 ONNX Runtime 跑一遍,对比输出。
import onnxruntime as ort import numpy as np # PyTorch 推理 with torch.no_grad(): torch_out = actor(torch.FloatTensor(test_obs)).numpy() # ONNX Runtime 推理 sess = ort.InferenceSession("policy.onnx") onnx_out = sess.run(["action"], {"obs": test_obs})[0] # 对比 diff = np.abs(torch_out - onnx_out) print("最大绝对误差:", diff.max()) print("平均绝对误差:", diff.mean())如果最大绝对误差在 1e-5 到 1e-4 之间,属于正常范围,可以接受。如果超过 1e-3,就要排查了。常见的误差来源包括:某些算子被降精度(比如 float64 被转成 float32)、归一化逻辑没导出、网络里有不支持的操作被替换成了近似实现。
我遇到过一次误差特别大的情况,排查了半天发现是 actor 网络里用了一个自定义的激活函数,ONNX 不支持,导出时被静默替换成了 ReLU。这种问题不会报错,只能靠数值对比发现。所以验证这一步,真的不能省。
6. 部署到真机前的最后几道关卡
6.1 ONNX 模型的量化与加速
ONNX 模型导出后,如果推理性能不够,可以考虑量化。最常见的是 INT8 量化,能把模型大小压缩到原来的四分之一,推理速度提升 2~4 倍。但量化会带来精度损失,对足式机器人这种对动作精度敏感的任务,要谨慎。
量化的方式有两种:动态量化和静态量化。动态量化不需要校准数据,实现简单,但精度损失相对大;静态量化需要一批校准数据,精度保持更好。我的建议是,如果真机算力够,优先用 FP16 而不是 INT8,FP16 的精度损失几乎可以忽略,速度提升也很明显。
如果非要用 INT8,一定要在量化后重新做数值一致性验证,而且要在真机上做低速测试,确认动作没有明显异常再逐步提速。
6.2 推理频率与实时性保障
真机部署时,推理频率必须和控制频率匹配。如果策略控制频率是 50Hz,那推理必须在 20ms 内完成。ONNX Runtime 在 CPU 上跑一个小型 MLP 通常几毫秒就够了,但如果模型比较大或者主控性能弱,就可能超时。
保障实时性的方法,一是模型轻量化,二是推理引擎优化(比如用 TensorRT),三是做好超时处理。超时处理的意思是:如果这一帧推理没在预算时间内完成,就用上一帧的动作,而不是阻塞等待。这样虽然会损失一点控制精度,但能避免系统卡死。
6.3 真机首测的安全策略
第一次把策略放到真机上跑,一定要做好安全措施。我的标准流程是:先把机器人吊起来,让腿悬空,观察动作是否合理;然后放到地面上,但用手扶着,感受力的方向;最后才完全放开,而且要在软垫上、低速命令下测试。
命令速度要从零开始,慢慢加。先测试站立,再测试原地踏步,然后才是慢速前进。每个阶段都要观察关节温度、电流,如果发现异常发热或者电流尖峰,立刻停止。
还有一个细节是动作限幅。仿真里策略可能输出超出关节限位的动作,真机上如果直接执行会触发保护或者损坏关节。部署时一定要在输出端加一层限幅,把动作裁剪到安全范围内。
7. 我在整个流程里踩过的几个真实坑
第一个坑是观测顺序不一致。训练时观测向量的拼接顺序是 [角速度, 重力方向, 命令, 关节角度, 关节速度, 上一帧动作],部署时我重新写了一遍观测拼接代码,结果顺序搞错了,把关节角度和关节速度调换了。机器人一上电就疯狂抖动,排查了两个小时才发现是顺序问题。教训是:观测拼接的代码,训练和部署必须共用同一份,不要重写。
第二个坑是归一化参数没更新。rsl_rl 的观测归一化是 running mean/std,训练结束时这些参数是最终值。我导出时用的是训练中期的 checkpoint,但归一化参数忘了同步,导致部署时归一化用的是一套过时的统计量,动作整体偏了一个方向。这个问题的隐蔽性在于,它不会导致完全不能动,而是动作有系统性偏差,很难一眼看出来。
第三个坑是ONNX 的 batch 维度。我导出时没设 dynamic_axes,模型固定 batch=1。后来想做多机器人并行测试,喂了 batch=4 的数据,直接报错。重新导出加上动态维度才解决。这个坑不大,但浪费时间。
第四个坑是真机执行器延迟。仿真里动作是即时生效的,真机上从发出指令到关节实际运动有几十毫秒的延迟。这个延迟在慢速运动时影响不大,但快速步态下会导致相位错乱。解决办法是在训练时加入动作延迟的随机化,让策略学会适应延迟。这个改动之后,真机步态的稳定性明显提升。
8. 这条流程还能往哪些方向延伸
Microduck 这套仿真强化学习流程跑通之后,其实可以往几个方向继续深挖。一个是课程学习,从简单地形逐步过渡到复杂地形,让策略在更难的环境里也能保持稳定。另一个是多任务学习,让一个策略同时支持多种步态和命令模式,减少真机切换策略的开销。
还有一个我觉得很有价值的方向是在线自适应。真机运行时会遇到仿真里没建模的情况,比如负载变化、地面突然变软。如果策略能根据实时观测在线微调,鲁棒性会更强。这需要在部署端保留一部分训练能力,或者用一个轻量的自适应网络来调整策略输出。
ONNX 这条链路本身也在演进。现在有些推理引擎支持直接加载 PyTorch 的 TorchScript,跳过了 ONNX 转换,减少了一层精度损失。但 ONNX 的通用性还是最好的,跨平台、跨引擎都能用。如果你的部署目标比较固定,可以评估一下 TorchScript 或者 TensorRT 的原生格式,说不定能省掉一些转换的麻烦。
最后分享一个我自己的习惯:每次训练出一个满意的策略,我都会把完整的配置、checkpoint、导出脚本、验证数据打包存档,标注清楚训练日期、环境版本、关键超参。因为强化学习的复现性很差,同样的配置换个时间跑可能结果完全不同。存档做得好,后面出问题的时候能省下大量排查时间。这个习惯看起来笨,但真的有用。