diff --git a/qiskit_machine_learning/algorithms/classifiers/vqc.py b/qiskit_machine_learning/algorithms/classifiers/vqc.py index 4192522f3..f20372bbb 100644 --- a/qiskit_machine_learning/algorithms/classifiers/vqc.py +++ b/qiskit_machine_learning/algorithms/classifiers/vqc.py @@ -20,7 +20,7 @@ from qiskit.algorithms.optimizers import Optimizer, OptimizerResult, Minimizer from qiskit.primitives import BaseSampler -from ...neural_networks import CircuitQNN, SamplerQNN +from ...neural_networks import SamplerQNN from ...utils import derive_num_qubits_feature_map_ansatz from ...utils.loss_functions import Loss @@ -158,7 +158,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, SamplerQNN): self._neural_network.set_interpret(self._get_interpret(num_classes), num_classes) function = self._create_objective(X, y) diff --git a/qiskit_machine_learning/algorithms/regressors/vqr.py b/qiskit_machine_learning/algorithms/regressors/vqr.py index d6ecf6c33..8ea343d87 100644 --- a/qiskit_machine_learning/algorithms/regressors/vqr.py +++ b/qiskit_machine_learning/algorithms/regressors/vqr.py @@ -17,7 +17,6 @@ import numpy as np from qiskit import QuantumCircuit from qiskit.algorithms.optimizers import Optimizer, Minimizer -from qiskit.opflow import PauliSumOp from qiskit.primitives import BaseEstimator from qiskit.quantum_info.operators.base_operator import BaseOperator @@ -35,7 +34,7 @@ def __init__( num_qubits: int | None = None, feature_map: QuantumCircuit | None = None, ansatz: QuantumCircuit | None = None, - observable: BaseOperator | PauliSumOp | None = None, + observable: BaseOperator | None = None, loss: str | Loss = "squared_error", optimizer: Optimizer | Minimizer | None = None, warm_start: bool = False, @@ -81,10 +80,10 @@ def __init__( can't be adjusted to ``num_qubits``. ValueError: if the type of the observable is not compatible with ``estimator``. """ - if observable is not None and not isinstance(observable, (BaseOperator, PauliSumOp)): + if observable is not None and not isinstance(observable, BaseOperator): raise ValueError( f"Unsupported type of the observable, expected " - f"'BaseOperator | PauliSumOp', got {type(observable)}" + f"'BaseOperator', got {type(observable)}" ) self._estimator = estimator diff --git a/qiskit_machine_learning/neural_networks/__init__.py b/qiskit_machine_learning/neural_networks/__init__.py index b75f02324..b539872d6 100644 --- a/qiskit_machine_learning/neural_networks/__init__.py +++ b/qiskit_machine_learning/neural_networks/__init__.py @@ -44,9 +44,6 @@ :toctree: ../stubs/ :nosignatures: - OpflowQNN - TwoLayerQNN - CircuitQNN EstimatorQNN SamplerQNN @@ -61,21 +58,15 @@ LocalEffectiveDimension """ -from .circuit_qnn import CircuitQNN from .effective_dimension import EffectiveDimension, LocalEffectiveDimension from .estimator_qnn import EstimatorQNN from .neural_network import NeuralNetwork -from .opflow_qnn import OpflowQNN from .sampling_neural_network import SamplingNeuralNetwork -from .two_layer_qnn import TwoLayerQNN from .sampler_qnn import SamplerQNN __all__ = [ "NeuralNetwork", - "OpflowQNN", - "TwoLayerQNN", "SamplingNeuralNetwork", - "CircuitQNN", "EffectiveDimension", "LocalEffectiveDimension", "EstimatorQNN", diff --git a/qiskit_machine_learning/neural_networks/circuit_qnn.py b/qiskit_machine_learning/neural_networks/circuit_qnn.py deleted file mode 100644 index a199e325a..000000000 --- a/qiskit_machine_learning/neural_networks/circuit_qnn.py +++ /dev/null @@ -1,544 +0,0 @@ -# This code is part of a Qiskit project. -# -# (C) Copyright IBM 2020, 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 -# 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 Sampling Neural Network based on a given quantum circuit.""" -import logging -from numbers import Integral -from typing import Tuple, Union, List, Callable, Optional, cast, Iterable - -import numpy as np - -from scipy.sparse import coo_matrix - -from qiskit import QuantumCircuit -from qiskit.circuit import Parameter -from qiskit.opflow import Gradient, CircuitSampler, StateFn, OpflowError, OperatorBase -from qiskit.providers import Backend -from qiskit.utils import QuantumInstance, deprecate_func - -import qiskit_machine_learning.optionals as _optionals -from .sampling_neural_network import SamplingNeuralNetwork -from ..exceptions import QiskitMachineLearningError, QiskitError - -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__) - - -class CircuitQNN(SamplingNeuralNetwork): - """Deprecated: A sampling neural network based on a given quantum circuit.""" - - @deprecate_func( - since="0.7.0", - additional_msg="Instead, use the ``qiskit_machine_learning.neural_networks.SamplerQNN``" - " class.", - package_name="qiskit-machine-learning", - ) - def __init__( - self, - circuit: QuantumCircuit, - input_params: Optional[List[Parameter]] = None, - weight_params: Optional[List[Parameter]] = None, - sparse: bool = False, - sampling: bool = False, - interpret: Optional[Callable[[int], Union[int, Tuple[int, ...]]]] = None, - output_shape: Union[int, Tuple[int, ...]] = None, - gradient: Gradient = None, - quantum_instance: Optional[Union[QuantumInstance, Backend]] = None, - input_gradients: bool = False, - ) -> None: - """ - Args: - circuit: The parametrized quantum circuit that generates the samples of this network. - There will be an attempt to transpile this circuit and cache the transpiled circuit - for subsequent usages by the network. If for some reasons the circuit can't be - transpiled, e.g. it originates from - :class:`~qiskit_machine_learning.circuit.library.RawFeatureVector`, the circuit - will be transpiled every time it is required to be executed and only when all - parameters are bound. This may impact overall performance on the 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. - sampling: Determines whether the network returns a batch of samples or (possibly - sparse) array of probabilities in its forward pass. In case of probabilities, - the backward pass returns the probability gradients, while it returns - ``(None, None)`` in the case of samples. Note that ``sampling==True`` will always - result in a dense return array independent of the other settings. - 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 this is used, and ``sampling==False``, the output shape of - the output needs to be given as a separate argument. 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, only used in the case - where an interpret function is provided and ``sampling==False``. Note that in the - remaining cases, the output shape is automatically inferred by: ``2^num_qubits`` if - ``sampling==False`` and ``interpret==None``, ``(num_samples,1)`` - if ``sampling==True`` and ``interpret==None``, and - ``(num_samples, interpret_shape)`` if ``sampling==True`` and an interpret function - is provided. - gradient: The gradient converter to be used for the probability gradients. - quantum_instance: The quantum instance to evaluate the circuits. Note that - if ``sampling==True``, a statevector simulator is not a valid backend for the - quantum instance. - 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: if ``interpret`` is passed without ``output_shape``. - - """ - self._gradient_circuit_constructed: bool = False - self._input_params = list(input_params or []) - self._weight_params = list(weight_params or []) - - # initialize gradient properties - self.input_gradients = input_gradients - - sparse = False if sampling else sparse - if sparse: - _optionals.HAS_SPARSE.require_now("DOK") - - # copy circuit and add measurements in case non are given - # TODO: need to be able to handle partial measurements! (partial trace...) - self._circuit = circuit.copy() - # we have not transpiled the circuit yet - self._circuit_transpiled = False - # these original values may be re-used when a quantum instance is set, - # but initially it was None - self._original_output_shape = output_shape - # next line is required by pylint only - self._interpret = interpret - self._original_interpret = interpret - - # we need this property in _set_quantum_instance despite it is initialized - # in the super class later on, review of SamplingNN is required. - self._sampling = sampling - - # set quantum instance and derive target output_shape and interpret - self._set_quantum_instance(quantum_instance, output_shape, interpret) - - # init super class - super().__init__( - len(self._input_params), - len(self._weight_params), - sparse, - sampling, - self._output_shape, - self._input_gradients, - ) - - self._original_circuit = circuit - # use given gradient or default - self._gradient = gradient or Gradient() - - def _construct_gradient_circuit(self) -> None: - if self._gradient_circuit_constructed: - return - - self._gradient_circuit: OperatorBase = None - try: - # todo: avoid copying the circuit - grad_circuit = self._original_circuit.copy() - grad_circuit.remove_final_measurements() # ideally this would not be necessary - if self._input_gradients: - params = self._input_params + self._weight_params - else: - params = self._weight_params - self._gradient_circuit = self._gradient.convert(StateFn(grad_circuit), params) - except (ValueError, TypeError, OpflowError, QiskitError): - logger.warning("Cannot compute gradient operator! Continuing without gradients!") - - self._gradient_circuit_constructed = True - - def _compute_output_shape(self, interpret, output_shape, sampling) -> Tuple[int, ...]: - """Validate and compute the output shape.""" - # a safety check cause we use quantum instance below - if self._quantum_instance is None: - raise QiskitMachineLearningError( - "A quantum instance is required to compute output shape!" - ) - - # this definition is required by mypy - output_shape_: Tuple[int, ...] = (-1,) - # todo: move sampling code to the super class - if sampling: - if output_shape is not None: - # Warn user that output_shape parameter will be ignored - logger.warning( - "In sampling mode, output_shape will be automatically inferred " - "from the number of samples and interpret function, if provided." - ) - - num_samples = self._quantum_instance.run_config.shots - if interpret is not None: - ret = interpret(0) # infer shape from function - result = np.array(ret) - if len(result.shape) == 0: - output_shape_ = (num_samples, 1) - else: - output_shape_ = (num_samples, *result.shape) - else: - output_shape_ = (num_samples, 1) - else: - 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_ - - @property - def circuit(self) -> QuantumCircuit: - """Returns the underlying quantum circuit.""" - return self._circuit - - @property - def input_params(self) -> List: - """Returns the list of input parameters.""" - return self._input_params - - @property - def weight_params(self) -> List: - """Returns the list of trainable weights parameters.""" - return self._weight_params - - @property - def interpret(self) -> Optional[Callable[[int], Union[int, Tuple[int, ...]]]]: - """Returns interpret function to be used by the neural network. If it is not set in - the constructor or can not be implicitly derived (e.g. a quantum instance is not provided), - then ``None`` is returned.""" - return self._interpret - - @property - def quantum_instance(self) -> QuantumInstance: - """Returns the quantum instance to evaluate the circuit.""" - return self._quantum_instance - - @quantum_instance.setter - def quantum_instance(self, quantum_instance: Optional[Union[QuantumInstance, Backend]]) -> None: - """Sets the quantum instance to evaluate the circuit and make sure circuit has - measurements or not depending on the type of backend used. - """ - self._set_quantum_instance( - quantum_instance, self._original_output_shape, self._original_interpret - ) - - def _set_quantum_instance( - self, - quantum_instance: Optional[Union[QuantumInstance, Backend]], - output_shape: Union[int, Tuple[int, ...]], - interpret: Optional[Callable[[int], Union[int, Tuple[int, ...]]]], - ) -> None: - """ - Internal method to set a quantum instance and compute/initialize internal properties such - as an interpret function, output shape and a sampler. - - Args: - quantum_instance: A quantum instance to set. - output_shape: An output shape of the custom interpretation. - interpret: A callable that maps the measured integer to another unsigned integer or - tuple of unsigned integers. - """ - if isinstance(quantum_instance, Backend): - quantum_instance = QuantumInstance(quantum_instance) - self._quantum_instance = quantum_instance - - if self._quantum_instance is not None: - # add measurements in case none are given - if self._quantum_instance.is_statevector: - if len(self._circuit.clbits) > 0: - self._circuit.remove_final_measurements() - elif len(self._circuit.clbits) == 0: - self._circuit.measure_all() - - # set interpret and compute output shape - self.set_interpret(interpret, output_shape) - - # prepare sampler - self._sampler = CircuitSampler(self._quantum_instance, param_qobj=False, caching="all") - - # transpile the QNN circuit - try: - self._circuit = self._quantum_instance.transpile( - self._circuit, pass_manager=self._quantum_instance.unbound_pass_manager - )[0] - self._circuit_transpiled = True - except QiskitError: - # likely it is caused by RawFeatureVector, we just ignore this error and - # transpile circuits when it is required. - self._circuit_transpiled = False - else: - self._output_shape = output_shape - - @property - def input_gradients(self) -> bool: - """Returns whether gradients with respect to input data are computed by this neural network - in the ``backward`` method or not. By default such gradients are not computed.""" - return self._input_gradients - - @input_gradients.setter - def input_gradients(self, input_gradients: bool) -> None: - """Turn on/off gradient with respect to input data.""" - self._input_gradients = input_gradients - - # reset gradient circuit - self._gradient_circuit = None - self._gradient_circuit_constructed = False - - 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._sampling) - self._interpret = interpret if interpret is not None else lambda x: x - - def _sample( - self, input_data: Optional[np.ndarray], weights: Optional[np.ndarray] - ) -> np.ndarray: - self._check_quantum_instance("samples") - - if self._quantum_instance.is_statevector: - raise QiskitMachineLearningError("Sampling does not work with statevector simulator!") - - # evaluate operator - orig_memory = self._quantum_instance.backend_options.get("memory") - self._quantum_instance.backend_options["memory"] = True - - circuits = [] - # iterate over samples, each sample is an element of a batch - num_samples = input_data.shape[0] - for i in range(num_samples): - param_values = { - input_param: input_data[i, j] for j, input_param in enumerate(self._input_params) - } - param_values.update( - {weight_param: weights[j] for j, weight_param in enumerate(self._weight_params)} - ) - circuits.append(self._circuit.bind_parameters(param_values)) - - if self._quantum_instance.bound_pass_manager is not None: - circuits = self._quantum_instance.transpile( - circuits, pass_manager=self._quantum_instance.bound_pass_manager - ) - - result = self._quantum_instance.execute(circuits, had_transpiled=self._circuit_transpiled) - # set the memory setting back - self._quantum_instance.backend_options["memory"] = orig_memory - - # return samples - samples = np.zeros((num_samples, *self._output_shape)) - # collect them from all executed circuits - for i, circuit in enumerate(circuits): - memory = result.get_memory(circuit) - for j, b in enumerate(memory): - samples[i, j, :] = self._interpret(int(b, 2)) - return samples - - def _probabilities( - self, input_data: Optional[np.ndarray], weights: Optional[np.ndarray] - ) -> Union[np.ndarray, SparseArray]: - self._check_quantum_instance("probabilities") - - # evaluate operator - circuits = [] - num_samples = input_data.shape[0] - for i in range(num_samples): - param_values = { - input_param: input_data[i, j] for j, input_param in enumerate(self._input_params) - } - param_values.update( - {weight_param: weights[j] for j, weight_param in enumerate(self._weight_params)} - ) - circuits.append(self._circuit.bind_parameters(param_values)) - - if self._quantum_instance.bound_pass_manager is not None: - circuits = self._quantum_instance.transpile( - circuits, pass_manager=self._quantum_instance.bound_pass_manager - ) - - result = self._quantum_instance.execute(circuits, had_transpiled=self._circuit_transpiled) - # initialize probabilities - 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, circuit in enumerate(circuits): - counts = result.get_counts(circuit) - shots = sum(counts.values()) - - # evaluate probabilities - for b, v in counts.items(): - key = self._interpret(int(b, 2)) - if isinstance(key, Integral): - key = (cast(int, key),) - key = (i, *key) # type: ignore - prob[key] += v / shots - - if self._sparse: - return prob.to_coo() - else: - return prob - - def _probability_gradients( - self, input_data: Optional[np.ndarray], weights: Optional[np.ndarray] - ) -> Tuple[Union[np.ndarray, SparseArray], Union[np.ndarray, SparseArray]]: - self._check_quantum_instance("probability gradients") - - self._construct_gradient_circuit() - - # check whether gradient circuit could be constructed - if self._gradient_circuit is None: - return None, None - - num_samples = input_data.shape[0] - - # initialize empty gradients - input_grad = None # by default we don't have data gradients - if self._sparse: - # pylint: disable=import-error - from sparse import DOK - - if self._input_gradients: - input_grad = DOK((num_samples, *self._output_shape, self._num_inputs)) - weights_grad = DOK((num_samples, *self._output_shape, self._num_weights)) - else: - if self._input_gradients: - input_grad = np.zeros((num_samples, *self._output_shape, self._num_inputs)) - weights_grad = np.zeros((num_samples, *self._output_shape, self._num_weights)) - - param_values = { - input_param: input_data[:, j] for j, input_param in enumerate(self._input_params) - } - param_values.update( - { - weight_param: np.full(num_samples, weights[j]) - for j, weight_param in enumerate(self._weight_params) - } - ) - - converted_op = self._sampler.convert(self._gradient_circuit, param_values) - # if statement is a workaround for https://github.com/Qiskit/qiskit-terra/issues/7608 - if len(converted_op.parameters) > 0: - # create an list of parameter bindings, each element corresponds to a sample in the dataset - param_bindings = [ - {param: param_values[i] for param, param_values in param_values.items()} - for i in range(num_samples) - ] - - grad = [] - # iterate over gradient vectors and bind the correct leftover parameters - for g_i, param_i in zip(converted_op, param_bindings): - # bind or re-bind remaining values and evaluate the gradient - grad.append(g_i.bind_parameters(param_i).eval()) - else: - grad = converted_op.eval() - - if self._input_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): - coo_grad = coo_matrix(grad[sample][i]) # this works for sparse and dense case - - # 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 - - for _, k, val in zip(coo_grad.row, coo_grad.col, coo_grad.data): - # 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) - # end of for each sample - - 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 _check_quantum_instance(self, feature: str): - if self._quantum_instance is None: - raise QiskitMachineLearningError( - f"Evaluation of {feature} requires a quantum instance!" - ) diff --git a/qiskit_machine_learning/neural_networks/effective_dimension.py b/qiskit_machine_learning/neural_networks/effective_dimension.py index 2415ff8f2..8e869d8bb 100644 --- a/qiskit_machine_learning/neural_networks/effective_dimension.py +++ b/qiskit_machine_learning/neural_networks/effective_dimension.py @@ -21,7 +21,6 @@ from qiskit.utils import algorithm_globals from qiskit_machine_learning import QiskitMachineLearningError from .estimator_qnn import EstimatorQNN -from .opflow_qnn import OpflowQNN from .neural_network import NeuralNetwork logger = logging.getLogger(__name__) @@ -173,9 +172,9 @@ def run_monte_carlo(self) -> Tuple[np.ndarray, np.ndarray]: grads[self._num_input_samples * i : self._num_input_samples * (i + 1)] = backward_pass outputs[self._num_input_samples * i : self._num_input_samples * (i + 1)] = forward_pass - # post-processing in the case of OpflowQNN and EstimatorQNN output, to match - # the CircuitQNN output format - if isinstance(self._model, (OpflowQNN, EstimatorQNN)): + # post-processing in the case of EstimatorQNN output, to match + # the SamplerQNN output format + if isinstance(self._model, EstimatorQNN): grads = np.concatenate([grads / 2, -1 * grads / 2], 1) outputs = np.concatenate([(outputs + 1) / 2, (1 - outputs) / 2], 1) diff --git a/qiskit_machine_learning/neural_networks/estimator_qnn.py b/qiskit_machine_learning/neural_networks/estimator_qnn.py index 4b319673c..85d00d9c8 100644 --- a/qiskit_machine_learning/neural_networks/estimator_qnn.py +++ b/qiskit_machine_learning/neural_networks/estimator_qnn.py @@ -25,7 +25,6 @@ ParamShiftEstimatorGradient, ) from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.opflow import PauliSumOp from qiskit.primitives import BaseEstimator, Estimator, EstimatorResult from qiskit.quantum_info import SparsePauliOp from qiskit.quantum_info.operators.base_operator import BaseOperator @@ -107,7 +106,7 @@ def __init__( *, circuit: QuantumCircuit, estimator: BaseEstimator | None = None, - observables: Sequence[BaseOperator | PauliSumOp] | BaseOperator | PauliSumOp | None = None, + observables: Sequence[BaseOperator] | BaseOperator | None = None, input_params: Sequence[Parameter] | None = None, weight_params: Sequence[Parameter] | None = None, gradient: BaseEstimatorGradient | None = None, @@ -152,7 +151,7 @@ def __init__( self._circuit = circuit if observables is None: observables = SparsePauliOp.from_list([("Z" * circuit.num_qubits, 1)]) - if isinstance(observables, (PauliSumOp, BaseOperator)): + if isinstance(observables, BaseOperator): observables = (observables,) self._observables = observables if isinstance(circuit, QNNCircuit): @@ -180,7 +179,7 @@ def circuit(self) -> QuantumCircuit: return copy(self._circuit) @property - def observables(self) -> Sequence[BaseOperator | PauliSumOp] | BaseOperator | PauliSumOp: + def observables(self) -> Sequence[BaseOperator] | BaseOperator: """Returns the underlying observables of this QNN.""" return copy(self._observables) diff --git a/qiskit_machine_learning/neural_networks/opflow_qnn.py b/qiskit_machine_learning/neural_networks/opflow_qnn.py deleted file mode 100644 index 198a2d378..000000000 --- a/qiskit_machine_learning/neural_networks/opflow_qnn.py +++ /dev/null @@ -1,320 +0,0 @@ -# This code is part of a Qiskit project. -# -# (C) Copyright IBM 2020, 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 -# 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. - -"""An Opflow Quantum Neural Network that allows to use a parametrized opflow object as a -neural network.""" -import logging -from typing import List, Optional, Union, Tuple, Dict - -import numpy as np -from qiskit.circuit import Parameter -from qiskit.opflow import ( - Gradient, - CircuitSampler, - ListOp, - OperatorBase, - ExpectationBase, - OpflowError, - ComposedOp, -) -from qiskit.providers import Backend -from qiskit.utils import QuantumInstance, deprecate_func -from qiskit.utils.backend_utils import is_aer_provider -import qiskit_machine_learning.optionals as _optionals -from .neural_network import NeuralNetwork -from ..exceptions import QiskitMachineLearningError, QiskitError - -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__) - - -class OpflowQNN(NeuralNetwork): - """Deprecated: Opflow Quantum Neural Network.""" - - @deprecate_func( - since="0.7.0", - additional_msg="Instead, use the ``qiskit_machine_learning.neural_networks.EstimatorQNN``" - " class.", - package_name="qiskit-machine-learning", - ) - def __init__( - self, - operator: OperatorBase, - input_params: Optional[List[Parameter]] = None, - weight_params: Optional[List[Parameter]] = None, - exp_val: Optional[ExpectationBase] = None, - gradient: Optional[Gradient] = None, - quantum_instance: Optional[Union[QuantumInstance, Backend]] = None, - input_gradients: bool = False, - ): - """ - Args: - operator: The parametrized operator that represents the neural network. - input_params: The operator parameters that correspond to the input of the network. - weight_params: The operator parameters that correspond to the trainable weights. - exp_val: The Expected Value converter to be used for the operator. - gradient: The Gradient converter to be used for the operator's backward pass. - quantum_instance: The quantum instance to evaluate the network. - 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``. - """ - self._gradient_operator_constructed: bool = False - self._input_params = list(input_params) or [] - self._weight_params = list(weight_params) or [] - self._set_quantum_instance(quantum_instance) - self._operator = operator - self._forward_operator = exp_val.convert(operator) if exp_val else operator - self._gradient = gradient - - # initialize gradient properties - self.input_gradients = input_gradients - - output_shape = self._compute_output_shape(operator) - super().__init__( - len(self._input_params), - len(self._weight_params), - sparse=False, - output_shape=output_shape, - input_gradients=input_gradients, - ) - - def _construct_gradient_operator(self) -> None: - if self._gradient_operator_constructed: - return - - self._gradient_operator: OperatorBase = None - try: - gradient = self._gradient or Gradient() - if self._input_gradients: - params = self._input_params + self._weight_params - else: - params = self._weight_params - - self._gradient_operator = gradient.convert(self._operator, params) - except (ValueError, TypeError, OpflowError, QiskitError): - logger.warning("Cannot compute gradient operator! Continuing without gradients!") - - self._gradient_operator_constructed = True - - def _compute_output_shape(self, op: OperatorBase) -> Tuple[int, ...]: - """Determines the output shape of a given operator.""" - # TODO: the whole method should eventually be moved to opflow and rewritten in a better way. - # if the operator is a composed one, then we only need to look at the first element of it. - if isinstance(op, ComposedOp): - return self._compute_output_shape(op.oplist[0].primitive) - # this "if" statement is on purpose, to prevent sub-classes. - # pylint:disable=unidiomatic-typecheck - if type(op) == ListOp: - shapes = [self._compute_output_shape(op_) for op_ in op.oplist] - if not np.all([shape == shapes[0] for shape in shapes]): - raise QiskitMachineLearningError( - "Only supports ListOps with children that return the same shape." - ) - if shapes[0] == (1,): - out = op.combo_fn(np.zeros((len(op.oplist)))) - else: - out = op.combo_fn(np.zeros((len(op.oplist), *shapes[0]))) - return out.shape - else: - return (1,) - - @property - def operator(self): - """Returns the underlying operator of this QNN.""" - return self._operator - - @property - def input_gradients(self) -> bool: - """Returns whether gradients with respect to input data are computed by this neural network - in the ``backward`` method or not. By default such gradients are not computed.""" - return self._input_gradients - - @input_gradients.setter - def input_gradients(self, input_gradients: bool) -> None: - """Turn on/off computation of gradients with respect to input data.""" - self._input_gradients = input_gradients - - # reset gradient operator - self._gradient_operator = None - self._gradient_operator_constructed = False - - @property - def quantum_instance(self) -> QuantumInstance: - """Returns the quantum instance to evaluate the operator.""" - return self._quantum_instance - - @quantum_instance.setter - def quantum_instance(self, quantum_instance: Optional[Union[QuantumInstance, Backend]]) -> None: - """Sets the quantum instance to evaluate the operator.""" - self._set_quantum_instance(quantum_instance) - - def _set_quantum_instance( - self, quantum_instance: Optional[Union[QuantumInstance, Backend]] - ) -> None: - """ - Internal method to set a quantum instance and compute/initialize a sampler. - - Args: - quantum_instance: A quantum instance to set. - - Returns: - None. - """ - - if isinstance(quantum_instance, Backend): - quantum_instance = QuantumInstance(quantum_instance) - self._quantum_instance = quantum_instance - - if quantum_instance is not None: - self._circuit_sampler = CircuitSampler( - self._quantum_instance, - param_qobj=is_aer_provider(self._quantum_instance.backend), - caching="all", - ) - else: - self._circuit_sampler = None - - def _forward( - self, input_data: Optional[np.ndarray], weights: Optional[np.ndarray] - ) -> Union[np.ndarray, SparseArray]: - # combine parameter dictionary - # take i-th column as values for the i-th param in a batch - param_values = {p: input_data[:, i].tolist() for i, p in enumerate(self._input_params)} - param_values.update( - {p: [weights[i]] * input_data.shape[0] for i, p in enumerate(self._weight_params)} - ) - - # evaluate operator - if self._circuit_sampler: - op = self._circuit_sampler.convert(self._forward_operator, param_values) - result = np.real(op.eval()) - else: - op = self._forward_operator.bind_parameters(param_values) - result = np.real(op.eval()) - result = np.array(result) - return result.reshape(-1, *self._output_shape) - - def _backward( - self, input_data: Optional[np.ndarray], weights: Optional[np.ndarray] - ) -> Tuple[Optional[Union[np.ndarray, SparseArray]], Optional[Union[np.ndarray, SparseArray]],]: - - self._construct_gradient_operator() - - # check whether gradient circuit could be constructed - if self._gradient_operator is None: - return None, None - - num_samples = input_data.shape[0] - if self._input_gradients: - num_params = self._num_inputs + self._num_weights - else: - num_params = self._num_weights - - param_values = { - input_param: input_data[:, j] for j, input_param in enumerate(self._input_params) - } - param_values.update( - { - weight_param: np.full(num_samples, weights[j]) - for j, weight_param in enumerate(self._weight_params) - } - ) - - if self._circuit_sampler: - converted_op = self._circuit_sampler.convert(self._gradient_operator, param_values) - # if statement is a workaround for https://github.com/Qiskit/qiskit-terra/issues/7608 - if len(converted_op.parameters) > 0: - # rebind the leftover parameters and evaluate the gradient - grad = self._evaluate_operator(converted_op, num_samples, param_values) - else: - # all parameters are bound by CircuitSampler, so we evaluate the operator directly - grad = np.asarray(converted_op.eval()) - else: - # we evaluate gradient operator for each sample separately, so we create a list of operators. - grad = self._evaluate_operator( - [self._gradient_operator] * num_samples, num_samples, param_values - ) - - grad = np.real(grad) - - # this is another workaround to fix output shape of the invocation result of CircuitSampler - if self._output_shape == (1,): - # at least 3 dimensions: batch, output, num_parameters, but in this case we don't have - # output dimension, so we add a dimension that corresponds to the output - grad = grad.reshape((num_samples, 1, num_params)) - else: - # swap last axis that corresponds to parameters and axes correspond to the output shape - last_axis = len(grad.shape) - 1 - grad = grad.transpose([0, last_axis, *(range(1, last_axis))]) - - # split into and return input and weights gradients - if self._input_gradients: - input_grad = grad[:, :, : self._num_inputs].reshape( - -1, *self._output_shape, self._num_inputs - ) - - weights_grad = grad[:, :, self._num_inputs :].reshape( - -1, *self._output_shape, self._num_weights - ) - else: - input_grad = None - weights_grad = grad.reshape(-1, *self._output_shape, self._num_weights) - - return input_grad, weights_grad - - def _evaluate_operator( - self, - operator: Union[OperatorBase, List[OperatorBase]], - num_samples: int, - param_values: Dict[Parameter, np.ndarray], - ) -> np.ndarray: - """ - Evaluates an operator or a list of operators for the samples in the dataset. If an operator - is passed then it is considered as an iterable that has `num_samples` elements. Usually such - operators are obtained as an output from `CircuitSampler`. If a list of operators is passed - then each operator in this list is evaluated with a set of values/parameters corresponding - to the sample index in the `param_values` as the operator in the list. - - Args: - operator: operator or list of operators to evaluate. - num_samples: a total number of samples - param_values: parameter values to use for operator evaluation. - - Returns: - the result of operator evaluation as an array. - """ - # create an list of parameter bindings, each element corresponds to a sample in the dataset - param_bindings = [ - {param: param_values[i] for param, param_values in param_values.items()} - for i in range(num_samples) - ] - - grad = [] - # iterate over gradient vectors and bind the correct parameters - for oper_i, param_i in zip(operator, param_bindings): - # bind or re-bind remaining values and evaluate the gradient - grad.append(oper_i.bind_parameters(param_i).eval()) - - return np.asarray(grad) diff --git a/qiskit_machine_learning/neural_networks/two_layer_qnn.py b/qiskit_machine_learning/neural_networks/two_layer_qnn.py deleted file mode 100644 index 828a182b4..000000000 --- a/qiskit_machine_learning/neural_networks/two_layer_qnn.py +++ /dev/null @@ -1,124 +0,0 @@ -# This code is part of a Qiskit project. -# -# (C) Copyright IBM 2020, 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 -# 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 Two Layer Neural Network consisting of a first parametrized circuit representing a feature map -to map the input data to a quantum states and a second one representing a ansatz that can -be trained to solve a particular tasks.""" -from __future__ import annotations - -from qiskit import QuantumCircuit -from qiskit.opflow import PauliSumOp, StateFn, OperatorBase, ExpectationBase -from qiskit.providers import Backend -from qiskit.utils import QuantumInstance, deprecate_func - -from .opflow_qnn import OpflowQNN -from ..utils import derive_num_qubits_feature_map_ansatz - - -class TwoLayerQNN(OpflowQNN): - """Deprecated: Two Layer Quantum Neural Network consisting of a feature map, a ansatz, - and an observable. - """ - - @deprecate_func( - since="0.7.0", - additional_msg="Instead, use the ``qiskit_machine_learning.neural_networks.EstimatorQNN``" - " class.", - package_name="qiskit-machine-learning", - ) - def __init__( - self, - num_qubits: int | None = None, - feature_map: QuantumCircuit | None = None, - ansatz: QuantumCircuit | None = None, - observable: OperatorBase | QuantumCircuit | None = None, - exp_val: ExpectationBase | None = None, - quantum_instance: QuantumInstance | Backend | None = None, - input_gradients: bool = False, - ): - r""" - Args: - num_qubits: The number of qubits to represent the network. 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. If ``None`` is given, - the ``ZZFeatureMap`` is used if the number of qubits is larger than 1. For - a single qubit two-layer QNN the ``ZFeatureMap`` circuit is used per default. - ansatz: The (parametrized) circuit to be used as an ansatz. If ``None`` is given, - the ``RealAmplitudes`` circuit is used. - observable: observable to be measured to determine the output of the network. If - ``None`` is given, the :math:`Z^{\otimes num\_qubits}` observable is used. - exp_val: The Expected Value converter to be used for the operator obtained from the - feature map and ansatz. - quantum_instance: The quantum instance to evaluate the network. - 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 - :class:`~qiskit_machine_learning.connectors.TorchConnector`. - 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 - can't be adjusted to ``num_qubits``. - """ - - num_qubits, feature_map, ansatz = derive_num_qubits_feature_map_ansatz( - num_qubits, feature_map, ansatz - ) - - self._feature_map = feature_map - input_params = list(self._feature_map.parameters) - - self._ansatz = ansatz - weight_params = list(self._ansatz.parameters) - - # construct circuit - self._circuit = QuantumCircuit(num_qubits) - self._circuit.append(self._feature_map, range(num_qubits)) - self._circuit.append(self._ansatz, range(num_qubits)) - - # construct observable - self.observable = ( - observable if observable is not None else PauliSumOp.from_list([("Z" * num_qubits, 1)]) - ) - - # combine all to operator - operator = StateFn(self.observable, is_measurement=True) @ StateFn(self._circuit) - - super().__init__( - operator=operator, - input_params=input_params, - weight_params=weight_params, - exp_val=exp_val, - quantum_instance=quantum_instance, - input_gradients=input_gradients, - ) - - @property - def feature_map(self) -> QuantumCircuit: - """Returns the used feature map.""" - return self._feature_map - - @property - def ansatz(self) -> QuantumCircuit: - """Returns the used ansatz.""" - return self._ansatz - - @property - def circuit(self) -> QuantumCircuit: - """Returns the underlying quantum circuit.""" - return self._circuit - - @property - def num_qubits(self) -> int: - """Returns the number of qubits used by ansatz and feature map.""" - return self._circuit.num_qubits diff --git a/test/algorithms/classifiers/test_neural_network_classifier.py b/test/algorithms/classifiers/test_neural_network_classifier.py index 1f860b4d8..3a30d4d7a 100644 --- a/test/algorithms/classifiers/test_neural_network_classifier.py +++ b/test/algorithms/classifiers/test_neural_network_classifier.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2018, 2023. +# (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 @@ -10,13 +10,14 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. """ Test Neural Network Classifier """ +from __future__ import annotations + import itertools import os import tempfile import unittest -import warnings from functools import partial -from typing import Tuple, Optional, Callable +from typing import Callable from test import QiskitMachineLearningTestCase @@ -26,18 +27,17 @@ from qiskit.algorithms.optimizers import COBYLA, L_BFGS_B, SPSA, Optimizer from qiskit.circuit import QuantumCircuit from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap -from qiskit.utils import QuantumInstance, algorithm_globals, optionals +from qiskit.utils import algorithm_globals, optionals from scipy.optimize import minimize from qiskit_machine_learning.algorithms import SerializableModelMixin from qiskit_machine_learning.algorithms.classifiers import NeuralNetworkClassifier from qiskit_machine_learning.exceptions import QiskitMachineLearningError -from qiskit_machine_learning.neural_networks import TwoLayerQNN, CircuitQNN, NeuralNetwork +from qiskit_machine_learning.neural_networks import NeuralNetwork, EstimatorQNN, SamplerQNN from qiskit_machine_learning.utils.loss_functions import CrossEntropyLoss OPTIMIZERS = ["cobyla", "bfgs", "callable", None] L1L2_ERRORS = ["absolute_error", "squared_error"] -QUANTUM_INSTANCES = ["statevector", "qasm"] CALLBACKS = [True, False] @@ -54,52 +54,22 @@ class TestNeuralNetworkClassifier(QiskitMachineLearningTestCase): @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required to run this test") def setUp(self): super().setUp() - warnings.filterwarnings("ignore", category=DeprecationWarning) # specify quantum instances algorithm_globals.random_seed = 12345 - from qiskit_aer import Aer - - self.sv_quantum_instance = QuantumInstance( - Aer.get_backend("aer_simulator_statevector"), - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) - self.qasm_quantum_instance = QuantumInstance( - Aer.get_backend("aer_simulator"), - shots=100, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) - - def tearDown(self) -> None: - super().tearDown() - warnings.filterwarnings("always", category=DeprecationWarning) - # enable the warnings if they were disabled for COBYLA - warnings.filterwarnings("always", category=RuntimeWarning) - def _create_optimizer(self, opt: str) -> Optional[Optimizer]: + def _create_optimizer(self, opt: str) -> Optimizer | None: if opt == "bfgs": optimizer = L_BFGS_B(maxiter=5) elif opt == "cobyla": optimizer = COBYLA(maxiter=25) elif opt == "callable": - # COBYLA raises a warning in the callable case - warnings.filterwarnings("ignore", category=RuntimeWarning) optimizer = partial(minimize, method="COBYLA", options={"maxiter": 25}) else: optimizer = None return optimizer - def _create_quantum_instance(self, q_i: str) -> QuantumInstance: - if q_i == "statevector": - quantum_instance = self.sv_quantum_instance - else: - quantum_instance = self.qasm_quantum_instance - - return quantum_instance - def _create_callback(self, cb_flag): if cb_flag: history = {"weights": [], "values": []} @@ -113,18 +83,25 @@ def callback(objective_weights, objective_value): callback = None return callback, history - @idata(itertools.product(OPTIMIZERS, L1L2_ERRORS, QUANTUM_INSTANCES, CALLBACKS)) + @idata(itertools.product(OPTIMIZERS, L1L2_ERRORS, CALLBACKS)) @unpack - def test_classifier_with_opflow_qnn(self, opt, loss, q_i, cb_flag): - """Test Neural Network Classifier with Opflow QNN (Two Layer QNN).""" + def test_classifier_with_estimator_qnn(self, opt, loss, cb_flag): + """Test Neural Network Classifier with Estimator QNN.""" - quantum_instance = self._create_quantum_instance(q_i) optimizer = self._create_optimizer(opt) callback, history = self._create_callback(cb_flag) num_inputs = 2 + feature_map = ZZFeatureMap(num_inputs) ansatz = RealAmplitudes(num_inputs, reps=1) - qnn = TwoLayerQNN(num_inputs, ansatz=ansatz, quantum_instance=quantum_instance) + + qc = QuantumCircuit(num_inputs) + qc.compose(feature_map, inplace=True) + qc.compose(ansatz, inplace=True) + + qnn = EstimatorQNN( + circuit=qc, input_params=feature_map.parameters, weight_params=ansatz.parameters + ) classifier = self._create_classifier(qnn, ansatz.num_parameters, optimizer, loss, callback) @@ -156,9 +133,7 @@ def _verify_callback_values(self, callback, history, num_weights): self.assertEqual(len(weights), num_weights) self.assertTrue(all(isinstance(weight, float) for weight in weights)) - def _create_circuit_qnn( - self, quantum_instance: QuantumInstance, output_shape=2 - ) -> Tuple[CircuitQNN, int, int]: + def _create_sampler_qnn(self, output_shape=2) -> tuple[SamplerQNN, int, int]: num_inputs = 2 feature_map = ZZFeatureMap(num_inputs) ansatz = RealAmplitudes(num_inputs, reps=1) @@ -172,19 +147,18 @@ def _create_circuit_qnn( def parity(x): return f"{x:b}".count("1") % 2 - qnn = CircuitQNN( - qc, + qnn = SamplerQNN( + circuit=qc, input_params=feature_map.parameters, weight_params=ansatz.parameters, sparse=False, interpret=parity, output_shape=output_shape, - quantum_instance=quantum_instance, ) return qnn, num_inputs, ansatz.num_parameters - def _generate_data(self, num_inputs: int) -> Tuple[np.ndarray, np.ndarray]: + def _generate_data(self, num_inputs: int) -> tuple[np.ndarray, np.ndarray]: # construct data num_samples = 6 features = algorithm_globals.random.random((num_samples, num_inputs)) @@ -198,7 +172,7 @@ def _create_classifier( num_parameters: int, optimizer: Optimizer, loss: str, - callback: Optional[Callable[[np.ndarray, float], None]] = None, + callback: Callable[[np.ndarray, float], None] | None = None, one_hot: bool = False, ): initial_point = np.array([0.5] * num_parameters) @@ -214,16 +188,15 @@ def _create_classifier( ) return classifier - @idata(itertools.product(OPTIMIZERS, L1L2_ERRORS, QUANTUM_INSTANCES, CALLBACKS)) + @idata(itertools.product(OPTIMIZERS, L1L2_ERRORS, CALLBACKS)) @unpack - def test_classifier_with_circuit_qnn(self, opt, loss, q_i, cb_flag): - """Test Neural Network Classifier with Circuit QNN.""" + def test_classifier_with_sampler_qnn(self, opt, loss, cb_flag): + """Test Neural Network Classifier with SamplerQNN.""" - quantum_instance = self._create_quantum_instance(q_i) optimizer = self._create_optimizer(opt) callback, history = self._create_callback(cb_flag) - qnn, num_inputs, num_parameters = self._create_circuit_qnn(quantum_instance) + qnn, num_inputs, num_parameters = self._create_sampler_qnn() classifier = self._create_classifier(qnn, num_parameters, optimizer, loss, callback) @@ -243,14 +216,12 @@ def test_classifier_with_circuit_qnn(self, opt, loss, q_i, cb_flag): np.testing.assert_array_equal(classifier.fit_result.x, classifier.weights) self.assertEqual(len(classifier.weights), num_parameters) - @idata(itertools.product(OPTIMIZERS, QUANTUM_INSTANCES)) - @unpack - def test_classifier_with_circuit_qnn_and_cross_entropy(self, opt, q_i): + @idata(OPTIMIZERS) + def test_classifier_with_sampler_qnn_and_cross_entropy(self, opt): """Test Neural Network Classifier with Circuit QNN and Cross Entropy loss.""" - quantum_instance = self._create_quantum_instance(q_i) optimizer = self._create_optimizer(opt) - qnn, num_inputs, num_parameters = self._create_circuit_qnn(quantum_instance) + qnn, num_inputs, num_parameters = self._create_sampler_qnn() loss = CrossEntropyLoss() classifier = self._create_classifier(qnn, num_parameters, optimizer, loss, one_hot=True) @@ -278,10 +249,9 @@ def test_categorical_data(self, config): one_hot, loss = config - quantum_instance = self.sv_quantum_instance optimizer = L_BFGS_B(maxiter=5) - qnn, num_inputs, num_parameters = self._create_circuit_qnn(quantum_instance) + qnn, num_inputs, num_parameters = self._create_sampler_qnn() classifier = self._create_classifier(qnn, num_parameters, optimizer, loss, one_hot=one_hot) @@ -301,13 +271,11 @@ def test_categorical_data(self, config): predict = classifier.predict(features[0, :]) self.assertIn(predict, ["A", "B"]) - @idata(itertools.product(QUANTUM_INSTANCES, L1L2_ERRORS + ["cross_entropy"])) - @unpack - def test_sparse_arrays(self, q_i, loss): + @idata(L1L2_ERRORS + ["cross_entropy"]) + def test_sparse_arrays(self, loss): """Tests classifier with sparse arrays as features and labels.""" optimizer = L_BFGS_B(maxiter=5) - quantum_instance = self._create_quantum_instance(q_i) - qnn, _, num_parameters = self._create_circuit_qnn(quantum_instance) + qnn, _, num_parameters = self._create_sampler_qnn() classifier = self._create_classifier(qnn, num_parameters, optimizer, loss, one_hot=True) features = scipy.sparse.csr_matrix([[0, 0], [1, 1]]) @@ -320,23 +288,29 @@ def test_sparse_arrays(self, q_i, loss): score = classifier.score(features, labels) self.assertGreater(score, 0.5) - @idata(["opflow", "circuit_qnn"]) + @idata(["estimator_qnn", "sampler_qnn"]) def test_save_load(self, qnn_type): """Tests save and load models.""" features = np.array([[0, 0], [0.1, 0.2], [1, 1], [0.9, 0.8]]) - if qnn_type == "opflow": + if qnn_type == "estimator_qnn": labels = np.array([-1, -1, 1, 1]) num_qubits = 2 + feature_map = ZZFeatureMap(num_qubits) ansatz = RealAmplitudes(num_qubits, reps=1) - qnn = TwoLayerQNN( - num_qubits, ansatz=ansatz, quantum_instance=self.qasm_quantum_instance + qc = QuantumCircuit(num_qubits) + qc.compose(feature_map, inplace=True) + qc.compose(ansatz, inplace=True) + qnn = EstimatorQNN( + circuit=qc, + input_params=feature_map.parameters, + weight_params=ansatz.parameters, ) num_parameters = ansatz.num_parameters - elif qnn_type == "circuit_qnn": + elif qnn_type == "sampler_qnn": labels = np.array([0, 0, 1, 1]) - qnn, _, num_parameters = self._create_circuit_qnn(self.qasm_quantum_instance) + qnn, _, num_parameters = self._create_sampler_qnn() else: raise ValueError(f"Unsupported QNN type: {qnn_type}") @@ -373,7 +347,7 @@ def test_num_classes_data(self, one_hot): """Test the number of assumed classes for one-hot and not one-hot data.""" optimizer = L_BFGS_B(maxiter=5) - qnn, num_inputs, num_parameters = self._create_circuit_qnn(self.sv_quantum_instance) + qnn, num_inputs, num_parameters = self._create_sampler_qnn() features, labels = self._generate_data(num_inputs) if one_hot: @@ -399,9 +373,7 @@ def test_binary_classification_with_multiclass_data(self): """Test that trying to train a binary classifier with multiclass data raises an error.""" optimizer = L_BFGS_B(maxiter=5) - qnn, num_inputs, num_parameters = self._create_circuit_qnn( - self.sv_quantum_instance, output_shape=1 - ) + qnn, num_inputs, num_parameters = self._create_sampler_qnn(output_shape=1) classifier = self._create_classifier( qnn, num_parameters, @@ -421,9 +393,7 @@ def test_bad_binary_shape(self): """Test that trying to train a binary classifier with misshaped data raises an error.""" optimizer = L_BFGS_B(maxiter=5) - qnn, num_inputs, num_parameters = self._create_circuit_qnn( - self.sv_quantum_instance, output_shape=1 - ) + qnn, num_inputs, num_parameters = self._create_sampler_qnn(output_shape=1) classifier = self._create_classifier( qnn, num_parameters, @@ -443,9 +413,7 @@ def test_bad_one_hot_data(self): """Test that trying to train a one-hot classifier with incompatible data raises an error.""" optimizer = L_BFGS_B(maxiter=5) - qnn, num_inputs, num_parameters = self._create_circuit_qnn( - self.sv_quantum_instance, output_shape=2 - ) + qnn, num_inputs, num_parameters = self._create_sampler_qnn(output_shape=2) classifier = self._create_classifier( qnn, num_parameters, optimizer, loss="absolute_error", one_hot=True ) @@ -460,7 +428,7 @@ def test_bad_one_hot_data(self): def test_untrained(self): """Test untrained classifier.""" - qnn, _, _ = self._create_circuit_qnn(self.sv_quantum_instance) + qnn, _, _ = self._create_sampler_qnn() classifier = NeuralNetworkClassifier(qnn) with self.assertRaises(QiskitMachineLearningError, msg="classifier.predict()"): classifier.predict(np.asarray([])) @@ -473,7 +441,15 @@ def test_untrained(self): def test_callback_setter(self): """Test the callback setter.""" - qnn = TwoLayerQNN(2, quantum_instance=self.qasm_quantum_instance) + num_qubits = 2 + feature_map = ZZFeatureMap(num_qubits) + ansatz = RealAmplitudes(num_qubits) + qc = QuantumCircuit(2) + qc.compose(feature_map, inplace=True) + qc.compose(ansatz, inplace=True) + qnn = EstimatorQNN( + circuit=qc, input_params=feature_map.parameters, weight_params=ansatz.parameters + ) single_step_opt = SPSA(maxiter=1, learning_rate=0.01, perturbation=0.1) classifier = NeuralNetworkClassifier(qnn, optimizer=single_step_opt) diff --git a/test/algorithms/classifiers/test_neural_network_classifier_sampler_qnn.py b/test/algorithms/classifiers/test_neural_network_classifier_sampler_qnn.py deleted file mode 100644 index 76e279cce..000000000 --- a/test/algorithms/classifiers/test_neural_network_classifier_sampler_qnn.py +++ /dev/null @@ -1,472 +0,0 @@ -# This code is part of a Qiskit project. -# -# (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 -# 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 Neural Network Classifier """ -from __future__ import annotations - -import itertools -import os -import tempfile -import unittest -from functools import partial -from typing import Callable - -from test import QiskitMachineLearningTestCase - -import numpy as np -import scipy -from ddt import ddt, data, idata, unpack -from qiskit.algorithms.optimizers import COBYLA, L_BFGS_B, SPSA, Optimizer -from qiskit.circuit import QuantumCircuit -from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap -from qiskit.utils import algorithm_globals, optionals -from scipy.optimize import minimize - -from qiskit_machine_learning.algorithms import SerializableModelMixin -from qiskit_machine_learning.algorithms.classifiers import NeuralNetworkClassifier -from qiskit_machine_learning.exceptions import QiskitMachineLearningError -from qiskit_machine_learning.neural_networks import NeuralNetwork, EstimatorQNN, SamplerQNN -from qiskit_machine_learning.utils.loss_functions import CrossEntropyLoss - -OPTIMIZERS = ["cobyla", "bfgs", "callable", None] -L1L2_ERRORS = ["absolute_error", "squared_error"] -CALLBACKS = [True, False] - - -def _one_hot_encode(y: np.ndarray) -> np.ndarray: - y_one_hot = np.zeros((y.size, int(y.max() + 1)), dtype=int) - y_one_hot[np.arange(y.size), y] = 1 - return y_one_hot - - -@ddt -class TestNeuralNetworkClassifier(QiskitMachineLearningTestCase): - """Neural Network Classifier Tests.""" - - @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required to run this test") - def setUp(self): - super().setUp() - - # specify quantum instances - algorithm_globals.random_seed = 12345 - - def _create_optimizer(self, opt: str) -> Optimizer | None: - if opt == "bfgs": - optimizer = L_BFGS_B(maxiter=5) - elif opt == "cobyla": - optimizer = COBYLA(maxiter=25) - elif opt == "callable": - optimizer = partial(minimize, method="COBYLA", options={"maxiter": 25}) - else: - optimizer = None - - return optimizer - - def _create_callback(self, cb_flag): - if cb_flag: - history = {"weights": [], "values": []} - - def callback(objective_weights, objective_value): - history["weights"].append(objective_weights) - history["values"].append(objective_value) - - else: - history = None - callback = None - return callback, history - - @idata(itertools.product(OPTIMIZERS, L1L2_ERRORS, CALLBACKS)) - @unpack - def test_classifier_with_estimator_qnn(self, opt, loss, cb_flag): - """Test Neural Network Classifier with Estimator QNN.""" - - optimizer = self._create_optimizer(opt) - callback, history = self._create_callback(cb_flag) - - num_inputs = 2 - feature_map = ZZFeatureMap(num_inputs) - ansatz = RealAmplitudes(num_inputs, reps=1) - - qc = QuantumCircuit(num_inputs) - qc.compose(feature_map, inplace=True) - qc.compose(ansatz, inplace=True) - - qnn = EstimatorQNN( - circuit=qc, input_params=feature_map.parameters, weight_params=ansatz.parameters - ) - - classifier = self._create_classifier(qnn, ansatz.num_parameters, optimizer, loss, callback) - - # construct data - num_samples = 6 - X = algorithm_globals.random.random( # pylint: disable=invalid-name - (num_samples, num_inputs) - ) - y = 2.0 * (np.sum(X, axis=1) <= 1) - 1.0 - - # fit to data - classifier.fit(X, y) - - # score - score = classifier.score(X, y) - self.assertGreater(score, 0.5) - - self._verify_callback_values(callback, history, qnn.num_weights) - - self.assertIsNotNone(classifier.fit_result) - self.assertIsNotNone(classifier.weights) - np.testing.assert_array_equal(classifier.fit_result.x, classifier.weights) - self.assertEqual(len(classifier.weights), ansatz.num_parameters) - - def _verify_callback_values(self, callback, history, num_weights): - if callback is not None: - self.assertTrue(all(isinstance(value, float) for value in history["values"])) - for weights in history["weights"]: - self.assertEqual(len(weights), num_weights) - self.assertTrue(all(isinstance(weight, float) for weight in weights)) - - def _create_sampler_qnn(self, output_shape=2) -> tuple[SamplerQNN, int, int]: - num_inputs = 2 - feature_map = ZZFeatureMap(num_inputs) - ansatz = RealAmplitudes(num_inputs, reps=1) - - # construct circuit - qc = QuantumCircuit(num_inputs) - qc.append(feature_map, range(2)) - qc.append(ansatz, range(2)) - - # construct qnn - def parity(x): - return f"{x:b}".count("1") % 2 - - qnn = SamplerQNN( - circuit=qc, - input_params=feature_map.parameters, - weight_params=ansatz.parameters, - sparse=False, - interpret=parity, - output_shape=output_shape, - ) - - return qnn, num_inputs, ansatz.num_parameters - - def _generate_data(self, num_inputs: int) -> tuple[np.ndarray, np.ndarray]: - # construct data - num_samples = 6 - features = algorithm_globals.random.random((num_samples, num_inputs)) - labels = 1.0 * (np.sum(features, axis=1) <= 1) - - return features, labels - - def _create_classifier( - self, - qnn: NeuralNetwork, - num_parameters: int, - optimizer: Optimizer, - loss: str, - callback: Callable[[np.ndarray, float], None] | None = None, - one_hot: bool = False, - ): - initial_point = np.array([0.5] * num_parameters) - - # construct classifier - classifier = NeuralNetworkClassifier( - qnn, - optimizer=optimizer, - loss=loss, - one_hot=one_hot, - initial_point=initial_point, - callback=callback, - ) - return classifier - - @idata(itertools.product(OPTIMIZERS, L1L2_ERRORS, CALLBACKS)) - @unpack - def test_classifier_with_sampler_qnn(self, opt, loss, cb_flag): - """Test Neural Network Classifier with SamplerQNN.""" - - optimizer = self._create_optimizer(opt) - callback, history = self._create_callback(cb_flag) - - qnn, num_inputs, num_parameters = self._create_sampler_qnn() - - classifier = self._create_classifier(qnn, num_parameters, optimizer, loss, callback) - - features, labels = self._generate_data(num_inputs) - - # fit to data - classifier.fit(features, labels) - - # score - score = classifier.score(features, labels) - self.assertGreater(score, 0.5) - - self._verify_callback_values(callback, history, qnn.num_weights) - - self.assertIsNotNone(classifier.fit_result) - self.assertIsNotNone(classifier.weights) - np.testing.assert_array_equal(classifier.fit_result.x, classifier.weights) - self.assertEqual(len(classifier.weights), num_parameters) - - @idata(OPTIMIZERS) - def test_classifier_with_circuit_qnn_and_cross_entropy(self, opt): - """Test Neural Network Classifier with Circuit QNN and Cross Entropy loss.""" - - optimizer = self._create_optimizer(opt) - qnn, num_inputs, num_parameters = self._create_sampler_qnn() - - loss = CrossEntropyLoss() - classifier = self._create_classifier(qnn, num_parameters, optimizer, loss, one_hot=True) - - features, labels = self._generate_data(num_inputs) - labels = np.array([labels, 1 - labels]).transpose() - - # fit to data - classifier.fit(features, labels) - - # score - score = classifier.score(features, labels) - self.assertGreater(score, 0.5) - - @data( - # one-hot, loss - (True, "absolute_error"), - (True, "squared_error"), - (True, "cross_entropy"), - (False, "absolute_error"), - (False, "squared_error"), - ) - def test_categorical_data(self, config): - """Test categorical labels using QNN""" - - one_hot, loss = config - - optimizer = L_BFGS_B(maxiter=5) - - qnn, num_inputs, num_parameters = self._create_sampler_qnn() - - classifier = self._create_classifier(qnn, num_parameters, optimizer, loss, one_hot=one_hot) - - features, labels = self._generate_data(num_inputs) - labels = labels.astype(str) - # convert to categorical - labels[labels == "0.0"] = "A" - labels[labels == "1.0"] = "B" - - # fit to data - classifier.fit(features, labels) - - # score - score = classifier.score(features, labels) - self.assertGreater(score, 0.5) - - predict = classifier.predict(features[0, :]) - self.assertIn(predict, ["A", "B"]) - - @idata(L1L2_ERRORS + ["cross_entropy"]) - def test_sparse_arrays(self, loss): - """Tests classifier with sparse arrays as features and labels.""" - optimizer = L_BFGS_B(maxiter=5) - qnn, _, num_parameters = self._create_sampler_qnn() - classifier = self._create_classifier(qnn, num_parameters, optimizer, loss, one_hot=True) - - features = scipy.sparse.csr_matrix([[0, 0], [1, 1]]) - labels = scipy.sparse.csr_matrix([[1, 0], [0, 1]]) - - # fit to data - classifier.fit(features, labels) - - # score - score = classifier.score(features, labels) - self.assertGreater(score, 0.5) - - @idata(["opflow", "circuit_qnn"]) - def test_save_load(self, qnn_type): - """Tests save and load models.""" - features = np.array([[0, 0], [0.1, 0.2], [1, 1], [0.9, 0.8]]) - - if qnn_type == "opflow": - labels = np.array([-1, -1, 1, 1]) - - num_qubits = 2 - feature_map = ZZFeatureMap(num_qubits) - ansatz = RealAmplitudes(num_qubits, reps=1) - qc = QuantumCircuit(num_qubits) - qc.compose(feature_map, inplace=True) - qc.compose(ansatz, inplace=True) - qnn = EstimatorQNN( - circuit=qc, - input_params=feature_map.parameters, - weight_params=ansatz.parameters, - ) - num_parameters = ansatz.num_parameters - elif qnn_type == "circuit_qnn": - labels = np.array([0, 0, 1, 1]) - qnn, _, num_parameters = self._create_sampler_qnn() - else: - raise ValueError(f"Unsupported QNN type: {qnn_type}") - - classifier = self._create_classifier( - qnn, num_parameters=num_parameters, optimizer=COBYLA(), loss="squared_error" - ) - classifier.fit(features, labels) - - # predicted labels from the newly trained model - test_features = np.array([[0.2, 0.1], [0.8, 0.9]]) - original_predicts = classifier.predict(test_features) - - # save/load, change the quantum instance and check if predicted values are the same - with tempfile.TemporaryDirectory() as dir_name: - file_name = os.path.join(dir_name, "classifier.model") - classifier.save(file_name) - - classifier_load = NeuralNetworkClassifier.load(file_name) - loaded_model_predicts = classifier_load.predict(test_features) - - np.testing.assert_array_almost_equal(original_predicts, loaded_model_predicts) - - # test loading warning - class FakeModel(SerializableModelMixin): - """Fake model class for test purposes.""" - - pass - - with self.assertRaises(TypeError): - FakeModel.load(file_name) - - @idata((True, False)) - def test_num_classes_data(self, one_hot): - """Test the number of assumed classes for one-hot and not one-hot data.""" - - optimizer = L_BFGS_B(maxiter=5) - qnn, num_inputs, num_parameters = self._create_sampler_qnn() - features, labels = self._generate_data(num_inputs) - - if one_hot: - # convert to one-hot - labels = _one_hot_encode(labels.astype(int)) - else: - # convert to categorical - labels = labels.astype(str) - labels[labels == "0.0"] = "A" - labels[labels == "1.0"] = "B" - - classifier = self._create_classifier( - qnn, num_parameters, optimizer, loss="absolute_error", one_hot=one_hot - ) - - # fit to data - classifier.fit(features, labels) - num_classes = classifier.num_classes - - self.assertEqual(num_classes, 2) - - def test_binary_classification_with_multiclass_data(self): - """Test that trying to train a binary classifier with multiclass data raises an error.""" - - optimizer = L_BFGS_B(maxiter=5) - qnn, num_inputs, num_parameters = self._create_sampler_qnn(output_shape=1) - classifier = self._create_classifier( - qnn, - num_parameters, - optimizer, - loss="absolute_error", - ) - - # construct data - num_samples = 3 - x = algorithm_globals.random.random((num_samples, num_inputs)) - y = np.asarray([0, 1, 2]) - - with self.assertRaises(QiskitMachineLearningError): - classifier.fit(x, y) - - def test_bad_binary_shape(self): - """Test that trying to train a binary classifier with misshaped data raises an error.""" - - optimizer = L_BFGS_B(maxiter=5) - qnn, num_inputs, num_parameters = self._create_sampler_qnn(output_shape=1) - classifier = self._create_classifier( - qnn, - num_parameters, - optimizer, - loss="absolute_error", - ) - - # construct data - num_samples = 2 - x = algorithm_globals.random.random((num_samples, num_inputs)) - y = np.array([[0, 1], [1, 0]]) - - with self.assertRaises(QiskitMachineLearningError): - classifier.fit(x, y) - - def test_bad_one_hot_data(self): - """Test that trying to train a one-hot classifier with incompatible data raises an error.""" - - optimizer = L_BFGS_B(maxiter=5) - qnn, num_inputs, num_parameters = self._create_sampler_qnn(output_shape=2) - classifier = self._create_classifier( - qnn, num_parameters, optimizer, loss="absolute_error", one_hot=True - ) - - # construct data - num_samples = 2 - x = algorithm_globals.random.random((num_samples, num_inputs)) - y = np.array([[0, 1], [2, 0]]) - - with self.assertRaises(QiskitMachineLearningError): - classifier.fit(x, y) - - def test_untrained(self): - """Test untrained classifier.""" - qnn, _, _ = self._create_sampler_qnn() - classifier = NeuralNetworkClassifier(qnn) - with self.assertRaises(QiskitMachineLearningError, msg="classifier.predict()"): - classifier.predict(np.asarray([])) - - with self.assertRaises(QiskitMachineLearningError, msg="classifier.fit_result"): - _ = classifier.fit_result - - with self.assertRaises(QiskitMachineLearningError, msg="classifier.weights"): - _ = classifier.weights - - def test_callback_setter(self): - """Test the callback setter.""" - num_qubits = 2 - feature_map = ZZFeatureMap(num_qubits) - ansatz = RealAmplitudes(num_qubits) - qc = QuantumCircuit(2) - qc.compose(feature_map, inplace=True) - qc.compose(ansatz, inplace=True) - qnn = EstimatorQNN( - circuit=qc, input_params=feature_map.parameters, weight_params=ansatz.parameters - ) - single_step_opt = SPSA(maxiter=1, learning_rate=0.01, perturbation=0.1) - classifier = NeuralNetworkClassifier(qnn, optimizer=single_step_opt) - - loss_history = [] - - def store_loss(_, loss): - loss_history.append(loss) - - # use setter for the callback instead of providing in the initialize method - classifier.callback = store_loss - - features = np.array([[0, 0], [1, 1]]) - labels = np.array([0, 1]) - classifier.fit(features, labels) - - self.assertEqual(len(loss_history), 3) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/algorithms/classifiers/test_vqc.py b/test/algorithms/classifiers/test_vqc.py index 19177fd39..13c0ae65a 100644 --- a/test/algorithms/classifiers/test_vqc.py +++ b/test/algorithms/classifiers/test_vqc.py @@ -171,7 +171,7 @@ def test_warm_start(self, d_s): self.assertGreater(score, 0.5) def _get_num_classes(self, func): - """Wrapper to record the number of classes assumed when building CircuitQNN.""" + """Wrapper to record the number of classes assumed when building SamplerQNN.""" @functools.wraps(func) def wrapper(num_classes): @@ -207,7 +207,7 @@ def test_batches_with_incomplete_labels(self): with self.subTest("Test all batches assume the correct number of classes."): self.assertTrue((np.asarray(num_classes_list) == 3).all()) - with self.subTest("Check correct number of classes is used to build CircuitQNN."): + with self.subTest("Check correct number of classes is used to build SamplerQNN."): self.assertTrue((np.asarray(self.num_classes_by_batch) == 3).all()) def test_multilabel_targets_raise_an_error(self): diff --git a/test/algorithms/regressors/test_neural_network_regressor.py b/test/algorithms/regressors/test_neural_network_regressor.py index 5d51d0770..5e6acdcb0 100644 --- a/test/algorithms/regressors/test_neural_network_regressor.py +++ b/test/algorithms/regressors/test_neural_network_regressor.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2018, 2023. +# (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 @@ -10,13 +10,13 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. """ Test Neural Network Regressor """ +from __future__ import annotations + import itertools import os import tempfile import unittest -import warnings from functools import partial -from typing import Tuple from test import QiskitMachineLearningTestCase @@ -25,14 +25,13 @@ from qiskit.algorithms.optimizers import COBYLA, L_BFGS_B, SPSA from qiskit.circuit import Parameter, QuantumCircuit from qiskit.circuit.library import ZZFeatureMap, RealAmplitudes -from qiskit.opflow import PauliSumOp -from qiskit.utils import QuantumInstance, algorithm_globals, optionals +from qiskit.utils import algorithm_globals from scipy.optimize import minimize from qiskit_machine_learning import QiskitMachineLearningError from qiskit_machine_learning.algorithms import SerializableModelMixin from qiskit_machine_learning.algorithms.regressors import NeuralNetworkRegressor -from qiskit_machine_learning.neural_networks import TwoLayerQNN +from qiskit_machine_learning.neural_networks import EstimatorQNN QUANTUM_INSTANCES = ["statevector", "qasm"] OPTIMIZERS = ["cobyla", "bfgs", "callable", None] @@ -43,26 +42,11 @@ class TestNeuralNetworkRegressor(QiskitMachineLearningTestCase): """Test Neural Network Regressor.""" - @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required to run this test") def setUp(self): super().setUp() - warnings.filterwarnings("ignore", category=DeprecationWarning) # specify quantum instances algorithm_globals.random_seed = 12345 - from qiskit_aer import Aer - - self.sv_quantum_instance = QuantumInstance( - Aer.get_backend("aer_simulator_statevector"), - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) - self.qasm_quantum_instance = QuantumInstance( - Aer.get_backend("aer_simulator"), - shots=100, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) num_samples = 20 eps = 0.2 @@ -73,13 +57,9 @@ def setUp(self): self.X = (ub - lb) * rng.random((num_samples, 1)) + lb self.y = np.sin(self.X[:, 0]) + eps * (2 * rng.random(num_samples) - 1) - def tearDown(self) -> None: - super().tearDown() - warnings.filterwarnings("always", category=DeprecationWarning) - def _create_regressor( - self, opt, q_i, callback=None - ) -> Tuple[NeuralNetworkRegressor, TwoLayerQNN, QuantumCircuit]: + self, opt, callback=None + ) -> tuple[NeuralNetworkRegressor, EstimatorQNN, QuantumCircuit]: num_qubits = 1 # construct simple feature map @@ -92,12 +72,9 @@ def _create_regressor( ansatz = QuantumCircuit(num_qubits, name="vf") ansatz.ry(param_y, 0) - if q_i == "statevector": - quantum_instance = self.sv_quantum_instance - elif q_i == "qasm": - quantum_instance = self.qasm_quantum_instance - else: - raise ValueError(f"Unsupported quantum instance: {q_i}") + qc = QuantumCircuit(num_qubits) + qc.compose(feature_map, inplace=True) + qc.compose(ansatz, inplace=True) if opt == "bfgs": optimizer = L_BFGS_B(maxiter=5) @@ -109,27 +86,27 @@ def _create_regressor( optimizer = None # construct QNN - regression_opflow_qnn = TwoLayerQNN( - num_qubits, feature_map, ansatz, quantum_instance=quantum_instance + regression_estimator_qnn = EstimatorQNN( + circuit=qc, input_params=feature_map.parameters, weight_params=ansatz.parameters ) initial_point = np.zeros(ansatz.num_parameters) # construct the regressor from the neural network regressor = NeuralNetworkRegressor( - neural_network=regression_opflow_qnn, + neural_network=regression_estimator_qnn, loss="squared_error", optimizer=optimizer, initial_point=initial_point, callback=callback, ) - return regressor, regression_opflow_qnn, ansatz + return regressor, regression_estimator_qnn, ansatz - @idata(itertools.product(OPTIMIZERS, QUANTUM_INSTANCES, CALLBACKS)) + @idata(itertools.product(OPTIMIZERS, CALLBACKS)) @unpack - def test_regressor_with_opflow_qnn(self, opt, q_i, cb_flag): - """Test Neural Network Regressor with Opflow QNN (Two Layer QNN).""" + def test_regressor_with_estimator_qnn(self, opt, cb_flag): + """Test Neural Network Regressor with Estimator QNN.""" if cb_flag: history = {"weights": [], "values": []} @@ -140,7 +117,7 @@ def callback(objective_weights, objective_value): else: callback = None - regressor, qnn, ansatz = self._create_regressor(opt, q_i, callback) + regressor, qnn, ansatz = self._create_regressor(opt, callback) # fit to data regressor.fit(self.X, self.y) @@ -161,11 +138,10 @@ def callback(objective_weights, objective_value): np.testing.assert_array_equal(regressor.fit_result.x, regressor.weights) self.assertEqual(len(regressor.weights), ansatz.num_parameters) - @idata(itertools.product(OPTIMIZERS, QUANTUM_INSTANCES)) - @unpack - def test_warm_start(self, opt, q_i): + @idata(OPTIMIZERS) + def test_warm_start(self, opt): """Test VQC when training from a warm start.""" - regressor, _, _ = self._create_regressor(opt, q_i) + regressor, _, _ = self._create_regressor(opt) regressor.warm_start = True # Fit the regressor to the first half of the data. @@ -188,12 +164,17 @@ def test_save_load(self): features = np.array([[0, 0], [0.1, 0.1], [0.4, 0.4], [1, 1]]) labels = np.array([0, 0.1, 0.4, 1]) num_inputs = 2 - qnn = TwoLayerQNN( - num_inputs, - feature_map=ZZFeatureMap(num_inputs), - ansatz=RealAmplitudes(num_inputs), - observable=PauliSumOp.from_list([("Z" * num_inputs, 1)]), - quantum_instance=self.qasm_quantum_instance, + + feature_map = ZZFeatureMap(num_inputs) + ansatz = RealAmplitudes(num_inputs) + qc = QuantumCircuit(num_inputs) + qc.compose(feature_map, inplace=True) + qc.compose(ansatz, inplace=True) + + qnn = EstimatorQNN( + circuit=qc, + input_params=feature_map.parameters, + weight_params=ansatz.parameters, ) regressor = NeuralNetworkRegressor(qnn, optimizer=COBYLA()) regressor.fit(features, labels) @@ -223,7 +204,8 @@ class FakeModel(SerializableModelMixin): def test_untrained(self): """Test untrained regressor.""" - qnn = TwoLayerQNN(2) + qnn = EstimatorQNN(circuit=QuantumCircuit(2)) + regressor = NeuralNetworkRegressor(qnn) with self.assertRaises(QiskitMachineLearningError, msg="regressor.predict()"): regressor.predict(np.asarray([])) @@ -236,7 +218,19 @@ def test_untrained(self): def test_callback_setter(self): """Test the callback setter.""" - qnn = TwoLayerQNN(2, quantum_instance=self.qasm_quantum_instance) + num_inputs = 2 + feature_map = ZZFeatureMap(num_inputs) + ansatz = RealAmplitudes(num_inputs) + qc = QuantumCircuit(num_inputs) + qc.compose(feature_map, inplace=True) + qc.compose(ansatz, inplace=True) + + qnn = EstimatorQNN( + circuit=qc, + input_params=feature_map.parameters, + weight_params=ansatz.parameters, + ) + single_step_opt = SPSA(maxiter=1, learning_rate=0.01, perturbation=0.1) regressor = NeuralNetworkRegressor(qnn, optimizer=single_step_opt) diff --git a/test/algorithms/regressors/test_neural_network_regressor_estimator_qnn.py b/test/algorithms/regressors/test_neural_network_regressor_estimator_qnn.py deleted file mode 100644 index 5e6acdcb0..000000000 --- a/test/algorithms/regressors/test_neural_network_regressor_estimator_qnn.py +++ /dev/null @@ -1,253 +0,0 @@ -# This code is part of a Qiskit project. -# -# (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 -# 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 Neural Network Regressor """ -from __future__ import annotations - -import itertools -import os -import tempfile -import unittest -from functools import partial - -from test import QiskitMachineLearningTestCase - -import numpy as np -from ddt import ddt, unpack, idata -from qiskit.algorithms.optimizers import COBYLA, L_BFGS_B, SPSA -from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.circuit.library import ZZFeatureMap, RealAmplitudes -from qiskit.utils import algorithm_globals -from scipy.optimize import minimize - -from qiskit_machine_learning import QiskitMachineLearningError -from qiskit_machine_learning.algorithms import SerializableModelMixin -from qiskit_machine_learning.algorithms.regressors import NeuralNetworkRegressor -from qiskit_machine_learning.neural_networks import EstimatorQNN - -QUANTUM_INSTANCES = ["statevector", "qasm"] -OPTIMIZERS = ["cobyla", "bfgs", "callable", None] -CALLBACKS = [True, False] - - -@ddt -class TestNeuralNetworkRegressor(QiskitMachineLearningTestCase): - """Test Neural Network Regressor.""" - - def setUp(self): - super().setUp() - - # specify quantum instances - algorithm_globals.random_seed = 12345 - - num_samples = 20 - eps = 0.2 - - # pylint: disable=invalid-name - lb, ub = -np.pi, np.pi - rng = np.random.default_rng(101) - self.X = (ub - lb) * rng.random((num_samples, 1)) + lb - self.y = np.sin(self.X[:, 0]) + eps * (2 * rng.random(num_samples) - 1) - - def _create_regressor( - self, opt, callback=None - ) -> tuple[NeuralNetworkRegressor, EstimatorQNN, QuantumCircuit]: - num_qubits = 1 - - # construct simple feature map - param_x = Parameter("x") - feature_map = QuantumCircuit(num_qubits, name="fm") - feature_map.ry(param_x, 0) - - # construct simple feature map - param_y = Parameter("y") - ansatz = QuantumCircuit(num_qubits, name="vf") - ansatz.ry(param_y, 0) - - qc = QuantumCircuit(num_qubits) - qc.compose(feature_map, inplace=True) - qc.compose(ansatz, inplace=True) - - if opt == "bfgs": - optimizer = L_BFGS_B(maxiter=5) - elif opt == "cobyla": - optimizer = COBYLA(maxiter=25) - elif opt == "callable": - optimizer = partial(minimize, method="COBYLA", options={"maxiter": 25}) - else: - optimizer = None - - # construct QNN - regression_estimator_qnn = EstimatorQNN( - circuit=qc, input_params=feature_map.parameters, weight_params=ansatz.parameters - ) - - initial_point = np.zeros(ansatz.num_parameters) - - # construct the regressor from the neural network - regressor = NeuralNetworkRegressor( - neural_network=regression_estimator_qnn, - loss="squared_error", - optimizer=optimizer, - initial_point=initial_point, - callback=callback, - ) - - return regressor, regression_estimator_qnn, ansatz - - @idata(itertools.product(OPTIMIZERS, CALLBACKS)) - @unpack - def test_regressor_with_estimator_qnn(self, opt, cb_flag): - """Test Neural Network Regressor with Estimator QNN.""" - if cb_flag: - history = {"weights": [], "values": []} - - def callback(objective_weights, objective_value): - history["weights"].append(objective_weights) - history["values"].append(objective_value) - - else: - callback = None - - regressor, qnn, ansatz = self._create_regressor(opt, callback) - - # fit to data - regressor.fit(self.X, self.y) - - # score the result - score = regressor.score(self.X, self.y) - self.assertGreater(score, 0.5) - - # callback - if callback is not None: - self.assertTrue(all(isinstance(value, float) for value in history["values"])) - for weights in history["weights"]: - self.assertEqual(len(weights), qnn.num_weights) - self.assertTrue(all(isinstance(weight, float) for weight in weights)) - - self.assertIsNotNone(regressor.fit_result) - self.assertIsNotNone(regressor.weights) - np.testing.assert_array_equal(regressor.fit_result.x, regressor.weights) - self.assertEqual(len(regressor.weights), ansatz.num_parameters) - - @idata(OPTIMIZERS) - def test_warm_start(self, opt): - """Test VQC when training from a warm start.""" - regressor, _, _ = self._create_regressor(opt) - regressor.warm_start = True - - # Fit the regressor to the first half of the data. - num_start = len(self.y) // 2 - regressor.fit(self.X[:num_start, :], self.y[:num_start]) - first_fit_final_point = regressor.weights - - # Fit the regressor to the second half of the data with a warm start. - regressor.fit(self.X[num_start:, :], self.y[num_start:]) - second_fit_initial_point = regressor._initial_point - - # Check the final optimization point from the first fit was used to start the second fit. - np.testing.assert_allclose(first_fit_final_point, second_fit_initial_point) - - score = regressor.score(self.X, self.y) - self.assertGreater(score, 0.5) - - def test_save_load(self): - """Tests save and load models.""" - features = np.array([[0, 0], [0.1, 0.1], [0.4, 0.4], [1, 1]]) - labels = np.array([0, 0.1, 0.4, 1]) - num_inputs = 2 - - feature_map = ZZFeatureMap(num_inputs) - ansatz = RealAmplitudes(num_inputs) - qc = QuantumCircuit(num_inputs) - qc.compose(feature_map, inplace=True) - qc.compose(ansatz, inplace=True) - - qnn = EstimatorQNN( - circuit=qc, - input_params=feature_map.parameters, - weight_params=ansatz.parameters, - ) - regressor = NeuralNetworkRegressor(qnn, optimizer=COBYLA()) - regressor.fit(features, labels) - - # predicted labels from the newly trained model - test_features = np.array([[0.5, 0.5]]) - original_predicts = regressor.predict(test_features) - - # save/load, change the quantum instance and check if predicted values are the same - with tempfile.TemporaryDirectory() as dir_name: - file_name = os.path.join(dir_name, "regressor.model") - regressor.save(file_name) - - regressor_load = NeuralNetworkRegressor.load(file_name) - loaded_model_predicts = regressor_load.predict(test_features) - - np.testing.assert_array_almost_equal(original_predicts, loaded_model_predicts) - - # test loading warning - class FakeModel(SerializableModelMixin): - """Fake model class for test purposes.""" - - pass - - with self.assertRaises(TypeError): - FakeModel.load(file_name) - - def test_untrained(self): - """Test untrained regressor.""" - qnn = EstimatorQNN(circuit=QuantumCircuit(2)) - - regressor = NeuralNetworkRegressor(qnn) - with self.assertRaises(QiskitMachineLearningError, msg="regressor.predict()"): - regressor.predict(np.asarray([])) - - with self.assertRaises(QiskitMachineLearningError, msg="regressor.fit_result"): - _ = regressor.fit_result - - with self.assertRaises(QiskitMachineLearningError, msg="regressor.weights"): - _ = regressor.weights - - def test_callback_setter(self): - """Test the callback setter.""" - num_inputs = 2 - feature_map = ZZFeatureMap(num_inputs) - ansatz = RealAmplitudes(num_inputs) - qc = QuantumCircuit(num_inputs) - qc.compose(feature_map, inplace=True) - qc.compose(ansatz, inplace=True) - - qnn = EstimatorQNN( - circuit=qc, - input_params=feature_map.parameters, - weight_params=ansatz.parameters, - ) - - single_step_opt = SPSA(maxiter=1, learning_rate=0.01, perturbation=0.1) - regressor = NeuralNetworkRegressor(qnn, optimizer=single_step_opt) - - loss_history = [] - - def store_loss(_, loss): - loss_history.append(loss) - - # use setter for the callback instead of providing in the initialize method - regressor.callback = store_loss - - features = np.array([[0, 0], [0.1, 0.1], [0.4, 0.4], [1, 1]]) - labels = np.array([0, 0.1, 0.4, 1]) - regressor.fit(features, labels) - - self.assertEqual(len(loss_history), 3) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/algorithms/regressors/test_vqr.py b/test/algorithms/regressors/test_vqr.py index b8a11a61b..0f69341f4 100644 --- a/test/algorithms/regressors/test_vqr.py +++ b/test/algorithms/regressors/test_vqr.py @@ -19,7 +19,6 @@ from qiskit.algorithms.optimizers import COBYLA, L_BFGS_B from qiskit.circuit import Parameter, QuantumCircuit from qiskit.circuit.library import ZZFeatureMap, RealAmplitudes -from qiskit.opflow import StateFn from qiskit.primitives import Estimator from qiskit.utils import algorithm_globals @@ -117,8 +116,6 @@ def test_incorrect_observable(self): """Test VQR with a wrong observable.""" with self.assertRaises(ValueError): _ = VQR(num_qubits=2, observable=QuantumCircuit(2)) - with self.assertRaises(ValueError): - _ = VQR(num_qubits=2, observable=StateFn(QuantumCircuit(2))) if __name__ == "__main__": diff --git a/test/connectors/test_torch_connector.py b/test/connectors/test_torch_connector.py index f849ab622..045eb0ee7 100644 --- a/test/connectors/test_torch_connector.py +++ b/test/connectors/test_torch_connector.py @@ -11,30 +11,20 @@ # that they have been altered from the originals. """Test Torch Connector.""" -import builtins import itertools -import sys -from typing import List, cast +from typing import cast from test.connectors.test_torch import TestTorch import numpy as np from ddt import ddt, data, unpack, idata from qiskit import QuantumCircuit -from qiskit.circuit import Parameter -from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap, ZFeatureMap -from qiskit.opflow import StateFn, ListOp, PauliSumOp +from qiskit.circuit.library import RealAmplitudes, ZFeatureMap from qiskit.quantum_info import SparsePauliOp from qiskit_machine_learning import QiskitMachineLearningError from qiskit_machine_learning.connectors import TorchConnector -from qiskit_machine_learning.neural_networks import ( - CircuitQNN, - TwoLayerQNN, - OpflowQNN, - SamplerQNN, - EstimatorQNN, -) +from qiskit_machine_learning.neural_networks import SamplerQNN, EstimatorQNN @ddt @@ -54,59 +44,6 @@ def setup_test(self): torch.tensor([[[1.0], [2.0]], [[3.0], [4.0]]]), ] - def _validate_output_shape(self, model: TorchConnector, test_data: List) -> None: - """Creates a Linear PyTorch module with the same in/out dimensions as the given model, - applies the list of test input data to both, and asserts that they have the same - output shape. - - Args: - model: model to be tested - test_data: list of test input tensors - - Raises: - QiskitMachineLearningError: Invalid input. - """ - from torch.nn import Linear - - # create benchmark model - in_dim = model.neural_network.num_inputs - if len(model.neural_network.output_shape) != 1: - raise QiskitMachineLearningError("Function only works for one dimensional output") - out_dim = model.neural_network.output_shape[0] - # we target our tests to either cpu or gpu - linear = Linear(in_dim, out_dim, device=self._device) - model.to(self._device) - - # iterate over test data and validate behavior of model - for x in test_data: - x = x.to(self._device) - # test linear model and track whether it failed or store the output shape - c_worked = True - try: - c_shape = linear(x).shape - except builtins.Exception: # pylint: disable=broad-except - c_worked = False - - # test quantum model and track whether it failed or store the output shape - q_worked = True - try: - output = model(x) - - # check output is sparse - model_sparse = model.sparse if model.sparse else False - self.assertEqual(output.is_sparse, model_sparse) - - q_shape = output.shape - except builtins.Exception: # pylint: disable=broad-except - q_worked = False - - # compare results and assert that the behavior is equal - with self.subTest("c_worked == q_worked", tensor=x): - self.assertEqual(c_worked, q_worked) - if c_worked and q_worked: - with self.subTest("c_shape == q_shape", tensor=x): - self.assertEqual(c_shape, q_shape) - def _validate_backward_automatically(self, model: TorchConnector) -> None: """Uses PyTorch to validate the backward pass / autograd. @@ -136,484 +73,6 @@ def _validate_backward_automatically(self, model: TorchConnector) -> None: test = torch.autograd.gradcheck(func, input_data, eps=1e-4, atol=1e-3) # type: ignore self.assertTrue(test) - @data("sv", "qasm") - def test_opflow_qnn_1_1(self, q_i): - """Test Torch Connector + Opflow QNN with input/output dimension 1/1.""" - if q_i == "sv": - quantum_instance = self._sv_quantum_instance - else: - quantum_instance = self._qasm_quantum_instance - - # construct simple feature map - param_x = Parameter("x") - feature_map = QuantumCircuit(1, name="fm") - feature_map.ry(param_x, 0) - - # construct simple feature map - param_y = Parameter("y") - ansatz = QuantumCircuit(1, name="vf") - ansatz.ry(param_y, 0) - - # construct QNN with statevector simulator - qnn = TwoLayerQNN( - 1, feature_map, ansatz, quantum_instance=quantum_instance, input_gradients=True - ) - model = TorchConnector(qnn) - - # test model - self._validate_output_shape(model, self._test_data) - if q_i == "sv": - self._validate_backward_automatically(model) - - @data("sv", "qasm") - def test_opflow_qnn_2_1(self, q_i): - """Test Torch Connector + Opflow QNN with input/output dimension 2/1.""" - - if q_i == "sv": - quantum_instance = self._sv_quantum_instance - else: - quantum_instance = self._qasm_quantum_instance - - # construct QNN - qnn = TwoLayerQNN(2, quantum_instance=quantum_instance, input_gradients=True) - model = TorchConnector(qnn) - - # test model - self._validate_output_shape(model, self._test_data) - if q_i == "sv": - self._validate_backward_automatically(model) - - @data("sv", "qasm") - def test_opflow_qnn_2_2(self, q_i): - """Test Torch Connector + Opflow QNN with input/output dimension 2/2.""" - - if q_i == "sv": - quantum_instance = self._sv_quantum_instance - else: - quantum_instance = self._qasm_quantum_instance - - # construct parametrized circuit - params_1 = [Parameter("input1"), Parameter("weight1")] - qc_1 = QuantumCircuit(1) - qc_1.h(0) - qc_1.ry(params_1[0], 0) - qc_1.rx(params_1[1], 0) - qc_sfn_1 = StateFn(qc_1) - - # construct cost operator - h_1 = StateFn(PauliSumOp.from_list([("Z", 1.0), ("X", 1.0)])) - - # combine operator and circuit to objective function - op_1 = ~h_1 @ qc_sfn_1 - - # construct parametrized circuit - params_2 = [Parameter("input2"), Parameter("weight2")] - qc_2 = QuantumCircuit(1) - qc_2.h(0) - qc_2.ry(params_2[0], 0) - qc_2.rx(params_2[1], 0) - qc_sfn_2 = StateFn(qc_2) - - # construct cost operator - h_2 = StateFn(PauliSumOp.from_list([("Z", 1.0), ("X", 1.0)])) - - # combine operator and circuit to objective function - op_2 = ~h_2 @ qc_sfn_2 - - op = ListOp([op_1, op_2]) - - qnn = OpflowQNN( - op, - [params_1[0], params_2[0]], - [params_1[1], params_2[1]], - quantum_instance=quantum_instance, - input_gradients=True, - ) - model = TorchConnector(qnn) - - # test model - self._validate_output_shape(model, self._test_data) - if q_i == "sv": - self._validate_backward_automatically(model) - - @data( - # interpret, output_shape, sparse, quantum_instance - (None, None, False, "sv"), - (None, None, True, "sv"), - (lambda x: np.sum(x) % 2, 2, False, "sv"), - (lambda x: np.sum(x) % 2, 2, True, "sv"), - (None, None, False, "qasm"), - (None, None, True, "qasm"), - (lambda x: np.sum(x) % 2, 2, False, "qasm"), - (lambda x: np.sum(x) % 2, 2, True, "qasm"), - ) - @unpack - def test_circuit_qnn_1_1(self, interpret, output_shape, sparse, q_i): - """Torch Connector + Circuit QNN with no interpret, dense output, - and input/output shape 1/1 .""" - - if q_i == "sv": - quantum_instance = self._sv_quantum_instance - else: - quantum_instance = self._qasm_quantum_instance - - qc = QuantumCircuit(1) - - # construct simple feature map - param_x = Parameter("x") - qc.ry(param_x, 0) - - # construct simple feature map - param_y = Parameter("y") - qc.ry(param_y, 0) - - qnn = CircuitQNN( - qc, - [param_x], - [param_y], - sparse=sparse, - sampling=False, - interpret=interpret, - output_shape=output_shape, - quantum_instance=quantum_instance, - input_gradients=True, - ) - if sparse and sys.version_info < (3, 8): - with self.assertRaises(QiskitMachineLearningError): - _ = TorchConnector(qnn, sparse=sparse) - else: - model = TorchConnector(qnn, sparse=sparse) - - # test model - self._validate_output_shape(model, self._test_data) - if q_i == "sv": - self._validate_backward_automatically(model) - - @data( - # interpret, output_shape, sparse, quantum_instance - (None, None, False, "sv"), - (None, None, True, "sv"), - (lambda x: np.sum(x) % 2, 2, False, "sv"), - (lambda x: np.sum(x) % 2, 2, True, "sv"), - (None, None, False, "qasm"), - (None, None, True, "qasm"), - (lambda x: np.sum(x) % 2, 2, False, "qasm"), - (lambda x: np.sum(x) % 2, 2, True, "qasm"), - ) - @unpack - def test_circuit_qnn_1_8(self, interpret, output_shape, sparse, q_i): - """Torch Connector + Circuit QNN with no interpret, dense output, - and input/output shape 1/8 .""" - - if q_i == "sv": - quantum_instance = self._sv_quantum_instance - else: - quantum_instance = self._qasm_quantum_instance - - qc = QuantumCircuit(3) - - # construct simple feature map - param_x = Parameter("x") - qc.ry(param_x, range(3)) - - # construct simple feature map - param_y = Parameter("y") - qc.ry(param_y, range(3)) - - qnn = CircuitQNN( - qc, - [param_x], - [param_y], - sparse=sparse, - sampling=False, - interpret=interpret, - output_shape=output_shape, - quantum_instance=quantum_instance, - input_gradients=True, - ) - if sparse and sys.version_info < (3, 8): - with self.assertRaises(QiskitMachineLearningError): - _ = TorchConnector(qnn, sparse=sparse) - else: - model = TorchConnector(qnn, sparse=sparse) - - # test model - self._validate_output_shape(model, self._test_data) - if q_i == "sv": - self._validate_backward_automatically(model) - - @data( - # interpret, output_shape, sparse, quantum_instance - (None, None, False, "sv"), - (None, None, True, "sv"), - (lambda x: np.sum(x) % 2, 2, False, "sv"), - (lambda x: np.sum(x) % 2, 2, True, "sv"), - (None, None, False, "qasm"), - (None, None, True, "qasm"), - (lambda x: np.sum(x) % 2, 2, False, "qasm"), - (lambda x: np.sum(x) % 2, 2, True, "qasm"), - ) - @unpack - def test_circuit_qnn_2_4(self, interpret, output_shape, sparse, q_i): - """Torch Connector + Circuit QNN with no interpret, dense output, - and input/output shape 1/8 .""" - - if q_i == "sv": - quantum_instance = self._sv_quantum_instance - else: - quantum_instance = self._qasm_quantum_instance - - qc = QuantumCircuit(2) - - # construct simple feature map - param_x_1, param_x_2 = Parameter("x1"), Parameter("x2") - qc.ry(param_x_1, range(2)) - qc.ry(param_x_2, range(2)) - - # construct simple feature map - param_y = Parameter("y") - qc.ry(param_y, range(2)) - - qnn = CircuitQNN( - qc, - [param_x_1, param_x_2], - [param_y], - sparse=sparse, - sampling=False, - interpret=interpret, - output_shape=output_shape, - quantum_instance=quantum_instance, - input_gradients=True, - ) - if sparse and sys.version_info < (3, 8): - with self.assertRaises(QiskitMachineLearningError): - _ = TorchConnector(qnn, sparse=sparse) - else: - model = TorchConnector(qnn, sparse=sparse) - - # test model - self._validate_output_shape(model, self._test_data) - if q_i == "sv": - self._validate_backward_automatically(model) - - def test_circuit_qnn_without_parameters(self): - """Tests CircuitQNN without parameters.""" - quantum_instance = self._sv_quantum_instance - qc = QuantumCircuit(2) - param_y = Parameter("y") - qc.ry(param_y, range(2)) - - qnn = CircuitQNN( - circuit=qc, - input_params=[param_y], - sparse=False, - quantum_instance=quantum_instance, - input_gradients=True, - ) - model = TorchConnector(qnn) - self._validate_backward_automatically(model) - - qnn = CircuitQNN( - circuit=qc, - weight_params=[param_y], - sparse=False, - quantum_instance=quantum_instance, - input_gradients=True, - ) - model = TorchConnector(qnn) - self._validate_backward_automatically(model) - - @data( - # interpret - (None), - (lambda x: np.sum(x) % 2), - ) - def test_circuit_qnn_sampling(self, interpret): - """Test Torch Connector + Circuit QNN for sampling.""" - from torch import Tensor - - qc = QuantumCircuit(2) - - # construct simple feature map - param_x1, param_x2 = Parameter("x1"), Parameter("x2") - qc.ry(param_x1, range(2)) - qc.ry(param_x2, range(2)) - - # construct simple feature map - param_y = Parameter("y") - qc.ry(param_y, range(2)) - - qnn = CircuitQNN( - qc, - [param_x1, param_x2], - [param_y], - sparse=False, - sampling=True, - interpret=interpret, - output_shape=None, - quantum_instance=self._qasm_quantum_instance, - input_gradients=True, - ) - model = TorchConnector(qnn) - model.to(self._device) - - test_data = [Tensor([2, 2]), Tensor([[1, 1], [2, 2]])] - for i, x in enumerate(test_data): - x = x.to(self._device) - if i == 0: - self.assertEqual(model(x).shape, qnn.output_shape) - else: - shape = model(x).shape - self.assertEqual(shape, (len(x), *qnn.output_shape)) - - def test_opflow_qnn_batch_gradients(self): - """Test backward pass for batch input.""" - import torch - - # construct random data set - num_inputs = 2 - num_samples = 10 - x = np.random.rand(num_samples, num_inputs) - - # set up QNN - qnn = TwoLayerQNN( - num_qubits=num_inputs, - quantum_instance=self._sv_quantum_instance, - ) - - # set up PyTorch module - initial_weights = np.random.rand(qnn.num_weights) - model = TorchConnector(qnn, initial_weights=initial_weights) - model.to(self._device) - - # test single gradient - w = model.weight.detach().cpu().numpy() - res_qnn = qnn.forward(x[0, :], w) - - # construct finite difference gradient for weight - eps = 1e-4 - grad = np.zeros(w.shape) - for k in range(len(w)): - delta = np.zeros(w.shape) - delta[k] += eps - - f_1 = qnn.forward(x[0, :], w + delta) - f_2 = qnn.forward(x[0, :], w - delta) - - grad[k] = (f_1 - f_2) / (2 * eps) - - grad_qnn = qnn.backward(x[0, :], w)[1][0, 0, :] - self.assertAlmostEqual(np.linalg.norm(grad - grad_qnn), 0.0, places=4) - - model.zero_grad() - res_model = model(torch.tensor(x[0, :], device=self._device)) - self.assertAlmostEqual( - np.linalg.norm(res_model.detach().cpu().numpy() - res_qnn[0]), 0.0, places=4 - ) - res_model.backward() - grad_model = model.weight.grad - self.assertAlmostEqual( - np.linalg.norm(grad_model.detach().cpu().numpy() - grad_qnn), 0.0, places=4 - ) - - # test batch input - batch_grad = np.zeros((*w.shape, num_samples, 1)) - for k in range(len(w)): - delta = np.zeros(w.shape) - delta[k] += eps - - f_1 = qnn.forward(x, w + delta) - f_2 = qnn.forward(x, w - delta) - - batch_grad[k] = (f_1 - f_2) / (2 * eps) - - batch_grad = np.sum(batch_grad, axis=1) - batch_grad_qnn = np.sum(qnn.backward(x, w)[1], axis=0) - self.assertAlmostEqual( - np.linalg.norm(batch_grad - batch_grad_qnn.transpose()), 0.0, places=4 - ) - - model.zero_grad() - batch_res_model = sum(model(torch.tensor(x, device=self._device))) - batch_res_model.backward() - self.assertAlmostEqual( - np.linalg.norm(model.weight.grad.detach().cpu().numpy() - batch_grad.transpose()[0]), - 0.0, - places=4, - ) - - @data( - # output_shape, interpret - (4, None), - (2, lambda x: f"{x:b}".count("1") % 2), - ) - @unpack - def test_circuit_qnn_batch_gradients(self, output_shape, interpret): - """Test batch gradient computation of CircuitQNN gives the same result as the sum of - individual gradients.""" - import torch - from torch.nn import MSELoss - from torch.optim import SGD - - 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 = CircuitQNN( - qc, - input_params=feature_map.parameters, - weight_params=ansatz.parameters, - interpret=interpret, - output_shape=output_shape, - quantum_instance=self._sv_quantum_instance, - ) - - # set up PyTorch module - initial_weights = np.array([0.1] * qnn.num_weights) - model = TorchConnector(qnn, initial_weights) - model.to(self._device) - - # random data set - x = torch.rand(5, 2) - y = torch.rand(5, output_shape) - - # define optimizer and loss - optimizer = SGD(model.parameters(), lr=0.1) - f_loss = MSELoss(reduction="sum") - - sum_of_individual_losses = 0.0 - for x_i, y_i in zip(x, y): - x_i = x_i.to(self._device) - y_i = y_i.to(self._device) - output = model(x_i) - sum_of_individual_losses += f_loss(output, y_i) - optimizer.zero_grad() - sum_of_individual_losses.backward() - sum_of_individual_gradients = model.weight.grad.detach().cpu() - - x = x.to(self._device) - y = y.to(self._device) - output = model(x) - batch_loss = f_loss(output, y) - optimizer.zero_grad() - batch_loss.backward() - batch_gradients = model.weight.grad.detach().cpu() - - self.assertAlmostEqual( - np.linalg.norm(sum_of_individual_gradients - batch_gradients), 0.0, places=4 - ) - - self.assertAlmostEqual( - sum_of_individual_losses.detach().cpu().numpy(), - batch_loss.detach().cpu().numpy(), - places=4, - ) - def _validate_forward(self, model: TorchConnector): import torch diff --git a/test/connectors/test_torch_networks.py b/test/connectors/test_torch_networks.py index 1e7d1eda5..52530c830 100644 --- a/test/connectors/test_torch_networks.py +++ b/test/connectors/test_torch_networks.py @@ -19,13 +19,7 @@ from qiskit import QuantumCircuit from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap -from qiskit_machine_learning.neural_networks import ( - CircuitQNN, - TwoLayerQNN, - NeuralNetwork, - EstimatorQNN, - SamplerQNN, -) +from qiskit_machine_learning.neural_networks import NeuralNetwork, EstimatorQNN, SamplerQNN from qiskit_machine_learning.connectors import TorchConnector @@ -59,42 +53,6 @@ def forward(self, x): return Net() - def _create_circuit_qnn(self) -> CircuitQNN: - output_shape = 2 - num_inputs = 2 - - def interpret(x): - return f"{x:b}".count("1") % 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 = CircuitQNN( - qc, - input_params=feature_map.parameters, - weight_params=ansatz.parameters, - input_gradients=True, # for hybrid qnn - interpret=interpret, - output_shape=output_shape, - quantum_instance=self._sv_quantum_instance, - ) - return qnn - - def _create_opflow_qnn(self) -> TwoLayerQNN: - num_inputs = 2 - - # set up QNN - qnn = TwoLayerQNN( - num_qubits=num_inputs, - quantum_instance=self._sv_quantum_instance, - input_gradients=True, # for hybrid qnn - ) - return qnn - def _create_estimator_qnn(self) -> EstimatorQNN: num_inputs = 2 @@ -137,21 +95,15 @@ def interpret(x): ) return qnn - @idata(["opflow", "circuit_qnn", "sampler_qnn", "estimator_qnn"]) + @idata(["sampler_qnn", "estimator_qnn"]) def test_hybrid_batch_gradients(self, qnn_type: str): """Test gradient back-prop for batch input in a qnn.""" import torch from torch.nn import MSELoss from torch.optim import SGD - qnn: Optional[Union[CircuitQNN, TwoLayerQNN, SamplerQNN, EstimatorQNN]] = None - if qnn_type == "opflow": - qnn = self._create_opflow_qnn() - output_size = 1 - elif qnn_type == "circuit_qnn": - qnn = self._create_circuit_qnn() - output_size = 2 - elif qnn_type == "sampler_qnn": + qnn: Optional[Union[SamplerQNN, EstimatorQNN]] = None + if qnn_type == "sampler_qnn": qnn = self._create_sampler_qnn() output_size = 2 elif qnn_type == "estimator_qnn": diff --git a/test/neural_networks/test_circuit_qnn.py b/test/neural_networks/test_circuit_qnn.py deleted file mode 100644 index f2fad56c3..000000000 --- a/test/neural_networks/test_circuit_qnn.py +++ /dev/null @@ -1,475 +0,0 @@ -# This code is part of a Qiskit project. -# -# (C) Copyright IBM 2018, 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 -# 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 Circuit QNN.""" -import itertools -import unittest -import warnings - -from test import QiskitMachineLearningTestCase - -from ddt import ddt, data, idata, unpack - -import numpy as np - -from qiskit.circuit import QuantumCircuit, Parameter -from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap -from qiskit.utils import QuantumInstance, algorithm_globals, optionals -from qiskit.providers.fake_provider import FakeToronto -from qiskit.transpiler import PassManagerConfig -from qiskit.transpiler.preset_passmanagers import level_1_pass_manager, level_2_pass_manager - -from qiskit_machine_learning import QiskitMachineLearningError -from qiskit_machine_learning.neural_networks import CircuitQNN -import qiskit_machine_learning.optionals as _optionals - -QASM = "qasm" -STATEVECTOR = "statevector" -CUSTOM_PASS_MANAGERS = "custom_pass_managers" - -SPARSE = [True, False] -SAMPLING = [True, False] -QUANTUM_INSTANCES = [STATEVECTOR, QASM, CUSTOM_PASS_MANAGERS] -INTERPRET_TYPES = [0, 1, 2] -BATCH_SIZES = [1, 2] - - -@ddt -class TestCircuitQNN(QiskitMachineLearningTestCase): - """Circuit QNN Tests.""" - - @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required to run this test") - def setUp(self): - super().setUp() - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = 12345 - from qiskit_aer import Aer, AerSimulator - - # specify "run configuration" - self.quantum_instance_sv = QuantumInstance( - Aer.get_backend("aer_simulator_statevector"), - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) - # pylint: disable=no-member - self.quantum_instance_qasm = QuantumInstance( - AerSimulator(), - shots=100, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) - self.quantum_instance_pm = QuantumInstance( - AerSimulator(), - shots=100, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - pass_manager=level_1_pass_manager(PassManagerConfig.from_backend(FakeToronto())), - bound_pass_manager=level_2_pass_manager(PassManagerConfig.from_backend(FakeToronto())), - ) - - # 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) - - # define interpret functions - def interpret_1d(x): - return sum((s == "1" for s in f"{x:0b}")) % 2 - - self.interpret_1d = interpret_1d - self.output_shape_1d = 2 # takes values in {0, 1} - - def interpret_2d(x): - return np.array([self.interpret_1d(x), 2 * self.interpret_1d(x)]) - - self.interpret_2d = interpret_2d - self.output_shape_2d = ( - 2, - 3, - ) # 1st dim. takes values in {0, 1} 2nd dim in {0, 1, 2} - - def tearDown(self) -> None: - super().tearDown() - warnings.filterwarnings("always", category=DeprecationWarning) - - def _get_qnn(self, sparse, sampling, quantum_instance_type, interpret_id): - """Construct QNN from configuration.""" - - # get quantum instance - if quantum_instance_type == STATEVECTOR: - quantum_instance = self.quantum_instance_sv - elif quantum_instance_type == QASM: - quantum_instance = self.quantum_instance_qasm - elif quantum_instance_type == CUSTOM_PASS_MANAGERS: - quantum_instance = self.quantum_instance_pm - else: - quantum_instance = None - - # 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 = CircuitQNN( - self.qc, - self.input_params, - self.weight_params, - sparse=sparse, - sampling=sampling, - interpret=interpret, - output_shape=output_shape, - quantum_instance=quantum_instance, - ) - return qnn - - def _verify_qnn( - self, - qnn: CircuitQNN, - quantum_instance_type: str, - batch_size: int, - ) -> None: - """ - Verifies that a QNN functions correctly - - Args: - qnn: a QNN to check - quantum_instance_type: - batch_size: - - Returns: - None. - """ - # pylint: disable=import-error - from sparse import SparseArray - - input_data = np.zeros((batch_size, qnn.num_inputs)) - weights = np.zeros(qnn.num_weights) - - # if sampling and statevector, make sure it fails - if quantum_instance_type == STATEVECTOR and qnn.sampling: - with self.assertRaises(QiskitMachineLearningError): - qnn.forward(input_data, weights) - else: - # evaluate QNN forward pass - result = qnn.forward(input_data, weights) - - # make sure forward result is sparse if it should be - if qnn.sparse and not qnn.sampling: - 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)) - - input_grad, weights_grad = qnn.backward(input_data, weights) - if qnn.sampling: - self.assertIsNone(input_grad) - self.assertIsNone(weights_grad) - else: - self.assertIsNone(input_grad) - self.assertEqual( - weights_grad.shape, (batch_size, *qnn.output_shape, qnn.num_weights) - ) - - # verify that input gradients are None if turned off - qnn.input_gradients = True - input_grad, weights_grad = qnn.backward(input_data, weights) - if qnn.sampling: - self.assertIsNone(input_grad) - self.assertIsNone(weights_grad) - else: - 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) - ) - - @unittest.skipIf(not _optionals.HAS_SPARSE, "Sparse not available.") - @idata(itertools.product(SPARSE, SAMPLING, QUANTUM_INSTANCES, INTERPRET_TYPES, BATCH_SIZES)) - @unpack - def test_circuit_qnn( - self, sparse: bool, sampling: bool, quantum_instance_type, interpret_type, batch_size - ): - """Circuit QNN Test.""" - qnn = self._get_qnn(sparse, sampling, quantum_instance_type, interpret_type) - self._verify_qnn(qnn, quantum_instance_type, batch_size) - - @data( - # sparse, sampling, quantum_instance_type, interpret (0=no, 1=1d, 2=2d), batch_size - (True, False, STATEVECTOR, 0, 1), - (True, False, STATEVECTOR, 0, 2), - (True, False, STATEVECTOR, 1, 1), - (True, False, STATEVECTOR, 1, 2), - (True, False, STATEVECTOR, 2, 1), - (True, False, STATEVECTOR, 2, 2), - (False, False, STATEVECTOR, 0, 1), - (False, False, STATEVECTOR, 0, 2), - (False, False, STATEVECTOR, 1, 1), - (False, False, STATEVECTOR, 1, 2), - (False, False, STATEVECTOR, 2, 1), - (False, False, STATEVECTOR, 2, 2), - ) - def test_circuit_qnn_gradient(self, config): - """Circuit QNN Gradient Test.""" - - # get configuration - sparse, sampling, quantum_instance_type, interpret_id, batch_size = config - if sparse and not _optionals.HAS_SPARSE: - self.skipTest("sparse library is required to run this test") - return - - # get QNN - qnn = self._get_qnn(sparse, sampling, quantum_instance_type, interpret_id) - 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) - if sparse: - grad = (f_1.todense() - f_2.todense()) / (2 * eps) - input_grad_ = ( - input_grad.todense() - .reshape((batch_size, -1, qnn.num_inputs))[:, :, k] - .reshape(grad.shape) - ) - diff = input_grad_ - grad - else: - 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) - if sparse: - grad = (f_1.todense() - f_2.todense()) / (2 * eps) - weights_grad_ = ( - weights_grad.todense() - .reshape((batch_size, -1, qnn.num_weights))[:, :, k] - .reshape(grad.shape) - ) - diff = weights_grad_ - grad - else: - 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) - - @unittest.skipIf(not _optionals.HAS_SPARSE, "Sparse not available.") - @idata(itertools.product(SPARSE, SAMPLING, QUANTUM_INSTANCES, INTERPRET_TYPES, BATCH_SIZES)) - @unpack - def test_no_quantum_instance( - self, - sparse: bool, - sampling: bool, - quantum_instance_type: str, - interpret_type: int, - batch_size: int, - ): - """Circuit QNN Test with and without QuantumInstance.""" - # get QNN with QuantumInstance - qnn_qi = self._get_qnn(sparse, sampling, quantum_instance_type, interpret_type) - - # get QNN without QuantumInstance - qnn_no_qi = self._get_qnn(sparse, sampling, None, interpret_type) - - with self.assertRaises(QiskitMachineLearningError): - qnn_no_qi.sample(input_data=None, weights=None) - - with self.assertRaises(QiskitMachineLearningError): - qnn_no_qi.probabilities(input_data=None, weights=None) - - with self.assertRaises(QiskitMachineLearningError): - qnn_no_qi.probability_gradients(input_data=None, weights=None) - - if quantum_instance_type == STATEVECTOR: - quantum_instance = self.quantum_instance_sv - elif quantum_instance_type == QASM: - quantum_instance = self.quantum_instance_qasm - elif quantum_instance_type == CUSTOM_PASS_MANAGERS: - quantum_instance = self.quantum_instance_pm - else: - # must never happen - quantum_instance = None - - qnn_no_qi.quantum_instance = quantum_instance - - self.assertEqual(qnn_qi.output_shape, qnn_no_qi.output_shape) - self._verify_qnn(qnn_no_qi, quantum_instance_type, batch_size) - - @data( - # interpret, output_shape, sparse, quantum_instance - (None, 1, False, "sv"), - (None, 1, True, "sv"), - (None, 1, False, "qasm"), - (None, 1, True, "qasm"), - ) - def test_warning1_circuit_qnn(self, config): - """Circuit QNN with no sampling and input/output shape 1/1 .""" - - interpret, output_shape, sparse, q_i = config - if sparse and not _optionals.HAS_SPARSE: - self.skipTest("sparse library is required to run this test") - return - - if q_i == "sv": - quantum_instance = self.quantum_instance_sv - elif q_i == "qasm": - quantum_instance = self.quantum_instance_qasm - else: - raise ValueError("Unsupported quantum instance") - - qc = QuantumCircuit(1) - - # construct simple feature map - param_x = Parameter("x") - qc.ry(param_x, 0) - - # construct simple feature map - param_y = Parameter("y") - qc.ry(param_y, 0) - - # check warning when output_shape defined without interpret - with self.assertLogs(level="WARNING") as w_1: - CircuitQNN( - qc, - [param_x], - [param_y], - sparse=sparse, - sampling=False, - interpret=interpret, - output_shape=output_shape, - quantum_instance=quantum_instance, - input_gradients=True, - ) - self.assertEqual( - w_1.output, - [ - "WARNING:qiskit_machine_learning.neural_networks.circuit_qnn:No " - "interpret function given, output_shape will be automatically " - "determined as 2^num_qubits." - ], - ) - - @data( - # interpret, output_shape, sparse, quantum_instance - (None, 1, False, "qasm"), - (None, 1, True, "qasm"), - (lambda x: np.sum(x) % 2, 2, True, "qasm"), - (lambda x: np.sum(x) % 2, 2, False, "qasm"), - ) - def test_warning2_circuit_qnn(self, config): - """Torch Connector + Circuit QNN with sampling and input/output shape 1/1 .""" - - interpret, output_shape, sparse, q_i = config - if q_i == "sv": - quantum_instance = self.quantum_instance_sv - elif q_i == "qasm": - quantum_instance = self.quantum_instance_qasm - else: - raise ValueError("Unsupported quantum instance") - - qc = QuantumCircuit(2) - - # construct simple feature map - param_x = Parameter("x") - qc.ry(param_x, 0) - - # construct simple feature map - param_y = Parameter("y") - qc.ry(param_y, 0) - - # check warning when sampling true - with self.assertLogs(level="WARNING") as w_2: - CircuitQNN( - qc, - [param_x], - [param_y], - sparse=sparse, - sampling=True, - interpret=interpret, - output_shape=output_shape, - quantum_instance=quantum_instance, - input_gradients=True, - ) - - self.assertEqual( - w_2.output, - [ - "WARNING:qiskit_machine_learning.neural_networks.circuit_qnn:" - "In sampling mode, output_shape will be automatically inferred " - "from the number of samples and interpret function, if provided." - ], - ) - - def test_delayed_gradient_initialization(self): - """Test delayed gradient initialization.""" - qc = QuantumCircuit(1) - input_param = Parameter("x") - qc.ry(input_param, 0) - - weight_param = Parameter("w") - qc.rx(weight_param, 0) - - # define QNN - qnn = CircuitQNN( - qc, [input_param], [weight_param], quantum_instance=self.quantum_instance_sv - ) - self.assertIsNone(qnn._gradient_circuit) - - qnn.backward(np.asarray([1]), np.asarray([1])) - grad_qc1 = qnn._gradient_circuit - self.assertIsNotNone(grad_qc1) - - qnn.input_gradients = True - self.assertIsNone(qnn._gradient_circuit) - qnn.backward(np.asarray([1]), np.asarray([1])) - grad_qc2 = qnn._gradient_circuit - self.assertIsNotNone(grad_qc1) - self.assertNotEqual(grad_qc1, grad_qc2) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/neural_networks/test_circuit_vs_sampler_qnn.py b/test/neural_networks/test_circuit_vs_sampler_qnn.py deleted file mode 100644 index 4215ef1ae..000000000 --- a/test/neural_networks/test_circuit_vs_sampler_qnn.py +++ /dev/null @@ -1,118 +0,0 @@ -# This code is part of a Qiskit project. -# -# (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 -# 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.""" -import warnings - -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 TestCircuitQNNvsSamplerQNN(QiskitMachineLearningTestCase): - """Circuit vs Sampler QNN Tests. To be removed once CircuitQNN is deprecated""" - - def setUp(self): - super().setUp() - warnings.filterwarnings("ignore", category=DeprecationWarning) - - 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() - - def tearDown(self) -> None: - super().tearDown() - warnings.filterwarnings("always", category=DeprecationWarning) - - @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) diff --git a/test/neural_networks/test_effective_dimension.py b/test/neural_networks/test_effective_dimension.py index 79073b7c8..5e2a3fc50 100644 --- a/test/neural_networks/test_effective_dimension.py +++ b/test/neural_networks/test_effective_dimension.py @@ -12,7 +12,6 @@ """ Unit Tests for Effective Dimension Algorithm """ import unittest -import warnings from test import QiskitMachineLearningTestCase @@ -21,13 +20,13 @@ from qiskit.circuit import QuantumCircuit from qiskit.circuit.library import ZFeatureMap, RealAmplitudes -from qiskit.utils import QuantumInstance, algorithm_globals, optionals +from qiskit.utils import algorithm_globals -from qiskit.opflow import PauliSumOp -from qiskit_machine_learning.neural_networks import TwoLayerQNN, CircuitQNN from qiskit_machine_learning.neural_networks import ( EffectiveDimension, LocalEffectiveDimension, + EstimatorQNN, + SamplerQNN, ) from qiskit_machine_learning import QiskitMachineLearningError @@ -36,26 +35,17 @@ class TestEffectiveDimension(QiskitMachineLearningTestCase): """Test the Effective Dimension algorithm""" - @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required to run this test") def setUp(self): super().setUp() - warnings.filterwarnings("ignore", category=DeprecationWarning) algorithm_globals.random_seed = 1234 - from qiskit_aer import Aer - - qi_sv = QuantumInstance( - Aer.get_backend("aer_simulator_statevector"), - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) # set up quantum neural networks num_qubits = 3 feature_map = ZFeatureMap(feature_dimension=num_qubits, reps=1) ansatz = RealAmplitudes(num_qubits, reps=1) - # CircuitQNNs + # SamplerQNNs qc = QuantumCircuit(num_qubits) qc.append(feature_map, range(num_qubits)) qc.append(ansatz, range(num_qubits)) @@ -63,51 +53,43 @@ def setUp(self): def parity(x): return f"{x:b}".count("1") % 2 - circuit_qnn_1 = CircuitQNN( - qc, + sampler_qnn_1 = SamplerQNN( + circuit=qc, input_params=feature_map.parameters, weight_params=ansatz.parameters, interpret=parity, output_shape=2, - sparse=False, - quantum_instance=qi_sv, ) - # qnn2 for checking result without parity - circuit_qnn_2 = CircuitQNN( - qc, + sampler_qnn_2 = SamplerQNN( + circuit=qc, input_params=feature_map.parameters, weight_params=ansatz.parameters, - sparse=False, - quantum_instance=qi_sv, ) - # OpflowQNN - observable = PauliSumOp.from_list([("Z" * num_qubits, 1)]) - opflow_qnn = TwoLayerQNN( - num_qubits, - feature_map=feature_map, - ansatz=ansatz, - observable=observable, - quantum_instance=qi_sv, + # EstimatorQNN + estimator_qnn = EstimatorQNN( + circuit=qc, + input_params=feature_map.parameters, + weight_params=ansatz.parameters, ) - self.qnns = {"circuit1": circuit_qnn_1, "circuit2": circuit_qnn_2, "opflow": opflow_qnn} + self.qnns = { + "sampler_qnn_1": sampler_qnn_1, + "sampler_qnn_2": sampler_qnn_2, + "estimator_qnn": estimator_qnn, + } # define sample numbers self.n_list = [5000, 8000, 10000, 40000, 60000, 100000, 150000, 200000, 500000, 1000000] self.n = 5000 - def tearDown(self) -> None: - super().tearDown() - warnings.filterwarnings("always", category=DeprecationWarning) - @data( # qnn_name, num_inputs, num_weights, result - ("circuit1", 10, 10, 4.51202148), - ("circuit1", 1, 1, 1.39529449), - ("circuit1", 10, 1, 3.97371533), - ("circuit2", 10, 10, 5.90859124), + ("sampler_qnn_1", 10, 10, 4.51202148), + ("sampler_qnn_1", 1, 1, 1.39529449), + ("sampler_qnn_1", 10, 1, 3.97371533), + ("sampler_qnn_2", 10, 10, 5.90859124), ) @unpack def test_alg_results(self, qnn_name, num_inputs, num_params, result): @@ -120,11 +102,11 @@ def test_alg_results(self, qnn_name, num_inputs, num_params, result): self.assertAlmostEqual(effdim, result, 5) def test_qnn_type(self): - """Test that the results are equivalent for opflow and circuit qnn.""" + """Test that the results are equivalent for EstimatorQNN and SamplerQNN.""" num_input_samples, num_weight_samples = 1, 1 - qnn1 = self.qnns["circuit1"] - qnn2 = self.qnns["opflow"] + qnn1 = self.qnns["sampler_qnn_1"] + qnn2 = self.qnns["estimator_qnn"] global_ed1 = EffectiveDimension( qnn=qnn1, @@ -148,7 +130,7 @@ def test_multiple_data(self): """Test results for a list of sampling sizes.""" num_input_samples, num_weight_samples = 10, 10 - qnn = self.qnns["circuit1"] + qnn = self.qnns["sampler_qnn_1"] global_ed1 = EffectiveDimension( qnn=qnn, @@ -164,7 +146,7 @@ def test_multiple_data(self): def test_inputs(self): """Test results for different input combinations.""" - qnn = self.qnns["circuit1"] + qnn = self.qnns["sampler_qnn_1"] num_input_samples, num_weight_samples = 10, 10 inputs = algorithm_globals.random.uniform(0, 1, size=(num_input_samples, qnn.num_inputs)) @@ -190,7 +172,7 @@ def test_inputs(self): def test_inputs_shapes(self): """Test results for different input combinations.""" - qnn = self.qnns["circuit1"] + qnn = self.qnns["sampler_qnn_1"] num_inputs, num_params = 10, 10 inputs_ok = algorithm_globals.random.uniform(0, 1, size=(num_inputs, qnn.num_inputs)) @@ -216,7 +198,7 @@ def test_inputs_shapes(self): def test_local_ed_params(self): """Test that QiskitMachineLearningError is raised for wrong parameters sizes.""" - qnn = self.qnns["circuit1"] + qnn = self.qnns["sampler_qnn_1"] num_inputs, num_params = 10, 10 inputs_ok = algorithm_globals.random.uniform(0, 1, size=(num_inputs, qnn.num_inputs)) diff --git a/test/neural_networks/test_effective_dimension_primitives.py b/test/neural_networks/test_effective_dimension_primitives.py deleted file mode 100644 index 22292d547..000000000 --- a/test/neural_networks/test_effective_dimension_primitives.py +++ /dev/null @@ -1,230 +0,0 @@ -# This code is part of a Qiskit project. -# -# (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 -# 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. -""" Unit Tests for Effective Dimension Algorithm """ - -import unittest - -from test import QiskitMachineLearningTestCase - -import numpy as np -from ddt import ddt, data, unpack - -from qiskit.circuit import QuantumCircuit -from qiskit.circuit.library import ZFeatureMap, RealAmplitudes -from qiskit.utils import algorithm_globals - -from qiskit_machine_learning.neural_networks import ( - EffectiveDimension, - LocalEffectiveDimension, - EstimatorQNN, - SamplerQNN, -) -from qiskit_machine_learning import QiskitMachineLearningError - - -@ddt -class TestEffectiveDimension(QiskitMachineLearningTestCase): - """Test the Effective Dimension algorithm""" - - def setUp(self): - super().setUp() - - algorithm_globals.random_seed = 1234 - - # set up quantum neural networks - num_qubits = 3 - feature_map = ZFeatureMap(feature_dimension=num_qubits, reps=1) - ansatz = RealAmplitudes(num_qubits, reps=1) - - # CircuitQNNs - qc = QuantumCircuit(num_qubits) - qc.append(feature_map, range(num_qubits)) - qc.append(ansatz, range(num_qubits)) - - def parity(x): - return f"{x:b}".count("1") % 2 - - sampler_qnn_1 = SamplerQNN( - circuit=qc, - input_params=feature_map.parameters, - weight_params=ansatz.parameters, - interpret=parity, - output_shape=2, - ) - - sampler_qnn_2 = SamplerQNN( - circuit=qc, - input_params=feature_map.parameters, - weight_params=ansatz.parameters, - ) - - # EstimatorQNN - estimator_qnn = EstimatorQNN( - circuit=qc, - input_params=feature_map.parameters, - weight_params=ansatz.parameters, - ) - - self.qnns = { - "sampler_qnn_1": sampler_qnn_1, - "sampler_qnn_2": sampler_qnn_2, - "estimator_qnn": estimator_qnn, - } - - # define sample numbers - self.n_list = [5000, 8000, 10000, 40000, 60000, 100000, 150000, 200000, 500000, 1000000] - self.n = 5000 - - @data( - # qnn_name, num_inputs, num_weights, result - ("sampler_qnn_1", 10, 10, 4.51202148), - ("sampler_qnn_1", 1, 1, 1.39529449), - ("sampler_qnn_1", 10, 1, 3.97371533), - ("sampler_qnn_2", 10, 10, 5.90859124), - ) - @unpack - def test_alg_results(self, qnn_name, num_inputs, num_params, result): - """Test that the algorithm results match the original code's.""" - qnn = self.qnns[qnn_name] - global_ed = EffectiveDimension(qnn=qnn, weight_samples=num_params, input_samples=num_inputs) - - effdim = global_ed.get_effective_dimension(self.n) - - self.assertAlmostEqual(effdim, result, 5) - - def test_qnn_type(self): - """Test that the results are equivalent for opflow and circuit qnn.""" - - num_input_samples, num_weight_samples = 1, 1 - qnn1 = self.qnns["sampler_qnn_1"] - qnn2 = self.qnns["estimator_qnn"] - - global_ed1 = EffectiveDimension( - qnn=qnn1, - weight_samples=num_weight_samples, - input_samples=num_input_samples, - ) - - global_ed2 = EffectiveDimension( - qnn=qnn2, - weight_samples=num_weight_samples, - input_samples=num_input_samples, - ) - - effdim1 = global_ed1.get_effective_dimension(self.n) - effdim2 = global_ed2.get_effective_dimension(self.n) - - self.assertAlmostEqual(effdim1, 1.395, 3) - self.assertAlmostEqual(effdim1, effdim2, 5) - - def test_multiple_data(self): - """Test results for a list of sampling sizes.""" - - num_input_samples, num_weight_samples = 10, 10 - qnn = self.qnns["sampler_qnn_1"] - - global_ed1 = EffectiveDimension( - qnn=qnn, - weight_samples=num_weight_samples, - input_samples=num_input_samples, - ) - - effdim1 = global_ed1.get_effective_dimension(self.n_list) - effdim2 = global_ed1.get_effective_dimension(np.asarray(self.n_list)) - - np.testing.assert_array_equal(effdim1, effdim2) - - def test_inputs(self): - """Test results for different input combinations.""" - - qnn = self.qnns["sampler_qnn_1"] - - num_input_samples, num_weight_samples = 10, 10 - inputs = algorithm_globals.random.uniform(0, 1, size=(num_input_samples, qnn.num_inputs)) - weights = algorithm_globals.random.uniform(0, 1, size=(num_weight_samples, qnn.num_weights)) - - global_ed1 = EffectiveDimension( - qnn=qnn, - weight_samples=num_weight_samples, - input_samples=num_input_samples, - ) - - global_ed2 = EffectiveDimension( - qnn=qnn, - weight_samples=weights, - input_samples=inputs, - ) - - effdim1 = global_ed1.get_effective_dimension(self.n_list) - effdim2 = global_ed2.get_effective_dimension(self.n_list) - - np.testing.assert_array_almost_equal(effdim1, effdim2, 0.2) - - def test_inputs_shapes(self): - """Test results for different input combinations.""" - - qnn = self.qnns["sampler_qnn_1"] - - num_inputs, num_params = 10, 10 - inputs_ok = algorithm_globals.random.uniform(0, 1, size=(num_inputs, qnn.num_inputs)) - weights_ok = algorithm_globals.random.uniform(0, 1, size=(num_params, qnn.num_weights)) - - inputs_wrong = algorithm_globals.random.uniform(0, 1, size=(num_inputs, 1)) - weights_wrong = algorithm_globals.random.uniform(0, 1, size=(num_params, 1)) - - with self.assertRaises(QiskitMachineLearningError): - EffectiveDimension( - qnn=qnn, - weight_samples=weights_ok, - input_samples=inputs_wrong, - ) - - with self.assertRaises(QiskitMachineLearningError): - EffectiveDimension( - qnn=qnn, - weight_samples=weights_wrong, - input_samples=inputs_ok, - ) - - def test_local_ed_params(self): - """Test that QiskitMachineLearningError is raised for wrong parameters sizes.""" - - qnn = self.qnns["sampler_qnn_1"] - - num_inputs, num_params = 10, 10 - inputs_ok = algorithm_globals.random.uniform(0, 1, size=(num_inputs, qnn.num_inputs)) - weights_ok = algorithm_globals.random.uniform(0, 1, size=(1, qnn.num_weights)) - weights_ok2 = algorithm_globals.random.uniform(0, 1, size=qnn.num_weights) - weights_wrong = algorithm_globals.random.uniform(0, 1, size=(num_params, qnn.num_weights)) - - LocalEffectiveDimension( - qnn=qnn, - weight_samples=weights_ok, - input_samples=inputs_ok, - ) - - LocalEffectiveDimension( - qnn=qnn, - weight_samples=weights_ok2, - input_samples=inputs_ok, - ) - - with self.assertRaises(QiskitMachineLearningError): - LocalEffectiveDimension( - qnn=qnn, - weight_samples=weights_wrong, - input_samples=inputs_ok, - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/neural_networks/test_estimator_qnn.py b/test/neural_networks/test_estimator_qnn.py index f72d5c6fd..b1bcd810f 100644 --- a/test/neural_networks/test_estimator_qnn.py +++ b/test/neural_networks/test_estimator_qnn.py @@ -171,7 +171,7 @@ class TestEstimatorQNN(QiskitMachineLearningTestCase): - """EstimatorQNN Tests. The correct references is obtained from OpflowQNN""" + """EstimatorQNN Tests. The correct references is obtained from EstimatorQNN""" def _test_network_passes( self, diff --git a/test/neural_networks/test_opflow_qnn.py b/test/neural_networks/test_opflow_qnn.py deleted file mode 100644 index 578a4cc1c..000000000 --- a/test/neural_networks/test_opflow_qnn.py +++ /dev/null @@ -1,357 +0,0 @@ -# This code is part of a Qiskit project. -# -# (C) Copyright IBM 2018, 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 -# 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 Opflow QNN """ -import warnings -from typing import List - -from test import QiskitMachineLearningTestCase - -import unittest -from ddt import ddt, data - -import numpy as np - -from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.opflow import PauliExpectation, Gradient, StateFn, PauliSumOp, ListOp -from qiskit.utils import QuantumInstance, algorithm_globals, optionals - - -from qiskit_machine_learning.neural_networks import OpflowQNN - -QASM = "qasm" -STATEVECTOR = "sv" - - -@ddt -class TestOpflowQNN(QiskitMachineLearningTestCase): - """Opflow QNN Tests.""" - - @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required to run this test") - def setUp(self): - super().setUp() - warnings.filterwarnings("ignore", category=DeprecationWarning) - - algorithm_globals.random_seed = 12345 - from qiskit_aer import Aer, AerSimulator - - # specify quantum instances - self.sv_quantum_instance = QuantumInstance( - Aer.get_backend("aer_simulator_statevector"), - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) - # pylint: disable=no-member - self.qasm_quantum_instance = QuantumInstance( - AerSimulator(), - shots=100, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) - np.random.seed(algorithm_globals.random_seed) - - def tearDown(self) -> None: - super().tearDown() - warnings.filterwarnings("always", category=DeprecationWarning) - - def validate_output_shape(self, qnn: OpflowQNN, test_data: List[np.ndarray]) -> None: - """ - Asserts that the opflow qnn returns results of the correct output shape. - - Args: - qnn: QNN to be tested - test_data: list of test input arrays - - Raises: - QiskitMachineLearningError: Invalid input. - """ - - # get weights - weights = np.random.rand(qnn.num_weights) - - # iterate over test data and validate behavior of model - for x in test_data: - - # evaluate network - forward_shape = qnn.forward(x, weights).shape - input_grad, weights_grad = qnn.backward(x, weights) - if qnn.input_gradients: - backward_shape_input = input_grad.shape - backward_shape_weights = weights_grad.shape - - # derive batch shape form input - batch_shape = x.shape[: -len(qnn.output_shape)] - if len(batch_shape) == 0: - batch_shape = (1,) - - # compare results and assert that the behavior is equal - self.assertEqual(forward_shape, (*batch_shape, *qnn.output_shape)) - if qnn.input_gradients: - self.assertEqual( - backward_shape_input, - (*batch_shape, *qnn.output_shape, qnn.num_inputs), - ) - else: - self.assertIsNone(input_grad) - self.assertEqual( - backward_shape_weights, - (*batch_shape, *qnn.output_shape, qnn.num_weights), - ) - - @data( - (STATEVECTOR, True), - (STATEVECTOR, False), - (QASM, True), - (QASM, False), - (None, True), - (None, False), - ) - def test_opflow_qnn_1_1(self, config): - """Test Opflow QNN with input/output dimension 1/1.""" - q_i, input_grad_required = config - - if q_i == STATEVECTOR: - quantum_instance = self.sv_quantum_instance - elif q_i == QASM: - quantum_instance = self.qasm_quantum_instance - else: - quantum_instance = None - - # specify how to evaluate expected values and gradients - expval = PauliExpectation() - gradient = Gradient() - - # construct parametrized circuit - params = [Parameter("input1"), Parameter("weight1")] - qc = QuantumCircuit(1) - qc.h(0) - qc.ry(params[0], 0) - qc.rx(params[1], 0) - qc_sfn = StateFn(qc) - - # construct cost operator - cost_operator = StateFn(PauliSumOp.from_list([("Z", 1.0), ("X", 1.0)])) - - # combine operator and circuit to objective function - op = ~cost_operator @ qc_sfn - - # define QNN - qnn = OpflowQNN( - op, - [params[0]], - [params[1]], - expval, - gradient, - quantum_instance=quantum_instance, - ) - qnn.input_gradients = input_grad_required - - test_data = [ - np.array(1), - np.array([1]), - np.array([[1], [2]]), - np.array([[[1], [2]], [[3], [4]]]), - ] - - # test model - self.validate_output_shape(qnn, test_data) - - # test the qnn after we set a quantum instance - if quantum_instance is None: - qnn.quantum_instance = self.qasm_quantum_instance - self.validate_output_shape(qnn, test_data) - - @data( - (STATEVECTOR, True), - (STATEVECTOR, False), - (QASM, True), - (QASM, False), - (None, True), - (None, False), - ) - def test_opflow_qnn_2_1(self, config): - """Test Opflow QNN with input/output dimension 2/1.""" - q_i, input_grad_required = config - - # construct QNN - if q_i == STATEVECTOR: - quantum_instance = self.sv_quantum_instance - elif q_i == QASM: - quantum_instance = self.qasm_quantum_instance - else: - quantum_instance = None - - # specify how to evaluate expected values and gradients - expval = PauliExpectation() - gradient = Gradient() - - # construct parametrized circuit - params = [ - Parameter("input1"), - Parameter("input2"), - Parameter("weight1"), - Parameter("weight2"), - ] - qc = QuantumCircuit(2) - qc.h(0) - qc.ry(params[0], 0) - qc.ry(params[1], 1) - qc.rx(params[2], 0) - qc.rx(params[3], 1) - qc_sfn = StateFn(qc) - - # construct cost operator - cost_operator = StateFn(PauliSumOp.from_list([("ZZ", 1.0), ("XX", 1.0)])) - - # combine operator and circuit to objective function - op = ~cost_operator @ qc_sfn - - # define QNN - qnn = OpflowQNN( - op, - params[:2], - params[2:], - expval, - gradient, - quantum_instance=quantum_instance, - ) - qnn.input_gradients = input_grad_required - - test_data = [np.array([1, 2]), np.array([[1, 2]]), np.array([[1, 2], [3, 4]])] - - # test model - self.validate_output_shape(qnn, test_data) - - # test the qnn after we set a quantum instance - if quantum_instance is None: - qnn.quantum_instance = self.qasm_quantum_instance - self.validate_output_shape(qnn, test_data) - - @data( - (STATEVECTOR, True), - (STATEVECTOR, False), - (QASM, True), - (QASM, False), - (None, True), - (None, False), - ) - def test_opflow_qnn_2_2(self, config): - """Test Opflow QNN with input/output dimension 2/2.""" - q_i, input_grad_required = config - - if q_i == STATEVECTOR: - quantum_instance = self.sv_quantum_instance - elif q_i == QASM: - quantum_instance = self.qasm_quantum_instance - else: - quantum_instance = None - - # construct parametrized circuit - params_1 = [Parameter("input1"), Parameter("weight1")] - qc_1 = QuantumCircuit(1) - qc_1.h(0) - qc_1.ry(params_1[0], 0) - qc_1.rx(params_1[1], 0) - qc_sfn_1 = StateFn(qc_1) - - # construct cost operator - h_1 = StateFn(PauliSumOp.from_list([("Z", 1.0), ("X", 1.0)])) - - # combine operator and circuit to objective function - op_1 = ~h_1 @ qc_sfn_1 - - # construct parametrized circuit - params_2 = [Parameter("input2"), Parameter("weight2")] - qc_2 = QuantumCircuit(1) - qc_2.h(0) - qc_2.ry(params_2[0], 0) - qc_2.rx(params_2[1], 0) - qc_sfn_2 = StateFn(qc_2) - - # construct cost operator - h_2 = StateFn(PauliSumOp.from_list([("Z", 1.0), ("X", 1.0)])) - - # combine operator and circuit to objective function - op_2 = ~h_2 @ qc_sfn_2 - - op = ListOp([op_1, op_2]) - - qnn = OpflowQNN( - op, - [params_1[0], params_2[0]], - [params_1[1], params_2[1]], - quantum_instance=quantum_instance, - ) - qnn.input_gradients = input_grad_required - - test_data = [np.array([1, 2]), np.array([[1, 2], [3, 4]])] - - # test model - self.validate_output_shape(qnn, test_data) - - # test the qnn after we set a quantum instance - if quantum_instance is None: - qnn.quantum_instance = self.qasm_quantum_instance - self.validate_output_shape(qnn, test_data) - - def test_composed_op(self): - """Tests OpflowQNN with ComposedOp as an operator.""" - qc = QuantumCircuit(1) - param = Parameter("param") - qc.rz(param, 0) - - h_1 = PauliSumOp.from_list([("Z", 1.0)]) - h_2 = PauliSumOp.from_list([("Z", 1.0)]) - - h_op = ListOp([h_1, h_2]) - op = ~StateFn(h_op) @ StateFn(qc) - - # initialize QNN - qnn = OpflowQNN(op, [], [param]) - - # create random data and weights for testing - input_data = np.random.rand(2, qnn.num_inputs) - weights = np.random.rand(qnn.num_weights) - - qnn.forward(input_data, weights) - qnn.backward(input_data, weights) - - def test_delayed_gradient_initialization(self): - """Test delayed gradient initialization.""" - qc = QuantumCircuit(1) - input_param = Parameter("x") - qc.ry(input_param, 0) - - weight_param = Parameter("w") - qc.rx(weight_param, 0) - - observable = StateFn(PauliSumOp.from_list([("Z", 1)])) - op = ~observable @ StateFn(qc) - - # define QNN - qnn = OpflowQNN(op, [input_param], [weight_param]) - self.assertIsNone(qnn._gradient_operator) - - qnn.backward(np.asarray([1]), np.asarray([1])) - grad_op1 = qnn._gradient_operator - self.assertIsNotNone(grad_op1) - - qnn.input_gradients = True - self.assertIsNone(qnn._gradient_operator) - qnn.backward(np.asarray([1]), np.asarray([1])) - grad_op2 = qnn._gradient_operator - self.assertIsNotNone(grad_op1) - self.assertNotEqual(grad_op1, grad_op2) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/neural_networks/test_two_layer_qnn.py b/test/neural_networks/test_two_layer_qnn.py deleted file mode 100644 index d1557b38e..000000000 --- a/test/neural_networks/test_two_layer_qnn.py +++ /dev/null @@ -1,171 +0,0 @@ -# This code is part of a Qiskit project. -# -# (C) Copyright IBM 2018, 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 -# 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 Two Layer QNN.""" - -import unittest -import warnings - -from test import QiskitMachineLearningTestCase - -import numpy as np - -from ddt import ddt, data -from qiskit import QuantumCircuit -from qiskit.circuit.library import RealAmplitudes, ZFeatureMap, ZZFeatureMap -from qiskit.utils import QuantumInstance, algorithm_globals, optionals - -from qiskit_machine_learning import QiskitMachineLearningError -from qiskit_machine_learning.neural_networks import TwoLayerQNN - - -@ddt -class TestTwoLayerQNN(QiskitMachineLearningTestCase): - """Two Layer QNN Tests.""" - - @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required to run this test") - def setUp(self): - super().setUp() - warnings.filterwarnings("ignore", category=DeprecationWarning) - algorithm_globals.random_seed = 12345 - from qiskit_aer import Aer - - # specify "run configuration" - self.quantum_instance = QuantumInstance( - Aer.get_backend("aer_simulator_statevector"), - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) - - # define QNN - num_qubits = 2 - feature_map = ZZFeatureMap(num_qubits) - ansatz = RealAmplitudes(num_qubits, reps=1) - self.qnn = TwoLayerQNN( - num_qubits, - feature_map=feature_map, - ansatz=ansatz, - quantum_instance=self.quantum_instance, - ) - - self.qnn_no_qi = TwoLayerQNN(num_qubits, feature_map=feature_map, ansatz=ansatz) - - def tearDown(self) -> None: - super().tearDown() - warnings.filterwarnings("always", category=DeprecationWarning) - - @data( - ("qi", True), - ("no_qi", True), - ("qi", False), - ("no_qi", False), - ) - def test_qnn_simple_new(self, config): - """Simple Opflow QNN Test for a specified neural network.""" - qnn_type, input_grad_required = config - - input_data = np.zeros(self.qnn.num_inputs) - weights = np.zeros(self.qnn.num_weights) - - if qnn_type == "qi": - qnn = self.qnn - else: - qnn = self.qnn_no_qi - qnn.input_gradients = input_grad_required - - # test forward pass - result = qnn.forward(input_data, weights) - self.assertEqual(result.shape, (1, *qnn.output_shape)) - - # test backward pass - result = qnn.backward(input_data, weights) - # batch dimension of 1 - if qnn.input_gradients: - self.assertEqual(result[0].shape, (1, *qnn.output_shape, qnn.num_inputs)) - else: - self.assertIsNone(result[0]) - - self.assertEqual(result[1].shape, (1, *qnn.output_shape, qnn.num_weights)) - - @data( - ("qi", True), - ("no_qi", True), - ("qi", False), - ("no_qi", False), - ) - def _test_qnn_batch(self, config): - """Batched Opflow QNN Test for the specified network.""" - qnn_type, input_grad_required = config - - batch_size = 10 - - input_data = np.arange(batch_size * self.qnn.num_inputs).reshape( - (batch_size, self.qnn.num_inputs) - ) - weights = np.zeros(self.qnn.num_weights) - - if qnn_type == "qi": - qnn = self.qnn - else: - qnn = self.qnn_no_qi - qnn.input_gradients = input_grad_required - - # test forward pass - result = qnn.forward(input_data, weights) - self.assertEqual(result.shape, (batch_size, *qnn.output_shape)) - - # test backward pass - result = qnn.backward(input_data, weights) - if qnn.input_gradients: - self.assertEqual(result[0].shape, (batch_size, *qnn.output_shape, qnn.num_inputs)) - else: - self.assertIsNone(result[0]) - - self.assertEqual(result[1].shape, (batch_size, *qnn.output_shape, qnn.num_weights)) - - @data(1, 2) - def test_default_construction(self, num_features): - """Test the default construction for 1 feature and more than 1 feature.""" - qnn = TwoLayerQNN(num_features) - - with self.subTest(msg="Check ansatz"): - self.assertIsInstance(qnn.ansatz, RealAmplitudes) - - with self.subTest(msg="Check feature map"): - expected_cls = ZZFeatureMap if num_features > 1 else ZFeatureMap - self.assertIsInstance(qnn.feature_map, expected_cls) - - def test_circuit_extensions(self): - """Test TwoLayerQNN when the number of qubits is different compared to - the feature map/ansatz.""" - num_qubits = 2 - classifier = TwoLayerQNN( - num_qubits=num_qubits, - feature_map=ZFeatureMap(1), - ansatz=RealAmplitudes(1), - quantum_instance=self.quantum_instance, - ) - self.assertEqual(classifier.feature_map.num_qubits, num_qubits) - self.assertEqual(classifier.ansatz.num_qubits, num_qubits) - - qc = QuantumCircuit(1) - with self.assertRaises(QiskitMachineLearningError): - _ = TwoLayerQNN( - num_qubits=num_qubits, - feature_map=qc, - ansatz=qc, - quantum_instance=self.quantum_instance, - ) - - -if __name__ == "__main__": - unittest.main()