Robotics
LeRobot
Safetensors
diffusion-policy
manipulation
ur7e
Bigenlight commited on
Commit
ae647b1
·
verified ·
1 Parent(s): 8f11d7d

EE (10-D) diffusion policy 100k + model card

Browse files
README.md ADDED
@@ -0,0 +1,434 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ library_name: lerobot
4
+ tags:
5
+ - robotics
6
+ - diffusion-policy
7
+ - lerobot
8
+ - manipulation
9
+ - ur7e
10
+ pipeline_tag: robotics
11
+ datasets:
12
+ - Bigenlight/banana_in_pot_lerobot_v3
13
+ ---
14
+
15
+ # Diffusion Policy — Put the right banana in the pot (UR7e, END-EFFECTOR action space)
16
+
17
+ A **Diffusion Policy** (visuomotor DDPM, 1D-conv UNet denoiser) trained by imitation
18
+ learning to perform the manipulation task *"put the right banana in the pot"* on a
19
+ **Universal Robots UR7e** arm with two RGB cameras. Actions are **10-D absolute
20
+ end-effector (EE) targets**: `action = [x, y, z (meters), r1..r6 (6-D rotation), grip]`.
21
+ The observation state is likewise **10-D** in the same EE parameterization.
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
+ (EE-pose action/state variant of the task).
32
+ - **This checkpoint:** step **100,000** — the best of the 80k–100k plateau: 100k has the
33
+ **best gripper accuracy (0.966)** and the **best overall L1**, and its `poseMAE` is tied
34
+ within noise with the 80k minimum (see [Results](#results--the-headline-finding)).
35
+ - **Framework:** [LeRobot](https://github.com/huggingface/lerobot) v0.6.1.
36
+
37
+ > **Headline finding (read this first):** on a held-out split the diffusion **denoising
38
+ > `eval_loss` ROSE ~4.5× (0.0250 @8k → 0.112 @100k)**, and LeRobot's auto-report flags this
39
+ > as "overfit from ~8k". That verdict is **misleading**. The deployment-relevant **open-loop
40
+ > rollout MAE kept IMPROVING then plateaued** (poseMAE 0.0665 → ~0.0368; gripAcc → 0.966) —
41
+ > **no destructive overfit through 100k.** **For a Diffusion Policy the held-out denoising
42
+ > loss is a misleading overfit/early-stop signal — select checkpoints by open-loop MAE, not
43
+ > by `eval_loss`.** (Exactly the lesson from the [JOINT sibling](https://huggingface.co/Bigenlight/diffusion_banana_in_pot_joint).)
44
+
45
+ ---
46
+
47
+ ## Task & data
48
+
49
+ **"put the right banana in the pot."** The tabletop holds several distractor objects —
50
+ **two bananas, an apple, carrots/peppers, and a slice of watermelon** — plus a **silver
51
+ pot**. The operator must grasp the **RIGHT banana** (the target) and place it inside the
52
+ pot. Success = the right banana ends up inside the pot. Every demonstration is a success.
53
+
54
+ - **Dataset:** [`Bigenlight/banana_in_pot_lerobot_v3`](https://huggingface.co/datasets/Bigenlight/banana_in_pot_lerobot_v3)
55
+ (LeRobot v3.0 format).
56
+ - **Scale:** **51 episodes / 21,524 frames / 30 fps / ~12 min.**
57
+ - **Action / state space:** 10-D absolute end-effector pose,
58
+ `[x, y, z (meters), r1..r6 (6-D rotation, unitless), grip]`. The gripper channel is
59
+ effectively binary (open/close). This is the **EE** variant; the sibling model uses a
60
+ 7-D joint action instead.
61
+ - **Cameras:** two RGB viewpoints (Intel RealSense D435 + D435if), captured at 1280×720
62
+ (720p) @ 30 fps, **RGB only** (no depth / IR). `cam1 ↔ cam2` order is fixed and must be
63
+ preserved at deploy time.
64
+
65
+ ### Train / held-out split
66
+
67
+ Training holds out the **LAST 6 episodes (indices 45–50)** as a true validation split and
68
+ trains on the other **45** episodes (0–44). The held-out episodes 45–50 are used both for
69
+ the in-training denoising `eval_loss` probe and for all offline open-loop evaluation below.
70
+
71
+ ---
72
+
73
+ ## Model architecture
74
+
75
+ LeRobot `DiffusionPolicy`. All values below are quoted directly from the checkpoint's
76
+ `config.json`. The architecture is **identical to the JOINT sibling** except that the first
77
+ (state) and last (action) layers are 10-D instead of 7-D.
78
+
79
+ **Observation encoder (vision):**
80
+
81
+ | Item | Value |
82
+ |---|---|
83
+ | Vision backbone | `resnet18` |
84
+ | Pretrained weights | `ResNet18_Weights.IMAGENET1K_V1` (ImageNet) |
85
+ | Per-camera encoder | `use_separate_rgb_encoder_per_camera = true` (separate ResNet18 per view) |
86
+ | Pooling | **SpatialSoftmax**, `spatial_softmax_num_keypoints = 32` |
87
+ | Group norm in encoder | `use_group_norm = false` (keeps BatchNorm from the pretrained backbone) |
88
+ | Cameras | 2 × RGB (`observation.images.cam1`, `observation.images.cam2`) |
89
+ | Network input resolution | **360 × 640** (`resize_shape = [360, 640]`; see [why](#why-the-two-non-default-flags-are-required)) |
90
+ | Crop | **OFF** — `crop_shape = null`, `crop_ratio = 1.0` (`crop_is_random` is moot with no crop) |
91
+ | State input | `observation.state`, shape `(10,)` |
92
+
93
+ > Note: `config.json` records the raw dataset image feature shape as `[3, 720, 1280]`, but
94
+ > the on-the-fly `Resize` to `[360, 640]` (`resize_shape`) means the network actually sees
95
+ > **360 × 640** frames. See the training section for why this must match at inference.
96
+
97
+ **Denoiser (conditional 1D-conv UNet):**
98
+
99
+ | Item | Value |
100
+ |---|---|
101
+ | Denoiser | 1D convolutional UNet (Diffusion Policy / Janner-style) |
102
+ | `down_dims` | `[512, 1024, 2048]` |
103
+ | `kernel_size` | `5` |
104
+ | `n_groups` (GroupNorm) | `8` |
105
+ | `diffusion_step_embed_dim` | `128` |
106
+ | FiLM conditioning | `use_film_scale_modulation = true` |
107
+ | `horizon` | `64` (prediction horizon, in frames) |
108
+ | `n_obs_steps` | `2` (observation context length) |
109
+ | `n_action_steps` | `32` (actions executed before replanning) |
110
+
111
+ > **Conditioning dimension (EE-specific):** each observation contributes
112
+ > `state 10 + vision 128 = 138-D` (per-camera ResNet18 → 64-D, two cameras → 128-D); with
113
+ > `n_obs_steps = 2` the UNet `global_cond` is **276-D** (the JOINT model's is 270-D). This
114
+ > 6-D difference is the only structural change from the JOINT config.
115
+
116
+ **Diffusion process (noise scheduler):**
117
+
118
+ | Item | Value |
119
+ |---|---|
120
+ | `noise_scheduler_type` | `DDPM` |
121
+ | `num_train_timesteps` | `100` |
122
+ | `beta_schedule` | `squaredcos_cap_v2` (cosine) |
123
+ | `beta_start` / `beta_end` | `0.0001` / `0.02` |
124
+ | `prediction_type` | `epsilon` (predict noise) |
125
+ | `clip_sample` | `true`, `clip_sample_range = 1.0` |
126
+ | `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)) |
127
+
128
+ **Normalization (`normalization_mapping`):**
129
+
130
+ | Feature group | Mode |
131
+ |---|---|
132
+ | `VISUAL` (images) | `MEAN_STD` (ImageNet stats, `use_imagenet_stats = true`) |
133
+ | `STATE` (observation.state) | `MIN_MAX` |
134
+ | `ACTION` (action) | `MIN_MAX` |
135
+
136
+ Normalizer statistics are baked into the pre/post-processor pipelines saved alongside the
137
+ checkpoint (`policy_preprocessor.json` / `policy_postprocessor.json`), not into
138
+ `forward()`.
139
+
140
+ **I/O summary:**
141
+
142
+ | I/O | Spec |
143
+ |---|---|
144
+ | `observation.state` | `(10,)` — `[x, y, z (m), r1..r6 (6-D rotation), grip]` |
145
+ | `observation.images.cam1` / `cam2` | RGB, network input **360 × 640** |
146
+ | `action` | `(10,)` — `[x, y, z (m), r1..r6 (6-D rotation), grip]`, **absolute** EE pose + ~binary gripper |
147
+
148
+ **Size:** ~**277.9M** parameters, fp32 ~**1.11 GB** — effectively identical to the JOINT
149
+ model (they differ only by the 3-D width of the first and last layers).
150
+
151
+ ---
152
+
153
+ ## Training setup
154
+
155
+ Trained with `lerobot-train` (LeRobot 0.6.1). Values below are from the saved training
156
+ config; hyperparameters match the JOINT run.
157
+
158
+ | Item | Value |
159
+ |---|---|
160
+ | Policy | `diffusion` (`--policy.type=diffusion`) |
161
+ | Dataset | `banana_in_pot_lerobot_v3` (EE action/state), holds out eps 45–50 |
162
+ | Batch size | **8** |
163
+ | Steps | **100,000**; checkpoints saved every 10,000 |
164
+ | Optimizer | **Adam**, `lr = 1e-4`, `betas = [0.95, 0.999]`, `eps = 1e-8`, `weight_decay = 1e-6` |
165
+ | LR scheduler | **cosine** (`scheduler_name = cosine`), `num_warmup_steps = 500` |
166
+ | Precision | **fp32** (`use_amp = false`) |
167
+ | EMA | **none** (no EMA weights in this config) |
168
+ | Image transform | on-the-fly `Resize` to `[360, 640]`, deterministic |
169
+ | `drop_n_last_frames` | **31** (non-default; see below) |
170
+ | `resize_shape` | `[360, 640]` (non-default; see below) |
171
+ | Control rate | dataset 30 fps, consecutive frames (step = 1) — no downsampling. obs 2 frames (~0.067 s) / horizon 64 (2.13 s) / exec 32 (1.07 s) @ 30 Hz |
172
+ | Eval probe | held-out denoising `eval_loss`; open-loop rollouts run offline per checkpoint |
173
+
174
+ ### Why the two non-default flags are required
175
+
176
+ Both `resize_shape=[360,640]` and `drop_n_last_frames=31` are **not** the LeRobot defaults;
177
+ they are mandatory for this dataset/config and encode real operational knowledge:
178
+
179
+ 1. **`resize_shape=[360,640]` — SpatialSoftmax is shape-rigid.** The Diffusion Policy RGB
180
+ encoder ends in a `SpatialSoftmax` layer whose keypoint geometry is fixed to the spatial
181
+ dimensions of the feature map at build time. The network must therefore be *constructed*
182
+ for the exact input resolution it will ever see. Setting `resize_shape=[360,640]` builds
183
+ the encoder for 360×640 and — combined with crop being **off** (`crop_shape=null`) —
184
+ guarantees the training image path, the offline-eval image path, and any deploy image
185
+ path all feed the encoder identically. A mismatched resolution (or leaving crop on)
186
+ changes the SpatialSoftmax grid and breaks the model.
187
+
188
+ 2. **`drop_n_last_frames=31` — horizon / n_action off-by-one at episode ends.** The
189
+ trajectory sampler must not draw a window that runs past the end of an episode. With
190
+ `horizon=64`, `n_obs_steps=2`, and `n_action_steps=32`, the correct number of trailing
191
+ frames to drop is `horizon - n_action_steps - (n_obs_steps - 1) = 64 - 32 - 1 = 31`.
192
+ Using the default (7, tuned for the reference `horizon=16` config) would let the sampler
193
+ pull frames off the end of an episode and corrupt the action targets. **If you change
194
+ `horizon`/`n_obs_steps`/`n_action_steps`, recompute `drop_n_last_frames`.**
195
+
196
+ ---
197
+
198
+ ## Results & the headline finding
199
+
200
+ ![Diffusion EE: held-out denoising eval_loss (rising, misleading) vs open-loop rollout MAE (improving then plateauing through 100k)](assets/diffusion_ee_overfit_diag.png)
201
+
202
+ Offline **open-loop** evaluation on the held-out episodes **45–50** with `eval_offline.py`
203
+ (each logged observation is fed to `select_action`; the predicted action is compared to the
204
+ dataset ground truth). Sampling used **DDIM with 10 inference steps**
205
+ (`--scheduler DDIM --num-inference-steps 10`) for ~10× faster rollouts; DDIM is a valid
206
+ sampler for a DDPM-trained ε model (same beta schedule). `poseMAE` is the mean absolute
207
+ error over the first 9 pose dims; `gripAcc` is the binary gripper-open/close accuracy
208
+ (threshold 0.5); `overall L1` averages all 10 dims. Numbers persisted at
209
+ [`assets/diffusion_ee_openloop_eval.csv`](assets/diffusion_ee_openloop_eval.csv).
210
+
211
+ | checkpoint | poseMAE (own scale ↓) | gripAcc ↑ | overall L1 ↓ |
212
+ |---|---|---|---|
213
+ | 10k | 0.06648 | 0.886 | 0.07517 |
214
+ | 20k | 0.04499 | 0.923 | 0.05031 |
215
+ | 30k | 0.04345 | 0.930 | 0.04839 |
216
+ | 40k | 0.03999 | 0.926 | 0.04440 |
217
+ | 50k | 0.03786 | 0.942 | 0.04126 |
218
+ | 60k | 0.03900 | 0.950 | 0.04131 |
219
+ | 70k | 0.03687 | 0.956 | 0.03846 |
220
+ | 80k | **0.03674** ⟵ poseMAE min | 0.961 | 0.03773 |
221
+ | 90k | 0.03718 | 0.955 | 0.03856 |
222
+ | **100k** ⭐ | 0.03717 | **0.966** ⟵ max | **0.03754** ⟵ min |
223
+
224
+ > **Units caveat — do NOT compare `poseMAE` across the two models.** The EE `poseMAE`
225
+ > mixes **meters** (x, y, z) with **6-D rotation** (unitless) channels, so it is on a
226
+ > completely different scale from the JOINT model's **radian** `poseMAE` (0.0845). The two
227
+ > numbers are not comparable — judge the EE model **only** by its own trend.
228
+
229
+ **Best checkpoint = the 80k–100k plateau.** `poseMAE` falls 0.0665 → ~0.0368 and then
230
+ flattens from ~70k (70k/80k/90k/100k all within ±0.0004 = eval noise); its strict minimum is
231
+ **0.03674 @ 80k**. `gripAcc` rises essentially monotonically to **0.966 @ 100k**;
232
+ `overall L1` is lowest at **100k (0.03754)**. There is **no open-loop overfitting through
233
+ 100k**. We upload **100k** because it gives the best gripper and best overall L1 while its
234
+ `poseMAE` is tied within noise with the 80k minimum — deploy 100k is safe and strictly
235
+ best-gripper. (Pick 80k if you want the strict `poseMAE` minimum; both sit on the plateau.)
236
+
237
+ ### The misleading `eval_loss` (the lesson)
238
+
239
+ During training the held-out **denoising `eval_loss`** (LeRobot's in-training validation
240
+ probe, computed under `policy.eval()` on eps 45–50) did the *opposite* of the rollout
241
+ metric:
242
+
243
+ | step | held-out eval_loss |
244
+ |---|---|
245
+ | 2k | 0.0331 |
246
+ | **8k** | **0.0250** (min) |
247
+ | 100k | 0.112 (~4.5× above the min) |
248
+
249
+ Read naively, the held-out `eval_loss` bottoms at **step 8k** and then rises ~4.5×, and
250
+ LeRobot's auto-report calls it **"overfit from ~8k"**. **That verdict is wrong for
251
+ deployment:** the same held-out episodes, evaluated by open-loop rollout, get
252
+ *monotonically better* out to the 70k–100k plateau.
253
+
254
+ **Why the two signals disagree:** a diffusion policy is trained to predict the noise added
255
+ at a **random** timestep, and `eval_loss` scores exactly that random-timestep
256
+ noise-prediction on held-out frames — so it is (a) high-variance/stochastic by construction
257
+ and (b) only loosely coupled to closed-loop action quality. But what drives the robot is the
258
+ **sampled action** — the *integral* of the full reverse-diffusion trajectory (here DDIM-10).
259
+ Those two quantities decorrelate: the network can get "worse" at random-timestep denoising
260
+ MSE while the *sampled* action trajectory keeps getting **closer** to ground truth. **The
261
+ only faithful held-out metric is to actually sample actions and compare them open-loop**,
262
+ which is what `eval_offline.py` does. **Takeaway: for a Diffusion Policy, select checkpoints
263
+ and early-stop by open-loop rollout MAE, not by held-out denoising `eval_loss`.** (The ACT
264
+ sibling did not show this divergence — there the two signals agreed — so this is a
265
+ diffusion-specific pitfall, reconfirmed here on the EE action space.)
266
+
267
+ ---
268
+
269
+ ## Deployment note — research artifact, NOT wired to the robot
270
+
271
+ **Read this before trying to run the model on hardware.** Unlike the JOINT sibling — whose
272
+ 7-D joint actions are **directly actuatable** and which is deployed on the real UR7e via the
273
+ [`Bigenlight/gello_software`](https://github.com/Bigenlight/gello_software) ROS 2 stack —
274
+ this EE model outputs **10-D end-effector poses** (`[x, y, z, r1..r6, grip]`). Those poses
275
+ must be converted to joint commands by **inverse kinematics (IK)** before they can drive the
276
+ arm.
277
+
278
+ **There is no IK deploy path in the current stack.** Accordingly this checkpoint is uploaded
279
+ as a **research artifact / reference for the EE action space**, **NOT** wired for
280
+ real-robot inference. It was **never run on the arm** and there are **no closed-loop
281
+ task-success results** for it.
282
+
283
+ For the actuatable path, use:
284
+
285
+ - the **JOINT model** [`Bigenlight/diffusion_banana_in_pot_joint`](https://huggingface.co/Bigenlight/diffusion_banana_in_pot_joint)
286
+ (7-D joint actions, directly commandable), and
287
+ - the **diffusion deploy node** in
288
+ [`Bigenlight/gello_software`](https://github.com/Bigenlight/gello_software) (ROS 2 Humble,
289
+ UR7e follower, dual RealSense) used to run the JOINT policy.
290
+
291
+ To deploy *this* EE model you would additionally have to add an IK stage (EE pose → joint
292
+ targets, with reachability/limit handling) after `select_action` — that work is out of scope
293
+ for this release.
294
+
295
+ ---
296
+
297
+ ## Usage / inference
298
+
299
+ The snippet below loads the policy and produces a 10-D EE action. **Note:** that action is
300
+ an end-effector pose — it needs **IK** before it can drive a robot (see the deployment note
301
+ above).
302
+
303
+ ### Load the policy (LeRobot 0.6.1)
304
+
305
+ Normalization is **not** baked into `forward()` in LeRobot 0.6.1 — it lives in the
306
+ pre/post-processor pipelines saved with the checkpoint. `select_action` returns a
307
+ **normalized** action; the post-processor converts it back to the EE units.
308
+
309
+ ```python
310
+ import torch
311
+ from lerobot.configs import PreTrainedConfig
312
+ from lerobot.policies.factory import get_policy_class, make_pre_post_processors
313
+
314
+ CKPT = "Bigenlight/diffusion_banana_in_pot_ee"
315
+ device = "cuda"
316
+
317
+ # (optional) speed up sampling: DDIM with 10 steps instead of the full DDPM schedule.
318
+ # Mutate the config BEFORE from_pretrained so the sampler is built with these settings.
319
+ cfg = PreTrainedConfig.from_pretrained(CKPT)
320
+ cfg.pretrained_path = CKPT
321
+ cfg.device = device
322
+ cfg.noise_scheduler_type = "DDIM" # valid sampler for a DDPM-trained epsilon model
323
+ cfg.num_inference_steps = 10 # ~10x faster rollouts
324
+
325
+ policy = get_policy_class(cfg.type).from_pretrained(CKPT, config=cfg) # -> DiffusionPolicy
326
+ policy.to(device)
327
+ policy.eval()
328
+
329
+ preprocessor, postprocessor = make_pre_post_processors(
330
+ policy_cfg=cfg,
331
+ pretrained_path=CKPT,
332
+ preprocessor_overrides={"device_processor": {"device": device}},
333
+ )
334
+ ```
335
+
336
+ ### Produce actions
337
+
338
+ Build the observation dict exactly as training did: EE state `(10,)` plus **both** cameras
339
+ as RGB CHW tensors in `[0, 1]`, **resized to 360×640** (aspect-preserving half-resolution).
340
+ `cam1`/`cam2` must map to the same physical viewpoints as at collection.
341
+
342
+ ```python
343
+ policy.reset() # once at the start of each episode/rollout
344
+ preprocessor.reset()
345
+ postprocessor.reset()
346
+
347
+ # obs = {
348
+ # "observation.state": state_10, # (10,) float32: [x,y,z, r1..r6, grip]
349
+ # "observation.images.cam1": img1_chw, # (3, 360, 640) float32 in [0,1]
350
+ # "observation.images.cam2": img2_chw, # (3, 360, 640) float32 in [0,1]
351
+ # "task": "put the right banana in the pot",
352
+ # }
353
+
354
+ with torch.inference_mode():
355
+ proc = preprocessor(obs) # rename -> add batch dim -> device -> normalize
356
+ action = policy.select_action(proc) # (1, 10) NORMALIZED
357
+ action = postprocessor(action) # (1, 10) EE units, on cpu
358
+ ee_target = action.squeeze(0).numpy() # (10,) -> [x, y, z, r1..r6, grip]
359
+ # NOTE: ee_target is an END-EFFECTOR POSE. To drive a robot you must first run IK
360
+ # (ee_target[:9] -> joint targets); no IK deploy path ships with this model.
361
+ ```
362
+
363
+ `select_action` returns **one** action per call from an internal queue. Because
364
+ `n_action_steps = 32`, the policy denoises a fresh action sequence, executes 32 actions
365
+ from it, then replans (with `n_obs_steps = 2` frames of observation context). Call
366
+ `policy.reset()` at the start of every episode to clear that queue. The gripper channel
367
+ `grip` is ~binary — threshold at `> 0.5 → close`.
368
+
369
+ ### Reproduce the offline evaluation
370
+
371
+ The repo's `eval_offline.py` runs the exact open-loop protocol used for the results table
372
+ (same 360×640 `Resize`, same normalization via the saved processors):
373
+
374
+ ```bash
375
+ python eval_offline.py \
376
+ --checkpoint outputs/train/diffusion_ee_val_diag/checkpoints/100000/pretrained_model \
377
+ --episodes 45,46,47,48,49,50 \
378
+ --device cuda \
379
+ --scheduler DDIM --num-inference-steps 10 \
380
+ --out eval_out_diffusion_ee_100k
381
+ ```
382
+
383
+ `--scheduler DDIM --num-inference-steps 10` gives the ~10× rollout speedup; omit them to
384
+ sample with the full trained DDPM schedule (`num_train_timesteps = 100`).
385
+
386
+ ---
387
+
388
+ ## Limitations & intended use
389
+
390
+ - **Research artifact only — not deployable as-is.** The 10-D EE action requires IK to
391
+ actuate and there is **no IK deploy path** in the stack, so this model was **never run on
392
+ hardware** and has **no closed-loop success rate**. For a directly actuatable, deployed
393
+ policy use the [JOINT sibling](https://huggingface.co/Bigenlight/diffusion_banana_in_pot_joint).
394
+ - **Small, single-task lab dataset:** 51 demonstrations, one scene layout, one operator.
395
+ Expect limited generalization to novel object arrangements, lighting, or camera placement.
396
+ - **Success-only demonstrations:** no failure/recovery data; not suited as-is for methods
397
+ that need negative examples.
398
+ - **Offline metrics only:** the best checkpoint (100k) reaches **held-out poseMAE ≈ 0.0372**
399
+ (EE's own mixed-unit scale) and gripper accuracy ≈ 0.966 in open-loop rollout. These are
400
+ *not* closed-loop task success rates, and the EE `poseMAE` is **not comparable** to the
401
+ JOINT model's radian `poseMAE`.
402
+ - **Absolute EE action space:** the policy was only ever conditioned on states near the
403
+ data-collection start pose; any real deployment would additionally need reachability /
404
+ joint-limit handling in the IK stage plus the safety guards used for the JOINT model.
405
+ - **Not for production.** Intended for research in imitation learning / diffusion policies
406
+ for robot manipulation. Workspace-, robot-, and camera-specific.
407
+ - The ResNet18 encoders are **ImageNet-pretrained** (not robotics-pretrained); the UNet
408
+ denoiser is trained from scratch on this task.
409
+
410
+ ---
411
+
412
+ ## Links
413
+
414
+ - **Dataset:** [`Bigenlight/banana_in_pot_lerobot_v3`](https://huggingface.co/datasets/Bigenlight/banana_in_pot_lerobot_v3)
415
+ - **JOINT sibling model (deployed, actuatable):** [`Bigenlight/diffusion_banana_in_pot_joint`](https://huggingface.co/Bigenlight/diffusion_banana_in_pot_joint)
416
+ - **Experiments repo:** [github.com/Bigenlight/banana-in-pot-experiments](https://github.com/Bigenlight/banana-in-pot-experiments)
417
+ - **Deployment stack (ROS 2 Humble, JOINT path):** [github.com/Bigenlight/gello_software](https://github.com/Bigenlight/gello_software)
418
+ - **Framework:** [LeRobot](https://github.com/huggingface/lerobot) v0.6.1
419
+
420
+ ## Citation
421
+
422
+ ```bibtex
423
+ @misc{theo2026bananainpotdiffusionee,
424
+ title = {Diffusion Policy for "put the right banana in the pot"
425
+ (UR7e, end-effector action space)},
426
+ author = {Theo and {Bigenlight}},
427
+ year = {2026},
428
+ howpublished = {\url{https://huggingface.co/Bigenlight/diffusion_banana_in_pot_ee}},
429
+ note = {LeRobot 0.6.1 DiffusionPolicy, 10-D EE action, trained on
430
+ banana_in_pot_lerobot_v3; research artifact (needs IK to actuate)}
431
+ }
432
+ ```
433
+
434
+ License: **Apache-2.0**.
assets/diffusion_ee_openloop_eval.csv ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ checkpoint,poseMAE,poseRMSE,gripMAE,gripAcc,overallL1
2
+ 010000,0.06648,0.09027,0.15344,0.886,0.07517
3
+ 020000,0.04499,0.06286,0.09818,0.923,0.05031
4
+ 030000,0.04345,0.06098,0.09293,0.930,0.04839
5
+ 040000,0.03999,0.05790,0.08407,0.926,0.04440
6
+ 050000,0.03786,0.05553,0.07187,0.942,0.04126
7
+ 060000,0.03900,0.05898,0.06212,0.950,0.04131
8
+ 070000,0.03687,0.05557,0.05277,0.956,0.03846
9
+ 080000,0.03674,0.05562,0.04661,0.961,0.03773
10
+ 090000,0.03718,0.05588,0.05097,0.955,0.03856
11
+ 100000,0.03717,0.05594,0.04091,0.966,0.03754
assets/diffusion_ee_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
+ 10
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
+ 10
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": null,
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:5af316015d01d153d44adc39be42a5af810e9952d70bb7aae38314eb8686c3ad
3
+ size 1112382512
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
+ 10
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:4ed85be0c5507baa295f108acc29cf024b0cd82bc3eae6fca036e3fc53175b17
3
+ size 7888
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
+ 10
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
+ 10
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:949a6fcbc332b8a330e8471ef5b9e3b68e5ee08c0d5915274577d890ae27ef20
3
+ size 7896
train_config.json ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "dataset": {
3
+ "repo_id": "theo/banana_in_pot_ee_action",
4
+ "root": "./banana_in_pot_ee_action_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
+ 10
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
+ 10
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": null,
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_ee_val_diag",
130
+ "job_name": "diffusion_ee_val_diag",
131
+ "resume": false,
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": 100000,
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": null
193
+ }