Skip to content

Commit

Permalink
Simplify SamplerQNN and EstimatorQNN interfaces with QNNCircuit (qisk…
Browse files Browse the repository at this point in the history
…it-community#609)

* init set up & QNNCircuit instance gate

* extend documentation & release notes

* add tests & extend docu.

* update parity function to pass pylint E0012

* omitt QNNCircuit type hint & different wording in documentation

* add quotation marks for property names in estimator_qnn.py

Co-authored-by: Anton Dekusar <[email protected]>

* add quotation marks for property names in sampler_qnn.py

Co-authored-by: Anton Dekusar <[email protected]>

* add quotation marks for weight_params in estimator_qnn.py

Co-authored-by: Anton Dekusar <[email protected]>

* add quotation marks for weight_params in sampler_qnn.py

Co-authored-by: Anton Dekusar <[email protected]>

* add quotation marks for input_params in sampler_qnn.py

Co-authored-by: Anton Dekusar <[email protected]>

* add quotation marks for input_params in estimator_qnn.py

Co-authored-by: Anton Dekusar <[email protected]>

---------

Co-authored-by: Anton Dekusar <[email protected]>
  • Loading branch information
david-alber and adekusar-drl authored May 2, 2023
1 parent 410d212 commit 10aa219
Show file tree
Hide file tree
Showing 5 changed files with 233 additions and 23 deletions.
44 changes: 39 additions & 5 deletions qiskit_machine_learning/neural_networks/estimator_qnn.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from qiskit.quantum_info import SparsePauliOp
from qiskit.quantum_info.operators.base_operator import BaseOperator

from qiskit_machine_learning.circuit.library import QNNCircuit
from qiskit_machine_learning.exceptions import QiskitMachineLearningError

from .neural_network import NeuralNetwork
Expand All @@ -45,19 +46,38 @@ class EstimatorQNN(NeuralNetwork):
their expectation value(s). 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).
In this case a :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` can be passed as
circuit to simplify the composition of a feature map and ansatz.
If a :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` is passed as circuit, the
input and weight parameters do not have to be provided, because these two properties are taken
from the :class:`~qiskit_machine_learning.circuit.library.QNNCircuit`.
Example:
.. code-block::
from qiskit import QuantumCircuit
from qiskit.circuit.library import ZZFeatureMap, RealAmplitudes
from qiskit_machine_learning.circuit.library import QNNCircuit
from qiskit_machine_learning.neural_networks import EstimatorQNN
num_qubits = 2
# Using the QNNCircuit:
# Create a parameterized 2 qubit circuit composed of the default ZZFeatureMap feature map
# and RealAmplitudes ansatz.
qnn_qc = QNNCircuit(num_qubits)
qnn = EstimatorQNN(
circuit=qnn_qc
)
qnn.forward(input_data=[1, 2], weights=[1, 2, 3, 4, 5, 6, 7, 8])
# Explicitly specifying the ansatz and feature map:
feature_map = ZZFeatureMap(feature_dimension=num_qubits)
ansatz = RealAmplitudes(num_qubits=num_qubits, reps=1)
ansatz = RealAmplitudes(num_qubits=num_qubits)
qc = QuantumCircuit(num_qubits)
qc.compose(feature_map, inplace=True)
Expand All @@ -69,7 +89,7 @@ class EstimatorQNN(NeuralNetwork):
weight_params=ansatz.parameters
)
qnn.forward(input_data=[1, 2], weights=[1, 2, 3, 4])
qnn.forward(input_data=[1, 2], weights=[1, 2, 3, 4, 5, 6, 7, 8])
The following attributes can be set via the constructor but can also be read and
Expand Down Expand Up @@ -98,13 +118,23 @@ def __init__(
estimator: The estimator used to compute neural network's results.
If ``None``, a default instance of the reference estimator,
:class:`~qiskit.primitives.Estimator`, will be used.
circuit: The quantum circuit to represent the neural network.
circuit: The quantum circuit to represent the neural network. If a
:class:`~qiskit_machine_learning.circuit.library.QNNCircuit` is passed, the
`input_params` and `weight_params` do not have to be provided, because these two
properties are taken from the
:class:`~qiskit_machine_learning.circuit.library.QNNCircuit`.
observables: The observables for outputs of the neural network. If ``None``,
use the default :math:`Z^{\otimes num\_qubits}` observable.
input_params: The parameters that correspond to the input data of the network.
If ``None``, the input data is not bound to any parameters.
If a :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` is provided the
`input_params` value here is ignored. Instead the value is taken from the
:class:`~qiskit_machine_learning.circuit.library.QNNCircuit` input_parameters.
weight_params: The parameters that correspond to the trainable weights.
If ``None``, the weights are not bound to any parameters.
If a :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` is provided the
`weight_params` value here is ignored. Instead the value is taken from the
:class:`~qiskit_machine_learning.circuit.library.QNNCircuit` weight_parameters.
gradient: The estimator gradient to be used for the backward pass.
If None, a default instance of the estimator gradient,
:class:`~qiskit.algorithms.gradients.ParamShiftEstimatorGradient`, will be used.
Expand All @@ -125,8 +155,12 @@ def __init__(
if isinstance(observables, (PauliSumOp, BaseOperator)):
observables = (observables,)
self._observables = observables
self._input_params = list(input_params) if input_params is not None else []
self._weight_params = list(weight_params) if weight_params is not None else []
if isinstance(circuit, QNNCircuit):
self._input_params = list(circuit.input_parameters)
self._weight_params = list(circuit.weight_parameters)
else:
self._input_params = list(input_params) if input_params is not None else []
self._weight_params = list(weight_params) if weight_params is not None else []
if gradient is None:
gradient = ParamShiftEstimatorGradient(self.estimator)
self.gradient = gradient
Expand Down
63 changes: 46 additions & 17 deletions qiskit_machine_learning/neural_networks/sampler_qnn.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
)
from qiskit.circuit import Parameter, QuantumCircuit
from qiskit.primitives import BaseSampler, SamplerResult, Sampler
from qiskit_machine_learning.circuit.library import QNNCircuit
from qiskit_machine_learning.exceptions import QiskitMachineLearningError
import qiskit_machine_learning.optionals as _optionals

Expand Down Expand Up @@ -55,6 +56,11 @@ class SamplerQNN(NeuralNetwork):
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).
In this case a :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` can be passed as
circuit to simplify the composition of a feature map and ansatz.
If a :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` is passed as circuit, the
input and weight parameters do not have to be provided, because these two properties are taken
from the :class:`~qiskit_machine_learning.circuit.library.QNNCircuit`.
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
Expand All @@ -67,22 +73,36 @@ class SamplerQNN(NeuralNetwork):
from qiskit import QuantumCircuit
from qiskit.circuit.library import ZZFeatureMap, RealAmplitudes
from qiskit_machine_learning.circuit.library import QNNCircuit
from qiskit_machine_learning.neural_networks import SamplerQNN
num_qubits = 2
def parity(x):
return f"{bin(x)}".count("1") % 2
# Using the QNNCircuit:
# Create a parameterized 2 qubit circuit composed of the default ZZFeatureMap feature map
# and RealAmplitudes ansatz.
qnn_qc = QNNCircuit(num_qubits)
qnn = SamplerQNN(
circuit=qnn_qc,
interpret=parity,
output_shape=2
)
qnn.forward(input_data=[1, 2], weights=[1, 2, 3, 4, 5, 6, 7, 8])
# Explicitly specifying the ansatz and feature map:
feature_map = ZZFeatureMap(feature_dimension=num_qubits)
ansatz = RealAmplitudes(num_qubits=num_qubits, reps=1)
ansatz = RealAmplitudes(num_qubits=num_qubits)
qc = QuantumCircuit(num_qubits)
qc.compose(feature_map, inplace=True)
qc.compose(ansatz, inplace=True)
def parity(x):
return "{:b}".format(x).count("1") % 2
qnn = SamplerQNN(
circuit=qc,
input_params=feature_map.parameters,
Expand All @@ -91,7 +111,7 @@ def parity(x):
output_shape=2
)
qnn.forward(input_data=[1, 2], weights=[1, 2, 3, 4])
qnn.forward(input_data=[1, 2], weights=[1, 2, 3, 4, 5, 6, 7, 8])
The following attributes can be set via the constructor but can also be read and
updated once the SamplerQNN object has been constructed.
Expand Down Expand Up @@ -121,16 +141,26 @@ def __init__(
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.
If a :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` is passed, the
`input_params` and `weight_params` do not have to be provided, because these two
properties are taken from the
:class:`~qiskit_machine_learning.circuit.library.QNNCircuit`.
input_params: The parameters of the circuit corresponding to the input. If a
:class:`~qiskit_machine_learning.circuit.library.QNNCircuit` is provided the
`input_params` value here is ignored. Instead the value is taken from the
:class:`~qiskit_machine_learning.circuit.library.QNNCircuit` input_parameters.
weight_params: The parameters of the circuit corresponding to the trainable weights. If
a :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` is provided the
`weight_params` value here is ignored. Instead the value is taken from the
:class:`~qiskit_machine_learning.circuit.library.QNNCircuit` weight_parameters.
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
passed, then an identity function will be used by this neural network.
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``..
``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.
Expand All @@ -155,13 +185,12 @@ def __init__(
if len(self._circuit.clbits) == 0:
self._circuit.measure_all()

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 isinstance(circuit, QNNCircuit):
self._input_params = list(circuit.input_parameters)
self._weight_params = list(circuit.weight_parameters)
else:
self._input_params = list(input_params) if input_params is not None else []
self._weight_params = list(weight_params) if weight_params is not None else []

if sparse:
_optionals.HAS_SPARSE.require_now("DOK")
Expand Down
58 changes: 58 additions & 0 deletions releasenotes/notes/add-qnn-circuit-to-qnns-2e0edc76f4893fdd.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
features:
- |
The :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` class can be passed as circuit
to the :class:`~qiskit_machine_learning.neural_networks.SamplerQNN`
and :class:`~qiskit_machine_learning.neural_networks.EstimatorQNN`.
This simplifies the interfaces to build a :class:`~qiskit.primitives.Sampler` or
:class:`~qiskit.primitives.Estimator` based neural network implementation from a feature map
and an ansatz circuit.
Using the :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` comes with the benefit
that the feature map and ansatz do not have to be composed explicitly.
If a :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` is passed to the
:class:`~qiskit_machine_learning.neural_networks.SamplerQNN` or
:class:`~qiskit_machine_learning.neural_networks.EstimatorQNN` the input and weight parameters
do not have to be provided, because these two properties are taken
from the :class:`~qiskit_machine_learning.circuit.library.QNNCircuit`.
An example of using :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` with the
:class:`~qiskit_machine_learning.neural_networks.SamplerQNN` class is as follows:
.. code-block:: python
from qiskit_machine_learning.circuit.library import QNNCircuit
from qiskit_machine_learning.neural_networks import SamplerQNN
def parity(x):
return f"{bin(x)}".count("1") % 2
# Create a parameterized 2 qubit circuit composed of the default ZZFeatureMap feature map
# and RealAmplitudes ansatz.
qnn_qc = QNNCircuit(num_qubits = 2)
qnn = SamplerQNN(
circuit=qnn_qc,
interpret=parity,
output_shape=2
)
qnn.forward(input_data=[1, 2], weights=[1, 2, 3, 4, 5, 6, 7, 8])
The :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` is used with the
:class:`~qiskit_machine_learning.neural_networks.EstimatorQNN` class in the same fashion:
.. code-block:: python
from qiskit_machine_learning.circuit.library import QNNCircuit
from qiskit_machine_learning.neural_networks import EstimatorQNN
# Create a parameterized 2 qubit circuit composed of the default ZZFeatureMap feature map
# and RealAmplitudes ansatz.
qnn_qc = QNNCircuit(num_qubits = 2)
qnn = EstimatorQNN(
circuit=qnn_qc
)
qnn.forward(input_data=[1, 2], weights=[1, 2, 3, 4, 5, 6, 7, 8])
44 changes: 43 additions & 1 deletion test/neural_networks/test_estimator_qnn.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2022.
# (C) Copyright IBM 2022, 2023.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
Expand All @@ -18,7 +18,9 @@

import numpy as np
from qiskit.circuit import Parameter, QuantumCircuit
from qiskit.circuit.library import ZZFeatureMap, RealAmplitudes
from qiskit.quantum_info import SparsePauliOp
from qiskit_machine_learning.circuit.library import QNNCircuit

from qiskit_machine_learning.neural_networks.estimator_qnn import EstimatorQNN

Expand Down Expand Up @@ -405,6 +407,46 @@ def test_setters_getters(self):
estimator_qnn.input_gradients = True
self.assertTrue(estimator_qnn.input_gradients)

def test_qnn_qc_circui_construction(self):
"""Test Estimator QNN properties and forward/backward pass for QNNCircuit construction"""
num_qubits = 2
feature_map = ZZFeatureMap(feature_dimension=num_qubits)
ansatz = RealAmplitudes(num_qubits=num_qubits, reps=1)

qnn_qc = QNNCircuit(num_qubits=num_qubits, feature_map=feature_map, ansatz=ansatz)
qc = QuantumCircuit(num_qubits)
qc.compose(feature_map, inplace=True)
qc.compose(ansatz, inplace=True)

estimator_qc = EstimatorQNN(
circuit=qc,
input_params=feature_map.parameters,
weight_params=ansatz.parameters,
input_gradients=True,
)
estimator_qnn_qc = EstimatorQNN(circuit=qnn_qc, input_gradients=True)

input_data = [1, 2]
weights = [1, 2, 3, 4]

with self.subTest("Test if Estimator QNN properties are equal."):
self.assertEqual(estimator_qnn_qc.input_params, estimator_qc.input_params)
self.assertEqual(estimator_qnn_qc.weight_params, estimator_qc.weight_params)
self.assertEqual(estimator_qnn_qc.observables, estimator_qc.observables)

with self.subTest("Test if forward pass yields equal results."):
forward_qc = estimator_qc.forward(input_data=input_data, weights=weights)
forward_qnn_qc = estimator_qnn_qc.forward(input_data=input_data, weights=weights)
np.testing.assert_array_almost_equal(forward_qc, forward_qnn_qc)

with self.subTest("Test if backward pass yields equal results."):
backward_qc = estimator_qc.backward(input_data=input_data, weights=weights)
backward_qnn_qc = estimator_qnn_qc.backward(input_data=input_data, weights=weights)
# Test if input grad is identical
np.testing.assert_array_almost_equal(backward_qc[0], backward_qnn_qc[0])
# Test if weights grad is identical
np.testing.assert_array_almost_equal(backward_qc[1], backward_qnn_qc[1])


if __name__ == "__main__":
unittest.main()
Loading

0 comments on commit 10aa219

Please sign in to comment.