From ae89ac4f1b6e1367f69604324e6d144c3729a125 Mon Sep 17 00:00:00 2001 From: Gregoire Cattan Date: Wed, 6 Dec 2023 12:31:15 +0100 Subject: [PATCH 01/20] - Use x-feature-map - Change predict to use predict_proba instead for QSVM --- pyriemann_qiskit/classification.py | 16 ++++++++++------ pyriemann_qiskit/pipelines.py | 18 +++++++----------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index 56d9ff8c..187455f3 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -5,6 +5,7 @@ quantum computer. """ from datetime import datetime +from scipy.special import softmax import logging import numpy as np @@ -256,6 +257,7 @@ class QuanticSVM(QuanticClassifierBase): Fix: copy estimator not keeping base class parameters. .. versionchanged:: 0.2.0 Add seed parameter + Predict is now using predict_proba with a softmax. Parameters ---------- @@ -360,6 +362,7 @@ def _init_algo(self, n_features): gamma=self.gamma, C=self.C, max_iter=max_iter, + probability=True ) else: max_iter = -1 if self.max_iter is None else self.max_iter @@ -386,11 +389,10 @@ def predict_proba(self, X): prob[n, 0] == True if the nth sample is assigned to 1st class; prob[n, 1] == True if the nth sample is assigned to 2nd class. """ - predicted_labels = self.predict(X) - ret = [ - np.array([c == self.classes_[0], c == self.classes_[1]]) - for c in predicted_labels - ] + + proba = self._classifier.predict_proba(X) + ret = softmax(proba, axis=0) + return np.array(ret) def predict(self, X): @@ -407,7 +409,9 @@ def predict(self, X): pred : array, shape (n_samples,) Class labels for samples in X. """ - labels = self._predict(X) + probs = self.predict_proba(X) + labels = [1 if prob[0] < prob[1] else 0 for prob in probs] + self._log("Prediction finished.") return self._map_indices_to_classes(labels) diff --git a/pyriemann_qiskit/pipelines.py b/pyriemann_qiskit/pipelines.py index 85bb6eb5..b6db7f4a 100644 --- a/pyriemann_qiskit/pipelines.py +++ b/pyriemann_qiskit/pipelines.py @@ -10,6 +10,7 @@ from pyriemann_qiskit.utils.filtering import NoDimRed from pyriemann_qiskit.utils.hyper_params_factory import ( gen_zz_feature_map, + gen_x_feature_map, gen_two_local, get_spsa, ) @@ -175,12 +176,6 @@ class QuantumClassifierWithDefaultRiemannianPipeline(BasePipeline): shots : int | None (default: 1024) Number of repetitions of each circuit, for sampling. If None, classical computation will be performed. - feature_entanglement : str | list[list[list[int]]] | \ - Callable[int, list[list[list[int]]]] - Specifies the entanglement structure for the ZZFeatureMap. - Entanglement structure can be provided with indices or string. - Possible string values are: 'full', 'linear', 'circular' and 'sca'. - See [2]_ for more details on entanglement structure. feature_reps : int (default: 2) The number of repeated circuits for the ZZFeatureMap, greater or equal to 1. @@ -206,12 +201,15 @@ class QuantumClassifierWithDefaultRiemannianPipeline(BasePipeline): Notes ----- .. versionadded:: 0.0.1 + .. versionchanged:: 0.2.0 + Change feature map from ZZFeatureMap to XFeatureMap. + Therefore remove unused parameter `feature_entanglement`. See Also -------- XdawnCovariances TangentSpace - gen_zz_feature_map + gen_x_feature_map gen_two_local get_spsa QuanticVQC @@ -236,7 +234,6 @@ def __init__( C=1.0, max_iter=None, shots=1024, - feature_entanglement="full", feature_reps=2, spsa_trials=None, two_local_reps=None, @@ -248,7 +245,6 @@ def __init__( self.C = C self.max_iter = max_iter self.shots = shots - self.feature_entanglement = feature_entanglement self.feature_reps = feature_reps self.spsa_trials = spsa_trials self.two_local_reps = two_local_reps @@ -261,7 +257,7 @@ def _create_pipe(self): is_vqc = self.spsa_trials and self.two_local_reps is_quantum = self.shots is not None - feature_map = gen_zz_feature_map(self.feature_reps, self.feature_entanglement) + feature_map = gen_x_feature_map(self.feature_reps) if is_vqc: self._log("QuanticVQC chosen.") @@ -320,7 +316,7 @@ class QuantumMDMWithRiemannianPipeline(BasePipeline): shots : int (default:1024) Number of repetitions of each circuit, for sampling. gen_feature_map : Callable[int, QuantumCircuit | FeatureMap] \ - (default : Callable[int, ZZFeatureMap]) + (default : Callable[int, XFeatureMap]) Function generating a feature map to encode data into a quantum state. Attributes From 5382e1f208b10bb22881bda0e62b60daf914aad8 Mon Sep 17 00:00:00 2001 From: Gregoire Cattan Date: Wed, 6 Dec 2023 12:48:35 +0100 Subject: [PATCH 02/20] - test for predict proba - add multiclass FVT for QSVM --- tests/conftest.py | 1 + tests/test_classification.py | 28 +++++++++++++++++++++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 45d4a28f..8b439e8d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -179,6 +179,7 @@ class BinaryFVT(BinaryTest): def additional_steps(self): self.quantum_instance.fit(self.samples, self.labels) self.prediction = self.quantum_instance.predict(self.samples) + self.predict_proab = self.quantum_instance.predict_proba(self.samples) print(self.labels, self.prediction) diff --git a/tests/test_classification.py b/tests/test_classification.py index c93c4dad..4c1dbed0 100644 --- a/tests/test_classification.py +++ b/tests/test_classification.py @@ -114,13 +114,15 @@ def get_params(self): } def check(self): - assert ( - self.prediction[: self.class_len].all() == self.quantum_instance.classes_[0] - ) - assert ( - self.prediction[self.class_len :].all() == self.quantum_instance.classes_[1] - ) - + # Check that all classes are predicted + assert len(self.prediction) == len(self.labels) + # Check the proba for each classes are returned + assert self.predict_proab.shape[1] == len(self.labels) + for i in len(self.quantum_instance.classes_): + assert ( + self.prediction[self.class_len * i : self.class_len * (i + 1)].all() == \ + self.quantum_instance.classes_[i] + ) class TestQuanticSVM(TestClassicalSVM): """Tests the Quantum version of Quantic SVM. @@ -140,6 +142,16 @@ def get_params(self): "type": "bin", } +class TestQuanticSVM_MultiClass(MultiClassFVT): + """Perform SVM on a simulated quantum computer + (multi-label classification)""" + + def get_params(self): + # multi-inheritance pattern + return TestQuanticSVM.get_params(self) + + def check(self): + TestQuanticSVM.check(self) class TestQuanticPegasosSVM(TestClassicalSVM): """Same as TestQuanticSVM, except it uses @@ -176,6 +188,8 @@ def check(self): assert len(self.prediction) == len(self.labels) # Check the number of classes is consistent assert len(np.unique(self.prediction)) == len(np.unique(self.labels)) + # Check the proba for each classes are returned + assert self.predict_proab.shape[1] == len(self.labels) class TestQuanticVQC_MultiClass(MultiClassFVT): From 9941d7c14d04c811903051268c74f1b53f110126 Mon Sep 17 00:00:00 2001 From: Gregoire Cattan Date: Wed, 6 Dec 2023 13:51:35 +0100 Subject: [PATCH 03/20] - fix predict_proba for multiclass - use predict_proba inside for qsvm predictions - add multiclass test for MDM --- pyriemann_qiskit/classification.py | 100 ++++++++++++++++------------- tests/test_classification.py | 30 +++++---- 2 files changed, 73 insertions(+), 57 deletions(-) diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index 187455f3..872f8267 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -237,6 +237,40 @@ def _predict(self, X): result = self._classifier.predict(X) self._log("Prediction finished.") return result + + def predict_proba(self, X): + """Return the probabilities associated with predictions. + + The default behavior is to return the nested classifier probabilities. + In case where no `predict_proba` method is available inside the classifier, + the method the predicted label number (0 or 1 for examples) and applies a + softmax in top of it. + + 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 + ------- + 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 + 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) + + return np.array(proba) class QuanticSVM(QuanticClassifierBase): @@ -257,7 +291,8 @@ class QuanticSVM(QuanticClassifierBase): Fix: copy estimator not keeping base class parameters. .. versionchanged:: 0.2.0 Add seed parameter - Predict is now using predict_proba with a softmax. + SVC and QSVC now compute probability (may impact time performance) + Predict is now using predict_proba with a softmax, when using QSVC. Parameters ---------- @@ -353,7 +388,9 @@ def _init_algo(self, n_features): self._log("[Warning] `gamma` is not supported by PegasosQSVC") num_steps = 1000 if self.max_iter is None else self.max_iter classifier = PegasosQSVC( - quantum_kernel=quantum_kernel, C=self.C, num_steps=num_steps + quantum_kernel=quantum_kernel, + C=self.C, + num_steps=num_steps ) else: max_iter = -1 if self.max_iter is None else self.max_iter @@ -366,7 +403,12 @@ def _init_algo(self, n_features): ) else: max_iter = -1 if self.max_iter is None else self.max_iter - classifier = SVC(gamma=self.gamma, C=self.C, max_iter=max_iter) + classifier = SVC( + gamma=self.gamma, + C=self.C, + max_iter=max_iter, + probability=True + ) return classifier def predict_proba(self, X): @@ -386,14 +428,15 @@ def predict_proba(self, X): Returns ------- prob : ndarray, shape (n_samples, n_classes) - prob[n, 0] == True if the nth sample is assigned to 1st class; - prob[n, 1] == True if the nth sample is assigned to 2nd class. + prob[n, i] == 1 if the nth sample is assigned to class `i`; """ - proba = self._classifier.predict_proba(X) - ret = softmax(proba, axis=0) + proba = super().predict_proba(X) + if(isinstance(self._classifier, QSVC)): + # apply additional softmax + proba = softmax(proba) - return np.array(ret) + return np.array(proba) def predict(self, X): """Calculates the predictions. @@ -409,8 +452,11 @@ def predict(self, X): pred : array, shape (n_samples,) Class labels for samples in X. """ - probs = self.predict_proba(X) - labels = [1 if prob[0] < prob[1] else 0 for prob in probs] + if(isinstance(self._classifier, QSVC)): + probs = self.predict_proba(X) + labels = [1 if prob[0] < prob[1] else 0 for prob in probs] + else: + labels = self._predict(X) self._log("Prediction finished.") return self._map_indices_to_classes(labels) @@ -518,24 +564,6 @@ def _init_algo(self, n_features): ) return vqc - def predict_proba(self, X): - """Returns the probabilities associated with 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 - ------- - prob : ndarray, shape (n_samples, n_classes) - prob[n, 0] == True if the nth sample is assigned to 1st class; - prob[n, 1] == True if the nth sample is assigned to 2nd class. - """ - proba, _ = self._predict(X) - return proba - def predict(self, X): """Calculates the predictions. @@ -668,22 +696,6 @@ def _init_algo(self, n_features): set_global_optimizer(self._optimizer) return classifier - def predict_proba(self, X): - """Return the probabilities associated with predictions. - - Parameters - ---------- - X : ndarray, shape (n_trials, n_channels, n_channels) - ndarray of trials. - - Returns - ------- - prob : ndarray, shape (n_samples, n_classes) - prob[n, 0] == True if the nth sample is assigned to 1st class; - prob[n, 1] == True if the nth sample is assigned to 2nd class. - """ - return self._classifier.predict_proba(X) - def predict(self, X): """Calculates the predictions. diff --git a/tests/test_classification.py b/tests/test_classification.py index 4c1dbed0..94b0032e 100644 --- a/tests/test_classification.py +++ b/tests/test_classification.py @@ -117,12 +117,7 @@ def check(self): # Check that all classes are predicted assert len(self.prediction) == len(self.labels) # Check the proba for each classes are returned - assert self.predict_proab.shape[1] == len(self.labels) - for i in len(self.quantum_instance.classes_): - assert ( - self.prediction[self.class_len * i : self.class_len * (i + 1)].all() == \ - self.quantum_instance.classes_[i] - ) + assert self.predict_proab.shape[1] == len(np.unique(self.labels)) class TestQuanticSVM(TestClassicalSVM): """Tests the Quantum version of Quantic SVM. @@ -189,7 +184,7 @@ def check(self): # Check the number of classes is consistent assert len(np.unique(self.prediction)) == len(np.unique(self.labels)) # Check the proba for each classes are returned - assert self.predict_proab.shape[1] == len(self.labels) + assert self.predict_proab.shape[1] == len(np.unique(self.labels)) class TestQuanticVQC_MultiClass(MultiClassFVT): @@ -221,9 +216,18 @@ def get_params(self): } def check(self): - assert ( - self.prediction[: self.class_len].all() == self.quantum_instance.classes_[0] - ) - assert ( - self.prediction[self.class_len :].all() == self.quantum_instance.classes_[1] - ) + for i in range(len(self.quantum_instance.classes_)): + assert ( + np.all(self.prediction[self.class_len * i : self.class_len * (i + 1)]) == self.quantum_instance.classes_[i] + ) + +class TestQuanticMDM_MultiClass(MultiClassFVT): + """Perform MDM on a simulated quantum computer + (multi-label classification)""" + + def get_params(self): + # multi-inheritance pattern + return TestClassicalMDM.get_params(self) + + def check(self): + TestClassicalMDM.check(self) \ No newline at end of file From cf89c8c8576630440f99313e95d44dfd281ab22b Mon Sep 17 00:00:00 2001 From: Gregoire Cattan Date: Wed, 6 Dec 2023 15:42:37 +0100 Subject: [PATCH 04/20] fix conftest --- tests/conftest.py | 5 ++--- tests/test_classification.py | 2 -- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 8b439e8d..f54b54b0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -98,10 +98,9 @@ def _get_dataset(n_samples, n_features, n_classes, type="bin"): labels = _get_labels(n_samples, n_classes) elif type == "bin_cov": samples_0 = make_covariances( - n_samples // n_classes, n_features, 0, return_params=False + n_samples // n_classes, n_features, 0, return_params=False ) - samples_1 = samples_0 * 2 - samples = np.concatenate((samples_0, samples_1), axis=0) + samples = np.concatenate([samples_0 * (i + 1) for i in range(n_classes)], axis=0) labels = _get_labels(n_samples, n_classes) else: samples, labels = get_mne_sample() diff --git a/tests/test_classification.py b/tests/test_classification.py index 94b0032e..ae6126f6 100644 --- a/tests/test_classification.py +++ b/tests/test_classification.py @@ -142,7 +142,6 @@ class TestQuanticSVM_MultiClass(MultiClassFVT): (multi-label classification)""" def get_params(self): - # multi-inheritance pattern return TestQuanticSVM.get_params(self) def check(self): @@ -226,7 +225,6 @@ class TestQuanticMDM_MultiClass(MultiClassFVT): (multi-label classification)""" def get_params(self): - # multi-inheritance pattern return TestClassicalMDM.get_params(self) def check(self): From 2f0adfb95d83075f7a672162c8aeabca759dca55 Mon Sep 17 00:00:00 2001 From: Gregoire Cattan Date: Wed, 6 Dec 2023 20:48:08 +0100 Subject: [PATCH 05/20] fix MDM multiclass test --- tests/test_classification.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_classification.py b/tests/test_classification.py index ae6126f6..174fa521 100644 --- a/tests/test_classification.py +++ b/tests/test_classification.py @@ -215,10 +215,11 @@ def get_params(self): } def check(self): - for i in range(len(self.quantum_instance.classes_)): - assert ( - np.all(self.prediction[self.class_len * i : self.class_len * (i + 1)]) == self.quantum_instance.classes_[i] - ) + assert len(self.prediction) == len(self.labels) + # Check the number of classes is consistent + assert len(np.unique(self.prediction)) == len(np.unique(self.labels)) + # Check the proba for each classes are returned + assert self.predict_proab.shape[1] == len(np.unique(self.labels)) class TestQuanticMDM_MultiClass(MultiClassFVT): """Perform MDM on a simulated quantum computer From 1e05ad6880fb0a7e10d38e9e3e3dbf005dfa4abb Mon Sep 17 00:00:00 2001 From: Gregoire Cattan Date: Wed, 6 Dec 2023 20:49:30 +0100 Subject: [PATCH 06/20] reactivate quantum pipeline with plot_classify_P300_bi.py --- examples/ERP/plot_classify_P300_bi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ERP/plot_classify_P300_bi.py b/examples/ERP/plot_classify_P300_bi.py index e2809c1b..a5ea971d 100644 --- a/examples/ERP/plot_classify_P300_bi.py +++ b/examples/ERP/plot_classify_P300_bi.py @@ -67,7 +67,7 @@ # reduce the number of subjects, the Quantum pipeline takes a lot of time # if executed on the entire dataset -n_subjects = 5 +n_subjects = 2 for dataset in datasets: dataset.subject_list = dataset.subject_list[0:n_subjects] @@ -78,7 +78,7 @@ # A Riemannian Quantum pipeline provided by pyRiemann-qiskit # You can choose between classical SVM and Quantum SVM. pipelines["RG+QuantumSVM"] = QuantumClassifierWithDefaultRiemannianPipeline( - shots=None, # 'None' forces classic SVM + shots=512, # 'None' forces classic SVM nfilter=2, # default 2 # default n_components=10, a higher value renders better performance with # the non-qunatum SVM version used in qiskit From 5b5644741c5637ed9010bd98ba4b77f18130c0fc Mon Sep 17 00:00:00 2001 From: Gregoire Cattan Date: Wed, 6 Dec 2023 21:02:14 +0100 Subject: [PATCH 07/20] fix labels for multiclass --- 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 872f8267..bf022a0a 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -454,7 +454,7 @@ def predict(self, X): """ if(isinstance(self._classifier, QSVC)): probs = self.predict_proba(X) - labels = [1 if prob[0] < prob[1] else 0 for prob in probs] + labels = [np.argmax(prob) for prob in probs] else: labels = self._predict(X) self._log("Prediction finished.") From 901edfa757ae84a079aeda77ddfd1858b05a3f8a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 6 Dec 2023 20:05:33 +0000 Subject: [PATCH 08/20] [pre-commit.ci] auto fixes from pre-commit.com hooks --- pyriemann_qiskit/classification.py | 30 +++++++++++++++--------------- tests/conftest.py | 6 ++++-- tests/test_classification.py | 6 +++++- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index bf022a0a..577b950d 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -237,7 +237,7 @@ def _predict(self, X): result = self._classifier.predict(X) self._log("Prediction finished.") return result - + def predict_proba(self, X): """Return the probabilities associated with predictions. @@ -258,12 +258,17 @@ def predict_proba(self, X): prob[n, i] == 1 if the nth sample is assigned to class `i`; """ - if(not hasattr(self._classifier, "predict_proba")): + if not hasattr(self._classifier, "predict_proba"): # Classifier has no predict_proba # Use the result from predict and apply a softmax proba = self._classifier.predict(X) proba = [ - np.array([1 if c == self.classes_[i] else 0 for i in range(len(self.classes_))]) + 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) @@ -388,9 +393,7 @@ def _init_algo(self, n_features): self._log("[Warning] `gamma` is not supported by PegasosQSVC") num_steps = 1000 if self.max_iter is None else self.max_iter classifier = PegasosQSVC( - quantum_kernel=quantum_kernel, - C=self.C, - num_steps=num_steps + quantum_kernel=quantum_kernel, C=self.C, num_steps=num_steps ) else: max_iter = -1 if self.max_iter is None else self.max_iter @@ -399,16 +402,13 @@ def _init_algo(self, n_features): gamma=self.gamma, C=self.C, max_iter=max_iter, - probability=True + probability=True, ) else: max_iter = -1 if self.max_iter is None else self.max_iter classifier = SVC( - gamma=self.gamma, - C=self.C, - max_iter=max_iter, - probability=True - ) + gamma=self.gamma, C=self.C, max_iter=max_iter, probability=True + ) return classifier def predict_proba(self, X): @@ -432,8 +432,8 @@ def predict_proba(self, X): """ proba = super().predict_proba(X) - if(isinstance(self._classifier, QSVC)): - # apply additional softmax + if isinstance(self._classifier, QSVC): + # apply additional softmax proba = softmax(proba) return np.array(proba) @@ -452,7 +452,7 @@ def predict(self, X): pred : array, shape (n_samples,) Class labels for samples in X. """ - if(isinstance(self._classifier, QSVC)): + if isinstance(self._classifier, QSVC): probs = self.predict_proba(X) labels = [np.argmax(prob) for prob in probs] else: diff --git a/tests/conftest.py b/tests/conftest.py index f54b54b0..333f39a2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -98,9 +98,11 @@ def _get_dataset(n_samples, n_features, n_classes, type="bin"): labels = _get_labels(n_samples, n_classes) elif type == "bin_cov": samples_0 = make_covariances( - n_samples // n_classes, n_features, 0, return_params=False + n_samples // n_classes, n_features, 0, return_params=False + ) + samples = np.concatenate( + [samples_0 * (i + 1) for i in range(n_classes)], axis=0 ) - samples = np.concatenate([samples_0 * (i + 1) for i in range(n_classes)], axis=0) labels = _get_labels(n_samples, n_classes) else: samples, labels = get_mne_sample() diff --git a/tests/test_classification.py b/tests/test_classification.py index 174fa521..ed28e3b3 100644 --- a/tests/test_classification.py +++ b/tests/test_classification.py @@ -119,6 +119,7 @@ def check(self): # Check the proba for each classes are returned assert self.predict_proab.shape[1] == len(np.unique(self.labels)) + class TestQuanticSVM(TestClassicalSVM): """Tests the Quantum version of Quantic SVM. It is executed on a simulated quantum computer. @@ -137,6 +138,7 @@ def get_params(self): "type": "bin", } + class TestQuanticSVM_MultiClass(MultiClassFVT): """Perform SVM on a simulated quantum computer (multi-label classification)""" @@ -147,6 +149,7 @@ def get_params(self): def check(self): TestQuanticSVM.check(self) + class TestQuanticPegasosSVM(TestClassicalSVM): """Same as TestQuanticSVM, except it uses PegasosQSVC instead of QSVC implementation. @@ -221,6 +224,7 @@ def check(self): # Check the proba for each classes are returned assert self.predict_proab.shape[1] == len(np.unique(self.labels)) + class TestQuanticMDM_MultiClass(MultiClassFVT): """Perform MDM on a simulated quantum computer (multi-label classification)""" @@ -229,4 +233,4 @@ def get_params(self): return TestClassicalMDM.get_params(self) def check(self): - TestClassicalMDM.check(self) \ No newline at end of file + TestClassicalMDM.check(self) From 7ab17bc6cab3e6ffb393e66ddae2096c68bf629c Mon Sep 17 00:00:00 2001 From: Gregoire Cattan Date: Wed, 6 Dec 2023 21:30:22 +0100 Subject: [PATCH 09/20] remove feature_entanglement from plot_compare_dim --- examples/MI/plot_compare_dim_red.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/examples/MI/plot_compare_dim_red.py b/examples/MI/plot_compare_dim_red.py index a47b349b..55bf6295 100644 --- a/examples/MI/plot_compare_dim_red.py +++ b/examples/MI/plot_compare_dim_red.py @@ -39,10 +39,6 @@ # Determine the number of "run" on the quantum machine (simulated or real) # the higher is this number, the lower the variability. "shots": [1024], # [512, 1024, 2048] - # This defines how we entangle the data into a quantum state - # the more complex is the kernel, the more outcomes we can expect from - # a quantum vs classical classifier. - "feature_entanglement": ["linear"], # ['linear', 'sca', 'full'], # This parameter change the depth of the circuit when entangling data. # There is a trade-off between accuracy and noise when the depth of the # circuit increases. From 02655f207721460e225fd42bc6bc04a2a9640c54 Mon Sep 17 00:00:00 2001 From: Gregoire Cattan Date: Fri, 8 Dec 2023 09:10:21 +0100 Subject: [PATCH 10/20] missing cast to np.array --- 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 577b950d..17986e01 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -150,7 +150,7 @@ def _map_indices_to_classes(self, y): n_classes = len(self.classes_) for idx in range(n_classes): y_copy[y == idx] = self.classes_[idx] - return y_copy + return np.array(y_copy) def fit(self, X, y): """Uses a quantum backend and fits the training data. @@ -261,6 +261,8 @@ def predict_proba(self, X): if not hasattr(self._classifier, "predict_proba"): # Classifier has no predict_proba # Use the result from predict and apply a softmax + self._log("No predict_proba method available.\ + Computing softmax probabilities...") proba = self._classifier.predict(X) proba = [ np.array( From f90aa689d61ec852179ede1d8cf4be45ee49122f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 8 Dec 2023 08:11:08 +0000 Subject: [PATCH 11/20] [pre-commit.ci] auto fixes from pre-commit.com hooks --- pyriemann_qiskit/classification.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index 17986e01..eeabc775 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -261,8 +261,10 @@ def predict_proba(self, X): if not hasattr(self._classifier, "predict_proba"): # Classifier has no predict_proba # Use the result from predict and apply a softmax - self._log("No predict_proba method available.\ - Computing softmax probabilities...") + self._log( + "No predict_proba method available.\ + Computing softmax probabilities..." + ) proba = self._classifier.predict(X) proba = [ np.array( From f9ad84ed44f627ce6b006bf5afb9b0c395ecca7f Mon Sep 17 00:00:00 2001 From: Gregoire Cattan Date: Fri, 8 Dec 2023 15:48:34 +0100 Subject: [PATCH 12/20] - fix financial example (inverted classifier rf and erp) - only apply softmax on predict (remove useless overriden method predict_proba inside QSVM). --- .../other_datasets/plot_financial_data.py | 4 +-- pyriemann_qiskit/classification.py | 29 +------------------ 2 files changed, 3 insertions(+), 30 deletions(-) diff --git a/examples/other_datasets/plot_financial_data.py b/examples/other_datasets/plot_financial_data.py index 2c539cf7..b45b0f87 100644 --- a/examples/other_datasets/plot_financial_data.py +++ b/examples/other_datasets/plot_financial_data.py @@ -443,7 +443,7 @@ def transform(self, X): class ERP_CollusionClassifier(ClassifierMixin): - def __init__(self, row_clf, erp_clf, threshold=0.5): + def __init__(self, erp_clf, row_clf, threshold=0.5): self.row_clf = row_clf self.erp_clf = erp_clf self.threshold = threshold @@ -453,7 +453,7 @@ def fit(self, X, y): return self def predict(self, X): - y_pred = self.row_clf.predict(X) + y_pred = self.row_clf.predict(X).astype(float) collusion_prob = self.erp_clf.predict_proba(X) y_pred[y_pred == 1] = collusion_prob[y_pred == 1, 1].transpose() y_pred[y_pred >= self.threshold] = 1 diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index eeabc775..93d58a8e 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -415,33 +415,6 @@ def _init_algo(self, n_features): ) return classifier - def predict_proba(self, X): - """Return the probabilities associated with predictions. - - This method is implemented for compatibility purpose - as SVM prediction probabilities are not available. - This method assigns a boolean value to each trial which - depends on whether the label was assigned to class 0 or 1 - - 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 - ------- - prob : ndarray, shape (n_samples, n_classes) - prob[n, i] == 1 if the nth sample is assigned to class `i`; - """ - - proba = super().predict_proba(X) - if isinstance(self._classifier, QSVC): - # apply additional softmax - proba = softmax(proba) - - return np.array(proba) - def predict(self, X): """Calculates the predictions. @@ -457,7 +430,7 @@ def predict(self, X): Class labels for samples in X. """ if isinstance(self._classifier, QSVC): - probs = self.predict_proba(X) + probs = softmax(self.predict_proba(X)) labels = [np.argmax(prob) for prob in probs] else: labels = self._predict(X) From d29991404863b387df4eccfe289f2f9e1d135f19 Mon Sep 17 00:00:00 2001 From: Gregoire Cattan Date: Fri, 8 Dec 2023 18:07:59 +0100 Subject: [PATCH 13/20] fix _map_indices_to_classes --- 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 93d58a8e..52f69f84 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -146,10 +146,10 @@ def _map_classes_to_indices(self, y): return y_copy def _map_indices_to_classes(self, y): - y_copy = y.copy() + y_copy = np.array(y.copy()) n_classes = len(self.classes_) for idx in range(n_classes): - y_copy[y == idx] = self.classes_[idx] + y_copy[np.array(y).transpose() == idx] = self.classes_[idx] return np.array(y_copy) def fit(self, X, y): From cb76985caa1b2379b2dde069fab00bd8aac23964 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 8 Dec 2023 17:18:39 +0000 Subject: [PATCH 14/20] [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 52f69f84..0879bb5e 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -149,7 +149,7 @@ def _map_indices_to_classes(self, y): y_copy = np.array(y.copy()) n_classes = len(self.classes_) for idx in range(n_classes): - y_copy[np.array(y).transpose() == idx] = self.classes_[idx] + y_copy[np.array(y).transpose() == idx] = self.classes_[idx] return np.array(y_copy) def fit(self, X, y): From 48099fc81564e6b718cd19dceb7289ce7ea590b0 Mon Sep 17 00:00:00 2001 From: Gregoire Cattan Date: Fri, 8 Dec 2023 18:22:46 +0100 Subject: [PATCH 15/20] Anton's suggestion --- README.md | 11 +++-------- pyriemann_qiskit/pipelines.py | 4 ++++ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 3eea2ca4..17e41d0d 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ This library is part of the [Qiskit Ecosystem](https://qiskit.org/ecosystem) _We recommend the use of [Anaconda](https://www.anaconda.com/) to manage python environements._ -`pyRiemann-qiskit` currently supports Windows, Mac and Linux OS with Python 3.9 - 3.11. +`pyRiemann-qiskit` currently supports Windows, Mac and Linux OS with **Python 3.9 - 3.11**. You can install `pyRiemann-qiskit` release from PyPI: @@ -105,13 +105,8 @@ pip install pyriemann-qiskit ``` The development version can be installed by cloning this repository and installing the -package on your local machine using the `setup.py` script: - -``` -python setup.py develop -``` - -Or directly pip: +package on your local machine using the `setup.py` script. +We recommand to do it using `pip`: ``` pip install . diff --git a/pyriemann_qiskit/pipelines.py b/pyriemann_qiskit/pipelines.py index b6db7f4a..c39d778a 100644 --- a/pyriemann_qiskit/pipelines.py +++ b/pyriemann_qiskit/pipelines.py @@ -257,6 +257,10 @@ def _create_pipe(self): is_vqc = self.spsa_trials and self.two_local_reps is_quantum = self.shots is not None + # Different feature maps can be used. + # Currently the best results are produced by the x_feature_map. + # This can change in the future as the code for the different feature maps + # is updated in the new versions of Qiskit. feature_map = gen_x_feature_map(self.feature_reps) if is_vqc: From 8828048d068440c01112b61cead8a0e71e2deca8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 8 Dec 2023 17:23:09 +0000 Subject: [PATCH 16/20] [pre-commit.ci] auto fixes from pre-commit.com hooks --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 17e41d0d..785e2728 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,8 @@ This library is part of the [Qiskit Ecosystem](https://qiskit.org/ecosystem) _We recommend the use of [Anaconda](https://www.anaconda.com/) to manage python environements._ -`pyRiemann-qiskit` currently supports Windows, Mac and Linux OS with **Python 3.9 - 3.11**. +`pyRiemann-qiskit` currently supports Windows, Mac and Linux OS with **Python 3.9 - +3.11**. You can install `pyRiemann-qiskit` release from PyPI: @@ -105,8 +106,8 @@ pip install pyriemann-qiskit ``` The development version can be installed by cloning this repository and installing the -package on your local machine using the `setup.py` script. -We recommand to do it using `pip`: +package on your local machine using the `setup.py` script. We recommand to do it using +`pip`: ``` pip install . From b3e61e5f76f73410bbecbc5c7f441dfaae0d9d4f Mon Sep 17 00:00:00 2001 From: Gregoire Cattan Date: Fri, 8 Dec 2023 18:55:54 +0100 Subject: [PATCH 17/20] various typo --- examples/MI/plot_compare_dim_red.py | 2 +- pyriemann_qiskit/classification.py | 4 ++-- pyriemann_qiskit/pipelines.py | 4 ++-- tests/test_classification.py | 10 +++++----- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/MI/plot_compare_dim_red.py b/examples/MI/plot_compare_dim_red.py index 55bf6295..48febbb9 100644 --- a/examples/MI/plot_compare_dim_red.py +++ b/examples/MI/plot_compare_dim_red.py @@ -39,7 +39,7 @@ # Determine the number of "run" on the quantum machine (simulated or real) # the higher is this number, the lower the variability. "shots": [1024], # [512, 1024, 2048] - # This parameter change the depth of the circuit when entangling data. + # This parameter changes the depth of the circuit when entangling data. # There is a trade-off between accuracy and noise when the depth of the # circuit increases. "feature_reps": [2], # [2, 3, 4] diff --git a/pyriemann_qiskit/classification.py b/pyriemann_qiskit/classification.py index 0879bb5e..39250e3c 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -243,7 +243,7 @@ def predict_proba(self, X): The default behavior is to return the nested classifier probabilities. In case where no `predict_proba` method is available inside the classifier, - the method the predicted label number (0 or 1 for examples) and applies a + the method predicts the label number (0 or 1 for examples) and applies a softmax in top of it. Parameters @@ -300,7 +300,7 @@ class QuanticSVM(QuanticClassifierBase): Fix: copy estimator not keeping base class parameters. .. versionchanged:: 0.2.0 Add seed parameter - SVC and QSVC now compute probability (may impact time performance) + SVC and QSVC now compute probability (may impact performance) Predict is now using predict_proba with a softmax, when using QSVC. Parameters diff --git a/pyriemann_qiskit/pipelines.py b/pyriemann_qiskit/pipelines.py index c39d778a..004082cd 100644 --- a/pyriemann_qiskit/pipelines.py +++ b/pyriemann_qiskit/pipelines.py @@ -177,7 +177,7 @@ class QuantumClassifierWithDefaultRiemannianPipeline(BasePipeline): Number of repetitions of each circuit, for sampling. If None, classical computation will be performed. feature_reps : int (default: 2) - The number of repeated circuits for the ZZFeatureMap, + The number of repeated circuits for the FeatureMap, greater or equal to 1. spsa_trials : int (default: None) Maximum number of iterations to perform using SPSA optimizer. @@ -202,7 +202,7 @@ class QuantumClassifierWithDefaultRiemannianPipeline(BasePipeline): ----- .. versionadded:: 0.0.1 .. versionchanged:: 0.2.0 - Change feature map from ZZFeatureMap to XFeatureMap. + Changed feature map from ZZFeatureMap to XFeatureMap. Therefore remove unused parameter `feature_entanglement`. See Also diff --git a/tests/test_classification.py b/tests/test_classification.py index ed28e3b3..3fe8dcc3 100644 --- a/tests/test_classification.py +++ b/tests/test_classification.py @@ -116,7 +116,7 @@ def get_params(self): def check(self): # Check that all classes are predicted assert len(self.prediction) == len(self.labels) - # Check the proba for each classes are returned + # Check if the proba for each classes are returned assert self.predict_proab.shape[1] == len(np.unique(self.labels)) @@ -183,9 +183,9 @@ def check(self): # Considering the inputs, this probably make no sense to test accuracy. # Instead, we could consider this test as a canary test assert len(self.prediction) == len(self.labels) - # Check the number of classes is consistent + # Check if the number of classes is consistent assert len(np.unique(self.prediction)) == len(np.unique(self.labels)) - # Check the proba for each classes are returned + # Check if the proba for each classes are returned assert self.predict_proab.shape[1] == len(np.unique(self.labels)) @@ -219,9 +219,9 @@ def get_params(self): def check(self): assert len(self.prediction) == len(self.labels) - # Check the number of classes is consistent + # Check if the number of classes is consistent assert len(np.unique(self.prediction)) == len(np.unique(self.labels)) - # Check the proba for each classes are returned + # Check if the proba for each classes are returned assert self.predict_proab.shape[1] == len(np.unique(self.labels)) From 3c421bac7b845a313c0051b0954d6606c0a12702 Mon Sep 17 00:00:00 2001 From: Gregoire Cattan Date: Fri, 8 Dec 2023 21:23:35 +0100 Subject: [PATCH 18/20] - Run multiclass classification example: Ok - fix titles --- examples/MI/multiclass_classification.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/examples/MI/multiclass_classification.py b/examples/MI/multiclass_classification.py index 61291f5f..ef77f972 100644 --- a/examples/MI/multiclass_classification.py +++ b/examples/MI/multiclass_classification.py @@ -99,13 +99,21 @@ # A confusion matrix is reported for each classifier. A perfectly performing # classifier will have only its diagonal filled and the rest will be zeros. names = ["aud left", "aud right", "vis left", "vis right"] - title = ( - ("VQC (" if idx == 0 else "Quantum SVM (" if idx == 1 else "Classical SVM (") - if idx == 2 - else "Quantum MDM (" - if idx == 3 - else "R-MDM (" + acc_str + ")" - ) + if idx == 0: + title="VQC" + elif idx == 1: + title="Q-SVM" + elif idx == 2: + title="SVM" + elif idx == 3: + title="Q-MDM" + else: + title="MDM" + + title = f"{title} (" + acc_str + ")" + + print(title) + axe = axes[idx] cm = confusion_matrix(y_pred, y_test) disp = ConfusionMatrixDisplay(cm, display_labels=names) From 2df3cdf86a7d1abd70b6df9189b1ac0919357991 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 8 Dec 2023 20:24:41 +0000 Subject: [PATCH 19/20] [pre-commit.ci] auto fixes from pre-commit.com hooks --- examples/MI/multiclass_classification.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/MI/multiclass_classification.py b/examples/MI/multiclass_classification.py index ef77f972..19e7fc75 100644 --- a/examples/MI/multiclass_classification.py +++ b/examples/MI/multiclass_classification.py @@ -100,15 +100,15 @@ # classifier will have only its diagonal filled and the rest will be zeros. names = ["aud left", "aud right", "vis left", "vis right"] if idx == 0: - title="VQC" + title = "VQC" elif idx == 1: - title="Q-SVM" + title = "Q-SVM" elif idx == 2: - title="SVM" + title = "SVM" elif idx == 3: - title="Q-MDM" + title = "Q-MDM" else: - title="MDM" + title = "MDM" title = f"{title} (" + acc_str + ")" From 7c6d6fb14bab9eb9512dc05170ed9b08b7180194 Mon Sep 17 00:00:00 2001 From: qbarthelemy Date: Fri, 8 Dec 2023 22:21:04 +0100 Subject: [PATCH 20/20] minor correction --- 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 39250e3c..823dfac4 100644 --- a/pyriemann_qiskit/classification.py +++ b/pyriemann_qiskit/classification.py @@ -255,7 +255,7 @@ def predict_proba(self, X): Returns ------- prob : ndarray, shape (n_samples, n_classes) - prob[n, i] == 1 if the nth sample is assigned to class `i`; + prob[n, i] == 1 if the nth sample is assigned to class `i`. """ if not hasattr(self._classifier, "predict_proba"):