LeRobot入门教程 13:LeRobot 微调 (LoRA)
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 [1m[33mLogs will be saved locally.[0m
INFO 2026-10-15 17:10:34 ot_train.py:298 Creating dataset
Downloading bytes: | 0.00B
Reconstructing (incomplete total...): | | 0.00B / 0.00B [A
Fetching 4 files: 0%| | 0/4 [00:00<?, ?it/s][A[A
Fetching 4 files: 100%|██████████████████████████| 4/4 [00:00<00:00, 73.83it/s]
Download complete: | 0.00B
Reconstruction complete: | | 0.00B / 0.00B [A
Reconstruction complete: | | 0.00B / 0.00B
Download complete: | 0.00B
Downloading bytes: | 0.00B
Reconstructing (incomplete total...): | | 0.00B / 0.00B [A
Fetching 4 files: 0%| | 0/4 [00:00<?, ?it/s][A[A
Fetching 4 files: 100%|█████████████████████████| 4/4 [00:00<00:00, 496.47it/s]
Download complete: | 0.00B
Reconstruction complete: | | 0.00B / 0.00B [A
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 [1m[33mOutput dir:[0m /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.path | lerobot/pi0_libero_finetuned | 从哪个权重开始微调(不是从零训练) |
--dataset.repo_id | lerobot/libero | 训练数据。换成你自己的数据集就能训自己的任务 |
--output_dir | /root/autodl-tmp/train_demo | 检查点和日志写这里。别写 /tmp,重启会没 |
--steps | 3 | 只跑 3 步,见第 7 节 |
--batch_size | 1 | 单卡显存能塞下的批次。π0 + LoRA 在 24 GB 卡上基本就 1 |
--num_workers | 0 | 数据加载子进程数。0 表示在主进程加载,调试用;正式训练调到 4–8 |
--save_freq | 0 | 每多少步存一次检查点,0 表示不按步数存 |
--log_freq | 1 | 每步打一行日志 |
--env_eval_freq | 0 | 训练中每隔多少步跑一次仿真评测,0 = 不跑(省时间) |
--eval_steps | 0 | 评测跑多少步 |
--wandb.enable | false | 不往 Weights & Biases 上传。要曲线图就设 true 并先 wandb login |
--policy.push_to_hub | false | 训完不自动推 Hub |
--peft.* | 见 1.2 | LoRA 的秩和缩放 |
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 走到时自动存 |