"""Quantum Support Vector Classifier (QSVC)."""
import warnings
from sklearn.base import BaseEstimator, ClassifierMixin
from sklearn.svm import SVC
from sklearn.utils.multiclass import unique_labels
from sklearn.utils.validation import check_is_fitted, validate_data
from psipose.encoders import BaseEncoder
from psipose.kernels import FidelityQuantumKernel
[docs]
class QSVC(BaseEstimator, ClassifierMixin):
"""Quantum Support Vector Classifier using a fidelity quantum kernel.
Combines a quantum feature map (encoder) with a classical SVM.
The quantum kernel computes similarity as
:math:`K(x_i, x_j) = |\\langle \\phi(x_i)|\\phi(x_j)\\rangle|^2`.
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.
- Yang, Awan & Vall-Llosera (2019), "Support Vector Machines
on Noisy Intermediate Scale Quantum Computers"
(arXiv:1909.11988). Adapts SVM for NISQ devices using
quantum kernel matrices.
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 training kernel matrix :math:`K_{\\text{train}}` is computed
and passed to :class:`sklearn.svm.SVC` with ``kernel="precomputed"``.
Parameters
----------
n_qubits : int, default=4
Number of qubits for the quantum feature map.
encoder : BaseEncoder, optional
Data encoding circuit. If None, defaults to AngleEncoder().
device : str, default="default.qubit"
PennyLane device name for simulation.
C : float, default=1.0
Regularization parameter for SVC.
kernel : {"precomputed"}, default="precomputed"
Kernel type. Must be "precomputed" (always uses quantum kernel).
degree : int, default=3
Degree for polynomial kernel (ignored for precomputed).
gamma : float or {"scale", "auto"}, default="scale"
Kernel coefficient for RBF/poly/sigmoid (ignored for precomputed).
coef0 : float, default=0.0
Independent term in poly/sigmoid kernel (ignored for precomputed).
shrinking : bool, default=True
Whether to use shrinking heuristic in SVC.
probability : bool, default=False
Whether to enable probability estimates.
tol : float, default=1e-3
Tolerance for stopping criterion.
cache_size : float, default=200
Kernel cache size in MB.
class_weight : dict or "balanced", optional
Class weights for handling imbalanced datasets.
verbose : bool, default=False
Enable verbose output.
max_iter : int, default=-1
Maximum iterations for SVC solver (-1 = no limit).
decision_function_shape : {"ovr", "ovo"}, default="ovr"
Shape of decision function for multi-class.
break_ties : bool, default=False
Break ties in multi-class classification.
random_state : int, optional
Random seed for reproducibility.
Attributes
----------
classes_ : ndarray
Class labels.
n_features_in_ : int
Number of features seen during fit.
n_classes_ : int
Number of classes.
kernel_ : FidelityQuantumKernel
Fitted quantum kernel instance.
K_train_ : ndarray, shape (n_samples, n_samples)
Training kernel matrix.
svc_ : SVC
Fitted sklearn SVC instance.
support_vectors_ : ndarray
Support vectors (from SVC).
dual_coef_ : ndarray
Coefficients of support vectors in decision function (from SVC).
intercept_ : ndarray
Constants in decision function (from SVC).
X_train_ : ndarray
Training data (stored for prediction).
Examples
--------
>>> from psipose import QSVC
>>> from sklearn.datasets import make_moons
>>> X, y = make_moons(n_samples=100, noise=0.1, random_state=42)
>>> clf = QSVC(n_qubits=2, random_state=42)
>>> clf.fit(X, y)
QSVC(n_qubits=2, random_state=42)
>>> y_pred = clf.predict(X)
"""
def __init__(
self,
*,
n_qubits: int = 4,
encoder: BaseEncoder | None = None,
device: str = "default.qubit",
C: float = 1.0,
kernel: str = "precomputed",
degree: int = 3,
gamma: float | str = "scale",
coef0: float = 0.0,
shrinking: bool = True,
probability: bool = False,
tol: float = 1e-3,
cache_size: float = 200,
class_weight: str | dict | None = None,
verbose: bool = False,
max_iter: int = -1,
decision_function_shape: str = "ovr",
break_ties: bool = False,
random_state: int | None = None,
):
self.n_qubits = n_qubits
self.encoder = encoder
self.device = device
self.C = C
self.kernel = kernel
self.degree = degree
self.gamma = gamma
self.coef0 = coef0
self.shrinking = shrinking
self.probability = probability
self.tol = tol
self.cache_size = cache_size
self.class_weight = class_weight
self.verbose = verbose
self.max_iter = max_iter
self.decision_function_shape = decision_function_shape
self.break_ties = break_ties
self.random_state = random_state
[docs]
def fit(self, X, y, sample_weight=None):
"""Fit the quantum SVM classifier.
Parameters
----------
X : array-like, shape (n_samples, n_features)
Training data.
y : array-like, shape (n_samples,)
Target labels.
sample_weight : array-like, shape (n_samples,), optional
Sample weights.
Returns
-------
self : QSVC
Fitted estimator.
"""
# Validate inputs
X, y = validate_data(self, X, y)
self.n_features_in_ = X.shape[1]
self.classes_ = unique_labels(y)
self.n_classes_ = len(self.classes_)
# Validate kernel parameter - must be precomputed
if self.kernel != "precomputed":
warnings.warn(
"QSVC always uses precomputed quantum kernel. "
f"The kernel='{self.kernel}' parameter is ignored.",
UserWarning,
stacklevel=2,
)
# Store training data for later predictions
self.X_train_ = X.copy()
# 1. Build and fit the quantum kernel
self.kernel_ = FidelityQuantumKernel(
encoder=self.encoder,
n_qubits=self.n_qubits,
device=self.device,
)
self.kernel_.fit(X)
# 2. Compute training kernel matrix
self.K_train_ = self.kernel_.compute_matrix(X)
# 3. Create and train SVC with precomputed kernel
self.svc_ = SVC(
C=self.C,
kernel="precomputed",
degree=self.degree,
gamma=self.gamma,
coef0=self.coef0,
shrinking=self.shrinking,
probability=self.probability,
tol=self.tol,
cache_size=self.cache_size,
class_weight=self.class_weight,
verbose=self.verbose,
max_iter=self.max_iter,
decision_function_shape=self.decision_function_shape,
break_ties=self.break_ties,
random_state=self.random_state,
)
self.svc_.fit(self.K_train_, y, sample_weight=sample_weight)
return self
def __getattr__(self, name):
"""Delegate attribute access to the underlying SVC for fitted attributes."""
if name == "svc_" or not hasattr(self, "svc_"):
raise AttributeError(
f"'{type(self).__name__}' object has no attribute '{name}'"
)
return getattr(self.svc_, name)
[docs]
def predict(self, X):
"""Predict class labels for samples.
Parameters
----------
X : array-like, shape (n_samples, n_features)
Input samples.
Returns
-------
y : ndarray, shape (n_samples,)
Predicted class labels.
"""
check_is_fitted(self, ["svc_", "kernel_", "K_train_", "X_train_"])
X = validate_data(self, X, reset=False)
# Compute kernel matrix between X and training data
K_test = self.kernel_.compute_matrix(X, self.X_train_)
return self.svc_.predict(K_test)
[docs]
def predict_proba(self, X):
"""Predict class probabilities.
Requires probability=True during initialization.
Parameters
----------
X : array-like, shape (n_samples, n_features)
Input samples.
Returns
-------
proba : ndarray, shape (n_samples, n_classes)
Class probabilities.
"""
check_is_fitted(self, ["svc_", "kernel_", "X_train_"])
if not self.probability:
raise AttributeError(
"predict_proba is not available when probability=False. "
"Set probability=True in QSVC constructor."
)
X = validate_data(self, X, reset=False)
K_test = self.kernel_.compute_matrix(X, self.X_train_)
return self.svc_.predict_proba(K_test)
[docs]
def decision_function(self, X):
"""Compute decision function for samples.
Parameters
----------
X : array-like, shape (n_samples, n_features)
Input samples.
Returns
-------
decision : ndarray, shape (n_samples,) or (n_samples, n_classes)
Decision function values.
"""
check_is_fitted(self, ["svc_", "kernel_", "X_train_"])
X = validate_data(self, X, reset=False)
K_test = self.kernel_.compute_matrix(X, self.X_train_)
return self.svc_.decision_function(K_test)