具身智能的数据从哪来?unitree_sim_isaaclab 仿真环境解读
- 搞清楚模仿学习需要什么形态的数据,以及真机采集贵在哪
- 读懂 unitree_sim_isaaclab 的三种运行模式:遥操作采集、数据回放、数据生成
- 知道这个仿真环境在「采数据 → 转格式 → 训模型 → 回评估」这条链路里占哪一格
你想训一个模型,让机器人把桌上的杯子拿起来,放到指定位置。
算法有现成的,模型结构也不用自己发明。然后你卡在了第一步:数据在哪。
这个问题在别的 AI 方向上很少这么尖锐。做图像分类,网上有海量带标签的图;做语言模型,整个互联网都是语料。可机器人操作任务需要的东西,互联网上一份都没有——它需要的是「这一帧画面里,机器人的每个关节处在什么位置,然后它下一步把每个关节转到了哪里」。这种数据不存在于任何公开语料里,只能一条一条采出来。
宇树开源的 unitree_sim_isaaclab 就是干这件事的工具之一。这篇我们把它读一遍,重点不在怎么装环境(那部分官方文档写得比我清楚),而在于它在整条数据链路里占哪一格,以及为什么这一格必须存在。
先说边界:本文基于该仓库公开的 README、目录结构与文件清单。我手上没有 G1 也没有对应的显卡环境,所以不会出现任何「我跑起来之后效果如何」的描述,涉及运行行为的地方一律以官方文档的说法为准。
一条「训练数据」到底长什么样
先把需求说清楚,不然看仓库时抓不住重点。
模仿学习的思路朴素得近乎笨:让模型看人是怎么做的,然后学着做。 它需要的一条样本是一个二元组:
观测 observation → 动作 action
观测这边通常包含两类东西:
- 外部感知:相机图像,让模型知道杯子在哪、桌子有多高、手离目标还差多少
- 本体状态:机器人自己每个关节当前的角度、速度,末端夹爪张开了多少
动作那边是模型要输出的东西——下一时刻每个关节的目标位置。
关键在于这两者必须成对,而且时间对齐。图像是 t 时刻拍的,动作就必须是 t 时刻发出的那条指令。差一帧,模型学到的因果关系就是错的:它会以为「看到杯子已经在手里了」应该输出「张开手指去抓」。这类错位不会让训练报错,只会让模型表现莫名其妙地烂,排查起来极其痛苦。
一整段连续的观测-动作序列叫一个 episode,也就是「一次完整的任务演示」。训练要的是成千上万个 episode。
真机采这种数据,贵在哪
理解了数据形态,就能理解成本。
得有人。 这种数据没法自动产生,必须由人通过遥操作设备把机器人当提线木偶一样操控着完成任务。这一路的接入方式可以看遥操作那一篇,宇树这边对应的是 xr_teleoperate 项目。人力成本是实打实的。
一次只能采一条。 真机是物理实体,同一时刻只能演示一个任务。没有并行这一说。
机器人会坏。 抓取任务里手指反复接触物体,减速器、编码器、指尖胶皮都是消耗品。而且一旦策略跑飞了撞到桌子,维修停机是按天算的。
场景布置耗时。 每采完一条,杯子得摆回原位,光照要保持一致,桌面要清干净。这个动作重复几千遍,采集效率被它锁死。
仿真绕开的正是这四条。它不能绕开的那部分,我们放到后面讲。
这个仓库提供了什么
README 开头一句话把定位说完了:这个项目基于 Isaac Lab 构建,用来在多种任务中仿真宇树机器人,便于数据采集、回放、生成与模型验证,并且可以和 xr_teleoperate 配合使用来采集数据集。
四个动词——采集、回放、生成、验证——就是这个仓库的四条主线。
机型与末端的组合
README 里那张任务表格是按「机型 × 末端执行器」组织的,四列分别是:
G1-29dof-gripper 两指夹爪
G1-29dof-dex3 三指灵巧手
G1-29dof-inspire Inspire 手
H1-2-inspire 另一款人形 + Inspire 手
这个二维结构本身就说明了一件事:在操作任务里,机器人本体和末端是可以解耦替换的两块。 同一个「把圆柱体拿起来放好」的任务,换个末端就是一套新的数据、新的动作空间。仓库里 tasks/common_observations/ 下的文件清单把这层结构映得很清楚:
g1_29dof_state.py 本体关节状态
h12_27dof_state.py 另一机型的本体状态
gripper_state.py 夹爪状态
dex3_state.py 三指手状态
inspire_state.py Inspire 手状态
camera_state.py 相机数据
本体一个文件、每种末端一个文件、相机一个文件——观测被拆成了正交的几块,组装成哪种配置就取哪几块。上面提到的「观测三件套」(图像 + 本体 + 末端),在这个目录里一一对上了号。
任务名是一套编码
任务名长这样:
Isaac-PickPlace-Cylinder-G129-Dex1-Joint
Isaac-PickPlace-RedBlock-G129-Dex3-Joint
Isaac-Stack-RgyBlock-G129-Dex1-Joint
Isaac-Move-Cylinder-G129-Dex1-Wholebody
Isaac-PickPlace-Cylinder-H12-27dof-Inspire-Joint
拆开看是「前缀 - 任务类型 - 操作对象 - 机型标识 - 末端 - 控制模式」。任务类型目前有拿放(PickPlace)、堆叠(Stack)、搬运(Move)三类,对象是圆柱体、红色方块、红绿黄三色方块。文件树里还能看到 README 表格中没有的 pick_redblock_into_drawer(把红块放进抽屉)目录。
结尾那一段是控制模式,README 特意点了一句:任务名里带 Wholebody 的才是移动操作任务,才能控制机器人走动;其余的 Joint 类任务机器人是站定不动的。想发移动指令,README 指向了仓库自带的 send_commands_8bit.py 和 send_commands_keyboard.py。
这种把配置信息编进名字的做法,好处是命令行里一眼能看出跑的是什么,坏处是名字很长。工程上这是个合算的交易——数据集攒到几十个任务之后,你会庆幸当初没起 task_01 这种名字。
相机是配置项,不是硬编码
tasks/common_config/camera_configs.py 的注释写的是「相机摆放相关配置」。相机位姿被抽成配置文件而不是写死在场景里,这个设计在下面讲数据生成时会显出价值。
下游的 unitree_lerobot 那边给了一条旁证:它的数据转换脚本支持一个叫 Unitree_G1_Dex1_Sim 的 robot_type,README 明确注明这是「用于 unitree_sim_isaaclab 中数据采集的机器人类型,头部配单视角相机」。仿真采的数据在下游有独立的身份标识,说明两边是认真对接过的,不是各写各的。
三种运行模式
sim_main.py 是唯一的入口,靠命令行参数切换模式。这个设计我挺喜欢——三种模式共享同一套场景和同一套观测代码,不容易出现「采集时的观测」和「回放时的观测」不一致这种隐蔽 bug。
模式一:遥操作采集
python sim_main.py --device cpu --enable_cameras \
--task Isaac-PickPlace-Cylinder-G129-Dex1-Joint \
--enable_dex1_dds --robot_type g129
--enable_cameras 打开相机,--enable_dex1_dds / --enable_dex3_dds 分别为两指夹爪、三指灵巧手启用 DDS 通道。
这里有个设计值得停一秒。README 的「重要提示」里写着:仿真器启动后,收发的 DDS 话题与真机完全一致,并且专门警告——如果同一网络里还有真机在跑,注意区分两者。
这句警告本身就是设计意图的最好说明。这个仿真环境在通信层面伪装成一台真机。你为真机写的遥操作程序、图像客户端、控制脚本,一行不改就能接到仿真上。这条 DDS 通道正是unitree_sdk2 那套通信机制,仓库的致谢里也确认了对 unitree_sdk2_python 的依赖。
代价是那句警告所指的风险:真机和仿真在同一个局域网里,话题名撞在一起,你以为在遥操作仿真,实际上真机也跟着动了。这不是设计缺陷,是「接口完全一致」这个优点的必然反面。
图像走的不是 DDS。image_server 模块的注释写明用 ZMQ 发布图像。这个分工很合理:DDS 适合高频小包的状态与指令,图像是大块二进制,另开一条通道更干净。README 也提醒了,配合 xr_teleoperate 采数据时,要把那边 image_server 的 IP 改成仿真运行的地址。
模式二:数据回放
python sim_main.py ... --replay --file_path "<数据集目录>"
--replay 把之前录的动作序列重新喂给仿真里的机器人。README 注明,这里用的数据集格式与通过 xr_teleoperate 遥操作录制的格式一致。
回放的直接用途是检查数据质量——哪条 episode 中途失败了、哪条动作抖得不像话,看一遍就知道。但它真正的价值在下一个模式。
模式三:数据生成
这是整个仓库里我认为最值得讲的一段。README 的原话是:在数据回放期间,通过修改光照条件和相机参数并重新采集图像数据,可以生成更多样的视觉特征用于数据增强,从而提升模型的泛化能力。
python sim_main.py ... --replay --file_path "<原始数据>" \
--generate_data --generate_data_dir "./data2"
配套开关有 --modify_light(改光照,需要相应调整 main 里的 update_light 函数)、--modify_camera(改相机参数,对应 batch_augment_cameras_by_name 函数)、--rerun_log(生成过程记录日志)。
把这件事的逻辑摊开:动作序列不变,只换光照和视角,重新渲染一遍图像。 同一条演示轨迹,就能变出很多条视觉上不同、动作上相同的训练数据。
这正是**域随机化(domain randomization)**的工程化形态。它背后的假设是:如果模型在光照、视角、颜色都乱变的情况下依然能完成任务,那它学到的就不是「像素点 (320,240) 是红的就往左伸手」这种脆弱的相关性,而是更接近「找到红块的位置」这件事本身。
真机上你做不到这个。你没法把同一条演示重新走一遍、只把灯光换掉——人的操作不可能复现到帧级一致。这是仿真相对真机的一个结构性优势,不是「便宜一点」的量变。
README 后面跟了一句很实在的提醒:如果要改光照或相机参数,请先仔细调试和测试参数,再进行大规模数据生成。这句话的潜台词大概每个做过数据管线的人都懂——参数没调好就批量生成,产出的是一整批废数据,而你要过很久才会发现。
代码结构:分层在哪里
unitree_sim_isaaclab/
├── action_provider 动作来源:读文件 / 收 DDS / 策略生成
├── dds DDS 通信模块
├── image_server 图像发布服务(ZMQ)
├── layeredcontrol 底层控制:取动作并写进虚拟环境
├── robots 机器人基础配置
├── tasks 任务相关
│ ├── common_config 相机摆放、机器人设置
│ ├── common_event 事件注册管理
│ ├── common_observations 各类观测数据获取
│ ├── common_scene 各任务的公共场景
│ ├── common_rewards 各任务的奖励
│ ├── common_termination 物体是否超出工作范围的判断
│ ├── g1_tasks G1 相关的全部任务
│ └── utils
├── tools USD 转换与修改工具
├── usd USD 模型文件
└── sim_main.py 主函数
action_provider 这个模块名起得好。它的注释说:提供读取文件动作、接收 DDS 动作、策略生成动作等接口,当前主要使用基于 DDS 的动作获取。目录下能看到 action_provider_dds.py、action_provider_replay.py、action_provider_wh_dds.py(对应 Wholebody)和一个 create_action_provider.py 工厂。
这一层是整个仓库的枢纽。 往下走,layeredcontrol 只管「拿到动作,写进仿真环境」,它不关心动作是人遥操作来的、从文件读的,还是模型推理出来的。往上走,三种数据来源被抽象成同一个接口。
这个抽象让上面那三种模式几乎是免费的——采集是 DDS 来源,回放是文件来源,模型验证是策略来源。想不通这一层,你会觉得 sim_main.py 一个入口塞三种模式很乱;想通了就会发现,模式切换只是换了个 provider。
tasks/common_termination/ 的注释是「判断不同任务中的物体是否超出指定工作范围」,示例代码里露出来的函数名是 reset_object_estimate。配合 dds/reset_pose_dds.py 和根目录的 reset_pose_test.py 看,物体位姿重置是被当成一等公民对待的。这也符合数据采集的实际节奏:采完一条就得把场景恢复原状,这个动作在真机上要靠人手,在仿真里是一行代码。
tools/ 下有 convert_urdf.py、edit_usda.py 这类文件。从文件名读出来的结论是:进 Isaac Sim 的资产要转成 USD 格式,仓库自带了从 URDF 转过去以及后续修改的工具链。还有 episode_writer.py(episode 落盘)、data_convert.py / data_json_load.py(格式转换与读取)、augmentation_utils.py(增强工具)、rerun_visualizer.py(可视化)。
新增一个任务的流程 README 也给了:在 common_scene 加场景配置,在 common_termination 加终止/重置判断,然后在 g1_tasks 下建目录,写 observations.py、terminations.py、环境配置类,最后在 __init__.py 里注册:
gym.register(
id="Isaac-PickPlace-Cylinder-G129-Dex1-Joint",
entry_point="isaaclab.envs:ManagerBasedRLEnv",
kwargs={...},
disable_env_checker=True,
)
用的是 gymnasium 的标准注册机制,entry_point 指向 Isaac Lab 的 ManagerBasedRLEnv。注册表这套模式在强化学习那个仓库里也见过——命令行 --task 后面那个字符串能生效,靠的就是它。
和强化学习训练环境的差别在哪
unitree_rl_gym 也是仿真环境,也在里面跑机器人,为什么要另起一个仓库?
因为目标不同,环境的设计取向就完全不同。
强化学习训练环境练的是运动控制——怎么走稳、怎么不摔。它的核心诉求是吞吐量:几千个机器人实例并行、关掉渲染只跑物理、地形随机生成。画面好不好看完全不重要,官方文档甚至直接建议训练时用无图形界面模式。
操作数据采集环境要的是另一套东西:
| 运动控制训练 | 操作数据采集 | |
|---|---|---|
| 数据从哪来 | 策略自己探索 | 人遥操作演示 |
| 视觉渲染 | 可以关掉 | 必须开,图像就是观测 |
| 物体交互 | 主要是脚与地面 | 手与物体的接触是任务核心 |
| 场景 | 地形程序化生成 | 桌面、物体、抽屉,逐个搭 |
| 并行度 | 越高越好 | 受人的操作速度限制 |
看这个仓库的具体设计就能对上:--enable_cameras 是常规参数不是可选项;usd/ 目录装着物体模型;common_scene/ 下每个任务一套场景;末端执行器有三种可选。这些在运动控制训练里全都不是重点。
反过来,unitree_rl_gym 里那些地形生成、并行环境数量的设计,在这个仓库里也看不到对应物。两个仓库解决的是具身智能栈里相邻但不同的两层问题:一个负责让机器人站得住走得动,一个负责让它知道该做什么。想把这两层的关系理清楚,具身智能这篇是个合适的起点。
下游:数据怎么变成模型
采出来的数据不能直接喂给训练框架,中间要转格式。这一段的承接方是 unitree_lerobot。
那边的 README 显示,原始数据是 JSON 格式,目录结构长这样:
datasets/
└── task_name/
├── episode_0001
│ ├── audios/ 音频
│ ├── colors/ 彩色图像
│ ├── depths/ 深度图像
│ └── data.json 状态与动作
├── episode_0002
└── ...
一个 episode 一个目录,图像按类型分文件夹,状态和动作统一进 data.json——正是文章开头说的那个二元组。
转换脚本 convert_unitree_json_to_lerobot.py 把它转成 LeRobot 格式。LeRobot 是 Hugging Face 那套机器人学习框架,转过去之后就能接上它支持的一系列模仿学习策略——unitree_lerobot 的 README 里给出了 ACT、Diffusion Policy、Pi0 等几种训练命令的示例。这些方法与 VLA 路线的关系,可以顺着视觉-语言-动作模型那篇往下读。
链路还有回程。unitree_lerobot 的 eval_robot/ 下有个 eval_g1_sim.py,README 说明它用于在 unitree_sim_isaaclab 仿真环境里跑推理测试,并且带一个 --save_data 选项——注释明确写着这个选项目前仅限仿真环境。
训完的模型回到同一个仿真环境里被评估,评估过程还能顺手再产生数据。 采集、训练、评估形成闭环,而闭环的两端都落在这个仿真环境上。它的 Release Note 里 v0.2 那条「新增仿真环境验证」,说的就是这件事。
顺带一提,unitree_lerobot 里还有个 PyQt 写的数据编辑器,能裁掉 episode 里多余的片段、删掉失败的那几条。这个工具的存在提醒了一件容易被忽略的事:采集出来的数据不是拿来就能用的,中间有一道人工质检。 一条中途失败的演示混进训练集,教给模型的就是错的东西。
仿真绕不开的那一关
把优势说完了,得说代价。
仿真里训出来的模型搬到真机上会掉性能,这个现象叫 sim-to-real gap。在操作任务上,它主要来自两处:
物理差异。 摩擦系数、接触刚度、物体质量分布,仿真器里都是近似模型。夹爪能不能夹稳一个圆柱体,恰恰对这些参数极其敏感。
视觉差异。 这一处在操作任务里往往比物理差异更棘手。渲染出来的图像和真实相机拍出来的图像,在噪声、动态范围、色彩响应、运动模糊上都不一样。真实相机有自动曝光、有滚动快门、有镜头畸变;渲染图像干净得不真实。模型在干净图像上学会的特征,换到真实成像上可能直接失效。
常见的缓解手段有两条,都不神秘:
一是域随机化——就是上面数据生成那一节做的事。既然不知道真实分布长什么样,那就把训练分布撑得足够宽,让真实情况落在里面。
二是真实数据与仿真数据混合训练。 仿真数据管量,真实数据管准。少量真机采的数据配上大量仿真数据,常常比纯仿真好得多。仿真在这里的角色不是替代真机采集,而是把真机采集的边际成本摊薄——原本需要一万条真机数据,现在可能一千条加上仿真就够了。
这两条我在事实源里读到了第一条的直接实现,第二条属于领域内的通行做法,仓库文档没有对此表态,我也不替它表态。
收束:这一格在整个栈里的位置
把具身智能这条栈从下往上摆:
硬件本体与关节驱动
↓
运动控制(走、站、平衡) ← unitree_rl_gym 这一层
↓
数据采集与仿真环境 ← unitree_sim_isaaclab 在这
↓
模仿学习训练与格式转换 ← unitree_lerobot
↓
任务理解与高层决策
unitree_sim_isaaclab 卡在中间那一格,两头都连着东西:上游接遥操作设备和真机通信协议,下游接训练框架的数据格式。它不训练模型,也不控制真机,它是这条链路上负责「把演示变成数据」的那一环。
读完这个仓库,我认为有三件事值得记住:
第一,具身智能当前最紧的瓶颈不在算法,在数据。 一个仓库把「采集、回放、生成、验证」四件事都做进去,而不是只做一个漂亮的物理引擎,这个取舍本身就说明了行业的焦点在哪。
第二,「与真机使用同一套 DDS 协议」是这个仿真环境最要紧的设计决策。 它带来的不是性能优势,而是代码复用——遥操作程序、图像客户端、部署脚本都不用为仿真单独写一份。同一个决策也带来了「别把真机和仿真放同一个网段」的风险,README 把它放在最前面警告,说明这个坑真实存在。
第三,数据生成那一步值得单独记住。 保持动作不变、只改光照和视角重新渲染——这个操作在真机上物理上不可能实现,是仿真独有的能力。它对应的是缓解 sim-to-real gap 的主流思路之一。
想接着往下看,遥操作数据采集是这条链路的上游入口,模仿学习与 LeRobot是下游出口,而强化学习那条运动控制路线和这条并行不悖——一台人形机器人身上,两条链路同时在跑。