← 返回文章库

模仿学习怎么落地?把机器人数据接进 LeRobot 的完整流程

最后更新 2026-08-23
⏱ 约 25 分钟 🟡 涉接线/强电
你将学到
  • 讲清模仿学习这条链路的四段:采集、转换、训练、推理,每段的输入输出分别是什么
  • 看懂 LeRobot 数据集格式的组织方式,以及宇树的原始 JSON 数据是怎么转过去的
  • 理解行为克隆的根本困难「误差累积」,以及为什么演示数据的质量比数量更要紧

假设你手上有一台带双臂和灵巧手的机器人,想让它学会「把面包放进烤面包机」这么一件事。

按强化学习的路子来,你得先写奖励函数。可你写着写着就会卡住:面包捏歪了扣多少分?手指碰到烤箱边缘算不算失败?松手的时机怎么量化?这类任务的「做得好」是一种整体性的判断,拆不成一个能求导的标量。更麻烦的是,就算你硬写出来一个,机器人也需要在真实世界里试错几百万次才能收敛——而真实世界里的试错是要摔坏硬件的。

模仿学习给的是另一个答案:别写奖励了,人做一遍给它看。

戴上 XR 头显,用自己的手远程操控机器人的手,把这件事从头到尾做几十上百遍,全程录下来。然后训练一个神经网络,让它看到同样的画面时输出同样的动作。这就是行为克隆(behavior cloning)——把机器人控制彻底还原成一个监督学习问题。

这条路今天在操作类任务上是主流。而要把它落地,你需要一整套数据基础设施:采集的格式、存储的格式、训练框架吃的格式,三者得对上。宇树开源的 unitree_lerobot 就是干这件事的。这篇我们把这条链路的每一环拆开看。

照例先说边界:本文基于该仓库的公开文档与代码结构写成,我没有真机,也没有跑过其中任何一条命令,所以不会有「实测效果如何」的描述,涉及运行结果的地方一律以官方文档的说法为准。

先弄清楚这个仓库的身份

打开仓库第一件要搞明白的事:它不是一个从零写的训练框架,而是对 LeRobot 的修改。

仓库自己的描述里写得很直白——这是一个对 LeRobot 开源训练框架的修改版本,目的是让宇树 G1 机器人双臂灵巧手采集到的数据能够被训练和测试。README 的介绍部分也再次强调,本仓库用于 LeRobot 训练验证与宇树数据转换两件事。

LeRobot 是 Hugging Face 主导的开源机器人学习框架,仓库的致谢部分把它列在第一位,同时列出的还有 unitree_sdk2_pythonxr_teleoperateunitree_sim_isaaclab。致谢里写了一句话值得注意:请访问这些 URL 查看各自的 LICENSE。这不是客套——基于它做二次开发时,上游各项目的许可条款是各自独立生效的,你得逐个去看。仓库根目录有 LICENSE 文件,以仓库当前内容为准。

顶层目录只有三块,README 用一张表说清了分工:

lerobot/      —— 用于训练的 lerobot 仓库代码
utils/        —— 宇树数据处理工具
eval_robot/   —— 宇树真机推理验证

lerobot/ 是以 git submodule 形式引入的,README 的表格里明确标注了它对应的 commit 版本号。这个细节挺重要:上游框架被钉死在了某个版本上。 机器学习框架的接口变动频率不低,数据集格式还在演进,如果不钉版本,用户拉下来的代码大概率跑不通。所以克隆时那句 --recurse-submodules 不是可选项。

这也解释了为什么整个仓库的自有代码这么少——真正的训练逻辑全在上游,宇树补的是两头:前面把自家数据转成上游认识的格式,后面把上游训出来的模型接到自家机器人上。

数据格式:这条链路真正的地基

整篇文章如果只能记住一节,就记这一节。模仿学习工程里最花时间的从来不是训练,是让数据长成正确的样子。

采集出来的原始数据长什么样

数据来自遥操作。README 指向的采集项目就是宇树那套 XR 遥操作程序——戴头显、用手部追踪或手柄驱动机器人双臂和末端执行器,程序开着录制模式,把整个过程存下来。

存出来的结构是这样:

datasets/                          # 数据集根目录
    └── task_name/                 # 任务名
        ├── episode_0001           # 第一条轨迹
        │    ├── audios/           # 音频信息
        │    ├── colors/           # 彩色图像
        │    ├── depths/           # 深度图像
        │    └── data.json         # 状态与动作信息
        ├── episode_0002
        ├── episode_...

这个结构透露了几件事。

第一,数据的基本单位是 episode,也就是一条完整轨迹。 不是一帧一帧散着存的,是一次任务尝试打一个包。这跟模仿学习的问题设定完全对应:你要学的是「从开始到完成」这个完整过程。

第二,一条 episode 里图像和状态是分开存的。 图像走文件目录,状态和动作走一个 data.json。彩色图和深度图分两个目录,音频还单独占一个位置。

第三,状态和动作在同一个文件里。 这是行为克隆的数据本质——你要的是一组「当时看到什么、当时做了什么」的配对。观测(图像 + 关节状态)是输入,动作是标签。监督学习的味道到这里就已经很浓了。

转换脚本干了什么

原始 JSON 不能直接喂给 LeRobot,中间有两步。

第一步是排序重命名:

python unitree_lerobot/utils/sort_and_rename_folders.py \
        --data_dir $HOME/datasets/task_name

README 给的理由是,生成 LeRobot 数据集时建议保证命名从 episode_0 开始、连续且有序。这条看起来很琐碎,但它对应一个真实的坑:你删掉几条坏轨迹之后,编号就会出现空洞。 上游框架按索引取数,中间断号轻则报错,重则静默错位。所以这个脚本不是可有可无的整理癖,是删数据之后的必要收尾。

第二步是格式转换:

python unitree_lerobot/utils/convert_unitree_json_to_lerobot.py \
    --raw-dir $HOME/datasets \
    --repo-id your_name/repo_task_name \
    --robot_type Unitree_G1_Dex3 \
    --push_to_hub

四个参数里,--robot_type 是最值得停一秒的那个。README 给出的可选值包括 Unitree_Z1_SingleUnitree_Z1_DualUnitree_G1_Dex1Unitree_G1_Dex3Unitree_G1_BraincoUnitree_G1_InspireUnitree_G1_Dex1_Sim,并且明确说你可以基于脚本里的 ROBOT_CONFIGS 定义自己的类型。

为什么需要这个参数?因为状态向量和动作向量的维度与含义,完全取决于你用的是什么本体。 单臂和双臂不一样,装 Dex3 灵巧手和装夹爪不一样,换成第三方的灵巧手编码方式又不一样。转换脚本要把 JSON 里那一堆数字摆进一个规整的张量,就必须先知道每一位代表哪个关节。ROBOT_CONFIGS 就是这张对照表。

这里还藏着一个提醒:那个 Unitree_G1_Dex1_Sim,README 注明它是用于仿真环境采集的机器人类型,头部配的是单视角相机。仿真采集和真机采集的观测构成不一样,所以要单独占一个类型。 这是个很实在的工程细节——相机数量变了,观测的 schema 就变了,不能混用。

--push_to_hub 决定要不要把转好的数据集推到 Hugging Face Hub。仓库里另外还有 convert_lerobot_to_h5.pyconvert_unitree_json_to_h5.py 两个脚本,说明这套数据在不同格式之间是双向可转的,HDF5 是机器人数据里另一个常见容器。

转完之后,你拿到的是什么

README 给了一段加载代码,这段代码比任何文字描述都更能说明 LeRobot 数据集的组织方式:

from lerobot.datasets.lerobot_dataset import LeRobotDataset
import tqdm

episode_index = 1
dataset = LeRobotDataset(repo_id="unitreerobotics/G1_Dex3_ToastedBread_Dataset")

from_idx = dataset.meta.episodes["dataset_from_index"][episode_index]
to_idx = dataset.meta.episodes["dataset_to_index"][episode_index]

for step_idx in tqdm.tqdm(range(from_idx, to_idx)):
    step = dataset[step_idx]

看懂这几行,你就看懂了这个格式的核心设计。

数据集在索引层面是「一整条扁平的帧序列」dataset[step_idx] 直接按全局帧号取一帧。episode 的边界不体现在存储结构上,而是记在元数据里——meta.episodes 里存着每条 episode 的起始帧号和结束帧号,你想取第 N 条轨迹,先查表拿到区间,再在区间里遍历。

这个设计是为训练服务的。监督学习要的是能随机打乱、随机采样的样本池,如果数据按 episode 嵌套存放,每次采样都要先选轨迹再选帧,多一层间接。摊平之后,采样器直接在一个大整数区间里随机抽就行了。而需要按轨迹操作时(比如回放、可视化、按 episode 划分训练验证集),查一次边界表也不贵。

顺带一提,README 的排错表里列了好几条 FFmpeg 相关的报错,包括编码器缺失和 libtorchcodec 加载失败。从这几条能反推出:图像不是一张张 PNG 平铺存着的,而是编码成了视频,读取时按需解码。 这对机器人数据集来说几乎是必然选择——一条轨迹几千帧、多路相机,散图存储的体积和 IO 开销都撑不住。代价就是环境里必须有一套能用的 FFmpeg,这也是新手在这一步最常卡住的地方。

README 还给了可视化脚本:

python src/lerobot/scripts/lerobot_dataset_viz.py \
    --repo-id unitreerobotics/G1_Dex3_ToastedBread_Dataset \
    --episode-index 0

转换完先可视化看一眼,是这条链路里性价比最高的一个习惯。 格式错位、时间戳错乱、图像和动作对不上,这些问题在训练曲线上看不出来,肉眼一看就知道。

数据编辑器:最容易被跳过、其实最该用的东西

仓库里有个 data_editor/ 目录,装着中英文两个版本的 PyQt5 图形程序。README 对它的说明是:帮助用户处理数据,目前可以裁掉 episode 里多余的片段、删除坏的 episode。

用之前要先装 PyQt5,然后:

cd data_editor
python data_editor_EN.py

操作方式 README 写得很具体:选中数据集路径之后,拖动红线可以跳转播放,按住 Shift 拖动红线选择一段区间,再点「Trim Selected Range」裁掉;当前这条轨迹整个不要,就点「Delete Current Episode」。

这个工具值得单独拿出来说,因为它对应模仿学习里一条最容易被忽略的原则。

README 举的例子很朴实:如果某条 episode 没能完成任务,你需要把它删掉,以提升数据质量。

行为克隆的模型会忠实地学会演示者的一切,包括坏习惯。 你遥操作的时候手抖了、犹豫了两秒、抓空一次又重新抓——这些全都会被当成「正确答案」写进训练集。模型不知道哪一段是你在犯错,它只知道「在这个观测下人类是这么动的」。所以一条失败的轨迹留在数据集里,不是白占空间,是主动往模型里灌毒。

这就是为什么在模仿学习里,演示数据的质量比数量更关键。多一百条平庸的轨迹,抵不上删掉十条烂轨迹。而裁剪功能对应的是另一类杂质:一条 episode 开头你在调整姿势、结尾你在发呆,这些片段里「观测」和「有意义的动作」是脱钩的,留着只会让模型学到「有时候可以不动」。

一个图形化的数据编辑器出现在一个机器人学习仓库里,本身就说明了一件事:这条链路上,人工筛数据是绕不过去的一环。

完整链路:四段的输入输出

把上面的内容串起来,整条链路是这样的:

遥操作采集  →  数据转换  →  训练  →  推理部署

第一段,遥操作采集。 输入是人的动作(XR 设备捕捉的手部或手柄姿态),输出是宇树 JSON 格式的原始 episode 目录。中间经过逆运动学解算和重定向——把人手的姿态映射成机器人关节角。这一段的工程量不小,XR 遥操作那篇展开讲过。

第二段,数据转换。 输入是原始 JSON 目录,输出是 LeRobot 格式的数据集(本地或推到 Hub)。中间夹着排序重命名和人工筛选两道工序。

第三段,训练。 输入是数据集的 repo_id,输出是 checkpoint 目录下的 pretrained_model。README 明确说训练部分请参考 LeRobot 官方的训练示例与参数,宇树这边不重复造轮子,只给出命令模板:

python src/lerobot/scripts/lerobot_train.py \
    --dataset.repo_id=unitreerobotics/G1_Dex3_ToastedBread_Dataset \
    --policy.push_to_hub=false \
    --policy.type=act

第四段,推理部署。 输入是训好的模型路径加数据集(用于确定观测格式),输出是发给机器人的关节指令。

这一段在 eval_robot/ 目录下,而且提供了三个入口,分工很清楚:

eval_g1.py           —— 真机推理
eval_g1_sim.py       —— 仿真环境推理
eval_g1_dataset.py   —— 在数据集上评估
replay_robot.py      —— 回放数据集到机器人
eval_UniArmL1.py     —— 另一款机械臂的推理入口

主要参数 README 逐条注释了:--policy.path 指向预训练模型、--repo_id 指定数据集、--frequency 控制评估的时间步频率(示例里给的是 30)、--arm 选机械臂型号(示例给了 G1_29G1_23)、--ee 选末端执行器(示例给了 dex3dex1inspire1brainco)、--visualization 开可视化、--send_real_robot 决定要不要真的把指令发给机器人。

--send_real_robot 这个开关设计得很好。 它让你可以先跑完整条推理流程、看着可视化确认模型输出正常,但一个指令都不发出去。真机调试的第一条规矩就是「先空跑」,这个参数把它变成了默认可用的能力。

eval_g1_dataset.py 的存在也值得说:在数据集上评估模型,本质是拿训练数据(或留出的验证轨迹)当输入,看模型的输出和演示者当时的动作差多少。这一步不需要机器人,是最便宜的一道过滤网,跟强化学习那条链路里的 Sim2Sim 起的是同类作用——在进入高成本验证之前,先用低成本手段筛掉明显不行的模型。

replay_robot.py 则是反过来:不带模型,直接把录好的数据集回放到机器人上。README 说它用于测试和验证机器人的行为。这个功能在排查问题时特别有用——如果回放都动不对,那问题在数据或坐标系上,跟模型无关。

推理还需要图像输入,所以 eval_robot/image_server/ 下有 image_server.pyimage_client.py 一对,README 也提示了运行前要先按遥操作项目的说明启动图像服务。注意这里的对称性:训练时的观测怎么来的,推理时就得怎么来。 相机型号、分辨率、话题名、图像预处理只要有一处对不上,模型看到的就是它没见过的分布。

eval_robot/robot_control/ 下的文件也印证了这一点:

robot_arm.py            —— 双臂关节控制
robot_arm_ik.py         —— 逆运动学
robot_hand_unitree.py   —— 宇树灵巧手
robot_hand_inspire.py   —— 因时灵巧手
robot_hand_brainco.py   —— 强脑灵巧手
mobile_control.py       —— 移动底盘

一只手一个文件,说明不同厂商的灵巧手在控制接口和指令编码上各不相同,得逐个适配。而 assets/ 下每种手都配了左右两个 URDF 和一套 STL 网格,则是逆运动学和重定向算法要用的。

支持哪些策略

训练命令里的 --policy.type 决定用哪种策略。README 给出了带完整命令示例的几种:actdiffusionpi0pi05groot。发布说明里也提到,v0.3 增加了对 pi05groot 的支持。README 还提到多卡训练可参考上游文档。

我不打算逐个展开这些策略的原理——README 只给了命令和指向上游文档的链接,超出这个范围就是我在推断了。但从命令行参数本身能读出一点东西。pi05 那条命令带了一串训练配置:--policy.compile_model--policy.gradient_checkpointing--policy.dtype=bfloat16,还有 --policy.pretrained_path 指向一个基座模型。带基座权重、要开梯度检查点、要用 bfloat16——这几个特征放在一起,说明它是个规模不小的预训练模型,走的是「加载通用基座再在你的数据上微调」的路子,而不是从随机初始化开始训。

排错表里还有一条:google/paligemma-3b-pt-224 访问受限,解决办法是登录 Hugging Face 并申请权限。这说明其中某些策略在初始化时会去拉取 Hub 上的门控模型权重。这类「跑之前先去申请权限」的依赖,是复现这类工作时最容易在半夜卡住的地方,提前知道能省不少时间。

关于这些模型能做到什么程度、哪个效果更好,README 没写,我也没有真机可验,所以这篇不给结论。想了解视觉—语言—动作这类模型的整体思路,可以先看VLA 模型那篇

行为克隆最根本的那个坑:误差累积

到这里,链路已经讲完了。但如果只讲工程流程不讲原理,你按流程走一遍很可能会撞上一堵墙:在数据集上评估得挺好的模型,一到机器人上就莫名其妙地崩掉。

这不是你的实现有 bug,这是行为克隆的结构性困难,学界叫它 compounding error,误差累积。

道理其实不复杂。训练的时候,模型看到的每一个观测都来自人类演示者走过的轨迹。它学会的是「在演示者到过的这些状态下该怎么动」。

但真正跑起来的时候,机器人是沿着模型自己的输出往前走的。第一步模型输出的动作和人类当时的动作差了那么一点点,机器人就会来到一个人类演示时没到过的位置。在这个稍微陌生的状态下,模型的输出会更不准,于是下一步偏得更多,来到一个更陌生的状态……

训练分布和执行分布不是同一个分布,而且偏差会自我放大。 监督学习的经典假设(训练数据和测试数据独立同分布)在这里从根上就不成立——测试时的数据分布,取决于模型自己的行为。

这就是为什么模仿学习不是「录点数据训一下就完事」。工程上有几条常见的缓解思路:

第一,更多、更多样的演示。 不只是量大,关键是覆盖面广——起始位置变一变,物体摆放变一变,中途故意做一些小偏移再纠正回来。目的是让模型见过「稍微偏了之后该怎么办」,而不只是见过完美轨迹。这解释了为什么这类工作动辄要采几十上百条 episode 而不是三五条。

第二,动作分块(action chunking)。 不是每一帧都预测下一个动作,而是一次预测未来一小段动作序列,然后按这段执行若干步再重新预测。这样做的直接好处是把决策次数减少了——误差累积的次数正比于决策次数,一次决策管十步,累积就慢十倍。副作用是动作更连贯,不容易出现逐帧预测常见的抖动。

第三,数据增强。 图像上做随机裁剪、颜色扰动之类的处理,让模型不要过度依赖某些和任务无关的视觉线索(比如背景里那张桌子的花纹)。

第四,就是前面说的数据质量。 演示越干净、动作模式越一致,模型学到的映射就越稳定。同一个任务,两个人用两种完全不同的手法演示,混在一起训,模型很可能学出一个两者的平均值——而这个平均值可能哪个都不像,甚至根本完不成任务。

回头再看那个数据编辑器,你会觉得它出现在这个仓库里一点也不多余。

什么时候用模仿学习,什么时候用强化学习

这两条路线不是替代关系,各自适合的场景差别很清楚。

奖励好写、能大量试错的,适合强化学习。 行走就是典型:站着别摔、往前走、别乱抖,这些都能写成明确的数值项;而 GPU 仿真器能同时跑几千个实例,试错成本趋近于零。宇树那套强化学习训练链路整条流水线的存在前提,就是「在仿真里可以随便摔」。

奖励难定义、演示容易获取的,适合模仿学习。 精细操作就是典型:怎么捏、怎么放、什么时候松手,这些用数值描述极其别扭,但人戴上头显做一遍毫不费力。而且操作任务往往涉及形状材质各异的物体,在仿真里建模的代价高、失真也大,反倒不如直接在真机上采数据。

实践中这两者经常组合使用。一种常见的分工是:底层的移动和平衡交给强化学习出来的控制器,上层的手臂操作交给模仿学习出来的策略。从这个仓库的参数设计里也能看到这种分层的痕迹——推理脚本关心的是 --arm--ee,也就是手臂和末端执行器,机器人怎么站稳、怎么走,不在它的职责范围内。

收束

读完这条链路,有三件事值得带走。

第一,模仿学习的工程重心在数据,不在模型。 从这个仓库的构成就能看出来——真正的训练代码是引入的上游 submodule,宇树自己写的是转换脚本、数据编辑器和部署适配。你要花的时间也会按这个比例分配。

第二,「格式」这个词在这里的分量比你想的重。 robot_type 决定向量语义,ROBOT_CONFIGS 是本体到张量的对照表,仿真采集要单独占一个类型,推理时的图像来源必须和训练时对齐。这条链路上所有难查的 bug,多数是格式对不上,而不是算法不行。

第三,误差累积是行为克隆的天花板所在。 理解了「测试分布由模型自己的行为决定」这件事,你就能明白为什么这个领域的努力方向是动作分块、更大规模更多样的数据、以及各种把执行轨迹拉回训练分布的手段。不知道这一点,你会把结构性问题当成调参问题,白白耗掉很多时间。

想继续往下走,VLA 模型那篇讲的是把语言指令也接进这条链路之后会发生什么;想看另一条技术路线的完整实现,强化学习那条链路从训练到真机部署也是四段式,两篇对着读,各自的代价和适用边界会清楚很多。

📄 来源 / 自校链接

本文为公开资料整理,非亲测。关键参数与代码请结合实物与下列官方来源验证。

内容有错、看不懂、或想看下一期?告诉我们 →

本文为公开资料的学习整理,非亲测。涉接线/花钱/合规的步骤请结合实物与官方最新资料验证,风险自负。见免责声明