-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
[ENH] Adjusted SSD algorithm to support non-full rank data #11458
Changes from 2 commits
40049f4
bb13fb7
ffc661f
0d7b0a5
d5ef90f
f030788
64df39b
f977f29
9898e05
ddb025c
59c689d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,15 +1,21 @@ | ||||||
# Author: Denis A. Engemann <denis.engemann@gmail.com> | ||||||
# Victoria Peterson <victoriapeterson09@gmail.com> | ||||||
# Thomas S. Binns <t.s.binns@outlook.com> | ||||||
# License: BSD-3-Clause | ||||||
|
||||||
import numpy as np | ||||||
import scipy as sp | ||||||
|
||||||
from ..filter import filter_data | ||||||
from ..cov import _regularized_covariance | ||||||
from . import TransformerMixin, BaseEstimator | ||||||
from ..time_frequency import psd_array_welch | ||||||
from ..utils import _time_mask, fill_doc, _validate_type, _check_option | ||||||
from ..cov import _regularized_covariance, Covariance | ||||||
from ..defaults import _handle_default | ||||||
from ..filter import filter_data | ||||||
from ..io.pick import _get_channel_types, _picks_to_idx | ||||||
from ..rank import compute_rank | ||||||
from ..time_frequency import psd_array_welch | ||||||
from ..utils import ( | ||||||
fill_doc, logger, _check_option, _time_mask, _validate_type, | ||||||
_verbose_safe_false) | ||||||
|
||||||
|
||||||
@fill_doc | ||||||
|
@@ -157,7 +163,6 @@ def fit(self, X, y=None): | |||||
self : instance of SSD | ||||||
Returns the modified instance. | ||||||
""" | ||||||
from scipy.linalg import eigh | ||||||
self._check_X(X) | ||||||
X_aux = X[..., self.picks_, :] | ||||||
|
||||||
|
@@ -172,17 +177,26 @@ def fit(self, X, y=None): | |||||
|
||||||
cov_signal = _regularized_covariance( | ||||||
X_signal, reg=self.reg, method_params=self.cov_method_params, | ||||||
rank=self.rank, info=self.info) | ||||||
rank="full", info=self.info) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if rank will always be "full" then it should be removed from the Parameters of the SSD class. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here |
||||||
cov_noise = _regularized_covariance( | ||||||
X_noise, reg=self.reg, method_params=self.cov_method_params, | ||||||
rank=self.rank, info=self.info) | ||||||
rank="full", info=self.info) | ||||||
|
||||||
cov_signal_red, cov_noise_red, dim_red = (_dimensionality_reduction( | ||||||
cov_signal, cov_noise, self.info, self.rank)) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is there a need to change the name of the cov matrices? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. Before when I was solving to find the spatial patterns this required the original covariance matrices, however this is no longer the case since I reverted to taking the inverse of the spatial filters. |
||||||
|
||||||
eigvals_, eigvects_ = eigh(cov_signal, cov_noise) | ||||||
eigvals_, eigvects_ = sp.linalg.eigh( | ||||||
cov_signal_red, cov_noise_red) | ||||||
# sort in descending order | ||||||
ix = np.argsort(eigvals_)[::-1] | ||||||
self.eigvals_ = eigvals_[ix] | ||||||
self.filters_ = eigvects_[:, ix] | ||||||
self.patterns_ = np.linalg.pinv(self.filters_) | ||||||
self.eigvals_ = np.real(eigvals_[ix]) | ||||||
filters_ = eigvects_[:, ix] | ||||||
# restores dimensionality | ||||||
self.filters_ = np.matmul(dim_red, filters_) | ||||||
self.patterns_ = np.linalg.solve( | ||||||
np.matmul(self.filters_.T, np.matmul(cov_signal, self.filters_)).T, | ||||||
np.matmul(cov_signal, self.filters_).T) | ||||||
|
||||||
# We assume that ordering by spectral ratio is more important | ||||||
# than the initial ordering. This ordering should be also learned when | ||||||
# fitting. | ||||||
|
@@ -191,6 +205,7 @@ def fit(self, X, y=None): | |||||
if self.sort_by_spectral_ratio: | ||||||
_, sorter_spec = self.get_spectral_ratio(ssd_sources=X_ssd) | ||||||
self.sorter_spec = sorter_spec | ||||||
logger.info("Done.") | ||||||
return self | ||||||
|
||||||
def transform(self, X): | ||||||
|
@@ -290,3 +305,36 @@ def apply(self, X): | |||||
pick_patterns = self.patterns_[self.sorter_spec][:self.n_components].T | ||||||
X = pick_patterns @ X_ssd | ||||||
return X | ||||||
|
||||||
|
||||||
def _dimensionality_reduction(cov_signal, cov_noise, info, rank): | ||||||
"""Perform dimensionality reduction on the covariance matrices.""" | ||||||
n_channels = cov_signal.shape[0] | ||||||
rank_signal = list(compute_rank( | ||||||
Covariance(cov_signal, info.ch_names, list(), list(), 0, | ||||||
verbose=_verbose_safe_false()), | ||||||
rank, _handle_default('scalings_cov_rank', None), info).values())[0] | ||||||
rank_noise = list(compute_rank( | ||||||
Covariance(cov_signal, info.ch_names, list(), list(), 0, | ||||||
verbose=_verbose_safe_false()), | ||||||
rank, _handle_default('scalings_cov_rank', None), info).values())[0] | ||||||
rank = np.min([rank_signal, rank_noise]) # should be identical | ||||||
|
||||||
if rank < n_channels: | ||||||
eigvals, eigvects = sp.linalg.eigh(cov_signal) | ||||||
# sort in descending order | ||||||
ix = np.argsort(eigvals)[::-1] | ||||||
eigvals = eigvals[ix] | ||||||
eigvects = eigvects[:, ix] | ||||||
# compute dimensionality reduction transformation matrix | ||||||
dim_red = np.matmul( | ||||||
eigvects[:, :rank], np.eye(rank) * (eigvals[:rank]**-0.5)) | ||||||
logger.info( | ||||||
"Reducing covariance rank from %i -> %i" % (n_channels, rank,)) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The rank of the covariance is not reduced, that is given by your matrix itself. What it is being done here is more a subspace projection. I would recommend changing the info msg. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure I understand entirely. Applying mne-python/mne/decoding/ssd.py Lines 335 to 336 in ae7ecf8
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. well, you are estimating the rank of the two matrices and taking the smallest value (which BTW, is there any practical case in which the So, personally, I don't see this step as a dimensionality reduction of "the rank" (as you have stated in the log message) but a projection to the rank subspace of the data. In these lines, you are in fact making a projection. so dim_reg is a projection matrix to the rank subspace. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I could not think of anything, but maybe there is some super strange edge case where the ranks are not equal. Since
Okay, I think I understand. So something like "Projecting covariance of X channels to Y rank subspace" would be more appropriate? |
||||||
else: | ||||||
dim_red = np.eye(n_channels) | ||||||
logger.info("Preserving covariance rank (%i)" % (rank,)) | ||||||
|
||||||
cov_signal_red = np.matmul(dim_red.T, np.matmul(cov_signal, dim_red)) | ||||||
cov_noise_red = np.matmul(dim_red.T, np.matmul(cov_noise, dim_red)) | ||||||
return cov_signal_red, cov_noise_red, dim_red | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. consider changing the name of the projection matrix from dim_red to proj_rank ? or something more accurate. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, it is a bit obtuse; perhaps I could change this as well. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and then use linalg.XXX below
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apologies, fixed now!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @agramfort, it seems
from scipy import linalg
causestest_import_nesting.py
to fail. How should I handle this?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nest the import inside the function that needs
linalg
like