四足强化学习里的 Actor 与 Critic:输入输出维度到底怎么算

相关笔记:03:Observation 里面到底放什么 · 04:Action 为什么常用关节位置目标 · 08:从公式走进 rsl_rl 与 legged_gym 的 PPO 训练闭环

写在前面:先把 45、48、12、24、1 对上号

看四足机器人 PPO 代码时,经常会同时遇到下面这些数字:

num_actor_obs = 45
num_critic_obs = 48
num_actions = 12
actor output = 12 或 24
critic output = 1

它们并不矛盾,只是在描述不同接口:

  • 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. 先统一符号

后面使用下面几个符号:

符号 含义
NN 并行环境数
TT 每轮 rollout 的时间步数
BB PPO 更新时的 batch 或 mini-batch 大小
dπd_\pi Actor observation 维度
dVd_V Critic observation 维度
dad_a action 维度

常见四足机器人每条腿有 3 个受控关节,所以:

da=4×3=12d_a=4\times3=12

这里数的是受控执行器,不是浮动基机器人完整的广义坐标维度。仿真器里的 q、qdot 可能还包含 floating base,但 policy 仍然通常只输出 12 个关节控制量。

2. Actor 的输入维度:policy 到底看到了多少个数

Actor 可以写成:

μθ=fθ(otπ)\mu_\theta=f_\theta(o_t^\pi)

其中:

otπ∈Rdπo_t^\pi\in\mathbb R^{d_\pi}

所以 Actor 第一层全连接层的 in_features 就是 dπd_\pi。

重要的是,dπd_\pi 不是凭经验填写的常数,而是 observation 拼接结果最后一维的长度:

actor_obs = torch.cat(
[
base_ang_vel,
projected_gravity,
commands,
joint_pos_rel,
joint_vel,
last_action,
],
dim=-1,
)

要得到正确维度,必须数每一项包含多少个标量,而不是数列表里有几项。

一个可部署的 12 关节示例

Actor observation 维度 说明
base_ang_vel 3 机体角速度
projected_gravity 3 重力在机体系的投影
commands 3 vxcmd,vycmd,ωzcmdv_x^{cmd},v_y^{cmd},\omega_z^{cmd}
joint_pos_rel 12 相对默认姿态的关节位置
joint_vel 12 关节速度
last_action 12 上一控制时刻的动作
合计 45 Actor 输入维度

因此:

dπ=3+3+3+12+12+12=45d_\pi=3+3+3+12+12+12=45

这一点很容易算错。上面虽然只有 6 个 observation term,但它们展开后共有 45 个标量,绝对不是 33 维。

关于每个 observation term 的物理意义,可以回看 03:Observation 里面到底放什么。

3. Actor 的输出维度:12 维动作,还是 24 维分布参数

连续控制 PPO 常把策略写成对角高斯分布:

πθ(at∣otπ)=N ⁣(μθ(otπ),diag⁡(σ2))\pi_\theta(a_t\mid o_t^\pi) =\mathcal N\!\left(\mu_\theta(o_t^\pi),\operatorname{diag}(\sigma^2)\right)

对于 12 个受控关节:

μt∈R12,σt∈R12,at∈R12\mu_t\in\mathbb R^{12},\qquad \sigma_t\in\mathbb R^{12},\qquad a_t\in\mathbb R^{12}

这里最容易出现两个不同说法。

实现 A:Actor MLP 只输出均值

经典 rsl_rl 风格通常是:

Actor MLP: 45 -> hidden layers -> 12 个 mean
log_std: 12 个独立可学习参数

log_std 会从 [12] 广播到 [B, 12]。这时 Actor 最后一个 Linear 的输出维度就是 12。

实现 B:Actor MLP 同时输出均值与标准差

也有实现会让网络输出 24 个数:

mean, log_std = actor_out.chunk(2, dim=-1)

这 24 个数由:

12 个 mean+12 个 log standard deviation12\text{ 个 mean}+12\text{ 个 log standard deviation}

组成。网络头输出 24 维,不等于机器人执行 24 维动作。 真正采样并送给环境的 action 仍然是 12 维。

训练时通常从分布中采样:

at∼πθ(⋅∣otπ)a_t\sim\pi_\theta(\cdot\mid o_t^\pi)

部署时通常直接使用均值:

at=μθ(otπ)a_t=\mu_\theta(o_t^\pi)

无论后面是否经过 clip、tanh、action_scale,或者被解释成关节位置偏移,动作的最后一维仍然是 12。动作怎样进入 PD 控制器,可以回看 04:Action 为什么常用关节位置目标。

4. Critic 的输入维度:与 Actor 相同,还是看得更多

PPO 的 Critic 估计状态价值:

Vϕ(otV)≈E ⁣[∑k=0∞γkrt+k]V_\phi(o_t^V) \approx \mathbb E\!\left[\sum_{k=0}^{\infty}\gamma^k r_{t+k}\right]

它的输入记作:

otV∈RdVo_t^V\in\mathbb R^{d_V}

Actor 与 Critic 的输入有两种常见关系。

对称 Actor–Critic

两者使用同一份 observation:

otπ=otV,dπ=dVo_t^\pi=o_t^V,\qquad d_\pi=d_V

例如 Actor 和 Critic 都接收 48 维 observation。它们的输入相同,不代表两张网络共享参数。

非对称 Actor–Critic

训练时让 Critic 看见更多仿真真值:

otπ≠otVo_t^\pi\neq o_t^V

常见 privileged information 包括:

  • 仿真器给出的真实机体线速度。
  • 精确足端接触状态或接触力。
  • 地形高度采样。
  • 地面摩擦系数。
  • 随机化后的质量、负载、电机强度。
  • 外力与扰动信息。

这样做的核心原则是:

Actor 必须只依赖真机部署时能够构造的输入;Critic 只在训练时帮助计算 value、return 和 advantage,部署时可以丢弃。

非对称 Actor–Critic 与左右腿镜像的数据增强不是一回事。

这类训练期特权信息如何帮助可部署的 student policy,可以继续看 四足机器人 Teacher 网络设计。

5. Critic 的输出维度为什么通常只有 1

PPO 里的 value Critic 映射为:

Vϕ:RdV→RV_\phi:\mathbb R^{d_V}\rightarrow\mathbb R

因此张量形状通常是:

critic_obs: [B, d_V]
value: [B, 1]

有些代码会 squeeze(-1),把 value 写成 [B],但每个环境仍然只有一个标量。

Critic 输出不是:

  • 12 个关节动作。
  • 每个 reward term 各一个分数。
  • 下一帧 observation。

即使奖励由速度跟踪、能耗、足端打滑等很多项组成,只要环境最后把它们加权成单个 reward,Critic 估计的仍是单个累计回报。

需要区分的是:SAC、TD3 等算法常使用 Q(s,a)Q(s,a) Critic,它会额外接收 action;这里讨论的是 PPO 中最常见的 VV-Critic。

6. 一个完整的 45 → 12、48 → 1 示例

假设 Actor 使用前面列出的 45 维可部署 observation,而 Critic 额外读取 3 维真实机体线速度:

dπ=45d_\pi=45

dV=45+3=48d_V=45+3=48

da=12d_a=12

单个环境的接口就是:

Actor: [45] -> mean [12] -> sampled action [12]
Critic: [48] -> value [1]

如果一次并行运行 N=4096N=4096 个环境:

actor_obs:  [4096, 45]
critic_obs: [4096, 48]
mean: [4096, 12]
actions: [4096, 12]
values: [4096, 1]

这里 4096 是 batch 轴。Actor 第一层仍然是 Linear(45, hidden_dim),绝不能写成 Linear(4096 * 45, hidden_dim)。

7. 为什么有时又会看到 48、235 或更大的输入

维度变大通常不是网络突然改变,而是 observation 加了新内容。

加入机体线速度:45 → 48

如果 Actor 也能看到 3 维 base_lin_vel:

dπ=45+3=48d_\pi=45+3=48

加入 187 个地形高度采样:48 → 235

如果再拼接 187 个高度点:

dπ=48+187=235d_\pi=48+187=235

这就是经典四足 locomotion 配置里常见的 235 维 observation 来源之一。高度点数量由采样网格决定,不是固定标准。

堆叠历史帧

如果完整堆叠 HH 帧 45 维 observation:

dπ=45Hd_\pi=45H

例如 H=4H=4 时是 180 维。

但工程里也可能只堆叠本体感知,把 command 只保留一次。假设本体感知与上一动作共 42 维、command 为 3 维:

dπ=42H+3d_\pi=42H+3

当 H=4H=4 时是 171 维。

需要先确认 history_length=4 表示“总共 4 帧”,还是“当前帧再加 4 帧历史”;后者实际是 5 帧,是常见的 off-by-one 来源。

如果使用 GRU,输入通常保留 [T,N,d_{frame}],GRU 的 input_size 是单帧维度 dframed_{frame},不是 T×dframeT\times d_{frame}。

8. Rollout 里的 TT、NN 也不是特征维度

单步并行采样的形状是:

actor_obs:  [N, d_pi]
critic_obs: [N, d_V]
actions: [N, d_a]
values: [N, 1]

保存完整 rollout 后:

actor_obs:  [T, N, d_pi]
critic_obs: [T, N, d_V]
actions: [T, N, d_a]
values: [T, N, 1]

PPO 更新前通常把前两个轴展平:

actor_obs:  [T*N, d_pi]
critic_obs: [T*N, d_V]
actions: [T*N, d_a]

例如 T=24,N=4096T=24,N=4096,展平后共有 98304 条 transition,但 Actor 的输入特征数仍然只是 45。

高斯分布还要注意:每个关节得到一个 log probability 后,需要在动作维上求和:

per_joint_log_prob = dist.log_prob(actions)  # [B, 12]
log_prob = per_joint_log_prob.sum(dim=-1) # [B]

PPO ratio 比较的是联合动作概率。漏掉 sum(dim=-1),会错误地得到每个关节一个 ratio。

9. 用断言让维度错误尽早暴露

不要等到第一层矩阵乘法报错才检查 observation。可以在拼接位置直接断言:

actor_parts = [
base_ang_vel,
projected_gravity,
commands,
joint_pos_rel,
joint_vel,
last_action,
]
actor_obs = torch.cat(actor_parts, dim=-1)

critic_obs = torch.cat([base_lin_vel, actor_obs], dim=-1)

assert actor_obs.shape[-1] == sum(x.shape[-1] for x in actor_parts)
assert actor_obs.shape[-1] == num_actor_obs == 45
assert critic_obs.shape[-1] == num_critic_obs == 48

网络输出也可以检查:

mean = actor(actor_obs)
value = critic(critic_obs)

assert mean.shape == (*actor_obs.shape[:-1], num_actions)
assert value.shape == (*critic_obs.shape[:-1], 1)

std = log_std.exp()
assert std.shape == (num_actions,) or std.shape == mean.shape
assert torch.all(std > 0)

dist = torch.distributions.Normal(mean, std)
actions = dist.sample()
log_prob = dist.log_prob(actions).sum(dim=-1)

assert actions.shape == mean.shape
assert log_prob.shape == actor_obs.shape[:-1]

这种断言比在配置里手工维护一个没有验证的数字可靠得多。

10. 最常见的维度误区

误区 1:把列表项数当成 observation 维度

6 个 observation term 不等于 6 维。必须把每一项展开后的标量数相加。

误区 2:Actor 有 12 个关节,所以一定输出 24 维

动作是 12 维。24 通常只是 12 个均值加 12 个标准差参数,而且很多实现的 MLP 本身只输出 12 个均值。

误区 3:Critic 也应该输出 12 维

PPO value Critic 通常每个状态只输出一个 VV 值,因此是 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 工程,可以按这个顺序检查:

  1. 找 observation 的实际 torch.cat(),逐项记录最后一维。
  2. 区分 actor_obs 与 critic_obs,确认是否使用 privileged information。
  3. 找 num_actions,确认它等于受控 actuator 数,而不是机器人完整自由度。
  4. 看 Actor 最后一层输出的是 mean,还是 mean 与 log standard deviation 的拼接。
  5. 看 Critic 最后一层是否为 1。
  6. 打印单步、rollout、flatten 后的 shape,区分 feature 轴与 batch 轴。
  7. 确认真机部署只保留 Actor,并且每个 Actor 输入都能在真实系统中构造。

总结

用本文的 12 关节非对称 PPO 例子,可以把整条维度链压缩成:

可部署 observation 45 维
↓
Actor 输出 mean 12 维,配合 std 形成动作分布
↓
采样或取均值得到 action 12 维

Actor observation 45 维 + 特权线速度 3 维
↓
Critic observation 48 维
↓
Critic 输出 value 1 维

所以真正应该记住的不是“Actor 一定 45 维、Critic 一定 48 维”,而是三条规则:

  1. 输入维度来自实际拼接的标量总数。
  2. Actor 动作维度来自受控执行器数量。
  3. PPO 的 value Critic 通常每个状态输出一个标量。

只要沿着 observation -> network -> distribution/value -> environment 逐段打印 shape,绝大多数维度问题都能在训练开始前发现。

知识共享许可协议