Align DeMemWM latent memory frame offsets
Browse files
configurations/dataset/video_minecraft_dememwm_latent.yaml
CHANGED
|
@@ -1,7 +1,8 @@
|
|
| 1 |
defaults:
|
| 2 |
- base_dataset
|
| 3 |
|
| 4 |
-
save_dir: data/
|
|
|
|
| 5 |
metadata: null
|
| 6 |
n_frames: 8
|
| 7 |
n_frames_valid: 500
|
|
|
|
| 1 |
defaults:
|
| 2 |
- base_dataset
|
| 3 |
|
| 4 |
+
save_dir: data/minecraft_simple_backforward
|
| 5 |
+
precomputed_feature_dir: ${dataset.save_dir}/vae_features
|
| 6 |
metadata: null
|
| 7 |
n_frames: 8
|
| 8 |
n_frames_valid: 500
|
datasets/video/memory_selection.py
CHANGED
|
@@ -336,10 +336,10 @@ def _select_anchor(candidates: np.ndarray, count: int, cfg) -> np.ndarray:
|
|
| 336 |
return np.asarray(selected[:count], dtype=np.int64)
|
| 337 |
|
| 338 |
|
| 339 |
-
def _select_dynamic(target_start: int, count: int) -> np.ndarray:
|
| 340 |
-
if count <= 0 or target_start <=
|
| 341 |
return np.empty((0,), dtype=np.int64)
|
| 342 |
-
start = max(
|
| 343 |
return np.arange(start, target_start, dtype=np.int64)[-count:]
|
| 344 |
|
| 345 |
|
|
@@ -351,6 +351,7 @@ def _select_revisit(
|
|
| 351 |
excluded: np.ndarray,
|
| 352 |
split: str,
|
| 353 |
rng=None,
|
|
|
|
| 354 |
) -> np.ndarray:
|
| 355 |
if count <= 0:
|
| 356 |
return np.empty((0,), dtype=np.int64)
|
|
@@ -358,7 +359,7 @@ def _select_revisit(
|
|
| 358 |
target_start = int(target_positions[0])
|
| 359 |
exclusion = max(0, int(cfg_get(cfg, "local_context_exclusion_frames", 8)))
|
| 360 |
causal_stop = max(0, target_start - exclusion)
|
| 361 |
-
candidates = np.arange(
|
| 362 |
if len(candidates) == 0:
|
| 363 |
return np.empty((0,), dtype=np.int64)
|
| 364 |
|
|
@@ -382,13 +383,14 @@ def _select_revisit(
|
|
| 382 |
)
|
| 383 |
|
| 384 |
|
| 385 |
-
def select_memory_indices(poses, target_positions, cfg=None, split: str = "training", rng=None):
|
| 386 |
"""Select causal memory indices and masks for [anchor][dynamic][revisit]."""
|
| 387 |
|
| 388 |
poses = _as_pose_array(poses)
|
| 389 |
target_positions = np.asarray(target_positions, dtype=np.int64)
|
| 390 |
target_positions = target_positions[(target_positions >= 0) & (target_positions < len(poses))]
|
| 391 |
enabled = bool(cfg_get(cfg, "enabled", True))
|
|
|
|
| 392 |
|
| 393 |
counts = {
|
| 394 |
"anchor": int(cfg_get(cfg, "max_anchor_frames", 0)) if enabled else 0,
|
|
@@ -402,12 +404,21 @@ def select_memory_indices(poses, target_positions, cfg=None, split: str = "train
|
|
| 402 |
|
| 403 |
target_start = int(target_positions[0])
|
| 404 |
|
| 405 |
-
dynamic = _select_dynamic(target_start, counts["dynamic"])
|
| 406 |
anchor_stop = max(0, target_start - counts["dynamic"])
|
| 407 |
-
anchor_candidates = np.arange(
|
| 408 |
anchor = _select_anchor(anchor_candidates, counts["anchor"], cfg)
|
| 409 |
excluded = np.concatenate([anchor, dynamic]) if len(anchor) or len(dynamic) else np.empty((0,), dtype=np.int64)
|
| 410 |
-
revisit = _select_revisit(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
|
| 412 |
_write_segment(indices, masks, "anchor", anchor)
|
| 413 |
_write_segment(indices, masks, "dynamic", dynamic)
|
|
|
|
| 336 |
return np.asarray(selected[:count], dtype=np.int64)
|
| 337 |
|
| 338 |
|
| 339 |
+
def _select_dynamic(target_start: int, count: int, min_candidate_frame: int = 0) -> np.ndarray:
|
| 340 |
+
if count <= 0 or target_start <= min_candidate_frame:
|
| 341 |
return np.empty((0,), dtype=np.int64)
|
| 342 |
+
start = max(min_candidate_frame, target_start - count)
|
| 343 |
return np.arange(start, target_start, dtype=np.int64)[-count:]
|
| 344 |
|
| 345 |
|
|
|
|
| 351 |
excluded: np.ndarray,
|
| 352 |
split: str,
|
| 353 |
rng=None,
|
| 354 |
+
min_candidate_frame: int = 0,
|
| 355 |
) -> np.ndarray:
|
| 356 |
if count <= 0:
|
| 357 |
return np.empty((0,), dtype=np.int64)
|
|
|
|
| 359 |
target_start = int(target_positions[0])
|
| 360 |
exclusion = max(0, int(cfg_get(cfg, "local_context_exclusion_frames", 8)))
|
| 361 |
causal_stop = max(0, target_start - exclusion)
|
| 362 |
+
candidates = np.arange(min_candidate_frame, causal_stop, dtype=np.int64)
|
| 363 |
if len(candidates) == 0:
|
| 364 |
return np.empty((0,), dtype=np.int64)
|
| 365 |
|
|
|
|
| 383 |
)
|
| 384 |
|
| 385 |
|
| 386 |
+
def select_memory_indices(poses, target_positions, cfg=None, split: str = "training", rng=None, min_candidate_frame: int = 0):
|
| 387 |
"""Select causal memory indices and masks for [anchor][dynamic][revisit]."""
|
| 388 |
|
| 389 |
poses = _as_pose_array(poses)
|
| 390 |
target_positions = np.asarray(target_positions, dtype=np.int64)
|
| 391 |
target_positions = target_positions[(target_positions >= 0) & (target_positions < len(poses))]
|
| 392 |
enabled = bool(cfg_get(cfg, "enabled", True))
|
| 393 |
+
min_candidate_frame = max(0, int(min_candidate_frame))
|
| 394 |
|
| 395 |
counts = {
|
| 396 |
"anchor": int(cfg_get(cfg, "max_anchor_frames", 0)) if enabled else 0,
|
|
|
|
| 404 |
|
| 405 |
target_start = int(target_positions[0])
|
| 406 |
|
| 407 |
+
dynamic = _select_dynamic(target_start, counts["dynamic"], min_candidate_frame)
|
| 408 |
anchor_stop = max(0, target_start - counts["dynamic"])
|
| 409 |
+
anchor_candidates = np.arange(min_candidate_frame, anchor_stop, dtype=np.int64)
|
| 410 |
anchor = _select_anchor(anchor_candidates, counts["anchor"], cfg)
|
| 411 |
excluded = np.concatenate([anchor, dynamic]) if len(anchor) or len(dynamic) else np.empty((0,), dtype=np.int64)
|
| 412 |
+
revisit = _select_revisit(
|
| 413 |
+
poses,
|
| 414 |
+
target_positions,
|
| 415 |
+
cfg,
|
| 416 |
+
counts["revisit"],
|
| 417 |
+
excluded,
|
| 418 |
+
split,
|
| 419 |
+
rng=rng,
|
| 420 |
+
min_candidate_frame=min_candidate_frame,
|
| 421 |
+
)
|
| 422 |
|
| 423 |
_write_segment(indices, masks, "anchor", anchor)
|
| 424 |
_write_segment(indices, masks, "dynamic", dynamic)
|
datasets/video/minecraft_video_dememwm_latent_dataset.py
CHANGED
|
@@ -2,7 +2,9 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
-
import
|
|
|
|
|
|
|
| 6 |
from pathlib import Path
|
| 7 |
import numpy as np
|
| 8 |
import torch
|
|
@@ -11,32 +13,60 @@ from omegaconf import DictConfig
|
|
| 11 |
from .memory_selection import SEGMENT_KEYS, cfg_get, memory_segment_lengths, select_memory_indices
|
| 12 |
|
| 13 |
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
return vec_25
|
| 32 |
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
class MinecraftVideoDeMemWMLatentDataset(torch.utils.data.Dataset):
|
| 35 |
"""Load precomputed VAE latents and append causal memory frames.
|
| 36 |
|
| 37 |
-
|
| 38 |
-
``
|
| 39 |
-
|
| 40 |
"""
|
| 41 |
|
| 42 |
def __init__(self, cfg: DictConfig, split: str = "training"):
|
|
@@ -44,8 +74,13 @@ class MinecraftVideoDeMemWMLatentDataset(torch.utils.data.Dataset):
|
|
| 44 |
self.cfg = cfg
|
| 45 |
self.split = split
|
| 46 |
self.save_dir = Path(cfg.save_dir)
|
|
|
|
|
|
|
| 47 |
self.wo_updown = bool(cfg_get(cfg, "wo_updown", False))
|
| 48 |
self.initial_frame_offset = _INITIAL_FRAME_OFFSET
|
|
|
|
|
|
|
|
|
|
| 49 |
self.n_frames = int(cfg_get(cfg, "n_frames_valid", cfg.n_frames)) if split in {"validation", "test"} else int(cfg.n_frames)
|
| 50 |
self.frame_skip = int(cfg_get(cfg, "frame_skip", 1))
|
| 51 |
self.target_span = max(1, (self.n_frames - 1) * self.frame_skip + 1)
|
|
@@ -59,10 +94,10 @@ class MinecraftVideoDeMemWMLatentDataset(torch.utils.data.Dataset):
|
|
| 59 |
self._cached_arrays = None
|
| 60 |
self.data_paths = self.get_data_paths(split)
|
| 61 |
if not self.data_paths:
|
| 62 |
-
raise FileNotFoundError(f"No
|
| 63 |
|
| 64 |
self.lengths = np.asarray([self._file_length(path) for path in self.data_paths], dtype=np.int64)
|
| 65 |
-
required_length = self.
|
| 66 |
too_short = self.lengths < required_length
|
| 67 |
if bool(too_short.any()):
|
| 68 |
bad_idx = int(np.nonzero(too_short)[0][0])
|
|
@@ -72,44 +107,47 @@ class MinecraftVideoDeMemWMLatentDataset(torch.utils.data.Dataset):
|
|
| 72 |
)
|
| 73 |
|
| 74 |
clips = (self.lengths - required_length + 1).astype(np.int64)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
if self.single_eval_clip:
|
| 76 |
clips = np.ones_like(clips)
|
| 77 |
self.clips_per_file = clips
|
| 78 |
self.cum_clips_per_file = np.cumsum(self.clips_per_file)
|
| 79 |
self.idx_remap = list(range(self.__len__()))
|
| 80 |
-
if
|
| 81 |
-
rng =
|
| 82 |
rng.shuffle(self.idx_remap)
|
| 83 |
|
| 84 |
def get_data_paths(self, split: str):
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
paths = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
if not paths:
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
)
|
| 107 |
-
warnings.warn(message, stacklevel=2)
|
| 108 |
-
raise FileNotFoundError(message)
|
| 109 |
-
if split_filter is not None:
|
| 110 |
-
return paths
|
| 111 |
-
for sub_path in sorted(split_dir.iterdir(), key=lambda path: path.name):
|
| 112 |
-
paths += sorted((path for path in sub_path.glob("**/*.npz") if path.is_file()), key=lambda path: path.name)
|
| 113 |
return paths
|
| 114 |
|
| 115 |
def __len__(self):
|
|
@@ -126,7 +164,7 @@ class MinecraftVideoDeMemWMLatentDataset(torch.utils.data.Dataset):
|
|
| 126 |
def load_data(self, idx: int):
|
| 127 |
# === 1. Resolve clip and load arrays ===
|
| 128 |
file_idx, frame_idx = self.split_idx(int(idx))
|
| 129 |
-
frame_idx += self.
|
| 130 |
arrays = self._load_arrays(self.data_paths[file_idx])
|
| 131 |
latents = arrays["latents"]
|
| 132 |
actions = arrays["actions"]
|
|
@@ -140,6 +178,7 @@ class MinecraftVideoDeMemWMLatentDataset(torch.utils.data.Dataset):
|
|
| 140 |
target_positions,
|
| 141 |
self.memory_selection,
|
| 142 |
split=self.split,
|
|
|
|
| 143 |
)
|
| 144 |
ordered_memory = [memory_indices[key] for key in SEGMENT_KEYS]
|
| 145 |
source_positions = np.concatenate([target_positions, *ordered_memory]).astype(np.int64)
|
|
@@ -172,39 +211,63 @@ class MinecraftVideoDeMemWMLatentDataset(torch.utils.data.Dataset):
|
|
| 172 |
"image_hw": torch.as_tensor(arrays["image_hw"], dtype=torch.long),
|
| 173 |
}
|
| 174 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
def _file_length(self, path: Path) -> int:
|
| 176 |
-
|
| 177 |
-
|
|
|
|
|
|
|
| 178 |
|
| 179 |
def _load_arrays(self, path: Path):
|
| 180 |
if self._cached_arrays_path == path:
|
| 181 |
return self._cached_arrays
|
| 182 |
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
|
|
|
|
|
|
|
|
|
| 199 |
"latents": latents,
|
| 200 |
"actions": actions,
|
| 201 |
"poses": poses,
|
| 202 |
-
"frame_indices":
|
| 203 |
-
"image_hw":
|
| 204 |
}
|
| 205 |
-
self._cached_arrays_path = path
|
| 206 |
-
self._cached_arrays = arrays
|
| 207 |
-
return arrays
|
| 208 |
|
| 209 |
def _normalize_actions(self, path: Path, actions: np.ndarray) -> np.ndarray:
|
| 210 |
if actions.ndim != 2:
|
|
@@ -213,7 +276,7 @@ class MinecraftVideoDeMemWMLatentDataset(torch.utils.data.Dataset):
|
|
| 213 |
return actions.astype(np.float32, copy=False)
|
| 214 |
if actions.shape[1] < 8:
|
| 215 |
raise ValueError(f"raw actions in {path} must have at least 8 columns to convert to 25-D actions")
|
| 216 |
-
return
|
| 217 |
|
| 218 |
@staticmethod
|
| 219 |
def _sanitize_poses(path: Path, poses: np.ndarray, action_length: int) -> np.ndarray:
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
+
import json
|
| 6 |
+
import os
|
| 7 |
+
import random
|
| 8 |
from pathlib import Path
|
| 9 |
import numpy as np
|
| 10 |
import torch
|
|
|
|
| 13 |
from .memory_selection import SEGMENT_KEYS, cfg_get, memory_segment_lengths, select_memory_indices
|
| 14 |
|
| 15 |
|
| 16 |
+
ACTION_KEYS = [
|
| 17 |
+
"inventory",
|
| 18 |
+
"ESC",
|
| 19 |
+
"hotbar.1",
|
| 20 |
+
"hotbar.2",
|
| 21 |
+
"hotbar.3",
|
| 22 |
+
"hotbar.4",
|
| 23 |
+
"hotbar.5",
|
| 24 |
+
"hotbar.6",
|
| 25 |
+
"hotbar.7",
|
| 26 |
+
"hotbar.8",
|
| 27 |
+
"hotbar.9",
|
| 28 |
+
"forward",
|
| 29 |
+
"back",
|
| 30 |
+
"left",
|
| 31 |
+
"right",
|
| 32 |
+
"cameraY",
|
| 33 |
+
"cameraX",
|
| 34 |
+
"jump",
|
| 35 |
+
"sneak",
|
| 36 |
+
"sprint",
|
| 37 |
+
"swapHands",
|
| 38 |
+
"attack",
|
| 39 |
+
"use",
|
| 40 |
+
"pickItem",
|
| 41 |
+
"drop",
|
| 42 |
+
]
|
| 43 |
+
|
| 44 |
+
def convert_action_space(actions):
|
| 45 |
+
vec_25 = torch.zeros(len(actions), len(ACTION_KEYS))
|
| 46 |
+
vec_25[actions[:,0]==1, 11] = 1
|
| 47 |
+
vec_25[actions[:,0]==2, 12] = 1
|
| 48 |
+
vec_25[actions[:,4]==11, 16] = -1
|
| 49 |
+
vec_25[actions[:,4]==13, 16] = 1
|
| 50 |
+
vec_25[actions[:,3]==11, 15] = -1
|
| 51 |
+
vec_25[actions[:,3]==13, 15] = 1
|
| 52 |
+
vec_25[actions[:,5]==6, 24] = 1
|
| 53 |
+
vec_25[actions[:,5]==1, 24] = 1
|
| 54 |
+
vec_25[actions[:,1]==1, 13] = 1
|
| 55 |
+
vec_25[actions[:,1]==2, 14] = 1
|
| 56 |
+
vec_25[actions[:,7]==1, 2] = 1
|
| 57 |
return vec_25
|
| 58 |
|
| 59 |
|
| 60 |
+
_INITIAL_FRAME_OFFSET = 100
|
| 61 |
+
_ACTION_DIM = len(ACTION_KEYS)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
class MinecraftVideoDeMemWMLatentDataset(torch.utils.data.Dataset):
|
| 65 |
"""Load precomputed VAE latents and append causal memory frames.
|
| 66 |
|
| 67 |
+
The index comes from the raw Minecraft ``.mp4`` tree, matching
|
| 68 |
+
``MinecraftVideoDataset``. Latents are read from the original DeMemWM
|
| 69 |
+
``vae_features`` cache at the same relative path.
|
| 70 |
"""
|
| 71 |
|
| 72 |
def __init__(self, cfg: DictConfig, split: str = "training"):
|
|
|
|
| 74 |
self.cfg = cfg
|
| 75 |
self.split = split
|
| 76 |
self.save_dir = Path(cfg.save_dir)
|
| 77 |
+
precomputed_feature_dir = cfg_get(cfg, "precomputed_feature_dir")
|
| 78 |
+
self.feature_root = Path(precomputed_feature_dir) if precomputed_feature_dir is not None else self.save_dir / "vae_features"
|
| 79 |
self.wo_updown = bool(cfg_get(cfg, "wo_updown", False))
|
| 80 |
self.initial_frame_offset = _INITIAL_FRAME_OFFSET
|
| 81 |
+
context_window = cfg_get(cfg, "context_window", cfg_get(cfg, "context_length", 0))
|
| 82 |
+
self.context_window = max(0, int(context_window)) if split == "training" else 0
|
| 83 |
+
self.target_start_offset = self.initial_frame_offset + self.context_window
|
| 84 |
self.n_frames = int(cfg_get(cfg, "n_frames_valid", cfg.n_frames)) if split in {"validation", "test"} else int(cfg.n_frames)
|
| 85 |
self.frame_skip = int(cfg_get(cfg, "frame_skip", 1))
|
| 86 |
self.target_span = max(1, (self.n_frames - 1) * self.frame_skip + 1)
|
|
|
|
| 94 |
self._cached_arrays = None
|
| 95 |
self.data_paths = self.get_data_paths(split)
|
| 96 |
if not self.data_paths:
|
| 97 |
+
raise FileNotFoundError(f"No raw .mp4 files found for split '{split}' under {self.save_dir / split}")
|
| 98 |
|
| 99 |
self.lengths = np.asarray([self._file_length(path) for path in self.data_paths], dtype=np.int64)
|
| 100 |
+
required_length = self.target_start_offset + self.target_span
|
| 101 |
too_short = self.lengths < required_length
|
| 102 |
if bool(too_short.any()):
|
| 103 |
bad_idx = int(np.nonzero(too_short)[0][0])
|
|
|
|
| 107 |
)
|
| 108 |
|
| 109 |
clips = (self.lengths - required_length + 1).astype(np.int64)
|
| 110 |
+
if split == "training":
|
| 111 |
+
# Keep targets inside the original 1300-frame training window after
|
| 112 |
+
# reserving the eval-style context prefix before each target clip.
|
| 113 |
+
original_training_clips = max(1, 1300 - self.context_window - self.target_span + 1)
|
| 114 |
+
clips = np.minimum(clips, original_training_clips)
|
| 115 |
if self.single_eval_clip:
|
| 116 |
clips = np.ones_like(clips)
|
| 117 |
self.clips_per_file = clips
|
| 118 |
self.cum_clips_per_file = np.cumsum(self.clips_per_file)
|
| 119 |
self.idx_remap = list(range(self.__len__()))
|
| 120 |
+
if bool(cfg_get(cfg, "shuffle_clips", True)):
|
| 121 |
+
rng = random.Random(int(cfg_get(cfg, "seed", 0)))
|
| 122 |
rng.shuffle(self.idx_remap)
|
| 123 |
|
| 124 |
def get_data_paths(self, split: str):
|
| 125 |
+
"""
|
| 126 |
+
Retrieve all video file paths for the given split.
|
| 127 |
+
|
| 128 |
+
Args:
|
| 129 |
+
split (str): Dataset split ("training" or "validation").
|
| 130 |
+
|
| 131 |
+
Returns:
|
| 132 |
+
List[Path]: List of video file paths.
|
| 133 |
+
"""
|
| 134 |
+
data_dir = self.save_dir / split
|
| 135 |
+
paths = sorted(list(data_dir.glob("**/*.mp4")), key=lambda x: x.name)
|
| 136 |
+
|
| 137 |
+
if self.wo_updown:
|
| 138 |
+
# Filter out paths containing "w_updown"
|
| 139 |
+
paths = [p for p in paths if "w_updown" not in str(p)]
|
| 140 |
+
|
| 141 |
+
if (split == "validation" or split == "test") and self.wo_updown:
|
| 142 |
+
paths = [p for p in paths if "w_updown" not in str(p)]
|
| 143 |
+
elif split == "validation" or split == "test":
|
| 144 |
+
paths = [p for p in paths if "w_updown" in str(p)]
|
| 145 |
|
| 146 |
if not paths:
|
| 147 |
+
sub_dirs = os.listdir(data_dir)
|
| 148 |
+
for sub_dir in sub_dirs:
|
| 149 |
+
sub_path = data_dir / sub_dir
|
| 150 |
+
paths += sorted(list(sub_path.glob("**/*.mp4")), key=lambda x: x.name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
return paths
|
| 152 |
|
| 153 |
def __len__(self):
|
|
|
|
| 164 |
def load_data(self, idx: int):
|
| 165 |
# === 1. Resolve clip and load arrays ===
|
| 166 |
file_idx, frame_idx = self.split_idx(int(idx))
|
| 167 |
+
frame_idx += self.target_start_offset
|
| 168 |
arrays = self._load_arrays(self.data_paths[file_idx])
|
| 169 |
latents = arrays["latents"]
|
| 170 |
actions = arrays["actions"]
|
|
|
|
| 178 |
target_positions,
|
| 179 |
self.memory_selection,
|
| 180 |
split=self.split,
|
| 181 |
+
min_candidate_frame=self.initial_frame_offset,
|
| 182 |
)
|
| 183 |
ordered_memory = [memory_indices[key] for key in SEGMENT_KEYS]
|
| 184 |
source_positions = np.concatenate([target_positions, *ordered_memory]).astype(np.int64)
|
|
|
|
| 211 |
"image_hw": torch.as_tensor(arrays["image_hw"], dtype=torch.long),
|
| 212 |
}
|
| 213 |
|
| 214 |
+
def _feature_cache_paths(self, video_path: Path) -> tuple[Path, Path]:
|
| 215 |
+
relative_video_path = video_path.relative_to(self.save_dir)
|
| 216 |
+
feature_dir = self.feature_root / relative_video_path.parent
|
| 217 |
+
video_index = relative_video_path.stem
|
| 218 |
+
return (
|
| 219 |
+
feature_dir / f"{video_index}_vae_feature.npy",
|
| 220 |
+
feature_dir / f"{video_index}_vae_feature_meta.json",
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
def _default_image_hw(self) -> np.ndarray:
|
| 224 |
+
return np.asarray(
|
| 225 |
+
cfg_get(self.cfg, "image_hw", [cfg_get(self.cfg, "resolution", 0), cfg_get(self.cfg, "resolution", 0)]),
|
| 226 |
+
dtype=np.int64,
|
| 227 |
+
).reshape(-1)[:2]
|
| 228 |
+
|
| 229 |
+
def _feature_image_hw(self, meta_path: Path) -> np.ndarray:
|
| 230 |
+
if not meta_path.exists():
|
| 231 |
+
return self._default_image_hw()
|
| 232 |
+
with open(meta_path, "r", encoding="utf-8") as handle:
|
| 233 |
+
meta = json.load(handle)
|
| 234 |
+
return np.asarray([int(meta["image_height"]), int(meta["image_width"])], dtype=np.int64)
|
| 235 |
+
|
| 236 |
def _file_length(self, path: Path) -> int:
|
| 237 |
+
feature_path, _ = self._feature_cache_paths(path)
|
| 238 |
+
if not feature_path.exists():
|
| 239 |
+
raise FileNotFoundError(f"Missing precomputed VAE features: {feature_path}")
|
| 240 |
+
return int(np.load(feature_path, mmap_mode="r").shape[0])
|
| 241 |
|
| 242 |
def _load_arrays(self, path: Path):
|
| 243 |
if self._cached_arrays_path == path:
|
| 244 |
return self._cached_arrays
|
| 245 |
|
| 246 |
+
arrays = self._load_feature_arrays(path)
|
| 247 |
+
|
| 248 |
+
self._validate_arrays(path, arrays["latents"], arrays["actions"], arrays["poses"], arrays["frame_indices"])
|
| 249 |
+
self._cached_arrays_path = path
|
| 250 |
+
self._cached_arrays = arrays
|
| 251 |
+
return arrays
|
| 252 |
+
|
| 253 |
+
def _load_feature_arrays(self, video_path: Path):
|
| 254 |
+
feature_path, meta_path = self._feature_cache_paths(video_path)
|
| 255 |
+
if not feature_path.exists():
|
| 256 |
+
raise FileNotFoundError(f"Missing precomputed VAE features: {feature_path}")
|
| 257 |
+
|
| 258 |
+
action_path = video_path.with_suffix(".npz")
|
| 259 |
+
with np.load(action_path, allow_pickle=False) as data:
|
| 260 |
+
latents = np.load(feature_path, mmap_mode="r")
|
| 261 |
+
actions = self._normalize_actions(action_path, np.asarray(data["actions"]))
|
| 262 |
+
poses = self._sanitize_poses(action_path, np.asarray(data["poses"], dtype=np.float32), len(actions))
|
| 263 |
+
|
| 264 |
+
return {
|
| 265 |
"latents": latents,
|
| 266 |
"actions": actions,
|
| 267 |
"poses": poses,
|
| 268 |
+
"frame_indices": np.arange(len(latents), dtype=np.int64),
|
| 269 |
+
"image_hw": self._feature_image_hw(meta_path),
|
| 270 |
}
|
|
|
|
|
|
|
|
|
|
| 271 |
|
| 272 |
def _normalize_actions(self, path: Path, actions: np.ndarray) -> np.ndarray:
|
| 273 |
if actions.ndim != 2:
|
|
|
|
| 276 |
return actions.astype(np.float32, copy=False)
|
| 277 |
if actions.shape[1] < 8:
|
| 278 |
raise ValueError(f"raw actions in {path} must have at least 8 columns to convert to 25-D actions")
|
| 279 |
+
return convert_action_space(actions).numpy()
|
| 280 |
|
| 281 |
@staticmethod
|
| 282 |
def _sanitize_poses(path: Path, poses: np.ndarray, action_length: int) -> np.ndarray:
|
tests/test_dememwm_latent_dataset.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import tempfile
|
| 2 |
import unittest
|
| 3 |
from unittest import mock
|
|
@@ -43,6 +44,7 @@ def _poses(num_frames):
|
|
| 43 |
def _dataset_cfg(root, **overrides):
|
| 44 |
cfg = {
|
| 45 |
"save_dir": str(root),
|
|
|
|
| 46 |
"n_frames": 3,
|
| 47 |
"n_frames_valid": 3,
|
| 48 |
"frame_skip": 1,
|
|
@@ -57,22 +59,21 @@ def _dataset_cfg(root, **overrides):
|
|
| 57 |
return OmegaConf.create(cfg)
|
| 58 |
|
| 59 |
|
| 60 |
-
def
|
| 61 |
-
|
|
|
|
|
|
|
| 62 |
if actions is None:
|
| 63 |
-
actions = np.ones((num_frames,
|
| 64 |
if poses is None:
|
| 65 |
poses = _poses(num_frames)
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
frame_indices=frame_indices,
|
| 74 |
-
image_hw=np.array([360, 640], dtype=np.int64),
|
| 75 |
-
)
|
| 76 |
|
| 77 |
|
| 78 |
class MemorySelectionTests(unittest.TestCase):
|
|
@@ -178,7 +179,7 @@ class DeMemWMLatentDatasetTests(unittest.TestCase):
|
|
| 178 |
def test_dataset_requires_split_directory_without_root_fallback(self):
|
| 179 |
with tempfile.TemporaryDirectory() as tmp:
|
| 180 |
root = Path(tmp)
|
| 181 |
-
|
| 182 |
|
| 183 |
with self.assertRaisesRegex(FileNotFoundError, "validation"):
|
| 184 |
MinecraftVideoDeMemWMLatentDataset(_dataset_cfg(root), split="validation")
|
|
@@ -186,46 +187,44 @@ class DeMemWMLatentDatasetTests(unittest.TestCase):
|
|
| 186 |
def test_dataset_matches_original_w_updown_split_filtering(self):
|
| 187 |
with tempfile.TemporaryDirectory() as tmp:
|
| 188 |
root = Path(tmp)
|
| 189 |
-
|
| 190 |
-
|
| 191 |
|
| 192 |
dataset = MinecraftVideoDeMemWMLatentDataset(_dataset_cfg(root), split="validation")
|
| 193 |
dataset_wo = MinecraftVideoDeMemWMLatentDataset(_dataset_cfg(root, wo_updown=True), split="validation")
|
| 194 |
|
| 195 |
-
self.assertEqual([path.name for path in dataset.data_paths], ["clip_w_updown.
|
| 196 |
-
self.assertEqual([path.name for path in dataset_wo.data_paths], ["plain.
|
| 197 |
|
| 198 |
def test_dataset_filters_nested_validation_files_by_selected_split(self):
|
| 199 |
with tempfile.TemporaryDirectory() as tmp:
|
| 200 |
root = Path(tmp)
|
| 201 |
-
|
| 202 |
-
|
| 203 |
|
| 204 |
dataset = MinecraftVideoDeMemWMLatentDataset(_dataset_cfg(root), split="validation")
|
| 205 |
dataset_wo = MinecraftVideoDeMemWMLatentDataset(_dataset_cfg(root, wo_updown=True), split="validation")
|
| 206 |
|
| 207 |
-
self.assertEqual([path.name for path in dataset.data_paths], ["clip_w_updown.
|
| 208 |
-
self.assertEqual([path.name for path in dataset_wo.data_paths], ["plain.
|
| 209 |
|
| 210 |
-
def
|
| 211 |
cases = [
|
| 212 |
-
("validation", False, "plain
|
| 213 |
-
("test", True, "clip_w_updown
|
| 214 |
]
|
| 215 |
-
for split, wo_updown,
|
| 216 |
-
with self.subTest(split=split,
|
| 217 |
root = Path(tmp)
|
| 218 |
-
|
| 219 |
-
|
| 220 |
|
| 221 |
-
|
| 222 |
-
with self.assertRaisesRegex(FileNotFoundError, message):
|
| 223 |
-
MinecraftVideoDeMemWMLatentDataset(_dataset_cfg(root, wo_updown=wo_updown), split=split)
|
| 224 |
|
| 225 |
def test_dataset_rejects_files_shorter_than_original_frame_skip_window(self):
|
| 226 |
with tempfile.TemporaryDirectory() as tmp:
|
| 227 |
root = Path(tmp)
|
| 228 |
-
|
| 229 |
|
| 230 |
with self.assertRaisesRegex(ValueError, "requires at least 103"):
|
| 231 |
MinecraftVideoDeMemWMLatentDataset(_dataset_cfg(root), split="training")
|
|
@@ -235,7 +234,7 @@ class DeMemWMLatentDatasetTests(unittest.TestCase):
|
|
| 235 |
root = Path(tmp)
|
| 236 |
poses = _poses(104)
|
| 237 |
poses[:, 1] = np.linspace(0.0, 3.0, 104, dtype=np.float32)
|
| 238 |
-
|
| 239 |
dataset = MinecraftVideoDeMemWMLatentDataset(_dataset_cfg(root), split="training")
|
| 240 |
|
| 241 |
with self.assertRaisesRegex(ValueError, "Pose height variation"):
|
|
@@ -249,7 +248,7 @@ class DeMemWMLatentDatasetTests(unittest.TestCase):
|
|
| 249 |
raw_actions[:, 3] = 11
|
| 250 |
raw_actions[:, 4] = 13
|
| 251 |
raw_actions[:, 7] = 1
|
| 252 |
-
|
| 253 |
sample = MinecraftVideoDeMemWMLatentDataset(_dataset_cfg(root), split="training")[0]
|
| 254 |
|
| 255 |
self.assertEqual(tuple(sample["actions"].shape), (3, 25))
|
|
@@ -258,10 +257,40 @@ class DeMemWMLatentDatasetTests(unittest.TestCase):
|
|
| 258 |
self.assertTrue(torch.equal(sample["actions"][:, 15], -torch.ones(3)))
|
| 259 |
self.assertTrue(torch.equal(sample["actions"][:, 2], torch.ones(3)))
|
| 260 |
|
| 261 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
with tempfile.TemporaryDirectory() as tmp:
|
| 263 |
root = Path(tmp)
|
| 264 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
dataset = MinecraftVideoDeMemWMLatentDataset(_dataset_cfg(root), split="training")
|
| 266 |
first = dataset[0]
|
| 267 |
|
|
@@ -714,7 +743,7 @@ class DeMemWMLatentDatasetTests(unittest.TestCase):
|
|
| 714 |
def test_disabled_memory_selection_does_not_allocate_memory_slots(self):
|
| 715 |
with tempfile.TemporaryDirectory() as tmp:
|
| 716 |
root = Path(tmp)
|
| 717 |
-
|
| 718 |
dataset = MinecraftVideoDeMemWMLatentDataset(
|
| 719 |
_dataset_cfg(root, memory_selection=_selection_cfg(enabled=False)),
|
| 720 |
split="training",
|
|
@@ -729,44 +758,97 @@ class DeMemWMLatentDatasetTests(unittest.TestCase):
|
|
| 729 |
for key in ("anchor", "dynamic", "revisit"):
|
| 730 |
self.assertEqual(tuple(sample["memory_masks"][key].shape), (0,))
|
| 731 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 732 |
def test_dataset_returns_target_anchor_dynamic_revisit_contract(self):
|
| 733 |
with tempfile.TemporaryDirectory() as tmp:
|
| 734 |
root = Path(tmp)
|
| 735 |
-
split_dir = root / "training"
|
| 736 |
-
split_dir.mkdir()
|
| 737 |
num_frames = 112
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
|
|
|
|
|
|
| 741 |
actions=np.ones((num_frames, 25), dtype=np.float32),
|
| 742 |
-
poses=_poses(num_frames),
|
| 743 |
-
frame_indices=np.arange(num_frames, dtype=np.int64),
|
| 744 |
-
image_hw=np.array([360, 640], dtype=np.int64),
|
| 745 |
)
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
"n_frames": 3,
|
| 750 |
-
"n_frames_valid": 3,
|
| 751 |
-
"frame_skip": 1,
|
| 752 |
-
"action_cond_dim": 25,
|
| 753 |
-
"resolution": 128,
|
| 754 |
-
"image_hw": [360, 640],
|
| 755 |
-
"shuffle_clips": False,
|
| 756 |
-
"single_eval_clip": True,
|
| 757 |
-
"memory_selection": _selection_cfg(),
|
| 758 |
-
}
|
| 759 |
)
|
| 760 |
-
dataset = MinecraftVideoDeMemWMLatentDataset(cfg, split="training")
|
| 761 |
sample = dataset[6]
|
| 762 |
|
| 763 |
self.assertEqual(sample["memory_segments"], {"target": 3, "anchor": 2, "dynamic": 2, "revisit": 2})
|
| 764 |
self.assertEqual(tuple(sample["latents"].shape), (9, 1, 1, 2))
|
| 765 |
self.assertEqual(sample["frame_indices"][:3].tolist(), [106, 107, 108])
|
| 766 |
-
self.assertEqual(sample["frame_indices"][3:5].tolist(), [
|
| 767 |
self.assertEqual(sample["frame_indices"][5:7].tolist(), [104, 105])
|
| 768 |
-
self.
|
| 769 |
-
self.assertEqual(len(sample["frame_indices"][7:9]), 2)
|
| 770 |
self.assertTrue(sample["memory_masks"]["target"].all().item())
|
| 771 |
self.assertTrue(sample["memory_masks"]["anchor"].all().item())
|
| 772 |
self.assertTrue(sample["memory_masks"]["dynamic"].all().item())
|
|
@@ -786,7 +868,7 @@ class DeMemWMLatentDatasetTests(unittest.TestCase):
|
|
| 786 |
self.assertTrue(torch.equal(preprocessed["actions"][3:], torch.ones_like(preprocessed["actions"][3:])))
|
| 787 |
self.assertEqual(tuple(preprocessed["poses"].shape), (9, 1, 5))
|
| 788 |
self.assertIs(preprocessed["frame_memory_pose"], preprocessed["poses"])
|
| 789 |
-
self.assertEqual(preprocessed["frame_memory_pose"][:7, 0, 0].tolist(), [0.0, 1.0, 2.0, -
|
| 790 |
self.assertEqual(tuple(preprocessed["frame_indices"].shape), (9, 1))
|
| 791 |
self.assertEqual(preprocessed["memory_segments"], sample["memory_segments"])
|
| 792 |
self.assertEqual(preprocessed["segment_lengths"], sample["memory_segments"])
|
|
@@ -803,7 +885,7 @@ class DeMemWMLatentDatasetTests(unittest.TestCase):
|
|
| 803 |
{"anchor": (3, 5), "dynamic": (5, 7), "revisit": (7, 9)},
|
| 804 |
)
|
| 805 |
self.assertEqual(preprocessed["target_tensors"]["frame_indices"][:, 0].tolist(), [106, 107, 108])
|
| 806 |
-
self.assertEqual(preprocessed["stream_tensors"]["anchor"]["frame_indices"][:, 0].tolist(), [
|
| 807 |
self.assertEqual(preprocessed["stream_tensors"]["dynamic"]["frame_indices"][:, 0].tolist(), [104, 105])
|
| 808 |
self.assertEqual(preprocessed["stream_tensors"]["revisit"]["frame_indices"].shape[0], 2)
|
| 809 |
self.assertEqual(tuple(preprocessed["memory_masks"]["target"].shape), (1, 3))
|
|
|
|
| 1 |
+
import json
|
| 2 |
import tempfile
|
| 3 |
import unittest
|
| 4 |
from unittest import mock
|
|
|
|
| 44 |
def _dataset_cfg(root, **overrides):
|
| 45 |
cfg = {
|
| 46 |
"save_dir": str(root),
|
| 47 |
+
"precomputed_feature_dir": str(Path(root) / "vae_features"),
|
| 48 |
"n_frames": 3,
|
| 49 |
"n_frames_valid": 3,
|
| 50 |
"frame_skip": 1,
|
|
|
|
| 59 |
return OmegaConf.create(cfg)
|
| 60 |
|
| 61 |
|
| 62 |
+
def _write_vae_feature_clip(root, feature_root, split="training", subdir="scene", stem="000001", num_frames=104, actions=None, poses=None):
|
| 63 |
+
video_path = Path(root) / split / subdir / f"{stem}.mp4"
|
| 64 |
+
video_path.parent.mkdir(parents=True, exist_ok=True)
|
| 65 |
+
video_path.write_bytes(b"")
|
| 66 |
if actions is None:
|
| 67 |
+
actions = np.ones((num_frames, 8), dtype=np.int32)
|
| 68 |
if poses is None:
|
| 69 |
poses = _poses(num_frames)
|
| 70 |
+
np.savez(video_path.with_suffix(".npz"), actions=actions, poses=poses)
|
| 71 |
+
|
| 72 |
+
feature_dir = Path(feature_root) / split / subdir
|
| 73 |
+
feature_dir.mkdir(parents=True, exist_ok=True)
|
| 74 |
+
np.save(feature_dir / f"{stem}_vae_feature.npy", np.arange(num_frames * 2, dtype=np.float16).reshape(num_frames, 1, 1, 2))
|
| 75 |
+
with open(feature_dir / f"{stem}_vae_feature_meta.json", "w", encoding="utf-8") as handle:
|
| 76 |
+
json.dump({"image_height": 360, "image_width": 640}, handle)
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
|
| 79 |
class MemorySelectionTests(unittest.TestCase):
|
|
|
|
| 179 |
def test_dataset_requires_split_directory_without_root_fallback(self):
|
| 180 |
with tempfile.TemporaryDirectory() as tmp:
|
| 181 |
root = Path(tmp)
|
| 182 |
+
(root / "sample.mp4").write_bytes(b"")
|
| 183 |
|
| 184 |
with self.assertRaisesRegex(FileNotFoundError, "validation"):
|
| 185 |
MinecraftVideoDeMemWMLatentDataset(_dataset_cfg(root), split="validation")
|
|
|
|
| 187 |
def test_dataset_matches_original_w_updown_split_filtering(self):
|
| 188 |
with tempfile.TemporaryDirectory() as tmp:
|
| 189 |
root = Path(tmp)
|
| 190 |
+
_write_vae_feature_clip(root, root / "vae_features", split="validation", stem="plain")
|
| 191 |
+
_write_vae_feature_clip(root, root / "vae_features", split="validation", stem="clip_w_updown")
|
| 192 |
|
| 193 |
dataset = MinecraftVideoDeMemWMLatentDataset(_dataset_cfg(root), split="validation")
|
| 194 |
dataset_wo = MinecraftVideoDeMemWMLatentDataset(_dataset_cfg(root, wo_updown=True), split="validation")
|
| 195 |
|
| 196 |
+
self.assertEqual([path.name for path in dataset.data_paths], ["clip_w_updown.mp4"])
|
| 197 |
+
self.assertEqual([path.name for path in dataset_wo.data_paths], ["plain.mp4"])
|
| 198 |
|
| 199 |
def test_dataset_filters_nested_validation_files_by_selected_split(self):
|
| 200 |
with tempfile.TemporaryDirectory() as tmp:
|
| 201 |
root = Path(tmp)
|
| 202 |
+
_write_vae_feature_clip(root, root / "vae_features", split="validation", subdir="nested", stem="plain")
|
| 203 |
+
_write_vae_feature_clip(root, root / "vae_features", split="validation", subdir="nested", stem="clip_w_updown")
|
| 204 |
|
| 205 |
dataset = MinecraftVideoDeMemWMLatentDataset(_dataset_cfg(root), split="validation")
|
| 206 |
dataset_wo = MinecraftVideoDeMemWMLatentDataset(_dataset_cfg(root, wo_updown=True), split="validation")
|
| 207 |
|
| 208 |
+
self.assertEqual([path.name for path in dataset.data_paths], ["clip_w_updown.mp4"])
|
| 209 |
+
self.assertEqual([path.name for path in dataset_wo.data_paths], ["plain.mp4"])
|
| 210 |
|
| 211 |
+
def test_dataset_matches_original_fallback_when_split_filter_has_no_matches(self):
|
| 212 |
cases = [
|
| 213 |
+
("validation", False, "plain"),
|
| 214 |
+
("test", True, "clip_w_updown"),
|
| 215 |
]
|
| 216 |
+
for split, wo_updown, stem in cases:
|
| 217 |
+
with self.subTest(split=split, wo_updown=wo_updown), tempfile.TemporaryDirectory() as tmp:
|
| 218 |
root = Path(tmp)
|
| 219 |
+
_write_vae_feature_clip(root, root / "vae_features", split=split, subdir="nested", stem=stem)
|
| 220 |
+
dataset = MinecraftVideoDeMemWMLatentDataset(_dataset_cfg(root, wo_updown=wo_updown), split=split)
|
| 221 |
|
| 222 |
+
self.assertEqual([path.name for path in dataset.data_paths], [f"{stem}.mp4"])
|
|
|
|
|
|
|
| 223 |
|
| 224 |
def test_dataset_rejects_files_shorter_than_original_frame_skip_window(self):
|
| 225 |
with tempfile.TemporaryDirectory() as tmp:
|
| 226 |
root = Path(tmp)
|
| 227 |
+
_write_vae_feature_clip(root, root / "vae_features", stem="short", num_frames=102)
|
| 228 |
|
| 229 |
with self.assertRaisesRegex(ValueError, "requires at least 103"):
|
| 230 |
MinecraftVideoDeMemWMLatentDataset(_dataset_cfg(root), split="training")
|
|
|
|
| 234 |
root = Path(tmp)
|
| 235 |
poses = _poses(104)
|
| 236 |
poses[:, 1] = np.linspace(0.0, 3.0, 104, dtype=np.float32)
|
| 237 |
+
_write_vae_feature_clip(root, root / "vae_features", stem="sample", poses=poses)
|
| 238 |
dataset = MinecraftVideoDeMemWMLatentDataset(_dataset_cfg(root), split="training")
|
| 239 |
|
| 240 |
with self.assertRaisesRegex(ValueError, "Pose height variation"):
|
|
|
|
| 248 |
raw_actions[:, 3] = 11
|
| 249 |
raw_actions[:, 4] = 13
|
| 250 |
raw_actions[:, 7] = 1
|
| 251 |
+
_write_vae_feature_clip(root, root / "vae_features", stem="sample", actions=raw_actions)
|
| 252 |
sample = MinecraftVideoDeMemWMLatentDataset(_dataset_cfg(root), split="training")[0]
|
| 253 |
|
| 254 |
self.assertEqual(tuple(sample["actions"].shape), (3, 25))
|
|
|
|
| 257 |
self.assertTrue(torch.equal(sample["actions"][:, 15], -torch.ones(3)))
|
| 258 |
self.assertTrue(torch.equal(sample["actions"][:, 2], torch.ones(3)))
|
| 259 |
|
| 260 |
+
def test_dataset_reads_dememwm_vae_feature_layout(self):
|
| 261 |
+
with tempfile.TemporaryDirectory() as tmp:
|
| 262 |
+
root = Path(tmp) / "minecraft"
|
| 263 |
+
feature_root = root / "vae_features"
|
| 264 |
+
raw_actions = np.zeros((104, 8), dtype=np.int64)
|
| 265 |
+
raw_actions[:, 0] = 1
|
| 266 |
+
raw_actions[:, 3] = 11
|
| 267 |
+
raw_actions[:, 4] = 13
|
| 268 |
+
raw_actions[:, 7] = 1
|
| 269 |
+
_write_vae_feature_clip(root, feature_root, actions=raw_actions)
|
| 270 |
+
dataset = MinecraftVideoDeMemWMLatentDataset(_dataset_cfg(root), split="training")
|
| 271 |
+
sample = dataset[0]
|
| 272 |
+
|
| 273 |
+
self.assertEqual([path.suffix for path in dataset.data_paths], [".mp4"])
|
| 274 |
+
self.assertEqual(tuple(sample["latents"].shape), (3, 1, 1, 2))
|
| 275 |
+
self.assertEqual(tuple(sample["actions"].shape), (3, 25))
|
| 276 |
+
self.assertEqual(sample["image_hw"].tolist(), [360, 640])
|
| 277 |
+
self.assertTrue(torch.equal(sample["actions"][:, 11], torch.ones(3)))
|
| 278 |
+
self.assertTrue(torch.equal(sample["actions"][:, 16], torch.ones(3)))
|
| 279 |
+
self.assertTrue(torch.equal(sample["actions"][:, 15], -torch.ones(3)))
|
| 280 |
+
self.assertTrue(torch.equal(sample["actions"][:, 2], torch.ones(3)))
|
| 281 |
+
|
| 282 |
+
def test_dataset_uses_original_training_clip_window(self):
|
| 283 |
with tempfile.TemporaryDirectory() as tmp:
|
| 284 |
root = Path(tmp)
|
| 285 |
+
_write_vae_feature_clip(root, root / "vae_features", stem="sample", num_frames=1500)
|
| 286 |
+
dataset = MinecraftVideoDeMemWMLatentDataset(_dataset_cfg(root, n_frames=8), split="training")
|
| 287 |
+
|
| 288 |
+
self.assertEqual(len(dataset), 1300 - 8 + 1)
|
| 289 |
+
|
| 290 |
+
def test_dataset_reuses_cached_feature_arrays_for_same_file(self):
|
| 291 |
+
with tempfile.TemporaryDirectory() as tmp:
|
| 292 |
+
root = Path(tmp)
|
| 293 |
+
_write_vae_feature_clip(root, root / "vae_features", stem="sample")
|
| 294 |
dataset = MinecraftVideoDeMemWMLatentDataset(_dataset_cfg(root), split="training")
|
| 295 |
first = dataset[0]
|
| 296 |
|
|
|
|
| 743 |
def test_disabled_memory_selection_does_not_allocate_memory_slots(self):
|
| 744 |
with tempfile.TemporaryDirectory() as tmp:
|
| 745 |
root = Path(tmp)
|
| 746 |
+
_write_vae_feature_clip(root, root / "vae_features", stem="sample")
|
| 747 |
dataset = MinecraftVideoDeMemWMLatentDataset(
|
| 748 |
_dataset_cfg(root, memory_selection=_selection_cfg(enabled=False)),
|
| 749 |
split="training",
|
|
|
|
| 758 |
for key in ("anchor", "dynamic", "revisit"):
|
| 759 |
self.assertEqual(tuple(sample["memory_masks"][key].shape), (0,))
|
| 760 |
|
| 761 |
+
def test_dataset_excludes_initial_offset_from_memory_candidates(self):
|
| 762 |
+
with tempfile.TemporaryDirectory() as tmp:
|
| 763 |
+
root = Path(tmp)
|
| 764 |
+
_write_vae_feature_clip(
|
| 765 |
+
root,
|
| 766 |
+
root / "vae_features",
|
| 767 |
+
stem="sample",
|
| 768 |
+
num_frames=112,
|
| 769 |
+
actions=np.ones((112, 25), dtype=np.float32),
|
| 770 |
+
)
|
| 771 |
+
dataset = MinecraftVideoDeMemWMLatentDataset(
|
| 772 |
+
_dataset_cfg(root, memory_selection=_selection_cfg()),
|
| 773 |
+
split="training",
|
| 774 |
+
)
|
| 775 |
+
sample = dataset[0]
|
| 776 |
+
|
| 777 |
+
self.assertEqual(sample["frame_indices"][:3].tolist(), [100, 101, 102])
|
| 778 |
+
self.assertEqual(sample["frame_indices"][3:].tolist(), [-1, -1, -1, -1, -1, -1])
|
| 779 |
+
for key in ("anchor", "dynamic", "revisit"):
|
| 780 |
+
self.assertFalse(sample["memory_masks"][key].any().item())
|
| 781 |
+
|
| 782 |
+
def test_training_context_window_shifts_target_start_but_not_memory_floor(self):
|
| 783 |
+
with tempfile.TemporaryDirectory() as tmp:
|
| 784 |
+
root = Path(tmp)
|
| 785 |
+
_write_vae_feature_clip(
|
| 786 |
+
root,
|
| 787 |
+
root / "vae_features",
|
| 788 |
+
stem="sample",
|
| 789 |
+
num_frames=112,
|
| 790 |
+
actions=np.ones((112, 25), dtype=np.float32),
|
| 791 |
+
)
|
| 792 |
+
dataset = MinecraftVideoDeMemWMLatentDataset(
|
| 793 |
+
_dataset_cfg(
|
| 794 |
+
root,
|
| 795 |
+
context_length=4,
|
| 796 |
+
memory_selection=_selection_cfg(max_anchor_frames=2, max_dynamic_frames=2, max_revisit_frames=0),
|
| 797 |
+
),
|
| 798 |
+
split="training",
|
| 799 |
+
)
|
| 800 |
+
sample = dataset[0]
|
| 801 |
+
|
| 802 |
+
self.assertEqual(dataset.context_window, 4)
|
| 803 |
+
self.assertEqual(len(dataset), 112 - (100 + 4 + 3) + 1)
|
| 804 |
+
self.assertEqual(sample["frame_indices"].tolist(), [104, 105, 106, 100, 101, 102, 103])
|
| 805 |
+
self.assertTrue(sample["memory_masks"]["anchor"].all().item())
|
| 806 |
+
self.assertTrue(sample["memory_masks"]["dynamic"].all().item())
|
| 807 |
+
self.assertEqual(tuple(sample["memory_masks"]["revisit"].shape), (0,))
|
| 808 |
+
|
| 809 |
+
def test_validation_ignores_training_context_window_shift(self):
|
| 810 |
+
with tempfile.TemporaryDirectory() as tmp:
|
| 811 |
+
root = Path(tmp)
|
| 812 |
+
_write_vae_feature_clip(
|
| 813 |
+
root,
|
| 814 |
+
root / "vae_features",
|
| 815 |
+
split="validation",
|
| 816 |
+
stem="clip_w_updown",
|
| 817 |
+
num_frames=112,
|
| 818 |
+
actions=np.ones((112, 25), dtype=np.float32),
|
| 819 |
+
)
|
| 820 |
+
dataset = MinecraftVideoDeMemWMLatentDataset(
|
| 821 |
+
_dataset_cfg(root, context_length=4),
|
| 822 |
+
split="validation",
|
| 823 |
+
)
|
| 824 |
+
sample = dataset[0]
|
| 825 |
+
|
| 826 |
+
self.assertEqual(dataset.context_window, 0)
|
| 827 |
+
self.assertEqual(sample["frame_indices"].tolist(), [100, 101, 102])
|
| 828 |
+
|
| 829 |
def test_dataset_returns_target_anchor_dynamic_revisit_contract(self):
|
| 830 |
with tempfile.TemporaryDirectory() as tmp:
|
| 831 |
root = Path(tmp)
|
|
|
|
|
|
|
| 832 |
num_frames = 112
|
| 833 |
+
_write_vae_feature_clip(
|
| 834 |
+
root,
|
| 835 |
+
root / "vae_features",
|
| 836 |
+
stem="sample",
|
| 837 |
+
num_frames=num_frames,
|
| 838 |
actions=np.ones((num_frames, 25), dtype=np.float32),
|
|
|
|
|
|
|
|
|
|
| 839 |
)
|
| 840 |
+
dataset = MinecraftVideoDeMemWMLatentDataset(
|
| 841 |
+
_dataset_cfg(root, memory_selection=_selection_cfg()),
|
| 842 |
+
split="training",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 843 |
)
|
|
|
|
| 844 |
sample = dataset[6]
|
| 845 |
|
| 846 |
self.assertEqual(sample["memory_segments"], {"target": 3, "anchor": 2, "dynamic": 2, "revisit": 2})
|
| 847 |
self.assertEqual(tuple(sample["latents"].shape), (9, 1, 1, 2))
|
| 848 |
self.assertEqual(sample["frame_indices"][:3].tolist(), [106, 107, 108])
|
| 849 |
+
self.assertEqual(sample["frame_indices"][3:5].tolist(), [100, 101])
|
| 850 |
self.assertEqual(sample["frame_indices"][5:7].tolist(), [104, 105])
|
| 851 |
+
self.assertEqual(sample["frame_indices"][7:9].tolist(), [102, 103])
|
|
|
|
| 852 |
self.assertTrue(sample["memory_masks"]["target"].all().item())
|
| 853 |
self.assertTrue(sample["memory_masks"]["anchor"].all().item())
|
| 854 |
self.assertTrue(sample["memory_masks"]["dynamic"].all().item())
|
|
|
|
| 868 |
self.assertTrue(torch.equal(preprocessed["actions"][3:], torch.ones_like(preprocessed["actions"][3:])))
|
| 869 |
self.assertEqual(tuple(preprocessed["poses"].shape), (9, 1, 5))
|
| 870 |
self.assertIs(preprocessed["frame_memory_pose"], preprocessed["poses"])
|
| 871 |
+
self.assertEqual(preprocessed["frame_memory_pose"][:7, 0, 0].tolist(), [0.0, 1.0, 2.0, -6.0, -5.0, -2.0, -1.0])
|
| 872 |
self.assertEqual(tuple(preprocessed["frame_indices"].shape), (9, 1))
|
| 873 |
self.assertEqual(preprocessed["memory_segments"], sample["memory_segments"])
|
| 874 |
self.assertEqual(preprocessed["segment_lengths"], sample["memory_segments"])
|
|
|
|
| 885 |
{"anchor": (3, 5), "dynamic": (5, 7), "revisit": (7, 9)},
|
| 886 |
)
|
| 887 |
self.assertEqual(preprocessed["target_tensors"]["frame_indices"][:, 0].tolist(), [106, 107, 108])
|
| 888 |
+
self.assertEqual(preprocessed["stream_tensors"]["anchor"]["frame_indices"][:, 0].tolist(), [100, 101])
|
| 889 |
self.assertEqual(preprocessed["stream_tensors"]["dynamic"]["frame_indices"][:, 0].tolist(), [104, 105])
|
| 890 |
self.assertEqual(preprocessed["stream_tensors"]["revisit"]["frame_indices"].shape[0], 2)
|
| 891 |
self.assertEqual(tuple(preprocessed["memory_masks"]["target"].shape), (1, 3))
|