Skip to content

Commit

Permalink
Fix QNN for input and weights ordering (#728)
Browse files Browse the repository at this point in the history
* Fix QNN for input and weights ordering

* Black

* Lint

* Update QCNN tutorial

* Add reno

* Fix draw style per #725

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
woodsp-ibm and mergify[bot] authored Jan 8, 2024
1 parent e5af2e6 commit a89e696
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 41 deletions.
2 changes: 1 addition & 1 deletion docs/tutorials/11_qcnn_initial_point.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
[1.930057091422052, 0.2829424508139703, 0.35555636265939633, 0.1750006532903061, 0.3002103666790018, 0.6641911912373437, 1.3310981300850042, 0.5022717197547227, 0.44912874128880675, 0.40236963192983266, 0.3459537084665159, 0.9786311288435154, 0.48716712269991697, -0.007081389738930712, 0.21570815199311827, 0.07334182375267477, 0.6907887498355103, 0.21771166428570735, 1.087665977608006, 1.2571463700739218, 1.0866597360102666, 2.126145551821481, 0.8914518096731741, 1.5053260036617715, 0.44798876926441555, 0.9498701675467225, 0.15490304396579338, 0.1338674031994701, -0.6938374500039391, 0.029396385425104116, -0.09785818314088227, -0.31198441382224246, 0.20004568516690807, 1.848494069662786, -0.028371899054628447, -0.15229494459622284, 0.7653870524298326, 0.6881492316484289, 0.6759011152318357, 1.6028387103546868, 0.47711915171800057, -0.26162053028790294, -0.12898443497061718, 0.5281303751714184, 0.4957555866394333, 1.6095784010055925, 0.5685823964468215, 1.2812276175594062, 0.3032325725579015, 1.4291081956286258, 0.7081163438891277, 1.8291375321912147, -0.11047287562207528, 0.2751308409529747, 0.2834764252747557, 0.29668607404725605, 0.008300790063532154, 0.6707732056265118, 0.5325267632509095, 0.7240676576317691, 0.08123934531343553, -0.0038536767244725153, -0.1001165849018211]
[1.68270961, 0.11605051, 0.34864916, 0.74675878, 1.87124355, 1.49219533, -0.38654013, 1.6794744, 1.46546974, 2.16547249, 1.05274095, 2.2565039, 0.31246977, -0.0977787, 0.26751274, -0.24319314, 0.28359516, 0.17431664, 0.86434056, 1.08183541, 1.64600062, 2.17350294, 0.17430376, 0.08381051, 0.30748524, 1.6671458, 0.23076889, 0.40720057, -0.38243368, -0.28842447, 0.08507067, 1.34472166, -0.08210173, 1.10931829, 0.15418569, 0.65755067, 3.09541972, 0.41647156, 1.12894435, 1.03898584, 0.64931532, -0.43442102, 0.24324246, 0.98370841, 0.57256531, 0.25832156, 0.42749823, 1.78949614, 0.27749909, 0.07237166, 0.05920573, 0.41896919, 0.66868785, 0.73035314, 0.00984019, 0.72243278, 1.10299638, 0.80821682, 0.39530007, 1.03814133, 0.41697893, 0.53016156, 1.13594375]
64 changes: 32 additions & 32 deletions docs/tutorials/11_quantum_convolutional_neural_networks.ipynb

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions qiskit_machine_learning/neural_networks/estimator_qnn.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ def __init__(
if estimator is None:
estimator = Estimator()
self.estimator = estimator
self._circuit = circuit
self._org_circuit = circuit
if observables is None:
observables = SparsePauliOp.from_list([("Z" * circuit.num_qubits, 1)])
if isinstance(observables, BaseOperator):
Expand All @@ -173,10 +173,12 @@ def __init__(
input_gradients=input_gradients,
)

self._circuit = self._reparameterize_circuit(circuit, input_params, weight_params)

@property
def circuit(self) -> QuantumCircuit:
"""The quantum circuit representing the neural network."""
return copy(self._circuit)
return copy(self._org_circuit)

@property
def observables(self) -> Sequence[BaseOperator] | BaseOperator:
Expand Down
53 changes: 53 additions & 0 deletions qiskit_machine_learning/neural_networks/neural_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Sequence

import numpy as np

from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit
import qiskit_machine_learning.optionals as _optionals
from ..exceptions import QiskitMachineLearningError

Expand Down Expand Up @@ -264,3 +266,54 @@ def _backward(
self, input_data: np.ndarray | None, weights: np.ndarray | None
) -> tuple[np.ndarray | SparseArray | None, np.ndarray | SparseArray | None]:
raise NotImplementedError

def _reparameterize_circuit(
self,
circuit: QuantumCircuit,
input_params: Sequence[Parameter] | None = None,
weight_params: Sequence[Parameter] | None = None,
) -> QuantumCircuit:
# As the data (parameter values) for the primitive is ordered as inputs followed by weights
# we need to ensure that the parameters are ordered like this naturally too so the rewrites
# parameters to ensure this. "inputs" as a name comes before "weights" and within they are
# numerically ordered.
if input_params and self.num_inputs != len(input_params):
raise ValueError(
f"input_params length {len(input_params)}"
f" mismatch with num_inputs (self.num_inputs)"
)
if weight_params and self.num_weights != len(weight_params):
raise ValueError(
f"weight_params length {len(weight_params)}"
f" mismatch with num_weights (self.num_weights)"
)

parameters = circuit.parameters

if len(parameters) != (self.num_inputs + self.num_weights):
raise ValueError(
f"Number of circuit parameters {len(parameters)}"
f" mismatch with sum of num inputs and weights"
f" {self.num_inputs + self.num_weights}"
)

new_input_params = ParameterVector("inputs", self.num_inputs)
new_weight_params = ParameterVector("weights", self.num_weights)

new_parameters = {}
if input_params:
for i, param in enumerate(input_params):
if param not in parameters:
raise ValueError(f"Input param `{param.name}` not present in circuit")
new_parameters[param] = new_input_params[i]

if weight_params:
for i, param in enumerate(weight_params):
if param not in parameters:
raise ValueError(f"Weight param {param.name} `not present in circuit")
new_parameters[param] = new_weight_params[i]

if new_parameters:
circuit = circuit.assign_parameters(new_parameters)

return circuit
13 changes: 8 additions & 5 deletions qiskit_machine_learning/neural_networks/sampler_qnn.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,7 @@ def __init__(
gradient = ParamShiftSamplerGradient(self.sampler)
self.gradient = gradient

self._circuit = circuit.copy()
if len(self._circuit.clbits) == 0:
self._circuit.measure_all()
self._org_circuit = circuit

if isinstance(circuit, QNNCircuit):
self._input_params = list(circuit.input_parameters)
Expand All @@ -207,10 +205,15 @@ def __init__(
input_gradients=self._input_gradients,
)

if len(circuit.clbits) == 0:
circuit = circuit.copy()
circuit.measure_all()
self._circuit = self._reparameterize_circuit(circuit, input_params, weight_params)

@property
def circuit(self) -> QuantumCircuit:
"""Returns the underlying quantum circuit."""
return self._circuit
return self._org_circuit

@property
def input_params(self) -> Sequence[Parameter]:
Expand Down Expand Up @@ -274,7 +277,7 @@ 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,)
output_shape_ = (2**self.circuit.num_qubits,)

return output_shape_

Expand Down
13 changes: 13 additions & 0 deletions releasenotes/notes/fix_qnn_binding_order-74caef8a49ecffe5.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
fixes:
- |
Fixes an issue for the Quantum Neural Networks where the binding order of the inputs
and weights might end up being incorrect. Though the params for the inputs and weights
are specified to the QNN, the code previously bound the inputs and weights in the order
given by the circuit.parameters. This would end up being the right order for the Qiskit
circuit library feature maps and ansatzes most often used, as the default parameter
names led to the order being as expected. However for custom names etc. this was not
always the case and then led to unexpected behavior. The sequences for the input and
weights parameters, as supplied, are now always used as the binding order, for the inputs
and weights respectively, such that the order of the parameters in the overall circuit
no longer matters.
22 changes: 21 additions & 1 deletion test/neural_networks/test_estimator_qnn.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

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

Expand Down Expand Up @@ -447,6 +447,26 @@ def test_qnn_qc_circui_construction(self):
# Test if weights grad is identical
np.testing.assert_array_almost_equal(backward_qc[1], backward_qnn_qc[1])

def test_binding_order(self):
"""Test parameter binding order gives result as expected"""
qc = ZFeatureMap(feature_dimension=2, reps=1)
input_params = qc.parameters
weight = Parameter("weight")
for i in range(qc.num_qubits):
qc.rx(weight, i)

observable1 = SparsePauliOp.from_list([("Z" * qc.num_qubits, 1)])
estimator_qnn = EstimatorQNN(
circuit=qc, observables=observable1, input_params=input_params, weight_params=[weight]
)

estimator_qnn_weights = [3]
estimator_qnn_input = [2, 33]
res = estimator_qnn.forward(estimator_qnn_input, estimator_qnn_weights)
# When parameters were used in circuit order, before being assigned correctly, so inputs
# went to input params, weights to weight params, this gave 0.00613403
self.assertAlmostEqual(res[0][0], 0.00040017)


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

0 comments on commit a89e696

Please sign in to comment.