Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quantum-kittens
GitHub Repository: quantum-kittens/platypus
Path: blob/main/translations/es/ch-machine-learning/machine-learning-qiskit-pytorch.ipynb
3861 views
Kernel: Python 3

Redes Neuronales Híbridas cuánticas-clásicas con PyTorch y Qiskit

El aprendizaje automático (machine learnng, ML) se ha establecido como un campo interdisciplinario exitoso que busca extraer matemáticamente información generalizable de los datos. Incluirlo en la computación cuántica da lugar a interesantes áreas de investigación que buscan aprovechar los principios de la mecánica cuántica para aumentar el aprendizaje automático o viceversa. Ya sea que su objetivo sea mejorar los algoritmos de ML clásicos dejando los cálculos difíciles a una computadora cuántica u optimizar algoritmos cuánticos usando arquitecturas de ML clásicas, ambos se encuentran bajo el marco diverso del aprendizaje automático cuántico (quantum machine learning, QML).

En este capítulo, exploramos cómo una red neuronal clásica se puede modificar parcialmente para crear una red neuronal cuántica-clásica híbrida. Codificaremos un ejemplo sencillo que integra Qiskit con un paquete de software de código abierto de última generación: PyTorch. El propósito de este ejemplo es demostrar la facilidad de integrar Qiskit con las herramientas de ML existentes y alentar a los profesionales de ML a explorar lo que es posible con la computación cuántica.

Contenido

  1. ¿Cómo Funciona?
    1.1 Preliminares

  2. Entonces, ¿Cómo entra en Escena lo Cuántico?

  3. ¡Programemos!
    3.1 Importaciones
    3.2 Crea una "Clase Cuántica" con Qiskit
    3.3 Crea una "Clase Cuántica-Clásica" con PyTorch
    3.4 Carga y Preprocesamiento de Datos
    3.5 Creación de la Red Neuronal Híbrida
    3.6 Entrenamiento de la Red
    3.7 Probar la Red

  4. ¿Qué Sigue?

1. ¿Cómo funciona?

La Figura 1 ilustra el marco que construiremos en este capítulo. En última instancia, crearemos una red neuronal cuántica-clásica híbrida que busca clasificar dígitos dibujados a mano. Ten en cuenta que las conexiones que se muestran en esta imagen están todas dirigidas hacia abajo; sin embargo, la direccionalidad no se indica visualmente.

1.1 Preliminares

Los antecedentes presentados aquí sobre las redes neuronales clásicas se incluyen para establecer ideas relevantes y terminología compartida; sin embargo, siguen siendo de un nivel extremadamente alto. Si deseas profundizar un poco más en las redes neuronales clásicas, mira la serie de videos bien hechos del youtuber 3Blue1Brown. Como alternativa, si ya estás familiarizado con las redes clásicas, puedes pasar a la siguiente sección.

Neuronas y Pesos

Una red neuronal es, en última instancia, solo una función elaborada que se construye al componer bloques de construcción más pequeños llamados neuronas. Una neurona suele ser una función simple, fácil de calcular y no lineal que mapea una o más entradas a un único número real. La salida única de una neurona generalmente se copia y se alimenta como entrada a otras neuronas. Gráficamente, representamos las neuronas como nodos en un grafo y dibujamos conexiones dirigidas entre los nodos para indicar que se usará la salida de una neurona como entrada para otras neuronas. También es importante tener en cuenta que cada arista de nuestro grafo suele estar asociada con un valor escalar llamado peso. La idea aquí es que cada una de las entradas a una neurona se multiplique por un escalar diferente antes de recopilarse y procesarse en un solo valor. El objetivo al entrenar una red neuronal consiste principalmente en elegir nuestros pesos para que la red se comporte de una manera particular.

Redes Neuronales Prealimentadas

También vale la pena señalar que el tipo particular de red neuronal del que nos ocuparemos se denomina red neuronal prealimentada (feed-forward neural network, FFNN). Esto significa que a medida que los datos fluyen a través de nuestra red neuronal, nunca volverán a una neurona que ya visitó. De manera equivalente, podrías decir que el grafo que describe nuestra red neuronal es un grafo acíclico dirigido (directed acyclic graph, DAG). Además, estipularemos que las neuronas dentro de la misma capa de nuestra red neuronal no tendrán conexiones entre ellas.

Estructura de Entrada y Salida de las Capas

La entrada a una red neuronal es un vector clásico (de valor real). Cada componente del vector de entrada se multiplica por un peso diferente y se alimenta a una capa de neuronas de acuerdo con la estructura del grafo de la red. Después de evaluar cada neurona de la capa, los resultados se recopilan en un nuevo vector donde el i-ésimo componente registra la salida de la i-ésima neurona. Este nuevo vector se puede tratar como una entrada para una nueva capa, y así sucesivamente. Usaremos el término estándar capa oculta para describir todas las capas de nuestra red, excepto la primera y la última.

2. Entonces, ¿Cómo entra en Escena lo Cuántico?

Para crear una red neuronal cuántica-clásica, uno puede implementar una capa oculta para nuestra red neuronal utilizando un circuito cuántico parametrizado. Por "circuito cuántico parametrizado", nos referimos a un circuito cuántico donde los ángulos de rotación para cada compuerta están especificados por los componentes de un vector de entrada clásico. Las salidas de la capa anterior de nuestra red neuronal se recopilarán y usarán como entradas para nuestro circuito parametrizado. Las mediciones estadísticas de nuestro circuito cuántico se pueden recopilar y utilizar como entradas para la siguiente capa. A continuación se muestra un simple ejemplo:

Aquí, σ\sigma es una función no lineal y hih_i es el valor de la neurona ii en cada capa oculta. R(hi)R(h_i) representa cualquier compuerta de rotación sobre un ángulo igual a hih_i e yy es el valor de predicción final generado a partir de la red híbrida.

¿Qué pasa con la retropropagación?

Si estás familiarizado con el ML clásico, es posible que te preguntes de inmediato ¿cómo calculamos los gradientes cuando se trata de circuitos cuánticos? Esto sería necesario para incorporar potentes técnicas de optimización como el descenso de gradiente. Se vuelve un poco técnico, pero en resumen, podemos ver un circuito cuántico como una caja negra y el gradiente de esta caja negra con respecto a sus parámetros se puede calcular de la siguiente manera:

donde θ\theta representa los parámetros del circuito cuántico y ss es un cambio macroscópico. El gradiente es entonces simplemente la diferencia entre nuestro circuito cuántico evaluado en θ+s\theta+s y θs\theta - s. Por lo tanto, podemos diferenciar sistemáticamente nuestro circuito cuántico como parte de una rutina de retropropagación más grande. Esta regla de forma cerrada para calcular el gradiente de los parámetros del circuito cuántico se conoce como regla de cambio de parámetros.

# instalar los paquetes necesarios para esta página específica !pip install torchvision

3. ¡Programemos!

3.1 Importaciones

Primero, importamos algunos paquetes útiles que necesitaremos, incluyendo Qiskit y PyTorch.

import numpy as np import matplotlib.pyplot as plt import torch from torch.autograd import Function from torchvision import datasets, transforms import torch.optim as optim import torch.nn as nn import torch.nn.functional as F import qiskit from qiskit import transpile, assemble from qiskit.visualization import *

3.2 Crea una "Clase Cuántica" con Qiskit

Convenientemente, podemos poner nuestras funciones cuánticas de Qiskit en una clase. Primero, especificamos cuántos parámetros cuánticos entrenables y cuántas iteraciones deseamos usar en nuestro circuito cuántico. En este ejemplo, lo mantendremos simple y utilizaremos un circuito de 1 qubit con un parámetro cuántico entrenable θ\theta. Codificamos de forma fija el circuito por simplicidad y empleamos una rotación RYRY con el ángulo θ\theta para entrenar la salida de nuestro circuito. El circuito se ve así:

Para medir la salida en la base zz, calculamos el valor esperado de σz\sigma_\mathbf{z}. σz=izip(zi)\sigma_\mathbf{z} = \sum_i z_i p(z_i) Más adelante veremos cómo todo esto se relaciona con la red neuronal híbrida.

class QuantumCircuit: """ Esta clase proporciona una interfaz simple para la interacción con el circuito cuántico """ def __init__(self, n_qubits, backend, shots): # --- Definición de circuito --- self._circuit = qiskit.QuantumCircuit(n_qubits) all_qubits = [i for i in range(n_qubits)] self.theta = qiskit.circuit.Parameter('theta') self._circuit.h(all_qubits) self._circuit.barrier() self._circuit.ry(self.theta, all_qubits) self._circuit.measure_all() # --------------------------- self.backend = backend self.shots = shots def run(self, thetas): t_qc = transpile(self._circuit, self.backend) qobj = assemble(t_qc, shots=self.shots, parameter_binds = [{self.theta: theta} for theta in thetas]) job = self.backend.run(qobj) result = job.result().get_counts() counts = np.array(list(result.values())) states = np.array(list(result.keys())).astype(float) # Calcular probabilidades para cada estado probabilities = counts / self.shots # Obtener el valor esperado del estado expectation = np.sum(states * probabilities) return np.array([expectation])

Probemos la implementación

simulator = qiskit.Aer.get_backend('aer_simulator') circuit = QuantumCircuit(1, simulator, 100) print('Valor esperado para la rotación pi {}'.format(circuit.run([np.pi])[0])) circuit._circuit.draw()
Valor esperado para la rotación pi 0.57
Image in a Jupyter notebook

3.3 Crea una "Clase Cuántica-Clásica" con PyTorch

Ahora, que nuestro circuito cuántico está definido, podemos crear las funciones necesarias para la retropropagación usando PyTorch. Los pases hacia adelante y hacia atrás contienen elementos de nuestra clase Qiskit. El pase hacia atrás calcula directamente los gradientes utilizando la fórmula de diferencia finita que presentamos anteriormente.

class HybridFunction(Function): """ Cuántico híbrido - definición de función clásica """ @staticmethod def forward(ctx, input, quantum_circuit, shift): """ Cálculo de pase hacia adelante """ ctx.shift = shift ctx.quantum_circuit = quantum_circuit expectation_z = ctx.quantum_circuit.run(input[0].tolist()) result = torch.tensor([expectation_z]) ctx.save_for_backward(input, result) return result @staticmethod def backward(ctx, grad_output): """ Cálculo de pase hacia atrás """ input, expectation_z = ctx.saved_tensors input_list = np.array(input.tolist()) shift_right = input_list + np.ones(input_list.shape) * ctx.shift shift_left = input_list - np.ones(input_list.shape) * ctx.shift gradients = [] for i in range(len(input_list)): expectation_right = ctx.quantum_circuit.run(shift_right[i]) expectation_left = ctx.quantum_circuit.run(shift_left[i]) gradient = torch.tensor([expectation_right]) - torch.tensor([expectation_left]) gradients.append(gradient) gradients = np.array([gradients]).T return torch.tensor([gradients]).float() * grad_output.float(), None, None class Hybrid(nn.Module): """ Cuántico híbrido - definición de capa clásica """ def __init__(self, backend, shots, shift): super(Hybrid, self).__init__() self.quantum_circuit = QuantumCircuit(1, backend, shots) self.shift = shift def forward(self, input): return HybridFunction.apply(input, self.quantum_circuit, self.shift)

3.4 Carga y Preprocesamiento de Datos

Uniendo todo:

Crearemos una red neuronal híbrida simple para clasificar imágenes de dos tipos de dígitos (0 o 1) del conjunto de datos MNIST. Primero cargamos la base de datos MNIST y filtramos las imágenes que contienen 0 y 1. Estos servirán como entradas para que nuestra red neuronal los clasifique.

Datos de entrenamiento

# Concentrándose en las primeras 100 muestras n_samples = 100 X_train = datasets.MNIST(root='./data', train=True, download=True, transform=transforms.Compose([transforms.ToTensor()])) # Dejando solo las etiquetas 0 y 1 idx = np.append(np.where(X_train.targets == 0)[0][:n_samples], np.where(X_train.targets == 1)[0][:n_samples]) X_train.data = X_train.data[idx] X_train.targets = X_train.targets[idx] train_loader = torch.utils.data.DataLoader(X_train, batch_size=1, shuffle=True)
n_samples_show = 6 data_iter = iter(train_loader) fig, axes = plt.subplots(nrows=1, ncols=n_samples_show, figsize=(10, 3)) while n_samples_show > 0: images, targets = data_iter.__next__() axes[n_samples_show - 1].imshow(images[0].numpy().squeeze(), cmap='gray') axes[n_samples_show - 1].set_xticks([]) axes[n_samples_show - 1].set_yticks([]) axes[n_samples_show - 1].set_title("Labeled: {}".format(targets.item())) n_samples_show -= 1
Image in a Jupyter notebook

Datos de prueba

n_samples = 50 X_test = datasets.MNIST(root='./data', train=False, download=True, transform=transforms.Compose([transforms.ToTensor()])) idx = np.append(np.where(X_test.targets == 0)[0][:n_samples], np.where(X_test.targets == 1)[0][:n_samples]) X_test.data = X_test.data[idx] X_test.targets = X_test.targets[idx] test_loader = torch.utils.data.DataLoader(X_test, batch_size=1, shuffle=True)

Hasta ahora, hemos cargado los datos y codificado una clase que crea nuestro circuito cuántico que contiene 1 parámetro entrenable. Este parámetro cuántico se insertará en una red neuronal clásica junto con los otros parámetros clásicos para formar la red neuronal híbrida. También creamos funciones de pase hacia atrás y hacia adelante que nos permiten hacer retropropagación y optimizar nuestra red neuronal. Por último, debemos especificar nuestra arquitectura de red neuronal, de modo que podamos comenzar a entrenar nuestros parámetros utilizando técnicas de optimización proporcionadas por PyTorch.

3.5 Creación de la Red Neuronal Híbrida

Podemos usar una canalización (pipeline) ordenada de PyTorch para crear una arquitectura de red neuronal. La red deberá ser compatible en términos de su dimensionalidad cuando insertemos la capa cuántica (es decir, nuestro circuito cuántico). Dado que lo cuántico en este ejemplo contiene 1 parámetro, debemos asegurarnos de que la red condense las neuronas reduciendo al tamaño 1. Creamos una red neuronal convolucional típica con dos capas completamente conectadas al final. El valor de la última neurona de la capa totalmente conectada se introduce como parámetro θ\theta en nuestro circuito cuántico. La medición del circuito luego sirve como la predicción final para 0 o 1 según lo proporcionado por una medición σz\sigma_z.

class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = nn.Conv2d(1, 6, kernel_size=5) self.conv2 = nn.Conv2d(6, 16, kernel_size=5) self.dropout = nn.Dropout2d() self.fc1 = nn.Linear(256, 64) self.fc2 = nn.Linear(64, 1) self.hybrid = Hybrid(qiskit.Aer.get_backend('aer_simulator'), 100, np.pi / 2) def forward(self, x): x = F.relu(self.conv1(x)) x = F.max_pool2d(x, 2) x = F.relu(self.conv2(x)) x = F.max_pool2d(x, 2) x = self.dropout(x) x = x.view(1, -1) x = F.relu(self.fc1(x)) x = self.fc2(x) x = self.hybrid(x) return torch.cat((x, 1 - x), -1)

3.6 Entrenamiento de la Red

¡Ya tenemos todos los ingredientes para entrenar nuestra red híbrida! Podemos especificar cualquier optimizador de PyTorch, tasa de aprendizaje y función de costo/pérdida para entrenar con múltiples épocas. En este caso, usamos el optimizador Adam, una tasa de aprendizaje de 0.001 y la función de pérdida de verosimilitud logarítmica negativa.

model = Net() optimizer = optim.Adam(model.parameters(), lr=0.001) loss_func = nn.NLLLoss() epochs = 20 loss_list = [] model.train() for epoch in range(epochs): total_loss = [] for batch_idx, (data, target) in enumerate(train_loader): optimizer.zero_grad() # Pase hacia adelante output = model(data) # Calcular la pérdida loss = loss_func(output, target) # Pase hacia atrás loss.backward() # Optimizar los pesos optimizer.step() total_loss.append(loss.item()) loss_list.append(sum(total_loss)/len(total_loss)) print('Training [{:.0f}%]\tLoss: {:.4f}'.format( 100. * (epoch + 1) / epochs, loss_list[-1]))
/usr/local/anaconda3/lib/python3.7/site-packages/ipykernel_launcher.py:32: FutureWarning: The input object of type 'Tensor' is an array-like implementing one of the corresponding protocols (`__array__`, `__array_interface__` or `__array_struct__`); but not a sequence (or 0-D). In the future, this object will be coerced as if it was first converted using `np.array(obj)`. To retain the old behaviour, you have to either modify the type 'Tensor', or assign to an empty array created with `np.empty(correct_shape, dtype=object)`.
Training [5%] Loss: -0.7741 Training [10%] Loss: -0.9155 Training [15%] Loss: -0.9489 Training [20%] Loss: -0.9400 Training [25%] Loss: -0.9496 Training [30%] Loss: -0.9561 Training [35%] Loss: -0.9627 Training [40%] Loss: -0.9499 Training [45%] Loss: -0.9664 Training [50%] Loss: -0.9676 Training [55%] Loss: -0.9761 Training [60%] Loss: -0.9790 Training [65%] Loss: -0.9846 Training [70%] Loss: -0.9836 Training [75%] Loss: -0.9857 Training [80%] Loss: -0.9877 Training [85%] Loss: -0.9895 Training [90%] Loss: -0.9912 Training [95%] Loss: -0.9936 Training [100%] Loss: -0.9901

Mostrar el grafo de entrenamiento

plt.plot(loss_list) plt.title('Convergencia de Entrenamiento de la NN Híbrida') plt.xlabel('Iteraciones de Entrenamiento') plt.ylabel('Pérdida de Verosimilitud Logarítmica Negativa')
Text(0, 0.5, 'Neg Log Likelihood Loss')
Image in a Jupyter notebook

3.7 Probar la Red

model.eval() with torch.no_grad(): correct = 0 for batch_idx, (data, target) in enumerate(test_loader): output = model(data) pred = output.argmax(dim=1, keepdim=True) correct += pred.eq(target.view_as(pred)).sum().item() loss = loss_func(output, target) total_loss.append(loss.item()) print('Rendimiento en datos de prueba:\n\tPérdida: {:.4f}\n\tPrecisión: {:.1f}%'.format( sum(total_loss) / len(total_loss), correct / len(test_loader) * 100) )
Rendimiento en datos de prueba: Pérdida: -0.9827 Precisión: 100.0%
n_samples_show = 6 count = 0 fig, axes = plt.subplots(nrows=1, ncols=n_samples_show, figsize=(10, 3)) model.eval() with torch.no_grad(): for batch_idx, (data, target) in enumerate(test_loader): if count == n_samples_show: break output = model(data) pred = output.argmax(dim=1, keepdim=True) axes[count].imshow(data[0].numpy().squeeze(), cmap='gray') axes[count].set_xticks([]) axes[count].set_yticks([]) axes[count].set_title('Predicho {}'.format(pred.item())) count += 1
Image in a Jupyter notebook

4. ¿Qué Sigue?

Si bien es totalmente posible crear redes neuronales híbridas, ¿realmente tiene algún beneficio?

De hecho, las capas clásicas de esta red se entrenan perfectamente bien (de hecho, mejor) sin la capa cuántica. Además, es posible que hayas notado que la capa cuántica que entrenamos aquí no genera entrelazamiento y, por lo tanto, seguirá siendo clásicamente simulable a medida que ampliemos esta arquitectura en particular. Esto significa que si esperas lograr una ventaja cuántica utilizando redes neuronales híbridas, deberás comenzar extendiendo este código para incluir una capa cuántica más sofisticada.

El objetivo de este ejercicio era hacerte pensar en la integración de técnicas de ML y computación cuántica para investigar si realmente hay algún elemento de interés, y gracias a PyTorch y Qiskit, esto se vuelve un poco más fácil.

import qiskit.tools.jupyter %qiskit_version_table