walk_these_ways项目学习记录第八篇(通过行为多样性 (MoB) 实现地形泛化)--策略网络

策略网络精读:Actor-Critic、Adaptation Module 与 RMA 风格观测设计

如果说环境回答的是"机器人身上和世界里发生了什么",那么策略网络回答的就是另一个核心问题:

面对这些观测,策略到底是如何决定动作的?

在这个项目里,这一层的关键代码位于 go2_gym_learn/ppo_cse/actor_critic.py。它虽然文件名叫 actor_critic,但实际结构并不是传统意义上"一个 actor、一个 critic"那么简单,而是三部分一起工作:

  1. adaptation_module
  2. actor_body
  3. critic_body

它们共同组成了一套很典型的 RMA(Rapid Motor Adaptation)风格策略结构

  • 训练时,teacher 可以看到特权信息 privileged_obs
  • student 看不到特权信息,只能看 obs_history
  • adaptation_module 的作用就是根据历史观测,去"猜出"当前隐藏环境因子
  • actor 再利用这个估计结果输出动作

所以,这个网络真正想解决的问题不是单纯的控制,而是:

在部分可观测环境里,如何从历史轨迹中在线估计隐藏动力学,再据此控制机器人。


一、整体结构:三个网络分别做什么

先看初始化定义,位于 actor_critic.py:20

python 复制代码
class ActorCritic(nn.Module):
    def __init__(self, num_obs, num_privileged_obs, num_obs_history, num_actions, **kwargs):
        self.num_obs_history = num_obs_history      # 历史观测总维度
        self.num_privileged_obs = num_privileged_obs# 特权观测维度

这里最容易忽略的一个点是:
在这个 ppo_cse 版本里,主输入不是当前单帧 obs,而是 obs_history

下面是三部分网络的定义。

1. Adaptation Module
python 复制代码
adaptation_module_layers.append(nn.Linear(self.num_obs_history, 256))
adaptation_module_layers.append(activation)
...
adaptation_module_layers.append(nn.Linear(128, self.num_privileged_obs))
self.adaptation_module = nn.Sequential(*adaptation_module_layers)
# 输入:obs_history
# 输出:一个与 privileged_obs 同维度的估计向量

这段非常关键。

在当前实现里,adaptation_module 不是输出一个抽象 latent code 再交给别的 encoder 解码,而是直接输出与 privileged_obs 同维度的预测结果

也就是说,在这个仓库的 ppo_cse 版本里,它学的是:

obs_history -> privileged_obs_hat

而不是:

obs_history -> latent_hat -> privileged_obs

这一点和很多论文或旧版本 RMA 实现略有不同。

2. Actor Body
python 复制代码
actor_layers.append(nn.Linear(self.num_privileged_obs + self.num_obs_history, 512))
...
actor_layers.append(nn.Linear(128, num_actions))
self.actor_body = nn.Sequential(*actor_layers)
# 输入:obs_history + 环境因子(teacher 用真实 privileged_obs,student 用 adaptation 预测值)
# 输出:动作均值

从结构上说,actor 并不是只看当前观测,它看的是:

  • 一整段历史观测
  • 再加上一个"环境因子向量"

所以这里的控制逻辑不是单纯 reactive policy,而是:

基于时间上下文 + 隐含动力学估计的条件控制器。

3. Critic Body
python 复制代码
critic_layers.append(nn.Linear(self.num_privileged_obs + self.num_obs_history, 512))
...
critic_layers.append(nn.Linear(128, 1))
self.critic_body = nn.Sequential(*critic_layers)
# 输入:obs_history + 真实 privileged_obs
# 输出:状态价值 V(s)

critic 的设计和 actor 很像,但关键区别在于:

critic 在训练中总是用真实 privileged information。

这是典型的 asymmetric actor-critic 设计:

actor 的部署输入受限,但 critic 训练时可以更"聪明",从而给 PPO 提供更稳定的 value estimate。


二、obs_historyprivileged_obs 分别服务谁

这是整篇最重要的一个问题。

obs_history 服务谁?

obs_history 是整个 student 路径的核心输入。

它会喂给:

  • adaptation_module
  • actor_body
  • critic_body

你可以从代码里直接看出来:

python 复制代码
latent = self.adaptation_module(observation_history)
actions_mean = self.actor_body(torch.cat((observation_history, latent), dim=-1))
# student 路径里,历史观测既用于环境因子估计,也直接作为动作网络输入

也就是说,obs_history 的角色不是辅助信息,而是主信息源。

物理上它承担的是两件事:

  1. 提供时序上下文,帮助处理延迟、惯性和接触切换
  2. 提供系统辨识线索,让 adaptation module 推断隐藏环境参数
privileged_obs 服务谁?

privileged_obs 在训练期主要服务两类模块:

  • teacher actor
  • critic
  • adaptation module 的监督目标

比如 teacher 路径:

python 复制代码
actions_mean = self.actor_body(torch.cat((observation_history, privileged_info), dim=-1))
# teacher 直接拿真实 privileged_obs 当环境因子输入

critic 也是一样:

python 复制代码
value = self.critic_body(torch.cat((observation_history, privileged_observations), dim=-1))
# critic 在训练时直接看真实环境参数,从而更准确估计 value

而 adaptation module 的训练目标也正是 privileged_obs

python 复制代码
adaptation_pred = self.actor_critic.adaptation_module(obs_history_batch)
adaptation_target = privileged_obs_batch
adaptation_loss = F.mse_loss(adaptation_pred, adaptation_target)
# adaptation module 学的是:只靠历史观测,把真实 privileged_obs 拟合出来

所以一句话总结:

obs_history 是 student 可见世界,privileged_obs 是训练期 teacher/critic 可见的真实世界。


三、为什么这是一种 RMA 风格观测设计

RMA 的核心思想并不是"多堆一层网络",而是:

把机器人控制拆成"在线环境辨识" + "条件动作生成"两步。

在这个实现里,对应关系非常清楚:

  • adaptation_module = 在线辨识器
  • actor_body = 条件动作策略

从观测设计上看,这种结构要求把输入分成两类:

第一类:显式可观测量

例如:

  • 姿态
  • 关节位置
  • 关节速度
  • 动作历史
  • 步态时钟
  • 命令信号

这些都进入 obs_history

第二类:隐藏但重要的环境量

例如:

  • friction
  • restitution
  • mass
  • gravity
  • motor strength
  • motor offset

这些进入 privileged_obs,只在仿真训练期可见。

于是整个 RMA 风格闭环就成立了:

  1. teacher 用真实环境因子控制
  2. student 用历史观测估计环境因子
  3. student 尽量逼近 teacher 的控制效果
  4. 部署时就算拿不到真实环境参数,也能靠历史轨迹在线适应

这就是为什么该结构特别适合 sim-to-real:

真实机器人部署时,你不可能直接读取"当前地面摩擦系数",但你可以从过去几十步的运动反馈里推断它。


四、student 和 teacher 两条路径的区别

这一版 actor_critic.py 里,student 和 teacher 的分界非常清楚。

teacher 路径

actor_critic.py:137

python 复制代码
def act_teacher(self, observation_history, privileged_info, policy_info={}):
    actions_mean = self.actor_body(torch.cat((observation_history, privileged_info), dim=-1))
    policy_info["latents"] = privileged_info
    return actions_mean

teacher 的特点是:

  • 直接使用真实 privileged_obs
  • 不需要 adaptation module 猜
  • 本质上等价于"上帝视角策略"

所以 teacher 回答的是:

如果我知道环境真实参数,我应该怎么控制?

student 路径

actor_critic.py:131

python 复制代码
def act_student(self, observation_history, policy_info={}):
    latent = self.adaptation_module(observation_history)
    actions_mean = self.actor_body(torch.cat((observation_history, latent), dim=-1))
    policy_info["latents"] = latent.detach().cpu().numpy()
    return actions_mean

student 的特点是:

  • 看不到真实 privileged_obs
  • 必须先通过 adaptation_module 预测一个环境因子向量
  • 再把它和 obs_history 拼接后送入 actor

所以 student 回答的是:

如果我只能看到历史轨迹,我能不能自己推断环境,再做出接近 teacher 的动作?


五、训练时 actor 和 critic 到底走哪条路

ppo_cse/ppo.py 可以看到,训练时 PPO 主 rollout 用的是 student actor + privileged critic:

python 复制代码
self.transition.actions = self.actor_critic.act(obs_history).detach()
self.transition.values = self.actor_critic.evaluate(obs_history, privileged_obs).detach()
# 动作是 student 路径生成的,因为部署时也必须靠 student
# 价值函数则用真实 privileged_obs 评估,因为 critic 只服务训练,不要求可部署

这点非常重要。

说明训练的目标不是"先训练 teacher,再蒸馏给 student",而是:

  • 策略 rollout 本身已经在用 student
  • critic 额外拿 privileged information 提供更强的训练信号
  • adaptation module 再单独用 MSE 去逼近 privileged_obs

因此这是一个典型的 asymmetric actor-critic + adaptation supervision 结构。


六、Adaptation Module 的监督方式:这里不是 latent distillation,而是直接回归 privileged_obs

这一点值得单独拎出来,因为非常容易讲错。

ppo_cse/ppo.py:170

python 复制代码
adaptation_pred = self.actor_critic.adaptation_module(obs_history_batch)
with torch.no_grad():
    adaptation_target = privileged_obs_batch

adaptation_loss = F.mse_loss(adaptation_pred[:num_train, selection_indices],
                             adaptation_target[:num_train, selection_indices])
# adaptation module 的训练目标就是 privileged_obs 本身
# 不是 teacher latent,也不是 encoder 输出

这说明当前版本的"latent"其实更像是一个直接可解释的环境参数估计向量

如果 num_privileged_obs = 2,那它输出的就是 2 维;

如果这 2 维刚好是 friction 和 restitution,那它本质上就在做一个迷你 system identification network。

从博客写作角度,这里可以直接下一个非常清晰的判断:

这个实现里,adaptation module 学到的不是抽象表征,而是任务选定的特权观测本身。


七、为什么部署时只需要 adaptation_module + actor_body

这一点在 ppo_cse/__init__.py 里体现得非常直接。训练保存模型时,会单独导出两个 TorchScript:

python 复制代码
adaptation_module = copy.deepcopy(self.alg.actor_critic.adaptation_module).to('cpu')
traced_script_adaptation_module = torch.jit.script(adaptation_module)
traced_script_adaptation_module.save(adaptation_module_path)
# 导出历史观测 -> 环境因子估计器

body_model = copy.deepcopy(self.alg.actor_critic.actor_body).to('cpu')
traced_script_body_module = torch.jit.script(body_model)
traced_script_body_module.save(body_path)
# 导出条件动作网络: (obs_history + 环境因子) -> action

为什么只导这两个?

因为部署时不需要 critic,也不需要完整 PPO。

部署时真正需要的推理链路只有:

  1. 从传感器维护 obs_history
  2. adaptation_module(obs_history) -> privileged_hat
  3. actor_body(concat(obs_history, privileged_hat)) -> action

这正好对应 student 路径。

而 critic 的作用只是训练时估计 value:

它不参与动作执行,所以部署毫无必要。

同理,训练器、rollout storage、advantages、returns 都只是训练阶段工具,也不需要上机器人。

所以部署端最精简的推理图就是:

obs_history -> adaptation_module -> privileged_hat -> actor_body -> action

这就是为什么只需要两个 TorchScript 文件。


八、act_inference():部署接口本质上就是 student 路径

虽然导出时拆成了两个 JIT 文件,但从逻辑上看,部署接口早就已经写好了。见 actor_critic.py:128

python 复制代码
def act_inference(self, ob, policy_info={}):
    return self.act_student(ob["obs_history"], policy_info=policy_info)
# 推理阶段只走 student 路径
# 输入只依赖 obs_history,不依赖 privileged_obs

这句其实就是整个 RMA 结构最核心的部署声明:

真实机器上,student 才是最终要跑的策略。

teacher 只存在于训练分析和上界参考中。


九、从网络结构看,这个策略到底在学什么

如果把整个 ActorCritic 抽象成一句话,它学的不是普通的状态反馈控制,而是:

"基于历史轨迹做环境辨识,再在辨识结果条件下生成动作。"

其中:

  • adaptation_module 学环境
  • actor_body 学控制
  • critic_body 学价值评估

所以这套结构的能力来源不是单个大 MLP 的暴力拟合,而是一个非常明确的分工:

  • 谁负责认世界
  • 谁负责做动作
  • 谁负责给训练打分

这也解释了为什么这种结构常见于 sim-to-real locomotion:

因为部署难点从来不是"我能不能把一个动作输出出来",而是"我能不能在地面变了、载荷变了、执行器特性变了的时候,还迅速知道自己正处在哪种物理条件里"。


结语

actor_critic.py 这一层最值得记住的,不是某个 hidden dim,也不是用的是 ELU 还是 ReLU,而是它的结构思想:

把控制问题拆成"历史观测中的环境辨识"与"条件动作生成"两部分。

teacher 路径用真实特权信息给出上界;

student 路径只靠历史轨迹逼近这个上界;

critic 则用特权信息帮助训练更稳;

部署时只保留 student 真正需要的两块:adaptation_module + actor_body

这就是这份实现的 RMA 风格核心。

相关推荐
飞Link2 小时前
逆向兼容的桥梁:3to2 自动化降级工具实现全解析
运维·开发语言·python·自动化
管二狗赶快去工作!2 小时前
体系结构论文(九十八):NPUEval: Optimizing NPU Kernels with LLMs and Open Source Compilers
人工智能·深度学习·自然语言处理·体系结构
zhangshuang-peta2 小时前
通过 MCP 控制平面引入技能
人工智能·机器学习·ai agent·mcp·peta
曾阿伦2 小时前
Python3 文件 (夹) 操作备忘录
开发语言·python
LX567772 小时前
传统编辑如何考取AI内容编辑师认证?学习路径详解
人工智能·学习
LaughingZhu2 小时前
Product Hunt 每日热榜 | 2026-04-10
人工智能·经验分享·深度学习·神经网络·产品运营
数据知道2 小时前
claw-code 源码分析:OmX `$team` / `$ralph`——把 AI 辅助开发从偶发灵感变成可重复流水线
数据库·人工智能·mysql·ai·claude code·claw code
dong__csdn2 小时前
jdk添加信任证书
java·开发语言
songyuc2 小时前
BM2『链表内指定区间反转』学习笔记
学习·链表