ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

分层强化学习实战:四足机器人多步态学习与避坑指南

分层强化学习实战:四足机器人多步态学习与避坑指南

简介:面向机器人运动控制与强化学习研究者的四足机器人步态学习实战项目,基于分层强化学习框架实现包含步行、跑步、爬楼梯等场景的多步态训练,适合高校学生、算法工程师及竞赛选手作为算法复现与二次开发的参考基准。压缩包共50个文件,整体约3.77MB,其中包含24个Python脚本(PPO算法、环境封装及辅助工具)、9个hpp与3个cpp文件(仿真接口与奖励函数实现)、4个yaml配置文件(参数与任务设定)、4个gif步态演示动画及README说明,可对照代码与动效快速理解训练流程。内容上并非简单demo,而是从环境构建到策略学习的完整工程:既有stable_baselines3、RaisimGym等常用依赖的封装模块,也有trot、bound、pace、multiple_gait等步态的仿真验证结果。目前已有230人学习浏览,适合具备一定强化学习基础、希望将理论落地到四足机器人运动控制的读者研究学习。

1. 用分层强化学习学四足机器人步态,难在哪

给四足机器人调步态,最让人头疼的不是让它走出某一种步态,而是让它在一个策略里同时学会多种步态,并且能在不同步态之间切换。单层强化学习策略在这个任务上经常翻车:训练初期还能看到几种步态的雏形,训练中后期就全坍缩成同一种,或者切换到另一个步态时关节抖振、直接摔砸。分层强化学习把步态学习拆成“高层决定此刻该用哪种步态、低层负责把关节轨迹跟踪到位”两个层次,正好对症这一任务。

这篇文章的目标读者是准备动手复现这类项目的工程师和学生。我会按我实际做四足步态研究时的路径来写:先讲清楚分层强化学习为什么能解决多步态问题,再给出仿真环境、状态动作空间、reward 设计和训练过程中的关键参数,最后是几组让我印象深刻的踩坑记录。你照着这个思路去读源码包,会比自己从头啃省一半时间。

2. 分层强化学习拆解步态决策:为什么单层 RL 在这里会翻车

2.1 单层策略的梯度冲突:多种步态为什么不能共享一套关节参数

先摆一个反直觉的结论:单层策略在数学上完全有能力拟合多种步态,但在实际训练时几乎必然失败,问题出在梯度冲突。

四足机器人的三种常用步态——对角小跑(trot)、爬行(crawl)、弹跳(bound)——各自的关节轨迹差异很大。trot 是对角腿成对落地,crawl 是单腿依次抬起,bound 是前后腿分别成对离地。单层策略网络把这些轨迹全塞进同一组权重里,每次环境返回的 reward 信号都在朝不同方向拉扯权重。一次更新希望髋关节在 0.1 秒内完成摆腿,另一次更新又希望它稳定承重,两次梯度方向接近垂直。

我见过最典型的现象是:训练 300 万步时,trot 已经收敛得很稳,然后你加入 crawl 的奖励条件,再训练 100 万步,trot 的步频开始紊乱,两条前腿落地时序越拉越近。这不是超参数没调好,而是单层策略的容量和优化路径在根本上不适合“多模式行为”学習。你需要的不是更大的网络,而是把“做什么”和“怎么做”拆开。

2.2 高层选步态、低层跟关节:HRL 的层次分工与训练节奏

分层强化学习在四足步態上的标准做法,是把策略分成两层,两层的时间尺度不同。

高层策略(也叫 meta-controller,元控制器)每隔 N 步决策一次,输出一个步态标签或目标速度区间。它不关心关节角度,只看躯干状态和任务目标。低层策略(也叫 sub-policy,子策略)接收高层的指令作为条件输入,再把自身观测映射成 12 个关节的动作,频率和机器人控制频率一致,一般是 30~50 Hz。这样,高层在一个慢时间尺度上选“用什么步态”,低层在快时间尺度上执行“关节怎么动”。

训练节奏上,比较可靠的顺序是先单独预训练低层策略,让它分别学会几种步态,然后冻结低层机制里的步态参数,只训练高层。如果从零开始同时训两层,低层还没学会走路,高层就在频繁切换步态,整个训练过程会陷入双盲探索,reward 曲线长期上不去。我一般会给低层 200 万步左右的预训练预算,再让高层接手。

2.3 什么场景下 HRL 才是划算的选择

不是所有四足控制问题都需要分层强化学习。判断标准很简单:如果任务只需要一种固定步态,单层 PPO 跑半小时就能拿到能用的策略,没必要引入两层训练的复杂度和调参成本。

需要上 HRL 的典型场景是三种:第一,任务要求机器人根据地形或速度指令自动切换步态(比如平地用 trot、上坡用 crawl);第二,单次训练要同时产出多种步态且要求切换平滑;第三,高层决策本身带有时间延展性,比如“先用 crawl 爬过障碍物,再切 trot 冲刺”,这类序列决策在单层框架下很难表达。

HRL 的代价也很明确:多一层策略就多一组超参数,多一个训练阶段。如果项目只要求演示单一步态,我建议直接跳过 HRL,把精力放 reward 调优上,性价比更高。

3. 仿真环境搭建与状态动作空间定义:从零把四足机器人立起来

3.1 仿真器选型:高速并行训练优先,还是物理精度优先

四足步态学習的仿真器,当前从业者主要就在 Isaac Gym 和 MuJoCo 之间选。两者的取舍很清晰。

Isaac Gym 的优势是 GPU 并行——同一个仿真场景可以同时跑几千个机器人实例,这对强化学习训练极其有利。PPO 这类 on-policy 算法需要大量采样,单机并行 4096 个环境能把每小时采样量推到百万步级别。代价是物理引擎的接触模型偏“理想化”,仿真到实体的 sim2real 迁移需要额外做域随机化。

MuJoCo 的优势是接触动力学精度高、仿真出的足端力和关节力矩更接近真实硬件,网上许多公开的四足行走 demo 都跑在它上面。缺点是单核只能并行少量环境,训练速度快不起来,需要配合多进程采样。

如果你手头没有团队维护的仿真平台,我的建议是:以 Isaac Gym 类方案起步,因为步态学習的核心矛盾是“如何让策略在大规模采样中收敛”,而不是“关节摩擦系数到底设多少”。等策略在仿真里稳定了,再针对实体机器人的特性做参数调整。

3.2 状态空间和动作空间定义:让低层策略拿到该拿到的信息

四足机器人的关节自由度一般是 12 个——每条腿 3 个关节,分别是横滚髋关节(HAA)、前摆髋关节(HIP)、膝关节(KNEE)。状态空间的常见设计是把以下信息拼接成一个向量:

状态组维度说明
躯干姿态3横滚/俯仰/偏航角
躯干速度3线速度,世界系
躯干角速度3机体系下的角速度
关节位置1212 个关节的当前角度
关节速度1212 个关节的角速度
上一时刻动作12用于平滑连续动作
高层步态指令1当前启用的步态编号

合计 46 维。这个维度不绝对,但信息完备性很重要——低层策略如果拿不到躯干偏航角,就永远学不会直线行走。

动作空间有两种常见选择。位置控制:策略输出 12 维关节目标位置增量,底层 PD 控制器去跟踪,输出的是位置增量而不是绝对位置,避免策略学会“僵直撑地”。力矩控制:策略直接输出 12 维关节力矩,弹性更大但对奖励函数和物理参数非常敏感。

我一般建议从位置控制起步。力矩控制的成功率太低,起步阶段很容易让人怀疑算法写错了。等整个流程指标达标后,再考虑尝试力矩控制提升动态性能。

3.3 步态参数化:不同步态的相位矩阵怎么构造

多种步态的分层强化学习实现方式,最重要的细节是步态本身不能用 enum 标签硬编码。硬编码的问题在于:低层策略收到“步态=1”这样的输入时,无法从中推断出“此时该哪条腿着地”。正确的做法是把步态参数化为占空比(duty factor)和相位偏移矩阵,高层输出的是步态参数的连续向量,低层基于这个向量计算目标足端轨迹。

一个简化的代码骨架如下:

import numpy as np # 四条腿的索引: 0=FL(前左), 1=FR(前右), 2=RL(后左), 3=RR(后右) # 步态参数: [占空比, 相位偏移(FL, FR, RL, RR 各自相对于基准的延迟)] # 基准相位: FL 始终为 0, 其他腿依次延迟 GAIT_LIBRARY = { "trot": {"duty": 0.5, "phases": [0.0, 0.5, 0.5, 0.0]}, "crawl": {"duty": 0.75, "phases": [0.0, 0.25, 0.5, 0.75]}, "bound": {"duty": 0.4, "phases": [0.0, 0.0, 0.5, 0.5]}, } def compute_swing_phase(gait_name: str, leg_idx: int, t: float) -> float: """ 计算指定腿在 t 时刻的摆动相位(0~1)。 0 -> 摆动相开始, duty -> 支撑相开始。 """ g = GAIT_LIBRARY[gait_name] period = 0.4 # 步态周期,单位秒 phase = (t / period + g["phases"][leg_idx]) % 1.0 # 当 phase 落在 [0, 1-duty] 区间时为摆动相 swing_phase = phase / (1.0 - g["duty"]) if phase < (1.0 - g["duty"]) else 1.0 return swing_phase

这段代码的逻辑是:每种步态用两个参数描述,占空比决定每条腿在周期内支撑相占比,相位偏移决定四条腿之间的落地时序关系。高层策略输出一个 one-hot 或连续权重向量,选择当前使用哪种步态参数,低层策略把这个参数向量和自身观测拼接作为输入。

参数说明:duty越大,机器人的支撑相越长,稳定性越高,但速度上限越低。crawl 用 0.75 就是典型的低速稳定步态;bound 用 0.4,两条腿同时离地时间长,动态性高。phases中各腿的偏移量差异,决定了机器人走起来像“爬”还是像“跑”。trot 里 FL 与 RR 相位一致、FR 与 RL 一致,就是对角步态的本质。

4. 分层强化学习策略实现:reward 设计、网络结构与训练代码骨架

4.1 低层策略的 reward 设计:速度跟踪优先,关节惩罚兜底

低层策略的职责是“把高层给的步态参数执行出来”,所以它的 reward 必须要踩在物理量上。我常用的 reward 项和默认权重如下:

奖励项公式权重作用
速度跟踪exp(-2.0 * vx_error)1.0推动机器人跟随目标速度
姿态稳定exp(-5.0 * roll_error^2)0.5抑制侧倾
关节加速度惩罚-0.001 * sum(action_diff^2)0.1抑制高频抖振
能耗惩罚-0.002 * sum(torque^2)0.05减少无意义耗力

这里的核心思想:速度跟踪用指数函数而不用线性距离,是因为指数函数在误差接近 0 时梯度更平滑,策略不会为了“追到精确速度”而牺牲稳定性。姿态稳定和能耗惩罚是辅助项,权重不能太大,否则机器人会“蹲在原地不动——因为不动就没有任何惩罚源”。

一个可直接用的 reward 计算函数:

def low_level_reward(state, action, last_action, target_speed): """ state: 包含躯干速度和姿态角 action: 当前动作(12维关节增量) last_action: 上一时刻动作 target_speed: 高层下发的目标速度 """ vx = state["base_lin_vel"][0] roll = state["base_rot"][0] # 速度跟踪奖励 vx_error = abs(vx - target_speed) reward_vx = np.exp(-2.0 * vx_error) # 姿态稳定性奖励 reward_roll = np.exp(-5.0 * roll**2) # 动作平滑惩罚 action_diff = np.sum((action - last_action) ** 2) reward_smooth = -0.001 * action_diff # 总奖励 reward = 1.0 * reward_vx + 0.5 * reward_roll + 0.1 * reward_smooth return reward

逻辑说明:这个函数让策略在“跑得准”和“迈得稳”之间做权衡。速度误差的指数衰减意味着只要误差稍微变大,奖励就快速下降,所以策略会被强烈激励去盯住目标速度。姿态稳定项的输入是躯干横滚角,四足在 trot 步态下横滚天然有周期性波动,指数系数设成 5 相对温和,设成 10 会让策略变得过于保守。

4.2 高层策略的 reward 设计:切换要付“切换成本”

高层策略的 reward 不能直接复用低层那套。高层的动作是步态切换,它需要判断的是“当前步态在当下的地形和速度任务下,是否让低层执行得够好”。

一个简单的做法是:高层每 N 步决策一次,它的 reward 等于这一个决策周期内低层平均 reward 的累计,另外加一个切换惩罚项。

def high_level_reward(gait_switch_count, low_level_reward_sum, switch_cost=0.2): """ high_level_reward: 高层决策周期内的累计奖励 gait_switch_count: 本周期内步态切换次数 switch_cost: 每次切换的固定成本 """ # 切换成本能防止高层策略以极高频率震荡 penalty = switch_cost * gait_switch_count return low_level_reward_sum - penalty

这里switch_cost很关键。如果设成 0,高层策略会倾向于每步都切换,因为它发现切换到某个步态能瞬间获得更高的低层奖励。设太大,高层又不敢换,训练出的机器人永远用一种步态。我见过最能稳定工作的区间是 0.1~0.3。更低的话,加上训练噪声,切换频率会失控。

4.3 低层/高层的网络结构与训练循环骨架

低层策略网络输入包含两部分:自身 46 维观测,和高层下发的步态参数(比如占空比+相位向量 5 维),拼接后是 51 维。中间层两层 MLP,隐藏层 256 和 128 个神经元,激活函数用 ELU——ReLU 在动作控制任务里容易死掉。

高层策略网络的输入只有高层自身的观测(躯干速度、地形坡度、任务目标,约 10 维),输出是可选步态的概率分布,最后采样得到一个步态编号,再映射到步态参数。

训练循环骨架:

# 伪代码,省略了具体 PPO 更新细节 for update in range(total_updates): # 1. 采样阶段: 用当前高层/低层策略跑仿真环境 trajectories = rollout(high_policy, low_policy, env) # 2. 低层更新: 用轨迹数据更新低层策略 low_policy.update(trajectories["low_obs"], trajectories["low_actions"], trajectories["low_rewards"]) # 3. 高层更新: 按决策周期聚合 reward 后更新高层策略 high_rewards = aggregate_by_decision_cycle(trajectories) high_policy.update(trajectories["high_obs"], trajectories["gait_actions"], high_rewards - switch_cost * trajectories["switch_count"])

训练参数:低层策略学习率 3e-4,高层策略学习率 1e-4。低层更新的 batch size 是 4096 条轨迹,高层因为决策频率低,batch size 通常只有低层的十分之一。GAE(广义优势估计)的 lambda 设 0.95,gamma 设 0.99。

4.4 拿到带源码的项目包,先看哪几个文件

这类四足步态学習的源码包,结构上通常包含 environment、algorithms、train、config 四个部分。我拿到手一般按这个顺序看:

首先是 config 文件。这里能看到 reward 各项的权重、状态空间维度、PPO 超参数——这些决定了项目作者认为什么重要。其次是环境文件里奖励函数和步态参数两部分,对照 4.1 和 4.2 的框架理解作者做了哪些取舍。最后才是训练脚本。如果先看训练脚本,你很容易被 1000 多行的参数初始化代码淹没。

有一个重点提醒:不要试图一次就跑通源码包里的完整训练。这类项目的完整训练时间普遍在一小时以上,中途任何一个参数不匹配都会静默失败。我常用的方式是先把训练步数砍到原来的十分之一,只跑 5 分钟,确认数据流是通的,再恢复完整预算。

5. 四足机器人步态训练避坑指南:4 个让训练崩盘的典型问题

5.1 现象一:好几种步态在训练中后期开始互相污染

现象:训练前期 trot 和 crawl 都能稳定跑出来,训练到中期后,两种步态的足端轨迹逐渐趋同,最终所有步态都变成同一种走法,而且往往是里最简单的那种。

原因:低层策略是共享参数的,不同步态的数据在优化时互相干扰。crawl 的支撑相交大,它的梯度会把 trot 里已经形成的“对侧腿同时摆动”的时序慢慢拉向“单腿依次摆动”,两种模式之间矩离不大,策略最终落到一个中间的折中状态。

解决:在低层策略的观测中加入一个步态 one-hot 向量,同时把步态参数从共享网络的前几层独立出来——即每条步态各自保留几层专属网络,只在最后两层共享。这是分层强化学习里常见的 “shared trunk with per-gait heads” 结构。代价是参数量增加,但步态独立性显著提升。

5.2 现象二:步态切换瞬间,关节出现明显抖振

现象:机器人能执行每种步态,但高层一发出切换命令,切换瞬间关节角度出现 10 Hz 左右的快速振荡,有时候直接把机器人抖倒。

原因:低层策略训练时用的是固定的步态参数,它只在稳态数据上见过这些参数。高层突然给出一个新参数组合时,低层策略的输入分布超出训练分布,策略给出无意义的高频动作。

解决:在低层训练时对步态参数做插值扰动——不要每次都喂训练完整的步态参数,而是随机把两个步态参数线性插值,让低层见过“中间态”。另一个更有效的手段是给切换过程加一个时间窗,高层发出切换指令后,步态参数在 0.3 秒内线性过渡,而不是瞬间跳变。过渡期的动作会在低层网络里生成平滑的中间输出,抖振基本消失。

5.3 现象三:训练几百轮后 reward 突然掉到接近 0,日志里全是 NaN

现象:reward 曲线前期正常上升,200 轮之后突然崩盘,训练日志里出现大量 NaN。

原因:大部分情况下是状态信息里出现了极端值,躯干角速度或关节速度字段在数值上发散,之后策略更新的梯度也跟着发散,整个网络权重变成 NaN。

解决:先看是哪个状态量爆了。在训练循环里对每个状态分量加一个有限性检查,一旦发现超过 10 倍合理范围就截断——但这是被动修复。更主动的做法是检查仿真环境里有没有“机器人被弹飞”的情况:足端接触模型在某些地形拼接处会产生异常接触力,把机器人弹射到空中,状态量直接爆。给躯干高度设置一个合理上限,超过就截断本回合,reward 归零,而不是让异常状态传入网络。

5.4 现象四:仿真里走得完美,换到真实机器人或新地形就摔

现象:仿真里 trot 步态已经能跟随 1.5 m/s 的目标速度,挪到真实四足上,机器人在第三步就直接砸地。

原因:这就是 sim2real gap,主要来自两个地方。第一是仿真里的地面刚度和摩擦系数是理想值,真实地面上每一个微小的凸起都会带来足端碰撞;第二是低层策略在仿真里学到了“用力推地”的动作,真实关节电机响应跟不上这个力矩请求。

解决:最实用的手段是域随机化,训练时把地面摩擦力在 0.4 到 1.0 之间随机扰动,把电机力矩上限在 ±20% 范围内随机缩放。如果换到新地形才摔,那就需要检查步态切换逻辑——上坡时默认的 trot 步态占空比太低,支撑相不足,换个更高占空比的步态基本能解决。

6. 验证多种步态是否真的学到:评估指标与导出前检查

训练完成之后,不要只看训练曲线的 reward 数值,那个数字在多种步态任务里具有很强的欺骗性——策略可能在 trot 上表现极好、在 crawl 上完全失效,但平均 reward 仍然在上升。我习惯用下面的评估表逐一检查:

指标建议通过线说明
步态保持时间≥5 秒不退化同一指令下,步态参数稳定执行
切换延迟≤0.3 秒指令发出到步态参数完成过渡的时间
速度跟踪误差≤0.15 m/s不同目标速度下稳定运行
姿态角最大偏差≤15°躯干不出现激烈倾斜
足端轨迹一致性标准差 < 0.02 m每个步态周期内落地位置不漂移

评估时有一个技巧:把高层策略的探索噪声关闭,固定输出每种步态,然后分别测量指标。如果固定输出场景下某种步态不达标,说明低层学习不充分;如果固定输出都达标,但自由切换后开始退化,问题出在高层的切换逻辑,去看 5.2 的过渡时间窗。

导出到真实机器人之前,还有一个容易忽略的检查项:把训练日志里的动作差分统计打出来,看每个关节是否存在超过 30% 的瞬间动作跳变。如果有,说明策略在仿真里依赖了高频抖动来维持平衡,这种策略导出去必摔。需要回去调大 4.1 里的动作平滑惩罚权重,重新训练。

我自己的习惯是:每次改完一个参数,都把评估指标表存一份带时间戳的副本。两周后你绝对不记得 gym_lr 从 3e-4 改成 2e-4 对切换延迟带来了什么影响,但指标表一对比就知道答案。这不是什么高级技巧,是我在这个项目里吃过最多亏的地方——把训练当黑匣子,把调参当玄学,最后只能靠翻日志重新推导。希望这些避坑思路和参数底线能帮你的四足机器人少摔几次,也别再为了“什么时候切换”这层逻辑熬夜调参。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进