Bigenlight commited on
Commit
201c5e3
·
verified ·
1 Parent(s): 912bb2f

Upload folder using huggingface_hub

Browse files
README.md ADDED
@@ -0,0 +1,409 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ library_name: lerobot
4
+ tags:
5
+ - robotics
6
+ - lerobot
7
+ - diffusion-policy
8
+ - imitation-learning
9
+ - ur7e
10
+ - manipulation
11
+ pipeline_tag: robotics
12
+ datasets:
13
+ - Bigenlight/banana_in_pot_lerobot_v3
14
+ ---
15
+
16
+ # Diffusion Policy — Put the right banana in the pot (UR7e, JOINT action space)
17
+
18
+ A **Diffusion Policy** (visuomotor DDPM, 1D-conv UNet denoiser) trained by imitation
19
+ learning to perform the manipulation task *"put the right banana in the pot"* on a
20
+ **Universal Robots UR7e** arm with two RGB cameras. Actions are **7-D absolute joint
21
+ targets** (6 UR joints in radians + gripper).
22
+
23
+ - **Policy:** LeRobot `DiffusionPolicy` — per-camera **ResNet18** visual encoder
24
+ (ImageNet-pretrained) + **SpatialSoftmax** keypoints, conditioning a **1D convolutional
25
+ UNet** denoiser. Receding-horizon action generation: `horizon = 64`, `n_obs_steps = 2`,
26
+ `n_action_steps = 32`.
27
+ - **Noise model:** **DDPM**, `num_train_timesteps = 100`, `beta_schedule =
28
+ squaredcos_cap_v2`, `prediction_type = epsilon` (ε-prediction), `clip_sample = true`.
29
+ - **Trained on:** [`Bigenlight/banana_in_pot_lerobot_v3`](https://huggingface.co/datasets/Bigenlight/banana_in_pot_lerobot_v3)
30
+ — 51 teleoperated episodes / 21,524 frames, UR7e follower + GELLO leader, 2 RGB cameras.
31
+ - **This checkpoint:** step **80,000** (best by open-loop held-out MAE; see
32
+ [Results](#results--the-headline-finding)).
33
+ - **Framework:** [LeRobot](https://github.com/huggingface/lerobot) v0.6.1.
34
+
35
+ > **Headline finding (read this first):** on a held-out split the diffusion **denoising
36
+ > `eval_loss` ROSE ~5× (0.029 → 0.149)** over training, which naively screams "severe
37
+ > overfitting". But the deployment-relevant **open-loop rollout MAE kept IMPROVING** to
38
+ > step 80k (0.119 → 0.085 rad). **For a Diffusion Policy the held-out denoising loss is a
39
+ > misleading overfit/early-stop signal — select checkpoints by open-loop MAE, not by
40
+ > `eval_loss`.** (Contrast the ACT sibling, where the two signals agreed.)
41
+
42
+ ---
43
+
44
+ ## Task & data
45
+
46
+ **"put the right banana in the pot."** The tabletop holds several distractor objects —
47
+ **two bananas, an apple, carrots/peppers, and a slice of watermelon** — plus a **silver
48
+ pot**. The operator must grasp the **RIGHT banana** (the target) and place it inside the
49
+ pot. Success = the right banana ends up inside the pot. Every demonstration is a success.
50
+
51
+ - **Dataset:** [`Bigenlight/banana_in_pot_lerobot_v3`](https://huggingface.co/datasets/Bigenlight/banana_in_pot_lerobot_v3)
52
+ (LeRobot v3.0 format).
53
+ - **Scale:** **51 episodes / 21,524 frames / 30 fps / ~12 min.**
54
+ - **Action / state space:** 7-D absolute joint (6 UR joints in radians + gripper), i.e.
55
+ `[cmd1..cmd6, grip_cmd]`. The gripper channel is effectively binary (open/close).
56
+ - **Cameras:** two RGB viewpoints (Intel RealSense D435 + D435if), captured at 1280×720
57
+ (720p) @ 30 fps, **RGB only** (no depth / IR). `cam1 ↔ cam2` order is fixed and must be
58
+ preserved at deploy time.
59
+
60
+ ### Train / held-out split
61
+
62
+ Training used `--dataset.eval_split=0.117`, which holds out the **LAST
63
+ `ceil(51 × 0.117) = 6` episodes (indices 45–50)** as a true validation split and trains on
64
+ the other **45** episodes (0–44). The held-out episodes 45–50 are used both for the
65
+ in-training denoising `eval_loss` probe and for all offline open-loop evaluation below.
66
+
67
+ ---
68
+
69
+ ## Model architecture
70
+
71
+ LeRobot `DiffusionPolicy`. All values below are quoted directly from the checkpoint's
72
+ `config.json` / `train_config.json`.
73
+
74
+ **Observation encoder (vision):**
75
+
76
+ | Item | Value |
77
+ |---|---|
78
+ | Vision backbone | `resnet18` |
79
+ | Pretrained weights | `ResNet18_Weights.IMAGENET1K_V1` (ImageNet) |
80
+ | Per-camera encoder | `use_separate_rgb_encoder_per_camera = true` (separate ResNet18 per view) |
81
+ | Pooling | **SpatialSoftmax**, `spatial_softmax_num_keypoints = 32` |
82
+ | Group norm in encoder | `use_group_norm = false` (keeps BatchNorm from the pretrained backbone) |
83
+ | Cameras | 2 × RGB (`observation.images.cam1`, `observation.images.cam2`) |
84
+ | Network input resolution | **360 × 640** (`resize_shape = [360, 640]`; see [why](#why-the-two-non-default-flags-are-required)) |
85
+ | Crop | **OFF** — `crop_shape = null`, `crop_ratio = 1.0` (`crop_is_random` is moot with no crop) |
86
+ | State input | `observation.state`, shape `(7,)` |
87
+
88
+ > Note: `config.json` records the raw dataset image feature shape as `[3, 720, 1280]`, but
89
+ > the on-the-fly `Resize` to `[360, 640]` (`resize_shape`) means the network actually sees
90
+ > **360 × 640** frames. See the training section for why this must match at inference.
91
+
92
+ **Denoiser (conditional 1D-conv UNet):**
93
+
94
+ | Item | Value |
95
+ |---|---|
96
+ | Denoiser | 1D convolutional UNet (Diffusion Policy / Janner-style) |
97
+ | `down_dims` | `[512, 1024, 2048]` |
98
+ | `kernel_size` | `5` |
99
+ | `n_groups` (GroupNorm) | `8` |
100
+ | `diffusion_step_embed_dim` | `128` |
101
+ | FiLM conditioning | `use_film_scale_modulation = true` |
102
+ | `horizon` | `64` (prediction horizon, in frames) |
103
+ | `n_obs_steps` | `2` (observation context length) |
104
+ | `n_action_steps` | `32` (actions executed before replanning) |
105
+
106
+ **Diffusion process (noise scheduler):**
107
+
108
+ | Item | Value |
109
+ |---|---|
110
+ | `noise_scheduler_type` | `DDPM` |
111
+ | `num_train_timesteps` | `100` |
112
+ | `beta_schedule` | `squaredcos_cap_v2` (cosine) |
113
+ | `beta_start` / `beta_end` | `0.0001` / `0.02` |
114
+ | `prediction_type` | `epsilon` (predict noise) |
115
+ | `clip_sample` | `true`, `clip_sample_range = 1.0` |
116
+ | `num_inference_steps` | `null` in config → defaults to the full DDPM schedule at inference unless overridden (evals here used **DDIM, 10 steps** for speed — see [Usage](#usage--inference)) |
117
+
118
+ **Normalization (`normalization_mapping`):**
119
+
120
+ | Feature group | Mode |
121
+ |---|---|
122
+ | `VISUAL` (images) | `MEAN_STD` (ImageNet stats, `use_imagenet_stats = true`) |
123
+ | `STATE` (observation.state) | `MIN_MAX` |
124
+ | `ACTION` (action) | `MIN_MAX` |
125
+
126
+ Normalizer statistics are baked into the pre/post-processor pipelines saved alongside the
127
+ checkpoint (`policy_preprocessor.json` / `policy_postprocessor.json`), not into
128
+ `forward()`.
129
+
130
+ **I/O summary:**
131
+
132
+ | I/O | Spec |
133
+ |---|---|
134
+ | `observation.state` | `(7,)` — UR joints `q1..q6` (radians) + gripper position |
135
+ | `observation.images.cam1` / `cam2` | RGB, network input **360 × 640** |
136
+ | `action` | `(7,)` — `[cmd1..cmd6, grip_cmd]`, **absolute** joint targets (radians) + ~binary gripper |
137
+
138
+ ---
139
+
140
+ ## Training setup
141
+
142
+ Trained with `lerobot-train` (LeRobot 0.6.1). Exact invocation:
143
+ `train_diffusion_joint_valdiag.sh`. Values below are from that script and the saved
144
+ `train_config.json`.
145
+
146
+ | Item | Value |
147
+ |---|---|
148
+ | Policy | `diffusion` (`--policy.type=diffusion`) |
149
+ | Dataset | `banana_in_pot_lerobot_v3`, `--dataset.eval_split=0.117` (holds out eps 45–50) |
150
+ | Batch size | **8** |
151
+ | Steps | script requested 100,000; this release / analysis is the run **to 80,000** (`steps: 80000` in `train_config.json`); checkpoints saved every 10,000 |
152
+ | Optimizer | **Adam**, `lr = 1e-4`, `betas = [0.95, 0.999]`, `eps = 1e-8`, `weight_decay = 1e-6` |
153
+ | Grad clip | `grad_clip_norm = 10.0` |
154
+ | LR scheduler | **cosine** (`scheduler.type = diffuser`, `name = cosine`), `num_warmup_steps = 500` |
155
+ | Seed | **1000** |
156
+ | Precision | **fp32** (`use_amp = false`) |
157
+ | EMA | **none** (no EMA weights in this config) |
158
+ | Image transform | on-the-fly `Resize` to `[360, 640]`, `max_num_transforms = 1`, deterministic |
159
+ | `drop_n_last_frames` | **31** (non-default; see below) |
160
+ | `resize_shape` | `[360, 640]` (non-default; see below) |
161
+ | Workers | `num_workers = 4`, `prefetch_factor = 4`, `persistent_workers = true` |
162
+ | Eval probe | held-out denoising `eval_loss` every 2,000 steps; train loss logged every 200 |
163
+ | GPU | single **RTX 3060 12 GB**, ~**9.7 GB** used, ~**2.2 step/s** |
164
+ | W&B | disabled |
165
+
166
+ ### Why the two non-default flags are required
167
+
168
+ Both `resize_shape=[360,640]` and `drop_n_last_frames=31` are **not** the LeRobot defaults;
169
+ they are mandatory for this dataset/config and encode real operational knowledge:
170
+
171
+ 1. **`resize_shape=[360,640]` — SpatialSoftmax is shape-rigid.** The Diffusion Policy RGB
172
+ encoder ends in a `SpatialSoftmax` layer whose keypoint geometry is fixed to the spatial
173
+ dimensions of the feature map at build time. The network must therefore be *constructed*
174
+ for the exact input resolution it will ever see. Setting `resize_shape=[360,640]` builds
175
+ the encoder for 360×640 and — combined with crop being **off** (`crop_shape=null`) —
176
+ guarantees the training image path, the offline-eval image path, and any deploy image
177
+ path all feed the encoder identically. A mismatched resolution (or leaving crop on)
178
+ changes the SpatialSoftmax grid and breaks the model. The same 360×640 `Resize` is
179
+ reproduced in `eval_offline.py` (`build_image_transforms`).
180
+
181
+ 2. **`drop_n_last_frames=31` — horizon / n_action off-by-one at episode ends.** The
182
+ trajectory sampler must not draw a window that runs past the end of an episode. With
183
+ `horizon=64`, `n_obs_steps=2`, and `n_action_steps=32`, the last valid start frame in an
184
+ episode has to leave room for the horizon, so the correct number of trailing frames to
185
+ drop is `horizon - n_action_steps - (n_obs_steps - 1) = 64 - 32 - 1 = 31`. Using the
186
+ default (7, tuned for the reference `horizon=16` config) would let the sampler pull
187
+ frames off the end of an episode and corrupt the action targets. **If you change
188
+ `horizon`/`n_obs_steps`/`n_action_steps`, recompute `drop_n_last_frames`.**
189
+
190
+ ---
191
+
192
+ ## Results & the headline finding
193
+
194
+ ![Diffusion joint: held-out denoising eval_loss (blue, rising) vs open-loop rollout MAE (improving to 80k)](assets/diffusion_joint_overfit_diag.png)
195
+
196
+ Offline **open-loop** evaluation on the held-out episodes **45–50** with `eval_offline.py`
197
+ (each logged observation is fed to `select_action`; the predicted action is compared to the
198
+ dataset ground truth). Sampling used **DDIM with 10 inference steps**
199
+ (`--scheduler DDIM --num-inference-steps 10`) for ~10× faster rollouts; DDIM is a valid
200
+ sampler for a DDPM-trained ε model (same beta schedule). `poseMAE` is the mean absolute
201
+ error over the 6 joint dims (radians); `gripAcc` is the binary gripper-open/close accuracy
202
+ (threshold 0.5); `overall L1` averages all 7 dims.
203
+
204
+ | checkpoint | poseMAE (rad) | gripAcc | overall L1 |
205
+ |---|---|---|---|
206
+ | 10k | 0.1193 | 0.729 | 0.1454 |
207
+ | 20k | 0.1037 | 0.888 | 0.1078 |
208
+ | 30k | 0.0921 | 0.919 | 0.0928 |
209
+ | 40k | 0.0907 | 0.949 | 0.0862 |
210
+ | 50k | 0.0865 | 0.942 | 0.0832 |
211
+ | 60k | 0.0849 | 0.944 | 0.0812 |
212
+ | 70k | 0.0855 | 0.951 | 0.0809 |
213
+ | **80k** ⭐ | **0.0845** | **0.953** | **0.0796** |
214
+
215
+ **Best checkpoint = 80k** (tied-best poseMAE, best gripAcc). Open-loop poseMAE improves
216
+ monotonically then **plateaus at ~0.085 rad from 60k onward** (60k/70k/80k =
217
+ 0.0849/0.0855/0.0845, within eval noise); gripper accuracy climbs all the way to **0.953 @
218
+ 80k**. There is **no open-loop overfitting through 80k**.
219
+
220
+ ### The misleading `eval_loss` (the lesson)
221
+
222
+ During training the held-out **denoising `eval_loss`** (LeRobot's in-training validation
223
+ probe, computed under `policy.eval()` on eps 45–50) did the opposite of the rollout metric:
224
+
225
+ | step | train loss | held-out eval_loss |
226
+ |---|---|---|
227
+ | 2k | 0.0310 | 0.0361 |
228
+ | 4k | 0.0240 | 0.0289 (min region) |
229
+ | 6k | 0.0210 | 0.0303 |
230
+ | 20k | 0.0120 | 0.0399 |
231
+ | 40k | 0.0080 | 0.0660 |
232
+ | 60k | 0.0050 | 0.1272 |
233
+ | 80k | 0.0050 | ~0.1487 |
234
+
235
+ Read naively, the held-out `eval_loss` bottoms near step 4k–6k and then rises ~5×, so an
236
+ early-stop rule would pick **~step 6k** and declare "severe overfit". **That
237
+ recommendation is wrong for deployment:** the same held-out episodes, evaluated by
238
+ open-loop rollout, get *monotonically better* out to 80k.
239
+
240
+ **Why:** the denoising loss is a per-sample ε-regression on a *randomly re-sampled noise
241
+ vector and diffusion timestep at every forward pass* — it is (a) high-variance/stochastic
242
+ by construction and (b) only loosely coupled to closed-loop action quality. As the model
243
+ sharpens its learned action distribution, the average ε-MSE on unseen frames can rise even
244
+ while the *sampled* action trajectories become more accurate. **Takeaway: for a Diffusion
245
+ Policy, select checkpoints and early-stop by open-loop rollout MAE, not by held-out
246
+ denoising `eval_loss`.** (The ACT sibling did not show this divergence — there the two
247
+ signals agreed — so this is a diffusion-specific pitfall.)
248
+
249
+ ---
250
+
251
+ ## Usage / inference
252
+
253
+ ### Load the policy (LeRobot 0.6.1)
254
+
255
+ Normalization is **not** baked into `forward()` in LeRobot 0.6.1 — it lives in the
256
+ pre/post-processor pipelines saved with the checkpoint. `select_action` returns a
257
+ **normalized** action; the post-processor converts it back to radians.
258
+
259
+ ```python
260
+ import torch
261
+ from lerobot.configs import PreTrainedConfig
262
+ from lerobot.policies.factory import get_policy_class, make_pre_post_processors
263
+
264
+ CKPT = "Bigenlight/diffusion_banana_in_pot_joint"
265
+ device = "cuda"
266
+
267
+ # (optional) speed up sampling: DDIM with 10 steps instead of the full DDPM schedule.
268
+ cfg = PreTrainedConfig.from_pretrained(CKPT)
269
+ cfg.pretrained_path = CKPT
270
+ cfg.device = device
271
+ cfg.noise_scheduler_type = "DDIM" # valid sampler for a DDPM-trained epsilon model
272
+ cfg.num_inference_steps = 10 # ~10x faster rollouts
273
+
274
+ policy = get_policy_class(cfg.type).from_pretrained(CKPT, config=cfg) # -> DiffusionPolicy
275
+ policy.to(device)
276
+ policy.eval()
277
+
278
+ preprocessor, postprocessor = make_pre_post_processors(
279
+ policy_cfg=cfg,
280
+ pretrained_path=CKPT,
281
+ preprocessor_overrides={"device_processor": {"device": device}},
282
+ )
283
+ ```
284
+
285
+ ### Run the control loop
286
+
287
+ Build the observation dict exactly as training did: joint state `(7,)` plus **both**
288
+ cameras as RGB CHW tensors in `[0, 1]`, **resized to 360×640** (aspect-preserving
289
+ half-resolution). `cam1`/`cam2` must map to the same physical viewpoints as at collection.
290
+
291
+ ```python
292
+ policy.reset() # once at the start of each episode/rollout
293
+ preprocessor.reset()
294
+ postprocessor.reset()
295
+
296
+ # obs = {
297
+ # "observation.state": state_7, # (7,) float32, radians + gripper
298
+ # "observation.images.cam1": img1_chw, # (3, 360, 640) float32 in [0,1]
299
+ # "observation.images.cam2": img2_chw, # (3, 360, 640) float32 in [0,1]
300
+ # "task": "put the right banana in the pot",
301
+ # }
302
+
303
+ with torch.inference_mode():
304
+ proc = preprocessor(obs) # rename -> add batch dim -> device -> normalize
305
+ action = policy.select_action(proc) # (1, 7) NORMALIZED
306
+ action = postprocessor(action) # (1, 7) radians, on cpu
307
+ q_target = action.squeeze(0).numpy() # (7,) -> [cmd1..cmd6, grip_cmd]
308
+ ```
309
+
310
+ `select_action` returns **one** action per call from an internal queue. Because
311
+ `n_action_steps = 32`, the policy denoises a fresh action sequence, executes 32 actions
312
+ from it, then replans (with `n_obs_steps = 2` frames of observation context). Call
313
+ `policy.reset()` at the start of every episode to clear that queue. The gripper channel
314
+ `grip_cmd` is ~binary — threshold at `> 0.5 → close` and map to your gripper driver.
315
+
316
+ ### Reproduce the offline evaluation
317
+
318
+ The repo's `eval_offline.py` runs the exact open-loop protocol used for the results table
319
+ (same 360×640 `Resize`, same normalization via the saved processors):
320
+
321
+ ```bash
322
+ python eval_offline.py \
323
+ --checkpoint outputs/train/diffusion_joint_val_diag/checkpoints/080000/pretrained_model \
324
+ --episodes 45,46,47,48,49,50 \
325
+ --device cuda \
326
+ --scheduler DDIM --num-inference-steps 10 \
327
+ --out eval_out_diffusion_80k
328
+ ```
329
+
330
+ `--scheduler DDIM --num-inference-steps 10` gives the ~10× rollout speedup; omit them to
331
+ sample with the full trained DDPM schedule (`num_train_timesteps = 100`).
332
+
333
+ ---
334
+
335
+ ## Deployment on a real UR7e
336
+
337
+ Closed-loop deployment targets a real **UR7e** through the ROS 2 Humble stack in
338
+ [**Bigenlight/gello_software**](https://github.com/Bigenlight/gello_software) — the same
339
+ stack used to collect this dataset (UR7e follower + GELLO leader, dual RealSense cameras).
340
+ LeRobot ships **no UR robot class**, so deployment requires a small **policy deploy node**
341
+ that, each control tick (target **30 Hz**):
342
+
343
+ 1. reads the UR7e measured joints + gripper → `observation.state` `(7,)`;
344
+ 2. grabs both camera frames, BGR→RGB, **resizes to 360×640**, CHW `[0,1]` →
345
+ `observation.images.cam1` / `cam2`;
346
+ 3. `preprocessor → policy.select_action → postprocessor` → `q_target` (7,);
347
+ 4. streams `q_target[:6]` to the arm (e.g. `servoJ` via `ur_rtde` / the ROS 2 driver) and
348
+ drives the gripper from `grip_cmd`.
349
+
350
+ This diffusion policy would need a **deploy node analogous to the ACT one**
351
+ ([`Bigenlight/act_banana_in_pot`](https://huggingface.co/Bigenlight/act_banana_in_pot)),
352
+ with two differences: (a) the action queue length is `n_action_steps = 32` (not ACT's 100),
353
+ so it replans ~every 32 ticks; and (b) inference runs a diffusion sampler — use **DDIM /
354
+ 10 steps** to keep per-replan latency low enough for 30 Hz.
355
+
356
+ **Safety — actions are ABSOLUTE joint positions:**
357
+
358
+ 1. **Start near the dataset initial pose** before enabling the policy, or the first absolute
359
+ command is a large jump.
360
+ 2. **First-command jump guard:** if `max(|q_target − getActualQ()|)` exceeds a small
361
+ threshold (~0.15 rad), **abort**.
362
+ 3. **Clamp per-tick joint change** and clamp to UR software joint limits; run at reduced
363
+ speed for first trials with a hand on the **E-stop**.
364
+ 4. **`cam1`/`cam2` mapping is fixed** — swap the two views and the policy fails silently.
365
+ Verify wiring every session.
366
+
367
+ ---
368
+
369
+ ## Limitations & intended use
370
+
371
+ - **Small, single-task lab dataset:** 51 demonstrations, one scene layout, one operator.
372
+ Expect limited generalization to novel object arrangements, lighting, or camera placement.
373
+ - **Success-only demonstrations:** no failure/recovery data; not suited as-is for methods
374
+ that need negative examples.
375
+ - **Offline metrics only:** the best checkpoint (80k) reaches **held-out poseMAE ≈ 0.085
376
+ rad** and gripper accuracy ≈ 0.953 in open-loop rollout. These are *not* closed-loop task
377
+ success rates — real closed-loop success on hardware has not been measured here and must
378
+ be validated on the arm.
379
+ - **Absolute-joint action space** demands the safety guards above; the policy was only ever
380
+ conditioned on states near the data-collection start pose.
381
+ - **Not for production.** Intended for research in imitation learning / diffusion policies
382
+ for robot manipulation. Workspace-, robot-, and camera-specific.
383
+ - The ResNet18 encoders are **ImageNet-pretrained** (not robotics-pretrained); the UNet
384
+ denoiser is trained from scratch on this task.
385
+
386
+ ---
387
+
388
+ ## Links
389
+
390
+ - **Dataset:** [`Bigenlight/banana_in_pot_lerobot_v3`](https://huggingface.co/datasets/Bigenlight/banana_in_pot_lerobot_v3)
391
+ - **Experiments repo:** [github.com/Bigenlight/banana-in-pot-experiments](https://github.com/Bigenlight/banana-in-pot-experiments)
392
+ - **ACT sibling model:** [`Bigenlight/act_banana_in_pot`](https://huggingface.co/Bigenlight/act_banana_in_pot)
393
+ - **Deployment stack (ROS 2 Humble):** [github.com/Bigenlight/gello_software](https://github.com/Bigenlight/gello_software)
394
+ - **Framework:** [LeRobot](https://github.com/huggingface/lerobot) v0.6.1
395
+
396
+ ## Citation
397
+
398
+ ```bibtex
399
+ @misc{theo2026bananainpotdiffusion,
400
+ title = {Diffusion Policy for "put the right banana in the pot"
401
+ (UR7e, joint action space)},
402
+ author = {Theo and {Bigenlight}},
403
+ year = {2026},
404
+ howpublished = {\url{https://huggingface.co/Bigenlight/diffusion_banana_in_pot_joint}},
405
+ note = {LeRobot 0.6.1 DiffusionPolicy, trained on banana_in_pot_lerobot_v3}
406
+ }
407
+ ```
408
+
409
+ License: **Apache-2.0**.
assets/diffusion_joint_overfit_diag.png ADDED
config.json ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "type": "diffusion",
3
+ "n_obs_steps": 2,
4
+ "input_features": {
5
+ "observation.state": {
6
+ "type": "STATE",
7
+ "shape": [
8
+ 7
9
+ ]
10
+ },
11
+ "observation.images.cam1": {
12
+ "type": "VISUAL",
13
+ "shape": [
14
+ 3,
15
+ 720,
16
+ 1280
17
+ ]
18
+ },
19
+ "observation.images.cam2": {
20
+ "type": "VISUAL",
21
+ "shape": [
22
+ 3,
23
+ 720,
24
+ 1280
25
+ ]
26
+ }
27
+ },
28
+ "output_features": {
29
+ "action": {
30
+ "type": "ACTION",
31
+ "shape": [
32
+ 7
33
+ ]
34
+ }
35
+ },
36
+ "device": "cuda",
37
+ "use_amp": false,
38
+ "use_peft": false,
39
+ "push_to_hub": false,
40
+ "repo_id": null,
41
+ "private": null,
42
+ "tags": null,
43
+ "license": null,
44
+ "pretrained_path": "outputs/train/diffusion_joint_val_diag/checkpoints/last/pretrained_model",
45
+ "pretrained_revision": null,
46
+ "horizon": 64,
47
+ "n_action_steps": 32,
48
+ "normalization_mapping": {
49
+ "VISUAL": "MEAN_STD",
50
+ "STATE": "MIN_MAX",
51
+ "ACTION": "MIN_MAX"
52
+ },
53
+ "drop_n_last_frames": 31,
54
+ "vision_backbone": "resnet18",
55
+ "resize_shape": [
56
+ 360,
57
+ 640
58
+ ],
59
+ "crop_ratio": 1.0,
60
+ "crop_shape": null,
61
+ "crop_is_random": true,
62
+ "pretrained_backbone_weights": "ResNet18_Weights.IMAGENET1K_V1",
63
+ "use_group_norm": false,
64
+ "spatial_softmax_num_keypoints": 32,
65
+ "use_separate_rgb_encoder_per_camera": true,
66
+ "down_dims": [
67
+ 512,
68
+ 1024,
69
+ 2048
70
+ ],
71
+ "kernel_size": 5,
72
+ "n_groups": 8,
73
+ "diffusion_step_embed_dim": 128,
74
+ "use_film_scale_modulation": true,
75
+ "noise_scheduler_type": "DDPM",
76
+ "num_train_timesteps": 100,
77
+ "beta_schedule": "squaredcos_cap_v2",
78
+ "beta_start": 0.0001,
79
+ "beta_end": 0.02,
80
+ "prediction_type": "epsilon",
81
+ "clip_sample": true,
82
+ "clip_sample_range": 1.0,
83
+ "num_inference_steps": null,
84
+ "compile_model": false,
85
+ "compile_mode": "reduce-overhead",
86
+ "do_mask_loss_for_padding": false,
87
+ "optimizer_lr": 0.0001,
88
+ "optimizer_betas": [
89
+ 0.95,
90
+ 0.999
91
+ ],
92
+ "optimizer_eps": 1e-08,
93
+ "optimizer_weight_decay": 1e-06,
94
+ "scheduler_name": "cosine",
95
+ "scheduler_warmup_steps": 500
96
+ }
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d4722b60caee5d76d004a37e16b2d7adecc1668f79703259ea7260fc9c723c57
3
+ size 1111651364
policy_postprocessor.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "policy_postprocessor",
3
+ "steps": [
4
+ {
5
+ "registry_name": "unnormalizer_processor",
6
+ "config": {
7
+ "eps": 1e-08,
8
+ "features": {
9
+ "action": {
10
+ "type": "ACTION",
11
+ "shape": [
12
+ 7
13
+ ]
14
+ }
15
+ },
16
+ "norm_map": {
17
+ "VISUAL": "MEAN_STD",
18
+ "STATE": "MIN_MAX",
19
+ "ACTION": "MIN_MAX"
20
+ }
21
+ },
22
+ "state_file": "policy_postprocessor_step_0_unnormalizer_processor.safetensors"
23
+ },
24
+ {
25
+ "registry_name": "device_processor",
26
+ "config": {
27
+ "device": "cpu",
28
+ "float_dtype": null
29
+ }
30
+ }
31
+ ]
32
+ }
policy_postprocessor_step_0_unnormalizer_processor.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:035168b1aa56baa0e57608cf3058bcbe872d35d7a945a29271a2482c31fd094f
3
+ size 7608
policy_preprocessor.json ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "policy_preprocessor",
3
+ "steps": [
4
+ {
5
+ "registry_name": "rename_observations_processor",
6
+ "config": {
7
+ "rename_map": {}
8
+ }
9
+ },
10
+ {
11
+ "registry_name": "to_batch_processor",
12
+ "config": {}
13
+ },
14
+ {
15
+ "registry_name": "device_processor",
16
+ "config": {
17
+ "device": "cuda",
18
+ "float_dtype": null
19
+ }
20
+ },
21
+ {
22
+ "registry_name": "normalizer_processor",
23
+ "config": {
24
+ "eps": 1e-08,
25
+ "features": {
26
+ "observation.state": {
27
+ "type": "STATE",
28
+ "shape": [
29
+ 7
30
+ ]
31
+ },
32
+ "observation.images.cam1": {
33
+ "type": "VISUAL",
34
+ "shape": [
35
+ 3,
36
+ 720,
37
+ 1280
38
+ ]
39
+ },
40
+ "observation.images.cam2": {
41
+ "type": "VISUAL",
42
+ "shape": [
43
+ 3,
44
+ 720,
45
+ 1280
46
+ ]
47
+ },
48
+ "action": {
49
+ "type": "ACTION",
50
+ "shape": [
51
+ 7
52
+ ]
53
+ }
54
+ },
55
+ "norm_map": {
56
+ "VISUAL": "MEAN_STD",
57
+ "STATE": "MIN_MAX",
58
+ "ACTION": "MIN_MAX"
59
+ }
60
+ },
61
+ "state_file": "policy_preprocessor_step_3_normalizer_processor.safetensors"
62
+ }
63
+ ]
64
+ }
policy_preprocessor_step_3_normalizer_processor.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1798a108428d8b529f04a915825c270ed877b2489c744abe55164ecf13f43604
3
+ size 7616
train_config.json ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "dataset": {
3
+ "repo_id": "theo/banana_in_pot",
4
+ "root": "./banana_in_pot_lerobot",
5
+ "episodes": null,
6
+ "image_transforms": {
7
+ "enable": true,
8
+ "max_num_transforms": 1,
9
+ "random_order": false,
10
+ "tfs": {
11
+ "resize": {
12
+ "weight": 1.0,
13
+ "type": "Resize",
14
+ "kwargs": {
15
+ "size": [
16
+ 360,
17
+ 640
18
+ ]
19
+ }
20
+ }
21
+ }
22
+ },
23
+ "revision": null,
24
+ "use_imagenet_stats": true,
25
+ "video_backend": "torchcodec",
26
+ "return_uint8": false,
27
+ "depth_output_unit": "mm",
28
+ "streaming": false,
29
+ "eval_split": 0.117
30
+ },
31
+ "env": null,
32
+ "policy": {
33
+ "type": "diffusion",
34
+ "n_obs_steps": 2,
35
+ "input_features": {
36
+ "observation.state": {
37
+ "type": "STATE",
38
+ "shape": [
39
+ 7
40
+ ]
41
+ },
42
+ "observation.images.cam1": {
43
+ "type": "VISUAL",
44
+ "shape": [
45
+ 3,
46
+ 720,
47
+ 1280
48
+ ]
49
+ },
50
+ "observation.images.cam2": {
51
+ "type": "VISUAL",
52
+ "shape": [
53
+ 3,
54
+ 720,
55
+ 1280
56
+ ]
57
+ }
58
+ },
59
+ "output_features": {
60
+ "action": {
61
+ "type": "ACTION",
62
+ "shape": [
63
+ 7
64
+ ]
65
+ }
66
+ },
67
+ "device": "cuda",
68
+ "use_amp": false,
69
+ "use_peft": false,
70
+ "push_to_hub": false,
71
+ "repo_id": null,
72
+ "private": null,
73
+ "tags": null,
74
+ "license": null,
75
+ "pretrained_path": "outputs/train/diffusion_joint_val_diag/checkpoints/last/pretrained_model",
76
+ "pretrained_revision": null,
77
+ "horizon": 64,
78
+ "n_action_steps": 32,
79
+ "normalization_mapping": {
80
+ "VISUAL": "MEAN_STD",
81
+ "STATE": "MIN_MAX",
82
+ "ACTION": "MIN_MAX"
83
+ },
84
+ "drop_n_last_frames": 31,
85
+ "vision_backbone": "resnet18",
86
+ "resize_shape": [
87
+ 360,
88
+ 640
89
+ ],
90
+ "crop_ratio": 1.0,
91
+ "crop_shape": null,
92
+ "crop_is_random": true,
93
+ "pretrained_backbone_weights": "ResNet18_Weights.IMAGENET1K_V1",
94
+ "use_group_norm": false,
95
+ "spatial_softmax_num_keypoints": 32,
96
+ "use_separate_rgb_encoder_per_camera": true,
97
+ "down_dims": [
98
+ 512,
99
+ 1024,
100
+ 2048
101
+ ],
102
+ "kernel_size": 5,
103
+ "n_groups": 8,
104
+ "diffusion_step_embed_dim": 128,
105
+ "use_film_scale_modulation": true,
106
+ "noise_scheduler_type": "DDPM",
107
+ "num_train_timesteps": 100,
108
+ "beta_schedule": "squaredcos_cap_v2",
109
+ "beta_start": 0.0001,
110
+ "beta_end": 0.02,
111
+ "prediction_type": "epsilon",
112
+ "clip_sample": true,
113
+ "clip_sample_range": 1.0,
114
+ "num_inference_steps": null,
115
+ "compile_model": false,
116
+ "compile_mode": "reduce-overhead",
117
+ "do_mask_loss_for_padding": false,
118
+ "optimizer_lr": 0.0001,
119
+ "optimizer_betas": [
120
+ 0.95,
121
+ 0.999
122
+ ],
123
+ "optimizer_eps": 1e-08,
124
+ "optimizer_weight_decay": 1e-06,
125
+ "scheduler_name": "cosine",
126
+ "scheduler_warmup_steps": 500
127
+ },
128
+ "reward_model": null,
129
+ "output_dir": "outputs/train/diffusion_joint_val_diag",
130
+ "job_name": "diffusion_joint_val_diag",
131
+ "resume": true,
132
+ "seed": 1000,
133
+ "cudnn_deterministic": false,
134
+ "num_workers": 4,
135
+ "batch_size": 8,
136
+ "prefetch_factor": 4,
137
+ "persistent_workers": true,
138
+ "steps": 80000,
139
+ "env_eval_freq": 20000,
140
+ "log_freq": 200,
141
+ "eval_steps": 2000,
142
+ "max_eval_samples": 0,
143
+ "tolerance_s": 0.0001,
144
+ "save_checkpoint": true,
145
+ "save_freq": 10000,
146
+ "use_policy_training_preset": true,
147
+ "optimizer": {
148
+ "type": "adam",
149
+ "lr": 0.0001,
150
+ "weight_decay": 1e-06,
151
+ "grad_clip_norm": 10.0,
152
+ "betas": [
153
+ 0.95,
154
+ 0.999
155
+ ],
156
+ "eps": 1e-08
157
+ },
158
+ "scheduler": {
159
+ "type": "diffuser",
160
+ "num_warmup_steps": 500,
161
+ "name": "cosine"
162
+ },
163
+ "eval": {
164
+ "n_episodes": 50,
165
+ "batch_size": 14,
166
+ "use_async_envs": true,
167
+ "recording": false,
168
+ "recording_repo_id": null,
169
+ "recording_private": false
170
+ },
171
+ "wandb": {
172
+ "enable": false,
173
+ "disable_artifact": false,
174
+ "project": "lerobot",
175
+ "entity": null,
176
+ "notes": null,
177
+ "run_id": null,
178
+ "mode": null,
179
+ "add_tags": true
180
+ },
181
+ "peft": null,
182
+ "job": {
183
+ "target": null,
184
+ "image": "huggingface/lerobot-gpu:latest",
185
+ "timeout": "2d",
186
+ "detach": false,
187
+ "tags": []
188
+ },
189
+ "save_checkpoint_to_hub": false,
190
+ "sample_weighting": null,
191
+ "rename_map": {},
192
+ "checkpoint_path": "outputs/train/diffusion_joint_val_diag/checkpoints/last"
193
+ }