Source code for obob_mne.decoding.temporal

# -*- coding: UTF-8 -*-
# Copyright (c) 2018, Thomas Hartmann
#
# This file is part of the obob_mne Project, see:
# https://gitlab.com/obob/obob_mne
#
#    obob_mne is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    obob_mne is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with obob_subjectdb. If not, see <http://www.gnu.org/licenses/>.

import mne
import copy
import numpy
import logging

from .helpers import _get_Y_from_metadata


[docs]class TemporalArray(mne.EvokedArray): """Base class for temporal decoding. Parameters ---------- raw_scores : :class:`numpy.ndarray` shape (n_times) or (n_folds, n_times) The scores of the classification weights : :class:`numpy.ndarray` shape (n_channels, n_times) Classifier weights n_classes : int The number of classes info : dict Info dict tmin : float Time of the first sample in seconds scoring_name : str Name of the scoring function (i.e. Accuracy...) c_factors_training : str Name of the factors over which was collapsed in the training set nave : int, optional Number of epochs in the training set. nave_testing : int or None Number of epochs in the testing set. If ``None``, it is copied from nave. c_factors_testing : None or str Name of the factors over which was collapsed in the testing set. If ``None``, it is copied from c_factors_training. """
[docs] def __init__(self, raw_scores, weights, n_classes, info, tmin, scoring_name, c_factors_training, nave=1, nave_testing=None, c_factors_testing=None): self._scores_raw = raw_scores self._nclasses = n_classes self._scoring_name = scoring_name self._c_factors_training = c_factors_training if c_factors_testing is None: self._c_factors_testing = self._c_factors_training else: self._c_factors_testing = c_factors_testing if nave_testing is None: nave_testing = nave self.nave_testing = nave_testing self.nave = nave super(TemporalArray, self).__init__(weights, info, tmin, nave=nave)
@property def scores(self): """:class:`numpy.ndarray`: The scores of the classification""" if self._scores_raw.ndim == 1: return self._scores_raw else: return numpy.average(self._scores_raw, axis=0) @property def nclasses(self): return self._nclasses @property def chance_level(self): """float: The chance level of the classifier.""" return 1.0 / self._nclasses def plot_scores(self, axes=None, show=True): """Plot the scores as a line plot. Parameters ---------- axes : :class:`matplotlib.axes.Axes` or None, optional The axes where to draw the plot. If ``None``, a new figure is created. show : bool, optional True to actually show the plot. """ import matplotlib.pyplot as plt if not isinstance(axes, plt.Axes): fig, axes = plt.subplots(1, 1) else: fig = axes.get_figure() axes.plot(self.times, self.scores, label='score') axes.axhline(self.chance_level, color='k', linestyle='--', label='chance') axes.set_xlabel('Times') axes.set_ylabel(self._scoring_name) axes.legend() axes.axvline(.0, color='k', linestyle='-') axes.set_title('Sensor space decoding') if show: plt.show() return fig
[docs]class Temporal(TemporalArray): """Apply a decoding pipeline to every sample. Use this class to perform Temporal decoding. This means that a classifier is trained on every sample and tested on that very sample. The minimum requirements are the data as :class:`mne.Epochs` and the pipeline as :class:`sklearn.pipeline.Pipeline`, which is most commonly generated using :func:`sklearn.pipeline.make_pipeline`. This class treats every individual ``event_id`` as an individual target. If you want to combine multiple ``events_id`` into one target, you can use :func:`epochs.collapse_conditions`. If you want the training and testing data to be different, you can supply the training data to ``epochs`` and the testing data to ``epochs_test``. Cross validation will be run only if training and testing data are equal (i.e., when ``epochs_test=None``). Parameters ---------- epochs : :class:`mne.Epochs` The epochs to apply the classifier pipeline to. pipeline : :class:`sklearn.pipeline.Pipeline` The classifier pipeline to use. Most likely created with :func:`sklearn.pipeline.make_pipeline` epochs_test : :class:`mne.Epochs` or None, optional If set, the classifier gets tested on this data. The event_ids must be equal. No crossvalidation will be run in this case. cv : int, optional The amount of folds for cross validation scoring : str or callable, optional The scoring function to use. n_jobs : int, optional Number of CPU cores to use """ def __init__(self, epochs, pipeline, epochs_test=None, cv=2, scoring='accuracy', n_jobs=-1, metadata_querylist=None, metadata_querylist_test=None): new_info = mne.create_info(epochs.info['ch_names'], epochs.info['sfreq']) new_info['chs'] = copy.deepcopy(epochs.info['chs']) self._decoder = mne.decoding.SlidingEstimator(pipeline, n_jobs=n_jobs, scoring=scoring) X = epochs.get_data() if metadata_querylist is None: y = epochs.events[:, 2] else: y = _get_Y_from_metadata(epochs, metadata_querylist) if metadata_querylist_test is None: metadata_querylist_test = metadata_querylist nave = len(epochs) nave_testing = None n_classes = numpy.unique(y).size c_factors_training = '' if 'collapsed_factors' in epochs.info: c_factors_training = epochs.info['collapsed_factors'] c_factors_testing = None if isinstance(epochs_test, mne.BaseEpochs): logging.info('Using epochs argument for training and epochs_test ' 'argument for testing') X_test = epochs_test.get_data() if metadata_querylist is None: y_test = epochs_test.events[:, 2] else: y_test = _get_Y_from_metadata(epochs_test, metadata_querylist_test) nave_testing = len(epochs_test) if 'collapsed_factors' in epochs_test.info: c_factors_testing = epochs_test.info['collapsed_factors'] if numpy.setdiff1d(y, y_test).size != 0: raise ValueError( 'Training and testing set must have the same targets!') if X.shape[1:] != X_test.shape[1:]: raise ValueError( 'Training and testing set must have the same number ' 'of channels and samples!') self._decoder.fit(X, y) scores_raw = self._decoder.score(X_test, y_test) else: scores_raw = mne.decoding.cross_val_multiscore(self._decoder, X, y, cv=cv) self._decoder.fit(X, y) weights = mne.decoding.get_coef(self._decoder, 'patterns_', inverse_transform=True) if weights.ndim == 3: weights = numpy.average(weights, axis=1) super(Temporal, self).__init__(raw_scores=scores_raw, weights=weights, n_classes=n_classes, info=new_info, tmin=epochs.times[0], scoring_name=scoring, nave=nave, nave_testing=nave_testing, c_factors_testing=c_factors_testing, c_factors_training=c_factors_training) @property def decoder(self): """The decoder.""" return self._decoder