具身智能领域数据格式扫盲
具身智能机器人数据格式 · 扫盲总结
一份给"入门者速查"的笔记。
覆盖 7 种主流格式:rosbag / MCAP / TFRecord / HDF5 / Zarr / Parquet / RLDS / LeRobot主线:机器人数据要经历三段旅程——先被录下来,再存到硬盘,最后加上语义变成训练数据。七种格式,就分布在这三段上。
〇、最重要的一张图:三层模型
初学者最大的困惑是"这些格式怎么感觉是一类又不是一类"。真相是它们不在同一层:
┌──────────────────────────────────────────────────────────────┐
│ ① 采集层 "记录时间上发生了什么" │
│ —— 像行车记录仪,按时间录下机器人的一切 │
│ rosbag、MCAP │
└──────────────────────────────────────────────────────────────┘
│ 转换(对齐时间、切分回合、定义动作)
▼
┌──────────────────────────────────────────────────────────────┐
│ ② 存储层 "字节在硬盘上怎么摆" │
│ —— 像仓库货架,只管高效存取,不懂内容是什么 │
│ TFRecord、HDF5、Zarr、Parquet │
└──────────────────────────────────────────────────────────────┘
│ 加语义(标注哪是观测、哪是动作、回合边界)
▼
┌──────────────────────────────────────────────────────────────┐
│ ③ 语义层 "这些数据是什么意思,怎么喂给模型" │
│ —— 像贴好标签的成品,拿来就能训练 │
│ RLDS、LeRobot │
└──────────────────────────────────────────────────────────────┘
一句话类比:
采集层是"摄像机拍的原始录像",存储层是"录像存进什么样的硬盘格式",语义层是"剪辑好、打上字幕、能直接用的成片"。
关键认知:语义层 = 存储层 + 一层约定。
- RLDS = TFRecord(存储)+ RL 风格的字段约定
- LeRobot = Parquet + MP4(存储)+ 目录规范
一、采集层:机器人的"行车记录仪"
机器人运行时,各个传感器(相机、关节编码器、力传感器…)不停产生数据。采集层就是把这些数据流按时间原样录下来。
特点:它只知道"某时刻、某个数据通道、来了一条消息",不懂什么是"一次抓取"、什么是"动作"。是原材料,不是成品。
rosbag(ROS 1 的录像格式)
| 维度 | 说明 |
|---|---|
| 是什么 | ROS 1 生态的标准录制格式,扩展名 .bag |
| 怎么工作 | 把各个话题(topic)的消息按时间交错存进一个文件,攒一批压缩一次 |
| 优点 | ROS 原生、工具链成熟、能快进回放 |
| 致命缺点 | 索引在文件末尾 → 录制中途崩溃/断电,索引没写成,整个文件基本报废 |
| 其他缺点 | 解析强依赖 ROS 环境;单文件太大后变慢;消息定义一改,旧文件可能读不了 |
科普比喻:像老式录像带——能录能放,但你得用配套的机器(ROS)才能读,而且录到一半停电,带子可能就废了。
MCAP(ROS 2 的现代录像格式)
| 维度 | 说明 |
|---|---|
| 是什么 | ROS 2 推荐的录制格式,扩展名 .mcap,是 rosbag 的现代替代 |
| 核心改进 | ①文件头尾都有标记,崩溃了也能抢救出已录数据 ②把"消息说明书"完整存进文件,脱离 ROS 也能读 ③支持消息格式升级 |
| 通用性 | 不绑定 ROS,能存任意数据(ROS 消息、protobuf、JSON 都行),是通用"时序黑匣子" |
rosbag → MCAP 的进化,一句话:
把 rosbag 里"隐式依赖 ROS 环境"的东西,全部显式写进文件本身,从"ROS 专用录像机"升级成"自带说明书、摔不坏的通用黑匣子"。
科普比喻:从录像带升级到"带元数据、能容错的数字视频文件"。
二、存储层:只管"怎么存字节"的通用仓库
采集完的原始数据,经过处理(时间对齐、切分回合、定义动作)后,需要存到硬盘反复用于训练。存储层就是"存字节的技术方案"。
核心:这一层的格式都不是机器人专用的——它们是通用工具,存图像、存文本、存表格都行,压根不知道"机器人"这回事。它们只关心一件事:字节怎么摆才能存得省、读得快。
四个成员,按"数据形状"分成两类:
存"多维数组"的(适合图像、张量)
图像天生是多维数组(高×宽×通道),关节序列也是(帧×关节)。这类数据用下面两个:
HDF5
| 维度 | 说明 |
|---|---|
| 是什么 | 经典科学计算格式(20+ 年历史),扩展名 .h5 |
| 本质 | 把一个文件系统塞进单个文件——文件内部有"文件夹"(Group)和"文件"(Dataset),像个微型硬盘 |
| 优点 | 自带索引(随机读快)、自描述(形状类型都在文件里)、单文件好搬运、原生支持多维数组 |
| 缺点 | 不能多进程同时写、云存储(S3)上很慢、文件头坏了可能整个文件打不开 |
| 谁在用 | RoboMimic、ALOHA/ACT 等大量学术数据集 |
科普比喻:一个"自带目录索引的超级文件夹",但整个装在一个盒子里——本地用很爽,搬到云上就笨重。
Zarr
| 维度 | 说明 |
|---|---|
| 是什么 | HDF5 的"云原生版本",扩展名是个 .zarr/ 文件夹 |
| 本质 | 和 HDF5 一样是"数组容器",但把数据切成小块(chunk),每块存成一个独立文件 |
| 优点 | 多进程能并行读写(各写各的块)、云存储友好(每块是独立对象,按需下载)、坏一块只丢一块 |
| 缺点 | 小文件爆炸(一个大数据集可能几百万个小文件,拷贝/备份痛苦)、生态比 HDF5 年轻 |
HDF5 vs Zarr 一句话:
同样是存多维数组。HDF5 = 装进一个文件(本地单机爽),Zarr = 摊开成一个文件夹、每块一个文件(云端、多进程爽)。
科普比喻:HDF5 是"一整本书",Zarr 是"活页夹,每页单独抽取"——多人同时看、放云端,活页夹更方便。
存"表格"的(适合状态、动作、数值)
关节角度、动作指令、时间戳、奖励——这些是"一行一行的结构化数值",更像 Excel 表。用下面这个:
Parquet
| 维度 | 说明 |
|---|---|
| 是什么 | 大数据领域的表格标准格式,扩展名 .parquet |
| 灵魂特性 | 列式存储——同一列的数据存在一起(而不是同一行存在一起) |
| 为什么强 | ①只读你要的列(训练只要 action?其他列碰都不碰,省 I/O)②压缩率极高(同列同类型,好压)③能被 pandas/SQL 直接分析 |
| 关键约定 | 图像不放 Parquet(会破坏列式优势),图像另存视频,用 frame_index 列关联 |
| 谁在用 | LeRobot 的数值部分就是 Parquet |
"列式存储"是唯一需要理解的概念,科普可以这样讲:
传统表格按"行"存(一个人的所有信息挨在一起)。Parquet 按"列"存(所有人的年龄挨在一起、所有人的身高挨在一起)。好处:你只想统计"平均年龄",只需读"年龄"这一列,不用把每个人的全部信息都翻出来。
TFRecord(存"线性记录流",比较特殊)
| 维度 | 说明 |
|---|---|
| 是什么 | Google 为 TensorFlow 造的通用序列化格式,扩展名 .tfrecord |
| 本质 | 一串首尾相接的记录,每条 = 一段带校验的字节,简单粗暴 |
| 特点 | 适合顺序流式读、天然可分片并行;但无内建索引(随机读慢)、只支持 3 种数据类型 |
| 地位 | 它是 RLDS 的地基——RLDS 就是在 TFRecord 上加语义盖起来的 |
科普比喻:像一长串首尾相连的"数据信封",一个接一个拆——适合从头到尾顺着读,不适合"直接翻到第 500 个"。
存储层四格式速查表
| 格式 | 数据模型 | 最擅长 | 落盘方式 | 机器人里存什么 |
|---|---|---|---|---|
| TFRecord | 线性记录流 | 顺序读、分片 | 单/多文件 | 整条样本打包 |
| HDF5 | 单文件树+数组 | 本地、自包含 | 单文件 | 图像+状态一锅端 |
| Zarr | 文件夹+数组 | 云端、并行 | 多文件夹 | 大规模图像/张量 |
| Parquet | 列式表格 | 选择性读列、分析 | 单文件 | 数值(状态/动作) |
三、语义层:贴好标签、能直接训练的"成品"
存储层只是把字节存下来,但它不知道"哪个字段是观测、哪个是动作、一个回合从哪到哪、这个任务是什么"。语义层就是在存储层之上,加一套约定,把裸数据变成有意义、能直接喂给模型的训练数据集。
两条路线,哲学相反:
RLDS(Google 路线)
| 维度 | 说明 |
|---|---|
| 是什么 | Reinforcement Learning Datasets,Google 的规范 + 加载库 |
| 建在谁上 | TFRecord |
| 数据模型 | 嵌套:数据集 → 一串回合(episode) → 每回合一串步(step) |
| 回合边界 | 靠每帧的 is_first/is_last 标记位来"逻辑地"切分 |
| 图像存法 | 每帧 JPEG 塞进记录里(无跨帧压缩) |
| 生态 | TensorFlow / JAX,适合超大规模、TPU 训练 |
| 血统 | 强化学习(自带折扣因子等 RL 字段) |
| 代表 | OXE(Open X-Embodiment)、RT-1、RT-2 |
LeRobot(HuggingFace 路线)
| 维度 | 说明 |
|---|---|
| 是什么 | HuggingFace 主推的机器人数据集规范 |
| 建在谁上 | Parquet(数值)+ MP4(图像)+ JSONL(元数据) |
| 数据模型 | 扁平:一行一帧,一个回合一个 Parquet 文件 |
| 回合边界 | 靠文件切分(换文件=换回合),直观 |
| 图像存法 | 编码成 MP4 视频(利用帧间相似性,更省空间) |
| 贴心设计 | 内置归一化统计量(stats)、fps 等,拿来即训 |
| 可读性 | JSON/Parquet 能直接打开看、视频能直接播放,调试友好 |
| 生态 | PyTorch / HuggingFace,社区上手快 |
LeRobot 的目录结构(科普时可展示)
franka_pick_dataset/
├── meta/ ← 说明书:有哪些字段、每回合多长、任务是什么、归一化统计量
├── data/ ← 数值(Parquet):状态、动作、奖励、时间戳
└── videos/ ← 图像(MP4):相机画面
三个分区一目了然:说明书 + 数值 + 画面,用 frame_index 把数值和画面对齐。
RLDS vs LeRobot 终极对照
| RLDS | LeRobot | |
|---|---|---|
| 存储地基 | TFRecord | Parquet + MP4 + JSONL |
| 数据模型 | 嵌套(回合套步) | 扁平(一行一帧) |
| 回合边界 | is_first/is_last 标记 |
文件切分 |
| 图像 | JPEG 塞记录(无跨帧压缩) | MP4 视频(跨帧压缩,更省) |
| 可读性 | ❌ 二进制,要写代码 | ✅ 直接看,可视化方便 |
| 归一化 | ❌ 自己算 | ✅ 内置 |
| 生态 | TensorFlow/JAX | PyTorch/HuggingFace |
| 血统 | 强化学习 | 模仿学习/监督学习 |
| 代表 | OXE、RT 系列 | HuggingFace 生态、众多开源 VLA |
一句话总结两条路线:
RLDS 走"Google/TensorFlow/大规模"路线(OXE 用它);LeRobot 走"HuggingFace/PyTorch/可读易上手"路线(社区新宠)。如今常把 OXE 数据转成 LeRobot 用 PyTorch 训——两者实践中会互转。
四、贯穿始终的两个"共同智慧"(科普加分点)
讲完七个格式,有两个设计思想反复出现,点出来能让科普更有深度:
1. “自描述”——把说明书装进数据本身
好格式都在做同一件事:不依赖外部文档,把"这数据长什么样"写进文件自己。
- MCAP 内嵌消息 schema、HDF5 内嵌形状类型、Zarr 有
zarr.json、Parquet 有 Footer、RLDS 有dataset_info、LeRobot 有info.json
科普讲法:好的数据格式像"自带说明书的家具"——不用另找图纸,拆开箱子说明书就在里面。
2. “chunk”(分块)——但两层含义不同,别混
"chunk(块)"这个词在两层都出现,但意思不一样,这是初学者最容易混的点:
采集层(rosbag/MCAP) 的 chunk = 一批消息打包 (沿"时间"切)
存储层(HDF5/Zarr) 的 chunk = 大数组切成瓷砖 (沿"数据维度"切)
记忆口诀:采集层的块沿时间切消息,存储层的块沿维度切数组。
五、实用速查:我该用哪个?
按场景选
| 你的场景 | 推荐 |
|---|---|
| ROS 机器人实时采集 | MCAP(ROS 2)/ rosbag(ROS 1) |
| PyTorch 训练、想快速上手、要常看数据 | LeRobot |
| TensorFlow/JAX、超大规模、混合多数据集 | RLDS |
| 本地单机、数据不太大、要方便拷贝 | HDF5 |
| 云端(S3/GCS)、多进程、超大规模 | Zarr |
| 只存数值做分析/统计 | Parquet |
后训练实习生的"够用就好"版认知
做后训练,你 90% 的时间面对的是 LeRobot 或 RLDS(因为你拿到的是别人处理好的训练数据)。
- 看到 RLDS → 想到"OXE 那套、TensorFlow、
for episode: for step读”- 看到 LeRobot → 想到"HuggingFace 那套、PyTorch、
meta/ + data/ + videos/目录、dataset[i]取帧"- 底层的 TFRecord/Parquet/HDF5/Zarr 知道是什么、谁是谁的地基即可,一般不用手动碰。
- rosbag/MCAP 是采集端的事,除非你要从原始数据做转换,否则接触不多。
六、七格式一句话总表(终极速查)
| 层 | 格式 | 一句话 |
|---|---|---|
| ① 采集 | rosbag | ROS 1 录像带,能录能放,但摔了(崩溃)容易废 |
| ① 采集 | MCAP | ROS 2 数字黑匣子,自带说明书、摔不坏、还通用 |
| ② 存储 | TFRecord | 一串数据信封,顺着拆,RLDS 的地基 |
| ② 存储 | HDF5 | 装进一个盒子的"带索引超级文件夹",本地爽 |
| ② 存储 | Zarr | 摊成活页夹的 HDF5,云端、多进程爽 |
| ② 存储 | Parquet | 按列存的 Excel,只读你要的列,LeRobot 的地基 |
| ③ 语义 | RLDS | TensorFlow 路线,嵌套结构,OXE 用它 |
| ③ 语义 | LeRobot | PyTorch 路线,目录清晰、可读易上手,社区新宠 |
附:科普讲解建议的叙事线
如果做一期科普视频/文章,推荐这条线索(由浅入深、有故事感):
- 抛问题:机器人是怎么"学会"干活的?靠喂给它海量"看到什么→做什么"的数据。那这些数据长什么样、存哪儿?
- 讲旅程:一条数据要走三步——录下来(采集)→ 存起来(存储)→ 贴标签变教材(语义)。(用三层图)
- 每层挑代表讲:采集讲 rosbag→MCAP 的"录像带进化数字黑匣子";存储讲"数组仓库(HDF5/Zarr) vs 表格仓库(Parquet)";语义讲 RLDS vs LeRobot 两条路线之争。
- 升华:好格式的两个共同智慧——"自带说明书(自描述)“和"化整为零(分块)”。
- 落地:如果观众也想入门具身智能,看到 LeRobot / OXE(RLDS) 这两个词,心里就有底了。
核心心法:别陷进字节细节。科普的价值是"建立地图和直觉",不是"背技术手册"。每个格式记住"它像什么、解决什么问题、谁在用"就足够打动大众了。
七、实战篇:同一份 Franka 数据,七种格式长什么样
前面讲的是"概念地图"。这一节把地图落到具体数据——用同一个例子,看每种格式的真实排布和关键参数含义。适合初学者对着"摸清每个字段到底是什么"。
统一实验设定
任务:Franka Panda 机械臂从桌面抓起一个红色方块。
录制:相机 30 FPS,我们截取 3 帧(一次抓取的开始/中间/抓住)。
这 3 帧的原始数据(后面所有格式都基于它,请记住这张表):
帧 时间(s) 7个关节角度(rad) 夹爪宽度(m) 8维动作 奖励
──────────────────────────────────────────────────────────────────────────────────────────────────────
帧0 0.000 [0.10,-0.30, 0.00,-1.20, 0.00,1.50,0.80] 0.08 [ 0.01,-0.02,-0.01,0,0,0, 0.0,0] 0.0
帧1 0.033 [0.15,-0.35,-0.05,-1.10, 0.02,1.45,0.75] 0.08 [ 0.00, 0.00,-0.02,0,0,0,-0.03,0] 0.0
帧2 0.066 [0.20,-0.40,-0.10,-0.90, 0.05,1.40,0.70] 0.02 [ 0.00, 0.05, 0.10,0,0,0, 0.0,0] 1.0
↑抓住了 ↑成功给奖励
每个字段/参数的物理含义(这是初学者最该搞懂的部分):
| 字段 | 形状 | 含义 | 关键点 |
|---|---|---|---|
| 图像 image | (480,640,3) | 相机 RGB 画面,高480×宽640×3通道 | 每个像素 0~255 的 uint8;这是模型的"眼睛" |
| 关节角度 joint_positions | (7,) | Franka 有 7 个可转动关节,每个的当前角度(弧度) | Franka 是 7 自由度机械臂,所以是 7 个数 |
| 夹爪宽度 gripper_width | (1,) | 两指张开的距离(米) | 0.08=全张开,0.02=夹住了方块 |
| 动作 action | (8,) | 告诉机器人"下一步怎么动" | 见下方拆解 ↓ |
| 奖励 reward | (1,) | 这一步做得好不好(RL 用) | 抓住瞬间给 1.0,其余 0.0 |
| 时间戳 timestamp | (1,) | 这一帧的时刻(秒) | 用于对齐图像和数值 |
8 维动作 action 拆解(初学者常问"这 8 个数是啥"):
action = [ dx, dy, dz, droll, dpitch, dyaw, gripper, pad ]
└───┬────┘ └────────┬─────────┘ └──┬──┘ └┬┘
末端位置增量 末端姿态增量 夹爪指令 补位
(往哪移动,米) (怎么转动,弧度) (开/合) (凑够8维)
例: 帧2的 [0, 0.05, 0.10, 0,0,0, 0.0, 0]
= 末端往 y 方向移 0.05m、往 z 方向(上)提 0.10m(把方块提起来)、姿态不变
概念补充:为什么动作是"增量(delta)“而不是"绝对位置”?
大多数具身策略输出的是"相对当前位置移动多少"(如 dz=0.10 表示"往上抬 10cm"),而不是"移动到坐标 (0.3, 0.2, 0.5)“。因为相对量更好学、更泛化。这类动作空间叫 末端笛卡尔增量控制 (delta end-effector control),是 OXE、LeRobot 里最常见的一种。你也会遇到"绝对关节角度”"绝对末端位姿"等其他动作空间——看数据第一件事就是搞清楚 action 的每一维是什么。
逐格式看排布
① rosbag / MCAP(采集层)——按时间交错的消息流
采集层不认识"帧"“动作”,它只有"某时刻、某话题、一条消息"。同样这 3 帧,在 MCAP 里是这样按时间铺开的:
pick.mcap (话题: /camera 图像, /joint_states 关节)
时间轴 →
t=0.000 /camera → 一帧图像(480×640×3)
t=0.000 /joint_states → position=[0.10,-0.30,0.00,-1.20,0.00,1.50,0.80]
t=0.033 /camera → 一帧图像
t=0.033 /joint_states → position=[0.15,-0.35,-0.05,-1.10,0.02,1.45,0.75]
t=0.066 /camera → 一帧图像
t=0.066 /joint_states → position=[0.20,-0.40,-0.10,-0.90,0.05,1.40,0.70]
关键观察 / 参数含义:
| 概念 | 含义 | 初学者注意 |
|---|---|---|
| topic(话题) | 一个数据通道,如 /camera、/joint_states |
不同传感器 = 不同 topic |
| 消息按时间交错 | 图像和关节不是分开存,是按时间戳穿插 | 和"整齐的表格"完全不同 |
| 这里没有 action! | 采集时只有"状态",没有"动作" | 动作是后期算出来的:如"下一帧关节 − 当前帧关节",或记录遥操作指令 |
| 时间戳可能不齐 | 相机 30Hz、关节可能 1000Hz,频率不同 | 转成训练数据时要做时间对齐,给每帧图像配上最近的关节 |
这就是"采集层→存储层"要做的转换:把交错的消息流,对齐时间、切好回合、算出动作,整理成下面那种整齐的结构。
② HDF5(存储层)——装进一个文件的树
3 帧数据在 HDF5 里,按"树形"组织(一个 episode 一个 Group):
franka_pick.h5
│ 【根属性 Attribute】
│ @robot_type = "franka_emika_panda"
│ @fps = 30
│
└─ episode_0/ ← Group(这次抓取)
│ @task = "pick up the red block"
│ @success = true
│
├─ observations 形状(3, 480, 640, 3) 类型uint8 ← 3帧图像
├─ joint_positions 形状(3, 7) 类型float32 ← 3帧×7关节
├─ gripper_width 形状(3,) 类型float32 ← [0.08, 0.08, 0.02]
└─ actions 形状(3, 8) 类型float32 ← 3帧×8维动作
关键参数含义:
| 参数 | 值 | 含义 |
|---|---|---|
| 形状 shape | 如 (3, 7) | 第一维 3=帧数,第二维 7=关节数。读懂 shape 就读懂了数据维度 |
| 类型 dtype | uint8 / float32 | 图像用 uint8(0~255省空间),数值用 float32 |
| Group | episode_0 |
相当于文件夹,一个回合一个 |
| Attribute(@) | @fps=30 |
挂在树节点上的小标签,存元信息 |
| chunk(分块) | 常设 (1,480,640,3) | 一帧一块,随机读某帧时只解压那一块 |
读法:file["episode_0/actions"][2] → 直接拿到帧2的动作 [0,0.05,0.10,...]。
③ Zarr(存储层)——摊成文件夹,每块一个文件
同样的数据,Zarr 把每个数组切块存成独立文件:
franka_pick.zarr/
├── zarr.json ← 根说明书(JSON纯文本,可直接打开)
│ {"attributes": {"robot":"franka", "fps":30}}
│
├── observations/
│ ├── zarr.json ← 这个数组的说明书 ↓
│ │ {"shape":[3,480,640,3], "data_type":"uint8",
│ │ "chunk_grid":{"chunk_shape":[1,480,640,3]}, ← 一帧一块
│ │ "codecs":[{"name":"blosc"}]} ← 压缩方式
│ └── c/
│ ├── 0/0/0/0 ← 帧0的图像(独立文件)
│ ├── 1/0/0/0 ← 帧1
│ └── 2/0/0/0 ← 帧2
│
├── joint_positions/ (shape[3,7]) c/0/0, c/1/0, c/2/0
├── actions/ (shape[3,8]) c/0/0, c/1/0, c/2/0
└── gripper_width/ (shape[3]) c/0
关键参数含义:
| 参数 | 含义 | 和 HDF5 的区别 |
|---|---|---|
| zarr.json | 每个数组的说明书,JSON 明文 | HDF5 藏在二进制里,Zarr 直接可读 |
| shape / data_type | 同 HDF5:形状 + 类型 | 一样 |
| chunk_shape [1,480,640,3] | 每块的大小=一帧 | 决定切成几个文件 |
| codecs: blosc | 压缩算法(blosc 很快) | — |
文件名 c/2/0/0/0 |
chunk 的坐标:第2帧、其余维第0块 | 每块是独立文件,这是 Zarr 的灵魂 |
④ Parquet(存储层)——列式表格(注意:不存图像)
数值部分(状态/动作/奖励)用 Parquet,是一张表。图像不在这(另存视频):
franka_pick.parquet → 逻辑上就是一张表:
timestamp frame_index observation.state action next.reward
0.000 0 [0.10,-0.30,...,0.80] [0.01,-0.02,...,0.0] 0.0
0.033 1 [0.15,-0.35,...,0.75] [0.00, 0.00,...,-0.03] 0.0
0.066 2 [0.20,-0.40,...,0.70] [0.00, 0.05,...,0.0] 1.0
物理上按"列"存(列式):
timestamp列: [0.000, 0.033, 0.066] ← 挨在一起
action列: [[...],[...],[...]] ← 挨在一起
reward列: [0.0, 0.0, 1.0] ← 挨在一起
关键参数含义:
| 参数 | 含义 | 为什么重要 |
|---|---|---|
| 列式存储 | 同列数据物理相邻 | 训练只要 action 列时,其他列不用读 |
| observation.state | 就是 7 个关节角度 | LeRobot 里习惯叫 observation.state |
| frame_index | 帧号 0,1,2 | 关键:用它去视频里找对应帧 |
| 无图像列 | 图像另存 MP4 | 图像放表里会毁掉列式压缩优势 |
⑤ LeRobot(语义层)——Parquet + MP4 + JSONL 拼成完整数据集
这是初学者实际最常拿到的格式。完整目录:
franka_pick_dataset/
│
├── meta/ ← 说明书区
│ ├── info.json ← schema:有哪些字段、形状、fps
│ ├── episodes.jsonl ← 回合索引:每回合多长、什么任务
│ ├── tasks.jsonl ← 任务表:语言描述
│ └── stats.json ← 归一化统计量:mean/std
│
├── data/chunk-000/
│ └── episode_000000.parquet ← 数值(上面④那张表)
│
└── videos/chunk-000/observation.images.cam_high/
└── episode_000000.mp4 ← 3帧图像编码成的视频
四个 meta 文件的实际内容 + 参数含义:
info.json(schema 说明书):
{
"robot_type": "franka_emika_panda",
"fps": 30,
"features": {
"observation.state": {"dtype":"float32", "shape":[7],
"names":["joint_1",...,"joint_7"]}, ← 7关节,每维有名字
"action": {"dtype":"float32", "shape":[8]}, ← 8维动作
"observation.images.cam_high": {"dtype":"video", ← 注意是"video"!
"shape":[480,640,3]} ← 表示去MP4取
}
}
episodes.jsonl(每行一个回合):
{"episode_index": 0, "tasks": ["pick up the red block"], "length": 3}
→ length:3 = 这回合 3 帧;episode_index:0 对应 episode_000000.* 文件。
stats.json(归一化统计,训练直接用):
{
"observation.state": {
"mean": [0.15,-0.35,-0.05,-1.07,0.02,1.45,0.75], ← 7关节各自的均值
"std": [0.04, 0.04, 0.04, 0.12,0.02,0.04,0.04] ← 各自的标准差
}
}
→ 训练时 归一化后 = (原值 − mean) / std,让数据分布规整,模型好学。
一帧是怎么被"拼出来"的(初学者关键):
想要"帧2"的完整数据:
① 从 episode_000000.parquet 第2行读:
state=[0.20,...], action=[0,0.05,0.10,...], reward=1.0, frame_index=2
② info.json说 cam_high 是"video" → 去 episode_000000.mp4 解码第2帧 → 得到图像
③ 拼成: {观测图像 + observation.state + action} → 喂给模型
└─ 图像来自MP4,数值来自Parquet,靠 frame_index=2 对齐
关键参数速查:
| 参数 | 含义 |
|---|---|
| fps | 帧率,30 表示每秒 30 帧 |
| features | 数据集的 schema,声明每个字段的 dtype/shape |
| dtype: “video” | 该字段图像存在 MP4 里,不在 Parquet |
| length | 一个回合有多少帧 |
| mean / std | 归一化用的均值和标准差 |
| frame_index | 缝合 Parquet 数值 和 MP4 画面 的帧号 |
⑥ RLDS(语义层)——嵌套结构,OXE 那套
同样数据在 RLDS 里,是"回合套步"的嵌套结构,一个"步(step)"长这样:
# 一个 episode = 3 个 step 的序列;下面是帧0的 step
step_0 = {
"observation": {
"image": <(480,640,3) uint8>, # 相机
"joint_positions": [0.10,-0.30,0.00,-1.20,0.00,1.50,0.80],
"gripper_width": 0.08,
},
"action": [0.01,-0.02,-0.01, 0,0,0, 0.0, 0], # 8维动作
"reward": 0.0,
"discount": 1.0, # RL折扣因子(RLDS特有)
"is_first": True, ← 这是回合第一帧 ┐ 回合边界
"is_last": False, ← 不是最后一帧 ┘ 靠标记位表示
"language_instruction": "pick up the red block",
}
关键参数含义(重点看和 LeRobot 的差异):
| 参数 | 含义 | 和 LeRobot 对比 |
|---|---|---|
| observation(嵌套字典) | 观测里再分 image/joint/gripper | LeRobot 是扁平的一行 |
| is_first / is_last | 标记回合的第一帧/最后一帧 | RLDS 靠标记位切回合;LeRobot 靠文件切 |
| discount | RL 折扣因子(未来奖励打折) | LeRobot 没有(它偏模仿学习) |
| 图像 | JPEG 直接塞进记录 | LeRobot 用 MP4(跨帧压缩更省) |
| 读法 | for episode: for step: 双层循环 |
LeRobot 是 dataset[i] 取帧 |
七格式实例总览(同一份 Franka 数据)
| 格式 | 3帧数据的组织形态 | action 在哪 | 图像在哪 | 回合边界怎么表示 |
|---|---|---|---|---|
| MCAP | 按时间交错的消息流 | ❌ 采集时没有,后期算 | /camera 话题 | 无(采集层不分回合) |
| HDF5 | 树: episode_0/{obs,joints,actions} | actions 数组(3,8) | observations 数组 | Group 分组 |
| Zarr | 文件夹: 每数组切块成文件 | actions/c/* | observations/c/* | Group 目录 |
| Parquet | 一张表(列式) | action 列 | ❌ 不存(另存视频) | (单文件=单回合) |
| LeRobot | meta+data(parquet)+videos(mp4) | Parquet 的 action 列 | MP4 视频 | 一回合一组文件 |
| RLDS | 嵌套: episode→steps | 每个 step 的 action | JPEG 塞进 step | is_first/is_last 标记 |
给初学者的三条实操建议:
- 拿到任何数据,先看三样东西:
action每一维是什么(增量?绝对?)、observation有哪些(几个相机?有没有本体状态?)、回合怎么切分。搞懂这三样,数据就"通"了。 - shape 是你的路标:看到
(T, 7)就知道 T 帧、7 个关节;(T,480,640,3)就知道是图像序列。读 shape = 读结构。 - 归一化统计量(mean/std)别忽略:训练前几乎都要归一化,LeRobot 直接给了(stats.json),RLDS/裸 HDF5 可能要自己算。数据量纲不统一,模型很难训。
总结这一节:概念地图 + 具体实例 = 真正"看懂数据"。下次你打开一个 LeRobot 或 OXE 数据集,对照这里的 Franka 例子,每个字段、每个参数就都对得上号了。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)