Nota
Esta página fue generada a partir de docs/tutorials/04_torch_qgan.ipynb.
Implementación de PyTorch qGAN¶
Descripción General¶
Este tutorial presenta paso a paso cómo construir un algoritmo de Red de Adversarios Generativos Cuántica basado en PyTorch.
Contexto¶
El qGAN [1] es un algoritmo híbrido cuántico-clásico utilizado para tareas de modelado generativo. El algoritmo utiliza la interacción de un generador cuántico \(G_{\theta}\), es decir, un ansatz, y un discriminador clásico \(D_{\phi}\), una red neuronal, para aprender la distribución de probabilidad subyacente dados los datos de entrenamiento.
The generator and discriminator are trained in alternating optimization steps, where the generator aims at generating samples that will be classified by the discriminator as training data samples (i.e, samples extracted from the real training distribution), and the discriminator tries to differentiate between original training data samples and data samples from the generator (in other words, telling apart the real and generated distributions). The final goal is for the quantum generator to learn a representation for the training data’s underlying probability distribution. The trained quantum generator can, thus, be used to load a quantum state which is an approximate model of the target distribution.
Referencias:
[1] Zoufal et al., Quantum Generative Adversarial Networks for learning and loading random distributions
Aplicación: qGANs para Cargar Distribuciones Aleatorias¶
Given \(k\)-dimensional data samples, we employ a quantum Generative Adversarial Network (qGAN) to learn the data’s underlying random distribution and to load it directly into a quantum state:
donde \(p_{\theta}^{j}\) describen las probabilidades de ocurrencia de los estados base \(\big| j\rangle\).
El objetivo del entrenamiento qGAN es generar un estado \(\big| g_{\theta}\rangle\) donde \(p_{\theta}^{j}\), para \(j\in \left\{0, \ldots, {2^n-1} \right\}\), describe una distribución de probabilidad cercana a la distribución subyacente a los datos de entrenamiento \(X=\left\{x^0, \ldots, x^{k-1} \right\}\).
Para obtener más detalles, consulta Quantum Generative Adversarial Networks for Learning and Loading Random Distributions Zoufal, Lucchi, Woerner [2019].
Para un ejemplo de cómo utilizar una qGAN entrenada en una aplicación, la fijación de precios de derivados financieros, consulta el tutorial Fijación de Precios de Opciones con qGANs.
Tutorial¶
Datos y Representación¶
Primero, necesitamos cargar nuestros datos de entrenamiento \(X\).
En este tutorial, los datos de entrenamiento están dados por muestras de una distribución normal multivariante 2D.
El objetivo del generador es aprender a representar dicha distribución, y el generador entrenado debe corresponder a un estado cuántico de \(n\) qubits \begin{equation} |g_{\text{trained}}\rangle=\sum\limits_{j=0}^{k-1}\sqrt{p_{j}}|x_{j}\rangle, \end{equation} donde el estado base \(|x_{j}\rangle\) representa los elementos de datos en el conjunto de datos de entrenamiento \(X={x_0, \ldots, x_{k-1}}\) con \(k\leq 2^n\) y \(p_j\) se refiere a la probabilidad de muestreo de \(|x_{j}\rangle\).
Para facilitar esta representación, necesitamos mapear las muestras de la distribución normal multivariante a valores discretos. La cantidad de valores que se pueden representar depende de la cantidad de qubits utilizados para el mapeo. Por lo tanto, la resolución de datos se define por el número de qubits. Si usamos \(3\) qubits para representar una característica, tenemos \(2^3 = 8\) valores discretos.
Primero comenzamos fijando semillas en los generadores de números aleatorios, luego importaremos las bibliotecas y los paquetes que necesitaremos para este tutorial.
[1]:
import torch
from qiskit.utils import algorithm_globals
torch.manual_seed(42)
algorithm_globals.random_seed = 42
Luego, muestreamos algunos datos de una distribución normal multivariante 2D como se describe anteriormente.
[2]:
import numpy as np
from qiskit_machine_learning.datasets.dataset_helper import discretize_and_truncate
# Load the training data
training_data = algorithm_globals.random.multivariate_normal(
mean=[0.0, 0.0], cov=[[1, 0], [0, 1]], size=1000, check_valid="warn", tol=1e-8, method="svd"
)
# Define minimal and maximal values for the training data
bounds_min = np.percentile(training_data, 5, axis=0)
bounds_max = np.percentile(training_data, 95, axis=0)
bounds = []
for i, _ in enumerate(bounds_min):
bounds.append([bounds_min[i], bounds_max[i]])
# Determine data resolution for each dimension of the training data in terms
# of the number of qubits used to represent each data dimension.
data_dim = [3, 3]
# Pre-processing, i.e., discretization of the data (gridding)
(training_data, grid_data, grid_elements, prob_data) = discretize_and_truncate(
training_data,
np.asarray(bounds),
data_dim,
return_data_grid_elements=True,
return_prob=True,
prob_non_zero=True,
)
Let’s visualize our distribution and samples. The discretize_and_truncate
function discretized the classical data we got from the multivariate normal so, a plot of the probability density function looks very coarse. But it is still a bell-shaped bivariate normal distribution.
[3]:
import matplotlib.pyplot as plt
from matplotlib import cm
fig, ax = plt.subplots(figsize=(12, 12), subplot_kw={"projection": "3d"})
x, y = np.meshgrid(grid_data[0, :], grid_data[1, :])
prob = np.reshape(prob_data, (8, 8))
surf = ax.plot_surface(x, y, prob, cmap=cm.coolwarm, linewidth=0, antialiased=False)
fig.colorbar(surf, shrink=0.5, aspect=5)
[3]:
<matplotlib.colorbar.Colorbar at 0x1e7c13ac048>

En la siguiente figura demostramos que los datos de entrenamiento fueron discretizados. Pasamos la resolución de datos como un arreglo de [3, 3]
. Este arreglo definió la cantidad de qubits utilizados para representar cada dimensión de datos. Por lo tanto, podemos definir \(2^3 = 8\) valores discretos y todas las muestras de entrenamiento deben caer bajo uno de estos valores discretos. En la siguiente figura hay un histograma para cada variable aleatoria y para ver la discretización real aumentamos el número de contenedores (bins). Algunos de los contenedores están vacíos y hay 8 contenedores no vacíos para cada variable.
[4]:
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(18, 6))
ax1.hist(training_data[:, 0], bins=20)
ax1.set_title("Histogram of the first variable")
ax1.set_xlabel("Values")
ax1.set_ylabel("Counts")
ax2.hist(training_data[:, 1], bins=20)
ax2.set_title("Histogram of the second variable")
ax2.set_xlabel("Values")
ax2.set_ylabel("Counts")
[4]:
Text(0, 0.5, 'Counts')

Pasamos al modelado con PyTorch y comenzamos convirtiendo arreglos de datos en tensores y creamos un cargador de datos a partir de nuestros datos de entrenamiento.
[5]:
from torch.utils.data import DataLoader
training_data = torch.tensor(training_data, dtype=torch.float)
grid_elements = torch.tensor(grid_elements, dtype=torch.float)
# Define the training batch size
batch_size = 300
dataloader = DataLoader(training_data, batch_size=batch_size, shuffle=True, drop_last=True)
Configuraciones del Backend¶
A continuación, debemos elegir un backend que se utilice para ejecutar el generador cuántico. El método presentado es compatible con todos los backends basados en iteraciones o shots (qasm, hardware falso, hardware real) proporcionados por Qiskit.
Primero, creamos una instancia cuántica (quantum instance) para el entrenamiento, donde el tamaño del lote (batch) define la cantidad de iteraciones (shots).
[6]:
from qiskit import Aer
from qiskit.utils import QuantumInstance
backend = Aer.get_backend("aer_simulator")
qi_training = QuantumInstance(backend, shots=batch_size)
Luego creamos una instancia cuántica para fines de evaluación, elegimos una mayor cantidad de iteraciones para obtener una mejor apreciación.
[7]:
qi_sampling = QuantumInstance(backend, shots=10000)
Inicializar la red neuronal cuántica ansatz¶
Ahora, definimos el circuito cuántico parametrizado \(G\left(\boldsymbol{\theta}\right)\) con \(\boldsymbol{\theta} = {\theta_1, ..., \theta_k}\) que será uilizado en nuestro generador cuántico.
To implement the quantum generator, we choose a depth-\(2\) ansatz that implements \(R_Y\) rotations and \(CX\) gates which takes a uniform distribution as an input state. Notably, for \(k>1\) the generator’s parameters must be chosen carefully. For example, the circuit depth should be more than \(1\) because higher circuit depths enable the representation of more complex structures.
[8]:
from qiskit import Aer, QuantumCircuit
from qiskit.circuit.library import TwoLocal
# sum(data_dim) corresponds to the total number of qubits in our quantum circuit (qc)
qc = QuantumCircuit(sum(data_dim))
qc.h(qc.qubits)
# We choose a hardware efficient ansatz.
twolocal = TwoLocal(sum(data_dim), "ry", "cx", reps=2, entanglement="sca")
qc.compose(twolocal, inplace=True)
Let’s draw our circuit and see what it looks like.
[9]:
qc.decompose().draw("mpl")
[9]:

Definición del generador cuántico¶
A continuación, definimos una función que crea el generador cuántico a partir de un circuito cuántico parametrizado dado. Como parámetros, esta función toma una instancia cuántica que se utilizará para el muestreo de datos. Encapsulamos una red neuronal cuántica creada en TorchConnector
para hacer uso del entrenamiento basado en PyTorch.
[10]:
from qiskit_machine_learning.connectors import TorchConnector
from qiskit_machine_learning.neural_networks import CircuitQNN
def create_generator(quantum_instance) -> TorchConnector:
circuit_qnn = CircuitQNN(
qc,
input_params=[],
weight_params=qc.parameters,
quantum_instance=quantum_instance,
sampling=True,
sparse=False,
interpret=lambda x: grid_elements[x],
)
return TorchConnector(circuit_qnn)
Definición del discriminador clásico¶
Después, definimos una red neuronal clásica basada en PyTorch que representa el discriminador clásico. Los gradientes subyacentes se pueden calcular automáticamente con PyTorch.
[11]:
import torch.nn as nn
class Discriminator(nn.Module):
def __init__(self, input_size):
super(Discriminator, self).__init__()
self.linear_input = nn.Linear(input_size, 20)
self.leaky_relu = nn.LeakyReLU(0.2)
self.linear20 = nn.Linear(20, 1)
self.sigmoid = nn.Sigmoid()
def forward(self, input: torch.Tensor) -> torch.Tensor:
x = self.linear_input(input)
x = self.leaky_relu(x)
x = self.linear20(x)
x = self.sigmoid(x)
return x
Definición de las funciones de pérdida¶
Queremos entrenar el generador y el discriminador con entropía cruzada binaria como función de pérdida:
donde \(x_j\) se refiere a una muestra de datos, mientras que \(y_j\) a la etiqueta correspondiente.
[12]:
# Generator loss function
gen_loss_fun = nn.BCELoss()
# Discriminator loss function
disc_loss_fun = nn.BCELoss()
Evaluación de gradientes personalizados para la función de pérdida BCE del generador¶
The evaluation of custom gradients for the quantum generator requires us to combine quantum gradients \(\frac{\partial p_j\left(\boldsymbol{\theta}\right)}{\partial \theta_l}\) that we compute with Qiskit’s gradient framework with the binary cross entropy as follows:
Primero, necesitamos definir cómo se evaluarán los gradientes. Según el backend, los gradientes pueden devolverse en un formato disperso. Dado que PyTorch proporciona solo un soporte limitado para gradientes dispersos, necesitamos escribir una función personalizada para el entrenamiento basado en gradientes.
[13]:
from qiskit.opflow import Gradient, StateFn
generator_grad = Gradient().gradient_wrapper(
StateFn(qc), twolocal.ordered_parameters, backend=qi_training
)
Aquí, definimos la función personalizada que evalúa los gradientes de la función de pérdida del generador considerando los gradientes personalizados del generador cuántico. Como los parámetros, la función toma una lista de valores de parámetros y el discriminador como una red neuronal clásica. La función devuelve una lista de valores de gradiente.
[14]:
import torch.nn.functional as F
def generator_loss_grad(parameter_values, discriminator):
# evaluate gradient
grads = generator_grad(parameter_values).tolist()
loss_grad = ()
for j, grad in enumerate(grads):
cx = grad[0].tocoo()
input = torch.zeros(len(cx.col), len(data_dim))
target = torch.ones(len(cx.col), 1)
weight = torch.zeros(len(cx.col), 1)
for i, (index, prob_grad) in enumerate(zip(cx.col, cx.data)):
input[i, :] = grid_elements[index]
weight[i, :] = prob_grad
bce_loss_grad = F.binary_cross_entropy(discriminator(input), target, weight)
loss_grad += (bce_loss_grad,)
loss_grad = torch.stack(loss_grad)
return loss_grad
Entropía relativa como métrica de evaluación comparativa¶
La entropía relativa describe una métrica de distancia para las distribuciones. Por lo tanto, podemos usarla para comparar qué tan cerca/lejos está la distribución entrenada de la distribución objetivo.
En la siguiente función, calculamos la entropía relativa entre la distribución objetivo y la entrenada.
[15]:
def get_relative_entropy(gen_data) -> float:
prob_gen = np.zeros(len(grid_elements))
for j, item in enumerate(grid_elements):
for gen_item in gen_data.detach().numpy():
if np.allclose(np.round(gen_item, 6), np.round(item, 6), rtol=1e-5):
prob_gen[j] += 1
prob_gen = prob_gen / len(gen_data)
prob_gen = [1e-8 if x == 0 else x for x in prob_gen]
return entropy(prob_gen, prob_data)
Definición de los optimizadores¶
Para entrenar el generador y el discriminador, necesitamos definir esquemas de optimización. A continuación, empleamos un optimizador basado en el momento llamado Adam, consulta Kingma et al., Adam: A method for stochastic optimization para obtener más detalles.
[16]:
from torch.optim import Adam
# Initialize generator and discriminator
generator = create_generator(qi_training)
discriminator = Discriminator(len(data_dim))
lr = 0.01 # learning rate
b1 = 0.9 # first momentum parameter
b2 = 0.999 # second momentum parameter
num_epochs = 50 # number of training epochs
# optimizer for the generator
optimizer_gen = Adam(generator.parameters(), lr=lr, betas=(b1, b2))
# optimizer for the discriminator
optimizer_disc = Adam(discriminator.parameters(), lr=lr, betas=(b1, b2))
Visualización del proceso de entrenamiento¶
We will visualize what is happening during the training by plotting the evolution of the generator’s and the discriminator’s loss functions during the training, as well as the progress in the relative entropy between the trained and the target distribution. We define a function that plots the loss functions and relative entropy. We call this function once an epoch of training is complete.
La visualización del proceso de entrenamiento comienza cuando los datos de entrenamiento se recopilan en dos épocas.
[17]:
from IPython.display import clear_output
def plot_training_progress():
# we don't plot if we don't have enough data
if len(generator_loss_values) < 2:
return
clear_output(wait=True)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(18, 6))
# Loss
ax1.set_title("Loss")
ax1.plot(generator_loss_values, label="generator loss", color="royalblue")
ax1.plot(discriminator_loss_values, label="discriminator loss", color="magenta")
ax1.legend(loc="best")
ax1.set_xlabel("Iteration")
ax1.set_ylabel("Loss")
ax1.grid()
# Relative Entropy
ax2.set_title("Relative entropy")
ax2.plot(relative_entropy_values)
ax2.set_xlabel("Iteration")
ax2.set_ylabel("Relative entropy")
ax2.grid()
plt.show()
Entrenamiento¶
Ahora, estamos listos para entrenar nuestro modelo. Puede llevar algo de tiempo entrenar al modelo, así que ten paciencia.
[18]:
from scipy.stats import entropy
# Relative entropy list
relative_entropy_values = []
# Generator loss list
generator_loss_values = []
# Discriminator loss list
discriminator_loss_values = []
for epoch in range(num_epochs):
relative_entropy_epoch = []
generator_loss_epoch = []
discriminator_loss_epoch = []
for i, data in enumerate(dataloader):
# Adversarial ground truths
valid = torch.ones(data.size(0), 1)
fake = torch.zeros(data.size(0), 1)
# Generate a batch of data points
gen_data = generator()
# Evaluate Relative Entropy
relative_entropy_epoch.append(get_relative_entropy(gen_data))
# Train Discriminator
optimizer_disc.zero_grad()
# Loss measures discriminator's ability to distinguish real from generated samples
disc_data = discriminator(data)
real_loss = disc_loss_fun(disc_data, valid)
fake_loss = disc_loss_fun(discriminator(gen_data), fake)
discriminator_loss = (real_loss + fake_loss) / 2
discriminator_loss.backward(retain_graph=True)
optimizer_disc.step()
# Train Generator
optimizer_gen.zero_grad()
# Loss measures generator's ability to prepare good data samples
generator_loss = gen_loss_fun(discriminator(gen_data), valid)
generator_loss.retain_grad = True
g_loss_grad = generator_loss_grad(generator.weight.data.numpy(), discriminator)
# generator_loss.backward(retain_graph=True)
for j, param in enumerate(generator.parameters()):
param.grad = g_loss_grad
optimizer_gen.step()
generator_loss_epoch.append(generator_loss.item())
discriminator_loss_epoch.append(discriminator_loss.item())
relative_entropy_values.append(np.mean(relative_entropy_epoch))
generator_loss_values.append(np.mean(generator_loss_epoch))
discriminator_loss_values.append(np.mean(discriminator_loss_epoch))
plot_training_progress()

Resultados: funciones de distribución acumulativas¶
En la sección final, comparamos la función de distribución acumulativa (cumulative distribution function, CDF) de la distribución entrenada con la CDF de la distribución objetivo.
Creamos un nuevo generador de muestreo que toma más iteraciones y, por lo tanto, brinda más información. Recuerda, la instancia cuántica de muestreo se creó con una mayor cantidad de iteraciones. Luego, ajustamos los pesos del generador de muestreo a los valores obtenidos en el proceso de entrenamiento.
[19]:
generator_sampling = create_generator(qi_sampling)
generator_sampling.weight.data = generator.weight.data
A continuación, obtenemos las muestras de datos del generador, las probabilidades de muestreo correspondientes y graficamos las funciones de distribución acumulativa.
[20]:
gen_data = generator_sampling().detach().numpy()
prob_gen = np.zeros(len(grid_elements))
for j, item in enumerate(grid_elements):
for gen_item in gen_data:
if np.allclose(np.round(gen_item, 6), np.round(item, 6), rtol=1e-5):
prob_gen[j] += 1
prob_gen = prob_gen / len(gen_data)
prob_gen = [1e-8 if x == 0 else x for x in prob_gen]
fig = plt.figure(figsize=(12, 12))
ax1 = fig.add_subplot(111, projection="3d")
ax1.set_title("Cumulative Distribution Function")
# there's a known issue in matplotlit with placing a legend on the 3d plot
ax1.bar3d(
np.transpose(grid_elements)[1],
np.transpose(grid_elements)[0],
np.zeros(len(prob_gen)),
0.05,
0.05,
np.cumsum(prob_gen),
label="generated data",
color="blue",
alpha=1,
)
ax1.bar3d(
np.transpose(grid_elements)[1] + 0.05,
np.transpose(grid_elements)[0] + 0.05,
np.zeros(len(prob_data)),
0.05,
0.05,
np.cumsum(prob_data),
label="training data",
color="orange",
alpha=1,
)
# ax1.legend(loc='upper right')
ax1.set_xlabel("x_1")
ax1.set_ylabel("x_0")
ax1.set_zlabel("p(x)")
plt.show()

En la gráfica de arriba, en color azul, se muestra la distribución generada y en color naranja la de entrenamiento. Puedes encontrar que ambos CDF son similares entre sí.
Conclusión¶
Las redes de adversarios generativos cuánticas emplean la interacción de un generador y un discriminador para mapear una representación aproximada de una distribución de probabilidad subyacente a muestras de datos dadas en un canal cuántico. Este tutorial presenta una implementación autónoma de qGAN basada en PyTorch, donde el generador está dado por un canal cuántico, es decir, un circuito cuántico variacional, y el discriminador por una red neuronal clásica, y analiza la aplicación de aprendizaje eficiente y carga de distribuciones de probabilidad genéricas, dadas implícitamente por muestras de datos, en estados cuánticos. Dado que esta carga aproximada requiere solo compuertas \(\mathscr{O}\left(poly\left(n\right)\right)\) y puede permitir el uso de algoritmos cuánticos potencialmente ventajosos al ofrecer un esquema de carga de datos eficiente.
[21]:
import qiskit.tools.jupyter
%qiskit_version_table
%qiskit_copyright
Version Information
Qiskit Software | Version |
---|---|
qiskit-terra | 0.22.0 |
qiskit-aer | 0.11.0 |
qiskit-ignis | 0.7.0 |
qiskit | 0.33.0 |
qiskit-machine-learning | 0.5.0 |
System information | |
Python version | 3.7.9 |
Python compiler | MSC v.1916 64 bit (AMD64) |
Python build | default, Aug 31 2020 17:10:11 |
OS | Windows |
CPUs | 4 |
Memory (Gb) | 31.837730407714844 |
Thu Oct 20 14:19:30 2022 GMT Daylight Time |
This code is a part of Qiskit
© Copyright IBM 2017, 2022.
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.