Vai al contenuto principale

Addestramento del kernel quantistico

Stima di utilizzo: meno di un minuto su un processore Heron r3 (NOTA: Questa è solo una stima. Il tempo di esecuzione effettivo potrebbe variare.)

Risultati dell'apprendimento

Al termine di questo tutorial, potrai comprendere le seguenti informazioni:

  • I metodi kernel e i loro utilizzi
  • I kernel quantistici e come possono fornire spazi delle caratteristiche migliorati
  • La costruzione di circuiti per kernel quantistici
  • Come addestrare un kernel quantistico usando un pattern Qiskit: mappare, ottimizzare, eseguire e post-elaborare

Prerequisiti

Si raccomanda di familiarizzare con i kernel quantistici, perché sono importanti e come vengono utilizzati in pratica.

È utile anche avere una comprensione di base della teoria dei gruppi.

Contesto

I metodi kernel sono comuni nelle applicazioni di machine learning. In questo contesto, "kernel" si riferisce alla matrice kernel o alle singole voci al suo interno. In generale, un kernel è una misura di similarità tra dati codificati in uno spazio delle caratteristiche (feature space) ad alta dimensionalità e può essere utilizzato, ad esempio, in compiti di classificazione con macchine a vettori di supporto.

I metodi kernel quantistici sono quelli che usano i computer quantistici per stimare un kernel. È noto che i computer quantistici possono codificare i dati in spazi delle caratteristiche potenziati quantisticamente, sostituendo efficacemente gli analoghi classici. Per xR\vec{x} \in \mathbb{R} e Ψ(x)Rd\Psi(\vec{x}) \in \mathbb{R}^{d'}, tipicamente con d>dd' >d, Ψ(x)\Psi(\vec{x}) è una mappa delle caratteristiche, xΨ(x)\vec{x} \mapsto \Psi(\vec{x}). L'obiettivo di Ψ(x)\Psi(\vec{x}) è fare in modo che le categorie dei dati siano separate da un iperpiano. Prendendo i vettori nello spazio mappato alle caratteristiche come argomenti, la funzione kernel K(x,y)=Ψ(x)Ψ(y)K(\vec{x}, \vec{y}) = \langle{\Psi(\vec{x}) | \Psi(\vec{y}) \rangle{}} restituisce il loro prodotto interno: K:RdK: \mathbb{R}^d \rightarrow Rd\mathbb{R}^d. Classicamente, le mappe delle caratteristiche di interesse sono quelle in cui la funzione kernel può essere facilmente valutata; ovvero, quando il prodotto interno nello spazio mappato alle caratteristiche può essere scritto in termini dei vettori di dati originali e Ψ(x)\Psi(\vec{x}) e Ψ(y)\Psi(\vec{y}) non devono essere costruiti. Nel caso dei kernel quantistici, la mappatura delle caratteristiche viene eseguita da un circuito quantistico, e il kernel viene stimato usando le probabilità di misura campionate dal circuito.

Questo tutorial mostra come costruire un pattern Qiskit per valutare le voci in una matrice kernel quantistica utilizzata per la classificazione binaria.

Requisiti

Prima di iniziare questo tutorial, assicurati di avere installato quanto segue:

  • Qiskit SDK v2.3.1 o successiva, con supporto per la visualizzazione
  • Qiskit Runtime v0.44.0 o successiva (pip install qiskit-ibm-runtime)

Configurazione

# 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

Esempio con simulatore su piccola scala

In questa sezione, percorriamo i quattro passaggi del pattern Qiskit su un'istanza a sette qubit del problema di etichettatura dei coset con errore e valutiamo una singola voce della matrice kernel usando la primitiva StatevectorSampler di Qiskit. Un simulatore statevector è esatto (fino al rumore dei shot) e ci mostra il metodo dall'inizio alla fine senza consumare tempo QPU. Poi ripetiamo la stessa istanza su hardware reale nella sezione dell'esempio hardware.

Passaggio 1: Mappare gli input classici a un problema quantistico

  • Input: Dataset di addestramento.
  • Output: Circuito astratto per calcolare una voce della matrice kernel.

Il problema di classificazione binaria che vogliamo risolvere qui è detto "etichettatura dei coset con errore". Il dataset di addestramento in input contiene una struttura di gruppo, composta da due coset formati da un gruppo e un sottogruppo. Il gruppo è preso come G=SU(2)nG = SU(2)^{\otimes n} per i qubit, che è il gruppo unitario speciale di 2×22 \times 2 matrici e ha ampia applicabilità in natura; ad esempio, nel Modello Standard della fisica delle particelle. Prendiamo il sottogruppo (stabilizzatore di 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 per un grafo con archi E\mathcal{E} e vertici V\mathcal{V}. Si noti che gli stabilizzatori fissano uno stato stabilizzatore tale che Dsψ=ψ, sSgraphD_s | \psi \rangle = | \psi \rangle,~ \forall s \in S_\text{graph}. Infine, definiamo due coset sinistri C±=c±SgraphC_\pm = c_\pm S_\text{graph} estraendo due c±Gc_\pm \in G casualmente.

Per maggiori dettagli sul dataset e su come viene generato, consulta questo notebook dal Quantum Kernel Training Toolkit.

Creiamo il circuito quantistico utilizzato per valutare una voce nella matrice kernel. I dati di input vengono utilizzati per determinare gli angoli di rotazione per i gate parametrizzati del circuito. Per semplicità, useremo i campioni di dati x1=14 e x2=19.

Nota: Il dataset utilizzato in questo tutorial può essere scaricato qui.

# 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

Passaggio 2: Ottimizzare il problema per l'esecuzione su hardware quantistico

  • Input: Circuito astratto, non ottimizzato per un particolare backend.
  • Output: Circuito target, ottimizzato per la QPU selezionata.

Per il percorso con simulatore statevector utilizzato in questa sezione, non è necessaria alcuna ottimizzazione specifica per il backend: il circuito astratto può essere campionato direttamente. Eseguiamo questo passaggio nell'esempio hardware qui sotto, dove il circuito viene transpilato su una QPU reale usando generate_preset_pass_manager con optimization_level=3.

Passaggio 3: Eseguire utilizzando le primitive Qiskit

  • Input: Circuito astratto.
  • Output: Distribuzione di quasi-probabilità.

Utilizza la primitiva StatevectorSampler di Qiskit per ricostruire una distribuzione di quasi-probabilità degli stati ottenuti dal campionamento del circuito. Per il compito di generare una matrice kernel, siamo particolarmente interessati alla probabilità di misurare lo stato |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

Passaggio 4: Post-elaborare e restituire il risultato nel formato classico desiderato

  • Input: Distribuzione di probabilità.
  • Output: Un singolo elemento della matrice kernel.

Calcola la probabilità di misurare 0|0 \rangle sul circuito di sovrapposizione e popola la matrice kernel nella posizione corrispondente ai campioni rappresentati da questo particolare circuito di sovrapposizione (riga 15, colonna 20).

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

Esempio hardware

Una matrice kernel quantistica ha O(N2)\mathcal{O}(N^2) voci per NN campioni di addestramento, e ogni voce richiede l'esecuzione di un circuito di sovrapposizione la cui profondità di gate a due qubit cresce con le dimensioni della mappa delle caratteristiche. Di conseguenza, scalare questo tutorial a un problema più grande ha due costi cumulativi: il tempo QPU per matrice kernel cresce quadraticamente con NN, e la profondità di unitary_overlap (che compone la mappa delle caratteristiche con il suo aggiunto) erode la fedeltà alla dimensione del sistema e alla connettività dell'hardware attuale. Per mantenere la demo breve e fare un confronto pulito, eseguiamo quindi la stessa istanza a sette qubit dell'esempio su piccola scala su una QPU reale e confrontiamo la fedeltà di una singola voce della matrice kernel con il valore del simulatore calcolato sopra.

# ------------------------------ 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

Per completare l'intera matrice kernel, eseguiremmo un esperimento quantistico per ciascuna delle sue N(N+1)/2N(N+1)/2 voci uniche. La figura sottostante mostra la matrice risultante per questo dataset; il rosso più scuro indica fedeltà più vicine a 1.0.

kernel_matrix.png

Passi successivi

Recommendations

Se hai trovato questo lavoro interessante, potresti essere interessato al seguente materiale: