四足机器人强化学习系列 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(qttargetqt)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π(atst)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(qtargetq)Kdq˙\tau = K_p(q^{target} - q) - K_d\dot q

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

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

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

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

这里的直觉是:

  • Kp(qtargetq)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=atat12r_{action\_rate} = -\|a_t - a_{t-1}\|^2

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

为什么要惩罚这个?

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

还有 action magnitude penalty:

raction=at2r_{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

位置误差是:

qitargetqi=0.950.85=0.10q^{target}_i - q_i = 0.95 - 0.85 = 0.10

PD torque 是:

τi=40×0.101×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:四足机器人到底是怎么通过奖励函数被“塑造”出走路行为的。

algorithms axis-angle bang-bang bode calibration chrome cmake cmakelists cnn colcon conan control cpp cpu d435i data_struct db design-pattern dots economics eigen factory-pattern fcpx figure finance forge fov gazebo gdb git gnu ibus interest isaac gym isaaclab kdl latex launch learning-notes legged locomotion legged-robot life linux mac math matlab matrix memory mlp money motion-control motor moveit mpc network ocs2 ode operator optimal algorithm optimal-control perf performance personal-finance ppo profiling python qos realsense reinforcement learning rnn robot robotics ros ros2 rtb security shell simulation stl thread tools twist ubuntu uml unitree urdf vae valgrind vcxsrv velocity vim web wifi work wsl 中文输入 交叉编译 依赖管理 分支管理 四足机器人 实验诊断 强化学习 机器人视觉 构建系统 深度学习 深度相机 点云 版本控制 神经网络 训练曲线 输入法 配置类
知识共享许可协议