具身数据合成老对不齐 / Xiaomi-Robotics-U0 该从哪一层接进来?

文章导读
先不要从模型侧动手。机械臂操作数据合成老对不齐,常见原因不是合成模型那一层理解错,而是原始数据的时间戳来源、通道定义、角度与弧度单位在三处以上不一致,接入层只是把已有的错位露了出来。判断顺序建议是:先在只读加载里确认字段能对上,再看单条 episode 内时间轴能否单调对齐,最后才决定改数据层、表征层还是训练层。Xiaomi-Robotics-U0 这一侧按统一合成模型对待,接入点选在哪层,取决于
📋 目录
  1. A 统计现有数据的模态数量、帧率与单条时长分布
  2. B 把三种接入点写成可对比的表格
  3. C 写只读加载骨架验证字段映射
  4. D 在小样本上串一次端到端并计时
  5. E 给接入失败建立错误分类与定位顺序
A A

先不要从模型侧动手。机械臂操作数据合成老对不齐,常见原因不是合成模型那一层理解错,而是原始数据的时间戳来源、通道定义、角度与弧度单位在三处以上不一致,接入层只是把已有的错位露了出来。判断顺序建议是:先在只读加载里确认字段能对上,再看单条 episode 内时间轴能否单调对齐,最后才决定改数据层、表征层还是训练层。Xiaomi-Robotics-U0 这一侧按统一合成模型对待,接入点选在哪层,取决于你数据的规范程度,而不是模型名气。

接入点不是选“最好”的那层,而是选改动最少、能回退的那层:落盘格式不规范就先补数据层字段;多模态单位、坐标系不一致就在表征层做一次显式转换;只有 batch 组装与对齐策略确实是瓶颈才动训练层。任何一层都先用只读脚本验证字段映射与时间轴单调性,通过后再串小样本端到端计时。失败时按类型分治,不要一边改格式一边调模型。以上判断需结合你自己的采集链路确认。

统计现有数据的模态数量、帧率与单条时长分布

把目录下每条 episode 的元数据读一遍,统计每个模态的通道数、时间戳来源、帧率与单条时长分布。规格不明就直接接,报错会集中在维度不符和时间轴断裂两类,事后很难分清是数据问题还是接入层问题。这一步只读不写,产出一张统计表再决定下一步。

维度取值方式用来判断什么
模态数量与名称扫描每条 episode 元数据里声明的模态列表是否需要多模态对齐,是否存在部分 episode 缺模态
通道数/维度读数组 shape 或元数据声明与目标表征的维度是否能直接对上
帧率或采样率由相邻时间戳差值的中位数反推多模态之间是否需要重采样,重采样放在表征层还是加载层
时间戳来源采集时钟、系统时间、还是序号推算不同来源混用时,对齐只能在表征层统一到同一时钟
单条时长与帧数末帧减首帧、帧计数决定小样本验证取哪几条,避免只拿超短片段试
时间轴是否单调检查相邻差值是否全为正非单调的数据先隔离,不进入首轮接入

脚本骨架如下,字段名要按你自己的元数据键替换,先跑通再谈统计结论。

import json, pathlib, statistics

root = pathlib.Path('data/robot_arm')
rows = []
for ep in sorted(p for p in root.iterdir() if p.is_dir()):
    meta_path = ep / 'meta.json'
    if not meta_path.exists():
        rows.append({'episode': ep.name, 'err': 'MISSING_META'})
        continue
    meta = json.loads(meta_path.read_text(encoding='utf-8'))
    for mod in meta.get('modalities', []):
        ts = mod.get('timestamps', [])
        dt = [b - a for a, b in zip(ts, ts[1:])]
        rows.append({
            'episode': ep.name,
            'modality': mod.get('name'),
            'channels': mod.get('channels'),
            'frames': len(ts),
            'monotonic': all(d > 0 for d in dt),
            'fps_est': round(1 / statistics.median(dt), 3) if dt else None,
            'duration_s': round(ts[-1] - ts[0], 3) if len(ts) > 1 else None,
        })
print(json.dumps(rows[:20], ensure_ascii=False, indent=2))

异常值处理口径建议统一成下面几条,避免边清洗边接入导致问题无法定位:时间戳非单调的 episode 标记隔离,原始文件不删;反推帧率为空或与声明值差一个量级的,按声明值归档并单独复核;帧数少于 2 或时长为 0 的片段不参与首轮验证;单条时长偏离同模态中位数数倍以上的单独成组,不混进小样本;缺模态的 episode 记录缺失项,不要用空数组补齐后再跑。

把三种接入点写成可对比的表格

三种接入点的差别不在概念,而在改哪些文件、补哪些字段、出问题能不能退回去。先按自己数据的实际缺陷对号入座,再动手。

接入点改什么不动什么验证什么涉及文件类型需补字段可回退程度
数据层(原始数据直接接入)落盘目录结构、元数据 schema、路径命名模型侧的表征转换逻辑、训练脚本字段可读、路径可枚举、时间戳单调采集配置 json/yaml、meta.json、目录命名规则episode_id、modality、timestamp、unit高,改回配置即可
表征层(先转统一表征再接入)转换脚本、中间格式、单位与坐标系声明原始落盘文件、训练侧 dataloader 接口转换前后帧数一致、单位显式声明、对齐索引可复现转换脚本 py、中间格式 npy/parquet、映射配置 yaml坐标系、单位、动作空间定义、对齐索引中,需保留原始数据与转换脚本版本
训练层(在数据集适配器里接)dataloader、collate、padding 与 mask 逻辑原始数据格式与表征定义batch 组装正确、长短样本混批不串位dataset/adapter 代码、训练配置batch 内 mask、样本权重、序列长度低,牵动训练代码,靠配置开关切换

判断依据可以按这个顺序走:如果统计阶段大量出现 MISSING_META、路径缺失,说明数据层还没规范,先在数据层补字段最省事;如果路径都对、字段也齐,但单位、坐标系、采样率在不同模态间不一致,就在表征层做一次转换,并把换算关系写进配置而不是硬编码在脚本里;只有当字段和单位都对齐、耗时仍集中在对齐与组 batch 上时,才值得动训练层。任何一层的改动都要能用一个开关退回上一版,否则后续排错会失去参照。

写只读加载骨架验证字段映射

这一步的目标是在不修改原始目录的前提下,确认字段名、路径、时间戳长度能对上。先写映射配置,再写只读加载脚本,脚本只读文件、只打日志,不发写操作。

具身数据合成老对不齐 / Xiaomi-Robotics-U0 该从哪一层接进来?
# field_map.yaml
version: 1
episode_root: data/robot_arm
required_fields: [episode_id, modality, timestamp, unit]
modalities:
  rgb:
    source: raw/front_rgb
    timestamp_from: meta.cameras.front.ts
    unit: s
  action:
    source: raw/joint_target.npy
    timestamp_from: meta.ctrl.ts
    unit: rad   # 原始若为角度,在表征层声明换算,不在数据层改
import json, pathlib, argparse, logging, yaml

logging.basicConfig(level=logging.INFO, format='%(levelname)s %(message)s')
log = logging.getLogger('dryrun')

def main(map_path, limit):
    fm = yaml.safe_load(pathlib.Path(map_path).read_text(encoding='utf-8'))
    root = pathlib.Path(fm['episode_root'])
    eps = sorted(p for p in root.iterdir() if p.is_dir())[:limit]
    for ep in eps:
        log.info('open episode=%s mode=readonly', ep.name)
        meta_file = ep / 'meta.json'
        if not meta_file.exists():
            log.error('missing episode=%s path=meta.json', ep.name)
            continue
        meta = json.loads(meta_file.read_text(encoding='utf-8'))
        for name, spec in fm['modalities'].items():
            src = ep / spec['source']
            if not src.exists():
                log.error('missing episode=%s modality=%s path=%s', ep.name, name, spec['source'])
                continue
            ts_key = spec['timestamp_from'].split('.')
            node = meta
            for k in ts_key:
                node = node.get(k, {}) if isinstance(node, dict) else {}
            n = len(node) if isinstance(node, list) else 0
            log.info('map episode=%s modality=%s unit=%s n=%s', ep.name, name, spec['unit'], n)

if __name__ == '__main__':
    ap = argparse.ArgumentParser()
    ap.add_argument('`--map`', default='field_map.yaml')
    ap.add_argument('`--limit`', type=int, default=5)
    a = ap.parse_args()
    main(a.map, a.limit)

预期日志与判读方式:出现 INFO map ... n=0 说明时间戳键路径写错了或该模态没有时间戳,先改映射配置;出现 ERROR missing ... path=raw/front_rgb 说明目录命名与配置不一致,属于数据层问题;如果各模态都能读到但日志里帧数差异明显,说明重采样或对齐要放到表征层,而不是在加载脚本里补零。只读脚本跑通再谈写入,能省掉大量“改了格式后原始数据对不上”的返工。

在小样本上串一次端到端并计时

小样本规模建议控制在 3 到 5 条,覆盖每种模态组合,并刻意包含最短、最长和中间时长各一条。样本再大就会把环境问题和数据问题混在一起,样本只有一条又验证不出长短样本混批时的对齐差异。

计时点按段拆开,每段用 time.perf_counter() 记录,写入一张表后再比较各层接入方式的差异。

import time
marks = {}
t0 = time.perf_counter()
marks['enumerate'] = time.perf_counter() - t0
# 依次在解析元数据、字段映射、构建对齐索引、组 batch、
# 调用一次合成或前向之后插入同样的打点
print(marks)
计时点记录值(自行填写)说明
枚举 episode 与文件路径层级过深会明显变慢,属数据层问题
解析元数据元数据过大或重复读取时耗时上升
字段映射与校验只读脚本的耗时基线,可作对照
构建对齐索引多模态重采样集中在这一段,属表征层
组 batch长短样本混批时变慢,属训练层
一次合成调用只用于确认链路能走通,不作为性能结论

耗时表只用于定位瓶颈在哪一段,不建议拿单次数字当结论。若耗时集中在前两项,先修数据层;集中在构建对齐索引,说明表征层的转换与索引策略要调整;集中在组 batch,再考虑训练层接入。

给接入失败建立错误分类与定位顺序

接入失败时最容易犯的错是把类型不匹配、缺帧、路径错误混在一起改。先在日志里给每类错误打固定前缀,再按类别顺序排查,回退动作也按类别定义。

类别日志关键字段典型现象定位命令回退步骤
字段或类型不匹配kind=SCHEMA_MISMATCH field= actual= expected=某个键读到空值,或列表被当成字典解析python -c "import json;print(json.load(open('meta.json')).keys())"回退 field_map.yaml 上一版本,不改原始数据
单位或坐标系不一致kind=UNIT_MISMATCH declared= expected=动作幅度异常或方向反了,但帧数正常grep -r UNIT_MISMATCH logs/ | head撤掉表征层的换算配置,确认原始单位后再开
缺帧或时间轴断裂kind=GAP modality= max_gap= frames=单条内时间戳跳变、模态间帧数不成比例grep -r GAP logs/ | head把该 episode 标记隔离,不参与本轮接入
路径或权限错误kind=PATH_NOT_FOUND path= episode=枚举到的文件与配置路径不一致find data/robot_arm -maxdepth 2 -name '*.npy' | head回退目录命名规则或映射配置,不移动原始文件
形状或维度不符kind=SHAPE_MISMATCH got= want=单模态能读,合批时维度对不上python -c "import numpy as np;print(np.load('raw/joint_target.npy').shape)"关闭训练层适配开关,退回表征层统一维度

定位顺序建议固定为:先看 PATH_NOT_FOUND 与 MISSING_META,排除取数问题;再看 SCHEMA_MISMATCH 与 UNIT_MISMATCH,确认字段映射与单位声明;最后看 GAP 与 SHAPE_MISMATCH,处理对齐与维度。每一类改动都保留一个开关或分支,回退时只回退当前类别,不要把数据层和训练层的改动一起撤回,否则下一次排错又要从头开始。