1. 项目缘起:当开源机械臂遇上通用机器人模型
最近在折腾一个挺有意思的项目,核心是把一个开源的桌面级机械臂——LeRobot SO-101,和一个号称“通用机器人基础模型”的GR00T N1.5给撮合到一起,最后让它跑在一块Jetson AGX Thor开发板上。这事儿听起来有点缝合怪,但背后的逻辑其实挺清晰的:我们想验证一下,在资源受限的边缘设备上,一个经过针对性微调的大模型,到底能不能让一台便宜的机械臂变得更“聪明”一点。
LeRobot SO-101是一款基于ROS 2的开源协作机械臂,设计初衷就是给研究者和爱好者用的,价格亲民,文档也还算齐全。GR00T N1.5则是NVIDIA推出的一系列机器人基础模型中的一个版本,它被训练来理解和执行各种自然语言指令,并生成相应的机器人动作或控制指令。而Jetson AGX Thor,是NVIDIA面向机器人和边缘AI推出的新一代计算平台,算力强悍,专门为运行这类复杂的AI模型而生。
所以,这个项目的完整链条就是:获取模型 -> 准备数据 -> 微调模型以适应SO-101的物理特性 -> 将微调后的模型部署到Jetson AGX Thor -> 通过ROS 2桥接,让模型能直接控制机械臂执行任务。整个过程涉及机器学习、机器人操作系统、嵌入式部署等多个环节,任何一个环节卡住,整个流程就断了。我踩的坑,多半也分布在这些连接处。
2. 环境搭建:从零开始的“脏活累活”
微调和部署的第一步,永远是搭建一个稳定、可复现的开发环境。这一步看似基础,却埋着最多的“暗雷”。我的工作流主要在两个地方进行:一台拥有RTX 3090显卡的Ubuntu 22.04开发机(用于模型微调),以及最终的Jetson AGX Thor目标设备。
2.1 开发机环境配置:依赖管理与虚拟环境
在开发机上,我强烈建议使用Miniconda或Anaconda来管理Python环境。这能有效避免不同项目间依赖包版本冲突的问题。为这个项目专门创建一个环境是必须的。
conda create -n lerobot_gr00t python=3.10 -y conda activate lerobot_gr00t接下来安装PyTorch。这里有个关键点:GR00T模型库通常对PyTorch和CUDA版本有特定要求。根据NVIDIA官方文档和模型发布页面的说明,我选择了PyTorch 2.1.0与CUDA 11.8的组合,这个组合在RTX 30系显卡上兼容性和性能都比较好。
pip install torch==2.1.0 torchvision==0.16.0 torchaudio==2.1.0 --index-url https://download.pytorch.org/whl/cu118安装完PyTorch后,再安装GR00T模型所需的代码库和依赖。通常,你需要从GitHub克隆相应的仓库。
git clone https://github.com/NVIDIA/gr00t.git cd gr00t pip install -e . # 以可编辑模式安装,方便后续修改代码 pip install -r requirements.txt在这个过程中,你可能会遇到各种奇怪的依赖错误。一个常见的坑是ffmpeg。有些数据处理或演示工具会用到它。如果你遇到类似“bash: /path/to/ffmpeg:无法执行二进制文件: 可执行文件格式错误”这样的报错,这通常不是因为ffmpeg没装,而是你conda环境里的ffmpeg可能与系统环境有冲突,或者架构不对。最稳妥的解决办法是使用系统包管理器安装:
sudo apt update sudo apt install ffmpeg然后确保你的conda环境没有安装自己的ffmpeg,或者在调用时使用绝对路径/usr/bin/ffmpeg。
2.2 Jetson AGX Thor 初始设置
Jetson AGX Thor是一台ARM架构的设备,其软件生态与x86_64的开发机截然不同。绝对不能直接把开发机上的conda环境复制过去,二进制文件格式完全不兼容。
首先,在Thor上安装基础系统。NVIDIA提供了基于Ubuntu 22.04的JetPack SDK。你需要通过SDK Manager刷入系统镜像。这个过程比较耗时,但按照官方指南一步步来问题不大。刷机完成后,第一件事是更新系统并安装一些基础工具:
sudo apt update && sudo apt upgrade -y sudo apt install -y curl wget git build-essential cmake接下来是Python环境。在Jetson平台上,我倾向于使用pip和venv,而不是conda,因为ARM架构的conda包支持相对较少,容易出问题。
python3 -m venv ~/venvs/gr00t_thor source ~/venvs/gr00t_thor/bin/activate然后安装PyTorch。这是最关键也最麻烦的一步。你必须安装NVIDIA为Jetson平台预编译的PyTorch wheel包,版本要与JetPack中的CUDA版本严格匹配。例如,JetPack 6.0可能对应PyTorch 2.2.0。你需要从NVIDIA的官方论坛或开发者网站找到正确的下载链接。
# 示例命令,具体URL需根据实际版本查找 wget https://developer.download.nvidia.com/compute/redist/jp/v60/pytorch/torch-2.2.0-cp310-cp310-linux_aarch64.whl pip install torch-2.2.0-cp310-cp310-linux_aarch64.whl安装成功后,同样克隆GR00T的代码库,并安装其Python依赖。注意,Jetson上编译某些依赖(如带CUDA扩展的包)可能非常慢,甚至失败。你需要有耐心,并且仔细查看错误日志,有时需要手动安装一些系统库(如libopenblas-dev)来解决问题。
2.3 ROS 2 Humble 安装与配置
LeRobot SO-101依赖ROS 2进行控制和通信。我们选择ROS 2 Humble版本,因为它与Ubuntu 22.04是长期支持组合。在开发机和Jetson AGX Thor上都需要安装ROS 2。
安装过程遵循官方步骤即可:
# 设置locale sudo apt update && sudo apt install locales -y sudo locale-gen en_US en_US.UTF-8 sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 export LANG=en_US.UTF-8 # 添加ROS 2仓库 sudo apt install software-properties-common -y sudo add-apt-repository universe sudo apt update && sudo apt install curl -y sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(. /etc/os-release && echo $UBUNTU_CODENAME) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null # 安装ROS 2核心包 sudo apt update sudo apt install ros-humble-desktop -y # 配置环境变量 source /opt/ros/humble/setup.bash echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc安装完成后,在开发机上,你需要克隆LeRobot SO-101的ROS 2工作空间,并编译。这能确保你拥有控制机械臂的所有消息、服务和动作接口定义。
mkdir -p ~/lerobot_ws/src cd ~/lerobot_ws/src git clone https://github.com/lerobot/so101_ros2.git cd ~/lerobot_ws rosdep install --from-paths src --ignore-src -r -y colcon build --symlink-install在Jetson AGX Thor上,你也需要编译同样的工作空间,因为最终运行的控制节点是在Thor上。确保两边的ROS 2包版本一致,避免消息格式不兼容。
3. 数据准备与模型微调:教会模型认识“新手臂”
GR00T N1.5是一个预训练好的通用模型,但它对LeRobot SO-101这个具体的机械臂一无所知。微调的目的,就是用SO-101的数据“教”它,让模型学会将抽象指令(如“拿起那个红色的方块”)映射到SO-101这个特定机械臂的关节角度或末端执行器位姿上。
3.1 数据收集策略
理想情况下,你需要一个SO-101机械臂的真实工作环境来收集演示数据。数据应该包括:
- 观测(Observation):通常是多视角的RGB图像或RGB-D点云,展示了当前场景。
- 指令(Language Instruction):自然语言描述的任务,如“将积木移动到桌子左上角”。
- 动作(Action):机械臂执行该任务时,一系列的控制命令。对于SO-101,这可能是关节位置(joint positions)、关节速度(joint velocities),或者是末端执行器的位姿(pose)。
如果你没有真实的机械臂,LeRobot项目可能提供了仿真环境(如lerobot仿真环境搭建相关的资源)和预录制的数据集(如lerobot v3数据集)。使用仿真数据是快速启动项目的有效方式。你需要将这些数据转换成GR00T模型训练所要求的格式。通常,GR00T期望数据是npz或hdf5文件,包含obs,language_instruction,action等键值对。
一个简单的数据转换脚本框架如下:
import h5py import numpy as np from your_data_loader import load_lerobot_demos # 假设的LeRobot数据加载函数 demos = load_lerobot_demos(‘path/to/lerobot/v3/data‘) with h5py.File(‘formatted_gr00t_data.hdf5‘, ‘w‘) as f: grp = f.create_group(‘data‘) for i, demo in enumerate(demos): ep_grp = grp.create_group(f‘episode_{i}‘) # 假设demo是字典,包含‘images‘, ‘instruction‘, ‘joint_positions‘ ep_grp.create_dataset(‘obs/image‘, data=demo[‘images‘]) # 可能需要调整维度 ep_grp.attrs[‘language_instruction‘] = demo[‘instruction‘] ep_grp.create_dataset(‘action‘, data=demo[‘joint_positions‘])注意:数据对齐是关键。
obs的每一帧必须与action的每一帧严格对应。动作数据是SO-101实际执行时发出的命令,还是事后标注的“理想动作”,这会影响模型学习的效果。通常使用离线演示数据中的“专家动作”。
3.2 微调流程与关键参数
GR00T的微调通常基于其提供的训练脚本。你需要准备一个配置文件(通常是YAML格式),指定模型参数、数据路径、训练超参数等。
# config/finetune_so101.yaml model: name: “gr00t_n1_5” pretrained_path: “/path/to/pretrained/gr00t_n1_5.pt” data: train_path: “/path/to/formatted_gr00t_data.hdf5” batch_size: 16 num_workers: 4 training: num_epochs: 50 learning_rate: 1e-5 optimizer: “adamw” weight_decay: 0.01 # 输出配置 output_dir: “./output/finetuned_so101”然后运行训练命令:
python -m gr00t.train.finetune \ --config config/finetune_so101.yaml \ --device cuda:0微调过程中需要监控的指标包括训练损失(loss)和验证损失。更重要的是,要定期进行“可视化评估”——让模型在验证集场景中生成动作序列,并在仿真(或安全环境下)执行,直观地看它是否学会了任务。仅仅损失下降并不代表模型真的学会了控制SO-101。
关键技巧:
- 学习率要小:微调预训练大模型,学习率通常设置得非常小(如1e-5到1e-6),以免破坏模型已有的通用知识。
- 冻结部分层:可以考虑冻结视觉编码器(Visual Encoder)的权重,只训练后续的语言-动作映射层(Adapter或Policy Head),这能加快训练并防止过拟合。
- 数据增强:对输入图像进行随机的色彩抖动、裁剪、旋转等增强,可以提高模型的泛化能力。
- 动作标准化:将SO-101的动作数据(如关节角度)进行标准化处理(减均值,除以标准差),有助于模型稳定训练。
4. 模型部署与优化:让模型在边缘设备上“跑起来”
将微调好的模型部署到Jetson AGX Thor上,并达到可用的推理速度,是另一个挑战。模型文件可能很大,且Thor的算力虽强,但相比服务器GPU仍有差距。
4.1 模型转换与量化
首先,你需要将训练好的PyTorch模型(.pt或.pth文件)导出为Thor上更高效的推理格式。NVIDIA推荐使用TensorRT进行加速。
步骤通常是:PyTorch -> ONNX -> TensorRT。
- 导出ONNX:使用PyTorch的
torch.onnx.export函数。这里需要仔细定义模型的输入(如图像张量、语言指令文本)和输出(动作张量)的维度。一个常见的坑是动态轴(Dynamic Axes)的设置,特别是批处理大小(batch size)和序列长度。
import torch from your_model_loader import load_finetuned_model model = load_finetuned_model(‘./output/finetuned_so101/best_model.pt‘) model.eval() # 示例输入 dummy_image = torch.randn(1, 3, 224, 224).cuda() # 假设输入图像是224x224 dummy_text = [“pick up the block”] # 导出ONNX,注意处理文本输入(可能需要先通过tokenizer) # 这里简化处理,实际需根据模型前向传播函数调整 torch.onnx.export( model, (dummy_image, dummy_text), “gr00t_so101.onnx“, input_names=[“image“, “text“], output_names=[“action“], dynamic_axes={ “image“: {0: “batch_size“}, # 批处理维度动态 “text“: {0: “batch_size“}, “action“: {0: “batch_size“} }, opset_version=14 )- 转换为TensorRT:在Jetson AGX Thor上,使用
trtexec工具将ONNX模型转换为TensorRT引擎(.engine文件)。这一步可以进行量化,以进一步提升速度、减少内存占用。
# 在Jetson AGX Thor上执行 /usr/src/tensorrt/bin/trtexec \ --onnx=gr00t_so101.onnx \ --saveEngine=gr00t_so101_fp16.engine \ --fp16 \ --workspace=2048 # 指定显存工作空间大小这里使用了--fp16进行半精度浮点数量化,能在几乎不损失精度的情况下大幅提升速度。如果模型仍然太大或速度不够,可以尝试更激进的--int8量化,但这通常需要校准数据集,过程更复杂。
4.2 部署架构设计:ROS 2节点与模型服务
模型在Thor上准备好后,我们需要将它集成到ROS 2系统中,使其能接收指令、处理传感器数据、并发布控制命令。一个典型的架构是设计一个专门的gr00t_policy_nodeROS 2节点。
这个节点的职责是:
- 订阅(Subscribe):订阅相机话题(如
/camera/color/image_raw)获取实时图像,订阅指令话题(如/task_command)获取自然语言指令。 - 推理(Inference):将图像和指令预处理后,送入TensorRT引擎进行推理,得到预测的动作(如SO-101的7个关节目标角度)。
- 发布(Publish):将预测的动作发布到控制话题(如
/so101/joint_trajectory_controller/joint_trajectory),由底层的控制器执行。
节点核心循环的伪代码如下:
# gr00t_policy_node.py 核心片段 import rclpy from rclpy.node import Node from sensor_msgs.msg import Image from std_msgs.msg import String from trajectory_msgs.msg import JointTrajectory, JointTrajectoryPoint import cv2 import numpy as np import PyTensorRT as trt # 假设的TensorRT Python绑定 class GR00TPolicyNode(Node): def __init__(self): super().__init__(‘gr00t_policy_node‘) # 订阅 self.image_sub = self.create_subscription(Image, ‘/camera/image‘, self.image_callback, 10) self.cmd_sub = self.create_subscription(String, ‘/task_command‘, self.command_callback, 10) # 发布 self.action_pub = self.create_publisher(JointTrajectory, ‘/so101/joint_trajectory‘, 10) self.current_image = None self.current_command = “” # 加载TensorRT引擎 self.trt_engine = self.load_trt_engine(‘gr00t_so101_fp16.engine‘) self.trt_context = self.trt_engine.create_execution_context() # 创建定时器,以固定频率运行策略 self.timer = self.create_timer(0.1, self.policy_loop) # 10Hz def policy_loop(self): if self.current_image is not None and self.current_command: # 1. 预处理图像和文本 processed_img = self.preprocess_image(self.current_image) tokenized_text = self.tokenize_text(self.current_command) # 2. 准备TensorRT输入绑定 # ... (分配设备内存,拷贝数据) # 3. 执行推理 self.trt_context.execute_v2(bindings) # 4. 获取输出动作 joint_angles = self.get_output_from_binding() # 5. 封装为ROS 2消息并发布 traj_msg = JointTrajectory() traj_msg.joint_names = [‘joint1‘, ‘joint2‘, ...] # SO-101关节名 point = JointTrajectoryPoint() point.positions = joint_angles.tolist() point.time_from_start.sec = 1 # 设置动作时间 traj_msg.points.append(point) self.action_pub.publish(traj_msg)性能优化点:
- 流水线(Pipeline):图像预处理(缩放、归一化)和推理可以异步进行,避免在回调函数中做耗时操作阻塞ROS 2通信。
- 批处理(Batching):如果可能,收集几帧图像或指令一起推理,能更充分利用TensorRT的并行计算能力。但实时控制通常要求低延迟,批处理大小设为1是常见选择。
- 内存复用:为TensorRT的输入输出绑定预分配GPU内存,避免在循环中反复分配释放。
5. 系统集成与实测挑战
当模型节点写好,机械臂硬件连接好后,真正的挑战才刚刚开始。系统集成是将所有独立模块串联成可靠工作流的过程,这里充满了接口不一致、时序问题和资源竞争。
5.1 ROS 2 通信与坐标变换
LeRobot SO-101的ROS 2驱动会发布其关节状态(/joint_states)和提供控制接口。我们的gr00t_policy_node需要与之对齐。
- 话题与服务匹配:确保你发布控制命令的话题名称、消息类型与SO-101控制器订阅的完全一致。仔细查看SO-101的启动文件或参数服务器中的配置。
- 坐标框架(TF):这是机器人学中的经典难题。相机有它的坐标系(
camera_color_optical_frame),机械臂底座有基坐标系(base_link),末端有工具坐标系(tool0)。GR00T模型预测的动作,是在哪个坐标系下?是关节空间(Joint Space)还是末端执行器的操作空间(Task Space)?如果模型输出的是末端位姿(x, y, z, roll, pitch, yaw),你需要通过机器人的运动学求解器(Kinematics Solver)将其转换为关节角度。SO-101的ROS包中应该包含robot_state_publisher和运动学库(如moveit),你需要正确配置TF树,并可能调用逆运动学(IK)服务。
一个常见的集成错误是忽略了单位(米 vs. 毫米)或坐标系朝向(ROS常用的是Z轴向上,而某些视觉模型可能默认Y轴向上)。务必在RViz中可视化所有坐标系,确保它们的关系正确。
5.2 实时性与延迟管理
从图像采集到控制命令发出,整个闭环的延迟必须足够小,机械臂的运动才会流畅、稳定。你需要测量并优化这个流水线:
- 图像采集与传输延迟:USB相机的驱动、图像压缩/传输到ROS话题会有延迟。考虑使用压缩图像话题或降低分辨率。
- 模型推理延迟:使用
/usr/src/tensorrt/bin/trtexec的--dumpProfile选项分析模型各层耗时,或者直接在Python代码中测量execute_v2调用的时间。FP16量化通常能显著降低延迟。 - ROS 2通信延迟:使用
ros2 topic hz /your/control/topic查看实际发布频率。确保你的节点运行频率(create_timer的参数)是稳定且可达的。 - 底层控制延迟:SO-101的底层控制器(如
joint_trajectory_controller)接收新指令并驱动电机也需要时间。
如果总延迟超过200-300毫秒,对于快速抓取等任务可能就不可接受了。优化手段包括:使用更轻量级的图像编码、尝试INT8量化、将节点设置为实时优先级(需小心)、甚至考虑使用ROS 2的Real-Time特性。
5.3 安全与异常处理
让一个AI模型直接控制物理机械臂,安全是重中之重。
- 运动范围限制:在将模型输出的关节角度发布出去之前,必须进行限幅(clamp),确保其在SO-101每个关节的安全软限位(soft limit)之内,防止机械臂撞到自身或外界。
- 异常指令过滤:对于模型输出的明显异常值(如NaN或极大的数值),要有检测和过滤机制,可以丢弃该指令并保持上一个有效姿态,或者让机械臂回到安全位置(home position)。
- 急停(E-Stop)集成:必须有一个外部急停开关,并且你的ROS 2节点应该订阅一个
/e_stop之类的话题,一旦收到信号,立即停止发布任何控制命令。 - 看门狗(Watchdog):设计一个简单的看门狗机制。如果超过一定时间没有收到新的图像或没有成功发布控制指令,则让节点进入安全模式,停止发布命令或发布零速度命令。
6. 效果评估与迭代改进
部署完成后,你需要系统地评估微调后模型在真实SO-101上的表现。不能只看它“动起来了”,而要量化其性能。
定性评估:
- 任务成功率:给定一系列指令(如“拿起杯子”、“推到左边”),在多次试验中统计成功完成的次数。
- 动作流畅度:观察机械臂运动是否平滑、有无剧烈抖动或卡顿。这反映了模型预测动作序列的连贯性。
- 泛化能力:改变物体位置、光照条件、背景,看模型是否还能完成任务。
定量评估:
- 推理延迟:记录从图像输入到动作输出的平均时间及方差。
- 控制误差:对于到达指定位置的任务,可以用动作捕捉系统或AR标记测量末端执行器实际到达位置与目标位置的误差。
- 数据记录与回放:使用
ros2 bag记录每次测试的传感器数据、指令和发出的动作。这不仅能用于复盘分析,更是后续迭代微调模型的宝贵数据。
迭代循环: 根据评估结果,你可能会发现模型在某些场景下表现不佳。这时就需要回到第3步,收集这些失败场景的数据(可能是手动操控机械臂完成该任务,录制成新的演示数据),加入到训练集中,重新进行微调。这个“部署-评估-收集数据-再训练”的循环,是让机器人系统在实际环境中不断进化的关键。
整个项目走下来,感觉就像在搭一个极其精密的乐高,从软件环境的一砖一瓦,到数据管道的涓涓细流,再到模型推理的引擎轰鸣,最后到机械臂实体的一举一动,任何一个接口的松动、任何一个时序的错位,都会让整个系统“趴窝”。但当你看到GR00T模型通过你微调的“大脑”,理解了一句简单的指令,并驱动着LeRobot SO-101准确无误地完成一个抓取动作时,那种所有环节严丝合缝对接成功的满足感,是对所有折腾的最好回报。这个过程里,最重要的经验可能就是:日志要详细,版本要控制,备份要频繁,以及,对边缘设备保持足够的耐心。