Source code for qiskit_machine_learning.neural_networks.estimator_qnn

# This code is part of a Qiskit project.
#
# (C) Copyright IBM 2022, 2024.
#
# 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.

"""Estimator quantum neural network class"""

from __future__ import annotations

import logging
from copy import copy
from typing import Sequence

import numpy as np
from qiskit.circuit import Parameter, QuantumCircuit
from qiskit.primitives import BaseEstimator, Estimator, EstimatorResult
from qiskit.quantum_info import SparsePauliOp
from qiskit.quantum_info.operators.base_operator import BaseOperator
from qiskit_algorithms.gradients import (
    BaseEstimatorGradient,
    EstimatorGradientResult,
    ParamShiftEstimatorGradient,
)

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

from .neural_network import NeuralNetwork

logger = logging.getLogger(__name__)


[docs]class EstimatorQNN(NeuralNetwork): """A neural network implementation based on the Estimator primitive. The ``EstimatorQNN`` is a neural network that takes in a parametrized quantum circuit with designated parameters for input data and/or weights, an optional observable(s) and outputs their expectation value(s). Quite often, a combined quantum circuit is used. Such a circuit is built from two circuits: a feature map, it provides input parameters for the network, and an ansatz (weight parameters). In this case a :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` can be passed as circuit to simplify the composition of a feature map and ansatz. If a :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` is passed as circuit, the input and weight parameters do not have to be provided, because these two properties are taken from the :class:`~qiskit_machine_learning.circuit.library.QNNCircuit`. Example: .. code-block:: from qiskit import QuantumCircuit from qiskit.circuit.library import ZZFeatureMap, RealAmplitudes from qiskit_machine_learning.circuit.library import QNNCircuit from qiskit_machine_learning.neural_networks import EstimatorQNN num_qubits = 2 # Using the QNNCircuit: # Create a parameterized 2 qubit circuit composed of the default ZZFeatureMap feature map # and RealAmplitudes ansatz. qnn_qc = QNNCircuit(num_qubits) qnn = EstimatorQNN( circuit=qnn_qc ) qnn.forward(input_data=[1, 2], weights=[1, 2, 3, 4, 5, 6, 7, 8]) # Explicitly specifying the ansatz and feature map: feature_map = ZZFeatureMap(feature_dimension=num_qubits) ansatz = RealAmplitudes(num_qubits=num_qubits) 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 ) qnn.forward(input_data=[1, 2], weights=[1, 2, 3, 4, 5, 6, 7, 8]) The following attributes can be set via the constructor but can also be read and updated once the EstimatorQNN object has been constructed. Attributes: estimator (BaseEstimator): The estimator primitive used to compute the neural network's results. gradient (BaseEstimatorGradient): The estimator gradient to be used for the backward pass. """ def __init__( self, *, circuit: QuantumCircuit, estimator: BaseEstimator | None = None, observables: Sequence[BaseOperator] | BaseOperator | None = None, input_params: Sequence[Parameter] | None = None, weight_params: Sequence[Parameter] | None = None, gradient: BaseEstimatorGradient | None = None, input_gradients: bool = False, ): r""" Args: estimator: The estimator used to compute neural network's results. If ``None``, a default instance of the reference estimator, :class:`~qiskit.primitives.Estimator`, will be used. circuit: The quantum circuit to represent the neural network. If a :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` is passed, the `input_params` and `weight_params` do not have to be provided, because these two properties are taken from the :class:`~qiskit_machine_learning.circuit.library.QNNCircuit`. observables: The observables for outputs of the neural network. If ``None``, use the default :math:`Z^{\otimes num\_qubits}` observable. input_params: The parameters that correspond to the input data of the network. If ``None``, the input data is not bound to any parameters. If a :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` is provided the `input_params` value here is ignored. Instead the value is taken from the :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` input_parameters. weight_params: The parameters that correspond to the trainable weights. If ``None``, the weights are not bound to any parameters. If a :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` is provided the `weight_params` value here is ignored. Instead the value is taken from the :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` weight_parameters. gradient: The estimator gradient to be used for the backward pass. If None, a default instance of the estimator gradient, :class:`~qiskit_algorithms.gradients.ParamShiftEstimatorGradient`, will be used. 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: Invalid parameter values. """ if estimator is None: estimator = Estimator() self.estimator = estimator self._org_circuit = circuit if observables is None: observables = SparsePauliOp.from_list([("Z" * circuit.num_qubits, 1)]) if isinstance(observables, BaseOperator): observables = (observables,) self._observables = observables if isinstance(circuit, QNNCircuit): self._input_params = list(circuit.input_parameters) self._weight_params = list(circuit.weight_parameters) else: self._input_params = list(input_params) if input_params is not None else [] self._weight_params = list(weight_params) if weight_params is not None else [] if gradient is None: gradient = ParamShiftEstimatorGradient(self.estimator) self.gradient = gradient self._input_gradients = input_gradients super().__init__( num_inputs=len(self._input_params), num_weights=len(self._weight_params), sparse=False, output_shape=len(self._observables), input_gradients=input_gradients, ) self._circuit = self._reparameterize_circuit(circuit, input_params, weight_params) @property def circuit(self) -> QuantumCircuit: """The quantum circuit representing the neural network.""" return copy(self._org_circuit) @property def observables(self) -> Sequence[BaseOperator] | BaseOperator: """Returns the underlying observables of this QNN.""" return copy(self._observables) @property def input_params(self) -> Sequence[Parameter] | None: """The parameters that correspond to the input data of the network.""" return copy(self._input_params) @property def weight_params(self) -> Sequence[Parameter] | None: """The parameters that correspond to the trainable weights.""" return copy(self._weight_params) @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 def _forward_postprocess(self, num_samples: int, result: EstimatorResult) -> np.ndarray: """Post-processing during forward pass of the network.""" return np.reshape(result.values, (-1, num_samples)).T def _forward( self, input_data: np.ndarray | None, weights: np.ndarray | None ) -> np.ndarray | None: """Forward pass of the neural network.""" parameter_values_, num_samples = self._preprocess_forward(input_data, weights) job = self.estimator.run( [self._circuit] * num_samples * self.output_shape[0], [op for op in self._observables for _ in range(num_samples)], np.tile(parameter_values_, (self.output_shape[0], 1)), ) try: results = job.result() except Exception as exc: raise QiskitMachineLearningError("Estimator job failed.") from exc return self._forward_postprocess(num_samples, results) def _backward_postprocess( self, num_samples: int, result: EstimatorGradientResult ) -> tuple[np.ndarray | None, np.ndarray]: """Post-processing during backward pass of the network.""" num_observables = self.output_shape[0] if self._input_gradients: input_grad = np.zeros((num_samples, num_observables, self._num_inputs)) else: input_grad = None weights_grad = np.zeros((num_samples, num_observables, self._num_weights)) gradients = np.asarray(result.gradients) for i in range(num_observables): if self._input_gradients: input_grad[:, i, :] = gradients[i * num_samples : (i + 1) * num_samples][ :, : self._num_inputs ] weights_grad[:, i, :] = gradients[i * num_samples : (i + 1) * num_samples][ :, self._num_inputs : ] else: weights_grad[:, i, :] = gradients[i * num_samples : (i + 1) * num_samples] return input_grad, weights_grad def _backward( self, input_data: np.ndarray | None, weights: np.ndarray | None ) -> tuple[np.ndarray | None, np.ndarray]: """Backward pass of the network.""" # prepare parameters in the required format parameter_values, num_samples = self._preprocess_forward(input_data, weights) input_grad, weights_grad = None, None if np.prod(parameter_values.shape) > 0: num_observables = self.output_shape[0] num_circuits = num_samples * num_observables circuits = [self._circuit] * num_circuits observables = [op for op in self._observables for _ in range(num_samples)] param_values = np.tile(parameter_values, (num_observables, 1)) job = None if self._input_gradients: job = self.gradient.run(circuits, observables, param_values) elif len(parameter_values[0]) > self._num_inputs: params = [self._circuit.parameters[self._num_inputs :]] * num_circuits job = self.gradient.run(circuits, observables, param_values, parameters=params) if job is not None: try: results = job.result() except Exception as exc: raise QiskitMachineLearningError("Estimator job failed.") from exc input_grad, weights_grad = self._backward_postprocess(num_samples, results) return input_grad, weights_grad