Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Fix/svc kernel #220

Merged
merged 20 commits into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions examples/ERP/plot_classify_P300_bi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand All @@ -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
Expand Down
4 changes: 0 additions & 4 deletions examples/MI/plot_compare_dim_red.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
gcattan marked this conversation as resolved.
Show resolved Hide resolved
# There is a trade-off between accuracy and noise when the depth of the
# circuit increases.
Expand Down
104 changes: 60 additions & 44 deletions pyriemann_qiskit/classification.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
quantum computer.
"""
from datetime import datetime
from scipy.special import softmax
import logging
import numpy as np

Expand Down Expand Up @@ -237,6 +238,45 @@ def _predict(self, 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
gcattan marked this conversation as resolved.
Show resolved Hide resolved
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):

Expand All @@ -256,6 +296,8 @@ 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)
gcattan marked this conversation as resolved.
Show resolved Hide resolved
Predict is now using predict_proba with a softmax, when using QSVC.

Parameters
----------
Expand Down Expand Up @@ -360,10 +402,13 @@ 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
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):
Expand All @@ -383,15 +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`;
"""
predicted_labels = self.predict(X)
ret = [
np.array([c == self.classes_[0], c == self.classes_[1]])
for c in predicted_labels
]
return np.array(ret)

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.
Expand All @@ -407,7 +452,12 @@ def predict(self, X):
pred : array, shape (n_samples,)
Class labels for samples in X.
"""
labels = self._predict(X)
if isinstance(self._classifier, QSVC):
probs = self.predict_proba(X)
labels = [np.argmax(prob) for prob in probs]
else:
labels = self._predict(X)
self._log("Prediction finished.")
return self._map_indices_to_classes(labels)


Expand Down Expand Up @@ -514,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.

Expand Down Expand Up @@ -664,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.

Expand Down
18 changes: 7 additions & 11 deletions pyriemann_qiskit/pipelines.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -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,
gcattan marked this conversation as resolved.
Show resolved Hide resolved
greater or equal to 1.
Expand All @@ -206,12 +201,15 @@ class QuantumClassifierWithDefaultRiemannianPipeline(BasePipeline):
Notes
-----
.. versionadded:: 0.0.1
.. versionchanged:: 0.2.0
Change feature map from ZZFeatureMap to XFeatureMap.
gcattan marked this conversation as resolved.
Show resolved Hide resolved
Therefore remove unused parameter `feature_entanglement`.

See Also
--------
XdawnCovariances
TangentSpace
gen_zz_feature_map
gen_x_feature_map
gen_two_local
get_spsa
QuanticVQC
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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)
gcattan marked this conversation as resolved.
Show resolved Hide resolved

if is_vqc:
self._log("QuanticVQC chosen.")
Expand Down Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,9 @@ def _get_dataset(n_samples, n_features, n_classes, type="bin"):
samples_0 = make_covariances(
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()
Expand Down Expand Up @@ -179,6 +180,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)


Expand Down
45 changes: 33 additions & 12 deletions tests/test_classification.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,10 @@ 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
gcattan marked this conversation as resolved.
Show resolved Hide resolved
assert self.predict_proab.shape[1] == len(np.unique(self.labels))


class TestQuanticSVM(TestClassicalSVM):
Expand All @@ -141,6 +139,17 @@ def get_params(self):
}


class TestQuanticSVM_MultiClass(MultiClassFVT):
"""Perform SVM on a simulated quantum computer
(multi-label classification)"""

def get_params(self):
return TestQuanticSVM.get_params(self)

def check(self):
TestQuanticSVM.check(self)


class TestQuanticPegasosSVM(TestClassicalSVM):
"""Same as TestQuanticSVM, except it uses
PegasosQSVC instead of QSVC implementation.
Expand Down Expand Up @@ -176,6 +185,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(np.unique(self.labels))


class TestQuanticVQC_MultiClass(MultiClassFVT):
Expand Down Expand Up @@ -207,9 +218,19 @@ 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]
)
assert len(self.prediction) == len(self.labels)
# Check the number of classes is consistent
gcattan marked this conversation as resolved.
Show resolved Hide resolved
assert len(np.unique(self.prediction)) == len(np.unique(self.labels))
# Check the proba for each classes are returned
gcattan marked this conversation as resolved.
Show resolved Hide resolved
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)"""

def get_params(self):
return TestClassicalMDM.get_params(self)

def check(self):
TestClassicalMDM.check(self)