强化学习这个词听起来唬人,但真正动手跑通第一个环境的人都知道,最折磨人的往往不是算法推导,而是环境装不上、模型导不出、仿真跑不起来这些"脏活"。我这次拿microduck做切入,从零把一条完整的强化学习实操链路走了一遍——环境搭建、仿真调试、策略训练、模型导出、推理部署,每一步都踩了坑也填了坑。microduck本身是个轻量级的四足机器人仿真项目,结构清晰、依赖干净,非常适合作为强化学习的入门载体。整条链路涉及mujoco物理引擎、PyTorch训练框架、ONNX模型导出与量化、以及最终的推理部署,覆盖了从仿真到落地的核心环节。不管你是刚接触强化学习的新手,还是想把手上的模型从实验室推到实际设备上的开发者,这篇内容都能给你一条可复现的路径。
1. 为什么选microduck作为强化学习的第一个实操项目
1.1 四足机器人仿真在强化学习入门中的独特位置
很多人入门强化学习的第一反应是CartPole或者LunarLander这类经典控制环境。这些环境确实简单,几十行代码就能跑起来,但它们有个共同问题:状态空间和动作空间都太抽象了,你很难把学到的东西和真实世界建立联系。而四足机器人不一样,它的状态是关节角度、角速度、躯干姿态,动作是关节力矩,这些东西你在真实机器人上都能一一对应。microduck作为一个四足机器人仿真项目,恰好卡在"足够简单能跑通"和"足够真实有迁移价值"之间。
我自己的体会是,用CartPole学强化学习,你学的是算法本身;用microduck学强化学习,你学的是"怎么让算法在物理世界里工作"。后者的价值大得多,因为真实项目里让你头疼的从来不是PPO的损失函数怎么写,而是仿真步长设多少、奖励函数怎么设计、观测噪声怎么加、训练出来的策略能不能扛住模型误差。microduck把这些真实问题都摆在你面前,但又不至于像完整的人形机器人那样复杂到劝退。
另外从计算资源角度看,microduck的模型规模适中,在单张消费级显卡上就能完成训练。我实测下来,用一块RTX 3060,大概四到六个小时能训练出一个能稳定行走的策略。这个时间成本对于个人开发者来说是可以接受的,不会出现"跑了一周还没收敛"的挫败感。
1.2 microduck与mujoco的配合逻辑
microduck的仿真后端用的是mujoco物理引擎。mujoco在机器人仿真领域的地位不用多说,它的接触求解器和约束求解器精度很高,四足机器人那种频繁的足地接触场景,用mujoco跑出来的结果和真实世界的差距相对较小。但mujoco也有它的脾气,安装配置这块坑不少,后面我会专门讲。
microduck的模型文件是标准的MJCF格式(MuJoCo XML),这意味着你可以直接修改XML里的几何体、关节、执行器参数,不需要重新编译任何东西。这个特性在调试阶段特别有用——比如你发现机器人走路时前腿总是打滑,可以直接在XML里调摩擦系数,改完存盘重新加载就行。相比之下,有些仿真环境改个参数要重新编译整个项目,调试效率差很多。
还有一点值得说:microduck的观测空间和动作空间定义得很干净。观测包括躯干高度、姿态四元数、各关节角度和角速度、上一帧的动作,动作就是各关节的目标角度。这种设计让奖励函数的编写变得直观——你可以直接根据躯干高度和姿态算奖励,不需要做什么复杂的坐标变换。对于刚入门的人来说,这种"所见即所得"的设计能大幅降低理解成本。
1.3 从microduck到真实部署的路径可行性
选microduck还有一个考虑:它的策略网络规模小,导出ONNX之后模型文件通常只有几MB,量化成int8之后能压到1MB以内。这意味着你训练出来的策略完全有可能部署到算力有限的嵌入式设备上。我这次实操的完整链路就是:microduck仿真训练 → PyTorch策略网络 → ONNX导出 → int8量化 → 推理引擎加载。这条路径走通之后,换成其他机器人平台,流程基本一致,只是观测和动作的维度需要调整。
2. 环境搭建:mujoco安装的坑与microduck依赖梳理
2.1 mujoco安装常见问题与版本选择
mujoco从2.3版本开始开源,安装方式发生了很大变化。网上很多教程还是老版本的安装方法,照着做大概率会报错。我建议直接用pip安装:
pip install mujoco当前稳定版本是3.x系列,API和2.x有一些不兼容的地方。microduck的代码如果写的是老版本API,你需要确认一下兼容性。我这次用的是mujoco 3.1.0,配合Python 3.10,实测稳定。
安装过程中最常见的问题是缺少系统依赖。在Ubuntu上,你需要确保这些包已经装好:
sudo apt-get install libgl1-mesa-glx libglew-dev libosmesa6-devWindows上相对简单,pip安装完基本就能用,但如果要做无头渲染(比如在服务器上训练),需要额外配置EGL或者OSMesa。我一开始在Windows 11上跑,渲染窗口正常,但后来迁移到Linux服务器做批量训练时,无头渲染折腾了好一阵。最后用的是OSMesa后端,设置环境变量:
export MUJOCO_GL=osmesa注意:如果你在服务器上没有显示设备,不设置这个环境变量的话,mujoco初始化渲染时会直接报错退出,而且报错信息不太直观,容易误以为是模型文件的问题。
还有一个坑是mujoco的许可证。虽然现在开源了,但某些旧版本的二进制包可能还带有许可证检查逻辑。如果你遇到莫名其妙的"license not found"错误,先确认一下安装的版本是不是3.x以上。
2.2 microduck项目依赖的完整清单
microduck的核心依赖不算多,但版本匹配很重要。我整理了一份实测可用的依赖清单:
| 依赖包 | 版本 | 用途 |
|---|---|---|
| mujoco | 3.1.0 | 物理仿真引擎 |
| pytorch | 2.1.0 | 策略网络训练 |
| gymnasium | 0.29.0 | 环境接口封装 |
| numpy | 1.24.0 | 数值计算 |
| onnx | 1.15.0 | 模型导出 |
| onnxruntime | 1.17.0 | 推理验证 |
| tensorboard | 2.15.0 | 训练可视化 |
这里要特别说一下gymnasium和gym的区别。gym已经停止维护了,gymnasium是它的继任者,API有细微差别。microduck如果用的是老版gym接口,你需要做适配。主要改动在reset()和step()的返回值上——gymnasium的reset()返回(obs, info),step()返回(obs, reward, terminated, truncated, info)。这个改动不大,但如果不注意,训练循环会直接报错。
2.3 环境验证:跑通第一个仿真步
装完依赖之后,别急着训练,先验证仿真环境能不能正常跑。写一个最小的测试脚本:
import mujoco import numpy as np # 加载microduck模型 model = mujoco.MjModel.from_xml_path("microduck.xml") data = mujoco.MjData(model) # 仿真一步 mujoco.mj_step(model, data) print("关节数量:", model.njnt) print("执行器数量:", model.nu) print("躯干高度:", data.qpos[2])如果这段代码能跑通并打印出合理的数值,说明mujoco和模型文件都没问题。如果报错,大概率是XML文件路径不对或者模型文件本身有语法错误。mujoco的XML报错信息会指出具体哪一行有问题,照着改就行。
我建议在这个阶段多花点时间,手动改改XML里的参数,观察仿真结果的变化。比如把重力从-9.81改成-1.62(月球重力),看看机器人会怎么动。这种"玩"的过程能帮你快速建立对仿真环境的直觉,后面调奖励函数的时候会很有用。
3. 观测空间、动作空间与奖励函数的设计取舍
3.1 观测向量里该放什么、不该放什么
microduck的观测向量设计直接决定了策略网络能"看到"什么信息。放少了,策略学不到东西;放多了,训练变慢还容易过拟合。我这次用的观测向量包含以下几类:
- 躯干状态:高度、姿态四元数、线速度、角速度
- 关节状态:12个关节的角度和角速度(四足各3个关节)
- 上一帧动作:12维,帮助策略学习动作平滑性
- 相位信息:一个标量,表示步态周期中的位置
总共大约50维左右。这个维度对于全连接网络来说刚好,不会太宽也不会太窄。
这里有个经验:不要把所有能拿到的信息都塞进观测里。我一开始把足底接触力也放进去了,结果训练时策略严重依赖接触力信号,仿真里接触力很干净,但真实传感器有噪声,迁移过去直接崩了。后来把接触力去掉,策略反而学得更鲁棒。这个教训说明,观测空间的设计要考虑真实部署时的传感器能力,仿真里能拿到的信息不代表真实系统里也能拿到。
另外,观测的归一化很重要。不同物理量的量纲差异很大——角度是弧度制,数值在-3.14到3.14之间;角速度可能到几十;高度在0.2到0.5之间。如果不做归一化,网络训练时梯度会被大量纲的维度主导。我的做法是维护一个运行均值方差,对每个观测维度做标准化:
class RunningMeanStd: def __init__(self, shape): self.mean = np.zeros(shape) self.var = np.ones(shape) self.count = 1e-4 def update(self, x): batch_mean = np.mean(x, axis=0) batch_var = np.var(x, axis=0) batch_count = x.shape[0] # 更新逻辑...这个类在训练过程中持续更新,确保观测始终在合理范围内。
3.2 动作空间的连续控制与平滑约束
microduck的动作空间是连续的,12个关节的目标角度。连续动作空间比离散动作空间难训,因为策略网络要输出一个12维的连续向量,而且这个向量的每个维度都有物理含义。
我用的方案是策略网络输出目标关节角度,然后通过一个PD控制器转换成力矩。这样做的好处是动作空间有明确的物理意义,而且PD控制器能保证一定的动作平滑性。PD参数的选择有讲究:P太大机器人会抖,P太小机器人会软。我实测下来,P=20、D=0.5这组参数在microduck上表现比较均衡。
动作平滑性是个容易被忽视但很重要的点。如果相邻两帧的动作差异太大,真实舵机会受不了,仿真里也会出现数值不稳定。我在奖励函数里加了一项动作变化率的惩罚:
action_smoothness_penalty = -0.01 * np.sum((action - prev_action) ** 2)这个系数不能太大,否则策略会变得过于保守,走路畏畏缩缩。0.01是我试了好几组之后觉得比较合适的值。
3.3 奖励函数的层次化设计思路
奖励函数设计是强化学习实操中最需要经验的部分。我的思路是分层次设计:
第一层:生存奖励。机器人每存活一个时间步给一个小的正奖励,比如0.1。这一层保证机器人不会一开始就学会"躺平"。
第二层:姿态奖励。躯干高度接近目标高度给正奖励,躯干倾斜给负奖励。这一层引导机器人保持直立。
第三层:运动奖励。前进速度接近目标速度给正奖励。这一层引导机器人往前走。
第四层:能耗和动作平滑惩罚。关节力矩过大给负奖励,动作变化剧烈给负奖励。这一层让策略更"优雅"。
这四层的权重需要仔细调。我一开始把运动奖励权重设得很大,结果机器人学会了"扑倒式前进"——直接往前扑,靠惯性滑行。后来把姿态奖励权重提上来,机器人就学会了正常走路。权重调整没有万能公式,我的经验是先用小权重跑几百步看看策略的"倾向",再根据倾向调整权重。
提示:奖励函数里尽量不要用绝对值太大的奖励。奖励值在-10到10之间比较合适,太大的奖励会导致值函数估计困难,训练不稳定。
4. 训练过程中的调参与踩坑记录
4.1 PPO超参数的初始设置与调整节奏
microduck的训练我用的PPO算法。PPO的超参数不算多,但每个都影响训练效果。我的初始设置如下:
| 超参数 | 初始值 | 调整方向 |
|---|---|---|
| 学习率 | 3e-4 | 训练不稳定时降到1e-4 |
| 裁剪系数 | 0.2 | 一般不动 |
| 折扣因子 | 0.99 | 一般不动 |
| GAE lambda | 0.95 | 一般不动 |
| 每次迭代步数 | 2048 | 显存不够时降到1024 |
| 批大小 | 64 | 一般不动 |
| 训练轮数 | 10 | 过拟合时降到5 |
学习率是最需要关注的。我一开始用3e-4,前1000次迭代loss下降正常,但之后开始震荡。降到1e-4之后训练曲线平滑了很多。这个现象在强化学习里很常见——策略梯度方法的方差本来就大,学习率大了容易在最优解附近跳来跳去。
还有一个经验:不要频繁改超参数。我见过有人每跑几百步就改一次学习率,结果训练曲线像心电图一样。强化学习的训练本来就有随机性,单次实验的波动不代表超参数有问题。我的做法是至少跑完一个完整的训练周期(比如50万步)再判断超参数是否合适。
4.2 训练不收敛时的排查链路
训练不收敛是强化学习实操中最常见的问题。我总结了一条排查链路,按顺序检查:
第一步:确认环境本身没问题。用随机策略跑几百步,看看机器人会不会直接穿模或者飞出去。如果随机策略都能让机器人飞起来,说明仿真参数有问题,先修仿真再谈训练。
第二步:检查奖励曲线。如果奖励一直不涨,先看看是不是奖励函数设计有问题。把奖励函数的每一项单独打印出来,看看哪一项在主导。我遇到过姿态奖励和运动奖励互相打架的情况——机器人往前走就必然低头,低头就被扣姿态奖励,结果策略学了个"原地不动"。
第三步:检查观测和动作的数值范围。打印观测向量的均值和方差,看看有没有异常大的值。我遇到过角速度观测偶尔飙到几百的情况,原因是仿真步长太大导致数值爆炸。把步长从0.01降到0.002就解决了。
第四步:检查策略网络的输出。如果策略网络输出的动作始终在某个小范围内,说明网络初始化有问题或者梯度消失了。可以试试换激活函数,或者加一层LayerNorm。
第五步:降低任务难度。如果以上都没问题但还是不收敛,把目标速度从1.0降到0.3,先让机器人学会慢慢走,再逐步提高目标速度。这种课程学习的思路在四足机器人训练里很有效。
4.3 训练加速:并行环境与向量化
单环境训练microduck,PPO大概需要200万步才能收敛,在单张显卡上要跑好几个小时。加速的方法是用并行环境——同时跑多个microduck实例,收集更多的经验。
我用的方案是SubprocVecEnv,开8个进程并行跑仿真:
from stable_baselines3.common.vec_env import SubprocVecEnv def make_env(): def _init(): return MicroduckEnv() return _init env = SubprocVecEnv([make_env() for _ in range(8)])8个并行环境能把训练时间压缩到原来的1/5左右。但要注意,并行环境会占用更多内存,每个mujoco实例大概占200MB左右,8个就是1.6GB,加上模型和优化器的显存,整体资源消耗要提前算好。
还有一个加速技巧是降低仿真渲染频率。训练的时候不需要每步都渲染,可以设置每100步渲染一次,或者干脆关掉渲染。mujoco的无头模式比渲染模式快很多,我实测下来能快3到4倍。
5. 从PyTorch到ONNX:策略网络的导出与量化
5.1 策略网络结构对导出友好性的影响
训练好的策略网络要导出成ONNX,网络结构的设计就要考虑导出友好性。有些PyTorch的操作在导出ONNX时会遇到问题,比如动态控制流、自定义算子、某些形式的索引操作。
我的策略网络用的是最朴素的全连接结构:输入50维观测,经过两个隐藏层(256维),输出12维动作。激活函数用ELU,因为ELU在ONNX里有标准实现,导出不会出问题。如果你用了Swish或者GELU,需要确认ONNX opset版本是否支持。
导出代码很直接:
import torch.onnx policy.eval() dummy_input = torch.randn(1, 50) torch.onnx.export( policy, dummy_input, "microduck_policy.onnx", input_names=["observation"], output_names=["action"], dynamic_axes={"observation": {0: "batch_size"}}, opset_version=17 )dynamic_axes这个参数很重要。如果不设置,导出的ONNX模型会固定batch size为1,后面想批量推理就不行了。设置成动态之后,模型可以接受任意batch size的输入。
导出之后一定要验证。用onnxruntime加载模型,和PyTorch的输出对比:
import onnxruntime as ort sess = ort.InferenceSession("microduck_policy.onnx") onnx_output = sess.run(None, {"observation": dummy_input.numpy()}) torch_output = policy(dummy_input).detach().numpy() print("最大误差:", np.max(np.abs(onnx_output[0] - torch_output)))误差在1e-5以内就算正常。如果误差很大,说明导出过程中有算子被错误转换了,需要检查网络结构。
5.2 int8量化的实操步骤与精度损失评估
ONNX模型量化成int8能大幅减小模型体积、提升推理速度,但会带来精度损失。对于microduck的策略网络,我的经验是int8量化后精度损失在可接受范围内,但需要做校准。
量化流程分三步:
第一步:准备校准数据。从训练环境里随机采样1000条观测数据,保存成numpy数组。这些数据要覆盖观测空间的主要区域,不能只采一种状态。
第二步:运行量化工具。ONNX Runtime提供了量化工具:
from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic( "microduck_policy.onnx", "microduck_policy_int8.onnx", weight_type=QuantType.QInt8 )第三步:评估精度损失。用同样的测试数据,对比量化前后的输出差异。我实测下来,动作输出的平均误差在0.02弧度左右,对于关节控制来说这个误差完全可以接受。
但要注意,动态量化(quantize_dynamic)只量化权重,不量化激活值。如果要进一步压缩,可以用静态量化(quantize_static),但需要更复杂的校准流程。对于microduck这个规模的网络,动态量化已经够用了。
5.3 ONNX模型在推理引擎上的部署验证
导出的ONNX模型最终要部署到推理引擎上。我这次在onnxruntime上做了验证,流程很简单:
sess = ort.InferenceSession("microduck_policy_int8.onnx") obs = env.reset() for _ in range(1000): action = sess.run(None, {"observation": obs.reshape(1, -1)})[0] obs, reward, done, info = env.step(action.flatten()) if done: obs = env.reset()这段代码跑通,说明模型从训练到部署的链路是完整的。实际部署到嵌入式设备时,推理引擎可能换成TensorRT或者OpenVINO,但ONNX作为中间格式是通用的,转换过去不会有太大问题。
注意:int8量化后的模型在某些推理引擎上可能需要额外的算子支持。如果部署时遇到"unsupported operator"错误,先检查推理引擎的版本是否支持你用的ONNX opset。
6. 仿真到现实的差距与应对思路
6.1 域随机化:让策略见过"世面"
仿真里训练出来的策略直接放到真实机器人上,大概率会失败。原因是仿真和现实之间存在差距:摩擦系数不同、电机响应有延迟、传感器有噪声、模型质量分布有偏差。应对这个问题的标准做法是域随机化——在训练时随机改变仿真参数,让策略学会适应各种条件。
我在microduck上做的域随机化包括:
- 摩擦系数:在0.5到1.5之间随机
- 躯干质量:在标称值的80%到120%之间随机
- 电机延迟:在0到20毫秒之间随机
- 观测噪声:给每个观测维度加高斯噪声,标准差为标称值的5%
- 外部扰动:每隔一段时间给躯干施加一个随机方向的力
这些随机化参数的范围需要仔细调。范围太小,策略学不到鲁棒性;范围太大,策略可能学不会走路。我的经验是从小范围开始,训练收敛后逐步扩大范围,类似于课程学习。
6.2 观测噪声与执行器延迟的建模
观测噪声和执行器延迟是两个最影响迁移效果的因素。观测噪声的建模相对简单,直接在观测上加高斯噪声就行。但要注意,噪声的标准差应该和真实传感器的噪声水平匹配。如果你不知道真实传感器的噪声水平,可以先设一个保守的值(比如标称值的3%),后面再根据实测调整。
执行器延迟的建模稍微复杂一些。真实舵机从收到指令到执行到位有延迟,这个延迟在仿真里需要显式建模。我的做法是维护一个动作缓冲区,策略输出的动作不直接作用于机器人,而是放入缓冲区,延迟若干步之后再取出执行:
class ActionBuffer: def __init__(self, delay_steps, action_dim): self.buffer = deque(maxlen=delay_steps + 1) self.action_dim = action_dim def push(self, action): self.buffer.append(action) def get(self): return self.buffer[0]延迟步数根据真实硬件的响应时间来确定。如果真实舵机响应时间是10毫秒,仿真步长是2毫秒,那延迟就是5步。
6.3 策略鲁棒性评估的实操方法
训练完之后,怎么判断策略够不够鲁棒?我用了几个测试方法:
方法一:参数扰动测试。把仿真参数设成训练时域随机化范围之外的值,看看策略还能不能走。比如训练时摩擦系数范围是0.5到1.5,测试时设成0.3和2.0,观察策略的表现。
方法二:持续外力测试。在机器人躯干上持续施加一个固定方向的外力,看看策略能不能抵抗。这个测试模拟的是真实环境中的风或者负载。
方法三:地形变化测试。在仿真里加一些小的台阶或者斜坡,看看策略能不能适应。microduck的XML里可以直接加地形几何体。
方法四:长时间运行测试。让策略连续跑10分钟以上,观察有没有累积误差或者发散。有些策略短时间看着没问题,跑久了就慢慢偏了。
这些测试的结果不一定要完美,但能帮你了解策略的边界在哪里。知道边界在哪里,比盲目相信策略能work要靠谱得多。
7. 一些零散但重要的实操心得
7.1 日志与可视化:训练过程必须可观测
强化学习训练是个黑盒,如果不做日志和可视化,出了问题你根本不知道从哪查。我建议至少记录以下内容:
- 每轮的平均奖励、最大奖励、最小奖励
- 策略网络的输出均值和方差
- 值函数的估计值
- 观测向量的均值和方差
- 动作的均值和方差
- 训练损失
这些数据用TensorBoard记录,训练过程中随时可以看。我遇到过奖励突然掉下去的情况,看TensorBoard发现是观测方差突然变大,追查下去发现是仿真里有个关节卡住了。如果没有日志,这个问题可能要排查很久。
7.2 模型保存与恢复训练的注意事项
训练过程中要定期保存模型,但保存频率不能太高,否则IO会成为瓶颈。我的做法是每10次迭代保存一次,同时保留最近5个检查点。这样即使最新的模型坏了,也能回退到之前的版本。
恢复训练的时候要注意,优化器的状态也要保存和恢复。如果只保存模型参数不保存优化器状态,恢复训练后loss会有一个明显的跳变。PyTorch里保存优化器状态很简单:
torch.save({ 'model_state_dict': policy.state_dict(), 'optimizer_state_dict': optimizer.state_dict(), 'iteration': iteration }, 'checkpoint.pt')还有一个细节:随机数种子。如果要做可复现的实验,训练开始时就要固定所有随机数种子,包括numpy、torch、以及仿真环境的种子。但要注意,固定种子之后并行环境的随机性会降低,可能影响策略的鲁棒性。我的做法是训练时用随机种子,做对比实验时才固定种子。
7.3 从microduck迁移到其他平台的思路
microduck上跑通的这套流程,迁移到其他机器人平台时,核心逻辑不变,需要改的主要是三个地方:
第一,观测和动作的维度。不同机器人的关节数量不同,观测向量的构成也不同。迁移时需要重新定义观测空间和动作空间,并相应调整策略网络的输入输出维度。
第二,奖励函数。奖励函数和机器人的形态强相关。四足机器人的奖励函数不能直接用在机械臂上。迁移时需要根据新平台的运动特点重新设计奖励。
第三,域随机化的参数范围。不同硬件的噪声水平、延迟特性、摩擦特性都不同,域随机化的范围需要根据目标硬件重新标定。
除此之外,训练流程、导出流程、量化流程都是通用的。这也是我推荐用microduck入门的原因——它让你把整条链路走通一遍,后面换平台只是换零件,不用重新学造轮子。
7.4 关于训练时长和硬件选择的建议
最后说一下硬件。microduck的训练对硬件要求不算高,但有几个地方值得注意:
- GPU:至少6GB显存,8GB比较舒服。并行环境多的时候显存占用会上去。
- CPU:并行环境需要多核CPU,8核以上比较好。每个仿真进程占一个核。
- 内存:16GB起步,32GB推荐。mujoco实例和并行环境都比较吃内存。
- 存储:SSD是必须的,训练日志和模型检查点写入频繁,机械硬盘会成为瓶颈。
训练时长方面,单环境200万步大概4到6小时,8并行环境大概1到1.5小时。这个时间对于调参来说是可以接受的。如果你要做大量的对比实验,建议用多张显卡或者云端的GPU实例,能大幅缩短实验周期。
我在整个实操过程中最大的体会是:强化学习的门槛不在算法,而在工程。算法论文里的公式看起来很优雅,但真正让机器人走起来,靠的是对仿真参数的反复调试、对奖励函数的细致打磨、对训练过程的持续观察。microduck是个很好的起点,它足够简单让你能快速跑通,又足够真实让你能学到真东西。把这条链路走一遍,后面再遇到新的机器人平台或者新的算法,你心里就有底了。