File size: 14,929 Bytes
752c314
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
"""BFN Hybrid Image Policy: image observations + categorical-discrete + continuous action heads.

This is the policy for real-robot PushT with hybrid action space:
- Discrete: 8 push directions
- Continuous: push distance
- Observation: one or more RGB cameras (cam0 top, cam1 side)
"""
from __future__ import annotations

import math
from typing import Any, Dict, List, Optional, Tuple, Union

import torch
import torch.nn as nn
import torch.nn.functional as F

from diffusion_policy.model.common.normalizer import LinearNormalizer
from diffusion_policy.model.diffusion.conditional_unet1d import ConditionalUnet1D
from diffusion_policy.common.pytorch_util import dict_apply

from policies.base import BasePolicy
from networks.base import BFNetwork

try:
    import robomimic.models.obs_core as rmbn
    import diffusion_policy.model.vision.crop_randomizer as dmvc
    from diffusion_policy.common.pytorch_util import replace_submodules
    import robomimic.utils.obs_utils as ObsUtils
    from robomimic.config import config_factory
    from robomimic.algo import algo_factory, PolicyAlgo
    from diffusion_policy.common.robomimic_config_util import get_robomimic_config
    HAS_ROBOMIMIC = True
except ImportError:
    HAS_ROBOMIMIC = False


__all__ = ["BFNHybridImagePolicy"]


class HybridUnetWrapper(BFNetwork):
    def __init__(self, model, horizon, continuous_dim, discrete_configs, cond_dim):
        super().__init__(is_conditional_model=True)
        self.model = model
        self.horizon = horizon
        self.continuous_dim = continuous_dim
        self.discrete_configs = discrete_configs
        self.cond_dim = cond_dim
        self.cond_is_discrete = False
        total_disc = sum(n for _, n in discrete_configs)
        self.input_dim = continuous_dim + total_disc

    def forward(self, x, t, cond=None):
        B = x.shape[0]
        x = x.view(B, self.horizon, self.input_dim)
        out = self.model(x, t, global_cond=cond)
        return out.reshape(B, -1)


class BFNHybridImagePolicy(BasePolicy):
    """BFN policy with image observations + hybrid (categorical + continuous) action head."""

    def __init__(
        self,
        shape_meta: dict,
        horizon: int = 16,
        n_action_steps: int = 8,
        n_obs_steps: int = 2,
        num_discrete_actions: int = 8,
        continuous_param_dim: int = 1,
        sigma_1: float = 0.001,
        beta_1: float = 0.2,
        n_timesteps: int = 20,
        crop_shape: tuple = (216, 216),
        obs_encoder_group_norm: bool = True,
        eval_fixed_crop: bool = True,
        diffusion_step_embed_dim: int = 128,
        down_dims: tuple = (256, 512, 1024),
        kernel_size: int = 5,
        n_groups: int = 8,
        cond_predict_scale: bool = True,
        device: str = "cpu",
        dtype: str = "float32",
        clip_actions: bool = True,
        **kwargs,
    ):
        super().__init__(action_space=None, device=device, dtype=dtype, clip_actions=clip_actions)

        self.horizon = horizon
        self.n_action_steps = n_action_steps
        self.n_obs_steps = n_obs_steps
        self.num_discrete_actions = num_discrete_actions
        self.continuous_dim = continuous_param_dim
        self.discrete_configs = [(0, num_discrete_actions)]
        self.discrete_action_indices = {0}
        self.total_action_dim = 1 + continuous_param_dim
        self.sigma_1 = sigma_1
        self.beta_1 = beta_1
        self.n_timesteps = n_timesteps

        # Parse shape_meta for image obs keys
        obs_shape_meta = shape_meta["obs"]
        obs_config = {"low_dim": [], "rgb": [], "depth": [], "scan": []}
        obs_key_shapes = {}
        self.rgb_keys: List[str] = []
        for key, attr in obs_shape_meta.items():
            obs_key_shapes[key] = list(attr["shape"])
            t = attr.get("type", "low_dim")
            if t == "rgb":
                obs_config["rgb"].append(key)
                self.rgb_keys.append(key)
            elif t == "low_dim":
                obs_config["low_dim"].append(key)
            else:
                raise ValueError(f"Unsupported obs type: {t}")
        assert HAS_ROBOMIMIC, "robomimic required for image policy"

        self.obs_encoder = self._build_robomimic_encoder(
            obs_config, obs_key_shapes, crop_shape, obs_encoder_group_norm, eval_fixed_crop
        )
        obs_feature_dim = self.obs_encoder.output_shape()[0]
        global_cond_dim = obs_feature_dim * n_obs_steps

        # U-Net input/output dim = continuous + discrete-logits
        unet_dim = continuous_param_dim + num_discrete_actions

        self.model = ConditionalUnet1D(
            input_dim=unet_dim,
            local_cond_dim=None,
            global_cond_dim=global_cond_dim,
            diffusion_step_embed_dim=diffusion_step_embed_dim,
            down_dims=list(down_dims),
            kernel_size=kernel_size,
            n_groups=n_groups,
            cond_predict_scale=cond_predict_scale,
        )
        self.unet_wrapper = HybridUnetWrapper(
            model=self.model,
            horizon=horizon,
            continuous_dim=continuous_param_dim,
            discrete_configs=self.discrete_configs,
            cond_dim=global_cond_dim,
        )
        self.normalizer = LinearNormalizer()
        self.global_cond_dim = global_cond_dim

        print(f"BFN Hybrid Image Policy:")
        print(f"  cameras: {self.rgb_keys}")
        print(f"  discrete: {num_discrete_actions}, continuous: {continuous_param_dim}")
        print(f"  obs_feature_dim: {obs_feature_dim}, global_cond_dim: {global_cond_dim}")
        print(f"  U-Net params: {sum(p.numel() for p in self.model.parameters()):.2e}")
        print(f"  Vision params: {sum(p.numel() for p in self.obs_encoder.parameters()):.2e}")

    def _build_robomimic_encoder(self, obs_config, obs_key_shapes, crop_shape, group_norm, eval_fixed_crop):
        config = get_robomimic_config(algo_name="bc_rnn", hdf5_type="image", task_name="square", dataset_type="ph")
        with config.unlocked():
            config.observation.modalities.obs = obs_config
            if crop_shape is None:
                for key, modality in config.observation.encoder.items():
                    if modality.obs_randomizer_class == "CropRandomizer":
                        modality["obs_randomizer_class"] = None
            else:
                ch, cw = crop_shape
                for key, modality in config.observation.encoder.items():
                    if modality.obs_randomizer_class == "CropRandomizer":
                        modality.obs_randomizer_kwargs.crop_height = ch
                        modality.obs_randomizer_kwargs.crop_width = cw
        ObsUtils.initialize_obs_utils_with_config(config)
        policy: PolicyAlgo = algo_factory(
            algo_name=config.algo_name, config=config,
            obs_key_shapes=obs_key_shapes, ac_dim=1 + self.num_discrete_actions, device="cpu",
        )
        obs_encoder = policy.nets["policy"].nets["encoder"].nets["obs"]
        if group_norm:
            replace_submodules(
                root_module=obs_encoder,
                predicate=lambda x: isinstance(x, nn.BatchNorm2d),
                func=lambda x: nn.GroupNorm(num_groups=x.num_features // 16, num_channels=x.num_features),
            )
        if eval_fixed_crop:
            replace_submodules(
                root_module=obs_encoder,
                predicate=lambda x: isinstance(x, rmbn.CropRandomizer),
                func=lambda x: dmvc.CropRandomizer(
                    input_shape=x.input_shape, crop_height=x.crop_height,
                    crop_width=x.crop_width, num_crops=x.num_crops, pos_enc=x.pos_enc,
                ),
            )
        return obs_encoder

    def set_normalizer(self, normalizer: LinearNormalizer):
        self.normalizer.load_state_dict(normalizer.state_dict())

    def _encode_obs(self, nobs: Dict[str, torch.Tensor]) -> torch.Tensor:
        """Encode obs dict to [B, global_cond_dim]."""
        B = nobs[self.rgb_keys[0]].shape[0]
        To = self.n_obs_steps
        # Stack across time: build dict of [B*To, C, H, W]
        flat = {}
        for k, v in nobs.items():
            v_t = v[:, :To]  # [B, To, C, H, W]
            flat[k] = v_t.reshape(B * To, *v_t.shape[2:])
        feats = self.obs_encoder(flat)  # [B*To, feat_dim]
        feats = feats.reshape(B, To, -1)
        return feats.reshape(B, -1)

    def forward(self, obs, *, deterministic: bool = False, **kwargs):
        if isinstance(obs, torch.Tensor):
            obs = {"obs": obs}
        return self.predict_action(obs)["action"]

    def predict_action(self, obs_dict: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]:
        nobs = self.normalizer.normalize(obs_dict)
        cond = self._encode_obs(nobs)
        B = cond.shape[0]
        device = cond.device
        dtype = cond.dtype
        naction = self._sample_hybrid_bfn(B, self.horizon, cond, device, dtype)
        start = self.n_obs_steps - 1
        end = start + self.n_action_steps
        action = naction[:, start:end]
        action_unnorm = action.clone()
        if action.shape[-1] > 1:
            full_unnorm = self.normalizer["action"].unnormalize(action.clone())
            action_unnorm[:, :, 1:] = full_unnorm[:, :, 1:]
        return {"action": action_unnorm, "action_pred": naction}

    @torch.no_grad()
    def _sample_hybrid_bfn(self, B, T, cond, device, dtype):
        n_steps = self.n_timesteps
        cont_dim = self.continuous_dim
        disc_configs = self.discrete_configs

        mu_cont = torch.zeros(B, T, cont_dim, device=device, dtype=dtype)
        rho_cont = 1.0
        theta_list = [
            torch.full((B, T, n), 1.0 / n, device=device, dtype=dtype) for _, n in disc_configs
        ]

        for i in range(1, n_steps + 1):
            t_val = (i - 1) / n_steps
            t_batch = torch.full((B,), t_val, device=device, dtype=dtype)
            net_input = torch.cat([mu_cont, *theta_list], dim=-1) if theta_list else mu_cont
            out_flat = self.unet_wrapper(net_input.reshape(B, -1), t_batch, cond=cond)
            out = out_flat.reshape(B, T, -1)

            x_cont_pred = out[:, :, :cont_dim]
            alpha_cont = (self.sigma_1 ** (-2.0 * i / n_steps)) * (1.0 - self.sigma_1 ** (2.0 / n_steps))
            sender_std = 1.0 / (alpha_cont ** 0.5 + 1e-8)
            y_cont = x_cont_pred + sender_std * torch.randn_like(x_cont_pred)
            new_rho = rho_cont + alpha_cont
            mu_cont = (rho_cont * mu_cont + alpha_cont * y_cont) / new_rho
            rho_cont = new_rho

            alpha_disc = self.beta_1 * (2 * i - 1) / (n_steps ** 2)
            offset = cont_dim
            new_theta_list = []
            for j, (_, n_classes) in enumerate(disc_configs):
                logits = out[:, :, offset:offset + n_classes]
                probs = torch.softmax(logits, dim=-1)
                probs_flat = probs.reshape(-1, n_classes)
                k_samples = torch.multinomial(probs_flat, num_samples=1).squeeze(-1).reshape(B, T)
                e_k = F.one_hot(k_samples, num_classes=n_classes).float()
                y_mean = alpha_disc * (n_classes * e_k - 1)
                y_std = (alpha_disc * n_classes + 1e-8) ** 0.5
                y_disc = y_mean + y_std * torch.randn_like(y_mean)
                log_theta = torch.log(theta_list[j] + 1e-8)
                theta_new = torch.softmax(log_theta + y_disc, dim=-1)
                new_theta_list.append(theta_new)
                offset += n_classes
            theta_list = new_theta_list

        # Final
        t_final = torch.ones(B, device=device, dtype=dtype)
        net_input = torch.cat([mu_cont, *theta_list], dim=-1) if theta_list else mu_cont
        out_final = self.unet_wrapper(net_input.reshape(B, -1), t_final, cond=cond).reshape(B, T, -1)
        x_cont_final = out_final[:, :, :cont_dim].clamp(-1.0, 1.0)

        disc_values = []
        offset = cont_dim
        for j, (_, n_classes) in enumerate(disc_configs):
            logits = out_final[:, :, offset:offset + n_classes]
            disc_values.append(logits.argmax(dim=-1).float().unsqueeze(-1))
            offset += n_classes

        if disc_values:
            return torch.cat([torch.cat(disc_values, dim=-1), x_cont_final], dim=-1)
        return x_cont_final

    def compute_loss(self, batch: Dict[str, torch.Tensor]) -> torch.Tensor:
        nobs = self.normalizer.normalize(batch["obs"])
        cond = self._encode_obs(nobs)
        raw_action = batch["action"]
        discrete_k = raw_action[:, :, 0].long()
        naction = self.normalizer["action"].normalize(raw_action)
        continuous_x = naction[:, :, 1:]

        B = raw_action.shape[0]
        T = self.horizon
        device = raw_action.device
        dtype = raw_action.dtype

        t = torch.rand(B, device=device, dtype=dtype).clamp(min=1e-5, max=1.0 - 1e-5)
        t_exp = t.view(B, 1, 1)
        gamma = 1.0 - (self.sigma_1 ** (2.0 * t_exp))
        var = gamma * (1.0 - gamma)
        std = (var + 1e-8).sqrt()
        mu_cont = gamma * continuous_x + std * torch.randn_like(continuous_x)

        beta = self.beta_1 * t_exp.pow(2.0)
        theta_list = []
        disc_targets = []
        for j, (_, n) in enumerate(self.discrete_configs):
            d = discrete_k.clamp(0, n - 1)
            disc_targets.append(d)
            e_x = F.one_hot(d, num_classes=n).float()
            mean = beta * (n * e_x - 1)
            std_disc = (beta * n + 1e-8).sqrt()
            y = mean + std_disc * torch.randn_like(mean)
            theta_list.append(torch.softmax(y, dim=-1))

        net_input = torch.cat([mu_cont, *theta_list], dim=-1) if theta_list else mu_cont
        out_flat = self.unet_wrapper(net_input.reshape(B, -1), t, cond=cond)
        out = out_flat.reshape(B, T, -1)

        x_cont_pred = out[:, :, :self.continuous_dim]
        cont_loss = (gamma * (continuous_x - x_cont_pred).pow(2.0)).mean()

        disc_loss = 0.0
        offset = self.continuous_dim
        for j, (_, n) in enumerate(self.discrete_configs):
            logits = out[:, :, offset:offset + n]
            disc_loss = disc_loss + F.cross_entropy(
                logits.reshape(-1, n), disc_targets[j].reshape(-1)
            )
            offset += n

        return cont_loss + disc_loss

    def state_dict(self):
        return {
            "obs_encoder": self.obs_encoder.state_dict(),
            "model": self.model.state_dict(),
            "normalizer": self.normalizer.state_dict(),
        }

    def load_state_dict(self, state_dict):
        self.obs_encoder.load_state_dict(state_dict["obs_encoder"])
        self.model.load_state_dict(state_dict["model"])
        if "normalizer" in state_dict:
            self.normalizer.load_state_dict(state_dict["normalizer"])

    def set_actions(self, action: torch.Tensor):
        pass

    def reset(self):
        pass