3、仿真环境搭建:MuJoCo/Gym环境配置、自定义关节环境、仿真与实物的差距分析

说实话,搞机器人控制这么多年,我踩过最大的坑就是——仿真里跑得飞起,一上真机就趴窝。

所以这一章,咱们不整虚的。我带你手把手把仿真环境搭起来,再聊聊那些仿真和实物之间「说不清道不明」的差距。你想想看,如果连仿真都跑不通,谈何强化学习落地?

3.1 MuJoCo 与 Gym 环境配置

MuJoCo 现在已经是机器人仿真界的标配了。为什么?因为它快,而且物理引擎准。我最早用 Bullet,后来换成 MuJoCo,嗯,回不去了。

3.1.1 安装与基础配置

我个人习惯用 conda 管理环境,干净利落。直接上命令:

# 创建虚拟环境
conda create -n robot_rl python=3.9
conda activate robot_rl

# 安装 MuJoCo(2.1.0 版本最稳)
pip install mujoco==2.1.0

# 安装 Gym 接口
pip install gymnasium
pip install gymnasium-robotics

# 验证安装
python -c "import mujoco_py; print('MuJoCo OK')"
⚠️ 注意: 如果你用的是 M1/M2 Mac,或者 Ubuntu 22.04,记得先装 libglfw3。我曾经在这上面卡了整整一下午,最后发现是 OpenGL 库没装全。

3.1.2 快速测试一个环境

装好了,咱们跑个简单的。看看能不能让一个机械臂动起来:

import gymnasium as gym
import time

env = gym.make('FetchReach-v2', render_mode='human')
obs, info = env.reset()

for _ in range(100):
    action = env.action_space.sample()
    obs, reward, terminated, truncated, info = env.step(action)
    env.render()
    time.sleep(0.02)

env.close()

看到那个机械臂在动了吗?如果没看到,检查一下你的显示器是不是接了独显。我有个学生用笔记本集显跑,画面卡成 PPT,后来换了台式机才流畅。

3.2 自定义关节环境

标准环境够用吗?说实话,大部分时候不够。你要控制的可能是四足机器人、灵巧手,甚至是某个非标关节。这时候就得自己动手了。

3.2.1 理解 MuJoCo 的 XML 模型

MuJoCo 用 XML 描述机器人。核心就三个东西:body(刚体)、joint(关节)、geom(几何形状)。

我给你看一个最简单的单关节模型:

<mujoco model="simple_arm">
  <worldbody>
    <body name="base" pos="0 0 0">
      <geom type="box" size="0.1 0.1 0.05" rgba="0.5 0.5 0.5 1"/>
      <joint name="shoulder" type="hinge" axis="0 0 1" pos="0 0 0.05"/>
      
      <body name="link1" pos="0 0 0.15">
        <geom type="cylinder" size="0.03 0.15" rgba="0.2 0.6 0.8 1"/>
        <joint name="elbow" type="hinge" axis="0 1 0"/>
        
        <body name="link2" pos="0 0 0.3">
          <geom type="sphere" size="0.05" rgba="0.8 0.2 0.2 1"/>
        </body>
      </body>
    </body>
  </worldbody>
</mujoco>

这个模型描述了一个两连杆机械臂。肩关节绕 Z 轴旋转,肘关节绕 Y 轴旋转。说白了,就是一个简易版的 SCARA。

3.2.2 封装成 Gym 环境

有了模型,怎么把它变成强化学习能用的环境?继承 gymnasium.Env 就行。我一般这么写:

import gymnasium as gym
from gymnasium import spaces
import numpy as np
import mujoco

class SimpleArmEnv(gym.Env):
    def __init__(self, xml_path='simple_arm.xml'):
        super().__init__()
        
        # 加载模型
        self.model = mujoco.MjModel.from_xml_path(xml_path)
        self.data = mujoco.MjData(self.model)
        
        # 动作空间:两个关节的扭矩
        self.action_space = spaces.Box(
            low=-1.0, high=1.0, shape=(2,), dtype=np.float32
        )
        
        # 观测空间:关节角度 + 角速度
        self.observation_space = spaces.Box(
            low=-np.inf, high=np.inf, shape=(4,), dtype=np.float32
        )
        
    def reset(self, seed=None):
        mujoco.mj_resetData(self.model, self.data)
        return self._get_obs(), {}
    
    def step(self, action):
        # 施加控制
        self.data.ctrl[:] = action
        mujoco.mj_step(self.model, self.data)
        
        # 计算奖励(这里用简单的距离奖励)
        obs = self._get_obs()
        reward = -np.linalg.norm(obs[:2])  # 让关节回到零位
        terminated = False
        
        return obs, reward, terminated, False, {}
    
    def _get_obs(self):
        return np.concatenate([
            self.data.qpos[:2],   # 关节角度
            self.data.qvel[:2]    # 关节速度
        ])
💡 我的经验: 定义观测空间时,别偷懒。我一开始只用了关节角度,结果策略学出来抖得厉害。加上角速度后,稳定性明显提升。你想想看,没有速度信息,策略怎么知道该减速?

3.3 仿真与实物的差距分析

好,环境搭好了,策略也训练了。然后呢?上真机?别急,先看看这张图:

仿真 vs 实物:差距来源分析 仿真环境 ✅ 理想物理模型 ✅ 无噪声传感器 ✅ 精确执行器 ✅ 零延迟通信 ✅ 可重复实验 差距 Sim-to-Real 实物环境 ❌ 摩擦/形变/间隙 ❌ 传感器噪声/漂移 ❌ 电机死区/饱和 ❌ 通信延迟/丢包 ❌ 磨损/温度变化 桥接方法 域随机化 | 系统辨识 | 在线自适应 | 课程学习

3.3.1 差距到底在哪?

我总结下来,差距主要来自三个方面:

  1. 动力学模型误差——仿真里的摩擦系数是常数,实物不是。关节间隙、皮带弹性,这些仿真很难精确建模。
  2. 感知与执行的不确定性——仿真里你读到的关节角度是精确值,实物上编码器有量化误差,电机有死区。我曾经遇到过仿真里能精确停到 0.01 弧度,真机上 ±0.05 弧度就算不错了。
  3. 时序与延迟——仿真里 step() 是同步的,实物上控制周期可能抖动,传感器数据有延迟。这个在高速运动中特别致命。

3.3.2 怎么缩小差距?

说实话,完全消除差距是不可能的。但我们可以让它「足够小」。我个人常用的方法:

  • 域随机化(Domain Randomization):训练时随机化物理参数。比如质量 ±20%,摩擦系数 ±50%。这样策略学出来更鲁棒。
  • 系统辨识:先采集真机数据,再用优化算法拟合仿真参数。我习惯用 CMA-ES 做这个,效果不错。
  • 加入噪声模型:在仿真中人为加入传感器噪声和执行器延迟。让策略提前适应「不完美」的世界。

🔑 核心观点: 仿真不是用来「复现」现实的,而是用来「覆盖」现实的不确定性。你覆盖的场景越多,策略上真机就越稳。

3.3.3 一个实战案例

我之前做一个四足机器人项目,仿真里能跑 3m/s,真机上 1m/s 就开始摔。查了三天,最后发现是仿真里用的电机模型太理想——没有考虑电流饱和。

解决办法?在仿真里加了一个简单的扭矩限幅:

# 在 step() 中加入电机饱和模型
max_torque = 2.0  # Nm
action_clipped = np.clip(action, -max_torque, max_torque)

# 再加一个一阶低通滤波,模拟电机响应延迟
self.filtered_action = 0.8 * self.filtered_action + 0.2 * action_clipped
self.data.ctrl[:] = self.filtered_action

就这么两行代码,真机速度直接提到了 2.5m/s。你想想看,有时候差距就在这些细节里。

3.4 本章小结

这一章我们干了三件事:

  • 搭好了 MuJoCo + Gym 的仿真环境
  • 学会了自定义关节环境,从 XML 模型到 Gym 封装
  • 分析了仿真与实物的差距,并给出了实用的桥接方法

记住一句话:仿真环境是你的「训练场」,不是你的「避风港」。在仿真里跑得再好,也不代表真机上能行。但反过来,如果仿真都跑不好,真机就更别想了。

下一章,我们会真正开始用强化学习来训练关节控制策略。到时候你会发现,环境搭得好不好,直接决定了训练效率。嗯,先把基础打牢吧。

📌 课后练习: 试着修改 simple_arm.xml,给关节增加摩擦和阻尼参数。然后在 Gym 环境中加入传感器噪声,看看策略性能会下降多少。这是你第一次直面 Sim-to-Real 的挑战。

可可精品资料库