3. 力矩控制原理

好,我们正式开始动手搭环境。这一章我会带你把强化学习实验的“地基”打牢。说白了,就是让机器人能在电脑里跑起来,并且能跟你的算法交互。

我个人习惯把环境搭建分成三层:物理引擎(算力怎么来)、标准接口(怎么跟算法对话)、具体任务(机器人要干嘛)。今天我们先搞定前两层,顺便跑通一个简单的例子。

3.1 为什么选MuJoCo?

市面上物理引擎不少,Bullet、ODE、PhysX……但我为什么推荐MuJoCo?

MuJoCo全称是Multi-Joint dynamics with Contact。它专为机器人设计,计算效率高,而且对关节约束、接触摩擦的模拟非常准。我在项目中遇到过用Bullet调了三天参数,结果换到MuJoCo一次就跑通的情况——不是Bullet不好,而是MuJoCo的默认参数更贴近真实机器人。

另外,MuJoCo在2021年开源了,现在由Google DeepMind维护。社区活跃,文档也全。你想想看,一个免费、高效、有巨头背书的引擎,不选它选谁?

核心优势:
  • 精度高:基于凸优化的求解器,接触稳定,不抖。
  • 速度快:C语言底层实现,Python接口调用,毫秒级仿真。
  • 可微分:支持自动微分,适合做基于梯度的控制优化。
  • 标准化:Gymnasium原生支持,开箱即用。

3.2 Gymnasium:强化学习的“通用插座”

Gymnasium是 Gym的继承者。它定义了一套标准接口:reset()、step()、render()。不管底层是MuJoCo还是别的引擎,你的算法代码都不用改。

我刚开始做强化学习时,自己写了一套环境接口,结果每次换任务都要改算法代码,改到崩溃。后来切到Gymnasium,世界清净了。说白了,它就是强化学习界的USB接口——插上就能用。

3.2.1 安装与验证

先装核心库。建议用Python 3.9+,虚拟环境隔离。

pip install gymnasium
pip install gymnasium[mujoco]  # 包含MuJoCo支持
pip install mujoco             # MuJoCo引擎本体

装完后跑个测试,看看能不能拉起一个机器人环境:

import gymnasium as gym

env = gym.make("HalfCheetah-v5", 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()
    if terminated or truncated:
        obs, info = env.reset()

env.close()

如果看到一个猎豹模型在地上乱扭,恭喜你,环境搭好了。

常见坑:
  • 渲染黑屏:检查有没有装glfw或osmesa。Linux下可以apt install libglfw3。
  • 版本冲突:Gymnasium v1.0+ 和旧版Gym不兼容。建议用pip list | grep gym确认版本。
  • MuJoCo key:开源版不需要license key。如果你遇到key报错,说明装了旧版mujoco-py,卸载换mujoco包。

3.3 MuJoCo物理引擎核心概念

MuJoCo用XML文件描述机器人模型。你写一个.xml,定义关节、连杆、碰撞体、摩擦系数……引擎就按物理规律算。

我见过不少新手直接拿别人的XML跑,结果机器人飞了。为什么?因为gravity没开,或者timestep设太大。嗯,这里要注意。

3.3.1 模型结构速览

一个典型的MuJoCo XML长这样:

<mujoco model="simple_arm">
  <compiler angle="radian" />
  <option timestep="0.01" gravity="0 0 -9.81" />

  <worldbody>
    <light name="top" pos="0 0 2" />
    <geom name="floor" type="plane" size="1 1 0.1" />

    <body name="link1" pos="0 0 0.1">
      <joint name="joint1" type="hinge" axis="0 0 1" />
      <geom name="link1_geom" type="capsule" size="0.05" fromto="0 0 0 0.3 0 0" />

      <body name="link2" pos="0.3 0 0">
        <joint name="joint2" type="hinge" axis="0 0 1" />
        <geom name="link2_geom" type="capsule" size="0.05" fromto="0 0 0 0.3 0 0" />
      </body>
    </body>
  </worldbody>
</mujoco>

关键元素:

  • <option>:全局参数,重力、时间步长、求解器迭代次数。
  • <worldbody>:世界坐标系下的物体。
  • <body>:刚体,可以嵌套形成运动链。
  • <joint>:关节,定义自由度。hinge是旋转,slide是滑动。
  • <geom>:几何形状,用于碰撞和渲染。
我的习惯:写XML时先画一个简图,标出关节位置和运动范围。不然嵌套多了,自己都搞不清哪个body属于哪个关节。

3.3.2 时间步长与仿真稳定性

MuJoCo默认timestep="0.005"(5ms)。你调大一点,仿真跑得快,但容易发散。调小一点,稳,但慢。

我曾经在调一个四足机器人时,把timestep设成0.02,结果机器人直接劈叉——关节力矩算不准,腿飞了。后来老老实实改回0.005,再配合solver_iterations,才稳住。

timestep 特点 适用场景
0.001 (1ms) 非常稳定,但慢 高精度控制、接触频繁的任务
0.005 (5ms) 默认,平衡性好 大多数机器人任务
0.01 (10ms) 快,但可能不稳定 简单环境、快速原型验证

3.4 Gymnasium + MuJoCo:它们怎么配合?

Gymnasium的MuJoCo环境,本质上是一个封装器。它加载XML模型,在step()里调用MuJoCo的mj_step(),然后把关节位置、速度、力矩等数据打包成observation返回。

你想想看,整个过程就是:

  1. reset:重置模型到初始姿态,清空速度。
  2. step(action):把action映射成关节力矩,传给MuJoCo,推进仿真一步。
  3. 观察:从MuJoCo读出关节角度、角速度、接触力等。
  4. 奖励:根据任务目标(比如跑得快、站得稳)算一个reward。
  5. 终止:如果机器人摔倒或出界,触发terminated。

下面这张图展示了整个数据流:

Gymnasium + MuJoCo 交互流程 强化学习算法 Gymnasium 环境 MuJoCo 引擎 action 力矩/位置 状态/接触力 obs, reward, done 关键说明: • 算法只跟 Gymnasium 交互,不直接调用 MuJoCo。 • Gymnasium 负责将 action 映射为力矩,并计算奖励。 • MuJoCo 只做物理仿真,不关心任务逻辑。

3.5 动手:配置一个自定义机器人环境

光跑现成的环境不够。我们得学会自己搭。下面我带你从零配置一个简单的二连杆机械臂环境。

3.5.1 准备XML模型

把上面那个simple_arm.xml保存到本地。注意,timestep我设了0.01,因为机械臂接触少,可以快一点。

3.5.2 写Gymnasium环境类

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

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)

        # 动作空间:两个关节的力矩,范围[-1, 1]
        self.action_space = spaces.Box(low=-1.0, high=1.0, shape=(2,), dtype=np.float32)

        # 观测空间:关节角度 + 角速度,共4维
        self.observation_space = spaces.Box(low=-np.inf, high=np.inf, shape=(4,), dtype=np.float32)

    def reset(self, seed=None, options=None):
        mujoco.mj_resetData(self.model, self.data)
        # 随机初始姿态
        self.data.qpos[:] = np.random.uniform(-0.5, 0.5, size=2)
        self.data.qvel[:] = 0.0
        mujoco.mj_forward(self.model, self.data)
        return self._get_obs(), {}

    def step(self, action):
        # 施加力矩
        self.data.ctrl[:] = np.clip(action, -1.0, 1.0)
        mujoco.mj_step(self.model, self.data)

        obs = self._get_obs()
        reward = -np.linalg.norm(obs[:2])  # 让末端靠近原点
        terminated = False
        truncated = False
        return obs, reward, terminated, truncated, {}

    def _get_obs(self):
        return np.concatenate([self.data.qpos[:2], self.data.qvel[:2]])

    def render(self, mode="human"):
        if not hasattr(self, "_viewer"):
            self._viewer = viewer.launch_passive(self.model, self.data)
        self._viewer.sync()
避坑指南:我曾经在reset()里忘了调mj_forward,结果第一次step()之前,data里的位置和速度没同步,导致奖励算错。记住:mj_resetData后一定要调mj_forward或mj_step。

3.5.3 注册并测试

from gymnasium.envs.registration import register

register(
    id="SimpleArm-v0",
    entry_point="simple_arm_env:SimpleArmEnv",
)

env = gym.make("SimpleArm-v0", render_mode="human")
obs, _ = env.reset()
for _ in range(200):
    action = env.action_space.sample()
    obs, reward, terminated, truncated, _ = env.step(action)
    env.render()
    if terminated or truncated:
        break
env.close()

如果看到一个二连杆在随机抖动,说明你的自定义环境跑通了。

3.6 本章小结

我们完成了三件事:

  • 安装了Gymnasium和MuJoCo,跑通了第一个仿真。
  • 理解了MuJoCo的XML模型结构和关键参数。
  • 从零写了一个自定义机械臂环境,并注册到Gymnasium。