PPO、奖励函数设计、参数指标、网路结构
mujoco + stable + baseline3

一、流程、数据流向、结构
1.1 实际流水线(MuJoCo + SB3)
a1.xml(MuJoCo MJCF:12 关节 + 躯干的几何/惯性/关节限位)
↓ 定义"机器人是谁"
A1SpeedFollowTrotEnv(gymnasium 接口:obs 35 / act 12 / step / reward)
↓ 定义"任务是什么"
DummyVecEnv(向量化包装)
↓
VecNormalize(obs_rms 在线归一化 → 产出 _norm.pkl)
↓
VecMonitor(episode 统计 → 产出 monitor.csv)
↓
PPO(SB3)训练 → model.save()
↓
phase1_final_xxx.zip + phase1_final_xxx_norm.pkl
1.2 一个 step 的数据流向(顺时针闭环)
| 步 | 数据 | 变换 |
|---|---|---|
| 1 | 原始状态 → obs(35) | 关节角/角速度 + 躯干角速度 + 投影重力 + 上帧动作 + 速度命令 |
| 2 | obs → 归一化 obs | (obs − obs_rms.mean)/√var,再 clip |
| 3 | 归一化 obs → Actor | MLP 前向,输出 12 维(Gaussian 均值) |
| 4 | → action(12) ∈ [−1,1] | tanh squash 到动作空间 |
| 5 | action → q_des(12) | q_des = stand_pos + scale × action |
| 6 | q_des → τ(12) | PD:τ = Kp(q_des−q) + Kd(0−q̇) |
| 7 | τ → mj_step | MuJoCo 物理积分,产生新状态 |
| 8 | → reward + next_obs | reward 由速度跟踪/能耗/姿态等加权 |
| 9 | → RolloutBuffer | 攒满 n_steps=2048 步 |
| 10 | → GAE 优势 Â | λ=0.95,Critic 提供 V(s) |
| 11 | → PPO 更新 | clip 裁剪,梯度更新 Actor + Critic 权重 |
| 12 | next_obs 回流 | 成为下一时刻 obs,闭环 |
1.3 控制频率分层
| 层 | 频率 | 谁负责 |
|---|---|---|
| Actor 推理 | ≈50 Hz(每次 env.step 一次) | ONNX/网络 |
| env 内部物理 | 200 Hz(若 decimation=4) | MuJoCo mj_step |
| PD | 跟随 env.step | 你的 env 代码 |
| FOC(仅真机) | ≈10 kHz | 电机驱动器内部,仿真不涉及 |
1.4 MuJoCo+SB3 vs Isaac Lab(两条路线)
| 维度 | 路线(MuJoCo + SB3) | Isaac Lab 路线 |
|---|---|---|
| 仿真后端 | MuJoCo(CPU) | PhysX / Isaac Gym(GPU) |
| 并行环境数 | 1 ~ 几十 | 4096(2¹²) |
| RL 框架 | Stable-Baselines3 PPO | rsl_rl PPO |
| 环境接口 | gymnasium | DirectRLEnv / ManagerBasedRLEnv |
| 归一化 | VecNormalize(obs_rms,自动存 pkl) |
手写 obs scale 或 EmpiricalNormalization |
| 模型保存 | .zip + _norm.pkl |
.pt → 转 ONNX |
| 适用阶段 | 快速原型 / 算法验证 / 教学 | 大规模训练、Sim2Real 主力 |
两者算法层完全一样(都是 PPO + Actor-Critic + GAE),差别只在"采样速度"和"工程封装"。你用 MuJoCo 把流程跑通,换到 Isaac Lab 只是换 env 和框架,思想不变。
二、MLP 网络
2.1 结构(model.policy 输出)
输入:obs (35,)
│
├─── Actor 分支(policy_net)
│ 35 → 256 (Tanh)
│ 256 → 128 (Tanh)
│ 128 → 64 (Tanh)
│ 64 → 12 (action_net) → 动作均值
│ + 可学习 log_std(12) → Gaussian 策略
│
└─── Critic 分支(value_net,更深)
35 → 256 (Tanh) → 128 (Tanh) → 64 (Tanh)
64 → 256 (Tanh) → 128 (Tanh) → 64 (Tanh)
64 → 1 (value_net) → V(s)
2.2 三个设计要点
- Tanh 而非 ReLU:输出有界、梯度平滑,机器人控制里比 ReLU 更稳(ReLU 死亡神经元会让动作突变)。
- Critic 比 Actor 深:价值函数要评估"长期回报",比"输出动作"更难学,所以多给一组 64→256→128→64 的容量。这是 SB3 的
MlpExtractor配置导致的,不是 bug。 - SB3 连续动作是 Gaussian + tanh squash:
action_net输出的是均值,采样后过 tanh 压到Box(−1,1);所以你看到的动作空间是Box(-1.0, 1.0, (12,))。
2.3 参数量自校验
这是从文件大小反推网络结构的实战技巧,完美印证:
| 文件 | 大小 | 反推 | 结论 |
|---|---|---|---|
policy.pth |
645,790 B | ÷4 B/参数 ≈ 16.1 万 | Actor(35→256→128→64→12)≈5.1万 + Critic(更深)≈10.8万 ≈ 15.9 万 ✓ 吻合(差 1.3% 来自少量非浮点计数器) |
policy.optimizer.pth |
1,293,780 B | ÷4÷2 ≈ 16.2 万 | Adam 每个参数存 exp_avg + exp_avg_sq 两个动量 → 2×16.1万×4B ≈ 1.29 MB ✓ 完美印证 |
所以:645KB 的权重 ≈ 16 万个 float32 参数 —— 这就是你那副
35→[256,128,64]→12网络的真实体量。以后看到.pth大小,除以 4 就能立刻估算参数量级。
三、训练过程参数解读与调参
3.1 关键参数
| 参数 | 你的值 | 含义 | 解读 |
|---|---|---|---|
observation_space |
Box(-inf, inf, (35,)) |
原始观测(未归一化) | 实际进网络前被 VecNormalize 归一化 |
action_space |
Box(-1, 1, (12,)) |
12 维连续动作 | 对应 12 个关节 |
n_steps |
2048 | 每次 update 前收集的步数 | 单 env 时即 2048 步/批 |
gamma |
0.99 | 折扣因子 | 有效视野 ≈ 1/(1−0.99) = 100 步 ≈ 2 秒(50Hz) |
learning_rate |
cosine_schedule | 余弦衰减 | 从初值平滑降到 ~0,适合长训练(1M 步) |
num_timesteps |
phase1 1,001,472 / phase2 210,944 | 总交互步数 | update 次数 = 1001472 ÷ 2048 ≈ 489 次 |
3.2 其余常见 SB3-PPO 参数(调参时用)
| 参数 | 典型值 | 作用 | 调参方向 |
|---|---|---|---|
gae_lambda |
0.95 | 优势估计的偏差-方差权衡 | 高了方差大、低了偏差大;0.95 是四足常用 |
clip_range |
0.2 | PPO 裁剪 ε(限制策略更新幅度) | 训练不稳→调小(0.1);学太慢→调大(0.3) |
ent_coef |
0.01→0.001 | 熵正则(鼓励探索) | 早期大一点促探索,后期减小促收敛 |
vf_coef |
0.5 | 价值损失权重 | 一般不动 |
max_grad_norm |
0.5 | 梯度裁剪 | 防梯度爆炸,训崩可降到 0.3 |
batch_size |
64(n_steps 的约数) | 小批量 | 显存够可加大 |
n_epochs |
3~10 | 每批数据重复训练轮数 | 多了易过拟合 |
3.3 症状 → 调参对照表
| 症状 | 优先检查 |
|---|---|
| reward 完全不涨 | 观测归一化是否生效?reward 量纲是否过大/过小?lr 是否太大? |
| 训到一半崩了 | clip_range ↓、max_grad_norm ↓、gae_lambda |
| 动作抖动 / 不平滑 | 把"上帧动作"放进观测(你 35 维里已有)、加 action-rate 惩罚 reward、lr ↓ |
| 只会原地不动 | 速度跟踪 reward 权重、探索(ent_coef ↑) |
| Sim2Sim 与训练表现不一致 | 观测拼接顺序 + 归一化 pkl 是否加载 + PD 参数 + 控制频率 |
| phase2 接着 phase1 训不动 | 确认 lr schedule 是否重置、obs 语义是否一致 |
3.4 Phase1 ↔ Phase2 兼容性结论
两者 obs=35 / act=12 / 网络结构 / n_steps / gamma / lr 策略 完全一致 → 权重可直接互相加载。唯一区别是 num_timesteps(1.0M vs 211K)。
⚠️ 前提是两个阶段的观测向量语义顺序没变过——维度对得上不等于语义对得上。
四、结果文件结构与分析
4.1 zip 里的 6 个文件
phase1_final_260830150443.zip
├── _stable_baselines3_version ← SB3 版本号
├── data ← ⚠️ 元数据(不是 data.pkl!)
├── policy.pth ← 645,790 B 网络权重(≈16 万参数)
├── policy.optimizer.pth ← 1,293,780 B Adam 动量状态
├── pytorch_variables.pth ← 额外变量(如 schedule 状态)
└── system_info.txt ← 环境信息
phase1_final_260830150443_norm.pkl ← VecNormalize 对象(单独文件)
monitor.csv ← VecMonitor 产出(episode r/l/t)
4.2 两个踩过的坑
| 坑 | 错误写法 | 正确写法 |
|---|---|---|
① zip 内叫 data,不是 data.pkl |
zf.open("data.pkl") → KeyError |
用 load_from_zip_file() 解析成 dict;或干脆 PPO.load() 后读属性 |
② _norm.pkl 是对象不是 dict |
data["observation_space"] → TypeError |
vn.observation_space(属性访问) |
## ✅ 正确解析 zip 元数据为 dict
from stable_baselines3.common.save_util import load_from_zip_file
data, params, pytorch_vars = load_from_zip_file(zip_path, device="cpu")
print(data["observation_space"].shape[0]) ## 35
print(data["num_timesteps"]) ## 1001472
## ✅ 正确读 norm.pkl
import pickle
with open(norm_pkl_path, "rb") as f:
vn = pickle.load(f)
print(vn.observation_space.shape[0]) ## 35
print(vn.obs_rms.mean, vn.obs_rms.var) ## 归一化统计量
## ✅ 最省事:直接 load 模型
model = PPO.load(zip_path, device="cpu")
print(model.observation_space.shape[0], model.action_space.shape[0])
4.3 训练曲线(monitor.csv)
import pandas as pd
df = pd.read_csv("monitor.csv", skiprows=1) ## 第一行是注释,跳过
print(df[["r", "l"]].rolling(50).mean()) ## r=episode奖励, l=长度
- episode reward 滚动均值上升 → 在学
- episode length 变长 → 狗能站稳更久(对站立/行走任务是好信号)
- 配合 TensorBoard:
tensorboard --logdir ./ppo_verify/ppo_model
4.4 部署时必须带上的东西
| 文件 | 是否必需 | 原因 |
|---|---|---|
policy.pth(在 zip 内) |
✅ | 网络权重 |
_norm.pkl |
✅ (若训练时用了 VecNormalize) | 不加载则观测尺度错位,策略直接失效 |
monitor.csv |
❌ | 仅分析用 |
optimizer.pth |
❌ | 只在"继续训练"时才需要;推理可弃 |
Sim2Sim / 真机部署的黄金法则:
zip提供"大脑",_norm.pkl提供"感官校准",两者必须配对加载,缺一个就白训。
五、总结
MuJoCo+SB3 的本质是一条"35 维观测进、12 维动作出"的闭环:环境链把 XML 变成 gym 接口,PPO 每 2048 步用 GAE 优势更新一次 Actor-Critic,Actor 那副 35→256→128→64→12 的 Tanh 网络(≈16 万参数,被 645KB 的 policy.pth 和 1.29MB 的 optimizer.pth 双重印证)每 20ms 吐一次关节目标角,经 PD 变成力矩喂给 MuJoCo;产物里 zip 存权重、norm.pkl 存归一化、monitor.csv 存曲线——三者配对加载,才是可复现、可部署的完整模型。
奖励函数
其实最重要的就是奖励函数设计,分阶段+续训模式,最终奖励值越高越好,值越高越认可当前输出状态
- 权重全都设置为正数,保证不混淆
- 奖励项为正数,奖励值越大越倾向于当前状态行为
- 惩罚项为负数,与设定的误差越大,惩罚越大,越不会倾向于当前状态行为

浙公网安备 33010602011771号