相关笔记:03:Observation 里面到底放什么 · 04:Action 为什么常用关节位置目标 · 08:从公式走进 rsl_rl 与 legged_gym 的 PPO 训练闭环
写在前面:先把 45、48、12、24、1 对上号
看四足机器人 PPO 代码时,经常会同时遇到下面这些数字:
num_actor_obs = 45 |
它们并不矛盾,只是在描述不同接口:
45:Actor 每个时刻看到的特征数。48:Critic 每个时刻看到的特征数,这里比 Actor 多了 3 维特权信息。12:12 个受控关节对应的动作维度,也是高斯策略均值的维度。24:某些实现把 12 个均值和 12 个标准差一起从网络输出,但动作仍然只有 12 维。1:PPO 的 value Critic 对每个状态只估计一个标量价值。
这篇只解决一个问题:给定 observation 的组成,怎样可靠地算出 Actor 和 Critic 的输入输出维度。
一句话结论
对一个 12 关节四足机器人,常见 PPO Actor 接收可部署的 policy observation,输出 12 维动作分布;Critic 接收普通观测或更丰富的 privileged observation,输出 1 个状态价值。输入维度由实际拼接的特征逐项相加,输出维度由“控制多少个执行器”和“估计几个价值”决定。
先记住这张表:
| 模块 | 输入 | 输出 | 典型示例 |
|---|---|---|---|
| Actor MLP | policy observation | 动作均值 | [B, 45] -> [B, 12] |
| Policy distribution | 均值与标准差 | 采样动作、log probability | mean/std/action: [B, 12] |
| Critic MLP | critic observation | 状态价值 | [B, 48] -> [B, 1] |
其中 B 是 batch size,只表示一次并行处理多少条数据,不属于网络的特征维度。
1. 先统一符号
后面使用下面几个符号:
| 符号 | 含义 |
|---|---|
| 并行环境数 | |
| 每轮 rollout 的时间步数 | |
| PPO 更新时的 batch 或 mini-batch 大小 | |
| Actor observation 维度 | |
| Critic observation 维度 | |
| action 维度 |
常见四足机器人每条腿有 3 个受控关节,所以:
这里数的是受控执行器,不是浮动基机器人完整的广义坐标维度。仿真器里的 q、qdot 可能还包含 floating base,但 policy 仍然通常只输出 12 个关节控制量。
2. Actor 的输入维度:policy 到底看到了多少个数
Actor 可以写成:
其中:
所以 Actor 第一层全连接层的 in_features 就是 。
重要的是, 不是凭经验填写的常数,而是 observation 拼接结果最后一维的长度:
actor_obs = torch.cat( |
要得到正确维度,必须数每一项包含多少个标量,而不是数列表里有几项。
一个可部署的 12 关节示例
| Actor observation | 维度 | 说明 |
|---|---|---|
base_ang_vel |
3 | 机体角速度 |
projected_gravity |
3 | 重力在机体系的投影 |
commands |
3 | |
joint_pos_rel |
12 | 相对默认姿态的关节位置 |
joint_vel |
12 | 关节速度 |
last_action |
12 | 上一控制时刻的动作 |
| 合计 | 45 | Actor 输入维度 |
因此:
这一点很容易算错。上面虽然只有 6 个 observation term,但它们展开后共有 45 个标量,绝对不是 33 维。
关于每个 observation term 的物理意义,可以回看 03:Observation 里面到底放什么。
3. Actor 的输出维度:12 维动作,还是 24 维分布参数
连续控制 PPO 常把策略写成对角高斯分布:
对于 12 个受控关节:
这里最容易出现两个不同说法。
实现 A:Actor MLP 只输出均值
经典 rsl_rl 风格通常是:
Actor MLP: 45 -> hidden layers -> 12 个 mean |
log_std 会从 [12] 广播到 [B, 12]。这时 Actor 最后一个 Linear 的输出维度就是 12。
实现 B:Actor MLP 同时输出均值与标准差
也有实现会让网络输出 24 个数:
mean, log_std = actor_out.chunk(2, dim=-1) |
这 24 个数由:
组成。网络头输出 24 维,不等于机器人执行 24 维动作。 真正采样并送给环境的 action 仍然是 12 维。
训练时通常从分布中采样:
部署时通常直接使用均值:
无论后面是否经过 clip、tanh、action_scale,或者被解释成关节位置偏移,动作的最后一维仍然是 12。动作怎样进入 PD 控制器,可以回看 04:Action 为什么常用关节位置目标。
4. Critic 的输入维度:与 Actor 相同,还是看得更多
PPO 的 Critic 估计状态价值:
它的输入记作:
Actor 与 Critic 的输入有两种常见关系。
对称 Actor–Critic
两者使用同一份 observation:
例如 Actor 和 Critic 都接收 48 维 observation。它们的输入相同,不代表两张网络共享参数。
非对称 Actor–Critic
训练时让 Critic 看见更多仿真真值:
常见 privileged information 包括:
- 仿真器给出的真实机体线速度。
- 精确足端接触状态或接触力。
- 地形高度采样。
- 地面摩擦系数。
- 随机化后的质量、负载、电机强度。
- 外力与扰动信息。
这样做的核心原则是:
Actor 必须只依赖真机部署时能够构造的输入;Critic 只在训练时帮助计算 value、return 和 advantage,部署时可以丢弃。
非对称 Actor–Critic 与左右腿镜像的数据增强不是一回事。
这类训练期特权信息如何帮助可部署的 student policy,可以继续看 四足机器人 Teacher 网络设计。
5. Critic 的输出维度为什么通常只有 1
PPO 里的 value Critic 映射为:
因此张量形状通常是:
critic_obs: [B, d_V] |
有些代码会 squeeze(-1),把 value 写成 [B],但每个环境仍然只有一个标量。
Critic 输出不是:
- 12 个关节动作。
- 每个 reward term 各一个分数。
- 下一帧 observation。
即使奖励由速度跟踪、能耗、足端打滑等很多项组成,只要环境最后把它们加权成单个 reward,Critic 估计的仍是单个累计回报。
需要区分的是:SAC、TD3 等算法常使用 Critic,它会额外接收 action;这里讨论的是 PPO 中最常见的 -Critic。
6. 一个完整的 45 → 12、48 → 1 示例
假设 Actor 使用前面列出的 45 维可部署 observation,而 Critic 额外读取 3 维真实机体线速度:
单个环境的接口就是:
Actor: [45] -> mean [12] -> sampled action [12] |
如果一次并行运行 个环境:
actor_obs: [4096, 45] |
这里 4096 是 batch 轴。Actor 第一层仍然是 Linear(45, hidden_dim),绝不能写成 Linear(4096 * 45, hidden_dim)。
7. 为什么有时又会看到 48、235 或更大的输入
维度变大通常不是网络突然改变,而是 observation 加了新内容。
加入机体线速度:45 → 48
如果 Actor 也能看到 3 维 base_lin_vel:
加入 187 个地形高度采样:48 → 235
如果再拼接 187 个高度点:
这就是经典四足 locomotion 配置里常见的 235 维 observation 来源之一。高度点数量由采样网格决定,不是固定标准。
堆叠历史帧
如果完整堆叠 帧 45 维 observation:
例如 时是 180 维。
但工程里也可能只堆叠本体感知,把 command 只保留一次。假设本体感知与上一动作共 42 维、command 为 3 维:
当 时是 171 维。
需要先确认 history_length=4 表示“总共 4 帧”,还是“当前帧再加 4 帧历史”;后者实际是 5 帧,是常见的 off-by-one 来源。
如果使用 GRU,输入通常保留 [T,N,d_{frame}],GRU 的 input_size 是单帧维度 ,不是 。
8. Rollout 里的 、 也不是特征维度
单步并行采样的形状是:
actor_obs: [N, d_pi] |
保存完整 rollout 后:
actor_obs: [T, N, d_pi] |
PPO 更新前通常把前两个轴展平:
actor_obs: [T*N, d_pi] |
例如 ,展平后共有 98304 条 transition,但 Actor 的输入特征数仍然只是 45。
高斯分布还要注意:每个关节得到一个 log probability 后,需要在动作维上求和:
per_joint_log_prob = dist.log_prob(actions) # [B, 12] |
PPO ratio 比较的是联合动作概率。漏掉 sum(dim=-1),会错误地得到每个关节一个 ratio。
9. 用断言让维度错误尽早暴露
不要等到第一层矩阵乘法报错才检查 observation。可以在拼接位置直接断言:
actor_parts = [ |
网络输出也可以检查:
mean = actor(actor_obs) |
这种断言比在配置里手工维护一个没有验证的数字可靠得多。
10. 最常见的维度误区
误区 1:把列表项数当成 observation 维度
6 个 observation term 不等于 6 维。必须把每一项展开后的标量数相加。
误区 2:Actor 有 12 个关节,所以一定输出 24 维
动作是 12 维。24 通常只是 12 个均值加 12 个标准差参数,而且很多实现的 MLP 本身只输出 12 个均值。
误区 3:Critic 也应该输出 12 维
PPO value Critic 通常每个状态只输出一个 值,因此是 1 维。
误区 4:把并行环境数写进 in_features
N、T、B 都是 batch 轴;真正进入 Linear 的是最后一维。
误区 5:配置写 45,实际又多拼了 3 维线速度
配置数字必须和 torch.cat() 的真实结果一致。最稳妥的办法是打印 shape 并加断言。
误区 6:训练 Actor 偷看了仿真真值
如果 Actor 输入了真实摩擦系数、精确接触力或无法测得的线速度,真机部署时就无法构造同一输入。特权信息应该只进入训练期 Critic,或者通过可部署的 estimator 估计。
误区 7:动作维度相同,就以为动作含义相同
12 维 action 可能表示关节位置偏移、绝对位置、速度目标或力矩。维度相同,不代表控制接口相同。
误区 8:归一化和缩放会改变维度
归一化、噪声、clip、action scale 改变的是数值范围,不改变张量最后一维。
11. 看代码时的最短检查路线
以后拿到一个四足 PPO 工程,可以按这个顺序检查:
- 找 observation 的实际
torch.cat(),逐项记录最后一维。 - 区分
actor_obs与critic_obs,确认是否使用 privileged information。 - 找
num_actions,确认它等于受控 actuator 数,而不是机器人完整自由度。 - 看 Actor 最后一层输出的是 mean,还是 mean 与 log standard deviation 的拼接。
- 看 Critic 最后一层是否为 1。
- 打印单步、rollout、flatten 后的 shape,区分 feature 轴与 batch 轴。
- 确认真机部署只保留 Actor,并且每个 Actor 输入都能在真实系统中构造。
总结
用本文的 12 关节非对称 PPO 例子,可以把整条维度链压缩成:
可部署 observation 45 维 |
所以真正应该记住的不是“Actor 一定 45 维、Critic 一定 48 维”,而是三条规则:
- 输入维度来自实际拼接的标量总数。
- Actor 动作维度来自受控执行器数量。
- PPO 的 value Critic 通常每个状态输出一个标量。
只要沿着 observation -> network -> distribution/value -> environment 逐段打印 shape,绝大多数维度问题都能在训练开始前发现。