Isaac Lab 轮足机器人强化学习(三):速度课程与地形课程

前两篇已经完成工程搭建和平地行走训练。平地版本能够在 $[-0.3,0.3]\ \mathrm{m/s}$ 的纵向速度指令内稳定行走后,接下来要做的是逐渐扩大速度范围,并把训练场景从平面换成包含粗糙地面、斜坡、方块和台阶的混合地形。

速度课程

从较小速度范围开始

直接把指令范围设成最终目标,会让训练初期同时面对更快的轮速、更大的姿态变化和更宽的状态分布。因此平地行走先从下面的纵向速度范围开始:

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),
}

速度课程只修改 base_velocity 命令项中的 ranges.lin_vel_x。初始范围是 (-0.3, 0.3),每次升级后正负方向各扩大 0.1 m/s,最终限制在 (-1.0, 1.0) m/s

用速度跟踪奖励判断升级

课程函数读取 track_lin_vel_xy 在本回合累计的加权奖励:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def lin_vel_cmd_levels(
env: ManagerBasedRLEnv,
env_ids: Sequence[int],
command_name: str = "base_velocity",
reward_term_name: str = "track_lin_vel_xy",
limit_range: tuple[float, float] = (-1.0, 1.0),
step_size: float = 0.1,
reward_threshold: float = 0.8,
) -> torch.Tensor:
command_term = env.command_manager.get_term(command_name)
ranges = command_term.cfg.ranges
reward_term = env.reward_manager.get_term_cfg(reward_term_name)

reward = (
torch.mean(
env.reward_manager._episode_sums[reward_term_name][env_ids]
)
/ env.max_episode_length_s
)

if (
env.common_step_counter % env.max_episode_length == 0
and reward > reward_term.weight * reward_threshold
):
delta = torch.tensor(
[-step_size, step_size],
device=env.device,
)
ranges.lin_vel_x = torch.clamp(
torch.tensor(ranges.lin_vel_x, device=env.device) + delta,
limit_range[0],
limit_range[1],
).tolist()

return torch.tensor(ranges.lin_vel_x[1], device=env.device)

这里的 env_ids 是当前参与课程计算的环境编号。_episode_sums[reward_term_name][env_ids] 先取这些环境中线速度跟踪项的回合累计值,再对环境求平均,最后除以最大回合时长 env.max_episode_length_s

如果把归一化后的平均线速度跟踪奖励记为 $\bar R_v$,奖励项权重记为 $w_v$,升级条件就是:

当前线速度跟踪权重为 3.0,所以代码中的比较阈值为 2.4。这里比较的是已经乘过奖励权重、时间步并累计的回合奖励,不是某一个仿真步上的瞬时速度误差。

另一个条件:

1
env.common_step_counter % env.max_episode_length == 0

用于限制修改指令范围的时机。课程不是每次调用都扩大速度,只有到达这个边界并且跟踪奖励超过阈值时才更新。

范围的变化过程为:

1
2
3
4
5
(-0.3, 0.3)
(-0.4, 0.4)
(-0.5, 0.5)
...
(-1.0, 1.0)

torch.clamp 保证上下界不会继续越过 limit_range

注册线速度课程

课程函数通过 CurriculumTermCfg 注册到环境:

1
2
3
4
5
6
7
8
9
10
@configclass
class CurriculumCfg:
lin_vel_cmd_levels = CurrTerm(
func=mdp.lin_vel_cmd_levels,
params={
"limit_range": (-1.0, 1.0),
"step_size": 0.1,
"reward_threshold": 0.8,
},
)

环境配置还需要真正持有这个课程组:

1
2
3
4
5
6
7
8
9
10
11
12
13
@configclass
class IsaaclabMbWheellegEnvCfg(ManagerBasedRLEnvCfg):
scene = IsaaclabMbWheellegSceneCfg(
num_envs=512,
env_spacing=1.0,
)
observations = ObservationsCfg()
actions = ActionsCfg()
commands = CommandsCfg()
events = EventCfg()
rewards = RewardsCfg()
terminations = TerminationsCfg()
curriculum = CurriculumCfg()

地形课程

混合地形配置

地形部分参考 Isaac Lab 的 TerrainGeneratorCfgTerrainImporterCfg。当前工程生成一块由 6 行、12 列子地形组成的混合地形,每块子地形尺寸为 2.0 m × 2.0 m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
WHEELLEG_MIXED_TERRAINS_CFG = terrain_gen.TerrainGeneratorCfg(
size=(2.0, 2.0),
border_width=1.0,
num_rows=6,
num_cols=12,
horizontal_scale=0.05,
vertical_scale=0.0025,
slope_threshold=0.75,
difficulty_range=(0.0, 1.0),
use_cache=False,
sub_terrains={
"flat": terrain_gen.MeshPlaneTerrainCfg(
proportion=0.25,
),
"random_rough": terrain_gen.HfRandomUniformTerrainCfg(
proportion=0.25,
noise_range=(0.0, 0.025),
noise_step=0.005,
border_width=0.25,
),
"gentle_slope": terrain_gen.HfPyramidSlopedTerrainCfg(
proportion=0.20,
slope_range=(0.0, 0.12),
platform_width=1.0,
border_width=0.25,
),
"low_boxes": terrain_gen.MeshRandomGridTerrainCfg(
proportion=0.15,
grid_width=0.35,
grid_height_range=(0.0, 0.025),
platform_width=1.0,
),
"low_stairs": terrain_gen.MeshPyramidStairsTerrainCfg(
proportion=0.15,
step_height_range=(0.0, 0.035),
step_width=0.45,
platform_width=1.0,
border_width=0.25,
holes=False,
),
},
)

这里一共包含五类地形。

平地占 25%,用于保留已经学会的基础行走能力。随机粗糙地形占 25%,高度噪声范围为 0~0.025 m,按 0.005 m 的步长离散。缓坡占 20%,坡度范围为 0~0.12。低矮方块和低台阶各占 15%,最大高度分别为 0.025 m0.035 m

这些尺寸是按当前小型轮足机器人的尺度设置的,不能直接复制到更大的机器人上。比如 0.035 m 对当前机器人已经是需要明显调整腿部姿态的台阶,但对大型平台可能只是很小的地面起伏。

场景通过 TerrainImporterCfg 使用生成器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
terrain = TerrainImporterCfg(
prim_path="/World/ground",
terrain_type="generator",
terrain_generator=WHEELLEG_MIXED_TERRAINS_CFG,
max_init_terrain_level=0,
collision_group=-1,
physics_material=sim_utils.RigidBodyMaterialCfg(
friction_combine_mode="multiply",
restitution_combine_mode="multiply",
static_friction=1.0,
dynamic_friction=1.0,
),
visual_material=sim_utils.PreviewSurfaceCfg(
diffuse_color=(0.35, 0.35, 0.35),
),
debug_vis=False,
)

max_init_terrain_level=0 表示训练开始时只从最低难度等级初始化。启用课程后,同一类型的地形会按难度从低到高排列,机器人不会在训练刚开始就直接落到最高难度位置。

地形升级与降级策略

地形课程根据一个回合内机器人相对环境原点的平面位移决定是否升级:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def terrain_levels_vel(
env: ManagerBasedRLEnv,
env_ids: Sequence[int],
asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"),
move_up_distance: float | None = None,
) -> torch.Tensor:
asset: Articulation = env.scene[asset_cfg.name]
terrain: TerrainImporter = env.scene.terrain
command = env.command_manager.get_command("base_velocity")

distance = torch.norm(
asset.data.root_pos_w[env_ids, :2]
- env.scene.env_origins[env_ids, :2],
dim=1,
)

move_up_threshold = (
terrain.cfg.terrain_generator.size[0] / 2
if move_up_distance is None
else move_up_distance
)
move_up = distance > move_up_threshold

expected_distance = (
torch.norm(command[env_ids, :2], dim=1)
* env.max_episode_length_s
)
move_down = distance < expected_distance * 0.5
move_down *= ~move_up

terrain.update_env_origins(env_ids, move_up, move_down)
return torch.mean(terrain.terrain_levels.float())

当前注册时显式设置 move_up_distance=0.5

1
2
3
4
5
6
@configclass
class CurriculumCfg:
terrain_levels = CurrTerm(
func=mdp.terrain_levels_vel,
params={"move_up_distance": 0.5},
)

升级条件是实际平面位移大于 0.5 m

降级条件则与当前速度指令有关。假设平面指令速度大小为 $\lVert v_{cmd}\rVert$,最大回合时长为 $T$,那么指令对应的理论位移为:

如果实际位移还不到理论位移的一半,就标记为降级:

代码中的 move_down *= ~move_up 规定升级优先:已经满足 0.5 m 升级阈值的环境不会在同一次判断中又被降级。最后调用 terrain.update_env_origins,让对应环境下一次重置到更高或更低的地形原点。[tag](其实感觉这里也有点不太严谨,毕竟每次速度是随机采样的,所以这里设置的平面位移比较小,一定是有更好的方式的)

课程函数返回所有环境当前地形等级的平均值,因此 TensorBoard 中可以观察整体训练难度是否在上升。

启用地形课程并开始训练

除了注册 terrain_levels,还需要把生成器的 curriculum 标志打开。当前环境在 __post_init__ 中根据课程项是否存在完成这件事:

1
2
3
4
5
6
7
8
9
10
11
12
def __post_init__(self) -> None:
self.decimation = 2
self.episode_length_s = 5
self.sim.dt = 1 / 120
self.sim.render_interval = self.decimation
self.sim.physics_material = self.scene.terrain.physics_material
self.sim.physx.gpu_max_rigid_patch_count = 10 * 2**15
self.scene.contact_forces.update_period = self.sim.dt

if self.curriculum.terrain_levels is not None:
if self.scene.terrain.terrain_generator is not None:
self.scene.terrain.terrain_generator.curriculum = True

训练命令与平地阶段相同:

1
python scripts/rsl_rl/train.py --task Wheelleg-Velocity-v0 --headless

启动后需要同时关注奖励曲线和两个课程标量:

1
2
Curriculum/lin_vel_cmd_levels
Curriculum/terrain_levels

只看总奖励会把“速度范围扩大导致任务变难”和“策略本身退化”混在一起。课程值上升时,短时间内奖励下降并不一定代表训练失败,需要结合当前指令上限和地形等级一起判断。

地形训练结果

训练完成后,策略能够在混合地形上保持行走,并具备一定的越障能力,课程等级也在不断上升,地形课程中由于难度升级后表现不佳,有部分回退现象。

image-20260719173140757

地形课程2 00_00_00-00_00_30

地形课程 00_00_00-00_00_30

目前效果

目前通过手柄控制能够实现部分地形的穿越

崎岖地形 00_00_00-00_00_30

至此,这个轮足工程已经完成了从平地站立、速度跟随,到线速度课程、地形课程和基本链路,但是仍然有较多地方需要完善,后续会继续进行修改,用DreamWAQ实现多种地形的跨越,以及抬腿上台阶。