# This code is part of a Qiskit project.
#
# (C) Copyright IBM 2021, 2023.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
""" Kernel Loss utilities """
from abc import ABC, abstractmethod
from typing import Sequence
import numpy as np
from sklearn.svm import SVC
# Prevent circular dependencies caused from type checking
from ...kernels import TrainableKernel
[docs]
class KernelLoss(ABC):
"""
Abstract base class for computing the loss of a kernel function.
Unlike many loss functions, which only take into account the labels and predictions
of a model, kernel loss functions may be a function of internal model parameters or
quantities that are generated during training.
"""
def __call__(
self,
parameter_values: Sequence[float],
quantum_kernel: TrainableKernel,
data: np.ndarray,
labels: np.ndarray,
) -> float:
"""
This method calls the ``evaluate`` method. This is a convenient method to compute loss.
"""
return self.evaluate(parameter_values, quantum_kernel, data, labels)
[docs]
@abstractmethod
def evaluate(
self,
parameter_values: Sequence[float],
quantum_kernel: TrainableKernel,
data: np.ndarray,
labels: np.ndarray,
) -> float:
"""
An abstract method for evaluating the loss of a kernel function on a labeled dataset.
Args:
parameter_values: An array of values to assign to the user params
quantum_kernel: A trainable quantum kernel object to evaluate
data: An ``(N, M)`` matrix containing the data
``N = # samples, M = dimension of data``
labels: A length-N array containing the truth labels
Returns:
A loss value
"""
raise NotImplementedError
[docs]
class SVCLoss(KernelLoss):
r"""
This class provides a kernel loss function for classification tasks by fitting an ``SVC`` model
from scikit-learn. Given training samples, :math:`x_{i}`, with binary labels, :math:`y_{i}`,
and a kernel, :math:`K_{θ}`, parameterized by values, :math:`θ`, the loss is defined as:
.. math::
SVCLoss = \sum_{i} a_i - 0.5 \sum_{i,j} a_i a_j y_{i} y_{j} K_θ(x_i, x_j)
where :math:`a_i` are the optimal Lagrange multipliers found by solving the standard SVM
quadratic program. Note that the hyper-parameter ``C`` for the soft-margin penalty can be
specified through the keyword args.
Minimizing this loss over the parameters, :math:`θ`, of the kernel is equivalent to maximizing a
weighted kernel alignment, which in turn yields the smallest upper bound to the SVM
generalization error for a given parameterization.
See https://arxiv.org/abs/2105.03406 for further details.
"""
def __init__(self, **kwargs):
"""
Args:
**kwargs: Arbitrary keyword arguments to pass to SVC constructor within
SVCLoss evaluation.
"""
self.kwargs = kwargs
[docs]
def evaluate(
self,
parameter_values: Sequence[float],
quantum_kernel: TrainableKernel,
data: np.ndarray,
labels: np.ndarray,
) -> float:
# Bind training parameters
quantum_kernel.assign_training_parameters(parameter_values)
# Get estimated kernel matrix
kmatrix = quantum_kernel.evaluate(np.array(data))
# Train a quantum support vector classifier
svc = SVC(kernel="precomputed", **self.kwargs)
svc.fit(kmatrix, labels)
# Get dual coefficients
dual_coefs = svc.dual_coef_[0]
# Get support vectors
support_vecs = svc.support_
# Prune kernel matrix of non-support-vector entries
kmatrix = kmatrix[support_vecs, :][:, support_vecs]
# Calculate loss
loss = np.sum(np.abs(dual_coefs)) - (0.5 * (dual_coefs.T @ kmatrix @ dual_coefs))
return loss