前三篇讲了多模态数据和数仓的差异,以及 Lance、Daft 各解决了什么。这一篇把它们放回一个场景:一支 20 台双臂机器人的车队,在厨房、仓库、桌面等场景里做抓取和放置。新一版策略上线一周后,运维报告:抓取透明的玻璃杯时失败率明显偏高,好几次夹爪直接扑空。怎样从几十万条录像里找出该补的数据,生成下一版训练集?

数据闭环:七步走一圈

自动驾驶和具身智能有个共识:模型能力的上限由数据决定,而好数据不是一次收集完的,是在使用中一轮轮补出来的。部署出去的策略在真实世界里失败,失败被记录下来回流到数据侧,变成下一版训练数据——这就是数据闭环,也叫数据飞轮。

第 1 步
采集上传

机器人边干活边录下多路相机、关节状态和操作员指令,每条 episode 存成一个录包文件上传。

第 2 步
对齐与质检

把频率不同、时钟不同的消息按时间戳拼成「一个时刻一行」,标出丢帧、错位、操作失误的 episode。

第 3 步
入湖

明细写进 Lance:结构化字段和视频在同一张表里,可以随机读取、加列、打版本。

第 4 步
加工

解码画面,过滤坏帧,用模型给每一帧打标签、算向量。

第 5 步
挖掘与组装训练集

去重、抽样,再拿线上失败案例做向量检索,把相似的数据补进来,打成新版本。

第 6 步
训练、评测、部署

用新版本训练策略,评测通过后上线。

第 7 步
回流

线上失败和人工接管的片段回到第 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)。连线是配对结果,红色是被丢弃的帧。

多传感器时间对齐相机帧和关节状态的频率不同、时钟不同,怎样按时间戳配对。
  1. 相机 30 fps
  2. 关节 100 Hz
  3. 按时间戳配对

容忍误差 6 ms · 相机时钟偏了 12 ms

  1. 默认时钟未同步:「流水线看到的误差」只有几毫秒,「真实错位」却在 12 毫秒左右。机械臂末端每秒移动 0.5 米时,相当于标签偏了约 6 毫米,足以让精细抓取学歪。

  2. 把「时钟同步」切到已同步:两个误差变得一样,看到的就是真实的。

  3. 把「状态消息丢失」调到 30%:空洞变多,有的帧在容忍误差内找不到状态,变成红色。再把「容忍误差」调大,看它们怎样被救回来,以及代价:配到的状态离画面更远了。

  4. 把「配对方式」切到 as-of:同样的容忍误差下丢弃的帧更多。as-of 只能往前找,误差最大接近一个状态周期,容忍误差要相应放宽。

对齐之后,像写数据质量规则一样逐条检查 episode:

规则

检查什么

不通过通常意味着

帧数完整

实际帧数和「时长 × 帧率」是否相差不到 2%

相机掉线、录制中断

对齐率

能在容忍误差内配到状态的帧是否超过 99%

总线丢包严重、时钟没同步

物理约束

关节角是否超出机械限位,速度是否突变

传感器读数错误

动作一致

夹爪的指令和实际开合是否一致

执行器故障、遥操作延迟

画面质量

有没有黑屏、长时间遮挡、严重模糊

镜头被挡、曝光异常

任务结果

操作员是否中途放弃,任务最终是否成功

失败的示范(可以留作难例,但不能当正确示范学)

不通过的 episode 不删除,打上标记留在明细层:分析失败原因、挖掘难例时还用得上。规则越早跑,后面浪费的 GPU 越少。

入湖与加工

帧表和 episode 表写进 Lance。episode 表的三路视频放在 Blob 列里,只引用贴源层已有的 MP4,不用搬;训练用的热数据按训练帧率抽出来存成 JPEG,换随机读的速度(原理见 Lance)。接着用 Daft 的流水线解码画面、过滤坏帧、打标签、算向量,每一帧多出阶段、夹爪状态、清晰度和一个 512 维向量(见 Daft)。切到「汇总与标签」看加工后的一行:

一条 episode 的数据拖动时间点、切换数据层,看同一时刻在原始消息、明细、汇总和训练样本里各是什么样。
  1. 原始消息
  2. 对齐明细
  3. 汇总与标签
  4. 训练样本

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 个失败案例(红色菱形)恰好落在那里。

去重与难例挖掘去掉重复的示范,再拿线上失败案例做向量检索,把少见的难例补进训练集。
  1. 去重
  2. 均匀抽样
  3. 以失败案例检索近邻

模拟的机器人 episode · 每个失败案例取 8 个近邻

  1. 默认:先去重,再按 30% 均匀抽样得到基础训练集,难例只占 4% 左右;以 5 个失败案例为查询、每个取 8 个近邻补进来之后,难例占比提到 12% 以上。

  2. 把「每个失败案例取的近邻数」调到 0:只靠均匀抽样,少见的场景几乎抽不到——这正是线上会在这里失败的原因。

  3. 把「去重阈值」拖到「不去重」:挖到的条数变多了,但很多是同一条 episode 的近似副本,挖掘的名额被白白浪费。

  4. 把去重阈值拖到最大:不同的 episode 也被合并,数据池明显变稀,有价值的数据跟着丢了。

第三步:生成下一版训练集

  1. 把基础抽样和挖到的 episode 合并、去掉重复,得到这一版的 episode 名单。

  2. 把每条 episode 按训练帧率展开成样本,写进样本表。

  3. 提交成 Lance 表的一个新版本,打上标签 train-v2。上一版 train-v1 的标签仍然指向旧版本,两版随时可以对比。

  4. 记下这一版的来源:用了哪几个失败案例、哪一版模型算的向量、去重阈值是多少。几个月后有人问「模型为什么在玻璃杯上变好了」,你能答得上来——这和数仓里记录任务血缘是同一回事。

第四步:训练、上线、回流

训练程序按标签读取 train-v2,按样本表里的引用随机读出画面和关节数据。评测通过后上线,新的失败再回流到贴源层,开始下一轮。回头看整个闭环,每一步都能对上数仓里熟悉的东西:

闭环的一步

数据的形态

核心原理

数仓里的对应

采集上传

录包文件:多话题消息流

视频的关键帧与差量帧

ODS 贴源层

对齐与质检

帧表:一个时刻一行

以相机为主时钟的时间近邻关联

多流关联、数据质量

入湖

Lance 表:帧表 + episode 表

最小读取单位、Blob 列

DWD 明细层

加工

补上标签和向量

CPU / GPU 流水线、流式执行

ETL、DWS 汇总层

回填

新的标签列、向量列

加列只写新列、版本清单

加字段、回刷历史

挖掘与组装

样本表 + 版本标签

去重、近似最近邻检索

ADS 应用层、分区快照

回流

失败片段

——

问题复盘、数据回补

回头看

玻璃杯的问题,是靠「和失败相似的数据」补上的,而不是靠多采一倍数据。整条闭环里,分层、质量规则、血缘都是数仓的老经验;真正换掉的,是被数据形态逼出来的两块:为随机读和加列设计的存储,和为解码与推理设计的计算。

  1. 从数仓到多模态:哪些数据直觉要改:数据本身变了什么,数仓的做法在哪里不够

  2. Lance:为随机读、加列和版本而设计:存储这一侧的问题怎么解决

  3. Daft:让解码和推理流起来:计算这一侧的问题怎么解决

  4. 例子:机器人数据闭环走一圈:把前三篇放回一个真实场景


全文评论

0

正在加载评论…