Source code for pyriemann_qiskit.classification.wrappers.quantic_classifier_base

"""Base class for quantum classifiers."""

import logging
from datetime import datetime

import numpy as np
from qiskit.primitives import BackendSamplerV2
from scipy.special import softmax
from sklearn.base import BaseEstimator, ClassifierMixin

from ...utils.dataset import get_feature_dimension
from ...utils.hyper_params_factory import gen_zz_feature_map
from ...utils.quantum_provider import get_device, get_provider, get_simulator

logging.basicConfig(level=logging.WARNING)


[docs] class QuanticClassifierBase(ClassifierMixin, BaseEstimator): """Quantum classifier This class implements a scikit-learn wrapper around Qiskit library [1]_. It provides a mean to run classification tasks on a local and simulated quantum computer or a remote and real quantum computer. Difference between simulated and real quantum computer will be that: * there is no noise on a simulated quantum computer (so results are better), * a real quantum computer is quicker than a quantum simulator, * tasks on a real quantum computer are assigned to a queue before being executed on a back-end (delayed execution). Parameters ---------- quantum : bool, default=True - If true will run on local or remote quantum backend (depending on q_account_token value), - If false, will perform classical computing instead. q_account_token : string | None, 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. gen_feature_map : Callable[[int, str], QuantumCircuit | FeatureMap], \ default=Callable[int, ZZFeatureMap] Function generating a feature map to encode data into a quantum state. seed : int | None, default=None Random seed for the simulation. Notes ----- .. versionadded:: 0.0.1 .. versionchanged:: 0.1.0 Added support for multi-class classification. .. versionchanged:: 0.2.0 Add seed parameter .. versionchanged:: 0.3.0 Switch from IBMProvider to QiskitRuntimeService. .. versionchanged:: 0.6.0 Migrate to Qiskit 2.x: replace ``BackendSampler`` with ``BackendSamplerV2``. Moved to \ :mod:`pyriemann_qiskit.classification.wrappers.quantic_classifier_base`. Attributes ---------- classes_ : list list of classes. See Also -------- QuanticSVM QuanticVQC QuanticMDM References ---------- .. [1] H. Abraham et al., Qiskit: An Open-source Framework for Quantum Computing. Zenodo, 2019. doi: 10.5281/zenodo.2562110. """
[docs] def __init__( self, quantum=True, q_account_token=None, verbose=True, shots=1024, gen_feature_map=gen_zz_feature_map(), seed=None, ): self.verbose = verbose self._log("Initializing Quantum Classifier") self.q_account_token = q_account_token self.quantum = quantum self.shots = shots self.seed = datetime.now().microsecond if seed is None else seed self.gen_feature_map = gen_feature_map # protected field for child classes self._training_input = {}
def _init_quantum(self): if self.quantum: if self.q_account_token: self._log("Real quantum computation will be performed") if not self.q_account_token == "load_account": from qiskit_ibm_runtime import QiskitRuntimeService QiskitRuntimeService.delete_account() QiskitRuntimeService.save_account( channel="ibm_quantum", token=self.q_account_token, overwrite=True, ) self._log("Getting provider...") self._provider = get_provider() else: self._log("Quantum simulation will be performed") self._backend = get_simulator() else: self._log("Classical computation will be performed") def _log(self, *values): if self.verbose: print("[QClass] ", *values) def _split_classes(self, X, y): n_classes = len(self.classes_) X_classes = [] for idx in range(n_classes): X_classes.append(X[y == self.classes_[idx]]) return X_classes def _map_classes_to_indices(self, y): y_copy = y.copy() n_classes = len(self.classes_) for idx in range(n_classes): y_copy[y == self.classes_[idx]] = idx return y_copy 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] return np.array(y_copy) def fit(self, X, y): """Fit the quantum classifier using training data. Initializes the quantum backend (simulator or real hardware) and trains the classifier on the provided data. The feature map is generated based on the number of features in X. Parameters ---------- X : ndarray, shape (n_samples, n_features) Training vectors, where `n_samples` is the number of samples and `n_features` is the number of features. y : ndarray, shape (n_samples,) Target labels corresponding to X. Returns ------- self : QuanticClassifierBase instance The fitted classifier instance. """ self._init_quantum() self._log("Fitting: ", X.shape) self.classes_ = np.unique(y) X_classes = self._split_classes(X, y) y = self._map_classes_to_indices(y) n_classes = len(self.classes_) for idx in range(n_classes): self._training_input[self.classes_[idx]] = X_classes[idx] n_features = get_feature_dimension(self._training_input) self._log("Feature dimension = ", n_features) if hasattr(self, "gen_feature_map") and self.gen_feature_map is not None: self._feature_map = self.gen_feature_map(n_features) if self.quantum: if not hasattr(self, "_backend"): self._backend = get_device(self._provider, n_features) self._log("Quantum backend = ", self._backend) self._log("seed = ", self.seed) self._quantum_instance = BackendSamplerV2( backend=self._backend, options={"default_shots": self.shots, "seed_simulator": self.seed}, ) self._classifier = self._init_algo(n_features) self._train(X, y) return self def _init_algo(self, n_features): raise Exception("Init algo method was not implemented") def _train(self, X, y): self._log("Training...") self._classifier.fit(X, y) def score(self, X, y): """Return the mean accuracy on the given test data and labels. Parameters ---------- X : ndarray, shape (n_samples, n_features) Test samples. y : ndarray, shape (n_samples,) True labels for X. Returns ------- score : float Mean accuracy of self.predict(X) with respect to y. Notes ----- For alternative metrics, use sklearn.model_selection.cross_val_score with a custom scoring parameter. """ y = self._map_classes_to_indices(y) self._log("Testing...") return self._classifier.score(X, y) def _predict(self, X): self._log("Prediction: ", X.shape) result = self._classifier.predict(X) self._log("Prediction finished.") return result def predict_proba(self, X): """Predict class probabilities for X. Returns the probability of each class for each sample. If the underlying classifier does not implement predict_proba, predictions are converted to one-hot encoding and softmax is applied. Parameters ---------- X : ndarray, shape (n_samples, n_features) Test samples. Returns ------- prob : ndarray, shape (n_samples, n_classes) Class probabilities for each sample. prob[i, j] is the probability that sample i belongs to class j. """ 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( [ 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)