Skip to main content
IBM Quantum Platform

Formación sobre kernel cuántico

Tiempo estimado de ejecución: menos de un minuto en un procesador Heron r3 (NOTA: Se trata solo de una estimación). (El tiempo de ejecución puede variar.)


Resultados del aprendizaje

Una vez completado este tutorial, habrás adquirido los siguientes conocimientos:

  • Métodos del núcleo y sus aplicaciones
  • Los núcleos cuánticos y cómo pueden proporcionar espacios de características mejorados
  • Construcción de circuitos de núcleo cuántico
  • Cómo entrenar un kernel cuántico utilizando un patrón de Qiskit : mapear, optimizar, ejecutar y posprocesar

Requisitos previos

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

También es útil tener unos conocimientos básicos de teoría de grupos.


En segundo plano

Los métodos de kernel son habituales en las aplicaciones de aprendizaje automático. En este contexto, el término «núcleo» se refiere a la matriz del núcleo o a las entradas individuales que la componen. 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 ordenadores cuánticos para estimar un kernel. Se sabe que los ordenadores cuánticos pueden codificar datos en espacios de características mejorados cuánticamente, sustituyendo de hecho a sus análogos clásicos. Para xR\vec{x} \in \mathbb{R} y Ψ(x)Rd\Psi(\vec{x}) \in \mathbb{R}^{d'}, normalmente junto 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 separar las categorías de datos mediante un hiperplano. Tomando como argumentos los vectores en el espacio de mapas de características, la función de núcleo K(x,y)=Ψ(x)Ψ(y)K(\vec{x}, \vec{y}) = \langle{\Psi(\vec{x}) | \Psi(\vec{y}) \rangle{}} devuelve su producto interno: K:RdK: \mathbb{R}^d \rightarrow Rd\mathbb{R}^d. Tradicionalmente, los mapas de características de interés son aquellos en los que la función de núcleo puede evaluarse fácilmente; es decir, cuando el producto interno en el espacio de mapas de características puede expresarse en términos de los vectores de datos originales y no es necesario construir Ψ(x)\Psi(\vec{x}) ni Ψ(y)\Psi(\vec{y}). En el caso de los núcleos cuánticos, la correspondencia de características se lleva a cabo mediante un circuito cuántico, y el núcleo se estima utilizando las probabilidades de medición obtenidas del circuito.

Este tutorial muestra cómo crear un patrón de Qiskit para evaluar las entradas en una matriz de núcleo cuántico utilizada para la clasificación binaria.


Requisitos

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

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

Configuración

# 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 de simulador a pequeña escala

En esta sección, repasamos los cuatro pasos del patrón de Qiskit en una instancia de siete qubits del problema de etiquetado de clases de resto con error y evaluamos una sola entrada de la matriz del núcleo utilizando la StatevectorSampler primitiva de Qiskit. Un simulador de vectores de estado es exacto (salvo por el ruido de disparo) y nos muestra el método de principio a fin sin consumir tiempo de la QPU. A continuación, repetimos el mismo ejemplo en hardware real en la sección de ejemplos de hardware.

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 kernel.

El problema de clasificación binaria que pretendemos resolver aquí se conoce como «etiquetado de clases con error » El conjunto de datos de entrenamiento de entrada contiene una estructura de grupo, compuesta por dos clases formadas por un grupo y un subgrupo. Se considera que el grupo es un grupo de G=SU(2)nG = SU(2)^{\otimes n} es para los qubits, que es el grupo unitario especial de matrices 2×22 \times 2 es y tiene una amplia aplicabilidad en la naturaleza; por ejemplo, en el Modelo Estándar de la física de partículas. Tomamos el subgrupo (estabilizador del grafo) Sgraph<GS_\text{graph} < G, donde 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}. Obsérvese que los estabilizadores fijan un estado de estabilizador tal que Dsψ=ψ, sSgraphD_s | \psi \rangle = | \psi \rangle,~ \forall s \in S_\text{graph}. Por último, definimos dos cosets a la izquierda C±=c±SgraphC_\pm = c_\pm S_\text{graph} seleccionando al azar dos c±Gc_\pm \in G.

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

Creamos el circuito cuántico que se utiliza para evaluar una entrada de la matriz del núcleo. Los datos de entrada se utilizan para determinar los ángulos de rotación de las puertas parametrizadas del circuito. Para simplificar, utilizaremos las muestras x1=14 de datos y x2=19.

Nota: El conjunto de datos utilizado en este tutorial puede descargarse 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:

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 concreto.
  • Resultado: Circuito de destino, optimizado para la QPU seleccionada.

En el caso de la ruta del simulador de vectores de estado utilizada en esta sección, no se requiere ninguna optimización específica del backend: el circuito abstracto puede muestrearse directamente. Aplicamos este paso en el ejemplo de hardware que se muestra a continuación, en el que el circuito se compila para una QPU real utilizando generate_preset_pass_manager con optimization_level=3.

Paso 3: Ejecutar utilizando Qiskit primitives

  • Entrada: Circuito abstracto.
  • Resultado: Distribución cuasiprobabilística.

Utiliza la StatevectorSampler función de Qiskit para reconstruir una distribución cuasiprobabilística de los estados obtenidos al muestrear el circuito. Para la tarea de generar una matriz de núcleo, nos interesa especialmente la probabilidad de observar 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:

Output of the previous code cell

Paso 4: Procesamiento posterior y devolución del resultado en el formato clásico deseado

  • Entrada: Distribución de probabilidad.
  • Resultado: un único elemento de la matriz del núcleo.

Calcula la probabilidad de obtener un valor de 0|0 \rangle en el circuito de solapamiento y rellena la matriz del núcleo en la posición correspondiente a las muestras representadas por este circuito de solapamiento concreto (fila 15, columna 20).

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

Output:

Fidelity (simulator): 0.8261

Ejemplo de hardware

Una matriz de núcleo cuántico tiene un O(N2)\mathcal{O}(N^2) es de entradas para un NN es de muestras de entrenamiento, y cada entrada requiere ejecutar un circuito de superposición cuya profundidad de puertas de dos qubits aumenta con el tamaño del mapa de características. En consecuencia, ampliar este tutorial a un problema de mayor envergadura conlleva dos costes que se acumulan: el tiempo de la QPU por matriz del núcleo crece cuadráticamente con NN, y la profundidad de unitary_overlap (que compone el mapa de características con su adjunto) reduce la fidelidad debido al tamaño del sistema y a la conectividad del hardware actual. Para que la demostración sea breve y poder realizar una comparación clara, ejecutamos la misma instancia de siete qubits del ejemplo a pequeña escala en una QPU real y comparamos la fidelidad de una sola entrada de la matriz del núcleo 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]}")

Output:

Using backend: ibm_pittsburgh
Output of the previous code cell
Fidelity (hardware): 0.7517

Para completar toda la matriz del núcleo, tendríamos que realizar un experimento cuántico para cada una de sus entradas únicas N(N+1)/2N(N+1)/2. La siguiente figura 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

Recomendaciones

Si te ha parecido interesante este trabajo, quizá te interese el siguiente material:

¿Le ha resultado útil esta página?
Informe de un error, de una errata o solicite contenido en GitHub.