Prepared as the reference knowledge-base for writing an A-grade minor project report.
| Item | Detail |
|---|---|
| Paper | Federated Motor Imagery Classification for Privacy-Preserving Brain-Computer Interfaces — Tianwang Jia et al. |
| Domain | Federated Learning (FL) × Brain-Computer Interfaces (BCI) |
| Core Idea | Train an EEG motor-imagery classifier across multiple subjects without sharing raw brain signals (privacy-preserving). |
| Dataset | BNCI2014001 (BCI Competition IV-2a) — 9 subjects, 4-class motor imagery, 22 EEG channels, 250 Hz |
| Model | EEGNet (compact CNN for EEG) |
| FL Strategies | FedAvg (baseline), FedProx (implicitly via same path), FedBS (novel contribution — SAM + BN-skip) |
| Evaluation | Leave-One-Subject-Out (LOSO): each of the 9 subjects takes a turn as the "server" (test), remaining 8 are "clients" (train) |
| Tech Stack | Python, PyTorch, CUDA, scipy, mne, moabb, scikit-learn, Docker |
Purpose: Defines the neural network used by both server and every client.
Architecture (3 sequential blocks + classifier):
| Block | Layers | Purpose |
|---|---|---|
| Block 1 — Temporal Filter | ZeroPad2d → Conv2d(1, F1=8, kernel=(1, 125)) → BatchNorm2d | Learns frequency-domain filters across the time axis. Kernel size = sample_rate/2 = 125 captures ~2 Hz resolution. |
| Block 2 — Spatial Filter | Conv2d(F1, D×F1=16, kernel=(22, 1), groups=F1) → BN → ELU → AvgPool(1,4) → Dropout | Depthwise convolution across all 22 channels. Each temporal filter gets D=2 spatial maps. |
| Block 3 — Separable Conv | ZeroPad2d → DepthwiseConv(16, 16, kernel=(1, 31)) → PointwiseConv(16, F2=16, kernel=(1,1)) → BN → ELU → AvgPool(1,8) → Dropout | Separable convolution for efficient feature extraction with fewer parameters. |
| Classifier | Linear(F2 × (time//32), 4) | Flattens and classifies into 4 motor imagery classes: left hand, right hand, feet, tongue. |
Key design choices:
bn_trackparameter: WhenFedBS=True, BatchNorm tracking is disabled (track_running_stats=False). This is critical — FedBS deliberately forces each client to use only mini-batch statistics, preventing running-mean/running-var from being influenced by the global model's aggregated stats.MaxNormConstraint(): Clips spatial filter weights to L2-norm ≤ 1.0 and classifier weights to ≤ 0.25. This is an EEGNet-specific regularization from the original paper.- Input shape:
(batch, 1, 22, 1001)→ channels=22 EEG electrodes, time=1001 samples (≈4s at 250 Hz).
Purpose: The SECRET WEAPON of FedBS. Replaces standard SGD with a two-step optimizer
that finds FLAT minima instead of sharp ones.
Algorithm (2-step process each mini-batch):
Step 1 — ASCENT: Perturb weights to find the worst-case loss in a ρ-neighborhood
ε = ρ × (gradient / ||gradient||₂) # Normalized gradient scaled by rho
θ̃ = θ + ε # Temporarily move TO the sharpest point
Step 2 — DESCENT: Compute gradient at the perturbed point, then apply SGD at the ORIGINAL point
θ̃ = θ̃ - ε # Undo the perturbation (back to θ)
θ = θ - lr × ∇L(θ̃) # Standard SGD step using the "worst-case" gradient
Why this matters for FL:
- In FL, each client's local data is small and non-IID. Standard SGD tends to find sharp minima that only work well for that specific client's data.
- SAM forces the optimizer to find flat minima — solutions that are robust to perturbations. These generalize better across all clients when aggregated at the server.
- The
rhohyperparameter (default 0.1) controls the perturbation radius. Larger ρ = flatter minima but slower convergence.
Implementation details:
- Uses
defaultdict(dict)to cache epsilon tensors per parameter (avoids repeated memory allocation). @torch.no_grad()decorator on both steps since we're manually manipulating weights.- Gradient norm has
+ 1e-16for numerical stability.
Purpose: Each subject (except the test subject) becomes a Client that trains a local copy
of the global model on its own EEG data.
__init__ — Client Initialization:
- Creates a local copy of EEGNet on the available device (CUDA/CPU).
- Applies Xavier initialization via
weights_init(). - Creates DataLoaders for train and eval datasets.
- If
fedbs=True: BatchNorm tracking is disabled (bn_track=False).
local_train(global_model) — The core FL training step:
# Step 1: Sync with global model
for name, param in global_model.state_dict().items():
if self.args.fedbs and 'bn' in name:
continue # ← FedBS KEY INNOVATION: Skip BatchNorm layers!
self.local_model.state_dict()[name].copy_(param.clone())Important
FedBS skips syncing BatchNorm parameters. In standard FedAvg, the server sends ALL parameters (including BN running_mean/running_var) to clients. FedBS deliberately lets each client maintain its own BatchNorm statistics, because EEG data distributions vary dramatically across subjects (brain signals are highly individual).
# Step 2: Local training loop
for epoch in range(local_epochs):
for X, y in train_dataloader:
if fedbs:
# Two-pass SAM optimization
loss1.backward() # Compute gradient
minimizer.ascent_step() # Perturb weights to worst-case
loss2.backward() # Compute gradient at perturbed point
minimizer.descent_step()# Step back and apply SGD
else:
# Standard FedAvg SGD
loss.backward()
optimizer.step()# Step 3: Compute weight diff (Δw = w_local - w_global) and return to server
weight_diff[name] = local_model[name] - global_model[name]local_eval(global_model) — Evaluation:
- Fully syncs local model with global model (including BN for eval).
- Runs forward pass on eval data, computes loss and accuracy.
- Labels are one-hot encoded → uses
y.max(1)[1]to get class index.
Purpose: Holds the global model, aggregates client updates, and tests the final model.
__init__:
- Creates the global EEGNet model (same architecture as clients).
- Creates a DataLoader for the test subject's data (batch_size=8, no shuffle).
model_aggregate(client_weight_dict, avg_weight_dict, candidates_id_list) — FedAvg Aggregation:
for each candidate client:
for each layer:
update = client_weight_diff × avg_weight # Weighted contribution
global_model[layer] += update # In-place additionavg_weight_dict: In this implementation, all clients get equal weight (1/N).- Handles type mismatch: BatchNorm's
num_batches_trackedisint64, but weight diffs arefloat32. The code explicitly casts to prevent PyTorch crashes.
model_test() — Final evaluation on held-out subject:
- Standard eval loop, returns test loss and accuracy.
- This accuracy is what goes into the LOSO results table.
Purpose: Loads .mat files containing preprocessed EEG motor imagery data and applies
transforms.
Data source: BNCI2014001 (BCI Competition IV Dataset 2a)
- 9 subjects, each performing 4 motor imagery tasks
- 22 EEG channels, sampled at 250 Hz, ~4s trials → 1001 time points
- Data stored as
.matfiles (MATLAB format):{subject_id}.matcontainingX(trials × channels × time) andy(labels)
Transforms (applied in order):
| Transform | Description |
|---|---|
EA (Euclidean Alignment) |
Aligns EEG covariance matrices across sessions/subjects using R⁻⁰·⁵ × X. This is a domain adaptation technique that reduces inter-subject variability. Uses scipy.linalg.fractional_matrix_power. |
ZScoreNorm |
Channel-wise z-score normalization (defined but not used by default). |
ArrayToTensor |
Converts numpy arrays to torch.FloatTensor. |
MIDataset class:
- Loads data for specified subject IDs from
.matfiles. - One-hot encodes labels using
pd.get_dummies(). - Supports
BNCI2015001by trimming to 400 trials. - Reshapes data to
(N, 1, C, T)for Conv2d input. - Splits into train/val (80/20) via
train_test_split. - Mode selector:
'train','val', or'all'(shuffled full dataset).
weights_init(m):
- Conv2d: Xavier Normal initialization
- BatchNorm2d: Normal(mean=1.0, std=0.02) for weight, Constant(0.0) for bias
- Linear: Xavier Normal initialization
EarlyStopping:
- Monitors validation loss; stops training if no improvement for
patienceepochs. - When
early=False(default): Simply saves the model at the last epoch (no early stopping logic). - When
early=True: Tracks best score, increments counter on no improvement, triggersearly_stop=Trueafter patience is exhausted. - Saves the best model checkpoint to disk via
torch.save().
Purpose: The entry point. Parses arguments, runs the complete LOSO federated training loop,
and outputs the results table.
Complete Data Flow:
graph TD
A["train.py: Parse CLI Arguments"] --> B["LOSO Loop: For each subject i in 1..9"]
B --> C["Subject i = Server (Test Subject)"]
B --> D["Remaining 8 subjects = Clients (Training)"]
C --> E["Server: Create test DataLoader"]
D --> F["Clients: Each creates local train/eval DataLoaders"]
F --> G["Global Epoch Loop (50-200 rounds)"]
G --> H["Random Sample 4 clients"]
H --> I["Each client: local_train(global_model)"]
I --> J["Each client: Sync → Train → Compute Δw"]
J --> K["Server: Aggregate Δw (weighted average)"]
K --> L["EarlyStopping check"]
L --> G
L --> M["Load best model → Test on Server subject"]
M --> N["Record accuracy for subject i"]
N --> B
B --> O["Output: 9-column accuracy table + Average"]
CLI Arguments (Key ones):
| Argument | Default | Purpose |
|---|---|---|
--model |
eegnet |
Model architecture |
--global_epochs |
200 | Number of FL communication rounds |
--local_epochs |
2 | Training epochs per client per round |
--sample_num |
4 | Clients randomly sampled per round |
--batch_size |
32 | Mini-batch size |
--lr |
0.005 | Learning rate |
--fedbs |
False | Enable FedBS (SAM + BN-skip) |
--rho |
0.1 | SAM perturbation radius |
--data_path |
./data/BNCI2014001 |
Path to .mat data files |
--sub_id |
1,2,3,4,5,6,7,8,9 |
Subject IDs to include |
LOSO Loop (Leave-One-Subject-Out):
For each subject i ∈ {1,2,...,9}:
Server = Subject i (test data only)
Clients = {1,...,9} \ {i} (training data)
Run federated training → Record test accuracy for subject i
Final output: Table of 9 accuracies + average
1. Server initializes global model
2. For each round:
a. Server sends global model to sampled clients
b. Each client trains locally with standard SGD
c. Each client sends weight difference (Δw) back
d. Server averages: w_global += (1/N) × Σ Δw_i
- In codebase:
--fedbs False(default). Uses plainSGD(lr=0.005, momentum=0.9, weight_decay=1e-4). - Weakness for BCI: Assumes IID data. EEG signals are highly non-IID across subjects (everyone's brain is different).
Same as FedAvg, but adds a proximal term:
L_total = L_task + (μ/2) × ||w_local - w_global||²
Note
Important implementation note: Looking at the codebase, FedProx is mentioned in the README (--fedprox True) but there is no --fedprox argument defined in argparse and no proximal term implemented in client.py. The README describes it as a concept, but the current code does not actually implement FedProx differently from FedAvg. For the report, you should either implement it or describe FedAvg results as the non-FedBS baseline.
FedBS = Federated Batch-norm Sharpness — two synergistic innovations:
Innovation 1: BatchNorm Skip (the "B")
- During global → local sync, skip BatchNorm parameters (
running_mean,running_var,weight,bias). - Each client maintains its own BN statistics that adapt to its subject-specific EEG distribution.
- During aggregation, BN parameters are still included in the weight diff, so the server model gets a form of averaged BN stats.
track_running_stats=Falsemeans BN always uses mini-batch statistics, never accumulated running stats.
Innovation 2: SAM Optimizer (the "S")
- Replaces SGD with Sharpness-Aware Minimization.
- Forces local models to converge to flat minima that generalize better across subjects.
- Two forward passes per batch (compute cost ~2× FedAvg per local step).
FedBS Pseudocode:
1. Server initializes global model with bn_track=False
2. For each round:
a. Server sends model (EXCEPT BN params) to sampled clients
b. Each client trains with SAM optimizer:
- Forward pass → compute loss → backward pass
- Ascent step: perturb weights toward worst-case
- Forward pass again → backward pass
- Descent step: undo perturbation, apply SGD
c. Client sends full weight diff (including BN) back
d. Server aggregates with weighted average
The Gap: Traditional centralized BCI training requires pooling raw EEG data from all subjects → privacy violation. FL solves data sharing, but standard FL (FedAvg) fails for BCI because:
- EEG data is extremely non-IID (each brain is unique)
- Sharp local minima don't generalize across subjects
- Shared BatchNorm statistics corrupt individual subject distributions
Quantifiable Objectives:
- Achieve competitive 4-class motor imagery classification accuracy (≥70% average across 9 subjects)
- Maintain data privacy (zero raw data sharing between subjects)
- Outperform FedAvg baseline by leveraging subject-specific BN and SAM
Diagrams needed:
- System Architecture: Server ↔ Client communication with LOSO rotation
- EEGNet Block Diagram: Input(1×22×1001) → Block1 → Block2 → Block3 → FC → 4 classes
- FedBS vs FedAvg Data Flow: Highlight BN-skip and SAM differences
- SAM Optimization Landscape: Show flat vs. sharp minima conceptually
Pseudocode needed for:
- LOSO cross-validation loop
- FedAvg aggregation
- FedBS local training (SAM + BN skip)
- EEGNet forward pass
- Euclidean Alignment preprocessing
Environment specifics:
| Component | Version |
|---|---|
| Base Image | pytorch/pytorch:1.11.0-cuda11.3-cudnn8-runtime |
| Python | 3.x (from Docker image) |
| PyTorch | 1.11.0 |
| CUDA | 11.3 |
| Key Libraries | scipy, scikit-learn, mne, moabb, pandas, matplotlib |
| Hardware | NVIDIA GPU with CUDA support (specify your actual GPU) |
Experiments to run and compare:
| Experiment | Command |
|---|---|
| FedAvg (baseline) | python src/train.py --model eegnet --global_epochs 50 --local_epochs 2 --data_path "..." |
| FedBS (proposed) | python src/train.py --model eegnet --global_epochs 50 --local_epochs 2 --data_path "..." --fedbs True --rho 0.1 |
Metrics to report:
- Per-subject test accuracy (9 subjects) — bar chart
- Average accuracy comparison — table
- Training loss convergence curves — line plot (needs TensorBoard or logging modification)
- Computational overhead: FedBS does 2× forward passes per batch (SAM)
Existing training runs found:
20260423_165729/— Complete run (9 subject models saved, ~22KB each)- Multiple earlier partial runs available
-
Brain signals are the ultimate non-IID data. No two brains produce the same EEG patterns for the same motor imagery task. FedAvg's assumption of homogeneous data distributions is fundamentally violated.
-
BatchNorm is the silent killer in FL for BCI. Standard BN accumulates running statistics. When the server averages BN stats from 8 subjects with wildly different EEG distributions, the resulting "average brain" statistics are meaningless. FedBS solves this by letting each client keep its own BN stats.
-
Sharp minima = memorization of one subject. With only ~288 trials per subject, local SGD quickly overfits to that subject's specific EEG patterns. SAM's flat minima are inherently more transferable across subjects.
-
The two innovations are synergistic. BN-skip prevents distribution corruption at the normalization level. SAM prevents overfitting at the optimization level. Together, they produce a global model that adapts well to any subject's data during testing.
- 2× computational cost per local training step (SAM requires two forward+backward passes).
- Hyperparameter sensitivity:
rhomust be tuned. Too large → underfitting; too small → no benefit. - Small dataset: Only 9 subjects. Statistical significance is limited.
- No differential privacy guarantees: FL prevents raw data sharing, but weight updates can still leak information via gradient inversion attacks.
- Equal client weighting: The
avg_weight_dictgives all clients1/Nweight regardless of data quality or quantity.
Raw .mat files (per subject)
↓
MIDataset: Load X(trials×22×1001), y(labels)
↓
Euclidean Alignment: R⁻⁰·⁵ × X (reduces cross-subject variance)
↓
ArrayToTensor: numpy → torch.FloatTensor
↓
Reshape: (N, 1, 22, 1001) for Conv2d
↓
One-hot encode labels via pd.get_dummies
↓
DataLoader(batch_size=32, shuffle=True for train)
↓
EEGNet: Block1(temporal) → Block2(spatial) → Block3(separable) → FC(4 classes)
↓
CrossEntropyLoss → SGD or SAM optimization
↓
Weight diff (Δw) → Server aggregation → Updated global model
↓
Repeat for 50 global epochs
↓
Test on held-out subject → Record accuracy