Circuiti Quantistici Variazionali e Reti Neurali Quantistiche
In questa lezione implementiamo diversi circuiti quantistici variazionali per un compito di classificazione dei dati, i cosiddetti classificatori quantistici variazionali (VQC). In passato era comune riferirsi a un sottoinsieme dei VQC come reti neurali quantistiche (QNN) per analogia con le reti neurali classiche. In effetti, esistono casi in cui strutture mutuate dalle reti neurali classiche, come i livelli di convoluzione, svolgono un ruolo importante nei VQC. In questi casi, dove l'analogia è forte, le QNN possono essere una descrizione utile. Ma i circuiti quantistici parametrizzati non devono necessariamente seguire la struttura generale di una rete neurale; per esempio, non tutti i dati devono essere caricati nel primo livello (di input): possiamo caricare alcuni dati nel primo livello, applicare alcune porte e poi caricare ulteriori dati (un processo chiamato "reuploading" dei dati). Dovremmo quindi considerare le QNN come un sottoinsieme dei circuiti quantistici parametrizzati, senza lasciarci limitare nell'esplorazione di circuiti quantistici utili dall'analogia con le reti neurali classiche.
Il dataset affrontato in questa lezione è composto da immagini contenenti strisce orizzontali e verticali; il nostro obiettivo è classificare immagini non viste in una delle due categorie in base all'orientamento della riga. Lo faremo con un VQC. Lungo il percorso, esamineremo modi per migliorare e scalare il calcolo. Il dataset qui scelto è eccezionalmente facile da classificare classicamente: è stato scelto per la sua semplicità, in modo da poterci concentrare sulla parte quantistica del problema e osservare come un attributo del dataset possa tradursi in una parte di un circuito quantistico. Non è ragionevole aspettarsi un'accelerazione quantistica per casi così semplici in cui gli algoritmi classici sono così efficienti.
Al termine di questa lezione sarai in grado di:
- Caricare dati da un'immagine in un circuito quantistico
- Costruire un ansatz per un VQC (o QNN) e adattarlo al tuo problema
- Addestrare il tuo VQC/QNN e usarlo per fare previsioni accurate sui dati di test
- Scalare il problema e riconoscere i limiti degli attuali computer quantistici
Generazione dei dati
Inizieremo costruendo i dati. I dataset spesso non vengono generati esplicitamente nell'ambito del framework dei pattern di Qiskit. Ma il tipo di dato e la sua preparazione sono fondamentali per applicare con successo il calcolo quantistico all'apprendimento automatico. Il codice seguente definisce un dataset di immagini con dimensioni in pixel predefinite. Un'intera riga o colonna dell'immagine viene assegnata al valore , mentre i pixel rimanenti ricevono valori casuali nell'intervallo . I valori casuali sono il rumore nei nostri dati. Scorri il codice per assicurarti di capire come vengono generate le immagini. In seguito scaleremo le immagini.
# 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)
Nota che il codice sopra ha anche generato etichette che indicano se le immagini contengono una riga verticale (+1) o orizzontale (-1). Utilizzeremo ora sklearn per suddividere un dataset di 100 immagini in un insieme di addestramento e uno di test (insieme alle corrispondenti etichette). Qui usiamo il del dataset per l'addestramento, con il restante tenuto da parte per il test.
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
)
Visualizziamo alcuni elementi del nostro dataset per vedere come appaiono queste righe:
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)
Ciascuna di queste immagini è ancora abbinata alla sua etichetta in train_labels in forma di semplice lista:
print(train_labels[:8])
[1, 1, 1, 1, -1, 1, 1, 1]
Classificatore quantistico variazionale: un primo tentativo
Step 1 dei pattern di Qiskit: Mappare il problema su un circuito quantistico
L'obiettivo è trovare una funzione con parametri che mappa un vettore di dati / immagine nella categoria corretta: . Ciò verrà realizzato usando un VQC con pochi livelli identificabili per i loro scopi distinti:
Qui, è il circuito di codifica, per il quale abbiamo molte opzioni come visto nelle lezioni precedenti. è un blocco circuitale variazionale, o addestrabile, e è l'insieme dei parametri da addestrare. Tali parametri verranno variati da algoritmi di ottimizzazione classici per trovare l'insieme di parametri che produce la migliore classificazione delle immagini da parte del circuito quantistico. Questo circuito variazionale è talvolta chiamato "ansatz". Infine, è un'osservabile che verrà stimata usando la primitiva Estimator. Non esiste alcun vincolo che obblighi i livelli a comparire in questo ordine, o ad essere completamente separati. Si potrebbero avere più livelli variazionali e/o di codifica in qualsiasi ordine motivato tecnicamente.
Iniziamo scegliendo una feature map per codificare i nostri dati. Useremo la z_feature_map, poiché mantiene le profondità del circuito ridotte rispetto ad alcune altre mappature delle feature.
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")
Dobbiamo ora decidere un ansatz da addestrare. Ci sono molte considerazioni nella scelta di un ansatz. Una descrizione completa va oltre lo scopo di questa introduzione; qui evidenziamo semplicemente alcune categorie di considerazioni.
- Hardware: Tutti i moderni computer quantistici sono più soggetti a errori e più suscettibili al rumore rispetto alle loro controparti classiche. Usare un ansatz eccessivamente profondo (specialmente in termini di profondità a due qubit traspilata) non produrrà buoni risultati. Un problema correlato è che i computer quantistici hanno una certa disposizione dei qubit, ovvero alcuni qubit fisici sono adiacenti sul computer quantistico, mentre altri possono essere molto distanti. L'entanglement di qubit adiacenti non aumenta molto la profondità, ma l'entanglement di qubit molto distanti può aumentarla sostanzialmente, poiché è necessario inserire porte di swap per spostare le informazioni su qubit adiacenti affinché possano essere entanglati.
- Il problema: Ogni volta che si dispone di informazioni sul problema che possono guidare la scelta dell'ansatz, è opportuno sfruttarle. Ad esempio, i dati in questa lezione sono composti da immagini di righe orizzontali e verticali. Si potrebbe considerare quale correlazione tra colori/valori adiacenti identifica un'immagine di una riga orizzontale o verticale. Quali attributi di un ansatz corrisponderebbero a questa correlazione tra pixel adiacenti? Ritorneremo su questo punto in modo più tecnico più avanti in questa lezione. Per ora, diciamo semplicemente che includere entanglement e porte CNOT tra qubit corrispondenti a pixel adiacenti sembra una buona idea. Nel quadro più ampio, considera se il problema è effettivamente meglio risolto usando un circuito quantistico, o se potrebbero esistere algoritmi classici in grado di fare altrettanto bene.
- Numero di parametri: Ogni porta quantistica indipendentemente parametrizzata nel circuito aumenta lo spazio da ottimizzare classicamente, e ciò comporta una convergenza più lenta. Ma man mano che i problemi si scalano, si potrebbe incontrare il fenomeno dei barren plateau. Questo termine si riferisce a un fenomeno per cui il panorama di ottimizzazione di un algoritmo quantistico variazionale diventa esponenzialmente piatto e privo di caratteristiche man mano che la dimensione del problema aumenta. Ciò causa gradienti che svaniscono, rendendo difficile addestrare efficacemente l'algoritmo[1]. I barren plateau sono rilevanti per gli algoritmi quantistici variazionali come i VQC/QNN. Va notato che il numero crescente di parametri non è l'unica considerazione per evitare i barren plateau; altre considerazioni includono le funzioni di costo globali e l'inizializzazione casuale dei parametri.
In questa lezione vedremo alcuni semplici esempi di buone pratiche nella costruzione degli ansatz. Proviamo prima l'ansatz seguente. Torneremo a rivederlo in seguito.
# 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]) ├─────────────────────────────────────
└──────────┘└───────────┘
Con la codifica dei dati e il circuito variazionale pronti, possiamo combinarli per formare il nostro ansatz completo. In questo caso, le componenti del nostro circuito quantistico sono abbastanza analoghe a quelle delle reti neurali, con che è il più simile al livello che carica i valori di input dall'immagine, e che è come il livello di "pesi" variabili. Poiché questa analogia vale in questo caso, adottiamo "qnn" in alcune delle nostre convenzioni di denominazione; ma questa analogia non dovrebbe essere limitante nella tua esplorazione dei VQC.

# 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)
Dobbiamo ora definire un'osservabile, in modo da poterla usare nella nostra funzione di costo. Otterremo un valore di aspettazione per questa osservabile usando Estimator. Se abbiamo selezionato un buon ansatz motivato dal problema, ogni qubit conterrà informazioni rilevanti per la classificazione. Si possono aggiungere livelli per combinare le informazioni su un numero inferiore di qubit (chiamato livello convoluzionale), in modo tale che le misurazioni siano necessarie solo su un sottoinsieme dei qubit nel circuito (come nelle reti neurali convoluzionali). Oppure si può misurare qualche attributo da ogni qubit. Qui optiamo per quest'ultimo approccio, quindi includiamo un operatore Z per ogni qubit. Non c'è nulla di univoco nella scelta di , ma è ben motivata:
- Questo è un compito di classificazione binaria, e una misura di può produrre due possibili risultati.
- Gli autovalori di () sono ragionevolmente ben separati e producono un risultato dell'estimator nell'intervallo [-1, +1], dove 0 può essere usato semplicemente come valore di soglia.
- È semplice misurare nella base di Pauli Z senza overhead aggiuntivo di porte.
Quindi, Z è una scelta molto naturale.
from qiskit.quantum_info import SparsePauliOp
observable = SparsePauliOp.from_list([("Z" * (num_qubits), 1)])
Abbiamo il nostro circuito quantistico e l'osservabile che vogliamo stimare. Ora abbiamo bisogno di alcune cose per eseguire e ottimizzare questo circuito. Prima di tutto, abbiamo bisogno di una funzione per eseguire un passaggio in avanti (forward pass). Nota che la funzione seguente riceve separatamente i input_params e i weight_params. I primi sono l'insieme dei parametri statici che descrivono i dati in un'immagine, e i secondi sono l'insieme dei parametri variabili da ottimizzare.
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
Funzione di perdita
Successivamente, abbiamo bisogno di una funzione di perdita per calcolare la differenza tra i valori previsti e calcolati delle etichette. La funzione riceverà le etichette predette dall'algoritmo e le etichette corrette, e restituirà la differenza quadratica media. Esistono molte diverse funzioni di perdita. Qui, MSE è un esempio che abbiamo scelto.
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")
Definiamo anche una funzione di perdita leggermente diversa che dipende dai parametri variabili (pesi), da utilizzare con l'ottimizzatore classico. Questa funzione accetta solo i parametri dell'ansatz come input; le altre variabili per il passaggio in avanti e la perdita sono impostate come parametri globali. L'ottimizzatore addestrerà il modello campionando diversi pesi e cercando di ridurre l'output della funzione di costo/perdita.
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
In precedenza abbiamo menzionato l'uso di un ottimizzatore classico. Quando si tratterà di cercare tra i pesi per minimizzare la funzione di costo, useremo l'ottimizzatore COBYLA:
from scipy.optimize import minimize
Imposteremo alcune variabili globali iniziali per la funzione di costo.
# Globals
circuit = full_circuit
observables = observable
# input_params = train_images_batch
# target = train_labels_batch
objective_func_vals = []
iter = 0
Step 2 dei Pattern di Qiskit: Ottimizzare il problema per l'esecuzione quantistica
Iniziamo selezionando un backend per l'esecuzione. In questo caso, utilizzeremo il backend meno occupato.
from qiskit_ibm_runtime import QiskitRuntimeService
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)
print(backend.name)
ibm_brisbane
Qui ottimizziamo il circuito per l'esecuzione su un backend reale specificando il livello di ottimizzazione e aggiungendo il decoupling dinamico. Il codice seguente genera un pass manager usando i preset pass manager di 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,
),
]
)
Ora utilizziamo il pass manager sul circuito. Le modifiche al layout che ne risultano devono essere applicate anche all'osservabile. Per circuiti molto grandi, le euristiche usate nell'ottimizzazione del circuito potrebbero non produrre sempre il circuito migliore e più superficiale. In questi casi, ha senso eseguire tali pass manager più volte e usare il circuito migliore. Lo vedremo in seguito quando scaleremo il nostro calcolo.
circuit_ibm = pm.run(full_circuit)
observable_ibm = observable.apply_layout(circuit_ibm.layout)
Passo 3 dei Qiskit Patterns: Eseguire con i Primitivi Qiskit
Iterare sul dataset in batch ed epoche
Implementiamo prima l'algoritmo completo usando un simulatore per un debug preliminare e per stimare l'errore. Possiamo ora scorrere l'intero dataset in batch per il numero di epoche desiderato, in modo da addestrare la nostra rete neurale quantistica.
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
Passo 4 dei Qiskit Patterns: Post-elaborazione, restituzione del risultato in formato classico
Test e accuratezza
Interpretiamo ora i risultati dell'addestramento. Prima verifichiamo l'accuratezza di training sul training set.
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%
L'accuratezza di training è solo del , il che non è certamente un buon risultato. È difficile immaginare che le prestazioni del modello sul test set possano essere migliori. Verifichiamolo.
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%
Il modello non classifica bene questi dati. Dovremmo chiederci perché, e in particolare verificare:
- Abbiamo interrotto il training troppo presto? Erano necessari più passi di ottimizzazione?
- Abbiamo costruito un ansatz scadente? Questo potrebbe significare molte cose. Quando lavoriamo su computer quantistici reali, la profondità del circuito sarà una considerazione fondamentale. Anche il numero di parametri è potenzialmente importante, così come l'entanglement tra i qubit.
- Combinando i due punti precedenti, abbiamo costruito un ansatz con troppi parametri per essere addestrabile?
Possiamo iniziare verificando la convergenza dell'ottimizzazione:
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()
Potremmo provare ad estendere i passi di ottimizzazione per assicurarci che l'ottimizzatore non si sia bloccato in un minimo locale nello spazio dei parametri. Ma sembra abbastanza converso. Osserviamo più da vicino le immagini che non sono state classificate correttamente, per capire cosa stia succedendo.
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)
Qui possiamo vedere che la grande maggioranza delle immagini classificate erroneamente contiene una linea verticale. Qualcosa nel nostro modello non riesce a catturare le informazioni su di esse. Potresti averlo intuito, osservando il primo circuito variazionale. Guardiamolo più da vicino.
Migliorare il modello
Passo 1 rivisitato
Nel mappare il problema su un circuito quantistico, avremmo dovuto pensare esplicitamente a come le informazioni nei pixel adiacenti determinano la classe. Per identificare le linee orizzontali, vogliamo sapere "se il pixel è giallo, il pixel è giallo" per tutti i pixel di ciascuna riga. Vogliamo anche sapere delle linee verticali. Ma poiché la classificazione è binaria, si potrebbe semplicemente dire che se una tale linea orizzontale non viene rilevata, allora si tratta di una linea verticale. Il nostro precedente circuito variazionale conteneva porte CNOT tra i qubit (e quindi i pixel) 0 e 1, 1 e 2, e 2 e 3. Questo copre le linee orizzontali nella parte superiore dell'immagine, ma non rileva direttamente le linee verticali, né rileva completamente le linee orizzontali, poiché ignora la riga inferiore. Per rilevare completamente tutte le linee orizzontali, vorremmo avere un insieme simile di porte CNOT tra i qubit (pixel) 4 e 5, 5 e 6, e 6 e 7. Potremmo tenere a mente che aggiungere porte CNOT tra qubit corrispondenti a linee verticali (come 0 e 4, o 2 e 6) potrebbe essere utile. Ma verificheremo prima se è sufficiente rilevare la presenza o assenza di una linea orizzontale.
# 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
Non abbiamo aumentato la profondità del circuito. Vediamo se abbiamo aumentato la sua capacità di modellare le nostre immagini.
Passo 2 rivisitato
Dovremo transpilare questo nuovo circuito per eseguirlo su un backend quantistico reale. Saltiamo questo passaggio per ora, per verificare se la revisione del circuito variazionale ha avuto l'effetto desiderato sui simulatori. Approfondiremo la transpilazione nella prossima sottosezione.
Passo 3 rivisitato
Applichiamo ora il modello aggiornato ai nostri dati di training.
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
Passo 4 rivisitato
Iniziamo verificando se il nostro ottimizzatore è pienamente converso.
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()
Questo non sembra pienamente convergente, poiché la funzione di perdita non è rimasta approssimativamente stabile per un numero sostanziale di passi. Tuttavia, la funzione di perdita è già circa il più bassa rispetto all'utilizzo del precedente circuito variazionale. Se questo fosse un progetto di ricerca, vorremmo garantire la piena convergenza. Ma per scopi esplorativi, è sufficiente. Verifichiamo l'accuratezza sui nostri dati di training e di test.
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\%$ di accuratezza su entrambi i set! La nostra ipotesi riguardo alla sufficienza del rilevamento accurato delle linee orizzontali era corretta! Inoltre, la nostra mappatura delle informazioni richieste sui pixel alle porte CNOT nel circuito quantistico è stata efficace. Vediamo ora come questo processo si scala per l'esecuzione su computer quantistici reali.
## Scalabilità ed esecuzione su computer quantistici reali \{#scaling-and-running-on-real-quantum-computers}
### Dati \{#data}
Iniziamo aumentando le dimensioni delle nostre immagini. La scelta di una griglia 6x6 non ha nulla di speciale, se non che supera il numero di qubit (32) che possiamo simulare per circuiti con gate 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)
Poiché il tempo di calcolo quantistico è una risorsa preziosa, utilizzeremo un training set molto ridotto e pochissimi passi di ottimizzazione. Questo sarà sufficiente per illustrare il flusso di lavoro.
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)
Passo 1: Mappare il problema su un circuito quantistico
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
Questa è una profondità a due qubit ragionevole. Dovremmo riuscire a ottenere risultati di alta qualità da un computer quantistico reale.
# 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
Poiché utilizziamo la z_feature_map, che non contiene gate CNOT, l'aggiunta del layer di codifica non aumenta la profondità a due qubit. Possiamo visualizzare il circuito completo qui.
full_circuit.decompose().draw("mpl", style="clifford", idle_wires=False, fold=-1)

Potresti notare che, se minimizzare la profondità a due qubit fosse di primaria importanza, potremmo effettivamente ridurla leggermente modificando l'ordine dei gate CNOT. Ad esempio, i gate CNOT su e potrebbero essere spostati a sinistra nel diagramma del circuito qui sopra, e potrebbero essere collocati direttamente sotto i gate CNOT su e , per esempio. Con una profondità a due qubit pari a 5, non è ovvio che questo faccia differenza dopo la trasposizione, ma è qualcosa da tenere a mente. Se l'ordine dei gate CNOT è importante per corrispondere logicamente al problema in questione, la profondità attuale va bene. Se invece l'ordine dei gate CNOT non è critico per modellare la struttura dei dati nelle nostre immagini, potremmo scrivere uno script per riordinare questi gate CNOT e minimizzare la profondità.
Dobbiamo anche ridefinire il nostro osservabile con le immagini più grandi:
from qiskit.quantum_info import SparsePauliOp
observable = SparsePauliOp.from_list([("Z" * (num_qubits), 1)])
Passo 2 dei Qiskit Patterns: ottimizzare il problema per l'esecuzione quantistica
Iniziamo selezionando un backend per l'esecuzione. In questo caso, utilizzeremo il backend meno occupato.
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
Ancora una volta, definiamo un pass manager con il livello di ottimizzazione impostato a 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,
),
]
)
Ora applicheremo il pass manager più volte. Per circuiti molto ampi o molto profondi, la profondità a due qubit dopo la trasposizione può variare notevolmente. Per tali circuiti è importante provare il pass manager più volte e utilizzare il risultato migliore (più superficiale).
# 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]
Vediamo che in questo caso la profondità a due qubit dopo la trasposizione è sempre stata 10. C'è stata una variazione minore nella profondità a singolo qubit, e utilizzeremo quella più superficiale. Ma su questo circuito a 36 qubit, non si tratta di un miglioramento critico. Possiamo visualizzare il circuito transpilato, sebbene a questa scala diventi sempre più difficile da interpretare visivamente.
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
Passo 3 dei Qiskit Patterns: eseguire con i Qiskit Primitives
Per limitare il tempo di utilizzo dei computer quantistici reali, eseguiremo qui solo pochi passi di ottimizzazione su un training set molto ridotto. Tuttavia, come scalare questo processo con più passi di ottimizzazione e dataset di test più ampi dovrebbe essere chiaro dalle istruzioni presenti nella lezione.
# 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()
Si raccomanda di salvare i parametri di peso restituiti da questo calcolo, nel caso in cui si decida di procedere con ulteriori iterazioni.
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])
Possiamo tracciare questi primi passi di ottimizzazione, anche se non ci aspettiamo alcuna convergenza dopo soli pochi passi. Queste curve sono state relativamente piatte nei primi passi, anche utilizzando simulatori. Va tuttavia notato che l'ottimizzazione ha attualmente 72 parametri liberi. Questo numero può essere ridotto di almeno un fattore 2-3 senza compromettere i risultati, ad esempio parametrizzando i qubit con dati corrispondenti a un sottoinsieme di righe e colonne complete. Infatti, lo spazio dei parametri dovrebbe essere ridotto prima di spendere ulteriore tempo di calcolo quantistico per minimizzare la funzione di perdita.
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()
Conclusioni
Per ricapitolare, in questa lezione abbiamo appreso il flusso di lavoro per la classificazione binaria di immagini tramite una rete neurale quantistica. Alcune considerazioni chiave per ciascun passo dei Qiskit Patterns sono state:
Passo 1: Mappare il problema su un circuito quantistico
- Caricare i dati di addestramento. Questo può essere fatto "manualmente" oppure utilizzando una feature map predefinita come
z_feature_map. - Costruire un ansatz contenente layer di rotazione e layer di entanglement appropriati per il problema.
- Monitorare la profondità del circuito per garantire risultati di qualità sui computer quantistici.
Passo 2: Ottimizzare il problema per l'esecuzione quantistica
- Selezionare un backend, spesso quello meno occupato.
- Usare un pass manager per transpilare sia il circuito che gli osservabili all'architettura del backend scelto.
- Per circuiti molto profondi o molto ampi, transpilare più volte e selezionare il circuito più superficiale.
Passo 3: Eseguire con i Qiskit (Runtime) Primitives
- Eseguire prove preliminari su simulatori per fare il debug e ottimizzare l'ansatz.
- Eseguire su un computer quantistico IBM®.
Passo 4: Post-elaborazione, restituzione del risultato in formato classico
- Calcolare l'accuratezza del modello sui dati di addestramento e sui dati di test.
- Monitorare la convergenza dell'ottimizzazione classica.