用键盘给 Lerobot 机械臂采集 HIL-SERL 训练数据听上去有点返璞归真但真正上手做过一遍之后你会发现键盘在调试和干预环节反而比 SpaceMouse、游戏手柄更直接。尤其是刚装机、手上没有专用遥操作设备的时候键盘就是最稳定的保底方案。我手上这台 SO-100 构型的样机第一次跑通数据采集闭环全靠键盘后来我发现键盘太适合做 HIL-SERL 里的人类干预入口于是把整套流程固定下来写成了一套能直接复用的配置。这篇内容就按我实际动手的顺序来写先说清楚 HIL-SERL 数据集要什么再讲 Ubuntu 上怎么把 Lerobot 环境跑起来然后给键盘控制方案和完整配置文件最后整理采集过程中的常见问题和排错记录照着做基本不会跑偏。1. HIL-SERL 到底是什么为什么非要键盘不可1.1 HIL-SERL 数据集的“人机协同”含义HIL-SERL 这个名字拆开看SERL 是采样高效的机器人强化学习框架HIL 是人在回路干预。合起来的典型做法是机器人先按某个策略或随机动作往前走当它在执行任务过程中快要出错、卡住或者进入不安全状态时人在旁边用键盘立刻介入纠正它的动作轨迹。这个“人被加入干预”的过程不会中断 episode而是记录下干预前的状态、干预后的动作以及当前奖励信号最后一起作为强化学习训练数据。这个逻辑跟传统的“先录演示数据再行为克隆”很不一样。行为克隆只关心专家演示而 HIL-SERL 关心的是策略探索过程中产生的“探索轨迹 人类纠正”。这样的数据更适合离线 RL 或者带有安全约束的策略微调因为模型不仅能学到“什么是对的”还能学到“错了之后怎么拉回来”。对于只有一台机械臂的家庭实验室或者学校课题组来说HIL-SERL 最大的吸引力在于不需要人工标注大量数据也不需要昂贵的高精度示教器。你就坐在屏幕前看着摄像头实时画面机械臂跑得不对劲就按一下键盘干预动作自然进入数据流。1.2 键盘遥操作到底有哪些优势很多人第一反应是用 SpaceMouse 或游戏手柄但实际环境里键盘有几个不可替代的好处。一是零额外成本。Ubuntu 主机上自带键盘不需要专门买 USB 设备也不存在驱动冲突问题。二是按键就是离散事件非常适合作为“纠正开关”。在 HIL-SERL 流程里你并不希望人类全程精细控制机械臂只要在关键时刻把人体现出来。键盘上的方向键、加减速键、夹爪开合键天然可以映射成“小幅纠正”和“紧急干预”两类动作。三是键盘脚本非常容易调试出错时可以直接看终端键事件不像手柄那样还要处理摇杆漂移。当然键盘也有短板。无法像 SpaceMouse 那样给出连续、平滑的六维速度指令如果全部依赖键盘来做复杂轨迹示教机械臂动作会显得一顿一顿。所以在我的流程里键盘只作为 HIL 干预入口不作为长时间连续轨迹的唯一来源。这样既发挥它响应快、易集成的优势又避开它不善于精确连续控制的劣势。1.3 采集什么样的数据才能喂给强化学习注意HIL-SERL 不是随便录几段动作就行。要保证后续训练有效数据流里至少要有三类信息状态观测各关节角度、关节速度、夹爪开合量以及摄像头采集到的 RGB 图像。动作信息每个时间步发送给电机的目标位置、目标速度或力矩值。干预标记当前这个动作是机器人自主策略产生的还是人类按键纠正产生的。这个“干预标记”是 HIL-SERL 和普通遥控示教数据的核心差异。有了标记训练时就可以给不同来源的动作分配不同权重也可以设计带人类优先级的奖励函数。Lerobot 的数据集格式默认能存状态和动作但不会自动给你按“人是否按键”分标签所以在采集脚本里要自己把按键状态同步写进 episode 数据。这部分的实现我会在第 4 节详细讲。2. Ubuntu 环境搭建从干净系统到 Lerobot 就绪2.1 硬件连接和系统检查开始前先确认机械臂和主机的连接方式。以 SO-100 这类串行总线舵机机械臂为例通常是一个 USB 转串口模块接到主机相机可能是 USB 摄像头或者 CSI 摄像头。先不要急着装软件把硬件接好然后打开终端看两个东西ls /dev/ttyUSB* ls /dev/video*/dev/ttyUSB0是串行总线设备/dev/video0是摄像头设备。如果这两个设备不存在先查线材和驱动。Ubuntu 对常见的 CH340、CP2102 串口芯片和 UVC 摄像头都有内置驱动一般插上就能识别。我遇到过主板 USB 3.0 口对老式串口芯片供电不稳的情况换到 USB 2.0 口就正常了排查时值得优先试一下。系统建议使用 Ubuntu 22.04 或 24.04 的桌面版不要为了省资源去装服务器版。采集数据时需要看实时画面桌面环境能省掉很多 X11/Wayland 转发的麻烦。Python 版本推荐 3.10 或 3.11这个在 22.04 和 24.04 上都是开箱即用。2.2 安装依赖、创建虚拟环境并安装 Lerobot我比较推荐用虚拟环境管理机器人项目的依赖避免和系统 Python 包冲突。这里用conda或者venv都可以我平时用conda多一些因为后续处理数据可能需要装一些带 CUDA 的库conda 在环境隔离上更省心。conda create -n lerobot python3.10 conda activate lerobot sudo apt update sudo apt install -y git ffmpeg git clone https://github.com/huggingface/lerobot.git cd lerobot pip install -e .执行完pip install -e .之后Lerobot 会把很多依赖一起拉进来包括 PyTorch、torchvision、opencv-python 等。如果网络状况一般建议先用国内 PyPI 镜像安装否则下载几十个包能等很久。装完之后顺手验证一下python -c import lerobot; print(lerobot.__version__)如果这行没报错说明环境基础没问题。注意如果你要跑带 CUDA 的 RL 后续训练还需要单独安装对应版本的 PyTorch CUDA 版本采集数据阶段其实不强制要求 GPU。2.3 设备权限、固件检查与关节校准Linux 下访问串口设备经常遇到权限问题。插上机械臂后如果程序报找不到设备或者打开失败先看权限ls -l /dev/ttyUSB0如果设备属于 root 且只有 dialout 组有读写权限而你当前用户不在这个组里就用这条命令把用户加进去sudo usermod -aG dialout $USER然后重新登录或者临时用sudo chmod 666 /dev/ttyUSB0快速验证。注意重启后权限不会失效于组设置但chmod的临时权限会丢所以正规做法还是加组。Lerobot 对机械臂设备有比较完整的自检流程。如果是从别人那里接手的二手机械臂校准数据很可能对不上第一次连接时务必做一次关节零位校准。校准方法一般是运行 Lerobot 仓库里的校准脚本执行后它会逐关节把机械臂移动到零位并记录电位器或编码器读数python lerobot/scripts/calibrate.py --robot-path lerobot/configs/robot/so100.yaml校准时有个很容易踩的坑机械臂摆放在桌面上时关节位置千奇百怪如果起始姿态和预期零位相差太大校准脚本里的运动学解算会直接报错。我的做法是先手动把机械臂掰到近似竖直摆放确保所有关节都在可动范围中间区域再跑脚本。校准完成后会生成一个校准文件内容记录了每个关节当前传感器读数对应的角度后续所有控制都依赖这份校准结果。很多人口中的“机械臂偏差”问题八成是这一步没做对。3. 配置键盘控制按键映射、速度调节和遥控器实现3.1 Lerobot 的遥控接口设计原理Lerobot 的数据采集链路并不复杂核心逻辑就是“控制设备产生动作 - 机器人执行动作 - 记录状态和动作”。官方默认支持 SpaceMouse、Aloha 等设备但键盘也是一个合理的输入源。Lerobot 的机器人控制接口要求我们维护一个目标关节位置数组然后以固定频率把它发给电机驱动器。想通了这件事键盘控制的实现方案就清晰了按键产生离散事件每次按下对应一个“目标位置的微小增量”然后把更新后的目标位置发送给机械臂。比如按下方向键“上”就把某个轴的期望角度增加 0.05 弧度按下“下”就减少 0.05 弧度。由于电机控制频率达到 30 到 50 赫兹视觉上机械臂会平滑地朝一个方向持续运动直到你松手。这里有个很关键的设计增量式控制一定要基于“期望位置”而不是“当前位置”。如果每次按键都去读当前实际位置再加上增量机械臂会因为负载和回程间隙造成误差累积越动越偏。正确做法是在程序里维护一个target_joint_pos数组初始值等于启动时的实际位置每次按键只修改这个数组的某个元素再把这个数组发给机器人。3.2 一套实用的按键映射方案我实际用的按键映射并不复杂核心思想是“按轴控制”而不是“按末端方向控制”。原因很简单按轴控制可靠性高不会因为机械臂姿态改变而出现反直觉的方向。下表是我现在一直用的映射方案按键作用说明Q / A增加/减少第 1 关节角度对应底座旋转或肩关节偏航W / S增加/减少第 2 关节角度对应肩部俯仰E / D增加/减少第 3 关节角度对应大臂俯仰R / F增加/减少第 4 关节角度对应肘部俯仰T / G增加/减少第 5 关节角度对应腕部俯仰Y / H增加/减少第 6 关节角度对应腕部旋转U / J夹爪开 / 夹爪合增量控制夹爪位置Space急停立即停止所有运动Tab标记干预写入当前时间步为人类干预每个关节按键控制一个轴看起来原始但在 HIL 场景下非常清楚。你在屏幕上看到机械臂哪个关节不对直接按对应键去纠正不需要想着末端笛卡尔方向。如果你更习惯控制末端在三维空间里的进退也可以把方向键映射到末端 XYZ 平移但那就需要额外做运动学逆解代码复杂不少。3.3 键盘遥操作核心代码从按键到关节动作现在拿出一个可用的最小实现片段。这个代码依赖pygame来捕获按键事件用 Lerobot 的make_robot函数创建机器人对象然后通过read/write接口和机械臂通信。你需要先安装pygamepip install pygame核心循环大概是下面这个样子import time import pygame import numpy as np from lerobot.common.robot_devices.robots.factory import make_robot # 从配置文件创建机器人对象 robot make_robot(config/so100_keyboard.yaml) robot.connect() # 读取当前关节位置作为目标初始值 target_pos np.array(robot.read(joint_pos), dtypenp.float32) step 0.05 # 每次按键的角度增量单位弧度 pygame.init() screen pygame.display.set_mode((400, 300)) clock pygame.time.Clock() intervention_flag False while True: for event in pygame.event.get(): if event.type pygame.QUIT: pygame.quit() break if event.type pygame.KEYDOWN: if event.key pygame.K_ESCAPE: pygame.quit() break if event.key pygame.K_q: target_pos[0] step elif event.key pygame.K_a: target_pos[0] - step elif event.key pygame.K_w: target_pos[1] step elif event.key pygame.K_s: target_pos[1] - step # 其它按键按 3.2 表格继续映射 elif event.key pygame.K_SPACE: robot.send_action(np.zeros_like(target_pos)) continue elif event.key pygame.K_TAB: intervention_flag True # 将更新后的目标位置发送给机械臂 robot.send_action(target_pos) # 将 intervention_flag 写入数据记录器 # 记录部分见下一节 clock.tick(30) # 30 Hz 控制频率这个代码段省略了不同机械臂的传感器读取名称适配但它描述的核心思路是成立的按键修改目标位置数组控制循环高频下发。实际操作时请把你自己的机械臂关节顺序和 Lerobot 配置里的电机 ID 对应好否则会出现“按 Q 键动的是第三个关节”这种混乱。4. 手把手采集 HIL-SERL 训练数据4.1 初始化数据集和采样参数键盘控制跑通之后剩下的事就是把它接到 Lerobot 的数据记录器上。Lerobot 提供LeRobotDataset这个类专门用于创建和追加数据集输出格式以 HDF5 为主同步保存视频。初始化数据集的代码大致如下from lerobot.common.datasets.lerobot_dataset import LeRobotDataset dataset LeRobotDataset.create( repo_idmy-lab/hil-serl-demo, fps30, rootdata, use_videosTrue, )这里的fps30是数据采集频率它必须和控制频率一致否则录制出来的视频和关节状态会对不上。use_videosTrue表示每个 episode 会把摄像头画面压缩成 MP4 视频同时把关节状态、动作状态写入到 HDF5 的 time series 里。在开始录制之前有一个参数需要特别注意就是warmup_time_s。机械臂在启动瞬间会有轻微抖动如果立刻开始录制前面几帧数据质量会很差。我一般会先让机械臂空跑 2 到 3 秒再真正进入记录这样后面训练时不会因为开头几帧的异常轨迹污染整个 episode。4.2 录制一条 episode 的全过程一个完整 episode 的流程并不长我拆成几个阶段复位。把机械臂恢复到任务初始姿态目标位置数组设置成零位或固定初始位。预热。程序空转 2 秒让传感器读数稳定。开始记录。调用dataset.add_frame()将当前机器人状态、动作、图像、额外信息写入缓存。人工干预。键盘 Tab 键产生的干预标记随当前帧一起落盘。达到预设步数后调用dataset.save_episode()结束当前 episode。实际代码里核心记录循环可以这么组织for step_idx in range(episode_time_steps): # 读取真实状态 obs robot.read_observations() # 发送当前目标动作 robot.send_action(target_pos) # 把这一步的数据写进数据集 dataset.add_frame( observationobs, actiontarget_pos, intervention_flagintervention_flag, ) intervention_flag False time.sleep(1.0 / fps) dataset.save_episode()这里最容易被忽略的是intervention_flag的同步。按键事件发生在控制循环里的某个瞬间而add_frame也在同一循环中执行所以只要在按键事件里把标记置为 True并在下一次add_frame时读取并清空就能保证干预标记和动作数据落在同一个时间步上。4.3 数据目录里到底会生成哪些内容采集完若干个 episode 后去检查data目录你会看到类似这样的结构data/ └── my-lab/hil-serld-demo/ ├── meta/info.json ├── data/chunk-000/episode_000000.parquet ├── data/chunk-000/videos/chunk-000-video.mp4 └── ...Lerobot 的新版格式不直接用一个大 HDF5 文件存放全部数据而是采用分块存储每个块里面包含状态序列和视频文件。训练前需要使用它自带的数据处理脚本把原始采集目录转换成标准的 LeRobotDataset 格式。如果你更习惯直接导出 HDF5 或 MCAP 做后续分析可以写一个小脚本把 parquet 中的关节位置、动作和干预标记读出来再转存。这里说一句关于采集数量的经验值。如果你的任务只有夹取和放置两三个动作每个 episode 控制在 200 到 400 步之间先采 50 条再训练基本能看到效果。如果任务涉及多阶段操作比如“拿起来再放到特定位置再按压”建议每个阶段单独采 50 条以上否则离线 RL 很容易在阶段切换处出现策略崩溃。5. 完整配置文件与逐行解读5.1 我的hil_serl_keyboard.yaml配置文件说到配置文件这是整个流程里最值得给出一份完整版本的部分。下面的 YAML 文件是我在一台 SO-100 样机上实际跑通的配置机器人型号、串口、电机编号和采集参数都集中在一起# hil_serl_keyboard.yaml # 用于 HIL-SERL 数据采集的 Lerobot 机械臂配置 # 适用机器人SO-100 或同构型串行总线舵机机械臂 robot: type: so100 serial_port: /dev/ttyUSB0 baudrate: 1000000 motor_ids: [1, 2, 3, 4, 5, 6] joint_names: - base_rotate - shoulder_pitch - elbow_pitch - wrist_pitch - wrist_roll - gripper angle_limits: base_rotate: [-3.14, 3.14] shoulder_pitch: [-1.57, 1.57] elbow_pitch: [-1.57, 1.57] wrist_pitch: [-1.57, 1.57] wrist_roll: [-3.14, 3.14] gripper: [0.0, 0.04] teleop: keyboard_speed_step: 0.05 # 每次按键关节角度增量单位弧度 control_frequency: 30 # 控制频率单位 Hz use_virtual_display: false # True 时不弹出键盘监控窗口 cameras: front_camera: device: /dev/video0 resolution: [640, 480] fps: 30 wrist_camera: device: /dev/video1 resolution: [320, 240] fps: 30 dataset: repo_id: my-lab/hil-serld-demo root: data fps: 30 episode_time_steps: 400 num_episodes: 100 warmup_time_s: 2.0 use_videos: true overwrite: false配置文件本身并不难懂难的是它背后每一个参数选多少才合适。接下来把关键参数逐个拆开讲。5.2 核心参数选择角度增量、控制频率、episode 长度keyboard_speed_step设成 0.05 弧度是我反复试过的结果。SO-100 这类小臂步长太大容易造成末端振动步长太小又看不出明显移动。0.05 弧度每次按键动作幅度大约相当于末端移动 1 到 2 毫米既足够精细又能通过快速连续按键实现较大范围调整。控制频率我固定用 30Hz。这个值不是拍脑袋定的它是“视觉流畅度”和“舵机响应能力”之间的折中。串行舵机常见的刷新周期是 10 到 20 毫秒机械臂整体链路在 30Hz 下已经有约 33 毫秒的指令间隔足够让每个关节稳定跟上如果提到 50Hz很多总线舵机在高速连续位置更新下会出现追不上的问题最终表现为机械臂抖动。episode_time_steps设成 400也就是在 30Hz 下约 13 秒一条数据。这个长度非常适合短程抓取、推方块这类单步任务。如果任务耗时更长可以把步数提高到 800 到 1200但我不建议把单条录得太长越长越容易混入人类犹豫阶段的无用数据。5.3 相机参数和视频记录的坑相机是 HIL-SERL 数据里非常重要的一环。强弱光、阴影、反光都会直接影响后续视觉表征学习所以宁可先在采数阶段把图像质量控好也别指望训练时能靠数据增强补救。resolution我推荐主相机至少用 640x480腕部相机可以用 320x240这样视频体积可控也不至于丢失太多空间信息。关于device路径这里有个常见问题多个 USB 摄像头插在同一台机器上/dev/video0和/dev/video1的顺序并不固定。拔插一次可能就换顺序。为了防止采了一半发现录的是错相机画面我建议用udev规则给相机绑定固定设备名或者至少在启动脚本里打印当前设备索引对应的相机型号。别问我为什么强调这个我因为摄像头顺序问题白录过整整一上午的数据。6. 常见问题与排查技巧实录6.1 键盘没反应或者按键延迟明显遇到键盘没反应先确认终端窗口是否处于焦点状态。键盘监听中断的一个低级原因是你在采集时不小心点了屏幕上的其他窗口导致 pygame 窗口失去焦点。另一个原因是pygame.event.get()没有放在循环最前面读取事件路径被阻塞。把事件读取放在循环开头并且把clock.tick的帧率设定为略高于控制频率可以有效减少按键延迟。如果按键偶尔丢了检查是否开启了某些系统的按键重复功能。Ubuntu 默认长按按键会重复触发KEYDOWN事件这在设置目标位置时反而容易造成过度响应。我的做法是在程序里对同一个按键加入去抖只有按键从“未按下”变为“按下”时才修改目标位置这个在代码里用一个按键状态数组维护即可。6.2 机械臂抖动、漂移和偏差这是被问得最多的一类问题。表现是机械臂静止时关节在目标位置附近来回抖动或者长时间运行后某个关节角度和真实位置明显偏离。抖动大概率出在两个地方。第一是控制频率过高舵机跟不上位置更新我一般从 30Hz 往下降到 20Hz 或者 15Hz 看是否改善。第二是目标位置增量过小已经低于电机编码器的分辨率这会让电机在相邻两个位置之间反复震荡属于典型的极限环。把keyboard_speed_step从 0.01 提高到 0.05 左右能避开这个问题。漂移偏差则首先怀疑校准文件尤其是从别人那里拷过来的环境。校准文件里记录的电机零位偏移和当前机械臂安装角度不匹配时末端执行器会以一个微小角度偏置运动场景越远偏得越明显。解决方法是重新跑一遍校准脚本并把校准文件备份到项目目录下。另外长期使用后丝杆和关节间隙变大也会累积出固定方向的偏差这种情况需要机械上重新调整而不是靠软件补偿。6.3 HDF5 文件写不进去或者数据缺帧采集过程正常但最后发现 HDF5 打不开大概率是写入过程中程序被强制中断文件头没有正常收尾。Lerobot 的save_episode()方法里涉及视频写入、元数据合并和索引更新如果中途断电或者按 CtrlC 强杀缓存区里的数据只落了一半。我的建议是给采集主程序加一个try-finish结构在退出前自动调用save_episode()并且定期把当前已采 episode 数量打印出来。数据缺帧则多半是因为控制循环里的time.sleep不够准确。当系统负载较高或者机械臂串口通信偶尔超时sleep后实际间隔可能会大于设定周期导致真实采集频率低于 30Hz。连续采多段后视频和关节轨迹的时间戳偏移量会越来越大。最优解是使用绝对时间调度next_time time.time() while True: # 控制逻辑 next_time 1.0 / fps time.sleep(max(0, next_time - time.time()))这样做虽然不至于绝对精确但能消除time.sleep本身带来的累积漂移。6.4 提高数据质量的三条实操建议最后分享几个让我少走弯路的建议。第一条每采完 5 到 10 条 episode回放一下录好的视频确认画面没花、动作没跑偏。不要等到采完 100 条再统一检查到那时候返工成本太高。第二条HIL 干预标记一定要花心思做准。我一开始偷懒没有记录每次按键的干预标记训练时模型完全分辨不出哪些是探索动作哪些是人类纠正效果非常差。后来在每帧数据里加了干预标签用同一批数据训练出来的模型成功率直接翻了快一倍。第三条保持机械臂启动状态一致性。每次开启采集会话之前先把机械臂放到同一个初始位置再执行一次零位检查。如果初始姿态不同即使数据量再大学到的策略也容易过拟合到某一种初始姿态上。我在实际使用中最深的一个体会是HIL-SERL 数据采集真正难的不是代码或者配置而是“干预时机”的把握。键盘按下得太早机械臂没有充分探索模型学不到容错能力按得太晚轨迹已经跑飞拉回来成本太高。这个东西没有捷径只能自己盯着机械臂多采几次手感自然就出来了。说实话键盘控臂在精确操作上确实比不上专业遥操作器但用来做 HIL 干预和快速验证数据链路是我目前用过最顺手的方式。希望这份配置文件能帮你省下初期的调试时间把精力集中在真正重要的采集质量和后续训练上。