☰
强化学习让机器人学会走路:奖励函数设计与PPO实战
2026/10/17 5:21:47 网站建设 项目流程

1. 从零理解机器人试错学走路的本质

1.1 为什么“试错”是机器人学会走路的唯一出路

很多人第一次接触机器人走路这件事,脑子里冒出来的第一个念头是:直接写一套规则不就行了吗?抬腿多少度、迈步多长、重心怎么移,全部写死。我一开始也是这么想的,直到真正上手才发现,这条路根本走不通。

原因很简单:双足机器人的动力学太复杂了。一个十二自由度的双足机器人,它的关节角度、角速度、地面摩擦力、重心位置、惯性矩阵,这些变量之间是高度耦合的。你改一个髋关节的角度,膝盖和踝关节的受力立刻全变。用传统控制方法,你得先建立精确的动力学方程,然后做线性化,再设计控制器。问题是,地面摩擦系数你没法精确知道,机器人自身的质量分布也有误差,电机还有响应延迟。这些误差累积起来,写死的规则一上真实地面就摔。

强化学习解决这个问题的思路完全不同。它不去精确建模整个系统,而是让机器人在仿真环境里反复尝试,摔倒了就调整策略,走稳了就强化这个行为。这个过程和人学走路一模一样——没有哪个小孩是看了力学教材才会走路的,都是摔了无数次之后,身体自己找到了平衡的感觉。

强化学习的核心逻辑:智能体在环境中执行动作,环境返回新的状态和奖励信号,智能体根据奖励调整策略,目标是让长期累积奖励最大化。

这里的关键在于奖励函数的设计。你不能只给一个“走到终点给满分”的稀疏奖励,因为机器人随机探索到终点的概率几乎为零。你得设计稠密奖励:往前走给正奖励,摔倒给负奖励,身体保持直立给正奖励,关节力矩过大给负奖励。这些奖励项加权求和,构成了机器人学习的“指挥棒”。

1.2 强化学习与传统控制的分水岭在哪里

我经常被问到这个问题:强化学习和传统的PID控制、MPC模型预测控制到底有什么区别?什么时候该用哪个?

传统控制方法的核心是基于模型的。你得知道系统的数学模型,然后设计控制器。优点是稳定、可解释、计算量小。缺点是模型不准的时候性能急剧下降,而且对于高自由度、强非线性的系统,建模本身就极其困难。

强化学习的核心是无模型的。它不需要你知道精确的动力学方程,只需要你能提供一个仿真环境,让智能体在里面试错。优点是适应性强,能处理传统方法搞不定的复杂场景。缺点是训练时间长、样本效率低、奖励函数设计需要经验。

对比维度传统控制(PID/MPC)强化学习(RL)
模型依赖需要精确模型无需精确模型
调试方式调参数设计奖励函数
计算开销实时计算量小训练开销大,推理开销小
适应性环境变化需重新调参泛化能力较强
可解释性强弱
典型场景工业机械臂重复作业复杂地形行走、灵巧操作

实际项目中,我见过太多人一上来就想用强化学习搞定一切,结果连仿真环境都没搭好就放弃了。我的建议是:如果你的任务有成熟的传统控制方案,先用传统方案跑通基线,再用强化学习去提升。这样你至少有一个能工作的系统,而不是从零开始赌强化学习能收敛。

1.3 仿真环境选型:为什么MuJoCo是绕不开的选择

做机器人强化学习,仿真环境的选择直接决定了你的开发效率。我试过PyBullet、Gazebo、Isaac Gym、MuJoCo这几个主流平台,各有优劣,但MuJoCo在双足行走这个场景下确实是最优解。

MuJoCo的物理引擎对接触力的计算非常精确,这对双足行走至关重要。机器人脚掌和地面的接触力如果算不准,训练出来的策略迁移到真机上就会完全失效。PyBullet虽然免费开源,但接触求解的精度差一些,训练出来的策略容易出现“仿真里走得稳,真机上抖成筛子”的情况。Gazebo更偏向于系统集成测试,物理精度不是它的强项。Isaac Gym的卖点是GPU并行加速,可以同时跑几千个环境,训练速度极快,但它的物理精度和MuJoCo比还是有差距,而且对硬件要求高。

我目前的配置是:MuJoCo做算法开发和策略验证,Isaac Gym做大规模并行训练加速。先用MuJoCo把算法调通,确认奖励函数和网络结构没问题,再迁移到Isaac Gym上做大规模训练。这样既保证了物理精度,又兼顾了训练效率。

实操心得:MuJoCo从2.1版本开始已经完全开源免费,安装方式很简单,pip install mujoco就能搞定。但要注意Python版本兼容性,建议用Python 3.8到3.10之间的版本,太新的Python版本可能会有依赖冲突。

仿真环境搭建好之后,下一步就是定义机器人的模型文件。MuJoCo使用MJCF格式的XML文件来描述机器人,包括连杆几何、关节类型、质量惯性、碰撞体、执行器等。这个文件写得好不好,直接影响训练效果。我的经验是:质量惯性参数一定要尽量准确,如果不知道真实值,至少要做合理的估计,否则训练出来的策略在真机上会完全不一样。

2. 奖励函数设计:决定机器人能不能学会走路的关键

2.1 奖励函数的组成与权重分配

奖励函数是强化学习里最需要经验和直觉的部分。我见过太多人在这里踩坑:奖励设计得不好,机器人要么学会原地不动骗奖励,要么学会疯狂抖动来获取微小的前进奖励,要么直接摆烂摔倒因为负奖励太大导致梯度爆炸。

一个能用的双足行走奖励函数,通常包含以下几个部分:

  • 前进速度奖励:机器人躯干在前进方向上的速度,乘以一个正权重。这是最主要的引导信号。
  • 保持直立奖励:躯干高度和姿态的偏差惩罚,鼓励机器人站直。
  • 关节力矩惩罚:关节输出力矩的平方和,乘以负权重,防止机器人用蛮力。
  • 关节速度惩罚:关节角速度的平方和,乘以负权重,让动作平滑。
  • 接触奖励:脚掌与地面的接触模式奖励,鼓励交替迈步。
  • 存活奖励:每个时间步给一个小的正奖励,鼓励机器人不要摔倒。

这些项的权重需要仔细调节。我的经验值是:前进速度奖励权重设为1.0到2.0,直立奖励权重设为0.5到1.0,力矩惩罚权重设为0.0001到0.001,速度惩罚权重设为0.0001到0.0005。存活奖励一般设为0.1到0.5。

注意:奖励权重的量级差异很大,前进速度可能是米每秒级别,力矩可能是几十牛米级别。如果不做归一化,力矩惩罚会完全主导整个奖励信号。建议在计算奖励之前,先对每个分量做归一化处理。

2.2 稀疏奖励与稠密奖励的取舍

稀疏奖励是指只在任务完成时给奖励,比如“走到终点给100分,其他时候0分”。稠密奖励是指每个时间步都给反馈,比如“往前走一步给0.1分”。

理论上稀疏奖励更接近真实目标,但实际训练中几乎不可行。因为机器人随机探索到终点的概率极低,智能体在获得第一个正奖励之前可能已经探索了几百万步,梯度信号完全消失。

稠密奖励的问题是奖励塑形偏差。你设计的稠密奖励可能引导机器人学会一些你没想到的“作弊”行为。比如你给“脚掌接触地面”正奖励,机器人可能学会一直拖着脚走,因为这样接触时间最长。你给“躯干高度”正奖励,机器人可能学会踮着脚尖走,因为这样躯干最高。

我的解决方案是课程学习:先用稠密奖励让机器人学会基本的站立和迈步,然后逐渐降低稠密奖励的权重,增加稀疏奖励的权重,最终让机器人以完成任务为主要目标。这个过程需要反复调试,没有一劳永逸的参数。

2.3 奖励函数代码实现与调试技巧

下面是一个我实际项目中使用的奖励函数实现,基于MuJoCo和Gym接口:

import numpy as np def compute_reward(env, action, obs): # 提取状态信息 torso_height = obs[0] # 躯干高度 torso_angle = obs[1] # 躯干倾角 forward_vel = obs[2] # 前进速度 joint_angles = obs[3:15] # 12个关节角度 joint_vels = obs[15:27] # 12个关节角速度 foot_contacts = obs[27:29] # 左右脚接触标志 # 前进速度奖励 reward_forward = 1.5 * forward_vel # 直立奖励 reward_upright = 0.8 * np.exp(-5.0 * torso_angle**2) # 高度奖励 target_height = 0.75 reward_height = 0.5 * np.exp(-20.0 * (torso_height - target_height)**2) # 关节力矩惩罚 torque = env.get_joint_torques() penalty_torque = -0.0002 * np.sum(np.square(torque)) # 关节速度惩罚 penalty_velocity = -0.0001 * np.sum(np.square(joint_vels)) # 接触奖励:鼓励交替接触 if foot_contacts[0] > 0.5 and foot_contacts[1] < 0.5: reward_contact = 0.3 elif foot_contacts[1] > 0.5 and foot_contacts[0] < 0.5: reward_contact = 0.3 elif foot_contacts[0] > 0.5 and foot_contacts[1] > 0.5: reward_contact = -0.5 # 双脚同时着地惩罚 else: reward_contact = -1.0 # 双脚离地惩罚 # 存活奖励 reward_alive = 0.2 total_reward = (reward_forward + reward_upright + reward_height + penalty_torque + penalty_velocity + reward_contact + reward_alive) return total_reward

调试奖励函数的时候,我习惯先把所有惩罚项设为0,只保留前进奖励和存活奖励,看机器人能不能学会往前走。如果能,再逐步加入惩罚项,观察行为变化。如果加入某个惩罚项之后训练不收敛了,说明这个惩罚项的权重太大了,需要降低。

还有一个技巧是记录每个奖励分量的值,训练过程中打印出来。如果某个分量的值远大于其他分量,说明权重需要调整。理想情况下,各个分量的值应该在同一个量级。

3. 算法选型与网络结构设计

3.1 PPO为什么成为机器人行走的首选算法

强化学习算法有很多种,DQN、DDPG、TD3、SAC、PPO,每个都有自己的适用场景。但在机器人连续控制这个领域,PPO(近端策略优化)是目前最主流的选择。

PPO的核心优势是稳定。它通过限制每次策略更新的幅度,避免了训练过程中的剧烈波动。机器人行走这个任务,策略稍微变一点,行为可能就完全不同,所以稳定性比样本效率更重要。SAC虽然样本效率更高,但在高维连续控制任务上容易不稳定。DDPG和TD3的探索能力不如PPO,容易陷入局部最优。

PPO的另一个优势是实现简单。它的核心思想就是:收集一批数据,计算优势函数,然后做多次梯度更新,但每次更新时限制新旧策略的KL散度不要太大。这个限制通过裁剪(clip)来实现,代码量很少。

# PPO核心更新逻辑 def ppo_update(policy, old_policy, optimizer, states, actions, old_log_probs, advantages, returns, clip_ratio=0.2): for _ in range(epochs): # 计算新策略下的动作概率 new_log_probs, entropy = policy.evaluate(states, actions) # 计算概率比 ratio = torch.exp(new_log_probs - old_log_probs) # 裁剪目标函数 surr1 = ratio * advantages surr2 = torch.clamp(ratio, 1 - clip_ratio, 1 + clip_ratio) * advantages policy_loss = -torch.min(surr1, surr2).mean() # 价值函数损失 values = policy.get_value(states) value_loss = F.mse_loss(values, returns) # 总损失 loss = policy_loss + 0.5 * value_loss - 0.01 * entropy.mean() optimizer.zero_grad() loss.backward() optimizer.step()

实操心得:PPO的clip_ratio一般设为0.2,这个值比较通用。如果训练不稳定,可以降到0.1;如果训练太慢,可以升到0.3。但不要超过0.3,否则裁剪就失去意义了。

3.2 策略网络与价值网络的结构设计

网络结构的设计对训练效果影响很大。我的经验是:策略网络和价值网络共享底层特征提取层,但顶层分开。这样既能共享特征,又能让两个网络专注于各自的任务。

对于双足机器人,输入维度通常是躯干状态(高度、角度、速度)加上12个关节的角度和角速度,总共大约30到40维。输出维度是12个关节的目标角度,每个关节一个连续值。

网络结构我一般用三层全连接:第一层256个神经元,第二层128个神经元,第三层64个神经元。激活函数用Tanh或者ELU。Tanh的输出范围是-1到1,适合控制任务;ELU的梯度更平滑,训练更稳定。我通常用ELU。

import torch.nn as nn class ActorCritic(nn.Module): def __init__(self, obs_dim, action_dim): super().__init__() # 共享特征层 self.shared = nn.Sequential( nn.Linear(obs_dim, 256), nn.ELU(), nn.Linear(256, 128), nn.ELU(), nn.Linear(128, 64), nn.ELU() ) # 策略头:输出动作均值和对数标准差 self.actor_mean = nn.Linear(64, action_dim) self.actor_log_std = nn.Parameter(torch.zeros(action_dim)) # 价值头:输出状态价值 self.critic = nn.Linear(64, 1) def forward(self, obs): features = self.shared(obs) action_mean = torch.tanh(self.actor_mean(features)) action_std = torch.exp(self.actor_log_std) value = self.critic(features) return action_mean, action_std, value

动作输出的tanh激活函数把动作限制在-1到1之间,然后通过一个缩放因子映射到关节的实际角度范围。这个缩放因子需要根据机器人的关节限位来设定。比如髋关节的活动范围是-1.5到1.5弧度,缩放因子就是1.5。

3.3 观测空间与动作空间的设计细节

观测空间的设计直接决定了机器人能感知到什么信息。太少的信息会导致机器人“盲人摸象”,太多的信息会增加训练难度。

我的观测空间包含以下几类信息:

  • 躯干状态:高度、俯仰角、滚转角、前进速度、侧向速度、偏航角速度。这些是机器人平衡的核心信息。
  • 关节状态:12个关节的角度和角速度。这是机器人感知自身姿态的基础。
  • 接触状态:左右脚的接触力大小。这是机器人感知地面接触的关键。
  • 上一帧动作:上一时刻的关节目标角度。这给机器人提供了动作平滑性的参考。

动作空间我选择目标关节角度而不是力矩。原因是角度控制更稳定,训练更容易收敛。力矩控制虽然更灵活,但容易导致机器人用蛮力,而且对电机模型的要求更高。目标角度通过PD控制器转换成力矩,PD参数需要仔细调节。

注意:观测信息一定要做归一化。不同物理量的量级差异很大,比如高度是0.7米左右,角速度可能是几弧度每秒,接触力可能是几百牛顿。不做归一化的话,网络很难学到有效的特征。我一般用运行均值方差来做归一化,训练过程中动态更新。

4. 训练流程与调参实战

4.1 从零开始搭建训练环境

搭建训练环境的第一步是安装依赖。我用的技术栈是:Python 3.9 + MuJoCo 2.3 + Gym 0.26 + PyTorch 1.13。这个组合经过多次验证,兼容性最好。

pip install mujoco==2.3.0 pip install gym==0.26.2 pip install torch==1.13.1 pip install numpy==1.23.5

安装完成之后,先跑一个简单的测试,确认MuJoCo能正常渲染和仿真:

import mujoco import mujoco.viewer model = mujoco.MjModel.from_xml_path("humanoid.xml") data = mujoco.MjData(model) with mujoco.viewer.launch_passive(model, data) as viewer: while viewer.is_running(): mujoco.mj_step(model, data) viewer.sync()

如果能看到机器人在窗口里掉下来,说明环境没问题。接下来就是定义Gym环境接口,把MuJoCo的仿真封装成标准的强化学习环境。

4.2 训练过程中的关键参数调节

训练参数对结果影响巨大。我整理了一份自己常用的参数表:

参数名推荐值说明
学习率3e-4策略网络的学习率,太大不收敛,太小训练慢
批量大小64每次更新的样本数
经验回放大小2048每次收集的步数
更新轮数10每批数据重复训练的轮数
折扣因子0.99未来奖励的折扣,行走任务用0.99比较合适
GAE参数0.95优势估计的平滑参数
裁剪比例0.2PPO的裁剪范围
熵系数0.01鼓励探索,太大导致策略随机
价值损失系数0.5价值函数损失的权重

这些参数不是绝对的,需要根据具体任务调整。我的经验是:先固定其他参数,只调学习率和熵系数。学习率决定了训练能不能收敛,熵系数决定了探索的充分程度。这两个调好了,其他参数微调一下就行。

训练过程中要监控几个关键指标:平均回报、策略熵、价值损失、KL散度。平均回报应该稳步上升,策略熵应该缓慢下降,价值损失应该逐渐减小,KL散度应该保持在0.01到0.05之间。如果KL散度突然变大,说明策略更新太剧烈,需要降低学习率或者增加裁剪比例。

4.3 训练加速与并行化技巧

纯CPU训练双足行走,收敛需要几百万步,可能要跑好几天。加速训练有几个方向:

第一,用GPU加速网络前向和反向传播。PyTorch默认会用GPU,但要注意把数据和模型都放到GPU上。对于小网络,GPU的加速比可能不明显,但对于大批量数据,GPU优势很大。

第二,用向量化环境并行采集数据。Gym提供了VectorEnv接口,可以同时跑多个环境实例。我一般用8到16个并行环境,训练速度能提升5到10倍。

from gym.vector import SyncVectorEnv def make_env(seed): def _init(): env = HumanoidEnv() env.seed(seed) return env return _init envs = SyncVectorEnv([make_env(i) for i in range(16)])

第三,用Isaac Gym做大规模并行训练。Isaac Gym可以在GPU上同时跑几千个环境,训练速度比CPU并行快几十倍。但Isaac Gym的物理精度不如MuJoCo,适合做前期的大规模探索,后期再用MuJoCo做精细调优。

实操心得:并行环境不是越多越好。环境太多,每个环境采集的数据就少,策略更新频率降低,反而可能影响收敛。我一般用8到32个环境,根据任务复杂度调整。

5. 仿真到真机的迁移与常见问题排查

5.1 Sim-to-Real迁移的核心挑战

仿真里训练好的策略,直接放到真机上,十有八九会摔。原因主要有三个:

第一,物理参数不匹配。仿真里的摩擦系数、质量、惯性、电机响应特性都和真机有差异。仿真里地面摩擦系数是1.0,真机可能是0.6,机器人脚底一打滑就摔了。

第二,传感器噪声。仿真里的状态信息是精确的,真机上的IMU有噪声,关节编码器有量化误差,接触传感器有延迟。这些噪声会让策略的输出抖动。

第三,执行器延迟。仿真里电机是瞬时响应的,真机上有几十毫秒的延迟。这个延迟在快速行走时会导致相位错位,机器人越走越晃。

解决这些问题的核心思路是域随机化。在训练时,随机改变仿真环境的物理参数,让策略学会适应不同的条件。具体做法是:每次重置环境时,随机采样摩擦系数、质量、电机增益、传感器噪声等参数,让策略在这些变化中学会鲁棒的控制。

def randomize_domain(env): # 随机化摩擦系数 env.model.geom_friction[:, 0] = np.random.uniform(0.5, 1.5) # 随机化躯干质量 env.model.body_mass[1] *= np.random.uniform(0.9, 1.1) # 随机化电机增益 env.model.actuator_gainprm[:, 0] *= np.random.uniform(0.8, 1.2) # 随机化传感器噪声 env.noise_scale = np.random.uniform(0.0, 0.05)

域随机化的范围需要仔细调节。范围太小,策略学不到鲁棒性;范围太大,策略学不会走路。我的经验是:先从小范围开始,训练收敛后逐步扩大范围。比如摩擦系数先从0.8到1.2,稳定后再扩大到0.5到1.5。

5.2 常见训练失败模式与解决方案

训练过程中会遇到各种奇怪的现象,我整理了一份速查表:

现象可能原因解决方案
机器人原地不动前进奖励太小,存活奖励太大增大前进奖励权重,降低存活奖励
机器人疯狂抖动动作惩罚太小,熵系数太大增大动作惩罚,降低熵系数
机器人一直摔倒直立奖励太小,初始探索太随机增大直立奖励,降低初始动作标准差
训练不收敛学习率太大,奖励量级差异大降低学习率,归一化奖励
策略突然崩溃KL散度太大,更新太剧烈降低学习率,增大裁剪比例
仿真稳真机摔域随机化不足,物理参数不匹配扩大域随机化范围,增加传感器噪声

还有一个常见问题是机器人学会“作弊”。比如你给“脚掌接触地面”奖励,机器人可能学会一直拖着脚走,因为这样接触时间最长。你给“躯干高度”奖励,机器人可能学会踮着脚尖走。解决方法是定期检查机器人的行为视频,发现异常行为就调整奖励函数。

5.3 真机部署的实操步骤与注意事项

真机部署的流程一般是:仿真训练 -> 策略导出 -> 真机推理 -> 安全保护。

策略导出就是把训练好的网络权重保存成ONNX或者TorchScript格式,方便在真机上加载。真机推理的频率一般是100Hz到1000Hz,取决于控制器的性能。安全保护包括:关节限位保护、力矩限幅、跌倒检测、紧急停止。

# 策略导出示例 policy.eval() dummy_input = torch.randn(1, obs_dim) torch.onnx.export(policy, dummy_input, "policy.onnx", input_names=["obs"], output_names=["action"])

真机部署时,先让机器人吊在架子上试,确认策略输出的动作范围合理,不会突然打满关节。然后再放到地面上,从慢速开始,逐步加速。第一次上真机,一定要有人扶着,随时准备急停。

注意:真机上的观测信息一定要做和仿真里一样的归一化。如果仿真里用了运行均值方差,真机上也要用同样的均值和方差,不能重新计算。否则观测分布不一致,策略输出会完全错误。

6. 进阶方向与个人经验总结

6.1 从行走扩展到跑跳与复杂地形

学会走路只是第一步。同样的框架可以扩展到跑步、跳跃、上下楼梯、跨越障碍。扩展的关键在于课程学习:先学走路,再学跑步,再学跳跃,逐步增加难度。

跑步和走路的区别在于腾空相。走路时至少有一只脚着地,跑步时有双脚同时离地的阶段。奖励函数需要增加“腾空时间”奖励,鼓励机器人跳起来。跳跃则需要增加“跳跃高度”奖励和“落地稳定性”奖励。

复杂地形需要地形随机化。在仿真里生成随机的高度场、斜坡、台阶、碎石地面,让机器人在这些地形上训练。地形随机化的难度也要逐步增加,从平地到小斜坡,再到大台阶。

6.2 多智能体与迁移学习的可能性

多智能体强化学习可以用于多机器人协作场景,比如两个机器人一起抬东西。每个机器人是一个智能体,共享一个全局奖励函数。训练时需要注意信用分配问题:哪个机器人的动作对全局奖励贡献大。

迁移学习可以用于跨机器人形态的迁移。比如在小型机器人上训练的策略,迁移到大型机器人上。由于动力学参数不同,直接迁移效果不好,但可以用小型机器人的策略作为初始化,在大型机器人上微调,能显著加快收敛速度。

6.3 我在实际项目中的几点体会

做了这么多机器人强化学习的项目,我最大的体会是:奖励函数设计比算法选择重要十倍。很多人花大量时间对比PPO和SAC哪个好,却不愿意花时间仔细设计奖励函数。实际上,一个好的奖励函数,用最简单的算法也能训出不错的效果;一个糟糕的奖励函数,用最先进的算法也训不出来。

第二个体会是:仿真环境的质量决定上限。仿真里的物理参数不准,训练出来的策略再好,真机上也白搭。我现在的习惯是,每训练一个新任务,先花两天时间校准仿真环境,确保仿真里的行为和真机尽可能一致。

第三个体会是:训练日志比训练结果更重要。不要只看最终的平均回报,要仔细分析训练过程中的每一个指标。策略熵的变化、价值损失的变化、KL散度的变化,这些曲线里藏着大量信息。我经常通过分析日志发现奖励函数的缺陷,然后调整后重新训练。

最后分享一个小技巧:定期保存策略快照,并录制视频。训练过程中,每隔一段时间保存一次模型权重,然后加载不同的快照录制视频。对比不同阶段的视频,能直观地看到机器人的行为变化,比看曲线图有用得多。我就是在对比视频时发现机器人学会了“拖着脚走”的作弊行为,然后调整奖励函数才纠正过来的。

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

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

立即咨询