链路串起来失败,多数不是某一段写错了,而是段与段之间的接口对不上:采集写出的字段、清洗脚本假设的字段、打包时的 dtype 和维度顺序、训练时读的归一化统计,只要有一处不一致,日志往往只报一句 key 找不到,或者干脆静默丢掉样本,看起来每段都能跑。要判断这条链路是否真的跑通,先把五段各自的产物文件固定下来,再用一次性小批量从头串到尾,确认能出 loss 和 checkpoint,再去讨论数据质量和模型效果。
这套做法适用于自采数据、自己维护预处理脚本的机器人策略训练项目:采集、清洗、合成、统一表征打包、训练五段各留一个可校验产物,接口字段以 schema 文件为准。判断链路是否跑通的验证方式是——任意一次全链路运行结束后,只看产物文件就能指出断在哪一段;边界是它只证明链路连通,不证明数据质量与模型效果。
为五段链路各定义一个可观测产物
五段按顺序是:采集、清洗、合成或增广、统一表征打包、训练。每段都要有一个“跑完了就一定会出现”的文件,以及一条能单独执行的校验命令。约定好之后,“跑通了”就不再是感觉,而是一次 `ls` 加一次 `jq` 能回答的问题。
| 段 | 可观测产物 | 生成条件 | 校验命令示例 |
|---|---|---|---|
| 采集 | `raw/manifest.jsonl`,一行一个 episode,含文件清单 | 每个 episode 正常停止并落盘 | `ls -d raw/*/ | wc -l` 与 `wc -l raw/manifest.jsonl` 数量一致 |
| 清洗 | `clean/manifest.jsonl`(每行带 status 与 reason)+ `reports/clean_report.json` | 只读扫描完成且判定写入 | `jq -r '.status' clean/manifest.jsonl | sort | uniq -c` |
| 合成 | `synth/manifest.jsonl`,每条带 source_episode_id 与 transform | 合成任务结束且写入索引 | `jq -r 'select(.source_episode_id==null) | .episode_id' synth/manifest.jsonl` 应为空 |
| 表征打包 | `dataset/schema.json` + 分片文件 + `dataset/stats.json` | 打包脚本正常退出 | `jq '.keys | length' dataset/schema.json`,并核对 stats 里每个 key 都有 min/max/mean |
| 训练 | `runs/ | 至少完成一次反向传播并保存 | `head -n1 runs/ |
串联失败时的排查方向是从后往前看:最后一个成功产物在哪一段,问题就落在它的下一段。比如 `clean/manifest.jsonl` 有内容但 `dataset/` 没生成,先怀疑打包脚本读取的字段名与清洗输出不一致,而不是回去怀疑采集。
在采集端核对元数据字段是否齐全
缺字段最典型的后果不是报错,而是丢数据:清洗阶段按 fps 对齐时间轴,字段为空就整条跳过;打包阶段按 action_dim 建张量,字段缺失就写出一个维度不同的分片。所以元数据核对要放在采集端收工之前做,而不是等训练报 shape 不匹配。
| 字段 | 用途 | 缺失时的典型后果 |
|---|---|---|
| episode_id | 全链路主键 | 清洗与合成无法回溯来源 |
| task_instruction | 语言条件、任务分层 | 无法做按任务抽样与分桶 |
| robot_model / config_version | 区分机身与配置 | 不同构型数据被混进同一 schema |
| fps / timestamp_unit | 时间轴对齐 | 时间戳单位误判,动作与图像错位 |
| state_dim / action_dim / control_mode | 张量形状与语义 | 关节与末端位姿混用,训练静默学错 |
| cameras(名称、内外参版本) | 多相机对齐 | 拼接顺序错,画面与动作不对应 |
| calib_version | 标定可追溯 | 标定变更后新旧数据无法区分 |
| success / 结果标记 | 筛选与评估 | 只能靠人工回看,抽样成本高 |
| files(相对路径与校验和) | 完整性校验 | 半截文件被当成正常样本 |
# tools/check_meta.py 用法: python tools/check_meta.py raw/manifest.jsonl
import json, sys
REQUIRED = ['episode_id','task_instruction','robot_model','config_version',
'fps','timestamp_unit','state_dim','action_dim','control_mode',
'cameras','calib_version','success','files']
def check(path):
bad = []
ok = 0
for ln, line in enumerate(open(path, encoding='utf-8'), 1):
if not line.strip():
continue
rec = json.loads(line)
lack = [k for k in REQUIRED if rec.get(k) in (None, '', [], {})]
if lack:
bad.append({'line': ln, 'episode_id': rec.get('episode_id'), 'missing': lack})
else:
ok += 1
print(json.dumps({'ok': ok, 'bad': len(bad)}, ensure_ascii=False))
for b in bad:
print(json.dumps(b, ensure_ascii=False))
return 1 if bad else 0
sys.exit(check(sys.argv[1]))
补齐方式按优先级来:先从 episode 目录里的 sidecar 文件和采集日志回填;回填不了就整条标 drop 并写 reason;不建议给 fps、control_mode 这类字段填全局默认值,默认值会把错位藏起来,后面更难查。
清洗脚本先做只读扫描再动数据
清洗是最容易把原始数据改坏的一段。建议拆成两个入口:扫描只读、只产出报告;写入必须显式开关,且写到新目录,不原地覆盖。
# 1) 只读扫描,不改任何原始文件
python tools/clean_scan.py `--input` raw `--out` reports/clean_scan.json `--dry-run`
# 2) 记录扫描前的校验和,作为事后比对基线
sha256sum raw/*/data.npz > reports/raw_sha256.before
# 3) 人工看过报告,确认无误后再打开写入开关
python tools/clean_apply.py `--input` raw `--out` clean \
`--report` reports/clean_scan.json `--only-status` keep `--apply`
# 4) 写入后核对原始文件没被碰过
sha256sum -c reports/raw_sha256.before
扫描报告至少输出这些字段:episode_id、status(keep/drop/review)、reason_code、frames、duration_s、dt 抖动、state_dim、action_dim、NaN 计数、动作绝对值上限、多相机帧同步差、是否被规则命中。`--apply` 默认关闭,脚本在没有该参数时只打印“将写入 N 条”,不落盘。写入目录与输入目录不同,是保证可回溯的最低成本做法。
对合成输出做抽样人工核对
合成或增广阶段常出现机器指标正常、动作语义不合理的情况:数值全在范围内,但夹爪开合与物体是否在位对不上,或者时间轴上出现大量静止帧。这类问题只能靠人眼看。
抽样规则建议分层:按任务指令、按机器人配置、按时长分位各抽一遍,每个分层至少抽到一条,总量以覆盖完所有任务与所有相机配置为准,覆盖不到就说明分层还不够细。抽样时把帧序列和对应的动作曲线画在同一张时间轴上,比单纯看视频更容易发现错位。
| 核对字段 | 看什么 |
|---|---|
| 指令与动作是否对应 | 描述的任务和实际轨迹是否一致 |
| 动作维度顺序 | 与 schema 声明的顺序是否一致 |
| 量纲与范围 | 关节角、末端位姿、夹爪量是否在同一量纲 |
| 时间对齐 | 图像切换与动作变化是否同步 |
| 静止段与异常帧 | 是否存在大段无变化或明显跳变 |
| 结论 | 通过 / 复采 / 丢弃,附复核人 |
不合格样本的处理路径要固定:先在 manifest 打 flag 并记录原因,再回到原始 episode 重新导出;如果原始数据本身就缺帧或缺标定,就整条剔除并保留记录。不建议在合成阶段“修正”原始动作来凑合格,这会破坏可回溯性。
训练前用一次性小批量跑通并留日志
正式训练前先跑一次小批量,目的是让字段名、维度、归一化、checkpoint 保存这些接口问题在几分钟内暴露,而不是在长时间训练里才发现 loss 一直不动。
# configs/smoke.yaml(字段名按自己的实现替换)
dataset:
manifest: dataset/manifest.jsonl
schema: dataset/schema.json
stats: dataset/stats.json
max_episodes: 8
loader:
batch_size: 2
num_workers: 0
shuffle: false
train:
max_steps: 20
seed: 0
log_every: 1
save_dir: runs/smoke
# 跑通并留日志,日志与解析后的配置一起存档
python train.py `--config` configs/smoke.yaml 2>&1 | tee runs/smoke.log
关键日志行大致是这几类,字段名可以不同,但信息必须齐:数据集解析到的 episode 数与 schema key 数、第一个 batch 的 obs 与 action 形状和 dtype、stats 里动作范围与 NaN 计数、first step 的 loss 是否为有限值、checkpoint 落盘路径。log 里只要有这五行,链路是否连通就不必再靠猜。
失败时的分段回退顺序:训练报 key 错或形状错,回到表征打包看 schema 与分片;打包读不到样本,回到合成看 manifest 是否写入;合成缺源,回到清洗看 status 与 reason;清洗没有产出,回到采集看 missing 字段。每次回退前先把当前日志留在 runs 目录里,避免同一问题被反复从头跑。