Quantenkerne
Einführung in Quantenkernel
Der Begriff „Quantenkernel-Methode“ bezeichnet jede Methode, bei der Quantencomputer zur Schätzung eines Kernels eingesetzt werden. In diesem Zusammenhang bezieht sich der Begriff „Kern“ auf die Kernmatrix oder einzelne Einträge darin. Zur Erinnerung: Eine Merkmalsabbildung ist eine Abbildung von auf , wobei in der Regel gilt und das Ziel dieser Abbildung darin besteht, die Datenkategorien durch eine Hyperfläche trennbar zu machen. Die Kernel-Funktion nimmt Vektoren im Feature-Map-Raum als Argumente entgegen und gibt deren Skalarprodukt zurück, d. h. mit . Klassischerweise interessieren uns Feature-Maps, für die sich die Kernel-Funktion leicht berechnen lässt. Dies bedeutet oft, eine Kernel-Funktion zu finden, für die sich das Skalarprodukt im Raum der Merkmalsabbildung als Funktion der ursprünglichen Datenvektoren darstellen lässt, ohne dass jemals und konstruiert werden müssen. Bei der Methode der Quantenkerne erfolgt die Merkmalsabbildung durch einen Quantenschaltkreis, und der Kernel wird anhand von Messungen an diesem Schaltkreis und den relativen Messwahrscheinlichkeiten geschätzt.
In dieser Lektion werden wir die Tiefe von vorcodierten Verschlüsselungsschaltungen untersuchen, die eine erhebliche Verschränkung verwenden, und diese mit der Tiefe von Schaltungen vergleichen, die wir von Hand codieren. Es geht nicht darum, eine Methode gegenüber einer anderen zu bevorzugen. Es kann sein, dass die vorcodierten Schaltungen zu tief sind und die Verschränkung in der selbstgebauten Schaltung nicht ausreicht, um nützlich zu sein. Auch diese werden nur angezeigt, damit Sie sie erforschen können.
Bevor wir eine Kernelmatrix-Schätzung im Detail durchgehen, wollen wir den Arbeitsablauf in der Sprache der Qiskit-Muster skizzieren.
Schritt 1: Klassische Eingaben einem Quantenproblem zuordnen
- Eingabe: Trainingsdatensatz
- Ausgabe: Abstrakte Schaltung zur Berechnung eines Kernelmatrixeintrags
Bei einem Datensatz besteht der Ausgangspunkt darin, die Daten in einen Quantenschaltkreis zu kodieren. Mit anderen Worten: Wir müssen unsere Daten auf den Hilbert-Raum der Zustände unseres Quantencomputers abbilden. Wir tun dies, indem wir eine datenabhängige Schaltung konstruieren. Es gibt viele Möglichkeiten, dies zu tun, und in der vorangegangenen Lektion wurde eine Reihe von Optionen vorgestellt. Sie können einen eigenen Schaltkreis zur Kodierung Ihrer Daten erstellen oder eine vorgefertigte Feature Map wie zz_feature_map verwenden. In dieser Lektion werden wir beides tun.
Um ein einzelnes Element der Kernelmatrix zu berechnen, müssen zwei verschiedene Punkte kodiert werden, damit wir ihr inneres Produkt schätzen können. Ein vollständiger Quantenkernel-Workflow umfasst natürlich viele solcher inneren Produkte zwischen abgebildeten Datenvektoren sowie klassische maschinelle Lernmethoden. Der wichtigste Schritt bei der Iteration ist jedoch die Schätzung eines einzelnen Kernelmatrixelements. Dazu wählen wir eine datenabhängige Quantenschaltung und bilden zwei Datenvektoren im Merkmalsraum ab.
Für die Aufgabe, eine Kernelmatrix zu erzeugen, sind wir besonders an der Wahrscheinlichkeit interessiert, den Zustand zu messen, in dem sich alle Qubits im Zustand befinden. Zur Veranschaulichung: Die Schaltung, die für die Kodierung und Abbildung eines Datenvektors verantwortlich ist, kann als geschrieben werden, und die Schaltung, die für die Kodierung und Abbildung von verantwortlich ist, ist , und bezeichnet die abgebildeten Zustände
Diese Zustände sind die Abbildung der Daten auf höhere Dimensionen, so dass unser gewünschter Kernel-Eintrag das innere Produkt ist
Wenn wir den Standardausgangszustand mit den beiden Schaltkreisen und bearbeiten, ist die Wahrscheinlichkeit, dass wir den Zustand messen, gleich
Dies ist genau der Wert, den wir wollen (bis zu ). Die Messschicht unserer Schaltung liefert Messwahrscheinlichkeiten (oder so genannte "Quasi-Wahrscheinlichkeiten", wenn bestimmte Methoden zur Fehlerbegrenzung verwendet werden). Die interessierende Wahrscheinlichkeit ist die des Nullzustandes, .
Schritt 2: Optimierung des Problems für die Quantenausführung
- Eingabe: Abstrakte Schaltung, nicht optimiert für ein bestimmtes Backend
- Ausgabe: Zielschaltung und Observable, optimiert für die ausgewählte QPU
In diesem Schritt werden wir die Funktion generate_preset_pass_manager von Qiskit verwenden, um eine Optimierungsroutine für unsere Schaltung in Bezug auf den realen Quantencomputer zu spezifizieren, auf dem wir das Experiment durchführen wollen. Wir stellen optimization_level=3 ein, was bedeutet, dass wir den voreingestellten Passmanager verwenden werden, der den höchsten Optimierungsgrad bietet. In diesem Zusammenhang bezieht sich der Begriff "Optimierung" auf die Optimierung der Implementierung des Schaltkreises auf einem echten Quantencomputer. Dazu gehören Überlegungen wie die Auswahl von physischen Qubits, die den Qubits im abstrakten Quantenschaltkreis entsprechen, um die Gattertiefe zu minimieren, oder die Auswahl von physischen Qubits mit den niedrigsten verfügbaren Fehlerraten. Dies steht nicht in direktem Zusammenhang mit der Optimierung des maschinellen Lernproblems (wie bei klassischen Optimierern wie COBYLA).
Je nachdem, wie Sie Schritt 2 umsetzen, müssen Sie die Schaltung möglicherweise mehr als einmal optimieren, da jedes an einem Matrixelement beteiligte Punktepaar eine andere zu messende Schaltung ergibt.
Schritt 3: Ausführung mithilfe von „ IBM Quantum “-Primitiven
- Eingabe: Zielschaltung
- Ausgabe: Wahrscheinlichkeitsverteilung
Verwenden Sie die Primitive Sampler „ IBM Quantum “, um eine Wahrscheinlichkeitsverteilung der Zustände zu rekonstruieren, die sich aus der Abtastung der Schaltung ergibt. Beachten Sie, dass dieser Begriff manchmal auch als „Quasi-Wahrscheinlichkeitsverteilung“ bezeichnet wird. Dieser Begriff findet Anwendung, wenn Rauschen eine Rolle spielt und wenn zusätzliche Schritte eingeführt werden, beispielsweise zur Fehlerminderung. In solchen Fällen ergibt die Summe aller Wahrscheinlichkeiten möglicherweise nicht genau 1; daher der Begriff „Quasi-Wahrscheinlichkeit“.
Schritt 4: Nachbearbeitung, Ergebnis im klassischen Format zurückgeben
- Eingabe: Wahrscheinlichkeitsverteilung
- Ausgabe: Ein einzelnes Kernelmatrixelement oder eine Kernelmatrix bei Wiederholung
Berechnen Sie die Wahrscheinlichkeit der Messung von auf der Quantenschaltung und füllen Sie die Kernelmatrix an der Stelle auf, die den beiden verwendeten Datenvektoren entspricht. Um die gesamte Kernelmatrix auszufüllen, müssen wir für jeden Eintrag ein Quantenexperiment durchführen. Sobald wir eine Kernelmatrix haben, können wir sie in vielen klassischen Algorithmen für maschinelles Lernen verwenden, die pre-calculated kernels akzeptieren. Beispiel: qml_svc = SVC(kernel="precomputed"). Wir können dann klassische Workstreams verwenden, um unser Modell auf unsere Testdaten anzuwenden und eine Genauigkeitsbewertung zu erhalten. Je nachdem, wie zufrieden wir mit unserer Genauigkeitsbewertung sind, müssen wir möglicherweise einige Aspekte unserer Berechnung überarbeiten, z. B. unsere Merkmalskarte.
Unterrichtsplan
In dieser Lektion werden wir diese Schritte auf verschiedene Arten durchführen, um Ihre Zeit auf echten Quantencomputern optimal zu nutzen. Wir werden eine Quantenkernmethode anwenden, um
- Ein einziger Kernelmatrixeintrag für Daten mit relativ wenigen Merkmalen, der ein echtes Backend verwendet, so dass wir leicht nachvollziehen können, was bei jedem Schritt geschieht.
- Ein ganzer Datensatz mit relativ wenigen Merkmalen unter Verwendung eines simulierten Backends, damit wir sehen können, wie sich der Quanten-Workstream mit klassischen maschinellen Lernmethoden verbindet
- Ein einziger Kernelmatrixeintrag für Daten mit vielen Merkmalen unter Verwendung eines echten Quantencomputers. Wir werden nicht die gesamte Kernelmatrix für einen großen Datensatz schätzen, um die Zeit auf IBM® Quantencomputern zu respektieren.
# If you have not already, install scikit learn
#!pip install scikit-learnEinzelner Kernel-Matrixeintrag
Schritt 1: Klassische Eingaben einem Quantenproblem zuordnen
Betrachten wir zunächst einen Datensatz mit nur wenigen Merkmalen, sagen wir 10. Der Datensatz kann beliebig groß sein, da wir die Elemente der Kernelmatrix einzeln berechnen. Wir brauchen mindestens zwei Punkte, also fangen wir damit an (im nächsten Beispiel werden wir einen vollständigen Datensatz importieren). Lassen Sie uns einige benötigte Pakete importieren:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# Two mock data points, including category labels, as in training
small_data = [
[-0.194, 0.114, -0.006, 0.301, -0.359, -0.088, -0.156, 0.342, -0.016, 0.143, 1],
[-0.1, 0.002, 0.244, 0.127, -0.064, -0.086, 0.072, 0.043, -0.053, 0.02, -1],
]
# Data points with labels removed, for inner product
train_data = [small_data[0][:-1], small_data[1][:-1]]Wir können versuchen, die z_feature_map zu verwenden.
# from qiskit.circuit.library import zz_feature_map
# fm = zz_feature_map(feature_dimension=np.shape(train_data)[1], entanglement='linear', reps=1)
from qiskit.circuit.library import z_feature_map
fm = z_feature_map(feature_dimension=np.shape(train_data)[1])
unitary1 = fm.assign_parameters(train_data[0])
unitary2 = fm.assign_parameters(train_data[1])Die beiden oben genannten Einheiten entsprechen genau den in der Einleitung beschriebenen und . Wir können sie mit unitary_overlap kombinieren. Wie immer wollen wir die Tiefe unseres Kreislaufs im Auge behalten.
from qiskit.circuit.library import unitary_overlap
overlap_circ = unitary_overlap(unitary1, unitary2)
overlap_circ.measure_all()
print("circuit depth = ", overlap_circ.decompose().depth())
overlap_circ.decompose().draw("mpl", scale=0.6, style="iqp")Output:
circuit depth = 9
Schritt 2: Optimierung des Problems für die Quantenausführung
Wir beginnen mit der Auswahl des am wenigsten ausgelasteten Backends und optimieren dann unsere Schaltung für die Ausführung auf diesem Backend.
# Import needed packages
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService
# Get the least busy backend
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=fm.num_qubits
)
print(backend)Output:
<IBMBackend('ibm_brisbane')>
# Apply level 3 optimization to our overlap circuit
pm = generate_preset_pass_manager(optimization_level=3, backend=backend)
overlap_ibm = pm.run(overlap_circ)Bei komplizierten Schaltkreisen erhöht dieser Schritt die Tiefe des Schaltkreises erheblich, da er sich auf native Gatter für echte Quantencomputer bezieht und Informationen möglicherweise von Qubit zu Qubit verschoben werden müssen. In diesem einfachen Fall wird die Tiefe kaum beeinflusst.
print("circuit depth = ", overlap_ibm.decompose().depth())
overlap_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)Output:
circuit depth = 10
1
Schritt 3: Ausführung mithilfe von „ IBM Quantum “-Primitiven
Die Syntax für die Ausführung auf einem Simulator ist unten auskommentiert. Für diesen Datensatz mit einer geringen Anzahl von Merkmalen ist die Ausführung auf einem Simulator immer noch eine Option. Für Berechnungen im Versorgungsmaßstab ist eine Simulation in der Regel nicht durchführbar. Simulatoren sollten nur zum Debuggen von verkleinertem Code verwendet werden.
# Run this for a simulator
# from qiskit.primitives import StatevectorSampler
# from qiskit_ibm_runtime import Options, Session, Sampler
# num_shots = 10000
# Evaluate the problem using state vector-based primitives from Qiskit
# sampler = StatevectorSampler()
# results = sampler.run([overlap_circ], shots=num_shots).result()
# .get_counts() returns counts associated with a state labeled by bit results
# such as |001101...01>.
# counts_bit = results[0].data.meas.get_counts()
# .get_int_counts returns the same counts, but labeled by integer equivalent
# of the above bit string.
# counts = results[0].data.meas.get_int_counts()# Benchmarked on an Eagle processor, 7-11-24, took 4 sec.
# Import the IBM Quantum primitive
from qiskit_ibm_runtime import Session, SamplerV2 as Sampler
num_shots = 10000
# Use sampler and get the counts
sampler = Sampler(mode=backend)
results = sampler.run([overlap_ibm], shots=num_shots).result()
# .get_counts() returns counts associated with a state labeled by bit results such as |001101...01>.
counts_bit = results[0].data.meas.get_counts()
# .get_int_counts returns the same counts, but labeled by integer equivalent
# of the above bit string.
counts = results[0].data.meas.get_int_counts()Schritt 4: Nachbearbeitung, Ergebnis im klassischen Format zurückgeben
Wie in der Einleitung beschrieben, ist die nützlichste Messung hier die Wahrscheinlichkeit, den Nullzustand zu messen .
counts.get(0, 0.0) / num_shotsOutput:
0.6525
Das ist das gewünschte Ergebnis: eine Schätzung des inneren Produkts (bis zum modifizierten Quadrat) der Vektoren, die zwei Datenpunkten entsprechen. Wenn wir uns die vollständige Verteilung der Messwahrscheinlichkeiten (oder Quasi-Wahrscheinlichkeiten) ansehen wollen, können wir dies mit der Funktion plot_distribution wie unten gezeigt tun. Man sieht, dass solche Bilder für eine große Anzahl von Qubits schnell unlösbar werden.
from qiskit.visualization import plot_distribution
plot_distribution(counts_bit)Output:
Alternativ könnte man auch eine Visualisierung wie die folgende definieren, um nur die 10 wahrscheinlichsten Messungen zu betrachten. Dies könnte für die Fehlersuche wichtig sein oder um ein besseres Gespür für die Daten zu bekommen. Aber die Messwahrscheinlichkeit des Nullzustands ist unser Kernelmatrixelement.
def visualize_counts(probs, num_qubits):
"""Visualize the outputs from the Qiskit Sampler primitive."""
zero_prob = probs.get(0, 0.0)
top_10 = dict(sorted(probs.items(), key=lambda item: item[1], reverse=True)[:10])
top_10.update({0: zero_prob})
by_key = dict(sorted(top_10.items(), key=lambda item: item[0]))
xvals, yvals = list(zip(*by_key.items()))
xvals = [bin(xval)[2:].zfill(num_qubits) for xval in xvals]
plt.bar(xvals, yvals)
plt.xticks(rotation=75)
plt.title("Results of sampling")
plt.xlabel("Measured bitstring")
plt.ylabel("Counts")
plt.show()
visualize_counts(counts, overlap_circ.num_qubits)Output:
Aus dieser Information über nur ein inneres Produkt zwischen zwei Datenpunkten im höherdimensionalen Merkmalsraum können wir nur schließen, dass ihre Überlappung im Vergleich zur maximalen Überlappung (die 1.0 wäre) ziemlich groß ist. Dies könnte ein Indikator dafür sein, dass diese beiden Datenpunkte in gewisser Weise ähnlich sind und in dieselben Klassen eingeteilt werden. Es könnte aber auch ein Indikator dafür sein, dass unsere Merkmalskarte nicht in der Lage ist, einen Raum abzubilden, in dem ähnliche Daten eine starke Überlappung und ungleiche Daten eine geringe Überlappung aufweisen. Um herauszufinden, was davon zutrifft, müssen wir unsere Merkmalskarte auf den gesamten Datensatz anwenden und sehen, ob die sich daraus ergebende Kernelmatrix so manipuliert werden kann, dass die Klassen mit hoher Genauigkeit getrennt werden können.
Es ist erwähnenswert, dass wir die z_feature_map verwendet haben, die zu einer geringen Zwei-Qubit-Transpilierungstiefe führt (Tiefe 1, in der Tat). Wenn Ihre Schaltkreise zu tief werden, führt dies mit Sicherheit zu starkem Rauschen, so dass die Wahrscheinlichkeit, den Nullzustand zu messen, sehr gering ist, selbst wenn Ihre Merkmalskarte gut an Ihre Daten angepasst ist. Eine Wiederholung des obigen Prozesses mit zz_feature_map und , entanglement='linear', reps=1 ergab beispielsweise dist.get(0,0.0) = 0.0015 mit denselben Datenpunkten. Das liegt an den viel größeren Schaltungstiefen und Zwei-Qubit-Tiefen von zz_feature_map. Die folgende Abbildung zeigt die Wahrscheinlichkeitsverteilung für diese Berechnung.
Es lohnt sich, mit einigen Datenpunkten aus derselben Kategorie zu spielen, um zu sehen, wie niedrig die Tiefe sein muss, um gute Ergebnisse zu erzielen. Die folgenden Hinweise sind grobe Ratschläge, von denen es sicher Ausnahmen gibt. Im Allgemeinen sollte eine transpilierte Zwei-Qubit-Tiefe von 10 oder weniger kein Problem darstellen. Eine transpilierte Zwei-Qubit-Tiefe von 50-60 ist Stand der Technik und erfordert unter anderem eine fortgeschrittene Fehlerminderung. Dazwischen können Ihre Ergebnisse je nach Datenähnlichkeit, Ausdruckskraft der Feature-Maps, Schaltkreisbreite und anderen Faktoren variieren.
Normalerweise würde der Nachbearbeitungsschritt auch klassische maschinelle Lernverfahren umfassen. Im nächsten Abschnitt werden wir diesen Prozess auf einen ganzen Datensatz ausweiten und den klassischen Arbeitsablauf des maschinellen Lernens darstellen.
Überprüfen Sie Ihr Verständnis
Wie viele verschiedene Zustände können in einem 10-Qubit-Quantenschaltkreis überhaupt gemessen werden?
oder 1024.
Nehmen wir einmal an, jemand, der sich mit Quantencomputern noch nicht auskennt, versucht, eine Quantenschaltung mit sehr hoher Zwei-Qubit-Tiefe zu verwenden, ohne dabei Fehlerminderungsmaßnahmen anzuwenden. Nehmen wir weiter an, dass dies zu einer Fehlerrate von 10 % bei jedem Qubit führt. Wenn das tatsächliche (fehlerfreie) Kernel-Matrixelement, das diesem Schaltkreis entspricht, sehr groß ist, beispielsweise 1.0, wie hoch wäre dann die Wahrscheinlichkeit, dass alle 10 Qubits in dem Zustand gemessen werden, in dem jedes Qubit den Wert annimmt?
Die Wahrscheinlichkeit, dass jedes Qubit korrekt im Zustand |0> gefunden wird, ist 0.90. Die Wahrscheinlichkeit, dass alle 10 Qubits im richtigen Zustand gefunden werden, beträgt oder etwa 35 %.
Erläutern Sie mit eigenen Worten, warum es so wichtig ist, die Tiefe der Stromkreise zu überwachen. Dies gilt allgemein, aber erklären Sie es im Zusammenhang mit der Quanten-Kernel-Schätzung.
In diesem QKE-Arbeitsablauf basieren unsere Schätzungen auf den Messungen des Nullzustands, d. h. des Zustands, in dem sich jedes Qubit im Zustand befindet. Sehr tiefe Schaltungen führen zu hohen Fehlerquoten. Wenn sich diese Fehlerrate über viele Qubits summiert, verringert sich die Wahrscheinlichkeit, den Nullzustand zu messen, erheblich.
Vollständige Kernelmatrix
In diesem Abschnitt wird das obige Verfahren auf die binäre Klassifizierung eines vollständigen Datensatzes ausgedehnt. Dadurch werden zwei wichtige Komponenten eingeführt: (1) wir können nun klassisches maschinelles Lernen im Post-Processing implementieren und (2) wir können Genauigkeitswerte für unser Training erhalten.
Schritt 1: Klassische Eingaben einem Quantenproblem zuordnen
Nun importieren wir einen bestehenden Datensatz für unsere Klassifizierung. Dieser Datensatz besteht aus 128 Zeilen (Datenpunkten) und 14 Merkmalen für jeden Punkt. Es gibt ein 15. Element, das die binäre Kategorie jedes Punktes angibt ( ). Der Datensatz wird nachstehend importiert, oder Sie können hier auf den Datensatz zugreifen und seine Struktur einsehen.
Wir verwenden die ersten 90 Datenpunkte zum Training und die nächsten 30 Punkte zum Testen.
!wget https://raw.githubusercontent.com/qiskit-community/prototype-quantum-kernel-training/main/data/dataset_graph7.csv
df = pd.read_csv("dataset_graph7.csv", sep=",", header=None)
# Prepare training data
train_size = 90
X_train = df.values[0:train_size, :-1]
train_labels = df.values[0:train_size, -1]
# Prepare testing data
test_size = 30
X_test = df.values[train_size : train_size + test_size, :-1]
test_labels = df.values[train_size : train_size + test_size, -1]Output:
--2024-07-11 23:05:22-- https://raw.githubusercontent.com/qiskit-community/prototype-quantum-kernel-training/main/data/dataset_graph7.csv
Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.110.133, 185.199.111.133, 185.199.109.133, ...
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.110.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 49405 (48K) [text/plain]
Saving to: ‘dataset_graph7.csv.15’
dataset_graph7.csv. 100%[===================>] 48.25K --.-KB/s in 0.02s
2024-07-11 23:05:23 (2.11 MB/s) - ‘dataset_graph7.csv.15’ saved [49405/49405]
Wir bereiten uns bereits auf die Speicherung mehrerer Ausgaben vor, indem wir eine Kernelmatrix und eine Testmatrix mit geeigneten Dimensionen konstruieren.
# Empty kernel matrix
num_samples = np.shape(X_train)[0]
kernel_matrix = np.full((num_samples, num_samples), np.nan)
test_matrix = np.full((test_size, num_samples), np.nan)Nun erstellen wir eine Feature-Map, um unsere klassischen Daten in einem Quantenschaltkreis zu kodieren und abzubilden. Es steht uns frei, unsere eigene Feature Map zu erstellen oder eine vorgefertigte zu verwenden. Sie können die nachstehende Feature-Map gerne ändern oder zurück zu ZFeatureMap wechseln. Achten Sie aber immer auf die Schaltungstiefe. Erinnern Sie sich daran, dass im vorherigen 6-Qubit-Beispiel die Tiefe der transpilierten Schaltkreise bei Verwendung von zz_feature_map unüberwindbar hoch war. Mit zunehmender Größe und Komplexität der Schaltung könnte die Tiefe schnell so weit ansteigen, dass das Rauschen unsere Ergebnisse überlagert. Wenn Sie etwas über Ihre Datenstruktur wissen, das Aufschluss darüber gibt, welche Feature-Map-Struktur am nützlichsten ist, ist es ratsam, Ihre eigene benutzerdefinierte Feature-Map zu erstellen, die dieses Wissen nutzt.
from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit
# Prepare feature map for computing overlap
num_features = np.shape(X_train)[1]
num_qubits = int(num_features / 2)
# To use a custom feature map use the lines below.
entangler_map = [[0, 2], [3, 4], [2, 5], [1, 4], [2, 3], [4, 6]]
fm = QuantumCircuit(num_qubits)
training_param = Parameter("θ")
feature_params = ParameterVector("x", num_qubits * 2)
fm.ry(training_param, fm.qubits)
for cz in entangler_map:
fm.cz(cz[0], cz[1])
for i in range(num_qubits):
fm.rz(-2 * feature_params[2 * i + 1], i)
fm.rx(-2 * feature_params[2 * i], i)Schritte 2 und 3: Problem optimieren und mit Primitiven ausführen
Wir werden eine Überlappungsschaltung konstruieren, und wenn wir in diesem Beispiel auf einem echten Quantencomputer arbeiten würden, würden wir sie wie zuvor für die Ausführung optimieren. In diesem Fall beabsichtigen wir jedoch, über alle Datenpunkte zu gehen und die vollständige Kernelmatrix zu berechnen. Für jedes Paar von Datenvektoren und erstellen wir eine andere Überlappungsschaltung. Wir müssen also unsere Schaltung für jedes Datenpunktpaar optimieren. Die Schritte 2 und 3 werden also in mehreren Iterationen zusammen durchgeführt.
Die nachstehende Codezelle führt genau denselben Prozess wie zuvor für ein einzelnes Datenpunktpaar aus. Diesmal wird sie einfach innerhalb von zwei for -Schleifen ausgeführt, und am Ende befindet sich die zusätzliche Zeile kernel_matrix[x_1,x_2] = ... , in der die Ergebnisse der einzelnen Berechnungen gespeichert werden. Beachten Sie, dass wir die Symmetrie einer Kernelmatrix genutzt haben, um die Anzahl der Berechnungen um 1/2 zu reduzieren. Wir haben auch die Diagonalelemente einfach auf 1 gesetzt, so wie sie bei Abwesenheit von Rauschen sein sollten. Je nach Ihrer Implementierung und der erforderlichen Genauigkeit können Sie die Diagonalelemente auch zur Schätzung des Rauschens verwenden oder sich über das Rauschen informieren, um Fehler zu minimieren.
Sobald die Kernelmatrix vollständig ausgefüllt ist, wiederholen wir den Vorgang für die Testdaten und füllen die test_matrix aus. Dies ist eigentlich auch eine Kernelmatrix; wir geben ihr nur einen anderen Namen, um die beiden zu unterscheiden.
# To use a simulator
from qiskit.primitives import StatevectorSampler
# Remember to insert your token in the QiskitRuntimeService constructor
# to use real quantum computers
# service = QiskitRuntimeService()
# backend = service.least_busy(
# operational=True, simulator=False, min_num_qubits=fm.num_qubits
# )
num_shots = 10000
# Evaluate the problem using state vector-based primitives from Qiskit.
sampler = StatevectorSampler()
for x1 in range(0, train_size):
for x2 in range(x1 + 1, train_size):
unitary1 = fm.assign_parameters(list(X_train[x1]) + [np.pi / 2])
unitary2 = fm.assign_parameters(list(X_train[x2]) + [np.pi / 2])
# Create the overlap circuit
overlap_circ = unitary_overlap(unitary1, unitary2)
overlap_circ.measure_all()
# These lines run the qiskit sampler primitive.
counts = (
sampler.run([overlap_circ], shots=num_shots)
.result()[0]
.data.meas.get_int_counts()
)
# Assign the probability of the 0 state to the kernel matrix, and the transposed element
# (since this is an inner product)
kernel_matrix[x1, x2] = counts.get(0, 0.0) / num_shots
kernel_matrix[x2, x1] = counts.get(0, 0.0) / num_shots
# Fill in on-diagonal elements with 1, again, since this is an inner-product corresponding to
# probability (or alter the code to check these entries and verify they yield 1)
kernel_matrix[x1, x1] = 1
print("training done")
# Similar process to above, but for testing data.
for x1 in range(0, test_size):
for x2 in range(0, train_size):
unitary1 = fm.assign_parameters(list(X_test[x1]) + [np.pi / 2])
unitary2 = fm.assign_parameters(list(X_train[x2]) + [np.pi / 2])
# Create the overlap circuit
overlap_circ = unitary_overlap(unitary1, unitary2)
overlap_circ.measure_all()
counts = (
sampler.run([overlap_circ], shots=num_shots)
.result()[0]
.data.meas.get_int_counts()
)
test_matrix[x1, x2] = counts.get(0, 0.0) / num_shots
print("test matrix done")Output:
training done
test matrix done
Schritt 4: Nachbearbeitung, Ergebnis im klassischen Format zurückgeben
Da wir nun eine Kernel-Matrix und eine ähnlich formatierte test_matrix aus Quantenkernel-Methoden haben, können wir klassische Algorithmen des maschinellen Lernens anwenden, um Vorhersagen über unsere Testdaten zu treffen und ihre Genauigkeit zu überprüfen. Wir beginnen mit dem Import von Scikit-Learn's sklearn.svc, einem Support-Vektor-Klassifikator (SVC). Wir müssen angeben, dass der SVC unseren vorberechneten Kernel unter kernel = precomputed verwenden soll.
# import a support vector classifier from a classical ML package.
from sklearn.svm import SVC
# Specify that you want to use a pre-computed kernel matrix
qml_svc = SVC(kernel="precomputed")Mit SVC.fit können wir nun die Kernelmatrix und die Trainingsmarkierungen eingeben, um eine Anpassung zu erhalten. SVC.score bewertet dann unsere Testdaten anhand dieser Anpassung unter Verwendung unserer test_matrix und gibt unsere Genauigkeit zurück.
# Feed in the pre-computed matrix and the labels of the training data.
# The classical algorithm gives you a fit.
qml_svc.fit(kernel_matrix, train_labels)
# Now use the .score to test your data, using the matrix of test data,
# and test labels as your inputs.
qml_score_precomputed_kernel = qml_svc.score(test_matrix, test_labels)
print(f"Precomputed kernel classification test score: {qml_score_precomputed_kernel}")Output:
Precomputed kernel classification test score: 1.0
Wir sehen, dass die Genauigkeit unseres trainierten Modells 100 % beträgt. Das ist großartig und zeigt, dass QKE funktionieren kann. Aber das ist etwas ganz anderes als ein Quantenvorteil. Klassische Kernel wären wahrscheinlich auch in der Lage gewesen, dieses Klassifizierungsproblem mit 100%iger Genauigkeit zu lösen. Es gibt noch viel zu tun, um die verschiedenen Datentypen und Datenbeziehungen zu charakterisieren, um herauszufinden, wo Quantenkerne in der gegenwärtigen Ära der Versorgungswirtschaft am nützlichsten sind. Wir überlassen es den Lernenden, Teile dieses Arbeitsablaufs zu ändern und die Wirksamkeit verschiedener Quantenmerkmalskarten zu untersuchen. Hier sind ein paar Dinge zu beachten:
- Wie zuverlässig ist die Genauigkeit? Gilt dies für alle Arten von Daten oder nur für diese speziellen Trainingsdaten?
- Welche Struktur in Ihren Daten lässt Sie vermuten, dass eine Quantenmerkmalskarte nützlich ist?
- Wie wird die Genauigkeit durch die Erhöhung/Verringerung der Trainingsdatenmenge beeinflusst?
- Welche Feature-Maps können Sie verwenden und wie variieren die Ergebnisse mit Feature-Maps?
- Wie werden die Genauigkeit und die Laufzeit durch die Erhöhung der Anzahl der Merkmale beeinflusst?
- Welche Trends, wenn überhaupt, erwarten Sie bei echten Quantencomputern?
Skalierung auf mehr Funktionen und Qubits
In diesem Abschnitt wiederholen wir die Berechnung eines einzelnen Matrixelements, aber für eine viel größere Anzahl von Merkmalen, und skizzieren den Weg zur Skalierung in Richtung Nutzen. Die Beschränkung auf ein einziges Matrizenelement dient dazu, den Prozess zu veranschaulichen, ohne zu viel Zeit auf Quantencomputern zu verbrauchen.
Schritt 1: Klassische Eingaben einem Quantenproblem zuordnen
Wir gehen von einem Datensatz aus, in dem jeder Datenpunkt 42 Merkmale aufweist. Wie im ersten Beispiel wird ein einzelnes Kernelmatrixelement berechnet, wofür zwei Datenpunkte benötigt werden. Die beiden folgenden Punkte haben 42 Merkmale und eine einzige Kategorievariable ( ).
# Two mock data points, including category labels, as in training
large_data = [
[
-0.028,
-1.49,
-1.698,
0.107,
-1.536,
-1.538,
-1.356,
-1.514,
-0.109,
-1.8,
-0.122,
-1.651,
-1.955,
-0.123,
-1.732,
0.091,
-0.048,
-0.128,
-0.026,
0.082,
-1.263,
0.065,
0.004,
-0.055,
-0.08,
-0.173,
-1.734,
-0.39,
-1.451,
0.078,
-1.578,
-0.025,
-0.184,
-0.119,
-1.336,
0.055,
-0.204,
-1.578,
0.132,
-0.121,
-1.599,
-0.187,
-1,
],
[
-1.414,
-1.439,
-1.606,
0.246,
-1.673,
0.002,
-1.317,
-1.262,
-0.178,
-1.814,
0.013,
-1.619,
-1.86,
-0.25,
-0.212,
-0.214,
-0.033,
0.071,
-0.11,
-1.607,
0.441,
-0.143,
-0.009,
-1.655,
-1.579,
0.381,
-1.86,
-0.079,
-0.088,
-0.058,
-1.481,
-0.064,
-0.065,
-1.507,
0.177,
-0.131,
-0.153,
0.07,
-1.627,
0.593,
-1.547,
-0.16,
-1,
],
]
train_data = [large_data[0][:-1], large_data[1][:-1]]Es sei daran erinnert, dass die zz_feature_map bei relativ wenigen Merkmalen (14 Merkmale) recht tiefe Schaltkreise erzeugt. Wenn wir die Anzahl der Funktionen erhöhen, müssen wir die Schaltungstiefe genau überwachen. Um dies zu veranschaulichen, werden wir zunächst versuchen, die zz_feature_map zu verwenden und die Tiefe der resultierenden Schaltung zu überprüfen.
from qiskit.circuit.library import zz_feature_map
fm = zz_feature_map(
feature_dimension=np.shape(train_data)[1], entanglement="linear", reps=1
)
unitary1 = fm.assign_parameters(train_data[0])
unitary2 = fm.assign_parameters(train_data[1])from qiskit.circuit.library import unitary_overlap
overlap_circ = unitary_overlap(unitary1, unitary2)
overlap_circ.measure_all()
print("circuit depth = ", overlap_circ.decompose(reps=2).depth())
print(
"two-qubit depth",
overlap_circ.decompose().depth(lambda instr: len(instr.qubits) > 1),
)
# overlap_circ.draw("mpl", scale=0.6, style="iqp")Output:
circuit depth = 251
two-qubit depth 165
Wie bereits beschrieben, ist die Frage, wie tief man zu tief ist, sehr differenziert. Aber eine Zwei-Qubit-Tiefe von mehr als 100, selbst vor der Transpilation, ist ein absolutes No-Go. Aus diesem Grund wurde in dieser Lektion der Schwerpunkt auf benutzerdefinierte Feature-Maps gelegt. Wenn Sie etwas über die Struktur Ihres gesamten Datensatzes wissen, sollten Sie eine Verflechtungskarte mit Blick auf diese Struktur entwerfen. Da wir hier nur das innere Produkt zwischen zwei solchen Datenpunkten berechnen, haben wir einer geringen Schaltungstiefe gegenüber einer detaillierten Betrachtung der Datenstruktur den Vorrang gegeben.
from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit
# Prepare feature map for computing overlap
entangler_map = [
[3, 4],
[2, 5],
[1, 4],
[2, 3],
[4, 6],
[7, 9],
[10, 11],
[9, 12],
[8, 11],
[9, 10],
[11, 13],
[14, 16],
[17, 18],
[16, 19],
[15, 18],
[16, 17],
[18, 20],
]# Use the entangler map above to build a feature map
num_features = np.shape(train_data)[1]
num_qubits = int(num_features / 2)
fm = QuantumCircuit(num_qubits)
training_param = Parameter("θ")
feature_params = ParameterVector("x", num_qubits * 2)
fm.ry(training_param, fm.qubits)
for cz in entangler_map:
fm.cz(cz[0], cz[1])
for i in range(num_qubits):
fm.rz(-2 * feature_params[2 * i + 1], i)
fm.rx(-2 * feature_params[2 * i], i)from qiskit.circuit.library import unitary_overlap
# Assign features of each data point to a unitary, an instance of the general feature map.
unitary1 = fm.assign_parameters(list(train_data[0]) + [np.pi / 2])
unitary2 = fm.assign_parameters(list(train_data[1]) + [np.pi / 2])
# Create the overlap circuit
overlap_circ = unitary_overlap(unitary1, unitary2)
overlap_circ.measure_all()Wir machen uns noch nicht die Mühe, die Tiefen zu überprüfen, denn was wirklich zählt, ist die transpilierte Zwei-Qubit-Tiefe.
Schritt 2: Optimierung des Problems für die Quantenausführung
Wir beginnen mit der Auswahl des am wenigsten ausgelasteten Backends und optimieren dann unsere Schaltung für die Ausführung auf diesem Backend.
# Import needed packages
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService
# Get the least busy backend
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=fm.num_qubits
)
print(backend)Output:
<IBMBackend('ibm_brisbane')>
Bei kleineren Aufträgen liefert ein voreingestellter Passmanager oft zuverlässig den gleichen Schaltkreis mit der gleichen Tiefe. Bei sehr großen, komplexen Schaltkreisen kann der Passmanager jedoch bei jedem Durchlauf andere transpilierte Schaltkreise zurückgeben. Dies liegt daran, dass Heuristiken verwendet werden und dass sehr große Schaltungen eine komplizierte Landschaft möglicher Optimierungen aufweisen. Oft ist es sinnvoll, ein paar Mal umzudrehen und die flachste Strecke zu nehmen. Dies führt nur zu einem klassischen Overhead und kann die Ergebnisse des Quantencomputers erheblich verbessern.
Hier transpilieren wir die unitäre Überlappungsschaltung 20 Mal und betrachten die Tiefe der erhaltenen Schaltungen.
# Apply level 3 optimization to our overlap circuit
transpiled_qcs = []
transpiled_depths = []
transpiled_twoqubit_depths = []
for i in range(1, 20):
pm = generate_preset_pass_manager(optimization_level=3, backend=backend)
overlap_ibm = pm.run(overlap_circ)
transpiled_qcs.append(overlap_ibm)
transpiled_depths.append(overlap_ibm.decompose().depth())
transpiled_twoqubit_depths.append(
overlap_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)
)
print("circuit depth = ", overlap_ibm.decompose().depth())Output:
circuit depth = 61
print(transpiled_depths)
print(transpiled_twoqubit_depths)Output:
[61, 60, 60, 69, 60, 60, 60, 65, 60, 60, 69, 61, 77, 77, 65, 60, 60, 77, 61]
[13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13]
Hier können Sie sehen, dass die Gesamttiefe des Gates bei verschiedenen Transpilationsdurchläufen variiert. Unsere Schaltung ist noch nicht tief/weit genug, um Variationen in den transponierten Zwei-Qubit-Tiefen zu sehen. Wir werden die transpiled_qcs[1] verwenden, die eine Tiefe von 60 aufweist, die nur geringfügig niedriger ist als die Tiefe des tiefsten erhaltenen Schaltkreises, die 77 betrug.
overlap_ibm = transpiled_qcs[1]Schritt 3: Ausführung mithilfe von „ IBM Quantum “-Primitiven
Wenn wir uns dem Nutzen annähern, werden Simulatoren nicht mehr nützlich sein. Hier wird nur die Syntax für echte Quantencomputer gezeigt.
# Run on ibm_osaka, 7-12-24, required 22 sec.
# Import our IBM Quantum primitive
from qiskit_ibm_runtime import SamplerV2 as Sampler
# Open a session:
session = Session(backend=backend)
num_shots = 10000
# Use sampler and get the counts
sampler = Sampler(mode=session)
options = sampler.options
options.dynamical_decoupling.enable = True
options.twirling.enable_gates = True
counts = (
sampler.run([overlap_ibm], shots=num_shots).result()[0].data.meas.get_int_counts()
)
# Close session after done
session.close()Schritt 4: Nachbearbeitung, Ergebnis im klassischen Format zurückgeben
Wie in der Einleitung beschrieben, ist die nützlichste Messung hier die Wahrscheinlichkeit, den Nullzustand zu messen .
counts.get(0, 0.0) / num_shotsOutput:
0.0138
Dieses Verfahren für das einzelne Kernelmatrixelement kann für andere Datenpaare in Ihrem Satz wiederholt werden, um die vollständige Kernelmatrix zu erhalten. Die Dimension der Kernelmatrix wird durch die Anzahl der Punkte in Ihren Trainingsdaten bestimmt, nicht durch die Anzahl der Merkmale. Der Rechenaufwand für die Umwandlung der Kernelmatrix in ein Vorhersagemodell skaliert also nicht mit der Anzahl der Merkmale oder Qubits. Selbst bei relativ kleinen Datensätzen mit einer großen Anzahl von Merkmalen müssten die Daten noch mit einer Merkmalskarte abgeglichen werden, die eine effektive Klassifizierung ermöglicht.
Skalierung und zukünftige Arbeiten
Die Kernel-Methode setzt voraus, dass wir die „ “ so genau wie möglich messen. Gate-Fehler und Auslesefehler bedeuten jedoch, dass eine von Null verschiedene Wahrscheinlichkeit besteht, dass ein beliebiges Qubit fälschlicherweise als im Zustand gemessen wird. Selbst unter der stark vereinfachten Annahme, dass die Wahrscheinlichkeit von „ “ gleich „ “ sein sollte, reduziert sich bei vielen Merkmalen, die beispielsweise auf „ “ Bits kodiert sind, die Wahrscheinlichkeit, alle Bits korrekt als „ “ zu messen, auf „ “. Mit zunehmendem „ “ verliert diese Methode zunehmend an Zuverlässigkeit. Die Überwindung dieser Schwierigkeit und die Ausweitung der Kernel-Schätzung auf immer mehr Merkmale ist ein aktueller Forschungsschwerpunkt. Weitere Informationen zu diesem Thema finden Sie in dieser Arbeit von Thanasilp, Wang, Cerezo und Holmes. Wir empfehlen Ihnen, sich damit auseinanderzusetzen, was mit den heutigen Quantencomputern bereits möglich ist, und sich gleichzeitig darauf zu freuen, welche Möglichkeiten sich im Zeitalter der Fehlerkorrektur eröffnen werden.
Prüfen
Die Berechnung eines Quantenkerns umfasst
- berechnung der Einträge der Kernelmatrix unter Verwendung von Paaren von Trainingsdatenpunkten
- kodierung der Daten und deren Abbildung über ein Feature Mapping
- optimierung Ihrer Schaltung für den Betrieb auf echten Quantencomputern / Backends
Der Quantenkern kann dann, wie in dieser Lektion, in klassischen Algorithmen für maschinelles Lernen verwendet werden.
Bei der Verwendung von Quantenkernen sind einige wichtige Dinge zu beachten:
- Ist es wahrscheinlich, dass der Datensatz von Quantenkernel-Methoden profitieren wird?
- Probieren Sie verschiedene Merkmalskarten und Verschränkungsschemata aus.
- Ist die Schaltungstiefe akzeptabel?
- Versuchen Sie, einen Passmanager mehrfach zu verwenden, und nutzen Sie die kleinstmögliche Schaltung.
Quanten-Kernel-Methoden sind potenziell leistungsstarke Werkzeuge, sofern die Datensätze mit quantenbestimmbaren Merkmalen und einer geeigneten Quantenmerkmalskarte richtig aufeinander abgestimmt sind. Um besser zu verstehen, wo Quantenkerne wahrscheinlich nützlich sind, empfehlen wir die Lektüre von Liu, Arunachalam & Temme (2021).