Kernels¶
Three overlap estimators, Gram matrices that stay positive semi-definite, and the models on top.
qmlkit.kernels.estimators¶
estimators ¶
The three ways to read a kernel off a circuit.
A quantum kernel is an overlap: :math:k(x, x') = |\langle \phi(x')|\phi(x)\rangle|^2.
Three estimators get at it, and they are not interchangeable:
============ ========== ==================== =========================================
Estimator Qubits Depth Gives you
============ ========== ==================== =========================================
inversion n 2 x feature map |<.>|^2 — the magnitude
swap test 2n + 1 1 map + n CSWAPs |<.>|^2, from an ancilla
Hadamard n + 1 controlled map Re<.> — signed
============ ========== ==================== =========================================
The inversion (compute-uncompute) test is the default: fewest qubits, no ancilla,
no controlled gates. The swap test earns its extra register when you already hold
two states and cannot rebuild one. The Hadamard test is the only one that keeps the
sign of the inner product — magnitude estimators map +1/2 and -1/2 to the
same number.
inversion_circuit ¶
inversion_circuit(
fmap: FeatureMap,
x: Sequence[float],
xp: Sequence[float],
) -> CircuitSpec
U(x) followed by U(x')†. P(all zeros) is the kernel.
fidelity_kernel ¶
fidelity_kernel(
fmap: FeatureMap,
x: Sequence[float],
xp: Sequence[float],
shots: int | None = None,
backend: BackendLike = None,
seed: int | None = None,
) -> float
k(x, x') by the compute-uncompute test — the default estimator.
With shots=None this reads the exact all-zeros probability. With a shot
budget it counts the all-zeros outcomes, which is what a device would do.
Source code in src/qmlkit/kernels/estimators.py
swap_readout ¶
swap_probability ¶
swap_test_kernel ¶
swap_test_kernel(
fmap: FeatureMap,
x: Sequence[float],
xp: Sequence[float],
shots: int | None = None,
backend: BackendLike = None,
seed: int | None = None,
) -> float
k(x, x') by the swap test — two registers plus one ancilla.
Costs 2n + 1 qubits against the inversion test's n, and needs a CSWAP
per qubit pair. Worth it only when you genuinely hold two states already.
Source code in src/qmlkit/kernels/estimators.py
hadamard_test ¶
hadamard_test(
fmap: FeatureMap,
x: Sequence[float],
xp: Sequence[float],
part: str = "real",
shots: int | None = None,
backend: BackendLike = None,
seed: int | None = None,
) -> float
Re<phi(x')|phi(x)> (or the imaginary part) — the signed inner product.
The only estimator that distinguishes +1/2 from -1/2; the magnitude ones
map both to 1/4. The price is an ancilla and a controlled feature map,
which roughly doubles depth and wants all-to-all connectivity to the ancilla —
which is why it is rarely the right choice on hardware.
Source code in src/qmlkit/kernels/estimators.py
qmlkit.kernels.matrix¶
matrix ¶
Gram matrices, PSD repair, and the diagnostics that say whether any of it will work.
Filling a Gram matrix is the expensive half of a quantum kernel method: m(m-1)/2
circuit evaluations for a training set of size m, since the diagonal is exactly
1 and the matrix is symmetric. :func:kernel_matrix exploits both.
Shot noise breaks positive semi-definiteness. Every entry is an estimate, so the estimated Gram matrix can have small negative eigenvalues even though the true one cannot — and an SVM solver will either refuse it or return nonsense. The repair functions here project back onto the PSD cone.
Exponential concentration is the real limit. As the feature map widens, distinct
inputs produce states whose overlaps all collapse toward the same value, at a rate
around 2^-n. Resolving that against shot noise costs about 4^n shots. The
diagnostics report both, because a kernel method that has concentrated looks like a
model that simply does not learn.
QuantumKernel ¶
QuantumKernel(
feature_map: FeatureMap,
estimator: str = "inversion",
shots: int | None = None,
backend: BackendLike = None,
bandwidth: float = 1.0,
seed: int | None = None,
cache: bool = True,
)
A feature map, as a kernel you can hand to any kernel method.
kernel = QuantumKernel(qk.ZZFeatureMap(2)) K = kernel(X) # training Gram matrix K_test = kernel(X_test, X) # rectangular, test against train
Arguments
estimator
How the overlap is measured. "inversion" (the default) runs the
compute-uncompute circuit and reads the all-zeros probability; "swap"
uses a swap test; "hadamard" runs two Hadamard tests and adds the
squares of the real and imaginary parts, because one Hadamard test measures
one component of a complex overlap and the kernel is its modulus. All three
agree on a simulator — :attr:n_evaluations is what differs, and on a device
so do the width and the connectivity each one needs.
shots
None reads the exact probability. A budget samples it, which is what a
device does — and a sampled kernel is not positive semi-definite by
construction, so pair it with :func:threshold_matrix.
bandwidth
The first thing to try when a kernel has concentrated. Every feature
vector is scaled by this before encoding, so it sets how far apart two points
are in the feature map rather than in the data. At the default 1.0 a
fidelity kernel over a wide register drives every off-diagonal entry toward
the same small number — every pair of points looks equally dissimilar, the
Gram matrix approaches the identity, and no amount of training recovers what
the encoding threw away. Shrinking the bandwidth (0.1-0.5 is the usual
range) compresses the data into a smaller region of state space and pulls the
off-diagonals back apart. :func:concentration_report measures whether you
have the problem, and qk.diagnose(K) names it as KERNEL_CONCENTRATED.
The alternative fix is a projected kernel, which survives width by measuring
local reduced states instead — see the kernels tutorial for when each applies.
cache
Memoises pair evaluations, which matters because a Gram matrix asks for the
same circuit many times. n_evaluations counts the circuits actually run.
Source code in src/qmlkit/kernels/matrix.py
evaluate ¶
One kernel entry, with the bandwidth rescaling applied.
Source code in src/qmlkit/kernels/matrix.py
square_kernel_matrix ¶
square_kernel_matrix(
X: NDArray[Any],
kernel: KernelFn,
assume_unit_diagonal: bool = True,
) -> NDArray[Any]
Symmetric Gram matrix, evaluating only the upper triangle.
m(m-1)/2 evaluations instead of m^2. assume_unit_diagonal sets
k(x, x) = 1 without measuring it, which is exact for a fidelity kernel and
saves m more evaluations.
Source code in src/qmlkit/kernels/matrix.py
kernel_matrix ¶
kernel_matrix(
X: NDArray[Any],
Y: NDArray[Any] | None = None,
kernel: KernelFn | None = None,
) -> NDArray[Any]
Gram matrix of X against Y (or itself, exploiting symmetry).
Source code in src/qmlkit/kernels/matrix.py
is_psd ¶
threshold_matrix ¶
Clip negative eigenvalues to zero — the standard projection onto the cone.
Source code in src/qmlkit/kernels/matrix.py
displace_matrix ¶
Shift the whole spectrum up until it is non-negative.
Keeps every eigenvector's relative weight, unlike thresholding, at the cost of inflating the diagonal.
Source code in src/qmlkit/kernels/matrix.py
flip_matrix ¶
Take the absolute value of each eigenvalue.
closest_psd_matrix ¶
Nearest PSD matrix by the named method.
Source code in src/qmlkit/kernels/matrix.py
center_kernel ¶
Centre the induced feature space at the origin.
Source code in src/qmlkit/kernels/matrix.py
normalize_kernel ¶
Rescale to a unit diagonal — the cosine of the feature-space angle.
Source code in src/qmlkit/kernels/matrix.py
target_alignment ¶
Kernel-target alignment: how much the Gram matrix looks like the labels.
<K, yy^T>_F / (||K||_F ||yy^T||_F) in [-1, 1]. This is the objective you
maximise to train a feature map, and a cheap way to compare candidates without
fitting an SVM to each.
Source code in src/qmlkit/kernels/matrix.py
kernel_shot_cost ¶
Total shots to fill an m x m Gram matrix.
kernel_spread ¶
shots_to_resolve ¶
concentration_report ¶
Is this Gram matrix telling you anything, or has it concentrated?
A concentrated kernel has near-identical off-diagonal entries: every pair of inputs looks equally similar, so no model built on it can separate them.
Source code in src/qmlkit/kernels/matrix.py
geometric_difference ¶
g(K_C || K_Q) — the statistic that says whether quantum could help.
.. math::
g = \sqrt{\lVert \sqrt{K_Q}\, K_C^{-1} \sqrt{K_Q} \rVert_\infty}
The number to compare it against is sqrt(N), for N samples — that is
the threshold in Huang et al. (2021), and it is the caller's to apply. A g
well below sqrt(N) says the classical kernel already sees everything the
quantum one does, so no separation is available whatever a later accuracy table
claims. A g at or above it says a separation is possible, not that one
exists.
Identical kernels give exactly 1. The statistic is scale-sensitive, so it
carries the paper's meaning only when the two kernels are normalised alike —
which any two with a unit diagonal are, a fidelity kernel and an RBF kernel
included. center_kernel or normalize_kernel will put an odd one right.
Source code in src/qmlkit/kernels/matrix.py
qmlkit.kernels.models¶
models ¶
Kernel models: sklearn estimators, a trainable kernel, and projected kernels.
The division of labour a quantum kernel method rests on: the quantum part fills
the Gram matrix, and the classical part solves a convex problem on it. That
means QSVC is a real SVM — same convergence guarantees, same solver — with one
matrix supplied from a circuit.
QSVC ¶
QSVC(
feature_map: FeatureMap, C: float = 1.0, **kwargs: Any
)
Bases: _KernelEstimator
Source code in src/qmlkit/kernels/models.py
QSVR ¶
QSVR(
feature_map: FeatureMap,
C: float = 1.0,
epsilon: float = 0.1,
**kwargs: Any,
)
Bases: _KernelEstimator
Quantum-kernel support vector regressor.
Source code in src/qmlkit/kernels/models.py
score ¶
R^2.
Source code in src/qmlkit/kernels/models.py
NearestFidelityClassifier ¶
NearestFidelityClassifier(
feature_map: FeatureMap,
shots: int | None = None,
backend: BackendLike = None,
)
Classify by fidelity to each class centroid — no solver, no sklearn.
The simplest quantum classifier there is: encode every training point, average within each class, and predict whichever class anchor a new point overlaps most.
Source code in src/qmlkit/kernels/models.py
TrainableKernel ¶
TrainableKernel(
feature_map_factory: Any,
n_params: int,
shots: int | None = None,
backend: BackendLike = None,
)
Train the feature map itself by maximising kernel-target alignment.
A fixed feature map is a guess. Alignment gives a differentiable score for how well a kernel matches the labels, so the embedding's own parameters can be optimised before any classifier is fitted — usually a bigger win than tuning the classifier afterwards.
Source code in src/qmlkit/kernels/models.py
fit ¶
fit(
X: NDArray[Any],
y: NDArray[Any],
n_iterations: int = 40,
theta0: Sequence[float] | None = None,
seed: int | None = None,
) -> TrainableKernel
Maximise alignment with SPSA — two evaluations per step, any parameter count.
Source code in src/qmlkit/kernels/models.py
projected_kernel_matrix ¶
projected_kernel_matrix(
feature_map: FeatureMap,
X: NDArray[Any],
gamma: float = 1.0,
backend: BackendLike = None,
) -> NDArray[Any]
Projected quantum kernel — the standard answer to exponential concentration.
Instead of a global fidelity, compare the one-qubit reduced density matrices:
.. math:: k(x, x') = \exp\left(-\gamma \sum_i |\rho_i(x) - \rho_i(x')|_F^2\right)
Global overlaps concentrate as the register widens — every pair of inputs ends up looking equally similar, and the kernel stops carrying information. Local reduced states do not, so this stays informative where the fidelity kernel has already collapsed (Huang et al. 2021).
Source code in src/qmlkit/kernels/models.py
rkhs_model ¶
rkhs_model(
alphas: Sequence[float],
anchors: NDArray[Any],
x: Sequence[float],
kernel: Any,
) -> float
f(x) = sum_i alpha_i k(x_i, x) — a kernel model is a weighted similarity sum.