Source code for psipose.estimators.classification

"""Classification estimators using quantum models."""

import warnings

import numpy as np
from sklearn.base import ClassifierMixin
from sklearn.svm import SVC
from sklearn.utils.multiclass import unique_labels
from sklearn.utils.validation import check_array, check_is_fitted, check_X_y

from psipose.ansatze import StronglyEntanglingAnsatz
from psipose.estimators.base import QuantumEstimator
from psipose.feature_maps import AngleFeatureMap
from psipose.feature_maps import FeatureMap as FMProtocol
from psipose.measurements import PauliZExpectation
from psipose.models import QuantumKernel, VariationalModel


[docs] class VQCClassifier(QuantumEstimator, ClassifierMixin): """Variational Quantum Classifier. A variational quantum classifier (VQC) is a quantum machine learning model that combines data encoding (feature map), a parameterized quantum circuit (ansatz), and measurement to perform binary or multi-class classification. The model is trained using gradient-based optimization. This implementation follows the circuit-centric quantum classifier approach of Schuld et al. (2018, arXiv:1804.00633) and the broader variational quantum algorithm (VQA) paradigm. For multi-class problems, it uses a one-vs-rest (OvR) strategy. The training process: 1. Encode classical data into quantum states using the feature map 2. Apply parameterized ansatz circuit 3. Measure expectation value (typically Pauli-Z on first qubit) 4. Minimize binary cross-entropy loss via gradient descent The gradients are computed using the parameter shift rule, which provides exact gradients on quantum hardware (Mitarai et al., 2018, arXiv:1803.00745). Parameters ---------- n_qubits : int, default=4 Number of qubits. feature_map : FeatureMap, optional Data encoding. Default: AngleFeatureMap() ansatz : Ansatz, optional Parameterized circuit. Default: StronglyEntanglingAnsatz(layers=2) measurement : Measurement, optional Measurement operator. Default: PauliZExpectation() optimizer : str or callable, default="adam" Optimizer (adam or sgd from PennyLane). learning_rate : float, default=0.01 Learning rate for the optimizer. n_iter : int, default=100 Number of optimization iterations. batch_size : int, optional Batch size for training. If None, uses full batch. random_state : int, optional Random seed for weight initialization. device : str, default="default.qubit" PennyLane device for simulation. Attributes ---------- classes_ : ndarray Class labels. n_features_in_ : int Number of features seen during fit. n_classes_ : int Number of classes. model_ : VariationalModel, optional The trained quantum model (binary case) or None (multiclass OvR). estimators_ : list[VQCClassifier] One-vs-rest classifiers for multi-class. loss_history_ : list[float] Training loss per iteration. qnode_ : callable, optional Cached QNode for inference (binary case only). weights_ : ndarray, optional Trained weights (binary case). References ---------- .. [1] M. Schuld, A. Bocharov, K. Svore, and N. Wiebe, "Circuit-centric quantum classifiers," arXiv:1804.00633, 2018. .. [2] K. Mitarai et al., "Quantum circuit learning," Phys. Rev. A 98, 032309, 2018. arXiv:1803.00745. """ def __init__( self, n_qubits: int = 4, feature_map: FMProtocol | None = None, ansatz=None, measurement=None, optimizer="adam", learning_rate=0.01, n_iter=100, batch_size=None, random_state=None, device="default.qubit", ): self.n_qubits = n_qubits self.feature_map = feature_map self.ansatz = ansatz self.measurement = measurement self.optimizer = optimizer self.learning_rate = learning_rate self.n_iter = n_iter self.batch_size = batch_size self.random_state = random_state self.device = device def _make_binary_classifier(self, random_state_offset=0): """Create binary classifier with independent random state.""" rs = self.random_state if rs is not None: rs = rs + random_state_offset return VQCClassifier( n_qubits=self.n_qubits, feature_map=self.feature_map, ansatz=self.ansatz, measurement=self.measurement, optimizer=self.optimizer, learning_rate=self.learning_rate, n_iter=self.n_iter, batch_size=self.batch_size, random_state=rs, device=self.device, )
[docs] def fit(self, X, y): """Fit the classifier.""" X, y = check_X_y(X, y) self.classes_ = unique_labels(y) self.n_features_in_ = X.shape[1] self.n_classes_ = len(self.classes_) # Binary case if self.n_classes_ == 2: y_encoded = np.array([0 if label == self.classes_[0] else 1 for label in y]) feature_map = self.feature_map or AngleFeatureMap(n_qubits=self.n_qubits) ansatz = self.ansatz or StronglyEntanglingAnsatz( n_qubits=self.n_qubits, layers=2 ) measurement = self.measurement or PauliZExpectation() self.model_ = VariationalModel( feature_map=feature_map, ansatz=ansatz, measurement=measurement, optimizer=self.optimizer, learning_rate=self.learning_rate, n_iter=self.n_iter, batch_size=self.batch_size, device=self.device, random_state=self.random_state, loss_fn="bce", ) self.model_.fit(X, y_encoded) # Expose attributes at classifier level for backward compatibility self.encoder_ = feature_map self.ansatz_ = ansatz self.weights_ = self.model_.weights_ self.qnode_ = self.model_.qnode_ self.loss_history_ = self.model_.loss_history_ return self # Multi-class: one-vs-rest self.estimators_ = [] loss_histories = [] for i, class_label in enumerate(self.classes_): y_binary = (y == class_label).astype(int) clf = self._make_binary_classifier(random_state_offset=i) clf.fit(X, y_binary) self.estimators_.append(clf) loss_histories.append(clf.loss_history_) self.loss_history_ = [ float(np.mean([h[i] for h in loss_histories])) for i in range(self.n_iter) ] return self
[docs] def predict(self, X): """Predict class labels.""" check_is_fitted(self, self._check_fitted_attributes()) X = check_array(X) if self.n_classes_ == 2: proba = self.predict_proba(X) indices = (proba[:, 1] > 0.5).astype(int) else: proba = self.predict_proba(X) indices = np.argmax(proba, axis=1) return self.classes_[indices]
def _check_fitted_attributes(self): if self.n_classes_ > 2: return ["estimators_", "classes_", "n_classes_"] return ["model_", "classes_", "weights_"]
[docs] def predict_proba(self, X): """Predict class probabilities.""" check_is_fitted(self, self._check_fitted_attributes()) X = check_array(X) if self.n_classes_ == 2: predictions = self.model_.predict(X) # Map from [-1, 1] to [0, 1] probs_class1 = (predictions + 1) / 2 probs = np.vstack([1 - probs_class1, probs_class1]).T return probs proba_positive = np.array( [est.predict_proba(X)[:, 1] for est in self.estimators_] ).T row_sums = proba_positive.sum(axis=1, keepdims=True) zero_mask = (row_sums == 0).flatten() if np.any(zero_mask): n_classes = proba_positive.shape[1] proba_positive[zero_mask] = 1.0 / n_classes row_sums[zero_mask] = 1.0 return proba_positive / row_sums
[docs] def decision_function(self, X): """Compute decision function (raw QNode output).""" check_is_fitted(self, self._check_fitted_attributes()) if self.n_classes_ == 2: return self.model_.predict(X) decisions = np.array([est.model_.predict(X) for est in self.estimators_]).T return decisions
[docs] def score(self, X, y): """Return classification accuracy.""" return super().score(X, y)
[docs] class QSVC(QuantumEstimator, ClassifierMixin): """Quantum Support Vector Classifier. A support vector machine that uses a quantum kernel to implicitly map data into a high-dimensional quantum feature space. The kernel is computed as the fidelity (squared inner product) between quantum- encoded states, following the quantum kernel method of Havlíček et al. (2019, arXiv:1904.01567). The implementation: 1. Encodes data using a quantum feature map 2. Computes the quantum kernel matrix (Gram matrix) 3. Trains a classical SVM with precomputed kernel This hybrid approach leverages quantum computers to generate kernel matrices that may be classically intractable to compute, while using well-established classical SVM training. Parameters ---------- n_qubits : int, default=4 Number of qubits for the quantum circuit. feature_map : FeatureMap, optional Quantum feature map for data encoding. Default: AngleFeatureMap() device : str, default="default.qubit" PennyLane device for simulation. C : float, default=1.0 Regularization parameter. The strength of regularization is inversely proportional to C. shrinking : bool, default=True Whether to use the shrinking heuristic. 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 imbalanced datasets. verbose : bool, default=False Enable verbose output. max_iter : int, default=-1 Maximum number of iterations (-1 for no limit). decision_function_shape : {"ovr", "ovo"}, default="ovr" Decision function shape (one-vs-rest or one-vs-one). break_ties : bool, default=False Whether to break ties according to confidence. random_state : int, optional Random seed. Attributes ---------- classes_ : ndarray Class labels. n_features_in_ : int Number of features seen during fit. n_classes_ : int Number of classes. kernel_ : QuantumKernel The fitted quantum kernel. X_train_ : ndarray Training data. K_train_ : ndarray Training kernel matrix. svc_ : SVC The underlying scikit-learn SVM classifier. References ---------- .. [1] V. Havlíček et al., "Supervised learning with quantum-inspired kernel," arXiv:1904.01567, 2019. .. [2] B. Schölkopf and A. Smola, "Learning with Kernels," MIT Press, 2002. (Standard SVM reference) """ def __init__( self, *, n_qubits: int = 4, feature_map: FMProtocol | None = None, device: str = "default.qubit", C: float = 1.0, shrinking: bool = True, probability: bool = False, tol: float = 1e-3, cache_size: float = 200, class_weight=None, verbose: bool = False, max_iter: int = -1, decision_function_shape: str = "ovr", break_ties: bool = False, random_state: int | None = None, kernel: str = "precomputed", # Accepted for sklearn compatibility, ignored ): self.n_qubits = n_qubits self.feature_map = feature_map self.device = device self.C = C 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 self.kernel = ( kernel # Store for sklearn compatibility, but always use precomputed ) if kernel != "precomputed": warnings.warn( f"QSVC always uses a quantum (precomputed) kernel; " f"ignoring kernel='{kernel}'", UserWarning, stacklevel=2, )
[docs] def fit(self, X, y, sample_weight=None): """Fit the quantum SVM.""" X, y = check_X_y(X, y) self.n_features_in_ = X.shape[1] self.classes_ = unique_labels(y) self.n_classes_ = len(self.classes_) self.kernel_ = QuantumKernel( feature_map=self.feature_map, device=self.device, ) self.kernel_.fit(X) self.X_train_ = X.copy() K_train = self.kernel_.compute_matrix(X) self.K_train_ = K_train self.svc_ = SVC( C=self.C, kernel="precomputed", 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(K_train, y, sample_weight=sample_weight) return self
[docs] def predict(self, X): """Predict class labels.""" check_is_fitted(self, ["svc_", "kernel_", "X_train_"]) X = check_array(X, ensure_2d=True) if X.shape[1] != self.n_features_in_: raise ValueError( f"X has {X.shape[1]} features, but QSVC is expecting " f"{self.n_features_in_} features as input." ) 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.""" check_is_fitted(self, ["svc_", "kernel_", "X_train_"]) if not self.probability: raise AttributeError( "predict_proba requires probability=True in QSVC constructor" ) X = check_array(X, ensure_2d=True) if X.shape[1] != self.n_features_in_: raise ValueError( f"X has {X.shape[1]} features, but QSVC is expecting " f"{self.n_features_in_} features as input." ) 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.""" check_is_fitted(self, ["svc_", "kernel_", "X_train_"]) X = check_array(X, ensure_2d=True) if X.shape[1] != self.n_features_in_: raise ValueError( f"X has {X.shape[1]} features, but QSVC is expecting " f"{self.n_features_in_} features as input." ) K_test = self.kernel_.compute_matrix(X, self.X_train_) return self.svc_.decision_function(K_test)
[docs] def score(self, X, y): """Return classification accuracy.""" return super().score(X, y)
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)