-
Notifications
You must be signed in to change notification settings - Fork 328
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add QuantumKernel
class using terra primitives
#437
Changes from 102 commits
71e1bf1
ae9c043
3aae2df
fa98e91
5106333
de53484
28d578c
cdfcfe8
f65b5f4
5368534
a1a2a7f
586c6a6
8f68be7
c67ed15
d2342e0
a092545
d255e02
52c928e
a3cf674
0636e2d
c3b0692
fcc670f
ba575ce
5de6d92
fef72ba
a855070
a724193
1b11415
c42228e
fe61463
44e20a6
e92e4b0
d368f19
460e0b2
a0c8847
e9ace5f
2eaecc3
fa7b434
69c06a3
bd32153
d553721
2fadf5a
fdfc0ce
af47536
36b5f2a
f8e3cca
beb1ec5
9667f3c
e164da8
eaf42a4
4c3bae1
bb2c8d4
cd5e55c
065c90e
4a35d64
4bd864d
fa9b41f
7c203a8
1bbc84c
769629c
e715a4a
c06b78d
600989e
760cbd3
dd906b9
9b24926
68b974c
e6cf1d1
7379f53
0d6d0d1
50ef2ca
5312d49
f3c47cf
05d7a62
1b2c592
f642185
1af8f9c
8d5453d
d4cf9e8
65489df
63219e2
9c031fc
4b35cdd
51b339f
a7f82b1
f59f38b
661773b
68432dc
5a28321
1458256
3cc7475
4ca12f7
5207a67
11e2352
30ad7e3
2ee5174
d306f9a
29f9989
4acbc1a
69cdae6
9b00a48
41809da
49540c7
fcac58d
024193c
156b041
c9b6588
8629917
6c8400a
41bd100
ecfc266
80c1954
7e53c38
5c7db95
0166fe6
bebbdd9
f90bf0c
57535b5
9739d48
b54f250
fbc7d84
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,15 +14,19 @@ | |
|
||
import logging | ||
from datetime import datetime | ||
from typing import Optional, Dict | ||
from typing import Optional, Dict, Union | ||
|
||
import numpy as np | ||
from qiskit.utils import algorithm_globals | ||
from sklearn.base import ClassifierMixin | ||
|
||
from ...algorithms.serializable_model import SerializableModelMixin | ||
from ...exceptions import QiskitMachineLearningError | ||
from ...kernels.quantum_kernel import QuantumKernel | ||
from ...kernels import BaseKernel, FidelityQuantumKernel | ||
from ...kernels.quantum_kernel import QuantumKernel as QuantumKernelOld | ||
|
||
QuantumKernel = Union[QuantumKernelOld, BaseKernel] | ||
|
||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
@@ -38,7 +42,7 @@ class PegasosQSVC(ClassifierMixin, SerializableModelMixin): | |
|
||
.. code-block:: python | ||
|
||
quantum_kernel = QuantumKernel() | ||
quantum_kernel = FidelityQuantumKernel() | ||
|
||
pegasos_qsvc = PegasosQSVC(quantum_kernel=quantum_kernel) | ||
pegasos_qsvc.fit(sample_train, label_train) | ||
|
@@ -64,7 +68,7 @@ def __init__( | |
) -> None: | ||
""" | ||
Args: | ||
quantum_kernel: QuantumKernel to be used for classification. Has to be ``None`` when | ||
quantum_kernel: a quantum kernel to be used for classification. Has to be ``None`` when | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to change things. when we are changing the code away from Union and Optional etc. Looking at the above constructor it still the old way - perhaps as we are changing things we can do this too. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is just to emphasize this is not a class, but rather a term. The PR is large enough, I don't want to change other things. All type hints require an additional revision of the QML code. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we can also update Union & Optional constructs too, like for the quantum_kernel arg here, to use the | that we are preferring to use now. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wanted to do that in a separate PR as this one is heavy now, but okay. Replaced with |
||
a precomputed kernel is used. | ||
C: Positive regularization parameter. The strength of the regularization is inversely | ||
proportional to C. Smaller ``C`` induce smaller weights which generally helps | ||
|
@@ -85,17 +89,20 @@ def __init__( | |
- if ``quantum_kernel`` is passed and ``precomputed`` is set to ``True``. To use | ||
a precomputed kernel, ``quantum_kernel`` has to be of the ``None`` type. | ||
TypeError: | ||
- if ``quantum_instance`` neither instance of ``QuantumKernel`` nor ``None``. | ||
- if ``quantum_kernel`` neither instance of | ||
:class:`~qiskit_machine_learning.kernels.BaseKernel` nor ``None``. | ||
Comment on lines
+90
to
+91
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not strictly true at present right since it allows the pre-existing QuantumKernel code too. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now should be fine :) |
||
""" | ||
|
||
if precomputed: | ||
if quantum_kernel is not None: | ||
raise ValueError("'quantum_kernel' has to be None to use a precomputed kernel") | ||
else: | ||
if quantum_kernel is None: | ||
quantum_kernel = QuantumKernel() | ||
elif not isinstance(quantum_kernel, QuantumKernel): | ||
raise TypeError("'quantum_kernel' has to be of type None or QuantumKernel") | ||
quantum_kernel = FidelityQuantumKernel() | ||
elif not isinstance(quantum_kernel, QuantumKernelOld) and not isinstance( | ||
quantum_kernel, BaseKernel | ||
): | ||
raise TypeError("'quantum_kernel' has to be of type None or BaseKernel") | ||
|
||
self._quantum_kernel = quantum_kernel | ||
self._precomputed = precomputed | ||
|
@@ -130,7 +137,8 @@ def fit( | |
"""Fit the model according to the given training data. | ||
|
||
Args: | ||
X: Train features. For a callable kernel (an instance of ``QuantumKernel``) the shape | ||
X: Train features. For a callable kernel (an instance of | ||
:class:`~qiskit_machine_learning.kernels.BaseKernel`) the shape | ||
should be ``(n_samples, n_features)``, for a precomputed kernel the shape should be | ||
``(n_samples, n_samples)``. | ||
y: shape (n_samples), train labels . Must not contain more than two unique labels. | ||
|
@@ -206,7 +214,8 @@ def predict(self, X: np.ndarray) -> np.ndarray: | |
Perform classification on samples in X. | ||
|
||
Args: | ||
X: Features. For a callable kernel (an instance of ``QuantumKernel``) the shape | ||
X: Features. For a callable kernel (an instance of | ||
:class:`~qiskit_machine_learning.kernels.BaseKernel`) the shape | ||
should be ``(m_samples, n_features)``, for a precomputed kernel the shape should be | ||
``(m_samples, n_samples)``. Where ``m`` denotes the set to be predicted and ``n`` the | ||
size of the training set. In that case, the kernel values in X have to be calculated | ||
|
@@ -234,7 +243,8 @@ def decision_function(self, X: np.ndarray) -> np.ndarray: | |
Evaluate the decision function for the samples in X. | ||
|
||
Args: | ||
X: Features. For a callable kernel (an instance of ``QuantumKernel``) the shape | ||
X: Features. For a callable kernel (an instance of | ||
:class:`~qiskit_machine_learning.kernels.BaseKernel`) the shape | ||
should be ``(m_samples, n_features)``, for a precomputed kernel the shape should be | ||
``(m_samples, n_samples)``. Where ``m`` denotes the set to be predicted and ``n`` the | ||
size of the training set. In that case, the kernel values in X have to be calculated | ||
|
@@ -340,14 +350,15 @@ def precomputed(self) -> bool: | |
@precomputed.setter | ||
def precomputed(self, precomputed: bool): | ||
"""Sets the pre-computed kernel flag. If ``True`` is passed then the previous kernel is | ||
cleared. If ``False`` is passed then a new instance of ``QuantumKernel`` is created.""" | ||
cleared. If ``False`` is passed then a new instance of | ||
:class:`~qiskit_machine_learning.kernels.BaseKernel` is created.""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be FidelityQuantumKernel - that is a BaseKernel, but is that too abstract that a user needs to know the actual type? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's right, updated. |
||
self._precomputed = precomputed | ||
if precomputed: | ||
# remove the kernel, a precomputed will | ||
self._quantum_kernel = None | ||
else: | ||
# re-create a new default quantum kernel | ||
self._quantum_kernel = QuantumKernel() | ||
self._quantum_kernel = FidelityQuantumKernel() | ||
|
||
# reset training status | ||
self._reset_state() | ||
|
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -11,6 +11,7 @@ | |||||||||
# that they have been altered from the originals. | ||||||||||
|
||||||||||
"""Quantum Kernel Trainer""" | ||||||||||
|
||||||||||
import copy | ||||||||||
from functools import partial | ||||||||||
from typing import Union, Optional, Sequence | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can take advantage of this PR to update the annotations in this class too, but I cannot comment on unchanged lines :(
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd keep the changes to the existing classes minimal in this PR. Just to make the review simpler. We can address type hints in a separate PR. |
||||||||||
|
@@ -22,7 +23,11 @@ | |||||||||
from qiskit.algorithms.variational_algorithm import VariationalResult | ||||||||||
from qiskit_machine_learning.utils.loss_functions import KernelLoss, SVCLoss | ||||||||||
|
||||||||||
from qiskit_machine_learning.kernels import QuantumKernel | ||||||||||
from qiskit_machine_learning.kernels import QuantumKernel as QuantumKernelOld | ||||||||||
from qiskit_machine_learning.kernels import TrainableFidelityQuantumKernel | ||||||||||
|
||||||||||
|
||||||||||
QuantumKernel = Union[QuantumKernelOld, TrainableFidelityQuantumKernel] | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should the latter Union type not be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I highly doubt that this would be straightforward. The whole reason for this PR (before we started adding the primitives) was that it was basically impossible to extend the QuantumKernel class in a clean way. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was not looking to extend the old QuantumKernel. More would it be simple enough to adapt it a little so the old code looked like the new way wrt to the interface e.g. BaseKernel that I commented elsewhere or TrainableKernelMixin here. The intent remains to drop it going forwards I was more wondering if adapting it lightly would mean this code could be what we want going forwards at this point rather than having some Union and having to change this once again when the QuantumKernel gets dropped. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, no more unions. |
||||||||||
|
||||||||||
|
||||||||||
class QuantumKernelTrainerResult(VariationalResult): | ||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
# This code is part of Qiskit. | ||
# | ||
# (C) Copyright IBM 2022. | ||
# | ||
# 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. | ||
|
||
"""Base kernel""" | ||
|
||
from __future__ import annotations | ||
|
||
from abc import abstractmethod, ABC | ||
|
||
import numpy as np | ||
from qiskit import QuantumCircuit | ||
from qiskit.circuit.library import ZZFeatureMap | ||
|
||
|
||
class BaseKernel(ABC): | ||
r""" | ||
Abstract class defining the Kernel interface. | ||
|
||
The general task of machine learning is to find and study patterns in data. For many | ||
algorithms, the datapoints are better understood in a higher dimensional feature space, | ||
through the use of a kernel function: | ||
|
||
.. math:: | ||
|
||
K(x, y) = \langle f(x), f(y)\rangle. | ||
|
||
Here K is the kernel function, x, y are n dimensional inputs. f is a map from n-dimension | ||
to m-dimension space. :math:`\langle x, y \rangle` denotes the dot product. | ||
Usually m is much larger than n. | ||
|
||
The quantum kernel algorithm calculates a kernel matrix, given datapoints x and y and feature | ||
map f, all of n dimension. This kernel matrix can then be used in classical machine learning | ||
algorithms such as support vector classification, spectral clustering or ridge regression. | ||
""" | ||
|
||
def __init__(self, feature_map: QuantumCircuit = 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. | ||
enforce_psd: Project to closest positive semidefinite matrix if ``x = y``. | ||
Default ``True``. | ||
""" | ||
if feature_map is None: | ||
feature_map = ZZFeatureMap(2) | ||
|
||
self._feature_map = feature_map | ||
self._enforce_psd = enforce_psd | ||
Comment on lines
+60
to
+61
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would we want to expose these (feature_map and enforce_psd) as public instance vars that the user can get/set to read/update these? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Resetting the feature map means reinitializing a fidelity instance with the corresponding circuit. If number of parameters change, this also needs to be accounted for which is especially non-trivial in the trainable case. We thought this through at some point and came to the conclusion that it might be easier to just create a new kernel instance when you need to change the feature map. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updating the feature map may be non-trivial, that's right. Even for trivial properties, I'm hesitant to provide setters, immutable object safer and easier to maintain. Added some getters. |
||
|
||
@abstractmethod | ||
def evaluate(self, x_vec: np.ndarray, y_vec: np.ndarray | None = None) -> np.ndarray: | ||
r""" | ||
Construct kernel matrix for given data. | ||
|
||
If y_vec is None, self inner product is calculated. | ||
|
||
Args: | ||
x_vec: 1D or 2D array of datapoints, NxD, where N is the number of datapoints, | ||
D is the feature dimension | ||
y_vec: 1D or 2D array of datapoints, MxD, where M is the number of datapoints, | ||
D is the feature dimension | ||
|
||
Returns: | ||
2D matrix, NxM | ||
""" | ||
raise NotImplementedError() | ||
|
||
def _validate_input( | ||
self, x_vec: np.ndarray, y_vec: np.ndarray | None | ||
) -> tuple[np.ndarray, np.ndarray | None]: | ||
x_vec = np.asarray(x_vec) | ||
|
||
if x_vec.ndim > 2: | ||
raise ValueError("x_vec must be a 1D or 2D array") | ||
|
||
if x_vec.ndim == 1: | ||
x_vec = np.reshape(x_vec, (-1, len(x_vec))) | ||
|
||
if x_vec.shape[1] != self._feature_map.num_parameters: | ||
# before raising an error we try to adjust the feature map | ||
# to the required number of qubit. | ||
try: | ||
self._feature_map.num_qubits = x_vec.shape[1] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we raise a warning here to let the user know the feature map used is not exactly what they provided as an argument? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This logic was adapted from another algorithm, perhaps VQE, where the same approach without warnings is implemented. I'd keep as is for consistency reasons. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems reasonable. I just thought it would be more transparent and probably help with debugging as I think if someone uses training data that doesn't fit the feature map it's probably more likely to be a mistake. It's nice that the algorithm still runs but might lead to unexpected results.. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The idea was these were more template circuits where you defined what you wanted upfront but maybe at that time do not have detail on the problem. Later when we do have actual problem it can then be updated. In VQE it adjusts the ansatz to match the operator. |
||
except AttributeError as a_e: | ||
raise ValueError( | ||
f"x_vec and class feature map have incompatible dimensions.\n" | ||
f"x_vec has {x_vec.shape[1]} dimensions, " | ||
f"but feature map has {self._feature_map.num_parameters}." | ||
) from a_e | ||
|
||
if y_vec is not None: | ||
y_vec = np.asarray(y_vec) | ||
|
||
if y_vec.ndim == 1: | ||
y_vec = np.reshape(y_vec, (-1, len(y_vec))) | ||
|
||
if y_vec.ndim > 2: | ||
raise ValueError("y_vec must be a 1D or 2D array") | ||
|
||
if y_vec.shape[1] != x_vec.shape[1]: | ||
raise ValueError( | ||
"x_vec and y_vec have incompatible dimensions.\n" | ||
f"x_vec has {x_vec.shape[1]} dimensions, but y_vec has {y_vec.shape[1]}." | ||
) | ||
|
||
return x_vec, y_vec | ||
|
||
# pylint: disable=invalid-name | ||
def _make_psd(self, kernel_matrix: np.ndarray) -> np.ndarray: | ||
r""" | ||
Find the closest positive semi-definite approximation to symmetric kernel matrix. | ||
The (symmetric) matrix should always be positive semi-definite by construction, | ||
but this can be violated in case of noise, such as sampling noise. | ||
|
||
Args: | ||
kernel_matrix: symmetric 2D array of the kernel entries | ||
|
||
Returns: | ||
the closest positive semi-definite matrix. | ||
""" | ||
d, u = np.linalg.eig(kernel_matrix) | ||
return u @ np.diag(np.maximum(0, d)) @ u.transpose() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could the existing QuantumKernel, i.e QuantumKernelOld alias here be easily altered and made a BaseKernel (i.e. extend it). Going forwards, unless we really want QuantumKernel to be a synonym for BaseKernel (when QuantumKernelOld here goes away) this would allow things to be changed to take and return a BaseKernel, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer to keep
QuantumKernel
separated from the new hierarchy. Especially considering it's training capabilities. While inheriting fromBaseKernel
may be not so bad, but adding the mixin will make it over complicated, the ways how kernel elements are computed are too different now. Since we want to deprecate the existing one, I don't really want to change it now.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was looking and being BaseKernel it would mean a bunch of change that could be done now to standardize around that rather needing a Union type (of which the same QuantumKernel type is different Unions in different places) which I don't know what you have in mind once half the Union above goes away. From what I could tell the former QuantumKernel already seemed to fit the BaseKernel public API. So it seemed, on the surface, simple enough and would mean going forwards the class would imply get removed, no other code like this Union etc would need changing. The change would have to leave it compatible, but if all it meant is changing to extend BaseKernel it seemed simply enough and from what I could see had the benefit of avoid a potential bunch of change again in the future,
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All right, now we have
BaseKernel
FidelityQuantumKernel(BaseKernel)
TrainableKernel(BaseKernel, ABC)
FidelityTrainableQuantumKernel(TrainableKernel, FidelityQuantumKernel)
QuantumKernel(BaseKernel, TrainableKernel)
Additional tweaks of
**kwargs
may be required in the future to support more inheritance patterns.