概述与QA

本系列为我在vla上的第一个实践:基于lerobot做π0.5的推理 、微调 、仿真。本文主要介绍在在MuJoCo中实现可视化仿真。

下面列出几个QA来引入:

Q:什么是MuJoCo?

A:MuJoCo 是一个能精准模拟多刚体动力学、复杂碰撞和摩擦的物理引擎,是强化学习和机器人控制最常用的“虚拟训练场”。


1. 常见仿真环境的简单对比

下面给出MuJoCo、Isaac、Gazebo、PyBullet这四款主流仿真引擎的对比:

特性维度MuJoCoNVIDIA Isaac SimGazeboPyBullet
核心定位学术界RL/控制研究的黄金标准工业级大规模AI训练与高保真Sim-to-RealROS生态的系统级集成测试场轻量级入门与快速原型验证
物理引擎自研MuJoCo引擎NVIDIA PhysX 5 (GPU加速)DART/Bullet/ODEBullet
物理精度/特点接触动力学极其精准、稳定,速度快大规模并行能力极强,物理精度高满足多数系统集成需求,精度中等接触稳定性稍逊于顶级引擎,但能满足多数验证
渲染能力一般 (OpenGL),非光线追踪,画质类似游戏顶级,RTX光线追踪,照片级真实感一般 (OGRE)较差,复古画质
GPU并行有限(部分变体如MJX支持)极强,可在GPU上并行模拟数千个环境,训练速度极快有限支持GPU加速,但并非核心设计
ROS 2 集成社区维护的wrapper,生态在增长官方桥接,成熟深度原生集成,事实上的ROS标准仿真器社区维护的桥接
核心优势接触仿真极快且准,是验证RL算法逻辑的理想选择GPU并行+光追渲染,为视觉Sim-to-Real和大规模训练而生与ROS生态无缝集成,适合系统级测试和导航任务极致轻量,pip install即可,学习曲线平缓
主要短板视觉渲染弱,不直接支持光线追踪硬件要求极高 (需RTX显卡),闭源,学习曲线陡峭物理引擎选项多导致行为可能不一致,视觉一般物理精度和视觉效果相对入门
典型硬件CPU即可必须 NVIDIA RTX GPUCPUCPU

本章选择MuJoCo作为仿真引擎,主要是考虑到其硬件要求不高、且LeRobot封装了适配接口,方便入门。


2. 环境准备

通过下面的指令进行MuJoCo的安装及环境激活:

# ===== 一次性的系统依赖(在 Ubuntu 本机终端) =====
sudo apt install -y python3.10-venv mesa-utils
# python3.10-venv:允许 Python 创建隔离虚拟环境。
# mesa-utils:提供 glxinfo,用来检查本机 OpenGL 渲染信息。


# ===== 创建 MuJoCo 虚拟环境 =====
mkdir -p ~/venvs
python3 -m venv ~/venvs/mujoco
# 在 ~/venvs/mujoco 创建独立 Python 环境,不修改 Ubuntu 系统 Python。


# ===== 安装 MuJoCo =====
~/venvs/mujoco/bin/python -m pip install --upgrade pip
~/venvs/mujoco/bin/python -m pip install mujoco
# 第一条更新该虚拟环境中的 pip。
# 第二条安装 MuJoCo Python 绑定及其必要依赖。


# ===== 日常进入 MuJoCo 环境 =====
source ~/venvs/mujoco/bin/activate
# 激活后,python 和 pip 都指向 ~/venvs/mujoco 内的版本。

python -c "import mujoco; print(mujoco.__version__)"
# 验证 MuJoCo 能被导入;当前应输出 3.15.0。

deactivate
# 退出虚拟环境,恢复 Ubuntu 系统 Python。

3. 本地ubuntu的可视化实践

由于我自己有一台ubuntu的电脑,所以刚好可以用来做一下MuJoCo的本地可视化实践。大致流程可分为构建MJCF和写代码让其运动并可视化。

3.1. 构建MJCF

通过下面的代码构建一个MJCF(MuJoCo 的 XML 模型格式) xml,说简单点就是构造一个适配MuJoCo的机器人模型:

<mujoco model="a1_pendulum">
    <!-- 全局配置选项,步长为0.01s,即100hz -->
    <option timestep="0.01" gravity="0 0 -9.81"/>

    <worldbody>
        <!-- 定义光源 -->
        <light pos="0 0 2"/>
        <!-- 定义几何体:地面 -->
        <geom name="floor" type="plane" size="2 2 0.1" rgba="0.8 0.8 0.8 1"/>
        <!-- 定义刚体pendulum,钟摆 -->
        <body name="pendulum" pos="0 0 1">
            <!-- 定义这个body相对于父级怎么运动,一个铰链joint,旋转轴axis绕y轴转,阻尼damping -->
            <joint name="hinge" type="hinge" axis="0 1 0" damping="0.1"/>
            <!-- 定义这个body的形状,capsule胶囊体,
            fromto="0 0 0 0 0 -0.5":从 body 原点 (0,0,0) 延伸到 (0,0,-0.5),长度 0.5 米,方向竖直向下。
            密度density 500  -->
            <geom 
                name="rod"
                type="capsule"
                fromto="0 0 0  0 0 -0.5"
                size="0.03"
                density="500"/>
            <!-- 末端标记 -->
            <site name="tip" pos="0 0 -0.5" size="0.04" rgba="1 0 0 1"/>
        </body>
    </worldbody>

    <!-- 执行器,给hinge加执行器,传动比/缩放系数gear为1 -->
    <actuator>
        <motor name="hinge_motor" joint="hinge" gear="1" />
    </actuator>

    <sensor>
        <!-- 关节位置传感器 -->
        <jointpos name="hinge_position" joint="hinge" />
        <!-- 关节速度传感器 -->
        <jointvel name="hinge_velocity" joint="hinge" />
    </sensor>
</mujoco>

这里简单构造了一个倒立摆,给其加上了运动执行器和相关传感器。

3.2. 运动及可视化

通过下面的代码引入MJCF模型,并让其动起来,在MuJoCo中可视化:

from pathlib import Path
import time

import mujoco
import mujoco.viewer

# 加载MJCF模型
model_path = Path(__file__).with_name("A1_1_pendulum.xml")
model = mujoco.MjModel.from_xml_path(str(model_path))
# 把目标引到data上
data = mujoco.MjData(model)
# 初始化状态,用mj_forward让状态立即生效
data.qpos[0] = 0.8
mujoco.mj_forward(model, data)
# 被动模式打开 viewer,物理推进仍然由代码控制;与之相对的是 launch(主动模式),它会自动以实时速度跑仿真,只能观察
with mujoco.viewer.launch_passive(model, data) as viewer:
    while viewer.is_running() and data.time < 8.0:
        # 取整秒
        phase = int(data.time // 1.0)
        data.ctrl[0] = 0.2 if phase % 2 == 0 else -0.2

        # 单步执行
        mujoco.mj_step(model, data)
        # 同步
        viewer.sync()
        time.sleep(model.opt.timestep)

结果:

可以看到倒立摆在画面中左右摆动了起来。


4. 云端的MuJoCo实践

由于本地的显卡不足以支持我的模型实验,所以我需要在云端也打通MuJoCo相关的链路,以便后续云端训练好模型后能方便的进行可视化。

4.1. 自己构建action让机械臂在MuJoCo中动起来

通过下面的代码手动让机械臂在MuJoCo中动一点,然后读取其状态:

import os
import numpy as np

os.environ["MUJOCO_GL"] = "egl"
os.environ["PYOPENGL_PLATFORM"] = "egl"
os.environ["LIBERO_CONFIG_PATH"] = "/root/autodl-tmp/libero-config"

from libero.libero import benchmark
from libero.libero.envs import OffScreenRenderEnv
# 获取任务
suite_class = benchmark.get_benchmark("libero_spatial")
suite = suite_class()
task_id = 0
task = suite.get_task(task_id)
# 搭完整环境
bddl_path = suite.get_task_bddl_file_path(task_id)
env = OffScreenRenderEnv(
    bddl_file_name=bddl_path,
    robots=["Panda"],
    controller="OSC_POSE",
    camera_names=["agentview", "robot0_eye_in_hand"],
    camera_heights=256,
    camera_widths=256,
    render_gpu_device_id=0,
)

# reset,记录初始状态
obs_before = env.reset()
time_before = env.sim.data.time
eef_before = obs_before["robot0_eef_pos"].copy()
joint_before = obs_before["robot0_joint_pos"].copy()

print("=== 动作前 ===")
print("MuJoCo time:", time_before)
print("末端位置:", eef_before)
print("关节位置:", joint_before)

# 手动构建action,前三维为末端位置增量xyz,中三维末端姿态增量(旋转量),最后一维夹爪控制
# 这个7维动作定义是由controller="OSC_POSE"决定的
action= np.zeros(7, dtype=np.float32)
# 尝试x轴上动0.2
action[0] = 0.2
print(f"\naction输入:{action}")

obs_after, reward, done, info = env.step(action)

time_after = env.sim.data.time
eef_after = obs_after["robot0_eef_pos"].copy()
joint_after = obs_after["robot0_joint_pos"].copy()

print("\n=== 动作后 ===")
print("MuJoCo time:", time_after)
print("末端位置:", eef_after)
print("关节位置:", joint_after)

print("\n=== 变化 ===")
print("物理时间增量:", time_after - time_before)
print("末端位置变化:", eef_after - eef_before)
print("关节位置变化:", joint_after - joint_before)
print("底层 ctrl:", env.sim.data.ctrl)
print("reward:", reward)
print("done:", done)
print("是否成功:", env.check_success())
print("接触对数量 ncon:", env.sim.data.ncon)

env.close()
print("\nA1_5_ENV_STEP_OK")

其中:

# 获取任务
suite_class = benchmark.get_benchmark("libero_spatial")
suite = suite_class()
task_id = 0
task = suite.get_task(task_id)
# 搭完整环境
bddl_path = suite.get_task_bddl_file_path(task_id)
env = OffScreenRenderEnv(
    bddl_file_name=bddl_path,
    robots=["Panda"],
    controller="OSC_POSE",
    camera_names=["agentview", "robot0_eye_in_hand"],
    camera_heights=256,
    camera_widths=256,
    render_gpu_device_id=0,
)

suite拿到的是libero数据集“libero_spatial”这一类别下的第一个任务,该任务有其对应的bddl文件,即Behavior Domain Definition Language(行为领域定义语言),MuJoCo拿到了这个bddl,便可以由此构建该任务的初始场景。说的更直白些,这个任务具体来说是把碗夹到盘子上,那么MuJoCo加载对应的bddl后,环境中便会出现对应的碗、盘子等物体。

# reset,记录初始状态
obs_before = env.reset()
time_before = env.sim.data.time
eef_before = obs_before["robot0_eef_pos"].copy()
joint_before = obs_before["robot0_joint_pos"].copy()

接着通过env.reset()拿到机械臂的初始观察结构体,得到其初始的状态信息,方便与运动后做对比。

# 手动构建action,前三维为末端位置增量xyz,中三维末端姿态增量(旋转量),最后一维夹爪控制
# 这个7维动作定义是由controller="OSC_POSE"决定的
action= np.zeros(7, dtype=np.float32)
# 尝试x轴上动0.2
action[0] = 0.2
print(f"\naction输入:{action}")

obs_after, reward, done, info = env.step(action)

手动构建了一个action,通过env.step(action)让机械臂以此action为参数做运动,然后拿到其运动之后的观测信息。最后得到运动前后的状态对比:

[robosuite WARNING] No private macro file found! (__init__.py:7)
[robosuite WARNING] It is recommended to use a private macro file (__init__.py:8)
[robosuite WARNING] To setup, run: python /root/miniconda3/envs/lerobot/lib/python3.12/site-packages/robosuite/scripts/setup_macros.py (__init__.py:9)
[info] using task orders [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Local assets not found. Downloading from HuggingFace Hub...
Assets already downloaded at /root/.cache/libero/assets
=== 动作前 ===
MuJoCo time: 0.0
末端位置: [-0.20846466  0.          1.17327948]
关节位置: [ 0.         -0.16103739  0.         -2.44459747  0.          2.2267522
  0.78539816]

action输入:[0.2 0.  0.  0.  0.  0.  0. ]

=== 动作后 ===
MuJoCo time: 0.05000000000000003
末端位置: [-2.07448173e-01  1.25641091e-10  1.17312582e+00]
关节位置: [ 1.23965311e-10 -1.58773374e-01  4.88064534e-10 -2.44314655e+00
 -1.30078402e-09  2.22994845e+00  7.85398163e-01]

=== 变化 ===
物理时间增量: 0.05000000000000003
末端位置变化: [ 1.01648798e-03  1.25641091e-10 -1.53660407e-04]
关节位置变化: [ 1.23965311e-10  2.26401465e-03  4.88064534e-10  1.45091611e-03
 -1.30078402e-09  3.19624830e-03  2.11285434e-11]
底层 ctrl: [ 1.14412382e-05 -1.65487243e+01  1.64244251e-05  1.85914614e+01
 -2.42811908e-05  2.15043672e+00  2.60274124e-07  2.00000000e-02
 -2.00000000e-02]
reward: 0.0
done: False
是否成功: False
接触对数量 ncon: 0

A1_5_ENV_STEP_OK

细究可以发现,action的第一位应代表末端x方向上的移动距离,这里给的是0.2,但实际的变化量却是1.01648798e-03,这是因为action输入之后实际还会进行一定比例的缩放,所以机械臂的真实位移跟输入并不相等。

4.2. 加入可视化,让机械臂连续运动,并录制视频

通过下面的代码让机械臂连续运动并基于相机画面回放制作视频:

import os
import numpy as np
import imageio.v2 as imageio

os.environ["MUJOCO_GL"] = "egl"
os.environ["PYOPENGL_PLATFORM"] = "egl"
os.environ["LIBERO_CONFIG_PATH"] = "/root/autodl-tmp/libero-config"
from libero.libero import benchmark
from libero.libero.envs import OffScreenRenderEnv

output_path = "/root/autodl-tmp/workspace/my_test/week3/a1_5_libero_agentview.mp4"
suite_class = benchmark.get_benchmark("libero_spatial")
suite = suite_class()
task_id = 0
task = suite.get_task(task_id)
# 搭完整环境
bddl_path = suite.get_task_bddl_file_path(task_id)
env = OffScreenRenderEnv(
    bddl_file_name=bddl_path,
    robots=["Panda"],
    controller="OSC_POSE",
    camera_names=["agentview", "robot0_eye_in_hand"],
    camera_heights=256,
    camera_widths=256,
    render_gpu_device_id=0,
)

obs = env.reset()
eef_start = obs["robot0_eef_pos"].copy()
# 创建视频写入器,每秒20帧
writer = imageio.get_writer(output_path, fps=20)

try:
    for step in range(100):
        action= np.zeros(7, dtype=np.float32)
        action[0] = 0.4 if step < 50 else -0.4
        # 记录执行动作前的外部相机画面
        writer.append_data(obs["agentview_image"])
        obs, reward, done, info = env.step(action)

        if step % 20 == 0:
            print(
                f"step={step:3d}, "
                f"time={env.sim.data.time:.2f}s, "
                f"eef_pos={obs['robot0_eef_pos']}"
            )

finally:
    writer.close()
    eef_end = obs["robot0_eef_pos"].copy()
    env.close()

print("\n起点末端位置:", eef_start)
print("终点末端位置:", eef_end)
print("视频已保存:", output_path)
print("A1_5_RENDER_VIDEO_OK")

大体流程跟4.1中的类似,只是视频生成的方式是基于相机的画面一帧一帧的拼成的。

输出:

step=  0, time=0.05s, eef_pos=[-2.06419356e-01  2.22131802e-10  1.17297472e+00]
step= 20, time=1.05s, eef_pos=[-1.11076906e-01 -4.63826865e-09  1.16437695e+00]
step= 40, time=2.05s, eef_pos=[-9.97307539e-03 -1.39111087e-08  1.15975094e+00]
step= 60, time=3.05s, eef_pos=[ 1.10731097e-02 -1.44967077e-08  1.17882865e+00]
step= 80, time=4.05s, eef_pos=[-9.45860408e-02 -4.35499803e-09  1.17959858e+00]

起点末端位置: [-0.20846466  0.          1.17327948]
终点末端位置: [-1.94633736e-01  3.18126055e-09  1.18002965e+00]
视频已保存: /root/autodl-tmp/workspace/my_test/week3/a1_5_libero_agentview.mp4
A1_5_RENDER_VIDEO_OK

最后生成了视频,并观测到机械臂顺x轴来回运动。


总结

本文介绍了MuJoCo并将其与主流仿真引擎做了对比、然后介绍了在本地和云端进行的MuJoCo实践。

文章仅做学习分享,如有谬误,欢迎指正。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐