SmolVLA 微调 1
一、实验目标与准备本实验以lerobot/smolvla_base为预训练模型在 SO101 pick-place 数据集上进行微调并通过多组对比实验评估微调对动作预测精度的影响。使用lerobot/smolvla_base对目标数据集进行微调1.1 冒烟测试执行 100 步微调先用少量步数验证训练流程能否跑通确认数据加载、模型前向和损失计算均正常。lerobot-train \--policy.pathlerobot/smolvla_base \--dataset.repo_idlerobot/svla_so101_pickplace \--policy.empty_cameras1 \--rename_map{observation.images.up:observation.images.camera1,observation.images.side:observation.images.camera2} \--batch_size1 \--steps100 \--num_workers0 \--output_diroutputs/train/smolvla_pickplace_smoke_20261019 \--job_namesmolvla_pickplace_smoke \--policy.devicecuda \--policy.freeze_vision_encodertrue \--policy.train_expert_onlytrue \--save_freq50 \--wandb.enablefalse二、模型评估1.2 对比实验原始模型 vs 微调 50 步 vs 微调 100 步在相同输入条件下比较三个模型的动作预测误差初步判断微调是否改变了模型行为。做对比实验使用相同的图像、机器人状态和语言指令比较原始 SmolVLA、训练 50 步和训练 100 步后的动作预测。1.2.1 编写统一评估脚本创建对比脚本统一数据预处理、评估样本选取和误差计算方式确保三个模型在完全一致的条件下进行对比。# 配置 # 加载数据 # 创建统一预处理器 # 统一使用当前数据集的统计量。# 三个模型使用同一套预处理流程确保对比条件一致。# 准备评估样本 max_start len(dataset) - CHUNK_SIZEcandidate_indices torch.linspace(0,max_start,stepsmin(12, max_start 1),).long().unique().tolist()eval_cases []for start_idx in candidate_indices:# 读取连续 50 步数据window [dataset[start_idx offset]for offset in range(CHUNK_SIZE)]# 不允许动作序列跨越 episode 边界episode_ids {int(torch.as_tensor(item[episode_index]).item())for item in window}if len(episode_ids) ! 1:print(Skip episode boundary:, start_idx)continue# 固定当前观测sample copy.deepcopy(dict(window[0]))batch preprocess(sample)# 50 步真实动作gt_raw torch.stack([torch.as_tensor(item[action]).float().cpu()for item in window])# 转换到与模型输出相同的归一化空间gt_norm (gt_raw - action_mean) / action_stdeval_cases.append({index: start_idx,batch: batch,gt: gt_norm,})if len(eval_cases) 3:raise RuntimeError(fOnly {len(eval_cases)} valid evaluation cases. Please check dataset episode boundaries.)print(Valid evaluation cases:, len(eval_cases))# 推理与评估 # 逐个比较 checkpoint # 最终对比 得到这组结果说明100 步微调已经改变了模型的行为而且完整动作块的误差有所下降但第一个动作的预测反而变差了。100 步模型的动作块 MAE 比原始模型下降了约9.35%但首步 MAE 增加了一倍以上。该现象源于SmolVLA 基于 Flow Matching 的动作块预测机制训练损失优化目标为整条动作序列的全局误差模型为降低长时序轨迹整体偏差牺牲了首步动作预测精度。属于块式动作预测微调中常见的权衡现象后续可通过首步损失加权等策略缓解首步精度退化同时需要结合任务成功率综合评估模型性能。接着我们进一步确认动作块误差的改善是不是集中在某几个关节上1.3 逐关节误差分析改善集中在哪些关节在整体误差分析的基础上进一步拆解到每个关节确认动作块误差的改善是否集中在特定关节上。在文件末尾添加下面的代码# Per-joint Error Analysis print(\n Per-joint Error Analysis )joint_names [shoulder_pan,shoulder_lift,elbow_flex,wrist_flex,wrist_roll,gripper,]# 建立样本索引到真实动作的映射case_by_index {case[index]: casefor case in eval_cases}# 只比较原始模型和训练 100 步的模型base_preds results[base][predictions]tuned_preds results[step100][predictions]common_indices sorted(set(base_preds.keys()) set(tuned_preds.keys()) set(case_by_index.keys()))print(Number of evaluation samples:, len(common_indices))base_errors []tuned_errors []base_first_errors []tuned_first_errors []for idx in common_indices:gt case_by_index[idx][gt]base_pred base_preds[idx]tuned_pred tuned_preds[idx]# 每个样本的完整动作块误差形状为 (50, 6)base_errors.append((base_pred - gt).abs())tuned_errors.append((tuned_pred - gt).abs())# 每个样本第一个动作的误差形状为 (6,)base_first_errors.append((base_pred[0] - gt[0]).abs())tuned_first_errors.append((tuned_pred[0] - gt[0]).abs())base_errors torch.stack(base_errors)tuned_errors torch.stack(tuned_errors)base_first_errors torch.stack(base_first_errors)tuned_first_errors torch.stack(tuned_first_errors)print(\nFull action chunk MAE by joint:)for i, name in enumerate(joint_names):base_mae base_errors[..., i].mean().item()tuned_mae tuned_errors[..., i].mean().item()delta tuned_mae - base_maeprint(f{name:16} fBase{base_mae:.4f} fStep100{tuned_mae:.4f} fDelta{delta:.4f})print(\nFirst-step MAE by joint:)for i, name in enumerate(joint_names):base_mae base_first_errors[:, i].mean().item()tuned_mae tuned_first_errors[:, i].mean().item()delta tuned_mae - base_maeprint(f{name:16} fBase{base_mae:.4f} fStep100{tuned_mae:.4f} fDelta{delta:.4f})# 统计每个评估样本是否改善base_case_mae base_errors.mean(dim(1, 2))tuned_case_mae tuned_errors.mean(dim(1, 2))improved (tuned_case_mae base_case_mae).sum().item()print(f\nImproved evaluation samples: f{improved}/{len(common_indices)})print(Mean paired MAE change:,(tuned_case_mae - base_case_mae).mean().item())得到这次结果说明100 步微调已经改变了模型的行为但不同关节的变化并不一致。4 个关节的完整动作块误差下降shoulder_lift、elbow_flex、wrist_flex、wrist_roll。2 个关节的误差上升shoulder_pan、gripper。10 个评估窗口中7 个窗口的整体动作块误差下降。但第一个动作的误差在 6 个关节上全部上升尤其是夹爪MAE 从0.3180增至0.8356。夹爪误差退化可能的原因SmolVLA Flow Matching 动作专家的损失目标优化整条动作 chunk 全局轨迹没有单独约束首步。为了匹配 pick-place 任务 “抬臂→接近→抓取” 整套连续轨迹模型在优化全局流场时牺牲了初始时刻第 1 步预测精度。SO101 pick-place 任务重点是夹爪闭合 / 打开微调让模型优先学到夹爪随时间变化的时序行为但破坏了预训练权重里初始时刻夹爪状态的预测能力这是首步误差最大来源。shoulder_pan肩部水平旋转无论是首步还是整条 chunk误差都上升说明当前微调步数下模型还没有学好这个旋转自由度。目前最重要的问题是这些改善是否只是模型对训练数据拟合得更好了之前训练使用了全部 50 个 episode所以现有结果不能证明模型具有更好的泛化能力。我们重新做一次有独立验证集的微调实验。1.4 独立验证集实验训练 1000 步并保留 20% 的 episode 用于验证之前训练使用了全部 50 个 episode现有结果不能证明模型具有更好的泛化能力。因此重新设计实验训练 1000 步并保留 20% 的 episode 作为独立验证集用于评估模型的泛化表现。执行lerobot-train --policy.pathlerobot/smolvla_base --policy.repo_idlocal/smolvla_so101_test --dataset.repo_idlerobot/svla_so101_pickplace --dataset.eval_split0.2 --policy.empty_cameras1 --rename_map{observation.images.up:observation.images.camera1,observation.images.side:observation.images.camera2} --batch_size1 --steps1000 --num_workers0 --eval_steps100 --max_eval_samples256 --output_diroutputs/train/smolvla_pickplace_holdout_1000_retry --job_namesmolvla_pickplace_holdout --policy.devicecuda --policy.freeze_vision_encodertrue --policy.train_expert_onlytrue --save_freq500 --wandb.enablefalse如果出现上传huggingFace报错不用管模型训练已经保存在本地了这个报错是训练程序正在尝试向 Hugging Face 上传模型但服务器返回了 401 Unauthorized未授权。运行评估SmolVLA 微调 1000 步后独立验证集上的 50 步动作块误差下降了 13.3%但第一个动作的误差反而增加了约 104%。这意味着模型可能正在学习目标任务的动作序列但暂时不能确定它是否改善了实际控制效果。我们先不继续训练直接分析误差究竟集中在动作序列的哪个位置。1.4.1 分析不同预测步长的误差在评估代码后加# # 8. 比较不同预测步长的误差# print(\n Error by Prediction Horizon )case_by_start {case[start]: casefor case in eval_cases}horizons [(Step 1, 0, 1),(Steps 1-5, 0, 5),(Steps 6-10, 5, 10),(Steps 11-25, 10, 25),(Steps 26-50, 25, 50),]for model_name in [base, step500, step1000]:if model_name not in results:continuepredictions results[model_name][predictions]print(f\n--- {model_name} ---)for label, start, end in horizons:errors []for idx, pred in predictions.items():if idx not in case_by_start:continuegt case_by_start[idx][gt]error (pred[start:end] - gt[start:end]).abs().mean()errors.append(error.item())if errors:print(f{label:15}: MAE f{sum(errors) / len(errors):.4f})结果发现:1000 步微调主要改善了动作块后半段的预测但前几步的预测反而变差了。预测区间原始模型 MAE微调 1000 步 MAE变化第 1 步0.16610.3385恶化 103.8%第 15 步0.21560.3211恶化 49.0%第 610 步0.36120.3938恶化 9.0%第 1125 步0.56370.4641改善 17.7%第 2650 步0.86720.7237改善 16.5%现在我们直接定位究竟是哪个关节导致微调后的前几步预测变差。修改eval_smolvla_holdout.py在文件末尾添加以下代码# # 9. 分析不同预测区间、不同关节的误差# print(\n Horizon x Joint Error Analysis )joint_names [shoulder_pan,shoulder_lift,elbow_flex,wrist_flex,wrist_roll,gripper,]horizons [(Step 1, 0, 1),(Steps 1-5, 0, 5),(Steps 6-10, 5, 10),(Steps 11-25, 10, 25),(Steps 26-50, 25, 50),]case_by_start {case[start]: casefor case in eval_cases}model_names [base, step500, step1000]for horizon_name, start, end in horizons:print(f\n{ * 12} {horizon_name} { * 12})for model_name in model_names:if model_name not in results:continuepredictions results[model_name][predictions]joint_errors []for idx, pred in predictions.items():if idx not in case_by_start:continuegt case_by_start[idx][gt]# 每个样本、每个关节在指定区间的平均绝对误差error (pred[start:end] - gt[start:end]).abs().mean(dim0)joint_errors.append(error)if not joint_errors:continuemean_errors torch.stack(joint_errors).mean(dim0)print(f\n{model_name}:)for joint, error in zip(joint_names, mean_errors):print(f {joint:16}: {error.item():.4f})得到各个关节1-50步区间在微调1000步后与原始模型的误差比较预测区间shoulder_panshoulder_liftelbow_flexwrist_flexwrist_rollgripper第 1 步原始模型0.07310.17700.19970.25330.19560.0978第 1 步微调 1000 步0.35160.25420.22340.41220.41520.3746第 1125 步原始模型0.35320.90680.47480.62560.52800.4935第 1125 步微调 1000 步0.60830.28840.48580.47550.49180.4350第 2650 步原始模型0.98131.00130.66250.90950.92970.7194第 2650 步微调 1000 步0.98290.64300.80840.51840.69760.6919这里有三个值得注意的发现肩部抬升shoulder_lift改善明显。第 1125 步的 MAE 从 0.9068 降到了 0.2884。腕部俯仰wrist_flex改善明显。第 2650 步的 MAE 从 0.9095 降到了 0.5184。夹爪gripper的首步预测明显变差。MAE 从 0.0978 上升到 0.3746。直接画出三个模型的动作轨迹弄清楚微调后的模型究竟是预测动作的时机发生了偏移、动作幅度发生了变化还是某些关节的动作轨迹本身出现了偏差。噪声影响消除在 LeRobot 0.6.1 的 SmolVLA 实现中如果调用predict_action_chunk(batch)时没有传入noise模型就会自行采样初始噪声。可以查看 SmolVLA 源码 中的sample_actions()。如果原始模型、500 步模型和 1000 步模型使用了不同的初始噪声那么它们的预测差异就同时包含了模型参数变化和随机采样变化这会影响我们对微调效果的判断。我们修改代码让原始模型、500 步模型和 1000 步模型对每个样本使用完全相同的初始噪声。得到图中蓝线是真实示教动作橙线是原始 SmolVLA绿线是微调 1000 步后的 SmolVLA。关节图中表现初步判断shoulder_pan微调模型能够跟随末段上升趋势原始模型基本没有跟上微调模型更好shoulder_lift微调模型后半段逐渐接近真实动作但波动明显部分改善elbow_flex微调模型波动较大未能稳定跟随真实轨迹仍需改善wrist_flex微调模型捕捉到了动作上升趋势但末段出现明显超调学到了趋势幅度仍不准确wrist_roll微调模型开始跟随末段上升趋势原始模型基本保持不变微调模型更好gripper微调模型捕捉到了夹爪从高位向低位变化的过程原始模型没有跟上微调模型更好这里最值得关注的是gripper。真实动作在大约第 17 步附近发生了明显变化微调模型也预测出了相应的变化过程而原始模型基本保持在高位。这说明微调可能帮助模型学到了示教中的某些动作转换而不只是改变了预测数值。同时再观察各个动作块在step500和step1000的误差指标原始 SmolVLA微调 500 步微调 1000 步首步 MAE0.18080.36950.3192完整动作块 MAE0.63280.66440.4831第 15 步 MAE0.23020.38710.3220第 610 步 MAE0.37970.48390.3855第 1125 步 MAE0.57920.61050.4469第 2650 步 MAE0.79610.78840.5566与原始模型相比1000 步模型的完整动作块 MAE 下降了约23.7%后半段的预测改善尤其明显。但是首步 MAE 增加了约 76.6%。这意味着模型虽然更好地拟合了整段动作序列却还没有改善最初动作的预测。三、模型微调这次不从原始模型重新开始而是以已经训练 1000 步的模型为起点再训练 1000 步并将学习率从1e-4降到5e-5这样可以在保留已有学习成果的基础上观察较小的学习率能否继续改善动作预测。lerobot-train \--policy.path$CKPT \--dataset.repo_idlerobot/svla_so101_pickplace \--dataset.eval_split0.2 \--policy.empty_cameras1 \--rename_map{observation.images.up:observation.images.camera1,observation.images.side:observation.images.camera2} \--batch_size1 \--steps1000 \--num_workers0 \--eval_steps250 \--max_eval_samples256 \--output_diroutputs/train/smolvla_pickplace_stage2_lr5e5 \--job_namesmolvla_pickplace_stage2_lr5e5 \--policy.devicecuda \--policy.freeze_vision_encodertrue \--policy.train_expert_onlytrue \--policy.optimizer_lr5e-5 \--policy.push_to_hubfalse \--save_freq250 \--wandb.enablefalse其中 $CKPT 之前的训练1000step的模型checkpoint训练完成后将eval_smolvla_holdout.py 中的OUTPUT_ROOTS 替换为新的模型实验目录。OUTPUT_ROOTS [Path(outputs/train/smolvla_pickplace_stage2_lr5e5),Path(outputs/train/smolvla_pickplace_holdout_1000_retry),Path(outputs/train/smolvla_pickplace_holdout_1000_final),Path(outputs/train/smolvla_pickplace_holdout_1000),]执行eval_smolvla_holdout.py 进行验证指标原始模型第一阶段 1000 步第二阶段 1000 步First-step MAE0.18080.31920.2339Chunk MAE0.63280.48310.3815SmolVLA 第二阶段微调已经取得了明确的进步完整动作块 MAE 从 0.4831 降到了 0.3815。第二阶段模型相较原始模型完整动作块 MAE 下降 39.7%。首步 MAE 比第一阶段下降 26.7%说明首步预测也在改善。不过首步 MAE 仍比原始模型高约 29.4%这个问题还没有完全解决。再看各关节的完整动作块误差关节原始模型第二阶段 1000 步误差变化shoulder_pan0.44400.4437基本不变shoulder_lift0.90700.2843显著改善elbow_flex0.56850.3040显著改善wrist_flex0.71200.4122明显改善wrist_roll0.62520.4505明显改善gripper0.54010.3941明显改善这说明模型的改善不是只发生在某一个关节上而是涉及多个关节。不过shoulder_pan基本没有改善我们后续需要单独分析它。接着我们现在把评估窗口扩大到最多 256 个并尽量均匀地覆盖每个保留的验证 episode。这样能更可靠地判断第二阶段微调的效果。得到完整动作块 MAE 从0.7791降到0.4940下降约36.6%。第 15 步 MAE 从0.2774降到0.2559说明短期动作预测也有改善。第 2650 步 MAE 从0.9998降到0.6104下降约38.9%。这反应了第二阶段微调确实改善了完整动作块的预测。指标原始模型第二阶段 1000 步变化完整动作块 MAE0.77910.4940下降 36.6%第 15 步 MAE0.27740.2559下降 7.7%第 610 步 MAE0.43870.3106下降 29.2%第 1125 步 MAE0.69180.4406下降 36.3%第 2650 步 MAE0.99980.6104下降 38.9%1.6 低学习率续训实验我们先不修改模型结构也不改损失函数。这次从第二阶段的 1000 步 checkpoint 出发使用更小的学习率2e-5继续训练500 步。得到 Loss指标250 步500 步训练 Loss—约 0.204Eval Loss0.38520.3797学习率—逐渐衰减至接近 0验证损失从 250 步时的 0.3852 降到了 500 步时的 0.3797Eval Loss 下降约 1.43%这说明训练过程仍有改善但改善幅度比较小。接着我们评估比较五个模型base原始 SmolVLA。step500第二阶段训练 500 步。step1000第二阶段训练 1000 步。stage3_250第三阶段额外训练 250 步。stage3_500第三阶段额外训练 500 步。模型首步 MAE完整动作块 MAE原始模型0.21400.7791第二阶段 1000 步0.26790.4940第三阶段 250 步0.27140.4716第三阶段 500 步0.26840.4745第三阶段训练 250 步后完整动作块 MAE 进一步下降继续训练到 500 步反而略有回升。首步 MAE 则基本没有改善。这次结果说明第三阶段已经接近平台期。因此我们下一步不应该单纯降低学习率继续训练而应该尝试时间步加权损失Temporal Loss Weighting。1.6.1 时间步加权损失Time-step weighted lossDiffusion Policy / SmolVLA / ACT 都是预测一个动作块action chunk比如 50 步连续动作。 原始默认每一步的误差权重一样加起来求平均。加权版本就是第 t 个时间步的权重。如果你设置(w_1 w_2...w_50) →更加看重首步预测精度抑制你现在 “首步 MAE 飙升” 的问题如果你反过来前面权重低、后面权重高 → 模型优先优化轨迹末尾就出现首步 MAE 基本没有改善的效果本质修改损失函数的目标偏好告诉模型哪些时序位置更重要梯度不同时间步的误差对参数更新的贡献不一样举你的场景 机器人推块装配第一个动作决定初始走向容错很低首步错了后面怎么修正都容易超调这时就可以调高前几步权重。1.6.2 降低学习率LR decay / 调小 lr学习率 n 出现在梯度更新式降低学习率 缩小 n。 不管这个梯度来自动作块第 1 步还是第 50 步所有梯度统一乘上同一个缩放系数。本质控制参数每次更新迈多大步子不改变 “我们关心哪些位置”不会改变 loss 内部各个时间步的相对权重 作用训练更稳、震荡变小、不容易一下子过拟合但它不会自动优先学好首步。1.7 时间步损失加权续训补丁在 SmolVLA 损失函数中引入时间步加权策略对动作块前序时间步赋予更高损失权重权重经过均值归一化保持整体 loss 尺度不变强制模型优先优化动作块首步预测精度缓解微调过程首步 MAE 退化问题。接着我们选择stage3_250作为起点进行训练因为它的完整动作块 MAE 为0.471572优于stage3_500的0.474530得到模型首步 MAE完整动作块 MAE原始模型0.21400.7791第二阶段 1000 步0.26790.4940第三阶段 250 步0.27140.4716加权损失 250 步0.26360.4658加权损失 500 步0.26010.4685与第三阶段的最佳模型stage3_250相比weighted_250首步 MAE 下降约2.86%完整动作块 MAE 下降约1.23%。weighted_500首步 MAE 下降约4.14%但完整动作块 MAE 比weighted_250略高。这说明时间步加权损失可能确实有帮助但改善幅度目前还比较小。再来看关节加权损失模型相对于第三阶段模型首步误差的变化如下关节stage3_250weighted_250变化shoulder_pan0.29920.2836改善shoulder_lift0.20660.2081略差elbow_flex0.19590.1979略差wrist_flex0.22550.2184改善wrist_roll0.33440.3194改善gripper0.36650.3542改善全局加权对shoulder_pan、wrist_flex、wrist_roll和gripper都有帮助。但与原始模型相比首步误差仍然偏高尤其是shoulder_pan0.2836原始模型为 0.2081。wrist_roll0.3194原始模型为 0.2405。gripper0.3542原始模型为 0.2329。因此我们下一次重点关注这三个关节同时将shoulder_lift纳入实验因为它的首步误差也高于原始模型。