Sari la conținutul principal

Kerneluri cuantice

Introducere în kernelurile cuantice

„Metoda kernel cuantic" se referă la orice metodă care utilizează calculatoare cuantice pentru a estima un kernel. În acest context, „kernel" va desemna matricea kernel sau elementele individuale ale acesteia. Reamintim că o mapare a caracteristicilor Φ(x)\Phi(\vec{x}) este o mapare de la xRd\vec{x}\in \mathbb{R}^d la Φ(x)Rd,\Phi(\vec{x})\in \mathbb{R}^{d'}, unde de obicei d>dd'>d și scopul acestei mapări este de a face categoriile de date separabile printr-un hiperplan. Funcția kernel primește ca argumente vectori din spațiul mapat al caracteristicilor și returnează produsul lor intern, adică K:Rd×RdRK:\mathbb{R}^d\times\mathbb{R}^d\rightarrow \mathbb{R} cu K(x,y)=Φ(x)Φ(y)K(x,y) = \langle \Phi(x)|\Phi(y)\rangle. În mod clasic, suntem interesați de mapări ale caracteristicilor pentru care funcția kernel este ușor de evaluat. Adesea, asta înseamnă găsirea unei funcții kernel pentru care produsul intern din spațiul caracteristicilor mapate poate fi scris în termenii vectorilor de date originali, fără a fi nevoie să construim Φ(x)\Phi(x) și Φ(y)\Phi(y). În metoda kernelurilor cuantice, maparea caracteristicilor este realizată printr-un Circuit cuantic, iar kernelul este estimat folosind măsurători pe acel circuit și probabilitățile relative de măsurare.

În această lecție vom examina adâncimile circuitelor de codificare pre-codate care utilizează un entanglement substanțial și le vom compara cu adâncimile circuitelor pe care le codificăm manual. Nu pledăm pentru o metodă în detrimentul alteia. S-ar putea să constați că circuitele pre-codate sunt prea adânci și că entanglementul din circuitul construit personalizat este insuficient pentru a fi util. Din nou, acestea sunt prezentate doar pentru a-ți permite să explorezi.

Înainte de a parcurge în detaliu estimarea unei matrice kernel, să prezentăm fluxul de lucru folosind limbajul pattern-urilor Qiskit.

Pasul 1: Mapează intrările clasice la o problemă cuantică

  • Intrare: Set de date de antrenament
  • Ieșire: Circuit abstract pentru calcularea unui element al matricei kernel

Pornind de la setul de date, primul pas este codificarea datelor într-un circuit cuantic. Cu alte cuvinte, trebuie să mapăm datele în spațiul Hilbert al stărilor calculatorului nostru cuantic. Facem asta construind un circuit dependent de date. Există mai multe moduri de a proceda, iar lecția anterioară a prezentat o serie de opțiuni. Poți construi propriul circuit pentru a-ți codifica datele sau poți folosi o mapare de caracteristici pre-creată, cum ar fi zz_feature_map. În această lecție, vom face ambele.

Reține că, pentru a calcula un singur element al matricei kernel, va trebui să codificăm două puncte diferite, astfel încât să putem estima produsul lor intern. Un flux de lucru complet al kernelului cuantic va implica, desigur, multe astfel de produse interne între vectorii de date mapați, precum și metode clasice de machine learning. Dar pasul de bază care se iterează este estimarea unui singur element al matricei kernel. Pentru aceasta, selectăm un circuit cuantic dependent de date și mapăm doi vectori de date în spațiul caracteristicilor.

Classical_Review_background_kernel_circuit

Pentru sarcina de a genera o matrice kernel, suntem în mod special interesați de probabilitatea de a măsura starea 0N|0\rangle^{\otimes N}, în care toți NN Qubiți se află în starea 0|0\rangle. Pentru a înțelege asta, considerăm că circuitul responsabil de codificarea și maparea unui vector de date xi\vec{x}_i poate fi scris ca Φ(xi)\Phi(\vec{x}_i), iar cel responsabil de codificarea și maparea lui xj\vec{x}_j este Φ(xj)\Phi(\vec{x}_j), și notăm stările mapate

ψ(xi)=Φ(xi)0N|\psi(\vec{x}_i)\rangle = \Phi(\vec{x}_i)|0\rangle^{\otimes N} ψ(xj)=Φ(xj)0N.|\psi(\vec{x}_j)\rangle = \Phi(\vec{x}_j)|0\rangle^{\otimes N}.

Aceste stări sunt maparea datelor în dimensiuni superioare, deci elementul kernel dorit este produsul intern

ψ(xj)ψ(xi)=0NΦ(xj)Φ(xi)0N.\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}.

Dacă operăm asupra stării inițiale implicite 0N|0\rangle^{\otimes N} cu ambele circuite Φ(xj)\Phi^\dagger(\vec{x}_j) și Φ(xi)\Phi(\vec{x}_i), probabilitatea de a măsura apoi starea 0N|0\rangle^{\otimes N} este

P0=0NΦ(xj)Φ(xi)0N2.P_0 = |\langle0|^{\otimes N}\Phi^\dagger(\vec{x}_j)\Phi(\vec{x}_i)|0\rangle^{\otimes N}|^2.

Aceasta este exact valoarea pe care o dorim (până la 2||^2). Stratul de măsurare al circuitului nostru va returna probabilități de măsurare (sau „quasi-probabilități", cum se numesc, dacă se folosesc anumite metode de mitigare a erorilor). Probabilitatea de interes este cea a stării zero, 0N|0\rangle^{\otimes N}.

Pasul 2: Optimizează problema pentru execuție cuantică

  • Intrare: Circuit abstract, neoptimizat pentru un backend specific
  • Ieșire: Circuitul țintă și observabila, optimizate pentru QPU-ul selectat

În acest pas, vom folosi funcția generate_preset_pass_manager din Qiskit pentru a specifica o rutină de optimizare pentru circuitul nostru în raport cu calculatorul cuantic real pe care intenționăm să rulăm experimentul. Setăm optimization_level=3, ceea ce înseamnă că vom folosi managerul de pași preset care oferă cel mai înalt nivel de optimizare. În acest context, „optimizare" se referă la optimizarea implementării circuitului pe un calculator cuantic real. Asta include considerații precum selectarea qubiților fizici corespunzători qubiților din circuitul cuantic abstract, care să minimizeze adâncimea Gate-urilor, sau selectarea qubiților fizici cu cele mai mici rate de eroare disponibile. Aceasta nu este direct legată de optimizarea problemei de machine learning (cum ar fi optimizatorii clasici de tipul COBYLA).

În funcție de modul în care implementezi pasul 2, este posibil să fie nevoie să optimizezi circuitul de mai multe ori, deoarece fiecare pereche de puncte implicată într-un element al matricei produce un circuit diferit care trebuie măsurat.

Pasul 3: Execută folosind Qiskit Runtime Primitives

  • Intrare: Circuitul țintă
  • Ieșire: Distribuție de probabilitate

Folosește primitiva Sampler din Qiskit Runtime pentru a reconstrui o distribuție de probabilitate a stărilor obținute prin eșantionarea circuitului. Reține că acest lucru poate fi denumit și „distribuție de quasi-probabilitate", un termen care se aplică acolo unde zgomotul constituie o problemă și când se introduc pași suplimentari, cum ar fi mitigarea erorilor. În astfel de cazuri, suma tuturor probabilităților poate să nu fie exact egală cu 1; de aceea termenul „quasi-probabilitate".

Pasul 4: Post-procesare, returnează rezultatul în format clasic

  • Intrare: Distribuție de probabilitate
  • Ieșire: Un singur element al matricei kernel sau o matrice kernel dacă se repetă

Calculează probabilitatea de a măsura 0N|0\rangle^{\otimes N} pe circuitul cuantic și populează matricea kernel în poziția corespunzătoare celor doi vectori de date utilizați. Pentru a completa întreaga matrice kernel, trebuie să rulăm un experiment cuantic pentru fiecare intrare. Odată ce avem o matrice kernel, o putem folosi în mulți algoritmi clasici de machine learning care acceptă pre-calculated kernels. De exemplu: qml_svc = SVC(kernel="precomputed"). Putem apoi folosi fluxuri de lucru clasice pentru a aplica modelul nostru pe datele de testare și a obține un scor de acuratețe. În funcție de satisfacția față de scorul de acuratețe, s-ar putea să fie nevoie să revizuim aspecte ale calculului, cum ar fi maparea caracteristicilor.

Prezentarea lecției

În această lecție vom parcurge acești pași în mai multe moduri pentru a face o utilizare optimă a timpului tău pe calculatoarele cuantice reale. Vom aplica o metodă de kernel cuantic la:

  • Un singur element al matricei kernel pentru date cu relativ puține caracteristici, folosind un backend real, astfel încât să putem urmări cu ușurință ce se întâmplă la fiecare pas.
  • Un set de date complet cu relativ puține caracteristici, folosind un backend simulat, astfel încât să putem vedea cum se conectează fluxul de lucru cuantic cu metodele clasice de machine learning
  • Un singur element al matricei kernel pentru date cu multe caracteristici, folosind un calculator cuantic real. Nu vom estima o întreagă matrice kernel pentru un set de date mare, pentru a respecta timpul pe calculatoarele cuantice 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

Element unic al matricei kernel

Pasul 1: Mapează intrările clasice la o problemă cuantică

Să considerăm mai întâi un set de date cu doar câteva caracteristici, să zicem 10. Setul de date ar putea fi oricât de mare, deoarece calculăm elementele matricei kernel unul câte unul. Avem nevoie de cel puțin două puncte, deci vom începe cu atât (în exemplul următor, vom importa un set de date complet). Să importăm câteva pachete necesare:

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

Putem încerca să folosim 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])

Cele două unitare de mai sus corespund exact lui U1U_1 și U2U_2 descrise în introducere. Le putem combina folosind unitary_overlap. Ca întotdeauna, vrem să urmărim adâncimea circuitului.

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

Rezultatul celulei de cod anterioare

Pasul 2: Optimizează problema pentru execuție cuantică

Începem prin a selecta backend-ul cel mai puțin ocupat, apoi optimizăm circuitul pentru a rula pe acel 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)

Pentru circuite complicate, acest pas va crește substanțial adâncimea circuitului, deoarece mapează la porți native pentru calculatoarele cuantice reale, iar informațiile pot trebui mutate de la Qubit la Qubit. În acest caz simplu, adâncimea aproape că nu este afectată.

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

Pasul 3: Execută folosind Qiskit Runtime Primitives

Sintaxa pentru rularea pe un simulator este comentată mai jos. Pentru acest set de date, cu un număr mic de caracteristici, rularea pe un simulator este încă o opțiune. Pentru calculele la scară utilă, simularea nu este de obicei fezabilă. Simulatoarele ar trebui folosite doar pentru a depana codul la scară redusă.

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

Pasul 4: Post-procesare, returnarea rezultatului în format clasic

Așa cum s-a descris în introducere, cea mai utilă măsurătoare de aici este probabilitatea de a măsura starea zero 00000|00000\rangle.

counts.get(0, 0.0) / num_shots
0.6525

Acesta este rezultatul pe care l-am dorit: o estimare a produsului intern (până la modul pătrat) al vectorilor corespunzători a două puncte de date. Dacă vrem să privim distribuția completă a probabilităților de măsurare (sau cuasiprobabilităților), putem face asta folosind funcția plot_distribution, așa cum este arătat mai jos. Se observă că, pentru un număr mare de qubiți, imagini ca aceasta devin rapid intractabile.

from qiskit.visualization import plot_distribution

plot_distribution(counts_bit)

Rezultatul celulei de cod anterioare

Alternativ, s-ar putea defini o vizualizare ca cea de mai jos pentru a privi doar primele 10 cele mai probabile măsurători. Aceasta ar putea fi importantă pentru depanare sau pentru a obține mai multă intuiție asupra datelor. Dar probabilitatea de măsurare a stării zero este elementul nostru din matricea 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)

Rezultatul celulei de cod anterioare

Din aceste informații despre un singur produs intern dintre două puncte de date în spațiul de caracteristici de dimensiune superioară, tot ce putem spune este că suprapunerea lor este destul de mare în comparație cu suprapunerea maximă (care ar fi 1.0). Acesta ar putea fi un indicator că aceste două puncte de date sunt cumva similare ca natură și vor fi încadrate în aceleași clase. Sau ar putea fi un indicator că harta noastră de caracteristici nu este eficientă în maparea într-un spațiu în care datele similare au o suprapunere puternică, iar datele diferite au o suprapunere mică. Pentru a ști care dintre variante este adevărată, trebuie să aplicăm harta de caracteristici întregului set de date și să vedem dacă matricea kernel rezultată poate fi manipulată pentru a separa eficient clasele cu acuratețe ridicată.

Merită menționat că am folosit z_feature_map, care a dus la o adâncime transpilată cu doi qubiți mică (adâncime 1, de fapt). Dacă circuitele tale devin prea adânci, este sigur că va rezulta mult zgomot, iar aceasta va reduce probabilitatea de a măsura starea zero la o valoare foarte mică, chiar dacă harta de caracteristici se potrivește bine datelor tale. De exemplu, o repetare a procesului de mai sus folosind zz_feature_map și , entanglement='linear', reps=1 a produs dist.get(0,0.0) = 0.0015 folosind aceleași puncte de date. Aceasta se datorează adâncimilor de circuit și adâncimilor cu doi qubiți mult mai mari ale zz_feature_map. Figura de mai jos arată distribuția de probabilitate pentru acel calcul.

Rezultate slabe de la o hartă de caracteristici zz.

Merită să experimentezi cu câteva puncte de date din aceeași categorie pentru a vedea cât de mică trebuie să fie adâncimea pentru a obține rezultate bune. Cele de mai jos sunt sfaturi generale care cu siguranță au excepții. În general, o adâncime transpilată cu doi qubiți de 10 sau mai puțin nu ar trebui să fie o problemă. O adâncime transpilată cu doi qubiți de 50-60 reprezintă cel mai avansat nivel actual și va necesita mitigarea avansată a erorilor, printre alte instrumente. Între aceste valori, rezultatele pot varia în funcție de similaritatea datelor, expresivitatea hărții de caracteristici, lățimea circuitului și alți factori. De obicei, pasul de post-procesare ar include și procese clasice de machine learning. În secțiunea următoare vom extinde acest proces la un întreg set de date și vom prezenta fluxul de lucru al machine learning-ului clasic.

Verifică-ți înțelegerea

Citește întrebările de mai jos, gândește-te la răspunsuri, apoi apasă pe triunghiuri pentru a dezvălui soluțiile.

Într-un circuit cuantic cu 10 qubiți, în general, câte stări diferite pot fi măsurate?

Răspuns:

2102^{10} sau 1024.

Să presupunem că cineva nou în domeniul calculului cuantic încearcă să folosească un circuit cuantic cu o adâncime cu doi qubiți foarte mare și nu folosește mitigarea erorilor. Să presupunem mai departe că aceasta duce la o rată de eroare de 10% pe fiecare qubit. Dacă elementul adevărat (fără erori) al matricei kernel corespunzător acestui circuit este foarte mare, să zicem 1.0, care ar fi probabilitatea de a măsura toți cei 10 qubiți în starea în care fiecare qubit este |0>?

Răspuns:

Probabilitatea ca fiecare qubit să fie găsit corect în starea |0> este 0.90. Probabilitatea ca toți cei 10 qubiți să fie găsiți în starea corectă este 0.90100.90^{10} sau aproximativ 35%.

Explică în cuvintele tale de ce este atât de important să monitorizezi adâncimile circuitelor. Aceasta este adevărat în general, dar explică-o în contextul estimării kernelului cuantic.

Răspuns:

În acest flux de lucru QKE, estimările noastre se bazează pe măsurătorile stării zero, adică starea în care fiecare qubit este găsit în starea 0|0\rangle. Circuitele foarte adânci vor introduce rate ridicate de eroare. Când acea rată de eroare se cumulează pe mai mulți qubiți, aceasta va reduce substanțial probabilitatea de a măsura starea zero.

Matricea kernel completă

În această secțiune, vom extinde procesul de mai sus la clasificarea binară a unui set de date complet. Aceasta va introduce două componente importante: (1) acum putem implementa machine learning clasic în post-procesare și (2) putem obține scoruri de acuratețe pentru antrenamentul nostru.

Pasul 1: Maparea intrărilor clasice la o problemă cuantică

Acum vom importa un set de date existent pentru clasificarea noastră. Acest set de date constă din 128 de rânduri (puncte de date) și 14 caracteristici pentru fiecare punct. Există un al 15-lea element care indică categoria binară a fiecărui punct (±1\pm 1). Setul de date este importat mai jos, sau poți accesa setul de date și vizualiza structura sa aici.

Vom folosi primele 90 de puncte de date pentru antrenament și următoarele 30 de puncte pentru testare.

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

Ne vom pregăti deja pentru stocarea mai multor rezultate, construind o matrice de kernel și o matrice de testare cu dimensiunile corespunzătoare.

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

Acum creăm o hartă de caracteristici pentru codificarea și maparea datelor noastre clasice într-un Circuit cuantic. Suntem liberi să construim propria noastră hartă de caracteristici sau să folosim una prefabricată. Nu ezita să modifici harta de caracteristici de mai jos sau să revii la ZFeatureMap. Dar acordă întotdeauna atenție adâncimii circuitului. Reamintim că în exemplul anterior cu 6 qubiți, adâncimea circuitului transpilat era prohibitiv de mare atunci când se folosea zz_feature_map. Pe măsură ce scara și complexitatea circuitului cresc, adâncimea poate crește rapid până la un punct în care zgomotul copleșește rezultatele noastre. Ori de câte ori știi ceva despre structura datelor tale care ar putea indica ce structură de hartă de caracteristici ar fi cea mai utilă, este recomandat să îți creezi propria hartă de caracteristici personalizată care valorifică acea cunoaștere.

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)

Pașii 2 și 3: Optimizarea problemei și execuția folosind primitive

Vom construi un circuit de suprapunere și, dacă am rula pe un calculator cuantic real în acest exemplu, l-am optimiza pentru execuție ca înainte. Dar în acest caz, intenționăm să parcurgem toate punctele de date și să calculăm matricea de kernel completă. Pentru fiecare pereche de vectori de date xi\vec{x}_i și xj\vec{x}_j, creăm un circuit de suprapunere diferit. Prin urmare, trebuie să optimizăm circuitul pentru fiecare pereche de puncte de date. Astfel, pașii 2 și 3 ar fi efectuați împreună în mai multe iterații.

Celula de cod de mai jos efectuează exact același proces ca înainte pentru o singură pereche de puncte de date. De această dată, este pur și simplu executat în interiorul a două bucle for, iar la final există linia suplimentară kernel_matrix[x_1,x_2] = ... pentru a stoca rezultatele fiecărui calcul. Rețineți că am utilizat simetria unei matrice de kernel pentru a reduce numărul de calcule la jumătate. De asemenea, am setat pur și simplu elementele diagonale la 1, deoarece ar trebui să fie astfel în absența zgomotului. În funcție de implementarea ta și de precizia necesară, ai putea folosi și elementele diagonale pentru a estima zgomotul sau a afla mai multe despre el în scopuri de atenuare a erorilor.

Odată ce matricea de kernel a fost complet populată, repetăm procesul pentru datele de testare și populăm test_matrix. Aceasta este de fapt tot o matrice de kernel; îi dăm pur și simplu un nume diferit pentru a le distinge.

# 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

Pasul 4: Post-procesare, returnarea rezultatului în format clasic

Acum că avem o matrice kernel și un test_matrix formatat similar, obținute prin metode kernel cuantice, putem aplica algoritmi clasici de machine learning pentru a face predicții despre datele de test și a verifica acuratețea. Vom începe prin importarea sklearn.svc din Scikit-Learn, un clasificator cu vectori suport (SVC). Trebuie să specificăm că dorim ca SVC-ul să folosească kernelul nostru precalculat, utilizând 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")

Folosind SVC.fit, putem acum furniza matricea kernel și etichetele de antrenament pentru a obține un model ajustat. SVC.score va evalua apoi datele noastre de test față de acel model, folosind test_matrix, și va returna acuratețea.

# 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

Vedem că acuratețea modelului antrenat a fost 100%. Acesta este un rezultat excelent și demonstrează că QKE poate funcționa. Dar asta este foarte diferit de avantajul cuantic. Kernelurile clasice ar fi putut rezolva această problemă de clasificare cu 100% acuratețe la fel de bine. Mai este mult de lucru pentru a caracteriza diferite tipuri de date și relații între date, pentru a determina unde kernelurile cuantice vor fi cele mai utile în era actuală de utilitate. Îi lăsăm celui care învață sarcina de a modifica părți din acest flux de lucru și de a studia eficacitatea diferitelor hărți de caracteristici cuantice. Iată câteva aspecte de luat în considerare:

  • Cât de robustă este acuratețea? Se menține pentru tipuri largi de date sau doar pentru aceste date de antrenament specifice?
  • Ce structură din datele tale te face să suspectezi că o hartă de caracteristici cuantice este utilă?
  • Cum este afectată acuratețea prin creșterea/scăderea cantității de date de antrenament?
  • Ce hărți de caracteristici poți folosi și cum variază rezultatele în funcție de hărți?
  • Cum sunt afectate acuratețea și timpul de execuție prin creșterea numărului de caracteristici?
  • Ce tendințe, dacă există, te aștepți să se mențină pe calculatoare cuantice reale?

Scalarea la mai multe caracteristici și qubiți

În această secțiune, vom repeta calculul unui singur element de matrice, dar pentru un număr mult mai mare de caracteristici, schițând calea spre scalare în direcția utilității. Restricția la un singur element de matrice este aplicată astfel încât procesul să poată fi demonstrat fără a consuma prea mult din timpul alocat pe calculatoarele cuantice.

Pasul 1: Maparea intrărilor clasice la o problemă cuantică

Vom presupune ca punct de plecare un set de date în care fiecare punct de date are 42 de caracteristici. Ca și în primul exemplu, vom calcula un singur element al matricei kernel, necesitând două puncte de date. Cele două puncte de mai jos au 42 de caracteristici și o singură variabilă categorică (±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]]

Reamintește-ți că zz_feature_map a produs circuite destul de adânci în cazul unui număr relativ mic de caracteristici (14 caracteristici). Pe măsură ce creștem numărul de caracteristici, trebuie să monitorizăm îndeaproape adâncimea circuitului. Pentru a ilustra acest lucru, vom încerca mai întâi să folosim zz_feature_map și să verificăm adâncimea circuitului rezultat.

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

După cum s-a descris anterior, determinarea exactă a cât de adânc este prea adânc este o problemă nuanțată. Dar o adâncime pe doi qubiți mai mare de 100, chiar înainte de transpilare, este de neacceptat. De aceea, pe parcursul acestei lecții s-a insistat pe hărți de caracteristici personalizate. Dacă știi ceva despre structura întregului tău set de date, ar trebui să proiectezi o hartă de entanglement ținând cont de acea structură. Aici, deoarece calculăm doar produsul intern dintre două astfel de puncte de date, am prioritizat adâncimea redusă a circuitului față de orice considerație detaliată a structurii datelor.

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

Nu ne vom mai obosi să verificăm adâncimile acum, deoarece ceea ce contează cu adevărat este adâncimea pe doi qubiți după transpilare.

Pasul 2: Optimizează problema pentru execuția cuantică

Începem prin selectarea backend-ului cel mai puțin ocupat, apoi optimizăm circuitul nostru pentru a rula pe acel 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')>

Pe joburi de mică amploare, un pass manager prestabilit va returna adesea același circuit cu aceeași adâncime, în mod fiabil. Dar în circuite foarte mari și complexe, pass manager-ul poate returna circuite transpilate diferite de fiecare dată când rulează. Acest lucru se întâmplă deoarece folosește euristici și pentru că circuitele foarte mari vor avea un peisaj complicat de optimizări posibile. Este adesea util să transpilezi de câteva ori și să alegi circuitul cel mai puțin adânc. Aceasta introduce doar overhead clasic și poate îmbunătăți substanțial rezultatele de la calculatorul cuantic.

Aici, transpilăm circuitul de suprapunere unitară de 20 de ori și analizăm adâncimile circuitelor obținute.

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

Aici poți vedea că există o oarecare variație în adâncimea totală a porților cu diferite pasuri de transpilare. Circuitul nostru nu este încă suficient de adânc/larg pentru a observa variație în adâncimile transpilate pe două qubiți. Vom folosi transpiled_qcs[1], care are o adâncime de 60, ușor mai mică decât adâncimea celui mai adânc circuit obținut, care a fost 77.

overlap_ibm = transpiled_qcs[1]

Pasul 3: Execuție folosind Qiskit Runtime Primitives

Pe măsură ce ne apropiem de utilitate la scară, simulatoarele nu vor mai fi utile. Aici este prezentată doar sintaxa pentru calculatoarele cuantice reale.

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

Pasul 4: Post-procesare, returnarea rezultatului în format clasic

Așa cum s-a descris în introducere, măsurătoarea cea mai utilă aici este probabilitatea de a măsura starea zero 00000|00000\rangle.

counts.get(0, 0.0) / num_shots
0.0138

Acest proces pentru elementul unic al matricei kernel ar putea fi repetat între alte perechi de date din setul tău pentru a obține matricea kernel completă. Dimensiunea matricei kernel este dictată de numărul de puncte din datele tale de antrenament, nu de numărul de caracteristici. Astfel, costul computațional al manipulării matricei kernel într-un model predictiv nu se scalează după numărul de caracteristici sau qubiți. Chiar și pentru seturi de date relativ mici cu un număr mare de caracteristici, datele ar trebui în continuare asociate cu o hartă de caracteristici care produce o clasificare eficientă.

Scalare și lucru viitor

Metoda kernel necesită să măsurăm 0|0\rangle cât mai precis posibil. Dar erorile de porți și erorile de citire înseamnă că există o probabilitate nenulă pp ca orice qubit dat să fie măsurat eronat ca fiind în starea 1|1\rangle. Chiar și cu simplificarea excesivă că probabilitatea 0|0\rangle ar trebui să fie 100%100\%, pentru multe caracteristici codificate pe, să zicem, NN biți, probabilitatea de a măsura corect toți biții ca 0|0\rangle se reduce la (1p)N(1-p)^N. Pe măsură ce NN devine mare, această metodă devine din ce în ce mai puțin fiabilă. Depășirea acestei dificultăți și scalarea estimării kernel la tot mai multe caracteristici este un domeniu de cercetare actual. Pentru a afla mai multe despre această problemă, consultă lucrarea lui Thanasilp, Wang, Cerezo și Holmes. Te recomandăm să explorezi ce se poate face cu calculatoarele cuantice actuale și să privești cu interes spre ceea ce va fi posibil în era corecției erorilor.

Recapitulare

Calcularea unui kernel cuantic implică

  • calcularea intrărilor matricei kernel, folosind perechi de puncte de date de antrenament
  • codificarea datelor și maparea lor printr-o hartă de caracteristici
  • optimizarea circuitului tău pentru rularea pe calculatoare cuantice reale / backend-uri

Kernel-ul cuantic poate fi apoi utilizat în algoritmi clasici de machine learning, așa cum s-a arătat în această lecție.

Câteva lucruri cheie de reținut când folosești kernel-uri cuantice:

  • Este setul de date susceptibil să beneficieze de metodele kernel cuantic?
  • Încearcă hărți de caracteristici și scheme de entanglement diferite.
  • Adâncimea circuitului este acceptabilă?
  • Încearcă să rulezi un pass manager de mai multe ori și folosește circuitul cu cea mai mică adâncime pe care îl poți obține.

Metodele kernel cuantic sunt instrumente potențial puternice, dacă există o potrivire adecvată între seturi de date cu caracteristici compatibile cu calculul cuantic și o hartă de caracteristici cuantice potrivită. Pentru a înțelege mai bine unde kernel-urile cuantice sunt susceptibile să fie utile, recomandăm să citești Liu, Arunachalam & Temme (2021).