Nuclei quantistici
Introduzione ai kernel quantistici
Il "metodo del kernel quantistico" si riferisce a qualsiasi metodo che utilizza computer quantistici per stimare un kernel. In questo contesto, il termine "kernel" si riferisce alla matrice kernel o alle singole voci in essa contenute. Ricordiamo che una mappatura delle caratteristiche è una mappatura da a , dove di solito e dove l'obiettivo di questa mappatura è rendere le categorie di dati separabili da un iperpiano. La funzione kernel prende come argomenti i vettori nello spazio delle mappe di caratteristiche e restituisce il loro prodotto interno, cioè con . Classicamente, siamo interessati a mappe di caratteristiche per le quali la funzione kernel è facile da valutare. Ciò significa spesso trovare una funzione kernel per la quale il prodotto interno nello spazio delle caratteristiche mappate può essere scritto in termini di vettori di dati originali, senza dover mai costruire e . Nel metodo dei kernel quantistici, la mappatura delle caratteristiche viene effettuata da un circuito quantistico e il kernel viene stimato utilizzando le misure su tale circuito e le relative probabilità di misura.
In questa lezione esamineremo le profondità dei circuiti di codifica precodificati che utilizzano un entanglement sostanziale e le confronteremo con le profondità dei circuiti che codifichiamo a mano. Non si tratta di sostenere un metodo piuttosto che un altro. Potreste scoprire che i circuiti precodificati sono troppo profondi e che l'entanglement nel circuito personalizzato è insufficiente per essere utile. Anche in questo caso, le indicazioni sono fornite solo per consentire l'esplorazione.
Prima di illustrare in dettaglio la stima di una matrice kernel, illustriamo il flusso di lavoro utilizzando il linguaggio dei modelli Qiskit.
Fase 1: mappare gli input classici su un problema quantistico
- Input: Set di dati di addestramento
- Uscita: Circuito astratto per il calcolo di una voce della matrice kernel
Dato il set di dati, il punto di partenza è la codifica dei dati in un circuito quantistico. In altre parole, dobbiamo mappare i nostri dati nello spazio di Hilbert degli stati del nostro computer quantistico. Per farlo, costruiamo un circuito dipendente dai dati. Ci sono molti modi per farlo e la lezione precedente ha illustrato una serie di opzioni. È possibile costruire il proprio circuito per codificare i dati, oppure utilizzare una mappa di caratteristiche preconfezionata come zz_feature_map. In questa lezione faremo entrambe le cose.
Si noti che per calcolare un singolo elemento della matrice kernel, si desidera codificare due punti diversi, in modo da poter stimare il loro prodotto interno. Un flusso di lavoro completo del kernel quantistico coinvolgerà ovviamente molti prodotti interni di questo tipo tra vettori di dati mappati, oltre ai metodi classici di apprendimento automatico. Ma il passo centrale che viene iterato è la stima di un singolo elemento della matrice kernel. A tal fine, selezioniamo un circuito quantistico dipendente dai dati e mappiamo due vettori di dati nello spazio delle caratteristiche.
Per il compito di generare una matrice kernel, siamo particolarmente interessati alla probabilità di misurare lo stato , in cui tutti i qubit sono nello stato . Per rendersene conto, si consideri che il circuito responsabile della codifica e della mappatura di un vettore di dati può essere scritto come , e quello responsabile della codifica e della mappatura di è , e si denotino gli stati mappati
Questi stati sono la mappatura dei dati in dimensioni superiori, quindi la voce desiderata del kernel è il prodotto interno
Se operiamo sullo stato iniziale predefinito con entrambi i circuiti e , la probabilità di misurare poi lo stato è
Questo è esattamente il valore che vogliamo (fino a ). Il livello di misurazione del nostro circuito restituirà le probabilità di misurazione (o le cosiddette "quasi-probabilità", se si utilizzano determinati metodi di attenuazione degli errori). La probabilità di interesse è quella dello stato zero, .
Fase 2: Ottimizzazione del problema per l'esecuzione quantistica
- Ingresso: Circuito astratto, non ottimizzato per un particolare backend
- Uscita: Circuito di destinazione e osservabile, ottimizzato per la QPU selezionata
In questa fase, utilizzeremo la funzione generate_preset_pass_manager di Qiskit per specificare una routine di ottimizzazione del nostro circuito rispetto al computer quantistico reale su cui intendiamo eseguire l'esperimento. Impostiamo optimization_level=3 , il che significa che useremo il gestore dei passaggi preimpostati 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 di qubit fisici che corrispondono a qubit del circuito quantistico astratto per ridurre al minimo la profondità del gate, o la selezione di qubit fisici con i tassi di errore più bassi disponibili. Questo non è direttamente collegato all'ottimizzazione del problema di apprendimento automatico (come negli ottimizzatori classici come COBYLA).
A seconda di come si implementa il passaggio 2, potrebbe essere necessario ottimizzare il circuito più di una volta, poiché ogni coppia di punti coinvolti in un elemento della matrice produce un circuito diverso da misurare.
Fase 3: Esecuzione tramite le primitive di " IBM Quantum "
- Ingresso: Circuito target
- Uscita: Distribuzione di probabilità
Utilizzare la primitiva Sampler IBM Quantum per ricostruire una distribuzione di probabilità degli stati ottenuta dal campionamento del circuito. Si noti che questo concetto può essere definito anche “distribuzione di quasi-probabilità”, un termine applicabile nei casi in cui il rumore costituisca un problema e quando vengono introdotti passaggi aggiuntivi, come ad esempio nella mitigazione degli errori. In tali casi, la somma di tutte le probabilità potrebbe non essere esattamente pari a 1; da qui il termine "quasi-probabilità".
Fase 4: Post-elaborazione, restituzione dei risultati in formato classico
- Ingresso: Distribuzione di probabilità
- Uscita: Un singolo elemento della matrice kernel, o una matrice kernel se si ripete
Calcolare la probabilità di misurare sul circuito quantistico e popolare la matrice kernel nella posizione corrispondente ai due vettori di dati utilizzati. Per riempire l'intera matrice del kernel, dobbiamo eseguire un esperimento quantistico per ogni voce. Una volta ottenuta una matrice kernel, possiamo utilizzarla in molti algoritmi classici di apprendimento automatico che accettano pre-calculated kernels. Ad esempio: qml_svc = SVC(kernel="precomputed"). Possiamo quindi utilizzare i flussi di lavoro classici per applicare il nostro modello ai dati di test e ottenere un punteggio di accuratezza. In base alla soddisfazione del punteggio di accuratezza, potrebbe essere necessario rivedere alcuni aspetti del calcolo, come la mappa delle caratteristiche.
Schema della lezione
In questa lezione eseguiremo questi passaggi in diversi modi per sfruttare al meglio il vostro tempo sui computer quantistici reali. Applicheremo il metodo del kernel quantistico a
- Una singola matrice kernel per dati con relativamente poche caratteristiche, utilizzando un backend reale, in modo da poter seguire facilmente ciò che accade in ogni fase.
- Un intero set di dati con un numero relativamente basso di caratteristiche, utilizzando un backend simulato, in modo da poter vedere come il flusso di lavoro quantistico si collega ai metodi classici di apprendimento automatico
- Una singola matrice kernel per dati con molte caratteristiche, utilizzando un vero computer quantistico. Non stimeremo un'intera matrice kernel per un grande insieme di dati, per rispettare i tempi dei computer quantistici IBM®.
# If you have not already, install scikit learn
#!pip install scikit-learnVoce singola della matrice del kernel
Fase 1: mappare gli input classici su un problema quantistico
Consideriamo per prima cosa un insieme di dati con poche caratteristiche, ad esempio 10. L'insieme dei dati può essere grande quanto si vuole, poiché gli elementi della matrice kernel vengono calcolati uno alla volta. Abbiamo bisogno di almeno due punti, quindi inizieremo con questo (nel prossimo esempio, importeremo un set di dati 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 utilizzare il sito 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])I due unitari di cui sopra corrispondono esattamente a e descritti nell'introduzione. Possiamo combinarli 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")Output:
circuit depth = 9
Fase 2: Ottimizzazione del problema per l'esecuzione quantistica
Si inizia selezionando il backend meno trafficato, quindi si ottimizza il circuito per l'esecuzione su tale 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)Per i circuiti più complicati, questo passaggio aumenterà sostanzialmente la profondità del circuito, poiché si tratta di una mappa delle porte native dei computer quantistici reali, e potrebbe essere necessario spostare le informazioni da un qubit all'altro. In questo semplice caso, la profondità non viene praticamente influenzata.
print("circuit depth = ", overlap_ibm.decompose().depth())
overlap_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)Output:
circuit depth = 10
1
Fase 3: Esecuzione tramite le primitive di " IBM Quantum "
La sintassi per l'esecuzione su un simulatore è commentata di seguito. Per questo set di dati, con un numero ridotto di caratteristiche, l'esecuzione su un simulatore è ancora un'opzione. Per i calcoli su scala industriale, la simulazione non è tipicamente fattibile. I simulatori dovrebbero essere usati solo per il debug del 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 the IBM Quantum 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()Fase 4: Post-elaborazione, restituzione dei risultati in formato classico
Come descritto nell'introduzione, la misura più utile è la probabilità di misurare lo stato zero .
counts.get(0, 0.0) / num_shotsOutput:
0.6525
Questo è il risultato che volevamo: una stima del prodotto interno (fino al mod. quadrato) dei vettori corrispondenti a due punti dati. Se vogliamo esaminare l'intera distribuzione delle probabilità di misura (o quasiprobabilità), possiamo farlo utilizzando la funzione plot_distribution , come mostrato di seguito. Si vede che per un numero elevato di qubit, immagini come questa diventano rapidamente intrattabili.
from qiskit.visualization import plot_distribution
plot_distribution(counts_bit)Output:
In alternativa, si potrebbe definire una visualizzazione come quella che segue per esaminare solo le 10 misure più probabili. Questo potrebbe essere importante per la risoluzione dei problemi o per cercare di ottenere una maggiore intuizione dei dati. Ma la probabilità di misura dello stato zero è l'elemento della nostra 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:
Da queste informazioni su un solo prodotto interno tra due punti di dati nello spazio delle caratteristiche a più alta dimensionalità, tutto ciò che possiamo dire è che la loro sovrapposizione è abbastanza grande rispetto alla sovrapposizione massima (che sarebbe 1.0 ). Questo potrebbe essere un indicatore del fatto che questi due punti di dati sono in qualche modo simili per natura e saranno classificati nelle stesse classi. Oppure potrebbe essere un indicatore del fatto che la nostra mappa di caratteristiche non è efficace nel mappare in uno spazio in cui i dati simili hanno una forte sovrapposizione e i dati diversi hanno una piccola sovrapposizione. Per sapere quale sia la verità, dobbiamo applicare la nostra mappa di caratteristiche all'intero set di dati e vedere se la matrice kernel risultante può essere manipolata per separare efficacemente le classi con un'elevata precisione.
Vale la pena di notare che abbiamo utilizzato il sito z_feature_map che ha dato luogo a una bassa profondità di trasposizione a due qubit (profondità 1, in effetti). Se i circuiti diventano troppo profondi, sicuramente si produrrà molto rumore e la probabilità di misurare lo stato zero sarà molto bassa, anche se la mappa delle caratteristiche è ben adattata ai dati. Ad esempio, ripetendo il procedimento sopra descritto con zz_feature_map e , entanglement='linear', reps=1 si ottiene dist.get(0,0.0) = 0.0015 utilizzando gli stessi punti dati. Ciò è dovuto alle profondità dei circuiti e dei due-qubit molto più elevate di zz_feature_map. La figura seguente mostra la distribuzione di probabilità per questo calcolo.
Vale la pena di giocare con alcuni punti dati della stessa categoria per vedere quanto deve essere bassa la profondità per ottenere buoni risultati. Quelli che seguono sono consigli di massima che sicuramente avranno delle eccezioni. In generale, una profondità di 10 o meno di 10 transpiled a due qubit non dovrebbe costituire un problema. Una profondità di 50-60 transpiled a due qubit è lo stato dell'arte e richiederà, tra gli altri strumenti, una mitigazione avanzata degli errori. Nel mezzo, i risultati possono variare in base alla somiglianza dei dati, all'espressività della mappa delle caratteristiche, alla larghezza del circuito e ad altri fattori.
Normalmente la fase di post-elaborazione comprende anche i classici processi di apprendimento automatico. Nella prossima sezione estenderemo questo processo a un intero set di dati e mostreremo il classico flusso di lavoro dell'apprendimento automatico.
Verifica la tua comprensione
In un circuito quantistico a 10 qubit, in generale, quanti stati diversi si possono misurare?
o 1024.
Supponiamo che una persona alle prime armi con l'informatica quantistica tenti di utilizzare un circuito quantistico con una profondità a due qubit molto elevata, senza ricorrere alla mitigazione degli errori. Supponiamo inoltre che ciò comporti un tasso di errore del 10% su ciascun qubit. Se l'elemento della matrice del kernel vero (privo di errori) corrispondente a questo circuito fosse molto grande, ad esempio 1.0, quale sarebbe la probabilità di misurare tutti e 10 i qubit nello stato in cui ogni qubit è ?
La probabilità che ogni qubit si trovi correttamente nello stato |0> è 0.90. La probabilità che tutti i 10 qubit si trovino nello stato corretto è o circa il 35%.
Spiegate con parole vostre perché è così importante monitorare le profondità dei circuiti. Questo è vero in generale, ma spiegatelo nel contesto della stima del kernel quantistico.
In questo flusso di lavoro QKE, le nostre stime si basano sulle misure dello stato zero, ovvero lo stato in cui ogni qubit si trova nello stato . I circuiti molto profondi introdurranno alti tassi di errore. Se questo tasso di errore viene sommato a quello di molti qubit, la probabilità di misurare lo stato zero si riduce in modo sostanziale.
Matrice kernel completa
In questa sezione, estenderemo il processo sopra descritto alla classificazione binaria di un set di dati completo. In questo modo si introdurranno due componenti importanti: (1) possiamo ora implementare l'apprendimento automatico classico in post-elaborazione e (2) possiamo ottenere punteggi di accuratezza per la nostra formazione.
Fase 1: mappare gli input classici su un problema quantistico
Ora importeremo un set di dati esistente per la nostra classificazione. Questo set di dati è composto da 128 righe (punti dati) e 14 caratteristiche per ogni punto. C'è un 15° elemento che indica la categoria binaria di ogni punto ( ). Il set di dati è importato di seguito, oppure è possibile accedere al set di dati e visualizzarne la struttura qui.
Utilizzeremo i primi 90 punti di dati per l'addestramento e i successivi 30 punti per i 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]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]
Ci prepariamo già a memorizzare uscite multiple costruendo una matrice kernel e una matrice test di 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 mappa di caratteristiche per codificare e mappare i nostri dati classici in un circuito quantistico. Siamo liberi di costruire la nostra mappa delle caratteristiche o di utilizzarne una già pronta. Sentitevi liberi di modificare la mappa delle caratteristiche qui sotto o di tornare a ZFeatureMap. Ma prestate sempre attenzione alla profondità del circuito. Ricordiamo che nel precedente esempio a 6-qubit la profondità del circuito transpilato era intrattabile quando si utilizzava zz_feature_map. Con l'aumento della scala e della complessità del circuito, la profondità potrebbe aumentare rapidamente fino al punto in cui il rumore sovrasta i nostri risultati. Quando si conosce qualcosa della struttura dei dati che può indicare la struttura della mappa delle caratteristiche più utile, è consigliabile creare una mappa delle caratteristiche personalizzata che sfrutti tale 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)Passaggi 2 e 3: Ottimizzare il problema ed eseguire utilizzando primitive
Costruiremo un circuito di sovrapposizione e, se in questo esempio stessimo lavorando su un vero computer quantistico, lo ottimizzeremmo per l'esecuzione come prima. In questo caso, però, intendiamo passare su tutti i punti dati e calcolare l'intera matrice kernel. Per ogni coppia di vettori di dati e , creiamo un diverso circuito di sovrapposizione. Dobbiamo quindi ottimizzare il nostro circuito per ogni coppia di punti dati. Quindi le fasi 2 e 3 verrebbero eseguite insieme nelle iterazioni multiple.
La cella di codice sottostante esegue esattamente lo stesso processo di prima per una coppia di punti dati singoli. Questa volta viene eseguito semplicemente all'interno di due loop for e c'è una riga aggiuntiva alla fine kernel_matrix[x_1,x_2] = ... per memorizzare i risultati di ogni calcolo. Si noti che abbiamo sfruttato la simmetria di una matrice kernel per ridurre di 1/2 il numero di calcoli. Abbiamo anche impostato semplicemente gli elementi diagonali a 1, come dovrebbero essere in assenza di rumore. A seconda dell'implementazione e della precisione richiesta, è possibile utilizzare gli elementi diagonali per stimare il rumore o per conoscerlo ai fini della mitigazione degli errori.
Una volta che la matrice del kernel è stata completamente popolata, si ripete il processo per i dati di prova e si popola la matrice di prova. Anche questa è in realtà 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")Output:
training done
test matrix done
Fase 4: Post-elaborazione, restituzione dei risultati in formato classico
Ora che disponiamo di una matrice kernel e di una matrice test formattata in modo simile dai metodi kernel quantistici, possiamo applicare gli algoritmi classici di apprendimento automatico per fare previsioni sui nostri dati di prova e verificarne l'accuratezza. Inizieremo importando sklearn.svc, un classificatore a vettori di supporto (SVC) di Scikit-Learn. Dobbiamo specificare che vogliamo che l'SVC usi 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")Utilizzando SVC.fit, possiamo ora inserire la matrice del kernel e le etichette di addestramento per ottenere un adattamento. SVC.score valuterà quindi i nostri dati di prova rispetto a tale adattamento utilizzando la nostra matrice di prova 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}")Output:
Precomputed kernel classification test score: 1.0
Vediamo che l'accuratezza del nostro modello addestrato è stata del 100%. È un'ottima cosa e dimostra che la QKE può funzionare. Ma questo è molto diverso dal vantaggio quantistico. I kernel classici sarebbero stati probabilmente in grado di risolvere anche questo problema di classificazione con una precisione del 100%. C'è molto lavoro da fare per caratterizzare i diversi tipi di dati e le relazioni tra i dati per capire dove i kernel quantistici saranno più utili nell'attuale era dell'utilità. Lasciamo all'allievo la possibilità di modificare parti di questo flusso di lavoro e di studiare l'efficacia di varie mappe di caratteristiche quantistiche. Ecco alcuni elementi da considerare:
- Quanto è robusta la precisione? Vale per un'ampia tipologia di dati o solo per questi dati specifici di formazione?
- Quale struttura dei vostri dati vi fa sospettare l'utilità di una mappa di caratteristiche quantistiche?
- Come viene influenzata l'accuratezza aumentando/diminuendo la quantità di dati di addestramento?
- Quali mappe caratteristiche si possono utilizzare e come variano i risultati con le mappe caratteristiche?
- Come vengono influenzati l'accuratezza e il tempo di esecuzione dall'aumento del numero di caratteristiche?
- Quali tendenze, se ce ne sono, prevede che si affermino nei veri computer quantistici?
Scalabilità verso più funzionalità e qubit
In questa sezione, ripeteremo il calcolo di un singolo elemento della matrice, ma per un numero molto maggiore di caratteristiche, abbozzando il percorso di scala verso l'utilità. La limitazione a un singolo elemento della matrice è stata fatta in modo che il processo possa essere mostrato senza consumare troppo tempo sui computer quantistici.
Fase 1: mappare gli input classici su un problema quantistico
Assumiamo come punto di partenza un set di dati in cui ogni punto di dati ha 42 caratteristiche. Come nel primo esempio, calcoleremo un singolo elemento della matrice kernel, richiedendo due punti dati. I due punti sottostanti hanno 42 caratteristiche e un'unica variabile di categoria ( ).
# 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]]Ricordiamo che zz_feature_map ha prodotto circuiti piuttosto profondi nel caso di un numero relativamente basso di caratteristiche (14 caratteristiche). Man mano che aumentiamo il numero di funzioni, dobbiamo monitorare attentamente la profondità del circuito. Per illustrarlo, proviamo prima a utilizzare il sito 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")Output:
circuit depth = 251
two-qubit depth 165
Come descritto in precedenza, determinare con esattezza quanto sia profonda la profondità è una questione ricca di sfumature. Ma una profondità di due qubit superiore a 100, anche prima della transpilazione, non è un'opzione valida. Questo è il motivo per cui le mappe di caratteristiche personalizzate sono state enfatizzate nel corso di questa lezione. Se si conosce la struttura dell'intero set di dati, si dovrebbe progettare una mappa di entanglement tenendo conto di tale struttura. In questo caso, dato che stiamo calcolando solo il prodotto interno tra due punti di dati, abbiamo privilegiato una 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()Non ci preoccuperemo ancora di verificare le profondità, poiché ciò che conta davvero è la profondità a due qubit trasposta.
Fase 2: Ottimizzazione del problema per l'esecuzione quantistica
Si inizia selezionando il backend meno trafficato, quindi si ottimizza il circuito per l'esecuzione su tale 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')>
Nei lavori di piccole dimensioni, un gestore di passaggi preimpostato spesso restituisce lo stesso circuito con la stessa profondità, in modo affidabile. Tuttavia, in circuiti molto grandi e complessi, il gestore dei passaggi può restituire circuiti transpilati diversi a ogni esecuzione. Ciò è dovuto all'uso di euristiche e al fatto che i circuiti molto grandi presentano un panorama complicato di possibili ottimizzazioni. Spesso è utile trasporre alcune volte e prendere il circuito più superficiale. Questo introduce solo un overhead classico e può migliorare sostanzialmente i risultati del computer quantistico.
In questo caso, transpiliamo il circuito di sovrapposizione unitario 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())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]
Qui si può notare che c'è una certa variazione nella profondità totale del gate con diversi passaggi di transpilazione. Il nostro circuito non è ancora sufficientemente profondo/ampio per vedere la variazione delle profondità di trasposizione dei due qubit. Utilizzeremo il sito transpiled_qcs[1], che ha una profondità di 60, di poco inferiore a quella del circuito più profondo ottenuto, che era 77.
overlap_ibm = transpiled_qcs[1]Fase 3: Esecuzione tramite le primitive di " IBM Quantum "
Quando ci avviciniamo all'utilità, i simulatori non saranno più utili. Qui viene mostrata solo la sintassi dei veri computer quantistici.
# Run on ibm_osaka, 7-12-24, required 22 sec.
# Import our IBM Quantum primitive
from qiskit_ibm_runtime import SamplerV2 as Sampler
# Open a 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()Fase 4: Post-elaborazione, restituzione dei risultati in formato classico
Come descritto nell'introduzione, la misura più utile è la probabilità di misurare lo stato zero .
counts.get(0, 0.0) / num_shotsOutput:
0.0138
Questo processo per il singolo elemento della matrice kernel può essere ripetuto tra le altre coppie di dati del set per ottenere la matrice kernel completa. La dimensione della matrice kernel è dettata dal numero di punti nei dati di addestramento, non dal numero di caratteristiche. Quindi il costo di calcolo della manipolazione della matrice kernel in un modello predittivo non scala come il numero di caratteristiche o di qubit. Anche per insiemi di dati relativamente piccoli con un gran numero di caratteristiche, i dati devono essere abbinati a una mappa di caratteristiche che produca una classificazione efficace.
Scalabilità e lavoro futuro
Il metodo kernel richiede che la misura di sia il più accurata possibile. Ma gli errori di gate e gli errori di lettura significano che esiste una probabilità non nulla che un dato qubit venga erroneamente misurato nello stato . Anche con l'eccessiva semplificazione che la probabilità di dovrebbe essere , per molte caratteristiche codificate, ad esempio, su bit, la probabilità di misurare correttamente tutti i bit si riduce a . Quando diventa grande, questo metodo diventa sempre meno affidabile. Superare questa difficoltà e scalare la stima del kernel a un numero sempre maggiore di caratteristiche è un'area di ricerca attuale. Per saperne di più su questo problema, si veda questo lavoro di Thanasilp, Wang, Cerezo e Holmes. Vi consigliamo di esplorare ciò che è possibile fare con gli attuali computer quantistici e di guardare anche a ciò che sarà possibile fare nell'era della correzione degli errori.
Riesamina
Il calcolo di un kernel quantistico comporta
- calcolare le voci della matrice kernel, utilizzando coppie di punti dati di addestramento
- codificare i dati e mapparli attraverso una mappatura delle caratteristiche
- ottimizzare il circuito per l'esecuzione su computer quantistici reali / backend
Il kernel quantistico può quindi essere utilizzato negli algoritmi classici di apprendimento automatico, come in questa lezione.
Alcuni elementi chiave da tenere a mente quando si utilizzano i kernel quantistici sono:
- È probabile che il set di dati tragga beneficio dai metodi quantum kernel?
- Provare diverse mappe di caratteristiche e schemi di entanglement.
- La profondità del circuito è accettabile?
- Provare a eseguire più volte un pass manager e utilizzare il circuito più piccolo possibile.
I metodi dei kernel quantistici sono strumenti potenzialmente potenti, se si considera un'adeguata corrispondenza tra i set di dati con caratteristiche quantistiche e un'adeguata mappa di caratteristiche quantistiche. Per capire meglio dove i kernel quantistici possono essere utili, si consiglia di leggere Liu, Arunachalam & Temme (2021).