Vai al contenuto principale

Kernel quantistici

Introduzione ai kernel quantistici​

Il "metodo kernel quantistico" si riferisce a qualsiasi metodo che utilizza computer quantistici per stimare un kernel. In questo contesto, "kernel" si riferirà alla matrice kernel o ai singoli elementi in essa contenuti. Ricorda che una mappatura delle caratteristiche Φ(x⃗)\Phi(\vec{x}) è una mappatura da x⃗∈Rd\vec{x}\in \mathbb{R}^d a Φ(x⃗)∈Rd′,\Phi(\vec{x})\in \mathbb{R}^{d'}, dove di solito d′>dd'>d e dove l'obiettivo di questa mappatura è rendere le categorie dei dati separabili da un iperpiano. La funzione kernel prende come argomenti i vettori nello spazio delle caratteristiche mappate e restituisce il loro prodotto interno, ovvero K:Rd×Rd→RK:\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. Classicamente, ci interessano le mappature di caratteristiche per cui la funzione kernel è facile da valutare. Questo significa spesso trovare una funzione kernel per cui il prodotto interno nello spazio delle caratteristiche mappate può essere espresso in termini dei vettori di dati originali, senza dover mai costruire Φ(x)\Phi(x) e Φ(y)\Phi(y). Nel metodo dei kernel quantistici, la mappatura delle caratteristiche viene eseguita da un circuito quantistico, e il kernel viene stimato usando le misurazioni su quel circuito e le relative probabilità di misurazione.

In questa lezione esamineremo le profondità dei circuiti di codifica pre-programmati che usano un sostanziale entanglement e li confronteremo con le profondità dei circuiti che costruiamo a mano. Questo non vuol dire promuovere un metodo rispetto all'altro. Potresti scoprire che i circuiti pre-programmati sono troppo profondi, e che l'entanglement nel circuito personalizzato è insufficiente per essere utile. Anche in questo caso, vengono mostrati solo per abilitare la tua esplorazione.

Prima di esaminare nel dettaglio la stima di una matrice kernel, descriviamo il flusso di lavoro usando il linguaggio dei pattern di Qiskit.

Passo 1: Mappare gli input classici su un problema quantistico​

  • Input: Dataset di addestramento
  • Output: circuito astratto per il calcolo di un elemento della matrice kernel

Dato il dataset, il punto di partenza è codificare i dati in un circuito quantistico. In altre parole, dobbiamo mappare i nostri dati nello spazio di Hilbert degli stati del nostro computer quantistico. Lo facciamo costruendo un circuito dipendente dai dati. Esistono molti modi per farlo, e la lezione precedente ha delineato diverse opzioni. Puoi costruire il tuo circuito per codificare i tuoi dati, oppure puoi usare una feature map già pronta come zz_feature_map. In questa lezione faremo entrambe le cose.

Nota che per calcolare un singolo elemento della matrice kernel, vorremo codificare due punti diversi, così da poter stimare il loro prodotto interno. Un flusso di lavoro completo per il kernel quantistico coinvolgerà ovviamente molti di questi prodotti interni tra vettori di dati mappati, oltre a metodi classici di machine learning. Ma il passaggio fondamentale che viene iterato è la stima di un singolo elemento della matrice kernel. Per questo selezioniamo un circuito quantistico dipendente dai dati e mappiamo due vettori di dati nello spazio delle caratteristiche.

Classical_Review_background_kernel_circuit

Per il compito di generare una matrice kernel, siamo particolarmente interessati alla probabilità di misurare lo stato ∣0⟩⊗N|0\rangle^{\otimes N}, in cui tutti gli NN qubit si trovano nello stato ∣0⟩|0\rangle. Per capirlo, considera che il circuito responsabile della codifica e della mappatura di un vettore di dati x⃗i\vec{x}_i può essere scritto come Φ(x⃗i)\Phi(\vec{x}_i), e quello responsabile della codifica e mappatura di x⃗j\vec{x}_j è Φ(x⃗j)\Phi(\vec{x}_j), e denota gli stati mappati

∣ψ(x⃗i)⟩=Φ(x⃗i)∣0⟩⊗N|\psi(\vec{x}_i)\rangle = \Phi(\vec{x}_i)|0\rangle^{\otimes N} ∣ψ(x⃗j)⟩=Φ(x⃗j)∣0⟩⊗N.|\psi(\vec{x}_j)\rangle = \Phi(\vec{x}_j)|0\rangle^{\otimes N}.

Questi stati sono la mappatura dei dati in dimensioni superiori, quindi l'elemento del kernel desiderato è il prodotto interno

⟨ψ(x⃗j)∣ψ(x⃗i)⟩=⟨0∣⊗NΦ†(x⃗j)Φ(x⃗i)∣0⟩⊗N.\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}.

Se operiamo sullo stato iniziale predefinito ∣0⟩⊗N|0\rangle^{\otimes N} con entrambi i circuiti Φ†(x⃗j)\Phi^\dagger(\vec{x}_j) e Φ(x⃗i)\Phi(\vec{x}_i), la probabilità di misurare poi lo stato ∣0⟩⊗N|0\rangle^{\otimes N} è

P0=∣⟨0∣⊗NΦ†(x⃗j)Φ(x⃗i)∣0⟩⊗N∣2.P_0 = |\langle0|^{\otimes N}\Phi^\dagger(\vec{x}_j)\Phi(\vec{x}_i)|0\rangle^{\otimes N}|^2.

Questo è esattamente il valore che vogliamo (a meno di ∣∣2||^2). Il livello di misurazione del nostro circuito restituirà probabilità di misurazione (o cosiddette "quasi-probabilità", se vengono usati certi metodi di mitigazione degli errori). La probabilità di interesse è quella dello stato zero, ∣0⟩⊗N|0\rangle^{\otimes N}.

Passo 2: Ottimizzare il problema per l'esecuzione quantistica​

  • Input: circuito astratto, non ottimizzato per un particolare backend
  • Output: circuito target e osservabile, ottimizzati per il QPU selezionato

In questo passaggio, useremo la funzione generate_preset_pass_manager di Qiskit per specificare una routine di ottimizzazione per il nostro circuito rispetto al computer quantistico reale su cui pianifichiamo di eseguire l'esperimento. Impostiamo optimization_level=3, il che significa che useremo il pass manager preset che fornisce il massimo livello di ottimizzazione. In questo contesto, "ottimizzazione" si riferisce all'ottimizzazione dell'implementazione del circuito su un computer quantistico reale. Ciò include considerazioni come la selezione dei qubit fisici che corrispondano ai qubit nel circuito quantistico astratto in modo da minimizzare la profondità delle gate, oppure la selezione dei qubit fisici con i tassi di errore disponibili più bassi. Questo non è direttamente correlato all'ottimizzazione del problema di machine learning (come avviene con i classici ottimizzatori come COBYLA).

A seconda di come implementi il passo 2, potresti dover ottimizzare il circuito più di una volta, poiché ogni coppia di punti coinvolta in un elemento della matrice produce un circuito diverso da misurare.

Passo 3: Eseguire usando i Primitivi Qiskit Runtime​

  • Input: circuito target
  • Output: Distribuzione di probabilità

Usa la primitiva Sampler da Qiskit Runtime per ricostruire una distribuzione di probabilità degli stati ottenuti dal campionamento del circuito. Nota che potresti vedere questo riferito come "distribuzione di quasi-probabilità", un termine applicabile quando il rumore è un problema e vengono introdotti passaggi aggiuntivi, come nella mitigazione degli errori. In tali casi, la somma di tutte le probabilità potrebbe non essere esattamente uguale a 1; da qui "quasi-probabilità".

Passo 4: Post-elaborazione, restituire il risultato in formato classico​

  • Input: Distribuzione di probabilità
  • Output: Un singolo elemento della matrice kernel, o una matrice kernel se si ripete

Calcola la probabilità di misurare ∣0⟩⊗N|0\rangle^{\otimes N} sul circuito quantistico, e popola la matrice kernel nella posizione corrispondente ai due vettori di dati utilizzati. Per riempire l'intera matrice kernel, dobbiamo eseguire un esperimento quantistico per ogni elemento. Una volta ottenuta una matrice kernel, possiamo usarla in molti algoritmi classici di machine learning che accettano kernel pre-calcolati. Ad esempio: qml_svc = SVC(kernel="precomputed"). Possiamo quindi usare flussi di lavoro classici per applicare il nostro modello sui dati di test e ottenere un punteggio di accuratezza. A seconda della nostra soddisfazione con il punteggio di accuratezza, potremmo dover riesaminare alcuni aspetti del nostro calcolo, come la nostra feature map.

Schema della lezione​

In questa lezione eseguiremo questi passaggi in diversi modi per fare un uso ottimale del tuo tempo sui computer quantistici reali. Applicheremo un metodo kernel quantistico a

  • Un singolo elemento della matrice kernel per dati con relativamente poche caratteristiche, usando un backend reale, così da poter seguire facilmente cosa accade a ogni passaggio.
  • Un intero dataset con relativamente poche caratteristiche, usando un backend simulato, così da vedere come il flusso di lavoro quantistico si connette con i metodi classici di machine learning
  • Un singolo elemento della matrice kernel per dati con molte caratteristiche, usando un computer quantistico reale. Non stimeremo un'intera matrice kernel per un grande dataset, al fine di rispettare il tempo sui computer quantistici 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

Singolo elemento della matrice kernel​

Passo 1: Mappare gli input classici su un problema quantistico​

Consideriamo prima un dataset con solo poche caratteristiche, diciamo 10. Il dataset potrebbe essere grande quanto vuoi, poiché calcoliamo gli elementi della matrice kernel uno alla volta. Abbiamo bisogno di almeno due punti, quindi inizieremo da lì (nel prossimo esempio, importeremo un dataset completo). Importiamo alcuni pacchetti necessari:

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

Possiamo provare a usare 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])

Le due unitarie sopra corrispondono esattamente a U1U_1 e U2U_2 descritte nell'introduzione. Possiamo combinarle usando unitary_overlap. Come sempre, vogliamo tenere d'occhio la profondità del nostro 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

Passo 2: Ottimizzare il problema per l'esecuzione quantistica​

Iniziamo selezionando il backend meno occupato, quindi ottimizziamo il nostro circuito per l'esecuzione su quel 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)

Per circuito complicati, questo passaggio aumenterà sostanzialmente la profondità del circuito man mano che viene mappato sulle gate native per i computer quantistici reali, e le informazioni potrebbero dover essere spostate da qubit a qubit. In questo caso semplice, la profondità è quasi del tutto invariata.

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

Passo 3: Eseguire usando i Primitivi Qiskit Runtime​

La sintassi per l'esecuzione su un simulatore è commentata di seguito. Per questo dataset, con un numero ridotto di caratteristiche, l'esecuzione su un simulatore è ancora un'opzione. Per i calcoli a scala utility, la simulazione non è tipicamente fattibile. I simulatori dovrebbero essere usati solo per eseguire il debug di codice ridotto.

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

Passo 4: Post-elaborazione, restituire il risultato in formato classico​

Come descritto nell'introduzione, la misurazione più utile qui è la probabilità di misurare lo stato zero ∣00000⟩|00000\rangle.

counts.get(0, 0.0) / num_shots
0.6525

Questo è il risultato che volevamo: una stima del prodotto interno (a meno del modulo quadro) dei vettori corrispondenti a due punti dati. Se vogliamo esaminare la distribuzione completa delle probabilità di misurazione (o quasi-probabilità), possiamo farlo usando la funzione plot_distribution come mostrato di seguito. Si nota che per un gran numero di qubit, immagini come questa diventano rapidamente intrattabili.

from qiskit.visualization import plot_distribution

plot_distribution(counts_bit)

Output of the previous code cell

In alternativa, si potrebbe definire una visualizzazione come quella qui sotto per esaminare solo le 10 misurazioni più probabili. Questo potrebbe essere importante per la risoluzione dei problemi o per cercare di ottenere maggiore intuizione sui dati. Ma la probabilità di misurazione dello stato zero è il nostro elemento della matrice 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

Da queste informazioni su un solo prodotto interno tra due punti dati nello spazio delle caratteristiche di dimensione superiore, tutto quello che possiamo dire è che la loro sovrapposizione è abbastanza grande rispetto alla sovrapposizione massima (che sarebbe 1,0). Questo potrebbe essere un indicatore che questi due punti dati sono in qualche modo simili per natura e saranno categorizzati nella stessa classe. Oppure potrebbe essere un indicatore che la nostra feature map non è efficace nel mappare in uno spazio in cui dati simili hanno una grande sovrapposizione e dati diversi hanno una piccola sovrapposizione. Per sapere quale delle due opzioni sia vera, dobbiamo applicare la nostra feature map all'intero insieme di dati e verificare se la matrice kernel risultante può essere manipolata per separare efficacemente le classi con alta accuratezza.

Vale la pena notare che abbiamo usato la z_feature_map che ha prodotto una bassa profondità traspilata a due qubit (profondità 1, infatti). Se i tuoi circuito diventano troppo profondi, ciò causerà sicuramente molto rumore, e questo ridurrà la probabilità di misurare lo stato zero anche se la tua feature map è ben adatta ai tuoi dati. Ad esempio, una ripetizione del processo precedente usando zz_feature_map e , entanglement='linear', reps=1 ha prodotto dist.get(0,0.0) = 0.0015 usando gli stessi punti dati. Ciò è dovuto alle profondità di circuito e a due qubit molto maggiori della zz_feature_map. La figura seguente mostra la distribuzione di probabilità per quel calcolo.

Risultati scadenti da una zz feature map.

Vale la pena sperimentare con alcuni punti dati della stessa categoria per vedere quanto deve essere bassa la tua profondità per ottenere buoni risultati. Di seguito sono riportati alcuni consigli approssimativi che sicuramente avranno eccezioni. In generale, una profondità traspilata a due qubit di 10 o meno non dovrebbe essere un problema. Una profondità traspilata a due qubit di 50-60 è allo stato dell'arte e richiederà mitigazione avanzata degli errori tra gli altri strumenti. Nel mezzo, i tuoi risultati potrebbero variare in base alla somiglianza dei dati, all'espressività della feature map, alla larghezza del circuito e ad altri fattori. Di norma, il passaggio di post-elaborazione includerebbe anche processi classici di machine learning. Nella sezione successiva estenderemo questo processo a un intero dataset e mostreremo il flusso di lavoro classico di machine learning.

Verifica la tua comprensione​

Leggi le domande di seguito, rifletti sulle tue risposte, quindi clicca sui triangoli per vedere le soluzioni.

In un circuito quantistico a 10 qubit, in generale, quanti stati diversi è possibile misurare?

Risposta:

2102^{10} ovvero 1024.

Supponiamo che qualcuno alle prime armi con il calcolo quantistico tenti di usare un circuito quantistico con una profondità a due qubit molto alta, e che non utilizzi la mitigazione degli errori. Supponiamo inoltre che ciò risulti in un tasso di errore del 10% su ogni qubit. Se il vero elemento della matrice kernel (privo di errori) corrispondente a questo circuito è molto grande, diciamo 1,0, qual sarebbe la probabilità di misurare tutti i 10 qubit nello stato con ogni qubit |0>?

Risposta:

La probabilità che ogni qubit venga trovato correttamente nello stato |0> è 0,90. La probabilità che tutti i 10 qubit vengano trovati nello stato corretto è 0.90100.90^{10} ovvero circa il 35%.

Spiega con parole tue perché è così importante monitorare le profondità dei circuiti. Questo vale in generale, ma spiegalo nel contesto della stima del kernel quantistico.

Risposta:

In questo flusso di lavoro QKE, le nostre stime si basano sulle misurazioni dello stato zero, ovvero lo stato in cui ogni qubit si trova nello stato ∣0⟩|0\rangle. I circuiti molto profondi introdurranno tassi di errore elevati. Quando quel tasso di errore viene composto su molti qubit, ciò ridurrà sostanzialmente la probabilità di misurare lo stato zero.

Matrice kernel completa​

In questa sezione estenderemo il processo precedente alla classificazione binaria di un dataset completo. Questo introduce due componenti importanti: (1) possiamo ora applicare il machine learning classico nel post-processing, e (2) possiamo ottenere punteggi di accuratezza per il nostro addestramento.

Passo 1: Mappare gli input classici in un problema quantistico​

Ora importeremo un dataset esistente per la nostra classificazione. Questo dataset è composto da 128 righe (punti dati) e 14 caratteristiche per ogni punto. C'è un 15° elemento che indica la categoria binaria di ogni punto (±1\pm 1). Il dataset viene importato di seguito, oppure puoi accedere al dataset e visualizzarne la struttura qui.

Utilizzeremo i primi 90 punti dati per l'addestramento e i successivi 30 per il test.

!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]

Ci prepareremo fin da subito a memorizzare più output costruendo una matrice kernel e una matrice di test delle dimensioni appropriate.

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

Ora creiamo una feature map per codificare e mappare i nostri dati classici in un circuito quantistico. Siamo liberi di costruire la nostra feature map o di usarne una già pronta. Sentiti libero di modificare la feature map qui sotto, o di tornare a ZFeatureMap. Tieni sempre d'occhio la profondità del circuito. Ricorda che nell'esempio precedente a 6 qubit la profondità del circuito traspilato era ingestibile quando si usava zz_feature_map. Man mano che la scala e la complessità del circuito aumentano, la profondità potrebbe crescere rapidamente fino a un punto in cui il rumore vanifica i risultati. Quando conosci qualcosa della struttura del tuo dataset che potrebbe indicare quale struttura di feature map sarebbe più utile, è consigliabile creare una feature map personalizzata che sfrutti quella conoscenza.

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)

Passi 2 e 3: Ottimizzare il problema ed eseguire con le primitive​

Costruiremo un overlap circuit e, se stessimo girando su un vero computer quantistico in questo esempio, lo ottimizzeremmo per l'esecuzione come prima. Ma in questo caso, intendiamo iterare su tutti i punti dati e calcolare la matrice kernel completa. Per ogni coppia di vettori dati x⃗i\vec{x}_i e x⃗j\vec{x}_j, creiamo un overlap circuit diverso. Quindi i passi 2 e 3 andrebbero eseguiti insieme nelle iterazioni multiple.

La cella di codice qui sotto esegue esattamente lo stesso processo di prima per una singola coppia di punti dati. Questa volta è semplicemente eseguita all'interno di due cicli for, e c'è la riga aggiuntiva finale kernel_matrix[x_1,x_2] = ... per memorizzare i risultati di ogni calcolo. Nota che abbiamo sfruttato la simmetria di una matrice kernel per ridurre il numero di calcoli della metà. Abbiamo anche semplicemente impostato gli elementi diagonali a 1, come dovrebbero essere in assenza di rumore. A seconda della tua implementazione e della precisione richiesta, potresti anche usare gli elementi diagonali per stimare il rumore o imparare qualcosa su di esso ai fini della mitigazione degli errori.

Una volta che la matrice kernel è stata completamente popolata, ripetiamo il processo per i dati di test e popoliamo la test_matrix. Anche questa è di fatto una matrice kernel; le diamo semplicemente un nome diverso per distinguerle.

# 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

Passo 4: Post-elaborazione, restituzione del risultato in formato classico​

Ora che abbiamo una matrice kernel e una test_matrix formattata analogamente ottenute con i metodi di quantum kernel, possiamo applicare algoritmi di machine learning classici per fare previsioni sui dati di test e verificarne l'accuratezza. Inizieremo importando sklearn.svc di Scikit-Learn, un classificatore a vettori di supporto (SVC). Dobbiamo specificare che vogliamo che l'SVC utilizzi il nostro kernel precalcolato usando 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")

Usando SVC.fit, possiamo ora fornire la matrice kernel e le etichette di addestramento per ottenere un fit. SVC.score valuterà poi i nostri dati di test rispetto a quel fit usando la nostra test_matrix, e restituirà la nostra accuratezza.

# 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

Vediamo che l'accuratezza del nostro modello addestrato è stata del 100%. È un ottimo risultato e dimostra che il QKE può funzionare. Ma questo è molto diverso dal vantaggio quantistico. I kernel classici avrebbero probabilmente potuto risolvere questo problema di classificazione con la stessa accuratezza del 100%. C'è ancora molto lavoro da fare per caratterizzare i diversi tipi di dati e le relazioni tra di essi, per capire dove i quantum kernel saranno più utili nell'attuale era dell'utilità. Lasciamo al discente il compito di modificare parti di questo flusso di lavoro e studiare l'efficacia delle varie quantum feature map. Ecco alcune cose da considerare:

  • Quanto è robusta l'accuratezza? Regge per tipologie ampie di dati o solo per questi specifici dati di addestramento?
  • Quale struttura nei tuoi dati ti fa sospettare che una quantum feature map sia utile?
  • In che modo l'accuratezza è influenzata dall'aumentare/diminuire la quantità di dati di addestramento?
  • Quali feature map puoi usare e come variano i risultati al variare della feature map?
  • Come sono influenzate l'accuratezza e il tempo di esecuzione all'aumentare del numero di caratteristiche?
  • Quali tendenze, se ve ne sono, ti aspetti che si confermino su veri computer quantistici?

Scaling a più caratteristiche e qubit​

In questa sezione ripeteremo il calcolo di un singolo elemento della matrice, ma per un numero molto maggiore di caratteristiche, tracciando il percorso verso lo scaling verso l'utilità. La restrizione a un singolo elemento della matrice è fatta per poter mostrare il processo senza consumare troppo del tempo concesso sui computer quantistici.

Passo 1: Mappare gli input classici in un problema quantistico​

Assumeremo come punto di partenza un dataset in cui ogni punto dati ha 42 caratteristiche. Come nel primo esempio, calcoleremo un singolo elemento della matrice kernel, richiedendo due punti dati. I due punti qui sotto hanno 42 caratteristiche e una singola variabile di categoria (±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]]

Ricorda che la zz_feature_map produceva circuito piuttosto profondi nel caso di un numero relativamente ridotto di caratteristiche (14 caratteristiche). Man mano che aumentiamo il numero di caratteristiche, dobbiamo monitorare attentamente la profondità del circuito. Per illustrare questo, proveremo prima a usare la zz_feature_map e a verificare la profondità del circuito risultante.

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

Come descritto in precedenza, determinare esattamente quanto profondo sia troppo profondo è una questione sfumata. Ma una profondità a due qubit superiore a 100, anche prima della traspilazione, è assolutamente inaccettabile. Ecco perché le feature map personalizzate sono state enfatizzate in tutta questa lezione. Se conosci qualcosa della struttura del tuo intero dataset, dovresti progettare una mappa di entanglement tenendo conto di quella struttura. Qui, poiché stiamo calcolando solo il prodotto interno tra due tali punti dati, abbiamo privilegiato la bassa profondità del circuito rispetto a qualsiasi considerazione dettagliata della struttura dei dati.

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()

Per ora non ci preoccuperemo di verificare le profondità, poiché ciò che conta davvero è la profondità traspilata a due qubit.

Passo 2: Ottimizzare il problema per l'esecuzione quantistica​

Iniziamo selezionando il Backend meno occupato, quindi ottimizziamo il nostro circuito per girare su quel 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')>

Su job di piccola scala, un preset pass manager restituirà spesso lo stesso circuito con la stessa profondità, in modo affidabile. Ma in circuito molto grandi e complessi il pass manager può restituire circuito traspilati diversi a ogni esecuzione. Questo perché utilizza euristiche e perché circuito molto grandi avranno un panorama complicato di possibili ottimizzazioni. È spesso utile traspilare più volte e prendere il circuito più superficiale. Questo introduce solo overhead classico e può migliorare sostanzialmente i risultati provenienti dal computer quantistico.

Qui traspiliamo l'overlap circuit 20 volte e osserviamo le profondità dei circuiti ottenuti.

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

Qui puoi vedere che c'è una certa variazione nella profondità totale dei gate con diverse traspilazioni. Il nostro circuito non è ancora abbastanza profondo/largo da vedere variazioni nelle profondità traspilate a due qubit. Useremo transpiled_qcs[1], che ha una profondità di 60, leggermente inferiore alla profondità del circuito più profondo ottenuto, che era 77.

overlap_ibm = transpiled_qcs[1]

Passo 3: Eseguire con le primitive di Qiskit Runtime​

Man mano che ci avviciniamo all'utilità, i simulatori non saranno utili. Qui viene mostrata solo la sintassi per i veri computer quantistici.

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

Passo 4: Post-elaborazione, restituzione del risultato in formato classico​

Come descritto nell'introduzione, la misura più utile qui è la probabilità di misurare lo stato zero ∣00000⟩|00000\rangle.

counts.get(0, 0.0) / num_shots
0.0138

Questo processo per il singolo elemento della matrice kernel potrebbe essere ripetuto tra altre coppie di dati nel tuo set per ottenere la matrice kernel completa. La dimensione della matrice kernel è determinata dal numero di punti nel tuo set di addestramento, non dal numero di caratteristiche. Quindi il costo computazionale di trasformare la matrice kernel in un modello predittivo non scala con il numero di caratteristiche o di qubit. Anche per dataset relativamente piccoli con un grande numero di caratteristiche, i dati dovrebbero comunque essere abbinati a una feature map che produca una classificazione efficace.

Scaling e lavori futuri​

Il metodo kernel richiede di misurare ∣0⟩|0\rangle nel modo più accurato possibile. Ma gli errori di gate e gli errori di lettura fanno sì che vi sia una probabilità non nulla pp che un dato qubit venga erroneamente misurato nello stato ∣1⟩|1\rangle. Anche con la semplificazione eccessiva che la probabilità di ∣0⟩|0\rangle dovrebbe essere 100%100\%, per molte caratteristiche codificate su, diciamo, NN bit, la probabilità di misurare correttamente tutti i bit come ∣0⟩|0\rangle si riduce a (1−p)N(1-p)^N. Al crescere di NN, questo metodo diventa sempre meno affidabile. Superare questa difficoltà e portare la stima del kernel a un numero sempre maggiore di caratteristiche è un'area di ricerca attiva. Per saperne di più su questa problematica, consulta questo lavoro di Thanasilp, Wang, Cerezo e Holmes. Ti consigliamo di esplorare ciò che è possibile fare con i computer quantistici attuali e di guardare con interesse a ciò che sarà possibile nell'era della correzione degli errori.

Riepilogo​

Il calcolo di un quantum kernel comporta:

  • il calcolo degli elementi della matrice kernel, usando coppie di punti dati di addestramento
  • la codifica dei dati e la loro mappatura tramite una feature mapping
  • l'ottimizzazione del circuito per l'esecuzione su veri computer quantistici / Backend

Il quantum kernel può poi essere utilizzato negli algoritmi di machine learning classici, come in questa lezione.

Alcune cose fondamentali da tenere a mente quando si usano i quantum kernel:

  • Il dataset è probabile che tragga beneficio dai metodi di quantum kernel?
  • Prova diverse feature map e schemi di entanglement.
  • La profondità del circuito è accettabile?
  • Prova a eseguire un pass manager più volte e usa il circuito con la profondità più bassa che riesci a ottenere.

I metodi di quantum kernel sono strumenti potenzialmente potenti dato un abbinamento appropriato tra dataset con caratteristiche adatte al quantistico e una feature map quantistica adeguata. Per capire meglio dove i quantum kernel sono probabilmente più utili, ti consigliamo di leggere Liu, Arunachalam & Temme (2021).