從零開始學習 MuJoCo 物理模擬與強化學習環境建置
適用於 UR5 機器手臂 + 二指夾爪
本教學將帶你從零開始,學會以下技能:
# 使用 conda(推薦)
conda create -n mujoco_rl python=3.10
conda activate mujoco_rl
# 或使用 venv
python -m venv mujoco_rl_env
# Windows:
mujoco_rl_env\Scripts\activate
# Linux/Mac:
source mujoco_rl_env/bin/activate
# 進入 tutorial 目錄
cd tutorial
# 安裝所有依賴
pip install -r requirements.txt
# 測試 MuJoCo 是否正確安裝
python -c "import mujoco; print(f'MuJoCo version: {mujoco.__version__}')"
# 測試 PyTorch
python -c "import torch; print(f'PyTorch version: {torch.__version__}')"
pip install mujoco 就能使用,完全免費開源!
| 套件 | 用途 | 必要性 |
|---|---|---|
mujoco | 物理模擬 | 必要 — MuJoCo 官方 Python 套件 |
numpy | 數值運算 | 必要 — 矩陣運算、數學計算 |
torch | 深度學習 | 必要 — 建構 Q-Network 策略 |
ikpy | 逆運動學 | 必要 — 將末端位置轉換為關節角度 |
opencv-python | 影像處理 | 必要 — 相機影像擷取與處理 |
gymnasium | RL 環境介面 | 選用 — 接 Stable-Baselines3 時才需要 |
MuJoCo(Multi-Joint dynamics with Contact)是一個高效的物理模擬引擎, 專門用於機器人學、生物力學和強化學習研究。它能夠精確模擬剛體動力學、接觸力、摩擦等物理現象。
包含所有靜態資訊:
從 XML 檔案載入,模擬過程中不會改變。
包含所有動態狀態:
qpos — 關節位置qvel — 關節速度ctrl — 控制輸入(馬達力矩)xpos — 剛體世界座標time — 模擬時間每次 mj_step() 都會更新。
呼叫 mujoco.mj_step(model, data) 時,MuJoCo 內部會執行以下管線:
mj_step() 每次呼叫只推進 一個時間步(dt=0.002 秒)。
要讓機器人移動到目標位置,需要在迴圈中反覆呼叫數百到數千次,每次根據 PID 控制器更新 data.ctrl。
Renderer 是獨立的 — 它只是讀取當前 data 的狀態來渲染影像,不影響物理模擬。
import mujoco
import mujoco.viewer
# 1. 載入模型(從 XML 檔案)
model = mujoco.MjModel.from_xml_path("UR5gripper_2_finger.xml")
# 2. 建立模擬數據
data = mujoco.MjData(model)
# 3. 查看模型資訊
print(f"關節數量: {model.njnt}")
print(f"致動器數量: {model.nu}")
print(f"時間步長: {model.opt.timestep}")
# 4. 執行模擬步進
mujoco.mj_step(model, data)
print(f"關節位置: {data.qpos[:7]}")
# 5. 啟動互動式檢視器
viewer = mujoco.viewer.launch_passive(model, data)
while viewer.is_running():
mujoco.mj_step(model, data)
viewer.sync()
python 01_hello_mujoco.py
MuJoCo 使用 MJCF(MuJoCo XML Format)來定義模型。以下是關鍵元素:
<!-- UR5 模型結構簡化版 -->
<mujoco model="ur5gripper">
<!-- 編譯器設定 -->
<compiler angle="radian" meshdir="mesh/visual/"/>
<!-- 模擬參數 -->
<option timestep="0.002"/>
<!-- 世界主體 -->
<worldbody>
<!-- 相機 -->
<camera name="top_down" pos="0 -0.6 2.0"/>
<!-- 機器人本體(巢狀結構 = 運動鏈) -->
<body name="base_link">
<body name="shoulder_link">
<joint name="shoulder_pan_joint" axis="0 0 1"/>
<geom type="mesh" mesh="shoulder"/>
<!-- ... 更多關節 ... -->
</body>
</body>
<!-- 桌上物件 -->
<body name="box_1">
<joint type="slide"/> <!-- 自由移動 -->
<geom type="box" size="0.02 0.02 0.02"/>
</body>
</worldbody>
<!-- 致動器(馬達) -->
<actuator>
<motor name="shoulder_pan_T" joint="shoulder_pan_joint"/>
<!-- ... 更多馬達 ... -->
</actuator>
</mujoco>
# 直接控制:設定每個馬達的控制訊號
data.ctrl[0] = 1.0 # 正力矩 -> 肩關節順時針旋轉
data.ctrl[0] = -1.0 # 負力矩 -> 肩關節逆時針旋轉
data.ctrl[6] = -1.0 # 關閉夾爪
# 執行一步模擬
mujoco.mj_step(model, data)
PID 控制器根據「目標值」與「當前值」的誤差,自動計算適當的力矩:
# 建立 PID 控制器(使用模擬時間步長,非系統時鐘)
pid = SimPID(kp=7.0, ki=0.0, kd=1.1,
setpoint=0.0, dt=model.opt.timestep)
# 控制迴圈
for step in range(5000):
current_angle = data.qpos[joint_id] # 讀取當前角度
torque = pid(current_angle) # PID 計算力矩
data.ctrl[actuator_id] = torque # 施加力矩
mujoco.mj_step(model, data) # 模擬步進
SimPID 類別,以模擬時間步長(dt=0.002s)計算微分項。
標準的 simple_pid 套件使用系統時鐘,在無視窗高速模擬時會導致 PID 發散。
from robot_controller import RobotController
controller = RobotController()
# 移動末端執行器到世界座標 [x, y, z]
controller.move_ee([0.0, -0.6, 0.95])
# 內部流程:
# 1. IK 逆運動學:[x,y,z] -> [q1,q2,q3,q4,q5] 關節角度
# 2. PID 控制:每個關節獨立追蹤目標角度
# 3. 模擬步進:直到所有關節到達目標(或超時)
視覺型 RL 的核心:從模擬相機擷取 RGB 彩色影像和深度圖。
# 擷取影像
rgb, depth_raw = controller.get_image(
width=200, height=200, camera='top_down'
)
# 將深度轉換為公尺
depth_meters = controller.depth_to_meters(depth_raw)
# 新 MuJoCo API 的渲染方式:
renderer = mujoco.Renderer(model, height=200, width=200)
renderer.update_scene(data, camera='top_down')
rgb = renderer.render() # RGB 影像
renderer.enable_depth_rendering()
renderer.update_scene(data, camera='top_down')
depth = renderer.render() # 深度影像
Gymnasium(原 OpenAI Gym)是強化學習的標準環境介面。所有 RL 演算法庫 (Stable-Baselines3、CleanRL、RLlib 等)都使用這個介面。
掌握 Gymnasium API = 可以對接任何 RL 演算法!
import gymnasium as gym
from gymnasium import spaces
class SimpleGraspEnv(gym.Env):
def __init__(self):
# 定義觀測空間
self.observation_space = spaces.Dict({
'rgb': spaces.Box(0, 255, shape=(200,200,3), dtype=np.uint8),
'depth': spaces.Box(0, 10, shape=(200,200), dtype=np.float32),
})
# 定義動作空間
self.action_space = spaces.Discrete(40000) # 200x200 像素
def reset(self, seed=None):
# 重置環境,回傳初始觀測
obs = self._get_observation()
return obs, {}
def step(self, action):
# 執行動作,回傳結果
# 1. 解碼動作 (像素 -> 世界座標)
# 2. 控制機器人執行抓取
# 3. 計算獎勵
return obs, reward, terminated, truncated, info
# 建立環境
env = gym.make('SimpleGrasp-v0', render_mode='human')
# 重置
obs, info = env.reset(seed=42)
# 互動迴圈
for step in range(10):
action = env.action_space.sample() # 隨機動作
obs, reward, terminated, truncated, info = env.step(action)
print(f"Step {step}: reward={reward}")
if terminated or truncated:
obs, info = env.reset()
env.close()
import gym
env = gym.make('Env-v0')
obs = env.reset()
obs, r, done, info = env.step(a)
import gymnasium as gym
env = gym.make('Env-v0')
obs, info = env.reset(seed=42)
obs, r, term, trunc, info = env.step(a)
Q-Learning 是最基本的 RL 演算法之一。核心思想:
# Epsilon-Greedy 動作選擇
if random.random() < epsilon:
action = env.action_space.sample() # 隨機探索
else:
q_values = policy_net(state) # 用神經網路預測 Q 值
action = q_values.argmax().item() # 選最大 Q 值的動作
Tutorial 05 使用網格化動作空間:將桌面分為 5x5 = 25 格,每格代表一個抓取候選位置。 狀態是每格的最小深度值(物件深度 < 桌面深度)。
for episode in range(NUM_EPISODES):
state = env.reset() # 重置機器人 + 隨機散佈物件
for step in range(STEPS_PER_EPISODE):
# 1. Epsilon-greedy 動作選擇
if random.random() < epsilon:
action = random.randint(0, 24) # 隨機探索
else:
q = q_net(torch.FloatTensor(state))
action = q.argmax().item() # 選 Q 值最高的格子
# 2. 在 MuJoCo 中執行抓取
next_state, reward, done, info = env.step(action)
# 3. 存入回放緩衝區
buffer.push(state, action, reward, next_state, done)
# 4. 從緩衝區取樣訓練 Q-network
if len(buffer) >= BATCH_SIZE:
train_step(buffer, q_net, optimizer)
state = next_state
if done: break
本專案原本使用已廢棄的 mujoco-py 和 gym。
以下是新舊 API 的對照表:
| 功能 | 舊版 (mujoco-py) | 新版 (mujoco) |
|---|---|---|
| 匯入 | import mujoco_py as mp |
import mujoco |
| 載入模型 | mp.load_model_from_path(path) |
mujoco.MjModel.from_xml_path(path) |
| 建立模擬 | sim = mp.MjSim(model) |
data = mujoco.MjData(model) |
| 模擬步進 | sim.step() |
mujoco.mj_step(model, data) |
| 讀取狀態 | sim.data.qpos |
data.qpos |
| 設定控制 | sim.data.ctrl[i] = v |
data.ctrl[i] = v |
| 檢視器 | mp.MjViewer(sim) |
mujoco.viewer.launch_passive(model, data) |
| 渲染影像 | sim.render(w, h, camera_name=c) |
mujoco.Renderer(model, h, w) |
| 名稱查詢 | model.body_name2id("name") |
mujoco.mj_name2id(model, TYPE, "name") |
| 剛體位置 | sim.data.body_xpos[id] |
data.xpos[id] |
| 功能 | 舊版 (gym) | 新版 (gymnasium) |
|---|---|---|
| 匯入 | import gym |
import gymnasium as gym |
| reset() | obs = env.reset() |
obs, info = env.reset(seed=42) |
| step() | obs, r, done, info |
obs, r, terminated, truncated, info |
| 渲染模式 | env.render(mode='human') |
gym.make(..., render_mode='human') |
| 環境類別 | class Env(gym.Env) |
class Env(gym.Env) (相同) |
修改 01_hello_mujoco.py,列出所有 body 的世界座標位置。
提示:使用 data.xpos[i] 和 mujoco.mj_id2name()
讓機器人依序移動到以下四個位置,畫出一個正方形:
positions = [
[0.2, -0.5, 1.0],
[-0.2, -0.5, 1.0],
[-0.2, -0.7, 1.0],
[0.2, -0.7, 1.0],
]
擷取 top-down 相機影像,找出深度圖中最高的物件(深度最小的點), 並將機器人移動到該位置上方。
提示:使用 np.argmin(depth_meters) 找最淺的像素
修改 04_gymnasium_env.py 的 SimpleGraspEnv,加入以下功能:
step() 中加入夾爪旋轉:根據動作的第二維度旋轉 wrist_3 關節reset() 中用 controller.reset_simulation() 確認物件被隨機散佈Tutorial 05 使用 5x5 網格,成功率很低。嘗試以下改善:
GRID_SIZE 從 5 改為 10(100 格,更精細)N_EPISODES 到 50 或 100GRID_SIZE = 10 # 10x10 = 100 格
N_EPISODES = 50 # 更多探索
結合所有學到的知識,建立一個完整的抓取系統: