本文用一个贯穿到底的例子,讲清楚 LeRobot 是什么、它定义了哪些"协议"、一台新机械臂和一个新模型分别要怎么接入,以及 6 个舵机和 12 个舵机的机器人如何被同一种模型适配。文中结论对照过 LeRobot 源码(基于 LeRobot 0.6.x 的分支阅读);为了讲清楚,例子里虚构了一个新模型 “MiniVLA” 和一台新机械臂 “双臂 12 舵机”。代码是简化版,但类名、方法名与真实 LeRobot 一致。

一、前世今生:LeRobot 是什么

LeRobot 出现之前(2023 年及以前):每个实验室、每家公司都自己写一套数据格式、训练脚本和机械臂驱动。A 实验室发了个好模型,B 公司想在自家机械臂上用,几乎要重写一遍;你采的数据别人也用不了。

LeRobot 之前:N × M 次适配

模型 A

机械臂 1

机械臂 2

模型 B

LeRobot 之后:N + M 次适配

模型 A

LeRobot
统一接口

模型 B

机械臂 1

机械臂 2

图 1:没有统一接口时,每个模型和每台机械臂都要两两适配;有了 LeRobot,各自只需对接一次中间层。

2024 年:Hugging Face 发布了 LeRobot,目标是做机器人领域的 “transformers 库”:统一数据格式、统一训练流程、统一硬件接口。再配上一台低成本、可 3D 打印的 SO-100 机械臂,让学生也玩得起。

现在:它成了模仿学习领域事实上的标准。仓库里已经接入的机器人包括:

类型例子
单臂SO-101、Koch、NexArm、OpenArm、OMX
双臂双 SO 臂、双 OpenArm
移动底盘加机械臂LeKiwi
人形或半人形宇树 G1、Reachy2、HopeJR
移动小车EarthRover

所以它不只管机械臂,而是"任何能读出状态、能接收动作的机器人"都能接。

二、LeRobot 到底定义了什么

它定义了 5 类插口和 1 种数据格式,再加上 3 个现成的流程脚本:

Robot
机器人插口

lerobot-record
采集

Teleoperator
遥操作插口

Camera
相机插口

Dataset
统一数据格式

lerobot-train
训练

Policy
模型插口

训练好的权重

lerobot-rollout
推理

图 2:LeRobot 的整体架构。五类插口和统一数据格式被三个现成脚本串起来:机器人、遥操作、相机供采集使用,数据集和模型供训练使用,机器人和模型再一起用于推理。脚本通过 --robot.type、–policy.type 这类名字在登记表里找到具体实现。

所谓"协议",在这里就是三样具体的东西:

  1. 一组 Python 抽象类:规定"你必须实现哪几个方法、返回什么格式"。比如所有机器人都必须继承 Robot 类,实现 get_observation() 和 send_action()。
  2. 一种数据格式:规定采集下来的数据怎么存、叫什么名字。
  3. 一个登记机制:每个机器人、每个模型用一个名字登记,脚本里用 --robot.type=名字、--policy.type=名字 来调用。

这和 USB 很像:USB 规定了接口形状和通信规则,键盘厂商按规则做键盘,电脑厂商按规则做接口,双方不需要认识对方。

三、接入一台新机械臂,要做什么

例子:某厂商出了一台"双臂 12 舵机"的机械臂,左右各 6 个舵机。要接进 LeRobot,只需写一个类,回答"我有什么、怎么读、怎么写":

@RobotConfig.register_subclass("dual12")          # ① 登记名字
class Dual12Config(RobotConfig):
    port: str
    cameras: dict = {}

class Dual12(Robot):
    config_class = Dual12Config
    name = "dual12"

    JOINTS = ["left_shoulder_pan", "left_shoulder_lift", "left_elbow", "left_wrist_flex",
              "left_wrist_roll", "left_gripper",
              "right_shoulder_pan", "right_shoulder_lift", "right_elbow", "right_wrist_flex",
              "right_wrist_roll", "right_gripper"]

    @property
    def observation_features(self):                 # ② 声明"我能提供什么"
        return {f"{j}.pos": float for j in self.JOINTS} | {"front": (480, 640, 3)}

    @property
    def action_features(self):                      # ③ 声明"我能接收什么"
        return {f"{j}.pos": float for j in self.JOINTS}

    def get_observation(self):                      # ④ 读:调用厂商的舵机协议
        pos = self.bus.read_all_positions()         #    这一行是厂商私有的部分
        return {f"{j}.pos": pos[i] for i, j in enumerate(self.JOINTS)} | {"front": self.cam.read()}

    def send_action(self, action):                  # ⑤ 写:把 12 个目标值发给舵机
        self.bus.write_all_positions([action[f"{j}.pos"] for j in self.JOINTS])
        return action

    # 另有 connect / disconnect / calibrate 等,略

机械臂这边只做三件事:声明有几个关节和相机,实现怎么读,实现怎么写。真正跟硬件相关的,只有 self.bus 背后那段舵机通信代码。比如幻尔为 NexArm 写的适配代码约 300 行,做的就是这些事。

一个现成的真实例子:LeRobot 里的"双 SO 臂"就是这么做的。它不重新写驱动,而是把两条 6 舵机的 SO 臂拼在一起,给左臂每个关节名加前缀 left_,右臂加 right_,对外就变成一台 12 个关节的机器人。

厂商私有部分

LeRobot 统一接口(Robot 抽象类)

observation_features
声明能提供什么

action_features
声明能接收什么

get_observation()
读

send_action()
写

舵机总线驱动
self.bus

相机

12 个舵机

图 3:接入新机械臂时,左边是 LeRobot 规定要实现的接口,右边才是和具体硬件相关的代码。

四、12 个舵机和 6 个舵机,怎么"适配"

这是理解全局的关键一步。采集数据时,LeRobot 会把机器人声明的关节自动拼成一个向量:有几个关节,向量就有多长。

6 舵机的机械臂采集后,数据集的说明文件里会写成这样:

"observation.state": { "shape": [6],  "names": ["shoulder_pan.pos", "shoulder_lift.pos", "elbow_flex.pos",
                                                 "wrist_flex.pos", "wrist_roll.pos", "gripper.pos"] },
"action":            { "shape": [6],  "names": [ 同上 ] },
"observation.images.front": { "shape": [480, 640, 3] },
"observation.images.wrist": { "shape": [480, 640, 3] }

双臂 12 舵机采集后,同一位置就变成 "shape": [12],名字是 left_... 和 right_... 共 12 个。

模型怎么"理解"6 和 12? 说实话,模型不理解,它只认数字的个数和位置:

  • 它看到的不是"肩关节转 30 度",而是"一个长度为 6 的数字列表,第 1 个数是 2048"。
  • 关节名字是给人看的,也是为了保证数据和硬件对得上号,模型本身不读名字。
  • 每个数代表什么,是模型从示范数据里"统计出来"的:它发现"每次夹东西前,第 6 个数都会变小",于是学会在该夹的时候把第 6 个数调小。

模型的网络大小怎么跟着变? 分两种:

模型类型怎么处理 6 和 12源码依据
从头训练的小模型,如 ACT训练开始时读数据集说明文件,看到 shape: [6] 就把输入输出层建成 6,看到 12 就建成 12LeRobot 有专门的函数把数据集特征转成模型的输入输出形状
有预训练基座的大模型,如 SmolVLA基座尺寸固定,统一把向量补零到 32 位:6 个关节就是 6 个真实数加 26 个 0,12 个关节就是 12 个真实数加 20 个 0SmolVLA 配置里最大状态维度和最大动作维度都是 32,模型代码里有补零函数

所以,同一种模型可以用在 6 舵机或 12 舵机上,但训练出来的那份权重只属于那一台机器:在 6 舵机数据上训练的,第 6 位是夹爪;在双臂上训练的,第 6 位是左夹爪、第 12 位是右夹爪,含义完全不同,不能混用。

12 舵机双臂

6 舵机单臂

取前 6 个

取前 12 个

6 个关节读数

补 26 个 0

12 个关节读数

补 20 个 0

统一的 32 维向量

预训练 VLA
如 SmolVLA

输出 32 维动作

发给 6 个舵机

发给 12 个舵机

图 4:预训练大模型的输入输出尺寸固定为 32 维,不同关节数的机器人通过补零接入,输出时只取前 N 个。ACT 这类从头训练的小模型没有这一步,直接按关节数建网络。

五、接入一个新模型,要做什么

例子:某实验室发布了 “MiniVLA”,效果很好。要接入 LeRobot,需要写三个文件,每个文件只关心数据,不关心机器人是谁:

# 文件 1:configuration_minivla.py —— 登记名字、声明超参数
@PreTrainedConfig.register_subclass("minivla")
class MiniVLAConfig(PreTrainedConfig):
    chunk_size: int = 50          # 一次输出多少步动作
    max_action_dim: int = 32      # 像 SmolVLA 一样,统一补到 32 位

# 文件 2:modeling_minivla.py —— 实现协议规定的方法
class MiniVLAPolicy(PreTrainedPolicy):
    config_class = MiniVLAConfig
    name = "minivla"

    def __init__(self, config):
        # 从 config 里读到"动作有几维"(6 或 12),据此搭网络
        action_dim = config.output_features["action"].shape[0]
        self.net = 实验室原来的网络代码(...)

    def forward(self, batch):            # 训练用:给一批数据,返回误差
        pred = self.net(batch["observation.images.front"], batch["observation.state"])
        return loss(pred, batch["action"]), {}

    def select_action(self, batch):      # 推理用:给当前观测,返回下一步动作
        return self.net(batch["observation.images.front"], batch["observation.state"])[:, 0]

    def reset(self): ...                 # 每轮开始清空缓存
    def get_optim_params(self): return self.parameters()

# 文件 3:processor_minivla.py —— 前后处理(归一化、反归一化、图片缩放)
def make_minivla_pre_post_processors(config, dataset_stats): ...

模型这边只做三件事:登记名字;把作者原来的网络包进 forward 和 select_action 两个方法;定义前后处理。它只认 observation.state、observation.images.xxx、action 这些标准名字,不管背后是哪台机械臂。

写好后有两条路发布:合并进官方 LeRobot;或者打包成一个名字以 lerobot_policy_ 开头的插件(比如 lerobot_policy_minivla),LeRobot 启动时会自动发现并加载。

新模型需要写的三个文件

标准输入(与机器人无关)

配置

observation.state

observation.images.*

configuration_xxx.py
登记名字、超参数

processor_xxx.py
归一化 / 反归一化

modeling_xxx.py
forward / select_action

processor_xxx.py
反归一化

action

图 5:接入新模型时,模型只认标准名字的输入输出,不关心背后是哪台机械臂。

六、1 + 1 = 2:把它们拼起来

已知:

  • 机械臂 = NexArm(6 舵机),已实现机器人插口,登记名 nexarm_follower
  • 模型 = MiniVLA,已实现模型插口,登记名 minivla

第 1 步:采集。用主动臂示范"抓小件挂到钩上"100 次:

人拖动主动臂示范

lerobot-record
采集

标准数据集
state:[6] action:[6]
视频 + 统计值

lerobot-train
训练 MiniVLA

权重 + 配置
+ 归一化统计值

lerobot-rollout
推理

NexArm 自主完成挂件

图 6:从采集到推理的完整链路。

lerobot-record --robot.type=nexarm_follower --robot.port=/dev/ttyUSB0 \
  --robot.cameras="{front: {...}, wrist: {...}}" \
  --teleop.type=nexarm_leader --teleop.port=/dev/ttyUSB1 \
  --dataset.repo_id=yourname/hang_v1 --dataset.num_episodes=100 \
  --dataset.single_task="hang the part on the hook"

脚本每秒 30 次调用 NexArm 的 get_observation(),得到 6 个数和 2 张图,再记录主动臂的 6 个数作为"正确动作"。最终存成标准数据集,说明文件写明 state: [6]、action: [6],并算好每个关节的均值和方差。

第 2 步:训练。

lerobot-train --dataset.repo_id=yourname/hang_v1 --policy.type=minivla \
  --output_dir=outputs/hang_minivla

脚本读数据集说明文件,发现动作是 6 维,于是告诉 MiniVLA “输出 6 维”(MiniVLA 内部再补零到 32 位)。然后反复调用 forward:喂进图片和 6 个关节数,让它预测 6 个动作数,和人的示范对比,算误差、调参数。结束后保存权重、配置和归一化统计值。

第 3 步:推理。

lerobot-rollout --robot.type=nexarm_follower --robot.port=/dev/ttyUSB0 \
  --robot.cameras="{front: {...}, wrist: {...}}" \
  --policy.path=outputs/hang_minivla/checkpoints/last/pretrained_model

每秒 30 次循环:

后处理MiniVLA前处理NexArm后处理MiniVLA前处理NexArmloop[每秒 30 次]get_observation() 6 个关节读数 + 2 张图归一化后的观测select_action() 输出归一化动作反归一化为舵机目标值,send_action()

图 7:推理时的实时控制循环。

NexArm.get_observation()      →  [2048, 1530, 2900, 1800, 2048, 3100] + 两张图
前处理:按训练时的统计值归一化  →  [0.01, -0.83, 1.21, ...]
MiniVLA.select_action()        →  [0.02, -0.80, 1.18, ..., -1.40]   (第 6 位变小:要夹了)
后处理:反归一化               →  [2052, 1545, 2880, 1810, 2048, 2200]
NexArm.send_action()           →  6 个舵机转到这些位置,夹爪闭合

结果:NexArm 夹住小件、挂上钩子。整个过程里,机械臂厂商没写过一行 MiniVLA 的代码,MiniVLA 的作者也没见过 NexArm。这就是 1 + 1 = 2。

换成双臂 12 舵机,会怎样? 把三条命令里的 nexarm_follower 换成 dual12,重新采一批双臂数据,说明文件就会变成 [12],训练出的 MiniVLA 自动输出 12 维。代码一行都不用改,但必须重新采数据、重新训练,因为 6 维那份权重学的是另一副身体。

七、总结:谁负责什么

角色负责写什么不需要关心什么
机械臂厂商机器人插口:声明关节和相机,实现读、写用什么模型
模型作者模型插口:配置、forward、select_action、前后处理用什么机械臂
Hugging Face / LeRobot数据格式、采集训练推理脚本、登记机制具体的硬件和模型
落地团队选机械臂和模型;采高质量的任务数据;训练;做评测;放到真实工位底层接口

接口已经被别人铺好了,落地团队真正的价值在于任务数据、评测方法和现场落地。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐