Skip to content

Commit

Permalink
Add TrainableFidelityStatevectorKernel (#639)
Browse files Browse the repository at this point in the history
* add trainable fidelity statevector kernel

* fix copyright

* refactor check on trainable parameters

* fix copyright

* consolidate trainable kernels tests

* avoid string flags in tests

* fix copyright

* test trainer with statevector kernel

* add release note

* fix style

* format release note

* lint

* style

* Update release note based on Steve's comments

* add imports to release note snippet

* fix typo

---------

Co-authored-by: Anton Dekusar <[email protected]>
  • Loading branch information
declanmillar and adekusar-drl authored Jun 10, 2023
1 parent 4df88ec commit 2471792
Show file tree
Hide file tree
Showing 9 changed files with 260 additions and 89 deletions.
3 changes: 3 additions & 0 deletions qiskit_machine_learning/kernels/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
FidelityStatevectorKernel
TrainableKernel
TrainableFidelityQuantumKernel
TrainableFidelityStatevectorKernel
Submodules
==========
Expand All @@ -64,6 +65,7 @@
from .fidelity_statevector_kernel import FidelityStatevectorKernel
from .trainable_kernel import TrainableKernel
from .trainable_fidelity_quantum_kernel import TrainableFidelityQuantumKernel
from .trainable_fidelity_statevector_kernel import TrainableFidelityStatevectorKernel

__all__ = [
"QuantumKernel",
Expand All @@ -72,4 +74,5 @@
"FidelityStatevectorKernel",
"TrainableKernel",
"TrainableFidelityQuantumKernel",
"TrainableFidelityStatevectorKernel",
]
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ def evaluate(
elif not np.array_equal(x_vec, y_vec):
is_symmetric = False

return self._evaluate(x_vec, y_vec, is_symmetric)

def _evaluate(self, x_vec: np.ndarray, y_vec: np.ndarray, is_symmetric: bool):
kernel_shape = (x_vec.shape[0], y_vec.shape[0])

x_svs = [self._get_statevector(tuple(x)) for x in x_vec]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2022.
# (C) Copyright IBM 2022, 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
Expand All @@ -23,7 +23,6 @@

from .fidelity_quantum_kernel import FidelityQuantumKernel, KernelIndices
from .trainable_kernel import TrainableKernel
from ..exceptions import QiskitMachineLearningError


class TrainableFidelityQuantumKernel(TrainableKernel, FidelityQuantumKernel):
Expand Down Expand Up @@ -101,28 +100,6 @@ def __init__(
]
self._parameter_dict = {parameter: None for parameter in feature_map.parameters}

def evaluate(self, x_vec: np.ndarray, y_vec: np.ndarray | None = None) -> np.ndarray:
for param in self._training_parameters:
if self._parameter_dict[param] is None:
raise QiskitMachineLearningError(
f"Trainable parameter {param} has not been bound. Make sure to bind all"
"trainable parameters to numerical values using `.assign_training_parameters()`"
"before calling `.evaluate()`."
)
return super().evaluate(x_vec, y_vec)

def _parameter_array(self, x_vec: np.ndarray) -> np.ndarray:
"""
Combines the feature values and the trainable parameters into one array.
"""
full_array = np.zeros((x_vec.shape[0], self._num_features + self._num_training_parameters))
for i, x in enumerate(x_vec):
self._parameter_dict.update(
{feature_param: x[j] for j, feature_param in enumerate(self._feature_parameters)}
)
full_array[i, :] = list(self._parameter_dict.values())
return full_array

def _get_parameterization(
self, x_vec: np.ndarray, y_vec: np.ndarray
) -> tuple[np.ndarray, np.ndarray, KernelIndices]:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 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.

"""Trainable Fidelity Statevector Kernel"""

from __future__ import annotations

from typing import Sequence, Type

import numpy as np
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter, ParameterVector
from qiskit.quantum_info import Statevector


from .fidelity_statevector_kernel import FidelityStatevectorKernel, SV
from .trainable_kernel import TrainableKernel


class TrainableFidelityStatevectorKernel(TrainableKernel, FidelityStatevectorKernel):
r"""
A trainable version of the
:class:`~qiskit_machine_learning.kernels.FidelityStatevectorKernel`.
Finding good quantum kernels for a specific machine learning task is a big challenge in quantum
machine learning. One way to choose the kernel is to add trainable parameters to the feature
map, which can be used to fine-tune the kernel.
This kernel has trainable parameters :math:`\theta` that can be bound using training algorithms.
The kernel entries are given as
.. math::
K_{\theta}(x,y) = |\langle \phi_{\theta}(x) | \phi_{\theta}(y) \rangle|^2
"""

def __init__(
self,
*,
feature_map: QuantumCircuit | None = None,
statevector_type: Type[SV] = Statevector,
training_parameters: ParameterVector | Sequence[Parameter] | None = None,
cache_size: int | None = None,
auto_clear_cache: bool = True,
shots: int | None = None,
enforce_psd: bool = True,
) -> None:
"""
Args:
feature_map: Parameterized circuit to be used as the feature map. If ``None`` is given,
:class:`~qiskit.circuit.library.ZZFeatureMap` is used with two qubits. If there's
a mismatch in the number of qubits of the feature map and the number of features
in the dataset, then the kernel will try to adjust the feature map to reflect the
number of features.
statevector_type: The type of Statevector that will be instantiated using the
``feature_map`` quantum circuit and used to compute the fidelity kernel. This type
should inherit from (and defaults to) :class:`~qiskit.quantum_info.Statevector`.
training_parameters: Iterable containing :class:`~qiskit.circuit.Parameter` objects
which correspond to quantum gates on the feature map circuit which may be tuned.
If users intend to tune feature map parameters to find optimal values, this field
should be set.
cache_size: Maximum size of the statevector cache. When ``None`` this is unbounded.
auto_clear_cache: Determines whether the statevector cache is retained when
:meth:`evaluate` is called. The cache is automatically cleared by default.
shots: The number of shots. If ``None``, the exact fidelity is used. Otherwise, the
mean is taken of samples drawn from a binomial distribution with probability equal
to the exact fidelity.
enforce_psd: Project to the closest positive semidefinite matrix if ``x = y``.
Default ``True``.
"""
super().__init__(
feature_map=feature_map,
statevector_type=statevector_type,
training_parameters=training_parameters,
cache_size=cache_size,
auto_clear_cache=auto_clear_cache,
shots=shots,
enforce_psd=enforce_psd,
)

# Override the number of features defined in the base class.
self._num_features = feature_map.num_parameters - self._num_training_parameters
self._feature_parameters = [
parameter
for parameter in feature_map.parameters
if parameter not in training_parameters
]
self._parameter_dict = {parameter: None for parameter in self.feature_map.parameters}

def _evaluate(self, x_vec: np.ndarray, y_vec: np.ndarray, is_symmetric: bool):
new_x_vec = self._parameter_array(x_vec)
new_y_vec = self._parameter_array(y_vec)
return super()._evaluate(new_x_vec, new_y_vec, is_symmetric)
27 changes: 26 additions & 1 deletion qiskit_machine_learning/kernels/trainable_kernel.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2022.
# (C) Copyright IBM 2022, 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
Expand All @@ -21,6 +21,7 @@
from qiskit.circuit.parameterexpression import ParameterValueType

from .base_kernel import BaseKernel
from ..exceptions import QiskitMachineLearningError


class TrainableKernel(BaseKernel, ABC):
Expand All @@ -44,6 +45,8 @@ def __init__(

self._parameter_dict = {parameter: None for parameter in training_parameters}

self._feature_parameters: Sequence[Parameter] = []

def assign_training_parameters(
self,
parameter_values: Mapping[Parameter, ParameterValueType] | Sequence[ParameterValueType],
Expand Down Expand Up @@ -93,3 +96,25 @@ def num_training_parameters(self) -> int:
Returns the number of training parameters.
"""
return len(self._training_parameters)

def _parameter_array(self, x_vec: np.ndarray) -> np.ndarray:
"""
Combines the feature values and the trainable parameters into one array.
"""
self._check_trainable_parameters()
full_array = np.zeros((x_vec.shape[0], self._num_features + self._num_training_parameters))
for i, x in enumerate(x_vec):
self._parameter_dict.update(
{feature_param: x[j] for j, feature_param in enumerate(self._feature_parameters)}
)
full_array[i, :] = list(self._parameter_dict.values())
return full_array

def _check_trainable_parameters(self) -> None:
for param in self._training_parameters:
if self._parameter_dict[param] is None:
raise QiskitMachineLearningError(
f"Trainable parameter {param} has not been bound. Make sure to bind all"
"trainable parameters to numerical values using `.assign_training_parameters()`"
"before calling `.evaluate()`."
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
features:
- |
A new :class:`~qiskit_machine_learning.kernels.TrainableFidelityStatevectorKernel` class has
been added that provides a trainable version of
:class:`~qiskit_machine_learning.kernels.FidelityStatevectorKernel`. This relationship mirrors
that between the existing
:class:`~qiskit_machine_learning.kernels.TrainableFidelityQuantumKernel`class and
:class:`~qiskit_machine_learning.kernels.FidelityQuantumKernel`. Thus,
:class:`~qiskit_machine_learning.kernels.TrainableFidelityStatevectorKernel` inherits from both
:class:`~qiskit_machine_learning.kernels.FidelityStatevectorKernel` and
:class:`~qiskit_machine_learning.kernels.TrainableKernel`.
This class is used with
:class:`~qiskit_machine_learning.kernels.algorithms.QuantumKernelTrainer` in an identical way to
:class:`~qiskit_machine_learning.kernels.TrainableFidelityQuantumKernel`, except for the
arguments specific to
:class:`~qiskit_machine_learning.kernels.TrainableFidelityStatevectorKernel`.
For an example, see the snippet below:
.. code-block:: python
from qiskit.quantum_info import Statevector
from qiskit_machine_learning.kernels import TrainableFidelityStatevectorKernel
from qiskit_machine_learning.kernels.algorithms import QuantumKernelTrainer
# Instantiate trainable fidelity statevector kernel.
quantum_kernel = TrainableFidelityStatevectorKernel(
feature_map=<your_feature_map>,
statevector_type=Statevector,
training_parameters=<your_training_parameters>,
cache_size=None,
auto_clear_cache=True,
shots=None,
enforce_psd=True,
)
# Instantiate a quantum kernel trainer (QKT).
qkt = QuantumKernelTrainer(quantum_kernel=quantum_kernel)
# Train the kernel using QKT directly.
qkt_results = qkt.fit(<your_X_train>, <your_y_train>)
optimized_kernel = qkt_results.quantum_kernel
49 changes: 35 additions & 14 deletions test/kernels/algorithms/test_fidelity_qkernel_trainer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2021, 2022.
# (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
Expand All @@ -18,6 +18,7 @@

from test import QiskitMachineLearningTestCase

from ddt import ddt, data
import numpy as np
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter, ParameterVector
Expand All @@ -26,13 +27,20 @@
from scipy.optimize import minimize

from qiskit_machine_learning.algorithms.classifiers import QSVC
from qiskit_machine_learning.kernels import TrainableFidelityQuantumKernel
from qiskit_machine_learning.kernels import (
TrainableFidelityQuantumKernel,
TrainableFidelityStatevectorKernel,
)
from qiskit_machine_learning.kernels.algorithms import QuantumKernelTrainer
from qiskit_machine_learning.utils.loss_functions import SVCLoss


@ddt
class TestQuantumKernelTrainer(QiskitMachineLearningTestCase):
"""Test QuantumKernelTrainer Algorithm"""
"""Test QuantumKernelTrainer Algorithm
Tests usage with ``TrainableFidelityQuantumKernel`` and ``TrainableFidelityStatevectorKernel``.
"""

def setUp(self):
super().setUp()
Expand All @@ -57,33 +65,46 @@ def setUp(self):
self.sample_test = np.asarray([[2.199114860, 5.15221195], [0.50265482, 0.06283185]])
self.label_test = np.asarray([1, 0])

self.quantum_kernel = TrainableFidelityQuantumKernel(
@data(
TrainableFidelityQuantumKernel,
TrainableFidelityStatevectorKernel,
)
def test_default_fit(self, trainable_kernel_type):
"""Test trainer with default parameters."""
quantum_kernel = trainable_kernel_type(
feature_map=self.feature_map,
training_parameters=self.training_parameters,
)

def test_default_fit(self):
"""Test trainer with default parameters."""
qkt = QuantumKernelTrainer(quantum_kernel=self.quantum_kernel)
qkt = QuantumKernelTrainer(quantum_kernel=quantum_kernel)
qkt_result = qkt.fit(self.sample_train, self.label_train)

self._fit_and_assert_score(qkt_result)

def test_fit_with_params(self):
@data(
TrainableFidelityQuantumKernel,
TrainableFidelityStatevectorKernel,
)
def test_fit_with_params(self, trainable_kernel_type):
"""Test trainer with custom parameters."""
quantum_kernel = trainable_kernel_type(
feature_map=self.feature_map,
training_parameters=self.training_parameters,
)
loss = SVCLoss(C=0.8, gamma="auto")
optimizer = partial(minimize, method="COBYLA", options={"maxiter": 25})
qkt = QuantumKernelTrainer(
quantum_kernel=self.quantum_kernel, loss=loss, optimizer=optimizer
)
qkt = QuantumKernelTrainer(quantum_kernel=quantum_kernel, loss=loss, optimizer=optimizer)
qkt_result = qkt.fit(self.sample_train, self.label_train)

# Ensure user parameters are bound to real values
self.assertTrue(np.all(qkt_result.quantum_kernel.parameter_values))

self._fit_and_assert_score(qkt_result)

def test_asymmetric_trainable_parameters(self):
@data(
TrainableFidelityQuantumKernel,
TrainableFidelityStatevectorKernel,
)
def test_asymmetric_trainable_parameters(self, trainable_kernel_type):
"""Test when the number of trainable parameters does not equal to the number of features."""
qc = QuantumCircuit(2)
training_parameters = Parameter("θ")
Expand All @@ -92,7 +113,7 @@ def test_asymmetric_trainable_parameters(self):
qc.rz(feature_params[0], 0)
qc.rz(feature_params[1], 1)

quantum_kernel = TrainableFidelityQuantumKernel(
quantum_kernel = trainable_kernel_type(
feature_map=qc,
training_parameters=[training_parameters],
)
Expand Down
Loading

0 comments on commit 2471792

Please sign in to comment.