Robotics
LeRobot
Safetensors
imitation-learning
act
b-spline
so101
bspline_act
File size: 10,551 Bytes
64bc958
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
---
license: apache-2.0
library_name: lerobot
pipeline_tag: robotics
tags:
  - robotics
  - lerobot
  - imitation-learning
  - act
  - b-spline
  - so101
  - bspline_act
datasets:
  - atharva-pantheon/pick_block_bowl
model_name: pickblock_bspline_act
---

# pickblock_bspline_act

An [ACT](https://huggingface.co/papers/2304.13705) policy that predicts **B-spline
trajectory segments** instead of discrete action chunks, trained on SO-101 black-cube
picking. This is the "Reg.+BSP" variant from
[B-spline Policy: Accelerating Manipulation Policies via B-spline Action Representations](https://arxiv.org/abs/2607.09648).

The policy outputs a curve, so executing `a(n·t)` replays the same trajectory geometry
n times faster without retraining. Speed-up is an inference flag, verified exact to
0.000e+00 deviation at 2× and 4×.

> [!WARNING]
> **This model is trained on 13 episodes and does not generalize well.** Held-out error
> is 22.8° against 6.2° on training data — a 3.7× gap that never closed across 40,000
> steps. It has never been run on hardware. Treat it as a pipeline validation artifact,
> not a working policy. See [Evaluation](#evaluation) and [Limitations](#limitations).

## Download

```bash
hf download Dimios45/pickblock_bspline_act --local-dir ./pickblock_bspline_act
```

The repository root holds the recommended checkpoint, so `--policy.path` also accepts
the repo id directly. Individual checkpoints live under `checkpoints/<step>/`.

## Run it

```bash
lerobot-rollout \
    --strategy.type=base \
    --policy.path=./pickblock_bspline_act/checkpoints/010000/pretrained_model \
    --policy.speed_up=1.0 \
    --device=cuda \
    --robot.type=so101_follower \
    --robot.port=/dev/ttyACM0 --robot.id=FOLLOWER \
    --robot.cameras='{front: {type: opencv, index_or_path: 0, width: 640, height: 360, fps: 30}, ee: {type: opencv, index_or_path: 4, width: 640, height: 360, fps: 30}}' \
    --task="Grab the black cube" \
    --fps=30 --duration=60
```

`--policy.path` takes either a Hub repo id (`namespace/name`) or a **local directory
containing `config.json`** — which is the `pretrained_model/` subdirectory, not the
step directory above it. There is no `--policy.subfolder` flag.

### Two things differ from the cup-stacking model — check both

**Resolution is 640×360, not 640×480.** The source demonstrations were recorded at
1920×1080; they were downscaled to 640×360 for training, preserving the 16:9 aspect
ratio. Your cameras must be configured to match.

**The cameras are `front` and `ee`**, not `front` and `gripper`:

| Key | Shape |
|---|---|
| `observation.images.front` | `(3, 360, 640)` |
| `observation.images.ee` | `(3, 360, 640)` |
| `observation.state` | `(6,)` |

Joint order is `shoulder_pan, shoulder_lift, elbow_flex, wrist_flex, wrist_roll, gripper`,
in degrees. Swapping the two cameras produces confident but wrong behaviour rather than
an error.

To try the speed-up, raise `--policy.speed_up` to 2.0 or 4.0. The paper reaches 4× on
cube picking specifically, so this task should tolerate it better than precise-placement
tasks — but validate at 1× first, and note this policy has not been shown to complete
the task at any speed.

### Requirements

This policy type is not in upstream LeRobot. You need a checkout containing the
`bspline_act` policy, installed with the scipy extra:

```bash
uv pip install -e ".[feetech,bspline]"
```

`scipy>=1.15` is required for `interpolate.generate_knots`.

Unnormalization statistics are stored as buffers inside `model.safetensors`, so each
checkpoint is self-contained — you do not need the training dataset on the robot machine.

## Checkpoints

| Step | Epoch | Train error | **Held-out error** | Eval loss | |
|---|---|---|---|---|---|
| **10000** | 58 | 6.19° | **22.81°** | **0.3903** | **recommended — best** |
| 20000 | 116 | 4.67° | 24.15° | 0.3986 | |
| 30000 | 174 | 3.90° | 24.77° | 0.3996 | |
| 40000 | 234 | 3.44° | 26.22° | 0.4030 | final, worst held-out |

Held-out error gets **monotonically worse** after step 10,000 while training error keeps
falling. Use 10000. Later checkpoints are included only for reproducibility — they are
strictly worse policies.

Error is mean absolute joint error in degrees, worst joint, over each predicted
segment's horizon (~0.76 s), measured by decoding the spline and comparing against
ground-truth actions.

## Training

| | |
|---|---|
| Steps | 40,000 |
| Batch size | 32 |
| Epochs | 234 |
| Dataset | [`atharva-pantheon/pick_block_bowl`](https://huggingface.co/datasets/atharva-pantheon/pick_block_bowl) → downscaled → idle-trimmed → B-spline converted |
| Episodes | **13 train / 3 held out** |
| Frames | 6,422 (from 10,342; 37.9% boundary idle removed) |
| Optimizer | AdamW, lr 2e-5 (backbone 1e-5), wd 1e-3 |
| Precision | bf16 autocast + `channels_last` |
| Augmentation | ColorJitter (brightness/contrast/saturation/hue/sharpness), up to 3 per frame |
| Hardware | 1× RTX 4090, ~2 h 45 min total |
| Seed | 1000 |

Augmentation and 10× weight decay were added specifically to combat overfitting. They
slowed the fit to training data and **moved held-out loss by 0.0003**. An unaugmented
control run is available on request; the two are indistinguishable on unseen data.

### Preprocessing

Source video is 1920×1080 AV1, which is unusable for ACT directly — a ResNet18
stride-32 feature map is 34×60, putting over 4,000 tokens into the transformer encoder
across two cameras. Transcoded to 640×360 H.264, which also sped up random-access
decode from 40 to 183 frames/s per worker.

Leading and trailing motionless frames were then removed: 46 s of lead-in and 27 s of
trailing idle. This dataset is unusually idle-heavy — 37.9% of frames — including
20 s and 32 s dead stretches at the ends of two episodes where recording continued
after the task finished.

Motion is measured as peak-to-peak joint range over a ±0.5 s window rather than
frame-to-frame delta, because isolated jitter samples inside a dead stretch defeat a
delta test and silently leave the idle in.

## Architecture

Standard ACT with one change: the decoder emits a B-spline **parameter matrix** rather
than an action chunk.

```
(n_knots, 1 + action_dim) = (16, 7) = 112 values per prediction
   column 0   knot vector, in source-frame units, 0 = "now"
   columns 1: control points, one per joint
```

| | |
|---|---|
| Params | 51.6 M |
| Vision backbone | ResNet18 (ImageNet init, fine-tuned) |
| dim_model / chunk_size | 512 / 16 |
| VAE | enabled, kl_weight 10.0 |
| B-spline degree | 3 (cubic, C² continuous) |
| bspline_chunk_size | 10 |
| Fitting tolerance ε | 0.2 (degrees) |
| Max knot span | 6 frames |

The paper uses a DINOv2 encoder for its regression variant; this port uses LeRobot's
ResNet18, which is what LeRobot's ACT supports. The paper reports no backbone ablation,
and holds the encoder fixed between its baseline and its B-spline variant, so none of
its claims depend on that choice.

Segments span 0.50–1.10 s (0.76 s mean). The network runs only when a segment is
exhausted, which decouples policy rate from control rate.

### On `max_knot_span`

This dataset is why the flag exists. Adaptive knot fitting covers a motionless stretch
with one enormous knot span, and uncapped fitting here produced segments with a **p95
of 18.4 seconds** — the policy would commit to eighteen seconds of open-loop motion
without looking at a camera. Capping span width at 6 frames bounds that at
`chunk_size × 6` = 2 s, costing 0.04× compression.

After correct idle-trimming the uncapped p95 drops to 1.17 s on its own, so the dead
recording time was the real cause. The cap remains as a cheap safety rail.

## Evaluation

**Offline only. No hardware evaluation has been performed.**

| | Train (13 eps) | **Held-out (3 eps)** |
|---|---|---|
| Best checkpoint (10k) | 6.19° | **22.81°** |
| Final (40k) | 3.44° | 26.22° |

Reference points:

- **Inter-demonstration spread: 54.83°** — how far apart two human demonstrations of
  this task are, phase-aligned. At 22.8° the policy is well inside that, so it is
  extracting real per-episode information from the cameras rather than replaying an
  average trajectory. This is the one genuinely positive result.
- **Hold-still baseline: 16.23°.** The policy scores worse than this. Over a 0.76 s
  horizon on a slow task, "don't move" is a decent short-horizon *predictor* while
  being a useless *policy*, so this is not the damning comparison it looks like — but
  combined with the widening train/eval gap it is not encouraging either.

Held-out loss plateaued at ~0.39 by step 2,000 and never improved across the remaining
38,000 steps.

### Why the held-out number is a pessimistic bound

The split holds out the last 3 of 16 episodes. Inspecting the raw recording:

- 8 of 15 episode boundaries are contiguous to under 1° (several to exactly 0.00°), so
  episodes were largely cut from a continuous teleoperation stream. Adjacent episodes
  are correlated, meaning an interleaved split would leak.
- There is a 97.67° discontinuity between episodes 9 and 10, and episodes 10, 13, 14
  and 15 all end with the gripper open (8–16°) while the other twelve end closed (~2°).
- **All three held-out episodes sit after that change point**, and two of them *start*
  with the gripper open — a state the model never sees at t=0 in training.

So the tail split is biased pessimistic and an interleaved split would be biased
optimistic. With 16 semi-continuous episodes there is no clean holdout. 22.8° is a
lower bound on quality, not a verdict.

## Limitations

- **13 training episodes is too few for this task.** This is the dominant limitation and
  no hyperparameter fixed it. 40–50 demonstrations with varied cube placement and an
  **independent reset between takes** would address both the training and the
  measurement problem.
- **No hardware evaluation.** Task success rate is unknown, and given the above it
  should not be assumed to be non-zero.
- **Held-out split is confounded** — see above.
- **Resolution and camera names differ** from the cup-stacking model. Check both.
- **Camera assignment is silent when wrong.**

## Citation

```bibtex
@article{han2026b,
  title={B-spline Policy: Accelerating Manipulation Policies via B-spline Action Representations},
  author={Han, Xiaoshen and Xiong, Haoyu and Chen, Haonan and Liu, Chaoqi and
          Torralba, Antonio and Zhu, Yuke and Du, Yilun},
  journal={arXiv preprint arXiv:2607.09648},
  year={2026}
}
```