Isaac Lab 轮足机器人强化学习(二):从站立到稳定行走
上一篇先把工程、机器人模型和 Manager-based 环境搭了起来,并通过站立任务确认整条训练链路能够工作。接下来真正让机器人走起来时,奖励函数、PD 参数、网络动作缩放、执行器力矩限制和速度限制都会共同影响训练结果。
加入通用奖励后训练变得不稳定
站立训练完成后,把之前训练机器狗时常用的一批奖励项加了进来,结果机器人并没有自然地从站立过渡到行走,反而开始剧烈晃动,甚至无法站稳。

这个时候的可能奖励和执行器部分都存在问题,首先是奖励部分绝对有问题。
所以改变思路,一点一点加奖励,机器人出现一种明确行为后,再针对这种行为增加约束。
机器人不动(增加速度方向奖励)
首先是只打开速度跟踪奖励这个时候发现机器人不走动,只会站立
查看各奖励项的变化后发现,在整体奖励下降时,部分惩罚项比如joint_acc反而上升且数值超过了速度奖励。
最开始的想法是继续提高速度跟踪奖励,也就是“重金之下有勇夫”,但实际没有解决问题,大奖励反而会让训练变得不稳定,机器人会盲目向前,且摔倒。

最后经过一系列调参和调研后,添加了速度方向的奖励:
只看指数形式的速度跟踪奖励时,正向误差和反向误差都表现为“离目标很远”。策略在早期还不知道轮子应该朝哪个方向转,因此增加了一个离散的方向奖励:实际纵向速度与指令同号时返回 1,反号时返回 -1。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| def lin_vel_x_sign( env: ManagerBasedRLEnv, command_name: str, command_threshold: float = 0.02, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ) -> torch.Tensor: asset: Articulation = env.scene[asset_cfg.name] command_x = env.command_manager.get_command(command_name)[:, 0]
sign_match = ( torch.sign(command_x) == torch.sign(asset.data.root_lin_vel_b[:, 0]) ).float() reward = 2.0 * sign_match - 1.0
return torch.where( torch.abs(command_x) < command_threshold, torch.zeros_like(reward), reward, )
|
这个函数的输入和输出关系比较直接:
command_x 来自 CommandManager 中的 base_velocity;
root_lin_vel_b[:, 0] 是机身坐标系下实际的纵向速度;
- 返回值也是
[num_envs],由 RewardManager 乘权重和时间步后累计。
command_threshold=0.02 用于排除接近零的指令。否则在站立环境中,微小数值抖动也可能被当成需要判断正反方向的行走指令。
它不能代替连续速度跟踪奖励,因为同号并不代表速度大小正确,但是能让机器人动起来:
最后的走动效果如下:

修正左右不对称
可以看到上面的走动效果非常丑陋,机器人开始移动后,两侧腿部动作不对称,轮子相对机身的位置也不稳定。这个阶段增加了左右关节对称奖励,以及让轮子保持在机身下方的奖励。
左右对称项直接比较成对关节的位置:
1 2 3 4 5 6 7 8 9 10 11 12 13
| def leg_symmetry_exp( env: ManagerBasedRLEnv, kernel: float = 0.01, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ) -> torch.Tensor: asset: Articulation = env.scene[asset_cfg.name] joint_pos = asset.data.joint_pos[:, asset_cfg.joint_ids] num_pairs = joint_pos.shape[1] // 2
left = joint_pos[:, :num_pairs] right = joint_pos[:, num_pairs:] error = torch.sum(torch.square(left - right), dim=1) return torch.exp(-error / kernel)
|
配置中使用 preserve_order=True 固定关节顺序:
1 2 3 4 5 6 7 8 9 10 11 12
| leg_joint_symmetry_exp = RewTerm( func=mdp.leg_symmetry_exp, weight=0.7, params={ "kernel": 0.01, "asset_cfg": SceneEntityCfg( "robot", joint_names=["l01", "l02", "r01", "r02"], preserve_order=True, ), }, )
|
如果不固定顺序,只看到 joint_names 中包含左右关节,并不能保证切片后的前半段和后半段仍然是预期的左右对应关系。
轮子位于机身下方的奖励先把左右轮的世界坐标转换到根节点坐标系,再惩罚前后方向的偏移:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| def wheel_under_body_l2( env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg, ) -> torch.Tensor: asset: Articulation = env.scene[asset_cfg.name] wheel_pos_w = asset.data.body_pos_w[:, asset_cfg.body_ids, :] wheel_pos_b = _body_points_to_root_frame(asset, wheel_pos_w) return torch.sum(torch.square(wheel_pos_b[..., 0]), dim=1)
def wheel_under_body_exp( env: ManagerBasedRLEnv, kernel: float = 0.1, asset_cfg: SceneEntityCfg = SceneEntityCfg( "robot", body_names=["left_wheel", "right_wheel"], preserve_order=True, ), ) -> torch.Tensor: return torch.exp(-wheel_under_body_l2(env, asset_cfg) / kernel)
|
这里必须先转换到机身坐标系。如果直接使用世界坐标的 $x$,机器人在场景中正常向前移动也会改变这个值,奖励衡量的就不再是“轮子相对机身的位置”。
加入这些约束以后,两侧动作开始接近,但走路姿态仍然比较难看:

调整 PD、Action Scale 和执行器限制
可以看到这个走路姿势是比较抖动的,在play.py中添加debug界面讲机器人运动时候的action和force打印,发现其变化急剧:

偏大的 PD 增益,表现出来就是动作冲得很快、机身晃动明显,这里的话对PD参数和action scale进行调整,最后收敛到一版效果较好的状态,
最终采用的速度控制版本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @configclass class ActionsCfg: leg_pos = mdp.JointPositionActionCfg( asset_name="robot", joint_names=["l01", "l02", "r01", "r02"], scale=0.15, use_default_offset=True, preserve_order=True, clip={".*": (-100.0, 100.0)}, )
wheel_vel = mdp.JointVelocityActionCfg( asset_name="robot", joint_names=["l03", "r03"], scale=5.0, use_default_offset=False, preserve_order=True, clip={".*": (-100.0, 100.0)}, )
|
执行器参数为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| actuators={ "legs": ImplicitActuatorCfg( joint_names_expr=[".*0[12]$"], effort_limit_sim=2.0, velocity_limit_sim=30.0, stiffness=3.0, damping=0.3, armature=0.05, ), "wheels": ImplicitActuatorCfg( joint_names_expr=[".*03$"], effort_limit_sim=9.0, velocity_limit_sim=45.0, stiffness=0.0, damping=1.0, armature=0.05, ), }
|
最终比例可以概括为:腿部 scale=0.15,轮子速度 scale=5.0。腿部 stiffness/damping 为 3.0/0.3,轮子为 0.0/1.0。
因为本项目中机器人基本为桌面级机器人,腿部的PD设置成3和0.3足以让关节有足够的位置跟踪能力。腿部设置scale0.15基本限制了action网络单步能够产生的关节位置变化幅度,避免了action轻微变化就对姿态产生很大的冲击。
然后轮子采用速度控制,不需要通过刚度项追踪固定的位置目标,因此设置 stiffness=0.0。damping=1.0 决定轮子对目标速度的跟踪强度。
加入机器人产生1.5m/s的运动,由于轮子半径较小,轮速需要达到47rpm,这个时候的话希望网络输出的action在-10到10,所以给出了scale=5.0。
能正常走以后开始原地转圈(加全局yaw约束)
调整奖励与控制参数后,机器人终于能够向前移动,抖动减小,速度收益也开始提高,但随后出现了一个很明显的问题:它会不断转圈。
只奖励纵向速度时,只要机身坐标系下的前向速度满足要求,策略并不会自动关心世界坐标系中的航向是否保持不变。初始时两侧动作只要产生一点偏差,偏航就可能逐渐积累。对奖励函数来说,“沿圆弧前进”仍然可以获得速度收益。
这个阶段一度继续追求速度奖励、削弱其他约束,但结果说明只管速度是不够的。要让机器人先学会直走,需要明确告诉它世界航向不能持续漂移。

这里加入全局航向约束
当时增加的 yaw_error_l1 为:
1 2 3 4 5 6 7 8 9
| from isaaclab.utils.math import wrap_to_pi
def yaw_error_l1( env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ) -> torch.Tensor: asset: Articulation = env.scene[asset_cfg.name] return torch.abs(wrap_to_pi(asset.data.heading_w))
|
惩罚的是机器人相对世界坐标系零航向的 yaw 偏差。heading_w 的 shape 是 [num_envs],wrap_to_pi 把角度限制到 $[-\pi,\pi]$,最后对每个环境返回一个非负误差。
这个阶段偏航速度指令为零,因此可以直接把初始航向作为目标。
加入 yaw_error_l1 后,机器人不再持续转圈,开始形成比较稳定的直线行走。
但是机器人始终是要学会转向的,一种方案是先让他学会直走然后再学习转弯的时候再解封,除此之
走着走着停下来
解决转圈后,又出现了走着走着不走,或者向前走一段再后退的现象:

结合当时的行为和奖励构成,问题在于初始动作造成的偏航误差会在运动过程中积累。当航向惩罚、姿态收益和速度收益发生冲突时,继续前进不一定还能提高总回报。停住反而可能成为一个局部折中。于是后续没有继续无限加强航向惩罚,而是重新平衡这部分约束,让它负责抑制无指令转动,而不是压过整个速度任务。
所以削减了这一项,进而改成了
1 2 3 4 5 6 7 8 9 10 11 12
| def yaw_error_l1( env: ManagerBasedRLEnv, command_name: str, command_threshold: float = 0.02, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ) -> torch.Tensor: """Penalize yaw rate only when the commanded base velocity is near zero.""" asset: Articulation = env.scene[asset_cfg.name] command = env.command_manager.get_command(command_name) is_standing = torch.all(torch.abs(command) < command_threshold, dim=1) yaw_rate_error = torch.square(asset.data.root_ang_vel_b[:, 2]) return torch.where(is_standing, yaw_rate_error, torch.zeros_like(yaw_rate_error))
|
这里不在惩罚全局坐标系下的yaw,而是惩罚在目标yaw为0的时候,集体自身的yaw速度。这样一来不会出现前期偏离后机器人就不走了,同时于跟踪yaw速度指令也不冲突。
处理上下晃动
直线运动基本形成后,机身还会明显上下晃动。对应的数据不是腿部关节角本身,而是机身竖直速度和机身高度:

最终配置中,竖直速度惩罚为:
1 2 3 4
| lin_vel_z = RewTerm( func=mdp.lin_vel_z_l2, weight=-10.0, )
|
同时使用指数形式的机身高度奖励:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| def base_height_exp( env: ManagerBasedRLEnv, target_height: float, kernel: float = 0.003, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ) -> torch.Tensor: asset: Articulation = env.scene[asset_cfg.name] height_error = torch.square( target_height - asset.data.root_pos_w[:, 2] ) return torch.exp(-height_error / kernel)
base_height_exp = RewTerm( func=mdp.base_height_exp, weight=0.5, params={"target_height": 0.07, "kernel": 0.003}, )
|
lin_vel_z_l2 负责压制持续上下运动,base_height_exp 负责把机身拉回目标高度附近。两者看起来都和 Z 方向有关,但区别在于一个约束速度,一个约束位置。只约束高度时,机器人仍可能围绕目标高度来回振荡;只约束竖直速度时,又不保证最后停在哪个高度。
调整速度跟踪误差范围
最后一个明显问题是速度跟踪能力差。这个机器人体量较小,速度误差的数值范围也比较小。如果指数核过窄,轻微误差就会让奖励快速衰减,策略很难从相邻速度之间获得平滑反馈。
线速度跟踪使用 Isaac Lab 的 track_lin_vel_xy_exp:
1 2 3 4 5 6 7 8
| track_lin_vel_xy = RewTerm( func=mdp.track_lin_vel_xy_exp, weight=3.0, params={ "command_name": "base_velocity", "std": math.sqrt(0.01), }, )
|
这里将 std 从 sqrt(0.005) 调整为 sqrt(0.01)。std 变大后,同样大小的速度误差受到的指数衰减更弱,也就是奖励曲线更宽。这个修改不是把目标速度变小,而是改变“多大的跟踪误差仍然能获得有区分度的奖励”。
最终行走阶段的指令先保持在较小范围:
1 2 3 4 5 6 7
| PHASE2_COMMAND_CFG = { "resampling_time_range": (2.5, 2.5), "rel_standing_envs": 0.4, "lin_vel_x": (-0.3, 0.3), "lin_vel_y": (0.0, 0.0), "ang_vel_z": (-0.5, 0.5), }
|
也就是说,平地行走先在 $[-0.3,0.3]\ \mathrm{m/s}$ 的纵向速度范围内学习。直接把最大速度改到后续目标值,会让尚未稳定的策略同时面对更大的动作需求和更宽的状态分布,所以速度范围的扩大放到下一篇的课程学习中完成。
最终稳定行走版本
最后工程收敛到了一个较为稳定的版本:

相比于上一节主要修改了:
- 腿部位置动作
scale=0.15,隐式 PD 为 3.0/0.3
- 轮子速度动作
scale=5.0,轮子执行器为 0.0/1.0
- 用连续速度跟踪奖励负责速度大小,用
lin_vel_x_sign 提供早期方向信号
- 用左右腿对称和轮子位于机身下方的奖励限制明显的不对称姿态
- 用yaw速度误差抑制持续转圈
- 用竖直速度和机身高度共同压制上下晃动
- 根据机器人实际速度尺度调整跟踪奖励的
std
但是其实还是存在一些上下晃动的,后续会查论文再优化一下。
下一篇在这个平地速度控制版本上继续增加线速度课程和地形课程。