updated
This commit is contained in:
parent
7e3da40be3
commit
fe683c1e1b
61
README.md
61
README.md
|
|
@ -15,13 +15,13 @@ cd humanoid_skateboarding
|
||||||
uv sync && uv pip install -e .
|
uv sync && uv pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
**(可选)LeRobot v3 导出 / 边播边录** 需要额外安装 `lerobot`(不在默认 `pyproject` 依赖里):
|
**(可选)LeRobot v3 导出 / 边播边录** 需要额外安装 `lerobot`(不在默认依赖里,以免与现有 PyTorch/CUDA 栈冲突):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv pip install lerobot
|
uv pip install lerobot
|
||||||
```
|
```
|
||||||
|
|
||||||
若安装后出现 `import torch` 报 NCCL 符号错误,可尝试:
|
若安装 `lerobot` 等包后出现 `import torch` 报 **NCCL 符号错误**(例如 `undefined symbol: ncclDevCommDestroy`):多为 **`nvidia-nccl-cu12` 与 `torch`(cu13)并存**,二者都往 `site-packages/nvidia/nccl/lib/` 装 `libnccl.so.2`,旧库覆盖了新库。可卸载 cu12 并重装 cu13 的 NCCL:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv pip uninstall nvidia-nccl-cu12
|
uv pip uninstall nvidia-nccl-cu12
|
||||||
|
|
@ -58,7 +58,46 @@ uv run play Mjlab-Skater-Flat-Unitree-G1 --checkpoint_file ckpts/test.pt
|
||||||
- **`--viewer auto`**(默认):有 `DISPLAY` / `WAYLAND_DISPLAY` 时用 **native**,否则 **rerun**。
|
- **`--viewer auto`**(默认):有 `DISPLAY` / `WAYLAND_DISPLAY` 时用 **native**,否则 **rerun**。
|
||||||
- **`--viewer native`**:本机有图形界面时使用 MuJoCo 原生窗口。
|
- **`--viewer native`**:本机有图形界面时使用 MuJoCo 原生窗口。
|
||||||
- **`--viewer rerun`**:Rerun Web Viewer(无头服务器常用)。
|
- **`--viewer rerun`**:Rerun Web Viewer(无头服务器常用)。
|
||||||
- **`--viewer viser`**:Viser。
|
- **`--viewer rerun_native`**:同一套 mjlab 仿真与策略步进,**同时**打开仓库自带的 **MuJoCo 原生 viewer**(`NativeMujocoViewer` / GLFW)并把离屏相机 / qpos **推到 Rerun**。与「两个进程各跑一套仿真」无关:仍是 **单一 `env`/单一仿真循环**。**不能**与 `--lerobot-record` 共用。
|
||||||
|
- **`--viewer viser`**:Viser(浏览器三维面板,另一种自带前端)。
|
||||||
|
- **`--viewer rerun_viser`**:**同一进程、单一仿真**,同时在浏览器里打开 **Rerun**(`--rerun-web-port` / `--rerun-grpc-port`)与 **Viser mjlab 面板**(`--viser-port`)。三者各占不同端口;适合 RoboHub 左 Rerun、右 Mujoco 双 iframe。
|
||||||
|
- **注意**:`rerun_native` 里的 **native 是 GLFW 桌面窗口,不占用 HTTP 端口**;若你要「两个端口都是网页服务」,用 **`rerun_viser`**,不要用 `rerun_native` 来凑端口。
|
||||||
|
|
||||||
|
`rerun_native` 示例(端口与 `rerun` 相同,见下节 SSH 转发):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run play Mjlab-Skater-Flat-Unitree-G1 --checkpoint_file ckpts/test.pt \
|
||||||
|
--viewer rerun_native --rerun-web-port 18080 --rerun-grpc-port 19876
|
||||||
|
```
|
||||||
|
|
||||||
|
`rerun_viser` 示例(**三个端口**:Rerun Web、Rerun gRPC、Viser,互不重复):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run play Mjlab-Skater-Flat-Unitree-G1 --checkpoint_file ckpts/test.pt \
|
||||||
|
--viewer rerun_viser \
|
||||||
|
--rerun-web-port 18080 \
|
||||||
|
--rerun-grpc-port 19876 \
|
||||||
|
--viser-port 19090
|
||||||
|
```
|
||||||
|
|
||||||
|
远程浏览器需 **三个** 本地转发(把示例端口换成你实际用的):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -N \
|
||||||
|
-L 18080:127.0.0.1:18080 \
|
||||||
|
-L 19876:127.0.0.1:19876 \
|
||||||
|
-L 19090:127.0.0.1:19090 \
|
||||||
|
user@云主机
|
||||||
|
```
|
||||||
|
|
||||||
|
**SSH / 无桌面 / RoboHub 技能里 `RuntimeError: … DISPLAY`:**
|
||||||
|
`rerun_native` 里的「自带 viewer」是 **本机 X11/Wayland 上的 GLFW 窗口**,不是 Rerun 网页。若 shell 里 **没有** `DISPLAY` 或 `WAYLAND_DISPLAY`(很多容器/编排默认不传),会报错。处理方式:
|
||||||
|
|
||||||
|
- **只想要浏览器里看 Rerun**(单后端、无 MuJoCo 小窗):用 `--viewer rerun`。
|
||||||
|
- **仍要 `rerun_native` 但机器无物理桌面**:可装 `xvfb` 用虚拟显示,例如:
|
||||||
|
`xvfb-run -a uv run play Mjlab-Skater-Flat-Unitree-G1 ... --viewer rerun_native`
|
||||||
|
(具体以你镜像是否已含 `xvfb` 为准。)
|
||||||
|
- **RoboHub 侧**:需在技能/容器环境注入 `DISPLAY` 或把启动命令包在 `xvfb-run` 里,否则与本地终端直跑表现一致。
|
||||||
|
|
||||||
完整参数:
|
完整参数:
|
||||||
|
|
||||||
|
|
@ -68,14 +107,22 @@ uv run play Mjlab-Skater-Flat-Unitree-G1 --help
|
||||||
|
|
||||||
### 无头 OpenGL(MuJoCo 离屏相机)
|
### 无头 OpenGL(MuJoCo 离屏相机)
|
||||||
|
|
||||||
在无 `DISPLAY` 的 Linux 上,`play` 会在导入 MuJoCo 前尽量设置 **`MUJOCO_GL=egl`**(见 `mjlab_husky/mujoco_gl.py`)。若仍失败可手动指定:
|
在无 `DISPLAY` / `WAYLAND_DISPLAY` 的 Linux 上,`play` 等在 **`import mujoco` 之前** 调用 `mujlab_husky/mujoco_gl.py`:未设置 `MUJOCO_GL` 时默认 **`osmesa`**。仅设 `MUJOCO_GL` 不够:无头时 PyOpenGL 仍可能按 `linux` 选 **GLX**,导致 `glGetError` / `eglQueryString`;因此脚本会同步设置 **`PYOPENGL_PLATFORM=osmesa`**(或在你使用 `MUJOCO_GL=egl` 时为 **`egl`**)。可按需手动指定:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export MUJOCO_GL=egl # GPU 无头(常见)
|
export MUJOCO_GL=egl # GPU + 可用 NVIDIA EGL 时(更快)
|
||||||
# 或
|
# 未设置时由 mujoco_gl 默认 osmesa;或显式 CPU 光栅:
|
||||||
export MUJOCO_GL=osmesa # 纯 CPU 软件光栅(更慢)
|
export MUJOCO_GL=osmesa # 需系统已装 libosmesa6(见下)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Ubuntu(OSMesa)**:若仍报 OpenGL / `glGetError`,请先安装运行时:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get update && sudo apt-get install -y libosmesa6
|
||||||
|
```
|
||||||
|
|
||||||
|
若报错 **`mjENBL_MULTICCD`**:来自 **MuJoCo Python 枚举与 `mujoco-warp` Git 修订不一致**。本项目用 PyPI `mujoco==3.8.x` 时,`uv.lock` 已将 **`mujoco-warp` 固定为上游标签 `v3.8.0`**;若在别处自行 `uv lock --upgrade-package mjlab`,需再次确认锁里两处一致。
|
||||||
|
|
||||||
### Rerun:端口与远程浏览器
|
### Rerun:端口与远程浏览器
|
||||||
|
|
||||||
Rerun 需要 **两个端口**:**Web**(默认 `8080`)+ **gRPC**(默认多为 `9876`,以终端打印为准)。
|
Rerun 需要 **两个端口**:**Web**(默认 `8080`)+ **gRPC**(默认多为 `9876`,以终端打印为准)。
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -31,6 +31,12 @@ play-lerobot-rerun = "mjlab_husky.scripts.play_lerobot_rerun:main"
|
||||||
src = ["src"] # Helpful for recognizing first-party imports.
|
src = ["src"] # Helpful for recognizing first-party imports.
|
||||||
indent-width = 4
|
indent-width = 4
|
||||||
|
|
||||||
|
[tool.uv]
|
||||||
|
# py.mujoco.org 上的 3.7.0.dev* 预发布包会被撤下,wheel 404;强制使用 PyPI 稳定版。
|
||||||
|
override-dependencies = ["mujoco>=3.8.0,<3.9"]
|
||||||
|
# `mjlab` 声明的 mujoco-warp git rev(1dc288c)依赖 mjENBL_MULTICCD,PyPI 的 mujoco 3.8.x 尚无该枚举。
|
||||||
|
# `uv.lock` 将 mujoco-warp 固定为上游标签 v3.8.0(与 mujoco 3.8.0 对齐);升级 mjlab / 跑 `uv lock` 后若冲突请复查该包。
|
||||||
|
|
||||||
[tool.uv.sources]
|
[tool.uv.sources]
|
||||||
mjlab = { path = "/opt/vendor/mjlab" }
|
mjlab = { git = "https://github.com/mujocolab/mjlab.git", rev = "13212ad" }
|
||||||
rsl-rl-lib = { path = "rsl_rl" }
|
rsl-rl-lib = { path = "rsl_rl" }
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,6 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
"""Main module for the rsl_rl package."""
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
"""Implementation of different RL agents."""
|
|
||||||
|
|
||||||
from .distillation import Distillation
|
|
||||||
from .ppo import PPO
|
|
||||||
from .amp_ppo import AMP_PPO
|
|
||||||
__all__ = ["PPO", "Distillation", "AMP_PPO"]
|
|
||||||
|
|
@ -1,571 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
from torch._tensor import Tensor
|
|
||||||
from torch._tensor import Tensor
|
|
||||||
from typing import Any
|
|
||||||
from copy import deepcopy
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
import torch.optim as optim
|
|
||||||
from itertools import chain
|
|
||||||
|
|
||||||
from rsl_rl.modules import ActorCritic
|
|
||||||
from rsl_rl.modules.rnd import RandomNetworkDistillation
|
|
||||||
from rsl_rl.storage import RolloutStorage, ReplayBufferMulti
|
|
||||||
from rsl_rl.utils import string_to_callable
|
|
||||||
|
|
||||||
|
|
||||||
class AMP_PPO:
|
|
||||||
"""Proximal Policy Optimization algorithm (https://arxiv.org/abs/1707.06347)."""
|
|
||||||
|
|
||||||
policy: ActorCritic
|
|
||||||
"""The actor critic module."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
policy,
|
|
||||||
discriminator,
|
|
||||||
amp_data,
|
|
||||||
amp_normalizer,
|
|
||||||
amp_num_frames=1,
|
|
||||||
amp_replay_buffer_size=100000,
|
|
||||||
num_learning_epochs=5,
|
|
||||||
num_mini_batches=4,
|
|
||||||
clip_param=0.2,
|
|
||||||
gamma=0.99,
|
|
||||||
lam=0.95,
|
|
||||||
value_loss_coef=1.0,
|
|
||||||
entropy_coef=0.01,
|
|
||||||
learning_rate=0.001,
|
|
||||||
max_grad_norm=1.0,
|
|
||||||
use_clipped_value_loss=True,
|
|
||||||
schedule="adaptive",
|
|
||||||
desired_kl=0.01,
|
|
||||||
device="cpu",
|
|
||||||
normalize_advantage_per_mini_batch=False,
|
|
||||||
# RND parameters
|
|
||||||
rnd_cfg: dict | None = None,
|
|
||||||
# Symmetry parameters
|
|
||||||
symmetry_cfg: dict | None = None,
|
|
||||||
# Distributed training parameters
|
|
||||||
multi_gpu_cfg: dict | None = None,
|
|
||||||
):
|
|
||||||
# device-related parameters
|
|
||||||
self.device = device
|
|
||||||
self.is_multi_gpu = multi_gpu_cfg is not None
|
|
||||||
# Multi-GPU parameters
|
|
||||||
if multi_gpu_cfg is not None:
|
|
||||||
self.gpu_global_rank = multi_gpu_cfg["global_rank"]
|
|
||||||
self.gpu_world_size = multi_gpu_cfg["world_size"]
|
|
||||||
else:
|
|
||||||
self.gpu_global_rank = 0
|
|
||||||
self.gpu_world_size = 1
|
|
||||||
|
|
||||||
# RND components
|
|
||||||
if rnd_cfg is not None:
|
|
||||||
# Extract parameters used in ppo
|
|
||||||
rnd_lr = rnd_cfg.pop("learning_rate", 1e-3)
|
|
||||||
# Create RND module
|
|
||||||
self.rnd = RandomNetworkDistillation(device=self.device, **rnd_cfg)
|
|
||||||
# Create RND optimizer
|
|
||||||
params = self.rnd.predictor.parameters()
|
|
||||||
self.rnd_optimizer = optim.Adam(params, lr=rnd_lr)
|
|
||||||
else:
|
|
||||||
self.rnd = None
|
|
||||||
self.rnd_optimizer = None
|
|
||||||
|
|
||||||
# Symmetry components
|
|
||||||
if symmetry_cfg is not None:
|
|
||||||
# Check if symmetry is enabled
|
|
||||||
use_symmetry = symmetry_cfg["use_data_augmentation"] or symmetry_cfg["use_mirror_loss"]
|
|
||||||
# Print that we are not using symmetry
|
|
||||||
if not use_symmetry:
|
|
||||||
print("Symmetry not used for learning. We will use it for logging instead.")
|
|
||||||
# If function is a string then resolve it to a function
|
|
||||||
if isinstance(symmetry_cfg["data_augmentation_func"], str):
|
|
||||||
symmetry_cfg["data_augmentation_func"] = string_to_callable(symmetry_cfg["data_augmentation_func"])
|
|
||||||
# Check valid configuration
|
|
||||||
if symmetry_cfg["use_data_augmentation"] and not callable(symmetry_cfg["data_augmentation_func"]):
|
|
||||||
raise ValueError(
|
|
||||||
"Data augmentation enabled but the function is not callable:"
|
|
||||||
f" {symmetry_cfg['data_augmentation_func']}"
|
|
||||||
)
|
|
||||||
# Store symmetry configuration
|
|
||||||
self.symmetry = symmetry_cfg
|
|
||||||
else:
|
|
||||||
self.symmetry = None
|
|
||||||
|
|
||||||
## AMP components
|
|
||||||
self.discriminator = discriminator
|
|
||||||
self.discriminator.to(self.device)
|
|
||||||
|
|
||||||
self.amp_storage = ReplayBufferMulti(discriminator.state_dim, amp_replay_buffer_size, amp_num_frames, device)
|
|
||||||
self.amp_data = amp_data
|
|
||||||
self.amp_normalizer = amp_normalizer
|
|
||||||
|
|
||||||
# PPO components
|
|
||||||
self.policy = policy
|
|
||||||
self.policy.to(self.device)
|
|
||||||
|
|
||||||
# Create rollout storage
|
|
||||||
self.storage: RolloutStorage = None # type: ignore
|
|
||||||
self.transition = RolloutStorage.Transition()
|
|
||||||
self.amp_transition = RolloutStorage.Transition()
|
|
||||||
params = [
|
|
||||||
{'params': self.policy.parameters(), 'name': 'policy'},
|
|
||||||
]
|
|
||||||
|
|
||||||
params.append({
|
|
||||||
'params': self.discriminator.trunk.parameters(),
|
|
||||||
'weight_decay': 10e-4,
|
|
||||||
'name': f'amp_trunk'
|
|
||||||
})
|
|
||||||
params.append({
|
|
||||||
'params': self.discriminator.amp_linear.parameters(),
|
|
||||||
'weight_decay': 10e-2,
|
|
||||||
'name': f'amp_head'
|
|
||||||
})
|
|
||||||
|
|
||||||
# Create optimizer
|
|
||||||
self.optimizer = optim.Adam(params, lr=learning_rate)
|
|
||||||
|
|
||||||
# PPO parameters
|
|
||||||
self.clip_param = clip_param
|
|
||||||
self.num_learning_epochs = num_learning_epochs
|
|
||||||
self.num_mini_batches = num_mini_batches
|
|
||||||
self.value_loss_coef = value_loss_coef
|
|
||||||
self.entropy_coef = entropy_coef
|
|
||||||
self.gamma = gamma
|
|
||||||
self.lam = lam
|
|
||||||
self.max_grad_norm = max_grad_norm
|
|
||||||
self.use_clipped_value_loss = use_clipped_value_loss
|
|
||||||
self.desired_kl = desired_kl
|
|
||||||
self.schedule = schedule
|
|
||||||
self.learning_rate = learning_rate
|
|
||||||
self.normalize_advantage_per_mini_batch = normalize_advantage_per_mini_batch
|
|
||||||
|
|
||||||
|
|
||||||
def init_storage(self, training_type, num_envs, num_transitions_per_env, obs, actions_shape):
|
|
||||||
# create rollout storage
|
|
||||||
self.storage = RolloutStorage(
|
|
||||||
training_type,
|
|
||||||
num_envs,
|
|
||||||
num_transitions_per_env,
|
|
||||||
obs,
|
|
||||||
actions_shape,
|
|
||||||
self.device,
|
|
||||||
)
|
|
||||||
|
|
||||||
def act(self, obs, amp_obs):
|
|
||||||
if self.policy.is_recurrent:
|
|
||||||
self.transition.hidden_states = self.policy.get_hidden_states()
|
|
||||||
# compute the actions and values
|
|
||||||
self.transition.actions = self.policy.act(obs).detach()
|
|
||||||
self.transition.values = self.policy.evaluate(obs).detach()
|
|
||||||
self.transition.actions_log_prob = self.policy.get_actions_log_prob(self.transition.actions).detach()
|
|
||||||
self.transition.action_mean = self.policy.action_mean.detach()
|
|
||||||
self.transition.action_sigma = self.policy.action_std.detach()
|
|
||||||
# need to record obs before env.step()
|
|
||||||
self.transition.observations = obs
|
|
||||||
self.amp_transition.observations = amp_obs
|
|
||||||
return self.transition.actions
|
|
||||||
|
|
||||||
def process_env_step(self, obs, rewards, dones, extras,amp_obs, amp_obs_frames=None):
|
|
||||||
# update the normalizers
|
|
||||||
self.policy.update_normalization(obs)
|
|
||||||
if self.rnd:
|
|
||||||
self.rnd.update_normalization(obs)
|
|
||||||
|
|
||||||
# Record the rewards and dones
|
|
||||||
# Note: we clone here because later on we bootstrap the rewards based on timeouts
|
|
||||||
self.transition.rewards = rewards.clone()
|
|
||||||
self.transition.dones = dones
|
|
||||||
|
|
||||||
# Compute the intrinsic rewards and add to extrinsic rewards
|
|
||||||
if self.rnd:
|
|
||||||
# Compute the intrinsic rewards
|
|
||||||
self.intrinsic_rewards = self.rnd.get_intrinsic_reward(obs)
|
|
||||||
# Add intrinsic rewards to extrinsic rewards
|
|
||||||
self.transition.rewards += self.intrinsic_rewards
|
|
||||||
|
|
||||||
# Bootstrapping on time outs
|
|
||||||
if "time_outs" in extras:
|
|
||||||
self.transition.rewards += self.gamma * torch.squeeze(
|
|
||||||
self.transition.values * extras["time_outs"].unsqueeze(1).to(self.device), 1
|
|
||||||
)
|
|
||||||
|
|
||||||
if amp_obs_frames is not None:
|
|
||||||
self.amp_storage.insert(amp_obs_frames)
|
|
||||||
else:
|
|
||||||
self.amp_storage.insert(self.amp_transition.observations, amp_obs)
|
|
||||||
|
|
||||||
# record the transition
|
|
||||||
self.storage.add_transitions(self.transition)
|
|
||||||
self.transition.clear()
|
|
||||||
self.amp_transition.clear()
|
|
||||||
self.policy.reset(dones)
|
|
||||||
|
|
||||||
def compute_returns(self, obs):
|
|
||||||
# compute value for the last step
|
|
||||||
last_values = self.policy.evaluate(obs).detach()
|
|
||||||
self.storage.compute_returns(
|
|
||||||
last_values, self.gamma, self.lam, normalize_advantage=not self.normalize_advantage_per_mini_batch
|
|
||||||
)
|
|
||||||
|
|
||||||
def update(self): # noqa: C901
|
|
||||||
mean_value_loss = 0
|
|
||||||
mean_surrogate_loss = 0
|
|
||||||
mean_entropy = 0
|
|
||||||
mean_amp_loss = 0
|
|
||||||
mean_grad_pen_loss = 0
|
|
||||||
mean_policy_pred = 0
|
|
||||||
mean_expert_pred = 0
|
|
||||||
# -- RND loss
|
|
||||||
if self.rnd:
|
|
||||||
mean_rnd_loss = 0
|
|
||||||
else:
|
|
||||||
mean_rnd_loss = None
|
|
||||||
# -- Symmetry loss
|
|
||||||
if self.symmetry:
|
|
||||||
mean_symmetry_loss = 0
|
|
||||||
else:
|
|
||||||
mean_symmetry_loss = None
|
|
||||||
|
|
||||||
# generator for mini batches
|
|
||||||
if self.policy.is_recurrent:
|
|
||||||
generator = self.storage.recurrent_mini_batch_generator(self.num_mini_batches, self.num_learning_epochs)
|
|
||||||
else:
|
|
||||||
generator = self.storage.mini_batch_generator(self.num_mini_batches, self.num_learning_epochs)
|
|
||||||
|
|
||||||
|
|
||||||
amp_policy_generator = self.amp_storage.feed_forward_generator(
|
|
||||||
self.num_learning_epochs * self.num_mini_batches,
|
|
||||||
self.storage.num_envs * self.storage.num_transitions_per_env // self.num_mini_batches,
|
|
||||||
)
|
|
||||||
|
|
||||||
amp_expert_generator = self.amp_data.feed_forward_generator_23dof_multi(
|
|
||||||
self.num_learning_epochs * self.num_mini_batches,
|
|
||||||
self.storage.num_envs * self.storage.num_transitions_per_env // self.num_mini_batches,
|
|
||||||
)
|
|
||||||
|
|
||||||
# iterate over batches
|
|
||||||
for sample, sample_amp_policy, sample_amp_expert in zip(generator, amp_policy_generator, amp_expert_generator):
|
|
||||||
(
|
|
||||||
obs_batch,
|
|
||||||
actions_batch,
|
|
||||||
target_values_batch,
|
|
||||||
advantages_batch,
|
|
||||||
returns_batch,
|
|
||||||
old_actions_log_prob_batch,
|
|
||||||
old_mu_batch,
|
|
||||||
old_sigma_batch,
|
|
||||||
hid_states_batch,
|
|
||||||
masks_batch,
|
|
||||||
) = sample
|
|
||||||
|
|
||||||
# number of augmentations per sample
|
|
||||||
# we start with 1 and increase it if we use symmetry augmentation
|
|
||||||
num_aug = 1
|
|
||||||
# original batch size
|
|
||||||
# we assume policy group is always there and needs augmentation
|
|
||||||
original_batch_size = obs_batch.batch_size[0]
|
|
||||||
|
|
||||||
# check if we should normalize advantages per mini batch
|
|
||||||
if self.normalize_advantage_per_mini_batch:
|
|
||||||
with torch.no_grad():
|
|
||||||
advantages_batch = (advantages_batch - advantages_batch.mean()) / (advantages_batch.std() + 1e-8)
|
|
||||||
|
|
||||||
# Perform symmetric augmentation
|
|
||||||
if self.symmetry and self.symmetry["use_data_augmentation"]:
|
|
||||||
# augmentation using symmetry
|
|
||||||
data_augmentation_func = self.symmetry["data_augmentation_func"]
|
|
||||||
# returned shape: [batch_size * num_aug, ...]
|
|
||||||
obs_batch, actions_batch = data_augmentation_func(
|
|
||||||
obs=obs_batch,
|
|
||||||
actions=actions_batch,
|
|
||||||
env=self.symmetry["_env"],
|
|
||||||
)
|
|
||||||
# compute number of augmentations per sample
|
|
||||||
# we assume policy group is always there and needs augmentation
|
|
||||||
num_aug = int(obs_batch.batch_size[0] / original_batch_size)
|
|
||||||
# repeat the rest of the batch
|
|
||||||
# -- actor
|
|
||||||
old_actions_log_prob_batch = old_actions_log_prob_batch.repeat(num_aug, 1)
|
|
||||||
# -- critic
|
|
||||||
target_values_batch = target_values_batch.repeat(num_aug, 1)
|
|
||||||
advantages_batch = advantages_batch.repeat(num_aug, 1)
|
|
||||||
returns_batch = returns_batch.repeat(num_aug, 1)
|
|
||||||
|
|
||||||
# Recompute actions log prob and entropy for current batch of transitions
|
|
||||||
# Note: we need to do this because we updated the policy with the new parameters
|
|
||||||
# -- actor
|
|
||||||
self.policy.act(obs_batch, masks=masks_batch, hidden_states=hid_states_batch[0])
|
|
||||||
actions_log_prob_batch = self.policy.get_actions_log_prob(actions_batch)
|
|
||||||
# -- critic
|
|
||||||
value_batch = self.policy.evaluate(obs_batch, masks=masks_batch, hidden_states=hid_states_batch[1])
|
|
||||||
# -- entropy
|
|
||||||
# we only keep the entropy of the first augmentation (the original one)
|
|
||||||
mu_batch = self.policy.action_mean[:original_batch_size]
|
|
||||||
sigma_batch = self.policy.action_std[:original_batch_size]
|
|
||||||
entropy_batch = self.policy.entropy[:original_batch_size]
|
|
||||||
|
|
||||||
# KL
|
|
||||||
if self.desired_kl is not None and self.schedule == "adaptive":
|
|
||||||
with torch.inference_mode():
|
|
||||||
kl = torch.sum(
|
|
||||||
torch.log(sigma_batch / old_sigma_batch + 1.0e-5)
|
|
||||||
+ (torch.square(old_sigma_batch) + torch.square(old_mu_batch - mu_batch))
|
|
||||||
/ (2.0 * torch.square(sigma_batch))
|
|
||||||
- 0.5,
|
|
||||||
axis=-1,
|
|
||||||
)
|
|
||||||
kl_mean = torch.mean(kl)
|
|
||||||
|
|
||||||
# Reduce the KL divergence across all GPUs
|
|
||||||
if self.is_multi_gpu:
|
|
||||||
torch.distributed.all_reduce(kl_mean, op=torch.distributed.ReduceOp.SUM)
|
|
||||||
kl_mean /= self.gpu_world_size
|
|
||||||
|
|
||||||
# Update the learning rate
|
|
||||||
# Perform this adaptation only on the main process
|
|
||||||
# TODO: Is this needed? If KL-divergence is the "same" across all GPUs,
|
|
||||||
# then the learning rate should be the same across all GPUs.
|
|
||||||
if self.gpu_global_rank == 0:
|
|
||||||
if kl_mean > self.desired_kl * 2.0:
|
|
||||||
self.learning_rate = max(1e-5, self.learning_rate / 1.5)
|
|
||||||
elif kl_mean < self.desired_kl / 2.0 and kl_mean > 0.0:
|
|
||||||
self.learning_rate = min(1e-2, self.learning_rate * 1.5)
|
|
||||||
|
|
||||||
# Update the learning rate for all GPUs
|
|
||||||
if self.is_multi_gpu:
|
|
||||||
lr_tensor = torch.tensor(self.learning_rate, device=self.device)
|
|
||||||
torch.distributed.broadcast(lr_tensor, src=0)
|
|
||||||
self.learning_rate = lr_tensor.item()
|
|
||||||
|
|
||||||
# Update the learning rate for all parameter groups
|
|
||||||
for param_group in self.optimizer.param_groups:
|
|
||||||
param_group["lr"] = self.learning_rate
|
|
||||||
|
|
||||||
# Surrogate loss
|
|
||||||
ratio = torch.exp(actions_log_prob_batch - torch.squeeze(old_actions_log_prob_batch))
|
|
||||||
surrogate = -torch.squeeze(advantages_batch) * ratio
|
|
||||||
surrogate_clipped = -torch.squeeze(advantages_batch) * torch.clamp(
|
|
||||||
ratio, 1.0 - self.clip_param, 1.0 + self.clip_param
|
|
||||||
)
|
|
||||||
surrogate_loss = torch.max(surrogate, surrogate_clipped).mean()
|
|
||||||
|
|
||||||
# Value function loss
|
|
||||||
if self.use_clipped_value_loss:
|
|
||||||
value_clipped = target_values_batch + (value_batch - target_values_batch).clamp(
|
|
||||||
-self.clip_param, self.clip_param
|
|
||||||
)
|
|
||||||
value_losses = (value_batch - returns_batch).pow(2)
|
|
||||||
value_losses_clipped = (value_clipped - returns_batch).pow(2)
|
|
||||||
value_loss = torch.max(value_losses, value_losses_clipped).mean()
|
|
||||||
else:
|
|
||||||
value_loss = (returns_batch - value_batch).pow(2).mean()
|
|
||||||
|
|
||||||
loss = surrogate_loss + self.value_loss_coef * value_loss - self.entropy_coef * entropy_batch.mean()
|
|
||||||
|
|
||||||
# Symmetry loss
|
|
||||||
if self.symmetry:
|
|
||||||
# obtain the symmetric actions
|
|
||||||
# if we did augmentation before then we don't need to augment again
|
|
||||||
if not self.symmetry["use_data_augmentation"]:
|
|
||||||
data_augmentation_func = self.symmetry["data_augmentation_func"]
|
|
||||||
obs_batch, _ = data_augmentation_func(obs=obs_batch, actions=None, env=self.symmetry["_env"])
|
|
||||||
# compute number of augmentations per sample
|
|
||||||
num_aug = int(obs_batch.shape[0] / original_batch_size)
|
|
||||||
|
|
||||||
# actions predicted by the actor for symmetrically-augmented observations
|
|
||||||
mean_actions_batch = self.policy.act_inference(obs_batch.detach().clone())
|
|
||||||
|
|
||||||
# compute the symmetrically augmented actions
|
|
||||||
# note: we are assuming the first augmentation is the original one.
|
|
||||||
# We do not use the action_batch from earlier since that action was sampled from the distribution.
|
|
||||||
# However, the symmetry loss is computed using the mean of the distribution.
|
|
||||||
action_mean_orig = mean_actions_batch[:original_batch_size]
|
|
||||||
_, actions_mean_symm_batch = data_augmentation_func(
|
|
||||||
obs=None, actions=action_mean_orig, env=self.symmetry["_env"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# compute the loss (we skip the first augmentation as it is the original one)
|
|
||||||
mse_loss = torch.nn.MSELoss()
|
|
||||||
symmetry_loss = mse_loss(
|
|
||||||
mean_actions_batch[original_batch_size:], actions_mean_symm_batch.detach()[original_batch_size:]
|
|
||||||
)
|
|
||||||
# add the loss to the total loss
|
|
||||||
if self.symmetry["use_mirror_loss"]:
|
|
||||||
loss += self.symmetry["mirror_loss_coeff"] * symmetry_loss
|
|
||||||
else:
|
|
||||||
symmetry_loss = symmetry_loss.detach()
|
|
||||||
|
|
||||||
# Random Network Distillation loss
|
|
||||||
# TODO: Move this processing to inside RND module.
|
|
||||||
if self.rnd:
|
|
||||||
# extract the rnd_state
|
|
||||||
# TODO: Check if we still need torch no grad. It is just an affine transformation.
|
|
||||||
with torch.no_grad():
|
|
||||||
rnd_state_batch = self.rnd.get_rnd_state(obs_batch[:original_batch_size])
|
|
||||||
rnd_state_batch = self.rnd.state_normalizer(rnd_state_batch)
|
|
||||||
# predict the embedding and the target
|
|
||||||
predicted_embedding = self.rnd.predictor(rnd_state_batch)
|
|
||||||
target_embedding = self.rnd.target(rnd_state_batch).detach()
|
|
||||||
# compute the loss as the mean squared error
|
|
||||||
mseloss = torch.nn.MSELoss()
|
|
||||||
rnd_loss = mseloss(predicted_embedding, target_embedding)
|
|
||||||
|
|
||||||
expert_states = sample_amp_expert
|
|
||||||
policy_states = sample_amp_policy
|
|
||||||
|
|
||||||
with torch.no_grad():
|
|
||||||
expert_states = self.amp_normalizer.normalize_torch(expert_states.to(self.device), self.device)
|
|
||||||
policy_states = self.amp_normalizer.normalize_torch(policy_states, self.device)
|
|
||||||
|
|
||||||
contact_phase_push = obs_batch['critic'][:, -4]
|
|
||||||
mask_push = contact_phase_push == 1.
|
|
||||||
|
|
||||||
if mask_push.any():
|
|
||||||
policy_d = self.discriminator(policy_states.flatten(1))
|
|
||||||
expert_states = expert_states.to(self.device)
|
|
||||||
expert_d = self.discriminator(expert_states.flatten(1))
|
|
||||||
|
|
||||||
expert_loss = torch.nn.MSELoss()(expert_d, torch.ones(expert_d.size(), device=self.device))
|
|
||||||
policy_loss = torch.nn.MSELoss()(policy_d, -1 * torch.ones(policy_d.size(), device=self.device))
|
|
||||||
amp_loss = 0.5 * (expert_loss + policy_loss)
|
|
||||||
|
|
||||||
# grad penalty
|
|
||||||
grad_pen_loss = self.discriminator.compute_grad_pen(expert_states, lambda_=5)
|
|
||||||
else:
|
|
||||||
amp_loss = torch.tensor(0.0, device=self.device)
|
|
||||||
grad_pen_loss = torch.tensor(0.0, device=self.device)
|
|
||||||
expert_loss = torch.tensor(0.0, device=self.device)
|
|
||||||
policy_loss = torch.tensor(0.0, device=self.device)
|
|
||||||
|
|
||||||
loss += (amp_loss + grad_pen_loss)
|
|
||||||
self.amp_normalizer.update(policy_states.cpu().numpy())
|
|
||||||
self.amp_normalizer.update(expert_states.cpu().numpy())
|
|
||||||
|
|
||||||
# Compute the gradients
|
|
||||||
# -- For PPO
|
|
||||||
self.optimizer.zero_grad()
|
|
||||||
loss.backward()
|
|
||||||
# -- For RND
|
|
||||||
if self.rnd:
|
|
||||||
self.rnd_optimizer.zero_grad() # type: ignore
|
|
||||||
rnd_loss.backward()
|
|
||||||
|
|
||||||
# Collect gradients from all GPUs
|
|
||||||
if self.is_multi_gpu:
|
|
||||||
self.reduce_parameters()
|
|
||||||
|
|
||||||
# Apply the gradients
|
|
||||||
# -- For PPO
|
|
||||||
nn.utils.clip_grad_norm_(self.policy.parameters(), self.max_grad_norm)
|
|
||||||
self.optimizer.step()
|
|
||||||
# -- For RND
|
|
||||||
if self.rnd_optimizer:
|
|
||||||
self.rnd_optimizer.step()
|
|
||||||
|
|
||||||
# Store the losses
|
|
||||||
mean_value_loss += value_loss.item()
|
|
||||||
mean_surrogate_loss += surrogate_loss.item()
|
|
||||||
mean_entropy += entropy_batch.mean().item()
|
|
||||||
mean_amp_loss += amp_loss.item()
|
|
||||||
mean_grad_pen_loss += grad_pen_loss.item()
|
|
||||||
mean_policy_pred += policy_loss.mean().item()
|
|
||||||
mean_expert_pred += expert_loss.mean().item()
|
|
||||||
# -- RND loss
|
|
||||||
if mean_rnd_loss is not None:
|
|
||||||
mean_rnd_loss += rnd_loss.item()
|
|
||||||
# -- Symmetry loss
|
|
||||||
if mean_symmetry_loss is not None:
|
|
||||||
mean_symmetry_loss += symmetry_loss.item()
|
|
||||||
|
|
||||||
# -- For PPO
|
|
||||||
num_updates = self.num_learning_epochs * self.num_mini_batches
|
|
||||||
mean_value_loss /= num_updates
|
|
||||||
mean_surrogate_loss /= num_updates
|
|
||||||
mean_entropy /= num_updates
|
|
||||||
mean_amp_loss /= num_updates
|
|
||||||
mean_grad_pen_loss /= num_updates
|
|
||||||
mean_policy_pred /= num_updates
|
|
||||||
mean_expert_pred /= num_updates
|
|
||||||
|
|
||||||
# -- For RND
|
|
||||||
if mean_rnd_loss is not None:
|
|
||||||
mean_rnd_loss /= num_updates
|
|
||||||
# -- For Symmetry
|
|
||||||
if mean_symmetry_loss is not None:
|
|
||||||
mean_symmetry_loss /= num_updates
|
|
||||||
# -- Clear the storage
|
|
||||||
self.storage.clear()
|
|
||||||
|
|
||||||
# construct the loss dictionary
|
|
||||||
loss_dict = {
|
|
||||||
"value_function": mean_value_loss,
|
|
||||||
"surrogate": mean_surrogate_loss,
|
|
||||||
"entropy": mean_entropy,
|
|
||||||
"amp": mean_amp_loss,
|
|
||||||
"amp_grad_pen": mean_grad_pen_loss,
|
|
||||||
"amp_policy_pred": mean_policy_pred,
|
|
||||||
"amp_expert_pred": mean_expert_pred,
|
|
||||||
}
|
|
||||||
if self.rnd:
|
|
||||||
loss_dict["rnd"] = mean_rnd_loss
|
|
||||||
if self.symmetry:
|
|
||||||
loss_dict["symmetry"] = mean_symmetry_loss
|
|
||||||
|
|
||||||
return loss_dict
|
|
||||||
|
|
||||||
"""
|
|
||||||
Helper functions
|
|
||||||
"""
|
|
||||||
|
|
||||||
def broadcast_parameters(self):
|
|
||||||
"""Broadcast model parameters to all GPUs."""
|
|
||||||
# obtain the model parameters on current GPU
|
|
||||||
model_params = [self.policy.state_dict()]
|
|
||||||
if self.rnd:
|
|
||||||
model_params.append(self.rnd.predictor.state_dict())
|
|
||||||
# broadcast the model parameters
|
|
||||||
torch.distributed.broadcast_object_list(model_params, src=0)
|
|
||||||
# load the model parameters on all GPUs from source GPU
|
|
||||||
self.policy.load_state_dict(model_params[0])
|
|
||||||
if self.rnd:
|
|
||||||
self.rnd.predictor.load_state_dict(model_params[1])
|
|
||||||
|
|
||||||
def reduce_parameters(self):
|
|
||||||
"""Collect gradients from all GPUs and average them.
|
|
||||||
|
|
||||||
This function is called after the backward pass to synchronize the gradients across all GPUs.
|
|
||||||
"""
|
|
||||||
# Create a tensor to store the gradients
|
|
||||||
grads = [param.grad.view(-1) for param in self.policy.parameters() if param.grad is not None]
|
|
||||||
if self.rnd:
|
|
||||||
grads += [param.grad.view(-1) for param in self.rnd.parameters() if param.grad is not None]
|
|
||||||
all_grads = torch.cat(grads)
|
|
||||||
|
|
||||||
# Average the gradients across all GPUs
|
|
||||||
torch.distributed.all_reduce(all_grads, op=torch.distributed.ReduceOp.SUM)
|
|
||||||
all_grads /= self.gpu_world_size
|
|
||||||
|
|
||||||
# Get all parameters
|
|
||||||
all_params = self.policy.parameters()
|
|
||||||
if self.rnd:
|
|
||||||
all_params = chain(all_params, self.rnd.parameters())
|
|
||||||
|
|
||||||
# Update the gradients for all parameters with the reduced gradients
|
|
||||||
offset = 0
|
|
||||||
for param in all_params:
|
|
||||||
if param.grad is not None:
|
|
||||||
numel = param.numel()
|
|
||||||
# copy data back from shared buffer
|
|
||||||
param.grad.data.copy_(all_grads[offset : offset + numel].view_as(param.grad.data))
|
|
||||||
# update the offset for the next parameter
|
|
||||||
offset += numel
|
|
||||||
|
|
@ -1,185 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
|
|
||||||
from rsl_rl.modules import StudentTeacher, StudentTeacherRecurrent
|
|
||||||
from rsl_rl.storage import RolloutStorage
|
|
||||||
from rsl_rl.utils import resolve_optimizer
|
|
||||||
|
|
||||||
|
|
||||||
class Distillation:
|
|
||||||
"""Distillation algorithm for training a student model to mimic a teacher model."""
|
|
||||||
|
|
||||||
policy: StudentTeacher | StudentTeacherRecurrent
|
|
||||||
"""The student teacher model."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
policy,
|
|
||||||
num_learning_epochs=1,
|
|
||||||
gradient_length=15,
|
|
||||||
learning_rate=1e-3,
|
|
||||||
max_grad_norm=None,
|
|
||||||
loss_type="mse",
|
|
||||||
optimizer="adam",
|
|
||||||
device="cpu",
|
|
||||||
# Distributed training parameters
|
|
||||||
multi_gpu_cfg: dict | None = None,
|
|
||||||
):
|
|
||||||
# device-related parameters
|
|
||||||
self.device = device
|
|
||||||
self.is_multi_gpu = multi_gpu_cfg is not None
|
|
||||||
# Multi-GPU parameters
|
|
||||||
if multi_gpu_cfg is not None:
|
|
||||||
self.gpu_global_rank = multi_gpu_cfg["global_rank"]
|
|
||||||
self.gpu_world_size = multi_gpu_cfg["world_size"]
|
|
||||||
else:
|
|
||||||
self.gpu_global_rank = 0
|
|
||||||
self.gpu_world_size = 1
|
|
||||||
|
|
||||||
# distillation components
|
|
||||||
self.policy = policy
|
|
||||||
self.policy.to(self.device)
|
|
||||||
self.storage = None # initialized later
|
|
||||||
|
|
||||||
# initialize the optimizer
|
|
||||||
self.optimizer = resolve_optimizer(optimizer)(self.policy.parameters(), lr=learning_rate)
|
|
||||||
|
|
||||||
# initialize the transition
|
|
||||||
self.transition = RolloutStorage.Transition()
|
|
||||||
self.last_hidden_states = None
|
|
||||||
|
|
||||||
# distillation parameters
|
|
||||||
self.num_learning_epochs = num_learning_epochs
|
|
||||||
self.gradient_length = gradient_length
|
|
||||||
self.learning_rate = learning_rate
|
|
||||||
self.max_grad_norm = max_grad_norm
|
|
||||||
|
|
||||||
# initialize the loss function
|
|
||||||
loss_fn_dict = {
|
|
||||||
"mse": nn.functional.mse_loss,
|
|
||||||
"huber": nn.functional.huber_loss,
|
|
||||||
}
|
|
||||||
if loss_type in loss_fn_dict:
|
|
||||||
self.loss_fn = loss_fn_dict[loss_type]
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown loss type: {loss_type}. Supported types are: {list(loss_fn_dict.keys())}")
|
|
||||||
|
|
||||||
self.num_updates = 0
|
|
||||||
|
|
||||||
def init_storage(self, training_type, num_envs, num_transitions_per_env, obs, actions_shape):
|
|
||||||
# create rollout storage
|
|
||||||
self.storage = RolloutStorage(
|
|
||||||
training_type,
|
|
||||||
num_envs,
|
|
||||||
num_transitions_per_env,
|
|
||||||
obs,
|
|
||||||
actions_shape,
|
|
||||||
self.device,
|
|
||||||
)
|
|
||||||
|
|
||||||
def act(self, obs):
|
|
||||||
# compute the actions
|
|
||||||
self.transition.actions = self.policy.act(obs).detach()
|
|
||||||
self.transition.privileged_actions = self.policy.evaluate(obs).detach()
|
|
||||||
# record the observations
|
|
||||||
self.transition.observations = obs
|
|
||||||
return self.transition.actions
|
|
||||||
|
|
||||||
def process_env_step(self, obs, rewards, dones, extras):
|
|
||||||
# update the normalizers
|
|
||||||
self.policy.update_normalization(obs)
|
|
||||||
|
|
||||||
# record the rewards and dones
|
|
||||||
self.transition.rewards = rewards
|
|
||||||
self.transition.dones = dones
|
|
||||||
# record the transition
|
|
||||||
self.storage.add_transitions(self.transition)
|
|
||||||
self.transition.clear()
|
|
||||||
self.policy.reset(dones)
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
self.num_updates += 1
|
|
||||||
mean_behavior_loss = 0
|
|
||||||
loss = 0
|
|
||||||
cnt = 0
|
|
||||||
|
|
||||||
for epoch in range(self.num_learning_epochs):
|
|
||||||
self.policy.reset(hidden_states=self.last_hidden_states)
|
|
||||||
self.policy.detach_hidden_states()
|
|
||||||
for obs, _, privileged_actions, dones in self.storage.generator():
|
|
||||||
|
|
||||||
# inference the student for gradient computation
|
|
||||||
actions = self.policy.act_inference(obs)
|
|
||||||
|
|
||||||
# behavior cloning loss
|
|
||||||
behavior_loss = self.loss_fn(actions, privileged_actions)
|
|
||||||
|
|
||||||
# total loss
|
|
||||||
loss = loss + behavior_loss
|
|
||||||
mean_behavior_loss += behavior_loss.item()
|
|
||||||
cnt += 1
|
|
||||||
|
|
||||||
# gradient step
|
|
||||||
if cnt % self.gradient_length == 0:
|
|
||||||
self.optimizer.zero_grad()
|
|
||||||
loss.backward()
|
|
||||||
if self.is_multi_gpu:
|
|
||||||
self.reduce_parameters()
|
|
||||||
if self.max_grad_norm:
|
|
||||||
nn.utils.clip_grad_norm_(self.policy.student.parameters(), self.max_grad_norm)
|
|
||||||
self.optimizer.step()
|
|
||||||
self.policy.detach_hidden_states()
|
|
||||||
loss = 0
|
|
||||||
|
|
||||||
# reset dones
|
|
||||||
self.policy.reset(dones.view(-1))
|
|
||||||
self.policy.detach_hidden_states(dones.view(-1))
|
|
||||||
|
|
||||||
mean_behavior_loss /= cnt
|
|
||||||
self.storage.clear()
|
|
||||||
self.last_hidden_states = self.policy.get_hidden_states()
|
|
||||||
self.policy.detach_hidden_states()
|
|
||||||
|
|
||||||
# construct the loss dictionary
|
|
||||||
loss_dict = {"behavior": mean_behavior_loss}
|
|
||||||
|
|
||||||
return loss_dict
|
|
||||||
|
|
||||||
"""
|
|
||||||
Helper functions
|
|
||||||
"""
|
|
||||||
|
|
||||||
def broadcast_parameters(self):
|
|
||||||
"""Broadcast model parameters to all GPUs."""
|
|
||||||
# obtain the model parameters on current GPU
|
|
||||||
model_params = [self.policy.state_dict()]
|
|
||||||
# broadcast the model parameters
|
|
||||||
torch.distributed.broadcast_object_list(model_params, src=0)
|
|
||||||
# load the model parameters on all GPUs from source GPU
|
|
||||||
self.policy.load_state_dict(model_params[0])
|
|
||||||
|
|
||||||
def reduce_parameters(self):
|
|
||||||
"""Collect gradients from all GPUs and average them.
|
|
||||||
|
|
||||||
This function is called after the backward pass to synchronize the gradients across all GPUs.
|
|
||||||
"""
|
|
||||||
# Create a tensor to store the gradients
|
|
||||||
grads = [param.grad.view(-1) for param in self.policy.parameters() if param.grad is not None]
|
|
||||||
all_grads = torch.cat(grads)
|
|
||||||
# Average the gradients across all GPUs
|
|
||||||
torch.distributed.all_reduce(all_grads, op=torch.distributed.ReduceOp.SUM)
|
|
||||||
all_grads /= self.gpu_world_size
|
|
||||||
# Update the gradients for all parameters with the reduced gradients
|
|
||||||
offset = 0
|
|
||||||
for param in self.policy.parameters():
|
|
||||||
if param.grad is not None:
|
|
||||||
numel = param.numel()
|
|
||||||
# copy data back from shared buffer
|
|
||||||
param.grad.data.copy_(all_grads[offset : offset + numel].view_as(param.grad.data))
|
|
||||||
# update the offset for the next parameter
|
|
||||||
offset += numel
|
|
||||||
|
|
@ -1,469 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
import torch.optim as optim
|
|
||||||
from itertools import chain
|
|
||||||
|
|
||||||
from rsl_rl.modules import ActorCritic
|
|
||||||
from rsl_rl.modules.rnd import RandomNetworkDistillation
|
|
||||||
from rsl_rl.storage import RolloutStorage
|
|
||||||
from rsl_rl.utils import string_to_callable
|
|
||||||
|
|
||||||
|
|
||||||
class PPO:
|
|
||||||
"""Proximal Policy Optimization algorithm (https://arxiv.org/abs/1707.06347)."""
|
|
||||||
|
|
||||||
policy: ActorCritic
|
|
||||||
"""The actor critic module."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
policy,
|
|
||||||
num_learning_epochs=5,
|
|
||||||
num_mini_batches=4,
|
|
||||||
clip_param=0.2,
|
|
||||||
gamma=0.99,
|
|
||||||
lam=0.95,
|
|
||||||
value_loss_coef=1.0,
|
|
||||||
entropy_coef=0.01,
|
|
||||||
learning_rate=0.001,
|
|
||||||
max_grad_norm=1.0,
|
|
||||||
use_clipped_value_loss=True,
|
|
||||||
schedule="adaptive",
|
|
||||||
desired_kl=0.01,
|
|
||||||
device="cpu",
|
|
||||||
normalize_advantage_per_mini_batch=False,
|
|
||||||
# RND parameters
|
|
||||||
rnd_cfg: dict | None = None,
|
|
||||||
# Symmetry parameters
|
|
||||||
symmetry_cfg: dict | None = None,
|
|
||||||
# Distributed training parameters
|
|
||||||
multi_gpu_cfg: dict | None = None,
|
|
||||||
):
|
|
||||||
# device-related parameters
|
|
||||||
self.device = device
|
|
||||||
self.is_multi_gpu = multi_gpu_cfg is not None
|
|
||||||
# Multi-GPU parameters
|
|
||||||
if multi_gpu_cfg is not None:
|
|
||||||
self.gpu_global_rank = multi_gpu_cfg["global_rank"]
|
|
||||||
self.gpu_world_size = multi_gpu_cfg["world_size"]
|
|
||||||
else:
|
|
||||||
self.gpu_global_rank = 0
|
|
||||||
self.gpu_world_size = 1
|
|
||||||
|
|
||||||
# RND components
|
|
||||||
if rnd_cfg is not None:
|
|
||||||
# Extract parameters used in ppo
|
|
||||||
rnd_lr = rnd_cfg.pop("learning_rate", 1e-3)
|
|
||||||
# Create RND module
|
|
||||||
self.rnd = RandomNetworkDistillation(device=self.device, **rnd_cfg)
|
|
||||||
# Create RND optimizer
|
|
||||||
params = self.rnd.predictor.parameters()
|
|
||||||
self.rnd_optimizer = optim.Adam(params, lr=rnd_lr)
|
|
||||||
else:
|
|
||||||
self.rnd = None
|
|
||||||
self.rnd_optimizer = None
|
|
||||||
|
|
||||||
# Symmetry components
|
|
||||||
if symmetry_cfg is not None:
|
|
||||||
# Check if symmetry is enabled
|
|
||||||
use_symmetry = symmetry_cfg["use_data_augmentation"] or symmetry_cfg["use_mirror_loss"]
|
|
||||||
# Print that we are not using symmetry
|
|
||||||
if not use_symmetry:
|
|
||||||
print("Symmetry not used for learning. We will use it for logging instead.")
|
|
||||||
# If function is a string then resolve it to a function
|
|
||||||
if isinstance(symmetry_cfg["data_augmentation_func"], str):
|
|
||||||
symmetry_cfg["data_augmentation_func"] = string_to_callable(symmetry_cfg["data_augmentation_func"])
|
|
||||||
# Check valid configuration
|
|
||||||
if symmetry_cfg["use_data_augmentation"] and not callable(symmetry_cfg["data_augmentation_func"]):
|
|
||||||
raise ValueError(
|
|
||||||
"Data augmentation enabled but the function is not callable:"
|
|
||||||
f" {symmetry_cfg['data_augmentation_func']}"
|
|
||||||
)
|
|
||||||
# Store symmetry configuration
|
|
||||||
self.symmetry = symmetry_cfg
|
|
||||||
else:
|
|
||||||
self.symmetry = None
|
|
||||||
|
|
||||||
# PPO components
|
|
||||||
self.policy = policy
|
|
||||||
self.policy.to(self.device)
|
|
||||||
# Create optimizer
|
|
||||||
self.optimizer = optim.Adam(self.policy.parameters(), lr=learning_rate)
|
|
||||||
# Create rollout storage
|
|
||||||
self.storage: RolloutStorage = None # type: ignore
|
|
||||||
self.transition = RolloutStorage.Transition()
|
|
||||||
|
|
||||||
# PPO parameters
|
|
||||||
self.clip_param = clip_param
|
|
||||||
self.num_learning_epochs = num_learning_epochs
|
|
||||||
self.num_mini_batches = num_mini_batches
|
|
||||||
self.value_loss_coef = value_loss_coef
|
|
||||||
self.entropy_coef = entropy_coef
|
|
||||||
self.gamma = gamma
|
|
||||||
self.lam = lam
|
|
||||||
self.max_grad_norm = max_grad_norm
|
|
||||||
self.use_clipped_value_loss = use_clipped_value_loss
|
|
||||||
self.desired_kl = desired_kl
|
|
||||||
self.schedule = schedule
|
|
||||||
self.learning_rate = learning_rate
|
|
||||||
self.normalize_advantage_per_mini_batch = normalize_advantage_per_mini_batch
|
|
||||||
|
|
||||||
def init_storage(self, training_type, num_envs, num_transitions_per_env, obs, actions_shape):
|
|
||||||
# create rollout storage
|
|
||||||
self.storage = RolloutStorage(
|
|
||||||
training_type,
|
|
||||||
num_envs,
|
|
||||||
num_transitions_per_env,
|
|
||||||
obs,
|
|
||||||
actions_shape,
|
|
||||||
self.device,
|
|
||||||
)
|
|
||||||
|
|
||||||
def act(self, obs):
|
|
||||||
if self.policy.is_recurrent:
|
|
||||||
self.transition.hidden_states = self.policy.get_hidden_states()
|
|
||||||
# compute the actions and values
|
|
||||||
self.transition.actions = self.policy.act(obs).detach()
|
|
||||||
self.transition.values = self.policy.evaluate(obs).detach()
|
|
||||||
self.transition.actions_log_prob = self.policy.get_actions_log_prob(self.transition.actions).detach()
|
|
||||||
self.transition.action_mean = self.policy.action_mean.detach()
|
|
||||||
self.transition.action_sigma = self.policy.action_std.detach()
|
|
||||||
# need to record obs before env.step()
|
|
||||||
self.transition.observations = obs
|
|
||||||
return self.transition.actions
|
|
||||||
|
|
||||||
def process_env_step(self, obs, rewards, dones, extras):
|
|
||||||
# update the normalizers
|
|
||||||
self.policy.update_normalization(obs)
|
|
||||||
if self.rnd:
|
|
||||||
self.rnd.update_normalization(obs)
|
|
||||||
|
|
||||||
# Record the rewards and dones
|
|
||||||
# Note: we clone here because later on we bootstrap the rewards based on timeouts
|
|
||||||
self.transition.rewards = rewards.clone()
|
|
||||||
self.transition.dones = dones
|
|
||||||
|
|
||||||
# Compute the intrinsic rewards and add to extrinsic rewards
|
|
||||||
if self.rnd:
|
|
||||||
# Compute the intrinsic rewards
|
|
||||||
self.intrinsic_rewards = self.rnd.get_intrinsic_reward(obs)
|
|
||||||
# Add intrinsic rewards to extrinsic rewards
|
|
||||||
self.transition.rewards += self.intrinsic_rewards
|
|
||||||
|
|
||||||
# Bootstrapping on time outs
|
|
||||||
if "time_outs" in extras:
|
|
||||||
self.transition.rewards += self.gamma * torch.squeeze(
|
|
||||||
self.transition.values * extras["time_outs"].unsqueeze(1).to(self.device), 1
|
|
||||||
)
|
|
||||||
|
|
||||||
# record the transition
|
|
||||||
self.storage.add_transitions(self.transition)
|
|
||||||
self.transition.clear()
|
|
||||||
self.policy.reset(dones)
|
|
||||||
|
|
||||||
def compute_returns(self, obs):
|
|
||||||
# compute value for the last step
|
|
||||||
last_values = self.policy.evaluate(obs).detach()
|
|
||||||
self.storage.compute_returns(
|
|
||||||
last_values, self.gamma, self.lam, normalize_advantage=not self.normalize_advantage_per_mini_batch
|
|
||||||
)
|
|
||||||
|
|
||||||
def update(self): # noqa: C901
|
|
||||||
mean_value_loss = 0
|
|
||||||
mean_surrogate_loss = 0
|
|
||||||
mean_entropy = 0
|
|
||||||
# -- RND loss
|
|
||||||
if self.rnd:
|
|
||||||
mean_rnd_loss = 0
|
|
||||||
else:
|
|
||||||
mean_rnd_loss = None
|
|
||||||
# -- Symmetry loss
|
|
||||||
if self.symmetry:
|
|
||||||
mean_symmetry_loss = 0
|
|
||||||
else:
|
|
||||||
mean_symmetry_loss = None
|
|
||||||
|
|
||||||
# generator for mini batches
|
|
||||||
if self.policy.is_recurrent:
|
|
||||||
generator = self.storage.recurrent_mini_batch_generator(self.num_mini_batches, self.num_learning_epochs)
|
|
||||||
else:
|
|
||||||
generator = self.storage.mini_batch_generator(self.num_mini_batches, self.num_learning_epochs)
|
|
||||||
|
|
||||||
# iterate over batches
|
|
||||||
for (
|
|
||||||
obs_batch,
|
|
||||||
actions_batch,
|
|
||||||
target_values_batch,
|
|
||||||
advantages_batch,
|
|
||||||
returns_batch,
|
|
||||||
old_actions_log_prob_batch,
|
|
||||||
old_mu_batch,
|
|
||||||
old_sigma_batch,
|
|
||||||
hid_states_batch,
|
|
||||||
masks_batch,
|
|
||||||
) in generator:
|
|
||||||
|
|
||||||
# number of augmentations per sample
|
|
||||||
# we start with 1 and increase it if we use symmetry augmentation
|
|
||||||
num_aug = 1
|
|
||||||
# original batch size
|
|
||||||
# we assume policy group is always there and needs augmentation
|
|
||||||
original_batch_size = obs_batch.batch_size[0]
|
|
||||||
|
|
||||||
# check if we should normalize advantages per mini batch
|
|
||||||
if self.normalize_advantage_per_mini_batch:
|
|
||||||
with torch.no_grad():
|
|
||||||
advantages_batch = (advantages_batch - advantages_batch.mean()) / (advantages_batch.std() + 1e-8)
|
|
||||||
|
|
||||||
# Perform symmetric augmentation
|
|
||||||
if self.symmetry and self.symmetry["use_data_augmentation"]:
|
|
||||||
# augmentation using symmetry
|
|
||||||
data_augmentation_func = self.symmetry["data_augmentation_func"]
|
|
||||||
# returned shape: [batch_size * num_aug, ...]
|
|
||||||
obs_batch, actions_batch = data_augmentation_func(
|
|
||||||
obs=obs_batch,
|
|
||||||
actions=actions_batch,
|
|
||||||
env=self.symmetry["_env"],
|
|
||||||
)
|
|
||||||
# compute number of augmentations per sample
|
|
||||||
# we assume policy group is always there and needs augmentation
|
|
||||||
num_aug = int(obs_batch.batch_size[0] / original_batch_size)
|
|
||||||
# repeat the rest of the batch
|
|
||||||
# -- actor
|
|
||||||
old_actions_log_prob_batch = old_actions_log_prob_batch.repeat(num_aug, 1)
|
|
||||||
# -- critic
|
|
||||||
target_values_batch = target_values_batch.repeat(num_aug, 1)
|
|
||||||
advantages_batch = advantages_batch.repeat(num_aug, 1)
|
|
||||||
returns_batch = returns_batch.repeat(num_aug, 1)
|
|
||||||
|
|
||||||
# Recompute actions log prob and entropy for current batch of transitions
|
|
||||||
# Note: we need to do this because we updated the policy with the new parameters
|
|
||||||
# -- actor
|
|
||||||
self.policy.act(obs_batch, masks=masks_batch, hidden_states=hid_states_batch[0])
|
|
||||||
actions_log_prob_batch = self.policy.get_actions_log_prob(actions_batch)
|
|
||||||
# -- critic
|
|
||||||
value_batch = self.policy.evaluate(obs_batch, masks=masks_batch, hidden_states=hid_states_batch[1])
|
|
||||||
# -- entropy
|
|
||||||
# we only keep the entropy of the first augmentation (the original one)
|
|
||||||
mu_batch = self.policy.action_mean[:original_batch_size]
|
|
||||||
sigma_batch = self.policy.action_std[:original_batch_size]
|
|
||||||
entropy_batch = self.policy.entropy[:original_batch_size]
|
|
||||||
|
|
||||||
# KL
|
|
||||||
if self.desired_kl is not None and self.schedule == "adaptive":
|
|
||||||
with torch.inference_mode():
|
|
||||||
kl = torch.sum(
|
|
||||||
torch.log(sigma_batch / old_sigma_batch + 1.0e-5)
|
|
||||||
+ (torch.square(old_sigma_batch) + torch.square(old_mu_batch - mu_batch))
|
|
||||||
/ (2.0 * torch.square(sigma_batch))
|
|
||||||
- 0.5,
|
|
||||||
axis=-1,
|
|
||||||
)
|
|
||||||
kl_mean = torch.mean(kl)
|
|
||||||
|
|
||||||
# Reduce the KL divergence across all GPUs
|
|
||||||
if self.is_multi_gpu:
|
|
||||||
torch.distributed.all_reduce(kl_mean, op=torch.distributed.ReduceOp.SUM)
|
|
||||||
kl_mean /= self.gpu_world_size
|
|
||||||
|
|
||||||
# Update the learning rate
|
|
||||||
# Perform this adaptation only on the main process
|
|
||||||
# TODO: Is this needed? If KL-divergence is the "same" across all GPUs,
|
|
||||||
# then the learning rate should be the same across all GPUs.
|
|
||||||
if self.gpu_global_rank == 0:
|
|
||||||
if kl_mean > self.desired_kl * 2.0:
|
|
||||||
self.learning_rate = max(1e-5, self.learning_rate / 1.5)
|
|
||||||
elif kl_mean < self.desired_kl / 2.0 and kl_mean > 0.0:
|
|
||||||
self.learning_rate = min(1e-2, self.learning_rate * 1.5)
|
|
||||||
|
|
||||||
# Update the learning rate for all GPUs
|
|
||||||
if self.is_multi_gpu:
|
|
||||||
lr_tensor = torch.tensor(self.learning_rate, device=self.device)
|
|
||||||
torch.distributed.broadcast(lr_tensor, src=0)
|
|
||||||
self.learning_rate = lr_tensor.item()
|
|
||||||
|
|
||||||
# Update the learning rate for all parameter groups
|
|
||||||
for param_group in self.optimizer.param_groups:
|
|
||||||
param_group["lr"] = self.learning_rate
|
|
||||||
|
|
||||||
# Surrogate loss
|
|
||||||
ratio = torch.exp(actions_log_prob_batch - torch.squeeze(old_actions_log_prob_batch))
|
|
||||||
surrogate = -torch.squeeze(advantages_batch) * ratio
|
|
||||||
surrogate_clipped = -torch.squeeze(advantages_batch) * torch.clamp(
|
|
||||||
ratio, 1.0 - self.clip_param, 1.0 + self.clip_param
|
|
||||||
)
|
|
||||||
surrogate_loss = torch.max(surrogate, surrogate_clipped).mean()
|
|
||||||
|
|
||||||
# Value function loss
|
|
||||||
if self.use_clipped_value_loss:
|
|
||||||
value_clipped = target_values_batch + (value_batch - target_values_batch).clamp(
|
|
||||||
-self.clip_param, self.clip_param
|
|
||||||
)
|
|
||||||
value_losses = (value_batch - returns_batch).pow(2)
|
|
||||||
value_losses_clipped = (value_clipped - returns_batch).pow(2)
|
|
||||||
value_loss = torch.max(value_losses, value_losses_clipped).mean()
|
|
||||||
else:
|
|
||||||
value_loss = (returns_batch - value_batch).pow(2).mean()
|
|
||||||
|
|
||||||
loss = surrogate_loss + self.value_loss_coef * value_loss - self.entropy_coef * entropy_batch.mean()
|
|
||||||
|
|
||||||
# Symmetry loss
|
|
||||||
if self.symmetry:
|
|
||||||
# obtain the symmetric actions
|
|
||||||
# if we did augmentation before then we don't need to augment again
|
|
||||||
if not self.symmetry["use_data_augmentation"]:
|
|
||||||
data_augmentation_func = self.symmetry["data_augmentation_func"]
|
|
||||||
obs_batch, _ = data_augmentation_func(obs=obs_batch, actions=None, env=self.symmetry["_env"])
|
|
||||||
# compute number of augmentations per sample
|
|
||||||
num_aug = int(obs_batch.shape[0] / original_batch_size)
|
|
||||||
|
|
||||||
# actions predicted by the actor for symmetrically-augmented observations
|
|
||||||
mean_actions_batch = self.policy.act_inference(obs_batch.detach().clone())
|
|
||||||
|
|
||||||
# compute the symmetrically augmented actions
|
|
||||||
# note: we are assuming the first augmentation is the original one.
|
|
||||||
# We do not use the action_batch from earlier since that action was sampled from the distribution.
|
|
||||||
# However, the symmetry loss is computed using the mean of the distribution.
|
|
||||||
action_mean_orig = mean_actions_batch[:original_batch_size]
|
|
||||||
_, actions_mean_symm_batch = data_augmentation_func(
|
|
||||||
obs=None, actions=action_mean_orig, env=self.symmetry["_env"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# compute the loss (we skip the first augmentation as it is the original one)
|
|
||||||
mse_loss = torch.nn.MSELoss()
|
|
||||||
symmetry_loss = mse_loss(
|
|
||||||
mean_actions_batch[original_batch_size:], actions_mean_symm_batch.detach()[original_batch_size:]
|
|
||||||
)
|
|
||||||
# add the loss to the total loss
|
|
||||||
if self.symmetry["use_mirror_loss"]:
|
|
||||||
loss += self.symmetry["mirror_loss_coeff"] * symmetry_loss
|
|
||||||
else:
|
|
||||||
symmetry_loss = symmetry_loss.detach()
|
|
||||||
|
|
||||||
# Random Network Distillation loss
|
|
||||||
# TODO: Move this processing to inside RND module.
|
|
||||||
if self.rnd:
|
|
||||||
# extract the rnd_state
|
|
||||||
# TODO: Check if we still need torch no grad. It is just an affine transformation.
|
|
||||||
with torch.no_grad():
|
|
||||||
rnd_state_batch = self.rnd.get_rnd_state(obs_batch[:original_batch_size])
|
|
||||||
rnd_state_batch = self.rnd.state_normalizer(rnd_state_batch)
|
|
||||||
# predict the embedding and the target
|
|
||||||
predicted_embedding = self.rnd.predictor(rnd_state_batch)
|
|
||||||
target_embedding = self.rnd.target(rnd_state_batch).detach()
|
|
||||||
# compute the loss as the mean squared error
|
|
||||||
mseloss = torch.nn.MSELoss()
|
|
||||||
rnd_loss = mseloss(predicted_embedding, target_embedding)
|
|
||||||
|
|
||||||
# Compute the gradients
|
|
||||||
# -- For PPO
|
|
||||||
self.optimizer.zero_grad()
|
|
||||||
loss.backward()
|
|
||||||
# -- For RND
|
|
||||||
if self.rnd:
|
|
||||||
self.rnd_optimizer.zero_grad() # type: ignore
|
|
||||||
rnd_loss.backward()
|
|
||||||
|
|
||||||
# Collect gradients from all GPUs
|
|
||||||
if self.is_multi_gpu:
|
|
||||||
self.reduce_parameters()
|
|
||||||
|
|
||||||
# Apply the gradients
|
|
||||||
# -- For PPO
|
|
||||||
nn.utils.clip_grad_norm_(self.policy.parameters(), self.max_grad_norm)
|
|
||||||
self.optimizer.step()
|
|
||||||
# -- For RND
|
|
||||||
if self.rnd_optimizer:
|
|
||||||
self.rnd_optimizer.step()
|
|
||||||
|
|
||||||
# Store the losses
|
|
||||||
mean_value_loss += value_loss.item()
|
|
||||||
mean_surrogate_loss += surrogate_loss.item()
|
|
||||||
mean_entropy += entropy_batch.mean().item()
|
|
||||||
# -- RND loss
|
|
||||||
if mean_rnd_loss is not None:
|
|
||||||
mean_rnd_loss += rnd_loss.item()
|
|
||||||
# -- Symmetry loss
|
|
||||||
if mean_symmetry_loss is not None:
|
|
||||||
mean_symmetry_loss += symmetry_loss.item()
|
|
||||||
|
|
||||||
# -- For PPO
|
|
||||||
num_updates = self.num_learning_epochs * self.num_mini_batches
|
|
||||||
mean_value_loss /= num_updates
|
|
||||||
mean_surrogate_loss /= num_updates
|
|
||||||
mean_entropy /= num_updates
|
|
||||||
# -- For RND
|
|
||||||
if mean_rnd_loss is not None:
|
|
||||||
mean_rnd_loss /= num_updates
|
|
||||||
# -- For Symmetry
|
|
||||||
if mean_symmetry_loss is not None:
|
|
||||||
mean_symmetry_loss /= num_updates
|
|
||||||
# -- Clear the storage
|
|
||||||
self.storage.clear()
|
|
||||||
|
|
||||||
# construct the loss dictionary
|
|
||||||
loss_dict = {
|
|
||||||
"value_function": mean_value_loss,
|
|
||||||
"surrogate": mean_surrogate_loss,
|
|
||||||
"entropy": mean_entropy,
|
|
||||||
}
|
|
||||||
if self.rnd:
|
|
||||||
loss_dict["rnd"] = mean_rnd_loss
|
|
||||||
if self.symmetry:
|
|
||||||
loss_dict["symmetry"] = mean_symmetry_loss
|
|
||||||
|
|
||||||
return loss_dict
|
|
||||||
|
|
||||||
"""
|
|
||||||
Helper functions
|
|
||||||
"""
|
|
||||||
|
|
||||||
def broadcast_parameters(self):
|
|
||||||
"""Broadcast model parameters to all GPUs."""
|
|
||||||
# obtain the model parameters on current GPU
|
|
||||||
model_params = [self.policy.state_dict()]
|
|
||||||
if self.rnd:
|
|
||||||
model_params.append(self.rnd.predictor.state_dict())
|
|
||||||
# broadcast the model parameters
|
|
||||||
torch.distributed.broadcast_object_list(model_params, src=0)
|
|
||||||
# load the model parameters on all GPUs from source GPU
|
|
||||||
self.policy.load_state_dict(model_params[0])
|
|
||||||
if self.rnd:
|
|
||||||
self.rnd.predictor.load_state_dict(model_params[1])
|
|
||||||
|
|
||||||
def reduce_parameters(self):
|
|
||||||
"""Collect gradients from all GPUs and average them.
|
|
||||||
|
|
||||||
This function is called after the backward pass to synchronize the gradients across all GPUs.
|
|
||||||
"""
|
|
||||||
# Create a tensor to store the gradients
|
|
||||||
grads = [param.grad.view(-1) for param in self.policy.parameters() if param.grad is not None]
|
|
||||||
if self.rnd:
|
|
||||||
grads += [param.grad.view(-1) for param in self.rnd.parameters() if param.grad is not None]
|
|
||||||
all_grads = torch.cat(grads)
|
|
||||||
|
|
||||||
# Average the gradients across all GPUs
|
|
||||||
torch.distributed.all_reduce(all_grads, op=torch.distributed.ReduceOp.SUM)
|
|
||||||
all_grads /= self.gpu_world_size
|
|
||||||
|
|
||||||
# Get all parameters
|
|
||||||
all_params = self.policy.parameters()
|
|
||||||
if self.rnd:
|
|
||||||
all_params = chain(all_params, self.rnd.parameters())
|
|
||||||
|
|
||||||
# Update the gradients for all parameters with the reduced gradients
|
|
||||||
offset = 0
|
|
||||||
for param in all_params:
|
|
||||||
if param.grad is not None:
|
|
||||||
numel = param.numel()
|
|
||||||
# copy data back from shared buffer
|
|
||||||
param.grad.data.copy_(all_grads[offset : offset + numel].view_as(param.grad.data))
|
|
||||||
# update the offset for the next parameter
|
|
||||||
offset += numel
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
"""Main module for the rsl_rl package."""
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
"""Implementation of different RL agents."""
|
|
||||||
|
|
||||||
from .distillation import Distillation
|
|
||||||
from .ppo import PPO
|
|
||||||
from .amp_ppo import AMP_PPO
|
|
||||||
__all__ = ["PPO", "Distillation", "AMP_PPO"]
|
|
||||||
|
|
@ -1,571 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
from torch._tensor import Tensor
|
|
||||||
from torch._tensor import Tensor
|
|
||||||
from typing import Any
|
|
||||||
from copy import deepcopy
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
import torch.optim as optim
|
|
||||||
from itertools import chain
|
|
||||||
|
|
||||||
from rsl_rl.modules import ActorCritic
|
|
||||||
from rsl_rl.modules.rnd import RandomNetworkDistillation
|
|
||||||
from rsl_rl.storage import RolloutStorage, ReplayBufferMulti
|
|
||||||
from rsl_rl.utils import string_to_callable
|
|
||||||
|
|
||||||
|
|
||||||
class AMP_PPO:
|
|
||||||
"""Proximal Policy Optimization algorithm (https://arxiv.org/abs/1707.06347)."""
|
|
||||||
|
|
||||||
policy: ActorCritic
|
|
||||||
"""The actor critic module."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
policy,
|
|
||||||
discriminator,
|
|
||||||
amp_data,
|
|
||||||
amp_normalizer,
|
|
||||||
amp_num_frames=1,
|
|
||||||
amp_replay_buffer_size=100000,
|
|
||||||
num_learning_epochs=5,
|
|
||||||
num_mini_batches=4,
|
|
||||||
clip_param=0.2,
|
|
||||||
gamma=0.99,
|
|
||||||
lam=0.95,
|
|
||||||
value_loss_coef=1.0,
|
|
||||||
entropy_coef=0.01,
|
|
||||||
learning_rate=0.001,
|
|
||||||
max_grad_norm=1.0,
|
|
||||||
use_clipped_value_loss=True,
|
|
||||||
schedule="adaptive",
|
|
||||||
desired_kl=0.01,
|
|
||||||
device="cpu",
|
|
||||||
normalize_advantage_per_mini_batch=False,
|
|
||||||
# RND parameters
|
|
||||||
rnd_cfg: dict | None = None,
|
|
||||||
# Symmetry parameters
|
|
||||||
symmetry_cfg: dict | None = None,
|
|
||||||
# Distributed training parameters
|
|
||||||
multi_gpu_cfg: dict | None = None,
|
|
||||||
):
|
|
||||||
# device-related parameters
|
|
||||||
self.device = device
|
|
||||||
self.is_multi_gpu = multi_gpu_cfg is not None
|
|
||||||
# Multi-GPU parameters
|
|
||||||
if multi_gpu_cfg is not None:
|
|
||||||
self.gpu_global_rank = multi_gpu_cfg["global_rank"]
|
|
||||||
self.gpu_world_size = multi_gpu_cfg["world_size"]
|
|
||||||
else:
|
|
||||||
self.gpu_global_rank = 0
|
|
||||||
self.gpu_world_size = 1
|
|
||||||
|
|
||||||
# RND components
|
|
||||||
if rnd_cfg is not None:
|
|
||||||
# Extract parameters used in ppo
|
|
||||||
rnd_lr = rnd_cfg.pop("learning_rate", 1e-3)
|
|
||||||
# Create RND module
|
|
||||||
self.rnd = RandomNetworkDistillation(device=self.device, **rnd_cfg)
|
|
||||||
# Create RND optimizer
|
|
||||||
params = self.rnd.predictor.parameters()
|
|
||||||
self.rnd_optimizer = optim.Adam(params, lr=rnd_lr)
|
|
||||||
else:
|
|
||||||
self.rnd = None
|
|
||||||
self.rnd_optimizer = None
|
|
||||||
|
|
||||||
# Symmetry components
|
|
||||||
if symmetry_cfg is not None:
|
|
||||||
# Check if symmetry is enabled
|
|
||||||
use_symmetry = symmetry_cfg["use_data_augmentation"] or symmetry_cfg["use_mirror_loss"]
|
|
||||||
# Print that we are not using symmetry
|
|
||||||
if not use_symmetry:
|
|
||||||
print("Symmetry not used for learning. We will use it for logging instead.")
|
|
||||||
# If function is a string then resolve it to a function
|
|
||||||
if isinstance(symmetry_cfg["data_augmentation_func"], str):
|
|
||||||
symmetry_cfg["data_augmentation_func"] = string_to_callable(symmetry_cfg["data_augmentation_func"])
|
|
||||||
# Check valid configuration
|
|
||||||
if symmetry_cfg["use_data_augmentation"] and not callable(symmetry_cfg["data_augmentation_func"]):
|
|
||||||
raise ValueError(
|
|
||||||
"Data augmentation enabled but the function is not callable:"
|
|
||||||
f" {symmetry_cfg['data_augmentation_func']}"
|
|
||||||
)
|
|
||||||
# Store symmetry configuration
|
|
||||||
self.symmetry = symmetry_cfg
|
|
||||||
else:
|
|
||||||
self.symmetry = None
|
|
||||||
|
|
||||||
## AMP components
|
|
||||||
self.discriminator = discriminator
|
|
||||||
self.discriminator.to(self.device)
|
|
||||||
|
|
||||||
self.amp_storage = ReplayBufferMulti(discriminator.state_dim, amp_replay_buffer_size, amp_num_frames, device)
|
|
||||||
self.amp_data = amp_data
|
|
||||||
self.amp_normalizer = amp_normalizer
|
|
||||||
|
|
||||||
# PPO components
|
|
||||||
self.policy = policy
|
|
||||||
self.policy.to(self.device)
|
|
||||||
|
|
||||||
# Create rollout storage
|
|
||||||
self.storage: RolloutStorage = None # type: ignore
|
|
||||||
self.transition = RolloutStorage.Transition()
|
|
||||||
self.amp_transition = RolloutStorage.Transition()
|
|
||||||
params = [
|
|
||||||
{'params': self.policy.parameters(), 'name': 'policy'},
|
|
||||||
]
|
|
||||||
|
|
||||||
params.append({
|
|
||||||
'params': self.discriminator.trunk.parameters(),
|
|
||||||
'weight_decay': 10e-4,
|
|
||||||
'name': f'amp_trunk'
|
|
||||||
})
|
|
||||||
params.append({
|
|
||||||
'params': self.discriminator.amp_linear.parameters(),
|
|
||||||
'weight_decay': 10e-2,
|
|
||||||
'name': f'amp_head'
|
|
||||||
})
|
|
||||||
|
|
||||||
# Create optimizer
|
|
||||||
self.optimizer = optim.Adam(params, lr=learning_rate)
|
|
||||||
|
|
||||||
# PPO parameters
|
|
||||||
self.clip_param = clip_param
|
|
||||||
self.num_learning_epochs = num_learning_epochs
|
|
||||||
self.num_mini_batches = num_mini_batches
|
|
||||||
self.value_loss_coef = value_loss_coef
|
|
||||||
self.entropy_coef = entropy_coef
|
|
||||||
self.gamma = gamma
|
|
||||||
self.lam = lam
|
|
||||||
self.max_grad_norm = max_grad_norm
|
|
||||||
self.use_clipped_value_loss = use_clipped_value_loss
|
|
||||||
self.desired_kl = desired_kl
|
|
||||||
self.schedule = schedule
|
|
||||||
self.learning_rate = learning_rate
|
|
||||||
self.normalize_advantage_per_mini_batch = normalize_advantage_per_mini_batch
|
|
||||||
|
|
||||||
|
|
||||||
def init_storage(self, training_type, num_envs, num_transitions_per_env, obs, actions_shape):
|
|
||||||
# create rollout storage
|
|
||||||
self.storage = RolloutStorage(
|
|
||||||
training_type,
|
|
||||||
num_envs,
|
|
||||||
num_transitions_per_env,
|
|
||||||
obs,
|
|
||||||
actions_shape,
|
|
||||||
self.device,
|
|
||||||
)
|
|
||||||
|
|
||||||
def act(self, obs, amp_obs):
|
|
||||||
if self.policy.is_recurrent:
|
|
||||||
self.transition.hidden_states = self.policy.get_hidden_states()
|
|
||||||
# compute the actions and values
|
|
||||||
self.transition.actions = self.policy.act(obs).detach()
|
|
||||||
self.transition.values = self.policy.evaluate(obs).detach()
|
|
||||||
self.transition.actions_log_prob = self.policy.get_actions_log_prob(self.transition.actions).detach()
|
|
||||||
self.transition.action_mean = self.policy.action_mean.detach()
|
|
||||||
self.transition.action_sigma = self.policy.action_std.detach()
|
|
||||||
# need to record obs before env.step()
|
|
||||||
self.transition.observations = obs
|
|
||||||
self.amp_transition.observations = amp_obs
|
|
||||||
return self.transition.actions
|
|
||||||
|
|
||||||
def process_env_step(self, obs, rewards, dones, extras,amp_obs, amp_obs_frames=None):
|
|
||||||
# update the normalizers
|
|
||||||
self.policy.update_normalization(obs)
|
|
||||||
if self.rnd:
|
|
||||||
self.rnd.update_normalization(obs)
|
|
||||||
|
|
||||||
# Record the rewards and dones
|
|
||||||
# Note: we clone here because later on we bootstrap the rewards based on timeouts
|
|
||||||
self.transition.rewards = rewards.clone()
|
|
||||||
self.transition.dones = dones
|
|
||||||
|
|
||||||
# Compute the intrinsic rewards and add to extrinsic rewards
|
|
||||||
if self.rnd:
|
|
||||||
# Compute the intrinsic rewards
|
|
||||||
self.intrinsic_rewards = self.rnd.get_intrinsic_reward(obs)
|
|
||||||
# Add intrinsic rewards to extrinsic rewards
|
|
||||||
self.transition.rewards += self.intrinsic_rewards
|
|
||||||
|
|
||||||
# Bootstrapping on time outs
|
|
||||||
if "time_outs" in extras:
|
|
||||||
self.transition.rewards += self.gamma * torch.squeeze(
|
|
||||||
self.transition.values * extras["time_outs"].unsqueeze(1).to(self.device), 1
|
|
||||||
)
|
|
||||||
|
|
||||||
if amp_obs_frames is not None:
|
|
||||||
self.amp_storage.insert(amp_obs_frames)
|
|
||||||
else:
|
|
||||||
self.amp_storage.insert(self.amp_transition.observations, amp_obs)
|
|
||||||
|
|
||||||
# record the transition
|
|
||||||
self.storage.add_transitions(self.transition)
|
|
||||||
self.transition.clear()
|
|
||||||
self.amp_transition.clear()
|
|
||||||
self.policy.reset(dones)
|
|
||||||
|
|
||||||
def compute_returns(self, obs):
|
|
||||||
# compute value for the last step
|
|
||||||
last_values = self.policy.evaluate(obs).detach()
|
|
||||||
self.storage.compute_returns(
|
|
||||||
last_values, self.gamma, self.lam, normalize_advantage=not self.normalize_advantage_per_mini_batch
|
|
||||||
)
|
|
||||||
|
|
||||||
def update(self): # noqa: C901
|
|
||||||
mean_value_loss = 0
|
|
||||||
mean_surrogate_loss = 0
|
|
||||||
mean_entropy = 0
|
|
||||||
mean_amp_loss = 0
|
|
||||||
mean_grad_pen_loss = 0
|
|
||||||
mean_policy_pred = 0
|
|
||||||
mean_expert_pred = 0
|
|
||||||
# -- RND loss
|
|
||||||
if self.rnd:
|
|
||||||
mean_rnd_loss = 0
|
|
||||||
else:
|
|
||||||
mean_rnd_loss = None
|
|
||||||
# -- Symmetry loss
|
|
||||||
if self.symmetry:
|
|
||||||
mean_symmetry_loss = 0
|
|
||||||
else:
|
|
||||||
mean_symmetry_loss = None
|
|
||||||
|
|
||||||
# generator for mini batches
|
|
||||||
if self.policy.is_recurrent:
|
|
||||||
generator = self.storage.recurrent_mini_batch_generator(self.num_mini_batches, self.num_learning_epochs)
|
|
||||||
else:
|
|
||||||
generator = self.storage.mini_batch_generator(self.num_mini_batches, self.num_learning_epochs)
|
|
||||||
|
|
||||||
|
|
||||||
amp_policy_generator = self.amp_storage.feed_forward_generator(
|
|
||||||
self.num_learning_epochs * self.num_mini_batches,
|
|
||||||
self.storage.num_envs * self.storage.num_transitions_per_env // self.num_mini_batches,
|
|
||||||
)
|
|
||||||
|
|
||||||
amp_expert_generator = self.amp_data.feed_forward_generator_23dof_multi(
|
|
||||||
self.num_learning_epochs * self.num_mini_batches,
|
|
||||||
self.storage.num_envs * self.storage.num_transitions_per_env // self.num_mini_batches,
|
|
||||||
)
|
|
||||||
|
|
||||||
# iterate over batches
|
|
||||||
for sample, sample_amp_policy, sample_amp_expert in zip(generator, amp_policy_generator, amp_expert_generator):
|
|
||||||
(
|
|
||||||
obs_batch,
|
|
||||||
actions_batch,
|
|
||||||
target_values_batch,
|
|
||||||
advantages_batch,
|
|
||||||
returns_batch,
|
|
||||||
old_actions_log_prob_batch,
|
|
||||||
old_mu_batch,
|
|
||||||
old_sigma_batch,
|
|
||||||
hid_states_batch,
|
|
||||||
masks_batch,
|
|
||||||
) = sample
|
|
||||||
|
|
||||||
# number of augmentations per sample
|
|
||||||
# we start with 1 and increase it if we use symmetry augmentation
|
|
||||||
num_aug = 1
|
|
||||||
# original batch size
|
|
||||||
# we assume policy group is always there and needs augmentation
|
|
||||||
original_batch_size = obs_batch.batch_size[0]
|
|
||||||
|
|
||||||
# check if we should normalize advantages per mini batch
|
|
||||||
if self.normalize_advantage_per_mini_batch:
|
|
||||||
with torch.no_grad():
|
|
||||||
advantages_batch = (advantages_batch - advantages_batch.mean()) / (advantages_batch.std() + 1e-8)
|
|
||||||
|
|
||||||
# Perform symmetric augmentation
|
|
||||||
if self.symmetry and self.symmetry["use_data_augmentation"]:
|
|
||||||
# augmentation using symmetry
|
|
||||||
data_augmentation_func = self.symmetry["data_augmentation_func"]
|
|
||||||
# returned shape: [batch_size * num_aug, ...]
|
|
||||||
obs_batch, actions_batch = data_augmentation_func(
|
|
||||||
obs=obs_batch,
|
|
||||||
actions=actions_batch,
|
|
||||||
env=self.symmetry["_env"],
|
|
||||||
)
|
|
||||||
# compute number of augmentations per sample
|
|
||||||
# we assume policy group is always there and needs augmentation
|
|
||||||
num_aug = int(obs_batch.batch_size[0] / original_batch_size)
|
|
||||||
# repeat the rest of the batch
|
|
||||||
# -- actor
|
|
||||||
old_actions_log_prob_batch = old_actions_log_prob_batch.repeat(num_aug, 1)
|
|
||||||
# -- critic
|
|
||||||
target_values_batch = target_values_batch.repeat(num_aug, 1)
|
|
||||||
advantages_batch = advantages_batch.repeat(num_aug, 1)
|
|
||||||
returns_batch = returns_batch.repeat(num_aug, 1)
|
|
||||||
|
|
||||||
# Recompute actions log prob and entropy for current batch of transitions
|
|
||||||
# Note: we need to do this because we updated the policy with the new parameters
|
|
||||||
# -- actor
|
|
||||||
self.policy.act(obs_batch, masks=masks_batch, hidden_states=hid_states_batch[0])
|
|
||||||
actions_log_prob_batch = self.policy.get_actions_log_prob(actions_batch)
|
|
||||||
# -- critic
|
|
||||||
value_batch = self.policy.evaluate(obs_batch, masks=masks_batch, hidden_states=hid_states_batch[1])
|
|
||||||
# -- entropy
|
|
||||||
# we only keep the entropy of the first augmentation (the original one)
|
|
||||||
mu_batch = self.policy.action_mean[:original_batch_size]
|
|
||||||
sigma_batch = self.policy.action_std[:original_batch_size]
|
|
||||||
entropy_batch = self.policy.entropy[:original_batch_size]
|
|
||||||
|
|
||||||
# KL
|
|
||||||
if self.desired_kl is not None and self.schedule == "adaptive":
|
|
||||||
with torch.inference_mode():
|
|
||||||
kl = torch.sum(
|
|
||||||
torch.log(sigma_batch / old_sigma_batch + 1.0e-5)
|
|
||||||
+ (torch.square(old_sigma_batch) + torch.square(old_mu_batch - mu_batch))
|
|
||||||
/ (2.0 * torch.square(sigma_batch))
|
|
||||||
- 0.5,
|
|
||||||
axis=-1,
|
|
||||||
)
|
|
||||||
kl_mean = torch.mean(kl)
|
|
||||||
|
|
||||||
# Reduce the KL divergence across all GPUs
|
|
||||||
if self.is_multi_gpu:
|
|
||||||
torch.distributed.all_reduce(kl_mean, op=torch.distributed.ReduceOp.SUM)
|
|
||||||
kl_mean /= self.gpu_world_size
|
|
||||||
|
|
||||||
# Update the learning rate
|
|
||||||
# Perform this adaptation only on the main process
|
|
||||||
# TODO: Is this needed? If KL-divergence is the "same" across all GPUs,
|
|
||||||
# then the learning rate should be the same across all GPUs.
|
|
||||||
if self.gpu_global_rank == 0:
|
|
||||||
if kl_mean > self.desired_kl * 2.0:
|
|
||||||
self.learning_rate = max(1e-5, self.learning_rate / 1.5)
|
|
||||||
elif kl_mean < self.desired_kl / 2.0 and kl_mean > 0.0:
|
|
||||||
self.learning_rate = min(1e-2, self.learning_rate * 1.5)
|
|
||||||
|
|
||||||
# Update the learning rate for all GPUs
|
|
||||||
if self.is_multi_gpu:
|
|
||||||
lr_tensor = torch.tensor(self.learning_rate, device=self.device)
|
|
||||||
torch.distributed.broadcast(lr_tensor, src=0)
|
|
||||||
self.learning_rate = lr_tensor.item()
|
|
||||||
|
|
||||||
# Update the learning rate for all parameter groups
|
|
||||||
for param_group in self.optimizer.param_groups:
|
|
||||||
param_group["lr"] = self.learning_rate
|
|
||||||
|
|
||||||
# Surrogate loss
|
|
||||||
ratio = torch.exp(actions_log_prob_batch - torch.squeeze(old_actions_log_prob_batch))
|
|
||||||
surrogate = -torch.squeeze(advantages_batch) * ratio
|
|
||||||
surrogate_clipped = -torch.squeeze(advantages_batch) * torch.clamp(
|
|
||||||
ratio, 1.0 - self.clip_param, 1.0 + self.clip_param
|
|
||||||
)
|
|
||||||
surrogate_loss = torch.max(surrogate, surrogate_clipped).mean()
|
|
||||||
|
|
||||||
# Value function loss
|
|
||||||
if self.use_clipped_value_loss:
|
|
||||||
value_clipped = target_values_batch + (value_batch - target_values_batch).clamp(
|
|
||||||
-self.clip_param, self.clip_param
|
|
||||||
)
|
|
||||||
value_losses = (value_batch - returns_batch).pow(2)
|
|
||||||
value_losses_clipped = (value_clipped - returns_batch).pow(2)
|
|
||||||
value_loss = torch.max(value_losses, value_losses_clipped).mean()
|
|
||||||
else:
|
|
||||||
value_loss = (returns_batch - value_batch).pow(2).mean()
|
|
||||||
|
|
||||||
loss = surrogate_loss + self.value_loss_coef * value_loss - self.entropy_coef * entropy_batch.mean()
|
|
||||||
|
|
||||||
# Symmetry loss
|
|
||||||
if self.symmetry:
|
|
||||||
# obtain the symmetric actions
|
|
||||||
# if we did augmentation before then we don't need to augment again
|
|
||||||
if not self.symmetry["use_data_augmentation"]:
|
|
||||||
data_augmentation_func = self.symmetry["data_augmentation_func"]
|
|
||||||
obs_batch, _ = data_augmentation_func(obs=obs_batch, actions=None, env=self.symmetry["_env"])
|
|
||||||
# compute number of augmentations per sample
|
|
||||||
num_aug = int(obs_batch.shape[0] / original_batch_size)
|
|
||||||
|
|
||||||
# actions predicted by the actor for symmetrically-augmented observations
|
|
||||||
mean_actions_batch = self.policy.act_inference(obs_batch.detach().clone())
|
|
||||||
|
|
||||||
# compute the symmetrically augmented actions
|
|
||||||
# note: we are assuming the first augmentation is the original one.
|
|
||||||
# We do not use the action_batch from earlier since that action was sampled from the distribution.
|
|
||||||
# However, the symmetry loss is computed using the mean of the distribution.
|
|
||||||
action_mean_orig = mean_actions_batch[:original_batch_size]
|
|
||||||
_, actions_mean_symm_batch = data_augmentation_func(
|
|
||||||
obs=None, actions=action_mean_orig, env=self.symmetry["_env"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# compute the loss (we skip the first augmentation as it is the original one)
|
|
||||||
mse_loss = torch.nn.MSELoss()
|
|
||||||
symmetry_loss = mse_loss(
|
|
||||||
mean_actions_batch[original_batch_size:], actions_mean_symm_batch.detach()[original_batch_size:]
|
|
||||||
)
|
|
||||||
# add the loss to the total loss
|
|
||||||
if self.symmetry["use_mirror_loss"]:
|
|
||||||
loss += self.symmetry["mirror_loss_coeff"] * symmetry_loss
|
|
||||||
else:
|
|
||||||
symmetry_loss = symmetry_loss.detach()
|
|
||||||
|
|
||||||
# Random Network Distillation loss
|
|
||||||
# TODO: Move this processing to inside RND module.
|
|
||||||
if self.rnd:
|
|
||||||
# extract the rnd_state
|
|
||||||
# TODO: Check if we still need torch no grad. It is just an affine transformation.
|
|
||||||
with torch.no_grad():
|
|
||||||
rnd_state_batch = self.rnd.get_rnd_state(obs_batch[:original_batch_size])
|
|
||||||
rnd_state_batch = self.rnd.state_normalizer(rnd_state_batch)
|
|
||||||
# predict the embedding and the target
|
|
||||||
predicted_embedding = self.rnd.predictor(rnd_state_batch)
|
|
||||||
target_embedding = self.rnd.target(rnd_state_batch).detach()
|
|
||||||
# compute the loss as the mean squared error
|
|
||||||
mseloss = torch.nn.MSELoss()
|
|
||||||
rnd_loss = mseloss(predicted_embedding, target_embedding)
|
|
||||||
|
|
||||||
expert_states = sample_amp_expert
|
|
||||||
policy_states = sample_amp_policy
|
|
||||||
|
|
||||||
with torch.no_grad():
|
|
||||||
expert_states = self.amp_normalizer.normalize_torch(expert_states.to(self.device), self.device)
|
|
||||||
policy_states = self.amp_normalizer.normalize_torch(policy_states, self.device)
|
|
||||||
|
|
||||||
contact_phase_push = obs_batch['critic'][:, -4]
|
|
||||||
mask_push = contact_phase_push == 1.
|
|
||||||
|
|
||||||
if mask_push.any():
|
|
||||||
policy_d = self.discriminator(policy_states.flatten(1))
|
|
||||||
expert_states = expert_states.to(self.device)
|
|
||||||
expert_d = self.discriminator(expert_states.flatten(1))
|
|
||||||
|
|
||||||
expert_loss = torch.nn.MSELoss()(expert_d, torch.ones(expert_d.size(), device=self.device))
|
|
||||||
policy_loss = torch.nn.MSELoss()(policy_d, -1 * torch.ones(policy_d.size(), device=self.device))
|
|
||||||
amp_loss = 0.5 * (expert_loss + policy_loss)
|
|
||||||
|
|
||||||
# grad penalty
|
|
||||||
grad_pen_loss = self.discriminator.compute_grad_pen(expert_states, lambda_=5)
|
|
||||||
else:
|
|
||||||
amp_loss = torch.tensor(0.0, device=self.device)
|
|
||||||
grad_pen_loss = torch.tensor(0.0, device=self.device)
|
|
||||||
expert_loss = torch.tensor(0.0, device=self.device)
|
|
||||||
policy_loss = torch.tensor(0.0, device=self.device)
|
|
||||||
|
|
||||||
loss += (amp_loss + grad_pen_loss)
|
|
||||||
self.amp_normalizer.update(policy_states.cpu().numpy())
|
|
||||||
self.amp_normalizer.update(expert_states.cpu().numpy())
|
|
||||||
|
|
||||||
# Compute the gradients
|
|
||||||
# -- For PPO
|
|
||||||
self.optimizer.zero_grad()
|
|
||||||
loss.backward()
|
|
||||||
# -- For RND
|
|
||||||
if self.rnd:
|
|
||||||
self.rnd_optimizer.zero_grad() # type: ignore
|
|
||||||
rnd_loss.backward()
|
|
||||||
|
|
||||||
# Collect gradients from all GPUs
|
|
||||||
if self.is_multi_gpu:
|
|
||||||
self.reduce_parameters()
|
|
||||||
|
|
||||||
# Apply the gradients
|
|
||||||
# -- For PPO
|
|
||||||
nn.utils.clip_grad_norm_(self.policy.parameters(), self.max_grad_norm)
|
|
||||||
self.optimizer.step()
|
|
||||||
# -- For RND
|
|
||||||
if self.rnd_optimizer:
|
|
||||||
self.rnd_optimizer.step()
|
|
||||||
|
|
||||||
# Store the losses
|
|
||||||
mean_value_loss += value_loss.item()
|
|
||||||
mean_surrogate_loss += surrogate_loss.item()
|
|
||||||
mean_entropy += entropy_batch.mean().item()
|
|
||||||
mean_amp_loss += amp_loss.item()
|
|
||||||
mean_grad_pen_loss += grad_pen_loss.item()
|
|
||||||
mean_policy_pred += policy_loss.mean().item()
|
|
||||||
mean_expert_pred += expert_loss.mean().item()
|
|
||||||
# -- RND loss
|
|
||||||
if mean_rnd_loss is not None:
|
|
||||||
mean_rnd_loss += rnd_loss.item()
|
|
||||||
# -- Symmetry loss
|
|
||||||
if mean_symmetry_loss is not None:
|
|
||||||
mean_symmetry_loss += symmetry_loss.item()
|
|
||||||
|
|
||||||
# -- For PPO
|
|
||||||
num_updates = self.num_learning_epochs * self.num_mini_batches
|
|
||||||
mean_value_loss /= num_updates
|
|
||||||
mean_surrogate_loss /= num_updates
|
|
||||||
mean_entropy /= num_updates
|
|
||||||
mean_amp_loss /= num_updates
|
|
||||||
mean_grad_pen_loss /= num_updates
|
|
||||||
mean_policy_pred /= num_updates
|
|
||||||
mean_expert_pred /= num_updates
|
|
||||||
|
|
||||||
# -- For RND
|
|
||||||
if mean_rnd_loss is not None:
|
|
||||||
mean_rnd_loss /= num_updates
|
|
||||||
# -- For Symmetry
|
|
||||||
if mean_symmetry_loss is not None:
|
|
||||||
mean_symmetry_loss /= num_updates
|
|
||||||
# -- Clear the storage
|
|
||||||
self.storage.clear()
|
|
||||||
|
|
||||||
# construct the loss dictionary
|
|
||||||
loss_dict = {
|
|
||||||
"value_function": mean_value_loss,
|
|
||||||
"surrogate": mean_surrogate_loss,
|
|
||||||
"entropy": mean_entropy,
|
|
||||||
"amp": mean_amp_loss,
|
|
||||||
"amp_grad_pen": mean_grad_pen_loss,
|
|
||||||
"amp_policy_pred": mean_policy_pred,
|
|
||||||
"amp_expert_pred": mean_expert_pred,
|
|
||||||
}
|
|
||||||
if self.rnd:
|
|
||||||
loss_dict["rnd"] = mean_rnd_loss
|
|
||||||
if self.symmetry:
|
|
||||||
loss_dict["symmetry"] = mean_symmetry_loss
|
|
||||||
|
|
||||||
return loss_dict
|
|
||||||
|
|
||||||
"""
|
|
||||||
Helper functions
|
|
||||||
"""
|
|
||||||
|
|
||||||
def broadcast_parameters(self):
|
|
||||||
"""Broadcast model parameters to all GPUs."""
|
|
||||||
# obtain the model parameters on current GPU
|
|
||||||
model_params = [self.policy.state_dict()]
|
|
||||||
if self.rnd:
|
|
||||||
model_params.append(self.rnd.predictor.state_dict())
|
|
||||||
# broadcast the model parameters
|
|
||||||
torch.distributed.broadcast_object_list(model_params, src=0)
|
|
||||||
# load the model parameters on all GPUs from source GPU
|
|
||||||
self.policy.load_state_dict(model_params[0])
|
|
||||||
if self.rnd:
|
|
||||||
self.rnd.predictor.load_state_dict(model_params[1])
|
|
||||||
|
|
||||||
def reduce_parameters(self):
|
|
||||||
"""Collect gradients from all GPUs and average them.
|
|
||||||
|
|
||||||
This function is called after the backward pass to synchronize the gradients across all GPUs.
|
|
||||||
"""
|
|
||||||
# Create a tensor to store the gradients
|
|
||||||
grads = [param.grad.view(-1) for param in self.policy.parameters() if param.grad is not None]
|
|
||||||
if self.rnd:
|
|
||||||
grads += [param.grad.view(-1) for param in self.rnd.parameters() if param.grad is not None]
|
|
||||||
all_grads = torch.cat(grads)
|
|
||||||
|
|
||||||
# Average the gradients across all GPUs
|
|
||||||
torch.distributed.all_reduce(all_grads, op=torch.distributed.ReduceOp.SUM)
|
|
||||||
all_grads /= self.gpu_world_size
|
|
||||||
|
|
||||||
# Get all parameters
|
|
||||||
all_params = self.policy.parameters()
|
|
||||||
if self.rnd:
|
|
||||||
all_params = chain(all_params, self.rnd.parameters())
|
|
||||||
|
|
||||||
# Update the gradients for all parameters with the reduced gradients
|
|
||||||
offset = 0
|
|
||||||
for param in all_params:
|
|
||||||
if param.grad is not None:
|
|
||||||
numel = param.numel()
|
|
||||||
# copy data back from shared buffer
|
|
||||||
param.grad.data.copy_(all_grads[offset : offset + numel].view_as(param.grad.data))
|
|
||||||
# update the offset for the next parameter
|
|
||||||
offset += numel
|
|
||||||
|
|
@ -1,185 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
|
|
||||||
from rsl_rl.modules import StudentTeacher, StudentTeacherRecurrent
|
|
||||||
from rsl_rl.storage import RolloutStorage
|
|
||||||
from rsl_rl.utils import resolve_optimizer
|
|
||||||
|
|
||||||
|
|
||||||
class Distillation:
|
|
||||||
"""Distillation algorithm for training a student model to mimic a teacher model."""
|
|
||||||
|
|
||||||
policy: StudentTeacher | StudentTeacherRecurrent
|
|
||||||
"""The student teacher model."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
policy,
|
|
||||||
num_learning_epochs=1,
|
|
||||||
gradient_length=15,
|
|
||||||
learning_rate=1e-3,
|
|
||||||
max_grad_norm=None,
|
|
||||||
loss_type="mse",
|
|
||||||
optimizer="adam",
|
|
||||||
device="cpu",
|
|
||||||
# Distributed training parameters
|
|
||||||
multi_gpu_cfg: dict | None = None,
|
|
||||||
):
|
|
||||||
# device-related parameters
|
|
||||||
self.device = device
|
|
||||||
self.is_multi_gpu = multi_gpu_cfg is not None
|
|
||||||
# Multi-GPU parameters
|
|
||||||
if multi_gpu_cfg is not None:
|
|
||||||
self.gpu_global_rank = multi_gpu_cfg["global_rank"]
|
|
||||||
self.gpu_world_size = multi_gpu_cfg["world_size"]
|
|
||||||
else:
|
|
||||||
self.gpu_global_rank = 0
|
|
||||||
self.gpu_world_size = 1
|
|
||||||
|
|
||||||
# distillation components
|
|
||||||
self.policy = policy
|
|
||||||
self.policy.to(self.device)
|
|
||||||
self.storage = None # initialized later
|
|
||||||
|
|
||||||
# initialize the optimizer
|
|
||||||
self.optimizer = resolve_optimizer(optimizer)(self.policy.parameters(), lr=learning_rate)
|
|
||||||
|
|
||||||
# initialize the transition
|
|
||||||
self.transition = RolloutStorage.Transition()
|
|
||||||
self.last_hidden_states = None
|
|
||||||
|
|
||||||
# distillation parameters
|
|
||||||
self.num_learning_epochs = num_learning_epochs
|
|
||||||
self.gradient_length = gradient_length
|
|
||||||
self.learning_rate = learning_rate
|
|
||||||
self.max_grad_norm = max_grad_norm
|
|
||||||
|
|
||||||
# initialize the loss function
|
|
||||||
loss_fn_dict = {
|
|
||||||
"mse": nn.functional.mse_loss,
|
|
||||||
"huber": nn.functional.huber_loss,
|
|
||||||
}
|
|
||||||
if loss_type in loss_fn_dict:
|
|
||||||
self.loss_fn = loss_fn_dict[loss_type]
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown loss type: {loss_type}. Supported types are: {list(loss_fn_dict.keys())}")
|
|
||||||
|
|
||||||
self.num_updates = 0
|
|
||||||
|
|
||||||
def init_storage(self, training_type, num_envs, num_transitions_per_env, obs, actions_shape):
|
|
||||||
# create rollout storage
|
|
||||||
self.storage = RolloutStorage(
|
|
||||||
training_type,
|
|
||||||
num_envs,
|
|
||||||
num_transitions_per_env,
|
|
||||||
obs,
|
|
||||||
actions_shape,
|
|
||||||
self.device,
|
|
||||||
)
|
|
||||||
|
|
||||||
def act(self, obs):
|
|
||||||
# compute the actions
|
|
||||||
self.transition.actions = self.policy.act(obs).detach()
|
|
||||||
self.transition.privileged_actions = self.policy.evaluate(obs).detach()
|
|
||||||
# record the observations
|
|
||||||
self.transition.observations = obs
|
|
||||||
return self.transition.actions
|
|
||||||
|
|
||||||
def process_env_step(self, obs, rewards, dones, extras):
|
|
||||||
# update the normalizers
|
|
||||||
self.policy.update_normalization(obs)
|
|
||||||
|
|
||||||
# record the rewards and dones
|
|
||||||
self.transition.rewards = rewards
|
|
||||||
self.transition.dones = dones
|
|
||||||
# record the transition
|
|
||||||
self.storage.add_transitions(self.transition)
|
|
||||||
self.transition.clear()
|
|
||||||
self.policy.reset(dones)
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
self.num_updates += 1
|
|
||||||
mean_behavior_loss = 0
|
|
||||||
loss = 0
|
|
||||||
cnt = 0
|
|
||||||
|
|
||||||
for epoch in range(self.num_learning_epochs):
|
|
||||||
self.policy.reset(hidden_states=self.last_hidden_states)
|
|
||||||
self.policy.detach_hidden_states()
|
|
||||||
for obs, _, privileged_actions, dones in self.storage.generator():
|
|
||||||
|
|
||||||
# inference the student for gradient computation
|
|
||||||
actions = self.policy.act_inference(obs)
|
|
||||||
|
|
||||||
# behavior cloning loss
|
|
||||||
behavior_loss = self.loss_fn(actions, privileged_actions)
|
|
||||||
|
|
||||||
# total loss
|
|
||||||
loss = loss + behavior_loss
|
|
||||||
mean_behavior_loss += behavior_loss.item()
|
|
||||||
cnt += 1
|
|
||||||
|
|
||||||
# gradient step
|
|
||||||
if cnt % self.gradient_length == 0:
|
|
||||||
self.optimizer.zero_grad()
|
|
||||||
loss.backward()
|
|
||||||
if self.is_multi_gpu:
|
|
||||||
self.reduce_parameters()
|
|
||||||
if self.max_grad_norm:
|
|
||||||
nn.utils.clip_grad_norm_(self.policy.student.parameters(), self.max_grad_norm)
|
|
||||||
self.optimizer.step()
|
|
||||||
self.policy.detach_hidden_states()
|
|
||||||
loss = 0
|
|
||||||
|
|
||||||
# reset dones
|
|
||||||
self.policy.reset(dones.view(-1))
|
|
||||||
self.policy.detach_hidden_states(dones.view(-1))
|
|
||||||
|
|
||||||
mean_behavior_loss /= cnt
|
|
||||||
self.storage.clear()
|
|
||||||
self.last_hidden_states = self.policy.get_hidden_states()
|
|
||||||
self.policy.detach_hidden_states()
|
|
||||||
|
|
||||||
# construct the loss dictionary
|
|
||||||
loss_dict = {"behavior": mean_behavior_loss}
|
|
||||||
|
|
||||||
return loss_dict
|
|
||||||
|
|
||||||
"""
|
|
||||||
Helper functions
|
|
||||||
"""
|
|
||||||
|
|
||||||
def broadcast_parameters(self):
|
|
||||||
"""Broadcast model parameters to all GPUs."""
|
|
||||||
# obtain the model parameters on current GPU
|
|
||||||
model_params = [self.policy.state_dict()]
|
|
||||||
# broadcast the model parameters
|
|
||||||
torch.distributed.broadcast_object_list(model_params, src=0)
|
|
||||||
# load the model parameters on all GPUs from source GPU
|
|
||||||
self.policy.load_state_dict(model_params[0])
|
|
||||||
|
|
||||||
def reduce_parameters(self):
|
|
||||||
"""Collect gradients from all GPUs and average them.
|
|
||||||
|
|
||||||
This function is called after the backward pass to synchronize the gradients across all GPUs.
|
|
||||||
"""
|
|
||||||
# Create a tensor to store the gradients
|
|
||||||
grads = [param.grad.view(-1) for param in self.policy.parameters() if param.grad is not None]
|
|
||||||
all_grads = torch.cat(grads)
|
|
||||||
# Average the gradients across all GPUs
|
|
||||||
torch.distributed.all_reduce(all_grads, op=torch.distributed.ReduceOp.SUM)
|
|
||||||
all_grads /= self.gpu_world_size
|
|
||||||
# Update the gradients for all parameters with the reduced gradients
|
|
||||||
offset = 0
|
|
||||||
for param in self.policy.parameters():
|
|
||||||
if param.grad is not None:
|
|
||||||
numel = param.numel()
|
|
||||||
# copy data back from shared buffer
|
|
||||||
param.grad.data.copy_(all_grads[offset : offset + numel].view_as(param.grad.data))
|
|
||||||
# update the offset for the next parameter
|
|
||||||
offset += numel
|
|
||||||
|
|
@ -1,469 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
import torch.optim as optim
|
|
||||||
from itertools import chain
|
|
||||||
|
|
||||||
from rsl_rl.modules import ActorCritic
|
|
||||||
from rsl_rl.modules.rnd import RandomNetworkDistillation
|
|
||||||
from rsl_rl.storage import RolloutStorage
|
|
||||||
from rsl_rl.utils import string_to_callable
|
|
||||||
|
|
||||||
|
|
||||||
class PPO:
|
|
||||||
"""Proximal Policy Optimization algorithm (https://arxiv.org/abs/1707.06347)."""
|
|
||||||
|
|
||||||
policy: ActorCritic
|
|
||||||
"""The actor critic module."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
policy,
|
|
||||||
num_learning_epochs=5,
|
|
||||||
num_mini_batches=4,
|
|
||||||
clip_param=0.2,
|
|
||||||
gamma=0.99,
|
|
||||||
lam=0.95,
|
|
||||||
value_loss_coef=1.0,
|
|
||||||
entropy_coef=0.01,
|
|
||||||
learning_rate=0.001,
|
|
||||||
max_grad_norm=1.0,
|
|
||||||
use_clipped_value_loss=True,
|
|
||||||
schedule="adaptive",
|
|
||||||
desired_kl=0.01,
|
|
||||||
device="cpu",
|
|
||||||
normalize_advantage_per_mini_batch=False,
|
|
||||||
# RND parameters
|
|
||||||
rnd_cfg: dict | None = None,
|
|
||||||
# Symmetry parameters
|
|
||||||
symmetry_cfg: dict | None = None,
|
|
||||||
# Distributed training parameters
|
|
||||||
multi_gpu_cfg: dict | None = None,
|
|
||||||
):
|
|
||||||
# device-related parameters
|
|
||||||
self.device = device
|
|
||||||
self.is_multi_gpu = multi_gpu_cfg is not None
|
|
||||||
# Multi-GPU parameters
|
|
||||||
if multi_gpu_cfg is not None:
|
|
||||||
self.gpu_global_rank = multi_gpu_cfg["global_rank"]
|
|
||||||
self.gpu_world_size = multi_gpu_cfg["world_size"]
|
|
||||||
else:
|
|
||||||
self.gpu_global_rank = 0
|
|
||||||
self.gpu_world_size = 1
|
|
||||||
|
|
||||||
# RND components
|
|
||||||
if rnd_cfg is not None:
|
|
||||||
# Extract parameters used in ppo
|
|
||||||
rnd_lr = rnd_cfg.pop("learning_rate", 1e-3)
|
|
||||||
# Create RND module
|
|
||||||
self.rnd = RandomNetworkDistillation(device=self.device, **rnd_cfg)
|
|
||||||
# Create RND optimizer
|
|
||||||
params = self.rnd.predictor.parameters()
|
|
||||||
self.rnd_optimizer = optim.Adam(params, lr=rnd_lr)
|
|
||||||
else:
|
|
||||||
self.rnd = None
|
|
||||||
self.rnd_optimizer = None
|
|
||||||
|
|
||||||
# Symmetry components
|
|
||||||
if symmetry_cfg is not None:
|
|
||||||
# Check if symmetry is enabled
|
|
||||||
use_symmetry = symmetry_cfg["use_data_augmentation"] or symmetry_cfg["use_mirror_loss"]
|
|
||||||
# Print that we are not using symmetry
|
|
||||||
if not use_symmetry:
|
|
||||||
print("Symmetry not used for learning. We will use it for logging instead.")
|
|
||||||
# If function is a string then resolve it to a function
|
|
||||||
if isinstance(symmetry_cfg["data_augmentation_func"], str):
|
|
||||||
symmetry_cfg["data_augmentation_func"] = string_to_callable(symmetry_cfg["data_augmentation_func"])
|
|
||||||
# Check valid configuration
|
|
||||||
if symmetry_cfg["use_data_augmentation"] and not callable(symmetry_cfg["data_augmentation_func"]):
|
|
||||||
raise ValueError(
|
|
||||||
"Data augmentation enabled but the function is not callable:"
|
|
||||||
f" {symmetry_cfg['data_augmentation_func']}"
|
|
||||||
)
|
|
||||||
# Store symmetry configuration
|
|
||||||
self.symmetry = symmetry_cfg
|
|
||||||
else:
|
|
||||||
self.symmetry = None
|
|
||||||
|
|
||||||
# PPO components
|
|
||||||
self.policy = policy
|
|
||||||
self.policy.to(self.device)
|
|
||||||
# Create optimizer
|
|
||||||
self.optimizer = optim.Adam(self.policy.parameters(), lr=learning_rate)
|
|
||||||
# Create rollout storage
|
|
||||||
self.storage: RolloutStorage = None # type: ignore
|
|
||||||
self.transition = RolloutStorage.Transition()
|
|
||||||
|
|
||||||
# PPO parameters
|
|
||||||
self.clip_param = clip_param
|
|
||||||
self.num_learning_epochs = num_learning_epochs
|
|
||||||
self.num_mini_batches = num_mini_batches
|
|
||||||
self.value_loss_coef = value_loss_coef
|
|
||||||
self.entropy_coef = entropy_coef
|
|
||||||
self.gamma = gamma
|
|
||||||
self.lam = lam
|
|
||||||
self.max_grad_norm = max_grad_norm
|
|
||||||
self.use_clipped_value_loss = use_clipped_value_loss
|
|
||||||
self.desired_kl = desired_kl
|
|
||||||
self.schedule = schedule
|
|
||||||
self.learning_rate = learning_rate
|
|
||||||
self.normalize_advantage_per_mini_batch = normalize_advantage_per_mini_batch
|
|
||||||
|
|
||||||
def init_storage(self, training_type, num_envs, num_transitions_per_env, obs, actions_shape):
|
|
||||||
# create rollout storage
|
|
||||||
self.storage = RolloutStorage(
|
|
||||||
training_type,
|
|
||||||
num_envs,
|
|
||||||
num_transitions_per_env,
|
|
||||||
obs,
|
|
||||||
actions_shape,
|
|
||||||
self.device,
|
|
||||||
)
|
|
||||||
|
|
||||||
def act(self, obs):
|
|
||||||
if self.policy.is_recurrent:
|
|
||||||
self.transition.hidden_states = self.policy.get_hidden_states()
|
|
||||||
# compute the actions and values
|
|
||||||
self.transition.actions = self.policy.act(obs).detach()
|
|
||||||
self.transition.values = self.policy.evaluate(obs).detach()
|
|
||||||
self.transition.actions_log_prob = self.policy.get_actions_log_prob(self.transition.actions).detach()
|
|
||||||
self.transition.action_mean = self.policy.action_mean.detach()
|
|
||||||
self.transition.action_sigma = self.policy.action_std.detach()
|
|
||||||
# need to record obs before env.step()
|
|
||||||
self.transition.observations = obs
|
|
||||||
return self.transition.actions
|
|
||||||
|
|
||||||
def process_env_step(self, obs, rewards, dones, extras):
|
|
||||||
# update the normalizers
|
|
||||||
self.policy.update_normalization(obs)
|
|
||||||
if self.rnd:
|
|
||||||
self.rnd.update_normalization(obs)
|
|
||||||
|
|
||||||
# Record the rewards and dones
|
|
||||||
# Note: we clone here because later on we bootstrap the rewards based on timeouts
|
|
||||||
self.transition.rewards = rewards.clone()
|
|
||||||
self.transition.dones = dones
|
|
||||||
|
|
||||||
# Compute the intrinsic rewards and add to extrinsic rewards
|
|
||||||
if self.rnd:
|
|
||||||
# Compute the intrinsic rewards
|
|
||||||
self.intrinsic_rewards = self.rnd.get_intrinsic_reward(obs)
|
|
||||||
# Add intrinsic rewards to extrinsic rewards
|
|
||||||
self.transition.rewards += self.intrinsic_rewards
|
|
||||||
|
|
||||||
# Bootstrapping on time outs
|
|
||||||
if "time_outs" in extras:
|
|
||||||
self.transition.rewards += self.gamma * torch.squeeze(
|
|
||||||
self.transition.values * extras["time_outs"].unsqueeze(1).to(self.device), 1
|
|
||||||
)
|
|
||||||
|
|
||||||
# record the transition
|
|
||||||
self.storage.add_transitions(self.transition)
|
|
||||||
self.transition.clear()
|
|
||||||
self.policy.reset(dones)
|
|
||||||
|
|
||||||
def compute_returns(self, obs):
|
|
||||||
# compute value for the last step
|
|
||||||
last_values = self.policy.evaluate(obs).detach()
|
|
||||||
self.storage.compute_returns(
|
|
||||||
last_values, self.gamma, self.lam, normalize_advantage=not self.normalize_advantage_per_mini_batch
|
|
||||||
)
|
|
||||||
|
|
||||||
def update(self): # noqa: C901
|
|
||||||
mean_value_loss = 0
|
|
||||||
mean_surrogate_loss = 0
|
|
||||||
mean_entropy = 0
|
|
||||||
# -- RND loss
|
|
||||||
if self.rnd:
|
|
||||||
mean_rnd_loss = 0
|
|
||||||
else:
|
|
||||||
mean_rnd_loss = None
|
|
||||||
# -- Symmetry loss
|
|
||||||
if self.symmetry:
|
|
||||||
mean_symmetry_loss = 0
|
|
||||||
else:
|
|
||||||
mean_symmetry_loss = None
|
|
||||||
|
|
||||||
# generator for mini batches
|
|
||||||
if self.policy.is_recurrent:
|
|
||||||
generator = self.storage.recurrent_mini_batch_generator(self.num_mini_batches, self.num_learning_epochs)
|
|
||||||
else:
|
|
||||||
generator = self.storage.mini_batch_generator(self.num_mini_batches, self.num_learning_epochs)
|
|
||||||
|
|
||||||
# iterate over batches
|
|
||||||
for (
|
|
||||||
obs_batch,
|
|
||||||
actions_batch,
|
|
||||||
target_values_batch,
|
|
||||||
advantages_batch,
|
|
||||||
returns_batch,
|
|
||||||
old_actions_log_prob_batch,
|
|
||||||
old_mu_batch,
|
|
||||||
old_sigma_batch,
|
|
||||||
hid_states_batch,
|
|
||||||
masks_batch,
|
|
||||||
) in generator:
|
|
||||||
|
|
||||||
# number of augmentations per sample
|
|
||||||
# we start with 1 and increase it if we use symmetry augmentation
|
|
||||||
num_aug = 1
|
|
||||||
# original batch size
|
|
||||||
# we assume policy group is always there and needs augmentation
|
|
||||||
original_batch_size = obs_batch.batch_size[0]
|
|
||||||
|
|
||||||
# check if we should normalize advantages per mini batch
|
|
||||||
if self.normalize_advantage_per_mini_batch:
|
|
||||||
with torch.no_grad():
|
|
||||||
advantages_batch = (advantages_batch - advantages_batch.mean()) / (advantages_batch.std() + 1e-8)
|
|
||||||
|
|
||||||
# Perform symmetric augmentation
|
|
||||||
if self.symmetry and self.symmetry["use_data_augmentation"]:
|
|
||||||
# augmentation using symmetry
|
|
||||||
data_augmentation_func = self.symmetry["data_augmentation_func"]
|
|
||||||
# returned shape: [batch_size * num_aug, ...]
|
|
||||||
obs_batch, actions_batch = data_augmentation_func(
|
|
||||||
obs=obs_batch,
|
|
||||||
actions=actions_batch,
|
|
||||||
env=self.symmetry["_env"],
|
|
||||||
)
|
|
||||||
# compute number of augmentations per sample
|
|
||||||
# we assume policy group is always there and needs augmentation
|
|
||||||
num_aug = int(obs_batch.batch_size[0] / original_batch_size)
|
|
||||||
# repeat the rest of the batch
|
|
||||||
# -- actor
|
|
||||||
old_actions_log_prob_batch = old_actions_log_prob_batch.repeat(num_aug, 1)
|
|
||||||
# -- critic
|
|
||||||
target_values_batch = target_values_batch.repeat(num_aug, 1)
|
|
||||||
advantages_batch = advantages_batch.repeat(num_aug, 1)
|
|
||||||
returns_batch = returns_batch.repeat(num_aug, 1)
|
|
||||||
|
|
||||||
# Recompute actions log prob and entropy for current batch of transitions
|
|
||||||
# Note: we need to do this because we updated the policy with the new parameters
|
|
||||||
# -- actor
|
|
||||||
self.policy.act(obs_batch, masks=masks_batch, hidden_states=hid_states_batch[0])
|
|
||||||
actions_log_prob_batch = self.policy.get_actions_log_prob(actions_batch)
|
|
||||||
# -- critic
|
|
||||||
value_batch = self.policy.evaluate(obs_batch, masks=masks_batch, hidden_states=hid_states_batch[1])
|
|
||||||
# -- entropy
|
|
||||||
# we only keep the entropy of the first augmentation (the original one)
|
|
||||||
mu_batch = self.policy.action_mean[:original_batch_size]
|
|
||||||
sigma_batch = self.policy.action_std[:original_batch_size]
|
|
||||||
entropy_batch = self.policy.entropy[:original_batch_size]
|
|
||||||
|
|
||||||
# KL
|
|
||||||
if self.desired_kl is not None and self.schedule == "adaptive":
|
|
||||||
with torch.inference_mode():
|
|
||||||
kl = torch.sum(
|
|
||||||
torch.log(sigma_batch / old_sigma_batch + 1.0e-5)
|
|
||||||
+ (torch.square(old_sigma_batch) + torch.square(old_mu_batch - mu_batch))
|
|
||||||
/ (2.0 * torch.square(sigma_batch))
|
|
||||||
- 0.5,
|
|
||||||
axis=-1,
|
|
||||||
)
|
|
||||||
kl_mean = torch.mean(kl)
|
|
||||||
|
|
||||||
# Reduce the KL divergence across all GPUs
|
|
||||||
if self.is_multi_gpu:
|
|
||||||
torch.distributed.all_reduce(kl_mean, op=torch.distributed.ReduceOp.SUM)
|
|
||||||
kl_mean /= self.gpu_world_size
|
|
||||||
|
|
||||||
# Update the learning rate
|
|
||||||
# Perform this adaptation only on the main process
|
|
||||||
# TODO: Is this needed? If KL-divergence is the "same" across all GPUs,
|
|
||||||
# then the learning rate should be the same across all GPUs.
|
|
||||||
if self.gpu_global_rank == 0:
|
|
||||||
if kl_mean > self.desired_kl * 2.0:
|
|
||||||
self.learning_rate = max(1e-5, self.learning_rate / 1.5)
|
|
||||||
elif kl_mean < self.desired_kl / 2.0 and kl_mean > 0.0:
|
|
||||||
self.learning_rate = min(1e-2, self.learning_rate * 1.5)
|
|
||||||
|
|
||||||
# Update the learning rate for all GPUs
|
|
||||||
if self.is_multi_gpu:
|
|
||||||
lr_tensor = torch.tensor(self.learning_rate, device=self.device)
|
|
||||||
torch.distributed.broadcast(lr_tensor, src=0)
|
|
||||||
self.learning_rate = lr_tensor.item()
|
|
||||||
|
|
||||||
# Update the learning rate for all parameter groups
|
|
||||||
for param_group in self.optimizer.param_groups:
|
|
||||||
param_group["lr"] = self.learning_rate
|
|
||||||
|
|
||||||
# Surrogate loss
|
|
||||||
ratio = torch.exp(actions_log_prob_batch - torch.squeeze(old_actions_log_prob_batch))
|
|
||||||
surrogate = -torch.squeeze(advantages_batch) * ratio
|
|
||||||
surrogate_clipped = -torch.squeeze(advantages_batch) * torch.clamp(
|
|
||||||
ratio, 1.0 - self.clip_param, 1.0 + self.clip_param
|
|
||||||
)
|
|
||||||
surrogate_loss = torch.max(surrogate, surrogate_clipped).mean()
|
|
||||||
|
|
||||||
# Value function loss
|
|
||||||
if self.use_clipped_value_loss:
|
|
||||||
value_clipped = target_values_batch + (value_batch - target_values_batch).clamp(
|
|
||||||
-self.clip_param, self.clip_param
|
|
||||||
)
|
|
||||||
value_losses = (value_batch - returns_batch).pow(2)
|
|
||||||
value_losses_clipped = (value_clipped - returns_batch).pow(2)
|
|
||||||
value_loss = torch.max(value_losses, value_losses_clipped).mean()
|
|
||||||
else:
|
|
||||||
value_loss = (returns_batch - value_batch).pow(2).mean()
|
|
||||||
|
|
||||||
loss = surrogate_loss + self.value_loss_coef * value_loss - self.entropy_coef * entropy_batch.mean()
|
|
||||||
|
|
||||||
# Symmetry loss
|
|
||||||
if self.symmetry:
|
|
||||||
# obtain the symmetric actions
|
|
||||||
# if we did augmentation before then we don't need to augment again
|
|
||||||
if not self.symmetry["use_data_augmentation"]:
|
|
||||||
data_augmentation_func = self.symmetry["data_augmentation_func"]
|
|
||||||
obs_batch, _ = data_augmentation_func(obs=obs_batch, actions=None, env=self.symmetry["_env"])
|
|
||||||
# compute number of augmentations per sample
|
|
||||||
num_aug = int(obs_batch.shape[0] / original_batch_size)
|
|
||||||
|
|
||||||
# actions predicted by the actor for symmetrically-augmented observations
|
|
||||||
mean_actions_batch = self.policy.act_inference(obs_batch.detach().clone())
|
|
||||||
|
|
||||||
# compute the symmetrically augmented actions
|
|
||||||
# note: we are assuming the first augmentation is the original one.
|
|
||||||
# We do not use the action_batch from earlier since that action was sampled from the distribution.
|
|
||||||
# However, the symmetry loss is computed using the mean of the distribution.
|
|
||||||
action_mean_orig = mean_actions_batch[:original_batch_size]
|
|
||||||
_, actions_mean_symm_batch = data_augmentation_func(
|
|
||||||
obs=None, actions=action_mean_orig, env=self.symmetry["_env"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# compute the loss (we skip the first augmentation as it is the original one)
|
|
||||||
mse_loss = torch.nn.MSELoss()
|
|
||||||
symmetry_loss = mse_loss(
|
|
||||||
mean_actions_batch[original_batch_size:], actions_mean_symm_batch.detach()[original_batch_size:]
|
|
||||||
)
|
|
||||||
# add the loss to the total loss
|
|
||||||
if self.symmetry["use_mirror_loss"]:
|
|
||||||
loss += self.symmetry["mirror_loss_coeff"] * symmetry_loss
|
|
||||||
else:
|
|
||||||
symmetry_loss = symmetry_loss.detach()
|
|
||||||
|
|
||||||
# Random Network Distillation loss
|
|
||||||
# TODO: Move this processing to inside RND module.
|
|
||||||
if self.rnd:
|
|
||||||
# extract the rnd_state
|
|
||||||
# TODO: Check if we still need torch no grad. It is just an affine transformation.
|
|
||||||
with torch.no_grad():
|
|
||||||
rnd_state_batch = self.rnd.get_rnd_state(obs_batch[:original_batch_size])
|
|
||||||
rnd_state_batch = self.rnd.state_normalizer(rnd_state_batch)
|
|
||||||
# predict the embedding and the target
|
|
||||||
predicted_embedding = self.rnd.predictor(rnd_state_batch)
|
|
||||||
target_embedding = self.rnd.target(rnd_state_batch).detach()
|
|
||||||
# compute the loss as the mean squared error
|
|
||||||
mseloss = torch.nn.MSELoss()
|
|
||||||
rnd_loss = mseloss(predicted_embedding, target_embedding)
|
|
||||||
|
|
||||||
# Compute the gradients
|
|
||||||
# -- For PPO
|
|
||||||
self.optimizer.zero_grad()
|
|
||||||
loss.backward()
|
|
||||||
# -- For RND
|
|
||||||
if self.rnd:
|
|
||||||
self.rnd_optimizer.zero_grad() # type: ignore
|
|
||||||
rnd_loss.backward()
|
|
||||||
|
|
||||||
# Collect gradients from all GPUs
|
|
||||||
if self.is_multi_gpu:
|
|
||||||
self.reduce_parameters()
|
|
||||||
|
|
||||||
# Apply the gradients
|
|
||||||
# -- For PPO
|
|
||||||
nn.utils.clip_grad_norm_(self.policy.parameters(), self.max_grad_norm)
|
|
||||||
self.optimizer.step()
|
|
||||||
# -- For RND
|
|
||||||
if self.rnd_optimizer:
|
|
||||||
self.rnd_optimizer.step()
|
|
||||||
|
|
||||||
# Store the losses
|
|
||||||
mean_value_loss += value_loss.item()
|
|
||||||
mean_surrogate_loss += surrogate_loss.item()
|
|
||||||
mean_entropy += entropy_batch.mean().item()
|
|
||||||
# -- RND loss
|
|
||||||
if mean_rnd_loss is not None:
|
|
||||||
mean_rnd_loss += rnd_loss.item()
|
|
||||||
# -- Symmetry loss
|
|
||||||
if mean_symmetry_loss is not None:
|
|
||||||
mean_symmetry_loss += symmetry_loss.item()
|
|
||||||
|
|
||||||
# -- For PPO
|
|
||||||
num_updates = self.num_learning_epochs * self.num_mini_batches
|
|
||||||
mean_value_loss /= num_updates
|
|
||||||
mean_surrogate_loss /= num_updates
|
|
||||||
mean_entropy /= num_updates
|
|
||||||
# -- For RND
|
|
||||||
if mean_rnd_loss is not None:
|
|
||||||
mean_rnd_loss /= num_updates
|
|
||||||
# -- For Symmetry
|
|
||||||
if mean_symmetry_loss is not None:
|
|
||||||
mean_symmetry_loss /= num_updates
|
|
||||||
# -- Clear the storage
|
|
||||||
self.storage.clear()
|
|
||||||
|
|
||||||
# construct the loss dictionary
|
|
||||||
loss_dict = {
|
|
||||||
"value_function": mean_value_loss,
|
|
||||||
"surrogate": mean_surrogate_loss,
|
|
||||||
"entropy": mean_entropy,
|
|
||||||
}
|
|
||||||
if self.rnd:
|
|
||||||
loss_dict["rnd"] = mean_rnd_loss
|
|
||||||
if self.symmetry:
|
|
||||||
loss_dict["symmetry"] = mean_symmetry_loss
|
|
||||||
|
|
||||||
return loss_dict
|
|
||||||
|
|
||||||
"""
|
|
||||||
Helper functions
|
|
||||||
"""
|
|
||||||
|
|
||||||
def broadcast_parameters(self):
|
|
||||||
"""Broadcast model parameters to all GPUs."""
|
|
||||||
# obtain the model parameters on current GPU
|
|
||||||
model_params = [self.policy.state_dict()]
|
|
||||||
if self.rnd:
|
|
||||||
model_params.append(self.rnd.predictor.state_dict())
|
|
||||||
# broadcast the model parameters
|
|
||||||
torch.distributed.broadcast_object_list(model_params, src=0)
|
|
||||||
# load the model parameters on all GPUs from source GPU
|
|
||||||
self.policy.load_state_dict(model_params[0])
|
|
||||||
if self.rnd:
|
|
||||||
self.rnd.predictor.load_state_dict(model_params[1])
|
|
||||||
|
|
||||||
def reduce_parameters(self):
|
|
||||||
"""Collect gradients from all GPUs and average them.
|
|
||||||
|
|
||||||
This function is called after the backward pass to synchronize the gradients across all GPUs.
|
|
||||||
"""
|
|
||||||
# Create a tensor to store the gradients
|
|
||||||
grads = [param.grad.view(-1) for param in self.policy.parameters() if param.grad is not None]
|
|
||||||
if self.rnd:
|
|
||||||
grads += [param.grad.view(-1) for param in self.rnd.parameters() if param.grad is not None]
|
|
||||||
all_grads = torch.cat(grads)
|
|
||||||
|
|
||||||
# Average the gradients across all GPUs
|
|
||||||
torch.distributed.all_reduce(all_grads, op=torch.distributed.ReduceOp.SUM)
|
|
||||||
all_grads /= self.gpu_world_size
|
|
||||||
|
|
||||||
# Get all parameters
|
|
||||||
all_params = self.policy.parameters()
|
|
||||||
if self.rnd:
|
|
||||||
all_params = chain(all_params, self.rnd.parameters())
|
|
||||||
|
|
||||||
# Update the gradients for all parameters with the reduced gradients
|
|
||||||
offset = 0
|
|
||||||
for param in all_params:
|
|
||||||
if param.grad is not None:
|
|
||||||
numel = param.numel()
|
|
||||||
# copy data back from shared buffer
|
|
||||||
param.grad.data.copy_(all_grads[offset : offset + numel].view_as(param.grad.data))
|
|
||||||
# update the offset for the next parameter
|
|
||||||
offset += numel
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
"""Main module for the rsl_rl package."""
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
"""Implementation of different RL agents."""
|
|
||||||
|
|
||||||
from .distillation import Distillation
|
|
||||||
from .ppo import PPO
|
|
||||||
from .amp_ppo import AMP_PPO
|
|
||||||
__all__ = ["PPO", "Distillation", "AMP_PPO"]
|
|
||||||
|
|
@ -1,571 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
from torch._tensor import Tensor
|
|
||||||
from torch._tensor import Tensor
|
|
||||||
from typing import Any
|
|
||||||
from copy import deepcopy
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
import torch.optim as optim
|
|
||||||
from itertools import chain
|
|
||||||
|
|
||||||
from rsl_rl.modules import ActorCritic
|
|
||||||
from rsl_rl.modules.rnd import RandomNetworkDistillation
|
|
||||||
from rsl_rl.storage import RolloutStorage, ReplayBufferMulti
|
|
||||||
from rsl_rl.utils import string_to_callable
|
|
||||||
|
|
||||||
|
|
||||||
class AMP_PPO:
|
|
||||||
"""Proximal Policy Optimization algorithm (https://arxiv.org/abs/1707.06347)."""
|
|
||||||
|
|
||||||
policy: ActorCritic
|
|
||||||
"""The actor critic module."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
policy,
|
|
||||||
discriminator,
|
|
||||||
amp_data,
|
|
||||||
amp_normalizer,
|
|
||||||
amp_num_frames=1,
|
|
||||||
amp_replay_buffer_size=100000,
|
|
||||||
num_learning_epochs=5,
|
|
||||||
num_mini_batches=4,
|
|
||||||
clip_param=0.2,
|
|
||||||
gamma=0.99,
|
|
||||||
lam=0.95,
|
|
||||||
value_loss_coef=1.0,
|
|
||||||
entropy_coef=0.01,
|
|
||||||
learning_rate=0.001,
|
|
||||||
max_grad_norm=1.0,
|
|
||||||
use_clipped_value_loss=True,
|
|
||||||
schedule="adaptive",
|
|
||||||
desired_kl=0.01,
|
|
||||||
device="cpu",
|
|
||||||
normalize_advantage_per_mini_batch=False,
|
|
||||||
# RND parameters
|
|
||||||
rnd_cfg: dict | None = None,
|
|
||||||
# Symmetry parameters
|
|
||||||
symmetry_cfg: dict | None = None,
|
|
||||||
# Distributed training parameters
|
|
||||||
multi_gpu_cfg: dict | None = None,
|
|
||||||
):
|
|
||||||
# device-related parameters
|
|
||||||
self.device = device
|
|
||||||
self.is_multi_gpu = multi_gpu_cfg is not None
|
|
||||||
# Multi-GPU parameters
|
|
||||||
if multi_gpu_cfg is not None:
|
|
||||||
self.gpu_global_rank = multi_gpu_cfg["global_rank"]
|
|
||||||
self.gpu_world_size = multi_gpu_cfg["world_size"]
|
|
||||||
else:
|
|
||||||
self.gpu_global_rank = 0
|
|
||||||
self.gpu_world_size = 1
|
|
||||||
|
|
||||||
# RND components
|
|
||||||
if rnd_cfg is not None:
|
|
||||||
# Extract parameters used in ppo
|
|
||||||
rnd_lr = rnd_cfg.pop("learning_rate", 1e-3)
|
|
||||||
# Create RND module
|
|
||||||
self.rnd = RandomNetworkDistillation(device=self.device, **rnd_cfg)
|
|
||||||
# Create RND optimizer
|
|
||||||
params = self.rnd.predictor.parameters()
|
|
||||||
self.rnd_optimizer = optim.Adam(params, lr=rnd_lr)
|
|
||||||
else:
|
|
||||||
self.rnd = None
|
|
||||||
self.rnd_optimizer = None
|
|
||||||
|
|
||||||
# Symmetry components
|
|
||||||
if symmetry_cfg is not None:
|
|
||||||
# Check if symmetry is enabled
|
|
||||||
use_symmetry = symmetry_cfg["use_data_augmentation"] or symmetry_cfg["use_mirror_loss"]
|
|
||||||
# Print that we are not using symmetry
|
|
||||||
if not use_symmetry:
|
|
||||||
print("Symmetry not used for learning. We will use it for logging instead.")
|
|
||||||
# If function is a string then resolve it to a function
|
|
||||||
if isinstance(symmetry_cfg["data_augmentation_func"], str):
|
|
||||||
symmetry_cfg["data_augmentation_func"] = string_to_callable(symmetry_cfg["data_augmentation_func"])
|
|
||||||
# Check valid configuration
|
|
||||||
if symmetry_cfg["use_data_augmentation"] and not callable(symmetry_cfg["data_augmentation_func"]):
|
|
||||||
raise ValueError(
|
|
||||||
"Data augmentation enabled but the function is not callable:"
|
|
||||||
f" {symmetry_cfg['data_augmentation_func']}"
|
|
||||||
)
|
|
||||||
# Store symmetry configuration
|
|
||||||
self.symmetry = symmetry_cfg
|
|
||||||
else:
|
|
||||||
self.symmetry = None
|
|
||||||
|
|
||||||
## AMP components
|
|
||||||
self.discriminator = discriminator
|
|
||||||
self.discriminator.to(self.device)
|
|
||||||
|
|
||||||
self.amp_storage = ReplayBufferMulti(discriminator.state_dim, amp_replay_buffer_size, amp_num_frames, device)
|
|
||||||
self.amp_data = amp_data
|
|
||||||
self.amp_normalizer = amp_normalizer
|
|
||||||
|
|
||||||
# PPO components
|
|
||||||
self.policy = policy
|
|
||||||
self.policy.to(self.device)
|
|
||||||
|
|
||||||
# Create rollout storage
|
|
||||||
self.storage: RolloutStorage = None # type: ignore
|
|
||||||
self.transition = RolloutStorage.Transition()
|
|
||||||
self.amp_transition = RolloutStorage.Transition()
|
|
||||||
params = [
|
|
||||||
{'params': self.policy.parameters(), 'name': 'policy'},
|
|
||||||
]
|
|
||||||
|
|
||||||
params.append({
|
|
||||||
'params': self.discriminator.trunk.parameters(),
|
|
||||||
'weight_decay': 10e-4,
|
|
||||||
'name': f'amp_trunk'
|
|
||||||
})
|
|
||||||
params.append({
|
|
||||||
'params': self.discriminator.amp_linear.parameters(),
|
|
||||||
'weight_decay': 10e-2,
|
|
||||||
'name': f'amp_head'
|
|
||||||
})
|
|
||||||
|
|
||||||
# Create optimizer
|
|
||||||
self.optimizer = optim.Adam(params, lr=learning_rate)
|
|
||||||
|
|
||||||
# PPO parameters
|
|
||||||
self.clip_param = clip_param
|
|
||||||
self.num_learning_epochs = num_learning_epochs
|
|
||||||
self.num_mini_batches = num_mini_batches
|
|
||||||
self.value_loss_coef = value_loss_coef
|
|
||||||
self.entropy_coef = entropy_coef
|
|
||||||
self.gamma = gamma
|
|
||||||
self.lam = lam
|
|
||||||
self.max_grad_norm = max_grad_norm
|
|
||||||
self.use_clipped_value_loss = use_clipped_value_loss
|
|
||||||
self.desired_kl = desired_kl
|
|
||||||
self.schedule = schedule
|
|
||||||
self.learning_rate = learning_rate
|
|
||||||
self.normalize_advantage_per_mini_batch = normalize_advantage_per_mini_batch
|
|
||||||
|
|
||||||
|
|
||||||
def init_storage(self, training_type, num_envs, num_transitions_per_env, obs, actions_shape):
|
|
||||||
# create rollout storage
|
|
||||||
self.storage = RolloutStorage(
|
|
||||||
training_type,
|
|
||||||
num_envs,
|
|
||||||
num_transitions_per_env,
|
|
||||||
obs,
|
|
||||||
actions_shape,
|
|
||||||
self.device,
|
|
||||||
)
|
|
||||||
|
|
||||||
def act(self, obs, amp_obs):
|
|
||||||
if self.policy.is_recurrent:
|
|
||||||
self.transition.hidden_states = self.policy.get_hidden_states()
|
|
||||||
# compute the actions and values
|
|
||||||
self.transition.actions = self.policy.act(obs).detach()
|
|
||||||
self.transition.values = self.policy.evaluate(obs).detach()
|
|
||||||
self.transition.actions_log_prob = self.policy.get_actions_log_prob(self.transition.actions).detach()
|
|
||||||
self.transition.action_mean = self.policy.action_mean.detach()
|
|
||||||
self.transition.action_sigma = self.policy.action_std.detach()
|
|
||||||
# need to record obs before env.step()
|
|
||||||
self.transition.observations = obs
|
|
||||||
self.amp_transition.observations = amp_obs
|
|
||||||
return self.transition.actions
|
|
||||||
|
|
||||||
def process_env_step(self, obs, rewards, dones, extras,amp_obs, amp_obs_frames=None):
|
|
||||||
# update the normalizers
|
|
||||||
self.policy.update_normalization(obs)
|
|
||||||
if self.rnd:
|
|
||||||
self.rnd.update_normalization(obs)
|
|
||||||
|
|
||||||
# Record the rewards and dones
|
|
||||||
# Note: we clone here because later on we bootstrap the rewards based on timeouts
|
|
||||||
self.transition.rewards = rewards.clone()
|
|
||||||
self.transition.dones = dones
|
|
||||||
|
|
||||||
# Compute the intrinsic rewards and add to extrinsic rewards
|
|
||||||
if self.rnd:
|
|
||||||
# Compute the intrinsic rewards
|
|
||||||
self.intrinsic_rewards = self.rnd.get_intrinsic_reward(obs)
|
|
||||||
# Add intrinsic rewards to extrinsic rewards
|
|
||||||
self.transition.rewards += self.intrinsic_rewards
|
|
||||||
|
|
||||||
# Bootstrapping on time outs
|
|
||||||
if "time_outs" in extras:
|
|
||||||
self.transition.rewards += self.gamma * torch.squeeze(
|
|
||||||
self.transition.values * extras["time_outs"].unsqueeze(1).to(self.device), 1
|
|
||||||
)
|
|
||||||
|
|
||||||
if amp_obs_frames is not None:
|
|
||||||
self.amp_storage.insert(amp_obs_frames)
|
|
||||||
else:
|
|
||||||
self.amp_storage.insert(self.amp_transition.observations, amp_obs)
|
|
||||||
|
|
||||||
# record the transition
|
|
||||||
self.storage.add_transitions(self.transition)
|
|
||||||
self.transition.clear()
|
|
||||||
self.amp_transition.clear()
|
|
||||||
self.policy.reset(dones)
|
|
||||||
|
|
||||||
def compute_returns(self, obs):
|
|
||||||
# compute value for the last step
|
|
||||||
last_values = self.policy.evaluate(obs).detach()
|
|
||||||
self.storage.compute_returns(
|
|
||||||
last_values, self.gamma, self.lam, normalize_advantage=not self.normalize_advantage_per_mini_batch
|
|
||||||
)
|
|
||||||
|
|
||||||
def update(self): # noqa: C901
|
|
||||||
mean_value_loss = 0
|
|
||||||
mean_surrogate_loss = 0
|
|
||||||
mean_entropy = 0
|
|
||||||
mean_amp_loss = 0
|
|
||||||
mean_grad_pen_loss = 0
|
|
||||||
mean_policy_pred = 0
|
|
||||||
mean_expert_pred = 0
|
|
||||||
# -- RND loss
|
|
||||||
if self.rnd:
|
|
||||||
mean_rnd_loss = 0
|
|
||||||
else:
|
|
||||||
mean_rnd_loss = None
|
|
||||||
# -- Symmetry loss
|
|
||||||
if self.symmetry:
|
|
||||||
mean_symmetry_loss = 0
|
|
||||||
else:
|
|
||||||
mean_symmetry_loss = None
|
|
||||||
|
|
||||||
# generator for mini batches
|
|
||||||
if self.policy.is_recurrent:
|
|
||||||
generator = self.storage.recurrent_mini_batch_generator(self.num_mini_batches, self.num_learning_epochs)
|
|
||||||
else:
|
|
||||||
generator = self.storage.mini_batch_generator(self.num_mini_batches, self.num_learning_epochs)
|
|
||||||
|
|
||||||
|
|
||||||
amp_policy_generator = self.amp_storage.feed_forward_generator(
|
|
||||||
self.num_learning_epochs * self.num_mini_batches,
|
|
||||||
self.storage.num_envs * self.storage.num_transitions_per_env // self.num_mini_batches,
|
|
||||||
)
|
|
||||||
|
|
||||||
amp_expert_generator = self.amp_data.feed_forward_generator_23dof_multi(
|
|
||||||
self.num_learning_epochs * self.num_mini_batches,
|
|
||||||
self.storage.num_envs * self.storage.num_transitions_per_env // self.num_mini_batches,
|
|
||||||
)
|
|
||||||
|
|
||||||
# iterate over batches
|
|
||||||
for sample, sample_amp_policy, sample_amp_expert in zip(generator, amp_policy_generator, amp_expert_generator):
|
|
||||||
(
|
|
||||||
obs_batch,
|
|
||||||
actions_batch,
|
|
||||||
target_values_batch,
|
|
||||||
advantages_batch,
|
|
||||||
returns_batch,
|
|
||||||
old_actions_log_prob_batch,
|
|
||||||
old_mu_batch,
|
|
||||||
old_sigma_batch,
|
|
||||||
hid_states_batch,
|
|
||||||
masks_batch,
|
|
||||||
) = sample
|
|
||||||
|
|
||||||
# number of augmentations per sample
|
|
||||||
# we start with 1 and increase it if we use symmetry augmentation
|
|
||||||
num_aug = 1
|
|
||||||
# original batch size
|
|
||||||
# we assume policy group is always there and needs augmentation
|
|
||||||
original_batch_size = obs_batch.batch_size[0]
|
|
||||||
|
|
||||||
# check if we should normalize advantages per mini batch
|
|
||||||
if self.normalize_advantage_per_mini_batch:
|
|
||||||
with torch.no_grad():
|
|
||||||
advantages_batch = (advantages_batch - advantages_batch.mean()) / (advantages_batch.std() + 1e-8)
|
|
||||||
|
|
||||||
# Perform symmetric augmentation
|
|
||||||
if self.symmetry and self.symmetry["use_data_augmentation"]:
|
|
||||||
# augmentation using symmetry
|
|
||||||
data_augmentation_func = self.symmetry["data_augmentation_func"]
|
|
||||||
# returned shape: [batch_size * num_aug, ...]
|
|
||||||
obs_batch, actions_batch = data_augmentation_func(
|
|
||||||
obs=obs_batch,
|
|
||||||
actions=actions_batch,
|
|
||||||
env=self.symmetry["_env"],
|
|
||||||
)
|
|
||||||
# compute number of augmentations per sample
|
|
||||||
# we assume policy group is always there and needs augmentation
|
|
||||||
num_aug = int(obs_batch.batch_size[0] / original_batch_size)
|
|
||||||
# repeat the rest of the batch
|
|
||||||
# -- actor
|
|
||||||
old_actions_log_prob_batch = old_actions_log_prob_batch.repeat(num_aug, 1)
|
|
||||||
# -- critic
|
|
||||||
target_values_batch = target_values_batch.repeat(num_aug, 1)
|
|
||||||
advantages_batch = advantages_batch.repeat(num_aug, 1)
|
|
||||||
returns_batch = returns_batch.repeat(num_aug, 1)
|
|
||||||
|
|
||||||
# Recompute actions log prob and entropy for current batch of transitions
|
|
||||||
# Note: we need to do this because we updated the policy with the new parameters
|
|
||||||
# -- actor
|
|
||||||
self.policy.act(obs_batch, masks=masks_batch, hidden_states=hid_states_batch[0])
|
|
||||||
actions_log_prob_batch = self.policy.get_actions_log_prob(actions_batch)
|
|
||||||
# -- critic
|
|
||||||
value_batch = self.policy.evaluate(obs_batch, masks=masks_batch, hidden_states=hid_states_batch[1])
|
|
||||||
# -- entropy
|
|
||||||
# we only keep the entropy of the first augmentation (the original one)
|
|
||||||
mu_batch = self.policy.action_mean[:original_batch_size]
|
|
||||||
sigma_batch = self.policy.action_std[:original_batch_size]
|
|
||||||
entropy_batch = self.policy.entropy[:original_batch_size]
|
|
||||||
|
|
||||||
# KL
|
|
||||||
if self.desired_kl is not None and self.schedule == "adaptive":
|
|
||||||
with torch.inference_mode():
|
|
||||||
kl = torch.sum(
|
|
||||||
torch.log(sigma_batch / old_sigma_batch + 1.0e-5)
|
|
||||||
+ (torch.square(old_sigma_batch) + torch.square(old_mu_batch - mu_batch))
|
|
||||||
/ (2.0 * torch.square(sigma_batch))
|
|
||||||
- 0.5,
|
|
||||||
axis=-1,
|
|
||||||
)
|
|
||||||
kl_mean = torch.mean(kl)
|
|
||||||
|
|
||||||
# Reduce the KL divergence across all GPUs
|
|
||||||
if self.is_multi_gpu:
|
|
||||||
torch.distributed.all_reduce(kl_mean, op=torch.distributed.ReduceOp.SUM)
|
|
||||||
kl_mean /= self.gpu_world_size
|
|
||||||
|
|
||||||
# Update the learning rate
|
|
||||||
# Perform this adaptation only on the main process
|
|
||||||
# TODO: Is this needed? If KL-divergence is the "same" across all GPUs,
|
|
||||||
# then the learning rate should be the same across all GPUs.
|
|
||||||
if self.gpu_global_rank == 0:
|
|
||||||
if kl_mean > self.desired_kl * 2.0:
|
|
||||||
self.learning_rate = max(1e-5, self.learning_rate / 1.5)
|
|
||||||
elif kl_mean < self.desired_kl / 2.0 and kl_mean > 0.0:
|
|
||||||
self.learning_rate = min(1e-2, self.learning_rate * 1.5)
|
|
||||||
|
|
||||||
# Update the learning rate for all GPUs
|
|
||||||
if self.is_multi_gpu:
|
|
||||||
lr_tensor = torch.tensor(self.learning_rate, device=self.device)
|
|
||||||
torch.distributed.broadcast(lr_tensor, src=0)
|
|
||||||
self.learning_rate = lr_tensor.item()
|
|
||||||
|
|
||||||
# Update the learning rate for all parameter groups
|
|
||||||
for param_group in self.optimizer.param_groups:
|
|
||||||
param_group["lr"] = self.learning_rate
|
|
||||||
|
|
||||||
# Surrogate loss
|
|
||||||
ratio = torch.exp(actions_log_prob_batch - torch.squeeze(old_actions_log_prob_batch))
|
|
||||||
surrogate = -torch.squeeze(advantages_batch) * ratio
|
|
||||||
surrogate_clipped = -torch.squeeze(advantages_batch) * torch.clamp(
|
|
||||||
ratio, 1.0 - self.clip_param, 1.0 + self.clip_param
|
|
||||||
)
|
|
||||||
surrogate_loss = torch.max(surrogate, surrogate_clipped).mean()
|
|
||||||
|
|
||||||
# Value function loss
|
|
||||||
if self.use_clipped_value_loss:
|
|
||||||
value_clipped = target_values_batch + (value_batch - target_values_batch).clamp(
|
|
||||||
-self.clip_param, self.clip_param
|
|
||||||
)
|
|
||||||
value_losses = (value_batch - returns_batch).pow(2)
|
|
||||||
value_losses_clipped = (value_clipped - returns_batch).pow(2)
|
|
||||||
value_loss = torch.max(value_losses, value_losses_clipped).mean()
|
|
||||||
else:
|
|
||||||
value_loss = (returns_batch - value_batch).pow(2).mean()
|
|
||||||
|
|
||||||
loss = surrogate_loss + self.value_loss_coef * value_loss - self.entropy_coef * entropy_batch.mean()
|
|
||||||
|
|
||||||
# Symmetry loss
|
|
||||||
if self.symmetry:
|
|
||||||
# obtain the symmetric actions
|
|
||||||
# if we did augmentation before then we don't need to augment again
|
|
||||||
if not self.symmetry["use_data_augmentation"]:
|
|
||||||
data_augmentation_func = self.symmetry["data_augmentation_func"]
|
|
||||||
obs_batch, _ = data_augmentation_func(obs=obs_batch, actions=None, env=self.symmetry["_env"])
|
|
||||||
# compute number of augmentations per sample
|
|
||||||
num_aug = int(obs_batch.shape[0] / original_batch_size)
|
|
||||||
|
|
||||||
# actions predicted by the actor for symmetrically-augmented observations
|
|
||||||
mean_actions_batch = self.policy.act_inference(obs_batch.detach().clone())
|
|
||||||
|
|
||||||
# compute the symmetrically augmented actions
|
|
||||||
# note: we are assuming the first augmentation is the original one.
|
|
||||||
# We do not use the action_batch from earlier since that action was sampled from the distribution.
|
|
||||||
# However, the symmetry loss is computed using the mean of the distribution.
|
|
||||||
action_mean_orig = mean_actions_batch[:original_batch_size]
|
|
||||||
_, actions_mean_symm_batch = data_augmentation_func(
|
|
||||||
obs=None, actions=action_mean_orig, env=self.symmetry["_env"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# compute the loss (we skip the first augmentation as it is the original one)
|
|
||||||
mse_loss = torch.nn.MSELoss()
|
|
||||||
symmetry_loss = mse_loss(
|
|
||||||
mean_actions_batch[original_batch_size:], actions_mean_symm_batch.detach()[original_batch_size:]
|
|
||||||
)
|
|
||||||
# add the loss to the total loss
|
|
||||||
if self.symmetry["use_mirror_loss"]:
|
|
||||||
loss += self.symmetry["mirror_loss_coeff"] * symmetry_loss
|
|
||||||
else:
|
|
||||||
symmetry_loss = symmetry_loss.detach()
|
|
||||||
|
|
||||||
# Random Network Distillation loss
|
|
||||||
# TODO: Move this processing to inside RND module.
|
|
||||||
if self.rnd:
|
|
||||||
# extract the rnd_state
|
|
||||||
# TODO: Check if we still need torch no grad. It is just an affine transformation.
|
|
||||||
with torch.no_grad():
|
|
||||||
rnd_state_batch = self.rnd.get_rnd_state(obs_batch[:original_batch_size])
|
|
||||||
rnd_state_batch = self.rnd.state_normalizer(rnd_state_batch)
|
|
||||||
# predict the embedding and the target
|
|
||||||
predicted_embedding = self.rnd.predictor(rnd_state_batch)
|
|
||||||
target_embedding = self.rnd.target(rnd_state_batch).detach()
|
|
||||||
# compute the loss as the mean squared error
|
|
||||||
mseloss = torch.nn.MSELoss()
|
|
||||||
rnd_loss = mseloss(predicted_embedding, target_embedding)
|
|
||||||
|
|
||||||
expert_states = sample_amp_expert
|
|
||||||
policy_states = sample_amp_policy
|
|
||||||
|
|
||||||
with torch.no_grad():
|
|
||||||
expert_states = self.amp_normalizer.normalize_torch(expert_states.to(self.device), self.device)
|
|
||||||
policy_states = self.amp_normalizer.normalize_torch(policy_states, self.device)
|
|
||||||
|
|
||||||
contact_phase_push = obs_batch['critic'][:, -4]
|
|
||||||
mask_push = contact_phase_push == 1.
|
|
||||||
|
|
||||||
if mask_push.any():
|
|
||||||
policy_d = self.discriminator(policy_states.flatten(1))
|
|
||||||
expert_states = expert_states.to(self.device)
|
|
||||||
expert_d = self.discriminator(expert_states.flatten(1))
|
|
||||||
|
|
||||||
expert_loss = torch.nn.MSELoss()(expert_d, torch.ones(expert_d.size(), device=self.device))
|
|
||||||
policy_loss = torch.nn.MSELoss()(policy_d, -1 * torch.ones(policy_d.size(), device=self.device))
|
|
||||||
amp_loss = 0.5 * (expert_loss + policy_loss)
|
|
||||||
|
|
||||||
# grad penalty
|
|
||||||
grad_pen_loss = self.discriminator.compute_grad_pen(expert_states, lambda_=5)
|
|
||||||
else:
|
|
||||||
amp_loss = torch.tensor(0.0, device=self.device)
|
|
||||||
grad_pen_loss = torch.tensor(0.0, device=self.device)
|
|
||||||
expert_loss = torch.tensor(0.0, device=self.device)
|
|
||||||
policy_loss = torch.tensor(0.0, device=self.device)
|
|
||||||
|
|
||||||
loss += (amp_loss + grad_pen_loss)
|
|
||||||
self.amp_normalizer.update(policy_states.cpu().numpy())
|
|
||||||
self.amp_normalizer.update(expert_states.cpu().numpy())
|
|
||||||
|
|
||||||
# Compute the gradients
|
|
||||||
# -- For PPO
|
|
||||||
self.optimizer.zero_grad()
|
|
||||||
loss.backward()
|
|
||||||
# -- For RND
|
|
||||||
if self.rnd:
|
|
||||||
self.rnd_optimizer.zero_grad() # type: ignore
|
|
||||||
rnd_loss.backward()
|
|
||||||
|
|
||||||
# Collect gradients from all GPUs
|
|
||||||
if self.is_multi_gpu:
|
|
||||||
self.reduce_parameters()
|
|
||||||
|
|
||||||
# Apply the gradients
|
|
||||||
# -- For PPO
|
|
||||||
nn.utils.clip_grad_norm_(self.policy.parameters(), self.max_grad_norm)
|
|
||||||
self.optimizer.step()
|
|
||||||
# -- For RND
|
|
||||||
if self.rnd_optimizer:
|
|
||||||
self.rnd_optimizer.step()
|
|
||||||
|
|
||||||
# Store the losses
|
|
||||||
mean_value_loss += value_loss.item()
|
|
||||||
mean_surrogate_loss += surrogate_loss.item()
|
|
||||||
mean_entropy += entropy_batch.mean().item()
|
|
||||||
mean_amp_loss += amp_loss.item()
|
|
||||||
mean_grad_pen_loss += grad_pen_loss.item()
|
|
||||||
mean_policy_pred += policy_loss.mean().item()
|
|
||||||
mean_expert_pred += expert_loss.mean().item()
|
|
||||||
# -- RND loss
|
|
||||||
if mean_rnd_loss is not None:
|
|
||||||
mean_rnd_loss += rnd_loss.item()
|
|
||||||
# -- Symmetry loss
|
|
||||||
if mean_symmetry_loss is not None:
|
|
||||||
mean_symmetry_loss += symmetry_loss.item()
|
|
||||||
|
|
||||||
# -- For PPO
|
|
||||||
num_updates = self.num_learning_epochs * self.num_mini_batches
|
|
||||||
mean_value_loss /= num_updates
|
|
||||||
mean_surrogate_loss /= num_updates
|
|
||||||
mean_entropy /= num_updates
|
|
||||||
mean_amp_loss /= num_updates
|
|
||||||
mean_grad_pen_loss /= num_updates
|
|
||||||
mean_policy_pred /= num_updates
|
|
||||||
mean_expert_pred /= num_updates
|
|
||||||
|
|
||||||
# -- For RND
|
|
||||||
if mean_rnd_loss is not None:
|
|
||||||
mean_rnd_loss /= num_updates
|
|
||||||
# -- For Symmetry
|
|
||||||
if mean_symmetry_loss is not None:
|
|
||||||
mean_symmetry_loss /= num_updates
|
|
||||||
# -- Clear the storage
|
|
||||||
self.storage.clear()
|
|
||||||
|
|
||||||
# construct the loss dictionary
|
|
||||||
loss_dict = {
|
|
||||||
"value_function": mean_value_loss,
|
|
||||||
"surrogate": mean_surrogate_loss,
|
|
||||||
"entropy": mean_entropy,
|
|
||||||
"amp": mean_amp_loss,
|
|
||||||
"amp_grad_pen": mean_grad_pen_loss,
|
|
||||||
"amp_policy_pred": mean_policy_pred,
|
|
||||||
"amp_expert_pred": mean_expert_pred,
|
|
||||||
}
|
|
||||||
if self.rnd:
|
|
||||||
loss_dict["rnd"] = mean_rnd_loss
|
|
||||||
if self.symmetry:
|
|
||||||
loss_dict["symmetry"] = mean_symmetry_loss
|
|
||||||
|
|
||||||
return loss_dict
|
|
||||||
|
|
||||||
"""
|
|
||||||
Helper functions
|
|
||||||
"""
|
|
||||||
|
|
||||||
def broadcast_parameters(self):
|
|
||||||
"""Broadcast model parameters to all GPUs."""
|
|
||||||
# obtain the model parameters on current GPU
|
|
||||||
model_params = [self.policy.state_dict()]
|
|
||||||
if self.rnd:
|
|
||||||
model_params.append(self.rnd.predictor.state_dict())
|
|
||||||
# broadcast the model parameters
|
|
||||||
torch.distributed.broadcast_object_list(model_params, src=0)
|
|
||||||
# load the model parameters on all GPUs from source GPU
|
|
||||||
self.policy.load_state_dict(model_params[0])
|
|
||||||
if self.rnd:
|
|
||||||
self.rnd.predictor.load_state_dict(model_params[1])
|
|
||||||
|
|
||||||
def reduce_parameters(self):
|
|
||||||
"""Collect gradients from all GPUs and average them.
|
|
||||||
|
|
||||||
This function is called after the backward pass to synchronize the gradients across all GPUs.
|
|
||||||
"""
|
|
||||||
# Create a tensor to store the gradients
|
|
||||||
grads = [param.grad.view(-1) for param in self.policy.parameters() if param.grad is not None]
|
|
||||||
if self.rnd:
|
|
||||||
grads += [param.grad.view(-1) for param in self.rnd.parameters() if param.grad is not None]
|
|
||||||
all_grads = torch.cat(grads)
|
|
||||||
|
|
||||||
# Average the gradients across all GPUs
|
|
||||||
torch.distributed.all_reduce(all_grads, op=torch.distributed.ReduceOp.SUM)
|
|
||||||
all_grads /= self.gpu_world_size
|
|
||||||
|
|
||||||
# Get all parameters
|
|
||||||
all_params = self.policy.parameters()
|
|
||||||
if self.rnd:
|
|
||||||
all_params = chain(all_params, self.rnd.parameters())
|
|
||||||
|
|
||||||
# Update the gradients for all parameters with the reduced gradients
|
|
||||||
offset = 0
|
|
||||||
for param in all_params:
|
|
||||||
if param.grad is not None:
|
|
||||||
numel = param.numel()
|
|
||||||
# copy data back from shared buffer
|
|
||||||
param.grad.data.copy_(all_grads[offset : offset + numel].view_as(param.grad.data))
|
|
||||||
# update the offset for the next parameter
|
|
||||||
offset += numel
|
|
||||||
|
|
@ -1,185 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
|
|
||||||
from rsl_rl.modules import StudentTeacher, StudentTeacherRecurrent
|
|
||||||
from rsl_rl.storage import RolloutStorage
|
|
||||||
from rsl_rl.utils import resolve_optimizer
|
|
||||||
|
|
||||||
|
|
||||||
class Distillation:
|
|
||||||
"""Distillation algorithm for training a student model to mimic a teacher model."""
|
|
||||||
|
|
||||||
policy: StudentTeacher | StudentTeacherRecurrent
|
|
||||||
"""The student teacher model."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
policy,
|
|
||||||
num_learning_epochs=1,
|
|
||||||
gradient_length=15,
|
|
||||||
learning_rate=1e-3,
|
|
||||||
max_grad_norm=None,
|
|
||||||
loss_type="mse",
|
|
||||||
optimizer="adam",
|
|
||||||
device="cpu",
|
|
||||||
# Distributed training parameters
|
|
||||||
multi_gpu_cfg: dict | None = None,
|
|
||||||
):
|
|
||||||
# device-related parameters
|
|
||||||
self.device = device
|
|
||||||
self.is_multi_gpu = multi_gpu_cfg is not None
|
|
||||||
# Multi-GPU parameters
|
|
||||||
if multi_gpu_cfg is not None:
|
|
||||||
self.gpu_global_rank = multi_gpu_cfg["global_rank"]
|
|
||||||
self.gpu_world_size = multi_gpu_cfg["world_size"]
|
|
||||||
else:
|
|
||||||
self.gpu_global_rank = 0
|
|
||||||
self.gpu_world_size = 1
|
|
||||||
|
|
||||||
# distillation components
|
|
||||||
self.policy = policy
|
|
||||||
self.policy.to(self.device)
|
|
||||||
self.storage = None # initialized later
|
|
||||||
|
|
||||||
# initialize the optimizer
|
|
||||||
self.optimizer = resolve_optimizer(optimizer)(self.policy.parameters(), lr=learning_rate)
|
|
||||||
|
|
||||||
# initialize the transition
|
|
||||||
self.transition = RolloutStorage.Transition()
|
|
||||||
self.last_hidden_states = None
|
|
||||||
|
|
||||||
# distillation parameters
|
|
||||||
self.num_learning_epochs = num_learning_epochs
|
|
||||||
self.gradient_length = gradient_length
|
|
||||||
self.learning_rate = learning_rate
|
|
||||||
self.max_grad_norm = max_grad_norm
|
|
||||||
|
|
||||||
# initialize the loss function
|
|
||||||
loss_fn_dict = {
|
|
||||||
"mse": nn.functional.mse_loss,
|
|
||||||
"huber": nn.functional.huber_loss,
|
|
||||||
}
|
|
||||||
if loss_type in loss_fn_dict:
|
|
||||||
self.loss_fn = loss_fn_dict[loss_type]
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown loss type: {loss_type}. Supported types are: {list(loss_fn_dict.keys())}")
|
|
||||||
|
|
||||||
self.num_updates = 0
|
|
||||||
|
|
||||||
def init_storage(self, training_type, num_envs, num_transitions_per_env, obs, actions_shape):
|
|
||||||
# create rollout storage
|
|
||||||
self.storage = RolloutStorage(
|
|
||||||
training_type,
|
|
||||||
num_envs,
|
|
||||||
num_transitions_per_env,
|
|
||||||
obs,
|
|
||||||
actions_shape,
|
|
||||||
self.device,
|
|
||||||
)
|
|
||||||
|
|
||||||
def act(self, obs):
|
|
||||||
# compute the actions
|
|
||||||
self.transition.actions = self.policy.act(obs).detach()
|
|
||||||
self.transition.privileged_actions = self.policy.evaluate(obs).detach()
|
|
||||||
# record the observations
|
|
||||||
self.transition.observations = obs
|
|
||||||
return self.transition.actions
|
|
||||||
|
|
||||||
def process_env_step(self, obs, rewards, dones, extras):
|
|
||||||
# update the normalizers
|
|
||||||
self.policy.update_normalization(obs)
|
|
||||||
|
|
||||||
# record the rewards and dones
|
|
||||||
self.transition.rewards = rewards
|
|
||||||
self.transition.dones = dones
|
|
||||||
# record the transition
|
|
||||||
self.storage.add_transitions(self.transition)
|
|
||||||
self.transition.clear()
|
|
||||||
self.policy.reset(dones)
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
self.num_updates += 1
|
|
||||||
mean_behavior_loss = 0
|
|
||||||
loss = 0
|
|
||||||
cnt = 0
|
|
||||||
|
|
||||||
for epoch in range(self.num_learning_epochs):
|
|
||||||
self.policy.reset(hidden_states=self.last_hidden_states)
|
|
||||||
self.policy.detach_hidden_states()
|
|
||||||
for obs, _, privileged_actions, dones in self.storage.generator():
|
|
||||||
|
|
||||||
# inference the student for gradient computation
|
|
||||||
actions = self.policy.act_inference(obs)
|
|
||||||
|
|
||||||
# behavior cloning loss
|
|
||||||
behavior_loss = self.loss_fn(actions, privileged_actions)
|
|
||||||
|
|
||||||
# total loss
|
|
||||||
loss = loss + behavior_loss
|
|
||||||
mean_behavior_loss += behavior_loss.item()
|
|
||||||
cnt += 1
|
|
||||||
|
|
||||||
# gradient step
|
|
||||||
if cnt % self.gradient_length == 0:
|
|
||||||
self.optimizer.zero_grad()
|
|
||||||
loss.backward()
|
|
||||||
if self.is_multi_gpu:
|
|
||||||
self.reduce_parameters()
|
|
||||||
if self.max_grad_norm:
|
|
||||||
nn.utils.clip_grad_norm_(self.policy.student.parameters(), self.max_grad_norm)
|
|
||||||
self.optimizer.step()
|
|
||||||
self.policy.detach_hidden_states()
|
|
||||||
loss = 0
|
|
||||||
|
|
||||||
# reset dones
|
|
||||||
self.policy.reset(dones.view(-1))
|
|
||||||
self.policy.detach_hidden_states(dones.view(-1))
|
|
||||||
|
|
||||||
mean_behavior_loss /= cnt
|
|
||||||
self.storage.clear()
|
|
||||||
self.last_hidden_states = self.policy.get_hidden_states()
|
|
||||||
self.policy.detach_hidden_states()
|
|
||||||
|
|
||||||
# construct the loss dictionary
|
|
||||||
loss_dict = {"behavior": mean_behavior_loss}
|
|
||||||
|
|
||||||
return loss_dict
|
|
||||||
|
|
||||||
"""
|
|
||||||
Helper functions
|
|
||||||
"""
|
|
||||||
|
|
||||||
def broadcast_parameters(self):
|
|
||||||
"""Broadcast model parameters to all GPUs."""
|
|
||||||
# obtain the model parameters on current GPU
|
|
||||||
model_params = [self.policy.state_dict()]
|
|
||||||
# broadcast the model parameters
|
|
||||||
torch.distributed.broadcast_object_list(model_params, src=0)
|
|
||||||
# load the model parameters on all GPUs from source GPU
|
|
||||||
self.policy.load_state_dict(model_params[0])
|
|
||||||
|
|
||||||
def reduce_parameters(self):
|
|
||||||
"""Collect gradients from all GPUs and average them.
|
|
||||||
|
|
||||||
This function is called after the backward pass to synchronize the gradients across all GPUs.
|
|
||||||
"""
|
|
||||||
# Create a tensor to store the gradients
|
|
||||||
grads = [param.grad.view(-1) for param in self.policy.parameters() if param.grad is not None]
|
|
||||||
all_grads = torch.cat(grads)
|
|
||||||
# Average the gradients across all GPUs
|
|
||||||
torch.distributed.all_reduce(all_grads, op=torch.distributed.ReduceOp.SUM)
|
|
||||||
all_grads /= self.gpu_world_size
|
|
||||||
# Update the gradients for all parameters with the reduced gradients
|
|
||||||
offset = 0
|
|
||||||
for param in self.policy.parameters():
|
|
||||||
if param.grad is not None:
|
|
||||||
numel = param.numel()
|
|
||||||
# copy data back from shared buffer
|
|
||||||
param.grad.data.copy_(all_grads[offset : offset + numel].view_as(param.grad.data))
|
|
||||||
# update the offset for the next parameter
|
|
||||||
offset += numel
|
|
||||||
|
|
@ -1,469 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
import torch.optim as optim
|
|
||||||
from itertools import chain
|
|
||||||
|
|
||||||
from rsl_rl.modules import ActorCritic
|
|
||||||
from rsl_rl.modules.rnd import RandomNetworkDistillation
|
|
||||||
from rsl_rl.storage import RolloutStorage
|
|
||||||
from rsl_rl.utils import string_to_callable
|
|
||||||
|
|
||||||
|
|
||||||
class PPO:
|
|
||||||
"""Proximal Policy Optimization algorithm (https://arxiv.org/abs/1707.06347)."""
|
|
||||||
|
|
||||||
policy: ActorCritic
|
|
||||||
"""The actor critic module."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
policy,
|
|
||||||
num_learning_epochs=5,
|
|
||||||
num_mini_batches=4,
|
|
||||||
clip_param=0.2,
|
|
||||||
gamma=0.99,
|
|
||||||
lam=0.95,
|
|
||||||
value_loss_coef=1.0,
|
|
||||||
entropy_coef=0.01,
|
|
||||||
learning_rate=0.001,
|
|
||||||
max_grad_norm=1.0,
|
|
||||||
use_clipped_value_loss=True,
|
|
||||||
schedule="adaptive",
|
|
||||||
desired_kl=0.01,
|
|
||||||
device="cpu",
|
|
||||||
normalize_advantage_per_mini_batch=False,
|
|
||||||
# RND parameters
|
|
||||||
rnd_cfg: dict | None = None,
|
|
||||||
# Symmetry parameters
|
|
||||||
symmetry_cfg: dict | None = None,
|
|
||||||
# Distributed training parameters
|
|
||||||
multi_gpu_cfg: dict | None = None,
|
|
||||||
):
|
|
||||||
# device-related parameters
|
|
||||||
self.device = device
|
|
||||||
self.is_multi_gpu = multi_gpu_cfg is not None
|
|
||||||
# Multi-GPU parameters
|
|
||||||
if multi_gpu_cfg is not None:
|
|
||||||
self.gpu_global_rank = multi_gpu_cfg["global_rank"]
|
|
||||||
self.gpu_world_size = multi_gpu_cfg["world_size"]
|
|
||||||
else:
|
|
||||||
self.gpu_global_rank = 0
|
|
||||||
self.gpu_world_size = 1
|
|
||||||
|
|
||||||
# RND components
|
|
||||||
if rnd_cfg is not None:
|
|
||||||
# Extract parameters used in ppo
|
|
||||||
rnd_lr = rnd_cfg.pop("learning_rate", 1e-3)
|
|
||||||
# Create RND module
|
|
||||||
self.rnd = RandomNetworkDistillation(device=self.device, **rnd_cfg)
|
|
||||||
# Create RND optimizer
|
|
||||||
params = self.rnd.predictor.parameters()
|
|
||||||
self.rnd_optimizer = optim.Adam(params, lr=rnd_lr)
|
|
||||||
else:
|
|
||||||
self.rnd = None
|
|
||||||
self.rnd_optimizer = None
|
|
||||||
|
|
||||||
# Symmetry components
|
|
||||||
if symmetry_cfg is not None:
|
|
||||||
# Check if symmetry is enabled
|
|
||||||
use_symmetry = symmetry_cfg["use_data_augmentation"] or symmetry_cfg["use_mirror_loss"]
|
|
||||||
# Print that we are not using symmetry
|
|
||||||
if not use_symmetry:
|
|
||||||
print("Symmetry not used for learning. We will use it for logging instead.")
|
|
||||||
# If function is a string then resolve it to a function
|
|
||||||
if isinstance(symmetry_cfg["data_augmentation_func"], str):
|
|
||||||
symmetry_cfg["data_augmentation_func"] = string_to_callable(symmetry_cfg["data_augmentation_func"])
|
|
||||||
# Check valid configuration
|
|
||||||
if symmetry_cfg["use_data_augmentation"] and not callable(symmetry_cfg["data_augmentation_func"]):
|
|
||||||
raise ValueError(
|
|
||||||
"Data augmentation enabled but the function is not callable:"
|
|
||||||
f" {symmetry_cfg['data_augmentation_func']}"
|
|
||||||
)
|
|
||||||
# Store symmetry configuration
|
|
||||||
self.symmetry = symmetry_cfg
|
|
||||||
else:
|
|
||||||
self.symmetry = None
|
|
||||||
|
|
||||||
# PPO components
|
|
||||||
self.policy = policy
|
|
||||||
self.policy.to(self.device)
|
|
||||||
# Create optimizer
|
|
||||||
self.optimizer = optim.Adam(self.policy.parameters(), lr=learning_rate)
|
|
||||||
# Create rollout storage
|
|
||||||
self.storage: RolloutStorage = None # type: ignore
|
|
||||||
self.transition = RolloutStorage.Transition()
|
|
||||||
|
|
||||||
# PPO parameters
|
|
||||||
self.clip_param = clip_param
|
|
||||||
self.num_learning_epochs = num_learning_epochs
|
|
||||||
self.num_mini_batches = num_mini_batches
|
|
||||||
self.value_loss_coef = value_loss_coef
|
|
||||||
self.entropy_coef = entropy_coef
|
|
||||||
self.gamma = gamma
|
|
||||||
self.lam = lam
|
|
||||||
self.max_grad_norm = max_grad_norm
|
|
||||||
self.use_clipped_value_loss = use_clipped_value_loss
|
|
||||||
self.desired_kl = desired_kl
|
|
||||||
self.schedule = schedule
|
|
||||||
self.learning_rate = learning_rate
|
|
||||||
self.normalize_advantage_per_mini_batch = normalize_advantage_per_mini_batch
|
|
||||||
|
|
||||||
def init_storage(self, training_type, num_envs, num_transitions_per_env, obs, actions_shape):
|
|
||||||
# create rollout storage
|
|
||||||
self.storage = RolloutStorage(
|
|
||||||
training_type,
|
|
||||||
num_envs,
|
|
||||||
num_transitions_per_env,
|
|
||||||
obs,
|
|
||||||
actions_shape,
|
|
||||||
self.device,
|
|
||||||
)
|
|
||||||
|
|
||||||
def act(self, obs):
|
|
||||||
if self.policy.is_recurrent:
|
|
||||||
self.transition.hidden_states = self.policy.get_hidden_states()
|
|
||||||
# compute the actions and values
|
|
||||||
self.transition.actions = self.policy.act(obs).detach()
|
|
||||||
self.transition.values = self.policy.evaluate(obs).detach()
|
|
||||||
self.transition.actions_log_prob = self.policy.get_actions_log_prob(self.transition.actions).detach()
|
|
||||||
self.transition.action_mean = self.policy.action_mean.detach()
|
|
||||||
self.transition.action_sigma = self.policy.action_std.detach()
|
|
||||||
# need to record obs before env.step()
|
|
||||||
self.transition.observations = obs
|
|
||||||
return self.transition.actions
|
|
||||||
|
|
||||||
def process_env_step(self, obs, rewards, dones, extras):
|
|
||||||
# update the normalizers
|
|
||||||
self.policy.update_normalization(obs)
|
|
||||||
if self.rnd:
|
|
||||||
self.rnd.update_normalization(obs)
|
|
||||||
|
|
||||||
# Record the rewards and dones
|
|
||||||
# Note: we clone here because later on we bootstrap the rewards based on timeouts
|
|
||||||
self.transition.rewards = rewards.clone()
|
|
||||||
self.transition.dones = dones
|
|
||||||
|
|
||||||
# Compute the intrinsic rewards and add to extrinsic rewards
|
|
||||||
if self.rnd:
|
|
||||||
# Compute the intrinsic rewards
|
|
||||||
self.intrinsic_rewards = self.rnd.get_intrinsic_reward(obs)
|
|
||||||
# Add intrinsic rewards to extrinsic rewards
|
|
||||||
self.transition.rewards += self.intrinsic_rewards
|
|
||||||
|
|
||||||
# Bootstrapping on time outs
|
|
||||||
if "time_outs" in extras:
|
|
||||||
self.transition.rewards += self.gamma * torch.squeeze(
|
|
||||||
self.transition.values * extras["time_outs"].unsqueeze(1).to(self.device), 1
|
|
||||||
)
|
|
||||||
|
|
||||||
# record the transition
|
|
||||||
self.storage.add_transitions(self.transition)
|
|
||||||
self.transition.clear()
|
|
||||||
self.policy.reset(dones)
|
|
||||||
|
|
||||||
def compute_returns(self, obs):
|
|
||||||
# compute value for the last step
|
|
||||||
last_values = self.policy.evaluate(obs).detach()
|
|
||||||
self.storage.compute_returns(
|
|
||||||
last_values, self.gamma, self.lam, normalize_advantage=not self.normalize_advantage_per_mini_batch
|
|
||||||
)
|
|
||||||
|
|
||||||
def update(self): # noqa: C901
|
|
||||||
mean_value_loss = 0
|
|
||||||
mean_surrogate_loss = 0
|
|
||||||
mean_entropy = 0
|
|
||||||
# -- RND loss
|
|
||||||
if self.rnd:
|
|
||||||
mean_rnd_loss = 0
|
|
||||||
else:
|
|
||||||
mean_rnd_loss = None
|
|
||||||
# -- Symmetry loss
|
|
||||||
if self.symmetry:
|
|
||||||
mean_symmetry_loss = 0
|
|
||||||
else:
|
|
||||||
mean_symmetry_loss = None
|
|
||||||
|
|
||||||
# generator for mini batches
|
|
||||||
if self.policy.is_recurrent:
|
|
||||||
generator = self.storage.recurrent_mini_batch_generator(self.num_mini_batches, self.num_learning_epochs)
|
|
||||||
else:
|
|
||||||
generator = self.storage.mini_batch_generator(self.num_mini_batches, self.num_learning_epochs)
|
|
||||||
|
|
||||||
# iterate over batches
|
|
||||||
for (
|
|
||||||
obs_batch,
|
|
||||||
actions_batch,
|
|
||||||
target_values_batch,
|
|
||||||
advantages_batch,
|
|
||||||
returns_batch,
|
|
||||||
old_actions_log_prob_batch,
|
|
||||||
old_mu_batch,
|
|
||||||
old_sigma_batch,
|
|
||||||
hid_states_batch,
|
|
||||||
masks_batch,
|
|
||||||
) in generator:
|
|
||||||
|
|
||||||
# number of augmentations per sample
|
|
||||||
# we start with 1 and increase it if we use symmetry augmentation
|
|
||||||
num_aug = 1
|
|
||||||
# original batch size
|
|
||||||
# we assume policy group is always there and needs augmentation
|
|
||||||
original_batch_size = obs_batch.batch_size[0]
|
|
||||||
|
|
||||||
# check if we should normalize advantages per mini batch
|
|
||||||
if self.normalize_advantage_per_mini_batch:
|
|
||||||
with torch.no_grad():
|
|
||||||
advantages_batch = (advantages_batch - advantages_batch.mean()) / (advantages_batch.std() + 1e-8)
|
|
||||||
|
|
||||||
# Perform symmetric augmentation
|
|
||||||
if self.symmetry and self.symmetry["use_data_augmentation"]:
|
|
||||||
# augmentation using symmetry
|
|
||||||
data_augmentation_func = self.symmetry["data_augmentation_func"]
|
|
||||||
# returned shape: [batch_size * num_aug, ...]
|
|
||||||
obs_batch, actions_batch = data_augmentation_func(
|
|
||||||
obs=obs_batch,
|
|
||||||
actions=actions_batch,
|
|
||||||
env=self.symmetry["_env"],
|
|
||||||
)
|
|
||||||
# compute number of augmentations per sample
|
|
||||||
# we assume policy group is always there and needs augmentation
|
|
||||||
num_aug = int(obs_batch.batch_size[0] / original_batch_size)
|
|
||||||
# repeat the rest of the batch
|
|
||||||
# -- actor
|
|
||||||
old_actions_log_prob_batch = old_actions_log_prob_batch.repeat(num_aug, 1)
|
|
||||||
# -- critic
|
|
||||||
target_values_batch = target_values_batch.repeat(num_aug, 1)
|
|
||||||
advantages_batch = advantages_batch.repeat(num_aug, 1)
|
|
||||||
returns_batch = returns_batch.repeat(num_aug, 1)
|
|
||||||
|
|
||||||
# Recompute actions log prob and entropy for current batch of transitions
|
|
||||||
# Note: we need to do this because we updated the policy with the new parameters
|
|
||||||
# -- actor
|
|
||||||
self.policy.act(obs_batch, masks=masks_batch, hidden_states=hid_states_batch[0])
|
|
||||||
actions_log_prob_batch = self.policy.get_actions_log_prob(actions_batch)
|
|
||||||
# -- critic
|
|
||||||
value_batch = self.policy.evaluate(obs_batch, masks=masks_batch, hidden_states=hid_states_batch[1])
|
|
||||||
# -- entropy
|
|
||||||
# we only keep the entropy of the first augmentation (the original one)
|
|
||||||
mu_batch = self.policy.action_mean[:original_batch_size]
|
|
||||||
sigma_batch = self.policy.action_std[:original_batch_size]
|
|
||||||
entropy_batch = self.policy.entropy[:original_batch_size]
|
|
||||||
|
|
||||||
# KL
|
|
||||||
if self.desired_kl is not None and self.schedule == "adaptive":
|
|
||||||
with torch.inference_mode():
|
|
||||||
kl = torch.sum(
|
|
||||||
torch.log(sigma_batch / old_sigma_batch + 1.0e-5)
|
|
||||||
+ (torch.square(old_sigma_batch) + torch.square(old_mu_batch - mu_batch))
|
|
||||||
/ (2.0 * torch.square(sigma_batch))
|
|
||||||
- 0.5,
|
|
||||||
axis=-1,
|
|
||||||
)
|
|
||||||
kl_mean = torch.mean(kl)
|
|
||||||
|
|
||||||
# Reduce the KL divergence across all GPUs
|
|
||||||
if self.is_multi_gpu:
|
|
||||||
torch.distributed.all_reduce(kl_mean, op=torch.distributed.ReduceOp.SUM)
|
|
||||||
kl_mean /= self.gpu_world_size
|
|
||||||
|
|
||||||
# Update the learning rate
|
|
||||||
# Perform this adaptation only on the main process
|
|
||||||
# TODO: Is this needed? If KL-divergence is the "same" across all GPUs,
|
|
||||||
# then the learning rate should be the same across all GPUs.
|
|
||||||
if self.gpu_global_rank == 0:
|
|
||||||
if kl_mean > self.desired_kl * 2.0:
|
|
||||||
self.learning_rate = max(1e-5, self.learning_rate / 1.5)
|
|
||||||
elif kl_mean < self.desired_kl / 2.0 and kl_mean > 0.0:
|
|
||||||
self.learning_rate = min(1e-2, self.learning_rate * 1.5)
|
|
||||||
|
|
||||||
# Update the learning rate for all GPUs
|
|
||||||
if self.is_multi_gpu:
|
|
||||||
lr_tensor = torch.tensor(self.learning_rate, device=self.device)
|
|
||||||
torch.distributed.broadcast(lr_tensor, src=0)
|
|
||||||
self.learning_rate = lr_tensor.item()
|
|
||||||
|
|
||||||
# Update the learning rate for all parameter groups
|
|
||||||
for param_group in self.optimizer.param_groups:
|
|
||||||
param_group["lr"] = self.learning_rate
|
|
||||||
|
|
||||||
# Surrogate loss
|
|
||||||
ratio = torch.exp(actions_log_prob_batch - torch.squeeze(old_actions_log_prob_batch))
|
|
||||||
surrogate = -torch.squeeze(advantages_batch) * ratio
|
|
||||||
surrogate_clipped = -torch.squeeze(advantages_batch) * torch.clamp(
|
|
||||||
ratio, 1.0 - self.clip_param, 1.0 + self.clip_param
|
|
||||||
)
|
|
||||||
surrogate_loss = torch.max(surrogate, surrogate_clipped).mean()
|
|
||||||
|
|
||||||
# Value function loss
|
|
||||||
if self.use_clipped_value_loss:
|
|
||||||
value_clipped = target_values_batch + (value_batch - target_values_batch).clamp(
|
|
||||||
-self.clip_param, self.clip_param
|
|
||||||
)
|
|
||||||
value_losses = (value_batch - returns_batch).pow(2)
|
|
||||||
value_losses_clipped = (value_clipped - returns_batch).pow(2)
|
|
||||||
value_loss = torch.max(value_losses, value_losses_clipped).mean()
|
|
||||||
else:
|
|
||||||
value_loss = (returns_batch - value_batch).pow(2).mean()
|
|
||||||
|
|
||||||
loss = surrogate_loss + self.value_loss_coef * value_loss - self.entropy_coef * entropy_batch.mean()
|
|
||||||
|
|
||||||
# Symmetry loss
|
|
||||||
if self.symmetry:
|
|
||||||
# obtain the symmetric actions
|
|
||||||
# if we did augmentation before then we don't need to augment again
|
|
||||||
if not self.symmetry["use_data_augmentation"]:
|
|
||||||
data_augmentation_func = self.symmetry["data_augmentation_func"]
|
|
||||||
obs_batch, _ = data_augmentation_func(obs=obs_batch, actions=None, env=self.symmetry["_env"])
|
|
||||||
# compute number of augmentations per sample
|
|
||||||
num_aug = int(obs_batch.shape[0] / original_batch_size)
|
|
||||||
|
|
||||||
# actions predicted by the actor for symmetrically-augmented observations
|
|
||||||
mean_actions_batch = self.policy.act_inference(obs_batch.detach().clone())
|
|
||||||
|
|
||||||
# compute the symmetrically augmented actions
|
|
||||||
# note: we are assuming the first augmentation is the original one.
|
|
||||||
# We do not use the action_batch from earlier since that action was sampled from the distribution.
|
|
||||||
# However, the symmetry loss is computed using the mean of the distribution.
|
|
||||||
action_mean_orig = mean_actions_batch[:original_batch_size]
|
|
||||||
_, actions_mean_symm_batch = data_augmentation_func(
|
|
||||||
obs=None, actions=action_mean_orig, env=self.symmetry["_env"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# compute the loss (we skip the first augmentation as it is the original one)
|
|
||||||
mse_loss = torch.nn.MSELoss()
|
|
||||||
symmetry_loss = mse_loss(
|
|
||||||
mean_actions_batch[original_batch_size:], actions_mean_symm_batch.detach()[original_batch_size:]
|
|
||||||
)
|
|
||||||
# add the loss to the total loss
|
|
||||||
if self.symmetry["use_mirror_loss"]:
|
|
||||||
loss += self.symmetry["mirror_loss_coeff"] * symmetry_loss
|
|
||||||
else:
|
|
||||||
symmetry_loss = symmetry_loss.detach()
|
|
||||||
|
|
||||||
# Random Network Distillation loss
|
|
||||||
# TODO: Move this processing to inside RND module.
|
|
||||||
if self.rnd:
|
|
||||||
# extract the rnd_state
|
|
||||||
# TODO: Check if we still need torch no grad. It is just an affine transformation.
|
|
||||||
with torch.no_grad():
|
|
||||||
rnd_state_batch = self.rnd.get_rnd_state(obs_batch[:original_batch_size])
|
|
||||||
rnd_state_batch = self.rnd.state_normalizer(rnd_state_batch)
|
|
||||||
# predict the embedding and the target
|
|
||||||
predicted_embedding = self.rnd.predictor(rnd_state_batch)
|
|
||||||
target_embedding = self.rnd.target(rnd_state_batch).detach()
|
|
||||||
# compute the loss as the mean squared error
|
|
||||||
mseloss = torch.nn.MSELoss()
|
|
||||||
rnd_loss = mseloss(predicted_embedding, target_embedding)
|
|
||||||
|
|
||||||
# Compute the gradients
|
|
||||||
# -- For PPO
|
|
||||||
self.optimizer.zero_grad()
|
|
||||||
loss.backward()
|
|
||||||
# -- For RND
|
|
||||||
if self.rnd:
|
|
||||||
self.rnd_optimizer.zero_grad() # type: ignore
|
|
||||||
rnd_loss.backward()
|
|
||||||
|
|
||||||
# Collect gradients from all GPUs
|
|
||||||
if self.is_multi_gpu:
|
|
||||||
self.reduce_parameters()
|
|
||||||
|
|
||||||
# Apply the gradients
|
|
||||||
# -- For PPO
|
|
||||||
nn.utils.clip_grad_norm_(self.policy.parameters(), self.max_grad_norm)
|
|
||||||
self.optimizer.step()
|
|
||||||
# -- For RND
|
|
||||||
if self.rnd_optimizer:
|
|
||||||
self.rnd_optimizer.step()
|
|
||||||
|
|
||||||
# Store the losses
|
|
||||||
mean_value_loss += value_loss.item()
|
|
||||||
mean_surrogate_loss += surrogate_loss.item()
|
|
||||||
mean_entropy += entropy_batch.mean().item()
|
|
||||||
# -- RND loss
|
|
||||||
if mean_rnd_loss is not None:
|
|
||||||
mean_rnd_loss += rnd_loss.item()
|
|
||||||
# -- Symmetry loss
|
|
||||||
if mean_symmetry_loss is not None:
|
|
||||||
mean_symmetry_loss += symmetry_loss.item()
|
|
||||||
|
|
||||||
# -- For PPO
|
|
||||||
num_updates = self.num_learning_epochs * self.num_mini_batches
|
|
||||||
mean_value_loss /= num_updates
|
|
||||||
mean_surrogate_loss /= num_updates
|
|
||||||
mean_entropy /= num_updates
|
|
||||||
# -- For RND
|
|
||||||
if mean_rnd_loss is not None:
|
|
||||||
mean_rnd_loss /= num_updates
|
|
||||||
# -- For Symmetry
|
|
||||||
if mean_symmetry_loss is not None:
|
|
||||||
mean_symmetry_loss /= num_updates
|
|
||||||
# -- Clear the storage
|
|
||||||
self.storage.clear()
|
|
||||||
|
|
||||||
# construct the loss dictionary
|
|
||||||
loss_dict = {
|
|
||||||
"value_function": mean_value_loss,
|
|
||||||
"surrogate": mean_surrogate_loss,
|
|
||||||
"entropy": mean_entropy,
|
|
||||||
}
|
|
||||||
if self.rnd:
|
|
||||||
loss_dict["rnd"] = mean_rnd_loss
|
|
||||||
if self.symmetry:
|
|
||||||
loss_dict["symmetry"] = mean_symmetry_loss
|
|
||||||
|
|
||||||
return loss_dict
|
|
||||||
|
|
||||||
"""
|
|
||||||
Helper functions
|
|
||||||
"""
|
|
||||||
|
|
||||||
def broadcast_parameters(self):
|
|
||||||
"""Broadcast model parameters to all GPUs."""
|
|
||||||
# obtain the model parameters on current GPU
|
|
||||||
model_params = [self.policy.state_dict()]
|
|
||||||
if self.rnd:
|
|
||||||
model_params.append(self.rnd.predictor.state_dict())
|
|
||||||
# broadcast the model parameters
|
|
||||||
torch.distributed.broadcast_object_list(model_params, src=0)
|
|
||||||
# load the model parameters on all GPUs from source GPU
|
|
||||||
self.policy.load_state_dict(model_params[0])
|
|
||||||
if self.rnd:
|
|
||||||
self.rnd.predictor.load_state_dict(model_params[1])
|
|
||||||
|
|
||||||
def reduce_parameters(self):
|
|
||||||
"""Collect gradients from all GPUs and average them.
|
|
||||||
|
|
||||||
This function is called after the backward pass to synchronize the gradients across all GPUs.
|
|
||||||
"""
|
|
||||||
# Create a tensor to store the gradients
|
|
||||||
grads = [param.grad.view(-1) for param in self.policy.parameters() if param.grad is not None]
|
|
||||||
if self.rnd:
|
|
||||||
grads += [param.grad.view(-1) for param in self.rnd.parameters() if param.grad is not None]
|
|
||||||
all_grads = torch.cat(grads)
|
|
||||||
|
|
||||||
# Average the gradients across all GPUs
|
|
||||||
torch.distributed.all_reduce(all_grads, op=torch.distributed.ReduceOp.SUM)
|
|
||||||
all_grads /= self.gpu_world_size
|
|
||||||
|
|
||||||
# Get all parameters
|
|
||||||
all_params = self.policy.parameters()
|
|
||||||
if self.rnd:
|
|
||||||
all_params = chain(all_params, self.rnd.parameters())
|
|
||||||
|
|
||||||
# Update the gradients for all parameters with the reduced gradients
|
|
||||||
offset = 0
|
|
||||||
for param in all_params:
|
|
||||||
if param.grad is not None:
|
|
||||||
numel = param.numel()
|
|
||||||
# copy data back from shared buffer
|
|
||||||
param.grad.data.copy_(all_grads[offset : offset + numel].view_as(param.grad.data))
|
|
||||||
# update the offset for the next parameter
|
|
||||||
offset += numel
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
"""Submodule defining the environment definitions."""
|
|
||||||
|
|
||||||
from .vec_env import VecEnv
|
|
||||||
|
|
||||||
__all__ = ["VecEnv"]
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
from tensordict import TensorDict
|
|
||||||
|
|
||||||
|
|
||||||
class VecEnv(ABC):
|
|
||||||
"""Abstract class for a vectorized environment.
|
|
||||||
|
|
||||||
The vectorized environment is a collection of environments that are synchronized. This means that
|
|
||||||
the same type of action is applied to all environments and the same type of observation is returned from all
|
|
||||||
environments.
|
|
||||||
"""
|
|
||||||
|
|
||||||
num_envs: int
|
|
||||||
"""Number of environments."""
|
|
||||||
|
|
||||||
num_actions: int
|
|
||||||
"""Number of actions."""
|
|
||||||
|
|
||||||
max_episode_length: int | torch.Tensor
|
|
||||||
|
|
||||||
max_episode_length_s: float
|
|
||||||
"""Maximum episode length.
|
|
||||||
|
|
||||||
The maximum episode length can be a scalar or a tensor. If it is a scalar, it is the same for all environments.
|
|
||||||
If it is a tensor, it is the maximum episode length for each environment. This is useful for dynamic episode
|
|
||||||
lengths.
|
|
||||||
"""
|
|
||||||
|
|
||||||
episode_length_buf: torch.Tensor
|
|
||||||
"""Buffer for current episode lengths."""
|
|
||||||
|
|
||||||
device: torch.device | str
|
|
||||||
"""Device to use."""
|
|
||||||
|
|
||||||
cfg: dict | object
|
|
||||||
"""Configuration object."""
|
|
||||||
|
|
||||||
reset_env_ids: torch.Tensor | None = None
|
|
||||||
|
|
||||||
contact_phase: torch.Tensor | None = None
|
|
||||||
"""
|
|
||||||
Operations.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_observations(self) -> TensorDict:
|
|
||||||
"""Return the current observations.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
observations (TensorDict): Observations from the environment.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_amp_observations(self) -> TensorDict:
|
|
||||||
"""Return the current AMP observations.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
observations (TensorDict): Observations from the environment.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def step(self, actions: torch.Tensor) -> tuple[TensorDict, torch.Tensor, torch.Tensor, dict]:
|
|
||||||
"""Apply input action to the environment.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
actions (torch.Tensor): Input actions to apply. Shape: (num_envs, num_actions)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
observations (TensorDict): Observations from the environment.
|
|
||||||
rewards (torch.Tensor): Rewards from the environment. Shape: (num_envs,)
|
|
||||||
dones (torch.Tensor): Done flags from the environment. Shape: (num_envs,)
|
|
||||||
extras (dict): Extra information from the environment.
|
|
||||||
|
|
||||||
Observations:
|
|
||||||
|
|
||||||
The observations TensorDict usually contains multiple observation groups. The `obs_groups`
|
|
||||||
dictionary of the runner configuration specifies which observation groups are used for which
|
|
||||||
purpose, i.e., it maps the available observation groups to observation sets. The observation sets
|
|
||||||
(keys of the `obs_groups` dictionary) currently used by rsl_rl are:
|
|
||||||
|
|
||||||
- "policy": Specified observation groups are used as input to the actor/student network.
|
|
||||||
- "critic": Specified observation groups are used as input to the critic network.
|
|
||||||
- "teacher": Specified observation groups are used as input to the teacher network.
|
|
||||||
- "rnd_state": Specified observation groups are used as input to the RND network.
|
|
||||||
|
|
||||||
Incomplete or incorrect configurations are handled in the `resolve_obs_groups()` function in
|
|
||||||
`rsl_rl/utils/utils.py`.
|
|
||||||
|
|
||||||
Extras:
|
|
||||||
|
|
||||||
The extras dictionary includes metrics such as the episode reward, episode length, etc. The following
|
|
||||||
dictionary keys are used by rsl_rl:
|
|
||||||
|
|
||||||
- "time_outs" (torch.Tensor): Timeouts for the environments. These correspond to terminations that
|
|
||||||
happen due to time limits and not due to the environment reaching a terminal state. This is useful
|
|
||||||
for environments that have a fixed episode length.
|
|
||||||
|
|
||||||
- "log" (dict[str, float | torch.Tensor]): Additional information for logging and debugging purposes.
|
|
||||||
The key should be a string and start with "/" for namespacing. The value can be a scalar or a
|
|
||||||
tensor. If it is a tensor, the mean of the tensor is used for logging.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
"""Definitions for neural-network components for RL-agents."""
|
|
||||||
|
|
||||||
from .actor_critic import ActorCritic
|
|
||||||
from .actor_critic_recurrent import ActorCriticRecurrent
|
|
||||||
from .rnd import *
|
|
||||||
from .student_teacher import StudentTeacher
|
|
||||||
from .student_teacher_recurrent import StudentTeacherRecurrent
|
|
||||||
from .symmetry import *
|
|
||||||
from .discriminator_multi import DiscriminatorMulti
|
|
||||||
__all__ = [
|
|
||||||
"ActorCritic",
|
|
||||||
"ActorCriticRecurrent",
|
|
||||||
"StudentTeacher",
|
|
||||||
"StudentTeacherRecurrent",
|
|
||||||
"DiscriminatorMulti",
|
|
||||||
]
|
|
||||||
|
|
@ -1,195 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
from torch.distributions import Normal
|
|
||||||
|
|
||||||
from rsl_rl.networks import MLP, EmpiricalNormalization
|
|
||||||
|
|
||||||
|
|
||||||
class ActorCritic(nn.Module):
|
|
||||||
is_recurrent = False
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
obs,
|
|
||||||
obs_groups,
|
|
||||||
num_actions,
|
|
||||||
actor_obs_normalization=False,
|
|
||||||
critic_obs_normalization=False,
|
|
||||||
actor_hidden_dims=[256, 256, 256],
|
|
||||||
critic_hidden_dims=[256, 256, 256],
|
|
||||||
activation="elu",
|
|
||||||
init_noise_std=1.0,
|
|
||||||
noise_std_type: str = "scalar",
|
|
||||||
state_dependent_std=False,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
if kwargs:
|
|
||||||
print(
|
|
||||||
"ActorCritic.__init__ got unexpected arguments, which will be ignored: "
|
|
||||||
+ str([key for key in kwargs.keys()])
|
|
||||||
)
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
# get the observation dimensions
|
|
||||||
self.obs_groups = obs_groups
|
|
||||||
num_actor_obs = 0
|
|
||||||
for obs_group in obs_groups["policy"]:
|
|
||||||
assert len(obs[obs_group].shape) == 2, "The ActorCritic module only supports 1D observations."
|
|
||||||
num_actor_obs += obs[obs_group].shape[-1]
|
|
||||||
num_critic_obs = 0
|
|
||||||
for obs_group in obs_groups["critic"]:
|
|
||||||
assert len(obs[obs_group].shape) == 2, "The ActorCritic module only supports 1D observations."
|
|
||||||
num_critic_obs += obs[obs_group].shape[-1]
|
|
||||||
|
|
||||||
self.state_dependent_std = state_dependent_std
|
|
||||||
# actor
|
|
||||||
if self.state_dependent_std:
|
|
||||||
self.actor = MLP(num_actor_obs, [2, num_actions], actor_hidden_dims, activation)
|
|
||||||
else:
|
|
||||||
self.actor = MLP(num_actor_obs, num_actions, actor_hidden_dims, activation)
|
|
||||||
# actor observation normalization
|
|
||||||
self.actor_obs_normalization = actor_obs_normalization
|
|
||||||
if actor_obs_normalization:
|
|
||||||
self.actor_obs_normalizer = EmpiricalNormalization(num_actor_obs)
|
|
||||||
else:
|
|
||||||
self.actor_obs_normalizer = torch.nn.Identity()
|
|
||||||
print(f"Actor MLP: {self.actor}")
|
|
||||||
|
|
||||||
# critic
|
|
||||||
self.critic = MLP(num_critic_obs, 1, critic_hidden_dims, activation)
|
|
||||||
# critic observation normalization
|
|
||||||
self.critic_obs_normalization = critic_obs_normalization
|
|
||||||
if critic_obs_normalization:
|
|
||||||
self.critic_obs_normalizer = EmpiricalNormalization(num_critic_obs)
|
|
||||||
else:
|
|
||||||
self.critic_obs_normalizer = torch.nn.Identity()
|
|
||||||
print(f"Critic MLP: {self.critic}")
|
|
||||||
|
|
||||||
# Action noise
|
|
||||||
self.noise_std_type = noise_std_type
|
|
||||||
if self.state_dependent_std:
|
|
||||||
torch.nn.init.zeros_(self.actor[-2].weight[num_actions:])
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
torch.nn.init.constant_(self.actor[-2].bias[num_actions:], init_noise_std)
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
torch.nn.init.constant_(
|
|
||||||
self.actor[-2].bias[num_actions:], torch.log(torch.tensor(init_noise_std + 1e-7))
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
else:
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
self.std = nn.Parameter(init_noise_std * torch.ones(num_actions))
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
self.log_std = nn.Parameter(torch.log(init_noise_std * torch.ones(num_actions)))
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
|
|
||||||
# Action distribution (populated in update_distribution)
|
|
||||||
self.distribution = None
|
|
||||||
# disable args validation for speedup
|
|
||||||
Normal.set_default_validate_args(False)
|
|
||||||
|
|
||||||
def reset(self, dones=None):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def forward(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@property
|
|
||||||
def action_mean(self):
|
|
||||||
return self.distribution.mean
|
|
||||||
|
|
||||||
@property
|
|
||||||
def action_std(self):
|
|
||||||
return self.distribution.stddev
|
|
||||||
|
|
||||||
@property
|
|
||||||
def entropy(self):
|
|
||||||
return self.distribution.entropy().sum(dim=-1)
|
|
||||||
|
|
||||||
def update_distribution(self, obs):
|
|
||||||
if self.state_dependent_std:
|
|
||||||
# compute mean and standard deviation
|
|
||||||
mean_and_std = self.actor(obs)
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
mean, std = torch.unbind(mean_and_std, dim=-2)
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
mean, log_std = torch.unbind(mean_and_std, dim=-2)
|
|
||||||
std = torch.exp(log_std)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
else:
|
|
||||||
# compute mean
|
|
||||||
mean = self.actor(obs)
|
|
||||||
# compute standard deviation
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
std = self.std.expand_as(mean)
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
std = torch.exp(self.log_std).expand_as(mean)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
# create distribution
|
|
||||||
self.distribution = Normal(mean, std)
|
|
||||||
|
|
||||||
def act(self, obs, **kwargs):
|
|
||||||
obs = self.get_actor_obs(obs)
|
|
||||||
obs = self.actor_obs_normalizer(obs)
|
|
||||||
self.update_distribution(obs)
|
|
||||||
return self.distribution.sample()
|
|
||||||
|
|
||||||
def act_inference(self, obs):
|
|
||||||
obs = self.get_actor_obs(obs)
|
|
||||||
obs = self.actor_obs_normalizer(obs)
|
|
||||||
return self.actor(obs)
|
|
||||||
|
|
||||||
def evaluate(self, obs, **kwargs):
|
|
||||||
obs = self.get_critic_obs(obs)
|
|
||||||
obs = self.critic_obs_normalizer(obs)
|
|
||||||
return self.critic(obs)
|
|
||||||
|
|
||||||
def get_actor_obs(self, obs):
|
|
||||||
obs_list = []
|
|
||||||
for obs_group in self.obs_groups["policy"]:
|
|
||||||
obs_list.append(obs[obs_group])
|
|
||||||
return torch.cat(obs_list, dim=-1)
|
|
||||||
|
|
||||||
def get_critic_obs(self, obs):
|
|
||||||
obs_list = []
|
|
||||||
for obs_group in self.obs_groups["critic"]:
|
|
||||||
obs_list.append(obs[obs_group])
|
|
||||||
return torch.cat(obs_list, dim=-1)
|
|
||||||
|
|
||||||
def get_actions_log_prob(self, actions):
|
|
||||||
return self.distribution.log_prob(actions).sum(dim=-1)
|
|
||||||
|
|
||||||
def update_normalization(self, obs):
|
|
||||||
if self.actor_obs_normalization:
|
|
||||||
actor_obs = self.get_actor_obs(obs)
|
|
||||||
self.actor_obs_normalizer.update(actor_obs)
|
|
||||||
if self.critic_obs_normalization:
|
|
||||||
critic_obs = self.get_critic_obs(obs)
|
|
||||||
self.critic_obs_normalizer.update(critic_obs)
|
|
||||||
|
|
||||||
def load_state_dict(self, state_dict, strict=True):
|
|
||||||
"""Load the parameters of the actor-critic model.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
state_dict (dict): State dictionary of the model.
|
|
||||||
strict (bool): Whether to strictly enforce that the keys in state_dict match the keys returned by this
|
|
||||||
module's state_dict() function.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: Whether this training resumes a previous training. This flag is used by the `load()` function of
|
|
||||||
`OnPolicyRunner` to determine how to load further parameters (relevant for, e.g., distillation).
|
|
||||||
"""
|
|
||||||
|
|
||||||
super().load_state_dict(state_dict, strict=strict)
|
|
||||||
return True # training resumes
|
|
||||||
|
|
@ -1,218 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
import warnings
|
|
||||||
from torch.distributions import Normal
|
|
||||||
|
|
||||||
from rsl_rl.networks import MLP, EmpiricalNormalization, Memory
|
|
||||||
|
|
||||||
|
|
||||||
class ActorCriticRecurrent(nn.Module):
|
|
||||||
is_recurrent = True
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
obs,
|
|
||||||
obs_groups,
|
|
||||||
num_actions,
|
|
||||||
actor_obs_normalization=False,
|
|
||||||
critic_obs_normalization=False,
|
|
||||||
actor_hidden_dims=[256, 256, 256],
|
|
||||||
critic_hidden_dims=[256, 256, 256],
|
|
||||||
activation="elu",
|
|
||||||
init_noise_std=1.0,
|
|
||||||
noise_std_type: str = "scalar",
|
|
||||||
state_dependent_std=False,
|
|
||||||
rnn_type="lstm",
|
|
||||||
rnn_hidden_dim=256,
|
|
||||||
rnn_num_layers=1,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
if "rnn_hidden_size" in kwargs:
|
|
||||||
warnings.warn(
|
|
||||||
"The argument `rnn_hidden_size` is deprecated and will be removed in a future version. "
|
|
||||||
"Please use `rnn_hidden_dim` instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
if rnn_hidden_dim == 256: # Only override if the new argument is at its default
|
|
||||||
rnn_hidden_dim = kwargs.pop("rnn_hidden_size")
|
|
||||||
if kwargs:
|
|
||||||
print(
|
|
||||||
"ActorCriticRecurrent.__init__ got unexpected arguments, which will be ignored: " + str(kwargs.keys()),
|
|
||||||
)
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
# get the observation dimensions
|
|
||||||
self.obs_groups = obs_groups
|
|
||||||
num_actor_obs = 0
|
|
||||||
for obs_group in obs_groups["policy"]:
|
|
||||||
assert len(obs[obs_group].shape) == 2, "The ActorCriticRecurrent module only supports 1D observations."
|
|
||||||
num_actor_obs += obs[obs_group].shape[-1]
|
|
||||||
num_critic_obs = 0
|
|
||||||
for obs_group in obs_groups["critic"]:
|
|
||||||
assert len(obs[obs_group].shape) == 2, "The ActorCriticRecurrent module only supports 1D observations."
|
|
||||||
num_critic_obs += obs[obs_group].shape[-1]
|
|
||||||
|
|
||||||
self.state_dependent_std = state_dependent_std
|
|
||||||
# actor
|
|
||||||
self.memory_a = Memory(num_actor_obs, type=rnn_type, num_layers=rnn_num_layers, hidden_size=rnn_hidden_dim)
|
|
||||||
if self.state_dependent_std:
|
|
||||||
self.actor = MLP(rnn_hidden_dim, [2, num_actions], actor_hidden_dims, activation)
|
|
||||||
else:
|
|
||||||
self.actor = MLP(rnn_hidden_dim, num_actions, actor_hidden_dims, activation)
|
|
||||||
|
|
||||||
# actor observation normalization
|
|
||||||
self.actor_obs_normalization = actor_obs_normalization
|
|
||||||
if actor_obs_normalization:
|
|
||||||
self.actor_obs_normalizer = EmpiricalNormalization(num_actor_obs)
|
|
||||||
else:
|
|
||||||
self.actor_obs_normalizer = torch.nn.Identity()
|
|
||||||
print(f"Actor RNN: {self.memory_a}")
|
|
||||||
print(f"Actor MLP: {self.actor}")
|
|
||||||
|
|
||||||
# critic
|
|
||||||
self.memory_c = Memory(num_critic_obs, type=rnn_type, num_layers=rnn_num_layers, hidden_size=rnn_hidden_dim)
|
|
||||||
self.critic = MLP(rnn_hidden_dim, 1, critic_hidden_dims, activation)
|
|
||||||
# critic observation normalization
|
|
||||||
self.critic_obs_normalization = critic_obs_normalization
|
|
||||||
if critic_obs_normalization:
|
|
||||||
self.critic_obs_normalizer = EmpiricalNormalization(num_critic_obs)
|
|
||||||
else:
|
|
||||||
self.critic_obs_normalizer = torch.nn.Identity()
|
|
||||||
print(f"Critic RNN: {self.memory_c}")
|
|
||||||
print(f"Critic MLP: {self.critic}")
|
|
||||||
|
|
||||||
# Action noise
|
|
||||||
self.noise_std_type = noise_std_type
|
|
||||||
if self.state_dependent_std:
|
|
||||||
torch.nn.init.zeros_(self.actor[-2].weight[num_actions:])
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
torch.nn.init.constant_(self.actor[-2].bias[num_actions:], init_noise_std)
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
torch.nn.init.constant_(
|
|
||||||
self.actor[-2].bias[num_actions:], torch.log(torch.tensor(init_noise_std + 1e-7))
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
else:
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
self.std = nn.Parameter(init_noise_std * torch.ones(num_actions))
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
self.log_std = nn.Parameter(torch.log(init_noise_std * torch.ones(num_actions)))
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
|
|
||||||
# Action distribution (populated in update_distribution)
|
|
||||||
self.distribution = None
|
|
||||||
# disable args validation for speedup
|
|
||||||
Normal.set_default_validate_args(False)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def action_mean(self):
|
|
||||||
return self.distribution.mean
|
|
||||||
|
|
||||||
@property
|
|
||||||
def action_std(self):
|
|
||||||
return self.distribution.stddev
|
|
||||||
|
|
||||||
@property
|
|
||||||
def entropy(self):
|
|
||||||
return self.distribution.entropy().sum(dim=-1)
|
|
||||||
|
|
||||||
def reset(self, dones=None):
|
|
||||||
self.memory_a.reset(dones)
|
|
||||||
self.memory_c.reset(dones)
|
|
||||||
|
|
||||||
def forward(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update_distribution(self, obs):
|
|
||||||
if self.state_dependent_std:
|
|
||||||
# compute mean and standard deviation
|
|
||||||
mean_and_std = self.actor(obs)
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
mean, std = torch.unbind(mean_and_std, dim=-2)
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
mean, log_std = torch.unbind(mean_and_std, dim=-2)
|
|
||||||
std = torch.exp(log_std)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
else:
|
|
||||||
# compute mean
|
|
||||||
mean = self.actor(obs)
|
|
||||||
# compute standard deviation
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
std = self.std.expand_as(mean)
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
std = torch.exp(self.log_std).expand_as(mean)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
# create distribution
|
|
||||||
self.distribution = Normal(mean, std)
|
|
||||||
|
|
||||||
def act(self, obs, masks=None, hidden_states=None):
|
|
||||||
obs = self.get_actor_obs(obs)
|
|
||||||
obs = self.actor_obs_normalizer(obs)
|
|
||||||
out_mem = self.memory_a(obs, masks, hidden_states).squeeze(0)
|
|
||||||
self.update_distribution(out_mem)
|
|
||||||
return self.distribution.sample()
|
|
||||||
|
|
||||||
def act_inference(self, obs):
|
|
||||||
obs = self.get_actor_obs(obs)
|
|
||||||
obs = self.actor_obs_normalizer(obs)
|
|
||||||
out_mem = self.memory_a(obs).squeeze(0)
|
|
||||||
return self.actor(out_mem)
|
|
||||||
|
|
||||||
def evaluate(self, obs, masks=None, hidden_states=None):
|
|
||||||
obs = self.get_critic_obs(obs)
|
|
||||||
obs = self.critic_obs_normalizer(obs)
|
|
||||||
out_mem = self.memory_c(obs, masks, hidden_states).squeeze(0)
|
|
||||||
return self.critic(out_mem)
|
|
||||||
|
|
||||||
def get_actor_obs(self, obs):
|
|
||||||
obs_list = []
|
|
||||||
for obs_group in self.obs_groups["policy"]:
|
|
||||||
obs_list.append(obs[obs_group])
|
|
||||||
return torch.cat(obs_list, dim=-1)
|
|
||||||
|
|
||||||
def get_critic_obs(self, obs):
|
|
||||||
obs_list = []
|
|
||||||
for obs_group in self.obs_groups["critic"]:
|
|
||||||
obs_list.append(obs[obs_group])
|
|
||||||
return torch.cat(obs_list, dim=-1)
|
|
||||||
|
|
||||||
def get_actions_log_prob(self, actions):
|
|
||||||
return self.distribution.log_prob(actions).sum(dim=-1)
|
|
||||||
|
|
||||||
def get_hidden_states(self):
|
|
||||||
return self.memory_a.hidden_states, self.memory_c.hidden_states
|
|
||||||
|
|
||||||
def update_normalization(self, obs):
|
|
||||||
if self.actor_obs_normalization:
|
|
||||||
actor_obs = self.get_actor_obs(obs)
|
|
||||||
self.actor_obs_normalizer.update(actor_obs)
|
|
||||||
if self.critic_obs_normalization:
|
|
||||||
critic_obs = self.get_critic_obs(obs)
|
|
||||||
self.critic_obs_normalizer.update(critic_obs)
|
|
||||||
|
|
||||||
def load_state_dict(self, state_dict, strict=True):
|
|
||||||
"""Load the parameters of the actor-critic model.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
state_dict (dict): State dictionary of the model.
|
|
||||||
strict (bool): Whether to strictly enforce that the keys in state_dict match the keys returned by this
|
|
||||||
module's state_dict() function.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: Whether this training resumes a previous training. This flag is used by the `load()` function of
|
|
||||||
`OnPolicyRunner` to determine how to load further parameters (relevant for, e.g., distillation).
|
|
||||||
"""
|
|
||||||
|
|
||||||
super().load_state_dict(state_dict, strict=strict)
|
|
||||||
return True
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
from torch import autograd
|
|
||||||
import torch.nn.utils.spectral_norm as spectral_norm
|
|
||||||
|
|
||||||
|
|
||||||
class DiscriminatorMulti(nn.Module):
|
|
||||||
def __init__(
|
|
||||||
self, state_dim, amp_reward_coef, hidden_layer_sizes, device,
|
|
||||||
num_frames=2, task_reward_lerp=0.0, use_lerp=True):
|
|
||||||
super(DiscriminatorMulti, self).__init__()
|
|
||||||
|
|
||||||
self.device = device
|
|
||||||
self.state_dim = state_dim
|
|
||||||
self.use_lerp = use_lerp
|
|
||||||
self.num_frames = num_frames # 存储帧数参数
|
|
||||||
|
|
||||||
self.amp_reward_coef = amp_reward_coef
|
|
||||||
amp_layers = []
|
|
||||||
|
|
||||||
curr_in_dim = state_dim * num_frames
|
|
||||||
for hidden_dim in hidden_layer_sizes:
|
|
||||||
amp_layers.append(spectral_norm(nn.Linear(curr_in_dim, hidden_dim)))
|
|
||||||
amp_layers.append(nn.ReLU())
|
|
||||||
curr_in_dim = hidden_dim
|
|
||||||
self.trunk = nn.Sequential(*amp_layers).to(device)
|
|
||||||
self.amp_linear = spectral_norm(nn.Linear(hidden_layer_sizes[-1], 1)).to(device)
|
|
||||||
|
|
||||||
self.trunk.train()
|
|
||||||
self.amp_linear.train()
|
|
||||||
|
|
||||||
self.task_reward_lerp = task_reward_lerp
|
|
||||||
|
|
||||||
def forward(self, x):
|
|
||||||
h = self.trunk(x)
|
|
||||||
d = self.amp_linear(h)
|
|
||||||
return d
|
|
||||||
|
|
||||||
def compute_grad_pen(self,
|
|
||||||
expert_states, # 改为接收多帧状态列表
|
|
||||||
lambda_=10):
|
|
||||||
# 将多帧状态沿最后一个维度拼接
|
|
||||||
expert_data = expert_states.flatten(1)
|
|
||||||
expert_data.requires_grad = True
|
|
||||||
|
|
||||||
disc = self.amp_linear(self.trunk(expert_data))
|
|
||||||
ones = torch.ones(disc.size(), device=disc.device)
|
|
||||||
grad = autograd.grad(
|
|
||||||
outputs=disc, inputs=expert_data,
|
|
||||||
grad_outputs=ones, create_graph=True,
|
|
||||||
retain_graph=True, only_inputs=True)[0]
|
|
||||||
|
|
||||||
# Enforce that the grad norm approaches 0.
|
|
||||||
grad_pen = lambda_ * (grad.norm(2, dim=1) - 0).pow(2).mean()
|
|
||||||
return grad_pen
|
|
||||||
|
|
||||||
|
|
||||||
def get_disc_weights(self):
|
|
||||||
weights = []
|
|
||||||
for m in self.trunk.modules():
|
|
||||||
if isinstance(m, nn.Linear):
|
|
||||||
weights.append(torch.flatten(m.weight))
|
|
||||||
|
|
||||||
weights.append(torch.flatten(self.amp_linear.weight))
|
|
||||||
return weights
|
|
||||||
|
|
||||||
def get_disc_logit_weights(self):
|
|
||||||
return torch.flatten(self.amp_linear.weight)
|
|
||||||
|
|
||||||
def predict_amp_reward(
|
|
||||||
self, states, # 改为接收多帧状态列表
|
|
||||||
task_reward, normalizer=None):
|
|
||||||
"""
|
|
||||||
states: torch.Tensor, shape=(num_envs, num_frames, state_dim)
|
|
||||||
task_reward: torch.Tensor, shape=(num_envs, 1)
|
|
||||||
"""
|
|
||||||
# import ipdb; ipdb.set_trace()
|
|
||||||
with torch.no_grad():
|
|
||||||
self.eval()
|
|
||||||
if normalizer is not None:
|
|
||||||
# 对每一帧状态进行归一化
|
|
||||||
states = normalizer.normalize_torch(states, self.device)
|
|
||||||
|
|
||||||
# 拼接多帧状态
|
|
||||||
state_cat = states.flatten(1)
|
|
||||||
d = self.amp_linear(self.trunk(state_cat))
|
|
||||||
disc_reward = self.amp_reward_coef * torch.clamp(1 - (1/4) * torch.square(d - 1), min=0)
|
|
||||||
|
|
||||||
if self.use_lerp:
|
|
||||||
if self.task_reward_lerp > 0:
|
|
||||||
reward = self._lerp_reward(disc_reward, task_reward.unsqueeze(-1))
|
|
||||||
self.train()
|
|
||||||
return reward.squeeze(), d, disc_reward.squeeze() * (1.0 - self.task_reward_lerp)
|
|
||||||
else:
|
|
||||||
disc_reward *= 0.02
|
|
||||||
reward = task_reward.unsqueeze(-1) + disc_reward
|
|
||||||
self.train()
|
|
||||||
return reward.squeeze(), d, disc_reward.squeeze()
|
|
||||||
|
|
||||||
def _lerp_reward(self, disc_r, task_r):
|
|
||||||
r = (1.0 - self.task_reward_lerp) * disc_r + self.task_reward_lerp * task_r
|
|
||||||
return r
|
|
||||||
|
|
@ -1,209 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
|
|
||||||
from rsl_rl.networks import MLP, EmpiricalDiscountedVariationNormalization, EmpiricalNormalization
|
|
||||||
|
|
||||||
|
|
||||||
class RandomNetworkDistillation(nn.Module):
|
|
||||||
"""Implementation of Random Network Distillation (RND) [1]
|
|
||||||
|
|
||||||
References:
|
|
||||||
.. [1] Burda, Yuri, et al. "Exploration by random network distillation." arXiv preprint arXiv:1810.12894 (2018).
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
num_states: int,
|
|
||||||
obs_groups: dict,
|
|
||||||
num_outputs: int,
|
|
||||||
predictor_hidden_dims: list[int],
|
|
||||||
target_hidden_dims: list[int],
|
|
||||||
activation: str = "elu",
|
|
||||||
weight: float = 0.0,
|
|
||||||
state_normalization: bool = False,
|
|
||||||
reward_normalization: bool = False,
|
|
||||||
device: str = "cpu",
|
|
||||||
weight_schedule: dict | None = None,
|
|
||||||
):
|
|
||||||
"""Initialize the RND module.
|
|
||||||
|
|
||||||
- If :attr:`state_normalization` is True, then the input state is normalized using an Empirical Normalization layer.
|
|
||||||
- If :attr:`reward_normalization` is True, then the intrinsic reward is normalized using an Empirical Discounted
|
|
||||||
Variation Normalization layer.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
If the hidden dimensions are -1 in the predictor and target networks configuration, then the number of states
|
|
||||||
is used as the hidden dimension.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
num_states: Number of states/inputs to the predictor and target networks.
|
|
||||||
num_outputs: Number of outputs (embedding size) of the predictor and target networks.
|
|
||||||
predictor_hidden_dims: List of hidden dimensions of the predictor network.
|
|
||||||
target_hidden_dims: List of hidden dimensions of the target network.
|
|
||||||
activation: Activation function. Defaults to "elu".
|
|
||||||
weight: Scaling factor of the intrinsic reward. Defaults to 0.0.
|
|
||||||
state_normalization: Whether to normalize the input state. Defaults to False.
|
|
||||||
reward_normalization: Whether to normalize the intrinsic reward. Defaults to False.
|
|
||||||
device: Device to use. Defaults to "cpu".
|
|
||||||
weight_schedule: The type of schedule to use for the RND weight parameter.
|
|
||||||
Defaults to None, in which case the weight parameter is constant.
|
|
||||||
It is a dictionary with the following keys:
|
|
||||||
|
|
||||||
- "mode": The type of schedule to use for the RND weight parameter.
|
|
||||||
- "constant": Constant weight schedule.
|
|
||||||
- "step": Step weight schedule.
|
|
||||||
- "linear": Linear weight schedule.
|
|
||||||
|
|
||||||
For the "step" weight schedule, the following parameters are required:
|
|
||||||
|
|
||||||
- "final_step": The step at which the weight parameter is set to the final value.
|
|
||||||
- "final_value": The final value of the weight parameter.
|
|
||||||
|
|
||||||
For the "linear" weight schedule, the following parameters are required:
|
|
||||||
- "initial_step": The step at which the weight parameter is set to the initial value.
|
|
||||||
- "final_step": The step at which the weight parameter is set to the final value.
|
|
||||||
- "final_value": The final value of the weight parameter.
|
|
||||||
"""
|
|
||||||
# initialize parent class
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
# Store parameters
|
|
||||||
self.num_states = num_states
|
|
||||||
self.obs_groups = obs_groups
|
|
||||||
self.num_outputs = num_outputs
|
|
||||||
self.initial_weight = weight
|
|
||||||
self.device = device
|
|
||||||
self.state_normalization = state_normalization
|
|
||||||
self.reward_normalization = reward_normalization
|
|
||||||
|
|
||||||
# Normalization of input gates
|
|
||||||
if state_normalization:
|
|
||||||
self.state_normalizer = EmpiricalNormalization(shape=[self.num_states], until=1.0e8).to(self.device)
|
|
||||||
else:
|
|
||||||
self.state_normalizer = torch.nn.Identity()
|
|
||||||
# Normalization of intrinsic reward
|
|
||||||
if reward_normalization:
|
|
||||||
self.reward_normalizer = EmpiricalDiscountedVariationNormalization(shape=[], until=1.0e8).to(self.device)
|
|
||||||
else:
|
|
||||||
self.reward_normalizer = torch.nn.Identity()
|
|
||||||
|
|
||||||
# counter for the number of updates
|
|
||||||
self.update_counter = 0
|
|
||||||
|
|
||||||
# resolve weight schedule
|
|
||||||
if weight_schedule is not None:
|
|
||||||
self.weight_scheduler_params = weight_schedule
|
|
||||||
self.weight_scheduler = getattr(self, f"_{weight_schedule['mode']}_weight_schedule")
|
|
||||||
else:
|
|
||||||
self.weight_scheduler = None
|
|
||||||
# Create network architecture
|
|
||||||
self.predictor = MLP(num_states, num_outputs, predictor_hidden_dims, activation).to(self.device)
|
|
||||||
self.target = MLP(num_states, num_outputs, target_hidden_dims, activation).to(self.device)
|
|
||||||
|
|
||||||
# make target network not trainable
|
|
||||||
self.target.eval()
|
|
||||||
|
|
||||||
def get_intrinsic_reward(self, obs) -> torch.Tensor:
|
|
||||||
# Note: the counter is updated number of env steps per learning iteration
|
|
||||||
self.update_counter += 1
|
|
||||||
# Extract the rnd state from the observation
|
|
||||||
rnd_state = self.get_rnd_state(obs)
|
|
||||||
rnd_state = self.state_normalizer(rnd_state)
|
|
||||||
# Obtain the embedding of the rnd state from the target and predictor networks
|
|
||||||
target_embedding = self.target(rnd_state).detach()
|
|
||||||
predictor_embedding = self.predictor(rnd_state).detach()
|
|
||||||
# Compute the intrinsic reward as the distance between the embeddings
|
|
||||||
intrinsic_reward = torch.linalg.norm(target_embedding - predictor_embedding, dim=1)
|
|
||||||
# Normalize intrinsic reward
|
|
||||||
intrinsic_reward = self.reward_normalizer(intrinsic_reward)
|
|
||||||
|
|
||||||
# Check the weight schedule
|
|
||||||
if self.weight_scheduler is not None:
|
|
||||||
self.weight = self.weight_scheduler(step=self.update_counter, **self.weight_scheduler_params)
|
|
||||||
else:
|
|
||||||
self.weight = self.initial_weight
|
|
||||||
# Scale intrinsic reward
|
|
||||||
intrinsic_reward *= self.weight
|
|
||||||
|
|
||||||
return intrinsic_reward
|
|
||||||
|
|
||||||
def forward(self, *args, **kwargs):
|
|
||||||
raise RuntimeError("Forward method is not implemented. Use get_intrinsic_reward instead.")
|
|
||||||
|
|
||||||
def train(self, mode: bool = True):
|
|
||||||
# sets module into training mode
|
|
||||||
self.predictor.train(mode)
|
|
||||||
if self.state_normalization:
|
|
||||||
self.state_normalizer.train(mode)
|
|
||||||
if self.reward_normalization:
|
|
||||||
self.reward_normalizer.train(mode)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def eval(self):
|
|
||||||
return self.train(False)
|
|
||||||
|
|
||||||
def get_rnd_state(self, obs):
|
|
||||||
obs_list = []
|
|
||||||
for obs_group in self.obs_groups["rnd_state"]:
|
|
||||||
obs_list.append(obs[obs_group])
|
|
||||||
return torch.cat(obs_list, dim=-1)
|
|
||||||
|
|
||||||
def update_normalization(self, obs):
|
|
||||||
# Normalize the state
|
|
||||||
if self.state_normalization:
|
|
||||||
rnd_state = self.get_rnd_state(obs)
|
|
||||||
self.state_normalizer.update(rnd_state)
|
|
||||||
|
|
||||||
"""
|
|
||||||
Different weight schedules.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _constant_weight_schedule(self, step: int, **kwargs):
|
|
||||||
return self.initial_weight
|
|
||||||
|
|
||||||
def _step_weight_schedule(self, step: int, final_step: int, final_value: float, **kwargs):
|
|
||||||
return self.initial_weight if step < final_step else final_value
|
|
||||||
|
|
||||||
def _linear_weight_schedule(self, step: int, initial_step: int, final_step: int, final_value: float, **kwargs):
|
|
||||||
if step < initial_step:
|
|
||||||
return self.initial_weight
|
|
||||||
elif step > final_step:
|
|
||||||
return final_value
|
|
||||||
else:
|
|
||||||
return self.initial_weight + (final_value - self.initial_weight) * (step - initial_step) / (
|
|
||||||
final_step - initial_step
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_rnd_config(alg_cfg, obs, obs_groups, env):
|
|
||||||
"""Resolve the RND configuration.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
alg_cfg: The algorithm configuration dictionary.
|
|
||||||
obs: The observation dictionary.
|
|
||||||
obs_groups: The observation groups dictionary.
|
|
||||||
env: The environment.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The resolved algorithm configuration dictionary.
|
|
||||||
"""
|
|
||||||
# resolve dimension of rnd gated state
|
|
||||||
if "rnd_cfg" in alg_cfg and alg_cfg["rnd_cfg"] is not None:
|
|
||||||
# get dimension of rnd gated state
|
|
||||||
num_rnd_state = 0
|
|
||||||
for obs_group in obs_groups["rnd_state"]:
|
|
||||||
assert len(obs[obs_group].shape) == 2, "The RND module only supports 1D observations."
|
|
||||||
num_rnd_state += obs[obs_group].shape[-1]
|
|
||||||
# add rnd gated state to config
|
|
||||||
alg_cfg["rnd_cfg"]["num_states"] = num_rnd_state
|
|
||||||
alg_cfg["rnd_cfg"]["obs_groups"] = obs_groups
|
|
||||||
# scale down the rnd weight with timestep
|
|
||||||
alg_cfg["rnd_cfg"]["weight"] *= env.unwrapped.step_dt
|
|
||||||
return alg_cfg
|
|
||||||
|
|
@ -1,206 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
from torch.distributions import Normal
|
|
||||||
|
|
||||||
from rsl_rl.networks import MLP, EmpiricalNormalization
|
|
||||||
|
|
||||||
|
|
||||||
class StudentTeacher(nn.Module):
|
|
||||||
is_recurrent = False
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
obs,
|
|
||||||
obs_groups,
|
|
||||||
num_actions,
|
|
||||||
student_obs_normalization=False,
|
|
||||||
teacher_obs_normalization=False,
|
|
||||||
student_hidden_dims=[256, 256, 256],
|
|
||||||
teacher_hidden_dims=[256, 256, 256],
|
|
||||||
activation="elu",
|
|
||||||
init_noise_std=0.1,
|
|
||||||
noise_std_type: str = "scalar",
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
if kwargs:
|
|
||||||
print(
|
|
||||||
"StudentTeacher.__init__ got unexpected arguments, which will be ignored: "
|
|
||||||
+ str([key for key in kwargs.keys()])
|
|
||||||
)
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.loaded_teacher = False # indicates if teacher has been loaded
|
|
||||||
|
|
||||||
# get the observation dimensions
|
|
||||||
self.obs_groups = obs_groups
|
|
||||||
num_student_obs = 0
|
|
||||||
for obs_group in obs_groups["policy"]:
|
|
||||||
assert len(obs[obs_group].shape) == 2, "The StudentTeacher module only supports 1D observations."
|
|
||||||
num_student_obs += obs[obs_group].shape[-1]
|
|
||||||
num_teacher_obs = 0
|
|
||||||
for obs_group in obs_groups["teacher"]:
|
|
||||||
assert len(obs[obs_group].shape) == 2, "The StudentTeacher module only supports 1D observations."
|
|
||||||
num_teacher_obs += obs[obs_group].shape[-1]
|
|
||||||
|
|
||||||
# student
|
|
||||||
self.student = MLP(num_student_obs, num_actions, student_hidden_dims, activation)
|
|
||||||
|
|
||||||
# student observation normalization
|
|
||||||
self.student_obs_normalization = student_obs_normalization
|
|
||||||
if student_obs_normalization:
|
|
||||||
self.student_obs_normalizer = EmpiricalNormalization(num_student_obs)
|
|
||||||
else:
|
|
||||||
self.student_obs_normalizer = torch.nn.Identity()
|
|
||||||
|
|
||||||
print(f"Student MLP: {self.student}")
|
|
||||||
|
|
||||||
# teacher
|
|
||||||
self.teacher = MLP(num_teacher_obs, num_actions, teacher_hidden_dims, activation)
|
|
||||||
self.teacher.eval()
|
|
||||||
|
|
||||||
# teacher observation normalization
|
|
||||||
self.teacher_obs_normalization = teacher_obs_normalization
|
|
||||||
if teacher_obs_normalization:
|
|
||||||
self.teacher_obs_normalizer = EmpiricalNormalization(num_teacher_obs)
|
|
||||||
else:
|
|
||||||
self.teacher_obs_normalizer = torch.nn.Identity()
|
|
||||||
|
|
||||||
print(f"Teacher MLP: {self.teacher}")
|
|
||||||
|
|
||||||
# action noise
|
|
||||||
self.noise_std_type = noise_std_type
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
self.std = nn.Parameter(init_noise_std * torch.ones(num_actions))
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
self.log_std = nn.Parameter(torch.log(init_noise_std * torch.ones(num_actions)))
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
|
|
||||||
# action distribution (populated in update_distribution)
|
|
||||||
self.distribution = None
|
|
||||||
# disable args validation for speedup
|
|
||||||
Normal.set_default_validate_args(False)
|
|
||||||
|
|
||||||
def reset(self, dones=None, hidden_states=None):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def forward(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@property
|
|
||||||
def action_mean(self):
|
|
||||||
return self.distribution.mean
|
|
||||||
|
|
||||||
@property
|
|
||||||
def action_std(self):
|
|
||||||
return self.distribution.stddev
|
|
||||||
|
|
||||||
@property
|
|
||||||
def entropy(self):
|
|
||||||
return self.distribution.entropy().sum(dim=-1)
|
|
||||||
|
|
||||||
def update_distribution(self, obs):
|
|
||||||
# compute mean
|
|
||||||
mean = self.student(obs)
|
|
||||||
# compute standard deviation
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
std = self.std.expand_as(mean)
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
std = torch.exp(self.log_std).expand_as(mean)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
# create distribution
|
|
||||||
self.distribution = Normal(mean, std)
|
|
||||||
|
|
||||||
def act(self, obs):
|
|
||||||
obs = self.get_student_obs(obs)
|
|
||||||
obs = self.student_obs_normalizer(obs)
|
|
||||||
self.update_distribution(obs)
|
|
||||||
return self.distribution.sample()
|
|
||||||
|
|
||||||
def act_inference(self, obs):
|
|
||||||
obs = self.get_student_obs(obs)
|
|
||||||
obs = self.student_obs_normalizer(obs)
|
|
||||||
return self.student(obs)
|
|
||||||
|
|
||||||
def evaluate(self, obs):
|
|
||||||
obs = self.get_teacher_obs(obs)
|
|
||||||
obs = self.teacher_obs_normalizer(obs)
|
|
||||||
with torch.no_grad():
|
|
||||||
return self.teacher(obs)
|
|
||||||
|
|
||||||
def get_student_obs(self, obs):
|
|
||||||
obs_list = []
|
|
||||||
for obs_group in self.obs_groups["policy"]:
|
|
||||||
obs_list.append(obs[obs_group])
|
|
||||||
return torch.cat(obs_list, dim=-1)
|
|
||||||
|
|
||||||
def get_teacher_obs(self, obs):
|
|
||||||
obs_list = []
|
|
||||||
for obs_group in self.obs_groups["teacher"]:
|
|
||||||
obs_list.append(obs[obs_group])
|
|
||||||
return torch.cat(obs_list, dim=-1)
|
|
||||||
|
|
||||||
def get_hidden_states(self):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def detach_hidden_states(self, dones=None):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def train(self, mode=True):
|
|
||||||
super().train(mode)
|
|
||||||
# make sure teacher is in eval mode
|
|
||||||
self.teacher.eval()
|
|
||||||
self.teacher_obs_normalizer.eval()
|
|
||||||
|
|
||||||
def update_normalization(self, obs):
|
|
||||||
if self.student_obs_normalization:
|
|
||||||
student_obs = self.get_student_obs(obs)
|
|
||||||
self.student_obs_normalizer.update(student_obs)
|
|
||||||
|
|
||||||
def load_state_dict(self, state_dict, strict=True):
|
|
||||||
"""Load the parameters of the student and teacher networks.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
state_dict (dict): State dictionary of the model.
|
|
||||||
strict (bool): Whether to strictly enforce that the keys in state_dict match the keys returned by this
|
|
||||||
module's state_dict() function.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: Whether this training resumes a previous training. This flag is used by the `load()` function of
|
|
||||||
`OnPolicyRunner` to determine how to load further parameters.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# check if state_dict contains teacher and student or just teacher parameters
|
|
||||||
if any("actor" in key for key in state_dict.keys()): # loading parameters from rl training
|
|
||||||
# rename keys to match teacher and remove critic parameters
|
|
||||||
teacher_state_dict = {}
|
|
||||||
teacher_obs_normalizer_state_dict = {}
|
|
||||||
for key, value in state_dict.items():
|
|
||||||
if "actor." in key:
|
|
||||||
teacher_state_dict[key.replace("actor.", "")] = value
|
|
||||||
if "actor_obs_normalizer." in key:
|
|
||||||
teacher_obs_normalizer_state_dict[key.replace("actor_obs_normalizer.", "")] = value
|
|
||||||
self.teacher.load_state_dict(teacher_state_dict, strict=strict)
|
|
||||||
self.teacher_obs_normalizer.load_state_dict(teacher_obs_normalizer_state_dict, strict=strict)
|
|
||||||
# set flag for successfully loading the parameters
|
|
||||||
self.loaded_teacher = True
|
|
||||||
self.teacher.eval()
|
|
||||||
self.teacher_obs_normalizer.eval()
|
|
||||||
return False # training does not resume
|
|
||||||
elif any("student" in key for key in state_dict.keys()): # loading parameters from distillation training
|
|
||||||
super().load_state_dict(state_dict, strict=strict)
|
|
||||||
# set flag for successfully loading the parameters
|
|
||||||
self.loaded_teacher = True
|
|
||||||
self.teacher.eval()
|
|
||||||
self.teacher_obs_normalizer.eval()
|
|
||||||
return True # training resumes
|
|
||||||
else:
|
|
||||||
raise ValueError("state_dict does not contain student or teacher parameters")
|
|
||||||
|
|
@ -1,249 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
import warnings
|
|
||||||
from torch.distributions import Normal
|
|
||||||
|
|
||||||
from rsl_rl.networks import MLP, EmpiricalNormalization, Memory
|
|
||||||
|
|
||||||
|
|
||||||
class StudentTeacherRecurrent(nn.Module):
|
|
||||||
is_recurrent = True
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
obs,
|
|
||||||
obs_groups,
|
|
||||||
num_actions,
|
|
||||||
student_obs_normalization=False,
|
|
||||||
teacher_obs_normalization=False,
|
|
||||||
student_hidden_dims=[256, 256, 256],
|
|
||||||
teacher_hidden_dims=[256, 256, 256],
|
|
||||||
activation="elu",
|
|
||||||
init_noise_std=0.1,
|
|
||||||
noise_std_type: str = "scalar",
|
|
||||||
rnn_type="lstm",
|
|
||||||
rnn_hidden_dim=256,
|
|
||||||
rnn_num_layers=1,
|
|
||||||
teacher_recurrent=False,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
if "rnn_hidden_size" in kwargs:
|
|
||||||
warnings.warn(
|
|
||||||
"The argument `rnn_hidden_size` is deprecated and will be removed in a future version. "
|
|
||||||
"Please use `rnn_hidden_dim` instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
if rnn_hidden_dim == 256: # Only override if the new argument is at its default
|
|
||||||
rnn_hidden_dim = kwargs.pop("rnn_hidden_size")
|
|
||||||
if kwargs:
|
|
||||||
print(
|
|
||||||
"StudentTeacherRecurrent.__init__ got unexpected arguments, which will be ignored: "
|
|
||||||
+ str(kwargs.keys()),
|
|
||||||
)
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.loaded_teacher = False # indicates if teacher has been loaded
|
|
||||||
self.teacher_recurrent = teacher_recurrent # indicates if teacher is recurrent too
|
|
||||||
|
|
||||||
# get the observation dimensions
|
|
||||||
self.obs_groups = obs_groups
|
|
||||||
num_student_obs = 0
|
|
||||||
for obs_group in obs_groups["policy"]:
|
|
||||||
assert len(obs[obs_group].shape) == 2, "The StudentTeacher module only supports 1D observations."
|
|
||||||
num_student_obs += obs[obs_group].shape[-1]
|
|
||||||
num_teacher_obs = 0
|
|
||||||
for obs_group in obs_groups["teacher"]:
|
|
||||||
assert len(obs[obs_group].shape) == 2, "The StudentTeacher module only supports 1D observations."
|
|
||||||
num_teacher_obs += obs[obs_group].shape[-1]
|
|
||||||
|
|
||||||
# student
|
|
||||||
self.memory_s = Memory(num_student_obs, type=rnn_type, num_layers=rnn_num_layers, hidden_size=rnn_hidden_dim)
|
|
||||||
self.student = MLP(rnn_hidden_dim, num_actions, student_hidden_dims, activation)
|
|
||||||
|
|
||||||
# student observation normalization
|
|
||||||
self.student_obs_normalization = student_obs_normalization
|
|
||||||
if student_obs_normalization:
|
|
||||||
self.student_obs_normalizer = EmpiricalNormalization(num_student_obs)
|
|
||||||
else:
|
|
||||||
self.student_obs_normalizer = torch.nn.Identity()
|
|
||||||
|
|
||||||
print(f"Student RNN: {self.memory_s}")
|
|
||||||
print(f"Student MLP: {self.student}")
|
|
||||||
|
|
||||||
# teacher
|
|
||||||
if self.teacher_recurrent:
|
|
||||||
self.memory_t = Memory(
|
|
||||||
num_teacher_obs, type=rnn_type, num_layers=rnn_num_layers, hidden_size=rnn_hidden_dim
|
|
||||||
)
|
|
||||||
num_teacher_obs = rnn_hidden_dim
|
|
||||||
self.teacher = MLP(num_teacher_obs, num_actions, teacher_hidden_dims, activation)
|
|
||||||
|
|
||||||
# teacher observation normalization
|
|
||||||
self.teacher_obs_normalization = teacher_obs_normalization
|
|
||||||
if teacher_obs_normalization:
|
|
||||||
self.teacher_obs_normalizer = EmpiricalNormalization(num_teacher_obs)
|
|
||||||
else:
|
|
||||||
self.teacher_obs_normalizer = torch.nn.Identity()
|
|
||||||
|
|
||||||
if self.teacher_recurrent:
|
|
||||||
print(f"Teacher RNN: {self.memory_t}")
|
|
||||||
print(f"Teacher MLP: {self.teacher}")
|
|
||||||
|
|
||||||
# action noise
|
|
||||||
self.noise_std_type = noise_std_type
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
self.std = nn.Parameter(init_noise_std * torch.ones(num_actions))
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
self.log_std = nn.Parameter(torch.log(init_noise_std * torch.ones(num_actions)))
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
|
|
||||||
# action distribution (populated in update_distribution)
|
|
||||||
self.distribution = None
|
|
||||||
# disable args validation for speedup
|
|
||||||
Normal.set_default_validate_args(False)
|
|
||||||
|
|
||||||
def reset(self, dones=None, hidden_states=None):
|
|
||||||
if hidden_states is None:
|
|
||||||
hidden_states = (None, None)
|
|
||||||
self.memory_s.reset(dones, hidden_states[0])
|
|
||||||
if self.teacher_recurrent:
|
|
||||||
self.memory_t.reset(dones, hidden_states[1])
|
|
||||||
|
|
||||||
def forward(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@property
|
|
||||||
def action_mean(self):
|
|
||||||
return self.distribution.mean
|
|
||||||
|
|
||||||
@property
|
|
||||||
def action_std(self):
|
|
||||||
return self.distribution.stddev
|
|
||||||
|
|
||||||
@property
|
|
||||||
def entropy(self):
|
|
||||||
return self.distribution.entropy().sum(dim=-1)
|
|
||||||
|
|
||||||
def update_distribution(self, obs):
|
|
||||||
# compute mean
|
|
||||||
mean = self.student(obs)
|
|
||||||
# compute standard deviation
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
std = self.std.expand_as(mean)
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
std = torch.exp(self.log_std).expand_as(mean)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
# create distribution
|
|
||||||
self.distribution = Normal(mean, std)
|
|
||||||
|
|
||||||
def act(self, obs):
|
|
||||||
obs = self.get_student_obs(obs)
|
|
||||||
obs = self.student_obs_normalizer(obs)
|
|
||||||
out_mem = self.memory_s(obs).squeeze(0)
|
|
||||||
self.update_distribution(out_mem)
|
|
||||||
return self.distribution.sample()
|
|
||||||
|
|
||||||
def act_inference(self, obs):
|
|
||||||
obs = self.get_student_obs(obs)
|
|
||||||
obs = self.student_obs_normalizer(obs)
|
|
||||||
out_mem = self.memory_s(obs).squeeze(0)
|
|
||||||
return self.student(out_mem)
|
|
||||||
|
|
||||||
def evaluate(self, obs):
|
|
||||||
obs = self.get_teacher_obs(obs)
|
|
||||||
obs = self.teacher_obs_normalizer(obs)
|
|
||||||
with torch.no_grad():
|
|
||||||
if self.teacher_recurrent:
|
|
||||||
self.memory_t.eval()
|
|
||||||
obs = self.memory_t(obs).squeeze(0)
|
|
||||||
return self.teacher(obs)
|
|
||||||
|
|
||||||
def get_student_obs(self, obs):
|
|
||||||
obs_list = []
|
|
||||||
for obs_group in self.obs_groups["policy"]:
|
|
||||||
obs_list.append(obs[obs_group])
|
|
||||||
return torch.cat(obs_list, dim=-1)
|
|
||||||
|
|
||||||
def get_teacher_obs(self, obs):
|
|
||||||
obs_list = []
|
|
||||||
for obs_group in self.obs_groups["teacher"]:
|
|
||||||
obs_list.append(obs[obs_group])
|
|
||||||
return torch.cat(obs_list, dim=-1)
|
|
||||||
|
|
||||||
def get_hidden_states(self):
|
|
||||||
if self.teacher_recurrent:
|
|
||||||
return self.memory_s.hidden_states, self.memory_t.hidden_states
|
|
||||||
else:
|
|
||||||
return self.memory_s.hidden_states, None
|
|
||||||
|
|
||||||
def detach_hidden_states(self, dones=None):
|
|
||||||
self.memory_s.detach_hidden_states(dones)
|
|
||||||
if self.teacher_recurrent:
|
|
||||||
self.memory_t.detach_hidden_states(dones)
|
|
||||||
|
|
||||||
def train(self, mode=True):
|
|
||||||
super().train(mode)
|
|
||||||
# make sure teacher is in eval mode
|
|
||||||
self.teacher.eval()
|
|
||||||
self.teacher_obs_normalizer.eval()
|
|
||||||
|
|
||||||
def update_normalization(self, obs):
|
|
||||||
if self.student_obs_normalization:
|
|
||||||
student_obs = self.get_student_obs(obs)
|
|
||||||
self.student_obs_normalizer.update(student_obs)
|
|
||||||
|
|
||||||
def load_state_dict(self, state_dict, strict=True):
|
|
||||||
"""Load the parameters of the student and teacher networks.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
state_dict (dict): State dictionary of the model.
|
|
||||||
strict (bool): Whether to strictly enforce that the keys in state_dict match the keys returned by this
|
|
||||||
module's state_dict() function.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: Whether this training resumes a previous training. This flag is used by the `load()` function of
|
|
||||||
`OnPolicyRunner` to determine how to load further parameters.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# check if state_dict contains teacher and student or just teacher parameters
|
|
||||||
if any("actor" in key for key in state_dict.keys()): # loading parameters from rl training
|
|
||||||
# rename keys to match teacher and remove critic parameters
|
|
||||||
teacher_state_dict = {}
|
|
||||||
teacher_obs_normalizer_state_dict = {}
|
|
||||||
for key, value in state_dict.items():
|
|
||||||
if "actor." in key:
|
|
||||||
teacher_state_dict[key.replace("actor.", "")] = value
|
|
||||||
if "actor_obs_normalizer." in key:
|
|
||||||
teacher_obs_normalizer_state_dict[key.replace("actor_obs_normalizer.", "")] = value
|
|
||||||
self.teacher.load_state_dict(teacher_state_dict, strict=strict)
|
|
||||||
self.teacher_obs_normalizer.load_state_dict(teacher_obs_normalizer_state_dict, strict=strict)
|
|
||||||
# also load recurrent memory if teacher is recurrent
|
|
||||||
if self.teacher_recurrent:
|
|
||||||
memory_t_state_dict = {}
|
|
||||||
for key, value in state_dict.items():
|
|
||||||
if "memory_a." in key:
|
|
||||||
memory_t_state_dict[key.replace("memory_a.", "")] = value
|
|
||||||
self.memory_t.load_state_dict(memory_t_state_dict, strict=strict)
|
|
||||||
# set flag for successfully loading the parameters
|
|
||||||
self.loaded_teacher = True
|
|
||||||
self.teacher.eval()
|
|
||||||
self.teacher_obs_normalizer.eval()
|
|
||||||
return False # training does not resume
|
|
||||||
elif any("student" in key for key in state_dict.keys()): # loading parameters from distillation training
|
|
||||||
super().load_state_dict(state_dict, strict=strict)
|
|
||||||
# set flag for successfully loading the parameters
|
|
||||||
self.loaded_teacher = True
|
|
||||||
self.teacher.eval()
|
|
||||||
self.teacher_obs_normalizer.eval()
|
|
||||||
return True # training resumes
|
|
||||||
else:
|
|
||||||
raise ValueError("state_dict does not contain student or teacher parameters")
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_symmetry_config(alg_cfg, env):
|
|
||||||
"""Resolve the symmetry configuration.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
alg_cfg: The algorithm configuration dictionary.
|
|
||||||
env: The environment.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The resolved algorithm configuration dictionary.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# if using symmetry then pass the environment config object
|
|
||||||
if "symmetry_cfg" in alg_cfg and alg_cfg["symmetry_cfg"] is not None:
|
|
||||||
# this is used by the symmetry function for handling different observation terms
|
|
||||||
alg_cfg["symmetry_cfg"]["_env"] = env
|
|
||||||
return alg_cfg
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
"""Definitions for components of modules."""
|
|
||||||
|
|
||||||
from .memory import Memory
|
|
||||||
from .mlp import MLP
|
|
||||||
from .normalization import EmpiricalDiscountedVariationNormalization, EmpiricalNormalization
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch.nn as nn
|
|
||||||
|
|
||||||
from rsl_rl.utils import unpad_trajectories
|
|
||||||
|
|
||||||
|
|
||||||
class Memory(nn.Module):
|
|
||||||
"""Memory module for recurrent networks.
|
|
||||||
|
|
||||||
This module is used to store the hidden states of the policy.
|
|
||||||
Currently only supports GRU and LSTM.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, input_size, type="lstm", num_layers=1, hidden_size=256):
|
|
||||||
super().__init__()
|
|
||||||
# RNN
|
|
||||||
rnn_cls = nn.GRU if type.lower() == "gru" else nn.LSTM
|
|
||||||
self.rnn = rnn_cls(input_size=input_size, hidden_size=hidden_size, num_layers=num_layers)
|
|
||||||
self.hidden_states = None
|
|
||||||
|
|
||||||
def forward(self, input, masks=None, hidden_states=None):
|
|
||||||
batch_mode = masks is not None
|
|
||||||
if batch_mode:
|
|
||||||
# batch mode: needs saved hidden states
|
|
||||||
if hidden_states is None:
|
|
||||||
raise ValueError("Hidden states not passed to memory module during policy update")
|
|
||||||
out, _ = self.rnn(input, hidden_states)
|
|
||||||
out = unpad_trajectories(out, masks)
|
|
||||||
else:
|
|
||||||
# inference/distillation mode: uses hidden states of last step
|
|
||||||
out, self.hidden_states = self.rnn(input.unsqueeze(0), self.hidden_states)
|
|
||||||
return out
|
|
||||||
|
|
||||||
def reset(self, dones=None, hidden_states=None):
|
|
||||||
if dones is None: # reset all hidden states
|
|
||||||
if hidden_states is None:
|
|
||||||
self.hidden_states = None
|
|
||||||
else:
|
|
||||||
self.hidden_states = hidden_states
|
|
||||||
elif self.hidden_states is not None: # reset hidden states of done environments
|
|
||||||
if hidden_states is None:
|
|
||||||
if isinstance(self.hidden_states, tuple): # tuple in case of LSTM
|
|
||||||
for hidden_state in self.hidden_states:
|
|
||||||
hidden_state[..., dones == 1, :] = 0.0
|
|
||||||
else:
|
|
||||||
self.hidden_states[..., dones == 1, :] = 0.0
|
|
||||||
else:
|
|
||||||
NotImplementedError(
|
|
||||||
"Resetting hidden states of done environments with custom hidden states is not implemented"
|
|
||||||
)
|
|
||||||
|
|
||||||
def detach_hidden_states(self, dones=None):
|
|
||||||
if self.hidden_states is not None:
|
|
||||||
if dones is None: # detach all hidden states
|
|
||||||
if isinstance(self.hidden_states, tuple): # tuple in case of LSTM
|
|
||||||
self.hidden_states = tuple(hidden_state.detach() for hidden_state in self.hidden_states)
|
|
||||||
else:
|
|
||||||
self.hidden_states = self.hidden_states.detach()
|
|
||||||
else: # detach hidden states of done environments
|
|
||||||
if isinstance(self.hidden_states, tuple): # tuple in case of LSTM
|
|
||||||
for hidden_state in self.hidden_states:
|
|
||||||
hidden_state[..., dones == 1, :] = hidden_state[..., dones == 1, :].detach()
|
|
||||||
else:
|
|
||||||
self.hidden_states[..., dones == 1, :] = self.hidden_states[..., dones == 1, :].detach()
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
from functools import reduce
|
|
||||||
|
|
||||||
from rsl_rl.utils import resolve_nn_activation
|
|
||||||
|
|
||||||
|
|
||||||
class MLP(nn.Sequential):
|
|
||||||
"""Multi-layer perceptron.
|
|
||||||
|
|
||||||
The MLP network is a sequence of linear layers and activation functions. The
|
|
||||||
last layer is a linear layer that outputs the desired dimension unless the
|
|
||||||
last activation function is specified.
|
|
||||||
|
|
||||||
It provides additional conveniences:
|
|
||||||
|
|
||||||
- If the hidden dimensions have a value of ``-1``, the dimension is inferred
|
|
||||||
from the input dimension.
|
|
||||||
- If the output dimension is a tuple, the output is reshaped to the desired
|
|
||||||
shape.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
input_dim: int,
|
|
||||||
output_dim: int | tuple[int] | list[int],
|
|
||||||
hidden_dims: tuple[int] | list[int],
|
|
||||||
activation: str = "elu",
|
|
||||||
last_activation: str | None = None,
|
|
||||||
):
|
|
||||||
"""Initialize the MLP.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
input_dim: Dimension of the input.
|
|
||||||
output_dim: Dimension of the output.
|
|
||||||
hidden_dims: Dimensions of the hidden layers. A value of ``-1`` indicates
|
|
||||||
that the dimension should be inferred from the input dimension.
|
|
||||||
activation: Activation function. Defaults to "elu".
|
|
||||||
last_activation: Activation function of the last layer. Defaults to None,
|
|
||||||
in which case the last layer is linear.
|
|
||||||
"""
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
# resolve activation functions
|
|
||||||
activation_mod = resolve_nn_activation(activation)
|
|
||||||
last_activation_mod = resolve_nn_activation(last_activation) if last_activation is not None else None
|
|
||||||
# resolve number of hidden dims if they are -1
|
|
||||||
hidden_dims_processed = [input_dim if dim == -1 else dim for dim in hidden_dims]
|
|
||||||
|
|
||||||
# create layers sequentially
|
|
||||||
layers = []
|
|
||||||
layers.append(nn.Linear(input_dim, hidden_dims_processed[0]))
|
|
||||||
layers.append(activation_mod)
|
|
||||||
|
|
||||||
for layer_index in range(len(hidden_dims_processed) - 1):
|
|
||||||
layers.append(nn.Linear(hidden_dims_processed[layer_index], hidden_dims_processed[layer_index + 1]))
|
|
||||||
layers.append(activation_mod)
|
|
||||||
|
|
||||||
# add last layer
|
|
||||||
if isinstance(output_dim, int):
|
|
||||||
layers.append(nn.Linear(hidden_dims_processed[-1], output_dim))
|
|
||||||
else:
|
|
||||||
# compute the total output dimension
|
|
||||||
total_out_dim = reduce(lambda x, y: x * y, output_dim)
|
|
||||||
# add a layer to reshape the output to the desired shape
|
|
||||||
layers.append(nn.Linear(hidden_dims_processed[-1], total_out_dim))
|
|
||||||
layers.append(nn.Unflatten(dim=-1, unflattened_size=output_dim))
|
|
||||||
|
|
||||||
# add last activation function if specified
|
|
||||||
if last_activation_mod is not None:
|
|
||||||
layers.append(last_activation_mod)
|
|
||||||
|
|
||||||
# register the layers
|
|
||||||
for idx, layer in enumerate(layers):
|
|
||||||
self.add_module(f"{idx}", layer)
|
|
||||||
|
|
||||||
def init_weights(self, scales: float | tuple[float]):
|
|
||||||
"""Initialize the weights of the MLP.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
scales: Scale factor for the weights.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_scale(idx) -> float:
|
|
||||||
"""Get the scale factor for the weights of the MLP.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
idx: Index of the layer.
|
|
||||||
"""
|
|
||||||
return scales[idx] if isinstance(scales, (list, tuple)) else scales
|
|
||||||
|
|
||||||
# initialize the weights
|
|
||||||
for idx, module in enumerate(self):
|
|
||||||
if isinstance(module, nn.Linear):
|
|
||||||
nn.init.orthogonal_(module.weight, gain=get_scale(idx))
|
|
||||||
nn.init.zeros_(module.bias)
|
|
||||||
|
|
||||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
||||||
"""Forward pass of the MLP.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
x: Input tensor.
|
|
||||||
"""
|
|
||||||
for layer in self:
|
|
||||||
x = layer(x)
|
|
||||||
return x
|
|
||||||
|
|
||||||
def reset(self, dones=None, hidden_states=None):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def detach_hidden_states(self, dones=None):
|
|
||||||
pass
|
|
||||||
|
|
@ -1,130 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
# Copyright (c) 2020 Preferred Networks, Inc.
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch
|
|
||||||
from torch import nn
|
|
||||||
|
|
||||||
|
|
||||||
class EmpiricalNormalization(nn.Module):
|
|
||||||
"""Normalize mean and variance of values based on empirical values."""
|
|
||||||
|
|
||||||
def __init__(self, shape, eps=1e-2, until=None):
|
|
||||||
"""Initialize EmpiricalNormalization module.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
shape (int or tuple of int): Shape of input values except batch axis.
|
|
||||||
eps (float): Small value for stability.
|
|
||||||
until (int or None): If this arg is specified, the module learns input values until the sum of batch sizes
|
|
||||||
exceeds it.
|
|
||||||
|
|
||||||
Note: The normalization parameters are computed over the whole batch, not for each environment separately.
|
|
||||||
"""
|
|
||||||
super().__init__()
|
|
||||||
self.eps = eps
|
|
||||||
self.until = until
|
|
||||||
self.register_buffer("_mean", torch.zeros(shape).unsqueeze(0))
|
|
||||||
self.register_buffer("_var", torch.ones(shape).unsqueeze(0))
|
|
||||||
self.register_buffer("_std", torch.ones(shape).unsqueeze(0))
|
|
||||||
self.register_buffer("count", torch.tensor(0, dtype=torch.long))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def mean(self):
|
|
||||||
return self._mean.squeeze(0).clone()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def std(self):
|
|
||||||
return self._std.squeeze(0).clone()
|
|
||||||
|
|
||||||
def forward(self, x):
|
|
||||||
"""Normalize mean and variance of values based on empirical values."""
|
|
||||||
|
|
||||||
return (x - self._mean) / (self._std + self.eps)
|
|
||||||
|
|
||||||
@torch.jit.unused
|
|
||||||
def update(self, x):
|
|
||||||
"""Learn input values without computing the output values of them"""
|
|
||||||
|
|
||||||
if not self.training:
|
|
||||||
return
|
|
||||||
if self.until is not None and self.count >= self.until:
|
|
||||||
return
|
|
||||||
|
|
||||||
count_x = x.shape[0]
|
|
||||||
self.count += count_x
|
|
||||||
rate = count_x / self.count
|
|
||||||
var_x = torch.var(x, dim=0, unbiased=False, keepdim=True)
|
|
||||||
mean_x = torch.mean(x, dim=0, keepdim=True)
|
|
||||||
delta_mean = mean_x - self._mean
|
|
||||||
self._mean += rate * delta_mean
|
|
||||||
self._var += rate * (var_x - self._var + delta_mean * (mean_x - self._mean))
|
|
||||||
self._std = torch.sqrt(self._var)
|
|
||||||
|
|
||||||
@torch.jit.unused
|
|
||||||
def inverse(self, y):
|
|
||||||
"""De-normalize values based on empirical values."""
|
|
||||||
|
|
||||||
return y * (self._std + self.eps) + self._mean
|
|
||||||
|
|
||||||
|
|
||||||
class EmpiricalDiscountedVariationNormalization(nn.Module):
|
|
||||||
"""Reward normalization from Pathak's large scale study on PPO.
|
|
||||||
|
|
||||||
Reward normalization. Since the reward function is non-stationary, it is useful to normalize
|
|
||||||
the scale of the rewards so that the value function can learn quickly. We did this by dividing
|
|
||||||
the rewards by a running estimate of the standard deviation of the sum of discounted rewards.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, shape, eps=1e-2, gamma=0.99, until=None):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.emp_norm = EmpiricalNormalization(shape, eps, until)
|
|
||||||
self.disc_avg = _DiscountedAverage(gamma)
|
|
||||||
|
|
||||||
def forward(self, rew):
|
|
||||||
if self.training:
|
|
||||||
# update discounted rewards
|
|
||||||
avg = self.disc_avg.update(rew)
|
|
||||||
# update moments from discounted rewards
|
|
||||||
self.emp_norm.update(avg)
|
|
||||||
|
|
||||||
# normalize rewards with the empirical std
|
|
||||||
if self.emp_norm._std > 0:
|
|
||||||
return rew / self.emp_norm._std
|
|
||||||
else:
|
|
||||||
return rew
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
Helper class.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class _DiscountedAverage:
|
|
||||||
r"""Discounted average of rewards.
|
|
||||||
|
|
||||||
The discounted average is defined as:
|
|
||||||
|
|
||||||
.. math::
|
|
||||||
|
|
||||||
\bar{R}_t = \gamma \bar{R}_{t-1} + r_t
|
|
||||||
|
|
||||||
Args:
|
|
||||||
gamma (float): Discount factor.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, gamma):
|
|
||||||
self.avg = None
|
|
||||||
self.gamma = gamma
|
|
||||||
|
|
||||||
def update(self, rew: torch.Tensor) -> torch.Tensor:
|
|
||||||
if self.avg is None:
|
|
||||||
self.avg = rew
|
|
||||||
else:
|
|
||||||
self.avg = self.avg * self.gamma + rew
|
|
||||||
return self.avg
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
"""Implementation of runners for environment-agent interaction."""
|
|
||||||
|
|
||||||
from .on_policy_runner import OnPolicyRunner # isort:skip
|
|
||||||
from .distillation_runner import DistillationRunner
|
|
||||||
from .amp_on_policy_runner import AMPOnPolicyRunner
|
|
||||||
|
|
||||||
__all__ = ["OnPolicyRunner", "DistillationRunner", "AMPOnPolicyRunner"]
|
|
||||||
|
|
@ -1,521 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
import statistics
|
|
||||||
import time
|
|
||||||
import torch
|
|
||||||
import warnings
|
|
||||||
from collections import deque
|
|
||||||
|
|
||||||
import rsl_rl
|
|
||||||
from rsl_rl.algorithms import AMP_PPO
|
|
||||||
from rsl_rl.env import VecEnv
|
|
||||||
from rsl_rl.modules import ActorCritic, ActorCriticRecurrent,DiscriminatorMulti, resolve_rnd_config, resolve_symmetry_config
|
|
||||||
from rsl_rl.utils import resolve_obs_groups, store_code_state, Normalizer, G1_AMPLoader
|
|
||||||
|
|
||||||
|
|
||||||
class AMPOnPolicyRunner:
|
|
||||||
"""On-policy runner for training and evaluation of actor-critic methods."""
|
|
||||||
|
|
||||||
def __init__(self, env: VecEnv, train_cfg: dict, log_dir: str | None = None, device="cpu"):
|
|
||||||
self.cfg = train_cfg
|
|
||||||
self.alg_cfg = train_cfg["algorithm"]
|
|
||||||
self.policy_cfg = train_cfg["policy"]
|
|
||||||
self.device = device
|
|
||||||
self.env = env
|
|
||||||
|
|
||||||
# check if multi-gpu is enabled
|
|
||||||
self._configure_multi_gpu()
|
|
||||||
|
|
||||||
# store training configuration
|
|
||||||
self.num_steps_per_env = self.cfg["num_steps_per_env"]
|
|
||||||
self.save_interval = self.cfg["save_interval"]
|
|
||||||
|
|
||||||
# query observations from environment for algorithm construction
|
|
||||||
obs = self.env.get_observations()
|
|
||||||
default_sets = ["critic"]
|
|
||||||
if "rnd_cfg" in self.alg_cfg and self.alg_cfg["rnd_cfg"] is not None:
|
|
||||||
default_sets.append("rnd_state")
|
|
||||||
self.cfg["obs_groups"] = resolve_obs_groups(obs, self.cfg["obs_groups"], default_sets)
|
|
||||||
|
|
||||||
self.amp_data = G1_AMPLoader(
|
|
||||||
device,
|
|
||||||
time_between_frames=1/50.0,
|
|
||||||
preload_transitions=True,
|
|
||||||
num_preload_transitions=train_cfg["amp_num_preload_transitions"],
|
|
||||||
motion_files=train_cfg["amp_motion_files"],
|
|
||||||
num_frames=train_cfg['amp_num_frames']
|
|
||||||
)
|
|
||||||
|
|
||||||
self.amp_observation_dim = self.amp_data.observation_dim if self.cfg["amp_num_obs"] == 0 else self.cfg["amp_num_obs"]
|
|
||||||
self.amp_num_frames = 0 if self.cfg["amp_num_frames"] == 0 else self.cfg["amp_num_frames"]
|
|
||||||
self.amp_normalizer = Normalizer(self.amp_observation_dim)
|
|
||||||
self.discriminator = DiscriminatorMulti(
|
|
||||||
self.amp_observation_dim,
|
|
||||||
train_cfg["amp_reward_coef"],
|
|
||||||
train_cfg["amp_discr_hidden_dims"],
|
|
||||||
device,
|
|
||||||
train_cfg["amp_num_frames"],
|
|
||||||
train_cfg["amp_task_reward_lerp"],
|
|
||||||
train_cfg['use_lerp'],
|
|
||||||
).to(self.device)
|
|
||||||
|
|
||||||
|
|
||||||
# create the algorithm
|
|
||||||
self.alg = self._construct_algorithm(obs)
|
|
||||||
|
|
||||||
# Decide whether to disable logging
|
|
||||||
# We only log from the process with rank 0 (main process)
|
|
||||||
self.disable_logs = self.is_distributed and self.gpu_global_rank != 0
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
self.log_dir = log_dir
|
|
||||||
self.writer = None
|
|
||||||
self.tot_timesteps = 0
|
|
||||||
self.tot_time = 0
|
|
||||||
self.current_learning_iteration = 0
|
|
||||||
self.git_status_repos = [rsl_rl.__file__]
|
|
||||||
|
|
||||||
def learn(self, num_learning_iterations: int, init_at_random_ep_len: bool = False): # noqa: C901
|
|
||||||
# initialize writer
|
|
||||||
self._prepare_logging_writer()
|
|
||||||
|
|
||||||
# randomize initial episode lengths (for exploration)
|
|
||||||
if init_at_random_ep_len:
|
|
||||||
self.env.episode_length_buf = torch.randint_like(
|
|
||||||
self.env.episode_length_buf, high=int(self.env.max_episode_length)
|
|
||||||
)
|
|
||||||
|
|
||||||
# start learning
|
|
||||||
obs = self.env.get_observations().to(self.device)
|
|
||||||
self.train_mode() # switch to train mode (for dropout for example)
|
|
||||||
amp_obs = self.env.get_amp_observations()
|
|
||||||
amp_obs = amp_obs.to(self.device)
|
|
||||||
if self.amp_num_frames != 0:
|
|
||||||
self.amp_obs_frames = torch.zeros(size=(self.env.num_envs, self.amp_num_frames, self.amp_observation_dim), device=self.device)
|
|
||||||
self.amp_obs_frames = torch.concat((self.amp_obs_frames[:, 1:], amp_obs.unsqueeze(1)), dim=1)
|
|
||||||
|
|
||||||
# Book keeping
|
|
||||||
ep_infos = []
|
|
||||||
rewbuffer = deque(maxlen=100)
|
|
||||||
lenbuffer = deque(maxlen=100)
|
|
||||||
step_discrewbuffer = deque(maxlen=100)
|
|
||||||
|
|
||||||
cur_reward_sum = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
cur_episode_length = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
cur_single_step_disc_rew = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
# create buffers for logging extrinsic and intrinsic rewards
|
|
||||||
if self.alg.rnd:
|
|
||||||
erewbuffer = deque(maxlen=100)
|
|
||||||
irewbuffer = deque(maxlen=100)
|
|
||||||
cur_ereward_sum = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
cur_ireward_sum = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
|
|
||||||
# Ensure all parameters are in-synced
|
|
||||||
if self.is_distributed:
|
|
||||||
print(f"Synchronizing parameters for rank {self.gpu_global_rank}...")
|
|
||||||
self.alg.broadcast_parameters()
|
|
||||||
|
|
||||||
# Start training
|
|
||||||
start_iter = self.current_learning_iteration
|
|
||||||
tot_iter = start_iter + num_learning_iterations
|
|
||||||
for it in range(start_iter, tot_iter):
|
|
||||||
start = time.time()
|
|
||||||
# Rollout
|
|
||||||
with torch.inference_mode():
|
|
||||||
for _ in range(self.num_steps_per_env):
|
|
||||||
# Sample actions
|
|
||||||
actions = self.alg.act(obs,amp_obs)
|
|
||||||
# Step the environment
|
|
||||||
obs, rewards, dones, extras = self.env.step(actions.to(self.env.device))
|
|
||||||
# Move to device
|
|
||||||
obs, rewards, dones = (obs.to(self.device), rewards.to(self.device), dones.to(self.device))
|
|
||||||
|
|
||||||
next_amp_obs = self.env.get_amp_observations()
|
|
||||||
next_amp_obs = next_amp_obs.to(self.device)
|
|
||||||
next_amp_obs_with_term = torch.clone(next_amp_obs)
|
|
||||||
|
|
||||||
reset_env_ids = self.env.reset_env_ids
|
|
||||||
terminal_amp_states = self.env.get_amp_observations()[reset_env_ids]
|
|
||||||
next_amp_obs_with_term[reset_env_ids] = terminal_amp_states
|
|
||||||
self.amp_obs_frames = torch.concat((self.amp_obs_frames[:, 1:], next_amp_obs_with_term.unsqueeze(1)), dim=1)
|
|
||||||
|
|
||||||
amp_reward = torch.zeros(self.env.num_envs, device=obs.device)
|
|
||||||
|
|
||||||
mask = self.env.contact_phase[:, 0] == 1.0
|
|
||||||
if mask.any():
|
|
||||||
rewards[mask], logit, disc_reward = self.alg.discriminator.predict_amp_reward(
|
|
||||||
self.amp_obs_frames[mask], rewards[mask], normalizer=self.alg.amp_normalizer
|
|
||||||
)
|
|
||||||
amp_reward[mask] += disc_reward
|
|
||||||
|
|
||||||
# process the step
|
|
||||||
self.alg.process_env_step(obs, rewards, dones, extras, next_amp_obs_with_term, self.amp_obs_frames)
|
|
||||||
self.amp_obs_frames[reset_env_ids] = 0
|
|
||||||
|
|
||||||
amp_obs = torch.clone(next_amp_obs)
|
|
||||||
# Extract intrinsic rewards (only for logging)
|
|
||||||
intrinsic_rewards = self.alg.intrinsic_rewards if self.alg.rnd else None
|
|
||||||
# book keeping
|
|
||||||
if self.log_dir is not None:
|
|
||||||
if "episode" in extras:
|
|
||||||
ep_infos.append(extras["episode"])
|
|
||||||
elif "log" in extras:
|
|
||||||
ep_infos.append(extras["log"])
|
|
||||||
# Update rewards
|
|
||||||
if self.alg.rnd:
|
|
||||||
cur_ereward_sum += rewards
|
|
||||||
cur_ireward_sum += intrinsic_rewards # type: ignore
|
|
||||||
cur_reward_sum += rewards + intrinsic_rewards
|
|
||||||
else:
|
|
||||||
cur_reward_sum += rewards
|
|
||||||
# Update episode length
|
|
||||||
cur_episode_length += 1
|
|
||||||
cur_single_step_disc_rew += amp_reward
|
|
||||||
# Clear data for completed episodes
|
|
||||||
# -- common
|
|
||||||
new_ids = (dones > 0).nonzero(as_tuple=False)
|
|
||||||
rewbuffer.extend(cur_reward_sum[new_ids][:, 0].cpu().numpy().tolist())
|
|
||||||
lenbuffer.extend(cur_episode_length[new_ids][:, 0].cpu().numpy().tolist())
|
|
||||||
cur_reward_sum[new_ids] = 0
|
|
||||||
cur_episode_length[new_ids] = 0
|
|
||||||
to_extend_disc = (cur_single_step_disc_rew[new_ids] / self.env.max_episode_length_s)[:, 0].cpu().numpy()
|
|
||||||
step_discrewbuffer.extend(to_extend_disc.tolist())
|
|
||||||
cur_single_step_disc_rew[new_ids] = 0
|
|
||||||
# -- intrinsic and extrinsic rewards
|
|
||||||
if self.alg.rnd:
|
|
||||||
erewbuffer.extend(cur_ereward_sum[new_ids][:, 0].cpu().numpy().tolist())
|
|
||||||
irewbuffer.extend(cur_ireward_sum[new_ids][:, 0].cpu().numpy().tolist())
|
|
||||||
cur_ereward_sum[new_ids] = 0
|
|
||||||
cur_ireward_sum[new_ids] = 0
|
|
||||||
|
|
||||||
stop = time.time()
|
|
||||||
collection_time = stop - start
|
|
||||||
start = stop
|
|
||||||
|
|
||||||
# compute returns
|
|
||||||
self.alg.compute_returns(obs)
|
|
||||||
|
|
||||||
# update policy
|
|
||||||
loss_dict = self.alg.update()
|
|
||||||
|
|
||||||
stop = time.time()
|
|
||||||
learn_time = stop - start
|
|
||||||
self.current_learning_iteration = it
|
|
||||||
# log info
|
|
||||||
if self.log_dir is not None and not self.disable_logs:
|
|
||||||
# Log information
|
|
||||||
self.log(locals())
|
|
||||||
# Save model
|
|
||||||
if it % self.save_interval == 0:
|
|
||||||
self.save(os.path.join(self.log_dir, f"model_{it}.pt"))
|
|
||||||
|
|
||||||
# Clear episode infos
|
|
||||||
ep_infos.clear()
|
|
||||||
# Save code state
|
|
||||||
if it == start_iter and not self.disable_logs:
|
|
||||||
# obtain all the diff files
|
|
||||||
git_file_paths = store_code_state(self.log_dir, self.git_status_repos)
|
|
||||||
# if possible store them to wandb
|
|
||||||
if self.logger_type in ["wandb", "neptune"] and git_file_paths:
|
|
||||||
for path in git_file_paths:
|
|
||||||
self.writer.save_file(path)
|
|
||||||
|
|
||||||
# Save the final model after training
|
|
||||||
if self.log_dir is not None and not self.disable_logs:
|
|
||||||
self.save(os.path.join(self.log_dir, f"model_{self.current_learning_iteration}.pt"))
|
|
||||||
|
|
||||||
def log(self, locs: dict, width: int = 80, pad: int = 35):
|
|
||||||
# Compute the collection size
|
|
||||||
collection_size = self.num_steps_per_env * self.env.num_envs * self.gpu_world_size
|
|
||||||
# Update total time-steps and time
|
|
||||||
self.tot_timesteps += collection_size
|
|
||||||
self.tot_time += locs["collection_time"] + locs["learn_time"]
|
|
||||||
iteration_time = locs["collection_time"] + locs["learn_time"]
|
|
||||||
|
|
||||||
# -- Episode info
|
|
||||||
ep_string = ""
|
|
||||||
if locs["ep_infos"]:
|
|
||||||
for key in locs["ep_infos"][0]:
|
|
||||||
infotensor = torch.tensor([], device=self.device)
|
|
||||||
for ep_info in locs["ep_infos"]:
|
|
||||||
# handle scalar and zero dimensional tensor infos
|
|
||||||
if key not in ep_info:
|
|
||||||
continue
|
|
||||||
if not isinstance(ep_info[key], torch.Tensor):
|
|
||||||
ep_info[key] = torch.Tensor([ep_info[key]])
|
|
||||||
if len(ep_info[key].shape) == 0:
|
|
||||||
ep_info[key] = ep_info[key].unsqueeze(0)
|
|
||||||
infotensor = torch.cat((infotensor, ep_info[key].to(self.device)))
|
|
||||||
value = torch.mean(infotensor)
|
|
||||||
# log to logger and terminal
|
|
||||||
if "/" in key:
|
|
||||||
self.writer.add_scalar(key, value, locs["it"])
|
|
||||||
ep_string += f"""{f'{key}:':>{pad}} {value:.4f}\n"""
|
|
||||||
else:
|
|
||||||
self.writer.add_scalar("Episode/" + key, value, locs["it"])
|
|
||||||
ep_string += f"""{f'Mean episode {key}:':>{pad}} {value:.4f}\n"""
|
|
||||||
|
|
||||||
mean_std = self.alg.policy.action_std.mean()
|
|
||||||
fps = int(collection_size / (locs["collection_time"] + locs["learn_time"]))
|
|
||||||
|
|
||||||
# -- Losses
|
|
||||||
for key, value in locs["loss_dict"].items():
|
|
||||||
self.writer.add_scalar(f"Loss/{key}", value, locs["it"])
|
|
||||||
self.writer.add_scalar("Loss/learning_rate", self.alg.learning_rate, locs["it"])
|
|
||||||
|
|
||||||
# -- Policy
|
|
||||||
self.writer.add_scalar("Policy/mean_noise_std", mean_std.item(), locs["it"])
|
|
||||||
|
|
||||||
# -- Performance
|
|
||||||
self.writer.add_scalar("Perf/total_fps", fps, locs["it"])
|
|
||||||
self.writer.add_scalar("Perf/collection time", locs["collection_time"], locs["it"])
|
|
||||||
self.writer.add_scalar("Perf/learning_time", locs["learn_time"], locs["it"])
|
|
||||||
|
|
||||||
# -- Training
|
|
||||||
if len(locs["rewbuffer"]) > 0:
|
|
||||||
# separate logging for intrinsic and extrinsic rewards
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
self.writer.add_scalar("Rnd/mean_extrinsic_reward", statistics.mean(locs["erewbuffer"]), locs["it"])
|
|
||||||
self.writer.add_scalar("Rnd/mean_intrinsic_reward", statistics.mean(locs["irewbuffer"]), locs["it"])
|
|
||||||
self.writer.add_scalar("Rnd/weight", self.alg.rnd.weight, locs["it"])
|
|
||||||
# everything else
|
|
||||||
self.writer.add_scalar("Train/mean_reward", statistics.mean(locs["rewbuffer"]), locs["it"])
|
|
||||||
self.writer.add_scalar("Train/mean_episode_length", statistics.mean(locs["lenbuffer"]), locs["it"])
|
|
||||||
self.writer.add_scalar('Train/mean_step_disc_reward', statistics.mean(locs['step_discrewbuffer']), locs['it'])
|
|
||||||
if self.logger_type != "wandb": # wandb does not support non-integer x-axis logging
|
|
||||||
self.writer.add_scalar("Train/mean_reward/time", statistics.mean(locs["rewbuffer"]), self.tot_time)
|
|
||||||
self.writer.add_scalar(
|
|
||||||
"Train/mean_episode_length/time", statistics.mean(locs["lenbuffer"]), self.tot_time
|
|
||||||
)
|
|
||||||
|
|
||||||
str = f" \033[1m Learning iteration {locs['it']}/{locs['tot_iter']} \033[0m "
|
|
||||||
|
|
||||||
if len(locs["rewbuffer"]) > 0:
|
|
||||||
log_string = (
|
|
||||||
f"""{'#' * width}\n"""
|
|
||||||
f"""{str.center(width, ' ')}\n\n"""
|
|
||||||
f"""{'Computation:':>{pad}} {fps:.0f} steps/s (collection: {locs[
|
|
||||||
'collection_time']:.3f}s, learning {locs['learn_time']:.3f}s)\n"""
|
|
||||||
f"""{'Mean action noise std:':>{pad}} {mean_std.item():.2f}\n"""
|
|
||||||
f"""{'Step disc reward:':>{pad}} {statistics.mean(locs['step_discrewbuffer']):.2f}\n"""
|
|
||||||
)
|
|
||||||
# -- Losses
|
|
||||||
for key, value in locs["loss_dict"].items():
|
|
||||||
log_string += f"""{f'Mean {key} loss:':>{pad}} {value:.4f}\n"""
|
|
||||||
# -- Rewards
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
log_string += (
|
|
||||||
f"""{'Mean extrinsic reward:':>{pad}} {statistics.mean(locs['erewbuffer']):.2f}\n"""
|
|
||||||
f"""{'Mean intrinsic reward:':>{pad}} {statistics.mean(locs['irewbuffer']):.2f}\n"""
|
|
||||||
)
|
|
||||||
log_string += f"""{'Mean reward:':>{pad}} {statistics.mean(locs['rewbuffer']):.2f}\n"""
|
|
||||||
# -- episode info
|
|
||||||
log_string += f"""{'Mean episode length:':>{pad}} {statistics.mean(locs['lenbuffer']):.2f}\n"""
|
|
||||||
else:
|
|
||||||
log_string = (
|
|
||||||
f"""{'#' * width}\n"""
|
|
||||||
f"""{str.center(width, ' ')}\n\n"""
|
|
||||||
f"""{'Computation:':>{pad}} {fps:.0f} steps/s (collection: {locs[
|
|
||||||
'collection_time']:.3f}s, learning {locs['learn_time']:.3f}s)\n"""
|
|
||||||
f"""{'Mean action noise std:':>{pad}} {mean_std.item():.2f}\n"""
|
|
||||||
)
|
|
||||||
for key, value in locs["loss_dict"].items():
|
|
||||||
log_string += f"""{f'{key}:':>{pad}} {value:.4f}\n"""
|
|
||||||
|
|
||||||
log_string += ep_string
|
|
||||||
log_string += (
|
|
||||||
f"""{'-' * width}\n"""
|
|
||||||
f"""{'Total timesteps:':>{pad}} {self.tot_timesteps}\n"""
|
|
||||||
f"""{'Iteration time:':>{pad}} {iteration_time:.2f}s\n"""
|
|
||||||
f"""{'Time elapsed:':>{pad}} {time.strftime("%H:%M:%S", time.gmtime(self.tot_time))}\n"""
|
|
||||||
f"""{'ETA:':>{pad}} {time.strftime(
|
|
||||||
"%H:%M:%S",
|
|
||||||
time.gmtime(
|
|
||||||
self.tot_time / (locs['it'] - locs['start_iter'] + 1)
|
|
||||||
* (locs['start_iter'] + locs['num_learning_iterations'] - locs['it'])
|
|
||||||
)
|
|
||||||
)}\n"""
|
|
||||||
)
|
|
||||||
print(log_string)
|
|
||||||
|
|
||||||
def save(self, path: str, infos=None):
|
|
||||||
# -- Save model
|
|
||||||
saved_dict = {
|
|
||||||
"model_state_dict": self.alg.policy.state_dict(),
|
|
||||||
"optimizer_state_dict": self.alg.optimizer.state_dict(),
|
|
||||||
"iter": self.current_learning_iteration,
|
|
||||||
"infos": infos,
|
|
||||||
}
|
|
||||||
# -- Save RND model if used
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
saved_dict["rnd_state_dict"] = self.alg.rnd.state_dict()
|
|
||||||
saved_dict["rnd_optimizer_state_dict"] = self.alg.rnd_optimizer.state_dict()
|
|
||||||
torch.save(saved_dict, path)
|
|
||||||
|
|
||||||
# upload model to external logging service
|
|
||||||
if self.logger_type in ["neptune", "wandb"] and not self.disable_logs:
|
|
||||||
self.writer.save_model(path, self.current_learning_iteration)
|
|
||||||
|
|
||||||
def load(self, path: str, load_optimizer: bool = True, map_location: str | None = None):
|
|
||||||
loaded_dict = torch.load(path, weights_only=False, map_location=map_location)
|
|
||||||
# -- Load model
|
|
||||||
resumed_training = self.alg.policy.load_state_dict(loaded_dict["model_state_dict"])
|
|
||||||
# -- Load RND model if used
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
self.alg.rnd.load_state_dict(loaded_dict["rnd_state_dict"])
|
|
||||||
# -- load optimizer if used
|
|
||||||
if load_optimizer and resumed_training:
|
|
||||||
# -- algorithm optimizer
|
|
||||||
self.alg.optimizer.load_state_dict(loaded_dict["optimizer_state_dict"])
|
|
||||||
# -- RND optimizer if used
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
self.alg.rnd_optimizer.load_state_dict(loaded_dict["rnd_optimizer_state_dict"])
|
|
||||||
# -- load current learning iteration
|
|
||||||
if resumed_training:
|
|
||||||
self.current_learning_iteration = loaded_dict["iter"]
|
|
||||||
return loaded_dict["infos"]
|
|
||||||
|
|
||||||
def get_inference_policy(self, device=None):
|
|
||||||
self.eval_mode() # switch to evaluation mode (dropout for example)
|
|
||||||
if device is not None:
|
|
||||||
self.alg.policy.to(device)
|
|
||||||
return self.alg.policy.act_inference
|
|
||||||
|
|
||||||
def train_mode(self):
|
|
||||||
# -- PPO
|
|
||||||
self.alg.policy.train()
|
|
||||||
self.alg.discriminator.train()
|
|
||||||
# -- RND
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
self.alg.rnd.train()
|
|
||||||
|
|
||||||
def eval_mode(self):
|
|
||||||
# -- PPO
|
|
||||||
self.alg.policy.eval()
|
|
||||||
self.alg.discriminator.eval()
|
|
||||||
# -- RND
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
self.alg.rnd.eval()
|
|
||||||
|
|
||||||
def add_git_repo_to_log(self, repo_file_path):
|
|
||||||
self.git_status_repos.append(repo_file_path)
|
|
||||||
|
|
||||||
"""
|
|
||||||
Helper functions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _configure_multi_gpu(self):
|
|
||||||
"""Configure multi-gpu training."""
|
|
||||||
# check if distributed training is enabled
|
|
||||||
self.gpu_world_size = int(os.getenv("WORLD_SIZE", "1"))
|
|
||||||
self.is_distributed = self.gpu_world_size > 1
|
|
||||||
|
|
||||||
# if not distributed training, set local and global rank to 0 and return
|
|
||||||
if not self.is_distributed:
|
|
||||||
self.gpu_local_rank = 0
|
|
||||||
self.gpu_global_rank = 0
|
|
||||||
self.multi_gpu_cfg = None
|
|
||||||
return
|
|
||||||
|
|
||||||
# get rank and world size
|
|
||||||
self.gpu_local_rank = int(os.getenv("LOCAL_RANK", "0"))
|
|
||||||
self.gpu_global_rank = int(os.getenv("RANK", "0"))
|
|
||||||
|
|
||||||
# make a configuration dictionary
|
|
||||||
self.multi_gpu_cfg = {
|
|
||||||
"global_rank": self.gpu_global_rank, # rank of the main process
|
|
||||||
"local_rank": self.gpu_local_rank, # rank of the current process
|
|
||||||
"world_size": self.gpu_world_size, # total number of processes
|
|
||||||
}
|
|
||||||
|
|
||||||
# check if user has device specified for local rank
|
|
||||||
if self.device != f"cuda:{self.gpu_local_rank}":
|
|
||||||
raise ValueError(
|
|
||||||
f"Device '{self.device}' does not match expected device for local rank '{self.gpu_local_rank}'."
|
|
||||||
)
|
|
||||||
# validate multi-gpu configuration
|
|
||||||
if self.gpu_local_rank >= self.gpu_world_size:
|
|
||||||
raise ValueError(
|
|
||||||
f"Local rank '{self.gpu_local_rank}' is greater than or equal to world size '{self.gpu_world_size}'."
|
|
||||||
)
|
|
||||||
if self.gpu_global_rank >= self.gpu_world_size:
|
|
||||||
raise ValueError(
|
|
||||||
f"Global rank '{self.gpu_global_rank}' is greater than or equal to world size '{self.gpu_world_size}'."
|
|
||||||
)
|
|
||||||
|
|
||||||
# initialize torch distributed
|
|
||||||
torch.distributed.init_process_group(backend="nccl", rank=self.gpu_global_rank, world_size=self.gpu_world_size)
|
|
||||||
# set device to the local rank
|
|
||||||
torch.cuda.set_device(self.gpu_local_rank)
|
|
||||||
|
|
||||||
def _construct_algorithm(self, obs) -> AMP_PPO:
|
|
||||||
"""Construct the actor-critic algorithm."""
|
|
||||||
# resolve RND config
|
|
||||||
self.alg_cfg = resolve_rnd_config(self.alg_cfg, obs, self.cfg["obs_groups"], self.env)
|
|
||||||
|
|
||||||
# resolve symmetry config
|
|
||||||
self.alg_cfg = resolve_symmetry_config(self.alg_cfg, self.env)
|
|
||||||
|
|
||||||
# resolve deprecated normalization config
|
|
||||||
if self.cfg.get("empirical_normalization") is not None:
|
|
||||||
warnings.warn(
|
|
||||||
"The `empirical_normalization` parameter is deprecated. Please set `actor_obs_normalization` and "
|
|
||||||
"`critic_obs_normalization` as part of the `policy` configuration instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
if self.policy_cfg.get("actor_obs_normalization") is None:
|
|
||||||
self.policy_cfg["actor_obs_normalization"] = self.cfg["empirical_normalization"]
|
|
||||||
if self.policy_cfg.get("critic_obs_normalization") is None:
|
|
||||||
self.policy_cfg["critic_obs_normalization"] = self.cfg["empirical_normalization"]
|
|
||||||
|
|
||||||
# initialize the actor-critic
|
|
||||||
actor_critic_class = eval(self.policy_cfg.pop("class_name"))
|
|
||||||
actor_critic: ActorCritic | ActorCriticRecurrent = actor_critic_class(
|
|
||||||
obs, self.cfg["obs_groups"], self.env.num_actions, **self.policy_cfg
|
|
||||||
).to(self.device)
|
|
||||||
|
|
||||||
# initialize the algorithm
|
|
||||||
alg_class = eval(self.alg_cfg.pop("class_name"))
|
|
||||||
|
|
||||||
alg: AMP_PPO = alg_class(actor_critic, self.discriminator, self.amp_data, self.amp_normalizer, self.amp_num_frames, device=self.device, **self.alg_cfg, multi_gpu_cfg=self.multi_gpu_cfg)
|
|
||||||
|
|
||||||
# initialize the storage
|
|
||||||
alg.init_storage(
|
|
||||||
"rl",
|
|
||||||
self.env.num_envs,
|
|
||||||
self.num_steps_per_env,
|
|
||||||
obs,
|
|
||||||
[self.env.num_actions],
|
|
||||||
)
|
|
||||||
|
|
||||||
return alg
|
|
||||||
|
|
||||||
def _prepare_logging_writer(self):
|
|
||||||
"""Prepares the logging writers."""
|
|
||||||
if self.log_dir is not None and self.writer is None and not self.disable_logs:
|
|
||||||
# Launch either Tensorboard or Neptune & Tensorboard summary writer(s), default: Tensorboard.
|
|
||||||
self.logger_type = self.cfg.get("logger", "tensorboard")
|
|
||||||
self.logger_type = self.logger_type.lower()
|
|
||||||
|
|
||||||
if self.logger_type == "neptune":
|
|
||||||
from rsl_rl.utils.neptune_utils import NeptuneSummaryWriter
|
|
||||||
|
|
||||||
self.writer = NeptuneSummaryWriter(log_dir=self.log_dir, flush_secs=10, cfg=self.cfg)
|
|
||||||
self.writer.log_config(self.env.cfg, self.cfg, self.alg_cfg, self.policy_cfg)
|
|
||||||
elif self.logger_type == "wandb":
|
|
||||||
from rsl_rl.utils.wandb_utils import WandbSummaryWriter
|
|
||||||
|
|
||||||
self.writer = WandbSummaryWriter(log_dir=self.log_dir, flush_secs=10, cfg=self.cfg)
|
|
||||||
self.writer.log_config(self.env.cfg, self.cfg, self.alg_cfg, self.policy_cfg)
|
|
||||||
elif self.logger_type == "tensorboard":
|
|
||||||
from torch.utils.tensorboard import SummaryWriter
|
|
||||||
|
|
||||||
self.writer = SummaryWriter(log_dir=self.log_dir, flush_secs=10)
|
|
||||||
else:
|
|
||||||
raise ValueError("Logger type not found. Please choose 'neptune', 'wandb' or 'tensorboard'.")
|
|
||||||
|
|
@ -1,179 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import torch
|
|
||||||
from collections import deque
|
|
||||||
|
|
||||||
import rsl_rl
|
|
||||||
from rsl_rl.algorithms import Distillation
|
|
||||||
from rsl_rl.env import VecEnv
|
|
||||||
from rsl_rl.modules import StudentTeacher, StudentTeacherRecurrent
|
|
||||||
from rsl_rl.runners import OnPolicyRunner
|
|
||||||
from rsl_rl.utils import resolve_obs_groups, store_code_state
|
|
||||||
|
|
||||||
|
|
||||||
class DistillationRunner(OnPolicyRunner):
|
|
||||||
"""On-policy runner for training and evaluation of teacher-student training."""
|
|
||||||
|
|
||||||
def __init__(self, env: VecEnv, train_cfg: dict, log_dir: str | None = None, device="cpu"):
|
|
||||||
self.cfg = train_cfg
|
|
||||||
self.alg_cfg = train_cfg["algorithm"]
|
|
||||||
self.policy_cfg = train_cfg["policy"]
|
|
||||||
self.device = device
|
|
||||||
self.env = env
|
|
||||||
|
|
||||||
# check if multi-gpu is enabled
|
|
||||||
self._configure_multi_gpu()
|
|
||||||
|
|
||||||
# store training configuration
|
|
||||||
self.num_steps_per_env = self.cfg["num_steps_per_env"]
|
|
||||||
self.save_interval = self.cfg["save_interval"]
|
|
||||||
|
|
||||||
# query observations from environment for algorithm construction
|
|
||||||
obs = self.env.get_observations()
|
|
||||||
self.cfg["obs_groups"] = resolve_obs_groups(obs, self.cfg["obs_groups"], default_sets=["teacher"])
|
|
||||||
|
|
||||||
# create the algorithm
|
|
||||||
self.alg = self._construct_algorithm(obs)
|
|
||||||
|
|
||||||
# Decide whether to disable logging
|
|
||||||
# We only log from the process with rank 0 (main process)
|
|
||||||
self.disable_logs = self.is_distributed and self.gpu_global_rank != 0
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
self.log_dir = log_dir
|
|
||||||
self.writer = None
|
|
||||||
self.tot_timesteps = 0
|
|
||||||
self.tot_time = 0
|
|
||||||
self.current_learning_iteration = 0
|
|
||||||
self.git_status_repos = [rsl_rl.__file__]
|
|
||||||
|
|
||||||
def learn(self, num_learning_iterations: int, init_at_random_ep_len: bool = False): # noqa: C901
|
|
||||||
# initialize writer
|
|
||||||
self._prepare_logging_writer()
|
|
||||||
# check if teacher is loaded
|
|
||||||
if not self.alg.policy.loaded_teacher:
|
|
||||||
raise ValueError("Teacher model parameters not loaded. Please load a teacher model to distill.")
|
|
||||||
|
|
||||||
# randomize initial episode lengths (for exploration)
|
|
||||||
if init_at_random_ep_len:
|
|
||||||
self.env.episode_length_buf = torch.randint_like(
|
|
||||||
self.env.episode_length_buf, high=int(self.env.max_episode_length)
|
|
||||||
)
|
|
||||||
|
|
||||||
# start learning
|
|
||||||
obs = self.env.get_observations().to(self.device)
|
|
||||||
self.train_mode() # switch to train mode (for dropout for example)
|
|
||||||
|
|
||||||
# Book keeping
|
|
||||||
ep_infos = []
|
|
||||||
rewbuffer = deque(maxlen=100)
|
|
||||||
lenbuffer = deque(maxlen=100)
|
|
||||||
cur_reward_sum = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
cur_episode_length = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
|
|
||||||
# Ensure all parameters are in-synced
|
|
||||||
if self.is_distributed:
|
|
||||||
print(f"Synchronizing parameters for rank {self.gpu_global_rank}...")
|
|
||||||
self.alg.broadcast_parameters()
|
|
||||||
|
|
||||||
# Start training
|
|
||||||
start_iter = self.current_learning_iteration
|
|
||||||
tot_iter = start_iter + num_learning_iterations
|
|
||||||
for it in range(start_iter, tot_iter):
|
|
||||||
start = time.time()
|
|
||||||
# Rollout
|
|
||||||
with torch.inference_mode():
|
|
||||||
for _ in range(self.num_steps_per_env):
|
|
||||||
# Sample actions
|
|
||||||
actions = self.alg.act(obs)
|
|
||||||
# Step the environment
|
|
||||||
obs, rewards, dones, extras = self.env.step(actions.to(self.env.device))
|
|
||||||
# Move to device
|
|
||||||
obs, rewards, dones = (obs.to(self.device), rewards.to(self.device), dones.to(self.device))
|
|
||||||
# process the step
|
|
||||||
self.alg.process_env_step(obs, rewards, dones, extras)
|
|
||||||
# book keeping
|
|
||||||
if self.log_dir is not None:
|
|
||||||
if "episode" in extras:
|
|
||||||
ep_infos.append(extras["episode"])
|
|
||||||
elif "log" in extras:
|
|
||||||
ep_infos.append(extras["log"])
|
|
||||||
# Update rewards
|
|
||||||
cur_reward_sum += rewards
|
|
||||||
# Update episode length
|
|
||||||
cur_episode_length += 1
|
|
||||||
# Clear data for completed episodes
|
|
||||||
new_ids = (dones > 0).nonzero(as_tuple=False)
|
|
||||||
rewbuffer.extend(cur_reward_sum[new_ids][:, 0].cpu().numpy().tolist())
|
|
||||||
lenbuffer.extend(cur_episode_length[new_ids][:, 0].cpu().numpy().tolist())
|
|
||||||
cur_reward_sum[new_ids] = 0
|
|
||||||
cur_episode_length[new_ids] = 0
|
|
||||||
|
|
||||||
stop = time.time()
|
|
||||||
collection_time = stop - start
|
|
||||||
start = stop
|
|
||||||
|
|
||||||
# update policy
|
|
||||||
loss_dict = self.alg.update()
|
|
||||||
|
|
||||||
stop = time.time()
|
|
||||||
learn_time = stop - start
|
|
||||||
self.current_learning_iteration = it
|
|
||||||
# log info
|
|
||||||
if self.log_dir is not None and not self.disable_logs:
|
|
||||||
# Log information
|
|
||||||
self.log(locals())
|
|
||||||
# Save model
|
|
||||||
if it % self.save_interval == 0:
|
|
||||||
self.save(os.path.join(self.log_dir, f"model_{it}.pt"))
|
|
||||||
|
|
||||||
# Clear episode infos
|
|
||||||
ep_infos.clear()
|
|
||||||
# Save code state
|
|
||||||
if it == start_iter and not self.disable_logs:
|
|
||||||
# obtain all the diff files
|
|
||||||
git_file_paths = store_code_state(self.log_dir, self.git_status_repos)
|
|
||||||
# if possible store them to wandb
|
|
||||||
if self.logger_type in ["wandb", "neptune"] and git_file_paths:
|
|
||||||
for path in git_file_paths:
|
|
||||||
self.writer.save_file(path)
|
|
||||||
|
|
||||||
# Save the final model after training
|
|
||||||
if self.log_dir is not None and not self.disable_logs:
|
|
||||||
self.save(os.path.join(self.log_dir, f"model_{self.current_learning_iteration}.pt"))
|
|
||||||
|
|
||||||
"""
|
|
||||||
Helper methods.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _construct_algorithm(self, obs) -> Distillation:
|
|
||||||
"""Construct the distillation algorithm."""
|
|
||||||
# initialize the actor-critic
|
|
||||||
student_teacher_class = eval(self.policy_cfg.pop("class_name"))
|
|
||||||
student_teacher: StudentTeacher | StudentTeacherRecurrent = student_teacher_class(
|
|
||||||
obs, self.cfg["obs_groups"], self.env.num_actions, **self.policy_cfg
|
|
||||||
).to(self.device)
|
|
||||||
|
|
||||||
# initialize the algorithm
|
|
||||||
alg_class = eval(self.alg_cfg.pop("class_name"))
|
|
||||||
alg: Distillation = alg_class(
|
|
||||||
student_teacher, device=self.device, **self.alg_cfg, multi_gpu_cfg=self.multi_gpu_cfg
|
|
||||||
)
|
|
||||||
|
|
||||||
# initialize the storage
|
|
||||||
alg.init_storage(
|
|
||||||
"distillation",
|
|
||||||
self.env.num_envs,
|
|
||||||
self.num_steps_per_env,
|
|
||||||
obs,
|
|
||||||
[self.env.num_actions],
|
|
||||||
)
|
|
||||||
|
|
||||||
return alg
|
|
||||||
|
|
@ -1,460 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
import statistics
|
|
||||||
import time
|
|
||||||
import torch
|
|
||||||
import warnings
|
|
||||||
from collections import deque
|
|
||||||
|
|
||||||
import rsl_rl
|
|
||||||
from rsl_rl.algorithms import PPO
|
|
||||||
from rsl_rl.env import VecEnv
|
|
||||||
from rsl_rl.modules import ActorCritic, ActorCriticRecurrent, resolve_rnd_config, resolve_symmetry_config
|
|
||||||
from rsl_rl.utils import resolve_obs_groups, store_code_state
|
|
||||||
|
|
||||||
|
|
||||||
class OnPolicyRunner:
|
|
||||||
"""On-policy runner for training and evaluation of actor-critic methods."""
|
|
||||||
|
|
||||||
def __init__(self, env: VecEnv, train_cfg: dict, log_dir: str | None = None, device="cpu"):
|
|
||||||
self.cfg = train_cfg
|
|
||||||
self.alg_cfg = train_cfg["algorithm"]
|
|
||||||
self.policy_cfg = train_cfg["policy"]
|
|
||||||
self.device = device
|
|
||||||
self.env = env
|
|
||||||
|
|
||||||
# check if multi-gpu is enabled
|
|
||||||
self._configure_multi_gpu()
|
|
||||||
|
|
||||||
# store training configuration
|
|
||||||
self.num_steps_per_env = self.cfg["num_steps_per_env"]
|
|
||||||
self.save_interval = self.cfg["save_interval"]
|
|
||||||
|
|
||||||
# query observations from environment for algorithm construction
|
|
||||||
obs = self.env.get_observations()
|
|
||||||
default_sets = ["critic"]
|
|
||||||
if "rnd_cfg" in self.alg_cfg and self.alg_cfg["rnd_cfg"] is not None:
|
|
||||||
default_sets.append("rnd_state")
|
|
||||||
self.cfg["obs_groups"] = resolve_obs_groups(obs, self.cfg["obs_groups"], default_sets)
|
|
||||||
|
|
||||||
# create the algorithm
|
|
||||||
self.alg = self._construct_algorithm(obs)
|
|
||||||
|
|
||||||
# Decide whether to disable logging
|
|
||||||
# We only log from the process with rank 0 (main process)
|
|
||||||
self.disable_logs = self.is_distributed and self.gpu_global_rank != 0
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
self.log_dir = log_dir
|
|
||||||
self.writer = None
|
|
||||||
self.tot_timesteps = 0
|
|
||||||
self.tot_time = 0
|
|
||||||
self.current_learning_iteration = 0
|
|
||||||
self.git_status_repos = [rsl_rl.__file__]
|
|
||||||
|
|
||||||
def learn(self, num_learning_iterations: int, init_at_random_ep_len: bool = False): # noqa: C901
|
|
||||||
# initialize writer
|
|
||||||
self._prepare_logging_writer()
|
|
||||||
|
|
||||||
# randomize initial episode lengths (for exploration)
|
|
||||||
if init_at_random_ep_len:
|
|
||||||
self.env.episode_length_buf = torch.randint_like(
|
|
||||||
self.env.episode_length_buf, high=int(self.env.max_episode_length)
|
|
||||||
)
|
|
||||||
|
|
||||||
# start learning
|
|
||||||
obs = self.env.get_observations().to(self.device)
|
|
||||||
self.train_mode() # switch to train mode (for dropout for example)
|
|
||||||
|
|
||||||
# Book keeping
|
|
||||||
ep_infos = []
|
|
||||||
rewbuffer = deque(maxlen=100)
|
|
||||||
lenbuffer = deque(maxlen=100)
|
|
||||||
cur_reward_sum = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
cur_episode_length = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
|
|
||||||
# create buffers for logging extrinsic and intrinsic rewards
|
|
||||||
if self.alg.rnd:
|
|
||||||
erewbuffer = deque(maxlen=100)
|
|
||||||
irewbuffer = deque(maxlen=100)
|
|
||||||
cur_ereward_sum = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
cur_ireward_sum = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
|
|
||||||
# Ensure all parameters are in-synced
|
|
||||||
if self.is_distributed:
|
|
||||||
print(f"Synchronizing parameters for rank {self.gpu_global_rank}...")
|
|
||||||
self.alg.broadcast_parameters()
|
|
||||||
|
|
||||||
# Start training
|
|
||||||
start_iter = self.current_learning_iteration
|
|
||||||
tot_iter = start_iter + num_learning_iterations
|
|
||||||
for it in range(start_iter, tot_iter):
|
|
||||||
start = time.time()
|
|
||||||
# Rollout
|
|
||||||
with torch.inference_mode():
|
|
||||||
for _ in range(self.num_steps_per_env):
|
|
||||||
# Sample actions
|
|
||||||
actions = self.alg.act(obs)
|
|
||||||
# Step the environment
|
|
||||||
obs, rewards, dones, extras = self.env.step(actions.to(self.env.device))
|
|
||||||
# Move to device
|
|
||||||
obs, rewards, dones = (obs.to(self.device), rewards.to(self.device), dones.to(self.device))
|
|
||||||
# process the step
|
|
||||||
self.alg.process_env_step(obs, rewards, dones, extras)
|
|
||||||
# Extract intrinsic rewards (only for logging)
|
|
||||||
intrinsic_rewards = self.alg.intrinsic_rewards if self.alg.rnd else None
|
|
||||||
# book keeping
|
|
||||||
if self.log_dir is not None:
|
|
||||||
if "episode" in extras:
|
|
||||||
ep_infos.append(extras["episode"])
|
|
||||||
elif "log" in extras:
|
|
||||||
ep_infos.append(extras["log"])
|
|
||||||
# Update rewards
|
|
||||||
if self.alg.rnd:
|
|
||||||
cur_ereward_sum += rewards
|
|
||||||
cur_ireward_sum += intrinsic_rewards # type: ignore
|
|
||||||
cur_reward_sum += rewards + intrinsic_rewards
|
|
||||||
else:
|
|
||||||
cur_reward_sum += rewards
|
|
||||||
# Update episode length
|
|
||||||
cur_episode_length += 1
|
|
||||||
# Clear data for completed episodes
|
|
||||||
# -- common
|
|
||||||
new_ids = (dones > 0).nonzero(as_tuple=False)
|
|
||||||
rewbuffer.extend(cur_reward_sum[new_ids][:, 0].cpu().numpy().tolist())
|
|
||||||
lenbuffer.extend(cur_episode_length[new_ids][:, 0].cpu().numpy().tolist())
|
|
||||||
cur_reward_sum[new_ids] = 0
|
|
||||||
cur_episode_length[new_ids] = 0
|
|
||||||
# -- intrinsic and extrinsic rewards
|
|
||||||
if self.alg.rnd:
|
|
||||||
erewbuffer.extend(cur_ereward_sum[new_ids][:, 0].cpu().numpy().tolist())
|
|
||||||
irewbuffer.extend(cur_ireward_sum[new_ids][:, 0].cpu().numpy().tolist())
|
|
||||||
cur_ereward_sum[new_ids] = 0
|
|
||||||
cur_ireward_sum[new_ids] = 0
|
|
||||||
|
|
||||||
stop = time.time()
|
|
||||||
collection_time = stop - start
|
|
||||||
start = stop
|
|
||||||
|
|
||||||
# compute returns
|
|
||||||
self.alg.compute_returns(obs)
|
|
||||||
|
|
||||||
# update policy
|
|
||||||
loss_dict = self.alg.update()
|
|
||||||
|
|
||||||
stop = time.time()
|
|
||||||
learn_time = stop - start
|
|
||||||
self.current_learning_iteration = it
|
|
||||||
# log info
|
|
||||||
if self.log_dir is not None and not self.disable_logs:
|
|
||||||
# Log information
|
|
||||||
self.log(locals())
|
|
||||||
# Save model
|
|
||||||
if it % self.save_interval == 0:
|
|
||||||
self.save(os.path.join(self.log_dir, f"model_{it}.pt"))
|
|
||||||
|
|
||||||
# Clear episode infos
|
|
||||||
ep_infos.clear()
|
|
||||||
# Save code state
|
|
||||||
if it == start_iter and not self.disable_logs:
|
|
||||||
# obtain all the diff files
|
|
||||||
git_file_paths = store_code_state(self.log_dir, self.git_status_repos)
|
|
||||||
# if possible store them to wandb
|
|
||||||
if self.logger_type in ["wandb", "neptune"] and git_file_paths:
|
|
||||||
for path in git_file_paths:
|
|
||||||
self.writer.save_file(path)
|
|
||||||
|
|
||||||
# Save the final model after training
|
|
||||||
if self.log_dir is not None and not self.disable_logs:
|
|
||||||
self.save(os.path.join(self.log_dir, f"model_{self.current_learning_iteration}.pt"))
|
|
||||||
|
|
||||||
def log(self, locs: dict, width: int = 80, pad: int = 35):
|
|
||||||
# Compute the collection size
|
|
||||||
collection_size = self.num_steps_per_env * self.env.num_envs * self.gpu_world_size
|
|
||||||
# Update total time-steps and time
|
|
||||||
self.tot_timesteps += collection_size
|
|
||||||
self.tot_time += locs["collection_time"] + locs["learn_time"]
|
|
||||||
iteration_time = locs["collection_time"] + locs["learn_time"]
|
|
||||||
|
|
||||||
# -- Episode info
|
|
||||||
ep_string = ""
|
|
||||||
if locs["ep_infos"]:
|
|
||||||
for key in locs["ep_infos"][0]:
|
|
||||||
infotensor = torch.tensor([], device=self.device)
|
|
||||||
for ep_info in locs["ep_infos"]:
|
|
||||||
# handle scalar and zero dimensional tensor infos
|
|
||||||
if key not in ep_info:
|
|
||||||
continue
|
|
||||||
if not isinstance(ep_info[key], torch.Tensor):
|
|
||||||
ep_info[key] = torch.Tensor([ep_info[key]])
|
|
||||||
if len(ep_info[key].shape) == 0:
|
|
||||||
ep_info[key] = ep_info[key].unsqueeze(0)
|
|
||||||
infotensor = torch.cat((infotensor, ep_info[key].to(self.device)))
|
|
||||||
value = torch.mean(infotensor)
|
|
||||||
# log to logger and terminal
|
|
||||||
if "/" in key:
|
|
||||||
self.writer.add_scalar(key, value, locs["it"])
|
|
||||||
ep_string += f"""{f'{key}:':>{pad}} {value:.4f}\n"""
|
|
||||||
else:
|
|
||||||
self.writer.add_scalar("Episode/" + key, value, locs["it"])
|
|
||||||
ep_string += f"""{f'Mean episode {key}:':>{pad}} {value:.4f}\n"""
|
|
||||||
|
|
||||||
mean_std = self.alg.policy.action_std.mean()
|
|
||||||
fps = int(collection_size / (locs["collection_time"] + locs["learn_time"]))
|
|
||||||
|
|
||||||
# -- Losses
|
|
||||||
for key, value in locs["loss_dict"].items():
|
|
||||||
self.writer.add_scalar(f"Loss/{key}", value, locs["it"])
|
|
||||||
self.writer.add_scalar("Loss/learning_rate", self.alg.learning_rate, locs["it"])
|
|
||||||
|
|
||||||
# -- Policy
|
|
||||||
self.writer.add_scalar("Policy/mean_noise_std", mean_std.item(), locs["it"])
|
|
||||||
|
|
||||||
# -- Performance
|
|
||||||
self.writer.add_scalar("Perf/total_fps", fps, locs["it"])
|
|
||||||
self.writer.add_scalar("Perf/collection time", locs["collection_time"], locs["it"])
|
|
||||||
self.writer.add_scalar("Perf/learning_time", locs["learn_time"], locs["it"])
|
|
||||||
|
|
||||||
# -- Training
|
|
||||||
if len(locs["rewbuffer"]) > 0:
|
|
||||||
# separate logging for intrinsic and extrinsic rewards
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
self.writer.add_scalar("Rnd/mean_extrinsic_reward", statistics.mean(locs["erewbuffer"]), locs["it"])
|
|
||||||
self.writer.add_scalar("Rnd/mean_intrinsic_reward", statistics.mean(locs["irewbuffer"]), locs["it"])
|
|
||||||
self.writer.add_scalar("Rnd/weight", self.alg.rnd.weight, locs["it"])
|
|
||||||
# everything else
|
|
||||||
self.writer.add_scalar("Train/mean_reward", statistics.mean(locs["rewbuffer"]), locs["it"])
|
|
||||||
self.writer.add_scalar("Train/mean_episode_length", statistics.mean(locs["lenbuffer"]), locs["it"])
|
|
||||||
if self.logger_type != "wandb": # wandb does not support non-integer x-axis logging
|
|
||||||
self.writer.add_scalar("Train/mean_reward/time", statistics.mean(locs["rewbuffer"]), self.tot_time)
|
|
||||||
self.writer.add_scalar(
|
|
||||||
"Train/mean_episode_length/time", statistics.mean(locs["lenbuffer"]), self.tot_time
|
|
||||||
)
|
|
||||||
|
|
||||||
str = f" \033[1m Learning iteration {locs['it']}/{locs['tot_iter']} \033[0m "
|
|
||||||
|
|
||||||
if len(locs["rewbuffer"]) > 0:
|
|
||||||
log_string = (
|
|
||||||
f"""{'#' * width}\n"""
|
|
||||||
f"""{str.center(width, ' ')}\n\n"""
|
|
||||||
f"""{'Computation:':>{pad}} {fps:.0f} steps/s (collection: {locs[
|
|
||||||
'collection_time']:.3f}s, learning {locs['learn_time']:.3f}s)\n"""
|
|
||||||
f"""{'Mean action noise std:':>{pad}} {mean_std.item():.2f}\n"""
|
|
||||||
)
|
|
||||||
# -- Losses
|
|
||||||
for key, value in locs["loss_dict"].items():
|
|
||||||
log_string += f"""{f'Mean {key} loss:':>{pad}} {value:.4f}\n"""
|
|
||||||
# -- Rewards
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
log_string += (
|
|
||||||
f"""{'Mean extrinsic reward:':>{pad}} {statistics.mean(locs['erewbuffer']):.2f}\n"""
|
|
||||||
f"""{'Mean intrinsic reward:':>{pad}} {statistics.mean(locs['irewbuffer']):.2f}\n"""
|
|
||||||
)
|
|
||||||
log_string += f"""{'Mean reward:':>{pad}} {statistics.mean(locs['rewbuffer']):.2f}\n"""
|
|
||||||
# -- episode info
|
|
||||||
log_string += f"""{'Mean episode length:':>{pad}} {statistics.mean(locs['lenbuffer']):.2f}\n"""
|
|
||||||
else:
|
|
||||||
log_string = (
|
|
||||||
f"""{'#' * width}\n"""
|
|
||||||
f"""{str.center(width, ' ')}\n\n"""
|
|
||||||
f"""{'Computation:':>{pad}} {fps:.0f} steps/s (collection: {locs[
|
|
||||||
'collection_time']:.3f}s, learning {locs['learn_time']:.3f}s)\n"""
|
|
||||||
f"""{'Mean action noise std:':>{pad}} {mean_std.item():.2f}\n"""
|
|
||||||
)
|
|
||||||
for key, value in locs["loss_dict"].items():
|
|
||||||
log_string += f"""{f'{key}:':>{pad}} {value:.4f}\n"""
|
|
||||||
|
|
||||||
log_string += ep_string
|
|
||||||
log_string += (
|
|
||||||
f"""{'-' * width}\n"""
|
|
||||||
f"""{'Total timesteps:':>{pad}} {self.tot_timesteps}\n"""
|
|
||||||
f"""{'Iteration time:':>{pad}} {iteration_time:.2f}s\n"""
|
|
||||||
f"""{'Time elapsed:':>{pad}} {time.strftime("%H:%M:%S", time.gmtime(self.tot_time))}\n"""
|
|
||||||
f"""{'ETA:':>{pad}} {time.strftime(
|
|
||||||
"%H:%M:%S",
|
|
||||||
time.gmtime(
|
|
||||||
self.tot_time / (locs['it'] - locs['start_iter'] + 1)
|
|
||||||
* (locs['start_iter'] + locs['num_learning_iterations'] - locs['it'])
|
|
||||||
)
|
|
||||||
)}\n"""
|
|
||||||
)
|
|
||||||
print(log_string)
|
|
||||||
|
|
||||||
def save(self, path: str, infos=None):
|
|
||||||
# -- Save model
|
|
||||||
saved_dict = {
|
|
||||||
"model_state_dict": self.alg.policy.state_dict(),
|
|
||||||
"optimizer_state_dict": self.alg.optimizer.state_dict(),
|
|
||||||
"iter": self.current_learning_iteration,
|
|
||||||
"infos": infos,
|
|
||||||
}
|
|
||||||
# -- Save RND model if used
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
saved_dict["rnd_state_dict"] = self.alg.rnd.state_dict()
|
|
||||||
saved_dict["rnd_optimizer_state_dict"] = self.alg.rnd_optimizer.state_dict()
|
|
||||||
torch.save(saved_dict, path)
|
|
||||||
|
|
||||||
# upload model to external logging service
|
|
||||||
if self.logger_type in ["neptune", "wandb"] and not self.disable_logs:
|
|
||||||
self.writer.save_model(path, self.current_learning_iteration)
|
|
||||||
|
|
||||||
def load(self, path: str, load_optimizer: bool = True, map_location: str | None = None):
|
|
||||||
loaded_dict = torch.load(path, weights_only=False, map_location=map_location)
|
|
||||||
# -- Load model
|
|
||||||
resumed_training = self.alg.policy.load_state_dict(loaded_dict["model_state_dict"])
|
|
||||||
# -- Load RND model if used
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
self.alg.rnd.load_state_dict(loaded_dict["rnd_state_dict"])
|
|
||||||
# -- load optimizer if used
|
|
||||||
if load_optimizer and resumed_training:
|
|
||||||
# -- algorithm optimizer
|
|
||||||
self.alg.optimizer.load_state_dict(loaded_dict["optimizer_state_dict"])
|
|
||||||
# -- RND optimizer if used
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
self.alg.rnd_optimizer.load_state_dict(loaded_dict["rnd_optimizer_state_dict"])
|
|
||||||
# -- load current learning iteration
|
|
||||||
if resumed_training:
|
|
||||||
self.current_learning_iteration = loaded_dict["iter"]
|
|
||||||
return loaded_dict["infos"]
|
|
||||||
|
|
||||||
def get_inference_policy(self, device=None):
|
|
||||||
self.eval_mode() # switch to evaluation mode (dropout for example)
|
|
||||||
if device is not None:
|
|
||||||
self.alg.policy.to(device)
|
|
||||||
return self.alg.policy.act_inference
|
|
||||||
|
|
||||||
def train_mode(self):
|
|
||||||
# -- PPO
|
|
||||||
self.alg.policy.train()
|
|
||||||
# -- RND
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
self.alg.rnd.train()
|
|
||||||
|
|
||||||
def eval_mode(self):
|
|
||||||
# -- PPO
|
|
||||||
self.alg.policy.eval()
|
|
||||||
# -- RND
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
self.alg.rnd.eval()
|
|
||||||
|
|
||||||
def add_git_repo_to_log(self, repo_file_path):
|
|
||||||
self.git_status_repos.append(repo_file_path)
|
|
||||||
|
|
||||||
"""
|
|
||||||
Helper functions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _configure_multi_gpu(self):
|
|
||||||
"""Configure multi-gpu training."""
|
|
||||||
# check if distributed training is enabled
|
|
||||||
self.gpu_world_size = int(os.getenv("WORLD_SIZE", "1"))
|
|
||||||
self.is_distributed = self.gpu_world_size > 1
|
|
||||||
|
|
||||||
# if not distributed training, set local and global rank to 0 and return
|
|
||||||
if not self.is_distributed:
|
|
||||||
self.gpu_local_rank = 0
|
|
||||||
self.gpu_global_rank = 0
|
|
||||||
self.multi_gpu_cfg = None
|
|
||||||
return
|
|
||||||
|
|
||||||
# get rank and world size
|
|
||||||
self.gpu_local_rank = int(os.getenv("LOCAL_RANK", "0"))
|
|
||||||
self.gpu_global_rank = int(os.getenv("RANK", "0"))
|
|
||||||
|
|
||||||
# make a configuration dictionary
|
|
||||||
self.multi_gpu_cfg = {
|
|
||||||
"global_rank": self.gpu_global_rank, # rank of the main process
|
|
||||||
"local_rank": self.gpu_local_rank, # rank of the current process
|
|
||||||
"world_size": self.gpu_world_size, # total number of processes
|
|
||||||
}
|
|
||||||
|
|
||||||
# check if user has device specified for local rank
|
|
||||||
if self.device != f"cuda:{self.gpu_local_rank}":
|
|
||||||
raise ValueError(
|
|
||||||
f"Device '{self.device}' does not match expected device for local rank '{self.gpu_local_rank}'."
|
|
||||||
)
|
|
||||||
# validate multi-gpu configuration
|
|
||||||
if self.gpu_local_rank >= self.gpu_world_size:
|
|
||||||
raise ValueError(
|
|
||||||
f"Local rank '{self.gpu_local_rank}' is greater than or equal to world size '{self.gpu_world_size}'."
|
|
||||||
)
|
|
||||||
if self.gpu_global_rank >= self.gpu_world_size:
|
|
||||||
raise ValueError(
|
|
||||||
f"Global rank '{self.gpu_global_rank}' is greater than or equal to world size '{self.gpu_world_size}'."
|
|
||||||
)
|
|
||||||
|
|
||||||
# initialize torch distributed
|
|
||||||
torch.distributed.init_process_group(backend="nccl", rank=self.gpu_global_rank, world_size=self.gpu_world_size)
|
|
||||||
# set device to the local rank
|
|
||||||
torch.cuda.set_device(self.gpu_local_rank)
|
|
||||||
|
|
||||||
def _construct_algorithm(self, obs) -> PPO:
|
|
||||||
"""Construct the actor-critic algorithm."""
|
|
||||||
# resolve RND config
|
|
||||||
self.alg_cfg = resolve_rnd_config(self.alg_cfg, obs, self.cfg["obs_groups"], self.env)
|
|
||||||
|
|
||||||
# resolve symmetry config
|
|
||||||
self.alg_cfg = resolve_symmetry_config(self.alg_cfg, self.env)
|
|
||||||
|
|
||||||
# resolve deprecated normalization config
|
|
||||||
if self.cfg.get("empirical_normalization") is not None:
|
|
||||||
warnings.warn(
|
|
||||||
"The `empirical_normalization` parameter is deprecated. Please set `actor_obs_normalization` and "
|
|
||||||
"`critic_obs_normalization` as part of the `policy` configuration instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
if self.policy_cfg.get("actor_obs_normalization") is None:
|
|
||||||
self.policy_cfg["actor_obs_normalization"] = self.cfg["empirical_normalization"]
|
|
||||||
if self.policy_cfg.get("critic_obs_normalization") is None:
|
|
||||||
self.policy_cfg["critic_obs_normalization"] = self.cfg["empirical_normalization"]
|
|
||||||
|
|
||||||
# initialize the actor-critic
|
|
||||||
actor_critic_class = eval(self.policy_cfg.pop("class_name"))
|
|
||||||
actor_critic: ActorCritic | ActorCriticRecurrent = actor_critic_class(
|
|
||||||
obs, self.cfg["obs_groups"], self.env.num_actions, **self.policy_cfg
|
|
||||||
).to(self.device)
|
|
||||||
|
|
||||||
# initialize the algorithm
|
|
||||||
alg_class = eval(self.alg_cfg.pop("class_name"))
|
|
||||||
alg: PPO = alg_class(actor_critic, device=self.device, **self.alg_cfg, multi_gpu_cfg=self.multi_gpu_cfg)
|
|
||||||
|
|
||||||
# initialize the storage
|
|
||||||
alg.init_storage(
|
|
||||||
"rl",
|
|
||||||
self.env.num_envs,
|
|
||||||
self.num_steps_per_env,
|
|
||||||
obs,
|
|
||||||
[self.env.num_actions],
|
|
||||||
)
|
|
||||||
|
|
||||||
return alg
|
|
||||||
|
|
||||||
def _prepare_logging_writer(self):
|
|
||||||
"""Prepares the logging writers."""
|
|
||||||
if self.log_dir is not None and self.writer is None and not self.disable_logs:
|
|
||||||
# Launch either Tensorboard or Neptune & Tensorboard summary writer(s), default: Tensorboard.
|
|
||||||
self.logger_type = self.cfg.get("logger", "tensorboard")
|
|
||||||
self.logger_type = self.logger_type.lower()
|
|
||||||
|
|
||||||
if self.logger_type == "neptune":
|
|
||||||
from rsl_rl.utils.neptune_utils import NeptuneSummaryWriter
|
|
||||||
|
|
||||||
self.writer = NeptuneSummaryWriter(log_dir=self.log_dir, flush_secs=10, cfg=self.cfg)
|
|
||||||
self.writer.log_config(self.env.cfg, self.cfg, self.alg_cfg, self.policy_cfg)
|
|
||||||
elif self.logger_type == "wandb":
|
|
||||||
from rsl_rl.utils.wandb_utils import WandbSummaryWriter
|
|
||||||
|
|
||||||
self.writer = WandbSummaryWriter(log_dir=self.log_dir, flush_secs=10, cfg=self.cfg)
|
|
||||||
self.writer.log_config(self.env.cfg, self.cfg, self.alg_cfg, self.policy_cfg)
|
|
||||||
elif self.logger_type == "tensorboard":
|
|
||||||
from torch.utils.tensorboard import SummaryWriter
|
|
||||||
|
|
||||||
self.writer = SummaryWriter(log_dir=self.log_dir, flush_secs=10)
|
|
||||||
else:
|
|
||||||
raise ValueError("Logger type not found. Please choose 'neptune', 'wandb' or 'tensorboard'.")
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
"""Implementation of transitions storage for RL-agent."""
|
|
||||||
|
|
||||||
from .rollout_storage import RolloutStorage
|
|
||||||
from .replay_buffer_multi import ReplayBufferMulti
|
|
||||||
__all__ = ["RolloutStorage", "ReplayBufferMulti"]
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import torch
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
|
|
||||||
class ReplayBufferMulti:
|
|
||||||
"""Fixed-size buffer to store experience tuples."""
|
|
||||||
|
|
||||||
def __init__(self, obs_dim, buffer_size, num_amp_frames, device):
|
|
||||||
"""Initialize a ReplayBuffer object.
|
|
||||||
Arguments:
|
|
||||||
buffer_size (int): maximum size of buffer
|
|
||||||
"""
|
|
||||||
self.states = torch.zeros(buffer_size, num_amp_frames, obs_dim).to(device)
|
|
||||||
self.num_amp_frames = num_amp_frames
|
|
||||||
self.buffer_size = buffer_size
|
|
||||||
self.device = device
|
|
||||||
|
|
||||||
self.step = 0
|
|
||||||
self.num_samples = 0
|
|
||||||
|
|
||||||
def insert(self, states):
|
|
||||||
"""Add new states to memory."""
|
|
||||||
num_states = states.shape[0]
|
|
||||||
start_idx = self.step
|
|
||||||
end_idx = self.step + num_states
|
|
||||||
if end_idx > self.buffer_size:
|
|
||||||
self.states[self.step:self.buffer_size] = states[:self.buffer_size - self.step]
|
|
||||||
self.states[:end_idx - self.buffer_size] = states[self.buffer_size - self.step:]
|
|
||||||
else:
|
|
||||||
self.states[start_idx:end_idx] = states
|
|
||||||
|
|
||||||
self.num_samples = min(self.buffer_size, max(end_idx, self.num_samples))
|
|
||||||
self.step = (self.step + num_states) % self.buffer_size
|
|
||||||
|
|
||||||
def feed_forward_generator(self, num_mini_batch, mini_batch_size):
|
|
||||||
for _ in range(num_mini_batch):
|
|
||||||
sample_idxs = np.random.choice(self.num_samples, size=mini_batch_size)
|
|
||||||
yield (self.states[sample_idxs].to(self.device))
|
|
||||||
|
|
@ -1,260 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch
|
|
||||||
from tensordict import TensorDict
|
|
||||||
|
|
||||||
from rsl_rl.utils import split_and_pad_trajectories
|
|
||||||
|
|
||||||
|
|
||||||
class RolloutStorage:
|
|
||||||
class Transition:
|
|
||||||
def __init__(self):
|
|
||||||
self.observations = None
|
|
||||||
self.actions = None
|
|
||||||
self.privileged_actions = None
|
|
||||||
self.rewards = None
|
|
||||||
self.dones = None
|
|
||||||
self.values = None
|
|
||||||
self.actions_log_prob = None
|
|
||||||
self.action_mean = None
|
|
||||||
self.action_sigma = None
|
|
||||||
self.hidden_states = None
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
self.__init__()
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
training_type,
|
|
||||||
num_envs,
|
|
||||||
num_transitions_per_env,
|
|
||||||
obs,
|
|
||||||
actions_shape,
|
|
||||||
device="cpu",
|
|
||||||
):
|
|
||||||
# store inputs
|
|
||||||
self.training_type = training_type
|
|
||||||
self.device = device
|
|
||||||
self.num_transitions_per_env = num_transitions_per_env
|
|
||||||
self.num_envs = num_envs
|
|
||||||
self.actions_shape = actions_shape
|
|
||||||
|
|
||||||
# Core
|
|
||||||
self.observations = TensorDict(
|
|
||||||
{key: torch.zeros(num_transitions_per_env, *value.shape, device=device) for key, value in obs.items()},
|
|
||||||
batch_size=[num_transitions_per_env, num_envs],
|
|
||||||
device=self.device,
|
|
||||||
)
|
|
||||||
self.rewards = torch.zeros(num_transitions_per_env, num_envs, 1, device=self.device)
|
|
||||||
self.actions = torch.zeros(num_transitions_per_env, num_envs, *actions_shape, device=self.device)
|
|
||||||
self.dones = torch.zeros(num_transitions_per_env, num_envs, 1, device=self.device).byte()
|
|
||||||
|
|
||||||
# for distillation
|
|
||||||
if training_type == "distillation":
|
|
||||||
self.privileged_actions = torch.zeros(num_transitions_per_env, num_envs, *actions_shape, device=self.device)
|
|
||||||
|
|
||||||
# for reinforcement learning
|
|
||||||
if training_type == "rl":
|
|
||||||
self.values = torch.zeros(num_transitions_per_env, num_envs, 1, device=self.device)
|
|
||||||
self.actions_log_prob = torch.zeros(num_transitions_per_env, num_envs, 1, device=self.device)
|
|
||||||
self.mu = torch.zeros(num_transitions_per_env, num_envs, *actions_shape, device=self.device)
|
|
||||||
self.sigma = torch.zeros(num_transitions_per_env, num_envs, *actions_shape, device=self.device)
|
|
||||||
self.returns = torch.zeros(num_transitions_per_env, num_envs, 1, device=self.device)
|
|
||||||
self.advantages = torch.zeros(num_transitions_per_env, num_envs, 1, device=self.device)
|
|
||||||
|
|
||||||
# For RNN networks
|
|
||||||
self.saved_hidden_states_a = None
|
|
||||||
self.saved_hidden_states_c = None
|
|
||||||
|
|
||||||
# counter for the number of transitions stored
|
|
||||||
self.step = 0
|
|
||||||
|
|
||||||
def add_transitions(self, transition: Transition):
|
|
||||||
# check if the transition is valid
|
|
||||||
if self.step >= self.num_transitions_per_env:
|
|
||||||
raise OverflowError("Rollout buffer overflow! You should call clear() before adding new transitions.")
|
|
||||||
|
|
||||||
# Core
|
|
||||||
self.observations[self.step].copy_(transition.observations)
|
|
||||||
self.actions[self.step].copy_(transition.actions)
|
|
||||||
self.rewards[self.step].copy_(transition.rewards.view(-1, 1))
|
|
||||||
self.dones[self.step].copy_(transition.dones.view(-1, 1))
|
|
||||||
|
|
||||||
# for distillation
|
|
||||||
if self.training_type == "distillation":
|
|
||||||
self.privileged_actions[self.step].copy_(transition.privileged_actions)
|
|
||||||
|
|
||||||
# for reinforcement learning
|
|
||||||
if self.training_type == "rl":
|
|
||||||
self.values[self.step].copy_(transition.values)
|
|
||||||
self.actions_log_prob[self.step].copy_(transition.actions_log_prob.view(-1, 1))
|
|
||||||
self.mu[self.step].copy_(transition.action_mean)
|
|
||||||
self.sigma[self.step].copy_(transition.action_sigma)
|
|
||||||
|
|
||||||
# For RNN networks
|
|
||||||
self._save_hidden_states(transition.hidden_states)
|
|
||||||
|
|
||||||
# increment the counter
|
|
||||||
self.step += 1
|
|
||||||
|
|
||||||
def _save_hidden_states(self, hidden_states):
|
|
||||||
if hidden_states is None or hidden_states == (None, None):
|
|
||||||
return
|
|
||||||
# make a tuple out of GRU hidden state sto match the LSTM format
|
|
||||||
hid_a = hidden_states[0] if isinstance(hidden_states[0], tuple) else (hidden_states[0],)
|
|
||||||
hid_c = hidden_states[1] if isinstance(hidden_states[1], tuple) else (hidden_states[1],)
|
|
||||||
# initialize if needed
|
|
||||||
if self.saved_hidden_states_a is None:
|
|
||||||
self.saved_hidden_states_a = [
|
|
||||||
torch.zeros(self.observations.shape[0], *hid_a[i].shape, device=self.device) for i in range(len(hid_a))
|
|
||||||
]
|
|
||||||
self.saved_hidden_states_c = [
|
|
||||||
torch.zeros(self.observations.shape[0], *hid_c[i].shape, device=self.device) for i in range(len(hid_c))
|
|
||||||
]
|
|
||||||
# copy the states
|
|
||||||
for i in range(len(hid_a)):
|
|
||||||
self.saved_hidden_states_a[i][self.step].copy_(hid_a[i])
|
|
||||||
self.saved_hidden_states_c[i][self.step].copy_(hid_c[i])
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
self.step = 0
|
|
||||||
|
|
||||||
def compute_returns(self, last_values, gamma, lam, normalize_advantage: bool = True):
|
|
||||||
advantage = 0
|
|
||||||
for step in reversed(range(self.num_transitions_per_env)):
|
|
||||||
# if we are at the last step, bootstrap the return value
|
|
||||||
if step == self.num_transitions_per_env - 1:
|
|
||||||
next_values = last_values
|
|
||||||
else:
|
|
||||||
next_values = self.values[step + 1]
|
|
||||||
# 1 if we are not in a terminal state, 0 otherwise
|
|
||||||
next_is_not_terminal = 1.0 - self.dones[step].float()
|
|
||||||
# TD error: r_t + gamma * V(s_{t+1}) - V(s_t)
|
|
||||||
delta = self.rewards[step] + next_is_not_terminal * gamma * next_values - self.values[step]
|
|
||||||
# Advantage: A(s_t, a_t) = delta_t + gamma * lambda * A(s_{t+1}, a_{t+1})
|
|
||||||
advantage = delta + next_is_not_terminal * gamma * lam * advantage
|
|
||||||
# Return: R_t = A(s_t, a_t) + V(s_t)
|
|
||||||
self.returns[step] = advantage + self.values[step]
|
|
||||||
|
|
||||||
# Compute the advantages
|
|
||||||
self.advantages = self.returns - self.values
|
|
||||||
# Normalize the advantages if flag is set
|
|
||||||
# This is to prevent double normalization (i.e. if per minibatch normalization is used)
|
|
||||||
if normalize_advantage:
|
|
||||||
self.advantages = (self.advantages - self.advantages.mean()) / (self.advantages.std() + 1e-8)
|
|
||||||
|
|
||||||
# for distillation
|
|
||||||
def generator(self):
|
|
||||||
if self.training_type != "distillation":
|
|
||||||
raise ValueError("This function is only available for distillation training.")
|
|
||||||
|
|
||||||
for i in range(self.num_transitions_per_env):
|
|
||||||
yield self.observations[i], self.actions[i], self.privileged_actions[i], self.dones[i]
|
|
||||||
|
|
||||||
# for reinforcement learning with feedforward networks
|
|
||||||
def mini_batch_generator(self, num_mini_batches, num_epochs=8):
|
|
||||||
if self.training_type != "rl":
|
|
||||||
raise ValueError("This function is only available for reinforcement learning training.")
|
|
||||||
batch_size = self.num_envs * self.num_transitions_per_env
|
|
||||||
mini_batch_size = batch_size // num_mini_batches
|
|
||||||
indices = torch.randperm(num_mini_batches * mini_batch_size, requires_grad=False, device=self.device)
|
|
||||||
|
|
||||||
# Core
|
|
||||||
observations = self.observations.flatten(0, 1)
|
|
||||||
actions = self.actions.flatten(0, 1)
|
|
||||||
values = self.values.flatten(0, 1)
|
|
||||||
returns = self.returns.flatten(0, 1)
|
|
||||||
|
|
||||||
# For PPO
|
|
||||||
old_actions_log_prob = self.actions_log_prob.flatten(0, 1)
|
|
||||||
advantages = self.advantages.flatten(0, 1)
|
|
||||||
old_mu = self.mu.flatten(0, 1)
|
|
||||||
old_sigma = self.sigma.flatten(0, 1)
|
|
||||||
|
|
||||||
for epoch in range(num_epochs):
|
|
||||||
for i in range(num_mini_batches):
|
|
||||||
# Select the indices for the mini-batch
|
|
||||||
start = i * mini_batch_size
|
|
||||||
end = (i + 1) * mini_batch_size
|
|
||||||
batch_idx = indices[start:end]
|
|
||||||
|
|
||||||
# Create the mini-batch
|
|
||||||
# -- Core
|
|
||||||
obs_batch = observations[batch_idx]
|
|
||||||
actions_batch = actions[batch_idx]
|
|
||||||
|
|
||||||
# -- For PPO
|
|
||||||
target_values_batch = values[batch_idx]
|
|
||||||
returns_batch = returns[batch_idx]
|
|
||||||
old_actions_log_prob_batch = old_actions_log_prob[batch_idx]
|
|
||||||
advantages_batch = advantages[batch_idx]
|
|
||||||
old_mu_batch = old_mu[batch_idx]
|
|
||||||
old_sigma_batch = old_sigma[batch_idx]
|
|
||||||
|
|
||||||
# yield the mini-batch
|
|
||||||
yield obs_batch, actions_batch, target_values_batch, advantages_batch, returns_batch, old_actions_log_prob_batch, old_mu_batch, old_sigma_batch, (
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
), None
|
|
||||||
|
|
||||||
# for reinfrocement learning with recurrent networks
|
|
||||||
def recurrent_mini_batch_generator(self, num_mini_batches, num_epochs=8):
|
|
||||||
if self.training_type != "rl":
|
|
||||||
raise ValueError("This function is only available for reinforcement learning training.")
|
|
||||||
padded_obs_trajectories, trajectory_masks = split_and_pad_trajectories(self.observations, self.dones)
|
|
||||||
|
|
||||||
mini_batch_size = self.num_envs // num_mini_batches
|
|
||||||
for ep in range(num_epochs):
|
|
||||||
first_traj = 0
|
|
||||||
for i in range(num_mini_batches):
|
|
||||||
start = i * mini_batch_size
|
|
||||||
stop = (i + 1) * mini_batch_size
|
|
||||||
|
|
||||||
dones = self.dones.squeeze(-1)
|
|
||||||
last_was_done = torch.zeros_like(dones, dtype=torch.bool)
|
|
||||||
last_was_done[1:] = dones[:-1]
|
|
||||||
last_was_done[0] = True
|
|
||||||
trajectories_batch_size = torch.sum(last_was_done[:, start:stop])
|
|
||||||
last_traj = first_traj + trajectories_batch_size
|
|
||||||
|
|
||||||
masks_batch = trajectory_masks[:, first_traj:last_traj]
|
|
||||||
obs_batch = padded_obs_trajectories[:, first_traj:last_traj]
|
|
||||||
actions_batch = self.actions[:, start:stop]
|
|
||||||
old_mu_batch = self.mu[:, start:stop]
|
|
||||||
old_sigma_batch = self.sigma[:, start:stop]
|
|
||||||
returns_batch = self.returns[:, start:stop]
|
|
||||||
advantages_batch = self.advantages[:, start:stop]
|
|
||||||
values_batch = self.values[:, start:stop]
|
|
||||||
old_actions_log_prob_batch = self.actions_log_prob[:, start:stop]
|
|
||||||
|
|
||||||
# reshape to [num_envs, time, num layers, hidden dim] (original shape: [time, num_layers, num_envs, hidden_dim])
|
|
||||||
# then take only time steps after dones (flattens num envs and time dimensions),
|
|
||||||
# take a batch of trajectories and finally reshape back to [num_layers, batch, hidden_dim]
|
|
||||||
last_was_done = last_was_done.permute(1, 0)
|
|
||||||
hid_a_batch = [
|
|
||||||
saved_hidden_states.permute(2, 0, 1, 3)[last_was_done][first_traj:last_traj]
|
|
||||||
.transpose(1, 0)
|
|
||||||
.contiguous()
|
|
||||||
for saved_hidden_states in self.saved_hidden_states_a
|
|
||||||
]
|
|
||||||
hid_c_batch = [
|
|
||||||
saved_hidden_states.permute(2, 0, 1, 3)[last_was_done][first_traj:last_traj]
|
|
||||||
.transpose(1, 0)
|
|
||||||
.contiguous()
|
|
||||||
for saved_hidden_states in self.saved_hidden_states_c
|
|
||||||
]
|
|
||||||
# remove the tuple for GRU
|
|
||||||
hid_a_batch = hid_a_batch[0] if len(hid_a_batch) == 1 else hid_a_batch
|
|
||||||
hid_c_batch = hid_c_batch[0] if len(hid_c_batch) == 1 else hid_c_batch
|
|
||||||
|
|
||||||
yield obs_batch, actions_batch, values_batch, advantages_batch, returns_batch, old_actions_log_prob_batch, old_mu_batch, old_sigma_batch, (
|
|
||||||
hid_a_batch,
|
|
||||||
hid_c_batch,
|
|
||||||
), masks_batch
|
|
||||||
|
|
||||||
first_traj = last_traj
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
"""Helper functions."""
|
|
||||||
|
|
||||||
from .utils import *
|
|
||||||
from .motion_loader_g1 import G1_AMPLoader
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"G1_AMPLoader",
|
|
||||||
]
|
|
||||||
|
|
@ -1,388 +0,0 @@
|
||||||
import os
|
|
||||||
from os.path import join as pjoin
|
|
||||||
import glob
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import numpy as np
|
|
||||||
from pybullet_utils import transformations
|
|
||||||
|
|
||||||
from rsl_rl.utils import motion_util
|
|
||||||
|
|
||||||
_EPS = np.finfo(float).eps * 4.0
|
|
||||||
def quaternion_slerp(q0, q1, fraction, spin=0, shortestpath=True):
|
|
||||||
"""Batch quaternion spherical linear interpolation."""
|
|
||||||
|
|
||||||
out = torch.zeros_like(q0)
|
|
||||||
|
|
||||||
zero_mask = torch.isclose(fraction, torch.zeros_like(fraction)).squeeze()
|
|
||||||
ones_mask = torch.isclose(fraction, torch.ones_like(fraction)).squeeze()
|
|
||||||
out[zero_mask] = q0[zero_mask]
|
|
||||||
out[ones_mask] = q1[ones_mask]
|
|
||||||
|
|
||||||
d = torch.sum(q0 * q1, dim=-1, keepdim=True)
|
|
||||||
dist_mask = (torch.abs(torch.abs(d) - 1.0) < _EPS).squeeze()
|
|
||||||
out[dist_mask] = q0[dist_mask]
|
|
||||||
|
|
||||||
if shortestpath:
|
|
||||||
d_old = torch.clone(d)
|
|
||||||
d = torch.where(d_old < 0, -d, d)
|
|
||||||
q1 = torch.where(d_old < 0, -q1, q1)
|
|
||||||
|
|
||||||
angle = torch.acos(d) + spin * torch.pi
|
|
||||||
angle_mask = (torch.abs(angle) < _EPS).squeeze()
|
|
||||||
out[angle_mask] = q0[angle_mask]
|
|
||||||
|
|
||||||
final_mask = torch.logical_or(zero_mask, ones_mask)
|
|
||||||
final_mask = torch.logical_or(final_mask, dist_mask)
|
|
||||||
final_mask = torch.logical_or(final_mask, angle_mask)
|
|
||||||
final_mask = torch.logical_not(final_mask)
|
|
||||||
|
|
||||||
isin = 1.0 / angle
|
|
||||||
q0 *= torch.sin((1.0 - fraction) * angle) * isin
|
|
||||||
q1 *= torch.sin(fraction * angle) * isin
|
|
||||||
q0 += q1
|
|
||||||
out[final_mask] = q0[final_mask]
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
class G1_AMPLoader:
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
device,
|
|
||||||
time_between_frames,
|
|
||||||
motion_files,
|
|
||||||
preload_transitions=False,
|
|
||||||
num_preload_transitions=1000000,
|
|
||||||
num_frames=5,
|
|
||||||
):
|
|
||||||
"""Expert dataset provides AMP observations from Dog mocap dataset.
|
|
||||||
|
|
||||||
time_between_frames: Amount of time in seconds between transition.
|
|
||||||
"""
|
|
||||||
self.device = device
|
|
||||||
self.time_between_frames = time_between_frames
|
|
||||||
self.num_frames = num_frames
|
|
||||||
|
|
||||||
# Values to store for each trajectory.
|
|
||||||
self.trajectories = []
|
|
||||||
self.trajectories_full = []
|
|
||||||
self.trajectory_names = []
|
|
||||||
self.trajectory_idxs = []
|
|
||||||
self.trajectory_lens = [] # Traj length in seconds.
|
|
||||||
self.trajectory_weights = []
|
|
||||||
self.trajectory_frame_durations = []
|
|
||||||
self.trajectory_num_frames = []
|
|
||||||
self.motion_dir = motion_files
|
|
||||||
# import ipdb; ipdb.set_trace()
|
|
||||||
for i, motion_file in enumerate(os.listdir(motion_files)):
|
|
||||||
self.trajectory_names.append(motion_file)
|
|
||||||
motion_path = pjoin(motion_files, motion_file)
|
|
||||||
motion_data = np.load(motion_path, allow_pickle=True)
|
|
||||||
motion_data_processed = np.zeros((motion_data.shape[0],36))
|
|
||||||
|
|
||||||
for f_i in range(motion_data.shape[0]):
|
|
||||||
motion_data_processed[f_i, :3] = motion_data[f_i, :3] # base pos
|
|
||||||
motion_data_processed[f_i, 3:7] = motion_data[f_i, 3:7] # base quat (wxyz)
|
|
||||||
motion_data_processed[f_i, 7:35] = motion_data[f_i, 7:35] # base vel
|
|
||||||
'''
|
|
||||||
NOTE The order of motion_data_processed is
|
|
||||||
base pos 0:3,
|
|
||||||
base quat 3:7, wxyz
|
|
||||||
dof pos 7:36, (mujoco joint order)
|
|
||||||
'''
|
|
||||||
self.trajectories.append(torch.tensor(
|
|
||||||
motion_data_processed[:, 7:],
|
|
||||||
dtype=torch.float32,
|
|
||||||
device=self.device
|
|
||||||
))
|
|
||||||
|
|
||||||
self.trajectories_full.append(torch.tensor(
|
|
||||||
motion_data_processed,
|
|
||||||
dtype=torch.float32,
|
|
||||||
device=self.device
|
|
||||||
))
|
|
||||||
|
|
||||||
self.trajectory_idxs.append(i)
|
|
||||||
self.trajectory_weights.append(1 / len(os.listdir(motion_files)))
|
|
||||||
frame_duration = 1 / 50
|
|
||||||
|
|
||||||
self.trajectory_frame_durations.append(frame_duration)
|
|
||||||
traj_len = (motion_data_processed.shape[0] - 1) * frame_duration # seconds
|
|
||||||
self.trajectory_lens.append(traj_len)
|
|
||||||
self.trajectory_num_frames.append(float(motion_data_processed.shape[0]))
|
|
||||||
print(f"Loaded {traj_len}s. motion from {motion_file}.")
|
|
||||||
|
|
||||||
# Trajectory weights are used to sample some trajectories more than others.
|
|
||||||
self.trajectory_weights = np.array(self.trajectory_weights) / np.sum(self.trajectory_weights)
|
|
||||||
self.trajectory_frame_durations = np.array(self.trajectory_frame_durations)
|
|
||||||
self.trajectory_lens = np.array(self.trajectory_lens)
|
|
||||||
self.trajectory_num_frames = np.array(self.trajectory_num_frames)
|
|
||||||
|
|
||||||
# Preload transitions.
|
|
||||||
self.preload_transitions = preload_transitions
|
|
||||||
if self.preload_transitions:
|
|
||||||
print(f'Preloading {num_preload_transitions} transitions')
|
|
||||||
|
|
||||||
traj_idxs = self.weighted_traj_idx_sample_batch(num_preload_transitions)
|
|
||||||
times = self.traj_time_sample_batch(traj_idxs)
|
|
||||||
self.preloaded_s_prior = self.get_full_frame_at_time_batch(traj_idxs, times - self.time_between_frames)
|
|
||||||
self.preloaded_s = self.get_full_frame_at_time_batch(traj_idxs, times)
|
|
||||||
self.preloaded_s_next = self.get_full_frame_at_time_batch(traj_idxs, times + self.time_between_frames)
|
|
||||||
print(f'Finished preloading')
|
|
||||||
|
|
||||||
# 预加载多帧数据
|
|
||||||
self.preloaded_frames = []
|
|
||||||
for i in range(self.num_frames):
|
|
||||||
frame_time = times + (i - (self.num_frames - 2)) * self.time_between_frames
|
|
||||||
full_frame = self.get_full_frame_at_time_batch(traj_idxs, frame_time)
|
|
||||||
# 预处理:提前提取并连接需要的列(7:26 和 29:33),避免每次生成时重复切片
|
|
||||||
processed_frame = torch.cat([
|
|
||||||
full_frame[:, 7:26],
|
|
||||||
full_frame[:, 29:33]
|
|
||||||
], dim=-1)
|
|
||||||
self.preloaded_frames.append(processed_frame)
|
|
||||||
print(f'Finished preloading multiple frames')
|
|
||||||
|
|
||||||
self.all_trajectories_full = torch.vstack(self.trajectories_full)
|
|
||||||
|
|
||||||
def weighted_traj_idx_sample(self):
|
|
||||||
"""Get traj idx via weighted sampling."""
|
|
||||||
return np.random.choice(
|
|
||||||
self.trajectory_idxs, p=self.trajectory_weights)
|
|
||||||
|
|
||||||
def weighted_traj_idx_sample_batch(self, size):
|
|
||||||
"""Batch sample traj idxs."""
|
|
||||||
return np.random.choice(
|
|
||||||
self.trajectory_idxs, size=size, p=self.trajectory_weights,
|
|
||||||
replace=True)
|
|
||||||
|
|
||||||
def traj_time_sample(self, traj_idx):
|
|
||||||
"""Sample random time for traj."""
|
|
||||||
subst = self.time_between_frames + self.trajectory_frame_durations[traj_idx]
|
|
||||||
return max(
|
|
||||||
0, (self.trajectory_lens[traj_idx] * np.random.uniform() - subst))
|
|
||||||
|
|
||||||
def traj_time_sample_batch(self, traj_idxs):
|
|
||||||
"""Sample random time for multiple trajectories."""
|
|
||||||
subst = self.time_between_frames + self.trajectory_frame_durations[traj_idxs]
|
|
||||||
time_samples = self.trajectory_lens[traj_idxs] * np.random.uniform(size=len(traj_idxs)) - subst
|
|
||||||
return np.maximum(np.zeros_like(time_samples), time_samples)
|
|
||||||
|
|
||||||
def slerp(self, val0, val1, blend):
|
|
||||||
return (1.0 - blend) * val0 + blend * val1
|
|
||||||
|
|
||||||
def get_trajectory(self, traj_idx):
|
|
||||||
"""Returns trajectory of AMP observations."""
|
|
||||||
return self.trajectories_full[traj_idx]
|
|
||||||
|
|
||||||
def get_frame_at_time(self, traj_idx, time):
|
|
||||||
"""Returns frame for the given trajectory at the specified time."""
|
|
||||||
p = float(time) / self.trajectory_lens[traj_idx]
|
|
||||||
n = self.trajectories[traj_idx].shape[0]
|
|
||||||
idx_low, idx_high = int(np.floor(p * n)), int(np.ceil(p * n))
|
|
||||||
frame_start = self.trajectories[traj_idx][idx_low]
|
|
||||||
frame_end = self.trajectories[traj_idx][idx_high]
|
|
||||||
blend = p * n - idx_low
|
|
||||||
return self.slerp(frame_start, frame_end, blend)
|
|
||||||
|
|
||||||
def get_frame_at_time_batch(self, traj_idxs, times):
|
|
||||||
"""Returns frame for the given trajectory at the specified time."""
|
|
||||||
p = times / self.trajectory_lens[traj_idxs]
|
|
||||||
n = self.trajectory_num_frames[traj_idxs]
|
|
||||||
idx_low, idx_high = np.floor(p * n).astype(np.int32), np.ceil(p * n).astype(np.int32)
|
|
||||||
all_frame_starts = torch.zeros(len(traj_idxs), self.observation_dim, device=self.device)
|
|
||||||
all_frame_ends = torch.zeros(len(traj_idxs), self.observation_dim, device=self.device)
|
|
||||||
for traj_idx in set(traj_idxs):
|
|
||||||
trajectory = self.trajectories[traj_idx]
|
|
||||||
traj_mask = traj_idxs == traj_idx
|
|
||||||
all_frame_starts[traj_mask] = trajectory[idx_low[traj_mask]]
|
|
||||||
all_frame_ends[traj_mask] = trajectory[idx_high[traj_mask]]
|
|
||||||
blend = torch.tensor(p * n - idx_low, device=self.device, dtype=torch.float32).unsqueeze(-1)
|
|
||||||
return self.slerp(all_frame_starts, all_frame_ends, blend)
|
|
||||||
|
|
||||||
def get_full_frame_at_time(self, traj_idx, time):
|
|
||||||
"""Returns full frame for the given trajectory at the specified time."""
|
|
||||||
p = float(time) / self.trajectory_lens[traj_idx]
|
|
||||||
n = self.trajectories_full[traj_idx].shape[0]
|
|
||||||
idx_low, idx_high = int(np.floor(p * n)), int(np.ceil(p * n))
|
|
||||||
frame_start = self.trajectories_full[traj_idx][idx_low]
|
|
||||||
frame_end = self.trajectories_full[traj_idx][idx_high]
|
|
||||||
blend = p * n - idx_low
|
|
||||||
print(idx_low, idx_high)
|
|
||||||
return self.blend_frame_pose(frame_start, frame_end, blend)
|
|
||||||
|
|
||||||
def get_full_frame_at_time_batch(self, traj_idxs, times):
|
|
||||||
p = times / self.trajectory_lens[traj_idxs]
|
|
||||||
n = self.trajectory_num_frames[traj_idxs]
|
|
||||||
idx_low, idx_high = np.floor(p * n).astype(np.int32), np.ceil(p * n).astype(np.int32)
|
|
||||||
all_frame_pos_starts = torch.zeros(len(traj_idxs), 3, device=self.device)
|
|
||||||
all_frame_pos_ends = torch.zeros(len(traj_idxs), 3, device=self.device)
|
|
||||||
all_frame_rot_starts = torch.zeros(len(traj_idxs), 4, device=self.device)
|
|
||||||
all_frame_rot_ends = torch.zeros(len(traj_idxs), 4, device=self.device)
|
|
||||||
all_frame_amp_starts = torch.zeros(len(traj_idxs), 29, device=self.device)
|
|
||||||
all_frame_amp_ends = torch.zeros(len(traj_idxs), 29, device=self.device)
|
|
||||||
for traj_idx in set(traj_idxs):
|
|
||||||
trajectory = self.trajectories_full[traj_idx]
|
|
||||||
traj_mask = traj_idxs == traj_idx
|
|
||||||
all_frame_pos_starts[traj_mask] = G1_AMPLoader.get_root_pos_batch(trajectory[idx_low[traj_mask]])
|
|
||||||
all_frame_pos_ends[traj_mask] = G1_AMPLoader.get_root_pos_batch(trajectory[idx_high[traj_mask]])
|
|
||||||
all_frame_rot_starts[traj_mask] = G1_AMPLoader.get_root_rot_batch(trajectory[idx_low[traj_mask]])
|
|
||||||
all_frame_rot_ends[traj_mask] = G1_AMPLoader.get_root_rot_batch(trajectory[idx_high[traj_mask]])
|
|
||||||
all_frame_amp_starts[traj_mask] = trajectory[idx_low[traj_mask]][:, 7:36] # base vel3+ang3, dof vel23+ang23
|
|
||||||
all_frame_amp_ends[traj_mask] = trajectory[idx_high[traj_mask]][:, 7:36] # base vel3+ang3, dof vel23+ang23
|
|
||||||
blend = torch.tensor(p * n - idx_low, device=self.device, dtype=torch.float32).unsqueeze(-1)
|
|
||||||
pos_blend = self.slerp(all_frame_pos_starts, all_frame_pos_ends, blend)
|
|
||||||
rot_blend = quaternion_slerp(all_frame_rot_starts, all_frame_rot_ends, blend)
|
|
||||||
amp_blend = self.slerp(all_frame_amp_starts, all_frame_amp_ends, blend)
|
|
||||||
return torch.cat([pos_blend, rot_blend, amp_blend], dim=-1)
|
|
||||||
|
|
||||||
def get_frame(self):
|
|
||||||
"""Returns random frame."""
|
|
||||||
traj_idx = self.weighted_traj_idx_sample()
|
|
||||||
sampled_time = self.traj_time_sample(traj_idx)
|
|
||||||
return self.get_frame_at_time(traj_idx, sampled_time)
|
|
||||||
|
|
||||||
def get_full_frame(self):
|
|
||||||
"""Returns random full frame."""
|
|
||||||
traj_idx = self.weighted_traj_idx_sample()
|
|
||||||
sampled_time = self.traj_time_sample(traj_idx)
|
|
||||||
return self.get_full_frame_at_time(traj_idx, sampled_time)
|
|
||||||
|
|
||||||
def get_full_frame_batch(self, num_frames):
|
|
||||||
if self.preload_transitions:
|
|
||||||
idxs = np.random.choice(
|
|
||||||
self.preloaded_s.shape[0], size=num_frames)
|
|
||||||
return self.preloaded_s[idxs]
|
|
||||||
else:
|
|
||||||
traj_idxs = self.weighted_traj_idx_sample_batch(num_frames)
|
|
||||||
times = self.traj_time_sample_batch(traj_idxs)
|
|
||||||
return self.get_full_frame_at_time_batch(traj_idxs, times)
|
|
||||||
|
|
||||||
def blend_frame_pose(self, frame0, frame1, blend):
|
|
||||||
"""Linearly interpolate between two frames, including orientation.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
frame0: First frame to be blended corresponds to (blend = 0).
|
|
||||||
frame1: Second frame to be blended corresponds to (blend = 1).
|
|
||||||
blend: Float between [0, 1], specifying the interpolation between
|
|
||||||
the two frames.
|
|
||||||
Returns:
|
|
||||||
An interpolation of the two frames.
|
|
||||||
"""
|
|
||||||
root_pos0, root_pos1 = G1_AMPLoader.get_root_pos(frame0), G1_AMPLoader.get_root_pos(frame1)
|
|
||||||
root_rot0, root_rot1 = G1_AMPLoader.get_root_rot(frame0), G1_AMPLoader.get_root_rot(frame1)
|
|
||||||
joints0, joints1 = G1_AMPLoader.get_joint_pose(frame0), G1_AMPLoader.get_joint_pose(frame1)
|
|
||||||
# tar_toe_pos_0, tar_toe_pos_1 = G1_AMPLoader.get_tar_toe_pos_local(frame0), G1_AMPLoader.get_tar_toe_pos_local(frame1)
|
|
||||||
linear_vel_0, linear_vel_1 = G1_AMPLoader.get_linear_vel(frame0), G1_AMPLoader.get_linear_vel(frame1)
|
|
||||||
angular_vel_0, angular_vel_1 = G1_AMPLoader.get_angular_vel(frame0), G1_AMPLoader.get_angular_vel(frame1)
|
|
||||||
joint_vel_0, joint_vel_1 = G1_AMPLoader.get_joint_vel(frame0), G1_AMPLoader.get_joint_vel(frame1)
|
|
||||||
|
|
||||||
blend_root_pos = self.slerp(root_pos0, root_pos1, blend)
|
|
||||||
blend_root_rot = transformations.quaternion_slerp(root_rot0.cpu().numpy(), root_rot1.cpu().numpy(), blend)
|
|
||||||
blend_root_rot = torch.tensor(motion_util.standardize_quaternion(blend_root_rot),dtype=torch.float32, device=self.device)
|
|
||||||
blend_joints = self.slerp(joints0, joints1, blend)
|
|
||||||
# blend_tar_toe_pos = self.slerp(tar_toe_pos_0, tar_toe_pos_1, blend)
|
|
||||||
blend_linear_vel = self.slerp(linear_vel_0, linear_vel_1, blend)
|
|
||||||
blend_angular_vel = self.slerp(angular_vel_0, angular_vel_1, blend)
|
|
||||||
blend_joints_vel = self.slerp(joint_vel_0, joint_vel_1, blend)
|
|
||||||
|
|
||||||
# return
|
|
||||||
# torch.cat([
|
|
||||||
# blend_root_pos, blend_root_rot, blend_linear_vel, blend_angular_vel, blend_joints, blend_joints_vel])
|
|
||||||
return torch.cat([blend_root_pos, blend_root_rot, blend_linear_vel, blend_angular_vel, blend_joints])
|
|
||||||
|
|
||||||
def feed_forward_generator_23dof_multi(self, num_mini_batch, mini_batch_size):
|
|
||||||
"""Generates a batch of AMP transitions."""
|
|
||||||
# import ipdb; ipdb.set_trace()
|
|
||||||
for _ in range(num_mini_batch):
|
|
||||||
if self.preload_transitions:
|
|
||||||
idxs = np.random.choice(self.preloaded_s.shape[0], size=mini_batch_size)
|
|
||||||
|
|
||||||
frames = []
|
|
||||||
for i in range(self.num_frames):
|
|
||||||
# 数据已在预加载时预处理,直接索引即可
|
|
||||||
s = self.preloaded_frames[i][idxs]
|
|
||||||
frames.append(s)
|
|
||||||
else:
|
|
||||||
NotImplementedError('preload transition')
|
|
||||||
yield torch.stack(frames, dim=1) # [batch, num_frames, 16]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def quaternion_to_euler_array(self, quat):
|
|
||||||
# Ensure quaternion is in the correct format [x, y, z, w]
|
|
||||||
x, y, z, w =quat
|
|
||||||
|
|
||||||
# Roll (x-axis rotation)
|
|
||||||
t0 = +2.0 * (w * x + y * z)
|
|
||||||
t1 = +1.0 - 2.0 * (x * x + y * y)
|
|
||||||
roll_x = np.arctan2(t0, t1)
|
|
||||||
|
|
||||||
# Pitch (y-axis rotation)
|
|
||||||
t2 = +2.0 * (w * y - z * x)
|
|
||||||
t2 = np.clip(t2, -1.0, 1.0)
|
|
||||||
pitch_y = np.arcsin(t2)
|
|
||||||
|
|
||||||
# Yaw (z-axis rotation)
|
|
||||||
t3 = +2.0 * (w * z + x * y)
|
|
||||||
t4 = +1.0 - 2.0 * (y * y + z * z)
|
|
||||||
yaw_z = np.arctan2(t3, t4)
|
|
||||||
|
|
||||||
# Returns roll, pitch, yaw in a NumPy array in radians
|
|
||||||
return np.array([roll_x, pitch_y, yaw_z])
|
|
||||||
|
|
||||||
def euler_to_quaternion(self, root_rot):
|
|
||||||
roll, pitch, yaw = root_rot[0], root_rot[1], root_rot[2]
|
|
||||||
cy = np.cos(yaw * 0.5)
|
|
||||||
sy = np.sin(yaw * 0.5)
|
|
||||||
cp = np.cos(pitch * 0.5)
|
|
||||||
sp = np.sin(pitch * 0.5)
|
|
||||||
cr = np.cos(roll * 0.5)
|
|
||||||
sr = np.sin(roll * 0.5)
|
|
||||||
|
|
||||||
qw = cy * cp * cr + sy * sp * sr
|
|
||||||
qx = cy * cp * sr - sy * sp * cr
|
|
||||||
qy = sy * cp * sr + cy * sp * cr
|
|
||||||
qz = sy * cp * cr - cy * sp * sr
|
|
||||||
|
|
||||||
return np.array([qx, qy, qz, qw])
|
|
||||||
|
|
||||||
@property
|
|
||||||
def observation_dim(self):
|
|
||||||
"""Size of AMP observations."""
|
|
||||||
return self.trajectories[0].shape[1] + 1
|
|
||||||
|
|
||||||
@property
|
|
||||||
def num_motions(self):
|
|
||||||
return len(self.trajectory_names)
|
|
||||||
@staticmethod
|
|
||||||
def get_root_pos(pose):
|
|
||||||
return pose[0:3]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_root_pos_batch(poses):
|
|
||||||
return poses[:, 0:3]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_root_rot(pose):
|
|
||||||
return pose[3:7]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_root_rot_batch(poses):
|
|
||||||
return poses[:, 3:7]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_joint_pose_batch_12dof(poses):
|
|
||||||
return poses[:, 13:25]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_tar_toe_pos_local(pose):
|
|
||||||
return pose[G1_AMPLoader.TAR_TOE_POS_LOCAL_START_IDX:G1_AMPLoader.TAR_TOE_POS_LOCAL_END_IDX]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_tar_toe_pos_local_batch(poses):
|
|
||||||
return poses[:, G1_AMPLoader.TAR_TOE_POS_LOCAL_START_IDX:G1_AMPLoader.TAR_TOE_POS_LOCAL_END_IDX]
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
# coding=utf-8
|
|
||||||
# Copyright 2020 The Google Research Authors.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
"""Utility functions for processing motion clips."""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import inspect
|
|
||||||
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
|
|
||||||
parentdir = os.path.dirname(os.path.dirname(currentdir))
|
|
||||||
os.sys.path.insert(0, parentdir)
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from rsl_rl.utils import pose3d
|
|
||||||
# from pybullet_utils import transformations
|
|
||||||
|
|
||||||
|
|
||||||
def standardize_quaternion(q):
|
|
||||||
"""Returns a quaternion where q.w >= 0 to remove redundancy due to q = -q.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
q: A quaternion to be standardized.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A quaternion with q.w >= 0.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if q[-1] < 0:
|
|
||||||
q = -q
|
|
||||||
return q
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_rotation_angle(theta):
|
|
||||||
"""Returns a rotation angle normalized between [-pi, pi].
|
|
||||||
|
|
||||||
Args:
|
|
||||||
theta: angle of rotation (radians).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
An angle of rotation normalized between [-pi, pi].
|
|
||||||
|
|
||||||
"""
|
|
||||||
norm_theta = theta
|
|
||||||
if np.abs(norm_theta) > np.pi:
|
|
||||||
norm_theta = np.fmod(norm_theta, 2 * np.pi)
|
|
||||||
if norm_theta >= 0:
|
|
||||||
norm_theta += -2 * np.pi
|
|
||||||
else:
|
|
||||||
norm_theta += 2 * np.pi
|
|
||||||
|
|
||||||
return norm_theta
|
|
||||||
|
|
||||||
|
|
||||||
def calc_heading(q):
|
|
||||||
"""Returns the heading of a rotation q, specified as a quaternion.
|
|
||||||
|
|
||||||
The heading represents the rotational component of q along the vertical
|
|
||||||
axis (z axis).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
q: A quaternion that the heading is to be computed from.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
An angle representing the rotation about the z axis.
|
|
||||||
|
|
||||||
"""
|
|
||||||
ref_dir = np.array([1, 0, 0])
|
|
||||||
rot_dir = pose3d.QuaternionRotatePoint(ref_dir, q)
|
|
||||||
heading = np.arctan2(rot_dir[1], rot_dir[0])
|
|
||||||
return heading
|
|
||||||
|
|
||||||
|
|
||||||
# def calc_heading_rot(q):
|
|
||||||
# """Return a quaternion representing the heading rotation of q along the vertical axis (z axis).
|
|
||||||
|
|
||||||
# Args:
|
|
||||||
# q: A quaternion that the heading is to be computed from.
|
|
||||||
|
|
||||||
# Returns:
|
|
||||||
# A quaternion representing the rotation about the z axis.
|
|
||||||
|
|
||||||
# """
|
|
||||||
# heading = calc_heading(q)
|
|
||||||
# q_heading = transformations.quaternion_about_axis(heading, [0, 0, 1])
|
|
||||||
# return q_heading
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
from dataclasses import asdict
|
|
||||||
from torch.utils.tensorboard import SummaryWriter
|
|
||||||
|
|
||||||
try:
|
|
||||||
import neptune
|
|
||||||
except ModuleNotFoundError:
|
|
||||||
raise ModuleNotFoundError("neptune-client is required to log to Neptune.")
|
|
||||||
|
|
||||||
|
|
||||||
class NeptuneLogger:
|
|
||||||
def __init__(self, project, token):
|
|
||||||
self.run = neptune.init_run(project=project, api_token=token)
|
|
||||||
|
|
||||||
def store_config(self, env_cfg, runner_cfg, alg_cfg, policy_cfg):
|
|
||||||
self.run["runner_cfg"] = runner_cfg
|
|
||||||
self.run["policy_cfg"] = policy_cfg
|
|
||||||
self.run["alg_cfg"] = alg_cfg
|
|
||||||
self.run["env_cfg"] = asdict(env_cfg)
|
|
||||||
|
|
||||||
|
|
||||||
class NeptuneSummaryWriter(SummaryWriter):
|
|
||||||
"""Summary writer for Neptune."""
|
|
||||||
|
|
||||||
def __init__(self, log_dir: str, flush_secs: int, cfg):
|
|
||||||
super().__init__(log_dir, flush_secs)
|
|
||||||
|
|
||||||
try:
|
|
||||||
project = cfg["neptune_project"]
|
|
||||||
except KeyError:
|
|
||||||
raise KeyError("Please specify neptune_project in the runner config, e.g. legged_gym.")
|
|
||||||
|
|
||||||
try:
|
|
||||||
token = os.environ["NEPTUNE_API_TOKEN"]
|
|
||||||
except KeyError:
|
|
||||||
raise KeyError(
|
|
||||||
"Neptune api token not found. Please run or add to ~/.bashrc: export NEPTUNE_API_TOKEN=YOUR_API_TOKEN"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
entity = os.environ["NEPTUNE_USERNAME"]
|
|
||||||
except KeyError:
|
|
||||||
raise KeyError(
|
|
||||||
"Neptune username not found. Please run or add to ~/.bashrc: export NEPTUNE_USERNAME=YOUR_USERNAME"
|
|
||||||
)
|
|
||||||
|
|
||||||
neptune_project = entity + "/" + project
|
|
||||||
|
|
||||||
self.neptune_logger = NeptuneLogger(neptune_project, token)
|
|
||||||
|
|
||||||
self.name_map = {
|
|
||||||
"Train/mean_reward/time": "Train/mean_reward_time",
|
|
||||||
"Train/mean_episode_length/time": "Train/mean_episode_length_time",
|
|
||||||
}
|
|
||||||
|
|
||||||
run_name = os.path.split(log_dir)[-1]
|
|
||||||
|
|
||||||
self.neptune_logger.run["log_dir"].log(run_name)
|
|
||||||
|
|
||||||
def _map_path(self, path):
|
|
||||||
if path in self.name_map:
|
|
||||||
return self.name_map[path]
|
|
||||||
else:
|
|
||||||
return path
|
|
||||||
|
|
||||||
def add_scalar(self, tag, scalar_value, global_step=None, walltime=None, new_style=False):
|
|
||||||
super().add_scalar(
|
|
||||||
tag,
|
|
||||||
scalar_value,
|
|
||||||
global_step=global_step,
|
|
||||||
walltime=walltime,
|
|
||||||
new_style=new_style,
|
|
||||||
)
|
|
||||||
self.neptune_logger.run[self._map_path(tag)].log(scalar_value, step=global_step)
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self.neptune_logger.run.stop()
|
|
||||||
|
|
||||||
def log_config(self, env_cfg, runner_cfg, alg_cfg, policy_cfg):
|
|
||||||
self.neptune_logger.store_config(env_cfg, runner_cfg, alg_cfg, policy_cfg)
|
|
||||||
|
|
||||||
def save_model(self, model_path, iter):
|
|
||||||
self.neptune_logger.run["model/saved_model_" + str(iter)].upload(model_path)
|
|
||||||
|
|
||||||
def save_file(self, path, iter=None):
|
|
||||||
name = path.rsplit("/", 1)[-1].split(".")[0]
|
|
||||||
self.neptune_logger.run["git_diff/" + name].upload(path)
|
|
||||||
|
|
@ -1,283 +0,0 @@
|
||||||
# coding=utf-8
|
|
||||||
# Copyright 2020 The Google Research Authors.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
"""Utilities for 3D pose conversion."""
|
|
||||||
import math
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
# from pybullet_utils import transformations
|
|
||||||
|
|
||||||
VECTOR3_0 = np.zeros(3, dtype=np.float64)
|
|
||||||
VECTOR3_1 = np.ones(3, dtype=np.float64)
|
|
||||||
VECTOR3_X = np.array([1, 0, 0], dtype=np.float64)
|
|
||||||
VECTOR3_Y = np.array([0, 1, 0], dtype=np.float64)
|
|
||||||
VECTOR3_Z = np.array([0, 0, 1], dtype=np.float64)
|
|
||||||
|
|
||||||
# QUATERNION_IDENTITY is the multiplicative identity 1.0 + 0i + 0j + 0k.
|
|
||||||
# When interpreted as a rotation, it is the identity rotation.
|
|
||||||
QUATERNION_IDENTITY = np.array([0.0, 0.0, 0.0, 1.0], dtype=np.float64)
|
|
||||||
|
|
||||||
|
|
||||||
def Vector3RandomNormal(sigma, mu=VECTOR3_0):
|
|
||||||
"""Returns a random 3D vector from a normal distribution.
|
|
||||||
|
|
||||||
Each component is selected independently from a normal distribution.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
sigma: Scale (or stddev) of distribution for all variables.
|
|
||||||
mu: Mean of distribution for each variable.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A 3D vector in a numpy array.
|
|
||||||
"""
|
|
||||||
|
|
||||||
random_v3 = np.random.normal(scale=sigma, size=3) + mu
|
|
||||||
return random_v3
|
|
||||||
|
|
||||||
|
|
||||||
def Vector3RandomUniform(low=VECTOR3_0, high=VECTOR3_1):
|
|
||||||
"""Returns a 3D vector selected uniformly from the input box.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
low: The min-value corner of the box.
|
|
||||||
high: The max-value corner of the box.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A 3D vector in a numpy array.
|
|
||||||
"""
|
|
||||||
|
|
||||||
random_x = np.random.uniform(low=low[0], high=high[0])
|
|
||||||
random_y = np.random.uniform(low=low[1], high=high[1])
|
|
||||||
random_z = np.random.uniform(low=low[2], high=high[2])
|
|
||||||
return np.array([random_x, random_y, random_z])
|
|
||||||
|
|
||||||
|
|
||||||
def Vector3RandomUnit():
|
|
||||||
"""Returns a random 3D vector with unit length.
|
|
||||||
|
|
||||||
Generates a 3D vector selected uniformly from the unit sphere.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A normalized 3D vector in a numpy array.
|
|
||||||
"""
|
|
||||||
longitude = np.random.uniform(low=-math.pi, high=math.pi)
|
|
||||||
sin_latitude = np.random.uniform(low=-1.0, high=1.0)
|
|
||||||
cos_latitude = math.sqrt(1.0 - sin_latitude * sin_latitude)
|
|
||||||
x = math.cos(longitude) * cos_latitude
|
|
||||||
y = math.sin(longitude) * cos_latitude
|
|
||||||
z = sin_latitude
|
|
||||||
return np.array([x, y, z], dtype=np.float64)
|
|
||||||
|
|
||||||
|
|
||||||
def QuaternionNormalize(q):
|
|
||||||
"""Normalizes the quaternion to length 1.
|
|
||||||
|
|
||||||
Divides the quaternion by its magnitude. If the magnitude is too
|
|
||||||
small, returns the quaternion identity value (1.0).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
q: A quaternion to be normalized.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If input quaternion has length near zero.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A quaternion with magnitude 1 in a numpy array [x, y, z, w].
|
|
||||||
|
|
||||||
"""
|
|
||||||
q_norm = np.linalg.norm(q)
|
|
||||||
if np.isclose(q_norm, 0.0):
|
|
||||||
raise ValueError(
|
|
||||||
'Quaternion may not be zero in QuaternionNormalize: |q| = %f, q = %s' %
|
|
||||||
(q_norm, q))
|
|
||||||
return q / q_norm
|
|
||||||
|
|
||||||
|
|
||||||
def QuaternionFromAxisAngle(axis, angle):
|
|
||||||
"""Returns a quaternion that generates the given axis-angle rotation.
|
|
||||||
|
|
||||||
Returns the quaternion: sin(angle/2) * axis + cos(angle/2).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
axis: Axis of rotation, a 3D vector in a numpy array.
|
|
||||||
angle: The angle of rotation (radians).
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If input axis is not a normalizable 3D vector.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A unit quaternion in a numpy array.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if len(axis) != 3:
|
|
||||||
raise ValueError('Axis vector should have three components: %s' % axis)
|
|
||||||
axis_norm = np.linalg.norm(axis)
|
|
||||||
if np.isclose(axis_norm, 0.0):
|
|
||||||
raise ValueError('Axis vector may not have zero length: |v| = %f, v = %s' %
|
|
||||||
(axis_norm, axis))
|
|
||||||
half_angle = angle * 0.5
|
|
||||||
q = np.zeros(4, dtype=np.float64)
|
|
||||||
q[0:3] = axis
|
|
||||||
q[0:3] *= math.sin(half_angle) / axis_norm
|
|
||||||
q[3] = math.cos(half_angle)
|
|
||||||
return q
|
|
||||||
|
|
||||||
|
|
||||||
def QuaternionToAxisAngle(quat, default_axis=VECTOR3_Z, direction_axis=None):
|
|
||||||
"""Calculates axis and angle of rotation performed by a quaternion.
|
|
||||||
|
|
||||||
Calculates the axis and angle of the rotation performed by the quaternion.
|
|
||||||
The quaternion should have four values and be normalized.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
quat: Unit quaternion in a numpy array.
|
|
||||||
default_axis: 3D vector axis used if the rotation is near to zero. Without
|
|
||||||
this default, small rotations would result in an exception. It is
|
|
||||||
reasonable to use a default axis for tiny rotations, because zero angle
|
|
||||||
rotations about any axis are equivalent.
|
|
||||||
direction_axis: Used to disambiguate rotation directions. If the
|
|
||||||
direction_axis is specified, the axis of the rotation will be chosen such
|
|
||||||
that its inner product with the direction_axis is non-negative.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If quat is not a normalized quaternion.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
axis: Axis of rotation.
|
|
||||||
angle: Angle in radians.
|
|
||||||
"""
|
|
||||||
if len(quat) != 4:
|
|
||||||
raise ValueError(
|
|
||||||
'Quaternion should have four components [x, y, z, w]: %s' % quat)
|
|
||||||
if not np.isclose(1.0, np.linalg.norm(quat)):
|
|
||||||
raise ValueError('Quaternion should have unit length: |q| = %f, q = %s' %
|
|
||||||
(np.linalg.norm(quat), quat))
|
|
||||||
axis = quat[:3].copy()
|
|
||||||
axis_norm = np.linalg.norm(axis)
|
|
||||||
min_axis_norm = 1e-8
|
|
||||||
if axis_norm < min_axis_norm:
|
|
||||||
axis = default_axis
|
|
||||||
if len(default_axis) != 3:
|
|
||||||
raise ValueError('Axis vector should have three components: %s' % axis)
|
|
||||||
if not np.isclose(np.linalg.norm(axis), 1.0):
|
|
||||||
raise ValueError('Axis vector should have unit length: |v| = %f, v = %s' %
|
|
||||||
(np.linalg.norm(axis), axis))
|
|
||||||
else:
|
|
||||||
axis /= axis_norm
|
|
||||||
sin_half_angle = axis_norm
|
|
||||||
if direction_axis is not None and np.inner(axis, direction_axis) < 0:
|
|
||||||
sin_half_angle = -sin_half_angle
|
|
||||||
axis = -axis
|
|
||||||
cos_half_angle = quat[3]
|
|
||||||
half_angle = math.atan2(sin_half_angle, cos_half_angle)
|
|
||||||
angle = half_angle * 2
|
|
||||||
return axis, angle
|
|
||||||
|
|
||||||
|
|
||||||
def QuaternionRandomRotation(max_angle=math.pi):
|
|
||||||
"""Creates a random small rotation around a random axis.
|
|
||||||
|
|
||||||
Generates a small rotation with the axis vector selected uniformly
|
|
||||||
from the unit sphere and an angle selected from a uniform
|
|
||||||
distribution over [0, max_angle].
|
|
||||||
|
|
||||||
If the max_angle is not specified, the rotation should be selected
|
|
||||||
uniformly over all possible rotation angles.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
max_angle: The maximum angle of rotation (radians).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A unit quaternion in a numpy array.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
angle = np.random.uniform(low=0, high=max_angle)
|
|
||||||
axis = Vector3RandomUnit()
|
|
||||||
return QuaternionFromAxisAngle(axis, angle)
|
|
||||||
|
|
||||||
|
|
||||||
# def QuaternionRotatePoint(point, quat):
|
|
||||||
# """Performs a rotation by quaternion.
|
|
||||||
|
|
||||||
# Rotate the point by the quaternion using quaternion multiplication,
|
|
||||||
# (q * p * q^-1), without constructing the rotation matrix.
|
|
||||||
|
|
||||||
# Args:
|
|
||||||
# point: The point to be rotated.
|
|
||||||
# quat: The rotation represented as a quaternion [x, y, z, w].
|
|
||||||
|
|
||||||
# Returns:
|
|
||||||
# A 3D vector in a numpy array.
|
|
||||||
# """
|
|
||||||
|
|
||||||
# q_point = np.array([point[0], point[1], point[2], 0.0])
|
|
||||||
# quat_inverse = transformations.quaternion_inverse(quat)
|
|
||||||
# q_point_rotated = transformations.quaternion_multiply(
|
|
||||||
# transformations.quaternion_multiply(quat, q_point), quat_inverse)
|
|
||||||
# return q_point_rotated[:3]
|
|
||||||
|
|
||||||
|
|
||||||
def IsRotationMatrix(m):
|
|
||||||
"""Returns true if the 3x3 submatrix represents a rotation.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
m: A transformation matrix.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If input is not a matrix of size at least 3x3.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the 3x3 submatrix is a rotation (orthogonal).
|
|
||||||
"""
|
|
||||||
if len(m.shape) != 2 or m.shape[0] < 3 or m.shape[1] < 3:
|
|
||||||
raise ValueError('Matrix should be 3x3 or 4x4: %s\n %s' % (m.shape, m))
|
|
||||||
rot = m[:3, :3]
|
|
||||||
eye = np.matmul(rot, np.transpose(rot))
|
|
||||||
return np.isclose(eye, np.identity(3), atol=1e-4).all()
|
|
||||||
|
|
||||||
# def ZAxisAlignedRobotPoseTool(robot_pose_tool):
|
|
||||||
# """Returns the current gripper pose rotated for alignment with the z-axis.
|
|
||||||
|
|
||||||
# Args:
|
|
||||||
# robot_pose_tool: a pose3d.Pose3d() instance.
|
|
||||||
|
|
||||||
# Returns:
|
|
||||||
# An instance of pose.Transform representing the current gripper pose
|
|
||||||
# rotated for alignment with the z-axis.
|
|
||||||
# """
|
|
||||||
# # Align the current pose to the z-axis.
|
|
||||||
# robot_pose_tool.quaternion = transformations.quaternion_multiply(
|
|
||||||
# RotationBetween(
|
|
||||||
# robot_pose_tool.matrix4x4[0:3, 0:3].dot(np.array([0, 0, 1])),
|
|
||||||
# np.array([0.0, 0.0, -1.0])), robot_pose_tool.quaternion)
|
|
||||||
# return robot_pose_tool
|
|
||||||
|
|
||||||
# def RotationBetween(a_translation_b, a_translation_c):
|
|
||||||
# """Computes the rotation from one vector to another.
|
|
||||||
|
|
||||||
# The computed rotation has the property that:
|
|
||||||
|
|
||||||
# a_translation_c = a_rotation_b_to_c * a_translation_b
|
|
||||||
|
|
||||||
# Args:
|
|
||||||
# a_translation_b: vec3, vector to rotate from
|
|
||||||
# a_translation_c: vec3, vector to rotate to
|
|
||||||
|
|
||||||
# Returns:
|
|
||||||
# a_rotation_b_to_c: new Orientation
|
|
||||||
# """
|
|
||||||
# rotation = rotation3.Rotation3.rotation_between(
|
|
||||||
# a_translation_b, a_translation_c, err_msg='RotationBetween')
|
|
||||||
# return rotation.quaternion.xyzw
|
|
||||||
|
|
@ -1,360 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import git
|
|
||||||
import importlib
|
|
||||||
import os
|
|
||||||
import pathlib
|
|
||||||
import torch
|
|
||||||
import warnings
|
|
||||||
from tensordict import TensorDict
|
|
||||||
from typing import Callable
|
|
||||||
import numpy as np
|
|
||||||
class RunningMeanStd:
|
|
||||||
def __init__(self, epsilon: float = 1e-4, shape: Tuple[int, ...] = ()):
|
|
||||||
"""
|
|
||||||
Calculates the running mean and std of a data stream
|
|
||||||
https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Parallel_algorithm
|
|
||||||
:param epsilon: helps with arithmetic issues
|
|
||||||
:param shape: the shape of the data stream's output
|
|
||||||
"""
|
|
||||||
self.mean = np.zeros(shape, np.float64)
|
|
||||||
self.var = np.ones(shape, np.float64)
|
|
||||||
self.count = epsilon
|
|
||||||
|
|
||||||
def update(self, arr: np.ndarray) -> None:
|
|
||||||
batch_mean = np.mean(arr, axis=0)
|
|
||||||
batch_var = np.var(arr, axis=0)
|
|
||||||
batch_count = arr.shape[0]
|
|
||||||
self.update_from_moments(batch_mean, batch_var, batch_count)
|
|
||||||
|
|
||||||
def update_from_moments(self, batch_mean: np.ndarray, batch_var: np.ndarray, batch_count: int) -> None:
|
|
||||||
delta = batch_mean - self.mean
|
|
||||||
tot_count = self.count + batch_count
|
|
||||||
|
|
||||||
new_mean = self.mean + delta * batch_count / tot_count
|
|
||||||
m_a = self.var * self.count
|
|
||||||
m_b = batch_var * batch_count
|
|
||||||
m_2 = m_a + m_b + np.square(delta) * self.count * batch_count / (self.count + batch_count)
|
|
||||||
new_var = m_2 / (self.count + batch_count)
|
|
||||||
|
|
||||||
new_count = batch_count + self.count
|
|
||||||
|
|
||||||
self.mean = new_mean
|
|
||||||
self.var = new_var
|
|
||||||
self.count = new_count
|
|
||||||
|
|
||||||
|
|
||||||
class Normalizer(RunningMeanStd):
|
|
||||||
def __init__(self, input_dim, epsilon=1e-4, clip_obs=10.0):
|
|
||||||
super().__init__(shape=input_dim)
|
|
||||||
self.epsilon = epsilon
|
|
||||||
self.clip_obs = clip_obs
|
|
||||||
|
|
||||||
def normalize(self, input):
|
|
||||||
return np.clip((input - self.mean) / np.sqrt(self.var + self.epsilon), -self.clip_obs, self.clip_obs)
|
|
||||||
|
|
||||||
def normalize_torch(self, input, device):
|
|
||||||
mean_torch = torch.tensor(self.mean, device=device, dtype=torch.float32)
|
|
||||||
std_torch = torch.sqrt(torch.tensor(self.var + self.epsilon, device=device, dtype=torch.float32))
|
|
||||||
return torch.clamp((input - mean_torch) / std_torch, -self.clip_obs, self.clip_obs)
|
|
||||||
|
|
||||||
def update_normalizer(self, rollouts, expert_loader):
|
|
||||||
policy_data_generator = rollouts.feed_forward_generator_amp(None, mini_batch_size=expert_loader.batch_size)
|
|
||||||
expert_data_generator = expert_loader.dataset.feed_forward_generator_amp(expert_loader.batch_size)
|
|
||||||
|
|
||||||
for expert_batch, policy_batch in zip(expert_data_generator, policy_data_generator):
|
|
||||||
self.update(torch.vstack(tuple(policy_batch) + tuple(expert_batch)).cpu().numpy())
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_nn_activation(act_name: str) -> torch.nn.Module:
|
|
||||||
"""Resolves the activation function from the name.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
act_name: The name of the activation function.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The activation function.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If the activation function is not found.
|
|
||||||
"""
|
|
||||||
act_dict = {
|
|
||||||
"elu": torch.nn.ELU(),
|
|
||||||
"selu": torch.nn.SELU(),
|
|
||||||
"relu": torch.nn.ReLU(),
|
|
||||||
"crelu": torch.nn.CELU(),
|
|
||||||
"lrelu": torch.nn.LeakyReLU(),
|
|
||||||
"tanh": torch.nn.Tanh(),
|
|
||||||
"sigmoid": torch.nn.Sigmoid(),
|
|
||||||
"softplus": torch.nn.Softplus(),
|
|
||||||
"gelu": torch.nn.GELU(),
|
|
||||||
"swish": torch.nn.SiLU(),
|
|
||||||
"mish": torch.nn.Mish(),
|
|
||||||
"identity": torch.nn.Identity(),
|
|
||||||
}
|
|
||||||
|
|
||||||
act_name = act_name.lower()
|
|
||||||
if act_name in act_dict:
|
|
||||||
return act_dict[act_name]
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Invalid activation function '{act_name}'. Valid activations are: {list(act_dict.keys())}")
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_optimizer(optimizer_name: str) -> torch.optim.Optimizer:
|
|
||||||
"""Resolves the optimizer from the name.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
optimizer_name: The name of the optimizer.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The optimizer.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If the optimizer is not found.
|
|
||||||
"""
|
|
||||||
optimizer_dict = {
|
|
||||||
"adam": torch.optim.Adam,
|
|
||||||
"adamw": torch.optim.AdamW,
|
|
||||||
"sgd": torch.optim.SGD,
|
|
||||||
"rmsprop": torch.optim.RMSprop,
|
|
||||||
}
|
|
||||||
|
|
||||||
optimizer_name = optimizer_name.lower()
|
|
||||||
if optimizer_name in optimizer_dict:
|
|
||||||
return optimizer_dict[optimizer_name]
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Invalid optimizer '{optimizer_name}'. Valid optimizers are: {list(optimizer_dict.keys())}")
|
|
||||||
|
|
||||||
|
|
||||||
def split_and_pad_trajectories(
|
|
||||||
tensor: torch.Tensor | TensorDict, dones: torch.Tensor
|
|
||||||
) -> tuple[torch.Tensor | TensorDict, torch.Tensor]:
|
|
||||||
"""Splits trajectories at done indices. Then concatenates them and pads with zeros up to the length of the longest
|
|
||||||
trajectory. Returns masks corresponding to valid parts of the trajectories.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
Input: [[a1, a2, a3, a4 | a5, a6],
|
|
||||||
[b1, b2 | b3, b4, b5 | b6]]
|
|
||||||
|
|
||||||
Output:[[a1, a2, a3, a4], | [[True, True, True, True],
|
|
||||||
[a5, a6, 0, 0], | [True, True, False, False],
|
|
||||||
[b1, b2, 0, 0], | [True, True, False, False],
|
|
||||||
[b3, b4, b5, 0], | [True, True, True, False],
|
|
||||||
[b6, 0, 0, 0]] | [True, False, False, False]]
|
|
||||||
|
|
||||||
Assumes that the input has the following order of dimensions: [time, number of envs, additional dimensions]
|
|
||||||
"""
|
|
||||||
|
|
||||||
dones = dones.clone()
|
|
||||||
dones[-1] = 1
|
|
||||||
# Permute the buffers to have order (num_envs, num_transitions_per_env, ...), for correct reshaping
|
|
||||||
flat_dones = dones.transpose(1, 0).reshape(-1, 1)
|
|
||||||
# Get length of trajectory by counting the number of successive not done elements
|
|
||||||
done_indices = torch.cat((flat_dones.new_tensor([-1], dtype=torch.int64), flat_dones.nonzero()[:, 0]))
|
|
||||||
trajectory_lengths = done_indices[1:] - done_indices[:-1]
|
|
||||||
trajectory_lengths_list = trajectory_lengths.tolist()
|
|
||||||
# Extract the individual trajectories
|
|
||||||
if isinstance(tensor, TensorDict):
|
|
||||||
padded_trajectories = {}
|
|
||||||
for k, v in tensor.items():
|
|
||||||
# split the tensor into trajectories
|
|
||||||
trajectories = torch.split(v.transpose(1, 0).flatten(0, 1), trajectory_lengths_list)
|
|
||||||
# add at least one full length trajectory
|
|
||||||
trajectories = trajectories + (torch.zeros(v.shape[0], *v.shape[2:], device=v.device),)
|
|
||||||
# pad the trajectories to the length of the longest trajectory
|
|
||||||
padded_trajectories[k] = torch.nn.utils.rnn.pad_sequence(trajectories)
|
|
||||||
# remove the added tensor
|
|
||||||
padded_trajectories[k] = padded_trajectories[k][:, :-1]
|
|
||||||
padded_trajectories = TensorDict(
|
|
||||||
padded_trajectories, batch_size=[tensor.batch_size[0], len(trajectory_lengths_list)]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# split the tensor into trajectories
|
|
||||||
trajectories = torch.split(tensor.transpose(1, 0).flatten(0, 1), trajectory_lengths_list)
|
|
||||||
# add at least one full length trajectory
|
|
||||||
trajectories = trajectories + (torch.zeros(tensor.shape[0], *tensor.shape[2:], device=tensor.device),)
|
|
||||||
# pad the trajectories to the length of the longest trajectory
|
|
||||||
padded_trajectories = torch.nn.utils.rnn.pad_sequence(trajectories)
|
|
||||||
# remove the added tensor
|
|
||||||
padded_trajectories = padded_trajectories[:, :-1]
|
|
||||||
# create masks for the valid parts of the trajectories
|
|
||||||
trajectory_masks = trajectory_lengths > torch.arange(0, tensor.shape[0], device=tensor.device).unsqueeze(1)
|
|
||||||
return padded_trajectories, trajectory_masks
|
|
||||||
|
|
||||||
|
|
||||||
def unpad_trajectories(trajectories, masks):
|
|
||||||
"""Does the inverse operation of split_and_pad_trajectories()"""
|
|
||||||
# Need to transpose before and after the masking to have proper reshaping
|
|
||||||
return (
|
|
||||||
trajectories.transpose(1, 0)[masks.transpose(1, 0)]
|
|
||||||
.view(-1, trajectories.shape[0], trajectories.shape[-1])
|
|
||||||
.transpose(1, 0)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def store_code_state(logdir, repositories) -> list:
|
|
||||||
git_log_dir = os.path.join(logdir, "git")
|
|
||||||
os.makedirs(git_log_dir, exist_ok=True)
|
|
||||||
file_paths = []
|
|
||||||
for repository_file_path in repositories:
|
|
||||||
try:
|
|
||||||
repo = git.Repo(repository_file_path, search_parent_directories=True)
|
|
||||||
t = repo.head.commit.tree
|
|
||||||
except Exception:
|
|
||||||
print(f"Could not find git repository in {repository_file_path}. Skipping.")
|
|
||||||
# skip if not a git repository
|
|
||||||
continue
|
|
||||||
# get the name of the repository
|
|
||||||
repo_name = pathlib.Path(repo.working_dir).name
|
|
||||||
diff_file_name = os.path.join(git_log_dir, f"{repo_name}.diff")
|
|
||||||
# check if the diff file already exists
|
|
||||||
if os.path.isfile(diff_file_name):
|
|
||||||
continue
|
|
||||||
# write the diff file
|
|
||||||
print(f"Storing git diff for '{repo_name}' in: {diff_file_name}")
|
|
||||||
with open(diff_file_name, "x", encoding="utf-8") as f:
|
|
||||||
content = f"--- git status ---\n{repo.git.status()} \n\n\n--- git diff ---\n{repo.git.diff(t)}"
|
|
||||||
f.write(content)
|
|
||||||
# add the file path to the list of files to be uploaded
|
|
||||||
file_paths.append(diff_file_name)
|
|
||||||
return file_paths
|
|
||||||
|
|
||||||
|
|
||||||
def string_to_callable(name: str) -> Callable:
|
|
||||||
"""Resolves the module and function names to return the function.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: The function name. The format should be 'module:attribute_name'.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: When the resolved attribute is not a function.
|
|
||||||
ValueError: When unable to resolve the attribute.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The function loaded from the module.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
mod_name, attr_name = name.split(":")
|
|
||||||
mod = importlib.import_module(mod_name)
|
|
||||||
callable_object = getattr(mod, attr_name)
|
|
||||||
# check if attribute is callable
|
|
||||||
if callable(callable_object):
|
|
||||||
return callable_object
|
|
||||||
else:
|
|
||||||
raise ValueError(f"The imported object is not callable: '{name}'")
|
|
||||||
except AttributeError as e:
|
|
||||||
msg = (
|
|
||||||
"We could not interpret the entry as a callable object. The format of input should be"
|
|
||||||
f" 'module:attribute_name'\nWhile processing input '{name}', received the error:\n {e}."
|
|
||||||
)
|
|
||||||
raise ValueError(msg)
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_obs_groups(
|
|
||||||
obs: TensorDict, obs_groups: dict[str, list[str]], default_sets: list[str]
|
|
||||||
) -> dict[str, list[str]]:
|
|
||||||
"""Validates the observation configuration and defaults missing observation sets.
|
|
||||||
|
|
||||||
The input is an observation dictionary `obs` containing observation groups and a configuration dictionary
|
|
||||||
`obs_groups` where the keys are the observation sets and the values are lists of observation groups.
|
|
||||||
|
|
||||||
The configuration dictionary could for example look like:
|
|
||||||
{
|
|
||||||
"policy": ["group_1", "group_2"],
|
|
||||||
"critic": ["group_1", "group_3"]
|
|
||||||
}
|
|
||||||
|
|
||||||
This means that the 'policy' observation set will contain the observations "group_1" and "group_2" and the
|
|
||||||
'critic' observation set will contain the observations "group_1" and "group_3". This function will check that all
|
|
||||||
the observations in the 'policy' and 'critic' observation sets are present in the observation dictionary from the
|
|
||||||
environment.
|
|
||||||
|
|
||||||
Additionally, if one of the `default_sets`, e.g. "critic", is not present in the configuration dictionary,
|
|
||||||
this function will:
|
|
||||||
|
|
||||||
1. Check if a group with the same name exists in the observations and assign this group to the observation set.
|
|
||||||
2. If 1. fails, it will assign the observations from the 'policy' observation set to the default observation set.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
obs: Observations from the environment in the form of a dictionary.
|
|
||||||
obs_groups: Observation sets configuration.
|
|
||||||
default_sets: Reserved observation set names used by the algorithm (besides 'policy').
|
|
||||||
If not provided in 'obs_groups', a default behavior gets triggered.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The resolved observation groups.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If any observation set is an empty list.
|
|
||||||
ValueError: If any observation set contains an observation term that is not present in the observations.
|
|
||||||
"""
|
|
||||||
# check if policy observation set exists
|
|
||||||
if "policy" not in obs_groups.keys():
|
|
||||||
if "policy" in obs:
|
|
||||||
obs_groups["policy"] = ["policy"]
|
|
||||||
warnings.warn(
|
|
||||||
"The observation configuration dictionary 'obs_groups' must contain the 'policy' key."
|
|
||||||
" As an observation group with the name 'policy' was found, this is assumed to be the observation set."
|
|
||||||
" Consider adding the 'policy' key to the 'obs_groups' dictionary for clarity."
|
|
||||||
" This behavior will be removed in a future version."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
"The observation configuration dictionary 'obs_groups' must contain the 'policy' key."
|
|
||||||
f" Found keys: {list(obs_groups.keys())}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# check all observation sets for valid observation groups
|
|
||||||
for set_name, groups in obs_groups.items():
|
|
||||||
# check if the list is empty
|
|
||||||
if len(groups) == 0:
|
|
||||||
msg = f"The '{set_name}' key in the 'obs_groups' dictionary can not be an empty list."
|
|
||||||
if set_name in default_sets:
|
|
||||||
if set_name not in obs:
|
|
||||||
msg += " Consider removing the key to default to the observations used for the 'policy' set."
|
|
||||||
else:
|
|
||||||
msg += (
|
|
||||||
f" Consider removing the key to default to the observation '{set_name}' from the environment."
|
|
||||||
)
|
|
||||||
raise ValueError(msg)
|
|
||||||
# check groups exist inside the observations from the environment
|
|
||||||
for group in groups:
|
|
||||||
if group not in obs:
|
|
||||||
raise ValueError(
|
|
||||||
f"Observation '{group}' in observation set '{set_name}' not found in the observations from the"
|
|
||||||
f" environment. Available observations from the environment: {list(obs.keys())}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# fill missing observation sets
|
|
||||||
for default_set_name in default_sets:
|
|
||||||
if default_set_name not in obs_groups.keys():
|
|
||||||
if default_set_name in obs:
|
|
||||||
obs_groups[default_set_name] = [default_set_name]
|
|
||||||
warnings.warn(
|
|
||||||
f"The observation configuration dictionary 'obs_groups' must contain the '{default_set_name}' key."
|
|
||||||
f" As an observation group with the name '{default_set_name}' was found, this is assumed to be the"
|
|
||||||
f" observation set. Consider adding the '{default_set_name}' key to the 'obs_groups' dictionary for"
|
|
||||||
" clarity. This behavior will be removed in a future version."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
obs_groups[default_set_name] = obs_groups["policy"].copy()
|
|
||||||
warnings.warn(
|
|
||||||
f"The observation configuration dictionary 'obs_groups' must contain the '{default_set_name}' key."
|
|
||||||
f" As the configuration for '{default_set_name}' is missing, the observations from the 'policy' set"
|
|
||||||
f" are used. Consider adding the '{default_set_name}' key to the 'obs_groups' dictionary for"
|
|
||||||
" clarity. This behavior will be removed in a future version."
|
|
||||||
)
|
|
||||||
|
|
||||||
# print the final parsed observation sets
|
|
||||||
print("-" * 80)
|
|
||||||
print("Resolved observation sets: ")
|
|
||||||
for set_name, groups in obs_groups.items():
|
|
||||||
print("\t", set_name, ": ", groups)
|
|
||||||
print("-" * 80)
|
|
||||||
|
|
||||||
return obs_groups
|
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
from dataclasses import asdict
|
|
||||||
from torch.utils.tensorboard import SummaryWriter
|
|
||||||
|
|
||||||
try:
|
|
||||||
import wandb
|
|
||||||
except ModuleNotFoundError:
|
|
||||||
raise ModuleNotFoundError("Wandb is required to log to Weights and Biases.")
|
|
||||||
|
|
||||||
|
|
||||||
class WandbSummaryWriter(SummaryWriter):
|
|
||||||
"""Summary writer for Weights and Biases."""
|
|
||||||
|
|
||||||
def __init__(self, log_dir: str, flush_secs: int, cfg):
|
|
||||||
super().__init__(log_dir, flush_secs)
|
|
||||||
|
|
||||||
# Get the run name
|
|
||||||
run_name = os.path.split(log_dir)[-1]
|
|
||||||
|
|
||||||
try:
|
|
||||||
project = cfg["wandb_project"]
|
|
||||||
except KeyError:
|
|
||||||
raise KeyError("Please specify wandb_project in the runner config, e.g. legged_gym.")
|
|
||||||
|
|
||||||
try:
|
|
||||||
entity = os.environ["WANDB_USERNAME"]
|
|
||||||
except KeyError:
|
|
||||||
entity = None
|
|
||||||
|
|
||||||
# Initialize wandb
|
|
||||||
wandb.init(project=project, entity=entity, name=run_name)
|
|
||||||
|
|
||||||
# Add log directory to wandb
|
|
||||||
wandb.config.update({"log_dir": log_dir})
|
|
||||||
|
|
||||||
self.name_map = {
|
|
||||||
"Train/mean_reward/time": "Train/mean_reward_time",
|
|
||||||
"Train/mean_episode_length/time": "Train/mean_episode_length_time",
|
|
||||||
}
|
|
||||||
|
|
||||||
def store_config(self, env_cfg, runner_cfg, alg_cfg, policy_cfg):
|
|
||||||
wandb.config.update({"runner_cfg": runner_cfg})
|
|
||||||
wandb.config.update({"policy_cfg": policy_cfg})
|
|
||||||
wandb.config.update({"alg_cfg": alg_cfg})
|
|
||||||
try:
|
|
||||||
wandb.config.update({"env_cfg": env_cfg.to_dict()})
|
|
||||||
except Exception:
|
|
||||||
wandb.config.update({"env_cfg": asdict(env_cfg)})
|
|
||||||
|
|
||||||
def add_scalar(self, tag, scalar_value, global_step=None, walltime=None, new_style=False):
|
|
||||||
super().add_scalar(
|
|
||||||
tag,
|
|
||||||
scalar_value,
|
|
||||||
global_step=global_step,
|
|
||||||
walltime=walltime,
|
|
||||||
new_style=new_style,
|
|
||||||
)
|
|
||||||
wandb.log({self._map_path(tag): scalar_value}, step=global_step)
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
wandb.finish()
|
|
||||||
|
|
||||||
def log_config(self, env_cfg, runner_cfg, alg_cfg, policy_cfg):
|
|
||||||
self.store_config(env_cfg, runner_cfg, alg_cfg, policy_cfg)
|
|
||||||
|
|
||||||
def save_model(self, model_path, iter):
|
|
||||||
wandb.save(model_path, base_path=os.path.dirname(model_path))
|
|
||||||
|
|
||||||
def save_file(self, path, iter=None):
|
|
||||||
wandb.save(path, base_path=os.path.dirname(path))
|
|
||||||
|
|
||||||
"""
|
|
||||||
Private methods.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _map_path(self, path):
|
|
||||||
if path in self.name_map:
|
|
||||||
return self.name_map[path]
|
|
||||||
else:
|
|
||||||
return path
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
"""Submodule defining the environment definitions."""
|
|
||||||
|
|
||||||
from .vec_env import VecEnv
|
|
||||||
|
|
||||||
__all__ = ["VecEnv"]
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
from tensordict import TensorDict
|
|
||||||
|
|
||||||
|
|
||||||
class VecEnv(ABC):
|
|
||||||
"""Abstract class for a vectorized environment.
|
|
||||||
|
|
||||||
The vectorized environment is a collection of environments that are synchronized. This means that
|
|
||||||
the same type of action is applied to all environments and the same type of observation is returned from all
|
|
||||||
environments.
|
|
||||||
"""
|
|
||||||
|
|
||||||
num_envs: int
|
|
||||||
"""Number of environments."""
|
|
||||||
|
|
||||||
num_actions: int
|
|
||||||
"""Number of actions."""
|
|
||||||
|
|
||||||
max_episode_length: int | torch.Tensor
|
|
||||||
|
|
||||||
max_episode_length_s: float
|
|
||||||
"""Maximum episode length.
|
|
||||||
|
|
||||||
The maximum episode length can be a scalar or a tensor. If it is a scalar, it is the same for all environments.
|
|
||||||
If it is a tensor, it is the maximum episode length for each environment. This is useful for dynamic episode
|
|
||||||
lengths.
|
|
||||||
"""
|
|
||||||
|
|
||||||
episode_length_buf: torch.Tensor
|
|
||||||
"""Buffer for current episode lengths."""
|
|
||||||
|
|
||||||
device: torch.device | str
|
|
||||||
"""Device to use."""
|
|
||||||
|
|
||||||
cfg: dict | object
|
|
||||||
"""Configuration object."""
|
|
||||||
|
|
||||||
reset_env_ids: torch.Tensor | None = None
|
|
||||||
|
|
||||||
contact_phase: torch.Tensor | None = None
|
|
||||||
"""
|
|
||||||
Operations.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_observations(self) -> TensorDict:
|
|
||||||
"""Return the current observations.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
observations (TensorDict): Observations from the environment.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_amp_observations(self) -> TensorDict:
|
|
||||||
"""Return the current AMP observations.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
observations (TensorDict): Observations from the environment.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def step(self, actions: torch.Tensor) -> tuple[TensorDict, torch.Tensor, torch.Tensor, dict]:
|
|
||||||
"""Apply input action to the environment.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
actions (torch.Tensor): Input actions to apply. Shape: (num_envs, num_actions)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
observations (TensorDict): Observations from the environment.
|
|
||||||
rewards (torch.Tensor): Rewards from the environment. Shape: (num_envs,)
|
|
||||||
dones (torch.Tensor): Done flags from the environment. Shape: (num_envs,)
|
|
||||||
extras (dict): Extra information from the environment.
|
|
||||||
|
|
||||||
Observations:
|
|
||||||
|
|
||||||
The observations TensorDict usually contains multiple observation groups. The `obs_groups`
|
|
||||||
dictionary of the runner configuration specifies which observation groups are used for which
|
|
||||||
purpose, i.e., it maps the available observation groups to observation sets. The observation sets
|
|
||||||
(keys of the `obs_groups` dictionary) currently used by rsl_rl are:
|
|
||||||
|
|
||||||
- "policy": Specified observation groups are used as input to the actor/student network.
|
|
||||||
- "critic": Specified observation groups are used as input to the critic network.
|
|
||||||
- "teacher": Specified observation groups are used as input to the teacher network.
|
|
||||||
- "rnd_state": Specified observation groups are used as input to the RND network.
|
|
||||||
|
|
||||||
Incomplete or incorrect configurations are handled in the `resolve_obs_groups()` function in
|
|
||||||
`rsl_rl/utils/utils.py`.
|
|
||||||
|
|
||||||
Extras:
|
|
||||||
|
|
||||||
The extras dictionary includes metrics such as the episode reward, episode length, etc. The following
|
|
||||||
dictionary keys are used by rsl_rl:
|
|
||||||
|
|
||||||
- "time_outs" (torch.Tensor): Timeouts for the environments. These correspond to terminations that
|
|
||||||
happen due to time limits and not due to the environment reaching a terminal state. This is useful
|
|
||||||
for environments that have a fixed episode length.
|
|
||||||
|
|
||||||
- "log" (dict[str, float | torch.Tensor]): Additional information for logging and debugging purposes.
|
|
||||||
The key should be a string and start with "/" for namespacing. The value can be a scalar or a
|
|
||||||
tensor. If it is a tensor, the mean of the tensor is used for logging.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
"""Definitions for neural-network components for RL-agents."""
|
|
||||||
|
|
||||||
from .actor_critic import ActorCritic
|
|
||||||
from .actor_critic_recurrent import ActorCriticRecurrent
|
|
||||||
from .rnd import *
|
|
||||||
from .student_teacher import StudentTeacher
|
|
||||||
from .student_teacher_recurrent import StudentTeacherRecurrent
|
|
||||||
from .symmetry import *
|
|
||||||
from .discriminator_multi import DiscriminatorMulti
|
|
||||||
__all__ = [
|
|
||||||
"ActorCritic",
|
|
||||||
"ActorCriticRecurrent",
|
|
||||||
"StudentTeacher",
|
|
||||||
"StudentTeacherRecurrent",
|
|
||||||
"DiscriminatorMulti",
|
|
||||||
]
|
|
||||||
|
|
@ -1,195 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
from torch.distributions import Normal
|
|
||||||
|
|
||||||
from rsl_rl.networks import MLP, EmpiricalNormalization
|
|
||||||
|
|
||||||
|
|
||||||
class ActorCritic(nn.Module):
|
|
||||||
is_recurrent = False
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
obs,
|
|
||||||
obs_groups,
|
|
||||||
num_actions,
|
|
||||||
actor_obs_normalization=False,
|
|
||||||
critic_obs_normalization=False,
|
|
||||||
actor_hidden_dims=[256, 256, 256],
|
|
||||||
critic_hidden_dims=[256, 256, 256],
|
|
||||||
activation="elu",
|
|
||||||
init_noise_std=1.0,
|
|
||||||
noise_std_type: str = "scalar",
|
|
||||||
state_dependent_std=False,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
if kwargs:
|
|
||||||
print(
|
|
||||||
"ActorCritic.__init__ got unexpected arguments, which will be ignored: "
|
|
||||||
+ str([key for key in kwargs.keys()])
|
|
||||||
)
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
# get the observation dimensions
|
|
||||||
self.obs_groups = obs_groups
|
|
||||||
num_actor_obs = 0
|
|
||||||
for obs_group in obs_groups["policy"]:
|
|
||||||
assert len(obs[obs_group].shape) == 2, "The ActorCritic module only supports 1D observations."
|
|
||||||
num_actor_obs += obs[obs_group].shape[-1]
|
|
||||||
num_critic_obs = 0
|
|
||||||
for obs_group in obs_groups["critic"]:
|
|
||||||
assert len(obs[obs_group].shape) == 2, "The ActorCritic module only supports 1D observations."
|
|
||||||
num_critic_obs += obs[obs_group].shape[-1]
|
|
||||||
|
|
||||||
self.state_dependent_std = state_dependent_std
|
|
||||||
# actor
|
|
||||||
if self.state_dependent_std:
|
|
||||||
self.actor = MLP(num_actor_obs, [2, num_actions], actor_hidden_dims, activation)
|
|
||||||
else:
|
|
||||||
self.actor = MLP(num_actor_obs, num_actions, actor_hidden_dims, activation)
|
|
||||||
# actor observation normalization
|
|
||||||
self.actor_obs_normalization = actor_obs_normalization
|
|
||||||
if actor_obs_normalization:
|
|
||||||
self.actor_obs_normalizer = EmpiricalNormalization(num_actor_obs)
|
|
||||||
else:
|
|
||||||
self.actor_obs_normalizer = torch.nn.Identity()
|
|
||||||
print(f"Actor MLP: {self.actor}")
|
|
||||||
|
|
||||||
# critic
|
|
||||||
self.critic = MLP(num_critic_obs, 1, critic_hidden_dims, activation)
|
|
||||||
# critic observation normalization
|
|
||||||
self.critic_obs_normalization = critic_obs_normalization
|
|
||||||
if critic_obs_normalization:
|
|
||||||
self.critic_obs_normalizer = EmpiricalNormalization(num_critic_obs)
|
|
||||||
else:
|
|
||||||
self.critic_obs_normalizer = torch.nn.Identity()
|
|
||||||
print(f"Critic MLP: {self.critic}")
|
|
||||||
|
|
||||||
# Action noise
|
|
||||||
self.noise_std_type = noise_std_type
|
|
||||||
if self.state_dependent_std:
|
|
||||||
torch.nn.init.zeros_(self.actor[-2].weight[num_actions:])
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
torch.nn.init.constant_(self.actor[-2].bias[num_actions:], init_noise_std)
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
torch.nn.init.constant_(
|
|
||||||
self.actor[-2].bias[num_actions:], torch.log(torch.tensor(init_noise_std + 1e-7))
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
else:
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
self.std = nn.Parameter(init_noise_std * torch.ones(num_actions))
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
self.log_std = nn.Parameter(torch.log(init_noise_std * torch.ones(num_actions)))
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
|
|
||||||
# Action distribution (populated in update_distribution)
|
|
||||||
self.distribution = None
|
|
||||||
# disable args validation for speedup
|
|
||||||
Normal.set_default_validate_args(False)
|
|
||||||
|
|
||||||
def reset(self, dones=None):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def forward(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@property
|
|
||||||
def action_mean(self):
|
|
||||||
return self.distribution.mean
|
|
||||||
|
|
||||||
@property
|
|
||||||
def action_std(self):
|
|
||||||
return self.distribution.stddev
|
|
||||||
|
|
||||||
@property
|
|
||||||
def entropy(self):
|
|
||||||
return self.distribution.entropy().sum(dim=-1)
|
|
||||||
|
|
||||||
def update_distribution(self, obs):
|
|
||||||
if self.state_dependent_std:
|
|
||||||
# compute mean and standard deviation
|
|
||||||
mean_and_std = self.actor(obs)
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
mean, std = torch.unbind(mean_and_std, dim=-2)
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
mean, log_std = torch.unbind(mean_and_std, dim=-2)
|
|
||||||
std = torch.exp(log_std)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
else:
|
|
||||||
# compute mean
|
|
||||||
mean = self.actor(obs)
|
|
||||||
# compute standard deviation
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
std = self.std.expand_as(mean)
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
std = torch.exp(self.log_std).expand_as(mean)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
# create distribution
|
|
||||||
self.distribution = Normal(mean, std)
|
|
||||||
|
|
||||||
def act(self, obs, **kwargs):
|
|
||||||
obs = self.get_actor_obs(obs)
|
|
||||||
obs = self.actor_obs_normalizer(obs)
|
|
||||||
self.update_distribution(obs)
|
|
||||||
return self.distribution.sample()
|
|
||||||
|
|
||||||
def act_inference(self, obs):
|
|
||||||
obs = self.get_actor_obs(obs)
|
|
||||||
obs = self.actor_obs_normalizer(obs)
|
|
||||||
return self.actor(obs)
|
|
||||||
|
|
||||||
def evaluate(self, obs, **kwargs):
|
|
||||||
obs = self.get_critic_obs(obs)
|
|
||||||
obs = self.critic_obs_normalizer(obs)
|
|
||||||
return self.critic(obs)
|
|
||||||
|
|
||||||
def get_actor_obs(self, obs):
|
|
||||||
obs_list = []
|
|
||||||
for obs_group in self.obs_groups["policy"]:
|
|
||||||
obs_list.append(obs[obs_group])
|
|
||||||
return torch.cat(obs_list, dim=-1)
|
|
||||||
|
|
||||||
def get_critic_obs(self, obs):
|
|
||||||
obs_list = []
|
|
||||||
for obs_group in self.obs_groups["critic"]:
|
|
||||||
obs_list.append(obs[obs_group])
|
|
||||||
return torch.cat(obs_list, dim=-1)
|
|
||||||
|
|
||||||
def get_actions_log_prob(self, actions):
|
|
||||||
return self.distribution.log_prob(actions).sum(dim=-1)
|
|
||||||
|
|
||||||
def update_normalization(self, obs):
|
|
||||||
if self.actor_obs_normalization:
|
|
||||||
actor_obs = self.get_actor_obs(obs)
|
|
||||||
self.actor_obs_normalizer.update(actor_obs)
|
|
||||||
if self.critic_obs_normalization:
|
|
||||||
critic_obs = self.get_critic_obs(obs)
|
|
||||||
self.critic_obs_normalizer.update(critic_obs)
|
|
||||||
|
|
||||||
def load_state_dict(self, state_dict, strict=True):
|
|
||||||
"""Load the parameters of the actor-critic model.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
state_dict (dict): State dictionary of the model.
|
|
||||||
strict (bool): Whether to strictly enforce that the keys in state_dict match the keys returned by this
|
|
||||||
module's state_dict() function.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: Whether this training resumes a previous training. This flag is used by the `load()` function of
|
|
||||||
`OnPolicyRunner` to determine how to load further parameters (relevant for, e.g., distillation).
|
|
||||||
"""
|
|
||||||
|
|
||||||
super().load_state_dict(state_dict, strict=strict)
|
|
||||||
return True # training resumes
|
|
||||||
|
|
@ -1,218 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
import warnings
|
|
||||||
from torch.distributions import Normal
|
|
||||||
|
|
||||||
from rsl_rl.networks import MLP, EmpiricalNormalization, Memory
|
|
||||||
|
|
||||||
|
|
||||||
class ActorCriticRecurrent(nn.Module):
|
|
||||||
is_recurrent = True
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
obs,
|
|
||||||
obs_groups,
|
|
||||||
num_actions,
|
|
||||||
actor_obs_normalization=False,
|
|
||||||
critic_obs_normalization=False,
|
|
||||||
actor_hidden_dims=[256, 256, 256],
|
|
||||||
critic_hidden_dims=[256, 256, 256],
|
|
||||||
activation="elu",
|
|
||||||
init_noise_std=1.0,
|
|
||||||
noise_std_type: str = "scalar",
|
|
||||||
state_dependent_std=False,
|
|
||||||
rnn_type="lstm",
|
|
||||||
rnn_hidden_dim=256,
|
|
||||||
rnn_num_layers=1,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
if "rnn_hidden_size" in kwargs:
|
|
||||||
warnings.warn(
|
|
||||||
"The argument `rnn_hidden_size` is deprecated and will be removed in a future version. "
|
|
||||||
"Please use `rnn_hidden_dim` instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
if rnn_hidden_dim == 256: # Only override if the new argument is at its default
|
|
||||||
rnn_hidden_dim = kwargs.pop("rnn_hidden_size")
|
|
||||||
if kwargs:
|
|
||||||
print(
|
|
||||||
"ActorCriticRecurrent.__init__ got unexpected arguments, which will be ignored: " + str(kwargs.keys()),
|
|
||||||
)
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
# get the observation dimensions
|
|
||||||
self.obs_groups = obs_groups
|
|
||||||
num_actor_obs = 0
|
|
||||||
for obs_group in obs_groups["policy"]:
|
|
||||||
assert len(obs[obs_group].shape) == 2, "The ActorCriticRecurrent module only supports 1D observations."
|
|
||||||
num_actor_obs += obs[obs_group].shape[-1]
|
|
||||||
num_critic_obs = 0
|
|
||||||
for obs_group in obs_groups["critic"]:
|
|
||||||
assert len(obs[obs_group].shape) == 2, "The ActorCriticRecurrent module only supports 1D observations."
|
|
||||||
num_critic_obs += obs[obs_group].shape[-1]
|
|
||||||
|
|
||||||
self.state_dependent_std = state_dependent_std
|
|
||||||
# actor
|
|
||||||
self.memory_a = Memory(num_actor_obs, type=rnn_type, num_layers=rnn_num_layers, hidden_size=rnn_hidden_dim)
|
|
||||||
if self.state_dependent_std:
|
|
||||||
self.actor = MLP(rnn_hidden_dim, [2, num_actions], actor_hidden_dims, activation)
|
|
||||||
else:
|
|
||||||
self.actor = MLP(rnn_hidden_dim, num_actions, actor_hidden_dims, activation)
|
|
||||||
|
|
||||||
# actor observation normalization
|
|
||||||
self.actor_obs_normalization = actor_obs_normalization
|
|
||||||
if actor_obs_normalization:
|
|
||||||
self.actor_obs_normalizer = EmpiricalNormalization(num_actor_obs)
|
|
||||||
else:
|
|
||||||
self.actor_obs_normalizer = torch.nn.Identity()
|
|
||||||
print(f"Actor RNN: {self.memory_a}")
|
|
||||||
print(f"Actor MLP: {self.actor}")
|
|
||||||
|
|
||||||
# critic
|
|
||||||
self.memory_c = Memory(num_critic_obs, type=rnn_type, num_layers=rnn_num_layers, hidden_size=rnn_hidden_dim)
|
|
||||||
self.critic = MLP(rnn_hidden_dim, 1, critic_hidden_dims, activation)
|
|
||||||
# critic observation normalization
|
|
||||||
self.critic_obs_normalization = critic_obs_normalization
|
|
||||||
if critic_obs_normalization:
|
|
||||||
self.critic_obs_normalizer = EmpiricalNormalization(num_critic_obs)
|
|
||||||
else:
|
|
||||||
self.critic_obs_normalizer = torch.nn.Identity()
|
|
||||||
print(f"Critic RNN: {self.memory_c}")
|
|
||||||
print(f"Critic MLP: {self.critic}")
|
|
||||||
|
|
||||||
# Action noise
|
|
||||||
self.noise_std_type = noise_std_type
|
|
||||||
if self.state_dependent_std:
|
|
||||||
torch.nn.init.zeros_(self.actor[-2].weight[num_actions:])
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
torch.nn.init.constant_(self.actor[-2].bias[num_actions:], init_noise_std)
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
torch.nn.init.constant_(
|
|
||||||
self.actor[-2].bias[num_actions:], torch.log(torch.tensor(init_noise_std + 1e-7))
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
else:
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
self.std = nn.Parameter(init_noise_std * torch.ones(num_actions))
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
self.log_std = nn.Parameter(torch.log(init_noise_std * torch.ones(num_actions)))
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
|
|
||||||
# Action distribution (populated in update_distribution)
|
|
||||||
self.distribution = None
|
|
||||||
# disable args validation for speedup
|
|
||||||
Normal.set_default_validate_args(False)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def action_mean(self):
|
|
||||||
return self.distribution.mean
|
|
||||||
|
|
||||||
@property
|
|
||||||
def action_std(self):
|
|
||||||
return self.distribution.stddev
|
|
||||||
|
|
||||||
@property
|
|
||||||
def entropy(self):
|
|
||||||
return self.distribution.entropy().sum(dim=-1)
|
|
||||||
|
|
||||||
def reset(self, dones=None):
|
|
||||||
self.memory_a.reset(dones)
|
|
||||||
self.memory_c.reset(dones)
|
|
||||||
|
|
||||||
def forward(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update_distribution(self, obs):
|
|
||||||
if self.state_dependent_std:
|
|
||||||
# compute mean and standard deviation
|
|
||||||
mean_and_std = self.actor(obs)
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
mean, std = torch.unbind(mean_and_std, dim=-2)
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
mean, log_std = torch.unbind(mean_and_std, dim=-2)
|
|
||||||
std = torch.exp(log_std)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
else:
|
|
||||||
# compute mean
|
|
||||||
mean = self.actor(obs)
|
|
||||||
# compute standard deviation
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
std = self.std.expand_as(mean)
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
std = torch.exp(self.log_std).expand_as(mean)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
# create distribution
|
|
||||||
self.distribution = Normal(mean, std)
|
|
||||||
|
|
||||||
def act(self, obs, masks=None, hidden_states=None):
|
|
||||||
obs = self.get_actor_obs(obs)
|
|
||||||
obs = self.actor_obs_normalizer(obs)
|
|
||||||
out_mem = self.memory_a(obs, masks, hidden_states).squeeze(0)
|
|
||||||
self.update_distribution(out_mem)
|
|
||||||
return self.distribution.sample()
|
|
||||||
|
|
||||||
def act_inference(self, obs):
|
|
||||||
obs = self.get_actor_obs(obs)
|
|
||||||
obs = self.actor_obs_normalizer(obs)
|
|
||||||
out_mem = self.memory_a(obs).squeeze(0)
|
|
||||||
return self.actor(out_mem)
|
|
||||||
|
|
||||||
def evaluate(self, obs, masks=None, hidden_states=None):
|
|
||||||
obs = self.get_critic_obs(obs)
|
|
||||||
obs = self.critic_obs_normalizer(obs)
|
|
||||||
out_mem = self.memory_c(obs, masks, hidden_states).squeeze(0)
|
|
||||||
return self.critic(out_mem)
|
|
||||||
|
|
||||||
def get_actor_obs(self, obs):
|
|
||||||
obs_list = []
|
|
||||||
for obs_group in self.obs_groups["policy"]:
|
|
||||||
obs_list.append(obs[obs_group])
|
|
||||||
return torch.cat(obs_list, dim=-1)
|
|
||||||
|
|
||||||
def get_critic_obs(self, obs):
|
|
||||||
obs_list = []
|
|
||||||
for obs_group in self.obs_groups["critic"]:
|
|
||||||
obs_list.append(obs[obs_group])
|
|
||||||
return torch.cat(obs_list, dim=-1)
|
|
||||||
|
|
||||||
def get_actions_log_prob(self, actions):
|
|
||||||
return self.distribution.log_prob(actions).sum(dim=-1)
|
|
||||||
|
|
||||||
def get_hidden_states(self):
|
|
||||||
return self.memory_a.hidden_states, self.memory_c.hidden_states
|
|
||||||
|
|
||||||
def update_normalization(self, obs):
|
|
||||||
if self.actor_obs_normalization:
|
|
||||||
actor_obs = self.get_actor_obs(obs)
|
|
||||||
self.actor_obs_normalizer.update(actor_obs)
|
|
||||||
if self.critic_obs_normalization:
|
|
||||||
critic_obs = self.get_critic_obs(obs)
|
|
||||||
self.critic_obs_normalizer.update(critic_obs)
|
|
||||||
|
|
||||||
def load_state_dict(self, state_dict, strict=True):
|
|
||||||
"""Load the parameters of the actor-critic model.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
state_dict (dict): State dictionary of the model.
|
|
||||||
strict (bool): Whether to strictly enforce that the keys in state_dict match the keys returned by this
|
|
||||||
module's state_dict() function.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: Whether this training resumes a previous training. This flag is used by the `load()` function of
|
|
||||||
`OnPolicyRunner` to determine how to load further parameters (relevant for, e.g., distillation).
|
|
||||||
"""
|
|
||||||
|
|
||||||
super().load_state_dict(state_dict, strict=strict)
|
|
||||||
return True
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
from torch import autograd
|
|
||||||
import torch.nn.utils.spectral_norm as spectral_norm
|
|
||||||
|
|
||||||
|
|
||||||
class DiscriminatorMulti(nn.Module):
|
|
||||||
def __init__(
|
|
||||||
self, state_dim, amp_reward_coef, hidden_layer_sizes, device,
|
|
||||||
num_frames=2, task_reward_lerp=0.0, use_lerp=True):
|
|
||||||
super(DiscriminatorMulti, self).__init__()
|
|
||||||
|
|
||||||
self.device = device
|
|
||||||
self.state_dim = state_dim
|
|
||||||
self.use_lerp = use_lerp
|
|
||||||
self.num_frames = num_frames # 存储帧数参数
|
|
||||||
|
|
||||||
self.amp_reward_coef = amp_reward_coef
|
|
||||||
amp_layers = []
|
|
||||||
|
|
||||||
curr_in_dim = state_dim * num_frames
|
|
||||||
for hidden_dim in hidden_layer_sizes:
|
|
||||||
amp_layers.append(spectral_norm(nn.Linear(curr_in_dim, hidden_dim)))
|
|
||||||
amp_layers.append(nn.ReLU())
|
|
||||||
curr_in_dim = hidden_dim
|
|
||||||
self.trunk = nn.Sequential(*amp_layers).to(device)
|
|
||||||
self.amp_linear = spectral_norm(nn.Linear(hidden_layer_sizes[-1], 1)).to(device)
|
|
||||||
|
|
||||||
self.trunk.train()
|
|
||||||
self.amp_linear.train()
|
|
||||||
|
|
||||||
self.task_reward_lerp = task_reward_lerp
|
|
||||||
|
|
||||||
def forward(self, x):
|
|
||||||
h = self.trunk(x)
|
|
||||||
d = self.amp_linear(h)
|
|
||||||
return d
|
|
||||||
|
|
||||||
def compute_grad_pen(self,
|
|
||||||
expert_states, # 改为接收多帧状态列表
|
|
||||||
lambda_=10):
|
|
||||||
# 将多帧状态沿最后一个维度拼接
|
|
||||||
expert_data = expert_states.flatten(1)
|
|
||||||
expert_data.requires_grad = True
|
|
||||||
|
|
||||||
disc = self.amp_linear(self.trunk(expert_data))
|
|
||||||
ones = torch.ones(disc.size(), device=disc.device)
|
|
||||||
grad = autograd.grad(
|
|
||||||
outputs=disc, inputs=expert_data,
|
|
||||||
grad_outputs=ones, create_graph=True,
|
|
||||||
retain_graph=True, only_inputs=True)[0]
|
|
||||||
|
|
||||||
# Enforce that the grad norm approaches 0.
|
|
||||||
grad_pen = lambda_ * (grad.norm(2, dim=1) - 0).pow(2).mean()
|
|
||||||
return grad_pen
|
|
||||||
|
|
||||||
|
|
||||||
def get_disc_weights(self):
|
|
||||||
weights = []
|
|
||||||
for m in self.trunk.modules():
|
|
||||||
if isinstance(m, nn.Linear):
|
|
||||||
weights.append(torch.flatten(m.weight))
|
|
||||||
|
|
||||||
weights.append(torch.flatten(self.amp_linear.weight))
|
|
||||||
return weights
|
|
||||||
|
|
||||||
def get_disc_logit_weights(self):
|
|
||||||
return torch.flatten(self.amp_linear.weight)
|
|
||||||
|
|
||||||
def predict_amp_reward(
|
|
||||||
self, states, # 改为接收多帧状态列表
|
|
||||||
task_reward, normalizer=None):
|
|
||||||
"""
|
|
||||||
states: torch.Tensor, shape=(num_envs, num_frames, state_dim)
|
|
||||||
task_reward: torch.Tensor, shape=(num_envs, 1)
|
|
||||||
"""
|
|
||||||
# import ipdb; ipdb.set_trace()
|
|
||||||
with torch.no_grad():
|
|
||||||
self.eval()
|
|
||||||
if normalizer is not None:
|
|
||||||
# 对每一帧状态进行归一化
|
|
||||||
states = normalizer.normalize_torch(states, self.device)
|
|
||||||
|
|
||||||
# 拼接多帧状态
|
|
||||||
state_cat = states.flatten(1)
|
|
||||||
d = self.amp_linear(self.trunk(state_cat))
|
|
||||||
disc_reward = self.amp_reward_coef * torch.clamp(1 - (1/4) * torch.square(d - 1), min=0)
|
|
||||||
|
|
||||||
if self.use_lerp:
|
|
||||||
if self.task_reward_lerp > 0:
|
|
||||||
reward = self._lerp_reward(disc_reward, task_reward.unsqueeze(-1))
|
|
||||||
self.train()
|
|
||||||
return reward.squeeze(), d, disc_reward.squeeze() * (1.0 - self.task_reward_lerp)
|
|
||||||
else:
|
|
||||||
disc_reward *= 0.02
|
|
||||||
reward = task_reward.unsqueeze(-1) + disc_reward
|
|
||||||
self.train()
|
|
||||||
return reward.squeeze(), d, disc_reward.squeeze()
|
|
||||||
|
|
||||||
def _lerp_reward(self, disc_r, task_r):
|
|
||||||
r = (1.0 - self.task_reward_lerp) * disc_r + self.task_reward_lerp * task_r
|
|
||||||
return r
|
|
||||||
|
|
@ -1,209 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
|
|
||||||
from rsl_rl.networks import MLP, EmpiricalDiscountedVariationNormalization, EmpiricalNormalization
|
|
||||||
|
|
||||||
|
|
||||||
class RandomNetworkDistillation(nn.Module):
|
|
||||||
"""Implementation of Random Network Distillation (RND) [1]
|
|
||||||
|
|
||||||
References:
|
|
||||||
.. [1] Burda, Yuri, et al. "Exploration by random network distillation." arXiv preprint arXiv:1810.12894 (2018).
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
num_states: int,
|
|
||||||
obs_groups: dict,
|
|
||||||
num_outputs: int,
|
|
||||||
predictor_hidden_dims: list[int],
|
|
||||||
target_hidden_dims: list[int],
|
|
||||||
activation: str = "elu",
|
|
||||||
weight: float = 0.0,
|
|
||||||
state_normalization: bool = False,
|
|
||||||
reward_normalization: bool = False,
|
|
||||||
device: str = "cpu",
|
|
||||||
weight_schedule: dict | None = None,
|
|
||||||
):
|
|
||||||
"""Initialize the RND module.
|
|
||||||
|
|
||||||
- If :attr:`state_normalization` is True, then the input state is normalized using an Empirical Normalization layer.
|
|
||||||
- If :attr:`reward_normalization` is True, then the intrinsic reward is normalized using an Empirical Discounted
|
|
||||||
Variation Normalization layer.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
If the hidden dimensions are -1 in the predictor and target networks configuration, then the number of states
|
|
||||||
is used as the hidden dimension.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
num_states: Number of states/inputs to the predictor and target networks.
|
|
||||||
num_outputs: Number of outputs (embedding size) of the predictor and target networks.
|
|
||||||
predictor_hidden_dims: List of hidden dimensions of the predictor network.
|
|
||||||
target_hidden_dims: List of hidden dimensions of the target network.
|
|
||||||
activation: Activation function. Defaults to "elu".
|
|
||||||
weight: Scaling factor of the intrinsic reward. Defaults to 0.0.
|
|
||||||
state_normalization: Whether to normalize the input state. Defaults to False.
|
|
||||||
reward_normalization: Whether to normalize the intrinsic reward. Defaults to False.
|
|
||||||
device: Device to use. Defaults to "cpu".
|
|
||||||
weight_schedule: The type of schedule to use for the RND weight parameter.
|
|
||||||
Defaults to None, in which case the weight parameter is constant.
|
|
||||||
It is a dictionary with the following keys:
|
|
||||||
|
|
||||||
- "mode": The type of schedule to use for the RND weight parameter.
|
|
||||||
- "constant": Constant weight schedule.
|
|
||||||
- "step": Step weight schedule.
|
|
||||||
- "linear": Linear weight schedule.
|
|
||||||
|
|
||||||
For the "step" weight schedule, the following parameters are required:
|
|
||||||
|
|
||||||
- "final_step": The step at which the weight parameter is set to the final value.
|
|
||||||
- "final_value": The final value of the weight parameter.
|
|
||||||
|
|
||||||
For the "linear" weight schedule, the following parameters are required:
|
|
||||||
- "initial_step": The step at which the weight parameter is set to the initial value.
|
|
||||||
- "final_step": The step at which the weight parameter is set to the final value.
|
|
||||||
- "final_value": The final value of the weight parameter.
|
|
||||||
"""
|
|
||||||
# initialize parent class
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
# Store parameters
|
|
||||||
self.num_states = num_states
|
|
||||||
self.obs_groups = obs_groups
|
|
||||||
self.num_outputs = num_outputs
|
|
||||||
self.initial_weight = weight
|
|
||||||
self.device = device
|
|
||||||
self.state_normalization = state_normalization
|
|
||||||
self.reward_normalization = reward_normalization
|
|
||||||
|
|
||||||
# Normalization of input gates
|
|
||||||
if state_normalization:
|
|
||||||
self.state_normalizer = EmpiricalNormalization(shape=[self.num_states], until=1.0e8).to(self.device)
|
|
||||||
else:
|
|
||||||
self.state_normalizer = torch.nn.Identity()
|
|
||||||
# Normalization of intrinsic reward
|
|
||||||
if reward_normalization:
|
|
||||||
self.reward_normalizer = EmpiricalDiscountedVariationNormalization(shape=[], until=1.0e8).to(self.device)
|
|
||||||
else:
|
|
||||||
self.reward_normalizer = torch.nn.Identity()
|
|
||||||
|
|
||||||
# counter for the number of updates
|
|
||||||
self.update_counter = 0
|
|
||||||
|
|
||||||
# resolve weight schedule
|
|
||||||
if weight_schedule is not None:
|
|
||||||
self.weight_scheduler_params = weight_schedule
|
|
||||||
self.weight_scheduler = getattr(self, f"_{weight_schedule['mode']}_weight_schedule")
|
|
||||||
else:
|
|
||||||
self.weight_scheduler = None
|
|
||||||
# Create network architecture
|
|
||||||
self.predictor = MLP(num_states, num_outputs, predictor_hidden_dims, activation).to(self.device)
|
|
||||||
self.target = MLP(num_states, num_outputs, target_hidden_dims, activation).to(self.device)
|
|
||||||
|
|
||||||
# make target network not trainable
|
|
||||||
self.target.eval()
|
|
||||||
|
|
||||||
def get_intrinsic_reward(self, obs) -> torch.Tensor:
|
|
||||||
# Note: the counter is updated number of env steps per learning iteration
|
|
||||||
self.update_counter += 1
|
|
||||||
# Extract the rnd state from the observation
|
|
||||||
rnd_state = self.get_rnd_state(obs)
|
|
||||||
rnd_state = self.state_normalizer(rnd_state)
|
|
||||||
# Obtain the embedding of the rnd state from the target and predictor networks
|
|
||||||
target_embedding = self.target(rnd_state).detach()
|
|
||||||
predictor_embedding = self.predictor(rnd_state).detach()
|
|
||||||
# Compute the intrinsic reward as the distance between the embeddings
|
|
||||||
intrinsic_reward = torch.linalg.norm(target_embedding - predictor_embedding, dim=1)
|
|
||||||
# Normalize intrinsic reward
|
|
||||||
intrinsic_reward = self.reward_normalizer(intrinsic_reward)
|
|
||||||
|
|
||||||
# Check the weight schedule
|
|
||||||
if self.weight_scheduler is not None:
|
|
||||||
self.weight = self.weight_scheduler(step=self.update_counter, **self.weight_scheduler_params)
|
|
||||||
else:
|
|
||||||
self.weight = self.initial_weight
|
|
||||||
# Scale intrinsic reward
|
|
||||||
intrinsic_reward *= self.weight
|
|
||||||
|
|
||||||
return intrinsic_reward
|
|
||||||
|
|
||||||
def forward(self, *args, **kwargs):
|
|
||||||
raise RuntimeError("Forward method is not implemented. Use get_intrinsic_reward instead.")
|
|
||||||
|
|
||||||
def train(self, mode: bool = True):
|
|
||||||
# sets module into training mode
|
|
||||||
self.predictor.train(mode)
|
|
||||||
if self.state_normalization:
|
|
||||||
self.state_normalizer.train(mode)
|
|
||||||
if self.reward_normalization:
|
|
||||||
self.reward_normalizer.train(mode)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def eval(self):
|
|
||||||
return self.train(False)
|
|
||||||
|
|
||||||
def get_rnd_state(self, obs):
|
|
||||||
obs_list = []
|
|
||||||
for obs_group in self.obs_groups["rnd_state"]:
|
|
||||||
obs_list.append(obs[obs_group])
|
|
||||||
return torch.cat(obs_list, dim=-1)
|
|
||||||
|
|
||||||
def update_normalization(self, obs):
|
|
||||||
# Normalize the state
|
|
||||||
if self.state_normalization:
|
|
||||||
rnd_state = self.get_rnd_state(obs)
|
|
||||||
self.state_normalizer.update(rnd_state)
|
|
||||||
|
|
||||||
"""
|
|
||||||
Different weight schedules.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _constant_weight_schedule(self, step: int, **kwargs):
|
|
||||||
return self.initial_weight
|
|
||||||
|
|
||||||
def _step_weight_schedule(self, step: int, final_step: int, final_value: float, **kwargs):
|
|
||||||
return self.initial_weight if step < final_step else final_value
|
|
||||||
|
|
||||||
def _linear_weight_schedule(self, step: int, initial_step: int, final_step: int, final_value: float, **kwargs):
|
|
||||||
if step < initial_step:
|
|
||||||
return self.initial_weight
|
|
||||||
elif step > final_step:
|
|
||||||
return final_value
|
|
||||||
else:
|
|
||||||
return self.initial_weight + (final_value - self.initial_weight) * (step - initial_step) / (
|
|
||||||
final_step - initial_step
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_rnd_config(alg_cfg, obs, obs_groups, env):
|
|
||||||
"""Resolve the RND configuration.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
alg_cfg: The algorithm configuration dictionary.
|
|
||||||
obs: The observation dictionary.
|
|
||||||
obs_groups: The observation groups dictionary.
|
|
||||||
env: The environment.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The resolved algorithm configuration dictionary.
|
|
||||||
"""
|
|
||||||
# resolve dimension of rnd gated state
|
|
||||||
if "rnd_cfg" in alg_cfg and alg_cfg["rnd_cfg"] is not None:
|
|
||||||
# get dimension of rnd gated state
|
|
||||||
num_rnd_state = 0
|
|
||||||
for obs_group in obs_groups["rnd_state"]:
|
|
||||||
assert len(obs[obs_group].shape) == 2, "The RND module only supports 1D observations."
|
|
||||||
num_rnd_state += obs[obs_group].shape[-1]
|
|
||||||
# add rnd gated state to config
|
|
||||||
alg_cfg["rnd_cfg"]["num_states"] = num_rnd_state
|
|
||||||
alg_cfg["rnd_cfg"]["obs_groups"] = obs_groups
|
|
||||||
# scale down the rnd weight with timestep
|
|
||||||
alg_cfg["rnd_cfg"]["weight"] *= env.unwrapped.step_dt
|
|
||||||
return alg_cfg
|
|
||||||
|
|
@ -1,206 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
from torch.distributions import Normal
|
|
||||||
|
|
||||||
from rsl_rl.networks import MLP, EmpiricalNormalization
|
|
||||||
|
|
||||||
|
|
||||||
class StudentTeacher(nn.Module):
|
|
||||||
is_recurrent = False
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
obs,
|
|
||||||
obs_groups,
|
|
||||||
num_actions,
|
|
||||||
student_obs_normalization=False,
|
|
||||||
teacher_obs_normalization=False,
|
|
||||||
student_hidden_dims=[256, 256, 256],
|
|
||||||
teacher_hidden_dims=[256, 256, 256],
|
|
||||||
activation="elu",
|
|
||||||
init_noise_std=0.1,
|
|
||||||
noise_std_type: str = "scalar",
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
if kwargs:
|
|
||||||
print(
|
|
||||||
"StudentTeacher.__init__ got unexpected arguments, which will be ignored: "
|
|
||||||
+ str([key for key in kwargs.keys()])
|
|
||||||
)
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.loaded_teacher = False # indicates if teacher has been loaded
|
|
||||||
|
|
||||||
# get the observation dimensions
|
|
||||||
self.obs_groups = obs_groups
|
|
||||||
num_student_obs = 0
|
|
||||||
for obs_group in obs_groups["policy"]:
|
|
||||||
assert len(obs[obs_group].shape) == 2, "The StudentTeacher module only supports 1D observations."
|
|
||||||
num_student_obs += obs[obs_group].shape[-1]
|
|
||||||
num_teacher_obs = 0
|
|
||||||
for obs_group in obs_groups["teacher"]:
|
|
||||||
assert len(obs[obs_group].shape) == 2, "The StudentTeacher module only supports 1D observations."
|
|
||||||
num_teacher_obs += obs[obs_group].shape[-1]
|
|
||||||
|
|
||||||
# student
|
|
||||||
self.student = MLP(num_student_obs, num_actions, student_hidden_dims, activation)
|
|
||||||
|
|
||||||
# student observation normalization
|
|
||||||
self.student_obs_normalization = student_obs_normalization
|
|
||||||
if student_obs_normalization:
|
|
||||||
self.student_obs_normalizer = EmpiricalNormalization(num_student_obs)
|
|
||||||
else:
|
|
||||||
self.student_obs_normalizer = torch.nn.Identity()
|
|
||||||
|
|
||||||
print(f"Student MLP: {self.student}")
|
|
||||||
|
|
||||||
# teacher
|
|
||||||
self.teacher = MLP(num_teacher_obs, num_actions, teacher_hidden_dims, activation)
|
|
||||||
self.teacher.eval()
|
|
||||||
|
|
||||||
# teacher observation normalization
|
|
||||||
self.teacher_obs_normalization = teacher_obs_normalization
|
|
||||||
if teacher_obs_normalization:
|
|
||||||
self.teacher_obs_normalizer = EmpiricalNormalization(num_teacher_obs)
|
|
||||||
else:
|
|
||||||
self.teacher_obs_normalizer = torch.nn.Identity()
|
|
||||||
|
|
||||||
print(f"Teacher MLP: {self.teacher}")
|
|
||||||
|
|
||||||
# action noise
|
|
||||||
self.noise_std_type = noise_std_type
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
self.std = nn.Parameter(init_noise_std * torch.ones(num_actions))
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
self.log_std = nn.Parameter(torch.log(init_noise_std * torch.ones(num_actions)))
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
|
|
||||||
# action distribution (populated in update_distribution)
|
|
||||||
self.distribution = None
|
|
||||||
# disable args validation for speedup
|
|
||||||
Normal.set_default_validate_args(False)
|
|
||||||
|
|
||||||
def reset(self, dones=None, hidden_states=None):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def forward(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@property
|
|
||||||
def action_mean(self):
|
|
||||||
return self.distribution.mean
|
|
||||||
|
|
||||||
@property
|
|
||||||
def action_std(self):
|
|
||||||
return self.distribution.stddev
|
|
||||||
|
|
||||||
@property
|
|
||||||
def entropy(self):
|
|
||||||
return self.distribution.entropy().sum(dim=-1)
|
|
||||||
|
|
||||||
def update_distribution(self, obs):
|
|
||||||
# compute mean
|
|
||||||
mean = self.student(obs)
|
|
||||||
# compute standard deviation
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
std = self.std.expand_as(mean)
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
std = torch.exp(self.log_std).expand_as(mean)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
# create distribution
|
|
||||||
self.distribution = Normal(mean, std)
|
|
||||||
|
|
||||||
def act(self, obs):
|
|
||||||
obs = self.get_student_obs(obs)
|
|
||||||
obs = self.student_obs_normalizer(obs)
|
|
||||||
self.update_distribution(obs)
|
|
||||||
return self.distribution.sample()
|
|
||||||
|
|
||||||
def act_inference(self, obs):
|
|
||||||
obs = self.get_student_obs(obs)
|
|
||||||
obs = self.student_obs_normalizer(obs)
|
|
||||||
return self.student(obs)
|
|
||||||
|
|
||||||
def evaluate(self, obs):
|
|
||||||
obs = self.get_teacher_obs(obs)
|
|
||||||
obs = self.teacher_obs_normalizer(obs)
|
|
||||||
with torch.no_grad():
|
|
||||||
return self.teacher(obs)
|
|
||||||
|
|
||||||
def get_student_obs(self, obs):
|
|
||||||
obs_list = []
|
|
||||||
for obs_group in self.obs_groups["policy"]:
|
|
||||||
obs_list.append(obs[obs_group])
|
|
||||||
return torch.cat(obs_list, dim=-1)
|
|
||||||
|
|
||||||
def get_teacher_obs(self, obs):
|
|
||||||
obs_list = []
|
|
||||||
for obs_group in self.obs_groups["teacher"]:
|
|
||||||
obs_list.append(obs[obs_group])
|
|
||||||
return torch.cat(obs_list, dim=-1)
|
|
||||||
|
|
||||||
def get_hidden_states(self):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def detach_hidden_states(self, dones=None):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def train(self, mode=True):
|
|
||||||
super().train(mode)
|
|
||||||
# make sure teacher is in eval mode
|
|
||||||
self.teacher.eval()
|
|
||||||
self.teacher_obs_normalizer.eval()
|
|
||||||
|
|
||||||
def update_normalization(self, obs):
|
|
||||||
if self.student_obs_normalization:
|
|
||||||
student_obs = self.get_student_obs(obs)
|
|
||||||
self.student_obs_normalizer.update(student_obs)
|
|
||||||
|
|
||||||
def load_state_dict(self, state_dict, strict=True):
|
|
||||||
"""Load the parameters of the student and teacher networks.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
state_dict (dict): State dictionary of the model.
|
|
||||||
strict (bool): Whether to strictly enforce that the keys in state_dict match the keys returned by this
|
|
||||||
module's state_dict() function.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: Whether this training resumes a previous training. This flag is used by the `load()` function of
|
|
||||||
`OnPolicyRunner` to determine how to load further parameters.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# check if state_dict contains teacher and student or just teacher parameters
|
|
||||||
if any("actor" in key for key in state_dict.keys()): # loading parameters from rl training
|
|
||||||
# rename keys to match teacher and remove critic parameters
|
|
||||||
teacher_state_dict = {}
|
|
||||||
teacher_obs_normalizer_state_dict = {}
|
|
||||||
for key, value in state_dict.items():
|
|
||||||
if "actor." in key:
|
|
||||||
teacher_state_dict[key.replace("actor.", "")] = value
|
|
||||||
if "actor_obs_normalizer." in key:
|
|
||||||
teacher_obs_normalizer_state_dict[key.replace("actor_obs_normalizer.", "")] = value
|
|
||||||
self.teacher.load_state_dict(teacher_state_dict, strict=strict)
|
|
||||||
self.teacher_obs_normalizer.load_state_dict(teacher_obs_normalizer_state_dict, strict=strict)
|
|
||||||
# set flag for successfully loading the parameters
|
|
||||||
self.loaded_teacher = True
|
|
||||||
self.teacher.eval()
|
|
||||||
self.teacher_obs_normalizer.eval()
|
|
||||||
return False # training does not resume
|
|
||||||
elif any("student" in key for key in state_dict.keys()): # loading parameters from distillation training
|
|
||||||
super().load_state_dict(state_dict, strict=strict)
|
|
||||||
# set flag for successfully loading the parameters
|
|
||||||
self.loaded_teacher = True
|
|
||||||
self.teacher.eval()
|
|
||||||
self.teacher_obs_normalizer.eval()
|
|
||||||
return True # training resumes
|
|
||||||
else:
|
|
||||||
raise ValueError("state_dict does not contain student or teacher parameters")
|
|
||||||
|
|
@ -1,249 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
import warnings
|
|
||||||
from torch.distributions import Normal
|
|
||||||
|
|
||||||
from rsl_rl.networks import MLP, EmpiricalNormalization, Memory
|
|
||||||
|
|
||||||
|
|
||||||
class StudentTeacherRecurrent(nn.Module):
|
|
||||||
is_recurrent = True
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
obs,
|
|
||||||
obs_groups,
|
|
||||||
num_actions,
|
|
||||||
student_obs_normalization=False,
|
|
||||||
teacher_obs_normalization=False,
|
|
||||||
student_hidden_dims=[256, 256, 256],
|
|
||||||
teacher_hidden_dims=[256, 256, 256],
|
|
||||||
activation="elu",
|
|
||||||
init_noise_std=0.1,
|
|
||||||
noise_std_type: str = "scalar",
|
|
||||||
rnn_type="lstm",
|
|
||||||
rnn_hidden_dim=256,
|
|
||||||
rnn_num_layers=1,
|
|
||||||
teacher_recurrent=False,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
if "rnn_hidden_size" in kwargs:
|
|
||||||
warnings.warn(
|
|
||||||
"The argument `rnn_hidden_size` is deprecated and will be removed in a future version. "
|
|
||||||
"Please use `rnn_hidden_dim` instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
if rnn_hidden_dim == 256: # Only override if the new argument is at its default
|
|
||||||
rnn_hidden_dim = kwargs.pop("rnn_hidden_size")
|
|
||||||
if kwargs:
|
|
||||||
print(
|
|
||||||
"StudentTeacherRecurrent.__init__ got unexpected arguments, which will be ignored: "
|
|
||||||
+ str(kwargs.keys()),
|
|
||||||
)
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.loaded_teacher = False # indicates if teacher has been loaded
|
|
||||||
self.teacher_recurrent = teacher_recurrent # indicates if teacher is recurrent too
|
|
||||||
|
|
||||||
# get the observation dimensions
|
|
||||||
self.obs_groups = obs_groups
|
|
||||||
num_student_obs = 0
|
|
||||||
for obs_group in obs_groups["policy"]:
|
|
||||||
assert len(obs[obs_group].shape) == 2, "The StudentTeacher module only supports 1D observations."
|
|
||||||
num_student_obs += obs[obs_group].shape[-1]
|
|
||||||
num_teacher_obs = 0
|
|
||||||
for obs_group in obs_groups["teacher"]:
|
|
||||||
assert len(obs[obs_group].shape) == 2, "The StudentTeacher module only supports 1D observations."
|
|
||||||
num_teacher_obs += obs[obs_group].shape[-1]
|
|
||||||
|
|
||||||
# student
|
|
||||||
self.memory_s = Memory(num_student_obs, type=rnn_type, num_layers=rnn_num_layers, hidden_size=rnn_hidden_dim)
|
|
||||||
self.student = MLP(rnn_hidden_dim, num_actions, student_hidden_dims, activation)
|
|
||||||
|
|
||||||
# student observation normalization
|
|
||||||
self.student_obs_normalization = student_obs_normalization
|
|
||||||
if student_obs_normalization:
|
|
||||||
self.student_obs_normalizer = EmpiricalNormalization(num_student_obs)
|
|
||||||
else:
|
|
||||||
self.student_obs_normalizer = torch.nn.Identity()
|
|
||||||
|
|
||||||
print(f"Student RNN: {self.memory_s}")
|
|
||||||
print(f"Student MLP: {self.student}")
|
|
||||||
|
|
||||||
# teacher
|
|
||||||
if self.teacher_recurrent:
|
|
||||||
self.memory_t = Memory(
|
|
||||||
num_teacher_obs, type=rnn_type, num_layers=rnn_num_layers, hidden_size=rnn_hidden_dim
|
|
||||||
)
|
|
||||||
num_teacher_obs = rnn_hidden_dim
|
|
||||||
self.teacher = MLP(num_teacher_obs, num_actions, teacher_hidden_dims, activation)
|
|
||||||
|
|
||||||
# teacher observation normalization
|
|
||||||
self.teacher_obs_normalization = teacher_obs_normalization
|
|
||||||
if teacher_obs_normalization:
|
|
||||||
self.teacher_obs_normalizer = EmpiricalNormalization(num_teacher_obs)
|
|
||||||
else:
|
|
||||||
self.teacher_obs_normalizer = torch.nn.Identity()
|
|
||||||
|
|
||||||
if self.teacher_recurrent:
|
|
||||||
print(f"Teacher RNN: {self.memory_t}")
|
|
||||||
print(f"Teacher MLP: {self.teacher}")
|
|
||||||
|
|
||||||
# action noise
|
|
||||||
self.noise_std_type = noise_std_type
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
self.std = nn.Parameter(init_noise_std * torch.ones(num_actions))
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
self.log_std = nn.Parameter(torch.log(init_noise_std * torch.ones(num_actions)))
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
|
|
||||||
# action distribution (populated in update_distribution)
|
|
||||||
self.distribution = None
|
|
||||||
# disable args validation for speedup
|
|
||||||
Normal.set_default_validate_args(False)
|
|
||||||
|
|
||||||
def reset(self, dones=None, hidden_states=None):
|
|
||||||
if hidden_states is None:
|
|
||||||
hidden_states = (None, None)
|
|
||||||
self.memory_s.reset(dones, hidden_states[0])
|
|
||||||
if self.teacher_recurrent:
|
|
||||||
self.memory_t.reset(dones, hidden_states[1])
|
|
||||||
|
|
||||||
def forward(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@property
|
|
||||||
def action_mean(self):
|
|
||||||
return self.distribution.mean
|
|
||||||
|
|
||||||
@property
|
|
||||||
def action_std(self):
|
|
||||||
return self.distribution.stddev
|
|
||||||
|
|
||||||
@property
|
|
||||||
def entropy(self):
|
|
||||||
return self.distribution.entropy().sum(dim=-1)
|
|
||||||
|
|
||||||
def update_distribution(self, obs):
|
|
||||||
# compute mean
|
|
||||||
mean = self.student(obs)
|
|
||||||
# compute standard deviation
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
std = self.std.expand_as(mean)
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
std = torch.exp(self.log_std).expand_as(mean)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
# create distribution
|
|
||||||
self.distribution = Normal(mean, std)
|
|
||||||
|
|
||||||
def act(self, obs):
|
|
||||||
obs = self.get_student_obs(obs)
|
|
||||||
obs = self.student_obs_normalizer(obs)
|
|
||||||
out_mem = self.memory_s(obs).squeeze(0)
|
|
||||||
self.update_distribution(out_mem)
|
|
||||||
return self.distribution.sample()
|
|
||||||
|
|
||||||
def act_inference(self, obs):
|
|
||||||
obs = self.get_student_obs(obs)
|
|
||||||
obs = self.student_obs_normalizer(obs)
|
|
||||||
out_mem = self.memory_s(obs).squeeze(0)
|
|
||||||
return self.student(out_mem)
|
|
||||||
|
|
||||||
def evaluate(self, obs):
|
|
||||||
obs = self.get_teacher_obs(obs)
|
|
||||||
obs = self.teacher_obs_normalizer(obs)
|
|
||||||
with torch.no_grad():
|
|
||||||
if self.teacher_recurrent:
|
|
||||||
self.memory_t.eval()
|
|
||||||
obs = self.memory_t(obs).squeeze(0)
|
|
||||||
return self.teacher(obs)
|
|
||||||
|
|
||||||
def get_student_obs(self, obs):
|
|
||||||
obs_list = []
|
|
||||||
for obs_group in self.obs_groups["policy"]:
|
|
||||||
obs_list.append(obs[obs_group])
|
|
||||||
return torch.cat(obs_list, dim=-1)
|
|
||||||
|
|
||||||
def get_teacher_obs(self, obs):
|
|
||||||
obs_list = []
|
|
||||||
for obs_group in self.obs_groups["teacher"]:
|
|
||||||
obs_list.append(obs[obs_group])
|
|
||||||
return torch.cat(obs_list, dim=-1)
|
|
||||||
|
|
||||||
def get_hidden_states(self):
|
|
||||||
if self.teacher_recurrent:
|
|
||||||
return self.memory_s.hidden_states, self.memory_t.hidden_states
|
|
||||||
else:
|
|
||||||
return self.memory_s.hidden_states, None
|
|
||||||
|
|
||||||
def detach_hidden_states(self, dones=None):
|
|
||||||
self.memory_s.detach_hidden_states(dones)
|
|
||||||
if self.teacher_recurrent:
|
|
||||||
self.memory_t.detach_hidden_states(dones)
|
|
||||||
|
|
||||||
def train(self, mode=True):
|
|
||||||
super().train(mode)
|
|
||||||
# make sure teacher is in eval mode
|
|
||||||
self.teacher.eval()
|
|
||||||
self.teacher_obs_normalizer.eval()
|
|
||||||
|
|
||||||
def update_normalization(self, obs):
|
|
||||||
if self.student_obs_normalization:
|
|
||||||
student_obs = self.get_student_obs(obs)
|
|
||||||
self.student_obs_normalizer.update(student_obs)
|
|
||||||
|
|
||||||
def load_state_dict(self, state_dict, strict=True):
|
|
||||||
"""Load the parameters of the student and teacher networks.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
state_dict (dict): State dictionary of the model.
|
|
||||||
strict (bool): Whether to strictly enforce that the keys in state_dict match the keys returned by this
|
|
||||||
module's state_dict() function.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: Whether this training resumes a previous training. This flag is used by the `load()` function of
|
|
||||||
`OnPolicyRunner` to determine how to load further parameters.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# check if state_dict contains teacher and student or just teacher parameters
|
|
||||||
if any("actor" in key for key in state_dict.keys()): # loading parameters from rl training
|
|
||||||
# rename keys to match teacher and remove critic parameters
|
|
||||||
teacher_state_dict = {}
|
|
||||||
teacher_obs_normalizer_state_dict = {}
|
|
||||||
for key, value in state_dict.items():
|
|
||||||
if "actor." in key:
|
|
||||||
teacher_state_dict[key.replace("actor.", "")] = value
|
|
||||||
if "actor_obs_normalizer." in key:
|
|
||||||
teacher_obs_normalizer_state_dict[key.replace("actor_obs_normalizer.", "")] = value
|
|
||||||
self.teacher.load_state_dict(teacher_state_dict, strict=strict)
|
|
||||||
self.teacher_obs_normalizer.load_state_dict(teacher_obs_normalizer_state_dict, strict=strict)
|
|
||||||
# also load recurrent memory if teacher is recurrent
|
|
||||||
if self.teacher_recurrent:
|
|
||||||
memory_t_state_dict = {}
|
|
||||||
for key, value in state_dict.items():
|
|
||||||
if "memory_a." in key:
|
|
||||||
memory_t_state_dict[key.replace("memory_a.", "")] = value
|
|
||||||
self.memory_t.load_state_dict(memory_t_state_dict, strict=strict)
|
|
||||||
# set flag for successfully loading the parameters
|
|
||||||
self.loaded_teacher = True
|
|
||||||
self.teacher.eval()
|
|
||||||
self.teacher_obs_normalizer.eval()
|
|
||||||
return False # training does not resume
|
|
||||||
elif any("student" in key for key in state_dict.keys()): # loading parameters from distillation training
|
|
||||||
super().load_state_dict(state_dict, strict=strict)
|
|
||||||
# set flag for successfully loading the parameters
|
|
||||||
self.loaded_teacher = True
|
|
||||||
self.teacher.eval()
|
|
||||||
self.teacher_obs_normalizer.eval()
|
|
||||||
return True # training resumes
|
|
||||||
else:
|
|
||||||
raise ValueError("state_dict does not contain student or teacher parameters")
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_symmetry_config(alg_cfg, env):
|
|
||||||
"""Resolve the symmetry configuration.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
alg_cfg: The algorithm configuration dictionary.
|
|
||||||
env: The environment.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The resolved algorithm configuration dictionary.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# if using symmetry then pass the environment config object
|
|
||||||
if "symmetry_cfg" in alg_cfg and alg_cfg["symmetry_cfg"] is not None:
|
|
||||||
# this is used by the symmetry function for handling different observation terms
|
|
||||||
alg_cfg["symmetry_cfg"]["_env"] = env
|
|
||||||
return alg_cfg
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
"""Definitions for components of modules."""
|
|
||||||
|
|
||||||
from .memory import Memory
|
|
||||||
from .mlp import MLP
|
|
||||||
from .normalization import EmpiricalDiscountedVariationNormalization, EmpiricalNormalization
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch.nn as nn
|
|
||||||
|
|
||||||
from rsl_rl.utils import unpad_trajectories
|
|
||||||
|
|
||||||
|
|
||||||
class Memory(nn.Module):
|
|
||||||
"""Memory module for recurrent networks.
|
|
||||||
|
|
||||||
This module is used to store the hidden states of the policy.
|
|
||||||
Currently only supports GRU and LSTM.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, input_size, type="lstm", num_layers=1, hidden_size=256):
|
|
||||||
super().__init__()
|
|
||||||
# RNN
|
|
||||||
rnn_cls = nn.GRU if type.lower() == "gru" else nn.LSTM
|
|
||||||
self.rnn = rnn_cls(input_size=input_size, hidden_size=hidden_size, num_layers=num_layers)
|
|
||||||
self.hidden_states = None
|
|
||||||
|
|
||||||
def forward(self, input, masks=None, hidden_states=None):
|
|
||||||
batch_mode = masks is not None
|
|
||||||
if batch_mode:
|
|
||||||
# batch mode: needs saved hidden states
|
|
||||||
if hidden_states is None:
|
|
||||||
raise ValueError("Hidden states not passed to memory module during policy update")
|
|
||||||
out, _ = self.rnn(input, hidden_states)
|
|
||||||
out = unpad_trajectories(out, masks)
|
|
||||||
else:
|
|
||||||
# inference/distillation mode: uses hidden states of last step
|
|
||||||
out, self.hidden_states = self.rnn(input.unsqueeze(0), self.hidden_states)
|
|
||||||
return out
|
|
||||||
|
|
||||||
def reset(self, dones=None, hidden_states=None):
|
|
||||||
if dones is None: # reset all hidden states
|
|
||||||
if hidden_states is None:
|
|
||||||
self.hidden_states = None
|
|
||||||
else:
|
|
||||||
self.hidden_states = hidden_states
|
|
||||||
elif self.hidden_states is not None: # reset hidden states of done environments
|
|
||||||
if hidden_states is None:
|
|
||||||
if isinstance(self.hidden_states, tuple): # tuple in case of LSTM
|
|
||||||
for hidden_state in self.hidden_states:
|
|
||||||
hidden_state[..., dones == 1, :] = 0.0
|
|
||||||
else:
|
|
||||||
self.hidden_states[..., dones == 1, :] = 0.0
|
|
||||||
else:
|
|
||||||
NotImplementedError(
|
|
||||||
"Resetting hidden states of done environments with custom hidden states is not implemented"
|
|
||||||
)
|
|
||||||
|
|
||||||
def detach_hidden_states(self, dones=None):
|
|
||||||
if self.hidden_states is not None:
|
|
||||||
if dones is None: # detach all hidden states
|
|
||||||
if isinstance(self.hidden_states, tuple): # tuple in case of LSTM
|
|
||||||
self.hidden_states = tuple(hidden_state.detach() for hidden_state in self.hidden_states)
|
|
||||||
else:
|
|
||||||
self.hidden_states = self.hidden_states.detach()
|
|
||||||
else: # detach hidden states of done environments
|
|
||||||
if isinstance(self.hidden_states, tuple): # tuple in case of LSTM
|
|
||||||
for hidden_state in self.hidden_states:
|
|
||||||
hidden_state[..., dones == 1, :] = hidden_state[..., dones == 1, :].detach()
|
|
||||||
else:
|
|
||||||
self.hidden_states[..., dones == 1, :] = self.hidden_states[..., dones == 1, :].detach()
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
from functools import reduce
|
|
||||||
|
|
||||||
from rsl_rl.utils import resolve_nn_activation
|
|
||||||
|
|
||||||
|
|
||||||
class MLP(nn.Sequential):
|
|
||||||
"""Multi-layer perceptron.
|
|
||||||
|
|
||||||
The MLP network is a sequence of linear layers and activation functions. The
|
|
||||||
last layer is a linear layer that outputs the desired dimension unless the
|
|
||||||
last activation function is specified.
|
|
||||||
|
|
||||||
It provides additional conveniences:
|
|
||||||
|
|
||||||
- If the hidden dimensions have a value of ``-1``, the dimension is inferred
|
|
||||||
from the input dimension.
|
|
||||||
- If the output dimension is a tuple, the output is reshaped to the desired
|
|
||||||
shape.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
input_dim: int,
|
|
||||||
output_dim: int | tuple[int] | list[int],
|
|
||||||
hidden_dims: tuple[int] | list[int],
|
|
||||||
activation: str = "elu",
|
|
||||||
last_activation: str | None = None,
|
|
||||||
):
|
|
||||||
"""Initialize the MLP.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
input_dim: Dimension of the input.
|
|
||||||
output_dim: Dimension of the output.
|
|
||||||
hidden_dims: Dimensions of the hidden layers. A value of ``-1`` indicates
|
|
||||||
that the dimension should be inferred from the input dimension.
|
|
||||||
activation: Activation function. Defaults to "elu".
|
|
||||||
last_activation: Activation function of the last layer. Defaults to None,
|
|
||||||
in which case the last layer is linear.
|
|
||||||
"""
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
# resolve activation functions
|
|
||||||
activation_mod = resolve_nn_activation(activation)
|
|
||||||
last_activation_mod = resolve_nn_activation(last_activation) if last_activation is not None else None
|
|
||||||
# resolve number of hidden dims if they are -1
|
|
||||||
hidden_dims_processed = [input_dim if dim == -1 else dim for dim in hidden_dims]
|
|
||||||
|
|
||||||
# create layers sequentially
|
|
||||||
layers = []
|
|
||||||
layers.append(nn.Linear(input_dim, hidden_dims_processed[0]))
|
|
||||||
layers.append(activation_mod)
|
|
||||||
|
|
||||||
for layer_index in range(len(hidden_dims_processed) - 1):
|
|
||||||
layers.append(nn.Linear(hidden_dims_processed[layer_index], hidden_dims_processed[layer_index + 1]))
|
|
||||||
layers.append(activation_mod)
|
|
||||||
|
|
||||||
# add last layer
|
|
||||||
if isinstance(output_dim, int):
|
|
||||||
layers.append(nn.Linear(hidden_dims_processed[-1], output_dim))
|
|
||||||
else:
|
|
||||||
# compute the total output dimension
|
|
||||||
total_out_dim = reduce(lambda x, y: x * y, output_dim)
|
|
||||||
# add a layer to reshape the output to the desired shape
|
|
||||||
layers.append(nn.Linear(hidden_dims_processed[-1], total_out_dim))
|
|
||||||
layers.append(nn.Unflatten(dim=-1, unflattened_size=output_dim))
|
|
||||||
|
|
||||||
# add last activation function if specified
|
|
||||||
if last_activation_mod is not None:
|
|
||||||
layers.append(last_activation_mod)
|
|
||||||
|
|
||||||
# register the layers
|
|
||||||
for idx, layer in enumerate(layers):
|
|
||||||
self.add_module(f"{idx}", layer)
|
|
||||||
|
|
||||||
def init_weights(self, scales: float | tuple[float]):
|
|
||||||
"""Initialize the weights of the MLP.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
scales: Scale factor for the weights.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_scale(idx) -> float:
|
|
||||||
"""Get the scale factor for the weights of the MLP.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
idx: Index of the layer.
|
|
||||||
"""
|
|
||||||
return scales[idx] if isinstance(scales, (list, tuple)) else scales
|
|
||||||
|
|
||||||
# initialize the weights
|
|
||||||
for idx, module in enumerate(self):
|
|
||||||
if isinstance(module, nn.Linear):
|
|
||||||
nn.init.orthogonal_(module.weight, gain=get_scale(idx))
|
|
||||||
nn.init.zeros_(module.bias)
|
|
||||||
|
|
||||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
||||||
"""Forward pass of the MLP.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
x: Input tensor.
|
|
||||||
"""
|
|
||||||
for layer in self:
|
|
||||||
x = layer(x)
|
|
||||||
return x
|
|
||||||
|
|
||||||
def reset(self, dones=None, hidden_states=None):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def detach_hidden_states(self, dones=None):
|
|
||||||
pass
|
|
||||||
|
|
@ -1,130 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
# Copyright (c) 2020 Preferred Networks, Inc.
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch
|
|
||||||
from torch import nn
|
|
||||||
|
|
||||||
|
|
||||||
class EmpiricalNormalization(nn.Module):
|
|
||||||
"""Normalize mean and variance of values based on empirical values."""
|
|
||||||
|
|
||||||
def __init__(self, shape, eps=1e-2, until=None):
|
|
||||||
"""Initialize EmpiricalNormalization module.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
shape (int or tuple of int): Shape of input values except batch axis.
|
|
||||||
eps (float): Small value for stability.
|
|
||||||
until (int or None): If this arg is specified, the module learns input values until the sum of batch sizes
|
|
||||||
exceeds it.
|
|
||||||
|
|
||||||
Note: The normalization parameters are computed over the whole batch, not for each environment separately.
|
|
||||||
"""
|
|
||||||
super().__init__()
|
|
||||||
self.eps = eps
|
|
||||||
self.until = until
|
|
||||||
self.register_buffer("_mean", torch.zeros(shape).unsqueeze(0))
|
|
||||||
self.register_buffer("_var", torch.ones(shape).unsqueeze(0))
|
|
||||||
self.register_buffer("_std", torch.ones(shape).unsqueeze(0))
|
|
||||||
self.register_buffer("count", torch.tensor(0, dtype=torch.long))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def mean(self):
|
|
||||||
return self._mean.squeeze(0).clone()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def std(self):
|
|
||||||
return self._std.squeeze(0).clone()
|
|
||||||
|
|
||||||
def forward(self, x):
|
|
||||||
"""Normalize mean and variance of values based on empirical values."""
|
|
||||||
|
|
||||||
return (x - self._mean) / (self._std + self.eps)
|
|
||||||
|
|
||||||
@torch.jit.unused
|
|
||||||
def update(self, x):
|
|
||||||
"""Learn input values without computing the output values of them"""
|
|
||||||
|
|
||||||
if not self.training:
|
|
||||||
return
|
|
||||||
if self.until is not None and self.count >= self.until:
|
|
||||||
return
|
|
||||||
|
|
||||||
count_x = x.shape[0]
|
|
||||||
self.count += count_x
|
|
||||||
rate = count_x / self.count
|
|
||||||
var_x = torch.var(x, dim=0, unbiased=False, keepdim=True)
|
|
||||||
mean_x = torch.mean(x, dim=0, keepdim=True)
|
|
||||||
delta_mean = mean_x - self._mean
|
|
||||||
self._mean += rate * delta_mean
|
|
||||||
self._var += rate * (var_x - self._var + delta_mean * (mean_x - self._mean))
|
|
||||||
self._std = torch.sqrt(self._var)
|
|
||||||
|
|
||||||
@torch.jit.unused
|
|
||||||
def inverse(self, y):
|
|
||||||
"""De-normalize values based on empirical values."""
|
|
||||||
|
|
||||||
return y * (self._std + self.eps) + self._mean
|
|
||||||
|
|
||||||
|
|
||||||
class EmpiricalDiscountedVariationNormalization(nn.Module):
|
|
||||||
"""Reward normalization from Pathak's large scale study on PPO.
|
|
||||||
|
|
||||||
Reward normalization. Since the reward function is non-stationary, it is useful to normalize
|
|
||||||
the scale of the rewards so that the value function can learn quickly. We did this by dividing
|
|
||||||
the rewards by a running estimate of the standard deviation of the sum of discounted rewards.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, shape, eps=1e-2, gamma=0.99, until=None):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.emp_norm = EmpiricalNormalization(shape, eps, until)
|
|
||||||
self.disc_avg = _DiscountedAverage(gamma)
|
|
||||||
|
|
||||||
def forward(self, rew):
|
|
||||||
if self.training:
|
|
||||||
# update discounted rewards
|
|
||||||
avg = self.disc_avg.update(rew)
|
|
||||||
# update moments from discounted rewards
|
|
||||||
self.emp_norm.update(avg)
|
|
||||||
|
|
||||||
# normalize rewards with the empirical std
|
|
||||||
if self.emp_norm._std > 0:
|
|
||||||
return rew / self.emp_norm._std
|
|
||||||
else:
|
|
||||||
return rew
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
Helper class.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class _DiscountedAverage:
|
|
||||||
r"""Discounted average of rewards.
|
|
||||||
|
|
||||||
The discounted average is defined as:
|
|
||||||
|
|
||||||
.. math::
|
|
||||||
|
|
||||||
\bar{R}_t = \gamma \bar{R}_{t-1} + r_t
|
|
||||||
|
|
||||||
Args:
|
|
||||||
gamma (float): Discount factor.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, gamma):
|
|
||||||
self.avg = None
|
|
||||||
self.gamma = gamma
|
|
||||||
|
|
||||||
def update(self, rew: torch.Tensor) -> torch.Tensor:
|
|
||||||
if self.avg is None:
|
|
||||||
self.avg = rew
|
|
||||||
else:
|
|
||||||
self.avg = self.avg * self.gamma + rew
|
|
||||||
return self.avg
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
"""Implementation of runners for environment-agent interaction."""
|
|
||||||
|
|
||||||
from .on_policy_runner import OnPolicyRunner # isort:skip
|
|
||||||
from .distillation_runner import DistillationRunner
|
|
||||||
from .amp_on_policy_runner import AMPOnPolicyRunner
|
|
||||||
|
|
||||||
__all__ = ["OnPolicyRunner", "DistillationRunner", "AMPOnPolicyRunner"]
|
|
||||||
|
|
@ -1,521 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
import statistics
|
|
||||||
import time
|
|
||||||
import torch
|
|
||||||
import warnings
|
|
||||||
from collections import deque
|
|
||||||
|
|
||||||
import rsl_rl
|
|
||||||
from rsl_rl.algorithms import AMP_PPO
|
|
||||||
from rsl_rl.env import VecEnv
|
|
||||||
from rsl_rl.modules import ActorCritic, ActorCriticRecurrent,DiscriminatorMulti, resolve_rnd_config, resolve_symmetry_config
|
|
||||||
from rsl_rl.utils import resolve_obs_groups, store_code_state, Normalizer, G1_AMPLoader
|
|
||||||
|
|
||||||
|
|
||||||
class AMPOnPolicyRunner:
|
|
||||||
"""On-policy runner for training and evaluation of actor-critic methods."""
|
|
||||||
|
|
||||||
def __init__(self, env: VecEnv, train_cfg: dict, log_dir: str | None = None, device="cpu"):
|
|
||||||
self.cfg = train_cfg
|
|
||||||
self.alg_cfg = train_cfg["algorithm"]
|
|
||||||
self.policy_cfg = train_cfg["policy"]
|
|
||||||
self.device = device
|
|
||||||
self.env = env
|
|
||||||
|
|
||||||
# check if multi-gpu is enabled
|
|
||||||
self._configure_multi_gpu()
|
|
||||||
|
|
||||||
# store training configuration
|
|
||||||
self.num_steps_per_env = self.cfg["num_steps_per_env"]
|
|
||||||
self.save_interval = self.cfg["save_interval"]
|
|
||||||
|
|
||||||
# query observations from environment for algorithm construction
|
|
||||||
obs = self.env.get_observations()
|
|
||||||
default_sets = ["critic"]
|
|
||||||
if "rnd_cfg" in self.alg_cfg and self.alg_cfg["rnd_cfg"] is not None:
|
|
||||||
default_sets.append("rnd_state")
|
|
||||||
self.cfg["obs_groups"] = resolve_obs_groups(obs, self.cfg["obs_groups"], default_sets)
|
|
||||||
|
|
||||||
self.amp_data = G1_AMPLoader(
|
|
||||||
device,
|
|
||||||
time_between_frames=1/50.0,
|
|
||||||
preload_transitions=True,
|
|
||||||
num_preload_transitions=train_cfg["amp_num_preload_transitions"],
|
|
||||||
motion_files=train_cfg["amp_motion_files"],
|
|
||||||
num_frames=train_cfg['amp_num_frames']
|
|
||||||
)
|
|
||||||
|
|
||||||
self.amp_observation_dim = self.amp_data.observation_dim if self.cfg["amp_num_obs"] == 0 else self.cfg["amp_num_obs"]
|
|
||||||
self.amp_num_frames = 0 if self.cfg["amp_num_frames"] == 0 else self.cfg["amp_num_frames"]
|
|
||||||
self.amp_normalizer = Normalizer(self.amp_observation_dim)
|
|
||||||
self.discriminator = DiscriminatorMulti(
|
|
||||||
self.amp_observation_dim,
|
|
||||||
train_cfg["amp_reward_coef"],
|
|
||||||
train_cfg["amp_discr_hidden_dims"],
|
|
||||||
device,
|
|
||||||
train_cfg["amp_num_frames"],
|
|
||||||
train_cfg["amp_task_reward_lerp"],
|
|
||||||
train_cfg['use_lerp'],
|
|
||||||
).to(self.device)
|
|
||||||
|
|
||||||
|
|
||||||
# create the algorithm
|
|
||||||
self.alg = self._construct_algorithm(obs)
|
|
||||||
|
|
||||||
# Decide whether to disable logging
|
|
||||||
# We only log from the process with rank 0 (main process)
|
|
||||||
self.disable_logs = self.is_distributed and self.gpu_global_rank != 0
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
self.log_dir = log_dir
|
|
||||||
self.writer = None
|
|
||||||
self.tot_timesteps = 0
|
|
||||||
self.tot_time = 0
|
|
||||||
self.current_learning_iteration = 0
|
|
||||||
self.git_status_repos = [rsl_rl.__file__]
|
|
||||||
|
|
||||||
def learn(self, num_learning_iterations: int, init_at_random_ep_len: bool = False): # noqa: C901
|
|
||||||
# initialize writer
|
|
||||||
self._prepare_logging_writer()
|
|
||||||
|
|
||||||
# randomize initial episode lengths (for exploration)
|
|
||||||
if init_at_random_ep_len:
|
|
||||||
self.env.episode_length_buf = torch.randint_like(
|
|
||||||
self.env.episode_length_buf, high=int(self.env.max_episode_length)
|
|
||||||
)
|
|
||||||
|
|
||||||
# start learning
|
|
||||||
obs = self.env.get_observations().to(self.device)
|
|
||||||
self.train_mode() # switch to train mode (for dropout for example)
|
|
||||||
amp_obs = self.env.get_amp_observations()
|
|
||||||
amp_obs = amp_obs.to(self.device)
|
|
||||||
if self.amp_num_frames != 0:
|
|
||||||
self.amp_obs_frames = torch.zeros(size=(self.env.num_envs, self.amp_num_frames, self.amp_observation_dim), device=self.device)
|
|
||||||
self.amp_obs_frames = torch.concat((self.amp_obs_frames[:, 1:], amp_obs.unsqueeze(1)), dim=1)
|
|
||||||
|
|
||||||
# Book keeping
|
|
||||||
ep_infos = []
|
|
||||||
rewbuffer = deque(maxlen=100)
|
|
||||||
lenbuffer = deque(maxlen=100)
|
|
||||||
step_discrewbuffer = deque(maxlen=100)
|
|
||||||
|
|
||||||
cur_reward_sum = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
cur_episode_length = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
cur_single_step_disc_rew = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
# create buffers for logging extrinsic and intrinsic rewards
|
|
||||||
if self.alg.rnd:
|
|
||||||
erewbuffer = deque(maxlen=100)
|
|
||||||
irewbuffer = deque(maxlen=100)
|
|
||||||
cur_ereward_sum = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
cur_ireward_sum = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
|
|
||||||
# Ensure all parameters are in-synced
|
|
||||||
if self.is_distributed:
|
|
||||||
print(f"Synchronizing parameters for rank {self.gpu_global_rank}...")
|
|
||||||
self.alg.broadcast_parameters()
|
|
||||||
|
|
||||||
# Start training
|
|
||||||
start_iter = self.current_learning_iteration
|
|
||||||
tot_iter = start_iter + num_learning_iterations
|
|
||||||
for it in range(start_iter, tot_iter):
|
|
||||||
start = time.time()
|
|
||||||
# Rollout
|
|
||||||
with torch.inference_mode():
|
|
||||||
for _ in range(self.num_steps_per_env):
|
|
||||||
# Sample actions
|
|
||||||
actions = self.alg.act(obs,amp_obs)
|
|
||||||
# Step the environment
|
|
||||||
obs, rewards, dones, extras = self.env.step(actions.to(self.env.device))
|
|
||||||
# Move to device
|
|
||||||
obs, rewards, dones = (obs.to(self.device), rewards.to(self.device), dones.to(self.device))
|
|
||||||
|
|
||||||
next_amp_obs = self.env.get_amp_observations()
|
|
||||||
next_amp_obs = next_amp_obs.to(self.device)
|
|
||||||
next_amp_obs_with_term = torch.clone(next_amp_obs)
|
|
||||||
|
|
||||||
reset_env_ids = self.env.reset_env_ids
|
|
||||||
terminal_amp_states = self.env.get_amp_observations()[reset_env_ids]
|
|
||||||
next_amp_obs_with_term[reset_env_ids] = terminal_amp_states
|
|
||||||
self.amp_obs_frames = torch.concat((self.amp_obs_frames[:, 1:], next_amp_obs_with_term.unsqueeze(1)), dim=1)
|
|
||||||
|
|
||||||
amp_reward = torch.zeros(self.env.num_envs, device=obs.device)
|
|
||||||
|
|
||||||
mask = self.env.contact_phase[:, 0] == 1.0
|
|
||||||
if mask.any():
|
|
||||||
rewards[mask], logit, disc_reward = self.alg.discriminator.predict_amp_reward(
|
|
||||||
self.amp_obs_frames[mask], rewards[mask], normalizer=self.alg.amp_normalizer
|
|
||||||
)
|
|
||||||
amp_reward[mask] += disc_reward
|
|
||||||
|
|
||||||
# process the step
|
|
||||||
self.alg.process_env_step(obs, rewards, dones, extras, next_amp_obs_with_term, self.amp_obs_frames)
|
|
||||||
self.amp_obs_frames[reset_env_ids] = 0
|
|
||||||
|
|
||||||
amp_obs = torch.clone(next_amp_obs)
|
|
||||||
# Extract intrinsic rewards (only for logging)
|
|
||||||
intrinsic_rewards = self.alg.intrinsic_rewards if self.alg.rnd else None
|
|
||||||
# book keeping
|
|
||||||
if self.log_dir is not None:
|
|
||||||
if "episode" in extras:
|
|
||||||
ep_infos.append(extras["episode"])
|
|
||||||
elif "log" in extras:
|
|
||||||
ep_infos.append(extras["log"])
|
|
||||||
# Update rewards
|
|
||||||
if self.alg.rnd:
|
|
||||||
cur_ereward_sum += rewards
|
|
||||||
cur_ireward_sum += intrinsic_rewards # type: ignore
|
|
||||||
cur_reward_sum += rewards + intrinsic_rewards
|
|
||||||
else:
|
|
||||||
cur_reward_sum += rewards
|
|
||||||
# Update episode length
|
|
||||||
cur_episode_length += 1
|
|
||||||
cur_single_step_disc_rew += amp_reward
|
|
||||||
# Clear data for completed episodes
|
|
||||||
# -- common
|
|
||||||
new_ids = (dones > 0).nonzero(as_tuple=False)
|
|
||||||
rewbuffer.extend(cur_reward_sum[new_ids][:, 0].cpu().numpy().tolist())
|
|
||||||
lenbuffer.extend(cur_episode_length[new_ids][:, 0].cpu().numpy().tolist())
|
|
||||||
cur_reward_sum[new_ids] = 0
|
|
||||||
cur_episode_length[new_ids] = 0
|
|
||||||
to_extend_disc = (cur_single_step_disc_rew[new_ids] / self.env.max_episode_length_s)[:, 0].cpu().numpy()
|
|
||||||
step_discrewbuffer.extend(to_extend_disc.tolist())
|
|
||||||
cur_single_step_disc_rew[new_ids] = 0
|
|
||||||
# -- intrinsic and extrinsic rewards
|
|
||||||
if self.alg.rnd:
|
|
||||||
erewbuffer.extend(cur_ereward_sum[new_ids][:, 0].cpu().numpy().tolist())
|
|
||||||
irewbuffer.extend(cur_ireward_sum[new_ids][:, 0].cpu().numpy().tolist())
|
|
||||||
cur_ereward_sum[new_ids] = 0
|
|
||||||
cur_ireward_sum[new_ids] = 0
|
|
||||||
|
|
||||||
stop = time.time()
|
|
||||||
collection_time = stop - start
|
|
||||||
start = stop
|
|
||||||
|
|
||||||
# compute returns
|
|
||||||
self.alg.compute_returns(obs)
|
|
||||||
|
|
||||||
# update policy
|
|
||||||
loss_dict = self.alg.update()
|
|
||||||
|
|
||||||
stop = time.time()
|
|
||||||
learn_time = stop - start
|
|
||||||
self.current_learning_iteration = it
|
|
||||||
# log info
|
|
||||||
if self.log_dir is not None and not self.disable_logs:
|
|
||||||
# Log information
|
|
||||||
self.log(locals())
|
|
||||||
# Save model
|
|
||||||
if it % self.save_interval == 0:
|
|
||||||
self.save(os.path.join(self.log_dir, f"model_{it}.pt"))
|
|
||||||
|
|
||||||
# Clear episode infos
|
|
||||||
ep_infos.clear()
|
|
||||||
# Save code state
|
|
||||||
if it == start_iter and not self.disable_logs:
|
|
||||||
# obtain all the diff files
|
|
||||||
git_file_paths = store_code_state(self.log_dir, self.git_status_repos)
|
|
||||||
# if possible store them to wandb
|
|
||||||
if self.logger_type in ["wandb", "neptune"] and git_file_paths:
|
|
||||||
for path in git_file_paths:
|
|
||||||
self.writer.save_file(path)
|
|
||||||
|
|
||||||
# Save the final model after training
|
|
||||||
if self.log_dir is not None and not self.disable_logs:
|
|
||||||
self.save(os.path.join(self.log_dir, f"model_{self.current_learning_iteration}.pt"))
|
|
||||||
|
|
||||||
def log(self, locs: dict, width: int = 80, pad: int = 35):
|
|
||||||
# Compute the collection size
|
|
||||||
collection_size = self.num_steps_per_env * self.env.num_envs * self.gpu_world_size
|
|
||||||
# Update total time-steps and time
|
|
||||||
self.tot_timesteps += collection_size
|
|
||||||
self.tot_time += locs["collection_time"] + locs["learn_time"]
|
|
||||||
iteration_time = locs["collection_time"] + locs["learn_time"]
|
|
||||||
|
|
||||||
# -- Episode info
|
|
||||||
ep_string = ""
|
|
||||||
if locs["ep_infos"]:
|
|
||||||
for key in locs["ep_infos"][0]:
|
|
||||||
infotensor = torch.tensor([], device=self.device)
|
|
||||||
for ep_info in locs["ep_infos"]:
|
|
||||||
# handle scalar and zero dimensional tensor infos
|
|
||||||
if key not in ep_info:
|
|
||||||
continue
|
|
||||||
if not isinstance(ep_info[key], torch.Tensor):
|
|
||||||
ep_info[key] = torch.Tensor([ep_info[key]])
|
|
||||||
if len(ep_info[key].shape) == 0:
|
|
||||||
ep_info[key] = ep_info[key].unsqueeze(0)
|
|
||||||
infotensor = torch.cat((infotensor, ep_info[key].to(self.device)))
|
|
||||||
value = torch.mean(infotensor)
|
|
||||||
# log to logger and terminal
|
|
||||||
if "/" in key:
|
|
||||||
self.writer.add_scalar(key, value, locs["it"])
|
|
||||||
ep_string += f"""{f'{key}:':>{pad}} {value:.4f}\n"""
|
|
||||||
else:
|
|
||||||
self.writer.add_scalar("Episode/" + key, value, locs["it"])
|
|
||||||
ep_string += f"""{f'Mean episode {key}:':>{pad}} {value:.4f}\n"""
|
|
||||||
|
|
||||||
mean_std = self.alg.policy.action_std.mean()
|
|
||||||
fps = int(collection_size / (locs["collection_time"] + locs["learn_time"]))
|
|
||||||
|
|
||||||
# -- Losses
|
|
||||||
for key, value in locs["loss_dict"].items():
|
|
||||||
self.writer.add_scalar(f"Loss/{key}", value, locs["it"])
|
|
||||||
self.writer.add_scalar("Loss/learning_rate", self.alg.learning_rate, locs["it"])
|
|
||||||
|
|
||||||
# -- Policy
|
|
||||||
self.writer.add_scalar("Policy/mean_noise_std", mean_std.item(), locs["it"])
|
|
||||||
|
|
||||||
# -- Performance
|
|
||||||
self.writer.add_scalar("Perf/total_fps", fps, locs["it"])
|
|
||||||
self.writer.add_scalar("Perf/collection time", locs["collection_time"], locs["it"])
|
|
||||||
self.writer.add_scalar("Perf/learning_time", locs["learn_time"], locs["it"])
|
|
||||||
|
|
||||||
# -- Training
|
|
||||||
if len(locs["rewbuffer"]) > 0:
|
|
||||||
# separate logging for intrinsic and extrinsic rewards
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
self.writer.add_scalar("Rnd/mean_extrinsic_reward", statistics.mean(locs["erewbuffer"]), locs["it"])
|
|
||||||
self.writer.add_scalar("Rnd/mean_intrinsic_reward", statistics.mean(locs["irewbuffer"]), locs["it"])
|
|
||||||
self.writer.add_scalar("Rnd/weight", self.alg.rnd.weight, locs["it"])
|
|
||||||
# everything else
|
|
||||||
self.writer.add_scalar("Train/mean_reward", statistics.mean(locs["rewbuffer"]), locs["it"])
|
|
||||||
self.writer.add_scalar("Train/mean_episode_length", statistics.mean(locs["lenbuffer"]), locs["it"])
|
|
||||||
self.writer.add_scalar('Train/mean_step_disc_reward', statistics.mean(locs['step_discrewbuffer']), locs['it'])
|
|
||||||
if self.logger_type != "wandb": # wandb does not support non-integer x-axis logging
|
|
||||||
self.writer.add_scalar("Train/mean_reward/time", statistics.mean(locs["rewbuffer"]), self.tot_time)
|
|
||||||
self.writer.add_scalar(
|
|
||||||
"Train/mean_episode_length/time", statistics.mean(locs["lenbuffer"]), self.tot_time
|
|
||||||
)
|
|
||||||
|
|
||||||
str = f" \033[1m Learning iteration {locs['it']}/{locs['tot_iter']} \033[0m "
|
|
||||||
|
|
||||||
if len(locs["rewbuffer"]) > 0:
|
|
||||||
log_string = (
|
|
||||||
f"""{'#' * width}\n"""
|
|
||||||
f"""{str.center(width, ' ')}\n\n"""
|
|
||||||
f"""{'Computation:':>{pad}} {fps:.0f} steps/s (collection: {locs[
|
|
||||||
'collection_time']:.3f}s, learning {locs['learn_time']:.3f}s)\n"""
|
|
||||||
f"""{'Mean action noise std:':>{pad}} {mean_std.item():.2f}\n"""
|
|
||||||
f"""{'Step disc reward:':>{pad}} {statistics.mean(locs['step_discrewbuffer']):.2f}\n"""
|
|
||||||
)
|
|
||||||
# -- Losses
|
|
||||||
for key, value in locs["loss_dict"].items():
|
|
||||||
log_string += f"""{f'Mean {key} loss:':>{pad}} {value:.4f}\n"""
|
|
||||||
# -- Rewards
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
log_string += (
|
|
||||||
f"""{'Mean extrinsic reward:':>{pad}} {statistics.mean(locs['erewbuffer']):.2f}\n"""
|
|
||||||
f"""{'Mean intrinsic reward:':>{pad}} {statistics.mean(locs['irewbuffer']):.2f}\n"""
|
|
||||||
)
|
|
||||||
log_string += f"""{'Mean reward:':>{pad}} {statistics.mean(locs['rewbuffer']):.2f}\n"""
|
|
||||||
# -- episode info
|
|
||||||
log_string += f"""{'Mean episode length:':>{pad}} {statistics.mean(locs['lenbuffer']):.2f}\n"""
|
|
||||||
else:
|
|
||||||
log_string = (
|
|
||||||
f"""{'#' * width}\n"""
|
|
||||||
f"""{str.center(width, ' ')}\n\n"""
|
|
||||||
f"""{'Computation:':>{pad}} {fps:.0f} steps/s (collection: {locs[
|
|
||||||
'collection_time']:.3f}s, learning {locs['learn_time']:.3f}s)\n"""
|
|
||||||
f"""{'Mean action noise std:':>{pad}} {mean_std.item():.2f}\n"""
|
|
||||||
)
|
|
||||||
for key, value in locs["loss_dict"].items():
|
|
||||||
log_string += f"""{f'{key}:':>{pad}} {value:.4f}\n"""
|
|
||||||
|
|
||||||
log_string += ep_string
|
|
||||||
log_string += (
|
|
||||||
f"""{'-' * width}\n"""
|
|
||||||
f"""{'Total timesteps:':>{pad}} {self.tot_timesteps}\n"""
|
|
||||||
f"""{'Iteration time:':>{pad}} {iteration_time:.2f}s\n"""
|
|
||||||
f"""{'Time elapsed:':>{pad}} {time.strftime("%H:%M:%S", time.gmtime(self.tot_time))}\n"""
|
|
||||||
f"""{'ETA:':>{pad}} {time.strftime(
|
|
||||||
"%H:%M:%S",
|
|
||||||
time.gmtime(
|
|
||||||
self.tot_time / (locs['it'] - locs['start_iter'] + 1)
|
|
||||||
* (locs['start_iter'] + locs['num_learning_iterations'] - locs['it'])
|
|
||||||
)
|
|
||||||
)}\n"""
|
|
||||||
)
|
|
||||||
print(log_string)
|
|
||||||
|
|
||||||
def save(self, path: str, infos=None):
|
|
||||||
# -- Save model
|
|
||||||
saved_dict = {
|
|
||||||
"model_state_dict": self.alg.policy.state_dict(),
|
|
||||||
"optimizer_state_dict": self.alg.optimizer.state_dict(),
|
|
||||||
"iter": self.current_learning_iteration,
|
|
||||||
"infos": infos,
|
|
||||||
}
|
|
||||||
# -- Save RND model if used
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
saved_dict["rnd_state_dict"] = self.alg.rnd.state_dict()
|
|
||||||
saved_dict["rnd_optimizer_state_dict"] = self.alg.rnd_optimizer.state_dict()
|
|
||||||
torch.save(saved_dict, path)
|
|
||||||
|
|
||||||
# upload model to external logging service
|
|
||||||
if self.logger_type in ["neptune", "wandb"] and not self.disable_logs:
|
|
||||||
self.writer.save_model(path, self.current_learning_iteration)
|
|
||||||
|
|
||||||
def load(self, path: str, load_optimizer: bool = True, map_location: str | None = None):
|
|
||||||
loaded_dict = torch.load(path, weights_only=False, map_location=map_location)
|
|
||||||
# -- Load model
|
|
||||||
resumed_training = self.alg.policy.load_state_dict(loaded_dict["model_state_dict"])
|
|
||||||
# -- Load RND model if used
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
self.alg.rnd.load_state_dict(loaded_dict["rnd_state_dict"])
|
|
||||||
# -- load optimizer if used
|
|
||||||
if load_optimizer and resumed_training:
|
|
||||||
# -- algorithm optimizer
|
|
||||||
self.alg.optimizer.load_state_dict(loaded_dict["optimizer_state_dict"])
|
|
||||||
# -- RND optimizer if used
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
self.alg.rnd_optimizer.load_state_dict(loaded_dict["rnd_optimizer_state_dict"])
|
|
||||||
# -- load current learning iteration
|
|
||||||
if resumed_training:
|
|
||||||
self.current_learning_iteration = loaded_dict["iter"]
|
|
||||||
return loaded_dict["infos"]
|
|
||||||
|
|
||||||
def get_inference_policy(self, device=None):
|
|
||||||
self.eval_mode() # switch to evaluation mode (dropout for example)
|
|
||||||
if device is not None:
|
|
||||||
self.alg.policy.to(device)
|
|
||||||
return self.alg.policy.act_inference
|
|
||||||
|
|
||||||
def train_mode(self):
|
|
||||||
# -- PPO
|
|
||||||
self.alg.policy.train()
|
|
||||||
self.alg.discriminator.train()
|
|
||||||
# -- RND
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
self.alg.rnd.train()
|
|
||||||
|
|
||||||
def eval_mode(self):
|
|
||||||
# -- PPO
|
|
||||||
self.alg.policy.eval()
|
|
||||||
self.alg.discriminator.eval()
|
|
||||||
# -- RND
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
self.alg.rnd.eval()
|
|
||||||
|
|
||||||
def add_git_repo_to_log(self, repo_file_path):
|
|
||||||
self.git_status_repos.append(repo_file_path)
|
|
||||||
|
|
||||||
"""
|
|
||||||
Helper functions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _configure_multi_gpu(self):
|
|
||||||
"""Configure multi-gpu training."""
|
|
||||||
# check if distributed training is enabled
|
|
||||||
self.gpu_world_size = int(os.getenv("WORLD_SIZE", "1"))
|
|
||||||
self.is_distributed = self.gpu_world_size > 1
|
|
||||||
|
|
||||||
# if not distributed training, set local and global rank to 0 and return
|
|
||||||
if not self.is_distributed:
|
|
||||||
self.gpu_local_rank = 0
|
|
||||||
self.gpu_global_rank = 0
|
|
||||||
self.multi_gpu_cfg = None
|
|
||||||
return
|
|
||||||
|
|
||||||
# get rank and world size
|
|
||||||
self.gpu_local_rank = int(os.getenv("LOCAL_RANK", "0"))
|
|
||||||
self.gpu_global_rank = int(os.getenv("RANK", "0"))
|
|
||||||
|
|
||||||
# make a configuration dictionary
|
|
||||||
self.multi_gpu_cfg = {
|
|
||||||
"global_rank": self.gpu_global_rank, # rank of the main process
|
|
||||||
"local_rank": self.gpu_local_rank, # rank of the current process
|
|
||||||
"world_size": self.gpu_world_size, # total number of processes
|
|
||||||
}
|
|
||||||
|
|
||||||
# check if user has device specified for local rank
|
|
||||||
if self.device != f"cuda:{self.gpu_local_rank}":
|
|
||||||
raise ValueError(
|
|
||||||
f"Device '{self.device}' does not match expected device for local rank '{self.gpu_local_rank}'."
|
|
||||||
)
|
|
||||||
# validate multi-gpu configuration
|
|
||||||
if self.gpu_local_rank >= self.gpu_world_size:
|
|
||||||
raise ValueError(
|
|
||||||
f"Local rank '{self.gpu_local_rank}' is greater than or equal to world size '{self.gpu_world_size}'."
|
|
||||||
)
|
|
||||||
if self.gpu_global_rank >= self.gpu_world_size:
|
|
||||||
raise ValueError(
|
|
||||||
f"Global rank '{self.gpu_global_rank}' is greater than or equal to world size '{self.gpu_world_size}'."
|
|
||||||
)
|
|
||||||
|
|
||||||
# initialize torch distributed
|
|
||||||
torch.distributed.init_process_group(backend="nccl", rank=self.gpu_global_rank, world_size=self.gpu_world_size)
|
|
||||||
# set device to the local rank
|
|
||||||
torch.cuda.set_device(self.gpu_local_rank)
|
|
||||||
|
|
||||||
def _construct_algorithm(self, obs) -> AMP_PPO:
|
|
||||||
"""Construct the actor-critic algorithm."""
|
|
||||||
# resolve RND config
|
|
||||||
self.alg_cfg = resolve_rnd_config(self.alg_cfg, obs, self.cfg["obs_groups"], self.env)
|
|
||||||
|
|
||||||
# resolve symmetry config
|
|
||||||
self.alg_cfg = resolve_symmetry_config(self.alg_cfg, self.env)
|
|
||||||
|
|
||||||
# resolve deprecated normalization config
|
|
||||||
if self.cfg.get("empirical_normalization") is not None:
|
|
||||||
warnings.warn(
|
|
||||||
"The `empirical_normalization` parameter is deprecated. Please set `actor_obs_normalization` and "
|
|
||||||
"`critic_obs_normalization` as part of the `policy` configuration instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
if self.policy_cfg.get("actor_obs_normalization") is None:
|
|
||||||
self.policy_cfg["actor_obs_normalization"] = self.cfg["empirical_normalization"]
|
|
||||||
if self.policy_cfg.get("critic_obs_normalization") is None:
|
|
||||||
self.policy_cfg["critic_obs_normalization"] = self.cfg["empirical_normalization"]
|
|
||||||
|
|
||||||
# initialize the actor-critic
|
|
||||||
actor_critic_class = eval(self.policy_cfg.pop("class_name"))
|
|
||||||
actor_critic: ActorCritic | ActorCriticRecurrent = actor_critic_class(
|
|
||||||
obs, self.cfg["obs_groups"], self.env.num_actions, **self.policy_cfg
|
|
||||||
).to(self.device)
|
|
||||||
|
|
||||||
# initialize the algorithm
|
|
||||||
alg_class = eval(self.alg_cfg.pop("class_name"))
|
|
||||||
|
|
||||||
alg: AMP_PPO = alg_class(actor_critic, self.discriminator, self.amp_data, self.amp_normalizer, self.amp_num_frames, device=self.device, **self.alg_cfg, multi_gpu_cfg=self.multi_gpu_cfg)
|
|
||||||
|
|
||||||
# initialize the storage
|
|
||||||
alg.init_storage(
|
|
||||||
"rl",
|
|
||||||
self.env.num_envs,
|
|
||||||
self.num_steps_per_env,
|
|
||||||
obs,
|
|
||||||
[self.env.num_actions],
|
|
||||||
)
|
|
||||||
|
|
||||||
return alg
|
|
||||||
|
|
||||||
def _prepare_logging_writer(self):
|
|
||||||
"""Prepares the logging writers."""
|
|
||||||
if self.log_dir is not None and self.writer is None and not self.disable_logs:
|
|
||||||
# Launch either Tensorboard or Neptune & Tensorboard summary writer(s), default: Tensorboard.
|
|
||||||
self.logger_type = self.cfg.get("logger", "tensorboard")
|
|
||||||
self.logger_type = self.logger_type.lower()
|
|
||||||
|
|
||||||
if self.logger_type == "neptune":
|
|
||||||
from rsl_rl.utils.neptune_utils import NeptuneSummaryWriter
|
|
||||||
|
|
||||||
self.writer = NeptuneSummaryWriter(log_dir=self.log_dir, flush_secs=10, cfg=self.cfg)
|
|
||||||
self.writer.log_config(self.env.cfg, self.cfg, self.alg_cfg, self.policy_cfg)
|
|
||||||
elif self.logger_type == "wandb":
|
|
||||||
from rsl_rl.utils.wandb_utils import WandbSummaryWriter
|
|
||||||
|
|
||||||
self.writer = WandbSummaryWriter(log_dir=self.log_dir, flush_secs=10, cfg=self.cfg)
|
|
||||||
self.writer.log_config(self.env.cfg, self.cfg, self.alg_cfg, self.policy_cfg)
|
|
||||||
elif self.logger_type == "tensorboard":
|
|
||||||
from torch.utils.tensorboard import SummaryWriter
|
|
||||||
|
|
||||||
self.writer = SummaryWriter(log_dir=self.log_dir, flush_secs=10)
|
|
||||||
else:
|
|
||||||
raise ValueError("Logger type not found. Please choose 'neptune', 'wandb' or 'tensorboard'.")
|
|
||||||
|
|
@ -1,179 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import torch
|
|
||||||
from collections import deque
|
|
||||||
|
|
||||||
import rsl_rl
|
|
||||||
from rsl_rl.algorithms import Distillation
|
|
||||||
from rsl_rl.env import VecEnv
|
|
||||||
from rsl_rl.modules import StudentTeacher, StudentTeacherRecurrent
|
|
||||||
from rsl_rl.runners import OnPolicyRunner
|
|
||||||
from rsl_rl.utils import resolve_obs_groups, store_code_state
|
|
||||||
|
|
||||||
|
|
||||||
class DistillationRunner(OnPolicyRunner):
|
|
||||||
"""On-policy runner for training and evaluation of teacher-student training."""
|
|
||||||
|
|
||||||
def __init__(self, env: VecEnv, train_cfg: dict, log_dir: str | None = None, device="cpu"):
|
|
||||||
self.cfg = train_cfg
|
|
||||||
self.alg_cfg = train_cfg["algorithm"]
|
|
||||||
self.policy_cfg = train_cfg["policy"]
|
|
||||||
self.device = device
|
|
||||||
self.env = env
|
|
||||||
|
|
||||||
# check if multi-gpu is enabled
|
|
||||||
self._configure_multi_gpu()
|
|
||||||
|
|
||||||
# store training configuration
|
|
||||||
self.num_steps_per_env = self.cfg["num_steps_per_env"]
|
|
||||||
self.save_interval = self.cfg["save_interval"]
|
|
||||||
|
|
||||||
# query observations from environment for algorithm construction
|
|
||||||
obs = self.env.get_observations()
|
|
||||||
self.cfg["obs_groups"] = resolve_obs_groups(obs, self.cfg["obs_groups"], default_sets=["teacher"])
|
|
||||||
|
|
||||||
# create the algorithm
|
|
||||||
self.alg = self._construct_algorithm(obs)
|
|
||||||
|
|
||||||
# Decide whether to disable logging
|
|
||||||
# We only log from the process with rank 0 (main process)
|
|
||||||
self.disable_logs = self.is_distributed and self.gpu_global_rank != 0
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
self.log_dir = log_dir
|
|
||||||
self.writer = None
|
|
||||||
self.tot_timesteps = 0
|
|
||||||
self.tot_time = 0
|
|
||||||
self.current_learning_iteration = 0
|
|
||||||
self.git_status_repos = [rsl_rl.__file__]
|
|
||||||
|
|
||||||
def learn(self, num_learning_iterations: int, init_at_random_ep_len: bool = False): # noqa: C901
|
|
||||||
# initialize writer
|
|
||||||
self._prepare_logging_writer()
|
|
||||||
# check if teacher is loaded
|
|
||||||
if not self.alg.policy.loaded_teacher:
|
|
||||||
raise ValueError("Teacher model parameters not loaded. Please load a teacher model to distill.")
|
|
||||||
|
|
||||||
# randomize initial episode lengths (for exploration)
|
|
||||||
if init_at_random_ep_len:
|
|
||||||
self.env.episode_length_buf = torch.randint_like(
|
|
||||||
self.env.episode_length_buf, high=int(self.env.max_episode_length)
|
|
||||||
)
|
|
||||||
|
|
||||||
# start learning
|
|
||||||
obs = self.env.get_observations().to(self.device)
|
|
||||||
self.train_mode() # switch to train mode (for dropout for example)
|
|
||||||
|
|
||||||
# Book keeping
|
|
||||||
ep_infos = []
|
|
||||||
rewbuffer = deque(maxlen=100)
|
|
||||||
lenbuffer = deque(maxlen=100)
|
|
||||||
cur_reward_sum = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
cur_episode_length = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
|
|
||||||
# Ensure all parameters are in-synced
|
|
||||||
if self.is_distributed:
|
|
||||||
print(f"Synchronizing parameters for rank {self.gpu_global_rank}...")
|
|
||||||
self.alg.broadcast_parameters()
|
|
||||||
|
|
||||||
# Start training
|
|
||||||
start_iter = self.current_learning_iteration
|
|
||||||
tot_iter = start_iter + num_learning_iterations
|
|
||||||
for it in range(start_iter, tot_iter):
|
|
||||||
start = time.time()
|
|
||||||
# Rollout
|
|
||||||
with torch.inference_mode():
|
|
||||||
for _ in range(self.num_steps_per_env):
|
|
||||||
# Sample actions
|
|
||||||
actions = self.alg.act(obs)
|
|
||||||
# Step the environment
|
|
||||||
obs, rewards, dones, extras = self.env.step(actions.to(self.env.device))
|
|
||||||
# Move to device
|
|
||||||
obs, rewards, dones = (obs.to(self.device), rewards.to(self.device), dones.to(self.device))
|
|
||||||
# process the step
|
|
||||||
self.alg.process_env_step(obs, rewards, dones, extras)
|
|
||||||
# book keeping
|
|
||||||
if self.log_dir is not None:
|
|
||||||
if "episode" in extras:
|
|
||||||
ep_infos.append(extras["episode"])
|
|
||||||
elif "log" in extras:
|
|
||||||
ep_infos.append(extras["log"])
|
|
||||||
# Update rewards
|
|
||||||
cur_reward_sum += rewards
|
|
||||||
# Update episode length
|
|
||||||
cur_episode_length += 1
|
|
||||||
# Clear data for completed episodes
|
|
||||||
new_ids = (dones > 0).nonzero(as_tuple=False)
|
|
||||||
rewbuffer.extend(cur_reward_sum[new_ids][:, 0].cpu().numpy().tolist())
|
|
||||||
lenbuffer.extend(cur_episode_length[new_ids][:, 0].cpu().numpy().tolist())
|
|
||||||
cur_reward_sum[new_ids] = 0
|
|
||||||
cur_episode_length[new_ids] = 0
|
|
||||||
|
|
||||||
stop = time.time()
|
|
||||||
collection_time = stop - start
|
|
||||||
start = stop
|
|
||||||
|
|
||||||
# update policy
|
|
||||||
loss_dict = self.alg.update()
|
|
||||||
|
|
||||||
stop = time.time()
|
|
||||||
learn_time = stop - start
|
|
||||||
self.current_learning_iteration = it
|
|
||||||
# log info
|
|
||||||
if self.log_dir is not None and not self.disable_logs:
|
|
||||||
# Log information
|
|
||||||
self.log(locals())
|
|
||||||
# Save model
|
|
||||||
if it % self.save_interval == 0:
|
|
||||||
self.save(os.path.join(self.log_dir, f"model_{it}.pt"))
|
|
||||||
|
|
||||||
# Clear episode infos
|
|
||||||
ep_infos.clear()
|
|
||||||
# Save code state
|
|
||||||
if it == start_iter and not self.disable_logs:
|
|
||||||
# obtain all the diff files
|
|
||||||
git_file_paths = store_code_state(self.log_dir, self.git_status_repos)
|
|
||||||
# if possible store them to wandb
|
|
||||||
if self.logger_type in ["wandb", "neptune"] and git_file_paths:
|
|
||||||
for path in git_file_paths:
|
|
||||||
self.writer.save_file(path)
|
|
||||||
|
|
||||||
# Save the final model after training
|
|
||||||
if self.log_dir is not None and not self.disable_logs:
|
|
||||||
self.save(os.path.join(self.log_dir, f"model_{self.current_learning_iteration}.pt"))
|
|
||||||
|
|
||||||
"""
|
|
||||||
Helper methods.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _construct_algorithm(self, obs) -> Distillation:
|
|
||||||
"""Construct the distillation algorithm."""
|
|
||||||
# initialize the actor-critic
|
|
||||||
student_teacher_class = eval(self.policy_cfg.pop("class_name"))
|
|
||||||
student_teacher: StudentTeacher | StudentTeacherRecurrent = student_teacher_class(
|
|
||||||
obs, self.cfg["obs_groups"], self.env.num_actions, **self.policy_cfg
|
|
||||||
).to(self.device)
|
|
||||||
|
|
||||||
# initialize the algorithm
|
|
||||||
alg_class = eval(self.alg_cfg.pop("class_name"))
|
|
||||||
alg: Distillation = alg_class(
|
|
||||||
student_teacher, device=self.device, **self.alg_cfg, multi_gpu_cfg=self.multi_gpu_cfg
|
|
||||||
)
|
|
||||||
|
|
||||||
# initialize the storage
|
|
||||||
alg.init_storage(
|
|
||||||
"distillation",
|
|
||||||
self.env.num_envs,
|
|
||||||
self.num_steps_per_env,
|
|
||||||
obs,
|
|
||||||
[self.env.num_actions],
|
|
||||||
)
|
|
||||||
|
|
||||||
return alg
|
|
||||||
|
|
@ -1,460 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
import statistics
|
|
||||||
import time
|
|
||||||
import torch
|
|
||||||
import warnings
|
|
||||||
from collections import deque
|
|
||||||
|
|
||||||
import rsl_rl
|
|
||||||
from rsl_rl.algorithms import PPO
|
|
||||||
from rsl_rl.env import VecEnv
|
|
||||||
from rsl_rl.modules import ActorCritic, ActorCriticRecurrent, resolve_rnd_config, resolve_symmetry_config
|
|
||||||
from rsl_rl.utils import resolve_obs_groups, store_code_state
|
|
||||||
|
|
||||||
|
|
||||||
class OnPolicyRunner:
|
|
||||||
"""On-policy runner for training and evaluation of actor-critic methods."""
|
|
||||||
|
|
||||||
def __init__(self, env: VecEnv, train_cfg: dict, log_dir: str | None = None, device="cpu"):
|
|
||||||
self.cfg = train_cfg
|
|
||||||
self.alg_cfg = train_cfg["algorithm"]
|
|
||||||
self.policy_cfg = train_cfg["policy"]
|
|
||||||
self.device = device
|
|
||||||
self.env = env
|
|
||||||
|
|
||||||
# check if multi-gpu is enabled
|
|
||||||
self._configure_multi_gpu()
|
|
||||||
|
|
||||||
# store training configuration
|
|
||||||
self.num_steps_per_env = self.cfg["num_steps_per_env"]
|
|
||||||
self.save_interval = self.cfg["save_interval"]
|
|
||||||
|
|
||||||
# query observations from environment for algorithm construction
|
|
||||||
obs = self.env.get_observations()
|
|
||||||
default_sets = ["critic"]
|
|
||||||
if "rnd_cfg" in self.alg_cfg and self.alg_cfg["rnd_cfg"] is not None:
|
|
||||||
default_sets.append("rnd_state")
|
|
||||||
self.cfg["obs_groups"] = resolve_obs_groups(obs, self.cfg["obs_groups"], default_sets)
|
|
||||||
|
|
||||||
# create the algorithm
|
|
||||||
self.alg = self._construct_algorithm(obs)
|
|
||||||
|
|
||||||
# Decide whether to disable logging
|
|
||||||
# We only log from the process with rank 0 (main process)
|
|
||||||
self.disable_logs = self.is_distributed and self.gpu_global_rank != 0
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
self.log_dir = log_dir
|
|
||||||
self.writer = None
|
|
||||||
self.tot_timesteps = 0
|
|
||||||
self.tot_time = 0
|
|
||||||
self.current_learning_iteration = 0
|
|
||||||
self.git_status_repos = [rsl_rl.__file__]
|
|
||||||
|
|
||||||
def learn(self, num_learning_iterations: int, init_at_random_ep_len: bool = False): # noqa: C901
|
|
||||||
# initialize writer
|
|
||||||
self._prepare_logging_writer()
|
|
||||||
|
|
||||||
# randomize initial episode lengths (for exploration)
|
|
||||||
if init_at_random_ep_len:
|
|
||||||
self.env.episode_length_buf = torch.randint_like(
|
|
||||||
self.env.episode_length_buf, high=int(self.env.max_episode_length)
|
|
||||||
)
|
|
||||||
|
|
||||||
# start learning
|
|
||||||
obs = self.env.get_observations().to(self.device)
|
|
||||||
self.train_mode() # switch to train mode (for dropout for example)
|
|
||||||
|
|
||||||
# Book keeping
|
|
||||||
ep_infos = []
|
|
||||||
rewbuffer = deque(maxlen=100)
|
|
||||||
lenbuffer = deque(maxlen=100)
|
|
||||||
cur_reward_sum = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
cur_episode_length = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
|
|
||||||
# create buffers for logging extrinsic and intrinsic rewards
|
|
||||||
if self.alg.rnd:
|
|
||||||
erewbuffer = deque(maxlen=100)
|
|
||||||
irewbuffer = deque(maxlen=100)
|
|
||||||
cur_ereward_sum = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
cur_ireward_sum = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
|
|
||||||
# Ensure all parameters are in-synced
|
|
||||||
if self.is_distributed:
|
|
||||||
print(f"Synchronizing parameters for rank {self.gpu_global_rank}...")
|
|
||||||
self.alg.broadcast_parameters()
|
|
||||||
|
|
||||||
# Start training
|
|
||||||
start_iter = self.current_learning_iteration
|
|
||||||
tot_iter = start_iter + num_learning_iterations
|
|
||||||
for it in range(start_iter, tot_iter):
|
|
||||||
start = time.time()
|
|
||||||
# Rollout
|
|
||||||
with torch.inference_mode():
|
|
||||||
for _ in range(self.num_steps_per_env):
|
|
||||||
# Sample actions
|
|
||||||
actions = self.alg.act(obs)
|
|
||||||
# Step the environment
|
|
||||||
obs, rewards, dones, extras = self.env.step(actions.to(self.env.device))
|
|
||||||
# Move to device
|
|
||||||
obs, rewards, dones = (obs.to(self.device), rewards.to(self.device), dones.to(self.device))
|
|
||||||
# process the step
|
|
||||||
self.alg.process_env_step(obs, rewards, dones, extras)
|
|
||||||
# Extract intrinsic rewards (only for logging)
|
|
||||||
intrinsic_rewards = self.alg.intrinsic_rewards if self.alg.rnd else None
|
|
||||||
# book keeping
|
|
||||||
if self.log_dir is not None:
|
|
||||||
if "episode" in extras:
|
|
||||||
ep_infos.append(extras["episode"])
|
|
||||||
elif "log" in extras:
|
|
||||||
ep_infos.append(extras["log"])
|
|
||||||
# Update rewards
|
|
||||||
if self.alg.rnd:
|
|
||||||
cur_ereward_sum += rewards
|
|
||||||
cur_ireward_sum += intrinsic_rewards # type: ignore
|
|
||||||
cur_reward_sum += rewards + intrinsic_rewards
|
|
||||||
else:
|
|
||||||
cur_reward_sum += rewards
|
|
||||||
# Update episode length
|
|
||||||
cur_episode_length += 1
|
|
||||||
# Clear data for completed episodes
|
|
||||||
# -- common
|
|
||||||
new_ids = (dones > 0).nonzero(as_tuple=False)
|
|
||||||
rewbuffer.extend(cur_reward_sum[new_ids][:, 0].cpu().numpy().tolist())
|
|
||||||
lenbuffer.extend(cur_episode_length[new_ids][:, 0].cpu().numpy().tolist())
|
|
||||||
cur_reward_sum[new_ids] = 0
|
|
||||||
cur_episode_length[new_ids] = 0
|
|
||||||
# -- intrinsic and extrinsic rewards
|
|
||||||
if self.alg.rnd:
|
|
||||||
erewbuffer.extend(cur_ereward_sum[new_ids][:, 0].cpu().numpy().tolist())
|
|
||||||
irewbuffer.extend(cur_ireward_sum[new_ids][:, 0].cpu().numpy().tolist())
|
|
||||||
cur_ereward_sum[new_ids] = 0
|
|
||||||
cur_ireward_sum[new_ids] = 0
|
|
||||||
|
|
||||||
stop = time.time()
|
|
||||||
collection_time = stop - start
|
|
||||||
start = stop
|
|
||||||
|
|
||||||
# compute returns
|
|
||||||
self.alg.compute_returns(obs)
|
|
||||||
|
|
||||||
# update policy
|
|
||||||
loss_dict = self.alg.update()
|
|
||||||
|
|
||||||
stop = time.time()
|
|
||||||
learn_time = stop - start
|
|
||||||
self.current_learning_iteration = it
|
|
||||||
# log info
|
|
||||||
if self.log_dir is not None and not self.disable_logs:
|
|
||||||
# Log information
|
|
||||||
self.log(locals())
|
|
||||||
# Save model
|
|
||||||
if it % self.save_interval == 0:
|
|
||||||
self.save(os.path.join(self.log_dir, f"model_{it}.pt"))
|
|
||||||
|
|
||||||
# Clear episode infos
|
|
||||||
ep_infos.clear()
|
|
||||||
# Save code state
|
|
||||||
if it == start_iter and not self.disable_logs:
|
|
||||||
# obtain all the diff files
|
|
||||||
git_file_paths = store_code_state(self.log_dir, self.git_status_repos)
|
|
||||||
# if possible store them to wandb
|
|
||||||
if self.logger_type in ["wandb", "neptune"] and git_file_paths:
|
|
||||||
for path in git_file_paths:
|
|
||||||
self.writer.save_file(path)
|
|
||||||
|
|
||||||
# Save the final model after training
|
|
||||||
if self.log_dir is not None and not self.disable_logs:
|
|
||||||
self.save(os.path.join(self.log_dir, f"model_{self.current_learning_iteration}.pt"))
|
|
||||||
|
|
||||||
def log(self, locs: dict, width: int = 80, pad: int = 35):
|
|
||||||
# Compute the collection size
|
|
||||||
collection_size = self.num_steps_per_env * self.env.num_envs * self.gpu_world_size
|
|
||||||
# Update total time-steps and time
|
|
||||||
self.tot_timesteps += collection_size
|
|
||||||
self.tot_time += locs["collection_time"] + locs["learn_time"]
|
|
||||||
iteration_time = locs["collection_time"] + locs["learn_time"]
|
|
||||||
|
|
||||||
# -- Episode info
|
|
||||||
ep_string = ""
|
|
||||||
if locs["ep_infos"]:
|
|
||||||
for key in locs["ep_infos"][0]:
|
|
||||||
infotensor = torch.tensor([], device=self.device)
|
|
||||||
for ep_info in locs["ep_infos"]:
|
|
||||||
# handle scalar and zero dimensional tensor infos
|
|
||||||
if key not in ep_info:
|
|
||||||
continue
|
|
||||||
if not isinstance(ep_info[key], torch.Tensor):
|
|
||||||
ep_info[key] = torch.Tensor([ep_info[key]])
|
|
||||||
if len(ep_info[key].shape) == 0:
|
|
||||||
ep_info[key] = ep_info[key].unsqueeze(0)
|
|
||||||
infotensor = torch.cat((infotensor, ep_info[key].to(self.device)))
|
|
||||||
value = torch.mean(infotensor)
|
|
||||||
# log to logger and terminal
|
|
||||||
if "/" in key:
|
|
||||||
self.writer.add_scalar(key, value, locs["it"])
|
|
||||||
ep_string += f"""{f'{key}:':>{pad}} {value:.4f}\n"""
|
|
||||||
else:
|
|
||||||
self.writer.add_scalar("Episode/" + key, value, locs["it"])
|
|
||||||
ep_string += f"""{f'Mean episode {key}:':>{pad}} {value:.4f}\n"""
|
|
||||||
|
|
||||||
mean_std = self.alg.policy.action_std.mean()
|
|
||||||
fps = int(collection_size / (locs["collection_time"] + locs["learn_time"]))
|
|
||||||
|
|
||||||
# -- Losses
|
|
||||||
for key, value in locs["loss_dict"].items():
|
|
||||||
self.writer.add_scalar(f"Loss/{key}", value, locs["it"])
|
|
||||||
self.writer.add_scalar("Loss/learning_rate", self.alg.learning_rate, locs["it"])
|
|
||||||
|
|
||||||
# -- Policy
|
|
||||||
self.writer.add_scalar("Policy/mean_noise_std", mean_std.item(), locs["it"])
|
|
||||||
|
|
||||||
# -- Performance
|
|
||||||
self.writer.add_scalar("Perf/total_fps", fps, locs["it"])
|
|
||||||
self.writer.add_scalar("Perf/collection time", locs["collection_time"], locs["it"])
|
|
||||||
self.writer.add_scalar("Perf/learning_time", locs["learn_time"], locs["it"])
|
|
||||||
|
|
||||||
# -- Training
|
|
||||||
if len(locs["rewbuffer"]) > 0:
|
|
||||||
# separate logging for intrinsic and extrinsic rewards
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
self.writer.add_scalar("Rnd/mean_extrinsic_reward", statistics.mean(locs["erewbuffer"]), locs["it"])
|
|
||||||
self.writer.add_scalar("Rnd/mean_intrinsic_reward", statistics.mean(locs["irewbuffer"]), locs["it"])
|
|
||||||
self.writer.add_scalar("Rnd/weight", self.alg.rnd.weight, locs["it"])
|
|
||||||
# everything else
|
|
||||||
self.writer.add_scalar("Train/mean_reward", statistics.mean(locs["rewbuffer"]), locs["it"])
|
|
||||||
self.writer.add_scalar("Train/mean_episode_length", statistics.mean(locs["lenbuffer"]), locs["it"])
|
|
||||||
if self.logger_type != "wandb": # wandb does not support non-integer x-axis logging
|
|
||||||
self.writer.add_scalar("Train/mean_reward/time", statistics.mean(locs["rewbuffer"]), self.tot_time)
|
|
||||||
self.writer.add_scalar(
|
|
||||||
"Train/mean_episode_length/time", statistics.mean(locs["lenbuffer"]), self.tot_time
|
|
||||||
)
|
|
||||||
|
|
||||||
str = f" \033[1m Learning iteration {locs['it']}/{locs['tot_iter']} \033[0m "
|
|
||||||
|
|
||||||
if len(locs["rewbuffer"]) > 0:
|
|
||||||
log_string = (
|
|
||||||
f"""{'#' * width}\n"""
|
|
||||||
f"""{str.center(width, ' ')}\n\n"""
|
|
||||||
f"""{'Computation:':>{pad}} {fps:.0f} steps/s (collection: {locs[
|
|
||||||
'collection_time']:.3f}s, learning {locs['learn_time']:.3f}s)\n"""
|
|
||||||
f"""{'Mean action noise std:':>{pad}} {mean_std.item():.2f}\n"""
|
|
||||||
)
|
|
||||||
# -- Losses
|
|
||||||
for key, value in locs["loss_dict"].items():
|
|
||||||
log_string += f"""{f'Mean {key} loss:':>{pad}} {value:.4f}\n"""
|
|
||||||
# -- Rewards
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
log_string += (
|
|
||||||
f"""{'Mean extrinsic reward:':>{pad}} {statistics.mean(locs['erewbuffer']):.2f}\n"""
|
|
||||||
f"""{'Mean intrinsic reward:':>{pad}} {statistics.mean(locs['irewbuffer']):.2f}\n"""
|
|
||||||
)
|
|
||||||
log_string += f"""{'Mean reward:':>{pad}} {statistics.mean(locs['rewbuffer']):.2f}\n"""
|
|
||||||
# -- episode info
|
|
||||||
log_string += f"""{'Mean episode length:':>{pad}} {statistics.mean(locs['lenbuffer']):.2f}\n"""
|
|
||||||
else:
|
|
||||||
log_string = (
|
|
||||||
f"""{'#' * width}\n"""
|
|
||||||
f"""{str.center(width, ' ')}\n\n"""
|
|
||||||
f"""{'Computation:':>{pad}} {fps:.0f} steps/s (collection: {locs[
|
|
||||||
'collection_time']:.3f}s, learning {locs['learn_time']:.3f}s)\n"""
|
|
||||||
f"""{'Mean action noise std:':>{pad}} {mean_std.item():.2f}\n"""
|
|
||||||
)
|
|
||||||
for key, value in locs["loss_dict"].items():
|
|
||||||
log_string += f"""{f'{key}:':>{pad}} {value:.4f}\n"""
|
|
||||||
|
|
||||||
log_string += ep_string
|
|
||||||
log_string += (
|
|
||||||
f"""{'-' * width}\n"""
|
|
||||||
f"""{'Total timesteps:':>{pad}} {self.tot_timesteps}\n"""
|
|
||||||
f"""{'Iteration time:':>{pad}} {iteration_time:.2f}s\n"""
|
|
||||||
f"""{'Time elapsed:':>{pad}} {time.strftime("%H:%M:%S", time.gmtime(self.tot_time))}\n"""
|
|
||||||
f"""{'ETA:':>{pad}} {time.strftime(
|
|
||||||
"%H:%M:%S",
|
|
||||||
time.gmtime(
|
|
||||||
self.tot_time / (locs['it'] - locs['start_iter'] + 1)
|
|
||||||
* (locs['start_iter'] + locs['num_learning_iterations'] - locs['it'])
|
|
||||||
)
|
|
||||||
)}\n"""
|
|
||||||
)
|
|
||||||
print(log_string)
|
|
||||||
|
|
||||||
def save(self, path: str, infos=None):
|
|
||||||
# -- Save model
|
|
||||||
saved_dict = {
|
|
||||||
"model_state_dict": self.alg.policy.state_dict(),
|
|
||||||
"optimizer_state_dict": self.alg.optimizer.state_dict(),
|
|
||||||
"iter": self.current_learning_iteration,
|
|
||||||
"infos": infos,
|
|
||||||
}
|
|
||||||
# -- Save RND model if used
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
saved_dict["rnd_state_dict"] = self.alg.rnd.state_dict()
|
|
||||||
saved_dict["rnd_optimizer_state_dict"] = self.alg.rnd_optimizer.state_dict()
|
|
||||||
torch.save(saved_dict, path)
|
|
||||||
|
|
||||||
# upload model to external logging service
|
|
||||||
if self.logger_type in ["neptune", "wandb"] and not self.disable_logs:
|
|
||||||
self.writer.save_model(path, self.current_learning_iteration)
|
|
||||||
|
|
||||||
def load(self, path: str, load_optimizer: bool = True, map_location: str | None = None):
|
|
||||||
loaded_dict = torch.load(path, weights_only=False, map_location=map_location)
|
|
||||||
# -- Load model
|
|
||||||
resumed_training = self.alg.policy.load_state_dict(loaded_dict["model_state_dict"])
|
|
||||||
# -- Load RND model if used
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
self.alg.rnd.load_state_dict(loaded_dict["rnd_state_dict"])
|
|
||||||
# -- load optimizer if used
|
|
||||||
if load_optimizer and resumed_training:
|
|
||||||
# -- algorithm optimizer
|
|
||||||
self.alg.optimizer.load_state_dict(loaded_dict["optimizer_state_dict"])
|
|
||||||
# -- RND optimizer if used
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
self.alg.rnd_optimizer.load_state_dict(loaded_dict["rnd_optimizer_state_dict"])
|
|
||||||
# -- load current learning iteration
|
|
||||||
if resumed_training:
|
|
||||||
self.current_learning_iteration = loaded_dict["iter"]
|
|
||||||
return loaded_dict["infos"]
|
|
||||||
|
|
||||||
def get_inference_policy(self, device=None):
|
|
||||||
self.eval_mode() # switch to evaluation mode (dropout for example)
|
|
||||||
if device is not None:
|
|
||||||
self.alg.policy.to(device)
|
|
||||||
return self.alg.policy.act_inference
|
|
||||||
|
|
||||||
def train_mode(self):
|
|
||||||
# -- PPO
|
|
||||||
self.alg.policy.train()
|
|
||||||
# -- RND
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
self.alg.rnd.train()
|
|
||||||
|
|
||||||
def eval_mode(self):
|
|
||||||
# -- PPO
|
|
||||||
self.alg.policy.eval()
|
|
||||||
# -- RND
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
self.alg.rnd.eval()
|
|
||||||
|
|
||||||
def add_git_repo_to_log(self, repo_file_path):
|
|
||||||
self.git_status_repos.append(repo_file_path)
|
|
||||||
|
|
||||||
"""
|
|
||||||
Helper functions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _configure_multi_gpu(self):
|
|
||||||
"""Configure multi-gpu training."""
|
|
||||||
# check if distributed training is enabled
|
|
||||||
self.gpu_world_size = int(os.getenv("WORLD_SIZE", "1"))
|
|
||||||
self.is_distributed = self.gpu_world_size > 1
|
|
||||||
|
|
||||||
# if not distributed training, set local and global rank to 0 and return
|
|
||||||
if not self.is_distributed:
|
|
||||||
self.gpu_local_rank = 0
|
|
||||||
self.gpu_global_rank = 0
|
|
||||||
self.multi_gpu_cfg = None
|
|
||||||
return
|
|
||||||
|
|
||||||
# get rank and world size
|
|
||||||
self.gpu_local_rank = int(os.getenv("LOCAL_RANK", "0"))
|
|
||||||
self.gpu_global_rank = int(os.getenv("RANK", "0"))
|
|
||||||
|
|
||||||
# make a configuration dictionary
|
|
||||||
self.multi_gpu_cfg = {
|
|
||||||
"global_rank": self.gpu_global_rank, # rank of the main process
|
|
||||||
"local_rank": self.gpu_local_rank, # rank of the current process
|
|
||||||
"world_size": self.gpu_world_size, # total number of processes
|
|
||||||
}
|
|
||||||
|
|
||||||
# check if user has device specified for local rank
|
|
||||||
if self.device != f"cuda:{self.gpu_local_rank}":
|
|
||||||
raise ValueError(
|
|
||||||
f"Device '{self.device}' does not match expected device for local rank '{self.gpu_local_rank}'."
|
|
||||||
)
|
|
||||||
# validate multi-gpu configuration
|
|
||||||
if self.gpu_local_rank >= self.gpu_world_size:
|
|
||||||
raise ValueError(
|
|
||||||
f"Local rank '{self.gpu_local_rank}' is greater than or equal to world size '{self.gpu_world_size}'."
|
|
||||||
)
|
|
||||||
if self.gpu_global_rank >= self.gpu_world_size:
|
|
||||||
raise ValueError(
|
|
||||||
f"Global rank '{self.gpu_global_rank}' is greater than or equal to world size '{self.gpu_world_size}'."
|
|
||||||
)
|
|
||||||
|
|
||||||
# initialize torch distributed
|
|
||||||
torch.distributed.init_process_group(backend="nccl", rank=self.gpu_global_rank, world_size=self.gpu_world_size)
|
|
||||||
# set device to the local rank
|
|
||||||
torch.cuda.set_device(self.gpu_local_rank)
|
|
||||||
|
|
||||||
def _construct_algorithm(self, obs) -> PPO:
|
|
||||||
"""Construct the actor-critic algorithm."""
|
|
||||||
# resolve RND config
|
|
||||||
self.alg_cfg = resolve_rnd_config(self.alg_cfg, obs, self.cfg["obs_groups"], self.env)
|
|
||||||
|
|
||||||
# resolve symmetry config
|
|
||||||
self.alg_cfg = resolve_symmetry_config(self.alg_cfg, self.env)
|
|
||||||
|
|
||||||
# resolve deprecated normalization config
|
|
||||||
if self.cfg.get("empirical_normalization") is not None:
|
|
||||||
warnings.warn(
|
|
||||||
"The `empirical_normalization` parameter is deprecated. Please set `actor_obs_normalization` and "
|
|
||||||
"`critic_obs_normalization` as part of the `policy` configuration instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
if self.policy_cfg.get("actor_obs_normalization") is None:
|
|
||||||
self.policy_cfg["actor_obs_normalization"] = self.cfg["empirical_normalization"]
|
|
||||||
if self.policy_cfg.get("critic_obs_normalization") is None:
|
|
||||||
self.policy_cfg["critic_obs_normalization"] = self.cfg["empirical_normalization"]
|
|
||||||
|
|
||||||
# initialize the actor-critic
|
|
||||||
actor_critic_class = eval(self.policy_cfg.pop("class_name"))
|
|
||||||
actor_critic: ActorCritic | ActorCriticRecurrent = actor_critic_class(
|
|
||||||
obs, self.cfg["obs_groups"], self.env.num_actions, **self.policy_cfg
|
|
||||||
).to(self.device)
|
|
||||||
|
|
||||||
# initialize the algorithm
|
|
||||||
alg_class = eval(self.alg_cfg.pop("class_name"))
|
|
||||||
alg: PPO = alg_class(actor_critic, device=self.device, **self.alg_cfg, multi_gpu_cfg=self.multi_gpu_cfg)
|
|
||||||
|
|
||||||
# initialize the storage
|
|
||||||
alg.init_storage(
|
|
||||||
"rl",
|
|
||||||
self.env.num_envs,
|
|
||||||
self.num_steps_per_env,
|
|
||||||
obs,
|
|
||||||
[self.env.num_actions],
|
|
||||||
)
|
|
||||||
|
|
||||||
return alg
|
|
||||||
|
|
||||||
def _prepare_logging_writer(self):
|
|
||||||
"""Prepares the logging writers."""
|
|
||||||
if self.log_dir is not None and self.writer is None and not self.disable_logs:
|
|
||||||
# Launch either Tensorboard or Neptune & Tensorboard summary writer(s), default: Tensorboard.
|
|
||||||
self.logger_type = self.cfg.get("logger", "tensorboard")
|
|
||||||
self.logger_type = self.logger_type.lower()
|
|
||||||
|
|
||||||
if self.logger_type == "neptune":
|
|
||||||
from rsl_rl.utils.neptune_utils import NeptuneSummaryWriter
|
|
||||||
|
|
||||||
self.writer = NeptuneSummaryWriter(log_dir=self.log_dir, flush_secs=10, cfg=self.cfg)
|
|
||||||
self.writer.log_config(self.env.cfg, self.cfg, self.alg_cfg, self.policy_cfg)
|
|
||||||
elif self.logger_type == "wandb":
|
|
||||||
from rsl_rl.utils.wandb_utils import WandbSummaryWriter
|
|
||||||
|
|
||||||
self.writer = WandbSummaryWriter(log_dir=self.log_dir, flush_secs=10, cfg=self.cfg)
|
|
||||||
self.writer.log_config(self.env.cfg, self.cfg, self.alg_cfg, self.policy_cfg)
|
|
||||||
elif self.logger_type == "tensorboard":
|
|
||||||
from torch.utils.tensorboard import SummaryWriter
|
|
||||||
|
|
||||||
self.writer = SummaryWriter(log_dir=self.log_dir, flush_secs=10)
|
|
||||||
else:
|
|
||||||
raise ValueError("Logger type not found. Please choose 'neptune', 'wandb' or 'tensorboard'.")
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
"""Implementation of transitions storage for RL-agent."""
|
|
||||||
|
|
||||||
from .rollout_storage import RolloutStorage
|
|
||||||
from .replay_buffer_multi import ReplayBufferMulti
|
|
||||||
__all__ = ["RolloutStorage", "ReplayBufferMulti"]
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import torch
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
|
|
||||||
class ReplayBufferMulti:
|
|
||||||
"""Fixed-size buffer to store experience tuples."""
|
|
||||||
|
|
||||||
def __init__(self, obs_dim, buffer_size, num_amp_frames, device):
|
|
||||||
"""Initialize a ReplayBuffer object.
|
|
||||||
Arguments:
|
|
||||||
buffer_size (int): maximum size of buffer
|
|
||||||
"""
|
|
||||||
self.states = torch.zeros(buffer_size, num_amp_frames, obs_dim).to(device)
|
|
||||||
self.num_amp_frames = num_amp_frames
|
|
||||||
self.buffer_size = buffer_size
|
|
||||||
self.device = device
|
|
||||||
|
|
||||||
self.step = 0
|
|
||||||
self.num_samples = 0
|
|
||||||
|
|
||||||
def insert(self, states):
|
|
||||||
"""Add new states to memory."""
|
|
||||||
num_states = states.shape[0]
|
|
||||||
start_idx = self.step
|
|
||||||
end_idx = self.step + num_states
|
|
||||||
if end_idx > self.buffer_size:
|
|
||||||
self.states[self.step:self.buffer_size] = states[:self.buffer_size - self.step]
|
|
||||||
self.states[:end_idx - self.buffer_size] = states[self.buffer_size - self.step:]
|
|
||||||
else:
|
|
||||||
self.states[start_idx:end_idx] = states
|
|
||||||
|
|
||||||
self.num_samples = min(self.buffer_size, max(end_idx, self.num_samples))
|
|
||||||
self.step = (self.step + num_states) % self.buffer_size
|
|
||||||
|
|
||||||
def feed_forward_generator(self, num_mini_batch, mini_batch_size):
|
|
||||||
for _ in range(num_mini_batch):
|
|
||||||
sample_idxs = np.random.choice(self.num_samples, size=mini_batch_size)
|
|
||||||
yield (self.states[sample_idxs].to(self.device))
|
|
||||||
|
|
@ -1,260 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch
|
|
||||||
from tensordict import TensorDict
|
|
||||||
|
|
||||||
from rsl_rl.utils import split_and_pad_trajectories
|
|
||||||
|
|
||||||
|
|
||||||
class RolloutStorage:
|
|
||||||
class Transition:
|
|
||||||
def __init__(self):
|
|
||||||
self.observations = None
|
|
||||||
self.actions = None
|
|
||||||
self.privileged_actions = None
|
|
||||||
self.rewards = None
|
|
||||||
self.dones = None
|
|
||||||
self.values = None
|
|
||||||
self.actions_log_prob = None
|
|
||||||
self.action_mean = None
|
|
||||||
self.action_sigma = None
|
|
||||||
self.hidden_states = None
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
self.__init__()
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
training_type,
|
|
||||||
num_envs,
|
|
||||||
num_transitions_per_env,
|
|
||||||
obs,
|
|
||||||
actions_shape,
|
|
||||||
device="cpu",
|
|
||||||
):
|
|
||||||
# store inputs
|
|
||||||
self.training_type = training_type
|
|
||||||
self.device = device
|
|
||||||
self.num_transitions_per_env = num_transitions_per_env
|
|
||||||
self.num_envs = num_envs
|
|
||||||
self.actions_shape = actions_shape
|
|
||||||
|
|
||||||
# Core
|
|
||||||
self.observations = TensorDict(
|
|
||||||
{key: torch.zeros(num_transitions_per_env, *value.shape, device=device) for key, value in obs.items()},
|
|
||||||
batch_size=[num_transitions_per_env, num_envs],
|
|
||||||
device=self.device,
|
|
||||||
)
|
|
||||||
self.rewards = torch.zeros(num_transitions_per_env, num_envs, 1, device=self.device)
|
|
||||||
self.actions = torch.zeros(num_transitions_per_env, num_envs, *actions_shape, device=self.device)
|
|
||||||
self.dones = torch.zeros(num_transitions_per_env, num_envs, 1, device=self.device).byte()
|
|
||||||
|
|
||||||
# for distillation
|
|
||||||
if training_type == "distillation":
|
|
||||||
self.privileged_actions = torch.zeros(num_transitions_per_env, num_envs, *actions_shape, device=self.device)
|
|
||||||
|
|
||||||
# for reinforcement learning
|
|
||||||
if training_type == "rl":
|
|
||||||
self.values = torch.zeros(num_transitions_per_env, num_envs, 1, device=self.device)
|
|
||||||
self.actions_log_prob = torch.zeros(num_transitions_per_env, num_envs, 1, device=self.device)
|
|
||||||
self.mu = torch.zeros(num_transitions_per_env, num_envs, *actions_shape, device=self.device)
|
|
||||||
self.sigma = torch.zeros(num_transitions_per_env, num_envs, *actions_shape, device=self.device)
|
|
||||||
self.returns = torch.zeros(num_transitions_per_env, num_envs, 1, device=self.device)
|
|
||||||
self.advantages = torch.zeros(num_transitions_per_env, num_envs, 1, device=self.device)
|
|
||||||
|
|
||||||
# For RNN networks
|
|
||||||
self.saved_hidden_states_a = None
|
|
||||||
self.saved_hidden_states_c = None
|
|
||||||
|
|
||||||
# counter for the number of transitions stored
|
|
||||||
self.step = 0
|
|
||||||
|
|
||||||
def add_transitions(self, transition: Transition):
|
|
||||||
# check if the transition is valid
|
|
||||||
if self.step >= self.num_transitions_per_env:
|
|
||||||
raise OverflowError("Rollout buffer overflow! You should call clear() before adding new transitions.")
|
|
||||||
|
|
||||||
# Core
|
|
||||||
self.observations[self.step].copy_(transition.observations)
|
|
||||||
self.actions[self.step].copy_(transition.actions)
|
|
||||||
self.rewards[self.step].copy_(transition.rewards.view(-1, 1))
|
|
||||||
self.dones[self.step].copy_(transition.dones.view(-1, 1))
|
|
||||||
|
|
||||||
# for distillation
|
|
||||||
if self.training_type == "distillation":
|
|
||||||
self.privileged_actions[self.step].copy_(transition.privileged_actions)
|
|
||||||
|
|
||||||
# for reinforcement learning
|
|
||||||
if self.training_type == "rl":
|
|
||||||
self.values[self.step].copy_(transition.values)
|
|
||||||
self.actions_log_prob[self.step].copy_(transition.actions_log_prob.view(-1, 1))
|
|
||||||
self.mu[self.step].copy_(transition.action_mean)
|
|
||||||
self.sigma[self.step].copy_(transition.action_sigma)
|
|
||||||
|
|
||||||
# For RNN networks
|
|
||||||
self._save_hidden_states(transition.hidden_states)
|
|
||||||
|
|
||||||
# increment the counter
|
|
||||||
self.step += 1
|
|
||||||
|
|
||||||
def _save_hidden_states(self, hidden_states):
|
|
||||||
if hidden_states is None or hidden_states == (None, None):
|
|
||||||
return
|
|
||||||
# make a tuple out of GRU hidden state sto match the LSTM format
|
|
||||||
hid_a = hidden_states[0] if isinstance(hidden_states[0], tuple) else (hidden_states[0],)
|
|
||||||
hid_c = hidden_states[1] if isinstance(hidden_states[1], tuple) else (hidden_states[1],)
|
|
||||||
# initialize if needed
|
|
||||||
if self.saved_hidden_states_a is None:
|
|
||||||
self.saved_hidden_states_a = [
|
|
||||||
torch.zeros(self.observations.shape[0], *hid_a[i].shape, device=self.device) for i in range(len(hid_a))
|
|
||||||
]
|
|
||||||
self.saved_hidden_states_c = [
|
|
||||||
torch.zeros(self.observations.shape[0], *hid_c[i].shape, device=self.device) for i in range(len(hid_c))
|
|
||||||
]
|
|
||||||
# copy the states
|
|
||||||
for i in range(len(hid_a)):
|
|
||||||
self.saved_hidden_states_a[i][self.step].copy_(hid_a[i])
|
|
||||||
self.saved_hidden_states_c[i][self.step].copy_(hid_c[i])
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
self.step = 0
|
|
||||||
|
|
||||||
def compute_returns(self, last_values, gamma, lam, normalize_advantage: bool = True):
|
|
||||||
advantage = 0
|
|
||||||
for step in reversed(range(self.num_transitions_per_env)):
|
|
||||||
# if we are at the last step, bootstrap the return value
|
|
||||||
if step == self.num_transitions_per_env - 1:
|
|
||||||
next_values = last_values
|
|
||||||
else:
|
|
||||||
next_values = self.values[step + 1]
|
|
||||||
# 1 if we are not in a terminal state, 0 otherwise
|
|
||||||
next_is_not_terminal = 1.0 - self.dones[step].float()
|
|
||||||
# TD error: r_t + gamma * V(s_{t+1}) - V(s_t)
|
|
||||||
delta = self.rewards[step] + next_is_not_terminal * gamma * next_values - self.values[step]
|
|
||||||
# Advantage: A(s_t, a_t) = delta_t + gamma * lambda * A(s_{t+1}, a_{t+1})
|
|
||||||
advantage = delta + next_is_not_terminal * gamma * lam * advantage
|
|
||||||
# Return: R_t = A(s_t, a_t) + V(s_t)
|
|
||||||
self.returns[step] = advantage + self.values[step]
|
|
||||||
|
|
||||||
# Compute the advantages
|
|
||||||
self.advantages = self.returns - self.values
|
|
||||||
# Normalize the advantages if flag is set
|
|
||||||
# This is to prevent double normalization (i.e. if per minibatch normalization is used)
|
|
||||||
if normalize_advantage:
|
|
||||||
self.advantages = (self.advantages - self.advantages.mean()) / (self.advantages.std() + 1e-8)
|
|
||||||
|
|
||||||
# for distillation
|
|
||||||
def generator(self):
|
|
||||||
if self.training_type != "distillation":
|
|
||||||
raise ValueError("This function is only available for distillation training.")
|
|
||||||
|
|
||||||
for i in range(self.num_transitions_per_env):
|
|
||||||
yield self.observations[i], self.actions[i], self.privileged_actions[i], self.dones[i]
|
|
||||||
|
|
||||||
# for reinforcement learning with feedforward networks
|
|
||||||
def mini_batch_generator(self, num_mini_batches, num_epochs=8):
|
|
||||||
if self.training_type != "rl":
|
|
||||||
raise ValueError("This function is only available for reinforcement learning training.")
|
|
||||||
batch_size = self.num_envs * self.num_transitions_per_env
|
|
||||||
mini_batch_size = batch_size // num_mini_batches
|
|
||||||
indices = torch.randperm(num_mini_batches * mini_batch_size, requires_grad=False, device=self.device)
|
|
||||||
|
|
||||||
# Core
|
|
||||||
observations = self.observations.flatten(0, 1)
|
|
||||||
actions = self.actions.flatten(0, 1)
|
|
||||||
values = self.values.flatten(0, 1)
|
|
||||||
returns = self.returns.flatten(0, 1)
|
|
||||||
|
|
||||||
# For PPO
|
|
||||||
old_actions_log_prob = self.actions_log_prob.flatten(0, 1)
|
|
||||||
advantages = self.advantages.flatten(0, 1)
|
|
||||||
old_mu = self.mu.flatten(0, 1)
|
|
||||||
old_sigma = self.sigma.flatten(0, 1)
|
|
||||||
|
|
||||||
for epoch in range(num_epochs):
|
|
||||||
for i in range(num_mini_batches):
|
|
||||||
# Select the indices for the mini-batch
|
|
||||||
start = i * mini_batch_size
|
|
||||||
end = (i + 1) * mini_batch_size
|
|
||||||
batch_idx = indices[start:end]
|
|
||||||
|
|
||||||
# Create the mini-batch
|
|
||||||
# -- Core
|
|
||||||
obs_batch = observations[batch_idx]
|
|
||||||
actions_batch = actions[batch_idx]
|
|
||||||
|
|
||||||
# -- For PPO
|
|
||||||
target_values_batch = values[batch_idx]
|
|
||||||
returns_batch = returns[batch_idx]
|
|
||||||
old_actions_log_prob_batch = old_actions_log_prob[batch_idx]
|
|
||||||
advantages_batch = advantages[batch_idx]
|
|
||||||
old_mu_batch = old_mu[batch_idx]
|
|
||||||
old_sigma_batch = old_sigma[batch_idx]
|
|
||||||
|
|
||||||
# yield the mini-batch
|
|
||||||
yield obs_batch, actions_batch, target_values_batch, advantages_batch, returns_batch, old_actions_log_prob_batch, old_mu_batch, old_sigma_batch, (
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
), None
|
|
||||||
|
|
||||||
# for reinfrocement learning with recurrent networks
|
|
||||||
def recurrent_mini_batch_generator(self, num_mini_batches, num_epochs=8):
|
|
||||||
if self.training_type != "rl":
|
|
||||||
raise ValueError("This function is only available for reinforcement learning training.")
|
|
||||||
padded_obs_trajectories, trajectory_masks = split_and_pad_trajectories(self.observations, self.dones)
|
|
||||||
|
|
||||||
mini_batch_size = self.num_envs // num_mini_batches
|
|
||||||
for ep in range(num_epochs):
|
|
||||||
first_traj = 0
|
|
||||||
for i in range(num_mini_batches):
|
|
||||||
start = i * mini_batch_size
|
|
||||||
stop = (i + 1) * mini_batch_size
|
|
||||||
|
|
||||||
dones = self.dones.squeeze(-1)
|
|
||||||
last_was_done = torch.zeros_like(dones, dtype=torch.bool)
|
|
||||||
last_was_done[1:] = dones[:-1]
|
|
||||||
last_was_done[0] = True
|
|
||||||
trajectories_batch_size = torch.sum(last_was_done[:, start:stop])
|
|
||||||
last_traj = first_traj + trajectories_batch_size
|
|
||||||
|
|
||||||
masks_batch = trajectory_masks[:, first_traj:last_traj]
|
|
||||||
obs_batch = padded_obs_trajectories[:, first_traj:last_traj]
|
|
||||||
actions_batch = self.actions[:, start:stop]
|
|
||||||
old_mu_batch = self.mu[:, start:stop]
|
|
||||||
old_sigma_batch = self.sigma[:, start:stop]
|
|
||||||
returns_batch = self.returns[:, start:stop]
|
|
||||||
advantages_batch = self.advantages[:, start:stop]
|
|
||||||
values_batch = self.values[:, start:stop]
|
|
||||||
old_actions_log_prob_batch = self.actions_log_prob[:, start:stop]
|
|
||||||
|
|
||||||
# reshape to [num_envs, time, num layers, hidden dim] (original shape: [time, num_layers, num_envs, hidden_dim])
|
|
||||||
# then take only time steps after dones (flattens num envs and time dimensions),
|
|
||||||
# take a batch of trajectories and finally reshape back to [num_layers, batch, hidden_dim]
|
|
||||||
last_was_done = last_was_done.permute(1, 0)
|
|
||||||
hid_a_batch = [
|
|
||||||
saved_hidden_states.permute(2, 0, 1, 3)[last_was_done][first_traj:last_traj]
|
|
||||||
.transpose(1, 0)
|
|
||||||
.contiguous()
|
|
||||||
for saved_hidden_states in self.saved_hidden_states_a
|
|
||||||
]
|
|
||||||
hid_c_batch = [
|
|
||||||
saved_hidden_states.permute(2, 0, 1, 3)[last_was_done][first_traj:last_traj]
|
|
||||||
.transpose(1, 0)
|
|
||||||
.contiguous()
|
|
||||||
for saved_hidden_states in self.saved_hidden_states_c
|
|
||||||
]
|
|
||||||
# remove the tuple for GRU
|
|
||||||
hid_a_batch = hid_a_batch[0] if len(hid_a_batch) == 1 else hid_a_batch
|
|
||||||
hid_c_batch = hid_c_batch[0] if len(hid_c_batch) == 1 else hid_c_batch
|
|
||||||
|
|
||||||
yield obs_batch, actions_batch, values_batch, advantages_batch, returns_batch, old_actions_log_prob_batch, old_mu_batch, old_sigma_batch, (
|
|
||||||
hid_a_batch,
|
|
||||||
hid_c_batch,
|
|
||||||
), masks_batch
|
|
||||||
|
|
||||||
first_traj = last_traj
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
"""Helper functions."""
|
|
||||||
|
|
||||||
from .utils import *
|
|
||||||
from .motion_loader_g1 import G1_AMPLoader
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"G1_AMPLoader",
|
|
||||||
]
|
|
||||||
|
|
@ -1,388 +0,0 @@
|
||||||
import os
|
|
||||||
from os.path import join as pjoin
|
|
||||||
import glob
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import numpy as np
|
|
||||||
from pybullet_utils import transformations
|
|
||||||
|
|
||||||
from rsl_rl.utils import motion_util
|
|
||||||
|
|
||||||
_EPS = np.finfo(float).eps * 4.0
|
|
||||||
def quaternion_slerp(q0, q1, fraction, spin=0, shortestpath=True):
|
|
||||||
"""Batch quaternion spherical linear interpolation."""
|
|
||||||
|
|
||||||
out = torch.zeros_like(q0)
|
|
||||||
|
|
||||||
zero_mask = torch.isclose(fraction, torch.zeros_like(fraction)).squeeze()
|
|
||||||
ones_mask = torch.isclose(fraction, torch.ones_like(fraction)).squeeze()
|
|
||||||
out[zero_mask] = q0[zero_mask]
|
|
||||||
out[ones_mask] = q1[ones_mask]
|
|
||||||
|
|
||||||
d = torch.sum(q0 * q1, dim=-1, keepdim=True)
|
|
||||||
dist_mask = (torch.abs(torch.abs(d) - 1.0) < _EPS).squeeze()
|
|
||||||
out[dist_mask] = q0[dist_mask]
|
|
||||||
|
|
||||||
if shortestpath:
|
|
||||||
d_old = torch.clone(d)
|
|
||||||
d = torch.where(d_old < 0, -d, d)
|
|
||||||
q1 = torch.where(d_old < 0, -q1, q1)
|
|
||||||
|
|
||||||
angle = torch.acos(d) + spin * torch.pi
|
|
||||||
angle_mask = (torch.abs(angle) < _EPS).squeeze()
|
|
||||||
out[angle_mask] = q0[angle_mask]
|
|
||||||
|
|
||||||
final_mask = torch.logical_or(zero_mask, ones_mask)
|
|
||||||
final_mask = torch.logical_or(final_mask, dist_mask)
|
|
||||||
final_mask = torch.logical_or(final_mask, angle_mask)
|
|
||||||
final_mask = torch.logical_not(final_mask)
|
|
||||||
|
|
||||||
isin = 1.0 / angle
|
|
||||||
q0 *= torch.sin((1.0 - fraction) * angle) * isin
|
|
||||||
q1 *= torch.sin(fraction * angle) * isin
|
|
||||||
q0 += q1
|
|
||||||
out[final_mask] = q0[final_mask]
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
class G1_AMPLoader:
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
device,
|
|
||||||
time_between_frames,
|
|
||||||
motion_files,
|
|
||||||
preload_transitions=False,
|
|
||||||
num_preload_transitions=1000000,
|
|
||||||
num_frames=5,
|
|
||||||
):
|
|
||||||
"""Expert dataset provides AMP observations from Dog mocap dataset.
|
|
||||||
|
|
||||||
time_between_frames: Amount of time in seconds between transition.
|
|
||||||
"""
|
|
||||||
self.device = device
|
|
||||||
self.time_between_frames = time_between_frames
|
|
||||||
self.num_frames = num_frames
|
|
||||||
|
|
||||||
# Values to store for each trajectory.
|
|
||||||
self.trajectories = []
|
|
||||||
self.trajectories_full = []
|
|
||||||
self.trajectory_names = []
|
|
||||||
self.trajectory_idxs = []
|
|
||||||
self.trajectory_lens = [] # Traj length in seconds.
|
|
||||||
self.trajectory_weights = []
|
|
||||||
self.trajectory_frame_durations = []
|
|
||||||
self.trajectory_num_frames = []
|
|
||||||
self.motion_dir = motion_files
|
|
||||||
# import ipdb; ipdb.set_trace()
|
|
||||||
for i, motion_file in enumerate(os.listdir(motion_files)):
|
|
||||||
self.trajectory_names.append(motion_file)
|
|
||||||
motion_path = pjoin(motion_files, motion_file)
|
|
||||||
motion_data = np.load(motion_path, allow_pickle=True)
|
|
||||||
motion_data_processed = np.zeros((motion_data.shape[0],36))
|
|
||||||
|
|
||||||
for f_i in range(motion_data.shape[0]):
|
|
||||||
motion_data_processed[f_i, :3] = motion_data[f_i, :3] # base pos
|
|
||||||
motion_data_processed[f_i, 3:7] = motion_data[f_i, 3:7] # base quat (wxyz)
|
|
||||||
motion_data_processed[f_i, 7:35] = motion_data[f_i, 7:35] # base vel
|
|
||||||
'''
|
|
||||||
NOTE The order of motion_data_processed is
|
|
||||||
base pos 0:3,
|
|
||||||
base quat 3:7, wxyz
|
|
||||||
dof pos 7:36, (mujoco joint order)
|
|
||||||
'''
|
|
||||||
self.trajectories.append(torch.tensor(
|
|
||||||
motion_data_processed[:, 7:],
|
|
||||||
dtype=torch.float32,
|
|
||||||
device=self.device
|
|
||||||
))
|
|
||||||
|
|
||||||
self.trajectories_full.append(torch.tensor(
|
|
||||||
motion_data_processed,
|
|
||||||
dtype=torch.float32,
|
|
||||||
device=self.device
|
|
||||||
))
|
|
||||||
|
|
||||||
self.trajectory_idxs.append(i)
|
|
||||||
self.trajectory_weights.append(1 / len(os.listdir(motion_files)))
|
|
||||||
frame_duration = 1 / 50
|
|
||||||
|
|
||||||
self.trajectory_frame_durations.append(frame_duration)
|
|
||||||
traj_len = (motion_data_processed.shape[0] - 1) * frame_duration # seconds
|
|
||||||
self.trajectory_lens.append(traj_len)
|
|
||||||
self.trajectory_num_frames.append(float(motion_data_processed.shape[0]))
|
|
||||||
print(f"Loaded {traj_len}s. motion from {motion_file}.")
|
|
||||||
|
|
||||||
# Trajectory weights are used to sample some trajectories more than others.
|
|
||||||
self.trajectory_weights = np.array(self.trajectory_weights) / np.sum(self.trajectory_weights)
|
|
||||||
self.trajectory_frame_durations = np.array(self.trajectory_frame_durations)
|
|
||||||
self.trajectory_lens = np.array(self.trajectory_lens)
|
|
||||||
self.trajectory_num_frames = np.array(self.trajectory_num_frames)
|
|
||||||
|
|
||||||
# Preload transitions.
|
|
||||||
self.preload_transitions = preload_transitions
|
|
||||||
if self.preload_transitions:
|
|
||||||
print(f'Preloading {num_preload_transitions} transitions')
|
|
||||||
|
|
||||||
traj_idxs = self.weighted_traj_idx_sample_batch(num_preload_transitions)
|
|
||||||
times = self.traj_time_sample_batch(traj_idxs)
|
|
||||||
self.preloaded_s_prior = self.get_full_frame_at_time_batch(traj_idxs, times - self.time_between_frames)
|
|
||||||
self.preloaded_s = self.get_full_frame_at_time_batch(traj_idxs, times)
|
|
||||||
self.preloaded_s_next = self.get_full_frame_at_time_batch(traj_idxs, times + self.time_between_frames)
|
|
||||||
print(f'Finished preloading')
|
|
||||||
|
|
||||||
# 预加载多帧数据
|
|
||||||
self.preloaded_frames = []
|
|
||||||
for i in range(self.num_frames):
|
|
||||||
frame_time = times + (i - (self.num_frames - 2)) * self.time_between_frames
|
|
||||||
full_frame = self.get_full_frame_at_time_batch(traj_idxs, frame_time)
|
|
||||||
# 预处理:提前提取并连接需要的列(7:26 和 29:33),避免每次生成时重复切片
|
|
||||||
processed_frame = torch.cat([
|
|
||||||
full_frame[:, 7:26],
|
|
||||||
full_frame[:, 29:33]
|
|
||||||
], dim=-1)
|
|
||||||
self.preloaded_frames.append(processed_frame)
|
|
||||||
print(f'Finished preloading multiple frames')
|
|
||||||
|
|
||||||
self.all_trajectories_full = torch.vstack(self.trajectories_full)
|
|
||||||
|
|
||||||
def weighted_traj_idx_sample(self):
|
|
||||||
"""Get traj idx via weighted sampling."""
|
|
||||||
return np.random.choice(
|
|
||||||
self.trajectory_idxs, p=self.trajectory_weights)
|
|
||||||
|
|
||||||
def weighted_traj_idx_sample_batch(self, size):
|
|
||||||
"""Batch sample traj idxs."""
|
|
||||||
return np.random.choice(
|
|
||||||
self.trajectory_idxs, size=size, p=self.trajectory_weights,
|
|
||||||
replace=True)
|
|
||||||
|
|
||||||
def traj_time_sample(self, traj_idx):
|
|
||||||
"""Sample random time for traj."""
|
|
||||||
subst = self.time_between_frames + self.trajectory_frame_durations[traj_idx]
|
|
||||||
return max(
|
|
||||||
0, (self.trajectory_lens[traj_idx] * np.random.uniform() - subst))
|
|
||||||
|
|
||||||
def traj_time_sample_batch(self, traj_idxs):
|
|
||||||
"""Sample random time for multiple trajectories."""
|
|
||||||
subst = self.time_between_frames + self.trajectory_frame_durations[traj_idxs]
|
|
||||||
time_samples = self.trajectory_lens[traj_idxs] * np.random.uniform(size=len(traj_idxs)) - subst
|
|
||||||
return np.maximum(np.zeros_like(time_samples), time_samples)
|
|
||||||
|
|
||||||
def slerp(self, val0, val1, blend):
|
|
||||||
return (1.0 - blend) * val0 + blend * val1
|
|
||||||
|
|
||||||
def get_trajectory(self, traj_idx):
|
|
||||||
"""Returns trajectory of AMP observations."""
|
|
||||||
return self.trajectories_full[traj_idx]
|
|
||||||
|
|
||||||
def get_frame_at_time(self, traj_idx, time):
|
|
||||||
"""Returns frame for the given trajectory at the specified time."""
|
|
||||||
p = float(time) / self.trajectory_lens[traj_idx]
|
|
||||||
n = self.trajectories[traj_idx].shape[0]
|
|
||||||
idx_low, idx_high = int(np.floor(p * n)), int(np.ceil(p * n))
|
|
||||||
frame_start = self.trajectories[traj_idx][idx_low]
|
|
||||||
frame_end = self.trajectories[traj_idx][idx_high]
|
|
||||||
blend = p * n - idx_low
|
|
||||||
return self.slerp(frame_start, frame_end, blend)
|
|
||||||
|
|
||||||
def get_frame_at_time_batch(self, traj_idxs, times):
|
|
||||||
"""Returns frame for the given trajectory at the specified time."""
|
|
||||||
p = times / self.trajectory_lens[traj_idxs]
|
|
||||||
n = self.trajectory_num_frames[traj_idxs]
|
|
||||||
idx_low, idx_high = np.floor(p * n).astype(np.int32), np.ceil(p * n).astype(np.int32)
|
|
||||||
all_frame_starts = torch.zeros(len(traj_idxs), self.observation_dim, device=self.device)
|
|
||||||
all_frame_ends = torch.zeros(len(traj_idxs), self.observation_dim, device=self.device)
|
|
||||||
for traj_idx in set(traj_idxs):
|
|
||||||
trajectory = self.trajectories[traj_idx]
|
|
||||||
traj_mask = traj_idxs == traj_idx
|
|
||||||
all_frame_starts[traj_mask] = trajectory[idx_low[traj_mask]]
|
|
||||||
all_frame_ends[traj_mask] = trajectory[idx_high[traj_mask]]
|
|
||||||
blend = torch.tensor(p * n - idx_low, device=self.device, dtype=torch.float32).unsqueeze(-1)
|
|
||||||
return self.slerp(all_frame_starts, all_frame_ends, blend)
|
|
||||||
|
|
||||||
def get_full_frame_at_time(self, traj_idx, time):
|
|
||||||
"""Returns full frame for the given trajectory at the specified time."""
|
|
||||||
p = float(time) / self.trajectory_lens[traj_idx]
|
|
||||||
n = self.trajectories_full[traj_idx].shape[0]
|
|
||||||
idx_low, idx_high = int(np.floor(p * n)), int(np.ceil(p * n))
|
|
||||||
frame_start = self.trajectories_full[traj_idx][idx_low]
|
|
||||||
frame_end = self.trajectories_full[traj_idx][idx_high]
|
|
||||||
blend = p * n - idx_low
|
|
||||||
print(idx_low, idx_high)
|
|
||||||
return self.blend_frame_pose(frame_start, frame_end, blend)
|
|
||||||
|
|
||||||
def get_full_frame_at_time_batch(self, traj_idxs, times):
|
|
||||||
p = times / self.trajectory_lens[traj_idxs]
|
|
||||||
n = self.trajectory_num_frames[traj_idxs]
|
|
||||||
idx_low, idx_high = np.floor(p * n).astype(np.int32), np.ceil(p * n).astype(np.int32)
|
|
||||||
all_frame_pos_starts = torch.zeros(len(traj_idxs), 3, device=self.device)
|
|
||||||
all_frame_pos_ends = torch.zeros(len(traj_idxs), 3, device=self.device)
|
|
||||||
all_frame_rot_starts = torch.zeros(len(traj_idxs), 4, device=self.device)
|
|
||||||
all_frame_rot_ends = torch.zeros(len(traj_idxs), 4, device=self.device)
|
|
||||||
all_frame_amp_starts = torch.zeros(len(traj_idxs), 29, device=self.device)
|
|
||||||
all_frame_amp_ends = torch.zeros(len(traj_idxs), 29, device=self.device)
|
|
||||||
for traj_idx in set(traj_idxs):
|
|
||||||
trajectory = self.trajectories_full[traj_idx]
|
|
||||||
traj_mask = traj_idxs == traj_idx
|
|
||||||
all_frame_pos_starts[traj_mask] = G1_AMPLoader.get_root_pos_batch(trajectory[idx_low[traj_mask]])
|
|
||||||
all_frame_pos_ends[traj_mask] = G1_AMPLoader.get_root_pos_batch(trajectory[idx_high[traj_mask]])
|
|
||||||
all_frame_rot_starts[traj_mask] = G1_AMPLoader.get_root_rot_batch(trajectory[idx_low[traj_mask]])
|
|
||||||
all_frame_rot_ends[traj_mask] = G1_AMPLoader.get_root_rot_batch(trajectory[idx_high[traj_mask]])
|
|
||||||
all_frame_amp_starts[traj_mask] = trajectory[idx_low[traj_mask]][:, 7:36] # base vel3+ang3, dof vel23+ang23
|
|
||||||
all_frame_amp_ends[traj_mask] = trajectory[idx_high[traj_mask]][:, 7:36] # base vel3+ang3, dof vel23+ang23
|
|
||||||
blend = torch.tensor(p * n - idx_low, device=self.device, dtype=torch.float32).unsqueeze(-1)
|
|
||||||
pos_blend = self.slerp(all_frame_pos_starts, all_frame_pos_ends, blend)
|
|
||||||
rot_blend = quaternion_slerp(all_frame_rot_starts, all_frame_rot_ends, blend)
|
|
||||||
amp_blend = self.slerp(all_frame_amp_starts, all_frame_amp_ends, blend)
|
|
||||||
return torch.cat([pos_blend, rot_blend, amp_blend], dim=-1)
|
|
||||||
|
|
||||||
def get_frame(self):
|
|
||||||
"""Returns random frame."""
|
|
||||||
traj_idx = self.weighted_traj_idx_sample()
|
|
||||||
sampled_time = self.traj_time_sample(traj_idx)
|
|
||||||
return self.get_frame_at_time(traj_idx, sampled_time)
|
|
||||||
|
|
||||||
def get_full_frame(self):
|
|
||||||
"""Returns random full frame."""
|
|
||||||
traj_idx = self.weighted_traj_idx_sample()
|
|
||||||
sampled_time = self.traj_time_sample(traj_idx)
|
|
||||||
return self.get_full_frame_at_time(traj_idx, sampled_time)
|
|
||||||
|
|
||||||
def get_full_frame_batch(self, num_frames):
|
|
||||||
if self.preload_transitions:
|
|
||||||
idxs = np.random.choice(
|
|
||||||
self.preloaded_s.shape[0], size=num_frames)
|
|
||||||
return self.preloaded_s[idxs]
|
|
||||||
else:
|
|
||||||
traj_idxs = self.weighted_traj_idx_sample_batch(num_frames)
|
|
||||||
times = self.traj_time_sample_batch(traj_idxs)
|
|
||||||
return self.get_full_frame_at_time_batch(traj_idxs, times)
|
|
||||||
|
|
||||||
def blend_frame_pose(self, frame0, frame1, blend):
|
|
||||||
"""Linearly interpolate between two frames, including orientation.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
frame0: First frame to be blended corresponds to (blend = 0).
|
|
||||||
frame1: Second frame to be blended corresponds to (blend = 1).
|
|
||||||
blend: Float between [0, 1], specifying the interpolation between
|
|
||||||
the two frames.
|
|
||||||
Returns:
|
|
||||||
An interpolation of the two frames.
|
|
||||||
"""
|
|
||||||
root_pos0, root_pos1 = G1_AMPLoader.get_root_pos(frame0), G1_AMPLoader.get_root_pos(frame1)
|
|
||||||
root_rot0, root_rot1 = G1_AMPLoader.get_root_rot(frame0), G1_AMPLoader.get_root_rot(frame1)
|
|
||||||
joints0, joints1 = G1_AMPLoader.get_joint_pose(frame0), G1_AMPLoader.get_joint_pose(frame1)
|
|
||||||
# tar_toe_pos_0, tar_toe_pos_1 = G1_AMPLoader.get_tar_toe_pos_local(frame0), G1_AMPLoader.get_tar_toe_pos_local(frame1)
|
|
||||||
linear_vel_0, linear_vel_1 = G1_AMPLoader.get_linear_vel(frame0), G1_AMPLoader.get_linear_vel(frame1)
|
|
||||||
angular_vel_0, angular_vel_1 = G1_AMPLoader.get_angular_vel(frame0), G1_AMPLoader.get_angular_vel(frame1)
|
|
||||||
joint_vel_0, joint_vel_1 = G1_AMPLoader.get_joint_vel(frame0), G1_AMPLoader.get_joint_vel(frame1)
|
|
||||||
|
|
||||||
blend_root_pos = self.slerp(root_pos0, root_pos1, blend)
|
|
||||||
blend_root_rot = transformations.quaternion_slerp(root_rot0.cpu().numpy(), root_rot1.cpu().numpy(), blend)
|
|
||||||
blend_root_rot = torch.tensor(motion_util.standardize_quaternion(blend_root_rot),dtype=torch.float32, device=self.device)
|
|
||||||
blend_joints = self.slerp(joints0, joints1, blend)
|
|
||||||
# blend_tar_toe_pos = self.slerp(tar_toe_pos_0, tar_toe_pos_1, blend)
|
|
||||||
blend_linear_vel = self.slerp(linear_vel_0, linear_vel_1, blend)
|
|
||||||
blend_angular_vel = self.slerp(angular_vel_0, angular_vel_1, blend)
|
|
||||||
blend_joints_vel = self.slerp(joint_vel_0, joint_vel_1, blend)
|
|
||||||
|
|
||||||
# return
|
|
||||||
# torch.cat([
|
|
||||||
# blend_root_pos, blend_root_rot, blend_linear_vel, blend_angular_vel, blend_joints, blend_joints_vel])
|
|
||||||
return torch.cat([blend_root_pos, blend_root_rot, blend_linear_vel, blend_angular_vel, blend_joints])
|
|
||||||
|
|
||||||
def feed_forward_generator_23dof_multi(self, num_mini_batch, mini_batch_size):
|
|
||||||
"""Generates a batch of AMP transitions."""
|
|
||||||
# import ipdb; ipdb.set_trace()
|
|
||||||
for _ in range(num_mini_batch):
|
|
||||||
if self.preload_transitions:
|
|
||||||
idxs = np.random.choice(self.preloaded_s.shape[0], size=mini_batch_size)
|
|
||||||
|
|
||||||
frames = []
|
|
||||||
for i in range(self.num_frames):
|
|
||||||
# 数据已在预加载时预处理,直接索引即可
|
|
||||||
s = self.preloaded_frames[i][idxs]
|
|
||||||
frames.append(s)
|
|
||||||
else:
|
|
||||||
NotImplementedError('preload transition')
|
|
||||||
yield torch.stack(frames, dim=1) # [batch, num_frames, 16]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def quaternion_to_euler_array(self, quat):
|
|
||||||
# Ensure quaternion is in the correct format [x, y, z, w]
|
|
||||||
x, y, z, w =quat
|
|
||||||
|
|
||||||
# Roll (x-axis rotation)
|
|
||||||
t0 = +2.0 * (w * x + y * z)
|
|
||||||
t1 = +1.0 - 2.0 * (x * x + y * y)
|
|
||||||
roll_x = np.arctan2(t0, t1)
|
|
||||||
|
|
||||||
# Pitch (y-axis rotation)
|
|
||||||
t2 = +2.0 * (w * y - z * x)
|
|
||||||
t2 = np.clip(t2, -1.0, 1.0)
|
|
||||||
pitch_y = np.arcsin(t2)
|
|
||||||
|
|
||||||
# Yaw (z-axis rotation)
|
|
||||||
t3 = +2.0 * (w * z + x * y)
|
|
||||||
t4 = +1.0 - 2.0 * (y * y + z * z)
|
|
||||||
yaw_z = np.arctan2(t3, t4)
|
|
||||||
|
|
||||||
# Returns roll, pitch, yaw in a NumPy array in radians
|
|
||||||
return np.array([roll_x, pitch_y, yaw_z])
|
|
||||||
|
|
||||||
def euler_to_quaternion(self, root_rot):
|
|
||||||
roll, pitch, yaw = root_rot[0], root_rot[1], root_rot[2]
|
|
||||||
cy = np.cos(yaw * 0.5)
|
|
||||||
sy = np.sin(yaw * 0.5)
|
|
||||||
cp = np.cos(pitch * 0.5)
|
|
||||||
sp = np.sin(pitch * 0.5)
|
|
||||||
cr = np.cos(roll * 0.5)
|
|
||||||
sr = np.sin(roll * 0.5)
|
|
||||||
|
|
||||||
qw = cy * cp * cr + sy * sp * sr
|
|
||||||
qx = cy * cp * sr - sy * sp * cr
|
|
||||||
qy = sy * cp * sr + cy * sp * cr
|
|
||||||
qz = sy * cp * cr - cy * sp * sr
|
|
||||||
|
|
||||||
return np.array([qx, qy, qz, qw])
|
|
||||||
|
|
||||||
@property
|
|
||||||
def observation_dim(self):
|
|
||||||
"""Size of AMP observations."""
|
|
||||||
return self.trajectories[0].shape[1] + 1
|
|
||||||
|
|
||||||
@property
|
|
||||||
def num_motions(self):
|
|
||||||
return len(self.trajectory_names)
|
|
||||||
@staticmethod
|
|
||||||
def get_root_pos(pose):
|
|
||||||
return pose[0:3]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_root_pos_batch(poses):
|
|
||||||
return poses[:, 0:3]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_root_rot(pose):
|
|
||||||
return pose[3:7]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_root_rot_batch(poses):
|
|
||||||
return poses[:, 3:7]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_joint_pose_batch_12dof(poses):
|
|
||||||
return poses[:, 13:25]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_tar_toe_pos_local(pose):
|
|
||||||
return pose[G1_AMPLoader.TAR_TOE_POS_LOCAL_START_IDX:G1_AMPLoader.TAR_TOE_POS_LOCAL_END_IDX]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_tar_toe_pos_local_batch(poses):
|
|
||||||
return poses[:, G1_AMPLoader.TAR_TOE_POS_LOCAL_START_IDX:G1_AMPLoader.TAR_TOE_POS_LOCAL_END_IDX]
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
# coding=utf-8
|
|
||||||
# Copyright 2020 The Google Research Authors.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
"""Utility functions for processing motion clips."""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import inspect
|
|
||||||
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
|
|
||||||
parentdir = os.path.dirname(os.path.dirname(currentdir))
|
|
||||||
os.sys.path.insert(0, parentdir)
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from rsl_rl.utils import pose3d
|
|
||||||
# from pybullet_utils import transformations
|
|
||||||
|
|
||||||
|
|
||||||
def standardize_quaternion(q):
|
|
||||||
"""Returns a quaternion where q.w >= 0 to remove redundancy due to q = -q.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
q: A quaternion to be standardized.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A quaternion with q.w >= 0.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if q[-1] < 0:
|
|
||||||
q = -q
|
|
||||||
return q
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_rotation_angle(theta):
|
|
||||||
"""Returns a rotation angle normalized between [-pi, pi].
|
|
||||||
|
|
||||||
Args:
|
|
||||||
theta: angle of rotation (radians).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
An angle of rotation normalized between [-pi, pi].
|
|
||||||
|
|
||||||
"""
|
|
||||||
norm_theta = theta
|
|
||||||
if np.abs(norm_theta) > np.pi:
|
|
||||||
norm_theta = np.fmod(norm_theta, 2 * np.pi)
|
|
||||||
if norm_theta >= 0:
|
|
||||||
norm_theta += -2 * np.pi
|
|
||||||
else:
|
|
||||||
norm_theta += 2 * np.pi
|
|
||||||
|
|
||||||
return norm_theta
|
|
||||||
|
|
||||||
|
|
||||||
def calc_heading(q):
|
|
||||||
"""Returns the heading of a rotation q, specified as a quaternion.
|
|
||||||
|
|
||||||
The heading represents the rotational component of q along the vertical
|
|
||||||
axis (z axis).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
q: A quaternion that the heading is to be computed from.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
An angle representing the rotation about the z axis.
|
|
||||||
|
|
||||||
"""
|
|
||||||
ref_dir = np.array([1, 0, 0])
|
|
||||||
rot_dir = pose3d.QuaternionRotatePoint(ref_dir, q)
|
|
||||||
heading = np.arctan2(rot_dir[1], rot_dir[0])
|
|
||||||
return heading
|
|
||||||
|
|
||||||
|
|
||||||
# def calc_heading_rot(q):
|
|
||||||
# """Return a quaternion representing the heading rotation of q along the vertical axis (z axis).
|
|
||||||
|
|
||||||
# Args:
|
|
||||||
# q: A quaternion that the heading is to be computed from.
|
|
||||||
|
|
||||||
# Returns:
|
|
||||||
# A quaternion representing the rotation about the z axis.
|
|
||||||
|
|
||||||
# """
|
|
||||||
# heading = calc_heading(q)
|
|
||||||
# q_heading = transformations.quaternion_about_axis(heading, [0, 0, 1])
|
|
||||||
# return q_heading
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
from dataclasses import asdict
|
|
||||||
from torch.utils.tensorboard import SummaryWriter
|
|
||||||
|
|
||||||
try:
|
|
||||||
import neptune
|
|
||||||
except ModuleNotFoundError:
|
|
||||||
raise ModuleNotFoundError("neptune-client is required to log to Neptune.")
|
|
||||||
|
|
||||||
|
|
||||||
class NeptuneLogger:
|
|
||||||
def __init__(self, project, token):
|
|
||||||
self.run = neptune.init_run(project=project, api_token=token)
|
|
||||||
|
|
||||||
def store_config(self, env_cfg, runner_cfg, alg_cfg, policy_cfg):
|
|
||||||
self.run["runner_cfg"] = runner_cfg
|
|
||||||
self.run["policy_cfg"] = policy_cfg
|
|
||||||
self.run["alg_cfg"] = alg_cfg
|
|
||||||
self.run["env_cfg"] = asdict(env_cfg)
|
|
||||||
|
|
||||||
|
|
||||||
class NeptuneSummaryWriter(SummaryWriter):
|
|
||||||
"""Summary writer for Neptune."""
|
|
||||||
|
|
||||||
def __init__(self, log_dir: str, flush_secs: int, cfg):
|
|
||||||
super().__init__(log_dir, flush_secs)
|
|
||||||
|
|
||||||
try:
|
|
||||||
project = cfg["neptune_project"]
|
|
||||||
except KeyError:
|
|
||||||
raise KeyError("Please specify neptune_project in the runner config, e.g. legged_gym.")
|
|
||||||
|
|
||||||
try:
|
|
||||||
token = os.environ["NEPTUNE_API_TOKEN"]
|
|
||||||
except KeyError:
|
|
||||||
raise KeyError(
|
|
||||||
"Neptune api token not found. Please run or add to ~/.bashrc: export NEPTUNE_API_TOKEN=YOUR_API_TOKEN"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
entity = os.environ["NEPTUNE_USERNAME"]
|
|
||||||
except KeyError:
|
|
||||||
raise KeyError(
|
|
||||||
"Neptune username not found. Please run or add to ~/.bashrc: export NEPTUNE_USERNAME=YOUR_USERNAME"
|
|
||||||
)
|
|
||||||
|
|
||||||
neptune_project = entity + "/" + project
|
|
||||||
|
|
||||||
self.neptune_logger = NeptuneLogger(neptune_project, token)
|
|
||||||
|
|
||||||
self.name_map = {
|
|
||||||
"Train/mean_reward/time": "Train/mean_reward_time",
|
|
||||||
"Train/mean_episode_length/time": "Train/mean_episode_length_time",
|
|
||||||
}
|
|
||||||
|
|
||||||
run_name = os.path.split(log_dir)[-1]
|
|
||||||
|
|
||||||
self.neptune_logger.run["log_dir"].log(run_name)
|
|
||||||
|
|
||||||
def _map_path(self, path):
|
|
||||||
if path in self.name_map:
|
|
||||||
return self.name_map[path]
|
|
||||||
else:
|
|
||||||
return path
|
|
||||||
|
|
||||||
def add_scalar(self, tag, scalar_value, global_step=None, walltime=None, new_style=False):
|
|
||||||
super().add_scalar(
|
|
||||||
tag,
|
|
||||||
scalar_value,
|
|
||||||
global_step=global_step,
|
|
||||||
walltime=walltime,
|
|
||||||
new_style=new_style,
|
|
||||||
)
|
|
||||||
self.neptune_logger.run[self._map_path(tag)].log(scalar_value, step=global_step)
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self.neptune_logger.run.stop()
|
|
||||||
|
|
||||||
def log_config(self, env_cfg, runner_cfg, alg_cfg, policy_cfg):
|
|
||||||
self.neptune_logger.store_config(env_cfg, runner_cfg, alg_cfg, policy_cfg)
|
|
||||||
|
|
||||||
def save_model(self, model_path, iter):
|
|
||||||
self.neptune_logger.run["model/saved_model_" + str(iter)].upload(model_path)
|
|
||||||
|
|
||||||
def save_file(self, path, iter=None):
|
|
||||||
name = path.rsplit("/", 1)[-1].split(".")[0]
|
|
||||||
self.neptune_logger.run["git_diff/" + name].upload(path)
|
|
||||||
|
|
@ -1,283 +0,0 @@
|
||||||
# coding=utf-8
|
|
||||||
# Copyright 2020 The Google Research Authors.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
"""Utilities for 3D pose conversion."""
|
|
||||||
import math
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
# from pybullet_utils import transformations
|
|
||||||
|
|
||||||
VECTOR3_0 = np.zeros(3, dtype=np.float64)
|
|
||||||
VECTOR3_1 = np.ones(3, dtype=np.float64)
|
|
||||||
VECTOR3_X = np.array([1, 0, 0], dtype=np.float64)
|
|
||||||
VECTOR3_Y = np.array([0, 1, 0], dtype=np.float64)
|
|
||||||
VECTOR3_Z = np.array([0, 0, 1], dtype=np.float64)
|
|
||||||
|
|
||||||
# QUATERNION_IDENTITY is the multiplicative identity 1.0 + 0i + 0j + 0k.
|
|
||||||
# When interpreted as a rotation, it is the identity rotation.
|
|
||||||
QUATERNION_IDENTITY = np.array([0.0, 0.0, 0.0, 1.0], dtype=np.float64)
|
|
||||||
|
|
||||||
|
|
||||||
def Vector3RandomNormal(sigma, mu=VECTOR3_0):
|
|
||||||
"""Returns a random 3D vector from a normal distribution.
|
|
||||||
|
|
||||||
Each component is selected independently from a normal distribution.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
sigma: Scale (or stddev) of distribution for all variables.
|
|
||||||
mu: Mean of distribution for each variable.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A 3D vector in a numpy array.
|
|
||||||
"""
|
|
||||||
|
|
||||||
random_v3 = np.random.normal(scale=sigma, size=3) + mu
|
|
||||||
return random_v3
|
|
||||||
|
|
||||||
|
|
||||||
def Vector3RandomUniform(low=VECTOR3_0, high=VECTOR3_1):
|
|
||||||
"""Returns a 3D vector selected uniformly from the input box.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
low: The min-value corner of the box.
|
|
||||||
high: The max-value corner of the box.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A 3D vector in a numpy array.
|
|
||||||
"""
|
|
||||||
|
|
||||||
random_x = np.random.uniform(low=low[0], high=high[0])
|
|
||||||
random_y = np.random.uniform(low=low[1], high=high[1])
|
|
||||||
random_z = np.random.uniform(low=low[2], high=high[2])
|
|
||||||
return np.array([random_x, random_y, random_z])
|
|
||||||
|
|
||||||
|
|
||||||
def Vector3RandomUnit():
|
|
||||||
"""Returns a random 3D vector with unit length.
|
|
||||||
|
|
||||||
Generates a 3D vector selected uniformly from the unit sphere.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A normalized 3D vector in a numpy array.
|
|
||||||
"""
|
|
||||||
longitude = np.random.uniform(low=-math.pi, high=math.pi)
|
|
||||||
sin_latitude = np.random.uniform(low=-1.0, high=1.0)
|
|
||||||
cos_latitude = math.sqrt(1.0 - sin_latitude * sin_latitude)
|
|
||||||
x = math.cos(longitude) * cos_latitude
|
|
||||||
y = math.sin(longitude) * cos_latitude
|
|
||||||
z = sin_latitude
|
|
||||||
return np.array([x, y, z], dtype=np.float64)
|
|
||||||
|
|
||||||
|
|
||||||
def QuaternionNormalize(q):
|
|
||||||
"""Normalizes the quaternion to length 1.
|
|
||||||
|
|
||||||
Divides the quaternion by its magnitude. If the magnitude is too
|
|
||||||
small, returns the quaternion identity value (1.0).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
q: A quaternion to be normalized.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If input quaternion has length near zero.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A quaternion with magnitude 1 in a numpy array [x, y, z, w].
|
|
||||||
|
|
||||||
"""
|
|
||||||
q_norm = np.linalg.norm(q)
|
|
||||||
if np.isclose(q_norm, 0.0):
|
|
||||||
raise ValueError(
|
|
||||||
'Quaternion may not be zero in QuaternionNormalize: |q| = %f, q = %s' %
|
|
||||||
(q_norm, q))
|
|
||||||
return q / q_norm
|
|
||||||
|
|
||||||
|
|
||||||
def QuaternionFromAxisAngle(axis, angle):
|
|
||||||
"""Returns a quaternion that generates the given axis-angle rotation.
|
|
||||||
|
|
||||||
Returns the quaternion: sin(angle/2) * axis + cos(angle/2).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
axis: Axis of rotation, a 3D vector in a numpy array.
|
|
||||||
angle: The angle of rotation (radians).
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If input axis is not a normalizable 3D vector.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A unit quaternion in a numpy array.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if len(axis) != 3:
|
|
||||||
raise ValueError('Axis vector should have three components: %s' % axis)
|
|
||||||
axis_norm = np.linalg.norm(axis)
|
|
||||||
if np.isclose(axis_norm, 0.0):
|
|
||||||
raise ValueError('Axis vector may not have zero length: |v| = %f, v = %s' %
|
|
||||||
(axis_norm, axis))
|
|
||||||
half_angle = angle * 0.5
|
|
||||||
q = np.zeros(4, dtype=np.float64)
|
|
||||||
q[0:3] = axis
|
|
||||||
q[0:3] *= math.sin(half_angle) / axis_norm
|
|
||||||
q[3] = math.cos(half_angle)
|
|
||||||
return q
|
|
||||||
|
|
||||||
|
|
||||||
def QuaternionToAxisAngle(quat, default_axis=VECTOR3_Z, direction_axis=None):
|
|
||||||
"""Calculates axis and angle of rotation performed by a quaternion.
|
|
||||||
|
|
||||||
Calculates the axis and angle of the rotation performed by the quaternion.
|
|
||||||
The quaternion should have four values and be normalized.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
quat: Unit quaternion in a numpy array.
|
|
||||||
default_axis: 3D vector axis used if the rotation is near to zero. Without
|
|
||||||
this default, small rotations would result in an exception. It is
|
|
||||||
reasonable to use a default axis for tiny rotations, because zero angle
|
|
||||||
rotations about any axis are equivalent.
|
|
||||||
direction_axis: Used to disambiguate rotation directions. If the
|
|
||||||
direction_axis is specified, the axis of the rotation will be chosen such
|
|
||||||
that its inner product with the direction_axis is non-negative.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If quat is not a normalized quaternion.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
axis: Axis of rotation.
|
|
||||||
angle: Angle in radians.
|
|
||||||
"""
|
|
||||||
if len(quat) != 4:
|
|
||||||
raise ValueError(
|
|
||||||
'Quaternion should have four components [x, y, z, w]: %s' % quat)
|
|
||||||
if not np.isclose(1.0, np.linalg.norm(quat)):
|
|
||||||
raise ValueError('Quaternion should have unit length: |q| = %f, q = %s' %
|
|
||||||
(np.linalg.norm(quat), quat))
|
|
||||||
axis = quat[:3].copy()
|
|
||||||
axis_norm = np.linalg.norm(axis)
|
|
||||||
min_axis_norm = 1e-8
|
|
||||||
if axis_norm < min_axis_norm:
|
|
||||||
axis = default_axis
|
|
||||||
if len(default_axis) != 3:
|
|
||||||
raise ValueError('Axis vector should have three components: %s' % axis)
|
|
||||||
if not np.isclose(np.linalg.norm(axis), 1.0):
|
|
||||||
raise ValueError('Axis vector should have unit length: |v| = %f, v = %s' %
|
|
||||||
(np.linalg.norm(axis), axis))
|
|
||||||
else:
|
|
||||||
axis /= axis_norm
|
|
||||||
sin_half_angle = axis_norm
|
|
||||||
if direction_axis is not None and np.inner(axis, direction_axis) < 0:
|
|
||||||
sin_half_angle = -sin_half_angle
|
|
||||||
axis = -axis
|
|
||||||
cos_half_angle = quat[3]
|
|
||||||
half_angle = math.atan2(sin_half_angle, cos_half_angle)
|
|
||||||
angle = half_angle * 2
|
|
||||||
return axis, angle
|
|
||||||
|
|
||||||
|
|
||||||
def QuaternionRandomRotation(max_angle=math.pi):
|
|
||||||
"""Creates a random small rotation around a random axis.
|
|
||||||
|
|
||||||
Generates a small rotation with the axis vector selected uniformly
|
|
||||||
from the unit sphere and an angle selected from a uniform
|
|
||||||
distribution over [0, max_angle].
|
|
||||||
|
|
||||||
If the max_angle is not specified, the rotation should be selected
|
|
||||||
uniformly over all possible rotation angles.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
max_angle: The maximum angle of rotation (radians).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A unit quaternion in a numpy array.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
angle = np.random.uniform(low=0, high=max_angle)
|
|
||||||
axis = Vector3RandomUnit()
|
|
||||||
return QuaternionFromAxisAngle(axis, angle)
|
|
||||||
|
|
||||||
|
|
||||||
# def QuaternionRotatePoint(point, quat):
|
|
||||||
# """Performs a rotation by quaternion.
|
|
||||||
|
|
||||||
# Rotate the point by the quaternion using quaternion multiplication,
|
|
||||||
# (q * p * q^-1), without constructing the rotation matrix.
|
|
||||||
|
|
||||||
# Args:
|
|
||||||
# point: The point to be rotated.
|
|
||||||
# quat: The rotation represented as a quaternion [x, y, z, w].
|
|
||||||
|
|
||||||
# Returns:
|
|
||||||
# A 3D vector in a numpy array.
|
|
||||||
# """
|
|
||||||
|
|
||||||
# q_point = np.array([point[0], point[1], point[2], 0.0])
|
|
||||||
# quat_inverse = transformations.quaternion_inverse(quat)
|
|
||||||
# q_point_rotated = transformations.quaternion_multiply(
|
|
||||||
# transformations.quaternion_multiply(quat, q_point), quat_inverse)
|
|
||||||
# return q_point_rotated[:3]
|
|
||||||
|
|
||||||
|
|
||||||
def IsRotationMatrix(m):
|
|
||||||
"""Returns true if the 3x3 submatrix represents a rotation.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
m: A transformation matrix.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If input is not a matrix of size at least 3x3.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the 3x3 submatrix is a rotation (orthogonal).
|
|
||||||
"""
|
|
||||||
if len(m.shape) != 2 or m.shape[0] < 3 or m.shape[1] < 3:
|
|
||||||
raise ValueError('Matrix should be 3x3 or 4x4: %s\n %s' % (m.shape, m))
|
|
||||||
rot = m[:3, :3]
|
|
||||||
eye = np.matmul(rot, np.transpose(rot))
|
|
||||||
return np.isclose(eye, np.identity(3), atol=1e-4).all()
|
|
||||||
|
|
||||||
# def ZAxisAlignedRobotPoseTool(robot_pose_tool):
|
|
||||||
# """Returns the current gripper pose rotated for alignment with the z-axis.
|
|
||||||
|
|
||||||
# Args:
|
|
||||||
# robot_pose_tool: a pose3d.Pose3d() instance.
|
|
||||||
|
|
||||||
# Returns:
|
|
||||||
# An instance of pose.Transform representing the current gripper pose
|
|
||||||
# rotated for alignment with the z-axis.
|
|
||||||
# """
|
|
||||||
# # Align the current pose to the z-axis.
|
|
||||||
# robot_pose_tool.quaternion = transformations.quaternion_multiply(
|
|
||||||
# RotationBetween(
|
|
||||||
# robot_pose_tool.matrix4x4[0:3, 0:3].dot(np.array([0, 0, 1])),
|
|
||||||
# np.array([0.0, 0.0, -1.0])), robot_pose_tool.quaternion)
|
|
||||||
# return robot_pose_tool
|
|
||||||
|
|
||||||
# def RotationBetween(a_translation_b, a_translation_c):
|
|
||||||
# """Computes the rotation from one vector to another.
|
|
||||||
|
|
||||||
# The computed rotation has the property that:
|
|
||||||
|
|
||||||
# a_translation_c = a_rotation_b_to_c * a_translation_b
|
|
||||||
|
|
||||||
# Args:
|
|
||||||
# a_translation_b: vec3, vector to rotate from
|
|
||||||
# a_translation_c: vec3, vector to rotate to
|
|
||||||
|
|
||||||
# Returns:
|
|
||||||
# a_rotation_b_to_c: new Orientation
|
|
||||||
# """
|
|
||||||
# rotation = rotation3.Rotation3.rotation_between(
|
|
||||||
# a_translation_b, a_translation_c, err_msg='RotationBetween')
|
|
||||||
# return rotation.quaternion.xyzw
|
|
||||||
|
|
@ -1,360 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import git
|
|
||||||
import importlib
|
|
||||||
import os
|
|
||||||
import pathlib
|
|
||||||
import torch
|
|
||||||
import warnings
|
|
||||||
from tensordict import TensorDict
|
|
||||||
from typing import Callable
|
|
||||||
import numpy as np
|
|
||||||
class RunningMeanStd:
|
|
||||||
def __init__(self, epsilon: float = 1e-4, shape: Tuple[int, ...] = ()):
|
|
||||||
"""
|
|
||||||
Calculates the running mean and std of a data stream
|
|
||||||
https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Parallel_algorithm
|
|
||||||
:param epsilon: helps with arithmetic issues
|
|
||||||
:param shape: the shape of the data stream's output
|
|
||||||
"""
|
|
||||||
self.mean = np.zeros(shape, np.float64)
|
|
||||||
self.var = np.ones(shape, np.float64)
|
|
||||||
self.count = epsilon
|
|
||||||
|
|
||||||
def update(self, arr: np.ndarray) -> None:
|
|
||||||
batch_mean = np.mean(arr, axis=0)
|
|
||||||
batch_var = np.var(arr, axis=0)
|
|
||||||
batch_count = arr.shape[0]
|
|
||||||
self.update_from_moments(batch_mean, batch_var, batch_count)
|
|
||||||
|
|
||||||
def update_from_moments(self, batch_mean: np.ndarray, batch_var: np.ndarray, batch_count: int) -> None:
|
|
||||||
delta = batch_mean - self.mean
|
|
||||||
tot_count = self.count + batch_count
|
|
||||||
|
|
||||||
new_mean = self.mean + delta * batch_count / tot_count
|
|
||||||
m_a = self.var * self.count
|
|
||||||
m_b = batch_var * batch_count
|
|
||||||
m_2 = m_a + m_b + np.square(delta) * self.count * batch_count / (self.count + batch_count)
|
|
||||||
new_var = m_2 / (self.count + batch_count)
|
|
||||||
|
|
||||||
new_count = batch_count + self.count
|
|
||||||
|
|
||||||
self.mean = new_mean
|
|
||||||
self.var = new_var
|
|
||||||
self.count = new_count
|
|
||||||
|
|
||||||
|
|
||||||
class Normalizer(RunningMeanStd):
|
|
||||||
def __init__(self, input_dim, epsilon=1e-4, clip_obs=10.0):
|
|
||||||
super().__init__(shape=input_dim)
|
|
||||||
self.epsilon = epsilon
|
|
||||||
self.clip_obs = clip_obs
|
|
||||||
|
|
||||||
def normalize(self, input):
|
|
||||||
return np.clip((input - self.mean) / np.sqrt(self.var + self.epsilon), -self.clip_obs, self.clip_obs)
|
|
||||||
|
|
||||||
def normalize_torch(self, input, device):
|
|
||||||
mean_torch = torch.tensor(self.mean, device=device, dtype=torch.float32)
|
|
||||||
std_torch = torch.sqrt(torch.tensor(self.var + self.epsilon, device=device, dtype=torch.float32))
|
|
||||||
return torch.clamp((input - mean_torch) / std_torch, -self.clip_obs, self.clip_obs)
|
|
||||||
|
|
||||||
def update_normalizer(self, rollouts, expert_loader):
|
|
||||||
policy_data_generator = rollouts.feed_forward_generator_amp(None, mini_batch_size=expert_loader.batch_size)
|
|
||||||
expert_data_generator = expert_loader.dataset.feed_forward_generator_amp(expert_loader.batch_size)
|
|
||||||
|
|
||||||
for expert_batch, policy_batch in zip(expert_data_generator, policy_data_generator):
|
|
||||||
self.update(torch.vstack(tuple(policy_batch) + tuple(expert_batch)).cpu().numpy())
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_nn_activation(act_name: str) -> torch.nn.Module:
|
|
||||||
"""Resolves the activation function from the name.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
act_name: The name of the activation function.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The activation function.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If the activation function is not found.
|
|
||||||
"""
|
|
||||||
act_dict = {
|
|
||||||
"elu": torch.nn.ELU(),
|
|
||||||
"selu": torch.nn.SELU(),
|
|
||||||
"relu": torch.nn.ReLU(),
|
|
||||||
"crelu": torch.nn.CELU(),
|
|
||||||
"lrelu": torch.nn.LeakyReLU(),
|
|
||||||
"tanh": torch.nn.Tanh(),
|
|
||||||
"sigmoid": torch.nn.Sigmoid(),
|
|
||||||
"softplus": torch.nn.Softplus(),
|
|
||||||
"gelu": torch.nn.GELU(),
|
|
||||||
"swish": torch.nn.SiLU(),
|
|
||||||
"mish": torch.nn.Mish(),
|
|
||||||
"identity": torch.nn.Identity(),
|
|
||||||
}
|
|
||||||
|
|
||||||
act_name = act_name.lower()
|
|
||||||
if act_name in act_dict:
|
|
||||||
return act_dict[act_name]
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Invalid activation function '{act_name}'. Valid activations are: {list(act_dict.keys())}")
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_optimizer(optimizer_name: str) -> torch.optim.Optimizer:
|
|
||||||
"""Resolves the optimizer from the name.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
optimizer_name: The name of the optimizer.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The optimizer.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If the optimizer is not found.
|
|
||||||
"""
|
|
||||||
optimizer_dict = {
|
|
||||||
"adam": torch.optim.Adam,
|
|
||||||
"adamw": torch.optim.AdamW,
|
|
||||||
"sgd": torch.optim.SGD,
|
|
||||||
"rmsprop": torch.optim.RMSprop,
|
|
||||||
}
|
|
||||||
|
|
||||||
optimizer_name = optimizer_name.lower()
|
|
||||||
if optimizer_name in optimizer_dict:
|
|
||||||
return optimizer_dict[optimizer_name]
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Invalid optimizer '{optimizer_name}'. Valid optimizers are: {list(optimizer_dict.keys())}")
|
|
||||||
|
|
||||||
|
|
||||||
def split_and_pad_trajectories(
|
|
||||||
tensor: torch.Tensor | TensorDict, dones: torch.Tensor
|
|
||||||
) -> tuple[torch.Tensor | TensorDict, torch.Tensor]:
|
|
||||||
"""Splits trajectories at done indices. Then concatenates them and pads with zeros up to the length of the longest
|
|
||||||
trajectory. Returns masks corresponding to valid parts of the trajectories.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
Input: [[a1, a2, a3, a4 | a5, a6],
|
|
||||||
[b1, b2 | b3, b4, b5 | b6]]
|
|
||||||
|
|
||||||
Output:[[a1, a2, a3, a4], | [[True, True, True, True],
|
|
||||||
[a5, a6, 0, 0], | [True, True, False, False],
|
|
||||||
[b1, b2, 0, 0], | [True, True, False, False],
|
|
||||||
[b3, b4, b5, 0], | [True, True, True, False],
|
|
||||||
[b6, 0, 0, 0]] | [True, False, False, False]]
|
|
||||||
|
|
||||||
Assumes that the input has the following order of dimensions: [time, number of envs, additional dimensions]
|
|
||||||
"""
|
|
||||||
|
|
||||||
dones = dones.clone()
|
|
||||||
dones[-1] = 1
|
|
||||||
# Permute the buffers to have order (num_envs, num_transitions_per_env, ...), for correct reshaping
|
|
||||||
flat_dones = dones.transpose(1, 0).reshape(-1, 1)
|
|
||||||
# Get length of trajectory by counting the number of successive not done elements
|
|
||||||
done_indices = torch.cat((flat_dones.new_tensor([-1], dtype=torch.int64), flat_dones.nonzero()[:, 0]))
|
|
||||||
trajectory_lengths = done_indices[1:] - done_indices[:-1]
|
|
||||||
trajectory_lengths_list = trajectory_lengths.tolist()
|
|
||||||
# Extract the individual trajectories
|
|
||||||
if isinstance(tensor, TensorDict):
|
|
||||||
padded_trajectories = {}
|
|
||||||
for k, v in tensor.items():
|
|
||||||
# split the tensor into trajectories
|
|
||||||
trajectories = torch.split(v.transpose(1, 0).flatten(0, 1), trajectory_lengths_list)
|
|
||||||
# add at least one full length trajectory
|
|
||||||
trajectories = trajectories + (torch.zeros(v.shape[0], *v.shape[2:], device=v.device),)
|
|
||||||
# pad the trajectories to the length of the longest trajectory
|
|
||||||
padded_trajectories[k] = torch.nn.utils.rnn.pad_sequence(trajectories)
|
|
||||||
# remove the added tensor
|
|
||||||
padded_trajectories[k] = padded_trajectories[k][:, :-1]
|
|
||||||
padded_trajectories = TensorDict(
|
|
||||||
padded_trajectories, batch_size=[tensor.batch_size[0], len(trajectory_lengths_list)]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# split the tensor into trajectories
|
|
||||||
trajectories = torch.split(tensor.transpose(1, 0).flatten(0, 1), trajectory_lengths_list)
|
|
||||||
# add at least one full length trajectory
|
|
||||||
trajectories = trajectories + (torch.zeros(tensor.shape[0], *tensor.shape[2:], device=tensor.device),)
|
|
||||||
# pad the trajectories to the length of the longest trajectory
|
|
||||||
padded_trajectories = torch.nn.utils.rnn.pad_sequence(trajectories)
|
|
||||||
# remove the added tensor
|
|
||||||
padded_trajectories = padded_trajectories[:, :-1]
|
|
||||||
# create masks for the valid parts of the trajectories
|
|
||||||
trajectory_masks = trajectory_lengths > torch.arange(0, tensor.shape[0], device=tensor.device).unsqueeze(1)
|
|
||||||
return padded_trajectories, trajectory_masks
|
|
||||||
|
|
||||||
|
|
||||||
def unpad_trajectories(trajectories, masks):
|
|
||||||
"""Does the inverse operation of split_and_pad_trajectories()"""
|
|
||||||
# Need to transpose before and after the masking to have proper reshaping
|
|
||||||
return (
|
|
||||||
trajectories.transpose(1, 0)[masks.transpose(1, 0)]
|
|
||||||
.view(-1, trajectories.shape[0], trajectories.shape[-1])
|
|
||||||
.transpose(1, 0)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def store_code_state(logdir, repositories) -> list:
|
|
||||||
git_log_dir = os.path.join(logdir, "git")
|
|
||||||
os.makedirs(git_log_dir, exist_ok=True)
|
|
||||||
file_paths = []
|
|
||||||
for repository_file_path in repositories:
|
|
||||||
try:
|
|
||||||
repo = git.Repo(repository_file_path, search_parent_directories=True)
|
|
||||||
t = repo.head.commit.tree
|
|
||||||
except Exception:
|
|
||||||
print(f"Could not find git repository in {repository_file_path}. Skipping.")
|
|
||||||
# skip if not a git repository
|
|
||||||
continue
|
|
||||||
# get the name of the repository
|
|
||||||
repo_name = pathlib.Path(repo.working_dir).name
|
|
||||||
diff_file_name = os.path.join(git_log_dir, f"{repo_name}.diff")
|
|
||||||
# check if the diff file already exists
|
|
||||||
if os.path.isfile(diff_file_name):
|
|
||||||
continue
|
|
||||||
# write the diff file
|
|
||||||
print(f"Storing git diff for '{repo_name}' in: {diff_file_name}")
|
|
||||||
with open(diff_file_name, "x", encoding="utf-8") as f:
|
|
||||||
content = f"--- git status ---\n{repo.git.status()} \n\n\n--- git diff ---\n{repo.git.diff(t)}"
|
|
||||||
f.write(content)
|
|
||||||
# add the file path to the list of files to be uploaded
|
|
||||||
file_paths.append(diff_file_name)
|
|
||||||
return file_paths
|
|
||||||
|
|
||||||
|
|
||||||
def string_to_callable(name: str) -> Callable:
|
|
||||||
"""Resolves the module and function names to return the function.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: The function name. The format should be 'module:attribute_name'.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: When the resolved attribute is not a function.
|
|
||||||
ValueError: When unable to resolve the attribute.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The function loaded from the module.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
mod_name, attr_name = name.split(":")
|
|
||||||
mod = importlib.import_module(mod_name)
|
|
||||||
callable_object = getattr(mod, attr_name)
|
|
||||||
# check if attribute is callable
|
|
||||||
if callable(callable_object):
|
|
||||||
return callable_object
|
|
||||||
else:
|
|
||||||
raise ValueError(f"The imported object is not callable: '{name}'")
|
|
||||||
except AttributeError as e:
|
|
||||||
msg = (
|
|
||||||
"We could not interpret the entry as a callable object. The format of input should be"
|
|
||||||
f" 'module:attribute_name'\nWhile processing input '{name}', received the error:\n {e}."
|
|
||||||
)
|
|
||||||
raise ValueError(msg)
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_obs_groups(
|
|
||||||
obs: TensorDict, obs_groups: dict[str, list[str]], default_sets: list[str]
|
|
||||||
) -> dict[str, list[str]]:
|
|
||||||
"""Validates the observation configuration and defaults missing observation sets.
|
|
||||||
|
|
||||||
The input is an observation dictionary `obs` containing observation groups and a configuration dictionary
|
|
||||||
`obs_groups` where the keys are the observation sets and the values are lists of observation groups.
|
|
||||||
|
|
||||||
The configuration dictionary could for example look like:
|
|
||||||
{
|
|
||||||
"policy": ["group_1", "group_2"],
|
|
||||||
"critic": ["group_1", "group_3"]
|
|
||||||
}
|
|
||||||
|
|
||||||
This means that the 'policy' observation set will contain the observations "group_1" and "group_2" and the
|
|
||||||
'critic' observation set will contain the observations "group_1" and "group_3". This function will check that all
|
|
||||||
the observations in the 'policy' and 'critic' observation sets are present in the observation dictionary from the
|
|
||||||
environment.
|
|
||||||
|
|
||||||
Additionally, if one of the `default_sets`, e.g. "critic", is not present in the configuration dictionary,
|
|
||||||
this function will:
|
|
||||||
|
|
||||||
1. Check if a group with the same name exists in the observations and assign this group to the observation set.
|
|
||||||
2. If 1. fails, it will assign the observations from the 'policy' observation set to the default observation set.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
obs: Observations from the environment in the form of a dictionary.
|
|
||||||
obs_groups: Observation sets configuration.
|
|
||||||
default_sets: Reserved observation set names used by the algorithm (besides 'policy').
|
|
||||||
If not provided in 'obs_groups', a default behavior gets triggered.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The resolved observation groups.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If any observation set is an empty list.
|
|
||||||
ValueError: If any observation set contains an observation term that is not present in the observations.
|
|
||||||
"""
|
|
||||||
# check if policy observation set exists
|
|
||||||
if "policy" not in obs_groups.keys():
|
|
||||||
if "policy" in obs:
|
|
||||||
obs_groups["policy"] = ["policy"]
|
|
||||||
warnings.warn(
|
|
||||||
"The observation configuration dictionary 'obs_groups' must contain the 'policy' key."
|
|
||||||
" As an observation group with the name 'policy' was found, this is assumed to be the observation set."
|
|
||||||
" Consider adding the 'policy' key to the 'obs_groups' dictionary for clarity."
|
|
||||||
" This behavior will be removed in a future version."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
"The observation configuration dictionary 'obs_groups' must contain the 'policy' key."
|
|
||||||
f" Found keys: {list(obs_groups.keys())}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# check all observation sets for valid observation groups
|
|
||||||
for set_name, groups in obs_groups.items():
|
|
||||||
# check if the list is empty
|
|
||||||
if len(groups) == 0:
|
|
||||||
msg = f"The '{set_name}' key in the 'obs_groups' dictionary can not be an empty list."
|
|
||||||
if set_name in default_sets:
|
|
||||||
if set_name not in obs:
|
|
||||||
msg += " Consider removing the key to default to the observations used for the 'policy' set."
|
|
||||||
else:
|
|
||||||
msg += (
|
|
||||||
f" Consider removing the key to default to the observation '{set_name}' from the environment."
|
|
||||||
)
|
|
||||||
raise ValueError(msg)
|
|
||||||
# check groups exist inside the observations from the environment
|
|
||||||
for group in groups:
|
|
||||||
if group not in obs:
|
|
||||||
raise ValueError(
|
|
||||||
f"Observation '{group}' in observation set '{set_name}' not found in the observations from the"
|
|
||||||
f" environment. Available observations from the environment: {list(obs.keys())}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# fill missing observation sets
|
|
||||||
for default_set_name in default_sets:
|
|
||||||
if default_set_name not in obs_groups.keys():
|
|
||||||
if default_set_name in obs:
|
|
||||||
obs_groups[default_set_name] = [default_set_name]
|
|
||||||
warnings.warn(
|
|
||||||
f"The observation configuration dictionary 'obs_groups' must contain the '{default_set_name}' key."
|
|
||||||
f" As an observation group with the name '{default_set_name}' was found, this is assumed to be the"
|
|
||||||
f" observation set. Consider adding the '{default_set_name}' key to the 'obs_groups' dictionary for"
|
|
||||||
" clarity. This behavior will be removed in a future version."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
obs_groups[default_set_name] = obs_groups["policy"].copy()
|
|
||||||
warnings.warn(
|
|
||||||
f"The observation configuration dictionary 'obs_groups' must contain the '{default_set_name}' key."
|
|
||||||
f" As the configuration for '{default_set_name}' is missing, the observations from the 'policy' set"
|
|
||||||
f" are used. Consider adding the '{default_set_name}' key to the 'obs_groups' dictionary for"
|
|
||||||
" clarity. This behavior will be removed in a future version."
|
|
||||||
)
|
|
||||||
|
|
||||||
# print the final parsed observation sets
|
|
||||||
print("-" * 80)
|
|
||||||
print("Resolved observation sets: ")
|
|
||||||
for set_name, groups in obs_groups.items():
|
|
||||||
print("\t", set_name, ": ", groups)
|
|
||||||
print("-" * 80)
|
|
||||||
|
|
||||||
return obs_groups
|
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
from dataclasses import asdict
|
|
||||||
from torch.utils.tensorboard import SummaryWriter
|
|
||||||
|
|
||||||
try:
|
|
||||||
import wandb
|
|
||||||
except ModuleNotFoundError:
|
|
||||||
raise ModuleNotFoundError("Wandb is required to log to Weights and Biases.")
|
|
||||||
|
|
||||||
|
|
||||||
class WandbSummaryWriter(SummaryWriter):
|
|
||||||
"""Summary writer for Weights and Biases."""
|
|
||||||
|
|
||||||
def __init__(self, log_dir: str, flush_secs: int, cfg):
|
|
||||||
super().__init__(log_dir, flush_secs)
|
|
||||||
|
|
||||||
# Get the run name
|
|
||||||
run_name = os.path.split(log_dir)[-1]
|
|
||||||
|
|
||||||
try:
|
|
||||||
project = cfg["wandb_project"]
|
|
||||||
except KeyError:
|
|
||||||
raise KeyError("Please specify wandb_project in the runner config, e.g. legged_gym.")
|
|
||||||
|
|
||||||
try:
|
|
||||||
entity = os.environ["WANDB_USERNAME"]
|
|
||||||
except KeyError:
|
|
||||||
entity = None
|
|
||||||
|
|
||||||
# Initialize wandb
|
|
||||||
wandb.init(project=project, entity=entity, name=run_name)
|
|
||||||
|
|
||||||
# Add log directory to wandb
|
|
||||||
wandb.config.update({"log_dir": log_dir})
|
|
||||||
|
|
||||||
self.name_map = {
|
|
||||||
"Train/mean_reward/time": "Train/mean_reward_time",
|
|
||||||
"Train/mean_episode_length/time": "Train/mean_episode_length_time",
|
|
||||||
}
|
|
||||||
|
|
||||||
def store_config(self, env_cfg, runner_cfg, alg_cfg, policy_cfg):
|
|
||||||
wandb.config.update({"runner_cfg": runner_cfg})
|
|
||||||
wandb.config.update({"policy_cfg": policy_cfg})
|
|
||||||
wandb.config.update({"alg_cfg": alg_cfg})
|
|
||||||
try:
|
|
||||||
wandb.config.update({"env_cfg": env_cfg.to_dict()})
|
|
||||||
except Exception:
|
|
||||||
wandb.config.update({"env_cfg": asdict(env_cfg)})
|
|
||||||
|
|
||||||
def add_scalar(self, tag, scalar_value, global_step=None, walltime=None, new_style=False):
|
|
||||||
super().add_scalar(
|
|
||||||
tag,
|
|
||||||
scalar_value,
|
|
||||||
global_step=global_step,
|
|
||||||
walltime=walltime,
|
|
||||||
new_style=new_style,
|
|
||||||
)
|
|
||||||
wandb.log({self._map_path(tag): scalar_value}, step=global_step)
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
wandb.finish()
|
|
||||||
|
|
||||||
def log_config(self, env_cfg, runner_cfg, alg_cfg, policy_cfg):
|
|
||||||
self.store_config(env_cfg, runner_cfg, alg_cfg, policy_cfg)
|
|
||||||
|
|
||||||
def save_model(self, model_path, iter):
|
|
||||||
wandb.save(model_path, base_path=os.path.dirname(model_path))
|
|
||||||
|
|
||||||
def save_file(self, path, iter=None):
|
|
||||||
wandb.save(path, base_path=os.path.dirname(path))
|
|
||||||
|
|
||||||
"""
|
|
||||||
Private methods.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _map_path(self, path):
|
|
||||||
if path in self.name_map:
|
|
||||||
return self.name_map[path]
|
|
||||||
else:
|
|
||||||
return path
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
"""Submodule defining the environment definitions."""
|
|
||||||
|
|
||||||
from .vec_env import VecEnv
|
|
||||||
|
|
||||||
__all__ = ["VecEnv"]
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
from tensordict import TensorDict
|
|
||||||
|
|
||||||
|
|
||||||
class VecEnv(ABC):
|
|
||||||
"""Abstract class for a vectorized environment.
|
|
||||||
|
|
||||||
The vectorized environment is a collection of environments that are synchronized. This means that
|
|
||||||
the same type of action is applied to all environments and the same type of observation is returned from all
|
|
||||||
environments.
|
|
||||||
"""
|
|
||||||
|
|
||||||
num_envs: int
|
|
||||||
"""Number of environments."""
|
|
||||||
|
|
||||||
num_actions: int
|
|
||||||
"""Number of actions."""
|
|
||||||
|
|
||||||
max_episode_length: int | torch.Tensor
|
|
||||||
|
|
||||||
max_episode_length_s: float
|
|
||||||
"""Maximum episode length.
|
|
||||||
|
|
||||||
The maximum episode length can be a scalar or a tensor. If it is a scalar, it is the same for all environments.
|
|
||||||
If it is a tensor, it is the maximum episode length for each environment. This is useful for dynamic episode
|
|
||||||
lengths.
|
|
||||||
"""
|
|
||||||
|
|
||||||
episode_length_buf: torch.Tensor
|
|
||||||
"""Buffer for current episode lengths."""
|
|
||||||
|
|
||||||
device: torch.device | str
|
|
||||||
"""Device to use."""
|
|
||||||
|
|
||||||
cfg: dict | object
|
|
||||||
"""Configuration object."""
|
|
||||||
|
|
||||||
reset_env_ids: torch.Tensor | None = None
|
|
||||||
|
|
||||||
contact_phase: torch.Tensor | None = None
|
|
||||||
"""
|
|
||||||
Operations.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_observations(self) -> TensorDict:
|
|
||||||
"""Return the current observations.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
observations (TensorDict): Observations from the environment.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_amp_observations(self) -> TensorDict:
|
|
||||||
"""Return the current AMP observations.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
observations (TensorDict): Observations from the environment.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def step(self, actions: torch.Tensor) -> tuple[TensorDict, torch.Tensor, torch.Tensor, dict]:
|
|
||||||
"""Apply input action to the environment.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
actions (torch.Tensor): Input actions to apply. Shape: (num_envs, num_actions)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
observations (TensorDict): Observations from the environment.
|
|
||||||
rewards (torch.Tensor): Rewards from the environment. Shape: (num_envs,)
|
|
||||||
dones (torch.Tensor): Done flags from the environment. Shape: (num_envs,)
|
|
||||||
extras (dict): Extra information from the environment.
|
|
||||||
|
|
||||||
Observations:
|
|
||||||
|
|
||||||
The observations TensorDict usually contains multiple observation groups. The `obs_groups`
|
|
||||||
dictionary of the runner configuration specifies which observation groups are used for which
|
|
||||||
purpose, i.e., it maps the available observation groups to observation sets. The observation sets
|
|
||||||
(keys of the `obs_groups` dictionary) currently used by rsl_rl are:
|
|
||||||
|
|
||||||
- "policy": Specified observation groups are used as input to the actor/student network.
|
|
||||||
- "critic": Specified observation groups are used as input to the critic network.
|
|
||||||
- "teacher": Specified observation groups are used as input to the teacher network.
|
|
||||||
- "rnd_state": Specified observation groups are used as input to the RND network.
|
|
||||||
|
|
||||||
Incomplete or incorrect configurations are handled in the `resolve_obs_groups()` function in
|
|
||||||
`rsl_rl/utils/utils.py`.
|
|
||||||
|
|
||||||
Extras:
|
|
||||||
|
|
||||||
The extras dictionary includes metrics such as the episode reward, episode length, etc. The following
|
|
||||||
dictionary keys are used by rsl_rl:
|
|
||||||
|
|
||||||
- "time_outs" (torch.Tensor): Timeouts for the environments. These correspond to terminations that
|
|
||||||
happen due to time limits and not due to the environment reaching a terminal state. This is useful
|
|
||||||
for environments that have a fixed episode length.
|
|
||||||
|
|
||||||
- "log" (dict[str, float | torch.Tensor]): Additional information for logging and debugging purposes.
|
|
||||||
The key should be a string and start with "/" for namespacing. The value can be a scalar or a
|
|
||||||
tensor. If it is a tensor, the mean of the tensor is used for logging.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
"""Definitions for neural-network components for RL-agents."""
|
|
||||||
|
|
||||||
from .actor_critic import ActorCritic
|
|
||||||
from .actor_critic_recurrent import ActorCriticRecurrent
|
|
||||||
from .rnd import *
|
|
||||||
from .student_teacher import StudentTeacher
|
|
||||||
from .student_teacher_recurrent import StudentTeacherRecurrent
|
|
||||||
from .symmetry import *
|
|
||||||
from .discriminator_multi import DiscriminatorMulti
|
|
||||||
__all__ = [
|
|
||||||
"ActorCritic",
|
|
||||||
"ActorCriticRecurrent",
|
|
||||||
"StudentTeacher",
|
|
||||||
"StudentTeacherRecurrent",
|
|
||||||
"DiscriminatorMulti",
|
|
||||||
]
|
|
||||||
|
|
@ -1,195 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
from torch.distributions import Normal
|
|
||||||
|
|
||||||
from rsl_rl.networks import MLP, EmpiricalNormalization
|
|
||||||
|
|
||||||
|
|
||||||
class ActorCritic(nn.Module):
|
|
||||||
is_recurrent = False
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
obs,
|
|
||||||
obs_groups,
|
|
||||||
num_actions,
|
|
||||||
actor_obs_normalization=False,
|
|
||||||
critic_obs_normalization=False,
|
|
||||||
actor_hidden_dims=[256, 256, 256],
|
|
||||||
critic_hidden_dims=[256, 256, 256],
|
|
||||||
activation="elu",
|
|
||||||
init_noise_std=1.0,
|
|
||||||
noise_std_type: str = "scalar",
|
|
||||||
state_dependent_std=False,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
if kwargs:
|
|
||||||
print(
|
|
||||||
"ActorCritic.__init__ got unexpected arguments, which will be ignored: "
|
|
||||||
+ str([key for key in kwargs.keys()])
|
|
||||||
)
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
# get the observation dimensions
|
|
||||||
self.obs_groups = obs_groups
|
|
||||||
num_actor_obs = 0
|
|
||||||
for obs_group in obs_groups["policy"]:
|
|
||||||
assert len(obs[obs_group].shape) == 2, "The ActorCritic module only supports 1D observations."
|
|
||||||
num_actor_obs += obs[obs_group].shape[-1]
|
|
||||||
num_critic_obs = 0
|
|
||||||
for obs_group in obs_groups["critic"]:
|
|
||||||
assert len(obs[obs_group].shape) == 2, "The ActorCritic module only supports 1D observations."
|
|
||||||
num_critic_obs += obs[obs_group].shape[-1]
|
|
||||||
|
|
||||||
self.state_dependent_std = state_dependent_std
|
|
||||||
# actor
|
|
||||||
if self.state_dependent_std:
|
|
||||||
self.actor = MLP(num_actor_obs, [2, num_actions], actor_hidden_dims, activation)
|
|
||||||
else:
|
|
||||||
self.actor = MLP(num_actor_obs, num_actions, actor_hidden_dims, activation)
|
|
||||||
# actor observation normalization
|
|
||||||
self.actor_obs_normalization = actor_obs_normalization
|
|
||||||
if actor_obs_normalization:
|
|
||||||
self.actor_obs_normalizer = EmpiricalNormalization(num_actor_obs)
|
|
||||||
else:
|
|
||||||
self.actor_obs_normalizer = torch.nn.Identity()
|
|
||||||
print(f"Actor MLP: {self.actor}")
|
|
||||||
|
|
||||||
# critic
|
|
||||||
self.critic = MLP(num_critic_obs, 1, critic_hidden_dims, activation)
|
|
||||||
# critic observation normalization
|
|
||||||
self.critic_obs_normalization = critic_obs_normalization
|
|
||||||
if critic_obs_normalization:
|
|
||||||
self.critic_obs_normalizer = EmpiricalNormalization(num_critic_obs)
|
|
||||||
else:
|
|
||||||
self.critic_obs_normalizer = torch.nn.Identity()
|
|
||||||
print(f"Critic MLP: {self.critic}")
|
|
||||||
|
|
||||||
# Action noise
|
|
||||||
self.noise_std_type = noise_std_type
|
|
||||||
if self.state_dependent_std:
|
|
||||||
torch.nn.init.zeros_(self.actor[-2].weight[num_actions:])
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
torch.nn.init.constant_(self.actor[-2].bias[num_actions:], init_noise_std)
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
torch.nn.init.constant_(
|
|
||||||
self.actor[-2].bias[num_actions:], torch.log(torch.tensor(init_noise_std + 1e-7))
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
else:
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
self.std = nn.Parameter(init_noise_std * torch.ones(num_actions))
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
self.log_std = nn.Parameter(torch.log(init_noise_std * torch.ones(num_actions)))
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
|
|
||||||
# Action distribution (populated in update_distribution)
|
|
||||||
self.distribution = None
|
|
||||||
# disable args validation for speedup
|
|
||||||
Normal.set_default_validate_args(False)
|
|
||||||
|
|
||||||
def reset(self, dones=None):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def forward(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@property
|
|
||||||
def action_mean(self):
|
|
||||||
return self.distribution.mean
|
|
||||||
|
|
||||||
@property
|
|
||||||
def action_std(self):
|
|
||||||
return self.distribution.stddev
|
|
||||||
|
|
||||||
@property
|
|
||||||
def entropy(self):
|
|
||||||
return self.distribution.entropy().sum(dim=-1)
|
|
||||||
|
|
||||||
def update_distribution(self, obs):
|
|
||||||
if self.state_dependent_std:
|
|
||||||
# compute mean and standard deviation
|
|
||||||
mean_and_std = self.actor(obs)
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
mean, std = torch.unbind(mean_and_std, dim=-2)
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
mean, log_std = torch.unbind(mean_and_std, dim=-2)
|
|
||||||
std = torch.exp(log_std)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
else:
|
|
||||||
# compute mean
|
|
||||||
mean = self.actor(obs)
|
|
||||||
# compute standard deviation
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
std = self.std.expand_as(mean)
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
std = torch.exp(self.log_std).expand_as(mean)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
# create distribution
|
|
||||||
self.distribution = Normal(mean, std)
|
|
||||||
|
|
||||||
def act(self, obs, **kwargs):
|
|
||||||
obs = self.get_actor_obs(obs)
|
|
||||||
obs = self.actor_obs_normalizer(obs)
|
|
||||||
self.update_distribution(obs)
|
|
||||||
return self.distribution.sample()
|
|
||||||
|
|
||||||
def act_inference(self, obs):
|
|
||||||
obs = self.get_actor_obs(obs)
|
|
||||||
obs = self.actor_obs_normalizer(obs)
|
|
||||||
return self.actor(obs)
|
|
||||||
|
|
||||||
def evaluate(self, obs, **kwargs):
|
|
||||||
obs = self.get_critic_obs(obs)
|
|
||||||
obs = self.critic_obs_normalizer(obs)
|
|
||||||
return self.critic(obs)
|
|
||||||
|
|
||||||
def get_actor_obs(self, obs):
|
|
||||||
obs_list = []
|
|
||||||
for obs_group in self.obs_groups["policy"]:
|
|
||||||
obs_list.append(obs[obs_group])
|
|
||||||
return torch.cat(obs_list, dim=-1)
|
|
||||||
|
|
||||||
def get_critic_obs(self, obs):
|
|
||||||
obs_list = []
|
|
||||||
for obs_group in self.obs_groups["critic"]:
|
|
||||||
obs_list.append(obs[obs_group])
|
|
||||||
return torch.cat(obs_list, dim=-1)
|
|
||||||
|
|
||||||
def get_actions_log_prob(self, actions):
|
|
||||||
return self.distribution.log_prob(actions).sum(dim=-1)
|
|
||||||
|
|
||||||
def update_normalization(self, obs):
|
|
||||||
if self.actor_obs_normalization:
|
|
||||||
actor_obs = self.get_actor_obs(obs)
|
|
||||||
self.actor_obs_normalizer.update(actor_obs)
|
|
||||||
if self.critic_obs_normalization:
|
|
||||||
critic_obs = self.get_critic_obs(obs)
|
|
||||||
self.critic_obs_normalizer.update(critic_obs)
|
|
||||||
|
|
||||||
def load_state_dict(self, state_dict, strict=True):
|
|
||||||
"""Load the parameters of the actor-critic model.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
state_dict (dict): State dictionary of the model.
|
|
||||||
strict (bool): Whether to strictly enforce that the keys in state_dict match the keys returned by this
|
|
||||||
module's state_dict() function.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: Whether this training resumes a previous training. This flag is used by the `load()` function of
|
|
||||||
`OnPolicyRunner` to determine how to load further parameters (relevant for, e.g., distillation).
|
|
||||||
"""
|
|
||||||
|
|
||||||
super().load_state_dict(state_dict, strict=strict)
|
|
||||||
return True # training resumes
|
|
||||||
|
|
@ -1,218 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
import warnings
|
|
||||||
from torch.distributions import Normal
|
|
||||||
|
|
||||||
from rsl_rl.networks import MLP, EmpiricalNormalization, Memory
|
|
||||||
|
|
||||||
|
|
||||||
class ActorCriticRecurrent(nn.Module):
|
|
||||||
is_recurrent = True
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
obs,
|
|
||||||
obs_groups,
|
|
||||||
num_actions,
|
|
||||||
actor_obs_normalization=False,
|
|
||||||
critic_obs_normalization=False,
|
|
||||||
actor_hidden_dims=[256, 256, 256],
|
|
||||||
critic_hidden_dims=[256, 256, 256],
|
|
||||||
activation="elu",
|
|
||||||
init_noise_std=1.0,
|
|
||||||
noise_std_type: str = "scalar",
|
|
||||||
state_dependent_std=False,
|
|
||||||
rnn_type="lstm",
|
|
||||||
rnn_hidden_dim=256,
|
|
||||||
rnn_num_layers=1,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
if "rnn_hidden_size" in kwargs:
|
|
||||||
warnings.warn(
|
|
||||||
"The argument `rnn_hidden_size` is deprecated and will be removed in a future version. "
|
|
||||||
"Please use `rnn_hidden_dim` instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
if rnn_hidden_dim == 256: # Only override if the new argument is at its default
|
|
||||||
rnn_hidden_dim = kwargs.pop("rnn_hidden_size")
|
|
||||||
if kwargs:
|
|
||||||
print(
|
|
||||||
"ActorCriticRecurrent.__init__ got unexpected arguments, which will be ignored: " + str(kwargs.keys()),
|
|
||||||
)
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
# get the observation dimensions
|
|
||||||
self.obs_groups = obs_groups
|
|
||||||
num_actor_obs = 0
|
|
||||||
for obs_group in obs_groups["policy"]:
|
|
||||||
assert len(obs[obs_group].shape) == 2, "The ActorCriticRecurrent module only supports 1D observations."
|
|
||||||
num_actor_obs += obs[obs_group].shape[-1]
|
|
||||||
num_critic_obs = 0
|
|
||||||
for obs_group in obs_groups["critic"]:
|
|
||||||
assert len(obs[obs_group].shape) == 2, "The ActorCriticRecurrent module only supports 1D observations."
|
|
||||||
num_critic_obs += obs[obs_group].shape[-1]
|
|
||||||
|
|
||||||
self.state_dependent_std = state_dependent_std
|
|
||||||
# actor
|
|
||||||
self.memory_a = Memory(num_actor_obs, type=rnn_type, num_layers=rnn_num_layers, hidden_size=rnn_hidden_dim)
|
|
||||||
if self.state_dependent_std:
|
|
||||||
self.actor = MLP(rnn_hidden_dim, [2, num_actions], actor_hidden_dims, activation)
|
|
||||||
else:
|
|
||||||
self.actor = MLP(rnn_hidden_dim, num_actions, actor_hidden_dims, activation)
|
|
||||||
|
|
||||||
# actor observation normalization
|
|
||||||
self.actor_obs_normalization = actor_obs_normalization
|
|
||||||
if actor_obs_normalization:
|
|
||||||
self.actor_obs_normalizer = EmpiricalNormalization(num_actor_obs)
|
|
||||||
else:
|
|
||||||
self.actor_obs_normalizer = torch.nn.Identity()
|
|
||||||
print(f"Actor RNN: {self.memory_a}")
|
|
||||||
print(f"Actor MLP: {self.actor}")
|
|
||||||
|
|
||||||
# critic
|
|
||||||
self.memory_c = Memory(num_critic_obs, type=rnn_type, num_layers=rnn_num_layers, hidden_size=rnn_hidden_dim)
|
|
||||||
self.critic = MLP(rnn_hidden_dim, 1, critic_hidden_dims, activation)
|
|
||||||
# critic observation normalization
|
|
||||||
self.critic_obs_normalization = critic_obs_normalization
|
|
||||||
if critic_obs_normalization:
|
|
||||||
self.critic_obs_normalizer = EmpiricalNormalization(num_critic_obs)
|
|
||||||
else:
|
|
||||||
self.critic_obs_normalizer = torch.nn.Identity()
|
|
||||||
print(f"Critic RNN: {self.memory_c}")
|
|
||||||
print(f"Critic MLP: {self.critic}")
|
|
||||||
|
|
||||||
# Action noise
|
|
||||||
self.noise_std_type = noise_std_type
|
|
||||||
if self.state_dependent_std:
|
|
||||||
torch.nn.init.zeros_(self.actor[-2].weight[num_actions:])
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
torch.nn.init.constant_(self.actor[-2].bias[num_actions:], init_noise_std)
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
torch.nn.init.constant_(
|
|
||||||
self.actor[-2].bias[num_actions:], torch.log(torch.tensor(init_noise_std + 1e-7))
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
else:
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
self.std = nn.Parameter(init_noise_std * torch.ones(num_actions))
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
self.log_std = nn.Parameter(torch.log(init_noise_std * torch.ones(num_actions)))
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
|
|
||||||
# Action distribution (populated in update_distribution)
|
|
||||||
self.distribution = None
|
|
||||||
# disable args validation for speedup
|
|
||||||
Normal.set_default_validate_args(False)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def action_mean(self):
|
|
||||||
return self.distribution.mean
|
|
||||||
|
|
||||||
@property
|
|
||||||
def action_std(self):
|
|
||||||
return self.distribution.stddev
|
|
||||||
|
|
||||||
@property
|
|
||||||
def entropy(self):
|
|
||||||
return self.distribution.entropy().sum(dim=-1)
|
|
||||||
|
|
||||||
def reset(self, dones=None):
|
|
||||||
self.memory_a.reset(dones)
|
|
||||||
self.memory_c.reset(dones)
|
|
||||||
|
|
||||||
def forward(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update_distribution(self, obs):
|
|
||||||
if self.state_dependent_std:
|
|
||||||
# compute mean and standard deviation
|
|
||||||
mean_and_std = self.actor(obs)
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
mean, std = torch.unbind(mean_and_std, dim=-2)
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
mean, log_std = torch.unbind(mean_and_std, dim=-2)
|
|
||||||
std = torch.exp(log_std)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
else:
|
|
||||||
# compute mean
|
|
||||||
mean = self.actor(obs)
|
|
||||||
# compute standard deviation
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
std = self.std.expand_as(mean)
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
std = torch.exp(self.log_std).expand_as(mean)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
# create distribution
|
|
||||||
self.distribution = Normal(mean, std)
|
|
||||||
|
|
||||||
def act(self, obs, masks=None, hidden_states=None):
|
|
||||||
obs = self.get_actor_obs(obs)
|
|
||||||
obs = self.actor_obs_normalizer(obs)
|
|
||||||
out_mem = self.memory_a(obs, masks, hidden_states).squeeze(0)
|
|
||||||
self.update_distribution(out_mem)
|
|
||||||
return self.distribution.sample()
|
|
||||||
|
|
||||||
def act_inference(self, obs):
|
|
||||||
obs = self.get_actor_obs(obs)
|
|
||||||
obs = self.actor_obs_normalizer(obs)
|
|
||||||
out_mem = self.memory_a(obs).squeeze(0)
|
|
||||||
return self.actor(out_mem)
|
|
||||||
|
|
||||||
def evaluate(self, obs, masks=None, hidden_states=None):
|
|
||||||
obs = self.get_critic_obs(obs)
|
|
||||||
obs = self.critic_obs_normalizer(obs)
|
|
||||||
out_mem = self.memory_c(obs, masks, hidden_states).squeeze(0)
|
|
||||||
return self.critic(out_mem)
|
|
||||||
|
|
||||||
def get_actor_obs(self, obs):
|
|
||||||
obs_list = []
|
|
||||||
for obs_group in self.obs_groups["policy"]:
|
|
||||||
obs_list.append(obs[obs_group])
|
|
||||||
return torch.cat(obs_list, dim=-1)
|
|
||||||
|
|
||||||
def get_critic_obs(self, obs):
|
|
||||||
obs_list = []
|
|
||||||
for obs_group in self.obs_groups["critic"]:
|
|
||||||
obs_list.append(obs[obs_group])
|
|
||||||
return torch.cat(obs_list, dim=-1)
|
|
||||||
|
|
||||||
def get_actions_log_prob(self, actions):
|
|
||||||
return self.distribution.log_prob(actions).sum(dim=-1)
|
|
||||||
|
|
||||||
def get_hidden_states(self):
|
|
||||||
return self.memory_a.hidden_states, self.memory_c.hidden_states
|
|
||||||
|
|
||||||
def update_normalization(self, obs):
|
|
||||||
if self.actor_obs_normalization:
|
|
||||||
actor_obs = self.get_actor_obs(obs)
|
|
||||||
self.actor_obs_normalizer.update(actor_obs)
|
|
||||||
if self.critic_obs_normalization:
|
|
||||||
critic_obs = self.get_critic_obs(obs)
|
|
||||||
self.critic_obs_normalizer.update(critic_obs)
|
|
||||||
|
|
||||||
def load_state_dict(self, state_dict, strict=True):
|
|
||||||
"""Load the parameters of the actor-critic model.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
state_dict (dict): State dictionary of the model.
|
|
||||||
strict (bool): Whether to strictly enforce that the keys in state_dict match the keys returned by this
|
|
||||||
module's state_dict() function.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: Whether this training resumes a previous training. This flag is used by the `load()` function of
|
|
||||||
`OnPolicyRunner` to determine how to load further parameters (relevant for, e.g., distillation).
|
|
||||||
"""
|
|
||||||
|
|
||||||
super().load_state_dict(state_dict, strict=strict)
|
|
||||||
return True
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
from torch import autograd
|
|
||||||
import torch.nn.utils.spectral_norm as spectral_norm
|
|
||||||
|
|
||||||
|
|
||||||
class DiscriminatorMulti(nn.Module):
|
|
||||||
def __init__(
|
|
||||||
self, state_dim, amp_reward_coef, hidden_layer_sizes, device,
|
|
||||||
num_frames=2, task_reward_lerp=0.0, use_lerp=True):
|
|
||||||
super(DiscriminatorMulti, self).__init__()
|
|
||||||
|
|
||||||
self.device = device
|
|
||||||
self.state_dim = state_dim
|
|
||||||
self.use_lerp = use_lerp
|
|
||||||
self.num_frames = num_frames # 存储帧数参数
|
|
||||||
|
|
||||||
self.amp_reward_coef = amp_reward_coef
|
|
||||||
amp_layers = []
|
|
||||||
|
|
||||||
curr_in_dim = state_dim * num_frames
|
|
||||||
for hidden_dim in hidden_layer_sizes:
|
|
||||||
amp_layers.append(spectral_norm(nn.Linear(curr_in_dim, hidden_dim)))
|
|
||||||
amp_layers.append(nn.ReLU())
|
|
||||||
curr_in_dim = hidden_dim
|
|
||||||
self.trunk = nn.Sequential(*amp_layers).to(device)
|
|
||||||
self.amp_linear = spectral_norm(nn.Linear(hidden_layer_sizes[-1], 1)).to(device)
|
|
||||||
|
|
||||||
self.trunk.train()
|
|
||||||
self.amp_linear.train()
|
|
||||||
|
|
||||||
self.task_reward_lerp = task_reward_lerp
|
|
||||||
|
|
||||||
def forward(self, x):
|
|
||||||
h = self.trunk(x)
|
|
||||||
d = self.amp_linear(h)
|
|
||||||
return d
|
|
||||||
|
|
||||||
def compute_grad_pen(self,
|
|
||||||
expert_states, # 改为接收多帧状态列表
|
|
||||||
lambda_=10):
|
|
||||||
# 将多帧状态沿最后一个维度拼接
|
|
||||||
expert_data = expert_states.flatten(1)
|
|
||||||
expert_data.requires_grad = True
|
|
||||||
|
|
||||||
disc = self.amp_linear(self.trunk(expert_data))
|
|
||||||
ones = torch.ones(disc.size(), device=disc.device)
|
|
||||||
grad = autograd.grad(
|
|
||||||
outputs=disc, inputs=expert_data,
|
|
||||||
grad_outputs=ones, create_graph=True,
|
|
||||||
retain_graph=True, only_inputs=True)[0]
|
|
||||||
|
|
||||||
# Enforce that the grad norm approaches 0.
|
|
||||||
grad_pen = lambda_ * (grad.norm(2, dim=1) - 0).pow(2).mean()
|
|
||||||
return grad_pen
|
|
||||||
|
|
||||||
|
|
||||||
def get_disc_weights(self):
|
|
||||||
weights = []
|
|
||||||
for m in self.trunk.modules():
|
|
||||||
if isinstance(m, nn.Linear):
|
|
||||||
weights.append(torch.flatten(m.weight))
|
|
||||||
|
|
||||||
weights.append(torch.flatten(self.amp_linear.weight))
|
|
||||||
return weights
|
|
||||||
|
|
||||||
def get_disc_logit_weights(self):
|
|
||||||
return torch.flatten(self.amp_linear.weight)
|
|
||||||
|
|
||||||
def predict_amp_reward(
|
|
||||||
self, states, # 改为接收多帧状态列表
|
|
||||||
task_reward, normalizer=None):
|
|
||||||
"""
|
|
||||||
states: torch.Tensor, shape=(num_envs, num_frames, state_dim)
|
|
||||||
task_reward: torch.Tensor, shape=(num_envs, 1)
|
|
||||||
"""
|
|
||||||
# import ipdb; ipdb.set_trace()
|
|
||||||
with torch.no_grad():
|
|
||||||
self.eval()
|
|
||||||
if normalizer is not None:
|
|
||||||
# 对每一帧状态进行归一化
|
|
||||||
states = normalizer.normalize_torch(states, self.device)
|
|
||||||
|
|
||||||
# 拼接多帧状态
|
|
||||||
state_cat = states.flatten(1)
|
|
||||||
d = self.amp_linear(self.trunk(state_cat))
|
|
||||||
disc_reward = self.amp_reward_coef * torch.clamp(1 - (1/4) * torch.square(d - 1), min=0)
|
|
||||||
|
|
||||||
if self.use_lerp:
|
|
||||||
if self.task_reward_lerp > 0:
|
|
||||||
reward = self._lerp_reward(disc_reward, task_reward.unsqueeze(-1))
|
|
||||||
self.train()
|
|
||||||
return reward.squeeze(), d, disc_reward.squeeze() * (1.0 - self.task_reward_lerp)
|
|
||||||
else:
|
|
||||||
disc_reward *= 0.02
|
|
||||||
reward = task_reward.unsqueeze(-1) + disc_reward
|
|
||||||
self.train()
|
|
||||||
return reward.squeeze(), d, disc_reward.squeeze()
|
|
||||||
|
|
||||||
def _lerp_reward(self, disc_r, task_r):
|
|
||||||
r = (1.0 - self.task_reward_lerp) * disc_r + self.task_reward_lerp * task_r
|
|
||||||
return r
|
|
||||||
|
|
@ -1,209 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
|
|
||||||
from rsl_rl.networks import MLP, EmpiricalDiscountedVariationNormalization, EmpiricalNormalization
|
|
||||||
|
|
||||||
|
|
||||||
class RandomNetworkDistillation(nn.Module):
|
|
||||||
"""Implementation of Random Network Distillation (RND) [1]
|
|
||||||
|
|
||||||
References:
|
|
||||||
.. [1] Burda, Yuri, et al. "Exploration by random network distillation." arXiv preprint arXiv:1810.12894 (2018).
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
num_states: int,
|
|
||||||
obs_groups: dict,
|
|
||||||
num_outputs: int,
|
|
||||||
predictor_hidden_dims: list[int],
|
|
||||||
target_hidden_dims: list[int],
|
|
||||||
activation: str = "elu",
|
|
||||||
weight: float = 0.0,
|
|
||||||
state_normalization: bool = False,
|
|
||||||
reward_normalization: bool = False,
|
|
||||||
device: str = "cpu",
|
|
||||||
weight_schedule: dict | None = None,
|
|
||||||
):
|
|
||||||
"""Initialize the RND module.
|
|
||||||
|
|
||||||
- If :attr:`state_normalization` is True, then the input state is normalized using an Empirical Normalization layer.
|
|
||||||
- If :attr:`reward_normalization` is True, then the intrinsic reward is normalized using an Empirical Discounted
|
|
||||||
Variation Normalization layer.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
If the hidden dimensions are -1 in the predictor and target networks configuration, then the number of states
|
|
||||||
is used as the hidden dimension.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
num_states: Number of states/inputs to the predictor and target networks.
|
|
||||||
num_outputs: Number of outputs (embedding size) of the predictor and target networks.
|
|
||||||
predictor_hidden_dims: List of hidden dimensions of the predictor network.
|
|
||||||
target_hidden_dims: List of hidden dimensions of the target network.
|
|
||||||
activation: Activation function. Defaults to "elu".
|
|
||||||
weight: Scaling factor of the intrinsic reward. Defaults to 0.0.
|
|
||||||
state_normalization: Whether to normalize the input state. Defaults to False.
|
|
||||||
reward_normalization: Whether to normalize the intrinsic reward. Defaults to False.
|
|
||||||
device: Device to use. Defaults to "cpu".
|
|
||||||
weight_schedule: The type of schedule to use for the RND weight parameter.
|
|
||||||
Defaults to None, in which case the weight parameter is constant.
|
|
||||||
It is a dictionary with the following keys:
|
|
||||||
|
|
||||||
- "mode": The type of schedule to use for the RND weight parameter.
|
|
||||||
- "constant": Constant weight schedule.
|
|
||||||
- "step": Step weight schedule.
|
|
||||||
- "linear": Linear weight schedule.
|
|
||||||
|
|
||||||
For the "step" weight schedule, the following parameters are required:
|
|
||||||
|
|
||||||
- "final_step": The step at which the weight parameter is set to the final value.
|
|
||||||
- "final_value": The final value of the weight parameter.
|
|
||||||
|
|
||||||
For the "linear" weight schedule, the following parameters are required:
|
|
||||||
- "initial_step": The step at which the weight parameter is set to the initial value.
|
|
||||||
- "final_step": The step at which the weight parameter is set to the final value.
|
|
||||||
- "final_value": The final value of the weight parameter.
|
|
||||||
"""
|
|
||||||
# initialize parent class
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
# Store parameters
|
|
||||||
self.num_states = num_states
|
|
||||||
self.obs_groups = obs_groups
|
|
||||||
self.num_outputs = num_outputs
|
|
||||||
self.initial_weight = weight
|
|
||||||
self.device = device
|
|
||||||
self.state_normalization = state_normalization
|
|
||||||
self.reward_normalization = reward_normalization
|
|
||||||
|
|
||||||
# Normalization of input gates
|
|
||||||
if state_normalization:
|
|
||||||
self.state_normalizer = EmpiricalNormalization(shape=[self.num_states], until=1.0e8).to(self.device)
|
|
||||||
else:
|
|
||||||
self.state_normalizer = torch.nn.Identity()
|
|
||||||
# Normalization of intrinsic reward
|
|
||||||
if reward_normalization:
|
|
||||||
self.reward_normalizer = EmpiricalDiscountedVariationNormalization(shape=[], until=1.0e8).to(self.device)
|
|
||||||
else:
|
|
||||||
self.reward_normalizer = torch.nn.Identity()
|
|
||||||
|
|
||||||
# counter for the number of updates
|
|
||||||
self.update_counter = 0
|
|
||||||
|
|
||||||
# resolve weight schedule
|
|
||||||
if weight_schedule is not None:
|
|
||||||
self.weight_scheduler_params = weight_schedule
|
|
||||||
self.weight_scheduler = getattr(self, f"_{weight_schedule['mode']}_weight_schedule")
|
|
||||||
else:
|
|
||||||
self.weight_scheduler = None
|
|
||||||
# Create network architecture
|
|
||||||
self.predictor = MLP(num_states, num_outputs, predictor_hidden_dims, activation).to(self.device)
|
|
||||||
self.target = MLP(num_states, num_outputs, target_hidden_dims, activation).to(self.device)
|
|
||||||
|
|
||||||
# make target network not trainable
|
|
||||||
self.target.eval()
|
|
||||||
|
|
||||||
def get_intrinsic_reward(self, obs) -> torch.Tensor:
|
|
||||||
# Note: the counter is updated number of env steps per learning iteration
|
|
||||||
self.update_counter += 1
|
|
||||||
# Extract the rnd state from the observation
|
|
||||||
rnd_state = self.get_rnd_state(obs)
|
|
||||||
rnd_state = self.state_normalizer(rnd_state)
|
|
||||||
# Obtain the embedding of the rnd state from the target and predictor networks
|
|
||||||
target_embedding = self.target(rnd_state).detach()
|
|
||||||
predictor_embedding = self.predictor(rnd_state).detach()
|
|
||||||
# Compute the intrinsic reward as the distance between the embeddings
|
|
||||||
intrinsic_reward = torch.linalg.norm(target_embedding - predictor_embedding, dim=1)
|
|
||||||
# Normalize intrinsic reward
|
|
||||||
intrinsic_reward = self.reward_normalizer(intrinsic_reward)
|
|
||||||
|
|
||||||
# Check the weight schedule
|
|
||||||
if self.weight_scheduler is not None:
|
|
||||||
self.weight = self.weight_scheduler(step=self.update_counter, **self.weight_scheduler_params)
|
|
||||||
else:
|
|
||||||
self.weight = self.initial_weight
|
|
||||||
# Scale intrinsic reward
|
|
||||||
intrinsic_reward *= self.weight
|
|
||||||
|
|
||||||
return intrinsic_reward
|
|
||||||
|
|
||||||
def forward(self, *args, **kwargs):
|
|
||||||
raise RuntimeError("Forward method is not implemented. Use get_intrinsic_reward instead.")
|
|
||||||
|
|
||||||
def train(self, mode: bool = True):
|
|
||||||
# sets module into training mode
|
|
||||||
self.predictor.train(mode)
|
|
||||||
if self.state_normalization:
|
|
||||||
self.state_normalizer.train(mode)
|
|
||||||
if self.reward_normalization:
|
|
||||||
self.reward_normalizer.train(mode)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def eval(self):
|
|
||||||
return self.train(False)
|
|
||||||
|
|
||||||
def get_rnd_state(self, obs):
|
|
||||||
obs_list = []
|
|
||||||
for obs_group in self.obs_groups["rnd_state"]:
|
|
||||||
obs_list.append(obs[obs_group])
|
|
||||||
return torch.cat(obs_list, dim=-1)
|
|
||||||
|
|
||||||
def update_normalization(self, obs):
|
|
||||||
# Normalize the state
|
|
||||||
if self.state_normalization:
|
|
||||||
rnd_state = self.get_rnd_state(obs)
|
|
||||||
self.state_normalizer.update(rnd_state)
|
|
||||||
|
|
||||||
"""
|
|
||||||
Different weight schedules.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _constant_weight_schedule(self, step: int, **kwargs):
|
|
||||||
return self.initial_weight
|
|
||||||
|
|
||||||
def _step_weight_schedule(self, step: int, final_step: int, final_value: float, **kwargs):
|
|
||||||
return self.initial_weight if step < final_step else final_value
|
|
||||||
|
|
||||||
def _linear_weight_schedule(self, step: int, initial_step: int, final_step: int, final_value: float, **kwargs):
|
|
||||||
if step < initial_step:
|
|
||||||
return self.initial_weight
|
|
||||||
elif step > final_step:
|
|
||||||
return final_value
|
|
||||||
else:
|
|
||||||
return self.initial_weight + (final_value - self.initial_weight) * (step - initial_step) / (
|
|
||||||
final_step - initial_step
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_rnd_config(alg_cfg, obs, obs_groups, env):
|
|
||||||
"""Resolve the RND configuration.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
alg_cfg: The algorithm configuration dictionary.
|
|
||||||
obs: The observation dictionary.
|
|
||||||
obs_groups: The observation groups dictionary.
|
|
||||||
env: The environment.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The resolved algorithm configuration dictionary.
|
|
||||||
"""
|
|
||||||
# resolve dimension of rnd gated state
|
|
||||||
if "rnd_cfg" in alg_cfg and alg_cfg["rnd_cfg"] is not None:
|
|
||||||
# get dimension of rnd gated state
|
|
||||||
num_rnd_state = 0
|
|
||||||
for obs_group in obs_groups["rnd_state"]:
|
|
||||||
assert len(obs[obs_group].shape) == 2, "The RND module only supports 1D observations."
|
|
||||||
num_rnd_state += obs[obs_group].shape[-1]
|
|
||||||
# add rnd gated state to config
|
|
||||||
alg_cfg["rnd_cfg"]["num_states"] = num_rnd_state
|
|
||||||
alg_cfg["rnd_cfg"]["obs_groups"] = obs_groups
|
|
||||||
# scale down the rnd weight with timestep
|
|
||||||
alg_cfg["rnd_cfg"]["weight"] *= env.unwrapped.step_dt
|
|
||||||
return alg_cfg
|
|
||||||
|
|
@ -1,206 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
from torch.distributions import Normal
|
|
||||||
|
|
||||||
from rsl_rl.networks import MLP, EmpiricalNormalization
|
|
||||||
|
|
||||||
|
|
||||||
class StudentTeacher(nn.Module):
|
|
||||||
is_recurrent = False
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
obs,
|
|
||||||
obs_groups,
|
|
||||||
num_actions,
|
|
||||||
student_obs_normalization=False,
|
|
||||||
teacher_obs_normalization=False,
|
|
||||||
student_hidden_dims=[256, 256, 256],
|
|
||||||
teacher_hidden_dims=[256, 256, 256],
|
|
||||||
activation="elu",
|
|
||||||
init_noise_std=0.1,
|
|
||||||
noise_std_type: str = "scalar",
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
if kwargs:
|
|
||||||
print(
|
|
||||||
"StudentTeacher.__init__ got unexpected arguments, which will be ignored: "
|
|
||||||
+ str([key for key in kwargs.keys()])
|
|
||||||
)
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.loaded_teacher = False # indicates if teacher has been loaded
|
|
||||||
|
|
||||||
# get the observation dimensions
|
|
||||||
self.obs_groups = obs_groups
|
|
||||||
num_student_obs = 0
|
|
||||||
for obs_group in obs_groups["policy"]:
|
|
||||||
assert len(obs[obs_group].shape) == 2, "The StudentTeacher module only supports 1D observations."
|
|
||||||
num_student_obs += obs[obs_group].shape[-1]
|
|
||||||
num_teacher_obs = 0
|
|
||||||
for obs_group in obs_groups["teacher"]:
|
|
||||||
assert len(obs[obs_group].shape) == 2, "The StudentTeacher module only supports 1D observations."
|
|
||||||
num_teacher_obs += obs[obs_group].shape[-1]
|
|
||||||
|
|
||||||
# student
|
|
||||||
self.student = MLP(num_student_obs, num_actions, student_hidden_dims, activation)
|
|
||||||
|
|
||||||
# student observation normalization
|
|
||||||
self.student_obs_normalization = student_obs_normalization
|
|
||||||
if student_obs_normalization:
|
|
||||||
self.student_obs_normalizer = EmpiricalNormalization(num_student_obs)
|
|
||||||
else:
|
|
||||||
self.student_obs_normalizer = torch.nn.Identity()
|
|
||||||
|
|
||||||
print(f"Student MLP: {self.student}")
|
|
||||||
|
|
||||||
# teacher
|
|
||||||
self.teacher = MLP(num_teacher_obs, num_actions, teacher_hidden_dims, activation)
|
|
||||||
self.teacher.eval()
|
|
||||||
|
|
||||||
# teacher observation normalization
|
|
||||||
self.teacher_obs_normalization = teacher_obs_normalization
|
|
||||||
if teacher_obs_normalization:
|
|
||||||
self.teacher_obs_normalizer = EmpiricalNormalization(num_teacher_obs)
|
|
||||||
else:
|
|
||||||
self.teacher_obs_normalizer = torch.nn.Identity()
|
|
||||||
|
|
||||||
print(f"Teacher MLP: {self.teacher}")
|
|
||||||
|
|
||||||
# action noise
|
|
||||||
self.noise_std_type = noise_std_type
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
self.std = nn.Parameter(init_noise_std * torch.ones(num_actions))
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
self.log_std = nn.Parameter(torch.log(init_noise_std * torch.ones(num_actions)))
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
|
|
||||||
# action distribution (populated in update_distribution)
|
|
||||||
self.distribution = None
|
|
||||||
# disable args validation for speedup
|
|
||||||
Normal.set_default_validate_args(False)
|
|
||||||
|
|
||||||
def reset(self, dones=None, hidden_states=None):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def forward(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@property
|
|
||||||
def action_mean(self):
|
|
||||||
return self.distribution.mean
|
|
||||||
|
|
||||||
@property
|
|
||||||
def action_std(self):
|
|
||||||
return self.distribution.stddev
|
|
||||||
|
|
||||||
@property
|
|
||||||
def entropy(self):
|
|
||||||
return self.distribution.entropy().sum(dim=-1)
|
|
||||||
|
|
||||||
def update_distribution(self, obs):
|
|
||||||
# compute mean
|
|
||||||
mean = self.student(obs)
|
|
||||||
# compute standard deviation
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
std = self.std.expand_as(mean)
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
std = torch.exp(self.log_std).expand_as(mean)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
# create distribution
|
|
||||||
self.distribution = Normal(mean, std)
|
|
||||||
|
|
||||||
def act(self, obs):
|
|
||||||
obs = self.get_student_obs(obs)
|
|
||||||
obs = self.student_obs_normalizer(obs)
|
|
||||||
self.update_distribution(obs)
|
|
||||||
return self.distribution.sample()
|
|
||||||
|
|
||||||
def act_inference(self, obs):
|
|
||||||
obs = self.get_student_obs(obs)
|
|
||||||
obs = self.student_obs_normalizer(obs)
|
|
||||||
return self.student(obs)
|
|
||||||
|
|
||||||
def evaluate(self, obs):
|
|
||||||
obs = self.get_teacher_obs(obs)
|
|
||||||
obs = self.teacher_obs_normalizer(obs)
|
|
||||||
with torch.no_grad():
|
|
||||||
return self.teacher(obs)
|
|
||||||
|
|
||||||
def get_student_obs(self, obs):
|
|
||||||
obs_list = []
|
|
||||||
for obs_group in self.obs_groups["policy"]:
|
|
||||||
obs_list.append(obs[obs_group])
|
|
||||||
return torch.cat(obs_list, dim=-1)
|
|
||||||
|
|
||||||
def get_teacher_obs(self, obs):
|
|
||||||
obs_list = []
|
|
||||||
for obs_group in self.obs_groups["teacher"]:
|
|
||||||
obs_list.append(obs[obs_group])
|
|
||||||
return torch.cat(obs_list, dim=-1)
|
|
||||||
|
|
||||||
def get_hidden_states(self):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def detach_hidden_states(self, dones=None):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def train(self, mode=True):
|
|
||||||
super().train(mode)
|
|
||||||
# make sure teacher is in eval mode
|
|
||||||
self.teacher.eval()
|
|
||||||
self.teacher_obs_normalizer.eval()
|
|
||||||
|
|
||||||
def update_normalization(self, obs):
|
|
||||||
if self.student_obs_normalization:
|
|
||||||
student_obs = self.get_student_obs(obs)
|
|
||||||
self.student_obs_normalizer.update(student_obs)
|
|
||||||
|
|
||||||
def load_state_dict(self, state_dict, strict=True):
|
|
||||||
"""Load the parameters of the student and teacher networks.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
state_dict (dict): State dictionary of the model.
|
|
||||||
strict (bool): Whether to strictly enforce that the keys in state_dict match the keys returned by this
|
|
||||||
module's state_dict() function.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: Whether this training resumes a previous training. This flag is used by the `load()` function of
|
|
||||||
`OnPolicyRunner` to determine how to load further parameters.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# check if state_dict contains teacher and student or just teacher parameters
|
|
||||||
if any("actor" in key for key in state_dict.keys()): # loading parameters from rl training
|
|
||||||
# rename keys to match teacher and remove critic parameters
|
|
||||||
teacher_state_dict = {}
|
|
||||||
teacher_obs_normalizer_state_dict = {}
|
|
||||||
for key, value in state_dict.items():
|
|
||||||
if "actor." in key:
|
|
||||||
teacher_state_dict[key.replace("actor.", "")] = value
|
|
||||||
if "actor_obs_normalizer." in key:
|
|
||||||
teacher_obs_normalizer_state_dict[key.replace("actor_obs_normalizer.", "")] = value
|
|
||||||
self.teacher.load_state_dict(teacher_state_dict, strict=strict)
|
|
||||||
self.teacher_obs_normalizer.load_state_dict(teacher_obs_normalizer_state_dict, strict=strict)
|
|
||||||
# set flag for successfully loading the parameters
|
|
||||||
self.loaded_teacher = True
|
|
||||||
self.teacher.eval()
|
|
||||||
self.teacher_obs_normalizer.eval()
|
|
||||||
return False # training does not resume
|
|
||||||
elif any("student" in key for key in state_dict.keys()): # loading parameters from distillation training
|
|
||||||
super().load_state_dict(state_dict, strict=strict)
|
|
||||||
# set flag for successfully loading the parameters
|
|
||||||
self.loaded_teacher = True
|
|
||||||
self.teacher.eval()
|
|
||||||
self.teacher_obs_normalizer.eval()
|
|
||||||
return True # training resumes
|
|
||||||
else:
|
|
||||||
raise ValueError("state_dict does not contain student or teacher parameters")
|
|
||||||
|
|
@ -1,249 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
import warnings
|
|
||||||
from torch.distributions import Normal
|
|
||||||
|
|
||||||
from rsl_rl.networks import MLP, EmpiricalNormalization, Memory
|
|
||||||
|
|
||||||
|
|
||||||
class StudentTeacherRecurrent(nn.Module):
|
|
||||||
is_recurrent = True
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
obs,
|
|
||||||
obs_groups,
|
|
||||||
num_actions,
|
|
||||||
student_obs_normalization=False,
|
|
||||||
teacher_obs_normalization=False,
|
|
||||||
student_hidden_dims=[256, 256, 256],
|
|
||||||
teacher_hidden_dims=[256, 256, 256],
|
|
||||||
activation="elu",
|
|
||||||
init_noise_std=0.1,
|
|
||||||
noise_std_type: str = "scalar",
|
|
||||||
rnn_type="lstm",
|
|
||||||
rnn_hidden_dim=256,
|
|
||||||
rnn_num_layers=1,
|
|
||||||
teacher_recurrent=False,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
if "rnn_hidden_size" in kwargs:
|
|
||||||
warnings.warn(
|
|
||||||
"The argument `rnn_hidden_size` is deprecated and will be removed in a future version. "
|
|
||||||
"Please use `rnn_hidden_dim` instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
if rnn_hidden_dim == 256: # Only override if the new argument is at its default
|
|
||||||
rnn_hidden_dim = kwargs.pop("rnn_hidden_size")
|
|
||||||
if kwargs:
|
|
||||||
print(
|
|
||||||
"StudentTeacherRecurrent.__init__ got unexpected arguments, which will be ignored: "
|
|
||||||
+ str(kwargs.keys()),
|
|
||||||
)
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.loaded_teacher = False # indicates if teacher has been loaded
|
|
||||||
self.teacher_recurrent = teacher_recurrent # indicates if teacher is recurrent too
|
|
||||||
|
|
||||||
# get the observation dimensions
|
|
||||||
self.obs_groups = obs_groups
|
|
||||||
num_student_obs = 0
|
|
||||||
for obs_group in obs_groups["policy"]:
|
|
||||||
assert len(obs[obs_group].shape) == 2, "The StudentTeacher module only supports 1D observations."
|
|
||||||
num_student_obs += obs[obs_group].shape[-1]
|
|
||||||
num_teacher_obs = 0
|
|
||||||
for obs_group in obs_groups["teacher"]:
|
|
||||||
assert len(obs[obs_group].shape) == 2, "The StudentTeacher module only supports 1D observations."
|
|
||||||
num_teacher_obs += obs[obs_group].shape[-1]
|
|
||||||
|
|
||||||
# student
|
|
||||||
self.memory_s = Memory(num_student_obs, type=rnn_type, num_layers=rnn_num_layers, hidden_size=rnn_hidden_dim)
|
|
||||||
self.student = MLP(rnn_hidden_dim, num_actions, student_hidden_dims, activation)
|
|
||||||
|
|
||||||
# student observation normalization
|
|
||||||
self.student_obs_normalization = student_obs_normalization
|
|
||||||
if student_obs_normalization:
|
|
||||||
self.student_obs_normalizer = EmpiricalNormalization(num_student_obs)
|
|
||||||
else:
|
|
||||||
self.student_obs_normalizer = torch.nn.Identity()
|
|
||||||
|
|
||||||
print(f"Student RNN: {self.memory_s}")
|
|
||||||
print(f"Student MLP: {self.student}")
|
|
||||||
|
|
||||||
# teacher
|
|
||||||
if self.teacher_recurrent:
|
|
||||||
self.memory_t = Memory(
|
|
||||||
num_teacher_obs, type=rnn_type, num_layers=rnn_num_layers, hidden_size=rnn_hidden_dim
|
|
||||||
)
|
|
||||||
num_teacher_obs = rnn_hidden_dim
|
|
||||||
self.teacher = MLP(num_teacher_obs, num_actions, teacher_hidden_dims, activation)
|
|
||||||
|
|
||||||
# teacher observation normalization
|
|
||||||
self.teacher_obs_normalization = teacher_obs_normalization
|
|
||||||
if teacher_obs_normalization:
|
|
||||||
self.teacher_obs_normalizer = EmpiricalNormalization(num_teacher_obs)
|
|
||||||
else:
|
|
||||||
self.teacher_obs_normalizer = torch.nn.Identity()
|
|
||||||
|
|
||||||
if self.teacher_recurrent:
|
|
||||||
print(f"Teacher RNN: {self.memory_t}")
|
|
||||||
print(f"Teacher MLP: {self.teacher}")
|
|
||||||
|
|
||||||
# action noise
|
|
||||||
self.noise_std_type = noise_std_type
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
self.std = nn.Parameter(init_noise_std * torch.ones(num_actions))
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
self.log_std = nn.Parameter(torch.log(init_noise_std * torch.ones(num_actions)))
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
|
|
||||||
# action distribution (populated in update_distribution)
|
|
||||||
self.distribution = None
|
|
||||||
# disable args validation for speedup
|
|
||||||
Normal.set_default_validate_args(False)
|
|
||||||
|
|
||||||
def reset(self, dones=None, hidden_states=None):
|
|
||||||
if hidden_states is None:
|
|
||||||
hidden_states = (None, None)
|
|
||||||
self.memory_s.reset(dones, hidden_states[0])
|
|
||||||
if self.teacher_recurrent:
|
|
||||||
self.memory_t.reset(dones, hidden_states[1])
|
|
||||||
|
|
||||||
def forward(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@property
|
|
||||||
def action_mean(self):
|
|
||||||
return self.distribution.mean
|
|
||||||
|
|
||||||
@property
|
|
||||||
def action_std(self):
|
|
||||||
return self.distribution.stddev
|
|
||||||
|
|
||||||
@property
|
|
||||||
def entropy(self):
|
|
||||||
return self.distribution.entropy().sum(dim=-1)
|
|
||||||
|
|
||||||
def update_distribution(self, obs):
|
|
||||||
# compute mean
|
|
||||||
mean = self.student(obs)
|
|
||||||
# compute standard deviation
|
|
||||||
if self.noise_std_type == "scalar":
|
|
||||||
std = self.std.expand_as(mean)
|
|
||||||
elif self.noise_std_type == "log":
|
|
||||||
std = torch.exp(self.log_std).expand_as(mean)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown standard deviation type: {self.noise_std_type}. Should be 'scalar' or 'log'")
|
|
||||||
# create distribution
|
|
||||||
self.distribution = Normal(mean, std)
|
|
||||||
|
|
||||||
def act(self, obs):
|
|
||||||
obs = self.get_student_obs(obs)
|
|
||||||
obs = self.student_obs_normalizer(obs)
|
|
||||||
out_mem = self.memory_s(obs).squeeze(0)
|
|
||||||
self.update_distribution(out_mem)
|
|
||||||
return self.distribution.sample()
|
|
||||||
|
|
||||||
def act_inference(self, obs):
|
|
||||||
obs = self.get_student_obs(obs)
|
|
||||||
obs = self.student_obs_normalizer(obs)
|
|
||||||
out_mem = self.memory_s(obs).squeeze(0)
|
|
||||||
return self.student(out_mem)
|
|
||||||
|
|
||||||
def evaluate(self, obs):
|
|
||||||
obs = self.get_teacher_obs(obs)
|
|
||||||
obs = self.teacher_obs_normalizer(obs)
|
|
||||||
with torch.no_grad():
|
|
||||||
if self.teacher_recurrent:
|
|
||||||
self.memory_t.eval()
|
|
||||||
obs = self.memory_t(obs).squeeze(0)
|
|
||||||
return self.teacher(obs)
|
|
||||||
|
|
||||||
def get_student_obs(self, obs):
|
|
||||||
obs_list = []
|
|
||||||
for obs_group in self.obs_groups["policy"]:
|
|
||||||
obs_list.append(obs[obs_group])
|
|
||||||
return torch.cat(obs_list, dim=-1)
|
|
||||||
|
|
||||||
def get_teacher_obs(self, obs):
|
|
||||||
obs_list = []
|
|
||||||
for obs_group in self.obs_groups["teacher"]:
|
|
||||||
obs_list.append(obs[obs_group])
|
|
||||||
return torch.cat(obs_list, dim=-1)
|
|
||||||
|
|
||||||
def get_hidden_states(self):
|
|
||||||
if self.teacher_recurrent:
|
|
||||||
return self.memory_s.hidden_states, self.memory_t.hidden_states
|
|
||||||
else:
|
|
||||||
return self.memory_s.hidden_states, None
|
|
||||||
|
|
||||||
def detach_hidden_states(self, dones=None):
|
|
||||||
self.memory_s.detach_hidden_states(dones)
|
|
||||||
if self.teacher_recurrent:
|
|
||||||
self.memory_t.detach_hidden_states(dones)
|
|
||||||
|
|
||||||
def train(self, mode=True):
|
|
||||||
super().train(mode)
|
|
||||||
# make sure teacher is in eval mode
|
|
||||||
self.teacher.eval()
|
|
||||||
self.teacher_obs_normalizer.eval()
|
|
||||||
|
|
||||||
def update_normalization(self, obs):
|
|
||||||
if self.student_obs_normalization:
|
|
||||||
student_obs = self.get_student_obs(obs)
|
|
||||||
self.student_obs_normalizer.update(student_obs)
|
|
||||||
|
|
||||||
def load_state_dict(self, state_dict, strict=True):
|
|
||||||
"""Load the parameters of the student and teacher networks.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
state_dict (dict): State dictionary of the model.
|
|
||||||
strict (bool): Whether to strictly enforce that the keys in state_dict match the keys returned by this
|
|
||||||
module's state_dict() function.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: Whether this training resumes a previous training. This flag is used by the `load()` function of
|
|
||||||
`OnPolicyRunner` to determine how to load further parameters.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# check if state_dict contains teacher and student or just teacher parameters
|
|
||||||
if any("actor" in key for key in state_dict.keys()): # loading parameters from rl training
|
|
||||||
# rename keys to match teacher and remove critic parameters
|
|
||||||
teacher_state_dict = {}
|
|
||||||
teacher_obs_normalizer_state_dict = {}
|
|
||||||
for key, value in state_dict.items():
|
|
||||||
if "actor." in key:
|
|
||||||
teacher_state_dict[key.replace("actor.", "")] = value
|
|
||||||
if "actor_obs_normalizer." in key:
|
|
||||||
teacher_obs_normalizer_state_dict[key.replace("actor_obs_normalizer.", "")] = value
|
|
||||||
self.teacher.load_state_dict(teacher_state_dict, strict=strict)
|
|
||||||
self.teacher_obs_normalizer.load_state_dict(teacher_obs_normalizer_state_dict, strict=strict)
|
|
||||||
# also load recurrent memory if teacher is recurrent
|
|
||||||
if self.teacher_recurrent:
|
|
||||||
memory_t_state_dict = {}
|
|
||||||
for key, value in state_dict.items():
|
|
||||||
if "memory_a." in key:
|
|
||||||
memory_t_state_dict[key.replace("memory_a.", "")] = value
|
|
||||||
self.memory_t.load_state_dict(memory_t_state_dict, strict=strict)
|
|
||||||
# set flag for successfully loading the parameters
|
|
||||||
self.loaded_teacher = True
|
|
||||||
self.teacher.eval()
|
|
||||||
self.teacher_obs_normalizer.eval()
|
|
||||||
return False # training does not resume
|
|
||||||
elif any("student" in key for key in state_dict.keys()): # loading parameters from distillation training
|
|
||||||
super().load_state_dict(state_dict, strict=strict)
|
|
||||||
# set flag for successfully loading the parameters
|
|
||||||
self.loaded_teacher = True
|
|
||||||
self.teacher.eval()
|
|
||||||
self.teacher_obs_normalizer.eval()
|
|
||||||
return True # training resumes
|
|
||||||
else:
|
|
||||||
raise ValueError("state_dict does not contain student or teacher parameters")
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_symmetry_config(alg_cfg, env):
|
|
||||||
"""Resolve the symmetry configuration.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
alg_cfg: The algorithm configuration dictionary.
|
|
||||||
env: The environment.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The resolved algorithm configuration dictionary.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# if using symmetry then pass the environment config object
|
|
||||||
if "symmetry_cfg" in alg_cfg and alg_cfg["symmetry_cfg"] is not None:
|
|
||||||
# this is used by the symmetry function for handling different observation terms
|
|
||||||
alg_cfg["symmetry_cfg"]["_env"] = env
|
|
||||||
return alg_cfg
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
"""Definitions for components of modules."""
|
|
||||||
|
|
||||||
from .memory import Memory
|
|
||||||
from .mlp import MLP
|
|
||||||
from .normalization import EmpiricalDiscountedVariationNormalization, EmpiricalNormalization
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch.nn as nn
|
|
||||||
|
|
||||||
from rsl_rl.utils import unpad_trajectories
|
|
||||||
|
|
||||||
|
|
||||||
class Memory(nn.Module):
|
|
||||||
"""Memory module for recurrent networks.
|
|
||||||
|
|
||||||
This module is used to store the hidden states of the policy.
|
|
||||||
Currently only supports GRU and LSTM.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, input_size, type="lstm", num_layers=1, hidden_size=256):
|
|
||||||
super().__init__()
|
|
||||||
# RNN
|
|
||||||
rnn_cls = nn.GRU if type.lower() == "gru" else nn.LSTM
|
|
||||||
self.rnn = rnn_cls(input_size=input_size, hidden_size=hidden_size, num_layers=num_layers)
|
|
||||||
self.hidden_states = None
|
|
||||||
|
|
||||||
def forward(self, input, masks=None, hidden_states=None):
|
|
||||||
batch_mode = masks is not None
|
|
||||||
if batch_mode:
|
|
||||||
# batch mode: needs saved hidden states
|
|
||||||
if hidden_states is None:
|
|
||||||
raise ValueError("Hidden states not passed to memory module during policy update")
|
|
||||||
out, _ = self.rnn(input, hidden_states)
|
|
||||||
out = unpad_trajectories(out, masks)
|
|
||||||
else:
|
|
||||||
# inference/distillation mode: uses hidden states of last step
|
|
||||||
out, self.hidden_states = self.rnn(input.unsqueeze(0), self.hidden_states)
|
|
||||||
return out
|
|
||||||
|
|
||||||
def reset(self, dones=None, hidden_states=None):
|
|
||||||
if dones is None: # reset all hidden states
|
|
||||||
if hidden_states is None:
|
|
||||||
self.hidden_states = None
|
|
||||||
else:
|
|
||||||
self.hidden_states = hidden_states
|
|
||||||
elif self.hidden_states is not None: # reset hidden states of done environments
|
|
||||||
if hidden_states is None:
|
|
||||||
if isinstance(self.hidden_states, tuple): # tuple in case of LSTM
|
|
||||||
for hidden_state in self.hidden_states:
|
|
||||||
hidden_state[..., dones == 1, :] = 0.0
|
|
||||||
else:
|
|
||||||
self.hidden_states[..., dones == 1, :] = 0.0
|
|
||||||
else:
|
|
||||||
NotImplementedError(
|
|
||||||
"Resetting hidden states of done environments with custom hidden states is not implemented"
|
|
||||||
)
|
|
||||||
|
|
||||||
def detach_hidden_states(self, dones=None):
|
|
||||||
if self.hidden_states is not None:
|
|
||||||
if dones is None: # detach all hidden states
|
|
||||||
if isinstance(self.hidden_states, tuple): # tuple in case of LSTM
|
|
||||||
self.hidden_states = tuple(hidden_state.detach() for hidden_state in self.hidden_states)
|
|
||||||
else:
|
|
||||||
self.hidden_states = self.hidden_states.detach()
|
|
||||||
else: # detach hidden states of done environments
|
|
||||||
if isinstance(self.hidden_states, tuple): # tuple in case of LSTM
|
|
||||||
for hidden_state in self.hidden_states:
|
|
||||||
hidden_state[..., dones == 1, :] = hidden_state[..., dones == 1, :].detach()
|
|
||||||
else:
|
|
||||||
self.hidden_states[..., dones == 1, :] = self.hidden_states[..., dones == 1, :].detach()
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
from functools import reduce
|
|
||||||
|
|
||||||
from rsl_rl.utils import resolve_nn_activation
|
|
||||||
|
|
||||||
|
|
||||||
class MLP(nn.Sequential):
|
|
||||||
"""Multi-layer perceptron.
|
|
||||||
|
|
||||||
The MLP network is a sequence of linear layers and activation functions. The
|
|
||||||
last layer is a linear layer that outputs the desired dimension unless the
|
|
||||||
last activation function is specified.
|
|
||||||
|
|
||||||
It provides additional conveniences:
|
|
||||||
|
|
||||||
- If the hidden dimensions have a value of ``-1``, the dimension is inferred
|
|
||||||
from the input dimension.
|
|
||||||
- If the output dimension is a tuple, the output is reshaped to the desired
|
|
||||||
shape.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
input_dim: int,
|
|
||||||
output_dim: int | tuple[int] | list[int],
|
|
||||||
hidden_dims: tuple[int] | list[int],
|
|
||||||
activation: str = "elu",
|
|
||||||
last_activation: str | None = None,
|
|
||||||
):
|
|
||||||
"""Initialize the MLP.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
input_dim: Dimension of the input.
|
|
||||||
output_dim: Dimension of the output.
|
|
||||||
hidden_dims: Dimensions of the hidden layers. A value of ``-1`` indicates
|
|
||||||
that the dimension should be inferred from the input dimension.
|
|
||||||
activation: Activation function. Defaults to "elu".
|
|
||||||
last_activation: Activation function of the last layer. Defaults to None,
|
|
||||||
in which case the last layer is linear.
|
|
||||||
"""
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
# resolve activation functions
|
|
||||||
activation_mod = resolve_nn_activation(activation)
|
|
||||||
last_activation_mod = resolve_nn_activation(last_activation) if last_activation is not None else None
|
|
||||||
# resolve number of hidden dims if they are -1
|
|
||||||
hidden_dims_processed = [input_dim if dim == -1 else dim for dim in hidden_dims]
|
|
||||||
|
|
||||||
# create layers sequentially
|
|
||||||
layers = []
|
|
||||||
layers.append(nn.Linear(input_dim, hidden_dims_processed[0]))
|
|
||||||
layers.append(activation_mod)
|
|
||||||
|
|
||||||
for layer_index in range(len(hidden_dims_processed) - 1):
|
|
||||||
layers.append(nn.Linear(hidden_dims_processed[layer_index], hidden_dims_processed[layer_index + 1]))
|
|
||||||
layers.append(activation_mod)
|
|
||||||
|
|
||||||
# add last layer
|
|
||||||
if isinstance(output_dim, int):
|
|
||||||
layers.append(nn.Linear(hidden_dims_processed[-1], output_dim))
|
|
||||||
else:
|
|
||||||
# compute the total output dimension
|
|
||||||
total_out_dim = reduce(lambda x, y: x * y, output_dim)
|
|
||||||
# add a layer to reshape the output to the desired shape
|
|
||||||
layers.append(nn.Linear(hidden_dims_processed[-1], total_out_dim))
|
|
||||||
layers.append(nn.Unflatten(dim=-1, unflattened_size=output_dim))
|
|
||||||
|
|
||||||
# add last activation function if specified
|
|
||||||
if last_activation_mod is not None:
|
|
||||||
layers.append(last_activation_mod)
|
|
||||||
|
|
||||||
# register the layers
|
|
||||||
for idx, layer in enumerate(layers):
|
|
||||||
self.add_module(f"{idx}", layer)
|
|
||||||
|
|
||||||
def init_weights(self, scales: float | tuple[float]):
|
|
||||||
"""Initialize the weights of the MLP.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
scales: Scale factor for the weights.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_scale(idx) -> float:
|
|
||||||
"""Get the scale factor for the weights of the MLP.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
idx: Index of the layer.
|
|
||||||
"""
|
|
||||||
return scales[idx] if isinstance(scales, (list, tuple)) else scales
|
|
||||||
|
|
||||||
# initialize the weights
|
|
||||||
for idx, module in enumerate(self):
|
|
||||||
if isinstance(module, nn.Linear):
|
|
||||||
nn.init.orthogonal_(module.weight, gain=get_scale(idx))
|
|
||||||
nn.init.zeros_(module.bias)
|
|
||||||
|
|
||||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
||||||
"""Forward pass of the MLP.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
x: Input tensor.
|
|
||||||
"""
|
|
||||||
for layer in self:
|
|
||||||
x = layer(x)
|
|
||||||
return x
|
|
||||||
|
|
||||||
def reset(self, dones=None, hidden_states=None):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def detach_hidden_states(self, dones=None):
|
|
||||||
pass
|
|
||||||
|
|
@ -1,130 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
# Copyright (c) 2020 Preferred Networks, Inc.
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import torch
|
|
||||||
from torch import nn
|
|
||||||
|
|
||||||
|
|
||||||
class EmpiricalNormalization(nn.Module):
|
|
||||||
"""Normalize mean and variance of values based on empirical values."""
|
|
||||||
|
|
||||||
def __init__(self, shape, eps=1e-2, until=None):
|
|
||||||
"""Initialize EmpiricalNormalization module.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
shape (int or tuple of int): Shape of input values except batch axis.
|
|
||||||
eps (float): Small value for stability.
|
|
||||||
until (int or None): If this arg is specified, the module learns input values until the sum of batch sizes
|
|
||||||
exceeds it.
|
|
||||||
|
|
||||||
Note: The normalization parameters are computed over the whole batch, not for each environment separately.
|
|
||||||
"""
|
|
||||||
super().__init__()
|
|
||||||
self.eps = eps
|
|
||||||
self.until = until
|
|
||||||
self.register_buffer("_mean", torch.zeros(shape).unsqueeze(0))
|
|
||||||
self.register_buffer("_var", torch.ones(shape).unsqueeze(0))
|
|
||||||
self.register_buffer("_std", torch.ones(shape).unsqueeze(0))
|
|
||||||
self.register_buffer("count", torch.tensor(0, dtype=torch.long))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def mean(self):
|
|
||||||
return self._mean.squeeze(0).clone()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def std(self):
|
|
||||||
return self._std.squeeze(0).clone()
|
|
||||||
|
|
||||||
def forward(self, x):
|
|
||||||
"""Normalize mean and variance of values based on empirical values."""
|
|
||||||
|
|
||||||
return (x - self._mean) / (self._std + self.eps)
|
|
||||||
|
|
||||||
@torch.jit.unused
|
|
||||||
def update(self, x):
|
|
||||||
"""Learn input values without computing the output values of them"""
|
|
||||||
|
|
||||||
if not self.training:
|
|
||||||
return
|
|
||||||
if self.until is not None and self.count >= self.until:
|
|
||||||
return
|
|
||||||
|
|
||||||
count_x = x.shape[0]
|
|
||||||
self.count += count_x
|
|
||||||
rate = count_x / self.count
|
|
||||||
var_x = torch.var(x, dim=0, unbiased=False, keepdim=True)
|
|
||||||
mean_x = torch.mean(x, dim=0, keepdim=True)
|
|
||||||
delta_mean = mean_x - self._mean
|
|
||||||
self._mean += rate * delta_mean
|
|
||||||
self._var += rate * (var_x - self._var + delta_mean * (mean_x - self._mean))
|
|
||||||
self._std = torch.sqrt(self._var)
|
|
||||||
|
|
||||||
@torch.jit.unused
|
|
||||||
def inverse(self, y):
|
|
||||||
"""De-normalize values based on empirical values."""
|
|
||||||
|
|
||||||
return y * (self._std + self.eps) + self._mean
|
|
||||||
|
|
||||||
|
|
||||||
class EmpiricalDiscountedVariationNormalization(nn.Module):
|
|
||||||
"""Reward normalization from Pathak's large scale study on PPO.
|
|
||||||
|
|
||||||
Reward normalization. Since the reward function is non-stationary, it is useful to normalize
|
|
||||||
the scale of the rewards so that the value function can learn quickly. We did this by dividing
|
|
||||||
the rewards by a running estimate of the standard deviation of the sum of discounted rewards.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, shape, eps=1e-2, gamma=0.99, until=None):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.emp_norm = EmpiricalNormalization(shape, eps, until)
|
|
||||||
self.disc_avg = _DiscountedAverage(gamma)
|
|
||||||
|
|
||||||
def forward(self, rew):
|
|
||||||
if self.training:
|
|
||||||
# update discounted rewards
|
|
||||||
avg = self.disc_avg.update(rew)
|
|
||||||
# update moments from discounted rewards
|
|
||||||
self.emp_norm.update(avg)
|
|
||||||
|
|
||||||
# normalize rewards with the empirical std
|
|
||||||
if self.emp_norm._std > 0:
|
|
||||||
return rew / self.emp_norm._std
|
|
||||||
else:
|
|
||||||
return rew
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
Helper class.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class _DiscountedAverage:
|
|
||||||
r"""Discounted average of rewards.
|
|
||||||
|
|
||||||
The discounted average is defined as:
|
|
||||||
|
|
||||||
.. math::
|
|
||||||
|
|
||||||
\bar{R}_t = \gamma \bar{R}_{t-1} + r_t
|
|
||||||
|
|
||||||
Args:
|
|
||||||
gamma (float): Discount factor.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, gamma):
|
|
||||||
self.avg = None
|
|
||||||
self.gamma = gamma
|
|
||||||
|
|
||||||
def update(self, rew: torch.Tensor) -> torch.Tensor:
|
|
||||||
if self.avg is None:
|
|
||||||
self.avg = rew
|
|
||||||
else:
|
|
||||||
self.avg = self.avg * self.gamma + rew
|
|
||||||
return self.avg
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
"""Implementation of runners for environment-agent interaction."""
|
|
||||||
|
|
||||||
from .on_policy_runner import OnPolicyRunner # isort:skip
|
|
||||||
from .distillation_runner import DistillationRunner
|
|
||||||
from .amp_on_policy_runner import AMPOnPolicyRunner
|
|
||||||
|
|
||||||
__all__ = ["OnPolicyRunner", "DistillationRunner", "AMPOnPolicyRunner"]
|
|
||||||
|
|
@ -1,521 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
import statistics
|
|
||||||
import time
|
|
||||||
import torch
|
|
||||||
import warnings
|
|
||||||
from collections import deque
|
|
||||||
|
|
||||||
import rsl_rl
|
|
||||||
from rsl_rl.algorithms import AMP_PPO
|
|
||||||
from rsl_rl.env import VecEnv
|
|
||||||
from rsl_rl.modules import ActorCritic, ActorCriticRecurrent,DiscriminatorMulti, resolve_rnd_config, resolve_symmetry_config
|
|
||||||
from rsl_rl.utils import resolve_obs_groups, store_code_state, Normalizer, G1_AMPLoader
|
|
||||||
|
|
||||||
|
|
||||||
class AMPOnPolicyRunner:
|
|
||||||
"""On-policy runner for training and evaluation of actor-critic methods."""
|
|
||||||
|
|
||||||
def __init__(self, env: VecEnv, train_cfg: dict, log_dir: str | None = None, device="cpu"):
|
|
||||||
self.cfg = train_cfg
|
|
||||||
self.alg_cfg = train_cfg["algorithm"]
|
|
||||||
self.policy_cfg = train_cfg["policy"]
|
|
||||||
self.device = device
|
|
||||||
self.env = env
|
|
||||||
|
|
||||||
# check if multi-gpu is enabled
|
|
||||||
self._configure_multi_gpu()
|
|
||||||
|
|
||||||
# store training configuration
|
|
||||||
self.num_steps_per_env = self.cfg["num_steps_per_env"]
|
|
||||||
self.save_interval = self.cfg["save_interval"]
|
|
||||||
|
|
||||||
# query observations from environment for algorithm construction
|
|
||||||
obs = self.env.get_observations()
|
|
||||||
default_sets = ["critic"]
|
|
||||||
if "rnd_cfg" in self.alg_cfg and self.alg_cfg["rnd_cfg"] is not None:
|
|
||||||
default_sets.append("rnd_state")
|
|
||||||
self.cfg["obs_groups"] = resolve_obs_groups(obs, self.cfg["obs_groups"], default_sets)
|
|
||||||
|
|
||||||
self.amp_data = G1_AMPLoader(
|
|
||||||
device,
|
|
||||||
time_between_frames=1/50.0,
|
|
||||||
preload_transitions=True,
|
|
||||||
num_preload_transitions=train_cfg["amp_num_preload_transitions"],
|
|
||||||
motion_files=train_cfg["amp_motion_files"],
|
|
||||||
num_frames=train_cfg['amp_num_frames']
|
|
||||||
)
|
|
||||||
|
|
||||||
self.amp_observation_dim = self.amp_data.observation_dim if self.cfg["amp_num_obs"] == 0 else self.cfg["amp_num_obs"]
|
|
||||||
self.amp_num_frames = 0 if self.cfg["amp_num_frames"] == 0 else self.cfg["amp_num_frames"]
|
|
||||||
self.amp_normalizer = Normalizer(self.amp_observation_dim)
|
|
||||||
self.discriminator = DiscriminatorMulti(
|
|
||||||
self.amp_observation_dim,
|
|
||||||
train_cfg["amp_reward_coef"],
|
|
||||||
train_cfg["amp_discr_hidden_dims"],
|
|
||||||
device,
|
|
||||||
train_cfg["amp_num_frames"],
|
|
||||||
train_cfg["amp_task_reward_lerp"],
|
|
||||||
train_cfg['use_lerp'],
|
|
||||||
).to(self.device)
|
|
||||||
|
|
||||||
|
|
||||||
# create the algorithm
|
|
||||||
self.alg = self._construct_algorithm(obs)
|
|
||||||
|
|
||||||
# Decide whether to disable logging
|
|
||||||
# We only log from the process with rank 0 (main process)
|
|
||||||
self.disable_logs = self.is_distributed and self.gpu_global_rank != 0
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
self.log_dir = log_dir
|
|
||||||
self.writer = None
|
|
||||||
self.tot_timesteps = 0
|
|
||||||
self.tot_time = 0
|
|
||||||
self.current_learning_iteration = 0
|
|
||||||
self.git_status_repos = [rsl_rl.__file__]
|
|
||||||
|
|
||||||
def learn(self, num_learning_iterations: int, init_at_random_ep_len: bool = False): # noqa: C901
|
|
||||||
# initialize writer
|
|
||||||
self._prepare_logging_writer()
|
|
||||||
|
|
||||||
# randomize initial episode lengths (for exploration)
|
|
||||||
if init_at_random_ep_len:
|
|
||||||
self.env.episode_length_buf = torch.randint_like(
|
|
||||||
self.env.episode_length_buf, high=int(self.env.max_episode_length)
|
|
||||||
)
|
|
||||||
|
|
||||||
# start learning
|
|
||||||
obs = self.env.get_observations().to(self.device)
|
|
||||||
self.train_mode() # switch to train mode (for dropout for example)
|
|
||||||
amp_obs = self.env.get_amp_observations()
|
|
||||||
amp_obs = amp_obs.to(self.device)
|
|
||||||
if self.amp_num_frames != 0:
|
|
||||||
self.amp_obs_frames = torch.zeros(size=(self.env.num_envs, self.amp_num_frames, self.amp_observation_dim), device=self.device)
|
|
||||||
self.amp_obs_frames = torch.concat((self.amp_obs_frames[:, 1:], amp_obs.unsqueeze(1)), dim=1)
|
|
||||||
|
|
||||||
# Book keeping
|
|
||||||
ep_infos = []
|
|
||||||
rewbuffer = deque(maxlen=100)
|
|
||||||
lenbuffer = deque(maxlen=100)
|
|
||||||
step_discrewbuffer = deque(maxlen=100)
|
|
||||||
|
|
||||||
cur_reward_sum = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
cur_episode_length = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
cur_single_step_disc_rew = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
# create buffers for logging extrinsic and intrinsic rewards
|
|
||||||
if self.alg.rnd:
|
|
||||||
erewbuffer = deque(maxlen=100)
|
|
||||||
irewbuffer = deque(maxlen=100)
|
|
||||||
cur_ereward_sum = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
cur_ireward_sum = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
|
|
||||||
# Ensure all parameters are in-synced
|
|
||||||
if self.is_distributed:
|
|
||||||
print(f"Synchronizing parameters for rank {self.gpu_global_rank}...")
|
|
||||||
self.alg.broadcast_parameters()
|
|
||||||
|
|
||||||
# Start training
|
|
||||||
start_iter = self.current_learning_iteration
|
|
||||||
tot_iter = start_iter + num_learning_iterations
|
|
||||||
for it in range(start_iter, tot_iter):
|
|
||||||
start = time.time()
|
|
||||||
# Rollout
|
|
||||||
with torch.inference_mode():
|
|
||||||
for _ in range(self.num_steps_per_env):
|
|
||||||
# Sample actions
|
|
||||||
actions = self.alg.act(obs,amp_obs)
|
|
||||||
# Step the environment
|
|
||||||
obs, rewards, dones, extras = self.env.step(actions.to(self.env.device))
|
|
||||||
# Move to device
|
|
||||||
obs, rewards, dones = (obs.to(self.device), rewards.to(self.device), dones.to(self.device))
|
|
||||||
|
|
||||||
next_amp_obs = self.env.get_amp_observations()
|
|
||||||
next_amp_obs = next_amp_obs.to(self.device)
|
|
||||||
next_amp_obs_with_term = torch.clone(next_amp_obs)
|
|
||||||
|
|
||||||
reset_env_ids = self.env.reset_env_ids
|
|
||||||
terminal_amp_states = self.env.get_amp_observations()[reset_env_ids]
|
|
||||||
next_amp_obs_with_term[reset_env_ids] = terminal_amp_states
|
|
||||||
self.amp_obs_frames = torch.concat((self.amp_obs_frames[:, 1:], next_amp_obs_with_term.unsqueeze(1)), dim=1)
|
|
||||||
|
|
||||||
amp_reward = torch.zeros(self.env.num_envs, device=obs.device)
|
|
||||||
|
|
||||||
mask = self.env.contact_phase[:, 0] == 1.0
|
|
||||||
if mask.any():
|
|
||||||
rewards[mask], logit, disc_reward = self.alg.discriminator.predict_amp_reward(
|
|
||||||
self.amp_obs_frames[mask], rewards[mask], normalizer=self.alg.amp_normalizer
|
|
||||||
)
|
|
||||||
amp_reward[mask] += disc_reward
|
|
||||||
|
|
||||||
# process the step
|
|
||||||
self.alg.process_env_step(obs, rewards, dones, extras, next_amp_obs_with_term, self.amp_obs_frames)
|
|
||||||
self.amp_obs_frames[reset_env_ids] = 0
|
|
||||||
|
|
||||||
amp_obs = torch.clone(next_amp_obs)
|
|
||||||
# Extract intrinsic rewards (only for logging)
|
|
||||||
intrinsic_rewards = self.alg.intrinsic_rewards if self.alg.rnd else None
|
|
||||||
# book keeping
|
|
||||||
if self.log_dir is not None:
|
|
||||||
if "episode" in extras:
|
|
||||||
ep_infos.append(extras["episode"])
|
|
||||||
elif "log" in extras:
|
|
||||||
ep_infos.append(extras["log"])
|
|
||||||
# Update rewards
|
|
||||||
if self.alg.rnd:
|
|
||||||
cur_ereward_sum += rewards
|
|
||||||
cur_ireward_sum += intrinsic_rewards # type: ignore
|
|
||||||
cur_reward_sum += rewards + intrinsic_rewards
|
|
||||||
else:
|
|
||||||
cur_reward_sum += rewards
|
|
||||||
# Update episode length
|
|
||||||
cur_episode_length += 1
|
|
||||||
cur_single_step_disc_rew += amp_reward
|
|
||||||
# Clear data for completed episodes
|
|
||||||
# -- common
|
|
||||||
new_ids = (dones > 0).nonzero(as_tuple=False)
|
|
||||||
rewbuffer.extend(cur_reward_sum[new_ids][:, 0].cpu().numpy().tolist())
|
|
||||||
lenbuffer.extend(cur_episode_length[new_ids][:, 0].cpu().numpy().tolist())
|
|
||||||
cur_reward_sum[new_ids] = 0
|
|
||||||
cur_episode_length[new_ids] = 0
|
|
||||||
to_extend_disc = (cur_single_step_disc_rew[new_ids] / self.env.max_episode_length_s)[:, 0].cpu().numpy()
|
|
||||||
step_discrewbuffer.extend(to_extend_disc.tolist())
|
|
||||||
cur_single_step_disc_rew[new_ids] = 0
|
|
||||||
# -- intrinsic and extrinsic rewards
|
|
||||||
if self.alg.rnd:
|
|
||||||
erewbuffer.extend(cur_ereward_sum[new_ids][:, 0].cpu().numpy().tolist())
|
|
||||||
irewbuffer.extend(cur_ireward_sum[new_ids][:, 0].cpu().numpy().tolist())
|
|
||||||
cur_ereward_sum[new_ids] = 0
|
|
||||||
cur_ireward_sum[new_ids] = 0
|
|
||||||
|
|
||||||
stop = time.time()
|
|
||||||
collection_time = stop - start
|
|
||||||
start = stop
|
|
||||||
|
|
||||||
# compute returns
|
|
||||||
self.alg.compute_returns(obs)
|
|
||||||
|
|
||||||
# update policy
|
|
||||||
loss_dict = self.alg.update()
|
|
||||||
|
|
||||||
stop = time.time()
|
|
||||||
learn_time = stop - start
|
|
||||||
self.current_learning_iteration = it
|
|
||||||
# log info
|
|
||||||
if self.log_dir is not None and not self.disable_logs:
|
|
||||||
# Log information
|
|
||||||
self.log(locals())
|
|
||||||
# Save model
|
|
||||||
if it % self.save_interval == 0:
|
|
||||||
self.save(os.path.join(self.log_dir, f"model_{it}.pt"))
|
|
||||||
|
|
||||||
# Clear episode infos
|
|
||||||
ep_infos.clear()
|
|
||||||
# Save code state
|
|
||||||
if it == start_iter and not self.disable_logs:
|
|
||||||
# obtain all the diff files
|
|
||||||
git_file_paths = store_code_state(self.log_dir, self.git_status_repos)
|
|
||||||
# if possible store them to wandb
|
|
||||||
if self.logger_type in ["wandb", "neptune"] and git_file_paths:
|
|
||||||
for path in git_file_paths:
|
|
||||||
self.writer.save_file(path)
|
|
||||||
|
|
||||||
# Save the final model after training
|
|
||||||
if self.log_dir is not None and not self.disable_logs:
|
|
||||||
self.save(os.path.join(self.log_dir, f"model_{self.current_learning_iteration}.pt"))
|
|
||||||
|
|
||||||
def log(self, locs: dict, width: int = 80, pad: int = 35):
|
|
||||||
# Compute the collection size
|
|
||||||
collection_size = self.num_steps_per_env * self.env.num_envs * self.gpu_world_size
|
|
||||||
# Update total time-steps and time
|
|
||||||
self.tot_timesteps += collection_size
|
|
||||||
self.tot_time += locs["collection_time"] + locs["learn_time"]
|
|
||||||
iteration_time = locs["collection_time"] + locs["learn_time"]
|
|
||||||
|
|
||||||
# -- Episode info
|
|
||||||
ep_string = ""
|
|
||||||
if locs["ep_infos"]:
|
|
||||||
for key in locs["ep_infos"][0]:
|
|
||||||
infotensor = torch.tensor([], device=self.device)
|
|
||||||
for ep_info in locs["ep_infos"]:
|
|
||||||
# handle scalar and zero dimensional tensor infos
|
|
||||||
if key not in ep_info:
|
|
||||||
continue
|
|
||||||
if not isinstance(ep_info[key], torch.Tensor):
|
|
||||||
ep_info[key] = torch.Tensor([ep_info[key]])
|
|
||||||
if len(ep_info[key].shape) == 0:
|
|
||||||
ep_info[key] = ep_info[key].unsqueeze(0)
|
|
||||||
infotensor = torch.cat((infotensor, ep_info[key].to(self.device)))
|
|
||||||
value = torch.mean(infotensor)
|
|
||||||
# log to logger and terminal
|
|
||||||
if "/" in key:
|
|
||||||
self.writer.add_scalar(key, value, locs["it"])
|
|
||||||
ep_string += f"""{f'{key}:':>{pad}} {value:.4f}\n"""
|
|
||||||
else:
|
|
||||||
self.writer.add_scalar("Episode/" + key, value, locs["it"])
|
|
||||||
ep_string += f"""{f'Mean episode {key}:':>{pad}} {value:.4f}\n"""
|
|
||||||
|
|
||||||
mean_std = self.alg.policy.action_std.mean()
|
|
||||||
fps = int(collection_size / (locs["collection_time"] + locs["learn_time"]))
|
|
||||||
|
|
||||||
# -- Losses
|
|
||||||
for key, value in locs["loss_dict"].items():
|
|
||||||
self.writer.add_scalar(f"Loss/{key}", value, locs["it"])
|
|
||||||
self.writer.add_scalar("Loss/learning_rate", self.alg.learning_rate, locs["it"])
|
|
||||||
|
|
||||||
# -- Policy
|
|
||||||
self.writer.add_scalar("Policy/mean_noise_std", mean_std.item(), locs["it"])
|
|
||||||
|
|
||||||
# -- Performance
|
|
||||||
self.writer.add_scalar("Perf/total_fps", fps, locs["it"])
|
|
||||||
self.writer.add_scalar("Perf/collection time", locs["collection_time"], locs["it"])
|
|
||||||
self.writer.add_scalar("Perf/learning_time", locs["learn_time"], locs["it"])
|
|
||||||
|
|
||||||
# -- Training
|
|
||||||
if len(locs["rewbuffer"]) > 0:
|
|
||||||
# separate logging for intrinsic and extrinsic rewards
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
self.writer.add_scalar("Rnd/mean_extrinsic_reward", statistics.mean(locs["erewbuffer"]), locs["it"])
|
|
||||||
self.writer.add_scalar("Rnd/mean_intrinsic_reward", statistics.mean(locs["irewbuffer"]), locs["it"])
|
|
||||||
self.writer.add_scalar("Rnd/weight", self.alg.rnd.weight, locs["it"])
|
|
||||||
# everything else
|
|
||||||
self.writer.add_scalar("Train/mean_reward", statistics.mean(locs["rewbuffer"]), locs["it"])
|
|
||||||
self.writer.add_scalar("Train/mean_episode_length", statistics.mean(locs["lenbuffer"]), locs["it"])
|
|
||||||
self.writer.add_scalar('Train/mean_step_disc_reward', statistics.mean(locs['step_discrewbuffer']), locs['it'])
|
|
||||||
if self.logger_type != "wandb": # wandb does not support non-integer x-axis logging
|
|
||||||
self.writer.add_scalar("Train/mean_reward/time", statistics.mean(locs["rewbuffer"]), self.tot_time)
|
|
||||||
self.writer.add_scalar(
|
|
||||||
"Train/mean_episode_length/time", statistics.mean(locs["lenbuffer"]), self.tot_time
|
|
||||||
)
|
|
||||||
|
|
||||||
str = f" \033[1m Learning iteration {locs['it']}/{locs['tot_iter']} \033[0m "
|
|
||||||
|
|
||||||
if len(locs["rewbuffer"]) > 0:
|
|
||||||
log_string = (
|
|
||||||
f"""{'#' * width}\n"""
|
|
||||||
f"""{str.center(width, ' ')}\n\n"""
|
|
||||||
f"""{'Computation:':>{pad}} {fps:.0f} steps/s (collection: {locs[
|
|
||||||
'collection_time']:.3f}s, learning {locs['learn_time']:.3f}s)\n"""
|
|
||||||
f"""{'Mean action noise std:':>{pad}} {mean_std.item():.2f}\n"""
|
|
||||||
f"""{'Step disc reward:':>{pad}} {statistics.mean(locs['step_discrewbuffer']):.2f}\n"""
|
|
||||||
)
|
|
||||||
# -- Losses
|
|
||||||
for key, value in locs["loss_dict"].items():
|
|
||||||
log_string += f"""{f'Mean {key} loss:':>{pad}} {value:.4f}\n"""
|
|
||||||
# -- Rewards
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
log_string += (
|
|
||||||
f"""{'Mean extrinsic reward:':>{pad}} {statistics.mean(locs['erewbuffer']):.2f}\n"""
|
|
||||||
f"""{'Mean intrinsic reward:':>{pad}} {statistics.mean(locs['irewbuffer']):.2f}\n"""
|
|
||||||
)
|
|
||||||
log_string += f"""{'Mean reward:':>{pad}} {statistics.mean(locs['rewbuffer']):.2f}\n"""
|
|
||||||
# -- episode info
|
|
||||||
log_string += f"""{'Mean episode length:':>{pad}} {statistics.mean(locs['lenbuffer']):.2f}\n"""
|
|
||||||
else:
|
|
||||||
log_string = (
|
|
||||||
f"""{'#' * width}\n"""
|
|
||||||
f"""{str.center(width, ' ')}\n\n"""
|
|
||||||
f"""{'Computation:':>{pad}} {fps:.0f} steps/s (collection: {locs[
|
|
||||||
'collection_time']:.3f}s, learning {locs['learn_time']:.3f}s)\n"""
|
|
||||||
f"""{'Mean action noise std:':>{pad}} {mean_std.item():.2f}\n"""
|
|
||||||
)
|
|
||||||
for key, value in locs["loss_dict"].items():
|
|
||||||
log_string += f"""{f'{key}:':>{pad}} {value:.4f}\n"""
|
|
||||||
|
|
||||||
log_string += ep_string
|
|
||||||
log_string += (
|
|
||||||
f"""{'-' * width}\n"""
|
|
||||||
f"""{'Total timesteps:':>{pad}} {self.tot_timesteps}\n"""
|
|
||||||
f"""{'Iteration time:':>{pad}} {iteration_time:.2f}s\n"""
|
|
||||||
f"""{'Time elapsed:':>{pad}} {time.strftime("%H:%M:%S", time.gmtime(self.tot_time))}\n"""
|
|
||||||
f"""{'ETA:':>{pad}} {time.strftime(
|
|
||||||
"%H:%M:%S",
|
|
||||||
time.gmtime(
|
|
||||||
self.tot_time / (locs['it'] - locs['start_iter'] + 1)
|
|
||||||
* (locs['start_iter'] + locs['num_learning_iterations'] - locs['it'])
|
|
||||||
)
|
|
||||||
)}\n"""
|
|
||||||
)
|
|
||||||
print(log_string)
|
|
||||||
|
|
||||||
def save(self, path: str, infos=None):
|
|
||||||
# -- Save model
|
|
||||||
saved_dict = {
|
|
||||||
"model_state_dict": self.alg.policy.state_dict(),
|
|
||||||
"optimizer_state_dict": self.alg.optimizer.state_dict(),
|
|
||||||
"iter": self.current_learning_iteration,
|
|
||||||
"infos": infos,
|
|
||||||
}
|
|
||||||
# -- Save RND model if used
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
saved_dict["rnd_state_dict"] = self.alg.rnd.state_dict()
|
|
||||||
saved_dict["rnd_optimizer_state_dict"] = self.alg.rnd_optimizer.state_dict()
|
|
||||||
torch.save(saved_dict, path)
|
|
||||||
|
|
||||||
# upload model to external logging service
|
|
||||||
if self.logger_type in ["neptune", "wandb"] and not self.disable_logs:
|
|
||||||
self.writer.save_model(path, self.current_learning_iteration)
|
|
||||||
|
|
||||||
def load(self, path: str, load_optimizer: bool = True, map_location: str | None = None):
|
|
||||||
loaded_dict = torch.load(path, weights_only=False, map_location=map_location)
|
|
||||||
# -- Load model
|
|
||||||
resumed_training = self.alg.policy.load_state_dict(loaded_dict["model_state_dict"])
|
|
||||||
# -- Load RND model if used
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
self.alg.rnd.load_state_dict(loaded_dict["rnd_state_dict"])
|
|
||||||
# -- load optimizer if used
|
|
||||||
if load_optimizer and resumed_training:
|
|
||||||
# -- algorithm optimizer
|
|
||||||
self.alg.optimizer.load_state_dict(loaded_dict["optimizer_state_dict"])
|
|
||||||
# -- RND optimizer if used
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
self.alg.rnd_optimizer.load_state_dict(loaded_dict["rnd_optimizer_state_dict"])
|
|
||||||
# -- load current learning iteration
|
|
||||||
if resumed_training:
|
|
||||||
self.current_learning_iteration = loaded_dict["iter"]
|
|
||||||
return loaded_dict["infos"]
|
|
||||||
|
|
||||||
def get_inference_policy(self, device=None):
|
|
||||||
self.eval_mode() # switch to evaluation mode (dropout for example)
|
|
||||||
if device is not None:
|
|
||||||
self.alg.policy.to(device)
|
|
||||||
return self.alg.policy.act_inference
|
|
||||||
|
|
||||||
def train_mode(self):
|
|
||||||
# -- PPO
|
|
||||||
self.alg.policy.train()
|
|
||||||
self.alg.discriminator.train()
|
|
||||||
# -- RND
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
self.alg.rnd.train()
|
|
||||||
|
|
||||||
def eval_mode(self):
|
|
||||||
# -- PPO
|
|
||||||
self.alg.policy.eval()
|
|
||||||
self.alg.discriminator.eval()
|
|
||||||
# -- RND
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
self.alg.rnd.eval()
|
|
||||||
|
|
||||||
def add_git_repo_to_log(self, repo_file_path):
|
|
||||||
self.git_status_repos.append(repo_file_path)
|
|
||||||
|
|
||||||
"""
|
|
||||||
Helper functions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _configure_multi_gpu(self):
|
|
||||||
"""Configure multi-gpu training."""
|
|
||||||
# check if distributed training is enabled
|
|
||||||
self.gpu_world_size = int(os.getenv("WORLD_SIZE", "1"))
|
|
||||||
self.is_distributed = self.gpu_world_size > 1
|
|
||||||
|
|
||||||
# if not distributed training, set local and global rank to 0 and return
|
|
||||||
if not self.is_distributed:
|
|
||||||
self.gpu_local_rank = 0
|
|
||||||
self.gpu_global_rank = 0
|
|
||||||
self.multi_gpu_cfg = None
|
|
||||||
return
|
|
||||||
|
|
||||||
# get rank and world size
|
|
||||||
self.gpu_local_rank = int(os.getenv("LOCAL_RANK", "0"))
|
|
||||||
self.gpu_global_rank = int(os.getenv("RANK", "0"))
|
|
||||||
|
|
||||||
# make a configuration dictionary
|
|
||||||
self.multi_gpu_cfg = {
|
|
||||||
"global_rank": self.gpu_global_rank, # rank of the main process
|
|
||||||
"local_rank": self.gpu_local_rank, # rank of the current process
|
|
||||||
"world_size": self.gpu_world_size, # total number of processes
|
|
||||||
}
|
|
||||||
|
|
||||||
# check if user has device specified for local rank
|
|
||||||
if self.device != f"cuda:{self.gpu_local_rank}":
|
|
||||||
raise ValueError(
|
|
||||||
f"Device '{self.device}' does not match expected device for local rank '{self.gpu_local_rank}'."
|
|
||||||
)
|
|
||||||
# validate multi-gpu configuration
|
|
||||||
if self.gpu_local_rank >= self.gpu_world_size:
|
|
||||||
raise ValueError(
|
|
||||||
f"Local rank '{self.gpu_local_rank}' is greater than or equal to world size '{self.gpu_world_size}'."
|
|
||||||
)
|
|
||||||
if self.gpu_global_rank >= self.gpu_world_size:
|
|
||||||
raise ValueError(
|
|
||||||
f"Global rank '{self.gpu_global_rank}' is greater than or equal to world size '{self.gpu_world_size}'."
|
|
||||||
)
|
|
||||||
|
|
||||||
# initialize torch distributed
|
|
||||||
torch.distributed.init_process_group(backend="nccl", rank=self.gpu_global_rank, world_size=self.gpu_world_size)
|
|
||||||
# set device to the local rank
|
|
||||||
torch.cuda.set_device(self.gpu_local_rank)
|
|
||||||
|
|
||||||
def _construct_algorithm(self, obs) -> AMP_PPO:
|
|
||||||
"""Construct the actor-critic algorithm."""
|
|
||||||
# resolve RND config
|
|
||||||
self.alg_cfg = resolve_rnd_config(self.alg_cfg, obs, self.cfg["obs_groups"], self.env)
|
|
||||||
|
|
||||||
# resolve symmetry config
|
|
||||||
self.alg_cfg = resolve_symmetry_config(self.alg_cfg, self.env)
|
|
||||||
|
|
||||||
# resolve deprecated normalization config
|
|
||||||
if self.cfg.get("empirical_normalization") is not None:
|
|
||||||
warnings.warn(
|
|
||||||
"The `empirical_normalization` parameter is deprecated. Please set `actor_obs_normalization` and "
|
|
||||||
"`critic_obs_normalization` as part of the `policy` configuration instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
if self.policy_cfg.get("actor_obs_normalization") is None:
|
|
||||||
self.policy_cfg["actor_obs_normalization"] = self.cfg["empirical_normalization"]
|
|
||||||
if self.policy_cfg.get("critic_obs_normalization") is None:
|
|
||||||
self.policy_cfg["critic_obs_normalization"] = self.cfg["empirical_normalization"]
|
|
||||||
|
|
||||||
# initialize the actor-critic
|
|
||||||
actor_critic_class = eval(self.policy_cfg.pop("class_name"))
|
|
||||||
actor_critic: ActorCritic | ActorCriticRecurrent = actor_critic_class(
|
|
||||||
obs, self.cfg["obs_groups"], self.env.num_actions, **self.policy_cfg
|
|
||||||
).to(self.device)
|
|
||||||
|
|
||||||
# initialize the algorithm
|
|
||||||
alg_class = eval(self.alg_cfg.pop("class_name"))
|
|
||||||
|
|
||||||
alg: AMP_PPO = alg_class(actor_critic, self.discriminator, self.amp_data, self.amp_normalizer, self.amp_num_frames, device=self.device, **self.alg_cfg, multi_gpu_cfg=self.multi_gpu_cfg)
|
|
||||||
|
|
||||||
# initialize the storage
|
|
||||||
alg.init_storage(
|
|
||||||
"rl",
|
|
||||||
self.env.num_envs,
|
|
||||||
self.num_steps_per_env,
|
|
||||||
obs,
|
|
||||||
[self.env.num_actions],
|
|
||||||
)
|
|
||||||
|
|
||||||
return alg
|
|
||||||
|
|
||||||
def _prepare_logging_writer(self):
|
|
||||||
"""Prepares the logging writers."""
|
|
||||||
if self.log_dir is not None and self.writer is None and not self.disable_logs:
|
|
||||||
# Launch either Tensorboard or Neptune & Tensorboard summary writer(s), default: Tensorboard.
|
|
||||||
self.logger_type = self.cfg.get("logger", "tensorboard")
|
|
||||||
self.logger_type = self.logger_type.lower()
|
|
||||||
|
|
||||||
if self.logger_type == "neptune":
|
|
||||||
from rsl_rl.utils.neptune_utils import NeptuneSummaryWriter
|
|
||||||
|
|
||||||
self.writer = NeptuneSummaryWriter(log_dir=self.log_dir, flush_secs=10, cfg=self.cfg)
|
|
||||||
self.writer.log_config(self.env.cfg, self.cfg, self.alg_cfg, self.policy_cfg)
|
|
||||||
elif self.logger_type == "wandb":
|
|
||||||
from rsl_rl.utils.wandb_utils import WandbSummaryWriter
|
|
||||||
|
|
||||||
self.writer = WandbSummaryWriter(log_dir=self.log_dir, flush_secs=10, cfg=self.cfg)
|
|
||||||
self.writer.log_config(self.env.cfg, self.cfg, self.alg_cfg, self.policy_cfg)
|
|
||||||
elif self.logger_type == "tensorboard":
|
|
||||||
from torch.utils.tensorboard import SummaryWriter
|
|
||||||
|
|
||||||
self.writer = SummaryWriter(log_dir=self.log_dir, flush_secs=10)
|
|
||||||
else:
|
|
||||||
raise ValueError("Logger type not found. Please choose 'neptune', 'wandb' or 'tensorboard'.")
|
|
||||||
|
|
@ -1,179 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import torch
|
|
||||||
from collections import deque
|
|
||||||
|
|
||||||
import rsl_rl
|
|
||||||
from rsl_rl.algorithms import Distillation
|
|
||||||
from rsl_rl.env import VecEnv
|
|
||||||
from rsl_rl.modules import StudentTeacher, StudentTeacherRecurrent
|
|
||||||
from rsl_rl.runners import OnPolicyRunner
|
|
||||||
from rsl_rl.utils import resolve_obs_groups, store_code_state
|
|
||||||
|
|
||||||
|
|
||||||
class DistillationRunner(OnPolicyRunner):
|
|
||||||
"""On-policy runner for training and evaluation of teacher-student training."""
|
|
||||||
|
|
||||||
def __init__(self, env: VecEnv, train_cfg: dict, log_dir: str | None = None, device="cpu"):
|
|
||||||
self.cfg = train_cfg
|
|
||||||
self.alg_cfg = train_cfg["algorithm"]
|
|
||||||
self.policy_cfg = train_cfg["policy"]
|
|
||||||
self.device = device
|
|
||||||
self.env = env
|
|
||||||
|
|
||||||
# check if multi-gpu is enabled
|
|
||||||
self._configure_multi_gpu()
|
|
||||||
|
|
||||||
# store training configuration
|
|
||||||
self.num_steps_per_env = self.cfg["num_steps_per_env"]
|
|
||||||
self.save_interval = self.cfg["save_interval"]
|
|
||||||
|
|
||||||
# query observations from environment for algorithm construction
|
|
||||||
obs = self.env.get_observations()
|
|
||||||
self.cfg["obs_groups"] = resolve_obs_groups(obs, self.cfg["obs_groups"], default_sets=["teacher"])
|
|
||||||
|
|
||||||
# create the algorithm
|
|
||||||
self.alg = self._construct_algorithm(obs)
|
|
||||||
|
|
||||||
# Decide whether to disable logging
|
|
||||||
# We only log from the process with rank 0 (main process)
|
|
||||||
self.disable_logs = self.is_distributed and self.gpu_global_rank != 0
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
self.log_dir = log_dir
|
|
||||||
self.writer = None
|
|
||||||
self.tot_timesteps = 0
|
|
||||||
self.tot_time = 0
|
|
||||||
self.current_learning_iteration = 0
|
|
||||||
self.git_status_repos = [rsl_rl.__file__]
|
|
||||||
|
|
||||||
def learn(self, num_learning_iterations: int, init_at_random_ep_len: bool = False): # noqa: C901
|
|
||||||
# initialize writer
|
|
||||||
self._prepare_logging_writer()
|
|
||||||
# check if teacher is loaded
|
|
||||||
if not self.alg.policy.loaded_teacher:
|
|
||||||
raise ValueError("Teacher model parameters not loaded. Please load a teacher model to distill.")
|
|
||||||
|
|
||||||
# randomize initial episode lengths (for exploration)
|
|
||||||
if init_at_random_ep_len:
|
|
||||||
self.env.episode_length_buf = torch.randint_like(
|
|
||||||
self.env.episode_length_buf, high=int(self.env.max_episode_length)
|
|
||||||
)
|
|
||||||
|
|
||||||
# start learning
|
|
||||||
obs = self.env.get_observations().to(self.device)
|
|
||||||
self.train_mode() # switch to train mode (for dropout for example)
|
|
||||||
|
|
||||||
# Book keeping
|
|
||||||
ep_infos = []
|
|
||||||
rewbuffer = deque(maxlen=100)
|
|
||||||
lenbuffer = deque(maxlen=100)
|
|
||||||
cur_reward_sum = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
cur_episode_length = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
|
|
||||||
# Ensure all parameters are in-synced
|
|
||||||
if self.is_distributed:
|
|
||||||
print(f"Synchronizing parameters for rank {self.gpu_global_rank}...")
|
|
||||||
self.alg.broadcast_parameters()
|
|
||||||
|
|
||||||
# Start training
|
|
||||||
start_iter = self.current_learning_iteration
|
|
||||||
tot_iter = start_iter + num_learning_iterations
|
|
||||||
for it in range(start_iter, tot_iter):
|
|
||||||
start = time.time()
|
|
||||||
# Rollout
|
|
||||||
with torch.inference_mode():
|
|
||||||
for _ in range(self.num_steps_per_env):
|
|
||||||
# Sample actions
|
|
||||||
actions = self.alg.act(obs)
|
|
||||||
# Step the environment
|
|
||||||
obs, rewards, dones, extras = self.env.step(actions.to(self.env.device))
|
|
||||||
# Move to device
|
|
||||||
obs, rewards, dones = (obs.to(self.device), rewards.to(self.device), dones.to(self.device))
|
|
||||||
# process the step
|
|
||||||
self.alg.process_env_step(obs, rewards, dones, extras)
|
|
||||||
# book keeping
|
|
||||||
if self.log_dir is not None:
|
|
||||||
if "episode" in extras:
|
|
||||||
ep_infos.append(extras["episode"])
|
|
||||||
elif "log" in extras:
|
|
||||||
ep_infos.append(extras["log"])
|
|
||||||
# Update rewards
|
|
||||||
cur_reward_sum += rewards
|
|
||||||
# Update episode length
|
|
||||||
cur_episode_length += 1
|
|
||||||
# Clear data for completed episodes
|
|
||||||
new_ids = (dones > 0).nonzero(as_tuple=False)
|
|
||||||
rewbuffer.extend(cur_reward_sum[new_ids][:, 0].cpu().numpy().tolist())
|
|
||||||
lenbuffer.extend(cur_episode_length[new_ids][:, 0].cpu().numpy().tolist())
|
|
||||||
cur_reward_sum[new_ids] = 0
|
|
||||||
cur_episode_length[new_ids] = 0
|
|
||||||
|
|
||||||
stop = time.time()
|
|
||||||
collection_time = stop - start
|
|
||||||
start = stop
|
|
||||||
|
|
||||||
# update policy
|
|
||||||
loss_dict = self.alg.update()
|
|
||||||
|
|
||||||
stop = time.time()
|
|
||||||
learn_time = stop - start
|
|
||||||
self.current_learning_iteration = it
|
|
||||||
# log info
|
|
||||||
if self.log_dir is not None and not self.disable_logs:
|
|
||||||
# Log information
|
|
||||||
self.log(locals())
|
|
||||||
# Save model
|
|
||||||
if it % self.save_interval == 0:
|
|
||||||
self.save(os.path.join(self.log_dir, f"model_{it}.pt"))
|
|
||||||
|
|
||||||
# Clear episode infos
|
|
||||||
ep_infos.clear()
|
|
||||||
# Save code state
|
|
||||||
if it == start_iter and not self.disable_logs:
|
|
||||||
# obtain all the diff files
|
|
||||||
git_file_paths = store_code_state(self.log_dir, self.git_status_repos)
|
|
||||||
# if possible store them to wandb
|
|
||||||
if self.logger_type in ["wandb", "neptune"] and git_file_paths:
|
|
||||||
for path in git_file_paths:
|
|
||||||
self.writer.save_file(path)
|
|
||||||
|
|
||||||
# Save the final model after training
|
|
||||||
if self.log_dir is not None and not self.disable_logs:
|
|
||||||
self.save(os.path.join(self.log_dir, f"model_{self.current_learning_iteration}.pt"))
|
|
||||||
|
|
||||||
"""
|
|
||||||
Helper methods.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _construct_algorithm(self, obs) -> Distillation:
|
|
||||||
"""Construct the distillation algorithm."""
|
|
||||||
# initialize the actor-critic
|
|
||||||
student_teacher_class = eval(self.policy_cfg.pop("class_name"))
|
|
||||||
student_teacher: StudentTeacher | StudentTeacherRecurrent = student_teacher_class(
|
|
||||||
obs, self.cfg["obs_groups"], self.env.num_actions, **self.policy_cfg
|
|
||||||
).to(self.device)
|
|
||||||
|
|
||||||
# initialize the algorithm
|
|
||||||
alg_class = eval(self.alg_cfg.pop("class_name"))
|
|
||||||
alg: Distillation = alg_class(
|
|
||||||
student_teacher, device=self.device, **self.alg_cfg, multi_gpu_cfg=self.multi_gpu_cfg
|
|
||||||
)
|
|
||||||
|
|
||||||
# initialize the storage
|
|
||||||
alg.init_storage(
|
|
||||||
"distillation",
|
|
||||||
self.env.num_envs,
|
|
||||||
self.num_steps_per_env,
|
|
||||||
obs,
|
|
||||||
[self.env.num_actions],
|
|
||||||
)
|
|
||||||
|
|
||||||
return alg
|
|
||||||
|
|
@ -1,460 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
import statistics
|
|
||||||
import time
|
|
||||||
import torch
|
|
||||||
import warnings
|
|
||||||
from collections import deque
|
|
||||||
|
|
||||||
import rsl_rl
|
|
||||||
from rsl_rl.algorithms import PPO
|
|
||||||
from rsl_rl.env import VecEnv
|
|
||||||
from rsl_rl.modules import ActorCritic, ActorCriticRecurrent, resolve_rnd_config, resolve_symmetry_config
|
|
||||||
from rsl_rl.utils import resolve_obs_groups, store_code_state
|
|
||||||
|
|
||||||
|
|
||||||
class OnPolicyRunner:
|
|
||||||
"""On-policy runner for training and evaluation of actor-critic methods."""
|
|
||||||
|
|
||||||
def __init__(self, env: VecEnv, train_cfg: dict, log_dir: str | None = None, device="cpu"):
|
|
||||||
self.cfg = train_cfg
|
|
||||||
self.alg_cfg = train_cfg["algorithm"]
|
|
||||||
self.policy_cfg = train_cfg["policy"]
|
|
||||||
self.device = device
|
|
||||||
self.env = env
|
|
||||||
|
|
||||||
# check if multi-gpu is enabled
|
|
||||||
self._configure_multi_gpu()
|
|
||||||
|
|
||||||
# store training configuration
|
|
||||||
self.num_steps_per_env = self.cfg["num_steps_per_env"]
|
|
||||||
self.save_interval = self.cfg["save_interval"]
|
|
||||||
|
|
||||||
# query observations from environment for algorithm construction
|
|
||||||
obs = self.env.get_observations()
|
|
||||||
default_sets = ["critic"]
|
|
||||||
if "rnd_cfg" in self.alg_cfg and self.alg_cfg["rnd_cfg"] is not None:
|
|
||||||
default_sets.append("rnd_state")
|
|
||||||
self.cfg["obs_groups"] = resolve_obs_groups(obs, self.cfg["obs_groups"], default_sets)
|
|
||||||
|
|
||||||
# create the algorithm
|
|
||||||
self.alg = self._construct_algorithm(obs)
|
|
||||||
|
|
||||||
# Decide whether to disable logging
|
|
||||||
# We only log from the process with rank 0 (main process)
|
|
||||||
self.disable_logs = self.is_distributed and self.gpu_global_rank != 0
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
self.log_dir = log_dir
|
|
||||||
self.writer = None
|
|
||||||
self.tot_timesteps = 0
|
|
||||||
self.tot_time = 0
|
|
||||||
self.current_learning_iteration = 0
|
|
||||||
self.git_status_repos = [rsl_rl.__file__]
|
|
||||||
|
|
||||||
def learn(self, num_learning_iterations: int, init_at_random_ep_len: bool = False): # noqa: C901
|
|
||||||
# initialize writer
|
|
||||||
self._prepare_logging_writer()
|
|
||||||
|
|
||||||
# randomize initial episode lengths (for exploration)
|
|
||||||
if init_at_random_ep_len:
|
|
||||||
self.env.episode_length_buf = torch.randint_like(
|
|
||||||
self.env.episode_length_buf, high=int(self.env.max_episode_length)
|
|
||||||
)
|
|
||||||
|
|
||||||
# start learning
|
|
||||||
obs = self.env.get_observations().to(self.device)
|
|
||||||
self.train_mode() # switch to train mode (for dropout for example)
|
|
||||||
|
|
||||||
# Book keeping
|
|
||||||
ep_infos = []
|
|
||||||
rewbuffer = deque(maxlen=100)
|
|
||||||
lenbuffer = deque(maxlen=100)
|
|
||||||
cur_reward_sum = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
cur_episode_length = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
|
|
||||||
# create buffers for logging extrinsic and intrinsic rewards
|
|
||||||
if self.alg.rnd:
|
|
||||||
erewbuffer = deque(maxlen=100)
|
|
||||||
irewbuffer = deque(maxlen=100)
|
|
||||||
cur_ereward_sum = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
cur_ireward_sum = torch.zeros(self.env.num_envs, dtype=torch.float, device=self.device)
|
|
||||||
|
|
||||||
# Ensure all parameters are in-synced
|
|
||||||
if self.is_distributed:
|
|
||||||
print(f"Synchronizing parameters for rank {self.gpu_global_rank}...")
|
|
||||||
self.alg.broadcast_parameters()
|
|
||||||
|
|
||||||
# Start training
|
|
||||||
start_iter = self.current_learning_iteration
|
|
||||||
tot_iter = start_iter + num_learning_iterations
|
|
||||||
for it in range(start_iter, tot_iter):
|
|
||||||
start = time.time()
|
|
||||||
# Rollout
|
|
||||||
with torch.inference_mode():
|
|
||||||
for _ in range(self.num_steps_per_env):
|
|
||||||
# Sample actions
|
|
||||||
actions = self.alg.act(obs)
|
|
||||||
# Step the environment
|
|
||||||
obs, rewards, dones, extras = self.env.step(actions.to(self.env.device))
|
|
||||||
# Move to device
|
|
||||||
obs, rewards, dones = (obs.to(self.device), rewards.to(self.device), dones.to(self.device))
|
|
||||||
# process the step
|
|
||||||
self.alg.process_env_step(obs, rewards, dones, extras)
|
|
||||||
# Extract intrinsic rewards (only for logging)
|
|
||||||
intrinsic_rewards = self.alg.intrinsic_rewards if self.alg.rnd else None
|
|
||||||
# book keeping
|
|
||||||
if self.log_dir is not None:
|
|
||||||
if "episode" in extras:
|
|
||||||
ep_infos.append(extras["episode"])
|
|
||||||
elif "log" in extras:
|
|
||||||
ep_infos.append(extras["log"])
|
|
||||||
# Update rewards
|
|
||||||
if self.alg.rnd:
|
|
||||||
cur_ereward_sum += rewards
|
|
||||||
cur_ireward_sum += intrinsic_rewards # type: ignore
|
|
||||||
cur_reward_sum += rewards + intrinsic_rewards
|
|
||||||
else:
|
|
||||||
cur_reward_sum += rewards
|
|
||||||
# Update episode length
|
|
||||||
cur_episode_length += 1
|
|
||||||
# Clear data for completed episodes
|
|
||||||
# -- common
|
|
||||||
new_ids = (dones > 0).nonzero(as_tuple=False)
|
|
||||||
rewbuffer.extend(cur_reward_sum[new_ids][:, 0].cpu().numpy().tolist())
|
|
||||||
lenbuffer.extend(cur_episode_length[new_ids][:, 0].cpu().numpy().tolist())
|
|
||||||
cur_reward_sum[new_ids] = 0
|
|
||||||
cur_episode_length[new_ids] = 0
|
|
||||||
# -- intrinsic and extrinsic rewards
|
|
||||||
if self.alg.rnd:
|
|
||||||
erewbuffer.extend(cur_ereward_sum[new_ids][:, 0].cpu().numpy().tolist())
|
|
||||||
irewbuffer.extend(cur_ireward_sum[new_ids][:, 0].cpu().numpy().tolist())
|
|
||||||
cur_ereward_sum[new_ids] = 0
|
|
||||||
cur_ireward_sum[new_ids] = 0
|
|
||||||
|
|
||||||
stop = time.time()
|
|
||||||
collection_time = stop - start
|
|
||||||
start = stop
|
|
||||||
|
|
||||||
# compute returns
|
|
||||||
self.alg.compute_returns(obs)
|
|
||||||
|
|
||||||
# update policy
|
|
||||||
loss_dict = self.alg.update()
|
|
||||||
|
|
||||||
stop = time.time()
|
|
||||||
learn_time = stop - start
|
|
||||||
self.current_learning_iteration = it
|
|
||||||
# log info
|
|
||||||
if self.log_dir is not None and not self.disable_logs:
|
|
||||||
# Log information
|
|
||||||
self.log(locals())
|
|
||||||
# Save model
|
|
||||||
if it % self.save_interval == 0:
|
|
||||||
self.save(os.path.join(self.log_dir, f"model_{it}.pt"))
|
|
||||||
|
|
||||||
# Clear episode infos
|
|
||||||
ep_infos.clear()
|
|
||||||
# Save code state
|
|
||||||
if it == start_iter and not self.disable_logs:
|
|
||||||
# obtain all the diff files
|
|
||||||
git_file_paths = store_code_state(self.log_dir, self.git_status_repos)
|
|
||||||
# if possible store them to wandb
|
|
||||||
if self.logger_type in ["wandb", "neptune"] and git_file_paths:
|
|
||||||
for path in git_file_paths:
|
|
||||||
self.writer.save_file(path)
|
|
||||||
|
|
||||||
# Save the final model after training
|
|
||||||
if self.log_dir is not None and not self.disable_logs:
|
|
||||||
self.save(os.path.join(self.log_dir, f"model_{self.current_learning_iteration}.pt"))
|
|
||||||
|
|
||||||
def log(self, locs: dict, width: int = 80, pad: int = 35):
|
|
||||||
# Compute the collection size
|
|
||||||
collection_size = self.num_steps_per_env * self.env.num_envs * self.gpu_world_size
|
|
||||||
# Update total time-steps and time
|
|
||||||
self.tot_timesteps += collection_size
|
|
||||||
self.tot_time += locs["collection_time"] + locs["learn_time"]
|
|
||||||
iteration_time = locs["collection_time"] + locs["learn_time"]
|
|
||||||
|
|
||||||
# -- Episode info
|
|
||||||
ep_string = ""
|
|
||||||
if locs["ep_infos"]:
|
|
||||||
for key in locs["ep_infos"][0]:
|
|
||||||
infotensor = torch.tensor([], device=self.device)
|
|
||||||
for ep_info in locs["ep_infos"]:
|
|
||||||
# handle scalar and zero dimensional tensor infos
|
|
||||||
if key not in ep_info:
|
|
||||||
continue
|
|
||||||
if not isinstance(ep_info[key], torch.Tensor):
|
|
||||||
ep_info[key] = torch.Tensor([ep_info[key]])
|
|
||||||
if len(ep_info[key].shape) == 0:
|
|
||||||
ep_info[key] = ep_info[key].unsqueeze(0)
|
|
||||||
infotensor = torch.cat((infotensor, ep_info[key].to(self.device)))
|
|
||||||
value = torch.mean(infotensor)
|
|
||||||
# log to logger and terminal
|
|
||||||
if "/" in key:
|
|
||||||
self.writer.add_scalar(key, value, locs["it"])
|
|
||||||
ep_string += f"""{f'{key}:':>{pad}} {value:.4f}\n"""
|
|
||||||
else:
|
|
||||||
self.writer.add_scalar("Episode/" + key, value, locs["it"])
|
|
||||||
ep_string += f"""{f'Mean episode {key}:':>{pad}} {value:.4f}\n"""
|
|
||||||
|
|
||||||
mean_std = self.alg.policy.action_std.mean()
|
|
||||||
fps = int(collection_size / (locs["collection_time"] + locs["learn_time"]))
|
|
||||||
|
|
||||||
# -- Losses
|
|
||||||
for key, value in locs["loss_dict"].items():
|
|
||||||
self.writer.add_scalar(f"Loss/{key}", value, locs["it"])
|
|
||||||
self.writer.add_scalar("Loss/learning_rate", self.alg.learning_rate, locs["it"])
|
|
||||||
|
|
||||||
# -- Policy
|
|
||||||
self.writer.add_scalar("Policy/mean_noise_std", mean_std.item(), locs["it"])
|
|
||||||
|
|
||||||
# -- Performance
|
|
||||||
self.writer.add_scalar("Perf/total_fps", fps, locs["it"])
|
|
||||||
self.writer.add_scalar("Perf/collection time", locs["collection_time"], locs["it"])
|
|
||||||
self.writer.add_scalar("Perf/learning_time", locs["learn_time"], locs["it"])
|
|
||||||
|
|
||||||
# -- Training
|
|
||||||
if len(locs["rewbuffer"]) > 0:
|
|
||||||
# separate logging for intrinsic and extrinsic rewards
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
self.writer.add_scalar("Rnd/mean_extrinsic_reward", statistics.mean(locs["erewbuffer"]), locs["it"])
|
|
||||||
self.writer.add_scalar("Rnd/mean_intrinsic_reward", statistics.mean(locs["irewbuffer"]), locs["it"])
|
|
||||||
self.writer.add_scalar("Rnd/weight", self.alg.rnd.weight, locs["it"])
|
|
||||||
# everything else
|
|
||||||
self.writer.add_scalar("Train/mean_reward", statistics.mean(locs["rewbuffer"]), locs["it"])
|
|
||||||
self.writer.add_scalar("Train/mean_episode_length", statistics.mean(locs["lenbuffer"]), locs["it"])
|
|
||||||
if self.logger_type != "wandb": # wandb does not support non-integer x-axis logging
|
|
||||||
self.writer.add_scalar("Train/mean_reward/time", statistics.mean(locs["rewbuffer"]), self.tot_time)
|
|
||||||
self.writer.add_scalar(
|
|
||||||
"Train/mean_episode_length/time", statistics.mean(locs["lenbuffer"]), self.tot_time
|
|
||||||
)
|
|
||||||
|
|
||||||
str = f" \033[1m Learning iteration {locs['it']}/{locs['tot_iter']} \033[0m "
|
|
||||||
|
|
||||||
if len(locs["rewbuffer"]) > 0:
|
|
||||||
log_string = (
|
|
||||||
f"""{'#' * width}\n"""
|
|
||||||
f"""{str.center(width, ' ')}\n\n"""
|
|
||||||
f"""{'Computation:':>{pad}} {fps:.0f} steps/s (collection: {locs[
|
|
||||||
'collection_time']:.3f}s, learning {locs['learn_time']:.3f}s)\n"""
|
|
||||||
f"""{'Mean action noise std:':>{pad}} {mean_std.item():.2f}\n"""
|
|
||||||
)
|
|
||||||
# -- Losses
|
|
||||||
for key, value in locs["loss_dict"].items():
|
|
||||||
log_string += f"""{f'Mean {key} loss:':>{pad}} {value:.4f}\n"""
|
|
||||||
# -- Rewards
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
log_string += (
|
|
||||||
f"""{'Mean extrinsic reward:':>{pad}} {statistics.mean(locs['erewbuffer']):.2f}\n"""
|
|
||||||
f"""{'Mean intrinsic reward:':>{pad}} {statistics.mean(locs['irewbuffer']):.2f}\n"""
|
|
||||||
)
|
|
||||||
log_string += f"""{'Mean reward:':>{pad}} {statistics.mean(locs['rewbuffer']):.2f}\n"""
|
|
||||||
# -- episode info
|
|
||||||
log_string += f"""{'Mean episode length:':>{pad}} {statistics.mean(locs['lenbuffer']):.2f}\n"""
|
|
||||||
else:
|
|
||||||
log_string = (
|
|
||||||
f"""{'#' * width}\n"""
|
|
||||||
f"""{str.center(width, ' ')}\n\n"""
|
|
||||||
f"""{'Computation:':>{pad}} {fps:.0f} steps/s (collection: {locs[
|
|
||||||
'collection_time']:.3f}s, learning {locs['learn_time']:.3f}s)\n"""
|
|
||||||
f"""{'Mean action noise std:':>{pad}} {mean_std.item():.2f}\n"""
|
|
||||||
)
|
|
||||||
for key, value in locs["loss_dict"].items():
|
|
||||||
log_string += f"""{f'{key}:':>{pad}} {value:.4f}\n"""
|
|
||||||
|
|
||||||
log_string += ep_string
|
|
||||||
log_string += (
|
|
||||||
f"""{'-' * width}\n"""
|
|
||||||
f"""{'Total timesteps:':>{pad}} {self.tot_timesteps}\n"""
|
|
||||||
f"""{'Iteration time:':>{pad}} {iteration_time:.2f}s\n"""
|
|
||||||
f"""{'Time elapsed:':>{pad}} {time.strftime("%H:%M:%S", time.gmtime(self.tot_time))}\n"""
|
|
||||||
f"""{'ETA:':>{pad}} {time.strftime(
|
|
||||||
"%H:%M:%S",
|
|
||||||
time.gmtime(
|
|
||||||
self.tot_time / (locs['it'] - locs['start_iter'] + 1)
|
|
||||||
* (locs['start_iter'] + locs['num_learning_iterations'] - locs['it'])
|
|
||||||
)
|
|
||||||
)}\n"""
|
|
||||||
)
|
|
||||||
print(log_string)
|
|
||||||
|
|
||||||
def save(self, path: str, infos=None):
|
|
||||||
# -- Save model
|
|
||||||
saved_dict = {
|
|
||||||
"model_state_dict": self.alg.policy.state_dict(),
|
|
||||||
"optimizer_state_dict": self.alg.optimizer.state_dict(),
|
|
||||||
"iter": self.current_learning_iteration,
|
|
||||||
"infos": infos,
|
|
||||||
}
|
|
||||||
# -- Save RND model if used
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
saved_dict["rnd_state_dict"] = self.alg.rnd.state_dict()
|
|
||||||
saved_dict["rnd_optimizer_state_dict"] = self.alg.rnd_optimizer.state_dict()
|
|
||||||
torch.save(saved_dict, path)
|
|
||||||
|
|
||||||
# upload model to external logging service
|
|
||||||
if self.logger_type in ["neptune", "wandb"] and not self.disable_logs:
|
|
||||||
self.writer.save_model(path, self.current_learning_iteration)
|
|
||||||
|
|
||||||
def load(self, path: str, load_optimizer: bool = True, map_location: str | None = None):
|
|
||||||
loaded_dict = torch.load(path, weights_only=False, map_location=map_location)
|
|
||||||
# -- Load model
|
|
||||||
resumed_training = self.alg.policy.load_state_dict(loaded_dict["model_state_dict"])
|
|
||||||
# -- Load RND model if used
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
self.alg.rnd.load_state_dict(loaded_dict["rnd_state_dict"])
|
|
||||||
# -- load optimizer if used
|
|
||||||
if load_optimizer and resumed_training:
|
|
||||||
# -- algorithm optimizer
|
|
||||||
self.alg.optimizer.load_state_dict(loaded_dict["optimizer_state_dict"])
|
|
||||||
# -- RND optimizer if used
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
self.alg.rnd_optimizer.load_state_dict(loaded_dict["rnd_optimizer_state_dict"])
|
|
||||||
# -- load current learning iteration
|
|
||||||
if resumed_training:
|
|
||||||
self.current_learning_iteration = loaded_dict["iter"]
|
|
||||||
return loaded_dict["infos"]
|
|
||||||
|
|
||||||
def get_inference_policy(self, device=None):
|
|
||||||
self.eval_mode() # switch to evaluation mode (dropout for example)
|
|
||||||
if device is not None:
|
|
||||||
self.alg.policy.to(device)
|
|
||||||
return self.alg.policy.act_inference
|
|
||||||
|
|
||||||
def train_mode(self):
|
|
||||||
# -- PPO
|
|
||||||
self.alg.policy.train()
|
|
||||||
# -- RND
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
self.alg.rnd.train()
|
|
||||||
|
|
||||||
def eval_mode(self):
|
|
||||||
# -- PPO
|
|
||||||
self.alg.policy.eval()
|
|
||||||
# -- RND
|
|
||||||
if hasattr(self.alg, "rnd") and self.alg.rnd:
|
|
||||||
self.alg.rnd.eval()
|
|
||||||
|
|
||||||
def add_git_repo_to_log(self, repo_file_path):
|
|
||||||
self.git_status_repos.append(repo_file_path)
|
|
||||||
|
|
||||||
"""
|
|
||||||
Helper functions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _configure_multi_gpu(self):
|
|
||||||
"""Configure multi-gpu training."""
|
|
||||||
# check if distributed training is enabled
|
|
||||||
self.gpu_world_size = int(os.getenv("WORLD_SIZE", "1"))
|
|
||||||
self.is_distributed = self.gpu_world_size > 1
|
|
||||||
|
|
||||||
# if not distributed training, set local and global rank to 0 and return
|
|
||||||
if not self.is_distributed:
|
|
||||||
self.gpu_local_rank = 0
|
|
||||||
self.gpu_global_rank = 0
|
|
||||||
self.multi_gpu_cfg = None
|
|
||||||
return
|
|
||||||
|
|
||||||
# get rank and world size
|
|
||||||
self.gpu_local_rank = int(os.getenv("LOCAL_RANK", "0"))
|
|
||||||
self.gpu_global_rank = int(os.getenv("RANK", "0"))
|
|
||||||
|
|
||||||
# make a configuration dictionary
|
|
||||||
self.multi_gpu_cfg = {
|
|
||||||
"global_rank": self.gpu_global_rank, # rank of the main process
|
|
||||||
"local_rank": self.gpu_local_rank, # rank of the current process
|
|
||||||
"world_size": self.gpu_world_size, # total number of processes
|
|
||||||
}
|
|
||||||
|
|
||||||
# check if user has device specified for local rank
|
|
||||||
if self.device != f"cuda:{self.gpu_local_rank}":
|
|
||||||
raise ValueError(
|
|
||||||
f"Device '{self.device}' does not match expected device for local rank '{self.gpu_local_rank}'."
|
|
||||||
)
|
|
||||||
# validate multi-gpu configuration
|
|
||||||
if self.gpu_local_rank >= self.gpu_world_size:
|
|
||||||
raise ValueError(
|
|
||||||
f"Local rank '{self.gpu_local_rank}' is greater than or equal to world size '{self.gpu_world_size}'."
|
|
||||||
)
|
|
||||||
if self.gpu_global_rank >= self.gpu_world_size:
|
|
||||||
raise ValueError(
|
|
||||||
f"Global rank '{self.gpu_global_rank}' is greater than or equal to world size '{self.gpu_world_size}'."
|
|
||||||
)
|
|
||||||
|
|
||||||
# initialize torch distributed
|
|
||||||
torch.distributed.init_process_group(backend="nccl", rank=self.gpu_global_rank, world_size=self.gpu_world_size)
|
|
||||||
# set device to the local rank
|
|
||||||
torch.cuda.set_device(self.gpu_local_rank)
|
|
||||||
|
|
||||||
def _construct_algorithm(self, obs) -> PPO:
|
|
||||||
"""Construct the actor-critic algorithm."""
|
|
||||||
# resolve RND config
|
|
||||||
self.alg_cfg = resolve_rnd_config(self.alg_cfg, obs, self.cfg["obs_groups"], self.env)
|
|
||||||
|
|
||||||
# resolve symmetry config
|
|
||||||
self.alg_cfg = resolve_symmetry_config(self.alg_cfg, self.env)
|
|
||||||
|
|
||||||
# resolve deprecated normalization config
|
|
||||||
if self.cfg.get("empirical_normalization") is not None:
|
|
||||||
warnings.warn(
|
|
||||||
"The `empirical_normalization` parameter is deprecated. Please set `actor_obs_normalization` and "
|
|
||||||
"`critic_obs_normalization` as part of the `policy` configuration instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
if self.policy_cfg.get("actor_obs_normalization") is None:
|
|
||||||
self.policy_cfg["actor_obs_normalization"] = self.cfg["empirical_normalization"]
|
|
||||||
if self.policy_cfg.get("critic_obs_normalization") is None:
|
|
||||||
self.policy_cfg["critic_obs_normalization"] = self.cfg["empirical_normalization"]
|
|
||||||
|
|
||||||
# initialize the actor-critic
|
|
||||||
actor_critic_class = eval(self.policy_cfg.pop("class_name"))
|
|
||||||
actor_critic: ActorCritic | ActorCriticRecurrent = actor_critic_class(
|
|
||||||
obs, self.cfg["obs_groups"], self.env.num_actions, **self.policy_cfg
|
|
||||||
).to(self.device)
|
|
||||||
|
|
||||||
# initialize the algorithm
|
|
||||||
alg_class = eval(self.alg_cfg.pop("class_name"))
|
|
||||||
alg: PPO = alg_class(actor_critic, device=self.device, **self.alg_cfg, multi_gpu_cfg=self.multi_gpu_cfg)
|
|
||||||
|
|
||||||
# initialize the storage
|
|
||||||
alg.init_storage(
|
|
||||||
"rl",
|
|
||||||
self.env.num_envs,
|
|
||||||
self.num_steps_per_env,
|
|
||||||
obs,
|
|
||||||
[self.env.num_actions],
|
|
||||||
)
|
|
||||||
|
|
||||||
return alg
|
|
||||||
|
|
||||||
def _prepare_logging_writer(self):
|
|
||||||
"""Prepares the logging writers."""
|
|
||||||
if self.log_dir is not None and self.writer is None and not self.disable_logs:
|
|
||||||
# Launch either Tensorboard or Neptune & Tensorboard summary writer(s), default: Tensorboard.
|
|
||||||
self.logger_type = self.cfg.get("logger", "tensorboard")
|
|
||||||
self.logger_type = self.logger_type.lower()
|
|
||||||
|
|
||||||
if self.logger_type == "neptune":
|
|
||||||
from rsl_rl.utils.neptune_utils import NeptuneSummaryWriter
|
|
||||||
|
|
||||||
self.writer = NeptuneSummaryWriter(log_dir=self.log_dir, flush_secs=10, cfg=self.cfg)
|
|
||||||
self.writer.log_config(self.env.cfg, self.cfg, self.alg_cfg, self.policy_cfg)
|
|
||||||
elif self.logger_type == "wandb":
|
|
||||||
from rsl_rl.utils.wandb_utils import WandbSummaryWriter
|
|
||||||
|
|
||||||
self.writer = WandbSummaryWriter(log_dir=self.log_dir, flush_secs=10, cfg=self.cfg)
|
|
||||||
self.writer.log_config(self.env.cfg, self.cfg, self.alg_cfg, self.policy_cfg)
|
|
||||||
elif self.logger_type == "tensorboard":
|
|
||||||
from torch.utils.tensorboard import SummaryWriter
|
|
||||||
|
|
||||||
self.writer = SummaryWriter(log_dir=self.log_dir, flush_secs=10)
|
|
||||||
else:
|
|
||||||
raise ValueError("Logger type not found. Please choose 'neptune', 'wandb' or 'tensorboard'.")
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
# Copyright (c) 2021-2025, ETH Zurich and NVIDIA CORPORATION
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
"""Implementation of transitions storage for RL-agent."""
|
|
||||||
|
|
||||||
from .rollout_storage import RolloutStorage
|
|
||||||
from .replay_buffer_multi import ReplayBufferMulti
|
|
||||||
__all__ = ["RolloutStorage", "ReplayBufferMulti"]
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue