SmolVLA + LIBERO + EEG — an 8-variant ablation on injecting a brain signal into a VLA
Eleven checkpoints and three EEG encoders from a study of whether an EEG motor-imagery signal can be made to steer a vision-language-action policy, and where in the architecture it has to enter for that to happen.
Every model here was trained on a single M5 MacBook (Apple MPS, no NVIDIA GPU).
Code: https://github.com/twu3202/SmolVLA_cl
Companion release: Twu31/smolvla-cross-embodiment-mps ·
Twu31/rt1-lerobot-mps
The question, and how it was narrowed down
The project ran in eight stages, each one an answer to what the previous stage had ruled out.
| Stage | Question | Answer | Artifact |
|---|---|---|---|
| 1 | Does a full VLA training pipeline run on Apple MPS at all? | Yes — MAE 0.931 → 0.246, gripper 60% → 84% | stage1_* |
| 2 | Can EEG be attached as a fourth modality? | Pipeline works; with randomly-paired EEG the model learns nothing from it | stage2_* |
| 3 | Force a deterministic EEG↔action pairing and 0% dropout — now does EEG steer the policy? | 1/4 directions. Only the gripper, the most binary dimension | v1 |
| 4 | Is the problem the EEG data or the architecture? | The data: EEG embeddings are 81.6% linearly separable. The architecture: additive injection cannot deliver an orthogonal signal | (analysis only) |
| 5 | Move the injection from the state vector into the VLM as a token | 1/4 → 2/4 → 3/4 as the encoder and sampling improve | v2, v3, v4 |
| 6 | Does a stronger EEG encoder (ATCNet) push it to 4/4? | No — 3/4 → 2/4. An auxiliary loss makes it 1/4 | v5, v6 |
| 7 | Does training longer finish the job? | No — left_fist finally passes, two others stop passing |
v7 |
| 8 | Is ATCNet's deficit just "the expert was tuned to EEGNet"? | No — retraining the expert from scratch reproduces v6 exactly | v8 |
The short version: the injection site was a real bottleneck and fixing it helped; the remaining ceiling is not. Nothing tried reaches 4/4, and the four directions trade off against each other rather than improving together.
Read this before you use the weights
The VLM backbone in v1–v7 is randomly initialised, not pretrained.
The EEG experiments branch off the Stage-1 run libero_spatial, which was trained with
LOAD_VLM=0. Verified by tensor-wise comparison against lerobot/smolvla_base:
| checkpoint | vision tensors matching a pretrained SmolVLM2 | text tensors matching |
|---|---|---|
stage1_libero_spatial_random_vlm |
0 / 197 | 0 / 146 |
stage1_libero_spatial_pretrained_vlm |
197 / 197 | 146 / 146 |
v4_..._balanced_v2 (best controllability) |
0 / 197 | 0 / 146 |
v8_..._atcnet_scratch |
197 / 197 | 146 / 146 |
So the token-level injection results (v2–v7) describe EEG tokens entering a randomly
initialised SmolVLM2 transformer that is then held frozen. Only
stage1_libero_spatial_pretrained_vlm and v8 carry pretrained
HuggingFaceTB/SmolVLM2-500M-Video-Instruct weights. Treat the controllability numbers as
statements about the injection site and the encoder, not about pretrained multimodal
grounding.
These are research artifacts from an architecture demo, not a working BCI. The headline result is a negative one — see Results.
Contents
dataset_stats.pt # LIBERO spatial state/action mean+std — required for inference
encoders/
eegnet_10subj_40.7/encoder_only.pt # EEGNet, 10 subjects, 40.7% val acc (used by v1–v3)
eegnet_109subj_51.6/encoder_only.pt # EEGNet, 109 subjects, 51.6% val acc (used by v4, v7)
eegnet_109subj_51.6/eeg_encoder.pt # ...same run, with the 4-class MLP head attached
atcnet_109subj_57.7/encoder_only.pt # ATCNet, 109 subjects, 57.7% val acc (used by v5, v6, v8)
stage1_libero_spatial_random_vlm/ # SmolVLA on LIBERO, random VLM (2000 steps)
stage1_libero_spatial_pretrained_vlm/ # SmolVLA on LIBERO, pretrained VLM (2000 steps)
stage2_libero_spatial_eeg_additive/ # + EEG, randomly paired, 50% dropout (1000 steps)
v1_libero_spatial_eeg_synth/ # additive injection, deterministic pairing
v2_libero_spatial_eeg_token/ # token-level injection
v3_libero_spatial_eeg_token_balanced/ # + class-balanced sampling
v4_libero_spatial_eeg_token_balanced_v2/ # + better encoder ← best result
v5_libero_spatial_eeg_token_final/ # ATCNet + auxiliary classification loss
v6_libero_spatial_eeg_token_atcnet_only/ # ATCNet, no aux loss
v7_libero_spatial_eeg_token_eegnet_5000/ # EEGNet, 5000 steps
v8_libero_spatial_eeg_token_atcnet_scratch/ # ATCNet, expert trained from scratch
eval_output/ # every figure and .npz behind the tables below
Each variant directory holds one step_*.pt plus the train_log.jsonl for that run.
optimizer_state has been removed from every checkpoint — inference and evaluation work
normally, resuming someone else's optimizer trajectory does not.
Results
Controllability: does switching the EEG class move the action in the expected direction?
The test fixes camera, robot state and language input, swaps only the EEG epoch, and checks
whether the output shifts the way the training pairing prescribes (left_fist → Δx < 0,
right_fist → Δx > 0, both_fists → gripper closes, both_feet → Δy > 0). A direction
counts as passed when the shift has the right sign and the bootstrap confidence interval
for that condition excludes the no-EEG baseline mean. Each condition is measured over 5 demos
× 10 tasks × 30 steps = 1,500 predictions.
| Variant | Injection site | EEG encoder | Steps | Balanced | Aux loss | Pass | Directions passed |
|---|---|---|---|---|---|---|---|
| v1 | additive (state vector) | EEGNet 40.7% | 1000 | ✗ | ✗ | 1/4 | both_fists |
| v2 | token (VLM position 0) | EEGNet 40.7% | 1000 | ✗ | ✗ | 2/4 | right_fist, both_feet |
| v3 | token | EEGNet 40.7% | 2000 | ✓ | ✗ | 2/4 | right_fist, both_feet |
| v4 | token | EEGNet 51.6% | 3000 | ✓ | ✗ | 3/4 ★ | right_fist, both_fists, both_feet |
| v5 | token | ATCNet 57.7% | 3000 | ✓ | 0.2 | 1/4 | both_feet |
| v6 | token | ATCNet 57.7% | 3000 | ✓ | ✗ | 2/4 | right_fist, both_feet |
| v7 | token | EEGNet 51.6% | 5000 | ✓ | ✗ | 2/4 | left_fist, right_fist |
| v8 | token | ATCNet 57.7% (expert from scratch) | 5000 | ✓ | ✗ | 2/4 | right_fist, both_feet |
No configuration passes all four directions. v4 is the best at 3/4; v7 is the only run in
which left_fist ever passes, and it loses two other directions to get there.
Four findings, three of them counter-intuitive
| Hypothesis | Outcome |
|---|---|
| Token-level injection beats additive injection | ✅ v1→v2: 1/4 → 2/4 |
| Class-balanced sampling helps | ✅ needed along the v2→v4 path (17/17/46/20% → 25/25/25/25%) |
| Training longer keeps improving things | ⚠️ v4→v7 unlocks left_fist but drops both_fists and both_feet |
| A stronger EEG encoder gives better control | ❌ ATCNet (57.7%) loses to EEGNet (51.6%) in every paired comparison |
| An auxiliary classification loss stabilises the EEG token | ❌ v5 (1/4) is worse than v6 (2/4) — the aux objective decouples from the action loss |
| "Encoder mismatch" explains ATCNet's deficit | ❌ v8 trains the expert from scratch and reproduces v6's outcome exactly |
Why the stronger encoder loses
5-fold logistic-regression probes on the two encoders' 64-dim embeddings:
| Class | EEGNet 51.6% | ATCNet 57.7% |
|---|---|---|
| left_fist | 68.9% | 64.3% |
| right_fist | 65.1% | 61.8% |
| both_fists | 72.0% | 73.9% |
| both_feet | 69.6% | 70.2% |
| overall (linear) | 67.2% | 65.3% |
| per-class spread | 6.9 pp | 12.1 pp |
ATCNet buys its higher MLP-head accuracy with non-linear fusion that a downstream module
reading it linearly cannot exploit, and it concentrates capacity on two classes. Across
all ATCNet runs (v5, v6, v8), left_fist fails — exactly the class where its embedding is
weakest. Per-class, linearly-readable discriminability matters more than headline
accuracy when the encoder feeds another network.
Representation analysis (Stage 4)
| Metric | Value |
|---|---|
| EEG → MI class, linear probe | 81.7% (chance 25%) |
| CKA(EEG embedding, action) — paired | 0.132 |
| CKA(EEG embedding, action) — shuffled control | 0.005 |
| Per-action-dim CKA | gripper 0.231 · translation ≈0.04 · rotation ≈0.006 |
| EEG → Δxyz regression R² | +0.065 (shuffled control: −0.023) |
The EEG representation is genuinely discriminative and nearly orthogonal to the action space; it carries a discrete intent class, not continuous kinematics. That orthogonality is why a 14-dim additive nudge cannot deliver it and why the injection site mattered so much.
Stage-1 open-loop baselines (no EEG)
| Model | MAE ↓ | L2 ↓ | Gripper accuracy ↑ |
|---|---|---|---|
| Random weights | 0.931 | 2.989 | 59.7% |
stage1_libero_spatial_random_vlm |
0.246 | 1.004 | 84.3% |
stage1_libero_spatial_pretrained_vlm |
0.255 | 0.997 | 92.0% |
A pretrained VLM buys +7.7 pp of gripper accuracy and almost nothing on translation — it helps the semantic when to grasp decision, not fine kinematics.
Architecture
SmolVLATokenLevelEEG (v2–v8):
EEG epoch (64ch × 320 samples @160Hz, 2 s)
→ frozen EEGNet / ATCNet → 64-dim embedding
→ eeg_proj: Linear(64,256) · GELU · Linear(256,960) · LayerNorm(960)
→ forward hook on vlm.get_input_embeddings() replaces the position-0 embedding
→ SmolVLM2 transformer (frozen; gradients pass through it)
→ flow-matching action expert → 50 × 7 action chunk
The additive variants (stage2, v1) instead use eeg_proj: Linear(64, 14) added onto the
normalised robot state — the shallow baseline the study set out to beat.
| Component | Params |
|---|---|
| SmolVLA policy total | 450.0 M |
| ├ VLM text tower (frozen) | 204.6 M |
| ├ VLM vision tower (frozen) | 86.4 M |
| ├ VLM connector (frozen) | 11.8 M |
| ├ I/O embeddings & heads | 48.9 M |
| └ flow-matching action expert (trained) | 98.2 M |
eeg_proj (trained) |
0.27 M |
| EEGNet encoder (frozen) | 0.59 M |
| ATCNet encoder (frozen) | 0.07 M |
train_expert_only=True, freeze_vision_encoder=True — the whole SmolVLM2 backbone stays
frozen, which the weight diff above confirms independently.
Input / output
INPUT
observation.images.image (3, 256, 256) ← LIBERO agentview camera
observation.images.image2 (3, 256, 256) ← wrist camera
both padded-resized to 512×512 internally
observation.state (14,) ← eef_pos(3) + eef_quat(4) + joint_pos(7), z-scored
language tokens (48,) ← LIBERO task string
EEG epoch (64, 320) ← 2 s @ 160 Hz, 4–40 Hz bandpass [EEG models]
OUTPUT
action chunk (50, 7) ← Δxyz(3) + Δrpy(3) + gripper(1), z-scored
Training: 1000–5000 steps, batch 4 × grad-accum 4 (effective 16), LR 5e-5 cosine, grad-norm clip 10, ~0.93 step/s on M5 MPS — the token-level hook costs nothing measurable over additive injection. Runs took 18 min (1000 steps) to 92 min (5000 steps).
Usage
These are plain torch.save dicts, not lerobot-loadable model folders. You need the
config and wrapper classes from the GitHub repo.
import torch
from huggingface_hub import hf_hub_download
REPO = "Twu31/smolvla-libero-eeg"
path = hf_hub_download(REPO, "v4_libero_spatial_eeg_token_balanced_v2/step_003000.pt")
ck = torch.load(path, map_location="cpu", weights_only=False)
ck["step"] # 3000
ck["vlm_hidden_dim"] # 960
ck["policy_state"] # SmolVLAPolicy state_dict, 500 tensors, bfloat16
ck["eeg_proj_state"] # Linear(64,256) / Linear(256,960) / LayerNorm(960)
Full reconstruction:
import torch
from huggingface_hub import hf_hub_download
from libero_smolvla_config import make_libero_smolvla_config # from the GitHub repo
from lerobot.policies.smolvla.modeling_smolvla import SmolVLAPolicy
from eeg_encoder import EEGNet # or eeg_encoder_atcnet.ATCNet
REPO = "Twu31/smolvla-libero-eeg"
device = "mps"
cfg = make_libero_smolvla_config(device)
cfg.load_vlm_weights = False # the checkpoint carries the full VLM state
policy = SmolVLAPolicy(cfg).to(device)
ck = torch.load(hf_hub_download(REPO, "v4_libero_spatial_eeg_token_balanced_v2/step_003000.pt"),
map_location=device, weights_only=False)
policy.load_state_dict(ck["policy_state"])
enc = EEGNet(n_channels=64, n_timepoints=320, n_classes=4, embed_dim=64).to(device)
enc.load_state_dict(torch.load(hf_hub_download(REPO, "encoders/eegnet_109subj_51.6/encoder_only.pt"),
map_location=device, weights_only=False)["backbone_state"],
strict=False)
stats = torch.load(hf_hub_download(REPO, "dataset_stats.pt"),
map_location="cpu", weights_only=False) # normalisation mean/std
Encoder ↔ checkpoint pairing is not optional — loading v4 or v7 with the 40.7% encoder, or v5/v6/v8 with an EEGNet, silently produces meaningless embeddings:
| Checkpoint | Required encoder |
|---|---|
v1, v2, v3 |
encoders/eegnet_10subj_40.7/ |
v4, v7 |
encoders/eegnet_109subj_51.6/ |
v5, v6, v8 |
encoders/atcnet_109subj_57.7/ |
Training data
| Source | Use | License |
|---|---|---|
LIBERO libero_spatial — 10 tasks × 50 demos |
robot demonstrations | MIT |
| PhysioNet EEGMMIDB — 109 subjects, 64 ch, 160 Hz | EEG motor imagery | ODC-BY 1.0 |
HuggingFaceTB/SmolVLM2-500M-Video-Instruct |
VLM backbone (Stage-1 pretrained variant and v8 only) | Apache-2.0 |
EEG preprocessing: 4–40 Hz bandpass, 60 Hz notch, 2 s epochs at +0.5 s from cue onset,
9,377 epochs over 4 classes. The preprocessed tensor is not redistributed here — it is
one command to rebuild (download_eeg_data.py) via mne.datasets.eegbci.
No human subjects were recruited for this work. All EEG comes from the public PhysioNet release, whose own consent and IRB coverage governs it.
The EEG↔action pairing in v1–v8 is synthetic: an epoch of class k is attached to a training frame whose action matches a hand-written rule. It is a controllability probe, not evidence that a person's motor imagery drives this robot.
Limitations
- Random VLM backbone for v1–v7 (see the warning above).
- Open-loop evaluation only. No LIBERO closed-loop success rates are reported here.
- Effect sizes are small: |Δ| ≈ 0.01–0.10 in normalised action units. The pass criterion establishes that a shift is reliable, not that it is large enough to control anything.
- Single seed per variant. The v4/v7 trade-off (which directions pass) is exactly the kind of effect that could move under reseeding, and was not tested.
- One suite (
libero_spatial), one embodiment, ~500 PhysioNet epochs per run.
Related releases
| Repo | What it holds |
|---|---|
Twu31/smolvla-cross-embodiment-mps |
SmolVLA on 16 LeRobot datasets across 4 embodiments |
Twu31/rt1-lerobot-mps |
RT-1 on 5 of the same datasets — architecture head-to-head |
Twu31/so101_hand_blue_napkin |
Real SO-ARM101 handover demonstrations |
Where this repo asks "can a new modality be added?",
smolvla-cross-embodiment-mps asks "how far does one architecture carry across robots?"
Citation
@software{smolvla_libero_eeg_2026,
author = {Twu31},
title = {SmolVLA + LIBERO + EEG: an 8-variant ablation on injecting motor-imagery
EEG into a vision-language-action policy},
year = {2026},
url = {https://github.com/twu3202/SmolVLA_cl}
}
Built on SmolVLA (Shukor et al., 2025), LIBERO (Liu et al., 2023), EEGNet (Lawhern et al., 2018) and ATCNet (Altaheri et al., 2023).