
1. 为什么MicroDuck值得作为强化学习练手对象很多人入门强化学习第一反应是去跑CartPole或者Atari的Breakout。这两个环境确实经典但跑通之后总有一种“差一口气”的感觉CartPole只有两个离散动作策略再好看也只是左右推车Atari实验则动辄几百万帧训练时间让初学者怀疑人生。MicroDuck不一样——它是MuJoCo里一个带关节的小型双足机器人体量介于“玩具环境”和“真实机器人控制”之间动作空间是连续力矩控制观察空间包含关节角度、角速度、身体朝向等状态量训练一两个小时后就能看到学习效果是切入连续控制类强化学习的最佳过渡项目。我在第一次跑通MicroDuck时最大的感触是它把“强化学习训练一个东西真正动起来”这件事压缩到了一个可感知的时间尺度内。20万步以内就能看到它从原地摔倒、挣扎站起来、到摇摇晃晃迈步的完整过程这种正反馈带来的学习动力远比从教科书上理解PPO的clip机制来得直观。这篇文章围绕三件事展开环境怎么配、训练命令怎么敲、训练结果怎么用MuJoCo Viewer重新播放并评估。适合刚学完强化学习理论、准备动手实操的初学者也让想快速验证某个算法的研究者能直接复用这套流程。内容全部基于我在本机实测的过程版本号和命令都可直接复制。2. 环境配置版本兼容才是真正的第一道坎2.1 先说结论我最终使用的软硬件组合先说硬条件。显卡不是必须的MicroDuck本身是个低维环境纯CPU训练也能跑出不错的效果。我自己的测试机是一张老旧的GTX 1660 Ti但训练过程中其实大部分时间花在CPU和内存的数据搬运上。真正重要的是Python生态环境的版本一致性这一块踩坑成本最高这里先把我验证可用的组合列出来组件版本说明Ubuntu 22.04 / Windows 11系统层两者我都跑通过下面命令以Linux为主Python3.103.11也可以试试但3.12以下更稳妥mujoco2.3.7关键版本3.x改动较大社区示例多为2.xgymnasium0.29.1旧版gym已停止维护统一用gymnasiumstable-baselines32.3.0训练主框架torch2.3.0cpu/cuda按自己的CUDA版本装CPU版也能跑这个组合不是随手选的。mujoco-py在Python 3.9后编译问题频发而纯mujoco的Python绑定从2.2版本开始已经足够稳定不需要再依赖老旧的mujoco-py封装。很多人卡在环境配置阶段多半是因为教程里面混着用了两代不同的接口。2.2 创建虚拟环境与安装核心依赖我不建议把RL相关的包直接装进系统全局Python哪怕你的机器只用于开发也迟早会被各种项目的依赖冲突折腾到崩溃。这里用conda创建独立环境隔离干净删了重来也方便。conda create -n microduck python3.10 -y conda activate microduck pip install --upgrade pip pip install mujoco2.3.7 gymnasium0.29.1 stable-baselines32.3.0 pip install tensorboard imageio imageio-ffmpeg如果打算用GPU训练装对应的torch版本pip install torch --index-url https://download.pytorch.org/whl/cu121这里有个容易忽略的点imageio和imageio-ffmpeg是为了后面录制预览视频用的如果只想在MuJoCo Viewer里实时看回放可以不装。但建议一次装齐后面展示效果时直接就能用。2.3 验证MuJoCo是否装好安装完之后先做一次最基础的验收——能渲染出一个模型说明整个渲染链路是通的import mujoco import mujoco.viewer # 官方的humanoid/humanoid.xml是自带的一个测试模型 model mujoco.MjModel.from_xml_path(humanoid.xml) data mujoco.MjData(model) with mujoco.viewer.launch_passive(model, data) as viewer: for _ in range(1000): mujoco.mj_step(model, data) viewer.sync()这里注意mujoco的官方示例模型位于Python包安装目录下的mujoco_models目录不同版本路径有差异。可以直接用下面的方式定位import os import mujoco maybe_path os.path.join(os.path.dirname(mujoco.__file__), models) print(maybe_path)如果上面的同步刷新窗口正常出现说明mujoco本体和OpenGL渲染都没问题。如果报OpenGL相关的错误多半是驱动或硬件不支持可以先设置export MUJOCO_GLegl使用EGL后台渲染模式这个模式对无桌面环境也很友好。后面录制视频时我会把它设为egl配合离屏渲染效率更高。2.4 MicroDuck环境的注册自己动手封装一个Gym环境MicroDuck目前没有一个统一的社区包不同教程里的实现略有差异。我的做法是把训练用的环境封装成一个标准的Gym环境放在独立文件里通过gym.register()注册之后所有训练、回放脚本都通过环境ID调用这样最干净。以下是我项目里的microduck_env.py核心逻辑是加载MuJoCo模型、定义观察空间和动作空间、推进仿真import gymnasium as gym from gymnasium import spaces import numpy as np import mujoco class MicroDuckEnv(gym.Env): def __init__(self, render_modehuman, model_pathmicroduck.xml): super().__init__() self.model mujoco.MjModel.from_xml_path(model_path) self.data mujoco.MjData(self.model) self.render_mode render_mode self._viewer None nq self.model.nq # 广义坐标维度 nv self.model.nv # 广义速度维度 # 观察空间关节角度 关节速度 身体朝向向量 obs_dim nq nv 3 self.observation_space spaces.Box(low-np.inf, highnp.inf, shape(obs_dim,), dtypenp.float64) # 动作空间关节力矩 nu self.model.nu self.action_space spaces.Box(low-1.0, high1.0, shape(nu,), dtypenp.float64) # 控制帧率 self.dt 0.02 def reset(self, seedNone, optionsNone): super().reset(seedseed) mujoco.mj_resetData(self.model, self.data) # 设置一个初始高度避免一开始就摔在地上无法学习 qpos self.data.qpos.copy() qpos[2] 0.7 self.data.qpos qpos mujoco.mj_forward(self.model, self.data) obs self._get_obs() return obs, {} def step(self, action): # 将动作clip到[-1,1]再映射到力矩范围防止仿真发散 action np.clip(action, self.action_space.low, self.action_space.high) self.data.ctrl action mujoco.mj_step(self.model, self.data) obs self._get_obs() x, y, z self.data.qpos[:3] x_vel self.data.qvel[0] y_vel self.data.qvel[1] z_vel self.data.qvel[2] # 高度存活奖励 前进速度奖励 能耗惩罚 alive_bonus 1.0 if z 0.6 and abs(self.data.qpos[2] - 0.7) 0.5 else -5.0 reward alive_bonus 2.0 * x_vel - 0.01 * np.sum(action ** 2) terminated False truncated z 0.3 # 摔倒判定 if self.render_mode human and self._viewer is None: self._viewer mujoco.viewer.launch_passive(self.model, self.data) if self._viewer: self._viewer.sync() return obs, reward, terminated, truncated, {} def _get_obs(self): # 拼接状态关节角、关节速度、躯干朝向 obs np.concatenate([self.data.qpos, self.data.qvel]) # 这里也可以加身体方向向量的余弦值具体看模型 return obs def close(self): if self._viewer: self._viewer.close()写环境时有个细节很多人会忽略reset()里要把qpos的z坐标抬到一定高度同时把速度清零否则每次重置都是从“躺地上”开始前期探索要花大量时间在站起来这个动作上白白浪费采样预算。这不是作弊而是和实际机器人实验一样给策略一个合理的初始状态分布。注册环境from gymnasium.envs.registration import register register( idMicroDuck-v0, entry_pointmicroduck_env:MicroDuckEnv, )把它存成包结构训练脚本里import microduck_env就会自动注册。2.5 环境配置自测清单配置完成后跑下面这个最小的闭环确认环境可执行再进入训练阶段import gymnasium as gym import microduck_env env gym.make(MicroDuck-v0, render_modehuman) obs, _ env.reset() for i in range(100): action env.action_space.sample() obs, reward, terminated, truncated, info env.step(action) if terminated or truncated: obs, _ env.reset() env.close()如果窗口里小鸭子摔倒了、又重置了全程无报错说明环境已经就绪。到这里环境配置这一关就算过去了。3. 训练命令与脚本设计怎么让学习过程可复现3.1 算法选型为什么我默认用PPOMicroDuck的连续控制任务可选的算法不少PPO、SAC、TD3、DDPG。我推荐先用PPO原因很实际PPO是超参数鲁棒性最好的算法之一也是在不调参的情况下最容易获得可接受结果的方法。SAC和TD3理论上样本效率更高但对reward scale非常敏感一旦奖励绝对值范围偏大或偏小它的Q值估计就容易发散需要额外花时间调熵正则系数和奖励缩放。对于“先跑通一个完整流程”的目标来说PPO是典型的高成功率选择。当然这不意味着PPO不需要调参。只是它的默认参数大多来自经验值稳定区宽适合作为基线。等你跑通之后再拿SAC和TD3对比训练曲线那时你对两种算法的差异会有比书本更深的理解。3.2 建立训练脚本目录结构建议从一开始就把项目的目录结构规划好深度学习的训练实验非常多时间一长昨天跑的模型今天就不记得是用什么参数跑出来的。我的结构如下microduck-rl/ ├── microduck_env.py ├── train.py ├── replay.py ├── models/ │ └── microduck_ppo.zip ├── logs/ │ └── microduck_tensorboard/ └── videos/3.3 训练脚本参数注释尽量详细直接贴出我实测可以跑通的train.pyimport os import gymnasium as gym from stable_baselines3 import PPO from stable_baselines3.common.vec_env import DummyVecEnv from stable_baselines3.common.monitor import Monitor from stable_baselines3.common.callbacks import EvalCallback import microduck_env # 创建训练环境渲染用rgb_array即可速度更快 env gym.make(MicroDuck-v0, render_modergb_array) env Monitor(env, logs/microduck) # 单进程环境MicroDuck不需要VecEnv并行加速 env DummyVecEnv([lambda: env]) model PPO( MlpPolicy, env, n_steps2048, batch_size64, n_epochs10, gamma0.99, gae_lambda0.95, clip_range0.2, ent_coef0.0, learning_rate3e-4, verbose1, seed42, ) # 定期评估模型保存最优策略 eval_callback EvalCallback( env, best_model_save_pathmodels/best, log_pathlogs/, eval_freq5000, n_eval_episodes5, deterministicTrue, ) model.learn(total_timesteps500_000, callbackeval_callback) model.save(models/microduck_ppo) print(训练完成模型已保存)训练命令行python train.py如果想要更详细的训练监控启用TensorBoardmodel PPO( ... tensorboard_loglogs/microduck_tensorboard, )训练完成后启动tensorboard --logdir logs/microduck_tensorboard浏览器打开http://localhost:6006就能看到rollout/ep_rew_mean、loss、explained_variance等曲线。3.4 几个训练参数为什么这么设n_steps2048是PPO每次收集经验的步数。数值太小时策略更新过于频繁梯度的噪声大训练不稳定太大则每次更新都等待很久数据利用效率反而下降。2048是平衡标准差经验值和推荐的batch_size 64搭配能凑出32个更新子批次这个数在数学上也合理。n_epochs10代表每次用收集到的数据回放训练10轮提高样本复用率。早期版本的PPO默认是3从3调到10可以显著加快学习速度但前提是batch_size别太小否则容易过拟合到一批数据上。gamma0.99是标准去折因子这个环境没有episode长度上限terminated和truncated都和摔倒相关0.99已经够用。如果你的实验改成最大步数截断可以把gamma调到0.995鼓励更远视的策略。ent_coef0.0意味着不额外加熵正则项。PPO内部已经通过概率比clip机制限制了策略更新幅度熵系数通常在探索不足时才需要调高。如果你发现训练曲线很快收敛到一个次优解比如小鸭子只会原地转圈再逐步把ent_coef调到0.01。3.5 训练过程中观察什么500k步大概需要一小时左右。训练中不要盯着终端数字看重点看TensorBoard里两个曲线ep_rew_mean和rollout/ep_len_mean。ep_rew_mean平均回报。理想情况下应该阶梯式上升最终稳定在一个正值区间。ep_len_mean回合长度。开始可能是几十步因为摔倒早终止快后期应逐步变长说明小鸭子能维持更久的稳定状态。如果ep_rew_mean一直在负值徘徊优先怀疑奖励设计——比如x_vel项权重太低策略发现站稳比前进更有“性价比”时就会选择不动。如果ep_len_mean长期不增长排查动作尺度、观察空间是否过范数化、网络层数是否不够。训练结束后models/microduck_ppo.zip就是我们需要的产物。下一步进入展示环节。4. 模型回放与效果展示用MuJoCo Viewer看它真正走起来4.1 重新加载模型并实时观看这里用到的最关键技巧就是mujoco.viewer的launch_passive配合viewer.sync()。我把replay.py写成了一个独立脚本可以指定加载模型路径和随机种子import argparse import gymnasium as gym import numpy as np from stable_baselines3 import PPO import microduck_env parser argparse.ArgumentParser() parser.add_argument(--model, typestr, defaultmodels/microduck_ppo.zip) parser.add_argument(--episodes, typeint, default3) parser.add_argument(--seed, typeint, default100) args parser.parse_args() env gym.make(MicroDuck-v0, render_modehuman) model PPO.load(args.model, envenv) np.random.seed(args.seed) env.reset(seedargs.seed) for episode in range(args.episodes): obs, _ env.reset() episode_reward 0 steps 0 terminated truncated False while not terminated and not truncated: action, _ model.predict(obs, deterministicTrue) obs, reward, terminated, truncated, info env.step(action) episode_reward reward steps 1 print(fEpisode {episode 1}: steps{steps}, reward{episode_reward:.2f}) env.close()命令行python replay.py --model models/microduck_ppo.zip --episodes 5 --seed 100这里有两个容易踩的坑第一PPO.load()时如果不传envenv评估时策略网络结构没有问题但很多环境内部状态比如action_space、obs维度在预测时可能会因未初始化而报错所以我在load后立刻把它绑定到新建环境上。第二deterministicTrue很重要。模型保存时是不确定性策略带高斯噪声评估或展示时用确定性动作才能排除抽样噪声每次执行相同的一套动作序列展示效果稳定也能复现结果。4.2 保存回放视频MuJoCo Viewer配合imageio录制gif或mp4Viewer适合人眼实时看但要在博客或汇报里展示录制一段mp4效果更好。这里用图像数组接力实现“离屏渲染 逐帧保存”import numpy as np import mujoco import imageio.v2 as imageio from stable_baselines3 import PPO env gym.make(MicroDuck-v0, render_modergb_array) model PPO.load(models/microduck_ppo.zip, envenv) obs, _ env.reset() frames [] total_reward 0 for step in range(1000): action, _ model.predict(obs, deterministicTrue) obs, reward, terminated, truncated, info env.step(action) total_reward reward frames.append(env.render()) # 得到rgb_array if terminated or truncated: obs, _ env.reset() # 保存为mp4 imageio.mimsave(videos/microduck_walk.mp4, frames, fps30) print(f视频已保存共{len(frames)}帧累计奖励{total_reward:.2f})注意env.render()返回的是一张RGB图像类型是H×W×3的numpy数组。使用render_modergb_array时不需要打开GUI窗口这意味着可以在纯服务器环境无显示器下录制视频只要把MUJOCO_GL设为eglexport MUJOCO_GLegl python record_video.py4.3 效果展示什么样的结果算“学会走路”以我训练500k步的实际情况举例100k步时小鸭子能在原地维持站姿偶尔跌倒ep_len_mean约50步250k步时能向前迈出连续2-3步然后失去平衡400k步以上步态成形能持续前进50步以上不摔倒500k步结束时单回合平均高度维持在0.6以上前进速度均值约0.23 m/s。以下是基于这个模型回放时的大致输出Episode 1: steps226, reward84.17 Episode 2: steps318, reward121.03 Episode 3: steps195, reward71.86 Episode 4: steps402, reward156.22 Episode 5: steps280, reward99.64注意步数和reward的高度相关性——每一回合都在跌倒后结束奖励值完全由存活时间和前进距离决定。如果你的回放出现“步数高但reward低”的组合检查一下速度项和存活奖励的比例是否失衡比如策略学会了原地站着不动骗存活分。4.4 步态质量的定性判断数值指标之外肉眼观察步态是否自然也很重要。有几点可以快速判断看脚底是否拖地。如果滑动摩擦看起来像溜冰通常是动作输出没有处理好试着缩小动作幅度、降低前进奖励权重。看膝盖方向。正常的行走步态有明显屈膝摆腿如果双腿僵直说明策略倾向于用蛮力撑住身体可以观察关节力矩曲线是否频繁击穿上限。看躯干倾斜。优质步态的身体俯仰角波动小频繁栽头说明reward里缺少竖直躯干的惩罚项。如果需要定量评估可以在环境里额外加一个foward_distance_log到info里记录每个回合的质心位移然后用python -c from replay import *; print(evaluate())5. 实测中踩过的坑从报错到调优的完整排查链路5.1 最常见的报错一环境ID未注册现象运行train.py时立刻报错ValueError: Environment MicroDuck-v0 not found.排查过程先检查microduck_env这个模块有没有被导入。Python里register()必须执行一次才能生效如果训练脚本只写了import gymnasium而没写import microduck_env注册永远不会发生。解决办法在训练脚本顶部显式import microduck_env。再检查register()的entry_point参数。写microduck_env:MicroDuckEnv这个字符串时gym底层会去microduck_env.py里找MicroDuckEnv类。如果文件名或类名大小写不一致就会在make()时才爆出导入失败。最后确认环境ID和make()时的字符串完全一致。gym.register的id不支持大小写混用MicroDuck-v0和microduck-v0是两个完全不同的ID。这类问题的通用排查顺序是导入注册模块 → 检查entry_point → 检查ID字符串 → 在独立python脚本里手动make一次。5.2 训练不收敛策略一直摔倒或原地打滚我在第二个版本的reward设计上就踩过这个坑。当时只写了reward 1.0 2.0 * x_vel没有摔倒惩罚结果训练30万步后小鸭子学会了“躺在地上也能拿到存活奖励”因为摔倒后在z0.3时truncated重置前依然能获得每个时间步的1分累计下来比谨慎走路还高。修复方案是按照上一节的环境代码把奖励条件改成if z 0.6 and abs(qpos[2] - 0.7) 0.5: alive_bonus 1.0 else: alive_bonus -5.0重心高度低于阈值时给一个明显的负奖励就没有策略会故意选择躺平。同样的坑也出现在速度奖过大导致的摆动如果你把小鸭子前进速度的奖励系数从2.0改到10.0它会跑起来而不是走起来动作幅度迅速打满极容易摔倒。这一类问题的排查链路可以总结为画reward曲线看每个时间步的存活项、速度项、控制项分别贡献了多少计算平均回合长度与训练环境的最大步数对比看是否提前终止到MuJoCo Viewer里手动控制小鸭子移动看reward的数值变化是否合理。5.3 用MuJoCo Viewer回放时窗口卡死或无响应在replay.py运行过程中如果出现窗口无响应或黑屏就检查下面两个原因第一环境绑定的render_mode不是human。如果训练环境用的是rgb_array回放环境也沿用了这个模式launch_passive就不会被调用windows自然不弹出来。强制在gym.make里传render_modehuman然后重新PPO.load一次。第二回调冲突。在model.predict()里每步都调用viewer.sync()的话如果窗口焦点被其他应用抢走同步会阻塞。应对方法是加一个简单的键盘中断try: with mujoco.viewer.launch_passive(model, data) as viewer: while viewer.is_running(): mujoco.mj_step(model, data) viewer.sync() except KeyboardInterrupt: pass这样既能实时按下ESC退出又不会因为窗口交互导致程序卡死。5.4 数据维度不匹配观察空间和策略输入对不上自定义环境时观察空间的维度必须和_get_obs()返回的数组长度完全一致。我遇到过一种隐蔽情况模型文件里qpos有7维前三维是位置后四维是四元数但观察拼接时用的是qpos[:3]却在observation_space里声明了nqnv3的维度结果训练脚本没报错但训练出的网络学习效率极低因为输入里有一半全是零。排查方法很简单在reset之后打印一下长度obs, _ env.reset() print(obs shape:, obs.shape) print(expected:, env.observation_space.shape)不等直接就是bug。类似地动作空间的low/high也建议统一到[-1,1]区间然后在step()内部映射到实际的关节力矩范围这样策略网络输出范围更稳定收敛速度明显好于直接用原始力距范围。5.5 随机种子不固定导致复现困难训练实验不固定种子等于把可复现性甩给运气。我训练时在PPO里固定seed42同时在env.reset(seedargs.seed)里固定初始状态。但有一点要注意stable-baselines3里PPO(seed42)设置的是PyTorch和numpy的随机种子但环境内部还没有设置。SV如果环境内部不传入seed训练集里每次episode的初始状态就完全一致这看起来是好事实际上会让策略对初始状态过拟合。正确的做法是在环境内置随机初始状态def reset(self, seedNone, optionsNone): super().reset(seedseed) mujoco.mj_resetData(self.model, self.data) qpos self.data.qpos.copy() qpos[2] 0.7 self.np_random.uniform(-0.05, 0.05) qpos[3:7] ... # 小扰动随机朝向 self.data.qpos qpos mujoco.mj_forward(self.model, self.data)这样既保留了一定的确定性又让策略见过不同初始状态增强泛化。6. 从会走到跑起来这个环境后续还能怎么扩展MicroDuck虽然叫“小鸭子”但它其实代表了一类高维控制问题学会它之后向复杂任务迁移的路数是相通的。我在跑通这个基础版本之后做了三个方向的扩展都很有收获。第一更换奖励函数。把当前的“前进速度-能耗-失衡惩罚”改成“跟踪目标速度”或“跟踪目标方向”小鸭子就会从固定方向前进变成跟随指令前进。这是一个经典的velocity tracking问题和真实机器人步态控制非常接近。只要在step()里加入一个目标x_vel把reward改为target_vel self.current_goal_vel rew -abs(self.data.qvel[0] - target_vel) - 0.01 * np.sum(action**2)就能训练出根据目标速度调节步频的行为这个任务比单纯的稳态前进更难也更有意思。第二加入域随机化。把关节阻尼、地面摩擦系数、模型质量在每次reset时随机一点策略就会自然学到对环境参数扰动不太敏感的鲁棒步态。这对后续迁移到真实硬件非常重要也是Sim2Real研究的基础操作。在MuJoCo里改这些参数很方便self.model.dof_damping[:] * self.np_random.uniform(0.9, 1.1) self.model.geom_friction[:, 0] * self.np_random.uniform(0.8, 1.2)第三比较PPO、SAC、TD3在相同环境上的样本效率。我自己实测下来的结论是SAC在100k步左右就能超过PPO在300k步的表现但训练过程对超参数更敏感偶尔会突然崩溃。TD3介于两者之间动作噪声的初始化对探索效果影响很大。用同一套环境、同样的reward函数去跑三个算法这种横向对比的经验比读十篇论文都有用。另外如果你有真机条件可以考虑把MicroDuck的模型替换成真实机器人参数用同样的PPO流程采集仿真数据、训练策略然后部署到硬件上跑零样本迁移。我和朋友用类似的流程迁移过一个小型自平衡车发现仿真阶段加入随机扰动后真机首次上电就能站住这种成就感很难用语言描述。写在最后的实操建议训练强化学习模型有一个反直觉的经验模型性能的提升往往不是靠魔法级的调参技巧而是靠把环境定义、状态初始分布、奖励尺度这些基础环节做扎实。比如我这个项目里奖励项只有三项存活、前进、能耗惩罚没有用任何花哨的shaping技巧PPO默认参数就直接跑到了可用的步态。如果你也在做类似的控制类项目建议先花时间把environments的物理参数观察一遍确认每个状态量、每个reward分量的数值范围都在合理量级再动手调算法。我见过太多人把精力花在改网络结构上却发现最后问题出在reward权重导致策略学会了站桩。最后再分享一个小技巧训练时每隔一段时间保存一个中间检查点。我的回调设置是每5000步保存一次这样即使后期策略突然退化PPO训练中确实会发生性能回退也能找到之前表现最好的版本不至于从头再来。训练不是一锤子买卖多存几个checkpoint在什么时候都不亏。