比赛已经提交,终于可以把这段时间散落在配置、日志和聊天记录里的东西整理一下。

这次我们以 北京大学武汉人工智能研究院 团队参与比赛,使用 LingBot-VLA 2.0 6B,在 RoboTwin 的 Aloha-AgileX clean 数据上做 post-training,也提交了两个 UR 真机操作 Demo(还有一个叠衣服的任务,但由于网络问题推理测试速度太慢没有提交)。整个过程涉及数据适配、归一化、训练增广、多 GPU 评测,以及最后的复现材料整理。

值得记下来的,除了训练参数,还有那些让流程真正跑通的细节:为什么不加载图像的统计脚本仍然很慢,为什么多相机增广要共享参数,以及评测中断后应该怎样统计结果。

数据准备

训练使用 RoboTwin 2.0 Aloha-AgileX clean 50 数据集:50 个任务,每任务 50 条示范,共 2500 条轨迹。转换后的元数据合计为 549787 帧。

处理流程如下:

RoboTwin 原始 HDF5 + 指令
    ↓ 官方 process_data_pi0.sh
中间数据
    ↓ convert_aloha_data_to_lerobot_robotwin.py
LeRobot 数据集
    ↓ clean 专用任务清单 + 归一化统计
LingBot-VLA 多任务训练

这里单独维护了 assets/training_data/robotwin_clean.txt。原仓库的数据清单同时包含 clean 和 randomized,直接沿用容易把训练数据范围弄错。本次只使用 clean 示范,randomized 留作评测。

转换脚本对目录结构有自己的约定。原始数据位于 assets 目录时,需要先适配到转换器期望的路径。另外,转换器会重新生成同名输出目录,重跑之前要确认目标位置,避免覆盖已有数据。

机器人映射也需要和数据一起固定下来。RoboTwin 的手臂关节、夹爪状态与三路相机,通过 robot config 接入模型的统一表示。配置里的 action_dim: 55 是每个时间步的统一动作表示维度;预测序列长度由 chunk / horizon 参数决定,本次使用 50。

归一化计算

数据准备后,先计算 clean 专用的归一化统计。第一次运行在初始化完 50 个数据集后,长时间停在首个 batch。

最初查到的是线程过量:8 个 worker 各自有 144 个线程。限制 OMP、MKL、OpenBLAS 等线程数,再把 worker 调到 2 个后,程序可以推进,但仍然大约需要 20 秒一个 batch。

继续往下查,问题出在图像解码。

虽然统计时设置了 load_image=False,跳过了显式视频加载,但 Parquet 里内嵌的相机图像仍然会在 Hugging Face Dataset 读取行和查询动作窗口时自动解码。统计只需要状态和动作,图像却仍在消耗时间。

最终在 base_dataset.py 中,对不加载图像的模式提前移除相机列。单数据集验证时,读取 48 个样本、每个动作窗口 50 步,耗时约 0.072 秒;同时抽查了索引 0、1、47 的状态和当前动作,与原始 Parquet 一致。这个数字只对应局部读取测试,不能当作全量统计耗时。

这次排查留下了一个很实用的经验:性能问题要追到实际访问的数据列。配置里关闭某项功能之后,还要确认底层读取路径是否真的停止了对应工作。

最终训练使用 clean 专用的 robotwin_clean.json,归一化方式为 bounds_99_woclip:

x_norm = 2 × (x - q01) / (q99 - q01 + 1e-6) - 1

它按每维的 1% 和 99% 分位数缩放,不做截断。复现时需要同时保留数据清单、robot config 和统计文件,才能让动作的归一化与反归一化对应起来。

clean 数据上的在线图像增广

这次主要的调优方向,是在 clean 示范上增加视觉扰动,尝试扩大模型训练时看到的外观变化,目的是想提高模型在random setting下的表现SR,因为在clean setting下想要提高就得不断过拟合,所以。

增广在训练读取时在线执行,不改写原始视频或 Parquet,状态、动作标签保持不变,也没有额外引入 randomized 示范。

扰动类型主要设置
颜色变化亮度、对比度、饱和度与色温扰动
图像噪声高斯噪声、椒盐噪声
模糊轻微高斯模糊或运动模糊
几何变化小范围裁剪、平移和缩放
遮挡小面积中性灰遮挡
压缩JPEG 压缩伪影

实现中需要特别处理的是多相机和时序一致性。同一个样本的顶部相机、左右腕部相机,以及当前帧、未来帧共享增强参数;深度教师和视频教师也使用同样增强后的图像。

这样做是为了减少额外的监督错位。如果当前图像被裁剪,未来图像或教师输入却保留原样,模型还需要适应人为引入的不一致。共享参数可以保持扰动方式一致,但并不代表二维图像增广具备严格的三维几何一致性。

其实做这部分内容之前也参考过openpi的一些实现,但我总觉得他们的实现偏保守,不够彻底,既然要做就得做得彻底一点,看一下到底会不会影响到模型学习的分布。

评测时关闭训练增广。具体实现位于 lingbotvla/data/vla_data/image_augmentation.py,默认强度与概率记录在项目的增广说明中。

clean 示范图像与在线扰动效果对比

图中展示原始图像及各类扰动,便于直观看到训练时增加了哪些外观变化。

双卡训练配置

训练在双卡B200上提交,主要参数如下:

参数最终提交对应设置
基础模型LingBot-VLA 2.0 6B
GPU 数2
Micro batch / 梯度累积32 / 4
Global batch256
优化器Muon
学习率1e-4,cosine 衰减至 5e-5
Warmup ratio0.03
LossL1_fm
训练预算 / 保存间隔24000 步 / 4000 步
提交 checkpointglobal_step_12000/hf_ckpt

有效 batch 的关系是:

32 × 2 GPU × 4 次梯度累积 = 256

因此,比较实验时只看训练步数不够,还要看 global batch、数据采样方式和学习率计划。

另一个容易混淆的参数是 L1_fm。在本项目实现中,它对 flow matching 的目标速度与预测速度计算 L1,目标为 noise - actions。它的 loss 数值不能直接解释为最终动作误差,也不适合与使用 MSE 的 fm 数值直接比较优劣。

模型沿用基础模型的 MoE、深度与未来视频对齐机制。本次工作主要集中在数据处理、训练配置和评测适配。

两阶段方案实际执行到了哪里

原计划先开启增广训练,再从该权重继续做关闭增广的原图微调,希望在视觉覆盖与动作精度之间找到更合适的平衡。

时间最终只够完成第一阶段的训练与提交准备。第二阶段没有执行,也没有形成“无增广、仅增广、两阶段”的完整消融。因此,本文记录的是这条调优思路及第一阶段实践,尚不能给出第二阶段收益或增广提升幅度的结论。

训练 loss 曲线

项目保留了多轮实验的 loss、学习率、梯度范数及辅助任务指标。这里放入两组 RoboTwin 历史实验,记录训练过程;它们不是最终 batch 256、12000 步提交权重的训练曲线,也不是严格控制变量的对照实验。

早期 RoboTwin 实验

早期 RoboTwin 实验的总 loss、动作 loss 与辅助对齐 loss

这组数据来自 training_plots_robotwin/parsed_metrics.csv,包含 step 1~48288。图中的灰线为原始值,蓝线为 100 点移动平均,红线为每 1000 步的阶段均值。

总 loss 和 VLA loss 在前期快速下降,随后下降趋缓,期间仍有单步波动。深度、未来深度和未来视频对齐项也呈下降趋势。最后一个记录点的总 loss 为 0.0209,VLA loss 为 0.0169;

辅助任务各自有不同的尺度和权重,不能把图中各项原始值直接相加解释为总 loss。曲线能够说明训练目标的拟合情况,任务完成效果仍需要另行评测。

batch 128 历史实验

batch 128 历史实验的训练 loss 曲线

这组数据来自 training_plots_robotwin_bs128/parsed_metrics.csv,包含 step 1~20000,最后一个记录点的总 loss 为 0.0130,VLA loss 为 0.0091。

batch 128 历史实验的学习率与梯度范数

学习率与梯度范数放在一起,有助于结合优化过程查看 loss 的变化。其实看两轮表现 虽然steps表现不一样,但通过loss的变化率可以很明显的发现大batch_size 多步数对比小batch_size 少步数的差异还是很明显的。

前期开环测试:checkpoint 与动作执行长度

在 RoboTwin 比赛训练期间,我们还用叠衣服任务检查过模型动作预测。这组实验对应 pku_g1 机器人映射和 70k、80k、90k、100k checkpoint,属于前期独立实验,不对应本次 RoboTwin 提交模型。

开环测试每次读取录制数据中的真实图像和状态,预测一段动作,再与录制动作比较;到下一个 chunk 起点时,仍然读取真实观测,不把预测动作反馈给机器人或仿真。它适合排查动作映射、夹爪时序、chunk 边界和预测误差。

测试设置

  • episodes 0~4,每条前 500 帧,均来自训练轨迹。
  • 训练 horizon 为 50,分别测试每次使用前 10 个动作和全部 50 个动作。
  • BF16、seed 42,实验记录中的实际推理使用 10 次去噪。
  • 每条轨迹在长度 10 时推理 50 次,在长度 50 时推理 10 次,覆盖相同的 500 帧。

这里的“执行长度”表示每次取多少个预测动作参与离线比较,并没有真实执行。项目通用评测脚本的 num_denoising_step 参数目前未接入 policy 调用链,不能通过修改该参数宣称改变了去噪次数。

70k~100k checkpoint 对比

Checkpoint使用长度Overall MAEOverall MSEArm MAE(rad)Gripper MAE
70000100.0239960.0030060.0237060.026026
80000100.0243300.0031830.0240060.026601
90000100.0232620.0030610.0228780.025943
100000100.0227560.0031370.0222450.026327
70000500.0582560.0143260.0593850.050355
80000500.0553620.0125690.0570300.043686
90000500.0561720.0137190.0572040.048942
100000500.0516280.0115680.0529380.042462

70k、80k、90k checkpoint 在使用长度 10 时的开环比较

增加训练步数后的误差变化并不单调。长度 10 下,80k 的 Overall MAE 略高于 70k;长度 50 下,90k 又略高于 80k。纳入 100k 后,它在两种长度下的 Overall MAE 分别最低,但长度 10 的 MSE 仍高于 70k,因此可见再继续训练已经没有必要。

使用长度 10 与 50 的区别

70k~90k checkpoint 在两种使用长度下的误差比较

100k checkpoint 的长度 50 与长度 10 对比

在 100k 这组样本中,长度从 50 缩短到 10 后,Overall MAE 从 0.051628 降为 0.022756,约降低 55.9%;Arm MAE 从 0.052938 rad 降为 0.022245 rad。

较短的使用长度意味着更频繁地用录制的真实观测重新预测,只采用预测序列前部的动作。这提示后续可以继续研究动作 chunk 的使用方式,但不能直接推导出真机成功率会提高相同幅度。频繁推理还会增加计算需求,闭环效果需要结合延迟和控制频率验证。

但真机部署其实远比模拟测试要复杂得多,观测变化并不是这种曲线可以表现出来的。

单条轨迹

100k checkpoint、episode 0、使用长度 10 的预测与录制动作

同一 checkpoint 和 episode、使用长度 50 的预测与录制动作

逐维动作曲线可以补充平均指标看不到的信息:误差主要出现在哪些关节、夹爪开合是否错位,以及 chunk 切换处是否存在跳变。例如,70k 到 90k 的长度 10 测试中,Overall MAE 虽然下降,左右夹爪按 0.5 阈值统计的不匹配帧数合计却从 89 增至 95。因此,动作误差与夹爪时序需要一起看。 真机测试

RoboTwin 闭环评测

训练和仿真使用两个独立环境:lingbotvla 负责模型推理,RoboTwin 负责仿真执行,通过 WebSocket 传递观测和动作。

RoboTwin:相机、状态、语言指令
    → WebSocket → LingBot 推理服务
    ← 长度 50 的动作 chunk ←
执行动作,获取后续观测,继续推理

本次评测使用 FP32 推理、seed 0、compile 开启,并关闭视频保存。clean 对应 demo_clean / seen,目标每任务 25 轮;randomized 对应 demo_randomized / unseen,目标每任务 20 轮。这是本次本地运行的设置,完整每任务 100 轮评测可以通过启动参数另行指定。

为了减少重复加载模型的开销,评测启动器在每张 GPU 上维护一个推理服务,由任务队列调度 50 个任务。另外加入了任务重试、逐轮进度保存和结果导出。评测中断后可以续跑;更换 checkpoint 或评测轮数时,则需要使用新的输出目录。

提交时的本地结果

Setting完成目标轮数的任务成功轮数 / 已评轮数已评轮次成功率
clean / seen47 / 50930 / 119178.09%
randomized / unseen1 / 5014 / 4035.00%

这里的分母是实际已评轮数。两组都没有完成全部任务,任务覆盖也不均衡,不能用这两个百分比代表完整 50 任务的最终表现,尤其 randomized 的覆盖还很少。

后续补充测评后的结果大概在:

Setting完成目标轮数的任务成功轮数 / 已评轮数已评轮次成功率
clean / seen50 / 501007 / 125080.56%
randomized / unseen15 / 50149 / 30049.67%

本机 B200 环境下,部分场景在加载过程中出现 ErrorCUDALost,最终没有跑完全量评测。现有记录能确认这一现象,尚不足以把根因确定为某一种硬件或软件兼容问题。

这些评测结果都还只是一阶段训练完成的checkpoint评测的结果,如果有时间训练完成二阶段,我认为表现可能会更好。

赛后还想继续做的事

回头看,数据读取、训练和仿真是三个需要分别验证的环节。归一化脚本推进缓慢,要检查数据访问;训练 loss 下降之后,仍要靠闭环执行判断任务效果;评测进程跑着,也要核对场景是否正常加载、任务是否完整覆盖。

前期叠衣服实验也有类似提醒:在五条训练轨迹上的开环测试中,执行长度 10 的动作误差低于长度 50,但那组结果依赖录制的真实观测,不能替代机器人闭环成功率。动作预测误差和真实执行效果,仍需要分别测量。

赛后优先想补的是完整 clean / randomized 评测,以及相同预算下的无增广对照。之后再验证原图微调的第二阶段,按任务查看抓取、放置、精细操作等不同失败类型。真机侧则继续探索异步推理、RTC 和失败重试。

参考资料