四足机器人强化学习系列 04:Action 为什么常用关节位置目标

上一篇整理:03:Observation 里面到底放什么

写在前面:Action 是 policy 的“输出接口”

上一篇看的是 observation,也就是 policy 每一步能看到什么。

这一篇继续往下看 action:policy 看完 observation 以后,到底输出了什么?

刚开始接触四足强化学习时,我很容易把 action 想象成很高层的命令,比如:

抬左前腿
迈右后腿
身体往前倾一点
切换成 trot 步态

但在大多数四足 locomotion 强化学习代码里,action 通常没有这么“语义化”。它更像是一串连续数值,最后会被转换成每个关节的控制目标。

所以这篇想先回答一个非常工程化的问题:

四足 RL 里的 action,为什么经常不是直接输出电机力矩,而是输出关节位置目标,或者关节位置目标附近的偏移量?

一句话结论

四足机器人强化学习里的 action 通常是 policy 的低层控制输出,最常见形式是每个关节的目标位置偏移:policy 输出归一化 action,经 action_scale 缩放后加到默认关节姿态上,再由底层 PD 控制器转换成电机力矩。

可以先记住这条链路:

observation → policy → action → target joint position → PD controller → torque → robot motion

写成公式大概是:

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

qttarget=qdefault+saatq^{target}_t = q^{default} + s_a a_t

τt=Kp(qttarget−qt)−Kdq˙t\tau_t = K_p(q^{target}_t - q_t) - K_d\dot q_t

其中:

  • oto_t:当前 observation。
  • ata_t:policy 输出的 action。
  • qdefaultq^{default}:默认站立姿态下的关节角。
  • sas_a:action scale,用来控制动作幅度。
  • qtq_t:当前真实关节角。
  • q˙t\dot q_t:当前关节速度。
  • Kp,KdK_p, K_d:PD 控制器参数。
  • τt\tau_t:最终作用到关节上的力矩。

Action 不是“动作语义”,而是一组连续控制量

强化学习理论里经常写:

at∼π(at∣st)a_t \sim \pi(a_t|s_t)

看起来 action 像是“智能体做了一个动作”。但落到四足机器人 locomotion 任务里,action 一般不是自然语言意义上的动作,而是一个固定维度向量。

以 12 自由度四足机器人为例,常见结构是:

每条腿 3 个关节
4 条腿一共 12 个关节
policy 输出 12 维 action

也就是:

action = [a0, a1, a2, ..., a11]

每一个元素通常对应某个关节的控制目标或控制目标偏移。

它不是告诉机器人“走路”,而是每个控制周期都给 12 个关节一个新的低层目标。走路这种行为,是在长期训练中从这些连续控制量里“涌现”出来的。

常见的几种 action 设计

四足 RL 里的 action 设计大概可以分成几类。

1. 直接输出关节位置目标

一种很直观的设计是:policy 直接输出每个关节的目标角度。

q_target = action

也就是说,网络输出什么,就把它当成目标关节角。

这种方式概念简单,但通常需要额外处理范围限制,因为神经网络输出本身没有物理常识:

  • 可能输出超过关节极限的角度。
  • 可能输出离当前姿态很远的目标。
  • 可能导致动作跳变过大。
  • 训练早期随机策略会让机器人姿态非常混乱。

所以实际工程里更常见的是下一种:输出默认姿态附近的偏移量。

2. 输出关节位置偏移

这是 legged locomotion 里非常常见的 action 形式:

q_target = q_default + action_scale * action

这里的核心思想是:

不让 policy 从零开始发明一个完整姿态,而是让它围绕一个合理的默认站立姿态做小范围调整。

比如默认姿态是机器人自然站立时的关节角:

q_default = [
hip_abduction, hip_flexion, knee,
hip_abduction, hip_flexion, knee,
...
]

policy 输出的 action 通常会被限制在一个比较小的范围内,比如 [−1,1][-1, 1]。然后通过 action_scale 缩放:

action = 0.5
action_scale = 0.25 rad
offset = 0.125 rad

这样目标关节角不会乱飞,而是在默认姿态附近变化。

这对训练很重要。因为训练初期 policy 基本是随机的,如果动作空间太自由,机器人很容易一开始就疯狂抽搐、穿模、摔倒,学习信号也会变得很差。

3. 输出关节速度目标

也可以让 policy 输出目标关节速度:

dq_target = action_scale * action

然后底层控制器根据速度误差产生力矩。

这种方式在某些机器人控制任务里也能用,但四足 locomotion 中不如位置目标常见。原因是单纯速度目标不直接约束姿态,训练时可能更难形成稳定的支撑结构。

4. 直接输出关节力矩

最自由的一种方式是 policy 直接输出 torque:

torque = action_scale * action

它的好处是表达能力强,policy 可以直接学习如何用力,不受 PD 目标位置形式的限制。

但问题也很明显:

  • 随机策略阶段很危险。
  • torque 高频抖动会很严重。
  • 对仿真精度更敏感。
  • sim2real 难度更高。
  • 真机上更容易过流、过热、撞限位。

所以对于很多四足机器人 locomotion 任务,直接 torque control 虽然理论上更自由,但工程上不一定更稳。

为什么常用“位置目标 + PD 控制”

我现在对这个选择的理解是:它在 policy 和电机之间加了一层稳定的低层控制器。

policy 不需要直接管每一瞬间应该输出多少力矩,而是给一个“我希望关节去哪里”的目标。真正的力矩由 PD 控制器生成。

公式是:

τ=Kp(qtarget−q)−Kdq˙\tau = K_p(q^{target} - q) - K_d\dot q

如果写完整一点,也可以是:

τ=Kp(qtarget−q)+Kd(q˙target−q˙)\tau = K_p(q^{target} - q) + K_d(\dot q^{target} - \dot q)

在很多四足 RL 实现里,q˙target\dot q^{target} 直接取 0,于是变成:

τ=Kp(qtarget−q)−Kdq˙\tau = K_p(q^{target} - q) - K_d\dot q

这里的直觉是:

  • Kp(qtarget−q)K_p(q^{target} - q):如果当前位置离目标位置有误差,就产生一个往目标拉回去的力矩。
  • −Kdq˙-K_d\dot q:如果关节运动太快,就加阻尼,减少抖动。

所以 PD 层本身就带有一定稳定性。

policy 只要学会输出合适的目标位置偏移,就可以借助 PD 控制器完成比较平滑的运动。

这相当于给 policy 加了“动作先验”

位置目标 + PD 并不是单纯为了偷懒,而是在动作空间里加入了一种很有用的先验。

这个先验大概是:

机器人关节不要每一帧乱打力矩
而是围绕一个合理姿态连续调整
底层控制器负责把目标姿态稳定跟上

这会让强化学习更容易。

因为 RL 的难点之一是探索。如果 action 直接是 torque,随机探索会非常粗暴;如果 action 是默认姿态附近的偏移,随机探索虽然仍然会乱,但乱的范围被控制住了。

也就是说:

torque action:policy 直接碰电机力矩
position target action:policy 先碰目标姿态,再由 PD 间接碰力矩

后者更像是给 policy 戴了一副“护栏”。

action_scale 为什么很关键

在代码里经常能看到类似参数:

self.action_scale = 0.25

或者配置文件中有:

action_scale: 0.25

它决定了 policy 输出的无量纲 action 最后会被放大成多大的关节角偏移。

例如:

action ∈ [-1, 1]
action_scale = 0.25 rad

那么每个关节目标相对默认角度的最大偏移大约是:

[-0.25 rad, 0.25 rad]

换成角度大概是:

0.25 rad ≈ 14.3°

这个量太小,机器人可能动不起来;太大,训练初期动作会很暴力,真机上也更危险。

可以这样理解:

action_scale 小:安全、保守、动作幅度不足
action_scale 大:表达能力强、动作激烈、训练和部署风险更高

所以 action_scale 不是一个随便调的数字,它直接改变了策略实际控制机器人的“力度”。

action clip 和 torque clip

实际代码里除了 action_scale,还经常会有 clip。

action clip

policy 输出的 action 可能先被限制在某个范围内:

action = clip(action, -clip_actions, clip_actions)

比如:

clip_actions = 1.0

这可以避免网络输出异常大值。

torque clip

PD 算出来的 torque 也可能被限制:

torque = clip(torque, -torque_limits, torque_limits)

这是为了符合电机物理限制,也为了保护仿真和真机。

这两个 clip 不一样:

action clip:限制 policy 输出
torque clip:限制最终电机力矩

训练时如果有这些限制,部署时也要对应上。否则策略在真机上的动作分布就变了。

decimation:policy 不一定每个仿真步都输出 action

四足 RL 代码里还经常出现一个参数:decimation。

它表示 policy action 的更新频率和物理仿真频率不一定一样。

比如:

仿真 dt = 0.005 s,也就是 200 Hz
decimation = 4

意思是:

物理仿真每 0.005 s 积分一次
policy 每 4 个仿真步输出一次 action
所以 policy 频率 = 200 / 4 = 50 Hz

在这 4 个仿真步内,通常保持同一个 action 或同一个 target position。

可以写成:

policy 输出 action
for i in range(decimation):
根据同一个 action 算 torque
physics step

这点很重要,因为部署时控制频率要对齐。

如果训练时 policy 是 50 Hz,真机部署时变成 100 Hz,动作效果就会变。不是说一定不能改频率,而是不能在没意识到的情况下改。

代码里通常怎么走

以常见 legged gym / Isaac Gym 风格为例,控制链路可以大概理解成下面这样。

第一步,policy 根据 observation 输出 action:

actions = policy(obs)

第二步,对 action 做 clip:

actions = torch.clip(actions, -clip_actions, clip_actions)

第三步,把 action 转成目标关节角:

target_q = default_dof_pos + action_scale * actions

第四步,PD 控制器算 torque:

torques = p_gains * (target_q - dof_pos) - d_gains * dof_vel

第五步,限制 torque:

torques = torch.clip(torques, -torque_limits, torque_limits)

第六步,送进仿真器:

gym.set_dof_actuation_force_tensor(sim, torques)

不同项目代码写法会不一样,但主线经常是这个逻辑。

为什么不让 policy 直接输出“足端位置”

还有一种直觉是:四足机器人不是靠脚走路吗?那 action 为什么不直接输出每个足端的位置?

比如:

front_left_foot_target = [x, y, z]

这当然也可以设计,但它会引入额外问题:

  1. 足端目标要通过逆运动学转成关节角。
  2. 逆运动学可能有多解或不可达。
  3. 关节限制、碰撞、动力学响应还要额外处理。
  4. 足端轨迹和身体姿态之间的协调并不简单。

对于强化学习来说,直接在关节空间输出目标比较统一:

policy 输出 12 维关节相关 action
底层 PD 控制每个关节
仿真自然产生接触和运动

也就是说,步态不是显式规划出来的,而是在优化奖励的过程中形成的。

Action 和 gait 的关系

如果 action 只是 12 个关节目标偏移,那 gait,也就是步态,是怎么出现的?

我的理解是:gait 不是 action 里的一个显式字段,而是 policy 在时间序列中输出 action 的规律。

例如 trot 步态大概会表现为:

左前腿 + 右后腿 协调
右前腿 + 左后腿 协调
两组对角腿交替支撑和摆动

但网络不会直接输出:

gait = trot

它只是每 20 ms 或类似频率输出一组关节目标。经过一段时间,这些目标序列形成了周期性的腿部运动,外部观察起来就像某种步态。

所以可以这样理解:

单步 action:一帧低层目标
连续 action 序列:形成步态和运动风格
reward:决定什么样的 action 序列会被保留下来

Action 和 reward 的互相影响

Action 设计不是孤立的,它和 reward 强相关。

如果 action 是关节位置目标,reward 里通常会关心:

  • 速度跟踪好不好。
  • 身体姿态稳不稳。
  • 关节动作是否过大。
  • action 是否平滑。
  • torque 是否过大。
  • 关节速度是否过大。
  • 足端接触是否合理。

其中 action 平滑项很常见:

raction_rate=−∥at−at−1∥2r_{action\_rate} = -\|a_t - a_{t-1}\|^2

它惩罚连续两帧 action 差太大。

为什么要惩罚这个?

因为即使用了 PD,如果 policy 每一帧目标位置跳来跳去,关节仍然会抖。action rate penalty 可以让策略输出更平滑。

还有 action magnitude penalty:

raction=−∥at∥2r_{action} = -\|a_t\|^2

它鼓励 policy 不要总是输出过大的动作。

但这类惩罚也不能太大,否则机器人会变得过于保守,宁愿不动也不愿输出动作。

部署时最容易踩的坑

如果后面要把训练好的 policy 部署到真机,action 这一块至少要核对这些东西。

1. 关节顺序

policy 输出第 0 维,到底对应哪个关节?

训练中可能是:

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

真机 SDK 里可能是另一种顺序。

如果顺序错了,policy 可能会把膝关节目标发给髋关节,把左腿动作发给右腿。模型本身没坏,但机器人会表现得非常离谱。

2. 关节正方向

仿真里某个关节正方向和真机编码器正方向可能相反。

如果符号错了,policy 想让关节抬腿,真机可能反方向压腿。

3. 默认关节角

训练时使用的 default_dof_pos 必须和部署时一致。

如果默认姿态不同,那么同一个 action 会变成不同的目标姿态。

4. action_scale

训练时:

action_scale = 0.25

部署时也必须一致。

如果部署时写成 0.5,动作幅度直接翻倍,真机表现会明显变激烈。

5. PD 参数

训练时用的 Kp,KdK_p, K_d 和真机低层控制器参数要尽量对应。

如果训练里关节很“硬”,真机上很“软”,策略可能跟不上目标。

如果训练里阻尼很大,真机上阻尼小,可能出现抖动。

6. 控制频率

训练时 policy 频率、PD 更新频率、仿真步长之间的关系要搞清楚。

部署时如果频率变了,等效控制行为也会变。

7. torque limit

真机电机力矩限制通常比仿真里更需要谨慎。

如果训练时经常依赖很大的 torque,但真机限制更低,部署后机器人可能无法完成同样动作。

一个简单例子:12 维 action 到 torque

假设有一个 12 自由度四足机器人。

policy 输出:

action shape = (12,)

其中某个关节的 action 是:

a_i = 0.6

配置中:

action_scale = 0.25 rad
q_default_i = 0.8 rad
q_i = 0.85 rad
dq_i = 0.2 rad/s
Kp = 40
Kd = 1

那么目标角度是:

qitarget=0.8+0.25×0.6=0.95q^{target}_i = 0.8 + 0.25 \times 0.6 = 0.95

位置误差是:

qitarget−qi=0.95−0.85=0.10q^{target}_i - q_i = 0.95 - 0.85 = 0.10

PD torque 是:

τi=40×0.10−1×0.2=3.8\tau_i = 40 \times 0.10 - 1 \times 0.2 = 3.8

所以 policy 并没有直接输出 3.8 这个 torque。它只是输出了一个归一化 action,后面经过 action_scale、默认姿态和 PD 控制,才变成最终力矩。

这个例子能帮助我把 action 的含义拆清楚。

我现在的理解小结

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

我现在会这样记:

observation 是 policy 的输入接口
action 是 policy 的输出接口

四足 locomotion 里,action 通常是一组关节相关的连续数值
最常见形式是默认关节角附近的目标位置偏移

target_q = default_q + action_scale * action

底层 PD 再把目标关节角转换成 torque

torque = Kp * (target_q - q) - Kd * dq

这种设计的好处是:

动作空间更稳定
训练早期不至于太暴力
真机部署更安全
可以复用成熟的关节 PD 控制

但也要注意:

action_scale、default_q、PD 参数、关节顺序、控制频率必须对齐
否则训练出来的 policy 到部署时可能完全变味

最重要的一句话是:

policy 输出的 action 通常不是最终电机力矩,而是经过缩放和低层控制器解释后的关节目标;理解这层转换,才能真正看懂四足 RL 代码里的 actions。

下一篇我准备继续整理 reward:四足机器人到底是怎么通过奖励函数被“塑造”出走路行为的。

知识共享许可协议