数据录制
6.1 带相机的遥操作
lerobot-teleoperate \
--robot.type=so101_follower \
--robot.port=/dev/tty.usbmodem5B8E1136841 \
--robot.id=my_awesome_follower_arm \
--teleop.type=so101_leader \
--teleop.port=/dev/tty.usbmodem5B790188161 \
--teleop.id=my_awesome_leader_arm \
--robot.cameras="{wrist: {type: opencv, index_or_path: 2, width: 640, height: 480, fps: 30}, \
environment: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}}" \
--display_data=true6.2 录制数据集
lerobot-record \
--robot.disable_torque_on_disconnect=true \
--robot.type=so101_follower \
--robot.port=/dev/tty.usbmodem5B8E1136841 \
--robot.id=my_awesome_follower_arm \
--teleop.type=so101_leader \
--teleop.port=/dev/tty.usbmodem5B790188161 \
--teleop.id=my_awesome_leader_arm \
--robot.cameras="{wrist: {type: opencv, index_or_path: 2, width: 640, height: 480, fps: 30}, \
environment: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}}" \
--dataset.repo_id=local/so101_demo \
--dataset.root=/Users/xxx/project/lerebot/datasets/so101_demo \
--dataset.single_task="Grab the black cube" \
--dataset.num_episodes=30 \
--dataset.episode_time_s=30 \
--dataset.reset_time_s=20 \
--dataset.push_to_hub=false \
--display_data=true本方案把数据固定写到项目的 datasets/ 下。注意 --dataset.root 指向的是数据集目录本身(源码里 Path(root) 直接用,不再拼 repo_id),所以一条数据集 = 一个文件夹:datasets/so101_demo/。父目录 datasets/ 可以预先建好,但 so101_demo/ 这个叶子目录首次录制时必须不存在(源码是 mkdir(exist_ok=False)),重录要先删掉它,或改用 --resume=true 续录。不写 --dataset.root 时才会落到默认的 ~/.cache/huggingface/lerobot/<repo_id>。
体量预估:参数是 30 条 × 30 秒、间隔复位 20 秒:总时长约
30 × (30 + 20) = 1500 秒 ≈ 25 分钟(不含调试和重录);总计约30 × 30 × 30 = 27000帧、两路相机,落盘大约几百 MB 量级。开始前确认磁盘空间,录制过程中别让 Mac 休眠。
先定标准再开录
ACT 这类模仿学习策略,效果上限取决于数据,不取决于训练步数。所以录之前先把下面三条定下来——录完几十条才发现样本不够,只能推倒重来:
| 维度 | 目标值 | 为什么是这个数 |
|---|---|---|
| 总条数 | ≥ 50 集 | 样本太少的策略很容易变成"背下这条轨迹",动作幅度、起点稍变就失败 |
| 单个位置重复次数 | 同一位置录约 10 次 | 用来抵消人手的细微差异,让模型学到"这件事怎么做"而不是"这一条轨迹怎么走" |
| 初始位置多样性 | ≥ 5 个不同位置 | 物体起点是抓取任务里最容易过拟合的变量,多样性基本等于泛化能力 |
三条乘起来正好是 50 集:5 个位置 × 每个 10 次。别把 50 集全录在同一个位置——loss 会降得很漂亮,真上手一换位置就崩。
落地两个小动作:物体摆放用胶带在地上标出 5 个位置,避免凭感觉摆;每录完一集在纸上打一个勾,配合键盘控制,条数和位置都能对上号。
6.3 record 参数参考
下面是 RecordConfig 与 DatasetRecordConfig 的全部字段。标 必填 的漏一个就直接起不来。
| 参数 | 默认 | 作用 |
|---|---|---|
--robot.* | — | 从臂:type / port / id / cameras / disable_torque_on_disconnect |
--teleop.* | — | 主臂:type / port / id(主臂一律用 --teleop.) |
--dataset.repo_id | 必填 | 数据集标识,必须写成「命名空间/名字」带一个斜杠;不上传时只决定本地目录名 |
--dataset.single_task | 必填 | 任务描述文本,会写进每条样本;为空直接 ValueError |
--dataset.num_episodes | 50 | 一共录多少条 episode |
--dataset.episode_time_s | 60 | 每条 episode 的录制时长(秒),到点自动进入复位阶段 |
--dataset.reset_time_s | 60 | 两条之间的复位时间(秒),这段时间不写入数据集,用来把物体摆回初始位 |
--dataset.fps | 30 | 数据集采样帧率(录制循环频率),相机的 fps 不要低于它 |
--dataset.root | None | 落盘位置;不填就用 HF_LEROBOT_HOME/repo_id(默认 ~/.cache/huggingface/lerobot) |
--dataset.video | True | 把图像编码成 mp4;关掉则存 png,体积暴涨 |
--dataset.push_to_hub | True | 本方案必须显式设 false,否则录完会自动上传 |
--dataset.private / --dataset.tags | False / None | 只在 push 到 Hub 时生效,本地录制用不到 |
--dataset.rename_map | {} | 重命名观测键,本地录制一般不需要 |
--play_sounds | True | 用语音播报状态("Recording episode N"、"Reset the environment"…) |
--resume=true | false | 续录:num_episodes 为新增条数(写法见 6.5) |
--display_data | False | 实时可视化:弹 Rerun 窗口看画面与关节曲线(详见下方) |
--display_data 到底是什么
它是「实时可视化」开关,不参与录制,完全不影响落盘数据。打开后会额外弹出一个 Rerun 窗口,把这轮的画面和关节数值实时画出来;不加它(默认 False)就什么都不弹,数据照样录。
源码里它只做两件事:init_rerun(session_name="recording") 启动 Rerun 查看器,然后在每个控制循环里调一次 log_rerun_data(observation, action) 把数据推过去。窗口里能看到:
- 两路相机画面——观测里的图像数组(CHW 转成 HWC)当成图片推过去
- 每个关节的数值曲线——一维数组会拆成
observation.state_0…5与action.action_0…5,按时间轴画出来
所以它的实际用途是:遥操作时确认两路画面没接反(wrist 跟着末端动、environment 视角固定)、录制时盯住有没有哪帧画面卡住或某个关节突然跳变、评测时看模型每一步输出了什么动作。
两个细节:窗口是独立的应用窗口(由 rerun-sdk 提供,LeRobot 依赖里已带);源码默认把它限制在系统内存的 10%(环境变量 LEROBOT_RERUN_MEMORY_LIMIT 可调),所以录得很久时它会丢弃早期数据,属正常现象。程序退出时会自动 rerun_shutdown() 关掉。
6.4 录制键盘控制
录制开始后终端会监听按键(由 init_keyboard_listener() 注册),不用等倒计时跑完:
| 按键 | 行为 |
|---|---|
→ 右方向键 | 提前结束当前阶段:这一条录够了就按它,不必等满 20 秒;在复位阶段按则提前结束复位 |
← 左方向键 | 放弃并重录上一条 episode,录废了用这个 |
Esc | 立即停止整个录制;当前这条也会被保存(哪怕没录满),已保存的 episode 都保留 |
Ctrl+C | 强制中断:正在录的这条会被丢弃(不调用 save_episode()),已保存的保留;终端会打一串 KeyboardInterrupt 回溯,属正常 |
节奏建议:正式录制之前,先小批量试水:
--dataset.num_episodes=2 --dataset.episode_time_s=10,确认数据能落盘、能再读回来,再跑完整参数。录完检查datasets/so101_demo/meta/info.json里的total_episodes是否等于预期;想重新录要先删掉datasets/so101_demo/目录(首次录制要求它不存在)。
6.5 续录数据集
录到一半中断、或想分几次录,用 --resume=true 在同一个数据集上接着录。它会加载 root 下已有的数据集并追加新 episode,而不是新建一个。
lerobot-record \
--robot.disable_torque_on_disconnect=true \
--robot.type=so101_follower \
--robot.port=/dev/tty.usbmodem5B8E1136841 \
--robot.id=my_awesome_follower_arm \
--teleop.type=so101_leader \
--teleop.port=/dev/tty.usbmodem5B790188161 \
--teleop.id=my_awesome_leader_arm \
--robot.cameras="{wrist: {type: opencv, index_or_path: 2, width: 640, height: 480, fps: 30}, \
environment: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}}" \
--dataset.repo_id=local/so101_demo \
--dataset.root=/Users/xxx/project/lerebot/datasets/so101_demo \
--dataset.single_task="Grab the black cube" \
--dataset.num_episodes=29 \
--dataset.episode_time_s=30 \
--dataset.reset_time_s=20 \
--dataset.push_to_hub=false \
--resume=true \
--display_data=true关键:
--dataset.num_episodes在--resume=true下表示本次还要新增几条,不是数据集总数。循环条件是while recorded_episodes < num_episodes,计数从 0 起。所以「已经录了 1 条、目标总共 30 条」应该写--dataset.num_episodes=29。另外注意日志:
Recording episode N打印的是数据集当前总条数(源码log_say(f"Recording episode {dataset.num_episodes}"),取的是meta.total_episodes),不是"本次第几条"。续录时它天然带着存量,所以显示到 31 并不代表多录了一条。换算:日志最后的数字 = 开始前的条数 + num_episodes − 1。想知道本次真录了多少,比较前后meta/info.json的total_episodes。
实战踩坑 · 录到一半相机掉线:
现象:录到第十几集时程序卡住,日志刷
Waiting for image writer to terminate...,然后抛错:textTimeoutError: Timed out waiting for frame from camera OpenCVCamera(8) after 200 ms. Read thread alive: True.原因:机械臂运动时拉扯到了末端相机的连接处,USB 接触不稳定,取帧超时。括号里的
(8)就是出问题那一路相机的索引,照着抓拍出来的图就能认出是哪台。处理:重新插紧相机线,并且预留足够长度的线缆余量——正式开录前先手动走一遍最远的姿态,看线会不会被绷到。然后用
--resume=true接着录,已保存的 episode 都在,不必从头再来。这也是第 3 篇"相机直连、别串 hub"的另一面:末端那路线是全场唯一一直在动的线,它最脆弱。