diff --git a/.pylintdict b/.pylintdict index dd1287005..67a5e0c07 100644 --- a/.pylintdict +++ b/.pylintdict @@ -6,6 +6,7 @@ ansatz ansatzes args asmatrix +async autograd autosummary backend @@ -62,6 +63,7 @@ et eval expressibility farrokh +fidelities formatter func gambetta diff --git a/qiskit_machine_learning/algorithms/classifiers/pegasos_qsvc.py b/qiskit_machine_learning/algorithms/classifiers/pegasos_qsvc.py index e54b65f66..082b3bb86 100644 --- a/qiskit_machine_learning/algorithms/classifiers/pegasos_qsvc.py +++ b/qiskit_machine_learning/algorithms/classifiers/pegasos_qsvc.py @@ -11,10 +11,11 @@ # that they have been altered from the originals. """Pegasos Quantum Support Vector Classifier.""" +from __future__ import annotations import logging from datetime import datetime -from typing import Optional, Dict +from typing import Dict import numpy as np from qiskit.utils import algorithm_globals @@ -22,7 +23,8 @@ from ...algorithms.serializable_model import SerializableModelMixin from ...exceptions import QiskitMachineLearningError -from ...kernels.quantum_kernel import QuantumKernel +from ...kernels import BaseKernel, FidelityQuantumKernel + logger = logging.getLogger(__name__) @@ -38,7 +40,7 @@ class PegasosQSVC(ClassifierMixin, SerializableModelMixin): .. code-block:: python - quantum_kernel = QuantumKernel() + quantum_kernel = FidelityQuantumKernel() pegasos_qsvc = PegasosQSVC(quantum_kernel=quantum_kernel) pegasos_qsvc.fit(sample_train, label_train) @@ -56,15 +58,15 @@ class PegasosQSVC(ClassifierMixin, SerializableModelMixin): # pylint: disable=invalid-name def __init__( self, - quantum_kernel: Optional[QuantumKernel] = None, + quantum_kernel: BaseKernel | None = None, C: float = 1.0, num_steps: int = 1000, precomputed: bool = False, - seed: Optional[int] = None, + seed: int | None = None, ) -> None: """ Args: - quantum_kernel: QuantumKernel to be used for classification. Has to be ``None`` when + quantum_kernel: a quantum kernel to be used for classification. Has to be ``None`` when a precomputed kernel is used. C: Positive regularization parameter. The strength of the regularization is inversely proportional to C. Smaller ``C`` induce smaller weights which generally helps @@ -85,7 +87,8 @@ def __init__( - if ``quantum_kernel`` is passed and ``precomputed`` is set to ``True``. To use a precomputed kernel, ``quantum_kernel`` has to be of the ``None`` type. TypeError: - - if ``quantum_instance`` neither instance of ``QuantumKernel`` nor ``None``. + - if ``quantum_kernel`` neither instance of + :class:`~qiskit_machine_learning.kernels.BaseKernel` nor ``None``. """ if precomputed: @@ -93,9 +96,7 @@ def __init__( raise ValueError("'quantum_kernel' has to be None to use a precomputed kernel") else: if quantum_kernel is None: - quantum_kernel = QuantumKernel() - elif not isinstance(quantum_kernel, QuantumKernel): - raise TypeError("'quantum_kernel' has to be of type None or QuantumKernel") + quantum_kernel = FidelityQuantumKernel() self._quantum_kernel = quantum_kernel self._precomputed = precomputed @@ -109,13 +110,13 @@ def __init__( raise ValueError(f"C has to be a positive number, found {C}.") # these are the parameters being fit and are needed for prediction - self._alphas: Optional[Dict[int, int]] = None - self._x_train: Optional[np.ndarray] = None - self._n_samples: Optional[int] = None - self._y_train: Optional[np.ndarray] = None - self._label_map: Optional[Dict[int, int]] = None - self._label_pos: Optional[int] = None - self._label_neg: Optional[int] = None + self._alphas: Dict[int, int] | None = None + self._x_train: np.ndarray | None = None + self._n_samples: int | None = None + self._y_train: np.ndarray | None = None + self._label_map: Dict[int, int] | None = None + self._label_pos: int | None = None + self._label_neg: int | None = None # added to all kernel values to include an implicit bias to the hyperplane self._kernel_offset = 1 @@ -125,12 +126,13 @@ def __init__( # pylint: disable=invalid-name def fit( - self, X: np.ndarray, y: np.ndarray, sample_weight: Optional[np.ndarray] = None + self, X: np.ndarray, y: np.ndarray, sample_weight: np.ndarray | None = None ) -> "PegasosQSVC": """Fit the model according to the given training data. Args: - X: Train features. For a callable kernel (an instance of ``QuantumKernel``) the shape + X: Train features. For a callable kernel (an instance of + :class:`~qiskit_machine_learning.kernels.BaseKernel`) the shape should be ``(n_samples, n_features)``, for a precomputed kernel the shape should be ``(n_samples, n_samples)``. y: shape (n_samples), train labels . Must not contain more than two unique labels. @@ -206,7 +208,8 @@ def predict(self, X: np.ndarray) -> np.ndarray: Perform classification on samples in X. Args: - X: Features. For a callable kernel (an instance of ``QuantumKernel``) the shape + X: Features. For a callable kernel (an instance of + :class:`~qiskit_machine_learning.kernels.BaseKernel`) the shape should be ``(m_samples, n_features)``, for a precomputed kernel the shape should be ``(m_samples, n_samples)``. Where ``m`` denotes the set to be predicted and ``n`` the size of the training set. In that case, the kernel values in X have to be calculated @@ -234,7 +237,8 @@ def decision_function(self, X: np.ndarray) -> np.ndarray: Evaluate the decision function for the samples in X. Args: - X: Features. For a callable kernel (an instance of ``QuantumKernel``) the shape + X: Features. For a callable kernel (an instance of + :class:`~qiskit_machine_learning.kernels.BaseKernel`) the shape should be ``(m_samples, n_features)``, for a precomputed kernel the shape should be ``(m_samples, n_samples)``. Where ``m`` denotes the set to be predicted and ``n`` the size of the training set. In that case, the kernel values in X have to be calculated @@ -302,12 +306,12 @@ def _compute_weighted_kernel_sum(self, index: int, X: np.ndarray, training: bool return value @property - def quantum_kernel(self) -> QuantumKernel: + def quantum_kernel(self) -> BaseKernel: """Returns quantum kernel""" return self._quantum_kernel @quantum_kernel.setter - def quantum_kernel(self, quantum_kernel: QuantumKernel): + def quantum_kernel(self, quantum_kernel: BaseKernel): """ Sets quantum kernel. If previously a precomputed kernel was set, it is reset to ``False``. """ @@ -340,14 +344,15 @@ def precomputed(self) -> bool: @precomputed.setter def precomputed(self, precomputed: bool): """Sets the pre-computed kernel flag. If ``True`` is passed then the previous kernel is - cleared. If ``False`` is passed then a new instance of ``QuantumKernel`` is created.""" + cleared. If ``False`` is passed then a new instance of + :class:`~qiskit_machine_learning.kernels.FidelityQuantumKernel` is created.""" self._precomputed = precomputed if precomputed: # remove the kernel, a precomputed will self._quantum_kernel = None else: # re-create a new default quantum kernel - self._quantum_kernel = QuantumKernel() + self._quantum_kernel = FidelityQuantumKernel() # reset training status self._reset_state() diff --git a/qiskit_machine_learning/algorithms/classifiers/qsvc.py b/qiskit_machine_learning/algorithms/classifiers/qsvc.py index 78b107248..917e84172 100644 --- a/qiskit_machine_learning/algorithms/classifiers/qsvc.py +++ b/qiskit_machine_learning/algorithms/classifiers/qsvc.py @@ -20,7 +20,7 @@ from qiskit_machine_learning.algorithms.serializable_model import SerializableModelMixin from qiskit_machine_learning.exceptions import QiskitMachineLearningWarning -from qiskit_machine_learning.kernels.quantum_kernel import QuantumKernel +from qiskit_machine_learning.kernels import BaseKernel, FidelityQuantumKernel class QSVC(SVC, SerializableModelMixin): @@ -41,10 +41,10 @@ class QSVC(SVC, SerializableModelMixin): qsvc.predict(sample_test) """ - def __init__(self, *args, quantum_kernel: Optional[QuantumKernel] = None, **kwargs): + def __init__(self, *args, quantum_kernel: Optional[BaseKernel] = None, **kwargs): """ Args: - quantum_kernel: QuantumKernel to be used for classification. + quantum_kernel: Quantum kernel to be used for classification. *args: Variable length argument list to pass to SVC constructor. **kwargs: Arbitrary keyword arguments to pass to SVC constructor. """ @@ -65,7 +65,7 @@ def __init__(self, *args, quantum_kernel: Optional[QuantumKernel] = None, **kwar # if we don't delete, then this value clashes with our quantum kernel del kwargs["kernel"] - self._quantum_kernel = quantum_kernel if quantum_kernel else QuantumKernel() + self._quantum_kernel = quantum_kernel if quantum_kernel else FidelityQuantumKernel() if "random_state" not in kwargs: kwargs["random_state"] = algorithm_globals.random_seed @@ -73,12 +73,12 @@ def __init__(self, *args, quantum_kernel: Optional[QuantumKernel] = None, **kwar super().__init__(kernel=self._quantum_kernel.evaluate, *args, **kwargs) @property - def quantum_kernel(self) -> QuantumKernel: + def quantum_kernel(self) -> BaseKernel: """Returns quantum kernel""" return self._quantum_kernel @quantum_kernel.setter - def quantum_kernel(self, quantum_kernel: QuantumKernel): + def quantum_kernel(self, quantum_kernel: BaseKernel): """Sets quantum kernel""" self._quantum_kernel = quantum_kernel self.kernel = self._quantum_kernel.evaluate diff --git a/qiskit_machine_learning/algorithms/regressors/qsvr.py b/qiskit_machine_learning/algorithms/regressors/qsvr.py index 1076e3756..4855cc5a6 100644 --- a/qiskit_machine_learning/algorithms/regressors/qsvr.py +++ b/qiskit_machine_learning/algorithms/regressors/qsvr.py @@ -17,9 +17,9 @@ from sklearn.svm import SVR -from ..serializable_model import SerializableModelMixin -from ...exceptions import QiskitMachineLearningWarning -from ...kernels.quantum_kernel import QuantumKernel +from qiskit_machine_learning.algorithms.serializable_model import SerializableModelMixin +from qiskit_machine_learning.exceptions import QiskitMachineLearningWarning +from qiskit_machine_learning.kernels import BaseKernel, FidelityQuantumKernel class QSVR(SVR, SerializableModelMixin): @@ -40,10 +40,10 @@ class QSVR(SVR, SerializableModelMixin): qsvr.predict(sample_test) """ - def __init__(self, *args, quantum_kernel: Optional[QuantumKernel] = None, **kwargs): + def __init__(self, *args, quantum_kernel: Optional[BaseKernel] = None, **kwargs): """ Args: - quantum_kernel: QuantumKernel to be used for regression. + quantum_kernel: Quantum kernel to be used for regression. *args: Variable length argument list to pass to SVR constructor. **kwargs: Arbitrary keyword arguments to pass to SVR constructor. """ @@ -64,17 +64,17 @@ def __init__(self, *args, quantum_kernel: Optional[QuantumKernel] = None, **kwar # if we don't delete, then this value clashes with our quantum kernel del kwargs["kernel"] - self._quantum_kernel = quantum_kernel if quantum_kernel else QuantumKernel() + self._quantum_kernel = quantum_kernel if quantum_kernel else FidelityQuantumKernel() super().__init__(kernel=self._quantum_kernel.evaluate, *args, **kwargs) @property - def quantum_kernel(self) -> QuantumKernel: + def quantum_kernel(self) -> BaseKernel: """Returns quantum kernel""" return self._quantum_kernel @quantum_kernel.setter - def quantum_kernel(self, quantum_kernel: QuantumKernel): + def quantum_kernel(self, quantum_kernel: BaseKernel): """Sets quantum kernel""" self._quantum_kernel = quantum_kernel self.kernel = self._quantum_kernel.evaluate diff --git a/qiskit_machine_learning/kernels/__init__.py b/qiskit_machine_learning/kernels/__init__.py index e08cff3f0..e04c1a42a 100644 --- a/qiskit_machine_learning/kernels/__init__.py +++ b/qiskit_machine_learning/kernels/__init__.py @@ -23,6 +23,10 @@ :nosignatures: QuantumKernel + BaseKernel + FidelityQuantumKernel + TrainableKernel + TrainableFidelityQuantumKernel Submodules ========== @@ -34,5 +38,15 @@ """ from .quantum_kernel import QuantumKernel - -__all__ = ["QuantumKernel"] +from .base_kernel import BaseKernel +from .fidelity_quantum_kernel import FidelityQuantumKernel +from .trainable_kernel import TrainableKernel +from .trainable_fidelity_quantum_kernel import TrainableFidelityQuantumKernel + +__all__ = [ + "QuantumKernel", + "BaseKernel", + "FidelityQuantumKernel", + "TrainableKernel", + "TrainableFidelityQuantumKernel", +] diff --git a/qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py b/qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py index 8e05c3ae1..51460d5c2 100644 --- a/qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py +++ b/qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py @@ -11,6 +11,7 @@ # that they have been altered from the originals. """Quantum Kernel Trainer""" + import copy from functools import partial from typing import Union, Optional, Sequence @@ -22,7 +23,7 @@ from qiskit.algorithms.variational_algorithm import VariationalResult from qiskit_machine_learning.utils.loss_functions import KernelLoss, SVCLoss -from qiskit_machine_learning.kernels import QuantumKernel +from qiskit_machine_learning.kernels import TrainableKernel class QuantumKernelTrainerResult(VariationalResult): @@ -30,15 +31,15 @@ class QuantumKernelTrainerResult(VariationalResult): def __init__(self) -> None: super().__init__() - self._quantum_kernel: QuantumKernel = None + self._quantum_kernel: TrainableKernel = None @property - def quantum_kernel(self) -> Optional[QuantumKernel]: + def quantum_kernel(self) -> Optional[TrainableKernel]: """Return the optimized quantum kernel object.""" return self._quantum_kernel @quantum_kernel.setter - def quantum_kernel(self, quantum_kernel: QuantumKernel) -> None: + def quantum_kernel(self, quantum_kernel: TrainableKernel) -> None: self._quantum_kernel = quantum_kernel @@ -66,10 +67,9 @@ class QuantumKernelTrainer: for i, param in enumerate(input_params): qc.rz(param, qc.qubits[i]) - quant_kernel = QuantumKernel( + quant_kernel = TrainableFidelityQuantumKernel( feature_map=qc, training_parameters=training_params, - quantum_instance=... ) loss_func = ... @@ -88,7 +88,7 @@ class QuantumKernelTrainer: def __init__( self, - quantum_kernel: QuantumKernel, + quantum_kernel: TrainableKernel, loss: Optional[Union[str, KernelLoss]] = None, optimizer: Optional[Optimizer] = None, initial_point: Optional[Sequence[float]] = None, @@ -118,12 +118,12 @@ def __init__( self._set_loss(loss) @property - def quantum_kernel(self) -> QuantumKernel: + def quantum_kernel(self) -> TrainableKernel: """Return the quantum kernel object.""" return self._quantum_kernel @quantum_kernel.setter - def quantum_kernel(self, quantum_kernel: QuantumKernel) -> None: + def quantum_kernel(self, quantum_kernel: TrainableKernel) -> None: """Set the quantum kernel.""" self._quantum_kernel = quantum_kernel diff --git a/qiskit_machine_learning/kernels/base_kernel.py b/qiskit_machine_learning/kernels/base_kernel.py new file mode 100644 index 000000000..f0af65e02 --- /dev/null +++ b/qiskit_machine_learning/kernels/base_kernel.py @@ -0,0 +1,153 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Base kernel""" + +from __future__ import annotations + +from abc import abstractmethod, ABC + +import numpy as np +from qiskit import QuantumCircuit +from qiskit.circuit.library import ZZFeatureMap + + +class BaseKernel(ABC): + r""" + Abstract class defining the Kernel interface. + + The general task of machine learning is to find and study patterns in data. For many + algorithms, the datapoints are better understood in a higher dimensional feature space, + through the use of a kernel function: + + .. math:: + + K(x, y) = \langle f(x), f(y)\rangle. + + Here K is the kernel function, x, y are n dimensional inputs. f is a map from n-dimension + to m-dimension space. :math:`\langle x, y \rangle` denotes the dot product. + Usually m is much larger than n. + + The quantum kernel algorithm calculates a kernel matrix, given datapoints x and y and feature + map f, all of n dimension. This kernel matrix can then be used in classical machine learning + algorithms such as support vector classification, spectral clustering or ridge regression. + """ + + def __init__(self, *, feature_map: QuantumCircuit = None, enforce_psd: bool = True) -> None: + """ + Args: + feature_map: Parameterized circuit to be used as the feature map. If ``None`` is given, + :class:`~qiskit.circuit.library.ZZFeatureMap` is used with two qubits. If there's + a mismatch in the number of qubits of the feature map and the number of features + in the dataset, then the kernel will try to adjust the feature map to reflect the + number of features. + enforce_psd: Project to closest positive semidefinite matrix if ``x = y``. + Default ``True``. + """ + if feature_map is None: + feature_map = ZZFeatureMap(2) + + self._num_features = feature_map.num_parameters + self._feature_map = feature_map + self._enforce_psd = enforce_psd + + @abstractmethod + def evaluate(self, x_vec: np.ndarray, y_vec: np.ndarray | None = None) -> np.ndarray: + r""" + Construct kernel matrix for given data. + + If y_vec is None, self inner product is calculated. + + Args: + x_vec: 1D or 2D array of datapoints, NxD, where N is the number of datapoints, + D is the feature dimension + y_vec: 1D or 2D array of datapoints, MxD, where M is the number of datapoints, + D is the feature dimension + + Returns: + 2D matrix, NxM + """ + raise NotImplementedError() + + @property + def feature_map(self) -> QuantumCircuit: + """Returns the feature map of this kernel.""" + return self._feature_map + + @property + def num_features(self) -> int: + """Returns the number of features in this kernel.""" + return self._num_features + + @property + def enforce_psd(self) -> bool: + """ + Returns ``True`` if the kernel matrix is required to project to the closest positive + semidefinite matrix. + """ + return self._enforce_psd + + def _validate_input( + self, x_vec: np.ndarray, y_vec: np.ndarray | None + ) -> tuple[np.ndarray, np.ndarray | None]: + x_vec = np.asarray(x_vec) + + if x_vec.ndim > 2: + raise ValueError("x_vec must be a 1D or 2D array") + + if x_vec.ndim == 1: + x_vec = np.reshape(x_vec, (-1, len(x_vec))) + + if x_vec.shape[1] != self._num_features: + # before raising an error we try to adjust the feature map + # to the required number of qubit. + try: + self._feature_map.num_qubits = x_vec.shape[1] + except AttributeError as a_e: + raise ValueError( + f"x_vec and class feature map have incompatible dimensions.\n" + f"x_vec has {x_vec.shape[1]} dimensions, " + f"but feature map has {self._feature_map.num_parameters}." + ) from a_e + + if y_vec is not None: + y_vec = np.asarray(y_vec) + + if y_vec.ndim == 1: + y_vec = np.reshape(y_vec, (-1, len(y_vec))) + + if y_vec.ndim > 2: + raise ValueError("y_vec must be a 1D or 2D array") + + if y_vec.shape[1] != x_vec.shape[1]: + raise ValueError( + "x_vec and y_vec have incompatible dimensions.\n" + f"x_vec has {x_vec.shape[1]} dimensions, but y_vec has {y_vec.shape[1]}." + ) + + return x_vec, y_vec + + # pylint: disable=invalid-name + def _make_psd(self, kernel_matrix: np.ndarray) -> np.ndarray: + r""" + Find the closest positive semi-definite approximation to symmetric kernel matrix. + The (symmetric) matrix should always be positive semi-definite by construction, + but this can be violated in case of noise, such as sampling noise. + + Args: + kernel_matrix: symmetric 2D array of the kernel entries + + Returns: + the closest positive semi-definite matrix. + """ + d, u = np.linalg.eig(kernel_matrix) + return u @ np.diag(np.maximum(0, d)) @ u.transpose() diff --git a/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py b/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py new file mode 100644 index 000000000..6616add54 --- /dev/null +++ b/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py @@ -0,0 +1,279 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +"""Fidelity Quantum Kernel""" + +from __future__ import annotations + +from typing import List, Tuple + +import numpy as np +from qiskit import QuantumCircuit +from qiskit.algorithms.state_fidelities import BaseStateFidelity, ComputeUncompute +from qiskit.primitives import Sampler + +from .base_kernel import BaseKernel + +KernelIndices = List[Tuple[int, int]] + + +class FidelityQuantumKernel(BaseKernel): + r""" + QuantumKernel + + The general task of machine learning is to find and study patterns in data. For many + algorithms, the datapoints are better understood in a higher dimensional feature space, + through the use of a kernel function: + + .. math:: + + K(x, y) = \langle f(x), f(y)\rangle. + + Here K is the kernel function, x, y are n dimensional inputs. f is a map from n-dimension + to m-dimension space. :math:`\langle x, y \rangle` denotes the dot product. + Usually m is much larger than n. + + The quantum kernel algorithm calculates a kernel matrix, given datapoints x and y and feature + map f, all of n dimension. This kernel matrix can then be used in classical machine learning + algorithms such as support vector classification, spectral clustering or ridge regression. + + Here, the kernel function is defined as the overlap of two quantum states defined by a + parametrized quantum circuit (called feature map): + + .. math:: + + K(x,y) = |\langle \phi(x) | \phi(y) \rangle|^2 + """ + + def __init__( + self, + *, + feature_map: QuantumCircuit | None = None, + fidelity: BaseStateFidelity | None = None, + enforce_psd: bool = True, + evaluate_duplicates: str = "off_diagonal", + ) -> None: + """ + Args: + feature_map: Parameterized circuit to be used as the feature map. If ``None`` is given, + :class:`~qiskit.circuit.library.ZZFeatureMap` is used with two qubits. If there's + a mismatch in the number of qubits of the feature map and the number of features + in the dataset, then the kernel will try to adjust the feature map to reflect the + number of features. + fidelity: An instance of the + :class:`~qiskit.algorithms.state_fidelities.BaseStateFidelity` primitive to be used + to compute fidelity between states. Default is + :class:`~qiskit.algorithms.state_fidelities.ComputeUncompute` which is created on + top of the reference sampler defined by :class:`~qiskit.primitives.Sampler`. + enforce_psd: Project to the closest positive semidefinite matrix if ``x = y``. + Default ``True``. + evaluate_duplicates: Defines a strategy how kernel matrix elements are evaluated if + duplicate samples are found. Possible values are: + + - ``all`` means that all kernel matrix elements are evaluated, even the diagonal + ones when training. This may introduce additional noise in the matrix. + - ``off_diagonal`` when training the matrix diagonal is set to `1`, the rest + elements are fully evaluated, e.g., for two identical samples in the + dataset. When inferring, all elements are evaluated. This is the default + value. + - ``none`` when training the diagonal is set to `1` and if two identical samples + are found in the dataset the corresponding matrix element is set to `1`. + When inferring, matrix elements for identical samples are set to `1`. + Raises: + ValueError: When unsupported value is passed to `evaluate_duplicates`. + """ + super().__init__(feature_map=feature_map, enforce_psd=enforce_psd) + + eval_duplicates = evaluate_duplicates.lower() + if eval_duplicates not in ("all", "off_diagonal", "none"): + raise ValueError( + f"Unsupported value passed as evaluate_duplicates: {evaluate_duplicates}" + ) + self._evaluate_duplicates = eval_duplicates + + if fidelity is None: + fidelity = ComputeUncompute(sampler=Sampler()) + self._fidelity = fidelity + + def evaluate(self, x_vec: np.ndarray, y_vec: np.ndarray | None = None) -> np.ndarray: + x_vec, y_vec = self._validate_input(x_vec, y_vec) + + # determine if calculating self inner product + is_symmetric = True + if y_vec is None: + y_vec = x_vec + elif not np.array_equal(x_vec, y_vec): + is_symmetric = False + + kernel_shape = (x_vec.shape[0], y_vec.shape[0]) + + if is_symmetric: + left_parameters, right_parameters, indices = self._get_symmetric_parameterization(x_vec) + kernel_matrix = self._get_symmetric_kernel_matrix( + kernel_shape, left_parameters, right_parameters, indices + ) + else: + left_parameters, right_parameters, indices = self._get_parameterization(x_vec, y_vec) + kernel_matrix = self._get_kernel_matrix( + kernel_shape, left_parameters, right_parameters, indices + ) + + if is_symmetric and self._enforce_psd: + kernel_matrix = self._make_psd(kernel_matrix) + + # due to truncation and rounding errors we may get complex numbers + kernel_matrix = np.real(kernel_matrix) + + return kernel_matrix + + def _get_parameterization( + self, x_vec: np.ndarray, y_vec: np.ndarray + ) -> tuple[np.ndarray, np.ndarray, KernelIndices]: + """ + Combines x_vec and y_vec to get all the combinations needed to evaluate the kernel entries. + """ + num_features = x_vec.shape[1] + left_parameters = np.zeros((0, num_features)) + right_parameters = np.zeros((0, num_features)) + + indices = [] + for i, x_i in enumerate(x_vec): + for j, y_j in enumerate(y_vec): + if self._is_trivial(i, j, x_i, y_j, False): + continue + + left_parameters = np.vstack((left_parameters, x_i)) + right_parameters = np.vstack((right_parameters, y_j)) + indices.append((i, j)) + + return left_parameters, right_parameters, indices + + def _get_symmetric_parameterization( + self, x_vec: np.ndarray + ) -> tuple[np.ndarray, np.ndarray, KernelIndices]: + """ + Combines two copies of x_vec to get all the combinations needed to evaluate the kernel entries. + """ + num_features = x_vec.shape[1] + left_parameters = np.zeros((0, num_features)) + right_parameters = np.zeros((0, num_features)) + + indices = [] + for i, x_i in enumerate(x_vec): + for j, x_j in enumerate(x_vec[i:]): + if self._is_trivial(i, i + j, x_i, x_j, True): + continue + + left_parameters = np.vstack((left_parameters, x_i)) + right_parameters = np.vstack((right_parameters, x_j)) + indices.append((i, i + j)) + + return left_parameters, right_parameters, indices + + def _get_kernel_matrix( + self, + kernel_shape: tuple[int, int], + left_parameters: np.ndarray, + right_parameters: np.ndarray, + indices: KernelIndices, + ) -> np.ndarray: + """ + Given a parameterization, this computes the symmetric kernel matrix. + """ + kernel_entries = self._get_kernel_entries(left_parameters, right_parameters) + + # fill in trivial entries and then update with fidelity values + kernel_matrix = np.ones(kernel_shape) + + for i, (col, row) in enumerate(indices): + kernel_matrix[col, row] = kernel_entries[i] + + return kernel_matrix + + def _get_symmetric_kernel_matrix( + self, + kernel_shape: tuple[int, int], + left_parameters: np.ndarray, + right_parameters: np.ndarray, + indices: KernelIndices, + ) -> np.ndarray: + """ + Given a set of parameterization, this computes the kernel matrix. + """ + kernel_entries = self._get_kernel_entries(left_parameters, right_parameters) + kernel_matrix = np.ones(kernel_shape) + + for i, (col, row) in enumerate(indices): + kernel_matrix[col, row] = kernel_entries[i] + kernel_matrix[row, col] = kernel_entries[i] + + return kernel_matrix + + def _get_kernel_entries(self, left_parameters: np.ndarray, right_parameters: np.ndarray): + """ + Gets kernel entries by executing the underlying fidelity instance and getting the results + back from the async job. + """ + num_circuits = left_parameters.shape[0] + if num_circuits != 0: + job = self._fidelity.run( + [self._feature_map] * num_circuits, + [self._feature_map] * num_circuits, + left_parameters, + right_parameters, + ) + kernel_entries = np.real(job.result().fidelities) + else: + # trivial case, only identical samples + kernel_entries = [] + return kernel_entries + + def _is_trivial( + self, i: int, j: int, x_i: np.ndarray, y_j: np.ndarray, symmetric: bool + ) -> bool: + """ + Verifies if the kernel entry is trivial (to be set to `1.0`) or not. + + Args: + i: row index of the entry in the kernel matrix. + j: column index of the entry in the kernel matrix. + x_i: a sample from the dataset that corresponds to the row in the kernel matrix. + y_j: a sample from the dataset that corresponds to the column in the kernel matrix. + symmetric: whether it is a symmetric case or not. + + Returns: + `True` if the entry is trivial, `False` otherwise. + """ + # if we evaluate all combinations, then it is non-trivial + if self._evaluate_duplicates == "all": + return False + + # if we are on the diagonal and we don't evaluate it, it is trivial + if symmetric and i == j and self._evaluate_duplicates == "off_diagonal": + return True + + # if don't evaluate any duplicates + if np.array_equal(x_i, y_j) and self._evaluate_duplicates == "none": + return True + + # otherwise evaluate + return False + + @property + def fidelity(self): + """Returns the fidelity primitive used by this kernel.""" + return self._fidelity + + @property + def evaluate_duplicates(self): + """Returns the strategy used by this kernel to evaluate kernel matrix elements if duplicate + samples are found.""" + return self._evaluate_duplicates diff --git a/qiskit_machine_learning/kernels/quantum_kernel.py b/qiskit_machine_learning/kernels/quantum_kernel.py index 05df6d7a4..4b138e218 100644 --- a/qiskit_machine_learning/kernels/quantum_kernel.py +++ b/qiskit_machine_learning/kernels/quantum_kernel.py @@ -31,10 +31,12 @@ deprecate_method, deprecate_property, ) +from .trainable_kernel import TrainableKernel +from .base_kernel import BaseKernel from ..exceptions import QiskitMachineLearningError -class QuantumKernel: +class QuantumKernel(TrainableKernel, BaseKernel): r"""Quantum Kernel. The general task of machine learning is to find and study patterns in data. For many @@ -91,6 +93,7 @@ def __init__( Raises: ValueError: When unsupported value is passed to `evaluate_duplicates`. """ + super().__init__(feature_map=feature_map, enforce_psd=enforce_psd) # Class fields self._feature_map: QuantumCircuit | None = None # type is required by mypy self._unbound_feature_map = None @@ -163,13 +166,14 @@ def training_parameters(self, training_params: ParameterVector | Sequence[Parame self._training_parameters = copy.deepcopy(training_params) def assign_training_parameters( - self, values: Mapping[Parameter, ParameterValueType] | Sequence[ParameterValueType] + self, + parameter_values: Mapping[Parameter, ParameterValueType] | Sequence[ParameterValueType], ) -> None: """ - Assign training parameters in the ``QuantumKernel`` feature map. + Assign training parameters in the quantum kernel's feature map. Args: - values (dict or iterable): Either a dictionary or iterable specifying the new + parameter_values (dict or iterable): Either a dictionary or iterable specifying the new parameter values. If a dict, it specifies the mapping from ``current_parameter`` to ``new_parameter``, where ``new_parameter`` can be a parameter expression or a numeric value. If an iterable, the elements are assigned to the existing parameters @@ -179,11 +183,12 @@ def assign_training_parameters( ValueError: Incompatible number of training parameters and values """ + values = parameter_values if self._training_parameters is None: raise ValueError( f""" The number of parameter values ({len(values)}) does not - match the number of training parameters tracked by the QuantumKernel + match the number of training parameters tracked by the quantum kernel (None). """ ) @@ -199,7 +204,7 @@ def assign_training_parameters( raise ValueError( f""" The number of parameter values ({len(values)}) does not - match the number of training parameters tracked by the QuantumKernel + match the number of training parameters tracked by the quantum kernel ({len(self._training_parameters)}). """ ) diff --git a/qiskit_machine_learning/kernels/trainable_fidelity_quantum_kernel.py b/qiskit_machine_learning/kernels/trainable_fidelity_quantum_kernel.py new file mode 100644 index 000000000..880bb451f --- /dev/null +++ b/qiskit_machine_learning/kernels/trainable_fidelity_quantum_kernel.py @@ -0,0 +1,135 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Trainable Quantum Kernel""" + +from __future__ import annotations + +from typing import Sequence + +import numpy as np +from qiskit import QuantumCircuit +from qiskit.algorithms.state_fidelities import BaseStateFidelity +from qiskit.circuit import Parameter, ParameterVector + +from .fidelity_quantum_kernel import FidelityQuantumKernel, KernelIndices +from .trainable_kernel import TrainableKernel +from ..exceptions import QiskitMachineLearningError + + +class TrainableFidelityQuantumKernel(TrainableKernel, FidelityQuantumKernel): + r""" + Finding good quantum kernels for a specific machine learning task is a big challenge in quantum + machine learning. One way to choose the kernel is to add trainable parameters to the feature + map, which can be used to fine-tune the kernel. + + This kernel has trainable parameters :math:`\theta` that can be bound using training algorithms. + The kernel entries are given as + + .. math:: + + K_{\theta}(x,y) = |\langle \phi_{\theta}(x) | \phi_{\theta}(y) \rangle|^2 + """ + + def __init__( + self, + *, + feature_map: QuantumCircuit | None = None, + fidelity: BaseStateFidelity | None = None, + training_parameters: ParameterVector | Sequence[Parameter] | None = None, + enforce_psd: bool = True, + evaluate_duplicates: str = "off_diagonal", + ) -> None: + """ + Args: + feature_map: Parameterized circuit to be used as the feature map. If ``None`` is given, + :class:`~qiskit.circuit.library.ZZFeatureMap` is used with two qubits. If there's + a mismatch in the number of qubits of the feature map and the number of features + in the dataset, then the kernel will try to adjust the feature map to reflect the + number of features. + fidelity: An instance of the + :class:`~qiskit.algorithms.state_fidelities.BaseStateFidelity` primitive to be used + to compute fidelity between states. Default is + :class:`~qiskit.algorithms.state_fidelities.ComputeUncompute` which is created on + top of the reference sampler defined by :class:`~qiskit.primitives.Sampler`. + training_parameters: Iterable containing :class:`~qiskit.circuit.Parameter` objects + which correspond to quantum gates on the feature map circuit which may be tuned. + If users intend to tune feature map parameters to find optimal values, this field + should be set. + enforce_psd: Project to the closest positive semidefinite matrix if ``x = y``. + Default ``True``. + evaluate_duplicates: Defines a strategy how kernel matrix elements are evaluated if + duplicate samples are found. Possible values are: + + - ``all`` means that all kernel matrix elements are evaluated, even the diagonal + ones when training. This may introduce additional noise in the matrix. + - ``off_diagonal`` when training the matrix diagonal is set to `1`, the rest + elements are fully evaluated, e.g., for two identical samples in the + dataset. When inferring, all elements are evaluated. This is the default + value. + - ``none`` when training the diagonal is set to `1` and if two identical samples + are found in the dataset the corresponding matrix element is set to `1`. + When inferring, matrix elements for identical samples are set to `1`. + """ + super().__init__( + feature_map=feature_map, + fidelity=fidelity, + training_parameters=training_parameters, + enforce_psd=enforce_psd, + evaluate_duplicates=evaluate_duplicates, + ) + + # override the num of features defined in the base class + self._num_features = feature_map.num_parameters - self._num_training_parameters + self._feature_parameters = [ + parameter + for parameter in feature_map.parameters + if parameter not in training_parameters + ] + self._parameter_dict = {parameter: None for parameter in feature_map.parameters} + + def evaluate(self, x_vec: np.ndarray, y_vec: np.ndarray | None = None) -> np.ndarray: + for param in self._training_parameters: + if self._parameter_dict[param] is None: + raise QiskitMachineLearningError( + f"Trainable parameter {param} has not been bound. Make sure to bind all" + "trainable parameters to numerical values using `.assign_training_parameters()`" + "before calling `.evaluate()`." + ) + return super().evaluate(x_vec, y_vec) + + def _parameter_array(self, x_vec: np.ndarray) -> np.ndarray: + """ + Combines the feature values and the trainable parameters into one array. + """ + full_array = np.zeros((x_vec.shape[0], self._num_features + self._num_training_parameters)) + for i, x in enumerate(x_vec): + self._parameter_dict.update( + {feature_param: x[j] for j, feature_param in enumerate(self._feature_parameters)} + ) + full_array[i, :] = list(self._parameter_dict.values()) + return full_array + + def _get_parameterization( + self, x_vec: np.ndarray, y_vec: np.ndarray + ) -> tuple[np.ndarray, np.ndarray, KernelIndices]: + new_x_vec = self._parameter_array(x_vec) + new_y_vec = self._parameter_array(y_vec) + + return super()._get_parameterization(new_x_vec, new_y_vec) + + def _get_symmetric_parameterization( + self, x_vec: np.ndarray + ) -> tuple[np.ndarray, np.ndarray, KernelIndices]: + new_x_vec = self._parameter_array(x_vec) + + return super()._get_symmetric_parameterization(new_x_vec) diff --git a/qiskit_machine_learning/kernels/trainable_kernel.py b/qiskit_machine_learning/kernels/trainable_kernel.py new file mode 100644 index 000000000..34440c9a4 --- /dev/null +++ b/qiskit_machine_learning/kernels/trainable_kernel.py @@ -0,0 +1,95 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +"""Trainable Quantum Kernel""" + +from __future__ import annotations + +from abc import ABC +from typing import Mapping, Sequence + +import numpy as np +from qiskit.circuit import Parameter, ParameterVector +from qiskit.circuit.parameterexpression import ParameterValueType + +from .base_kernel import BaseKernel + + +class TrainableKernel(BaseKernel, ABC): + """An abstract class that adds ability to train kernel.""" + + def __init__( + self, *, training_parameters: ParameterVector | Sequence[Parameter] | None = None, **kwargs + ) -> None: + """ + Args: + training_parameters: a sequence of training parameters. + **kwargs: Additional parameters may be used by the super class. + """ + super().__init__(**kwargs) + + if training_parameters is None: + training_parameters = [] + + self._training_parameters = training_parameters + self._num_training_parameters = len(self._training_parameters) + + self._parameter_dict = {parameter: None for parameter in training_parameters} + + def assign_training_parameters( + self, + parameter_values: Mapping[Parameter, ParameterValueType] | Sequence[ParameterValueType], + ) -> None: + """ + Fix the training parameters to numerical values. + """ + if not isinstance(parameter_values, dict): + if len(parameter_values) != self._num_training_parameters: + raise ValueError( + f"The number of given parameters is wrong: {len(parameter_values)}, " + f"expected {self._num_training_parameters}." + ) + self._parameter_dict.update( + { + parameter: parameter_values[i] + for i, parameter in enumerate(self._training_parameters) + } + ) + else: + for key in parameter_values: + if key not in self._training_parameters: + raise ValueError( + f"Parameter {key} is not a trainable parameter of the feature map and " + f"thus cannot be bound. Make sure {key} is provided in the the trainable " + "parameters when initializing the kernel." + ) + self._parameter_dict[key] = parameter_values[key] + + @property + def parameter_values(self) -> np.ndarray: + """ + Returns numerical values assigned to the training parameters as a numpy array. + """ + return np.asarray([self._parameter_dict[param] for param in self._training_parameters]) + + @property + def training_parameters(self) -> ParameterVector | Sequence[Parameter]: + """ + Returns the vector of training parameters. + """ + return self._training_parameters + + @property + def num_training_parameters(self) -> int: + """ + Returns the number of training parameters. + """ + return len(self._training_parameters) diff --git a/qiskit_machine_learning/utils/loss_functions/kernel_loss_functions.py b/qiskit_machine_learning/utils/loss_functions/kernel_loss_functions.py index 20f1826de..3060c50c9 100644 --- a/qiskit_machine_learning/utils/loss_functions/kernel_loss_functions.py +++ b/qiskit_machine_learning/utils/loss_functions/kernel_loss_functions.py @@ -19,7 +19,7 @@ from sklearn.svm import SVC # Prevent circular dependencies caused from type checking -from ...kernels import QuantumKernel +from ...kernels import TrainableKernel class KernelLoss(ABC): @@ -33,7 +33,7 @@ class KernelLoss(ABC): def __call__( self, parameter_values: Sequence[float], - quantum_kernel: QuantumKernel, + quantum_kernel: TrainableKernel, data: np.ndarray, labels: np.ndarray, ) -> float: @@ -46,7 +46,7 @@ def __call__( def evaluate( self, parameter_values: Sequence[float], - quantum_kernel: QuantumKernel, + quantum_kernel: TrainableKernel, data: np.ndarray, labels: np.ndarray, ) -> float: @@ -98,7 +98,7 @@ def __init__(self, **kwargs): def evaluate( self, parameter_values: Sequence[float], - quantum_kernel: QuantumKernel, + quantum_kernel: TrainableKernel, data: np.ndarray, labels: np.ndarray, ) -> float: diff --git a/releasenotes/notes/add-fidelity-quantum-kernel-d40278abb49e19b5.yaml b/releasenotes/notes/add-fidelity-quantum-kernel-d40278abb49e19b5.yaml new file mode 100644 index 000000000..6ac8bbff8 --- /dev/null +++ b/releasenotes/notes/add-fidelity-quantum-kernel-d40278abb49e19b5.yaml @@ -0,0 +1,58 @@ +--- +features: + - | + Introduced Quantum Kernels based on (runtime) primitives. This implementation leverages the + fidelity primitive (see :class:`~qiskit.algorithms.state_fidelities.BaseStateFidelity`) and + provides more flexibility to end users. The fidelity primitive calculates state + fidelities/overlaps for pairs of quantum circuits and requires an instance of + :class:`~qiskit.primitives.Sampler`. Thus, users may plug in their own implementations of + fidelity calculations. + + The new kernels expose the same interface and the same parameters except the `quantum_instance` + parameter. This parameter does not have a direct replacement and instead the `fidelity` + parameter must be used. + + A new hierarchy is introduced: + + - A base and abstract class :class:`~qiskit_machine_learning.kernels.BaseKernel` is + introduced. All concrete implementation must inherit this class. + - A fidelity based quantum kernel + :class:`~qiskit_machine_learning.kernels.FidelityQuantumKernel` is added. This is a direct + replacement of :class:`~qiskit_machine_learning.kernels.QuantumKernel`. The difference is + that the new class takes either a sampler or a fidelity instance to estimate overlaps and + construct kernel matrix. + - A new abstract class :class:`~qiskit_machine_learning.kernels.TrainableKernel` is + introduced to generalize ability to train quantum kernels. + - A fidelity-based trainable quantum kernel + :class:`~qiskit_machine_learning.kernels.TrainableFidelityQuantumKernel` is introduced. This + is a replacement of the existing :class:`~qiskit_machine_learning.kernels.QuantumKernel` if + a trainable kernel is required. The trainer + :class:`~qiskit_machine_learning.kernels.algorithms.QuantumKernelTrainer` now accepts both + quantum kernel implementations, the new one and the existing one. + + The existing algorithms such as :class:`~qiskit_machine_learning.algorithms.QSVC`, + :class:`~qiskit_machine_learning.algorithms.QSVR` and other kernel-based algorithms are updated + to accept both implementations. + + For example a QSVM classifier can be trained as follows: + + .. code-block:: python + + from qiskit.algorithms.state_fidelities import ComputeUncompute + from qiskit.circuit.library import ZZFeatureMap + from qiskit.primitives import Sampler + from sklearn.datasets import make_blobs + + from qiskit_machine_learning.algorithms import QSVC + from qiskit_machine_learning.kernels import FidelityQuantumKernel + + # generate a simple dataset + features, labels = make_blobs(n_samples=20, centers=2, center_box=(-1, 1), cluster_std=0.1) + + # fidelity is optional and quantum kernel will create it automatically if none is passed + fidelity = ComputeUncompute(sampler=Sampler()) + + feature_map = ZZFeatureMap(2) + kernel = FidelityQuantumKernel(feature_map=feature_map, fidelity=fidelity) + qsvc = QSVC(quantum_kernel=kernel) + qsvc.fit(features, labels) diff --git a/requirements.txt b/requirements.txt index 53eca70fe..c4cb83090 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -qiskit-terra>=0.20.0 +qiskit-terra>=0.22 scipy>=1.4 numpy>=1.17 psutil>=5 diff --git a/test/algorithms/classifiers/test_fidelity_quantum_kernel_pegasos_qsvc.py b/test/algorithms/classifiers/test_fidelity_quantum_kernel_pegasos_qsvc.py new file mode 100644 index 000000000..0b8ec0b4a --- /dev/null +++ b/test/algorithms/classifiers/test_fidelity_quantum_kernel_pegasos_qsvc.py @@ -0,0 +1,232 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" Test Pegasos QSVC """ +import os +import tempfile +import unittest + +from test import QiskitMachineLearningTestCase + +import numpy as np +from qiskit.circuit.library import ZFeatureMap +from qiskit.utils import algorithm_globals +from sklearn.datasets import make_blobs +from sklearn.preprocessing import MinMaxScaler + +from qiskit_machine_learning.algorithms import PegasosQSVC, SerializableModelMixin +from qiskit_machine_learning.kernels import FidelityQuantumKernel + + +class TestPegasosQSVC(QiskitMachineLearningTestCase): + """Test Pegasos QSVC Algorithm""" + + def setUp(self): + super().setUp() + + algorithm_globals.random_seed = 10598 + + # number of qubits is equal to the number of features + self.q = 2 + # number of steps performed during the training procedure + self.tau = 100 + + self.feature_map = ZFeatureMap(feature_dimension=self.q, reps=1) + + sample, label = make_blobs( + n_samples=20, n_features=2, centers=2, random_state=3, shuffle=True + ) + sample = MinMaxScaler(feature_range=(0, np.pi)).fit_transform(sample) + + # split into train and test set + self.sample_train = sample[:15] + self.label_train = label[:15] + self.sample_test = sample[15:] + self.label_test = label[15:] + + # The same for a 4-dimensional example + # number of qubits is equal to the number of features + self.q_4d = 4 + self.feature_map_4d = ZFeatureMap(feature_dimension=self.q_4d, reps=1) + + sample_4d, label_4d = make_blobs( + n_samples=20, n_features=self.q_4d, centers=2, random_state=3, shuffle=True + ) + sample_4d = MinMaxScaler(feature_range=(0, np.pi)).fit_transform(sample_4d) + + # split into train and test set + self.sample_train_4d = sample_4d[:15] + self.label_train_4d = label_4d[:15] + self.sample_test_4d = sample_4d[15:] + self.label_test_4d = label_4d[15:] + + def test_qsvc(self): + """Test PegasosQSVC""" + qkernel = FidelityQuantumKernel(feature_map=self.feature_map) + + pegasos_qsvc = PegasosQSVC(quantum_kernel=qkernel, C=1000, num_steps=self.tau) + + pegasos_qsvc.fit(self.sample_train, self.label_train) + score = pegasos_qsvc.score(self.sample_test, self.label_test) + + self.assertEqual(score, 1.0) + + def test_decision_function(self): + """Test PegasosQSVC.""" + qkernel = FidelityQuantumKernel(feature_map=self.feature_map) + + pegasos_qsvc = PegasosQSVC(quantum_kernel=qkernel, C=1000, num_steps=self.tau) + + pegasos_qsvc.fit(self.sample_train, self.label_train) + decision_function = pegasos_qsvc.decision_function(self.sample_test) + + self.assertTrue(np.all((decision_function > 0) == (self.label_test == 0))) + + def test_qsvc_4d(self): + """Test PegasosQSVC with 4-dimensional input data""" + qkernel = FidelityQuantumKernel(feature_map=self.feature_map_4d) + + pegasos_qsvc = PegasosQSVC(quantum_kernel=qkernel, C=1000, num_steps=self.tau) + + pegasos_qsvc.fit(self.sample_train_4d, self.label_train_4d) + score = pegasos_qsvc.score(self.sample_test_4d, self.label_test_4d) + self.assertEqual(score, 1.0) + + def test_precomputed_kernel(self): + """Test PegasosQSVC with a precomputed kernel matrix""" + qkernel = FidelityQuantumKernel(feature_map=self.feature_map) + + pegasos_qsvc = PegasosQSVC(C=1000, num_steps=self.tau, precomputed=True) + + # training + kernel_matrix_train = qkernel.evaluate(self.sample_train, self.sample_train) + pegasos_qsvc.fit(kernel_matrix_train, self.label_train) + + # testing + kernel_matrix_test = qkernel.evaluate(self.sample_test, self.sample_train) + score = pegasos_qsvc.score(kernel_matrix_test, self.label_test) + + self.assertEqual(score, 1.0) + + def test_change_kernel(self): + """Test QSVC with QuantumKernel later""" + qkernel = FidelityQuantumKernel(feature_map=self.feature_map) + + pegasos_qsvc = PegasosQSVC(C=1000, num_steps=self.tau) + pegasos_qsvc.quantum_kernel = qkernel + pegasos_qsvc.fit(self.sample_train, self.label_train) + score = pegasos_qsvc.score(self.sample_test, self.label_test) + + self.assertEqual(score, 1) + + def test_labels(self): + """Test PegasosQSVC with different integer labels than {0, 1}""" + qkernel = FidelityQuantumKernel(feature_map=self.feature_map) + + pegasos_qsvc = PegasosQSVC(quantum_kernel=qkernel, C=1000, num_steps=self.tau) + + label_train_temp = self.label_train.copy() + label_train_temp[self.label_train == 0] = 2 + label_train_temp[self.label_train == 1] = 3 + + label_test_temp = self.label_test.copy() + label_test_temp[self.label_test == 0] = 2 + label_test_temp[self.label_test == 1] = 3 + + pegasos_qsvc.fit(self.sample_train, label_train_temp) + score = pegasos_qsvc.score(self.sample_test, label_test_temp) + + self.assertEqual(score, 1.0) + + def test_constructor(self): + """Tests properties of PegasosQSVC""" + with self.subTest("Default parameters"): + pegasos_qsvc = PegasosQSVC() + self.assertIsInstance(pegasos_qsvc.quantum_kernel, FidelityQuantumKernel) + self.assertFalse(pegasos_qsvc.precomputed) + self.assertEqual(pegasos_qsvc.num_steps, 1000) + + with self.subTest("PegasosQSVC with QuantumKernel"): + qkernel = FidelityQuantumKernel(feature_map=self.feature_map) + pegasos_qsvc = PegasosQSVC(quantum_kernel=qkernel) + self.assertIsInstance(pegasos_qsvc.quantum_kernel, FidelityQuantumKernel) + self.assertFalse(pegasos_qsvc.precomputed) + + with self.subTest("PegasosQSVC with precomputed kernel"): + pegasos_qsvc = PegasosQSVC(precomputed=True) + self.assertIsNone(pegasos_qsvc.quantum_kernel) + self.assertTrue(pegasos_qsvc.precomputed) + + with self.subTest("PegasosQSVC with wrong parameters"): + qkernel = FidelityQuantumKernel(feature_map=self.feature_map) + with self.assertRaises(ValueError): + _ = PegasosQSVC(quantum_kernel=qkernel, precomputed=True) + + with self.subTest("Both kernel and precomputed are passed"): + qkernel = FidelityQuantumKernel(feature_map=self.feature_map) + self.assertRaises(ValueError, PegasosQSVC, quantum_kernel=qkernel, precomputed=True) + + def test_change_kernel_types(self): + """Test PegasosQSVC with a precomputed kernel matrix""" + qkernel = FidelityQuantumKernel(feature_map=self.feature_map) + + pegasos_qsvc = PegasosQSVC(C=1000, num_steps=self.tau, precomputed=True) + + # train precomputed + kernel_matrix_train = qkernel.evaluate(self.sample_train, self.sample_train) + pegasos_qsvc.fit(kernel_matrix_train, self.label_train) + + # now train the same instance, but with a new quantum kernel + pegasos_qsvc.quantum_kernel = FidelityQuantumKernel(feature_map=self.feature_map) + pegasos_qsvc.fit(self.sample_train, self.label_train) + score = pegasos_qsvc.score(self.sample_test, self.label_test) + + self.assertEqual(score, 1.0) + + def test_save_load(self): + """Tests save and load models.""" + features = np.array([[0, 0], [0.1, 0.2], [1, 1], [0.9, 0.8]]) + labels = np.array([0, 0, 1, 1]) + + qkernel = FidelityQuantumKernel(feature_map=self.feature_map) + + regressor = PegasosQSVC(quantum_kernel=qkernel, C=1000, num_steps=self.tau) + regressor.fit(features, labels) + + # predicted labels from the newly trained model + test_features = np.array([[0.5, 0.5]]) + original_predicts = regressor.predict(test_features) + + # save/load, change the quantum instance and check if predicted values are the same + file_name = os.path.join(tempfile.gettempdir(), "pegasos.model") + regressor.save(file_name) + try: + regressor_load = PegasosQSVC.load(file_name) + loaded_model_predicts = regressor_load.predict(test_features) + + np.testing.assert_array_almost_equal(original_predicts, loaded_model_predicts) + + # test loading warning + class FakeModel(SerializableModelMixin): + """Fake model class for test purposes.""" + + pass + + with self.assertRaises(TypeError): + FakeModel.load(file_name) + + finally: + os.remove(file_name) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/algorithms/classifiers/test_fidelity_quantum_kernel_qsvc.py b/test/algorithms/classifiers/test_fidelity_quantum_kernel_qsvc.py new file mode 100644 index 000000000..16e736bcc --- /dev/null +++ b/test/algorithms/classifiers/test_fidelity_quantum_kernel_qsvc.py @@ -0,0 +1,129 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +"""Test QSVC on fidelity quantum kernel.""" + +import os +import tempfile +import unittest + +from test import QiskitMachineLearningTestCase + +import numpy as np +from qiskit.circuit.library import ZZFeatureMap +from qiskit.utils import algorithm_globals + +from qiskit_machine_learning.algorithms import QSVC, SerializableModelMixin +from qiskit_machine_learning.exceptions import QiskitMachineLearningWarning +from qiskit_machine_learning.kernels import FidelityQuantumKernel + + +class TestQSVC(QiskitMachineLearningTestCase): + """Test QSVC Algorithm on fidelity quantum kernel.""" + + def setUp(self): + super().setUp() + + algorithm_globals.random_seed = 10598 + + self.feature_map = ZZFeatureMap(feature_dimension=2, reps=2) + + self.sample_train = np.asarray( + [ + [3.07876080, 1.75929189], + [6.03185789, 5.27787566], + [6.22035345, 2.70176968], + [0.18849556, 2.82743339], + ] + ) + self.label_train = np.asarray([0, 0, 1, 1]) + + self.sample_test = np.asarray([[2.199114860, 5.15221195], [0.50265482, 0.06283185]]) + self.label_test = np.asarray([0, 1]) + + def test_qsvc(self): + """Test QSVC""" + qkernel = FidelityQuantumKernel(feature_map=self.feature_map) + + qsvc = QSVC(quantum_kernel=qkernel) + qsvc.fit(self.sample_train, self.label_train) + score = qsvc.score(self.sample_test, self.label_test) + + self.assertEqual(score, 0.5) + + def test_change_kernel(self): + """Test QSVC with QuantumKernel later""" + qkernel = FidelityQuantumKernel(feature_map=self.feature_map) + + qsvc = QSVC() + qsvc.quantum_kernel = qkernel + qsvc.fit(self.sample_train, self.label_train) + score = qsvc.score(self.sample_test, self.label_test) + + self.assertEqual(score, 0.5) + + def test_qsvc_parameters(self): + """Test QSVC with extra constructor parameters""" + qkernel = FidelityQuantumKernel(feature_map=self.feature_map) + + qsvc = QSVC(quantum_kernel=qkernel, tol=1e-4, C=0.5) + qsvc.fit(self.sample_train, self.label_train) + score = qsvc.score(self.sample_test, self.label_test) + + self.assertEqual(score, 0.5) + + def test_qsvc_to_string(self): + """Test QSVC print works when no *args passed in""" + qsvc = QSVC() + _ = str(qsvc) + + def test_with_kernel_parameter(self): + """Test QSVC with the `kernel` argument.""" + with self.assertWarns(QiskitMachineLearningWarning): + QSVC(kernel=1) + + def test_save_load(self): + """Tests save and load models.""" + features = np.array([[0, 0], [0.1, 0.2], [1, 1], [0.9, 0.8]]) + labels = np.array([0, 0, 1, 1]) + + quantum_kernel = FidelityQuantumKernel() + classifier = QSVC(quantum_kernel=quantum_kernel) + classifier.fit(features, labels) + + # predicted labels from the newly trained model + test_features = np.array([[0.2, 0.1], [0.8, 0.9]]) + original_predicts = classifier.predict(test_features) + + # save/load, change the quantum instance and check if predicted values are the same + file_name = os.path.join(tempfile.gettempdir(), "qsvc.model") + classifier.save(file_name) + try: + classifier_load = QSVC.load(file_name) + loaded_model_predicts = classifier_load.predict(test_features) + + np.testing.assert_array_almost_equal(original_predicts, loaded_model_predicts) + + # test loading warning + class FakeModel(SerializableModelMixin): + """Fake model class for test purposes.""" + + pass + + with self.assertRaises(TypeError): + FakeModel.load(file_name) + + finally: + os.remove(file_name) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/algorithms/classifiers/test_pegasos_qsvc.py b/test/algorithms/classifiers/test_pegasos_qsvc.py index 38d8634e2..c4be983dd 100644 --- a/test/algorithms/classifiers/test_pegasos_qsvc.py +++ b/test/algorithms/classifiers/test_pegasos_qsvc.py @@ -27,7 +27,7 @@ from qiskit.utils import QuantumInstance, algorithm_globals from qiskit_machine_learning.algorithms import PegasosQSVC, SerializableModelMixin -from qiskit_machine_learning.kernels import QuantumKernel +from qiskit_machine_learning.kernels import QuantumKernel, FidelityQuantumKernel from qiskit_machine_learning.exceptions import QiskitMachineLearningError @@ -157,18 +157,6 @@ def test_change_kernel(self): self.assertEqual(score, 1) - def test_wrong_parameters(self): - """Tests PegasosQSVC with incorrect constructor parameter values.""" - qkernel = QuantumKernel( - feature_map=self.feature_map, quantum_instance=self.statevector_simulator - ) - - with self.subTest("Both kernel and precomputed are passed"): - self.assertRaises(ValueError, PegasosQSVC, quantum_kernel=qkernel, precomputed=True) - - with self.subTest("Incorrect quantum kernel value is passed"): - self.assertRaises(TypeError, PegasosQSVC, quantum_kernel=1) - def test_labels(self): """Test PegasosQSVC with different integer labels than {0, 1}""" qkernel = QuantumKernel( @@ -194,7 +182,7 @@ def test_constructor(self): """Tests properties of PegasosQSVC""" with self.subTest("Default parameters"): pegasos_qsvc = PegasosQSVC() - self.assertIsInstance(pegasos_qsvc.quantum_kernel, QuantumKernel) + self.assertIsInstance(pegasos_qsvc.quantum_kernel, FidelityQuantumKernel) self.assertFalse(pegasos_qsvc.precomputed) self.assertEqual(pegasos_qsvc.num_steps, 1000) @@ -218,9 +206,11 @@ def test_constructor(self): with self.assertRaises(ValueError): _ = PegasosQSVC(quantum_kernel=qkernel, precomputed=True) - with self.subTest("PegasosQSVC with wrong type of kernel"): - with self.assertRaises(TypeError): - _ = PegasosQSVC(quantum_kernel=object()) + with self.subTest("Both kernel and precomputed are passed"): + qkernel = QuantumKernel( + feature_map=self.feature_map, quantum_instance=self.statevector_simulator + ) + self.assertRaises(ValueError, PegasosQSVC, quantum_kernel=qkernel, precomputed=True) def test_change_kernel_types(self): """Test PegasosQSVC with a precomputed kernel matrix""" diff --git a/test/algorithms/regressors/test_fidelity_quantum_kernel_qsvr.py b/test/algorithms/regressors/test_fidelity_quantum_kernel_qsvr.py new file mode 100644 index 000000000..6d6777a8d --- /dev/null +++ b/test/algorithms/regressors/test_fidelity_quantum_kernel_qsvr.py @@ -0,0 +1,131 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +"""Test QSVR on fidelity quantum kernel.""" + +import os +import tempfile +import unittest + +from test import QiskitMachineLearningTestCase + +import numpy as np +from qiskit.circuit.library import ZZFeatureMap +from qiskit.primitives import Sampler +from qiskit.utils import algorithm_globals + +from qiskit_machine_learning.algorithms import QSVR, SerializableModelMixin +from qiskit_machine_learning.exceptions import QiskitMachineLearningWarning +from qiskit_machine_learning.kernels import FidelityQuantumKernel + + +class TestQSVR(QiskitMachineLearningTestCase): + """Test QSVR Algorithm on fidelity quantum kernel.""" + + def setUp(self): + super().setUp() + + algorithm_globals.random_seed = 10598 + + self.sampler = Sampler() + self.feature_map = ZZFeatureMap(feature_dimension=2, reps=2) + + self.sample_train = np.asarray( + [ + [3.07876080, 1.75929189], + [6.03185789, 5.27787566], + [6.22035345, 2.70176968], + [0.18849556, 2.82743339], + ] + ) + self.label_train = np.asarray([0, 0, 1, 1]) + + self.sample_test = np.asarray([[2.199114860, 5.15221195], [0.50265482, 0.06283185]]) + self.label_test = np.asarray([0, 1]) + + def test_qsvr(self): + """Test QSVR""" + qkernel = FidelityQuantumKernel(feature_map=self.feature_map) + + qsvr = QSVR(quantum_kernel=qkernel) + qsvr.fit(self.sample_train, self.label_train) + score = qsvr.score(self.sample_test, self.label_test) + + self.assertAlmostEqual(score, 0.38, places=2) + + def test_change_kernel(self): + """Test QSVR with QuantumKernel later""" + qkernel = FidelityQuantumKernel(feature_map=self.feature_map) + + qsvr = QSVR() + qsvr.quantum_kernel = qkernel + qsvr.fit(self.sample_train, self.label_train) + score = qsvr.score(self.sample_test, self.label_test) + + self.assertAlmostEqual(score, 0.38, places=2) + + def test_qsvr_parameters(self): + """Test QSVR with extra constructor parameters""" + qkernel = FidelityQuantumKernel(feature_map=self.feature_map) + + qsvr = QSVR(quantum_kernel=qkernel, tol=1e-4, C=0.5) + qsvr.fit(self.sample_train, self.label_train) + score = qsvr.score(self.sample_test, self.label_test) + + self.assertAlmostEqual(score, 0.38, places=2) + + def test_qsvc_to_string(self): + """Test QSVR print works when no *args passed in""" + qsvr = QSVR() + _ = str(qsvr) + + def test_with_kernel_parameter(self): + """Test QSVC with the `kernel` argument.""" + with self.assertWarns(QiskitMachineLearningWarning): + QSVR(kernel=1) + + def test_save_load(self): + """Tests save and load models.""" + features = np.array([[0, 0], [0.1, 0.1], [0.4, 0.4], [1, 1]]) + labels = np.array([0, 0.1, 0.4, 1]) + + quantum_kernel = FidelityQuantumKernel(feature_map=ZZFeatureMap(2)) + regressor = QSVR(quantum_kernel=quantum_kernel) + regressor.fit(features, labels) + + # predicted labels from the newly trained model + test_features = np.array([[0.5, 0.5]]) + original_predicts = regressor.predict(test_features) + + # save/load, change the quantum instance and check if predicted values are the same + file_name = os.path.join(tempfile.gettempdir(), "qsvr.model") + regressor.save(file_name) + try: + regressor_load = QSVR.load(file_name) + loaded_model_predicts = regressor_load.predict(test_features) + + np.testing.assert_array_almost_equal(original_predicts, loaded_model_predicts) + + # test loading warning + class FakeModel(SerializableModelMixin): + """Fake model class for test purposes.""" + + pass + + with self.assertRaises(TypeError): + FakeModel.load(file_name) + + finally: + os.remove(file_name) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/kernels/algorithms/test_fidelity_qkernel_trainer.py b/test/kernels/algorithms/test_fidelity_qkernel_trainer.py new file mode 100644 index 000000000..f196b3dd4 --- /dev/null +++ b/test/kernels/algorithms/test_fidelity_qkernel_trainer.py @@ -0,0 +1,113 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" Test QuantumKernelTrainer """ +from __future__ import annotations + +import unittest + +from test import QiskitMachineLearningTestCase + +import numpy as np +from qiskit import QuantumCircuit +from qiskit.algorithms.optimizers import COBYLA +from qiskit.circuit import Parameter, ParameterVector +from qiskit.circuit.library import ZZFeatureMap +from qiskit.utils import algorithm_globals + +from qiskit_machine_learning.algorithms.classifiers import QSVC +from qiskit_machine_learning.kernels import TrainableFidelityQuantumKernel +from qiskit_machine_learning.kernels.algorithms import QuantumKernelTrainer +from qiskit_machine_learning.utils.loss_functions import SVCLoss + + +class TestQuantumKernelTrainer(QiskitMachineLearningTestCase): + """Test QuantumKernelTrainer Algorithm""" + + def setUp(self): + super().setUp() + algorithm_globals.random_seed = 10598 + self.optimizer = COBYLA(maxiter=25) + data_block = ZZFeatureMap(2) + trainable_block = ZZFeatureMap(2, parameter_prefix="θ") + training_parameters = trainable_block.parameters + + self.feature_map = data_block.compose(trainable_block).compose(data_block) + self.training_parameters = training_parameters + + self.sample_train = np.asarray( + [ + [3.07876080, 1.75929189], + [6.03185789, 5.27787566], + [6.22035345, 2.70176968], + [0.18849556, 2.82743339], + ] + ) + self.label_train = np.asarray([0, 0, 1, 1]) + + self.sample_test = np.asarray([[2.199114860, 5.15221195], [0.50265482, 0.06283185]]) + self.label_test = np.asarray([1, 0]) + + self.quantum_kernel = TrainableFidelityQuantumKernel( + feature_map=self.feature_map, + training_parameters=self.training_parameters, + ) + + def test_qkt(self): + """Test QuantumKernelTrainer""" + with self.subTest("check default fit"): + qkt = QuantumKernelTrainer(quantum_kernel=self.quantum_kernel) + qkt_result = qkt.fit(self.sample_train, self.label_train) + + self._fit_and_assert_score(qkt_result) + + with self.subTest("check fit with params"): + loss = SVCLoss(C=0.8, gamma="auto") + qkt = QuantumKernelTrainer( + quantum_kernel=self.quantum_kernel, loss=loss, optimizer=self.optimizer + ) + qkt_result = qkt.fit(self.sample_train, self.label_train) + + # Ensure user parameters are bound to real values + self.assertTrue(np.all(qkt_result.quantum_kernel.parameter_values)) + + self._fit_and_assert_score(qkt_result) + + def test_asymmetric_trainable_parameters(self): + """Test when the number of trainable parameters does not equal to the number of features.""" + qc = QuantumCircuit(2) + training_parameters = Parameter("θ") + qc.ry(training_parameters, [0, 1]) + feature_params = ParameterVector("x", 2) + qc.rz(feature_params[0], 0) + qc.rz(feature_params[1], 1) + + quantum_kernel = TrainableFidelityQuantumKernel( + feature_map=qc, + training_parameters=[training_parameters], + ) + + qkt = QuantumKernelTrainer(quantum_kernel=quantum_kernel) + qkt_result = qkt.fit(self.sample_train, self.label_train) + + self._fit_and_assert_score(qkt_result) + + def _fit_and_assert_score(self, qkt_result): + # Ensure kernel training functions and is deterministic + qsvc = QSVC(quantum_kernel=qkt_result.quantum_kernel) + qsvc.fit(self.sample_train, self.label_train) + score = qsvc.score(self.sample_test, self.label_test) + self.assertGreaterEqual(score, 0.5) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/kernels/test_fidelity_qkernel.py b/test/kernels/test_fidelity_qkernel.py new file mode 100644 index 000000000..02b7850a8 --- /dev/null +++ b/test/kernels/test_fidelity_qkernel.py @@ -0,0 +1,431 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +"""Test FidelityQuantumKernel.""" + +from __future__ import annotations + +import functools +import itertools +import unittest +from typing import Sequence + +from test import QiskitMachineLearningTestCase + +import numpy as np +from ddt import ddt, idata, unpack +from qiskit import QuantumCircuit +from qiskit.algorithms.state_fidelities import ( + ComputeUncompute, + BaseStateFidelity, + StateFidelityResult, +) +from qiskit.circuit import Parameter +from qiskit.circuit.library import ZFeatureMap +from qiskit.primitives import Sampler +from qiskit.utils import algorithm_globals +from sklearn.svm import SVC + +from qiskit_machine_learning.kernels import FidelityQuantumKernel + + +@ddt +class TestFidelityQuantumKernel(QiskitMachineLearningTestCase): + """Test FidelityQuantumKernel.""" + + def setUp(self): + super().setUp() + + algorithm_globals.random_seed = 10598 + + self.feature_map = ZFeatureMap(feature_dimension=2, reps=2) + + self.sample_train = np.asarray( + [ + [3.07876080, 1.75929189], + [6.03185789, 5.27787566], + [6.22035345, 2.70176968], + [0.18849556, 2.82743339], + ] + ) + self.label_train = np.asarray([0, 0, 1, 1]) + + self.sample_test = np.asarray([[2.199114860, 5.15221195], [0.50265482, 0.06283185]]) + self.label_test = np.asarray([0, 1]) + + self.sampler = Sampler() + self.fidelity = ComputeUncompute(self.sampler) + + self.properties = dict( + samples_1=self.sample_train[0], + samples_4=self.sample_train, + samples_test=self.sample_test, + z_fm=self.feature_map, + no_fm=None, + ) + + def test_svc_callable(self): + """Test callable kernel in sklearn.""" + kernel = FidelityQuantumKernel(feature_map=self.feature_map) + svc = SVC(kernel=kernel.evaluate) + svc.fit(self.sample_train, self.label_train) + score = svc.score(self.sample_test, self.label_test) + + self.assertEqual(score, 1.0) + + def test_svc_precomputed(self): + """Test precomputed kernel in sklearn.""" + kernel = FidelityQuantumKernel(feature_map=self.feature_map) + kernel_train = kernel.evaluate(x_vec=self.sample_train) + kernel_test = kernel.evaluate(x_vec=self.sample_test, y_vec=self.sample_train) + + svc = SVC(kernel="precomputed") + svc.fit(kernel_train, self.label_train) + score = svc.score(kernel_test, self.label_test) + + self.assertEqual(score, 1.0) + + def test_defaults(self): + """Test quantum kernel with all default values.""" + features = algorithm_globals.random.random((10, 2)) - 0.5 + labels = np.sign(features[:, 0]) + + kernel = FidelityQuantumKernel() + svc = SVC(kernel=kernel.evaluate) + svc.fit(features, labels) + score = svc.score(features, labels) + + self.assertGreaterEqual(score, 0.5) + + def test_exceptions(self): + """Test quantum kernel raises exceptions and warnings.""" + with self.assertRaises(ValueError, msg="Unsupported value of 'evaluate_duplicates'."): + _ = FidelityQuantumKernel(evaluate_duplicates="wrong") + + @idata( + # params, fidelity, feature map, enforce_psd, duplicate + itertools.product( + ["samples_1", "samples_4"], + ["default_fidelity", "fidelity_instance"], + ["no_fm", "z_fm"], + [True, False], + ["none", "off_diagonal", "all"], + ) + ) + @unpack + def test_evaluate_symmetric(self, params, fidelity, feature_map, enforce_psd, duplicates): + """Test QuantumKernel.evaluate(x) for a symmetric kernel.""" + solution = self._get_symmetric_solution(params, feature_map) + + x_vec = self.properties[params] + feature_map = self.properties[feature_map] + kernel = self._create_kernel(fidelity, feature_map, enforce_psd, duplicates) + + kernel_matrix = kernel.evaluate(x_vec) + + np.testing.assert_allclose(kernel_matrix, solution, rtol=1e-4, atol=1e-10) + + @idata( + itertools.product( + ["samples_1", "samples_4"], + ["samples_1", "samples_4", "samples_test"], + ["default_fidelity", "fidelity_instance"], + ["no_fm", "z_fm"], + [True, False], + ["none", "off_diagonal", "all"], + ) + ) + @unpack + def test_evaluate_asymmetric( + self, params_x, params_y, fidelity, feature_map, enforce_psd, duplicates + ): + """Test QuantumKernel.evaluate(x,y) for an asymmetric kernel.""" + solution = self._get_asymmetric_solution(params_x, params_y, feature_map) + + x_vec = self.properties[params_x] + y_vec = self.properties[params_y] + feature_map = self.properties[feature_map] + kernel = self._create_kernel(fidelity, feature_map, enforce_psd, duplicates) + + if isinstance(solution, str) and solution == "wrong": + with self.assertRaises(ValueError): + _ = kernel.evaluate(x_vec, y_vec) + else: + kernel_matrix = kernel.evaluate(x_vec, y_vec) + np.testing.assert_allclose(kernel_matrix, solution, rtol=1e-4, atol=1e-10) + + def _create_kernel(self, fidelity, feature_map, enforce_psd, duplicates): + if fidelity == "default_fidelity": + kernel = FidelityQuantumKernel( + feature_map=feature_map, + enforce_psd=enforce_psd, + evaluate_duplicates=duplicates, + ) + elif fidelity == "fidelity_instance": + kernel = FidelityQuantumKernel( + feature_map=feature_map, + fidelity=self.fidelity, + enforce_psd=enforce_psd, + evaluate_duplicates=duplicates, + ) + else: + raise ValueError("Unsupported configuration!") + return kernel + + def _get_symmetric_solution(self, params, feature_map): + if params == "samples_1": + solution = np.array([[1.0]]) + + elif params == "samples_4" and feature_map == "z_fm": + solution = np.array( + [ + [1.0, 0.78883982, 0.15984355, 0.06203766], + [0.78883982, 1.0, 0.49363215, 0.32128356], + [0.15984355, 0.49363215, 1.0, 0.91953051], + [0.06203766, 0.32128356, 0.91953051, 1.0], + ] + ) + else: + # ZZFeatureMap with 4 params + solution = np.array( + [ + [1.0, 0.81376617, 0.05102078, 0.06033439], + [0.81376617, 1.0, 0.14750292, 0.09980414], + [0.05102078, 0.14750292, 1.0, 0.26196463], + [0.06033439, 0.09980414, 0.26196463, 1.0], + ] + ) + return solution + + def _get_asymmetric_solution(self, params_x, params_y, feature_map): + if params_x == "wrong" or params_y == "wrong": + return "wrong" + # check if hidden symmetric case + if params_x == params_y: + return self._get_symmetric_solution(params_x, feature_map) + + if feature_map == "z_fm": + if params_x == "samples_1" and params_y == "samples_4": + solution = np.array([[1.0, 0.78883982, 0.15984355, 0.06203766]]) + elif params_x == "samples_1" and params_y == "samples_test": + solution = np.array([[0.30890363, 0.04543022]]) + elif params_x == "samples_4" and params_y == "samples_1": + solution = np.array([[1.0, 0.78883982, 0.15984355, 0.06203766]]).T + else: + # 4_param and 2_param + solution = np.array( + [ + [0.30890363, 0.04543022], + [0.39666513, 0.23044328], + [0.11826802, 0.58742761], + [0.10665779, 0.7650088], + ] + ) + else: + # ZZFeatureMap + if params_x == "samples_1" and params_y == "samples_4": + solution = np.array([[1.0, 0.81376617, 0.05102078, 0.06033439]]) + elif params_x == "samples_1" and params_y == "samples_test": + solution = np.array([[0.24610242, 0.17510262]]) + elif params_x == "samples_4" and params_y == "samples_1": + solution = np.array([[1.0, 0.81376617, 0.05102078, 0.06033439]]).T + else: + # 4_param and 2_param + solution = np.array( + [ + [0.24610242, 0.17510262], + [0.36660828, 0.06476594], + [0.13924611, 0.48450828], + [0.24435258, 0.31099496], + ] + ) + return solution + + def test_enforce_psd(self): + """Test enforce_psd""" + + class MockFidelity(BaseStateFidelity): + """Custom fidelity that returns -0.5 for any input.""" + + def create_fidelity_circuit( + self, circuit_1: QuantumCircuit, circuit_2: QuantumCircuit + ) -> QuantumCircuit: + raise NotImplementedError() + + def _run( + self, + circuits_1: QuantumCircuit | Sequence[QuantumCircuit], + circuits_2: QuantumCircuit | Sequence[QuantumCircuit], + values_1: Sequence[float] | Sequence[Sequence[float]] | None = None, + values_2: Sequence[float] | Sequence[Sequence[float]] | None = None, + **options, + ) -> StateFidelityResult: + values = np.asarray(values_1) + fidelities = np.full(values.shape[0], -0.5) + return StateFidelityResult(fidelities, [], {}, options) + + with self.subTest("No PSD enforcement"): + kernel = FidelityQuantumKernel(fidelity=MockFidelity(), enforce_psd=False) + matrix = kernel.evaluate(self.sample_train) + eigen_values = np.linalg.eigvals(matrix) + # there's a negative eigenvalue + self.assertFalse(np.all(np.greater_equal(eigen_values, -1e-10))) + + with self.subTest("PSD enforced"): + kernel = FidelityQuantumKernel(fidelity=MockFidelity(), enforce_psd=True) + matrix = kernel.evaluate(self.sample_train) + eigen_values = np.linalg.eigvals(matrix) + # all eigenvalues are non-negative with some tolerance + self.assertTrue(np.all(np.greater_equal(eigen_values, -1e-10))) + + def test_validate_input(self): + """Test validation of input data in the base (abstract) class.""" + with self.subTest("Incorrect size of x_vec"): + kernel = FidelityQuantumKernel() + + x_vec = np.asarray([[[0]]]) + self.assertRaises(ValueError, kernel.evaluate, x_vec) + + x_vec = np.asarray([]) + self.assertRaises(ValueError, kernel.evaluate, x_vec) + + with self.subTest("Adjust the number of qubits in the feature map"): + kernel = FidelityQuantumKernel() + + x_vec = np.asarray([[1, 2, 3]]) + kernel.evaluate(x_vec) + self.assertEqual(kernel.feature_map.num_qubits, 3) + + with self.subTest("Fail to adjust the number of qubits in the feature map"): + qc = QuantumCircuit(1) + kernel = FidelityQuantumKernel(feature_map=qc) + + x_vec = np.asarray([[1, 2]]) + self.assertRaises(ValueError, kernel.evaluate, x_vec) + + with self.subTest("Incorrect size of y_vec"): + kernel = FidelityQuantumKernel() + + x_vec = np.asarray([[1, 2]]) + y_vec = np.asarray([[[0]]]) + self.assertRaises(ValueError, kernel.evaluate, x_vec, y_vec) + + x_vec = np.asarray([[1, 2]]) + y_vec = np.asarray([]) + self.assertRaises(ValueError, kernel.evaluate, x_vec, y_vec) + + with self.subTest("Fail when x_vec and y_vec have different shapes"): + kernel = FidelityQuantumKernel() + + x_vec = np.asarray([[1, 2]]) + y_vec = np.asarray([[1, 2, 3]]) + self.assertRaises(ValueError, kernel.evaluate, x_vec, y_vec) + + def test_properties(self): + """Test properties of the base (abstract) class and fidelity based kernel.""" + qc = QuantumCircuit(1) + qc.ry(Parameter("w"), 0) + fidelity = ComputeUncompute(sampler=Sampler()) + kernel = FidelityQuantumKernel( + feature_map=qc, fidelity=fidelity, enforce_psd=False, evaluate_duplicates="none" + ) + + self.assertEqual(qc, kernel.feature_map) + self.assertEqual(fidelity, kernel.fidelity) + self.assertEqual(False, kernel.enforce_psd) + self.assertEqual("none", kernel.evaluate_duplicates) + self.assertEqual(1, kernel.num_features) + + +@ddt +class TestDuplicates(QiskitMachineLearningTestCase): + """Test quantum kernel with duplicate entries.""" + + def setUp(self) -> None: + super().setUp() + + self.feature_map = ZFeatureMap(feature_dimension=2, reps=1) + + self.properties = { + "no_dups": np.array([[1, 2], [2, 3], [3, 4]]), + "dups": np.array([[1, 2], [1, 2], [3, 4]]), + "y_vec": np.array([[0, 1], [1, 2]]), + } + + counting_sampler = Sampler() + counting_sampler.run = self.count_circuits(counting_sampler.run) + self.counting_sampler = counting_sampler + self.circuit_counts = 0 + + def count_circuits(self, func): + """Wrapper to record the number of circuits passed to QuantumInstance.execute. + + Args: + func (Callable): execute function to be wrapped + + Returns: + Callable: function wrapper + """ + + @functools.wraps(func) + def wrapper(*args, **kwargs): + self.circuit_counts += len(kwargs["circuits"]) + return func(*args, **kwargs) + + return wrapper + + @idata( + [ + ("no_dups", "all", 6), + ("no_dups", "off_diagonal", 3), + ("no_dups", "none", 3), + ("dups", "all", 6), + ("dups", "off_diagonal", 3), + ("dups", "none", 2), + ] + ) + @unpack + def test_evaluate_duplicates(self, dataset_name, evaluate_duplicates, expected_num_circuits): + """Tests quantum kernel evaluation with duplicate samples.""" + self.circuit_counts = 0 + kernel = FidelityQuantumKernel( + fidelity=ComputeUncompute(sampler=self.counting_sampler), + feature_map=self.feature_map, + evaluate_duplicates=evaluate_duplicates, + ) + kernel.evaluate(self.properties.get(dataset_name)) + + self.assertEqual(self.circuit_counts, expected_num_circuits) + + @idata( + [ + ("no_dups", "all", 6), + ("no_dups", "off_diagonal", 6), + ("no_dups", "none", 5), + ] + ) + @unpack + def test_evaluate_duplicates_asymmetric( + self, dataset_name, evaluate_duplicates, expected_num_circuits + ): + """Tests asymmetric quantum kernel evaluation with duplicate samples.""" + self.circuit_counts = 0 + kernel = FidelityQuantumKernel( + fidelity=ComputeUncompute(sampler=self.counting_sampler), + feature_map=self.feature_map, + evaluate_duplicates=evaluate_duplicates, + ) + kernel.evaluate(self.properties.get(dataset_name), self.properties.get("y_vec")) + self.assertEqual(self.circuit_counts, expected_num_circuits) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/kernels/test_new_vs_old_kernel.py b/test/kernels/test_new_vs_old_kernel.py new file mode 100644 index 000000000..e15bdd5bc --- /dev/null +++ b/test/kernels/test_new_vs_old_kernel.py @@ -0,0 +1,90 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +"""Test primitive-based quantum kernel versus the original implementation.""" + +from __future__ import annotations + +import itertools + +from test import QiskitMachineLearningTestCase + +import numpy as np +from ddt import ddt, idata, unpack +from qiskit import BasicAer +from qiskit.circuit.library import ZFeatureMap, ZZFeatureMap +from qiskit.utils import algorithm_globals, QuantumInstance + +from qiskit_machine_learning.kernels import QuantumKernel +from qiskit_machine_learning.kernels import FidelityQuantumKernel + + +@ddt +class TestNewVsOldQuantumKernel(QiskitMachineLearningTestCase): + """ + Test new quantum kernel versus the old one. The old one is evaluated on a statevector + simulator. To be removed when old quantum kernel is removed. + """ + + def setUp(self): + super().setUp() + algorithm_globals.random_seed = 10598 + + self.statevector_simulator = QuantumInstance(BasicAer.get_backend("statevector_simulator")) + self.properties = dict( + z1=ZFeatureMap(1), + z2=ZFeatureMap(2), + zz2=ZZFeatureMap(2), + z4=ZFeatureMap(4), + zz4=ZZFeatureMap(4), + ) + + @idata( + itertools.product( + ["z1", "z2", "zz2", "z4", "zz4"], + [True, False], + ["none", "off_diagonal", "all"], + ) + ) + @unpack + def test_new_vs_old(self, feature_map_name, enforce_psd, duplicates): + """Test new versus old.""" + feature_map = self.properties[feature_map_name] + features = algorithm_globals.random.random((10, feature_map.num_qubits)) + # add some duplicates + features = np.concatenate((features, features[0, :].reshape(1, -1))) + + new_qk = FidelityQuantumKernel( + feature_map=feature_map, + enforce_psd=enforce_psd, + evaluate_duplicates=duplicates, + ) + old_qk = QuantumKernel( + feature_map, + enforce_psd=enforce_psd, + quantum_instance=self.statevector_simulator, + evaluate_duplicates=duplicates, + ) + + new_matrix = new_qk.evaluate(features) + old_matrix = old_qk.evaluate(features) + + np.testing.assert_almost_equal(new_matrix, old_matrix) + + # test asymmetric case + unseen_features = algorithm_globals.random.random((5, feature_map.num_qubits)) + # add some duplicates from the seen features + unseen_features = np.concatenate((unseen_features, features[0, :].reshape(1, -1))) + + new_matrix = new_qk.evaluate(features, unseen_features) + old_matrix = old_qk.evaluate(features, unseen_features) + + np.testing.assert_almost_equal(new_matrix, old_matrix) diff --git a/test/kernels/test_trainable_fidelity_qkernel.py b/test/kernels/test_trainable_fidelity_qkernel.py new file mode 100644 index 000000000..2d840270b --- /dev/null +++ b/test/kernels/test_trainable_fidelity_qkernel.py @@ -0,0 +1,185 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" Test TrainableQuantumKernel using primitives """ + +import unittest + +from test import QiskitMachineLearningTestCase + +import numpy as np +from ddt import ddt, data +from qiskit.circuit import Parameter +from qiskit.circuit.library import ZZFeatureMap + +from qiskit_machine_learning import QiskitMachineLearningError +from qiskit_machine_learning.kernels import TrainableFidelityQuantumKernel + + +@ddt +class TestPrimitivesTrainableQuantumKernelClassify(QiskitMachineLearningTestCase): + """Test trainable QuantumKernel.""" + + def setUp(self): + super().setUp() + + # Create an arbitrary 3-qubit feature map circuit + circ1 = ZZFeatureMap(3) + circ2 = ZZFeatureMap(3, parameter_prefix="θ") + self.feature_map = circ1.compose(circ2).compose(circ1) + self.num_features = circ1.num_parameters + self.training_parameters = circ2.parameters + + self.sample_train = np.array( + [[0.53833689, 0.44832616, 0.74399926], [0.43359057, 0.11213606, 0.97568932]] + ) + self.sample_test = np.array([0.0, 1.0, 2.0]) + + def test_training_parameters(self): + """Test assigning/re-assigning user parameters""" + + kernel = TrainableFidelityQuantumKernel( + feature_map=self.feature_map, + training_parameters=self.training_parameters, + ) + + with self.subTest("check basic instantiation"): + # Ensure we can instantiate a QuantumKernel with user parameters + self.assertEqual(kernel._training_parameters, self.training_parameters) + + with self.subTest("test wrong number of parameters"): + # Try to set the user parameters using an incorrect number of values + training_param_values = [2.0, 4.0, 6.0, 8.0] + with self.assertRaises(ValueError): + kernel.assign_training_parameters(training_param_values) + + with self.subTest("test invalid parameter assignment"): + # Try to set the user parameters using incorrect parameter + param_binds = {Parameter("x"): 0.5} + with self.assertRaises(ValueError): + kernel.assign_training_parameters(param_binds) + + with self.subTest("test parameter assignment"): + # Assign params to some new values, and also test the bind_training_parameters interface + param_binds = { + self.training_parameters[0]: 0.1, + self.training_parameters[1]: 0.2, + self.training_parameters[2]: 0.3, + } + kernel.assign_training_parameters(param_binds) + + # Ensure the values are properly bound + np.testing.assert_array_equal(kernel.parameter_values, list(param_binds.values())) + + with self.subTest("test partial parameter assignment"): + # Assign params to some new values, and also test the bind_training_parameters interface + param_binds = {self.training_parameters[0]: 0.5, self.training_parameters[1]: 0.4} + kernel.assign_training_parameters(param_binds) + + # Ensure values were properly bound and param 2 was unchanged + np.testing.assert_array_equal(kernel.parameter_values, [0.5, 0.4, 0.3]) + + with self.subTest("test parameter list assignment"): + # Assign params to some new values, and also test the bind_training_parameters interface + param_binds = [0.1, 0.7, 1.7] + kernel.assign_training_parameters(param_binds) + + # Ensure the values are properly bound + np.testing.assert_array_equal(kernel.parameter_values, param_binds) + + with self.subTest("test parameter array assignment"): + # Assign params to some new values, and also test the bind_training_parameters interface + param_binds = np.array([0.1, 0.7, 1.7]) + kernel.assign_training_parameters(param_binds) + + # Ensure the values are properly bound + np.testing.assert_array_equal(kernel.parameter_values, param_binds) + + @data("params_1", "params_2") + def test_evaluate_symmetric(self, params): + """Test kernel evaluations for different training parameters""" + if params == "params_1": + training_params = [0.0, 0.0, 0.0] + else: + training_params = [0.1, 0.531, 4.12] + + kernel = TrainableFidelityQuantumKernel( + feature_map=self.feature_map, + training_parameters=self.training_parameters, + ) + + kernel.assign_training_parameters(training_params) + kernel_matrix = kernel.evaluate(self.sample_train) + + # Ensure that the calculations are correct + np.testing.assert_allclose( + kernel_matrix, self._get_symmetric_solution(params), rtol=1e-7, atol=1e-7 + ) + + def _get_symmetric_solution(self, params): + if params == "params_1": + return np.array([[1.0, 0.03351197], [0.03351197, 1.0]]) + return np.array([[1.0, 0.082392], [0.082392, 1.0]]) + + @data("params_1", "params_2") + def test_evaluate_asymmetric(self, params): + """Test kernel evaluations for different training parameters""" + if params == "params_1": + training_params = [0.0, 0.0, 0.0] + else: + training_params = [0.1, 0.531, 4.12] + + kernel = TrainableFidelityQuantumKernel( + feature_map=self.feature_map, + training_parameters=self.training_parameters, + ) + + kernel.assign_training_parameters(training_params) + kernel_matrix = kernel.evaluate(self.sample_train, self.sample_test) + + # Ensure that the calculations are correct + np.testing.assert_allclose( + kernel_matrix, self._get_asymmetric_solution(params), rtol=1e-7, atol=1e-7 + ) + + def _get_asymmetric_solution(self, params): + if params == "params_1": + return np.array([[0.00569059], [0.07038205]]) + return np.array([[0.10568674], [0.122404]]) + + def test_incomplete_binding(self): + """Test if an exception is raised when not all training parameter are bound.""" + # assign all parameters except the last one + training_params = { + self.training_parameters[i]: 0 for i in range(len(self.training_parameters) - 1) + } + + kernel = TrainableFidelityQuantumKernel( + feature_map=self.feature_map, + training_parameters=self.training_parameters, + ) + + kernel.assign_training_parameters(training_params) + with self.assertRaises(QiskitMachineLearningError): + kernel.evaluate(self.sample_train) + + def test_properties(self): + """Test properties of the trainable quantum kernel.""" + kernel = TrainableFidelityQuantumKernel( + feature_map=self.feature_map, + training_parameters=self.training_parameters, + ) + self.assertEqual(len(self.training_parameters), kernel.num_training_parameters) + self.assertEqual(self.num_features, kernel.num_features) + + +if __name__ == "__main__": + unittest.main()