EEGNet-GNN feature extractor (F1=6, F2=12)

Standard EEGNet with one change: Layer 2's depthwise spatial conv is replaced by a graph convolution over the electrode montage, so electrodes mix according to how close they are on the scalp rather than as a flat, order-agnostic channel list.

Temporal conv -> GRAPH conv -> Separable conv -> Avg-pool + flatten -> flat vector
  (Layer 1)      (Layer 2)      (Layer 3)          (Layer 4)           OUTPUT
  IDENTICAL      THE SWAP       IDENTICAL          IDENTICAL

The model stops at Layer 4 and returns a flat feature vector; EEGNetGNNClassifier attaches a linear head. Output size is exposed as model.flat_dim (372 with the defaults: F1=6, F2=12, pool1=4, pool2=8, n_times=1000).

The swap

Standard EEGNet's Layer 2 learns, per output map, a single signed weighted sum over all electrodes β€” topology is ignored, but the signs let it compute contrasts. Here that becomes H = Γ‚ X W + X W_res, where Γ‚ is the symmetric-normalised adjacency of the montage. Each electrode aggregates from its physical neighbours (e.g. C3 from FC3, FC1, C5, C1, CP3, CP1); a residual path preserves un-smoothed focal detail; then a learned signed, per-feature-map readout (F2, N) collapses electrodes to the same (F2, 1, T) shape the depthwise conv produced, so Layers 1/3/4 are unchanged.

The readout must stay signed. An earlier version used softmax(node_weight), giving a non-negative convex combination shared across all feature maps. That cannot represent a difference between electrodes, and the C3-vs-C4 ERD asymmetry is the dominant motor imagery feature β€” so the model sat at chance (~25% on 4-class) no matter how long it trained.

spatial="depthwise" restores the original EEGNet for a controlled A/B.

Options

arg default effect
readout "weighted" "weighted" (signed per-map) / "attention" / "mean"
residual True adds un-smoothed node features back after message passing
learn_adjacency False makes Γ‚ a trainable parameter
pool1, pool2 4, 8 two-stage temporal pooling, as in EEGNet-8,2

Usage

from eegnet_gnn import EEGNetGNN, EEGNetGNNClassifier
import torch

backbone = EEGNetGNN(spatial="graph", F1=6, F2=12)
model = EEGNetGNNClassifier(backbone, n_classes=4)

x = torch.randn(1, 1, 22, 1000)     # (batch, 1, channels, time)
logits = model(x)                   # (1, 4)

Train the whole thing end-to-end. Do not freeze the backbone unless you are loading genuinely pretrained weights β€” freezing a random init reduces the model to logistic regression on random projections.

Downloads last month
5
Safetensors
Model size
1.73k params
Tensor type
F32
Β·
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support