策略网络精读:Actor-Critic、Adaptation Module 与 RMA 风格观测设计
如果说环境回答的是"机器人身上和世界里发生了什么",那么策略网络回答的就是另一个核心问题:
面对这些观测,策略到底是如何决定动作的?
在这个项目里,这一层的关键代码位于 go2_gym_learn/ppo_cse/actor_critic.py。它虽然文件名叫 actor_critic,但实际结构并不是传统意义上"一个 actor、一个 critic"那么简单,而是三部分一起工作:
adaptation_moduleactor_bodycritic_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_history 和 privileged_obs 分别服务谁
这是整篇最重要的一个问题。
obs_history 服务谁?
obs_history 是整个 student 路径的核心输入。
它会喂给:
adaptation_moduleactor_bodycritic_body
你可以从代码里直接看出来:
python
latent = self.adaptation_module(observation_history)
actions_mean = self.actor_body(torch.cat((observation_history, latent), dim=-1))
# student 路径里,历史观测既用于环境因子估计,也直接作为动作网络输入
也就是说,obs_history 的角色不是辅助信息,而是主信息源。
物理上它承担的是两件事:
- 提供时序上下文,帮助处理延迟、惯性和接触切换
- 提供系统辨识线索,让 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 风格闭环就成立了:
- teacher 用真实环境因子控制
- student 用历史观测估计环境因子
- student 尽量逼近 teacher 的控制效果
- 部署时就算拿不到真实环境参数,也能靠历史轨迹在线适应
这就是为什么该结构特别适合 sim-to-real:
真实机器人部署时,你不可能直接读取"当前地面摩擦系数",但你可以从过去几十步的运动反馈里推断它。
四、student 和 teacher 两条路径的区别
这一版 actor_critic.py 里,student 和 teacher 的分界非常清楚。
teacher 路径
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 路径
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
这一点值得单独拎出来,因为非常容易讲错。
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。
部署时真正需要的推理链路只有:
- 从传感器维护
obs_history adaptation_module(obs_history) -> privileged_hatactor_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 风格核心。