Isaac Lab 轮足机器人强化学习(一):项目创建与环境搭建

最近在看 Isaac Lab 的官方文档,借这个机会整理几篇文章,记录一个轮足机器人强化学习工程从创建、行走训练到课程学习的完整过程。这里没有直接使用开源的人形或四足模型,而是使用自己建模的双轮足机器人,目的就是把模型导入、环境搭建、训练和验证这条链路完整跑通。

整个工程参考了 Isaac Lab 的项目模板和教程,采用外部工程、Manager-based 环境与 RSL-RL。本文先完成项目创建、机器人导入和站立训练;下一篇再记录如何把机器人从“站住”逐步训练到能够稳定行走。速度课程和地形课程放到第三篇。

本工程对应的本地环境为 Isaac Lab 2.3.2、Isaac Sim 5.1.0、rsl_rl 5.4.1、PyTorch 2.7.0+cu128 和 Python 3.11.15。下面的接口和配置都以这个工程的实际代码为准。

Isaac Lab 与参考资料

Isaac Lab 中常见的强化学习任务主要有 Direct 和 Manager-based 两种组织方式。Direct 环境把观测、奖励、终止条件和动作处理集中写在环境类中,数据流比较直观,和以前阅读 Isaac Gym 环境时的感觉接近。Manager-based 环境则把这些逻辑拆给不同的 Manager,配置更模块化,也更方便后续增加奖励、随机化和课程项。

这个工程最终选择 Manager-based。后面的主要调用关系可以概括为:

1
2
3
4
5
6
7
Gym 任务注册
- ManagerBasedRLEnv
- Scene / Command / Action / Observation
- Event / Reward / Termination / Curriculum Manager
- RSL-RL VecEnv Wrapper
- OnPolicyRunner
- PPO

安装环境可以参考:

第一次启动 Isaac Sim 时,着色器缓存和扩展加载会占用较长时间,界面也可能暂时比较卡。这和后面已经完成缓存后的启动速度不是一回事。

生成外部工程

在 Isaac Lab 根目录下运行模板生成器:

1
isaaclab.bat --new

模板生成过程中选择 External Project、Manager-based 和 RSL-RL。External Project 的好处是工程放在 Isaac Lab 仓库之外,自己的任务代码和官方源码不会混在一起。

当前工程中,与任务实现直接相关的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
isaaclab_mb_wheelleg/
├─ scripts/
│ ├─ list_envs.py
│ ├─ zero_agent.py
│ ├─ random_agent.py
│ └─ rsl_rl/
│ ├─ train.py
│ ├─ play.py
│ └─ play_debug.py
├─ source/isaaclab_mb_wheelleg/
│ ├─ config/extension.toml
│ ├─ setup.py
│ └─ isaaclab_mb_wheelleg/
│ ├─ robots/wheelleg.py
│ └─ tasks/manager_based/isaaclab_mb_wheelleg/
│ ├─ __init__.py
│ ├─ isaaclab_mb_wheelleg_env_cfg.py
│ ├─ mdp/
│ │ ├─ rewards.py
│ │ └─ curriculums.py
│ └─ agents/rsl_rl_ppo_cfg.py
├─ logs/
└─ outputs/

source/isaaclab_mb_wheelleg 是可以通过 pip install -e 安装的 Python 包,同时也是 Isaac Sim 扩展。robots/wheelleg.py 负责定义机器人资产,isaaclab_mb_wheelleg_env_cfg.py 负责组织环境,mdp 目录存放自定义奖励和课程函数,agents 目录存放 RSL-RL 的 PPO 配置。

训练之后,工程中会出现两类输出:

logs/rsl_rl/wheelleg_velocity/<时间戳>/ 是 RSL-RL 的核心训练目录,实际保存了:

  • model_*.pt checkpoint;
  • TensorBoard 的 events.out.tfevents.*
  • params/env.yamlparams/agent.yaml
  • 导出的策略以及当前 Git 状态记录。

outputs/<日期>/<时间>/ 则由 Hydra 生成,其中主要是 .hydra/config.yamlhydra.yamloverrides.yaml。前者用于恢复训练和查看曲线,后者更适合检查一次启动时 Hydra 最终合成了什么配置。

导入自己的轮足机器人

机器人的建模流程是:先在 SolidWorks 中完成结构和关节定义,再通过 sw2urdf 导出 URDF

image-20260719122250971

随后在 Isaac Sim 中导入 URDF 并保存为 USD,这里在保存为USD之前是可以在右侧属性区设置关节参数的,比如默认姿态和刚度这些。

image-20260719120704623

这里有两点注意:

首先,轮足机器人需要在场景中自由运动,导入时不能把根节点做成固定基座。固定基座更适合机械臂,这里需要的是 movable base。

然后,Isaac Sim 界面里查看和编辑关节角时常用的是度,但 ArticulationCfg.InitialStateCfg.joint_pos 中填写的是弧度。默认姿态会直接参与位置动作的 offset 计算,所以最好在导出 USD 前就把站立姿态调整清楚。

工程中的 USD 路径不是写死的绝对路径,而是从 wheelleg.py 的位置推导:

1
2
3
4
from pathlib import Path

USD_DIR = Path(__file__).resolve().parent.parent / "USD"
WHEELLEG_USD_PATH = USD_DIR / "wheelleg.usd"

机器人资产由 ArticulationCfg 描述,主要包含 USD、初始状态和执行器三部分:

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
43
44
45
46
47
48
import isaaclab.sim as sim_utils
from isaaclab.assets import ArticulationCfg
from isaaclab.actuators import ImplicitActuatorCfg

WHEELLEG_CONFIG = ArticulationCfg(
spawn=sim_utils.UsdFileCfg(
usd_path=str(WHEELLEG_USD_PATH),
activate_contact_sensors=True,
rigid_props=sim_utils.RigidBodyPropertiesCfg(
disable_gravity=False,
max_depenetration_velocity=5.0,
),
articulation_props=sim_utils.ArticulationRootPropertiesCfg(
enabled_self_collisions=True,
solver_position_iteration_count=8,
solver_velocity_iteration_count=0,
),
),
init_state=ArticulationCfg.InitialStateCfg(
pos=(0.0, 0.0, 0.085),
joint_pos={
"l01": 0.9599,
"l02": -1.8850,
"l03": 0.0,
"r01": 0.9599,
"r02": -1.8850,
"r03": 0.0,
},
),
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,
),
},
)

左右两条腿各有三个关节。l01l02r01r02 是腿部位置关节,l03r03 是轮子关节。正则表达式 .*0[12]$.*03$ 把两类关节分给不同的执行器参数。

环境配置的组成

这个 Manager-based 环境没有把所有逻辑塞进 step(),而是在配置文件中按职责分成以下部分:

  • IsaaclabMbWheellegSceneCfg:地面、机器人、传感器和灯光;
  • CommandsCfg:期望线速度和偏航角速度;
  • ActionsCfg:策略输出如何映射到关节目标;
  • ObservationsCfg:actor 和 critic 分别能看到什么;
  • EventCfg:启动、重置和间隔事件;
  • RewardsCfg:每个奖励项及权重;
  • TerminationsCfg:超时、机身触地、腿部触地等终止条件;
  • CurriculumCfg:后续使用的速度课程和地形课程。

Manager-Based

Manager-based 不是简单地把一个大配置文件拆成很多小类。环境初始化时,Isaac Lab 会解析这些配置并创建对应的运行时 Manager。配置类负责声明“有哪些项”,Manager 则持有 Tensor、按固定时机调用每一项,并把结果交给下一层。

在一轮“读取指令—计算观测—策略推理—执行动作”的数据流里,最直接的三个 Manager 是 CommandManagerObservationManagerActionManager

1
2
3
4
5
6
7
8
9
10
11
12
CommandManager
│ 生成任务目标 base_velocity: [num_envs, 3]

ObservationManager ──> policy observation ──> actor
▲ │
│ 读取机器人状态、历史观测和指令 │ action: [num_envs, 6]
│ ▼
仿真更新 <── Articulation <── ActionManager

├─> RewardManager 计算本步奖励
├─> TerminationManager 判断回合是否结束
└─> EventManager 在指定生命周期修改状态或物理参数

CommandManager:生成任务目标

CommandManager 管的不是机器人当前状态,而是当前希望机器人完成的目标。这个工程只有一个 command term,名字是 base_velocity,每个环境对应三维指令:

1
[lin_vel_x, lin_vel_y, ang_vel_z]

512 个并行环境运行时,CommandManager.get_command("base_velocity") 返回的 Tensor shape 是 [512, 3],Tensor 位于环境的 env.device 上。

UniformVelocityCommandCfg 负责在给定范围内采样指令。环境重置时,对应的 command term 会重置并重新采样;运行过程中,Manager 根据 resampling_time_range 维护剩余时间并更新指令。当前行走阶段每隔 2.5 秒重新采样一次,rel_standing_envs=0.4 表示其中一部分环境会收到零速度指令。

同一份 command Tensor 会流向两个地方:

  • ObservationManager 把它放进 velocity_commands,让 actor 知道目标是什么;
  • RewardManagertrack_lin_vel_xy 等项中读取它,与机器人实际速度比较。

这也解释了为什么“配置了 command”还不够:如果没有把 command 加入观测,策略看不到目标;如果奖励函数没有读取同一个 command,策略也不会因为跟踪目标而得到反馈。

ObservationManager:把环境状态整理成网络输入

ObservationManager 按 observation group 组织观测。这个工程定义了 policycritic 两组:policy 给 actor 使用,critic 只在训练期用于估值。

每一个 ObsTerm 都是一个独立的数据来源。例如:

  • base_ang_vel 从机器人资产读取机身角速度;
  • projected_gravity 表示重力在机身坐标系中的投影;
  • velocity_commandsCommandManager 读取目标速度;
  • joint_pos_reljoint_vel_rel 读取关节状态;
  • last_actionActionManager 读取上一个控制步的动作。

Manager 会依次计算这些 term,并按照配置应用 modifier、噪声、clip、scale 和历史缓存。当前 concatenate_terms=True,所以各项最终沿最后一维拼接成一个 Tensor,而不是以字典形式分别返回。

这里还有一个容易忽略的数据依赖:last_action 并不是从 PhysX 重新计算的关节状态,而是 ActionManager 保存的上一份策略动作。它用于告诉 actor 上一步自己发出了什么命令,与 joint_pos_reljoint_vel_rel 这种实际物理状态不是一类信息。

后文会具体计算观测维度。当前 5 帧历史展开后,policy 输入为 [512, 135],critic 输入为 [512, 180]

ActionManager:把网络输出变成关节目标

actor 每个控制步输出 [512, 6] 的动作。ActionManager 根据 ActionsCfg 中 term 的声明顺序和维度,把它拆成:

1
2
leg_pos  : [512, 4]
wheel_vel: [512, 2]

处理动作可以分成两步。

第一步是 process_action:保存 raw action 和 previous action,再按每个 ActionTerm 的规则执行 clip、scale 和 offset。腿部四维动作因此被转换为相对默认姿态的位置目标,轮子两维动作被转换为速度目标。

第二步是 apply_action:把处理后的目标写入机器人 articulation。当前 decimation=2,策略每 1/60 s 产生一次新动作,而 PhysX 以 1/120 s 运行,因此同一份处理后的动作会在接下来的两个物理步中持续应用。

更准确地说,ActionManager 负责的是“动作 Tensor 到资产控制目标”的映射,ImplicitActuatorCfg 和 PhysX 再负责根据目标、PD 参数和物理限制产生实际关节运动。网络输出不等于最终力矩,中间还隔着动作缩放、执行器模型与求解器。

RewardManagerTerminationManager 位于这条闭环之后:前者把各个 RewardTerm[num_envs] 输出乘权重和时间步后求和,后者把各个终止项组合为每个环境的 done 信号。EventManager 不在每步策略 Tensor 的直接传递链上,它按生命周期事件修改场景、资产状态或物理参数,下面单独展开。

在第一阶段先使用平地,不急着引入复杂地形:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@configclass
class IsaaclabMbWheellegSceneCfg(InteractiveSceneCfg):
ground = AssetBaseCfg(
prim_path="/World/ground",
spawn=sim_utils.GroundPlaneCfg(size=(100.0, 100.0)),
)

robot: ArticulationCfg = WHEELLEG_CONFIG.replace(
prim_path="{ENV_REGEX_NS}/Robot"
)

contact_forces = ContactSensorCfg(
prim_path="{ENV_REGEX_NS}/Robot/wl2/.*",
history_length=3,
)

dome_light = AssetBaseCfg(
prim_path="/World/DomeLight",
spawn=sim_utils.DomeLightCfg(
color=(0.9, 0.9, 0.9),
intensity=500.0,
),
)

{ENV_REGEX_NS} 会被每个并行环境自己的命名空间替换。机器人、接触传感器和环境原点因此可以在同一个 USD Stage 中复制很多份,而不会互相串数据。

域随机化和随机扰动

随机化和扰动统一通过 EventCfg 注册,由运行时的 EventManager 调度。Event 与 observation、reward 的区别在于:它通常不返回一项供网络直接使用的 Tensor,而是在指定时机修改场景属性、机器人状态或外部作用。

常用的四种 Event 模式

Isaac Lab 官方文档列出了四种常用模式:

  • prestartup:仿真开始之前执行一次,主要用于需要在 USD Stage 层修改的属性;
  • startup:仿真启动之后执行一次;
  • reset:对应环境每次重置时执行;
  • interval:训练运行过程中,按配置的时间间隔重复执行。

因此常规模式是四种,不是三种。当前轮足工程使用了 startupresetinterval,没有使用 prestartup

Event 的 mode 本质上是一个字符串,框架也允许定义自定义模式。不过自定义名字不会自动获得触发时机,必须在环境实现中显式调用。四种常用模式中,interval 的计时和触发由 EventManager 自己处理,其余模式由环境在对应生命周期调用。

对于 intervalinterval_range_s=(5.0, 10.0) 表示触发间隔从该范围均匀采样。is_global_time 默认是 False,所以各个并行环境分别维护自己的剩余时间,不要求 512 个环境在同一仿真步一起受到推动。

随机化、初始状态和外部扰动

从训练作用上看,Event 可以分成三类来理解,但这三类是用途分类,不是 EventManager 的 mode:

  • 物理参数随机化:改变摩擦、质量、质心、执行器增益或关节参数,使策略不要只适应一套精确模型;
  • 初始状态随机化:在 reset 时改变根节点位姿、速度和关节状态,让每个回合从略有差异的状态开始;
  • 外部扰动:通过外力、外矩或根节点速度变化打断当前运动,用来检验和训练恢复能力。

当前工程没有把官方提供的所有事件函数都打开。实际启用的是 6 个 EventTerm:两个 startup、三个 reset 和一个 interval

工程中的核心配置如下:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
@configclass
class EventCfg:
physics_material = EventTerm(
func=mdp.randomize_rigid_body_material,
mode="startup",
params={
"asset_cfg": SceneEntityCfg("robot", body_names=".*"),
"static_friction_range": (0.6, 1.2),
"dynamic_friction_range": (0.5, 1.0),
"restitution_range": (0.0, 0.05),
"num_buckets": 64,
},
)

add_base_mass = EventTerm(
func=mdp.randomize_rigid_body_mass,
mode="startup",
params={
"asset_cfg": SceneEntityCfg("robot", body_names="base_link"),
"mass_distribution_params": (-0.02, 0.03),
"operation": "add",
},
)

base_external_force_torque = EventTerm(
func=mdp.apply_external_force_torque,
mode="reset",
params={
"asset_cfg": SceneEntityCfg("robot", body_names="base_link"),
"force_range": (-0.2, 0.2),
"torque_range": (-0.005, 0.005),
},
)

reset_base = EventTerm(
func=mdp.reset_root_state_uniform,
mode="reset",
params={
"pose_range": {
"x": (-0.05, 0.05),
"y": (-0.05, 0.05),
"z": (0.0, 0.0),
"roll": (-0.03, 0.03),
"pitch": (-0.03, 0.03),
"yaw": (-0.1, 0.1),
},
"velocity_range": {
"x": (-0.03, 0.03),
"y": (-0.02, 0.02),
"z": (0.0, 0.0),
"roll": (-0.05, 0.05),
"pitch": (-0.05, 0.05),
"yaw": (-0.05, 0.05),
},
},
)

reset_robot_joints = EventTerm(
func=mdp.reset_joints_by_scale,
mode="reset",
params={
"position_range": (1.0, 1.0),
"velocity_range": (-0.2, 0.2),
},
)

push_robot = EventTerm(
func=mdp.push_by_setting_velocity,
mode="interval",
interval_range_s=(5.0, 10.0),
params={
"asset_cfg": SceneEntityCfg("robot"),
"velocity_range": {
"x": (-0.03, 0.03),
"y": (-0.02, 0.02),
},
},
)

physics_material 在 startup 阶段随机化机器人所有刚体几何的静摩擦、动摩擦和恢复系数。num_buckets=64 表示把连续随机范围离散成 64 组材料参数,减少大量并行环境各自创建独立物理材质带来的开销。因为它是 startup 事件,同一个环境不会在每次 reset 时重新采样材料。

add_base_mass 同样只在 startup 执行,对 base_link 质量做加法随机化,范围为 -0.02~0.03 kg。这里的 operation="add" 很重要:采样值是加到默认质量上,不是把机身总质量直接设置成这个范围。

base_external_force_torque 在 reset 时为 base_link 采样外力和外矩。它改变的是作用在刚体上的 wrench,不是策略动作,也不改变 ActionManager 的 6 维动作空间。

reset_base 在 reset 时随机化根节点状态。位置只在水平面 ±0.05 m 内变化,roll、pitch 和 yaw 也从较小范围采样;同时为根节点线速度和角速度加入小范围初值。换到 Tensor 上,它只修改本次重置的 env_ids,不会把其他仍在运行的环境一起重置。

reset_robot_joints 使用 reset_joints_by_scale。当前 position_range=(1.0, 1.0),所以关节位置仍然是默认关节位置乘 1,并没有额外位置随机化;实际保留的随机性来自 velocity_range=(-0.2, 0.2)。这一点只看函数名容易误判,参数才决定最终有没有随机化。

push_robot 是 interval 扰动,每个环境每隔 5~10 s 独立触发一次。push_by_setting_velocity 直接把根节点速度的 x、y 分量设置到给定随机范围,用效果来模拟一次推搡。它不是持续施加的力,也不是在 observation 中添加噪声。

整个流程为:

1
2
3
4
5
6
7
8
9
10
创建环境
prestartup:本工程暂时没使用
启动仿真
startup:随机摩擦材料、随机 base_link 质量

每个环境开始新回合
reset:采样外力/外矩、根节点状态、关节速度

回合运行中
interval:每 5~10 秒改变一次根节点平面速度

随机化范围不能脱离训练阶段单独讨论。站立和基础行走尚未形成时,过强的初始倾角、速度或外部扰动会让“动作映射错误”和“任务本身太难”混在一起。当前做法是先用较小范围确认策略能够恢复,再根据实际回放决定是否继续扩大,而不是一开始把官方示例中的随机化全部照搬过来。

关节动作与控制方式

腿和轮子的物理功能不同,动作项也分成两组。本文使用的位置是最终稳定的速度控制版本:

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

假设策略输出为 $a$,腿部目标位置可以直观理解为:

轮子目标速度则为:

这里的核心不是“腿和轮子必须用同一种控制”,而是动作语义要与关节功能对应。腿需要围绕默认姿态调节支撑结构,适合位置目标;轮子需要持续转动,速度目标更直接。

工程后期也试过 RelativeJointPositionActionCfg:每一步在当前轮子位置上叠加位置增量,位置环同样能够训练出稳定行走。不过它本质上是连续更新的增量位置伺服,而不是直接给轮速。按照多数论文中的配置,本工程后续统一采用上面的轮子速度控制版本。

显式 PD 与隐式 PD

当前机器人使用的是 ImplicitActuatorCfg。在位置控制下,可以用下面的形式理解关节驱动力:

隐式执行器把这部分 drive 计算交给 PhysX;显式执行器则由 Isaac Lab 的执行器模型先计算力矩,再把力矩写入仿真。两者看起来都有 stiffness 和 damping,但力矩在哪一层计算并不相同。

轮子使用速度控制时将 stiffness 设为 0.0,保留 damping=1.0。这样位置误差不参与轮子驱动,主要由速度误差形成控制作用。effort_limit_simvelocity_limit_sim 则分别限制物理求解器中的力矩与速度。

指令与观测

站立阶段把速度指令全部设为零。进入行走阶段后,再由 UniformVelocityCommandCfg 周期性采样期望速度:

1
2
3
4
5
6
7
8
9
10
11
base_velocity = mdp.UniformVelocityCommandCfg(
asset_name="robot",
resampling_time_range=(2.5, 2.5), #这里是多久变化一次速度
rel_standing_envs=0.4,
debug_vis=True,
ranges=mdp.UniformVelocityCommandCfg.Ranges(
lin_vel_x=(-0.3, 0.3),
lin_vel_y=(0.0, 0.0),
ang_vel_z=(-0.5, 0.5),
),
)

观测分为 policy 和 critic 两组。policy 是部署时 actor 真正依赖的输入,critic 只在训练期用于估值,因此可以增加特权信息。

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
@configclass
class PolicyCfg(ObsGroup):
base_ang_vel = ObsTerm(func=mdp.base_ang_vel, scale=0.25)
projected_gravity = ObsTerm(func=mdp.projected_gravity)
velocity_commands = ObsTerm(
func=mdp.generated_commands,
params={"command_name": "base_velocity"},
)
joint_pos_rel = ObsTerm(func=mdp.joint_pos_rel)
joint_vel_rel = ObsTerm(func=mdp.joint_vel_rel, scale=0.05)
last_action = ObsTerm(func=mdp.last_action)

def __post_init__(self):
self.history_length = 5
self.enable_corruption = False
self.concatenate_terms = True


@configclass
class CriticCfg(ObsGroup):
base_ang_vel = ObsTerm(func=mdp.base_ang_vel, scale=0.25)
projected_gravity = ObsTerm(func=mdp.projected_gravity)
velocity_commands = ObsTerm(
func=mdp.generated_commands,
params={"command_name": "base_velocity"},
)
joint_pos_rel = ObsTerm(func=mdp.joint_pos_rel)
joint_vel_rel = ObsTerm(func=mdp.joint_vel_rel, scale=0.05)
last_action = ObsTerm(func=mdp.last_action)

base_lin_vel = ObsTerm(func=mdp.base_lin_vel)
joint_effort = ObsTerm(func=mdp.joint_effort, scale=0.01)

def __post_init__(self):
self.history_length = 5
self.concatenate_terms = True

每个环境中,policy 单帧观测维度为:

1
2
3
4
5
6
7
8
base_ang_vel        3
projected_gravity 3
velocity_commands 3
joint_pos_rel 6
joint_vel_rel 6
last_action 6

单帧合计 27

历史长度为 5,并沿最后一维展开,因此 actor 输入是 27 × 5 = 135。critic 额外获得 3 维机身线速度和 6 维关节力矩,单帧为 36 维,最终输入为 36 × 5 = 180。实际 checkpoint 的首层权重分别是 (512, 135)(512, 180),策略输出层是 (6, 128),与 6 维动作完全对应。

在一次 512 环境、cuda:0 的训练中,数据流对应为:

1
2
3
policy observation : [512, 135]
critic observation : [512, 180]
policy action : [512, 6]

后续为了功能扩展,预留了 height scanner 和 depth camera,这里暂时没启用

站立训练

这里要求不高,先以站立为目标训练看一下效果如何。验证算法和环境是否跑通

环境刚搭好时不直接要求机器人行走,而是先确认模型、关节、接触和 PPO 训练链路能够正常工作。站立阶段将线速度和偏航速度跟踪权重设为零,以存活、姿态和机身高度为主要信号。

当时与站立直接相关的权重为:

1
2
3
4
5
6
7
8
9
10
11
STAND_REWARD_WEIGHTS = [
5.0, # alive
-200.0, # terminating
-0.05, # lin_vel_z
-0.02, # ang_vel_xy
-4.0, # pitch_orientation
-50.0, # base_height
-0.1, # leg_joint_deviation
-0.1, # leg_joint_symmetry
-0.5, # wheel_air
]

对应的奖励项仍然通过 RewardsCfg 注册,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
alive = RewTerm(func=mdp.is_alive, weight=PHASE1_REWARD_WEIGHTS[ALIVE])
terminating = RewTerm(
func=mdp.is_terminated,
weight=PHASE1_REWARD_WEIGHTS[TERMINATING],
)
lin_vel_z = RewTerm(
func=mdp.lin_vel_z_l2,
weight=PHASE1_REWARD_WEIGHTS[LIN_VEL_Z],
)
base_height = RewTerm(
func=mdp.base_height_l2,
weight=PHASE1_REWARD_WEIGHTS[BASE_HEIGHT],
params={"target_height": 0.07},
)

这里的目标是:不要摔倒,并把 base_link 保持在目标高度附近。

zero_Agent和random_Agent

工程搭建完之后,先拿这两个来看一下环境会不会有问题,会不会崩。

同时这里在zero_agent这里加了一个力的显示,大概看一下机器人要施加的力处在一个什么样的范围

image-20260614142845213

这里也把足端的力给出来了,所以能看到机器人的重量,左右份别2.5N,总共机器人应该是0.5KG,和Solidworks里设置一致
运行后看到random_agent里,机器人运行随机指令但是也不会崩溃。

随机 00_00_00-00_00_30

注册环境并开始训练

任务通过 Gymnasium 注册,入口是 ManagerBasedRLEnv

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import gymnasium as gym

from . import agents

gym.register(
id="Wheelleg-Velocity-v0",
entry_point="isaaclab.envs:ManagerBasedRLEnv",
disable_env_checker=True,
kwargs={
"env_cfg_entry_point": (
f"{__name__}.isaaclab_mb_wheelleg_env_cfg:IsaaclabMbWheellegEnvCfg"
),
"play_env_cfg_entry_point": (
f"{__name__}.isaaclab_mb_wheelleg_env_cfg:IsaaclabMbWheellegPlayEnvCfg"
),
"rsl_rl_cfg_entry_point": (
f"{agents.__name__}.rsl_rl_ppo_cfg:PPORunnerCfg"
),
},
)

先以 editable mode (-e)安装工程,再检查任务是否能被发现:

1
2
python -m pip install -e source/isaaclab_mb_wheelleg
python scripts/list_envs.py

随后启动训练:

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

本工程的 PPO 配置每个环境收集 24 步再进入更新,actor 和 critic 的隐藏层都是 [512, 256, 128]。环境使用 512 个并行实例,仿真步长为 1/120 sdecimation=2,所以策略控制周期为 1/60 s;单回合长度为 5 秒。

然后可以看到机器人在训练过程中不断探索:

低趴行走 00_00_00-00_00_30

然后最终学会稳定的站立:

平衡啦 00_00_00-00_00_30

站立训练开始后,机器人先逐渐学会避免提前终止,随后在机身高度和姿态奖励的作用下保持直立。至此,工程搭建完成并学会站立。下一篇训练行走。