做具身智能的同行,应该都经历过这么一段纠结:机械臂、人形机器人在真实硬件上调试,又贵又慢,还得提心吊胆怕撞坏。所以大多数项目都会先在仿真器里完成原型验证、运动规划甚至强化学习训练。但仿真器选型这事,真不是一句“用ROS和Gazebo”就能糊弄过去的。我最早是用Gazebo做机械臂抓取,后来试过PyBullet,最后在这两年把训练主力切到了MuJoCo和dm_control这套组合上,稳定跑了不少PPO/TD3实验。这篇是这个系列的第一篇,我会把为什么选MuJoCo、底层原理、安装避坑、机械臂IK实战、dm_control接入RL框架以及训练调优这些内容,按我实际走通的路径一次讲清楚。
1. 为什么具身智能需要MuJoCo,而不是别的仿真器
1.1 具身智能对仿真器的核心诉求
具身智能和传统机器人仿真的差别,在于“交互”和“学习”的重量级完全不同。传统仿真主要用来验证轨迹、检测碰撞,跑得慢一点没关系,只要轨迹算得出来就行。但具身智能强调感知-决策-执行闭环,尤其是强化学习场景,一个策略需要几百万步甚至上千万步的交互采样,每一步都要计算接触力、关节动力学和渲染状态。这时候仿真的单步耗时就变成了决定性因素。
我在早期用Gazebo做机械臂抓取时,跑一个简单的PPO实验,数据采样速度经常只有真实时间的1/10,一个晚上跑完可能才积累几十万步,完全不够模型收敛。换成MuJoCo之后,同样的策略同数量级步数,采样速度能提升一到两个数量级,这才让我有底气把实验矩阵铺开。另一个关键诉求是接触计算的真实性。抓取、插孔、像人一样行走这类任务,接触模型写得好不好,直接决定学出来的策略能不能迁移到真机。MuJoCo的软接触模型在这种场景下的表现,明显比传统刚体碰撞模型更平滑,训练的收敛性也更好。
1.2 与Gazebo、PyBullet、Isaac的分工对比
不少初学者一上来就问“哪个仿真器最好”,其实这是伪命题,不同工具有明确的分工。
| 仿真器 | 强项 | 弱项 | 适合场景 |
|---|---|---|---|
| Gazebo | 与ROS生态深度绑定,传感器模型丰富 | 物理速度慢,接触模型偏弱 | 机器人在复杂环境中的SLAM、多传感器仿真、多机协同 |
| PyBullet | 简单易懂,Python API友好 | 接触计算不够稳定,批量仿真性能一般 | 入门教学、轻量算法验证 |
| MuJoCo | 接触模型细腻,速度快,数值稳定 | 传感器仿真不如Gazebo丰富 | 机械臂操作、足式机器人、强化学习训练 |
| Isaac Sim/Gym | 支持GPU并行上千环境 | 学习曲线陡,硬件要求高 | 大规模并行RL、人形机器人训练 |
我现在的习惯是:如果项目主要做运动规划、SLAM,或者要跟ROS节点深度联调,那用Gazebo并不过时;如果只是快速验证一个控制算法,PyBullet也能用;但一旦进入强化学习,或者任务本身对接触力仿真要求很高,比如灵巧手抓取、双足/四足行走,MuJoCo基本是首选。Isaac的好处在GPU并行,但成本也高,单卡训练环境少的话优势发挥不出来。
1.3 MuJoCo的“前世今生”,以及为什么DeepMind愿意开源
MuJoCo全称是Multi-Joint dynamics with Contact,最初是华盛顿大学等团队在2015年左右推出的商业授权物理引擎。它一开始就是面向“带接触的多刚体系统优化和控制”来设计的,所以底层算法跟游戏物理引擎不一样,更照顾机器人学和控制领域的需要。2021年DeepMind宣布收购MuJoCo,并在2022年把它开源,采用Apache 2.0协议,2023年发布了全面重写后的3.0版本,把Python绑定、渲染和模型库都重新做了一遍。
这段历史解释了一件事:为什么MuJoCo的API一度有点“非主流”。它更像个研究工具,不适合拿来做游戏特效,但你真要算一个七自由度机械臂的雅可比、做逆运动学、甚至求动力学逆解,MuJoCo的mj_jacSite、mj_comPos这类底层函数直接摆在那里,比你要去别的引擎里翻半源代码方便得多。DeepMind开源MuJoCo,本质上也是为了让整个具身智能社区有统一的高质量物理底座,dm_control就相当于在这个底座上长出来的标准接口层。
2. 从pip install到第一个能动的仿真:环境准备与最小示例
2.1 安装MuJoCo的现代姿势(3.x版本)
如果你在搜索引擎里看到Old School安装教程,让你去官网下压缩包、设置MUJOCO_PYTHON_INCLUDE_PATH之类的环境变量,请先确认版本。MuJoCo 3.0之后的安装非常简单,Python环境只需要一条命令:
pip install mujoco这条命令会同时装上底层的C库和Python绑定,安装完成后就可以直接import mujoco。控制套件再加一条:
pip install dm_controldm_control依赖mujoco包,所以它会拉取对应版本。我个人建议在虚拟环境里装,避免跟其他项目的依赖互相污染。另外如果你准备接强化学习训练,顺手把numpy、gymnasium、stable-baselines3装好,下面用得着。
安装时有个小细节:MuJoCo在Linux上会用到OpenGL相关的渲染后端,如果你用的是WSL或纯命令行的服务器,需要确认libglfw3和libosmesa6这些系统库是否齐全。没有GUI的服务器也可以用mujoco.viewer的passive接口跑后台渲染,但需要设置MUJOCO_GL=egl或MUJOCO_GL=osmesa,否则会在创建渲染上下文时报错。
2.2 Windows 11上的坑与解法
网上搜“Windows 11安装mujoco”,报错最多的两类:第一类是ImportError: DLL load failed while importing mujoco,第二类是GLFW error或者窗口一闪而过。
先处理DLL问题:MuJoCo的Python包自带预编译二进制,但需要系统里装了Visual C++ Redistributable 2015-2022。很多新装系统的机器没装这个运行库,就会在导入阶段失败,装一下官方VC运行库基本能解决。
再处理渲染窗口问题:Windows环境下MuJoCo默认走OpenGL,如果你的电脑显卡驱动太老或笔记本有双显卡,launch_passive可能创建窗口失败或黑屏。解决办法有两条:一是更新显卡驱动,二是强制禁用独立显卡试试。我遇到过一台NVIDIA独显笔记本,只要切到集成显卡跑,窗口就正常了,属于驱动层面的兼容问题。另外如果你用的是conda环境,不要把别的深度学习包和mujoco混装在一个已经乱七八糟的环境里,最好新建干净环境。
2.3 一个只有十几行的最小仿真循环
装完之后,先别急着上复杂模型。跑通最小仿真循环是关键里程碑。下面这段代码可以直接保存成minimal.py运行:
import mujoco import mujoco.viewer import numpy as np # 加载自带的pendulum模型 xml = """ <mujoco> <option timestep="0.002"/> <worldbody> <body name="pendulum" pos="0 0 1"> <joint name="hinge" type="hinge" axis="0 1 0"/> <geom name="pole" type="capsule" fromto="0 0 0 0 0 -0.5" size="0.02"/> </body> </worldbody> </mujoco> """ model = mujoco.MjModel.from_xml_string(xml) data = mujoco.MjData(model) with mujoco.viewer.launch_passive(model, data) as viewer: for _ in range(10000): mujoco.mj_step(model, data) viewer.sync() if not viewer.is_running(): breakmj_step每调用一次,就走一个物理步长timestep,这里是0.002秒,也就是500Hz。viewer.launch_passive提供了一个可交互的观察窗口,sync()负责把最新的仿真状态推送到渲染线程。这段代码跑起来,你应该能看到一个在重力作用下摆动的杆子。
这个最小示例虽然简单,但它展示了MuJoCo的应用模式:加载模型,拿到model和data,循环里调mj_step,然后再决定要不要渲染、要不要采样。后面所有复杂的强化学习环境,基本都是在mj_step外面包一层更高级的接口而已。
3. 仿真器内部在算什么:时间积分、约束求解与接触模型
3.1 控制方程:关节空间动力学
MuJoCo用广义坐标来描述机械系统。以机械臂为例,它内部的核心动力学方程可以写成:
[ M(q)\ddot{q} + c(q,\dot{q}) = \tau + J^T f ]
这里M(q)是质量矩阵,c(q,dot q)包含科氏力和重力项,tau是关节驱动力,J^T f是关节空间上的接触/约束力贡献。MuJoCo的核心工作就是把质量矩阵、科氏力这些机器人学里繁琐的项全部在内部高效算好,用户只需要调mj_step就行。
理解这个方程很重要,因为很多调试都归结为“动力学模型到底把哪些力算进去了”。比如你发现机械臂在仿真里模拟出来的下坠速度跟真实世界明显不一致,那就该检查阻尼项damping、关节摩擦frictionloss和惯性参数inertia是不是设置得跟真机差距太大。
3.2 Euler积分与RK4,以及为什么默认步长是2ms量级
MuJoCo里常用的积分器有Euler(显式欧拉)、RK4(四阶龙格-库塔)和implicitfast。默认情况下,它用的是Euler。你可能会问:Euler不是数值稳定性差吗?为什么这样一个高级仿真器还默认用Euler?
关键原因是,在带接触的机器人仿真里,每步都需要求解约束/接触力,RK4虽然精度高,但代价是每步要多次评估动力学和约束求解,速度会明显下降。而MuJoCo的Euler是半隐式的,它能更自然地跟接触力求解耦合,在很多dexterous manipulation场景里不会发散。实际经验是:对绝大多数具身智能训练任务,Euler加2-5毫秒的步长已经足够稳定。只有在做高精度轨迹规划验证,或者碰到高频柔性问题时,我才考虑把积分器改成RK4。
步长选择也有讲究。timestep越小,单步越接近真实连续动力学,但同样的控制周期内需要的步数就越多,仿真速度越慢。我做机械臂操作时常用timestep=0.002,配合每步的nsubsteps来达到控制频率200Hz-500Hz。四足和人形机器人行走任务,我习惯用timestep=0.005,降低算力开销。
3.3 软接触模型:为什么MuJoCo这么快,还这么稳
MuJoCo最有辨识度的一个设计就是软接触模型。传统物理引擎经常会用完全非弹性碰撞加库仑摩擦来解决接触问题,但要付出现象级的麻烦:摩擦锥线性化、接触矩阵求解不稳定,最后表现就是物体会抖、会弹跳、会穿透。
MuJoCo用的是连续可微的软接触模型,法向力通过一组“阻抗+阻尼”的数学关系来计算,接触力在接触深度和相对速度上是光滑的。这就带来了两个巨大优势:一是在强化学习里,策略梯度算法对奖励函数和状态转移的平滑性非常敏感,软接触让动力学从“可能突然跳变”变成“连续过渡”,大大提升训练稳定性;二是数值上不再需要频繁重试求解器,可以走固定迭代次数并保持不错的精度,这让每步仿真快了很多。
概念上可以类比一下:传统接触像两块硬塑料用力怼上,啪一下停住,容易弹开;MuJoCo的软接触更像中间垫了层橡胶,既能传递力,又不会突然脱手。代价是参数多,核心是solref和solimp,分别控制接触的刚度和阻尼特性。实调时如果发现物体容易陷进去,或者接触力振荡,优先检查这两个参数,而不是盲目改mass。
3.4 MJCF模型格式的核心结构
MuJoCo原生模型格式是MJCF,一个XML方言。第一次看到MJCF的人容易懵,但它的结构其实非常直观:
<mujoco> <option timestep="0.002"/> <worldbody> <body name="link1" pos="0 0 0"> <joint name="joint1" type="hinge" axis="0 0 1"/> <geom type="capsule" fromto="0 0 0 0 0 0.3" size="0.03"/> <body name="link2" pos="0 0 0.3"> <!-- ... --> </body> </body> </worldbody> </mujoco>层级结构是:worldbody下面是所有刚体body,每个body下可以挂joint(关节)、geom(几何外形)、site(参考坐标系)和子body。关节描述自由度,geom决定碰撞和视觉外形,site则经常用来定义末端执行器位置。
MJCF这种层级嵌套特别适合机械臂、人形机器人这种“父body套子body”的结构。如果你手里只有URDF,也没关系,MuJoCo模型编译工具会自动把URDF转换成MJCF,转换后最好检查一下单位、惯性矩阵和坐标轴方向,它们的默认约定跟ROS生态可能不一致。这个问题在真实项目中很常见,我从URDF转过来后经常发现质量参数和轴的朝向要修正。
4. 手把手写一个机械臂仿真:模型构建、正向运动学与IK
4.1 在MJCF里定义一只三连杆机械臂
下面这个模型,是我常用来说明机械臂基础用法的三连杆臂,三个旋转关节,绕Y轴转动,末端在XZ平面内运动:
<mujoco model="simple_arm"> <option timestep="0.002"/> <worldbody> <body name="base" pos="0 0 0"> <joint name="shoulder" type="hinge" axis="0 1 0"/> <geom type="capsule" fromto="0 0 0 0 0 0.3" size="0.03"/> <body name="mid" pos="0 0 0.3"> <joint name="elbow" type="hinge" axis="0 1 0"/> <geom type="capsule" fromto="0 0 0 0 0 0.25" size="0.025"/> <body name="top" pos="0 0 0.25"> <joint name="wrist" type="hinge" axis="0 1 0"/> <geom type="capsule" fromto="0 0 0 0 0 0.15" size="0.02"/> <site name="ee" pos="0 0 0.15"/> </body> </body> </body> </worldbody> </mujoco>你看到fromto这个属性:它表示这个几何体从哪一点延伸到哪一点,对于胶囊和圆柱这类细长形状,比用pos和size去拼坐标要直观很多。最末端的site我命名为ee(end-effector),后面算末端位置和雅可比都靠它。
4.2 正向运动学:从关节角到末端位置
所谓正向运动学(FK),就是知道关节角度,求末端位置。MuJoCo里不需要我们手推公式,每次mj_forward之后,末端位置直接放在data.site_xpos[site_id]里。代码是这样的:
import mujoco import numpy as np xml = open("simple_arm.xml", encoding="utf-8").read() model = mujoco.MjModel.from_xml_string(xml) data = mujoco.MjData(model) site_id = model.site("ee").id data.qpos[:] = np.array([0.5, -0.8, 0.3]) mujoco.mj_forward(model, data) ee_pos = data.site_xpos[site_id].copy() print("末端位置:", ee_pos)这里有个新手容易踩的坑:如果你只设置data.qpos然后直接读site_xpos,读出来的是上一帧的结果。mj_forward负责根据当前位形重新计算运动学量,包括site_xpos、雅可比、质量矩阵等,但不推进仿真。mj_step则是在mj_forward基础上再算动力学并前进一个物理步。所以静态分析时用mj_forward,需要动力学仿真时用mj_step。
4.3 用雅可比做阻尼最小二乘逆运动学
逆运动学(IK)是机械臂控制里绕不开的问题:我已知目标末端位置,反求各关节角。最朴素的Jacobian转置法就行,但我在实际使用中更推荐阻尼最小二乘法(DLS),它在接近奇异位形时不会产生爆炸式的关节速度。
核心步骤如下:
def ik_solve(model, data, site_id, target, max_iter=300, tol=1e-4, damping=0.01): for _ in range(max_iter): mujoco.mj_forward(model, data) current_pos = data.site_xpos[site_id].copy() error = target - current_pos if np.linalg.norm(error) < tol: break jacp = np.zeros((3, model.nv)) jacr = np.zeros((3, model.nv)) mujoco.mj_jacSite(model, data, jacp, jacr, site_id) # 阻尼最小二乘逆: (J J^T + lambda^2 I)^-1 jjt = jacp @ jacp.T j_inv = jacp.T @ np.linalg.inv(jjt + damping ** 2 * np.eye(3)) delta_q = j_inv @ error data.qpos[:] += delta_q这里mujoco.mj_jacSite一次性给出末端的平移雅可比jacp和旋转雅可比jacr。平移雅可比把关节速度映射为末端线速度,旋转雅可比把关节速度映射为末端角速度。IK里我们只需要关心位置误差,所以只用jacp。
阻尼系数damping是关键。设成0就是纯Jacobian逆,在奇异位形会出很大的关节速度;设一个小值比如0.01,相当于在求解时加了正则化,换来了数值稳定性。实际调试时,如果发现IK末端在某个区域抖得厉害,把damping往上抬到0.05甚至0.1,通常会好很多。代价是离目标点会有几毫米的静态误差,对大多数抓取任务来说完全可接受。
4.4 把IK封装成仿真环境中的控制器
上面这个IK函数只能用于离线位形计算,想把它用在实时控制里,需要跟mj_step配合。一个常见的写法是:每个控制周期开始,调用IK得到目标关节角,然后用一个简单的PD控制器跟踪关节角。
kp, kv = 100.0, 20.0 for _ in range(1000): ik_solve(model, data, site_id, target_pos) desired_qpos = data.qpos.copy() # 这里用PD控制跟踪 data.ctrl[:] = kp * (desired_qpos - data.qpos) - kv * data.qvel mujoco.mj_step(model, data)当然这个代码的前提是模型里有对应的执行器(actuator),否则data.ctrl不会有任何效果。在解析关节空间跟踪时,我会在MJCF里加上三个motor actuator,这样ctrl对应三个关节的力矩。
5. 接入dm_control,把环境喂给主流强化学习框架
5.1 dm_control与MuJoCo有什么关系
理解了MuJoCo之后再看dm_control,就很清晰:它是一层更高层的Python环境接口层,基于MuJoCo实现。就像游戏里“物理引擎”和“游戏逻辑框架”的关系。dm_control里的suite模块封装了大量现成的连续控制任务,包括倒立摆、步行、跑跳、操作等,这些环境都遵循dm_env接口,统一了reset和step的样式:
from dm_control import suite import numpy as np env = suite.load("humanoid", "run") timestep = env.reset() action_spec = env.action_spec() for _ in range(200): action = np.random.uniform(action_spec.minimum, action_spec.maximum, size=action_spec.shape) timestep = env.step(action) if timestep.last(): timestep = env.reset()为什么我推荐dm_control而不是直接裸写MuJoCo?当你的任务比较复杂时,环境层面要处理观测定义、奖励函数、终止条件、重置逻辑这些琐事,全用裸MuJoCo写很容易出bug,而且代码难以复用。dm_control把这些约束放进dm_env框架里,顶层还能用dm_control.composer来组合新的物体和任务,省事很多。
5.2 从suite加载现成环境
DeepMind Control Suite是一套现成的基准任务集合。我的习惯是先跑一遍这些环境,验证自己的强化学习框架是否正常工作,再往自定义任务迁移。常用的几个环境包括:
cartpole: swingup:倒立摆,入门首选,状态维度低,适合调试算法。reacher: easy/hard:两连杆机械臂触达目标,接近真实机械臂操作。walker: walk/run:双足行走,接触丰富,适合研究sim-to-real。humanoid: run:全身人形运动,reward shaping复杂度高。
这些环境直接用suite.load(domain, task)就能拿到,比如suite.load("cartpole", "swingup")。
5.3 自定义dm_env环境:把之前的机械臂包装成RL环境
如果你想跑自己的机械臂任务,最简单的做法是实现一个dm_env.Environment子类。下面是基本骨架:
import dm_env from dm_env import specs import numpy as np class SimpleArmEnv(dm_env.Environment): def __init__(self, model_path): self.model = mujoco.MjModel.from_xml_path(model_path) self.data = mujoco.MjData(self.model) self.site_id = self.model.site("ee").id def reset(self): self.data.qpos[:] = np.random.uniform(-0.5, 0.5, self.model.nq) self.data.qvel[:] = 0.0 mujoco.mj_forward(self.model, self.data) return dm_env.restart(self._get_obs()) def step(self, action): self.data.ctrl[:] = action mujoco.mj_step(self.model, self.data) obs = self._get_obs() reward = self._get_reward(obs) return dm_env.transition(reward, obs) def _get_obs(self): return { "qpos": self.data.qpos.copy(), "qvel": self.data.qvel.copy(), "ee_pos": self.data.site_xpos[self.site_id].copy(), } def observation_spec(self): return { "qpos": specs.Array((self.model.nq,), float), "qvel": specs.Array((self.model.nv,), float), "ee_pos": specs.Array((3,), float), } def action_spec(self): return specs.BoundedArray((self.model.nu,), float, minimum=-1.0, maximum=1.0)这里有几个细节要提醒:dm_env.restart表示环境从新回合开始,dm_env.transition表示普通中间步,timestep.last()对应终止步,需要用dm_env.termination。连续控制任务里,我一般让环境跑固定回合长度,到时用termination结束。另外action_spec必须跟data.ctrl的维度一致,否则dm_control内部校验会直接报错。
5.4 与Stable-Baselines3/Gymnasium的桥接
Stable-Baselines3本身不直接识别dm_env,所以接入PPO时有两种常用路线:要么把dm_env包装成Gymnasium接口,要么直接用Gymnasium内置的MuJoCo环境。我实际训练时,如果只是做实验,优先用内置环境,比如HalfCheetah-v4这种,开箱即用:
import gymnasium as gym from stable_baselines3 import PPO env = gym.make("HalfCheetah-v4", render_mode=None) model = PPO("MlpPolicy", env, verbose=1) model.learn(total_timesteps=1_000_000)如果要跑自己的机器人,就需要写一个从dm_env到 gymnasium.Env的wrapper,重点是把reset的返回格式改成(obs, info),把step的返回改成(obs, reward, terminated, truncated, info)。这个wrapper几十行代码就能写完,最值得注意的地方是,dm_env终止时返回的观测可能只是过渡值,Gymnasium的terminated语义要求这一步的观测是有意义的,所以处理终止过渡时要么不取最后一步,要么提前调用reset并返回新回合初始观测。
6. 训练与部署中的性能调优和疑难杂症
6.1 无头渲染加速与mujoco.viewer耗电之谜
很多人在训练开始时习惯开着viewer看效果,然后跑一晚上发现不仅GPU占用高,训练速度还上不去。原因是viewer的实时渲染会不断消耗CPU/GPU资源,而且为了跟仿真循环同步,它在拖慢主循环。我现在的经验是:训练过程中一律不渲染,把环境里相关的render_mode设为None,只在训练结束后用一段独立的回放脚本加载现成的轨迹,逐帧渲染成视频。
如果你必须在服务器上做可视化,则要走无头渲染路线。MuJoCo支持EGL或OSMesa后端,在命令行前设置MUJOCO_GL=egl或MUJOCO_GL=osmesa,可以没有显示器的情况下渲染图像。EGL性能更好,但要求GPU环境;OSMesa是纯软件渲染,慢很多,但兼容性最好。远程调试的时候我一般用EGL加mujoco.viewer的launch_passive,注意要确保本机能转发窗口,或者直接把图像保存成文件再看。
6.2 子步长、控制频率与训练稳定性
在MuJoCo里,物理步长timestep是一个仿真步的时间,但RL环境通常要求固定控制频率。比如你希望策略每秒执行50次决策,也就是控制周期0.02秒;如果物理步长是0.002秒,那么每个控制周期内部要执行10次mj_step。这个“次数”就是nsubsteps。
我推荐在自定义环境里把控制频率和物理步长分开配置,让用户可以调节。原因很简单:如果控制频率太高,策略需要更长的时间域输入才能学会合理的时序动作;如果控制频率太低,则每一段决策间隔里动力学演化可能太剧烈,训练不稳定。机械臂操作任务我常用控制频率50Hz,物理步长2ms,nsubsteps=10;人形机器人行走我会把物理步长降到5ms,控制频率降到30-50Hz。
调试技巧:如果训练过程中出现“高频抖动”,尤其人形机器人容易站立不稳,多半是PD控制器的kp和kv没有配合好nsubsteps。减少nsubsteps会让单步决策间隔更短,等价于提高了控制频率,通常对稳定性有帮助,但会显著增加计算量,需要根据实验效果平衡。
6.3 常见报错清单与修复方案
我在各种机器上跑MuJoCo,遇到的报错类型其实高度重复,列一个最常见的表格给各位抄作业:
| 报错信息 | 可能原因 | 处理方式 |
|---|---|---|
DLL load failed while importing mujoco | 缺少VC运行库,或Python环境不干净 | 安装Visual C++ Redistributable,新建干净虚拟环境 |
GLFW error/OpenGL context creation failed | 无显示环境,或者显卡驱动问题 | 设置MUJOCO_GL=egl或osmesa,更新显卡驱动 |
XML parse error | MJCF文件路径或标签写错 | 用from_xml_string先排除文件读取问题,再逐标签检查 |
Timestep must be greater than zero | <option timestep>未设置或为0 | 显式设置timestep,比如0.002 |
KeyError: 'xxx'in dm_control suite | 任务名或domain名拼写错误 | 查阅suite.ALL_TASKS确认名称 |
assert data.ctrl.size == nu | action维度跟actuator数量不一致 | 检查MJCF中.actuator数量,以及action_spec().shape |
还有一个反复出现的诡异问题:在Linux服务器上装了mujoco后,import mujoco没问题,但运行一段代码后直接段错误。这种情况我在老版NVIDIA驱动加专用GPU环境下遇过几次,大概率跟OpenGL后端冲突有关,建议先试试MUJOCO_GL=osmesa,能跑就说明是GPU渲染路径的问题。
6.4 从仿真到真机的差距,训练前就值得关注
最后聊一个很多人忽略的点:MuJoCo仿真调得再好,也不可能100%复现真机。差距主要来自三方面:接触参数、摩擦力、延迟。
接触参数方面,MuJoCo的solref、solimp在默认场景下表现很好,但不同材质差异很大。比如我的机械爪抓金属工件,默认接触参数会显得“太硬”,摩擦也过于理想,真机上一抓就滑。训练前最好做个简单校准:用同一种材料分别做滑动和碰撞实验,调整摩擦系数到仿真里的力传感器读数和真机接近。别小看这一步,它对sim-to-real效果影响巨大。
延迟方面,MuJoCo仿真默认是零延迟决策,但真机从感知到执行往往有几十毫秒的通讯延迟。一个比较实际的做法是训练时在环境动作里加入随机动作延迟,比如随机延迟1-5个控制周期,让策略学会在不确定延迟下做出稳健动作。我这套方案在机械臂抓取任务上实验过,真机首次成功率比不做延迟训练大概提升了三成以上。
这些调优经验都是从一个个跑崩的实验里攒下来的。MuJoCo和dm_control本身只是工具,真正拉开计算效率差距的,是你对模型参数和仿真环境的设计。这套组合最值得投入时间的地方,不是玩法,而是深入理解接触模型和动力学如何影响你的强化学习任务。熟悉之后你会发现,很多训练不收敛的问题,根子其实早在仿真环境设置阶段就埋下了。