LeRobot入门教程 13:LeRobot 微调 (LoRA)

目录:00_LeRobot入门教程_目录.md

1. 什么时候需要微调

Hub 上的 pi0_libero_finetuned 是在 LIBERO 全家桶上训练的。如果你要做的是别的任务、别的物体、别的机械臂,成功率会掉得厉害。这时需要用自己的演示数据继续训练——这就是微调。

微调要解决的核心矛盾:π0 有几十亿参数,全量微调动辄要几张 A100 和几天的数据。所以工业界普遍用 LoRA。

2. LoRA 是什么

LoRA(Low-Rank Adaptation)不改原来的权重,而是在旁边挂两个小矩阵,用它们的乘积去「修正」原权重:

W' = W + (alpha / r) · B · A
记号含义本册取值
W原来冻结的权重不动
A, B新增的低秩矩阵秩 r
r秩,越小越省8
alpha缩放系数,控制修正幅度16

效果:可训练参数从几十亿降到几百万,显存需求大幅下降,训练完只需要保存几十 MB 的 LoRA 权重。

--peft.target_modules=all-linear 的意思是「所有线性层都挂 LoRA」。范围越大效果越好、越费显存;只想省显存可以只挂 attention 的 q_proj,k_proj,v_proj。

3. 训练命令

!rm -rf /root/autodl-tmp/train_demo && lerobot-train --policy.path=lerobot/pi0_libero_finetuned --dataset.repo_id=lerobot/libero --output_dir=/root/autodl-tmp/train_demo --steps=3 --batch_size=1 --num_workers=0 --save_freq=0 --log_freq=1 --env_eval_freq=0 --eval_steps=0 --wandb.enable=false --policy.push_to_hub=false --peft.target_modules=all-linear --peft.r=8 --peft.lora_alpha=16

stderr:

/root/miniconda3/lib/python3.12/pty.py:95: DeprecationWarning: This process (pid=68158) is multi-threaded, use of forkpty() may lead to deadlocks in the child.
  pid, fd = os.forkpty()
INFO 2026-10-15 17:10:34 ot_train.py:272 {'batch_size': 1,
 'checkpoint_path': None,
 'cudnn_deterministic': False,
 'dataloader_multiprocessing_context': 'spawn',
 'dataset': {'depth_output_unit': 'mm',
             'episodes': None,
             'eval_split': 0.0,
             'image_transforms': {'enable': False,
                                  'max_num_transforms': 3,
                                  'random_order': False,
                                  'tfs': {'affine': {'kwargs': {'degrees': [-5.0,
                                                                            5.0],
                                                                'translate': [0.05,
                                                                              0.05]},
                                                     'type': 'RandomAffine',
                                                     'weight': 1.0},
                                          'brightness': {'kwargs': {'brightness': [0.8,
                                                                                   1.2]},
                                                         'type': 'ColorJitter',
                                                         'weight': 1.0},
                                          'contrast': {'kwargs': {'contrast': [0.8,
                                                                               1.2]},
                                                       'type': 'ColorJitter',
                                                       'weight': 1.0},
                                          'hue': {'kwargs': {'hue': [-0.05,
                                                                     0.05]},
                                                  'type': 'ColorJitter',
                                                  'weight': 1.0},
                                          'saturation': {'kwargs': {'saturation': [0.5,
                                                                                   1.5]},
                                                         'type': 'ColorJitter',
                                                         'weight': 1.0},
                                          'sharpness': {'kwargs': {'sharpness': [0.5,
                                                                                 1.5]},
                                                        'type': 'SharpnessJitter',
                                                        'weight': 1.0}}},
             'repo_id': 'lerobot/libero',
             'repo_type': 'dataset',
             'return_uint8': False,
             'revision': None,
             'root': None,
             'streaming': False,
             'use_imagenet_stats': True,
             'video_backend': 'torchcodec'},
 'env': None,
 'env_eval_freq': 0,
 'eval': {'batch_size': 50,
          'n_episodes': 50,
          'recording': False,
          'recording_private': False,
          'recording_repo_id': None,
          'use_async_envs': True},
 'eval_steps': 0,
 'job': {'detach': False,
         'image': 'huggingface/lerobot-gpu:latest',
         'tags': [],
         'target': None,
         'timeout': '2d'},
 'job_name': 'pi0',
 'log_freq': 1,
 'max_eval_samples': 0,
 'num_workers': 0,
 'optimizer': {'betas': [0.9, 0.95],
               'eps': 1e-08,
               'grad_clip_norm': 1.0,
               'lr': 2.5e-05,
               'type': 'adamw',
               'weight_decay': 0.01},
 'output_dir': '/root/autodl-tmp/train_demo',
 'peft': {'full_training_modules': None,
          'init_type': None,
          'lora_alpha': 16,
          'method_type': 'LORA',
          'r': 8,
          'target_modules': 'all-linear'},
 'persistent_workers': True,
 'policy': {'action_expert_variant': 'gemma_300m',
            'action_feature_names': None,
            'chunk_size': 50,
            'compile_mode': 'max-autotune',
            'compile_model': True,
            'device': 'cuda',
            'dtype': 'bfloat16',
            'empty_cameras': 1,
            'freeze_vision_encoder': False,
            'gradient_checkpointing': True,
            'image_resolution': [224, 224],
            'input_features': {'observation.images.empty_camera_0': {'shape': [3,
                                                                               224,
                                                                               224],
                                                                     'type': <FeatureType.VISUAL: 'VISUAL'>},
                               'observation.images.image': {'shape': [3,
                                                                      256,
                                                                      256],
                                                            'type': <FeatureType.VISUAL: 'VISUAL'>},
                               'observation.images.image2': {'shape': [3,
                                                                       256,
                                                                       256],
                                                             'type': <FeatureType.VISUAL: 'VISUAL'>},
                               'observation.state': {'shape': [8],
                                                     'type': <FeatureType.STATE: 'STATE'>}},
            'license': None,
            'max_action_dim': 32,
            'max_period': 4.0,
            'max_state_dim': 32,
            'min_period': 0.004,
            'n_action_steps': 50,
            'n_obs_steps': 1,
            'normalization_mapping': {'ACTION': <NormalizationMode.MEAN_STD: 'MEAN_STD'>,
                                      'STATE': <NormalizationMode.MEAN_STD: 'MEAN_STD'>,
                                      'VISUAL': <NormalizationMode.IDENTITY: 'IDENTITY'>},
            'num_inference_steps': 10,
            'optimizer_betas': [0.9, 0.95],
            'optimizer_eps': 1e-08,
            'optimizer_grad_clip_norm': 1.0,
            'optimizer_lr': 2.5e-05,
            'optimizer_weight_decay': 0.01,
            'output_features': {'action': {'shape': [7],
                                           'type': <FeatureType.ACTION: 'ACTION'>}},
            'paligemma_variant': 'gemma_2b',
            'pretrained_path': 'lerobot/pi0_libero_finetuned',
            'pretrained_revision': None,
            'private': None,
            'push_to_hub': False,
            'relative_exclude_joints': ['gripper'],
            'repo_id': 'pepijn223/pi0_libero_finetuned_extra2',
            'rtc_config': None,
            'scheduler_decay_lr': 2.5e-06,
            'scheduler_decay_steps': 10000,
            'scheduler_warmup_steps': 1000,
            'tags': None,
            'time_sampling_beta_alpha': 1.5,
            'time_sampling_beta_beta': 1.0,
            'time_sampling_offset': 0.001,
            'time_sampling_scale': 0.999,
            'tokenizer_max_length': 48,
            'train_expert_only': False,
            'type': 'pi0',
            'use_amp': False,
            'use_peft': False,
            'use_relative_actions': False},
 'prefetch_factor': 4,
 'rename_map': {},
 'resume': False,
 'reward_model': None,
 'sample_weighting': None,
 'save_checkpoint': True,
 'save_checkpoint_to_hub': False,
 'save_freq': 0,
 'scheduler': {'decay_lr': 2.5e-06,
               'num_decay_steps': 10000,
               'num_warmup_steps': 1000,
               'peak_lr': 2.5e-05,
               'type': 'cosine_decay_with_warmup'},
 'seed': 1000,
 'steps': 3,
 'tolerance_s': 0.0001,
 'use_policy_training_preset': True,
 'wandb': {'add_tags': True,
           'disable_artifact': False,
           'enable': False,
           'entity': None,
           'mode': None,
           'notes': None,
           'project': 'lerobot',
           'run_id': None}}
INFO 2026-10-15 17:10:34 ot_train.py:280 Logs will be saved locally.
INFO 2026-10-15 17:10:34 ot_train.py:298 Creating dataset

Downloading bytes:                                         |  0.00B            

Reconstructing (incomplete total...): |           |  0.00B /  0.00B            

Fetching 4 files:   0%|                                  | 0/4 [00:00<?, ?it/s]

Fetching 4 files: 100%|██████████████████████████| 4/4 [00:00<00:00, 73.83it/s]

Download complete:                                         |  0.00B            

Reconstruction complete: |                        |  0.00B /  0.00B            

Reconstruction complete: |                        |  0.00B /  0.00B            

Download complete:                                         |  0.00B

Downloading bytes:                                         |  0.00B            

Reconstructing (incomplete total...): |           |  0.00B /  0.00B            

Fetching 4 files:   0%|                                  | 0/4 [00:00<?, ?it/s]
Fetching 4 files: 100%|█████████████████████████| 4/4 [00:00<00:00, 496.47it/s]

Download complete:                                         |  0.00B            

Reconstruction complete: |                        |  0.00B /  0.00B            

Reconstruction complete: |                        |  0.00B /  0.00B            

Download complete:                                         |  0.00B
INFO 2026-10-15 17:10:49 ot_train.py:324 Creating policy
The PI0 model is a direct port of the OpenPI implementation. 
This implementation follows the original OpenPI structure for compatibility. 
Original implementation: https://github.com/Physical-Intelligence/openpi
INFO 2026-10-15 17:12:50 ling_pi0.py:453 Enabled gradient checkpointing for PI0Pytorch model
Loading model from: lerobot/pi0_libero_finetuned
✓ Loaded state dict from model.safetensors
WARNING 2026-10-15 17:12:53 ling_pi0.py:943 Vision embedding key might need handling: model.paligemma_with_expert.paligemma.model.vision_tower.vision_model.embeddings.patch_embedding.bias
WARNING 2026-10-15 17:12:53 ling_pi0.py:943 Vision embedding key might need handling: model.paligemma_with_expert.paligemma.model.vision_tower.vision_model.embeddings.patch_embedding.weight
All keys loaded successfully!
INFO 2026-10-15 17:12:54 ot_train.py:339 Using PEFT! Wrapping model.
INFO 2026-10-15 17:12:55 etrained.py:427 Wrapped pi0 with PEFT (LoraConfig)
INFO 2026-10-15 17:12:59 ot_train.py:402 Creating optimizer and scheduler
INFO 2026-10-15 17:12:59 hedulers.py:155 Auto-scaling LR scheduler: num_training_steps (3) < num_decay_steps (10000). Scaling warmup: 1000 → 0, decay: 10000 → 3 (scale factor: 0.000)
INFO 2026-10-15 17:12:59 ot_train.py:434 Output dir: /root/autodl-tmp/train_demo
INFO 2026-10-15 17:12:59 ot_train.py:441 cfg.steps=3 (3)
INFO 2026-10-15 17:12:59 ot_train.py:442 dataset.num_frames=273465 (273K)
INFO 2026-10-15 17:12:59 ot_train.py:443 dataset.num_episodes=1693
INFO 2026-10-15 17:12:59 ot_train.py:446 Effective batch size: 1 x 1 = 1
INFO 2026-10-15 17:12:59 ot_train.py:447 num_learnable_params=21849600 (22M)
INFO 2026-10-15 17:12:59 ot_train.py:448 num_total_params=4049869072 (4B)

Training:   0%|                                        | 0/3 [00:00<?, ?step/s]INFO 2026-10-15 17:12:59 ot_train.py:597 Start offline training on a fixed dataset, with effective batch size: 1

Training:  33%|██████████▋                     | 1/3 [00:02<00:04,  2.30s/step]INFO 2026-10-15 17:13:01 ot_train.py:641 step:1 smpl:1 ep:0 epch:0.00 loss:0.103 grdn:0.554 lr:1.9e-05 updt_s:2.025 data_s:0.273 smp/s:0 mem_gb:9.55

Training:  67%|█████████████████████▎          | 2/3 [00:03<00:01,  1.73s/step]INFO 2026-10-15 17:13:03 ot_train.py:641 step:2 smpl:2 ep:0 epch:0.00 loss:0.189 grdn:0.924 lr:8.1e-06 updt_s:1.182 data_s:0.136 smp/s:1 mem_gb:9.68

Training: 100%|████████████████████████████████| 3/3 [00:05<00:00,  1.58s/step]INFO 2026-10-15 17:13:04 ot_train.py:641 step:3 smpl:3 ep:0 epch:0.00 loss:0.232 grdn:1.118 lr:2.5e-06 updt_s:1.259 data_s:0.134 smp/s:1 mem_gb:9.68
INFO 2026-10-15 17:13:04 ot_train.py:687 Checkpoint policy after step 3
/root/miniconda3/lib/python3.12/site-packages/peft/utils/save_and_load.py:296: UserWarning: Setting `save_embedding_layers` to `True` as embedding layers found in `target_modules`.
  warnings.warn("Setting `save_embedding_layers` to `True` as embedding layers found in `target_modules`.")
/root/miniconda3/lib/python3.12/site-packages/peft/utils/save_and_load.py:344: UserWarning: Could not identify embedding layer(s) because the model is not a 🤗 transformers model.
  warnings.warn("Could not identify embedding layer(s) because the model is not a 🤗 transformers model.")

Training: 100%|████████████████████████████████| 3/3 [00:05<00:00,  1.82s/step]
INFO 2026-10-15 17:13:04 ot_train.py:770 End of training

4. 参数逐个看

参数本例说明
--policy.pathlerobot/pi0_libero_finetuned从哪个权重开始微调(不是从零训练)
--dataset.repo_idlerobot/libero训练数据。换成你自己的数据集就能训自己的任务
--output_dir/root/autodl-tmp/train_demo检查点和日志写这里。别写 /tmp,重启会没
--steps3只跑 3 步,见第 7 节
--batch_size1单卡显存能塞下的批次。π0 + LoRA 在 24 GB 卡上基本就 1
--num_workers0数据加载子进程数。0 表示在主进程加载,调试用;正式训练调到 4–8
--save_freq0每多少步存一次检查点,0 表示不按步数存
--log_freq1每步打一行日志
--env_eval_freq0训练中每隔多少步跑一次仿真评测,0 = 不跑(省时间)
--eval_steps0评测跑多少步
--wandb.enablefalse不往 Weights & Biases 上传。要曲线图就设 true 并先 wandb login
--policy.push_to_hubfalse训完不自动推 Hub
--peft.*见 1.2LoRA 的秩和缩放

5. 怎么读训练日志

step:1 smpl:1 ep:0 epch:0.00 loss:0.103 grdn:0.554 lr:1.9e-05 updt_s:2.025 data_s:0.273 smp/s:1 mem_gb:9.55
字段含义怎么看
loss当前批次的损失趋势向下才是好事;单步抖动正常
grdn梯度范数突然飙到几十上百说明训练不稳(学习率太大/数据有问题)
lr当前学习率带 warmup + 衰减,所以前几步一直在变
updt_s一步反向传播耗时(秒)这个才是真正的训练速度
data_s取一个 batch 的耗时比 updt_s 还大,说明瓶颈在磁盘/解码,该加 num_workers
smp/s每秒样本数换算成「一个 epoch 要多久」用
mem_gb显存占用用来判断还能不能加 batch_size

下面这几行日志值得单独说:

Auto-scaling LR scheduler: num_training_steps (3) < num_decay_steps (10000).
Scaling warmup: 1000 → 0, decay: 10000 → 3
Using PEFT! Wrapping model.
Wrapped pi0 with PEFT (LoraConfig)
  • 前两行:因为只跑 3 步,学习率调度器自动把「warmup 1000 步」缩到 0 步、「衰减 10000 步」缩到 3 步,否则整个训练都在 warmup 里,学不到东西。这是 LeRobot 的贴心设计,不是报错;
  • 后两行:确认 LoRA 真的挂上了。

6. 输出目录里有什么

/root/autodl-tmp/train_demo/
├── checkpoints/          # 每 save_freq 步存一次
│   └── 000003/           # 第 3 步的检查点
├── train_config.json     # 这次训练的全部参数,复现靠它
└── ...

train_config.json 很有用:以后忘了当时用了什么参数,直接翻它;也能拿它当模板改。

7. 这次为什么只跑 3 步

--steps=3 是试跑,目的只有一个:确认「能加载、能前向、能反传、能存盘」这条链路通。真正的微调:

  • 步数通常几万步起(LIBERO 这种规模常见 20k–50k);
  • save_freq 设成几千,log_freq 设 50–100;
  • env_eval_freq 设几千,练一段就评测一次,别训完才发现方向错了;
  • 先小数据小步数验证,再放大——先跑通再跑好,是深度学习的基本纪律。

8. 常见问题

现象原因处理
CUDA out of memoryπ0 + LoRA 本身吃显存batch_size=1,并把 --peft.target_modules 缩小到 attention 层
训练很慢、GPU 闲着数据加载卡住(data_s 很大)提高 num_workers,数据放本地盘而不是网络盘
loss 不降学习率、数据量、任务难度先用 3 步确认链路,再检查数据集是否正确加载
中断后想续训没存检查点设 save_freq;续训用 --checkpoint_path
训完发现没保存save_freq=0 且只看最后一步设 save_freq,或确认 --steps 走到时自动存