mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-07 18:11:50 +00:00
c764afb8ef
The LIBERO/RoboTwin action (un)normalization quantiles were hardcoded as module constants in processor_lingbot_va.py. They are already serialized into each checkpoint's policy_postprocessor.json (via LingBotVAActionUnnormalizeStep.get_config) and restored on load by PolicyProcessorPipeline.from_pretrained, so the constants are dead at eval/load time for the released checkpoints (verified: libero_long/robotwin/base all carry their quantiles on the Hub). - Remove LIBERO_ACTION_Q01/Q99, ROBOTWIN_ACTION_Q01/Q99 and _default_action_quantiles. - make_lingbot_va_pre_post_processors now defaults a fresh (unconverted) build to a neutral [-1, 1] mapping (identity rescale); real per-benchmark stats come from the saved checkpoint (or postprocessor_overrides), analogous to dataset-stats normalization. - Update the config doc comment to point at the checkpoint as the source of truth. - Tests: replace the LIBERO-default assertion with a neutral-default check, and add a save_pretrained/from_pretrained round-trip guard for the quantile serialization. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
102 lines
4.1 KiB
Python
102 lines
4.1 KiB
Python
#!/usr/bin/env python
|
|
|
|
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
from __future__ import annotations
|
|
|
|
import torch
|
|
|
|
from lerobot.configs.types import FeatureType, PolicyFeature
|
|
from lerobot.policies.lingbot_va.configuration_lingbot_va import LingBotVAConfig
|
|
from lerobot.policies.lingbot_va.processor_lingbot_va import (
|
|
LingBotVAActionUnnormalizeStep,
|
|
make_lingbot_va_pre_post_processors,
|
|
)
|
|
from lerobot.processor import PolicyProcessorPipeline
|
|
from lerobot.utils.constants import (
|
|
OBS_IMAGES,
|
|
POLICY_POSTPROCESSOR_DEFAULT_NAME,
|
|
POLICY_PREPROCESSOR_DEFAULT_NAME,
|
|
)
|
|
|
|
|
|
def _make_config() -> LingBotVAConfig:
|
|
cfg = LingBotVAConfig(device="cpu")
|
|
cfg.input_features = {f"{OBS_IMAGES}.image": PolicyFeature(type=FeatureType.VISUAL, shape=(3, 128, 128))}
|
|
cfg.output_features = {}
|
|
cfg.validate_features()
|
|
return cfg
|
|
|
|
|
|
def test_action_unnormalize_inverts_quantile_norm() -> None:
|
|
q01 = [-1.0, -0.5, 0.0]
|
|
q99 = [1.0, 0.5, 2.0]
|
|
step = LingBotVAActionUnnormalizeStep(action_q01=q01, action_q99=q99)
|
|
|
|
# Forward (the policy-side) quantile normalization: (x - q01) / (q99 - q01 + eps) * 2 - 1.
|
|
q01_t = torch.tensor(q01)
|
|
q99_t = torch.tensor(q99)
|
|
raw = torch.tensor([[0.3, 0.1, 1.0]])
|
|
normed = (raw - q01_t) / (q99_t - q01_t + 1e-6) * 2.0 - 1.0
|
|
|
|
recovered = step.action(normed)
|
|
assert torch.allclose(recovered, raw, atol=1e-4)
|
|
|
|
|
|
def test_action_unnormalize_config_roundtrip() -> None:
|
|
step = LingBotVAActionUnnormalizeStep(action_q01=[0.0, 1.0], action_q99=[2.0, 3.0])
|
|
cfg = step.get_config()
|
|
assert cfg == {"action_q01": [0.0, 1.0], "action_q99": [2.0, 3.0]}
|
|
rebuilt = LingBotVAActionUnnormalizeStep(**cfg)
|
|
assert rebuilt.action_q01 == step.action_q01
|
|
assert rebuilt.action_q99 == step.action_q99
|
|
|
|
|
|
def test_make_pre_post_processors_names_and_steps() -> None:
|
|
cfg = _make_config()
|
|
pre, post = make_lingbot_va_pre_post_processors(cfg, dataset_stats=None)
|
|
assert pre.name == POLICY_PREPROCESSOR_DEFAULT_NAME
|
|
assert post.name == POLICY_POSTPROCESSOR_DEFAULT_NAME
|
|
# The postprocessor must contain the dedicated quantile unnormalize step.
|
|
assert any(isinstance(s, LingBotVAActionUnnormalizeStep) for s in post.steps)
|
|
|
|
|
|
def test_freshly_built_postprocessor_is_neutral() -> None:
|
|
# A fresh (unconverted) policy defaults to a neutral [-1, 1] mapping (identity rescale): the real
|
|
# per-benchmark quantiles are NOT hardcoded, they are restored from the saved checkpoint on load.
|
|
cfg = _make_config()
|
|
_, post = make_lingbot_va_pre_post_processors(cfg, dataset_stats=None)
|
|
normed = torch.tensor([[0.3, -0.5, 1.0, -1.0, 0.0, 0.7, -0.2]])
|
|
out = post(normed)
|
|
assert torch.allclose(out, normed, atol=1e-4)
|
|
|
|
|
|
def test_postprocessor_quantiles_survive_save_load(tmp_path) -> None:
|
|
# Regression guard for the Hub mechanism this policy relies on: the benchmark quantiles live in
|
|
# the serialized post-processor config and must round-trip through save_pretrained/from_pretrained.
|
|
q01 = [-0.6, -0.8, -0.9, -0.1, -0.15, -0.25, -1.0]
|
|
q99 = [0.9, 0.85, 0.9, 0.17, 0.18, 0.34, 1.0]
|
|
post = PolicyProcessorPipeline[torch.Tensor, torch.Tensor](
|
|
steps=[LingBotVAActionUnnormalizeStep(action_q01=q01, action_q99=q99)],
|
|
name=POLICY_POSTPROCESSOR_DEFAULT_NAME,
|
|
)
|
|
post.save_pretrained(tmp_path)
|
|
loaded = PolicyProcessorPipeline.from_pretrained(
|
|
tmp_path, config_filename=f"{POLICY_POSTPROCESSOR_DEFAULT_NAME}.json"
|
|
)
|
|
step = next(s for s in loaded.steps if isinstance(s, LingBotVAActionUnnormalizeStep))
|
|
assert step.action_q01 == q01
|
|
assert step.action_q99 == q99
|