无视复杂困难场景--基于混合专家模型的四足机器人鲁棒运动控制全开源
1. 四足机器人为什么需要强化学习
四足机器人的运动控制是机器人学中一个经典而困难的问题。传统方法依赖精确的动力学建模和轨迹优化,需要工程师手动设计步态模式、落足点规划和全身动力学控制器。这类方法在平坦地面上表现尚可,但面对楼梯、碎石、斜坡、雪地等非结构化地形时,模型误差会急剧放大,控制器的鲁棒性难以保证。
近年来,强化学习(Reinforcement Learning, RL)为这一问题提供了一条截然不同的路径。其核心思想是:在物理仿真器中构建大规模并行环境,让机器人通过数十亿次试错自主学习运动策略,而非依赖人工设计的控制规则。Isaac Gym 等 GPU 加速仿真器的出现,使得同时运行数千个仿真环境成为可能,极大地加速了策略的收敛速度。Unitree Go2 作为当前学术界广泛使用的四足平台,其紧凑的体型、丰富的传感器接口和相对低廉的成本,使其成为验证 RL 运动策略的理想载体。
然而,仿真训练出的策略要在真实机器人上稳定运行,必须跨越一道关键障碍——Sim-to-Real 鸿沟。仿真器中的物理参数(摩擦系数、电机响应延迟、地形几何精度等)与真实世界存在系统性偏差,一个在仿真中表现完美的策略,部署到真机后可能立刻摔倒。这篇工作正是围绕这一核心挑战展开的。

2. 核心问题:Sim-to-Real 鸿沟与策略选择困境
Sim-to-Real 迁移的难点可以从两个层面理解。第一个层面是物理参数的不确定性。真实世界中,地面摩擦系数随材质和湿度变化,电机的力矩输出受温度和老化影响,机器人的质心位置因负载不同而偏移。如果策略只在单一参数设定下训练,它学到的是一个过于"窄"的控制规则,稍有偏差就会失效。
第二个层面更为棘手:如何在不频繁进行真机实验的前提下,判断一个策略是否适合部署。真机实验成本高昂——不仅耗时,还有损坏硬件的风险。研究者通常训练多个候选策略,但缺乏可靠的离线评估手段来预测哪个策略在真机上表现最好。传统做法是凭经验或在仿真中观察行为来"拍脑袋"选择,这显然不够科学。
本文提出的框架正是针对这两个层面给出了系统性的解决方案:用混合专家(Mixture of Experts, MoE)策略解决第一个问题,用 RoboGauge 评估套件解决第二个问题。两者形成闭环——MoE 策略提供多个候选方案,RoboGauge 通过 Sim-to-Sim 测试量化每个方案的迁移可靠性,从而在不接触真机的情况下完成策略筛选。

3. 整体框架概览:从训练到部署的闭环
该工作的技术框架可以拆解为三个相互衔接的子系统,分别对应三个开源代码仓库:
第一个子系统是策略训练(go2_rl_gym)。它基于 Isaac Gym 仿真器,使用 PPO(Proximal Policy Optimization)算法训练 MoE 运动策略。训练过程采用并行师生学习(Concurrent Teacher-Student, CTS)架构:教师网络可以访问地形高度图、接触力等特权信息,学生网络仅依赖本体感知(关节角度、角速度、IMU 数据),通过隐空间蒸馏将教师的地形理解能力迁移给学生。训练环境包含 8192 个并行仿真实例,覆盖斜坡、楼梯、障碍物、波浪地形等 9 种地形类型,并施加大范围的域随机化(摩擦系数、质量偏移、电机延迟等)。
第二个子系统是策略评估(RoboGauge)。它将 Isaac Gym 中训练好的策略迁移到 MuJoCo 仿真器中进行 Sim-to-Sim 测试。之所以选择跨仿真器评估,是因为两个仿真器的物理引擎实现不同(Isaac Gym 基于 PhysX,MuJoCo 基于自研求解器),策略在不同物理引擎间的表现差异,可以部分反映其在真实世界中的鲁棒性。RoboGauge 定义了 6 个量化指标,覆盖速度跟踪精度、关节极限违规、能耗、姿态稳定性和力矩平滑度,在 5 种地形、10 个难度等级上进行系统性测试。
第三个子系统是真机部署(unitree_cpp_deploy)。它提供了一套 C++ 推理框架,将训练导出的 ONNX 模型加载到 Unitree Go2 机载的 Orin NX 计算平台上,以 50Hz 的频率执行策略推理,并通过 DDS 协议将关节位置指令下发给电机控制器。整个部署流水线包含有限状态机管理、线程安全的观测-推理-动作流水线、以及完善的安全保护机制。
三个子系统的数据流向是单向的:Isaac Gym 训练产出 ONNX 模型,RoboGauge 对模型进行离线评估并输出量化分数,得分最高的模型被部署到真机。但从方法论角度看,RoboGauge 的评估结果可以反馈指导训练超参数的调整,形成一个迭代优化的闭环。

4. MoE 策略架构:让不同专家处理不同地形
混合专家模型(Mixture of Experts)的核心直觉非常朴素:与其训练一个"万能"网络来处理所有地形,不如训练多个"专家"网络,每个专家擅长处理特定类型的地形或运动模式,再由一个门控网络根据当前状态动态分配各专家的权重。这种分而治之的策略在自然语言处理领域(如 Switch Transformer)已经被证明有效,本文将其引入四足运动控制。
在具体实现中,MoE 模块被嵌入到学生编码器中,用于从本体感知的历史观测序列中提取地形隐表示。整个网络架构可以分为四个组件:教师编码器、学生 MoE 编码器、策略网络(Actor)和价值网络(Critic)。
教师编码器是一个标准的多层感知机(MLP),输入维度为 263(包含关节状态、IMU 数据、地形高度图、接触力等特权信息),通过 [263, 512, 256, 32] 的网络结构映射到 32 维隐空间,最后经过 L2 归一化。教师编码器只在训练阶段使用,部署时不需要。
学生 MoE 编码器是本文的核心创新。它的输入是最近若干帧本体感知观测的拼接(例如 6 帧 x 45 维 = 270 维),输出同样是 32 维隐向量。其内部结构如下:
# 文件:rsl_rl/modules/utils.py
class Experts(nn.Module):
"""
多专家网络:共享骨干 + 分组卷积实现独立专家头。
每个专家负责从输入中提取一种特定的地形/运动模式表示,
最终由外部的门控网络决定各专家输出的混合权重。
"""
def __init__(self,
expert_num, # 专家数量,默认8。即有8个并行的"子网络",
# 每个专家可以理解为擅长处理一种地形模式
input_dim, # 输入维度。对于学生编码器,等于
# num_obs * history_length(如 45*6=270)
backbone_hidden_dims, # 共享骨干的隐藏层维度列表,如 [512, 256]。
# 所有专家共用这些层来提取通用特征
expert_hidden_dim, # 每个专家独立头的隐藏维度,如 256。
# 骨干输出维度 = expert_num * expert_hidden_dim
output_dim, # 每个专家的输出维度,即隐空间维度,默认32
activation='elu', # 激活函数类型,ELU 比 ReLU 更平滑,
# 适合连续控制任务
):
super().__init__()
self.expert_num = expert_num
self.output_dim = output_dim
# 共享骨干网络:所有专家共用前几层特征提取。
# 网络结构示例:[270, 512, 256, 8*256=2048]
# 最后一层输出维度 = expert_num * expert_hidden_dim,
# 相当于为每个专家预留了 expert_hidden_dim 维的特征空间。
# last_activation=True 表示最后一层也加激活函数,
# 因为后面还有 Conv1d 层继续处理。
self.backbone = MLP(
[input_dim, *backbone_hidden_dims, expert_num * expert_hidden_dim],
activation, last_activation=True
)
# 分组卷积(Grouped Convolution):每个专家独立的输出头。
# 这是实现"多专家独立输出"的关键技巧:
# - in_channels = expert_num * expert_hidden_dim(如 8*256=2048)
# - out_channels = expert_num * output_dim(如 8*32=256)
# - groups = expert_num(如 8)
# groups 参数将输入通道分成 expert_num 组,每组独立做卷积,
# 等价于 8 个独立的线性层,但用一次 Conv1d 调用高效实现。
# kernel_size=1 表示这是一个逐点卷积(等价于全连接层)。
self.experts = nn.Conv1d(
in_channels=expert_num * expert_hidden_dim,
out_channels=expert_num * output_dim,
kernel_size=1,
groups=expert_num,
)
def forward(self, x):
# x 形状:(B, input_dim),B 是 batch size(并行环境数)
# backbone 输出:(B, expert_num * expert_hidden_dim)
# unsqueeze(-1) 添加一个维度给 Conv1d:(B, channels, 1)
shared_features = self.backbone(x).unsqueeze(-1)
# Conv1d 分组卷积后:(B, expert_num * output_dim, 1)
# squeeze(-1) 去掉最后一维:(B, expert_num * output_dim)
expert_outs = self.experts(shared_features).squeeze(-1)
# reshape 成 (B, expert_num, output_dim),
# 即每个样本有 expert_num 个专家,每个专家输出 output_dim 维向量
expert_outs = expert_outs.reshape(-1, self.expert_num, self.output_dim)
return expert_outs
这段代码有一个值得注意的设计:专家网络并非完全独立的 N 个 MLP,而是采用"共享骨干 + 分组卷积"的结构。所有专家共享前几层的特征提取(backbone),只在最后一层通过 nn.Conv1d 的 groups 参数实现独立输出。这种设计在保持专家多样性的同时,大幅减少了参数量,避免了完全独立专家带来的训练不稳定问题。
门控网络负责根据输入动态计算每个专家的权重:
class MoE(nn.Module):
"""
混合专家模块:专家网络 + 门控网络。
门控网络根据输入状态动态决定每个专家的贡献权重,
最终输出是所有专家输出的加权和。这种"软路由"机制
允许多个专家同时参与决策,而非硬性选择单一专家。
"""
def __init__(self, expert_num, input_dim, hidden_dims,
output_dim, activation='elu'):
# expert_num: 专家数量,默认8。更多专家意味着更细粒度的
# 地形/运动模式划分,但也增加参数量和训练难度
# input_dim: 输入维度,与 Experts 相同(如 270)
# hidden_dims: 隐藏层维度列表,如 [512, 256, 256]。
# 前 N-1 层用于共享骨干和门控网络,
# 最后一层仅用于专家独立头
# output_dim: 输出维度,即隐空间维度(如 32)
# activation: 激活函数,默认 'elu'
super().__init__()
self.experts = Experts(
expert_num=expert_num, input_dim=input_dim,
backbone_hidden_dims=hidden_dims[:-1], # [512, 256],共享骨干层
expert_hidden_dim=hidden_dims[-1], # 256,每个专家独立头的维度
output_dim=output_dim, activation=activation,
)
# 门控网络:一个轻量 MLP + Softmax,输出 expert_num 维的概率分布。
# 网络结构:[input_dim, 512, 256, expert_num] → Softmax
# 输出的每个值表示对应专家的"激活程度",所有值之和为 1。
# 例如输出 [0.3, 0.1, 0.05, 0.4, 0.02, 0.03, 0.05, 0.05]
# 表示第4个专家(权重0.4)和第1个专家(权重0.3)主导当前决策。
self.gating_network = nn.Sequential(
MLP([input_dim, *hidden_dims[:-1], expert_num], activation),
nn.Softmax(dim=-1) # 归一化为概率分布,确保权重和为 1
)
def forward(self, x):
# x 形状:(B, input_dim),B 是 batch size(并行环境数,如 8192)
weights = self.gating_network(x) # (B, expert_num),每个样本的专家权重
expert_outs = self.experts(x) # (B, expert_num, output_dim),每个专家的输出
# 加权求和:weights.unsqueeze(-1) 将 (B, expert_num) 扩展为 (B, expert_num, 1),
# 与 expert_outs (B, expert_num, output_dim) 逐元素相乘后沿 expert 维度求和,
# 得到最终的混合输出 (B, output_dim)。
# 这等价于:output[b] = Σ_i weights[b,i] * expert_outs[b,i,:]
output = torch.sum(
weights.unsqueeze(-1) * expert_outs, dim=1
) # (B, output_dim)
return output, weights # 同时返回权重,用于计算负载均衡损失
门控网络的输出经过 Softmax 归一化,确保所有专家的权重之和为 1。最终输出是所有专家输出的加权和。在训练过程中,还会引入负载均衡损失(load balancing loss),防止门控网络退化为只激活少数几个专家的模式。
默认配置使用 8 个专家,隐藏层维度为 [512, 256, 256],隐空间维度为 32。输出的隐向量经过 L2 归一化后,与当前帧的观测拼接,送入策略网络生成 12 维的关节位置指令。

5. 并行师生学习(CTS):特权信息的高效蒸馏
仅有 MoE 结构还不够。部署时机器人只能获取本体感知信息(关节编码器和 IMU),无法直接感知脚下的地形类型和高度。但训练时仿真器可以提供完整的地形高度图、接触力分布等特权信息。如何将这些特权信息中蕴含的地形理解能力,迁移到只依赖本体感知的学生网络中?这就是并行师生学习(Concurrent Teacher-Student, CTS)要解决的问题。
CTS 的核心思想源自论文 “Concurrent Training of a Control Policy and a State Estimator for Dynamic and Robust Legged Locomotion”(arXiv:2405.10830)。与传统的两阶段蒸馏不同,CTS 让教师和学生在同一个训练循环中并行学习。具体做法是将 8192 个并行环境按比例划分:75% 的环境分配给教师(使用特权观测),25% 分配给学生(仅使用历史本体感知)。两者共享同一个策略网络(Actor)和价值网络(Critic),只是编码器不同。
以下是 ActorCriticMoECTS 网络的完整前向推理逻辑:
# 文件:rsl_rl/modules/actor_critic_moe_cts.py
class ActorCriticMoECTS(nn.Module):
"""
并行师生学习(CTS)的核心网络。
包含四个子模块:教师编码器、学生 MoE 编码器、共享策略网络、共享价值网络。
训练时教师和学生并行运行,部署时仅保留学生编码器 + 策略网络。
"""
def __init__(self, num_obs, # 单帧本体感知观测维度,默认45。
# 包含:角速度(3) + 重力投影(3) + 速度指令(3)
# + 关节位置(12) + 关节速度(12) + 上帧动作(12)
num_critic_obs, # 特权观测维度,默认263。
# 包含 num_obs 的全部内容,额外加上:
# 机身线速度(3) + 地形高度图(187) + 足端接触力(12)
# + 摩擦系数(12) + 预留(4)
num_actions, # 动作维度,固定12。对应4条腿×3个关节
# (髋关节横摆、大腿前后摆、小腿屈伸)的
# 期望角度偏移量(相对于默认站立姿态)
num_envs, # 并行仿真环境数量,默认8192。
# 其中75%分配给教师,25%分配给学生
history_length, # 历史帧数,默认6。学生编码器的输入是
# 最近6帧观测的拼接,用于从时序模式中
# 推断地形信息(如踩到台阶时的力矩突变)
expert_num=8, # MoE 专家数量,默认8
latent_dim=32, # 隐空间维度,默认32。教师和学生编码器
# 都输出32维向量,蒸馏损失在此空间计算
...):
# 教师编码器:标准 MLP,将263维特权观测压缩到32维隐空间。
# 网络结构 [263, 512, 256, 32],最后经 L2 归一化使隐向量落在单位超球面上。
# 仅在训练阶段使用,部署时完全丢弃。
self.teacher_encoder = nn.Sequential(
MLP([num_critic_obs, 512, 256, latent_dim]), # 263 -> 512 -> 256 -> 32
L2Norm() # L2 归一化:||latent||_2 = 1,稳定蒸馏训练
)
# 学生 MoE 编码器:将历史本体感知(6帧×45维=270维)映射到32维隐空间。
# 内部包含 MoE 模块(8个专家 + 门控网络),是本文的核心创新。
# 部署时这是唯一的编码器,必须仅从关节数据中"推断"地形信息。
self.student_moe_encoder = StudentMoEEncoder(
expert_num=expert_num, # 8个专家
input_dim=num_obs * history_length, # 45 × 6 = 270
hidden_dims=[512, 256, 256], # 骨干 [512, 256] + 专家头 256
output_dim=latent_dim, # 32维输出
)
# 共享策略网络(Actor):输入 = 32维隐向量 + 45维当前观测 = 77维,
# 输出 = 12维关节角度偏移。教师和学生共享同一个 Actor,
# 确保两者学到的隐表示语义一致。
# 网络结构 [77, 512, 256, 128, 12]
self.actor = MLP([latent_dim + num_obs, 512, 256, 128, num_actions])
# 共享价值网络(Critic):输入 = 32维隐向量 + 263维特权观测 = 295维,
# 输出 = 1维状态价值估计。Critic 始终使用特权观测,
# 因为价值函数只在训练时使用,部署时不需要。
# 网络结构 [295, 512, 256, 128, 1]
self.critic = MLP([latent_dim + num_critic_obs, 512, 256, 128, 1])
def act(self, obs, privileged_obs, history, is_teacher):
"""
训练时的动作采样。
参数:
obs: 当前帧本体感知观测 (B, 45)
privileged_obs: 特权观测 (B, 263),仅教师使用
history: 历史观测序列 (B, history_length*num_obs),仅学生使用
is_teacher: 布尔值,True=使用教师编码器,False=使用学生编码器
"""
if is_teacher:
# 教师路径:直接从特权观测(含地形高度图、接触力等)编码
latent = self.teacher_encoder(privileged_obs)
else:
# 学生路径:从历史本体感知中推断地形隐表示。
# torch.no_grad() 表示学生编码器在此处不计算梯度,
# 其梯度通过蒸馏损失单独更新,避免 PPO 梯度干扰编码器训练。
with torch.no_grad():
latent, _ = self.student_moe_encoder(history)
# 将32维隐向量与45维当前观测拼接为77维,送入共享 Actor
x = torch.cat([latent, obs], dim=1)
# 构建高斯分布 N(mean, std),mean 由 Actor 输出,std 是可学习参数
self.update_distribution(x)
# 从分布中采样动作(训练时需要随机性以探索)
return self.distribution.sample()
def act_inference(self, obs):
"""
部署时的确定性推理(仅使用学生编码器)。
与训练时的区别:
1. 不使用教师编码器(真机无特权信息)
2. 不采样,直接输出均值(消除随机性,保证行为稳定)
3. 自动维护历史缓冲区(滑动窗口)
参数:
obs: 当前帧本体感知观测 (B, 45)
"""
# 滑动窗口更新历史:丢弃最旧一帧,追加当前帧。
# history 形状从 (B, 6, 45) 的窗口中去掉第0帧,
# 在末尾追加当前 obs,保持窗口长度为 6。
self.history = torch.cat(
[self.history[:, 1:], obs.unsqueeze(1)], dim=1
)
# flatten(1) 将 (B, 6, 45) 展平为 (B, 270),送入学生 MoE 编码器
latent, _ = self.student_moe_encoder(self.history.flatten(1))
x = torch.cat([latent, obs], dim=1) # (B, 77)
return self.actor(x) # 返回均值动作,不采样,确保部署行为确定性
训练过程中的损失函数由四部分组成。第一部分是标准的 PPO 损失(策略梯度的 clipped surrogate 目标 + 价值函数损失 + 熵正则化),这是强化学习的基础优化目标。第二部分是隐空间蒸馏损失,要求学生编码器的输出尽可能接近教师编码器的输出,通常使用 MSE 或余弦相似度。第三部分是 MoE 负载均衡损失,防止门控网络将所有权重集中在少数专家上,确保每个专家都能被充分利用。第四部分是可选的行为克隆损失,在训练早期用教师的动作分布监督学生。
这种并行训练的优势在于:教师和学生始终在同一个策略水平上学习,避免了两阶段蒸馏中"教师已经收敛但学生还在追赶"的滞后问题。同时,学生在训练过程中就直接参与环境交互,其产生的轨迹数据也被用于 PPO 更新,进一步提升了样本效率。

6. 训练环境:地形课程与域随机化
策略的泛化能力很大程度上取决于训练环境的多样性。go2_rl_gym 在 Isaac Gym 中构建了一套精心设计的地形课程系统,包含 9 种地形类型,每种地形有多个难度等级。8192 个并行环境被分配到不同地形上,机器人在训练过程中根据自身表现自动升降难度——表现好的个体被提升到更难的地形,表现差的则回退到简单地形。这种课程学习机制避免了一开始就在高难度地形上训练导致的策略崩溃。
以下是地形配置的核心参数:
# 文件:legged_gym/envs/go2/go2_config.py
# 9种地形类型及其在训练中的占比。
# 8192 个并行环境按以下比例分配到不同地形上,
# 每种地形内部还有多个难度等级(由课程学习机制自动调节)。
terrain_types = [
'wave', # 波浪地形:地面呈正弦波起伏,测试机器人对不规则地面的适应性
'slope', # 斜坡:倾斜平面,测试上下坡能力和重心调整
'rough_slope', # 粗糙斜坡:斜坡 + 随机凸起,比普通斜坡更具挑战性
'stairs_up', # 上楼梯:离散台阶向上,需要精确的抬腿高度控制
'stairs_down', # 下楼梯:离散台阶向下,需要稳定的着地缓冲
'obstacles', # 障碍物:随机分布的方块/圆柱体,需要跨越或绕行
'stepping_stones', # 踏脚石:离散的小平台,需要精确落足(当前未启用)
'gap', # 间隙:地面上的沟壑,需要跨越(当前未启用)
'flat' # 平地:无障碍平面,作为基线和恢复训练用
]
# 各地形在训练中的占比,所有值之和为 1.0。
# 比例设计逻辑:重点训练真实部署中最常遇到的挑战性地形。
terrain_proportions = [0.05, 0.20, 0.05, 0.25, 0.10, 0.20, 0.0, 0.0, 0.15]
# 分析:楼梯(上25%+下10%=35%) + 障碍物(20%) + 斜坡(20%) 占主要比重,
# 波浪(5%) + 粗糙斜坡(5%) 作为补充,平地(15%) 保证基础稳定性,
# 踏脚石和间隙(各0%) 在当前配置中未启用。
从比例分配可以看出,训练重点放在楼梯(上下合计 35%)、障碍物(20%)和斜坡(20%)上,这些正是真实部署中最常遇到的挑战性地形。波浪地形和粗糙斜坡各占 5% 作为补充,踏脚石和间隙地形在当前配置中未启用。平地占 15%,确保策略在简单场景下也能保持稳定。
域随机化是弥合 Sim-to-Real 鸿沟的另一个关键手段。其核心思想是:如果策略在大范围参数变化下都能正常工作,那么真实世界的参数大概率落在这个范围内。go2_rl_gym 对以下物理参数施加了随机化:
# 域随机化参数范围(Domain Randomization)
# 核心思想:如果策略在大范围参数变化下都能正常工作,
# 那么真实世界的参数大概率落在这个范围内,从而实现 Sim-to-Real 迁移。
# 每个参数在每次环境重置时从指定范围内均匀采样。
# === 地面物理属性随机化 ===
friction_range = [0.0, 2.0] # 地面摩擦系数。0.0=完全光滑(冰面),
# 2.0=极高摩擦(橡胶地面)。
# 真实世界中瓷砖约0.3-0.5,水泥约0.6-0.8,
# 草地约1.0-1.5。范围覆盖了几乎所有常见地面。
base_mass_offset = [-1.0, 1.0] # 机身质量偏移 (kg)。Go2 标称质量约15kg,
# ±1kg 模拟负载变化(如背包、传感器安装)
link_mass_scale = [0.9, 1.1] # 连杆质量缩放因子。0.9=轻10%,1.1=重10%,
# 模拟制造公差和零件磨损
base_com_offset = [-0.03, 0.03] # 质心位置偏移 (m),xyz 三个方向各±3cm。
# 模拟不对称负载导致的质心偏移
restitution_range = [0.0, 0.5] # 恢复系数(弹性碰撞程度)。0=完全非弹性,
# 0.5=中等弹性。影响足端着地时的反弹行为
# === 电机参数随机化 ===
motor_zero_offset = [-0.035, 0.035] # 关节零位偏移 (rad,约±2°)。
# 模拟电机编码器的零位标定误差,
# 真机上每个关节的"零位"都有微小偏差
motor_strength = [0.8, 1.2] # 电机力矩缩放因子。0.8=输出力矩打8折,
# 1.2=输出力矩增加20%。模拟电机老化、
# 温度变化和个体差异对力矩输出的影响
action_delay = [0, 20] # 动作执行延迟 (ms)。从策略输出指令到
# 电机实际执行之间的时间差。真实电机的
# 通信+响应延迟通常在5-15ms,训练时
# 扩大到0-20ms以增强鲁棒性
# === 外部扰动(模拟意外推力) ===
push_interval = 4.0 # 推力施加间隔 (s)。每隔4秒随机施加一次
# 外力,模拟被人推、被物体撞击等场景
max_push_vel_xy = 0.4 # 最大推力等效线速度 (m/s)。推力大小以
# 等效速度变化量表示,0.4m/s 约等于
# 一个中等力度的侧推
max_push_ang_vel = 0.6 # 最大推力等效角速度 (rad/s)。模拟
# 旋转方向的扰动,如被斜向推一下
其中动作延迟的随机化尤为重要。真实电机从接收指令到执行存在不可忽略的延迟,且延迟量随电机负载和温度变化。训练时引入 0-20ms 的随机延迟,迫使策略学会对延迟具有鲁棒性的控制行为,而非依赖精确的即时响应。

7. 奖励函数设计:引导策略学会"好的"行走方式
强化学习的核心驱动力是奖励函数。一个设计不当的奖励函数会导致策略学到各种"投机取巧"的行为——比如用不自然的姿态滑行来最大化速度跟踪奖励,或者通过剧烈抖动关节来获取额外的探索奖励。go2_rl_gym 的奖励函数由多个子项加权组成,每个子项对应一个期望的行为特征或需要惩罚的不良行为。
以下是各奖励子项及其权重:
# 文件:legged_gym/envs/base/legged_robot_config.py
class scales:
"""
奖励函数各子项的权重。正值=鼓励该行为,负值=惩罚该行为。
总奖励 = Σ (weight_i × reward_i),每个 reward_i 的计算方式不同。
权重的绝对值反映了该行为在整体优化目标中的重要程度。
"""
# === 正向奖励:鼓励期望行为 ===
tracking_lin_vel = 1.0 # 线速度跟踪(主奖励,权重最高)。
# 计算方式:exp(-||v_actual - v_cmd||² / σ²),
# 其中 σ=0.25。当实际速度完美匹配指令时=1.0,
# 偏差越大衰减越快。这是策略优化的首要目标。
tracking_ang_vel = 0.5 # 角速度(转向)跟踪。权重为线速度的一半,
# 因为转向精度的优先级低于前进速度。
# 计算方式同上,σ=0.25。
feet_air_time = 1.0 # 足端腾空时间奖励。鼓励机器人抬腿迈步,
# 而非拖着脚滑行。每只脚离地时间越接近
# 目标值(约0.5秒),奖励越高。
# 没有这个奖励,策略会学到"拖行"步态。
# === 负向惩罚:抑制不良行为 ===
lin_vel_z = -2.0 # 惩罚垂直方向(z轴)速度波动。权重较大(-2.0),
# 因为机身上下颠簸是最明显的不稳定信号。
# 理想状态下机身高度应保持恒定。
ang_vel_xy = -0.05 # 惩罚横滚(roll)和俯仰(pitch)角速度。
# 权重较小,允许一定程度的机身晃动,
# 但过度晃动会被惩罚。
collision = -1.0 # 惩罚非足端部位的碰撞(如膝盖、机身撞地)。
# 权重-1.0,与主奖励同量级,表示碰撞是
# 严重的不良行为。
action_rate = -0.01 # 惩罚相邻两帧动作的变化量 ||a_t - a_{t-1}||²。
# 权重很小,轻微约束动作平滑性,
# 避免关节指令剧烈抖动。
torques = -0.00001 # 惩罚关节力矩的平方和 Στ²。权重极小,
# 仅作为正则化项,鼓励策略用更小的力矩
# 完成运动,降低能耗和电机磨损。
dof_acc = -2.5e-7 # 惩罚关节加速度的平方和。权重极小,
# 抑制关节角速度的突变,使运动更平滑。
# 与 action_rate 互补:action_rate 约束指令层面,
# dof_acc 约束物理执行层面。
速度跟踪奖励是整个奖励体系的核心。它采用高斯核函数的形式,当实际速度与指令速度的误差为零时奖励最大,误差越大奖励衰减越快:
r e w a r d t r a c k i n g = e x p ( − e r r o r 2 / ( 2 ∗ s i g m a 2 ) ) reward_tracking = exp(-error² / (2 * sigma²)) rewardtracking=exp(−error2/(2∗sigma2))
其中 sigma = 0.25 控制了奖励的"宽容度"。较小的 sigma 意味着策略必须非常精确地跟踪指令速度才能获得高奖励,较大的 sigma 则允许一定的跟踪误差。0.25 是一个经验值,在跟踪精度和训练稳定性之间取得了平衡。
值得注意的是配置中启用了 only_positive_rewards = True,即所有负值奖励会被截断为零。这意味着惩罚项不会直接减少总奖励,而是通过降低正向奖励的相对占比来间接发挥作用。这种设计避免了训练早期因大量惩罚导致的策略崩溃,让机器人有更多"试错空间"。
足端腾空时间奖励(feet_air_time)是另一个关键设计。没有这个奖励项,策略倾向于学到一种"拖行"步态——脚不离地,靠滑动前进。这种步态在仿真中可能有效,但在真实世界中会因摩擦力不一致而失败。通过奖励足端的腾空时间,策略被迫学会抬腿迈步的自然步态。
奖励函数还支持课程化调整。某些奖励项的权重可以随训练进度线性变化,例如在训练初期降低对垂直速度的惩罚(让机器人先学会走路),后期再逐步加大惩罚(让步态更平稳):
# 奖励课程示例(Reward Curriculum)
# 某些奖励项的权重随训练迭代次数线性变化,
# 实现"先学会走,再学会走好"的渐进式训练策略。
curriculum_rewards = {
'reward_name': 'lin_vel_z', # 要调整的奖励项名称。
# lin_vel_z 是垂直速度惩罚,
# 用于抑制机身上下颠簸。
'start_iter': 0, # 课程开始的训练迭代次数。
# 0 表示从训练一开始就生效。
'end_iter': 1500, # 课程结束的训练迭代次数。
# 在第 0~1500 次迭代之间线性过渡。
'start_value': 1.0, # 初始缩放因子。1.0 表示惩罚权重
# 保持原值(-2.0 × 1.0 = -2.0)。
# 训练初期允许较大的垂直波动,
# 让机器人先学会基本的前进运动。
'end_value': 0.0 # 最终缩放因子。0.0 表示惩罚权重
# 降为零(-2.0 × 0.0 = 0.0)。
# 注意:这里 end_value < start_value,
# 实际上是逐渐放松惩罚,而非加重。
# 具体策略取决于训练需求。
}
# 线性插值公式:
# progress = clip((current_iter - start_iter) / (end_iter - start_iter), 0, 1)
# current_scale = (1 - progress) * start_value + progress * end_value
# 最终权重 = 原始权重 × current_scale
8. RoboGauge:用 Sim-to-Sim 预测 Sim-to-Real
RoboGauge 是本文的另一个核心贡献。它的设计动机很直接:既然我们无法廉价地在真机上测试策略,那能否通过在另一个仿真器中测试来间接预测真机表现?答案是肯定的,前提是两个仿真器的物理引擎足够不同。Isaac Gym 使用 NVIDIA PhysX 作为物理后端,MuJoCo 使用自研的凸优化求解器,两者在接触力计算、摩擦模型、积分方法等方面存在显著差异。如果一个策略能在两个物理引擎间保持一致的表现,说明它学到的控制行为不依赖于特定仿真器的"漏洞",更有可能在真实世界中也能工作。
RoboGauge 定义了 6 个评估指标,通过加权几何平均计算综合质量分数:
# 文件:robogauge/tasks/gauge/base_gauge_config.py
# 综合质量分数的各指标权重,用于加权几何平均。
# 几何平均公式:Q = Π(score_i ^ w_i) ^ (1/Σw_i)
# 相比算术平均,几何平均对"短板"更敏感——
# 任何一个指标极差都会显著拉低总分,避免"偏科"策略获得高分。
QUALITY_WEIGHTS = {
'lin_vel_err': 2, # 线速度跟踪误差(权重2,最高优先级)。
# 衡量机器人实际前进/侧移速度与指令的偏差。
# 权重为2表示该指标在总分中的影响力是其他指标的2倍,
# 因为精确的速度跟踪是运动控制的首要目标。
'ang_vel_err': 2, # 角速度(转向)跟踪误差(权重2,最高优先级)。
# 衡量机器人实际转向角速度与指令的偏差。
# 与线速度同等重要,因为转向失控同样危险。
'dof_limits': 1, # 关节极限违规程度(权重1)。
# 衡量关节角度是否接近或超过物理极限。
# 频繁触及极限会损坏电机和结构件。
'dof_power': 1, # 电机能耗(权重1)。
# 衡量 |力矩 × 角速度| 的均方根值,
# 以 100W 为基准归一化。能耗过高意味着
# 策略效率低下,电池续航会大幅缩短。
'orientation_stability': 1, # 姿态稳定性(权重1)。
# 衡量机身横滚和俯仰角的波动幅度。
# 机身晃动越小,运动越稳定。
'torque_smoothness': 1, # 力矩平滑度(权重1)。
# 衡量相邻时刻力矩变化量的均方根值,
# 以 30Nm 为基准归一化。力矩抖动会
# 加速齿轮磨损并产生噪音。
}
速度跟踪误差的权重最高(各为 2),因为精确的速度跟踪是运动控制的首要目标。其余四个指标权重为 1,分别衡量策略的安全性(关节是否接近极限)、效率(能耗是否合理)、稳定性(机身是否晃动)和平滑性(力矩输出是否抖动)。
每个指标的计算方式都经过归一化处理,输出范围在 [0, 1] 之间,1 表示最优。以线速度跟踪误差为例:
# 各指标的计算方式(伪代码),所有指标输出范围 [0, 1],1=最优。
# 线速度跟踪误差指标:衡量实际速度与指令速度的偏差
# actual_lin_vel: 机器人实际线速度 (m/s),从仿真器读取
# target_lin_vel: 用户指令线速度 (m/s),由速度指令生成器提供
# norm_vel: 归一化基准速度,防止除零并统一量纲
vel_err = ||actual_lin_vel - target_lin_vel|| / norm_vel
score = 1 - vel_err # 误差越小,分数越接近 1.0
# 关节极限违规指标:衡量关节角度是否接近物理极限
# joint_pos: 当前关节角度 (rad)
# soft_limit: 软极限 = 硬极限 × 0.9(留10%安全裕度)
# dof_range: 关节可动范围(硬极限上界 - 硬极限下界)
violation = max(0, |joint_pos| - soft_limit) / dof_range
score = 1 - sqrt(mean(violation²)) # RMS 归一化,对大违规更敏感
# 电机能耗指标:衡量运动的能量效率
# torque: 关节力矩 (Nm),velocity: 关节角速度 (rad/s)
# 功率 = |力矩 × 角速度|,物理意义是电机瞬时输出功率
# scaling_factor: 100W,作为"合理能耗"的基准值
power = |torque * velocity|
score = 1 - rms(power) / scaling_factor # 能耗越低,分数越高
RoboGauge 的评估流水线分为四个层级,从简单到复杂依次为:单次评估(BasePipeline)、多种子并行评估(MultiPipeline)、难度等级搜索(LevelPipeline)和全面压力测试(StressPipeline)。
…详情请参照古月居
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)