十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MuJoCo环境下实现PPO:从四个经典运动控制任务到连续动作空间调参实战

MuJoCo环境下实现PPO:从四个经典运动控制任务到连续动作空间调参实战 简介这份代码包基于PyTorch在Mujoco物理引擎中实现了PPOProximal Policy Optimization算法覆盖Ant-v2、Humanoid-v2、Hopper-v2、HalfCheetah-v2等经典连续控制环境适合强化学习初学者与研究人员快速上手策略梯度类算法。资源共13个文件包含4个Python脚本如PPO.py、main.py、model.py、parameters.py、4个训练效果图PNG、3个日志文本记录不同环境与超参数下的回报、1个README说明文档以及1个Hopper-v2模型文件压缩包整体约598KB轻量且结构清晰。已有1807人学习/下载热度不错。通过阅读README可了解环境配置、运行命令与参数调节方式附带日志和曲线图有助于对比训练效果、分析收敛趋势节省调参时间。同时工程结构完整可直接用python main.py --env_name Hopper-v2启动训练方便在此基础上扩展实验。1. 在 mujoco 环境下实现 PPO从四个经典运动控制任务开始MuJoCo 的更新频率、接触力结算和 gym 生态让它成为验证连续动作强化学习算法的默认场景。想在 mujoco 环境下实现 PPO最容易看的例子就是 Ant-v2 这类四足、单足、仿人的运动控制任务动作空间连续、每一毫秒的力矩都要通过物理引擎结算成下一步状态。这类任务真正难的不是 PPO 的公式而是环境安装、奖励信号量纲、early termination 和连续动作分布这几个点上。下面按“环境怎么装 → PPO 为什么会在这里起效 → 训练循环怎么写 → Humanoid-v2 怎么调参”的顺序把一套不依赖现成强化学习库也能复现的训练管线讲清楚。适合从 RL 理论切到工程验证的读者论文读了 clip 和 GAE 但没亲手动过 MuJoCo或者只是想评估物理引擎接入自家机器人仿真。全程以 Ant-v2、Hopper-v2、HalfCheetah-v2、Humanoid-v2 四个任务为靶子装好环境、调通主干、最后能看懂 loss 曲线背后到底发生了什么。2. PPO 为什么适合 MuJoCo连续动作、clip 目标与奖励信号2.1 clip 与广义优势估计PPO 的“近端”究竟限制了什么PPO 的核心不是某条特定损失而是对“策略更新步长不可控”做截断。连续控制任务的策略输出是一个分布更新时我们用新旧策略的似然比 r_t(θ) π_θ(a_t|s_t) / π_old(a_t|s_t) 来衡量一次更新走了多远。r_t 趋近 1 说明动作被选中的概率基本变化r_t 偏大时一次大步更新很容易把策略推到悬崖边下一轮 rollout 的数据分布直接崩溃。PPO 用 clip 把这个比值锁在 [1-ε, 1ε] 区间梯度上只保留符合改进方向的部分避免过激更新。当然后续还有 dual-clip 这类变体也是在 clip 机制上加保险基础的比值约束没有变。这个设计对 MuJoCo 尤其重要因为 MuJoCo 的动力学由 forward dynamics 按 500Hz 甚至 2kHz 的仿真步长推进动作每一毫秒都在改变关节力矩网络参数一点扰动都会被接触动力学放大。广义优势估计 GAE 负责平衡多步累积误差与 reward 尖刺λ 越靠近 1值函数估计方差越小、偏差越大λ 越靠近 0则反过来。MuJoCo 连续任务里我一般先把 λ 固定在 0.95再统一调 γ0.99这样 advantage 不会过早衰减也不会把噪声带进 critic 的回归目标。2.2 MuJoCo 的奖励与终止信号对策略追踪的影响这四个环境虽然出自同一个 gym 的 MuJoCo 系列但难度坡度完全不同。Hopper-v2 的奖励由站立推进速度、额外激励项与动作成本组成并且自带 unhealthy 终止摔倒或关节角度超限立刻 doneHalfCheetah-v2 偏向 x 方向前进速度叠加关节力矩惩罚且没有早停每次 episode 都要跑满 1000 步Ant-v2 在四足爬行基础上加入接触外力相关的奖励系数Humanoid-v2 则是高维、稀疏信号并存的典型episode 前半段 reward 几乎都是负的。所以策略早期是“先学会不扣分、再学会跑”而不是“一上来就把步子迈大”。PPO 的 entropy 正则在这里量级很关键设大了策略一直保持高随机性接触点乱颤设小了前期探索不足人形机器人根本站不起来。我通常把 entropy coef 当作最后一步调的动作先让它保持 0.0 跑通环境再根据 episode 长度曲线决定要不要加。2.3 连续动作空间的建模高斯策略 tanh 压缩的取舍MuJoCo 的动作空间默认范围是 [-1,1]策略网络输出一个实数后必须映射回这个区间。常见做法是输出均值 mean 与对数标准差 log_std 组成对角高斯分布再用 tanh 压缩到区间内。tanh 会改变分布形状理论需要在 log_prob 中补 Jacobian 修正。工程里很多实现省略这一步在 Humanoid-v2 这类 17 维动作任务上动作饱和时 log_prob 误差会被明显放大import torch import torch.nn as nn class GaussianPolicy(nn.Module): def __init__(self, obs_dim, act_dim, hidden256, log_std_min-2.0, log_std_max2.0): super().__init__() self.net nn.Sequential( nn.Linear(obs_dim, hidden), nn.Tanh(), nn.Linear(hidden, hidden), nn.Tanh(), nn.Linear(hidden, act_dim * 2) # mean 和 log_std 拼在一个输出层 ) self.act_dim act_dim self.log_std_min log_std_min self.log_std_max log_std_max def forward(self, obs, actNone): mean, log_std torch.chunk(self.net(obs), 2, dim-1) log_std log_std.clamp(self.log_std_min, self.log_std_max) std log_std.exp() dist torch.distributions.Normal(mean, std) if act is None: # 采样阶段 raw dist.rsample() # reparameterization 采样 action torch.tanh(raw) # 反函数恢复原分布 log_prob补偿 tanh 的 Jacobian log_prob dist.log_prob(raw) log_prob - torch.log(1.0 - action.pow(2) 1e-6) return action, log_prob.sum(-1) # 更新阶段把动作反变换回 tanh 前的空间 raw torch.atanh(act.clamp(-1 1e-6, 1 - 1e-6)) log_prob dist.log_prob(raw) log_prob - torch.log(1.0 - act.pow(2) 1e-6) return log_prob.sum(-1)这段代码把高斯策略、tanh 压缩、log_prob 修正压缩在一个类里。关键参数 log_std_min 与 log_std_max 决定策略最低探索强度Ant-v2 上我习惯把 log_std 上限设到 0让越学到后面的高斯分布越窄Humanoid-v2 则保留 log_std_max2.0用更大随机性顶住前期探索不足。采样用 rsample 而不是 sample是为了让策略梯度能够通过重参数化路径回传。3. 安装 mujoco 与加载 Ant-v2、HalfCheetah-v2 等环境的还原3.1 识别环境版本v2、v3、v4 的任务名差异MuJoCo 环境近几年经历了一次大迁移。标题里的 Ant-v2、Hopper-v2、HalfCheetah-v2、Humanoid-v2 是经典 gym 0.21 时代注册的任务名现代 gymnasium 则默认提供 Ant-v4、HalfCheetah-v4 这组替换名。若在 gymnasium 里直接执行gym.make(Ant-v2)会遇到 KeyError这是版本迁移里最常见的坑。先用一段小脚本识别当前环境有没有注册 v2 系列import gym # gym 0.21 查看注册项gymnasium 下大部分打印的是 v4 mu_envs [ name for name in gym.envs.registry.env_specs.keys() if Ant in name or Hopper in name ] print(\n.join(sorted(mu_envs)))如果打印结果只有 Ant-v4 而没有 Ant-v2说明手上是 gymnasium。要精确复现 v2 的 benchmark建议开一个独立 venv固定安装 gym0.21 与配套的 mujoco_py不想碰 mujoco_py 编译的话直接把任务名换成 Ant-v4 也能验证同一套 PPO 代码。v4 与 v2 的奖励函数略有差异但连续动作空间的建模和终止条件语义基本一致。3.2 Ubuntu 22.04 与 Windows 11 安装 mujoco 的最小路径安装 MuJoCo 本体最常见的问题是 libGL 缺失与 mujoco_py 编译失败。先给结论纯用官方 mujoco 包时目标是import mujoco成功用 gym 绑定环境时目标变成gym.make(Ant-v2)能返回环境。Ubuntu 22.04 推荐的安装路径是直接用 pip 装官方绑定不碰 mujoco_py 的源码编译依赖sudo apt update sudo apt install libgl1-mesa-glx libgl1-mesa-dev libosmesa6-dev patchelf pip install mujoco joblib python -c import mujoco; print(mujoco.__version__)libgl1-mesa-glx 解决libGL.so.1: cannot open shared object filelibosmesa6-dev 是 mujoco_py 编译时代备用的。新版 mujoco pip 包自带预编译的 libmujoco.so 与模型资源不需要手动下载 MuJoCo 解压到 ~/.mujoco。Windows 11 上多数人走 WSL2在 Ubuntu 22.04 里执行同样命令即可少数在原生 Windows 上跑报错通常集中在路径编码和 Visual C 运行库WSL2 会省掉这一层。3.3 最小验证脚本与四个任务的动作空间对比表写一个最小验证脚本把四个任务的观察维度与动作边界打出来import gym env_names [Ant-v2, Hopper-v2, Humanoid-v2, HalfCheetah-v2] for name in env_names: env gym.make(name) obs env.reset() print(f{name:15s} obs{obs.shape} act_space{env.action_space.shape}, fact_high{env.action_space.high[0]:.1f}) env.close()输出里能看到 Humanoid-v2 的 obs 维度是 376 维HalfCheetah 只有 17 维。策略网络输入输出宽度直接依赖这个表任务名动作维度观察维度核心奖励来源终止条件特征Ant-v28111前进速度 接触力惩罚不健康状态触发Hopper-v2311较高激励 控制成本摔倒即 unhealthyHalfCheetah-v2617前进速度与控制成本无早停跑满 1000 步Humanoid-v217376前进速度 - 控制成本躯干触碰地面这个维度表的意义是提前预估网络规模。Humanoid 的 17 维动作与 376 维 obs 意味着第一个全连接层会占全网络大半参数lr 如果沿用 Ant 的 3e-4会出现收敛慢或前期梯度路径太长的问题。3.4 训练前先检查的 4 个环境特性环境装好不等于可以直接训。我会在写算法前做四件事第一手动推一把 Ant 的关节确认 reset 后 step 计数递增排除 done 信号导致 seed 重置的隐藏问题。第二打印 HalfCheetah 前 100 步的 reward 分布看 reward scale 是稳定在个位数还是在百位数波动这决定 critic 的输出初始化方式。第三检查 Humanoid 的 terminated 与 truncated 语义MuJoCo v2 时代的重置逻辑与 gymnasium 不完全相同误判会让 GAE 的优势估计在 episode 边界处失真。第四统计随机策略下的 episode 长度中位数Hopper 常见只有几十到两百步H屁股这决定 rollout 长度应该取 1024 还是 2048。这些排查的产出不是代码而是超参起点。同一份 PPO 在 Ant 上稳定的超参直接撒到 Humanoid 上大概率出现 loss 正常但 return 纹丝不动的情况。把环境特性连同维度表放进实验笔记能快速拉开算法问题与环境问题的边界。4. 从零实现 PPO 训练管线采样、GAE 与 clip 更新4.1 一个不加框架依赖的训练循环骨架PPO 落到代码上只有三件事收集 rollout、计算 GAE、用 clip 更新策略与价值网络。先写核心更新器import torch import torch.nn.functional as F def ppo_update(policy, critic, buffer, opt, cfg): obs, act, ret, adv, old_logp buffer.sample() for _ in range(cfg.k_epochs): idx torch.randperm(len(obs)) for i in range(0, len(obs), cfg.batch_size): mb idx[i:i cfg.batch_size] sb, ab, rb, acb obs[mb], act[mb], ret[mb], adv[mb] logp policy(sb, ab) ratio (logp - old_logp[mb]).exp() surr1 ratio * acb surr2 torch.clamp(ratio, 1.0 - cfg.clip, 1.0 cfg.clip) * acb actor_loss -torch.min(surr1, surr2).mean() critic_loss F.mse_loss(critic(sb).squeeze(-1), rb) loss actor_loss 0.5 * critic_loss opt.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_( list(policy.parameters()) list(critic.parameters()), cfg.max_grad_norm) opt.step()ratio.exp() 来自新旧 log_prob 之差clip 上下限 1.2/0.8 由 cfg.clip 决定。actor_loss 取两种形式的较小值一种是原始 ratio 乘 advantage另一种是 clip 后的 ratio 乘 advantage这保证策略更新只在优势方向推进。critic_loss 用 MSE 拟合 return0.5 系数是 PPO 论文里的常见写法让价值网络更新强度相对小一点。注意 buffer 里的 advantage 需要先做标准化否则 critic 与 actor 的梯度尺度不一致。4.2 观察归一化与奖励尺度的处理MuJoCo 观察空间不同维度的量纲差异很大接触力动辄上千关节角度在负和正之间大幅摆动直接用会拖慢网络收敛。用 running mean 与 running std 做在线归一化不参与梯度回传class RunningNorm: def __init__(self, dim, clip10.0): self.mean np.zeros(dim, dtypenp.float64) self.var np.ones(dim, dtypenp.float64) self.n 0 self.clip clip def update(self, obs): self.n 1 delta obs - self.mean self.mean delta / self.n delta2 obs - self.mean self.var delta * delta2 def __call__(self, obs): std np.sqrt(self.var / max(self.n, 1) 1e-8) return np.clip((obs - self.mean) / std, -self.clip, self.clip)reward 的处理更微妙。不要在 rollout 里对每步 reward 做减均值因为 GAE 需要真实 reward 来维持 value 函数尺度。我的做法是先跑几百次随机策略统计 reward 均值与标准差让 advantage 初始 std 接近 1。对于接触力造成的 reward 尖峰反而要保留它否则 critic 学不到真实风险。4.3 四个环境的推荐超参数与调参边界参数Ant-v2Hopper-v2HalfCheetah-v2Humanoid-v2rollout steps2048204820484096minibatch size646464128clip 范围0.20.20.20.3lr3e-43e-43e-41e-4 至 3e-4entropy coef0.00.00.010.005GAE lambda0.950.950.950.97网络隐藏层[256,256][128,128][256,256][512,512]这张表的重点是理解边界而不是照抄。Hopper 的 3 维动作如果 entropy 设到 0.1单腿跳跃会变得不稳定所以表格里默认 0.0仅在 episode 长度长期不变时再引入。Humanoid 的 376 维 obs 有大量对称冗余隐藏层 256 也能跑但 512 在 GPU 上收敛更快。clip 0.3 在 Humanoid 上更容易逃出局部最优代价是 value loss 波动更大需要同步降低 lr。4.4 训练中途最容易踩的三个坑第一个坑是 early termination 导致 buffer 里 done 时空转。MuJoCo 的早期终止意味着“摔倒了就重置”此时下一个状态的 bootstrap value 不应进入 return 计算否则 critic 会对倒地的状态价值产生幻觉。在 rollout 循环里把 done 位置的 value 掩码直接置 0。第二个坑是 log_std 回退。策略训练两千步后如果 log_std 快速下降到 -2 以下观察是否加了 tanh 但没有补 Jacobian 修正。log_prob 被低估后ratio 误差会在每次 update 里指数放大最后表现为动作几乎不变但 loss 乱跳。第三个坑是梯度范数突然变 NaN。如果输入里没有 NaN检查是否混合使用 fp16 自动混合精度MuJoCo 接触力带来的大 reward 在 fp16 下容易让 critic loss 溢出。更隐蔽的诱因是 reward 里出现 inf通常来自接触力结算的分子溢出。在 ppo_update 入口加一句assert torch.isfinite(loss)能快速定位是哪一路梯度爆掉。5. Humanoid-v2 调参验证优势标量的诊断技巧5.1 Humanoid-v2 的高维观察与稀疏训练信号Humanoid-v2 是四个任务里唯一把奖励藏得很深的。376 维观察包含大量对称关节的共同速度与外力策略网络前几层很容易出现神经元死区。训练早期最典型的现象是 return 在零轴附近反复优势估计的尺度被值函数拉大拉小。我习惯把 critic 的最后一层偏置初始化为 0同时把 GAE lambda 调到 0.97减少前期价值 bootstrap 的偏差。Humanoid 的 17 维动作输出不做 tanh 修正时动作饱和区域的 log_prob 会偏小策略会错误地回避极限动作这是它比 Ant 更难收敛的一个隐藏原因。5.2 用 advantage 分位数与熵系数验证训练状态与其只盯 TensorBoard 的 mean return我更常看两个诊断信号。第一个是每次 ppo_update 后检查 raw advantage 的分位数def adv_diagnose(adv): p95 torch.kthvalue(adv.flatten(), int(0.95 * adv.numel())).values p5 torch.kthvalue(adv.flatten(), max(1, int(0.05 * adv.numel()))).values print(fadv_p95{p95:.3f} adv_p5{p5:.3f} std{adv.std():.3f})如果连续两轮 p95 与 p5 的绝对值都在下降但 return 没有回升说明优势正被价值函数吸收策略更新幅度不足优先把 clip 从 0.2 提到 0.25或把 minibatch 从 128 降到 64。如果 p95 突然从 2 跳到 20说明采样中遇到过大 reward这时要降 lr 而不是加熵。第二个是给 log_std 加一条松弛拉回机制。训练过程中记录每个 epoch 的策略熵若连续 20 轮熵单调下降到原来一半以下就把 log_std 下限从 -2 抬到 0等于人为保留一层固定探索。这个操作在 Humanoid-v2 上能避免策略在二十万步左右过早僵化。如果想把这套方案迁移到扫地机器人这类带轮子和 bump 传感器的 MuJoCo 场景核心调整是把 rollout steps 拉大到 8192因为轮式任务接触力更尖GAE 的 lambda 要降到 0.92 左右。反过来从 HalfCheetah 换回 Ant-v2 时先查 clip 是否触发过多而不是一上来改网络层数。v2 环境名整体换成 v4 后同样把 Humanoid-v2 的 batch size 从 64 提到 128这套 PPO 管线可以直接重跑出可比较的曲线。本文还有配套的精品资源点击获取
返回列表