Skip to content

ENH: Add GED transformer #13259

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 74 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
632c819
assert_allclose for base ged for csp, spoc, ssd and xdawn
Genuster May 22, 2025
4f8b5fa
Merge remote-tracking branch 'upstream/main' into base-GED
Genuster May 22, 2025
7c072d1
update _epoch_cov logging following merge
Genuster May 22, 2025
211d23f
add a few preliminary docstrings
Genuster May 22, 2025
a701e42
Merge remote-tracking branch 'upstream/main' into base-GED
Genuster May 22, 2025
0d58c8d
bump rtol/atol for spoc
Genuster May 22, 2025
2a1c5cb
Add big sklearn compliance test
Genuster May 29, 2025
3e9c32c
Merge remote-tracking branch 'upstream/main' into base-GED
Genuster May 29, 2025
6e8b3aa
add __sklearn_tags__ to vulture's whitelist
Genuster Jun 2, 2025
b2e24ea
calm vulture down per attribute
Genuster Jun 2, 2025
fbd585e
put the TransformerMixin back
Genuster Jun 2, 2025
a9d5390
Merge remote-tracking branch 'upstream/main' into base-GED
Genuster Jun 2, 2025
d142bd0
fix validation of covariances
Genuster Jun 2, 2025
9329494
Merge remote-tracking branch 'upstream/main' into base-GED
Genuster Jun 3, 2025
6796366
add gedtranformer tests with audvis dataset
Genuster Jun 3, 2025
7a291b1
fixes following Eric's comments
Genuster Jun 4, 2025
9b34bd3
Merge remote-tracking branch 'upstream/main' into base-GED
Genuster Jun 4, 2025
7c867ec
document shapes
Genuster Jun 4, 2025
e1e8d6d
another small test for GEDtransformer
Genuster Jun 6, 2025
5edc6fa
change name of restricting map to restricting matrix
Genuster Jun 6, 2025
89fb141
a few more ged tests
Genuster Jun 6, 2025
3986c99
fix multiplication order in original SSD
Genuster Jun 6, 2025
11b038f
add assert_allclose to xdawn and csp transform methods.
Genuster Jun 6, 2025
25e1ae3
more ged tests
Genuster Jun 6, 2025
6bbc459
clean up _xdawn_estimate
Genuster Jun 10, 2025
99d297e
Merge remote-tracking branch 'upstream/main' into base-GED
Genuster Jun 10, 2025
029691b
add _validate_params for _XdawnTransformer
Genuster Jun 10, 2025
f38ce7d
review suggestions
Genuster Jun 13, 2025
11c31f7
address Eric's suggestions
Genuster Jun 13, 2025
85fb50f
add default no op for mod_ged_callable
Genuster Jun 13, 2025
3c7df08
replace mod_params with partial as well
Genuster Jun 13, 2025
9c7c711
add ged entry in the implementation details
Genuster Jun 13, 2025
95544c5
add feature to perform GED in the principal subspace for xdawn
Genuster Jun 13, 2025
8755089
add option for CSP to select restr_type and provide info
Genuster Jun 13, 2025
87a2466
add restr_type for SCoP and SSD
Genuster Jun 13, 2025
969a73e
fix SSD's filters_ shape inconsistency
Genuster Jun 13, 2025
5266372
use mne's pinv in SSD and Xdawn instead of np.linalg.pinv
Genuster Jun 13, 2025
726c500
move mne.preprocessing._XdawnTransformer to decoding and make it public
Genuster Jun 13, 2025
8e8bf3f
fix docstring
Genuster Jun 13, 2025
a374546
fix some terminological imprecisions in the implementation details
Genuster Jun 13, 2025
226abf4
add parameter validation for gedtransformer
Genuster Jun 15, 2025
3da3266
slightly improve validation in csp and ssd
Genuster Jun 15, 2025
4f5d436
rename xdawntranformer's method_params to cov_method_params for consi…
Genuster Jun 15, 2025
5e12465
add picks test for ssd
Genuster Jun 20, 2025
2bfc931
make ssd store ordered filters instead of sorting in transform
Genuster Jun 23, 2025
3a0dd1a
add expected failure for sklearn compliance test
Genuster Jun 25, 2025
19d01bd
Merge remote-tracking branch 'upstream/main' into base-GED
Genuster Jun 26, 2025
29da3ff
better solution for the previous fix
Genuster Jun 26, 2025
8d1e656
add temporary xfail for windows pip CIs
Genuster Jun 27, 2025
ffac4fd
Merge remote-tracking branch 'upstream/main' into base-GED
Genuster Jun 27, 2025
3761ff4
another try
Genuster Jun 27, 2025
8903664
and another
Genuster Jun 27, 2025
60d7360
add sorter return for _mod_ged functions
Genuster Jun 27, 2025
f8b8d6b
(1) clean up csp and remove asserts
Genuster Jun 27, 2025
10224fc
(2) clean up spoc and remove asserts
Genuster Jun 27, 2025
ac077f7
(3) clean up xdawn, remove asserts and make it store all filters and …
Genuster Jun 27, 2025
e23448c
(4) clean up ssd and remove asserts
Genuster Jun 27, 2025
5c612cb
more tests
Genuster Jul 4, 2025
17fae36
Merge remote-tracking branch 'upstream/main' into base-GED
Genuster Jul 4, 2025
3c782b3
replace ssd's old whitening with compute_whitener
Genuster Jul 4, 2025
7522910
make XdawnTransformer properly public
Genuster Jul 4, 2025
a57934b
add changelog entry
Genuster Jul 4, 2025
7951a43
fix xdawntransformer docstring
Genuster Jul 4, 2025
b3a378d
more docstring adventures
Genuster Jul 5, 2025
7b9ad11
Merge remote-tracking branch 'upstream/main' into base-GED
Genuster Jul 5, 2025
268f54f
Merge remote-tracking branch 'upstream/main' into base-GED
Genuster Jul 6, 2025
10afd78
fix docdict order
Genuster Jul 6, 2025
5049e8c
Merge remote-tracking branch 'upstream/main' into base-GED
Genuster Jul 11, 2025
9ec6835
make all init arguments have default in ged transformer
Genuster Jul 11, 2025
99f6e9c
Merge branch 'main' into base-GED
larsoner Jul 11, 2025
9364b04
Merge branch 'main' into base-GED
larsoner Jul 11, 2025
2b690ea
temporarily skip the problematic test
Genuster Jul 12, 2025
0117163
unskip the test
Genuster Jul 12, 2025
b08cf81
fix unskip
Genuster Jul 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions doc/_includes/ged.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
:orphan:

Generalized eigendecomposition in decoding
==========================================

.. NOTE: part of this file is included in doc/overview/implementation.rst.
Changes here are reflected there. If you want to link to this content, link
to :ref:`ged` to link to that section of the implementation.rst page.
The next line is a target for :start-after: so we can omit the title from
the include:
ged-begin-content

This section describes the mathematical formulation and application of
Generalized Eigendecomposition (GED), often used in spatial filtering
and source separation algorithms, such as :class:`mne.decoding.CSP`,
:class:`mne.decoding.SPoC`, :class:`mne.decoding.SSD` and
:class:`mne.preprocessing.Xdawn`.

The core principle of GED is to find a set of channel weights (spatial filter)
that maximizes the ratio of signal power between two data features.
These features are defined by the researcher and are represented by two covariance matrices:
a "signal" matrix :math:`S` and a "reference" matrix :math:`R`.
For example, :math:`S` could be the covariance of data from a task time interval,
and :math:`S` could be the covariance from a baseline time interval. For more details see :footcite:`Cohen2022`.

Algebraic formulation of GED
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

A few definitions first:
Let :math:`n \in \mathbb{N}^+` be a number of channels.
Let :math:`\text{Symm}_n(\mathbb{R}) \subset M_n(\mathbb{R})` be a vector space of real symmetric matrices.
Let :math:`S^n_+, S^n_{++} \subset \text{Symm}_n(\mathbb{R})` be sets of real positive semidefinite and positive definite matrices, respectively.
Let :math:`S, R \in S^n_+` be covariance matrices estimated from electrophysiological data :math:`X_S \in M_{n \times t_S}(\mathbb{R})` and :math:`X_R \in M_{n \times t_R}(\mathbb{R})`.

GED (or simultaneous diagonalization by congruence) of :math:`S` and :math:`R`
is possible when :math:`R` is full rank (and thus :math:`R \in S^n_{++}`):

.. math::

SW = RWD,

where :math:`W \in M_n(\mathbb{R})` is an invertible matrix of eigenvectors
of :math:`(S, R)` and :math:`D` is a diagonal matrix of eigenvalues :math:`\lambda_i`.

Each eigenvector :math:`\mathbf{w} \in W` is a spatial filter that solves
an optimization problem of the form:

.. math::

\operatorname{argmax}_{\mathbf{w}} \frac{\mathbf{w}^t S \mathbf{w}}{\mathbf{w}^t R \mathbf{w}}

That is, using spatial filters :math:`W` on time-series :math:`X \in M_{n \times t}(\mathbb{R})`:

.. math::

\mathbf{A} = W^t X,

results in "activation" time-series :math:`A` of the estimated "sources",
such that the ratio of their variances,
:math:`\frac{\text{Var}(\mathbf{w}^T X_S)}{\text{Var}(\mathbf{w}^T X_R)} = \frac{\mathbf{w}^T S \mathbf{w}}{\mathbf{w}^T R \mathbf{w}}`,
is sequentially maximized spatial filters :math:`\mathbf{w}_i`, sorted according to :math:`\lambda_i`.

GED in the principal subspace
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Unfortunately, :math:`R` might not be full rank depending on the data :math:`X_R` (for example due to average reference, removed PCA/ICA components, etc.).
In such cases, GED can be performed on :math:`S` and :math:`R` in the principal subspace :math:`Q = \operatorname{Im}(C_{ref}) \subset \mathbb{R}^n` of some reference
covariance :math:`C_{ref}` (in Common Spatial Pattern (CSP) algorithm, for example, :math:`C_{ref}=\frac{1}{2}(S+R)` and GED is performed on S and R'=S+R).

More formally:
Let :math:`r \leq n` be a rank of :math:`C \in S^n_+`.
Let :math:`Q=\operatorname{Im}(C_{ref})` be a principal subspace of :math:`C_{ref}`.
Let :math:`P \in M_{n \times r}(\mathbb{R})` be an isometry formed by orthonormal basis of :math:`Q`.
Let :math:`f:S^n_+ \to S^r_+`, :math:`A \mapsto P^t A P` be a "restricting" map, that restricts quadratic form
:math:`q_A:\mathbb{R}^n \to \mathbb{R}` to :math:`q_{A|_Q}:\mathbb{R}^n \to \mathbb{R}` (in practical terms, :math:`q_A` maps
spatial filters to variance of the spatially filtered data :math:`X_A`).

Then, the GED of :math:`S` and :math:`R` in the principal subspace :math:`Q` of :math:`C_{ref}` is performed as follows:

1. :math:`S` and :math:`R` are transformed to :math:`S_Q = f(S) = P^t S P` and :math:`R_Q = f(R) = P^t R P`,
such that :math:`S_Q` and :math:`R_Q` are matrix representations of restricted :math:`q_{S|_Q}` and :math:`q_{R|_Q}`.
2. GED is performed on :math:`S_Q` and :math:`R_Q`: :math:`S_Q W_Q = R_Q W_Q D`.
3. Eigenvectors :math:`W_Q` of :math:`(S_Q, R_Q)` are transformed back to :math:`\mathbb{R}^n`
by :math:`W = P W_Q \in \mathbb{R}^{n \times r}` to obtain :math:`r` spatial filters.

Note that the solution to the original optimization problem is preserved:

.. math::

\frac{\mathbf{w_Q}^t S_Q \mathbf{w_Q}}{\mathbf{w_Q}^t R_Q \mathbf{w_Q}}= \frac{\mathbf{w_Q}^t (P^t S P) \mathbf{w_Q}}{\mathbf{w_Q}^t (P^t R P)
\mathbf{w_Q}} = \frac{\mathbf{w}^t S \mathbf{w}}{\mathbf{w}^t R \mathbf{w}} = \lambda


In addition to restriction, :math:`q_S` and :math:`q_R` can be rescaled based on the whitened :math:`C_{ref}`.
In this case the whitening map :math:`f_{wh}:S^n_+ \to S^r_+`,
:math:`A \mapsto P_{wh}^t A P_{wh}` transforms :math:`A` into matrix representation of :math:`q_{A|Q}` rescaled according to :math:`\Lambda^{-1/2}`,
where :math:`\Lambda` is a diagonal matrix of eigenvalues of :math:`C_{ref}` and so :math:`P_{wh} = P \Lambda^{-1/2}`.

In MNE-Python, the matrix :math:`P` of the restricting map can be obtained using
::

_, ref_evecs, mask = mne.cov._smart_eigh(C_ref, ..., proj_subspace=True, ...)
restr_mat = ref_evecs[mask]

while :math:`P_{wh}` using:
::

restr_mat = compute_whitener(C_ref, ..., pca=True, ...)
1 change: 1 addition & 0 deletions doc/api/decoding.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Decoding
GeneralizingEstimator
SPoC
SSD
XdawnTransformer

Functions that assist with decoding and model fitting:

Expand Down
3 changes: 3 additions & 0 deletions doc/changes/devel/13259.newfeature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Implement GEDTransformer superclass that generalizes
:class:`mne.decoding.CSP`, :class:`mne.decoding.SPoC`, :class:`mne.decoding.XdawnTransformer`,
:class:`mne.decoding.SSD` and fix related bugs and inconsistencies, by `Gennadiy Belonosov`_.
8 changes: 8 additions & 0 deletions doc/documentation/implementation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,14 @@ Morphing and averaging source estimates
:start-after: morph-begin-content


.. _ged:

Generalized eigendecomposition in decoding
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. include:: ../_includes/ged.rst
:start-after: ged-begin-content

References
^^^^^^^^^^
.. footbibliography::
12 changes: 12 additions & 0 deletions doc/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,18 @@ @article{Cohen2019
year = {2019}
}

@article{Cohen2022,
author = {Cohen, Michael X},
doi = {10.1016/j.neuroimage.2021.118809},
journal = {NeuroImage},
pages = {118809},
title = {A tutorial on generalized eigendecomposition for denoising, contrast enhancement, and dimension reduction in multichannel electrophysiology},
volume = {247},
year = {2022},
issn = {1053-8119},

}

@article{CohenHosaka1976,
author = {Cohen, David and Hosaka, Hidehiro},
doi = {10.1016/S0022-0736(76)80041-6},
Expand Down
2 changes: 2 additions & 0 deletions mne/decoding/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ __all__ = [
"TransformerMixin",
"UnsupervisedSpatialFilter",
"Vectorizer",
"XdawnTransformer",
"compute_ems",
"cross_val_multiscore",
"get_coef",
Expand All @@ -43,3 +44,4 @@ from .transformer import (
UnsupervisedSpatialFilter,
Vectorizer,
)
from .xdawn import XdawnTransformer
Loading
Loading