Núcleos cuánticos
Introducción a los núcleos cuánticos
El "método del núcleo cuántico" se refiere a cualquier método que utilice ordenadores cuánticos para estimar un núcleo. En este contexto, "núcleo" se referirá a la matriz del núcleo o a entradas individuales de la misma. Recordemos que un mapeo de características es un mapeo de a donde normalmente y donde 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 cartografiado como argumentos y devuelve su producto interno, es decir, con . Clásicamente, nos interesan los mapas de características para los que la función kernel es fácil de evaluar. A menudo, esto significa encontrar una función de núcleo para la que el producto interno en el espacio mapeado de características pueda escribirse en términos de los vectores de datos originales, sin tener que construir nunca y . En el método de los núcleos cuánticos, el mapeo de características se realiza mediante un circuito cuántico, y el núcleo se estima utilizando mediciones en ese circuito y las probabilidades de medición relativas.
En esta lección examinaremos las profundidades de los circuitos de codificación precodificados que utilizan un entrelazamiento sustancial y las compararemos con las profundidades de los circuitos que codificamos a mano. No se trata de defender un método frente a otro. Puede que descubras que los circuitos precodificados son demasiado profundos y que el enredo del circuito construido a medida es insuficiente para ser útil. De nuevo, se muestran sólo para permitir su exploración.
Antes de recorrer en detalle la estimación de una matriz kernel, vamos a esbozar el flujo de trabajo utilizando el lenguaje de los patrones Qiskit.
Paso 1: Asignar entradas clásicas a un problema cuántico
- Entrada: Conjunto de datos de entrenamiento
- Salida: Circuito abstracto para calcular una entrada de la matriz del núcleo
Dado el conjunto de datos, el punto de partida es codificarlos en un circuito cuántico. En otras palabras, necesitamos mapear nuestros datos en el espacio de Hilbert de estados de nuestro ordenador cuántico. Para ello, construimos un circuito dependiente de los datos. Hay muchas maneras de hacerlo, y en la lección anterior se esbozaron varias opciones. Puedes construir tu propio circuito para codificar los datos o utilizar un mapa de características prefabricado como zz_feature_map. En esta lección haremos ambas cosas.
Tenga en cuenta que para calcular un único elemento de la matriz del núcleo, querremos codificar dos puntos diferentes, de modo que podamos estimar su producto interior. Un flujo de trabajo de núcleo cuántico completo implicará, por supuesto, muchos de estos productos internos entre vectores de datos mapeados, así como métodos clásicos de aprendizaje automático. Pero el paso central que se itera es la estimación de un único elemento de la matriz del núcleo. Para ello, seleccionamos un circuito cuántico dependiente de los datos y mapeamos dos vectores de datos en el espacio de características.
Para la tarea de generar una matriz kernel, estamos particularmente interesados en la probabilidad de medir el estado , en el que todos los qubits están en el estado . Para ver esto, considere que el circuito responsable de la codificación y mapeo de un vector de datos se puede escribir como , y el responsable de la codificación y mapeo es , y denote los estados mapeados
Estos estados son el mapeo de los datos a dimensiones superiores, por lo que nuestra entrada deseada en el núcleo es el producto interior
Si operamos sobre el estado inicial por defecto con ambos circuitos y , la probabilidad de medir entonces el estado es
Este es exactamente el valor que queremos (hasta ). La capa de medición de nuestro circuito devolverá probabilidades de medición (o las llamadas "cuasi-probabilidades", si se utilizan ciertos métodos de mitigación de errores). La probabilidad de interés es la del estado cero, .
Paso 2: Optimizar el problema para la ejecución cuántica
- Entrada: Circuito abstracto, no optimizado para un backend concreto
- Salida: Circuito objetivo y observable, optimizado para la QPU seleccionada
En este paso, utilizaremos la función generate_preset_pass_manager de Qiskit para especificar una rutina de optimización para nuestro circuito con respecto al ordenador cuántico real en el que planeamos realizar el experimento. Establecemos optimization_level=3 , lo que significa que utilizaremos el gestor de pases preestablecido que proporciona el máximo nivel de optimización. En este contexto, "optimización" se refiere a la optimización de la implementación del circuito en un ordenador cuántico real. Esto incluye consideraciones como la selección de qubits físicos que se correspondan con los qubits del circuito cuántico abstracto para minimizar la profundidad de compuerta, o la selección de qubits físicos con las tasas de error más bajas disponibles. Esto no está directamente relacionado con la optimización del problema de aprendizaje automático (como en los optimizadores clásicos como COBYLA).
Dependiendo de cómo implemente el paso 2, es posible que tenga que optimizar el circuito más de una vez, ya que cada par de puntos implicados en un elemento matricial produce un circuito diferente para ser medido.
Paso 3: Ejecutar utilizando primitivas de Qiskit Runtime
- Entrada: Circuito objetivo
- Salida: Distribución de probabilidad
Utilice la primitiva Sampler de Qiskit Runtime para reconstruir una distribución de probabilidad de los estados obtenidos del muestreo del circuito. Tenga en cuenta que es posible que esto se denomine "distribución cuasi-probabilística", un término que se aplica cuando el ruido es un problema y cuando se introducen pasos adicionales, como en la mitigación de errores. En estos casos, la suma de todas las probabilidades puede no ser exactamente igual a 1; de ahí lo de "cuasiprobabilidad".
Paso 4: Posprocesamiento, devolución del resultado en formato clásico
- Entrada: Distribución de probabilidad
- Salida: Un único elemento de la matriz del núcleo, o una matriz del núcleo si se repite
Calcular la probabilidad de medir en el circuito cuántico, y poblar la matriz del núcleo en la posición correspondiente a los dos vectores de datos utilizados. Para rellenar toda la matriz del núcleo, necesitamos realizar un experimento cuántico para cada entrada. Una vez que tenemos una matriz kernel, podemos utilizarla en muchos algoritmos clásicos de aprendizaje automático que aceptan pre-calculated kernels. Por ejemplo: qml_svc = SVC(kernel="precomputed"). A continuación, podemos utilizar los flujos de trabajo clásicos para aplicar nuestro modelo a los datos de prueba y obtener una puntuación de precisión. Dependiendo de nuestra satisfacción con la puntuación de precisión, puede que tengamos que revisar aspectos de nuestro cálculo, como nuestro mapa de características.
Esquema de la lección
En esta lección llevaremos a cabo estos pasos de varias formas para hacer un uso óptimo de tu tiempo en ordenadores cuánticos reales. Aplicaremos un método de núcleo cuántico para
- Una única entrada de matriz de núcleo para datos con relativamente pocas características, utilizando un backend real, para que podamos seguir fácilmente lo que ocurre en cada paso.
- Un conjunto completo de datos con relativamente pocas características, utilizando un backend simulado, para que podamos ver cómo el flujo de trabajo cuántico se conecta con los métodos clásicos de aprendizaje automático
- Una única entrada de matriz de núcleo para datos con muchas características, utilizando un ordenador cuántico real. No estimaremos una matriz kernel completa para un gran conjunto de datos, a fin de respetar el tiempo en los ordenadores cuánticos de IBM®.
# If you have not already, install scikit learn
#!pip install scikit-learnEntrada de matriz de núcleo único
Paso 1: Asignar 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 tan grande como se desee, ya que estamos calculando los elementos de la matriz del núcleo de uno en uno. Necesitamos al menos dos puntos, así que empezaremos por ahí (en el siguiente ejemplo, importaremos un conjunto de datos completo). Vamos a importar 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 a utilizar la dirección 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])Los dos unitarios anteriores corresponden exactamente a y descritos en la introducción. Podemos combinarlos utilizando unitary_overlap. Como siempre, queremos vigilar la profundidad de nuestro 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")Output:
circuit depth = 9
Paso 2: Optimizar el problema para la ejecución cuántica
Comenzamos seleccionando el backend menos ocupado y luego optimizamos nuestro circuito para que se ejecute 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)Output:
<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)En el caso de circuitos complicados, este paso aumentará sustancialmente la profundidad del circuito, ya que se corresponde con las puertas nativas de los ordenadores cuánticos reales, y puede que sea necesario trasladar la información de qubit a qubit. En este sencillo caso, la profundidad apenas se ve afectada.
print("circuit depth = ", overlap_ibm.decompose().depth())
overlap_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)Output:
circuit depth = 10
1
Paso 3: Ejecutar utilizando primitivas de Qiskit Runtime
A continuación se comenta la sintaxis para la ejecución en un simulador. 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 los cálculos a escala comercial, la simulación no suele ser factible. Los simuladores sólo deben utilizarse para depurar código a escala.
# 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: Posprocesamiento, devolución del resultado en formato clásico
Como se describe en la introducción, la medida más útil aquí es la probabilidad de medir el estado cero .
counts.get(0, 0.0) / num_shotsOutput:
0.6525
Este es el resultado que queríamos: una estimación del producto interno (hasta mod al cuadrado) de los vectores correspondientes a dos puntos de datos. Si queremos ver la distribución completa de probabilidades de medición (o cuasiprobabilidades), podemos hacerlo utilizando la función plot_distribution , como se muestra a continuación. Se ve que para un gran número de qubits, imágenes como ésta se vuelven rápidamente intratables.
from qiskit.visualization import plot_distribution
plot_distribution(counts_bit)Output:
Otra posibilidad es definir una visualización como la que se muestra a continuación para examinar únicamente las 10 mediciones más probables. Esto podría ser importante para la resolución de problemas o para tratar de intuir mejor los datos. Pero la probabilidad de medición del estado cero es nuestro elemento de la matriz del núcleo.
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:
A partir de esta información sobre un único producto interno entre dos puntos de datos en el espacio de características de mayor dimensión, todo lo que podemos decir es que su solapamiento es bastante grande en comparación con el solapamiento máximo (que sería 1.0 ). Esto podría ser un indicador de que estos dos puntos de datos son de alguna manera similares en naturaleza y serán categorizados en las mismas clases de clase. O podría ser un indicador de que nuestro mapa de características no es eficaz a la hora de mapear un espacio en el que los datos similares se solapan mucho y los datos diferentes se solapan poco. Para saber qué es cierto, debemos aplicar nuestro mapa de características a todo el conjunto de datos y ver si la matriz kernel resultante puede manipularse para separar eficazmente las clases con gran precisión.
Vale la pena señalar que utilizamos el z_feature_map que dio lugar a una baja profundidad transpilada de dos qubits (profundidad 1, de hecho). Si los circuitos son demasiado profundos, es seguro que se producirá mucho ruido, y esto hará que la probabilidad de medir el estado cero sea muy baja, aunque el mapa de características se ajuste bien a los datos. Por ejemplo, si se repite el proceso anterior con zz_feature_map y , entanglement='linear', reps=1 se obtiene dist.get(0,0.0) = 0.0015 con los mismos puntos de datos. Esto se debe a la profundidad mucho mayor de los circuitos y de los dos qubits de zz_feature_map. La figura siguiente muestra la distribución de probabilidad para ese cálculo.
Merece la pena jugar con algunos puntos de datos de la misma categoría para ver cómo de baja debe ser la profundidad para obtener buenos resultados. Lo que sigue es un consejo aproximado que seguramente tendrá excepciones. En general, una profundidad transpilada de dos qubits de 10 o menos no debería suponer ningún problema. Una profundidad transpilada de dos qubits de 50-60 es el estado del arte y requerirá una mitigación de errores avanzada, entre otras herramientas. Entre medias, los resultados pueden variar en función de la similitud de los datos, la expresividad del mapa de características, la anchura del circuito y otros factores.
Normalmente, la fase de posprocesamiento también incluiría procesos clásicos de aprendizaje automático. En la siguiente sección ampliaremos este proceso a un conjunto de datos completo y mostraremos el flujo de trabajo clásico del aprendizaje automático.
Comprueba tu comprensión
En un circuito cuántico de 10 qubits, en general, ¿cuántos estados diferentes se podrían medir?
o 1024.
Supongamos que alguien que se inicia en la computación cuántica intenta utilizar un circuito cuántico con una profundidad de dos qubits muy elevada y no aplica medidas de mitigación de errores. Supongamos además que esto da lugar a una tasa de error del 10 % en cada qubit. Si el elemento de la matriz del núcleo verdadero (sin errores) correspondiente a este circuito es muy grande, por ejemplo, 1.0, ¿cuál sería la probabilidad de medir que los 10 qubits se encuentran en el estado en el que cada qubit es ?
La probabilidad de que cada qubit se encuentre correctamente en el estado |0> es 0.90. La probabilidad de que los 10 qubits se encuentren en el estado correcto es o aproximadamente el 35%.
Explica con tus propias palabras por qué es tan importante controlar la profundidad de los circuitos. Esto es cierto en general, pero explíquelo en el contexto de la estimación del núcleo cuántico.
En este flujo de trabajo 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 . Los circuitos muy profundos introducirán altas tasas de error. Cuando esa tasa de error se acumula en muchos qubits, se reduce sustancialmente la probabilidad de medir el estado cero.
Matriz completa del núcleo
En esta sección, ampliaremos el proceso anterior a la clasificación binaria de un conjunto de datos completo. Esto introducirá dos componentes importantes: (1) ahora podemos aplicar el aprendizaje automático clásico en el postprocesamiento, y (2) podemos obtener puntuaciones de precisión para nuestro entrenamiento.
Paso 1: Asignar entradas clásicas a un problema cuántico
Ahora importaremos un conjunto de datos existente para nuestra clasificación. Este conjunto de datos consta de 128 filas (puntos de datos) y 14 características en cada punto. Hay un 15º elemento que indica la categoría binaria de cada punto ( ). El conjunto de datos se importa a continuación, o puede acceder al conjunto de datos y ver su estructura aquí.
Utilizaremos los primeros 90 puntos de datos para el entrenamiento y los 30 siguientes 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]Output:
--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]
Ya nos prepararemos para almacenar salidas múltiples construyendo una matriz de núcleo y una matriz de prueba de dimensiones apropiadas.
# 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 utilizar uno prefabricado. Siéntase libre de modificar el mapa de características que aparece a continuación, o vuelva a ZFeatureMap. Pero presta siempre atención a la profundidad del circuito. Recordemos que, en el ejemplo anterior de 6 qubits, la profundidad del circuito transpilado era muy elevada cuando se utilizaba zz_feature_map. A medida que aumentan la escala y la complejidad del circuito, la profundidad podría aumentar rápidamente hasta un punto en el que el ruido anulara nuestros resultados. Siempre que sepa algo sobre su estructura de datos que pueda informarle sobre qué estructura de mapa de características sería más útil, es aconsejable crear su 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 ejecutarlo utilizando primitivas
Construiremos un circuito de superposición, y si estuviéramos ejecutando en un ordenador cuántico real en este ejemplo, lo optimizaríamos para su ejecución como antes. Pero en este caso, pretendemos recorrer todos los puntos de datos y calcular la matriz kernel completa. Para cada par de vectores de datos y , creamos un circuito de superposición diferente. Por tanto, debemos optimizar nuestro circuito para cada par de puntos de datos. Así que los pasos 2 y 3 se harían juntos en las múltiples iteraciones.
La celda de código siguiente realiza exactamente el mismo proceso que antes para un único 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. Obsérvese que hemos aprovechado la simetría de una matriz de núcleo para reducir el número de cálculos en 1/2. También hemos fijado simplemente los elementos diagonales en 1, como debería ser en ausencia de ruido. Dependiendo de su implementación y de la precisión requerida, también podría utilizar los elementos diagonales para estimar el ruido o conocerlo con fines de mitigación de errores.
Una vez que la matriz del núcleo se ha rellenado completamente, repetimos el proceso para los datos de prueba y rellenamos la matriz_de_prueba. En realidad, también se trata de una matriz de núcleo; simplemente le damos un nombre diferente para distinguirlas.
# 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")Output:
training done
test matrix done
Paso 4: Posprocesamiento, devolución del resultado en formato clásico
Ahora que tenemos una matriz kernel y una test\matrix con un formato similar a partir de métodos kernel cuánticos, podemos aplicar algoritmos clásicos de aprendizaje automático para hacer predicciones sobre nuestros datos de prueba y comprobar su precisión. Empezaremos importando sklearn.svc de Scikit-Learn, un clasificador de vectores de soporte (SVC). Debemos especificar que queremos que el SVC utilice nuestro núcleo precalculado utilizando 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")Utilizando SVC.fit, ahora podemos introducir la matriz del núcleo y las etiquetas de entrenamiento para obtener un ajuste. SVC.score comparará nuestros datos de prueba con ese ajuste utilizando nuestra matriz_de_prueba, y nos devolverá 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}")Output:
Precomputed kernel classification test score: 1.0
Vemos que la precisión de nuestro modelo entrenado fue del 100%. Esto es estupendo y demuestra que la QKE puede funcionar. Pero eso es muy diferente de la ventaja cuántica. Los núcleos clásicos probablemente también habrían podido resolver este problema de clasificación con una precisión del 100%. Queda mucho trabajo por hacer para caracterizar los distintos tipos y relaciones de datos y ver dónde serán más útiles los núcleos cuánticos en la actual era de las utilidades. Dejamos que el alumno modifique partes de este flujo de trabajo y estudie la eficacia de varios mapas de características cuánticas. He aquí algunas cosas a tener en cuenta:
- ¿Cuál es el grado de precisión? ¿Es válido para todo tipo de datos o sólo para estos datos de formación en concreto?
- ¿Qué estructura de sus datos le hace sospechar que un mapa de características cuánticas es útil?
- ¿Cómo se ve afectada la precisión al aumentar/disminuir la cantidad de datos de entrenamiento?
- ¿Qué mapas de características se pueden utilizar y cómo varían los resultados con los mapas de características?
- ¿Cómo influye en la precisión y el tiempo de ejecución el aumento del número de características?
- ¿Qué tendencias, si las hay, espera que se mantengan en los ordenadores cuánticos reales?
Ampliación a más funciones y qubits
En esta sección, repetiremos el cálculo de un solo elemento de la matriz, pero para un número mucho mayor de características, esbozando el camino para escalar hacia la utilidad. La restricción a un único elemento de la matriz se hace para que el proceso pueda mostrarse sin consumir demasiado del tiempo asignado a los ordenadores cuánticos.
Paso 1: Asignar entradas clásicas a un problema cuántico
Partiremos de un conjunto de datos en el que cada punto tiene 42 características. Como en el primer ejemplo, calcularemos un único elemento de la matriz del núcleo, para lo que necesitaremos dos puntos de datos. Los dos puntos siguientes tienen 42 características y una única variable de categoría ( ).
# 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]]Recordemos que el sitio zz_feature_map produjo circuitos bastante profundos en el caso de relativamente pocos rasgos (14 rasgos). A medida que aumentamos el número de funciones, tenemos que vigilar de cerca la profundidad del circuito. Para ilustrarlo, probaremos primero a utilizar la dirección zz_feature_map y comprobaremos 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")Output:
circuit depth = 251
two-qubit depth 165
Como ya se ha dicho, determinar exactamente hasta qué profundidad es demasiado profundo es una cuestión de matiz. Pero una profundidad de dos qubits de más de 100, incluso antes de la transpilación, es imposible. Esta es la razón por la que se ha hecho hincapié en 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 enredo teniendo en cuenta esa estructura. Aquí, puesto que sólo estamos calculando el producto interno entre dos puntos de datos de este tipo, hemos dado prioridad a la baja profundidad del circuito por encima de cualquier 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 nos molestaremos en comprobar las profundidades todavía, ya que lo que realmente importa es la profundidad transpilada de dos qubits.
Paso 2: Optimizar el problema para la ejecución cuántica
Comenzamos seleccionando el backend menos ocupado y luego optimizamos nuestro circuito para que se ejecute 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)Output:
<IBMBackend('ibm_brisbane')>
En trabajos a pequeña escala, un gestor de pasadas preestablecido suele devolver el mismo circuito con la misma profundidad, de forma fiable. Pero en circuitos muy grandes y complejos, el gestor de pases puede devolver circuitos transpilados diferentes cada vez que se ejecuta. Esto se debe a que utiliza heurísticas, y a que los circuitos muy grandes tendrán un panorama complicado de posibles optimizaciones. A menudo es útil transpilar varias veces y tomar el circuito menos profundo. Esto sólo introduce una sobrecarga clásica y puede mejorar sustancialmente los resultados del ordenador cuántico.
Aquí, transpilamos el circuito unitario de solapamiento 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())Output:
circuit depth = 61
print(transpiled_depths)
print(transpiled_twoqubit_depths)Output:
[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 puede ver que hay cierta variación en la profundidad total de la puerta con diferentes pases de transpilación. Nuestro circuito aún no es lo suficientemente profundo/ancho como para ver variaciones en las profundidades transpiladas de dos qubits. Utilizaremos el transpiled_qcs[1], que tiene una profundidad de 60, apenas inferior a la profundidad del circuito más profundo obtenido, que era de 77.
overlap_ibm = transpiled_qcs[1]Paso 3: Ejecutar utilizando primitivas de Qiskit Runtime
A medida que nos acerquemos a la utilidad, los simuladores no serán útiles. Aquí sólo se muestra la sintaxis de los ordenadores cuánticos 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: Posprocesamiento, devolución del resultado en formato clásico
Como se describe en la introducción, la medida más útil aquí es la probabilidad de medir el estado cero .
counts.get(0, 0.0) / num_shotsOutput:
0.0138
Este proceso para el único elemento de la matriz del núcleo podría repetirse entre otros pares de datos de su conjunto para obtener la matriz del núcleo completa. La dimensión de la matriz del núcleo viene dictada por el número de puntos de los datos de entrenamiento, no por el número de características. Por tanto, el coste informático de la manipulación de la matriz del núcleo en un modelo predictivo no escala como el número de características o qubits. Incluso en el caso de conjuntos de datos relativamente pequeños con un gran número de características, los datos deben ajustarse a un mapa de características que permita una clasificación eficaz.
Escalabilidad y trabajo futuro
El método del núcleo requiere que midamos la con la mayor precisión posible. Pero los errores de puerta y los errores de lectura significan que existe una probabilidad distinta de cero de que un qubit determinado se mida erróneamente en el estado . Incluso con la simplificación excesiva de que la probabilidad de debe ser , para muchas características codificadas en, digamos, bits, la probabilidad de medir correctamente todos los bits para que sean se reduce a . A medida que se hace grande, este método se vuelve cada vez menos fiable. Superar esta dificultad y escalar la estimación del núcleo a más y más características es un área de investigación actual. Para saber más sobre este tema, véase este trabajo de Thanasilp, Wang, Cerezo y Holmes. Le recomendamos que explore lo que se puede hacer con los ordenadores cuánticos actuales, y también que mire con ilusión lo que será posible en la era de la corrección de errores.
Revisar
Calcular un núcleo cuántico implica
- cálculo de las entradas de la matriz del núcleo, utilizando pares de puntos de datos de entrenamiento
- codificar los datos y asignarlos mediante un mapeo de características
- optimizar su circuito para que funcione en ordenadores cuánticos / backends reales
A continuación, el núcleo cuántico puede utilizarse en algoritmos clásicos de aprendizaje automático, como en esta lección.
Algunas cosas clave que hay que tener en cuenta al utilizar núcleos cuánticos son:
- ¿Es probable que el conjunto de datos se beneficie de los métodos de núcleo cuántico?
- Pruebe diferentes mapas de características y esquemas de enredo.
- ¿Es aceptable la profundidad del circuito?
- Intenta ejecutar un gestor de pases varias veces y utiliza el circuito de menor profundidad que puedas conseguir.
Los métodos de núcleo cuántico son herramientas potencialmente potentes si se dispone de una correspondencia adecuada entre los conjuntos de datos con características cuánticas y un mapa de características cuánticas apropiado. Para entender mejor dónde es probable que sean útiles los núcleos cuánticos, recomendamos leer Liu, Arunachalam & Temme (2021).