Saltar al contenido principal

Entrenamiento de kernel cuántico

Estimación de uso: menos de un minuto en un procesador Heron r3 (NOTA: Esto es solo una estimación. Su tiempo de ejecución puede variar.)

Resultados de aprendizaje

Al completar este tutorial, podrás entender la siguiente información:

  • Los métodos de kernel y sus usos
  • Los kernels cuánticos y cómo pueden proporcionar espacios de características mejorados
  • La construcción de circuitos de kernel cuántico
  • Cómo entrenar un kernel cuántico usando un patrón de Qiskit: mapear, optimizar, ejecutar y post-procesar

Requisitos previos

Se recomienda que te familiarices con los kernels cuánticos, por qué son importantes y cómo se utilizan en la práctica.

También es útil tener una comprensión básica de la teoría de grupos.

Contexto

Los métodos de kernel son comunes en las aplicaciones de aprendizaje automático. En este contexto, "kernel" se refiere a la matriz de kernel o a las entradas individuales de la misma. En general, un kernel es una medida de similitud entre datos codificados en un espacio de características de alta dimensión y puede utilizarse, por ejemplo, en tareas de clasificación con máquinas de vectores de soporte.

Los métodos de kernel cuántico son aquellos que utilizan computadoras cuánticas para estimar un kernel. Se sabe que las computadoras cuánticas pueden codificar datos en espacios de características mejorados cuánticamente, reemplazando efectivamente los análogos clásicos. Para xR\vec{x} \in \mathbb{R} y Ψ(x)Rd\Psi(\vec{x}) \in \mathbb{R}^{d'}, típicamente con d>dd' >d, Ψ(x)\Psi(\vec{x}) es un mapa de características, xΨ(x)\vec{x} \mapsto \Psi(\vec{x}). El objetivo de Ψ(x)\Psi(\vec{x}) es hacer que las categorías de datos estén separadas por un hiperplano. Tomando los vectores en el espacio transformado por el mapa de características como argumentos, la función de kernel K(x,y)=Ψ(x)Ψ(y)K(\vec{x}, \vec{y}) = \langle{\Psi(\vec{x}) | \Psi(\vec{y}) \rangle{}} devuelve su producto interior: K:RdK: \mathbb{R}^d \rightarrow Rd\mathbb{R}^d. Clásicamente, los mapas de características de interés son aquellos en los que la función de kernel puede evaluarse fácilmente; es decir, cuando el producto interior en el espacio transformado puede escribirse en términos de los vectores de datos originales y Ψ(x)\Psi(\vec{x}) y Ψ(y)\Psi(\vec{y}) no necesitan construirse. En el caso de los kernels cuánticos, el mapeo de características se realiza mediante un circuito cuántico, y el kernel se estima usando las probabilidades de medición muestreadas del circuito.

Este tutorial muestra cómo construir un patrón de Qiskit para evaluar entradas en una matriz de kernel cuántico utilizada para clasificación binaria.

Requisitos

Antes de comenzar este tutorial, asegúrate de tener instalado lo siguiente:

  • Qiskit SDK v2.3.1 o posterior, con soporte de visualización
  • Qiskit Runtime v0.44.0 o posterior (pip install qiskit-ibm-runtime)

Configuración

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy pandas qiskit qiskit-ibm-runtime
# General Imports and helper functions
import urllib.request

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

from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit
from qiskit.circuit.library import unitary_overlap
from qiskit.primitives import StatevectorSampler
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

from qiskit_ibm_runtime import QiskitRuntimeService, Sampler

# Download the dataset (portable across platforms)
urllib.request.urlretrieve(
"https://raw.githubusercontent.com/qiskit-community/prototype-quantum-kernel-training/main/data/dataset_graph7.csv",
"dataset_graph7.csv",
)

def visualize_counts(res_counts, num_qubits, num_shots):
"""Visualize the outputs from the Qiskit Sampler primitive."""
zero_prob = res_counts.get(0, 0.0)
top_10 = dict(
sorted(res_counts.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]))
x_vals, y_vals = list(zip(*by_key.items()))
x_vals = [bin(x_val)[2:].zfill(num_qubits) for x_val in x_vals]
y_vals_prob = []
for t in range(len(y_vals)):
y_vals_prob.append(y_vals[t] / num_shots)
y_vals = y_vals_prob
plt.bar(x_vals, y_vals)
plt.xticks(rotation=75)
plt.title("Results of sampling")
plt.xlabel("Measured bitstring")
plt.ylabel("Probability")
plt.show()

def get_training_data():
"""Read the training data."""
df = pd.read_csv("dataset_graph7.csv", sep=",", header=None)
training_data = df.values[:20, :]
ind = np.argsort(training_data[:, -1])
X_train = training_data[ind][:, :-1]

return X_train

Ejemplo con simulador a pequeña escala

En esta sección, recorremos los cuatro pasos del patrón de Qiskit en una instancia de siete qubits del problema de etiquetado de clases laterales con error y evaluamos una única entrada de la matriz de kernel usando la primitiva StatevectorSampler de Qiskit. Un simulador de vector de estado es exacto (hasta el ruido de disparo) y nos muestra el método de principio a fin sin consumir tiempo de QPU. Luego repetimos la misma instancia en hardware real en la sección de ejemplo de hardware.

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

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

El problema de clasificación binaria que buscamos resolver aquí se denomina "etiquetado de clases laterales con error." El conjunto de datos de entrenamiento de entrada contiene una estructura de grupo, que consiste en dos clases laterales formadas por un grupo y un subgrupo. El grupo se toma como G=SU(2)nG = SU(2)^{\otimes n} para qubits, que es el grupo unitario especial de matrices 2×22 \times 2 y tiene amplia aplicabilidad en la naturaleza; por ejemplo, el Modelo Estándar de física de partículas. Tomamos el subgrupo (estabilizador de grafo) Sgraph<GS_\text{graph} < G con Sgraph={Xik:(k,i)EZk}iV}S_\text{graph} = \langle \{ X_i \otimes _{k:(k,i) \in \mathcal{E}} Z_k\} _{i \in \mathcal{V}} \} \rangle para un grafo con aristas E\mathcal{E} y vértices V\mathcal{V}. Nota que los estabilizadores fijan un estado estabilizador tal que Dsψ=ψ, sSgraphD_s | \psi \rangle = | \psi \rangle,~ \forall s \in S_\text{graph}. Finalmente, definimos dos clases laterales izquierdas C±=c±SgraphC_\pm = c_\pm S_\text{graph} eligiendo dos c±Gc_\pm \in G al azar.

Para más detalles sobre el conjunto de datos y cómo se genera, consulta este cuaderno del Quantum Kernel Training Toolkit.

Creamos el circuito cuántico utilizado para evaluar una entrada en la matriz de kernel. Los datos de entrada se utilizan para determinar los ángulos de rotación de las compuertas parametrizadas del circuito. Por simplicidad, usaremos las muestras de datos x1=14 y x2=19.

Nota: El conjunto de datos utilizado en este tutorial se puede descargar aquí.

# Prepare training data
X_train = get_training_data()

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

# Prepare feature map for computing overlap
num_features = np.shape(X_train)[1]
num_qubits = int(num_features / 2)
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)

# Assign tunable parameter to known optimal value and set the data params for
# first two samples
x1 = 14
x2 = 19
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()
overlap_circ.draw("mpl", scale=0.6, style="iqp")

Output of the previous code cell

Paso 2: Optimizar el problema para la ejecución en hardware cuántico

  • Entrada: Circuito abstracto, no optimizado para un backend en particular.
  • Salida: Circuito objetivo, optimizado para la QPU seleccionada.

Para la ruta del simulador de vector de estado utilizada en esta sección, no se requiere optimización específica del backend: el circuito abstracto puede muestrearse directamente. Ejercitamos este paso en el ejemplo de hardware a continuación, donde el circuito se transpila contra una QPU real usando generate_preset_pass_manager con optimization_level=3.

Paso 3: Ejecutar utilizando primitivas de Qiskit

  • Entrada: Circuito abstracto.
  • Salida: Distribución de cuasi-probabilidad.

Utiliza la primitiva StatevectorSampler de Qiskit para reconstruir una distribución de cuasi-probabilidad de los estados obtenidos al muestrear el circuito. Para la tarea de generar una matriz de kernel, estamos particularmente interesados en la probabilidad de medir el estado |0>.

sampler = StatevectorSampler()

# Execute and get counts
num_shots = 10_000
results = sampler.run([overlap_circ], shots=num_shots).result()
counts = results[0].data.meas.get_int_counts()

# Plot counts
visualize_counts(counts, num_qubits, num_shots)

Output of the previous code cell

Paso 4: Post-procesar y devolver el resultado en el formato clásico deseado

  • Entrada: Distribución de probabilidad.
  • Salida: Un solo elemento de la matriz de kernel.

Calcula la probabilidad de medir 0|0 \rangle en el circuito de solapamiento y completa la matriz de kernel en la posición correspondiente a las muestras representadas por este circuito de solapamiento en particular (fila 15, columna 20).

kernel_matrix[x1, x2] = counts.get(0, 0.0) / num_shots
print(f"Fidelity (simulator): {kernel_matrix[x1, x2]}")
Fidelity (simulator): 0.8261

Ejemplo de hardware

Una matriz de kernel cuántico tiene O(N2)\mathcal{O}(N^2) entradas para NN muestras de entrenamiento, y cada entrada requiere ejecutar un circuito de solapamiento cuya profundidad de compuertas de dos qubits crece con el tamaño del mapa de características. Como resultado, escalar este tutorial a un problema más grande tiene dos costos compuestos: el tiempo de QPU por matriz de kernel crece cuadráticamente con NN, y la profundidad de unitary_overlap (que compone el mapa de características con su adjunto) erosiona la fidelidad al tamaño del sistema y la conectividad del hardware actual. Para mantener la demostración corta y hacer una comparación limpia, ejecutamos la misma instancia de siete qubits del ejemplo a pequeña escala en una QPU real y comparamos la fidelidad de una única entrada de la matriz de kernel con el valor del simulador calculado anteriormente.

# ------------------------------ Step 1 ------------------------------
# Prepare training data
X_train = get_training_data()

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

# Prepare feature map for computing overlap
num_features = np.shape(X_train)[1]
num_qubits = int(num_features / 2)
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)

# Assign tunable parameter to known optimal value and
# set the data params for first two samples
x1 = 14
x2 = 19
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()

# ------------------------------ Step 2 ------------------------------
service = QiskitRuntimeService()
# backend = service.least_busy(
# operational=True, simulator=False, min_num_qubits=overlap_circ.num_qubits
# )
backend = service.backend("ibm_pittsburgh")
print(f"Using backend: {backend.name}")
pm = generate_preset_pass_manager(optimization_level=3, backend=backend)
overlap_ibm = pm.run(overlap_circ)

# ------------------------------ Step 3 ------------------------------
sampler = Sampler(mode=backend)
sampler.options.environment.job_tags = ["TUT_QKT"]

num_shots = 10_000
results = sampler.run([overlap_ibm], shots=num_shots).result()
counts = results[0].data.meas.get_int_counts()
visualize_counts(counts, num_qubits, num_shots)

# ------------------------------ Step 4 ------------------------------
kernel_matrix[x1, x2] = counts.get(0, 0.0) / num_shots
print(f"Fidelity (hardware): {kernel_matrix[x1, x2]}")
Using backend: ibm_pittsburgh

Output of the previous code cell

Fidelity (hardware): 0.7517

Para completar toda la matriz de kernel, ejecutaríamos un experimento cuántico para cada una de sus N(N+1)/2N(N+1)/2 entradas únicas. La figura a continuación muestra la matriz resultante para este conjunto de datos; el rojo más oscuro indica fidelidades más cercanas a 1.0.

kernel_matrix.png

Próximos pasos

Recommendations

Si este trabajo te resultó interesante, puede que te interese el siguiente material: