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>:几何形状,用于碰撞和渲染。
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返回。
你想想看,整个过程就是:
- reset:重置模型到初始姿态,清空速度。
- step(action):把action映射成关节力矩,传给MuJoCo,推进仿真一步。
- 观察:从MuJoCo读出关节角度、角速度、接触力等。
- 奖励:根据任务目标(比如跑得快、站得稳)算一个reward。
- 终止:如果机器人摔倒或出界,触发terminated。
下面这张图展示了整个数据流:
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。