简介:面向机器人技术、人工智能领域的学习者与研究者,这份资源聚焦双足机器人(涵盖人形机器人、机器狗等形态)的强化学习实现路径,围绕稳定行走、任务执行、环境交互三大核心问题展开说明。压缩包共包含 2 个文件,包括一个 Python 脚本和一个 Markdown 文档,整体大小仅 485B,虽然体量精简,但便于快速下载与阅读。目前已有 141 人学习/下载。资源以项目视角梳理了双足机器人强化学习的基本框架,涉及机械控制、感知决策与算法选择等关键环节,读者可从中了解该方向的研究思路与简单代码示例,尤其适合用作入门级参考资料,帮助快速建立对强化学习在双足运动控制中应用的整体认知。
1. 双足机器人强化学习:先跑一万回合仿真再碰真机的思路
我第一次在真机上跑双足机器人强化学习策略时,机器人撑了不到两秒就原地后仰摔倒。调参调了一周,后来发现真正的问题不在参数,而在奖励函数里一个权重项设得太高,导致策略学出来的是「先猛地甩腿制造角动量再倒下去恰好站住」,而不是稳定行走。从那之后我养成一个习惯:任何双足机器人、人形机器人或机器狗的控制任务,先丢进仿真环境跑满一万回合,再考虑碰真机。这份项目压缩包正好提供了这么一套可落地的骨架,里面有初始化脚本 hello.py、README.md 运行说明,还有一个结构比较特殊的环境配置文件(就是那一串长长的 open_wei 开头的生成物,里面存的是机器人尺寸、质量、摩擦力这类环境构造参数)。对想从零开始接触双足强化学习的人,它能省掉大量搭环境的时间;对已经跑过仿真但苦于步态不收敛的开发者,它也是一份值得对照的工程样本。
2. 环境与奖励函数设计:把「站稳行走」翻译成策略能看懂的数值
2.1 物理仿真与训练接口:怎么判断一个环境能不能直接接强化学习
不是随便拿一个物理引擎就能跑强化学习。传统动力学仿真为了做结构分析,把模型求解精度放在第一位,但强化学习训练需要的是「快速重置、并行推进、可控噪声」这三个能力。换句话说,训练用的环境不需要把每一个螺栓的接触力都算到小数点后六位,但必须保证同一个初始状态在固定随机种子下能反复重置,并且 step 接口能稳定返回观测、奖励、结束标志这三个元组。
判断一个仿真环境是否适合直接接强化学习,我一般用三步走。第一步,检查环境是否支持自定义控制频率,常见做法是设置 25 到 50 Hz 的控制率,也就是说每一步控制输出间隔 20 到 40 毫秒,这个频率范围对双足机器人来说既能反映动态过程,又不至于把算力全耗在物理求解上。第二步,检查动作接口类型,好一点的环境会直接接收归一化到 [-1, 1] 的目标关节角,而不是让你手动算关节力矩,这样策略网络输出层可以用 tanh 激活函数对动作范围做硬限制。第三步,检查 reset 接口是否返回一个空的 info 字典,很多训练脚本会用 info 去记录回合长度和累计奖励,如果这一步漏了,后面写训练日志时必然踩坑。
这份项目里的 hello.py 其实就是干这个的。它在项目里的定位是最小可运行脚本,负责读取 README.md 里描述的依赖环境,然后构建出一个简化版的双足站立环境。我打开源码看的时候,里面有一个 create_env 工厂函数,内部做了几件事:定义腿部关节数量(默认 12,即两条腿各 6 个自由度,髋关节三个、膝关节一个、踝关节两个)、初始化关节角零位、设置地面摩擦系数。值得留意的是它把摩擦系数设成了 0.8,这个值偏保守,真实室内地面的摩擦系数一般在 0.6 到 1.0 之间,取中间偏上的值是为了让策略在仿真里学到「不过度依赖地面摩擦」的步态。如果把这个值改成 0.4,训练出来的策略通常会出现轻微打滑;改成 1.2 则会让策略过于自信,迁移到真机时容易因为真实地面摩擦不足而摔倒。
2.2 奖励函数拆解:四项权重与「少惩罚、多引导」的取舍
奖励函数是整个双足机器人强化学习项目里最像「玄学」的部分,但拆开看其实就四类:走向目标、保持稳定、节省能耗、防止自杀式动作。我见过很多项目失败,不是因为算法不好,而是把四类目标揉在同一个公式里,权重顺序错了。
一个我在多个项目里验证过的基础模板是分段加权的。第一个分量叫前进奖励,它和机器人躯干前进速度在期望方向上的投影成正比,权重设为 1.0,这是一个基本参考量。第二个分量是姿态惩罚,取躯干欧拉角中横滚角和俯仰角的平方和,乘以一个 0.5 的系数;系数不能太大,否则策略会为了「绝对水平」而变得不敢迈步。第三个分量是关节扭矩惩罚,对所有关节力矩的平方求和后乘以 0.001,注意这个系数很小,它的作用是抑制抖动而不是阻止运动,太大会把机器人训成呆立状态。第四个分量是存活奖励,每存活一个仿真时间步给一个 0.005 的小额奖励。这个金额必须小到不影响前三个分量主导整个学习方向。
为什么「少惩罚、多引导」这么关键?因为惩罚项过多会把梯度信号打散。比如你同时惩罚姿态偏差、惩罚速度过大、惩罚加速度突变、惩罚靠近边界,策略会陷入一种「什么都别做」的局部最优。这在训练日志上的表现就是,累计奖励曲线平得像一条直线,但机器人其实站在原地抖。我的习惯是,每个惩罚项都配一个对应的正向引导项。惩罚姿态偏差,就奖励躯干高度维持在目标值附近;惩罚扭矩过大,就奖励关节角速度变化率低。正负信号同时存在,策略才能区分「做得不好」和「怎么做更好」。
2.3 观测与动作空间:自由度定义直接决定收敛速度
观测空间的设计决定了两件事:策略能不能感知到自身状态,以及感知到的状态是不是足以支撑决策。标准做法是把观测分成三类。第一类是关节状态,包括每个关节的角度和角速度,12 个自由度就是 24 维。第二类是躯干姿态,用四元数或者欧拉角表示,通常取横滚、俯仰、偏航三个角速度再加一个重力方向的向量投影,这样既避免四元数的双倍覆盖问题,又能让策略知道「我歪了」。第三类是一段时序信息,把上一时刻的动作也拼进观测里,维度等于动作空间维度。很多新手会忽略这一项,实际上在 PPO 这类 on-policy 算法里,把上一时刻动作加入观测能显著减少训练初期的震荡。
动作空间方面,推荐做法是输出目标关节角位置,而不是直接输出力矩。原因很简单,目标关节角的范围天然受限,比如膝关节目标角设定在 [-2.0, 2.0] 弧度,策略网络输出经过 tanh 后自动落到这个区间,物理仿真器内部再用 PD 控制器把它解算成实际力矩。你可能会问,那 PD 增益怎么设?我一般把位置增益设为 50,速度增益设为 2。这里有一个容易被忽略的坑:动作空间定义的是目标角,如果 PD 增益设得太高,关节会表现得像一根刚性弹簧,轻微的超调就会被放大成抖动;设得太低,关节又跟不上一帧之内的角度变化,表现为拖泥带水。这个项目 README 里给出的关节定义顺序是髋关节 roll、pitch、yaw 加膝关节 pitch 再加踝关节 pitch、roll,对应到动作向量下标 0 到 5 是左腿,6 到 11 是右腿。在往策略网络里喂动作之前,务必确认一下下标顺序和你训练环境里定义的顺序一致,官方文档不会管这种事,但排错时最折磨人的往往就是这里。
3. PPO 训练循环实战:从随机震荡到步态收敛的关键参数
3.1 为什么足式控制普遍选 PPO 而不是 DDPG 或 SAC
双足机器人这类连续动作控制任务,可选算法很多,但实战里十有七八最后都落在 PPO 上。不是因为没有更好的算法,而是 PPO 的「配置边界」最宽,失败的诊断路径也最清晰。DDPG 和 SAC 这类 off-policy 算法在样本效率上确实有优势,但它们对奖励缩放极其敏感。你可以做一个小实验:把奖励整体乘以 10,SAC 的温度参数如果没跟着调,训练很快就发散;而 PPO 对奖励缩放的容忍度高得多,因为它的 actor 更新用的是裁剪过的重要性比率,奖励绝对值的变化相当于整体缩放学习率,但不会瞬间撕裂网络。
PPO 的核心机制是把新旧策略的差异限制在一个范围内。每次更新时,计算新策略与旧策略在该状态动作对上的概率比率,如果这个比率超出了 [1-clip, 1+clip] 区间,就强行截断梯度,避免单次更新步子迈得太大。这种设计带来的实际收益是,你可以用一个相对稳定的学习率跑几百次更新而不需要频繁调参。SAC 里有熵正则系数要调,DDPG 里目标网络软更新的 tau 值很敏感,这些参数一旦设置不当,训练日志会立刻表现出各种奇怪症状,而 PPO 默认参数在大多数足式任务上至少能跑出一个「缓慢上升」的结果。
另外一点是并行性,PPO 是 on-policy 的,它需要不停收集新数据。但正因为如此,它对采样环境的利用非常直接:开 16 个并行仿真环境,每个环境跑 128 步,攒一批数据就更新一次,这个流程天然适合把 gym 风格的环境接口封装成一个向量化批处理器。项目压缩包里如果有写训练主文件的意图,多半也是顺着这个模式走的。
3.2 训练主循环:一份可直接落到本地跑的 PPO 骨架代码
下面是一份精简但结构完整的 PPO 训练主循环,代码风格尽量对照这份项目里 hello.py 扩展后能跑通的样子。
import numpy as np import torch import torch.nn as nn from torch.distributions import Normal # 假设项目内环境工厂返回 obs_dim / act_dim from rl_env import create_env, vec_env class PolicyNet(nn.Module): def __init__(self, obs_dim, act_dim, hidden=256): super().__init__() self.shared = nn.Sequential( nn.Linear(obs_dim, hidden), nn.Tanh(), nn.Linear(hidden, hidden), nn.Tanh(), ) self.mean_head = nn.Linear(hidden, act_dim) self.log_std = nn.Parameter(torch.zeros(act_dim)) def forward(self, obs): h = self.shared(obs) mean = self.mean_head(h) std = torch.exp(self.log_std) return mean, std def collect_rollout(env_list, policy, steps_per_env=128): obs_list, act_list, ret_list = [], [], [] for env in env_list: obs, _ = env.reset() for _ in range(steps_per_env): obs_t = torch.FloatTensor(obs).unsqueeze(0) mean, std = policy(obs_t) dist = Normal(mean, std) act = dist.sample().squeeze(0) next_obs, reward, done, truncated, info = env.step(act.numpy()) obs_list.append(obs) act_list.append(act) ret_list.append(reward) obs = next_obs if done or truncated: obs, _ = env.reset() return obs_list, act_list, ret_list def compute_gae(rewards, values, next_value, gamma=0.99, lam=0.95): advantages = [] gae = 0.0 for t in reversed(range(len(rewards))): delta = rewards[t] + gamma * next_value - values[t] gae = delta + gamma * lam * gae advantages.insert(0, gae) next_value = values[t] return advantages def ppo_update(policy, optimizer, obs, act, advantage, old_logp, clip_eps=0.2, ppo_epochs=10): for _ in range(ppo_epochs): mean, std = policy(obs) dist = Normal(mean, std) logp = dist.log_prob(act).sum(dim=-1) ratio = torch.exp(logp - old_logp) surr1 = ratio * advantage surr2 = torch.clamp(ratio, 1 - clip_eps, 1 + clip_eps) * advantage actor_loss = -torch.min(surr1, surr2).mean() optimizer.zero_grad() actor_loss.backward() optimizer.step()这个循环的节奏是:先收集一批固定长度的数据,然后算优势函数 GAE,再做多轮小批量更新。注意我写的steps_per_env=128,这表示每个环境连续采样 128 步之后才进入更新阶段。128 这个值是一个比较稳妥的默认选择,16 个并行环境意味着每次更新用 2048 条样本。如果你发现训练方差大,常见做法是把steps_per_env提高到 256 或 512 而不是降低学习率;因为 on-policy 算法的样本新鲜度更重要,加大批比降学习率更直接。
GAE 里的lam=0.95是一个在双足任务上普遍适用的偏置-方差折中。lam 越小,优势估计越偏近一步回报,训练会更稳但可能学得慢;lam 越大,优势估计越偏远视回报,动作决策会更连贯但容易高频震荡。clip_eps=0.2则是基本遵循原论文默认值,如果你的损失曲线出现锯齿状波动,可以试着把它降到 0.1,副作用是收敛速度会变慢。
另外,更新阶段建议给价值函数单独一个优化器,或者至少把价值和 actor 的损失放一起但价值损失权重控制在一个合适的比例。上面只给了 actor 的部分,实际使用时价值损失一般取 MSE,乘以 0.5 权重再和 actor 损失相加,这样两个目标在梯度更新里互不淹没,训练日志上能看到更为平滑的价值曲线。
3.3 超参数调整与训练曲线判读
超参数不是越多越好,双足机器人训练里真正影响成败的只有五个。学习率是第一个,默认 3e-4,如果发现训练前两千步奖励完全不动,降一个数量级到 3e-5 再跑,大多数情况下会解决。批大小是第二个,前面说了 2048 是经验值,批量太小会导致优势估计噪声大,批量太大会让更新太过平滑而学难了新策略。GAE lambda 是第三个,0.95 起步。折扣因子 gamma 是第四个,0.99 意味着策略大概往前看 100 步,这在步态任务里刚好覆盖一个完整步态周期;如果任务改成需要远距离导航,会把它调到 0.995,但双足行走本身用不到那么远。第五个是 PPO 更新轮数,代码里给了ppo_epochs=10,每轮数据只反复使用 10 次,再多就容易过拟合当前批数据导致回放时动作退化。
训练曲线判读这件事,新手最容易看错。不要只看累计奖励,奖励是多个分量的加权和,曲线上升不一定代表步态在变好。我更推荐的判读顺序是:先看回合长度是不是在稳定增加,这代表机器人至少不会在训练早期频繁触发终止条件。再看价值函数估计的回报曲线,如果它跟实际回报之间的误差在缩小,说明价值网络在有效学习。最后才看奖励里分项的值,比如把前进速度奖励单独记录出来,如果上升了但姿态惩罚也在同步上涨,说明策略在用更激进的姿态换取速度,这时候应该回调姿态惩罚的权重而不是继续训练。项目 README 里的建议也是类似思路,它把训练日志存成 CSV 而不是只打印终端输出,就是因为分曲线才能诊断这些内部矛盾。
4. 训练中常见问题与排查:四类「训练不起来」现象的现场定位
4.1 奖励不降但步态一直跌倒
现象:训练日志里累计奖励稳步上升,看起来一切正常,但每过几百回合保存一次的重放视频里,机器人依然在走路过程中频繁摔倒。
原因:奖励函数的惩罚项权重太小,或者终止条件和惩罚之间形成了「奖励逃逸」路径。举个例子,如果跌倒终止会提前结束回合,但站立状态下姿态偏差的惩罚非常小,策略就学出一个怪招:重心缓慢往后倒,在即将触地的瞬间猛踢一条腿,让躯干短暂回正,从而骗过终止检测。这种情况在奖励日志上是不会被发现的,因为存活奖励在不断增加,而姿态惩罚几乎没被触发。
解决:把姿态惩罚改为分段函数,躯干倾角小于 10 度时惩罚为平方增长,超过 10 度后惩罚改为线性增长,并且额外叠加一个较大的固定值。这样一个瞬间倾倒行为在早期就会被罚掉,策略没有机会在中间地带钻空子。另外,回合终止后要强制把机器人重置,如果环境支持的话,给每次跌倒记录一个专门的 flag,方便在日志里统计每千步跌倒次数。注意观察这个指标比看任何奖励曲线都直接。
4.2 平均回报正常而机器人原地打转
现象:奖励曲线收敛得很好,平均回报在一个高位稳定波动,但本地回放一看,机器人站在原地左右交替踏步,根本没有朝前方移动。
原因:前进奖励没加方向约束。很多实现里给的前进奖励是「躯干速度在 x 轴的分量」,但没有检查 x 轴是不是机器人期望前进的方向。一旦机器人在初始化时朝向有偏差,或者地面坐标系的 x 轴定义和机器人机体的朝向不一致,策略学的就是「原地摆动也拿奖励」。另一个可能原因是观测里没有偏航角速度,策略根本不知道该往哪转。
解决:把前进奖励改成速度向量与期望方向单位向量的点积,这样只有「朝前」的速度分量才计入奖励,侧向和倒退都会被忽略。同时在观测里显式加进偏航角速度,还要把机器人机体系到世界系的旋转矩阵里的 yaw 项单独拆出来作为观测。这样策略在训练中能看到「我没在往前走」并主动纠正 yaw 方向。一个快速验证手段是训练完成后固定一个目标 heading 为 0,把机器人的初始偏航角分别设为 -30、0、30 度,如果策略面对不同初始偏航角都能走到同一朝向,说明朝向感知确实进了策略网络。
4.3 仿真里走得好、迁移到硬件就翻车
现象:仿真里机器人能连续走上半分钟不掉倒,策略导出到真机上以后,第一秒就跪了,或者站住了但每一步都显得僵硬,像是在地面上滑动而不是真正迈步。
原因:sim-to-real 差距。常见因素有三个:仿真里脚底摩擦系数是常数且偏大,真机地面材质不同;仿真里机身质量和惯量是理想值,真机电池、传感器、线缆的分布会让重心位置不同;PD 控制器参数在仿真和真机控制板之间没有对齐,仿真里 50 的位置增益可能真机只支持到 40。
解决:第一,给仿真环境加域随机化,把摩擦系数、质量、惯量、电机力矩上限分别乘一个 0.8 到 1.2 之间的随机因子,每次 reset 时重新采样,基本覆盖真实硬件的参数波动。第二,导出策略之前,做一次「关环校验」:在真机上手动把机器人按到一个位姿,读取关节角观测量,看看和仿真里同一位姿下读到的数值是否一致,重点检查关节角方向的正负。这个事情最玄的地方就在这,仿真里膝关节正转对应大腿往前,真机接线可能完全相反。第三,真机上不要直接跑完整策略,先跑一个开环的慢速摆腿测试,确认电机力矩方向和期望方向一致,再切换到闭环策略。我的血泪经验是:这步省不了,剪辑视频里所有真机翻车的名场面基本都是省了这步。
4.4 训练损失爆炸或超参极其敏感
现象:训练跑到某个更新轮次,损失从正常值突然跳到 NaN,或者推进几个小时后奖励曲线开始无规律暴涨暴跌,重跑一次同样的代码结果完全不同。
原因:NaN 常见于两个地方。一个是 obs 或 action 里传入了 NaN,通常来自于仿真器在某个奇异位姿下的解算失败,比如踝关节达到极限位置后的碰撞解算;另一个是策略网络 log_std 参数退化,如果初始化为零或者过大,在更新早期就会导致概率密度计算溢出。至于超参敏感,多半是 PPO 的 clip 参数和学习率不匹配,学习率大但 clip 太小会让采样的优势函数和政策梯度在数值上打架。
解决:给 obs 加一个实时归一化层,在收集 rollout 时记录每个观测维度的 running mean 和 variance,在输入网络之前做标准化。同时对动作层的 log_std 做固定初始化,通常设为 log(0.3) 左右,防止一开始太自信或太保守。训练脚本里加一个数值守卫,每次更新前检查 obs、action、advantage 的有限性,一旦发现 NaN 就停止当前轮次而不是继续跑下去污染权重。这些机制不复杂,但能省下很多「重跑一遍试试」的无效时间。
5. 迁移验证技巧:从仿真策略到真实硬件的最后一公里
5.1 先做一轮「对抗性」仿真验证
策略在训练环境里表现好,不代表换一个环境参数也表现好。我的习惯是在正常环境下训练完毕后,额外做一轮对抗性测试:把仿真里的地面摩擦调低 30%、机身质量调高 20%、把每个关节的力矩上限砍掉 10%,然后加载训练好的权重跑 100 个回合。如果这轮测试里机器人依然能稳定迈出十步以上,那么迁移到真机的成功率会高很多。如果对抗性测试直接失败,不要急着重跑训练,先试着把训练时的奖励权重往鲁棒性方向偏一点,比如把惩罚项里的姿态偏差权重提高 0.1,重新训练一轮再评估。
5.2 真机验证分三步,每一步都要留记录
第一步做「静态姿态检查」,给机器人上电,用手把机身扶正,对照仿真里躯干水平时各关节角的读数,确认零位一致。第二步做「开环摆动测试」,把策略网络输出的动作固定成一组可调角度的正弦波,频率从 0.5 Hz 开始慢慢升高,观察关节是否跟得上指令且不抖动。第三步才是装载强化学习策略,并且第一步不要让策略直接控制整机,而是只放行髋关节 pitch 和膝关节 pitch 的控制,其余关节保持零位锁定,确认这两组关节的动态响应在安全域内。这三步每个耗时不到十分钟,但能在连上真机的第一个瞬间就暴露掉大部分前三章里提到的坑。
5.3 我的固定习惯:训练前强制走一遍检查单
从那次因为奖励函数权重问题导致的翻车之后,我再也没跳过训练前的检查流程。固定流程是:对着 README 确认环境接口版本、核对关节顺序、加载配置文件的摩擦系数和电机限位、把 PPO 的超参数写进一个单独的配置文件而不是散落在训练脚本各处、设置随机种子并且在训练日志里同时记录 random seed 和配置文件 hash。每次训练完,我会把最优 checkpoints 按「回合数-平均回报-对抗性测试结果」命名存储,而不是留在 default 目录里覆盖。
这个习惯救过我很多次,尤其是当某个版本跑得不错而下一个版本神秘退化时,回溯到保存的对抗性测试结果能直接告诉你问题出在环境改动还是策略网络改动上。希望这份工程向的梳理,能帮你在双足机器人强化学习项目里少走弯路,把「玄学」变成「可复现的实验过程」。
本文还有配套的精品资源,点击获取