Skip to content

Fix EEGRegressor loss broadcasting for one target per trial - #1197

Merged
bruAristimunha merged 1 commit into
braindecode:masterfrom
Arthur031221:fix-s21
Oct 1, 2026
Merged

bruAristimunha merged 1 commit into
braindecode:masterfrom
Arthur031221:fix-s21

Conversation

@Arthur031221

Copy link
Copy Markdown
Contributor

Summary

Users who train a trial-wise EEGRegressor on a dataset with one target per trial (create_from_X_y, or windows with a metadata target such as age) got a model that learns almost nothing: the (batch, 1) prediction was compared with a (batch,) target, and MSELoss broadcast the pair to (batch, batch), pulling every prediction toward the batch mean target. fit(X, y) with numpy arrays was not affected because it already reshapes y.

Closes #1180. The report and reproduction are by @raghav-rathi. The issue has had no maintainer reply and I found no linked branch or PR for it. If @raghav-rathi already has a fix in progress, I am glad to close this in favour of theirs.

Changes

  • EEGRegressor.get_loss reshapes a 1-D tensor target to (batch, 1) when the prediction has shape (batch, 1), then calls the parent get_loss. Other shapes (cropped outputs, multi-output targets, Mixup tuples) are passed through unchanged.
  • EEGRegressor.fit now returns self, like EEGClassifier.fit, so net.fit(X, y).predict(X) works.
  • docs/whats_new.rst entry under Bugs.

Testing

Reproduction from the issue (ShallowFBCSPNet, 16 trials, seed 0), loss reported by net.get_loss against the mean squared error per trial:

  • before: loss: 1.3908 per-trial mse: 1.8189

  • after: loss: 1.5123 per-trial mse: 1.5123

  • Two regression tests in test/unit_tests/test_eegneuralnet.py: the loss equals the per-trial MSE for dataset targets, and fit returns the estimator. With regressor.py reverted to master both fail; with the fix pytest test/unit_tests/test_eegneuralnet.py gives 55 passed.

  • pytest test/unit_tests/training test/unit_tests/test_util.py test/unit_tests/augmentation/test_base.py: 78 passed, 8 skipped, 1 failed (test_predict_trials, needs moabb, which is not installed in my environment; it fails the same way on master).

  • ruff check on the two changed Python files reports 6 findings, none on the added lines (checked against the diff hunks). pre-commit itself and the full suite were not run.

  • docs/whats_new.rst updated.

Notes for reviewers

The reshape is done in get_loss rather than in the dataset or iterator so that classification, cropped decoding and Mixup targets keep their current paths. Targets that are already (batch, 1) are untouched.

…n self from fit

A (batch,) target against a (batch, 1) prediction was broadcast to
(batch, batch) by the criterion, so every prediction was pulled toward the
batch mean target. Reshape the target in get_loss. EEGRegressor.fit also
now returns self like EEGClassifier.fit.
@codecov

codecov Bot commented Oct 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.75%. Comparing base (0af5317) to head (8d5a666).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1197      +/-   ##
==========================================
+ Coverage   87.73%   87.75%   +0.02%     
==========================================
  Files         149      149              
  Lines       17452    17457       +5     
==========================================
+ Hits        15312    15320       +8     
+ Misses       2140     2137       -3     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@bruAristimunha
bruAristimunha merged commit 83f1645 into braindecode:master Oct 1, 2026
13 of 14 checks passed
@bruAristimunha

Copy link
Copy Markdown
Collaborator

thank you @Arthur031221 🙏🏽

@raghav-rathi

Copy link
Copy Markdown
Contributor

Thanks for picking this up, and sorry I only saw this now! I had a fix in progress too. While testing it I noticed that cropped training has the same problem when y is a 1-D numpy array, so I opened #1198 for that part.

bruAristimunha pushed a commit that referenced this pull request Oct 4, 2026
In cropped mode, EEGRegressor.fit reshapes a 1-D numpy y to (n, 1), but
CroppedLoss squeezed the time-averaged prediction to (batch,), so the
criterion compared every prediction with every target of the batch.
#1197 fixed the trialwise counterpart of this in EEGRegressor.get_loss.

CroppedLoss now keeps the output dimension when the target is a 2-D
tensor. 1-D targets, such as class labels and dataset regression
targets, and Mixup's (y_a, y_b, lam) list are handled as before.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

EEGRegressor computes its loss on a (batch, batch) broadcast when each trial has one target, and the model doesn't learn

3 participants