☰
分层强化学习实现四足机器人多步态切换:从端到端困境到真机部署
2026/10/9 12:51:25 网站建设 项目流程

简介:这份资源面向机器人运动控制方向的研究者与开发者,聚焦用分层强化学习训练四足机器人掌握多种步态,解决复杂运动策略学习中状态与动作空间过大、训练效率低的问题。压缩包共50个文件,约3.77MB,以24个Python脚本为核心,配合9个hpp与3个cpp头文件与源文件搭建仿真环境,另有4个yaml参数配置、4个gif步态演示及md说明文档,覆盖算法、环境与配置各环节。项目将决策拆分为高层方向速度规划与低层腿足协调控制,借助PPO与stable_baselines3实现训练,并给出trot、bound、pace等多种步态的可视化结果,便于对照复现。目前已有230人学习下载,适合希望深入马尔可夫决策过程、策略梯度与分层框架实践的中高级读者,可据此快速搭建实验、调参优化并拓展至爬楼梯等复杂动作研究。

1. 四足机器人步态学习:为什么分层强化学习是绕不开的那条路

四足机器人步态学习这件事,单靠一个端到端策略网络硬训,能走,但走不好。我最早做四足控制的时候,用 PPO 直接输出十二个关节的目标角度,训练了三天,机器人在平地上能小碎步挪动,一上斜坡就翻,切换速度更是灾难——慢走和快跑之间没有过渡,策略在两种步态之间反复横跳,关节抖得像筛糠。后来换成分层强化学习,上层策略负责选步态类型和切换时机,下层策略负责在当前步态下输出关节动作,训练稳定性和步态多样性同时上来了。这个项目标题讲的就是这件事:用分层强化学习让四足机器人学会多种步态,而不是只会一种。适合谁看?做足式机器人控制的、想从单一步态扩展到多步态切换的、以及被端到端训练折磨过的从业者。下面我把这套方案的选型理由、实现路径、参数设置和踩坑记录拆开讲。

2. 分层强化学习做步态学习:上层选步态,下层控关节

2.1 为什么端到端策略学不会多种步态

端到端策略的问题不在于网络容量不够,而在于奖励信号冲突。四足机器人走平地和爬斜坡,最优关节轨迹差异很大,如果用一个策略同时拟合这两种模式,梯度更新会互相拉扯。更麻烦的是步态切换——从 trot 切到 bound,中间需要一个过渡过程,端到端策略没有显式的步态表示,切换点完全靠隐状态碰运气。我试过在观测里加一个步态标签作为输入,让策略自己学切换,结果策略学会了偷懒:不管什么地形都输出同一个标签,因为切换带来的奖励波动太大,策略宁愿保守。

分层强化学习的思路是把这个问题拆开。上层策略的观测是机器人本体状态加地形特征,动作空间是离散的步态选择;下层策略的观测是本体状态加当前步态编码,动作空间是连续的关节目标角度。两层策略分开训练,上层学切换逻辑,下层学步态执行。这样做的好处是每层策略的优化目标更清晰,下层只需要在给定步态下把动作做稳,上层只需要判断什么时候该换步态。

2.2 分层策略的网络结构与观测设计

上层策略我一般用一个小型 MLP,三层,每层 128 个单元,输出离散动作的 logits。观测包括:机身姿态四元数、机身线速度、角速度、四个足端的接触状态、地形坡度估计值。地形坡度估计值可以用机身姿态和足端高度差算出来,不需要额外的感知模块。下层策略用两层 MLP,每层 256 个单元,输出十二维关节目标角度,再经过一个 PD 控制器转成力矩。

步态编码我用的是 one-hot 向量,维度等于步态数量。比如 trot、walk、bound、pace 四种步态,编码就是四维 one-hot。下层策略的输入是本体观测拼接步态编码,这样同一个下层网络可以处理所有步态,不需要为每种步态单独训一个网络。如果步态数量多,可以用 embedding 层把 one-hot 压成低维向量,减少输入维度。

import torch import torch.nn as nn class UpperPolicy(nn.Module): """上层策略:输出步态选择的离散动作分布""" def __init__(self, obs_dim, num_gaits): super().__init__() self.net = nn.Sequential( nn.Linear(obs_dim, 128), nn.ReLU(), nn.Linear(128, 128), nn.ReLU(), nn.Linear(128, num_gaits) # 输出每个步态的 logit ) def forward(self, obs): logits = self.net(obs) return torch.distributions.Categorical(logits=logits) class LowerPolicy(nn.Module): """下层策略:给定步态编码,输出关节目标角度""" def __init__(self, obs_dim, gait_embed_dim, action_dim): super().__init__() self.gait_embed = nn.Linear(gait_embed_dim, 32) # 步态编码嵌入 self.net = nn.Sequential( nn.Linear(obs_dim + 32, 256), nn.ReLU(), nn.Linear(256, 256), nn.ReLU(), nn.Linear(256, action_dim) # 十二维关节目标角度 ) def forward(self, obs, gait_onehot): gait_feat = torch.relu(self.gait_embed(gait_onehot)) x = torch.cat([obs, gait_feat], dim=-1) return self.net(x)

上层策略的输出是离散分布,训练时用 REINFORCE 或者 PPO 的离散版本。下层策略输出连续动作,用 PPO 或者 SAC 都行。我一般用 PPO,因为实现简单,超参不敏感。步态编码的嵌入层是可选的,如果步态数量少于六种,直接用 one-hot 拼接就够了,加嵌入层反而增加调参负担。

2.3 训练流程:两阶段训练与联合微调

训练分两个阶段。第一阶段单独训练下层策略,固定步态编码,让下层学会在每种步态下稳定行走。具体做法是:每次 rollout 随机选一个步态编码,保持整个 episode 不变,下层策略只在这个步态下优化。这样训练出来的下层策略在每种步态下都有基本的行走能力。第二阶段训练上层策略,固定下层策略的参数,上层策略根据地形和本体状态选择步态,下层执行。上层策略的奖励包括:前进速度、姿态稳定、步态切换频率惩罚。

两阶段训练完之后,可以做一轮联合微调,上下层一起更新,但学习率要调小,否则下层策略会遗忘之前学到的步态。联合微调的时候,上层策略的探索噪声要降低,避免频繁切换步态导致下层策略来不及适应。

# 第一阶段:训练下层策略 for gait_id in range(num_gaits): gait_onehot = torch.zeros(num_gaits) gait_onehot[gait_id] = 1.0 for episode in range(num_episodes_per_gait): obs = env.reset() for step in range(max_steps): action = lower_policy(obs, gait_onehot) next_obs, reward, done, info = env.step(action) # 只优化下层策略,步态编码保持不变 lower_ppo.update(obs, action, reward, next_obs, done) obs = next_obs if done: break # 第二阶段:训练上层策略 for episode in range(num_upper_episodes): obs = env.reset() gait_onehot = upper_policy(obs).sample() # 上层选择步态 for step in range(max_steps): action = lower_policy(obs, gait_onehot) # 下层执行 next_obs, reward, done, info = env.step(action) # 上层策略根据奖励更新,下层策略冻结 upper_ppo.update(obs, gait_onehot, reward, next_obs, done) obs = next_obs if done: break

第一阶段的 episode 数量取决于步态复杂度,trot 和 walk 一般 500 到 1000 个 episode 就能走稳,bound 和 pace 需要 1500 个 episode 左右。第二阶段上层策略的训练量大概是下层总训练量的三分之一,因为上层只需要学切换逻辑,不需要学关节控制。联合微调阶段学习率降到初始值的十分之一,跑 200 到 300 个 episode 就够了。

3. 步态切换的奖励设计与地形观测处理

3.1 奖励函数拆解:前进、稳定、切换代价

奖励函数是分层强化学习里最容易翻车的地方。我见过有人把前进速度奖励设得很大,结果机器人学会了用前腿刨地往前蹭,姿态完全不管。也有人把姿态惩罚设得太重,机器人站在原地不敢动,因为动起来就有姿态误差。我的经验是分三块:前进奖励、稳定奖励、切换代价。

前进奖励用机身线速度在前进方向的分量,乘以一个系数。系数一般设 1.0 到 1.5,太小机器人走得慢,太大机器人会冲刺导致失稳。稳定奖励包括:机身高度维持在目标高度附近、机身俯仰角和滚转角接近零、足端接触力不要突变。机身高度奖励用指数函数,偏差越大奖励越小。姿态奖励用余弦相似度,机身朝向和目标朝向的夹角越小奖励越大。切换代价是上层策略每切换一次步态给一个小的负奖励,防止策略频繁切换。切换代价的绝对值不能太大,否则上层策略会一直不切换,退化成单一步态。

def compute_reward(obs, action, gait_changed): # 前进奖励:线速度在前进方向的分量 forward_vel = obs['linear_vel'][0] # 假设 x 轴是前进方向 reward_forward = 1.2 * forward_vel # 稳定奖励:机身高度和姿态 height_error = abs(obs['height'] - target_height) reward_height = 0.5 * np.exp(-5.0 * height_error) roll, pitch = obs['roll_pitch'] reward_attitude = 0.3 * (np.cos(roll) + np.cos(pitch)) / 2.0 # 切换代价:每次切换给 -0.1 reward_switch = -0.1 if gait_changed else 0.0 total_reward = reward_forward + reward_height + reward_attitude + reward_switch return total_reward

前进奖励的系数需要根据机器人的最大速度调。如果机器人最大速度是 1.5 米每秒,系数设 1.2 意味着全速前进时前进奖励是 1.8,加上稳定奖励大概 0.8,总奖励 2.6 左右。切换代价 -0.1 相对于总奖励来说很小,但累积起来能抑制频繁切换。如果发现上层策略还是频繁切换,可以把切换代价调到 -0.3,或者加一个切换冷却时间,切换后若干步内不能再切。

3.2 地形观测:用本体感受估计坡度与粗糙度

分层强化学习做地形适应,不一定需要外部感知。本体感受就够用。坡度估计可以用四个足端的高度差算:前足和后足的高度差除以轴距,得到俯仰方向的坡度;左足和右足的高度差除以轮距,得到滚转方向的坡度。粗糙度可以用足端接触力的方差来估计,方差大说明地面不平。

地形观测的维度不用太高,坡度两个维度、粗糙度一个维度,加上机身姿态和速度,总共十几个维度就够了。观测维度太高会增加上层策略的训练难度,而且容易过拟合。我试过把地形观测加到三十维,结果上层策略在训练集上表现很好,换一个地形就翻车,因为策略记住了特定地形的观测模式,没有学到泛化的切换逻辑。

def estimate_terrain(obs): # 足端高度:四个足端的 z 坐标 foot_heights = obs['foot_heights'] # [fl, fr, rl, rr] wheelbase = 0.4 # 轴距,单位米 track_width = 0.3 # 轮距,单位米 # 俯仰方向坡度:前足平均高度减后足平均高度,除以轴距 front_height = (foot_heights[0] + foot_heights[1]) / 2.0 rear_height = (foot_heights[2] + foot_heights[3]) / 2.0 pitch_slope = (front_height - rear_height) / wheelbase # 滚转方向坡度:左足平均高度减右足平均高度,除以轮距 left_height = (foot_heights[0] + foot_heights[2]) / 2.0 right_height = (foot_heights[1] + foot_heights[3]) / 2.0 roll_slope = (left_height - right_height) / track_width # 粗糙度:足端接触力的方差 contact_forces = obs['contact_forces'] roughness = np.var(contact_forces) return np.array([pitch_slope, roll_slope, roughness])

坡度估计的精度取决于足端高度测量的噪声。真实机器人上足端高度用关节编码器算,会有累积误差。我一般会在估计值上做一个低通滤波,截止频率设 5 赫兹左右,滤掉高频噪声。粗糙度用接触力方差,接触力用足端力传感器测,没有力传感器的话可以用关节电流估计,精度差一些但能用。

3.3 步态编码与切换时机:上层策略的观测窗口

上层策略的观测窗口很重要。如果只看当前时刻的观测,上层策略无法判断地形变化趋势,切换时机会滞后。我一般给上层策略一个观测窗口,包含过去 10 到 20 步的观测,用一维卷积或者 LSTM 提取时序特征。一维卷积比 LSTM 训练快,而且不容易梯度爆炸。卷积核大小设 3 到 5,层数两层就够了。

切换时机的判断逻辑是:当地形坡度超过阈值,或者粗糙度超过阈值,上层策略切换到更适合的步态。比如平地上用 trot,上斜坡切到 walk,下斜坡切到 bound。阈值不需要手动设,让上层策略自己学。但可以在奖励函数里加一个引导项:如果地形坡度和当前步态不匹配,给一个小的负奖励。引导项的系数要小,否则会限制上层策略的探索。

class UpperPolicyWithHistory(nn.Module): """带观测窗口的上层策略""" def __init__(self, obs_dim, num_gaits, window_size=15): super().__init__() self.conv = nn.Sequential( nn.Conv1d(obs_dim, 64, kernel_size=3, padding=1), nn.ReLU(), nn.Conv1d(64, 64, kernel_size=3, padding=1), nn.ReLU(), ) self.fc = nn.Sequential( nn.Linear(64 * window_size, 128), nn.ReLU(), nn.Linear(128, num_gaits) ) def forward(self, obs_history): # obs_history: [batch, window_size, obs_dim] x = obs_history.transpose(1, 2) # 转成 [batch, obs_dim, window_size] x = self.conv(x) x = x.flatten(start_dim=1) logits = self.fc(x) return torch.distributions.Categorical(logits=logits)

观测窗口的大小需要根据控制频率调。如果控制频率是 50 赫兹,窗口 15 步对应 0.3 秒,足够判断地形变化趋势。如果控制频率是 100 赫兹,窗口 15 步只有 0.15 秒,可能不够,需要加到 30 步。窗口太大也会有问题,卷积的计算量增加,而且早期观测对当前决策的影响被稀释。我一般控制在 0.2 到 0.5 秒的时间窗口内。

4. 避坑与排查:分层强化学习步态学习的五个血泪教训

4.1 下层策略遗忘:联合微调时学习率没降

现象:两阶段训练完之后,联合微调跑了 50 个 episode,机器人突然不会走 trot 了,只会用 bound 蹦。原因:联合微调时下层策略的学习率没有降,上层策略切换步态的频率又高,下层策略在多个步态之间反复更新,把之前学到的 trot 策略覆盖了。解决:联合微调时下层策略的学习率降到初始值的十分之一,上层策略的学习率保持不变。如果还是遗忘,可以冻结下层策略的前几层,只微调输出层。

4.2 上层策略摆烂:切换代价设得太大

现象:上层策略训练完之后,不管什么地形都输出同一个步态,从来不切换。原因:切换代价设得太大,比如 -1.0,上层策略发现不切换的累积奖励更高,因为切换带来的前进奖励提升抵不过切换代价。解决:切换代价从 -0.1 开始调,观察上层策略的切换频率。如果切换频率太低,减小切换代价;如果切换频率太高,增大切换代价。我一般把切换代价设在 -0.05 到 -0.2 之间。

4.3 地形观测噪声:坡度估计值抖动导致误切换

现象:机器人在平地上走,上层策略频繁切换步态,关节动作抖动。原因:坡度估计值有噪声,足端高度测量的误差导致坡度估计在零附近抖动,上层策略误以为地形在变化。解决:在坡度估计值上加低通滤波,截止频率 5 赫兹。如果还是抖动,可以在上层策略的观测里加一个坡度变化率的阈值,只有坡度变化超过阈值才触发切换。

4.4 步态编码维度不匹配:下层策略输入维度对不上

现象:训练下层策略的时候正常,加载上层策略做联合推理时报维度错误。原因:下层策略的步态编码维度是训练时定的,上层策略输出的步态数量如果和下层不一致,拼接的时候维度对不上。解决:把步态数量和步态编码维度写成配置文件,上下层策略都从配置文件读。如果步态数量变了,下层策略的嵌入层需要重新训练,不能直接复用。

4.5 仿真到真机迁移:关节摩擦和延迟没建模

现象:仿真里步态切换很流畅,真机上切换时关节抖动,甚至触发过流保护。原因:仿真里的关节摩擦模型太理想,真机上的摩擦非线性,而且控制指令有延迟。解决:在仿真里加关节摩擦模型,用库仑摩擦加粘性摩擦的组合。控制延迟可以在仿真里加一个延迟缓冲,把动作指令延迟几毫秒再执行。迁移到真机时,先用手动模式测试每种步态的关节轨迹,确认没有过流风险再跑上层策略。

5. 从仿真到真机:步态切换的验证方法与调参技巧

真机验证分层强化学习步态切换,我一般分三步走。第一步,固定步态编码,手动切换,验证下层策略在每种步态下的关节轨迹是否安全。具体做法是:把机器人吊起来,足端悬空,手动给下层策略输入步态编码,观察关节动作是否平滑,有没有突变。如果关节动作有突变,检查下层策略的输出是否经过了平滑滤波,我一般会在输出层加一个一阶低通滤波,截止频率 20 赫兹。

第二步,让上层策略在平地上跑,观察切换频率和切换时的姿态变化。平地上上层策略应该很少切换,如果频繁切换,说明地形观测有噪声或者切换代价太小。切换时的姿态变化用机身俯仰角和滚转角的峰值来衡量,峰值超过 15 度说明切换太剧烈,需要在下层策略的奖励里加一个切换时的姿态惩罚。

第三步,在斜坡和粗糙地面上跑,验证上层策略的切换逻辑是否符合预期。我一般会记录切换时的地形坡度和粗糙度,画成散点图,看切换点是否集中在地形特征变化的区域。如果切换点分散,说明上层策略没有学到有效的地形特征,需要增加地形观测的维度或者调整观测窗口的大小。

调参方面,下层策略的 PPO 裁剪系数我一般设 0.2,学习率 3e-4,batch size 4096。上层策略的 PPO 裁剪系数设 0.1,学习率 1e-4,batch size 2048。上层策略的学习率比下层小,因为上层策略的观测维度低,容易过拟合。折扣因子下层设 0.99,上层设 0.95,上层更关注近期奖励,因为步态切换的收益是短期的。

参数下层策略上层策略
学习率3e-41e-4
PPO 裁剪系数0.20.1
batch size40962048
折扣因子0.990.95
网络层数22
每层单元数256128

验证步态切换是否成功,我一般看三个指标:切换成功率、切换时的姿态误差峰值、切换后的速度恢复时间。切换成功率是上层策略发出切换指令后,下层策略在 0.5 秒内达到目标步态的速度范围。姿态误差峰值是切换过程中机身俯仰角和滚转角的最大偏差。速度恢复时间是切换后速度回到目标速度正负 10% 以内的时间。这三个指标在仿真里调好之后,真机上一般会有 10% 到 20% 的衰减,属于正常范围。

真机调参有个玄学:同样的参数,早上跑和下午跑效果不一样。后来发现是电池电压的影响,电压低的时候关节力矩输出不足,步态切换会变慢。解决办法是在观测里加电池电压,让下层策略自己适应电压变化。这个改动很小,但效果很明显,电压从满电到低电,步态切换的成功率从 60% 提到了 85%。

最后说一个我自己的习惯:每次改奖励函数或者网络结构,先跑 100 个 episode 看趋势,不要等跑完几千个 episode 再看。如果 100 个 episode 奖励曲线没有上升趋势,说明改动有问题,赶紧回滚。这个习惯帮我省了很多时间,也避免了很多次翻车。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询