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.2 | PPO的裁剪范围 |
| 熵系数 | 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散度的变化,这些曲线里藏着大量信息。我经常通过分析日志发现奖励函数的缺陷,然后调整后重新训练。
最后分享一个小技巧:定期保存策略快照,并录制视频。训练过程中,每隔一段时间保存一次模型权重,然后加载不同的快照录制视频。对比不同阶段的视频,能直观地看到机器人的行为变化,比看曲线图有用得多。我就是在对比视频时发现机器人学会了“拖着脚走”的作弊行为,然后调整奖励函数才纠正过来的。