OSCR

Architecture-data matching for EEG-EMG decoding: compact deep models match classical spectral decoders on the WAY-EEG-GAL grasp-and-lift dataset.

Code ↔ Paper

29 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 29 matches
  1. [1] § Materials and methods › Input representations ↔ code/02_classical_baseline.py, lines 1–61 · score 0.99 · magnitude squared coherence, 15–30 Hz, 30–45 Hz, 8–12 Hz, 55–95 Hz, low gamma
  2. [2] § Materials and methods › Decoders ↔ code/run_deep_models_local.py, lines 161–191 · score 0.94 · attention weighted edges, fully connected, Graph attention network, channel tokens, temporal CNN, Conv1d
  3. [3] § Materials and methods › Decoders ↔ code/run_deep_models_local.py, lines 128–158 · score 0.93 · learnable CLS token, Conv1d patch embedding, positional embedding, encoder layers, Transformer encoder, feed
  4. [4] § Materials and methods › Decoders ↔ code/02b_classical_eval.py, lines 1–23 · score 0.92 · random forest, linear SVM, gradient boosting, max_depth, max_iter, logistic regression
  5. [5] § Materials and methods › Decoders ↔ code/revision_ablations.py, lines 123–154 · score 0.89 · convolution attention hybrids, DBConformer, MFTNet, EEG Conformer, encoder layers, Transformer encoder
  6. [6] § Materials and methods › Decoders ↔ code/07_figures.py, lines 70–103 · score 0.89 · gradient boosting, hidden layers, max_depth, max_iter, logistic regression, class weight
  7. [7] § Materials and methods › Decoders ↔ code/04_neural_baselines.py, lines 1–25 · score 0.87 · flattened raw downsampled, hand crafted spectral, channel band, band power, log, coherence
  8. [8] § Materials and methods › Input representations ↔ code/revision_ablations.py, lines 247–288 · score 0.84 · 15–30 Hz, 30–45 Hz, 8–12 Hz, 55–95 Hz, IQR, log10
  9. [9] § Materials and methods › Decoders ↔ code/revision_ablations.py, lines 190–227 · score 0.80 · cross entropy loss, AdamW, weight decay, LOSO folds, PyTorch, seed
  10. [10] § Materials and methods › Preprocessing and trial epoching ↔ code/01_build_dataset.py, lines 1–52 · score 0.79 · 1–100 Hz, Interference EMG, 20–500 Hz, rectification, notch, decimated
  11. [11] § Materials and methods › Decoders ↔ code/run_deep_models_local.py, lines 87–125 · score 0.79 · separable temporal convolution, depthwise spatial convolution, EEGNet, kernel, ELU, blocks
  12. [12] § Materials and methods › Interpretability ↔ extract_deep_interpretability.py, lines 1–36 · score 0.79 · model native interpretability, deep decoders, channel attention, LOSO folds, EEGNet, filters
  13. [13] § Results › Robustness analyses › Deep-model interpretability sanity check ↔ extract_deep_interpretability.py, lines 124–242 · score 0.78 · depthwise spatial weights, attention rollout, temporal filters, EEG channels, EEGNet, heads
  14. [14] § Materials and methods › Decoders ↔ code/regen_fig3.py, lines 108–140 · score 0.76 · cross entropy loss, AdamW, weight decay, PyTorch, seed, batch
  15. [15] § Materials and methods › Preprocessing and trial epoching ↔ code/01_build_dataset.py, lines 1–52 · score 0.75 · Skipping ICA, sustained hold, notch, artifact, decimation, pipeline
  16. [16] § Results › Fused-model feature importance suggests sensorimotor EEG involvement; modality ablation (Section 3.5.3) shows dominant EMG dependence ↔ code/02_classical_baseline.py, lines 1–61 · score 0.74 · low gamma, high gamma, band power, EEG channel, AD, CED
  17. [17] § Materials and methods › Decoders ↔ code/07_figures.py, lines 47–68 · score 0.71 · MLP_spectral, MLP_temporal, MLP_graph, log, classical
  18. [18] § Materials and methods › Preprocessing and trial epoching ↔ code/07_figures.py, lines 142–168 · score 0.70 · Pipeline overview, raw temporal, channel pooled, WAY EEG GAL, Friedman, Wilcoxon
  19. [19] § Materials and methods › Interpretability ↔ code/06_interpretability.py, lines 1–24 · score 0.69 · coefficient magnitude, rest classes, model native, HGBM, classical, surface
  20. [20] § Materials and methods › Preprocessing and trial epoching ↔ code/01_build_dataset.py, lines 142–215 · score 0.67 · LEDOff, fallback window, LEDOn, cropped, lift, Preprocessing
  21. [21] § Materials and methods › Interpretability ↔ code/revision_ablations.py, lines 1–50 · score 0.67 · channel attention matrices, model native interpretability, style, convolutional, heads, folds
  22. [22] § Results › Confusion structure ↔ code/regen_figS3.py, lines 112–156 · score 0.65 · confusion matrices, logistic regression, best classical, suede, predictions, sandpaper
  23. [23] § Results › Confusion structure ↔ code/07_figures.py, lines 70–103 · score 0.65 · confusion matrices, logistic regression, best classical, suede, predictions, sandpaper
  24. [24] § Materials and methods › Decoders ↔ code/revision_ablations.py, lines 424–456 · score 0.63 · deep model EMG, bandwidth harmonization, EMG bandwidth, baseline, 250 Hz, 95 Hz
  25. [25] § Results › Confusion structure ↔ code/regen_fig3.py, lines 157–209 · score 0.59 · confusion matrices, logistic regression, EEGNet, suede, predictions, sandpaper
  26. [26] § Results › Confusion structure ↔ code/regen_figS3.py, lines 112–156 · score 0.59 · confusion matrices, logistic regression, EEGNet, suede, predictions, sandpaper
  27. [27] § Materials and methods › Dataset ↔ code/run_deep_models_local.py, lines 1–65 · score 0.53 · WAY EEG GAL, grasp, suede, sandpaper, lift, silk
  28. [28] § Materials and methods › Evaluation protocol and statistical analysis ↔ code/05_compare.py, lines 1–64 · score 0.52 · paired Wilcoxon, Cohen, dz, Balanced accuracy, Friedman, score
  29. [29] § Materials and methods › Evaluation protocol and statistical analysis ↔ code/revision_ablations.py, lines 247–288 · score 0.52 · F1 score, modality ablation, scaler, Balanced accuracy, fitted, macro

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Python · 528 lines · 28 KB · MIT · 6 matches

  1. """
  2. revision_ablations.py
  3. =====================
  4. Follow-up ablations requested by the Stanford AI reviewer. Five experiments
  5. in one resumable script:
  6. (i) 5-seed robustness for cnn / transformer / gnn (means +/- SD across seeds)
  7. (ii) modality ablation: EEG-only, EMG-only, fused for HGBM, CNN, GNN
  8. (iii) compact convolution-attention hybrid (Conformer-style) as a tighter
  9. attention comparator distinct from the 4-layer Transformer
  10. (iv) partial-crossing conditioning analyses (decode weight at fixed surface,
  11. decode surface at fixed weight)
  12. (v) model-native interpretability dump: GNN per-channel attention weights,
  13. CNN gradient saliency over time-channel for representative trials
  14. (vi) EMG-bandwidth harmonisation: 95-Hz lowpass on EMG inputs to deep models
  15. so they see the same bandwidth as classical features (20-95 Hz)
  16. (vii) early-hold (first 500 ms) vs late-hold (last 500 ms) decoding to test
  17. whether tactile-evoked cortical signatures concentrate near contact
  18. Setup (same venv used for run_complete_local.py):
  19. .venv\Scripts\activate
  20. python code\revision_ablations.py
  21. Default run: all five experiments, ~ 45-90 min on RTX 5080. Use flags to
  22. restrict (e.g. --skip seeds modality):
  23. python code\revision_ablations.py --only seeds
  24. python code\revision_ablations.py --only modality conformer
  25. python code\revision_ablations.py --skip interp
  26. Outputs (all under <root>/results/):
  27. seed_robustness.csv per (model, task, seed, fold) row
  28. modality_ablation.csv per (model, task, modality, fold) row
  29. conformer_hybrid.csv per (task, fold) row (model = conformer)
  30. crossing_conditioning.csv per (task_within_factor, fold) row
  31. gnn_attention_weights.npy per-fold attention matrices [12, n_layers, n_heads, 37, 37]
  32. cnn_saliency_examples.npy [n_examples, 37, 500] gradient saliency
  33. revision_summary.txt human-readable summary block
  34. """
  35. from __future__ import annotations
  36. import argparse, sys, time
  37. from pathlib import Path
  38. import numpy as np, pandas as pd
  39. import torch, torch.nn as nn, torch.nn.functional as F
  40. from sklearn.ensemble import HistGradientBoostingClassifier
  41. from sklearn.metrics import balanced_accuracy_score, f1_score
  42. from sklearn.preprocessing import StandardScaler
  43. from sklearn.utils.class_weight import compute_class_weight
  44. from torch.utils.data import DataLoader, TensorDataset
  45. # --- Paths (auto-detect; same convention as run_complete_local.py) -----------
  46. SCRIPT_DIR = Path(__file__).resolve().parent
  47. ROOT = SCRIPT_DIR.parent if SCRIPT_DIR.name == "code" else SCRIPT_DIR
  48. CACHE = ROOT / "cache_v2"
  49. RESULTS = ROOT / "results"; RESULTS.mkdir(exist_ok=True)
  50. N_EEG, N_EMG, N_CHANNELS, N_SAMPLES, N_CLASSES = 32, 5, 37, 500, 3
  51. # ---- Models (same as run_complete_local.py) --------------------------------
  52. class EEGNet(nn.Module):
  53. def __init__(self, n_channels=N_CHANNELS, F1=16, D=2, F2=32, dropout=0.4):
  54. super().__init__()
  55. self.conv1 = nn.Conv2d(1, F1, (1, 64), padding=(0, 32), bias=False)
  56. self.bn1 = nn.BatchNorm2d(F1)
  57. self.conv2 = nn.Conv2d(F1, F1*D, (n_channels, 1), groups=F1, bias=False)
  58. self.bn2 = nn.BatchNorm2d(F1*D)
  59. self.pool1 = nn.AvgPool2d((1, 4)); self.drop1 = nn.Dropout(dropout)
  60. self.conv3 = nn.Conv2d(F1*D, F2, (1, 16), padding=(0, 8), bias=False)
  61. self.bn3 = nn.BatchNorm2d(F2)
  62. self.pool2 = nn.AvgPool2d((1, 8)); self.drop2 = nn.Dropout(dropout)
  63. with torch.no_grad():
  64. d = torch.zeros(1, 1, n_channels, N_SAMPLES)
  65. h = self.bn2(self.conv2(self.bn1(self.conv1(d))))
  66. h = self.pool2(self.bn3(self.conv3(self.pool1(h))))
  67. self.flat = h.numel()
  68. self.fc = nn.Linear(self.flat, N_CLASSES)
  69. def forward(self, x):
  70. x = x.unsqueeze(1)
  71. x = self.bn1(self.conv1(x))
  72. x = F.elu(self.bn2(self.conv2(x))); x = self.drop1(self.pool1(x))
  73. x = F.elu(self.bn3(self.conv3(x))); x = self.drop2(self.pool2(x))
  74. return self.fc(x.flatten(1))
  75. class EEGTransformer(nn.Module):
  76. def __init__(self, n_channels=N_CHANNELS, n_tokens=25, d_model=128, n_heads=8, n_layers=4, dropout=0.2):
  77. super().__init__()
  78. stride = N_SAMPLES // n_tokens
  79. self.patch = nn.Conv1d(n_channels, d_model, kernel_size=stride, stride=stride)
  80. self.cls = nn.Parameter(torch.randn(1, 1, d_model)*0.02)
  81. self.pos = nn.Parameter(torch.randn(1, n_tokens+1, d_model)*0.02)
  82. layer = nn.TransformerEncoderLayer(d_model=d_model, nhead=n_heads,
  83. dim_feedforward=d_model*2, dropout=dropout, batch_first=True, activation="gelu")
  84. self.encoder = nn.TransformerEncoder(layer, num_layers=n_layers)
  85. self.norm = nn.LayerNorm(d_model)
  86. self.fc = nn.Linear(d_model, N_CLASSES)
  87. def forward(self, x):
  88. x = self.patch(x).transpose(1, 2)
  89. cls = self.cls.expand(x.size(0), -1, -1)
  90. x = torch.cat([cls, x], dim=1) + self.pos
  91. x = self.encoder(x)
  92. return self.fc(self.norm(x[:, 0]))
  93. class EEGGNN(nn.Module):
  94. def __init__(self, n_channels=N_CHANNELS, embed_dim=64, n_heads=4, n_layers=2, dropout=0.2):
  95. super().__init__()
  96. self.stem = nn.Sequential(
  97. nn.Conv1d(1, 16, 16, stride=4), nn.ELU(),
  98. nn.Conv1d(16, 32, 8, stride=4), nn.ELU(),
  99. nn.AdaptiveAvgPool1d(1))
  100. self.proj = nn.Linear(32, embed_dim)
  101. layer = nn.TransformerEncoderLayer(d_model=embed_dim, nhead=n_heads,
  102. dim_feedforward=embed_dim*2, dropout=dropout, batch_first=True, activation="gelu")
  103. self.gat = nn.TransformerEncoder(layer, num_layers=n_layers)
  104. self.norm = nn.LayerNorm(embed_dim)
  105. self.fc = nn.Linear(embed_dim, N_CLASSES)
  106. def forward(self, x):
  107. B, C, T = x.shape
  108. h = self.stem(x.reshape(B*C, 1, T)).squeeze(-1)
  109. h = self.proj(h.view(B, C, 32))
  110. h = self.gat(h)
  111. return self.fc(self.norm(h.mean(1)))
  112. class ConformerHybrid(nn.Module):
  113. """Compact convolution-attention hybrid: an EEGNet stem (~4k params) followed by
  114. a single self-attention block (~25k params). ~30k params total -- aligned with
  115. the EEG-Conformer / DBConformer / EEG-MFTNet family at clinical sample size."""
  116. def __init__(self, n_channels=N_CHANNELS, F1=16, D=2, embed_dim=64, n_heads=4, dropout=0.2):
  117. super().__init__()
  118. self.conv1 = nn.Conv2d(1, F1, (1, 64), padding=(0, 32), bias=False)
  119. self.bn1 = nn.BatchNorm2d(F1)
  120. self.conv2 = nn.Conv2d(F1, F1*D, (n_channels, 1), groups=F1, bias=False)
  121. self.bn2 = nn.BatchNorm2d(F1*D)
  122. self.pool = nn.AvgPool2d((1, 8))
  123. self.drop = nn.Dropout(dropout)
  124. # tokenize the (F1*D)-channel time stream into 12 tokens
  125. with torch.no_grad():
  126. d = torch.zeros(1, 1, n_channels, N_SAMPLES)
  127. h = self.pool(self.bn2(self.conv2(self.bn1(self.conv1(d)))))
  128. self._L = h.shape[-1]
  129. self.proj = nn.Linear(F1*D, embed_dim)
  130. layer = nn.TransformerEncoderLayer(d_model=embed_dim, nhead=n_heads,
  131. dim_feedforward=embed_dim*2, dropout=dropout, batch_first=True, activation="gelu")
  132. self.attn = nn.TransformerEncoder(layer, num_layers=1)
  133. self.norm = nn.LayerNorm(embed_dim)
  134. self.fc = nn.Linear(embed_dim, N_CLASSES)
  135. def forward(self, x):
  136. x = x.unsqueeze(1)
  137. x = F.elu(self.bn2(self.conv2(self.bn1(self.conv1(x)))))
  138. x = self.drop(self.pool(x))
  139. # x: (B, F1*D, 1, L) -> tokens (B, L, F1*D)
  140. x = x.squeeze(2).transpose(1, 2)
  141. x = self.proj(x)
  142. x = self.attn(x)
  143. return self.fc(self.norm(x.mean(1)))
  144. MODELS = {"cnn": EEGNet, "transformer": EEGTransformer, "gnn": EEGGNN, "conformer": ConformerHybrid}
  145. # ---- Data loading ----------------------------------------------------------
  146. def load_all_data(task, modality="fused"):
  147. """modality in {fused, eeg, emg}. eeg drops EMG channels; emg keeps only EMG."""
  148. Xs, ys, subj = [], [], []
  149. for p in range(1, 13):
  150. d = np.load(CACHE / f"P{p}.npz")
  151. v = d["valid_mask"]
  152. eeg = d["eeg"][v]; emg = d["emg"][v]
  153. if modality == "eeg":
  154. x = eeg.astype(np.float32) # (n, 32, 500)
  155. elif modality == "emg":
  156. x = emg.astype(np.float32) # (n, 5, 500)
  157. else:
  158. x = np.concatenate([eeg, emg], axis=1).astype(np.float32) # (n, 37, 500)
  159. y = (d["y_weight"] if task == "weight" else d["y_surface"])[v]
  160. Xs.append(x); ys.append(y); subj.append(np.full(len(x), p, dtype=np.int8))
  161. return np.concatenate(Xs), np.concatenate(ys), np.concatenate(subj)
  162. def load_metadata():
  163. """Loads CurW and CurS per trial across all 12 subjects (used for conditioning)."""
  164. rows = []
  165. for p in range(1, 13):
  166. d = np.load(CACHE / f"P{p}.npz")
  167. v = d["valid_mask"]
  168. n = int(v.sum())
  169. rows.append(pd.DataFrame({
  170. "subj": np.full(n, p),
  171. "weight": d["y_weight"][v],
  172. "surface": d["y_surface"][v]
  173. }))
  174. return pd.concat(rows, ignore_index=True)
  175. # ---- Deep training one fold ------------------------------------------------
  176. def deep_loso_fold(model_class, X, y, subj, held, *, device, n_epochs, batch_size, lr, seed, n_channels=N_CHANNELS):
  177. torch.manual_seed(seed); np.random.seed(seed)
  178. test = subj == held
  179. Xtr, ytr = X[~test], y[~test].astype(np.int64)
  180. Xte, yte = X[test], y[test].astype(np.int64)
  181. mu = Xtr.mean(0, keepdims=True); sd = Xtr.std(0, keepdims=True) + 1e-6
  182. Xtr = ((Xtr-mu)/sd).astype(np.float32); Xte = ((Xte-mu)/sd).astype(np.float32)
  183. Xtr_t, ytr_t = torch.from_numpy(Xtr), torch.from_numpy(ytr)
  184. Xte_t = torch.from_numpy(Xte)
  185. cw = compute_class_weight("balanced", classes=np.array([0,1,2]), y=ytr)
  186. loss_fn = nn.CrossEntropyLoss(weight=torch.tensor(cw, dtype=torch.float32).to(device))
  187. if model_class in (EEGNet, ConformerHybrid):
  188. model = model_class(n_channels=n_channels).to(device)
  189. elif model_class is EEGGNN:
  190. model = model_class(n_channels=n_channels).to(device)
  191. elif model_class is EEGTransformer:
  192. model = model_class(n_channels=n_channels).to(device)
  193. else:
  194. model = model_class().to(device)
  195. opt = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4)
  196. loader = DataLoader(TensorDataset(Xtr_t, ytr_t), batch_size=batch_size, shuffle=True)
  197. model.train()
  198. for _ in range(n_epochs):
  199. for xb, yb in loader:
  200. xb, yb = xb.to(device), yb.to(device)
  201. opt.zero_grad()
  202. loss_fn(model(xb), yb).backward()
  203. opt.step()
  204. model.eval()
  205. preds = []
  206. with torch.no_grad():
  207. for i in range(0, len(Xte_t), batch_size):
  208. preds.append(model(Xte_t[i:i+batch_size].to(device)).argmax(1).cpu().numpy())
  209. yp = np.concatenate(preds)
  210. return dict(held_out_subject=int(held), n_test=int(test.sum()),
  211. balanced_accuracy=float(balanced_accuracy_score(yte, yp)),
  212. f1_macro=float(f1_score(yte, yp, average="macro"))), model
  213. # ---- Experiment 1: seed robustness -----------------------------------------
  214. def exp_seeds(device, models=("cnn","transformer","gnn"), seeds=(0,1,2,3,4),
  215. n_epochs=30, batch_size=64, lr=1e-3):
  216. rows = []
  217. for task in ("weight","surface"):
  218. X, y, subj = load_all_data(task, "fused")
  219. for m in models:
  220. for s in seeds:
  221. for h in range(1, 13):
  222. t0 = time.time()
  223. res, _ = deep_loso_fold(MODELS[m], X, y, subj, h,
  224. device=device, n_epochs=n_epochs,
  225. batch_size=batch_size, lr=lr, seed=s)
  226. rows.append({"experiment":"seeds","model":m,"task":task,"seed":s,**res,"sec":round(time.time()-t0,1)})
  227. print(f"[seeds] {m}/{task}/seed{s}/P{h:02d}: bal={res['balanced_accuracy']:.3f}", flush=True)
  228. pd.DataFrame(rows).to_csv(RESULTS / "seed_robustness.csv", index=False)
  229. print(f"saved {RESULTS/'seed_robustness.csv'}")
  230. # ---- Experiment 2: modality ablation ---------------------------------------
  231. def exp_modality(device, n_epochs=30, batch_size=64, lr=1e-3, seed=0):
  232. rows = []
  233. for task in ("weight","surface"):
  234. for modality in ("eeg","emg","fused"):
  235. n_ch = {"eeg": N_EEG, "emg": N_EMG, "fused": N_CHANNELS}[modality]
  236. X, y, subj = load_all_data(task, modality)
  237. # HGBM on flattened mean+std+IQR per channel + log-power per channel
  238. from scipy.signal import welch
  239. Xf = []
  240. for x in X:
  241. ch_mean = x.mean(axis=1); ch_std = x.std(axis=1)
  242. ch_iqr = np.percentile(x, 75, axis=1) - np.percentile(x, 25, axis=1)
  243. feats = [ch_mean, ch_std, ch_iqr]
  244. for lo, hi in [(8,12),(15,30),(30,45),(55,95)]:
  245. f, pxx = welch(x, fs=500, nperseg=128, axis=-1)
  246. mask = (f >= lo) & (f <= hi)
  247. feats.append(np.log10(pxx[..., mask].mean(axis=-1) + 1e-12))
  248. Xf.append(np.concatenate(feats))
  249. Xf = np.stack(Xf)
  250. sc = StandardScaler().fit(Xf); Xs = sc.transform(Xf)
  251. for h in range(1, 13):
  252. test = subj == h
  253. clf = HistGradientBoostingClassifier(max_iter=200, max_depth=4,
  254. learning_rate=0.05, class_weight="balanced",
  255. random_state=0)
  256. clf.fit(Xs[~test], y[~test])
  257. yp = clf.predict(Xs[test])
  258. rows.append({"experiment":"modality","model":"HGBM","modality":modality,
  259. "task":task,"held_out_subject":h,
  260. "balanced_accuracy": balanced_accuracy_score(y[test], yp),
  261. "f1_macro": f1_score(y[test], yp, average="macro")})
  262. for m in ("cnn", "gnn"):
  263. for h in range(1, 13):
  264. res, _ = deep_loso_fold(MODELS[m], X, y, subj, h, device=device,
  265. n_epochs=n_epochs, batch_size=batch_size,
  266. lr=lr, seed=seed, n_channels=n_ch)
  267. rows.append({"experiment":"modality","model":m,"modality":modality,
  268. "task":task,**res})
  269. print(f"[modality] {m}/{task}/{modality}/P{h:02d}: bal={res['balanced_accuracy']:.3f}", flush=True)
  270. pd.DataFrame(rows).to_csv(RESULTS / "modality_ablation.csv", index=False)
  271. print(f"saved {RESULTS/'modality_ablation.csv'}")
  272. # ---- Experiment 3: Conformer-style hybrid ----------------------------------
  273. def exp_conformer(device, n_epochs=30, batch_size=64, lr=1e-3, seed=0):
  274. rows = []
  275. for task in ("weight","surface"):
  276. X, y, subj = load_all_data(task, "fused")
  277. for h in range(1, 13):
  278. res, _ = deep_loso_fold(ConformerHybrid, X, y, subj, h,
  279. device=device, n_epochs=n_epochs,
  280. batch_size=batch_size, lr=lr, seed=seed)
  281. rows.append({"experiment":"conformer","model":"conformer","task":task,**res})
  282. print(f"[conformer] {task}/P{h:02d}: bal={res['balanced_accuracy']:.3f}", flush=True)
  283. pd.DataFrame(rows).to_csv(RESULTS / "conformer_hybrid.csv", index=False)
  284. print(f"saved {RESULTS/'conformer_hybrid.csv'}")
  285. # ---- Experiment 4: partial-crossing conditioning ---------------------------
  286. def exp_crossing(device, n_epochs=30, batch_size=64, lr=1e-3, seed=0):
  287. rows = []
  288. md = load_metadata()
  289. X_full, y_full, subj_full = load_all_data("weight", "fused")
  290. # Decode weight conditioned on a fixed surface (the surface with most coverage)
  291. for fix_surf in [0, 2]: # sandpaper, silk
  292. keep = md.surface.values == fix_surf
  293. X_sub = X_full[keep]; y_sub = y_full[keep]; subj_sub = subj_full[keep]
  294. for m in ("cnn", "gnn"):
  295. for h in range(1, 13):
  296. if (subj_sub == h).sum() < 5: continue
  297. res, _ = deep_loso_fold(MODELS[m], X_sub, y_sub, subj_sub, h,
  298. device=device, n_epochs=n_epochs,
  299. batch_size=batch_size, lr=lr, seed=seed)
  300. rows.append({"experiment":"conditioning","model":m,
  301. "decode":"weight","fixed_factor":"surface","fixed_value":int(fix_surf),
  302. "task":"weight",**res})
  303. # Decode surface conditioned on a fixed weight
  304. X_full2, y_full2, subj_full2 = load_all_data("surface", "fused")
  305. for fix_w in [0, 1, 2]:
  306. keep = md.weight.values == fix_w
  307. X_sub = X_full2[keep]; y_sub = y_full2[keep]; subj_sub = subj_full2[keep]
  308. for m in ("cnn", "gnn"):
  309. for h in range(1, 13):
  310. if (subj_sub == h).sum() < 5: continue
  311. res, _ = deep_loso_fold(MODELS[m], X_sub, y_sub, subj_sub, h,
  312. device=device, n_epochs=n_epochs,
  313. batch_size=batch_size, lr=lr, seed=seed)
  314. rows.append({"experiment":"conditioning","model":m,
  315. "decode":"surface","fixed_factor":"weight","fixed_value":int(fix_w),
  316. "task":"surface",**res})
  317. pd.DataFrame(rows).to_csv(RESULTS / "crossing_conditioning.csv", index=False)
  318. print(f"saved {RESULTS/'crossing_conditioning.csv'}")
  319. # ---- Experiment 5: model-native interpretability ---------------------------
  320. def exp_interp(device, n_epochs=30, batch_size=64, lr=1e-3, seed=0):
  321. """Hook attention weights from the GNN (per-fold) and gradient saliency for the CNN."""
  322. X, y, subj = load_all_data("weight", "fused")
  323. attn_per_fold = []
  324. for h in range(1, 13):
  325. torch.manual_seed(seed); np.random.seed(seed)
  326. test = subj == h
  327. Xtr, ytr = X[~test], y[~test].astype(np.int64)
  328. mu = Xtr.mean(0, keepdims=True); sd = Xtr.std(0, keepdims=True) + 1e-6
  329. Xtr = ((Xtr-mu)/sd).astype(np.float32)
  330. Xte = ((X[test]-mu)/sd).astype(np.float32)
  331. cw = compute_class_weight("balanced", classes=np.array([0,1,2]), y=ytr)
  332. loss_fn = nn.CrossEntropyLoss(weight=torch.tensor(cw, dtype=torch.float32).to(device))
  333. model = EEGGNN().to(device)
  334. opt = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4)
  335. Xtr_t = torch.from_numpy(Xtr); ytr_t = torch.from_numpy(ytr)
  336. loader = DataLoader(TensorDataset(Xtr_t, ytr_t), batch_size=batch_size, shuffle=True)
  337. model.train()
  338. for _ in range(n_epochs):
  339. for xb, yb in loader:
  340. xb, yb = xb.to(device), yb.to(device)
  341. opt.zero_grad(); loss_fn(model(xb), yb).backward(); opt.step()
  342. model.eval()
  343. # Hook attention from the first GAT layer
  344. attn_buffer = []
  345. def hook(module, inputs, output):
  346. # MultiheadAttention returns (attn_output, attn_weights) when need_weights=True
  347. pass
  348. # Compute attention manually by re-running the model with need_weights=True path
  349. Xte_t = torch.from_numpy(Xte).to(device)
  350. with torch.no_grad():
  351. B, C, T = Xte_t.shape
  352. h_ = model.stem(Xte_t.reshape(B*C, 1, T)).squeeze(-1)
  353. h_ = model.proj(h_.view(B, C, 32))
  354. # iterate the encoder layers and capture attention
  355. attns = []
  356. x_ = h_
  357. for layer in model.gat.layers:
  358. # nn.TransformerEncoderLayer self-attn
  359. x_norm = layer.norm1(x_)
  360. attn_out, attn_w = layer.self_attn(x_norm, x_norm, x_norm,
  361. need_weights=True, average_attn_weights=False)
  362. x_ = x_ + layer.dropout1(attn_out)
  363. x_ = x_ + layer.dropout2(layer.linear2(layer.dropout(layer.activation(layer.linear1(layer.norm2(x_))))))
  364. attns.append(attn_w.mean(0).cpu().numpy()) # [n_heads, C, C] averaged over batch
  365. attn_per_fold.append(np.stack(attns)) # [n_layers, n_heads, C, C]
  366. np.save(RESULTS / "gnn_attention_weights.npy", np.stack(attn_per_fold))
  367. print(f"saved {RESULTS/'gnn_attention_weights.npy'}", " shape:", np.stack(attn_per_fold).shape)
  368. # CNN saliency: train a single fold (P1 held out), compute per-class saliency on a few test trials
  369. torch.manual_seed(seed); np.random.seed(seed)
  370. test = subj == 1
  371. Xtr, ytr = X[~test], y[~test].astype(np.int64)
  372. Xte, yte = X[test], y[test].astype(np.int64)
  373. mu = Xtr.mean(0, keepdims=True); sd = Xtr.std(0, keepdims=True) + 1e-6
  374. Xtr = ((Xtr-mu)/sd).astype(np.float32); Xte = ((Xte-mu)/sd).astype(np.float32)
  375. cw = compute_class_weight("balanced", classes=np.array([0,1,2]), y=ytr)
  376. loss_fn = nn.CrossEntropyLoss(weight=torch.tensor(cw, dtype=torch.float32).to(device))
  377. cnn = EEGNet().to(device)
  378. opt = torch.optim.AdamW(cnn.parameters(), lr=lr, weight_decay=1e-4)
  379. loader = DataLoader(TensorDataset(torch.from_numpy(Xtr), torch.from_numpy(ytr)), batch_size=batch_size, shuffle=True)
  380. cnn.train()
  381. for _ in range(n_epochs):
  382. for xb, yb in loader:
  383. xb, yb = xb.to(device), yb.to(device)
  384. opt.zero_grad(); loss_fn(cnn(xb), yb).backward(); opt.step()
  385. cnn.eval()
  386. # Pick 2 trials per class
  387. examples_idx = []
  388. for c in range(3):
  389. cls_idx = np.where(yte == c)[0][:2]
  390. examples_idx.extend(cls_idx)
  391. saliencies = []
  392. for idx in examples_idx:
  393. x = torch.from_numpy(Xte[idx:idx+1]).to(device).requires_grad_(True)
  394. logits = cnn(x)
  395. c = int(yte[idx])
  396. cnn.zero_grad()
  397. logits[0, c].backward()
  398. sal = x.grad.abs().squeeze(0).cpu().numpy() # (37, 500)
  399. saliencies.append(sal)
  400. np.save(RESULTS / "cnn_saliency_examples.npy", np.stack(saliencies))
  401. print(f"saved {RESULTS/'cnn_saliency_examples.npy'}", " shape:", np.stack(saliencies).shape)
  402. # ---- Experiment 6: EMG-bandwidth harmonisation -----------------------------
  403. def exp_bandwidth(device, n_epochs=30, batch_size=64, lr=1e-3, seed=0):
  404. """Apply a 95-Hz lowpass to the deep-model EMG input and re-train CNN/GNN/HGBM
  405. so all decoders see the same EMG bandwidth (20-95 Hz). Compare the resulting
  406. LOSO accuracies to the unharmonised baseline."""
  407. from scipy.signal import butter, filtfilt
  408. rows = []
  409. # 95-Hz lowpass at 500 Hz: butter order 4
  410. b95, a95 = butter(4, 95.0 / 250.0, btype="low")
  411. for task in ("weight","surface"):
  412. # Load fused (32 EEG + 5 EMG)
  413. Xs, ys, subj = [], [], []
  414. for p in range(1, 13):
  415. d = np.load(CACHE / f"P{p}.npz")
  416. v = d["valid_mask"]
  417. eeg = d["eeg"][v]
  418. emg = d["emg"][v]
  419. # Apply 95-Hz LP to EMG channels along time axis
  420. emg_lp = filtfilt(b95, a95, emg, axis=-1).astype(np.float32)
  421. x = np.concatenate([eeg, emg_lp], axis=1).astype(np.float32)
  422. y = (d["y_weight"] if task == "weight" else d["y_surface"])[v]
  423. Xs.append(x); ys.append(y); subj.append(np.full(len(x), p, dtype=np.int8))
  424. X = np.concatenate(Xs); y = np.concatenate(ys); subj = np.concatenate(subj)
  425. for m in ("cnn", "gnn"):
  426. for h in range(1, 13):
  427. res, _ = deep_loso_fold(MODELS[m], X, y, subj, h, device=device,
  428. n_epochs=n_epochs, batch_size=batch_size,
  429. lr=lr, seed=seed)
  430. rows.append({"experiment":"bandwidth","model":m,"task":task,
  431. "harmonisation":"emg_lowpass_95hz",**res})
  432. print(f"[bandwidth] {m}/{task}/P{h:02d}: bal={res['balanced_accuracy']:.3f}", flush=True)
  433. pd.DataFrame(rows).to_csv(RESULTS / "bandwidth_harmonisation.csv", index=False)
  434. print(f"saved {RESULTS/'bandwidth_harmonisation.csv'}")
  435. # ---- Experiment 7: early-hold vs late-hold ---------------------------------
  436. def exp_earlyhold(device, n_epochs=30, batch_size=64, lr=1e-3, seed=0):
  437. """Decode from the first 500 ms (early-hold) vs the last 500 ms (late-hold)
  438. of the sustained-hold window. The cached window is already 1 s; we slice
  439. in half along the time axis."""
  440. rows = []
  441. for task in ("weight","surface"):
  442. X, y, subj = load_all_data(task, "fused") # (n, 37, 500)
  443. halves = {"early": X[..., :250], "late": X[..., 250:]}
  444. for label, X_half in halves.items():
  445. # Pad/repeat to 500 samples so the existing CNN/GNN architectures fit unchanged
  446. X_pad = np.concatenate([X_half, X_half], axis=-1) # tile to 500
  447. for m in ("cnn", "gnn"):
  448. for h in range(1, 13):
  449. res, _ = deep_loso_fold(MODELS[m], X_pad, y, subj, h, device=device,
  450. n_epochs=n_epochs, batch_size=batch_size,
  451. lr=lr, seed=seed)
  452. rows.append({"experiment":"earlyhold","model":m,"task":task,
  453. "phase":label, **res})
  454. print(f"[earlyhold] {m}/{task}/{label}/P{h:02d}: bal={res['balanced_accuracy']:.3f}", flush=True)
  455. pd.DataFrame(rows).to_csv(RESULTS / "earlyhold_vs_latehold.csv", index=False)
  456. print(f"saved {RESULTS/'earlyhold_vs_latehold.csv'}")
  457. # ---- Summary ---------------------------------------------------------------
  458. def summarize():
  459. summary = []
  460. for name, fn in [
  461. ("seed_robustness.csv", lambda df: df.groupby(["model","task"])["balanced_accuracy"].agg(["mean","std","count"]).round(3)),
  462. ("modality_ablation.csv", lambda df: df.groupby(["model","modality","task"])["balanced_accuracy"].agg(["mean","std"]).round(3)),
  463. ("conformer_hybrid.csv", lambda df: df.groupby(["task"])["balanced_accuracy"].agg(["mean","std"]).round(3)),
  464. ("crossing_conditioning.csv", lambda df: df.groupby(["model","decode","fixed_factor","fixed_value"])["balanced_accuracy"].agg(["mean","std","count"]).round(3)),
  465. ("bandwidth_harmonisation.csv", lambda df: df.groupby(["model","task"])["balanced_accuracy"].agg(["mean","std"]).round(3)),
  466. ("earlyhold_vs_latehold.csv", lambda df: df.groupby(["model","task","phase"])["balanced_accuracy"].agg(["mean","std"]).round(3)),
  467. ]:
  468. path = RESULTS / name
  469. if path.exists():
  470. df = pd.read_csv(path)
  471. summary.append(f"\n=== {name} ===\n{fn(df).to_string()}")
  472. out = "\n".join(summary)
  473. (RESULTS / "revision_summary.txt").write_text(out)
  474. print("\n" + out)
  475. print(f"\nsaved {RESULTS/'revision_summary.txt'}")
  476. # ---- Main ------------------------------------------------------------------
  477. def main():
  478. ap = argparse.ArgumentParser()
  479. ap.add_argument("--only", nargs="+", choices=["seeds","modality","conformer","crossing","interp","bandwidth","earlyhold"])
  480. ap.add_argument("--skip", nargs="+", default=[], choices=["seeds","modality","conformer","crossing","interp","bandwidth","earlyhold"])
  481. ap.add_argument("--epochs", type=int, default=30)
  482. ap.add_argument("--batch-size", type=int, default=64)
  483. ap.add_argument("--lr", type=float, default=1e-3)
  484. args = ap.parse_args()
  485. device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
  486. print("Using device:", device)
  487. todo = ["seeds","modality","conformer","crossing","interp","bandwidth","earlyhold"]
  488. if args.only:
  489. todo = [t for t in todo if t in args.only]
  490. todo = [t for t in todo if t not in args.skip]
  491. print("Plan:", todo)
  492. if "seeds" in todo: exp_seeds(device, n_epochs=args.epochs, batch_size=args.batch_size, lr=args.lr)
  493. if "modality" in todo: exp_modality(device, n_epochs=args.epochs, batch_size=args.batch_size, lr=args.lr)
  494. if "conformer" in todo: exp_conformer(device, n_epochs=args.epochs, batch_size=args.batch_size, lr=args.lr)
  495. if "crossing" in todo: exp_crossing(device, n_epochs=args.epochs, batch_size=args.batch_size, lr=args.lr)
  496. if "interp" in todo: exp_interp(device, n_epochs=args.epochs, batch_size=args.batch_size, lr=args.lr)
  497. if "bandwidth" in todo: exp_bandwidth(device, n_epochs=args.epochs, batch_size=args.batch_size, lr=args.lr)
  498. if "earlyhold" in todo: exp_earlyhold(device, n_epochs=args.epochs, batch_size=args.batch_size, lr=args.lr)
  499. summarize()
  500. if __name__ == "__main__":
  501. main()

revision_ablations.py at commit 2caa855, under MIT · at the source

Overview

Authors: Osmar Pinto Neto1,2,3
  1. Biomedical Engineering Postgraduate Program, Anhembi Morumbi University, São José dos Campos, Brazil
  2. Department of Kinesiology, California State University San Marcos (CSUSM), San Marcos, CA, United States
  3. Neurometra, Carlsbad, CA, United States
Journal: Frontiers in neuroscience, volume 20, article 1874302
Dates: received 6 May 2026; accepted 26 June 2026; published online 20 July 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.3389/fnins.2026.1874302 · PMID 42548755 · PMCID PMC13429724 · OpenAlex W7169816556
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: EEG (modality), other (modality), methods / tools (subfield)
Methods: Spectral & time-frequency, Preprocessing, Connectivity, Statistics, Smoothing, state filtering, decompositions, Machine learning, Physiology & signal measures
Keywords: brain-computer interface, deep learning, EEG, EEGNet, EMG, graph attention network, grasp-and-lift, interpretability
Topic: EEG and Brain-Computer Interfaces (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Funding: Anima Institute (12/2025)
Citations: not cited yet (Europe PMC); 31 references in the paper

Abstract

Modern deep learning has broadened the tools available for non-invasive neural decoding, but its advantage over well-engineered classical pipelines remains unclear at clinical neural-engineering sample sizes. We compared four classical decoders, three multilayer perceptron (MLP) variants, and three deep-learning architectures (an EEGNet-style compact convolutional network, a four-layer Transformer encoder trained from scratch, and a graph attention network) on the public WAY-EEG-GAL grasp-and-lift dataset (12 participants, 3,528 trials). Models were evaluated using leave-one-subject-out (LOSO) cross-validation to decode object weight (165, 330, and 660 g) and grasp-surface friction (sandpaper, suede, and silk). After Benjamini–Hochberg false discovery rate (BH-FDR) correction within the primary/robustness family, none of the deep-versus-best-classical comparisons reached significance. The graph attention network led nominally on weight (0.643), and the compact convolutional network (CNN) led nominally on surface (0.565), but neither exceeded the best classical baselines (HGBM = 0.617 for weight; logistic regression = 0.562 for surface). Two-direction bandwidth controls showed that the nominal graph neural network (GNN) advantage on weight reflected access to higher-frequency electromyography (EMG) content rather than a robust architectural gain. The GNN weight signal was concentrated in the first 500 ms of sustained hold (0.639 early vs. 0.540 late, q = 0.007), whereas the compact CNN was comparatively robust to bandwidth and phase. A four-layer Transformer trained from scratch underperformed (q = 0.003 for both tasks), consistent with a parameter–data mismatch at n = 12. Modality ablation showed that both tasks were dominated by peripheral EMG features: electroencephalography (EEG)-only decoding was near chance for weight (0.34–0.37) and surface (0.36–0.38; all q < 0.0015 vs. EMG-only). EMG-only HGBM achieved the highest accuracy in the study (0.712 for weight and 0.584 for surface), and adding EEG channels reduced HGBM weight decoding (q = 0.007) and CNN surface decoding (q = 0.012). In contrast, the graph attention network was robust to EEG-induced fusion dilution (q > 0.5), consistent with attention-based down-weighting of low-information channels. Together, bandwidth, modality, phase, and conditioning controls indicate that, at clinical sample sizes, the critical design choice is not architectural complexity but modality-relevant channel selection and the ability to suppress low-information channels.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repositories

Its files are read in the Code ↔ Paper reader above, with 29 matches between paragraphs and lines of code.

osmar235/eeg-emg-architecture-data-matching

License: MIT
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 2caa855503f5697eb11bdee065cc40b2694df736, 22 June 2026
Languages: Python (14), Jupyter (1)
Size: 45 files, 15 scripts
Software Heritage: not archived
Found in: “Data availability statement”
Holds: README, license file, CITATION.cff, environment (requirements.txt), documentation, 1 notebook
Not found: tests, continuous integration
Tools: NumPy (15 files), pandas (13 files), scikit-learn (13 files), SciPy (8 files), PyTorch (7 files), Matplotlib (6 files), JAX (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
17 files

Zenodo 19966634

License: MIT
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Size: 1 file
Software Heritage: not checked
Found in: “Data availability statement”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (13 files), pandas (12 files), scikit-learn (11 files), SciPy (7 files), PyTorch (5 files), Matplotlib (4 files), JAX (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers (HTTP 200)
  • 27 September 2026: the link answers (HTTP 200)
15 files

The paper's code and data availability statement is in the Data section.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 2 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 28 scripts, each with its path and the digest of its content;
  • 29 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

No dataset and no data link were found in the paper.

Data availability statement

The raw WAY-EEG-GAL dataset analyzed in this study is publicly available from Luciw et al. (2014) and via PhysioNet. All analysis code, derived per-subject feature tensors, model checkpoints, and figure-generation scripts are available on GitHub (https://github.com/osmar235/eeg-emg-architecture-data-matching) and archived on Zenodo (DOI: 10.5281/zenodo.19966634 (https://doi.org/10.5281/zenodo.19966634)).

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, pages, dates, 1 author, 8 keywords, 1 funder, 28 references.

Cite

This paper

Pinto Neto, O. (2026). Architecture-data matching for EEG-EMG decoding: compact deep models match classical spectral decoders on the WAY-EEG-GAL grasp-and-lift dataset. Frontiers in neuroscience, 20, 1874302. https://doi.org/10.3389/fnins.2026.1874302

BibTeX

@article{pintoneto2026architecture,
author = {Pinto Neto, Osmar},
title = {{Architecture-data matching for EEG-EMG decoding: compact deep models match classical spectral decoders on the WAY-EEG-GAL grasp-and-lift dataset}},
journal = {Frontiers in neuroscience},
year = {2026},
month = jul,
volume = {20},
pages = {1874302},
publisher = {Frontiers Media SA},
issn = {1662-4548},
doi = {10.3389/fnins.2026.1874302},
url = {https://doi.org/10.3389/fnins.2026.1874302},
pmid = {42548755},
pmcid = {PMC13429724}
}

RIS

TY - JOUR
AU - Pinto Neto, Osmar
TI - Architecture-data matching for EEG-EMG decoding: compact deep models match classical spectral decoders on the WAY-EEG-GAL grasp-and-lift dataset
T2 - Frontiers in neuroscience
J2 - Front Neurosci
PY - 2026
DA - 2026/07/20
VL - 20
SP - 1874302
SN - 1662-4548
PB - Frontiers Media SA
DO - 10.3389/fnins.2026.1874302
UR - https://doi.org/10.3389/fnins.2026.1874302
LA - en
ER -

CSL-JSON

{
"id": "10.3389/fnins.2026.1874302",
"type": "article-journal",
"title": "Architecture-data matching for EEG-EMG decoding: compact deep models match classical spectral decoders on the WAY-EEG-GAL grasp-and-lift dataset",
"container-title": "Frontiers in neuroscience",
"author": [
{
"family": "Pinto Neto",
"given": "Osmar"
}
],
"container-title-short": "Front Neurosci",
"volume": "20",
"page": "1874302",
"DOI": "10.3389/fnins.2026.1874302",
"PMID": "42548755",
"PMCID": "PMC13429724",
"ISSN": "1662-4548",
"publisher": "Frontiers Media SA",
"URL": "https://doi.org/10.3389/fnins.2026.1874302",
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
20
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1371/journal.pone.0347671 [code]
RMETNet: A cross-subject motor imagery EEG signal classification model based on TSLANet and riemannian geometry features.
Journal: PloS one
In common: PyTorch, scikit-learn, pandas, 3 other tools, methods / tools, EEG, 4 references
[2] doi:10.3390/s26103065 [code]
Subject-Wise Depression Screening from Eight-Channel Resting-State EEG Using Asymmetry-Aware Spectral Features and Connectivity Ablation.
Journal: Sensors (Basel, Switzerland)
In common: PyTorch, scikit-learn, pandas, 3 other tools, other, EEG, 3 references
[3] doi:10.1038/s41598-026-68186-2 [code]
NeuroStream: spectral-spatio-temporal deep learning for visual stimulus classification from EEG.
Journal: Scientific reports
In common: PyTorch, scikit-learn, pandas, 3 other tools, methods / tools, EEG, 3 references
[4] doi:10.1038/s41746-026-02778-0 [code]
Trust-gated synthetic EEG augmentation reduces performance drops when generalizing to new patients.
Journal: NPJ digital medicine
In common: PyTorch, scikit-learn, pandas, 3 other tools, EEG, 3 references
[5] doi:10.3390/bios16080437 [code]
Deep Learning for Ear-EEG-Based Brain-Computer Interface: A Systematic Comparison and Design Insights.
Journal: Biosensors
In common: PyTorch, scikit-learn, pandas, 2 other tools, EEG, 3 references
[6] doi:10.3389/fpsyg.2026.1774068 [code]
Analysis of cognitive mechanisms in phoneme perception and pronunciation errors among Korean language learners.
Journal: Frontiers in psychology
In common: PyTorch, scikit-learn, pandas, 3 other tools, EEG, 2 references
[7] doi:10.1038/s41586-026-10658-6 [code]
An AI system to help scientists write expert-level empirical software.
Journal: Nature
In common: JAX, PyTorch, scikit-learn, 4 other tools, methods / tools
[8] doi:10.1371/journal.pone.0346575 [code]
Statistically valid explainable black-box machine learning: applications in sex classification across species using brain imaging.
Journal: PloS one
In common: JAX, PyTorch, scikit-learn, 4 other tools, methods / tools
[9] doi:10.3390/e28030310
Entropy-Based Dual-Teacher Distillation for Efficient Motor Imagery EEG Classification.
Journal: Entropy (Basel, Switzerland)
In common: methods / tools, EEG, 5 references
[10] doi:10.3390/s26051730 [code]
SFE-GAT: Structure-Feature Evolution Graph Attention Network for Motor Imagery Decoding.
Journal: Sensors (Basel, Switzerland)
In common: PyTorch, SciPy, Matplotlib, 1 other tool, EEG, 3 references

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.