LeRobot机器人模仿学习:数据质量量化与版本化实战

发布时间:2026/10/14 5:53:17
LeRobot机器人模仿学习:数据质量量化与版本化实战
1. 数据量迷信是怎么来的又为什么必须打破刚接触机器人模仿学习那会儿我跟大多数人的想法一样数据嘛多多益善。反正采数据就是遥控机械臂来回跑跑个几百条轨迹丢进网络里训loss 降下去了模型看起来就“学会了”。直到有一次我用同一套 SO-ARM101 采了 300 条抓取轨迹训练出来的策略在真机上成功率只有三成出头而隔壁同事只用了 80 条精挑细选的数据成功率稳定在八成以上。那一刻我才真正意识到训练数据的价值从来不是由条数决定的而是由质量、分布和可追溯性共同决定的。这篇文章想聊的就是这件事在 LeRobot 这套机器人学习框架下数据质量到底怎么衡量版本化又该怎么落地。关键词里出现的 LeRobot、数据质量、版本化、Python、LanceDB其实正好串成了一条完整的链路——用 Python 写采集和清洗脚本用 LeRobot 做数据集格式统一和策略训练用 LanceDB 这类向量/列式存储做数据检索与版本比对。适合谁看如果你正在用 LeRobot 跑 ACT、Diffusion Policy 这类模仿学习策略或者你手上有 SO-ARM101 之类的低成本机械臂正准备认认真真做一批能复现的数据集那这篇内容应该能帮你少走不少弯路。我先把结论摆在前面后面再一条条拆开讲数据量的边际收益会非常快地衰减而数据质量的边际收益在很长一段时间内是递增的。原因不复杂——模仿学习本质上是让策略去拟合“状态到动作”的映射如果你的数据里同一个视觉状态下对应了互相矛盾的动作网络只能学出一个“平均值”而这个平均值在真机上往往就是抓空、撞桌、抖动。数据越多这种矛盾如果没被清理反而会把错误放大。所以真正的问题不是“要不要更多数据”而是“我怎么知道手上这批数据干不干净、够不够、能不能复现”。这就引出了版本化——不是 Git 那种管代码的版本化而是管数据内容、管采集条件、管清洗规则的版本化。下面我按实际做项目的顺序把整套思路和操作拆开。2. 先搞清楚 LeRobot 的数据长什么样2.1 LeRobotDataset 的目录结构与字段含义要用好 LeRobot第一步是弄明白它把一条轨迹存成了什么。LeRobot 的数据集遵循一套相对固定的目录约定核心是meta和data两块。meta/info.json里记录了 fps、特征维度、总帧数、总 episode 数这些全局信息meta/episodes.jsonl每一行描述一个 episode 的起止帧和任务描述真正的帧数据按 chunk 分片存在data/下面通常是 Parquet 格式每一帧包含图像、关节状态、动作等字段。这里有个容易被忽略的点LeRobot 的 action 通常是相对当前状态的增量而不是绝对关节角。如果你自己写采集脚本时存的是绝对角度直接塞进 LeRobot 训练策略学出来的动作幅度会完全不对。我第一次踩这个坑就是因为在采集端存了绝对位置训练时 loss 看着正常真机一动就飞出去。后来统一在转换脚本里做差分问题才解决。import numpy as np def to_delta_action(states, actions): # states/actions: (T, D) delta actions[1:] - states[:-1] return delta上面这段是最朴素的差分逻辑实际用的时候要注意首帧对齐和末帧丢弃否则长度会对不上。LeRobot 官方在lerobot.common.datasets里提供了转换工具但理解底层逻辑能让你在出问题时知道去哪查。2.2 为什么字段一致性比数据量更致命我见过太多数据集栽在字段不一致上有的 episode 图像是 480x640有的是 240x320有的相机帧率 30fps有的 15fps有的任务描述写“pick the cube”有的写“grab cube”。这些在单条数据里都看不出问题一旦混在一起训练网络就要花大量容量去“对齐”这些无关变量真正该学的动作映射反而学不好。LeRobot 在加载时会做一部分校验但校验是宽松的不会因为你混了分辨率就报错它只会默默按某个规则 resize。默默处理是数据质量最大的敌人因为你根本不知道模型看到的到底是什么。我的做法是在采集脚本里就强制统一分辨率、fps、相机内参、任务描述模板全部写死成常量任何一条不符合的直接拒绝入库。字段常见坑我的处理方式图像分辨率混用不同相机采集端强制 resize 到统一尺寸fps不同设备帧率不一统一重采样到 30fpsaction 定义绝对/增量混用转换脚本统一转增量任务描述措辞不统一用固定模板 枚举任务名关节顺序左右臂顺序颠倒在 info.json 里显式声明这张表是我踩坑之后总结的每一条背后都对应过一次真机翻车。字段一致性这件事做的时候觉得啰嗦不做的时候代价极大。3. 数据质量到底怎么量化别只靠肉眼3.1 从“看起来还行”到可计算的指标“这批数据质量不错”这种话在项目里毫无意义因为没法复现也没法比较。我后来强迫自己把质量拆成几个可计算的维度轨迹完整性、动作平滑度、状态覆盖度、任务成功率、视觉多样性。每一个都能用 Python 算出来算出来之后就能排序、能筛选、能对比不同批次。轨迹完整性最简单看每条 episode 的帧数是否落在合理区间有没有中途断掉。动作平滑度用相邻帧动作的差分范数突变特别大的地方往往是遥控抖动或者采集丢帧。状态覆盖度稍微麻烦一点我一般把关节状态做归一化后投影到低维看这批数据在状态空间里覆盖了多大范围——如果 300 条数据全挤在一个小区域那本质上和 30 条没区别。def smoothness_score(actions): diffs np.diff(actions, axis0) jerk np.linalg.norm(diffs, axis1) return float(np.mean(jerk))这个分数越低越平滑。实测下来正常遥控采集的轨迹 jerk 均值在某个区间内明显高于这个区间的八成是采集时手抖或者遥控器信号问题我会直接标出来人工复核。3.2 用 LanceDB 做数据检索与相似度去重数据一多肉眼根本看不过来这时候 LanceDB 就派上用场了。它是个嵌入式向量数据库Python 里几行代码就能建库、写入、检索特别适合做“找相似帧”和“去重”这类操作。我的典型用法是把每一帧的图像过一个预训练视觉编码器得到 embedding连同 episode_id、frame_id 一起写进 LanceDB然后做近邻检索。为什么要做这个因为遥控采集时人会不自觉地重复相似动作导致大量冗余帧。这些冗余帧对训练几乎没有增益却会拖慢训练、放大分布偏差。用 LanceDB 检索出相似度极高的帧簇每个簇只保留少量代表帧数据量能砍掉三四成训练效果反而更稳。import lancedb import numpy as np db lancedb.connect(./lerobot_emb) table db.create_table(frames, data[ {episode: 0, frame: 0, vec: np.random.rand(128).tolist()} ]) query np.random.rand(128).tolist() results table.search(query).limit(5).to_list()上面是 LanceDB 的最小可用示例实际项目里 embedding 维度会更高写入时建议分批避免一次性把内存打满。检索出来的相似帧我会按 episode 分组统计如果某个 episode 里超过一半的帧都能在别的 episode 找到高度相似的对应这条 episode 的边际价值就很低可以考虑剔除。提示LanceDB 的相似度检索默认用 L2 距离做图像 embedding 去重时建议先做归一化否则距离会被向量模长主导检索结果不准。3.3 质量筛选的阈值怎么定别拍脑袋阈值这东西最忌讳拍脑袋。我的做法是先算一批已知“好数据”的指标分布取分位数作为初筛线再在验证集上微调。比如动作平滑度我取好数据分布的 95 分位作为上限超过的进人工复核队列状态覆盖度则用聚类看有没有明显的空洞区域。这里有个经验不要一次性把阈值卡死。数据筛选是个迭代过程第一轮筛完训一版看真机失败案例集中在哪再回头调整筛选规则。我一般会保留一个“灰名单”就是那些指标处于边界的数据先不删等模型表现稳定后再决定去留。4. 版本化让每一次训练都能说清楚用了什么数据4.1 为什么 Git 管不了机器人数据代码可以用 Git但机器人数据不行。原因很直接数据太大二进制文件多而且数据的“版本”不只是文件内容变了还包括采集条件、清洗规则、筛选阈值这些元信息。你光记录“这次用了 v3 数据集”没用得能回答v3 相比 v2 删了哪些 episode、为什么删、用的什么筛选脚本、当时的阈值是多少。我现在的做法是给每个数据集版本建一个清单文件记录内容哈希、episode 列表、清洗脚本的 commit、筛选参数、采集日期和设备信息。这个清单本身很小可以进 Git数据本体则放在对象存储或本地磁盘用哈希关联。这样任何时候都能复现“当时到底喂了什么给模型”。4.2 用 LanceDB 做版本比对与增量更新LanceDB 除了检索还有个很适合版本化的特性它支持对表做版本管理每次写入会生成新的版本号可以按版本查询历史数据。这意味着我可以把每个 episode 的元信息和 embedding 写进 LanceDB每次清洗产生新版本时直接对比两个版本之间哪些 episode 被删了、哪些被改了。# 写入新版本 table.add(new_records) # 查看版本历史 print(table.list_versions()) # 按历史版本查询 old table.checkout(version1)这套机制配合前面说的清单文件基本能做到“数据可追溯”。我实际用下来最大的好处不是技术上的而是心理上的——当模型表现波动时我能快速定位是不是数据版本变了而不是盲目调参。4.3 版本命名与分支策略版本命名我建议用“语义化 日期”的组合比如v1.2.0-20240612主版本号在数据结构变化时递增次版本号在筛选规则变化时递增修订号在修 bug 时递增。分支策略上我一般维护两条线main是经过验证的稳定数据集exp是实验性的任何新采集或新清洗规则先在exp上跑验证有效再合并到main。这套策略听起来像软件工程但用在数据上一样成立。区别在于数据的“合并”不是代码合并而是把 exp 里验证过的 episode 追加到 main 的清单里同时更新哈希。5. 实操从采集到训练的一条完整链路5.1 采集端的硬性约束采集是整个链路的地基地基歪了后面全白搭。我在采集脚本里写死了几条硬约束固定 fps、固定分辨率、固定相机位置、固定任务描述模板、每条 episode 强制记录起止时间戳。SO-ARM101 这类低成本臂关节回读噪声不小所以我会在采集时同时记录原始读数和滤波后的值方便后面判断是采集噪声还是真实动作。采集时还有个细节每条 episode 开头留 0.5 秒静止帧。这半秒看起来浪费但它给了策略一个稳定的初始状态参考训练时对齐会容易很多。我对比过留和不留的效果留静止帧的版本在真机启动阶段抖动明显更小。5.2 清洗脚本的模块化设计清洗脚本我拆成四个独立模块格式校验、指标计算、相似度去重、版本写入。每个模块单独可跑输出中间结果方便排查。格式校验负责字段一致性指标计算负责算前面说的那几个分数相似度去重调 LanceDB版本写入负责更新清单和数据库。def clean_pipeline(raw_dir, out_dir, thresholds): episodes load_episodes(raw_dir) episodes validate_format(episodes) episodes compute_metrics(episodes) episodes dedup_by_embedding(episodes, thresholds) write_version(episodes, out_dir) return episodes模块化的好处是当某一批数据出问题时我能快速定位是哪个环节的锅。比如有一次真机成功率骤降我跑了一遍清洗脚本发现是相似度去重那步把一批关键的长尾动作误删了调整阈值后恢复。5.3 训练前的数据体检清单正式训练前我一定会跑一遍体检清单确认几件事episode 数量、总帧数、状态覆盖范围、动作分布、任务描述分布、图像亮度均值。这些指标不用很精确但必须看一眼因为很多问题在训练 loss 上是看不出来的。比如图像亮度均值如果某批明显偏暗说明采集时光照变了模型很可能过拟合到光照条件上。体检项正常范围异常时的处理episode 数量按任务复杂度定过少则补采状态覆盖覆盖任务所需区间有空洞则定向补采动作分布近似对称无长尾长尾过多则复核图像亮度批次间差异小差异大则分批次训任务描述枚举值有限措辞混乱则重映射这张清单我贴在工位上每次训练前扫一眼能挡掉大部分低级错误。6. 常见问题与排查技巧实录6.1 训练 loss 正常但真机失败这是最经典的问题几乎每个做模仿学习的人都遇到过。loss 正常说明模型在训练分布上拟合得不错真机失败说明训练分布和真机分布有偏差。排查顺序我一般是先看真机失败案例的状态是否在训练数据覆盖范围内如果不在就是覆盖度问题需要补采如果在范围内但动作不对就是数据里有矛盾动作需要去重和清洗。还有一种情况是视觉偏差训练时的背景、光照和真机不一致。这种最隐蔽因为状态和动作都对就是图像分布变了。我的应对是采集时尽量覆盖多种光照和背景或者在训练时加图像增强。6.2 数据版本混乱导致结果不可复现这个问题通常出现在多人协作或者项目周期长的时候。解决办法就是前面说的清单文件加 LanceDB 版本管理强制每次训练记录数据版本哈希。我现在的要求是任何一次训练结果必须能通过版本哈希还原出当时用的完整数据集做不到就不算完成。6.3 相似度去重误删关键数据去重阈值定得太激进会把一些看起来相似但实际关键的长尾动作删掉。我的经验是去重前先按任务阶段分组只在同一阶段内做去重跨阶段的相似帧不要动。另外去重后一定要在验证集上对比效果不能只看数据量降了多少。注意去重不是越多越好保留一定的冗余对策略鲁棒性其实有帮助关键是冗余不能是矛盾的冗余。6.4 采集设备漂移带来的隐性偏差机械臂用久了会有机械漂移相机支架也可能被碰歪。这种漂移是渐进的单看一天的数据看不出来但跨周对比就会发现状态分布整体偏移。我的做法是每周固定做一次标定把标定参数也纳入版本清单这样即使漂移了也能追溯。7. 我在这套流程里踩过的几个真实坑第一个坑是过度依赖自动筛选。有段时间我完全信任脚本算出来的指标结果把一批指标一般但实际很关键的探索性数据删了模型在遇到新物体时表现明显变差。后来我改成自动筛选加人工抽检抽检比例大概 10%专门看那些指标处于边界的数据。第二个坑是版本命名太随意。早期我用日期命名结果同一天跑了好几个版本自己都分不清哪个是哪个。后来改成语义化命名配合清单文件才彻底解决。第三个坑是忽略了采集时的元信息。有批数据我一直觉得质量不错但模型就是学不好查了很久才发现那批数据采集时相机曝光是自动的导致图像亮度波动很大。从那以后采集脚本里所有相机参数都锁死成手动。这套流程跑下来我最大的体会是数据质量这件事投入在采集和清洗上的时间远比投入在调参上的时间划算。模型结构、超参这些在数据质量过关的前提下调起来是线性的数据质量不过关调参就是在一个噪声很大的空间里瞎撞。LeRobot 提供了很好的框架基础LanceDB 补上了检索和版本化这块剩下的就是老老实实把工程细节做扎实。