2026-08-24 · 训练配方

为什么今天那个 6000 步的模型更差

——以及两小时之内怎么训一个更好的。

一句话:那个 6000 步的模型不是训坏了,是训到 30% 就停了。这件事跟用不用 A100 基本没关系,跟 batch 也没有想象中那么大关系。真正被改掉的,是跑了多少步。

更新(2026-08-24 深夜,查完 LeRobot 源码和 Hub 仓库之后):两件事变了。

① 不用从头重训。那个 6000 步 checkpoint 的 training_state/(优化器状态 412 MB、调度器状态、随机数状态)还完整地留在 Hub 仓库里——你本地只下载了 pretrained_model/,所以看起来像没有。它可以直接 --resume 接着跑,那 6000 步的钱不用再付一次。

② 学习率那个数字从「复算」变成「实测」。仓库里 scheduler_state.json 存的是 "_last_lr": [7.990453104925807e-05],跟我复算的 7.99e-05 一模一样。这条现在是硬证据。

下面第七节的命令已经按这个改过了。完整的严谨版(含最小对照实验设计、梯度累积的坑、引用行号)写在仓库里的 05-training/SMOLVLA-TRAINING-RECIPE.md。

一、先纠正一个说法(包括我自己之前跟着说的)

之前的判断是「batch 从 2 提到 64 是主要问题」。数字都对,但结论要改。

LeRobot 官方文档里给的 SmolVLA 微调命令(docs/source/smolvla.mdx 第 57–66 行)就是:

--batch_size=64 \
--steps=20000

同一份文档第 49 行还写着:「20000 步在单张 A100 上大约要 4 小时」。

更巧的是,文档第 35–40 行描述他们论文用的参考数据集:「50 集,5 个不同的方块位置,每个位置 10 集」——跟你录的结构一模一样。他们还特意提了一句:试过 25 集,不够,效果差。

所以 batch=64 不是错误,那就是官方配方。错的是这个配方要配 20000 步,而你只跑了 6000。

二、证据:学习率停在哪里

先用大白话说清楚「学习率」是什么。训练就像拧一个旋钮找最佳位置。学习率是每次拧多少。标准做法是一开始拧得大(快速找到大概方向),越往后拧得越小(精修)。最后那段小幅度的精修,正是把「大概对」变成「真的对」的部分。

老模型(8-20 那个)今天这个(A100)
batch264
命令里写的步数2000020000
实际跑到16000
(18000 步时磁盘满崩了)
6000
走完了降温曲线的80%30%
停下时的学习率1.18e-057.99e-05

两个都是从 1.0e-04 出发的。老模型停下时已经降到了出发时的八分之一——它正在做最后的精修。今天这个停下时还有 8.0e-05,几乎就是出发时的大小,还在大刀阔斧地改参数。

它不是「训坏了」,是没降完温就出锅了。

一个很容易踩的坑:lerobot 的降温曲线会按你写的 --steps 自动伸缩(src/lerobot/optim/schedulers.py 第 149–153 行)。你写 --steps=20000,它就把整条降温曲线摊到 20000 步上。

所以中途停在 6000 步,不等于「提前练完了 6000 步的量」,而等于「一条为 20000 步设计的曲线,只走了前面 30%」。这两件事差别很大。

三、「没训完」为什么正好表现成「往中间夹」

一个模型学东西是有顺序的。它先学会所有示范的共同点——动作的大形状、伸手的大概高度、什么时候闭爪。最后才学会那些必须看图才知道的细节:这个杯子在左边,所以腰要多转 20 度。

半路停下,它就停在一个「谁都不得罪」的中间姿势上。这不是抽象的说法,是能量出来的:

今天两轮各 10 次实机试验(都是这个 6000 步模型,都是 0/10),我从 trace 里取出每一次闭爪那一瞬间的姿态,一共 27 次:

你的示范模型
抓取时腰部转角的范围−37° 到 +27°
跨度 64°
−25.8° 到 +1.7°
这个角度的散布(标准差)19.86.4
压到三成
抓取时手腕转角平均 −13.2°−3.7°
≈ 全程平均值 −5.2°

你示范里有 19 集(占 38%)抓取时腰要转到 +6° 以上,模型这 27 次一次都没到过。

这三行数字,正好对上你亲眼看到的三件事:

另外「正前方的杯子它能跨在两侧」也说得通:正前方那组需要 −7.4°,它给 −9.5°,碰巧对准了。五个位置里只有这一个能对上。

三·五、又查出两件事(这是后来补的)

1) 代码版本的嫌疑,排除了

本来担心「云端和 Jetson 用的 LeRobot 版本不一样」会把结论搞乱。查完了:两次训练的 183 个配置字段里只有 6 项不同,而两棵源码树里 schedulers.py、 factory.py、lerobot_train.py、configs/train.py、 SmolVLA 的配置文件 全部 0 行差异,模型代码只差一行 import 写法(Python 3.10 的兼容写法)。 不是版本问题。

2) 但发现了一个我之前漏掉的差别:老模型少训了 10 集

老模型那次用了 eval_split=0.2,新模型用的是 0.0。我原以为这只是「有没有留验证集」的区别, 其实它决定了训练集里有哪些 episode。

LeRobot 的切法是「每个 task 的最后 ceil(集数×比例) 集留出」(datasets/factory.py 第 156 行)。 这个数据集只有一个 task,50 集、留 20% → 训练集是 ep0–39,ep40–49 被整个留出。 训练日志原话也印着:Train/eval split: 40 train, 10 eval。

而 ep40–49 正好是我量出来的最右边那一组杯位(杯子在画面里 0.84,抓取时腰要转到 +11.7°)。

也就是说:那个表现更好的老模型,从来没有训练过第五个杯位。

这有两层意思。第一,你拿老模型去试最右边的杯子时,它是真的没见过,抓不到很正常。 第二,新模型数据更多、位置更全却更差 —— 这个方向说明「数据不够」解释不了新模型为什么差, 反而让「没训完」这个解释更站得住。

四、你的示范数据没有问题

你担心是不是自己动作不够统一。数据不支持这个担心,反而相反。

我把杯子在头部相机画面里的位置用颜色检出来(50 集全部检出),和抓取时的关节角度对照:

对照相关系数意思
杯子在画面里左右的位置 ↔ 抓取时腰部转角0.953几乎是一条直线
杯子在画面里远近的位置 ↔ 抓取时肩部角度−0.846同样很干净

还有一条:闭爪的那 9 帧里,肩、肘、腰的运动幅度中位数分别是 0.42° / 0.44° / 0.43°,90% 的情况下不超过 1°。也就是说你示范时是停稳、夹住、再抬,纪律非常好。

而模型每个控制周期肩部的变化,中位数 3.98°,但有 10% 超过 23.8°、1% 超过 64.2°。这就是「一边往上跑一边夹」的来源。

五、Batch 到底怎么选(你要的表)

先说三条规则,比表本身重要:

1--steps 必须设成你真的会跑完的数字

它同时决定两件事:更新多少次参数,以及降温曲线摊多长。设 20000 就得跑满 20000。中途停 = 白花钱,今天就是这么丢的。

2batch 只决定「每步多快」和「梯度多平滑」

它不改变降温曲线,也不改变更新次数。所以「用不用 A100」「batch 是 16 还是 64」,对最终效果的影响,远小于「跑没跑完」。

3学习率保持 1e-4,不要动

官方就是拿 1e-4 配 batch 64 调好的。你把 batch 往小调,继续用 1e-4 是安全的(梯度噪声大一点,对小数据集反而略有好处)。不要往上调——那是往大 batch 走时才需要的补偿,你这次是往小走。

表:不同 batch 下,其它该怎么配

batch学习率--steps更新次数看几遍数据预计 A100 时长评价
64
resume
不动20000
从 6000 接着跑
1600053 遍+2.0 h 估最推荐。跑到 16000 步时,更新次数/曲线完成度/学习率累积三项与老模型完全相同,唯一变量只剩 batch —— 这本身就是一次干净的对照实验
64
resume
不动20000
跑满
2000066 遍+2.8 h 估再多 0.8 小时就能把整条曲线走完,等于补齐官方配方
21e-420000200002.1 遍~0.6–1 h 估老模型的路子。A100 上极度浪费,读数据会先成为瓶颈,实际未必比 16 快
161e-4200002000016 遍~1.3 h 估推荐。两小时内能跑完整条降温曲线,更新次数比老模型还多 25%
321e-4200002000033 遍~2.2 h 估最接近官方,可能稍微超两小时。如果 16 的时候发现读数据是瓶颈,就换这个
641e-4200002000066 遍~4 h 官方实测官方原版配方。效果大概率最好,但时间不够
641e-4100001000033 遍~2 h 估不推荐。时间是够了,但更新次数只有老模型的 62%,等于换个方式重蹈今天的覆辙

时长是怎么来的,得说清楚:官方只给了一个实测点——batch 64、20000 步、约 4 小时,折合每步 0.72 秒。其余几行是按「计算量大致跟 batch 成正比、外加一点固定开销」推的,是估算,不是实测。

校准方法很简单:开跑三分钟后,日志里会打 step_s:0.xxx。拿这个数 × 步数,就是真实总时长。如果超预算,前 200 步不值钱,当场停掉换个 batch 重开。

六、除了 batch 和步数,还要改三处

1) --save_freq=4000,把中间存档都留下

这样你会拿到 8000 / 12000 / 16000 / 20000 四个存档,回来一个一个上机器测。不要默认最后一个最好——GOAL-NEXT.md 第二节第 2 条早就写了这一条。一个存档 869 MB,四个约 3.5 GB,租机器时留够盘。(上一轮 Jetson 就是跑到 18000 步磁盘满了崩的,别再来一次。)

2) 图像增强:开颜色类,关掉平移类

这条不是常识,是我量出来的,所以详细说一下。

杯子在头部相机画面里的横向位置,五个位置分别落在画面宽度的 0.56 / 0.62 / 0.70 / 0.82 / 0.84。相邻两个之间只差 0.06。

而 lerobot 默认的 affine 增强,会把图像随机平移 ±0.05——差不多正好是一个位置的间距。

平移增强教给模型的是「画面挪了,动作不用变」。可这个任务要它学的偏偏是「画面挪了,动作就得跟着变」。两者方向正好相反。开了反而把唯一的空间线索抹掉。

--dataset.image_transforms.enable=true \
--dataset.image_transforms.tfs.affine.weight=0

亮度、对比度、饱和度、色调、锐度这几个可以开,房间光线本来就不太稳。如果你想严格只改一个变量,这一条也可以先不开——它不是关键项。

3) --dataset.eval_split=0.0,50 集全部拿去训

之前做过一次「留出一个位置」的离线评测:没见过的位置 loss 0.2300,训练过的位置 0.2348,比值 0.98——看起来完美。然后实机 0/10。

这个指标在这个任务上没有预测力。为了它切掉 10 集训练数据不划算,尤其官方文档还专门说过 25 集就不够用了。真正的验收是上机器跑。

七、两小时的具体做法

首选:接着跑,不要重训

lerobot-train \
  --resume=true \
  --config_path=Suyang99/xlerobot-smolvla-cup-grasp-right-a100-exp \
  --steps=20000 \
  --save_freq=2000 \
  --output_dir=outputs/train/a100_resume_20k

--config_path 可以直接写 Hub 仓库名,它会自己把最新的 checkpoint 连同 training_state/ 下载下来再续跑(configs/train.py 第 222–258 行)。

resume 的时候不要改 --steps 的长度,也不要改 batch。原来那次配置的就是 20000,存下来的调度器位置 last_epoch=6000 正好是这条曲线上的点。你要是把长度改了,恢复出来的位置就落在另一条曲线上了。

备选:只租到 4090 / 24G,装不下 batch 64

那就别 resume(resume 要求同样的 batch 64),改成一次干净的新训练:

lerobot-train \
  --policy.path=lerobot/smolvla_base \
  --dataset.repo_id=Suyang99/xlerobot-cup-grasp-20260820-0230 \
  --rename_map='{"observation.images.head":"observation.images.camera1","observation.images.right_arm_wrist":"observation.images.camera2","observation.images.left_arm_wrist":"observation.images.camera3"}' \
  --batch_size=8 --num_workers=8 \
  --steps=20000 --save_freq=2000 \
  --dataset.eval_split=0.2 \
  --policy.device=cuda --policy.push_to_hub=false \
  --output_dir=outputs/train/b8_eval02_20k \
  --job_name=b8_eval02_20k

--dataset.eval_split=0.2 不是写错。老模型用的就是 0.2、训练集是 ep0–39。 把这一项对齐,这次训练才能和老模型构成「只差 batch」的干净对照(见上面三·五)。 20000 步跑满,估计 1.5 小时左右。

两小时到点还没跑完?不要紧,这次不一样。

LeRobot 每存一个 checkpoint,都会把 pretrained_model/ 和 training_state/ 一起写进去(common/train_utils.py 第 243、314 行)。所以到点直接停, 把最后一个存档连 training_state/ 一起拉下来,以后随时能再 resume 续到 20000。

⚠️ 下载时千万记得带上 training_state/。上次就是只下了 pretrained_model/, 才让人以为不能续跑。

按官方那个实测点(20000 步 / batch 64 / 约 4 小时)估:+1 小时到约 11000 步, +2 小时到约 16000 步(正好等于老模型的更新次数),+2.8 小时跑满 20000。

七·五、一次训练能回答三个问题

中间存档不要钱,所以一次 resume 就能同时做三组对照:

拿谁比谁唯一的差别回答什么
现有的 6000  vs  新的 20000只有步数是不是「只是没训完」
新的 16000  vs  老模型 16000只有 batch(2 vs 64)大 batch 本身有没有害
新的 16000  vs  新的 20000只有曲线走没走完把温降完值不值那 0.8 小时

每个存档的测法一样:杯子摆训练用过的 5 个位置,每位 2 次共 10 次,每次记「成功/失败、闭爪时腰部转到了多少度、有没有降到杯沿以下、失败类型」。腰部转角是重点 —— 它是目前唯一被量化证明坍缩的量。

如果所有存档都一样失败,那说明训练超参根本不是瓶颈,就别再往训练上花钱了,去补位置数据(第十节)和修换块跳变。

八、我不建议做的三件事

九、这一页里,哪些是实测、哪些是推断

结论依据性质
两次训练除了 batch 和 eval_split,其余逐项相同两个 train_config.json 并排读实测
官方配方是 batch 64 / 20000 步 / 4 小时docs/source/smolvla.mdx 第 49、57–66 行实测(文档原文)
降温曲线按 --steps 自动伸缩schedulers.py 第 149–153 行实测(代码)
停下时学习率 7.99e-05 vs 1.18e-05按上述代码复算。老模型日志在 step:18K 处打的是 lr:5.5e-06,复算在 17,700 步处为 5.65e-06 —— 日志的步数是取整显示的,两者一致实测
模型闭爪时腰角散布压到三成、从未超过 +1.7°今天 20 次试验的 trace,27 个闭爪时刻实测
示范里图像位置与关节角相关系数 0.953 / −0.84650 集头部相机首帧 + 抓取帧关节角实测
「没训完 → 先学共同点、后学细节」训练过程的一般规律,与上面的数字一致推断(但和实测吻合)
batch 16 约 1.3 小时从官方唯一一个实测点按比例推的估算,开跑三分钟即可校准
平移增强会抹掉空间线索位置间距 0.06 vs 增强幅度 ±0.05,都是实测;「因此有害」是推理推断(依据是实测的)

十、就算这轮训好了,还有一个坑

五个杯子位置在画面里落在 0.56 / 0.62 / 0.70 / 0.82 / 0.84。注意 0.70 和 0.82 之间是空的——这一段没有任何训练数据。

你今天试的「右边偏内」那个位置,很可能正好落在这个洞里。所以:先把训练配方修对(这一页),再去补位置数据。顺序反了的话,你会分不清是配方的问题还是数据的问题。

← 回到目录