Saltar al contenido principal

Kernels cuánticos

Introducción a los kernels cuánticos

El "método de kernel cuántico" designa cualquier método que utiliza computadoras cuánticas para estimar un kernel. En este contexto, "kernel" se refiere a la matriz kernel o a entradas individuales de la misma. Como recordatorio: un mapeo de características Φ(x)\Phi(\vec{x}) es un mapeo de xRd\vec{x}\in \mathbb{R}^d a Φ(x)Rd,\Phi(\vec{x})\in \mathbb{R}^{d'}, donde generalmente d>dd'>d y el objetivo de este mapeo es hacer que las categorías de datos sean separables por un hiperplano. La función kernel toma vectores en el espacio mapeado por características como argumentos y devuelve su producto interno, es decir, K:Rd×RdRK:\mathbb{R}^d\times\mathbb{R}^d\rightarrow \mathbb{R} con K(x,y)=Φ(x)Φ(y)K(x,y) = \langle \Phi(x)|\Phi(y)\rangle. Clásicamente, nos interesan mapeos de características para los cuales la función kernel es fácil de evaluar. Esto a menudo significa encontrar una función kernel donde el producto interno en el espacio mapeado por características pueda expresarse en términos de los vectores de datos originales, sin necesidad de construir explícitamente Φ(x)\Phi(x) y Φ(y)\Phi(y). En el método de kernel cuántico, el mapeo de características se realiza mediante un circuito cuántico, y el kernel se estima a partir de las mediciones en este circuito y las probabilidades de medición relativas.

En esta lección investigamos las profundidades de circuitos de codificación preconstruidos que utilizan entrelazamiento extensivo, y las comparamos con las profundidades de circuitos que programamos nosotros mismos. Esto no es una recomendación de un método sobre otro. Quizás descubras que los circuitos preconstruidos son demasiado profundos y que el entrelazamiento en el circuito auto-construido no es suficiente para un uso útil. Estos ejemplos sirven únicamente para posibilitar tu propia exploración.

Antes de recorrer en detalle una estimación de matriz kernel, esbozamos el flujo de trabajo utilizando el lenguaje de Qiskit Patterns.

Paso 1: Mapear entradas clásicas a un problema cuántico

  • Entrada: Conjunto de datos de entrenamiento
  • Salida: Circuito abstracto para calcular una entrada de la matriz kernel

Partiendo del conjunto de datos, el primer paso es codificar los datos en un circuito cuántico. Dicho de otro modo: debemos mapear nuestros datos al espacio de Hilbert de los estados de nuestra computadora cuántica. Esto lo hacemos construyendo un circuito dependiente de los datos. Hay muchas formas de hacerlo, y la lección anterior presentó varias opciones. Puedes construir tu propio circuito para codificar tus datos o usar un mapa de características preconstruido como zz_feature_map. En esta lección haremos ambas cosas.

Para calcular un único elemento de la matriz kernel, queremos codificar dos puntos diferentes para poder estimar su producto interno. Un flujo de trabajo completo de kernel cuántico incluye, por supuesto, muchos de estos productos internos entre vectores de datos mapeados, así como métodos clásicos de Machine Learning. Sin embargo, el paso central que se itera es la estimación de un único elemento de la matriz kernel. Para ello, elegimos un circuito cuántico dependiente de los datos y mapeamos dos vectores de datos al espacio de características.

Classical_Review_background_kernel_circuit

Para la tarea de generar una matriz kernel, nos interesa particularmente la probabilidad de medir el estado 0N|0\rangle^{\otimes N}, en el que todos los NN qubits están en el estado 0|0\rangle. Para entender esto: el circuito responsable de la codificación y mapeo de un vector de datos xi\vec{x}_i puede escribirse como Φ(xi)\Phi(\vec{x}_i), y el de xj\vec{x}_j como Φ(xj)\Phi(\vec{x}_j). Los estados mapeados son entonces

ψ(xi)=Φ(xi)0N|\psi(\vec{x}_i)\rangle = \Phi(\vec{x}_i)|0\rangle^{\otimes N} ψ(xj)=Φ(xj)0N.|\psi(\vec{x}_j)\rangle = \Phi(\vec{x}_j)|0\rangle^{\otimes N}.

Estos estados son el mapeo de los datos a dimensiones más altas, por lo que nuestra entrada de kernel deseada es el producto interno

ψ(xj)ψ(xi)=0NΦ(xj)Φ(xi)0N.\langle\psi(\vec{x}_j)|\psi(\vec{x}_i)\rangle = \langle 0 |^{\otimes N}\Phi^\dagger(\vec{x}_j)\Phi(\vec{x}_i)|0\rangle^{\otimes N}.

Si aplicamos al estado inicial estándar 0N|0\rangle^{\otimes N} ambos circuitos Φ(xj)\Phi^\dagger(\vec{x}_j) y Φ(xi)\Phi(\vec{x}_i), la probabilidad de medir después el estado 0N|0\rangle^{\otimes N} es

P0=0NΦ(xj)Φ(xi)0N2.P_0 = |\langle0|^{\otimes N}\Phi^\dagger(\vec{x}_j)\Phi(\vec{x}_i)|0\rangle^{\otimes N}|^2.

Este es exactamente el valor que buscamos (salvo 2||^2). La capa de medición de nuestro circuito proporciona probabilidades de medición (o las llamadas "cuasi-probabilidades", si se utilizan ciertos métodos de mitigación de errores). La probabilidad que nos interesa es la del estado cero, 0N|0\rangle^{\otimes N}.

Paso 2: Optimizar el problema para la ejecución cuántica

  • Entrada: Circuito abstracto, no optimizado para un backend específico
  • Salida: Circuito objetivo y observable, optimizados para el QPU seleccionado

En este paso utilizamos la función generate_preset_pass_manager de Qiskit para establecer una rutina de optimización para nuestro circuito respecto a la computadora cuántica real en la que queremos ejecutar el experimento. Establecemos optimization_level=3, lo que significa que usamos el pass manager preestablecido con el nivel de optimización más alto. "Optimización" aquí se refiere a la optimización de la implementación del circuito en una computadora cuántica real. Esto incluye consideraciones como la selección de qubits físicos que corresponden a los qubits en el circuito cuántico abstracto y minimizan la profundidad de puertas, o la selección de qubits físicos con las tasas de error más bajas disponibles. Esto no tiene relación directa con la optimización del problema de Machine Learning (como con optimizadores clásicos como COBYLA).

Dependiendo de la implementación del Paso 2, es posible que necesites optimizar el circuito más de una vez, ya que cada par de puntos involucrado en un elemento de la matriz produce un circuito diferente para medir.

Paso 3: Ejecución con Qiskit Runtime Primitives

  • Entrada: Circuito objetivo
  • Salida: Distribución de probabilidad

Usa el primitivo Sampler de Qiskit Runtime para reconstruir una distribución de probabilidad de los estados producidos al muestrear el circuito. Ten en cuenta que esto a veces se denomina "distribución de cuasi-probabilidad", un término que se aplica cuando el ruido es un problema y se introducen pasos adicionales, como en la mitigación de errores. En tales casos, la suma de todas las probabilidades no necesariamente es exactamente 1; de ahí "cuasi-probabilidad".

Paso 4: Post-procesamiento, devolver resultado en formato clásico

  • Entrada: Distribución de probabilidad
  • Salida: Un único elemento de la matriz kernel o una matriz kernel si se repite

Calcula la probabilidad de medir 0N|0\rangle^{\otimes N} en el circuito cuántico y llena la matriz kernel en la posición correspondiente a los dos vectores de datos utilizados. Para llenar toda la matriz kernel, debemos realizar un experimento cuántico para cada entrada. Una vez que tenemos una matriz kernel, podemos usarla en muchos algoritmos clásicos de Machine Learning que aceptan pre-calculated kernels. Por ejemplo: qml_svc = SVC(kernel="precomputed"). Entonces podemos usar flujos de trabajo clásicos para aplicar nuestro modelo a nuestros datos de prueba y obtener una puntuación de precisión. Dependiendo de la satisfacción con nuestra precisión, es posible que necesitemos revisar aspectos de nuestro cálculo, como nuestro mapa de características.

Esquema de la lección

En esta lección realizamos estos pasos de diferentes maneras para optimizar tu tiempo en computadoras cuánticas reales. Aplicamos un método de kernel cuántico a:

  • Un único elemento de la matriz kernel para datos con relativamente pocas características, en un backend real, para que podamos seguir fácilmente lo que sucede en cada paso.
  • Un conjunto de datos completo con relativamente pocas características, en un backend simulado, para que podamos ver cómo el flujo de trabajo cuántico se conecta con métodos clásicos de Machine Learning.
  • Un único elemento de la matriz kernel para datos con muchas características, en una computadora cuántica real. No estimamos una matriz kernel completa para un conjunto de datos grande, para respetar el tiempo en las computadoras cuánticas de IBM®.
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy pandas qiskit qiskit-ibm-runtime scikit-learn
# If you have not already, install scikit learn
#!pip install scikit-learn

Elemento único de la matriz kernel

Paso 1: Mapear entradas clásicas a un problema cuántico

Consideremos primero un conjunto de datos con pocas características, digamos 10. El conjunto de datos puede ser de cualquier tamaño, ya que calculamos los elementos de la matriz kernel individualmente. Necesitamos al menos dos puntos, así que comencemos con eso (en el siguiente ejemplo importaremos un conjunto de datos completo). Importemos algunos paquetes necesarios:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# Two mock data points, including category labels, as in training
small_data = [
[-0.194, 0.114, -0.006, 0.301, -0.359, -0.088, -0.156, 0.342, -0.016, 0.143, 1],
[-0.1, 0.002, 0.244, 0.127, -0.064, -0.086, 0.072, 0.043, -0.053, 0.02, -1],
]

# Data points with labels removed, for inner product
train_data = [small_data[0][:-1], small_data[1][:-1]]

Podemos probar la z_feature_map.

# from qiskit.circuit.library import zz_feature_map
# fm = zz_feature_map(feature_dimension=np.shape(train_data)[1], entanglement='linear', reps=1)

from qiskit.circuit.library import z_feature_map

fm = z_feature_map(feature_dimension=np.shape(train_data)[1])

unitary1 = fm.assign_parameters(train_data[0])
unitary2 = fm.assign_parameters(train_data[1])

Las dos unitarias anteriores corresponden exactamente a U1U_1 y U2U_2 de la introducción. Podemos combinarlas usando unitary_overlap. Como siempre, debemos vigilar la profundidad del circuito.

from qiskit.circuit.library import unitary_overlap

overlap_circ = unitary_overlap(unitary1, unitary2)
overlap_circ.measure_all()

print("circuit depth = ", overlap_circ.decompose().depth())
overlap_circ.decompose().draw("mpl", scale=0.6, style="iqp")
circuit depth =  9

Output of the previous code cell

Paso 2: Optimizar el problema para la ejecución cuántica

Comenzamos seleccionando el backend menos ocupado y luego optimizamos nuestro circuito para la ejecución en ese backend.

# Import needed packages
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService

# Get the least busy backend
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=fm.num_qubits
)
print(backend)
<IBMBackend('ibm_brisbane')>
# Apply level 3 optimization to our overlap circuit
pm = generate_preset_pass_manager(optimization_level=3, backend=backend)
overlap_ibm = pm.run(overlap_circ)

Para circuitos complejos, este paso aumenta significativamente la profundidad del circuito, ya que se realiza el mapeo a puertas nativas para computadoras cuánticas reales y la información debe moverse de qubit a qubit. En este caso sencillo, la profundidad apenas se ve afectada.

print("circuit depth = ", overlap_ibm.decompose().depth())
overlap_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)
circuit depth =  10
1

Paso 3: Ejecución con Qiskit Runtime Primitives

La sintaxis para la ejecución en un simulador está comentada abajo. Para este conjunto de datos con un número reducido de características, la ejecución en un simulador sigue siendo una opción. Para cálculos a escala de utilidad, la simulación generalmente no es practicable. Los simuladores solo deben usarse para depurar código simplificado.

# Run this for a simulator
# from qiskit.primitives import StatevectorSampler

# from qiskit_ibm_runtime import Options, Session, Sampler

# num_shots = 10000

# Evaluate the problem using state vector-based primitives from Qiskit
# sampler = StatevectorSampler()
# results = sampler.run([overlap_circ], shots=num_shots).result()
# .get_counts() returns counts associated with a state labeled by bit results such as |001101...01>.
# counts_bit = results[0].data.meas.get_counts()
# .get_int_counts returns the same counts, but labeled by integer equivalent of the above bit string.
# counts = results[0].data.meas.get_int_counts()
# Benchmarked on an Eagle processor, 7-11-24, took 4 sec.

# Import our runtime primitive
from qiskit_ibm_runtime import Session, SamplerV2 as Sampler

num_shots = 10000

# Use sampler and get the counts

sampler = Sampler(mode=backend)
results = sampler.run([overlap_ibm], shots=num_shots).result()
# .get_counts() returns counts associated with a state labeled by bit results such as |001101...01>.
counts_bit = results[0].data.meas.get_counts()
# .get_int_counts returns the same counts, but labeled by integer equivalent of the above bit string.
counts = results[0].data.meas.get_int_counts()

Paso 4: Post-procesamiento, devolver resultado en formato clásico

Como se describe en la introducción, la medición más útil aquí es la probabilidad de medir el estado cero 00000|00000\rangle.

counts.get(0, 0.0) / num_shots
0.6525

Este es el resultado que queríamos: una estimación del producto interno (salvo módulo cuadrado) de los vectores correspondientes a dos puntos de datos. Si queremos examinar la distribución completa de probabilidades de medición (o cuasi-probabilidades), podemos hacerlo con la función plot_distribution, como se muestra abajo. Se puede ver que tales imágenes se vuelven rápidamente confusas para un gran número de qubits.

from qiskit.visualization import plot_distribution

plot_distribution(counts_bit)

Output of the previous code cell

Alternativamente, se puede definir una visualización como la siguiente para observar solo las 10 mediciones más probables. Esto puede ser útil para depuración o para ganar intuición sobre los datos. Sin embargo, la probabilidad de medición del estado cero es nuestro elemento de la matriz kernel.

def visualize_counts(probs, num_qubits):
"""Visualize the outputs from the Qiskit Sampler primitive."""
zero_prob = probs.get(0, 0.0)
top_10 = dict(sorted(probs.items(), key=lambda item: item[1], reverse=True)[:10])
top_10.update({0: zero_prob})
by_key = dict(sorted(top_10.items(), key=lambda item: item[0]))
xvals, yvals = list(zip(*by_key.items()))
xvals = [bin(xval)[2:].zfill(num_qubits) for xval in xvals]
plt.bar(xvals, yvals)
plt.xticks(rotation=75)
plt.title("Results of sampling")
plt.xlabel("Measured bitstring")
plt.ylabel("Counts")
plt.show()

visualize_counts(counts, overlap_circ.num_qubits)

Output of the previous code cell

A partir de esta información sobre un solo producto interno entre dos puntos de datos en el espacio de características de mayor dimensión, solo podemos decir que su superposición es bastante grande en comparación con la superposición máxima (que sería 1,0). Esto podría ser un indicio de que estos dos puntos de datos son de alguna manera de naturaleza similar y pertenecen a la misma clase. O podría indicar que nuestro mapa de características no es eficaz para mapear a un espacio donde datos similares tienen una gran superposición y datos diferentes tienen una superposición pequeña. Para averiguar cuál de las dos opciones es correcta, debemos aplicar nuestro mapa de características a todo el conjunto de datos y verificar si la matriz kernel resultante puede manipularse para separar clases con alta precisión de manera efectiva.

Cabe destacar que usamos la z_feature_map, lo que resultó en una baja profundidad de dos qubits transpilada (de hecho, profundidad 1). Si tus circuitos se vuelven demasiado profundos, esto ciertamente provocará mucho ruido, y la probabilidad de medir el estado cero será muy baja, incluso si tu mapa de características se ajusta bien a tus datos. Una repetición del proceso anterior con la zz_feature_map y , entanglement='linear', reps=1 produjo, por ejemplo, dist.get(0,0.0) = 0.0015 con los mismos puntos de datos. Esto se debe a las profundidades de circuito y profundidades de dos qubits significativamente mayores de la zz_feature_map. La siguiente figura muestra la distribución de probabilidad para este cálculo.

Bad results from a zz feature map.

Vale la pena experimentar con algunos puntos de datos de la misma categoría para averiguar cuán baja debe ser la profundidad para obtener buenos resultados. El siguiente consejo es una guía aproximada que ciertamente tiene excepciones. En general, una profundidad de dos qubits transpilada de 10 o menos no debería ser un problema. Una profundidad de dos qubits transpilada de 50-60 corresponde al estado del arte y requiere mitigación de errores avanzada y otras herramientas. En el medio, tus resultados pueden variar según la similitud de los datos, la expresividad del mapa de características, el ancho del circuito y otros factores.

Normalmente, el paso de post-procesamiento también incluiría procesos clásicos de Machine Learning. En la siguiente sección, extenderemos este proceso a un conjunto de datos completo y mostraremos el flujo de trabajo clásico de Machine Learning.

Preguntas de comprensión

Lee las siguientes preguntas, piensa en las respuestas y luego haz clic en los triángulos para ver las soluciones.

¿Cuántos estados diferentes pueden medirse generalmente en un circuito cuántico de 10 qubits?

Respuesta:

2102^{10} o 1024.

Supongamos que alguien nuevo en la computación cuántica intenta usar un circuito cuántico con una profundidad de dos qubits muy alta, sin emplear mitigación de errores. Supongamos que esto conduce a una tasa de error del 10% por qubit. Si el elemento real (sin errores) de la matriz kernel de este circuito es muy grande, digamos 1,0, ¿cuál sería la probabilidad de medir todos los 10 qubits en el estado |0>?

Respuesta:

La probabilidad de que cada qubit se mida correctamente en el estado |0> es 0,90. La probabilidad de que los 10 qubits se midan en el estado correcto es 0,90100,90^{10}, aproximadamente un 35%.

Explica con tus propias palabras por qué es tan importante monitorear las profundidades de los circuitos. Esto se aplica en general, pero explícalo en el contexto de la estimación de kernel cuántico.

Respuesta:

En este flujo de trabajo de QKE, nuestras estimaciones se basan en las mediciones del estado cero, es decir, el estado en el que cada qubit se encuentra en el estado 0|0\rangle. Circuitos muy profundos conducen a altas tasas de error. Cuando esta tasa de error se acumula sobre muchos qubits, la probabilidad de medir el estado cero se reduce significativamente.

Matriz kernel completa

En esta sección extendemos el proceso anterior a la clasificación binaria de un conjunto de datos completo. Esto trae dos componentes importantes: (1) ahora podemos implementar Machine Learning clásico en el post-procesamiento, y (2) podemos obtener puntuaciones de precisión para nuestro entrenamiento.

Paso 1: Mapear entradas clásicas a un problema cuántico

Ahora importamos un conjunto de datos existente para nuestra clasificación. Este conjunto de datos consta de 128 filas (puntos de datos) y 14 características por punto. Hay un 15.° elemento que indica la categoría binaria de cada punto (±1\pm 1). El conjunto de datos se importa a continuación; también puedes ver el conjunto de datos y examinar su estructura aquí.

Usamos los primeros 90 puntos de datos para el entrenamiento y los siguientes 30 puntos para las pruebas.

!wget https://raw.githubusercontent.com/qiskit-community/prototype-quantum-kernel-training/main/data/dataset_graph7.csv

df = pd.read_csv("dataset_graph7.csv", sep=",", header=None)

# Prepare training data

train_size = 90
X_train = df.values[0:train_size, :-1]
train_labels = df.values[0:train_size, -1]

# Prepare testing data
test_size = 30
X_test = df.values[train_size : train_size + test_size, :-1]
test_labels = df.values[train_size : train_size + test_size, -1]
--2024-07-11 23:05:22--  https://raw.githubusercontent.com/qiskit-community/prototype-quantum-kernel-training/main/data/dataset_graph7.csv
Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.110.133, 185.199.111.133, 185.199.109.133, ...
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.110.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 49405 (48K) [text/plain]
Saving to: ‘dataset_graph7.csv.15’

dataset_graph7.csv. 100%[===================>] 48.25K --.-KB/s in 0.02s

2024-07-11 23:05:23 (2.11 MB/s) - ‘dataset_graph7.csv.15’ saved [49405/49405]

Preparamos el almacenamiento de múltiples salidas construyendo una matriz kernel y una matriz de prueba en dimensiones adecuadas.

# Empty kernel matrix
num_samples = np.shape(X_train)[0]
kernel_matrix = np.full((num_samples, num_samples), np.nan)
test_matrix = np.full((test_size, num_samples), np.nan)

Ahora creamos un mapa de características para codificar y mapear nuestros datos clásicos en un circuito cuántico. Podemos construir nuestro propio mapa de características o usar uno preconstruido. Siéntete libre de modificar el mapa de características a continuación o volver a la ZFeatureMap. Pero siempre vigila la profundidad del circuito. Recuerda: en el ejemplo anterior de 6 qubits, la profundidad transpilada del circuito al usar la zz_feature_map no era manejable. A medida que aumenta el tamaño y la complejidad del circuito, la profundidad puede crecer tan rápido que el ruido abrume nuestros resultados. Si sabes algo sobre la estructura de tus datos que sugiera qué estructura de mapa de características sería más útil, se recomienda crear tu propio mapa de características personalizado que aproveche ese conocimiento.

from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit

# Prepare feature map for computing overlap
num_features = np.shape(X_train)[1]
num_qubits = int(num_features / 2)

# To use a custom feature map use the lines below.
entangler_map = [[0, 2], [3, 4], [2, 5], [1, 4], [2, 3], [4, 6]]

fm = QuantumCircuit(num_qubits)
training_param = Parameter("θ")
feature_params = ParameterVector("x", num_qubits * 2)
fm.ry(training_param, fm.qubits)
for cz in entangler_map:
fm.cz(cz[0], cz[1])
for i in range(num_qubits):
fm.rz(-2 * feature_params[2 * i + 1], i)
fm.rx(-2 * feature_params[2 * i], i)

Pasos 2 y 3: Optimizar el problema y ejecutar con Primitives

Creamos un circuito de superposición (overlap) y, si en este ejemplo ejecutáramos en una computadora cuántica real, lo optimizaríamos como antes para la ejecución. Sin embargo, en este caso pretendemos recorrer todos los puntos de datos y calcular la matriz kernel completa. Para cada par de vectores de datos xi\vec{x}_i y xj\vec{x}_j creamos un circuito de superposición diferente. Por lo tanto, debemos optimizar nuestro circuito para cada par de puntos de datos. Los Pasos 2 y 3 se realizarían conjuntamente en las múltiples iteraciones.

La siguiente celda de código ejecuta exactamente el mismo proceso que antes para un par de puntos de datos. Esta vez simplemente se ejecuta dentro de dos bucles for, y hay una línea adicional al final kernel_matrix[x_1,x_2] = ... para almacenar los resultados de cada cálculo. Ten en cuenta que hemos aprovechado la simetría de una matriz kernel para reducir el número de cálculos a la mitad. También hemos establecido los elementos diagonales en 1, ya que así deberían ser en ausencia de ruido. Dependiendo de tu implementación y la precisión requerida, también podrías usar los elementos diagonales para estimar el ruido u obtener información para la mitigación de errores.

Una vez que la matriz kernel está completamente llena, repetimos el proceso para los datos de prueba y llenamos la test_matrix. En realidad es también una matriz kernel; solo le damos un nombre diferente para distinguir ambas.

# To use a simulator
from qiskit.primitives import StatevectorSampler

# Remember to insert your token in the QiskitRuntimeService constructor to use real quantum computers
# service = QiskitRuntimeService()
# backend = service.least_busy(
# operational=True, simulator=False, min_num_qubits=fm.num_qubits
# )

num_shots = 10000

# Evaluate the problem using state vector-based primitives from Qiskit.
sampler = StatevectorSampler()

for x1 in range(0, train_size):
for x2 in range(x1 + 1, train_size):
unitary1 = fm.assign_parameters(list(X_train[x1]) + [np.pi / 2])
unitary2 = fm.assign_parameters(list(X_train[x2]) + [np.pi / 2])

# Create the overlap circuit
overlap_circ = unitary_overlap(unitary1, unitary2)
overlap_circ.measure_all()

# These lines run the qiskit sampler primitive.
counts = (
sampler.run([overlap_circ], shots=num_shots)
.result()[0]
.data.meas.get_int_counts()
)

# Assign the probability of the 0 state to the kernel matrix, and the transposed element (since this is an inner product)
kernel_matrix[x1, x2] = counts.get(0, 0.0) / num_shots
kernel_matrix[x2, x1] = counts.get(0, 0.0) / num_shots
# Fill in on-diagonal elements with 1, again, since this is an inner-product corresponding to probability (or alter the code to check these entries and verify they yield 1)
kernel_matrix[x1, x1] = 1

print("training done")

# Similar process to above, but for testing data.
for x1 in range(0, test_size):
for x2 in range(0, train_size):
unitary1 = fm.assign_parameters(list(X_test[x1]) + [np.pi / 2])
unitary2 = fm.assign_parameters(list(X_train[x2]) + [np.pi / 2])

# Create the overlap circuit
overlap_circ = unitary_overlap(unitary1, unitary2)
overlap_circ.measure_all()

counts = (
sampler.run([overlap_circ], shots=num_shots)
.result()[0]
.data.meas.get_int_counts()
)

test_matrix[x1, x2] = counts.get(0, 0.0) / num_shots

print("test matrix done")
training done
test matrix done

Paso 4: Post-procesamiento, devolver resultado en formato clásico

Dado que ahora tenemos una matriz kernel y una test_matrix formateada correspondiente a partir de métodos de kernel cuántico, podemos aplicar algoritmos clásicos de Machine Learning para hacer predicciones sobre nuestros datos de prueba y verificar su precisión. Comenzamos importando sklearn.svc de Scikit-Learn, un Support Vector Classifier (SVC). Debemos especificar que el SVC use nuestro kernel precalculado: kernel = precomputed.

# import a support vector classifier from a classical ML package.
from sklearn.svm import SVC

# Specify that you want to use a pre-computed kernel matrix
qml_svc = SVC(kernel="precomputed")

Con SVC.fit ahora podemos introducir la matriz kernel y las etiquetas de entrenamiento para obtener un ajuste. SVC.score evalúa entonces nuestros datos de prueba contra este ajuste usando nuestra test_matrix y devuelve nuestra precisión.

# Feed in the pre-computed matrix and the labels of the training data. The classical algorithm gives you a fit.
qml_svc.fit(kernel_matrix, train_labels)

# Now use the .score to test your data, using the matrix of test data, and test labels as your inputs.
qml_score_precomputed_kernel = qml_svc.score(test_matrix, test_labels)
print(f"Precomputed kernel classification test score: {qml_score_precomputed_kernel}")
Precomputed kernel classification test score: 1.0

Vemos que la precisión de nuestro modelo entrenado es del 100%. Esto es excelente y demuestra que la QKE puede funcionar. Sin embargo, está lejos de ser una ventaja cuántica. Los kernels clásicos probablemente habrían resuelto este problema de clasificación también con un 100% de precisión. Queda mucho trabajo en la caracterización de diferentes tipos de datos y relaciones de datos para averiguar dónde los kernels cuánticos serán más útiles en la era actual de utilidad.

Te dejamos modificar partes de este flujo de trabajo e investigar la efectividad de diferentes mapas de características cuánticos. Aquí hay algunas cosas para considerar:

  • ¿Qué tan robusta es la precisión? ¿Se aplica a tipos de datos amplios o solo a estos datos de entrenamiento específicos?
  • ¿Qué estructura en tus datos te hace pensar que un mapa de características cuántico es útil?
  • ¿Cómo se ve afectada la precisión al aumentar/disminuir la cantidad de datos de entrenamiento?
  • ¿Qué mapas de características puedes usar y cómo varían los resultados con los mapas de características?
  • ¿Cómo se ven afectadas la precisión y el tiempo de ejecución al aumentar el número de características?
  • ¿Qué tendencias, si las hay, esperas ver en computadoras cuánticas reales?

Escalado a más características y qubits

En esta sección repetimos el cálculo de un único elemento de la matriz, pero para un número significativamente mayor de características, mostrando así el camino hacia el escalado hacia la utilidad. La restricción a un único elemento de la matriz se hace para que el proceso pueda mostrarse sin consumir demasiado de tu tiempo asignado en computadoras cuánticas.

Paso 1: Mapear entradas clásicas a un problema cuántico

Partimos de un conjunto de datos donde cada punto de datos tiene 42 características. Como en el primer ejemplo, calculamos un único elemento de la matriz kernel, para lo cual necesitamos dos puntos de datos. Los dos puntos siguientes tienen 42 características y una única variable de categoría (±1\pm 1).

# Two mock data points, including category labels, as in training

large_data = [
[
-0.028,
-1.49,
-1.698,
0.107,
-1.536,
-1.538,
-1.356,
-1.514,
-0.109,
-1.8,
-0.122,
-1.651,
-1.955,
-0.123,
-1.732,
0.091,
-0.048,
-0.128,
-0.026,
0.082,
-1.263,
0.065,
0.004,
-0.055,
-0.08,
-0.173,
-1.734,
-0.39,
-1.451,
0.078,
-1.578,
-0.025,
-0.184,
-0.119,
-1.336,
0.055,
-0.204,
-1.578,
0.132,
-0.121,
-1.599,
-0.187,
-1,
],
[
-1.414,
-1.439,
-1.606,
0.246,
-1.673,
0.002,
-1.317,
-1.262,
-0.178,
-1.814,
0.013,
-1.619,
-1.86,
-0.25,
-0.212,
-0.214,
-0.033,
0.071,
-0.11,
-1.607,
0.441,
-0.143,
-0.009,
-1.655,
-1.579,
0.381,
-1.86,
-0.079,
-0.088,
-0.058,
-1.481,
-0.064,
-0.065,
-1.507,
0.177,
-0.131,
-0.153,
0.07,
-1.627,
0.593,
-1.547,
-0.16,
-1,
],
]
train_data = [large_data[0][:-1], large_data[1][:-1]]

Recuerda: la zz_feature_map ya producía circuitos bastante profundos con relativamente pocas características (14 características). Con un número creciente de características, debemos observar la profundidad del circuito con atención. Para ilustrar esto, primero intentamos la zz_feature_map y comprobamos la profundidad del circuito resultante.

from qiskit.circuit.library import zz_feature_map

fm = zz_feature_map(
feature_dimension=np.shape(train_data)[1], entanglement="linear", reps=1
)

unitary1 = fm.assign_parameters(train_data[0])
unitary2 = fm.assign_parameters(train_data[1])
from qiskit.circuit.library import unitary_overlap

overlap_circ = unitary_overlap(unitary1, unitary2)
overlap_circ.measure_all()

print("circuit depth = ", overlap_circ.decompose(reps=2).depth())
print(
"two-qubit depth",
overlap_circ.decompose().depth(lambda instr: len(instr.qubits) > 1),
)
# overlap_circ.draw("mpl", scale=0.6, style="iqp")
circuit depth =  251
two-qubit depth 165

Como se describió anteriormente, la determinación exacta de cuándo un circuito es demasiado profundo es una cuestión con matices. Pero una profundidad de dos qubits de más de 100, aún antes de la transpilación, es un criterio eliminatorio. Por eso se han enfatizado los mapas de características personalizados a lo largo de esta lección. Si sabes algo sobre la estructura de todo tu conjunto de datos, deberías diseñar un mapa de entrelazamiento que tenga en cuenta esa estructura. Dado que aquí solo calculamos el producto interno entre dos puntos de datos, hemos dado prioridad a una baja profundidad de circuito sobre una consideración detallada de la estructura de los datos.

from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit

# Prepare feature map for computing overlap

entangler_map = [
[3, 4],
[2, 5],
[1, 4],
[2, 3],
[4, 6],
[7, 9],
[10, 11],
[9, 12],
[8, 11],
[9, 10],
[11, 13],
[14, 16],
[17, 18],
[16, 19],
[15, 18],
[16, 17],
[18, 20],
]
# Use the entangler map above to build a feature map

num_features = np.shape(train_data)[1]
num_qubits = int(num_features / 2)

fm = QuantumCircuit(num_qubits)
training_param = Parameter("θ")
feature_params = ParameterVector("x", num_qubits * 2)
fm.ry(training_param, fm.qubits)
for cz in entangler_map:
fm.cz(cz[0], cz[1])
for i in range(num_qubits):
fm.rz(-2 * feature_params[2 * i + 1], i)
fm.rx(-2 * feature_params[2 * i], i)
from qiskit.circuit.library import unitary_overlap

# Assign features of each data point to a unitary, an instance of the general feature map.

unitary1 = fm.assign_parameters(list(train_data[0]) + [np.pi / 2])
unitary2 = fm.assign_parameters(list(train_data[1]) + [np.pi / 2])

# Create the overlap circuit

overlap_circ = unitary_overlap(unitary1, unitary2)
overlap_circ.measure_all()

No verificamos las profundidades por ahora, ya que lo que realmente importa es la profundidad de dos qubits transpilada.

Paso 2: Optimizar el problema para la ejecución cuántica

Comenzamos seleccionando el backend menos ocupado y luego optimizamos nuestro circuito para la ejecución en ese backend.

# Import needed packages
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService

# Get the least busy backend
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=fm.num_qubits
)
print(backend)
<IBMBackend('ibm_brisbane')>

Para trabajos pequeños, un pass manager preestablecido suele devolver de forma fiable el mismo circuito con la misma profundidad. Sin embargo, para circuitos muy grandes y complejos, el pass manager puede devolver circuitos transpilados diferentes en cada ejecución. Esto se debe a que utiliza heurísticas y los circuitos muy grandes presentan un panorama complicado de posibles optimizaciones. A menudo tiene sentido transpilar varias veces y tomar el circuito más superficial. Esto solo genera sobrecarga clásica y puede mejorar significativamente los resultados de la computadora cuántica.

Aquí transpilamos el circuito de superposición unitaria 20 veces y observamos las profundidades de los circuitos obtenidos.

# Apply level 3 optimization to our overlap circuit
transpiled_qcs = []
transpiled_depths = []
transpiled_twoqubit_depths = []
for i in range(1, 20):
pm = generate_preset_pass_manager(optimization_level=3, backend=backend)
overlap_ibm = pm.run(overlap_circ)
transpiled_qcs.append(overlap_ibm)
transpiled_depths.append(overlap_ibm.decompose().depth())
transpiled_twoqubit_depths.append(
overlap_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)
)

print("circuit depth = ", overlap_ibm.decompose().depth())
circuit depth =  61
print(transpiled_depths)
print(transpiled_twoqubit_depths)
[61, 60, 60, 69, 60, 60, 60, 65, 60, 60, 69, 61, 77, 77, 65, 60, 60, 77, 61]
[13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13]

Aquí se ve que hay cierta variación en la profundidad total de puertas en diferentes ejecuciones de transpilación. Nuestro circuito aún no es lo suficientemente profundo/ancho para ver variación en las profundidades de dos qubits transpiladas. Usamos transpiled_qcs[1], que tiene una profundidad de 60, algo menor que la profundidad del circuito más profundo obtenido, que fue 77.

overlap_ibm = transpiled_qcs[1]

Paso 3: Ejecución con Qiskit Runtime Primitives

A medida que nos acercamos a la escala de utilidad, los simuladores dejarán de ser útiles. Aquí solo se muestra la sintaxis para computadoras cuánticas reales.

# Run on ibm_osaka, 7-12-24, required 22 sec.

# Import our runtime primitive
from qiskit_ibm_runtime import SamplerV2 as Sampler

# Open a Runtime session:
session = Session(backend=backend)
num_shots = 10000
# Use sampler and get the counts

sampler = Sampler(mode=session)
options = sampler.options
options.dynamical_decoupling.enable = True
options.twirling.enable_gates = True
counts = (
sampler.run([overlap_ibm], shots=num_shots).result()[0].data.meas.get_int_counts()
)

# Close session after done
session.close()

Paso 4: Post-procesamiento, devolver resultado en formato clásico

Como se describe en la introducción, la medición más útil aquí es la probabilidad de medir el estado cero 00000|00000\rangle.

counts.get(0, 0.0) / num_shots
0.0138

Este proceso para el único elemento de la matriz kernel podría repetirse entre otros emparejamientos de datos en tu conjunto de datos para obtener la matriz kernel completa. La dimensión de la matriz kernel está determinada por el número de puntos en tus datos de entrenamiento, no por el número de características. Por lo tanto, el esfuerzo computacional para convertir la matriz kernel en un modelo predictivo no escala como el número de características o qubits. Incluso con conjuntos de datos relativamente pequeños con grandes números de características, los datos aún necesitarían ser emparejados con un mapa de características que permita una clasificación efectiva.

Escalado y trabajo futuro

El método de kernel requiere que midamos el estado 0|0\rangle con la mayor precisión posible. Sin embargo, los errores de puerta y los errores de lectura significan que hay una probabilidad distinta de cero pp de que un qubit dado se mida erróneamente en el estado 1|1\rangle. Incluso con la suposición simplificadora de que la probabilidad de 0|0\rangle debería ser del 100%, con muchas características codificadas en NN bits, la probabilidad de medir todos los bits correctamente como 0|0\rangle se reduce a (1p)N(1-p)^N. A medida que NN aumenta, este método se vuelve cada vez más poco fiable. Superar esta dificultad y escalar la estimación del kernel a cada vez más características es un área de investigación activa. Aprende más sobre este problema en este trabajo de Thanasilp, Wang, Cerezo y Holmes. Te animamos a explorar lo que es posible con las computadoras cuánticas actuales, y también a mirar hacia adelante a lo que será posible en la era de la corrección de errores.

Resumen

El cálculo de un kernel cuántico incluye:

  • el cálculo de entradas de la matriz kernel a partir de pares de puntos de datos de entrenamiento
  • la codificación de los datos y su mapeo a través de un mapa de características
  • la optimización de tu circuito para la ejecución en computadoras cuánticas reales / backends

El kernel cuántico puede entonces usarse en algoritmos clásicos de Machine Learning, como se muestra en esta lección.

Algunas cosas importantes que debes tener en cuenta al usar kernels cuánticos:

  • ¿Es probable que el conjunto de datos se beneficie de métodos de kernel cuántico?
  • Prueba diferentes mapas de características y esquemas de entrelazamiento.
  • ¿Es aceptable la profundidad del circuito?
  • Ejecuta el pass manager varias veces y usa el circuito con la menor profundidad que puedas obtener.

Los métodos de kernel cuántico son herramientas potencialmente poderosas cuando existe una buena correspondencia entre conjuntos de datos con características aptas para lo cuántico y un mapa de características cuántico adecuado. Para comprender mejor dónde los kernels cuánticos probablemente serán útiles, recomendamos la lectura de Liu, Arunachalam & Temme (2021).