强化学习 Policy 的 Tracking 能力全解析,以Legged_gym为例解说Policy的训练流程

概述:Policy 作为通用 Tracker

Tracking 的核心思想

Legged Gym 的强化学习 Policy 本质上是一个通用的多模态跟踪系统。通过设计不同的观测空间和奖励函数,Policy 可以学习跟踪各种目标:

复制代码
🎯 Tracking = 观测 (Observation) + 目标 (Target) + 奖励 (Reward)

训练的本质是让 Policy 学会:

  1. 感知当前状态 (Where am I?)
  2. 理解目标 (Where should I be?)
  3. 计算偏差 (How far am I from the goal?)
  4. 输出控制 (What should I do?)

为什么 RL Policy 擅长 Tracking?

优势 传统控制器 RL Policy
非线性系统 需要精确建模 ✅ 从数据学习
高维耦合 难以设计 ✅ 端到端学习
扰动鲁棒性 需要显式设计 ✅ 隐式学习
多目标平衡 需要手动调优 ✅ 奖励函数自动平衡
泛化能力 局限于设计范围 ✅ 可泛化到未见场景

Legged Gym 的 Tracking 框架

基于 Isaac Gym 和 PPO (Proximal Policy Optimization) 算法,Legged Gym 提供了一个统一的框架,可以训练 Policy 跟踪:

  • 速度指令 (Velocity Commands)
  • 参考轨迹 (Reference Trajectories)
  • 关节状态 (Joint States)
  • 身体姿态 (Body Pose)
  • 足端位置 (Foot Positions)
  • 运动风格 (Motion Style)
  • 多模态组合 (Multi-modal Combinations)

Tracking 的本质与范式

Tracking 的数学定义

在强化学习中,Tracking 问题可以形式化为:

min⁡πEτ∼π[∑t=0T∥st−stref∥2] \min_{\pi} \mathbb{E}{\tau \sim \pi} \left[ \sum{t=0}^{T} \| s_t - s_t^{ref} \|^2 \right] πminEτ∼π[t=0∑T∥st−stref∥2]

其中:

  • π\piπ: Policy 策略
  • sts_tst: 当前状态
  • strefs_t^{ref}stref: 参考目标
  • τ\tauτ: 轨迹

Tracking 的三种范式

1. 目标跟踪 (Target Tracking)
复制代码
给定固定/变化的目标 → Policy 持续跟踪
例:速度指令跟踪、姿态保持
2. 轨迹跟踪 (Trajectory Tracking)
复制代码
给定时间序列轨迹 → Policy 按时间跟随
例:动作捕捉数据重现、预定义步态
3. 分布跟踪 (Distribution Tracking)
复制代码
给定状态分布 → Policy 采样符合分布的行为
例:风格模仿、多样性生成

Legged Gym 中的 Tracking 实现

在 Legged Gym 中,所有 Tracking 任务都遵循统一的框架:

python 复制代码
# 统一的 Tracking 框架
class TrackingPolicy:
    def __init__(self):
        self.target = None  # 跟踪目标
        self.current_state = None  # 当前状态
        
    def compute_tracking_error(self):
        """计算跟踪误差"""
        return self.target - self.current_state
    
    def compute_tracking_reward(self, error):
        """将误差转换为奖励"""
        return torch.exp(-error / sigma)  # 高斯核
    
    def step(self, obs):
        """执行控制"""
        action = self.policy(obs)
        return action

可跟踪的模态与量

1. 运动学量 (Kinematic Quantities)

维度 观测空间 典型范围
线速度 3D 身体坐标系 [-2, 2] m/s
角速度 3D 身体坐标系 [-2, 2] rad/s
位置 3D 世界坐标系 任意
朝向 4D (四元数) 世界坐标系 SO(3)
关节角度 12D 关节空间 机械限位
关节角速度 12D 关节空间 [-10, 10] rad/s

2. 动力学量 (Dynamic Quantities)

维度 观测空间 应用场景
关节力矩 12D 关节空间 力控制任务
接触力 4×3D 足端 力感知步态
动量 3D 身体坐标系 跳跃、冲击
能量消耗 1D 标量 效率优化

3. 几何量 (Geometric Quantities)

维度 观测空间 应用场景
足端位置 4×3D 身体坐标系 精确落脚
质心位置 3D 身体坐标系 平衡控制
支撑多边形 2D 水平面 稳定性
地形高度图 187D 局部栅格 地形适应

4. 时序量 (Temporal Quantities)

维度 观测空间 应用场景
相位 1D [0, 2π] 周期步态
步频 1D [0.5, 3] Hz 运动节奏
占空比 1D [0, 1] 支撑/摆动
时序轨迹 T×D 时间窗口 动作复现

5. 高级语义量 (Semantic Quantities)

维度 观测空间 应用场景
步态类型 离散 {trot, pace, gallop} 步态切换
运动风格 连续 潜在空间 风格化运动
任务目标 混合 任务相关 多任务学习
地形类型 离散 {flat, rough, stairs} 地形识别

Tracking 可行性分析

不是所有量都适合直接跟踪,需要考虑:

因素 说明 示例
可观测性 能否实时测量? ✅ 关节角度 vs ❌ 肌肉激活
可控性 Policy 能否影响? ✅ 速度 vs ❌ 外部扰动
时间尺度 控制频率匹配? ✅ 50Hz 关节控制 vs ❌ 0.1Hz 规划
耦合度 是否强耦合? ⚠️ 位置+姿态耦合
物理约束 是否违反动力学? ❌ 突变速度指令

运动跟踪 (Motion Tracking)

基础:速度指令跟踪

这是 Legged Gym 的默认任务,也是最基础的 Tracking 形式。

跟踪目标
python 复制代码
commands = [vel_x, vel_y, ang_vel_yaw, heading]  # 4D 向量
# vel_x:      前后线速度 (m/s)
# vel_y:      左右线速度 (m/s)  
# ang_vel_yaw: 偏航角速度 (rad/s)
# heading:    目标朝向 (rad) - 可选
观测空间
python 复制代码
obs = [
    base_lin_vel,       # [3] 当前线速度
    base_ang_vel,       # [3] 当前角速度
    projected_gravity,  # [3] 重力投影
    commands[:3],       # [3] 速度指令
    dof_pos,           # [12] 关节位置
    dof_vel,           # [12] 关节速度
    actions_prev,      # [12] 上一动作
    # height_map        # [187] 地形高度(可选)
]  # Total: 48 或 235
跟踪奖励
python 复制代码
def _reward_tracking_lin_vel(self):
    """线速度跟踪奖励"""
    error = torch.sum(
        torch.square(self.commands[:, :2] - self.base_lin_vel[:, :2]), 
        dim=1
    )
    return torch.exp(-error / 0.25)  # 高斯核,sigma=0.25

def _reward_tracking_ang_vel(self):
    """角速度跟踪奖励"""
    error = torch.square(self.commands[:, 2] - self.base_ang_vel[:, 2])
    return torch.exp(-error / 0.25)

奖励曲线

复制代码
误差 (m/s) | 奖励
-----------|------
0.0        | 1.00  ← 完美跟踪
0.1        | 0.85
0.25       | 0.37
0.5        | 0.14
1.0        | 0.02
训练配置
python 复制代码
class rewards:
    class scales:
        tracking_lin_vel = 1.0   # 主要优化目标
        tracking_ang_vel = 0.5   # 次要目标
        lin_vel_z = -2.0         # 惩罚跳动
        ang_vel_xy = -0.05       # 惩罚翻滚

进阶:身体姿态跟踪

跟踪目标
python 复制代码
target_pose = {
    'position': [x, y, z],           # 目标位置
    'orientation': [qw, qx, qy, qz], # 目标四元数
    'height': h                      # 目标高度
}
姿态跟踪奖励
python 复制代码
def _reward_orientation(self):
    """惩罚非水平姿态"""
    # 使用重力投影向量
    # 理想值: [0, 0, -1] (完全水平)
    error = torch.sum(
        torch.square(self.projected_gravity[:, :2]), 
        dim=1
    )
    return torch.exp(-error / 0.25)

def _reward_base_height(self):
    """高度跟踪"""
    target_height = 0.52  # ANYmal-C 标称高度
    error = torch.square(
        self.root_states[:, 2] - target_height
    )
    return torch.exp(-error / 0.1)
应用场景
  1. 平衡任务: 在动态扰动下保持姿态
  2. 俯仰控制: 爬坡时调整身体角度
  3. 滚转避免: 侧向行走时防止翻倒

轨迹跟踪 (Trajectory Tracking)

概述

轨迹跟踪要求 Policy 不仅跟踪目标值,还要按照时间顺序精确重现整条轨迹。

参考轨迹来源

1. Motion Capture 数据
python 复制代码
# 从动捕系统获取参考轨迹
mocap_data = {
    'joint_pos': [T, 12],      # 关节角度轨迹
    'joint_vel': [T, 12],      # 关节速度轨迹
    'root_pos': [T, 3],        # 根节点位置
    'root_orient': [T, 4],     # 根节点朝向
    'time': [T]                # 时间戳
}
2. 动画/仿真数据
python 复制代码
# 从物理仿真或动画软件导出
animation_data = load_animation("trot_cycle.npy")
# Shape: [100, 48]  # 100 帧,48 维状态
3. 专家策略生成
python 复制代码
# 使用传统控制器生成示范
expert_trajectory = classical_controller.rollout(
    duration=5.0, 
    command=[1.0, 0.0, 0.0]
)

实现:时序轨迹跟踪

扩展观测空间
python 复制代码
class LeggedRobotCfg:
    class env:
        num_observations = 48 + 12  # 原始观测 + 参考状态
        reference_state_dim = 12    # 参考关节角度
        
obs = torch.cat([
    standard_obs,           # [48] 标准观测
    reference_joint_pos     # [12] 当前时刻的参考关节角度
], dim=-1)
轨迹跟踪奖励
python 复制代码
def _reward_joint_tracking(self):
    """关节轨迹跟踪"""
    # 获取当前时间对应的参考状态
    t = self.episode_length_buf * self.dt
    ref_pos = self.get_reference_at_time(t)
    
    # 计算跟踪误差
    error = torch.sum(
        torch.square(self.dof_pos - ref_pos), 
        dim=1
    )
    return torch.exp(-error / 0.1)

def _reward_joint_vel_tracking(self):
    """关节速度跟踪"""
    t = self.episode_length_buf * self.dt
    ref_vel = self.get_reference_vel_at_time(t)
    
    error = torch.sum(
        torch.square(self.dof_vel - ref_vel), 
        dim=1
    )
    return torch.exp(-error / 0.5)
参考轨迹插值
python 复制代码
def get_reference_at_time(self, t):
    """根据时间插值参考轨迹"""
    # 循环轨迹
    t_normalized = t % self.traj_duration
    
    # 线性插值
    idx = (t_normalized / self.dt).long()
    alpha = (t_normalized / self.dt) - idx
    
    ref_pos = (1 - alpha) * self.trajectory[idx] + \
              alpha * self.trajectory[idx + 1]
    
    return ref_pos

相位跟踪 (Phase Tracking)

周期步态的相位表示
python 复制代码
# 相位作为观测
phase = (t % T) / T * 2 * π  # [0, 2π]
obs = torch.cat([
    standard_obs,
    torch.sin(phase),  # 正弦编码
    torch.cos(phase)   # 余弦编码
], dim=-1)
相位条件奖励
python 复制代码
def _reward_phase_based_foot_contact(self):
    """相位同步的接触奖励"""
    phase = self.get_phase()
    
    # 期望接触模式 (trot 步态)
    expected_contact = torch.tensor([
        [1, 0, 0, 1],  # 对角支撑 1
        [0, 1, 1, 0]   # 对角支撑 2
    ])
    
    phase_idx = (phase / (2*π) * 2).long() % 2
    target = expected_contact[phase_idx]
    
    actual_contact = self.contact_forces[:, self.feet_indices, 2] > 1.0
    
    # 匹配度奖励
    match = (actual_contact == target).float().mean(dim=1)
    return match

状态跟踪 (State Tracking)

多关节状态同步跟踪

挑战

同时跟踪 12 个关节的位置和速度,共 24 维状态:

python 复制代码
target_state = {
    'dof_pos': [12],  # 关节角度
    'dof_vel': [12]   # 关节速度
}
加权跟踪奖励
python 复制代码
def _reward_weighted_joint_tracking(self):
    """加权关节跟踪"""
    # 不同关节不同权重
    weights = torch.tensor([
        1.0, 1.0, 0.8,  # 前左腿 (HAA, HFE, KFE)
        1.0, 1.0, 0.8,  # 前右腿
        1.0, 1.0, 0.8,  # 后左腿
        1.0, 1.0, 0.8   # 后右腿
    ])
    
    error = torch.square(self.dof_pos - self.target_dof_pos)
    weighted_error = (error * weights).sum(dim=1)
    
    return torch.exp(-weighted_error / 0.5)

足端轨迹跟踪

正向运动学计算
python 复制代码
def compute_foot_positions(self):
    """计算足端位置(身体坐标系)"""
    # 使用机器人运动学模型
    foot_pos = []
    for leg_id in range(4):
        joint_angles = self.dof_pos[:, leg_id*3:(leg_id+1)*3]
        pos = forward_kinematics(joint_angles, leg_id)
        foot_pos.append(pos)
    
    return torch.stack(foot_pos, dim=1)  # [N, 4, 3]
足端跟踪奖励
python 复制代码
def _reward_foot_position_tracking(self):
    """足端位置跟踪"""
    current_foot_pos = self.compute_foot_positions()
    target_foot_pos = self.get_target_foot_positions()
    
    error = torch.sum(
        torch.square(current_foot_pos - target_foot_pos), 
        dim=[1, 2]
    )
    return torch.exp(-error / 0.05)

多模态融合跟踪

概述

实际应用中,往往需要同时跟踪多个目标,这需要在奖励函数中平衡多个 Tracking 目标。

多目标跟踪框架

python 复制代码
class MultiModalTracking:
    def compute_reward(self):
        """多模态跟踪总奖励"""
        reward = 0
        
        # 1. 速度跟踪 (主要目标)
        reward += 1.0 * self.tracking_lin_vel()
        reward += 0.5 * self.tracking_ang_vel()
        
        # 2. 姿态跟踪 (次要目标)
        reward += 0.2 * self.orientation_tracking()
        reward += 0.1 * self.height_tracking()
        
        # 3. 步态跟踪 (风格约束)
        reward += 0.3 * self.phase_based_gait()
        
        # 4. 稳定性 (软约束)
        reward -= 2.0 * self.lin_vel_z_penalty()
        reward -= 0.05 * self.ang_vel_xy_penalty()
        
        return reward

典型组合场景

场景 1: 速度 + 姿态跟踪
python 复制代码
# 应用:斜坡行走
class SlopeWalkingTracking:
    rewards = {
        'tracking_lin_vel': 1.0,      # 保持速度
        'tracking_ang_vel': 0.5,      # 转向能力
        'orientation': 0.5,           # 保持水平(抵抗斜坡倾斜)
        'base_height': 0.2,           # 高度稳定
        'torques': -0.0001,           # 能量效率
    }
场景 2: 轨迹 + 相位跟踪
python 复制代码
# 应用:动作复现
class MotionImitationTracking:
    rewards = {
        'joint_pos_tracking': 2.0,    # 关节角度匹配
        'joint_vel_tracking': 1.0,    # 关节速度匹配
        'phase_tracking': 0.5,        # 时序对齐
        'foot_contact': 0.3,          # 接触时机
        'root_pose': 0.5,             # 身体位姿
    }

Policy 网络结构

Actor-Critic 架构

系统使用 Actor-Critic 双网络结构:

python 复制代码
class ActorCritic(nn.Module):
    def __init__(self):
        # Actor: 策略网络,用于控制
        self.actor = MLP(
            input_dim=num_observations,      # 观测维度
            output_dim=num_actions,          # 动作维度 (12个关节)
            hidden_dims=[512, 256, 128],     # 隐藏层
            activation='elu'
        )
        
        # Critic: 价值网络,仅用于训练
        self.critic = MLP(
            input_dim=num_observations,      # 或 num_privileged_obs
            output_dim=1,                    # 输出状态价值
            hidden_dims=[512, 256, 128],
            activation='elu'
        )

网络配置示例

配置 1: 轻量级速度跟踪
python 复制代码
class policy:
    actor_hidden_dims = [128, 64, 32]
    critic_hidden_dims = [128, 64, 32]
    activation = 'elu'
    
# 参数量:~50K
# 推理速度:<0.5ms
# 适用:简单平地行走
配置 2: 标准复杂地形跟踪
python 复制代码
class policy:
    actor_hidden_dims = [512, 256, 128]
    critic_hidden_dims = [512, 256, 128]
    activation = 'elu'
    
# 参数量:~400K
# 推理速度:~1ms
# 适用:复杂地形、多目标跟踪
配置 3: 轨迹跟踪 (LSTM)
python 复制代码
class policy:
    actor_hidden_dims = [256, 128]
    critic_hidden_dims = [256, 128]
    rnn_type = 'lstm'
    rnn_hidden_size = 256
    rnn_num_layers = 2
    
# 参数量:~600K
# 推理速度:~2ms
# 适用:动作模仿、相位跟踪

从训练到部署

Policy 的本质理解

训练完成后,Policy 是一个条件控制器

复制代码
Policy: (Observation, Target) → Action

关键理解

  1. ✅ Policy 需要外部提供跟踪目标 (target/command)
  2. ✅ Policy 根据当前状态和目标输出控制动作
  3. ❌ Policy 不会自己决定目标是什么
  4. ❌ Policy 没有全局规划能力

训练流程

1. 环境配置
python 复制代码
# train.py
def train(args):
    # 创建环境
    env, env_cfg = task_registry.make_env(name=args.task, args=args)
    
    # 创建 PPO Runner
    ppo_runner, train_cfg = task_registry.make_alg_runner(
        env=env, 
        name=args.task,
        args=args
    )
    
    # 开始训练
    ppo_runner.learn(
        num_learning_iterations=train_cfg.runner.max_iterations,
        init_at_random_ep_len=True
    )
2. 训练循环
python 复制代码
# PPO 训练循环
for iteration in range(max_iterations):
    # 采集经验
    for step in range(num_steps_per_env):
        obs = env.get_observations()
        actions = policy(obs)
        obs_next, rewards, dones, _ = env.step(actions)
        
        buffer.add(obs, actions, rewards, dones)
    
    # 更新策略
    for epoch in range(num_learning_epochs):
        for mini_batch in buffer.iterate(num_mini_batches):
            loss = compute_ppo_loss(mini_batch)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
    
    # 保存模型
    if iteration % save_interval == 0:
        save_model(f"model_{iteration}.pt")

部署流程

1. 加载模型
python 复制代码
# play.py
def load_policy(model_path):
    # 加载训练配置
    env_cfg, train_cfg = task_registry.get_cfgs(name=args.task)
    
    # 创建环境
    env, _ = task_registry.make_env(name=args.task, env_cfg=env_cfg)
    
    # 加载 Policy
    train_cfg.runner.resume = True
    ppo_runner, _ = task_registry.make_alg_runner(env=env, train_cfg=train_cfg)
    policy = ppo_runner.get_inference_policy(device=env.device)
    
    return policy, env
2. 实时推理

训练算法:PPO

PPO 核心参数

python 复制代码
# 来自 LeggedRobotCfgPPO.algorithm
class algorithm:
    # 价值函数损失
    value_loss_coef = 1.0              # 价值损失系数
    use_clipped_value_loss = True      # 使用裁剪的价值损失
    
    # PPO 特定参数
    clip_param = 0.2                   # PPO 裁剪参数 ε
    entropy_coef = 0.01                # 熵正则化系数
    
    # 优化参数
    num_learning_epochs = 5            # 每次迭代的 epoch 数
    num_mini_batches = 4               # mini-batch 数量
    learning_rate = 1.e-3              # 学习率
    schedule = 'adaptive'              # 学习率调度: adaptive/fixed
    
    # GAE (广义优势估计)
    gamma = 0.99                       # 折扣因子
    lam = 0.95                         # GAE λ 参数
    
    # KL 散度约束
    desired_kl = 0.01                  # 目标 KL 散度
    max_grad_norm = 1.0                # 梯度裁剪

训练循环参数

python 复制代码
# 来自 LeggedRobotCfgPPO.runner
class runner:
    num_steps_per_env = 24             # 每个环境每次迭代采集的步数
    max_iterations = 1500              # 总迭代次数
    save_interval = 50                 # 每 50 次迭代保存一次模型
    
    # 并行环境数
    num_envs = 4096                    # 同时运行 4096 个环境

关键计算:

python 复制代码
# 每次迭代采样总步数
total_steps = num_envs * num_steps_per_env = 4096 * 24 = 98,304 步

# Mini-batch 大小
mini_batch_size = total_steps / num_mini_batches = 98,304 / 4 = 24,576

# 总训练样本数
total_samples = max_iterations * total_steps = 1500 * 98,304 ≈ 1.47 亿步

PPO 损失函数

python 复制代码
# 1. Policy Loss (Clipped Surrogate Objective)
ratio = π_new(a|s) / π_old(a|s)
L_policy = -min(ratio * A, clip(ratio, 1-ε, 1+ε) * A)

# 2. Value Loss
L_value = (V(s) - V_target)^2

# 3. Entropy Bonus
L_entropy = -entropy(π(·|s))

# 总损失
Loss = L_policy + value_loss_coef * L_value + entropy_coef * L_entropy

奖励函数设计

奖励函数的作用

通过精心设计的奖励函数组合,引导 Policy 学习期望的行为。

主要奖励项

python 复制代码
# 来自 LeggedRobotCfg.rewards.scales
class scales:
    # === 核心跟踪奖励 ===
    tracking_lin_vel = 1.0      # 线速度跟踪
    tracking_ang_vel = 0.5      # 角速度跟踪
    
    # === 稳定性惩罚 ===
    lin_vel_z = -2.0            # 惩罚 z 轴速度(上下跳动)
    ang_vel_xy = -0.05          # 惩罚 x,y 轴角速度(翻滚/俯仰)
    orientation = -0.0          # 惩罚非水平姿态
    base_height = -0.0          # 惩罚偏离目标高度
    
    # === 能量与平滑性 ===
    torques = -0.00001          # 惩罚力矩(能量消耗)
    dof_vel = -0.0              # 惩罚关节速度
    dof_acc = -2.5e-7           # 惩罚关节加速度
    action_rate = -0.01         # 惩罚动作变化(平滑性)
    
    # === 步态奖励 ===
    feet_air_time = 1.0         # 奖励长步幅(脚在空中的时间)
    
    # === 碰撞惩罚 ===
    collision = -1.0            # 惩罚非脚部的碰撞
    feet_stumble = -0.0         # 惩罚脚部撞到垂直面
    
    # === 终止惩罚 ===
    termination = -0.0          # 跌倒时的惩罚
    
    # === 其他 ===
    stand_still = -0.0          # 零速度指令时惩罚运动

关键奖励函数实现

1. 速度跟踪奖励(最重要)
python 复制代码
def _reward_tracking_lin_vel(self):
    """线速度跟踪 - 核心训练目标"""
    # 计算指令速度与实际速度的误差
    lin_vel_error = torch.sum(
        torch.square(self.commands[:, :2] - self.base_lin_vel[:, :2]), 
        dim=1
    )
    # 使用高斯核将误差转换为 [0, 1] 的奖励
    return torch.exp(-lin_vel_error / tracking_sigma)
    # tracking_sigma = 0.25

def _reward_tracking_ang_vel(self):
    """角速度跟踪"""
    ang_vel_error = torch.square(self.commands[:, 2] - self.base_ang_vel[:, 2])
    return torch.exp(-ang_vel_error / tracking_sigma)

奖励机制分析:

  • 完美跟踪 (误差=0) → 奖励 = 1.0
  • 误差 = 0.25 → 奖励 ≈ 0.37
  • 误差 = 0.5 → 奖励 ≈ 0.14
2. 步态奖励
python 复制代码
def _reward_feet_air_time(self):
    """奖励长步幅 - 促进自然步态"""
    # 检测脚的接触状态
    contact = self.contact_forces[:, self.feet_indices, 2] > 1.0
    contact_filt = torch.logical_or(contact, self.last_contacts)
    
    # 检测首次接触
    first_contact = (self.feet_air_time > 0.) * contact_filt
    
    # 累积空中时间
    self.feet_air_time += self.dt
    
    # 只在首次接触时给奖励
    rew_airTime = torch.sum((self.feet_air_time - 0.5) * first_contact, dim=1)
    
    # 仅在有运动指令时给奖励
    rew_airTime *= torch.norm(self.commands[:, :2], dim=1) > 0.1
    
    # 重置接触脚的空中时间
    self.feet_air_time *= ~contact_filt
    return rew_airTime
3. 关节限制惩罚
python 复制代码
def _reward_dof_pos_limits(self):
    """惩罚关节位置接近极限"""
    # 计算超出软限制的量
    out_of_limits = -(self.dof_pos - self.dof_pos_limits[:, 0]).clip(max=0.)  # 下限
    out_of_limits += (self.dof_pos - self.dof_pos_limits[:, 1]).clip(min=0.)  # 上限
    return torch.sum(out_of_limits, dim=1)

奖励组合计算

python 复制代码
def compute_reward(self):
    """计算总奖励"""
    self.rew_buf[:] = 0.
    
    # 遍历所有非零奖励项
    for i in range(len(self.reward_functions)):
        name = self.reward_names[i]
        # 调用奖励函数并乘以 scale 和 dt
        rew = self.reward_functions[i]() * self.reward_scales[name]
        self.rew_buf += rew
        self.episode_sums[name] += rew  # 记录累积奖励
    
    # 可选:只保留正奖励
    if self.cfg.rewards.only_positive_rewards:
        self.rew_buf[:] = torch.clip(self.rew_buf[:], min=0.)
    
    # 添加终止惩罚(不受 clipping 影响)
    if "termination" in self.reward_scales:
        rew = self._reward_termination() * self.reward_scales["termination"]
        self.rew_buf += rew

注意 : 所有奖励 scale 会自动乘以 dt(控制频率),这样奖励值不依赖于控制频率。


训练流程

整体架构

复制代码
train.py → TaskRegistry → Environment + PPO Runner → 训练循环

训练脚本 (train.py)

python 复制代码
def train(args):
    # 1. 创建环境
    env, env_cfg = task_registry.make_env(name=args.task, args=args)
    
    # 2. 创建 PPO Runner
    ppo_runner, train_cfg = task_registry.make_alg_runner(
        env=env, 
        name=args.task, 
        args=args
    )
    
    # 3. 开始训练
    ppo_runner.learn(
        num_learning_iterations=train_cfg.runner.max_iterations,
        init_at_random_ep_len=True
    )

if __name__ == '__main__':
    args = get_args()
    train(args)

环境主循环 (step 函数)

python 复制代码
def step(self, actions):
    """单步仿真"""
    # 1. 限幅动作
    self.actions = torch.clip(actions, -clip_actions, clip_actions)
    
    # 2. 渲染
    self.render()
    
    # 3. 物理仿真(重复 decimation 次)
    for _ in range(self.cfg.control.decimation):  # 默认 4 次
        # 计算力矩 (PD 控制)
        self.torques = self._compute_torques(self.actions)
        
        # 应用力矩并仿真
        self.gym.set_dof_actuation_force_tensor(self.sim, self.torques)
        self.gym.simulate(self.sim)
        
        # 更新状态张量
        self.gym.refresh_dof_state_tensor(self.sim)
    
    # 4. 后处理
    self.post_physics_step()
    
    # 5. 限幅观测
    self.obs_buf = torch.clip(self.obs_buf, -clip_obs, clip_obs)
    
    # 6. 返回
    return self.obs_buf, self.privileged_obs_buf, self.rew_buf, \
           self.reset_buf, self.extras

PD 控制器

python 复制代码
def _compute_torques(self, actions):
    """从动作计算关节力矩"""
    # 缩放动作
    actions_scaled = actions * self.cfg.control.action_scale
    
    # PD 控制 (位置模式)
    if control_type == "P":
        torques = self.p_gains * (actions_scaled + self.default_dof_pos - self.dof_pos) \
                  - self.d_gains * self.dof_vel
    
    # PD 控制 (速度模式)
    elif control_type == "V":
        torques = self.p_gains * (actions_scaled - self.dof_vel) \
                  - self.d_gains * (self.dof_vel - self.last_dof_vel) / self.dt
    
    # 直接力矩控制
    elif control_type == "T":
        torques = actions_scaled
    
    # 限幅力矩
    return torch.clip(torques, -self.torque_limits, self.torque_limits)

PD 增益配置:

python 复制代码
class control:
    stiffness = {  # P 增益 [N·m/rad]
        'HAA': 80.0,   # 髋部外展/内收
        'HFE': 80.0,   # 髋部屈伸
        'KFE': 80.0    # 膝部屈伸
    }
    damping = {    # D 增益 [N·m·s/rad]
        'HAA': 2.0,
        'HFE': 2.0,
        'KFE': 2.0
    }

后处理流程

python 复制代码
def post_physics_step(self):
    """物理步后的处理"""
    # 1. 刷新状态
    self.gym.refresh_actor_root_state_tensor(self.sim)
    self.gym.refresh_net_contact_force_tensor(self.sim)
    
    # 2. 更新计数器
    self.episode_length_buf += 1
    self.common_step_counter += 1
    
    # 3. 计算派生量
    self.base_lin_vel = quat_rotate_inverse(self.base_quat, self.root_states[:, 7:10])
    self.base_ang_vel = quat_rotate_inverse(self.base_quat, self.root_states[:, 10:13])
    self.projected_gravity = quat_rotate_inverse(self.base_quat, self.gravity_vec)
    
    # 4. 回调(重采样指令、推机器人等)
    self._post_physics_step_callback()
    
    # 5. 检查终止条件
    self.check_termination()
    
    # 6. 计算奖励
    self.compute_reward()
    
    # 7. 重置终止的环境
    env_ids = self.reset_buf.nonzero(as_tuple=False).flatten()
    self.reset_idx(env_ids)
    
    # 8. 计算观测
    self.compute_observations()
    
    # 9. 保存历史
    self.last_actions[:] = self.actions[:]
    self.last_dof_vel[:] = self.dof_vel[:]

终止条件

python 复制代码
def check_termination(self):
    """检查是否需要重置环境"""
    # 1. 碰撞终止:关键部位接触地面
    self.reset_buf = torch.any(
        torch.norm(
            self.contact_forces[:, self.termination_contact_indices, :], 
            dim=-1
        ) > 1.0, 
        dim=1
    )
    
    # 2. 超时终止
    self.time_out_buf = self.episode_length_buf > self.max_episode_length
    
    # 3. 合并
    self.reset_buf |= self.time_out_buf

指令跟踪机制

指令的生成与更新

python 复制代码
def _resample_commands(self, env_ids):
    """随机采样速度指令"""
    # 线速度 x (前后)
    self.commands[env_ids, 0] = torch_rand_float(
        self.command_ranges["lin_vel_x"][0],  # -1.0 m/s
        self.command_ranges["lin_vel_x"][1],  # +1.0 m/s
        (len(env_ids), 1), 
        device=self.device
    ).squeeze(1)
    
    # 线速度 y (左右)
    self.commands[env_ids, 1] = torch_rand_float(
        self.command_ranges["lin_vel_y"][0],  # -1.0 m/s
        self.command_ranges["lin_vel_y"][1],  # +1.0 m/s
        (len(env_ids), 1),
        device=self.device
    ).squeeze(1)
    
    # 角速度 yaw (旋转) 或 heading (朝向)
    if self.cfg.commands.heading_command:
        # Heading 模式:指定目标朝向
        self.commands[env_ids, 3] = torch_rand_float(
            self.command_ranges["heading"][0],   # -π
            self.command_ranges["heading"][1],   # +π
            (len(env_ids), 1),
            device=self.device
        ).squeeze(1)
    else:
        # 直接角速度模式
        self.commands[env_ids, 2] = torch_rand_float(
            self.command_ranges["ang_vel_yaw"][0],  # -1 rad/s
            self.command_ranges["ang_vel_yaw"][1],  # +1 rad/s
            (len(env_ids), 1),
            device=self.device
        ).squeeze(1)
    
    # 过滤小速度指令(避免不稳定)
    self.commands[env_ids, :2] *= (
        torch.norm(self.commands[env_ids, :2], dim=1) > 0.2
    ).unsqueeze(1)

指令重采样时机

python 复制代码
def _post_physics_step_callback(self):
    """每个物理步后的回调"""
    # 定期重采样指令 (默认 10 秒)
    env_ids = (
        self.episode_length_buf % int(self.cfg.commands.resampling_time / self.dt) == 0
    ).nonzero(as_tuple=False).flatten()
    self._resample_commands(env_ids)
    
    # Heading 模式:计算角速度指令
    if self.cfg.commands.heading_command:
        forward = quat_apply(self.base_quat, self.forward_vec)
        heading = torch.atan2(forward[:, 1], forward[:, 0])
        # 将 heading 误差转换为角速度指令
        self.commands[:, 2] = torch.clip(
            0.5 * wrap_to_pi(self.commands[:, 3] - heading), 
            -1.0, 
            1.0
        )

指令课程学习 (Curriculum)

python 复制代码
def update_command_curriculum(self, env_ids):
    """根据表现逐步增加指令难度"""
    # 如果线速度跟踪奖励 > 80% 最大值
    if torch.mean(self.episode_sums["tracking_lin_vel"][env_ids]) / \
       self.max_episode_length > 0.8 * self.reward_scales["tracking_lin_vel"]:
        
        # 扩大速度指令范围
        self.command_ranges["lin_vel_x"][0] = np.clip(
            self.command_ranges["lin_vel_x"][0] - 0.5,  # 减小下限
            -self.cfg.commands.max_curriculum,           # 最低 -max_curriculum
            0.
        )
        self.command_ranges["lin_vel_x"][1] = np.clip(
            self.command_ranges["lin_vel_x"][1] + 0.5,  # 增大上限
            0.,
            self.cfg.commands.max_curriculum            # 最高 +max_curriculum
        )

训练后的 Policy 使用方式

Policy 的本质

训练完成后,Policy 是一个映射函数

复制代码
f: Observations → Actions

关键理解:

  1. ✅ Policy 需要外部提供速度指令 (commands)
  2. ✅ Policy 根据指令和当前状态输出关节动作
  3. ❌ Policy 不会自己决定往哪里走
  4. ❌ Policy 没有全局路径规划能力

推理/部署流程

python 复制代码
# 来自 play.py
def play(args):
    # 1. 加载环境和配置
    env_cfg, train_cfg = task_registry.get_cfgs(name=args.task)
    
    # 2. 创建环境
    env, _ = task_registry.make_env(name=args.task, args=args, env_cfg=env_cfg)
    obs = env.get_observations()
    
    # 3. 加载训练好的 Policy
    train_cfg.runner.resume = True
    ppo_runner, _ = task_registry.make_alg_runner(
        env=env, 
        name=args.task, 
        args=args, 
        train_cfg=train_cfg
    )
    policy = ppo_runner.get_inference_policy(device=env.device)
    
    # 4. 推理循环
    for i in range(10 * int(env.max_episode_length)):
        # 获取动作
        actions = policy(obs.detach())
        
        # 执行动作
        obs, _, rews, dones, infos = env.step(actions.detach())

Policy 调用示例

python 复制代码
# 伪代码:实际机器人部署
class RobotController:
    def __init__(self):
        self.policy = load_trained_policy("model.pt")
        self.commands = [0.0, 0.0, 0.0, 0.0]  # [vx, vy, yaw, heading]
    
    def set_velocity_command(self, vx, vy, yaw):
        """外部设置速度指令"""
        self.commands = [vx, vy, yaw, 0.0]
    
    def control_step(self):
        """单步控制"""
        # 1. 读取传感器获取观测
        obs = self.get_observations()  # [48] 或 [235]
        # obs 中已包含 self.commands
        
        # 2. Policy 推理
        actions = self.policy(obs)  # [12] 关节目标
        
        # 3. 发送到低层控制器
        self.send_joint_targets(actions)
    
    def move_forward(self):
        """示例:向前走"""
        self.set_velocity_command(vx=0.5, vy=0.0, yaw=0.0)
        for _ in range(100):
            self.control_step()
    
    def turn_left(self):
        """示例:左转"""
        self.set_velocity_command(vx=0.0, vy=0.0, yaw=0.5)
        for _ in range(50):
            self.control_step()

关键代码解析

1. 环境初始化

python 复制代码
class LeggedRobot(BaseTask):
    def __init__(self, cfg, sim_params, physics_engine, sim_device, headless):
        self.cfg = cfg
        self._parse_cfg(self.cfg)
        
        # 调用父类初始化(创建仿真)
        super().__init__(self.cfg, sim_params, physics_engine, sim_device, headless)
        
        # 设置相机
        if not self.headless:
            self.set_camera(self.cfg.viewer.pos, self.cfg.viewer.lookat)
        
        # 初始化缓冲区
        self._init_buffers()
        
        # 准备奖励函数
        self._prepare_reward_function()
        
        self.init_done = True

2. 缓冲区初始化

python 复制代码
def _init_buffers(self):
    """初始化训练所需的张量"""
    # 获取 Isaac Gym 的状态张量
    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.contact_forces = gymtorch.wrap_tensor(net_contact_forces)
    
    # 创建视图
    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]
    
    # 初始化其他缓冲区
    self.actions = torch.zeros(self.num_envs, self.num_actions, device=self.device)
    self.commands = torch.zeros(self.num_envs, 4, device=self.device)
    # ... 更多缓冲区

3. 奖励函数准备

python 复制代码
def _prepare_reward_function(self):
    """动态构建奖励函数列表"""
    # 移除零权重的奖励项,并将非零项乘以 dt
    for key in list(self.reward_scales.keys()):
        scale = self.reward_scales[key]
        if scale == 0:
            self.reward_scales.pop(key)
        else:
            self.reward_scales[key] *= self.dt
    
    # 构建函数列表
    self.reward_functions = []
    self.reward_names = []
    for name, scale in self.reward_scales.items():
        if name == "termination":
            continue
        self.reward_names.append(name)
        # 通过名称获取方法: '_reward_' + name
        name = '_reward_' + name
        self.reward_functions.append(getattr(self, name))
    
    # 初始化累积奖励
    self.episode_sums = {
        name: torch.zeros(self.num_envs, device=self.device)
        for name in self.reward_scales.keys()
    }

4. 地形课程学习

python 复制代码
def _update_terrain_curriculum(self, env_ids):
    """根据表现调整地形难度"""
    if not self.init_done:
        return
    
    # 计算机器人移动距离
    distance = torch.norm(
        self.root_states[env_ids, :2] - self.env_origins[env_ids, :2], 
        dim=1
    )
    
    # 走得远 → 升级地形
    move_up = distance > self.terrain.env_length / 2
    
    # 走得近 → 降级地形
    expected_distance = torch.norm(self.commands[env_ids, :2], dim=1) * \
                        self.max_episode_length_s * 0.5
    move_down = (distance < expected_distance) * ~move_up
    
    # 更新地形等级
    self.terrain_levels[env_ids] += 1 * move_up - 1 * move_down
    
    # 解决最高难度的机器人随机重置
    self.terrain_levels[env_ids] = torch.where(
        self.terrain_levels[env_ids] >= self.max_terrain_level,
        torch.randint_like(self.terrain_levels[env_ids], self.max_terrain_level),
        torch.clip(self.terrain_levels[env_ids], 0)
    )
    
    # 更新起始位置
    self.env_origins[env_ids] = self.terrain_origins[
        self.terrain_levels[env_ids], 
        self.terrain_types[env_ids]
    ]

5. 域随机化

python 复制代码
def _process_rigid_shape_props(self, props, env_id):
    """随机化摩擦系数"""
    if self.cfg.domain_rand.randomize_friction:
        if env_id == 0:
            # 创建摩擦系数桶
            num_buckets = 64
            bucket_ids = torch.randint(0, num_buckets, (self.num_envs, 1))
            friction_buckets = torch_rand_float(
                friction_range[0], 
                friction_range[1], 
                (num_buckets, 1), 
                device='cpu'
            )
            self.friction_coeffs = friction_buckets[bucket_ids]
        
        # 应用到每个形状
        for s in range(len(props)):
            props[s].friction = self.friction_coeffs[env_id]
    
    return props

def _push_robots(self):
    """随机推动机器人(测试鲁棒性)"""
    max_vel = self.cfg.domain_rand.max_push_vel_xy
    self.root_states[:, 7:9] = torch_rand_float(
        -max_vel, max_vel, (self.num_envs, 2), device=self.device
    )
    self.gym.set_actor_root_state_tensor(self.sim, self.root_states)

相关推荐
hg01181 小时前
豫非搭建“黄金水道” 河南首个海外港口枢纽启动试运营
大数据·人工智能·物联网
张彦峰ZYF1 小时前
AI赋能原则7解读思考:AI时代构建可组合的能力比单点专业更重要
人工智能·ai·ai赋能与落地
穿越光年1 小时前
想部署一套本地知识库问答系统,Dify or FastGPT 到底该怎么选?
人工智能·知识图谱
强化学习与机器人控制仿真1 小时前
ProtoMotions 3 入门教程(一)开源 GPU 加速人形机器人强化学习仿真训练框架
人工智能·stm32·深度学习·机器人·强化学习·人形机器人·模仿学习
小白程序员成长日记1 小时前
2025.12.02 力扣每日一题
数据结构·算法·leetcode
像风没有归宿a1 小时前
AI+教育:个性化学习与教师角色的未来
人工智能
badfl1 小时前
OpenAI文本嵌入模型text-embedding-3是什么?
人工智能·机器学习·ai
永远都不秃头的程序员(互关)1 小时前
在vscodeC语言多文件编译实战指南
c语言·数据结构·算法
点云SLAM2 小时前
Discrepancy 英文单词学习
人工智能·学习·英文单词学习·雅思备考·discrepancy·不一致、不协调·矛盾