Sari la conținutul principal

Circuite Cuantice Variaționale și Rețele Neuronale Cuantice

În această lecție, implementăm mai multe circuite cuantice variaționale pentru o sarcină de clasificare a datelor, denumite clasificatoare cuantice variaționale (VQC-uri). La un moment dat, era obișnuit să se facă referire la un subset de VQC-uri ca rețele neuronale cuantice (QNN-uri), prin analogie cu rețelele neuronale clasice. Într-adevăr, există cazuri în care structuri împrumutate din rețelele neuronale clasice, cum ar fi straturile de convoluție, joacă un rol important în VQC-uri. În cazurile în care analogia este puternică, QNN-urile pot fi o descriere utilă. Totuși, circuitele cuantice parametrizate nu trebuie să urmeze structura generală a unei rețele neuronale; de exemplu, nu toate datele trebuie să fie încărcate în primul strat (de intrare); putem încărca unele date în primul strat, aplica câteva porți și apoi încărca date suplimentare (un proces numit „reîncărcare" a datelor). Ar trebui, prin urmare, să gândim QNN-urile ca un subset al circuitelor cuantice parametrizate și să nu ne limităm explorarea circuitelor cuantice utile prin analogia cu rețelele neuronale clasice.

Setul de date abordat în această lecție constă din imagini ce conțin dungi orizontale și verticale, iar scopul nostru este să etichetăm imaginile nevăzute în una dintre cele două categorii, în funcție de orientarea liniei lor. Vom realiza acest lucru cu un VQC. Pe parcurs, vom aborda moduri prin care calculul poate fi îmbunătățit și scalat. Setul de date de față este extrem de ușor de clasificat clasic. A fost ales pentru simplitatea sa, astfel încât să ne putem concentra pe partea cuantică a acestei probleme și să vedem cum un atribut al unui set de date s-ar putea traduce într-o parte a unui circuit cuantic. Nu este rezonabil să ne așteptăm la o accelerare cuantică pentru cazuri atât de simple, unde algoritmii clasici sunt atât de eficienți.

La sfârșitul acestei lecții, ar trebui să fii capabil să:

  • Încarci date dintr-o imagine într-un circuit cuantic
  • Construiești un ansatz pentru un VQC (sau QNN) și să îl ajustezi pentru a se potrivi problemei tale
  • Antrenezi VQC-ul/QNN-ul tău și să îl folosești pentru a face predicții precise pe datele de testare
  • Scalezi problema și să recunoști limitele calculatoarelor cuantice actuale

Generarea datelor

Vom începe prin a construi datele. Seturile de date nu sunt adesea generate explicit ca parte a framework-ului de tipare Qiskit. Totuși, tipul și pregătirea datelor sunt esențiale pentru aplicarea cu succes a calculului cuantic în învățarea automată. Codul de mai jos definește un set de date de imagini cu dimensiuni fixe de pixeli. O linie completă sau o coloană completă a imaginii primește valoarea π/2\pi/2, iar pixelii rămași primesc valori aleatoare pe intervalul (0,π/4)(0,\pi/4). Valorile aleatoare reprezintă zgomot în datele noastre. Parcurge codul pentru a te asigura că înțelegi cum sunt generate imaginile. Mai târziu vom mări dimensiunea imaginilor.

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-ibm-runtime scipy scikit-learn
# This code defines the images to be classified:

import numpy as np

# Total number of "pixels"/qubits
size = 8
# One dimension of the image (called vertical, but it doesn't matter). Must be a divisor of `size`
vert_size = 2
# The length of the line to be detected (yellow). Must be less than or equal to the smallest dimension of the image (`<=min(vert_size,size/vert_size)`
line_size = 2

def generate_dataset(num_images):
images = []
labels = []
hor_array = np.zeros((size - (line_size - 1) * vert_size, size))
ver_array = np.zeros((round(size / vert_size) * (vert_size - line_size + 1), size))

j = 0
for i in range(0, size - 1):
if i % (size / vert_size) <= (size / vert_size) - line_size:
for p in range(0, line_size):
hor_array[j][i + p] = np.pi / 2
j += 1

# Make two adjacent entries pi/2, then move down to the next row. Careful to avoid the "pixels" at size/vert_size - linesize, because we want to fold this list into a grid.

j = 0
for i in range(0, round(size / vert_size) * (vert_size - line_size + 1)):
for p in range(0, line_size):
ver_array[j][i + p * round(size / vert_size)] = np.pi / 2
j += 1

# Make entries pi/2, spaced by the length/rows, so that when folded, the entries appear on top of each other.

for n in range(num_images):
rng = np.random.randint(0, 2)
if rng == 0:
labels.append(-1)
random_image = np.random.randint(0, len(hor_array))
images.append(np.array(hor_array[random_image]))

elif rng == 1:
labels.append(1)
random_image = np.random.randint(0, len(ver_array))
images.append(np.array(ver_array[random_image]))
# Randomly select 0 or 1 for a horizontal or vertical array, assign the corresponding label.

# Create noise
for i in range(size):
if images[-1][i] == 0:
images[-1][i] = np.random.rand() * np.pi / 4
return images, labels

hor_size = round(size / vert_size)

Rețineți că codul de mai sus a generat, de asemenea, etichete care indică dacă imaginile conțin o linie verticală (+1) sau orizontală (-1). Vom folosi acum sklearn pentru a împărți un set de date de 100 de imagini într-un set de antrenament și unul de testare (împreună cu etichetele lor corespunzătoare). Aici folosim 7070% din setul de date pentru antrenament, restul de 3030% fiind rezervat pentru testare.

from sklearn.model_selection import train_test_split

np.random.seed(42)
images, labels = generate_dataset(200)

train_images, test_images, train_labels, test_labels = train_test_split(
images, labels, test_size=0.3, random_state=246
)

Să reprezentăm grafic câteva elemente din setul nostru de date pentru a vedea cum arată aceste linii:

import matplotlib.pyplot as plt

# Make subplot titles so we can identify categories
titles = []
for i in range(8):
title = "category: " + str(train_labels[i])
titles.append(title)

# Generate a figure with nested images using subplots.
fig, ax = plt.subplots(4, 2, figsize=(10, 6), subplot_kw={"xticks": [], "yticks": []})

for i in range(8):
ax[i // 2, i % 2].imshow(
train_images[i].reshape(vert_size, hor_size),
aspect="equal",
)
ax[i // 2, i % 2].set_title(titles[i])
plt.subplots_adjust(wspace=0.1, hspace=0.3)

Rezultatul celulei de cod anterioare

Fiecare dintre aceste imagini este încă asociată cu eticheta sa în train_labels sub formă simplă de listă:

print(train_labels[:8])
[1, 1, 1, 1, -1, 1, 1, 1]

Clasificator cuantic variațional: o primă încercare

Pasul 1 din Qiskit patterns: Maparea problemei pe un circuit cuantic

Scopul este de a găsi o funcție ff cu parametri θ\theta care mapează un vector de date / imagine x\vec{x} la categoria corectă: fθ(x)±1f_\theta(\vec{x}) \rightarrow \pm1. Aceasta se va realiza folosind un VQC cu puține straturi care pot fi identificate prin scopurile lor distincte:

fθ(x)=0U(x)W(θ)OW(θ)U(x)0f_\theta(\vec{x}) = \langle 0|U^{\dagger}(\vec{x})W^\dagger(\theta)OW(\theta)U(\vec{x})|0\rangle

Aici, U(x)U(\vec{x}) este circuitul de codificare, pentru care avem multe opțiuni, așa cum am văzut în lecțiile anterioare. W(θ)W(\theta) este un bloc de circuit variațional sau antrenabil, iar θ\theta este setul de parametri care urmează să fie antrenați. Acești parametri vor fi variați de algoritmi de optimizare clasici pentru a găsi setul de parametri care produce cea mai bună clasificare a imaginilor de către circuitul cuantic. Acest circuit variațional este uneori numit „ansatz". În cele din urmă, OO este un observabil care va fi estimat folosind primitiva Estimator. Nu există nicio constrângere care să impună ca straturile să vină în această ordine, sau chiar să fie complet separate. Se pot avea mai multe straturi variaționale și/sau de codificare în orice ordine motivată tehnic.

Începem prin alegerea unei mapări de caracteristici pentru a codifica datele. Vom folosi z_feature_map, deoarece păstrează adâncimile circuitelor scăzute în comparație cu alte mapări de caracteristici.

from qiskit.circuit.library import z_feature_map

# One qubit per data feature
num_qubits = len(train_images[0])

# Data encoding
# Note that qiskit orders parameters alphabetically. We assign the parameter prefix "a" to ensure our data encoding goes to the first part of the circuit, the feature mapping.
feature_map = z_feature_map(num_qubits, parameter_prefix="a")

Acum trebuie să decidem asupra unui ansatz care să fie antrenat. Există multe considerente atunci când se selectează un ansatz. O descriere completă depășește scopul acestei introduceri; aici indicăm pur și simplu câteva categorii de considerente.

  1. Hardware: Toate calculatoarele cuantice moderne sunt mai predispuse la erori și mai susceptibile la zgomot decât omologiile lor clasice. Utilizarea unui ansatz excesiv de adânc (în special în adâncimea transpilată, cu porți de doi qubiți) nu va produce rezultate bune. O problemă conexă este că calculatoarele cuantice au un anumit aranjament al qubiților, ceea ce înseamnă că unii qubiți fizici sunt adiacenți pe calculatorul cuantic, iar alții pot fi foarte departe unul de celălalt. Entanglarea qubiților adiacenți nu crește prea mult adâncimea, dar entanglarea qubiților foarte distanți poate crește substanțial adâncimea, deoarece trebuie să inserăm porți swap pentru a muta informația pe qubiți adiacenți, pentru a putea fi entanglați.
  2. Problema: Ori de câte ori ai informații despre problema ta care ar putea ghida ansatz-ul, folosește-le. De exemplu, datele din această lecție constau din imagini cu linii orizontale și verticale. S-ar putea considera ce corelație între culorile/valorile adiacente identifică o imagine cu o linie orizontală sau verticală. Ce atribute ale unui ansatz ar corespunde acestei corelații între pixelii adiacenți? Vom reveni la acest punct mai tehnic ulterior în această lecție. Dar deocamdată, să spunem pur și simplu că includerea entanglementului și a porților CNOT între qubiții corespunzători pixelilor adiacenți pare o idee bună. În ansamblu, ia în considerare dacă problema este într-adevăr rezolvată cel mai bine folosind un circuit cuantic, sau dacă ar putea exista algoritmi clasici care pot face la fel de bine.
  3. Numărul de parametri: Fiecare poartă cuantică parametrizată independent din circuit mărește spațiul care trebuie optimizat clasic, ceea ce duce la convergență mai lentă. Dar pe măsură ce problemele cresc în dimensiune, se poate întâlni fenomenul de platouri aride (barren plateaus). Acest termen se referă la un fenomen în care peisajul de optimizare al unui algoritm cuantic variațional devine exponențial plat și lipsit de trăsături pe măsură ce dimensiunea problemei crește. Aceasta cauzează gradienți care dispar, făcând dificilă antrenarea eficientă a algoritmului[1]. Platourile aride sunt relevante pentru algoritmii cuantici variaționali precum VQC-urile/QNN-urile. Trebuie remarcat că numărul crescând de parametri nu este singura considerentă în evitarea platourilor aride; alte considerente includ funcțiile de cost globale și inițializarea aleatoare a parametrilor.

În această lecție vom vedea câteva exemple simple de bune practici în construcția ansatz-ului. Să încercăm mai întâi ansatz-ul de mai jos. Ne vom întoarce să-l revizuim, ulterior.

# Import the necessary packages
from qiskit import QuantumCircuit
from qiskit.circuit import ParameterVector

# Initialize the circuit using the same number of qubits as the image has pixels
qnn_circuit = QuantumCircuit(size)

# We choose to have two variational parameters for each qubit.
params = ParameterVector("θ", length=2 * size)

# A first variational layer:
for i in range(size):
qnn_circuit.ry(params[i], i)

# Here is a list of qubit pairs between which we want CNOT gates. The choice of these is not yet obvious.
qnn_cnot_list = [[0, 1], [1, 2], [2, 3]]

for i in range(len(qnn_cnot_list)):
qnn_circuit.cx(qnn_cnot_list[i][0], qnn_cnot_list[i][1])

# The second variational layer:
for i in range(size):
qnn_circuit.rx(params[size + i], i)

# Check the circuit depth, and the two-qubit gate depth
print(qnn_circuit.decompose().depth())
print(
f"2+ qubit depth: {qnn_circuit.decompose().depth(lambda instr: len(instr.qubits) > 1)}"
)

# Draw the circuit
qnn_circuit.draw("mpl")
5
2+ qubit depth: 3
┌──────────┐             ┌──────────┐                         
q_0: ┤ Ry(θ[0]) ├──────■──────┤ Rx(θ[8]) ├─────────────────────────
├──────────┤ ┌─┴─┐ └──────────┘┌──────────┐
q_1: ┤ Ry(θ[1]) ├────┤ X ├─────────■──────┤ Rx(θ[9]) ├─────────────
├──────────┤ └───┘ ┌─┴─┐ └──────────┘┌───────────┐
q_2: ┤ Ry(θ[2]) ├────────────────┤ X ├─────────■──────┤ Rx(θ[10]) ├
├──────────┤ └───┘ ┌─┴─┐ ├───────────┤
q_3: ┤ Ry(θ[3]) ├────────────────────────────┤ X ├────┤ Rx(θ[11]) ├
├──────────┤┌───────────┐ └───┘ └───────────┘
q_4: ┤ Ry(θ[4]) ├┤ Rx(θ[12]) ├─────────────────────────────────────
├──────────┤├───────────┤
q_5: ┤ Ry(θ[5]) ├┤ Rx(θ[13]) ├─────────────────────────────────────
├──────────┤├───────────┤
q_6: ┤ Ry(θ[6]) ├┤ Rx(θ[14]) ├─────────────────────────────────────
├──────────┤├───────────┤
q_7: ┤ Ry(θ[7]) ├┤ Rx(θ[15]) ├─────────────────────────────────────
└──────────┘└───────────┘

Cu codificarea datelor și circuitul variațional pregătite, le putem combina pentru a forma ansatz-ul nostru complet. În acest caz, componentele circuitului nostru cuantic sunt destul de analoge cu cele din rețelele neuronale: U(x)U(\vec{x}) seamănă cel mai mult cu stratul care încarcă valorile de intrare din imagine, iar W(θ)W(\theta) este similar cu stratul de „ponderi" variabile. Deoarece această analogie se aplică în acest caz, adoptăm „qnn" în unele dintre convențiile noastre de denumire; însă această analogie nu ar trebui să îți limiteze explorarea VQC-urilor.

QML_CR_background_QNN_circuit-2.png

# QNN ansatz
ansatz = qnn_circuit

# Combine the feature map with the ansatz
full_circuit = QuantumCircuit(num_qubits)
full_circuit.compose(feature_map, range(num_qubits), inplace=True)
full_circuit.compose(ansatz, range(num_qubits), inplace=True)

# Display the circuit
full_circuit.decompose().draw("mpl", style="clifford", fold=-1)

Rezultatul celulei de cod anterioare

Trebuie acum să definim un observabil, pentru a-l putea folosi în funcția noastră de cost. Vom obține o valoare de așteptare pentru acest observabil folosind Estimator. Dacă am ales un ansatz bun, motivat de problemă, atunci fiecare qubit va conține informații relevante pentru clasificare. Se pot adăuga straturi pentru a combina informațiile pe mai puțini qubiți (numit strat convoluțional), astfel încât măsurătorile să fie necesare doar pe un subset al qubiților din circuit (ca în rețelele neuronale convoluționale). Sau se poate măsura un atribut de la fiecare qubit. Aici vom opta pentru aceasta din urmă, deci includem un operator Z pentru fiecare qubit. Nu există nimic special în alegerea ZZ, dar este bine motivată:

  • Aceasta este o sarcină de clasificare binară, iar o măsurătoare a ZZ poate produce două rezultate posibile.
  • Valorile proprii ale ZZ (±1\pm 1) sunt rezonabil de bine separate și produc un rezultat al estimatorului în intervalul [-1, +1], unde 0 poate fi folosit pur și simplu ca valoare de prag.
  • Este simplu să măsori în baza Pauli Z fără overhead suplimentar de porți.

Deci, Z este o alegere foarte naturală.

from qiskit.quantum_info import SparsePauliOp

observable = SparsePauliOp.from_list([("Z" * (num_qubits), 1)])

Avem circuitul cuantic și observabilul pe care vrem să îl estimăm. Acum avem nevoie de câteva lucruri pentru a rula și optimiza acest circuit. În primul rând, avem nevoie de o funcție pentru a rula o trecere înainte (forward pass). Observă că funcția de mai jos primește separat input_params și weight_params. Prima este setul de parametri statici care descriu datele dintr-o imagine, iar cea de-a doua este setul de parametri variabili care urmează să fie optimizați.

from qiskit.primitives import BaseEstimatorV2
from qiskit.quantum_info.operators.base_operator import BaseOperator

def forward(
circuit: QuantumCircuit,
input_params: np.ndarray,
weight_params: np.ndarray,
estimator: BaseEstimatorV2,
observable: BaseOperator,
) -> np.ndarray:
"""
Forward pass of the neural network.

Args:
circuit: circuit consisting of data loader gates and the neural network ansatz.
input_params: data encoding parameters.
weight_params: neural network ansatz parameters.
estimator: EstimatorV2 primitive.
observable: a single observable to compute the expectation over.

Returns:
expectation_values: an array (for one observable) or a matrix (for a sequence of observables) of expectation values.
Rows correspond to observables and columns to data samples.
"""
num_samples = input_params.shape[0]
weights = np.broadcast_to(weight_params, (num_samples, len(weight_params)))
params = np.concatenate((input_params, weights), axis=1)
pub = (circuit, observable, params)
job = estimator.run([pub])
result = job.result()[0]
expectation_values = result.data.evs

return expectation_values

Funcția de pierdere

În continuare, avem nevoie de o funcție de pierdere pentru a calcula diferența dintre valorile prezise și cele calculate ale etichetelor. Funcția va primi etichetele prezise de algoritm și etichetele corecte și va returna diferența medie pătratică. Există multe funcții de pierdere diferite. Aici, MSE este un exemplu pe care l-am ales.

def mse_loss(predict: np.ndarray, target: np.ndarray) -> np.ndarray:
"""
Mean squared error (MSE).

prediction: predictions from the forward pass of neural network.
target: true labels.

output: MSE loss.
"""
if len(predict.shape) <= 1:
return ((predict - target) ** 2).mean()
else:
raise AssertionError("input should be 1d-array")

Să definim și o funcție de pierdere ușor diferită, care este o funcție a parametrilor variabili (ponderi), pentru a fi utilizată de optimizatorul clasic. Această funcție primește ca intrare doar parametrii ansatz; celelalte variabile pentru trecerea înainte și pierdere sunt setate ca parametri globali. Optimizatorul va antrena modelul eșantionând diferite ponderi și încercând să reducă ieșirea funcției de cost/pierdere.

def mse_loss_weights(weight_params: np.ndarray) -> np.ndarray:
"""
Cost function for the optimizer to update the ansatz parameters.

weight_params: ansatz parameters to be updated by the optimizer.

output: MSE loss.
"""
predictions = forward(
circuit=circuit,
input_params=input_params,
weight_params=weight_params,
estimator=estimator,
observable=observable,
)

cost = mse_loss(predict=predictions, target=target)
objective_func_vals.append(cost)

global iter
if iter % 50 == 0:
print(f"Iter: {iter}, loss: {cost}")
iter += 1

return cost

Mai sus am menționat utilizarea unui optimizator clasic. Când ajungem la căutarea ponderilor pentru a minimiza funcția de cost, vom folosi optimizatorul COBYLA:

from scipy.optimize import minimize

Vom seta câteva variabile globale inițiale pentru funcția de cost.

# Globals
circuit = full_circuit
observables = observable
# input_params = train_images_batch
# target = train_labels_batch
objective_func_vals = []
iter = 0

Pasul 2 din Qiskit Patterns: Optimizarea problemei pentru execuție cuantică

Începem prin selectarea unui backend pentru execuție. În acest caz, vom folosi backend-ul cel mai puțin ocupat.

from qiskit_ibm_runtime import QiskitRuntimeService

service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)
print(backend.name)
ibm_brisbane

Aici optimizăm circuitul pentru rularea pe un backend real, specificând optimization_level și adăugând decuplare dinamică. Codul de mai jos generează un manager de pasuri folosind manageri de pasuri predefiniți din qiskit.transpiler.

from qiskit.circuit.library import XGate
from qiskit.transpiler import PassManager
from qiskit.transpiler.passes import (
ALAPScheduleAnalysis,
ConstrainedReschedule,
PadDynamicalDecoupling,
)
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

target = backend.target

pm = generate_preset_pass_manager(target=target, optimization_level=3)
pm.scheduling = PassManager(
[
ALAPScheduleAnalysis(target=target),
ConstrainedReschedule(
acquire_alignment=target.acquire_alignment,
pulse_alignment=target.pulse_alignment,
target=target,
),
PadDynamicalDecoupling(
target=target,
dd_sequence=[XGate(), XGate()],
pulse_alignment=target.pulse_alignment,
),
]
)

Acum folosim managerul de pasuri pe circuit. Modificările de layout rezultate trebuie aplicate și observabilului. Pentru circuite foarte mari, euristicile utilizate în optimizarea circuitului s-ar putea să nu producă întotdeauna cel mai bun și mai puțin adânc circuit. În acele cazuri, are sens să rulezi astfel de manageri de pasuri de mai multe ori și să folosești cel mai bun circuit. Vom vedea acest lucru mai târziu când scalăm calculul nostru.

circuit_ibm = pm.run(full_circuit)
observable_ibm = observable.apply_layout(circuit_ibm.layout)

Pasul 3 din Qiskit Patterns: Execuție folosind Qiskit Primitives

Iterare peste setul de date în loturi și epoci

Mai întâi implementăm algoritmul complet folosind un simulator pentru depanare rapidă și pentru estimări ale erorii. Putem acum parcurge întregul set de date în loturi, pe numărul dorit de epoci, pentru a antrena rețeaua noastră neuronală cuantică.

from qiskit.primitives import StatevectorEstimator as Estimator

batch_size = 140
num_epochs = 1
num_samples = len(train_images)

# Globals
circuit = full_circuit
estimator = Estimator() # simulator for debugging
observables = observable
objective_func_vals = []
iter = 0

# Random initial weights for the ansatz
np.random.seed(42)
weight_params = np.random.rand(len(ansatz.parameters)) * 2 * np.pi

for epoch in range(num_epochs):
for i in range((num_samples - 1) // batch_size + 1):
print(f"Epoch: {epoch}, batch: {i}")
start_i = i * batch_size
end_i = start_i + batch_size
train_images_batch = np.array(train_images[start_i:end_i])
train_labels_batch = np.array(train_labels[start_i:end_i])
input_params = train_images_batch
target = train_labels_batch
iter = 0
res = minimize(
mse_loss_weights, weight_params, method="COBYLA", options={"maxiter": 100}
)
weight_params = res["x"]
Epoch: 0, batch: 0
Iter: 0, loss: 1.0002309063537163
Iter: 50, loss: 0.9434121445008878

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

Testare și acuratețe

Acum interpretăm rezultatele din antrenament. Mai întâi testăm acuratețea pe setul de antrenament.

import copy
from sklearn.metrics import accuracy_score
from qiskit.primitives import StatevectorEstimator as Estimator # simulator
# from qiskit_ibm_runtime import EstimatorV2 as Estimator # real quantum computer

estimator = Estimator()
# estimator = Estimator(backend=backend)

pred_train = forward(circuit, np.array(train_images), res["x"], estimator, observable)
# pred_train = forward(circuit_ibm, np.array(train_images), res['x'], estimator, observable_ibm)

print(pred_train)

pred_train_labels = copy.deepcopy(pred_train)
pred_train_labels[pred_train_labels >= 0] = 1
pred_train_labels[pred_train_labels < 0] = -1
print(pred_train_labels)
print(train_labels)

accuracy = accuracy_score(train_labels, pred_train_labels)
print(f"Train accuracy: {accuracy * 100}%")
[-2.27688499e-02 -1.46227204e-02 -1.73927452e-02  9.93331786e-02
-4.85553548e-01 1.43558565e-01 8.34567054e-02 -1.40133992e-02
1.52169596e-01 -1.95082515e-01 8.24373578e-03 -9.90696638e-02
-3.54268344e-02 -4.77017954e-01 1.38713848e-02 -2.99706215e-01
-5.78378029e-02 3.25528779e-02 -4.11354239e-02 -1.06483708e-01
1.53095800e-01 2.90110884e-02 1.25745450e-02 6.46323079e-02
-1.53538943e-01 -1.57694952e-02 -1.67800067e-02 -1.99820822e-01
1.70360075e-01 7.86148038e-03 -2.33373818e-02 6.64233020e-02
-1.14895445e-01 -1.11296215e-01 1.15120303e-01 -2.94096140e-01
-1.00531392e-03 -1.69209726e-01 -1.26120885e-01 3.26298176e-02
-1.33517383e-02 -5.86983444e-02 -4.32341361e-01 -4.36509551e-01
-4.17940102e-02 1.76935235e-03 8.14479984e-03 1.86985655e-01
-2.75525019e-01 -1.63229907e-03 -1.08571055e-01 -7.37452387e-04
-6.44440657e-02 6.72812834e-04 2.16785530e-03 1.41381850e-01
-9.82570410e-02 4.35973325e-01 -7.62261965e-02 -1.86193980e-01
-1.56971183e-02 -4.02757541e-01 -1.53869367e-01 2.29262129e-02
-7.02788246e-03 3.65719683e-02 4.68232163e-01 2.36434668e-02
-2.59520939e-02 3.70550137e-01 -1.19630110e-01 -5.79555318e-02
2.09554455e-01 5.04689780e-02 7.39494314e-02 -1.77647326e-02
-1.45407207e-01 -9.54908878e-02 7.56029640e-02 -2.74049696e-02
3.34885873e-01 1.58546171e-03 1.09339091e-01 -8.84693274e-02
-2.36450457e-02 1.41892239e-01 -2.34453218e-01 -7.50717757e-02
-1.13281310e-01 -1.66649414e-01 -3.17224197e-01 -6.38220597e-02
3.28916563e-02 3.04739203e-02 2.67720196e-02 -1.16485785e-01
-3.08115732e-02 -2.95372010e-02 -7.54669023e-02 6.20013872e-02
-3.85258710e-01 -1.16456443e-01 -7.38548075e-02 -3.20558243e-02
-4.22284741e-02 1.01285659e-01 -1.76949246e-01 -2.02767491e-01
-1.12407344e-01 -3.81408267e-02 -4.33345231e-01 -9.24507501e-02
-4.21765393e-02 -6.06533771e-02 -2.22257783e-01 -1.17312535e-01
-6.74132262e-02 -2.76206274e-01 -9.13971800e-02 -2.27653991e-01
1.66358563e-01 2.17230774e-04 5.76426304e-02 -2.82079169e-02
-1.15482051e-01 -3.46716009e-01 -3.21448755e-01 -5.20041405e-02
-2.16833625e-01 -1.06154654e-02 -7.74854811e-02 -3.28257935e-01
-7.83242410e-02 1.65547682e-01 -2.55294862e-01 -8.89085025e-02
4.47581491e-01 1.92351832e-02 2.74083885e-02 -3.61304571e-01]
[-1. -1. -1. 1. -1. 1. 1. -1. 1. -1. 1. -1. -1. -1. 1. -1. -1. 1.
-1. -1. 1. 1. 1. 1. -1. -1. -1. -1. 1. 1. -1. 1. -1. -1. 1. -1.
-1. -1. -1. 1. -1. -1. -1. -1. -1. 1. 1. 1. -1. -1. -1. -1. -1. 1.
1. 1. -1. 1. -1. -1. -1. -1. -1. 1. -1. 1. 1. 1. -1. 1. -1. -1.
1. 1. 1. -1. -1. -1. 1. -1. 1. 1. 1. -1. -1. 1. -1. -1. -1. -1.
-1. -1. 1. 1. 1. -1. -1. -1. -1. 1. -1. -1. -1. -1. -1. 1. -1. -1.
-1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. 1. 1. 1. -1. -1. -1.
-1. -1. -1. -1. -1. -1. -1. 1. -1. -1. 1. 1. 1. -1.]
[1, 1, 1, 1, -1, 1, 1, 1, -1, 1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1, 1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1, 1, -1, 1, 1, -1, 1, 1, -1, -1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, -1, 1, 1, -1, 1, 1, 1, 1, -1, -1, -1, -1, 1, -1, 1, 1, 1, -1, -1, 1, 1, -1, 1, 1, -1, -1, 1, -1, 1, 1, 1, 1, 1, -1, -1, 1, -1, -1, -1, -1, -1, 1, 1, -1, -1, 1, -1, -1, 1, 1, -1, 1, -1, 1, 1, 1, 1, 1, -1, -1, -1, 1, -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, 1, 1, -1, -1, -1, 1, 1, 1, -1, -1, -1, 1, -1, 1, 1, -1, -1, -1]
Train accuracy: 60.0%

Acuratețea de antrenament este doar 6060%, ceea ce nu este deloc bun. E greu de imaginat că performanța modelului pe setul de test ar putea fi mai bună. Să verificăm.

pred_test = forward(circuit, np.array(test_images), res["x"], estimator, observable)
# pred_test = forward(circuit_ibm, np.array(test_images), res['x'], estimator, observable_ibm)

print(pred_test)

pred_test_labels = copy.deepcopy(pred_test)
pred_test_labels[pred_test_labels >= 0] = 1
pred_test_labels[pred_test_labels < 0] = -1
print(pred_test_labels)
print(test_labels)

accuracy = accuracy_score(test_labels, pred_test_labels)
print(f"Test accuracy: {accuracy * 100}%")
[-2.77978120e-01 -2.62194862e-01  4.59636095e-02 -8.09344165e-02
-2.97362966e-01 9.22947242e-02 2.06693174e-01 3.31629460e-02
1.10971762e-03 -2.14602152e-01 -1.62671993e-01 -6.07179155e-04
-1.59948633e-01 -8.55722523e-02 -1.13057027e-01 -3.00187433e-01
-2.92832827e-01 7.38580629e-02 -6.03706270e-02 -8.57643552e-02
-1.52402062e-02 -3.57505447e-01 -3.54890597e-02 1.36534749e-01
-1.54688180e-01 -2.93714726e-01 1.89548513e-02 -6.15715564e-02
1.11042670e-01 -2.22861100e-02 -3.84230105e-02 1.67351034e-01
-8.38766333e-02 2.56348613e-01 -1.10653111e-01 -1.18989476e-01
-6.75723266e-05 -6.88580547e-02 1.02431393e-02 -2.42125353e-01
-1.09142367e-01 -1.22540757e-01 -1.63735850e-01 3.93334838e-01
2.36705685e-01 -2.34259814e-02 -3.91877756e-02 -1.95106746e-01
1.86707523e-01 4.74775215e-02 -4.24907432e-02 -2.06453265e-01
4.09184710e-02 -3.54762080e-02 -9.47513112e-02 2.97270112e-01
-2.99708696e-02 9.93941064e-03 -1.26760302e-01 -1.36183355e-01]
[-1. -1. 1. -1. -1. 1. 1. 1. 1. -1. -1. -1. -1. -1. -1. -1. -1. 1.
-1. -1. -1. -1. -1. 1. -1. -1. 1. -1. 1. -1. -1. 1. -1. 1. -1. -1.
-1. -1. 1. -1. -1. -1. -1. 1. 1. -1. -1. -1. 1. 1. -1. -1. 1. -1.
-1. 1. -1. 1. -1. -1.]
[-1, -1, 1, 1, -1, -1, 1, -1, 1, -1, 1, 1, 1, 1, -1, 1, -1, 1, 1, 1, 1, -1, -1, 1, 1, -1, 1, 1, 1, -1, 1, -1, -1, 1, 1, 1, -1, 1, 1, -1, -1, -1, 1, 1, 1, -1, 1, 1, 1, 1, -1, -1, 1, 1, 1, 1, 1, 1, -1, -1]
Test accuracy: 60.0%

Modelul nu clasifică aceste date bine. Ar trebui să ne întrebăm de ce se întâmplă asta și, în special, să verificăm:

  • Am oprit antrenarea prea devreme? Au fost necesari mai mulți pași de optimizare?
  • Am construit un ansatz greșit? Asta poate însemna multe lucruri. Când lucrăm pe calculatoare cuantice reale, adâncimea circuitului va fi o considerație majoră. Numărul de parametri este de asemenea potențial important, la fel și împletirea dintre qubiți.
  • Combinând cele două de mai sus, am construit un ansatz cu prea mulți parametri pentru a putea fi antrenat?

Putem începe prin a verifica convergența în optimizare:

obj_func_vals_first = objective_func_vals
# import matplotlib.pyplot as plt

plt.figure(figsize=(12, 6))
plt.plot(obj_func_vals_first, label="first ansatz")
plt.xlabel("iteration")
plt.ylabel("loss")
plt.legend()
plt.show()

Rezultatul celulei de cod anterioare

Am putea încerca să extindem pașii de optimizare pentru a ne asigura că optimizatorul nu s-a blocat doar într-un minim local în spațiul parametrilor. Dar arată destul de conversat. Să aruncăm o privire mai atentă la imaginile care nu au fost clasificate corect și să vedem dacă putem înțelege ce se întâmplă.

missed = []
for i in range(len(test_labels)):
if pred_test_labels[i] != test_labels[i]:
missed.append(test_images[i])
print(len(missed))
24
fig, ax = plt.subplots(12, 2, figsize=(6, 6), subplot_kw={"xticks": [], "yticks": []})
for i in range(len(missed)):
ax[i // 2, i % 2].imshow(
missed[i].reshape(vert_size, hor_size),
aspect="equal",
)
plt.subplots_adjust(wspace=0.02, hspace=0.025)

Rezultatul celulei de cod anterioare

Putem vedea că marea majoritate a imaginilor clasificate greșit au o linie verticală. Ceva din modelul nostru nu reușește să capteze informații despre acestea. Poate ai anticipat asta, pe baza primului circuit variațional. Să-l analizăm mai îndeaproape.

Îmbunătățirea modelului

Pasul 1 revizuit

Când am mapat problema noastră pe un circuit cuantic, ar fi trebuit să ne gândim explicit la modul în care informațiile din pixelii adiacenți determină clasa. Pentru a identifica linii orizontale, vrem să știm „dacă pixelul ii este galben, este pixelul i+1i+1 galben" pentru toți pixelii de pe fiecare rând. Vrem de asemenea să știm despre liniile verticale. Dar deoarece clasificarea este binară, s-ar putea imagina că dacă o astfel de linie orizontală nu este detectată, atunci este o linie verticală. Circuitul nostru variațional anterior conținea porți CNOT între qubiți (și, prin urmare, pixeli) 0 și 1, 1 și 2, și 2 și 3. Aceasta acoperă orice linii orizontale de-a lungul părții de sus a imaginii, dar nu detectează direct linii verticale și nici nu detectează complet linii orizontale, deoarece ignoră rândul de jos. Pentru a detecta complet toate liniile orizontale, am dori să avem un set similar de porți CNOT între qubiți (pixeli) 4 și 5, 5 și 6, și 6 și 7. Am putea ține cont că adăugarea de porți CNOT între qubiți corespunzători liniilor verticale (cum ar fi 0 și 4, sau 2 și 6) poate fi de asemenea utilă. Dar vom verifica mai întâi dacă este suficient să detectăm că există sau nu o linie orizontală.

# Initialize the circuit using the same number of qubits as the image has pixels
qnn_circuit = QuantumCircuit(size)

# We choose to have two variational parameters for each qubit.
params = ParameterVector("θ", length=2 * size)

# A first variational layer:
for i in range(size):
qnn_circuit.ry(params[i], i)

# Here is an extended list of qubit pairs between which we want CNOT gates. This now covers all pixels connected by horizontal lines.
qnn_cnot_list = [[0, 1], [1, 2], [2, 3], [4, 5], [5, 6], [6, 7]]

for i in range(len(qnn_cnot_list)):
qnn_circuit.cx(qnn_cnot_list[i][0], qnn_cnot_list[i][1])

# The second variational layer:
for i in range(size):
qnn_circuit.rx(params[size + i], i)

# Check the circuit depth, and the two-qubit gate depth
print(qnn_circuit.decompose().depth())
print(
f"2+ qubit depth: {qnn_circuit.decompose().depth(lambda instr: len(instr.qubits) > 1)}"
)

# Combine the feature map and variational circuit
ansatz = qnn_circuit

# Combine the feature map with the ansatz
full_circuit = QuantumCircuit(num_qubits)
full_circuit.compose(feature_map, range(num_qubits), inplace=True)
full_circuit.compose(ansatz, range(num_qubits), inplace=True)

# Display the circuit
full_circuit.decompose().draw("mpl", style="clifford", fold=-1)
5
2+ qubit depth: 3

Rezultatul celulei de cod anterioare

Nu am crescut adâncimea circuitului. Să vedem dacă i-am crescut capacitatea de a modela imaginile noastre.

Pasul 2 revizuit

Va trebui să transpilăm acest nou circuit pentru a-l rula pe un backend cuantic real. Să sărim peste acest pas deocamdată pentru a vedea dacă revizuirea circuitului variațional a avut efectul dorit pe simulatoare. Vom aprofunda transpilarea în subsecțiunea următoare.

Pasul 3 revizuit

Acum aplicăm modelul actualizat datelor noastre de antrenare.

from qiskit.primitives import StatevectorEstimator as Estimator

batch_size = 140
num_epochs = 1
num_samples = len(train_images)

# Globals
circuit = full_circuit
estimator = Estimator() # simulator for debugging
observables = observable
objective_func_vals = []
iter = 0

# Random initial weights for the ansatz
np.random.seed(42)
weight_params = np.random.rand(len(ansatz.parameters)) * 2 * np.pi

for epoch in range(num_epochs):
for i in range((num_samples - 1) // batch_size + 1):
print(f"Epoch: {epoch}, batch: {i}")
start_i = i * batch_size
end_i = start_i + batch_size
train_images_batch = np.array(train_images[start_i:end_i])
train_labels_batch = np.array(train_labels[start_i:end_i])
input_params = train_images_batch
target = train_labels_batch
iter = 0
res = minimize(
mse_loss_weights, weight_params, method="COBYLA", options={"maxiter": 100}
)
weight_params = res["x"]
Epoch: 0, batch: 0
Iter: 0, loss: 1.0049762969140237
Iter: 50, loss: 0.8274276543780351

Pasul 4 revizuit

Să începem prin a verifica dacă optimizatorul nostru a convergit complet.

obj_func_vals_revised = objective_func_vals
# import matplotlib.pyplot as plt

plt.figure(figsize=(12, 6))
plt.plot(obj_func_vals_revised, label="revised ansatz")
plt.xlabel("iteration")
plt.ylabel("loss")
plt.legend()
plt.show()

Rezultatul celulei de cod anterioare

Aceasta nu pare să fi convergit complet, deoarece funcția de pierdere nu a rămas aproximativ constantă pentru un număr semnificativ de pași. Totuși, funcția de pierdere este deja cu ~60% mai mică față de utilizarea circuitului variațional anterior. Dacă acesta ar fi un proiect de cercetare, am vrea să asigurăm convergența completă. Dar în scopul explorării, acest lucru este suficient. Să verificăm acuratețea pe datele noastre de antrenament și de testare.

from sklearn.metrics import accuracy_score
from qiskit.primitives import StatevectorEstimator as Estimator # simulator
# from qiskit_ibm_runtime import EstimatorV2 as Estimator # real quantum computer

estimator = Estimator()
# estimator = Estimator(backend=backend)

pred_train = forward(circuit, np.array(train_images), res["x"], estimator, observable)
# pred_train = forward(circuit_ibm, np.array(train_images), res['x'], estimator, observable_ibm)

print(pred_train)

pred_train_labels = copy.deepcopy(pred_train)
pred_train_labels[pred_train_labels >= 0] = 1
pred_train_labels[pred_train_labels < 0] = -1
print(pred_train_labels)
print(train_labels)

accuracy = accuracy_score(train_labels, pred_train_labels)
print(f"Train accuracy: {accuracy * 100}%")
[ 0.46144755  0.42579688  0.35255977  0.55207273 -0.48578418  0.50805845
0.44892649 0.6173847 -0.62428139 0.40405121 0.46862421 0.29503395
-0.5740469 -0.71794562 -0.45022095 -0.45330418 -0.19795258 -0.46821777
-0.5622049 -0.32114059 0.54947838 -0.4889812 0.28327445 0.58149728
-0.27026749 0.41328304 0.21119412 0.60108606 0.39204178 -0.24974605
0.38496469 0.39867586 -0.38946996 0.62616766 0.61212525 -0.49719567
0.30860002 0.68443904 -0.27505907 -0.41508947 -0.49666422 0.67716994
-0.54696613 -0.70058779 0.42711815 -0.5285338 0.37678572 0.43888249
-0.30844464 0.42347715 -0.4250844 0.67324132 0.59914067 -0.45184567
0.13604098 0.65336342 0.26099853 0.60316559 -0.38743183 -0.54784284
-0.29549031 -0.45592302 0.41613453 -0.38781528 0.56903087 0.54955451
0.55532336 -0.3931852 -0.57599675 0.61246236 0.42014135 -0.38171749
0.56760389 0.45383135 -0.50473943 -0.47551181 0.54221517 -0.64987023
0.28845851 0.54403865 0.53841148 0.64477078 0.71912049 -0.63178323
-0.50764757 0.50304637 -0.38099972 -0.27707127 -0.24353841 -0.52045267
-0.61500665 0.65443173 0.31902266 -0.64969037 -0.4814051 0.47980608
-0.649786 -0.43048551 0.34562588 0.308998 -0.32454238 0.29558168
-0.45410187 0.54600712 0.33204827 0.22627804 0.4283921 0.56191874
-0.25400294 -0.6493613 -0.47445293 0.42272138 -0.35472546 -0.52240474
-0.45207595 0.40292125 -0.3361856 -0.46620886 0.60202719 -0.56505744
0.47169796 -0.43577622 0.40689437 0.48869108 -0.39701189 -0.57698634
-0.39236332 0.31294648 0.41797597 0.63004836 -0.52884541 -0.43805812
-0.3193499 0.36860211 -0.49190995 0.65000193 0.50260077 -0.56737168
-0.29693083 -0.40956432]
[ 1. 1. 1. 1. -1. 1. 1. 1. -1. 1. 1. 1. -1. -1. -1. -1. -1. -1.
-1. -1. 1. -1. 1. 1. -1. 1. 1. 1. 1. -1. 1. 1. -1. 1. 1. -1.
1. 1. -1. -1. -1. 1. -1. -1. 1. -1. 1. 1. -1. 1. -1. 1. 1. -1.
1. 1. 1. 1. -1. -1. -1. -1. 1. -1. 1. 1. 1. -1. -1. 1. 1. -1.
1. 1. -1. -1. 1. -1. 1. 1. 1. 1. 1. -1. -1. 1. -1. -1. -1. -1.
-1. 1. 1. -1. -1. 1. -1. -1. 1. 1. -1. 1. -1. 1. 1. 1. 1. 1.
-1. -1. -1. 1. -1. -1. -1. 1. -1. -1. 1. -1. 1. -1. 1. 1. -1. -1.
-1. 1. 1. 1. -1. -1. -1. 1. -1. 1. 1. -1. -1. -1.]
[1, 1, 1, 1, -1, 1, 1, 1, -1, 1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1, 1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1, 1, -1, 1, 1, -1, 1, 1, -1, -1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, -1, 1, 1, -1, 1, 1, 1, 1, -1, -1, -1, -1, 1, -1, 1, 1, 1, -1, -1, 1, 1, -1, 1, 1, -1, -1, 1, -1, 1, 1, 1, 1, 1, -1, -1, 1, -1, -1, -1, -1, -1, 1, 1, -1, -1, 1, -1, -1, 1, 1, -1, 1, -1, 1, 1, 1, 1, 1, -1, -1, -1, 1, -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, 1, 1, -1, -1, -1, 1, 1, 1, -1, -1, -1, 1, -1, 1, 1, -1, -1, -1]
Train accuracy: 100.0%
pred_test = forward(circuit, np.array(test_images), res["x"], estimator, observable)
# pred_test = forward(circuit_ibm, np.array(test_images), res['x'], estimator, observable_ibm)

print(pred_test)

pred_test_labels = copy.deepcopy(pred_test)
pred_test_labels[pred_test_labels >= 0] = 1
pred_test_labels[pred_test_labels < 0] = -1
print(pred_test_labels)
print(test_labels)

accuracy = accuracy_score(test_labels, pred_test_labels)
print(f"Test accuracy: {accuracy * 100}%")
[-0.48396136 -0.57123828  0.28373249  0.38983869 -0.45799092 -0.63643031
0.69164877 -0.47749808 0.16965244 -0.39669469 0.39366915 0.44206948
0.69733951 0.40445979 -0.33663432 0.54511581 -0.49397081 0.55934553
0.69269512 0.38875983 0.39724004 -0.49635863 -0.19131387 0.38813936
0.39537369 -0.46262489 0.5307315 0.21783317 0.31949453 -0.49772087
0.56409526 -0.66254365 -0.57507262 0.37363552 0.35154205 0.69295687
-0.31205475 0.37787066 0.67903997 -0.29984861 -0.46435535 -0.32610974
0.4327188 0.64626537 0.37592731 -0.14328906 0.59694745 0.71880638
0.32414334 0.42119333 -0.60745236 -0.42520033 0.28334222 0.21699081
0.34837252 0.31538989 0.30754545 0.5995197 -0.34678026 -0.46587602]
[-1. -1. 1. 1. -1. -1. 1. -1. 1. -1. 1. 1. 1. 1. -1. 1. -1. 1.
1. 1. 1. -1. -1. 1. 1. -1. 1. 1. 1. -1. 1. -1. -1. 1. 1. 1.
-1. 1. 1. -1. -1. -1. 1. 1. 1. -1. 1. 1. 1. 1. -1. -1. 1. 1.
1. 1. 1. 1. -1. -1.]
[-1, -1, 1, 1, -1, -1, 1, -1, 1, -1, 1, 1, 1, 1, -1, 1, -1, 1, 1, 1, 1, -1, -1, 1, 1, -1, 1, 1, 1, -1, 1, -1, -1, 1, 1, 1, -1, 1, 1, -1, -1, -1, 1, 1, 1, -1, 1, 1, 1, 1, -1, -1, 1, 1, 1, 1, 1, 1, -1, -1]
Test accuracy: 100.0%
```bash

$100\%$ acuratețe pe ambele seturi! Suspiciunea noastră că detectarea precisă a liniilor orizontale ar fi suficientă s-a dovedit corectă! Mai mult, maparea de la informațiile necesare despre pixeli la porțile CNOT din circuitul cuantic a fost eficientă. Să vedem acum cum se scalează acest proces pentru rularea pe calculatoare cuantice reale.

## Scalare și rulare pe calculatoare cuantice reale \{#scaling-and-running-on-real-quantum-computers}

### Date \{#data}

Să începem prin a mări dimensiunea imaginilor noastre. Nu există nimic special în alegerea unei grile 6x6, cu excepția faptului că depășește numărul de qubiți (32) pe care îi putem simula pentru circuite care folosesc porți non-Clifford.

```python
# This code defines the images to be classified:

import numpy as np

# Total number of "pixels"/qubits
size = 36
# One dimension of the image (called vertical, but it doesn't matter). Must be a divisor of `size`
vert_size = 6
# The length of the line to be detected (yellow). Must be less than or equal to the smallest dimension of the image (`<=min(vert_size,size/vert_size)`
line_size = 6

def generate_dataset(num_images):
images = []
labels = []
hor_array = np.zeros((size - (line_size - 1) * vert_size, size))
ver_array = np.zeros((round(size / vert_size) * (vert_size - line_size + 1), size))

j = 0
for i in range(0, size - 1):
if i % (size / vert_size) <= (size / vert_size) - line_size:
for p in range(0, line_size):
hor_array[j][i + p] = np.pi / 2
j += 1

# Make two adjacent entries pi/2, then move down to the next row. Careful to avoid the "pixels" at size/vert_size - linesize, because we want to fold this list into a grid.

j = 0
for i in range(0, round(size / vert_size) * (vert_size - line_size + 1)):
for p in range(0, line_size):
ver_array[j][i + p * round(size / vert_size)] = np.pi / 2
j += 1

# Make entries pi/2, spaced by the length/rows, so that when folded, the entries appear on top of each other.

for n in range(num_images):
rng = np.random.randint(0, 2)
if rng == 0:
labels.append(-1)
random_image = np.random.randint(0, len(hor_array))
images.append(np.array(hor_array[random_image]))
# Randomly select one of the several rows you made above.
elif rng == 1:
labels.append(1)
random_image = np.random.randint(0, len(ver_array))
images.append(np.array(ver_array[random_image]))
# Randomly select one of the several rows you made above.

# Create noise
for i in range(size):
if images[-1][i] == 0:
images[-1][i] = np.random.rand() * np.pi / 4
return images, labels

hor_size = round(size / vert_size)

Deoarece timpul de calcul cuantic este o resursă prețioasă, vom folosi un set de antrenament foarte mic și foarte puțini pași de optimizare. Acest lucru va fi suficient pentru a demonstra fluxul de lucru.

from sklearn.model_selection import train_test_split

np.random.seed(42)
# Here we specify a very small data set. Increase for realism, but monitor use of quantum computing time.
images, labels = generate_dataset(10)

train_images, test_images, train_labels, test_labels = train_test_split(
images, labels, test_size=0.3, random_state=246
)
import matplotlib.pyplot as plt

# Generate a figure with nested images using subplots.

fig, ax = plt.subplots(2, 2, figsize=(10, 6), subplot_kw={"xticks": [], "yticks": []})
for i in range(4):
ax[i // 2, i % 2].imshow(
train_images[i].reshape(vert_size, hor_size),
aspect="equal",
)
plt.subplots_adjust(wspace=0.1, hspace=0.025)

Rezultatul celulei de cod anterioare

Pasul 1: Mapează problema pe un Circuit cuantic

from qiskit.circuit.library import z_feature_map

# One qubit per data feature
num_qubits = len(train_images[0])

# Data encoding
# Note that qiskit orders parameters alphabetically. We assign the parameter prefix "a" to ensure our data encoding goes to the first part of the circuit, the feature mapping.
feature_map = z_feature_map(num_qubits, parameter_prefix="a")
# This creates a circuit with the cxs in the compressed order.

from qiskit import QuantumCircuit
from qiskit.circuit import ParameterVector

qnn_circuit = QuantumCircuit(size)
params = ParameterVector("θ", length=2 * size)
for i in range(size):
qnn_circuit.ry(params[i], i)

# CNOT gates between horizontally adjacent qubits.
for i in range(vert_size):
for j in range(hor_size):
if j < hor_size - 1:
qnn_circuit.cx((i * hor_size) + j, (i * hor_size) + j + 1)

# CNOT gates between vertically adjacent qubits, likely not necessary based on our preliminary simulation.
# if i<vert_size-1:
# qnn_circuit.cx((i*hor_size)+j,(i*hor_size)+j+hor_size)
for i in range(size):
qnn_circuit.rx(params[size + i], i)
qnn_circuit_large = qnn_circuit

print(qnn_circuit_large.decompose().depth())
print(
f"2+ qubit depth: {qnn_circuit_large.decompose().depth(lambda instr: len(instr.qubits) > 1)}"
)
# qnn_circuit_large.draw()
7
2+ qubit depth: 5

Aceasta este o adâncime rezonabilă la doi qubiți. Ar trebui să putem obține rezultate de înaltă calitate de pe un calculator cuantic real.

# Combine the feature map and variational circuit
ansatz = qnn_circuit

# Combine the feature map with the ansatz
full_circuit = QuantumCircuit(num_qubits)
full_circuit.compose(feature_map, range(num_qubits), inplace=True)
full_circuit.compose(ansatz, range(num_qubits), inplace=True)

# Check the depth of the full circuit
print(full_circuit.decompose().depth())
print(
f"2+ qubit depth: {full_circuit.decompose().depth(lambda instr: len(instr.qubits) > 1)}"
)
11
2+ qubit depth: 5

Deoarece folosim z_feature_map, care nu are gate-uri CNOT, adăugarea stratului de codificare nu crește adâncimea noastră la doi qubiți. Putem vizualiza circuitul complet aici.

full_circuit.decompose().draw("mpl", style="clifford", idle_wires=False, fold=-1)

Rezultatul celulei de cod anterioare

Poate observi că, dacă minimizarea adâncimii la doi qubiți ar fi de o importanță capitală, am putea-o reduce puțin schimbând ordinea CNOT-urilor. De exemplu, CNOT-urile de pe q35q_{35} și q34q_{34} ar putea fi mutate la stânga în diagrama de Circuit de mai sus și ar putea fi plasate direct sub CNOT-urile de pe q30q_{30} și q31q_{31}, de pildă. Pentru o adâncime de gate cu doi qubiți de 5, nu este evident că acest lucru va face o diferență după transpilare, dar este ceva de ținut minte. Dacă ordinea gate-urilor CNOT este importantă pentru a se potrivi logic cu problema în cauză, adâncimea de aici este bună. Dacă ordinea CNOT-urilor nu este critică pentru modelarea structurii datelor din imaginile noastre, atunci am putea scrie un script pentru a reordona aceste gate-uri CNOT și a minimiza adâncimea.

De asemenea, trebuie să redefinim observabilul nostru pentru imaginile mai mari:

from qiskit.quantum_info import SparsePauliOp

observable = SparsePauliOp.from_list([("Z" * (num_qubits), 1)])

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

Începem prin selectarea unui Backend pentru execuție. În acest caz, vom folosi Backend-ul cel mai puțin ocupat.

from qiskit_ibm_runtime import QiskitRuntimeService

# To run on hardware, select the least busy quantum computer or specify a particular one.
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)
# backend = service.backend("ibm_brisbaneane")

print(backend.name)
ibm_brisbane

Din nou, definim un pass manager, cu nivelul de optimizare setat la 3.

from qiskit.circuit.library import XGate
from qiskit.transpiler import PassManager
from qiskit.transpiler.passes import (
ALAPScheduleAnalysis,
ConstrainedReschedule,
PadDynamicalDecoupling,
)
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

target = backend.target

pm = generate_preset_pass_manager(target=target, optimization_level=3)
pm.scheduling = PassManager(
[
ALAPScheduleAnalysis(target=target),
ConstrainedReschedule(
acquire_alignment=target.acquire_alignment,
pulse_alignment=target.pulse_alignment,
target=target,
),
PadDynamicalDecoupling(
target=target,
dd_sequence=[XGate(), XGate()],
pulse_alignment=target.pulse_alignment,
),
]
)

Acum vom aplica pass manager-ul de mai multe ori. Pentru circuite foarte largi sau foarte adânci, poate exista o variabilitate mare a adâncimilor cu doi qubiți după transpilare. Pentru astfel de circuite, este important să încerci pass manager-ul de mai multe ori și să folosești cel mai bun rezultat (cel mai puțin adânc).

# Try pass manager several times, since heuristics can return various transpilations on large circuits, and we want the shallowest.

transpiled_qcs = []
transpiled_depths = []
transpiled_2q_depths = []
for i in range(1, 10):
circuit_ibm = pm.run(full_circuit)
transpiled_qcs.append(circuit_ibm)
transpiled_depths.append(circuit_ibm.decompose().depth())
transpiled_2q_depths.append(
circuit_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)
)
# print(i)

print(transpiled_depths)
print(transpiled_2q_depths)

# Use the shallowest

minpos = transpiled_2q_depths.index(min(transpiled_2q_depths))
[85, 85, 81, 89, 81, 81, 89, 85, 85]
[10, 10, 10, 10, 10, 10, 10, 10, 10]

Observăm că, în acest caz, adâncimea cu doi qubiți după transpilare a fost mereu 10. A existat o variație minoră în adâncimea cu un singur qubit, iar noi vom folosi cel mai puțin adânc rezultat. Însă pe acest Circuit de 36 de qubiți, aceasta nu este o îmbunătățire critică. Putem vizualiza circuitul transpilat, deși la această scară devine din ce în ce mai dificil de interpretat vizual.

circuit_ibm = transpiled_qcs[2]
observable_ibm = observable.apply_layout(circuit_ibm.layout)
print(circuit_ibm.decompose().depth())
print(
f"2+ qubit depth: {circuit_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)}"
)
81
2+ qubit depth: 10

Qiskit Patterns Pasul 3: Execuție cu Qiskit Primitives

Pentru a limita timpul folosit pe calculatoarele cuantice reale, vom efectua doar câțiva pași de optimizare aici, și facem asta pe un set de antrenament foarte mic. Dar scalarea la mai mulți pași de optimizare și seturi de date de testare mai mari ar trebui să fie clară din instrucțiunile din lecție.

# This was run on an Eagle r3 processor on 10-4-24, and took 7 min.

from qiskit_ibm_runtime import EstimatorV2 as Estimator, Session

batch_size = 7
num_epochs = 1
num_samples = len(train_images)

# Globals
circuit = circuit_ibm
observable = observable_ibm
objective_func_vals = []
iter = 0

# Random initial weights for the ansatz
np.random.seed(42)
# weight_params = np.random.rand(len(ansatz.parameters)) * 2 * np.pi
# Or re-load weights from a previous calculation
weight_params = np.array(
[
3.35330497,
5.97351416,
4.59925358,
3.76148219,
0.98029403,
0.98014248,
0.3649501,
6.44234523,
3.77691701,
4.44895122,
0.12933619,
6.09412333,
5.23039137,
1.33416598,
1.14243996,
1.15236452,
1.91161039,
3.2971419,
3.71399059,
1.82984665,
3.84438512,
0.87646578,
1.83559896,
2.30191935,
2.86557222,
4.93340606,
1.25458737,
3.23103027,
3.72225051,
0.29185655,
3.81731689,
1.07143467,
0.40873121,
5.96202367,
6.067245,
5.07931034,
1.91394476,
0.61369199,
4.2991629,
2.76555968,
0.76678884,
3.11128829,
0.21606945,
5.71342859,
1.62596258,
4.16275028,
1.95853845,
3.26768375,
3.43508199,
1.1614748,
6.09207989,
4.87030317,
5.90304595,
5.62236606,
3.75671636,
5.79230665,
0.55601479,
1.23139664,
0.28417144,
2.04411075,
2.44213144,
1.70493625,
5.20711134,
2.24154726,
1.76516358,
3.40986006,
0.88545302,
5.04035228,
0.46841551,
6.2007935,
4.85215699,
1.24856745,
]
)

# Running in a session avoids repeated queuing. This is available to Premium Plan, Flex Plan, and On-Prem (IBM Quantum Platform API) Plan users.

with Session(backend=backend) as session:
estimator = Estimator(mode=session, options={"resilience_level": 1})

for epoch in range(num_epochs):
for i in range((num_samples - 1) // batch_size + 1):
print(f"Epoch: {epoch}, batch: {i}")
start_i = i * batch_size
end_i = start_i + batch_size
train_images_batch = np.array(train_images[start_i:end_i])
train_labels_batch = np.array(train_labels[start_i:end_i])
input_params = train_images_batch
target = train_labels_batch
iter = 0
# We can increase maxiter to do a full optimization.
res = minimize(
mse_loss_weights,
weight_params,
method="COBYLA",
options={"maxiter": 20},
)
weight_params = res["x"]
session.close()

# Open users can carry out the same calculation using a batch, but repeated queuing is possible.
# from qiskit_ibm_runtime import Batch

# with Batch(backend=backend) as batch:
# estimator = Estimator(
# mode=batch, options={"resilience_level": 1}
# )
#
# for epoch in range(num_epochs):
# for i in range((num_samples - 1) // batch_size + 1):
# print(f"Epoch: {epoch}, batch: {i}")
# start_i = i * batch_size
# end_i = start_i + batch_size
# train_images_batch = np.array(train_images[start_i:end_i])
# train_labels_batch = np.array(train_labels[start_i:end_i])
# input_params = train_images_batch
# target = train_labels_batch
# iter = 0
# # We can increase maxiter to do a full optimization.
# res = minimize(
# mse_loss_weights,
# weight_params,
# method="COBYLA",
# options={"maxiter": 20},
# )
# weight_params = res["x"]
# batch.close()

Este recomandat să salvezi parametrii de pondere returnați de acest calcul, în cazul în care decizi să continui iterațiile.

weight_params
array([3.35330497, 6.97351416, 5.59925358, 3.76148219, 0.98029403,
0.98014248, 0.3649501 , 6.44234523, 3.77691701, 4.44895122,
1.12933619, 7.09412333, 5.23039137, 1.33416598, 1.14243996,
1.15236452, 1.91161039, 3.2971419 , 3.71399059, 1.82984665,
3.84438512, 0.87646578, 1.83559896, 2.30191935, 2.86557222,
4.93340606, 1.25458737, 3.23103027, 3.72225051, 0.29185655,
3.81731689, 1.07143467, 0.40873121, 5.96202367, 6.067245 ,
5.07931034, 1.91394476, 0.61369199, 4.2991629 , 2.76555968,
0.76678884, 3.11128829, 0.21606945, 5.71342859, 1.62596258,
4.16275028, 1.95853845, 3.26768375, 3.43508199, 1.1614748 ,
6.09207989, 4.87030317, 5.90304595, 5.62236606, 3.75671636,
5.79230665, 0.55601479, 1.23139664, 0.28417144, 2.04411075,
2.44213144, 1.70493625, 5.20711134, 2.24154726, 1.76516358,
3.40986006, 0.88545302, 5.04035228, 0.46841551, 6.2007935 ,
4.85215699, 1.24856745])

Putem reprezenta grafic primii câțiva pași de optimizare, deși nu ne așteptăm la nicio convergență după atât de puțini pași. Aceste curbe au fost relativ plate în primii pași, chiar și folosind simulatoare. Trebuie să remarcăm, totuși, că optimizarea are în prezent 72 de parametri liberi. Aceștia pot fi reduși cu cel puțin un factor de 2-3 fără a compromite rezultatele, de exemplu, prin parametrizarea qubiților cu date corespunzătoare unui subset de rânduri și coloane complete. Într-adevăr, spațiul parametrilor ar trebui redus înainte de a aloca mai mult timp de calcul cuantic pentru minimizarea funcției de pierdere.

obj_func_vals_qc = objective_func_vals
# import matplotlib.pyplot as plt

plt.figure(figsize=(12, 6))
plt.plot(obj_func_vals_qc, label="revised ansatz")
plt.xlabel("iteration")
plt.ylabel("loss")
plt.legend()
plt.show()

Output of the previous code cell

Încheiere

Recapitulând, în această lecție am învățat fluxul de lucru pentru clasificarea binară a imaginilor cu ajutorul unei rețele neurale cuantice. Câteva considerații-cheie pentru fiecare pas al tiparelor Qiskit au fost:

Pasul 1: Maparea problemei pe un Circuit cuantic

  • Încarcă datele de antrenament. Acest lucru se poate face „manual" sau folosind o hartă de caracteristici pre-construită, cum ar fi z_feature_map.
  • Construiește un ansatz ce conține straturi de rotație și de entanglare potrivite pentru problema ta.
  • Monitorizează adâncimea Circuit-ului pentru a asigura rezultate de calitate pe calculatoarele cuantice.

Pasul 2: Optimizarea problemei pentru execuția cuantică

  • Selectează un Backend, de obicei cel mai puțin ocupat.
  • Folosește un manager de treceri (pass manager) pentru a transpila atât Circuit-ul, cât și observabilele la arhitectura Backend-ului ales.
  • Pentru Circuit-uri foarte adânci sau largi, transpilează de mai multe ori și selectează Circuit-ul cel mai puțin adânc.

Pasul 3: Execuție cu Primitivele Qiskit (Runtime)

  • Efectuează teste preliminare pe simulatoare pentru a depana și optimiza ansatz-ul.
  • Execută pe un calculator cuantic IBM®.

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

  • Calculează acuratețea modelului pe datele de antrenament și pe datele de testare.
  • Monitorizează convergența optimizării clasice.

Referințe