Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix QNN for input and weights ordering #728

Merged
merged 7 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()