Source code for qlearnkit.algorithms.qsvm.qsvc

import logging
from typing import Optional, Union
import numpy as np
from qiskit_machine_learning.kernels import QuantumKernel
from sklearn.base import ClassifierMixin
from sklearn.exceptions import NotFittedError
from qiskit.utils import QuantumInstance
from qiskit.providers import BaseBackend, Backend
from qiskit.circuit.library import NLocal, ZZFeatureMap
from ..quantum_estimator import QuantumEstimator

logger = logging.getLogger(__name__)


[docs]class QSVClassifier(ClassifierMixin, QuantumEstimator): r""" The Quantum Support Vector Machine algorithm for classification. Maps datapoints to quantum states using a FeatureMap or similar QuantumCircuit. Example: Classify data using the Iris dataset. .. jupyter-execute:: import numpy as np from qlearnkit.algorithms import QSVClassifier from qiskit import BasicAer from qiskit.utils import QuantumInstance, algorithm_globals from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from qiskit.circuit.library import ZZFeatureMap seed = 42 algorithm_globals.random_seed = seed quantum_instance = QuantumInstance(BasicAer.get_backend('statevector_simulator'), shots=1024, optimization_level=1, seed_simulator=seed, seed_transpiler=seed) # Use iris data set for training and test data X, y = load_iris(return_X_y=True) num_features = 2 X = np.asarray([x[0:num_features] for x, y_ in zip(X, y) if y_ != 2]) y = np.asarray([y_ for x, y_ in zip(X, y) if y_ != 2]) encoding_map = ZZFeatureMap(2) qsvc = QSVClassifier( encoding_map=encoding_map, quantum_instance=quantum_instance ) # use iris dataset X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.20, random_state=seed) qsvc.fit(X_train, y_train) print(f"Testing accuracy: " f"{qsvc.score(X_test, y_test):0.2f}") """ def __init__(self, encoding_map: Optional[NLocal] = None, quantum_instance: Optional[Union[QuantumInstance, BaseBackend, Backend]] = None, gamma: Union[float, str] = 'scale'): """ Creates a Quantum Support Vector Classifier Args: encoding_map: map to classical data to quantum states. Default: :class:`~qiskit_machine_learning.circuit.library.ZZFeatureMap` quantum_instance: the quantum instance to set. Can be a :class:`~qiskit.utils.QuantumInstance`, a :class:`~qiskit.providers.Backend` or a :class:`~qiskit.providers.BaseBackend` gamma: regularization parameter """ encoding_map = encoding_map if encoding_map else ZZFeatureMap(2) super().__init__(encoding_map, quantum_instance) # Initial setting for gamma # Numerical value is set in fit method self.gamma_init = gamma self.label_class_dict = None self.class_label_dict = None self.alpha = None self.bias = None self.n_classes = None self.train_kernel_matrix = None
[docs] def fit(self, X, y): """ Fits the model using X as training dataset and y as training labels. The actual computation is done at the predict stage to allow running the qiskit backend only once Args: X: training dataset y: training labels """ self.label_class_dict = None self.class_label_dict = None self.alpha = None self.bias = None self.X_train = np.asarray(X) self.y_train = np.asarray(y) self.n_classes = np.unique(y).size n_features = self.X_train.shape[1] if self.gamma_init == 'scale': self.gamma = 1 / (n_features * np.var(self.X_train)) elif self.gamma_init == 'auto': self.gamma = 1 / n_features elif isinstance(self.gamma_init, str): raise ValueError("Invalid argument value %s",self.gamma_init) else: self.gamma = self.gamma_init self.label_class_dict, self.class_label_dict = QSVClassifier._create_label_class_dicts(self.y_train) if self.n_classes == 1: raise ValueError("All samples have the same label") if self.n_classes == 2: classes_matrix = np.array(np.vectorize(self.label_class_dict.get)(self.y_train)) else: # Prepares an array of [+1,-1] values for each class # and organizes them in a matrix per the svm formulation. # This matrix notation will be useful later on to avoid nested for loops. classes_array = np.array([np.vectorize(self.label_class_dict.get)(self.y_train)]) classes_array = classes_array.T classes_matrix = np.equal(classes_array, np.arange(self.n_classes) * np.ones((classes_array.size, self.n_classes))) self.classes_train = classes_matrix * 2 - 1 logger.info("setting training data: ") for _X, _y in zip(X, y): logger.info("%s: %s", _X, _y) # Sets the training matrix to None to signal it must be computed again self.train_kernel_matrix = None
@staticmethod def _create_label_class_dicts(labels): """ Creates dictionaries to convert from labels to classes used by svm. Classes are the integer values in range [0, 1, ..., n_classes] Args: labels: labels for which the dictionaries will be created Returns: dictionaries to convert from the user labels to the internal representation and vice versa """ unique_labels = np.unique(labels) label_class_dict = {unique_labels[i]: i for i in range(unique_labels.size)} class_label_dict = {c: unique_labels[c] for c in range(unique_labels.size)} return label_class_dict, class_label_dict def _compute_alpha(self,train_kernel_matrix): """ Computes alpha parameters for data in the training set. Alpha parameters will be used as weights in prediction. Internally distinguishes between binary and multiclass case Args: train_kernel_matrix: matrix of distances from each point to each point in the training set Returns: numpy ndarray of alpha parameters """ n_train = train_kernel_matrix.shape[0] omega = train_kernel_matrix gamma_inv = 1 / self.gamma ones = np.ones(n_train) eye = np.eye(n_train) A = np.vstack([ np.block([np.zeros(1), ones.reshape([1, n_train])]), np.block([ones.reshape([n_train, 1]), omega + gamma_inv * eye]) ]) if self.n_classes == 2: B = np.vstack([np.zeros(1), self.classes_train.reshape(-1,1)]) else: B = np.vstack([np.zeros(self.n_classes), self.classes_train]) # Binary case: X is a vector containing alpha values. # Multiclass case: X is a (n_train+1,n_classes) matrix # containing alpha values for each of the n_classes linear systems. # This is equivalent to solving n_classes distinct binary problems. X = np.linalg.solve(A, B) bias = X[0, :] alpha = np.squeeze(X[1:, :]) return alpha, bias def _compute_kernel_matrices(self, X_train, X_test): """ Computes the kernel matrices of distances between each training datapoint and between training and test datapoints. Takes advantage of quantum circuits for faster computation. Args: X_train: the training data X_test: the unclassified input data Returns: ndarray of train and test kernel matrices """ q_kernel = QuantumKernel(feature_map=self._encoding_map, quantum_instance=self.quantum_instance) if self.train_kernel_matrix is None: n_train = X_train.shape[0] # Train and test data stacked together to run backend only once X_total = np.vstack([X_train, X_test]) total_kernel_matrix = q_kernel.evaluate(x_vec=X_train, y_vec=X_total) # Splitting the total matrix into training and test part self.train_kernel_matrix = total_kernel_matrix[:, 0:n_train] # Transposed for ease of use later on test_kernel_matrix = total_kernel_matrix[:, n_train:].T else: # Only the test kernel matrix is needed as the train one has already been computed test_kernel_matrix = q_kernel.evaluate(x_vec=X_test, y_vec=X_train) return self.train_kernel_matrix, test_kernel_matrix def _compute_predictions_multiclass(self, train_kernel_matrix, test_kernel_matrix): """ Uses kernel matrices to find n_classes dividing hyperplanes, following a one-to-rest approach. Based on Least Squares Support Vector Machine formulation. Actually solves n_classes linear systems in order to separate multiple classes. Args: train_kernel_matrix: matrix of distances between training datapoints test_kernel_matrix: matrix of distances between training and test datapoints Returns: numpy ndarray of predicted classes. Uses the internal representation """ # Fit self.alpha, self.bias = self._compute_alpha(train_kernel_matrix) # Predict prediction_classes = np.argmax(test_kernel_matrix @ self.alpha + self.bias, axis=1) return prediction_classes def _compute_predictions_binary(self, train_kernel_matrix, test_kernel_matrix): """ Uses kernel matrices to find the dividing hyperplane. Based on Least Squares Support Vector Machine formulation. Specialized case which uses a np.sign call instead of computing multiple hyperplanes and using argmax Args: train_kernel_matrix: matrix of distances between training datapoints test_kernel_matrix: matrix of distances between training and test datapoints Returns: numpy ndarray of predicted classes. Uses the internal representation """ # Fit self.alpha, self.bias = self._compute_alpha(train_kernel_matrix) # Predict prediction_classes = np.sign(test_kernel_matrix @ self.alpha + self.bias) prediction_classes = (prediction_classes + 1) / 2 return prediction_classes
[docs] def predict(self, X_test: np.ndarray) -> np.ndarray: """ Solves a Least Squares problem to predict value of input data. Internally distinguishes between binary and multiclass case. For the binary case solves an optimization problem to find a dividing hyperplane. For the multiclass case uses a one-to-rest approach and thus needs to run the algorithm n_classes different times. Args: X_test: the test data Returns: numpy ndarray of predicted labels """ if self.X_train is None: raise NotFittedError( "This QSVClassifier instance is not fitted yet. " "Call 'fit' with appropriate arguments before using " "this estimator.") logger.info("Computing kernel matrices...") train_kernel_matrix, test_kernel_matrix = self._compute_kernel_matrices(self.X_train, X_test) logger.info("Done.") logger.info("Computing predictions...") if self.n_classes == 2: classes_predict = self._compute_predictions_binary(train_kernel_matrix, test_kernel_matrix) else: classes_predict = self._compute_predictions_multiclass(train_kernel_matrix, test_kernel_matrix) # Converts back from internal numerical classes used in SVM # to user provided labels. y_predict = np.vectorize(self.class_label_dict.get)(classes_predict) logger.info("Done.") return y_predict