From 71e1bf1084390c47de122c48c520f297d3526deb Mon Sep 17 00:00:00 2001 From: Manoel Marques Date: Wed, 29 Jun 2022 10:32:43 -0400 Subject: [PATCH 01/61] Add primitives branch to CI workflow --- .github/workflows/main.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5f3f93eeb..d177615be 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,17 +16,16 @@ on: push: branches: - main + - primitives - 'stable/**' pull_request: branches: - main + - primitives - 'stable/**' - schedule: - # run every day at 1AM - - cron: '0 1 * * *' concurrency: - group: ${{ github.repository }}-${{ github.ref }}-${{ github.head_ref }} + group: ${{ github.repository }}-${{ github.ref }}-${{ github.head_ref }}-${{ github.workflow }} cancel-in-progress: true jobs: From ae9c043be427212c058741014398c1feea347e93 Mon Sep 17 00:00:00 2001 From: Gian Gentinetta Date: Wed, 29 Jun 2022 18:01:58 +0200 Subject: [PATCH 02/61] added pocs --- .../primitives/__init__.py | 24 +++ .../primitives/kernels/__init__.py | 44 +++++ .../primitives/kernels/base_kernel.py | 118 ++++++++++++++ .../primitives/kernels/pseudo_kernel.py | 131 +++++++++++++++ .../primitives/kernels/quantum_kernel.py | 152 ++++++++++++++++++ .../kernels/trainable_quantum_kernel.py | 69 ++++++++ qiskit_machine_learning/utils/__init__.py | 4 + qiskit_machine_learning/utils/utils.py | 20 +++ 8 files changed, 562 insertions(+) create mode 100644 qiskit_machine_learning/primitives/__init__.py create mode 100644 qiskit_machine_learning/primitives/kernels/__init__.py create mode 100644 qiskit_machine_learning/primitives/kernels/base_kernel.py create mode 100644 qiskit_machine_learning/primitives/kernels/pseudo_kernel.py create mode 100644 qiskit_machine_learning/primitives/kernels/quantum_kernel.py create mode 100644 qiskit_machine_learning/primitives/kernels/trainable_quantum_kernel.py create mode 100644 qiskit_machine_learning/utils/utils.py diff --git a/qiskit_machine_learning/primitives/__init__.py b/qiskit_machine_learning/primitives/__init__.py new file mode 100644 index 000000000..66253d188 --- /dev/null +++ b/qiskit_machine_learning/primitives/__init__.py @@ -0,0 +1,24 @@ +# 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. + +""" +Primitives (:mod:`qiskit_machine_learning.primitives`) +=============================================================== + +.. currentmodule:: qiskit_machine_learning.primitives + +""" + +from .kernels import BaseKernel, QuantumKernel, TrainableQuantumKernel, PseudoKernel + + +__all__ = ["BaseKernel", "QuantumKernel", "TrainableQuantumKernel", "PseudoKernel"] diff --git a/qiskit_machine_learning/primitives/kernels/__init__.py b/qiskit_machine_learning/primitives/kernels/__init__.py new file mode 100644 index 000000000..c28b088d5 --- /dev/null +++ b/qiskit_machine_learning/primitives/kernels/__init__.py @@ -0,0 +1,44 @@ +# 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. + +""" +Primitives Quantum Kernels (:mod:`qiskit_machine_learning.primitives.kernels`) + +.. currentmodule:: qiskit_machine_learning.primitives.kernels + +Quantum Kernels +=============== + +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + BaseKernel + QuantumKernel + TrainableKernel + PseudoKernel + +Submodules +========== + +.. autosummary:: + :toctree: + + algorithms +""" + +from .base_kernel import BaseKernel +from .quantum_kernel import QuantumKernel +from .trainable_quantum_kernel import TrainableQuantumKernel +from .pseudo_kernel import PseudoKernel + +__all__ = ["BaseKernel", "QuantumKernel", "TrainableQuantumKernel", "PseudoKernel"] diff --git a/qiskit_machine_learning/primitives/kernels/base_kernel.py b/qiskit_machine_learning/primitives/kernels/base_kernel.py new file mode 100644 index 000000000..5826696ab --- /dev/null +++ b/qiskit_machine_learning/primitives/kernels/base_kernel.py @@ -0,0 +1,118 @@ +# 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. +from abc import abstractmethod +from typing import Tuple, Callable, List + +import numpy as np + +from qiskit import QuantumCircuit +from qiskit.primitives import Sampler + +SamplerFactory = Callable[[List[QuantumCircuit]], Sampler] + + +class BaseKernel: + """ + Abstract class providing the interface for the quantum kernel classes. + """ + + def __init__(self, sampler_factory: SamplerFactory, *, enforce_psd: bool = True) -> None: + """ + Args: + sampler_factory: A callable that creates a qiskit primitives sampler given a list of circuits. + enforce_psd: Project to closest positive semidefinite matrix if x = y. + Only enforced when not using the state vector simulator. Default True. + """ + self._num_features: int = 0 + self._enforce_psd = enforce_psd + self._sampler_factory = sampler_factory + + @abstractmethod + def evaluate(self, x_vec: np.ndarray, y_vec: np.ndarray = 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 + + Raises: + QiskitMachineLearningError: + - A quantum instance or backend has not been provided + ValueError: + - x_vec and/or y_vec are not one or two dimensional arrays + - x_vec and y_vec have have incompatible dimensions + - x_vec and/or y_vec have incompatible dimension with feature map and + and feature map can not be modified to match. + """ + raise NotImplementedError() + + def _check_and_reshape(self, x_vec: np.ndarray, y_vec: np.ndarray = None) -> Tuple[np.ndarray]: + r""" + Performs checks on the dimensions of the input data x_vec and y_vec. + Reshapes the arrays so that `x_vec.shape = (N,D)` and `y_vec.shape = (M,D)`. + """ + if not isinstance(x_vec, np.ndarray): + x_vec = np.asarray(x_vec) + + if y_vec is not None and not isinstance(y_vec, np.ndarray): + y_vec = np.asarray(y_vec) + + if x_vec.ndim > 2: + raise ValueError("x_vec must be a 1D or 2D array") + + if x_vec.ndim == 1: + x_vec = x_vec.reshape(1, -1) + + if y_vec is not None and y_vec.ndim > 2: + raise ValueError("y_vec must be a 1D or 2D array") + + if y_vec is not None and y_vec.ndim == 1: + y_vec = y_vec.reshape(1, -1) + + if y_vec is not None and 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]}." + ) + + if x_vec.shape[1] != self._num_features: + raise ValueError( + "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._num_features}." + ) + + if y_vec is None: + y_vec = x_vec + + return x_vec, y_vec + + 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, thus the + adjustment is only done if NOT using the statevector simulation. + + Args: + kernel_matrix: symmetric 2D array of the kernel entries + """ + d, u = np.linalg.eig(kernel_matrix) + return u @ np.diag(np.maximum(0, d)) @ u.transpose() diff --git a/qiskit_machine_learning/primitives/kernels/pseudo_kernel.py b/qiskit_machine_learning/primitives/kernels/pseudo_kernel.py new file mode 100644 index 000000000..1b1eff6d2 --- /dev/null +++ b/qiskit_machine_learning/primitives/kernels/pseudo_kernel.py @@ -0,0 +1,131 @@ +# 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. + +"""Pseudo Overlap Kernel""" + +from typing import Optional, Callable, Union, List +import numpy as np + +from qiskit import QuantumCircuit +from qiskit.primitives import Sampler +from qiskit.primitives.fidelity import BaseFidelity + +from qiskit_machine_learning.primitives import QuantumKernel +from qiskit_machine_learning.utils import make_2d + +SamplerFactory = Callable[[List[QuantumCircuit]], Sampler] + + +class PseudoKernel(QuantumKernel): + """ + Pseudo kernel + """ + + def __init__( + self, + sampler_factory: SamplerFactory, + feature_map: Optional[QuantumCircuit] = None, + *, + num_training_parameters: int = 0, + fidelity: Union[str, BaseFidelity] = "zero_prob", + enforce_psd: bool = True, + ) -> None: + super().__init__(sampler_factory, feature_map, fidelity=fidelity, enforce_psd=enforce_psd) + self.num_parameters = num_training_parameters + + def evaluate(self, x_vec: np.ndarray, y_vec: np.ndarray = None) -> np.ndarray: + # allow users to only provide features, parameters are set to 0 + if x_vec.shape[1] + self.num_parameters == self._num_features: + return self.evaluate_batch(x_vec, y_vec) + else: + return super().evaluate(x_vec, y_vec) + + def evaluate_batch( + self, + x_vec: np.ndarray, + y_vec: np.ndarray, + x_parameters: np.ndarray = None, + y_parameters: np.ndarray = None, + ) -> np.ndarray: + r""" + Construct kernel matrix for given data and feature map + + If y_vec is None, self inner product is calculated. + If using `statevector_simulator`, only build circuits for :math:`\Psi(x)|0\rangle`, + then perform inner product classically. + + 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 + x_parameters: 1D or 2D array of parameters, NxP, where N is the number of datapoints, + P is the number of trainable parameters + y_parameters: 1D or 2D array of parameters, MxP + + + Returns: + 2D matrix, NxM + + Raises: + QiskitMachineLearningError: + - A quantum instance or backend has not been provided + ValueError: + - x_vec and/or y_vec are not one or two dimensional arrays + - x_vec and y_vec have have incompatible dimensions + - x_vec and/or y_vec have incompatible dimension with feature map and + and feature map can not be modified to match. + """ + if x_parameters is None: + x_parameters = np.zeros((x_vec.shape[0], self.num_parameters)) + + if y_parameters is None: + y_parameters = np.zeros((y_vec.shape[0], self.num_parameters)) + + if len(x_vec.shape) == 1: + x_vec = x_vec.reshape(1, -1) + + if len(y_vec.shape) == 1: + y_vec = y_vec.reshape(1, -1) + + if len(x_parameters.shape) == 1: + x_parameters = make_2d(x_parameters, x_vec.shape[0]) + + if len(y_parameters.shape) == 1: + y_parameters = make_2d(y_parameters, y_vec.shape[0]) + + if x_vec.shape[0] != x_parameters.shape[0]: + if x_parameters.shape[0] == 1: + x_parameters = make_2d(x_parameters, x_vec.shape[0]) + else: + raise ValueError( + f"Number of x data points ({x_vec.shape[0]}) does not coincide with number of parameter vectors {x_parameters.shape[0]}." + ) + if y_vec.shape[0] != y_parameters.shape[0]: + if y_parameters.shape[0] == 1: + x_parameters = make_2d(y_parameters, y_vec.shape[0]) + else: + raise ValueError( + f"Number of y data points ({y_vec.shape[0]}) does not coincide with number of parameter vectors {y_parameters.shape[0]}." + ) + + if x_parameters.shape[1] != self.num_parameters: + raise ValueError( + f"Number of parameters provided ({x_parameters.shape[0]}) does not coincide with the number provided in the feature map ({self.num_parameters})." + ) + + if y_parameters.shape[1] != self.num_parameters: + raise ValueError( + f"Number of parameters provided ({y_parameters.shape[0]}) does not coincide with the number provided in the feature map ({self.num_parameters})." + ) + + return self.evaluate(np.hstack((x_vec, x_parameters)), np.hstack((y_vec, y_parameters))) diff --git a/qiskit_machine_learning/primitives/kernels/quantum_kernel.py b/qiskit_machine_learning/primitives/kernels/quantum_kernel.py new file mode 100644 index 000000000..02be21b1d --- /dev/null +++ b/qiskit_machine_learning/primitives/kernels/quantum_kernel.py @@ -0,0 +1,152 @@ +# 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. + +from typing import Optional, Tuple, Callable, List, Union +import numpy as np + +from qiskit import QuantumCircuit +from qiskit.circuit.library import ZZFeatureMap +from qiskit.primitives import Sampler +from qiskit.primitives.fidelity import BaseFidelity, Fidelity + +from qiskit_machine_learning.primitives import BaseKernel + + +SamplerFactory = Callable[[List[QuantumCircuit]], Sampler] + + +class QuantumKernel(BaseKernel): + """ + Overlap Kernel + """ + + def __init__( + self, + sampler_factory: SamplerFactory, + feature_map: Optional[QuantumCircuit] = None, + *, + fidelity: Union[str, BaseFidelity] = "zero_prob", + enforce_psd: bool = True, + ) -> None: + super().__init__(sampler_factory, enforce_psd=enforce_psd) + + if feature_map is None: + feature_map = ZZFeatureMap(2).decompose() + + self._num_features = feature_map.num_parameters + + if isinstance(fidelity, str) and fidelity == "zero_prob": + self._fidelity = Fidelity() + else: + self._fidelity = fidelity + self._fidelity.set_circuits(feature_map, feature_map) + + def evaluate(self, x_vec: np.ndarray, y_vec: np.ndarray = None) -> np.ndarray: + x_vec, y_vec = self._check_and_reshape(x_vec, y_vec) + is_symmetric = np.all(x_vec == y_vec) + shape = len(x_vec), len(y_vec) + + if is_symmetric: + left_parameters, right_parameters = self._get_symmetric_parametrization(x_vec) + kernel_matrix = self._get_symmetric_kernel_matrix( + left_parameters, right_parameters, shape + ) + + else: + left_parameters, right_parameters = self._get_parametrization(x_vec, y_vec) + kernel_matrix = self._get_kernel_matrix(left_parameters, right_parameters, shape) + + if is_symmetric and self._enforce_psd: + kernel_matrix = self._make_psd(kernel_matrix) + return kernel_matrix + + def _get_parametrization(self, x_vec: np.ndarray, y_vec: np.ndarray) -> Tuple[np.ndarray]: + """ + Combines x_vec and y_vec to get all the combinations needed to evaluate the kernel entries. + """ + x_count = x_vec.shape[0] + y_count = y_vec.shape[0] + + left_parameters = np.zeros((x_count * y_count, x_vec.shape[1])) + right_parameters = np.zeros((x_count * y_count, y_vec.shape[1])) + index = 0 + for x_i in x_vec: + for y_j in y_vec: + left_parameters[index, :] = x_i + right_parameters[index, :] = y_j + index += 1 + + return left_parameters, right_parameters + + def _get_symmetric_parametrization(self, x_vec: np.ndarray) -> Tuple[np.ndarray]: + """ + Combines two copies of x_vec to get all the combinations needed to evaluate the kernel entries. + """ + x_count = x_vec.shape[0] + + left_parameters = np.zeros((x_count * (x_count + 1) // 2, x_vec.shape[1])) + right_parameters = np.zeros((x_count * (x_count + 1) // 2, x_vec.shape[1])) + + index = 0 + for i, x_i in enumerate(x_vec): + for x_j in x_vec[i:]: + left_parameters[index, :] = x_i + right_parameters[index, :] = x_j + index += 1 + + return left_parameters, right_parameters + + def _get_kernel_matrix( + self, left_parameters: np.ndarray, right_parameters: np.ndarray, shape: Tuple + ) -> np.ndarray: + """ + Given a parametrization, this computes the symmetric kernel matrix. + """ + kernel_entries = self._fidelity.compute(left_parameters, right_parameters) + kernel_matrix = np.zeros(shape) + + index = 0 + for i in range(shape[0]): + for j in range(shape[1]): + kernel_matrix[i, j] = kernel_entries[index] + index += 1 + return kernel_matrix + + def _get_symmetric_kernel_matrix( + self, left_parameters: np.ndarray, right_parameters: np.ndarray, shape: Tuple + ) -> np.ndarray: + """ + Given a set of parametrization, this computes the kernel matrix. + """ + kernel_entries = self._fidelity.compute(left_parameters, right_parameters) + kernel_matrix = np.zeros(shape) + index = 0 + for i in range(shape[0]): + for j in range(i, shape[1]): + kernel_matrix[i, j] = kernel_entries[index] + index += 1 + + kernel_matrix = kernel_matrix + kernel_matrix.T - np.diag(kernel_matrix.diagonal()) + return kernel_matrix + + def __enter__(self): + """ + Creates the full fidelity class by creating the sampler from the factory. + """ + self._fidelity.sampler_from_factory(self._sampler_factory) + return self + + def __exit__(self, *args): + """ + Closes the sampler session. + """ + self._fidelity.sampler.close() diff --git a/qiskit_machine_learning/primitives/kernels/trainable_quantum_kernel.py b/qiskit_machine_learning/primitives/kernels/trainable_quantum_kernel.py new file mode 100644 index 000000000..3041ec182 --- /dev/null +++ b/qiskit_machine_learning/primitives/kernels/trainable_quantum_kernel.py @@ -0,0 +1,69 @@ +# 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. + +"""Trainable Quantum Kernel""" + +from typing import Tuple, Optional, Callable, List, Union +import numpy as np + +from qiskit import QuantumCircuit +from qiskit.primitives import Sampler +from qiskit.primitives.fidelity import BaseFidelity + + +from qiskit_machine_learning.primitives.kernels import QuantumKernel +from qiskit_machine_learning.utils import make_2d + +SamplerFactory = Callable[[List[QuantumCircuit]], Sampler] + + +class TrainableQuantumKernel(QuantumKernel): + """ + Trainable overlap kernel. + """ + + def __init__( + self, + sampler_factory: SamplerFactory, + feature_map: Optional[QuantumCircuit] = None, + *, + fidelity: Union[str, BaseFidelity] = "zero_prob", + num_training_parameters: int = 0, + enforce_psd: bool = True, + ) -> None: + super().__init__(sampler_factory, feature_map, fidelity=fidelity, enforce_psd=enforce_psd) + self.num_parameters = num_training_parameters + self._num_features = self._num_features - self.num_parameters + + self.parameter_values = np.zeros(self.num_parameters) + + def bind_parameters_values(self, parameter_values: np.ndarray) -> None: + """ + Fix the training parameters to numerical values. + """ + if parameter_values.shape == self.parameter_values.shape: + self.parameter_values = parameter_values + else: + raise ValueError( + f"The given parameters are in the wrong shape {parameter_values.shape}, expected {self.parameter_values.shape}." + ) + + def _get_parametrization(self, x_vec: np.ndarray, y_vec: np.ndarray) -> Tuple[np.ndarray]: + new_x_vec = np.hstack((x_vec, make_2d(self.parameter_values, len(x_vec)))) + new_y_vec = np.hstack((y_vec, make_2d(self.parameter_values, len(y_vec)))) + + return super()._get_parametrization(new_x_vec, new_y_vec) + + def _get_symmetric_parametrization(self, x_vec: np.ndarray) -> np.ndarray: + new_x_vec = np.hstack((x_vec, make_2d(self.parameter_values, len(x_vec)))) + + return super()._get_symmetric_parametrization(new_x_vec) diff --git a/qiskit_machine_learning/utils/__init__.py b/qiskit_machine_learning/utils/__init__.py index 000618000..0ac0473ae 100644 --- a/qiskit_machine_learning/utils/__init__.py +++ b/qiskit_machine_learning/utils/__init__.py @@ -26,3 +26,7 @@ loss_functions """ + +from .utils import make_2d + +__all__ = ["make_2d"] diff --git a/qiskit_machine_learning/utils/utils.py b/qiskit_machine_learning/utils/utils.py new file mode 100644 index 000000000..901658f3c --- /dev/null +++ b/qiskit_machine_learning/utils/utils.py @@ -0,0 +1,20 @@ +# 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. + +import numpy as np + + +def make_2d(array: np.ndarray, n_copies: int): + """ + Takes a 1D numpy array and copies n times it along a second axis. + """ + return np.repeat(array[np.newaxis, :], n_copies, axis=0) From 3aae2df0fb70e686e77e321ad2ce69cabb469415 Mon Sep 17 00:00:00 2001 From: Gian Gentinetta Date: Thu, 30 Jun 2022 17:52:02 +0200 Subject: [PATCH 03/61] init fixed --- qiskit_machine_learning/primitives/kernels/quantum_kernel.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qiskit_machine_learning/primitives/kernels/quantum_kernel.py b/qiskit_machine_learning/primitives/kernels/quantum_kernel.py index 02be21b1d..e7ec20c2e 100644 --- a/qiskit_machine_learning/primitives/kernels/quantum_kernel.py +++ b/qiskit_machine_learning/primitives/kernels/quantum_kernel.py @@ -9,6 +9,7 @@ # 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. +"""Overlap Quantum Kernel""" from typing import Optional, Tuple, Callable, List, Union import numpy as np From fa98e9197e1bad69caab71e9e6b63021b4225954 Mon Sep 17 00:00:00 2001 From: Gian Gentinetta Date: Thu, 30 Jun 2022 17:56:28 +0200 Subject: [PATCH 04/61] more init --- qiskit_machine_learning/primitives/__init__.py | 5 ----- qiskit_machine_learning/primitives/kernels/pseudo_kernel.py | 2 +- qiskit_machine_learning/primitives/kernels/quantum_kernel.py | 2 +- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/qiskit_machine_learning/primitives/__init__.py b/qiskit_machine_learning/primitives/__init__.py index 66253d188..886a6a30c 100644 --- a/qiskit_machine_learning/primitives/__init__.py +++ b/qiskit_machine_learning/primitives/__init__.py @@ -17,8 +17,3 @@ .. currentmodule:: qiskit_machine_learning.primitives """ - -from .kernels import BaseKernel, QuantumKernel, TrainableQuantumKernel, PseudoKernel - - -__all__ = ["BaseKernel", "QuantumKernel", "TrainableQuantumKernel", "PseudoKernel"] diff --git a/qiskit_machine_learning/primitives/kernels/pseudo_kernel.py b/qiskit_machine_learning/primitives/kernels/pseudo_kernel.py index 1b1eff6d2..63b6e4b3d 100644 --- a/qiskit_machine_learning/primitives/kernels/pseudo_kernel.py +++ b/qiskit_machine_learning/primitives/kernels/pseudo_kernel.py @@ -19,7 +19,7 @@ from qiskit.primitives import Sampler from qiskit.primitives.fidelity import BaseFidelity -from qiskit_machine_learning.primitives import QuantumKernel +from qiskit_machine_learning.primitives.kernels import QuantumKernel from qiskit_machine_learning.utils import make_2d SamplerFactory = Callable[[List[QuantumCircuit]], Sampler] diff --git a/qiskit_machine_learning/primitives/kernels/quantum_kernel.py b/qiskit_machine_learning/primitives/kernels/quantum_kernel.py index e7ec20c2e..16d10e3b0 100644 --- a/qiskit_machine_learning/primitives/kernels/quantum_kernel.py +++ b/qiskit_machine_learning/primitives/kernels/quantum_kernel.py @@ -19,7 +19,7 @@ from qiskit.primitives import Sampler from qiskit.primitives.fidelity import BaseFidelity, Fidelity -from qiskit_machine_learning.primitives import BaseKernel +from qiskit_machine_learning.primitives.kernels import BaseKernel SamplerFactory = Callable[[List[QuantumCircuit]], Sampler] From 51063336a45737435e96a631346f20b27c8da0fc Mon Sep 17 00:00:00 2001 From: ElePT Date: Tue, 5 Jul 2022 11:12:15 +0200 Subject: [PATCH 05/61] Add qnns PoC --- .../qnns/neural_networks/sampler_qnn.py | 305 ++++++++++++++++++ .../primitives/qnns/primitives/__init__.py | 58 ++++ .../qnns/primitives/base_estimator.py | 238 ++++++++++++++ .../qnns/primitives/base_sampler.py | 181 +++++++++++ .../primitives/qnns/primitives/estimator.py | 115 +++++++ .../qnns/primitives/estimator_result.py | 44 +++ .../qnns/primitives/factories/__init__.py | 2 + .../primitives/factories/estimator_factory.py | 16 + .../statevector_estimator_factory.py | 13 + .../qnns/primitives/gradient/__init__.py | 14 + .../gradient/base_estimator_gradient.py | 37 +++ .../finite_diff_estimator_gradient.py | 56 ++++ .../gradient/finite_diff_sampler_gradient.py | 55 ++++ .../gradient/lin_comb_estimator_gradient.py | 224 +++++++++++++ .../gradient/lin_comb_sampler_gradient.py | 221 +++++++++++++ .../param_shift_estimator_gradient.py | 139 ++++++++ .../gradient/param_shift_sampler_gradient.py | 134 ++++++++ .../primitives/qnns/primitives/sampler.py | 115 +++++++ .../qnns/primitives/sampler_result.py | 43 +++ .../primitives/qnns/primitives/utils.py | 113 +++++++ .../primitives/qnns/sampler-qnn-example.py | 75 +++++ 21 files changed, 2198 insertions(+) create mode 100644 qiskit_machine_learning/primitives/qnns/neural_networks/sampler_qnn.py create mode 100644 qiskit_machine_learning/primitives/qnns/primitives/__init__.py create mode 100644 qiskit_machine_learning/primitives/qnns/primitives/base_estimator.py create mode 100644 qiskit_machine_learning/primitives/qnns/primitives/base_sampler.py create mode 100644 qiskit_machine_learning/primitives/qnns/primitives/estimator.py create mode 100644 qiskit_machine_learning/primitives/qnns/primitives/estimator_result.py create mode 100644 qiskit_machine_learning/primitives/qnns/primitives/factories/__init__.py create mode 100644 qiskit_machine_learning/primitives/qnns/primitives/factories/estimator_factory.py create mode 100644 qiskit_machine_learning/primitives/qnns/primitives/factories/statevector_estimator_factory.py create mode 100755 qiskit_machine_learning/primitives/qnns/primitives/gradient/__init__.py create mode 100644 qiskit_machine_learning/primitives/qnns/primitives/gradient/base_estimator_gradient.py create mode 100644 qiskit_machine_learning/primitives/qnns/primitives/gradient/finite_diff_estimator_gradient.py create mode 100644 qiskit_machine_learning/primitives/qnns/primitives/gradient/finite_diff_sampler_gradient.py create mode 100644 qiskit_machine_learning/primitives/qnns/primitives/gradient/lin_comb_estimator_gradient.py create mode 100644 qiskit_machine_learning/primitives/qnns/primitives/gradient/lin_comb_sampler_gradient.py create mode 100644 qiskit_machine_learning/primitives/qnns/primitives/gradient/param_shift_estimator_gradient.py create mode 100644 qiskit_machine_learning/primitives/qnns/primitives/gradient/param_shift_sampler_gradient.py create mode 100644 qiskit_machine_learning/primitives/qnns/primitives/sampler.py create mode 100644 qiskit_machine_learning/primitives/qnns/primitives/sampler_result.py create mode 100644 qiskit_machine_learning/primitives/qnns/primitives/utils.py create mode 100644 qiskit_machine_learning/primitives/qnns/sampler-qnn-example.py diff --git a/qiskit_machine_learning/primitives/qnns/neural_networks/sampler_qnn.py b/qiskit_machine_learning/primitives/qnns/neural_networks/sampler_qnn.py new file mode 100644 index 000000000..9d3f3000a --- /dev/null +++ b/qiskit_machine_learning/primitives/qnns/neural_networks/sampler_qnn.py @@ -0,0 +1,305 @@ +import logging +from numbers import Integral +import numpy as np +from typing import Optional, Union, List, Tuple, Callable, cast, Iterable +from qiskit import QuantumCircuit +from qiskit.circuit import Parameter +from qiskit_machine_learning.exceptions import QiskitMachineLearningError, QiskitError + +logger = logging.getLogger(__name__) + +# from primitives.gradient import FiniteDiffEstimatorGradient, FiniteDiffSamplerGradient + +from scipy.sparse import coo_matrix + +from primitives.gradient.param_shift_sampler_gradient import ParamShiftSamplerGradient +from qiskit.primitives import Sampler + +class SamplerQNN(): + + def __init__( + self, + circuit: QuantumCircuit, + input_params: Optional[List[Parameter]] = None, + weight_params: Optional[List[Parameter]] = None, + interpret: Optional[Callable[[int], Union[int, Tuple[int, ...]]]] = None, + output_shape: Union[int, Tuple[int, ...]] = None, + sampler_factory: Callable = None, + gradient_method: str = "param_shift", + + ): + # IGNORING SPARSE + # SKIPPING CUSTOM GRADIENT + # SKIPPING "INPUT GRADIENTS" -> by default with primitives? + + # we allow only one circuit at this moment + self._circuit = circuit + # self._gradient = ParamShiftSamplerGradient(sampler, self._circuit) + + self._gradient_method = gradient_method + self._sampler_factory = sampler_factory + + self._input_params = list(input_params or []) + self._weight_params = list(weight_params or []) + + self.output_shape = None + self._num_inputs = len(self._input_params) + self._num_weights = len(self._weight_params) + self.num_weights = self._num_weights + # the circuit must always have measurements.... (?) + # add measurements in case none are given + if len(self._circuit.clbits) == 0: + self._circuit.measure_all() + + self._interpret = interpret + self._original_interpret = interpret + + # set interpret and compute output shape + self.set_interpret(interpret, output_shape) + + self._input_gradients = None + + # def output_shape(self): + # return self._output_shape + # return self._output_shape + + def __enter__(self): + self.open() + return self + + def __exit__(self, *exc_info): + self.close() + + def open(self): + # we should delay instantiation of the primitives till they are really required + if self._gradient_method == "param_shift": + # if gradient method -> sampler with gradient functionality + self._sampler = ParamShiftSamplerGradient( + circuit = self._circuit, + sampler = self._sampler_factory + ) + else: + # if no gradient method -> sampler without gradient functionality + self._sampler = self._sampler_factory( + circuits = [self._circuit], + parameters = [self._input_params + self._weight_params] + ) + pass + + def close(self): + self._sampler.__exit__() + + def set_interpret( + self, + interpret: Optional[Callable[[int], Union[int, Tuple[int, ...]]]], + output_shape: Union[int, Tuple[int, ...]] = None, + ) -> None: + """Change 'interpret' and corresponding 'output_shape'. If self.sampling==True, the + output _shape does not have to be set and is inferred from the interpret function. + Otherwise, the output_shape needs to be given. + + Args: + interpret: A callable that maps the measured integer to another unsigned integer or + tuple of unsigned integers. See constructor for more details. + output_shape: The output shape of the custom interpretation, only used in the case + where an interpret function is provided and ``sampling==False``. See constructor + for more details. + """ + + # save original values + self._original_output_shape = output_shape + self._original_interpret = interpret + + # derive target values to be used in computations + self._output_shape = self._compute_output_shape(interpret, output_shape) + self._interpret = interpret if interpret is not None else lambda x: x + self.output_shape = self._output_shape + + def _compute_output_shape(self, interpret, output_shape) -> Tuple[int, ...]: + """Validate and compute the output shape.""" + + # this definition is required by mypy + output_shape_: Tuple[int, ...] = (-1,) + # todo: move sampling code to the super class + + if interpret is not None: + if output_shape is None: + raise QiskitMachineLearningError( + "No output shape given, but required in case of custom interpret!" + ) + if isinstance(output_shape, Integral): + output_shape = int(output_shape) + output_shape_ = (output_shape,) + else: + output_shape_ = output_shape + else: + if output_shape is not None: + # Warn user that output_shape parameter will be ignored + logger.warning( + "No interpret function given, output_shape will be automatically " + "determined as 2^num_qubits." + ) + + output_shape_ = (2 ** self._circuit.num_qubits,) + + # # final validation + # output_shape_ = self._validate_output_shape(output_shape_) + + return output_shape_ + + def forward( + self, + input_data: Optional[Union[List[float], np.ndarray, float]], + weights: Optional[Union[List[float], np.ndarray, float]], + ) -> np.ndarray: + + result = self._forward(input_data, weights) + return result + + def _preprocess(self, input_data, weights): + if len(input_data.shape) == 1: + input_data = np.expand_dims(input_data, 0) + num_samples = input_data.shape[0] + # quick fix for 0 inputs + if num_samples == 0: + num_samples = 1 + + parameters = [] + for i in range(num_samples): + param_values = [input_data[i,j] for j, input_param in enumerate(self._input_params)] + param_values += [weights[j] for j, weight_param in enumerate(self._weight_params)] + parameters.append(param_values) + + return parameters, num_samples + + def _postprocess(self, num_samples, result): + + prob = np.zeros((num_samples, *self._output_shape)) + + for i in range(num_samples): + counts = result[i].quasi_dists[0] + print(counts) + shots = sum(counts.values()) + + # evaluate probabilities + for b, v in counts.items(): + key = (i, int(self._interpret(b))) # type: ignore + prob[key] += v / shots + + return prob + + def _forward( + self, input_data: Optional[np.ndarray], weights: Optional[np.ndarray] + ) -> np.ndarray: + + parameter_values, num_samples = self._preprocess(input_data, weights) + + # result = self._sampler([0] * num_samples, parameter_values) + + results = [] + for sample in range(num_samples): + result = self._sampler(parameter_values) + results.append(result) + + result = self._postprocess(num_samples, results) + + return result + + def backward( + self, + input_data: Optional[Union[List[float], np.ndarray, float]], + weights: Optional[Union[List[float], np.ndarray, float]], + ) -> Tuple[Optional[np.ndarray], Optional[np.ndarray],]: + + result = self._backward(input_data, weights) + return result + + def _preprocess_gradient(self, input_data, weights): + + if len(input_data.shape) == 1: + input_data = np.expand_dims(input_data, 0) + + num_samples = input_data.shape[0] + # quick fix for 0 inputs + if num_samples == 0: + num_samples = 1 + + parameters = [] + for i in range(num_samples): + + param_values = [input_data[i, j] for j, input_param in enumerate(self._input_params)] + param_values += [weights[j] for j, weight_param in enumerate(self._weight_params)] + parameters.append(param_values) + + return parameters, num_samples + + def _postprocess_gradient(self, num_samples, results): + + input_grad = np.zeros((num_samples, 1, self._num_inputs)) if self._input_gradients else None + weights_grad = np.zeros((num_samples, *self._output_shape, self._num_weights)) + + if self._input_gradients: + num_grad_vars = self._num_inputs + self._num_weights + else: + num_grad_vars = self._num_weights + + for sample in range(num_samples): + + for i in range(num_grad_vars): + grad = results[sample].quasi_dists[i + self._num_inputs] + for k in grad.keys(): + val = results[sample].quasi_dists[i + self._num_inputs][k] + # get index for input or weights gradients + if self._input_gradients: + grad_index = i if i < self._num_inputs else i - self._num_inputs + else: + grad_index = i + # interpret integer and construct key + key = self._interpret(k) + if isinstance(key, Integral): + key = (sample, int(key), grad_index) + else: + # if key is an array-type, cast to hashable tuple + key = tuple(cast(Iterable[int], key)) + key = (sample, *key, grad_index) + # store value for inputs or weights gradients + if self._input_gradients: + # we compute input gradients first + if i < self._num_inputs: + input_grad[key] += np.real(val) + else: + weights_grad[key] += np.real(val) + else: + weights_grad[key] += np.real(val) + + return input_grad, weights_grad + + def _backward( + self, input_data: Optional[np.ndarray], weights: Optional[np.ndarray] + ) -> Tuple[Optional[np.ndarray], Optional[np.ndarray],]: + + # prepare parameters in the required format + parameter_values, num_samples = self._preprocess_gradient(input_data, weights) + + results = [] + for sample in range(num_samples): + if self._input_gradients: + result = self._sampler.gradient(parameter_values[sample]) + else: + result = self._sampler.gradient(parameter_values[sample], + partial=self._sampler._circuit.parameters[self._num_inputs:]) + + results.append(result) + input_grad, weights_grad = self._postprocess_gradient(num_samples, results) + + return None , weights_grad # `None` for gradients wrt input data, see TorchConnector + + + + + + + + + + diff --git a/qiskit_machine_learning/primitives/qnns/primitives/__init__.py b/qiskit_machine_learning/primitives/qnns/primitives/__init__.py new file mode 100644 index 000000000..bd00dedc7 --- /dev/null +++ b/qiskit_machine_learning/primitives/qnns/primitives/__init__.py @@ -0,0 +1,58 @@ +# 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. + +""" +===================================== +Primitives (:mod:`qiskit.primitives`) +===================================== + +.. currentmodule:: qiskit.primitives + +.. automodule:: qiskit.primitives.base_estimator +.. automodule:: qiskit.primitives.base_sampler + +.. currentmodule:: qiskit.primitives + +Estimator +========= + +.. autosummary:: + :toctree: ../stubs/ + + BaseEstimator + Estimator + +Sampler +======= + +.. autosummary:: + :toctree: ../stubs/ + + BaseSampler + Sampler + +Results +======= + +.. autosummary:: + :toctree: ../stubs/ + + EstimatorResult + SamplerResult +""" + +from .base_estimator import BaseEstimator +from .base_sampler import BaseSampler +from .estimator import Estimator +from .estimator_result import EstimatorResult +from .sampler import Sampler +from .sampler_result import SamplerResult diff --git a/qiskit_machine_learning/primitives/qnns/primitives/base_estimator.py b/qiskit_machine_learning/primitives/qnns/primitives/base_estimator.py new file mode 100644 index 000000000..199a5b503 --- /dev/null +++ b/qiskit_machine_learning/primitives/qnns/primitives/base_estimator.py @@ -0,0 +1,238 @@ +# 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. +r""" + +.. estimator-desc: + +===================== +Overview of Estimator +===================== + +Estimator class estimates expectation values of quantum circuits and observables. + +An estimator object is initialized with multiple quantum circuits and observables +and users can specify pairs of quantum circuits and observables +to estimate the expectation values. + +An estimator is initialized with the following elements. + +* quantum circuits (:math:`\psi_i(\theta)`): list of (parameterized) quantum circuits + (a list of :class:`~qiskit.circuit.QuantumCircuit`)) + +* observables (:math:`H_j`): a list of :class:`~qiskit.quantum_info.SparsePauliOp`. + +The estimator is called with the following inputs. + +* circuit indexes: a list of indexes of the quantum circuits. + +* observable indexes: a list of indexes of the observables. + +* parameters: a list of parameters of the quantum circuits. + (:class:`~qiskit.circuit.parametertable.ParameterView` or + a list of :class:`~qiskit.circuit.Parameter`). + +* parameter values (:math:`\theta_k`): list of sets of values + to be bound to the parameters of the quantum circuits. + (list of list of float) + +The output is an :class:`~qiskit.primitives.EstimatorResult` which contains a list of +expectation values plus optional metadata like confidence intervals for the estimation. + +.. math:: + + \langle\psi_i(\theta_k)|H_j|\psi_i(\theta_k)\rangle + + +The estimator object is expected to be ``close()`` d after use or +accessed inside "with" context +and the objects are called with parameter values and run options +(e.g., ``shots`` or number of shots). + +Here is an example of how estimator is used. + +.. code-block:: python + + from qiskit.circuit.library import RealAmplitudes + from qiskit.quantum_info import SparsePauliOp + + psi1 = RealAmplitudes(num_qubits=2, reps=2) + psi2 = RealAmplitudes(num_qubits=2, reps=3) + + params1 = psi1.parameters + params2 = psi2.parameters + + H1 = SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)]) + H2 = SparsePauliOp.from_list([("IZ", 1)]) + H3 = SparsePauliOp.from_list([("ZI", 1), ("ZZ", 1)]) + + with Estimator([psi1, psi2], [H1, H2, H3], [params1, params2]) as e: + theta1 = [0, 1, 1, 2, 3, 5] + theta2 = [0, 1, 1, 2, 3, 5, 8, 13] + theta3 = [1, 2, 3, 4, 5, 6] + + # calculate [ ] + result = e([0], [0], [theta1]) + print(result) + + # calculate [ , ] + result2 = e([0, 0], [1, 2], [theta1]*2) + print(result2) + + # calculate [ ] + result3 = e([1], [1], [theta2]) + print(result3) + + # calculate [ , ] + result4 = e([0, 0], [0, 0], [theta1, theta3]) + print(result4) + + # calculate [ , + # , + # ] + result5 = e([0, 1, 0], [0, 1, 2], [theta1, theta2, theta3]) + print(result5) +""" +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Iterable, Sequence + +from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.circuit.parametertable import ParameterView +from qiskit.exceptions import QiskitError +from qiskit.quantum_info.operators import SparsePauliOp + +from .estimator_result import EstimatorResult + + +class BaseEstimator(ABC): + """Estimator base class. + + Base class for Estimator that estimates expectation values of quantum circuits and observables. + """ + + def __init__( + self, + circuits: Iterable[QuantumCircuit], + observables: Iterable[SparsePauliOp], + parameters: Iterable[Iterable[Parameter]] | None = None, + ): + """ + Creating an instance of an Estimator, or using one in a ``with`` context opens a session that + holds resources until the instance is ``close()`` ed or the context is exited. + + Args: + circuits: Quantum circuits that represent quantum states. + observables: Observables. + parameters: Parameters of quantum circuits, specifying the order in which values + will be bound. Defaults to ``[circ.parameters for circ in circuits]`` + The indexing is such that ``parameters[i, j]`` is the j-th formal parameter of + ``circuits[i]``. + + Raises: + QiskitError: For mismatch of circuits and parameters list. + """ + self._circuits = tuple(circuits) + self._observables = tuple(observables) + if parameters is None: + self._parameters = tuple(circ.parameters for circ in self._circuits) + else: + self._parameters = tuple(ParameterView(par) for par in parameters) + if len(self._parameters) != len(self._circuits): + raise QiskitError( + f"Different number of parameters ({len(self._parameters)} and " + f"circuits ({len(self._circuits)}" + ) + for i, (circ, params) in enumerate(zip(self._circuits, self._parameters)): + if circ.num_parameters != len(params): + raise QiskitError( + f"Different numbers of parameters of {i}-th circuit: " + f"expected {circ.num_parameters}, actual {len(params)}." + ) + + def __enter__(self): + return self + + def __exit__(self, *exc_info): + self.close() + + @abstractmethod + def close(self): + """Close the session and free resources""" + ... + + @property + def circuits(self) -> tuple[QuantumCircuit, ...]: + """Quantum circuits that represents quantum states. + + Returns: + The quantum circuits. + """ + return self._circuits + + @property + def observables(self) -> tuple[SparsePauliOp, ...]: + """Observables to be estimated. + + Returns: + The observables. + """ + return self._observables + + @property + def parameters(self) -> tuple[ParameterView, ...]: + """Parameters of the quantum circuits. + + Returns: + Parameters, where ``parameters[i][j]`` is the j-th parameter of the i-th circuit. + """ + return self._parameters + + @abstractmethod + def __call__( + self, + circuit_indices: Sequence[int], + observable_indices: Sequence[int], + parameter_values: Sequence[Sequence[float]], + **run_options, + ) -> EstimatorResult: + """Run the estimation of expectation value(s). + + ``circuit_indices``, ``observable_indices``, and ``parameter_values`` should have the same + length. The i-th element of the result is the expectation of observable + + .. code-block:: python + + obs = self.observables[observable_indices[i]] + + for the state prepared by + + .. code-block:: python + + circ = self.circuits[circuit_indices[i]] + + with bound parameters + + .. code-block:: python + + values = parameter_values[i]. + + Args: + circuit_indices: the list of circuit indices. + observable_indices: the list of observable indices. + parameter_values: concrete parameters to be bound. + run_options: runtime options used for circuit execution. + + Returns: + EstimatorResult: The result of the estimator. + """ + ... diff --git a/qiskit_machine_learning/primitives/qnns/primitives/base_sampler.py b/qiskit_machine_learning/primitives/qnns/primitives/base_sampler.py new file mode 100644 index 000000000..28b5b69d6 --- /dev/null +++ b/qiskit_machine_learning/primitives/qnns/primitives/base_sampler.py @@ -0,0 +1,181 @@ +# 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. +r""" +=================== +Overview of Sampler +=================== + +Sampler class calculates probabilities or quasi-probabilities of bitstrings from quantum circuits. + +A sampler is initialized with the following elements. + +* quantum circuits (:math:`\psi_i(\theta)`): list of (parameterized) quantum circuits. + (a list of :class:`~qiskit.circuit.QuantumCircuit`)) + +* parameters: a list of parameters of the quantum circuits. + (:class:`~qiskit.circuit.parametertable.ParameterView` or + a list of :class:`~qiskit.circuit.Parameter`). + +The sampler is run with the following inputs. + +* circuit indexes: a list of indices of the circuits to evaluate. + +* parameter values (:math:`\theta_k`): list of sets of parameter values + to be bound to the parameters of the quantum circuits. + (list of list of float) + +The output is a :class:`~qiskit.primitives.SamplerResult` which contains probabilities +or quasi-probabilities of bitstrings, +plus optional metadata like error bars in the samples. + +The sampler object is expected to be closed after use or +accessed within "with" context +and the objects are called with parameter values and run options +(e.g., ``shots`` or number of shots). + +Here is an example of how sampler is used. + +.. code-block:: python + + from qiskit import QuantumCircuit + from qiskit.circuit.library import RealAmplitudes + + bell = QuantumCircuit(2) + bell.h(0) + bell.cx(0, 1) + bell.measure_all() + + # executes a Bell circuit + with Sampler(circuits=[bell], parameters=[[]]) as sampler: + result = sampler(parameters=[[]], circuits=[0]) + print([q.binary_probabilities() for q in result.quasi_dists]) + + # executes three Bell circuits + with Sampler([bell]*3, [[]] * 3) as sampler: + result = sampler([0, 1, 2], [[]]*3) + print([q.binary_probabilities() for q in result.quasi_dists]) + + # parameterized circuit + pqc = RealAmplitudes(num_qubits=2, reps=2) + pqc.measure_all() + pqc2 = RealAmplitudes(num_qubits=2, reps=3) + pqc2.measure_all() + + theta1 = [0, 1, 1, 2, 3, 5] + theta2 = [1, 2, 3, 4, 5, 6] + theta3 = [0, 1, 2, 3, 4, 5, 6, 7] + + with Sampler(circuits=[pqc, pqc2], parameters=[pqc.parameters, pqc2.parameters]) as sampler: + result = sampler([0, 0, 1], [theta1, theta2, theta3]) + + # result of pqc(theta1) + print(result.quasi_dists[0].binary_probabilities()) + + # result of pqc(theta2) + print(result.quasi_dists[1].binary_probabilities()) + + # result of pqc2(theta3) + print(result.quasi_dists[2].binary_probabilities()) + +""" +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Iterable, Sequence + +from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.circuit.parametertable import ParameterView +from qiskit.exceptions import QiskitError + +from .sampler_result import SamplerResult + + +class BaseSampler(ABC): + """Sampler base class + + Base class of Sampler that calculates quasi-probabilities of bitstrings from quantum circuits. + """ + + def __init__( + self, + circuits: Iterable[QuantumCircuit], + parameters: Iterable[Iterable[Parameter]] | None = None, + ): + """ + Args: + circuits: Quantum circuits to be executed. + parameters: Parameters of each of the quantum circuits. + Defaults to ``[circ.parameters for circ in circuits]``. + + Raises: + QiskitError: For mismatch of circuits and parameters list. + """ + self._circuits = tuple(circuits) + if parameters is None: + self._parameters = tuple(circ.parameters for circ in self._circuits) + else: + self._parameters = tuple(ParameterView(par) for par in parameters) + if len(self._parameters) != len(self._circuits): + raise QiskitError( + f"Different number of parameters ({len(self._parameters)} " + f"and circuits ({len(self._circuits)}" + ) + + def __enter__(self): + return self + + def __exit__(self, *exc_info): + self.close() + + @abstractmethod + def close(self): + """Close the session and free resources""" + ... + + @property + def circuits(self) -> tuple[QuantumCircuit, ...]: + """Quantum circuits to be sampled. + + Returns: + The quantum circuits to be sampled. + """ + return self._circuits + + @property + def parameters(self) -> tuple[ParameterView, ...]: + """Parameters of quantum circuits. + + Returns: + List of the parameters in each quantum circuit. + """ + return self._parameters + + @abstractmethod + def __call__( + self, + circuit_indices: Sequence[int], + parameter_values: Sequence[Sequence[float]], + **run_options, + ) -> SamplerResult: + """Run the sampling of bitstrings. + + Args: + circuit_indices: Indices of the circuits to evaluate. + parameter_values: Parameters to be bound to the circuit. + run_options: Backend runtime options used for circuit execution. + + Returns: + The result of the sampler. The i-th result corresponds to + ``self.circuits[circuit_indices[i]]`` evaluated with parameters bound as + ``parameter_values[i]``. + """ + ... diff --git a/qiskit_machine_learning/primitives/qnns/primitives/estimator.py b/qiskit_machine_learning/primitives/qnns/primitives/estimator.py new file mode 100644 index 000000000..6e9a08c86 --- /dev/null +++ b/qiskit_machine_learning/primitives/qnns/primitives/estimator.py @@ -0,0 +1,115 @@ +# 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. +""" +Estimator class +""" + +from __future__ import annotations + +from collections.abc import Iterable, Sequence +from typing import cast + +import numpy as np + +from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.exceptions import QiskitError +from qiskit.opflow import PauliSumOp +from qiskit.quantum_info import Statevector +from qiskit.quantum_info.operators.base_operator import BaseOperator + +from .base_estimator import BaseEstimator +from .estimator_result import EstimatorResult +from .utils import init_circuit, init_observable + + +class Estimator(BaseEstimator): + """ + Estimator class + """ + + def __init__( + self, + circuits: QuantumCircuit | Iterable[QuantumCircuit], + observables: BaseOperator | PauliSumOp | Iterable[BaseOperator | PauliSumOp], + parameters: Iterable[Iterable[Parameter]] | None = None, + ): + if isinstance(circuits, QuantumCircuit): + circuits = [circuits] + circuits = [init_circuit(circuit) for circuit in circuits] + + if isinstance(observables, (PauliSumOp, BaseOperator)): + observables = [observables] + observables = [init_observable(observable) for observable in observables] + + super().__init__( + circuits=circuits, + observables=observables, + parameters=parameters, + ) + self._is_closed = False + + def __call__( + self, + circuit_indices: Sequence[int] | None = None, + observable_indices: Sequence[int] | None = None, + parameter_values: Sequence[Sequence[float]] | Sequence[float] | None = None, + **run_options, + ) -> EstimatorResult: + if self._is_closed: + raise QiskitError("The primitive has been closed.") + + if isinstance(parameter_values, np.ndarray): + parameter_values = parameter_values.tolist() + if parameter_values and not isinstance(parameter_values[0], (np.ndarray, Sequence)): + parameter_values = cast("Sequence[float]", parameter_values) + parameter_values = [parameter_values] + if circuit_indices is None: + circuit_indices = list(range(len(self._circuits))) + if observable_indices is None: + observable_indices = list(range(len(self._observables))) + if parameter_values is None: + parameter_values = [[]] * len(circuit_indices) + if len(circuit_indices) != len(observable_indices): + raise QiskitError( + f"The number of circuit indices ({len(circuit_indices)}) does not match " + f"the number of observable indices ({len(observable_indices)})." + ) + if len(circuit_indices) != len(parameter_values): + raise QiskitError( + f"The number of circuit indices ({len(circuit_indices)}) does not match " + f"the number of parameter value sets ({len(parameter_values)})." + ) + + bound_circuits = [] + for i, value in zip(circuit_indices, parameter_values): + if len(value) != len(self._parameters[i]): + raise QiskitError( + f"The number of values ({len(value)}) does not match " + f"the number of parameters ({len(self._parameters[i])})." + ) + bound_circuits.append( + self._circuits[i].bind_parameters(dict(zip(self._parameters[i], value))) + ) + sorted_observables = [self._observables[i] for i in observable_indices] + expectation_values = [] + for circ, obs in zip(bound_circuits, sorted_observables): + if circ.num_qubits != obs.num_qubits: + raise QiskitError( + f"The number of qubits of a circuit ({circ.num_qubits}) does not match " + f"the number of qubits of a observable ({obs.num_qubits})." + ) + expectation_values.append(Statevector(circ).expectation_value(obs)) + + return EstimatorResult(np.real_if_close(expectation_values), [{}] * len(expectation_values)) + + def close(self): + self._is_closed = True diff --git a/qiskit_machine_learning/primitives/qnns/primitives/estimator_result.py b/qiskit_machine_learning/primitives/qnns/primitives/estimator_result.py new file mode 100644 index 000000000..e26d9848c --- /dev/null +++ b/qiskit_machine_learning/primitives/qnns/primitives/estimator_result.py @@ -0,0 +1,44 @@ +# 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. +""" +Estimator result class +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + import numpy as np + + +@dataclass(frozen=True) +class EstimatorResult: + """Result of Estimator. + + .. code-block:: python + + result = estimator(circuits, observables, params) + + where the i-th elements of ``result`` correspond to the circuit and observable given by + ``circuit_indices[i]``, ``observable_indices[i]``, and the parameter values bounds by ``params[i]``. + For example, ``results.values[i]`` gives the expectation value, and ``result.metadata[i]`` + is a metadata dictionary for this circuit and parameters. + + Args: + values (np.ndarray): The array of the expectation values. + metadata (list[dict]): List of the metadata. + """ + + values: "np.ndarray[Any, np.dtype[np.float64]]" + metadata: list[dict[str, Any]] diff --git a/qiskit_machine_learning/primitives/qnns/primitives/factories/__init__.py b/qiskit_machine_learning/primitives/qnns/primitives/factories/__init__.py new file mode 100644 index 000000000..d0ac2ee93 --- /dev/null +++ b/qiskit_machine_learning/primitives/qnns/primitives/factories/__init__.py @@ -0,0 +1,2 @@ +from .estimator_factory import EstimatorFactory +from .statevector_estimator_factory import StatevectorEstimatorFactory diff --git a/qiskit_machine_learning/primitives/qnns/primitives/factories/estimator_factory.py b/qiskit_machine_learning/primitives/qnns/primitives/factories/estimator_factory.py new file mode 100644 index 000000000..6be57a319 --- /dev/null +++ b/qiskit_machine_learning/primitives/qnns/primitives/factories/estimator_factory.py @@ -0,0 +1,16 @@ +from abc import ABC, abstractmethod + +from qiskit.primitives import BaseEstimator + +class EstimatorFactory(ABC): + """Class to construct an estimator, given circuits and observables.""" + + def __init__(self, circuits=None, observables=None, parameters=None): + self.circuits = circuits + self.observables = observables + self.parameters = parameters + + @abstractmethod + def __call__(self, circuits=None, observables=None, parameters=None) -> BaseEstimator: + pass + diff --git a/qiskit_machine_learning/primitives/qnns/primitives/factories/statevector_estimator_factory.py b/qiskit_machine_learning/primitives/qnns/primitives/factories/statevector_estimator_factory.py new file mode 100644 index 000000000..3c9b95c8a --- /dev/null +++ b/qiskit_machine_learning/primitives/qnns/primitives/factories/statevector_estimator_factory.py @@ -0,0 +1,13 @@ +from qiskit.primitives import Estimator +from .estimator_factory import EstimatorFactory + +class StatevectorEstimatorFactory(EstimatorFactory): + """Estimator factory evaluated with statevector simulations.""" + + def __call__(self, circuits=None, observables=None, parameters=None) -> Estimator: + circuits = circuits or self.circuits + observables = observables or self.observables + parameters = parameters or self.parameters + + return Estimator(circuits, observables, parameters) + diff --git a/qiskit_machine_learning/primitives/qnns/primitives/gradient/__init__.py b/qiskit_machine_learning/primitives/qnns/primitives/gradient/__init__.py new file mode 100755 index 000000000..687a25aae --- /dev/null +++ b/qiskit_machine_learning/primitives/qnns/primitives/gradient/__init__.py @@ -0,0 +1,14 @@ +# 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. + +from .finite_diff_estimator_gradient import FiniteDiffEstimatorGradient +from .finite_diff_sampler_gradient import FiniteDiffSamplerGradient diff --git a/qiskit_machine_learning/primitives/qnns/primitives/gradient/base_estimator_gradient.py b/qiskit_machine_learning/primitives/qnns/primitives/gradient/base_estimator_gradient.py new file mode 100644 index 000000000..f38f4fa0a --- /dev/null +++ b/qiskit_machine_learning/primitives/qnns/primitives/gradient/base_estimator_gradient.py @@ -0,0 +1,37 @@ +# 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. + +""" +Abstract Base class of Gradient. +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Sequence + +from ..base_estimator import BaseEstimator + + +class BaseEstimatorGradient(ABC): + def __init__(self, estimator: BaseEstimator): + self._estimator = estimator + + @abstractmethod + def gradient( + self, + circuit_indices: Sequence[int], + observable_indices: Sequence[int], + parameter_values: Sequence[Sequence[float]], + **run_options, + ) -> EstimatorResult: + ... diff --git a/qiskit_machine_learning/primitives/qnns/primitives/gradient/finite_diff_estimator_gradient.py b/qiskit_machine_learning/primitives/qnns/primitives/gradient/finite_diff_estimator_gradient.py new file mode 100644 index 000000000..ab61934b0 --- /dev/null +++ b/qiskit_machine_learning/primitives/qnns/primitives/gradient/finite_diff_estimator_gradient.py @@ -0,0 +1,56 @@ +# 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. + +from __future__ import annotations + +from collections.abc import Sequence + +import numpy as np + +from ..base_estimator import BaseEstimator +from ..estimator_result import EstimatorResult +from .base_estimator_gradient import BaseEstimatorGradient + + +class FiniteDiffEstimatorGradient(BaseEstimatorGradient): + def __init__(self, estimator: BaseEstimator, epsilon: float = 1e-6): + self._epsilon = epsilon + super().__init__(estimator) + + def gradient( + self, + circuit_index: int, + observable_index: int, + parameter_value: Sequence[float], + **run_options, + ) -> EstimatorResult: + run_options = run_options.copy() + + dim = len(parameter_value) + ret = [parameter_value] + for i in range(dim): + ei = parameter_value.copy() + ei[i] += self._epsilon + ret.append(ei) + param_array = np.array(ret).tolist() + circuit_indices = [circuit_index] * (dim + 1) + observable_indices = [observable_index] * (dim + 1) + results = self._estimator.__call__( + circuit_indices, observable_indices, param_array, **run_options + ) + + values = results.values + grad = np.zeros(dim) + f_ref = values[0] + for i, f_i in enumerate(values[1:]): + grad[i] = (f_i - f_ref) / self._epsilon + return EstimatorResult(values=grad, metadata=[{}] * len(grad)) diff --git a/qiskit_machine_learning/primitives/qnns/primitives/gradient/finite_diff_sampler_gradient.py b/qiskit_machine_learning/primitives/qnns/primitives/gradient/finite_diff_sampler_gradient.py new file mode 100644 index 000000000..c40131e40 --- /dev/null +++ b/qiskit_machine_learning/primitives/qnns/primitives/gradient/finite_diff_sampler_gradient.py @@ -0,0 +1,55 @@ +# 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. + +from __future__ import annotations + +from collections.abc import Sequence + +import numpy as np + +from qiskit.result import QuasiDistribution + +from ..base_sampler import BaseSampler +from ..sampler_result import SamplerResult + + +class FiniteDiffSamplerGradient: + def __init__(self, sampler: BaseSampler, epsilon: float = 1e-6): + self._epsilon = epsilon + self._sampler = sampler + + def gradient( + self, + circuit_index: int, + parameter_value: Sequence[float], + **run_options, + ) -> SamplerResult: + run_options = run_options.copy() + + dim = len(parameter_value) + params = [parameter_value] + for i in range(dim): + ei = parameter_value.copy() + ei[i] += self._epsilon + params.append(ei) + param_list = np.array(params).tolist() + circuit_indices = [circuit_index] * (dim + 1) + results = self._sampler.__call__(circuit_indices, param_list, **run_options) + + quasi_dists = results.quasi_dists + ret = [] + f_ref = quasi_dists[0] + for f_i in quasi_dists[1:]: + ret.append( + QuasiDistribution({key: (f_i[key] - f_ref[key]) / self._epsilon for key in f_ref}) + ) + return SamplerResult(quasi_dists=ret, metadata=[{}] * len(ret)) diff --git a/qiskit_machine_learning/primitives/qnns/primitives/gradient/lin_comb_estimator_gradient.py b/qiskit_machine_learning/primitives/qnns/primitives/gradient/lin_comb_estimator_gradient.py new file mode 100644 index 000000000..40d5604f1 --- /dev/null +++ b/qiskit_machine_learning/primitives/qnns/primitives/gradient/lin_comb_estimator_gradient.py @@ -0,0 +1,224 @@ +# 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. +""" +Gradient of expectation values with linear combination of unitaries (LCU) +""" + +from __future__ import annotations + +from collections import defaultdict +from dataclasses import dataclass +from typing import Sequence, Type + +import numpy as np + +from qiskit import transpile +from qiskit.circuit import ( + Gate, + Instruction, + Parameter, + ParameterExpression, + QuantumCircuit, + QuantumRegister, +) +from qiskit.circuit.library.standard_gates import ( + CXGate, + CYGate, + CZGate, + RXGate, + RXXGate, + RYGate, + RYYGate, + RZGate, + RZXGate, + RZZGate, +) +from qiskit.quantum_info import Pauli, SparsePauliOp + +from ..base_estimator import BaseEstimator +from ..estimator_result import EstimatorResult +from .base_estimator_gradient import BaseEstimatorGradient + +Pauli_Z = Pauli("Z") + + +@dataclass +class SubEstimator: + coeff: float | ParameterExpression + circuit: QuantumCircuit + index: int + + +class LinCombEstimatorGradient(BaseEstimatorGradient): + """LCU estimator gradient""" + + SUPPORTED_GATES = [ + "rx", + "ry", + "rz", + "rzx", + "rzz", + "ryy", + "rxx", + "cx", + "cy", + "cz", + "ccx", + "swap", + "iswap", + "h", + "t", + "s", + "sdg", + "x", + "y", + "z", + ] + + def __init__( + self, estimator: Type[BaseEstimator], circuit: QuantumCircuit, observable: SparsePauliOp + ): + self._circuit = circuit + self._observable = observable + self._grad, observable = self._preprocessing() + circuits = [self._circuit] + observables = [self._observable, observable] + for param, lst in self._grad.items(): + for arg in lst: + circuits.append(arg.circuit) + super().__init__(estimator(circuits=circuits, observables=observables)) + + def __enter__(self): + return self + + def __exit__(self, *exc_info): + pass + + @classmethod + def _gradient_circuits(cls, circuit: QuantumCircuit): + circuit2 = transpile(circuit, basis_gates=cls.SUPPORTED_GATES, optimization_level=0) + qr_superpos = QuantumRegister(1, "superpos") + circuit2.add_register(qr_superpos) + circuit2.h(qr_superpos) + circuit2.data.insert(0, circuit2.data.pop()) + circuit2.sdg(qr_superpos) + circuit2.data.insert(1, circuit2.data.pop()) + ret = defaultdict(list) + for i, (inst, qregs, _) in enumerate(circuit2.data): + if inst.is_parameterized(): + param = inst.params[0] + for p in param.parameters: + gate = cls._gate_gradient(inst) + circuit3 = circuit2.copy() + # insert `gate` to i-th position + circuit3.append(gate, [qr_superpos[0]] + qregs, []) + circuit3.data.insert(i, circuit3.data.pop()) + # + circuit3.h(qr_superpos) + ret[p].append((circuit3, param.gradient(p))) + return ret + + def _preprocessing(self): + grad = self._gradient_circuits(self._circuit) + ret = {} + observable = self._observable.expand(Pauli_Z) + index = 1 + for param in self._circuit.parameters: + lst = [] + for circ, coeff in grad[param]: + lst.append(SubEstimator(coeff=coeff, circuit=circ, index=index)) + index += 1 + ret[param] = lst + return ret, observable + + def __call__( + self, parameter_values: Sequence[Sequence[float]], **run_options + ) -> EstimatorResult: + return self._estimator([0], [0], parameter_values, **run_options) + + def gradient( + self, + parameter_value: Sequence[float], + partial: Sequence[Parameter] | None = None, + **run_options, + ) -> EstimatorResult: + parameters = partial or self._circuit.parameters + + param_map = {} + for j, param in enumerate(self._circuit.parameters): + param_map[param] = parameter_value[j] + + circ_indices = [] + for param in parameters: + circ_indices.extend([f.index for f in self._grad[param]]) + size = len(circ_indices) + results = self._estimator(circ_indices, [1] * size, [parameter_value] * size, **run_options) + + param_set = set(parameters) + values = np.zeros(len(parameter_value)) + metadata = [{} for _ in range(len(parameters))] + i = 0 + for j, (param, lst) in enumerate(self._grad.items()): + if param not in param_set: + continue + for subest in lst: + coeff = subest.coeff + if isinstance(coeff, ParameterExpression): + local_map = {param: param_map[param] for param in coeff.parameters} + bound_coeff = coeff.bind(local_map) + else: + bound_coeff = coeff + values[j] += bound_coeff * results.values[i] + i += 1 + + return EstimatorResult(values=values, metadata=metadata) + + @staticmethod + def _gate_gradient(gate: Gate) -> Instruction: + if isinstance(gate, RXGate): + # theta + return CXGate() + if isinstance(gate, RYGate): + # theta + return CYGate() + if isinstance(gate, RZGate): + # theta + return CZGate() + if isinstance(gate, RXXGate): + # theta + cxx_circ = QuantumCircuit(3) + cxx_circ.cx(0, 1) + cxx_circ.cx(0, 2) + cxx = cxx_circ.to_instruction() + return cxx + if isinstance(gate, RYYGate): + # theta + cyy_circ = QuantumCircuit(3) + cyy_circ.cy(0, 1) + cyy_circ.cy(0, 2) + cyy = cyy_circ.to_instruction() + return cyy + if isinstance(gate, RZZGate): + # theta + czz_circ = QuantumCircuit(3) + czz_circ.cz(0, 1) + czz_circ.cz(0, 2) + czz = czz_circ.to_instruction() + return czz + if isinstance(gate, RZXGate): + # theta + czx_circ = QuantumCircuit(3) + czx_circ.cx(0, 2) + czx_circ.cz(0, 1) + czx = czx_circ.to_instruction() + return czx + raise TypeError(f"Unrecognized parameterized gate, {gate}") diff --git a/qiskit_machine_learning/primitives/qnns/primitives/gradient/lin_comb_sampler_gradient.py b/qiskit_machine_learning/primitives/qnns/primitives/gradient/lin_comb_sampler_gradient.py new file mode 100644 index 000000000..0c7f3409f --- /dev/null +++ b/qiskit_machine_learning/primitives/qnns/primitives/gradient/lin_comb_sampler_gradient.py @@ -0,0 +1,221 @@ +# 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. +""" +Gradient of probabilities with linear combination of unitaries (LCU) +""" + +from __future__ import annotations + +from collections import Counter, defaultdict +from dataclasses import dataclass +from typing import Sequence, Type + +from qiskit import transpile +from qiskit.circuit import ( + ClassicalRegister, + Gate, + Instruction, + Parameter, + ParameterExpression, + QuantumCircuit, + QuantumRegister, +) +from qiskit.circuit.library.standard_gates import ( + CXGate, + CYGate, + CZGate, + RXGate, + RXXGate, + RYGate, + RYYGate, + RZGate, + RZXGate, + RZZGate, +) +from qiskit.result import QuasiDistribution + +from ..base_sampler import BaseSampler +from ..sampler_result import SamplerResult + + +@dataclass +class SubEstimator: + coeff: float | ParameterExpression + circuit: QuantumCircuit + index: int + + +class LinCombSamplerGradient: + """LCU sampler gradient""" + + SUPPORTED_GATES = [ + "rx", + "ry", + "rz", + "rzx", + "rzz", + "ryy", + "rxx", + "cx", + "cy", + "cz", + "ccx", + "swap", + "iswap", + "h", + "t", + "s", + "sdg", + "x", + "y", + "z", + ] + + def __init__(self, sampler: Type[BaseSampler], circuit: QuantumCircuit): + self._circuit = circuit + self._grad = self._preprocessing() + circuits = [self._circuit] + for param, lst in self._grad.items(): + for arg in lst: + circuits.append(arg.circuit) + self._sampler = sampler(circuits=circuits) + + def __enter__(self): + return self + + def __exit__(self, *exc_info): + pass + + @classmethod + def _gradient_circuits(cls, circuit: QuantumCircuit): + circuit2 = transpile(circuit, basis_gates=cls.SUPPORTED_GATES, optimization_level=0) + qr_superpos = QuantumRegister(1, "superpos") + cr_superpos = ClassicalRegister(1, "superpos") + circuit2.add_register(qr_superpos) + circuit2.add_bits(cr_superpos) + circuit2.h(qr_superpos) + circuit2.data.insert(0, circuit2.data.pop()) + circuit2.sdg(qr_superpos) + circuit2.data.insert(1, circuit2.data.pop()) + ret = defaultdict(list) + for i, (inst, qregs, _) in enumerate(circuit2.data): + if inst.is_parameterized(): + param = inst.params[0] + for p in param.parameters: + gate = cls._gate_gradient(inst) + circuit3 = circuit2.copy() + # insert `gate` to i-th position + circuit3.append(gate, [qr_superpos[0]] + qregs, []) + circuit3.data.insert(i, circuit3.data.pop()) + # + circuit3.h(qr_superpos) + circuit3.measure(qr_superpos, cr_superpos) + ret[p].append((circuit3, param.gradient(p))) + return ret + + def _preprocessing(self): + grad = self._gradient_circuits(self._circuit) + ret = {} + index = 1 + for param in self._circuit.parameters: + lst = [] + for circ, coeff in grad[param]: + lst.append(SubEstimator(coeff=coeff, circuit=circ, index=index)) + index += 1 + ret[param] = lst + return ret + + def __call__(self, parameter_values: Sequence[Sequence[float]], **run_options) -> SamplerResult: + return self._sampler([0], parameter_values, **run_options) + + def gradient( + self, + parameter_value: Sequence[float], + partial: Sequence[Parameter] | None = None, + **run_options, + ) -> SamplerResult: + parameters = partial or self._circuit.parameters + + param_map = {} + for j, param in enumerate(self._circuit.parameters): + param_map[param] = parameter_value[j] + + circ_indices = [] + for param in parameters: + circ_indices.extend([f.index for f in self._grad[param]]) + size = len(circ_indices) + results = self._sampler(circ_indices, [parameter_value] * size, **run_options) + + param_set = set(parameters) + dists = [Counter() for _ in range(len(parameter_value))] + metadata = [{} for _ in range(len(parameters))] + num_bitstrings = 2**self._circuit.num_qubits + i = 0 + for j, (param, lst) in enumerate(self._grad.items()): + if param not in param_set: + continue + for subest in lst: + coeff = subest.coeff + if isinstance(coeff, ParameterExpression): + local_map = {param: param_map[param] for param in coeff.parameters} + bound_coeff = float(coeff.bind(local_map)) + else: + bound_coeff = coeff + for k, v in results.quasi_dists[i].items(): + sign, k2 = divmod(k, num_bitstrings) + dists[j][k2] += (-1) ** sign * bound_coeff * v + i += 1 + + return SamplerResult( + quasi_dists=[QuasiDistribution(dist) for dist in dists], metadata=metadata + ) + + @staticmethod + def _gate_gradient(gate: Gate) -> Instruction: + if isinstance(gate, RXGate): + # theta + return CXGate() + if isinstance(gate, RYGate): + # theta + return CYGate() + if isinstance(gate, RZGate): + # theta + return CZGate() + if isinstance(gate, RXXGate): + # theta + cxx_circ = QuantumCircuit(3) + cxx_circ.cx(0, 1) + cxx_circ.cx(0, 2) + cxx = cxx_circ.to_instruction() + return cxx + if isinstance(gate, RYYGate): + # theta + cyy_circ = QuantumCircuit(3) + cyy_circ.cy(0, 1) + cyy_circ.cy(0, 2) + cyy = cyy_circ.to_instruction() + return cyy + if isinstance(gate, RZZGate): + # theta + czz_circ = QuantumCircuit(3) + czz_circ.cz(0, 1) + czz_circ.cz(0, 2) + czz = czz_circ.to_instruction() + return czz + if isinstance(gate, RZXGate): + # theta + czx_circ = QuantumCircuit(3) + czx_circ.cx(0, 2) + czx_circ.cz(0, 1) + czx = czx_circ.to_instruction() + return czx + raise TypeError(f"Unrecognized parameterized gate, {gate}") diff --git a/qiskit_machine_learning/primitives/qnns/primitives/gradient/param_shift_estimator_gradient.py b/qiskit_machine_learning/primitives/qnns/primitives/gradient/param_shift_estimator_gradient.py new file mode 100644 index 000000000..b112e85ff --- /dev/null +++ b/qiskit_machine_learning/primitives/qnns/primitives/gradient/param_shift_estimator_gradient.py @@ -0,0 +1,139 @@ +# 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. +""" +Gradient of expectation values with parameter shift +""" + +from __future__ import annotations + +from collections import defaultdict +from dataclasses import dataclass +from typing import Sequence, Type + +import numpy as np + +from qiskit import transpile +from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit +from qiskit.quantum_info import SparsePauliOp + +from ..base_estimator import BaseEstimator +from ..estimator_result import EstimatorResult +from .base_estimator_gradient import BaseEstimatorGradient + + +@dataclass +class SubEstimator: + coeff: float | ParameterExpression + circuit: QuantumCircuit + observable: SparsePauliOp + index: int + + +class ParamShiftEstimatorGradient(BaseEstimatorGradient): + """Parameter shift estimator gradient""" + + SUPPORTED_GATES = ["x", "y", "z", "h", "rx", "ry", "rz", "p", "cx", "cy", "cz"] + + def __init__( + self, estimator: Type[BaseEstimator], circuit: QuantumCircuit, observable: SparsePauliOp + ): + self._circuit = circuit + self._observable = observable + self._grad = self._preprocessing() + circuits = [self._circuit] + observables = [self._observable] + for param, lst in self._grad.items(): + for arg in lst: + circuits.append(arg.circuit) + super().__init__(estimator(circuits=circuits, observables=observables)) + + def __enter__(self): + return self + + def __exit__(self, *exc_info): + pass + + @classmethod + def _gradient_circuits(cls, circuit: QuantumCircuit): + circuit2 = transpile(circuit, basis_gates=cls.SUPPORTED_GATES, optimization_level=0) + ret = defaultdict(list) + for inst in circuit2.data: + if inst[0].is_parameterized(): + param = inst[0].params[0] + for p in param.parameters: + # TODO: Need to wait for an appropriate way to update parameters of + # a particular instruction. + # See https://github.com/Qiskit/qiskit-terra/issues/7894 + inst[0].params[0] = param + np.pi / 2 + ret[p].append((circuit2.copy(), param.gradient(p) / 2)) + inst[0].params[0] = param - np.pi / 2 + ret[p].append((circuit2.copy(), -param.gradient(p) / 2)) + inst[0].params[0] = param + return ret + + def _preprocessing(self): + grad = self._gradient_circuits(self._circuit) + ret = {} + index = 1 + for param in self._circuit.parameters: + lst = [] + for circ, coeff in grad[param]: + lst.append( + SubEstimator( + coeff=coeff, circuit=circ, observable=self._observable, index=index + ) + ) + index += 1 + ret[param] = lst + return ret + + def __call__( + self, parameter_values: Sequence[Sequence[float]], **run_options + ) -> EstimatorResult: + return self._estimator([0], [0], parameter_values, **run_options) + + def gradient( + self, + parameter_value: Sequence[float], + partial: Sequence[Parameter] | None = None, + **run_options, + ) -> EstimatorResult: + parameters = partial or self._circuit.parameters + + param_map = {} + for j, param in enumerate(self._circuit.parameters): + param_map[param] = parameter_value[j] + + circ_indices = [] + for param in parameters: + circ_indices.extend([f.index for f in self._grad[param]]) + size = len(circ_indices) + results = self._estimator(circ_indices, [0] * size, [parameter_value] * size, **run_options) + + param_set = set(parameters) + values = np.zeros(len(parameter_value)) + metadata = [{} for _ in range(len(parameters))] + i = 0 + for j, (param, lst) in enumerate(self._grad.items()): + if param not in param_set: + continue + for subest in lst: + coeff = subest.coeff + if isinstance(coeff, ParameterExpression): + local_map = {param: param_map[param] for param in coeff.parameters} + bound_coeff = coeff.bind(local_map) + else: + bound_coeff = coeff + values[j] += bound_coeff * results.values[i] + i += 1 + + return EstimatorResult(values=values, metadata=metadata) diff --git a/qiskit_machine_learning/primitives/qnns/primitives/gradient/param_shift_sampler_gradient.py b/qiskit_machine_learning/primitives/qnns/primitives/gradient/param_shift_sampler_gradient.py new file mode 100644 index 000000000..8c8b6f938 --- /dev/null +++ b/qiskit_machine_learning/primitives/qnns/primitives/gradient/param_shift_sampler_gradient.py @@ -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. +""" +Gradient of probabilities with parameter shift +""" + +from __future__ import annotations + +from collections import Counter, defaultdict +from dataclasses import dataclass +from typing import Sequence, Type + +import numpy as np + +from qiskit import transpile +from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit +from qiskit.result import QuasiDistribution + +from ..base_sampler import BaseSampler +from ..sampler_result import SamplerResult + + +@dataclass +class SubSampler: + coeff: float | ParameterExpression + circuit: QuantumCircuit + index: int + + +class ParamShiftSamplerGradient: + """Parameter shift estimator gradient""" + + SUPPORTED_GATES = ["x", "y", "z", "h", "rx", "ry", "rz", "p", "cx", "cy", "cz"] + + def __init__(self, sampler: Type[BaseSampler], circuit: QuantumCircuit): + self._circuit = circuit + self._grad = self._preprocessing() + circuits = [self._circuit] + for param, lst in self._grad.items(): + for arg in lst: + circuits.append(arg.circuit) + self._sampler = sampler(circuits=circuits) + + def __enter__(self): + return self + + def __exit__(self, *exc_info): + pass + + @classmethod + def _gradient_circuits(cls, circuit: QuantumCircuit): + circuit2 = transpile(circuit, basis_gates=cls.SUPPORTED_GATES, optimization_level=0) + ret = defaultdict(list) + for inst in circuit2.data: + if inst[0].is_parameterized(): + param = inst[0].params[0] + for p in param.parameters: + # TODO: Need to wait for an appropriate way to update parameters of + # a particular instruction. + # See https://github.com/Qiskit/qiskit-terra/issues/7894 + inst[0].params[0] = param + np.pi / 2 + ret[p].append((circuit2.copy(), param.gradient(p) / 2)) + inst[0].params[0] = param - np.pi / 2 + ret[p].append((circuit2.copy(), -param.gradient(p) / 2)) + inst[0].params[0] = param + return ret + + def _preprocessing(self): + grad = self._gradient_circuits(self._circuit) + # print("gradient circuits: ", grad) + ret = {} + index = 1 + for param in self._circuit.parameters: + lst = [] + for circ, coeff in grad[param]: + lst.append(SubSampler(coeff=coeff, circuit=circ, index=index)) + index += 1 + ret[param] = lst + return ret + + def __call__(self, parameter_values: Sequence[Sequence[float]], **run_options) -> SamplerResult: + return self._sampler([0], parameter_values, **run_options) + + def gradient( + self, + parameter_value: Sequence[float], + partial: Sequence[Parameter] | None = None, + **run_options, + ) -> SamplerResult: + + parameters = partial or self._circuit.parameters + + param_map = {} + + for j, param in enumerate(self._circuit.parameters): + param_map[param] = parameter_value[j] + + circ_indices = [] + for param in parameters: + circ_indices.extend([f.index for f in self._grad[param]]) + size = len(circ_indices) + results = self._sampler(circ_indices, [parameter_value] * size, **run_options) + + param_set = set(parameters) + dists = [Counter() for _ in range(len(parameter_value))] + metadata = [{} for _ in range(len(parameters))] + i = 0 + for j, (param, lst) in enumerate(self._grad.items()): + if param not in param_set: + continue + for subest in lst: + coeff = subest.coeff + if isinstance(coeff, ParameterExpression): + local_map = {param: param_map[param] for param in coeff.parameters} + bound_coeff = float(coeff.bind(local_map)) + else: + bound_coeff = coeff + dists[j].update( + Counter({k: bound_coeff * v for k, v in results.quasi_dists[i].items()}) + ) + i += 1 + + return SamplerResult( + quasi_dists=[QuasiDistribution(dist) for dist in dists], metadata=metadata + ) diff --git a/qiskit_machine_learning/primitives/qnns/primitives/sampler.py b/qiskit_machine_learning/primitives/qnns/primitives/sampler.py new file mode 100644 index 000000000..cecb0c948 --- /dev/null +++ b/qiskit_machine_learning/primitives/qnns/primitives/sampler.py @@ -0,0 +1,115 @@ +# 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. +""" +Sampler class +""" + +from __future__ import annotations + +from collections.abc import Iterable, Sequence +from typing import cast + +import numpy as np + +from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.exceptions import QiskitError +from qiskit.quantum_info import Statevector +from qiskit.result import QuasiDistribution + +from .base_sampler import BaseSampler +from .sampler_result import SamplerResult +from .utils import final_measurement_mapping, init_circuit + + +class Sampler(BaseSampler): + """ + Sampler class + """ + + def __init__( + self, + circuits: QuantumCircuit | Iterable[QuantumCircuit], + parameters: Iterable[Iterable[Parameter]] | None = None, + ): + """ + Args: + circuits: circuits to be executed + parameters: Parameters of each of the quantum circuits. + Defaults to ``[circ.parameters for circ in circuits]``. + + Raises: + QiskitError: if some classical bits are not used for measurements. + """ + if isinstance(circuits, QuantumCircuit): + circuits = [circuits] + circuits = [init_circuit(circuit) for circuit in circuits] + q_c_mappings = [final_measurement_mapping(circuit) for circuit in circuits] + self._qargs_list = [] + for circuit, q_c_mapping in zip(circuits, q_c_mappings): + if set(range(circuit.num_clbits)) != set(q_c_mapping.values()): + raise QiskitError( + "some classical bits are not used for measurements." + f" the number of classical bits {circuit.num_clbits}," + f" the used classical bits {set(q_c_mapping.values())}." + ) + c_q_mapping = sorted((c, q) for q, c in q_c_mapping.items()) + self._qargs_list.append([q for _, q in c_q_mapping]) + circuits = [circuit.remove_final_measurements(inplace=False) for circuit in circuits] + super().__init__(circuits, parameters) + self._is_closed = False + + def __call__( + self, + circuit_indices: Sequence[int] | None = None, + parameter_values: Sequence[Sequence[float]] | Sequence[float] | None = None, + **run_options, + ) -> SamplerResult: + if self._is_closed: + raise QiskitError("The primitive has been closed.") + + if isinstance(parameter_values, np.ndarray): + parameter_values = parameter_values.tolist() + if parameter_values and not isinstance(parameter_values[0], (np.ndarray, Sequence)): + parameter_values = cast("Sequence[float]", parameter_values) + parameter_values = [parameter_values] + if circuit_indices is None: + circuit_indices = list(range(len(self._circuits))) + if parameter_values is None: + parameter_values = [[]] * len(circuit_indices) + if len(circuit_indices) != len(parameter_values): + raise QiskitError( + f"The number of circuit indices ({len(circuit_indices)}) does not match " + f"the number of parameter value sets ({len(parameter_values)})." + ) + + bound_circuits_qargs = [] + for i, value in zip(circuit_indices, parameter_values): + if len(value) != len(self._parameters[i]): + raise QiskitError( + f"The number of values ({len(value)}) does not match " + f"the number of parameters ({len(self._parameters[i])})." + ) + bound_circuits_qargs.append( + ( + self._circuits[i].bind_parameters(dict(zip(self._parameters[i], value))), + self._qargs_list[i], + ) + ) + probabilities = [ + Statevector(circ).probabilities(qargs=qargs) for circ, qargs in bound_circuits_qargs + ] + quasis = [QuasiDistribution(dict(enumerate(p))) for p in probabilities] + + return SamplerResult(quasis, [{}] * len(circuit_indices)) + + def close(self): + self._is_closed = True diff --git a/qiskit_machine_learning/primitives/qnns/primitives/sampler_result.py b/qiskit_machine_learning/primitives/qnns/primitives/sampler_result.py new file mode 100644 index 000000000..5b53b1066 --- /dev/null +++ b/qiskit_machine_learning/primitives/qnns/primitives/sampler_result.py @@ -0,0 +1,43 @@ +# 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. +""" +Sampler result class +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from qiskit.result import QuasiDistribution + + +@dataclass(frozen=True) +class SamplerResult: + """Result of Sampler. + + .. code-block:: python + + result = sampler(circuits, params) + + where the i-th elements of ``result`` correspond to the circuit given by ``circuit_indices[i]``, + and the parameter values bounds by ``params[i]``. + For example, ``results.quasi_dists[i]`` gives the quasi-probabilities of bitstrings, and + ``result.metadata[i]`` is a metadata dictionary for this circuit and parameters. + + Args: + quasi_dists (list[QuasiDistribution]): List of the quasi-probabilities. + metadata (list[dict]): List of the metadata. + """ + + quasi_dists: list[QuasiDistribution] + metadata: list[dict[str, Any]] diff --git a/qiskit_machine_learning/primitives/qnns/primitives/utils.py b/qiskit_machine_learning/primitives/qnns/primitives/utils.py new file mode 100644 index 000000000..e5abc4120 --- /dev/null +++ b/qiskit_machine_learning/primitives/qnns/primitives/utils.py @@ -0,0 +1,113 @@ +# 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. +""" +Utility functions for primitives +""" + +from __future__ import annotations + +from qiskit.circuit import ParameterExpression, QuantumCircuit +from qiskit.extensions.quantum_initializer.initializer import Initialize +from qiskit.opflow import PauliSumOp +from qiskit.quantum_info import SparsePauliOp, Statevector +from qiskit.quantum_info.operators.base_operator import BaseOperator +from qiskit.quantum_info.operators.symplectic.base_pauli import BasePauli + + +def init_circuit(state: QuantumCircuit | Statevector) -> QuantumCircuit: + """Initialize state by converting the input to a quantum circuit. + + Args: + state: The state as quantum circuit or statevector. + + Returns: + The state as quantum circuit. + """ + if isinstance(state, QuantumCircuit): + return state + if not isinstance(state, Statevector): + state = Statevector(state) + qc = QuantumCircuit(state.num_qubits) + qc.append(Initialize(state), qargs=range(state.num_qubits)) + return qc + + +def init_observable(observable: BaseOperator | PauliSumOp) -> SparsePauliOp: + """Initialize observable by converting the input to a :class:`~qiskit.quantum_info.SparsePauliOp`. + + Args: + observable: The observable. + + Returns: + The observable as :class:`~qiskit.quantum_info.SparsePauliOp`. + + Raises: + TypeError: If the observable is a :class:`~qiskit.opflow.PauliSumOp` and has a parameterized + coefficient. + """ + if isinstance(observable, SparsePauliOp): + return observable + elif isinstance(observable, PauliSumOp): + if isinstance(observable.coeff, ParameterExpression): + raise TypeError( + f"Observable must have numerical coefficient, not {type(observable.coeff)}." + ) + return observable.coeff * observable.primitive + elif isinstance(observable, BasePauli): + return SparsePauliOp(observable) + elif isinstance(observable, BaseOperator): + return SparsePauliOp.from_operator(observable) + else: + return SparsePauliOp(observable) + + +def final_measurement_mapping(circuit: QuantumCircuit) -> dict[int, int]: + """Return the final measurement mapping for the circuit. + + Dict keys label measured qubits, whereas the values indicate the + classical bit onto which that qubits measurement result is stored. + + Note: this function is a slightly simplified version of a utility function + ``_final_measurement_mapping`` of + `mthree `_. + + Parameters: + circuit: Input quantum circuit. + + Returns: + Mapping of qubits to classical bits for final measurements. + """ + active_qubits = list(range(circuit.num_qubits)) + active_cbits = list(range(circuit.num_clbits)) + + # Find final measurements starting in back + mapping = {} + for item in circuit._data[::-1]: + if item[0].name == "measure": + cbit = circuit.find_bit(item[2][0]).index + qbit = circuit.find_bit(item[1][0]).index + if cbit in active_cbits and qbit in active_qubits: + mapping[qbit] = cbit + active_cbits.remove(cbit) + active_qubits.remove(qbit) + elif item[0].name != "barrier": + for qq in item[1]: + _temp_qubit = circuit.find_bit(qq).index + if _temp_qubit in active_qubits: + active_qubits.remove(_temp_qubit) + + if not active_cbits or not active_qubits: + break + + # Sort so that classical bits are in numeric order low->high. + mapping = dict(sorted(mapping.items(), key=lambda item: item[1])) + return mapping diff --git a/qiskit_machine_learning/primitives/qnns/sampler-qnn-example.py b/qiskit_machine_learning/primitives/qnns/sampler-qnn-example.py new file mode 100644 index 000000000..7ff555ea9 --- /dev/null +++ b/qiskit_machine_learning/primitives/qnns/sampler-qnn-example.py @@ -0,0 +1,75 @@ +# THIS EXAMPLE USES THE TERRA PRIMITIVES! +import numpy as np +from qiskit.primitives import Sampler, Estimator +from qiskit import Aer, QuantumCircuit +from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap +from qiskit.utils import QuantumInstance, algorithm_globals +from qiskit_machine_learning.neural_networks import CircuitQNN +from primitives.gradient.param_shift_sampler_gradient import ParamShiftSamplerGradient +from primitives.gradient.finite_diff_sampler_gradient import FiniteDiffSamplerGradient +algorithm_globals.random_seed = 42 + +# DEFINE CIRCUIT FOR SAMPLER +num_qubits = 3 +qc = RealAmplitudes(num_qubits, entanglement="linear", reps=1) +qc.draw(output="mpl") +# ADD MEASUREMENT HERE --> TRICKY +qc.measure_all() + +# --------------------- + +from qiskit import Aer +from qiskit.utils import QuantumInstance + +qi_qasm = QuantumInstance(Aer.get_backend("aer_simulator"), shots=10) +qi_sv = QuantumInstance(Aer.get_backend("statevector_simulator")) + +parity = lambda x: "{:b}".format(x).count("1") % 2 +output_shape = 2 # this is required in case of a callable with dense output + +qnn2 = CircuitQNN( + qc, + input_params=qc.parameters[:3], + weight_params=qc.parameters[3:], + sparse = False, + interpret = parity, + output_shape = output_shape, + quantum_instance=qi_sv, +) +inputs = np.asarray(algorithm_globals.random.random(size = (1, qnn2._num_inputs))) +weights = algorithm_globals.random.random(qnn2._num_weights) +print("inputs: ", inputs) +print("weights: ", weights) + +np.set_printoptions(precision=2) +f = qnn2.forward(inputs, weights) +print( f"fwd pass: {f}") +np.set_printoptions(precision=2) +b = qnn2.backward(inputs, weights) +print( f"bkwd pass: {b}" ) +# --------------------- + +# IMPORT QNN +from neural_networks.sampler_qnn_2 import SamplerQNN + +with SamplerQNN( + circuit=qc, + input_params=qc.parameters[:3], + weight_params=qc.parameters[3:], + sampler_factory=Sampler, + interpret = parity, + output_shape = output_shape, + ) as qnn: + # inputs = np.asarray(algorithm_globals.random.random((2, qnn._num_inputs))) + # weights = algorithm_globals.random.random(qnn._num_weights) + print("inputs: ", inputs) + print("weights: ", weights) + + np.set_printoptions(precision=2) + f = qnn.forward(inputs, weights) + print(f"fwd pass: {f}") + np.set_printoptions(precision=2) + b = qnn.backward(inputs, weights) + print(f"bkwd pass: {b}") + + From eecbdab7443f6996eaaf687c56dd662647ab0e88 Mon Sep 17 00:00:00 2001 From: ElePT Date: Fri, 8 Jul 2022 11:38:20 +0200 Subject: [PATCH 06/61] Establish qnn branch --- .../primitives/kernels/__init__.py | 44 ----- .../primitives/kernels/base_kernel.py | 118 -------------- .../primitives/kernels/pseudo_kernel.py | 131 --------------- .../primitives/kernels/quantum_kernel.py | 153 ------------------ .../kernels/trainable_quantum_kernel.py | 69 -------- 5 files changed, 515 deletions(-) delete mode 100644 qiskit_machine_learning/primitives/kernels/__init__.py delete mode 100644 qiskit_machine_learning/primitives/kernels/base_kernel.py delete mode 100644 qiskit_machine_learning/primitives/kernels/pseudo_kernel.py delete mode 100644 qiskit_machine_learning/primitives/kernels/quantum_kernel.py delete mode 100644 qiskit_machine_learning/primitives/kernels/trainable_quantum_kernel.py diff --git a/qiskit_machine_learning/primitives/kernels/__init__.py b/qiskit_machine_learning/primitives/kernels/__init__.py deleted file mode 100644 index c28b088d5..000000000 --- a/qiskit_machine_learning/primitives/kernels/__init__.py +++ /dev/null @@ -1,44 +0,0 @@ -# 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. - -""" -Primitives Quantum Kernels (:mod:`qiskit_machine_learning.primitives.kernels`) - -.. currentmodule:: qiskit_machine_learning.primitives.kernels - -Quantum Kernels -=============== - -.. autosummary:: - :toctree: ../stubs/ - :nosignatures: - - BaseKernel - QuantumKernel - TrainableKernel - PseudoKernel - -Submodules -========== - -.. autosummary:: - :toctree: - - algorithms -""" - -from .base_kernel import BaseKernel -from .quantum_kernel import QuantumKernel -from .trainable_quantum_kernel import TrainableQuantumKernel -from .pseudo_kernel import PseudoKernel - -__all__ = ["BaseKernel", "QuantumKernel", "TrainableQuantumKernel", "PseudoKernel"] diff --git a/qiskit_machine_learning/primitives/kernels/base_kernel.py b/qiskit_machine_learning/primitives/kernels/base_kernel.py deleted file mode 100644 index 5826696ab..000000000 --- a/qiskit_machine_learning/primitives/kernels/base_kernel.py +++ /dev/null @@ -1,118 +0,0 @@ -# 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. -from abc import abstractmethod -from typing import Tuple, Callable, List - -import numpy as np - -from qiskit import QuantumCircuit -from qiskit.primitives import Sampler - -SamplerFactory = Callable[[List[QuantumCircuit]], Sampler] - - -class BaseKernel: - """ - Abstract class providing the interface for the quantum kernel classes. - """ - - def __init__(self, sampler_factory: SamplerFactory, *, enforce_psd: bool = True) -> None: - """ - Args: - sampler_factory: A callable that creates a qiskit primitives sampler given a list of circuits. - enforce_psd: Project to closest positive semidefinite matrix if x = y. - Only enforced when not using the state vector simulator. Default True. - """ - self._num_features: int = 0 - self._enforce_psd = enforce_psd - self._sampler_factory = sampler_factory - - @abstractmethod - def evaluate(self, x_vec: np.ndarray, y_vec: np.ndarray = 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 - - Raises: - QiskitMachineLearningError: - - A quantum instance or backend has not been provided - ValueError: - - x_vec and/or y_vec are not one or two dimensional arrays - - x_vec and y_vec have have incompatible dimensions - - x_vec and/or y_vec have incompatible dimension with feature map and - and feature map can not be modified to match. - """ - raise NotImplementedError() - - def _check_and_reshape(self, x_vec: np.ndarray, y_vec: np.ndarray = None) -> Tuple[np.ndarray]: - r""" - Performs checks on the dimensions of the input data x_vec and y_vec. - Reshapes the arrays so that `x_vec.shape = (N,D)` and `y_vec.shape = (M,D)`. - """ - if not isinstance(x_vec, np.ndarray): - x_vec = np.asarray(x_vec) - - if y_vec is not None and not isinstance(y_vec, np.ndarray): - y_vec = np.asarray(y_vec) - - if x_vec.ndim > 2: - raise ValueError("x_vec must be a 1D or 2D array") - - if x_vec.ndim == 1: - x_vec = x_vec.reshape(1, -1) - - if y_vec is not None and y_vec.ndim > 2: - raise ValueError("y_vec must be a 1D or 2D array") - - if y_vec is not None and y_vec.ndim == 1: - y_vec = y_vec.reshape(1, -1) - - if y_vec is not None and 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]}." - ) - - if x_vec.shape[1] != self._num_features: - raise ValueError( - "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._num_features}." - ) - - if y_vec is None: - y_vec = x_vec - - return x_vec, y_vec - - 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, thus the - adjustment is only done if NOT using the statevector simulation. - - Args: - kernel_matrix: symmetric 2D array of the kernel entries - """ - d, u = np.linalg.eig(kernel_matrix) - return u @ np.diag(np.maximum(0, d)) @ u.transpose() diff --git a/qiskit_machine_learning/primitives/kernels/pseudo_kernel.py b/qiskit_machine_learning/primitives/kernels/pseudo_kernel.py deleted file mode 100644 index 63b6e4b3d..000000000 --- a/qiskit_machine_learning/primitives/kernels/pseudo_kernel.py +++ /dev/null @@ -1,131 +0,0 @@ -# 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. - -"""Pseudo Overlap Kernel""" - -from typing import Optional, Callable, Union, List -import numpy as np - -from qiskit import QuantumCircuit -from qiskit.primitives import Sampler -from qiskit.primitives.fidelity import BaseFidelity - -from qiskit_machine_learning.primitives.kernels import QuantumKernel -from qiskit_machine_learning.utils import make_2d - -SamplerFactory = Callable[[List[QuantumCircuit]], Sampler] - - -class PseudoKernel(QuantumKernel): - """ - Pseudo kernel - """ - - def __init__( - self, - sampler_factory: SamplerFactory, - feature_map: Optional[QuantumCircuit] = None, - *, - num_training_parameters: int = 0, - fidelity: Union[str, BaseFidelity] = "zero_prob", - enforce_psd: bool = True, - ) -> None: - super().__init__(sampler_factory, feature_map, fidelity=fidelity, enforce_psd=enforce_psd) - self.num_parameters = num_training_parameters - - def evaluate(self, x_vec: np.ndarray, y_vec: np.ndarray = None) -> np.ndarray: - # allow users to only provide features, parameters are set to 0 - if x_vec.shape[1] + self.num_parameters == self._num_features: - return self.evaluate_batch(x_vec, y_vec) - else: - return super().evaluate(x_vec, y_vec) - - def evaluate_batch( - self, - x_vec: np.ndarray, - y_vec: np.ndarray, - x_parameters: np.ndarray = None, - y_parameters: np.ndarray = None, - ) -> np.ndarray: - r""" - Construct kernel matrix for given data and feature map - - If y_vec is None, self inner product is calculated. - If using `statevector_simulator`, only build circuits for :math:`\Psi(x)|0\rangle`, - then perform inner product classically. - - 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 - x_parameters: 1D or 2D array of parameters, NxP, where N is the number of datapoints, - P is the number of trainable parameters - y_parameters: 1D or 2D array of parameters, MxP - - - Returns: - 2D matrix, NxM - - Raises: - QiskitMachineLearningError: - - A quantum instance or backend has not been provided - ValueError: - - x_vec and/or y_vec are not one or two dimensional arrays - - x_vec and y_vec have have incompatible dimensions - - x_vec and/or y_vec have incompatible dimension with feature map and - and feature map can not be modified to match. - """ - if x_parameters is None: - x_parameters = np.zeros((x_vec.shape[0], self.num_parameters)) - - if y_parameters is None: - y_parameters = np.zeros((y_vec.shape[0], self.num_parameters)) - - if len(x_vec.shape) == 1: - x_vec = x_vec.reshape(1, -1) - - if len(y_vec.shape) == 1: - y_vec = y_vec.reshape(1, -1) - - if len(x_parameters.shape) == 1: - x_parameters = make_2d(x_parameters, x_vec.shape[0]) - - if len(y_parameters.shape) == 1: - y_parameters = make_2d(y_parameters, y_vec.shape[0]) - - if x_vec.shape[0] != x_parameters.shape[0]: - if x_parameters.shape[0] == 1: - x_parameters = make_2d(x_parameters, x_vec.shape[0]) - else: - raise ValueError( - f"Number of x data points ({x_vec.shape[0]}) does not coincide with number of parameter vectors {x_parameters.shape[0]}." - ) - if y_vec.shape[0] != y_parameters.shape[0]: - if y_parameters.shape[0] == 1: - x_parameters = make_2d(y_parameters, y_vec.shape[0]) - else: - raise ValueError( - f"Number of y data points ({y_vec.shape[0]}) does not coincide with number of parameter vectors {y_parameters.shape[0]}." - ) - - if x_parameters.shape[1] != self.num_parameters: - raise ValueError( - f"Number of parameters provided ({x_parameters.shape[0]}) does not coincide with the number provided in the feature map ({self.num_parameters})." - ) - - if y_parameters.shape[1] != self.num_parameters: - raise ValueError( - f"Number of parameters provided ({y_parameters.shape[0]}) does not coincide with the number provided in the feature map ({self.num_parameters})." - ) - - return self.evaluate(np.hstack((x_vec, x_parameters)), np.hstack((y_vec, y_parameters))) diff --git a/qiskit_machine_learning/primitives/kernels/quantum_kernel.py b/qiskit_machine_learning/primitives/kernels/quantum_kernel.py deleted file mode 100644 index 16d10e3b0..000000000 --- a/qiskit_machine_learning/primitives/kernels/quantum_kernel.py +++ /dev/null @@ -1,153 +0,0 @@ -# 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. -"""Overlap Quantum Kernel""" - -from typing import Optional, Tuple, Callable, List, Union -import numpy as np - -from qiskit import QuantumCircuit -from qiskit.circuit.library import ZZFeatureMap -from qiskit.primitives import Sampler -from qiskit.primitives.fidelity import BaseFidelity, Fidelity - -from qiskit_machine_learning.primitives.kernels import BaseKernel - - -SamplerFactory = Callable[[List[QuantumCircuit]], Sampler] - - -class QuantumKernel(BaseKernel): - """ - Overlap Kernel - """ - - def __init__( - self, - sampler_factory: SamplerFactory, - feature_map: Optional[QuantumCircuit] = None, - *, - fidelity: Union[str, BaseFidelity] = "zero_prob", - enforce_psd: bool = True, - ) -> None: - super().__init__(sampler_factory, enforce_psd=enforce_psd) - - if feature_map is None: - feature_map = ZZFeatureMap(2).decompose() - - self._num_features = feature_map.num_parameters - - if isinstance(fidelity, str) and fidelity == "zero_prob": - self._fidelity = Fidelity() - else: - self._fidelity = fidelity - self._fidelity.set_circuits(feature_map, feature_map) - - def evaluate(self, x_vec: np.ndarray, y_vec: np.ndarray = None) -> np.ndarray: - x_vec, y_vec = self._check_and_reshape(x_vec, y_vec) - is_symmetric = np.all(x_vec == y_vec) - shape = len(x_vec), len(y_vec) - - if is_symmetric: - left_parameters, right_parameters = self._get_symmetric_parametrization(x_vec) - kernel_matrix = self._get_symmetric_kernel_matrix( - left_parameters, right_parameters, shape - ) - - else: - left_parameters, right_parameters = self._get_parametrization(x_vec, y_vec) - kernel_matrix = self._get_kernel_matrix(left_parameters, right_parameters, shape) - - if is_symmetric and self._enforce_psd: - kernel_matrix = self._make_psd(kernel_matrix) - return kernel_matrix - - def _get_parametrization(self, x_vec: np.ndarray, y_vec: np.ndarray) -> Tuple[np.ndarray]: - """ - Combines x_vec and y_vec to get all the combinations needed to evaluate the kernel entries. - """ - x_count = x_vec.shape[0] - y_count = y_vec.shape[0] - - left_parameters = np.zeros((x_count * y_count, x_vec.shape[1])) - right_parameters = np.zeros((x_count * y_count, y_vec.shape[1])) - index = 0 - for x_i in x_vec: - for y_j in y_vec: - left_parameters[index, :] = x_i - right_parameters[index, :] = y_j - index += 1 - - return left_parameters, right_parameters - - def _get_symmetric_parametrization(self, x_vec: np.ndarray) -> Tuple[np.ndarray]: - """ - Combines two copies of x_vec to get all the combinations needed to evaluate the kernel entries. - """ - x_count = x_vec.shape[0] - - left_parameters = np.zeros((x_count * (x_count + 1) // 2, x_vec.shape[1])) - right_parameters = np.zeros((x_count * (x_count + 1) // 2, x_vec.shape[1])) - - index = 0 - for i, x_i in enumerate(x_vec): - for x_j in x_vec[i:]: - left_parameters[index, :] = x_i - right_parameters[index, :] = x_j - index += 1 - - return left_parameters, right_parameters - - def _get_kernel_matrix( - self, left_parameters: np.ndarray, right_parameters: np.ndarray, shape: Tuple - ) -> np.ndarray: - """ - Given a parametrization, this computes the symmetric kernel matrix. - """ - kernel_entries = self._fidelity.compute(left_parameters, right_parameters) - kernel_matrix = np.zeros(shape) - - index = 0 - for i in range(shape[0]): - for j in range(shape[1]): - kernel_matrix[i, j] = kernel_entries[index] - index += 1 - return kernel_matrix - - def _get_symmetric_kernel_matrix( - self, left_parameters: np.ndarray, right_parameters: np.ndarray, shape: Tuple - ) -> np.ndarray: - """ - Given a set of parametrization, this computes the kernel matrix. - """ - kernel_entries = self._fidelity.compute(left_parameters, right_parameters) - kernel_matrix = np.zeros(shape) - index = 0 - for i in range(shape[0]): - for j in range(i, shape[1]): - kernel_matrix[i, j] = kernel_entries[index] - index += 1 - - kernel_matrix = kernel_matrix + kernel_matrix.T - np.diag(kernel_matrix.diagonal()) - return kernel_matrix - - def __enter__(self): - """ - Creates the full fidelity class by creating the sampler from the factory. - """ - self._fidelity.sampler_from_factory(self._sampler_factory) - return self - - def __exit__(self, *args): - """ - Closes the sampler session. - """ - self._fidelity.sampler.close() diff --git a/qiskit_machine_learning/primitives/kernels/trainable_quantum_kernel.py b/qiskit_machine_learning/primitives/kernels/trainable_quantum_kernel.py deleted file mode 100644 index 3041ec182..000000000 --- a/qiskit_machine_learning/primitives/kernels/trainable_quantum_kernel.py +++ /dev/null @@ -1,69 +0,0 @@ -# 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. - -"""Trainable Quantum Kernel""" - -from typing import Tuple, Optional, Callable, List, Union -import numpy as np - -from qiskit import QuantumCircuit -from qiskit.primitives import Sampler -from qiskit.primitives.fidelity import BaseFidelity - - -from qiskit_machine_learning.primitives.kernels import QuantumKernel -from qiskit_machine_learning.utils import make_2d - -SamplerFactory = Callable[[List[QuantumCircuit]], Sampler] - - -class TrainableQuantumKernel(QuantumKernel): - """ - Trainable overlap kernel. - """ - - def __init__( - self, - sampler_factory: SamplerFactory, - feature_map: Optional[QuantumCircuit] = None, - *, - fidelity: Union[str, BaseFidelity] = "zero_prob", - num_training_parameters: int = 0, - enforce_psd: bool = True, - ) -> None: - super().__init__(sampler_factory, feature_map, fidelity=fidelity, enforce_psd=enforce_psd) - self.num_parameters = num_training_parameters - self._num_features = self._num_features - self.num_parameters - - self.parameter_values = np.zeros(self.num_parameters) - - def bind_parameters_values(self, parameter_values: np.ndarray) -> None: - """ - Fix the training parameters to numerical values. - """ - if parameter_values.shape == self.parameter_values.shape: - self.parameter_values = parameter_values - else: - raise ValueError( - f"The given parameters are in the wrong shape {parameter_values.shape}, expected {self.parameter_values.shape}." - ) - - def _get_parametrization(self, x_vec: np.ndarray, y_vec: np.ndarray) -> Tuple[np.ndarray]: - new_x_vec = np.hstack((x_vec, make_2d(self.parameter_values, len(x_vec)))) - new_y_vec = np.hstack((y_vec, make_2d(self.parameter_values, len(y_vec)))) - - return super()._get_parametrization(new_x_vec, new_y_vec) - - def _get_symmetric_parametrization(self, x_vec: np.ndarray) -> np.ndarray: - new_x_vec = np.hstack((x_vec, make_2d(self.parameter_values, len(x_vec)))) - - return super()._get_symmetric_parametrization(new_x_vec) From a212196d814d1b193dc621647965819534479209 Mon Sep 17 00:00:00 2001 From: ElePT Date: Mon, 11 Jul 2022 13:04:22 +0200 Subject: [PATCH 07/61] Add fwd unit test --- test/primitives/test_sampler_qnn.py | 76 +++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 test/primitives/test_sampler_qnn.py diff --git a/test/primitives/test_sampler_qnn.py b/test/primitives/test_sampler_qnn.py new file mode 100644 index 000000000..7a671a098 --- /dev/null +++ b/test/primitives/test_sampler_qnn.py @@ -0,0 +1,76 @@ +# 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. + +"""Test Sampler QNN with Terra primitives.""" +import numpy as np +from test import QiskitMachineLearningTestCase + +from qiskit.primitives import Sampler +from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap +from qiskit.utils import QuantumInstance, algorithm_globals +from qiskit import Aer +from qiskit.utils import QuantumInstance + +from qiskit_machine_learning.neural_networks import CircuitQNN +from qiskit_machine_learning.primitives.sampler_qnn import SamplerQNN + +algorithm_globals.random_seed = 42 + + +class TestSamplerQNN(QiskitMachineLearningTestCase): + """Sampler QNN Tests.""" + + def setUp(self): + super().setUp() + algorithm_globals.random_seed = 12345 + + # define test circuit + num_qubits = 3 + self.qc = RealAmplitudes(num_qubits, entanglement="linear", reps=1) + self.qi_qasm = QuantumInstance(Aer.get_backend("aer_simulator"), shots=10) + + def test_forward_pass(self): + + parity = lambda x: "{:b}".format(x).count("1") % 2 + output_shape = 2 # this is required in case of a callable with dense output + + circuit_qnn = CircuitQNN( + self.qc, + input_params=self.qc.parameters[:3], + weight_params=self.qc.parameters[3:], + sparse=False, + interpret=parity, + output_shape=output_shape, + quantum_instance=self.qi_qasm, + ) + + inputs = np.asarray(algorithm_globals.random.random(size=(1, circuit_qnn._num_inputs))) + weights = algorithm_globals.random.random(circuit_qnn._num_weights) + circuit_qnn_fwd = circuit_qnn.forward(inputs, weights) + + sampler_factory = Sampler + with SamplerQNN( + circuit=self.qc, + input_params=self.qc.parameters[:3], + weight_params=self.qc.parameters[3:], + sampler_factory=sampler_factory, + interpret = parity, + output_shape = output_shape, + ) as qnn: + + sampler_qnn_fwd = qnn.forward(inputs, weights) + + np.testing.assert_array_almost_equal(np.asarray(sampler_qnn_fwd), + np.asarray(circuit_qnn_fwd),0.1) + + + From 81f8082a5444f0eab95207970790bebc5de14e58 Mon Sep 17 00:00:00 2001 From: ElePT Date: Mon, 11 Jul 2022 13:04:41 +0200 Subject: [PATCH 08/61] Refactor sampler qnn --- .../primitives/qnns/primitives/__init__.py | 58 ----- .../qnns/primitives/base_estimator.py | 238 ------------------ .../qnns/primitives/base_sampler.py | 181 ------------- .../primitives/qnns/primitives/estimator.py | 115 --------- .../qnns/primitives/estimator_result.py | 44 ---- .../qnns/primitives/factories/__init__.py | 2 - .../primitives/factories/estimator_factory.py | 16 -- .../statevector_estimator_factory.py | 13 - .../qnns/primitives/gradient/__init__.py | 14 -- .../gradient/base_estimator_gradient.py | 37 --- .../finite_diff_estimator_gradient.py | 56 ----- .../gradient/finite_diff_sampler_gradient.py | 55 ---- .../gradient/lin_comb_estimator_gradient.py | 224 ----------------- .../gradient/lin_comb_sampler_gradient.py | 221 ---------------- .../param_shift_estimator_gradient.py | 139 ---------- .../gradient/param_shift_sampler_gradient.py | 134 ---------- .../primitives/qnns/primitives/sampler.py | 115 --------- .../qnns/primitives/sampler_result.py | 43 ---- .../primitives/qnns/primitives/utils.py | 113 --------- .../primitives/qnns/sampler-qnn-example.py | 75 ------ .../{qnns/neural_networks => }/sampler_qnn.py | 87 ++++--- 21 files changed, 51 insertions(+), 1929 deletions(-) delete mode 100644 qiskit_machine_learning/primitives/qnns/primitives/__init__.py delete mode 100644 qiskit_machine_learning/primitives/qnns/primitives/base_estimator.py delete mode 100644 qiskit_machine_learning/primitives/qnns/primitives/base_sampler.py delete mode 100644 qiskit_machine_learning/primitives/qnns/primitives/estimator.py delete mode 100644 qiskit_machine_learning/primitives/qnns/primitives/estimator_result.py delete mode 100644 qiskit_machine_learning/primitives/qnns/primitives/factories/__init__.py delete mode 100644 qiskit_machine_learning/primitives/qnns/primitives/factories/estimator_factory.py delete mode 100644 qiskit_machine_learning/primitives/qnns/primitives/factories/statevector_estimator_factory.py delete mode 100755 qiskit_machine_learning/primitives/qnns/primitives/gradient/__init__.py delete mode 100644 qiskit_machine_learning/primitives/qnns/primitives/gradient/base_estimator_gradient.py delete mode 100644 qiskit_machine_learning/primitives/qnns/primitives/gradient/finite_diff_estimator_gradient.py delete mode 100644 qiskit_machine_learning/primitives/qnns/primitives/gradient/finite_diff_sampler_gradient.py delete mode 100644 qiskit_machine_learning/primitives/qnns/primitives/gradient/lin_comb_estimator_gradient.py delete mode 100644 qiskit_machine_learning/primitives/qnns/primitives/gradient/lin_comb_sampler_gradient.py delete mode 100644 qiskit_machine_learning/primitives/qnns/primitives/gradient/param_shift_estimator_gradient.py delete mode 100644 qiskit_machine_learning/primitives/qnns/primitives/gradient/param_shift_sampler_gradient.py delete mode 100644 qiskit_machine_learning/primitives/qnns/primitives/sampler.py delete mode 100644 qiskit_machine_learning/primitives/qnns/primitives/sampler_result.py delete mode 100644 qiskit_machine_learning/primitives/qnns/primitives/utils.py delete mode 100644 qiskit_machine_learning/primitives/qnns/sampler-qnn-example.py rename qiskit_machine_learning/primitives/{qnns/neural_networks => }/sampler_qnn.py (83%) diff --git a/qiskit_machine_learning/primitives/qnns/primitives/__init__.py b/qiskit_machine_learning/primitives/qnns/primitives/__init__.py deleted file mode 100644 index bd00dedc7..000000000 --- a/qiskit_machine_learning/primitives/qnns/primitives/__init__.py +++ /dev/null @@ -1,58 +0,0 @@ -# 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. - -""" -===================================== -Primitives (:mod:`qiskit.primitives`) -===================================== - -.. currentmodule:: qiskit.primitives - -.. automodule:: qiskit.primitives.base_estimator -.. automodule:: qiskit.primitives.base_sampler - -.. currentmodule:: qiskit.primitives - -Estimator -========= - -.. autosummary:: - :toctree: ../stubs/ - - BaseEstimator - Estimator - -Sampler -======= - -.. autosummary:: - :toctree: ../stubs/ - - BaseSampler - Sampler - -Results -======= - -.. autosummary:: - :toctree: ../stubs/ - - EstimatorResult - SamplerResult -""" - -from .base_estimator import BaseEstimator -from .base_sampler import BaseSampler -from .estimator import Estimator -from .estimator_result import EstimatorResult -from .sampler import Sampler -from .sampler_result import SamplerResult diff --git a/qiskit_machine_learning/primitives/qnns/primitives/base_estimator.py b/qiskit_machine_learning/primitives/qnns/primitives/base_estimator.py deleted file mode 100644 index 199a5b503..000000000 --- a/qiskit_machine_learning/primitives/qnns/primitives/base_estimator.py +++ /dev/null @@ -1,238 +0,0 @@ -# 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. -r""" - -.. estimator-desc: - -===================== -Overview of Estimator -===================== - -Estimator class estimates expectation values of quantum circuits and observables. - -An estimator object is initialized with multiple quantum circuits and observables -and users can specify pairs of quantum circuits and observables -to estimate the expectation values. - -An estimator is initialized with the following elements. - -* quantum circuits (:math:`\psi_i(\theta)`): list of (parameterized) quantum circuits - (a list of :class:`~qiskit.circuit.QuantumCircuit`)) - -* observables (:math:`H_j`): a list of :class:`~qiskit.quantum_info.SparsePauliOp`. - -The estimator is called with the following inputs. - -* circuit indexes: a list of indexes of the quantum circuits. - -* observable indexes: a list of indexes of the observables. - -* parameters: a list of parameters of the quantum circuits. - (:class:`~qiskit.circuit.parametertable.ParameterView` or - a list of :class:`~qiskit.circuit.Parameter`). - -* parameter values (:math:`\theta_k`): list of sets of values - to be bound to the parameters of the quantum circuits. - (list of list of float) - -The output is an :class:`~qiskit.primitives.EstimatorResult` which contains a list of -expectation values plus optional metadata like confidence intervals for the estimation. - -.. math:: - - \langle\psi_i(\theta_k)|H_j|\psi_i(\theta_k)\rangle - - -The estimator object is expected to be ``close()`` d after use or -accessed inside "with" context -and the objects are called with parameter values and run options -(e.g., ``shots`` or number of shots). - -Here is an example of how estimator is used. - -.. code-block:: python - - from qiskit.circuit.library import RealAmplitudes - from qiskit.quantum_info import SparsePauliOp - - psi1 = RealAmplitudes(num_qubits=2, reps=2) - psi2 = RealAmplitudes(num_qubits=2, reps=3) - - params1 = psi1.parameters - params2 = psi2.parameters - - H1 = SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)]) - H2 = SparsePauliOp.from_list([("IZ", 1)]) - H3 = SparsePauliOp.from_list([("ZI", 1), ("ZZ", 1)]) - - with Estimator([psi1, psi2], [H1, H2, H3], [params1, params2]) as e: - theta1 = [0, 1, 1, 2, 3, 5] - theta2 = [0, 1, 1, 2, 3, 5, 8, 13] - theta3 = [1, 2, 3, 4, 5, 6] - - # calculate [ ] - result = e([0], [0], [theta1]) - print(result) - - # calculate [ , ] - result2 = e([0, 0], [1, 2], [theta1]*2) - print(result2) - - # calculate [ ] - result3 = e([1], [1], [theta2]) - print(result3) - - # calculate [ , ] - result4 = e([0, 0], [0, 0], [theta1, theta3]) - print(result4) - - # calculate [ , - # , - # ] - result5 = e([0, 1, 0], [0, 1, 2], [theta1, theta2, theta3]) - print(result5) -""" -from __future__ import annotations - -from abc import ABC, abstractmethod -from collections.abc import Iterable, Sequence - -from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.circuit.parametertable import ParameterView -from qiskit.exceptions import QiskitError -from qiskit.quantum_info.operators import SparsePauliOp - -from .estimator_result import EstimatorResult - - -class BaseEstimator(ABC): - """Estimator base class. - - Base class for Estimator that estimates expectation values of quantum circuits and observables. - """ - - def __init__( - self, - circuits: Iterable[QuantumCircuit], - observables: Iterable[SparsePauliOp], - parameters: Iterable[Iterable[Parameter]] | None = None, - ): - """ - Creating an instance of an Estimator, or using one in a ``with`` context opens a session that - holds resources until the instance is ``close()`` ed or the context is exited. - - Args: - circuits: Quantum circuits that represent quantum states. - observables: Observables. - parameters: Parameters of quantum circuits, specifying the order in which values - will be bound. Defaults to ``[circ.parameters for circ in circuits]`` - The indexing is such that ``parameters[i, j]`` is the j-th formal parameter of - ``circuits[i]``. - - Raises: - QiskitError: For mismatch of circuits and parameters list. - """ - self._circuits = tuple(circuits) - self._observables = tuple(observables) - if parameters is None: - self._parameters = tuple(circ.parameters for circ in self._circuits) - else: - self._parameters = tuple(ParameterView(par) for par in parameters) - if len(self._parameters) != len(self._circuits): - raise QiskitError( - f"Different number of parameters ({len(self._parameters)} and " - f"circuits ({len(self._circuits)}" - ) - for i, (circ, params) in enumerate(zip(self._circuits, self._parameters)): - if circ.num_parameters != len(params): - raise QiskitError( - f"Different numbers of parameters of {i}-th circuit: " - f"expected {circ.num_parameters}, actual {len(params)}." - ) - - def __enter__(self): - return self - - def __exit__(self, *exc_info): - self.close() - - @abstractmethod - def close(self): - """Close the session and free resources""" - ... - - @property - def circuits(self) -> tuple[QuantumCircuit, ...]: - """Quantum circuits that represents quantum states. - - Returns: - The quantum circuits. - """ - return self._circuits - - @property - def observables(self) -> tuple[SparsePauliOp, ...]: - """Observables to be estimated. - - Returns: - The observables. - """ - return self._observables - - @property - def parameters(self) -> tuple[ParameterView, ...]: - """Parameters of the quantum circuits. - - Returns: - Parameters, where ``parameters[i][j]`` is the j-th parameter of the i-th circuit. - """ - return self._parameters - - @abstractmethod - def __call__( - self, - circuit_indices: Sequence[int], - observable_indices: Sequence[int], - parameter_values: Sequence[Sequence[float]], - **run_options, - ) -> EstimatorResult: - """Run the estimation of expectation value(s). - - ``circuit_indices``, ``observable_indices``, and ``parameter_values`` should have the same - length. The i-th element of the result is the expectation of observable - - .. code-block:: python - - obs = self.observables[observable_indices[i]] - - for the state prepared by - - .. code-block:: python - - circ = self.circuits[circuit_indices[i]] - - with bound parameters - - .. code-block:: python - - values = parameter_values[i]. - - Args: - circuit_indices: the list of circuit indices. - observable_indices: the list of observable indices. - parameter_values: concrete parameters to be bound. - run_options: runtime options used for circuit execution. - - Returns: - EstimatorResult: The result of the estimator. - """ - ... diff --git a/qiskit_machine_learning/primitives/qnns/primitives/base_sampler.py b/qiskit_machine_learning/primitives/qnns/primitives/base_sampler.py deleted file mode 100644 index 28b5b69d6..000000000 --- a/qiskit_machine_learning/primitives/qnns/primitives/base_sampler.py +++ /dev/null @@ -1,181 +0,0 @@ -# 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. -r""" -=================== -Overview of Sampler -=================== - -Sampler class calculates probabilities or quasi-probabilities of bitstrings from quantum circuits. - -A sampler is initialized with the following elements. - -* quantum circuits (:math:`\psi_i(\theta)`): list of (parameterized) quantum circuits. - (a list of :class:`~qiskit.circuit.QuantumCircuit`)) - -* parameters: a list of parameters of the quantum circuits. - (:class:`~qiskit.circuit.parametertable.ParameterView` or - a list of :class:`~qiskit.circuit.Parameter`). - -The sampler is run with the following inputs. - -* circuit indexes: a list of indices of the circuits to evaluate. - -* parameter values (:math:`\theta_k`): list of sets of parameter values - to be bound to the parameters of the quantum circuits. - (list of list of float) - -The output is a :class:`~qiskit.primitives.SamplerResult` which contains probabilities -or quasi-probabilities of bitstrings, -plus optional metadata like error bars in the samples. - -The sampler object is expected to be closed after use or -accessed within "with" context -and the objects are called with parameter values and run options -(e.g., ``shots`` or number of shots). - -Here is an example of how sampler is used. - -.. code-block:: python - - from qiskit import QuantumCircuit - from qiskit.circuit.library import RealAmplitudes - - bell = QuantumCircuit(2) - bell.h(0) - bell.cx(0, 1) - bell.measure_all() - - # executes a Bell circuit - with Sampler(circuits=[bell], parameters=[[]]) as sampler: - result = sampler(parameters=[[]], circuits=[0]) - print([q.binary_probabilities() for q in result.quasi_dists]) - - # executes three Bell circuits - with Sampler([bell]*3, [[]] * 3) as sampler: - result = sampler([0, 1, 2], [[]]*3) - print([q.binary_probabilities() for q in result.quasi_dists]) - - # parameterized circuit - pqc = RealAmplitudes(num_qubits=2, reps=2) - pqc.measure_all() - pqc2 = RealAmplitudes(num_qubits=2, reps=3) - pqc2.measure_all() - - theta1 = [0, 1, 1, 2, 3, 5] - theta2 = [1, 2, 3, 4, 5, 6] - theta3 = [0, 1, 2, 3, 4, 5, 6, 7] - - with Sampler(circuits=[pqc, pqc2], parameters=[pqc.parameters, pqc2.parameters]) as sampler: - result = sampler([0, 0, 1], [theta1, theta2, theta3]) - - # result of pqc(theta1) - print(result.quasi_dists[0].binary_probabilities()) - - # result of pqc(theta2) - print(result.quasi_dists[1].binary_probabilities()) - - # result of pqc2(theta3) - print(result.quasi_dists[2].binary_probabilities()) - -""" -from __future__ import annotations - -from abc import ABC, abstractmethod -from collections.abc import Iterable, Sequence - -from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.circuit.parametertable import ParameterView -from qiskit.exceptions import QiskitError - -from .sampler_result import SamplerResult - - -class BaseSampler(ABC): - """Sampler base class - - Base class of Sampler that calculates quasi-probabilities of bitstrings from quantum circuits. - """ - - def __init__( - self, - circuits: Iterable[QuantumCircuit], - parameters: Iterable[Iterable[Parameter]] | None = None, - ): - """ - Args: - circuits: Quantum circuits to be executed. - parameters: Parameters of each of the quantum circuits. - Defaults to ``[circ.parameters for circ in circuits]``. - - Raises: - QiskitError: For mismatch of circuits and parameters list. - """ - self._circuits = tuple(circuits) - if parameters is None: - self._parameters = tuple(circ.parameters for circ in self._circuits) - else: - self._parameters = tuple(ParameterView(par) for par in parameters) - if len(self._parameters) != len(self._circuits): - raise QiskitError( - f"Different number of parameters ({len(self._parameters)} " - f"and circuits ({len(self._circuits)}" - ) - - def __enter__(self): - return self - - def __exit__(self, *exc_info): - self.close() - - @abstractmethod - def close(self): - """Close the session and free resources""" - ... - - @property - def circuits(self) -> tuple[QuantumCircuit, ...]: - """Quantum circuits to be sampled. - - Returns: - The quantum circuits to be sampled. - """ - return self._circuits - - @property - def parameters(self) -> tuple[ParameterView, ...]: - """Parameters of quantum circuits. - - Returns: - List of the parameters in each quantum circuit. - """ - return self._parameters - - @abstractmethod - def __call__( - self, - circuit_indices: Sequence[int], - parameter_values: Sequence[Sequence[float]], - **run_options, - ) -> SamplerResult: - """Run the sampling of bitstrings. - - Args: - circuit_indices: Indices of the circuits to evaluate. - parameter_values: Parameters to be bound to the circuit. - run_options: Backend runtime options used for circuit execution. - - Returns: - The result of the sampler. The i-th result corresponds to - ``self.circuits[circuit_indices[i]]`` evaluated with parameters bound as - ``parameter_values[i]``. - """ - ... diff --git a/qiskit_machine_learning/primitives/qnns/primitives/estimator.py b/qiskit_machine_learning/primitives/qnns/primitives/estimator.py deleted file mode 100644 index 6e9a08c86..000000000 --- a/qiskit_machine_learning/primitives/qnns/primitives/estimator.py +++ /dev/null @@ -1,115 +0,0 @@ -# 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. -""" -Estimator class -""" - -from __future__ import annotations - -from collections.abc import Iterable, Sequence -from typing import cast - -import numpy as np - -from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.exceptions import QiskitError -from qiskit.opflow import PauliSumOp -from qiskit.quantum_info import Statevector -from qiskit.quantum_info.operators.base_operator import BaseOperator - -from .base_estimator import BaseEstimator -from .estimator_result import EstimatorResult -from .utils import init_circuit, init_observable - - -class Estimator(BaseEstimator): - """ - Estimator class - """ - - def __init__( - self, - circuits: QuantumCircuit | Iterable[QuantumCircuit], - observables: BaseOperator | PauliSumOp | Iterable[BaseOperator | PauliSumOp], - parameters: Iterable[Iterable[Parameter]] | None = None, - ): - if isinstance(circuits, QuantumCircuit): - circuits = [circuits] - circuits = [init_circuit(circuit) for circuit in circuits] - - if isinstance(observables, (PauliSumOp, BaseOperator)): - observables = [observables] - observables = [init_observable(observable) for observable in observables] - - super().__init__( - circuits=circuits, - observables=observables, - parameters=parameters, - ) - self._is_closed = False - - def __call__( - self, - circuit_indices: Sequence[int] | None = None, - observable_indices: Sequence[int] | None = None, - parameter_values: Sequence[Sequence[float]] | Sequence[float] | None = None, - **run_options, - ) -> EstimatorResult: - if self._is_closed: - raise QiskitError("The primitive has been closed.") - - if isinstance(parameter_values, np.ndarray): - parameter_values = parameter_values.tolist() - if parameter_values and not isinstance(parameter_values[0], (np.ndarray, Sequence)): - parameter_values = cast("Sequence[float]", parameter_values) - parameter_values = [parameter_values] - if circuit_indices is None: - circuit_indices = list(range(len(self._circuits))) - if observable_indices is None: - observable_indices = list(range(len(self._observables))) - if parameter_values is None: - parameter_values = [[]] * len(circuit_indices) - if len(circuit_indices) != len(observable_indices): - raise QiskitError( - f"The number of circuit indices ({len(circuit_indices)}) does not match " - f"the number of observable indices ({len(observable_indices)})." - ) - if len(circuit_indices) != len(parameter_values): - raise QiskitError( - f"The number of circuit indices ({len(circuit_indices)}) does not match " - f"the number of parameter value sets ({len(parameter_values)})." - ) - - bound_circuits = [] - for i, value in zip(circuit_indices, parameter_values): - if len(value) != len(self._parameters[i]): - raise QiskitError( - f"The number of values ({len(value)}) does not match " - f"the number of parameters ({len(self._parameters[i])})." - ) - bound_circuits.append( - self._circuits[i].bind_parameters(dict(zip(self._parameters[i], value))) - ) - sorted_observables = [self._observables[i] for i in observable_indices] - expectation_values = [] - for circ, obs in zip(bound_circuits, sorted_observables): - if circ.num_qubits != obs.num_qubits: - raise QiskitError( - f"The number of qubits of a circuit ({circ.num_qubits}) does not match " - f"the number of qubits of a observable ({obs.num_qubits})." - ) - expectation_values.append(Statevector(circ).expectation_value(obs)) - - return EstimatorResult(np.real_if_close(expectation_values), [{}] * len(expectation_values)) - - def close(self): - self._is_closed = True diff --git a/qiskit_machine_learning/primitives/qnns/primitives/estimator_result.py b/qiskit_machine_learning/primitives/qnns/primitives/estimator_result.py deleted file mode 100644 index e26d9848c..000000000 --- a/qiskit_machine_learning/primitives/qnns/primitives/estimator_result.py +++ /dev/null @@ -1,44 +0,0 @@ -# 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. -""" -Estimator result class -""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: - import numpy as np - - -@dataclass(frozen=True) -class EstimatorResult: - """Result of Estimator. - - .. code-block:: python - - result = estimator(circuits, observables, params) - - where the i-th elements of ``result`` correspond to the circuit and observable given by - ``circuit_indices[i]``, ``observable_indices[i]``, and the parameter values bounds by ``params[i]``. - For example, ``results.values[i]`` gives the expectation value, and ``result.metadata[i]`` - is a metadata dictionary for this circuit and parameters. - - Args: - values (np.ndarray): The array of the expectation values. - metadata (list[dict]): List of the metadata. - """ - - values: "np.ndarray[Any, np.dtype[np.float64]]" - metadata: list[dict[str, Any]] diff --git a/qiskit_machine_learning/primitives/qnns/primitives/factories/__init__.py b/qiskit_machine_learning/primitives/qnns/primitives/factories/__init__.py deleted file mode 100644 index d0ac2ee93..000000000 --- a/qiskit_machine_learning/primitives/qnns/primitives/factories/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .estimator_factory import EstimatorFactory -from .statevector_estimator_factory import StatevectorEstimatorFactory diff --git a/qiskit_machine_learning/primitives/qnns/primitives/factories/estimator_factory.py b/qiskit_machine_learning/primitives/qnns/primitives/factories/estimator_factory.py deleted file mode 100644 index 6be57a319..000000000 --- a/qiskit_machine_learning/primitives/qnns/primitives/factories/estimator_factory.py +++ /dev/null @@ -1,16 +0,0 @@ -from abc import ABC, abstractmethod - -from qiskit.primitives import BaseEstimator - -class EstimatorFactory(ABC): - """Class to construct an estimator, given circuits and observables.""" - - def __init__(self, circuits=None, observables=None, parameters=None): - self.circuits = circuits - self.observables = observables - self.parameters = parameters - - @abstractmethod - def __call__(self, circuits=None, observables=None, parameters=None) -> BaseEstimator: - pass - diff --git a/qiskit_machine_learning/primitives/qnns/primitives/factories/statevector_estimator_factory.py b/qiskit_machine_learning/primitives/qnns/primitives/factories/statevector_estimator_factory.py deleted file mode 100644 index 3c9b95c8a..000000000 --- a/qiskit_machine_learning/primitives/qnns/primitives/factories/statevector_estimator_factory.py +++ /dev/null @@ -1,13 +0,0 @@ -from qiskit.primitives import Estimator -from .estimator_factory import EstimatorFactory - -class StatevectorEstimatorFactory(EstimatorFactory): - """Estimator factory evaluated with statevector simulations.""" - - def __call__(self, circuits=None, observables=None, parameters=None) -> Estimator: - circuits = circuits or self.circuits - observables = observables or self.observables - parameters = parameters or self.parameters - - return Estimator(circuits, observables, parameters) - diff --git a/qiskit_machine_learning/primitives/qnns/primitives/gradient/__init__.py b/qiskit_machine_learning/primitives/qnns/primitives/gradient/__init__.py deleted file mode 100755 index 687a25aae..000000000 --- a/qiskit_machine_learning/primitives/qnns/primitives/gradient/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# 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. - -from .finite_diff_estimator_gradient import FiniteDiffEstimatorGradient -from .finite_diff_sampler_gradient import FiniteDiffSamplerGradient diff --git a/qiskit_machine_learning/primitives/qnns/primitives/gradient/base_estimator_gradient.py b/qiskit_machine_learning/primitives/qnns/primitives/gradient/base_estimator_gradient.py deleted file mode 100644 index f38f4fa0a..000000000 --- a/qiskit_machine_learning/primitives/qnns/primitives/gradient/base_estimator_gradient.py +++ /dev/null @@ -1,37 +0,0 @@ -# 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. - -""" -Abstract Base class of Gradient. -""" - -from __future__ import annotations - -from abc import ABC, abstractmethod -from collections.abc import Sequence - -from ..base_estimator import BaseEstimator - - -class BaseEstimatorGradient(ABC): - def __init__(self, estimator: BaseEstimator): - self._estimator = estimator - - @abstractmethod - def gradient( - self, - circuit_indices: Sequence[int], - observable_indices: Sequence[int], - parameter_values: Sequence[Sequence[float]], - **run_options, - ) -> EstimatorResult: - ... diff --git a/qiskit_machine_learning/primitives/qnns/primitives/gradient/finite_diff_estimator_gradient.py b/qiskit_machine_learning/primitives/qnns/primitives/gradient/finite_diff_estimator_gradient.py deleted file mode 100644 index ab61934b0..000000000 --- a/qiskit_machine_learning/primitives/qnns/primitives/gradient/finite_diff_estimator_gradient.py +++ /dev/null @@ -1,56 +0,0 @@ -# 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. - -from __future__ import annotations - -from collections.abc import Sequence - -import numpy as np - -from ..base_estimator import BaseEstimator -from ..estimator_result import EstimatorResult -from .base_estimator_gradient import BaseEstimatorGradient - - -class FiniteDiffEstimatorGradient(BaseEstimatorGradient): - def __init__(self, estimator: BaseEstimator, epsilon: float = 1e-6): - self._epsilon = epsilon - super().__init__(estimator) - - def gradient( - self, - circuit_index: int, - observable_index: int, - parameter_value: Sequence[float], - **run_options, - ) -> EstimatorResult: - run_options = run_options.copy() - - dim = len(parameter_value) - ret = [parameter_value] - for i in range(dim): - ei = parameter_value.copy() - ei[i] += self._epsilon - ret.append(ei) - param_array = np.array(ret).tolist() - circuit_indices = [circuit_index] * (dim + 1) - observable_indices = [observable_index] * (dim + 1) - results = self._estimator.__call__( - circuit_indices, observable_indices, param_array, **run_options - ) - - values = results.values - grad = np.zeros(dim) - f_ref = values[0] - for i, f_i in enumerate(values[1:]): - grad[i] = (f_i - f_ref) / self._epsilon - return EstimatorResult(values=grad, metadata=[{}] * len(grad)) diff --git a/qiskit_machine_learning/primitives/qnns/primitives/gradient/finite_diff_sampler_gradient.py b/qiskit_machine_learning/primitives/qnns/primitives/gradient/finite_diff_sampler_gradient.py deleted file mode 100644 index c40131e40..000000000 --- a/qiskit_machine_learning/primitives/qnns/primitives/gradient/finite_diff_sampler_gradient.py +++ /dev/null @@ -1,55 +0,0 @@ -# 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. - -from __future__ import annotations - -from collections.abc import Sequence - -import numpy as np - -from qiskit.result import QuasiDistribution - -from ..base_sampler import BaseSampler -from ..sampler_result import SamplerResult - - -class FiniteDiffSamplerGradient: - def __init__(self, sampler: BaseSampler, epsilon: float = 1e-6): - self._epsilon = epsilon - self._sampler = sampler - - def gradient( - self, - circuit_index: int, - parameter_value: Sequence[float], - **run_options, - ) -> SamplerResult: - run_options = run_options.copy() - - dim = len(parameter_value) - params = [parameter_value] - for i in range(dim): - ei = parameter_value.copy() - ei[i] += self._epsilon - params.append(ei) - param_list = np.array(params).tolist() - circuit_indices = [circuit_index] * (dim + 1) - results = self._sampler.__call__(circuit_indices, param_list, **run_options) - - quasi_dists = results.quasi_dists - ret = [] - f_ref = quasi_dists[0] - for f_i in quasi_dists[1:]: - ret.append( - QuasiDistribution({key: (f_i[key] - f_ref[key]) / self._epsilon for key in f_ref}) - ) - return SamplerResult(quasi_dists=ret, metadata=[{}] * len(ret)) diff --git a/qiskit_machine_learning/primitives/qnns/primitives/gradient/lin_comb_estimator_gradient.py b/qiskit_machine_learning/primitives/qnns/primitives/gradient/lin_comb_estimator_gradient.py deleted file mode 100644 index 40d5604f1..000000000 --- a/qiskit_machine_learning/primitives/qnns/primitives/gradient/lin_comb_estimator_gradient.py +++ /dev/null @@ -1,224 +0,0 @@ -# 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. -""" -Gradient of expectation values with linear combination of unitaries (LCU) -""" - -from __future__ import annotations - -from collections import defaultdict -from dataclasses import dataclass -from typing import Sequence, Type - -import numpy as np - -from qiskit import transpile -from qiskit.circuit import ( - Gate, - Instruction, - Parameter, - ParameterExpression, - QuantumCircuit, - QuantumRegister, -) -from qiskit.circuit.library.standard_gates import ( - CXGate, - CYGate, - CZGate, - RXGate, - RXXGate, - RYGate, - RYYGate, - RZGate, - RZXGate, - RZZGate, -) -from qiskit.quantum_info import Pauli, SparsePauliOp - -from ..base_estimator import BaseEstimator -from ..estimator_result import EstimatorResult -from .base_estimator_gradient import BaseEstimatorGradient - -Pauli_Z = Pauli("Z") - - -@dataclass -class SubEstimator: - coeff: float | ParameterExpression - circuit: QuantumCircuit - index: int - - -class LinCombEstimatorGradient(BaseEstimatorGradient): - """LCU estimator gradient""" - - SUPPORTED_GATES = [ - "rx", - "ry", - "rz", - "rzx", - "rzz", - "ryy", - "rxx", - "cx", - "cy", - "cz", - "ccx", - "swap", - "iswap", - "h", - "t", - "s", - "sdg", - "x", - "y", - "z", - ] - - def __init__( - self, estimator: Type[BaseEstimator], circuit: QuantumCircuit, observable: SparsePauliOp - ): - self._circuit = circuit - self._observable = observable - self._grad, observable = self._preprocessing() - circuits = [self._circuit] - observables = [self._observable, observable] - for param, lst in self._grad.items(): - for arg in lst: - circuits.append(arg.circuit) - super().__init__(estimator(circuits=circuits, observables=observables)) - - def __enter__(self): - return self - - def __exit__(self, *exc_info): - pass - - @classmethod - def _gradient_circuits(cls, circuit: QuantumCircuit): - circuit2 = transpile(circuit, basis_gates=cls.SUPPORTED_GATES, optimization_level=0) - qr_superpos = QuantumRegister(1, "superpos") - circuit2.add_register(qr_superpos) - circuit2.h(qr_superpos) - circuit2.data.insert(0, circuit2.data.pop()) - circuit2.sdg(qr_superpos) - circuit2.data.insert(1, circuit2.data.pop()) - ret = defaultdict(list) - for i, (inst, qregs, _) in enumerate(circuit2.data): - if inst.is_parameterized(): - param = inst.params[0] - for p in param.parameters: - gate = cls._gate_gradient(inst) - circuit3 = circuit2.copy() - # insert `gate` to i-th position - circuit3.append(gate, [qr_superpos[0]] + qregs, []) - circuit3.data.insert(i, circuit3.data.pop()) - # - circuit3.h(qr_superpos) - ret[p].append((circuit3, param.gradient(p))) - return ret - - def _preprocessing(self): - grad = self._gradient_circuits(self._circuit) - ret = {} - observable = self._observable.expand(Pauli_Z) - index = 1 - for param in self._circuit.parameters: - lst = [] - for circ, coeff in grad[param]: - lst.append(SubEstimator(coeff=coeff, circuit=circ, index=index)) - index += 1 - ret[param] = lst - return ret, observable - - def __call__( - self, parameter_values: Sequence[Sequence[float]], **run_options - ) -> EstimatorResult: - return self._estimator([0], [0], parameter_values, **run_options) - - def gradient( - self, - parameter_value: Sequence[float], - partial: Sequence[Parameter] | None = None, - **run_options, - ) -> EstimatorResult: - parameters = partial or self._circuit.parameters - - param_map = {} - for j, param in enumerate(self._circuit.parameters): - param_map[param] = parameter_value[j] - - circ_indices = [] - for param in parameters: - circ_indices.extend([f.index for f in self._grad[param]]) - size = len(circ_indices) - results = self._estimator(circ_indices, [1] * size, [parameter_value] * size, **run_options) - - param_set = set(parameters) - values = np.zeros(len(parameter_value)) - metadata = [{} for _ in range(len(parameters))] - i = 0 - for j, (param, lst) in enumerate(self._grad.items()): - if param not in param_set: - continue - for subest in lst: - coeff = subest.coeff - if isinstance(coeff, ParameterExpression): - local_map = {param: param_map[param] for param in coeff.parameters} - bound_coeff = coeff.bind(local_map) - else: - bound_coeff = coeff - values[j] += bound_coeff * results.values[i] - i += 1 - - return EstimatorResult(values=values, metadata=metadata) - - @staticmethod - def _gate_gradient(gate: Gate) -> Instruction: - if isinstance(gate, RXGate): - # theta - return CXGate() - if isinstance(gate, RYGate): - # theta - return CYGate() - if isinstance(gate, RZGate): - # theta - return CZGate() - if isinstance(gate, RXXGate): - # theta - cxx_circ = QuantumCircuit(3) - cxx_circ.cx(0, 1) - cxx_circ.cx(0, 2) - cxx = cxx_circ.to_instruction() - return cxx - if isinstance(gate, RYYGate): - # theta - cyy_circ = QuantumCircuit(3) - cyy_circ.cy(0, 1) - cyy_circ.cy(0, 2) - cyy = cyy_circ.to_instruction() - return cyy - if isinstance(gate, RZZGate): - # theta - czz_circ = QuantumCircuit(3) - czz_circ.cz(0, 1) - czz_circ.cz(0, 2) - czz = czz_circ.to_instruction() - return czz - if isinstance(gate, RZXGate): - # theta - czx_circ = QuantumCircuit(3) - czx_circ.cx(0, 2) - czx_circ.cz(0, 1) - czx = czx_circ.to_instruction() - return czx - raise TypeError(f"Unrecognized parameterized gate, {gate}") diff --git a/qiskit_machine_learning/primitives/qnns/primitives/gradient/lin_comb_sampler_gradient.py b/qiskit_machine_learning/primitives/qnns/primitives/gradient/lin_comb_sampler_gradient.py deleted file mode 100644 index 0c7f3409f..000000000 --- a/qiskit_machine_learning/primitives/qnns/primitives/gradient/lin_comb_sampler_gradient.py +++ /dev/null @@ -1,221 +0,0 @@ -# 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. -""" -Gradient of probabilities with linear combination of unitaries (LCU) -""" - -from __future__ import annotations - -from collections import Counter, defaultdict -from dataclasses import dataclass -from typing import Sequence, Type - -from qiskit import transpile -from qiskit.circuit import ( - ClassicalRegister, - Gate, - Instruction, - Parameter, - ParameterExpression, - QuantumCircuit, - QuantumRegister, -) -from qiskit.circuit.library.standard_gates import ( - CXGate, - CYGate, - CZGate, - RXGate, - RXXGate, - RYGate, - RYYGate, - RZGate, - RZXGate, - RZZGate, -) -from qiskit.result import QuasiDistribution - -from ..base_sampler import BaseSampler -from ..sampler_result import SamplerResult - - -@dataclass -class SubEstimator: - coeff: float | ParameterExpression - circuit: QuantumCircuit - index: int - - -class LinCombSamplerGradient: - """LCU sampler gradient""" - - SUPPORTED_GATES = [ - "rx", - "ry", - "rz", - "rzx", - "rzz", - "ryy", - "rxx", - "cx", - "cy", - "cz", - "ccx", - "swap", - "iswap", - "h", - "t", - "s", - "sdg", - "x", - "y", - "z", - ] - - def __init__(self, sampler: Type[BaseSampler], circuit: QuantumCircuit): - self._circuit = circuit - self._grad = self._preprocessing() - circuits = [self._circuit] - for param, lst in self._grad.items(): - for arg in lst: - circuits.append(arg.circuit) - self._sampler = sampler(circuits=circuits) - - def __enter__(self): - return self - - def __exit__(self, *exc_info): - pass - - @classmethod - def _gradient_circuits(cls, circuit: QuantumCircuit): - circuit2 = transpile(circuit, basis_gates=cls.SUPPORTED_GATES, optimization_level=0) - qr_superpos = QuantumRegister(1, "superpos") - cr_superpos = ClassicalRegister(1, "superpos") - circuit2.add_register(qr_superpos) - circuit2.add_bits(cr_superpos) - circuit2.h(qr_superpos) - circuit2.data.insert(0, circuit2.data.pop()) - circuit2.sdg(qr_superpos) - circuit2.data.insert(1, circuit2.data.pop()) - ret = defaultdict(list) - for i, (inst, qregs, _) in enumerate(circuit2.data): - if inst.is_parameterized(): - param = inst.params[0] - for p in param.parameters: - gate = cls._gate_gradient(inst) - circuit3 = circuit2.copy() - # insert `gate` to i-th position - circuit3.append(gate, [qr_superpos[0]] + qregs, []) - circuit3.data.insert(i, circuit3.data.pop()) - # - circuit3.h(qr_superpos) - circuit3.measure(qr_superpos, cr_superpos) - ret[p].append((circuit3, param.gradient(p))) - return ret - - def _preprocessing(self): - grad = self._gradient_circuits(self._circuit) - ret = {} - index = 1 - for param in self._circuit.parameters: - lst = [] - for circ, coeff in grad[param]: - lst.append(SubEstimator(coeff=coeff, circuit=circ, index=index)) - index += 1 - ret[param] = lst - return ret - - def __call__(self, parameter_values: Sequence[Sequence[float]], **run_options) -> SamplerResult: - return self._sampler([0], parameter_values, **run_options) - - def gradient( - self, - parameter_value: Sequence[float], - partial: Sequence[Parameter] | None = None, - **run_options, - ) -> SamplerResult: - parameters = partial or self._circuit.parameters - - param_map = {} - for j, param in enumerate(self._circuit.parameters): - param_map[param] = parameter_value[j] - - circ_indices = [] - for param in parameters: - circ_indices.extend([f.index for f in self._grad[param]]) - size = len(circ_indices) - results = self._sampler(circ_indices, [parameter_value] * size, **run_options) - - param_set = set(parameters) - dists = [Counter() for _ in range(len(parameter_value))] - metadata = [{} for _ in range(len(parameters))] - num_bitstrings = 2**self._circuit.num_qubits - i = 0 - for j, (param, lst) in enumerate(self._grad.items()): - if param not in param_set: - continue - for subest in lst: - coeff = subest.coeff - if isinstance(coeff, ParameterExpression): - local_map = {param: param_map[param] for param in coeff.parameters} - bound_coeff = float(coeff.bind(local_map)) - else: - bound_coeff = coeff - for k, v in results.quasi_dists[i].items(): - sign, k2 = divmod(k, num_bitstrings) - dists[j][k2] += (-1) ** sign * bound_coeff * v - i += 1 - - return SamplerResult( - quasi_dists=[QuasiDistribution(dist) for dist in dists], metadata=metadata - ) - - @staticmethod - def _gate_gradient(gate: Gate) -> Instruction: - if isinstance(gate, RXGate): - # theta - return CXGate() - if isinstance(gate, RYGate): - # theta - return CYGate() - if isinstance(gate, RZGate): - # theta - return CZGate() - if isinstance(gate, RXXGate): - # theta - cxx_circ = QuantumCircuit(3) - cxx_circ.cx(0, 1) - cxx_circ.cx(0, 2) - cxx = cxx_circ.to_instruction() - return cxx - if isinstance(gate, RYYGate): - # theta - cyy_circ = QuantumCircuit(3) - cyy_circ.cy(0, 1) - cyy_circ.cy(0, 2) - cyy = cyy_circ.to_instruction() - return cyy - if isinstance(gate, RZZGate): - # theta - czz_circ = QuantumCircuit(3) - czz_circ.cz(0, 1) - czz_circ.cz(0, 2) - czz = czz_circ.to_instruction() - return czz - if isinstance(gate, RZXGate): - # theta - czx_circ = QuantumCircuit(3) - czx_circ.cx(0, 2) - czx_circ.cz(0, 1) - czx = czx_circ.to_instruction() - return czx - raise TypeError(f"Unrecognized parameterized gate, {gate}") diff --git a/qiskit_machine_learning/primitives/qnns/primitives/gradient/param_shift_estimator_gradient.py b/qiskit_machine_learning/primitives/qnns/primitives/gradient/param_shift_estimator_gradient.py deleted file mode 100644 index b112e85ff..000000000 --- a/qiskit_machine_learning/primitives/qnns/primitives/gradient/param_shift_estimator_gradient.py +++ /dev/null @@ -1,139 +0,0 @@ -# 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. -""" -Gradient of expectation values with parameter shift -""" - -from __future__ import annotations - -from collections import defaultdict -from dataclasses import dataclass -from typing import Sequence, Type - -import numpy as np - -from qiskit import transpile -from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit -from qiskit.quantum_info import SparsePauliOp - -from ..base_estimator import BaseEstimator -from ..estimator_result import EstimatorResult -from .base_estimator_gradient import BaseEstimatorGradient - - -@dataclass -class SubEstimator: - coeff: float | ParameterExpression - circuit: QuantumCircuit - observable: SparsePauliOp - index: int - - -class ParamShiftEstimatorGradient(BaseEstimatorGradient): - """Parameter shift estimator gradient""" - - SUPPORTED_GATES = ["x", "y", "z", "h", "rx", "ry", "rz", "p", "cx", "cy", "cz"] - - def __init__( - self, estimator: Type[BaseEstimator], circuit: QuantumCircuit, observable: SparsePauliOp - ): - self._circuit = circuit - self._observable = observable - self._grad = self._preprocessing() - circuits = [self._circuit] - observables = [self._observable] - for param, lst in self._grad.items(): - for arg in lst: - circuits.append(arg.circuit) - super().__init__(estimator(circuits=circuits, observables=observables)) - - def __enter__(self): - return self - - def __exit__(self, *exc_info): - pass - - @classmethod - def _gradient_circuits(cls, circuit: QuantumCircuit): - circuit2 = transpile(circuit, basis_gates=cls.SUPPORTED_GATES, optimization_level=0) - ret = defaultdict(list) - for inst in circuit2.data: - if inst[0].is_parameterized(): - param = inst[0].params[0] - for p in param.parameters: - # TODO: Need to wait for an appropriate way to update parameters of - # a particular instruction. - # See https://github.com/Qiskit/qiskit-terra/issues/7894 - inst[0].params[0] = param + np.pi / 2 - ret[p].append((circuit2.copy(), param.gradient(p) / 2)) - inst[0].params[0] = param - np.pi / 2 - ret[p].append((circuit2.copy(), -param.gradient(p) / 2)) - inst[0].params[0] = param - return ret - - def _preprocessing(self): - grad = self._gradient_circuits(self._circuit) - ret = {} - index = 1 - for param in self._circuit.parameters: - lst = [] - for circ, coeff in grad[param]: - lst.append( - SubEstimator( - coeff=coeff, circuit=circ, observable=self._observable, index=index - ) - ) - index += 1 - ret[param] = lst - return ret - - def __call__( - self, parameter_values: Sequence[Sequence[float]], **run_options - ) -> EstimatorResult: - return self._estimator([0], [0], parameter_values, **run_options) - - def gradient( - self, - parameter_value: Sequence[float], - partial: Sequence[Parameter] | None = None, - **run_options, - ) -> EstimatorResult: - parameters = partial or self._circuit.parameters - - param_map = {} - for j, param in enumerate(self._circuit.parameters): - param_map[param] = parameter_value[j] - - circ_indices = [] - for param in parameters: - circ_indices.extend([f.index for f in self._grad[param]]) - size = len(circ_indices) - results = self._estimator(circ_indices, [0] * size, [parameter_value] * size, **run_options) - - param_set = set(parameters) - values = np.zeros(len(parameter_value)) - metadata = [{} for _ in range(len(parameters))] - i = 0 - for j, (param, lst) in enumerate(self._grad.items()): - if param not in param_set: - continue - for subest in lst: - coeff = subest.coeff - if isinstance(coeff, ParameterExpression): - local_map = {param: param_map[param] for param in coeff.parameters} - bound_coeff = coeff.bind(local_map) - else: - bound_coeff = coeff - values[j] += bound_coeff * results.values[i] - i += 1 - - return EstimatorResult(values=values, metadata=metadata) diff --git a/qiskit_machine_learning/primitives/qnns/primitives/gradient/param_shift_sampler_gradient.py b/qiskit_machine_learning/primitives/qnns/primitives/gradient/param_shift_sampler_gradient.py deleted file mode 100644 index 8c8b6f938..000000000 --- a/qiskit_machine_learning/primitives/qnns/primitives/gradient/param_shift_sampler_gradient.py +++ /dev/null @@ -1,134 +0,0 @@ -# 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. -""" -Gradient of probabilities with parameter shift -""" - -from __future__ import annotations - -from collections import Counter, defaultdict -from dataclasses import dataclass -from typing import Sequence, Type - -import numpy as np - -from qiskit import transpile -from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit -from qiskit.result import QuasiDistribution - -from ..base_sampler import BaseSampler -from ..sampler_result import SamplerResult - - -@dataclass -class SubSampler: - coeff: float | ParameterExpression - circuit: QuantumCircuit - index: int - - -class ParamShiftSamplerGradient: - """Parameter shift estimator gradient""" - - SUPPORTED_GATES = ["x", "y", "z", "h", "rx", "ry", "rz", "p", "cx", "cy", "cz"] - - def __init__(self, sampler: Type[BaseSampler], circuit: QuantumCircuit): - self._circuit = circuit - self._grad = self._preprocessing() - circuits = [self._circuit] - for param, lst in self._grad.items(): - for arg in lst: - circuits.append(arg.circuit) - self._sampler = sampler(circuits=circuits) - - def __enter__(self): - return self - - def __exit__(self, *exc_info): - pass - - @classmethod - def _gradient_circuits(cls, circuit: QuantumCircuit): - circuit2 = transpile(circuit, basis_gates=cls.SUPPORTED_GATES, optimization_level=0) - ret = defaultdict(list) - for inst in circuit2.data: - if inst[0].is_parameterized(): - param = inst[0].params[0] - for p in param.parameters: - # TODO: Need to wait for an appropriate way to update parameters of - # a particular instruction. - # See https://github.com/Qiskit/qiskit-terra/issues/7894 - inst[0].params[0] = param + np.pi / 2 - ret[p].append((circuit2.copy(), param.gradient(p) / 2)) - inst[0].params[0] = param - np.pi / 2 - ret[p].append((circuit2.copy(), -param.gradient(p) / 2)) - inst[0].params[0] = param - return ret - - def _preprocessing(self): - grad = self._gradient_circuits(self._circuit) - # print("gradient circuits: ", grad) - ret = {} - index = 1 - for param in self._circuit.parameters: - lst = [] - for circ, coeff in grad[param]: - lst.append(SubSampler(coeff=coeff, circuit=circ, index=index)) - index += 1 - ret[param] = lst - return ret - - def __call__(self, parameter_values: Sequence[Sequence[float]], **run_options) -> SamplerResult: - return self._sampler([0], parameter_values, **run_options) - - def gradient( - self, - parameter_value: Sequence[float], - partial: Sequence[Parameter] | None = None, - **run_options, - ) -> SamplerResult: - - parameters = partial or self._circuit.parameters - - param_map = {} - - for j, param in enumerate(self._circuit.parameters): - param_map[param] = parameter_value[j] - - circ_indices = [] - for param in parameters: - circ_indices.extend([f.index for f in self._grad[param]]) - size = len(circ_indices) - results = self._sampler(circ_indices, [parameter_value] * size, **run_options) - - param_set = set(parameters) - dists = [Counter() for _ in range(len(parameter_value))] - metadata = [{} for _ in range(len(parameters))] - i = 0 - for j, (param, lst) in enumerate(self._grad.items()): - if param not in param_set: - continue - for subest in lst: - coeff = subest.coeff - if isinstance(coeff, ParameterExpression): - local_map = {param: param_map[param] for param in coeff.parameters} - bound_coeff = float(coeff.bind(local_map)) - else: - bound_coeff = coeff - dists[j].update( - Counter({k: bound_coeff * v for k, v in results.quasi_dists[i].items()}) - ) - i += 1 - - return SamplerResult( - quasi_dists=[QuasiDistribution(dist) for dist in dists], metadata=metadata - ) diff --git a/qiskit_machine_learning/primitives/qnns/primitives/sampler.py b/qiskit_machine_learning/primitives/qnns/primitives/sampler.py deleted file mode 100644 index cecb0c948..000000000 --- a/qiskit_machine_learning/primitives/qnns/primitives/sampler.py +++ /dev/null @@ -1,115 +0,0 @@ -# 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. -""" -Sampler class -""" - -from __future__ import annotations - -from collections.abc import Iterable, Sequence -from typing import cast - -import numpy as np - -from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.exceptions import QiskitError -from qiskit.quantum_info import Statevector -from qiskit.result import QuasiDistribution - -from .base_sampler import BaseSampler -from .sampler_result import SamplerResult -from .utils import final_measurement_mapping, init_circuit - - -class Sampler(BaseSampler): - """ - Sampler class - """ - - def __init__( - self, - circuits: QuantumCircuit | Iterable[QuantumCircuit], - parameters: Iterable[Iterable[Parameter]] | None = None, - ): - """ - Args: - circuits: circuits to be executed - parameters: Parameters of each of the quantum circuits. - Defaults to ``[circ.parameters for circ in circuits]``. - - Raises: - QiskitError: if some classical bits are not used for measurements. - """ - if isinstance(circuits, QuantumCircuit): - circuits = [circuits] - circuits = [init_circuit(circuit) for circuit in circuits] - q_c_mappings = [final_measurement_mapping(circuit) for circuit in circuits] - self._qargs_list = [] - for circuit, q_c_mapping in zip(circuits, q_c_mappings): - if set(range(circuit.num_clbits)) != set(q_c_mapping.values()): - raise QiskitError( - "some classical bits are not used for measurements." - f" the number of classical bits {circuit.num_clbits}," - f" the used classical bits {set(q_c_mapping.values())}." - ) - c_q_mapping = sorted((c, q) for q, c in q_c_mapping.items()) - self._qargs_list.append([q for _, q in c_q_mapping]) - circuits = [circuit.remove_final_measurements(inplace=False) for circuit in circuits] - super().__init__(circuits, parameters) - self._is_closed = False - - def __call__( - self, - circuit_indices: Sequence[int] | None = None, - parameter_values: Sequence[Sequence[float]] | Sequence[float] | None = None, - **run_options, - ) -> SamplerResult: - if self._is_closed: - raise QiskitError("The primitive has been closed.") - - if isinstance(parameter_values, np.ndarray): - parameter_values = parameter_values.tolist() - if parameter_values and not isinstance(parameter_values[0], (np.ndarray, Sequence)): - parameter_values = cast("Sequence[float]", parameter_values) - parameter_values = [parameter_values] - if circuit_indices is None: - circuit_indices = list(range(len(self._circuits))) - if parameter_values is None: - parameter_values = [[]] * len(circuit_indices) - if len(circuit_indices) != len(parameter_values): - raise QiskitError( - f"The number of circuit indices ({len(circuit_indices)}) does not match " - f"the number of parameter value sets ({len(parameter_values)})." - ) - - bound_circuits_qargs = [] - for i, value in zip(circuit_indices, parameter_values): - if len(value) != len(self._parameters[i]): - raise QiskitError( - f"The number of values ({len(value)}) does not match " - f"the number of parameters ({len(self._parameters[i])})." - ) - bound_circuits_qargs.append( - ( - self._circuits[i].bind_parameters(dict(zip(self._parameters[i], value))), - self._qargs_list[i], - ) - ) - probabilities = [ - Statevector(circ).probabilities(qargs=qargs) for circ, qargs in bound_circuits_qargs - ] - quasis = [QuasiDistribution(dict(enumerate(p))) for p in probabilities] - - return SamplerResult(quasis, [{}] * len(circuit_indices)) - - def close(self): - self._is_closed = True diff --git a/qiskit_machine_learning/primitives/qnns/primitives/sampler_result.py b/qiskit_machine_learning/primitives/qnns/primitives/sampler_result.py deleted file mode 100644 index 5b53b1066..000000000 --- a/qiskit_machine_learning/primitives/qnns/primitives/sampler_result.py +++ /dev/null @@ -1,43 +0,0 @@ -# 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. -""" -Sampler result class -""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any - -from qiskit.result import QuasiDistribution - - -@dataclass(frozen=True) -class SamplerResult: - """Result of Sampler. - - .. code-block:: python - - result = sampler(circuits, params) - - where the i-th elements of ``result`` correspond to the circuit given by ``circuit_indices[i]``, - and the parameter values bounds by ``params[i]``. - For example, ``results.quasi_dists[i]`` gives the quasi-probabilities of bitstrings, and - ``result.metadata[i]`` is a metadata dictionary for this circuit and parameters. - - Args: - quasi_dists (list[QuasiDistribution]): List of the quasi-probabilities. - metadata (list[dict]): List of the metadata. - """ - - quasi_dists: list[QuasiDistribution] - metadata: list[dict[str, Any]] diff --git a/qiskit_machine_learning/primitives/qnns/primitives/utils.py b/qiskit_machine_learning/primitives/qnns/primitives/utils.py deleted file mode 100644 index e5abc4120..000000000 --- a/qiskit_machine_learning/primitives/qnns/primitives/utils.py +++ /dev/null @@ -1,113 +0,0 @@ -# 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. -""" -Utility functions for primitives -""" - -from __future__ import annotations - -from qiskit.circuit import ParameterExpression, QuantumCircuit -from qiskit.extensions.quantum_initializer.initializer import Initialize -from qiskit.opflow import PauliSumOp -from qiskit.quantum_info import SparsePauliOp, Statevector -from qiskit.quantum_info.operators.base_operator import BaseOperator -from qiskit.quantum_info.operators.symplectic.base_pauli import BasePauli - - -def init_circuit(state: QuantumCircuit | Statevector) -> QuantumCircuit: - """Initialize state by converting the input to a quantum circuit. - - Args: - state: The state as quantum circuit or statevector. - - Returns: - The state as quantum circuit. - """ - if isinstance(state, QuantumCircuit): - return state - if not isinstance(state, Statevector): - state = Statevector(state) - qc = QuantumCircuit(state.num_qubits) - qc.append(Initialize(state), qargs=range(state.num_qubits)) - return qc - - -def init_observable(observable: BaseOperator | PauliSumOp) -> SparsePauliOp: - """Initialize observable by converting the input to a :class:`~qiskit.quantum_info.SparsePauliOp`. - - Args: - observable: The observable. - - Returns: - The observable as :class:`~qiskit.quantum_info.SparsePauliOp`. - - Raises: - TypeError: If the observable is a :class:`~qiskit.opflow.PauliSumOp` and has a parameterized - coefficient. - """ - if isinstance(observable, SparsePauliOp): - return observable - elif isinstance(observable, PauliSumOp): - if isinstance(observable.coeff, ParameterExpression): - raise TypeError( - f"Observable must have numerical coefficient, not {type(observable.coeff)}." - ) - return observable.coeff * observable.primitive - elif isinstance(observable, BasePauli): - return SparsePauliOp(observable) - elif isinstance(observable, BaseOperator): - return SparsePauliOp.from_operator(observable) - else: - return SparsePauliOp(observable) - - -def final_measurement_mapping(circuit: QuantumCircuit) -> dict[int, int]: - """Return the final measurement mapping for the circuit. - - Dict keys label measured qubits, whereas the values indicate the - classical bit onto which that qubits measurement result is stored. - - Note: this function is a slightly simplified version of a utility function - ``_final_measurement_mapping`` of - `mthree `_. - - Parameters: - circuit: Input quantum circuit. - - Returns: - Mapping of qubits to classical bits for final measurements. - """ - active_qubits = list(range(circuit.num_qubits)) - active_cbits = list(range(circuit.num_clbits)) - - # Find final measurements starting in back - mapping = {} - for item in circuit._data[::-1]: - if item[0].name == "measure": - cbit = circuit.find_bit(item[2][0]).index - qbit = circuit.find_bit(item[1][0]).index - if cbit in active_cbits and qbit in active_qubits: - mapping[qbit] = cbit - active_cbits.remove(cbit) - active_qubits.remove(qbit) - elif item[0].name != "barrier": - for qq in item[1]: - _temp_qubit = circuit.find_bit(qq).index - if _temp_qubit in active_qubits: - active_qubits.remove(_temp_qubit) - - if not active_cbits or not active_qubits: - break - - # Sort so that classical bits are in numeric order low->high. - mapping = dict(sorted(mapping.items(), key=lambda item: item[1])) - return mapping diff --git a/qiskit_machine_learning/primitives/qnns/sampler-qnn-example.py b/qiskit_machine_learning/primitives/qnns/sampler-qnn-example.py deleted file mode 100644 index 7ff555ea9..000000000 --- a/qiskit_machine_learning/primitives/qnns/sampler-qnn-example.py +++ /dev/null @@ -1,75 +0,0 @@ -# THIS EXAMPLE USES THE TERRA PRIMITIVES! -import numpy as np -from qiskit.primitives import Sampler, Estimator -from qiskit import Aer, QuantumCircuit -from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap -from qiskit.utils import QuantumInstance, algorithm_globals -from qiskit_machine_learning.neural_networks import CircuitQNN -from primitives.gradient.param_shift_sampler_gradient import ParamShiftSamplerGradient -from primitives.gradient.finite_diff_sampler_gradient import FiniteDiffSamplerGradient -algorithm_globals.random_seed = 42 - -# DEFINE CIRCUIT FOR SAMPLER -num_qubits = 3 -qc = RealAmplitudes(num_qubits, entanglement="linear", reps=1) -qc.draw(output="mpl") -# ADD MEASUREMENT HERE --> TRICKY -qc.measure_all() - -# --------------------- - -from qiskit import Aer -from qiskit.utils import QuantumInstance - -qi_qasm = QuantumInstance(Aer.get_backend("aer_simulator"), shots=10) -qi_sv = QuantumInstance(Aer.get_backend("statevector_simulator")) - -parity = lambda x: "{:b}".format(x).count("1") % 2 -output_shape = 2 # this is required in case of a callable with dense output - -qnn2 = CircuitQNN( - qc, - input_params=qc.parameters[:3], - weight_params=qc.parameters[3:], - sparse = False, - interpret = parity, - output_shape = output_shape, - quantum_instance=qi_sv, -) -inputs = np.asarray(algorithm_globals.random.random(size = (1, qnn2._num_inputs))) -weights = algorithm_globals.random.random(qnn2._num_weights) -print("inputs: ", inputs) -print("weights: ", weights) - -np.set_printoptions(precision=2) -f = qnn2.forward(inputs, weights) -print( f"fwd pass: {f}") -np.set_printoptions(precision=2) -b = qnn2.backward(inputs, weights) -print( f"bkwd pass: {b}" ) -# --------------------- - -# IMPORT QNN -from neural_networks.sampler_qnn_2 import SamplerQNN - -with SamplerQNN( - circuit=qc, - input_params=qc.parameters[:3], - weight_params=qc.parameters[3:], - sampler_factory=Sampler, - interpret = parity, - output_shape = output_shape, - ) as qnn: - # inputs = np.asarray(algorithm_globals.random.random((2, qnn._num_inputs))) - # weights = algorithm_globals.random.random(qnn._num_weights) - print("inputs: ", inputs) - print("weights: ", weights) - - np.set_printoptions(precision=2) - f = qnn.forward(inputs, weights) - print(f"fwd pass: {f}") - np.set_printoptions(precision=2) - b = qnn.backward(inputs, weights) - print(f"bkwd pass: {b}") - - diff --git a/qiskit_machine_learning/primitives/qnns/neural_networks/sampler_qnn.py b/qiskit_machine_learning/primitives/sampler_qnn.py similarity index 83% rename from qiskit_machine_learning/primitives/qnns/neural_networks/sampler_qnn.py rename to qiskit_machine_learning/primitives/sampler_qnn.py index 9d3f3000a..dd344e0db 100644 --- a/qiskit_machine_learning/primitives/qnns/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/primitives/sampler_qnn.py @@ -1,21 +1,30 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020, 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. + +"""A Neural Network implementation based on the Sampler primitive.""" + import logging from numbers import Integral import numpy as np from typing import Optional, Union, List, Tuple, Callable, cast, Iterable + from qiskit import QuantumCircuit from qiskit.circuit import Parameter from qiskit_machine_learning.exceptions import QiskitMachineLearningError, QiskitError logger = logging.getLogger(__name__) -# from primitives.gradient import FiniteDiffEstimatorGradient, FiniteDiffSamplerGradient - -from scipy.sparse import coo_matrix - -from primitives.gradient.param_shift_sampler_gradient import ParamShiftSamplerGradient -from qiskit.primitives import Sampler - class SamplerQNN(): + """A Neural Network implementation based on the Sampler primitive.""" def __init__( self, @@ -25,16 +34,10 @@ def __init__( interpret: Optional[Callable[[int], Union[int, Tuple[int, ...]]]] = None, output_shape: Union[int, Tuple[int, ...]] = None, sampler_factory: Callable = None, - gradient_method: str = "param_shift", + gradient_method: str = None, ): - # IGNORING SPARSE - # SKIPPING CUSTOM GRADIENT - # SKIPPING "INPUT GRADIENTS" -> by default with primitives? - - # we allow only one circuit at this moment self._circuit = circuit - # self._gradient = ParamShiftSamplerGradient(sampler, self._circuit) self._gradient_method = gradient_method self._sampler_factory = sampler_factory @@ -45,8 +48,10 @@ def __init__( self.output_shape = None self._num_inputs = len(self._input_params) self._num_weights = len(self._weight_params) + self.num_weights = self._num_weights - # the circuit must always have measurements.... (?) + + # the circuit must always have measurements... # add measurements in case none are given if len(self._circuit.clbits) == 0: self._circuit.measure_all() @@ -57,11 +62,9 @@ def __init__( # set interpret and compute output shape self.set_interpret(interpret, output_shape) + # not implemented yet self._input_gradients = None - # def output_shape(self): - # return self._output_shape - # return self._output_shape def __enter__(self): self.open() @@ -71,22 +74,31 @@ def __exit__(self, *exc_info): self.close() def open(self): - # we should delay instantiation of the primitives till they are really required + """ + Open sampler/gradient session. + """ + + # we should delay instantiation of the primitives until they are really required if self._gradient_method == "param_shift": # if gradient method -> sampler with gradient functionality - self._sampler = ParamShiftSamplerGradient( - circuit = self._circuit, - sampler = self._sampler_factory - ) + # self._sampler = ParamShiftSamplerGradient( + # circuit = self._circuit, + # sampler = self._sampler_factory + # ) + pass # waiting for gradients + else: # if no gradient method -> sampler without gradient functionality self._sampler = self._sampler_factory( - circuits = [self._circuit], + circuits = self._circuit, parameters = [self._input_params + self._weight_params] ) pass def close(self): + """ + Close sampler/gradient session. + """ self._sampler.__exit__() def set_interpret( @@ -94,15 +106,13 @@ def set_interpret( interpret: Optional[Callable[[int], Union[int, Tuple[int, ...]]]], output_shape: Union[int, Tuple[int, ...]] = None, ) -> None: - """Change 'interpret' and corresponding 'output_shape'. If self.sampling==True, the - output _shape does not have to be set and is inferred from the interpret function. - Otherwise, the output_shape needs to be given. + """Change 'interpret' and corresponding 'output_shape'. Args: interpret: A callable that maps the measured integer to another unsigned integer or tuple of unsigned integers. See constructor for more details. output_shape: The output shape of the custom interpretation, only used in the case - where an interpret function is provided and ``sampling==False``. See constructor + where an interpret function is provided. See constructor for more details. """ @@ -177,7 +187,7 @@ def _postprocess(self, num_samples, result): prob = np.zeros((num_samples, *self._output_shape)) for i in range(num_samples): - counts = result[i].quasi_dists[0] + counts = result.quasi_dists[i] print(counts) shots = sum(counts.values()) @@ -194,12 +204,14 @@ def _forward( parameter_values, num_samples = self._preprocess(input_data, weights) - # result = self._sampler([0] * num_samples, parameter_values) + # sampler allows batching (gradient doesn't) + results = self._sampler([0] * num_samples, parameter_values) - results = [] - for sample in range(num_samples): - result = self._sampler(parameter_values) - results.append(result) + # results = [] + # for sample in range(num_samples): + # result = self._sampler(circuits = self._circuit, + # parameter_values = parameter_values) + # results.append(result) result = self._postprocess(num_samples, results) @@ -211,8 +223,9 @@ def backward( weights: Optional[Union[List[float], np.ndarray, float]], ) -> Tuple[Optional[np.ndarray], Optional[np.ndarray],]: - result = self._backward(input_data, weights) - return result + # result = self._backward(input_data, weights) + # return result + pass def _preprocess_gradient(self, input_data, weights): @@ -303,3 +316,5 @@ def _backward( + + From ada284bb37282ee67cc9a07d345dfadfc70bff12 Mon Sep 17 00:00:00 2001 From: ElePT Date: Mon, 11 Jul 2022 13:12:49 +0200 Subject: [PATCH 09/61] Undo kernel changes --- qiskit_machine_learning/utils/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/qiskit_machine_learning/utils/__init__.py b/qiskit_machine_learning/utils/__init__.py index 0ac0473ae..000618000 100644 --- a/qiskit_machine_learning/utils/__init__.py +++ b/qiskit_machine_learning/utils/__init__.py @@ -26,7 +26,3 @@ loss_functions """ - -from .utils import make_2d - -__all__ = ["make_2d"] From b2d42f21829366e77ffd6894d88113c49b5d1154 Mon Sep 17 00:00:00 2001 From: ElePT Date: Thu, 14 Jul 2022 13:30:22 +0200 Subject: [PATCH 10/61] Start adding new type hint style --- .../primitives/sampler_qnn.py | 189 ++++++++++-------- test/primitives/test_sampler_qnn.py | 26 ++- 2 files changed, 122 insertions(+), 93 deletions(-) diff --git a/qiskit_machine_learning/primitives/sampler_qnn.py b/qiskit_machine_learning/primitives/sampler_qnn.py index dd344e0db..3ab503531 100644 --- a/qiskit_machine_learning/primitives/sampler_qnn.py +++ b/qiskit_machine_learning/primitives/sampler_qnn.py @@ -12,36 +12,60 @@ """A Neural Network implementation based on the Sampler primitive.""" +from __future__ import annotations import logging from numbers import Integral -import numpy as np from typing import Optional, Union, List, Tuple, Callable, cast, Iterable +import numpy as np + from qiskit import QuantumCircuit from qiskit.circuit import Parameter from qiskit_machine_learning.exceptions import QiskitMachineLearningError, QiskitError logger = logging.getLogger(__name__) -class SamplerQNN(): + +class SamplerQNN: """A Neural Network implementation based on the Sampler primitive.""" def __init__( - self, - circuit: QuantumCircuit, - input_params: Optional[List[Parameter]] = None, - weight_params: Optional[List[Parameter]] = None, - interpret: Optional[Callable[[int], Union[int, Tuple[int, ...]]]] = None, - output_shape: Union[int, Tuple[int, ...]] = None, - sampler_factory: Callable = None, - gradient_method: str = None, - - ): + self, + circuit: QuantumCircuit, + input_params: List[Parameter] | None = None, + weight_params: List[Parameter] | None = None, + interpret: Callable[[int], int | Tuple[int, ...]] | None = None, + output_shape: int | Tuple[int, ...] | None = None, + sampler_factory: Callable | None = None, + gradient_factory: Callable | str | None = None, + )-> None: + """ + + Args: + circuit: The parametrized quantum circuit that generates the samples of this network. + input_params: The parameters of the circuit corresponding to the input. + weight_params: The parameters of the circuit corresponding to the trainable weights. + interpret: A callable that maps the measured integer to another unsigned integer or + tuple of unsigned integers. These are used as new indices for the (potentially + sparse) output array. If no interpret function is + passed, then an identity function will be used by this neural network. + output_shape: The output shape of the custom interpretation + sampler_factory: Factory for sampler primitive + gradient_factory: String indicating pre-implemented gradient method or factory for + gradient class + input_gradients: to be added + Raises: + QiskitMachineLearningError: Invalid parameter values. + """ + self._circuit = circuit - self._gradient_method = gradient_method + self._gradient_factory = gradient_factory self._sampler_factory = sampler_factory + self._sampler = None + self._gradient = None + self._input_params = list(input_params or []) self._weight_params = list(weight_params or []) @@ -63,39 +87,39 @@ def __init__( self.set_interpret(interpret, output_shape) # not implemented yet - self._input_gradients = None - + # self._input_gradients = None def __enter__(self): + """ + QNN used with context managers. + """ self.open() return self - def __exit__(self, *exc_info): + def __exit__(self, *exc_info) -> None: + """ + QNN used with context managers. + """ self.close() - def open(self): + def open(self) -> None: """ Open sampler/gradient session. """ - # we should delay instantiation of the primitives until they are really required - if self._gradient_method == "param_shift": + self._sampler = self._sampler_factory( + circuits=self._circuit, parameters=[self._input_params + self._weight_params] + ) + + if self._gradient_factory is not None: # if gradient method -> sampler with gradient functionality - # self._sampler = ParamShiftSamplerGradient( + # self._gradient = ParamShiftSamplerGradient( # circuit = self._circuit, # sampler = self._sampler_factory # ) - pass # waiting for gradients + pass # waiting for gradients - else: - # if no gradient method -> sampler without gradient functionality - self._sampler = self._sampler_factory( - circuits = self._circuit, - parameters = [self._input_params + self._weight_params] - ) - pass - - def close(self): + def close(self) -> None: """ Close sampler/gradient session. """ @@ -103,8 +127,8 @@ def close(self): def set_interpret( self, - interpret: Optional[Callable[[int], Union[int, Tuple[int, ...]]]], - output_shape: Union[int, Tuple[int, ...]] = None, + interpret: Callable[[int], int| Tuple[int, ...]] | None = None, + output_shape: int | Tuple[int, ...] | None = None ) -> None: """Change 'interpret' and corresponding 'output_shape'. @@ -125,12 +149,15 @@ def set_interpret( self._interpret = interpret if interpret is not None else lambda x: x self.output_shape = self._output_shape - def _compute_output_shape(self, interpret, output_shape) -> Tuple[int, ...]: + def _compute_output_shape( + self, + interpret: Callable[[int], int | Tuple[int, ...]] | None = None, + output_shape: int | Tuple[int, ...] | None = None + ) -> Tuple[int, ...]: """Validate and compute the output shape.""" # this definition is required by mypy output_shape_: Tuple[int, ...] = (-1,) - # todo: move sampling code to the super class if interpret is not None: if output_shape is None: @@ -150,23 +177,30 @@ def _compute_output_shape(self, interpret, output_shape) -> Tuple[int, ...]: "determined as 2^num_qubits." ) - output_shape_ = (2 ** self._circuit.num_qubits,) - - # # final validation - # output_shape_ = self._validate_output_shape(output_shape_) + output_shape_ = (2**self._circuit.num_qubits,) return output_shape_ def forward( - self, - input_data: Optional[Union[List[float], np.ndarray, float]], - weights: Optional[Union[List[float], np.ndarray, float]], + self, + input_data: List[float] | np.ndarray | float | None, + weights: List[float] | np.ndarray | float | None, ) -> np.ndarray: - + """ + Forward pass of the network. Returns the probabilities. + Format depends on the set interpret function. + """ result = self._forward(input_data, weights) return result - def _preprocess(self, input_data, weights): + def _preprocess( + self, + input_data: List[float] | np.ndarray | float | None, + weights: List[float] | np.ndarray | float | None, + ) -> Tuple[List[float], int]: + """ + Pre-processing during forward pass of the network. + """ if len(input_data.shape) == 1: input_data = np.expand_dims(input_data, 0) num_samples = input_data.shape[0] @@ -176,14 +210,16 @@ def _preprocess(self, input_data, weights): parameters = [] for i in range(num_samples): - param_values = [input_data[i,j] for j, input_param in enumerate(self._input_params)] + param_values = [input_data[i, j] for j, input_param in enumerate(self._input_params)] param_values += [weights[j] for j, weight_param in enumerate(self._weight_params)] parameters.append(param_values) return parameters, num_samples def _postprocess(self, num_samples, result): - + """ + Post-processing during forward pass of the network. + """ prob = np.zeros((num_samples, *self._output_shape)) for i in range(num_samples): @@ -193,42 +229,43 @@ def _postprocess(self, num_samples, result): # evaluate probabilities for b, v in counts.items(): - key = (i, int(self._interpret(b))) # type: ignore + key = (i, int(self._interpret(b))) # type: ignore prob[key] += v / shots return prob def _forward( - self, input_data: Optional[np.ndarray], weights: Optional[np.ndarray] + self, input_data: Optional[np.ndarray], weights: Optional[np.ndarray] ) -> np.ndarray: - + """ + Forward pass of the network. + """ parameter_values, num_samples = self._preprocess(input_data, weights) # sampler allows batching (gradient doesn't) results = self._sampler([0] * num_samples, parameter_values) - # results = [] - # for sample in range(num_samples): - # result = self._sampler(circuits = self._circuit, - # parameter_values = parameter_values) - # results.append(result) - result = self._postprocess(num_samples, results) return result def backward( - self, - input_data: Optional[Union[List[float], np.ndarray, float]], - weights: Optional[Union[List[float], np.ndarray, float]], + self, + input_data: Optional[Union[List[float], np.ndarray, float]], + weights: Optional[Union[List[float], np.ndarray, float]], ) -> Tuple[Optional[np.ndarray], Optional[np.ndarray],]: - - # result = self._backward(input_data, weights) - # return result - pass + """Backward pass of the network. Returns (None, None) when no gradient is + provided and the corresponding here probability gradients otherwise. + """ + if self._gradient: + return self._backward(input_data, weights) + else: + return None, None def _preprocess_gradient(self, input_data, weights): - + """ + Pre-processing during backward pass of the network. + """ if len(input_data.shape) == 1: input_data = np.expand_dims(input_data, 0) @@ -247,7 +284,9 @@ def _preprocess_gradient(self, input_data, weights): return parameters, num_samples def _postprocess_gradient(self, num_samples, results): - + """ + Post-processing during backward pass of the network. + """ input_grad = np.zeros((num_samples, 1, self._num_inputs)) if self._input_gradients else None weights_grad = np.zeros((num_samples, *self._output_shape, self._num_weights)) @@ -291,30 +330,22 @@ def _backward( self, input_data: Optional[np.ndarray], weights: Optional[np.ndarray] ) -> Tuple[Optional[np.ndarray], Optional[np.ndarray],]: + """Backward pass of the network. + """ # prepare parameters in the required format parameter_values, num_samples = self._preprocess_gradient(input_data, weights) results = [] for sample in range(num_samples): if self._input_gradients: - result = self._sampler.gradient(parameter_values[sample]) + result = self._gradient(parameter_values[sample]) else: - result = self._sampler.gradient(parameter_values[sample], - partial=self._sampler._circuit.parameters[self._num_inputs:]) + result = self._gradient( + parameter_values[sample], + partial=self._sampler._circuit.parameters[self._num_inputs :], + ) results.append(result) input_grad, weights_grad = self._postprocess_gradient(num_samples, results) - return None , weights_grad # `None` for gradients wrt input data, see TorchConnector - - - - - - - - - - - - + return None, weights_grad # `None` for gradients wrt input data, see TorchConnector diff --git a/test/primitives/test_sampler_qnn.py b/test/primitives/test_sampler_qnn.py index 7a671a098..544f356a2 100644 --- a/test/primitives/test_sampler_qnn.py +++ b/test/primitives/test_sampler_qnn.py @@ -51,7 +51,7 @@ def test_forward_pass(self): interpret=parity, output_shape=output_shape, quantum_instance=self.qi_qasm, - ) + ) inputs = np.asarray(algorithm_globals.random.random(size=(1, circuit_qnn._num_inputs))) weights = algorithm_globals.random.random(circuit_qnn._num_weights) @@ -59,18 +59,16 @@ def test_forward_pass(self): sampler_factory = Sampler with SamplerQNN( - circuit=self.qc, - input_params=self.qc.parameters[:3], - weight_params=self.qc.parameters[3:], - sampler_factory=sampler_factory, - interpret = parity, - output_shape = output_shape, - ) as qnn: - - sampler_qnn_fwd = qnn.forward(inputs, weights) - - np.testing.assert_array_almost_equal(np.asarray(sampler_qnn_fwd), - np.asarray(circuit_qnn_fwd),0.1) - + circuit=self.qc, + input_params=self.qc.parameters[:3], + weight_params=self.qc.parameters[3:], + sampler_factory=sampler_factory, + interpret=parity, + output_shape=output_shape, + ) as qnn: + sampler_qnn_fwd = qnn.forward(inputs, weights) + np.testing.assert_array_almost_equal( + np.asarray(sampler_qnn_fwd), np.asarray(circuit_qnn_fwd), 0.1 + ) From d6574d39660a2125ae9adbe06957bb3fb5d4f52f Mon Sep 17 00:00:00 2001 From: ElePT Date: Thu, 14 Jul 2022 13:51:50 +0200 Subject: [PATCH 11/61] Move to neural_networks --- __init__.py | 0 .../neural_networks/__init__.py | 11 +++++++++++ .../sampler_qnn.py | 0 .../primitives/__init__.py | 19 ------------------- 4 files changed, 11 insertions(+), 19 deletions(-) create mode 100644 __init__.py rename qiskit_machine_learning/{primitives => neural_networks}/sampler_qnn.py (100%) delete mode 100644 qiskit_machine_learning/primitives/__init__.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/qiskit_machine_learning/neural_networks/__init__.py b/qiskit_machine_learning/neural_networks/__init__.py index f1b86c5c1..2a5794ac5 100644 --- a/qiskit_machine_learning/neural_networks/__init__.py +++ b/qiskit_machine_learning/neural_networks/__init__.py @@ -57,6 +57,15 @@ EffectiveDimension LocalEffectiveDimension +Neural Networks using Primitives +================================ + +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + SamplerQNN + """ from .circuit_qnn import CircuitQNN @@ -65,6 +74,7 @@ from .opflow_qnn import OpflowQNN from .sampling_neural_network import SamplingNeuralNetwork from .two_layer_qnn import TwoLayerQNN +from .sampler_qnn import SamplerQNN __all__ = [ "NeuralNetwork", @@ -74,4 +84,5 @@ "CircuitQNN", "EffectiveDimension", "LocalEffectiveDimension", + "SamplerQNN" ] diff --git a/qiskit_machine_learning/primitives/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py similarity index 100% rename from qiskit_machine_learning/primitives/sampler_qnn.py rename to qiskit_machine_learning/neural_networks/sampler_qnn.py diff --git a/qiskit_machine_learning/primitives/__init__.py b/qiskit_machine_learning/primitives/__init__.py deleted file mode 100644 index 886a6a30c..000000000 --- a/qiskit_machine_learning/primitives/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# 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. - -""" -Primitives (:mod:`qiskit_machine_learning.primitives`) -=============================================================== - -.. currentmodule:: qiskit_machine_learning.primitives - -""" From fc0f5846097c85995f54af5ff6f430dccca5b691 Mon Sep 17 00:00:00 2001 From: ElePT Date: Wed, 14 Sep 2022 12:11:56 +0200 Subject: [PATCH 12/61] Runnable tests --- .../neural_networks/circuit_qnn.py | 1 - .../neural_networks/sampler_qnn.py | 147 +++++--------- test/neural_networks/test_sampler_qnn.py | 182 ++++++++++++++++++ test/primitives/test_sampler_qnn.py | 74 ------- 4 files changed, 228 insertions(+), 176 deletions(-) create mode 100644 test/neural_networks/test_sampler_qnn.py delete mode 100644 test/primitives/test_sampler_qnn.py diff --git a/qiskit_machine_learning/neural_networks/circuit_qnn.py b/qiskit_machine_learning/neural_networks/circuit_qnn.py index 21d029157..ff64ed1ea 100644 --- a/qiskit_machine_learning/neural_networks/circuit_qnn.py +++ b/qiskit_machine_learning/neural_networks/circuit_qnn.py @@ -490,7 +490,6 @@ def _probability_gradients( num_grad_vars = self._num_inputs + self._num_weights else: num_grad_vars = self._num_weights - # construct gradients for sample in range(num_samples): for i in range(num_grad_vars): diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index 3ab503531..6d5cb0905 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -21,23 +21,27 @@ from qiskit import QuantumCircuit from qiskit.circuit import Parameter +from qiskit.primitives import BaseSampler +from qiskit.algorithms.gradients import BaseSamplerGradient from qiskit_machine_learning.exceptions import QiskitMachineLearningError, QiskitError +from .neural_network import NeuralNetwork logger = logging.getLogger(__name__) -class SamplerQNN: +class SamplerQNN(NeuralNetwork): """A Neural Network implementation based on the Sampler primitive.""" def __init__( self, + sampler: BaseSampler, circuit: QuantumCircuit, input_params: List[Parameter] | None = None, weight_params: List[Parameter] | None = None, interpret: Callable[[int], int | Tuple[int, ...]] | None = None, output_shape: int | Tuple[int, ...] | None = None, - sampler_factory: Callable | None = None, - gradient_factory: Callable | str | None = None, + gradient: BaseSamplerGradient | None = None, + input_gradients: bool = False )-> None: """ @@ -57,74 +61,43 @@ def __init__( Raises: QiskitMachineLearningError: Invalid parameter values. """ - - self._circuit = circuit - - self._gradient_factory = gradient_factory - self._sampler_factory = sampler_factory - - self._sampler = None - self._gradient = None + # set sampler --> make property? + self.sampler = sampler + # use given gradient or default + self.gradient = gradient self._input_params = list(input_params or []) self._weight_params = list(weight_params or []) - self.output_shape = None - self._num_inputs = len(self._input_params) - self._num_weights = len(self._weight_params) + # initialize gradient properties + self.input_gradients = input_gradients # TODO - self.num_weights = self._num_weights + # sparse = False --> Hard coded TODO: look into sparse - # the circuit must always have measurements... - # add measurements in case none are given + self._circuit = circuit.copy() if len(self._circuit.clbits) == 0: self._circuit.measure_all() + # self._circuit_transpiled = False TODO: look into transpilation - self._interpret = interpret - self._original_interpret = interpret + # these original values may be re-used when a quantum instance is set, + # but initially it was None + self._original_output_shape = output_shape + self._output_shape = output_shape - # set interpret and compute output shape self.set_interpret(interpret, output_shape) + # next line is required by pylint only + # self._interpret = interpret + self._original_interpret = interpret - # not implemented yet - # self._input_gradients = None - - def __enter__(self): - """ - QNN used with context managers. - """ - self.open() - return self - - def __exit__(self, *exc_info) -> None: - """ - QNN used with context managers. - """ - self.close() - - def open(self) -> None: - """ - Open sampler/gradient session. - """ - # we should delay instantiation of the primitives until they are really required - self._sampler = self._sampler_factory( - circuits=self._circuit, parameters=[self._input_params + self._weight_params] + # init super clas + super().__init__( + len(self._input_params), + len(self._weight_params), + False, # sparse + self._output_shape, + self._input_gradients, ) - if self._gradient_factory is not None: - # if gradient method -> sampler with gradient functionality - # self._gradient = ParamShiftSamplerGradient( - # circuit = self._circuit, - # sampler = self._sampler_factory - # ) - pass # waiting for gradients - - def close(self) -> None: - """ - Close sampler/gradient session. - """ - self._sampler.__exit__() - def set_interpret( self, interpret: Callable[[int], int| Tuple[int, ...]] | None = None, @@ -147,7 +120,7 @@ def set_interpret( # derive target values to be used in computations self._output_shape = self._compute_output_shape(interpret, output_shape) self._interpret = interpret if interpret is not None else lambda x: x - self.output_shape = self._output_shape + # self.output_shape = self._compute_output_shape(self._interpret, output_shape) def _compute_output_shape( self, @@ -181,18 +154,6 @@ def _compute_output_shape( return output_shape_ - def forward( - self, - input_data: List[float] | np.ndarray | float | None, - weights: List[float] | np.ndarray | float | None, - ) -> np.ndarray: - """ - Forward pass of the network. Returns the probabilities. - Format depends on the set interpret function. - """ - result = self._forward(input_data, weights) - return result - def _preprocess( self, input_data: List[float] | np.ndarray | float | None, @@ -223,7 +184,7 @@ def _postprocess(self, num_samples, result): prob = np.zeros((num_samples, *self._output_shape)) for i in range(num_samples): - counts = result.quasi_dists[i] + counts = result[i] print(counts) shots = sum(counts.values()) @@ -242,26 +203,14 @@ def _forward( """ parameter_values, num_samples = self._preprocess(input_data, weights) - # sampler allows batching (gradient doesn't) - results = self._sampler([0] * num_samples, parameter_values) + # sampler allows batching + job = self.sampler.run([self._circuit] * num_samples, parameter_values) + results = job.result().quasi_dists result = self._postprocess(num_samples, results) return result - def backward( - self, - input_data: Optional[Union[List[float], np.ndarray, float]], - weights: Optional[Union[List[float], np.ndarray, float]], - ) -> Tuple[Optional[np.ndarray], Optional[np.ndarray],]: - """Backward pass of the network. Returns (None, None) when no gradient is - provided and the corresponding here probability gradients otherwise. - """ - if self._gradient: - return self._backward(input_data, weights) - else: - return None, None - def _preprocess_gradient(self, input_data, weights): """ Pre-processing during backward pass of the network. @@ -276,7 +225,6 @@ def _preprocess_gradient(self, input_data, weights): parameters = [] for i in range(num_samples): - param_values = [input_data[i, j] for j, input_param in enumerate(self._input_params)] param_values += [weights[j] for j, weight_param in enumerate(self._weight_params)] parameters.append(param_values) @@ -287,7 +235,7 @@ def _postprocess_gradient(self, num_samples, results): """ Post-processing during backward pass of the network. """ - input_grad = np.zeros((num_samples, 1, self._num_inputs)) if self._input_gradients else None + input_grad = np.zeros((num_samples, *self._output_shape, self._num_inputs)) if self._input_gradients else None weights_grad = np.zeros((num_samples, *self._output_shape, self._num_weights)) if self._input_gradients: @@ -298,9 +246,9 @@ def _postprocess_gradient(self, num_samples, results): for sample in range(num_samples): for i in range(num_grad_vars): - grad = results[sample].quasi_dists[i + self._num_inputs] + grad = results.gradients[sample][i] for k in grad.keys(): - val = results[sample].quasi_dists[i + self._num_inputs][k] + val = results.gradients[sample][i][k] # get index for input or weights gradients if self._input_gradients: grad_index = i if i < self._num_inputs else i - self._num_inputs @@ -335,17 +283,14 @@ def _backward( # prepare parameters in the required format parameter_values, num_samples = self._preprocess_gradient(input_data, weights) - results = [] - for sample in range(num_samples): - if self._input_gradients: - result = self._gradient(parameter_values[sample]) - else: - result = self._gradient( - parameter_values[sample], - partial=self._sampler._circuit.parameters[self._num_inputs :], - ) + if self._input_gradients: + job = self.gradient.run([self._circuit] * num_samples, parameter_values) + else: + job = self.gradient.run([self._circuit] * num_samples, parameter_values, + parameters=[self._circuit.parameters[self._num_inputs:]]) + + results = job.result() - results.append(result) input_grad, weights_grad = self._postprocess_gradient(num_samples, results) - return None, weights_grad # `None` for gradients wrt input data, see TorchConnector + return input_grad, weights_grad # `None` for gradients wrt input data, see TorchConnector diff --git a/test/neural_networks/test_sampler_qnn.py b/test/neural_networks/test_sampler_qnn.py new file mode 100644 index 000000000..8fa01c3bd --- /dev/null +++ b/test/neural_networks/test_sampler_qnn.py @@ -0,0 +1,182 @@ +# 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. + +"""Test Sampler QNN with Terra primitives.""" +import numpy as np +from test import QiskitMachineLearningTestCase + +from qiskit.primitives import Sampler +from qiskit.algorithms.gradients import ParamShiftSamplerGradient, FiniteDiffSamplerGradient +from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap +from qiskit.utils import QuantumInstance, algorithm_globals +from qiskit import Aer +from qiskit.utils import QuantumInstance + +from qiskit_machine_learning.neural_networks import CircuitQNN +from qiskit_machine_learning.neural_networks.sampler_qnn import SamplerQNN + +algorithm_globals.random_seed = 42 +from test.connectors.test_torch import TestTorch +import os + +os.environ['KMP_DUPLICATE_LIB_OK']='True' + +class TestSamplerQNN(QiskitMachineLearningTestCase): + """Sampler QNN Tests.""" + + def setUp(self): + super().setUp() + algorithm_globals.random_seed = 12345 + + # define test circuit + num_qubits = 3 + self.qc = RealAmplitudes(num_qubits, entanglement="linear", reps=1) + self.qi_qasm = QuantumInstance(Aer.get_backend("aer_simulator")) + self.sampler = Sampler() + + def test_forward_pass(self): + + parity = lambda x: "{:b}".format(x).count("1") % 2 + output_shape = 2 # this is required in case of a callable with dense output + + circuit_qnn = CircuitQNN( + self.qc, + input_params=self.qc.parameters[:3], + weight_params=self.qc.parameters[3:], + sparse=False, + interpret=parity, + output_shape=output_shape, + quantum_instance=self.qi_qasm, + ) + + sampler_qnn = SamplerQNN( + sampler=self.sampler, + circuit=self.qc, + input_params=self.qc.parameters[:3], + weight_params=self.qc.parameters[3:], + interpret=parity, + output_shape=output_shape, + ) + + inputs = np.asarray(algorithm_globals.random.random(size=(1, circuit_qnn._num_inputs))) + weights = algorithm_globals.random.random(circuit_qnn._num_weights) + + circuit_qnn_fwd = circuit_qnn.forward(inputs, weights) + sampler_qnn_fwd = sampler_qnn.forward(inputs, weights) + + np.testing.assert_array_almost_equal( + np.asarray(sampler_qnn_fwd), np.asarray(circuit_qnn_fwd), 0.1 + ) + + def test_backward_pass(self): + + parity = lambda x: "{:b}".format(x).count("1") % 2 + output_shape = 2 # this is required in case of a callable with dense output + from qiskit.opflow import Gradient + circuit_qnn = CircuitQNN( + self.qc, + input_params=self.qc.parameters[:3], + weight_params=self.qc.parameters[3:], + sparse=False, + interpret=parity, + output_shape=output_shape, + quantum_instance=self.qi_qasm, + gradient=Gradient("param_shift"), + ) + + sampler_qnn = SamplerQNN( + sampler=self.sampler, + circuit=self.qc, + input_params=self.qc.parameters[:3], + weight_params=self.qc.parameters[3:], + interpret=parity, + output_shape=output_shape, + gradient=ParamShiftSamplerGradient(self.sampler), + ) + + inputs = np.asarray(algorithm_globals.random.random(size=(1, circuit_qnn._num_inputs))) + weights = algorithm_globals.random.random(circuit_qnn._num_weights) + + circuit_qnn_fwd = circuit_qnn.backward(inputs, weights) + sampler_qnn_fwd = sampler_qnn.backward(inputs, weights) + + print(circuit_qnn_fwd) + print(sampler_qnn_fwd) + np.testing.assert_array_almost_equal( + np.asarray(sampler_qnn_fwd[1]), np.asarray(circuit_qnn_fwd[1]), 0.1 + ) + + def test_input_gradients(self): + + parity = lambda x: "{:b}".format(x).count("1") % 2 + output_shape = 2 # this is required in case of a callable with dense output + from qiskit.opflow import Gradient + circuit_qnn = CircuitQNN( + self.qc, + input_params=self.qc.parameters[:3], + weight_params=self.qc.parameters[3:], + sparse=False, + interpret=parity, + output_shape=output_shape, + quantum_instance=self.qi_qasm, + gradient=Gradient("param_shift"), + input_gradients=True + ) + + sampler_qnn = SamplerQNN( + sampler=self.sampler, + circuit=self.qc, + input_params=self.qc.parameters[:3], + weight_params=self.qc.parameters[3:], + interpret=parity, + output_shape=output_shape, + gradient=ParamShiftSamplerGradient(self.sampler), + input_gradients=True + + ) + + inputs = np.asarray(algorithm_globals.random.random(size=(1, circuit_qnn._num_inputs))) + weights = algorithm_globals.random.random(circuit_qnn._num_weights) + + circuit_qnn_fwd = circuit_qnn.backward(inputs, weights) + sampler_qnn_fwd = sampler_qnn.backward(inputs, weights) + + print(circuit_qnn_fwd) + print(sampler_qnn_fwd) + np.testing.assert_array_almost_equal( + np.asarray(sampler_qnn_fwd), np.asarray(circuit_qnn_fwd), 0.1 + ) + + from qiskit_machine_learning.connectors import TorchConnector + import torch + + model = TorchConnector(sampler_qnn) + func = TorchConnector._TorchNNFunction.apply # (input, weights, qnn) + input_data = ( + torch.randn( + model.neural_network.num_inputs, + dtype=torch.double, + requires_grad=True, + ), + torch.randn( + model.neural_network.num_weights, + dtype=torch.double, + requires_grad=True, + ), + model.neural_network, + False, + ) + test = torch.autograd.gradcheck(func, input_data, eps=1e-4, atol=1e-3) # type: ignore + self.assertTrue(test) + + # def test_torch_connector(self): + # from qiskit_machine_learning.connectors import TorchConnector diff --git a/test/primitives/test_sampler_qnn.py b/test/primitives/test_sampler_qnn.py deleted file mode 100644 index 544f356a2..000000000 --- a/test/primitives/test_sampler_qnn.py +++ /dev/null @@ -1,74 +0,0 @@ -# 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. - -"""Test Sampler QNN with Terra primitives.""" -import numpy as np -from test import QiskitMachineLearningTestCase - -from qiskit.primitives import Sampler -from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap -from qiskit.utils import QuantumInstance, algorithm_globals -from qiskit import Aer -from qiskit.utils import QuantumInstance - -from qiskit_machine_learning.neural_networks import CircuitQNN -from qiskit_machine_learning.primitives.sampler_qnn import SamplerQNN - -algorithm_globals.random_seed = 42 - - -class TestSamplerQNN(QiskitMachineLearningTestCase): - """Sampler QNN Tests.""" - - def setUp(self): - super().setUp() - algorithm_globals.random_seed = 12345 - - # define test circuit - num_qubits = 3 - self.qc = RealAmplitudes(num_qubits, entanglement="linear", reps=1) - self.qi_qasm = QuantumInstance(Aer.get_backend("aer_simulator"), shots=10) - - def test_forward_pass(self): - - parity = lambda x: "{:b}".format(x).count("1") % 2 - output_shape = 2 # this is required in case of a callable with dense output - - circuit_qnn = CircuitQNN( - self.qc, - input_params=self.qc.parameters[:3], - weight_params=self.qc.parameters[3:], - sparse=False, - interpret=parity, - output_shape=output_shape, - quantum_instance=self.qi_qasm, - ) - - inputs = np.asarray(algorithm_globals.random.random(size=(1, circuit_qnn._num_inputs))) - weights = algorithm_globals.random.random(circuit_qnn._num_weights) - circuit_qnn_fwd = circuit_qnn.forward(inputs, weights) - - sampler_factory = Sampler - with SamplerQNN( - circuit=self.qc, - input_params=self.qc.parameters[:3], - weight_params=self.qc.parameters[3:], - sampler_factory=sampler_factory, - interpret=parity, - output_shape=output_shape, - ) as qnn: - - sampler_qnn_fwd = qnn.forward(inputs, weights) - - np.testing.assert_array_almost_equal( - np.asarray(sampler_qnn_fwd), np.asarray(circuit_qnn_fwd), 0.1 - ) From 7963cbc05f59374c7f569b3e42bacac336d0ad62 Mon Sep 17 00:00:00 2001 From: ElePT Date: Mon, 10 Oct 2022 10:56:38 +0200 Subject: [PATCH 13/61] Update typehints, refactor --- .../neural_networks/__init__.py | 11 +-- .../neural_networks/sampler_qnn.py | 81 +++++++++---------- 2 files changed, 40 insertions(+), 52 deletions(-) diff --git a/qiskit_machine_learning/neural_networks/__init__.py b/qiskit_machine_learning/neural_networks/__init__.py index 2a5794ac5..2f26e75e5 100644 --- a/qiskit_machine_learning/neural_networks/__init__.py +++ b/qiskit_machine_learning/neural_networks/__init__.py @@ -46,6 +46,7 @@ OpflowQNN TwoLayerQNN CircuitQNN + SamplerQNN Neural Network Metrics ====================== @@ -56,16 +57,6 @@ EffectiveDimension LocalEffectiveDimension - -Neural Networks using Primitives -================================ - -.. autosummary:: - :toctree: ../stubs/ - :nosignatures: - - SamplerQNN - """ from .circuit_qnn import CircuitQNN diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index 6d5cb0905..fc9b17597 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -14,16 +14,16 @@ from __future__ import annotations import logging + from numbers import Integral -from typing import Optional, Union, List, Tuple, Callable, cast, Iterable +from typing import Callable, cast, Iterable, Sequence import numpy as np - -from qiskit import QuantumCircuit -from qiskit.circuit import Parameter -from qiskit.primitives import BaseSampler from qiskit.algorithms.gradients import BaseSamplerGradient +from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.primitives import BaseSampler from qiskit_machine_learning.exceptions import QiskitMachineLearningError, QiskitError + from .neural_network import NeuralNetwork logger = logging.getLogger(__name__) @@ -36,16 +36,17 @@ def __init__( self, sampler: BaseSampler, circuit: QuantumCircuit, - input_params: List[Parameter] | None = None, - weight_params: List[Parameter] | None = None, - interpret: Callable[[int], int | Tuple[int, ...]] | None = None, - output_shape: int | Tuple[int, ...] | None = None, + *, + input_params: Sequence[Parameter] | None = None, + weight_params: Sequence[Parameter] | None = None, + interpret: Callable[[int], int | tuple[int, ...]] | None = None, + output_shape: int | tuple[int, ...] | None = None, gradient: BaseSamplerGradient | None = None, input_gradients: bool = False - )-> None: + ): """ - Args: + sampler: The sampler primitive used to compute neural network's results. circuit: The parametrized quantum circuit that generates the samples of this network. input_params: The parameters of the circuit corresponding to the input. weight_params: The parameters of the circuit corresponding to the trainable weights. @@ -54,39 +55,31 @@ def __init__( sparse) output array. If no interpret function is passed, then an identity function will be used by this neural network. output_shape: The output shape of the custom interpretation - sampler_factory: Factory for sampler primitive - gradient_factory: String indicating pre-implemented gradient method or factory for - gradient class - input_gradients: to be added + gradient: An optional sampler gradient to be used for the backward pass. + input_gradients: Determines whether to compute gradients with respect to input data. + Note that this parameter is ``False`` by default, and must be explicitly set to + ``True`` for a proper gradient computation when using ``TorchConnector``. Raises: QiskitMachineLearningError: Invalid parameter values. """ - # set sampler --> make property? self.sampler = sampler - # use given gradient or default self.gradient = gradient + self._circuit = circuit.copy() + if len(self._circuit.clbits) == 0: + self._circuit.measure_all() + self._input_params = list(input_params or []) self._weight_params = list(weight_params or []) - # initialize gradient properties - self.input_gradients = input_gradients # TODO + self._input_gradients = input_gradients # sparse = False --> Hard coded TODO: look into sparse - - self._circuit = circuit.copy() - if len(self._circuit.clbits) == 0: - self._circuit.measure_all() # self._circuit_transpiled = False TODO: look into transpilation - # these original values may be re-used when a quantum instance is set, - # but initially it was None - self._original_output_shape = output_shape self._output_shape = output_shape self.set_interpret(interpret, output_shape) - # next line is required by pylint only - # self._interpret = interpret self._original_interpret = interpret # init super clas @@ -100,8 +93,8 @@ def __init__( def set_interpret( self, - interpret: Callable[[int], int| Tuple[int, ...]] | None = None, - output_shape: int | Tuple[int, ...] | None = None + interpret: Callable[[int], int| tuple[int, ...]] | None = None, + output_shape: int | tuple[int, ...] | None = None ) -> None: """Change 'interpret' and corresponding 'output_shape'. @@ -120,17 +113,16 @@ def set_interpret( # derive target values to be used in computations self._output_shape = self._compute_output_shape(interpret, output_shape) self._interpret = interpret if interpret is not None else lambda x: x - # self.output_shape = self._compute_output_shape(self._interpret, output_shape) def _compute_output_shape( self, - interpret: Callable[[int], int | Tuple[int, ...]] | None = None, - output_shape: int | Tuple[int, ...] | None = None - ) -> Tuple[int, ...]: + interpret: Callable[[int], int | tuple[int, ...]] | None = None, + output_shape: int | tuple[int, ...] | None = None + ) -> tuple[int, ...]: """Validate and compute the output shape.""" # this definition is required by mypy - output_shape_: Tuple[int, ...] = (-1,) + output_shape_: tuple[int, ...] = (-1,) if interpret is not None: if output_shape is None: @@ -156,9 +148,9 @@ def _compute_output_shape( def _preprocess( self, - input_data: List[float] | np.ndarray | float | None, - weights: List[float] | np.ndarray | float | None, - ) -> Tuple[List[float], int]: + input_data: list[float] | np.ndarray | float | None, + weights: list[float] | np.ndarray | float | None, + ) -> tuple[list[float], int]: """ Pre-processing during forward pass of the network. """ @@ -196,7 +188,9 @@ def _postprocess(self, num_samples, result): return prob def _forward( - self, input_data: Optional[np.ndarray], weights: Optional[np.ndarray] + self, + input_data: np.ndarray | None, + weights: np.ndarray | None ) -> np.ndarray: """ Forward pass of the network. @@ -211,7 +205,7 @@ def _forward( return result - def _preprocess_gradient(self, input_data, weights): + def _preprocess_gradient(self, input_data: np.ndarray, weights: np.ndarray): """ Pre-processing during backward pass of the network. """ @@ -275,8 +269,10 @@ def _postprocess_gradient(self, num_samples, results): return input_grad, weights_grad def _backward( - self, input_data: Optional[np.ndarray], weights: Optional[np.ndarray] - ) -> Tuple[Optional[np.ndarray], Optional[np.ndarray],]: + self, + input_data: np.ndarray | None, + weights: np.ndarray | None + ) -> tuple[np.ndarray | None, np.ndarray | None]: """Backward pass of the network. """ @@ -294,3 +290,4 @@ def _backward( input_grad, weights_grad = self._postprocess_gradient(num_samples, results) return input_grad, weights_grad # `None` for gradients wrt input data, see TorchConnector + From 8248138393b33c9af337169d7d7c2bc4605a1dbd Mon Sep 17 00:00:00 2001 From: ElePT Date: Mon, 10 Oct 2022 13:24:27 +0200 Subject: [PATCH 14/61] Add new unittests, fix code --- __init__.py | 0 .../neural_networks/__init__.py | 2 +- .../neural_networks/sampler_qnn.py | 96 +++-- test/neural_networks/test_sampler_qnn.py | 367 ++++++++++++------ 4 files changed, 310 insertions(+), 155 deletions(-) delete mode 100644 __init__.py diff --git a/__init__.py b/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/qiskit_machine_learning/neural_networks/__init__.py b/qiskit_machine_learning/neural_networks/__init__.py index 2f26e75e5..d897be5a8 100644 --- a/qiskit_machine_learning/neural_networks/__init__.py +++ b/qiskit_machine_learning/neural_networks/__init__.py @@ -75,5 +75,5 @@ "CircuitQNN", "EffectiveDimension", "LocalEffectiveDimension", - "SamplerQNN" + "SamplerQNN", ] diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index fc9b17597..245ad79d0 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -20,9 +20,10 @@ import numpy as np from qiskit.algorithms.gradients import BaseSamplerGradient +from qiskit.algorithms.gradients import ParamShiftSamplerGradient from qiskit.circuit import Parameter, QuantumCircuit from qiskit.primitives import BaseSampler -from qiskit_machine_learning.exceptions import QiskitMachineLearningError, QiskitError +from qiskit_machine_learning.exceptions import QiskitMachineLearningError from .neural_network import NeuralNetwork @@ -42,7 +43,7 @@ def __init__( interpret: Callable[[int], int | tuple[int, ...]] | None = None, output_shape: int | tuple[int, ...] | None = None, gradient: BaseSamplerGradient | None = None, - input_gradients: bool = False + input_gradients: bool = False, ): """ Args: @@ -62,8 +63,12 @@ def __init__( Raises: QiskitMachineLearningError: Invalid parameter values. """ + # set primitive self.sampler = sampler - self.gradient = gradient + + # set gradient + # TODO: provide default gradient? + self.gradient = gradient or ParamShiftSamplerGradient(self.sampler) self._circuit = circuit.copy() if len(self._circuit.clbits) == 0: @@ -72,29 +77,50 @@ def __init__( self._input_params = list(input_params or []) self._weight_params = list(weight_params or []) - self._input_gradients = input_gradients - - # sparse = False --> Hard coded TODO: look into sparse - # self._circuit_transpiled = False TODO: look into transpilation + # the final output shape will depend on the + # interpret method, and it must be set before + # applying the default to the latter + self.set_interpret_out_shape(interpret, output_shape) - self._output_shape = output_shape + self._input_gradients = input_gradients - self.set_interpret(interpret, output_shape) - self._original_interpret = interpret + # TODO: will primitives ever support sparse? + # TODO: look into custom transpilation + # TODO: sampling?? - # init super clas super().__init__( len(self._input_params), len(self._weight_params), - False, # sparse + False, # sparse self._output_shape, self._input_gradients, ) - def set_interpret( + @property + def circuit(self) -> QuantumCircuit: + """Returns the underlying quantum circuit.""" + return self._circuit + + @property + def input_params(self) -> Sequence: + """Returns the list of input parameters.""" + return self._input_params + + @property + def weight_params(self) -> Sequence: + """Returns the list of trainable weights parameters.""" + return self._weight_params + + @property + def interpret(self) -> Callable[[int], int | tuple[int, ...]] | None: + """Returns interpret function to be used by the neural network. If it is not set in + the constructor or can not be implicitly derived, then ``None`` is returned.""" + return self._interpret + + def set_interpret_out_shape( self, - interpret: Callable[[int], int| tuple[int, ...]] | None = None, - output_shape: int | tuple[int, ...] | None = None + interpret: Callable[[int], int | tuple[int, ...]] | None = None, + output_shape: int | tuple[int, ...] | None = None, ) -> None: """Change 'interpret' and corresponding 'output_shape'. @@ -106,10 +132,6 @@ def set_interpret( for more details. """ - # save original values - self._original_output_shape = output_shape - self._original_interpret = interpret - # derive target values to be used in computations self._output_shape = self._compute_output_shape(interpret, output_shape) self._interpret = interpret if interpret is not None else lambda x: x @@ -117,7 +139,7 @@ def set_interpret( def _compute_output_shape( self, interpret: Callable[[int], int | tuple[int, ...]] | None = None, - output_shape: int | tuple[int, ...] | None = None + output_shape: int | tuple[int, ...] | None = None, ) -> tuple[int, ...]: """Validate and compute the output shape.""" @@ -177,21 +199,19 @@ def _postprocess(self, num_samples, result): for i in range(num_samples): counts = result[i] - print(counts) shots = sum(counts.values()) # evaluate probabilities for b, v in counts.items(): - key = (i, int(self._interpret(b))) # type: ignore + key = self._interpret(b) + if isinstance(key, Integral): + key = (cast(int, key),) + key = (i, *key) # type: ignore prob[key] += v / shots return prob - def _forward( - self, - input_data: np.ndarray | None, - weights: np.ndarray | None - ) -> np.ndarray: + def _forward(self, input_data: np.ndarray | None, weights: np.ndarray | None) -> np.ndarray: """ Forward pass of the network. """ @@ -229,7 +249,11 @@ def _postprocess_gradient(self, num_samples, results): """ Post-processing during backward pass of the network. """ - input_grad = np.zeros((num_samples, *self._output_shape, self._num_inputs)) if self._input_gradients else None + input_grad = ( + np.zeros((num_samples, *self._output_shape, self._num_inputs)) + if self._input_gradients + else None + ) weights_grad = np.zeros((num_samples, *self._output_shape, self._num_weights)) if self._input_gradients: @@ -238,7 +262,6 @@ def _postprocess_gradient(self, num_samples, results): num_grad_vars = self._num_weights for sample in range(num_samples): - for i in range(num_grad_vars): grad = results.gradients[sample][i] for k in grad.keys(): @@ -269,25 +292,24 @@ def _postprocess_gradient(self, num_samples, results): return input_grad, weights_grad def _backward( - self, - input_data: np.ndarray | None, - weights: np.ndarray | None + self, input_data: np.ndarray | None, weights: np.ndarray | None ) -> tuple[np.ndarray | None, np.ndarray | None]: - """Backward pass of the network. - """ + """Backward pass of the network.""" # prepare parameters in the required format parameter_values, num_samples = self._preprocess_gradient(input_data, weights) if self._input_gradients: job = self.gradient.run([self._circuit] * num_samples, parameter_values) else: - job = self.gradient.run([self._circuit] * num_samples, parameter_values, - parameters=[self._circuit.parameters[self._num_inputs:]]) + job = self.gradient.run( + [self._circuit] * num_samples, + parameter_values, + parameters=[self._circuit.parameters[self._num_inputs :]] * num_samples, + ) results = job.result() input_grad, weights_grad = self._postprocess_gradient(num_samples, results) return input_grad, weights_grad # `None` for gradients wrt input data, see TorchConnector - diff --git a/test/neural_networks/test_sampler_qnn.py b/test/neural_networks/test_sampler_qnn.py index 8fa01c3bd..cccd08f80 100644 --- a/test/neural_networks/test_sampler_qnn.py +++ b/test/neural_networks/test_sampler_qnn.py @@ -11,9 +11,14 @@ # that they have been altered from the originals. """Test Sampler QNN with Terra primitives.""" +import itertools +import unittest import numpy as np from test import QiskitMachineLearningTestCase +from ddt import ddt, data, idata, unpack + +from qiskit.circuit import QuantumCircuit, Parameter from qiskit.primitives import Sampler from qiskit.algorithms.gradients import ParamShiftSamplerGradient, FiniteDiffSamplerGradient from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap @@ -28,8 +33,16 @@ from test.connectors.test_torch import TestTorch import os -os.environ['KMP_DUPLICATE_LIB_OK']='True' +os.environ["KMP_DUPLICATE_LIB_OK"] = "True" + +DEFAULT = "default" +SHOTS = "shots" +SAMPLING = [True, False] # TODO +SAMPLERS = [DEFAULT, SHOTS] +INTERPRET_TYPES = [0, 1, 2] +BATCH_SIZES = [1, 2] +@ddt class TestSamplerQNN(QiskitMachineLearningTestCase): """Sampler QNN Tests.""" @@ -37,146 +50,266 @@ def setUp(self): super().setUp() algorithm_globals.random_seed = 12345 - # define test circuit - num_qubits = 3 - self.qc = RealAmplitudes(num_qubits, entanglement="linear", reps=1) - self.qi_qasm = QuantumInstance(Aer.get_backend("aer_simulator")) - self.sampler = Sampler() + # # define test circuit + # num_qubits = 3 + # self.qc = RealAmplitudes(num_qubits, entanglement="linear", reps=1) - def test_forward_pass(self): + # define feature map and ansatz + num_qubits = 2 + feature_map = ZZFeatureMap(num_qubits, reps=1) + var_form = RealAmplitudes(num_qubits, reps=1) - parity = lambda x: "{:b}".format(x).count("1") % 2 - output_shape = 2 # this is required in case of a callable with dense output + # construct circuit + self.qc = QuantumCircuit(num_qubits) + self.qc.append(feature_map, range(2)) + self.qc.append(var_form, range(2)) - circuit_qnn = CircuitQNN( - self.qc, - input_params=self.qc.parameters[:3], - weight_params=self.qc.parameters[3:], - sparse=False, - interpret=parity, - output_shape=output_shape, - quantum_instance=self.qi_qasm, - ) + # store params + self.input_params = list(feature_map.parameters) + self.weight_params = list(var_form.parameters) - sampler_qnn = SamplerQNN( - sampler=self.sampler, - circuit=self.qc, - input_params=self.qc.parameters[:3], - weight_params=self.qc.parameters[3:], - interpret=parity, - output_shape=output_shape, - ) + # define interpret functions + def interpret_1d(x): + return sum((s == "1" for s in f"{x:0b}")) % 2 - inputs = np.asarray(algorithm_globals.random.random(size=(1, circuit_qnn._num_inputs))) - weights = algorithm_globals.random.random(circuit_qnn._num_weights) + self.interpret_1d = interpret_1d + self.output_shape_1d = 2 # takes values in {0, 1} - circuit_qnn_fwd = circuit_qnn.forward(inputs, weights) - sampler_qnn_fwd = sampler_qnn.forward(inputs, weights) + def interpret_2d(x): + return np.array([self.interpret_1d(x), 2 * self.interpret_1d(x)]) - np.testing.assert_array_almost_equal( - np.asarray(sampler_qnn_fwd), np.asarray(circuit_qnn_fwd), 0.1 - ) + self.interpret_2d = interpret_2d + self.output_shape_2d = ( + 2, + 3, + ) # 1st dim. takes values in {0, 1} 2nd dim in {0, 1, 2} + + # define sampler primitives + self.sampler = Sampler() + self.sampler_shots = Sampler(options={"shots":100}) + + def _get_qnn(self, sampler_type, interpret_id): + """Construct QNN from configuration.""" - def test_backward_pass(self): + # get quantum instance + if sampler_type == SHOTS: + sampler = self.sampler_shots + elif sampler_type == DEFAULT: + sampler = self.sampler + else: + sampler = None - parity = lambda x: "{:b}".format(x).count("1") % 2 - output_shape = 2 # this is required in case of a callable with dense output - from qiskit.opflow import Gradient - circuit_qnn = CircuitQNN( + # get interpret setting + interpret = None + output_shape = None + if interpret_id == 1: + interpret = self.interpret_1d + output_shape = self.output_shape_1d + elif interpret_id == 2: + interpret = self.interpret_2d + output_shape = self.output_shape_2d + + # construct QNN + qnn = SamplerQNN( + sampler, self.qc, - input_params=self.qc.parameters[:3], - weight_params=self.qc.parameters[3:], - sparse=False, - interpret=parity, + input_params=self.input_params, + weight_params=self.weight_params, + interpret=interpret, output_shape=output_shape, - quantum_instance=self.qi_qasm, - gradient=Gradient("param_shift"), ) + return qnn - sampler_qnn = SamplerQNN( - sampler=self.sampler, - circuit=self.qc, - input_params=self.qc.parameters[:3], - weight_params=self.qc.parameters[3:], - interpret=parity, - output_shape=output_shape, - gradient=ParamShiftSamplerGradient(self.sampler), - ) + def _verify_qnn( + self, + qnn: CircuitQNN, + sampler_type: str, + batch_size: int, + ) -> None: + """ + Verifies that a QNN functions correctly - inputs = np.asarray(algorithm_globals.random.random(size=(1, circuit_qnn._num_inputs))) - weights = algorithm_globals.random.random(circuit_qnn._num_weights) + Args: + qnn: a QNN to check + sampler_type: + batch_size: - circuit_qnn_fwd = circuit_qnn.backward(inputs, weights) - sampler_qnn_fwd = sampler_qnn.backward(inputs, weights) + Returns: + None. + """ + input_data = np.zeros((batch_size, qnn.num_inputs)) + weights = np.zeros(qnn.num_weights) - print(circuit_qnn_fwd) - print(sampler_qnn_fwd) - np.testing.assert_array_almost_equal( - np.asarray(sampler_qnn_fwd[1]), np.asarray(circuit_qnn_fwd[1]), 0.1 - ) + # evaluate QNN forward pass + result = qnn.forward(input_data, weights) + self.assertTrue(isinstance(result, np.ndarray)) + # check forward result shape + self.assertEqual(result.shape, (batch_size, *qnn.output_shape)) - def test_input_gradients(self): + # evaluate QNN backward pass + input_grad, weights_grad = qnn.backward(input_data, weights) - parity = lambda x: "{:b}".format(x).count("1") % 2 - output_shape = 2 # this is required in case of a callable with dense output - from qiskit.opflow import Gradient - circuit_qnn = CircuitQNN( - self.qc, - input_params=self.qc.parameters[:3], - weight_params=self.qc.parameters[3:], - sparse=False, - interpret=parity, - output_shape=output_shape, - quantum_instance=self.qi_qasm, - gradient=Gradient("param_shift"), - input_gradients=True + self.assertIsNone(input_grad) + # verify that input gradients are None if turned off + self.assertEqual( + weights_grad.shape, (batch_size, *qnn.output_shape, qnn.num_weights) ) - sampler_qnn = SamplerQNN( - sampler=self.sampler, - circuit=self.qc, - input_params=self.qc.parameters[:3], - weight_params=self.qc.parameters[3:], - interpret=parity, - output_shape=output_shape, - gradient=ParamShiftSamplerGradient(self.sampler), - input_gradients=True - - ) + # verify that input gradients are not None if turned on + qnn.input_gradients = True + input_grad, weights_grad = qnn.backward(input_data, weights) - inputs = np.asarray(algorithm_globals.random.random(size=(1, circuit_qnn._num_inputs))) - weights = algorithm_globals.random.random(circuit_qnn._num_weights) + self.assertEqual(input_grad.shape, (batch_size, *qnn.output_shape, qnn.num_inputs)) + self.assertEqual( + weights_grad.shape, (batch_size, *qnn.output_shape, qnn.num_weights) + ) - circuit_qnn_fwd = circuit_qnn.backward(inputs, weights) - sampler_qnn_fwd = sampler_qnn.backward(inputs, weights) + @idata(itertools.product(SAMPLERS, INTERPRET_TYPES, BATCH_SIZES)) + @unpack + def test_sampler_qnn( + self, sampler_type, interpret_type, batch_size + ): + """Sampler QNN Test.""" + qnn = self._get_qnn(sampler_type, interpret_type) + self._verify_qnn(qnn, sampler_type, batch_size) - print(circuit_qnn_fwd) - print(sampler_qnn_fwd) - np.testing.assert_array_almost_equal( - np.asarray(sampler_qnn_fwd), np.asarray(circuit_qnn_fwd), 0.1 - ) - from qiskit_machine_learning.connectors import TorchConnector - import torch - - model = TorchConnector(sampler_qnn) - func = TorchConnector._TorchNNFunction.apply # (input, weights, qnn) - input_data = ( - torch.randn( - model.neural_network.num_inputs, - dtype=torch.double, - requires_grad=True, - ), - torch.randn( - model.neural_network.num_weights, - dtype=torch.double, - requires_grad=True, - ), - model.neural_network, - False, - ) - test = torch.autograd.gradcheck(func, input_data, eps=1e-4, atol=1e-3) # type: ignore - self.assertTrue(test) + # + # def test_forward_pass(self): + # + # parity = lambda x: "{:b}".format(x).count("1") % 2 + # output_shape = 2 # this is required in case of a callable with dense output + # + # circuit_qnn = CircuitQNN( + # self.qc, + # input_params=self.qc.parameters[:3], + # weight_params=self.qc.parameters[3:], + # sparse=False, + # interpret=parity, + # output_shape=output_shape, + # quantum_instance=self.qi_qasm, + # ) + # + # sampler_qnn = SamplerQNN( + # sampler=self.sampler, + # circuit=self.qc, + # input_params=self.qc.parameters[:3], + # weight_params=self.qc.parameters[3:], + # interpret=parity, + # output_shape=output_shape, + # ) + # + # inputs = np.asarray(algorithm_globals.random.random(size=(1, circuit_qnn._num_inputs))) + # weights = algorithm_globals.random.random(circuit_qnn._num_weights) + # + # circuit_qnn_fwd = circuit_qnn.forward(inputs, weights) + # sampler_qnn_fwd = sampler_qnn.forward(inputs, weights) + # + # np.testing.assert_array_almost_equal( + # np.asarray(sampler_qnn_fwd), np.asarray(circuit_qnn_fwd), 0.1 + # ) + # + # def test_backward_pass(self): + # + # parity = lambda x: "{:b}".format(x).count("1") % 2 + # output_shape = 2 # this is required in case of a callable with dense output + # from qiskit.opflow import Gradient + # + # circuit_qnn = CircuitQNN( + # self.qc, + # input_params=self.qc.parameters[:3], + # weight_params=self.qc.parameters[3:], + # sparse=False, + # interpret=parity, + # output_shape=output_shape, + # quantum_instance=self.qi_qasm, + # gradient=Gradient("param_shift"), + # ) + # + # sampler_qnn = SamplerQNN( + # sampler=self.sampler, + # circuit=self.qc, + # input_params=self.qc.parameters[:3], + # weight_params=self.qc.parameters[3:], + # interpret=parity, + # output_shape=output_shape, + # gradient=ParamShiftSamplerGradient(self.sampler), + # ) + # + # inputs = np.asarray(algorithm_globals.random.random(size=(1, circuit_qnn._num_inputs))) + # weights = algorithm_globals.random.random(circuit_qnn._num_weights) + # + # circuit_qnn_fwd = circuit_qnn.backward(inputs, weights) + # sampler_qnn_fwd = sampler_qnn.backward(inputs, weights) + # + # print(circuit_qnn_fwd) + # print(sampler_qnn_fwd) + # np.testing.assert_array_almost_equal( + # np.asarray(sampler_qnn_fwd[1]), np.asarray(circuit_qnn_fwd[1]), 0.1 + # ) + # + # def test_input_gradients(self): + # + # parity = lambda x: "{:b}".format(x).count("1") % 2 + # output_shape = 2 # this is required in case of a callable with dense output + # from qiskit.opflow import Gradient + # + # circuit_qnn = CircuitQNN( + # self.qc, + # input_params=self.qc.parameters[:3], + # weight_params=self.qc.parameters[3:], + # sparse=False, + # interpret=parity, + # output_shape=output_shape, + # quantum_instance=self.qi_qasm, + # gradient=Gradient("param_shift"), + # input_gradients=True, + # ) + # + # sampler_qnn = SamplerQNN( + # sampler=self.sampler, + # circuit=self.qc, + # input_params=self.qc.parameters[:3], + # weight_params=self.qc.parameters[3:], + # interpret=parity, + # output_shape=output_shape, + # gradient=ParamShiftSamplerGradient(self.sampler), + # input_gradients=True, + # ) + # + # inputs = np.asarray(algorithm_globals.random.random(size=(1, circuit_qnn._num_inputs))) + # weights = algorithm_globals.random.random(circuit_qnn._num_weights) + # + # circuit_qnn_fwd = circuit_qnn.backward(inputs, weights) + # sampler_qnn_fwd = sampler_qnn.backward(inputs, weights) + # + # print(circuit_qnn_fwd) + # print(sampler_qnn_fwd) + # np.testing.assert_array_almost_equal( + # np.asarray(sampler_qnn_fwd), np.asarray(circuit_qnn_fwd), 0.1 + # ) + # + # from qiskit_machine_learning.connectors import TorchConnector + # import torch + # + # model = TorchConnector(sampler_qnn) + # func = TorchConnector._TorchNNFunction.apply # (input, weights, qnn) + # input_data = ( + # torch.randn( + # model.neural_network.num_inputs, + # dtype=torch.double, + # requires_grad=True, + # ), + # torch.randn( + # model.neural_network.num_weights, + # dtype=torch.double, + # requires_grad=True, + # ), + # model.neural_network, + # False, + # ) + # test = torch.autograd.gradcheck(func, input_data, eps=1e-4, atol=1e-3) # type: ignore + # self.assertTrue(test) # def test_torch_connector(self): # from qiskit_machine_learning.connectors import TorchConnector From f4f1771f534d0f03f1c1bd570645e665daaf8dab Mon Sep 17 00:00:00 2001 From: ElePT Date: Mon, 10 Oct 2022 13:33:31 +0200 Subject: [PATCH 15/61] Add gradient unit test --- test/neural_networks/test_sampler_qnn.py | 64 ++++++++++++++++++++---- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/test/neural_networks/test_sampler_qnn.py b/test/neural_networks/test_sampler_qnn.py index cccd08f80..26b25a56a 100644 --- a/test/neural_networks/test_sampler_qnn.py +++ b/test/neural_networks/test_sampler_qnn.py @@ -37,11 +37,12 @@ DEFAULT = "default" SHOTS = "shots" -SAMPLING = [True, False] # TODO +SAMPLING = [True, False] # TODO SAMPLERS = [DEFAULT, SHOTS] INTERPRET_TYPES = [0, 1, 2] BATCH_SIZES = [1, 2] + @ddt class TestSamplerQNN(QiskitMachineLearningTestCase): """Sampler QNN Tests.""" @@ -86,7 +87,7 @@ def interpret_2d(x): # define sampler primitives self.sampler = Sampler() - self.sampler_shots = Sampler(options={"shots":100}) + self.sampler_shots = Sampler(options={"shots": 100}) def _get_qnn(self, sampler_type, interpret_id): """Construct QNN from configuration.""" @@ -151,28 +152,69 @@ def _verify_qnn( self.assertIsNone(input_grad) # verify that input gradients are None if turned off - self.assertEqual( - weights_grad.shape, (batch_size, *qnn.output_shape, qnn.num_weights) - ) + self.assertEqual(weights_grad.shape, (batch_size, *qnn.output_shape, qnn.num_weights)) # verify that input gradients are not None if turned on qnn.input_gradients = True input_grad, weights_grad = qnn.backward(input_data, weights) self.assertEqual(input_grad.shape, (batch_size, *qnn.output_shape, qnn.num_inputs)) - self.assertEqual( - weights_grad.shape, (batch_size, *qnn.output_shape, qnn.num_weights) - ) + self.assertEqual(weights_grad.shape, (batch_size, *qnn.output_shape, qnn.num_weights)) @idata(itertools.product(SAMPLERS, INTERPRET_TYPES, BATCH_SIZES)) @unpack - def test_sampler_qnn( - self, sampler_type, interpret_type, batch_size - ): + def test_sampler_qnn(self, sampler_type, interpret_type, batch_size): """Sampler QNN Test.""" qnn = self._get_qnn(sampler_type, interpret_type) self._verify_qnn(qnn, sampler_type, batch_size) + @idata(itertools.product(INTERPRET_TYPES, BATCH_SIZES)) + def test_sampler_qnn_gradient(self, config): + """Circuit QNN Sampler Test.""" + + # get configuration + interpret_id, batch_size = config + + # get QNN + qnn = self._get_qnn(DEFAULT, interpret_id) + + # set input gradients to True + qnn.input_gradients = True + input_data = np.ones((batch_size, qnn.num_inputs)) + weights = np.ones(qnn.num_weights) + input_grad, weights_grad = qnn.backward(input_data, weights) + + # test input gradients + eps = 1e-2 + for k in range(qnn.num_inputs): + delta = np.zeros(input_data.shape) + delta[:, k] = eps + + f_1 = qnn.forward(input_data + delta, weights) + f_2 = qnn.forward(input_data - delta, weights) + + grad = (f_1 - f_2) / (2 * eps) + input_grad_ = input_grad.reshape((batch_size, -1, qnn.num_inputs))[:, :, k].reshape( + grad.shape + ) + diff = input_grad_ - grad + self.assertAlmostEqual(np.max(np.abs(diff)), 0.0, places=3) + + # test weight gradients + eps = 1e-2 + for k in range(qnn.num_weights): + delta = np.zeros(weights.shape) + delta[k] = eps + + f_1 = qnn.forward(input_data, weights + delta) + f_2 = qnn.forward(input_data, weights - delta) + + grad = (f_1 - f_2) / (2 * eps) + weights_grad_ = weights_grad.reshape((batch_size, -1, qnn.num_weights))[ + :, :, k + ].reshape(grad.shape) + diff = weights_grad_ - grad + self.assertAlmostEqual(np.max(np.abs(diff)), 0.0, places=3) # # def test_forward_pass(self): From 7bf715d993449703315fcbd1b0676242234f8d9a Mon Sep 17 00:00:00 2001 From: ElePT Date: Mon, 10 Oct 2022 13:52:12 +0200 Subject: [PATCH 16/61] Add torch tests --- test/connectors/test_torch_networks.py | 32 +++- test/neural_networks/test_sampler_qnn.py | 194 +++++++---------------- 2 files changed, 82 insertions(+), 144 deletions(-) diff --git a/test/connectors/test_torch_networks.py b/test/connectors/test_torch_networks.py index c3c2cbf48..d6c7a707f 100644 --- a/test/connectors/test_torch_networks.py +++ b/test/connectors/test_torch_networks.py @@ -19,9 +19,10 @@ from qiskit import QuantumCircuit from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap -from qiskit_machine_learning.neural_networks import CircuitQNN, TwoLayerQNN, NeuralNetwork -from qiskit_machine_learning.connectors import TorchConnector +from qiskit.primitives import Sampler +from qiskit_machine_learning.neural_networks import CircuitQNN, TwoLayerQNN, NeuralNetwork, SamplerQNN +from qiskit_machine_learning.connectors import TorchConnector @ddt class TestTorchNetworks(TestTorch): @@ -86,7 +87,29 @@ def _create_opflow_qnn(self) -> TwoLayerQNN: ) return qnn - @idata(["opflow", "circuit_qnn"]) + def _create_sampler_qnn(self) -> SamplerQNN: + output_shape, interpret = 2, lambda x: f"{x:b}".count("1") % 2 + num_inputs = 2 + + feature_map = ZZFeatureMap(num_inputs) + ansatz = RealAmplitudes(num_inputs, entanglement="linear", reps=1) + + qc = QuantumCircuit(num_inputs) + qc.append(feature_map, range(num_inputs)) + qc.append(ansatz, range(num_inputs)) + + qnn = SamplerQNN( + Sampler(), + qc, + input_params=feature_map.parameters, + weight_params=ansatz.parameters, + input_gradients=True, # for hybrid qnn + interpret=interpret, + output_shape=output_shape, + ) + return qnn + + @idata(["opflow", "circuit_qnn", "sampler_qnn"]) def test_hybrid_batch_gradients(self, qnn_type: str): """Test gradient back-prop for batch input in a qnn.""" import torch @@ -100,6 +123,9 @@ def test_hybrid_batch_gradients(self, qnn_type: str): elif qnn_type == "circuit_qnn": qnn = self._create_circuit_qnn() output_size = 2 + elif qnn_type == "sampler_qnn": + qnn = self._create_sampler_qnn() + output_size = 2 else: raise ValueError("Unsupported QNN type") diff --git a/test/neural_networks/test_sampler_qnn.py b/test/neural_networks/test_sampler_qnn.py index 26b25a56a..b437da6ee 100644 --- a/test/neural_networks/test_sampler_qnn.py +++ b/test/neural_networks/test_sampler_qnn.py @@ -39,7 +39,7 @@ SHOTS = "shots" SAMPLING = [True, False] # TODO SAMPLERS = [DEFAULT, SHOTS] -INTERPRET_TYPES = [0, 1, 2] +INTERPRET_TYPES = [2] BATCH_SIZES = [1, 2] @@ -170,7 +170,7 @@ def test_sampler_qnn(self, sampler_type, interpret_type, batch_size): @idata(itertools.product(INTERPRET_TYPES, BATCH_SIZES)) def test_sampler_qnn_gradient(self, config): - """Circuit QNN Sampler Test.""" + """Sampler QNN Gradient Test.""" # get configuration interpret_id, batch_size = config @@ -216,142 +216,54 @@ def test_sampler_qnn_gradient(self, config): diff = weights_grad_ - grad self.assertAlmostEqual(np.max(np.abs(diff)), 0.0, places=3) - # - # def test_forward_pass(self): - # - # parity = lambda x: "{:b}".format(x).count("1") % 2 - # output_shape = 2 # this is required in case of a callable with dense output - # - # circuit_qnn = CircuitQNN( - # self.qc, - # input_params=self.qc.parameters[:3], - # weight_params=self.qc.parameters[3:], - # sparse=False, - # interpret=parity, - # output_shape=output_shape, - # quantum_instance=self.qi_qasm, - # ) - # - # sampler_qnn = SamplerQNN( - # sampler=self.sampler, - # circuit=self.qc, - # input_params=self.qc.parameters[:3], - # weight_params=self.qc.parameters[3:], - # interpret=parity, - # output_shape=output_shape, - # ) - # - # inputs = np.asarray(algorithm_globals.random.random(size=(1, circuit_qnn._num_inputs))) - # weights = algorithm_globals.random.random(circuit_qnn._num_weights) - # - # circuit_qnn_fwd = circuit_qnn.forward(inputs, weights) - # sampler_qnn_fwd = sampler_qnn.forward(inputs, weights) - # - # np.testing.assert_array_almost_equal( - # np.asarray(sampler_qnn_fwd), np.asarray(circuit_qnn_fwd), 0.1 - # ) - # - # def test_backward_pass(self): - # - # parity = lambda x: "{:b}".format(x).count("1") % 2 - # output_shape = 2 # this is required in case of a callable with dense output - # from qiskit.opflow import Gradient - # - # circuit_qnn = CircuitQNN( - # self.qc, - # input_params=self.qc.parameters[:3], - # weight_params=self.qc.parameters[3:], - # sparse=False, - # interpret=parity, - # output_shape=output_shape, - # quantum_instance=self.qi_qasm, - # gradient=Gradient("param_shift"), - # ) - # - # sampler_qnn = SamplerQNN( - # sampler=self.sampler, - # circuit=self.qc, - # input_params=self.qc.parameters[:3], - # weight_params=self.qc.parameters[3:], - # interpret=parity, - # output_shape=output_shape, - # gradient=ParamShiftSamplerGradient(self.sampler), - # ) - # - # inputs = np.asarray(algorithm_globals.random.random(size=(1, circuit_qnn._num_inputs))) - # weights = algorithm_globals.random.random(circuit_qnn._num_weights) - # - # circuit_qnn_fwd = circuit_qnn.backward(inputs, weights) - # sampler_qnn_fwd = sampler_qnn.backward(inputs, weights) - # - # print(circuit_qnn_fwd) - # print(sampler_qnn_fwd) - # np.testing.assert_array_almost_equal( - # np.asarray(sampler_qnn_fwd[1]), np.asarray(circuit_qnn_fwd[1]), 0.1 - # ) - # - # def test_input_gradients(self): - # - # parity = lambda x: "{:b}".format(x).count("1") % 2 - # output_shape = 2 # this is required in case of a callable with dense output - # from qiskit.opflow import Gradient - # - # circuit_qnn = CircuitQNN( - # self.qc, - # input_params=self.qc.parameters[:3], - # weight_params=self.qc.parameters[3:], - # sparse=False, - # interpret=parity, - # output_shape=output_shape, - # quantum_instance=self.qi_qasm, - # gradient=Gradient("param_shift"), - # input_gradients=True, - # ) - # - # sampler_qnn = SamplerQNN( - # sampler=self.sampler, - # circuit=self.qc, - # input_params=self.qc.parameters[:3], - # weight_params=self.qc.parameters[3:], - # interpret=parity, - # output_shape=output_shape, - # gradient=ParamShiftSamplerGradient(self.sampler), - # input_gradients=True, - # ) - # - # inputs = np.asarray(algorithm_globals.random.random(size=(1, circuit_qnn._num_inputs))) - # weights = algorithm_globals.random.random(circuit_qnn._num_weights) - # - # circuit_qnn_fwd = circuit_qnn.backward(inputs, weights) - # sampler_qnn_fwd = sampler_qnn.backward(inputs, weights) - # - # print(circuit_qnn_fwd) - # print(sampler_qnn_fwd) - # np.testing.assert_array_almost_equal( - # np.asarray(sampler_qnn_fwd), np.asarray(circuit_qnn_fwd), 0.1 - # ) - # - # from qiskit_machine_learning.connectors import TorchConnector - # import torch - # - # model = TorchConnector(sampler_qnn) - # func = TorchConnector._TorchNNFunction.apply # (input, weights, qnn) - # input_data = ( - # torch.randn( - # model.neural_network.num_inputs, - # dtype=torch.double, - # requires_grad=True, - # ), - # torch.randn( - # model.neural_network.num_weights, - # dtype=torch.double, - # requires_grad=True, - # ), - # model.neural_network, - # False, - # ) - # test = torch.autograd.gradcheck(func, input_data, eps=1e-4, atol=1e-3) # type: ignore - # self.assertTrue(test) - - # def test_torch_connector(self): - # from qiskit_machine_learning.connectors import TorchConnector + + def test_circuit_vs_sampler_qnn(self): + """Circuit vs Sampler QNN Test. To be removed once CircuitQNN is deprecated""" + from qiskit.opflow import Gradient + import importlib + aer = importlib.import_module("qiskit.providers.aer") + + parity = lambda x: "{:b}".format(x).count("1") % 2 + output_shape = 2 # this is required in case of a callable with dense output + + qi_qasm = QuantumInstance( + aer.AerSimulator(), + shots=100, + seed_simulator=algorithm_globals.random_seed, + seed_transpiler=algorithm_globals.random_seed, + ) + + circuit_qnn = CircuitQNN( + self.qc, + input_params=self.qc.parameters[:3], + weight_params=self.qc.parameters[3:], + sparse=False, + interpret=parity, + output_shape=output_shape, + quantum_instance=qi_qasm, + gradient=Gradient("param_shift"), + input_gradients=True, + ) + + sampler_qnn = SamplerQNN( + sampler=self.sampler, + circuit=self.qc, + input_params=self.qc.parameters[:3], + weight_params=self.qc.parameters[3:], + interpret=parity, + output_shape=output_shape, + gradient=ParamShiftSamplerGradient(self.sampler), + input_gradients=True, + ) + + inputs = np.asarray(algorithm_globals.random.random(size=(1, circuit_qnn._num_inputs))) + weights = algorithm_globals.random.random(circuit_qnn._num_weights) + + circuit_qnn_fwd = circuit_qnn.backward(inputs, weights) + sampler_qnn_fwd = sampler_qnn.backward(inputs, weights) + + print(circuit_qnn_fwd) + print(sampler_qnn_fwd) + np.testing.assert_array_almost_equal( + np.asarray(sampler_qnn_fwd), np.asarray(circuit_qnn_fwd), 0.1 + ) From aeb05f075a46915d654f4b8a6723cd87fc134206 Mon Sep 17 00:00:00 2001 From: ElePT <57907331+ElePT@users.noreply.github.com> Date: Mon, 10 Oct 2022 13:53:51 +0200 Subject: [PATCH 17/61] Remove unwanted change circuitQNN --- qiskit_machine_learning/neural_networks/circuit_qnn.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qiskit_machine_learning/neural_networks/circuit_qnn.py b/qiskit_machine_learning/neural_networks/circuit_qnn.py index ff64ed1ea..21d029157 100644 --- a/qiskit_machine_learning/neural_networks/circuit_qnn.py +++ b/qiskit_machine_learning/neural_networks/circuit_qnn.py @@ -490,6 +490,7 @@ def _probability_gradients( num_grad_vars = self._num_inputs + self._num_weights else: num_grad_vars = self._num_weights + # construct gradients for sample in range(num_samples): for i in range(num_grad_vars): From f727ea052d8805a42439e2635402054193270651 Mon Sep 17 00:00:00 2001 From: ElePT Date: Mon, 10 Oct 2022 13:55:30 +0200 Subject: [PATCH 18/61] Remove utils --- qiskit_machine_learning/utils/utils.py | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 qiskit_machine_learning/utils/utils.py diff --git a/qiskit_machine_learning/utils/utils.py b/qiskit_machine_learning/utils/utils.py deleted file mode 100644 index 901658f3c..000000000 --- a/qiskit_machine_learning/utils/utils.py +++ /dev/null @@ -1,20 +0,0 @@ -# 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. - -import numpy as np - - -def make_2d(array: np.ndarray, n_copies: int): - """ - Takes a 1D numpy array and copies n times it along a second axis. - """ - return np.repeat(array[np.newaxis, :], n_copies, axis=0) From 0a104ae199ed7343f2245fde2896ac3a4e24d2ed Mon Sep 17 00:00:00 2001 From: ElePT Date: Mon, 10 Oct 2022 14:11:59 +0200 Subject: [PATCH 19/61] Fix style, lint --- test/connectors/test_torch_networks.py | 8 ++++++- test/neural_networks/test_sampler_qnn.py | 27 ++++++++---------------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/test/connectors/test_torch_networks.py b/test/connectors/test_torch_networks.py index d6c7a707f..d942f3c1f 100644 --- a/test/connectors/test_torch_networks.py +++ b/test/connectors/test_torch_networks.py @@ -21,9 +21,15 @@ from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap from qiskit.primitives import Sampler -from qiskit_machine_learning.neural_networks import CircuitQNN, TwoLayerQNN, NeuralNetwork, SamplerQNN +from qiskit_machine_learning.neural_networks import ( + CircuitQNN, + TwoLayerQNN, + NeuralNetwork, + SamplerQNN, +) from qiskit_machine_learning.connectors import TorchConnector + @ddt class TestTorchNetworks(TestTorch): """Base class for hybrid PyTorch network tests.""" diff --git a/test/neural_networks/test_sampler_qnn.py b/test/neural_networks/test_sampler_qnn.py index b437da6ee..50fb41a78 100644 --- a/test/neural_networks/test_sampler_qnn.py +++ b/test/neural_networks/test_sampler_qnn.py @@ -11,29 +11,24 @@ # that they have been altered from the originals. """Test Sampler QNN with Terra primitives.""" + +from test import QiskitMachineLearningTestCase + import itertools -import unittest import numpy as np -from test import QiskitMachineLearningTestCase -from ddt import ddt, data, idata, unpack +from ddt import ddt, idata, unpack -from qiskit.circuit import QuantumCircuit, Parameter +from qiskit.circuit import QuantumCircuit from qiskit.primitives import Sampler -from qiskit.algorithms.gradients import ParamShiftSamplerGradient, FiniteDiffSamplerGradient +from qiskit.algorithms.gradients import ParamShiftSamplerGradient from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap from qiskit.utils import QuantumInstance, algorithm_globals -from qiskit import Aer -from qiskit.utils import QuantumInstance from qiskit_machine_learning.neural_networks import CircuitQNN from qiskit_machine_learning.neural_networks.sampler_qnn import SamplerQNN algorithm_globals.random_seed = 42 -from test.connectors.test_torch import TestTorch -import os - -os.environ["KMP_DUPLICATE_LIB_OK"] = "True" DEFAULT = "default" SHOTS = "shots" @@ -124,7 +119,6 @@ def _get_qnn(self, sampler_type, interpret_id): def _verify_qnn( self, qnn: CircuitQNN, - sampler_type: str, batch_size: int, ) -> None: """ @@ -132,8 +126,7 @@ def _verify_qnn( Args: qnn: a QNN to check - sampler_type: - batch_size: + batch_size: batch size Returns: None. @@ -166,7 +159,7 @@ def _verify_qnn( def test_sampler_qnn(self, sampler_type, interpret_type, batch_size): """Sampler QNN Test.""" qnn = self._get_qnn(sampler_type, interpret_type) - self._verify_qnn(qnn, sampler_type, batch_size) + self._verify_qnn(qnn, batch_size) @idata(itertools.product(INTERPRET_TYPES, BATCH_SIZES)) def test_sampler_qnn_gradient(self, config): @@ -216,11 +209,11 @@ def test_sampler_qnn_gradient(self, config): diff = weights_grad_ - grad self.assertAlmostEqual(np.max(np.abs(diff)), 0.0, places=3) - def test_circuit_vs_sampler_qnn(self): """Circuit vs Sampler QNN Test. To be removed once CircuitQNN is deprecated""" from qiskit.opflow import Gradient import importlib + aer = importlib.import_module("qiskit.providers.aer") parity = lambda x: "{:b}".format(x).count("1") % 2 @@ -262,8 +255,6 @@ def test_circuit_vs_sampler_qnn(self): circuit_qnn_fwd = circuit_qnn.backward(inputs, weights) sampler_qnn_fwd = sampler_qnn.backward(inputs, weights) - print(circuit_qnn_fwd) - print(sampler_qnn_fwd) np.testing.assert_array_almost_equal( np.asarray(sampler_qnn_fwd), np.asarray(circuit_qnn_fwd), 0.1 ) From c9abd9182626a51be0785fe1edb232d65c7192c0 Mon Sep 17 00:00:00 2001 From: ElePT Date: Mon, 10 Oct 2022 17:47:14 +0200 Subject: [PATCH 20/61] Add VQC --- .../algorithms/classifiers/vqc.py | 36 ++++-- .../neural_networks/sampler_qnn.py | 11 +- test/algorithms/classifiers/test_vqc.py | 115 ++++++++++++++---- 3 files changed, 124 insertions(+), 38 deletions(-) diff --git a/qiskit_machine_learning/algorithms/classifiers/vqc.py b/qiskit_machine_learning/algorithms/classifiers/vqc.py index d08748fc8..5f57f9998 100644 --- a/qiskit_machine_learning/algorithms/classifiers/vqc.py +++ b/qiskit_machine_learning/algorithms/classifiers/vqc.py @@ -20,8 +20,9 @@ from qiskit.providers import Backend from qiskit.utils import QuantumInstance from qiskit.algorithms.optimizers import Optimizer, OptimizerResult +from qiskit.primitives import BaseSampler -from ...neural_networks import CircuitQNN +from ...neural_networks import CircuitQNN, SamplerQNN from ...utils import derive_num_qubits_feature_map_ansatz from ...utils.loss_functions import Loss @@ -47,6 +48,7 @@ class VQC(NeuralNetworkClassifier): def __init__( self, + sampler: BaseSampler = None, num_qubits: int | None = None, feature_map: QuantumCircuit | None = None, ansatz: QuantumCircuit | None = None, @@ -100,16 +102,28 @@ def __init__( self._circuit.compose(self.feature_map, inplace=True) self._circuit.compose(self.ansatz, inplace=True) - # construct circuit QNN - neural_network = CircuitQNN( - self._circuit, - input_params=self.feature_map.parameters, - weight_params=self.ansatz.parameters, - interpret=self._get_interpret(2), - output_shape=2, - quantum_instance=quantum_instance, - input_gradients=False, - ) + if sampler is None: + # construct circuit QNN + neural_network = CircuitQNN( + self._circuit, + input_params=self.feature_map.parameters, + weight_params=self.ansatz.parameters, + interpret=self._get_interpret(2), + output_shape=2, + quantum_instance=quantum_instance, + input_gradients=False, + ) + else: + # construct sampler QNN + neural_network = SamplerQNN( + sampler, + self._circuit, + input_params=self.feature_map.parameters, + weight_params=self.ansatz.parameters, + interpret=self._get_interpret(2), + output_shape=2, + input_gradients=False, + ) super().__init__( neural_network=neural_network, diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index 245ad79d0..e9149d41b 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -19,8 +19,7 @@ from typing import Callable, cast, Iterable, Sequence import numpy as np -from qiskit.algorithms.gradients import BaseSamplerGradient -from qiskit.algorithms.gradients import ParamShiftSamplerGradient +from qiskit.algorithms.gradients import BaseSamplerGradient, ParamShiftSamplerGradient from qiskit.circuit import Parameter, QuantumCircuit from qiskit.primitives import BaseSampler from qiskit_machine_learning.exceptions import QiskitMachineLearningError @@ -80,7 +79,7 @@ def __init__( # the final output shape will depend on the # interpret method, and it must be set before # applying the default to the latter - self.set_interpret_out_shape(interpret, output_shape) + self.set_interpret(interpret, output_shape) self._input_gradients = input_gradients @@ -117,7 +116,7 @@ def interpret(self) -> Callable[[int], int | tuple[int, ...]] | None: the constructor or can not be implicitly derived, then ``None`` is returned.""" return self._interpret - def set_interpret_out_shape( + def set_interpret( self, interpret: Callable[[int], int | tuple[int, ...]] | None = None, output_shape: int | tuple[int, ...] | None = None, @@ -143,8 +142,8 @@ def _compute_output_shape( ) -> tuple[int, ...]: """Validate and compute the output shape.""" - # this definition is required by mypy - output_shape_: tuple[int, ...] = (-1,) + # # this definition is required by mypy + # output_shape_: tuple[int, ...] = (-1,) if interpret is not None: if output_shape is None: diff --git a/test/algorithms/classifiers/test_vqc.py b/test/algorithms/classifiers/test_vqc.py index 4895327c4..cdef75ea1 100644 --- a/test/algorithms/classifiers/test_vqc.py +++ b/test/algorithms/classifiers/test_vqc.py @@ -31,11 +31,12 @@ from qiskit.algorithms.optimizers import COBYLA, L_BFGS_B from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap, ZFeatureMap from qiskit.utils import QuantumInstance, algorithm_globals, optionals +from qiskit.primitives import Sampler from qiskit_machine_learning.algorithms import VQC from qiskit_machine_learning.exceptions import QiskitMachineLearningError -QUANTUM_INSTANCES = ["statevector", "qasm"] +QUANTUM_INSTANCES = ["statevector", "sampler", "qasm"] NUM_QUBITS_LIST = [2, None] FEATURE_MAPS = ["zz_feature_map", None] ANSATZES = ["real_amplitudes", None] @@ -90,11 +91,12 @@ def setUp(self): seed_simulator=algorithm_globals.random_seed, seed_transpiler=algorithm_globals.random_seed, ) - + sampler = Sampler() # We want string keys to ensure DDT-generated tests have meaningful names. self.properties = { "statevector": statevector, "qasm": qasm, + "sampler": sampler, "bfgs": L_BFGS_B(maxiter=5), "cobyla": COBYLA(maxiter=25), "real_amplitudes": RealAmplitudes(num_qubits=2, reps=1), @@ -119,8 +121,13 @@ def test_VQC(self, q_i, num_qubits, f_m, ans, opt, d_s): self.skipTest( "At least one of num_qubits, feature_map, or ansatz must be set by the user." ) + if q_i != "sampler": + sampler = None + quantum_instance = self.properties.get(q_i) + else: + sampler = self.properties.get(q_i) + quantum_instance = None - quantum_instance = self.properties.get(q_i) feature_map = self.properties.get(f_m) optimizer = self.properties.get(opt) ansatz = self.properties.get(ans) @@ -129,6 +136,7 @@ def test_VQC(self, q_i, num_qubits, f_m, ans, opt, d_s): initial_point = np.array([0.5] * ansatz.num_parameters) if ansatz is not None else None classifier = VQC( + sampler=sampler, quantum_instance=quantum_instance, num_qubits=num_qubits, feature_map=feature_map, @@ -148,27 +156,45 @@ def test_VQC(self, q_i, num_qubits, f_m, ans, opt, d_s): # the predicted value should be in the labels self.assertTrue(np.all(predict == unique_labels, axis=1).any()) - def test_VQC_non_parameterized(self): + @idata(QUANTUM_INSTANCES[:-1]) + def test_VQC_non_parameterized(self, test): """ Test VQC without an optimizer set. """ + if test != "sampler": + sampler = None + quantum_instance = self.properties.get(test) + else: + sampler = self.properties.get(test) + quantum_instance = None + classifier = VQC( + sampler=sampler, num_qubits=2, optimizer=None, - quantum_instance=self.properties.get("statevector"), + quantum_instance=quantum_instance, ) dataset = self.properties.get("binary") classifier.fit(dataset.x, dataset.y) score = classifier.score(dataset.x, dataset.y) self.assertGreater(score, 0.5) - @idata(DATASETS) - def test_warm_start(self, d_s): + @idata(itertools.product(DATASETS, QUANTUM_INSTANCES[:-1])) + @unpack + def test_warm_start(self, d_s, test): """Test VQC when training from a warm start.""" + if test != "sampler": + sampler = None + quantum_instance = self.properties.get(test) + else: + sampler = self.properties.get(test) + quantum_instance = None + classifier = VQC( + sampler=sampler, feature_map=self.properties.get("zz_feature_map"), - quantum_instance=self.properties.get("statevector"), + quantum_instance=quantum_instance, warm_start=True, ) dataset = self.properties.get(d_s) @@ -198,19 +224,28 @@ def wrapper(num_classes): return wrapper - def test_batches_with_incomplete_labels(self): + @idata(QUANTUM_INSTANCES[:-1]) + def test_batches_with_incomplete_labels(self, test): """Test VQC when targets are one-hot and some batches don't have all possible labels.""" + if test != "sampler": + sampler = None + quantum_instance = self.properties.get(test) + else: + sampler = self.properties.get(test) + quantum_instance = None + # Generate data with batches that have incomplete labels. x = algorithm_globals.random.random((6, 2)) y = np.asarray([0, 0, 1, 1, 2, 2]) y_one_hot = OneHotEncoder().fit_transform(y.reshape(-1, 1)) classifier = VQC( + sampler=sampler, feature_map=self.properties.get("zz_feature_map"), ansatz=self.properties.get("real_amplitudes"), warm_start=True, - quantum_instance=self.properties.get("statevector"), + quantum_instance=quantum_instance, ) classifier._get_interpret = self._get_num_classes(classifier._get_interpret) @@ -229,29 +264,43 @@ def test_batches_with_incomplete_labels(self): with self.subTest("Check correct number of classes is used to build CircuitQNN."): self.assertTrue((np.asarray(self.num_classes_by_batch) == 3).all()) - def test_multilabel_targets_raise_an_error(self): + @idata(QUANTUM_INSTANCES[:-1]) + def test_multilabel_targets_raise_an_error(self, test): """Tests VQC multi-label input raises an error.""" + if test != "sampler": + sampler = None + quantum_instance = self.properties.get(test) + else: + sampler = self.properties.get(test) + quantum_instance = None + # Generate multi-label data. x = algorithm_globals.random.random((3, 2)) y = np.asarray([[1, 1, 0], [1, 0, 1], [0, 1, 1]]) - classifier = VQC(num_qubits=2, quantum_instance=self.properties.get("statevector")) + classifier = VQC(sampler=sampler, num_qubits=2, quantum_instance=quantum_instance) with self.assertRaises(QiskitMachineLearningError): classifier.fit(x, y) - def test_changing_classes_raises_error(self): + @idata(QUANTUM_INSTANCES[:-1]) + def test_changing_classes_raises_error(self, test): """Tests VQC raises an error when fitting new data with a different number of classes.""" + if test != "sampler": + sampler = None + quantum_instance = self.properties.get(test) + else: + sampler = self.properties.get(test) + quantum_instance = None + targets1 = np.asarray([[0, 0, 1], [0, 1, 0]]) targets2 = np.asarray([[0, 1], [1, 0]]) features1 = algorithm_globals.random.random((len(targets1), 2)) features2 = algorithm_globals.random.random((len(targets2), 2)) classifier = VQC( - num_qubits=2, - warm_start=True, - quantum_instance=self.properties.get("statevector"), + sampler=sampler, num_qubits=2, warm_start=True, quantum_instance=quantum_instance ) classifier.fit(features1, targets1) with self.assertRaises(QiskitMachineLearningError): @@ -261,6 +310,9 @@ def test_changing_classes_raises_error(self): @unpack def test_sparse_arrays(self, q_i, loss): """Tests VQC on sparse features and labels.""" + if q_i == "sampler": + self.skipTest("skipping test because SamplerQNN does not support sparse arrays") + quantum_instance = self.properties.get(q_i) classifier = VQC(num_qubits=2, loss=loss, quantum_instance=quantum_instance) x = scipy.sparse.csr_matrix([[0, 0], [1, 1]]) @@ -271,12 +323,22 @@ def test_sparse_arrays(self, q_i, loss): score = classifier.score(x, y) self.assertGreaterEqual(score, 0.5) - def test_categorical(self): + @idata(QUANTUM_INSTANCES[:-1]) + def test_categorical(self, test): """Test VQC on categorical labels.""" + + if test != "sampler": + sampler = None + quantum_instance = self.properties.get(test) + else: + sampler = self.properties.get(test) + quantum_instance = None + classifier = VQC( + sampler=sampler, num_qubits=2, optimizer=COBYLA(25), - quantum_instance=self.properties.get("statevector"), + quantum_instance=quantum_instance, ) dataset = self.properties.get("no_one_hot") features = dataset.x @@ -291,14 +353,24 @@ def test_categorical(self): predict = classifier.predict(features[0, :]) self.assertIn(predict, ["A", "B"]) - def test_circuit_extensions(self): + @idata(QUANTUM_INSTANCES[:-1]) + def test_circuit_extensions(self, test): """Test VQC when the number of qubits is different compared to the feature map/ansatz.""" + + if test != "sampler": + sampler = None + quantum_instance = self.properties.get(test) + else: + sampler = self.properties.get(test) + quantum_instance = None + num_qubits = 2 classifier = VQC( + sampler=sampler, num_qubits=num_qubits, feature_map=ZFeatureMap(1), ansatz=RealAmplitudes(1), - quantum_instance=self.properties.get("statevector"), + quantum_instance=quantum_instance, ) self.assertEqual(classifier.feature_map.num_qubits, num_qubits) self.assertEqual(classifier.ansatz.num_qubits, num_qubits) @@ -306,10 +378,11 @@ def test_circuit_extensions(self): qc = QuantumCircuit(1) with self.assertRaises(QiskitMachineLearningError): _ = VQC( + sampler=sampler, num_qubits=num_qubits, feature_map=qc, ansatz=qc, - quantum_instance=self.properties.get("statevector"), + quantum_instance=quantum_instance, ) From 16b2ee3e20c5c5eb0ea5911f638563b790c7c5ac Mon Sep 17 00:00:00 2001 From: ElePT Date: Mon, 10 Oct 2022 19:01:44 +0200 Subject: [PATCH 21/61] Fix mypy --- .../algorithms/classifiers/vqc.py | 12 ++- .../connectors/torch_connector.py | 4 +- .../neural_networks/sampler_qnn.py | 80 +++++++++---------- test/connectors/test_torch_connector.py | 2 +- test/connectors/test_torch_networks.py | 2 +- test/neural_networks/test_sampler_qnn.py | 7 -- 6 files changed, 49 insertions(+), 58 deletions(-) diff --git a/qiskit_machine_learning/algorithms/classifiers/vqc.py b/qiskit_machine_learning/algorithms/classifiers/vqc.py index 5f57f9998..db573ff19 100644 --- a/qiskit_machine_learning/algorithms/classifiers/vqc.py +++ b/qiskit_machine_learning/algorithms/classifiers/vqc.py @@ -102,6 +102,8 @@ def __init__( self._circuit.compose(self.feature_map, inplace=True) self._circuit.compose(self.ansatz, inplace=True) + # needed for mypy + neural_network: SamplerQNN | CircuitQNN = None if sampler is None: # construct circuit QNN neural_network = CircuitQNN( @@ -168,9 +170,13 @@ def _fit_internal(self, X: np.ndarray, y: np.ndarray) -> OptimizerResult: """ X, y = self._validate_input(X, y) num_classes = self._num_classes - cast(CircuitQNN, self._neural_network).set_interpret( - self._get_interpret(num_classes), num_classes - ) + + if isinstance(self._neural_network, SamplerQNN): + self._neural_network.set_interpret(self._get_interpret(num_classes), num_classes) + else: + cast(CircuitQNN, self._neural_network).set_interpret( + self._get_interpret(num_classes), num_classes + ) return super()._minimize(X, y) diff --git a/qiskit_machine_learning/connectors/torch_connector.py b/qiskit_machine_learning/connectors/torch_connector.py index 789fd1677..dd85d1e85 100644 --- a/qiskit_machine_learning/connectors/torch_connector.py +++ b/qiskit_machine_learning/connectors/torch_connector.py @@ -55,7 +55,7 @@ class TorchConnector(Module): class _TorchNNFunction(Function): # pylint: disable=arguments-differ @staticmethod - def forward( # type: ignore + def forward( ctx: Any, input_data: Tensor, weights: Tensor, @@ -117,7 +117,7 @@ def forward( # type: ignore return result_tensor @staticmethod - def backward(ctx: Any, grad_output: Tensor) -> Tuple: # type: ignore + def backward(ctx: Any, grad_output: Tensor) -> Tuple: """Backward pass computation. Args: ctx: context diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index e9149d41b..dbb2f4c63 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -19,9 +19,13 @@ from typing import Callable, cast, Iterable, Sequence import numpy as np -from qiskit.algorithms.gradients import BaseSamplerGradient, ParamShiftSamplerGradient +from qiskit.algorithms.gradients import ( + BaseSamplerGradient, + ParamShiftSamplerGradient, + SamplerGradientResult, +) from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.primitives import BaseSampler +from qiskit.primitives import BaseSampler, SamplerResult from qiskit_machine_learning.exceptions import QiskitMachineLearningError from .neural_network import NeuralNetwork @@ -142,8 +146,8 @@ def _compute_output_shape( ) -> tuple[int, ...]: """Validate and compute the output shape.""" - # # this definition is required by mypy - # output_shape_: tuple[int, ...] = (-1,) + # this definition is required by mypy + output_shape_: tuple[int, ...] = (-1,) if interpret is not None: if output_shape is None: @@ -154,7 +158,7 @@ def _compute_output_shape( output_shape = int(output_shape) output_shape_ = (output_shape,) else: - output_shape_ = output_shape + output_shape_ = cast(tuple[int, ...], output_shape) else: if output_shape is not None: # Warn user that output_shape parameter will be ignored @@ -169,35 +173,35 @@ def _compute_output_shape( def _preprocess( self, - input_data: list[float] | np.ndarray | float | None, - weights: list[float] | np.ndarray | float | None, - ) -> tuple[list[float], int]: + input_data: list[float] | np.ndarray | float, + weights: list[float] | np.ndarray | float, + ) -> tuple[np.ndarray, int]: """ Pre-processing during forward pass of the network. """ + + if not isinstance(input_data, np.ndarray): + input_data = np.asarray(input_data) if len(input_data.shape) == 1: input_data = np.expand_dims(input_data, 0) - num_samples = input_data.shape[0] - # quick fix for 0 inputs - if num_samples == 0: - num_samples = 1 - parameters = [] - for i in range(num_samples): - param_values = [input_data[i, j] for j, input_param in enumerate(self._input_params)] - param_values += [weights[j] for j, weight_param in enumerate(self._weight_params)] - parameters.append(param_values) + if not isinstance(weights, np.ndarray): + weights = np.asarray(weights) + + num_samples = max(input_data.shape[0], 1) + weights = np.broadcast_to(weights, (num_samples, len(weights))) + parameters = np.concatenate((input_data, weights), axis=1) return parameters, num_samples - def _postprocess(self, num_samples, result): + def _postprocess(self, num_samples: int, result: SamplerResult) -> np.ndarray: """ Post-processing during forward pass of the network. """ prob = np.zeros((num_samples, *self._output_shape)) for i in range(num_samples): - counts = result[i] + counts = result.quasi_dists[i] shots = sum(counts.values()) # evaluate probabilities @@ -210,7 +214,11 @@ def _postprocess(self, num_samples, result): return prob - def _forward(self, input_data: np.ndarray | None, weights: np.ndarray | None) -> np.ndarray: + def _forward( + self, + input_data: list[float] | np.ndarray | float, + weights: list[float] | np.ndarray | float, + ) -> np.ndarray: """ Forward pass of the network. """ @@ -218,33 +226,15 @@ def _forward(self, input_data: np.ndarray | None, weights: np.ndarray | None) -> # sampler allows batching job = self.sampler.run([self._circuit] * num_samples, parameter_values) - results = job.result().quasi_dists + results = job.result() result = self._postprocess(num_samples, results) return result - def _preprocess_gradient(self, input_data: np.ndarray, weights: np.ndarray): - """ - Pre-processing during backward pass of the network. - """ - if len(input_data.shape) == 1: - input_data = np.expand_dims(input_data, 0) - - num_samples = input_data.shape[0] - # quick fix for 0 inputs - if num_samples == 0: - num_samples = 1 - - parameters = [] - for i in range(num_samples): - param_values = [input_data[i, j] for j, input_param in enumerate(self._input_params)] - param_values += [weights[j] for j, weight_param in enumerate(self._weight_params)] - parameters.append(param_values) - - return parameters, num_samples - - def _postprocess_gradient(self, num_samples, results): + def _postprocess_gradient( + self, num_samples: int, results: SamplerGradientResult + ) -> tuple[np.ndarray | None, np.ndarray]: """ Post-processing during backward pass of the network. """ @@ -291,12 +281,14 @@ def _postprocess_gradient(self, num_samples, results): return input_grad, weights_grad def _backward( - self, input_data: np.ndarray | None, weights: np.ndarray | None + self, + input_data: list[float] | np.ndarray | float, + weights: list[float] | np.ndarray | float, ) -> tuple[np.ndarray | None, np.ndarray | None]: """Backward pass of the network.""" # prepare parameters in the required format - parameter_values, num_samples = self._preprocess_gradient(input_data, weights) + parameter_values, num_samples = self._preprocess(input_data, weights) if self._input_gradients: job = self.gradient.run([self._circuit] * num_samples, parameter_values) diff --git a/test/connectors/test_torch_connector.py b/test/connectors/test_torch_connector.py index 729b0e7d2..31811aadf 100644 --- a/test/connectors/test_torch_connector.py +++ b/test/connectors/test_torch_connector.py @@ -107,7 +107,7 @@ def _validate_backward_pass(self, model: TorchConnector) -> None: model.neural_network, False, ) - test = torch.autograd.gradcheck(func, input_data, eps=1e-4, atol=1e-3) # type: ignore + test = torch.autograd.gradcheck(func, input_data, eps=1e-4, atol=1e-3) self.assertTrue(test) @data("sv", "qasm") diff --git a/test/connectors/test_torch_networks.py b/test/connectors/test_torch_networks.py index d942f3c1f..98e7163cf 100644 --- a/test/connectors/test_torch_networks.py +++ b/test/connectors/test_torch_networks.py @@ -122,7 +122,7 @@ def test_hybrid_batch_gradients(self, qnn_type: str): from torch.nn import MSELoss from torch.optim import SGD - qnn: Optional[Union[CircuitQNN, TwoLayerQNN]] = None + qnn: Optional[Union[CircuitQNN, TwoLayerQNN, SamplerQNN]] = None if qnn_type == "opflow": qnn = self._create_opflow_qnn() output_size = 1 diff --git a/test/neural_networks/test_sampler_qnn.py b/test/neural_networks/test_sampler_qnn.py index 50fb41a78..f49a9b7a3 100644 --- a/test/neural_networks/test_sampler_qnn.py +++ b/test/neural_networks/test_sampler_qnn.py @@ -123,13 +123,6 @@ def _verify_qnn( ) -> None: """ Verifies that a QNN functions correctly - - Args: - qnn: a QNN to check - batch_size: batch size - - Returns: - None. """ input_data = np.zeros((batch_size, qnn.num_inputs)) weights = np.zeros(qnn.num_weights) From c36cbc2fa52a51553be4f56d1446d4ccedd08dcf Mon Sep 17 00:00:00 2001 From: ElePT <57907331+ElePT@users.noreply.github.com> Date: Mon, 10 Oct 2022 19:16:25 +0200 Subject: [PATCH 22/61] fix mypy --- qiskit_machine_learning/connectors/torch_connector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_machine_learning/connectors/torch_connector.py b/qiskit_machine_learning/connectors/torch_connector.py index dd85d1e85..bc134ecc0 100644 --- a/qiskit_machine_learning/connectors/torch_connector.py +++ b/qiskit_machine_learning/connectors/torch_connector.py @@ -117,7 +117,7 @@ def forward( return result_tensor @staticmethod - def backward(ctx: Any, grad_output: Tensor) -> Tuple: + def backward(ctx: Any, grad_output: Tensor) -> Tuple: # type: ignore """Backward pass computation. Args: ctx: context From cde487727b34cfa74fec289776dd9dda1d7b5914 Mon Sep 17 00:00:00 2001 From: ElePT <57907331+ElePT@users.noreply.github.com> Date: Mon, 10 Oct 2022 19:17:03 +0200 Subject: [PATCH 23/61] fix mypy --- qiskit_machine_learning/connectors/torch_connector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_machine_learning/connectors/torch_connector.py b/qiskit_machine_learning/connectors/torch_connector.py index bc134ecc0..789fd1677 100644 --- a/qiskit_machine_learning/connectors/torch_connector.py +++ b/qiskit_machine_learning/connectors/torch_connector.py @@ -55,7 +55,7 @@ class TorchConnector(Module): class _TorchNNFunction(Function): # pylint: disable=arguments-differ @staticmethod - def forward( + def forward( # type: ignore ctx: Any, input_data: Tensor, weights: Tensor, From 551c7acf37a0a98033e8a4a799aff052f32c5ced Mon Sep 17 00:00:00 2001 From: ElePT <57907331+ElePT@users.noreply.github.com> Date: Mon, 10 Oct 2022 19:17:49 +0200 Subject: [PATCH 24/61] fix mypy --- test/connectors/test_torch_connector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/connectors/test_torch_connector.py b/test/connectors/test_torch_connector.py index 31811aadf..729b0e7d2 100644 --- a/test/connectors/test_torch_connector.py +++ b/test/connectors/test_torch_connector.py @@ -107,7 +107,7 @@ def _validate_backward_pass(self, model: TorchConnector) -> None: model.neural_network, False, ) - test = torch.autograd.gradcheck(func, input_data, eps=1e-4, atol=1e-3) + test = torch.autograd.gradcheck(func, input_data, eps=1e-4, atol=1e-3) # type: ignore self.assertTrue(test) @data("sv", "qasm") From f9d11e68f49512229129126d0f5712fcbf220a7f Mon Sep 17 00:00:00 2001 From: ElePT Date: Mon, 10 Oct 2022 19:30:53 +0200 Subject: [PATCH 25/61] Restore workflow --- .github/workflows/main.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d177615be..333ca15fe 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,16 +16,17 @@ on: push: branches: - main - - primitives - 'stable/**' pull_request: branches: - main - - primitives - 'stable/**' + schedule: + # run every day at 1AM + - cron: '0 1 * * *' concurrency: - group: ${{ github.repository }}-${{ github.ref }}-${{ github.head_ref }}-${{ github.workflow }} + group: ${{ github.repository }}-${{ github.ref }}-${{ github.head_ref }} cancel-in-progress: true jobs: From 3e4613758015a6078befd1f64d87531df6e8fba1 Mon Sep 17 00:00:00 2001 From: ElePT <57907331+ElePT@users.noreply.github.com> Date: Mon, 10 Oct 2022 19:31:34 +0200 Subject: [PATCH 26/61] Restore workflow --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 333ca15fe..cf01d7050 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,7 +22,7 @@ on: - main - 'stable/**' schedule: - # run every day at 1AM + # run every day at 1AM - cron: '0 1 * * *' concurrency: From af88a141bdde05491d0ccba16c86ecfc4e1ad552 Mon Sep 17 00:00:00 2001 From: ElePT <57907331+ElePT@users.noreply.github.com> Date: Mon, 10 Oct 2022 19:32:18 +0200 Subject: [PATCH 27/61] Update .github/workflows/main.yml --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cf01d7050..5f3f93eeb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,7 +23,7 @@ on: - 'stable/**' schedule: # run every day at 1AM - - cron: '0 1 * * *' + - cron: '0 1 * * *' concurrency: group: ${{ github.repository }}-${{ github.ref }}-${{ github.head_ref }} From 328485183b4143c80fd6bba232f4db76c2fd18a3 Mon Sep 17 00:00:00 2001 From: ElePT Date: Tue, 11 Oct 2022 09:02:07 +0200 Subject: [PATCH 28/61] Fix mypy --- qiskit_machine_learning/neural_networks/sampler_qnn.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index dbb2f4c63..c7353e13b 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -158,7 +158,7 @@ def _compute_output_shape( output_shape = int(output_shape) output_shape_ = (output_shape,) else: - output_shape_ = cast(tuple[int, ...], output_shape) + output_shape_ = output_shape # type: ignore else: if output_shape is not None: # Warn user that output_shape parameter will be ignored @@ -188,7 +188,7 @@ def _preprocess( if not isinstance(weights, np.ndarray): weights = np.asarray(weights) - num_samples = max(input_data.shape[0], 1) + num_samples = max(input_data.shape[0], 1) # type: ignore weights = np.broadcast_to(weights, (num_samples, len(weights))) parameters = np.concatenate((input_data, weights), axis=1) From 13b8c8995e54bc9e3839bba04af787d1a700a02d Mon Sep 17 00:00:00 2001 From: ElePT Date: Tue, 11 Oct 2022 10:12:51 +0200 Subject: [PATCH 29/61] Fix CI (hopefully) --- qiskit_machine_learning/neural_networks/sampler_qnn.py | 4 ++-- test/neural_networks/test_sampler_qnn.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index c7353e13b..f17214b71 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -158,7 +158,7 @@ def _compute_output_shape( output_shape = int(output_shape) output_shape_ = (output_shape,) else: - output_shape_ = output_shape # type: ignore + output_shape_ = output_shape # type: ignore else: if output_shape is not None: # Warn user that output_shape parameter will be ignored @@ -188,7 +188,7 @@ def _preprocess( if not isinstance(weights, np.ndarray): weights = np.asarray(weights) - num_samples = max(input_data.shape[0], 1) # type: ignore + num_samples = max(input_data.shape[0], 1) # type: ignore weights = np.broadcast_to(weights, (num_samples, len(weights))) parameters = np.concatenate((input_data, weights), axis=1) diff --git a/test/neural_networks/test_sampler_qnn.py b/test/neural_networks/test_sampler_qnn.py index f49a9b7a3..917a9f231 100644 --- a/test/neural_networks/test_sampler_qnn.py +++ b/test/neural_networks/test_sampler_qnn.py @@ -209,7 +209,7 @@ def test_circuit_vs_sampler_qnn(self): aer = importlib.import_module("qiskit.providers.aer") - parity = lambda x: "{:b}".format(x).count("1") % 2 + parity = lambda x: f"{x:b}".count("1") % 2 output_shape = 2 # this is required in case of a callable with dense output qi_qasm = QuantumInstance( From 9b350ba39c3931b8ea3564c8642abd142b4ffb3e Mon Sep 17 00:00:00 2001 From: ElePT Date: Tue, 11 Oct 2022 10:21:01 +0200 Subject: [PATCH 30/61] Update requirements --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 53eca70fe..32e96ecd5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -qiskit-terra>=0.20.0 +qiskit-terra>=0.22.* scipy>=1.4 numpy>=1.17 psutil>=5 From 0252925c8a3e3b327f1fc5883197fe274f41deaf Mon Sep 17 00:00:00 2001 From: ElePT Date: Wed, 12 Oct 2022 10:28:46 +0200 Subject: [PATCH 31/61] Add sparse support --- .../neural_networks/sampler_qnn.py | 79 ++++++++++++++----- test/neural_networks/test_sampler_qnn.py | 45 ++++++++--- 2 files changed, 95 insertions(+), 29 deletions(-) diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index f17214b71..2acf74e37 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -27,9 +27,23 @@ from qiskit.circuit import Parameter, QuantumCircuit from qiskit.primitives import BaseSampler, SamplerResult from qiskit_machine_learning.exceptions import QiskitMachineLearningError +import qiskit_machine_learning.optionals as _optionals from .neural_network import NeuralNetwork +if _optionals.HAS_SPARSE: + # pylint: disable=import-error + from sparse import SparseArray +else: + + class SparseArray: # type: ignore + """Empty SparseArray class + Replacement if sparse.SparseArray is not present. + """ + + pass + + logger = logging.getLogger(__name__) @@ -43,6 +57,7 @@ def __init__( *, input_params: Sequence[Parameter] | None = None, weight_params: Sequence[Parameter] | None = None, + sparse: bool = False, interpret: Callable[[int], int | tuple[int, ...]] | None = None, output_shape: int | tuple[int, ...] | None = None, gradient: BaseSamplerGradient | None = None, @@ -54,6 +69,7 @@ def __init__( circuit: The parametrized quantum circuit that generates the samples of this network. input_params: The parameters of the circuit corresponding to the input. weight_params: The parameters of the circuit corresponding to the trainable weights. + sparse: Returns whether the output is sparse or not. interpret: A callable that maps the measured integer to another unsigned integer or tuple of unsigned integers. These are used as new indices for the (potentially sparse) output array. If no interpret function is @@ -80,21 +96,19 @@ def __init__( self._input_params = list(input_params or []) self._weight_params = list(weight_params or []) - # the final output shape will depend on the - # interpret method, and it must be set before - # applying the default to the latter + if sparse: + _optionals.HAS_SPARSE.require_now("DOK") + self.set_interpret(interpret, output_shape) self._input_gradients = input_gradients - # TODO: will primitives ever support sparse? # TODO: look into custom transpilation - # TODO: sampling?? super().__init__( len(self._input_params), len(self._weight_params), - False, # sparse + sparse, self._output_shape, self._input_gradients, ) @@ -158,7 +172,7 @@ def _compute_output_shape( output_shape = int(output_shape) output_shape_ = (output_shape,) else: - output_shape_ = output_shape # type: ignore + output_shape_ = output_shape else: if output_shape is not None: # Warn user that output_shape parameter will be ignored @@ -188,7 +202,7 @@ def _preprocess( if not isinstance(weights, np.ndarray): weights = np.asarray(weights) - num_samples = max(input_data.shape[0], 1) # type: ignore + num_samples = max(input_data.shape[0], 1) weights = np.broadcast_to(weights, (num_samples, len(weights))) parameters = np.concatenate((input_data, weights), axis=1) @@ -198,7 +212,13 @@ def _postprocess(self, num_samples: int, result: SamplerResult) -> np.ndarray: """ Post-processing during forward pass of the network. """ - prob = np.zeros((num_samples, *self._output_shape)) + if self._sparse: + # pylint: disable=import-error + from sparse import DOK + + prob = DOK((num_samples, *self._output_shape)) + else: + prob = np.zeros((num_samples, *self._output_shape)) for i in range(num_samples): counts = result.quasi_dists[i] @@ -212,13 +232,16 @@ def _postprocess(self, num_samples: int, result: SamplerResult) -> np.ndarray: key = (i, *key) # type: ignore prob[key] += v / shots - return prob + if self._sparse: + return prob.to_coo() + else: + return prob def _forward( self, input_data: list[float] | np.ndarray | float, weights: list[float] | np.ndarray | float, - ) -> np.ndarray: + ) -> np.ndarray | SparseArray: """ Forward pass of the network. """ @@ -234,16 +257,27 @@ def _forward( def _postprocess_gradient( self, num_samples: int, results: SamplerGradientResult - ) -> tuple[np.ndarray | None, np.ndarray]: + ) -> tuple[np.ndarray | SparseArray | None, np.ndarray | SparseArray]: """ Post-processing during backward pass of the network. """ - input_grad = ( - np.zeros((num_samples, *self._output_shape, self._num_inputs)) - if self._input_gradients - else None - ) - weights_grad = np.zeros((num_samples, *self._output_shape, self._num_weights)) + if self._sparse: + # pylint: disable=import-error + from sparse import DOK + + input_grad = ( + DOK((num_samples, *self._output_shape, self._num_inputs)) + if self._input_gradients + else None + ) + weights_grad = DOK((num_samples, *self._output_shape, self._num_weights)) + else: + input_grad = ( + np.zeros((num_samples, *self._output_shape, self._num_inputs)) + if self._input_gradients + else None + ) + weights_grad = np.zeros((num_samples, *self._output_shape, self._num_weights)) if self._input_gradients: num_grad_vars = self._num_inputs + self._num_weights @@ -253,6 +287,7 @@ def _postprocess_gradient( for sample in range(num_samples): for i in range(num_grad_vars): grad = results.gradients[sample][i] + for k in grad.keys(): val = results.gradients[sample][i][k] # get index for input or weights gradients @@ -268,6 +303,7 @@ def _postprocess_gradient( # if key is an array-type, cast to hashable tuple key = tuple(cast(Iterable[int], key)) key = (sample, *key, grad_index) + # store value for inputs or weights gradients if self._input_gradients: # we compute input gradients first @@ -278,13 +314,18 @@ def _postprocess_gradient( else: weights_grad[key] += np.real(val) + if self._sparse: + if self._input_gradients: + input_grad = input_grad.to_coo() + weights_grad = weights_grad.to_coo() + return input_grad, weights_grad def _backward( self, input_data: list[float] | np.ndarray | float, weights: list[float] | np.ndarray | float, - ) -> tuple[np.ndarray | None, np.ndarray | None]: + ) -> tuple[np.ndarray | SparseArray | None, np.ndarray | SparseArray]: """Backward pass of the network.""" # prepare parameters in the required format diff --git a/test/neural_networks/test_sampler_qnn.py b/test/neural_networks/test_sampler_qnn.py index 917a9f231..b35e37a74 100644 --- a/test/neural_networks/test_sampler_qnn.py +++ b/test/neural_networks/test_sampler_qnn.py @@ -15,6 +15,7 @@ from test import QiskitMachineLearningTestCase import itertools +import unittest import numpy as np from ddt import ddt, idata, unpack @@ -27,12 +28,13 @@ from qiskit_machine_learning.neural_networks import CircuitQNN from qiskit_machine_learning.neural_networks.sampler_qnn import SamplerQNN +import qiskit_machine_learning.optionals as _optionals algorithm_globals.random_seed = 42 DEFAULT = "default" SHOTS = "shots" -SAMPLING = [True, False] # TODO +SPARSE = [True, False] SAMPLERS = [DEFAULT, SHOTS] INTERPRET_TYPES = [2] BATCH_SIZES = [1, 2] @@ -84,7 +86,7 @@ def interpret_2d(x): self.sampler = Sampler() self.sampler_shots = Sampler(options={"shots": 100}) - def _get_qnn(self, sampler_type, interpret_id): + def _get_qnn(self, sparse, sampler_type, interpret_id): """Construct QNN from configuration.""" # get quantum instance @@ -111,6 +113,7 @@ def _get_qnn(self, sampler_type, interpret_id): self.qc, input_params=self.input_params, weight_params=self.weight_params, + sparse=sparse, interpret=interpret, output_shape=output_shape, ) @@ -124,22 +127,36 @@ def _verify_qnn( """ Verifies that a QNN functions correctly """ + # pylint: disable=import-error + from sparse import SparseArray + input_data = np.zeros((batch_size, qnn.num_inputs)) weights = np.zeros(qnn.num_weights) # evaluate QNN forward pass result = qnn.forward(input_data, weights) - self.assertTrue(isinstance(result, np.ndarray)) + + # make sure forward result is sparse if it should be + if qnn.sparse: + self.assertTrue(isinstance(result, SparseArray)) + else: + self.assertTrue(isinstance(result, np.ndarray)) + # check forward result shape self.assertEqual(result.shape, (batch_size, *qnn.output_shape)) # evaluate QNN backward pass input_grad, weights_grad = qnn.backward(input_data, weights) - self.assertIsNone(input_grad) # verify that input gradients are None if turned off + self.assertIsNone(input_grad) self.assertEqual(weights_grad.shape, (batch_size, *qnn.output_shape, qnn.num_weights)) + if qnn.sparse: + self.assertTrue(isinstance(weights_grad, SparseArray)) + else: + self.assertTrue(isinstance(weights_grad, np.ndarray)) + # verify that input gradients are not None if turned on qnn.input_gradients = True input_grad, weights_grad = qnn.backward(input_data, weights) @@ -147,22 +164,30 @@ def _verify_qnn( self.assertEqual(input_grad.shape, (batch_size, *qnn.output_shape, qnn.num_inputs)) self.assertEqual(weights_grad.shape, (batch_size, *qnn.output_shape, qnn.num_weights)) - @idata(itertools.product(SAMPLERS, INTERPRET_TYPES, BATCH_SIZES)) + if qnn.sparse: + self.assertTrue(isinstance(weights_grad, SparseArray)) + self.assertTrue(isinstance(input_grad, SparseArray)) + else: + self.assertTrue(isinstance(weights_grad, np.ndarray)) + self.assertTrue(isinstance(input_grad, np.ndarray)) + + @unittest.skipIf(not _optionals.HAS_SPARSE, "Sparse not available.") + @idata(itertools.product(SPARSE, SAMPLERS, INTERPRET_TYPES, BATCH_SIZES)) @unpack - def test_sampler_qnn(self, sampler_type, interpret_type, batch_size): + def test_sampler_qnn(self, sparse: bool, sampler_type, interpret_type, batch_size): """Sampler QNN Test.""" - qnn = self._get_qnn(sampler_type, interpret_type) + qnn = self._get_qnn(sparse, sampler_type, interpret_type) self._verify_qnn(qnn, batch_size) - @idata(itertools.product(INTERPRET_TYPES, BATCH_SIZES)) + @idata(itertools.product(SPARSE, INTERPRET_TYPES, BATCH_SIZES)) def test_sampler_qnn_gradient(self, config): """Sampler QNN Gradient Test.""" # get configuration - interpret_id, batch_size = config + sparse, interpret_id, batch_size = config # get QNN - qnn = self._get_qnn(DEFAULT, interpret_id) + qnn = self._get_qnn(sparse, DEFAULT, interpret_id) # set input gradients to True qnn.input_gradients = True From d5e18e506589ef7ae69420ef46f11ffc54841dd8 Mon Sep 17 00:00:00 2001 From: ElePT Date: Wed, 12 Oct 2022 10:53:12 +0200 Subject: [PATCH 32/61] Fix style etc --- .../neural_networks/sampler_qnn.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index 2acf74e37..b2353f1ea 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -172,7 +172,7 @@ def _compute_output_shape( output_shape = int(output_shape) output_shape_ = (output_shape,) else: - output_shape_ = output_shape + output_shape_ = cast(tuple[int, ...], output_shape) else: if output_shape is not None: # Warn user that output_shape parameter will be ignored @@ -194,14 +194,11 @@ def _preprocess( Pre-processing during forward pass of the network. """ - if not isinstance(input_data, np.ndarray): - input_data = np.asarray(input_data) + input_data = np.asarray(input_data) + weights = np.asarray(weights) if len(input_data.shape) == 1: input_data = np.expand_dims(input_data, 0) - if not isinstance(weights, np.ndarray): - weights = np.asarray(weights) - num_samples = max(input_data.shape[0], 1) weights = np.broadcast_to(weights, (num_samples, len(weights))) parameters = np.concatenate((input_data, weights), axis=1) @@ -308,15 +305,15 @@ def _postprocess_gradient( if self._input_gradients: # we compute input gradients first if i < self._num_inputs: - input_grad[key] += np.real(val) + input_grad[key] += val else: - weights_grad[key] += np.real(val) + weights_grad[key] += val else: - weights_grad[key] += np.real(val) + weights_grad[key] += val if self._sparse: if self._input_gradients: - input_grad = input_grad.to_coo() + input_grad = input_grad.to_coo() # pylint: disable=no-member weights_grad = weights_grad.to_coo() return input_grad, weights_grad From e40c893701c10c16bcc8e2089bfe0e1f933c3a97 Mon Sep 17 00:00:00 2001 From: ElePT Date: Wed, 12 Oct 2022 10:58:34 +0200 Subject: [PATCH 33/61] Add type ignore --- qiskit_machine_learning/neural_networks/sampler_qnn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index b2353f1ea..c963dfda7 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -199,7 +199,7 @@ def _preprocess( if len(input_data.shape) == 1: input_data = np.expand_dims(input_data, 0) - num_samples = max(input_data.shape[0], 1) + num_samples = max(input_data.shape[0], 1) # type: ignore weights = np.broadcast_to(weights, (num_samples, len(weights))) parameters = np.concatenate((input_data, weights), axis=1) From 16441a6a9447006d43274c41ed9e82ab6281c181 Mon Sep 17 00:00:00 2001 From: ElePT Date: Wed, 12 Oct 2022 14:56:37 +0200 Subject: [PATCH 34/61] Fix style? --- qiskit_machine_learning/neural_networks/sampler_qnn.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index c963dfda7..9c9c1992b 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -172,7 +172,7 @@ def _compute_output_shape( output_shape = int(output_shape) output_shape_ = (output_shape,) else: - output_shape_ = cast(tuple[int, ...], output_shape) + output_shape_ = output_shape # type: ignore else: if output_shape is not None: # Warn user that output_shape parameter will be ignored @@ -199,7 +199,7 @@ def _preprocess( if len(input_data.shape) == 1: input_data = np.expand_dims(input_data, 0) - num_samples = max(input_data.shape[0], 1) # type: ignore + num_samples = max(input_data.shape[0], 1) weights = np.broadcast_to(weights, (num_samples, len(weights))) parameters = np.concatenate((input_data, weights), axis=1) From ca9e1c534e8a211645c656894d33ae2d60fbb01e Mon Sep 17 00:00:00 2001 From: ElePT Date: Wed, 12 Oct 2022 14:58:02 +0200 Subject: [PATCH 35/61] skip test if not sparse --- test/neural_networks/test_sampler_qnn.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/neural_networks/test_sampler_qnn.py b/test/neural_networks/test_sampler_qnn.py index b35e37a74..c4b0428d0 100644 --- a/test/neural_networks/test_sampler_qnn.py +++ b/test/neural_networks/test_sampler_qnn.py @@ -179,6 +179,7 @@ def test_sampler_qnn(self, sparse: bool, sampler_type, interpret_type, batch_siz qnn = self._get_qnn(sparse, sampler_type, interpret_type) self._verify_qnn(qnn, batch_size) + @unittest.skipIf(not _optionals.HAS_SPARSE, "Sparse not available.") @idata(itertools.product(SPARSE, INTERPRET_TYPES, BATCH_SIZES)) def test_sampler_qnn_gradient(self, config): """Sampler QNN Gradient Test.""" From 8fc31d0cd093d51de3d9ddec778bb43d5a418bfd Mon Sep 17 00:00:00 2001 From: ElePT Date: Wed, 12 Oct 2022 15:09:05 +0200 Subject: [PATCH 36/61] Add type ignore --- qiskit_machine_learning/neural_networks/sampler_qnn.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index 9c9c1992b..51e27d836 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -180,7 +180,6 @@ def _compute_output_shape( "No interpret function given, output_shape will be automatically " "determined as 2^num_qubits." ) - output_shape_ = (2**self._circuit.num_qubits,) return output_shape_ @@ -200,7 +199,7 @@ def _preprocess( input_data = np.expand_dims(input_data, 0) num_samples = max(input_data.shape[0], 1) - weights = np.broadcast_to(weights, (num_samples, len(weights))) + weights = np.broadcast_to(weights, (num_samples, len(weights))) # type: ignore parameters = np.concatenate((input_data, weights), axis=1) return parameters, num_samples From 82c96137d14004278f3d6e6c128f5dc2ea9b45a3 Mon Sep 17 00:00:00 2001 From: Manoel Marques Date: Wed, 12 Oct 2022 10:14:26 -0400 Subject: [PATCH 37/61] Fix mypy --- .../neural_networks/sampler_qnn.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index 51e27d836..9ee51fd2d 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -193,14 +193,14 @@ def _preprocess( Pre-processing during forward pass of the network. """ - input_data = np.asarray(input_data) - weights = np.asarray(weights) - if len(input_data.shape) == 1: - input_data = np.expand_dims(input_data, 0) - - num_samples = max(input_data.shape[0], 1) - weights = np.broadcast_to(weights, (num_samples, len(weights))) # type: ignore - parameters = np.concatenate((input_data, weights), axis=1) + np_input_data: np.ndarray = np.asarray(input_data) + np_weights: np.ndarray = np.asarray(weights) + if len(np_input_data.shape) == 1: + np_input_data = np.expand_dims(np_input_data, 0) + + num_samples = max(np_input_data.shape[0], 1) + np_weights = np.broadcast_to(np_weights, (num_samples, len(np_weights))) + parameters = np.concatenate((np_input_data, np_weights), axis=1) return parameters, num_samples From b15d380efef7044197281c7c849f29123206cf1e Mon Sep 17 00:00:00 2001 From: ElePT Date: Thu, 13 Oct 2022 10:41:11 +0200 Subject: [PATCH 38/61] Make keyword args --- qiskit_machine_learning/algorithms/classifiers/vqc.py | 1 + qiskit_machine_learning/neural_networks/sampler_qnn.py | 2 +- test/neural_networks/test_sampler_qnn.py | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/qiskit_machine_learning/algorithms/classifiers/vqc.py b/qiskit_machine_learning/algorithms/classifiers/vqc.py index db573ff19..4295683d0 100644 --- a/qiskit_machine_learning/algorithms/classifiers/vqc.py +++ b/qiskit_machine_learning/algorithms/classifiers/vqc.py @@ -48,6 +48,7 @@ class VQC(NeuralNetworkClassifier): def __init__( self, + *, sampler: BaseSampler = None, num_qubits: int | None = None, feature_map: QuantumCircuit | None = None, diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index 51e27d836..c091b29ed 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -52,9 +52,9 @@ class SamplerQNN(NeuralNetwork): def __init__( self, + *, sampler: BaseSampler, circuit: QuantumCircuit, - *, input_params: Sequence[Parameter] | None = None, weight_params: Sequence[Parameter] | None = None, sparse: bool = False, diff --git a/test/neural_networks/test_sampler_qnn.py b/test/neural_networks/test_sampler_qnn.py index c4b0428d0..e4f2a7a14 100644 --- a/test/neural_networks/test_sampler_qnn.py +++ b/test/neural_networks/test_sampler_qnn.py @@ -109,8 +109,8 @@ def _get_qnn(self, sparse, sampler_type, interpret_id): # construct QNN qnn = SamplerQNN( - sampler, - self.qc, + sampler=sampler, + circuit=self.qc, input_params=self.input_params, weight_params=self.weight_params, sparse=sparse, From 108255f3f23f722207ebb3e6452ef74f30df6360 Mon Sep 17 00:00:00 2001 From: ElePT Date: Thu, 13 Oct 2022 10:53:23 +0200 Subject: [PATCH 39/61] Make keyword args --- qiskit_machine_learning/algorithms/classifiers/vqc.py | 4 ++-- test/connectors/test_torch_networks.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit_machine_learning/algorithms/classifiers/vqc.py b/qiskit_machine_learning/algorithms/classifiers/vqc.py index 4295683d0..2522583a4 100644 --- a/qiskit_machine_learning/algorithms/classifiers/vqc.py +++ b/qiskit_machine_learning/algorithms/classifiers/vqc.py @@ -119,8 +119,8 @@ def __init__( else: # construct sampler QNN neural_network = SamplerQNN( - sampler, - self._circuit, + sampler=sampler, + circuit=self._circuit, input_params=self.feature_map.parameters, weight_params=self.ansatz.parameters, interpret=self._get_interpret(2), diff --git a/test/connectors/test_torch_networks.py b/test/connectors/test_torch_networks.py index 98e7163cf..f690051a2 100644 --- a/test/connectors/test_torch_networks.py +++ b/test/connectors/test_torch_networks.py @@ -105,8 +105,8 @@ def _create_sampler_qnn(self) -> SamplerQNN: qc.append(ansatz, range(num_inputs)) qnn = SamplerQNN( - Sampler(), - qc, + sampler=Sampler(), + circuit=qc, input_params=feature_map.parameters, weight_params=ansatz.parameters, input_gradients=True, # for hybrid qnn From a9f0179ca37f22829d86c2d763c0a5769721e706 Mon Sep 17 00:00:00 2001 From: ElePT Date: Fri, 14 Oct 2022 18:54:14 +0200 Subject: [PATCH 40/61] Apply review comments --- .../algorithms/classifiers/vqc.py | 25 ++- .../neural_networks/sampler_qnn.py | 157 +++++++------ test/neural_networks/test_sampler_qnn.py | 209 +++++++++--------- 3 files changed, 209 insertions(+), 182 deletions(-) diff --git a/qiskit_machine_learning/algorithms/classifiers/vqc.py b/qiskit_machine_learning/algorithms/classifiers/vqc.py index 2522583a4..0375a6cb4 100644 --- a/qiskit_machine_learning/algorithms/classifiers/vqc.py +++ b/qiskit_machine_learning/algorithms/classifiers/vqc.py @@ -48,8 +48,6 @@ class VQC(NeuralNetworkClassifier): def __init__( self, - *, - sampler: BaseSampler = None, num_qubits: int | None = None, feature_map: QuantumCircuit | None = None, ansatz: QuantumCircuit | None = None, @@ -59,6 +57,8 @@ def __init__( quantum_instance: QuantumInstance | Backend | None = None, initial_point: np.ndarray | None = None, callback: Callable[[np.ndarray, float], None] | None = None, + *, + sampler: BaseSampler = None, ) -> None: """ Args: @@ -85,6 +85,7 @@ def __init__( On each iteration an optimizer invokes the callback and passes current weights as an array and a computed value as a float of the objective function being optimized. This allows to track how well optimization / training process is going on. + sampler: The sampler primitive used to compute neural network's results. Raises: QiskitMachineLearningError: Needs at least one out of ``num_qubits``, ``feature_map`` or ``ansatz`` to be given. Or the number of qubits in the feature map and/or ansatz @@ -105,26 +106,26 @@ def __init__( # needed for mypy neural_network: SamplerQNN | CircuitQNN = None - if sampler is None: - # construct circuit QNN - neural_network = CircuitQNN( - self._circuit, + if quantum_instance is not None: + # construct sampler QNN + neural_network = SamplerQNN( + sampler=sampler, + circuit=self._circuit, input_params=self.feature_map.parameters, weight_params=self.ansatz.parameters, interpret=self._get_interpret(2), output_shape=2, - quantum_instance=quantum_instance, input_gradients=False, ) else: - # construct sampler QNN - neural_network = SamplerQNN( - sampler=sampler, - circuit=self._circuit, + # construct circuit QNN + neural_network = CircuitQNN( + self._circuit, input_params=self.feature_map.parameters, weight_params=self.ansatz.parameters, interpret=self._get_interpret(2), output_shape=2, + quantum_instance=quantum_instance, input_gradients=False, ) @@ -174,7 +175,7 @@ def _fit_internal(self, X: np.ndarray, y: np.ndarray) -> OptimizerResult: if isinstance(self._neural_network, SamplerQNN): self._neural_network.set_interpret(self._get_interpret(num_classes), num_classes) - else: + elif isinstance(self._neural_network, CircuitQNN): cast(CircuitQNN, self._neural_network).set_interpret( self._get_interpret(num_classes), num_classes ) diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index 68c1b1856..44b4db37c 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2022. +# (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 @@ -25,7 +25,7 @@ SamplerGradientResult, ) from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.primitives import BaseSampler, SamplerResult +from qiskit.primitives import BaseSampler, SamplerResult, Sampler from qiskit_machine_learning.exceptions import QiskitMachineLearningError import qiskit_machine_learning.optionals as _optionals @@ -53,8 +53,8 @@ class SamplerQNN(NeuralNetwork): def __init__( self, *, - sampler: BaseSampler, - circuit: QuantumCircuit, + sampler: BaseSampler | None = None, + circuit: QuantumCircuit | None = None, input_params: Sequence[Parameter] | None = None, weight_params: Sequence[Parameter] | None = None, sparse: bool = False, @@ -82,19 +82,27 @@ def __init__( Raises: QiskitMachineLearningError: Invalid parameter values. """ - # set primitive + # set primitive, provide default + if sampler is None: + sampler = Sampler() self.sampler = sampler # set gradient - # TODO: provide default gradient? - self.gradient = gradient or ParamShiftSamplerGradient(self.sampler) + if gradient is None: + gradient = ParamShiftSamplerGradient(self.sampler) + self.gradient = gradient self._circuit = circuit.copy() if len(self._circuit.clbits) == 0: self._circuit.measure_all() - self._input_params = list(input_params or []) - self._weight_params = list(weight_params or []) + if input_params is None: + input_params = [] + self._input_params = list(input_params) + + if weight_params is None: + weight_params = [] + self._weight_params = list(weight_params) if sparse: _optionals.HAS_SPARSE.require_now("DOK") @@ -106,11 +114,11 @@ def __init__( # TODO: look into custom transpilation super().__init__( - len(self._input_params), - len(self._weight_params), - sparse, - self._output_shape, - self._input_gradients, + num_inputs=len(self._input_params), + num_weights=len(self._weight_params), + sparse=sparse, + output_shape=self._output_shape, + input_gradients=self._input_gradients, ) @property @@ -119,12 +127,12 @@ def circuit(self) -> QuantumCircuit: return self._circuit @property - def input_params(self) -> Sequence: + def input_params(self) -> Sequence[Parameter]: """Returns the list of input parameters.""" return self._input_params @property - def weight_params(self) -> Sequence: + def weight_params(self) -> Sequence[Parameter]: """Returns the list of trainable weights parameters.""" return self._weight_params @@ -186,28 +194,39 @@ def _compute_output_shape( def _preprocess( self, - input_data: list[float] | np.ndarray | float, - weights: list[float] | np.ndarray | float, - ) -> tuple[np.ndarray, int]: + input_data: np.ndarray | None, + weights: np.ndarray | None, + ) -> tuple[np.ndarray | None, int | None]: """ Pre-processing during forward pass of the network. """ - np_input_data: np.ndarray = np.asarray(input_data) - np_weights: np.ndarray = np.asarray(weights) - if len(np_input_data.shape) == 1: - np_input_data = np.expand_dims(np_input_data, 0) - - num_samples = max(np_input_data.shape[0], 1) - np_weights = np.broadcast_to(np_weights, (num_samples, len(np_weights))) - parameters = np.concatenate((np_input_data, np_weights), axis=1) + if input_data is not None: + num_samples = input_data.shape[0] + if weights is not None: + weights = np.broadcast_to(weights, (num_samples, len(weights))) + parameters = np.concatenate((input_data, weights), axis=1) + else: + parameters = input_data + else: + if weights is not None: + num_samples = 1 + parameters = np.broadcast_to(weights, (num_samples, len(weights))) + else: + return None, None return parameters, num_samples - def _postprocess(self, num_samples: int, result: SamplerResult) -> np.ndarray: + def _postprocess( + self, num_samples: int | None, result: SamplerResult | None + ) -> np.ndarray | SparseArray | None: """ Post-processing during forward pass of the network. """ + + if result is None: + return None + if self._sparse: # pylint: disable=import-error from sparse import DOK @@ -233,30 +252,15 @@ def _postprocess(self, num_samples: int, result: SamplerResult) -> np.ndarray: else: return prob - def _forward( - self, - input_data: list[float] | np.ndarray | float, - weights: list[float] | np.ndarray | float, - ) -> np.ndarray | SparseArray: - """ - Forward pass of the network. - """ - parameter_values, num_samples = self._preprocess(input_data, weights) - - # sampler allows batching - job = self.sampler.run([self._circuit] * num_samples, parameter_values) - results = job.result() - - result = self._postprocess(num_samples, results) - - return result - def _postprocess_gradient( - self, num_samples: int, results: SamplerGradientResult - ) -> tuple[np.ndarray | SparseArray | None, np.ndarray | SparseArray]: + self, num_samples: int | None, results: SamplerGradientResult | None + ) -> tuple[np.ndarray | SparseArray | None, np.ndarray | SparseArray | None]: """ Post-processing during backward pass of the network. """ + if num_samples is None or results is None: + return None, None + if self._sparse: # pylint: disable=import-error from sparse import DOK @@ -268,6 +272,7 @@ def _postprocess_gradient( ) weights_grad = DOK((num_samples, *self._output_shape, self._num_weights)) else: + input_grad = ( np.zeros((num_samples, *self._output_shape, self._num_inputs)) if self._input_gradients @@ -283,14 +288,13 @@ def _postprocess_gradient( for sample in range(num_samples): for i in range(num_grad_vars): grad = results.gradients[sample][i] - - for k in grad.keys(): - val = results.gradients[sample][i][k] + for k, val in grad.items(): # get index for input or weights gradients if self._input_gradients: grad_index = i if i < self._num_inputs else i - self._num_inputs else: grad_index = i + # interpret integer and construct key key = self._interpret(k) if isinstance(key, Integral): @@ -317,26 +321,53 @@ def _postprocess_gradient( return input_grad, weights_grad + def _forward( + self, + input_data: np.ndarray | None, + weights: np.ndarray | None, + ) -> np.ndarray | SparseArray | None: + """ + Forward pass of the network. + """ + parameter_values, num_samples = self._preprocess(input_data, weights) + + if num_samples is not None and np.prod(parameter_values.shape) > 0: + # sampler allows batching + job = self.sampler.run([self._circuit] * num_samples, parameter_values) + results = job.result() + else: + results = None + + result = self._postprocess(num_samples, results) + + return result + def _backward( self, - input_data: list[float] | np.ndarray | float, - weights: list[float] | np.ndarray | float, - ) -> tuple[np.ndarray | SparseArray | None, np.ndarray | SparseArray]: + input_data: np.ndarray | None, + weights: np.ndarray | None, + ) -> tuple[np.ndarray | SparseArray | None, np.ndarray | SparseArray | None]: """Backward pass of the network.""" # prepare parameters in the required format parameter_values, num_samples = self._preprocess(input_data, weights) - if self._input_gradients: - job = self.gradient.run([self._circuit] * num_samples, parameter_values) + if num_samples is not None and np.prod(parameter_values.shape) > 0: + if self._input_gradients: + job = self.gradient.run([self._circuit] * num_samples, parameter_values) + results = job.result() + else: + if len(parameter_values[0]) is not self._num_inputs: + job = self.gradient.run( + [self._circuit] * num_samples, + parameter_values, + parameters=[self._circuit.parameters[self._num_inputs :]] * num_samples, + ) + results = job.result() + else: + results = None else: - job = self.gradient.run( - [self._circuit] * num_samples, - parameter_values, - parameters=[self._circuit.parameters[self._num_inputs :]] * num_samples, - ) - - results = job.result() + results = None input_grad, weights_grad = self._postprocess_gradient(num_samples, results) diff --git a/test/neural_networks/test_sampler_qnn.py b/test/neural_networks/test_sampler_qnn.py index e4f2a7a14..b723a1843 100644 --- a/test/neural_networks/test_sampler_qnn.py +++ b/test/neural_networks/test_sampler_qnn.py @@ -18,26 +18,36 @@ import unittest import numpy as np -from ddt import ddt, idata, unpack +from ddt import ddt, idata from qiskit.circuit import QuantumCircuit from qiskit.primitives import Sampler -from qiskit.algorithms.gradients import ParamShiftSamplerGradient from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap -from qiskit.utils import QuantumInstance, algorithm_globals +from qiskit.utils import algorithm_globals -from qiskit_machine_learning.neural_networks import CircuitQNN from qiskit_machine_learning.neural_networks.sampler_qnn import SamplerQNN import qiskit_machine_learning.optionals as _optionals -algorithm_globals.random_seed = 42 +if _optionals.HAS_SPARSE: + # pylint: disable=import-error + from sparse import SparseArray +else: + + class SparseArray: # type: ignore + """Empty SparseArray class + Replacement if sparse.SparseArray is not present. + """ + + pass + DEFAULT = "default" SHOTS = "shots" SPARSE = [True, False] SAMPLERS = [DEFAULT, SHOTS] -INTERPRET_TYPES = [2] -BATCH_SIZES = [1, 2] +INTERPRET_TYPES = [0, 1, 2] +BATCH_SIZES = [2] +INPUT_GRADS = [True, False] @ddt @@ -48,10 +58,6 @@ def setUp(self): super().setUp() algorithm_globals.random_seed = 12345 - # # define test circuit - # num_qubits = 3 - # self.qc = RealAmplitudes(num_qubits, entanglement="linear", reps=1) - # define feature map and ansatz num_qubits = 2 feature_map = ZZFeatureMap(num_qubits, reps=1) @@ -84,9 +90,13 @@ def interpret_2d(x): # define sampler primitives self.sampler = Sampler() - self.sampler_shots = Sampler(options={"shots": 100}) + self.sampler_shots = Sampler(options={"shots": 100, "seed": 42}) + + self.array_type = {True: SparseArray, False: np.ndarray} - def _get_qnn(self, sparse, sampler_type, interpret_id): + def _get_qnn( + self, sparse, sampler_type, interpret_id, input_params, weight_params, input_grads + ): """Construct QNN from configuration.""" # get quantum instance @@ -111,73 +121,103 @@ def _get_qnn(self, sparse, sampler_type, interpret_id): qnn = SamplerQNN( sampler=sampler, circuit=self.qc, - input_params=self.input_params, - weight_params=self.weight_params, + input_params=input_params, + weight_params=weight_params, sparse=sparse, interpret=interpret, output_shape=output_shape, + input_gradients=input_grads, ) return qnn def _verify_qnn( - self, - qnn: CircuitQNN, - batch_size: int, + self, qnn: SamplerQNN, batch_size: int, input_data: np.ndarray, weights: np.ndarray ) -> None: """ Verifies that a QNN functions correctly """ - # pylint: disable=import-error - from sparse import SparseArray - - input_data = np.zeros((batch_size, qnn.num_inputs)) - weights = np.zeros(qnn.num_weights) - # evaluate QNN forward pass result = qnn.forward(input_data, weights) - # make sure forward result is sparse if it should be - if qnn.sparse: - self.assertTrue(isinstance(result, SparseArray)) - else: - self.assertTrue(isinstance(result, np.ndarray)) + if input_data is None: + batch_size = 1 + self.assertTrue(isinstance(result, self.array_type[qnn.sparse])) # check forward result shape self.assertEqual(result.shape, (batch_size, *qnn.output_shape)) # evaluate QNN backward pass input_grad, weights_grad = qnn.backward(input_data, weights) - # verify that input gradients are None if turned off - self.assertIsNone(input_grad) - self.assertEqual(weights_grad.shape, (batch_size, *qnn.output_shape, qnn.num_weights)) - - if qnn.sparse: - self.assertTrue(isinstance(weights_grad, SparseArray)) - else: - self.assertTrue(isinstance(weights_grad, np.ndarray)) - - # verify that input gradients are not None if turned on - qnn.input_gradients = True - input_grad, weights_grad = qnn.backward(input_data, weights) - - self.assertEqual(input_grad.shape, (batch_size, *qnn.output_shape, qnn.num_inputs)) - self.assertEqual(weights_grad.shape, (batch_size, *qnn.output_shape, qnn.num_weights)) + if qnn.input_gradients: + if input_data is not None: + self.assertEqual(input_grad.shape, (batch_size, *qnn.output_shape, qnn.num_inputs)) + self.assertTrue(isinstance(input_grad, self.array_type[qnn.sparse])) + else: + # verify that input gradients are None if turned off + self.assertIsNone(input_grad) + if weights is not None: + self.assertEqual( + weights_grad.shape, (batch_size, *qnn.output_shape, qnn.num_weights) + ) + self.assertTrue(isinstance(weights_grad, self.array_type[qnn.sparse])) + else: + # verify that input gradients are None if turned off + self.assertIsNone(weights_grad) - if qnn.sparse: - self.assertTrue(isinstance(weights_grad, SparseArray)) - self.assertTrue(isinstance(input_grad, SparseArray)) else: - self.assertTrue(isinstance(weights_grad, np.ndarray)) - self.assertTrue(isinstance(input_grad, np.ndarray)) + # verify that input gradients are None if turned off + self.assertIsNone(input_grad) + if weights is not None: + self.assertEqual( + weights_grad.shape, (batch_size, *qnn.output_shape, qnn.num_weights) + ) + self.assertTrue(isinstance(weights_grad, self.array_type[qnn.sparse])) @unittest.skipIf(not _optionals.HAS_SPARSE, "Sparse not available.") - @idata(itertools.product(SPARSE, SAMPLERS, INTERPRET_TYPES, BATCH_SIZES)) - @unpack - def test_sampler_qnn(self, sparse: bool, sampler_type, interpret_type, batch_size): + @idata(itertools.product(SPARSE, SAMPLERS, INTERPRET_TYPES, BATCH_SIZES, INPUT_GRADS)) + def test_sampler_qnn(self, config): """Sampler QNN Test.""" - qnn = self._get_qnn(sparse, sampler_type, interpret_type) - self._verify_qnn(qnn, batch_size) + + sparse, sampler_type, interpret_type, batch_size, input_grads = config + # Test QNN with input and weight params + qnn = self._get_qnn( + sparse, + sampler_type, + interpret_type, + input_params=self.input_params, + weight_params=self.weight_params, + input_grads=True, + ) + input_data = np.zeros((batch_size, qnn.num_inputs)) + weights = np.zeros(qnn.num_weights) + self._verify_qnn(qnn, batch_size, input_data, weights) + + # Test QNN with no input params + qnn = self._get_qnn( + sparse, + sampler_type, + interpret_type, + input_params=None, + weight_params=self.weight_params + self.input_params, + input_grads=input_grads, + ) + input_data = None + weights = np.zeros(qnn.num_weights) + self._verify_qnn(qnn, batch_size, input_data, weights) + + # Test QNN with no weight params + qnn = self._get_qnn( + sparse, + sampler_type, + interpret_type, + input_params=self.weight_params + self.input_params, + weight_params=None, + input_grads=input_grads, + ) + input_data = np.zeros((batch_size, qnn.num_inputs)) + weights = None + self._verify_qnn(qnn, batch_size, input_data, weights) @unittest.skipIf(not _optionals.HAS_SPARSE, "Sparse not available.") @idata(itertools.product(SPARSE, INTERPRET_TYPES, BATCH_SIZES)) @@ -188,10 +228,15 @@ def test_sampler_qnn_gradient(self, config): sparse, interpret_id, batch_size = config # get QNN - qnn = self._get_qnn(sparse, DEFAULT, interpret_id) + qnn = self._get_qnn( + sparse, + DEFAULT, + interpret_id, + input_params=self.input_params, + weight_params=self.weight_params, + input_grads=True, + ) - # set input gradients to True - qnn.input_gradients = True input_data = np.ones((batch_size, qnn.num_inputs)) weights = np.ones(qnn.num_weights) input_grad, weights_grad = qnn.backward(input_data, weights) @@ -227,53 +272,3 @@ def test_sampler_qnn_gradient(self, config): ].reshape(grad.shape) diff = weights_grad_ - grad self.assertAlmostEqual(np.max(np.abs(diff)), 0.0, places=3) - - def test_circuit_vs_sampler_qnn(self): - """Circuit vs Sampler QNN Test. To be removed once CircuitQNN is deprecated""" - from qiskit.opflow import Gradient - import importlib - - aer = importlib.import_module("qiskit.providers.aer") - - parity = lambda x: f"{x:b}".count("1") % 2 - output_shape = 2 # this is required in case of a callable with dense output - - qi_qasm = QuantumInstance( - aer.AerSimulator(), - shots=100, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) - - circuit_qnn = CircuitQNN( - self.qc, - input_params=self.qc.parameters[:3], - weight_params=self.qc.parameters[3:], - sparse=False, - interpret=parity, - output_shape=output_shape, - quantum_instance=qi_qasm, - gradient=Gradient("param_shift"), - input_gradients=True, - ) - - sampler_qnn = SamplerQNN( - sampler=self.sampler, - circuit=self.qc, - input_params=self.qc.parameters[:3], - weight_params=self.qc.parameters[3:], - interpret=parity, - output_shape=output_shape, - gradient=ParamShiftSamplerGradient(self.sampler), - input_gradients=True, - ) - - inputs = np.asarray(algorithm_globals.random.random(size=(1, circuit_qnn._num_inputs))) - weights = algorithm_globals.random.random(circuit_qnn._num_weights) - - circuit_qnn_fwd = circuit_qnn.backward(inputs, weights) - sampler_qnn_fwd = sampler_qnn.backward(inputs, weights) - - np.testing.assert_array_almost_equal( - np.asarray(sampler_qnn_fwd), np.asarray(circuit_qnn_fwd), 0.1 - ) From b2db7b89e7a796b3b8610b3936fd5e33eb342493 Mon Sep 17 00:00:00 2001 From: ElePT Date: Mon, 17 Oct 2022 13:54:28 +0200 Subject: [PATCH 41/61] Fix tests, final refactor, add docstring --- .../algorithms/classifiers/vqc.py | 23 ++-- .../neural_networks/sampler_qnn.py | 20 +++- test/algorithms/classifiers/test_vqc.py | 113 ++++++------------ .../test_circuit_vs_sampler_qnn.py | 111 +++++++++++++++++ 4 files changed, 176 insertions(+), 91 deletions(-) create mode 100644 test/neural_networks/test_circuit_vs_sampler_qnn.py diff --git a/qiskit_machine_learning/algorithms/classifiers/vqc.py b/qiskit_machine_learning/algorithms/classifiers/vqc.py index 0375a6cb4..8a7994968 100644 --- a/qiskit_machine_learning/algorithms/classifiers/vqc.py +++ b/qiskit_machine_learning/algorithms/classifiers/vqc.py @@ -12,7 +12,7 @@ """An implementation of variational quantum classifier.""" from __future__ import annotations -from typing import Callable, cast +from typing import Callable import numpy as np @@ -107,25 +107,25 @@ def __init__( # needed for mypy neural_network: SamplerQNN | CircuitQNN = None if quantum_instance is not None: - # construct sampler QNN - neural_network = SamplerQNN( - sampler=sampler, - circuit=self._circuit, + # construct circuit QNN only if qi is provided + neural_network = CircuitQNN( + self._circuit, input_params=self.feature_map.parameters, weight_params=self.ansatz.parameters, interpret=self._get_interpret(2), output_shape=2, + quantum_instance=quantum_instance, input_gradients=False, ) else: - # construct circuit QNN - neural_network = CircuitQNN( - self._circuit, + # construct sampler QNN by default + neural_network = SamplerQNN( + sampler=sampler, + circuit=self._circuit, input_params=self.feature_map.parameters, weight_params=self.ansatz.parameters, interpret=self._get_interpret(2), output_shape=2, - quantum_instance=quantum_instance, input_gradients=False, ) @@ -173,12 +173,11 @@ def _fit_internal(self, X: np.ndarray, y: np.ndarray) -> OptimizerResult: X, y = self._validate_input(X, y) num_classes = self._num_classes + # instance check required by mypy (alternative to cast) if isinstance(self._neural_network, SamplerQNN): self._neural_network.set_interpret(self._get_interpret(num_classes), num_classes) elif isinstance(self._neural_network, CircuitQNN): - cast(CircuitQNN, self._neural_network).set_interpret( - self._get_interpret(num_classes), num_classes - ) + self._neural_network.set_interpret(self._get_interpret(num_classes), num_classes) return super()._minimize(X, y) diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index 44b4db37c..3c6a4ea1e 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -48,7 +48,20 @@ class SparseArray: # type: ignore class SamplerQNN(NeuralNetwork): - """A Neural Network implementation based on the Sampler primitive.""" + """A Neural Network implementation based on the Sampler primitive. + + The ``Sampler QNN`` is a neural network that takes in a parametrized quantum circuit + with the combined network's feature map (input parameters) and ansatz (weight parameters) + and outputs its measurements for the forward and backward passes. + The output can be set up in different formats, and an optional post-processing step + can be used to interpret the sampler's output in a particular context (e.g. mapping the + resulting bitstring to match the number of classes). + + Attributes: + + sampler (BaseSampler): The sampler primitive used to compute the neural network's results. + gradient (BaseSamplerGradient): An optional sampler gradient to be used for the backward pass. + """ def __init__( self, @@ -65,7 +78,7 @@ def __init__( ): """ Args: - sampler: The sampler primitive used to compute neural network's results. + sampler: The sampler primitive used to compute the neural network's results. circuit: The parametrized quantum circuit that generates the samples of this network. input_params: The parameters of the circuit corresponding to the input. weight_params: The parameters of the circuit corresponding to the trainable weights. @@ -108,11 +121,8 @@ def __init__( _optionals.HAS_SPARSE.require_now("DOK") self.set_interpret(interpret, output_shape) - self._input_gradients = input_gradients - # TODO: look into custom transpilation - super().__init__( num_inputs=len(self._input_params), num_weights=len(self._weight_params), diff --git a/test/algorithms/classifiers/test_vqc.py b/test/algorithms/classifiers/test_vqc.py index 1141db3be..5c98bd978 100644 --- a/test/algorithms/classifiers/test_vqc.py +++ b/test/algorithms/classifiers/test_vqc.py @@ -36,7 +36,7 @@ from qiskit_machine_learning.algorithms import VQC from qiskit_machine_learning.exceptions import QiskitMachineLearningError -QUANTUM_INSTANCES = ["statevector", "sampler", "qasm"] +RUN_METHODS = ["statevector", "sampler", "qasm"] NUM_QUBITS_LIST = [2, None] FEATURE_MAPS = ["zz_feature_map", None] ANSATZES = ["real_amplitudes", None] @@ -93,9 +93,6 @@ def setUp(self): sampler = Sampler() # We want string keys to ensure DDT-generated tests have meaningful names. self.properties = { - "statevector": statevector, - "qasm": qasm, - "sampler": sampler, "bfgs": L_BFGS_B(maxiter=5), "cobyla": COBYLA(maxiter=25), "real_amplitudes": RealAmplitudes(num_qubits=2, reps=1), @@ -105,13 +102,19 @@ def setUp(self): "no_one_hot": _create_dataset(6, 2, one_hot=False), } + self.run_methods = { # Tuple of type: (primitive, qi) + "sampler": (sampler, None), + "statevector": (None, statevector), + "qasm": (None, qasm), + } + @idata( itertools.product( - QUANTUM_INSTANCES, NUM_QUBITS_LIST, FEATURE_MAPS, ANSATZES, OPTIMIZERS, DATASETS + RUN_METHODS, NUM_QUBITS_LIST, FEATURE_MAPS, ANSATZES, OPTIMIZERS, DATASETS ) ) @unpack - def test_VQC(self, q_i, num_qubits, f_m, ans, opt, d_s): + def test_VQC(self, run_method, num_qubits, f_m, ans, opt, d_s): """ Test VQC with binary and multiclass data using a range of quantum instances, numbers of qubits, feature maps, and optimizers. @@ -120,12 +123,8 @@ def test_VQC(self, q_i, num_qubits, f_m, ans, opt, d_s): self.skipTest( "At least one of num_qubits, feature_map, or ansatz must be set by the user." ) - if q_i != "sampler": - sampler = None - quantum_instance = self.properties.get(q_i) - else: - sampler = self.properties.get(q_i) - quantum_instance = None + + sampler, quantum_instance = self.run_methods.get(run_method) feature_map = self.properties.get(f_m) optimizer = self.properties.get(opt) @@ -155,17 +154,12 @@ def test_VQC(self, q_i, num_qubits, f_m, ans, opt, d_s): # the predicted value should be in the labels self.assertTrue(np.all(predict == unique_labels, axis=1).any()) - @idata(QUANTUM_INSTANCES[:-1]) - def test_VQC_non_parameterized(self, test): + @idata(RUN_METHODS[:-1]) + def test_VQC_non_parameterized(self, run_method): """ Test VQC without an optimizer set. """ - if test != "sampler": - sampler = None - quantum_instance = self.properties.get(test) - else: - sampler = self.properties.get(test) - quantum_instance = None + sampler, quantum_instance = self.run_methods.get(run_method) classifier = VQC( sampler=sampler, @@ -178,17 +172,12 @@ def test_VQC_non_parameterized(self, test): score = classifier.score(dataset.x, dataset.y) self.assertGreater(score, 0.5) - @idata(itertools.product(DATASETS, QUANTUM_INSTANCES[:-1])) + @idata(itertools.product(DATASETS, RUN_METHODS[:-1])) @unpack - def test_warm_start(self, d_s, test): + def test_warm_start(self, d_s, run_method): """Test VQC when training from a warm start.""" - if test != "sampler": - sampler = None - quantum_instance = self.properties.get(test) - else: - sampler = self.properties.get(test) - quantum_instance = None + sampler, quantum_instance = self.run_methods.get(run_method) classifier = VQC( sampler=sampler, @@ -223,16 +212,11 @@ def wrapper(num_classes): return wrapper - @idata(QUANTUM_INSTANCES[:-1]) - def test_batches_with_incomplete_labels(self, test): + @idata(RUN_METHODS[:-1]) + def test_batches_with_incomplete_labels(self, run_method): """Test VQC when targets are one-hot and some batches don't have all possible labels.""" - if test != "sampler": - sampler = None - quantum_instance = self.properties.get(test) - else: - sampler = self.properties.get(test) - quantum_instance = None + sampler, quantum_instance = self.run_methods.get(run_method) # Generate data with batches that have incomplete labels. x = algorithm_globals.random.random((6, 2)) @@ -263,16 +247,11 @@ def test_batches_with_incomplete_labels(self, test): with self.subTest("Check correct number of classes is used to build CircuitQNN."): self.assertTrue((np.asarray(self.num_classes_by_batch) == 3).all()) - @idata(QUANTUM_INSTANCES[:-1]) - def test_multilabel_targets_raise_an_error(self, test): + @idata(RUN_METHODS[:-1]) + def test_multilabel_targets_raise_an_error(self, run_method): """Tests VQC multi-label input raises an error.""" - if test != "sampler": - sampler = None - quantum_instance = self.properties.get(test) - else: - sampler = self.properties.get(test) - quantum_instance = None + sampler, quantum_instance = self.run_methods.get(run_method) # Generate multi-label data. x = algorithm_globals.random.random((3, 2)) @@ -282,16 +261,11 @@ def test_multilabel_targets_raise_an_error(self, test): with self.assertRaises(QiskitMachineLearningError): classifier.fit(x, y) - @idata(QUANTUM_INSTANCES[:-1]) - def test_changing_classes_raises_error(self, test): + @idata(RUN_METHODS[:-1]) + def test_changing_classes_raises_error(self, run_method): """Tests VQC raises an error when fitting new data with a different number of classes.""" - if test != "sampler": - sampler = None - quantum_instance = self.properties.get(test) - else: - sampler = self.properties.get(test) - quantum_instance = None + sampler, quantum_instance = self.run_methods.get(run_method) targets1 = np.asarray([[0, 0, 1], [0, 1, 0]]) targets2 = np.asarray([[0, 1], [1, 0]]) @@ -305,15 +279,16 @@ def test_changing_classes_raises_error(self, test): with self.assertRaises(QiskitMachineLearningError): classifier.fit(features2, targets2) - @idata(itertools.product(QUANTUM_INSTANCES, LOSSES)) + @idata(itertools.product(RUN_METHODS, LOSSES)) @unpack - def test_sparse_arrays(self, q_i, loss): + def test_sparse_arrays(self, run_method, loss): """Tests VQC on sparse features and labels.""" - if q_i == "sampler": - self.skipTest("skipping test because SamplerQNN does not support sparse arrays") - quantum_instance = self.properties.get(q_i) - classifier = VQC(num_qubits=2, loss=loss, quantum_instance=quantum_instance) + sampler, quantum_instance = self.run_methods.get(run_method) + + classifier = VQC( + sampler=sampler, num_qubits=2, loss=loss, quantum_instance=quantum_instance + ) x = scipy.sparse.csr_matrix([[0, 0], [1, 1]]) y = scipy.sparse.csr_matrix([[1, 0], [0, 1]]) @@ -322,16 +297,11 @@ def test_sparse_arrays(self, q_i, loss): score = classifier.score(x, y) self.assertGreaterEqual(score, 0.5) - @idata(QUANTUM_INSTANCES[:-1]) - def test_categorical(self, test): + @idata(RUN_METHODS[:-1]) + def test_categorical(self, run_method): """Test VQC on categorical labels.""" - if test != "sampler": - sampler = None - quantum_instance = self.properties.get(test) - else: - sampler = self.properties.get(test) - quantum_instance = None + sampler, quantum_instance = self.run_methods.get(run_method) classifier = VQC( sampler=sampler, @@ -352,16 +322,11 @@ def test_categorical(self, test): predict = classifier.predict(features[0, :]) self.assertIn(predict, ["A", "B"]) - @idata(QUANTUM_INSTANCES[:-1]) - def test_circuit_extensions(self, test): + @idata(RUN_METHODS[:-1]) + def test_circuit_extensions(self, run_method): """Test VQC when the number of qubits is different compared to the feature map/ansatz.""" - if test != "sampler": - sampler = None - quantum_instance = self.properties.get(test) - else: - sampler = self.properties.get(test) - quantum_instance = None + sampler, quantum_instance = self.run_methods.get(run_method) num_qubits = 2 classifier = VQC( diff --git a/test/neural_networks/test_circuit_vs_sampler_qnn.py b/test/neural_networks/test_circuit_vs_sampler_qnn.py new file mode 100644 index 000000000..045edafae --- /dev/null +++ b/test/neural_networks/test_circuit_vs_sampler_qnn.py @@ -0,0 +1,111 @@ +# 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. + +"""Test Sampler QNN vs Circuit QNN.""" + +from test import QiskitMachineLearningTestCase + +import itertools +import unittest +import numpy as np +from ddt import ddt, idata + +from qiskit import BasicAer +from qiskit.algorithms.gradients import ParamShiftSamplerGradient +from qiskit.circuit import QuantumCircuit +from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap +from qiskit.opflow import Gradient +from qiskit.primitives import Sampler +from qiskit.utils import QuantumInstance, algorithm_globals + +from qiskit_machine_learning.neural_networks import CircuitQNN, SamplerQNN +import qiskit_machine_learning.optionals as _optionals + +SPARSE = [True, False] +INPUT_GRADS = [True, False] + + +@ddt +class TestCircuitvsSamplerQNN(QiskitMachineLearningTestCase): + """Circuit vs Sampler QNN Tests. To be removed once CircuitQNN is deprecated""" + + def setUp(self): + super().setUp() + algorithm_globals.random_seed = 10598 + + self.parity = lambda x: f"{x:b}".count("1") % 2 + self.output_shape = 2 # this is required in case of a callable with dense output + + # define feature map and ansatz + num_qubits = 2 + feature_map = ZZFeatureMap(num_qubits, reps=1) + var_form = RealAmplitudes(num_qubits, reps=1) + # construct circuit + self.qc = QuantumCircuit(num_qubits) + self.qc.append(feature_map, range(2)) + self.qc.append(var_form, range(2)) + + # store params + self.input_params = list(feature_map.parameters) + self.weight_params = list(var_form.parameters) + + self.sampler = Sampler() + + @unittest.skipIf(not _optionals.HAS_SPARSE, "Sparse not available.") + @idata(itertools.product(SPARSE, INPUT_GRADS)) + def test_new_vs_old(self, config): + """Circuit vs Sampler QNN Test. To be removed once CircuitQNN is deprecated""" + + sparse, input_grads = config + qi_sv = QuantumInstance(BasicAer.get_backend("statevector_simulator")) + + circuit_qnn = CircuitQNN( + self.qc, + input_params=self.qc.parameters[:3], + weight_params=self.qc.parameters[3:], + sparse=sparse, + interpret=self.parity, + output_shape=self.output_shape, + quantum_instance=qi_sv, + gradient=Gradient("param_shift"), + input_gradients=input_grads, + ) + + sampler_qnn = SamplerQNN( + sampler=self.sampler, + circuit=self.qc, + input_params=self.qc.parameters[:3], + weight_params=self.qc.parameters[3:], + interpret=self.parity, + output_shape=self.output_shape, + gradient=ParamShiftSamplerGradient(self.sampler), + input_gradients=input_grads, + ) + + inputs = np.asarray(algorithm_globals.random.random(size=(3, circuit_qnn._num_inputs))) + weights = algorithm_globals.random.random(circuit_qnn._num_weights) + + circuit_qnn_fwd = circuit_qnn.forward(inputs, weights) + sampler_qnn_fwd = sampler_qnn.forward(inputs, weights) + + diff_fwd = circuit_qnn_fwd - sampler_qnn_fwd + self.assertAlmostEqual(np.max(np.abs(diff_fwd)), 0.0, places=3) + + circuit_qnn_input_grads, circuit_qnn_weight_grads = circuit_qnn.backward(inputs, weights) + sampler_qnn_input_grads, sampler_qnn_weight_grads = sampler_qnn.backward(inputs, weights) + + diff_weight = circuit_qnn_weight_grads - sampler_qnn_weight_grads + self.assertAlmostEqual(np.max(np.abs(diff_weight)), 0.0, places=3) + + if input_grads: + diff_input = circuit_qnn_input_grads - sampler_qnn_input_grads + self.assertAlmostEqual(np.max(np.abs(diff_input)), 0.0, places=3) From f3a79919d5977347383aa3bce9aa385c73fca1c2 Mon Sep 17 00:00:00 2001 From: ElePT Date: Mon, 17 Oct 2022 14:20:14 +0200 Subject: [PATCH 42/61] Add Sampler QNN --- .../add-sampler-qnn-a093431afc1c5441.yaml | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 releasenotes/notes/add-sampler-qnn-a093431afc1c5441.yaml diff --git a/releasenotes/notes/add-sampler-qnn-a093431afc1c5441.yaml b/releasenotes/notes/add-sampler-qnn-a093431afc1c5441.yaml new file mode 100644 index 000000000..a7a8f33bc --- /dev/null +++ b/releasenotes/notes/add-sampler-qnn-a093431afc1c5441.yaml @@ -0,0 +1,54 @@ +--- +features: + - | + Introduced Sampler Quantum Neural Network + (:class:`~qiskit_machine_learning.neural_networks.SamplerQNN`) based on (runtime) primitives. + This implementation leverages the sampler primitive + (see :class:`~qiskit.primitives.BaseSampler`) and the sampler gradients + (see :class:`~qiskit.algorithms.gradients.BaseSamplerGradient`) to enable runtime access and + more efficient computation of forward and backward passes more efficiently. + + The new Sampler QNN exposes a similar interface to the Circuit QNN, with a few differences. + One is the `quantum_instance` parameter. This parameter does not have a direct replacement, + and instead the `sampler` parameter must be used. The `gradient` parameter keeps the same name + as in the Circuit QNN implementation, but it no longer accepts Opflow gradient classes as inputs; + instead, this parameter expects an (optionally custom) primitive gradient. The `sampling` option + has been removed for the time being, as this information is not currently exposed by the `Sampler`, + and might correspond to future lower-level primitives. + + The existing training algorithms such as :class:`~qiskit_machine_learning.algorithms.VQC`, + that were based on the Circuit QNN, are updated to accept both implementations. The implementation + of :class:`~qiskit_machine_learning.algorithms.NeuralNetworkClassifier` has not changed. + + For example a `VQC` using `SamplerQNN` can be trained as follows: + + .. code-block:: python + + from qiskit.circuit.library import ZZFeatureMap, RealAmplitudes + from qiskit.algorithms.optimizers import COBYLA + from qiskit.primitives import Sampler + from sklearn.datasets import make_blobs + + from qiskit_machine_learning.algorithms import VQC + + # generate a simple dataset + num_inputs = 20 + features, labels = make_blobs(n_samples=num_inputs, centers=2, center_box=(-1, 1), cluster_std=0.1) + + # construct feature map + feature_map = ZZFeatureMap(num_inputs) + + # construct ansatz + ansatz = RealAmplitudes(num_inputs, reps=1) + + # construct variational quantum classifier + vqc = VQC( + sampler=sampler, + feature_map=feature_map, + ansatz=ansatz, + loss="cross_entropy", + optimizer=COBYLA(maxiter=30), + ) + + # fit classifier to data + vqc.fit(features, labels) From 349fb8abac75ed7cf79b1b7fc5064317053626c1 Mon Sep 17 00:00:00 2001 From: ElePT <57907331+ElePT@users.noreply.github.com> Date: Wed, 19 Oct 2022 11:25:12 +0200 Subject: [PATCH 43/61] Update qiskit_machine_learning/algorithms/classifiers/vqc.py Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> --- qiskit_machine_learning/algorithms/classifiers/vqc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_machine_learning/algorithms/classifiers/vqc.py b/qiskit_machine_learning/algorithms/classifiers/vqc.py index 8a7994968..2acb07fd4 100644 --- a/qiskit_machine_learning/algorithms/classifiers/vqc.py +++ b/qiskit_machine_learning/algorithms/classifiers/vqc.py @@ -58,7 +58,7 @@ def __init__( initial_point: np.ndarray | None = None, callback: Callable[[np.ndarray, float], None] | None = None, *, - sampler: BaseSampler = None, + sampler: BaseSampler | None = None, ) -> None: """ Args: From 04042bab1f6a54c75e5a422628e301c7e6603800 Mon Sep 17 00:00:00 2001 From: ElePT Date: Wed, 19 Oct 2022 12:34:21 +0200 Subject: [PATCH 44/61] Apply reviews vqc --- .../algorithms/classifiers/vqc.py | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/qiskit_machine_learning/algorithms/classifiers/vqc.py b/qiskit_machine_learning/algorithms/classifiers/vqc.py index 8a7994968..4dcac2335 100644 --- a/qiskit_machine_learning/algorithms/classifiers/vqc.py +++ b/qiskit_machine_learning/algorithms/classifiers/vqc.py @@ -62,30 +62,37 @@ def __init__( ) -> None: """ Args: - num_qubits: The number of qubits for the underlying - :class:`~qiskit_machine_learning.neural_networks.CircuitQNN`. If ``None`` is given, - the number of qubits is derived from the feature map or ansatz. If neither of those - is given, raises an exception. The number of qubits in the feature map and ansatz - are adjusted to this number if required. + num_qubits: The number of qubits for the underlying QNN. + If ``None`` is given, the number of qubits is derived from the + feature map or ansatz. If neither of those is given, raises an exception. + The number of qubits in the feature map and ansatz are adjusted to this + number if required. feature_map: The (parametrized) circuit to be used as a feature map for the underlying - :class:`~qiskit_machine_learning.neural_networks.CircuitQNN`. If ``None`` is given, - the ``ZZFeatureMap`` is used if the number of qubits is larger than 1. For a single - qubit classification problem the ``ZFeatureMap`` is used per default. + QNN. If ``None`` is given, the ``ZZFeatureMap`` is used if the number of qubits + is larger than 1. For a single qubit classification problem the ``ZFeatureMap`` + is used by default. ansatz: The (parametrized) circuit to be used as an ansatz for the underlying - :class:`~qiskit_machine_learning.neural_networks.CircuitQNN`. If ``None`` is given - then the ``RealAmplitudes`` circuit is used. + QNN. If ``None`` is given then the ``RealAmplitudes`` circuit is used. loss: A target loss function to be used in training. Default value is ``cross_entropy``. optimizer: An instance of an optimizer to be used in training. When ``None`` defaults to SLSQP. warm_start: Use weights from previous fit to start next fit. - quantum_instance: The quantum instance to execute circuits on. + quantum_instance: If a quantum instance is sent and ``sampler`` is ``None``, + the underlying QNN will be of type + :class:`~qiskit_machine_learning.neural_networks.CircuitQNN`, and the quantum + instance will be used to compute the neural network's results. If a sampler + instance is also set, it will override the `quantum_instance` parameter and + a :class:`~qiskit_machine_learning.neural_networks.SamplerQNN` + will be used instead. initial_point: Initial point for the optimizer to start from. callback: a reference to a user's callback function that has two parameters and returns ``None``. The callback can access intermediate data during training. On each iteration an optimizer invokes the callback and passes current weights as an array and a computed value as a float of the objective function being optimized. This allows to track how well optimization / training process is going on. - sampler: The sampler primitive used to compute neural network's results. + sampler: If a sampler instance is sent, the underlying QNN will be of type + :class:`~qiskit_machine_learning.neural_networks.SamplerQNN`, and the sampler + primitive will be used to compute the neural network's results. Raises: QiskitMachineLearningError: Needs at least one out of ``num_qubits``, ``feature_map`` or ``ansatz`` to be given. Or the number of qubits in the feature map and/or ansatz @@ -106,8 +113,7 @@ def __init__( # needed for mypy neural_network: SamplerQNN | CircuitQNN = None - if quantum_instance is not None: - # construct circuit QNN only if qi is provided + if quantum_instance is not None and sampler is None: neural_network = CircuitQNN( self._circuit, input_params=self.feature_map.parameters, @@ -174,9 +180,7 @@ def _fit_internal(self, X: np.ndarray, y: np.ndarray) -> OptimizerResult: num_classes = self._num_classes # instance check required by mypy (alternative to cast) - if isinstance(self._neural_network, SamplerQNN): - self._neural_network.set_interpret(self._get_interpret(num_classes), num_classes) - elif isinstance(self._neural_network, CircuitQNN): + if isinstance(self._neural_network, CircuitQNN|SamplerQNN): self._neural_network.set_interpret(self._get_interpret(num_classes), num_classes) return super()._minimize(X, y) From 5c5e705b8da6bbf3b26707bf93593ffe26b8edcc Mon Sep 17 00:00:00 2001 From: ElePT Date: Wed, 19 Oct 2022 13:00:24 +0200 Subject: [PATCH 45/61] Apply reviews --- .../algorithms/classifiers/vqc.py | 4 +-- .../neural_networks/sampler_qnn.py | 35 ++++++++----------- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/qiskit_machine_learning/algorithms/classifiers/vqc.py b/qiskit_machine_learning/algorithms/classifiers/vqc.py index 4dcac2335..8020a724e 100644 --- a/qiskit_machine_learning/algorithms/classifiers/vqc.py +++ b/qiskit_machine_learning/algorithms/classifiers/vqc.py @@ -58,7 +58,7 @@ def __init__( initial_point: np.ndarray | None = None, callback: Callable[[np.ndarray, float], None] | None = None, *, - sampler: BaseSampler = None, + sampler: BaseSampler | None = None, ) -> None: """ Args: @@ -180,7 +180,7 @@ def _fit_internal(self, X: np.ndarray, y: np.ndarray) -> OptimizerResult: num_classes = self._num_classes # instance check required by mypy (alternative to cast) - if isinstance(self._neural_network, CircuitQNN|SamplerQNN): + if isinstance(self._neural_network, CircuitQNN | SamplerQNN): self._neural_network.set_interpret(self._get_interpret(num_classes), num_classes) return super()._minimize(X, y) diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index 3c6a4ea1e..64f0ffcbe 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -67,7 +67,7 @@ def __init__( self, *, sampler: BaseSampler | None = None, - circuit: QuantumCircuit | None = None, + circuit: QuantumCircuit, input_params: Sequence[Parameter] | None = None, weight_params: Sequence[Parameter] | None = None, sparse: bool = False, @@ -79,6 +79,8 @@ def __init__( """ Args: sampler: The sampler primitive used to compute the neural network's results. + If ``None`` is given, a default instance of the reference sampler defined + by :class:`~qiskit.primitives.Sampler` will be used. circuit: The parametrized quantum circuit that generates the samples of this network. input_params: The parameters of the circuit corresponding to the input. weight_params: The parameters of the circuit corresponding to the trainable weights. @@ -89,6 +91,8 @@ def __init__( passed, then an identity function will be used by this neural network. output_shape: The output shape of the custom interpretation gradient: An optional sampler gradient to be used for the backward pass. + If ``None`` is given, a default instance of + :class:`~qiskit.algorithms.gradients.ParamShiftSamplerGradient` will be used. input_gradients: Determines whether to compute gradients with respect to input data. Note that this parameter is ``False`` by default, and must be explicitly set to ``True`` for a proper gradient computation when using ``TorchConnector``. @@ -227,16 +231,11 @@ def _preprocess( return parameters, num_samples - def _postprocess( - self, num_samples: int | None, result: SamplerResult | None - ) -> np.ndarray | SparseArray | None: + def _postprocess(self, num_samples: int, result: SamplerResult) -> np.ndarray | SparseArray: """ Post-processing during forward pass of the network. """ - if result is None: - return None - if self._sparse: # pylint: disable=import-error from sparse import DOK @@ -263,13 +262,11 @@ def _postprocess( return prob def _postprocess_gradient( - self, num_samples: int | None, results: SamplerGradientResult | None - ) -> tuple[np.ndarray | SparseArray | None, np.ndarray | SparseArray | None]: + self, num_samples: int, results: SamplerGradientResult + ) -> tuple[np.ndarray | SparseArray | None, np.ndarray | SparseArray]: """ Post-processing during backward pass of the network. """ - if num_samples is None or results is None: - return None, None if self._sparse: # pylint: disable=import-error @@ -345,10 +342,9 @@ def _forward( # sampler allows batching job = self.sampler.run([self._circuit] * num_samples, parameter_values) results = job.result() + result = self._postprocess(num_samples, results) else: - results = None - - result = self._postprocess(num_samples, results) + result = None return result @@ -362,23 +358,22 @@ def _backward( # prepare parameters in the required format parameter_values, num_samples = self._preprocess(input_data, weights) + results = None if num_samples is not None and np.prod(parameter_values.shape) > 0: if self._input_gradients: job = self.gradient.run([self._circuit] * num_samples, parameter_values) results = job.result() else: - if len(parameter_values[0]) is not self._num_inputs: + if len(parameter_values[0]) > self._num_inputs: job = self.gradient.run( [self._circuit] * num_samples, parameter_values, parameters=[self._circuit.parameters[self._num_inputs :]] * num_samples, ) results = job.result() - else: - results = None - else: - results = None - input_grad, weights_grad = self._postprocess_gradient(num_samples, results) + if results is None: + return None, None + input_grad, weights_grad = self._postprocess_gradient(num_samples, results) return input_grad, weights_grad # `None` for gradients wrt input data, see TorchConnector From c2eaf90283560f8e5bca6afb116f617020d77c80 Mon Sep 17 00:00:00 2001 From: ElePT Date: Wed, 19 Oct 2022 13:05:39 +0200 Subject: [PATCH 46/61] Fix neko test --- qiskit_machine_learning/algorithms/classifiers/vqc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_machine_learning/algorithms/classifiers/vqc.py b/qiskit_machine_learning/algorithms/classifiers/vqc.py index 8020a724e..b35d7c1d0 100644 --- a/qiskit_machine_learning/algorithms/classifiers/vqc.py +++ b/qiskit_machine_learning/algorithms/classifiers/vqc.py @@ -180,7 +180,7 @@ def _fit_internal(self, X: np.ndarray, y: np.ndarray) -> OptimizerResult: num_classes = self._num_classes # instance check required by mypy (alternative to cast) - if isinstance(self._neural_network, CircuitQNN | SamplerQNN): + if isinstance(self._neural_network, (CircuitQNN, SamplerQNN)): self._neural_network.set_interpret(self._get_interpret(num_classes), num_classes) return super()._minimize(X, y) From 8dd2d785c684db7218984bedbc502887cb5c7c10 Mon Sep 17 00:00:00 2001 From: ElePT Date: Wed, 19 Oct 2022 13:15:46 +0200 Subject: [PATCH 47/61] Fix spell check --- test/algorithms/classifiers/test_vqc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/algorithms/classifiers/test_vqc.py b/test/algorithms/classifiers/test_vqc.py index 5c98bd978..77c6031b4 100644 --- a/test/algorithms/classifiers/test_vqc.py +++ b/test/algorithms/classifiers/test_vqc.py @@ -102,7 +102,7 @@ def setUp(self): "no_one_hot": _create_dataset(6, 2, one_hot=False), } - self.run_methods = { # Tuple of type: (primitive, qi) + self.run_methods = { # Tuple of type: (primitive, quantum_instance) "sampler": (sampler, None), "statevector": (None, statevector), "qasm": (None, qasm), From f6014925321bfce4d425b98b11ed51e185cbffd9 Mon Sep 17 00:00:00 2001 From: ElePT <57907331+ElePT@users.noreply.github.com> Date: Thu, 27 Oct 2022 11:09:11 +0200 Subject: [PATCH 48/61] Update qiskit_machine_learning/neural_networks/sampler_qnn.py --- qiskit_machine_learning/neural_networks/sampler_qnn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index 64f0ffcbe..431e964fc 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -66,8 +66,8 @@ class SamplerQNN(NeuralNetwork): def __init__( self, *, - sampler: BaseSampler | None = None, circuit: QuantumCircuit, + sampler: BaseSampler | None = None, input_params: Sequence[Parameter] | None = None, weight_params: Sequence[Parameter] | None = None, sparse: bool = False, From a0d92ecb5bd46e51e3ea266583c3c8a88ce779b6 Mon Sep 17 00:00:00 2001 From: ElePT Date: Fri, 28 Oct 2022 11:46:38 +0200 Subject: [PATCH 49/61] Add try-catch --- .../neural_networks/sampler_qnn.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index 431e964fc..07286da94 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -341,7 +341,10 @@ def _forward( if num_samples is not None and np.prod(parameter_values.shape) > 0: # sampler allows batching job = self.sampler.run([self._circuit] * num_samples, parameter_values) - results = job.result() + try: + results = job.result() + except Exception as exc: + raise QiskitMachineLearningError("Sampler job failed.") from exc result = self._postprocess(num_samples, results) else: result = None @@ -362,7 +365,6 @@ def _backward( if num_samples is not None and np.prod(parameter_values.shape) > 0: if self._input_gradients: job = self.gradient.run([self._circuit] * num_samples, parameter_values) - results = job.result() else: if len(parameter_values[0]) > self._num_inputs: job = self.gradient.run( @@ -370,7 +372,10 @@ def _backward( parameter_values, parameters=[self._circuit.parameters[self._num_inputs :]] * num_samples, ) - results = job.result() + try: + results = job.result() + except Exception as exc: + raise QiskitMachineLearningError("Sampler job failed.") from exc if results is None: return None, None From 959621a00190078e13c9d3fea6894c59b767bf34 Mon Sep 17 00:00:00 2001 From: ElePT Date: Fri, 28 Oct 2022 14:08:34 +0200 Subject: [PATCH 50/61] Add deprecations --- qiskit_machine_learning/algorithms/classifiers/vqc.py | 6 +++++- qiskit_machine_learning/neural_networks/sampler_qnn.py | 3 +++ test/algorithms/classifiers/test_vqc.py | 6 +++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/qiskit_machine_learning/algorithms/classifiers/vqc.py b/qiskit_machine_learning/algorithms/classifiers/vqc.py index b35d7c1d0..40d2dec06 100644 --- a/qiskit_machine_learning/algorithms/classifiers/vqc.py +++ b/qiskit_machine_learning/algorithms/classifiers/vqc.py @@ -22,6 +22,7 @@ from qiskit.algorithms.optimizers import Optimizer, OptimizerResult from qiskit.primitives import BaseSampler +from ...deprecation import warn_deprecated, DeprecatedType from ...neural_networks import CircuitQNN, SamplerQNN from ...utils import derive_num_qubits_feature_map_ansatz from ...utils.loss_functions import Loss @@ -77,7 +78,7 @@ def __init__( optimizer: An instance of an optimizer to be used in training. When ``None`` defaults to SLSQP. warm_start: Use weights from previous fit to start next fit. - quantum_instance: If a quantum instance is sent and ``sampler`` is ``None``, + quantum_instance: Deprecated: If a quantum instance is sent and ``sampler`` is ``None``, the underlying QNN will be of type :class:`~qiskit_machine_learning.neural_networks.CircuitQNN`, and the quantum instance will be used to compute the neural network's results. If a sampler @@ -114,6 +115,9 @@ def __init__( # needed for mypy neural_network: SamplerQNN | CircuitQNN = None if quantum_instance is not None and sampler is None: + warn_deprecated( + "0.5.0", DeprecatedType.ARGUMENT, old_name="quantum_instance", new_name="sampler" + ) neural_network = CircuitQNN( self._circuit, input_params=self.feature_map.parameters, diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index 07286da94..15d687650 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -57,6 +57,9 @@ class SamplerQNN(NeuralNetwork): can be used to interpret the sampler's output in a particular context (e.g. mapping the resulting bitstring to match the number of classes). + The following attributes can be set via the constructor but can also be read and + updated once the SamplerQNN object has been constructed. + Attributes: sampler (BaseSampler): The sampler primitive used to compute the neural network's results. diff --git a/test/algorithms/classifiers/test_vqc.py b/test/algorithms/classifiers/test_vqc.py index 77c6031b4..12b25b9ac 100644 --- a/test/algorithms/classifiers/test_vqc.py +++ b/test/algorithms/classifiers/test_vqc.py @@ -23,7 +23,7 @@ from ddt import ddt, idata, unpack import numpy as np import scipy - +import warnings from sklearn.datasets import make_classification from sklearn.preprocessing import MinMaxScaler, OneHotEncoder @@ -73,6 +73,7 @@ class TestVQC(QiskitMachineLearningTestCase): @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required to run this test") def setUp(self): + super().setUp() algorithm_globals.random_seed = 1111111 self.num_classes_by_batch = [] @@ -108,6 +109,9 @@ def setUp(self): "qasm": (None, qasm), } + # ignore deprecation warnings + warnings.filterwarnings("ignore") + @idata( itertools.product( RUN_METHODS, NUM_QUBITS_LIST, FEATURE_MAPS, ANSATZES, OPTIMIZERS, DATASETS From c5a6c11550116896b060819c7010ae7e74fc0f33 Mon Sep 17 00:00:00 2001 From: ElePT Date: Fri, 28 Oct 2022 14:19:29 +0200 Subject: [PATCH 51/61] Update tests --- .../neural_networks/sampler_qnn.py | 12 ++++++---- test/neural_networks/test_sampler_qnn.py | 24 ++++++++++++++++++- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index 15d687650..e0cb67484 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -368,6 +368,10 @@ def _backward( if num_samples is not None and np.prod(parameter_values.shape) > 0: if self._input_gradients: job = self.gradient.run([self._circuit] * num_samples, parameter_values) + try: + results = job.result() + except Exception as exc: + raise QiskitMachineLearningError("Sampler job failed.") from exc else: if len(parameter_values[0]) > self._num_inputs: job = self.gradient.run( @@ -375,10 +379,10 @@ def _backward( parameter_values, parameters=[self._circuit.parameters[self._num_inputs :]] * num_samples, ) - try: - results = job.result() - except Exception as exc: - raise QiskitMachineLearningError("Sampler job failed.") from exc + try: + results = job.result() + except Exception as exc: + raise QiskitMachineLearningError("Sampler job failed.") from exc if results is None: return None, None diff --git a/test/neural_networks/test_sampler_qnn.py b/test/neural_networks/test_sampler_qnn.py index b723a1843..d1877f642 100644 --- a/test/neural_networks/test_sampler_qnn.py +++ b/test/neural_networks/test_sampler_qnn.py @@ -20,7 +20,7 @@ from ddt import ddt, idata -from qiskit.circuit import QuantumCircuit +from qiskit.circuit import Parameter, QuantumCircuit from qiskit.primitives import Sampler from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap from qiskit.utils import algorithm_globals @@ -272,3 +272,25 @@ def test_sampler_qnn_gradient(self, config): ].reshape(grad.shape) diff = weights_grad_ - grad self.assertAlmostEqual(np.max(np.abs(diff)), 0.0, places=3) + + def test_setters_getters(self): + """Test Sampler QNN properties.""" + params = [Parameter("input1"), Parameter("weight1")] + qc = QuantumCircuit(1) + qc.h(0) + qc.ry(params[0], 0) + qc.rx(params[1], 0) + qc.measure_all() + sampler_qnn = SamplerQNN( + circuit=qc, + input_params=[params[0]], + weight_params=[params[1]], + ) + with self.subTest("Test input_params getter."): + self.assertEqual(sampler_qnn.input_params, [params[0]]) + with self.subTest("Test weight_params getter."): + self.assertEqual(sampler_qnn.weight_params, [params[1]]) + with self.subTest("Test input_gradients setter and getter."): + self.assertFalse(sampler_qnn.input_gradients) + sampler_qnn.input_gradients = True + self.assertTrue(sampler_qnn.input_gradients) From f7394c6eb64713558b53d2daeeb5aac6fd4e3c1b Mon Sep 17 00:00:00 2001 From: ElePT Date: Fri, 28 Oct 2022 14:19:45 +0200 Subject: [PATCH 52/61] Fix tests --- test/neural_networks/test_sampler_qnn.py | 40 ++++++++++++------------ 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/test/neural_networks/test_sampler_qnn.py b/test/neural_networks/test_sampler_qnn.py index d1877f642..338bb1a06 100644 --- a/test/neural_networks/test_sampler_qnn.py +++ b/test/neural_networks/test_sampler_qnn.py @@ -274,23 +274,23 @@ def test_sampler_qnn_gradient(self, config): self.assertAlmostEqual(np.max(np.abs(diff)), 0.0, places=3) def test_setters_getters(self): - """Test Sampler QNN properties.""" - params = [Parameter("input1"), Parameter("weight1")] - qc = QuantumCircuit(1) - qc.h(0) - qc.ry(params[0], 0) - qc.rx(params[1], 0) - qc.measure_all() - sampler_qnn = SamplerQNN( - circuit=qc, - input_params=[params[0]], - weight_params=[params[1]], - ) - with self.subTest("Test input_params getter."): - self.assertEqual(sampler_qnn.input_params, [params[0]]) - with self.subTest("Test weight_params getter."): - self.assertEqual(sampler_qnn.weight_params, [params[1]]) - with self.subTest("Test input_gradients setter and getter."): - self.assertFalse(sampler_qnn.input_gradients) - sampler_qnn.input_gradients = True - self.assertTrue(sampler_qnn.input_gradients) + """Test Sampler QNN properties.""" + params = [Parameter("input1"), Parameter("weight1")] + qc = QuantumCircuit(1) + qc.h(0) + qc.ry(params[0], 0) + qc.rx(params[1], 0) + qc.measure_all() + sampler_qnn = SamplerQNN( + circuit=qc, + input_params=[params[0]], + weight_params=[params[1]], + ) + with self.subTest("Test input_params getter."): + self.assertEqual(sampler_qnn.input_params, [params[0]]) + with self.subTest("Test weight_params getter."): + self.assertEqual(sampler_qnn.weight_params, [params[1]]) + with self.subTest("Test input_gradients setter and getter."): + self.assertFalse(sampler_qnn.input_gradients) + sampler_qnn.input_gradients = True + self.assertTrue(sampler_qnn.input_gradients) From 438a4342fee1167b1a526ce3abd8b9f057b73821 Mon Sep 17 00:00:00 2001 From: ElePT Date: Fri, 28 Oct 2022 14:26:24 +0200 Subject: [PATCH 53/61] Filter warnings --- test/algorithms/classifiers/test_vqc.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/algorithms/classifiers/test_vqc.py b/test/algorithms/classifiers/test_vqc.py index 12b25b9ac..c605030a1 100644 --- a/test/algorithms/classifiers/test_vqc.py +++ b/test/algorithms/classifiers/test_vqc.py @@ -109,8 +109,10 @@ def setUp(self): "qasm": (None, qasm), } + def tearDown(self) -> None: # ignore deprecation warnings - warnings.filterwarnings("ignore") + super().tearDown() + warnings.filterwarnings("always", category=DeprecationWarning) @idata( itertools.product( From 7824b0739271f1d03af9d8dd2f2848bca3c0a750 Mon Sep 17 00:00:00 2001 From: ElePT Date: Fri, 28 Oct 2022 14:31:43 +0200 Subject: [PATCH 54/61] Fix filter --- test/algorithms/classifiers/test_vqc.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/algorithms/classifiers/test_vqc.py b/test/algorithms/classifiers/test_vqc.py index c605030a1..15ae72e0a 100644 --- a/test/algorithms/classifiers/test_vqc.py +++ b/test/algorithms/classifiers/test_vqc.py @@ -108,9 +108,12 @@ def setUp(self): "statevector": (None, statevector), "qasm": (None, qasm), } + # ignore deprecation warnings + warnings.filterwarnings("ignore", category=DeprecationWarning) + def tearDown(self) -> None: - # ignore deprecation warnings + # restore warnings super().tearDown() warnings.filterwarnings("always", category=DeprecationWarning) From 35bdc54159a621845c3b2df93b5c75eaf7afdcd5 Mon Sep 17 00:00:00 2001 From: Anton Dekusar Date: Fri, 28 Oct 2022 16:03:32 +0100 Subject: [PATCH 55/61] fix black, pylint --- qiskit_machine_learning/algorithms/classifiers/vqc.py | 4 ++-- test/algorithms/classifiers/test_vqc.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/qiskit_machine_learning/algorithms/classifiers/vqc.py b/qiskit_machine_learning/algorithms/classifiers/vqc.py index 40d2dec06..a68d18cc4 100644 --- a/qiskit_machine_learning/algorithms/classifiers/vqc.py +++ b/qiskit_machine_learning/algorithms/classifiers/vqc.py @@ -116,8 +116,8 @@ def __init__( neural_network: SamplerQNN | CircuitQNN = None if quantum_instance is not None and sampler is None: warn_deprecated( - "0.5.0", DeprecatedType.ARGUMENT, old_name="quantum_instance", new_name="sampler" - ) + "0.5.0", DeprecatedType.ARGUMENT, old_name="quantum_instance", new_name="sampler" + ) neural_network = CircuitQNN( self._circuit, input_params=self.feature_map.parameters, diff --git a/test/algorithms/classifiers/test_vqc.py b/test/algorithms/classifiers/test_vqc.py index 15ae72e0a..be9bac8ff 100644 --- a/test/algorithms/classifiers/test_vqc.py +++ b/test/algorithms/classifiers/test_vqc.py @@ -19,11 +19,11 @@ import functools import itertools import unittest +import warnings from ddt import ddt, idata, unpack import numpy as np import scipy -import warnings from sklearn.datasets import make_classification from sklearn.preprocessing import MinMaxScaler, OneHotEncoder @@ -111,7 +111,6 @@ def setUp(self): # ignore deprecation warnings warnings.filterwarnings("ignore", category=DeprecationWarning) - def tearDown(self) -> None: # restore warnings super().tearDown() From 12c64761381ee7406dc78dc0be3910761c8efe61 Mon Sep 17 00:00:00 2001 From: Anton Dekusar Date: Tue, 1 Nov 2022 13:38:49 +0000 Subject: [PATCH 56/61] update docstring --- .../neural_networks/sampler_qnn.py | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index e0cb67484..7fa3c4cd8 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -51,12 +51,48 @@ class SamplerQNN(NeuralNetwork): """A Neural Network implementation based on the Sampler primitive. The ``Sampler QNN`` is a neural network that takes in a parametrized quantum circuit - with the combined network's feature map (input parameters) and ansatz (weight parameters) - and outputs its measurements for the forward and backward passes. + with designated parameters for input data and/or weights and translates the quasi-probabilities + estimated by the :class:`~qiskit.primitives.Sampler` primitive into predicted classes. Quite + often, a combined quantum circuit is used. Such a circuit is built from two circuits: + a feature map, it provides input parameters for the network, and an ansatz (weight parameters). + The output can be set up in different formats, and an optional post-processing step can be used to interpret the sampler's output in a particular context (e.g. mapping the resulting bitstring to match the number of classes). + In this example the network maps the output of the quantum circuit to two classes via a custom + `interpret` function: + + .. code-block:: + + from qiskit import QuantumCircuit + from qiskit.circuit.library import ZZFeatureMap, RealAmplitudes + + from qiskit_machine_learning.neural_networks import SamplerQNN + + num_qubits = 2 + fm = ZZFeatureMap(feature_dimension=num_qubits) + ansatz = RealAmplitudes(num_qubits=num_qubits, reps=1) + + qc = QuantumCircuit(num_qubits) + qc.compose(fm, inplace=True) + qc.compose(ansatz, inplace=True) + + + def parity(x): + return "{:b}".format(x).count("1") % 2 + + + qnn = SamplerQNN( + circuit=qc, + input_params=fm.parameters, + weight_params=ansatz.parameters, + interpret=parity, + output_shape=2 + ) + + qnn.forward(input_data=[1, 2], weights=[1, 2, 3, 4]) + The following attributes can be set via the constructor but can also be read and updated once the SamplerQNN object has been constructed. From 4f43cd719eac4a2317eab0f0d83f30876c2d8c02 Mon Sep 17 00:00:00 2001 From: Anton Dekusar Date: Tue, 1 Nov 2022 14:13:54 +0000 Subject: [PATCH 57/61] pulled the method up, modern type hints --- .../neural_networks/estimator_qnn.py | 27 +------- .../neural_networks/neural_network.py | 63 +++++++++++++------ .../neural_networks/sampler_qnn.py | 29 +-------- test/connectors/test_torch_networks.py | 2 +- .../test_circuit_vs_sampler_qnn.py | 2 +- 5 files changed, 49 insertions(+), 74 deletions(-) diff --git a/qiskit_machine_learning/neural_networks/estimator_qnn.py b/qiskit_machine_learning/neural_networks/estimator_qnn.py index d1a6bec01..09ebf5ed0 100644 --- a/qiskit_machine_learning/neural_networks/estimator_qnn.py +++ b/qiskit_machine_learning/neural_networks/estimator_qnn.py @@ -171,29 +171,6 @@ def input_gradients(self, input_gradients: bool) -> None: """Turn on/off computation of gradients with respect to input data.""" self._input_gradients = input_gradients - def _preprocess( - self, - input_data: np.ndarray | None, - weights: np.ndarray | None, - ) -> tuple[np.ndarray | None, int | None]: - """ - Pre-processing during forward pass of the network. - """ - if input_data is not None: - num_samples = input_data.shape[0] - if weights is not None: - weights = np.broadcast_to(weights, (num_samples, len(weights))) - parameters = np.concatenate((input_data, weights), axis=1) - else: - parameters = input_data - else: - if weights is not None: - num_samples = 1 - parameters = np.broadcast_to(weights, (num_samples, len(weights))) - else: - return None, None - return parameters, num_samples - def _forward_postprocess(self, num_samples: int, result: EstimatorResult) -> np.ndarray: """Post-processing during forward pass of the network.""" if num_samples is None: @@ -205,7 +182,7 @@ def _forward( self, input_data: np.ndarray | None, weights: np.ndarray | None ) -> np.ndarray | None: """Forward pass of the neural network.""" - parameter_values_, num_samples = self._preprocess(input_data, weights) + parameter_values_, num_samples = self._preprocess_forward(input_data, weights) if num_samples is None: job = self.estimator.run(self._circuit, self._observables) else: @@ -250,7 +227,7 @@ def _backward( ) -> tuple[np.ndarray | None, np.ndarray]: """Backward pass of the network.""" # prepare parameters in the required format - parameter_values_, num_samples = self._preprocess(input_data, weights) + parameter_values_, num_samples = self._preprocess_forward(input_data, weights) if num_samples is None or (not self._input_gradients and self._num_weights == 0): return None, None diff --git a/qiskit_machine_learning/neural_networks/neural_network.py b/qiskit_machine_learning/neural_networks/neural_network.py index 643464f9b..67ac5b824 100644 --- a/qiskit_machine_learning/neural_networks/neural_network.py +++ b/qiskit_machine_learning/neural_networks/neural_network.py @@ -13,9 +13,9 @@ """A Neural Network abstract class for all (quantum) neural networks within Qiskit's machine learning module.""" +from __future__ import annotations from abc import ABC, abstractmethod -from typing import Tuple, Union, List, Optional import numpy as np @@ -45,7 +45,7 @@ def __init__( num_inputs: int, num_weights: int, sparse: bool, - output_shape: Union[int, Tuple[int, ...]], + output_shape: int | tuple[int, ...], input_gradients: bool = False, ) -> None: """ @@ -92,7 +92,7 @@ def sparse(self) -> bool: return self._sparse @property - def output_shape(self) -> Tuple[int, ...]: + def output_shape(self) -> tuple[int, ...]: """Returns the output shape.""" return self._output_shape @@ -117,8 +117,8 @@ def _validate_output_shape(self, output_shape): return output_shape def _validate_input( - self, input_data: Optional[Union[List[float], np.ndarray, float]] - ) -> Tuple[Union[np.ndarray, None], Union[Tuple[int, ...], None]]: + self, input_data: float | list[float] | np.ndarray | None + ) -> tuple[np.ndarray | None, tuple[int, ...] | None]: if input_data is None: return None, None input_ = np.array(input_data) @@ -144,16 +144,39 @@ def _validate_input( return input_, shape + def _preprocess_forward( + self, + input_data: np.ndarray | None, + weights: np.ndarray | None, + ) -> tuple[np.ndarray | None, int | None]: + """ + Pre-processing during forward pass of the network for the primitive-based networks. + """ + if input_data is not None: + num_samples = input_data.shape[0] + if weights is not None: + weights = np.broadcast_to(weights, (num_samples, len(weights))) + parameters = np.concatenate((input_data, weights), axis=1) + else: + parameters = input_data + else: + if weights is not None: + num_samples = 1 + parameters = np.broadcast_to(weights, (num_samples, len(weights))) + else: + return None, None + return parameters, num_samples + def _validate_weights( - self, weights: Optional[Union[List[float], np.ndarray, float]] - ) -> Union[np.ndarray, None]: + self, weights: float | list[float] | np.ndarray | None + ) -> np.ndarray | None: if weights is None: return None weights_ = np.array(weights) return weights_.reshape(self._num_weights) def _validate_forward_output( - self, output_data: np.ndarray, original_shape: Tuple[int, ...] + self, output_data: np.ndarray, original_shape: tuple[int, ...] ) -> np.ndarray: if original_shape and len(original_shape) >= 2: output_data = output_data.reshape((*original_shape[:-1], *self._output_shape)) @@ -164,8 +187,8 @@ def _validate_backward_output( self, input_grad: np.ndarray, weight_grad: np.ndarray, - original_shape: Tuple[int, ...], - ) -> Tuple[Union[np.ndarray, SparseArray], Union[np.ndarray, SparseArray]]: + original_shape: tuple[int, ...], + ) -> tuple[np.ndarray | SparseArray, np.ndarray | SparseArray]: if input_grad is not None and np.prod(input_grad.shape) == 0: input_grad = None if input_grad is not None and original_shape and len(original_shape) >= 2: @@ -183,9 +206,9 @@ def _validate_backward_output( def forward( self, - input_data: Optional[Union[List[float], np.ndarray, float]], - weights: Optional[Union[List[float], np.ndarray, float]], - ) -> Union[np.ndarray, SparseArray]: + input_data: float | list[float] | np.ndarray | None, + weights: float | list[float] | np.ndarray | None, + ) -> np.ndarray | SparseArray: """Forward pass of the network. Args: @@ -203,15 +226,15 @@ def forward( @abstractmethod def _forward( - self, input_data: Optional[np.ndarray], weights: Optional[np.ndarray] - ) -> Union[np.ndarray, SparseArray]: + self, input_data: np.ndarray | None, weights: np.ndarray | None + ) -> np.ndarray | SparseArray: raise NotImplementedError def backward( self, - input_data: Optional[Union[List[float], np.ndarray, float]], - weights: Optional[Union[List[float], np.ndarray, float]], - ) -> Tuple[Optional[Union[np.ndarray, SparseArray]], Optional[Union[np.ndarray, SparseArray]],]: + input_data: float | list[float] | np.ndarray | None, + weights: float | list[float] | np.ndarray | None, + ) -> tuple[np.ndarray | SparseArray | None, np.ndarray | SparseArray | None]: """Backward pass of the network. Args: @@ -236,6 +259,6 @@ def backward( @abstractmethod def _backward( - self, input_data: Optional[np.ndarray], weights: Optional[np.ndarray] - ) -> Tuple[Optional[Union[np.ndarray, SparseArray]], Optional[Union[np.ndarray, SparseArray]],]: + self, input_data: np.ndarray | None, weights: np.ndarray | None + ) -> tuple[np.ndarray | SparseArray | None, np.ndarray | SparseArray | None]: raise NotImplementedError diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index 7fa3c4cd8..bdcb0767f 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -245,31 +245,6 @@ def _compute_output_shape( return output_shape_ - def _preprocess( - self, - input_data: np.ndarray | None, - weights: np.ndarray | None, - ) -> tuple[np.ndarray | None, int | None]: - """ - Pre-processing during forward pass of the network. - """ - - if input_data is not None: - num_samples = input_data.shape[0] - if weights is not None: - weights = np.broadcast_to(weights, (num_samples, len(weights))) - parameters = np.concatenate((input_data, weights), axis=1) - else: - parameters = input_data - else: - if weights is not None: - num_samples = 1 - parameters = np.broadcast_to(weights, (num_samples, len(weights))) - else: - return None, None - - return parameters, num_samples - def _postprocess(self, num_samples: int, result: SamplerResult) -> np.ndarray | SparseArray: """ Post-processing during forward pass of the network. @@ -375,7 +350,7 @@ def _forward( """ Forward pass of the network. """ - parameter_values, num_samples = self._preprocess(input_data, weights) + parameter_values, num_samples = self._preprocess_forward(input_data, weights) if num_samples is not None and np.prod(parameter_values.shape) > 0: # sampler allows batching @@ -398,7 +373,7 @@ def _backward( """Backward pass of the network.""" # prepare parameters in the required format - parameter_values, num_samples = self._preprocess(input_data, weights) + parameter_values, num_samples = self._preprocess_forward(input_data, weights) results = None if num_samples is not None and np.prod(parameter_values.shape) > 0: diff --git a/test/connectors/test_torch_networks.py b/test/connectors/test_torch_networks.py index 80c1d6f87..867fd5285 100644 --- a/test/connectors/test_torch_networks.py +++ b/test/connectors/test_torch_networks.py @@ -24,7 +24,7 @@ TwoLayerQNN, NeuralNetwork, EstimatorQNN, - SamplerQNN + SamplerQNN, ) from qiskit_machine_learning.connectors import TorchConnector diff --git a/test/neural_networks/test_circuit_vs_sampler_qnn.py b/test/neural_networks/test_circuit_vs_sampler_qnn.py index 045edafae..a790de4f0 100644 --- a/test/neural_networks/test_circuit_vs_sampler_qnn.py +++ b/test/neural_networks/test_circuit_vs_sampler_qnn.py @@ -35,7 +35,7 @@ @ddt -class TestCircuitvsSamplerQNN(QiskitMachineLearningTestCase): +class TestCircuitQNNvsSamplerQNN(QiskitMachineLearningTestCase): """Circuit vs Sampler QNN Tests. To be removed once CircuitQNN is deprecated""" def setUp(self): From b8c1042eed55265044861c46e648bd0a292aac0a Mon Sep 17 00:00:00 2001 From: Anton Dekusar Date: Tue, 1 Nov 2022 15:05:15 +0000 Subject: [PATCH 58/61] fix spell --- qiskit_machine_learning/neural_networks/sampler_qnn.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index bdcb0767f..0c8281465 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -71,11 +71,11 @@ class SamplerQNN(NeuralNetwork): from qiskit_machine_learning.neural_networks import SamplerQNN num_qubits = 2 - fm = ZZFeatureMap(feature_dimension=num_qubits) + feature_map = ZZFeatureMap(feature_dimension=num_qubits) ansatz = RealAmplitudes(num_qubits=num_qubits, reps=1) qc = QuantumCircuit(num_qubits) - qc.compose(fm, inplace=True) + qc.compose(feature_map, inplace=True) qc.compose(ansatz, inplace=True) @@ -85,7 +85,7 @@ def parity(x): qnn = SamplerQNN( circuit=qc, - input_params=fm.parameters, + input_params=feature_map.parameters, weight_params=ansatz.parameters, interpret=parity, output_shape=2 From 79b221aaa23725b6acd5ba29dc4d44d22d4ce70f Mon Sep 17 00:00:00 2001 From: Anton Dekusar <62334182+adekusar-drl@users.noreply.github.com> Date: Tue, 1 Nov 2022 21:40:41 +0000 Subject: [PATCH 59/61] Update qiskit_machine_learning/neural_networks/sampler_qnn.py Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> --- qiskit_machine_learning/neural_networks/sampler_qnn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index 0c8281465..fc70fa5a8 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -99,7 +99,7 @@ def parity(x): Attributes: sampler (BaseSampler): The sampler primitive used to compute the neural network's results. - gradient (BaseSamplerGradient): An optional sampler gradient to be used for the backward pass. + gradient (BaseSamplerGradient): A sampler gradient to be used for the backward pass. """ def __init__( From f5a4698f4e0f943b3d512baa810fd05c165fc5dd Mon Sep 17 00:00:00 2001 From: Anton Dekusar <62334182+adekusar-drl@users.noreply.github.com> Date: Tue, 1 Nov 2022 21:40:51 +0000 Subject: [PATCH 60/61] Update qiskit_machine_learning/neural_networks/sampler_qnn.py Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> --- qiskit_machine_learning/neural_networks/sampler_qnn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index fc70fa5a8..af810f9d1 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -227,7 +227,7 @@ def _compute_output_shape( if interpret is not None: if output_shape is None: raise QiskitMachineLearningError( - "No output shape given, but required in case of custom interpret!" + "No output shape given; it's required when using custom interpret!" ) if isinstance(output_shape, Integral): output_shape = int(output_shape) From 8124e653df44a9df032deb30d9d17de4f774cde7 Mon Sep 17 00:00:00 2001 From: Anton Dekusar Date: Tue, 1 Nov 2022 22:21:08 +0000 Subject: [PATCH 61/61] code review --- .../neural_networks/circuit_qnn.py | 2 +- .../neural_networks/sampler_qnn.py | 17 ++++++++++------- .../neural_networks/sampling_neural_network.py | 2 +- .../notes/add-sampler-qnn-a093431afc1c5441.yaml | 16 ++++++++++------ 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/qiskit_machine_learning/neural_networks/circuit_qnn.py b/qiskit_machine_learning/neural_networks/circuit_qnn.py index 21d029157..cfeb16b57 100644 --- a/qiskit_machine_learning/neural_networks/circuit_qnn.py +++ b/qiskit_machine_learning/neural_networks/circuit_qnn.py @@ -46,7 +46,7 @@ class SparseArray: # type: ignore class CircuitQNN(SamplingNeuralNetwork): - """A Sampling Neural Network based on a given quantum circuit.""" + """A sampling neural network based on a given quantum circuit.""" def __init__( self, diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index 0c8281465..afc06469e 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -48,9 +48,9 @@ class SparseArray: # type: ignore class SamplerQNN(NeuralNetwork): - """A Neural Network implementation based on the Sampler primitive. + """A neural network implementation based on the Sampler primitive. - The ``Sampler QNN`` is a neural network that takes in a parametrized quantum circuit + The ``SamplerQNN`` is a neural network that takes in a parametrized quantum circuit with designated parameters for input data and/or weights and translates the quasi-probabilities estimated by the :class:`~qiskit.primitives.Sampler` primitive into predicted classes. Quite often, a combined quantum circuit is used. Such a circuit is built from two circuits: @@ -128,13 +128,16 @@ def __init__( tuple of unsigned integers. These are used as new indices for the (potentially sparse) output array. If no interpret function is passed, then an identity function will be used by this neural network. - output_shape: The output shape of the custom interpretation + output_shape: The output shape of the custom interpretation. It is ignored if no custom + interpret method is provided where the shape is taken to be + ``2^circuit.num_qubits``.. gradient: An optional sampler gradient to be used for the backward pass. If ``None`` is given, a default instance of :class:`~qiskit.algorithms.gradients.ParamShiftSamplerGradient` will be used. input_gradients: Determines whether to compute gradients with respect to input data. Note that this parameter is ``False`` by default, and must be explicitly set to - ``True`` for a proper gradient computation when using ``TorchConnector``. + ``True`` for a proper gradient computation when using + :class:`~qiskit_machine_learning.connectors.TorchConnector`. Raises: QiskitMachineLearningError: Invalid parameter values. """ @@ -205,9 +208,9 @@ def set_interpret( Args: interpret: A callable that maps the measured integer to another unsigned integer or tuple of unsigned integers. See constructor for more details. - output_shape: The output shape of the custom interpretation, only used in the case - where an interpret function is provided. See constructor - for more details. + output_shape: The output shape of the custom interpretation. It is ignored if no custom + interpret method is provided where the shape is taken to be + ``2^circuit.num_qubits``. """ # derive target values to be used in computations diff --git a/qiskit_machine_learning/neural_networks/sampling_neural_network.py b/qiskit_machine_learning/neural_networks/sampling_neural_network.py index 525abf1e3..72c8fa3e7 100644 --- a/qiskit_machine_learning/neural_networks/sampling_neural_network.py +++ b/qiskit_machine_learning/neural_networks/sampling_neural_network.py @@ -35,7 +35,7 @@ class SparseArray: # type: ignore class SamplingNeuralNetwork(NeuralNetwork): """ - A Sampling Neural Network abstract class for all (quantum) neural networks within Qiskit's + A sampling neural network abstract class for all (quantum) neural networks within Qiskit's machine learning module that generate samples instead of (expected) values. """ diff --git a/releasenotes/notes/add-sampler-qnn-a093431afc1c5441.yaml b/releasenotes/notes/add-sampler-qnn-a093431afc1c5441.yaml index a7a8f33bc..490b99bd2 100644 --- a/releasenotes/notes/add-sampler-qnn-a093431afc1c5441.yaml +++ b/releasenotes/notes/add-sampler-qnn-a093431afc1c5441.yaml @@ -8,19 +8,23 @@ features: (see :class:`~qiskit.algorithms.gradients.BaseSamplerGradient`) to enable runtime access and more efficient computation of forward and backward passes more efficiently. - The new Sampler QNN exposes a similar interface to the Circuit QNN, with a few differences. - One is the `quantum_instance` parameter. This parameter does not have a direct replacement, - and instead the `sampler` parameter must be used. The `gradient` parameter keeps the same name - as in the Circuit QNN implementation, but it no longer accepts Opflow gradient classes as inputs; + The new :class:`~qiskit_machine_learning.neural_networks.SamplerQNN` exposes a similar + interface to the :class:`~qiskit_machine_learning.neural_networks.CircuitQNN`, with a + few differences. One is the `quantum_instance` parameter. This parameter does not have + a direct replacement, and instead the `sampler` parameter must be used. The `gradient` parameter + keeps the same name as in the :class:`~qiskit_machine_learning.neural_networks.CircuitQNN` + implementation, but it no longer accepts Opflow gradient classes as inputs; instead, this parameter expects an (optionally custom) primitive gradient. The `sampling` option has been removed for the time being, as this information is not currently exposed by the `Sampler`, and might correspond to future lower-level primitives. The existing training algorithms such as :class:`~qiskit_machine_learning.algorithms.VQC`, - that were based on the Circuit QNN, are updated to accept both implementations. The implementation + that were based on the :class:`~qiskit_machine_learning.neural_networks.CircuitQNN`, are updated + to accept both implementations. The implementation of :class:`~qiskit_machine_learning.algorithms.NeuralNetworkClassifier` has not changed. - For example a `VQC` using `SamplerQNN` can be trained as follows: + For example a :class:`~qiskit_machine_learning.algorithms.VQC` using + :class:`~qiskit_machine_learning.neural_networks.SamplerQNN` can be trained as follows: .. code-block:: python