简介:本资源是一份基于PyTorch实现的近端策略优化(PPO)强化学习算法代码包,专为MuJoCo物理仿真环境设计,支持Ant-v2、Humanoid-v2、Hopper-v2、HalfCheetah-v2等经典连续控制任务,面向强化学习初学者与进阶研究者,可用于算法复现、超参调优及策略训练效果对比。压缩包共13个文件,含4个核心Python脚本(如main.py、PPO.py、model.py)、4张训练曲线PNG图(直观展示各任务收敛过程)、3个日志txt文件(记录不同超参下的训练轨迹)、1份README.md说明文档及1个Hopper-v2模型文件,整体仅598KB,轻量易部署。已有1807人学习下载,提供开箱即用的完整训练流程:仅需执行python main.py --env_name Hopper-v2即可启动实验,并附详细参数配置、环境依赖说明与结果可视化逻辑,便于快速理解PPO在高维连续动作空间中的实现细节与工程落地要点。
1. 为什么在 MuJoCo 里跑 PPO 不是“调个库就完事”,而是要亲手拧紧每颗物理螺丝?
你不是在训练一个图像分类器——你在教一个虚拟的 Ant-v2 在 MuJoCo 的刚体动力学黑匣子里学会“用六条腿走路”。PPO 算法本身只负责决策逻辑,而 MuJoCo 才是那个决定“抬腿多高会打滑”“关节扭矩超限是否炸关节”“地面反作用力怎么算才不飘”的物理守门人。Ant-v2、Hopper-v2、Humanoid-v2 这些经典 benchmark,表面看是 Gym 风格的 env ID,实则背后绑定了特定版本的 XML 模型、精确到小数点后四位的惯性张量、带接触约束的碰撞几何体,以及 MuJoCo 2.3.1+ 强制启用的mj_contact精确求解器。我见过太多人 pip install gym[mujoco] 后直接 run PPO,结果 Humanoid-v2 走三步就原地跪倒、Hopper-v2 跳着跳着飞出仿真边界——不是 PPO 收敛失败,是 MuJoCo 的 contact solver 没配对、XML 的 frictionloss 设得太低、甚至 Windows 上 DLL 加载路径漏了一个斜杠。这不是玄学,是物理引擎和强化学习算法之间必须手动对齐的 7 类参数:接触模型、积分步长、观测空间缩放、奖励函数梯度敏感度、动作裁剪策略、状态归一化方式、以及最关键的——MuJoCo 的mjModel编译时精度模式(float32 vs float64)。本文只讲一件事:用 PyTorch 实现 PPO,在 MuJoCo 环境下让 Ant-v2 稳定行走超过 500 步,且 Humanoid-v2 能自主恢复站立,所有步骤可复现、所有坑有解法、所有参数有依据。
2. 从零构建 MuJoCo-PPO 流水线:环境准备、模型加载与观测对齐
2.1 Windows 11 下 MuJoCo 安装的硬核避坑三连击(非 conda 依赖)
MuJoCo 官方不再提供 Windows 预编译二进制包,但mujoco==3.1.2(最新稳定版)已支持 Windows 11 + Python 3.9–3.11。关键不是“装上”,而是“装得对”。常见翻车点:
- DLL 路径污染:
os.environ['MUJOCO_GL'] = 'glfw'必须在import mujoco前设置,否则 OpenGL 初始化失败报GLXBadContext(即使在 Windows); - License 文件位置错误:
MUJOCO_KEY_PATH必须指向.key文件全路径(不是目录),且文件名必须为mjkey.txt(旧版命名)或mujoco_license.txt(新版),大小写敏感; - Python 架构错配:x64 Python 必须配 x64 MuJoCo DLL;若用 Miniconda,务必
conda install -c conda-forge mujoco而非pip install mujoco,后者在 Windows 上常缺 glfw.dll。
# 推荐安装流程(PowerShell 管理员权限) $env:MUJOCO_KEY_PATH="C:\Users\YourName\.mujoco\mjkey.txt" conda install -c conda-forge mujoco python=3.10 pip install gymnasium # 注意:gym 已弃用,必须用 gymnasium提示:验证安装是否成功,运行以下代码不报
ImportError且能打印mujoco.__version__即可:import os os.environ['MUJOCO_GL'] = 'glfw' import mujoco print(mujoco.__version__) # 输出应为 3.1.2
2.2 Gymnasium 环境封装:为什么不能直接用gym.make("Ant-v2")?
MuJoCo v3.x 已移除所有*-v2环境别名(如Ant-v2),官方明确要求使用gymnasium.make()+ 显式 XML 路径。gymnasium的mujoco_v4系列环境(如"Ant-v4")底层仍调用 MuJoCo 2.3.1 模型,但v4版本修复了Humanoid-v2中 torso 自旋失控问题。必须放弃v2,拥抱v4——这是 2024 年实测唯一能让 Humanoid 稳定站立的版本。
import gymnasium as gym from gymnasium.envs.mujoco import AntEnv, HumanoidEnv # ✅ 正确:显式指定 v4 环境,强制使用 MuJoCo 3.1.2 兼容模型 env = gym.make("Ant-v4", render_mode="rgb_array") # 注意 render_mode 参数 # ❌ 错误:v2 已废弃,且在 MuJoCo 3.x 下触发 deprecated warning 并可能崩溃 # env = gym.make("Ant-v2") # 🔧 进阶:自定义 XML 路径(用于调试机械狗/四足机器人) # env = AntEnv( # xml_file="custom_ant.xml", # 你的修改版 XML # ctrl_cost_weight=0.5, # 动作惩罚权重,v4 默认 0.1 # use_contact_forces=False # 关闭 contact force 观测(减少噪声) # )参数说明:
render_mode="rgb_array":避免 GUI 窗口阻塞训练进程;后续用cv2.imshow()或imageio.mimsave()可视化;ctrl_cost_weight:控制能量消耗惩罚项,Ant-v4 默认0.1,但 Hopper-v4 建议调至0.01(否则跳跃无力);use_contact_forces=False:contact force 向量含高频噪声,PPO 对其敏感,关闭后收敛更稳(实测 Ant-v4 训练步数减少 23%)。
2.3 观测空间对齐:为什么原始 obs 是“毒药”,必须做三重归一化?
MuJoCo 返回的原始观测(env.observation_space.sample())是 raw state vector,包含:
- 关节角度(rad)、角速度(rad/s)、身体质心位置(m)、线速度(m/s);
- 但各维度量纲、量级、物理意义差异极大:
qpos[0](torso x 位移)范围 [-10,10],qvel[3](hip joint velocity)范围 [-20,20],cfrc_ext(外部接触力)可达 ±500N。
直接喂给神经网络会导致梯度爆炸、策略震荡。必须做三重处理:
| 处理层 | 作用 | 实现方式 | Ant-v4 示例 |
|---|---|---|---|
| 物理量纲归一化 | 消除单位差异 | obs[i] /= scale[i],scale 来自 MuJoCo model 的stat字段 | qpos[0]/10.0,qvel[3]/20.0 |
| 统计归一化(在线) | 抑制 episode 内 drift | running mean/std 更新(alpha=0.01) | obs_norm = (obs - mean) / (std + 1e-8) |
| 动作空间映射 | 将 [-1,1] action 映射到真实 torque | torque = action * model.actuator_gainadr | model.actuator_gainadr[0]给出 hip motor 增益 |
import numpy as np class ObsNormalizer: def __init__(self, obs_dim, alpha=0.01): self.mean = np.zeros(obs_dim) self.std = np.ones(obs_dim) self.alpha = alpha self.count = 1e-8 def update(self, obs): batch_mean = np.mean(obs, axis=0) batch_std = np.std(obs, axis=0) self.mean = self.alpha * batch_mean + (1 - self.alpha) * self.mean self.std = self.alpha * batch_std + (1 - self.alpha) * self.std self.count += len(obs) def normalize(self, obs): return (obs - self.mean) / (self.std + 1e-8) # 使用示例 normalizer = ObsNormalizer(obs_dim=env.observation_space.shape[0]) obs, _ = env.reset() obs_norm = normalizer.normalize(obs) # 归一化后的 obs注意:
ObsNormalizer必须在每个 rollout 中持续 update,不能只在 pretrain 阶段跑一遍——MuJoCo 环境的动态分布随策略变化剧烈,静态归一化会失效。
3. PyTorch PPO 核心实现:策略网络、优势计算与 clip 机制落地细节
3.1 Actor-Critic 网络结构:为什么用 Tanh 而不用 ReLU?为什么 critic 要加 dropout?
PPO 的 actor 输出连续动作(torque),必须保证输出范围匹配 MuJoCo 的actuator_ctrlrange。Ant-v4 的 hip motor range 是[-1,1],因此 actor 最后一层必须用tanh,而非ReLU(会截断负值导致策略偏置)。
import torch import torch.nn as nn class Actor(nn.Module): def __init__(self, obs_dim, act_dim, hidden_dim=256): super().__init__() self.net = nn.Sequential( nn.Linear(obs_dim, hidden_dim), nn.Tanh(), # ✅ 必须用 Tanh!ReLU 会丢失负向 torque nn.Linear(hidden_dim, hidden_dim), nn.Tanh(), nn.Linear(hidden_dim, act_dim), nn.Tanh() # 输出 [-1,1],与 MuJoCo actuator range 对齐 ) def forward(self, obs): return self.net(obs) class Critic(nn.Module): def __init__(self, obs_dim, hidden_dim=256): super().__init__() self.net = nn.Sequential( nn.Linear(obs_dim, hidden_dim), nn.Tanh(), nn.Dropout(0.1), # ✅ critic 加 dropout 防止过拟合 value function nn.Linear(hidden_dim, hidden_dim), nn.Tanh(), nn.Linear(hidden_dim, 1) ) def forward(self, obs): return self.net(obs).squeeze(-1)关键设计理由:
nn.Tanh()输出严格 ∈ [-1,1],与 MuJoCoactuator_ctrlrange完全一致,避免torch.clamp(action, -1, 1)引入梯度不连续;nn.Dropout(0.1)在 critic 中至关重要:MuJoCo 的 reward signal 稀疏(Ant 走一步只有 +1,摔倒 -100),value network 易记忆 noise,dropout 强制泛化;hidden_dim=256是 Ant/Hopper 的黄金值;Humanoid-v4 因 obs_dim=376,需升至512,否则 policy collapse。
3.2 GAE 优势计算:为什么gamma=0.99和lam=0.95是 MuJoCo 的“安全组合”
PPO 的核心是 clipped surrogate objective,而它的质量取决于优势估计A_t。MuJoCo 环境 timestep 短(Ant-v4max_episode_steps=1000),reward 稀疏,必须用 Generalized Advantage Estimation(GAE)平衡 bias-variance。
def compute_gae(next_value, rewards, dones, values, masks, gamma=0.99, lam=0.95): """ next_value: critic 输出的 next_state value (shape: [batch]) rewards: episode reward (shape: [T]) dones: done flag (shape: [T]) values: critic 输出的当前 states value (shape: [T]) masks: 1-dones (shape: [T]),用于 reset RNN hidden state """ gae = 0 advantages = torch.zeros_like(rewards) for t in reversed(range(len(rewards))): delta = rewards[t] + gamma * next_value * masks[t] - values[t] gae = delta + gamma * lam * masks[t] * gae advantages[t] = gae next_value = values[t] return advantages # 使用示例(在 rollout 后) advantages = compute_gae( next_value=critic(obs_next).detach(), rewards=torch.tensor(reward_batch), dones=torch.tensor(done_batch), values=values_batch, # critic(obs_batch) masks=1 - torch.tensor(done_batch), gamma=0.99, lam=0.95 )参数选择依据:
gamma=0.99:MuJoCo 任务 discount 应较高,因 long-horizon control(Humanoid 站立需 200+ steps);lam=0.95:在 bias(lam→0 时退化为 MC)和 variance(lam→1 时退化为 TD)间折中;实测lam=0.95在 Ant-v4 上比0.99收敛快 1.8×,且无 policy oscillation;masks=1-dones:必须用 mask 重置 GAE 累积,否则 episode 截断处A_t严重失真(我曾因此让 Hopper-v4 在第 300 步突然拒绝跳跃)。
3.3 Clipped Surrogate Objective:clip_eps=0.2 的物理含义与动态调整策略
PPO 的 loss 函数为: $$ L^{CLIP}(\theta) = \mathbb{E}_t \left[ \min\left( r_t(\theta)\hat{A}_t,; \text{clip}(r_t(\theta), 1-\varepsilon, 1+\varepsilon)\hat{A}_t \right) \right] $$ 其中r_t(theta) = pi_theta(a_t|s_t) / pi_theta_old(a_t|s_t)是重要性采样比。
def ppo_loss(actor, old_log_probs, obs, actions, advantages, clip_eps=0.2): log_probs = actor.get_log_prob(obs, actions) # 自定义方法,返回 log π(a|s) ratio = torch.exp(log_probs - old_log_probs) surr1 = ratio * advantages surr2 = torch.clamp(ratio, 1.0 - clip_eps, 1.0 + clip_eps) * advantages loss = -torch.min(surr1, surr2).mean() return lossclip_eps=0.2 的物理含义:
- 它限制策略更新步长:
ratio ∈ [0.8,1.2]意味着新策略在任意(s,a)下的概率不能比旧策略低 20% 或高 20%; - 在 MuJoCo 中,这等价于“禁止策略突然改变 torque 方向”——Ant 的 hip joint 若从 +0.8 torque 突变到 -0.8,必然摔倒;
- 动态调整技巧:训练初期(前 100K steps)用
clip_eps=0.3加速探索;中期(100K–500K)降为0.2;后期(>500K)用0.1微调稳定性(Humanoid-v4 站立需此阶段)。
4. MuJoCo-PPO 训练全流程:rollout、buffer、update 与硬件级优化
4.1 Rollout 策略:为什么用 2048 steps/episode 而不是 1000?
MuJoCo 的max_episode_steps=1000是硬上限,但 PPO 需要足够长的 trajectory 计算 GAE。若每 episode 只采 1000 steps,则 GAE 在末尾 200 steps 的 variance 极高(因next_value估计不准)。最佳实践是:用n_steps=2048固定长度 rollout,遇到 done 则 padding + mask。
def rollout(env, actor, n_steps=2048): obs_list, act_list, logp_list, rew_list, done_list = [], [], [], [], [] obs, _ = env.reset() for _ in range(n_steps): obs_tensor = torch.tensor(obs, dtype=torch.float32).unsqueeze(0) with torch.no_grad(): action = actor(obs_tensor)[0].numpy() log_prob = actor.get_log_prob(obs_tensor, torch.tensor(action)).item() next_obs, reward, terminated, truncated, _ = env.step(action) done = terminated or truncated obs_list.append(obs) act_list.append(action) logp_list.append(log_prob) rew_list.append(reward) done_list.append(done) obs = next_obs if done: obs, _ = env.reset() return ( np.array(obs_list), np.array(act_list), np.array(logp_list), np.array(rew_list), np.array(done_list) ) # 使用示例 obs_batch, act_batch, logp_batch, rew_batch, done_batch = rollout(env, actor)为什么 2048?
- 2048 是 2 的幂,GPU batch 计算友好;
- 覆盖 2×
max_episode_steps,确保 GAE 在中间 1000 steps 内 bias < 0.05(实测数据); - Ant-v4 平均 episode length ≈ 650,2048 可容纳 3 个完整 episode + 1 个截断,信息密度最优。
4.2 Buffer 设计:为何不用ReplayBuffer,而用RolloutBuffer?
PPO 是 on-policy 算法,旧数据立即失效。ReplayBuffer(DQN/SAC 用)在此完全错误——它会把已过期的(s,a,r,s')喂给 critic,导致 value overestimation。必须用RolloutBuffer:每次 update 后清空,只存最近一次 rollout。
class RolloutBuffer: def __init__(self, obs_dim, act_dim, n_steps=2048): self.obs = np.zeros((n_steps, obs_dim), dtype=np.float32) self.actions = np.zeros((n_steps, act_dim), dtype=np.float32) self.log_probs = np.zeros(n_steps, dtype=np.float32) self.rewards = np.zeros(n_steps, dtype=np.float32) self.dones = np.zeros(n_steps, dtype=bool) self.values = np.zeros(n_steps, dtype=np.float32) self.advantages = np.zeros(n_steps, dtype=np.float32) self.returns = np.zeros(n_steps, dtype=np.float32) self.pos = 0 def add(self, obs, action, log_prob, reward, done, value): self.obs[self.pos] = obs self.actions[self.pos] = action self.log_probs[self.pos] = log_prob self.rewards[self.pos] = reward self.dones[self.pos] = done self.values[self.pos] = value self.pos += 1 def clear(self): self.pos = 0提示:
RolloutBuffer不需要sample()方法——PPO update 时直接全量使用 buffer 数据,无需 shuffle(on-policy 要求 temporal consistency)。
4.3 Update 阶段:为什么用 10 epochs × 64 batch size,且 critic loss 权重为 0.5?
PPO 的 update 包含 actor loss + critic loss + entropy bonus: $$ L = L^{CLIP} + c_1 L^{VF} - c_2 S[\pi_\theta] $$
def update_ppo(actor, critic, buffer, optimizer, ent_coef=0.01, vf_coef=0.5): obs = torch.tensor(buffer.obs, dtype=torch.float32) actions = torch.tensor(buffer.actions, dtype=torch.float32) old_log_probs = torch.tensor(buffer.log_probs, dtype=torch.float32) advantages = torch.tensor(buffer.advantages, dtype=torch.float32) returns = torch.tensor(buffer.returns, dtype=torch.float32) # 分 batch 训练(防止 OOM) dataset = torch.utils.data.TensorDataset(obs, actions, old_log_probs, advantages, returns) dataloader = torch.utils.data.DataLoader(dataset, batch_size=64, shuffle=True) for epoch in range(10): # ✅ 10 epochs 是 MuJoCo 的经验值 for batch in dataloader: obs_b, act_b, old_logp_b, adv_b, ret_b = batch # Actor loss logp_b = actor.get_log_prob(obs_b, act_b) ratio = torch.exp(logp_b - old_logp_b) surr1 = ratio * adv_b surr2 = torch.clamp(ratio, 0.8, 1.2) * adv_b actor_loss = -torch.min(surr1, surr2).mean() # Critic loss value_b = critic(obs_b).squeeze() critic_loss = 0.5 * (value_b - ret_b).pow(2).mean() # Entropy bonus dist = actor.get_dist(obs_b) entropy_bonus = dist.entropy().mean() loss = actor_loss + vf_coef * critic_loss - ent_coef * entropy_bonus optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(actor.parameters(), 0.5) torch.nn.utils.clip_grad_norm_(critic.parameters(), 0.5) optimizer.step()参数依据:
epochs=10:太少(<5)导致 policy 更新不足,Ant-v4 无法学会协调腿;太多(>15)引发 overfitting,Hopper-v4 跳跃高度下降;vf_coef=0.5:critic loss 权重必须显著高于 actor(0.5 vs 1.0),否则 value error 会拖垮 GAE 计算;grad_norm=0.5:MuJoCo 的 reward gradient 极大(摔倒 -100),不 clip 必然 NaN。
5. MuJoCo-PPO 常见问题排查:5 类致命翻车现场与血泪解法
5.1 现象:Ant-v4 走 5 步后原地旋转,reward 停在 5–10 不涨
原因:qpos[2](torso yaw angle)未归一化,导致 actor 学会“用 yaw 控制方向”而非“用腿推进”,物理上违反运动学约束。
解决:在观测中移除qpos[2](yaw),或用cos(yaw), sin(yaw)替代(保持周期性)。
# 修改 obs 处理 obs_proc = np.concatenate([ obs[:2], # x,y pos obs[3:15], # qpos without yaw (index 2) obs[15:] # qvel, cfrc_ext... ])5.2 现象:Humanoid-v4 训练 2M steps 后 reward 波动剧烈(±300),无法稳定站立
原因:ctrl_cost_weight=0.1过高,导致 policy 过度抑制 torque,站立时 torso 颤抖;同时use_contact_forces=True引入地面噪声。
解决:
- 将
ctrl_cost_weight降至0.01; use_contact_forces=False;- 在 actor 输出后加 low-pass filter:
action_filtered = 0.9 * action_prev + 0.1 * action_new。
5.3 现象:Hopper-v4 跳跃高度越来越低,最终只能原地弹跳
原因:GAE 的gamma=0.99在单腿跳跃任务中 discount 过重,policy 认为“跳不高没关系,反正后面还有 step”。
解决:对 Hopper 专用gamma=0.995,并增加 reward shaping:reward += 0.01 * obs[0](鼓励 x 方向前进)。
5.4 现象:训练 loss 为 NaN,critic输出 inf
原因:obs中含inf或nan(MuJoCo 在 contact failure 时返回nan),未清洗即送入网络。
解决:在 rollout 中添加 obs 检查:
if np.any(np.isnan(obs)) or np.any(np.isinf(obs)): obs, _ = env.reset() # 重置环境,丢弃坏帧5.5 现象:Windows 上训练卡死在env.step(),CPU 占用 100%,GPU 空闲
原因:gymnasium的render_mode="rgb_array"在 Windows + glfw 下触发 OpenGL context lock。
解决:
- 设置
os.environ['MUJOCO_GL'] = 'egl'(需 NVIDIA 驱动支持); - 或彻底禁用渲染:
render_mode=None,仅在 eval 阶段用render_mode="rgb_array"; - 或改用
env = gym.make("Ant-v4", render_mode=None)。
6. 进阶技巧:从训练到部署——保存、加载与跨平台推理实战
6.1 模型保存:为什么.pt文件必须包含mujo_model和obs_normalizer?
MuJoCo 的 policy 部署不是“加载 PyTorch 模型就行”。Actor网络只输出 action,但 action 必须经 MuJoCo 的model映射到真实 torque。若只存actor.state_dict(),加载后无法知道actuator_gainadr,导致 torque 缩放错误。
def save_checkpoint(actor, critic, normalizer, model, path): torch.save({ 'actor_state_dict': actor.state_dict(), 'critic_state_dict': critic.state_dict(), 'normalizer_mean': normalizer.mean, 'normalizer_std': normalizer.std, 'mujo_model_xml': model.get_xml(), # 保存 XML 字符串 'mujo_model_binary': model._userdata # 保存编译后 model(可选) }, path) def load_checkpoint(path, env): checkpoint = torch.load(path) # 重建 model(必须用同版本 MuJoCo) model = mujoco.MjModel.from_xml_path(env.env.model_path) # 或用 checkpoint['mujo_model_xml'] # 加载网络 actor.load_state_dict(checkpoint['actor_state_dict']) critic.load_state_dict(checkpoint['critic_state_dict']) # 恢复归一化器 normalizer.mean = checkpoint['normalizer_mean'] normalizer.std = checkpoint['normalizer_std']注意:
model_path在gymnasium.make("Ant-v4")中默认为gymnasium/envs/mujoco/assets/ant.xml,但不同 gymnasium 版本路径可能变,务必用env.env.model_path获取真实路径。
6.2 跨平台推理:如何在无 GPU 服务器上运行训练好的 PPO?
MuJoCo-PPO 的 inference 只需 CPU + PyTorch,无需 CUDA。但需注意:
torch.set_num_threads(1):MuJoCo 的mj_step()是单线程,多线程反而降低性能;actor.eval()+torch.no_grad():关闭 dropout 和 grad;obs_normalizer必须用训练时 final mean/std,不能重新 fit。
def evaluate_policy(actor, normalizer, env, n_episodes=10): torch.set_num_threads(1) actor.eval() total_reward = 0 for _ in range(n_episodes): obs, _ = env.reset() episode_reward = 0 while True: with torch.no_grad(): obs_tensor = torch.tensor(obs, dtype=torch.float32).unsqueeze(0) obs_norm = normalizer.normalize(obs_tensor) action = actor(obs_norm)[0].numpy() obs, reward, done, _, _ = env.step(action) episode_reward += reward if done: break total_reward += episode_reward return total_reward / n_episodes # 在 CPU 服务器上运行 device = torch.device('cpu') actor.to(device) avg_reward = evaluate_policy(actor, normalizer, env) print(f"Average reward: {avg_reward:.2f}") # Ant-v4 > 3500 即达标6.3 机械狗/四足机器人迁移:3 步适配自定义 XML
标题里的 “mujoco torch机械狗” 是真实需求。将 Ant-v4 PPO 迁移到 custom quadruped,只需三步:
- XML 替换:用你的
quadruped.xml替换ant.xml,确保<actuator>中ctrlrange与torque单位一致(MuJoCo 默认 N·m); - 观测对齐:检查
qpos/qvel维度,用env.unwrapped.model.nq获取实际自由度,调整 actor 输入层; - 奖励重设计:Ant 的
forward_reward不适用于机械狗,改用body_velocity_x+energy_penalty+contact_stability(脚掌 contact force variance < 5N²)。
<!-- quadruped.xml 中关键修改 --> <default> <motor ctrlrange="-1 1" /> <!-- 与 PPO actor 输出 [-1,1] 对齐 --> </default> <worldbody> <body name="torso" ...> <geom type="box" ... /> <body name="front_left_leg" ...> <joint name="hip_joint" type="hinge" ... /> <geom ... /> <body name="knee" ...> <joint name="knee_joint" type="hinge" ... /> </body> </body> </body> </worldbody>我当年在实验室调机械狗,最大的教训是:不要迷信“SOTA 算法”,MuJoCo 的 XML 才是 ground truth。一个没配对的<inertial>标签,能让 PPO 训练 10 天却走不出 1 米。把mujoco_py升级到mujoco3.1.2 后,我重写了全部 contact 参数,才让四足机器人在斜坡上稳定行走。希望帮到你。
本文还有配套的精品资源,点击获取