--- license: mit pipeline_tag: feature-extraction tags: - eeg - bci - motor-imagery - eegnet - graph-neural-network - positional-encoding - feature-extraction - pytorch --- # EEGNet-GNN-Pos feature extractor (F1=6, F2=12) **EEGNet** with Layer 2 replaced by a graph convolution over the electrode montage, where the montage geometry is a first-class input rather than something used once to build a fixed adjacency and then discarded. ``` Temporal conv -> POS-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; `EEGNetGNNPosClassifier` attaches a linear head. Output size is exposed as `model.flat_dim` (**372** with the defaults). ## What is position-conditioned `H = FiLM_pos(Â X W) + X W_res`, then BatchNorm + ELU, then a signed readout collapsing the electrodes to one. Coordinates enter twice: - **FiLM** — per-node scale and shift predicted from `(x, y)`. `Â` alone cannot distinguish C3 from C4: their neighbourhoods are isomorphic, so message passing treats both hemispheres identically. - **Readout** — `node_weight[:, n] = MLP(gamma(x_n, y_n))`, a continuous spatial field sampled at each electrode, rather than a free `(F_out, N)` table indexed by channel order. That buys three things a lookup table cannot have: invariance to channel ordering, transfer to a montage with a different electrode count (`transfer_to_montage`), and a spatial field that can be evaluated between electrodes (`pattern_at`). > The readout must stay **signed**. A softmax over electrodes is a non-negative convex > combination and cannot build the spatial high-pass filter that isolates focal C3/C4 ERD. ## Options | arg | default | effect | |-----|---------|--------| | `readout` | `"coord"` | `"coord"` / `"lookup"` (free table) / `"attention"` / `"mean"` | | `pos_mode` | `"film"` | `"film"` / `"bias"` (shift only) / `"none"` | | `residual` | `True` | adds un-smoothed node features back after message passing | | `learn_adjacency` | `False` | makes `Â` a trainable parameter | | `spatial` | `"graph"` | `"depthwise"` restores stock EEGNet for a controlled A/B | ## Usage ```python from eegnet_gnn_pos import EEGNetGNNPos, EEGNetGNNPosClassifier import torch backbone = EEGNetGNNPos(spatial="graph", readout="coord") model = EEGNetGNNPosClassifier(backbone, n_classes=4) x = torch.randn(1, 1, 22, 1000) # (batch, 1, channels, time) logits = model(x) # (1, 4) ``` Train end-to-end. Do **not** freeze the backbone unless loading genuinely pretrained weights. ## Caveat `readout="coord"` has more parameters than the free table it replaces (~2.1k backbone vs ~1.2k). On ~288 trials per subject that is a real cost, and the geometric structure has to earn it. The honest comparison is a three-way A/B against `readout="lookup"` and `spatial="depthwise"` with an identical head, which is what Notebook 2 runs.