Initial upload of BFN-hybrid + DDPM PushT-xarm policies
Browse files- README.md +71 -0
- bfn/latest.ckpt +3 -0
- bfn/policy_config.yaml +127 -0
- bfn_hybrid_image_policy.py +356 -0
- ddpm/latest.ckpt +3 -0
- ddpm/policy_config.yaml +133 -0
- inference.py +100 -0
- networks/__init__.py +0 -0
- networks/base.py +146 -0
- policies/__init__.py +0 -0
- policies/base.py +242 -0
- requirements.txt +12 -0
README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: mit
|
| 3 |
+
tags:
|
| 4 |
+
- robotics
|
| 5 |
+
- bfn
|
| 6 |
+
- bayesian-flow-networks
|
| 7 |
+
- diffusion-policy
|
| 8 |
+
- pusht
|
| 9 |
+
- xarm
|
| 10 |
+
- hybrid-action
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
# PushT-xarm Real-Robot Policies (BFN-Hybrid vs DDPM-OneHot)
|
| 14 |
+
|
| 15 |
+
Real-robot push-T policies trained on [borueihuang/pusht_xarm_merged](https://huggingface.co/datasets/borueihuang/pusht_xarm_merged).
|
| 16 |
+
Both policies were trained on 144 episodes / 7835 frames at 30 Hz, top camera only, 200 epochs.
|
| 17 |
+
|
| 18 |
+
## Action space
|
| 19 |
+
|
| 20 |
+
- **Discrete:** 8 push directions (`action.direction` in {0..7})
|
| 21 |
+
- **Continuous:** push distance (`action.distance` in `[0, 50]`)
|
| 22 |
+
|
| 23 |
+
## Observation space
|
| 24 |
+
|
| 25 |
+
- `camera_0`: top-view RGB image, 3x224x224, two-step history (`n_obs_steps=2`)
|
| 26 |
+
|
| 27 |
+
## Policies
|
| 28 |
+
|
| 29 |
+
| File | Method | Action treatment | Inference steps |
|
| 30 |
+
|------|--------|------------------|-----------------|
|
| 31 |
+
| `bfn/latest.ckpt` | **BFN-Hybrid** (categorical + continuous Bayesian flow) | true hybrid | 20 |
|
| 32 |
+
| `ddpm/latest.ckpt` | **DDPM** | one-hot continuous (9D) | 100 |
|
| 33 |
+
|
| 34 |
+
## Quick start (BFN)
|
| 35 |
+
|
| 36 |
+
```bash
|
| 37 |
+
pip install -r requirements.txt
|
| 38 |
+
python inference.py --ckpt bfn/latest.ckpt --config bfn/policy_config.yaml
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
Programmatic use:
|
| 42 |
+
|
| 43 |
+
```python
|
| 44 |
+
from inference import load_bfn_policy, infer_step
|
| 45 |
+
|
| 46 |
+
policy = load_bfn_policy("bfn/latest.ckpt", "bfn/policy_config.yaml", "cuda")
|
| 47 |
+
actions = infer_step(policy, cam0_now, cam0_prev, "cuda")
|
| 48 |
+
# actions: List[{"direction": int 0..7, "distance": float 0..50}], len = n_action_steps (8)
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
## DDPM checkpoint
|
| 52 |
+
|
| 53 |
+
The DDPM policy uses `diffusion_policy.policy.diffusion_unet_hybrid_image_policy.DiffusionUnetHybridImagePolicy`
|
| 54 |
+
from the `diffusion-policy` library. Action is a 9D continuous vector: `[one_hot(8), distance]`.
|
| 55 |
+
At inference time, take `argmax` of the first 8 dims for the direction, and the 9th dim for distance.
|
| 56 |
+
|
| 57 |
+
## Files
|
| 58 |
+
|
| 59 |
+
```
|
| 60 |
+
bfn/
|
| 61 |
+
latest.ckpt
|
| 62 |
+
policy_config.yaml
|
| 63 |
+
ddpm/
|
| 64 |
+
latest.ckpt
|
| 65 |
+
policy_config.yaml
|
| 66 |
+
bfn_hybrid_image_policy.py # standalone BFN policy class
|
| 67 |
+
policies/base.py # BasePolicy abstract class
|
| 68 |
+
networks/base.py # BFNetwork wrapper
|
| 69 |
+
inference.py # example loader + inference
|
| 70 |
+
requirements.txt
|
| 71 |
+
```
|
bfn/latest.ckpt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:bd3e457f54d235da8d379d0e1771ffeb1a2d2e420b5fdd86212c1cdfb62777b7
|
| 3 |
+
size 697951714
|
bfn/policy_config.yaml
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
_target_: workspaces.train_bfn_workspace.TrainBFNWorkspace
|
| 2 |
+
name: train_bfn_pusht_xarm_top
|
| 3 |
+
task_name: pusht_xarm
|
| 4 |
+
exp_name: bfn_hybrid_top
|
| 5 |
+
shape_meta:
|
| 6 |
+
obs:
|
| 7 |
+
camera_0:
|
| 8 |
+
shape:
|
| 9 |
+
- 3
|
| 10 |
+
- 224
|
| 11 |
+
- 224
|
| 12 |
+
type: rgb
|
| 13 |
+
action:
|
| 14 |
+
shape:
|
| 15 |
+
- 2
|
| 16 |
+
horizon: 16
|
| 17 |
+
n_obs_steps: 2
|
| 18 |
+
n_action_steps: 8
|
| 19 |
+
n_latency_steps: 0
|
| 20 |
+
dataset_obs_steps: 2
|
| 21 |
+
past_action_visible: false
|
| 22 |
+
obs_as_global_cond: true
|
| 23 |
+
policy:
|
| 24 |
+
_target_: policies.bfn_hybrid_image_policy.BFNHybridImagePolicy
|
| 25 |
+
shape_meta: ${shape_meta}
|
| 26 |
+
horizon: ${horizon}
|
| 27 |
+
n_action_steps: ${n_action_steps}
|
| 28 |
+
n_obs_steps: ${n_obs_steps}
|
| 29 |
+
num_discrete_actions: 8
|
| 30 |
+
continuous_param_dim: 1
|
| 31 |
+
sigma_1: 0.001
|
| 32 |
+
beta_1: 0.2
|
| 33 |
+
n_timesteps: 20
|
| 34 |
+
crop_shape:
|
| 35 |
+
- 216
|
| 36 |
+
- 216
|
| 37 |
+
obs_encoder_group_norm: true
|
| 38 |
+
eval_fixed_crop: true
|
| 39 |
+
diffusion_step_embed_dim: 128
|
| 40 |
+
down_dims:
|
| 41 |
+
- 256
|
| 42 |
+
- 512
|
| 43 |
+
- 1024
|
| 44 |
+
kernel_size: 5
|
| 45 |
+
n_groups: 8
|
| 46 |
+
cond_predict_scale: true
|
| 47 |
+
task:
|
| 48 |
+
dataset:
|
| 49 |
+
_target_: dataset.pusht_xarm_dataset.PushTXArmDataset
|
| 50 |
+
zarr_path: data/pusht_xarm_merged/replay.zarr
|
| 51 |
+
horizon: ${horizon}
|
| 52 |
+
pad_before: 1
|
| 53 |
+
pad_after: 7
|
| 54 |
+
n_obs_steps: ${n_obs_steps}
|
| 55 |
+
seed: 42
|
| 56 |
+
val_ratio: 0.1
|
| 57 |
+
cameras:
|
| 58 |
+
- camera_0
|
| 59 |
+
action_mode: hybrid
|
| 60 |
+
env_runner: null
|
| 61 |
+
ema:
|
| 62 |
+
_target_: diffusion_policy.model.diffusion.ema_model.EMAModel
|
| 63 |
+
update_after_step: 0
|
| 64 |
+
inv_gamma: 1.0
|
| 65 |
+
power: 0.75
|
| 66 |
+
min_value: 0.0
|
| 67 |
+
max_value: 0.9999
|
| 68 |
+
optimizer:
|
| 69 |
+
_target_: torch.optim.AdamW
|
| 70 |
+
lr: 0.0001
|
| 71 |
+
betas:
|
| 72 |
+
- 0.95
|
| 73 |
+
- 0.999
|
| 74 |
+
eps: 1.0e-08
|
| 75 |
+
weight_decay: 1.0e-06
|
| 76 |
+
training:
|
| 77 |
+
device: cuda:0
|
| 78 |
+
seed: 42
|
| 79 |
+
debug: false
|
| 80 |
+
resume: false
|
| 81 |
+
lr_scheduler: cosine
|
| 82 |
+
lr_warmup_steps: 500
|
| 83 |
+
num_epochs: 200
|
| 84 |
+
gradient_accumulate_every: 1
|
| 85 |
+
use_ema: true
|
| 86 |
+
rollout_every: 50
|
| 87 |
+
checkpoint_every: 50
|
| 88 |
+
val_every: 1
|
| 89 |
+
sample_every: 5
|
| 90 |
+
max_train_steps: null
|
| 91 |
+
max_val_steps: null
|
| 92 |
+
tqdm_interval_sec: 1.0
|
| 93 |
+
dataloader:
|
| 94 |
+
batch_size: 32
|
| 95 |
+
num_workers: 4
|
| 96 |
+
shuffle: true
|
| 97 |
+
pin_memory: true
|
| 98 |
+
persistent_workers: false
|
| 99 |
+
val_dataloader:
|
| 100 |
+
batch_size: 32
|
| 101 |
+
num_workers: 2
|
| 102 |
+
shuffle: false
|
| 103 |
+
pin_memory: true
|
| 104 |
+
persistent_workers: false
|
| 105 |
+
checkpoint:
|
| 106 |
+
topk:
|
| 107 |
+
monitor_key: train_loss
|
| 108 |
+
mode: min
|
| 109 |
+
k: 1
|
| 110 |
+
format_str: epoch={epoch:04d}-train_loss={train_loss:.4f}.ckpt
|
| 111 |
+
save_last_ckpt: true
|
| 112 |
+
save_last_snapshot: false
|
| 113 |
+
logging:
|
| 114 |
+
project: pusht_xarm_bfn
|
| 115 |
+
resume: true
|
| 116 |
+
mode: offline
|
| 117 |
+
name: ${now:%Y.%m.%d-%H.%M.%S}_${name}
|
| 118 |
+
tags:
|
| 119 |
+
- bfn
|
| 120 |
+
- pusht_xarm
|
| 121 |
+
- hybrid
|
| 122 |
+
- top
|
| 123 |
+
id: null
|
| 124 |
+
group: null
|
| 125 |
+
multi_run:
|
| 126 |
+
run_dir: data/outputs/${now:%Y.%m.%d}/${now:%H.%M.%S}_${name}
|
| 127 |
+
wandb_name_base: ${now:%Y.%m.%d-%H.%M.%S}_${name}
|
bfn_hybrid_image_policy.py
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""BFN Hybrid Image Policy: image observations + categorical-discrete + continuous action heads.
|
| 2 |
+
|
| 3 |
+
This is the policy for real-robot PushT with hybrid action space:
|
| 4 |
+
- Discrete: 8 push directions
|
| 5 |
+
- Continuous: push distance
|
| 6 |
+
- Observation: one or more RGB cameras (cam0 top, cam1 side)
|
| 7 |
+
"""
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import math
|
| 11 |
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
import torch.nn as nn
|
| 15 |
+
import torch.nn.functional as F
|
| 16 |
+
|
| 17 |
+
from diffusion_policy.model.common.normalizer import LinearNormalizer
|
| 18 |
+
from diffusion_policy.model.diffusion.conditional_unet1d import ConditionalUnet1D
|
| 19 |
+
from diffusion_policy.common.pytorch_util import dict_apply
|
| 20 |
+
|
| 21 |
+
from policies.base import BasePolicy
|
| 22 |
+
from networks.base import BFNetwork
|
| 23 |
+
|
| 24 |
+
try:
|
| 25 |
+
import robomimic.models.obs_core as rmbn
|
| 26 |
+
import diffusion_policy.model.vision.crop_randomizer as dmvc
|
| 27 |
+
from diffusion_policy.common.pytorch_util import replace_submodules
|
| 28 |
+
import robomimic.utils.obs_utils as ObsUtils
|
| 29 |
+
from robomimic.config import config_factory
|
| 30 |
+
from robomimic.algo import algo_factory, PolicyAlgo
|
| 31 |
+
from diffusion_policy.common.robomimic_config_util import get_robomimic_config
|
| 32 |
+
HAS_ROBOMIMIC = True
|
| 33 |
+
except ImportError:
|
| 34 |
+
HAS_ROBOMIMIC = False
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
__all__ = ["BFNHybridImagePolicy"]
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class HybridUnetWrapper(BFNetwork):
|
| 41 |
+
def __init__(self, model, horizon, continuous_dim, discrete_configs, cond_dim):
|
| 42 |
+
super().__init__(is_conditional_model=True)
|
| 43 |
+
self.model = model
|
| 44 |
+
self.horizon = horizon
|
| 45 |
+
self.continuous_dim = continuous_dim
|
| 46 |
+
self.discrete_configs = discrete_configs
|
| 47 |
+
self.cond_dim = cond_dim
|
| 48 |
+
self.cond_is_discrete = False
|
| 49 |
+
total_disc = sum(n for _, n in discrete_configs)
|
| 50 |
+
self.input_dim = continuous_dim + total_disc
|
| 51 |
+
|
| 52 |
+
def forward(self, x, t, cond=None):
|
| 53 |
+
B = x.shape[0]
|
| 54 |
+
x = x.view(B, self.horizon, self.input_dim)
|
| 55 |
+
out = self.model(x, t, global_cond=cond)
|
| 56 |
+
return out.reshape(B, -1)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class BFNHybridImagePolicy(BasePolicy):
|
| 60 |
+
"""BFN policy with image observations + hybrid (categorical + continuous) action head."""
|
| 61 |
+
|
| 62 |
+
def __init__(
|
| 63 |
+
self,
|
| 64 |
+
shape_meta: dict,
|
| 65 |
+
horizon: int = 16,
|
| 66 |
+
n_action_steps: int = 8,
|
| 67 |
+
n_obs_steps: int = 2,
|
| 68 |
+
num_discrete_actions: int = 8,
|
| 69 |
+
continuous_param_dim: int = 1,
|
| 70 |
+
sigma_1: float = 0.001,
|
| 71 |
+
beta_1: float = 0.2,
|
| 72 |
+
n_timesteps: int = 20,
|
| 73 |
+
crop_shape: tuple = (216, 216),
|
| 74 |
+
obs_encoder_group_norm: bool = True,
|
| 75 |
+
eval_fixed_crop: bool = True,
|
| 76 |
+
diffusion_step_embed_dim: int = 128,
|
| 77 |
+
down_dims: tuple = (256, 512, 1024),
|
| 78 |
+
kernel_size: int = 5,
|
| 79 |
+
n_groups: int = 8,
|
| 80 |
+
cond_predict_scale: bool = True,
|
| 81 |
+
device: str = "cpu",
|
| 82 |
+
dtype: str = "float32",
|
| 83 |
+
clip_actions: bool = True,
|
| 84 |
+
**kwargs,
|
| 85 |
+
):
|
| 86 |
+
super().__init__(action_space=None, device=device, dtype=dtype, clip_actions=clip_actions)
|
| 87 |
+
|
| 88 |
+
self.horizon = horizon
|
| 89 |
+
self.n_action_steps = n_action_steps
|
| 90 |
+
self.n_obs_steps = n_obs_steps
|
| 91 |
+
self.num_discrete_actions = num_discrete_actions
|
| 92 |
+
self.continuous_dim = continuous_param_dim
|
| 93 |
+
self.discrete_configs = [(0, num_discrete_actions)]
|
| 94 |
+
self.discrete_action_indices = {0}
|
| 95 |
+
self.total_action_dim = 1 + continuous_param_dim
|
| 96 |
+
self.sigma_1 = sigma_1
|
| 97 |
+
self.beta_1 = beta_1
|
| 98 |
+
self.n_timesteps = n_timesteps
|
| 99 |
+
|
| 100 |
+
# Parse shape_meta for image obs keys
|
| 101 |
+
obs_shape_meta = shape_meta["obs"]
|
| 102 |
+
obs_config = {"low_dim": [], "rgb": [], "depth": [], "scan": []}
|
| 103 |
+
obs_key_shapes = {}
|
| 104 |
+
self.rgb_keys: List[str] = []
|
| 105 |
+
for key, attr in obs_shape_meta.items():
|
| 106 |
+
obs_key_shapes[key] = list(attr["shape"])
|
| 107 |
+
t = attr.get("type", "low_dim")
|
| 108 |
+
if t == "rgb":
|
| 109 |
+
obs_config["rgb"].append(key)
|
| 110 |
+
self.rgb_keys.append(key)
|
| 111 |
+
elif t == "low_dim":
|
| 112 |
+
obs_config["low_dim"].append(key)
|
| 113 |
+
else:
|
| 114 |
+
raise ValueError(f"Unsupported obs type: {t}")
|
| 115 |
+
assert HAS_ROBOMIMIC, "robomimic required for image policy"
|
| 116 |
+
|
| 117 |
+
self.obs_encoder = self._build_robomimic_encoder(
|
| 118 |
+
obs_config, obs_key_shapes, crop_shape, obs_encoder_group_norm, eval_fixed_crop
|
| 119 |
+
)
|
| 120 |
+
obs_feature_dim = self.obs_encoder.output_shape()[0]
|
| 121 |
+
global_cond_dim = obs_feature_dim * n_obs_steps
|
| 122 |
+
|
| 123 |
+
# U-Net input/output dim = continuous + discrete-logits
|
| 124 |
+
unet_dim = continuous_param_dim + num_discrete_actions
|
| 125 |
+
|
| 126 |
+
self.model = ConditionalUnet1D(
|
| 127 |
+
input_dim=unet_dim,
|
| 128 |
+
local_cond_dim=None,
|
| 129 |
+
global_cond_dim=global_cond_dim,
|
| 130 |
+
diffusion_step_embed_dim=diffusion_step_embed_dim,
|
| 131 |
+
down_dims=list(down_dims),
|
| 132 |
+
kernel_size=kernel_size,
|
| 133 |
+
n_groups=n_groups,
|
| 134 |
+
cond_predict_scale=cond_predict_scale,
|
| 135 |
+
)
|
| 136 |
+
self.unet_wrapper = HybridUnetWrapper(
|
| 137 |
+
model=self.model,
|
| 138 |
+
horizon=horizon,
|
| 139 |
+
continuous_dim=continuous_param_dim,
|
| 140 |
+
discrete_configs=self.discrete_configs,
|
| 141 |
+
cond_dim=global_cond_dim,
|
| 142 |
+
)
|
| 143 |
+
self.normalizer = LinearNormalizer()
|
| 144 |
+
self.global_cond_dim = global_cond_dim
|
| 145 |
+
|
| 146 |
+
print(f"BFN Hybrid Image Policy:")
|
| 147 |
+
print(f" cameras: {self.rgb_keys}")
|
| 148 |
+
print(f" discrete: {num_discrete_actions}, continuous: {continuous_param_dim}")
|
| 149 |
+
print(f" obs_feature_dim: {obs_feature_dim}, global_cond_dim: {global_cond_dim}")
|
| 150 |
+
print(f" U-Net params: {sum(p.numel() for p in self.model.parameters()):.2e}")
|
| 151 |
+
print(f" Vision params: {sum(p.numel() for p in self.obs_encoder.parameters()):.2e}")
|
| 152 |
+
|
| 153 |
+
def _build_robomimic_encoder(self, obs_config, obs_key_shapes, crop_shape, group_norm, eval_fixed_crop):
|
| 154 |
+
config = get_robomimic_config(algo_name="bc_rnn", hdf5_type="image", task_name="square", dataset_type="ph")
|
| 155 |
+
with config.unlocked():
|
| 156 |
+
config.observation.modalities.obs = obs_config
|
| 157 |
+
if crop_shape is None:
|
| 158 |
+
for key, modality in config.observation.encoder.items():
|
| 159 |
+
if modality.obs_randomizer_class == "CropRandomizer":
|
| 160 |
+
modality["obs_randomizer_class"] = None
|
| 161 |
+
else:
|
| 162 |
+
ch, cw = crop_shape
|
| 163 |
+
for key, modality in config.observation.encoder.items():
|
| 164 |
+
if modality.obs_randomizer_class == "CropRandomizer":
|
| 165 |
+
modality.obs_randomizer_kwargs.crop_height = ch
|
| 166 |
+
modality.obs_randomizer_kwargs.crop_width = cw
|
| 167 |
+
ObsUtils.initialize_obs_utils_with_config(config)
|
| 168 |
+
policy: PolicyAlgo = algo_factory(
|
| 169 |
+
algo_name=config.algo_name, config=config,
|
| 170 |
+
obs_key_shapes=obs_key_shapes, ac_dim=1 + self.num_discrete_actions, device="cpu",
|
| 171 |
+
)
|
| 172 |
+
obs_encoder = policy.nets["policy"].nets["encoder"].nets["obs"]
|
| 173 |
+
if group_norm:
|
| 174 |
+
replace_submodules(
|
| 175 |
+
root_module=obs_encoder,
|
| 176 |
+
predicate=lambda x: isinstance(x, nn.BatchNorm2d),
|
| 177 |
+
func=lambda x: nn.GroupNorm(num_groups=x.num_features // 16, num_channels=x.num_features),
|
| 178 |
+
)
|
| 179 |
+
if eval_fixed_crop:
|
| 180 |
+
replace_submodules(
|
| 181 |
+
root_module=obs_encoder,
|
| 182 |
+
predicate=lambda x: isinstance(x, rmbn.CropRandomizer),
|
| 183 |
+
func=lambda x: dmvc.CropRandomizer(
|
| 184 |
+
input_shape=x.input_shape, crop_height=x.crop_height,
|
| 185 |
+
crop_width=x.crop_width, num_crops=x.num_crops, pos_enc=x.pos_enc,
|
| 186 |
+
),
|
| 187 |
+
)
|
| 188 |
+
return obs_encoder
|
| 189 |
+
|
| 190 |
+
def set_normalizer(self, normalizer: LinearNormalizer):
|
| 191 |
+
self.normalizer.load_state_dict(normalizer.state_dict())
|
| 192 |
+
|
| 193 |
+
def _encode_obs(self, nobs: Dict[str, torch.Tensor]) -> torch.Tensor:
|
| 194 |
+
"""Encode obs dict to [B, global_cond_dim]."""
|
| 195 |
+
B = nobs[self.rgb_keys[0]].shape[0]
|
| 196 |
+
To = self.n_obs_steps
|
| 197 |
+
# Stack across time: build dict of [B*To, C, H, W]
|
| 198 |
+
flat = {}
|
| 199 |
+
for k, v in nobs.items():
|
| 200 |
+
v_t = v[:, :To] # [B, To, C, H, W]
|
| 201 |
+
flat[k] = v_t.reshape(B * To, *v_t.shape[2:])
|
| 202 |
+
feats = self.obs_encoder(flat) # [B*To, feat_dim]
|
| 203 |
+
feats = feats.reshape(B, To, -1)
|
| 204 |
+
return feats.reshape(B, -1)
|
| 205 |
+
|
| 206 |
+
def forward(self, obs, *, deterministic: bool = False, **kwargs):
|
| 207 |
+
if isinstance(obs, torch.Tensor):
|
| 208 |
+
obs = {"obs": obs}
|
| 209 |
+
return self.predict_action(obs)["action"]
|
| 210 |
+
|
| 211 |
+
def predict_action(self, obs_dict: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]:
|
| 212 |
+
nobs = self.normalizer.normalize(obs_dict)
|
| 213 |
+
cond = self._encode_obs(nobs)
|
| 214 |
+
B = cond.shape[0]
|
| 215 |
+
device = cond.device
|
| 216 |
+
dtype = cond.dtype
|
| 217 |
+
naction = self._sample_hybrid_bfn(B, self.horizon, cond, device, dtype)
|
| 218 |
+
start = self.n_obs_steps - 1
|
| 219 |
+
end = start + self.n_action_steps
|
| 220 |
+
action = naction[:, start:end]
|
| 221 |
+
action_unnorm = action.clone()
|
| 222 |
+
if action.shape[-1] > 1:
|
| 223 |
+
full_unnorm = self.normalizer["action"].unnormalize(action.clone())
|
| 224 |
+
action_unnorm[:, :, 1:] = full_unnorm[:, :, 1:]
|
| 225 |
+
return {"action": action_unnorm, "action_pred": naction}
|
| 226 |
+
|
| 227 |
+
@torch.no_grad()
|
| 228 |
+
def _sample_hybrid_bfn(self, B, T, cond, device, dtype):
|
| 229 |
+
n_steps = self.n_timesteps
|
| 230 |
+
cont_dim = self.continuous_dim
|
| 231 |
+
disc_configs = self.discrete_configs
|
| 232 |
+
|
| 233 |
+
mu_cont = torch.zeros(B, T, cont_dim, device=device, dtype=dtype)
|
| 234 |
+
rho_cont = 1.0
|
| 235 |
+
theta_list = [
|
| 236 |
+
torch.full((B, T, n), 1.0 / n, device=device, dtype=dtype) for _, n in disc_configs
|
| 237 |
+
]
|
| 238 |
+
|
| 239 |
+
for i in range(1, n_steps + 1):
|
| 240 |
+
t_val = (i - 1) / n_steps
|
| 241 |
+
t_batch = torch.full((B,), t_val, device=device, dtype=dtype)
|
| 242 |
+
net_input = torch.cat([mu_cont, *theta_list], dim=-1) if theta_list else mu_cont
|
| 243 |
+
out_flat = self.unet_wrapper(net_input.reshape(B, -1), t_batch, cond=cond)
|
| 244 |
+
out = out_flat.reshape(B, T, -1)
|
| 245 |
+
|
| 246 |
+
x_cont_pred = out[:, :, :cont_dim]
|
| 247 |
+
alpha_cont = (self.sigma_1 ** (-2.0 * i / n_steps)) * (1.0 - self.sigma_1 ** (2.0 / n_steps))
|
| 248 |
+
sender_std = 1.0 / (alpha_cont ** 0.5 + 1e-8)
|
| 249 |
+
y_cont = x_cont_pred + sender_std * torch.randn_like(x_cont_pred)
|
| 250 |
+
new_rho = rho_cont + alpha_cont
|
| 251 |
+
mu_cont = (rho_cont * mu_cont + alpha_cont * y_cont) / new_rho
|
| 252 |
+
rho_cont = new_rho
|
| 253 |
+
|
| 254 |
+
alpha_disc = self.beta_1 * (2 * i - 1) / (n_steps ** 2)
|
| 255 |
+
offset = cont_dim
|
| 256 |
+
new_theta_list = []
|
| 257 |
+
for j, (_, n_classes) in enumerate(disc_configs):
|
| 258 |
+
logits = out[:, :, offset:offset + n_classes]
|
| 259 |
+
probs = torch.softmax(logits, dim=-1)
|
| 260 |
+
probs_flat = probs.reshape(-1, n_classes)
|
| 261 |
+
k_samples = torch.multinomial(probs_flat, num_samples=1).squeeze(-1).reshape(B, T)
|
| 262 |
+
e_k = F.one_hot(k_samples, num_classes=n_classes).float()
|
| 263 |
+
y_mean = alpha_disc * (n_classes * e_k - 1)
|
| 264 |
+
y_std = (alpha_disc * n_classes + 1e-8) ** 0.5
|
| 265 |
+
y_disc = y_mean + y_std * torch.randn_like(y_mean)
|
| 266 |
+
log_theta = torch.log(theta_list[j] + 1e-8)
|
| 267 |
+
theta_new = torch.softmax(log_theta + y_disc, dim=-1)
|
| 268 |
+
new_theta_list.append(theta_new)
|
| 269 |
+
offset += n_classes
|
| 270 |
+
theta_list = new_theta_list
|
| 271 |
+
|
| 272 |
+
# Final
|
| 273 |
+
t_final = torch.ones(B, device=device, dtype=dtype)
|
| 274 |
+
net_input = torch.cat([mu_cont, *theta_list], dim=-1) if theta_list else mu_cont
|
| 275 |
+
out_final = self.unet_wrapper(net_input.reshape(B, -1), t_final, cond=cond).reshape(B, T, -1)
|
| 276 |
+
x_cont_final = out_final[:, :, :cont_dim].clamp(-1.0, 1.0)
|
| 277 |
+
|
| 278 |
+
disc_values = []
|
| 279 |
+
offset = cont_dim
|
| 280 |
+
for j, (_, n_classes) in enumerate(disc_configs):
|
| 281 |
+
logits = out_final[:, :, offset:offset + n_classes]
|
| 282 |
+
disc_values.append(logits.argmax(dim=-1).float().unsqueeze(-1))
|
| 283 |
+
offset += n_classes
|
| 284 |
+
|
| 285 |
+
if disc_values:
|
| 286 |
+
return torch.cat([torch.cat(disc_values, dim=-1), x_cont_final], dim=-1)
|
| 287 |
+
return x_cont_final
|
| 288 |
+
|
| 289 |
+
def compute_loss(self, batch: Dict[str, torch.Tensor]) -> torch.Tensor:
|
| 290 |
+
nobs = self.normalizer.normalize(batch["obs"])
|
| 291 |
+
cond = self._encode_obs(nobs)
|
| 292 |
+
raw_action = batch["action"]
|
| 293 |
+
discrete_k = raw_action[:, :, 0].long()
|
| 294 |
+
naction = self.normalizer["action"].normalize(raw_action)
|
| 295 |
+
continuous_x = naction[:, :, 1:]
|
| 296 |
+
|
| 297 |
+
B = raw_action.shape[0]
|
| 298 |
+
T = self.horizon
|
| 299 |
+
device = raw_action.device
|
| 300 |
+
dtype = raw_action.dtype
|
| 301 |
+
|
| 302 |
+
t = torch.rand(B, device=device, dtype=dtype).clamp(min=1e-5, max=1.0 - 1e-5)
|
| 303 |
+
t_exp = t.view(B, 1, 1)
|
| 304 |
+
gamma = 1.0 - (self.sigma_1 ** (2.0 * t_exp))
|
| 305 |
+
var = gamma * (1.0 - gamma)
|
| 306 |
+
std = (var + 1e-8).sqrt()
|
| 307 |
+
mu_cont = gamma * continuous_x + std * torch.randn_like(continuous_x)
|
| 308 |
+
|
| 309 |
+
beta = self.beta_1 * t_exp.pow(2.0)
|
| 310 |
+
theta_list = []
|
| 311 |
+
disc_targets = []
|
| 312 |
+
for j, (_, n) in enumerate(self.discrete_configs):
|
| 313 |
+
d = discrete_k.clamp(0, n - 1)
|
| 314 |
+
disc_targets.append(d)
|
| 315 |
+
e_x = F.one_hot(d, num_classes=n).float()
|
| 316 |
+
mean = beta * (n * e_x - 1)
|
| 317 |
+
std_disc = (beta * n + 1e-8).sqrt()
|
| 318 |
+
y = mean + std_disc * torch.randn_like(mean)
|
| 319 |
+
theta_list.append(torch.softmax(y, dim=-1))
|
| 320 |
+
|
| 321 |
+
net_input = torch.cat([mu_cont, *theta_list], dim=-1) if theta_list else mu_cont
|
| 322 |
+
out_flat = self.unet_wrapper(net_input.reshape(B, -1), t, cond=cond)
|
| 323 |
+
out = out_flat.reshape(B, T, -1)
|
| 324 |
+
|
| 325 |
+
x_cont_pred = out[:, :, :self.continuous_dim]
|
| 326 |
+
cont_loss = (gamma * (continuous_x - x_cont_pred).pow(2.0)).mean()
|
| 327 |
+
|
| 328 |
+
disc_loss = 0.0
|
| 329 |
+
offset = self.continuous_dim
|
| 330 |
+
for j, (_, n) in enumerate(self.discrete_configs):
|
| 331 |
+
logits = out[:, :, offset:offset + n]
|
| 332 |
+
disc_loss = disc_loss + F.cross_entropy(
|
| 333 |
+
logits.reshape(-1, n), disc_targets[j].reshape(-1)
|
| 334 |
+
)
|
| 335 |
+
offset += n
|
| 336 |
+
|
| 337 |
+
return cont_loss + disc_loss
|
| 338 |
+
|
| 339 |
+
def state_dict(self):
|
| 340 |
+
return {
|
| 341 |
+
"obs_encoder": self.obs_encoder.state_dict(),
|
| 342 |
+
"model": self.model.state_dict(),
|
| 343 |
+
"normalizer": self.normalizer.state_dict(),
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
def load_state_dict(self, state_dict):
|
| 347 |
+
self.obs_encoder.load_state_dict(state_dict["obs_encoder"])
|
| 348 |
+
self.model.load_state_dict(state_dict["model"])
|
| 349 |
+
if "normalizer" in state_dict:
|
| 350 |
+
self.normalizer.load_state_dict(state_dict["normalizer"])
|
| 351 |
+
|
| 352 |
+
def set_actions(self, action: torch.Tensor):
|
| 353 |
+
pass
|
| 354 |
+
|
| 355 |
+
def reset(self):
|
| 356 |
+
pass
|
ddpm/latest.ckpt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:af0076703b823c77145c56da3234220b9aa4336fac725ebccab39e6c559b3e73
|
| 3 |
+
size 697958870
|
ddpm/policy_config.yaml
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
_target_: workspaces.train_bfn_workspace.TrainBFNWorkspace
|
| 2 |
+
name: train_ddpm_pusht_xarm_top
|
| 3 |
+
task_name: pusht_xarm
|
| 4 |
+
exp_name: ddpm_top
|
| 5 |
+
shape_meta:
|
| 6 |
+
obs:
|
| 7 |
+
camera_0:
|
| 8 |
+
shape:
|
| 9 |
+
- 3
|
| 10 |
+
- 224
|
| 11 |
+
- 224
|
| 12 |
+
type: rgb
|
| 13 |
+
action:
|
| 14 |
+
shape:
|
| 15 |
+
- 9
|
| 16 |
+
horizon: 16
|
| 17 |
+
n_obs_steps: 2
|
| 18 |
+
n_action_steps: 8
|
| 19 |
+
n_latency_steps: 0
|
| 20 |
+
dataset_obs_steps: 2
|
| 21 |
+
past_action_visible: false
|
| 22 |
+
obs_as_global_cond: true
|
| 23 |
+
policy:
|
| 24 |
+
_target_: diffusion_policy.policy.diffusion_unet_hybrid_image_policy.DiffusionUnetHybridImagePolicy
|
| 25 |
+
shape_meta: ${shape_meta}
|
| 26 |
+
horizon: ${horizon}
|
| 27 |
+
n_action_steps: ${n_action_steps}
|
| 28 |
+
n_obs_steps: ${n_obs_steps}
|
| 29 |
+
noise_scheduler:
|
| 30 |
+
_target_: diffusers.schedulers.scheduling_ddpm.DDPMScheduler
|
| 31 |
+
num_train_timesteps: 100
|
| 32 |
+
beta_start: 0.0001
|
| 33 |
+
beta_end: 0.02
|
| 34 |
+
beta_schedule: squaredcos_cap_v2
|
| 35 |
+
variance_type: fixed_small
|
| 36 |
+
clip_sample: true
|
| 37 |
+
prediction_type: epsilon
|
| 38 |
+
num_inference_steps: 100
|
| 39 |
+
obs_as_global_cond: ${obs_as_global_cond}
|
| 40 |
+
obs_encoder_group_norm: true
|
| 41 |
+
eval_fixed_crop: true
|
| 42 |
+
crop_shape:
|
| 43 |
+
- 216
|
| 44 |
+
- 216
|
| 45 |
+
diffusion_step_embed_dim: 128
|
| 46 |
+
down_dims:
|
| 47 |
+
- 256
|
| 48 |
+
- 512
|
| 49 |
+
- 1024
|
| 50 |
+
kernel_size: 5
|
| 51 |
+
n_groups: 8
|
| 52 |
+
cond_predict_scale: true
|
| 53 |
+
task:
|
| 54 |
+
dataset:
|
| 55 |
+
_target_: dataset.pusht_xarm_dataset.PushTXArmDataset
|
| 56 |
+
zarr_path: data/pusht_xarm_merged/replay.zarr
|
| 57 |
+
horizon: ${horizon}
|
| 58 |
+
pad_before: 1
|
| 59 |
+
pad_after: 7
|
| 60 |
+
n_obs_steps: ${n_obs_steps}
|
| 61 |
+
seed: 42
|
| 62 |
+
val_ratio: 0.1
|
| 63 |
+
cameras:
|
| 64 |
+
- camera_0
|
| 65 |
+
action_mode: onehot
|
| 66 |
+
env_runner: null
|
| 67 |
+
ema:
|
| 68 |
+
_target_: diffusion_policy.model.diffusion.ema_model.EMAModel
|
| 69 |
+
update_after_step: 0
|
| 70 |
+
inv_gamma: 1.0
|
| 71 |
+
power: 0.75
|
| 72 |
+
min_value: 0.0
|
| 73 |
+
max_value: 0.9999
|
| 74 |
+
optimizer:
|
| 75 |
+
_target_: torch.optim.AdamW
|
| 76 |
+
lr: 0.0001
|
| 77 |
+
betas:
|
| 78 |
+
- 0.95
|
| 79 |
+
- 0.999
|
| 80 |
+
eps: 1.0e-08
|
| 81 |
+
weight_decay: 1.0e-06
|
| 82 |
+
training:
|
| 83 |
+
device: cuda:0
|
| 84 |
+
seed: 42
|
| 85 |
+
debug: false
|
| 86 |
+
resume: false
|
| 87 |
+
lr_scheduler: cosine
|
| 88 |
+
lr_warmup_steps: 500
|
| 89 |
+
num_epochs: 200
|
| 90 |
+
gradient_accumulate_every: 1
|
| 91 |
+
use_ema: true
|
| 92 |
+
rollout_every: 50
|
| 93 |
+
checkpoint_every: 50
|
| 94 |
+
val_every: 1
|
| 95 |
+
sample_every: 5
|
| 96 |
+
max_train_steps: null
|
| 97 |
+
max_val_steps: null
|
| 98 |
+
tqdm_interval_sec: 1.0
|
| 99 |
+
dataloader:
|
| 100 |
+
batch_size: 32
|
| 101 |
+
num_workers: 4
|
| 102 |
+
shuffle: true
|
| 103 |
+
pin_memory: true
|
| 104 |
+
persistent_workers: false
|
| 105 |
+
val_dataloader:
|
| 106 |
+
batch_size: 32
|
| 107 |
+
num_workers: 2
|
| 108 |
+
shuffle: false
|
| 109 |
+
pin_memory: true
|
| 110 |
+
persistent_workers: false
|
| 111 |
+
checkpoint:
|
| 112 |
+
topk:
|
| 113 |
+
monitor_key: train_loss
|
| 114 |
+
mode: min
|
| 115 |
+
k: 1
|
| 116 |
+
format_str: epoch={epoch:04d}-train_loss={train_loss:.4f}.ckpt
|
| 117 |
+
save_last_ckpt: true
|
| 118 |
+
save_last_snapshot: false
|
| 119 |
+
logging:
|
| 120 |
+
project: pusht_xarm_ddpm
|
| 121 |
+
resume: true
|
| 122 |
+
mode: offline
|
| 123 |
+
name: ${now:%Y.%m.%d-%H.%M.%S}_${name}
|
| 124 |
+
tags:
|
| 125 |
+
- ddpm
|
| 126 |
+
- pusht_xarm
|
| 127 |
+
- onehot
|
| 128 |
+
- top
|
| 129 |
+
id: null
|
| 130 |
+
group: null
|
| 131 |
+
multi_run:
|
| 132 |
+
run_dir: data/outputs/${now:%Y.%m.%d}/${now:%H.%M.%S}_${name}
|
| 133 |
+
wandb_name_base: ${now:%Y.%m.%d-%H.%M.%S}_${name}
|
inference.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Standalone real-robot inference for PushT-xarm BFN-hybrid policy.
|
| 2 |
+
|
| 3 |
+
Inputs each step:
|
| 4 |
+
cam0: HxWx3 uint8 RGB image from the top camera (any size; will be resized to 224x224)
|
| 5 |
+
cam0_prev: same, one step earlier (n_obs_steps=2)
|
| 6 |
+
|
| 7 |
+
Output per step (predicts horizon=16, returns next n_action_steps=8):
|
| 8 |
+
direction: int in {0..7}
|
| 9 |
+
distance: float in [0, 50]
|
| 10 |
+
"""
|
| 11 |
+
import argparse
|
| 12 |
+
import sys
|
| 13 |
+
import time
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
|
| 16 |
+
import numpy as np
|
| 17 |
+
import torch
|
| 18 |
+
import yaml
|
| 19 |
+
from PIL import Image
|
| 20 |
+
|
| 21 |
+
THIS_DIR = Path(__file__).resolve().parent
|
| 22 |
+
sys.path.insert(0, str(THIS_DIR))
|
| 23 |
+
|
| 24 |
+
from bfn_hybrid_image_policy import BFNHybridImagePolicy # noqa: E402
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def load_bfn_policy(ckpt_path: str, config_path: str, device: str = "cuda"):
|
| 28 |
+
with open(config_path) as f:
|
| 29 |
+
cfg = yaml.safe_load(f)
|
| 30 |
+
pcfg = cfg["policy"]
|
| 31 |
+
policy = BFNHybridImagePolicy(
|
| 32 |
+
shape_meta=cfg["shape_meta"],
|
| 33 |
+
horizon=cfg["horizon"],
|
| 34 |
+
n_action_steps=cfg["n_action_steps"],
|
| 35 |
+
n_obs_steps=cfg["n_obs_steps"],
|
| 36 |
+
num_discrete_actions=pcfg.get("num_discrete_actions", 8),
|
| 37 |
+
continuous_param_dim=pcfg.get("continuous_param_dim", 1),
|
| 38 |
+
sigma_1=pcfg.get("sigma_1", 0.001),
|
| 39 |
+
beta_1=pcfg.get("beta_1", 0.2),
|
| 40 |
+
n_timesteps=pcfg.get("n_timesteps", 20),
|
| 41 |
+
crop_shape=tuple(pcfg.get("crop_shape", [216, 216])),
|
| 42 |
+
obs_encoder_group_norm=pcfg.get("obs_encoder_group_norm", True),
|
| 43 |
+
eval_fixed_crop=pcfg.get("eval_fixed_crop", True),
|
| 44 |
+
diffusion_step_embed_dim=pcfg.get("diffusion_step_embed_dim", 128),
|
| 45 |
+
down_dims=tuple(pcfg.get("down_dims", [256, 512, 1024])),
|
| 46 |
+
kernel_size=pcfg.get("kernel_size", 5),
|
| 47 |
+
n_groups=pcfg.get("n_groups", 8),
|
| 48 |
+
cond_predict_scale=pcfg.get("cond_predict_scale", True),
|
| 49 |
+
)
|
| 50 |
+
ckpt = torch.load(ckpt_path, map_location=device, weights_only=False)
|
| 51 |
+
state = ckpt["state_dicts"]["model"] if "state_dicts" in ckpt else ckpt
|
| 52 |
+
policy.load_state_dict(state)
|
| 53 |
+
policy.to(device).eval()
|
| 54 |
+
return policy
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def preprocess_image(img: np.ndarray) -> np.ndarray:
|
| 58 |
+
"""Resize HxWx3 uint8 -> 3x224x224 float32 in [0,1]."""
|
| 59 |
+
if img.shape[:2] != (224, 224):
|
| 60 |
+
img = np.array(Image.fromarray(img).resize((224, 224), Image.BILINEAR))
|
| 61 |
+
img = img.astype(np.float32) / 255.0
|
| 62 |
+
return img.transpose(2, 0, 1)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def infer_step(policy, cam0_now: np.ndarray, cam0_prev: np.ndarray, device: str = "cuda"):
|
| 66 |
+
"""One inference call. Returns a list of dicts [{direction, distance}, ...] of length n_action_steps."""
|
| 67 |
+
a = preprocess_image(cam0_prev)
|
| 68 |
+
b = preprocess_image(cam0_now)
|
| 69 |
+
obs = torch.from_numpy(np.stack([a, b])).unsqueeze(0).to(device) # [1, 2, 3, 224, 224]
|
| 70 |
+
with torch.no_grad():
|
| 71 |
+
out = policy.predict_action({"camera_0": obs})
|
| 72 |
+
actions = out["action"][0].cpu().numpy() # [n_action_steps, 2] = [direction, distance]
|
| 73 |
+
return [
|
| 74 |
+
{"direction": int(round(a[0])) % 8, "distance": float(np.clip(a[1], 0, 50))}
|
| 75 |
+
for a in actions
|
| 76 |
+
]
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def main():
|
| 80 |
+
p = argparse.ArgumentParser()
|
| 81 |
+
p.add_argument("--ckpt", required=True)
|
| 82 |
+
p.add_argument("--config", required=True)
|
| 83 |
+
p.add_argument("--device", default="cuda")
|
| 84 |
+
args = p.parse_args()
|
| 85 |
+
|
| 86 |
+
print(f"Loading policy from {args.ckpt}...")
|
| 87 |
+
policy = load_bfn_policy(args.ckpt, args.config, args.device)
|
| 88 |
+
print("Policy loaded.")
|
| 89 |
+
|
| 90 |
+
# Dummy roundtrip test
|
| 91 |
+
dummy = np.random.randint(0, 255, (224, 224, 3), dtype=np.uint8)
|
| 92 |
+
t0 = time.time()
|
| 93 |
+
actions = infer_step(policy, dummy, dummy, args.device)
|
| 94 |
+
dt = (time.time() - t0) * 1000
|
| 95 |
+
print(f"Smoke test: {len(actions)} actions in {dt:.1f} ms")
|
| 96 |
+
print(f"First action: {actions[0]}")
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
if __name__ == "__main__":
|
| 100 |
+
main()
|
networks/__init__.py
ADDED
|
File without changes
|
networks/base.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (C) 2023 Maxime Robeyns <dev@maximerobeyns.com>
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Base network classes for torch_bfn, as well as some utilities"""
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
import math
|
| 18 |
+
import torch as t
|
| 19 |
+
import torch.nn as nn
|
| 20 |
+
|
| 21 |
+
from abc import abstractmethod
|
| 22 |
+
from typing import Optional
|
| 23 |
+
from functools import partial
|
| 24 |
+
|
| 25 |
+
try:
|
| 26 |
+
from torchtyping import TensorType as Tensor
|
| 27 |
+
except ImportError: # pragma: no cover - optional dependency
|
| 28 |
+
|
| 29 |
+
class _TensorAlias:
|
| 30 |
+
def __class_getitem__(cls, key):
|
| 31 |
+
return t.Tensor
|
| 32 |
+
|
| 33 |
+
Tensor = _TensorAlias
|
| 34 |
+
|
| 35 |
+
__all__ = [
|
| 36 |
+
"BFNetwork",
|
| 37 |
+
"DiscreteBFNetwork",
|
| 38 |
+
"SinusoidalPosEmb",
|
| 39 |
+
"RandomOrLearnedSinusoidalPosEmb",
|
| 40 |
+
]
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class BFNetwork(nn.Module):
|
| 44 |
+
"""
|
| 45 |
+
Abstraact base class for neural networks (nn.Module) for use with
|
| 46 |
+
torch_bfn.
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
def __init__(self, is_conditional_model: bool = False):
|
| 50 |
+
super().__init__()
|
| 51 |
+
self.is_conditional_model = is_conditional_model
|
| 52 |
+
|
| 53 |
+
@abstractmethod
|
| 54 |
+
def forward(
|
| 55 |
+
self,
|
| 56 |
+
x: Tensor["B", "D"],
|
| 57 |
+
time: Tensor["B"],
|
| 58 |
+
cond: Optional[Tensor["B", "C"]] = None,
|
| 59 |
+
cond_drop_prob: Optional[float] = None,
|
| 60 |
+
) -> Tensor["B", "D"]:
|
| 61 |
+
"""Returns a value of the same shape as x (e.g. predicts the noise
|
| 62 |
+
applied to x) at time t with optional conditioning information.
|
| 63 |
+
|
| 64 |
+
Args:
|
| 65 |
+
x: the current parameter vector
|
| 66 |
+
time: current timestep
|
| 67 |
+
cond: conditioning information
|
| 68 |
+
cond_drop_prob: probability of dropping conditioning info out for
|
| 69 |
+
classifier-free guidance.
|
| 70 |
+
|
| 71 |
+
Returns:
|
| 72 |
+
Tensor["B", "D"]: updated parameter vector
|
| 73 |
+
"""
|
| 74 |
+
raise NotImplementedError
|
| 75 |
+
|
| 76 |
+
def forward_with_cond_scale(
|
| 77 |
+
self, *args, cond_scale=1.0, rescaled_phi=0.0, **kwargs
|
| 78 |
+
) -> Tensor["B", "D"]:
|
| 79 |
+
"""For conditional sampling, this additionally scales the conditional
|
| 80 |
+
guidance, and sharpens phi.
|
| 81 |
+
|
| 82 |
+
This abstract class just invokes the forward method as a fallback.
|
| 83 |
+
"""
|
| 84 |
+
logits = self.forward(*args, cond_drop_prob=0.0, **kwargs)
|
| 85 |
+
if cond_scale == 1.0:
|
| 86 |
+
return logits
|
| 87 |
+
|
| 88 |
+
null_logits = self.forward(*args, cond_drop_prob=1.0, **kwargs)
|
| 89 |
+
scaled_logits = null_logits + (logits - null_logits) * cond_scale
|
| 90 |
+
|
| 91 |
+
if rescaled_phi == 0.0:
|
| 92 |
+
return scaled_logits
|
| 93 |
+
|
| 94 |
+
std_fn = partial(t.std, dim=tuple(range(1, scaled_logits.ndim)), keepdim=True)
|
| 95 |
+
rescaled_logits = scaled_logits * (std_fn(logits) / std_fn(scaled_logits))
|
| 96 |
+
return rescaled_logits * rescaled_phi + scaled_logits * (1.0 - rescaled_phi)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
class DiscreteBFNetwork(BFNetwork):
|
| 100 |
+
"""
|
| 101 |
+
For discrete variants of BFNs, we require networks to use the last tensor
|
| 102 |
+
dimemsion as the class label dimension, as is conventional in transformer
|
| 103 |
+
models for language.
|
| 104 |
+
"""
|
| 105 |
+
|
| 106 |
+
@abstractmethod
|
| 107 |
+
def forward(
|
| 108 |
+
self,
|
| 109 |
+
x: Tensor["B", "D", "K"],
|
| 110 |
+
time: Tensor["B"],
|
| 111 |
+
cond: Optional[Tensor["B", "C"]],
|
| 112 |
+
cond_drop_prob: Optional[float] = None,
|
| 113 |
+
) -> Tensor["B", "D", "K"]:
|
| 114 |
+
raise NotImplementedError
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
class SinusoidalPosEmb(nn.Module):
|
| 118 |
+
def __init__(self, dim: int):
|
| 119 |
+
super().__init__()
|
| 120 |
+
self.dim = dim
|
| 121 |
+
|
| 122 |
+
def forward(self, x: t.Tensor) -> t.Tensor:
|
| 123 |
+
half_dim = self.dim // 2
|
| 124 |
+
emb = math.log(10000) / (half_dim - 1)
|
| 125 |
+
emb = t.exp(t.arange(half_dim, device=x.device) * -emb)
|
| 126 |
+
emb = x[:, None] * emb[None, :]
|
| 127 |
+
emb = t.cat((emb.sin(), emb.cos()), dim=-1)
|
| 128 |
+
return emb
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
class RandomOrLearnedSinusoidalPosEmb(nn.Module):
|
| 132 |
+
"""
|
| 133 |
+
https://github.com/crowsonkb/v-diffusion-jax/blob/master/diffusion/models/danbooru_128.py#L8
|
| 134 |
+
"""
|
| 135 |
+
|
| 136 |
+
def __init__(self, dim: int, is_random: bool = False):
|
| 137 |
+
super().__init__()
|
| 138 |
+
assert dim % 2 == 0, "Sinusoidal positional embedding dim must be even"
|
| 139 |
+
half_dim = dim // 2
|
| 140 |
+
self.weights = nn.Parameter(t.randn(half_dim), requires_grad=not is_random)
|
| 141 |
+
|
| 142 |
+
def forward(self, x: Tensor["B", 1]) -> Tensor["B", "dim+1"]:
|
| 143 |
+
freqs = x * self.weights[None, :] * 2 * math.pi
|
| 144 |
+
fouriered = t.cat((freqs.sin(), freqs.cos()), -1)
|
| 145 |
+
fouriered = t.cat((x, fouriered), -1)
|
| 146 |
+
return fouriered
|
policies/__init__.py
ADDED
|
File without changes
|
policies/base.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Base class for all robotics policies.
|
| 2 |
+
|
| 3 |
+
This module defines the abstract base class `BasePolicy`, which standardizes
|
| 4 |
+
the interface for environment interaction (`act`), model inference (`forward`),
|
| 5 |
+
training (`compute_loss`), and device/data management.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from abc import ABC, abstractmethod
|
| 11 |
+
from typing import Any, Dict, List, Mapping, Optional, Tuple, Union
|
| 12 |
+
|
| 13 |
+
import numpy as np
|
| 14 |
+
import torch
|
| 15 |
+
import torch.nn as nn
|
| 16 |
+
|
| 17 |
+
from utils.bfn_utils import str_to_torch_dtype
|
| 18 |
+
|
| 19 |
+
__all__ = ["BasePolicy"]
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class BasePolicy(nn.Module, ABC):
|
| 23 |
+
"""Abstract base class for robotics policies.
|
| 24 |
+
|
| 25 |
+
Provides shared functionality for:
|
| 26 |
+
1. Device and dtype management.
|
| 27 |
+
2. Data conversion (Numpy <-> Torch).
|
| 28 |
+
3. Automatic batch dimension handling.
|
| 29 |
+
4. Action clipping and normalization hooks.
|
| 30 |
+
|
| 31 |
+
Subclasses must implement:
|
| 32 |
+
- `forward(obs, ...)`: The core PyTorch inference logic.
|
| 33 |
+
- `compute_loss(batch)`: The training logic (optional, raises NotImplementedError by default).
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
def __init__(
|
| 37 |
+
self,
|
| 38 |
+
action_space: Any,
|
| 39 |
+
*,
|
| 40 |
+
device: str = "cpu",
|
| 41 |
+
dtype: str = "float32",
|
| 42 |
+
clip_actions: bool = True,
|
| 43 |
+
normalizer: Optional[Any] = None,
|
| 44 |
+
):
|
| 45 |
+
"""Initializes the BasePolicy.
|
| 46 |
+
|
| 47 |
+
Args:
|
| 48 |
+
action_space: The Gym action space (used for clipping).
|
| 49 |
+
device: Default device to place tensors on ('cpu', 'cuda').
|
| 50 |
+
dtype: Default dtype for tensor creation ('float32', 'float16').
|
| 51 |
+
clip_actions: Whether to clamp output actions to the action_space bounds.
|
| 52 |
+
normalizer: Optional normalization module (e.g., LinearNormalizer).
|
| 53 |
+
"""
|
| 54 |
+
super().__init__()
|
| 55 |
+
self.action_space = action_space
|
| 56 |
+
self._device = torch.device(device)
|
| 57 |
+
self._dtype = str_to_torch_dtype(dtype)
|
| 58 |
+
self.clip_actions = clip_actions
|
| 59 |
+
self.normalizer = normalizer
|
| 60 |
+
|
| 61 |
+
# --- Properties ---
|
| 62 |
+
|
| 63 |
+
@property
|
| 64 |
+
def device(self) -> torch.device:
|
| 65 |
+
"""Returns the current device of the policy.
|
| 66 |
+
|
| 67 |
+
Infers device from the first parameter if available (robust to .to() calls),
|
| 68 |
+
otherwise falls back to the initialization value.
|
| 69 |
+
"""
|
| 70 |
+
try:
|
| 71 |
+
return next(self.parameters()).device
|
| 72 |
+
except StopIteration:
|
| 73 |
+
return self._device
|
| 74 |
+
|
| 75 |
+
@property
|
| 76 |
+
def dtype(self) -> torch.dtype:
|
| 77 |
+
"""Returns the current dtype of the policy."""
|
| 78 |
+
try:
|
| 79 |
+
return next(self.parameters()).dtype
|
| 80 |
+
except StopIteration:
|
| 81 |
+
return self._dtype
|
| 82 |
+
|
| 83 |
+
# --- Public Interface ---
|
| 84 |
+
|
| 85 |
+
def set_normalizer(self, normalizer: Any) -> None:
|
| 86 |
+
"""Updates the normalizer used by the policy."""
|
| 87 |
+
self.normalizer = normalizer
|
| 88 |
+
|
| 89 |
+
@abstractmethod
|
| 90 |
+
def forward(
|
| 91 |
+
self,
|
| 92 |
+
obs: Union[torch.Tensor, Dict[str, torch.Tensor]],
|
| 93 |
+
*,
|
| 94 |
+
deterministic: bool = False,
|
| 95 |
+
**kwargs: Any,
|
| 96 |
+
) -> torch.Tensor:
|
| 97 |
+
"""Core inference method returning a batch of actions.
|
| 98 |
+
|
| 99 |
+
Args:
|
| 100 |
+
obs: Observations with leading batch dimension [B, ...].
|
| 101 |
+
deterministic: Whether to sample deterministically (policy dependent).
|
| 102 |
+
**kwargs: Additional arguments (e.g., conditioning info).
|
| 103 |
+
|
| 104 |
+
Returns:
|
| 105 |
+
Action tensor of shape [B, ActionDim].
|
| 106 |
+
"""
|
| 107 |
+
raise NotImplementedError
|
| 108 |
+
|
| 109 |
+
def compute_loss(self, batch: Any) -> torch.Tensor:
|
| 110 |
+
"""Computes training loss for the policy.
|
| 111 |
+
|
| 112 |
+
Args:
|
| 113 |
+
batch: A batch of data (usually containing 'obs', 'action').
|
| 114 |
+
|
| 115 |
+
Returns:
|
| 116 |
+
Scalar loss tensor.
|
| 117 |
+
|
| 118 |
+
Raises:
|
| 119 |
+
NotImplementedError: If the policy does not support internal training logic.
|
| 120 |
+
"""
|
| 121 |
+
raise NotImplementedError(
|
| 122 |
+
f"compute_loss is not implemented for {self.__class__.__name__}."
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
@torch.inference_mode()
|
| 126 |
+
def act(
|
| 127 |
+
self,
|
| 128 |
+
obs: Any,
|
| 129 |
+
*,
|
| 130 |
+
deterministic: bool = False,
|
| 131 |
+
return_torch: bool = False,
|
| 132 |
+
**kwargs: Any,
|
| 133 |
+
) -> Union[np.ndarray, torch.Tensor]:
|
| 134 |
+
"""Convenience wrapper for environment interaction.
|
| 135 |
+
|
| 136 |
+
Handles:
|
| 137 |
+
1. conversion of numpy obs -> torch tensors.
|
| 138 |
+
2. adding batch dimension if missing.
|
| 139 |
+
3. inference via `forward`.
|
| 140 |
+
4. clipping actions.
|
| 141 |
+
5. conversion of torch output -> numpy action (optional).
|
| 142 |
+
|
| 143 |
+
Args:
|
| 144 |
+
obs: Observation from the environment (Numpy array, Dict, or Tensor).
|
| 145 |
+
deterministic: Whether to use deterministic mode.
|
| 146 |
+
return_torch: If True, returns a Tensor on device; else returns Numpy array.
|
| 147 |
+
**kwargs: Passed to `forward`.
|
| 148 |
+
|
| 149 |
+
Returns:
|
| 150 |
+
Action (Numpy array or Tensor).
|
| 151 |
+
"""
|
| 152 |
+
# 1. Convert to Tensor
|
| 153 |
+
obs_t = self._to_tensor(obs)
|
| 154 |
+
|
| 155 |
+
# 2. Auto-Batching
|
| 156 |
+
# We assume environment interaction usually provides a single unbatched observation
|
| 157 |
+
obs_t, batch_added = self._maybe_add_batch_dim(obs_t)
|
| 158 |
+
|
| 159 |
+
# 3. Inference
|
| 160 |
+
action = self.forward(obs_t, deterministic=deterministic, **kwargs)
|
| 161 |
+
|
| 162 |
+
# 4. Clipping
|
| 163 |
+
action = self._clip_actions(action)
|
| 164 |
+
|
| 165 |
+
# 5. Return
|
| 166 |
+
if return_torch:
|
| 167 |
+
return action
|
| 168 |
+
|
| 169 |
+
# Remove batch dim if we added it
|
| 170 |
+
if batch_added:
|
| 171 |
+
action = action.squeeze(0)
|
| 172 |
+
|
| 173 |
+
return action.detach().cpu().numpy()
|
| 174 |
+
|
| 175 |
+
# --- Internal Helpers ---
|
| 176 |
+
|
| 177 |
+
def _to_tensor(self, data: Any) -> Any:
|
| 178 |
+
"""Recursively converts input data to tensors on the correct device/dtype."""
|
| 179 |
+
if isinstance(data, torch.Tensor):
|
| 180 |
+
return data.to(device=self.device, dtype=self.dtype)
|
| 181 |
+
|
| 182 |
+
if isinstance(data, Mapping):
|
| 183 |
+
return {k: self._to_tensor(v) for k, v in data.items()}
|
| 184 |
+
|
| 185 |
+
if isinstance(data, (list, tuple)):
|
| 186 |
+
return type(data)(self._to_tensor(v) for v in data)
|
| 187 |
+
|
| 188 |
+
# Fallback for numpy arrays / scalars
|
| 189 |
+
return torch.as_tensor(data, device=self.device, dtype=self.dtype)
|
| 190 |
+
|
| 191 |
+
def _maybe_add_batch_dim(self, obs: Any) -> Tuple[Any, bool]:
|
| 192 |
+
"""Adds a leading batch dimension if the input appears to be unbatched.
|
| 193 |
+
|
| 194 |
+
Note: This uses a heuristic. If the input is a Tensor, we assume it is
|
| 195 |
+
unbatched if it matches the observation space shape (not implemented here generic enough)
|
| 196 |
+
OR we rely on the caller context (usually `act` is single-step).
|
| 197 |
+
|
| 198 |
+
Here, we unconditionally unsqueeze dim 0 for `act` convenience.
|
| 199 |
+
"""
|
| 200 |
+
batch_added = False
|
| 201 |
+
|
| 202 |
+
if isinstance(obs, torch.Tensor):
|
| 203 |
+
# Heuristic: We assume `act` is called with single observations.
|
| 204 |
+
# For robust batch detection, one would check obs_space.shape.
|
| 205 |
+
# Here we simply unsqueeze to ensure [1, ...] shape.
|
| 206 |
+
obs = obs.unsqueeze(0)
|
| 207 |
+
batch_added = True
|
| 208 |
+
|
| 209 |
+
elif isinstance(obs, Mapping):
|
| 210 |
+
# Handle Dict inputs (e.g. {'image': ..., 'state': ...})
|
| 211 |
+
# Only unsqueeze tensors.
|
| 212 |
+
new_obs = {}
|
| 213 |
+
for k, v in obs.items():
|
| 214 |
+
if isinstance(v, torch.Tensor):
|
| 215 |
+
new_obs[k] = v.unsqueeze(0)
|
| 216 |
+
batch_added = True # Mark true if ANY tensor was unsqueezed
|
| 217 |
+
else:
|
| 218 |
+
new_obs[k] = v
|
| 219 |
+
obs = new_obs
|
| 220 |
+
|
| 221 |
+
return obs, batch_added
|
| 222 |
+
|
| 223 |
+
def _clip_actions(self, action: torch.Tensor) -> torch.Tensor:
|
| 224 |
+
"""Clips actions to the environment bounds if `clip_actions` is True."""
|
| 225 |
+
if not self.clip_actions:
|
| 226 |
+
return action
|
| 227 |
+
|
| 228 |
+
# Check if action space has bounds
|
| 229 |
+
if not hasattr(self.action_space, "low") or not hasattr(
|
| 230 |
+
self.action_space, "high"
|
| 231 |
+
):
|
| 232 |
+
return action
|
| 233 |
+
|
| 234 |
+
# Create tensor bounds on the fly (caching could be an optimization)
|
| 235 |
+
low = torch.as_tensor(
|
| 236 |
+
self.action_space.low, device=action.device, dtype=action.dtype
|
| 237 |
+
)
|
| 238 |
+
high = torch.as_tensor(
|
| 239 |
+
self.action_space.high, device=action.device, dtype=action.dtype
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
return torch.clamp(action, low, high)
|
requirements.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch>=2.0
|
| 2 |
+
torchvision
|
| 3 |
+
diffusers>=0.18
|
| 4 |
+
einops
|
| 5 |
+
numpy
|
| 6 |
+
zarr
|
| 7 |
+
pyyaml
|
| 8 |
+
pillow
|
| 9 |
+
hydra-core
|
| 10 |
+
robomimic
|
| 11 |
+
# diffusion-policy library is needed for DDPM/DDIM/EDM/Consistency policies.
|
| 12 |
+
# Install from source: pip install git+https://github.com/real-stanford/diffusion_policy.git
|