news 2026/9/12 14:17:13

在MuJoCo中实现PPO:从环境安装到调参的完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
在MuJoCo中实现PPO:从环境安装到调参的完整指南

简介:面向希望在Mujoco物理仿真环境中实践强化学习的开发者,这份资源提供了基于PyTorch的PPO算法实现,覆盖Ant-v2、Humanoid-v2、Hopper-v2、HalfCheetah-v2等常见连续控制任务。压缩包共13个文件,大小仅598KB,包含4个Python脚本(主程序、模型定义、参数配置与PPO核心逻辑)、4张训练效果示意图、3份运行日志、1份README说明及1个Hopper-v2相关数据文件,代码结构清晰,便于按模块阅读和复用。已有1807人浏览学习,适合正在入门或调试PPO算法的学生与研究人员。通过阅读README可快速掌握各环境下的启动方式,结合日志和图像可直观对比不同超参数(如clip系数、beta值)对训练曲线的影响,是一份轻量且完整的PPO算法学习与实验参考。

1. 在 MuJoCo 里跑 PPO:Ant-v2 到 Humanoid-v2 的差距比想象中大

很多人把 PPO 代码从仓库里 clone 下来,在 HalfCheetah-v2 上跑出漂亮曲线,然后原封不动换到 Ant-v2 或 Humanoid-v2,却发现 reward 曲线不动,甚至 loss 直接变成 NaN。反直觉的结论是:问题很少出在 PPO 本身,而是出在 MuJoCo 物理引擎的仿真特性和 v2 环境的 reward 结构上——提前终止、接触力观测、健康奖励这几个细节,决定了同一套超参数在四个环境里截然不同的表现。这篇文章按「环境安装 → 算法设计 → 完整实现 → 评估调参」的顺序,把在 mujoco 环境下实现 PPO 的通用做法讲清楚,并把 Ant-v2、Hopper-v2、HalfCheetah-v2、Humanoid-v2 各自要改的参数单独列出。第一次写连续动作策略的人能照抄,写过 PPO 但换环境就调不动的人可以拿它对照检查。

2. MuJoCo 环境安装与 v2 注册:Ubuntu 22.04、Windows 11 和版本匹配

先说一个最容易踩的坑:Ant-v2、Hopper-v2、HalfCheetah-v2、Humanoid-v2 并不是新版 mujoco python 包自带的。它们属于 gym 的经典 MujocoEnv 环境族,背后是 OpenAI 时代的 mujoco-py 绑定库;DeepMind 后来发布的mujoco包对应的是 gymnasium 里的 v3/v4 环境。装之前先决定走哪条路线:要的是指定环境名 v2,就走 mujoco-py 老路线;只是想把 PPO 在 MuJoCo 物理引擎上跑起来,新版mujoco+ gymnasium 完全够用,算法代码一字不用改,只需把 done 拆成 done 和 truncated。

MuJoCo 物理引擎本身从 2.x 到 3.x 改过接触求解器和渲染接口,gym 0.26 之后又换了step的返回结构,这两层版本叠在一起,就是「安装 mujoco 常见问题」里大半报错的来源。所以我先给出一条我在 Ubuntu 22.04 上反复验证过的安装路径。

2.1 Ubuntu 22.04 上的 mujoco 安装和部署:经典 v2 路线的四个步骤

# 1. 系统依赖:mujoco-py 编译时需要 OSMesa 和 OpenGL 头文件 sudo apt update sudo apt install -y libosmesa6-dev libgl1-mesa-dev libglfw3 libglfw3-dev patchelf # 2. 下载 mujoco210 的 Linux 二进制并解压到 ~/.mujoco/mujoco210 # 目录里需要有 bin/、include/、model/ 三个子目录,解压后不要移动 # 3. 安装绑定库和 gym,注意 v2 环境要配 gym 0.21.x pip install "gym==0.21.0" "mujoco-py==2.1.2.14" # 4. 运行时环境变量,建议写进 ~/.bashrc export MUJOCO_PY_MUJOCO_PATH=$HOME/.mujoco/mujoco210 export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$HOME/.mujoco/mujoco210/bin

四步的职责分别是:第一步解决编译期依赖,mujoco-py 是 Cython 写的,它要链接 OSMesa 来做离屏渲染,缺了libosmesa6-dev会在 import 阶段报GL相关错误;第二步的 mujoco210 是 DeepMind 发布的最后一个 2.x 二进制包,mujoco-py 的 2.1.x 长期用它做后端,版本不能换成 3.x,否则 API 对不上;第三步里gym==0.21.0的注册表里才有带-v2后缀的环境,新版 gym 已经把 mujoco 环境整组移除;第四步的MUJOCO_PY_MUJOCO_PATH是 mujoco-py 找物理库的位置,不导出会在运行时抛mujoco_py.mujoco_py.MjLoadError

Windows 11 上我分两种情况处理。明确要 v2 环境的话,建议直接在 Windows 11 里开 WSL2,装 Ubuntu 22.04 后按上面命令走,最省时间;只是要跑 PPO,可以原生pip install mujoco gymnasium,然后gymnasium.make("Ant-v4"),物理引擎是同一个,只是环境名从 v2 换成了 v4。

2.2 冒烟测试:确认环境注册与物理步进真的能跑

装完别急着写算法,先用一段脚本把四个环境的注册、观测维度和随机策略步进都验一遍:

import gym for env_name in ["Ant-v2", "Humanoid-v2", "Hopper-v2", "HalfCheetah-v2"]: env = gym.make(env_name) obs = env.reset() total_reward = 0.0 for _ in range(100): action = env.action_space.sample() obs, reward, done, info = env.step(action) total_reward += reward if done: break print(f"{env_name}: obs={obs.shape}, act={env.action_space.shape}, " f"100步随机return={total_reward:.2f}, done={done}") env.close()

这段脚本的价值在于把「环境问题」和「算法问题」在第一时间切开。obs的 shape 要和标准值对齐:Ant-v2 是 (111,),Humanoid-v2 是 (376,),Hopper-v2 是 (11,),HalfCheetah-v2 是 (17,);对不上说明你拿到的不是经典 v2 环境而是别的版本。done在 100 步内就变 True 是正常的,Ant 和 Humanoid 随机策略很容易摔倒。随机策略 100 步 return 为负也是正常的,接触惩罚和控制代价会让前期回报很难看,不要据此怀疑环境装坏了。

2.3 四个 v2 环境的观测、动作与奖励差异:一张表看明白

gym 0.21 注册表里这四个环境的默认配置差异很大,我把关键项列出来,后面所有调参都围绕这张表展开:

环境观测维度动作维度健康奖励提前终止条件对 PPO 影响最大的点
HalfCheetah-v2176无(仅 1000 步超时)奖励无上界,控制代价权重 0.1
Hopper-v2113每步 +1跌倒或躯干角度越界(z 不在 0.7~1.2、角度超 ±0.2)奖励被健康项垫底,前期不会太负
Ant-v21118每步 +1z 不在 0.2~1.0观测含 80+ 维接触力,量纲很大
Humanoid-v237617每步 +5躯干 z 不在 1.0~2.0 等终止频繁,17 维力矩输出易发散

这里有几个细节值得展开。第一,HalfCheetah 是四个环境里唯一没有终止条件的,它只在 1000 步被 TimeLimit 截断,所以 GAE 里所有 done 都按「超时」处理即可;其余三个环境都有健康终止,必须区分「摔倒」和「超时」。第二,Ant 和 Humanoid 的观测里包含接触力向量,量级和关节角的量级差出几个数量级,如果不做观测归一化,PPO 的价值网络前几千步基本在学噪声。第三,v2 和后续 v3/v4 的差异主要在 reward 组成,v3 起对接触惩罚做过调整,如果你在网上找到的基线代码用的是 v3/v4,直接套到 v2 上奖励曲线会不一样,这是版本问题不是算法问题。

2.4 mujoco 安装常见问题:动态库、渲染后端与注册失败排查

我自己在不同机器上踩过的问题,按现象和修法列成一张速查表:

报错或现象原因处理方式
import 时ImportError: libGL.so.1缺 OpenGL 动态库sudo apt install libgl1-mesa-dev libgl1
mujoco-py 编译报找不到glfw.h缺 GLFW 开发头文件sudo apt install libglfw3-dev
运行时MjLoadError: could not open mujoco210MUJOCO_PY_MUJOCO_PATH没指向 mujoco210 根目录检查路径必须直接包含bin/mujoco210include/
gym.error.NamespaceNotFound: Ant-v2gym 版本过新,v2 注册表被移除降到gym==0.21.0,并确认import mujoco_py不报错
训练时渲染黑屏或报 GL 初始化失败无头服务器的渲染后端不对mujoco-py 用MUJOCO_GL=osmesa,新版 mujoco 用MUJOCO_GL=egl

最后补一句:如果import mujoco_py本身成功但gym.make仍然找不到 v2 环境,最常见的原因是 mujoco-py 的 import 在 gym 注册时失败被静默跳过了。先在 Python 里单独执行import mujoco_py,把这一层查干净再动 gym。

3. PPO 的连续动作三件套:高斯策略、GAE 和 clip 边界

环境装好后,我们来把 PPO 在 MuJoCo 上能跑起来依赖的三个机制讲透:高斯策略给出可微的 log 概率,GAE 用 TD 误差递推压低方差,clip 目标限制单步更新幅度。这三个设计任何一个写错,reward 曲线都不会好看。这一章讲原理,第四章给完整代码。

3.1 高斯策略:均值头加独立 log_std,ppo 连续动作代码讲解的标准结构

离散动作的 PPO 用 categorical 分布在动作空间上直接取概率;连续动作环境里动作是一个向量,策略必须是一个分布,最常用的是各维度独立的高斯分布,即π(a|s) = N(μ(s), σ²)。μ 由网络输出,σ 有两种接法:一种是从网络再分出一个头输出状态相关的方差,另一种是做一个独立的log_std参数,对所有状态共享。

我在 MuJoCo 任务上几乎总是用第二种。原因有三个:参数少,状态无关的 σ 不会因为某条异常状态把方差顶到爆炸;探索强度全局可控,想调探索只需改一个向量;训练初期更稳,状态相关的方差头在小样本下容易对刚见过的状态过拟合,造成动作越界。这也是网上 ppo 连续动作代码讲解里最常见的写法:

self.mean_head = nn.Linear(hidden, act_dim) self.log_std = nn.Parameter(torch.full((act_dim,), -0.5)) def dist(self, obs): hidden = self.trunk(obs) mean = self.mean_head(hidden) std = self.log_std.exp().expand_as(mean) return torch.distributions.Normal(mean, std)

log_std初始化为 -0.5 对应 σ≈0.6,初始探索既不会太保守也不会太猛。注意正态分布采样理论上无界,而 MuJoCo 环境的action_space已经约束在 [-1, 1],所以env.step之前必须np.clip(action, low, high),否则仿真求解器会拿到越界力矩。这一步是后面 NaN 的头号来源,务必养成习惯。计算 log 概率时用log_prob(action).sum(-1),因为各动作维是独立的,联合概率的对数是各维之和。

3.2 GAE 与 v2 环境的终止语义:摔倒和超时必须分开处理

GAE(Generalized Advantage Estimation)的核心公式是:

δ_t = r_t + γ·V(s_{t+1}) − V(s_t)A_t = Σ_{k≥0} (γλ)^k · δ_{t+k}

直观理解:δ 是单步 TD 误差,把未来 K 步的 TD 误差按(γλ)^k衰减叠加,λ 在偏差和方差之间做权衡。λ=0 退化成一步 TD,方差小但偏差大;λ=1 等价于蒙特卡洛 return,偏差小但方差大。MuJoCo 任务里 γ=0.99、λ=0.95 是几乎不动摇的默认值。

真正的坑在 done 的语义。gym 0.21 里这组环境的done=True分两种情况:摔倒导致的健康终止,和达到 1000 步的 TimeLimit 截断。如果都把 done 当成真实终止,最后一步的 value 不做 bootstrap,价值网络会系统性低估后续回报,reward 曲线尾部掉头;如果都当成截断去 bootstrap,摔倒后那个并不存在的"未来"会被 value 网络学进去,损失函数噪声显著变大。正确做法是从info里把截断标志取出来单独判断:

def compute_gae(rewards, falls, values, last_value, gamma=0.99, lam=0.95): """falls[i]=1 表示第 i 步是真实终止(摔倒),超时不算 fall。""" advantages = torch.zeros_like(values) gae = 0.0 for t in reversed(range(len(rewards))): if t == len(rewards) - 1: next_value = last_value else: next_value = values[t + 1] delta = rewards[t] + gamma * next_value * (1 - falls[t]) - values[t] gae = delta + gamma * lam * (1 - falls[t]) * gae advantages[t] = gae returns = advantages + values return advantages, returns

采样时用truncated = info.get("TimeLimit.truncated", False),然后fall = done and not truncated,再把 fall 存进缓冲区。代码里(1 - falls[t])的作用是:真实终止时把 next_value 和后续 GAE 全部置零,也就是不 bootstrap;超时则正常往后传。GAE 要从 buffer 尾部往前算,因为每一步的 advantage 依赖未来的 δ,方向反了结果全错。用 gymnasium 的话,donetruncatedstep的两个独立返回值,逻辑更直白,但算法代码里那一处(1 - falls)依然要保留。

3.3 clip 目标与 dual-clip ppo:当 ratio 撞上边界

PPO 的策略损失是L = E[min(ratio·A, clip(ratio, 1−ε, 1+ε)·A)],其中ratio = π_new(a|s) / π_old(a|s),ε 默认 0.2。min 的作用是:当优势为正、新策略把某动作概率抬高到 ratio 超过 1.2 时,梯度不再继续放大这一步,防止单次更新把策略推过头;优势为负时同理,限制惩罚幅度。clip 的作用本质上是给每次参数更新上一道保险,这也是 PPO 相比 TRPO 更实用的原因——不要显式求 KL,只约束 surrogate。

在这基础上还有一个变体叫 dual-clip ppo,它专门处理负优势侧的极端样本。标准 clip 在优势为负时只限制 ratio 的上界 1+ε,但如果某条 transition 的 reward 方差极大,ratio 在另一侧也可能异常,负优势配上极小 ratio 会让 surrogate 出现一个假的高值。dual-clip 的做法是给负优势侧再加一个更宽的下界 c 参与裁剪:

l1 = ratio * adv l2 = torch.clamp(ratio, 1.0 - clip, 1.0 + clip) * adv if dual_clip: pg_loss = -(torch.where(adv > 0, torch.min(l1, l2), torch.max(l1, l2))).mean() else: pg_loss = -torch.min(l1, l2).mean()

优势为正取 min、为负取 max,这样两个方向的梯度都被限制在裁剪区间内。我一般只在 reward 方差大的环境(比如 HalfCheetah-v2,单步 reward 幅度受速度影响很大)里把 dual_clip 打开,代价是收敛略慢但曲线更稳;Ant-v2 和 Hopper-v2 用不用差别不大。另外,训练时把ratio.mean()打出来,如果它持续明显偏离 1,说明 clip 边界被频繁触发,学习率应该减半而不是去调 ε。

3.4 网络规模与初始化:为什么两层 256 足够

这四个环境里观测维度最大的是 Humanoid-v2 的 376 维,但也完全不需要 RNN 或 Transformer,两层 tanh MLP 就够了。我用的是hidden=256、两层nn.Linear + nn.Tanh的共享主干,策略头输出均值,价值头输出标量。真正影响训练的是初始化:策略均值头的权重用方差 0.01 级别的小初始化,让初始动作靠近 0;价值头同样小初始化,避免初始 value 估计过大把 GAE 的第一轮 δ 撑爆。

观测归一化对 Ant-v2 和 Humanoid-v2 是必须的,对 Hopper-v2 和 HalfCheetah-v2 可做可不做。原因是后两者的观测主要是关节角和角速度,量纲一致;前两者的观测里混着接触力,数值比关节量大两个数量级。实现时维护一个 running mean/std,对 obs 做(obs − mean) / (std + 1e-6)并 clamp 到 ±10,注意统计量只能用训练数据更新,eval 时用训练末期冻结的统计量。这套「MuJoCo 物理引擎 + 高斯策略 + 归一化」的组合,不止适用于这四个标准环境,四足、双足乃至扫地机器人的运动规划策略迁移,走的都是同一条 pipeline。

4. 一个能跑的 PPO 实现:采样循环、GAE 与 dual-clip 更新

这一章给出完整可抄的实现片段。结构上分四块:网络、缓冲区与采样、GAE 与更新循环、参数表。把代码按顺序拼进一个文件,Ant-v2 和 Hopper-v2 可以直接训练;Humanoid-v2 记得加上观测归一化和更小的学习率。

4.1 数据流总览:一次 update 在做什么

一个 update 分两个阶段。采样阶段用当前策略跑 N 条 transition(N = num_envs × steps_per_env),把 obs、action、logp、value、reward、fall 存进缓冲区;学习阶段先用 GAE 算整段 advantage,再打散成 minibatch 做 k_epoch 轮梯度更新。MuJoCo 的单次 step 开销在毫秒级,单个环境采 2048 步要十几秒,所以采样阶段我一般开 4~8 个并行环境,这既是速度问题也是方差问题——多条独立轨迹平均后的 advantage 比单条轨迹平滑得多。做并行环境用SubprocVecEnv或 gym 的AsyncVectorEnv,Windows 上 fork 容易出问题就用SyncVectorEnv把并行数调小。

4.2 网络定义:共享主干加两个头

import torch import torch.nn as nn from torch.distributions import Normal class ActorCritic(nn.Module): def __init__(self, obs_dim, act_dim, hidden=256): super().__init__() self.trunk = nn.Sequential( nn.Linear(obs_dim, hidden), nn.Tanh(), nn.Linear(hidden, hidden), nn.Tanh(), ) self.mean_head = nn.Linear(hidden, act_dim) self.log_std = nn.Parameter(torch.full((act_dim,), -0.5)) self.value_head = nn.Linear(hidden, 1) # 小初始化:初始动作靠近 0,避免物理仿真被大力矩打散 nn.init.orthogonal_(self.mean_head.weight, gain=0.01) nn.init.orthogonal_(self.value_head.weight, gain=1.0) def dist(self, obs): h = self.trunk(obs) mean = self.mean_head(h) std = self.log_std.exp().expand_as(mean) return Normal(mean, std) def step(self, obs): d = self.dist(obs) action = d.sample() logp = d.log_prob(action).sum(-1) value = self.value_head(self.trunk(obs)).squeeze(-1) return action, logp, value def evaluate(self, obs, action): d = self.dist(obs) logp = d.log_prob(action).sum(-1) value = self.value_head(self.trunk(obs)).squeeze(-1) return logp, value, d.entropy().sum(-1)

log_stdnn.Parameter,不随输入变化,所以evaluate阶段重算 logp 时用的是更新后的 log_std。step用于采样,evaluate用于更新阶段,两个方法必须分开,因为采样阶段不能对网络求梯度、也不能重新采样动作。熵项在evaluate里直接由分布算出,连续动作任务我一般把熵系数设 0,靠log_std自身调节探索强度。

4.3 采样循环:缓冲区与截断标志的记录

class RolloutBuffer: def __init__(self): self.obs, self.actions, self.logps = [], [], [] self.rewards, self.falls, self.values = [], [], [] def store(self, obs, action, logp, reward, fall, value): self.obs.append(obs); self.actions.append(action) self.logps.append(logp); self.rewards.append(reward) self.falls.append(float(fall)); self.values.append(value) def get(self): return (torch.tensor(self.obs, dtype=torch.float32), torch.tensor(self.actions, dtype=torch.float32), torch.tensor(self.logps, dtype=torch.float32), torch.tensor(self.rewards, dtype=torch.float32), torch.tensor(self.falls, dtype=torch.float32), torch.tensor(self.values, dtype=torch.float32)) buffer = RolloutBuffer() obs = env.reset() for _ in range(steps_per_env): with torch.no_grad(): action, logp, value = ac.step(torch.as_tensor(obs, dtype=torch.float32)) action_clip = np.clip(action.numpy(), env.action_space.low, env.action_space.high) obs_next, reward, done, info = env.step(action_clip) truncated = info.get("TimeLimit.truncated", False) fall = done and not truncated buffer.store(obs, action.numpy(), logp.item(), reward, fall, value.item()) obs = obs_next if not done else env.reset()

这里有几处必须按这个写法。第一,action_clipenv.step前,存进 buffer 的也是裁剪后的动作,因为更新阶段要重算这个动作在新策略下的 logp,如果存的是未裁剪动作,logp 和实际执行的策略对不上;第二,falldone and not truncated计算,采样阶段就把终止语义分好类,GAE 阶段就不用再猜;第三,done之后要立刻env.reset()续上采样,否则缓冲区里会混入跨 episode 的 transition,GAE 计算会错位。

4.4 GAE 与更新循环:advantage 归一化、value clip 和梯度裁剪

def compute_gae(rewards, falls, values, last_value, gamma=0.99, lam=0.95): advantages = torch.zeros_like(values) gae = 0.0 for t in reversed(range(len(rewards))): if t == len(rewards) - 1: next_value = last_value else: next_value = values[t + 1] delta = rewards[t] + gamma * next_value * (1 - falls[t]) - values[t] gae = delta + gamma * lam * (1 - falls[t]) * gae advantages[t] = gae return advantages, advantages + values obs, actions, old_logps, rewards, falls, old_values = buffer.get() advantages, returns = compute_gae(rewards, falls, old_values, last_value) for _ in range(k_epochs): indices = torch.randperm(len(obs)) for mb in indices.split(minibatch_size): old_logp_mb = old_logps[mb] adv_mb = advantages[mb] adv_mb = (adv_mb - adv_mb.mean()) / (adv_mb.std() + 1e-8) logp_new, value_new, entropy = ac.evaluate(obs[mb], actions[mb]) ratio = (logp_new - old_logp_mb).exp() l1 = ratio * adv_mb l2 = torch.clamp(ratio, 1.0 - clip, 1.0 + clip) * adv_mb if use_dual_clip: pg_loss = -(torch.where( adv_mb > 0, torch.min(l1, l2), torch.max(l1, l2))).mean() else: pg_loss = -torch.min(l1, l2).mean() value_clip = old_values[mb] + (value_new - old_values[mb]).clamp(-0.2, 0.2) value_loss = torch.max( (value_new - returns[mb]).pow(2), (value_clip - returns[mb]).pow(2)).mean() loss = pg_loss + 0.5 * value_loss optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(ac.parameters(), max_grad_norm=0.5) optimizer.step()

这段代码里有三个细节值得说明。第一,advantage 的归一化在 minibatch 内部做,均值减掉、标准差除掉,这等价于给策略损失加了一个自适应学习率,比手动调lr更有效;标准差的+1e-8是为了防止 buffer 里全是相同 reward 时除零。第二,value loss 用 clip 后的目标取 max,和策略的 clip 同理,不允许价值网络单次更新跑太远,这个设计能明显抑制 reward 曲线的震荡。第三,clip_grad_norm_设 0.5,MuJoCo 任务的梯度经常有尖峰,不裁剪的话一次 update 就能把 log_std 打到 NaN。

4.5 四个 v2 环境的参数表:从 Hopper 起步,把 Humanoid 留到最后

环境lrbuffer/updatek_epochsminibatchobs 归一化建议总步数
Hopper-v23e-4204810256不需要3e5~5e5
HalfCheetah-v23e-4409610256可选1e6
Ant-v23e-4409610256建议2e6
Humanoid-v21e-4~3e-4409610256必须5e6~1e7

公共参数:γ=0.99、λ=0.95、clip=0.2、Adam 的 eps 取 1e-5。选 Hopper-v2 当第一个实验对象是有意的:它维度低、终止条件明确、两三百万步内就能看到明显上升,任何实现上的 bug 都会在曲线形态上暴露出来。HalfCheetah-v2 没有提前终止,reward 数值大但不稳定,适合验证 dual-clip 和 reward 归一化的效果。Ant-v2 开始接触力进入观测,这时必须确认归一化代码没有污染训练统计量。最后才是 Humanoid-v2,它需要 5e6 起步的步数预算,单机 CPU 训练经常要跑到过夜,建议先在小步数上确认 loss 不 NaN、explained variance 在上升,再放长跑。

5. Humanoid-v2 的 PPO 验证与调参:eval、NaN 排查和最后一个技巧

训练脚本能跑通只算完成一半,剩下的一半是让结果可信。这一章讲我验证一个 PPO 实现是否真的学会了的三个手段。

5.1 用确定性策略评估,而不是盯训练 reward 曲线

训练阶段的 reward 是带探索采样出来的,单条曲线方差很大,Humanoid-v2 上相邻两次 update 的 return 能差出 50%,直接盯训练曲线很容易误判。我每 25 次 update 跑一次确定性评估:采样时取均值动作而不是重采样,跑 10 个 episode 记录 mean±std:

def evaluate(ac, env_name, episodes=10): env = gym.make(env_name) returns = [] for _ in range(episodes): obs = env.reset() total = 0.0 while True: with torch.no_grad(): d = ac.dist(torch.as_tensor(obs, dtype=torch.float32)) action = d.mean # 确定性动作 obs, reward, done, _ = env.step(action.numpy()) total += reward if done: break returns.append(total) env.close() return float(np.mean(returns)), float(np.std(returns))

d.mean代替d.sample()就是确定性评估的全部区别。eval return 的方差主要来自随机初始状态,10 个 episode 的 mean±std 足够画出可信的带状图。Humanoid-v2 的 eval return 从 800 爬到 3000 这个过程是平滑的,如果 eval 曲线长时间不动而训练 reward 在涨,说明策略停留在局部最优附近,问题出在探索而不是拟合。

5.2 NaN 与动作越界的排查顺序

训练中途 loss 变 NaN,我按下面的顺序查,能覆盖九成情况。第一步查物理仿真,打印env.unwrapped.sim.data.qpos,如果 qpos 本身就是 NaN,说明不是 PPO 的问题,而是给 MuJoCo 物理引擎的力矩越界导致接触求解发散,先检查是否每个 step 都做了np.clip,再把初始log_std调小到 -1.0。第二步查log_std,如果它发散到 1 以上,std 就会指数膨胀,动作接近随机噪声,此时给log_stdclamp(-20, 2),并确认 advantage 归一化没写错。第三步查归一化的 running 统计量,obs 里一旦混入 inf,mean/std 会被污染,后面所有 batch 的输入都不正常,解决办法是 obs 进 buffer 前做一次torch.isfinite检查。第四步查 value loss,如果它震荡幅度超过 pg_loss 一个数量级,把 lr 减半再把梯度裁剪降到 0.5。

5.3 最后一个技巧:把熵、ratio 均值和 explained variance 写进日志

reward 曲线只能告诉你「好不好」,告诉不了你「哪一侧出了问题」。我在每个 update 里额外记三个字段:

with torch.no_grad(): entropy = ac.dist(obs).entropy().mean().item() explained_var = 1 - (returns - old_values).var() / (returns.var() + 1e-8) mean_ratio = (logp_new - old_logps).exp().mean().item()

读法如下:explained_var接近 1 说明价值网络对 return 的预测能力很强,接近 0 或为负说明 value 还没跟上,这个时候别急着调策略侧参数,先加大 value loss 权重或多训几个 epoch;entropy掉到 0.3 以下且 eval return 不再增长,说明策略过早确定性化,把初始log_std抬高或加一个小的熵系数;mean_ratio持续偏离 1 说明 clip 边界频繁被触发,策略单步更新过猛,lr 减半比调 clip 有效。把这三个字段和 eval return 放进同一张 CSV,训练结束时用双 y 轴画出来,eval_return一旦跌破前几个百分位,先看explained_varmean_ratio再动参数,这比盯着 reward 曲线猜要快得多。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 14:16:13

跨平台图形化ADB工具选型与使用指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 14:15:21

C语言系统学习指南:从基础到内存管理实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 14:15:05

QGIS文件夹批量加载技巧:从多选到脚本自动化,告别逐个拖拽

如果你跟我一样,手里攒了一整个项目的shp、tif、影像成果和临时导出的小文件,正打算在QGIS里一口气全部打开来检查,你大概率会这样做:打开数据源管理器,选中一个文件,双击,再选中下一个&#xf…

作者头像 李华
网站建设 2026/9/12 14:14:53

基于LSTM与TensorFlow的带情绪检测聊天机器人毕业设计落地指南

简介:基于长短期记忆网络(LSTM)与TensorFlow的聊天机器人毕业设计项目源码,集成情绪(抑郁)检测功能,适用于计算机、通信、人工智能、自动化等相关专业本科或研究生作为毕业设计、课程设计或实际…

作者头像 李华