lerobot搭建

设备查询

本文是记录ubuntu系统lerobot试验的快捷命令,方便开始负责执行设备,不会介绍为什么?

python -m lerobot.find_port
python -m lerobot.find_cameras

机器标定

从臂标定

python -m lerobot.calibrate \
    --robot.type=so101_follower \
    --robot.port=/dev/ttyACM0 \
    --robot.id=R12252801

主臂标定

python -m lerobot.calibrate \
    --teleop.type=so101_leader \
    --teleop.port=/dev/ttyACM1 \
    --teleop.id=R07252608

标定的文件路径默认存储在/home/laumy/.cache/huggingface/lerobot/calibration。如果要更改路径。

--robot.calibration_dir=/home/laumy/lerobot/calibrations
--teleop.calibration_dir=/home/laumy/lerobot/calibrations

如果指定了路径,后续的代码都需要指定。

示教

python -m lerobot.teleoperate \
    --robot.type=so101_follower \
    --robot.port=/dev/ttyACM0 \
    --robot.id=R12252801 \
    --teleop.type=so101_leader \
    --teleop.port=/dev/ttyACM1 \
    --teleop.id=R07252608

数据采集

python -m lerobot.record \
    --robot.disable_torque_on_disconnect=true \
    --robot.type=so101_follower \
    --robot.port=/dev/ttyACM0 \
    --robot.id=R12252801 \
    --robot.cameras="{ handeye: {type: opencv, index_or_path: 6, width: 640, height: 480, fps: 30}, fixed: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}}" \
    --teleop.type=so101_leader \
    --teleop.port=/dev/ttyACM1 \
    --teleop.id=R07252608 \
    --dataset.repo_id=laumy/record-07271148\
    --dataset.num_episodes=10 \
    --dataset.reset_time_s=5 \
    --dataset.push_to_hub=false \
    --dataset.single_task="Grab the cube" \
    --display_data=true

repo_id必现为"用户名/数据集名"格式,代码中会检测是否有"/"。

数据存储默认的路径为/home/laumy/.cache/huggingface/lerobot/laumy/record-07271148如果要修改路径的话,加上下面参数。

--dataset.root=/home/laumy/lerobot/data/record_07271148

如果要继续上一次的录制,可以添加在上面命令基础上加上: --resume=true

如果数据采集过程中,突然异常终止,无法恢复报错如下时。

Traceback (most recent call last):
  File "/home/laumy/lerobot/./src/lerobot/scripts/train.py", line 291, in <module>
    train()
  File "/home/laumy/lerobot/src/lerobot/configs/parser.py", line 226, in wrapper_inner
    response = fn(cfg, *args, **kwargs)
  File "/home/laumy/lerobot/./src/lerobot/scripts/train.py", line 128, in train
    dataset = make_dataset(cfg)
  File "/home/laumy/lerobot/src/lerobot/datasets/factory.py", line 90, in make_dataset
    dataset = LeRobotDataset(
  File "/home/laumy/lerobot/src/lerobot/datasets/lerobot_dataset.py", line 489, in __init__
    check_timestamps_sync(timestamps, episode_indices, ep_data_index_np, self.fps, self.tolerance_s)
  File "/home/laumy/lerobot/src/lerobot/datasets/utils.py", line 585, in check_timestamps_sync
    raise ValueError(
ValueError: One or several timestamps unexpectedly violate the tolerance inside episode range.
                This might be due to synchronization issues during data collection.

[{'diff': np.float32(-15.966666),
  'episode_index': 90,
  'timestamps': [np.float32(15.966666), np.float32(0.0)]}]

解决办法就是到数据集路径下~/.cache/huggingface/lerobot/laumy/record-07261516/检查数据,一般就是数据集不匹配了,打开data和videos下面的数据,看看数量是否和meta里面的jsonl对齐了,因为程序采集过程中异常终止,可能只写了data目录或videos目录,但是meta目录下没来得及写就崩溃退出,把最新的一组数据删除了并把meta下面的所有文件数量对齐就可以了。

需要注意的时,info.json中的数据要特别进行修改,下面是需要修改info.json的地方。

{
    "codebase_version": "v2.1",
    "robot_type": "so101_follower",
    "total_episodes": 54,                ------采集的周期
    "total_frames": 21243,             ------采集的总长度,是episodes.jsonl的总和,这个可以通过后面的脚步来计算确认
    "total_tasks": 1,
    "total_videos": 108,                  -----总的视频数量,一般是episodes * 2
    "total_chunks": 1,
    "chunks_size": 1000,
    "fps": 30,
    "splits": {
        "train": "0:54"                       -----用于划分给训练集的数据,后面的数据一般要和total_episodes一致。
    },

如果meta/info.json中total_frames是所有视频的总帧数,是从meta/episodes.jsonl中所有的length字段获取的总和,如果做了调整要重新计算更新一下这个total_frames。下面是重新计算的脚步。

import os
import json
import argparse
from pathlib import Path

def calibrate_total_frames(dataset_root):
    dataset_root = Path(dataset_root)
    # 手动指定 INFO_PATH 和 EPISODES_PATH
    info_path = dataset_root / "info.json"  # 替换为实际的元数据文件路径
    episodes_path = dataset_root / "episodes.jsonl"  # 替换为实际的剧集信息文件路径

    # 检查元数据文件是否存在
    if not info_path.exists():
        print(f"Metadata file {info_path} does not exist.")
        return

    # 检查剧集信息文件是否存在
    if not episodes_path.exists():
        print(f"Episodes file {episodes_path} does not exist.")
        return

    # 加载元数据python ./src/lerobot/scripts/train.py \
  --dataset.repo_id=${HF_USER}/record-07271539 \
  --policy.type=act \
  --output_dir=outputs/train/weigh_07271539 \
  --job_name=act_so101_test \
  --policy.device=cuda \
  --policy.push_to_hub=false \
  --wandb.enable=false
    with open(info_path, 'r') as f:
        info = json.load(f)

    # 重新计算 total_frames
    total_frames = 0
    with open(episodes_path, 'r') as f:
        for line in f:
            episode = json.loads(line)
            total_frames += episode.get('length', 0)

    # 更新元数据
#    info["total_frames"] = total_frames
#    with open(info_path, 'w') as f:
#        json.dump(info, f, indent=4)

    print(f"Total frames calibrated to {total_frames}")

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Calibrate total frames in dataset metadata.')
    parser.add_argument('dataset_root', type=str, help='Root path of the dataset')
    args = parser.parse_args()
    calibrate_total_frames(args.dataset_root)

训练

python ./src/lerobot/scripts/train.py \
  --dataset.repo_id=laumy/record-07271539 \
  --policy.type=act \
  --output_dir=outputs/train/weigh_07271539 \
  --job_name=act_so101_test \
  --policy.device=cuda \
  --policy.push_to_hub=false \
  --wandb.enable=false

训练过程如果不小心终止了,执行下面命令可以接着上次的训练。

python ./src/lerobot/scripts/train.py \
  --config_path=outputs/train/weigh_07271539/checkpoints/last/pretrained_model/train_config.json \
  --resume=true --steps=200000

--steps参数表示迭代次数,这里表示200K次。

测试

python -m lerobot.record  \
  --robot.type=so101_follower \
  --robot.disable_torque_on_disconnect=true \
  --robot.port=/dev/ttyACM0 \
--robot.cameras="{ handeye: {type: opencv, index_or_path: 6, width: 640, height: 480, fps: 30}, fixed: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}}" \
  --robot.id=R12252801 \
  --display_data=false \
  --dataset.single_task="Put brick into the box" \
  --policy.path=outputs/weigh_07280842/pretrained_model \
  --dataset.episode_time_s=240  \
  --dataset.repo_id=laumy/eval_so101_07280842


默认录制时长是60s,60S后会停止,如果要改长加上--dataset.episode_time_s=640


class ACTConfig(PreTrainedConfig):
    n_obs_steps: int = 1
    chunk_size: int = 100
    n_action_steps: int = 1
  • n_obs_steps:传递给策略的观测时间窗口大小,具体是指包含当前步骤在内的连续观测步数。也就是说我要观测几步动作来预测接下来的动作。
  • chunk_size=100: 动作预测分块大小,为单次模型前向传播预测的动作序列总长度(以环境步数为单位),简单理解就是预测的动作步数,实际执行的动作会从这里面选择。
  • n_action_steps:从预测的动作分块中实际执行的步数,所以必须满足 n_action_steps ≤ chunk_size。
观测序列 → [O_t-n, ..., O_t] → 策略网络 → [A1...A100] → 执行[A1...A50]
                    ↑                     ↑                               ↑                             ↑
        n_obs_steps=3                             chunk_size=1         n_action_steps=50

一般来说如果是抓取固定的物体,n_obs_steps(3~5)设置可设较小值,chun_size设置大一点以减少策略调用频率,n_action_steps一般取chunk_size/2。如果是动态的环境(如装配、堆叠)n_obs_steps建议设置大一点10~20,chunk_size也缩减一点。

  • action.xxx: 代表的是要设定执行的动作,也就是主臂或者模型推理处理发送的指令。
  • observation.xxx:实际执行的动作。

一般情况下observation要跟action越吻合越好。

其他(废弃)

用于记录,可不用看

mac

找uart和摄像头

export HF_USER=laumy
python -m lerobot.find_port
python -m lerobot.find_cameras

从臂标定

python -m lerobot.calibrate \
    --robot.type=so101_follower \
    --robot.port=/dev/tty.usbmodem5A7A0576331 \
    --robot.id=R12252801

主臂标定

python -m lerobot.calibrate \
    --teleop.type=so101_leader \
    --teleop.port=/dev/tty.usbmodem5A7A0582001 \
    --teleop.id=R07252608

示教

python -m lerobot.teleoperate \
    --robot.type=so101_follower \
    --robot.port=/dev/ttyACM0 \
    --robot.id=R12252801 \
    --teleop.type=so101_leader \
    --teleop.port=/dev/ttyACM1 \
    --teleop.id=R07252608

采集数据

python -m lerobot.record \
    --robot.disable_torque_on_disconnect=true \
    --robot.type=so101_follower \
    --robot.port=/dev/ttyACM0 \
    --robot.id=R12252801 \
    --robot.cameras="{ handeye: {type: opencv, index_or_path: 8, width: 640, height: 360, fps: 30}, fixed: {type: opencv, index_or_path: 10, width: 640, height: 360, fps: 30}}" \
    --teleop.type=so101_leader \
    --teleop.port=/dev/ttyACM1 \
    --teleop.id=R07252608 \
    --dataset.repo_id=${HF_USER}/record-test \
    --dataset.num_episodes=10 \
    --dataset.reset_time_s=5 \
    --dataset.push_to_hub=false \
    --dataset.single_task="Grab the cube"

训练

python ./src/lerobot/scripts/train.py \
  --dataset.repo_id=${HF_USER}/data_07181406 \
  --policy.type=act \
  --output_dir=outputs/train/weigh_07181406 \
  --job_name=act_so101_test \
  --policy.device=cuda \
  --policy.push_to_hub=false \
  --wandb.enable=false

测试

python -m lerobot.record  \
  --robot.type=so101_follower \
  --robot.disable_torque_on_disconnect=true \
  --robot.port=/dev/ttyACM0 \
--robot.cameras="{ handeye: {type: opencv, index_or_path: 8, width: 640, height: 360, fps: 30}, fixed: {type: opencv, index_or_path: 10, width: 640, height: 360, fps: 30}}" \
  --robot.id=R12252801 \
  --display_data=false \
  --dataset.single_task="Put lego brick into the transparent box" \
  --policy.path=outputs/100000/pretrained_model \
  --dataset.repo_id=${HF_USER}/eval_so101

windows

找uart和摄像头

set HF_USER=86152
python -m lerobot.find_port
python -m lerobot.find_cameras

从臂标定

python -m lerobot.calibrate `
    --robot.type=so101_follower `
    --robot.port=COM5 `
    --robot.id=R12252801

主臂标定

python -m lerobot.calibrate `
    --teleop.type=so101_leader `
    --teleop.port=COM6 `
    --teleop.id=R07252608

示教

python -m lerobot.teleoperate `
    --robot.type=so101_follower `
    --robot.port=COM5 `
    --robot.id=R12252801 `
    --teleop.type=so101_leader `
    --teleop.port=COM6 `
    --teleop.id=R07252608

录制

t

训练

python src/lerobot/scripts/train.py `
  --dataset.repo_id=86152/data_07181406 `
  --policy.type=act `
  --output_dir=outputs/train/weigh_07181406 `
  --job_name=act_so101_test `
  --policy.device=cuda `
  --policy.push_to_hub=false `
  --wandb.enable=false

测试

python -m lerobot.record  `
  --robot.type=so101_follower `
  --robot.disable_torque_on_disconnect=true `
  --robot.port=COM5 `
  --robot.cameras="{ handeye: {type: opencv, index_or_path: 1, width: 640, height: 360, fps: 30}, fixed: {type: opencv, index_or_path: 2, width: 640, height: 360, fps: 30}}" `
  --robot.id=R12252801 `
  --display_data=false `
  --dataset.single_task="Put lego brick into the transparent box" `
  --dataset.repo_id=${HF_USER}/eval_so101 `
  --policy.path=outputs/train/weigh_07172300/checkpoints/last/pretrained_model

unbuntu

export HF_USER=laumy
python lerobot/scripts/find_motors_bus_port.py
python lerobot/common/robot_devices/cameras/opencv.py
python lerobot/scripts/control_robot.py \
  --robot.type=so101 \
  --control.type=record \
  --control.fps=30 \
  --control.single_task="Grasp a lego block and put it in the bin." \
  --control.repo_id=${HF_USER}/so101_test \
  --control.tags='["so101","tutorial"]' \
  --control.warmup_time_s=5 \
  --control.episode_time_s=30 \
  --control.reset_time_s=5 \
  --control.num_episodes=10 \
  --control.push_to_hub=false

如果要继续上一次的录制,可以添加在上面命令基础上加上:--control.resume=true

可视化训练数据

python lerobot/scripts/visualize_dataset_html.py --repo-id ${HF_USER}/so101_test

拷贝数据到服务器

scp -P 18620 so101_test.tar.gz root@connect.bjb1.seetacloud.com:~/
python lerobot/scripts/train.py \
  --dataset.repo_id=${HF_USER}/so101_test \
  --policy.type=act \
  --output_dir=outputs/train/act_so101_test0717 \
  --job_name=act_so101_test \
  --policy.device=cuda \
  --wandb.enable=false
python lerobot/scripts/control_robot.py \
  --robot.type=so101 \
  --control.type=record \
  --control.fps=30 \
  --control.single_task="Grasp a lego block and put it in the bin." \
  --control.repo_id=${HF_USER}/eval_act_so101_test \
  --control.tags='["tutorial"]' \
  --control.warmup_time_s=5 \
  --control.episode_time_s=30 \
  --control.reset_time_s=30 \
  --control.num_episodes=10 \
  --control.push_to_hub=false \
  --control.policy.path=outputs/train/act_so101_test/checkpoints/last/pretrained_model