Idiomas
English
Bengali
French
Hindi
Italian
Japanese
Korean
Malayalam
Russian
Spanish
Tamil
Turkish
Vietnamese

Nota

Esta página fue generada a partir de docs/tutorials/01_neural_networks.ipynb.

Redes Neuronales Cuánticas

Descripción General

Este cuaderno muestra diferentes implementaciones de redes neuronales cuánticas (quantum neural network, QNN) proporcionadas en qiskit-machine-learning, y cómo se pueden integrar en flujos de trabajo básicos de machine learning cuántico (quantum machine learning, QML).

El tutorial está estructurado de la siguiente manera:

  1. Introducción

  2. Cómo Instanciar QNNs

  3. Cómo Ejecutar un Paso hacia Adelante

  4. Cómo Ejecutar un Paso hacia Atrás

  5. Funcionalidad Avanzada

  6. Conclusión

1. Introducción

1.1. Quantum vs. Classical Neural Networks

Las redes neuronales clásicas son modelos algorítmicos inspirados en el cerebro humano que se pueden entrenar para reconocer patrones en los datos y aprender a resolver problemas complejos. Se basan en una serie de nodos interconectados, o neuronas, organizados en una estructura en capas, con parámetros que se pueden aprender aplicando estrategias de entrenamiento de machine learning o deep learning.

La motivación detrás del machine learning cuántico (QML) es integrar nociones de computación cuántica y de machine learning clásico para abrir el camino a esquemas de aprendizaje nuevos y mejorados. Las QNNs aplican este principio genérico al combinar redes neuronales clásicas y circuitos cuánticos parametrizados. Debido a que se encuentran en una intersección entre dos campos, las QNNs se pueden ver desde dos perspectivas:

  • Desde una perspectiva de machine learning, las QNNs son, una vez más, modelos algorítmicos que se pueden entrenar para encontrar patrones ocultos en los datos de manera similar a sus contrapartes clásicas. Estos modelos pueden cargar datos clásicos (entradas) en un estado cuántico, y luego procesarlos con compuertas cuánticas parametrizadas por pesos entrenables. La Figura 1 muestra un ejemplo genérico de QNN que incluye los pasos de carga y procesamiento de datos. La salida de la medición de este estado se puede conectar a una función de pérdida para entrenar los pesos (ponderaciones) a través de la retropropagación.

  • Desde la perspectiva de la computación cuántica, las QNNs son algoritmos cuánticos basados en circuitos cuánticos parametrizados que se pueden entrenar de forma variacional utilizando optimizadores clásicos. Estos circuitos contienen un mapa de características (con parámetros de entrada) y un ansatz (con pesos entrenables), como se ve en la Figura 1.

new_qnn-3.jpg

Figura 1. Estructura genérica de una red neuronal cuántica (QNN).

As you can see, these two perspectives are complementary, and do not necessarily rely on strict definitions of concepts such as «quantum neuron» or what constitutes a QNN’s «layer».

1.2. Implementación en qiskit-machine-learning

Las QNNs en qiskit-machine-learning están pensadas como unidades computacionales independientes de la aplicación en las que se pueden usar para diferentes casos de uso, y su configuración dependerá de la aplicación para la que se necesiten. El módulo contiene una interfaz para las QNNs y dos implementaciones específicas:

  1. NeuralNetwork: La interfaz para redes neuronales. Esta es una clase abstracta de la que heredan todas las QNN.

  2. EstimatorQNN: Una red basada en la evaluación de observables mecánicos cuánticos.

  3. SamplerQNN: Una red basada en las muestras resultantes de medir un circuito cuántico.

Estas implementaciones se basan en las primitivas de qiskit. Las primitivas son el punto de entrada para ejecutar QNNs en un simulador o en hardware cuántico real. Cada implementación, EstimatorQNN y SamplerQNN, toma una instancia opcional de su primitiva correspondiente, que puede ser cualquier subclase de BaseEstimator y BaseSampler, respectivamente.

El módulo qiskit.primitives proporciona una implementación de referencia para las clases Sampler y Estimator para ejecutar simulaciones de vectores de estado. De forma predeterminada, si no se pasa ninguna instancia a una clase QNN, la red crea automáticamente una instancia de la primitiva de referencia correspondiente (Sampler o Estimator). Para obtener más información sobre las primitivas, consulta la documentación de las primitivas.

La clase NeuralNetwork es la interfaz para todas las QNNs disponibles en qiskit-machine-learning. Expone un paso hacia adelante y hacia atrás que toman muestras de datos y pesos entrenables como entrada.

It’s important to note that NeuralNetworks are «stateless». They do not contain any training capabilities (these are pushed to the actual algorithms or applications: classifiers, regressors, etc), nor do they store the values for trainable weights.


Let’s now look into specific examples for the two NeuralNetwork implementations. But first, let’s set the algorithmic seed to ensure that the results don’t change between runs.

[25]:
from qiskit.utils import algorithm_globals

algorithm_globals.random_seed = 42

2. Cómo Instanciar QNNs

2.1. EstimatorQNN

La EstimatorQNN toma como entrada un circuito cuántico parametrizado, así como un observable mecánico cuántico opcional, y genera cálculos de valor esperado para el paso hacia adelante. La EstimatorQNN también acepta listas de observables para construir QNNs más complejas.

Let’s see an EstimatorQNN in action with a simple example. We start by constructing the parametrized circuit. This quantum circuit has two parameters, one represents a QNN input and the other represents a trainable weight:

[26]:
from qiskit.circuit import Parameter
from qiskit import QuantumCircuit

params1 = [Parameter("input1"), Parameter("weight1")]
qc1 = QuantumCircuit(1)
qc1.h(0)
qc1.ry(params1[0], 0)
qc1.rx(params1[1], 0)
qc1.draw("mpl")
[26]:
../_images/tutorials_01_neural_networks_6_0.png

Ahora podemos crear un observable para definir el cálculo del valor esperado. Si no se establece, EstimatorQNN creará automáticamente el observable predeterminado \(Z^{\otimes n}\). Aquí, \(n\) es el número de qubits del circuito cuántico.

En este ejemplo, cambiaremos las cosas y usaremos el observable \(Y^{\otimes n}\):

[27]:
from qiskit.quantum_info import SparsePauliOp

observable1 = SparsePauliOp.from_list([("Y" * qc1.num_qubits, 1)])

Junto con el circuito cuántico definido anteriormente y el observable que hemos creado, el constructor de EstimatorQNN toma los siguientes argumentos de palabras clave:

  • estimator: instancia primitiva opcional

  • input_params: list of quantum circuit parameters that should be treated as «network inputs»

  • weight_params: list of quantum circuit parameters that should be treated as «network weights»

En este ejemplo, previamente decidimos que el primer parámetro de params1 debería ser la entrada, mientras que el segundo debería ser el peso. Como estamos realizando una simulación de vector de estado local, no estableceremos el parámetro estimator; la red creará una instancia de la primitiva Estimator de referencia para nosotros. Si necesitáramos acceder a recursos en la nube o simuladores Aer, tendríamos que definir las respectivas instancias de Estimator y pasarlas al EstimatorQNN.

[28]:
from qiskit_machine_learning.neural_networks import EstimatorQNN

estimator_qnn = EstimatorQNN(
    circuit=qc1, observables=observable1, input_params=[params1[0]], weight_params=[params1[1]]
)
estimator_qnn
[28]:
<qiskit_machine_learning.neural_networks.estimator_qnn.EstimatorQNN at 0x7fd668ca0e80>

We’ll see how to use the QNN in the following sections, but before that, let’s check out the SamplerQNN class.

2.2. SamplerQNN

La SamplerQNN se instancia de manera similar a la EstimatorQNN, pero debido a que consume muestras directamente al medir el circuito cuántico, no requiere un observable personalizado.

These output samples are interpreted by default as the probabilities of measuring the integer index corresponding to a bitstring. However, the SamplerQNN also allows us to specify an interpret function to post-process the samples. This function should be defined so that it takes a measured integer (from a bitstring) and maps it to a new value, i.e. non-negative integer.

(!) It’s important to note that if a custom interpret function is defined, the output_shape cannot be inferred by the network, and needs to be provided explicitly.

(!) It’s also important to keep in mind that if no interpret function is used, the dimension of the probability vector will scale exponentially with the number of qubits. With a custom interpret function, this scaling can change. If, for instance, an index is mapped to the parity of the corresponding bitstring, i.e., to 0 or 1, the result will be a probability vector of length 2 independently of the number of qubits.

Let’s create a different quantum circuit for the SamplerQNN. In this case, we will have two input parameters and four trainable weights that parametrize a two-local circuit.

[29]:
from qiskit.circuit import ParameterVector

inputs2 = ParameterVector("input", 2)
weights2 = ParameterVector("weight", 4)
print(f"input parameters: {[str(item) for item in inputs2.params]}")
print(f"weight parameters: {[str(item) for item in weights2.params]}")

qc2 = QuantumCircuit(2)
qc2.ry(inputs2[0], 0)
qc2.ry(inputs2[1], 1)
qc2.cx(0, 1)
qc2.ry(weights2[0], 0)
qc2.ry(weights2[1], 1)
qc2.cx(0, 1)
qc2.ry(weights2[2], 0)
qc2.ry(weights2[3], 1)

qc2.draw(output="mpl")
input parameters: ['input[0]', 'input[1]']
weight parameters: ['weight[0]', 'weight[1]', 'weight[2]', 'weight[3]']
[29]:
../_images/tutorials_01_neural_networks_14_1.png

Similarly to the EstimatorQNN, we must specify inputs and weights when instantiating the SamplerQNN. In this case, the keyword arguments will be: - sampler: optional primitive instance - input_params: list of quantum circuit parameters that should be treated as «network inputs» - weight_params: list of quantum circuit parameters that should be treated as «network weights»

Ten en cuenta que, una vez más, estamos eligiendo no configurar la instancia de Sampler en la QNN y confiando en el valor predeterminado.

[30]:
from qiskit_machine_learning.neural_networks import SamplerQNN

sampler_qnn = SamplerQNN(circuit=qc2, input_params=inputs2, weight_params=weights2)
sampler_qnn
[30]:
<qiskit_machine_learning.neural_networks.sampler_qnn.SamplerQNN at 0x7fd659264880>

Además de los argumentos básicos que se muestran arriba, la SamplerQNN acepta tres configuraciones más: input_gradients, interpret, y output_shape. Estos se introducirán en las secciones 4 y 5.

3. Cómo Ejecutar un Paso hacia Adelante

3.1. Configuración

En un entorno real, las entradas estarían definidas por el conjunto de datos y los pesos estarían definidos por el algoritmo de entrenamiento o como parte de un modelo preentrenado. Sin embargo, por el bien de este tutorial, especificaremos conjuntos aleatorios de entrada y pesos de la dimensión correcta:

3.1.1. Ejemplo de EstimatorQNN

[31]:
estimator_qnn_input = algorithm_globals.random.random(estimator_qnn.num_inputs)
estimator_qnn_weights = algorithm_globals.random.random(estimator_qnn.num_weights)
[32]:
print(
    f"Number of input features for EstimatorQNN: {estimator_qnn.num_inputs} \nInput: {estimator_qnn_input}"
)
print(
    f"Number of trainable weights for EstimatorQNN: {estimator_qnn.num_weights} \nWeights: {estimator_qnn_weights}"
)
Number of input features for EstimatorQNN: 1
Input: [0.77395605]
Number of trainable weights for EstimatorQNN: 1
Weights: [0.43887844]

3.1.2. Ejemplo de SamplerQNN

[33]:
sampler_qnn_input = algorithm_globals.random.random(sampler_qnn.num_inputs)
sampler_qnn_weights = algorithm_globals.random.random(sampler_qnn.num_weights)
[34]:
print(
    f"Number of input features for SamplerQNN: {sampler_qnn.num_inputs} \nInput: {sampler_qnn_input}"
)
print(
    f"Number of trainable weights for SamplerQNN: {sampler_qnn.num_weights} \nWeights: {sampler_qnn_weights}"
)
Number of input features for SamplerQNN: 2
Input: [0.85859792 0.69736803]
Number of trainable weights for SamplerQNN: 4
Weights: [0.09417735 0.97562235 0.7611397  0.78606431]

Once we have the inputs and the weights, let’s see the results for batched and non-batched passes.

3.2. Paso hacia Adelante Sin Lotes

3.2.1. Ejemplo de EstimatorQNN

Para la EstimatorQNN, la forma de salida esperada para el paso hacia adelante es (1, num_qubits * num_observables) donde 1 en nuestro caso es el número de muestras:

[35]:
estimator_qnn_forward = estimator_qnn.forward(estimator_qnn_input, estimator_qnn_weights)

print(
    f"Forward pass result for EstimatorQNN: {estimator_qnn_forward}. \nShape: {estimator_qnn_forward.shape}"
)
Forward pass result for EstimatorQNN: [[0.2970094]].
Shape: (1, 1)

3.2.2. Ejemplo de SamplerQNN

Para la SamplerQNN (sin una función de interpretación personalizada), la forma de salida esperada para el paso hacia adelante es (1, 2**num_qubits). Con una función de interpretación personalizada, la forma de salida será (1, output_shape), donde 1 en nuestro caso es el número de muestras:

[36]:
sampler_qnn_forward = sampler_qnn.forward(sampler_qnn_input, sampler_qnn_weights)

print(
    f"Forward pass result for SamplerQNN: {sampler_qnn_forward}.  \nShape: {sampler_qnn_forward.shape}"
)
Forward pass result for SamplerQNN: [[0.01826527 0.25735654 0.5267981  0.19758009]].
Shape: (1, 4)

3.3. Paso hacia Adelante Con Lotes

3.3.1. Ejemplo de EstimatorQNN

Para el EstimatorQNN, la forma de salida esperada para el paso hacia adelante es (batch_size, num_qubits * num_observables):

[37]:
estimator_qnn_forward_batched = estimator_qnn.forward(
    [estimator_qnn_input, estimator_qnn_input], estimator_qnn_weights
)

print(
    f"Forward pass result for EstimatorQNN: {estimator_qnn_forward_batched}.  \nShape: {estimator_qnn_forward_batched.shape}"
)
Forward pass result for EstimatorQNN: [[0.2970094]
 [0.2970094]].
Shape: (2, 1)

3.3.2. Ejemplo de SamplerQNN

Para la SamplerQNN (sin la función de interpretación personalizada), la forma de salida esperada para el paso hacia adelante es (batch_size, 2**num_qubits). Con una función de interpretación personalizada, la forma de salida será (batch_size, output_shape).

[38]:
sampler_qnn_forward_batched = sampler_qnn.forward(
    [sampler_qnn_input, sampler_qnn_input], sampler_qnn_weights
)

print(
    f"Forward pass result for SamplerQNN: {sampler_qnn_forward_batched}.  \nShape: {sampler_qnn_forward_batched.shape}"
)
Forward pass result for SamplerQNN: [[0.01826527 0.25735654 0.5267981  0.19758009]
 [0.01826527 0.25735654 0.5267981  0.19758009]].
Shape: (2, 4)

4. Cómo Ejecutar un Paso hacia Atrás

Let’s take advantage of the inputs and weights defined above to show how the backward pass works. This pass returns a tuple (input_gradients, weight_gradients). By default, the backward pass will only calculate gradients with respect to the weight parameters.

Si deseas habilitar gradientes con respecto a los parámetros de entrada, debes configurar el siguiente indicador durante la instanciación de la QNN:

qnn = ...QNN(..., input_gradients=True)

Recuerda que los gradientes de entrada son requeridos para el uso de TorchConnector para la integración de PyTorch.

4.1. Paso hacia Atrás sin Gradientes de Entrada

4.1.1. Ejemplo de EstimatorQNN

Para EstimatorQNN, la forma de salida esperada para los gradientes de peso es (batch_size, num_qubits * num_observables, num_weights):

[39]:
estimator_qnn_input_grad, estimator_qnn_weight_grad = estimator_qnn.backward(
    estimator_qnn_input, estimator_qnn_weights
)

print(
    f"Input gradients for EstimatorQNN: {estimator_qnn_input_grad}.  \nShape: {estimator_qnn_input_grad}"
)
print(
    f"Weight gradients for EstimatorQNN: {estimator_qnn_weight_grad}.  \nShape: {estimator_qnn_weight_grad.shape}"
)
Input gradients for EstimatorQNN: None.
Shape: None
Weight gradients for EstimatorQNN: [[[0.63272767]]].
Shape: (1, 1, 1)

4.1.2. Ejemplo de SamplerQNN

Para la SamplerQNN (sin la función de interpretación personalizada), la forma de salida esperada para el paso hacia adelante es (batch_size, 2**num_qubits, num_weights). Con una función de interpretación personalizada, la forma de salida será (batch_size, output_shape, num_weights).:

[40]:
sampler_qnn_input_grad, sampler_qnn_weight_grad = sampler_qnn.backward(
    sampler_qnn_input, sampler_qnn_weights
)

print(
    f"Input gradients for SamplerQNN: {sampler_qnn_input_grad}.  \nShape: {sampler_qnn_input_grad}"
)
print(
    f"Weight gradients for SamplerQNN: {sampler_qnn_weight_grad}.  \nShape: {sampler_qnn_weight_grad.shape}"
)
Input gradients for SamplerQNN: None.
Shape: None
Weight gradients for SamplerQNN: [[[ 0.00606238 -0.1124595  -0.06856156 -0.09809236]
  [ 0.21167414 -0.09069775  0.06856156 -0.22549618]
  [-0.48846674  0.32499215 -0.32262178  0.09809236]
  [ 0.27073021 -0.12183491  0.32262178  0.22549618]]].
Shape: (1, 4, 4)

4.2. Paso hacia Atrás con Gradientes de Entrada

Let’s enable the input_gradients to show what the expected output sizes are for this option.

[41]:
estimator_qnn.input_gradients = True
sampler_qnn.input_gradients = True

4.2.1. Ejemplo de EstimatorQNN

Para la EstimatorQNN, la forma de salida esperada para los gradientes de entrada es `(batch_size, num_qubits * num_observables, num_inputs):

[42]:
estimator_qnn_input_grad, estimator_qnn_weight_grad = estimator_qnn.backward(
    estimator_qnn_input, estimator_qnn_weights
)

print(
    f"Input gradients for EstimatorQNN: {estimator_qnn_input_grad}.  \nShape: {estimator_qnn_input_grad.shape}"
)
print(
    f"Weight gradients for EstimatorQNN: {estimator_qnn_weight_grad}.  \nShape: {estimator_qnn_weight_grad.shape}"
)
Input gradients for EstimatorQNN: [[[0.3038852]]].
Shape: (1, 1, 1)
Weight gradients for EstimatorQNN: [[[0.63272767]]].
Shape: (1, 1, 1)

4.2.2. Ejemplo de SamplerQNN

Para la SamplerQNN (sin función de interpretación personalizada), la forma de salida esperada para los gradientes de entrada es (batch_size, 2**num_qubits, num_inputs). Con una función de interpretación personalizada, la forma de salida será (batch_size, output_shape, num_inputs).

[43]:
sampler_qnn_input_grad, sampler_qnn_weight_grad = sampler_qnn.backward(
    sampler_qnn_input, sampler_qnn_weights
)

print(
    f"Input gradients for SamplerQNN: {sampler_qnn_input_grad}.  \nShape: {sampler_qnn_input_grad.shape}"
)
print(
    f"Weight gradients for SamplerQNN: {sampler_qnn_weight_grad}.  \nShape: {sampler_qnn_weight_grad.shape}"
)
Input gradients for SamplerQNN: [[[-0.05844702 -0.10621091]
  [ 0.38798796 -0.19544083]
  [-0.34561132  0.09459601]
  [ 0.01607038  0.20705573]]].
Shape: (1, 4, 2)
Weight gradients for SamplerQNN: [[[ 0.00606238 -0.1124595  -0.06856156 -0.09809236]
  [ 0.21167414 -0.09069775  0.06856156 -0.22549618]
  [-0.48846674  0.32499215 -0.32262178  0.09809236]
  [ 0.27073021 -0.12183491  0.32262178  0.22549618]]].
Shape: (1, 4, 4)

5. Funcionalidad Avanzada

5.1. EstimatorQNN con Múltiples Observables

La EstimatorQNN permite pasar listas de observables para arquitecturas QNN más complejas. Por ejemplo (ten en cuenta el cambio en la forma de salida):

[44]:
observable2 = SparsePauliOp.from_list([("Z" * qc1.num_qubits, 1)])

estimator_qnn2 = EstimatorQNN(
    circuit=qc1,
    observables=[observable1, observable2],
    input_params=[params1[0]],
    weight_params=[params1[1]],
)
[45]:
estimator_qnn_forward2 = estimator_qnn2.forward(estimator_qnn_input, estimator_qnn_weights)
estimator_qnn_input_grad2, estimator_qnn_weight_grad2 = estimator_qnn2.backward(
    estimator_qnn_input, estimator_qnn_weights
)

print(f"Forward output for EstimatorQNN1: {estimator_qnn_forward.shape}")
print(f"Forward output for EstimatorQNN2: {estimator_qnn_forward2.shape}")
print(f"Backward output for EstimatorQNN1: {estimator_qnn_weight_grad.shape}")
print(f"Backward output for EstimatorQNN2: {estimator_qnn_weight_grad2.shape}")
Forward output for EstimatorQNN1: (1, 1)
Forward output for EstimatorQNN2: (1, 2)
Backward output for EstimatorQNN1: (1, 1, 1)
Backward output for EstimatorQNN2: (1, 2, 1)

5.2. SamplerQNN con interpret personalizado

Un método interpret común para SamplerQNN es la función parity, que le permite realizar una clasificación binaria. Como se explicó en la sección de creación de instancias, el uso de funciones de interpretación modificará la forma de salida de los pasos hacia adelante y hacia atrás. En el caso de la función de interpretación de paridad, output_shape se fija en 2. Por lo tanto, las formas esperadas del gradiente de peso y gradiente hacia adelante son (batch_size, 2) y (batch_size, 2, num_weights), respectivamente:

[46]:
parity = lambda x: "{:b}".format(x).count("1") % 2
output_shape = 2  # parity = 0, 1

sampler_qnn2 = SamplerQNN(
    circuit=qc2,
    input_params=inputs2,
    weight_params=weights2,
    interpret=parity,
    output_shape=output_shape,
)
[47]:
sampler_qnn_forward2 = sampler_qnn2.forward(sampler_qnn_input, sampler_qnn_weights)
sampler_qnn_input_grad2, sampler_qnn_weight_grad2 = sampler_qnn2.backward(
    sampler_qnn_input, sampler_qnn_weights
)

print(f"Forward output for SamplerQNN1: {sampler_qnn_forward.shape}")
print(f"Forward output for SamplerQNN2: {sampler_qnn_forward2.shape}")
print(f"Backward output for SamplerQNN1: {sampler_qnn_weight_grad.shape}")
print(f"Backward output for SamplerQNN2: {sampler_qnn_weight_grad2.shape}")
Forward output for SamplerQNN1: (1, 4)
Forward output for SamplerQNN2: (1, 2)
Backward output for SamplerQNN1: (1, 4, 4)
Backward output for SamplerQNN2: (1, 2, 4)

6. Conclusión

En este tutorial, presentamos las dos clases de redes neuronales proporcionadas por qiskit-machine-learning, es decir, EstimatorQNN y SamplerQNN, que amplían la clase base NeuralNetwork. Brindamos algunos antecedentes teóricos, los pasos clave para la inicialización de QNN, el uso básico en pasos hacia adelante y hacia atrás y funcionalidad avanzada.

Ahora te alentamos a que juegues con la configuración del problema y veas cómo los diferentes tamaños de circuito, la entrada y las longitudes de los parámetros de peso influyen en las formas de salida.

[48]:
import qiskit.tools.jupyter

%qiskit_version_table
%qiskit_copyright

Version Information

Qiskit SoftwareVersion
qiskit-terra0.22.3
qiskit-machine-learning0.6.0
System information
Python version3.9.15
Python compilerClang 14.0.6
Python buildmain, Nov 24 2022 08:29:02
OSDarwin
CPUs8
Memory (Gb)64.0
Mon Jan 23 11:57:49 2023 CET

This code is a part of Qiskit

© Copyright IBM 2017, 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.