双臂操作的
数据工厂
与考卷
一个模拟器之上只做两件事的平台:批量造出带标注的双臂操作数据,并用一套统一的评分标准把所有人的策略放在同一张桌子上比较。这本书拆开它的每一层。
为什么值得为它写一本书
机器人学习过去五年的困境,不是模型不够大,而是没有足够多、足够脏、足够多样的数据。真机采一条轨迹要几十秒到几分钟,还要人、要设备、要场地;而一个 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)。凡属推测或经验判断,文中会明确标出。
这是一个什么项目
A data generator and a benchmark, not a simulator
如果你以为它是"另一个 Isaac Gym",会一直找不到重点。它的核心资产不是物理引擎,而是那 50 个任务脚本、731 个带标注的物体、五根随机化轴,以及一套 100 次试验的评分规则。
1.1一句话定位
RoboTwin 是建立在 SAPIEN 物理引擎之上的双臂操作任务层。它自己不做刚体求解、不做渲染,这些交给 SAPIEN;它做的是三件 SAPIEN 不做的事:
最下面一层是别人的,上面三层是 RoboTwin 的全部工作量所在。这也解释了为什么它的依赖里既有 sapien 也有 curobo、mplib、toppra——它是一套"会做动作的仿真场景",而不只是"能渲染的仿真场景"。
1.2三代版本:从生成孪生到数据工厂
理解这个项目最好的方式,是看它的三次自我修正。名字没变,但野心变了。
第一代(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) | 5 | aloha-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 fork | 2026-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 研究的选题清单":
- 单任务微调能力——给定 50 条示范,你的模型能不能学会一个任务?这是最低门槛,也是所有榜单数字的基线。
- 视觉鲁棒性——在干净场景训练、在撒了杂物和奇怪光照的场景测试(c2r),掉多少点?这是 2.0 最想暴露的问题。
- 语言多样性鲁棒性——同一个任务换一种说法还能不能做?每个任务预置了 100 条 seen 与 100 条 unseen 指令,这是很少见的"语言泛化"评测资源。
- 多任务能力——一个模型同时学 50 个任务,能不能不互相干扰。
- 跨本体性能——在 aloha-agilex 上训的策略,换到 piper 或 franka 上还剩多少。
还有一类使用者容易被忽略:做数据引擎的人。他们不关心策略分数,关心的是"怎么在无人值守的情况下,一晚上生成两千条带失败案例的轨迹"。对这类使用者,第二章的 scripts/collect_data.py 与 env_cfg 才是全书重点,第三、四章可以跳读。
模块详解
Reading the repository, directory by directory
仓库只有七个一级目录,但真正的复杂度集中在一个 1796 行的文件里。这一章按"你打开它会先点开哪个文件夹"的顺序走,并且在每个模块末尾给出它和其他模块的接口。
2.1仓库地图
注意最后一行是子模块。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 行里定义了六十多个方法。不要试图顺读,按四组分类看会快得多。
① 生命周期:一个任务从创建到出数据
这条链上有一个容易被忽略的设计:物体不是刚体摆件,而是"带功能点的对象"。每个 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.py | 714 行,封装一台双臂机器人:加载 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) 支持两种模式,这是接策略时最容易搞错的地方:
| 模式 | 每臂维度 | 含义与使用场景 |
|---|---|---|
| qpos | 6 或 7 + 1 | 关节角 + 夹爪。训练用的默认模式,数据里的 state/left_arm_joint_states 就是它 |
| ee | 7 + 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:四类配置
配置分四层,各管一件事。理解这四层的边界,比记住字段更重要。
demo_clean 与 demo_randomized:两份设定,一个开关
仓库只给两份任务配置,差异全在 domain_randomization 段。这是全书最该逐字段读的一张表:
| 字段 | demo_clean | demo_randomized | 实际影响 |
|---|---|---|---|
| episode_num | 50 | 50 | 每个任务采多少条——榜单标准就是 50 |
| save_freq | 15 | 15 | 每秒存 15 帧,即视频里看到的数据频率 |
| language_num | 100 | 100 | 每回合生成多少条语言变体 |
| eval_instruction | seen | unseen | 用见过的说法还是没见过的说法——这两份配置连语言难度都不同 |
| random_background | false | true | 贴图换桌面与墙面(seen/unseen 两套贴图库) |
| clean_background_rate | 1 | 0.02 | 以多大概率保持纯色。1 = 永远干净,0.02 = 98% 带纹理 |
| cluttered_table | false | true | 桌上撒 10 个随机杂物 |
| random_light | false | true | 随机光照方向与强度 |
| crazy_random_light_rate | 0 | 0.02 | 2% 的概率来一次"极端"光照,专治过拟合 |
| random_table_height | 0 | 0.03 | 桌面高度上下浮动 3 厘米——真实桌子就是不平的 |
| random_head_camera_dis | 0 | 0 | 相机距离抖动,默认关 |
注意 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。
实现上,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")
三个设计细节值得抄走:
- 给的是 API 文档而不是代码库。
AVAILABLE_ENV_FUNCTION与FUNCTION_EXAMPLE是人工整理的原语清单和示范。这比把_base_task.py整个塞进上下文有效得多——1796 行会淹没重点,还会诱导模型去调用不该碰的内部方法。 - 判据是"多次试验的成功率",不是"跑通一次"。因为随机摆放会让同代码有时成功有时失败,只跑一次等于抽奖。50% 的阈值是在"能跑"和"稳定"之间取的折中。
- 报错原文回传。不翻译、不总结,直接把 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() 做四件事,顺序很重要:
- 按名字动态导入任务类(
class_decorator里importlib.import_module(f"envs.{task_name}"))。所以任务文件名必须等于类名,写错一个下划线就 "No such task"。 - 读任务配置,再读本体配置。本体字段允许两种写法:长度 1 表示两臂同型(
dual_arm_embodied=True),长度 3 表示两臂异构且第三个元素是间距。 - 算保存路径:
data/<task_config>/<task>/<embodiment>/,其中本体名会被规范化(aloha-agilex→aloha_agilex,ARX-X5→arx_x5)。 - 两阶段采集:先"搜种子"——反复随机直到找到一个能让目标数量都成功的种子序列;再"重放种子"正式采。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 的实际时序如下。这张图值得贴在显示器边上——出问题时,先判断卡在哪一段。
2.13产物长什么样
采集与下载的数据都遵循一套 XPolicyLab 布局(data_type 默认值是 RoboDojo,与 RoboTwin 共用)。目录结构:
单个 HDF5 内部的组结构(来自 pkl2hdf5.py 的 create_xpolicylab_hdf5):
三个要点:
- 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打开)。
行业位置、原理与趋势
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 套任务代码:抽象层级选对了,规模才能扩。
把两者合起来,数据合成的完整链条是:
需要点明代价:这套方法只能覆盖"可被几何描述的操作"。倒水、揉面、插拔线缆这类依赖形变与柔性接触的任务,脚本写不出来,规划器也算不准。这也是 50 个任务里全是刚性物体抓放、开关、堆叠、交接的原因。不是不想做,是这套方法的天花板就在那里。
3.4域随机化:五根轴
域随机化的理论依据很朴素:训练分布与测试分布不一致时,性能按两者的差异程度衰减。既然无法枚举真实世界的所有变化,就让训练分布尽量"包住"它。RoboTwin 2.0 把这个想法拆成五根可以独立开关的轴。
几个实现细节比概念更值得注意:
- 背景贴图分 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:
| 统计口径 | n | 95% 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 那份报告里说的一样:看分项,不看均值。
搭建你的 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 会多一层麻烦,不建议 |
| GPU | NVIDIA,显存 ≥ 12 GB 更稳 | 单任务采集 8 GB 能跑;多任务并行评测按任务数线性增加。cuRobo 后端会额外吃显存 |
| 驱动 | 支持 Vulkan 的 NVIDIA 驱动 + CUDA 12.x | vulkaninfo 能输出设备信息,是最快的自检方式 |
| Python | 3.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 步:克隆(必须带子模块)
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 -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 | 可选,但强烈建议:这是官方推荐路径 |
| 官方检查点 | 基准模型的 ckpt | Hugging 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.x | pip install numpy==1.26.4,并检查有没有别的包把它升上去 |
| 3 | URDF 加载失败,报编码错误或找不到 .srdf | sapien 补丁没打上 | 手动改 wrapper/urdf_loader.py,或重跑 _install.sh |
| 4 | 专家轨迹在某些位姿下必然失败,日志里有 screw plan failed | mplib 的碰撞判据补丁没打 | 去掉判据里的 or collide,见 4.2 节 |
| 5 | 执行 collect_data.sh 报 "No such task" | 文件名与类名不一致 | 两者必须完全相同,包括下划线 |
| 6 | 下载数据总是中断,或者卡在某个 zip | HF 直连不稳定 | 设 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 的前三步中的大部分,只做数据部分:
- 只读数据:装
h5py+opencv-python+XPolicyLab(纯 Python 部分),就能读 HDF5、解图像、统计分布。这一套在笔记本上就能跑。 - 只做转换:把 XPolicyLab HDF5 转成 LeRobot 格式,也不需要 GPU。这一步在 CPU 机器上跑一晚,能把几十个任务转完。
- 用别人的检查点做推理分析:如果只是想看某个策略的输出分布,可以把策略跑在 CPU 上(慢但可行),仿真部分借一台云机器。
必须用 GPU 的只有两件事:跑仿真(渲染 + 物理)、以及训练稍大的策略模型。所以最省钱的组合是本地 CPU 做数据处理 + 按小时租一张云 GPU 跑采集和评测。采集是批处理型任务,租用成本可控;训练则需要长期占卡,另作打算。
如何参与 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打榜的四项硬要求
想上榜,四件材料缺一不可,这是提交政策明确要求的:
- 公开代码,且必须包含后训练与部署两部分。只给推理脚本不算——别人要能从你的数据复现训练。
- 公开权重,包括最终检查点。
- 技术报告,arXiv 论文或同等公开文档。
- 如果基于预训练模型微调,还要公开预训练权重,以便别人从同一初始化重跑 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 的四个建议
- 一个 PR 只做一件事。修表格偏移和加新任务不要放在一起,后者会让前者永远排在 review 队列末尾。
- 带上复现步骤与前后对比。仿真类 bug 没有截图/视频基本没人能验证。采一条 1 回合的轨迹、录个 gif,比写三百字描述有效。
- 不要动
_base_task.py的公共接口签名。50 个任务和外部策略都依赖它。确实要改就加默认参数保持兼容。 - 改数据格式要极其谨慎。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 上评测。这条路径会逼你踩到本文提到的绝大多数坑——图像解码、动作维度、步数上限、判据容差——而每一步都有明确的成功信号。
命令速查
| 命令 | 作用 |
|---|---|
| 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 已被替换,直接照抄会找不到文件。
术语表
| 术语 | 本书中的含义 |
|---|---|
| 任务(task) | 一个 envs/*.py 文件,定义物体摆放、专家动作序列、成功判据 |
| 任务配置(task_config) | env_cfg/task_config/*.yml,决定采多少条、开哪些随机化、存什么模态 |
| 本体(embodiment) | 机器人的具体型号,目前五个;决定关节维度与 URDF |
| 功能点(functional point) | 物体上的语义位置,如握把、敲击面、把手、内腔;专家代码据此定位而非硬编码坐标 |
| 原语(primitive) | 基类提供的语义级动作,如 grasp_actor、place_actor |
| 专家(expert) | 脚本化的操作序列,用来生成示范,不是学习得到的策略 |
| 搜种子 | 采集前的阶段:反复随机直到找到能让目标数量都成功的种子序列 |
| c2c | clean2clean,干净训练、干净测试,考"学没学会" |
| c2r | clean2random,干净训练、随机化测试,考"扛不扛变" |
| 域随机化 | 五根轴:杂物、光照、背景、桌面高度、语言 |
| XPolicyLab | 策略层子模块,统一 69 个策略的接入、服务化与评测 |
| RoboTwin-OD | 物体库,731 个实例 / 147 个类别,带语义与操作标注 |
| legacy / standard | HDF5 里图像字节的两种格式;前者通道反,只能用 decode_image_bit 统一解 |
| 动作契约 | 策略输出向量的维度与顺序,由 _robot_info.json 定义 |
五十个任务
The full task list, grouped by verb
按动词分组比按字母序有用,因为同组任务的专家代码结构高度相似——想学着写任务,先从最短的那组里挑一个读完。
放(place / put)——最多的一组,19 个
抓(pick / grab / lift)——6 个
操作(open / click / press / turn / rotate)——7 个
双臂协同(handover / lift / hang)——4 个
堆叠与排序(stack / ranking)——6 个
移动与整理(move / dump / shake)——8 个
工具使用——1 个,也是最好的入门样本
分组之间只允许一处重叠: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(多物体怎样扩展)。