dennis96 commited on
Commit
0adab2f
·
verified ·
1 Parent(s): 07f85c4

Upload folder using huggingface_hub

Browse files
Files changed (2) hide show
  1. src/dataset.py +194 -0
  2. src/model.py +209 -0
src/dataset.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Dataset utilities for cached A2C2 BEHAVIOR/OpenPI parquet exports."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from pathlib import Path
7
+ import random
8
+ from typing import Iterator, NamedTuple
9
+
10
+ import numpy as np
11
+ import pyarrow as pa
12
+ import pyarrow.parquet as pq
13
+ import torch
14
+ from torch import Tensor
15
+ from torch.utils.data import IterableDataset, get_worker_info
16
+
17
+
18
+ class EpisodePair(NamedTuple):
19
+ data_path: Path
20
+ latent_path: Path
21
+
22
+
23
+ def resolve_dataset_root(path: Path) -> Path:
24
+ """Resolve either an A2C2 root or its parent directory."""
25
+
26
+ path = path.expanduser().resolve()
27
+ if (path / "data").is_dir() and (path / "latent" / "data").is_dir():
28
+ return path
29
+
30
+ candidates = [p for p in path.iterdir() if (p / "data").is_dir() and (p / "latent" / "data").is_dir()]
31
+ if len(candidates) == 1:
32
+ return candidates[0].resolve()
33
+ if not candidates:
34
+ raise FileNotFoundError(f"No A2C2 dataset root found under {path}")
35
+ names = ", ".join(str(p) for p in candidates)
36
+ raise ValueError(f"Multiple dataset roots found under {path}; pass one explicitly: {names}")
37
+
38
+
39
+ def discover_episode_pairs(dataset_root: Path, task_dir: str | None = None) -> list[EpisodePair]:
40
+ """Find matching data/latent parquet pairs."""
41
+
42
+ dataset_root = resolve_dataset_root(dataset_root)
43
+ pattern = f"{task_dir}/episode_*.parquet" if task_dir else "task-*/episode_*.parquet"
44
+ data_paths = sorted((dataset_root / "data").glob(pattern))
45
+ pairs: list[EpisodePair] = []
46
+ for data_path in data_paths:
47
+ rel = data_path.relative_to(dataset_root / "data")
48
+ latent_path = dataset_root / "latent" / "data" / rel
49
+ if not latent_path.is_file():
50
+ raise FileNotFoundError(f"Missing latent parquet for {data_path}: {latent_path}")
51
+ pairs.append(EpisodePair(data_path=data_path, latent_path=latent_path))
52
+ if not pairs:
53
+ raise FileNotFoundError(f"No episode parquet files found in {dataset_root / 'data'}")
54
+ return pairs
55
+
56
+
57
+ def split_episode_pairs(
58
+ pairs: list[EpisodePair],
59
+ val_ratio: float,
60
+ seed: int,
61
+ max_episodes: int | None = None,
62
+ ) -> tuple[list[EpisodePair], list[EpisodePair]]:
63
+ """Shuffle episode pairs and split into train/validation subsets."""
64
+
65
+ pairs = list(pairs)
66
+ rng = random.Random(seed)
67
+ rng.shuffle(pairs)
68
+ if max_episodes is not None:
69
+ pairs = pairs[:max_episodes]
70
+ val_count = int(round(len(pairs) * val_ratio))
71
+ if val_ratio > 0 and val_count == 0 and len(pairs) > 1:
72
+ val_count = 1
73
+ val_pairs = pairs[:val_count]
74
+ train_pairs = pairs[val_count:]
75
+ if not train_pairs:
76
+ raise ValueError("No training episodes left after split.")
77
+ return train_pairs, val_pairs
78
+
79
+
80
+ def fixed_or_variable_list_to_numpy(column: pa.ChunkedArray, dtype: np.dtype) -> np.ndarray:
81
+ """Convert Arrow list/fixed-size-list columns to dense numpy arrays."""
82
+
83
+ array = column.combine_chunks()
84
+ if pa.types.is_fixed_size_list(array.type):
85
+ outer_size = array.type.list_size
86
+ inner = array.values
87
+ if pa.types.is_fixed_size_list(inner.type):
88
+ inner_size = inner.type.list_size
89
+ flat = inner.values.to_numpy(zero_copy_only=False)
90
+ return np.asarray(flat, dtype=dtype).reshape(len(array), outer_size, inner_size)
91
+ flat = inner.to_numpy(zero_copy_only=False)
92
+ return np.asarray(flat, dtype=dtype).reshape(len(array), outer_size)
93
+ return np.asarray(array.to_pylist(), dtype=dtype)
94
+
95
+
96
+ def load_episode(pair: EpisodePair) -> dict[str, np.ndarray]:
97
+ """Load one episode's state/action/chunk rows plus aligned base-policy latents."""
98
+
99
+ data = pq.read_table(
100
+ pair.data_path,
101
+ columns=[
102
+ "observation.state",
103
+ "action",
104
+ "a2c2.base_action_chunk",
105
+ "a2c2.valid_action_mask",
106
+ ],
107
+ )
108
+ latent = pq.read_table(pair.latent_path, columns=["a2c2.base_policy_z"])
109
+ if data.num_rows != latent.num_rows:
110
+ raise ValueError(f"Row mismatch: {pair.data_path} has {data.num_rows}, {pair.latent_path} has {latent.num_rows}")
111
+
112
+ return {
113
+ "states": fixed_or_variable_list_to_numpy(data.column("observation.state"), np.float32),
114
+ "actions": fixed_or_variable_list_to_numpy(data.column("action"), np.float32),
115
+ "chunks": fixed_or_variable_list_to_numpy(data.column("a2c2.base_action_chunk"), np.float32),
116
+ "masks": fixed_or_variable_list_to_numpy(data.column("a2c2.valid_action_mask"), np.bool_),
117
+ "zs": fixed_or_variable_list_to_numpy(latent.column("a2c2.base_policy_z"), np.float32),
118
+ }
119
+
120
+
121
+ class A2C2RandomSampleDataset(IterableDataset):
122
+ """Randomly sample valid (source frame t, chunk offset k) training examples."""
123
+
124
+ def __init__(
125
+ self,
126
+ episode_pairs: list[EpisodePair],
127
+ action_horizon: int,
128
+ samples_per_episode: int,
129
+ seed: int,
130
+ total_samples: int | None = None,
131
+ ) -> None:
132
+ super().__init__()
133
+ self.episode_pairs = list(episode_pairs)
134
+ self.action_horizon = action_horizon
135
+ self.samples_per_episode = samples_per_episode
136
+ self.seed = seed
137
+ self.total_samples = total_samples
138
+
139
+ def __iter__(self) -> Iterator[dict[str, np.ndarray]]:
140
+ worker = get_worker_info()
141
+ worker_id = worker.id if worker else 0
142
+ num_workers = worker.num_workers if worker else 1
143
+ pairs = self.episode_pairs[worker_id::num_workers]
144
+ if not pairs:
145
+ return
146
+
147
+ rng = np.random.default_rng(self.seed + worker_id)
148
+ yielded = 0
149
+ while self.total_samples is None or yielded < self.total_samples:
150
+ order = rng.permutation(len(pairs))
151
+ for episode_idx in order:
152
+ episode = load_episode(pairs[int(episode_idx)])
153
+ rows = episode["actions"].shape[0]
154
+ for _ in range(self.samples_per_episode):
155
+ if self.total_samples is not None and yielded >= self.total_samples:
156
+ return
157
+ source_idx = int(rng.integers(0, rows))
158
+ valid_offsets = np.flatnonzero(episode["masks"][source_idx])
159
+ if valid_offsets.size == 0:
160
+ continue
161
+ k = int(rng.choice(valid_offsets))
162
+ target_idx = source_idx + k
163
+ if target_idx >= rows:
164
+ continue
165
+
166
+ base_action = episode["chunks"][source_idx, k]
167
+ expert_action = episode["actions"][target_idx]
168
+ denom = max(self.action_horizon - 1, 1)
169
+ phase = 2.0 * math.pi * float(k) / denom
170
+ yield {
171
+ "observation_state": episode["states"][target_idx],
172
+ "base_action_chunk": episode["chunks"][source_idx],
173
+ "base_policy_z": episode["zs"][source_idx],
174
+ "time_feature": np.asarray([math.sin(phase), math.cos(phase)], dtype=np.float32),
175
+ "valid_action_mask": episode["masks"][source_idx],
176
+ "base_action": base_action,
177
+ "target_delta": expert_action - base_action,
178
+ "expert_action": expert_action,
179
+ }
180
+ yielded += 1
181
+
182
+
183
+ def move_batch_to_device(batch: dict[str, Tensor], device: torch.device) -> dict[str, Tensor]:
184
+ return {key: value.to(device, non_blocking=True) if torch.is_tensor(value) else value for key, value in batch.items()}
185
+
186
+
187
+ def pick_device(raw: str) -> torch.device:
188
+ if raw != "auto":
189
+ return torch.device(raw)
190
+ if torch.cuda.is_available():
191
+ return torch.device("cuda")
192
+ if torch.backends.mps.is_available():
193
+ return torch.device("mps")
194
+ return torch.device("cpu")
src/model.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """A2C2 correction head architecture for cached BEHAVIOR/OpenPI features."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import math
7
+
8
+ import torch
9
+ from torch import Tensor, nn
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class A2C2CorrectionHeadConfig:
14
+ state_dim: int = 256
15
+ action_dim: int = 23
16
+ action_horizon: int = 32
17
+ base_policy_z_dim: int = 2048
18
+ use_base_policy_z: bool = True
19
+ time_dim: int = 2
20
+
21
+ dim_model: int = 512
22
+ n_heads: int = 8
23
+ n_encoder_layers: int = 6
24
+ dim_feedforward: int = 2048
25
+ dropout: float = 0.1
26
+ mlp_hidden_dim: int = 1024
27
+
28
+
29
+ def _sinusoidal_positions(length: int, dim: int) -> Tensor:
30
+ if dim % 2 != 0:
31
+ raise ValueError("dim must be even for sinusoidal positional encoding.")
32
+
33
+ position = torch.arange(length, dtype=torch.float32).unsqueeze(1)
34
+ div_term = torch.exp(torch.arange(0, dim, 2, dtype=torch.float32) * (-math.log(10000.0) / dim))
35
+ pe = torch.zeros(length, dim, dtype=torch.float32)
36
+ pe[:, 0::2] = torch.sin(position * div_term)
37
+ pe[:, 1::2] = torch.cos(position * div_term)
38
+ return pe
39
+
40
+
41
+ class A2C2CorrectionHead(nn.Module):
42
+ """Transformer + MLP correction head following the A2C2 residual design."""
43
+
44
+ def __init__(self, config: A2C2CorrectionHeadConfig | None = None) -> None:
45
+ super().__init__()
46
+ self.config = config or A2C2CorrectionHeadConfig()
47
+ cfg = self.config
48
+
49
+ self.cls_token = nn.Parameter(torch.zeros(1, 1, cfg.dim_model))
50
+ self.type_embedding = nn.Parameter(torch.zeros(6, cfg.dim_model))
51
+
52
+ self.state_proj = nn.Linear(cfg.state_dim, cfg.dim_model)
53
+ if cfg.use_base_policy_z:
54
+ self.z_proj = nn.Linear(cfg.base_policy_z_dim, cfg.dim_model)
55
+ self.time_proj = nn.Linear(cfg.time_dim, cfg.dim_model)
56
+ self.action_proj = nn.Linear(cfg.action_dim, cfg.dim_model)
57
+
58
+ chunk_pos = _sinusoidal_positions(cfg.action_horizon, cfg.dim_model)
59
+ self.register_buffer("chunk_pos_embedding", chunk_pos, persistent=False)
60
+
61
+ encoder_layer = nn.TransformerEncoderLayer(
62
+ d_model=cfg.dim_model,
63
+ nhead=cfg.n_heads,
64
+ dim_feedforward=cfg.dim_feedforward,
65
+ dropout=cfg.dropout,
66
+ activation="gelu",
67
+ batch_first=True,
68
+ norm_first=True,
69
+ )
70
+ self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=cfg.n_encoder_layers)
71
+ self.encoder_norm = nn.LayerNorm(cfg.dim_model)
72
+
73
+ head_input_token_count = 5 if cfg.use_base_policy_z else 4
74
+ head_input_dim = cfg.dim_model * head_input_token_count + cfg.action_dim
75
+ self.residual_head = nn.Sequential(
76
+ nn.Linear(head_input_dim, cfg.mlp_hidden_dim),
77
+ nn.GELU(),
78
+ nn.Dropout(cfg.dropout),
79
+ nn.Linear(cfg.mlp_hidden_dim, cfg.mlp_hidden_dim),
80
+ nn.GELU(),
81
+ nn.Dropout(cfg.dropout),
82
+ nn.Linear(cfg.mlp_hidden_dim, cfg.action_dim),
83
+ )
84
+
85
+ self._reset_parameters()
86
+
87
+ @staticmethod
88
+ def make_time_feature(chunk_index: Tensor, horizon: int) -> Tensor:
89
+ """Create [sin, cos] phase features from chunk indices."""
90
+
91
+ idx = chunk_index.to(dtype=torch.float32)
92
+ denom = max(horizon - 1, 1)
93
+ phase = 2.0 * math.pi * idx / denom
94
+ return torch.stack([torch.sin(phase), torch.cos(phase)], dim=-1)
95
+
96
+ def forward(
97
+ self,
98
+ observation_state: Tensor,
99
+ selected_base_action: Tensor,
100
+ base_action_chunk: Tensor,
101
+ base_policy_z: Tensor,
102
+ time_feature: Tensor,
103
+ valid_action_mask: Tensor | None = None,
104
+ ) -> Tensor:
105
+ """Predict residual action delta.
106
+
107
+ Args:
108
+ observation_state: [B, state_dim]
109
+ selected_base_action: [B, action_dim], the current action being corrected.
110
+ base_action_chunk: [B, H, action_dim]
111
+ base_policy_z: [B, z_dim]
112
+ time_feature: [B, 2]
113
+ valid_action_mask: optional bool tensor [B, H], True for valid chunk
114
+ entries. Invalid chunk entries are ignored by transformer attention.
115
+
116
+ Returns:
117
+ Tensor [B, action_dim], the predicted residual delta.
118
+ """
119
+
120
+ cfg = self.config
121
+ batch_size = observation_state.shape[0]
122
+ device = observation_state.device
123
+ dtype = observation_state.dtype
124
+
125
+ self._validate_inputs(observation_state, selected_base_action, base_action_chunk, base_policy_z, time_feature)
126
+
127
+ cls = self.cls_token.to(device=device, dtype=dtype).expand(batch_size, -1, -1)
128
+ cls = cls + self.type_embedding[0].to(device=device, dtype=dtype)
129
+
130
+ state_token = self.state_proj(observation_state).unsqueeze(1)
131
+ state_token = state_token + self.type_embedding[1].to(device=device, dtype=dtype)
132
+
133
+ time_token = self.time_proj(time_feature).unsqueeze(1)
134
+ time_token = time_token + self.type_embedding[3].to(device=device, dtype=dtype)
135
+
136
+ selected_action_token = self.action_proj(selected_base_action).unsqueeze(1)
137
+ selected_action_token = selected_action_token + self.type_embedding[4].to(device=device, dtype=dtype)
138
+
139
+ chunk_tokens = self.action_proj(base_action_chunk)
140
+ chunk_pos = self.chunk_pos_embedding[: base_action_chunk.shape[1]].to(device=device, dtype=dtype)
141
+ chunk_tokens = chunk_tokens + chunk_pos.unsqueeze(0)
142
+ chunk_tokens = chunk_tokens + self.type_embedding[5].to(device=device, dtype=dtype)
143
+
144
+ prefix_tokens = [cls, state_token]
145
+ if cfg.use_base_policy_z:
146
+ z_token = self.z_proj(base_policy_z).unsqueeze(1)
147
+ z_token = z_token + self.type_embedding[2].to(device=device, dtype=dtype)
148
+ prefix_tokens.append(z_token)
149
+ prefix_tokens.extend([time_token, selected_action_token])
150
+
151
+ tokens = torch.cat([*prefix_tokens, chunk_tokens], dim=1)
152
+
153
+ padding_mask = None
154
+ if valid_action_mask is not None:
155
+ valid_action_mask = valid_action_mask.to(device=device, dtype=torch.bool)
156
+ prefix_mask = torch.zeros(batch_size, len(prefix_tokens), device=device, dtype=torch.bool)
157
+ padding_mask = torch.cat([prefix_mask, ~valid_action_mask], dim=1)
158
+
159
+ encoded = self.encoder(tokens, src_key_padding_mask=padding_mask)
160
+ encoded = self.encoder_norm(encoded)
161
+
162
+ cls_state = encoded[:, 0]
163
+ state_state = encoded[:, 1]
164
+ if cfg.use_base_policy_z:
165
+ z_state = encoded[:, 2]
166
+ time_state = encoded[:, 3]
167
+ selected_action_state = encoded[:, 4]
168
+ head_states = [cls_state, state_state, z_state, time_state, selected_action_state]
169
+ else:
170
+ time_state = encoded[:, 2]
171
+ selected_action_state = encoded[:, 3]
172
+ head_states = [cls_state, state_state, time_state, selected_action_state]
173
+
174
+ head_input = torch.cat(
175
+ [*head_states, selected_base_action],
176
+ dim=-1,
177
+ )
178
+ return self.residual_head(head_input)
179
+
180
+ def _validate_inputs(
181
+ self,
182
+ observation_state: Tensor,
183
+ selected_base_action: Tensor,
184
+ base_action_chunk: Tensor,
185
+ base_policy_z: Tensor,
186
+ time_feature: Tensor,
187
+ ) -> None:
188
+ cfg = self.config
189
+ if observation_state.ndim != 2 or observation_state.shape[-1] != cfg.state_dim:
190
+ raise ValueError(f"observation_state must have shape [B, {cfg.state_dim}].")
191
+ if selected_base_action.ndim != 2 or selected_base_action.shape[-1] != cfg.action_dim:
192
+ raise ValueError(f"selected_base_action must have shape [B, {cfg.action_dim}].")
193
+ if base_action_chunk.ndim != 3 or base_action_chunk.shape[-1] != cfg.action_dim:
194
+ raise ValueError(f"base_action_chunk must have shape [B, H, {cfg.action_dim}].")
195
+ if base_action_chunk.shape[1] > cfg.action_horizon:
196
+ raise ValueError(f"base_action_chunk horizon cannot exceed {cfg.action_horizon}.")
197
+ if cfg.use_base_policy_z and (base_policy_z.ndim != 2 or base_policy_z.shape[-1] != cfg.base_policy_z_dim):
198
+ raise ValueError(f"base_policy_z must have shape [B, {cfg.base_policy_z_dim}].")
199
+ if time_feature.ndim != 2 or time_feature.shape[-1] != cfg.time_dim:
200
+ raise ValueError(f"time_feature must have shape [B, {cfg.time_dim}].")
201
+
202
+ def _reset_parameters(self) -> None:
203
+ nn.init.trunc_normal_(self.cls_token, std=0.02)
204
+ nn.init.trunc_normal_(self.type_embedding, std=0.02)
205
+ for module in self.modules():
206
+ if isinstance(module, nn.Linear):
207
+ nn.init.xavier_uniform_(module.weight)
208
+ if module.bias is not None:
209
+ nn.init.zeros_(module.bias)