mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-24 18:26:11 +00:00
Fix GROOT relative action training stats
This commit is contained in:
@@ -581,27 +581,31 @@ def _resolve_visual_modality_keys_from_dataset_meta(dataset_meta: Any | None) ->
|
|||||||
return keys or None
|
return keys or None
|
||||||
|
|
||||||
|
|
||||||
def _slice_stats_entry(stats: dict[str, Any], indices: list[int]) -> dict[str, list[float]]:
|
def _slice_stats_entry(stats: dict[str, Any], indices: list[int]) -> dict[str, Any]:
|
||||||
if not indices:
|
if not indices:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
max_index = max(indices)
|
max_index = max(indices)
|
||||||
sliced: dict[str, list[float]] = {}
|
sliced: dict[str, Any] = {}
|
||||||
for stat_name, value in stats.items():
|
for stat_name, value in stats.items():
|
||||||
tensor = torch.as_tensor(value, dtype=torch.float32).flatten()
|
tensor = torch.as_tensor(value, dtype=torch.float32)
|
||||||
|
if tensor.ndim >= 2:
|
||||||
|
if tensor.shape[-1] <= max_index:
|
||||||
|
continue
|
||||||
|
sliced[stat_name] = tensor[..., indices].tolist()
|
||||||
|
else:
|
||||||
|
tensor = tensor.flatten()
|
||||||
if tensor.numel() <= max_index:
|
if tensor.numel() <= max_index:
|
||||||
continue
|
continue
|
||||||
sliced[stat_name] = [float(tensor[index].item()) for index in indices]
|
sliced[stat_name] = [float(tensor[index].item()) for index in indices]
|
||||||
|
|
||||||
if "min" in sliced and "max" in sliced:
|
if "min" in sliced and "max" in sliced:
|
||||||
|
min_arr = np.asarray(sliced["min"], dtype=np.float32)
|
||||||
|
max_arr = np.asarray(sliced["max"], dtype=np.float32)
|
||||||
if "mean" not in sliced:
|
if "mean" not in sliced:
|
||||||
sliced["mean"] = [
|
sliced["mean"] = ((min_arr + max_arr) * 0.5).tolist()
|
||||||
(low + high) * 0.5 for low, high in zip(sliced["min"], sliced["max"], strict=True)
|
|
||||||
]
|
|
||||||
if "std" not in sliced:
|
if "std" not in sliced:
|
||||||
sliced["std"] = [
|
sliced["std"] = (np.abs(max_arr - min_arr) * 0.5).tolist()
|
||||||
abs(high - low) * 0.5 for low, high in zip(sliced["min"], sliced["max"], strict=True)
|
|
||||||
]
|
|
||||||
return sliced
|
return sliced
|
||||||
|
|
||||||
|
|
||||||
@@ -887,6 +891,7 @@ def make_groot_pre_post_processors(
|
|||||||
clip_outliers=clip_outliers,
|
clip_outliers=clip_outliers,
|
||||||
video_modality_keys=video_modality_keys,
|
video_modality_keys=video_modality_keys,
|
||||||
raw_stats=checkpoint_assets.raw_stats if checkpoint_assets is not None else None,
|
raw_stats=checkpoint_assets.raw_stats if checkpoint_assets is not None else None,
|
||||||
|
use_percentiles=checkpoint_assets.use_percentiles if checkpoint_assets is not None else False,
|
||||||
modality_config=checkpoint_assets.modality_config if checkpoint_assets is not None else None,
|
modality_config=checkpoint_assets.modality_config if checkpoint_assets is not None else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1179,6 +1184,7 @@ class GrootN17PackInputsStep(ProcessorStep):
|
|||||||
normalize_min_max: bool = True
|
normalize_min_max: bool = True
|
||||||
stats: dict[str, dict[str, Any]] | None = None
|
stats: dict[str, dict[str, Any]] | None = None
|
||||||
clip_outliers: bool = True
|
clip_outliers: bool = True
|
||||||
|
use_percentiles: bool = False
|
||||||
video_modality_keys: list[str] | None = None
|
video_modality_keys: list[str] | None = None
|
||||||
raw_stats: dict[str, Any] | None = None
|
raw_stats: dict[str, Any] | None = None
|
||||||
modality_config: dict[str, Any] | None = None
|
modality_config: dict[str, Any] | None = None
|
||||||
@@ -1327,6 +1333,73 @@ class GrootN17PackInputsStep(ProcessorStep):
|
|||||||
|
|
||||||
return converted
|
return converted
|
||||||
|
|
||||||
|
def _normalize_action_groups_for_training(self, action: torch.Tensor) -> torch.Tensor | None:
|
||||||
|
if self.modality_config is None or self.raw_stats is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
action_config = self.modality_config.get("action", {})
|
||||||
|
if not isinstance(action_config, dict):
|
||||||
|
return None
|
||||||
|
action_keys = action_config.get("modality_keys", [])
|
||||||
|
action_configs = action_config.get("action_configs", [])
|
||||||
|
if not isinstance(action_keys, list) or not isinstance(action_configs, list):
|
||||||
|
return None
|
||||||
|
|
||||||
|
normalized_groups: list[torch.Tensor] = []
|
||||||
|
start_idx = 0
|
||||||
|
for idx, key in enumerate(action_keys):
|
||||||
|
if not isinstance(key, str):
|
||||||
|
continue
|
||||||
|
cfg = (
|
||||||
|
action_configs[idx]
|
||||||
|
if idx < len(action_configs) and isinstance(action_configs[idx], dict)
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
is_relative = config_value(cfg.get("rep")) == "relative"
|
||||||
|
stats_modality = "relative_action" if is_relative else "action"
|
||||||
|
key_stats = self.raw_stats.get(stats_modality, {}).get(key, {})
|
||||||
|
dim = stat_dim_from_entry(key_stats) if isinstance(key_stats, dict) else 0
|
||||||
|
if dim <= 0:
|
||||||
|
continue
|
||||||
|
end_idx = start_idx + dim
|
||||||
|
if end_idx > action.shape[-1]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
min_v, max_v = _n1_7_decode_stats_for_action(
|
||||||
|
self.raw_stats,
|
||||||
|
key,
|
||||||
|
cfg,
|
||||||
|
use_relative_action=True,
|
||||||
|
use_percentiles=self.use_percentiles,
|
||||||
|
)
|
||||||
|
group = action[..., start_idx:end_idx]
|
||||||
|
min_t = torch.as_tensor(min_v, dtype=group.dtype, device=group.device)
|
||||||
|
max_t = torch.as_tensor(max_v, dtype=group.dtype, device=group.device)
|
||||||
|
if min_t.ndim == 1:
|
||||||
|
min_t = min_t.view(1, 1, -1)
|
||||||
|
max_t = max_t.view(1, 1, -1)
|
||||||
|
elif min_t.ndim == 2:
|
||||||
|
if group.shape[1] > min_t.shape[0]:
|
||||||
|
return None
|
||||||
|
min_t = min_t[: group.shape[1]].unsqueeze(0)
|
||||||
|
max_t = max_t[: group.shape[1]].unsqueeze(0)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
denom = max_t - min_t
|
||||||
|
mask = denom != 0
|
||||||
|
safe_denom = torch.where(mask, denom, torch.ones_like(denom))
|
||||||
|
normalized = torch.where(mask, 2 * (group - min_t) / safe_denom - 1, torch.zeros_like(group))
|
||||||
|
if self.clip_outliers:
|
||||||
|
normalized = normalized.clamp(-1.0, 1.0)
|
||||||
|
normalized_groups.append(normalized)
|
||||||
|
start_idx = end_idx
|
||||||
|
|
||||||
|
if not normalized_groups or start_idx != action.shape[-1]:
|
||||||
|
return None
|
||||||
|
return torch.cat(normalized_groups, dim=-1)
|
||||||
|
|
||||||
|
|
||||||
def __call__(self, transition: EnvTransition) -> EnvTransition:
|
def __call__(self, transition: EnvTransition) -> EnvTransition:
|
||||||
obs = transition.get(TransitionKey.OBSERVATION, {}) or {}
|
obs = transition.get(TransitionKey.OBSERVATION, {}) or {}
|
||||||
comp = transition.get(TransitionKey.COMPLEMENTARY_DATA, {}) or {}
|
comp = transition.get(TransitionKey.COMPLEMENTARY_DATA, {}) or {}
|
||||||
@@ -1441,6 +1514,10 @@ class GrootN17PackInputsStep(ProcessorStep):
|
|||||||
if raw_state_for_action is not None:
|
if raw_state_for_action is not None:
|
||||||
action = self._convert_relative_action_groups_for_training(action, raw_state_for_action)
|
action = self._convert_relative_action_groups_for_training(action, raw_state_for_action)
|
||||||
if self.normalize_min_max:
|
if self.normalize_min_max:
|
||||||
|
normalized_action = self._normalize_action_groups_for_training(action)
|
||||||
|
if normalized_action is not None:
|
||||||
|
action = normalized_action
|
||||||
|
else:
|
||||||
flat = _min_max_norm(action.reshape(bsz * horizon, dim), ACTION)
|
flat = _min_max_norm(action.reshape(bsz * horizon, dim), ACTION)
|
||||||
action = flat.view(bsz, horizon, dim)
|
action = flat.view(bsz, horizon, dim)
|
||||||
valid_dim = min(dim, self.max_action_dim)
|
valid_dim = min(dim, self.max_action_dim)
|
||||||
@@ -1500,6 +1577,7 @@ class GrootN17PackInputsStep(ProcessorStep):
|
|||||||
"embodiment_mapping": self.embodiment_mapping,
|
"embodiment_mapping": self.embodiment_mapping,
|
||||||
"normalize_min_max": self.normalize_min_max,
|
"normalize_min_max": self.normalize_min_max,
|
||||||
"clip_outliers": self.clip_outliers,
|
"clip_outliers": self.clip_outliers,
|
||||||
|
"use_percentiles": self.use_percentiles,
|
||||||
"video_modality_keys": self.video_modality_keys,
|
"video_modality_keys": self.video_modality_keys,
|
||||||
"raw_stats": self.raw_stats,
|
"raw_stats": self.raw_stats,
|
||||||
"modality_config": self.modality_config,
|
"modality_config": self.modality_config,
|
||||||
|
|||||||
@@ -111,7 +111,14 @@ def has_modality_stats(stats: dict[str, dict[str, Any]] | None) -> bool:
|
|||||||
def stat_dim_from_entry(entry: dict[str, Any]) -> int:
|
def stat_dim_from_entry(entry: dict[str, Any]) -> int:
|
||||||
for stat_name in ("mean", "q01", "min", "max", "std"):
|
for stat_name in ("mean", "q01", "min", "max", "std"):
|
||||||
value = entry.get(stat_name)
|
value = entry.get(stat_name)
|
||||||
|
if isinstance(value, torch.Tensor):
|
||||||
|
return int(value.shape[-1]) if value.ndim > 0 else 1
|
||||||
|
if isinstance(value, np.ndarray):
|
||||||
|
return int(value.shape[-1]) if value.ndim > 0 else 1
|
||||||
if isinstance(value, list) and len(value) > 0:
|
if isinstance(value, list) and len(value) > 0:
|
||||||
|
first = value[0]
|
||||||
|
if isinstance(first, (list, tuple)) and len(first) > 0:
|
||||||
|
return len(first)
|
||||||
return len(value)
|
return len(value)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ from typing import TYPE_CHECKING, Any
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from accelerate import Accelerator
|
from accelerate import Accelerator
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
import torch
|
import torch
|
||||||
from termcolor import colored
|
from termcolor import colored
|
||||||
from torch.optim import Optimizer
|
from torch.optim import Optimizer
|
||||||
@@ -228,6 +229,63 @@ def _unpadded_relative_action_vectors(relative_action: torch.Tensor, pad_mask: A
|
|||||||
return relative_action.reshape(-1, relative_action.shape[-1])
|
return relative_action.reshape(-1, relative_action.shape[-1])
|
||||||
|
|
||||||
|
|
||||||
|
def _relative_action_chunks_by_horizon(
|
||||||
|
relative_action: torch.Tensor, pad_mask: Any | None
|
||||||
|
) -> list[list[np.ndarray]]:
|
||||||
|
"""Return per-horizon lists of valid relative action vectors."""
|
||||||
|
|
||||||
|
if relative_action.ndim == 2:
|
||||||
|
relative_action = relative_action.unsqueeze(0)
|
||||||
|
if relative_action.ndim != 3:
|
||||||
|
raise ValueError(
|
||||||
|
"Cannot compute horizon-preserving relative action statistics from "
|
||||||
|
f"shape {tuple(relative_action.shape)}."
|
||||||
|
)
|
||||||
|
|
||||||
|
batch_size, horizon, _action_dim = relative_action.shape
|
||||||
|
keep = torch.ones(batch_size, horizon, dtype=torch.bool)
|
||||||
|
if pad_mask is not None:
|
||||||
|
mask = torch.as_tensor(pad_mask, dtype=torch.bool).cpu()
|
||||||
|
if mask.ndim == 1 and batch_size == 1 and mask.numel() == horizon:
|
||||||
|
keep[0] = ~mask
|
||||||
|
elif mask.ndim == 2 and tuple(mask.shape) == (batch_size, horizon):
|
||||||
|
keep = ~mask
|
||||||
|
|
||||||
|
chunks: list[list[np.ndarray]] = [[] for _ in range(horizon)]
|
||||||
|
relative_np = relative_action.detach().cpu().numpy()
|
||||||
|
for batch_idx in range(batch_size):
|
||||||
|
for horizon_idx in range(horizon):
|
||||||
|
if keep[batch_idx, horizon_idx]:
|
||||||
|
chunks[horizon_idx].append(relative_np[batch_idx, horizon_idx])
|
||||||
|
return chunks
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_horizon_relative_action_stats(chunks_by_horizon: list[list[np.ndarray]]) -> dict[str, np.ndarray]:
|
||||||
|
if not chunks_by_horizon or not any(chunks_by_horizon):
|
||||||
|
raise ValueError("Cannot compute relative action statistics without unpadded action vectors.")
|
||||||
|
|
||||||
|
stats: dict[str, list[np.ndarray]] = {key: [] for key in ("min", "max", "mean", "std", "q01", "q99")}
|
||||||
|
counts: list[int] = []
|
||||||
|
for horizon_idx, vectors in enumerate(chunks_by_horizon):
|
||||||
|
if len(vectors) < 2:
|
||||||
|
raise ValueError(
|
||||||
|
"Cannot compute horizon-preserving relative action statistics from fewer than 2 "
|
||||||
|
f"unpadded vectors at action timestep {horizon_idx}."
|
||||||
|
)
|
||||||
|
values = np.stack(vectors, axis=0).astype(np.float32)
|
||||||
|
stats["min"].append(np.min(values, axis=0))
|
||||||
|
stats["max"].append(np.max(values, axis=0))
|
||||||
|
stats["mean"].append(np.mean(values, axis=0))
|
||||||
|
stats["std"].append(np.std(values, axis=0))
|
||||||
|
stats["q01"].append(np.quantile(values, 0.01, axis=0).astype(np.float32))
|
||||||
|
stats["q99"].append(np.quantile(values, 0.99, axis=0).astype(np.float32))
|
||||||
|
counts.append(len(vectors))
|
||||||
|
|
||||||
|
computed = {key: np.stack(values, axis=0) for key, values in stats.items()}
|
||||||
|
computed["count"] = np.asarray(counts, dtype=np.int64)
|
||||||
|
return computed
|
||||||
|
|
||||||
|
|
||||||
def _iter_action_state_training_samples(dataset: Any):
|
def _iter_action_state_training_samples(dataset: Any):
|
||||||
"""Yield action chunks, reference states, and action padding masks without decoding videos when possible."""
|
"""Yield action chunks, reference states, and action padding masks without decoding videos when possible."""
|
||||||
|
|
||||||
@@ -271,6 +329,7 @@ def _make_relative_action_training_stats(
|
|||||||
*,
|
*,
|
||||||
exclude_joints: list[str] | None,
|
exclude_joints: list[str] | None,
|
||||||
action_names: list[str] | None,
|
action_names: list[str] | None,
|
||||||
|
preserve_action_horizon: bool = False,
|
||||||
) -> dict[str, dict[str, Any]]:
|
) -> dict[str, dict[str, Any]]:
|
||||||
"""Return dataset stats whose action entry describes the relative action tensor used for training."""
|
"""Return dataset stats whose action entry describes the relative action tensor used for training."""
|
||||||
|
|
||||||
@@ -296,6 +355,7 @@ def _make_relative_action_training_stats(
|
|||||||
action_names=action_names,
|
action_names=action_names,
|
||||||
)
|
)
|
||||||
num_vectors = 0
|
num_vectors = 0
|
||||||
|
chunks_by_horizon: list[list[np.ndarray]] | None = None
|
||||||
|
|
||||||
for action_value, state_value, pad_mask in _iter_action_state_training_samples(dataset):
|
for action_value, state_value, pad_mask in _iter_action_state_training_samples(dataset):
|
||||||
action = _to_float_tensor(action_value, key=ACTION)
|
action = _to_float_tensor(action_value, key=ACTION)
|
||||||
@@ -316,6 +376,19 @@ def _make_relative_action_training_stats(
|
|||||||
state_batch,
|
state_batch,
|
||||||
relative_step._build_mask(action_batch.shape[-1]),
|
relative_step._build_mask(action_batch.shape[-1]),
|
||||||
)
|
)
|
||||||
|
if preserve_action_horizon:
|
||||||
|
sample_chunks = _relative_action_chunks_by_horizon(relative_action, pad_mask)
|
||||||
|
if chunks_by_horizon is None:
|
||||||
|
chunks_by_horizon = [[] for _ in range(len(sample_chunks))]
|
||||||
|
if len(sample_chunks) != len(chunks_by_horizon):
|
||||||
|
raise ValueError(
|
||||||
|
"Cannot compute horizon-preserving relative action statistics from samples with "
|
||||||
|
f"different action horizons ({len(sample_chunks)} vs {len(chunks_by_horizon)})."
|
||||||
|
)
|
||||||
|
for horizon_idx, vectors in enumerate(sample_chunks):
|
||||||
|
chunks_by_horizon[horizon_idx].extend(vectors)
|
||||||
|
num_vectors += len(vectors)
|
||||||
|
else:
|
||||||
vectors = _unpadded_relative_action_vectors(relative_action, pad_mask)
|
vectors = _unpadded_relative_action_vectors(relative_action, pad_mask)
|
||||||
if vectors.numel() == 0:
|
if vectors.numel() == 0:
|
||||||
continue
|
continue
|
||||||
@@ -328,7 +401,11 @@ def _make_relative_action_training_stats(
|
|||||||
"Cannot compute relative action statistics from fewer than 2 unpadded action vectors."
|
"Cannot compute relative action statistics from fewer than 2 unpadded action vectors."
|
||||||
)
|
)
|
||||||
|
|
||||||
stats[ACTION] = running_stats.get_statistics()
|
stats[ACTION] = (
|
||||||
|
_compute_horizon_relative_action_stats(chunks_by_horizon or [])
|
||||||
|
if preserve_action_horizon
|
||||||
|
else running_stats.get_statistics()
|
||||||
|
)
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
|
|
||||||
@@ -472,6 +549,7 @@ def train(cfg: TrainPipelineConfig, accelerator: "Accelerator | None" = None):
|
|||||||
dataset,
|
dataset,
|
||||||
exclude_joints=getattr(active_cfg, "relative_exclude_joints", []),
|
exclude_joints=getattr(active_cfg, "relative_exclude_joints", []),
|
||||||
action_names=_resolve_action_feature_names(dataset),
|
action_names=_resolve_action_feature_names(dataset),
|
||||||
|
preserve_action_horizon=getattr(active_cfg, "type", None) == "groot",
|
||||||
)
|
)
|
||||||
|
|
||||||
processor_kwargs = {}
|
processor_kwargs = {}
|
||||||
|
|||||||
@@ -1872,10 +1872,11 @@ def test_groot_n1_7_relative_action_training_processors_save_native_grouped_stat
|
|||||||
_RelativeStatsDataset(),
|
_RelativeStatsDataset(),
|
||||||
exclude_joints=["gripper"],
|
exclude_joints=["gripper"],
|
||||||
action_names=action_names,
|
action_names=action_names,
|
||||||
|
preserve_action_horizon=True,
|
||||||
)
|
)
|
||||||
expected_relative_action_stats = {
|
expected_relative_action_stats = {
|
||||||
"min": torch.tensor([-2.0, -3.0, -4.0, -5.0, -6.0, 0.0]),
|
"min": torch.tensor([-2.0, -3.0, -4.0, -5.0, -6.0, 1.0, 2.0, 3.0, 4.0, 5.0, 0.0]),
|
||||||
"max": torch.tensor([2.0, 3.0, 4.0, 5.0, 6.0, 100.0]),
|
"max": torch.tensor([-1.0, -2.0, -3.0, -4.0, -5.0, 2.0, 3.0, 4.0, 5.0, 6.0, 100.0]),
|
||||||
}
|
}
|
||||||
|
|
||||||
preprocessor, postprocessor = make_groot_pre_post_processors(
|
preprocessor, postprocessor = make_groot_pre_post_processors(
|
||||||
@@ -1899,7 +1900,10 @@ def test_groot_n1_7_relative_action_training_processors_save_native_grouped_stat
|
|||||||
{"rep": "RELATIVE", "type": "NON_EEF", "format": "DEFAULT", "state_key": None},
|
{"rep": "RELATIVE", "type": "NON_EEF", "format": "DEFAULT", "state_key": None},
|
||||||
{"rep": "ABSOLUTE", "type": "NON_EEF", "format": "DEFAULT", "state_key": None},
|
{"rep": "ABSOLUTE", "type": "NON_EEF", "format": "DEFAULT", "state_key": None},
|
||||||
]
|
]
|
||||||
assert pack_config["raw_stats"]["relative_action"]["single_arm"]["min"] == [-2.0, -3.0, -4.0, -5.0, -6.0]
|
assert pack_config["raw_stats"]["relative_action"]["single_arm"]["min"] == [
|
||||||
|
[-2.0, -3.0, -4.0, -5.0, -6.0],
|
||||||
|
[1.0, 2.0, 3.0, 4.0, 5.0],
|
||||||
|
]
|
||||||
assert pack_config["raw_stats"]["action"]["gripper"]["min"] == [0.0]
|
assert pack_config["raw_stats"]["action"]["gripper"]["min"] == [0.0]
|
||||||
assert pack_config["raw_stats"]["action"]["gripper"]["max"] == [100.0]
|
assert pack_config["raw_stats"]["action"]["gripper"]["max"] == [100.0]
|
||||||
|
|
||||||
@@ -1918,10 +1922,185 @@ def test_groot_n1_7_relative_action_training_processors_save_native_grouped_stat
|
|||||||
)
|
)
|
||||||
decode_config = decode_entry["config"]
|
decode_config = decode_entry["config"]
|
||||||
assert decode_config["use_relative_action"] is True
|
assert decode_config["use_relative_action"] is True
|
||||||
assert decode_config["raw_stats"]["relative_action"]["single_arm"]["max"] == [2.0, 3.0, 4.0, 5.0, 6.0]
|
assert decode_config["raw_stats"]["relative_action"]["single_arm"]["max"] == [
|
||||||
|
[-1.0, -2.0, -3.0, -4.0, -5.0],
|
||||||
|
[2.0, 3.0, 4.0, 5.0, 6.0],
|
||||||
|
]
|
||||||
assert decode_config["raw_stats"]["action"]["gripper"]["max"] == [100.0]
|
assert decode_config["raw_stats"]["action"]["gripper"]["max"] == [100.0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_groot_n1_7_generated_relative_stats_match_oss_gr00t_reference_numbers():
|
||||||
|
input_features, output_features = _groot_features(state_dim=6, action_dim=6)
|
||||||
|
action_names = [
|
||||||
|
"shoulder_pan.pos",
|
||||||
|
"shoulder_lift.pos",
|
||||||
|
"elbow_flex.pos",
|
||||||
|
"wrist_flex.pos",
|
||||||
|
"wrist_roll.pos",
|
||||||
|
"gripper.pos",
|
||||||
|
]
|
||||||
|
config = GrootConfig(
|
||||||
|
input_features=input_features,
|
||||||
|
output_features=output_features,
|
||||||
|
device="cpu",
|
||||||
|
use_bf16=False,
|
||||||
|
action_decode_transform=None,
|
||||||
|
chunk_size=3,
|
||||||
|
n_action_steps=3,
|
||||||
|
use_relative_actions=True,
|
||||||
|
relative_exclude_joints=["gripper"],
|
||||||
|
)
|
||||||
|
absolute_dataset_stats = {
|
||||||
|
OBS_STATE: {
|
||||||
|
"min": torch.tensor([-20.0, -30.0, -40.0, -50.0, -60.0, 0.0]),
|
||||||
|
"max": torch.tensor([80.0, 70.0, 60.0, 50.0, 40.0, 100.0]),
|
||||||
|
"mean": torch.tensor([30.0, 20.0, 10.0, 0.0, -10.0, 50.0]),
|
||||||
|
"std": torch.tensor([10.0, 10.0, 10.0, 10.0, 10.0, 10.0]),
|
||||||
|
"q01": torch.tensor([-10.0, -20.0, -30.0, -40.0, -50.0, 10.0]),
|
||||||
|
"q99": torch.tensor([70.0, 60.0, 50.0, 40.0, 30.0, 90.0]),
|
||||||
|
},
|
||||||
|
ACTION: {
|
||||||
|
"min": torch.tensor([-5.0, -20.0, 0.0, -25.0, 10.0, 20.0]),
|
||||||
|
"max": torch.tensor([20.0, 30.0, 45.0, 60.0, 70.0, 90.0]),
|
||||||
|
"mean": torch.tensor([5.0, 5.0, 20.0, 20.0, 40.0, 55.0]),
|
||||||
|
"std": torch.tensor([5.0, 10.0, 10.0, 20.0, 20.0, 25.0]),
|
||||||
|
"q01": torch.tensor([-4.0, -19.0, 1.0, -24.0, 11.0, 20.0]),
|
||||||
|
"q99": torch.tensor([19.0, 29.0, 44.0, 59.0, 69.0, 90.0]),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
state_a = torch.tensor([10.0, 20.0, 30.0, 40.0, 50.0, 25.0])
|
||||||
|
state_b = torch.tensor([0.0, -10.0, 10.0, -20.0, 20.0, 75.0])
|
||||||
|
action_a = torch.tensor(
|
||||||
|
[
|
||||||
|
[11.0, 22.0, 33.0, 44.0, 55.0, 20.0],
|
||||||
|
[12.0, 24.0, 36.0, 48.0, 60.0, 80.0],
|
||||||
|
[13.0, 26.0, 39.0, 52.0, 65.0, 90.0],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
action_b = torch.tensor(
|
||||||
|
[
|
||||||
|
[-1.0, -8.0, 13.0, -16.0, 25.0, 30.0],
|
||||||
|
[-2.0, -6.0, 16.0, -12.0, 30.0, 40.0],
|
||||||
|
[-3.0, -4.0, 19.0, -8.0, 35.0, 50.0],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
samples = [
|
||||||
|
{OBS_STATE: state_a, ACTION: action_a},
|
||||||
|
{OBS_STATE: state_b, ACTION: action_b},
|
||||||
|
]
|
||||||
|
|
||||||
|
class _Dataset:
|
||||||
|
meta = SimpleNamespace(
|
||||||
|
stats=absolute_dataset_stats,
|
||||||
|
features={ACTION: {"names": action_names}},
|
||||||
|
)
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(samples)
|
||||||
|
|
||||||
|
def __getitem__(self, idx):
|
||||||
|
return samples[idx]
|
||||||
|
|
||||||
|
relative_dataset_stats = _make_relative_action_training_stats(
|
||||||
|
_Dataset(),
|
||||||
|
exclude_joints=["gripper"],
|
||||||
|
action_names=action_names,
|
||||||
|
preserve_action_horizon=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Static reference values from OSS GR00T's JointActionChunk.relative_chunking +
|
||||||
|
# calculate_stats_for_key path: stats are computed per chunk timestep, not
|
||||||
|
# flattened over all timesteps.
|
||||||
|
oss_arm_min = torch.tensor(
|
||||||
|
[
|
||||||
|
[-1.0, 2.0, 3.0, 4.0, 5.0],
|
||||||
|
[-2.0, 4.0, 6.0, 8.0, 10.0],
|
||||||
|
[-3.0, 6.0, 9.0, 12.0, 15.0],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
oss_arm_max = torch.tensor(
|
||||||
|
[
|
||||||
|
[1.0, 2.0, 3.0, 4.0, 5.0],
|
||||||
|
[2.0, 4.0, 6.0, 8.0, 10.0],
|
||||||
|
[3.0, 6.0, 9.0, 12.0, 15.0],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
oss_arm_mean = torch.tensor(
|
||||||
|
[
|
||||||
|
[0.0, 2.0, 3.0, 4.0, 5.0],
|
||||||
|
[0.0, 4.0, 6.0, 8.0, 10.0],
|
||||||
|
[0.0, 6.0, 9.0, 12.0, 15.0],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
oss_arm_std = torch.tensor(
|
||||||
|
[
|
||||||
|
[1.0, 0.0, 0.0, 0.0, 0.0],
|
||||||
|
[2.0, 0.0, 0.0, 0.0, 0.0],
|
||||||
|
[3.0, 0.0, 0.0, 0.0, 0.0],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
oss_arm_q01 = torch.tensor(
|
||||||
|
[
|
||||||
|
[-0.98, 2.0, 3.0, 4.0, 5.0],
|
||||||
|
[-1.96, 4.0, 6.0, 8.0, 10.0],
|
||||||
|
[-2.94, 6.0, 9.0, 12.0, 15.0],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
oss_arm_q99 = torch.tensor(
|
||||||
|
[
|
||||||
|
[0.98, 2.0, 3.0, 4.0, 5.0],
|
||||||
|
[1.96, 4.0, 6.0, 8.0, 10.0],
|
||||||
|
[2.94, 6.0, 9.0, 12.0, 15.0],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
torch.testing.assert_close(torch.as_tensor(relative_dataset_stats[ACTION]["min"][:, :5]), oss_arm_min)
|
||||||
|
torch.testing.assert_close(torch.as_tensor(relative_dataset_stats[ACTION]["max"][:, :5]), oss_arm_max)
|
||||||
|
torch.testing.assert_close(torch.as_tensor(relative_dataset_stats[ACTION]["mean"][:, :5]), oss_arm_mean)
|
||||||
|
torch.testing.assert_close(torch.as_tensor(relative_dataset_stats[ACTION]["std"][:, :5]), oss_arm_std)
|
||||||
|
torch.testing.assert_close(torch.as_tensor(relative_dataset_stats[ACTION]["q01"][:, :5]), oss_arm_q01)
|
||||||
|
torch.testing.assert_close(torch.as_tensor(relative_dataset_stats[ACTION]["q99"][:, :5]), oss_arm_q99)
|
||||||
|
|
||||||
|
preprocessor, postprocessor = make_groot_pre_post_processors(
|
||||||
|
config,
|
||||||
|
dataset_stats=relative_dataset_stats,
|
||||||
|
dataset_meta=_Dataset.meta,
|
||||||
|
)
|
||||||
|
pack_step = next(step for step in preprocessor.steps if isinstance(step, GrootN17PackInputsStep))
|
||||||
|
decode_step = next(step for step in postprocessor.steps if isinstance(step, GrootN17ActionDecodeStep))
|
||||||
|
|
||||||
|
assert pack_step.use_percentiles is True
|
||||||
|
torch.testing.assert_close(
|
||||||
|
torch.as_tensor(pack_step.raw_stats["relative_action"]["single_arm"]["min"]),
|
||||||
|
oss_arm_min,
|
||||||
|
)
|
||||||
|
torch.testing.assert_close(
|
||||||
|
torch.as_tensor(pack_step.raw_stats["relative_action"]["single_arm"]["q99"]),
|
||||||
|
oss_arm_q99,
|
||||||
|
)
|
||||||
|
assert pack_step.stats[ACTION]["min"] == pytest.approx([*oss_arm_min.flatten().tolist(), 20.0])
|
||||||
|
assert pack_step.stats[ACTION]["max"] == pytest.approx([*oss_arm_max.flatten().tolist(), 90.0])
|
||||||
|
|
||||||
|
packed = pack_step(
|
||||||
|
{
|
||||||
|
TransitionKey.OBSERVATION: {OBS_STATE: state_a.unsqueeze(0)},
|
||||||
|
TransitionKey.ACTION: action_a.unsqueeze(0),
|
||||||
|
TransitionKey.COMPLEMENTARY_DATA: {"task": ["Move the vial"]},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expected_normalized = torch.tensor(
|
||||||
|
[
|
||||||
|
[1.0, 0.0, 0.0, 0.0, 0.0, -1.0],
|
||||||
|
[1.0, 0.0, 0.0, 0.0, 0.0, 5.0 / 7.0],
|
||||||
|
[1.0, 0.0, 0.0, 0.0, 0.0, 1.0],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
torch.testing.assert_close(packed[TransitionKey.ACTION][0, :3, :6], expected_normalized)
|
||||||
|
|
||||||
|
decoded = decode_step({TransitionKey.ACTION: packed[TransitionKey.ACTION]})
|
||||||
|
torch.testing.assert_close(decoded[TransitionKey.ACTION], action_a.unsqueeze(0), atol=1e-5, rtol=1e-5)
|
||||||
|
|
||||||
|
|
||||||
def test_groot_policy_selects_n1_7_model_class(monkeypatch):
|
def test_groot_policy_selects_n1_7_model_class(monkeypatch):
|
||||||
from lerobot.policies.groot.groot_n1_7 import GR00TN17
|
from lerobot.policies.groot.groot_n1_7 import GR00TN17
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user