From 624e942c10952119337e02d0fded58f5561da973 Mon Sep 17 00:00:00 2001 From: Anton Date: Mon, 4 Mar 2024 16:17:18 +0100 Subject: [PATCH 01/45] Initial version of NearestConvexHull. --- pyriemann_qiskit/classification.py | 143 +++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index 98cf4df6..8dc46c66 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -27,6 +27,7 @@ from .utils.hyper_params_factory import gen_zz_feature_map, gen_two_local, get_spsa from .utils import get_provider, get_devices, get_simulator +from .utils.distance import distance_logeuclid_cpm logger.level = logging.WARNING @@ -710,3 +711,145 @@ def predict(self, X): """ labels = self._predict(X) return self._map_indices_to_classes(labels) + +class NearestConvexHull(QuanticClassifierBase): + + """Nearest Convex Hull Classifier (NCH) + + In NCH, for each class a convex hull is produced by the set of matrices + corresponding to each class. There is no training. Calculating a distance + to a hull is an optimization problem and it is calculated for each testing + sample (SPD matrix) and each hull/class. The minimal distance defines the + predicted class. + + Notes + ----- + .. versionadded:: 0.1.1 + + Parameters + ---------- + quantum : bool (default: True) + Only applies if `metric` contains a cpm distance or mean. + + - If true will run on local or remote backend + (depending on q_account_token value), + - If false, will perform classical computing instead. + q_account_token : string (default:None) + If `quantum` is True and `q_account_token` provided, + the classification task will be running on a IBM quantum backend. + If `load_account` is provided, the classifier will use the previous + token saved with `IBMProvider.save_account()`. + verbose : bool (default:True) + If true, will output all intermediate results and logs. + shots : int (default:1024) + Number of repetitions of each circuit, for sampling. + seed: int | None (default: None) + Random seed for the simulation + upper_bound : int (default: 7) + The maximum integer value for matrix normalization. + regularization: MixinTransformer (defulat: None) + Additional post-processing to regularize means. + classical_optimizer : OptimizationAlgorithm + An instance of OptimizationAlgorithm [3]_ + + """ + + def __init__( + self, + optimizer=get_spsa(), + gen_var_form=gen_two_local(), + quantum=True, + q_account_token=None, + verbose=True, + shots=1024, + gen_feature_map=gen_zz_feature_map(), + seed=None, + ): + QuanticClassifierBase.__init__( + self, quantum, q_account_token, verbose, shots, gen_feature_map, seed + ) + self.optimizer = optimizer + self.gen_var_form = gen_var_form + + def _init_algo(self, n_features): + + self._log("Nearest Convex Hull Classifier initiating algorithm") + + classifier = None + + if self.quantum: + self._log("Using NaiveQAOAOptimizer") + self._optimizer = NaiveQAOAOptimizer( + quantum_instance=self._quantum_instance, upper_bound=self.upper_bound + ) + else: + self._log("Using ClassicalOptimizer (COBYLA)") + self._optimizer = ClassicalOptimizer(self.classical_optimizer) + set_global_optimizer(self._optimizer) + + self.matrices_per_class_ = {} + + return classifier + + def _train(self, X, y): + """Fit (store the training data). + + Parameters + ---------- + X : ndarray, shape (n_matrices, n_channels, n_channels) + Set of SPD matrices. + y : ndarray, shape (n_matrices,) + Labels for each matrix. + sample_weight : None + Not used, here for compatibility with sklearn API. + + Returns + ------- + self : NearestNeighbor instance + The NearestNeighbor instance. + """ + + #self.covmeans_ = X + #self.classmeans_ = y + self.classes_ = np.unique(y) + + for c in self.classes_: + self.matrices_per_class_[c] = [] + + for i in range(0,len(y)): + self.matrices_per_class_[y[i]].append(X(i)) + + for c in self.classes_: + self.matrices_per_class_[c] = np.asarray(self.matrices_per_class_[c]) + + def predict(self, X): + """Calculates the predictions. + + Parameters + ---------- + X : ndarray, shape (n_samples, n_features) + Input vector, where `n_samples` is the number of samples and + `n_features` is the number of features. + + Returns + ------- + pred : array, shape (n_samples,) + Class labels for samples in X. + """ + + best_distance = -1 + best_class = -1 + + pred = [] + for test_sample in X: + for c in self.classes_: + + distance = distance_logeuclid_cpm(self.matrices_per_class_[c], test_sample) + + if distance < best_distance: + best_distance = distance + best_class = c + + pred.append(best_class) + + return pred \ No newline at end of file From cf3f6d9cc57a6e7d3064635ed13c754bc2ff47ed Mon Sep 17 00:00:00 2001 From: Anton Date: Mon, 4 Mar 2024 16:41:25 +0100 Subject: [PATCH 02/45] Added script for testing. --- examples/ERP/classify_P300_nch.py | 144 +++++++++++++++++++++++++++++ pyriemann_qiskit/classification.py | 21 +++-- 2 files changed, 155 insertions(+), 10 deletions(-) create mode 100644 examples/ERP/classify_P300_nch.py diff --git a/examples/ERP/classify_P300_nch.py b/examples/ERP/classify_P300_nch.py new file mode 100644 index 00000000..abf01784 --- /dev/null +++ b/examples/ERP/classify_P300_nch.py @@ -0,0 +1,144 @@ +""" +==================================================================== +Classification of P300 datasets from MOABB +==================================================================== + +It demonstrates the QuantumClassifierWithDefaultRiemannianPipeline(). This +pipeline uses Riemannian Geometry, Tangent Space and a quantum SVM +classifier. MOABB is used to access many EEG datasets and also for the +evaluation and comparison with other classifiers. + +In QuantumClassifierWithDefaultRiemannianPipeline(): +If parameter "shots" is None then a classical SVM is used similar to the one +in scikit learn. +If "shots" is not None and IBM Qunatum token is provided with "q_account_token" +then a real Quantum computer will be used. +You also need to adjust the "n_components" in the PCA procedure to the number +of qubits supported by the real quantum computer you are going to use. +A list of real quantum computers is available in your IBM quantum account. + +""" +# Author: Anton Andreev +# Modified from plot_classify_EEG_tangentspace.py of pyRiemann +# License: BSD (3-clause) + +from pyriemann.estimation import XdawnCovariances +from pyriemann.tangentspace import TangentSpace +from sklearn.pipeline import make_pipeline +from matplotlib import pyplot as plt +import warnings +import seaborn as sns +from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA +from moabb import set_log_level +from moabb.datasets import bi2012 +from moabb.evaluations import WithinSessionEvaluation +from moabb.paradigms import P300 +from pyriemann_qiskit.pipelines import ( + QuantumClassifierWithDefaultRiemannianPipeline, +) +from sklearn.decomposition import PCA +from pyriemann_qiskit.classification import NearestConvexHull + +print(__doc__) + +############################################################################## +# getting rid of the warnings about the future +warnings.simplefilter(action="ignore", category=FutureWarning) +warnings.simplefilter(action="ignore", category=RuntimeWarning) + +warnings.filterwarnings("ignore") + +set_log_level("info") + +############################################################################## +# Create Pipelines +# ---------------- +# +# Pipelines must be a dict of sklearn pipeline transformer. + +############################################################################## +# We have to do this because the classes are called 'Target' and 'NonTarget' +# but the evaluation function uses a LabelEncoder, transforming them +# to 0 and 1 +labels_dict = {"Target": 1, "NonTarget": 0} + +paradigm = P300(resample=128) + +datasets = [bi2012()] # MOABB provides several other P300 datasets + +# reduce the number of subjects, the Quantum pipeline takes a lot of time +# if executed on the entire dataset +n_subjects = 2 +for dataset in datasets: + dataset.subject_list = dataset.subject_list[0:n_subjects] + +overwrite = True # set to True if we want to overwrite cached results + +pipelines = {} + +# A Riemannian Quantum pipeline provided by pyRiemann-qiskit +# You can choose between classical SVM and Quantum SVM. +pipelines["RG+NCH"] = make_pipeline( + # applies XDawn and calculates the covariance matrix, output it matrices + XdawnCovariances( + nfilter=2, + classes=[labels_dict["Target"]], + estimator="lwf", + xdawn_estimator="scm", + ), + NearestConvexHull(), # you can use other classifiers +) + +# Here we provide a pipeline for comparison: + +# This is a standard pipeline similar to +# QuantumClassifierWithDefaultRiemannianPipeline, but with LDA classifier +# instead. +pipelines["RG+LDA"] = make_pipeline( + # applies XDawn and calculates the covariance matrix, output it matrices + XdawnCovariances( + nfilter=2, + classes=[labels_dict["Target"]], + estimator="lwf", + xdawn_estimator="scm", + ), + TangentSpace(), + PCA(n_components=10), + LDA(solver="lsqr", shrinkage="auto"), # you can use other classifiers +) + +print("Total pipelines to evaluate: ", len(pipelines)) + +evaluation = WithinSessionEvaluation( + paradigm=paradigm, datasets=datasets, suffix="examples", overwrite=overwrite +) + +results = evaluation.process(pipelines) + +print("Averaging the session performance:") +print(results.groupby("pipeline").mean("score")[["score", "time"]]) + +############################################################################## +# Plot Results +# ---------------- +# +# Here we plot the results to compare the two pipelines + +fig, ax = plt.subplots(facecolor="white", figsize=[8, 4]) + +sns.stripplot( + data=results, + y="score", + x="pipeline", + ax=ax, + jitter=True, + alpha=0.5, + zorder=1, + palette="Set1", +) +sns.pointplot(data=results, y="score", x="pipeline", ax=ax, palette="Set1") + +ax.set_ylabel("ROC AUC") +ax.set_ylim(0.3, 1) + +plt.show() diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index 8dc46c66..3eba63b3 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -756,20 +756,21 @@ class NearestConvexHull(QuanticClassifierBase): def __init__( self, - optimizer=get_spsa(), - gen_var_form=gen_two_local(), - quantum=True, + quantum=False, #change to True in final version q_account_token=None, verbose=True, shots=1024, - gen_feature_map=gen_zz_feature_map(), seed=None, + upper_bound=7, + regularization=None, + classical_optimizer=CobylaOptimizer(rhobeg=2.1, rhoend=0.000001), ): QuanticClassifierBase.__init__( - self, quantum, q_account_token, verbose, shots, gen_feature_map, seed + self, quantum, q_account_token, verbose, shots, None, seed ) - self.optimizer = optimizer - self.gen_var_form = gen_var_form + self.upper_bound = upper_bound + self.regularization = regularization + self.classical_optimizer = classical_optimizer def _init_algo(self, n_features): @@ -817,10 +818,10 @@ def _train(self, X, y): self.matrices_per_class_[c] = [] for i in range(0,len(y)): - self.matrices_per_class_[y[i]].append(X(i)) + self.matrices_per_class_[y[i]].append(X[i,:,:]) for c in self.classes_: - self.matrices_per_class_[c] = np.asarray(self.matrices_per_class_[c]) + self.matrices_per_class_[c] = np.array(self.matrices_per_class_[c]) def predict(self, X): """Calculates the predictions. @@ -852,4 +853,4 @@ def predict(self, X): pred.append(best_class) - return pred \ No newline at end of file + return np.array(pred) \ No newline at end of file From fd85f0abbf09195ed28f5ed8d83958e3f2fc51c6 Mon Sep 17 00:00:00 2001 From: Anton Date: Mon, 4 Mar 2024 17:01:48 +0100 Subject: [PATCH 03/45] First version that runs. --- pyriemann_qiskit/classification.py | 45 +++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index 3eba63b3..cff8aa4d 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -268,17 +268,23 @@ def predict_proba(self, X): "No predict_proba method available.\ Computing softmax probabilities..." ) - proba = self._classifier.predict(X) - proba = [ - np.array( - [ - 1 if c == self.classes_[i] else 0 - for i in range(len(self.classes_)) - ] + if (self._classifier == None): + self._log( + "No classifier available, using class predict()" ) - for c in proba - ] - proba = softmax(proba, axis=0) + return self.predict(X) + else: + proba = self._classifier.predict(X) + proba = [ + np.array( + [ + 1 if c == self.classes_[i] else 0 + for i in range(len(self.classes_)) + ] + ) + for c in proba + ] + proba = softmax(proba, axis=0) else: proba = self._classifier.predict_proba(X) @@ -810,6 +816,8 @@ def _train(self, X, y): The NearestNeighbor instance. """ + self._log("Start NCH Train") + #self.covmeans_ = X #self.classmeans_ = y self.classes_ = np.unique(y) @@ -822,6 +830,8 @@ def _train(self, X, y): for c in self.classes_: self.matrices_per_class_[c] = np.array(self.matrices_per_class_[c]) + + self._log("End NCH Train") def predict(self, X): """Calculates the predictions. @@ -838,11 +848,19 @@ def predict(self, X): Class labels for samples in X. """ - best_distance = -1 - best_class = -1 - + self._log("Start NCH Predict") pred = [] + + self._log("Total test samples:", X.shape[0]) + i = 1; + for test_sample in X: + + self._log("Processing sample:", i, " / ", X.shape[0]) + + best_distance = -1 + best_class = -1 + for c in self.classes_: distance = distance_logeuclid_cpm(self.matrices_per_class_[c], test_sample) @@ -852,5 +870,6 @@ def predict(self, X): best_class = c pred.append(best_class) + self._log("Predicted: ", best_class) return np.array(pred) \ No newline at end of file From 4548b783759403e7506a86fc9c927e6fe1a24f40 Mon Sep 17 00:00:00 2001 From: Anton Date: Mon, 4 Mar 2024 18:46:30 +0100 Subject: [PATCH 04/45] Improved code. --- examples/ERP/classify_P300_nch.py | 6 +++--- pyriemann_qiskit/classification.py | 9 +++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/examples/ERP/classify_P300_nch.py b/examples/ERP/classify_P300_nch.py index abf01784..4699bcdf 100644 --- a/examples/ERP/classify_P300_nch.py +++ b/examples/ERP/classify_P300_nch.py @@ -30,7 +30,7 @@ import seaborn as sns from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA from moabb import set_log_level -from moabb.datasets import bi2012 +from moabb.datasets import bi2012, BNCI2014009 from moabb.evaluations import WithinSessionEvaluation from moabb.paradigms import P300 from pyriemann_qiskit.pipelines import ( @@ -64,11 +64,11 @@ paradigm = P300(resample=128) -datasets = [bi2012()] # MOABB provides several other P300 datasets +datasets = [BNCI2014009()] # MOABB provides several other P300 datasets # reduce the number of subjects, the Quantum pipeline takes a lot of time # if executed on the entire dataset -n_subjects = 2 +n_subjects = 1 for dataset in datasets: dataset.subject_list = dataset.subject_list[0:n_subjects] diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index cff8aa4d..95096005 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -864,12 +864,17 @@ def predict(self, X): for c in self.classes_: distance = distance_logeuclid_cpm(self.matrices_per_class_[c], test_sample) + print("Distance: ", distance) - if distance < best_distance: + if best_distance == -1: + best_distance = distance + best_class = c + elif distance < best_distance: best_distance = distance best_class = c pred.append(best_class) - self._log("Predicted: ", best_class) + self._log("Predicted: ", best_class) + i=i+1 return np.array(pred) \ No newline at end of file From 0de3c40c3296cad17bf4902a49a5b4d2cd2acb3b Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 5 Mar 2024 07:22:40 +0100 Subject: [PATCH 05/45] Added support for parallel processing. It gives an error: AttributeError: Pipeline has none of the following attributes: decision_function. --- pyriemann_qiskit/classification.py | 68 +++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index 95096005..2984be78 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -28,6 +28,7 @@ from .utils.hyper_params_factory import gen_zz_feature_map, gen_two_local, get_spsa from .utils import get_provider, get_devices, get_simulator from .utils.distance import distance_logeuclid_cpm +from joblib import Parallel, delayed logger.level = logging.WARNING @@ -832,7 +833,25 @@ def _train(self, X, y): self.matrices_per_class_[c] = np.array(self.matrices_per_class_[c]) self._log("End NCH Train") - + + def _process_sample(self,test_sample): + best_distance = -1 + best_class = -1 + + for c in self.classes_: + + distance = distance_logeuclid_cpm(self.matrices_per_class_[c], test_sample) + #print("Distance: ", distance) + + if best_distance == -1: + best_distance = distance + best_class = c + elif distance < best_distance: + best_distance = distance + best_class = c + + return best_class + def predict(self, X): """Calculates the predictions. @@ -852,29 +871,36 @@ def predict(self, X): pred = [] self._log("Total test samples:", X.shape[0]) - i = 1; + k = 0; - for test_sample in X: - - self._log("Processing sample:", i, " / ", X.shape[0]) - - best_distance = -1 - best_class = -1 + parallel = True + + if (parallel): + + pred = Parallel(n_jobs=12)(delayed(self._process_sample)(test_sample) for test_sample in X) - for c in self.classes_: + else: + for test_sample in X: - distance = distance_logeuclid_cpm(self.matrices_per_class_[c], test_sample) - print("Distance: ", distance) + k = k + 1 + self._log("Processing sample:", k, " / ", X.shape[0]) - if best_distance == -1: - best_distance = distance - best_class = c - elif distance < best_distance: - best_distance = distance - best_class = c - - pred.append(best_class) - self._log("Predicted: ", best_class) - i=i+1 + best_distance = -1 + best_class = -1 + + for c in self.classes_: + + distance = distance_logeuclid_cpm(self.matrices_per_class_[c], test_sample) + print("Distance: ", distance) + + if best_distance == -1: + best_distance = distance + best_class = c + elif distance < best_distance: + best_distance = distance + best_class = c + + pred.append(best_class) + self._log("Predicted: ", best_class) return np.array(pred) \ No newline at end of file From 37491eb9d10a2f967cb1d56bd9c32ad87919b73f Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 5 Mar 2024 08:43:07 +0100 Subject: [PATCH 06/45] renamed --- examples/ERP/classify_P300_nch.py | 4 ++-- pyriemann_qiskit/classification.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/ERP/classify_P300_nch.py b/examples/ERP/classify_P300_nch.py index 4699bcdf..e0e4ded2 100644 --- a/examples/ERP/classify_P300_nch.py +++ b/examples/ERP/classify_P300_nch.py @@ -37,7 +37,7 @@ QuantumClassifierWithDefaultRiemannianPipeline, ) from sklearn.decomposition import PCA -from pyriemann_qiskit.classification import NearestConvexHull +from pyriemann_qiskit.classification import QuanticNCH print(__doc__) @@ -86,7 +86,7 @@ estimator="lwf", xdawn_estimator="scm", ), - NearestConvexHull(), # you can use other classifiers + QuanticNCH(), # you can use other classifiers ) # Here we provide a pipeline for comparison: diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index 2984be78..2f94d7c2 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -719,7 +719,7 @@ def predict(self, X): labels = self._predict(X) return self._map_indices_to_classes(labels) -class NearestConvexHull(QuanticClassifierBase): +class QuanticNCH(QuanticClassifierBase): """Nearest Convex Hull Classifier (NCH) From 1c1d17be017d7be191acd00bce6238c123a3a6a1 Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 5 Mar 2024 10:50:43 +0100 Subject: [PATCH 07/45] New version that uses a new class that implements a NCH classifier. --- pyriemann_qiskit/classification.py | 220 +++++++++++++++-------------- 1 file changed, 116 insertions(+), 104 deletions(-) diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index 2f94d7c2..2104840c 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -261,7 +261,7 @@ def predict_proba(self, X): prob : ndarray, shape (n_samples, n_classes) prob[n, i] == 1 if the nth sample is assigned to class `i`. """ - + if not hasattr(self._classifier, "predict_proba"): # Classifier has no predict_proba # Use the result from predict and apply a softmax @@ -269,23 +269,17 @@ def predict_proba(self, X): "No predict_proba method available.\ Computing softmax probabilities..." ) - if (self._classifier == None): - self._log( - "No classifier available, using class predict()" + proba = self._classifier.predict(X) + proba = [ + np.array( + [ + 1 if c == self.classes_[i] else 0 + for i in range(len(self.classes_)) + ] ) - return self.predict(X) - else: - proba = self._classifier.predict(X) - proba = [ - np.array( - [ - 1 if c == self.classes_[i] else 0 - for i in range(len(self.classes_)) - ] - ) - for c in proba - ] - proba = softmax(proba, axis=0) + for c in proba + ] + proba = softmax(proba, axis=0) else: proba = self._classifier.predict_proba(X) @@ -719,87 +713,15 @@ def predict(self, X): labels = self._predict(X) return self._map_indices_to_classes(labels) -class QuanticNCH(QuanticClassifierBase): - - """Nearest Convex Hull Classifier (NCH) - - In NCH, for each class a convex hull is produced by the set of matrices - corresponding to each class. There is no training. Calculating a distance - to a hull is an optimization problem and it is calculated for each testing - sample (SPD matrix) and each hull/class. The minimal distance defines the - predicted class. - - Notes - ----- - .. versionadded:: 0.1.1 - - Parameters - ---------- - quantum : bool (default: True) - Only applies if `metric` contains a cpm distance or mean. - - - If true will run on local or remote backend - (depending on q_account_token value), - - If false, will perform classical computing instead. - q_account_token : string (default:None) - If `quantum` is True and `q_account_token` provided, - the classification task will be running on a IBM quantum backend. - If `load_account` is provided, the classifier will use the previous - token saved with `IBMProvider.save_account()`. - verbose : bool (default:True) - If true, will output all intermediate results and logs. - shots : int (default:1024) - Number of repetitions of each circuit, for sampling. - seed: int | None (default: None) - Random seed for the simulation - upper_bound : int (default: 7) - The maximum integer value for matrix normalization. - regularization: MixinTransformer (defulat: None) - Additional post-processing to regularize means. - classical_optimizer : OptimizationAlgorithm - An instance of OptimizationAlgorithm [3]_ - - """ - - def __init__( - self, - quantum=False, #change to True in final version - q_account_token=None, - verbose=True, - shots=1024, - seed=None, - upper_bound=7, - regularization=None, - classical_optimizer=CobylaOptimizer(rhobeg=2.1, rhoend=0.000001), - ): - QuanticClassifierBase.__init__( - self, quantum, q_account_token, verbose, shots, None, seed - ) - self.upper_bound = upper_bound - self.regularization = regularization - self.classical_optimizer = classical_optimizer - - def _init_algo(self, n_features): - - self._log("Nearest Convex Hull Classifier initiating algorithm") - - classifier = None - - if self.quantum: - self._log("Using NaiveQAOAOptimizer") - self._optimizer = NaiveQAOAOptimizer( - quantum_instance=self._quantum_instance, upper_bound=self.upper_bound - ) - else: - self._log("Using ClassicalOptimizer (COBYLA)") - self._optimizer = ClassicalOptimizer(self.classical_optimizer) - set_global_optimizer(self._optimizer) - +from sklearn.base import BaseEstimator, ClassifierMixin, TransformerMixin +class NearestConvexHull(BaseEstimator, ClassifierMixin, TransformerMixin): + + def __init__(self, n_jobs=1): + """Init.""" + self.n_jobs = n_jobs self.matrices_per_class_ = {} - - return classifier - def _train(self, X, y): + def fit(self, X, y): """Fit (store the training data). Parameters @@ -817,11 +739,16 @@ def _train(self, X, y): The NearestNeighbor instance. """ - self._log("Start NCH Train") + print("Start NCH Train") #self.covmeans_ = X #self.classmeans_ = y self.classes_ = np.unique(y) + print("Classes:", self.classes_) + print("Class 1", sum(y)) + print("Class 2", len(y) - sum(y)) + if (max(self.classes_) > 1): + raise Exception("Currently designed for two classes 0 et 1") for c in self.classes_: self.matrices_per_class_[c] = [] @@ -832,8 +759,8 @@ def _train(self, X, y): for c in self.classes_: self.matrices_per_class_[c] = np.array(self.matrices_per_class_[c]) - self._log("End NCH Train") - + print("End NCH Train") + def _process_sample(self,test_sample): best_distance = -1 best_class = -1 @@ -867,10 +794,14 @@ def predict(self, X): Class labels for samples in X. """ - self._log("Start NCH Predict") + print("Start NCH Predict") pred = [] - self._log("Total test samples:", X.shape[0]) + #testing only!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + #labels = [1] * X.shape[0] + #return labels + + print("Total test samples:", X.shape[0]) k = 0; parallel = True @@ -883,7 +814,7 @@ def predict(self, X): for test_sample in X: k = k + 1 - self._log("Processing sample:", k, " / ", X.shape[0]) + print("Processing sample:", k, " / ", X.shape[0]) best_distance = -1 best_class = -1 @@ -901,6 +832,87 @@ def predict(self, X): best_class = c pred.append(best_class) - self._log("Predicted: ", best_class) + print("Predicted: ", best_class) - return np.array(pred) \ No newline at end of file + return np.array(pred) + + +class QuanticNCH(QuanticClassifierBase): + + """Nearest Convex Hull Classifier (NCH) + + In NCH, for each class a convex hull is produced by the set of matrices + corresponding to each class. There is no training. Calculating a distance + to a hull is an optimization problem and it is calculated for each testing + sample (SPD matrix) and each hull/class. The minimal distance defines the + predicted class. + + Notes + ----- + .. versionadded:: 0.1.1 + + Parameters + ---------- + quantum : bool (default: True) + Only applies if `metric` contains a cpm distance or mean. + + - If true will run on local or remote backend + (depending on q_account_token value), + - If false, will perform classical computing instead. + q_account_token : string (default:None) + If `quantum` is True and `q_account_token` provided, + the classification task will be running on a IBM quantum backend. + If `load_account` is provided, the classifier will use the previous + token saved with `IBMProvider.save_account()`. + verbose : bool (default:True) + If true, will output all intermediate results and logs. + shots : int (default:1024) + Number of repetitions of each circuit, for sampling. + seed: int | None (default: None) + Random seed for the simulation + upper_bound : int (default: 7) + The maximum integer value for matrix normalization. + regularization: MixinTransformer (defulat: None) + Additional post-processing to regularize means. + classical_optimizer : OptimizationAlgorithm + An instance of OptimizationAlgorithm [3]_ + + """ + + def __init__( + self, + quantum=False, #change to True in final version + q_account_token=None, + verbose=True, + shots=1024, + seed=None, + upper_bound=7, + regularization=None, + classical_optimizer=CobylaOptimizer(rhobeg=2.1, rhoend=0.000001), + ): + QuanticClassifierBase.__init__( + self, quantum, q_account_token, verbose, shots, None, seed + ) + self.upper_bound = upper_bound + self.regularization = regularization + self.classical_optimizer = classical_optimizer + + def _init_algo(self, n_features): + + self._log("Nearest Convex Hull Classifier initiating algorithm") + + classifier = NearestConvexHull() + + if self.quantum: + self._log("Using NaiveQAOAOptimizer") + self._optimizer = NaiveQAOAOptimizer( + quantum_instance=self._quantum_instance, upper_bound=self.upper_bound + ) + else: + self._log("Using ClassicalOptimizer (COBYLA)") + self._optimizer = ClassicalOptimizer(self.classical_optimizer) + set_global_optimizer(self._optimizer) + + return classifier + + \ No newline at end of file From dc5633ec46c7ffd4b9efe7795261acf185a592ec Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 5 Mar 2024 11:22:03 +0100 Subject: [PATCH 08/45] small update --- pyriemann_qiskit/classification.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index 2104840c..43475b20 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -799,6 +799,7 @@ def predict(self, X): #testing only!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! #labels = [1] * X.shape[0] + #print("End NCH Predict") #return labels print("Total test samples:", X.shape[0]) @@ -833,7 +834,8 @@ def predict(self, X): pred.append(best_class) print("Predicted: ", best_class) - + + print("End NCH Predict") return np.array(pred) From e07cd39b11eaac1cc77bf410ea9602779259b7f8 Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 5 Mar 2024 14:06:57 +0100 Subject: [PATCH 09/45] Updated to newest code - the new version of the distance function. Added an example that runs on a small number of test samples, so that we can get results quicker. --- examples/ERP/classify_P300_nch_no_moabb.py | 65 ++++++++++++++++++++++ pyriemann_qiskit/classification.py | 6 +- 2 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 examples/ERP/classify_P300_nch_no_moabb.py diff --git a/examples/ERP/classify_P300_nch_no_moabb.py b/examples/ERP/classify_P300_nch_no_moabb.py new file mode 100644 index 00000000..7dd18b4b --- /dev/null +++ b/examples/ERP/classify_P300_nch_no_moabb.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +""" +Created on Tue Mar 5 12:26:17 2024 + +@author: antona +""" + +import matplotlib.pyplot as plt +from pyriemann.estimation import Covariances, ERPCovariances, XdawnCovariances +from pyriemann.tangentspace import TangentSpace +from sklearn.linear_model import LogisticRegression +from sklearn.pipeline import make_pipeline +from sklearn.model_selection import cross_val_score +from sklearn.metrics import balanced_accuracy_score, make_scorer +from moabb.datasets import bi2013a, BNCI2014008, BNCI2014009, BNCI2015003, EPFLP300, Lee2019_ERP +from moabb.paradigms import P300 +from sklearn.model_selection import train_test_split +from pyriemann_qiskit.classification import QuanticNCH +import numpy as np +from sklearn.preprocessing import LabelEncoder +import os +import time +from joblib import Parallel, delayed +from multiprocessing import Process +from PIL import Image + +import warnings +warnings.filterwarnings("ignore", category=DeprecationWarning) + +from mne import set_log_level +set_log_level("CRITICAL") + +paradigm = P300() +labels_dict = {"Target": 1, "NonTarget": 0} + +le = LabelEncoder() + +db = BNCI2014009()#BNCI2014008() +n_subjects = 1 + +for subject_i, subject in enumerate(db.subject_list[0:n_subjects]): + + print("Loading subject:" , subject) + + X, y, _ = paradigm.get_data(dataset=db, subjects=[subject]) + y = le.fit_transform(y) + print(X.shape) + + X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.005, random_state=42) + + pipelines = {} + + pipelines["RG+NCH"] = make_pipeline( + XdawnCovariances( + nfilter=3, #increased, might be a problem for quantum + classes=[labels_dict["Target"]], + estimator="lwf", + xdawn_estimator="scm", + ), + QuanticNCH(), + ) + + score = pipelines["RG+NCH"].fit(X_train, y_train).score(X_test, y_test) + print("Classification score - subject, score:", subject, score) + \ No newline at end of file diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index 4da867bd..cd82b9a7 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -30,7 +30,7 @@ from .utils.hyper_params_factory import gen_zz_feature_map, gen_two_local, get_spsa from .utils import get_provider, get_devices, get_simulator -from .utils.distance import distance_logeuclid_cpm +from .utils.distance import qdistance_logeuclid_to_convex_hull from joblib import Parallel, delayed logger.level = logging.WARNING @@ -800,7 +800,7 @@ def _process_sample(self,test_sample): for c in self.classes_: - distance = distance_logeuclid_cpm(self.matrices_per_class_[c], test_sample) + distance = qdistance_logeuclid_to_convex_hull(self.matrices_per_class_[c], test_sample) #print("Distance: ", distance) if best_distance == -1: @@ -855,7 +855,7 @@ def predict(self, X): for c in self.classes_: - distance = distance_logeuclid_cpm(self.matrices_per_class_[c], test_sample) + distance = qdistance_logeuclid_to_convex_hull(self.matrices_per_class_[c], test_sample) print("Distance: ", distance) if best_distance == -1: From a80ea8d6456530dd8a390337125fc1b1f3dcd291 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 5 Mar 2024 16:11:05 +0000 Subject: [PATCH 10/45] [pre-commit.ci] auto fixes from pre-commit.com hooks --- examples/ERP/classify_P300_nch_no_moabb.py | 61 ++++--- pyriemann_qiskit/classification.py | 193 +++++++++++---------- 2 files changed, 132 insertions(+), 122 deletions(-) diff --git a/examples/ERP/classify_P300_nch_no_moabb.py b/examples/ERP/classify_P300_nch_no_moabb.py index 7dd18b4b..88a8dfe9 100644 --- a/examples/ERP/classify_P300_nch_no_moabb.py +++ b/examples/ERP/classify_P300_nch_no_moabb.py @@ -12,7 +12,14 @@ from sklearn.pipeline import make_pipeline from sklearn.model_selection import cross_val_score from sklearn.metrics import balanced_accuracy_score, make_scorer -from moabb.datasets import bi2013a, BNCI2014008, BNCI2014009, BNCI2015003, EPFLP300, Lee2019_ERP +from moabb.datasets import ( + bi2013a, + BNCI2014008, + BNCI2014009, + BNCI2015003, + EPFLP300, + Lee2019_ERP, +) from moabb.paradigms import P300 from sklearn.model_selection import train_test_split from pyriemann_qiskit.classification import QuanticNCH @@ -25,9 +32,11 @@ from PIL import Image import warnings + warnings.filterwarnings("ignore", category=DeprecationWarning) from mne import set_log_level + set_log_level("CRITICAL") paradigm = P300() @@ -35,31 +44,31 @@ le = LabelEncoder() -db = BNCI2014009()#BNCI2014008() +db = BNCI2014009() # BNCI2014008() n_subjects = 1 - + for subject_i, subject in enumerate(db.subject_list[0:n_subjects]): - - print("Loading subject:" , subject) - - X, y, _ = paradigm.get_data(dataset=db, subjects=[subject]) - y = le.fit_transform(y) - print(X.shape) - - X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.005, random_state=42) - - pipelines = {} + print("Loading subject:", subject) + + X, y, _ = paradigm.get_data(dataset=db, subjects=[subject]) + y = le.fit_transform(y) + print(X.shape) + + X_train, X_test, y_train, y_test = train_test_split( + X, y, test_size=0.005, random_state=42 + ) + + pipelines = {} + + pipelines["RG+NCH"] = make_pipeline( + XdawnCovariances( + nfilter=3, # increased, might be a problem for quantum + classes=[labels_dict["Target"]], + estimator="lwf", + xdawn_estimator="scm", + ), + QuanticNCH(), + ) - pipelines["RG+NCH"] = make_pipeline( - XdawnCovariances( - nfilter=3, #increased, might be a problem for quantum - classes=[labels_dict["Target"]], - estimator="lwf", - xdawn_estimator="scm", - ), - QuanticNCH(), - ) - - score = pipelines["RG+NCH"].fit(X_train, y_train).score(X_test, y_test) - print("Classification score - subject, score:", subject, score) - \ No newline at end of file + score = pipelines["RG+NCH"].fit(X_train, y_train).score(X_test, y_test) + print("Classification score - subject, score:", subject, score) diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index cd82b9a7..67bc4789 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -264,7 +264,7 @@ def predict_proba(self, X): prob : ndarray, shape (n_samples, n_classes) prob[n, i] == 1 if the nth sample is assigned to class `i`. """ - + if not hasattr(self._classifier, "predict_proba"): # Classifier has no predict_proba # Use the result from predict and apply a softmax @@ -746,14 +746,16 @@ def predict(self, X): labels = self._predict(X) return self._map_indices_to_classes(labels) + from sklearn.base import BaseEstimator, ClassifierMixin, TransformerMixin + + class NearestConvexHull(BaseEstimator, ClassifierMixin, TransformerMixin): - def __init__(self, n_jobs=1): """Init.""" self.n_jobs = n_jobs self.matrices_per_class_ = {} - + def fit(self, X, y): """Fit (store the training data). @@ -771,47 +773,48 @@ def fit(self, X, y): self : NearestNeighbor instance The NearestNeighbor instance. """ - - print("Start NCH Train") - - #self.covmeans_ = X - #self.classmeans_ = y + + print("Start NCH Train") + + # self.covmeans_ = X + # self.classmeans_ = y self.classes_ = np.unique(y) print("Classes:", self.classes_) print("Class 1", sum(y)) print("Class 2", len(y) - sum(y)) - if (max(self.classes_) > 1): + if max(self.classes_) > 1: raise Exception("Currently designed for two classes 0 et 1") - + for c in self.classes_: self.matrices_per_class_[c] = [] - - for i in range(0,len(y)): - self.matrices_per_class_[y[i]].append(X[i,:,:]) - + + for i in range(0, len(y)): + self.matrices_per_class_[y[i]].append(X[i, :, :]) + for c in self.classes_: self.matrices_per_class_[c] = np.array(self.matrices_per_class_[c]) - - print("End NCH Train") - - def _process_sample(self,test_sample): + + print("End NCH Train") + + def _process_sample(self, test_sample): best_distance = -1 best_class = -1 - + for c in self.classes_: - - distance = qdistance_logeuclid_to_convex_hull(self.matrices_per_class_[c], test_sample) - #print("Distance: ", distance) - + distance = qdistance_logeuclid_to_convex_hull( + self.matrices_per_class_[c], test_sample + ) + # print("Distance: ", distance) + if best_distance == -1: best_distance = distance best_class = c elif distance < best_distance: best_distance = distance best_class = c - + return best_class - + def predict(self, X): """Calculates the predictions. @@ -826,97 +829,98 @@ def predict(self, X): pred : array, shape (n_samples,) Class labels for samples in X. """ - - print("Start NCH Predict") + + print("Start NCH Predict") pred = [] - - #testing only!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - #labels = [1] * X.shape[0] - #print("End NCH Predict") - #return labels - - print("Total test samples:", X.shape[0]) - k = 0; - + + # testing only!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + # labels = [1] * X.shape[0] + # print("End NCH Predict") + # return labels + + print("Total test samples:", X.shape[0]) + k = 0 + parallel = True - - if (parallel): - - pred = Parallel(n_jobs=12)(delayed(self._process_sample)(test_sample) for test_sample in X) - - else: + + if parallel: + pred = Parallel(n_jobs=12)( + delayed(self._process_sample)(test_sample) for test_sample in X + ) + + else: for test_sample in X: - k = k + 1 - print("Processing sample:", k, " / ", X.shape[0]) - + print("Processing sample:", k, " / ", X.shape[0]) + best_distance = -1 best_class = -1 - + for c in self.classes_: - - distance = qdistance_logeuclid_to_convex_hull(self.matrices_per_class_[c], test_sample) + distance = qdistance_logeuclid_to_convex_hull( + self.matrices_per_class_[c], test_sample + ) print("Distance: ", distance) - + if best_distance == -1: best_distance = distance best_class = c elif distance < best_distance: best_distance = distance best_class = c - + pred.append(best_class) print("Predicted: ", best_class) - - print("End NCH Predict") + + print("End NCH Predict") return np.array(pred) - - + + class QuanticNCH(QuanticClassifierBase): """Nearest Convex Hull Classifier (NCH) - In NCH, for each class a convex hull is produced by the set of matrices - corresponding to each class. There is no training. Calculating a distance - to a hull is an optimization problem and it is calculated for each testing - sample (SPD matrix) and each hull/class. The minimal distance defines the - predicted class. - - Notes - ----- - .. versionadded:: 0.1.1 - - Parameters - ---------- - quantum : bool (default: True) - Only applies if `metric` contains a cpm distance or mean. - - - If true will run on local or remote backend - (depending on q_account_token value), - - If false, will perform classical computing instead. - q_account_token : string (default:None) - If `quantum` is True and `q_account_token` provided, - the classification task will be running on a IBM quantum backend. - If `load_account` is provided, the classifier will use the previous - token saved with `IBMProvider.save_account()`. - verbose : bool (default:True) - If true, will output all intermediate results and logs. - shots : int (default:1024) - Number of repetitions of each circuit, for sampling. - seed: int | None (default: None) - Random seed for the simulation - upper_bound : int (default: 7) - The maximum integer value for matrix normalization. - regularization: MixinTransformer (defulat: None) - Additional post-processing to regularize means. - classical_optimizer : OptimizationAlgorithm - An instance of OptimizationAlgorithm [3]_ + In NCH, for each class a convex hull is produced by the set of matrices + corresponding to each class. There is no training. Calculating a distance + to a hull is an optimization problem and it is calculated for each testing + sample (SPD matrix) and each hull/class. The minimal distance defines the + predicted class. + + Notes + ----- + .. versionadded:: 0.1.1 + + Parameters + ---------- + quantum : bool (default: True) + Only applies if `metric` contains a cpm distance or mean. + + - If true will run on local or remote backend + (depending on q_account_token value), + - If false, will perform classical computing instead. + q_account_token : string (default:None) + If `quantum` is True and `q_account_token` provided, + the classification task will be running on a IBM quantum backend. + If `load_account` is provided, the classifier will use the previous + token saved with `IBMProvider.save_account()`. + verbose : bool (default:True) + If true, will output all intermediate results and logs. + shots : int (default:1024) + Number of repetitions of each circuit, for sampling. + seed: int | None (default: None) + Random seed for the simulation + upper_bound : int (default: 7) + The maximum integer value for matrix normalization. + regularization: MixinTransformer (defulat: None) + Additional post-processing to regularize means. + classical_optimizer : OptimizationAlgorithm + An instance of OptimizationAlgorithm [3]_ """ def __init__( self, - quantum=False, #change to True in final version + quantum=False, # change to True in final version q_account_token=None, verbose=True, shots=1024, @@ -933,11 +937,10 @@ def __init__( self.classical_optimizer = classical_optimizer def _init_algo(self, n_features): - self._log("Nearest Convex Hull Classifier initiating algorithm") - + classifier = NearestConvexHull() - + if self.quantum: self._log("Using NaiveQAOAOptimizer") self._optimizer = NaiveQAOAOptimizer( @@ -947,7 +950,5 @@ def _init_algo(self, n_features): self._log("Using ClassicalOptimizer (COBYLA)") self._optimizer = ClassicalOptimizer(self.classical_optimizer) set_global_optimizer(self._optimizer) - + return classifier - - \ No newline at end of file From 0f8136f5014c39daee1f5008eebed0e621d18ecd Mon Sep 17 00:00:00 2001 From: gcattan Date: Tue, 5 Mar 2024 20:25:15 +0100 Subject: [PATCH 11/45] reinforce constraint on weights --- pyriemann_qiskit/utils/distance.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyriemann_qiskit/utils/distance.py b/pyriemann_qiskit/utils/distance.py index 28fdc732..17c33859 100644 --- a/pyriemann_qiskit/utils/distance.py +++ b/pyriemann_qiskit/utils/distance.py @@ -124,6 +124,11 @@ def log_prod(m1, m2): prob.set_objective("min", objective) prob.add_constraint(prob.sum(w) == 1) + for wi in w: + # reinforce constraint on lower and upper bound + prob.add_constraint(wi >= 0) + prob.add_constraint(wi <= 1) + weights = optimizer.solve(prob, reshape=False) return weights From f7cbe9fcc065baf9f25c9eb659622cb692637d56 Mon Sep 17 00:00:00 2001 From: Gregoire Cattan Date: Tue, 5 Mar 2024 21:49:05 +0100 Subject: [PATCH 12/45] - remove constraints on weights - limite size of training set - change to slsqp optimizer --- examples/ERP/classify_P300_nch_no_moabb.py | 11 +++++++++-- pyriemann_qiskit/utils/distance.py | 8 ++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/examples/ERP/classify_P300_nch_no_moabb.py b/examples/ERP/classify_P300_nch_no_moabb.py index 88a8dfe9..3a3db857 100644 --- a/examples/ERP/classify_P300_nch_no_moabb.py +++ b/examples/ERP/classify_P300_nch_no_moabb.py @@ -20,6 +20,7 @@ EPFLP300, Lee2019_ERP, ) +from qiskit_optimization.algorithms import ADMMOptimizer, SlsqpOptimizer from moabb.paradigms import P300 from sklearn.model_selection import train_test_split from pyriemann_qiskit.classification import QuanticNCH @@ -37,6 +38,8 @@ from mne import set_log_level +from imblearn.under_sampling import NearMiss + set_log_level("CRITICAL") paradigm = P300() @@ -55,9 +58,13 @@ print(X.shape) X_train, X_test, y_train, y_test = train_test_split( - X, y, test_size=0.005, random_state=42 + X, y, test_size=0.05, random_state=42 ) + train_size = 40 + X_train = X_train[0: train_size] + y_train = y_train[0: train_size] + pipelines = {} pipelines["RG+NCH"] = make_pipeline( @@ -67,7 +74,7 @@ estimator="lwf", xdawn_estimator="scm", ), - QuanticNCH(), + QuanticNCH(classical_optimizer=SlsqpOptimizer()), ) score = pipelines["RG+NCH"].fit(X_train, y_train).score(X_test, y_test) diff --git a/pyriemann_qiskit/utils/distance.py b/pyriemann_qiskit/utils/distance.py index 17c33859..b0c49cd8 100644 --- a/pyriemann_qiskit/utils/distance.py +++ b/pyriemann_qiskit/utils/distance.py @@ -124,10 +124,10 @@ def log_prod(m1, m2): prob.set_objective("min", objective) prob.add_constraint(prob.sum(w) == 1) - for wi in w: - # reinforce constraint on lower and upper bound - prob.add_constraint(wi >= 0) - prob.add_constraint(wi <= 1) + # for wi in w: + # # reinforce constraint on lower and upper bound + # prob.add_constraint(wi >= 0) + # prob.add_constraint(wi <= 1) weights = optimizer.solve(prob, reshape=False) From 60f5d58b2d5d0ac3928e9aa169423e77a8d8fdb5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 5 Mar 2024 20:49:31 +0000 Subject: [PATCH 13/45] [pre-commit.ci] auto fixes from pre-commit.com hooks --- examples/ERP/classify_P300_nch_no_moabb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ERP/classify_P300_nch_no_moabb.py b/examples/ERP/classify_P300_nch_no_moabb.py index 3a3db857..fb7782fa 100644 --- a/examples/ERP/classify_P300_nch_no_moabb.py +++ b/examples/ERP/classify_P300_nch_no_moabb.py @@ -62,8 +62,8 @@ ) train_size = 40 - X_train = X_train[0: train_size] - y_train = y_train[0: train_size] + X_train = X_train[0:train_size] + y_train = y_train[0:train_size] pipelines = {} From 9aa2fb45f9fd5896f45d8690f335ff5efca8df6f Mon Sep 17 00:00:00 2001 From: Anton Date: Wed, 6 Mar 2024 13:52:02 +0100 Subject: [PATCH 14/45] Added n_max_hull parameter. MOABB support tested. --- examples/ERP/classify_P300_nch.py | 10 ++++---- pyriemann_qiskit/classification.py | 40 ++++++++++++++++++++++++------ 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/examples/ERP/classify_P300_nch.py b/examples/ERP/classify_P300_nch.py index e0e4ded2..b927f5de 100644 --- a/examples/ERP/classify_P300_nch.py +++ b/examples/ERP/classify_P300_nch.py @@ -68,7 +68,7 @@ # reduce the number of subjects, the Quantum pipeline takes a lot of time # if executed on the entire dataset -n_subjects = 1 +n_subjects = 3 for dataset in datasets: dataset.subject_list = dataset.subject_list[0:n_subjects] @@ -78,15 +78,15 @@ # A Riemannian Quantum pipeline provided by pyRiemann-qiskit # You can choose between classical SVM and Quantum SVM. -pipelines["RG+NCH"] = make_pipeline( +pipelines["NCH"] = make_pipeline( # applies XDawn and calculates the covariance matrix, output it matrices XdawnCovariances( - nfilter=2, + nfilter=3, classes=[labels_dict["Target"]], estimator="lwf", xdawn_estimator="scm", ), - QuanticNCH(), # you can use other classifiers + QuanticNCH(n_max_hull = 50), # you can use other classifiers ) # Here we provide a pipeline for comparison: @@ -97,7 +97,7 @@ pipelines["RG+LDA"] = make_pipeline( # applies XDawn and calculates the covariance matrix, output it matrices XdawnCovariances( - nfilter=2, + nfilter=3, classes=[labels_dict["Target"]], estimator="lwf", xdawn_estimator="scm", diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index 67bc4789..eeb63598 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -24,7 +24,7 @@ from qiskit_ibm_provider import IBMProvider, least_busy from qiskit_machine_learning.algorithms import QSVC, VQC, PegasosQSVC from qiskit_machine_learning.kernels.quantum_kernel import QuantumKernel -from qiskit_optimization.algorithms import CobylaOptimizer +from qiskit_optimization.algorithms import CobylaOptimizer, ADMMOptimizer, SlsqpOptimizer from sklearn.base import BaseEstimator, ClassifierMixin from sklearn.svm import SVC @@ -751,9 +751,10 @@ def predict(self, X): class NearestConvexHull(BaseEstimator, ClassifierMixin, TransformerMixin): - def __init__(self, n_jobs=1): + def __init__(self, n_jobs=12, n_max_hull = 30): """Init.""" self.n_jobs = n_jobs + self.n_max_hull = n_max_hull self.matrices_per_class_ = {} def fit(self, X, y): @@ -788,12 +789,29 @@ def fit(self, X, y): for c in self.classes_: self.matrices_per_class_[c] = [] + max_train_samples_per_class = self.n_max_hull + class_0_count = 0 + class_1_count = 1 + for i in range(0, len(y)): - self.matrices_per_class_[y[i]].append(X[i, :, :]) + + if (y[i] == 0): + class_0_count = class_0_count + 1 + if class_0_count < max_train_samples_per_class: + self.matrices_per_class_[y[i]].append(X[i, :, :]) + elif (y[i] == 1): + class_1_count = class_1_count + 1 + if class_1_count < max_train_samples_per_class: + self.matrices_per_class_[y[i]].append(X[i, :, :]) + + # for i in range(0, len(y)): + # self.matrices_per_class_[y[i]].append(X[i, :, :]) for c in self.classes_: self.matrices_per_class_[c] = np.array(self.matrices_per_class_[c]) - + + print("Class 1 for real training: ", self.matrices_per_class_[0].shape[0]) + print("Class 2 for real training: ", self.matrices_per_class_[1].shape[0]) print("End NCH Train") def _process_sample(self, test_sample): @@ -844,7 +862,7 @@ def predict(self, X): parallel = True if parallel: - pred = Parallel(n_jobs=12)( + pred = Parallel(n_jobs=self.n_jobs)( delayed(self._process_sample)(test_sample) for test_sample in X ) @@ -927,7 +945,9 @@ def __init__( seed=None, upper_bound=7, regularization=None, - classical_optimizer=CobylaOptimizer(rhobeg=2.1, rhoend=0.000001), + #classical_optimizer=CobylaOptimizer(rhobeg=2.1, rhoend=0.000001), + classical_optimizer=SlsqpOptimizer(), + n_max_hull = 30 ): QuanticClassifierBase.__init__( self, quantum, q_account_token, verbose, shots, None, seed @@ -935,11 +955,12 @@ def __init__( self.upper_bound = upper_bound self.regularization = regularization self.classical_optimizer = classical_optimizer + self.n_max_hull = n_max_hull def _init_algo(self, n_features): self._log("Nearest Convex Hull Classifier initiating algorithm") - classifier = NearestConvexHull() + classifier = NearestConvexHull(n_max_hull = self.n_max_hull) if self.quantum: self._log("Using NaiveQAOAOptimizer") @@ -947,8 +968,11 @@ def _init_algo(self, n_features): quantum_instance=self._quantum_instance, upper_bound=self.upper_bound ) else: - self._log("Using ClassicalOptimizer (COBYLA)") + self._log("Using ClassicalOptimizer") self._optimizer = ClassicalOptimizer(self.classical_optimizer) set_global_optimizer(self._optimizer) return classifier + + def predict(self,X): + return self._predict(X) From 66aaca1739fe14e239f0e162d2b57110d8fdc2bc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 6 Mar 2024 12:53:04 +0000 Subject: [PATCH 15/45] [pre-commit.ci] auto fixes from pre-commit.com hooks --- examples/ERP/classify_P300_nch.py | 2 +- pyriemann_qiskit/classification.py | 29 ++++++++++++++++------------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/examples/ERP/classify_P300_nch.py b/examples/ERP/classify_P300_nch.py index b927f5de..cb30eac9 100644 --- a/examples/ERP/classify_P300_nch.py +++ b/examples/ERP/classify_P300_nch.py @@ -86,7 +86,7 @@ estimator="lwf", xdawn_estimator="scm", ), - QuanticNCH(n_max_hull = 50), # you can use other classifiers + QuanticNCH(n_max_hull=50), # you can use other classifiers ) # Here we provide a pipeline for comparison: diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index eeb63598..6234cbe0 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -24,7 +24,11 @@ from qiskit_ibm_provider import IBMProvider, least_busy from qiskit_machine_learning.algorithms import QSVC, VQC, PegasosQSVC from qiskit_machine_learning.kernels.quantum_kernel import QuantumKernel -from qiskit_optimization.algorithms import CobylaOptimizer, ADMMOptimizer, SlsqpOptimizer +from qiskit_optimization.algorithms import ( + CobylaOptimizer, + ADMMOptimizer, + SlsqpOptimizer, +) from sklearn.base import BaseEstimator, ClassifierMixin from sklearn.svm import SVC @@ -751,7 +755,7 @@ def predict(self, X): class NearestConvexHull(BaseEstimator, ClassifierMixin, TransformerMixin): - def __init__(self, n_jobs=12, n_max_hull = 30): + def __init__(self, n_jobs=12, n_max_hull=30): """Init.""" self.n_jobs = n_jobs self.n_max_hull = n_max_hull @@ -792,24 +796,23 @@ def fit(self, X, y): max_train_samples_per_class = self.n_max_hull class_0_count = 0 class_1_count = 1 - + for i in range(0, len(y)): - - if (y[i] == 0): + if y[i] == 0: class_0_count = class_0_count + 1 if class_0_count < max_train_samples_per_class: self.matrices_per_class_[y[i]].append(X[i, :, :]) - elif (y[i] == 1): + elif y[i] == 1: class_1_count = class_1_count + 1 if class_1_count < max_train_samples_per_class: self.matrices_per_class_[y[i]].append(X[i, :, :]) - + # for i in range(0, len(y)): # self.matrices_per_class_[y[i]].append(X[i, :, :]) for c in self.classes_: self.matrices_per_class_[c] = np.array(self.matrices_per_class_[c]) - + print("Class 1 for real training: ", self.matrices_per_class_[0].shape[0]) print("Class 2 for real training: ", self.matrices_per_class_[1].shape[0]) print("End NCH Train") @@ -945,9 +948,9 @@ def __init__( seed=None, upper_bound=7, regularization=None, - #classical_optimizer=CobylaOptimizer(rhobeg=2.1, rhoend=0.000001), + # classical_optimizer=CobylaOptimizer(rhobeg=2.1, rhoend=0.000001), classical_optimizer=SlsqpOptimizer(), - n_max_hull = 30 + n_max_hull=30, ): QuanticClassifierBase.__init__( self, quantum, q_account_token, verbose, shots, None, seed @@ -960,7 +963,7 @@ def __init__( def _init_algo(self, n_features): self._log("Nearest Convex Hull Classifier initiating algorithm") - classifier = NearestConvexHull(n_max_hull = self.n_max_hull) + classifier = NearestConvexHull(n_max_hull=self.n_max_hull) if self.quantum: self._log("Using NaiveQAOAOptimizer") @@ -973,6 +976,6 @@ def _init_algo(self, n_features): set_global_optimizer(self._optimizer) return classifier - - def predict(self,X): + + def predict(self, X): return self._predict(X) From f4f02bfb54e377f1f568dbe93c8921b6a8c7a54c Mon Sep 17 00:00:00 2001 From: Anton Date: Wed, 6 Mar 2024 19:47:48 +0100 Subject: [PATCH 16/45] added multiple hulls. --- examples/ERP/classify_P300_nch.py | 4 +- pyriemann_qiskit/classification.py | 76 +++++++++++++++--------------- 2 files changed, 41 insertions(+), 39 deletions(-) diff --git a/examples/ERP/classify_P300_nch.py b/examples/ERP/classify_P300_nch.py index b927f5de..5ed6415c 100644 --- a/examples/ERP/classify_P300_nch.py +++ b/examples/ERP/classify_P300_nch.py @@ -70,7 +70,7 @@ # if executed on the entire dataset n_subjects = 3 for dataset in datasets: - dataset.subject_list = dataset.subject_list[0:n_subjects] + dataset.subject_list = dataset.subject_list[5:8] overwrite = True # set to True if we want to overwrite cached results @@ -86,7 +86,7 @@ estimator="lwf", xdawn_estimator="scm", ), - QuanticNCH(n_max_hull = 50), # you can use other classifiers + QuanticNCH(n_max_hull = 15), # you can use other classifiers ) # Here we provide a pipeline for comparison: diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index eeb63598..8def710d 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -32,6 +32,7 @@ from .utils import get_provider, get_devices, get_simulator from .utils.distance import qdistance_logeuclid_to_convex_hull from joblib import Parallel, delayed +import random logger.level = logging.WARNING @@ -789,23 +790,23 @@ def fit(self, X, y): for c in self.classes_: self.matrices_per_class_[c] = [] - max_train_samples_per_class = self.n_max_hull - class_0_count = 0 - class_1_count = 1 + # max_train_samples_per_class = self.n_max_hull + # class_0_count = 0 + # class_1_count = 1 - for i in range(0, len(y)): + # for i in range(0, len(y)): - if (y[i] == 0): - class_0_count = class_0_count + 1 - if class_0_count < max_train_samples_per_class: - self.matrices_per_class_[y[i]].append(X[i, :, :]) - elif (y[i] == 1): - class_1_count = class_1_count + 1 - if class_1_count < max_train_samples_per_class: - self.matrices_per_class_[y[i]].append(X[i, :, :]) + # if (y[i] == 0): + # class_0_count = class_0_count + 1 + # if class_0_count < max_train_samples_per_class: + # self.matrices_per_class_[y[i]].append(X[i, :, :]) + # elif (y[i] == 1): + # class_1_count = class_1_count + 1 + # if class_1_count < max_train_samples_per_class: + # self.matrices_per_class_[y[i]].append(X[i, :, :]) - # for i in range(0, len(y)): - # self.matrices_per_class_[y[i]].append(X[i, :, :]) + for i in range(0, len(y)): + self.matrices_per_class_[y[i]].append(X[i, :, :]) for c in self.classes_: self.matrices_per_class_[c] = np.array(self.matrices_per_class_[c]) @@ -819,10 +820,27 @@ def _process_sample(self, test_sample): best_class = -1 for c in self.classes_: - distance = qdistance_logeuclid_to_convex_hull( - self.matrices_per_class_[c], test_sample - ) - # print("Distance: ", distance) + + # distance = qdistance_logeuclid_to_convex_hull( + # self.matrices_per_class_[c], test_sample + # ) + + #start using multiple hulls + total_distance = 0 + + for i in range(0,3): + + random_samples = random.sample(range(self.matrices_per_class_[c].shape[0]), k = self.n_max_hull) + + hull_data = self.matrices_per_class_[c][random_samples,:,:] + + distance = qdistance_logeuclid_to_convex_hull( + hull_data, test_sample + ) + total_distance = total_distance + distance + + distance = total_distance #TODO: improve + #end using multiple hulls if best_distance == -1: best_distance = distance @@ -857,7 +875,6 @@ def predict(self, X): # return labels print("Total test samples:", X.shape[0]) - k = 0 parallel = True @@ -867,26 +884,11 @@ def predict(self, X): ) else: + k = 0 for test_sample in X: k = k + 1 - print("Processing sample:", k, " / ", X.shape[0]) - - best_distance = -1 - best_class = -1 - - for c in self.classes_: - distance = qdistance_logeuclid_to_convex_hull( - self.matrices_per_class_[c], test_sample - ) - print("Distance: ", distance) - - if best_distance == -1: - best_distance = distance - best_class = c - elif distance < best_distance: - best_distance = distance - best_class = c - + print(k) + best_class = self._process_sample(test_sample) pred.append(best_class) print("Predicted: ", best_class) From c9357cb9d550fb9e346cbe9d313f7356a0ee4f1b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 7 Mar 2024 13:52:17 +0000 Subject: [PATCH 17/45] [pre-commit.ci] auto fixes from pre-commit.com hooks --- examples/ERP/classify_P300_nch.py | 2 +- examples/ERP/classify_P300_nch_no_moabb.py | 66 +++++++++++++--------- pyriemann_qiskit/classification.py | 58 ++++++++++--------- 3 files changed, 69 insertions(+), 57 deletions(-) diff --git a/examples/ERP/classify_P300_nch.py b/examples/ERP/classify_P300_nch.py index 25f23e7f..d9577da4 100644 --- a/examples/ERP/classify_P300_nch.py +++ b/examples/ERP/classify_P300_nch.py @@ -86,7 +86,7 @@ estimator="lwf", xdawn_estimator="scm", ), - QuanticNCH(n_max_hull = 15), # you can use other classifiers + QuanticNCH(n_max_hull=15), # you can use other classifiers ) # Here we provide a pipeline for comparison: diff --git a/examples/ERP/classify_P300_nch_no_moabb.py b/examples/ERP/classify_P300_nch_no_moabb.py index 8cdee3a2..5e27e559 100644 --- a/examples/ERP/classify_P300_nch_no_moabb.py +++ b/examples/ERP/classify_P300_nch_no_moabb.py @@ -47,56 +47,66 @@ le = LabelEncoder() + # Returns a Test dataset that contains an equal amounts of each class # y should contain only two classes 0 and 1 -def SplitEqual(X, y, samples_n, train_samples_n): #samples_n per class - +def SplitEqual(X, y, samples_n, train_samples_n): # samples_n per class indicesClass1 = [] indicesClass2 = [] - + for i in range(0, len(y)): if y[i] == 0 and len(indicesClass1) < samples_n: indicesClass1.append(i) elif y[i] == 1 and len(indicesClass2) < samples_n: indicesClass2.append(i) - + if len(indicesClass1) == samples_n and len(indicesClass2) == samples_n: break - + X_test_class1 = X[indicesClass1] X_test_class2 = X[indicesClass2] - - X_test = np.concatenate((X_test_class1,X_test_class2), axis=0) - - #remove x_test from X + + X_test = np.concatenate((X_test_class1, X_test_class2), axis=0) + + # remove x_test from X X_train = np.delete(X, indicesClass1 + indicesClass2, axis=0) - + Y_test_class1 = y[indicesClass1] Y_test_class2 = y[indicesClass2] - - y_test = np.concatenate((Y_test_class1,Y_test_class2), axis=0) - - #remove y_test from y + + y_test = np.concatenate((Y_test_class1, Y_test_class2), axis=0) + + # remove y_test from y y_train = np.delete(y, indicesClass1 + indicesClass2, axis=0) - - if (X_test.shape[0] != 2 * samples_n or y_test.shape[0] != 2 * samples_n): + + if X_test.shape[0] != 2 * samples_n or y_test.shape[0] != 2 * samples_n: raise Exception("Problem with split 1!") - - if (X_train.shape[0] + X_test.shape[0] != X.shape[0] or y_train.shape[0] + y_test.shape[0] != y.shape[0]): + + if ( + X_train.shape[0] + X_test.shape[0] != X.shape[0] + or y_train.shape[0] + y_test.shape[0] != y.shape[0] + ): raise Exception("Problem with split 2!") - + #################################################### X_train_1 = X_train[(y_train == 1)] X_train_1 = X_train_1[0:train_samples_n] X_train_2 = X_train[(y_train == 0)] - X_train_2 = X_train_2[0:train_samples_n,:,:] - - X_train = np.concatenate((X_train_1, X_train_2), axis=0) - - y_train = np.concatenate((np.ones(train_samples_n, dtype = np.int8), np.zeros(train_samples_n, dtype = np.int8)), axis=0) - + X_train_2 = X_train_2[0:train_samples_n, :, :] + + X_train = np.concatenate((X_train_1, X_train_2), axis=0) + + y_train = np.concatenate( + ( + np.ones(train_samples_n, dtype=np.int8), + np.zeros(train_samples_n, dtype=np.int8), + ), + axis=0, + ) + return X_train, X_test, y_train, y_test + db = BNCI2014009() # BNCI2014008() n_subjects = 4 @@ -116,7 +126,7 @@ def SplitEqual(X, y, samples_n, train_samples_n): #samples_n per class print("Test Class 1 count:", sum(y_test)) print("Test Class 2 count:", len(y_test) - sum(y_test)) - + # train_size = 350 # X_train = X_train[0:train_size] # y_train = y_train[0:train_size] @@ -133,8 +143,8 @@ def SplitEqual(X, y, samples_n, train_samples_n): #samples_n per class QuanticNCH(classical_optimizer=SlsqpOptimizer()), ) - #score = pipelines["RG+NCH"].fit(X_train, y_train).score(X_test, y_test) - #print("Classification score - subject, score:", subject_i, " , ", score) + # score = pipelines["RG+NCH"].fit(X_train, y_train).score(X_test, y_test) + # print("Classification score - subject, score:", subject_i, " , ", score) pipelines["RG+NCH"].fit(X_train, y_train) y_pred = pipelines["RG+NCH"].predict(X_test) print("Prediction: ", y_pred) diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index 18737a5f..2db96361 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -24,7 +24,11 @@ from qiskit_ibm_provider import IBMProvider, least_busy from qiskit_machine_learning.algorithms import QSVC, VQC, PegasosQSVC from qiskit_machine_learning.kernels.quantum_kernel import QuantumKernel -from qiskit_optimization.algorithms import CobylaOptimizer, ADMMOptimizer, SlsqpOptimizer +from qiskit_optimization.algorithms import ( + CobylaOptimizer, + ADMMOptimizer, + SlsqpOptimizer, +) from sklearn.base import BaseEstimator, ClassifierMixin from sklearn.svm import SVC @@ -752,7 +756,7 @@ def predict(self, X): class NearestConvexHull(BaseEstimator, ClassifierMixin, TransformerMixin): - def __init__(self, n_jobs=12, n_max_hull = 30): + def __init__(self, n_jobs=12, n_max_hull=30): """Init.""" self.n_jobs = n_jobs self.n_max_hull = n_max_hull @@ -793,9 +797,9 @@ def fit(self, X, y): # max_train_samples_per_class = self.n_max_hull # class_0_count = 0 # class_1_count = 1 - + # for i in range(0, len(y)): - + # if (y[i] == 0): # class_0_count = class_0_count + 1 # if class_0_count < max_train_samples_per_class: @@ -804,15 +808,15 @@ def fit(self, X, y): # class_1_count = class_1_count + 1 # if class_1_count < max_train_samples_per_class: # self.matrices_per_class_[y[i]].append(X[i, :, :]) - + for i in range(0, len(y)): self.matrices_per_class_[y[i]].append(X[i, :, :]) for c in self.classes_: self.matrices_per_class_[c] = np.array(self.matrices_per_class_[c]) - - #print("Class 1 for real training: ", self.matrices_per_class_[0].shape[0]) - #print("Class 2 for real training: ", self.matrices_per_class_[1].shape[0]) + + # print("Class 1 for real training: ", self.matrices_per_class_[0].shape[0]) + # print("Class 2 for real training: ", self.matrices_per_class_[1].shape[0]) print("End NCH Train") def _process_sample(self, test_sample): @@ -820,27 +824,25 @@ def _process_sample(self, test_sample): best_class = -1 for c in self.classes_: - # distance = qdistance_logeuclid_to_convex_hull( # self.matrices_per_class_[c], test_sample # ) - - #start using multiple hulls + + # start using multiple hulls total_distance = 0 - - for i in range(0,3): - - random_samples = random.sample(range(self.matrices_per_class_[c].shape[0]), k = self.n_max_hull) - - hull_data = self.matrices_per_class_[c][random_samples,:,:] - - distance = qdistance_logeuclid_to_convex_hull( - hull_data, test_sample + + for i in range(0, 3): + random_samples = random.sample( + range(self.matrices_per_class_[c].shape[0]), k=self.n_max_hull ) + + hull_data = self.matrices_per_class_[c][random_samples, :, :] + + distance = qdistance_logeuclid_to_convex_hull(hull_data, test_sample) total_distance = total_distance + distance - - distance = total_distance #TODO: improve - #end using multiple hulls + + distance = total_distance # TODO: improve + # end using multiple hulls if best_distance == -1: best_distance = distance @@ -947,9 +949,9 @@ def __init__( seed=None, upper_bound=7, regularization=None, - #classical_optimizer=CobylaOptimizer(rhobeg=2.1, rhoend=0.000001), + # classical_optimizer=CobylaOptimizer(rhobeg=2.1, rhoend=0.000001), classical_optimizer=SlsqpOptimizer(), - n_max_hull = 30 + n_max_hull=30, ): QuanticClassifierBase.__init__( self, quantum, q_account_token, verbose, shots, None, seed @@ -962,7 +964,7 @@ def __init__( def _init_algo(self, n_features): self._log("Nearest Convex Hull Classifier initiating algorithm") - classifier = NearestConvexHull(n_max_hull = self.n_max_hull) + classifier = NearestConvexHull(n_max_hull=self.n_max_hull) if self.quantum: self._log("Using NaiveQAOAOptimizer") @@ -975,6 +977,6 @@ def _init_algo(self, n_features): set_global_optimizer(self._optimizer) return classifier - - def predict(self,X): + + def predict(self, X): return self._predict(X) From 8ab59afb0082f9ade8a0f0365a7f9f99543e8f65 Mon Sep 17 00:00:00 2001 From: Anton Date: Thu, 7 Mar 2024 15:53:01 +0100 Subject: [PATCH 18/45] Code cleanups. Added second parameter that specifies the number of hulls. --- examples/ERP/classify_P300_nch.py | 12 ++--- pyriemann_qiskit/classification.py | 70 +++++++++--------------------- 2 files changed, 25 insertions(+), 57 deletions(-) diff --git a/examples/ERP/classify_P300_nch.py b/examples/ERP/classify_P300_nch.py index d9577da4..c346d3a2 100644 --- a/examples/ERP/classify_P300_nch.py +++ b/examples/ERP/classify_P300_nch.py @@ -1,14 +1,10 @@ """ ==================================================================== -Classification of P300 datasets from MOABB +Classification of P300 datasets from MOABB using NCH ==================================================================== -It demonstrates the QuantumClassifierWithDefaultRiemannianPipeline(). This -pipeline uses Riemannian Geometry, Tangent Space and a quantum SVM -classifier. MOABB is used to access many EEG datasets and also for the -evaluation and comparison with other classifiers. +Demonstrates classification with QunatumNCH. -In QuantumClassifierWithDefaultRiemannianPipeline(): If parameter "shots" is None then a classical SVM is used similar to the one in scikit learn. If "shots" is not None and IBM Qunatum token is provided with "q_account_token" @@ -68,7 +64,7 @@ # reduce the number of subjects, the Quantum pipeline takes a lot of time # if executed on the entire dataset -n_subjects = 3 +n_subjects = 1 for dataset in datasets: dataset.subject_list = dataset.subject_list[0:n_subjects] @@ -86,7 +82,7 @@ estimator="lwf", xdawn_estimator="scm", ), - QuanticNCH(n_max_hull=15), # you can use other classifiers + QuanticNCH(n_hulls=3, n_samples_per_hull=15), # you can use other classifiers ) # Here we provide a pipeline for comparison: diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index 2db96361..4453c0e0 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -756,10 +756,12 @@ def predict(self, X): class NearestConvexHull(BaseEstimator, ClassifierMixin, TransformerMixin): - def __init__(self, n_jobs=12, n_max_hull=30): + + def __init__(self, n_jobs=12, n_hulls=3, n_samples_per_hull=30): """Init.""" self.n_jobs = n_jobs - self.n_max_hull = n_max_hull + self.n_samples_per_hull = n_samples_per_hull + self.n_hulls = n_hulls self.matrices_per_class_ = {} def fit(self, X, y): @@ -781,42 +783,21 @@ def fit(self, X, y): """ print("Start NCH Train") - - # self.covmeans_ = X - # self.classmeans_ = y self.classes_ = np.unique(y) - print("Classes:", self.classes_) - print("Class 1", sum(y)) - print("Class 2", len(y) - sum(y)) - if max(self.classes_) > 1: - raise Exception("Currently designed for two classes 0 et 1") - + for c in self.classes_: self.matrices_per_class_[c] = [] - # max_train_samples_per_class = self.n_max_hull - # class_0_count = 0 - # class_1_count = 1 - - # for i in range(0, len(y)): - - # if (y[i] == 0): - # class_0_count = class_0_count + 1 - # if class_0_count < max_train_samples_per_class: - # self.matrices_per_class_[y[i]].append(X[i, :, :]) - # elif (y[i] == 1): - # class_1_count = class_1_count + 1 - # if class_1_count < max_train_samples_per_class: - # self.matrices_per_class_[y[i]].append(X[i, :, :]) - for i in range(0, len(y)): self.matrices_per_class_[y[i]].append(X[i, :, :]) for c in self.classes_: self.matrices_per_class_[c] = np.array(self.matrices_per_class_[c]) + + print("Samples per class:") + for c in self.classes_: + print("Class: ", c , " Count: ", self.matrices_per_class_[c].shape[0]) - # print("Class 1 for real training: ", self.matrices_per_class_[0].shape[0]) - # print("Class 2 for real training: ", self.matrices_per_class_[1].shape[0]) print("End NCH Train") def _process_sample(self, test_sample): @@ -824,16 +805,13 @@ def _process_sample(self, test_sample): best_class = -1 for c in self.classes_: - # distance = qdistance_logeuclid_to_convex_hull( - # self.matrices_per_class_[c], test_sample - # ) - - # start using multiple hulls + total_distance = 0 - for i in range(0, 3): + #using multiple hulls + for i in range(0, self.n_hulls): random_samples = random.sample( - range(self.matrices_per_class_[c].shape[0]), k=self.n_max_hull + range(self.matrices_per_class_[c].shape[0]), k=self.n_samples_per_hull ) hull_data = self.matrices_per_class_[c][random_samples, :, :] @@ -841,14 +819,11 @@ def _process_sample(self, test_sample): distance = qdistance_logeuclid_to_convex_hull(hull_data, test_sample) total_distance = total_distance + distance - distance = total_distance # TODO: improve - # end using multiple hulls - if best_distance == -1: - best_distance = distance + best_distance = total_distance best_class = c - elif distance < best_distance: - best_distance = distance + elif total_distance < best_distance: + best_distance = total_distance best_class = c return best_class @@ -871,11 +846,6 @@ def predict(self, X): print("Start NCH Predict") pred = [] - # testing only!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - # labels = [1] * X.shape[0] - # print("End NCH Predict") - # return labels - print("Total test samples:", X.shape[0]) parallel = True @@ -951,7 +921,8 @@ def __init__( regularization=None, # classical_optimizer=CobylaOptimizer(rhobeg=2.1, rhoend=0.000001), classical_optimizer=SlsqpOptimizer(), - n_max_hull=30, + n_hulls = 3, + n_samples_per_hull=10, ): QuanticClassifierBase.__init__( self, quantum, q_account_token, verbose, shots, None, seed @@ -959,12 +930,13 @@ def __init__( self.upper_bound = upper_bound self.regularization = regularization self.classical_optimizer = classical_optimizer - self.n_max_hull = n_max_hull + self.n_hulls = n_hulls + self.n_samples_per_hull = n_samples_per_hull def _init_algo(self, n_features): self._log("Nearest Convex Hull Classifier initiating algorithm") - classifier = NearestConvexHull(n_max_hull=self.n_max_hull) + classifier = NearestConvexHull(n_hulls=self.n_hulls, n_samples_per_hull=self.n_samples_per_hull) if self.quantum: self._log("Using NaiveQAOAOptimizer") From 3ae1b1ec01a65ffc70e1dcb47babe07387eebee8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 7 Mar 2024 14:55:08 +0000 Subject: [PATCH 19/45] [pre-commit.ci] auto fixes from pre-commit.com hooks --- examples/ERP/classify_P300_nch.py | 2 +- pyriemann_qiskit/classification.py | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/examples/ERP/classify_P300_nch.py b/examples/ERP/classify_P300_nch.py index c346d3a2..3c8775a8 100644 --- a/examples/ERP/classify_P300_nch.py +++ b/examples/ERP/classify_P300_nch.py @@ -3,7 +3,7 @@ Classification of P300 datasets from MOABB using NCH ==================================================================== -Demonstrates classification with QunatumNCH. +Demonstrates classification with QunatumNCH. If parameter "shots" is None then a classical SVM is used similar to the one in scikit learn. diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index 4453c0e0..4d5986f9 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -756,7 +756,6 @@ def predict(self, X): class NearestConvexHull(BaseEstimator, ClassifierMixin, TransformerMixin): - def __init__(self, n_jobs=12, n_hulls=3, n_samples_per_hull=30): """Init.""" self.n_jobs = n_jobs @@ -784,7 +783,7 @@ def fit(self, X, y): print("Start NCH Train") self.classes_ = np.unique(y) - + for c in self.classes_: self.matrices_per_class_[c] = [] @@ -793,10 +792,10 @@ def fit(self, X, y): for c in self.classes_: self.matrices_per_class_[c] = np.array(self.matrices_per_class_[c]) - + print("Samples per class:") for c in self.classes_: - print("Class: ", c , " Count: ", self.matrices_per_class_[c].shape[0]) + print("Class: ", c, " Count: ", self.matrices_per_class_[c].shape[0]) print("End NCH Train") @@ -805,13 +804,13 @@ def _process_sample(self, test_sample): best_class = -1 for c in self.classes_: - total_distance = 0 - #using multiple hulls + # using multiple hulls for i in range(0, self.n_hulls): random_samples = random.sample( - range(self.matrices_per_class_[c].shape[0]), k=self.n_samples_per_hull + range(self.matrices_per_class_[c].shape[0]), + k=self.n_samples_per_hull, ) hull_data = self.matrices_per_class_[c][random_samples, :, :] @@ -921,7 +920,7 @@ def __init__( regularization=None, # classical_optimizer=CobylaOptimizer(rhobeg=2.1, rhoend=0.000001), classical_optimizer=SlsqpOptimizer(), - n_hulls = 3, + n_hulls=3, n_samples_per_hull=10, ): QuanticClassifierBase.__init__( @@ -936,7 +935,9 @@ def __init__( def _init_algo(self, n_features): self._log("Nearest Convex Hull Classifier initiating algorithm") - classifier = NearestConvexHull(n_hulls=self.n_hulls, n_samples_per_hull=self.n_samples_per_hull) + classifier = NearestConvexHull( + n_hulls=self.n_hulls, n_samples_per_hull=self.n_samples_per_hull + ) if self.quantum: self._log("Using NaiveQAOAOptimizer") From d7c6e1a181060dc19e6b5fec1b6703ae284e25ee Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 8 Mar 2024 12:13:20 +0100 Subject: [PATCH 20/45] Improved code. Added support for transform(). Added a new pipeline [NCH+LDA] --- examples/ERP/classify_P300_nch.py | 22 +++++-- pyriemann_qiskit/classification.py | 100 +++++++++++++++++------------ 2 files changed, 77 insertions(+), 45 deletions(-) diff --git a/examples/ERP/classify_P300_nch.py b/examples/ERP/classify_P300_nch.py index 3c8775a8..56e366cc 100644 --- a/examples/ERP/classify_P300_nch.py +++ b/examples/ERP/classify_P300_nch.py @@ -27,7 +27,7 @@ from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA from moabb import set_log_level from moabb.datasets import bi2012, BNCI2014009, bi2013a -from moabb.evaluations import WithinSessionEvaluation +from moabb.evaluations import WithinSessionEvaluation, CrossSubjectEvaluation from moabb.paradigms import P300 from pyriemann_qiskit.pipelines import ( QuantumClassifierWithDefaultRiemannianPipeline, @@ -72,9 +72,20 @@ pipelines = {} -# A Riemannian Quantum pipeline provided by pyRiemann-qiskit -# You can choose between classical SVM and Quantum SVM. -pipelines["NCH"] = make_pipeline( +# NCH as a classifier +# pipelines["NCH"] = make_pipeline( +# # applies XDawn and calculates the covariance matrix, output it matrices +# XdawnCovariances( +# nfilter=3, +# classes=[labels_dict["Target"]], +# estimator="lwf", +# xdawn_estimator="scm", +# ), +# QuanticNCH(n_hulls=1, n_samples_per_hull=3), # you can use other classifiers +# ) + +#NCH as a transformer +pipelines["NCH+LDA"] = make_pipeline( # applies XDawn and calculates the covariance matrix, output it matrices XdawnCovariances( nfilter=3, @@ -82,7 +93,8 @@ estimator="lwf", xdawn_estimator="scm", ), - QuanticNCH(n_hulls=3, n_samples_per_hull=15), # you can use other classifiers + QuanticNCH(n_hulls=1, n_samples_per_hull=3), + LDA(solver="lsqr", shrinkage="auto"), ) # Here we provide a pipeline for comparison: diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index 4d5986f9..5d1400d3 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -799,11 +799,35 @@ def fit(self, X, y): print("End NCH Train") - def _process_sample(self, test_sample): - best_distance = -1 - best_class = -1 + def _predict_distances(self, X): + """Helper to predict the distance. Equivalent to transform.""" + dist = [] + + print("Total test samples:", X.shape[0]) + parallel = True + + if parallel: + dist = Parallel(n_jobs=self.n_jobs)( + delayed(self._process_sample)(test_sample) for test_sample in X + ) + + else: + #k = 0 + for test_sample in X: + #k = k + 1 + #print(k) + dist_sample = self._process_sample(test_sample) + dist.append(dist_sample) + + return dist + + def _process_sample(self, test_sample): + + distances=[] + for c in self.classes_: + total_distance = 0 # using multiple hulls @@ -818,53 +842,44 @@ def _process_sample(self, test_sample): distance = qdistance_logeuclid_to_convex_hull(hull_data, test_sample) total_distance = total_distance + distance - if best_distance == -1: - best_distance = total_distance - best_class = c - elif total_distance < best_distance: - best_distance = total_distance - best_class = c + distances.append(total_distance) - return best_class + return distances def predict(self, X): - """Calculates the predictions. - + """Get the predictions. Parameters ---------- - X : ndarray, shape (n_samples, n_features) - Input vector, where `n_samples` is the number of samples and - `n_features` is the number of features. - + X : ndarray, shape (n_matrices, n_channels, n_channels) + Set of SPD matrices. Returns ------- - pred : array, shape (n_samples,) - Class labels for samples in X. + pred : ndarray of int, shape (n_matrices,) + Predictions for each matrix according to the closest convex hull. """ - print("Start NCH Predict") - pred = [] - - print("Total test samples:", X.shape[0]) - - parallel = True - - if parallel: - pred = Parallel(n_jobs=self.n_jobs)( - delayed(self._process_sample)(test_sample) for test_sample in X - ) - - else: - k = 0 - for test_sample in X: - k = k + 1 - print(k) - best_class = self._process_sample(test_sample) - pred.append(best_class) - print("Predicted: ", best_class) - + dist = self._predict_distances(X) + + predictions = [self.classes_[min(range(len(values)), key=values.__getitem__)] for values in dist] + #print(predictions) + return predictions + + #return self.classes_[dist.argmin(axis=1)] print("End NCH Predict") - return np.array(pred) + + def transform(self, X): + """Get the distance to each convex hull. + Parameters + ---------- + X : ndarray, shape (n_matrices, n_channels, n_channels) + Set of SPD matrices. + Returns + ------- + dist : ndarray, shape (n_matrices, n_classes) + The distance to each convex hull. + """ + print("NCH Transform") + return self._predict_distances(X) class QuanticNCH(QuanticClassifierBase): @@ -952,4 +967,9 @@ def _init_algo(self, n_features): return classifier def predict(self, X): + print("QuanticNCH Predict") return self._predict(X) + + def transform(self, X): + print("QuanticNCH Transform") + return self._classifier.transform(X) From 4e8be611156a642531f03c97aa87d5d23a6fd592 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 8 Mar 2024 11:13:54 +0000 Subject: [PATCH 21/45] [pre-commit.ci] auto fixes from pre-commit.com hooks --- examples/ERP/classify_P300_nch.py | 2 +- pyriemann_qiskit/classification.py | 33 +++++++++++++++--------------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/examples/ERP/classify_P300_nch.py b/examples/ERP/classify_P300_nch.py index 56e366cc..c679e7c5 100644 --- a/examples/ERP/classify_P300_nch.py +++ b/examples/ERP/classify_P300_nch.py @@ -84,7 +84,7 @@ # QuanticNCH(n_hulls=1, n_samples_per_hull=3), # you can use other classifiers # ) -#NCH as a transformer +# NCH as a transformer pipelines["NCH+LDA"] = make_pipeline( # applies XDawn and calculates the covariance matrix, output it matrices XdawnCovariances( diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index 5d1400d3..52060cd5 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -813,21 +813,19 @@ def _predict_distances(self, X): ) else: - #k = 0 + # k = 0 for test_sample in X: - #k = k + 1 - #print(k) + # k = k + 1 + # print(k) dist_sample = self._process_sample(test_sample) dist.append(dist_sample) - + return dist - + def _process_sample(self, test_sample): - - distances=[] - + distances = [] + for c in self.classes_: - total_distance = 0 # using multiple hulls @@ -859,14 +857,17 @@ def predict(self, X): """ print("Start NCH Predict") dist = self._predict_distances(X) - - predictions = [self.classes_[min(range(len(values)), key=values.__getitem__)] for values in dist] - #print(predictions) + + predictions = [ + self.classes_[min(range(len(values)), key=values.__getitem__)] + for values in dist + ] + # print(predictions) return predictions - - #return self.classes_[dist.argmin(axis=1)] + + # return self.classes_[dist.argmin(axis=1)] print("End NCH Predict") - + def transform(self, X): """Get the distance to each convex hull. Parameters @@ -969,7 +970,7 @@ def _init_algo(self, n_features): def predict(self, X): print("QuanticNCH Predict") return self._predict(X) - + def transform(self, X): print("QuanticNCH Transform") return self._classifier.transform(X) From f4f836cc50ce16553cbf122ca632b61724d03d10 Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 8 Mar 2024 18:18:08 +0100 Subject: [PATCH 22/45] updated default parameters --- examples/ERP/classify_P300_nch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ERP/classify_P300_nch.py b/examples/ERP/classify_P300_nch.py index c679e7c5..dfa8fff0 100644 --- a/examples/ERP/classify_P300_nch.py +++ b/examples/ERP/classify_P300_nch.py @@ -93,7 +93,7 @@ estimator="lwf", xdawn_estimator="scm", ), - QuanticNCH(n_hulls=1, n_samples_per_hull=3), + QuanticNCH(n_hulls=3, n_samples_per_hull=15), LDA(solver="lsqr", shrinkage="auto"), ) From 4d20109bd54a766947a0bc02d3e253f852851a89 Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 12 Mar 2024 13:18:13 +0100 Subject: [PATCH 23/45] General improvements. Improvements requested by GC. --- examples/ERP/classify_P300_nch.py | 33 +---- examples/ERP/classify_P300_nch_no_moabb.py | 152 --------------------- pyriemann_qiskit/classification.py | 73 ++++++---- pyriemann_qiskit/utils/distance.py | 4 - 4 files changed, 51 insertions(+), 211 deletions(-) delete mode 100644 examples/ERP/classify_P300_nch_no_moabb.py diff --git a/examples/ERP/classify_P300_nch.py b/examples/ERP/classify_P300_nch.py index dfa8fff0..3ce641e1 100644 --- a/examples/ERP/classify_P300_nch.py +++ b/examples/ERP/classify_P300_nch.py @@ -4,6 +4,7 @@ ==================================================================== Demonstrates classification with QunatumNCH. +Evaluation is done using MOABB. If parameter "shots" is None then a classical SVM is used similar to the one in scikit learn. @@ -34,6 +35,7 @@ ) from sklearn.decomposition import PCA from pyriemann_qiskit.classification import QuanticNCH +from pyriemann.classification import MDM print(__doc__) @@ -72,20 +74,7 @@ pipelines = {} -# NCH as a classifier -# pipelines["NCH"] = make_pipeline( -# # applies XDawn and calculates the covariance matrix, output it matrices -# XdawnCovariances( -# nfilter=3, -# classes=[labels_dict["Target"]], -# estimator="lwf", -# xdawn_estimator="scm", -# ), -# QuanticNCH(n_hulls=1, n_samples_per_hull=3), # you can use other classifiers -# ) - -# NCH as a transformer -pipelines["NCH+LDA"] = make_pipeline( +pipelines["NCH"] = make_pipeline( # applies XDawn and calculates the covariance matrix, output it matrices XdawnCovariances( nfilter=3, @@ -93,26 +82,18 @@ estimator="lwf", xdawn_estimator="scm", ), - QuanticNCH(n_hulls=3, n_samples_per_hull=15), - LDA(solver="lsqr", shrinkage="auto"), + QuanticNCH(n_hulls=3, n_samples_per_hull=15, n_jobs=12, quantum=False), ) -# Here we provide a pipeline for comparison: - -# This is a standard pipeline similar to -# QuantumClassifierWithDefaultRiemannianPipeline, but with LDA classifier -# instead. -pipelines["RG+LDA"] = make_pipeline( - # applies XDawn and calculates the covariance matrix, output it matrices +#this is a non quantum pipeline +pipelines["XD+MDM"] = make_pipeline( XdawnCovariances( nfilter=3, classes=[labels_dict["Target"]], estimator="lwf", xdawn_estimator="scm", ), - TangentSpace(), - PCA(n_components=10), - LDA(solver="lsqr", shrinkage="auto"), # you can use other classifiers + MDM() ) print("Total pipelines to evaluate: ", len(pipelines)) diff --git a/examples/ERP/classify_P300_nch_no_moabb.py b/examples/ERP/classify_P300_nch_no_moabb.py deleted file mode 100644 index 5e27e559..00000000 --- a/examples/ERP/classify_P300_nch_no_moabb.py +++ /dev/null @@ -1,152 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Tue Mar 5 12:26:17 2024 - -@author: antona -""" - -import matplotlib.pyplot as plt -from pyriemann.estimation import Covariances, ERPCovariances, XdawnCovariances -from pyriemann.tangentspace import TangentSpace -from sklearn.linear_model import LogisticRegression -from sklearn.pipeline import make_pipeline -from sklearn.model_selection import cross_val_score -from sklearn.metrics import balanced_accuracy_score, make_scorer -from moabb.datasets import ( - bi2013a, - BNCI2014008, - BNCI2014009, - BNCI2015003, - EPFLP300, - Lee2019_ERP, -) -from qiskit_optimization.algorithms import ADMMOptimizer, SlsqpOptimizer -from moabb.paradigms import P300 -from sklearn.model_selection import train_test_split -from pyriemann_qiskit.classification import QuanticNCH -import numpy as np -from sklearn.preprocessing import LabelEncoder -import os -import time -from joblib import Parallel, delayed -from multiprocessing import Process -from PIL import Image - -import warnings - -warnings.filterwarnings("ignore", category=DeprecationWarning) - -from mne import set_log_level - -from imblearn.under_sampling import NearMiss - -set_log_level("CRITICAL") - -paradigm = P300() -labels_dict = {"Target": 1, "NonTarget": 0} - -le = LabelEncoder() - - -# Returns a Test dataset that contains an equal amounts of each class -# y should contain only two classes 0 and 1 -def SplitEqual(X, y, samples_n, train_samples_n): # samples_n per class - indicesClass1 = [] - indicesClass2 = [] - - for i in range(0, len(y)): - if y[i] == 0 and len(indicesClass1) < samples_n: - indicesClass1.append(i) - elif y[i] == 1 and len(indicesClass2) < samples_n: - indicesClass2.append(i) - - if len(indicesClass1) == samples_n and len(indicesClass2) == samples_n: - break - - X_test_class1 = X[indicesClass1] - X_test_class2 = X[indicesClass2] - - X_test = np.concatenate((X_test_class1, X_test_class2), axis=0) - - # remove x_test from X - X_train = np.delete(X, indicesClass1 + indicesClass2, axis=0) - - Y_test_class1 = y[indicesClass1] - Y_test_class2 = y[indicesClass2] - - y_test = np.concatenate((Y_test_class1, Y_test_class2), axis=0) - - # remove y_test from y - y_train = np.delete(y, indicesClass1 + indicesClass2, axis=0) - - if X_test.shape[0] != 2 * samples_n or y_test.shape[0] != 2 * samples_n: - raise Exception("Problem with split 1!") - - if ( - X_train.shape[0] + X_test.shape[0] != X.shape[0] - or y_train.shape[0] + y_test.shape[0] != y.shape[0] - ): - raise Exception("Problem with split 2!") - - #################################################### - X_train_1 = X_train[(y_train == 1)] - X_train_1 = X_train_1[0:train_samples_n] - X_train_2 = X_train[(y_train == 0)] - X_train_2 = X_train_2[0:train_samples_n, :, :] - - X_train = np.concatenate((X_train_1, X_train_2), axis=0) - - y_train = np.concatenate( - ( - np.ones(train_samples_n, dtype=np.int8), - np.zeros(train_samples_n, dtype=np.int8), - ), - axis=0, - ) - - return X_train, X_test, y_train, y_test - - -db = BNCI2014009() # BNCI2014008() -n_subjects = 4 - -for subject_i, subject in enumerate(db.subject_list[1:n_subjects]): - print("Loading subject:", subject) - - X, y, _ = paradigm.get_data(dataset=db, subjects=[subject]) - y = le.fit_transform(y) - print(X.shape) - - X_train, X_test, y_train, y_test = train_test_split( - X, y, test_size=0.020, random_state=42, stratify=None - ) - # X_train, X_test, y_train, y_test = SplitEqual( - # X, y , 20, 220 - # ) - - print("Test Class 1 count:", sum(y_test)) - print("Test Class 2 count:", len(y_test) - sum(y_test)) - - # train_size = 350 - # X_train = X_train[0:train_size] - # y_train = y_train[0:train_size] - - pipelines = {} - - pipelines["RG+NCH"] = make_pipeline( - XdawnCovariances( - nfilter=3, # increased, might be a problem for quantum - classes=[labels_dict["Target"]], - estimator="lwf", - xdawn_estimator="scm", - ), - QuanticNCH(classical_optimizer=SlsqpOptimizer()), - ) - - # score = pipelines["RG+NCH"].fit(X_train, y_train).score(X_test, y_test) - # print("Classification score - subject, score:", subject_i, " , ", score) - pipelines["RG+NCH"].fit(X_train, y_train) - y_pred = pipelines["RG+NCH"].predict(X_test) - print("Prediction: ", y_pred) - print("Ground truth: ", y_test) - print("Balanced accuracy: ", balanced_accuracy_score(y_test, y_pred)) diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index 52060cd5..83db9d60 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -10,6 +10,7 @@ import numpy as np from warnings import warn +from sklearn.base import BaseEstimator, ClassifierMixin from pyriemann.classification import MDM from pyriemann_qiskit.datasets import get_feature_dimension from pyriemann_qiskit.utils import ( @@ -26,10 +27,9 @@ from qiskit_machine_learning.kernels.quantum_kernel import QuantumKernel from qiskit_optimization.algorithms import ( CobylaOptimizer, - ADMMOptimizer, +# ADMMOptimizer, SlsqpOptimizer, ) -from sklearn.base import BaseEstimator, ClassifierMixin from sklearn.svm import SVC from .utils.hyper_params_factory import gen_zz_feature_map, gen_two_local, get_spsa @@ -756,12 +756,27 @@ def predict(self, X): class NearestConvexHull(BaseEstimator, ClassifierMixin, TransformerMixin): - def __init__(self, n_jobs=12, n_hulls=3, n_samples_per_hull=30): + + '''Nearest Convex Hull Classifier (NCH) + + In NCH, for each class a convex hull is produced by the set of matrices + corresponding to each class. There is no training. Calculating a distance + to a hull is an optimization problem and it is calculated for each testing + sample (SPD matrix) and each hull/class. The minimal distance defines the + predicted class. + + Notes + ----- + .. versionadded:: 0.2.0 + ''' + + def __init__(self, n_jobs=6, n_hulls=3, n_samples_per_hull=30): """Init.""" self.n_jobs = n_jobs self.n_samples_per_hull = n_samples_per_hull self.n_hulls = n_hulls self.matrices_per_class_ = {} + self.debug = False def fit(self, X, y): """Fit (store the training data). @@ -781,7 +796,7 @@ def fit(self, X, y): The NearestNeighbor instance. """ - print("Start NCH Train") + if (self.debug): print("Start NCH Train") self.classes_ = np.unique(y) for c in self.classes_: @@ -793,19 +808,25 @@ def fit(self, X, y): for c in self.classes_: self.matrices_per_class_[c] = np.array(self.matrices_per_class_[c]) - print("Samples per class:") + if (self.debug): print("Samples per class:") for c in self.classes_: - print("Class: ", c, " Count: ", self.matrices_per_class_[c].shape[0]) + if (self.debug): print("Class: ", c, " Count: ", self.matrices_per_class_[c].shape[0]) - print("End NCH Train") + if (self.debug): print("End NCH Train") def _predict_distances(self, X): """Helper to predict the distance. Equivalent to transform.""" dist = [] - print("Total test samples:", X.shape[0]) + if (self.debug): print("Total test samples:", X.shape[0]) - parallel = True + parallel = self.n_jobs > 1 + + if (self.debug): + if (parallel): + print("Running in parallel") + else: + print("Not running in parallel") if parallel: dist = Parallel(n_jobs=self.n_jobs)( @@ -813,10 +834,7 @@ def _predict_distances(self, X): ) else: - # k = 0 for test_sample in X: - # k = k + 1 - # print(k) dist_sample = self._process_sample(test_sample) dist.append(dist_sample) @@ -855,18 +873,18 @@ def predict(self, X): pred : ndarray of int, shape (n_matrices,) Predictions for each matrix according to the closest convex hull. """ - print("Start NCH Predict") + if (self.debug): print("Start NCH Predict") dist = self._predict_distances(X) predictions = [ self.classes_[min(range(len(values)), key=values.__getitem__)] for values in dist ] - # print(predictions) + if (self.debug): print(predictions) return predictions # return self.classes_[dist.argmin(axis=1)] - print("End NCH Predict") + if (self.debug): print("End NCH Predict") def transform(self, X): """Get the distance to each convex hull. @@ -879,23 +897,19 @@ def transform(self, X): dist : ndarray, shape (n_matrices, n_classes) The distance to each convex hull. """ - print("NCH Transform") + + if (self.debug): print("NCH Transform") return self._predict_distances(X) class QuanticNCH(QuanticClassifierBase): - """Nearest Convex Hull Classifier (NCH) - - In NCH, for each class a convex hull is produced by the set of matrices - corresponding to each class. There is no training. Calculating a distance - to a hull is an optimization problem and it is calculated for each testing - sample (SPD matrix) and each hull/class. The minimal distance defines the - predicted class. + """A Quantum wrapper around the NCH algorithm. It allows both classical + and Quantum versions to be executed. Notes ----- - .. versionadded:: 0.1.1 + .. versionadded:: 0.2.0 Parameters ---------- @@ -934,8 +948,8 @@ def __init__( seed=None, upper_bound=7, regularization=None, - # classical_optimizer=CobylaOptimizer(rhobeg=2.1, rhoend=0.000001), - classical_optimizer=SlsqpOptimizer(), + n_jobs=6, + classical_optimizer=SlsqpOptimizer(), # set here new default optimizer n_hulls=3, n_samples_per_hull=10, ): @@ -947,12 +961,13 @@ def __init__( self.classical_optimizer = classical_optimizer self.n_hulls = n_hulls self.n_samples_per_hull = n_samples_per_hull + self.n_jobs = n_jobs def _init_algo(self, n_features): self._log("Nearest Convex Hull Classifier initiating algorithm") classifier = NearestConvexHull( - n_hulls=self.n_hulls, n_samples_per_hull=self.n_samples_per_hull + n_hulls=self.n_hulls, n_samples_per_hull=self.n_samples_per_hull, n_jobs=self.n_jobs ) if self.quantum: @@ -968,9 +983,9 @@ def _init_algo(self, n_features): return classifier def predict(self, X): - print("QuanticNCH Predict") + #self._log("QuanticNCH Predict") return self._predict(X) def transform(self, X): - print("QuanticNCH Transform") + #self._log("QuanticNCH Transform") return self._classifier.transform(X) diff --git a/pyriemann_qiskit/utils/distance.py b/pyriemann_qiskit/utils/distance.py index b0c49cd8..eedbdfc5 100644 --- a/pyriemann_qiskit/utils/distance.py +++ b/pyriemann_qiskit/utils/distance.py @@ -124,10 +124,6 @@ def log_prod(m1, m2): prob.set_objective("min", objective) prob.add_constraint(prob.sum(w) == 1) - # for wi in w: - # # reinforce constraint on lower and upper bound - # prob.add_constraint(wi >= 0) - # prob.add_constraint(wi <= 1) weights = optimizer.solve(prob, reshape=False) From edc35615df77d34fa485a3d76ff28f8ad522ea5a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 12 Mar 2024 12:19:00 +0000 Subject: [PATCH 24/45] [pre-commit.ci] auto fixes from pre-commit.com hooks --- examples/ERP/classify_P300_nch.py | 4 +-- pyriemann_qiskit/classification.py | 55 ++++++++++++++++++------------ 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/examples/ERP/classify_P300_nch.py b/examples/ERP/classify_P300_nch.py index 3ce641e1..b8c60998 100644 --- a/examples/ERP/classify_P300_nch.py +++ b/examples/ERP/classify_P300_nch.py @@ -85,7 +85,7 @@ QuanticNCH(n_hulls=3, n_samples_per_hull=15, n_jobs=12, quantum=False), ) -#this is a non quantum pipeline +# this is a non quantum pipeline pipelines["XD+MDM"] = make_pipeline( XdawnCovariances( nfilter=3, @@ -93,7 +93,7 @@ estimator="lwf", xdawn_estimator="scm", ), - MDM() + MDM(), ) print("Total pipelines to evaluate: ", len(pipelines)) diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index 83db9d60..6cf30f3f 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -27,7 +27,7 @@ from qiskit_machine_learning.kernels.quantum_kernel import QuantumKernel from qiskit_optimization.algorithms import ( CobylaOptimizer, -# ADMMOptimizer, + # ADMMOptimizer, SlsqpOptimizer, ) from sklearn.svm import SVC @@ -756,8 +756,8 @@ def predict(self, X): class NearestConvexHull(BaseEstimator, ClassifierMixin, TransformerMixin): - - '''Nearest Convex Hull Classifier (NCH) + + """Nearest Convex Hull Classifier (NCH) In NCH, for each class a convex hull is produced by the set of matrices corresponding to each class. There is no training. Calculating a distance @@ -768,8 +768,8 @@ class NearestConvexHull(BaseEstimator, ClassifierMixin, TransformerMixin): Notes ----- .. versionadded:: 0.2.0 - ''' - + """ + def __init__(self, n_jobs=6, n_hulls=3, n_samples_per_hull=30): """Init.""" self.n_jobs = n_jobs @@ -796,7 +796,8 @@ def fit(self, X, y): The NearestNeighbor instance. """ - if (self.debug): print("Start NCH Train") + if self.debug: + print("Start NCH Train") self.classes_ = np.unique(y) for c in self.classes_: @@ -808,22 +809,26 @@ def fit(self, X, y): for c in self.classes_: self.matrices_per_class_[c] = np.array(self.matrices_per_class_[c]) - if (self.debug): print("Samples per class:") + if self.debug: + print("Samples per class:") for c in self.classes_: - if (self.debug): print("Class: ", c, " Count: ", self.matrices_per_class_[c].shape[0]) + if self.debug: + print("Class: ", c, " Count: ", self.matrices_per_class_[c].shape[0]) - if (self.debug): print("End NCH Train") + if self.debug: + print("End NCH Train") def _predict_distances(self, X): """Helper to predict the distance. Equivalent to transform.""" dist = [] - if (self.debug): print("Total test samples:", X.shape[0]) + if self.debug: + print("Total test samples:", X.shape[0]) parallel = self.n_jobs > 1 - - if (self.debug): - if (parallel): + + if self.debug: + if parallel: print("Running in parallel") else: print("Not running in parallel") @@ -873,18 +878,21 @@ def predict(self, X): pred : ndarray of int, shape (n_matrices,) Predictions for each matrix according to the closest convex hull. """ - if (self.debug): print("Start NCH Predict") + if self.debug: + print("Start NCH Predict") dist = self._predict_distances(X) predictions = [ self.classes_[min(range(len(values)), key=values.__getitem__)] for values in dist ] - if (self.debug): print(predictions) + if self.debug: + print(predictions) return predictions # return self.classes_[dist.argmin(axis=1)] - if (self.debug): print("End NCH Predict") + if self.debug: + print("End NCH Predict") def transform(self, X): """Get the distance to each convex hull. @@ -897,8 +905,9 @@ def transform(self, X): dist : ndarray, shape (n_matrices, n_classes) The distance to each convex hull. """ - - if (self.debug): print("NCH Transform") + + if self.debug: + print("NCH Transform") return self._predict_distances(X) @@ -949,7 +958,7 @@ def __init__( upper_bound=7, regularization=None, n_jobs=6, - classical_optimizer=SlsqpOptimizer(), # set here new default optimizer + classical_optimizer=SlsqpOptimizer(), # set here new default optimizer n_hulls=3, n_samples_per_hull=10, ): @@ -967,7 +976,9 @@ def _init_algo(self, n_features): self._log("Nearest Convex Hull Classifier initiating algorithm") classifier = NearestConvexHull( - n_hulls=self.n_hulls, n_samples_per_hull=self.n_samples_per_hull, n_jobs=self.n_jobs + n_hulls=self.n_hulls, + n_samples_per_hull=self.n_samples_per_hull, + n_jobs=self.n_jobs, ) if self.quantum: @@ -983,9 +994,9 @@ def _init_algo(self, n_features): return classifier def predict(self, X): - #self._log("QuanticNCH Predict") + # self._log("QuanticNCH Predict") return self._predict(X) def transform(self, X): - #self._log("QuanticNCH Transform") + # self._log("QuanticNCH Transform") return self._classifier.transform(X) From 1ffc5bcd1f1c3c569604dea4f2d3cd27e91d1338 Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 12 Mar 2024 14:31:54 +0100 Subject: [PATCH 25/45] removed commented code --- pyriemann_qiskit/classification.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index 6cf30f3f..f7b01e39 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -890,7 +890,6 @@ def predict(self, X): print(predictions) return predictions - # return self.classes_[dist.argmin(axis=1)] if self.debug: print("End NCH Predict") From 705164bd398b050286f6532206cd4aa7cd8a8c55 Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 12 Mar 2024 14:39:26 +0100 Subject: [PATCH 26/45] Small adjustments. --- pyriemann_qiskit/classification.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index f7b01e39..19bf5e54 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -770,7 +770,7 @@ class NearestConvexHull(BaseEstimator, ClassifierMixin, TransformerMixin): .. versionadded:: 0.2.0 """ - def __init__(self, n_jobs=6, n_hulls=3, n_samples_per_hull=30): + def __init__(self, n_jobs=6, n_hulls=3, n_samples_per_hull=10): """Init.""" self.n_jobs = n_jobs self.n_samples_per_hull = n_samples_per_hull @@ -811,11 +811,9 @@ def fit(self, X, y): if self.debug: print("Samples per class:") - for c in self.classes_: - if self.debug: + for c in self.classes_: print("Class: ", c, " Count: ", self.matrices_per_class_[c].shape[0]) - if self.debug: print("End NCH Train") def _predict_distances(self, X): @@ -949,7 +947,7 @@ class QuanticNCH(QuanticClassifierBase): def __init__( self, - quantum=False, # change to True in final version + quantum=True, q_account_token=None, verbose=True, shots=1024, From c562d2ae8e29392ef3f01677473d64da787d48ea Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 12 Mar 2024 15:40:48 +0100 Subject: [PATCH 27/45] Better class separation. --- pyriemann_qiskit/classification.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index 19bf5e54..45aee201 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -801,13 +801,7 @@ def fit(self, X, y): self.classes_ = np.unique(y) for c in self.classes_: - self.matrices_per_class_[c] = [] - - for i in range(0, len(y)): - self.matrices_per_class_[y[i]].append(X[i, :, :]) - - for c in self.classes_: - self.matrices_per_class_[c] = np.array(self.matrices_per_class_[c]) + self.matrices_per_class_[c] = X[y==c,:,:] if self.debug: print("Samples per class:") From b9d30fccf48764893612284781520c4cd1dcf514 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 12 Mar 2024 14:41:06 +0000 Subject: [PATCH 28/45] [pre-commit.ci] auto fixes from pre-commit.com hooks --- pyriemann_qiskit/classification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index 45aee201..3aae2398 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -801,7 +801,7 @@ def fit(self, X, y): self.classes_ = np.unique(y) for c in self.classes_: - self.matrices_per_class_[c] = X[y==c,:,:] + self.matrices_per_class_[c] = X[y == c, :, :] if self.debug: print("Samples per class:") From 25bed43aaa3ade0c21765728102d443939eb3a95 Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 12 Mar 2024 19:53:29 +0100 Subject: [PATCH 29/45] Added support for n_samples_per_hull = -1 which takes all the samples for a class. --- pyriemann_qiskit/classification.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index 3aae2398..b90edcec 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -776,7 +776,7 @@ def __init__(self, n_jobs=6, n_hulls=3, n_samples_per_hull=10): self.n_samples_per_hull = n_samples_per_hull self.n_hulls = n_hulls self.matrices_per_class_ = {} - self.debug = False + self.debug = True def fit(self, X, y): """Fit (store the training data). @@ -845,12 +845,15 @@ def _process_sample(self, test_sample): # using multiple hulls for i in range(0, self.n_hulls): - random_samples = random.sample( - range(self.matrices_per_class_[c].shape[0]), - k=self.n_samples_per_hull, - ) - - hull_data = self.matrices_per_class_[c][random_samples, :, :] + + if (self.n_samples_per_hull == -1): + hull_data = self.matrices_per_class_[c] + else: + random_samples = random.sample( + range(self.matrices_per_class_[c].shape[0]), + k=self.n_samples_per_hull, + ) + hull_data = self.matrices_per_class_[c][random_samples, :, :] distance = qdistance_logeuclid_to_convex_hull(hull_data, test_sample) total_distance = total_distance + distance From 8eae303d8b5afb2f6d1afd5f8edd1bce92ed39c8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 12 Mar 2024 18:54:51 +0000 Subject: [PATCH 30/45] [pre-commit.ci] auto fixes from pre-commit.com hooks --- pyriemann_qiskit/classification.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index b90edcec..fd5b7fb0 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -845,8 +845,7 @@ def _process_sample(self, test_sample): # using multiple hulls for i in range(0, self.n_hulls): - - if (self.n_samples_per_hull == -1): + if self.n_samples_per_hull == -1: hull_data = self.matrices_per_class_[c] else: random_samples = random.sample( From f0d5153a322b2bc71069bafa874a98c8611f79ed Mon Sep 17 00:00:00 2001 From: toncho11 Date: Wed, 13 Mar 2024 09:15:19 +0100 Subject: [PATCH 31/45] Update pyriemann_qiskit/classification.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Set of SPD matrices. Co-authored-by: Quentin Barthélemy --- pyriemann_qiskit/classification.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index fd5b7fb0..bf80c3c1 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -893,6 +893,7 @@ def transform(self, X): ---------- X : ndarray, shape (n_matrices, n_channels, n_channels) Set of SPD matrices. + Returns ------- dist : ndarray, shape (n_matrices, n_classes) From 1d458c9cc43a1ad40d8536823594ca6b33ef4d03 Mon Sep 17 00:00:00 2001 From: toncho11 Date: Wed, 13 Mar 2024 09:15:48 +0100 Subject: [PATCH 32/45] Update pyriemann_qiskit/classification.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added new lines to before Parameters Co-authored-by: Quentin Barthélemy --- pyriemann_qiskit/classification.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index bf80c3c1..85e48ad7 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -889,6 +889,7 @@ def predict(self, X): def transform(self, X): """Get the distance to each convex hull. + Parameters ---------- X : ndarray, shape (n_matrices, n_channels, n_channels) From a3533e4672509a677d0d4eb06cff0614adb774a0 Mon Sep 17 00:00:00 2001 From: toncho11 Date: Wed, 13 Mar 2024 09:22:04 +0100 Subject: [PATCH 33/45] Update pyriemann_qiskit/classification.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [y == c, :, :] => [y == c] Co-authored-by: Quentin Barthélemy --- pyriemann_qiskit/classification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index 85e48ad7..146736e4 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -801,7 +801,7 @@ def fit(self, X, y): self.classes_ = np.unique(y) for c in self.classes_: - self.matrices_per_class_[c] = X[y == c, :, :] + self.matrices_per_class_[c] = X[y == c] if self.debug: print("Samples per class:") From 94e91b97b331eb5fb3b5d9c597b7a89878c48316 Mon Sep 17 00:00:00 2001 From: toncho11 Date: Wed, 13 Mar 2024 15:03:53 +0100 Subject: [PATCH 34/45] Update pyriemann_qiskit/classification.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NearestConvexHull text change Co-authored-by: Quentin Barthélemy --- pyriemann_qiskit/classification.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index 146736e4..b4fb0158 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -792,8 +792,8 @@ def fit(self, X, y): Returns ------- - self : NearestNeighbor instance - The NearestNeighbor instance. + self : NearestConvexHull instance + The NearestConvexHull instance. """ if self.debug: From a3cde26b31d227ebedcd28b5a4af62d76200f69d Mon Sep 17 00:00:00 2001 From: Anton Date: Wed, 13 Mar 2024 15:41:32 +0100 Subject: [PATCH 35/45] Improvements proposed by Quentin. --- examples/ERP/classify_P300_nch.py | 2 +- pyriemann_qiskit/classification.py | 36 +++++++++++++++++++++++++----- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/examples/ERP/classify_P300_nch.py b/examples/ERP/classify_P300_nch.py index b8c60998..635aecc3 100644 --- a/examples/ERP/classify_P300_nch.py +++ b/examples/ERP/classify_P300_nch.py @@ -82,7 +82,7 @@ estimator="lwf", xdawn_estimator="scm", ), - QuanticNCH(n_hulls=3, n_samples_per_hull=15, n_jobs=12, quantum=False), + QuanticNCH(n_hulls_per_class=3, n_samples_per_hull=15, n_jobs=12, quantum=False), ) # this is a non quantum pipeline diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index b4fb0158..1ab5fc66 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -768,13 +768,30 @@ class NearestConvexHull(BaseEstimator, ClassifierMixin, TransformerMixin): Notes ----- .. versionadded:: 0.2.0 + + Parameters + ---------- + n_jobs : int, default=6 + The number of jobs to use for the computation. This works by computing + each of the hulls in parallel. + n_hulls_per_class: int, default 3 + The number of hulls used per class. + n_samples_per_hull: int, default 15 + Defines how many samples are used to build a hull. + + References + ---------- + .. [1] \ + K. Zhao, A. Wiliem, S. Chen, and B. C. Lovell, + ‘Convex Class Model on Symmetric Positive Definite Manifolds’, + Image and Vision Computing, 2019. """ - def __init__(self, n_jobs=6, n_hulls=3, n_samples_per_hull=10): + def __init__(self, n_jobs=6, n_hulls_per_class=3, n_samples_per_hull=10): """Init.""" self.n_jobs = n_jobs self.n_samples_per_hull = n_samples_per_hull - self.n_hulls = n_hulls + self.n_hulls_per_class = n_hulls_per_class self.matrices_per_class_ = {} self.debug = True @@ -844,7 +861,7 @@ def _process_sample(self, test_sample): total_distance = 0 # using multiple hulls - for i in range(0, self.n_hulls): + for i in range(0, self.n_hulls_per_class): if self.n_samples_per_hull == -1: hull_data = self.matrices_per_class_[c] else: @@ -940,6 +957,13 @@ class QuanticNCH(QuanticClassifierBase): Additional post-processing to regularize means. classical_optimizer : OptimizationAlgorithm An instance of OptimizationAlgorithm [3]_ + n_jobs : int, default=6 + The number of jobs to use for the computation. This works by computing + each of the hulls in parallel. + n_hulls_per_class: int, default 3 + The number of hulls used per class. + n_samples_per_hull: int, default 15 + Defines how many samples are used to build a hull. """ @@ -954,7 +978,7 @@ def __init__( regularization=None, n_jobs=6, classical_optimizer=SlsqpOptimizer(), # set here new default optimizer - n_hulls=3, + n_hulls_per_class=3, n_samples_per_hull=10, ): QuanticClassifierBase.__init__( @@ -963,7 +987,7 @@ def __init__( self.upper_bound = upper_bound self.regularization = regularization self.classical_optimizer = classical_optimizer - self.n_hulls = n_hulls + self.n_hulls_per_class = n_hulls_per_class self.n_samples_per_hull = n_samples_per_hull self.n_jobs = n_jobs @@ -971,7 +995,7 @@ def _init_algo(self, n_features): self._log("Nearest Convex Hull Classifier initiating algorithm") classifier = NearestConvexHull( - n_hulls=self.n_hulls, + n_hulls_per_class=self.n_hulls_per_class, n_samples_per_hull=self.n_samples_per_hull, n_jobs=self.n_jobs, ) From 32bc8a3d6a2173cfe96e7508b1dcc1efbbe28bfc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 13 Mar 2024 14:41:49 +0000 Subject: [PATCH 36/45] [pre-commit.ci] auto fixes from pre-commit.com hooks --- pyriemann_qiskit/classification.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index 1ab5fc66..c5375936 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -768,23 +768,23 @@ class NearestConvexHull(BaseEstimator, ClassifierMixin, TransformerMixin): Notes ----- .. versionadded:: 0.2.0 - + Parameters ---------- n_jobs : int, default=6 The number of jobs to use for the computation. This works by computing each of the hulls in parallel. n_hulls_per_class: int, default 3 - The number of hulls used per class. + The number of hulls used per class. n_samples_per_hull: int, default 15 Defines how many samples are used to build a hull. - + References ---------- .. [1] \ K. Zhao, A. Wiliem, S. Chen, and B. C. Lovell, ‘Convex Class Model on Symmetric Positive Definite Manifolds’, - Image and Vision Computing, 2019. + Image and Vision Computing, 2019. """ def __init__(self, n_jobs=6, n_hulls_per_class=3, n_samples_per_hull=10): @@ -961,7 +961,7 @@ class QuanticNCH(QuanticClassifierBase): The number of jobs to use for the computation. This works by computing each of the hulls in parallel. n_hulls_per_class: int, default 3 - The number of hulls used per class. + The number of hulls used per class. n_samples_per_hull: int, default 15 Defines how many samples are used to build a hull. From 3676e632f59f846c1968934f5e948b29f6b9145f Mon Sep 17 00:00:00 2001 From: Anton Date: Wed, 13 Mar 2024 17:09:07 +0100 Subject: [PATCH 37/45] Added comment for the optimizer. --- pyriemann_qiskit/classification.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index c5375936..5aec280e 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -1008,6 +1008,7 @@ def _init_algo(self, n_features): else: self._log("Using ClassicalOptimizer") self._optimizer = ClassicalOptimizer(self.classical_optimizer) + set_global_optimizer(self._optimizer) return classifier From a040bedc7d758e14e4ccb5b9b0eca8e6bc1c013a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 13 Mar 2024 16:09:28 +0000 Subject: [PATCH 38/45] [pre-commit.ci] auto fixes from pre-commit.com hooks --- pyriemann_qiskit/classification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index 5aec280e..4d5f8142 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -1008,7 +1008,7 @@ def _init_algo(self, n_features): else: self._log("Using ClassicalOptimizer") self._optimizer = ClassicalOptimizer(self.classical_optimizer) - + set_global_optimizer(self._optimizer) return classifier From b9837ae84b2de2df1395cb446a2bcba7b1111bdc Mon Sep 17 00:00:00 2001 From: Anton Date: Wed, 13 Mar 2024 18:47:17 +0100 Subject: [PATCH 39/45] Added some comments in classification. Changes about the global optimizer so, that it is more evident that a global one is used. --- pyriemann_qiskit/classification.py | 11 ++++++----- pyriemann_qiskit/utils/distance.py | 5 ++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index 4d5f8142..c029d312 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -793,7 +793,7 @@ def __init__(self, n_jobs=6, n_hulls_per_class=3, n_samples_per_hull=10): self.n_samples_per_hull = n_samples_per_hull self.n_hulls_per_class = n_hulls_per_class self.matrices_per_class_ = {} - self.debug = True + self.debug = False def fit(self, X, y): """Fit (store the training data). @@ -862,9 +862,10 @@ def _process_sample(self, test_sample): # using multiple hulls for i in range(0, self.n_hulls_per_class): - if self.n_samples_per_hull == -1: + + if self.n_samples_per_hull == -1: # use all data per class hull_data = self.matrices_per_class_[c] - else: + else: # use a subset of the data per class random_samples = random.sample( range(self.matrices_per_class_[c].shape[0]), k=self.n_samples_per_hull, @@ -1008,8 +1009,8 @@ def _init_algo(self, n_features): else: self._log("Using ClassicalOptimizer") self._optimizer = ClassicalOptimizer(self.classical_optimizer) - - set_global_optimizer(self._optimizer) + + set_global_optimizer(self._optimizer) # sets the optimizer for the distance functions used in NearestConvexHull class return classifier diff --git a/pyriemann_qiskit/utils/distance.py b/pyriemann_qiskit/utils/distance.py index eedbdfc5..4a377fb5 100644 --- a/pyriemann_qiskit/utils/distance.py +++ b/pyriemann_qiskit/utils/distance.py @@ -19,7 +19,7 @@ def logeucl_dist_convex(): pass -def qdistance_logeuclid_to_convex_hull(A, B, optimizer=ClassicalOptimizer()): +def qdistance_logeuclid_to_convex_hull(A, B, optimizer=get_global_optimizer(ClassicalOptimizer())): """Log-Euclidean distance to a convex hull of SPD matrices. Log-Euclidean distance between a SPD matrix B and the convex hull of a set @@ -65,7 +65,7 @@ def qdistance_logeuclid_to_convex_hull(A, B, optimizer=ClassicalOptimizer()): return distance -def weights_logeuclid_to_convex_hull(A, B, optimizer=ClassicalOptimizer()): +def weights_logeuclid_to_convex_hull(A, B, optimizer=get_global_optimizer(ClassicalOptimizer())): """Weights for Log-Euclidean distance to a convex hull of SPD matrices. Weights for Log-Euclidean distance between a SPD matrix B @@ -113,7 +113,6 @@ def log_prod(m1, m2): return np.nansum(logm(m1).flatten() * logm(m2).flatten()) prob = Model() - optimizer = get_global_optimizer(optimizer) w = optimizer.get_weights(prob, matrices) wtLogAtLogAw = prob.sum( From 01d655ca7cad80e1da68921e6ac80e6d021fe912 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 13 Mar 2024 17:50:32 +0000 Subject: [PATCH 40/45] [pre-commit.ci] auto fixes from pre-commit.com hooks --- pyriemann_qiskit/classification.py | 11 ++++++----- pyriemann_qiskit/utils/distance.py | 8 ++++++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index c029d312..b144a849 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -862,10 +862,9 @@ def _process_sample(self, test_sample): # using multiple hulls for i in range(0, self.n_hulls_per_class): - - if self.n_samples_per_hull == -1: # use all data per class + if self.n_samples_per_hull == -1: # use all data per class hull_data = self.matrices_per_class_[c] - else: # use a subset of the data per class + else: # use a subset of the data per class random_samples = random.sample( range(self.matrices_per_class_[c].shape[0]), k=self.n_samples_per_hull, @@ -1009,8 +1008,10 @@ def _init_algo(self, n_features): else: self._log("Using ClassicalOptimizer") self._optimizer = ClassicalOptimizer(self.classical_optimizer) - - set_global_optimizer(self._optimizer) # sets the optimizer for the distance functions used in NearestConvexHull class + + set_global_optimizer( + self._optimizer + ) # sets the optimizer for the distance functions used in NearestConvexHull class return classifier diff --git a/pyriemann_qiskit/utils/distance.py b/pyriemann_qiskit/utils/distance.py index 4a377fb5..96edaa52 100644 --- a/pyriemann_qiskit/utils/distance.py +++ b/pyriemann_qiskit/utils/distance.py @@ -19,7 +19,9 @@ def logeucl_dist_convex(): pass -def qdistance_logeuclid_to_convex_hull(A, B, optimizer=get_global_optimizer(ClassicalOptimizer())): +def qdistance_logeuclid_to_convex_hull( + A, B, optimizer=get_global_optimizer(ClassicalOptimizer()) +): """Log-Euclidean distance to a convex hull of SPD matrices. Log-Euclidean distance between a SPD matrix B and the convex hull of a set @@ -65,7 +67,9 @@ def qdistance_logeuclid_to_convex_hull(A, B, optimizer=get_global_optimizer(Clas return distance -def weights_logeuclid_to_convex_hull(A, B, optimizer=get_global_optimizer(ClassicalOptimizer())): +def weights_logeuclid_to_convex_hull( + A, B, optimizer=get_global_optimizer(ClassicalOptimizer()) +): """Weights for Log-Euclidean distance to a convex hull of SPD matrices. Weights for Log-Euclidean distance between a SPD matrix B From 1eac2510511bc2fd0207d59e3e25d29a5cf2b9c2 Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 15 Mar 2024 08:59:09 +0100 Subject: [PATCH 41/45] Implemented min hull. Added support for both "min-hull" and "random-hull" using the constructor parameter "hull-type". --- examples/ERP/classify_P300_nch.py | 15 +++- pyriemann_qiskit/classification.py | 130 +++++++++++++++++++---------- 2 files changed, 101 insertions(+), 44 deletions(-) diff --git a/examples/ERP/classify_P300_nch.py b/examples/ERP/classify_P300_nch.py index 635aecc3..a094d270 100644 --- a/examples/ERP/classify_P300_nch.py +++ b/examples/ERP/classify_P300_nch.py @@ -74,7 +74,7 @@ pipelines = {} -pipelines["NCH"] = make_pipeline( +pipelines["NCH+RANDOM_HULL"] = make_pipeline( # applies XDawn and calculates the covariance matrix, output it matrices XdawnCovariances( nfilter=3, @@ -82,7 +82,18 @@ estimator="lwf", xdawn_estimator="scm", ), - QuanticNCH(n_hulls_per_class=3, n_samples_per_hull=15, n_jobs=12, quantum=False), + QuanticNCH(n_hulls_per_class=1, n_samples_per_hull=3, n_jobs=12, hull_type = "random-hull", quantum=False), +) + +pipelines["NCH+MIN_HULL"] = make_pipeline( + # applies XDawn and calculates the covariance matrix, output it matrices + XdawnCovariances( + nfilter=3, + classes=[labels_dict["Target"]], + estimator="lwf", + xdawn_estimator="scm", + ), + QuanticNCH(n_hulls_per_class=1, n_samples_per_hull=3, n_jobs=12, hull_type = "min-hull", quantum=False), ) # this is a non quantum pipeline diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index b144a849..41fdb6f9 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -10,7 +10,7 @@ import numpy as np from warnings import warn -from sklearn.base import BaseEstimator, ClassifierMixin +from sklearn.base import BaseEstimator, ClassifierMixin, TransformerMixin from pyriemann.classification import MDM from pyriemann_qiskit.datasets import get_feature_dimension from pyriemann_qiskit.utils import ( @@ -34,7 +34,7 @@ from .utils.hyper_params_factory import gen_zz_feature_map, gen_two_local, get_spsa from .utils import get_provider, get_devices, get_simulator -from .utils.distance import qdistance_logeuclid_to_convex_hull +from .utils.distance import qdistance_logeuclid_to_convex_hull, distance_logeuclid from joblib import Parallel, delayed import random @@ -751,10 +751,6 @@ def predict(self, X): labels = self._predict(X) return self._map_indices_to_classes(labels) - -from sklearn.base import BaseEstimator, ClassifierMixin, TransformerMixin - - class NearestConvexHull(BaseEstimator, ClassifierMixin, TransformerMixin): """Nearest Convex Hull Classifier (NCH) @@ -771,13 +767,16 @@ class NearestConvexHull(BaseEstimator, ClassifierMixin, TransformerMixin): Parameters ---------- - n_jobs : int, default=6 + n_jobs : int, (default=6) The number of jobs to use for the computation. This works by computing each of the hulls in parallel. - n_hulls_per_class: int, default 3 + n_hulls_per_class: int, (default 3) The number of hulls used per class. - n_samples_per_hull: int, default 15 + n_samples_per_hull: int, (default 15) Defines how many samples are used to build a hull. + hull_type: string, (default "min-hull") + Selects how the hull is constructed. Possible values are + "min-hull" and "random-hull" References ---------- @@ -787,13 +786,17 @@ class NearestConvexHull(BaseEstimator, ClassifierMixin, TransformerMixin): Image and Vision Computing, 2019. """ - def __init__(self, n_jobs=6, n_hulls_per_class=3, n_samples_per_hull=10): + def __init__(self, n_jobs=6, n_hulls_per_class=3, n_samples_per_hull=10, hull_type = "min-hull"): """Init.""" self.n_jobs = n_jobs self.n_samples_per_hull = n_samples_per_hull self.n_hulls_per_class = n_hulls_per_class self.matrices_per_class_ = {} self.debug = False + self.hull_type = hull_type + + if (hull_type not in ["min-hull","random-hull"]): + raise Exception("Error: Unknown hull type.") def fit(self, X, y): """Fit (store the training data). @@ -817,9 +820,9 @@ def fit(self, X, y): print("Start NCH Train") self.classes_ = np.unique(y) - for c in self.classes_: + for c in self.classes_: self.matrices_per_class_[c] = X[y == c] - + if self.debug: print("Samples per class:") for c in self.classes_: @@ -827,6 +830,60 @@ def fit(self, X, y): print("End NCH Train") + def _process_sample_min_hull(self, test_sample): + """Finds the closes N covmats and uses them to build a single hull per class + """ + distances = [] + + for c in self.classes_: + distances_to_covs = [distance_logeuclid(test_sample,cov) for cov in self.matrices_per_class_[c]] + + #take the first N min distances + indexes = np.argsort(np.array(distances_to_covs))[0:self.n_samples_per_hull] + + if self.debug: + print("Distances to test sample: ", distances_to_covs) + print("Smallest N distances indexes:", indexes) + print("Smallest N distances: ") + for pp in indexes: + print(distances_to_covs[pp]) + + d = qdistance_logeuclid_to_convex_hull(self.matrices_per_class_[c][indexes], test_sample) + + if self.debug: + print("Final hull distance:", d) + + distances.append(d) + + return distances + + def _process_sample_random_hull(self, test_sample): + """Uses random samples to build a hull, can be several hulls per class + """ + distances = [] + + for c in self.classes_: + total_distance = 0 + + # using multiple hulls + for i in range(0, self.n_hulls_per_class): + + if self.n_samples_per_hull == -1: # use all data per class + hull_data = self.matrices_per_class_[c] + else: # use a subset of the data per class + random_samples = random.sample( + range(self.matrices_per_class_[c].shape[0]), + k=self.n_samples_per_hull, + ) + hull_data = self.matrices_per_class_[c][random_samples, :, :] + + distance = qdistance_logeuclid_to_convex_hull(hull_data, test_sample) + total_distance = total_distance + distance + + distances.append(total_distance) + + return distances + def _predict_distances(self, X): """Helper to predict the distance. Equivalent to transform.""" dist = [] @@ -834,6 +891,13 @@ def _predict_distances(self, X): if self.debug: print("Total test samples:", X.shape[0]) + if self.hull_type == "min-hull": + self._process_sample = self._process_sample_min_hull + elif self.hull_type == "random-hull": + self._process_sample = self._process_sample_random_hull + else: + raise Exception("Error: Unknown hull type.") + parallel = self.n_jobs > 1 if self.debug: @@ -854,30 +918,6 @@ def _predict_distances(self, X): return dist - def _process_sample(self, test_sample): - distances = [] - - for c in self.classes_: - total_distance = 0 - - # using multiple hulls - for i in range(0, self.n_hulls_per_class): - if self.n_samples_per_hull == -1: # use all data per class - hull_data = self.matrices_per_class_[c] - else: # use a subset of the data per class - random_samples = random.sample( - range(self.matrices_per_class_[c].shape[0]), - k=self.n_samples_per_hull, - ) - hull_data = self.matrices_per_class_[c][random_samples, :, :] - - distance = qdistance_logeuclid_to_convex_hull(hull_data, test_sample) - total_distance = total_distance + distance - - distances.append(total_distance) - - return distances - def predict(self, X): """Get the predictions. Parameters @@ -897,12 +937,12 @@ def predict(self, X): self.classes_[min(range(len(values)), key=values.__getitem__)] for values in dist ] - if self.debug: - print(predictions) - return predictions if self.debug: print("End NCH Predict") + + return predictions + def transform(self, X): """Get the distance to each convex hull. @@ -957,13 +997,16 @@ class QuanticNCH(QuanticClassifierBase): Additional post-processing to regularize means. classical_optimizer : OptimizationAlgorithm An instance of OptimizationAlgorithm [3]_ - n_jobs : int, default=6 + n_jobs : int, (default=6) The number of jobs to use for the computation. This works by computing each of the hulls in parallel. - n_hulls_per_class: int, default 3 + n_hulls_per_class: int, (default 3) The number of hulls used per class. - n_samples_per_hull: int, default 15 + n_samples_per_hull: int, (default 15) Defines how many samples are used to build a hull. + hull_type: string, (default "min-hull") + Selects how the hull is constructed. Possible values are + "min-hull" and "random-hull" """ @@ -980,6 +1023,7 @@ def __init__( classical_optimizer=SlsqpOptimizer(), # set here new default optimizer n_hulls_per_class=3, n_samples_per_hull=10, + hull_type = "min-hull" ): QuanticClassifierBase.__init__( self, quantum, q_account_token, verbose, shots, None, seed @@ -990,6 +1034,7 @@ def __init__( self.n_hulls_per_class = n_hulls_per_class self.n_samples_per_hull = n_samples_per_hull self.n_jobs = n_jobs + self.hull_type = hull_type def _init_algo(self, n_features): self._log("Nearest Convex Hull Classifier initiating algorithm") @@ -998,6 +1043,7 @@ def _init_algo(self, n_features): n_hulls_per_class=self.n_hulls_per_class, n_samples_per_hull=self.n_samples_per_hull, n_jobs=self.n_jobs, + hull_type=self.hull_type ) if self.quantum: From 67d3dc46f68d97fe4ecc6efa4f52b60eca49219c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 15 Mar 2024 07:59:27 +0000 Subject: [PATCH 42/45] [pre-commit.ci] auto fixes from pre-commit.com hooks --- examples/ERP/classify_P300_nch.py | 16 ++++++- pyriemann_qiskit/classification.py | 76 ++++++++++++++++-------------- 2 files changed, 55 insertions(+), 37 deletions(-) diff --git a/examples/ERP/classify_P300_nch.py b/examples/ERP/classify_P300_nch.py index a094d270..47b5ba16 100644 --- a/examples/ERP/classify_P300_nch.py +++ b/examples/ERP/classify_P300_nch.py @@ -82,7 +82,13 @@ estimator="lwf", xdawn_estimator="scm", ), - QuanticNCH(n_hulls_per_class=1, n_samples_per_hull=3, n_jobs=12, hull_type = "random-hull", quantum=False), + QuanticNCH( + n_hulls_per_class=1, + n_samples_per_hull=3, + n_jobs=12, + hull_type="random-hull", + quantum=False, + ), ) pipelines["NCH+MIN_HULL"] = make_pipeline( @@ -93,7 +99,13 @@ estimator="lwf", xdawn_estimator="scm", ), - QuanticNCH(n_hulls_per_class=1, n_samples_per_hull=3, n_jobs=12, hull_type = "min-hull", quantum=False), + QuanticNCH( + n_hulls_per_class=1, + n_samples_per_hull=3, + n_jobs=12, + hull_type="min-hull", + quantum=False, + ), ) # this is a non quantum pipeline diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index 41fdb6f9..bb1f3677 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -751,6 +751,7 @@ def predict(self, X): labels = self._predict(X) return self._map_indices_to_classes(labels) + class NearestConvexHull(BaseEstimator, ClassifierMixin, TransformerMixin): """Nearest Convex Hull Classifier (NCH) @@ -775,7 +776,7 @@ class NearestConvexHull(BaseEstimator, ClassifierMixin, TransformerMixin): n_samples_per_hull: int, (default 15) Defines how many samples are used to build a hull. hull_type: string, (default "min-hull") - Selects how the hull is constructed. Possible values are + Selects how the hull is constructed. Possible values are "min-hull" and "random-hull" References @@ -786,7 +787,9 @@ class NearestConvexHull(BaseEstimator, ClassifierMixin, TransformerMixin): Image and Vision Computing, 2019. """ - def __init__(self, n_jobs=6, n_hulls_per_class=3, n_samples_per_hull=10, hull_type = "min-hull"): + def __init__( + self, n_jobs=6, n_hulls_per_class=3, n_samples_per_hull=10, hull_type="min-hull" + ): """Init.""" self.n_jobs = n_jobs self.n_samples_per_hull = n_samples_per_hull @@ -794,8 +797,8 @@ def __init__(self, n_jobs=6, n_hulls_per_class=3, n_samples_per_hull=10, hull_ty self.matrices_per_class_ = {} self.debug = False self.hull_type = hull_type - - if (hull_type not in ["min-hull","random-hull"]): + + if hull_type not in ["min-hull", "random-hull"]: raise Exception("Error: Unknown hull type.") def fit(self, X, y): @@ -820,9 +823,9 @@ def fit(self, X, y): print("Start NCH Train") self.classes_ = np.unique(y) - for c in self.classes_: + for c in self.classes_: self.matrices_per_class_[c] = X[y == c] - + if self.debug: print("Samples per class:") for c in self.classes_: @@ -831,43 +834,47 @@ def fit(self, X, y): print("End NCH Train") def _process_sample_min_hull(self, test_sample): - """Finds the closes N covmats and uses them to build a single hull per class - """ + """Finds the closes N covmats and uses them to build a single hull per class""" distances = [] - + for c in self.classes_: - distances_to_covs = [distance_logeuclid(test_sample,cov) for cov in self.matrices_per_class_[c]] - - #take the first N min distances - indexes = np.argsort(np.array(distances_to_covs))[0:self.n_samples_per_hull] - + distances_to_covs = [ + distance_logeuclid(test_sample, cov) + for cov in self.matrices_per_class_[c] + ] + + # take the first N min distances + indexes = np.argsort(np.array(distances_to_covs))[ + 0 : self.n_samples_per_hull + ] + if self.debug: print("Distances to test sample: ", distances_to_covs) print("Smallest N distances indexes:", indexes) print("Smallest N distances: ") for pp in indexes: print(distances_to_covs[pp]) - - d = qdistance_logeuclid_to_convex_hull(self.matrices_per_class_[c][indexes], test_sample) - + + d = qdistance_logeuclid_to_convex_hull( + self.matrices_per_class_[c][indexes], test_sample + ) + if self.debug: print("Final hull distance:", d) - + distances.append(d) - + return distances - + def _process_sample_random_hull(self, test_sample): - """Uses random samples to build a hull, can be several hulls per class - """ + """Uses random samples to build a hull, can be several hulls per class""" distances = [] - + for c in self.classes_: total_distance = 0 - + # using multiple hulls for i in range(0, self.n_hulls_per_class): - if self.n_samples_per_hull == -1: # use all data per class hull_data = self.matrices_per_class_[c] else: # use a subset of the data per class @@ -876,14 +883,14 @@ def _process_sample_random_hull(self, test_sample): k=self.n_samples_per_hull, ) hull_data = self.matrices_per_class_[c][random_samples, :, :] - + distance = qdistance_logeuclid_to_convex_hull(hull_data, test_sample) total_distance = total_distance + distance - + distances.append(total_distance) - + return distances - + def _predict_distances(self, X): """Helper to predict the distance. Equivalent to transform.""" dist = [] @@ -897,7 +904,7 @@ def _predict_distances(self, X): self._process_sample = self._process_sample_random_hull else: raise Exception("Error: Unknown hull type.") - + parallel = self.n_jobs > 1 if self.debug: @@ -940,9 +947,8 @@ def predict(self, X): if self.debug: print("End NCH Predict") - + return predictions - def transform(self, X): """Get the distance to each convex hull. @@ -1005,7 +1011,7 @@ class QuanticNCH(QuanticClassifierBase): n_samples_per_hull: int, (default 15) Defines how many samples are used to build a hull. hull_type: string, (default "min-hull") - Selects how the hull is constructed. Possible values are + Selects how the hull is constructed. Possible values are "min-hull" and "random-hull" """ @@ -1023,7 +1029,7 @@ def __init__( classical_optimizer=SlsqpOptimizer(), # set here new default optimizer n_hulls_per_class=3, n_samples_per_hull=10, - hull_type = "min-hull" + hull_type="min-hull", ): QuanticClassifierBase.__init__( self, quantum, q_account_token, verbose, shots, None, seed @@ -1043,7 +1049,7 @@ def _init_algo(self, n_features): n_hulls_per_class=self.n_hulls_per_class, n_samples_per_hull=self.n_samples_per_hull, n_jobs=self.n_jobs, - hull_type=self.hull_type + hull_type=self.hull_type, ) if self.quantum: From fe9deb1ade3730eee03ae648b0238963493e6749 Mon Sep 17 00:00:00 2001 From: Anton Date: Mon, 18 Mar 2024 10:48:45 +0100 Subject: [PATCH 43/45] Reverted to previous version as requested by Gregoire. --- pyriemann_qiskit/utils/distance.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pyriemann_qiskit/utils/distance.py b/pyriemann_qiskit/utils/distance.py index 96edaa52..eedbdfc5 100644 --- a/pyriemann_qiskit/utils/distance.py +++ b/pyriemann_qiskit/utils/distance.py @@ -19,9 +19,7 @@ def logeucl_dist_convex(): pass -def qdistance_logeuclid_to_convex_hull( - A, B, optimizer=get_global_optimizer(ClassicalOptimizer()) -): +def qdistance_logeuclid_to_convex_hull(A, B, optimizer=ClassicalOptimizer()): """Log-Euclidean distance to a convex hull of SPD matrices. Log-Euclidean distance between a SPD matrix B and the convex hull of a set @@ -67,9 +65,7 @@ def qdistance_logeuclid_to_convex_hull( return distance -def weights_logeuclid_to_convex_hull( - A, B, optimizer=get_global_optimizer(ClassicalOptimizer()) -): +def weights_logeuclid_to_convex_hull(A, B, optimizer=ClassicalOptimizer()): """Weights for Log-Euclidean distance to a convex hull of SPD matrices. Weights for Log-Euclidean distance between a SPD matrix B @@ -117,6 +113,7 @@ def log_prod(m1, m2): return np.nansum(logm(m1).flatten() * logm(m2).flatten()) prob = Model() + optimizer = get_global_optimizer(optimizer) w = optimizer.get_weights(prob, matrices) wtLogAtLogAw = prob.sum( From a7da2c52e0a2f611c83dad61bc1583d48975c1a3 Mon Sep 17 00:00:00 2001 From: gcattan Date: Mon, 18 Mar 2024 10:56:01 +0100 Subject: [PATCH 44/45] fix lint issues --- examples/ERP/classify_P300_nch.py | 10 ++-------- pyriemann_qiskit/classification.py | 4 +++- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/examples/ERP/classify_P300_nch.py b/examples/ERP/classify_P300_nch.py index 47b5ba16..2876edc1 100644 --- a/examples/ERP/classify_P300_nch.py +++ b/examples/ERP/classify_P300_nch.py @@ -20,20 +20,14 @@ # License: BSD (3-clause) from pyriemann.estimation import XdawnCovariances -from pyriemann.tangentspace import TangentSpace from sklearn.pipeline import make_pipeline from matplotlib import pyplot as plt import warnings import seaborn as sns -from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA from moabb import set_log_level -from moabb.datasets import bi2012, BNCI2014009, bi2013a -from moabb.evaluations import WithinSessionEvaluation, CrossSubjectEvaluation +from moabb.datasets import bi2013a +from moabb.evaluations import WithinSessionEvaluation from moabb.paradigms import P300 -from pyriemann_qiskit.pipelines import ( - QuantumClassifierWithDefaultRiemannianPipeline, -) -from sklearn.decomposition import PCA from pyriemann_qiskit.classification import QuanticNCH from pyriemann.classification import MDM diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index bb1f3677..c85a5d79 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -1061,9 +1061,11 @@ def _init_algo(self, n_features): self._log("Using ClassicalOptimizer") self._optimizer = ClassicalOptimizer(self.classical_optimizer) + # sets the optimizer for the distance functions + # used in NearestConvexHull class set_global_optimizer( self._optimizer - ) # sets the optimizer for the distance functions used in NearestConvexHull class + ) return classifier From 7047f465fc254667e5b93e0c16919286bb797996 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 18 Mar 2024 09:56:40 +0000 Subject: [PATCH 45/45] [pre-commit.ci] auto fixes from pre-commit.com hooks --- pyriemann_qiskit/classification.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index c85a5d79..1cc94ecf 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -1063,9 +1063,7 @@ def _init_algo(self, n_features): # sets the optimizer for the distance functions # used in NearestConvexHull class - set_global_optimizer( - self._optimizer - ) + set_global_optimizer(self._optimizer) return classifier