CoolFace
Modelpublic

eugenehp/ST-EEGFormer

sourceHugging Facemitupdated 6mo agoView on Hugging Face
2likes14downloads
Model Card

ST-EEGFormer — Safetensors Weights

Pre-converted safetensors weights for the ST-EEGFormer EEG foundation model, ready for use with [steegformer-rs](https://github.com/eugenehp/steegformer-rs) (pure-Rust inference on Burn 0.20) or any framework that supports safetensors.

Weights are converted from the official PyTorch .pth checkpoints published at LiuyinYang1101/STEEGFormer.

ST-EEGFormer won 1st Place in the NeurIPS 2025 EEG Foundation Challenge and was accepted at ICLR 2026.

Model Files

Encoder Only (for inference / embedding extraction)

FileVariantParamsSizeLayersHeadsembed_dim
`ST-EEGFormer_small_encoder.safetensors`Small25.6 M102 MB88512
`ST-EEGFormer_base_encoder.safetensors`Base85.6 M342 MB1212768
`ST-EEGFormer_large_encoder.safetensors`Large303.0 M1,212 MB24161024
`ST-EEGFormer_largeV2_encoder.safetensors`Large V2303.1 M1,212 MB24161024

Full MAE (encoder + decoder, for reconstruction / fine-tuning)

FileVariantParamsSizeDecoder dimDecoder depth
`ST-EEGFormer_small_mae.safetensors`Small33.1 M132 MB3844
`ST-EEGFormer_base_mae.safetensors`Base111.5 M446 MB5128
`ST-EEGFormer_large_mae.safetensors`Large329.1 M1,316 MB5128
`ST-EEGFormer_largeV2_mae.safetensors`Large V2329.3 M1,317 MB5128

Config

FileDescription
`config.json`Model hyperparameters for all variants
Large V2 has undergone further pre-training on the HBN dataset for the NeurIPS 2025 EEG Foundation Challenge.

Quick Start — Rust

bash
# Install
cargo add steegformer-rs

# Download weights
huggingface-cli download eugenehp/ST-EEGFormer \
    ST-EEGFormer_small_encoder.safetensors \
    config.json \
    --local-dir weights/

# Run inference
cargo run --release --bin infer -- \
    --config weights/config.json \
    --weights weights/ST-EEGFormer_small_encoder.safetensors

Library API

rust
use steegformer_rs::{STEEGFormerEncoder, ModelConfig, data};
use std::path::Path;

// Load model
let cfg = ModelConfig::small();
let (encoder, _ms) = STEEGFormerEncoder::<B>::load_from_config(
    cfg,
    Path::new("ST-EEGFormer_small_encoder.safetensors"),
    device,
)?;

// Build input: 4 channels × 6 seconds @ 128 Hz
let channels = &["Fz", "C3", "C4", "Pz"];
let signal = vec![0.0f32; channels.len() * 768];
let batch = data::build_batch_named::<B>(signal, channels, 768, &device);

// Extract embeddings
let result = encoder.run_batch(&batch)?;
println!("Embedding shape: {:?}", result.shape);  // [512]

Quick Start — Python

python
from safetensors.torch import load_file

# Load encoder weights
state_dict = load_file("ST-EEGFormer_small_encoder.safetensors")

# Build model and load
from models_mae_eeg import mae_vit_small_patch16
model = mae_vit_small_patch16()
model.load_state_dict(state_dict, strict=False)
model.eval()

Architecture

EEG signal (B, C, T)  — up to 142 channels, 128 Hz, ≤ 6s
    │
    ▼
┌──────────────────────────────────────┐
│  PatchEmbedEEG                       │
│  Unfold → 16-sample patches          │
│  Linear(16, embed_dim)               │
│  → (B, num_patches × C, D)           │
└──────────────────────────────────────┘
    │
    + Sinusoidal Temporal PE (fixed)
    + Learned Channel Embedding (nn.Embedding(145, D))
    │
    ▼
┌──────────────────────────────────────┐
│  [CLS] token prepend                 │
└──────────────────────────────────────┘
    │
    ▼
┌──────────────────────────────────────┐
│  N × Transformer Encoder Block       │
│  Pre-norm: LN → MHSA → residual     │
│            LN → FFN  → residual     │
│  (qkv_bias=True, GELU activation)   │
└──────────────────────────────────────┘
    │
    ▼
┌──────────────────────────────────────┐
│  LayerNorm → CLS token              │
│  → (B, embed_dim) embedding         │
└──────────────────────────────────────┘

MAE Pre-training (decoder, included in *_mae.safetensors)

Encoder output (25% of tokens)
    │
    ▼
  Linear(embed_dim → decoder_dim)
  + Insert mask tokens at masked positions
  + Decoder temporal/channel PE
    │
    ▼
  M × Decoder Transformer Blocks
    │
    ▼
  Linear(decoder_dim → patch_size)
  → Reconstructed EEG patches

Numerical Parity (Rust vs Python)

Verified at every stage against the official PyTorch implementation:

StageRMSEPearson r
Patch embedding0.0000001.000000
Channel embedding0.0000001.000000
Temporal encoding0.0000001.000000
After positional encoding0.0000001.000000
After transformer block 00.0000041.000000
Full encoder (8 blocks)0.0000011.000000

Benchmarks

Platform: Apple M4 Pro, 64 GB RAM, macOS (arm64)

Inference Latency — ST-EEGFormer-Small (22ch × 768 samples)

BackendMeanMin
Rust CPU (NdArray + Accelerate)608.4 ms601.4 ms
Python CPU (PyTorch 2.6)78.1 ms77.2 ms
Rust GPU (Burn wgpu + Metal)38.1 ms7.9 ms
Python MPS (PyTorch + Metal)19.2 ms19.0 ms

Channel Scaling (T=768)

ChannelsRust CPUPython CPURust GPUPython MPS
475.5 ms21.8 ms11.5 ms4.0 ms
22596.0 ms77.9 ms32.7 ms19.3 ms
643853.2 ms301.9 ms119.4 ms90.1 ms

Weight Key Format

Encoder keys

patch_embed.proj.weight             [embed_dim, 16]
patch_embed.proj.bias               [embed_dim]
cls_token                           [1, 1, embed_dim]
enc_channel_emd.channel_transformation.weight  [145, embed_dim]
enc_temporal_emd.pe                 [1, 512, embed_dim]
blocks.{i}.norm1.weight             [embed_dim]
blocks.{i}.norm1.bias               [embed_dim]
blocks.{i}.attn.qkv.weight         [3*embed_dim, embed_dim]
blocks.{i}.attn.qkv.bias           [3*embed_dim]
blocks.{i}.attn.proj.weight        [embed_dim, embed_dim]
blocks.{i}.attn.proj.bias          [embed_dim]
blocks.{i}.norm2.weight             [embed_dim]
blocks.{i}.norm2.bias               [embed_dim]
blocks.{i}.mlp.fc1.weight          [4*embed_dim, embed_dim]
blocks.{i}.mlp.fc1.bias            [4*embed_dim]
blocks.{i}.mlp.fc2.weight          [embed_dim, 4*embed_dim]
blocks.{i}.mlp.fc2.bias            [embed_dim]
norm.weight                         [embed_dim]
norm.bias                           [embed_dim]

Decoder keys (MAE only)

decoder_embed.weight                [dec_dim, embed_dim]
decoder_embed.bias                  [dec_dim]
mask_token                          [1, 1, dec_dim]
dec_channel_emd.channel_transformation.weight  [145, dec_dim]
dec_temporal_emd.pe                 [1, 512, dec_dim]
decoder_blocks.{i}.*               (same structure as encoder)
decoder_norm.weight                 [dec_dim]
decoder_norm.bias                   [dec_dim]
decoder_pred.weight                 [16, dec_dim]
decoder_pred.bias                   [16]

Conversion

These weights were converted from the official .pth files:

python
import torch
from safetensors.torch import save_file

ckpt = torch.load("checkpoint.pth", map_location="cpu", weights_only=False)
state_dict = ckpt["model"]

# Encoder only
encoder = {k: v.float().contiguous() for k, v in state_dict.items()
           if any(k.startswith(p) for p in
                  ["patch_embed.", "cls_token", "enc_", "blocks.", "norm."])}
save_file(encoder, "encoder.safetensors")

Or use the included conversion script:

bash
python scripts/convert_to_safetensors.py --all

Citation

bibtex
@inproceedings{yang2026_steegformer,
  title={Are {EEG} Foundation Models Worth It? Comparative Evaluation
         with Traditional Decoders in Diverse {BCI} Tasks},
  author={Liuyin Yang and Qiang Sun and Ang Li and Marc M. Van Hulle},
  booktitle={The Fourteenth International Conference on Learning Representations},
  year={2026},
  url={https://openreview.net/forum?id=5Xwm8e6vbh}
}

License

MIT — same as the original ST-EEGFormer release.

Links