| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | """Fidelity Quantum Kernel"""
|
| |
|
| | from __future__ import annotations
|
| |
|
| | from collections.abc import Sequence
|
| | from typing import List, Tuple
|
| |
|
| | import numpy as np
|
| | from qiskit import QuantumCircuit
|
| | from qiskit.primitives import Sampler
|
| | from qiskit_algorithms.state_fidelities import BaseStateFidelity, ComputeUncompute
|
| |
|
| | from .base_kernel import BaseKernel
|
| |
|
| | KernelIndices = List[Tuple[int, int]]
|
| |
|
| |
|
| | class FidelityQuantumKernel(BaseKernel):
|
| | r"""
|
| | An implementation of the quantum kernel interface based on the
|
| | :class:`~qiskit_algorithms.state_fidelities.BaseStateFidelity` algorithm.
|
| |
|
| | 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)
|
| |
|
| |
|
| | 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)
|
| |
|
| | 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)
|
| |
|
| |
|
| | 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
|
| | ) -> Sequence[float]:
|
| | """
|
| | 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 = job.result().fidelities
|
| | else:
|
| |
|
| | 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 self._evaluate_duplicates == "all":
|
| | return False
|
| |
|
| |
|
| | if symmetric and i == j and self._evaluate_duplicates == "off_diagonal":
|
| | return True
|
| |
|
| |
|
| | if np.array_equal(x_i, y_j) and self._evaluate_duplicates == "none":
|
| | return True
|
| |
|
| |
|
| | 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
|
| |
|