在前面的两篇笔记中,我们探讨了如何通过 RNN 赋予机器人应对部分可观测环境的"记忆",以及如何通过繁复的蒸馏学习把上帝视角的特权信息传递给受限的实机模型。
今天,作为本系列训练范式探索的最后一篇,我们将介绍一种在实现难度和训练收益上具有极高性价比,且已被业界广泛采用的"白嫖"技巧------非对称演员评论家(Asymmetric Actor-Critic)架构,简称AsAC。
在进入硬核配置之前,先来复习一下强化学习中最经典的基础知识。
一、 基础扫盲:Actor-Critic 与 PPO 的双人转
要理解非对称,首先得理解什么是"对称"的 Actor-Critic。
现代深度强化学习(尤其是我们在 IsaacLab 中默认使用的 PPO,Proximal Policy Optimization)通常基于 Actor-Critic(演员-评论家) 架构。你可以把这个系统想象成一个正在拍戏的剧组:
- Actor(演员/策略网络) :负责"表演"。它观察当前的环境状态,然后决定下一步该做什么动作(比如各个关节的输出扭矩或目标角度)。它的目标是最大化未来的总奖励。
- Critic(评论家/价值网络) :负责"打分"。它同样观察当前的环境状态,然后给出一个预测分数,用来评估"当前这个状态有多好"(即 Value 函数)。它的目标是尽可能准确地预测未来的总奖励。
在传统的 PPO 训练中,Actor 和 Critic 就像是坐在一起的搭档。环境给出一套观测数据(Observations),比如各关节的角度、IMU 数据、激光雷达扫描等,原封不动地同时塞给 Actor 和 Critic 。
Critic 根据这套数据评估出当前状态的价值,并计算出"优势函数(Advantage)"------也就是告诉 Actor:"你刚才那一步动作,比我预期的要好多少"。Actor 随后根据这个反馈来更新自己的策略权重。
由于双方看到的输入信息是完全相同的(对称的),这就是传统的对称 Actor-Critic 架构。
二、 为什么要"非对称"?打破信息的壁垒
那么,问题出在哪里?
问题在于:Actor 和 Critic 在未来的命运是截然不同的。
- Actor 是要被部署到实机上的。 它必须在充满噪声、传感器受限的真实世界中工作。所以,在仿真训练时,我们必须对 Actor 的输入"痛下狠手"------不仅要剔除它在现实中无法获取的数据(比如机器人的绝对线速度、准确的高程图),还要给它的输入加上大量的噪声(Noise),以模拟现实的粗糙。
- Critic 是永远留在仿真器里的。 训练一旦结束,Critic 就会被无情抛弃,它不需要部署到实机。
如果我们使用对称架构 ,为了照顾 Actor 的实机部署,我们不得不把输入给 Critic 的数据也变得残缺不全并充满噪声。
这会导致一个严重的后果:Critic 会变成一个"瞎眼"的评论家。 它连机器人当前的真实速度都看不准,怎么可能准确评估当前状态的价值?如果 Critic 评估得不准,它给 Actor 的反馈(Advantage)就会充满方差,导致 Actor 的学习过程极其低效,甚至无法收敛。
这就像一个近视眼的导演在指导一个蒙着眼睛的演员,两人在黑暗中互相折磨。
AsAC 便是打破这一僵局的魔法。
它的核心理念是:既然 Critic 不出差,那就给它开挂!
我们允许 Critic 看到无噪声的、全量的、甚至是上帝视角的特权信息(Privileged Information)(比如真实的质心速度、精确的地形高度等),从而让 Critic 变成一个明察秋毫的顶级导演。而 Actor 依然只能看到带有噪声的、受限的本体感受数据(Proprioception)。
顶级导演指导蒙眼演员,学习效率和最终效果都会得到质的飞跃。

三、 代码实战:在 IsaacLab 中配置AsAC架构
在 IsaacLab 官方的一些基础 Demo 中(例如 G1 机器人的基础粗糙地形配置),由于任务简单,往往并没有使用这种非对称范式。
你可以看看基础配置是长什么样的:
python
# 传统的对称配置示例 (截取自基础 Demo)
@configclass
class ObservationsCfg:
"""Observation specifications for the MDP."""
@configclass
class PolicyCfg(ObsGroup):
"""Observations for policy group."""
# observation terms (order preserved)
base_lin_vel = ObsTerm(func=mdp.base_lin_vel, noise=Unoise(n_min=-0.1, n_max=0.1))
base_ang_vel = ObsTerm(func=mdp.base_ang_vel, noise=Unoise(n_min=-0.2, n_max=0.2))
projected_gravity = ObsTerm(
func=mdp.projected_gravity,
noise=Unoise(n_min=-0.05, n_max=0.05),
)
velocity_commands = ObsTerm(func=mdp.generated_commands, params={"command_name": "base_velocity"})
joint_pos = ObsTerm(func=mdp.joint_pos_rel, noise=Unoise(n_min=-0.01, n_max=0.01))
joint_vel = ObsTerm(func=mdp.joint_vel_rel, noise=Unoise(n_min=-1.5, n_max=1.5))
actions = ObsTerm(func=mdp.last_action)
height_scan = ObsTerm(
func=mdp.height_scan,
params={"sensor_cfg": SceneEntityCfg("height_scanner")},
noise=Unoise(n_min=-0.1, n_max=0.1),
clip=(-1.0, 1.0),
)
def __post_init__(self):
self.enable_corruption = True
self.concatenate_terms = True
# 只有一个 policy 观测组,Actor 和 Critic 都在用这套含噪声、带雷达的数据
policy: PolicyCfg = PolicyCfg()
在这个配置中,base_lin_vel(绝对线速度)居然被加了噪声直接喂给了 policy。这在实机中是不可能的,因为机器人根本无法直接测出自己的绝对线速度。
拥抱AsAC:参考宇树 (Unitree) 的工业级配置
为了实现落地部署并最大化训练收益,我们可以参考业界成熟的(如宇树等官方示例)配置方式。
要在 IsaacLab 中启用AsAC架构非常简单,你只需要在 ObservationsCfg 中显式地定义出 critic 观测组即可 。RSL-RL 的底层检测到同时存在 policy 和 critic 两个组时,会自动触发AsAC训练模式,而不需要在算法端做任何其他修改!
以下是经过优化的AsAC配置示例:
python
@configclass
class ObservationsCfg:
"""Observation specifications for the MDP."""
# 【1】 这是给 Actor (部署实机) 看的观测
@configclass
class PolicyCfg(ObsGroup):
"""Observations for policy group (Proprioception)."""
# 注意:这里去掉了 base_lin_vel,因为实机测不到
base_ang_vel = ObsTerm(func=mdp.base_ang_vel, scale=0.2, noise=Unoise(n_min=-0.2, n_max=0.2))
projected_gravity = ObsTerm(func=mdp.projected_gravity, noise=Unoise(n_min=-0.05, n_max=0.05))
velocity_commands = ObsTerm(func=mdp.generated_commands, params={"command_name": "base_velocity"})
joint_pos_rel = ObsTerm(func=mdp.joint_pos_rel, noise=Unoise(n_min=-0.01, n_max=0.01))
joint_vel_rel = ObsTerm(func=mdp.joint_vel_rel, scale=0.05, noise=Unoise(n_min=-1.5, n_max=1.5))
last_action = ObsTerm(func=mdp.last_action)
def __post_init__(self):
self.history_length = 5
self.enable_corruption = True # 开启噪声,让 Actor 适应恶劣环境
self.concatenate_terms = True
policy: PolicyCfg = PolicyCfg()
# 【2】 这是给 Critic (留在仿真器) 看的特权观测
@configclass
class CriticCfg(ObsGroup):
"""Observations for critic group (Privileged)."""
# 注意:这里把 base_lin_vel 放进来了!因为仿真器能直接读取
base_lin_vel = ObsTerm(func=mdp.base_lin_vel)
# 下面这些项与 Policy 相同,但是【去掉了所有噪声 (noise)】
base_ang_vel = ObsTerm(func=mdp.base_ang_vel, scale=0.2)
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)
# 你甚至可以把雷达数据(高程图)、接触力等上帝视角数据塞给 Critic
# height_scanner = ObsTerm(func=mdp.height_scan,
# params={"sensor_cfg": SceneEntityCfg("height_scanner")},
# clip=(-1.0, 5.0),
# )
def __post_init__(self):
self.history_length = 5
# 注意:这里没有设置 self.enable_corruption = True,Critic 享受纯净数据
critic: CriticCfg = CriticCfg()
在这个配置中,我们完成了两件事:
- 物理剥离 :把实机无法获取的
base_lin_vel(绝对线速度)从policy中彻底剥离,放进了critic的特权字典中。 - 噪声隔离 :
policy中的每一项都加了巨大的noise,而critic中的每一项都干干净净。
四、 效果总结:业界常态,入局必选
当使用这套AsAC架构进行训练时,你会直观地感受到:Critic 的 Value Loss 收敛得更加平稳且迅速,Actor 也能更快地找到高 Reward 的动作策略。
在四足和双足机器人的强化学习领域,这种剥离 base_lin_vel 并在 Critic 中引入特权信息的AsAC架构,已经不再是什么奇技淫巧,而是不折不扣的"业界常态"和"行业基准"。
它不像我们上一篇介绍的"蒸馏学习(Distillation)"那样,需要冗长的三步走、需要繁琐的中间数据生成和复杂的损失函数匹配。AsAC只是一次单纯的结构重组,几乎没有任何额外的计算开销,甚至无需修改一行底层算法代码,只需要在 YAML/Cfg 文件中敲下几行配置,就能获得显著的训练改善。
可以毫不夸张地说,只要你打算将模型部署到实机,这种非对称架构就是必须使用的基础配置。