"""Fidelity-based quantum kernel.
Computes the kernel matrix as
:math:`K(x_i, x_j) = |\\langle \\phi(x_i) | \\phi(x_j) \\rangle|^2`,
where :math:`\\phi` is a quantum state prepared by an encoder circuit.
This implements the quantum kernel approach from:
- Havlíček et al. (2019), "Supervised learning with
quantum-enhanced feature spaces" (Nature 567, 209-212).
Introduces quantum kernels via the fidelity between
quantum-encoded feature maps, combined with classical SVM.
- Schuld & Killoran (2019), "Quantum machine learning in
feature Hilbert spaces" (PRL 122, 040504).
Frames quantum input encoding as a nonlinear map to Hilbert
space and shows how quantum kernels can be used with
classical ML methods.
- Schuld, Bocharov, Svore & Wiebe (2018), "Circuit-centric
quantum classifiers" (arXiv:1804.00633). Uses angle encoding
as the feature map :math:`\\phi` for quantum kernels.
Mathematically, the quantum kernel is defined as:
.. math::
K(x_i, x_j) = |\\langle \\phi(x_i) | \\phi(x_j) \\rangle|^2
= |\\langle 0^{\\otimes n}|
U_{\\text{enc}}^\\dagger(x_j)
U_{\\text{enc}}(x_i)
|0^{\\otimes n}\\rangle|^2
where :math:`|\\phi(x)\\rangle = U_{\\text{enc}}(x)|0^{\\otimes n}\\rangle`
is the quantum feature map implemented by the encoder.
The fidelity is computed by measuring the probability of :math:`|0\\rangle^{\\otimes n}`
in the combined circuit :math:`U_{\\text{enc}}^\\dagger(x_j) U_{\\text{enc}}(x_i)`:
.. math::
|\\langle \\phi(x_i) | \\phi(x_j) \\rangle|^2
= \\langle \\phi(x_j) | \\phi(x_i) \\rangle
\\langle \\phi(x_i) | \\phi(x_j) \\rangle
= \\text{Prob}(00\\dots 0 | U_{\\text{enc}}^\\dagger(x_j)
U_{\\text{enc}}(x_i) |00\\dots 0\\rangle)
Computationally, this is implemented as:
1. Encode :math:`x_1`: :math:`U_{\\text{enc}}(x_1) |0^{\\otimes n}\\rangle`
2. Apply adjoint: :math:`U_{\\text{enc}}^\\dagger(x_2)`
3. Measure :math:`\\text{Prob}(|0\\rangle^{\\otimes n})` via :func:`qml.probs`
"""
import numpy as np
import pennylane as qml
from sklearn.base import BaseEstimator
from sklearn.utils.validation import check_is_fitted
from psipose.encoders import AngleEncoder, BaseEncoder
[docs]
class FidelityQuantumKernel(BaseEstimator):
"""Fidelity-based quantum kernel matrix calculator.
Computes the Gram matrix where each entry is the fidelity
(squared inner product) between quantum-encoded data points:
:math:`K[i, j] = |\\langle \\phi(x_i) | \\phi(x_j) \\rangle|^2`
The quantum state :math:`|\\phi(x)\\rangle` is prepared by
the given encoder circuit.
This implements the quantum kernel approach from:
- Havlíček et al. (2019), "Supervised learning with
quantum-enhanced feature spaces" (Nature 567, 209-212).
- Schuld & Killoran (2019), "Quantum machine learning in
feature Hilbert spaces" (PRL 122, 040504).
- Schuld, Bocharov, Svore & Wiebe (2018), "Circuit-centric
quantum classifiers" (arXiv:1804.00633).
Mathematically, the quantum kernel is defined as:
.. math::
K(x_i, x_j) = |\\langle \\phi(x_i) | \\phi(x_j) \\rangle|^2
where :math:`|\\phi(x)\\rangle = U_{\\text{enc}}(x)|0^{\\otimes n}\\rangle`.
Parameters
----------
encoder : BaseEncoder, optional
The quantum encoding circuit. If None, defaults to AngleEncoder().
n_qubits : int, default=4
Number of qubits (only used if encoder is None).
device : str, default="default.qubit"
PennyLane device to use for simulation.
batch_size : int, optional
Number of circuits to evaluate in parallel. If None, evaluates all.
Attributes
----------
encoder_ : BaseEncoder
Fitted encoder.
n_qubits_ : int
Number of qubits used.
Examples
--------
>>> from psipose import FidelityQuantumKernel, AngleEncoder
>>> kernel = FidelityQuantumKernel(encoder=AngleEncoder(n_qubits=4))
>>> K = kernel.compute_matrix(X_train)
>>> # Use with sklearn SVC:
>>> from sklearn.svm import SVC
>>> svc = SVC(kernel="precomputed")
>>> svc.fit(K, y_train)
"""
def __init__(
self,
encoder: BaseEncoder | None = None,
n_qubits: int = 4,
device: str = "default.qubit",
batch_size: int | None = None,
):
self.encoder = encoder
self.n_qubits = n_qubits
self.device = device
self.batch_size = batch_size
[docs]
def fit(self, X):
"""Fit the kernel by determining the encoder parameters.
Parameters
----------
X : array-like, shape (n_samples, n_features)
Training data.
Returns
-------
self : FidelityQuantumKernel
"""
if self.encoder is None:
self.encoder_ = AngleEncoder(n_qubits=self.n_qubits)
else:
self.encoder_ = self.encoder
self.encoder_.fit(X)
self.n_qubits_ = self.encoder_.n_qubits_
return self
def _compute_overlap(self, x1, x2):
"""Compute fidelity between two encoded states."""
dev = qml.device(self.device, wires=self.n_qubits_)
@qml.qnode(dev, interface="autograd")
def circuit():
self.encoder_.encode(x1, wires=range(self.n_qubits_))
qml.adjoint(self.encoder_.encode)(x2, wires=range(self.n_qubits_))
return qml.probs(wires=range(self.n_qubits_))
# Probability of all zeros = :math:`|\\langle x1|x2 \\rangle|^2`
probs = circuit()
return probs[0]
[docs]
def compute_matrix(self, X, Y=None):
"""Compute the kernel matrix.
Parameters
----------
X : array-like, shape (n_samples_X, n_features)
First set of vectors.
Y : array-like, shape (n_samples_Y, n_features), optional
Second set of vectors. If None, uses X.
Returns
-------
K : ndarray, shape (n_samples_X, n_samples_Y)
Kernel matrix.
"""
check_is_fitted(self, ["encoder_", "n_qubits_"])
if Y is None:
Y = X
symmetric = True
else:
symmetric = False
n_samples_X = len(X)
n_samples_Y = len(Y)
K = np.zeros((n_samples_X, n_samples_Y))
# Simple double loop with optional row batching
if self.batch_size is None:
# Original: no batching
for i in range(n_samples_X):
for j in range(n_samples_Y):
if symmetric and j < i:
K[i, j] = K[j, i]
else:
K[i, j] = self._compute_overlap(X[i], Y[j])
else:
# Row-based batching
for i_start in range(0, n_samples_X, self.batch_size):
i_end = min(i_start + self.batch_size, n_samples_X)
for i in range(i_start, i_end):
for j in range(n_samples_Y):
if symmetric and j < i:
K[i, j] = K[j, i]
else:
K[i, j] = self._compute_overlap(X[i], Y[j])
return K
def __call__(self, X, Y=None):
"""Compute kernel matrix (makes object callable like a sklearn kernel)."""
return self.compute_matrix(X, Y)