把 UnifoLM-WLA-1.0 接进自建机器人之前,真正卡人的往往不是模型接口,而是三路数据的时间戳对不上:图像由相机驱动写帧时间,指令由上层任务下发的时刻决定,本体状态来自控制器上报,三者频率和来源都不一样。这时先别急着调模型参数,先把时间基准定死、把对齐规则写下来、再用一小段回放数据验证一遍。判断方向很简单:如果一帧动作找不到时间上说得通的观测,那么这条样本进训练就是在教模型学错的东西。
接 UnifoLM-WLA-1.0 这类观测到动作的模型前,先把时间对齐做掉比调参更省事。适用场景是多路采集、时钟来源不统一的机器人本体:记录每路时间戳来源与实测间隔中位值,选一条统一基准并写死匹配方式和容差,再用一小段回放数据抽查每一帧动作能否找到对应观测。验证方式是看对齐脚本输出的超差清单与无匹配帧计数。容差应从动作帧与最近观测帧间隔的分布里来,对不上的片段宁可丢弃并标记,不要静默插值补帧。
记录三路数据各自的时间戳来源与采样频率
先弄清差异从哪来,再决定以谁为准。常见情况是:图像带有设备侧时间戳,指令是任务层调用时刻,本体状态是控制器上报时刻,三者可能分别落在采集主机时钟、相机内部时钟、控制器时钟上。不要先猜,先把每路的事实填进下面这张表,中位值必须是从日志里算出来的,不是估的。
| 数据路 | 时钟来源 | 标称频率 | 实测间隔中位值 | 是否单调 |
|---|---|---|---|---|
| 图像 | 相机驱动写帧时刻 / 设备硬件时间戳 | 相机配置帧率(以设备配置为准) | 待填,由日志统计 | 待填 |
| 指令 | 上层任务下发时刻 | 事件驱动,无固定频率 | 待填,只统计相邻指令间隔 | 待填 |
| 本体状态 | 控制器上报时间戳 / 采集主机接收时刻 | 控制器上报周期 | 待填,由日志统计 | 待填 |
| 动作 | 与本体状态同源或同一下发时刻 | 控制周期 | 待填,由日志统计 | 待填 |
统计中位值的做法很轻,把三路时间戳导出成两列 CSV(channel, timestamp_s)之后跑一次即可。注意:只有落在同一时钟下的时间戳才能放在一起比,跨时钟先别算间隔。
python3 - <<'PY'
import csv, statistics
rows = {}
for r in csv.DictReader(open('ts.csv')):
rows.setdefault(r['channel'], []).append(float(r['timestamp_s']))
for ch, ts in rows.items():
ts.sort()
d = [b - a for a, b in zip(ts, ts[1:])]
print(ch, 'n=%d' % len(ts), 'median_dt=%.6f' % statistics.median(d))
PY
顺带看一眼有没有时间戳回退或跳变,回退通常意味着存在多时钟混用或缓冲重排,这种情况下后面的容差怎么设都不稳。如果相机有硬件时间戳而本体状态没有,需要先确认两者的固定偏移大概是多少——用一个能同时被两边看到的触发信号双读一次,看偏移是不是稳定的常数,不稳就先别做跨时钟对齐。
选一条统一时间基准并写出对齐规则
基准的选择要服务于动作语义。通常建议以动作帧时刻为基准,把图像、指令、本体状态对齐到动作帧上,因为控制周期是这条数据链里语义最明确的一路。若动作时刻本身没有独立时间戳,可以先用本体状态的上报时刻代替,但要在规则里写明这一点,后续换硬件时重新确认。
匹配方式按通道分别定,不要一套打天下:
- 图像:用最近邻(零阶保持)。图像插值出来的是不存在的画面,对训练没有意义。若最近一帧图像同时有更早和更晚两个候选,取更早的那帧更保守。
- 指令:用最近邻,并且要求指令下发时刻不晚于动作帧时刻。用未来才下发的指令去解释当前动作,是典型的标签泄漏。
- 本体状态:若其上报频率明显高于动作频率,可以线性插值到动作帧时刻;频率接近时仍用最近邻。插值只对连续量做,离散量(比如夹爪开合状态位)不要插值。
容差不能拍脑袋给。可行的顺序是:先按最近邻把整段数据匹配一遍,得到每帧动作与其最近观测的 |Δt| 分布,再取一个分位点作为容差阈值,具体取多少取决于这批数据的实际分布和动作的时效要求,需要结合环境确认。算出来的值写进对齐规则文件,别留在脑子里。
align:
base_channel: action
match:
image: {method: nearest, max_dt_s: 实测分位点}
instruction: {method: nearest_prev_only, max_dt_s: 实测分位点}
proprio: {method: linear_or_nearest, max_dt_s: 实测分位点}
on_violation:
train: drop
eval: keep_and_flag
超差时丢弃还是保留,取决于用途:训练数据一般直接丢弃该动作帧,并把它写进 rejected 清单,避免模型学到错配的观测;若这类帧特别宝贵,可以保留但打上 is_stale 标记,训练和评测时按标记过滤。评测回放时不要丢弃,要让它暴露出来,否则你会以为对齐没问题。
用一小段回放数据验证对齐结果
不必跑全量数据,取一段能覆盖一次完整动作的原始数据即可,短一点没关系。回放脚本按上面那份配置逐动作帧找观测,输出统计而不是只打印一句「通过」。
python3 align_check.py `--data` ./session_001 `--config` align.yaml `--out` ./align_report
输出检查项至少包含:动作帧总数、成功匹配数、无匹配数;每路 |Δt| 的最小值、中位值、最大值;时间戳非单调的帧号;以及一份超差或无匹配的帧清单。清单里要能直接看到是哪一路缺了观测。
bad = []
for idx, t_a in enumerate(action_ts):
for ch, ts in obs_ts.items():
if not any(abs(t - t_a) <= tol[ch] for t in ts):
bad.append((idx, t_a, ch)) # 该动作帧在 ch 上无对应观测
脚本之外还要做一次目视抽查:把匹配上的图像帧和动作帧按时间顺序成对看几帧,确认画面里的状态变化和动作时刻说得通,比如夹爪开合的瞬间在画面里出现的时刻不至于差得很远。计数只说明「找得到」,抽查才说明「找得对」。
把对齐检查整理成可反复跑的清单
每批新数据都按同一套标准过一遍,避免这次用手感、下次凭印象。必查项与处置方式如下:
- 三路时间戳单调递增。出现回退,回退到「记录时间戳来源」这一步,查采集侧是否混用了时钟或存在缓冲重排。
- 每路实测间隔中位值与标称频率的偏差。偏差明显,回退到采集侧查丢帧和缓冲,先别动对齐参数。
- 各通道超差帧占比,阈值用本批数据统计出的分位点。数量异常偏多,回退到「选统一时间基准」,重新确认时钟关系和基准选择是否合理。
- 无对应观测的动作帧数。只要不为零就逐帧列出来人工看,不要只看汇总数字。
- 指令时刻晚于动作帧时刻的帧数。这一项应当为零,不为零说明匹配规则或指令时间戳取错了。
回退顺序建议固定成:先查采集侧时钟与频率,再回到基准选择,然后才是容差与匹配方式,最后才判定这批数据不可用、需要重采。把这份清单和 align.yaml 放在同一个目录里,新数据来了直接按顺序跑,哪一步没过就停在哪一步。