Quantenvariationsschaltkreise und Quantenneuronale Netze
In dieser Lektion implementieren wir mehrere variative Quantenschaltkreise für eine Datenklassifikationsaufgabe – sogenannte variative Quantenklassifizierer (VQCs). Früher war es üblich, eine Teilmenge von VQCs in Analogie zu klassischen neuronalen Netzen als Quantenneuronale Netze (QNNs) zu bezeichnen. Tatsächlich gibt es Fälle, in denen aus klassischen neuronalen Netzen übernommene Strukturen – etwa Faltungsschichten – eine wichtige Rolle in VQCs spielen. In solchen Fällen, wo die Analogie stark ist, kann die Bezeichnung QNN sinnvoll sein. Parametrierte Quantenschaltkreise müssen jedoch nicht der allgemeinen Struktur eines neuronalen Netzes folgen: So müssen beispielsweise nicht alle Daten in der ersten (Eingabe-)Schicht geladen werden; wir können einen Teil der Daten in der ersten Schicht laden, einige Gates anwenden und dann weitere Daten laden (ein Vorgang, der als „Reuploading" bezeichnet wird). Wir sollten QNNs daher als eine Teilmenge parametrierter Quantenschaltkreise betrachten und uns bei der Erkundung nützlicher Quantenschaltkreise nicht durch die Analogie zu klassischen neuronalen Netzen einschränken lassen.
Der in dieser Lektion verwendete Datensatz besteht aus Bildern mit horizontalen und vertikalen Streifen. Unser Ziel ist es, unbekannte Bilder je nach Ausrichtung ihrer Linien einer der beiden Kategorien zuzuordnen. Das werden wir mithilfe eines VQC erreichen. Dabei befassen wir uns mit Möglichkeiten, die Berechnung zu verbessern und zu skalieren. Der hier verwendete Datensatz ist klassisch außergewöhnlich leicht zu klassifizieren. Er wurde wegen seiner Einfachheit gewählt, damit wir uns auf den Quantenaspekt des Problems konzentrieren und untersuchen können, wie sich ein Attribut eines Datensatzes in einen Teil eines Quantenschaltkreises übersetzen lässt. Es wäre unrealistisch, für so einfache Fälle, in denen klassische Algorithmen so effizient sind, einen Quantenvorteil zu erwarten.
Am Ende dieser Lektion solltest du in der Lage sein:
- Daten aus einem Bild in einen Quantenschaltkreis zu laden
- Einen Ansatz für einen VQC (oder QNN) zu konstruieren und ihn an dein Problem anzupassen
- Deinen VQC/QNN zu trainieren und damit genaue Vorhersagen auf Testdaten zu treffen
- Das Problem zu skalieren und die Grenzen aktueller Quantencomputer zu erkennen
Datengenerierung
Wir beginnen damit, die Daten zu erstellen. Datensätze werden im Rahmen des Qiskit-Patterns-Frameworks häufig nicht explizit generiert. Doch Datentyp und -aufbereitung sind entscheidend für die erfolgreiche Anwendung von Quantencomputing auf maschinelles Lernen. Der folgende Code definiert einen Datensatz aus Bildern mit festgelegten Pixelabmessungen. Eine vollständige Zeile oder Spalte des Bildes erhält den Wert , und die verbleibenden Pixel werden mit Zufallswerten im Intervall belegt. Die Zufallswerte sind Rauschen in unseren Daten. Lies den Code durch, um sicherzustellen, dass du verstehst, wie die Bilder generiert werden. Später werden wir die Bilder vergrößern.
# 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)
Beachte, dass der obige Code auch Labels generiert hat, die angeben, ob die Bilder eine vertikale (+1) oder horizontale (-1) Linie enthalten. Wir verwenden nun sklearn, um einen Datensatz von 100 Bildern in ein Trainings- und ein Testset aufzuteilen (zusammen mit den entsprechenden Labels). Dabei nutzen wir des Datensatzes zum Training; die verbleibenden werden für den Test zurückgehalten.
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
)
Lass uns einige Elemente unseres Datensatzes darstellen, um zu sehen, wie diese Linien aussehen:
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)
Jedes dieser Bilder ist in train_labels weiterhin in einfacher Listenform mit seinem Label verknüpft:
print(train_labels[:8])
[1, 1, 1, 1, -1, 1, 1, 1]
Variationeller Quantenklassifikator: ein erster Versuch
Qiskit-Patterns Schritt 1: Das Problem auf einen Quantenschaltkreis abbilden
Das Ziel ist es, eine Funktion mit Parametern zu finden, die einen Datenvektor / ein Bild der richtigen Kategorie zuordnet: . Dies wird mithilfe eines VQC mit wenigen Schichten erreicht, die anhand ihrer unterschiedlichen Aufgaben identifiziert werden können:
Hier ist der Kodierungsschaltkreis, für den wir viele Optionen haben, wie in früheren Lektionen gesehen. ist ein variationeller bzw. trainierbarer Schaltkreisblock, und ist die Menge der zu trainierenden Parameter. Diese Parameter werden durch klassische Optimierungsalgorithmen variiert, um die Parameterkombination zu finden, die die beste Klassifizierung von Bildern durch den Quantenschaltkreis liefert. Dieser variationelle Schaltkreis wird manchmal als „Ansatz" bezeichnet. Schließlich ist eine Observable, die mithilfe des Estimator-Primitives geschätzt wird. Es gibt keine Einschränkung, die die Schichten zu dieser Reihenfolge zwingt oder sie vollständig voneinander trennt. Man könnte mehrere variationelle und/oder Kodierungsschichten in jeder beliebigen, technisch motivierten Reihenfolge haben.
Wir beginnen mit der Wahl einer Feature Map zur Kodierung unserer Daten. Wir verwenden die z_feature_map, da sie im Vergleich zu einigen anderen Feature Mappings die Schaltkreistiefen niedrig hält.
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")
Wir müssen nun einen Ansatz zum Trainieren auswählen. Bei der Wahl eines Ansatzes gibt es viele Überlegungen anzustellen. Eine vollständige Beschreibung würde den Rahmen dieser Einführung sprengen; hier weisen wir lediglich auf einige Kategorien von Überlegungen hin.
- Hardware: Alle modernen Quantencomputer sind fehleranfälliger und anfälliger für Rauschen als ihre klassischen Gegenstücke. Die Verwendung eines Ansatzes, der übermäßig tief ist (insbesondere in der transpilierten Zwei-Qubit-Tiefe), führt zu keinen guten Ergebnissen. Ein verwandtes Problem besteht darin, dass Quantencomputer ein bestimmtes Qubit-Layout haben, was bedeutet, dass einige physische Qubits auf dem Quantencomputer benachbart sind, während andere sehr weit voneinander entfernt sein können. Das Verschränken benachbarter Qubits erhöht die Tiefe nicht allzu sehr, aber das Verschränken sehr weit entfernter Qubits kann die Tiefe erheblich erhöhen, da wir Swap-Gates einfügen müssen, um Informationen auf benachbarte Qubits zu verschieben, damit sie verschränkt werden können.
- Das Problem: Wann immer du über Informationen zu deinem Problem verfügst, die deinen Ansatz leiten könnten, solltest du diese nutzen. Die Daten in dieser Lektion bestehen beispielsweise aus Bildern von horizontalen und vertikalen Linien. Man könnte überlegen, welche Korrelation zwischen benachbarten Farben/Werten ein Bild einer horizontalen oder vertikalen Linie kennzeichnet. Welche Eigenschaften eines Ansatzes würden dieser Korrelation zwischen benachbarten Pixeln entsprechen? Wir werden diesen Punkt später in dieser Lektion technisch genauer betrachten. Für jetzt sei gesagt, dass die Einbeziehung von Verschränkung und CNOT-Gates zwischen Qubits, die benachbarten Pixeln entsprechen, eine gute Idee zu sein scheint. Im größeren Zusammenhang sollte man überlegen, ob das Problem tatsächlich am besten mit einem Quantenschaltkreis gelöst wird oder ob es klassische Algorithmen geben könnte, die genauso gute Ergebnisse liefern.
- Anzahl der Parameter: Jedes unabhängig parametrisierte Quantengate im Schaltkreis vergrößert den klassisch zu optimierenden Raum und führt zu einer langsameren Konvergenz. Aber wenn Probleme skalieren, kann man auf Barren Plateaus stoßen. Dieser Begriff bezieht sich auf ein Phänomen, bei dem die Optimierungslandschaft eines variationellen Quantenalgorithmus mit zunehmender Problemgröße exponentiell flach und strukturlos wird. Dies führt zu verschwindenden Gradienten, was das effektive Training des Algorithmus erschwert[1]. Barren Plateaus sind relevant für variationelle Quantenalgorithmen wie VQCs/QNNs. Es sei darauf hingewiesen, dass die wachsende Anzahl von Parametern nicht die einzige Überlegung zur Vermeidung von Barren Plateaus ist; weitere Aspekte umfassen globale Kostenfunktionen und zufällige Parameterinitialisierung.
In dieser Lektion werden wir einige einfache Beispiele für gute Praktiken bei der Ansatzkonstruktion sehen. Versuchen wir zunächst den folgenden Ansatz. Wir werden später darauf zurückkommen, um ihn zu überarbeiten.
# 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]) ├─────────────────────────────────────
└──────────┘└───────────┘
Mit der vorbereiteten Datenkodierung und dem variationellen Schaltkreis können wir diese zu unserem vollständigen Ansatz kombinieren. In diesem Fall sind die Komponenten unseres Quantenschaltkreises denen in neuronalen Netzen sehr ähnlich: entspricht am ehesten der Schicht, die Eingabewerte aus dem Bild lädt, und ähnelt der Schicht mit variablen „Gewichten". Da diese Analogie in diesem Fall zutrifft, verwenden wir „qnn" in einigen unserer Namenskonventionen; aber diese Analogie sollte deine Erkundung von VQCs nicht einschränken.

# 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)
Wir müssen nun eine Observable definieren, damit wir sie in unserer Kostenfunktion verwenden können. Den Erwartungswert dieser Observable erhalten wir mithilfe des Estimators. Wenn wir einen guten, problemorientierten Ansatz gewählt haben, enthält jedes Qubit relevante Informationen für die Klassifizierung. Man kann Schichten hinzufügen, um Informationen auf weniger Qubits zu bündeln (sogenannte Convolutional Layer), sodass Messungen nur auf einem Teil der Qubits im Schaltkreis erforderlich sind (wie bei Convolutional Neural Networks). Oder man misst ein Attribut von jedem Qubit. Hier entscheiden wir uns für letzteres und fügen für jedes Qubit einen Z-Operator ein. Es gibt nichts Besonderes an der Wahl von , aber sie ist gut begründet:
- Dies ist eine binäre Klassifizierungsaufgabe, und eine Messung von kann zwei mögliche Ergebnisse liefern.
- Die Eigenwerte von () sind vernünftig voneinander getrennt und führen zu einem Estimator-Ergebnis im Intervall [-1, +1], wobei 0 einfach als Schwellenwert verwendet werden kann.
- Eine Messung in der Pauli-Z-Basis ist ohne zusätzlichen Gate-Aufwand unkompliziert.
Z ist also eine sehr naheliegende Wahl.
from qiskit.quantum_info import SparsePauliOp
observable = SparsePauliOp.from_list([("Z" * (num_qubits), 1)])
Wir haben unseren Quantenschaltkreis und die Observable, die wir schätzen wollen. Jetzt benötigen wir einige Dinge, um diesen Schaltkreis auszuführen und zu optimieren. Zunächst brauchen wir eine Funktion für einen Vorwärtsdurchlauf. Beachte, dass die folgende Funktion input_params und weight_params getrennt entgegennimmt. Erstere ist die Menge der statischen Parameter, die die Daten in einem Bild beschreiben, und letztere ist die Menge der variablen Parameter, die optimiert werden sollen.
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
Verlustfunktion
Als nächstes brauchen wir eine Verlustfunktion, um die Differenz zwischen den vorhergesagten und den berechneten Werten der Labels zu berechnen. Die Funktion nimmt die vom Algorithmus vorhergesagten Labels und die korrekten Labels entgegen und gibt den mittleren quadratischen Fehler zurück. Es gibt viele verschiedene Verlustfunktionen. Hier ist MSE ein Beispiel, das wir gewählt haben.
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")
Definieren wir außerdem eine etwas andere Verlustfunktion, die eine Funktion der variablen Parameter (Gewichte) ist, zur Verwendung durch den klassischen Optimierer. Diese Funktion nimmt nur die Ansatz-Parameter als Eingabe entgegen; andere Variablen für den Vorwärtsdurchlauf und den Verlust werden als globale Parameter gesetzt. Der Optimierer trainiert das Modell, indem er verschiedene Gewichte abtastet und versucht, den Ausgabewert der Kosten-/Verlustfunktion zu senken.
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
Oben haben wir die Verwendung eines klassischen Optimierers erwähnt. Bei der Suche nach Gewichten zur Minimierung der Kostenfunktion verwenden wir den Optimierer COBYLA:
from scipy.optimize import minimize
Wir setzen einige anfängliche globale Variablen für die Kostenfunktion.
# Globals
circuit = full_circuit
observables = observable
# input_params = train_images_batch
# target = train_labels_batch
objective_func_vals = []
iter = 0
Qiskit-Patterns Schritt 2: Problem für die Quantenausführung optimieren
Wir beginnen mit der Auswahl eines Backends für die Ausführung. In diesem Fall verwenden wir das am wenigsten ausgelastete Backend.
from qiskit_ibm_runtime import QiskitRuntimeService
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)
print(backend.name)
ibm_brisbane
Hier optimieren wir den Schaltkreis für die Ausführung auf einem echten Backend, indem wir das optimization_level angeben und Dynamical Decoupling hinzufügen. Der folgende Code generiert einen Pass Manager mithilfe von vordefinierten Pass Managern aus 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,
),
]
)
Jetzt wenden wir den Pass Manager auf den Schaltkreis an. Die daraus resultierenden Layout-Änderungen müssen auch auf die Observable angewendet werden. Bei sehr großen Schaltkreisen liefern die in der Schaltkreisoptimierung verwendeten Heuristiken möglicherweise nicht immer den besten und flachsten Schaltkreis. In solchen Fällen ist es sinnvoll, solche Pass Manager mehrfach auszuführen und den besten Schaltkreis zu verwenden. Dies werden wir später sehen, wenn wir unsere Berechnung skalieren.
circuit_ibm = pm.run(full_circuit)
observable_ibm = observable.apply_layout(circuit_ibm.layout)
Qiskit-Patterns Schritt 3: Ausführung mit Qiskit Primitives
Über den Datensatz in Batches und Epochen iterieren
Wir implementieren zunächst den vollständigen Algorithmus mit einem Simulator zur schnellen Fehlersuche und zur Abschätzung von Fehlern. Wir können nun den gesamten Datensatz in Batches über eine gewünschte Anzahl von Epochen durchlaufen, um unser Quantenneuronales Netz zu trainieren.
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
Qiskit Patterns Schritt 4: Nachverarbeitung, Ergebnis im klassischen Format zurückgeben
Testen und Genauigkeit
Wir interpretieren nun die Ergebnisse des Trainings. Zuerst testen wir die Trainingsgenauigkeit über das Trainingsset.
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%
Die Trainingsgenauigkeit beträgt nur , was definitiv nicht gut ist. Es ist kaum vorstellbar, dass die Modellleistung auf dem Testset besser sein könnte. Lass uns das überprüfen.
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%
Das Modell klassifiziert diese Daten nicht gut. Wir sollten fragen, warum das so ist, und insbesondere sollten wir prüfen:
- Haben wir das Training zu früh abgebrochen? Waren mehr Optimierungsschritte nötig?
- Haben wir einen schlechten Ansatz konstruiert? Das kann vieles bedeuten. Wenn wir auf echten Quantencomputern arbeiten, ist die Circuit-Tiefe ein wesentlicher Aspekt. Auch die Anzahl der Parameter ist potenziell wichtig, ebenso wie die Verschränkung zwischen Qubits.
- Haben wir in Kombination beider oben genannter Punkte einen Ansatz mit zu vielen Parametern konstruiert, der sich nicht trainieren lässt?
Wir können damit beginnen, die Konvergenz bei der Optimierung zu überprüfen:
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()
Wir könnten versuchen, die Optimierungsschritte zu verlängern, um sicherzustellen, dass der Optimizer nicht in einem lokalen Minimum des Parameterraums steckengeblieben ist. Aber es sieht ziemlich konvergiert aus. Lass uns die Bilder, die nicht korrekt klassifiziert wurden, genauer betrachten und versuchen zu verstehen, was passiert.
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)
Hier sehen wir, dass die große Mehrheit der falsch klassifizierten Bilder eine vertikale Linie aufweist. Irgendetwas an unserem Modell schafft es nicht, Informationen darüber zu erfassen. Du hast das vielleicht schon beim ersten Variational Circuit geahnt. Lass uns ihn genauer betrachten.
Das Modell verbessern
Schritt 1 erneut betrachtet
Bei der Abbildung unseres Problems auf einen Quantum Circuit hätten wir explizit darüber nachdenken sollen, wie die Information in benachbarten Pixeln die Klasse bestimmt. Um horizontale Linien zu erkennen, wollen wir für alle Pixel einer Zeile wissen: „Wenn Pixel gelb ist, ist dann Pixel auch gelb?" Wir wollen auch über vertikale Linien Bescheid wissen. Da die Klassifizierung jedoch binär ist, könnte man sich vorstellen, einfach zu sagen: Wenn eine solche horizontale Linie nicht erkannt wird, handelt es sich um eine vertikale Linie. Unser vorheriger Variational Circuit enthielt CNOT Gates zwischen Qubits (und damit Pixeln) 0 und 1, 1 und 2 sowie 2 und 3. Das deckt alle horizontalen Linien im oberen Bereich des Bildes ab, erkennt jedoch weder direkt vertikale Linien noch vollständig horizontale Linien, da die untere Zeile ignoriert wird. Um alle horizontalen Linien vollständig zu erkennen, wären ähnliche CNOT Gates zwischen Qubits (Pixeln) 4 und 5, 5 und 6 sowie 6 und 7 nötig. Wir könnten auch in Betracht ziehen, CNOT Gates zwischen Qubits, die vertikalen Linien entsprechen (z. B. 0 und 4 oder 2 und 6), hinzuzufügen. Wir prüfen jedoch zunächst, ob es ausreicht zu erkennen, ob eine horizontale Linie vorhanden ist oder nicht.
# 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
Wir haben die Tiefe des Circuits nicht erhöht. Lass uns sehen, ob wir seine Fähigkeit verbessert haben, unsere Bilder zu modellieren.
Schritt 2 erneut betrachtet
Wir müssen diesen neuen Circuit für die Ausführung auf einem echten Quanten-Backend transpilieren. Lass uns diesen Schritt vorerst überspringen, um zu prüfen, ob unsere Überarbeitung des Variational Circuits die gewünschte Wirkung auf Simulatoren hat. Wir gehen im nächsten Unterabschnitt tiefer auf die Transpilierung ein.
Schritt 3 erneut betrachtet
Wir wenden nun das aktualisierte Modell auf unsere Trainingsdaten an.
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
Schritt 4 erneut betrachtet
Lass uns zunächst prüfen, ob unser Optimizer vollständig konvergiert ist.
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()
Dies sieht nicht vollständig konvergiert aus, da die Verlustfunktion nicht über viele Schritte hinweg annähernd konstant geblieben ist. Aber die Verlustfunktion liegt bereits ca. 60 % niedriger als beim vorherigen Variational Circuit. Handelte es sich um ein Forschungsprojekt, würden wir vollständige Konvergenz sicherstellen wollen. Für die Zwecke dieser Erkundung ist das jedoch ausreichend. Lass uns die Genauigkeit auf unseren Trainings- und Testdaten überprüfen.
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\%$ accuracy on both sets! Our suspicion about accurate detection of horizontal lines being sufficient was correct! Further, our mapping from required information about the pixels to the CNOT gates in the quantum circuit was effective. Let's now look at how this process scales for running on real quantum computers.
## Scaling and running on real quantum computers
### Data
Let us begin by increasing the size of our images. There is nothing special about the choice of a 6x6 grid, except that it exceeds the number of qubits (32) that we can simulate for circuits using non-Clifford gates.
```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)
Da Quantencomputerzeit eine wertvolle Ressource ist, verwenden wir ein sehr kleines Trainings-Set und sehr wenige Optimierungsschritte. Das reicht aus, um den Arbeitsablauf zu demonstrieren.
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)
Schritt 1: Das Problem auf einen Quantum Circuit abbilden
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
Das ist eine vernünftige Zwei-Qubit-Tiefe. Wir sollten in der Lage sein, hochwertige Ergebnisse von einem echten Quantencomputer zu erhalten.
# 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
Da wir die z_feature_map verwenden, die keine CNOT-Gates enthält, erhöht das Hinzufügen der Kodierungsschicht unsere Zwei-Qubit-Tiefe nicht. Wir können den vollständigen Circuit hier visualisieren.
full_circuit.decompose().draw("mpl", style="clifford", idle_wires=False, fold=-1)

Du wirst vielleicht feststellen, dass wir die Zwei-Qubit-Tiefe tatsächlich etwas reduzieren könnten, wenn ihre Minimierung von höchster Bedeutung wäre, indem wir die Reihenfolge der CNOTs ändern. Zum Beispiel könnten die CNOTs auf und im obigen Circuit-Diagramm nach links verschoben werden und direkt unterhalb der CNOTs auf und platziert werden. Bei einer Zwei-Qubit-Gate-Tiefe von 5 ist es nicht offensichtlich, dass dies nach der Transpilierung einen Unterschied macht, aber es ist etwas, das man im Hinterkopf behalten sollte. Wenn die Reihenfolge der CNOT-Gates für die logische Modellierung des vorliegenden Problems wichtig ist, ist die Tiefe hier in Ordnung. Wenn die Reihenfolge der CNOTs für die Modellierung der Datenstruktur in unseren Bildern nicht entscheidend ist, könnten wir ein Skript schreiben, um diese CNOT-Gates neu zu ordnen und die Tiefe zu minimieren.
Wir müssen außerdem unsere Observable für unsere größeren Bilder neu definieren:
from qiskit.quantum_info import SparsePauliOp
observable = SparsePauliOp.from_list([("Z" * (num_qubits), 1)])
Qiskit Patterns Schritt 2: Problem für die Quantenausführung optimieren
Wir beginnen damit, ein Backend für die Ausführung auszuwählen. In diesem Fall verwenden wir das am wenigsten ausgelastete Backend.
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
Wir definieren erneut einen Pass Manager mit Optimierungsstufe 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,
),
]
)
Jetzt werden wir den Pass Manager mehrmals anwenden. Bei sehr breiten oder sehr tiefen Circuits kann es große Schwankungen in der transpilierten Zwei-Qubit-Tiefe geben. Bei solchen Circuits ist es wichtig, den Pass Manager viele Male auszuprobieren und das beste (flachste) Ergebnis zu verwenden.
# 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]
Wir sehen, dass die transpilierte Zwei-Qubit-Tiefe in diesem Fall immer 10 betrug. Es gab geringfügige Schwankungen in der Einzelqubit-Tiefe, und wir werden die flachste davon verwenden. Bei diesem 36-Qubit-Circuit ist das jedoch keine entscheidende Verbesserung. Wir können diesen transpilierten Circuit visualisieren, obwohl er in diesem Maßstab visuell immer schwieriger zu lesen wird.
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 Schritt 3: Ausführung mit Qiskit Primitives
Um die auf echten Quantencomputern verwendete Zeit zu begrenzen, führen wir hier nur wenige Optimierungsschritte durch und arbeiten mit einem sehr kleinen Trainings-Set. Die Skalierung auf mehr Optimierungsschritte und größere Testdaten-Sets sollte jedoch anhand der Erläuterungen in dieser Lektion klar erkennbar sein.
# 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()
Es wird empfohlen, die aus dieser Berechnung zurückgegebenen Gewichtsparameter zu speichern, falls du weitere Iterationen durchführen möchtest.
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])
Wir können diese ersten paar Optimierungsschritte darstellen, obwohl wir nach nur wenigen Optimierungsschritten keine Konvergenz erwarten würden. Diese Kurven waren für die ersten paar Schritte relativ flach, selbst bei der Verwendung von Simulatoren. Wir sollten jedoch beachten, dass die Optimierung derzeit 72 freie Parameter hat. Diese Anzahl kann ohne Qualitätseinbußen um mindestens einen Faktor 2–3 reduziert werden, zum Beispiel durch Parametrisierung von Qubits mit Daten, die einer Teilmenge vollständiger Zeilen und Spalten entsprechen. Tatsächlich sollte der Parameterraum reduziert werden, bevor weitere Quantencomputerzeit für die Minimierung der Verlustfunktion aufgewendet wird.
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()
Abschluss
Zur Zusammenfassung haben wir in dieser Lektion den Arbeitsablauf für die binäre Klassifizierung von Bildern mithilfe eines quantenneuronalen Netzes gelernt. Einige wesentliche Überlegungen bei jedem Qiskit Patterns-Schritt waren:
Schritt 1: Das Problem auf einen Quantum Circuit abbilden
- Trainingsdaten laden. Dies kann „von Hand" oder mithilfe einer vorgefertigten Feature Map wie
z_feature_maperfolgen. - Einen Ansatz konstruieren, der Rotations- und Verschränkungsschichten enthält, die für das jeweilige Problem geeignet sind.
- Die Circuit-Tiefe überwachen, um qualitativ hochwertige Ergebnisse auf Quantencomputern sicherzustellen.
Schritt 2: Problem für die Quantenausführung optimieren
- Ein Backend auswählen, häufig das am wenigsten ausgelastete.
- Einen Pass Manager verwenden, um sowohl den Circuit als auch die Observablen auf die Architektur des gewählten Backends zu transpilieren.
- Bei sehr tiefen oder breiten Circuits mehrfach transpilieren und den flachsten Circuit auswählen.
Schritt 3: Ausführung mit Qiskit (Runtime) Primitives
- Vorläufige Tests auf Simulatoren durchführen, um den Ansatz zu debuggen und zu optimieren.
- Auf einem IBM®-Quantencomputer ausführen.
Schritt 4: Nachbearbeitung, Ergebnis im klassischen Format zurückgeben
- Modellgenauigkeit auf Trainings- und Testdaten berechnen.
- Konvergenz der klassischen Optimierung überwachen.