一、概述

LeRobot Dataset v3.0 是 Hugging Face 推出的标准化机器人学习数据格式,提供统一的多模态时序数据访问接口,支持感官运动信号、多摄像头视频,以及丰富的元数据用于索引、搜索和可视化。

版本信息: v3.0(包含在 lerobot >= 0.4.0 中)

二、v3.0 核心特性

2.1 主要改进点

特性 v2.1 (旧版) v3.0 (新版)
存储方式 每集一个文件 多集合并到大文件中
元数据格式 JSON Lines Apache Parquet
文件组织 基于文件名的边界 基于元数据的关系查询
流式加载 不支持 Hub原生流式支持
文件系统压力 高(数千个小文件) 低(少量大文件)
初始化速度 快(3-5倍提升)

2.2 关键优势

  • 文件整合: 多个episode合并到同一个Parquet/MP4文件
  • 关系型元数据: 通过元数据而非文件名解析episode边界
  • Hub流式支持: 可直接从Hub消费数据,无需下载
  • 存储效率: 更少、更大的文件 → 更快的初始化
  • 内存映射: 通过PyArrow实现高效的内存使用

三、数据集结构

3.1 设计理念

v3.0 的核心原则是解耦存储与用户API

  • 存储层: 高效存储(少量大文件)
  • API层: 直观的episode级访问

3.2 三大支柱

1. 表格数据 (Tabular Data)
  • 格式: Apache Parquet
  • 内容: 低维、高频信号(状态、动作、时间戳)
  • 访问: 内存映射或通过 datasets 库流式访问
  • 存储路径: data/chunk-XXX/file-XXX.parquet
2. 视觉数据 (Visual Data)
  • 格式: MP4 视频
  • 内容: 摄像头帧,按episode分组
  • 组织: 每个摄像头一个分片
  • 存储路径: videos/{camera_key}/chunk-XXX/file-XXX.mp4
3. 元数据 (Metadata)
  • 格式: JSON + Parquet
  • 内容:
    • Schema定义(特征名、数据类型、形状)
    • 帧率信息
    • 归一化统计
    • Episode分割信息(在共享文件中的起止偏移)

3.3 目录结构

dataset_root/
├── meta/                                    # 元数据目录
│   ├── info.json                           # 数据集schema信息
│   ├── stats.json                          # 全局特征统计(归一化用)
│   ├── tasks.parquet                       # 任务描述映射
│   └── episodes/                           # Episode元数据(分块存储)
│       └── chunk-000/
│           └── file-000.parquet            # Episode记录(长度、任务、偏移)
│
├── data/                                    # 表格数据目录
│   └── chunk-000/
│       └── file-000.parquet                # 多个episode的帧数据
│
└── videos/                                  # 视频数据目录
    ├── front_camera/                       # 按摄像头分目录
    │   └── chunk-000/
    │       └── file-000.mp4                # 多个episode的视频帧
    └── wrist_camera/
        └── chunk-000/
            └── file-000.mp4

3.4 路径模板常量

# 文件路径模板
CHUNK_FILE_PATTERN = "chunk-{chunk_index:03d}/file-{file_index:03d}"

# 各类文件的默认路径
INFO_PATH = "meta/info.json"
STATS_PATH = "meta/stats.json"
DEFAULT_TASKS_PATH = "meta/tasks.parquet"
DEFAULT_EPISODES_PATH = "meta/episodes/" + CHUNK_FILE_PATTERN + ".parquet"
DEFAULT_DATA_PATH = "data/" + CHUNK_FILE_PATTERN + ".parquet"
DEFAULT_VIDEO_PATH = "videos/{video_key}/" + CHUNK_FILE_PATTERN + ".mp4"

# 文件大小限制
DEFAULT_CHUNK_SIZE = 1000                   # 每个chunk最多1000个文件
DEFAULT_DATA_FILE_SIZE_IN_MB = 100          # 数据文件最大100MB
DEFAULT_VIDEO_FILE_SIZE_IN_MB = 500         # 视频文件最大500MB

四、元数据文件详解

4.1 meta/info.json

存储数据集的Schema和配置信息:

{
  "codebase_version": "v3.0",
  "robot_type": "so100",
  "fps": 30,
  "features": {
    "timestamp": {
      "dtype": "float32",
      "shape": [1],
      "names": null
    },
    "episode_index": {
      "dtype": "int64", 
      "shape": [1],
      "names": null
    },
    "observation.state": {
      "dtype": "float32",
      "shape": [8],
      "names": ["joint_1", "joint_2", ..., "gripper"]
    },
    "observation.images.front": {
      "dtype": "image",
      "shape": [480, 640, 3],
      "names": ["height", "width", "channels"]
    },
    "action": {
      "dtype": "float32",
      "shape": [8],
      "names": ["joint_1", "joint_2", ..., "gripper"]
    }
  },
  "data_path": "data/chunk-{chunk_index:03d}/file-{file_index:03d}.parquet",
  "video_path": "videos/{video_key}/chunk-{chunk_index:03d}/file-{file_index:03d}.mp4"
}

默认特征(所有数据集都包含):

DEFAULT_FEATURES = {
    "timestamp": {"dtype": "float32", "shape": (1,)},      # 时间戳
    "frame_index": {"dtype": "int64", "shape": (1,)},      # 帧索引
    "episode_index": {"dtype": "int64", "shape": (1,)},    # Episode索引
    "index": {"dtype": "int64", "shape": (1,)},            # 全局索引
    "task_index": {"dtype": "int64", "shape": (1,)},       # 任务索引
}

4.2 meta/stats.json

存储用于归一化的全局统计信息:

{
  "observation.state": {
    "mean": [0.0, 0.0, 0.0, ...],
    "std": [1.0, 1.0, 1.0, ...],
    "min": [-3.14, -3.14, ...],
    "max": [3.14, 3.14, ...]
  },
  "action": {
    "mean": [0.0, 0.0, ...],
    "std": [1.0, 1.0, ...],
    "min": [-3.14, ...],
    "max": [3.14, ...]
  }
}

访问方式:

dataset = LeRobotDataset(repo_id)
stats = dataset.meta.stats  # 返回字典,值为numpy数组

4.3 meta/tasks.parquet

任务描述的映射表:

task_index task
0 “Grab the red cube”
1 “Place the cube in the box”
2 “Open the drawer”

4.4 meta/episodes/chunk-XXX/file-XXX.parquet

每个episode的元数据记录:

列名 类型 说明
episode_index int64 Episode索引
length int64 Episode帧数
task_index int64 关联的任务ID
data/chunk_index int64 数据文件chunk索引
data/file_index int64 数据文件file索引
data/from int64 在数据文件中的起始行
data/to int64 在数据文件中的结束行
videos/{camera}/chunk_index int64 视频chunk索引
videos/{camera}/file_index int64 视频file索引
videos/{camera}/from int64 视频起始帧
videos/{camera}/to int64 视频结束帧
dataset_from_index int64 在dataset中的起始索引
dataset_to_index int64 在dataset中的结束索引

示例:

# 访问第0个episode的信息
episode_info = dataset.meta.episodes.iloc[0]
print(episode_info['length'])              # 150帧
print(episode_info['data/chunk_index'])    # 0
print(episode_info['data/from'])           # 0
print(episode_info['data/to'])             # 150

五、数据加载与使用

5.1 基本加载

from lerobot.datasets.lerobot_dataset import LeRobotDataset

# 方式1: 加载整个数据集(会缓存到本地)
dataset = LeRobotDataset("lerobot/aloha_mobile_cabinet")

# 方式2: 只加载特定episodes
dataset = LeRobotDataset(
    "lerobot/aloha_mobile_cabinet",
    episodes=[0, 10, 11, 23]
)

# 方式3: 流式加载(不下载到本地)
from lerobot.datasets.streaming_dataset import StreamingLeRobotDataset
dataset = StreamingLeRobotDataset("lerobot/aloha_mobile_cabinet")

5.2 数据访问

# 随机访问单帧
sample = dataset[100]
# 返回字典:
# {
#   'observation.state': tensor([...]),        # 形状: (state_dim,)
#   'action': tensor([...]),                   # 形状: (action_dim,)
#   'observation.images.front': tensor([...]), # 形状: (C, H, W)
#   'timestamp': tensor(1.234),
#   ...
# }

# 访问episode
episode_idx = 0
from_idx = dataset.meta.episodes["dataset_from_index"][episode_idx]
to_idx = dataset.meta.episodes["dataset_to_index"][episode_idx]
episode_frames = [dataset[i] for i in range(from_idx, to_idx)]

5.3 时序窗口 (Delta Timestamps)

# 定义相对时间偏移
delta_timestamps = {
    # 加载4张图像: 当前帧、以及之前1秒、0.5秒、0.2秒的帧
    "observation.images.front": [-1.0, -0.5, -0.2, 0.0],
    
    # 加载6个状态向量
    "observation.state": [-1.5, -1.0, -0.5, -0.2, -0.1, 0.0],
    
    # 加载64个未来动作(用于action chunking)
    "action": [t / 30 for t in range(64)],  # 假设fps=30
}

dataset = LeRobotDataset(repo_id, delta_timestamps=delta_timestamps)

sample = dataset[100]
print(sample["observation.images.front"].shape)  # [4, C, H, W]
print(sample["observation.state"].shape)         # [6, state_dim]
print(sample["action"].shape)                    # [64, action_dim]

5.4 PyTorch DataLoader集成

import torch

dataloader = torch.utils.data.DataLoader(
    dataset,
    batch_size=32,
    num_workers=4,
    shuffle=True,
)

for batch in dataloader:
    observations = batch["observation.state"].to("cuda")    # [32, T, state_dim]
    actions = batch["action"].to("cuda")                    # [32, T, action_dim]
    images = batch["observation.images.front"].to("cuda")   # [32, T, C, H, W]
    # 前向传播
    # output = model(batch)

5.5 图像增强 (Image Transforms)

from lerobot.datasets.transforms import ImageTransforms, ImageTransformsConfig

# 配置数据增强
transforms_config = ImageTransformsConfig(
    enable=True,
    max_num_transforms=3,  # 每帧最多应用3种变换
    random_order=True,     # 随机顺序应用
    tfs={
        "brightness": {
            "weight": 1.0,
            "type": "ColorJitter",
            "kwargs": {"brightness": (0.7, 1.3)}
        },
        "contrast": {
            "weight": 2.0,  # 更高权重 = 更可能被选中
            "type": "ColorJitter",
            "kwargs": {"contrast": (0.8, 1.2)}
        },
        "sharpness": {
            "weight": 0.5,
            "type": "SharpnessJitter",
            "kwargs": {"sharpness": (0.3, 2.0)}
        },
    }
)

dataset = LeRobotDataset(
    repo_id,
    image_transforms=ImageTransforms(transforms_config)
)

六、数据集创建与记录

6.1 创建数据集

from lerobot.datasets.lerobot_dataset import LeRobotDataset

# 定义特征
features = {
    "observation.state": {"dtype": "float32", "shape": (8,)},
    "observation.images.front": {"dtype": "image"},
    "action": {"dtype": "float32", "shape": (8,)},
}

# 创建数据集
dataset = LeRobotDataset.create(
    repo_id="your-username/my-dataset",
    fps=30,
    robot_type="your_robot",
    features=features,
    use_videos=True,
)

# 记录episodes
for episode_idx in range(num_episodes):
    for frame_data in episode_frames:
        dataset.add_frame({
            "observation.state": state_array,
            "observation.images.front": image_array,  # 形状: (H, W, C)
            "action": action_array,
        })
    dataset.save_episode(task="Pick up the cube")

# ⚠️ 重要: 完成后必须调用finalize()
dataset.finalize()  # 刷新缓冲区、关闭parquet写入器

# 推送到Hub
dataset.push_to_hub()

6.2 命令行记录

lerobot-record \
  --robot.type=so101_follower \
  --robot.port=/dev/ttyUSB0 \
  --robot.cameras="{ front: {type: opencv, index_or_path: 0, width: 1920, height: 1080, fps: 30}}" \
  --teleop.type=so101_leader \
  --teleop.port=/dev/ttyUSB1 \
  --dataset.repo_id=${HF_USER}/my-dataset \
  --dataset.num_episodes=50 \
  --dataset.single_task="Grab the black cube"

七、数据格式转换

7.1 从 v2.1 迁移到 v3.0

# 安装包含v3支持的版本
pip install "lerobot>=0.4.0"

# 转换Hub上的v2.1数据集
python -m lerobot.datasets.v30.convert_dataset_v21_to_v30 \
    --repo-id=your-username/your-v21-dataset

转换操作:

  • 聚合parquet文件: episode-0000.parquet, … → chunk-000/file-000.parquet
  • 聚合视频文件: episode-0000.mp4, … → chunk-000/file-000.mp4
  • 更新元数据格式: JSON Lines → Parquet
  • 添加episode偏移信息

7.2 大规模数据集迁移 (SLURM)

对于大型数据集(如DROID的76k episodes),推荐使用集群并行处理:

# 1. 并行转换分片
python examples/port_datasets/slurm_port_shards.py \
    --raw-dir /data/droid/1.0.1 \
    --repo-id your-username/droid_v3 \
    --workers 2048 \
    --cpus-per-task 8 \
    --mem-per-cpu 1950M

# 2. 聚合分片
python examples/port_datasets/slurm_aggregate_shards.py \
    --repo-id your-username/droid_v3 \
    --workers 2048

# 3. 上传到Hub
python examples/port_datasets/slurm_upload.py \
    --repo-id your-username/droid_v3 \
    --workers 50

八、特征类型系统

8.1 支持的数据类型

类型 dtype 说明 示例
标量 float32, float64, int32, int64, bool 单个数值 温度、成功标志
向量 float32 + shape 一维数组 关节角度、笛卡尔位置
矩阵 float32 + shape 多维数组 力矩传感器矩阵
图像 image RGB图像 摄像头帧
字符串 string 文本 任务描述

8.2 特征命名约定

LeRobot使用层级命名,用 /. 分隔:

# 观测数据
"observation.state"                    # 机器人状态
"observation.images.front"             # 前置摄像头
"observation.images.wrist_left"        # 左腕摄像头
"observation.force_torque"             # 力矩传感器

# 动作数据
"action"                               # 标准动作
"action.gripper_position"              # 夹爪位置
"action.cartesian_position"            # 笛卡尔空间位置

# 元数据
"timestamp"                            # 时间戳
"episode_index"                        # Episode ID
"task_index"                           # 任务ID

8.3 图像特征特殊处理

# 定义图像特征
features = {
    "observation.images.front": {
        "dtype": "image",
        "shape": (480, 640, 3),  # (H, W, C) - 存储格式
        "names": ["height", "width", "channels"]
    }
}

# 存储: 图像编码为MP4视频
# - 路径: videos/observation.images.front/chunk-000/file-000.mp4
# - 编码: H.264/H.265
# - 多个episode的帧连接在一起

# 加载: 返回PyTorch张量
image = dataset[0]["observation.images.front"]
print(image.shape)  # [C, H, W] - PyTorch格式(通道优先)
print(image.dtype)  # torch.float32
print(image.min(), image.max())  # [0.0, 1.0] - 归一化到[0,1]

九、性能优化

9.1 内存映射

# PyArrow自动使用内存映射访问Parquet文件
dataset = LeRobotDataset(repo_id)

# 优势:
# - 不需要加载整个数据集到RAM
# - 操作系统自动管理内存
# - 支持大于RAM的数据集

9.2 批量加载优化

# 使用多进程DataLoader
dataloader = torch.utils.data.DataLoader(
    dataset,
    batch_size=32,
    num_workers=8,      # 多进程预加载
    prefetch_factor=2,  # 每个worker预取2个batch
    persistent_workers=True,  # 保持worker进程
)

9.3 流式加载

from lerobot.datasets.streaming_dataset import StreamingLeRobotDataset

# 直接从Hub流式加载,无需下载
dataset = StreamingLeRobotDataset(
    repo_id="yaak-ai/L2D-v3",
    # 自动从HF Hub流式读取Parquet和MP4
)

# 优势:
# - 零本地存储需求
# - 快速开始训练
# - 自动处理网络缓存

十、最佳实践

10.1 数据集创建

  1. 合理的FPS: 根据任务选择合适的采样率(通常10-30 Hz)
  2. 一致的特征: 确保所有episode使用相同的特征schema
  3. 任务标注: 为每个episode关联清晰的任务描述
  4. 及时finalize: 记录完成后立即调用 dataset.finalize()

10.2 训练优化

  1. 使用delta_timestamps: 加载动作轨迹和观测历史
  2. 适当的batch_size: 根据GPU内存调整
  3. 图像增强: 提高模型泛化能力,但不要过度
  4. 多进程加载: 使用DataLoader的num_workers

10.3 存储管理

  1. 定期清理缓存: LeRobot缓存在 ~/.cache/huggingface/lerobot/
  2. 使用流式加载: 对于非常大的数据集
  3. 合理的文件大小: 使用默认的100MB/500MB限制

十一、常见问题

Q1: 如何查看数据集元数据而不下载数据?

from lerobot.datasets.lerobot_dataset import LeRobotDatasetMetadata

meta = LeRobotDatasetMetadata(repo_id)
print(meta.total_episodes)
print(meta.total_frames)
print(meta.features)

Q2: 如何只加载部分episodes?

dataset = LeRobotDataset(repo_id, episodes=[0, 5, 10, 15])

Q3: ParquetWriter错误如何解决?

确保调用 finalize():

dataset.finalize()  # 关闭writer,写入footer
dataset.push_to_hub()

Q4: 如何处理不同分辨率的图像?

在加载时统一调整:

from torchvision.transforms import v2

transforms = v2.Compose([
    v2.Resize((480, 640)),
    # 其他变换...
])

dataset = LeRobotDataset(repo_id, image_transforms=transforms)

Q5: 如何计算数据集统计?

from lerobot.datasets.compute_stats import compute_stats

stats = compute_stats(dataset)
# stats包含每个特征的mean/std/min/max

十二、总结

关键要点

  1. 文件组织: v3.0使用文件基础而非episode基础的存储
  2. 元数据驱动: Episode边界通过元数据而非文件名解析
  3. 高效访问: 内存映射 + 流式加载 = 低内存占用
  4. 标准化格式: 统一的schema支持跨数据集训练
  5. Hub集成: 原生支持Hugging Face生态

与其他格式对比

特性 LeRobot v3.0 RLDS/TensorFlow RoboMimic HDF5
格式 Parquet + MP4 TFRecord HDF5
流式支持
Hub集成
跨平台 Python + TF Python
随机访问 快速 中等 快速
大规模数据 优秀 良好 一般

适用场景

  • 大规模机器人数据集 (如DROID的76k episodes)
  • 多模态时序数据 (图像 + 状态 + 动作)
  • 分布式训练 (Hub流式加载)
  • 数据共享 (Hugging Face Hub生态)
  • 快速原型开发 (标准化API)

参考资源:

  • 官方文档: https://huggingface.co/docs/lerobot
  • GitHub仓库: https://github.com/huggingface/lerobot
  • 数据集浏览: https://huggingface.co/datasets?other=LeRobot
  • 示例代码: examples/dataset/ 目录
Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐