四足机器人强化学习系列 03:Observation 里面到底放什么

上一篇整理:02:用四足机器人理解 MDP

写在前面:Observation 是 policy 的“输入接口”

上一篇把四足 locomotion 任务先放进了 MDP 框架里:状态、动作、转移、奖励、折扣因子。

这一篇继续往下拆一个最容易在代码里遇到、但也最容易被忽略的东西:observation。

我现在对 observation 的理解是:它不是“机器人真实拥有的所有信息”,而是我选择让 policy 看到的那一串数字。这串数字决定了神经网络凭什么做判断,也决定了后面真机部署时我能不能把同样的输入喂给它。

如果 action 是 policy 的输出接口,那么 observation 就是 policy 的输入接口。

一句话结论

Observation 是 policy 真正能看到的信息。它通常由 IMU、编码器、命令、上一帧 action、地形信息等拼成一个固定顺序的一维向量。四足机器人强化学习里,obs 设计的核心不是“给得越多越好”,而是“训练时能稳定学,部署时也能真实拿到”。

可以先记住这个形式:

at=π(ot)a_t=\pi(o_t)

其中:

  • oto_t:当前 observation。
  • π\pi:policy 网络。
  • ata_t:policy 输出的 action。

也就是说,policy 并不是直接根据完整世界状态 sts_t 做决策,而是根据我喂给它的 oto_t 做决策。

State 和 Observation 不完全一样

理论上 MDP 里常说 state sts_t。但在四足机器人代码里,更常见的是 obs、observations、obs_buf。

我现在先把两者区分成:

state:环境真实完整状态
observation:policy 实际拿到的状态切片 / 传感器量 / 人为构造量

比如仿真器内部可能知道:

  • 机器人 base 在世界坐标系下的精确位置。
  • 机器人 base 的精确线速度。
  • 每个足端的接触力。
  • 地面真实摩擦系数。
  • 机器人质量、惯量、关节摩擦。
  • 地形完整高度图。
  • 所有外部扰动。

但真机上 policy 不一定能直接知道这些。

所以真正喂给 policy 的 observation 往往会更克制:

IMU 可以测到的
电机编码器可以测到的
上层命令给出的
上一帧自己输出过的
少量可以估计或采样的

这也是为什么很多四足 locomotion 任务严格说更像 POMDP,而不是完全可观测的 MDP。

一个常见的四足 locomotion observation 结构

以速度跟踪任务为例,常见 observation 可以写成:

ot=[ωbasegprojccmdq−qdefaultq˙at−1]o_t= \begin{bmatrix} \omega_{base} \\ g_{proj} \\ c_{cmd} \\ q-q_{default} \\ \dot q \\ a_{t-1} \end{bmatrix}

对应成更工程一点的列表就是:

base_ang_vel
projected_gravity
velocity_commands
dof_pos - default_dof_pos
dof_vel
previous_action

如果机器人有 12 个关节,一个典型维度可能是:

项目 维度 含义
base angular velocity 3 机身角速度
projected gravity 3 重力方向投影到机身坐标系
commands 3 期望 vx,vy,ωzv_x, v_y, \omega_z
joint position error 12 关节角相对默认站姿偏差
joint velocity 12 关节速度
previous action 12 上一帧 policy 输出
合计 45 policy 输入维度

所以一个很常见的 obs 维度就是 45 维。当然不同项目会不一样,加入地形高度采样以后可能一下变成一百多维、两百多维。

注意:表格只是理解用,具体维度一定要以代码里的 num_obs 或 observation 拼接逻辑为准。

1. base angular velocity:机身角速度

base angular velocity 通常是机身角速度,可以理解成 IMU 陀螺仪能给出的量。

它一般是 3 维:

ωbase=[ωx,ωy,ωz]\omega_{base}=[\omega_x,\omega_y,\omega_z]

这个量告诉 policy:

身体现在是不是正在翻滚?
是不是正在俯仰?
是不是正在偏航旋转?

对四足机器人来说,这很重要。因为 locomotion 不只是腿在动,身体姿态的稳定也很关键。

比如机器人快要向前栽倒时,pitch 方向角速度会变大;policy 如果能看到这个量,就有机会通过腿部动作把身体拉回来。

2. projected gravity:比 roll/pitch 更常见的姿态表达

很多四足 RL 代码不会直接把 roll、pitch、yaw 喂给 policy,而是喂 projected gravity。

直觉上,projected gravity 表示:

重力方向在机器人机身坐标系下看起来指向哪里

如果机器人是正着站的,重力大致沿着机身坐标系的负 z 方向;如果机器人身体歪了,重力在机身坐标系下的投影也会跟着变。

它通常也是 3 维:

gproj∈R3g_{proj}\in\mathbb{R}^3

我现在把它理解成 policy 的“身体倾斜感”。它告诉策略:

  • 机身是不是向前倾了。
  • 机身是不是向侧面倒了。
  • 机身是不是接近翻车了。

为什么不直接给欧拉角?

主要有几个原因:

  1. 欧拉角有奇异性和跳变问题。
  2. yaw 对很多速度跟踪任务不一定重要。
  3. projected gravity 对姿态倾斜表达更直接。
  4. 它和 IMU 姿态估计结果更容易对应。

比如四足机器人在原地朝不同方向转头,只要身体没有倾倒,重力在机身坐标系下的投影不会因为世界系 yaw 改变而无意义地变化。这对学习稳定步态是有好处的。

3. commands:上层希望机器人怎么走

commands 是速度命令,通常来自上层控制器、键盘、遥控器或者课程采样器。

常见是 3 维:

ccmd=[vxcmd,vycmd,ωzcmd]c_{cmd}=[v_x^{cmd},v_y^{cmd},\omega_z^{cmd}]

分别表示:

  • vxcmdv_x^{cmd}:希望机器人向前 / 向后走多快。
  • vycmdv_y^{cmd}:希望机器人横向走多快。
  • ωzcmd\omega_z^{cmd}:希望机器人绕 z 轴转多快。

为什么 commands 要放进 observation?

因为同一个 policy 通常不是只学一种速度,而是要学一个“速度跟踪器”。

如果不给 command,policy 不知道当前到底应该:

站着不动
向前走
向后走
横移
左转
右转

给了 command 之后,policy 学到的是一个条件策略:

at=π(ot,ccmd)a_t=\pi(o_t,c_{cmd})

也就是:在当前身体状态下,结合目标速度,输出合适的腿部动作。

4. joint position:通常给相对默认姿态的偏差

关节角是四足机器人最基础的本体感知信息之一。

但常见做法不是直接给 qq,而是给:

q−qdefaultq-q_{default}

其中:

  • qq:当前关节角。
  • qdefaultq_{default}:默认站姿关节角。

这样做的好处是让输入更居中。默认站姿附近的关节角偏差接近 0,神经网络更容易处理。

如果机器人有 12 个主动关节,这一项就是 12 维。

FL_hip, FL_thigh, FL_calf,
FR_hip, FR_thigh, FR_calf,
RL_hip, RL_thigh, RL_calf,
RR_hip, RR_thigh, RR_calf

具体顺序非常关键。训练和部署必须完全一致。

5. joint velocity:关节速度

关节速度 q˙\dot q 也是常见 observation。

它告诉 policy:

每个关节现在正在往哪个方向动?动得快不快?

只知道关节位置还不够,因为同一个关节角下,关节可能正在快速向前摆,也可能正在快速向后收。速度信息能帮助 policy 判断运动趋势。

如果有 12 个关节,这一项也是 12 维。

不过真机上关节速度可能噪声比较大,所以工程里经常会做滤波,或者在训练中加噪声,让 policy 不要过度依赖过于干净的速度值。

6. previous action:上一帧动作为什么要放进去

上一帧 action at−1a_{t-1} 在四足 RL 里很常见。

它看起来有点奇怪:上一帧动作不是 policy 自己输出的吗?为什么还要再喂回来?

我的理解是,它至少有三个作用:

  1. 让 policy 知道自己上一刻刚刚命令了什么。
  2. 帮助输出更连续,不要每一帧剧烈跳变。
  3. 在没有显式历史网络的时候,给 policy 一点点“短期记忆”。

很多 reward 里还会惩罚 action rate:

∥at−at−1∥2\|a_t-a_{t-1}\|^2

这会鼓励 action 不要抖得太厉害。

把 previous action 放到 observation 里,也让 policy 更容易根据上一帧动作调整当前动作。

复杂地形时会加入 height samples

如果任务不是平地速度跟踪,而是走楼梯、斜坡、石头路等复杂地形,policy 往往还需要看到地形信息。

一种常见做法是在机器人周围采样一圈或一片高度点:

height samples around base

比如在机身前方、侧方采样若干个点,得到相对机身高度。

这相当于告诉 policy:

前面有没有台阶?
左脚附近是不是有坑?
右前方是不是有凸起?

如果没有地形 observation,机器人只能靠接触之后的反馈来反应;如果提前看到高度采样,就能更早调整摆腿高度和落脚位置。

但 height samples 也有部署问题:

  • 真机上需要深度相机、激光雷达或其他地形估计模块。
  • 感知延迟会影响 policy。
  • 高度图噪声会影响策略稳定性。
  • 仿真里的完美高度信息不一定能在现实中复现。

所以是否加入地形观测,要看任务目标。如果只是先学平地 locomotion,可以不急着加。

privileged observation:训练时能看,部署时不能看

四足 RL 里还经常出现一个概念:privileged observation。

它指的是:训练时 critic 或 teacher 可以看到的额外信息,但部署时 actor / policy 不直接看到。

比如:

真实摩擦系数
机器人质量扰动
外部推力
精确机身线速度
足端接触力
地形真实参数

这些信息在仿真里很好拿,但真机上不一定能拿到。

一个常见结构是:

actor observation:部署时 policy 能看到的信息
critic observation:训练时 value function 能看到的更多信息

也就是说:

at=π(otactor)a_t=\pi(o_t^{actor})

而 critic 可能用:

V=V(otcritic)V=V(o_t^{critic})

这样做的好处是训练时 value 估计更准,但最终部署的 actor 仍然只依赖真机可获得的信息。

我后面看代码时要注意区分:

num_observations
num_privileged_obs
obs_buf
privileged_obs_buf
critic_obs

它们不一定是同一个东西。

为什么 observation 不是越多越好

直觉上可能会觉得:信息越多,policy 越聪明。

但四足机器人里不一定。

因为多给的信息可能带来几个问题:

  1. 真机拿不到
    仿真里能直接读 base linear velocity,真机上可能只能估计。

  2. 噪声太大
    传感器信号不干净,policy 如果过度依赖,部署会变脆。

  3. 维度变高,训练更难
    不重要的信息太多,网络要自己学会忽略。

  4. 造成仿真作弊
    policy 在仿真里利用了现实不存在的信息,sim2real 时直接失效。

  5. 不同频率和延迟
    IMU、电机、视觉、高度图的更新频率可能不同,把它们强行拼一起会引入时序问题。

所以 observation 设计的原则不是“全塞进去”,而是:

足够完成任务
尽量贴近真实传感器
维度别太臃肿
顺序和尺度稳定
部署时能复现

observation 的尺度也很重要

神经网络不喜欢不同输入量级差异太大。

比如:

角速度可能是 0~几 rad/s
关节角可能是 -1~1 rad
关节速度可能是 -几十 rad/s
速度命令可能是 -2~2 m/s
height samples 可能是 -1~1 m

如果不做 scale,有些量会在网络输入里显得特别大,有些量几乎被淹没。

所以代码里经常有类似:

obs = torch.cat((
base_ang_vel * obs_scales.ang_vel,
projected_gravity,
commands * commands_scale,
(dof_pos - default_dof_pos) * obs_scales.dof_pos,
dof_vel * obs_scales.dof_vel,
actions,
), dim=-1)

这些 scale 不是随便写的,它们会影响训练稳定性。

我看代码时应该特别关注:

obs_scales.ang_vel
obs_scales.dof_pos
obs_scales.dof_vel
commands_scale
clip_observations

因为最后喂给 policy 的不是原始物理量,而是缩放、裁剪、拼接后的向量。

observation noise:训练时故意加噪声

真机传感器一定有噪声,所以训练时经常会给 observation 加噪声。

例如:

角速度加一点噪声
projected gravity 加一点噪声
关节角加一点噪声
关节速度加一点噪声
height samples 加一点噪声

这样 policy 不会只适应仿真里的完美观测。

可以把它理解成一种 domain randomization:

不是只随机物理参数,也随机机器人看到的东西

但噪声也不是越大越好。噪声太大会让任务变得过难,policy 学不到稳定规律。

observation history:为什么有时要堆叠历史帧

如果 policy 只看当前一帧 observation,它对过去发生了什么知道得很少。

有些项目会把多帧 observation 拼起来:

o~t=[ot,ot−1,ot−2,⋯ ]\tilde o_t=[o_t,o_{t-1},o_{t-2},\cdots]

这样可以给 MLP policy 一点历史信息。

比如:

当前帧看起来一样,但过去几帧趋势不同

堆叠历史后,policy 更容易估计速度、延迟、接触状态等隐含信息。

不过代价是输入维度会变大。例如单帧 45 维,堆 4 帧就是 180 维。

另一种路线是用 RNN / GRU,让网络自己维护隐藏状态。但工程上 MLP + frame stack 更直接。

obs 顺序是部署时最容易踩的坑

这一点非常重要。

训练时 obs 拼接顺序如果是:

角速度 → 重力方向 → 命令 → 关节角 → 关节速度 → 上一帧动作

部署时就必须完全一样。

如果部署时写成:

命令 → 角速度 → 关节角 → 重力方向 → 关节速度 → 上一帧动作

从维度上看可能完全没报错,但 policy 会直接乱掉。

因为神经网络不会知道“第 0~2 维现在不是角速度了”。它只会按训练时学到的权重解释每一维。

这类 bug 很隐蔽:

程序能跑
维度也对
模型也加载成功
但机器人行为完全不对

所以部署前最好做一个 obs 对照表,把训练端和部署端逐项核对。

一个简单的 obs 拼接示例

假设我现在有一个 12 自由度四足机器人,采用平地速度跟踪任务,可以先这样理解:

obs = concat([
base_ang_vel * ang_vel_scale, # 3
projected_gravity, # 3
commands[:, :3] * commands_scale, # 3
(dof_pos - default_dof_pos) * dof_pos_scale, # 12
dof_vel * dof_vel_scale, # 12
actions, # 12
])

维度就是:

3 + 3 + 3 + 12 + 12 + 12 = 45

这 45 个数每一步都会输入 policy,policy 输出下一步 action。

如果加上 187 个 height samples,那可能变成:

45 + 187 = 232

这就是为什么看代码时 num_obs 很重要。它必须和模型训练时的输入维度一致。

真机部署时要核对哪些东西

如果以后要把训练好的 policy 放到真机上,observation 这一块我觉得至少要核对:

  1. 维度是否一致
    训练时 num_obs=45,部署时也必须是 45。

  2. 顺序是否一致
    每一段的排列顺序必须一样。

  3. 关节顺序是否一致
    仿真里的 12 个关节顺序和真机驱动里的顺序可能不同。

  4. 单位是否一致
    rad 还是 degree?m/s 还是 mm/s?

  5. 坐标系是否一致
    角速度是在 body frame 还是 world frame?

  6. 符号是否一致
    某个关节正方向在仿真和真机是否相反?

  7. scale 是否一致
    训练时乘了 obs_scales,部署时也要乘同样的 scale。

  8. clip 是否一致
    训练时 observation clip 到某个范围,部署时也要一致。

  9. 上一帧 action 是否一致
    是网络原始输出?还是 scale 后的 action?这个要看训练代码。

  10. 频率是否一致
    policy 是 50 Hz、100 Hz 还是其他频率?不同频率下 obs 动态分布会变。

这些问题里,最容易出错的是:关节顺序、坐标系、符号、scale。

我现在的理解小结

这篇先把 observation 理成四足 RL 里的“输入接口”。

我现在会这样记:

obs 不是完整 state
obs 是 policy 实际看到的一维向量
obs 通常包含 IMU、关节、本体命令、上一帧动作
复杂地形可能加入 height samples
训练时可能有 privileged obs,但部署 actor 不能依赖它
obs 的顺序、尺度、坐标系、单位必须严格一致

最重要的一句话是:

训练时 policy 看到什么,部署时就必须以同样顺序、同样单位、同样尺度再给它一次。

否则模型本身没坏,机器人也可能跑得很离谱。

下一篇整理:04:Action 为什么常用关节位置目标

知识共享许可协议