LeRobot documentation

Процессоры среды

You are viewing main version, which requires installation from source. If you'd like regular pip install, checkout the latest stable version (v0.6.1).
Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Процессоры среды

Процессоры среды являются критически важным слоем в архитектуре обработки данных LeRobot, который выполняет специфичные для среды преобразования, отделённые от обработки, специфичной для политики. Такое разделение ответственности обеспечивает более чистый код, лучшую модульность и упрощает эксперименты с различными средами и политиками.

Зачем нужны обработчики окружения?

При работе с различными робототехническими средами (LIBERO, MetaWorld, Aloha и др.) каждая среда часто имеет уникальные форматы данных, системы координат и соглашения, которые необходимо стандартизировать до обработки политикой. Без процессоров среды эти преобразования были бы:

  1. Заданы жёстко в коде окружения — это затрудняет эксперименты с различными представлениями состояния
  2. Дублирование между политиками — каждой политике пришлось бы учитывать особенности конкретной среды
  3. Смешивание с логикой политики — нарушение разделения ответственности и усложнение отладки

Процессоры среды решают эту проблему, обеспечивая выделенный слой обработки между сырыми наблюдениями среды и входами для политики.

Конвейер обработки

Вот как данные проходят через полный конвейер обработки во время оценки:

# In lerobot_eval.py rollout() function:

# 1. Raw environment observation (numpy arrays, various formats)
raw_observation = env.step(action)

# 2. Convert numpy to torch, normalize images [0,1]
observation = preprocess_observation(raw_observation)

# 3. Add the task description (for multi-task environments)
observation["task"] = list(env.call("task_description"))

# 4. ENVIRONMENT-SPECIFIC preprocessing (NEW!)
#    - Flatten robot states
#    - Rotate images to match dataset conventions
#    - Handle environment-specific coordinate systems
observation = env_preprocessor(observation)

# 5. POLICY-SPECIFIC preprocessing
#    - Normalize with dataset statistics
#    - Add batch dimensions
#    - Move to GPU
#    - Tokenize language instructions
observation = preprocessor(observation)

# 6. Policy inference
action = policy.select_action(observation)

# 7. POLICY-SPECIFIC postprocessing
#    - Unnormalize actions
#    - Remove batch dimensions
action = postprocessor(action)

# 8. ENVIRONMENT-SPECIFIC postprocessing (NEW!)
#    - Convert action formats if needed
#    - Apply environment-specific constraints
action_transition = {"action": action}
action_transition = env_postprocessor(action_transition)
action = action_transition["action"]

# 9. Execute in environment
env.step(action)

Преимущества

1. Разделение ответственности

Процессоры среды обрабатывают преобразования, специфичные для формата данных среды, а процессоры политики — преобразования, специфичные для требований модели.

# ❌ Before: Mixed concerns
class LiberoVLAPolicy:
    def preprocess(self, obs):
        # Environment-specific: Flatten robot state (shouldn't be in policy!)
        state = self._flatten_robot_state(obs["robot_state"])
        # Policy-specific: Normalize with dataset stats
        state = self.normalizer(state)
        return state

# ✅ After: Clear separation
# Environment processor: Handles LIBERO's nested robot state
env_preprocessor = LiberoProcessorStep()  # Flattens robot_state

# Policy processor: Handles model requirements
policy_preprocessor = NormalizerProcessorStep(stats=dataset_stats)

2. Гибкость и переиспользование

Одна и та же политика может работать с различными обработчиками среды, а один и тот же обработчик среды может работать с различными политиками:

# Use SmolVLA policy with LIBERO environment
libero_preprocessor, libero_postprocessor = make_env_pre_post_processors(
    env_cfg=libero_cfg,
    policy_cfg=smolvla_cfg,
)
smolvla_preprocessor, smolvla_postprocessor = make_pre_post_processors(smolvla_cfg)

# Or use ACT policy with the same LIBERO environment
libero_preprocessor, libero_postprocessor = make_env_pre_post_processors(
    env_cfg=libero_cfg,
    policy_cfg=act_cfg,
)
act_preprocessor, act_postprocessor = make_pre_post_processors(act_cfg)

3. Упрощение экспериментов

Хотите попробовать другие представления состояния для LIBERO? Просто создайте новый процессор:

# Original: 8D state (pos + quat→axisangle + gripper)
@ProcessorStepRegistry.register("libero_processor")
class LiberoProcessorStep(ObservationProcessorStep):
    def _process_observation(self, obs):
        eef_pos = robot_state["eef"]["pos"]          # 3D
        eef_axisangle = quat2axisangle(quat)         # 3D
        gripper = robot_state["gripper"]["qpos"]     # 2D
        state = torch.cat([eef_pos, eef_axisangle, gripper], dim=-1)  # 8D
        return state

# Experiment: Add velocity for better control
@ProcessorStepRegistry.register("libero_velocity_processor")
class LiberoVelocityProcessorStep(ObservationProcessorStep):
    def _process_observation(self, obs):
        # Include velocities for 14D state
        eef_pos = robot_state["eef"]["pos"]          # 3D
        eef_axisangle = quat2axisangle(quat)         # 3D
        eef_vel = robot_state["eef"]["vel"]          # 3D  (NEW)
        gripper_pos = robot_state["gripper"]["qpos"] # 2D
        gripper_vel = robot_state["gripper"]["qvel"] # 3D  (NEW)
        state = torch.cat([eef_pos, eef_axisangle, eef_vel,
                          gripper_pos, gripper_vel], dim=-1)  # 14D
        return state

4. Более чистый код окружения

Среды предоставляют все доступные данные без необходимости знать, что именно будут использовать модели на нижних уровнях:

# LIBERO environment exposes full robot state
observation = {
    "pixels": {"image": img, "image2": img2},
    "robot_state": {
        "eef": {"pos": ..., "quat": ..., "vel": ..., "mat": ..., "axisangle": ...},
        "gripper": {"qpos": ..., "qvel": ...},
        "joints": {"pos": ..., "vel": ...}
    }
}

# Environment processor decides what to use
# Policy processor handles model-specific transformations

Использование обработчиков среды

Фабричная функция

Функция make_env_pre_post_processors следует тому же шаблону, что и make_pre_post_processors для политик:

from lerobot.envs import make_env_pre_post_processors, PushtEnv
from lerobot.envs.configs import LiberoEnv

# policy_cfg is your policy's config (e.g. cfg.policy in lerobot-eval); a few policies,
# such as XVLA, provide their own environment processors

# For LIBERO: Returns LiberoProcessorStep in preprocessor
libero_cfg = LiberoEnv(task="libero_spatial")
env_preprocessor, env_postprocessor = make_env_pre_post_processors(libero_cfg, policy_cfg=policy_cfg)

# For other environments: Returns identity processors (no-op)
pusht_cfg = PushtEnv()
env_preprocessor, env_postprocessor = make_env_pre_post_processors(pusht_cfg, policy_cfg=policy_cfg)

Реализация в envs/factory.py

def make_env_pre_post_processors(
    env_cfg: EnvConfig,
) -> tuple[
    PolicyProcessorPipeline[dict[str, Any], dict[str, Any]],
    PolicyProcessorPipeline[dict[str, Any], dict[str, Any]],
]:
    """
    Create preprocessor and postprocessor pipelines for environment observations.

    Args:
        env_cfg: The configuration of the environment.

    Returns:
        A tuple containing:
            - preprocessor: Pipeline that processes environment observations
            - postprocessor: Pipeline that processes environment outputs
    """
    # For LIBERO environments, add the LiberoProcessorStep to preprocessor
    if isinstance(env_cfg, LiberoEnv) or "libero" in env_cfg.type:
        preprocessor = PolicyProcessorPipeline(steps=[LiberoProcessorStep()])
    else:
        # For all other environments, return an identity preprocessor
        preprocessor = PolicyProcessorPipeline(steps=[])

    # Postprocessor is currently identity for all environments
    # Future: Could add environment-specific action transformations
    postprocessor = PolicyProcessorPipeline(steps=[])

    return preprocessor, postprocessor

Интеграция в Evaluation

В lerobot_eval.py обработчики среды создаются один раз и используются на протяжении всего выполнения:

def eval_main(cfg: EvalPipelineConfig):
    # Create environment
    envs = make_env(cfg.env, n_envs=cfg.eval.batch_size)

    # Create policy
    policy = make_policy(cfg=cfg.policy, env_cfg=cfg.env)

    # Create policy processors
    preprocessor, postprocessor = make_pre_post_processors(
        policy_cfg=cfg.policy,
        pretrained_path=cfg.policy.pretrained_path,
    )

    # Create environment processors (NEW!)
    env_preprocessor, env_postprocessor = make_env_pre_post_processors(env_cfg=cfg.env, policy_cfg=cfg.policy)

    # Run evaluation with both processor types
    eval_policy_all(
        envs=envs,
        policy=policy,
        env_preprocessor=env_preprocessor,      # Environment-specific
        env_postprocessor=env_postprocessor,    # Environment-specific
        preprocessor=preprocessor,              # Policy-specific
        postprocessor=postprocessor,            # Policy-specific
        n_episodes=cfg.eval.n_episodes,
    )

Пример: процессор среды LIBERO

Класс LiberoProcessorStep демонстрирует процессор для реальной среды:

from lerobot.processor import ObservationProcessorStep

@dataclass
@ProcessorStepRegistry.register(name="libero_processor")
class LiberoProcessorStep(ObservationProcessorStep):
    """
    Processes LIBERO observations into the LeRobot format.

    **State Processing:**
    - Extracts end-effector position (3D)
    - Converts quaternion to axis-angle representation (3D)
    - Extracts gripper joint positions (2D)
    - Concatenates into 8D state vector

    **Image Processing:**
    - Rotates images 180° to match HuggingFaceVLA/libero convention
    """

    def _process_observation(self, observation):
        processed_obs = observation.copy()

        # Process images: Flip 180° for camera convention
        for key in list(processed_obs.keys()):
            if key.startswith("observation.images."):
                img = processed_obs[key]
                img = torch.flip(img, dims=[2, 3])  # Flip H and W
                processed_obs[key] = img

        # Process robot_state: Flatten to 8D vector
        if "observation.robot_state" in processed_obs:
            robot_state = processed_obs.pop("observation.robot_state")

            eef_pos = robot_state["eef"]["pos"]           # (B, 3)
            eef_quat = robot_state["eef"]["quat"]         # (B, 4)
            gripper_qpos = robot_state["gripper"]["qpos"] # (B, 2)

            # Convert quaternion to axis-angle
            eef_axisangle = self._quat2axisangle(eef_quat)  # (B, 3)

            # Concatenate into single state vector
            state = torch.cat((eef_pos, eef_axisangle, gripper_qpos), dim=-1)
            state = state.float()

            processed_obs["observation.state"] = state

        return processed_obs

Зачем нужны эти преобразования?

  1. Поворот изображений: В наборе данных HuggingFaceVLA/libero изображения повёрнуты на 180° по сравнению с исходным симулятором LIBERO. Процессор учитывает это расхождение в соглашениях, чтобы политики, обученные на данном наборе данных, работали без дополнительных действий.

  2. Схлопывание состояния: исходная среда LIBERO предоставляет вложенные словари со всей доступной информацией о состоянии (позиция, кватернион, скорость, матричное представление и т. д.). Процессор:

    • Выбирает соответствующие компоненты (pos, quat, gripper)
    • Преобразует кватернион в представление «ось-угол» (более подходит для обучения)
    • Схлопывает в единый 8D-вектор, который ожидают политики
  3. Гибкость: среда по-прежнему предоставляет все исходные данные. Если вы хотите попробовать различные представления состояния (например, включить скорости, использовать матричное представление вместо оси-угла), вы можете создать новый процессор, не изменяя код среды.

Добавление обработчиков окружения для новых сред

Чтобы добавить обработчики окружения для нового окружения:

1. Создание шага Processor

# In src/lerobot/processor/env_processor.py

@dataclass
@ProcessorStepRegistry.register(name="myenv_processor")
class MyEnvProcessorStep(ObservationProcessorStep):
    """Process observations from MyEnv."""

    def _process_observation(self, observation):
        processed = observation.copy()

        # Your environment-specific transformations
        if "myenv.specific.state" in processed:
            state = processed.pop("myenv.specific.state")
            # Transform to standard format
            processed["observation.state"] = self._transform_state(state)

        return processed

2. Обновите подкласс EnvConfig

# In src/lerobot/envs/factory.py

def make_env_pre_post_processors(env_cfg: EnvConfig):
    if isinstance(env_cfg, LiberoEnv) or "libero" in env_cfg.type:
        preprocessor = PolicyProcessorPipeline(steps=[LiberoProcessorStep()])
    elif isinstance(env_cfg, MyEnvConfig) or "myenv" in env_cfg.type:
        preprocessor = PolicyProcessorPipeline(steps=[MyEnvProcessorStep()])
    else:
        preprocessor = PolicyProcessorPipeline(steps=[])

    postprocessor = PolicyProcessorPipeline(steps=[])
    return preprocessor, postprocessor

3. Использование в оценке

Изменения не требуются! Скрипт оценки автоматически использует подходящий процессор:

lerobot-eval \
    --policy.path=lerobot/my_policy \
    --env.type=myenv \  # Automatically uses MyEnvProcessorStep
    --eval.n_episodes=10

Будущее: Постпроцессоры среды

В настоящее время постпроцессоры являются идентичными (no-op) для всех сред. Будущие сценарии использования включают:

Преобразования пространства действий

@dataclass
class MyEnvActionPostprocessor(ProcessorStep):
    """Convert policy actions to environment-specific format."""

    def __call__(self, transition: EnvTransition) -> EnvTransition:
        action = transition["action"]

        # Example: Convert from Cartesian to joint space
        if self.action_space == "joint":
            action = self.ik_solver(action)

        # Example: Apply environment-specific safety limits
        action = torch.clamp(action, self.min_action, self.max_action)

        transition["action"] = action
        return transition

Преобразования систем координат

@dataclass
class CoordinateTransformPostprocessor(ProcessorStep):
    """Transform actions between coordinate systems."""

    def __call__(self, transition: EnvTransition) -> EnvTransition:
        action = transition["action"]

        # Example: Policy outputs in world frame, env expects base frame
        action = self.world_to_base_transform(action)

        transition["action"] = action
        return transition

Лучшие практики

  1. Держите обработчики окружения простыми: они должны решать только вопросы формата данных, специфичные для окружения, а не выполнять сложные преобразования, связанные с обучением.

  2. Используйте обработчики политик для требований к модели: нормализация, батчинг, размещение на устройстве и токенизация должны выполняться в обработчиках политик.

  3. Предоставлять все данные из окружений: пусть процессоры сами решают, что использовать, вместо того чтобы жёстко зашивать выбор в окружении.

  4. Соглашения в документации: Чётко документируйте любые соглашения о системах координат, ориентациях камер или форматах данных, которые обрабатывает ваш процессор.

  5. Тестирование независимо: процессоры окружения должны быть тестируемыми без загрузки полных политик или окружений.

Краткое описание

Процессоры среды обеспечивают чистое разделение между преобразованиями данных, специфичными для среды, и требованиями модели, специфичными для политики. Эта архитектура:

  • ✅ Облегчает эксперименты с различными представлениями состояния
  • ✅ Позволяет политикам работать бесшовно в разных средах
  • ✅ Обеспечивает, чтобы код окружения был сфокусирован на интерфейсе симуляции/оборудования
  • ✅ Делает конвейеры процессоров более поддерживаемыми и удобными для отладки
  • ✅ Соответствует принципу единственной ответственности

Ключевая идея: окружения определяют форматы данных, процессоры их стандартизируют, а политики потребляют стандартизированные данные. Каждый слой имеет чёткую и сфокусированную ответственность.

Update on GitHub