LeRobot documentation

实现你自己的 Robot Processor

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

实现你自己的 Robot Processor

在本教程中,您将学习如何实现自己的 Robot Processor。 首先,我们将探讨为何需要自定义处理器,然后以 NormalizerProcessorStep 为例,说明如何实现、配置和序列化处理器。有关 LeRobot 自带的处理器步骤,请参阅 常见处理器步骤。

为什么需要自定义处理器?

在大多数情况下,当从传感器读取原始数据或模型输出动作时,你需要处理这些数据,使其与目标系统兼容。例如,一个常见的需求是归一化数据范围,使其适合神经网络使用。

LeRobot 的 NormalizerProcessorStep 负责处理这项关键任务:

# Input: raw joint positions in [0, 180] degrees
raw_action = torch.tensor([90.0, 45.0, 135.0])

# After processing: normalized to [-1, 1] range for model training
normalizer = NormalizerProcessorStep(features=features, norm_map=norm_map, stats=dataset_stats)
normalized_result = normalizer(transition)
# ...

其他常见的处理需求包括:

  • 设备放置:在 CPU/GPU 之间移动张量并转换数据类型
  • 格式转换:在不同数据结构之间进行转换
  • 批处理:添加/移除批处理维度以兼容模型
  • 安全约束:对机器人指令施加限制
# Example pipeline combining multiple processors
pipeline = PolicyProcessorPipeline([
    RenameObservationsProcessorStep(rename_map={}),
    AddBatchDimensionProcessorStep(),
    NormalizerProcessorStep(features=features, norm_map=norm_map, stats=stats),
    DeviceProcessorStep(device="cuda"),
    # ...
])

LeRobot 提供了一套流水线机制,用于为输入数据和输出动作实现一系列处理步骤,从而可以轻松地将这些转换按正确的顺序组合起来,以获得最佳性能。

如何实现你自己的处理器?

我们将以 NormalizerProcessorStep 作为主要示例,因为它展示了处理器中常见的几种基本模式,包括状态管理、配置序列化和张量处理。

准备解决您的问题所需的处理步骤序列。处理步骤是一个实现了以下方法的类:

  • __call__:实现输入转换的处理步骤。
  • get_config:获取处理器步骤的配置。
  • state_dict:获取处理器步骤的状态。
  • load_state_dict:加载处理器步骤的状态。
  • reset:重置处理器步骤的状态。
  • transform_features:描述处理器步骤如何修改特征空间。

实现 __call__ 方法

__call__ 方法是处理器步骤的核心。它接收一个 EnvTransition 并返回一个修改后的 EnvTransition。NormalizerProcessorStep 的工作方式如下:

@dataclass
@ProcessorStepRegistry.register("normalizer_processor")
class NormalizerProcessorStep(ProcessorStep):
    """Normalize observations/actions using dataset statistics."""

    features: dict[str, PolicyFeature]
    norm_map: dict[FeatureType, NormalizationMode]
    stats: dict[str, dict[str, Any]] | None = None
    eps: float = 1e-8
    _tensor_stats: dict = field(default_factory=dict, init=False, repr=False)

    def __post_init__(self):
        """Convert stats to tensors for efficient computation."""
        self.stats = self.stats or {}
        self._tensor_stats = to_tensor(self.stats, device=self.device, dtype=torch.float32)

    def __call__(self, transition: EnvTransition) -> EnvTransition:
        new_transition = transition.copy()
        # Normalize observations
        # ...
        # Normalize action
        # ...
        return new_transition

完整的实现细节请参见 src/lerobot/processor/normalize_processor.py。

核心原则:

  • 始终使用 transition.copy() 以避免副作用
  • 一致地处理观测和动作
  • 将配置与状态分离:get_config() 返回可 JSON 序列化的参数,state_dict() 返回张量
  • 在 __post_init__() 中 将统计信息转换为张量 以实现高效计算

配置与状态管理

处理器支持通过三种方法将配置与张量状态分离进行序列化。NormalizerProcessorStep 完美地展示了这一点——它在其状态中携带数据集统计信息(张量),并在其配置中携带超参数:

# Continuing the NormalizerProcessorStep example...

def get_config(self) -> dict[str, Any]:
    """JSON-serializable configuration (no tensors)."""
    return {
        "eps": self.eps,
        "features": {k: {"type": v.type.value, "shape": v.shape} for k, v in self.features.items()},
        "norm_map": {ft.value: nm.value for ft, nm in self.norm_map.items()},
        # ...
    }

def state_dict(self) -> dict[str, torch.Tensor]:
    """Tensor state only (e.g., dataset statistics)."""
    flat: dict[str, torch.Tensor] = {}
    for key, sub in self._tensor_stats.items():
        for stat_name, tensor in sub.items():
            flat[f"{key}.{stat_name}"] = tensor.cpu()  # Always save to CPU
    return flat

def load_state_dict(self, state: dict[str, torch.Tensor]) -> None:
    """Restore tensor state at runtime."""
    self._tensor_stats.clear()
    for flat_key, tensor in state.items():
        key, stat_name = flat_key.rsplit(".", 1)
        # Load to processor's configured device
        self._tensor_stats.setdefault(key, {})[stat_name] = tensor.to(
            dtype=torch.float32, device=self.device
        )
        # ...

用法:

# Save (e.g., inside a policy)
config = normalizer.get_config()
tensors = normalizer.state_dict()

# Restore (e.g., loading a pretrained policy)
new_normalizer = NormalizerProcessorStep(**config)
new_normalizer.load_state_dict(tensors)
# Now new_normalizer has the same stats and configuration

转换特征

transform_features 方法定义了处理器如何转换特征名称和形状。这对于策略配置和调试至关重要。

对于 NormalizerProcessorStep,特征通常保持不变,因为归一化不会更改键或形状:

def transform_features(self, features: dict[PipelineFeatureType, dict[str, PolicyFeature]]) -> dict[PipelineFeatureType, dict[str, PolicyFeature]]:
    """Normalization preserves all feature definitions."""
    return features  # No changes to feature structure
    # ...

当您的处理器重命名或重塑数据时,请实现此方法以反映下游组件的映射关系。例如,一个简单的重命名处理器:

def transform_features(self, features: dict[str, PolicyFeature]) -> dict[str, PolicyFeature]:
    # Simple renaming
    if "pixels" in features:
        features["observation.image"] = features.pop("pixels")

    # Pattern-based renaming
    for key in list(features.keys()):
        if key.startswith("env_state."):
            suffix = key[len("env_state."):]
            features[f"observation.{suffix}"] = features.pop(key)
            # ...

    return features

核心原则:

  • 使用 features.pop(old_key) 来移除并获取旧的特征
  • 使用 features[new_key] = old_feature 添加重命名后的特征
  • 始终返回修改后的 features 字典
  • 在文档字符串中清晰地说明数据转换

使用覆盖

您可以在加载时使用 overrides 来覆盖步骤参数。这对于不可序列化的对象或特定于站点的设置非常有用。它既适用于策略工厂,也适用于 DataProcessorPipeline.from_pretrained(...)。

基础模型适配:当处理基础预训练策略时,这尤其有用,因为您通常无法获取原始训练统计数据。您可以注入自己的数据集统计信息,以将归一化器适配到您特定的机器人或环境数据。

示例:在机器人上进行策略评估时,覆盖设备并重命名映射。 使用此功能可在仅配备 CPU 的机器人上运行在 CUDA 上训练的策略,或在机器人使用的名称与数据集不同时重新映射相机键。

直接使用 from_pretrained:

from lerobot.datasets import LeRobotDataset
from lerobot.processor import PolicyProcessorPipeline

# Load a foundational policy trained on diverse robot data
# but adapt normalization to your specific robot/environment
new_stats = LeRobotDataset(repo_id="username/my-dataset").meta.stats
processor = PolicyProcessorPipeline.from_pretrained(
    "huggingface/foundational-robot-policy",  # Pretrained foundation model
    config_filename="policy_preprocessor.json",  # The policy's saved preprocessor
    overrides={
        "normalizer_processor": {"stats": new_stats},     # Inject your robot's statistics
        "device_processor": {"device": "cuda:0"},         # registry name for registered steps
        "rename_observations_processor": {"rename_map": robot_key_map}, # Map your robot's observation keys
        # ...
    },
)

最佳实践

基于对所有 LeRobot 处理器实现的分析,以下是关键的模式和实践:

1. 安全的数据处理

始终创建输入数据的副本,以避免产生意外的副作用。请使用 transition.copy() 和 observation.copy(),而不是就地修改数据。这可以防止您的处理器意外影响流水线中的其他组件。

在处理之前检查所需的数据,并妥善处理缺失的数据。如果你的处理器期望某些键(例如用于图像处理的 "pixels"),请先验证它们是否存在。对于可选数据,请使用 transition.get() 等安全访问模式,并适当处理 None 值。

当数据验证失败时,请提供清晰且可操作的错误信息,帮助用户了解问题所在以及如何修复。

2. 选择合适的基类

LeRobot 提供了专用的基类,以减少样板代码并确保一致性。当仅需修改观测值时,请使用 ObservationProcessorStep;仅处理动作时,请使用 ActionProcessorStep;而对于基于字典的机器人动作,请专门使用 RobotActionProcessorStep。

仅在需要完全控制整个过渡过程,或需要同时处理多个过渡组件时,才直接继承自 ProcessorStep。专用基类会为您处理过渡管理,并提供类型安全。

3. 注册和命名

使用 @ProcessorStepRegistry.register() 以具有描述性且带命名空间的名称注册您的处理器。使用诸如 "robotics_lab/safety_clipper" 或 "acme_corp/vision_enhancer" 的组织前缀来避免命名冲突。避免使用诸如 "processor" 或 "step" 等通用名称,因为它们可能会与其他实现发生冲突。

良好的注册机制可以让您的处理器被发现,并在保存和加载流水线时实现干净的序列化/反序列化。

4. 状态管理模式

区分配置参数(可 JSON 序列化的值)和内部状态(张量、缓冲区)。对于不应出现在构造函数或字符串表示中的内部状态,请使用带有 init=False, repr=False 的 dataclass 字段。

实现 reset() 方法以在回合之间清除内部状态。这对于随时间累积数据的有状态处理器(例如移动平均或时间滤波器)至关重要。

请记住,get_config() 应仅返回可 JSON 序列化的配置,而 state_dict() 则单独处理张量状态。

5. 输入验证与错误处理

在处理之前验证输入类型和形状。检查张量属性(如 dtype 和维度),以确保其与您的算法兼容。对于机器人动作,请验证所需的位姿分量或关节值是否存在,并且处于预期范围内。

对于无需处理的边缘情况,请使用提前返回。提供清晰且具有描述性的错误信息,其中应包含预期与实际的数据类型或形状。这将使用户调试变得更加容易。

6. 设备和数据类型感知

设计你的处理器,使其能够自动适配输入张量的设备和数据类型。内部张量(例如归一化统计量)应与输入张量的设备和数据类型保持一致,以确保与多 GPU 训练、混合精度以及分布式设置的兼容性。

实现一个 to() 方法,将处理器的内部状态移动到指定设备。在运行时检查设备/数据类型兼容性,并在需要时自动迁移内部状态。此模式可实现在不同硬件配置之间无缝运行,无需手动干预。

结论

现在,你已经拥有了在 LeRobot 中实现自定义处理器的所有工具!关键步骤如下:

  1. 将处理器定义为包含所需方法(__call__、get_config、state_dict、load_state_dict、reset、transform_features)的数据类
  2. 使用 @ProcessorStepRegistry.register("name") 注册它 以便发现
  3. 将其集成到 DataProcessorPipeline 中,与其他处理步骤一起使用
  4. 尽可能使用 基类,例如 ObservationProcessorStep,以减少样板代码
  5. 实现设备/数据类型感知以支持多 GPU 和混合精度配置

处理器系统采用模块化且可组合的设计,允许您从简单、专注的组件构建复杂的数据处理流水线。无论是为训练预处理传感器数据,还是为机器人执行后处理模型输出,自定义处理器都能为您提供所需的灵活性,以处理机器人应用中任何数据转换需求。

构建稳健处理器的关键原则:

  • 设备/数据类型适配:内部张量应与输入张量匹配
  • 清晰的错误信息:帮助用户理解出了什么问题
  • 基类用法:利用专门的基类来减少样板代码
  • 功能契约:使用 transform_features() 声明数据结构变更

从简单开始,充分测试,并确保你的处理器在不同的硬件配置上都能无缝运行!

Update on GitHub