1. 项目概述:四足机器人强化学习环境架构解析
在四足机器人强化学习研究中,大多数开发者会首先关注奖励函数设计、策略网络架构和PPO算法实现。但真正支撑整个训练闭环的底层基础,其实是环境骨架的设计与实现。本文将以walk_these_ways项目中的环境架构为例,深入剖析一个完整的四足机器人强化学习环境是如何构建的。
这个环境架构的核心由四个关键组件组成:
BaseTask:定义了Isaac Gym环境的最小生命周期骨架LeggedRobot:扩展为完整的机器人任务环境VelocityTrackingEasyEnv:提供训练器可直接消费的标准接口HistoryWrapper:在环境外侧实现观测历史窗口功能
这些组件并非简单的继承关系,而是在运行时形成了一条完整的处理流水线,共同完成一次仿真循环。理解这个架构对于开发自定义机器人任务、调试训练问题以及优化性能都至关重要。
2. 环境骨架核心组件详解
2.1 BaseTask:最小环境骨架实现
BaseTask类位于base_task.py文件中,它不关心具体的机器人行为,而是专注于搭建一个统一的环境框架。这个基础类主要处理以下核心任务:
- 获取Isaac Gym的全局接口句柄
- 解析和存储仿真参数配置
- 分配基础RL缓冲区
- 创建仿真实例和可视化界面
- 定义统一的reset()和step()接口约定
初始化逻辑的关键代码如下:
python复制class BaseTask(gym.Env):
def __init__(self, cfg, sim_params, physics_engine, sim_device, headless, eval_cfg=None):
self.gym = gymapi.acquire_gym() # 获取Isaac Gym全局接口
self.sim_params = sim_params # 仿真参数(dt、子步数等)
self.sim_device = sim_device # 仿真运行设备(如cuda:0)
self.headless = headless # 是否无界面运行
设备选择逻辑特别值得注意:
python复制if sim_device_type == 'cuda' and sim_params.use_gpu_pipeline:
self.device = self.sim_device # 仿真和状态tensor都留在GPU
else:
self.device = 'cpu' # 退回CPU张量
这个判断直接影响训练效率——当状态tensor可以直接在GPU上被策略网络读取时,大规模并行训练才能充分发挥性能优势。
2.2 基础缓冲区分配
BaseTask初始化时会创建一系列基础缓冲区,这些缓冲区不是临时变量,而是整个并行环境系统的长期状态寄存器:
python复制self.obs_buf = torch.zeros(self.num_envs, self.num_obs, device=self.device) # 当前观测
self.rew_buf = torch.zeros(self.num_envs, device=self.device) # 当前步奖励
self.reset_buf = torch.ones(self.num_envs, device=self.device, dtype=torch.long) # 需要reset的环境标记
self.episode_length_buf = torch.zeros(self.num_envs, device=self.device) # 各环境已运行步数
self.privileged_obs_buf = torch.zeros(self.num_envs, self.num_privileged_obs, device=self.device) # 特权观测
特权观测(privileged observations)是一个重要概念,它包含训练时对critic或adaptation模块可见,但部署时策略网络无法获取的信息,如地面摩擦系数、payload质量等物理参数。
2.3 reset()方法的特殊实现
BaseTask的reset()实现方式值得特别关注:
python复制def reset(self):
self.reset_idx(torch.arange(self.num_envs, device=self.device)) # 重置所有环境
obs, privileged_obs, _, _, _ = self.step(
torch.zeros(self.num_envs, self.num_actions, device=self.device, requires_grad=False)
) # 执行一次全零动作的step
return obs, privileged_obs
这种实现确保了返回的初始观测不是理论上的理想值,而是经过完整物理刷新后的真实观测。这种设计避免了"完美初始状态"与实际情况的偏差,使训练更加稳健。
技术细节:
torch.arange(self.num_envs, device=self.device)生成一个从0到num_envs-1的环境编号张量,用于索引所有并行环境的状态。将其放在self.device上是为了高效访问GPU/CPU上的环境状态数据。
3. LeggedRobot:从抽象骨架到具体机器人环境
3.1 初始化流程解析
LeggedRobot类(位于legged_robot.py)将BaseTask的抽象骨架扩展为具体的机器人环境。它的初始化流程非常系统化:
python复制self._parse_cfg(self.cfg) # 解析配置参数
super().__init__(self.cfg, sim_params, ...) # 调用BaseTask初始化
self._init_command_distribution(...) # 初始化命令分布
self._init_buffers() # 初始化状态缓冲区
self._prepare_reward_function() # 准备奖励函数
其中_init_buffers()是最关键的步骤之一,它负责将Isaac Gym中的原始状态tensor连接到PyTorch张量视图,为后续训练做好准备。
3.2 仿真世界创建过程
LeggedRobot.create_sim()方法构建了完整的仿真世界:
python复制self.sim = self.gym.create_sim(...) # 创建仿真实例
# 根据配置创建地形
mesh_type = self.cfg.terrain.mesh_type
if mesh_type == 'plane':
self._create_ground_plane()
elif mesh_type == 'heightfield':
self._create_heightfield()
elif mesh_type == 'trimesh':
self._create_trimesh()
self._create_envs() # 批量创建机器人实例
这个过程体现了典型的仿真场景构建逻辑:先创建物理世界,然后添加地形,最后放置机器人实体。这种分阶段构建方式既清晰又灵活,便于支持各种复杂场景。
3.3 状态缓冲区初始化详解
_init_buffers()方法实现了从Isaac Gym原始状态到PyTorch张量的转换:
python复制# 从Isaac Gym获取原始状态tensor
actor_root_state = self.gym.acquire_actor_root_state_tensor(self.sim)
dof_state_tensor = self.gym.acquire_dof_state_tensor(self.sim)
net_contact_forces = self.gym.acquire_net_contact_force_tensor(self.sim)
# 包装为PyTorch视图
self.root_states = gymtorch.wrap_tensor(actor_root_state)
self.dof_state = gymtorch.wrap_tensor(dof_state_tensor)
self.dof_pos = self.dof_state.view(self.num_envs, self.num_dof, 2)[..., 0]
self.dof_vel = self.dof_state.view(self.num_envs, self.num_dof, 2)[..., 1]
这些张量包含了机器人控制的全部物理基础:
root_states:机身在世界坐标系中的位姿和速度dof_pos/dof_vel:关节位置和速度contact_forces:足端接触力信息
3.4 派生状态计算
环境还会计算一系列派生状态量,这些量对控制策略非常有用:
python复制self.base_lin_vel = quat_rotate_inverse(self.base_quat, self.root_states[:self.num_envs, 7:10])
self.base_ang_vel = quat_rotate_inverse(self.base_quat, self.root_states[:self.num_envs, 10:13])
self.projected_gravity = quat_rotate_inverse(self.base_quat, self.gravity_vec)
其中projected_gravity特别重要,它本质上是将重力方向投影到机身坐标系,相当于一个虚拟的IMU传感器,让策略能够感知机身当前的倾斜状态。
4. 仿真步进与控制的实现细节
4.1 step()方法的执行流程
LeggedRobot.step()实现了一个完整的控制循环:
python复制def step(self, actions):
# 1. 动作裁剪
self.actions = torch.clip(actions, -clip_actions, clip_actions).to(self.device)
# 2. 执行decimation个物理子步
for _ in range(self.cfg.control.decimation):
self.torques = self._compute_torques(self.actions)
self.gym.set_dof_actuation_force_tensor(self.sim, gymtorch.unwrap_tensor(self.torques))
self.gym.simulate(self.sim)
self.gym.fetch_results(self.sim, True)
self.gym.refresh_dof_state_tensor(self.sim)
# 3. 后处理
self.post_physics_step()
return self.obs_buf, self.privileged_obs_buf, self.rew_buf, self.reset_buf, self.extras
关键点在于:
- 一个RL步(step)包含多个物理子步(simulate)
- 在每个物理子步中都会根据当前状态重新计算扭矩
- 动作在整个RL步期间保持恒定
4.2 物理子步与RL步的关系
项目中典型的配置是:
- 物理步长(sim_params.dt):0.005秒
- decimation:4
- 因此RL步长(self.dt):0.02秒
这意味着:
- 策略每0.02秒输出一个新动作
- 该动作会在4个物理子步中持续生效
- 每个物理子步都会根据最新状态计算扭矩
这种设计平衡了仿真精度和训练效率,同时保持了控制的连贯性。
4.3 后处理阶段关键操作
post_physics_step()完成了几个重要任务:
python复制def post_physics_step(self):
# 刷新所有状态张量
self.gym.refresh_actor_root_state_tensor(self.sim)
self.gym.refresh_net_contact_force_tensor(self.sim)
# 更新派生状态
self.base_pos[:] = self.root_states[:self.num_envs, 0:3]
self.base_quat[:] = self.root_states[:self.num_envs, 3:7]
# 检查终止条件
self.check_termination()
# 计算奖励
self.compute_reward()
# 重置已完成的环境
env_ids = self.reset_buf.nonzero(as_tuple=False).flatten()
self.reset_idx(env_ids)
# 计算新观测
self.compute_observations()
这个执行顺序确保了:
- 使用最新物理状态判断终止
- 基于终止状态计算奖励
- 只重置已完成的环境
- 为新一步提供正确的观测
5. 接口层与历史窗口实现
5.1 VelocityTrackingEasyEnv的接口适配
VelocityTrackingEasyEnv(位于velocity_tracking/init.py)主要做了两件事:
- 将配置转换为Isaac Gym的SimParams:
python复制sim_params = gymapi.SimParams()
gymutil.parse_sim_config(vars(cfg.sim), sim_params)
- 调整step()返回格式,将额外信息放入extras字典:
python复制def step(self, actions):
obs, privileged_obs, rew, reset, extras = super().step(actions)
extras.update({
"privileged_obs": privileged_obs,
"joint_pos": self.dof_pos.cpu().numpy(),
"contact_states": (self.contact_forces[:, self.feet_indices, 2] > 1.).cpu().numpy()
})
return obs, rew, reset, extras
这种设计使接口更加灵活,便于扩展新的调试信息而不破坏原有结构。
5.2 HistoryWrapper的工作原理
HistoryWrapper(位于history_wrapper.py)实现了观测历史窗口功能:
初始化时创建历史缓冲区:
python复制self.obs_history = torch.zeros(self.env.num_envs,
self.obs_history_length * self.num_obs,
device=self.env.device)
每一步更新滑动窗口:
python复制def step(self, action):
obs, rew, done, info = self.env.step(action)
self.obs_history = torch.cat((self.obs_history[:, self.env.num_obs:], obs), dim=-1)
return {'obs': obs, 'obs_history': self.obs_history}, rew, done, info
这种实现将历史窗口展平为二维矩阵,便于直接输入到MLP网络。窗口更新采用FIFO策略,始终保持固定长度。
5.3 reset()的特殊处理
HistoryWrapper在reset时会清空历史窗口:
python复制def reset(self):
ret = super().reset()
self.obs_history[:, :] = 0
return {"obs": ret, "obs_history": self.obs_history}
这种设计明确告知策略:这是一条全新轨迹,没有历史信息可用。相比用当前观测填充整个窗口,这种做法更加清晰一致。
6. 环境架构的完整工作流程
将各组件串联起来,一次完整的环境步执行流程如下:
- 训练器调用
HistoryWrapper.step(action) HistoryWrapper调用内部的VelocityTrackingEasyEnv.step(action)VelocityTrackingEasyEnv调用父类LeggedRobot.step(action)LeggedRobot.step()执行:- 动作裁剪
- 多个物理子步循环
- 状态刷新和奖励计算
- 环境重置
- 观测构建
- 结果沿调用链返回,
HistoryWrapper更新观测历史 - 最终返回给训练器的观测包含:
- 当前观测(obs)
- 特权观测(privileged_obs)
- 历史窗口(obs_history)
这种分层设计既保持了各组件职责单一,又通过清晰的接口实现了复杂功能的组合。理解这个架构对于开发自定义机器人任务、调试训练问题以及优化性能都至关重要。
在实际应用中,这种架构可以支持:
- 快速更换不同的机器人模型
- 灵活调整观测空间和奖励函数
- 方便地添加新的传感器模态
- 高效实现课程学习和域随机化
掌握环境骨架的实现原理,是开发高质量机器人强化学习系统的关键基础。