Skip to content

Commit ea94895

Browse files
ElePTmanoelmarquesgentinettagianadekusar-drlwoodsp-ibm
authored
Add SamplerQNN class using terra primitives (#436)
* Add primitives branch to CI workflow * added pocs * init fixed * more init * Add qnns PoC * Establish qnn branch * Add fwd unit test * Refactor sampler qnn * Undo kernel changes * Start adding new type hint style * Move to neural_networks * Runnable tests * Update typehints, refactor * Add new unittests, fix code * Add gradient unit test * Add torch tests * Remove unwanted change circuitQNN * Remove utils * Fix style, lint * Add VQC * Fix mypy * fix mypy * fix mypy * fix mypy * Restore workflow * Restore workflow * Update .github/workflows/main.yml * Fix mypy * Fix CI (hopefully) * Update requirements * Add sparse support * Fix style etc * Add type ignore * Fix style? * skip test if not sparse * Add type ignore * Fix mypy * Make keyword args * Make keyword args * Apply review comments * Fix tests, final refactor, add docstring * Add Sampler QNN * Update qiskit_machine_learning/algorithms/classifiers/vqc.py Co-authored-by: Steve Wood <[email protected]> * Apply reviews vqc * Apply reviews * Fix neko test * Fix spell check * Update qiskit_machine_learning/neural_networks/sampler_qnn.py * Add try-catch * Add deprecations * Update tests * Fix tests * Filter warnings * Fix filter * fix black, pylint * update docstring * pulled the method up, modern type hints * fix spell * Update qiskit_machine_learning/neural_networks/sampler_qnn.py Co-authored-by: Steve Wood <[email protected]> * Update qiskit_machine_learning/neural_networks/sampler_qnn.py Co-authored-by: Steve Wood <[email protected]> * code review Co-authored-by: Manoel Marques <[email protected]> Co-authored-by: Gian Gentinetta <[email protected]> Co-authored-by: Anton Dekusar <[email protected]> Co-authored-by: Steve Wood <[email protected]> Co-authored-by: Anton Dekusar <[email protected]>
1 parent 670bc12 commit ea94895

File tree

12 files changed

+1078
-106
lines changed

12 files changed

+1078
-106
lines changed

qiskit_machine_learning/algorithms/classifiers/vqc.py

Lines changed: 55 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,18 @@
1212
"""An implementation of variational quantum classifier."""
1313

1414
from __future__ import annotations
15-
from typing import Callable, cast
15+
from typing import Callable
1616

1717
import numpy as np
1818

1919
from qiskit import QuantumCircuit
2020
from qiskit.providers import Backend
2121
from qiskit.utils import QuantumInstance
2222
from qiskit.algorithms.optimizers import Optimizer, OptimizerResult
23+
from qiskit.primitives import BaseSampler
2324

24-
from ...neural_networks import CircuitQNN
25+
from ...deprecation import warn_deprecated, DeprecatedType
26+
from ...neural_networks import CircuitQNN, SamplerQNN
2527
from ...utils import derive_num_qubits_feature_map_ansatz
2628
from ...utils.loss_functions import Loss
2729

@@ -56,32 +58,42 @@ def __init__(
5658
quantum_instance: QuantumInstance | Backend | None = None,
5759
initial_point: np.ndarray | None = None,
5860
callback: Callable[[np.ndarray, float], None] | None = None,
61+
*,
62+
sampler: BaseSampler | None = None,
5963
) -> None:
6064
"""
6165
Args:
62-
num_qubits: The number of qubits for the underlying
63-
:class:`~qiskit_machine_learning.neural_networks.CircuitQNN`. If ``None`` is given,
64-
the number of qubits is derived from the feature map or ansatz. If neither of those
65-
is given, raises an exception. The number of qubits in the feature map and ansatz
66-
are adjusted to this number if required.
66+
num_qubits: The number of qubits for the underlying QNN.
67+
If ``None`` is given, the number of qubits is derived from the
68+
feature map or ansatz. If neither of those is given, raises an exception.
69+
The number of qubits in the feature map and ansatz are adjusted to this
70+
number if required.
6771
feature_map: The (parametrized) circuit to be used as a feature map for the underlying
68-
:class:`~qiskit_machine_learning.neural_networks.CircuitQNN`. If ``None`` is given,
69-
the ``ZZFeatureMap`` is used if the number of qubits is larger than 1. For a single
70-
qubit classification problem the ``ZFeatureMap`` is used per default.
72+
QNN. If ``None`` is given, the ``ZZFeatureMap`` is used if the number of qubits
73+
is larger than 1. For a single qubit classification problem the ``ZFeatureMap``
74+
is used by default.
7175
ansatz: The (parametrized) circuit to be used as an ansatz for the underlying
72-
:class:`~qiskit_machine_learning.neural_networks.CircuitQNN`. If ``None`` is given
73-
then the ``RealAmplitudes`` circuit is used.
76+
QNN. If ``None`` is given then the ``RealAmplitudes`` circuit is used.
7477
loss: A target loss function to be used in training. Default value is ``cross_entropy``.
7578
optimizer: An instance of an optimizer to be used in training. When ``None`` defaults
7679
to SLSQP.
7780
warm_start: Use weights from previous fit to start next fit.
78-
quantum_instance: The quantum instance to execute circuits on.
81+
quantum_instance: Deprecated: If a quantum instance is sent and ``sampler`` is ``None``,
82+
the underlying QNN will be of type
83+
:class:`~qiskit_machine_learning.neural_networks.CircuitQNN`, and the quantum
84+
instance will be used to compute the neural network's results. If a sampler
85+
instance is also set, it will override the `quantum_instance` parameter and
86+
a :class:`~qiskit_machine_learning.neural_networks.SamplerQNN`
87+
will be used instead.
7988
initial_point: Initial point for the optimizer to start from.
8089
callback: a reference to a user's callback function that has two parameters and
8190
returns ``None``. The callback can access intermediate data during training.
8291
On each iteration an optimizer invokes the callback and passes current weights
8392
as an array and a computed value as a float of the objective function being
8493
optimized. This allows to track how well optimization / training process is going on.
94+
sampler: If a sampler instance is sent, the underlying QNN will be of type
95+
:class:`~qiskit_machine_learning.neural_networks.SamplerQNN`, and the sampler
96+
primitive will be used to compute the neural network's results.
8597
Raises:
8698
QiskitMachineLearningError: Needs at least one out of ``num_qubits``, ``feature_map`` or
8799
``ansatz`` to be given. Or the number of qubits in the feature map and/or ansatz
@@ -100,16 +112,32 @@ def __init__(
100112
self._circuit.compose(self.feature_map, inplace=True)
101113
self._circuit.compose(self.ansatz, inplace=True)
102114

103-
# construct circuit QNN
104-
neural_network = CircuitQNN(
105-
self._circuit,
106-
input_params=self.feature_map.parameters,
107-
weight_params=self.ansatz.parameters,
108-
interpret=self._get_interpret(2),
109-
output_shape=2,
110-
quantum_instance=quantum_instance,
111-
input_gradients=False,
112-
)
115+
# needed for mypy
116+
neural_network: SamplerQNN | CircuitQNN = None
117+
if quantum_instance is not None and sampler is None:
118+
warn_deprecated(
119+
"0.5.0", DeprecatedType.ARGUMENT, old_name="quantum_instance", new_name="sampler"
120+
)
121+
neural_network = CircuitQNN(
122+
self._circuit,
123+
input_params=self.feature_map.parameters,
124+
weight_params=self.ansatz.parameters,
125+
interpret=self._get_interpret(2),
126+
output_shape=2,
127+
quantum_instance=quantum_instance,
128+
input_gradients=False,
129+
)
130+
else:
131+
# construct sampler QNN by default
132+
neural_network = SamplerQNN(
133+
sampler=sampler,
134+
circuit=self._circuit,
135+
input_params=self.feature_map.parameters,
136+
weight_params=self.ansatz.parameters,
137+
interpret=self._get_interpret(2),
138+
output_shape=2,
139+
input_gradients=False,
140+
)
113141

114142
super().__init__(
115143
neural_network=neural_network,
@@ -154,9 +182,10 @@ def _fit_internal(self, X: np.ndarray, y: np.ndarray) -> OptimizerResult:
154182
"""
155183
X, y = self._validate_input(X, y)
156184
num_classes = self._num_classes
157-
cast(CircuitQNN, self._neural_network).set_interpret(
158-
self._get_interpret(num_classes), num_classes
159-
)
185+
186+
# instance check required by mypy (alternative to cast)
187+
if isinstance(self._neural_network, (CircuitQNN, SamplerQNN)):
188+
self._neural_network.set_interpret(self._get_interpret(num_classes), num_classes)
160189

161190
return super()._minimize(X, y)
162191

qiskit_machine_learning/neural_networks/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
TwoLayerQNN
4848
CircuitQNN
4949
EstimatorQNN
50+
SamplerQNN
5051
5152
Neural Network Metrics
5253
======================
@@ -57,7 +58,6 @@
5758
5859
EffectiveDimension
5960
LocalEffectiveDimension
60-
6161
"""
6262

6363
from .circuit_qnn import CircuitQNN
@@ -67,6 +67,7 @@
6767
from .opflow_qnn import OpflowQNN
6868
from .sampling_neural_network import SamplingNeuralNetwork
6969
from .two_layer_qnn import TwoLayerQNN
70+
from .sampler_qnn import SamplerQNN
7071

7172
__all__ = [
7273
"NeuralNetwork",
@@ -77,4 +78,5 @@
7778
"EffectiveDimension",
7879
"LocalEffectiveDimension",
7980
"EstimatorQNN",
81+
"SamplerQNN",
8082
]

qiskit_machine_learning/neural_networks/circuit_qnn.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class SparseArray: # type: ignore
4646

4747

4848
class CircuitQNN(SamplingNeuralNetwork):
49-
"""A Sampling Neural Network based on a given quantum circuit."""
49+
"""A sampling neural network based on a given quantum circuit."""
5050

5151
def __init__(
5252
self,

qiskit_machine_learning/neural_networks/estimator_qnn.py

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -171,29 +171,6 @@ def input_gradients(self, input_gradients: bool) -> None:
171171
"""Turn on/off computation of gradients with respect to input data."""
172172
self._input_gradients = input_gradients
173173

174-
def _preprocess(
175-
self,
176-
input_data: np.ndarray | None,
177-
weights: np.ndarray | None,
178-
) -> tuple[np.ndarray | None, int | None]:
179-
"""
180-
Pre-processing during forward pass of the network.
181-
"""
182-
if input_data is not None:
183-
num_samples = input_data.shape[0]
184-
if weights is not None:
185-
weights = np.broadcast_to(weights, (num_samples, len(weights)))
186-
parameters = np.concatenate((input_data, weights), axis=1)
187-
else:
188-
parameters = input_data
189-
else:
190-
if weights is not None:
191-
num_samples = 1
192-
parameters = np.broadcast_to(weights, (num_samples, len(weights)))
193-
else:
194-
return None, None
195-
return parameters, num_samples
196-
197174
def _forward_postprocess(self, num_samples: int, result: EstimatorResult) -> np.ndarray:
198175
"""Post-processing during forward pass of the network."""
199176
if num_samples is None:
@@ -205,7 +182,7 @@ def _forward(
205182
self, input_data: np.ndarray | None, weights: np.ndarray | None
206183
) -> np.ndarray | None:
207184
"""Forward pass of the neural network."""
208-
parameter_values_, num_samples = self._preprocess(input_data, weights)
185+
parameter_values_, num_samples = self._preprocess_forward(input_data, weights)
209186
if num_samples is None:
210187
job = self.estimator.run(self._circuit, self._observables)
211188
else:
@@ -250,7 +227,7 @@ def _backward(
250227
) -> tuple[np.ndarray | None, np.ndarray]:
251228
"""Backward pass of the network."""
252229
# prepare parameters in the required format
253-
parameter_values_, num_samples = self._preprocess(input_data, weights)
230+
parameter_values_, num_samples = self._preprocess_forward(input_data, weights)
254231

255232
if num_samples is None or (not self._input_gradients and self._num_weights == 0):
256233
return None, None

qiskit_machine_learning/neural_networks/neural_network.py

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
"""A Neural Network abstract class for all (quantum) neural networks within Qiskit's
1414
machine learning module."""
1515

16+
from __future__ import annotations
1617

1718
from abc import ABC, abstractmethod
18-
from typing import Tuple, Union, List, Optional
1919

2020
import numpy as np
2121

@@ -45,7 +45,7 @@ def __init__(
4545
num_inputs: int,
4646
num_weights: int,
4747
sparse: bool,
48-
output_shape: Union[int, Tuple[int, ...]],
48+
output_shape: int | tuple[int, ...],
4949
input_gradients: bool = False,
5050
) -> None:
5151
"""
@@ -92,7 +92,7 @@ def sparse(self) -> bool:
9292
return self._sparse
9393

9494
@property
95-
def output_shape(self) -> Tuple[int, ...]:
95+
def output_shape(self) -> tuple[int, ...]:
9696
"""Returns the output shape."""
9797
return self._output_shape
9898

@@ -117,8 +117,8 @@ def _validate_output_shape(self, output_shape):
117117
return output_shape
118118

119119
def _validate_input(
120-
self, input_data: Optional[Union[List[float], np.ndarray, float]]
121-
) -> Tuple[Union[np.ndarray, None], Union[Tuple[int, ...], None]]:
120+
self, input_data: float | list[float] | np.ndarray | None
121+
) -> tuple[np.ndarray | None, tuple[int, ...] | None]:
122122
if input_data is None:
123123
return None, None
124124
input_ = np.array(input_data)
@@ -144,16 +144,39 @@ def _validate_input(
144144

145145
return input_, shape
146146

147+
def _preprocess_forward(
148+
self,
149+
input_data: np.ndarray | None,
150+
weights: np.ndarray | None,
151+
) -> tuple[np.ndarray | None, int | None]:
152+
"""
153+
Pre-processing during forward pass of the network for the primitive-based networks.
154+
"""
155+
if input_data is not None:
156+
num_samples = input_data.shape[0]
157+
if weights is not None:
158+
weights = np.broadcast_to(weights, (num_samples, len(weights)))
159+
parameters = np.concatenate((input_data, weights), axis=1)
160+
else:
161+
parameters = input_data
162+
else:
163+
if weights is not None:
164+
num_samples = 1
165+
parameters = np.broadcast_to(weights, (num_samples, len(weights)))
166+
else:
167+
return None, None
168+
return parameters, num_samples
169+
147170
def _validate_weights(
148-
self, weights: Optional[Union[List[float], np.ndarray, float]]
149-
) -> Union[np.ndarray, None]:
171+
self, weights: float | list[float] | np.ndarray | None
172+
) -> np.ndarray | None:
150173
if weights is None:
151174
return None
152175
weights_ = np.array(weights)
153176
return weights_.reshape(self._num_weights)
154177

155178
def _validate_forward_output(
156-
self, output_data: np.ndarray, original_shape: Tuple[int, ...]
179+
self, output_data: np.ndarray, original_shape: tuple[int, ...]
157180
) -> np.ndarray:
158181
if original_shape and len(original_shape) >= 2:
159182
output_data = output_data.reshape((*original_shape[:-1], *self._output_shape))
@@ -164,8 +187,8 @@ def _validate_backward_output(
164187
self,
165188
input_grad: np.ndarray,
166189
weight_grad: np.ndarray,
167-
original_shape: Tuple[int, ...],
168-
) -> Tuple[Union[np.ndarray, SparseArray], Union[np.ndarray, SparseArray]]:
190+
original_shape: tuple[int, ...],
191+
) -> tuple[np.ndarray | SparseArray, np.ndarray | SparseArray]:
169192
if input_grad is not None and np.prod(input_grad.shape) == 0:
170193
input_grad = None
171194
if input_grad is not None and original_shape and len(original_shape) >= 2:
@@ -183,9 +206,9 @@ def _validate_backward_output(
183206

184207
def forward(
185208
self,
186-
input_data: Optional[Union[List[float], np.ndarray, float]],
187-
weights: Optional[Union[List[float], np.ndarray, float]],
188-
) -> Union[np.ndarray, SparseArray]:
209+
input_data: float | list[float] | np.ndarray | None,
210+
weights: float | list[float] | np.ndarray | None,
211+
) -> np.ndarray | SparseArray:
189212
"""Forward pass of the network.
190213
191214
Args:
@@ -203,15 +226,15 @@ def forward(
203226

204227
@abstractmethod
205228
def _forward(
206-
self, input_data: Optional[np.ndarray], weights: Optional[np.ndarray]
207-
) -> Union[np.ndarray, SparseArray]:
229+
self, input_data: np.ndarray | None, weights: np.ndarray | None
230+
) -> np.ndarray | SparseArray:
208231
raise NotImplementedError
209232

210233
def backward(
211234
self,
212-
input_data: Optional[Union[List[float], np.ndarray, float]],
213-
weights: Optional[Union[List[float], np.ndarray, float]],
214-
) -> Tuple[Optional[Union[np.ndarray, SparseArray]], Optional[Union[np.ndarray, SparseArray]],]:
235+
input_data: float | list[float] | np.ndarray | None,
236+
weights: float | list[float] | np.ndarray | None,
237+
) -> tuple[np.ndarray | SparseArray | None, np.ndarray | SparseArray | None]:
215238
"""Backward pass of the network.
216239
217240
Args:
@@ -236,6 +259,6 @@ def backward(
236259

237260
@abstractmethod
238261
def _backward(
239-
self, input_data: Optional[np.ndarray], weights: Optional[np.ndarray]
240-
) -> Tuple[Optional[Union[np.ndarray, SparseArray]], Optional[Union[np.ndarray, SparseArray]],]:
262+
self, input_data: np.ndarray | None, weights: np.ndarray | None
263+
) -> tuple[np.ndarray | SparseArray | None, np.ndarray | SparseArray | None]:
241264
raise NotImplementedError

0 commit comments

Comments
 (0)