RoboTwin Platform · 技术手册

双臂操作的
数据工厂
与考卷

一个模拟器之上只做两件事的平台:批量造出带标注的双臂操作数据,并用一套统一的评分标准把所有人的策略放在同一张桌子上比较。这本书拆开它的每一层。

仓库
RoboTwin-Platform / RoboTwin
当前版本
RoboTwin 2.0
许可
MIT(代码)
规模
50 任务 · 5 本体 · 5 轴随机化
前言

为什么值得为它写一本书

机器人学习过去五年的困境,不是模型不够大,而是没有足够多、足够脏、足够多样的数据。真机采一条轨迹要几十秒到几分钟,还要人、要设备、要场地;而一个 VLA 模型动辄要几十万条。这个缺口只能靠仿真补,但仿真一直有两个老问题:场景太干净,任务太少。

RoboTwin 值得单独写一本书,因为它在同一份代码里把这两个问题当作工程问题解决了,而不是当作论文里的一个章节:

  • 任务不是手写的,是生成的。给定一段自然语言描述和一张桌子上的物体,多模态大模型写出一段可执行的机械臂操作代码,仿真器跑一遍验证、失败就把报错喂回去重写。论文报告这套闭环把代码生成成功率提高了 10.9%。
  • 数据不是干净的,是故意弄脏的。五个维度同时随机化:桌面杂物、光照、背景、桌面高度、语言指令。你在 demo_randomized 上训练,等于提前见过几万种"不标准"的现场。
  • 不只是一个数据集,还是一张考卷。榜单固定 50 个任务 × 每个任务 50 条示范训练、100 次试验评测,并区分"干净场景测试"与"随机化场景测试"。这个设定让不同论文的数字第一次可以横向比较。

但它的代码库并不好读。1796 行的 _base_task.py 里既有物理仿真、又有运动规划、又有数据落盘、又有评测视频编码;50 个任务文件每个只有几十行,却调用了十几个隐式约定;再加一个 2026 年才嵌入的 XPolicyLab 子模块,把"策略"这件事整个挪到了仓库外面。新手打开会不知道从哪一行开始看。

所以这本书的组织方式是按你打开仓库时会产生的疑问排序:第一章说清它到底在解决谁的什么问题;第二章把七个目录逐个拆开,告诉你每个文件的职责边界与那些没写在文档里的约定;第三章把它放回行业里,解释为什么这样设计、代价是什么;第四章从零搭起来,包括那四个必须打补丁的地方;第五章讲怎么把你的东西放进去——一个任务、一个本体、一次打榜。

关于本书的事实来源

书中所有目录名、函数名、配置字段、命令行参数,均来自仓库 main 分支的实际内容(2026 年 9 月快照,v2.0,MIT 许可,约 3.0k star、535 fork、122 个 open issue)。论文数字引自 arXiv:2506.18088(RoboTwin 2.0)与 arXiv:2504.13059(RoboTwin 1.0,CVPR 2025 Highlight)。凡属推测或经验判断,文中会明确标出。

Chapter 01

这是一个什么项目

A data generator and a benchmark, not a simulator

如果你以为它是"另一个 Isaac Gym",会一直找不到重点。它的核心资产不是物理引擎,而是那 50 个任务脚本、731 个带标注的物体、五根随机化轴,以及一套 100 次试验的评分规则。

1.1一句话定位

RoboTwin 是建立在 SAPIEN 物理引擎之上的双臂操作任务层。它自己不做刚体求解、不做渲染,这些交给 SAPIEN;它做的是三件 SAPIEN 不做的事:

任务层TASK
50 个任务脚本 成功判定 专家轨迹 步数上限
数据层DATA
731 物体 / 147 类 五轴随机化 10 万+ 轨迹 seen / unseen 指令
评测层EVAL
c2c / c2r 双设定 100 trials 策略服务化 排行榜
仿真引擎ENGINE
SAPIEN 3.0 mplib / cuRobo 规划 toppra 时间参数化 D435 相机模型

最下面一层是别人的,上面三层是 RoboTwin 的全部工作量所在。这也解释了为什么它的依赖里既有 sapien 也有 curobo、mplib、toppra——它是一套"会做动作的仿真场景",而不只是"能渲染的仿真场景"。

1.2三代版本:从生成孪生到数据工厂

理解这个项目最好的方式,是看它的三次自我修正。名字没变,但野心变了。

2024.09 ECCV Workshop 最佳论文 early 单张 2D 图 → 3D 孪生 LLM 拆解任务 规模:个位数任务 2025.04 CVPR 2025 Highlight 1.0 空间关系感知的代码生成 仿真 + 真机联合基准 +70% 单臂 / +40% 双臂 2025.06 → 2026 ICML 2026 2.0 731 物体库 + 五轴随机化 50 任务 / 5 本体 10 万+ 轨迹 + 排行榜 问题:数据从哪来 问题:代码谁来写 问题:怎么算公平 THREE ITERATIONS
图 1-1三代版本各回答了一个不同的问题。注意 1.0 的贡献是"让大模型写得出代码",2.0 的贡献是"让写出来的代码能造出足够脏的数据"。

第一代(early / 1.0)的野心是"生成式数字孪生"。给一张物体的单张 2D 图片,用 3D 生成基础模型造出可交互的仿真资产;再让大语言模型把"把面包放进篮子里"这类指令拆成空间约束与运动代码。这条路解决了"仿真里没有这个物体"的问题,也第一次让任务数量不再受人力限制。论文报告的策略是在真机上验证的:用 RoboTwin 数据预训练、少量真机数据微调,单臂任务成功率提升超过 70%,双臂超过 40%。

第二代(2.0)把重心从"生成物体"转向"生成多样性"。物体库固化成 731 个实例、147 个类别,每个带语义标注与操作相关标注(RoboTwin-OD);真正的创新在于承认一件事——模型在干净仿真里学得再好,一到真实世界就崩,因为真实世界是脏的。于是它把"脏"做成可配置的:桌面撒杂物、光照乱跳、背景贴图换掉、桌子高度偏 3 厘米、同一句话换 100 种说法。这一版的评估也更硬:VLA 模型只用 10 条真机示范加上合成数据,相对只用 10 条真机的基线提升 367%;完全不用真机、纯合成数据训练的零样本模型也有 228% 的提升。

2.5 代(2026 年的生态)则把"策略"这件事挪出了仓库。2026 年 8 月起,评测统一走 XPolicyLab 子模块:策略住在自己的 conda 环境里、通过 websocket 与仿真通信,RoboTwin 只负责出题和判分。这个改动的意义在文档里写得很直白——把"N 个策略 × M 个环境"的连接数从 O(N×M) 降到 O(N+M)。对使用者来说,好处是不必为了跑一个新模型去改仿真器的依赖;坏处是仓库的入口从 scripts/eval_policy.py 变成了一个 shell 脚本分发器。

1.3为什么开源

这个问题值得认真回答,因为它决定了你该怎么用它。一个学术团队把耗时两年、几十人参与的仿真平台以 MIT 许可全部放出来,还配套 10 万条轨迹和检查点,动机不止"学术影响力"。

①基准的可信度只能来自可复现

机器人领域的"成功率"是最容易被稀释的指标:换个相机内参、换个初始位姿分布、换个成功判定阈值,数字就能涨十几个点。要让别人相信"我的方法在双臂上更好",唯一办法是把题面、判分代码、训练数据全部公开,让别人用你的判分器复现。这也是榜单的提交政策要求代码 + 权重 + 技术报告三件齐备、基于预训练模型微调的还要公开预训练权重的原因——不公开就无法从同一个初始化重跑 SFT,数字就没有可比性。

②数据缺口大到一家填不满

50 个任务 × 50 条示范 = 2,500 条,就足够做一个标准的单任务微调基线;但要训练一个通用策略,需要的是十万量级。一家实验室采不完,也不该只有一种采法。开源让每个团队可以在同一套任务定义上补充自己关心的分布——比如你是做厨房场景的,就只采厨房那十几个任务的高清版本。

③生态位比论文引用更值钱

从 2026 年的更新记录能看到一条清晰的路径:RoboTwin 被 StarVLA(VLA 开发框架)、RLinf(强化学习基础设施)、IsaacLab-Arena 接纳为可选环境,被 RMBench(记忆依赖操作基准)当作底座,被 XPolicyLab 当作两个基准之一。一个基准一旦成为别人代码里的默认选项,它的维护成本就由整个社区分摊了。

一个必须说清的前提

MIT 许可覆盖的是这份代码。仓库里的 3D 资产(assets/ 下的 731 个物体、背景贴图、本体模型)通过 Hugging Face 单独分发,它们各自的来源与授权需要单独确认;用这些资产生成的数据集、以及在真实世界复现这些物体时的工业设计权,是另外的问题。学术使用通常没有障碍,商用前请逐项核对。

1.4场景价值:为什么值得为双臂单独造一座

单臂抓取已经有相当成熟的基准(RLBench、CALVIN、LIBERO 都是单臂或单臂序列)。为什么还要为双臂重新造一套?因为双臂不是"两个单臂加起来"。

维度单臂双臂RoboTwin 的应对
动作空间6—7 维 + 1 夹爪12—14 维 + 2 夹爪,且两臂动作相关_robot_info.json 显式声明每臂维度
核心难点抓不抓得住两只手之间的相对关系(交接、协同抬举、一手扶一手拧)任务里大量出现 handover_*、lift_pot、hanging_mug
失败模式滑脱、碰撞自碰撞、两臂抢空间、时序错位add_prohibit_area + 双臂规划器 + 步数上限
数据成本一人一机可采遥操作需要双臂主手或动捕,人力翻倍脚本化专家自动生成,无需人
评测难点成功率即可还要判"是否真的协同完成"check_success 里常同时判位姿与接触

这解释了任务列表的形状:50 个任务里,纯粹的"拿起来放到另一处"只占一部分,更多是交接(handover)、双手协同(lift_pot、hanging_mug)、需要一只手持物另一只手操作(open_laptop、open_microwave、press_stapler)。这些任务在单臂基准里根本没有对应物,而它们恰恰是家务和服务机器人最常见的形态。

另一个常被忽略的价值是它的任务定义本身就是一种"任务分解"的标注。每个任务的 play_once 都是一段人类可读的操作序列:抓锤子 → 抬起 7 厘米 → 对准方块的功能点放下。这段代码既是专家策略,也是一份"这个任务应该怎么做"的结构化说明。对研究任务规划、技能发现、甚至 VLM 的步骤推理,这都是一份免费的监督信号——而且是能被执行、能验证对错的那种。

1.5数字速览

项目数字含义与出处
任务数50每个任务一个 envs/*.py,一个指令模板,一条步数上限
本体(embodiment)5aloha-agilex、piper、franka-panda、ARX-X5、ur5-wsg
物体库731 / 147 类RoboTwin-OD,含语义与操作相关标注(论文)
公开轨迹100,000+Hugging Face 上的 RoboTwin 2.0 数据集,可按任务单独下载
随机化轴5杂物、光照、背景、桌面高度、语言(论文明确列出)
代码生成增益+10.9%MLLM + 仿真在环迭代,相对无迭代版本的成功率提升
10 条真机 + 合成+367%相对只用 10 条真机数据的 VLA 基线(论文)
纯合成零样本+228%完全不使用真机数据时的相对提升(论文)
社区规模3.0k star / 535 fork2026-09 快照;122 个 open issue,说明仍在高频迭代
安装耗时约 20 分钟官方 README 给出的估算,不含资产与数据下载

1.6它不是什么

四种最常见的误解,每一种都会导致你在错误的地方浪费时间。

不是通用仿真器

它不提供物理引擎、不提供场景编辑器、不提供 URDF 导入向导。想要"随便搭个场景",应该去 Isaac Sim / MuJoCo;RoboTwin 的世界是一张 1.2m × 0.7m 的桌子 + 两面墙 + 两台机械臂,几乎一切都在这个假设之内。任务里那些魔法数字(zlim=[0.76]、0.783)就是桌面高度加上物体半径算出来的。这不是缺陷,是取舍:固定舞台换来的是 50 个任务可以共享同一套专家代码骨架。

不是强化学习环境

虽然 Base_Task 继承自 gymnasium.Env,但它的主要用途是"跑脚本化专家采数据"和"跑策略做评测"。仓库里没有 reward 函数、没有 episode reset 的训练逻辑、没有并行采样器。想做 RL,得自己接(2026 年有 RLinf_support 分支正是为此)。

不是模型库

它不训练、不发布策略权重(榜单上的检查点由各提交方自己维护)。它提供的是数据 + 题面 + 判分器。策略在 XPolicyLab/policy/ 下,而且每个策略的依赖、检查点布局、训练脚本都由该策略自己的 README 管理。

不是跨平台工具

SAPIEN 需要 Linux + NVIDIA GPU(Vulkan 渲染)。Windows 和 macOS 不在支持范围;没有独显的机器连渲染都起不来。第四章会给"没有 GPU 也能做点什么"的路线,但那是绕路,不是正路。

1.7谁在用,用来做什么

从 README 推荐的五个探索方向可以看出它期望的使用者是谁。这五条几乎就是一份"双臂 VLA 研究的选题清单":

  1. 单任务微调能力——给定 50 条示范,你的模型能不能学会一个任务?这是最低门槛,也是所有榜单数字的基线。
  2. 视觉鲁棒性——在干净场景训练、在撒了杂物和奇怪光照的场景测试(c2r),掉多少点?这是 2.0 最想暴露的问题。
  3. 语言多样性鲁棒性——同一个任务换一种说法还能不能做?每个任务预置了 100 条 seen 与 100 条 unseen 指令,这是很少见的"语言泛化"评测资源。
  4. 多任务能力——一个模型同时学 50 个任务,能不能不互相干扰。
  5. 跨本体性能——在 aloha-agilex 上训的策略,换到 piper 或 franka 上还剩多少。

还有一类使用者容易被忽略:做数据引擎的人。他们不关心策略分数,关心的是"怎么在无人值守的情况下,一晚上生成两千条带失败案例的轨迹"。对这类使用者,第二章的 scripts/collect_data.py 与 env_cfg 才是全书重点,第三、四章可以跳读。

Chapter 02

模块详解

Reading the repository, directory by directory

仓库只有七个一级目录,但真正的复杂度集中在一个 1796 行的文件里。这一章按"你打开它会先点开哪个文件夹"的顺序走,并且在每个模块末尾给出它和其他模块的接口。

2.1仓库地图

RoboTwin/ ├── envs/ # 环境层:基类 + 50 个任务 + 机器人/相机/工具 │ ├── _base_task.py # 1796 行,Base_Task(gym.Env),全书最重要的一只文件 │ ├── _GLOBAL_CONFIGS.py # 路径常量 + 12 种抓取方向 + 4 种世界方向 │ ├── robot/ # robot.py / planner.py / ik.py │ ├── camera/ # camera.py,D435 与 L515 的相机模型 │ ├── utils/ # 13 个模块:造物体、随机摆放、pkl→hdf5、转视频… │ └── <task>.py # 50 个任务,每个 60—200 行 ├── env_cfg/ # 配置:任务设定 / 本体 / 相机 / 评测调度 │ ├── task_config/ # demo_clean.yml、demo_randomized.yml、步数上限… │ ├── robot/_robot_info.json # 动作维度契约 │ └── eval/all_tasks.yml # 50 个任务的评测清单与 GPU 分配 ├── description/ # 语言层:50 份指令模板 + 147 类物体描述 + LLM 生成脚本 ├── code_gen/ # 让多模态大模型写 play_once 的流水线 ├── scripts/ # 安装、下载、采集、评测四个入口 ├── data/ # 两个工具脚本(图像解码、卡住检测),不是数据 ├── assets/ # 资产下载器 + files/(图片素材) └── XPolicyLab/ # git submodule:69 个策略适配器 + 服务化评测

注意最后一行是子模块。git clone 时不加 --recurse-submodules 会得到一个空的 XPolicyLab/ 目录,而所有评测命令都依赖它——这是新手第一个坑,第四章会讲怎么补。

目录职责你什么时候会改它
envs/定义"世界怎么运转"加任务、改成功判定、改专家动作序列
env_cfg/定义"这一次跑什么设定"改采集条数、开随机化、换本体、调步数上限
description/定义"人怎么说话"加语言模板、扩 unseen 表达、改物体描述
code_gen/定义"代码怎么被自动写出来"换模型、改提示词、调重试阈值
scripts/定义"怎么一键跑"批量采集、评测调度、CI
XPolicyLab/定义"策略怎么接进来"加自己的策略适配器、改动作契约
data/工具脚本,不是数据集几乎不改;但必须知道 decode_image_bit 在这

2.2envs:环境层与生命周期

envs/_base_task.py 是整个项目的宪法。它声明了 class Base_Task(gym.Env),然后在 1796 行里定义了六十多个方法。不要试图顺读,按四组分类看会快得多。

① 生命周期:一个任务从创建到出数据

实例化任务类 class_decorator() 初始化环境 _init_task_env_() 搭场景 setup_scene() 放物体 load_actors() 演一次示范 play_once() 判是否成功 check_success() 逐帧落盘 _take_picture() 合成 HDF5 merge_pkl_to_hdf5_video() 读 yml 建表、墙、灯 随机摆放 取功能点 三条失败路径也会被记录 · 规划失败 → plan_success=False,后续 move 全部短路 · 物体不稳 → check_stable 抛 UnStableError,本回合丢弃 · 超出步数 → step_lim 截断,标记为失败样本(不删)
图 2-1Base_Task 的七个阶段。前四个由基类实现,后三个由具体任务继承或覆盖——这也是为什么一个任务文件通常只有 80 行。

这条链上有一个容易被忽略的设计:物体不是刚体摆件,而是"带功能点的对象"。每个 actor 通过 get_functional_point(idx, "pose") 取到语义位置——锤子的 0 号功能点是握把、1 号是敲击面;抽屉的 0 号是把手、1 号是内部空间。专家代码里不写坐标,只写"把锤子的 0 号点放到方块的 1 号点上"。这个抽象是 50 个任务能共用一套原语的根本原因,也是 description/ 能自动生成语言标注的原因(它知道 {A} 是哪个物体)。

② 动作原语:专家不直接输出关节角

写一个任务的体验,接近写伪代码。基类提供了一组语义级原语,每个原语返回一个 Action 对象(内含 ArmTag、类型、目标位姿或夹爪开合),然后统一交给 self.move() 执行。常用的十来个如下:

原语作用内部做了什么
grasp_actor(actor, arm_tag, pre_grasp_dis, grasp_dis)抓一个物体枚举抓取位姿 → 选最优 → 规划接近 → 合爪
place_actor(actor, target_pose, functional_point_id, pre_dis, dis)把物体放到目标位姿按功能点对齐 → 两段式下放 → 开爪
move_by_displacement(arm_tag, x/y/z, move_axis)沿某轴平移一段相对位移,常用于"抬起 7cm"
move_to_pose(arm_tag, pose)到指定位姿逆解 + 规划 + 时间参数化
open_gripper / close_gripper(arm_tag, pos)夹爪开合到指定值渐变插值,非瞬变
back_to_origin(arm_tag)回初始位任务收尾常用来腾出空间
delay(n)原地保持 n 帧让物理稳定,也制造"等待"样本
add_prohibit_area(actor, padding)标记禁放区规划时避让,防止两臂抢位
check_actors_contact(a, b)判两个物体是否接触成功判定里常与位姿判据并联
take_dense_action(control_seq)直接喂一段控制序列绕过规划,用于精细操作或回放

self.move() 的签名值得单独看一眼,它解释了"双臂为什么难"在工程上是什么样子:

# envs/_base_task.py
def move(self, actions_by_arm1, actions_by_arm2=None, save_freq=-1):
    # 两臂的 Action 列表按长度对齐,短的一侧补 None
    max_len = max(len(left_actions), len(right_actions))
    for left, right in zip(left_actions, right_actions):
        if left.action == "move" and right.action == "move":
            self.together_move_to_pose(...)   # 两臂同步走,共享一条时间轴

同步执行不是优化,是正确性要求:如果左右臂各自按自己的时长走,物理时间就对不齐,接触会算错,数据里的"协同"就是假的。delay() 的实现也印证这点——它是靠反复闭合夹爪(位置不变)来推进仿真步,而不是 sleep。

③ 观测:一个嵌套字典

get_obs() 返回的结构固定为 now_obs["observation"][<camera>]["rgb" | "depth" | ...],加上本体状态。相机名固定为四个:head_camera(第三人称)、left_camera / right_camera(腕部)、front_camera(可选第三视角)。写策略适配器时这四个名字要记牢,因为落盘时的 HDF5 组名会变成 cam_head / cam_left_wrist / cam_right_wrist / cam_third_view。

④ 记录:为什么先写 pkl 再合成

采集时每帧不是直接往 HDF5 里追加,而是写到 <save_dir>/_traj_data/episodeN.pkl 这类缓存文件,最后一次性 merge_pkl_to_hdf5_video(),同时用 ffmpeg 生成一个 mp4 预览。三个原因:HDF5 的 chunk 写入对随机访问不友好;编码图像很费 CPU,放到回合结束后批量做更快;中途崩溃时 pkl 还在,能续。clear_cache_freq 控制多久清一次缓存,也是为此。

2.3解剖一个任务文件

envs/beat_block_hammer.py 只有 87 行,是全书最好的教学样本。它完整演示了"一个任务要写哪四件事"。

# 1) 声明类,继承基类,绑定场景初始化
class beat_block_hammer(Base_Task):
    def setup_demo(self, **kwags):
        super()._init_task_env_(**kwags)

    # 2) 摆物体:位置可以是随机的,但要留出禁放区
    def load_actors(self):
        self.hammer = create_actor(
            scene=self,
            pose=sapien.Pose([0, -0.06, 0.783], [0, 0, 0.995, 0.105]),
            modelname="020_hammer", convex=True, model_id=0)

        block_pose = rand_pose(
            xlim=[-0.25, 0.25], ylim=[-0.05, 0.15], zlim=[0.76],
            qpos=[1, 0, 0, 0], rotate_rand=True, rotate_lim=[0, 0, 0.5])
        # 重采样直到方块不压在中轴线上(否则两臂都够不到)
        while abs(block_pose.p[0]) < 0.05 or np.sum(pow(block_pose.p[:2], 2)) < 0.001:
            block_pose = rand_pose(...)

        self.block = create_box(scene=self, pose=block_pose,
                                half_size=(0.025, 0.025, 0.025),
                                color=(1, 0, 0), name="box", is_static=True)
        self.hammer.set_mass(0.001)
        self.add_prohibit_area(self.hammer, padding=0.10)
        self.prohibited_area.append([...])   # 方块周围也不许放杂物

    # 3) 专家示范:只会被"人"(脚本)走一遍,不是策略
    def play_once(self):
        block_pose = self.block.get_functional_point(0, "pose").p
        arm_tag = ArmTag("left" if block_pose[0] < 0 else "right")  # 就近选臂

        self.move(self.grasp_actor(self.hammer, arm_tag=arm_tag,
                                   pre_grasp_dis=0.12, grasp_dis=0.01))
        self.move(self.move_by_displacement(arm_tag, z=0.07, move_axis="arm"))
        self.move(self.place_actor(self.hammer,
                                   target_pose=self.block.get_functional_point(1, "pose"),
                                   arm_tag=arm_tag, functional_point_id=0,
                                   pre_dis=0.06, dis=0, is_open=False))

        self.info["info"] = {"{A}": "020_hammer/base0", "{a}": str(arm_tag)}
        return self.info

    # 4) 成功判定:位姿 + 接触,两个条件同时满足
    def check_success(self):
        hammer_target_pose = self.hammer.get_functional_point(0, "pose").p
        block_pose = self.block.get_functional_point(1, "pose").p
        eps = np.array([0.02, 0.02])
        return np.all(abs(hammer_target_pose[:2] - block_pose[:2]) < eps) \
               and self.check_actors_contact(self.hammer.get_name(), self.block.get_name())

四段各自有一处值得学的地方:

  • 位置是随机的,但随机得有道理。那段 while 循环在拒绝"方块压在中轴线"的采样。这类"拒绝采样"在 50 个任务里反复出现,是任务脚本里最能体现领域知识的部分——它不是为了让分布好看,是为了让任务有解。
  • 禁放区是双臂特有的需求。单臂不需要 add_prohibit_area;双臂必须告诉规划器"这块地儿别撒杂物,否则另一只手会撞"。这是 2.0 能开 cluttered_table 而任务仍可解的前提。
  • 专家代码只写相对关系。没有一处硬编码机械臂坐标,只有"方块的功能点 1"和"锤子的功能点 0"。换本体、换桌面高度,这段代码不用改。
  • 成功判定必须可复现。eps = [0.02, 0.02] 是 2 厘米;这是判分器的一部分,改它等于改题。任何"我复现不出论文数字"的怀疑,都应该先核对这类阈值。
一个反直觉的事实

50 个任务里,没有一个用强化学习训练专家。全部是"脚本化策略 + 运动规划器"。原因很实际:RL 训练一个任务要几小时到几天,而且换一个随机种子就要重训;脚本化专家在你写完之后,能在几秒内生成一条轨迹,还能保证成功。这是数据合成与策略学习的分工——造数据的人用确定性的方法,学策略的人用统计的方法。

2.4robot:规划与控制

envs/robot/ 下只有三个文件,但撑起了"动作能被执行"这件事。

文件职责关键点
robot.py714 行,封装一台双臂机器人:加载 URDF、初始化关节、管理规划器、读写状态对外暴露 get_left_arm_jointState()、get_left_gripper_val()、get_left_ee_pose() 等成对接口
planner.py运动规划,双后端:mplib(默认)与 cuRobo(可选,需 CUDA)cuRobo 在 GPU 上做并行轨迹优化,速度快但编译麻烦;mplib 稳但慢
ik.py逆运动学求解,把末端位姿换成关节角有解析解时用解析,否则数值迭代

规划链路是:目标末端位姿 → IK → 关节空间路径(mplib/cuRobo,带碰撞检测)→ toppra 时间参数化 → 逐帧关节角。最后一步常被忽略:路径是几何的,没有时间;要让机械臂以合理速度走完且不超加速度限制,需要 toppra 把路径重参数化成时间序列。这也是为什么 _base_task.py 一开头就 ta.setup_logging("CRITICAL")——它在压住 toppra 的日志。

动作空间:两种模式,维度由契约决定

take_action(action, action_type) 支持两种模式,这是接策略时最容易搞错的地方:

模式每臂维度含义与使用场景
qpos6 或 7 + 1关节角 + 夹爪。训练用的默认模式,数据里的 state/left_arm_joint_states 就是它
ee7 + 1末端 6D 位姿 + 夹爪。需要模型输出笛卡尔空间动作时用,内部会先做 IK

维度不能凭感觉写。env_cfg/robot/_robot_info.json 是唯一的真相来源:

{
  "aloha_agilex": { "arm_dim": [6, 6], "ee_dim": [1, 1] },   # 左6 + 左夹爪1 + 右6 + 右夹爪1 = 14 维
  "franka":       { "arm_dim": [7],    "ee_dim": [1] },      # 单臂 7+1 = 8 维
  "piper":        { "arm_dim": [6],    "ee_dim": [1] },
  "x5":           { "arm_dim": [6],    "ee_dim": [1] },
  "dual_x5":      { "arm_dim": [6, 6], "ee_dim": [1, 1] },
  "dual_franka":  { "arm_dim": [7, 7], "ee_dim": [1, 1] }
}

顺带解释一个常见困惑:评测命令里要传 --env-cfg-type arx_x5,而任务配置里写的是 embodiment: [aloha-agilex]。两者不是一回事——前者是 XPolicyLab 的动作契约(策略输出几维、按什么顺序),后者是 仿真里的机器人(加载哪个 URDF)。README 明确说 arx_x5 与 RoboTwin 默认的 aloha-agilex 布局一致,因为两者都是 6+1 双臂共 14 维。传错了不会立刻报错,但动作会错位——这是"分数莫名其妙接近 0"的经典原因。

2.5camera 与 utils

envs/camera/camera.py 只做一件事:按配置创建一个符合 D435 / L515 内参的仿真相机,并输出 RGB、深度、内参、外参。分辨率和视场角写在 env_cfg/task_config/_camera_config.yml:

D435:        { fovy: 37, w: 320, h: 240 }   # 默认,车规级 RealSense 的近似
Large_D435:  { fovy: 37, w: 640, h: 480 }   # 想要高清就换这个
L515:        { fovy: 45, w: 320, h: 180 }   # 更广的视场,适合高桌面
Large_L515:  { fovy: 45, w: 640, h: 360 }

envs/utils/ 是 13 个模块的杂物间,但其中四个决定了数据质量:

  • create_actor.py / rand_create_actor.py:造物体。create_actor 走"给定位姿",rand_* 走"在范围内随机采样并检查不重叠"。后者是域随机化的执行者。
  • rand_create_cluttered_actor.py:往桌面上撒杂物,带半径避让。这是 cluttered_table 的实现。
  • pkl2hdf5.py:把整回合缓存合成 HDF5 + mp4。**数据格式最终由它决定**,比任何文档都权威。
  • transforms.py:位姿与矩阵转换。所有"坐标不对"的 bug 最后都会落到这个文件。

2.6env_cfg:四类配置

配置分四层,各管一件事。理解这四层的边界,比记住字段更重要。

任务设定task_config/*.yml
采几条开哪些随机化存哪些模态用哪个本体
本体_embodiment_config.yml
5 个本体的资产路径aloha-agilex / piper / franka-panda / ARX-X5 / ur5-wsg
评测eval/*.yml
50 个任务清单GPU 分配策略服务地址
契约robot/_robot_info.json
每臂动作维度夹爪维度

demo_clean 与 demo_randomized:两份设定,一个开关

仓库只给两份任务配置,差异全在 domain_randomization 段。这是全书最该逐字段读的一张表:

字段demo_cleandemo_randomized实际影响
episode_num5050每个任务采多少条——榜单标准就是 50
save_freq1515每秒存 15 帧,即视频里看到的数据频率
language_num100100每回合生成多少条语言变体
eval_instructionseenunseen用见过的说法还是没见过的说法——这两份配置连语言难度都不同
random_backgroundfalsetrue贴图换桌面与墙面(seen/unseen 两套贴图库)
clean_background_rate10.02以多大概率保持纯色。1 = 永远干净,0.02 = 98% 带纹理
cluttered_tablefalsetrue桌上撒 10 个随机杂物
random_lightfalsetrue随机光照方向与强度
crazy_random_light_rate00.022% 的概率来一次"极端"光照,专治过拟合
random_table_height00.03桌面高度上下浮动 3 厘米——真实桌子就是不平的
random_head_camera_dis00相机距离抖动,默认关

注意 eval_instruction 这一行:随机化不只是视觉的,还包括语言。这是 2.0 论文里"五轴"的第五轴,也是它比多数视觉随机化基准多做的一件事。同一个"敲方块"任务,clean 用 seen 模板,randomized 用 unseen 模板,模型必须同时扛住"桌上有杂物"和"人换了种说法"。

另外三个必看的配置

  • data_type:九个开关。默认只开 rgb、endpose、qpos;depth、pointcloud、mesh_segmentation、actor_segmentation 都要显式打开,否则文件里根本没有这些字段。开 depth 会把存储体积翻几倍。
  • _eval_step_limit.yml:每个任务允许的最大步数。多数是 400,blocks_ranking_* 是 1200(要排序多个方块),open_microwave 是 1500(长时序),hanging_mug 是 900(挂杯子需要精细对齐)。这个表是"任务难度"的另一种量化。
  • eval/all_tasks.yml:50 个任务逐行列出,配 gpu_ids: "0-7"、jobs_per_gpu、策略服务端口池 [18080, 18081, 18082, 18083]。想只评一个任务,就把它删到只剩一行。

2.7description:语言层

大多数仿真基准只有一个任务名当指令("beat block hammer")。RoboTwin 给每个任务准备了 100 条 seen + 100 条 unseen 的自然语言,这是它作为 VLA 训练数据源时最被低估的部分。

结构是一个 JSON,五个字段:

{
  "full_description": "there is a hammer and a block on the table, use the arm to <grab the hammer> and <beat the block>",
  "schema": "{A} notifies the hammer, {a} notifies the arm to grab the hammer",
  "preference": "num of words should not exceed 10",
  "seen": ["Pick {A} and strike the block.", "Lift {A} using {a} to hit the block.", ...],
  "unseen": [ ... ]
}

{A} 是物体占位符,{a} 是手臂占位符。运行时由 description/utils/generate_episode_instructions.py 替换成这一回合真实用到的物体名与手臂,并写进 data/<config>/instruction/episode_0000000.json。替换逻辑里有个细节:unseen 用的是另一套替换函数(replace_placeholders_unseen),它会刻意选择模板里没出现过的同义表达。

这套设计解决了一个真实问题:VLA 模型很容易把指令当成任务 ID 背下来。如果 50 个任务各只有一句固定指令,模型学到的可能是"看到这句话就执行这段动作",而不是"理解这句话"。100 条变体 + 100 条未见变体,把这个捷径堵掉了一半。

同一目录下还有三组 LLM 生成脚本(gen_episode_instructions.sh、gen_object_descriptions.sh、gen_task_instruction_templates.sh)与配套提示词模板。也就是说:语言数据也是生成的,不是人写的。要加一个新任务的语言,不必手写 200 句。

2.8code_gen:让大模型写任务

这是 RoboTwin 最有论文气质的部分,也是它区别于其他基准的核心机制。目标:给一段任务描述和桌上几个物体,自动产出一个能跑通的 play_once。

任务描述 "把锤子敲在方块上" 物体清单 enrich_actors() MLLM API 文档 + 示例 + 当前代码 deepseek / 可换 写文件 envs_gen/gpt_*.py 跑一次 成功率 / 报错 达标? 5 次里 ≥50% 成功 报错喂回去 las_error → 重新生成 失败则回到生成,最多 5 轮 SIMULATION-IN-THE-LOOP
图 2-2代码生成闭环。关键不是"让大模型写代码",而是"让仿真器当编译器"——语法正确不算过,跑得通才算。

实现上,code_gen/task_generation.py 的 generate_code() 有一个明确的成功判据:

# code_gen/task_generation.py(节选)
generate_num = 5            # 最多重写 5 轮
success_threshold = 0.5     # 5 次试验里成功率 ≥ 50% 才算过

# 第一次:给完整提示词(基础信息 + 任务描述 + 物体 + 可用 API + 函数示例)
# 之后每一轮:把上一轮的报错作为 las_error 塞回提示词
Prompt = (f"The code is unsuccessful, \n# Last Error Message: \n{las_error}\n\n"
          f"# Task description: \n{task_description}\n\n"
          f"# Actor List: \n{actor_list}\n\n")

三个设计细节值得抄走:

  1. 给的是 API 文档而不是代码库。AVAILABLE_ENV_FUNCTION 与 FUNCTION_EXAMPLE 是人工整理的原语清单和示范。这比把 _base_task.py 整个塞进上下文有效得多——1796 行会淹没重点,还会诱导模型去调用不该碰的内部方法。
  2. 判据是"多次试验的成功率",不是"跑通一次"。因为随机摆放会让同代码有时成功有时失败,只跑一次等于抽奖。50% 的阈值是在"能跑"和"稳定"之间取的折中。
  3. 报错原文回传。不翻译、不总结,直接把 traceback 给它。轨迹里的文件行号、变量名是模型定位问题最有效的线索。

论文报告的 +10.9% 就是这套闭环相对"一次性生成"的增益。也要诚实地说清代价:这条流水线需要调用外部模型 API(默认配置指向 deepseek),需要跑仿真验证,一个有难度的任务可能要几十次调用。它是"造新任务"的工具,不是"跑基准"的必经之路——用现成 50 个任务时完全不需要它。

2.9data:两种图像字节格式

data/ 目录只有两个脚本,但它藏着全书最容易踩、后果最隐蔽的一个坑。README 用了整整一段加一张表来讲它,值得复述。

RGB 图像在 HDF5 里不是存数组,而是存 编码后的 JPEG 字节串(省空间)。问题在于这些字节有两种写法:

格式怎么被写出来的标准解码器看到什么
legacy把 RGB 数组直接交给 cv2.imencode,而它按 BGR 解释输入红蓝互换——字节相对 JPEG 标准是反的,cv2.imdecode 又把它反过来
standard先转 BGR,再写一个 JPEG COM 段,内容为 XPL-RGB1颜色正确

致命之处在最后一句:legacy 数据永远不会被迁移,因为 JPEG 无法无损换通道,所以两种格式会长期共存,甚至出现在同一次训练里。而单个样本看不出自己是哪种格式。

正确做法只有一条:

from XPolicyLab.utils.process_data import decode_image_bit, encode_image_bit

rgb = decode_image_bit(image_bits)   # 两种格式都返回 RGB
# 千万不要在后面再加 cv2.cvtColor(..., COLOR_BGR2RGB)

README 说得很直白:"that swap is the bug"。很多"训练 loss 正常但真机完全不对"的案例,最后都追到这一行多余的转换上。自己写 dataloader 读这些 HDF5 时,必须调用它;自己采集时,必须用 encode_image_bit 而不是裸的 cv2.imencode,否则你写出的数据会缺那个标记。评测时策略服务端给的是已解码的 RGB,model.py 里不能再解一次。

另一个脚本 process_stuck.py 用于检测"卡住"的轨迹——物理仿真偶尔会出现物体穿透或数值爆炸,导致整条轨迹报废。这类清洗工具不在论文里,但在实际造数据时天天用。

2.10scripts:采集与评测入口

七个脚本,两条主线。

采集线

# 入口只有三行
bash collect_data.sh ${task_name} ${task_config} ${gpu_id}
# 例:在 0 号卡上,用随机化设定采 beat_block_hammer
bash collect_data.sh beat_block_hammer demo_randomized 0

collect_data.py 的 main() 做四件事,顺序很重要:

  1. 按名字动态导入任务类(class_decorator 里 importlib.import_module(f"envs.{task_name}"))。所以任务文件名必须等于类名,写错一个下划线就 "No such task"。
  2. 读任务配置,再读本体配置。本体字段允许两种写法:长度 1 表示两臂同型(dual_arm_embodied=True),长度 3 表示两臂异构且第三个元素是间距。
  3. 算保存路径:data/<task_config>/<task>/<embodiment>/,其中本体名会被规范化(aloha-agilex → aloha_agilex,ARX-X5 → arx_x5)。
  4. 两阶段采集:先"搜种子"——反复随机直到找到一个能让目标数量都成功的种子序列;再"重放种子"正式采。README 的原话是 "first search for a random seed for the target collection quantity, and then replay the seed to collect data"。

第四点是个聪明设计。专家脚本有随机性(物体位置、光照、杂物),某些采样会让任务无解。如果边采边试,会得到一堆失败轨迹混在数据里;先搜种子再重放,能保证"这一批都是成功的",同时种子可复现。

评测线

eval_policy.sh 其实是个分发器:看到 serve 就去起策略服务,看到 multitask 或带 --config 就去跑调度器,否则退回到单任务评测。--dry-run 可以只验证调度计划不真跑,调 GPU 分配时很有用。

bash scripts/eval_policy.sh multitask \
  --config env_cfg/eval/all_tasks.yml \
  --policy-name <policy_name> \
  --ckpt-name <checkpoint> \
  --env-cfg-type arx_x5 \
  --policy-conda-env <policy_env> \
  --eval-env-conda-env <robotwin_env> \
  --action-type <action_type>

注意 --policy-conda-env 与 --eval-env-conda-env 是两个环境。这不是冗余,是 XPolicyLab 的核心主张:策略的依赖(某个版本的 torch、某个自定义 CUDA 算子)与仿真的依赖(sapien、curobo)经常互斥,硬装在一个环境里必然打架。分居两个 conda 环境、用 websocket 通信,是这个矛盾的正解。

2.11XPolicyLab:策略层

作为 git submodule 嵌进来的 XPolicyLab/,是 2026 年最大的一次结构变动。它的定位用一句话说:把 N 个策略接到 M 个环境上,从 O(N×M) 条胶水代码降到 O(N+M)。

它统一了什么具体做法
依赖隔离每个策略在自己的 conda/uv 环境里,通过 websocket 与仿真通信,可同机可跨机
观测/动作契约统一定义"策略收到什么、输出什么",动作维度对照 _robot_info.json
生命周期安装 → 数据转换 → 训练 → 服务化 → 评测,五步同名同参数
数据格式统一 HDF5 布局(RoboTwin 与 RoboDojo 共用),并提供官方 LeRobot 转换器

截至 2026 年 10 月,它集成了 69 个策略,从经典模仿学习基线(ACT、DP、DP3、RDT)到 VLA(π0、π0.5、GR00T N1.7、OpenVLA-OFT、SmolVLA、X-VLA、MolmoAct2)到世界动作模型(DreamZero、GigaWorldPolicy 等)。这个名单变化很快。要跑某个具体模型,永远以 XPolicyLab/policy/<NAME>/README.md 为准——检查点布局、训练命令、已知问题都写在那里。

子模块会漂移

XPolicyLab 在自己的 main 上持续更新,而 RoboTwin 只钉住某个提交。这两者不同步时,最典型的症状是"策略加载了但动作全零"或"评测脚本参数不认识"。仓库给了 scripts/update_xpolicylab.sh:默认拉最新并刷新钉住的提交,加 --stage --install 会顺便暂存并重装可编辑包。升级前先看它的 CHANGELOG,别在生产评测机上随手跑。

2.12一次采集的完整时序

把上面所有模块串起来,一次 collect_data.sh 的实际时序如下。这张图值得贴在显示器边上——出问题时,先判断卡在哪一段。

shell config env planner disk export CUDA 读 yml import 任务类 搜种子 建目录 python 启动 本体 → 路径 SAPIEN 起场景 每回合 play_once 写 pkl 帧 随机化参数展开 撒杂物 / 换贴图 move() 逐段执行 mp4 预览 mplib 规划 + toppra 合成 HDF5 碰撞检测 / 时间参数 清 cache 失败路径:搜种子阶段若长时间无解 → 该任务配置本身有问题,不是随机性
图 2-3"搜种子"发生在建目录之前,意味着如果这一步卡住,磁盘上什么都不会产生——这也是排查"为什么没数据"的第一个断点。

2.13产物长什么样

采集与下载的数据都遵循一套 XPolicyLab 布局(data_type 默认值是 RoboDojo,与 RoboTwin 共用)。目录结构:

data/ ├── demo_clean/ # <task_config>:对应 env_cfg 里的设定名 │ ├── beat_block_hammer/ │ │ └── aloha_agilex/ # <embodiment>:本体名规范化后 │ │ ├── data/ │ │ │ ├── episode_0000000.hdf5 │ │ │ └── episode_0000001.hdf5 │ │ ├── video/ │ │ │ └── episode_0000000.mp4 │ │ └── _traj_data/ # 采集中间缓存,可删 │ └── place_bread_basket/ └── instruction/ └── episode_0000000.json # 语言:seen / unseen 各一批

单个 HDF5 内部的组结构(来自 pkl2hdf5.py 的 create_xpolicylab_hdf5):

episode_0000000.hdf5 ├── attr source_format = "RoboTwin" ├── attr source_path = "native_collection" ├── data_format_version = "v1.0" ├── instructions # JSON 字符串,含本回合所有语言变体 ├── additional_info/ │ └── frequency # 采集频率,默认 15 ├── state/ │ ├── left_arm_joint_states # (T, 6) │ ├── left_ee_joint_states # (T, 1) 夹爪 │ ├── right_arm_joint_states │ ├── right_ee_joint_states │ ├── left_ee_poses # (T, 8) 位置+四元数 │ └── right_ee_poses ├── action/ # 与 state 同构,但整体右移一帧(t+1) │ └── ... 同上六个 └── vision/ ├── cam_head/ # 第三人称 │ ├── colors # 编码后的 JPEG 字节串数组 │ ├── shape # 原图尺寸 │ ├── intrinsic_matrix │ └── extrinsics_matrix ├── cam_left_wrist/ └── cam_right_wrist/

三个要点:

  • state 与 action 是同一序列错位一帧。state = values[:-1]、action = values[1:]。这个约定隐含了"动作即下一帧状态"的假设,写 dataloader 时不要自己再 shift 一次。
  • 图像是字节串,不是数组。必须先 decode_image_bit,见 2.9 节。
  • 相机名被重映射了。仿真里叫 head_camera,落盘叫 cam_head;left_camera → cam_left_wrist。front_camera → cam_third_view(需在 data_type.third_view 打开)。
Chapter 03

行业位置、原理与趋势

Where it sits, why it works, and what is changing

这一章不解释代码,解释设计。为什么是脚本化专家而不是强化学习?为什么随机化要分五根轴而不是一根?为什么榜单要跑 100 次而不是 10 次?每个"为什么"背后都有一个可以被检验的理由。

3.1行业地图:三条数据路线

2026 年的具身智能数据,基本只有三条路。RoboTwin 站在第二条上,但它和第一条、第三条是互补关系,不是替代。

路线代表单位成本根本瓶颈
真机遥操作 DROID、Open X-Embodiment、AgiBot World、LeRobot 生态 高
人力 × 设备 × 场地
规模受人力线性限制;场景分布由"你能把机器人搬到哪里"决定,通常只有实验室
仿真合成 RoboTwin、RoboCasa、MimicGen、DexMimicGen、Genie Sim 低
一次投入,边际近零
sim2real 鸿沟;物体与场景的丰富度受资产库限制;接触物理(柔性、液体、摩擦)不可信
人类视频 EgoDex、Ego4D、各类第一视角数据集 中
采集便宜,标注贵
没有动作标签,需要重定向;本体差异(人手 vs 夹爪)无法消除

三条路的分工正在变得清晰:真机数据提供"真实分布的锚点",仿真数据提供"规模与多样性",视频数据提供"语义与长尾场景"。RoboTwin 2.0 的 367% 这个数字,本质上就是在说这件事——10 条真机数据做锚点,加上几万条合成数据做规模,效果远好于 10 条真机或者纯合成。它没有宣称"合成可以取代真机",而是证明了"合成能把少量真机的价值放大"。

在仿真合成这条支线内部,还可以再分两派:

  • 生成式派(RoboCasa、Genie Sim):用生成模型造场景、造物体、甚至造视频,追求"看起来像真的"。RoboTwin 1.0 属于这一派(单图生成 3D 孪生)。
  • 随机化派(RoboTwin 2.0、MimicGen):不追求单条数据逼真,追求分布覆盖——用大量"不完美但多样"的样本,让策略见过足够多的变化。这一派的赌注是:泛化来自覆盖,不来自保真。

RoboTwin 从 1.0 走到 2.0,正是从生成式派转向随机化派。这个转向值得记住,因为它解释了仓库里为什么有那么多"故意弄脏"的开关,却几乎没有"提升渲染真实度"的工作。

3.2双臂为什么难

把双臂操作写成一个优化问题,难点会自己浮现。一个双臂任务可以形式化为:

给定观测 ot(多相机 RGB + 本体状态)与语言指令 l,输出动作序列 at:t+H(左右各 6—7 维关节 + 各 1 维夹爪),使得在有限步数 T 内达成由 check_success 定义的终态条件,同时全过程不发生自碰撞、不超出关节限位、不丢失物体接触。

四个约束里,每一个都比单臂多一层:

① 维度与耦合

14 维动作空间不是"两个 7 维"的笛卡尔积。两臂共享工作空间,一臂的轨迹会改变另一臂的可行域。这就是为什么 move() 必须让两臂沿同一时间轴同步执行——分开规划再拼起来,会在时间上产生不可能的姿态组合。

② 接触丰富

"lift_pot"(抬锅)需要两只手同时施力且力方向大致相反;"hanging_mug"(挂杯子)需要一只手稳定、另一只手精确对位。这类任务的成功与否取决于接触状态,而接触在仿真里是最不可靠的部分。RoboTwin 的应对是把判据写成"位姿容差 + 接触检测"的合取(见 2.3 节的 check_success),而不是纯几何判据。

③ 长时序与误差累积

单臂任务通常是"抓—移—放"三步;双臂任务常是"抓—递—接—转—放"五到八步,且中间任何一步的 2 厘米偏差都会传导到终点。_eval_step_limit.yml 里 open_microwave 给到 1500 步、hanging_mug 给到 900 步,就是长时序的量化体现——它们不是"更难的动作",是"更多不能出错的动作"。

④ 数据获取成本翻倍

真机采双臂数据需要双臂主手或动捕系统,操作员要同时控制两只手,学习曲线陡。这是双臂数据集远少于单臂的根本原因,也是 RoboTwin 这类平台存在的最大理由:把"人的协调能力"换成"代码的确定性"。

3.3数据合成的原理

"仿真能造数据"这句话谁都会说。真正的问题是:怎么在没有人的情况下,让机械臂做出正确的动作?三条路——强化学习、视频重定向、脚本化专家——RoboTwin 选了第三条。

方法怎么产生动作单条成本为什么没被选
强化学习定义 reward,训练到收敛,再 rollout数小时—数天/任务换一个随机化设定就要重训;reward 设计本身是难题;成功率不稳
视频重定向从人类视频提取手部轨迹,映射到机器人分钟级需要大量视频;重定向误差大;双臂协同视频稀缺
脚本化专家人手写操作序列,运动规划器补全关节轨迹秒级被选中的方案

脚本化专家之所以可行,依赖两个前提,而这两个前提 RoboTwin 都刻意构造了:

前提一:任务可以被分解成"语义原语"

人类做"把锤子敲在方块上"这件事,不需要计算关节角。他只需要知道:抓住握把、抬起来、让锤头对准方块、落下。RoboTwin 把这四件事做成了四个原语(grasp_actor、move_by_displacement、place_actor、open_gripper),每个原语内部再调用 IK 与运动规划补全细节。于是"写一个任务"变成了"写四行伪代码"。

前提二:物体有"功能点"

这是整个设计里最关键的一个抽象。如果只有物体位姿,专家代码得写"锤头在握把坐标系下的偏移是 (0, -0.05, 0.12)"——换一个锤子模型就要重算。有了功能点,代码写的是"把 0 号点放到 1 号点上",语义稳定。这也是为什么 731 个物体、147 个类别能共用 50 套任务代码:抽象层级选对了,规模才能扩。

把两者合起来,数据合成的完整链条是:

LEVEL 1 · 语义 LEVEL 2 · 几何 LEVEL 3 · 时间 LEVEL 4 · 数据 操作序列 抓 A → 抬 7cm → 放到 B 的 1 号点 人写的,几十行 IK + 碰撞规划 位姿 → 关节路径 mplib / cuRobo 求解器填的,自动 时间参数化 路径 → 逐帧关节角 toppra 受加速度约束 落盘 HDF5 + mp4 15 Hz 失败反馈:规划失败 / 物体不稳 / 超步数 → 换个种子重来,或回到 Level 1 改写操作序列 这条回路同时是 code_gen 的判据来源,也是 2.0 论文里 +10.9% 的来源
图 3-1四级抽象。人在 Level 1 只写十几行,Level 2—4 全部由求解器完成。这个分工是"一晚上造两千条轨迹"的技术前提。

需要点明代价:这套方法只能覆盖"可被几何描述的操作"。倒水、揉面、插拔线缆这类依赖形变与柔性接触的任务,脚本写不出来,规划器也算不准。这也是 50 个任务里全是刚性物体抓放、开关、堆叠、交接的原因。不是不想做,是这套方法的天花板就在那里。

3.4域随机化:五根轴

域随机化的理论依据很朴素:训练分布与测试分布不一致时,性能按两者的差异程度衰减。既然无法枚举真实世界的所有变化,就让训练分布尽量"包住"它。RoboTwin 2.0 把这个想法拆成五根可以独立开关的轴。

FIVE AXES OF RANDOMIZATION 杂物 cluttered_table 10 件 随机物体 遮挡 + 干扰 光照 random_light 方向 / 强度 2% 极端光照 高光与阴影 背景 random_background 桌面 + 墙面贴图 seen / unseen 两库 纹理偏置 桌面高度 random_table_height ±3 cm + 相机距离 几何标定误差 语言 language_num 100 条变体 seen / unseen 表达差异 为什么是这五根,而不是别的 · 前四根对应真实部署中最常变的东西:现场更乱、灯不一样、桌布不一样、桌子高度不一样 · 第五根对应"人怎么说"——同一件事十个人十种说法,而模型最容易在这里过拟合 · 五根可以独立开关,意味着你可以做消融:单独开杂物 vs 单独开光照,看哪一根真正贡献了鲁棒性
图 3-2五根轴中,只有"语言"这一根是多数视觉鲁棒性基准没有的。它把评测从"看得清不清"扩展到"听得懂不懂"。

几个实现细节比概念更值得注意:

  • 背景贴图分 seen / unseen 两个库。create_table_and_wall 里 texture_type = "seen" if not self.eval_mode else "unseen"。训练用一套,评测换另一套——这是"不留后门"的做法,否则模型可以靠记住贴图作弊。
  • clean_background_rate 是"保持干净"的概率,不是"弄脏"的概率。randomized 配置里是 0.02,意味着 98% 的样本带纹理,2% 保持纯色。这个 2% 不是随手写的:全脏会让模型失去"纯色桌面也能干活"的能力,少量干净样本起到锚点作用。
  • crazy_random_light_rate 同样是 2%。极端光照是小概率事件,但真实部署时一定会遇到(逆光、单侧强光)。用 2% 的频率注入,比均匀随机更能覆盖尾部。
  • 桌面高度 ±3 厘米。这是"几何标定误差"的代理。真机上相机外参标定误差通常在厘米级,这个量级是对的。
随机化不是越多越好

有一个反直觉的经验:随机化过头会让任务变得"不可学"。如果杂物撒到机械臂必经之路上、或者光照强到图像全白,专家轨迹本身就会失败,采出来的数据是噪声。RoboTwin 用两个机制控制这个风险:杂物生成带 prohibited_area 避让(见 2.3 节),以及"搜种子"阶段会剔除无解采样。做自己的随机化时,这两条都要有。

3.5评测原理:c2c 与 c2r

榜单的设定值得完整抄下来,因为它定义了"什么叫好":

项目设定
训练数据固定 50 条 demo_clean 轨迹 × 50 个任务(共 2,500 条)
机器人Aloha-AgileX
评测规模每个任务 100 次试验
两个测试条件demo_clean(干净)与 demo_randomized(随机化)
clean2clean(c2c)干净训练 → 干净测试。考"学没学会"
clean2random(c2r)干净训练 → 随机化测试。考"扛不扛变"
默认排名Average = (c2c + c2r) / 2
深度过滤可分"用深度 / 不用深度"筛选;从 RGB 估的深度、仅训练期用的深度监督都不算"用深度"

分开报 c2c 和 c2r 是这套基准最有价值的一个决定。因为这两个数字的差距,直接衡量了"鲁棒性"这件事:

  • c2c 高、c2r 低 → 模型记住了训练分布,泛化差。这是 2024 年之前大多数方法的画像。
  • c2c 略低、c2r 也高 → 学到了可迁移的表征。这才是想要的。
  • 两个都低 → 模型容量或数据量不足,先别谈鲁棒性。

更实用的判断是看落差:如果一个方法的 Average 靠 c2c 撑起来(比如 c2c 80、c2r 30),它的实际部署价值远低于一个 c2c 60、c2r 55 的方法。榜单默认按 Average 排名,但读榜单时应该把三列一起看。

100 次试验意味着什么

这个数字不是随手定的,它决定了榜单上多小的差距可以忽略。二项分布的 95% 置信区间约为 ±1.96·√(p(1−p)/n)。取 p≈0.7:

统计口径n95% CI含义
单个任务100±9.0 个百分点单任务上 60 与 68 的差距没有统计意义
50 任务平均5,000±1.3 个百分点榜单 Average 上 1—2 分的差距仍可能来自随机性

所以在榜单上看到 78.2 vs 77.9,正确的解读是"并列",而不是"后者落后 0.3"。真正值得注意的差距是 5 分以上。这也解释了为什么提交政策卡得那么严:既然数字之间的分辨率只有 1—2 分,那么训练数据、随机种子、判分阈值任何一处不一致,都足以制造一个"虚假的领先"。

提交门槛:四件套

榜单要求上榜模型必须同时具备:公开代码(含后训练与部署)、公开权重、公开技术报告、若基于预训练模型微调则还需公开预训练权重。最后一条最容易被忽略,也最合理——不公开预训练权重,别人无法从同一个初始化重跑 SFT,你的数字就无法被复现,也就无法被比较。

3.6合成数据为什么有效

论文给了三个数字,它们各自说明一件事。理解这三个数字的关系,比记住任何一个都重要。

实验数字它在证明什么
代码生成闭环+10.9% 论文报告代码生成成功率提升 10.9%。说明仿真器作为验证器有价值——它能把"看起来对"的代码筛成"跑得通"的代码
10 条真机 + 合成+367% 相对"只用 10 条真机"的 VLA 基线。说明合成数据能把少量真机的价值放大数倍,这是最贴近实际落地的场景
纯合成零样本+228% 完全不用真机。说明合成数据本身携带可迁移的结构,不只是"预训练权重"的贡献

三个数字连起来读,结论是:合成数据的主要作用不是"替代真机",而是"提高每一条真机数据的收益率"。367% 远大于 228%,多出来的部分就是那 10 条真机的贡献——它提供的不是数据量,是"真实分布的锚点":真实相机的噪声、真实的光照、真实的物体材质。

这也给了一个可操作的建议:如果你有 50 条真机数据,不要用它直接训练一个 VLA(太少),而是用它做两件事——先用合成数据预训练,再用这 50 条做微调;或者反过来,用这 50 条筛选合成数据的哪些子集最有用。后一种做法在 2026 年的实践中越来越常见。

3.72026 的六个趋势

① 基准正在从"模型评测"变成"训练数据源"

2024 年的仿真基准主要用于"证明我的方法更好";2026 年越来越多团队把它当数据工厂用:先在这里预训练,再去真机微调。这从 RoboTwin 自己的定位变化能看出来——1.0 说"benchmark",2.0 的标题里第一位是"scalable data generator",第二位才是"benchmark"。

② 评测正在服务化、跨机化

XPolicyLab 的架构(策略服务 + 仿真客户端,websocket 连接)不是工程洁癖,是必然:策略的依赖越来越重(几十 GB 的模型、特定 CUDA 版本),不可能和仿真器装在一个环境里。这个模式还有一个副作用——评测可以变成一种公共资源:机构 A 提供策略服务,机构 B 提供仿真算力,双方不必交换代码和权重。

③ 随机化从"像素"走向"物理"

当前的随机化主要作用在视觉层(贴图、光照、杂物)。下一步很明确:质量、摩擦系数、关节间隙、相机内参误差、控制延迟。这些"物理域"的随机化对 sim2real 的贡献可能比视觉更大,但实现成本也更高——视觉随机化只是换贴图,物理随机化要改求解器参数并保证数值稳定。

④ 双臂基准正在向"记忆"和"长时序"扩展

2026 年 3 月发布的 RMBench 直接建在 RoboTwin 2.0 之上,专测"依赖记忆的操作"——比如"刚才那个杯子被我放到哪了"。这类任务在 50 个基础任务里几乎没有,因为每个任务都是"一眼看全"的。这是基准演化的典型路径:先有底座,再长出专门化的子基准。

⑤ 与强化学习基础设施合流

2026 年 1 月的 RLinf_support 分支说明一件事:仿真基准不只服务于模仿学习。有了可并行、可重置、带成功判定的环境,RL 的采样成本问题就有了另一种解法——在仿真里大量试错,再迁移。IsaacLab-Arena 分支是同一条思路的另一端。

⑥ 数据格式正在收敛到 LeRobot

RoboTwin 原生格式是 XPolicyLab HDF5,但仓库提供了官方的 LeRobot v2.1 / v3.0 转换脚本。这个选择很务实:LeRobot 生态在 2025—2026 年成了事实标准,不兼容它就等于把自己隔离在主流训练代码之外。代价是转换环节又多了一个出错点(图像通道、状态错位、频率),这也是 4.6 节要专门讲的地方。

3.8这个平台的边界

诚实地列出局限,比罗列优点更有用。以下五条不是缺陷报告,而是"什么时候你不该用它"的判断依据。

边界具体表现替代或补足方案
只有刚性物体 柔性物、液体、颗粒、线缆全部缺席;接触模型在插入类任务上不可信 柔性用 SoftGym / 专用引擎;插入类考虑真机或高保真接触仿真
舞台固定 一张 1.2×0.7 m 的桌子 + 两面墙;没有移动底盘、没有多房间 移动操作看 Habitat / iGibson;多房间看 AI2-THOR
本体只有五个 aloha-agilex、piper、franka-panda、ARX-X5、ur5-wsg;换别的要自己做 URDF 与配置 5.3 节给了接入流程;但夹爪几何差异大时专家代码可能要调
平台受限 SAPIEN 需要 Linux + NVIDIA GPU;无 Windows / macOS 支持 只用数据、不跑仿真的话,任何平台都能做(见 4.9 节)
专家是"死"的 脚本化专家只能做它被写出来的那条路;遇到扰动不会自适应,也不会从失败中学习 DAgger 式闭环纠错需要自己搭;或用 RL 分支

还有一条不太显眼但重要的边界:50 个任务的难度分布并不均匀。_eval_step_limit.yml 里 400 步的任务和 1500 步的任务,学习难度差一个量级。看榜单平均分时要知道,这个平均是被大量"中等难度"任务拉平的——真正的分水岭往往在少数几个长时序任务上。做法和物理 AI 那份报告里说的一样:看分项,不看均值。

Chapter 04

搭建你的 RoboTwin

From a bare machine to your first trajectory

官方说二十分钟装完。这句话成立,但有三个前提:Ubuntu、NVIDIA 显卡、网络能连 Hugging Face。缺任何一个,二十分钟会变成两天。这一章把三个前提都摊开讲。

4.1前置条件

项目要求说明
操作系统Ubuntu 20.04 / 22.04(x86_64)SAPIEN 依赖 Vulkan 渲染,Windows 与 macOS 不支持。WSL2 有人跑通过,但渲染与 CUDA 会多一层麻烦,不建议
GPUNVIDIA,显存 ≥ 12 GB 更稳单任务采集 8 GB 能跑;多任务并行评测按任务数线性增加。cuRobo 后端会额外吃显存
驱动支持 Vulkan 的 NVIDIA 驱动 + CUDA 12.xvulkaninfo 能输出设备信息,是最快的自检方式
Python3.10(conda 环境)安装脚本注释里的示例路径是 envs/RoboTwin/lib/python3.10/site-packages,说明官方环境用的是 3.10
磁盘建议 ≥ 200 GB 可用资产几个 GB;公开数据集按任务分包,几十 GB 起;转成 LeRobot 后会再涨一轮;训练检查点是另一个量级
内存≥ 32 GB多进程采集时每个 worker 都会加载场景与资产

先跑这两条,任一条不过就别往下走:

nvidia-smi                       # 能列出显卡与驱动版本
vulkaninfo | head -20            # 能列出 GPU 设备,说明渲染后端可用

4.2安装四步

1 克隆 含子模块 约 1 分钟 2 建环境 conda + py3.10 约 2 分钟 3 跑 _install.sh 依赖 + 四个补丁 10—20 分钟 4 下资产 3 个 zip 看网速 第 3 步是唯一可能失败的环节:四个补丁都在改第三方包的源码,包版本一换就失效。
图 4-1安装四步。第 3 步之所以在前面标成暖色,是因为它是"二十分钟"与"两天"的分界点。

第 1 步:克隆(必须带子模块)

git clone --recurse-submodules https://github.com/RoboTwin-Platform/RoboTwin.git
cd RoboTwin

# 已经 clone 过、但 XPolicyLab 是空目录的,补这一步:
git submodule update --init --recursive XPolicyLab

忘掉 --recurse-submodules 的后果不是"少几个文件",而是所有评测命令都跑不起来,并且报错信息通常指向一个不存在的模块,很难第一眼联想到子模块。

第 2 步:建 conda 环境

conda create -n RoboTwin python=3.10 -y
conda activate RoboTwin

不要用系统 Python,也不要试图用 3.12。原因见 requirements.txt 第一行注释:RoboTwin native extensions currently require the NumPy 1.x ABI——它钉死在 numpy==1.26.4,而 numpy 2.x 会改 ABI,导致 sapien 的 C++ 扩展直接导入失败。

第 3 步:跑安装脚本

bash scripts/_install.sh

这个脚本做了七件事,其中四件是"打补丁",值得逐条知道,因为它们解释了你后面可能遇到的怪现象:

动作具体内容不打会怎样
装基础依赖 按 requirements.txt 装 torch 2.4.1、sapien 3.0.0b1、mplib 0.2.1、open3d、trimesh 等 缺任何一个都跑不起来
装 pytorch3d 从源码 @stable 编译,--no-build-isolation 部分资产处理与渲染工具不可用;编译很慢,是安装耗时主因
补丁① sapien 给 wrapper/urdf_loader.py 的 open() 加 encoding="utf-8",并修 srdf_file = urdf_file[:-4] + ".srdf" 少一个点的 bug URDF 里有中文/特殊字符路径时读不了;SRDF 永远找不到
补丁② mplib 把 screw plan 失败判据里的 or collide 去掉 某些本可解的姿态被判为碰撞,规划失败,轨迹直接缺失
装 cuRobo clone v0.7.8 编译,另装 warp-lang==1.12.0 与 setuptools==69.5.1 没有 GPU 规划后端,只能走 mplib,慢一些但能跑
装 XPolicyLab pip install -e XPolicyLab(可编辑安装) 评测与数据转换全部不可用
这些补丁靠 sed 匹配字符串,很脆

两个补丁都是 sed -i -E 直接改第三方包的源码。一旦 sapien 或 mplib 升级、改了那行代码的写法,sed 就匹配不上,脚本静默跳过——你会在几周后遇到一个莫名其妙的规划失败。装完之后手动确认一次:打开 site-packages/mplib/planner.py,搜 screw plan failed,看那行里是不是已经没有 collide。

第 4 步:下资产

bash scripts/_download_assets.sh

它做三件事:用 huggingface_hub.snapshot_download 从 TianxingChen/RoboTwin2.0 拉三个包(objects.zip、embodiments.zip、background_texture.zip),解压,然后跑 update_embodiment_config_path.py 把 _embodiment_config.yml 里的路径改成你机器上的绝对路径。

最后这一步常被忽略:本体配置里存的是路径。如果你把仓库挪了位置(或者从别人那里拷了一份),必须重跑这个脚本,否则报"找不到 URDF"。

4.3资产与数据

资产(assets)和数据(data)是两回事,很多人第一次会混淆。

类别内容从哪来要不要下载
资产731 个物体模型、5 个机器人 URDF、背景贴图_download_assets.sh必须下,否则连场景都起不来
预采数据10 万+ 条轨迹(XPolicyLab HDF5)download_xpolicylab_data.sh可选,但强烈建议:这是官方推荐路径
官方检查点基准模型的 ckptHugging Face cotrain_ckpt 目录只做评测时下载

下载数据时,按任务筛选是最实用的技巧——全量下载可能是几十上百 GB,而你初期只需要两三个任务:

# 只下两个任务
bash scripts/download_xpolicylab_data.sh adjust_bottle beat_block_hammer

# 网络好、磁盘快的时候调并发(默认各 8)
HF_MAX_WORKERS=8 HF_EXTRACT_WORKERS=16 bash scripts/download_xpolicylab_data.sh

# 国内网络建议先设镜像,否则会很慢甚至超时
export HF_ENDPOINT=https://hf-mirror.com

脚本设计得不错:下载和解压都是并行的,而且"一个任务的 zip 下完就开始解压",不用等全部下完。数据落在 data/demo_clean/<task>/aloha_agilex/data/——注意这个路径和自采数据的 data/<task_config>/... 是同构的,所以下游脚本不用区分来源。

4.4跑通第一个任务

第一次不要选复杂的任务。beat_block_hammer 是最合适的:物体少(一个锤子一个方块)、动作三步、成功判据清晰(2 厘米 + 接触)。

# 语法:task_name  task_config  gpu_id
bash collect_data.sh beat_block_hammer demo_clean 0

屏幕上会先打印一段配置回显,这是它最有用的设计之一——采集前先让你确认所有开关:

============= Config =============

Messy Table: False
Random Background: False
Random Light: False
Random Table Height: 0
Random Head Camera Distance: 0
Head Camera Config: D435, True
Wrist Camera Config: D435, True
Embodiment Config: aloha-agilex

==================================

然后进入"搜种子"阶段。这一步没有任何进度条,只有一个不断滚动的 step: 12 / 400。如果你发现它跑了十分钟还没进入正式采集,说明这个任务在这套配置下很难解——先换 demo_clean 确认基础流程通了,再开随机化。

正式采集时,每完成一个回合会打印一次成功与否。50 个回合,单卡大约几分钟到十几分钟,取决于任务步数与是否开深度。

只采一条,快速验证

改 env_cfg/task_config/demo_clean.yml 里的 episode_num: 50 为 1,跑一次,几十秒就能拿到一个完整的 HDF5。这是调试任务代码时的标准做法——不要为了看一个 bug 等五十个回合。

4.5验证产物

采完之后,先别急着训练。用这三步确认数据是好的。

① 看目录结构

ls -R data/demo_clean/beat_block_hammer/aloha_agilex | head -30

应该看到 data/episode_0000000.hdf5、video/episode_0000000.mp4,以及可能残留的 _traj_data/。collect_data.sh 最后会删掉各本体的 .cache,但 _traj_data 可能还在,可以手动清。

② 看视频

直接打开 mp4。这是最有效的质检手段:颜色对不对(红蓝有没有反)、动作是否连贯、物体有没有穿模、成功的那一帧长什么样。一眼能看出的问题,比读十行日志快。

③ 读 HDF5 的键

import h5py
with h5py.File("data/demo_clean/beat_block_hammer/aloha_agilex/data/episode_0000000.hdf5") as f:
    def show(name, obj):
        if isinstance(obj, h5py.Dataset):
            print(name, obj.shape, obj.dtype)
    f.visititems(show)
    print("frequency:", f["additional_info/frequency"][()])
    print("instructions:", f["instructions"][()][:200])

重点核对三件事:state/left_arm_joint_states 的第二维是不是 6(aloha 是 6,franka 是 7);action 组是不是比 state 少一帧;instructions 里是不是有你预期的中文/英文句子。

4.6转 LeRobot 并训练

XPolicyLab 里的策略大多吃 LeRobot 数据集(v2.1 或 v3.0 两种格式都有)。仓库提供了官方转换脚本,位置在子模块里:

export HF_LEROBOT_HOME=/data/lerobot        # 转出来的数据放这,别放家目录

# v2.1,全部 demo_clean 任务,每个任务最多 50 条
python XPolicyLab/scripts/transform_lerobot_v21_format.py \
  "demo_clean.*.aloha_agilex" \
  --repo_id robotwin_demo_clean_aloha_agilex \
  --max_episode 50

# v3.0,同样的选择
python XPolicyLab/scripts/transform_lerobot_v30_format.py \
  "demo_clean.*.aloha_agilex" \
  --repo_id robotwin_demo_clean_aloha_agilex_v30 \
  --max_episode 50

三个参数最容易出错:

  • 匹配模式是 <task_config>.<task>.<embodiment>,支持 * 通配,并且是相对 data/ 解析的。写 demo_clean.beat_block_hammer.aloha_agilex 是单个任务。
  • --data_type 保持默认 RoboDojo。RoboTwin 的 XPolicyLab 轨迹和 RoboDojo 共用同一套 HDF5 布局,改成别的反而会错。
  • 分辨率默认自动探测,RoboTwin 通常是 240x320。如果转换报尺寸不匹配,显式传 --resolution 240x320。
转换脚本里那一行不能动

脚本内部调用 decode_image_bit 来解图像。README 明确警告:不要把它换成 cv2.imdecode 或 PIL。原因见 2.9 节——存下来的字节有两种格式,只有 decode_image_bit 能区分。你若 fork 了这个脚本并"顺手优化"了这一行,得到的 LeRobot 数据集里会有一半样本颜色是反的,而且训练曲线看起来完全正常。

转换完成后,训练照该策略自己的 README 走。XPolicyLab 的约定是:每个 policy/<NAME>/ 下有自己的 README、环境文件、检查点布局说明。不要照抄别人的训练命令——ACT 和 π0 的训练参数完全不是一个量级。

4.7评测的三种模式

所有评测都走 scripts/eval_policy.sh,它根据第一个参数分发。三种模式对应三种算力条件:

模式命令特征适用场景
单任务 不带 multitask,或 all_tasks.yml 只留一个任务 调通流程、debug 动作维度、看一条轨迹跑得对不对
多任务多卡 multitask --config env_cfg/eval/all_tasks.yml 正式跑榜。调度器按 gpu_ids 与 jobs_per_gpu 给每个任务起一对"策略服务 + 仿真"
分离部署 策略机跑 serve --config remote_server.yml,仿真机跑 multitask --enable-remote 策略需要大显存、仿真是另一批卡;或跨机构协作评测
# 正式跑榜前,先 dry-run 看调度计划,不浪费卡时
bash scripts/eval_policy.sh multitask \
  --config env_cfg/eval/all_tasks.yml \
  --policy-name <policy_name> \
  --env-cfg-type arx_x5 \
  --policy-conda-env <policy_env> \
  --eval-env-conda-env RoboTwin \
  --action-type <action_type> \
  --dry-run

结果默认写到 eval_result/multitask/。第一件要检查的事不是分数,而是有没有任务因为"策略超时"或"动作维度不符"被跳过——被跳过的任务在算平均时如果按 0 计入,会白白拉低十个点。

4.8十二个常见故障

按"你看到的现象"排序,不是按"可能的原因"排序——因为人是从现象出发的。

#现象原因处理
1评测报"找不到 XPolicyLab"或模块为空clone 时没带子模块git submodule update --init --recursive XPolicyLab
2导入 sapien 报 ABI / numpy 相关错误环境里是 numpy 2.xpip install numpy==1.26.4,并检查有没有别的包把它升上去
3URDF 加载失败,报编码错误或找不到 .srdfsapien 补丁没打上手动改 wrapper/urdf_loader.py,或重跑 _install.sh
4专家轨迹在某些位姿下必然失败,日志里有 screw plan failedmplib 的碰撞判据补丁没打去掉判据里的 or collide,见 4.2 节
5执行 collect_data.sh 报 "No such task"文件名与类名不一致两者必须完全相同,包括下划线
6下载数据总是中断,或者卡在某个 zipHF 直连不稳定设 HF_ENDPOINT 镜像;调小 HF_MAX_WORKERS;脚本支持断点续传,重跑即可
7训练 loss 正常,但推理时物体位置全错图像通道反了(多余的一次 BGR→RGB)只用 decode_image_bit,删掉任何 cvtColor
8策略分数接近 0,且动作幅度很小--env-cfg-type 与本体维度不匹配aloha-agilex 用 arx_x5;换本体要核对 _robot_info.json
9多任务评测起不来,报端口占用策略服务端口池冲突改 all_tasks.yml 的 policy_server_port 列表,或先杀掉残留进程
10采集速度极慢,GPU 利用率低开了 depth / pointcloud,或分辨率用了 Large_*先用默认 D435 320×240 + 只开 rgb 跑通
11渲染起不来,报 Vulkan / EGL 错误无头服务器缺渲染后端,或驱动不支持 Vulkan装 NVIDIA 驱动 + vulkan-tools;确认 vulkaninfo 能跑
12磁盘迅速被 .cache 与 _traj_data 塞满中间产物没清collect_data.sh 会清 .cache;_traj_data 手动删;调小 clear_cache_freq

4.9没有 GPU 怎么办

有相当多的工作根本不需要跑仿真。如果你的目标是"用这批数据训练一个策略"或者"分析这个数据集",可以跳过 4.2 的前三步中的大部分,只做数据部分:

  1. 只读数据:装 h5py + opencv-python + XPolicyLab(纯 Python 部分),就能读 HDF5、解图像、统计分布。这一套在笔记本上就能跑。
  2. 只做转换:把 XPolicyLab HDF5 转成 LeRobot 格式,也不需要 GPU。这一步在 CPU 机器上跑一晚,能把几十个任务转完。
  3. 用别人的检查点做推理分析:如果只是想看某个策略的输出分布,可以把策略跑在 CPU 上(慢但可行),仿真部分借一台云机器。

必须用 GPU 的只有两件事:跑仿真(渲染 + 物理)、以及训练稍大的策略模型。所以最省钱的组合是本地 CPU 做数据处理 + 按小时租一张云 GPU 跑采集和评测。采集是批处理型任务,租用成本可控;训练则需要长期占卡,另作打算。

Chapter 05

如何参与 RoboTwin

Five ways to contribute, ranked by scarcity

这个项目最缺的不是代码。_base_task.py 已经够用,改它反而容易引入不兼容;真正稀缺的是"别人没有的任务"和"别人没有的场景"。

5.1五种角色

角色产出物稀缺度门槛
任务作者 一个新的 envs/*.py + 指令模板 高 要懂一点运动规划直觉,能判断"什么动作是机器人真能做到的"
数据贡献者 一批带特定分布的轨迹(新本体、新物体、新随机化) 高 有 GPU 就能做;不需要写算法
策略提交者 一个上榜条目(代码 + 权重 + 报告) 中 要能把一个 VLA/IL 模型训到可用,并公开全部材料
修 bug 的人 PR,通常几行 中 能读懂 traceback,愿意读 1796 行的基类
文档与语言 教程、翻译、指令模板扩充、示例 notebook 中低 几乎没有技术门槛,但对新人的帮助最大

排序的依据是"缺了会怎样"。少一个 PR,项目照常运转;少一类任务或一批数据,整个社区的能力边界就窄一块。所以下面先讲任务,再讲本体,最后讲 PR。

5.2加一个新任务

完整流程六步。前四步是写代码,后两步是让它"合法"。

第一步:确认它值得加

先问三个问题:这个任务能不能被分解成现有的原语(抓、移、放、开合、旋转)?成功能不能用几何 + 接触判据写出来?它和已有的 50 个任务实质区别在哪?如果第三问答不出来,那它大概率只是已有任务的一个变体,改随机化参数就够了,不需要新任务。

参考 50 个任务的命名可以看出一套约定:place_A_B(把 A 放到 B 上)、pick_*(抓取,终点不严格)、move_A_B(移动)、handover_*(两臂交接)、stack_*(堆叠)、click/open/press/turn/shake(操作类)。命名即分类,新任务最好延续这个约定,评测清单和文档都会更好查。

第二步:写文件骨架

新建 envs/place_object_laptop.py,文件名必须与类名完全一致(collect_data.py 用 importlib 按名字导入,且 envs/__init__.py 是空的,不需要注册):

from ._base_task import Base_Task
from .utils import *
import sapien
from ._GLOBAL_CONFIGS import *


class place_object_laptop(Base_Task):

    def setup_demo(self, **kwags):
        super()._init_task_env_(**kwags)

    def load_actors(self):
        self.laptop = create_actor(
            scene=self,
            pose=sapien.Pose([0.0, 0.0, 0.741], [0, 0, 0.707, 0.707]),
            modelname="039_laptop", convex=True, model_id=0)

        obj_pose = rand_pose(
            xlim=[-0.20, 0.20], ylim=[0.10, 0.25], zlim=[0.76],
            qpos=[1, 0, 0, 0], rotate_rand=True, rotate_lim=[0, 0, 0.4])
        self.obj = create_box(
            scene=self, pose=obj_pose, half_size=(0.03, 0.03, 0.02),
            color=(0.8, 0.3, 0.2), name="obj", is_static=True)

        # 双臂任务:给两件东西都划出禁放区,避免杂物挡路
        self.add_prohibit_area(self.laptop, padding=0.12)
        self.add_prohibit_area(self.obj, padding=0.08)

    def play_once(self):
        # 抓取点取物体的 0 号功能点,落点取笔记本的 1 号功能点
        self.move(self.grasp_actor(self.obj, arm_tag=ArmTag("right"),
                                   pre_grasp_dis=0.10, grasp_dis=0.01))
        self.move(self.move_by_displacement(ArmTag("right"), z=0.08, move_axis="arm"))
        self.move(self.place_actor(self.obj,
                                   target_pose=self.laptop.get_functional_point(1, "pose"),
                                   arm_tag=ArmTag("right"),
                                   functional_point_id=0, pre_dis=0.05, dis=0, is_open=True))
        self.info["info"] = {"{A}": "039_laptop/base0", "{a}": "right"}
        return self.info

    def check_success(self):
        # 位姿容差 + 静置稳定,两条同时成立
        obj_p = self.obj.get_functional_point(0, "pose").p
        tgt_p = self.laptop.get_functional_point(1, "pose").p
        eps = np.array([0.025, 0.025])
        return bool(np.all(abs(obj_p[:2] - tgt_p[:2]) < eps)) and self.check_stable()

四段里最容易写错的是 check_success。三条经验:

  • 容差要留余量。2 厘米是常见值,1 厘米以下会让专家自己都判不过——因为规划器的最末端误差就有几毫米,加上物理沉降会超出。
  • 能加接触判据就加。光判位姿会把"物体正好飞过去但没放稳"算成成功。check_actors_contact 是最便宜的加固方式。
  • 不要写"必须有动作发生过"这类条件。判据要只看终态,否则评测时策略的不同路径会被不公平地惩罚。

第三步:写指令模板

新建 description/task_instruction/place_object_laptop.json,结构与已有任务一致:full_description、schema、preference,以及 seen / unseen 两个数组。最低要求是各写几条能跑通流程,想要 100 条就用手写 + LLM 扩写混合:先让模型生成 200 条,人工删掉语法错误和语义偏移的,保留 100 条。

unseen 不是 seen 的复制。它应该刻意使用不同的语序、不同的动词、不同的指代方式。比如 seen 里是 "pick up the box",unseen 里就该出现 "grab the carton"、"take the container" 这类换词。这一步决定了你的语言鲁棒性评测有没有意义。

第四步:登记步数上限

在 env_cfg/task_config/_eval_step_limit.yml 加一行。经验值:简单抓放 400,中等 600—800,长时序 900—1500。定低了专家自己会被截断,定高了评测时间会膨胀。

place_object_laptop: 600

第五步:采一条,看视频

bash collect_data.sh place_object_laptop demo_clean 0   # 先把 episode_num 改成 1

打开 mp4。要确认的四件事:物体没穿模、机械臂没自碰撞、动作连贯(不是瞬移)、成功那一帧看着确实成功。这一步过不了,后面全白做。

第六步:跑一遍随机化

bash collect_data.sh place_object_laptop demo_randomized 0

随机化下要重点看两件事:杂物有没有挡住必经路径(有的话加大 add_prohibit_area 的 padding);桌面高度变化 3 厘米后,抓取高度是不是还够得着(不够就说明 pre_grasp_dis 设小了)。

提交任务时的加分项

一个能被合并的任务 PR,通常包含:任务文件、指令模板、步数上限、一条成功轨迹的截图或 gif、以及一段说明"它和现有哪个任务最接近、区别在哪"。最后这项最容易被省略,却是维护者判断要不要收的第一依据。

5.3加一个新本体

如果你手上有 URDF、想让 RoboTwin 支持你的机器人,要改四个地方。现有五个本体(aloha-agilex、piper、franka-panda、ARX-X5、ur5-wsg)就是这套流程的样例。

① 放资产

把 URDF 与 mesh 放到 assets/embodiments/<your-robot>/,目录里需要一个 config.yml,描述关节名、夹爪关节、初始位姿等。直接抄一个最接近的现有本体的 config.yml 改成你的名字,比从零写快得多。

② 登记路径

在 env_cfg/task_config/_embodiment_config.yml 加一项,file_path 指向上面那个目录。

my-robot:
  file_path: "./assets/embodiments/my-robot/"

③ 声明动作维度

在 env_cfg/robot/_robot_info.json 加一项。这一步错了,一切都会错位——因为 take_action 就是按这张表切分动作数组的。

"my_robot": { "arm_dim": [6, 6], "ee_dim": [1, 1] }

④ 用任务配置指定它

在你要跑的 task_config 里改 embodiment 字段。两种写法:

# 两臂同型
embodiment: [my-robot]

# 两臂异构:左臂、右臂、两臂间距(米)
embodiment: [my-robot, piper, 0.9]

异构双臂是 2.0 支持但很少被用到的能力。collect_data.py 里对长度 3 的配置会设 dual_arm_embodied=False 并把第三个元素当间距,落盘目录名会变成 my_robot_piper。想研究"两只不同的手怎么协作",这是现成的入口。

专家代码可能需要微调

任务代码里写的是功能点,理论上换本体不用改。但实践中有两处会出问题:夹爪开口小于物体尺寸(抓不住)、臂展够不到桌面远端(规划失败)。前者要换物体或换夹爪,后者要在任务里加"就近选臂"的逻辑(参考 beat_block_hammer 里按方块 x 坐标选左右臂那三行)。

5.4打榜的四项硬要求

想上榜,四件材料缺一不可,这是提交政策明确要求的:

  1. 公开代码,且必须包含后训练与部署两部分。只给推理脚本不算——别人要能从你的数据复现训练。
  2. 公开权重,包括最终检查点。
  3. 技术报告,arXiv 论文或同等公开文档。
  4. 如果基于预训练模型微调,还要公开预训练权重,以便别人从同一初始化重跑 SFT。

除此之外有三条隐含要求,是从评测设定里推出来的:

  • 训练只能使用规定的 50 条 demo_clean × 50 任务(单任务 SFT 赛道)。用更多数据要进 co-train 赛道,别混着报。
  • 深度要么用、要么不用,不能"偷偷用"。从 RGB 估出来的深度不算"用深度",但要在报告里说明。
  • c2c 与 c2r 两个数都要报。只报平均分会让人觉得你在藏落差。

提交方式按榜单页面的说明联系维护者。提交前先自查一件事:把 eval_policy.sh 的命令原样写进报告,包括 --env-cfg-type 与 --action-type 的取值。这两项不同,分数不可比。

5.5修 bug 与 PR 流程

这个仓库有 122 个 open issue,是一个"issue 比 PR 多"的项目——意味着修 bug 的价值很高。从 2025—2026 的更新记录看,被合并的修复大多是这类:

  • 某个本体的腕部模型朝向错了(Piper wrist bug,需要重新下载资产)
  • 某个基线策略的评测代码写错了(DP3 的 eval、ACT 的 deployment)
  • 默认参数被就地修改导致累积偏移(issue #288:xlim 默认值被原地更新,多次调用后桌子偏移量叠加)
  • 解压/编码/路径的边角情况

第三类特别值得学:get_cluttered_table 里那句注释写得很清楚——"Rebind instead of mutating: the defaults are shared across calls, so in-place updates would accumulate the table offset"。Python 默认参数是可变对象这个经典陷阱,在仿真代码里会表现为"跑到第 30 个回合桌子开始漂移",极难排查。

提交 PR 的四个建议

  1. 一个 PR 只做一件事。修表格偏移和加新任务不要放在一起,后者会让前者永远排在 review 队列末尾。
  2. 带上复现步骤与前后对比。仿真类 bug 没有截图/视频基本没人能验证。采一条 1 回合的轨迹、录个 gif,比写三百字描述有效。
  3. 不要动 _base_task.py 的公共接口签名。50 个任务和外部策略都依赖它。确实要改就加默认参数保持兼容。
  4. 改数据格式要极其谨慎。HDF5 布局是跨机构契约(RoboTwin 与 RoboDojo 共用),改字段名会一次性打断所有人的 dataloader。真需要加字段,加成新组而不是改旧组。

5.6贡献对象与场景

除了写代码,还有一条门槛更低、价值同样高的路:往 731 个物体的库里加东西。物体库是 RoboTwin-OD,每个实例需要:网格模型、碰撞体、质量、功能点定义、语义标注,以及一段自然语言描述(description/objects_description/ 下的 JSON)。

加物体的价值在于它会被所有任务自动受益。你加了十个厨房小物件,理论上 place_* 系列任务就能组合出更多变体——因为任务代码写的是功能点,不写具体模型。这也解释了 50 个任务为什么能用 731 个物体:组合空间是乘出来的。

另一条被低估的贡献是随机化素材:背景贴图库(assets/background_texture/seen 与 unseen)。加 100 张真实办公桌/厨房台面的照片,比改十行代码更能提升 c2r 上的泛化难度。注意要分别加进 seen 和 unseen 两个库,且两边不能重复——这是"训练/测试不共享贴图"的约定。

5.7三十天路径

阶段目标具体动作
第 1 周
跑通
能采出一条自己的轨迹 装环境 → 下资产 → beat_block_hammer 采 1 条 → 看视频 → 读 HDF5 键名。遇到坑就对照 4.8 节的表
第 2 周
读懂
能解释一次采集发生了什么 通读 beat_block_hammer.py(87 行)→ 顺着 collect_data.py 的 main() 走一遍 → 读 pkl2hdf5.py 确认数据格式 → 画一张自己的时序图
第 3 周
动手
有一个自己能跑的东西 二选一:① 加一个任务(5.2 节,两天能完成)② 把一个 ACT 或 DP 训到 c2c 60 分以上(用官方 50 条数据,别碰随机化)
第 4 周
交出去
有一个对外可见的产物 提一个 PR(哪怕只修一行)/发一个数据集到 HF/写一篇复现笔记。三选一即可,重点是让东西离开你的硬盘

如果只有周末两天,做这个最小闭环:下一个任务、采 50 条、转成 LeRobot、用 ACT 训 2 万步、在 clean 上评测。这条路径会逼你踩到本文提到的绝大多数坑——图像解码、动作维度、步数上限、判据容差——而每一步都有明确的成功信号。

Appendix A

命令速查

命令作用
git clone --recurse-submodules <repo>克隆,含 XPolicyLab 子模块
git submodule update --init --recursive XPolicyLab补/更新子模块(已 clone 过时用)
bash scripts/update_xpolicylab.sh拉 XPolicyLab 最新并刷新钉住的提交;--stage --install 一并暂存重装
bash scripts/_install.sh装依赖 + 四个补丁 + cuRobo + XPolicyLab
bash scripts/_download_assets.sh下三个资产包并解压、回写本体路径
bash scripts/download_xpolicylab_data.sh [task...]下预采数据,可只下指定任务;HF_MAX_WORKERS 控并发
bash collect_data.sh <task> <config> <gpu>采数据(先搜种子再重放)
python scripts/collect_data.py <task> <config>同上,直连 Python 入口(便于调试)
python XPolicyLab/scripts/transform_lerobot_v21_format.py "<cfg>.<task>.<emb>" --repo_id <id>转 LeRobot v2.1
python XPolicyLab/scripts/transform_lerobot_v30_format.py ...转 LeRobot v3.0
bash scripts/eval_policy.sh multitask --config ... --dry-run只验证调度计划,不真跑
bash scripts/eval_policy.sh serve --config env_cfg/eval/remote_server.yml在策略机上起服务池
python scripts/eval_policy_xpolicylab.py ...单任务评测的实际入口(一般由 shell 分发)
bash scripts/create_messy_data.py / create_object_data.py生成杂乱场景 / 物体相关数据
python data/decode_image_bit.py图像字节解码的最小示例(自采数据用它)

命令以仓库 main 分支为准。旧教程里的 python script/collect_data.py(单数 script)与 eval_policy.py 在 2.0 已被替换,直接照抄会找不到文件。

Appendix B

术语表

术语本书中的含义
任务(task)一个 envs/*.py 文件,定义物体摆放、专家动作序列、成功判据
任务配置(task_config)env_cfg/task_config/*.yml,决定采多少条、开哪些随机化、存什么模态
本体(embodiment)机器人的具体型号,目前五个;决定关节维度与 URDF
功能点(functional point)物体上的语义位置,如握把、敲击面、把手、内腔;专家代码据此定位而非硬编码坐标
原语(primitive)基类提供的语义级动作,如 grasp_actor、place_actor
专家(expert)脚本化的操作序列,用来生成示范,不是学习得到的策略
搜种子采集前的阶段:反复随机直到找到能让目标数量都成功的种子序列
c2cclean2clean,干净训练、干净测试,考"学没学会"
c2rclean2random,干净训练、随机化测试,考"扛不扛变"
域随机化五根轴:杂物、光照、背景、桌面高度、语言
XPolicyLab策略层子模块,统一 69 个策略的接入、服务化与评测
RoboTwin-OD物体库,731 个实例 / 147 个类别,带语义与操作标注
legacy / standardHDF5 里图像字节的两种格式;前者通道反,只能用 decode_image_bit 统一解
动作契约策略输出向量的维度与顺序,由 _robot_info.json 定义
Appendix C

五十个任务

The full task list, grouped by verb

按动词分组比按字母序有用,因为同组任务的专家代码结构高度相似——想学着写任务,先从最短的那组里挑一个读完。

放(place / put)——最多的一组,19 个

place_*11 个
place_a2b_leftplace_a2b_right place_bread_basketplace_bread_skillet place_burger_friesplace_can_basket place_cans_plasticboxplace_container_plate place_dual_shoesplace_empty_cup place_fan
place_* / put_*8 个
place_mouse_padplace_object_basket place_object_scaleplace_object_stand place_phone_standplace_shoe put_bottles_dustbinput_object_cabinet

抓(pick / grab / lift)——6 个

pick_*3 个
pick_diverse_bottlespick_dual_bottles grab_rollerlift_pot scan_objectstamp_seal

操作(open / click / press / turn / rotate)——7 个

精细操作step 上限偏高
open_laptopopen_microwave click_alarmclockclick_bell press_staplerturn_switch rotate_qrcode

双臂协同(handover / lift / hang)——4 个

协同最能体现"双臂不可替代"
handover_blockhandover_mic hanging_muglift_pot

堆叠与排序(stack / ranking)——6 个

多物体步数上限 1200
stack_blocks_twostack_blocks_three stack_bowls_twostack_bowls_three blocks_ranking_rgbblocks_ranking_size

移动与整理(move / dump / shake)——8 个

搬运含"倒出"类
move_can_potmove_pillbottle_pad move_playingcard_awaymove_stapler_pad dump_bin_bigbinshake_bottle shake_bottle_horizontallyadjust_bottle

工具使用——1 个,也是最好的入门样本

唯一的工具类87 行
beat_block_hammer 全书以它为例讲解任务实现

分组之间只允许一处重叠:lift_pot 同时属于"抓取"与"双臂协同"(两个手一起抬锅)。去重后合计 50 个,与 env_cfg/eval/all_tasks.yml 里的清单一致。想看某个任务的完整实现,直接读 envs/<name>.py;想知道它允许多少步,查 _eval_step_limit.yml。最省力的入门路径是读 beat_block_hammer.py(87 行),再看 stack_bowls_two.py(多物体怎样扩展)。