前三篇讲了多模态数据和数仓的差异,以及 Lance、Daft 各解决了什么。这一篇把它们放回一个场景:一支 20 台双臂机器人的车队,在厨房、仓库、桌面等场景里做抓取和放置。新一版策略上线一周后,运维报告:抓取透明的玻璃杯时失败率明显偏高,好几次夹爪直接扑空。怎样从几十万条录像里找出该补的数据,生成下一版训练集?
数据闭环:七步走一圈
自动驾驶和具身智能有个共识:模型能力的上限由数据决定,而好数据不是一次收集完的,是在使用中一轮轮补出来的。部署出去的策略在真实世界里失败,失败被记录下来回流到数据侧,变成下一版训练数据——这就是数据闭环,也叫数据飞轮。
机器人边干活边录下多路相机、关节状态和操作员指令,每条 episode 存成一个录包文件上传。
把频率不同、时钟不同的消息按时间戳拼成「一个时刻一行」,标出丢帧、错位、操作失误的 episode。
明细写进 Lance:结构化字段和视频在同一张表里,可以随机读取、加列、打版本。
解码画面,过滤坏帧,用模型给每一帧打标签、算向量。
去重、抽样,再拿线上失败案例做向量检索,把相似的数据补进来,打成新版本。
用新版本训练策略,评测通过后上线。
线上失败和人工接管的片段回到第 1 步,开始下一轮。
每一步的输入输出,和数仓里的对应:
步骤 | 输入 | 输出 | 数仓里的对应 |
|---|---|---|---|
采集上传 | 传感器消息 | 每条 episode 一个录包文件 | 日志采集、ODS 落地 |
对齐与质检 | 录包文件 | 帧表:一个相机时刻一行 | 多流关联、DWD 清洗 |
入湖 | 帧表、视频 | Lance 表,带版本 | 建表、分区 |
加工 | 帧表 | 补上标签和向量的帧表、episode 汇总表 | ETL、DWS 汇总 |
挖掘与组装 | 汇总表 + 失败案例 | 训练样本索引,打上版本标签 | ADS 应用层 |
训练与部署 | 训练样本 | 模型 | 下游应用 |
回流 | 线上失败片段 | 新的录包文件 | 问题复盘、数据回补 |
采集与对齐:从消息流到明细表
机器人上的每个传感器都是一个「话题」(topic),按自己的频率发布消息,录包程序把它们按到达顺序写进一个文件(ROS 2 默认的格式叫 MCAP),一条 episode 一个文件,原样落在贴源层。训练要的却是「同一时刻」的画面、状态和动作,所以第一步是对齐成一张帧表:以头部相机为主时钟,每一帧一行,把这一刻的其他数据都挂上去。
列 | 类型 | 例子 | 说明 |
|---|---|---|---|
episode、帧号 | 字符串、整数 | ep-000123,第 225 帧 | 定位一行:episode + 时间,没有业务主键 |
时间 | 整数(纳秒) | 7.500 秒 | 这一帧的时间戳,来自头部相机 |
三路画面 | 引用 | head.mp4 第 225 帧 | 不存像素,只存「哪个视频的第几帧」 |
关节角 | 16 个 float32 | 右臂 -0.195 / -0.040 / 0.115 … 夹爪 0.00 | 离这一帧最近的那条关节状态 |
动作 | 16 个 float32 | 右臂 -0.195 / -0.040 / 0.115 … 夹爪 0.10 | 操作员给出的目标,比实际状态超前约 0.1 秒:夹爪还没合上,指令已经在合了 |
对齐误差 | float32 | 2.3 毫秒 | 配到的状态离这一帧多远,质检要用 |
一条 30 秒的 episode,原始消息有 1 万多条(三路相机共 2700 帧、关节状态 6000 条、操作员指令 1500 条),对齐后变成 900 行。
相机每秒 30 帧,关节每秒 200 条,两边的时间点天然对不齐,只能为每一帧找「最合适」的一条状态:
最近邻:取前后最近的一条,误差最小,适合离线训练数据。
只看之前(as-of):只取这一帧之前最近的一条。模型上线后拿不到「未来」的状态,想让训练条件和线上一致时就用它,否则会有信息泄漏。
容忍误差:找到的状态离画面太远(比如消息丢了留下空洞),宁可丢掉这一帧,也不要硬配。
相机和关节控制器往往各有各的时钟。没有同步时,相机的时间戳可能比真实拍摄时刻快十几毫秒,而且这个偏差在对齐流程里看不出来:按时间戳配对,误差看上去只有几毫秒,实际上每一帧都配到了错误的时刻。解决办法在采集端:用 PTP(精确时间协议)同步各设备的时钟,或者用硬件信号同时触发相机曝光。
下面是 400 毫秒内的两条时间轴:上面是相机帧,下面是关节状态(为了看清,频率设成了 100 Hz)。连线是配对结果,红色是被丢弃的帧。
- 相机 30 fps
- 关节 100 Hz
- 按时间戳配对
容忍误差 6 ms · 相机时钟偏了 12 ms
默认时钟未同步:「流水线看到的误差」只有几毫秒,「真实错位」却在 12 毫秒左右。机械臂末端每秒移动 0.5 米时,相当于标签偏了约 6 毫米,足以让精细抓取学歪。
把「时钟同步」切到已同步:两个误差变得一样,看到的就是真实的。
把「状态消息丢失」调到 30%:空洞变多,有的帧在容忍误差内找不到状态,变成红色。再把「容忍误差」调大,看它们怎样被救回来,以及代价:配到的状态离画面更远了。
把「配对方式」切到 as-of:同样的容忍误差下丢弃的帧更多。as-of 只能往前找,误差最大接近一个状态周期,容忍误差要相应放宽。
对齐之后,像写数据质量规则一样逐条检查 episode:
规则 | 检查什么 | 不通过通常意味着 |
|---|---|---|
帧数完整 | 实际帧数和「时长 × 帧率」是否相差不到 2% | 相机掉线、录制中断 |
对齐率 | 能在容忍误差内配到状态的帧是否超过 99% | 总线丢包严重、时钟没同步 |
物理约束 | 关节角是否超出机械限位,速度是否突变 | 传感器读数错误 |
动作一致 | 夹爪的指令和实际开合是否一致 | 执行器故障、遥操作延迟 |
画面质量 | 有没有黑屏、长时间遮挡、严重模糊 | 镜头被挡、曝光异常 |
任务结果 | 操作员是否中途放弃,任务最终是否成功 | 失败的示范(可以留作难例,但不能当正确示范学) |
不通过的 episode 不删除,打上标记留在明细层:分析失败原因、挖掘难例时还用得上。规则越早跑,后面浪费的 GPU 越少。
入湖与加工
帧表和 episode 表写进 Lance。episode 表的三路视频放在 Blob 列里,只引用贴源层已有的 MP4,不用搬;训练用的热数据按训练帧率抽出来存成 JPEG,换随机读的速度(原理见 Lance)。接着用 Daft 的流水线解码画面、过滤坏帧、打标签、算向量,每一帧多出阶段、夹爪状态、清晰度和一个 512 维向量(见 Daft)。切到「汇总与标签」看加工后的一行:
- 原始消息
- 对齐明细
- 汇总与标签
- 训练样本
episode「把杯子放进水槽」· 从第 12.0 秒的「汇总与标签」看起
挖掘难例,生成下一版训练集
回到玻璃杯的问题。人工接管的失败片段已经回流到贴源层;按标签筛(任务 = 抓杯子)太粗,让人去翻视频不现实。
先明确最终产物。训练集在数仓里相当于 ADS 层:面向一个具体用途、可以追溯来源。它由两张很小的表组成:
样本表的列 | 例子 | 说明 |
|---|---|---|
样本 ID | s-0000001 | |
episode、帧号 | ep-000123,第 360 帧 | 引用精选层的一帧;图像、关节角、动作训练时按引用随机读取 |
版本记录的列 | 例子 |
|---|---|
版本标签 | train-v2 |
包含 | 1.2 万条 episode,展开成 360 万个样本(30 帧每秒的帧按每秒 10 帧取样) |
来源 | 基础抽样 1.1 万条 + 由 5 个失败案例挖到的 900 条 |
依据 | 向量来自哪一版模型、去重阈值、挖掘参数、对应的 Lance 表版本 |
可以看到,组装训练集的关键不在搬数据,而在选数据:选哪些 episode,比例怎么配。下面两步就是在做选择。
第一步:去重
遥操作采集的数据重复度很高:同一个操作员、同一个场景,一下午能录几十条几乎一样的示范。重复数据浪费存储和训练时间,还会让模型过度拟合少数几种轨迹。去重和文本去重是一个思路,只是「像不像」的度量换成了图像的:
帧级:感知哈希。把画面缩成很小的灰度图,提取低频特征,得到一个 64 位的指纹;两个指纹只差几位,画面就几乎一样。
episode 级:把一条 episode 若干关键帧的向量取平均,作为这条 episode 的向量;两条 episode 的距离小于阈值,就只留一条。
阈值是关键:太小,重复删不干净;太大,会把「相似但不同」的有价值数据也合并掉。实践中要抽一些被合并的样本人工看看,校准阈值。
第二步:用失败案例做向量检索
把每个失败片段也算成同样的 episode 向量,拿它去数据池里找最近的邻居——和失败场景相似的数据,最可能帮模型补上短板。
几十万条里找最近的邻居,靠的是 Lance 表里的向量索引:先分桶、再压缩,用一点准确性换大量速度,原理见 Lance 的核心原理三。下面转一轮数据飞轮:
下面是 539 条模拟 episode 的向量投影到平面上的样子,按场景聚成几团。右下角虚线圈里的「透明 / 反光物体」很少见,而线上的 5 个失败案例(红色菱形)恰好落在那里。
- 去重
- 均匀抽样
- 以失败案例检索近邻
模拟的机器人 episode · 每个失败案例取 8 个近邻
默认:先去重,再按 30% 均匀抽样得到基础训练集,难例只占 4% 左右;以 5 个失败案例为查询、每个取 8 个近邻补进来之后,难例占比提到 12% 以上。
把「每个失败案例取的近邻数」调到 0:只靠均匀抽样,少见的场景几乎抽不到——这正是线上会在这里失败的原因。
把「去重阈值」拖到「不去重」:挖到的条数变多了,但很多是同一条 episode 的近似副本,挖掘的名额被白白浪费。
把去重阈值拖到最大:不同的 episode 也被合并,数据池明显变稀,有价值的数据跟着丢了。
第三步:生成下一版训练集
把基础抽样和挖到的 episode 合并、去掉重复,得到这一版的 episode 名单。
把每条 episode 按训练帧率展开成样本,写进样本表。
提交成 Lance 表的一个新版本,打上标签 train-v2。上一版 train-v1 的标签仍然指向旧版本,两版随时可以对比。
记下这一版的来源:用了哪几个失败案例、哪一版模型算的向量、去重阈值是多少。几个月后有人问「模型为什么在玻璃杯上变好了」,你能答得上来——这和数仓里记录任务血缘是同一回事。
第四步:训练、上线、回流
训练程序按标签读取 train-v2,按样本表里的引用随机读出画面和关节数据。评测通过后上线,新的失败再回流到贴源层,开始下一轮。回头看整个闭环,每一步都能对上数仓里熟悉的东西:
闭环的一步 | 数据的形态 | 核心原理 | 数仓里的对应 |
|---|---|---|---|
采集上传 | 录包文件:多话题消息流 | 视频的关键帧与差量帧 | ODS 贴源层 |
对齐与质检 | 帧表:一个时刻一行 | 以相机为主时钟的时间近邻关联 | 多流关联、数据质量 |
入湖 | Lance 表:帧表 + episode 表 | 最小读取单位、Blob 列 | DWD 明细层 |
加工 | 补上标签和向量 | CPU / GPU 流水线、流式执行 | ETL、DWS 汇总层 |
回填 | 新的标签列、向量列 | 加列只写新列、版本清单 | 加字段、回刷历史 |
挖掘与组装 | 样本表 + 版本标签 | 去重、近似最近邻检索 | ADS 应用层、分区快照 |
回流 | 失败片段 | —— | 问题复盘、数据回补 |
回头看
玻璃杯的问题,是靠「和失败相似的数据」补上的,而不是靠多采一倍数据。整条闭环里,分层、质量规则、血缘都是数仓的老经验;真正换掉的,是被数据形态逼出来的两块:为随机读和加列设计的存储,和为解码与推理设计的计算。
从数仓到多模态:哪些数据直觉要改:数据本身变了什么,数仓的做法在哪里不够
Lance:为随机读、加列和版本而设计:存储这一侧的问题怎么解决
Daft:让解码和推理流起来:计算这一侧的问题怎么解决
例子:机器人数据闭环走一圈:把前三篇放回一个真实场景
全文评论
0正在加载评论…