Zum Hauptinhalt springen

Quantenkerne

Einführung in Quantenkerne

Die „Quantenkernel-Methode" bezeichnet jede Methode, die Quantencomputer zur Schätzung eines Kerns verwendet. In diesem Zusammenhang bezeichnet „Kern" die Kernmatrix oder einzelne Einträge darin. Zur Erinnerung: Eine Feature-Abbildung Φ(x)\Phi(\vec{x}) ist eine Abbildung von xRd\vec{x}\in \mathbb{R}^d nach Φ(x)Rd,\Phi(\vec{x})\in \mathbb{R}^{d'}, wobei in der Regel d>dd'>d gilt und das Ziel dieser Abbildung darin besteht, die Datenkategorien durch eine Hyperebene trennbar zu machen. Die Kernfunktion nimmt Vektoren im feature-abgebildeten Raum als Argumente und gibt ihr inneres Produkt zurück, also K:Rd×RdRK:\mathbb{R}^d\times\mathbb{R}^d\rightarrow \mathbb{R} mit K(x,y)=Φ(x)Φ(y)K(x,y) = \langle \Phi(x)|\Phi(y)\rangle. Klassisch sind wir an Feature-Abbildungen interessiert, für die die Kernfunktion leicht auszuwerten ist. Das bedeutet oft, eine Kernfunktion zu finden, bei der das innere Produkt im feature-abgebildeten Raum in Form der ursprünglichen Datenvektoren ausgedrückt werden kann, ohne Φ(x)\Phi(x) und Φ(y)\Phi(y) jemals explizit zu konstruieren. Bei der Quantenkernel-Methode erfolgt die Feature-Abbildung durch einen Quantenschaltkreis, und der Kern wird anhand von Messungen an diesem Schaltkreis sowie der relativen Messwahrscheinlichkeiten geschätzt.

In dieser Lektion untersuchen wir die Tiefen vorgefertigter Encoding-Schaltkreise, die umfangreiches Entanglement nutzen, und vergleichen sie mit den Tiefen von Schaltkreisen, die wir selbst programmieren. Dies ist keine Empfehlung für die eine oder andere Methode. Vielleicht stellst du fest, dass vorgefertigte Schaltkreise zu tief sind und dass das Entanglement im selbst erstellten Schaltkreis für einen nützlichen Einsatz nicht ausreicht. Diese Beispiele dienen allein dazu, deine eigene Erkundung zu ermöglichen.

Bevor wir eine Kernmatrixschätzung im Detail durchgehen, skizzieren wir den Arbeitsablauf mithilfe der Sprache der Qiskit-Muster.

Schritt 1: Klassische Eingaben auf ein Quantenproblem abbilden

  • Eingabe: Trainingsdatensatz
  • Ausgabe: Abstrakter Schaltkreis zur Berechnung eines Kernmatrixeintrags

Ausgehend vom Datensatz besteht der erste Schritt darin, die Daten in einen Quantenschaltkreis zu kodieren. Anders gesagt: Wir müssen unsere Daten in den Hilbertraum der Zustände unseres Quantencomputers abbilden. Das tun wir durch die Konstruktion eines datenabhängigen Schaltkreises. Es gibt viele Möglichkeiten, dies zu tun, und die vorherige Lektion hat eine Reihe von Optionen vorgestellt. Du kannst deinen eigenen Schaltkreis zum Enkodieren deiner Daten bauen oder eine vorgefertigte Feature-Map wie zz_feature_map verwenden. In dieser Lektion werden wir beides tun.

Um ein einzelnes Kernmatrixelement zu berechnen, möchten wir zwei verschiedene Punkte enkodieren, damit wir ihr inneres Produkt schätzen können. Ein vollständiger Quantenkernel-Arbeitsablauf umfasst natürlich viele solcher innerer Produkte zwischen abgebildeten Datenvektoren sowie klassische Machine-Learning-Methoden. Der Kernschritt, der iteriert wird, ist jedoch die Schätzung eines einzelnen Kernmatrixelements. Dafür wählen wir einen datenabhängigen Quantenschaltkreis und bilden zwei Datenvektoren in den Feature-Raum ab.

Classical_Review_background_kernel_circuit

Für die Aufgabe der Erzeugung einer Kernmatrix interessiert uns insbesondere die Wahrscheinlichkeit, den Zustand 0N|0\rangle^{\otimes N} zu messen, bei dem alle NN Qubits im Zustand 0|0\rangle sind. Um das zu verstehen: Der Schaltkreis, der für die Enkodierung und Abbildung eines Datenvektors xi\vec{x}_i zuständig ist, lässt sich als Φ(xi)\Phi(\vec{x}_i) schreiben, und derjenige für xj\vec{x}_j als Φ(xj)\Phi(\vec{x}_j). Die abgebildeten Zustände sind dann

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

Diese Zustände sind die Abbildung der Daten in höhere Dimensionen, also ist unser gewünschter Kerneintrag das innere Produkt

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

Wenn wir auf den standardmäßigen Ausgangszustand 0N|0\rangle^{\otimes N} beide Schaltkreise Φ(xj)\Phi^\dagger(\vec{x}_j) und Φ(xi)\Phi(\vec{x}_i) anwenden, ist die Wahrscheinlichkeit, danach den Zustand 0N|0\rangle^{\otimes N} zu messen,

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

Das ist genau der Wert, den wir suchen (bis auf 2||^2). Die Messschicht unseres Schaltkreises liefert Messwahrscheinlichkeiten (oder sogenannte „Quasi-Wahrscheinlichkeiten", falls bestimmte Fehlerminderungsmethoden verwendet werden). Die interessante Wahrscheinlichkeit ist die des Nullzustands, 0N|0\rangle^{\otimes N}.

Schritt 2: Problem für die Quantenausführung optimieren

  • Eingabe: Abstrakter Schaltkreis, nicht für ein bestimmtes Backend optimiert
  • Ausgabe: Zielschaltkreis und Observable, optimiert für den ausgewählten QPU

In diesem Schritt verwenden wir die Funktion generate_preset_pass_manager aus Qiskit, um eine Optimierungsroutine für unseren Schaltkreis bezüglich des realen Quantencomputers festzulegen, auf dem wir das Experiment durchführen möchten. Wir setzen optimization_level=3, was bedeutet, dass wir den voreingestellten Pass-Manager mit der höchsten Optimierungsstufe verwenden. „Optimierung" bezieht sich hier auf die Optimierung der Schaltkreisimplementierung auf einem realen Quantencomputer. Dazu gehören Überlegungen wie die Auswahl physischer Qubits, die den Qubits im abstrakten Quantenschaltkreis entsprechen und die Gate-Tiefe minimieren, oder die Auswahl physischer Qubits mit den niedrigsten verfügbaren Fehlerraten. Das hat keinen direkten Bezug zur Optimierung des Machine-Learning-Problems (wie bei klassischen Optimierern wie COBYLA).

Je nach Implementierung von Schritt 2 musst du den Schaltkreis möglicherweise mehr als einmal optimieren, da jedes Punktepaar, das an einem Matrixelement beteiligt ist, einen anderen zu messenden Schaltkreis erzeugt.

Schritt 3: Ausführung mit Qiskit Runtime Primitives

  • Eingabe: Zielschaltkreis
  • Ausgabe: Wahrscheinlichkeitsverteilung

Verwende das Sampler-Primitive aus Qiskit Runtime, um eine Wahrscheinlichkeitsverteilung der Zustände zu rekonstruieren, die durch Sampling des Schaltkreises erzeugt werden. Beachte, dass dies manchmal als „Quasi-Wahrscheinlichkeitsverteilung" bezeichnet wird – ein Begriff, der gilt, wenn Rauschen ein Problem darstellt und zusätzliche Schritte eingeführt werden, etwa bei der Fehlerminderung. In solchen Fällen muss die Summe aller Wahrscheinlichkeiten nicht exakt 1 ergeben; daher „Quasi-Wahrscheinlichkeit".

Schritt 4: Nachverarbeitung, Ergebnis im klassischen Format zurückgeben

  • Eingabe: Wahrscheinlichkeitsverteilung
  • Ausgabe: Ein einzelnes Kernmatrixelement oder eine Kernmatrix bei Wiederholung

Berechne die Wahrscheinlichkeit, 0N|0\rangle^{\otimes N} auf dem Quantenschaltkreis zu messen, und befülle die Kernmatrix an der Position, die den zwei verwendeten Datenvektoren entspricht. Um die gesamte Kernmatrix auszufüllen, müssen wir für jeden Eintrag ein Quantenexperiment durchführen. Sobald wir eine Kernmatrix haben, können wir sie in vielen klassischen Machine-Learning-Algorithmen verwenden, die pre-calculated kernels akzeptieren. Zum Beispiel: qml_svc = SVC(kernel="precomputed"). Wir können dann klassische Arbeitsabläufe nutzen, um unser Modell auf unsere Testdaten anzuwenden und eine Genauigkeitsbewertung zu erhalten. Je nach Zufriedenheit mit unserer Genauigkeit müssen wir möglicherweise Aspekte unserer Berechnung überarbeiten, etwa unsere Feature-Map.

Überblick über die Lektion

In dieser Lektion führen wir diese Schritte auf verschiedene Weisen durch, um deine Zeit auf realen Quantencomputern optimal zu nutzen. Wir wenden eine Quantenkernel-Methode an auf:

  • Ein einzelnes Kernmatrixelement für Daten mit relativ wenigen Features, auf einem realen Backend, damit wir leicht nachvollziehen können, was in jedem Schritt passiert.
  • Einen vollständigen Datensatz mit relativ wenigen Features, auf einem simulierten Backend, damit wir sehen können, wie der Quanten-Arbeitsablauf mit klassischen Machine-Learning-Methoden verbunden ist.
  • Ein einzelnes Kernmatrixelement für Daten mit vielen Features, auf einem realen Quantencomputer. Eine gesamte Kernmatrix für einen großen Datensatz schätzen wir nicht, um die Zeit auf IBM®-Quantencomputern zu respektieren.
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy pandas qiskit qiskit-ibm-runtime scikit-learn
# If you have not already, install scikit learn
#!pip install scikit-learn

Einzelnes Kernmatrixelement

Schritt 1: Klassische Eingaben auf ein Quantenproblem abbilden

Betrachten wir zunächst einen Datensatz mit nur wenigen Features, sagen wir 10. Der Datensatz kann beliebig groß sein, da wir die Kernmatrixelemente einzeln berechnen. Wir benötigen mindestens zwei Punkte, also starten wir damit (im nächsten Beispiel importieren wir einen vollständigen Datensatz). Lass 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 die z_feature_map ausprobieren.

# 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 obigen Unitären entsprechen genau U1U_1 und U2U_2 aus der Einführung. Wir können sie mithilfe von unitary_overlap kombinieren. Wie immer sollten wir die Schaltkreistiefe im Blick 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")
circuit depth =  9

Output of the previous code cell

Schritt 2: Problem für die Quantenausführung optimieren

Wir beginnen mit der Auswahl des am wenigsten ausgelasteten Backends und optimieren dann unseren Schaltkreis 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)
<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 komplexen Schaltkreisen erhöht dieser Schritt die Schaltkreistiefe erheblich, da die Abbildung auf native Gates für reale Quantencomputer erfolgt und Informationen 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)
circuit depth =  10
1

Schritt 3: Ausführung mit Qiskit Runtime Primitives

Die Syntax für die Ausführung auf einem Simulator ist unten auskommentiert. Bei diesem Datensatz mit einer geringen Anzahl von Features ist die Ausführung auf einem Simulator noch eine Option. Bei Berechnungen im Utility-Maßstab ist Simulation in der Regel nicht praktikabel. Simulatoren sollten nur zum Debuggen von vereinfachtem 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 our runtime primitive
from qiskit_ibm_runtime import Session, SamplerV2 as Sampler

num_shots = 10000

# Use sampler and get the counts

sampler = Sampler(mode=backend)
results = sampler.run([overlap_ibm], shots=num_shots).result()
# .get_counts() returns counts associated with a state labeled by bit results such as |001101...01>.
counts_bit = results[0].data.meas.get_counts()
# .get_int_counts returns the same counts, but labeled by integer equivalent of the above bit string.
counts = results[0].data.meas.get_int_counts()

Schritt 4: Nachverarbeitung, Ergebnis im klassischen Format zurückgeben

Wie in der Einführung beschrieben, ist die nützlichste Messung hier die Wahrscheinlichkeit, den Nullzustand 00000|00000\rangle zu messen.

counts.get(0, 0.0) / num_shots
0.6525

Das ist das Ergebnis, das wir wollten: eine Schätzung des inneren Produkts (bis auf Betragsquadrat) der Vektoren, die zwei Datenpunkten entsprechen. Wenn wir die vollständige Verteilung der Messwahrscheinlichkeiten (oder Quasi-Wahrscheinlichkeiten) betrachten möchten, können wir dies mit der Funktion plot_distribution tun, wie unten gezeigt. Man sieht, dass solche Bilder für eine große Anzahl von Qubits schnell unübersichtlich werden.

from qiskit.visualization import plot_distribution

plot_distribution(counts_bit)

Output of the previous code cell

Alternativ kann man eine Visualisierung wie die folgende definieren, um nur die 10 wahrscheinlichsten Messungen zu betrachten. Das kann beim Debuggen oder beim Gewinnen von Intuition für die Daten hilfreich sein. Die Messwahrscheinlichkeit des Nullzustands ist jedoch unser Kernmatrixelement.

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 of the previous code cell

Aus dieser Information über nur ein inneres Produkt zwischen zwei Datenpunkten im höherdimensionalen Feature-Raum lässt sich lediglich sagen, dass ihre Überlappung im Vergleich zur maximalen Überlappung (die 1,0 wäre) ziemlich groß ist. Das könnte ein Hinweis darauf sein, dass diese beiden Datenpunkte irgendwie ähnlicher Natur sind und derselben Klasse zugeordnet werden. Oder es könnte ein Hinweis sein, dass unsere Feature-Map nicht effektiv darin ist, in einen Raum abzubilden, in dem ähnliche Daten eine starke Überlappung und unähnliche Daten eine geringe Überlappung haben. Um herauszufinden, was davon zutrifft, müssen wir unsere Feature-Map auf den gesamten Datensatz anwenden und prüfen, ob die resultierende Kernmatrix so manipuliert werden kann, dass sie Klassen mit hoher Genauigkeit effektiv trennt.

Es ist erwähnenswert, dass wir die z_feature_map verwendet haben, was zu einer geringen transpilierten Zwei-Qubit-Tiefe führte (tatsächlich Tiefe 1). Werden deine Schaltkreise zu tief, führt das mit Sicherheit zu viel Rauschen, und die Wahrscheinlichkeit, den Nullzustand zu messen, wird sehr gering sein – selbst wenn deine Feature-Map gut zu deinen Daten passt. Eine Wiederholung des obigen Prozesses mit der zz_feature_map und , entanglement='linear', reps=1 ergab zum Beispiel dist.get(0,0.0) = 0.0015 mit denselben Datenpunkten. Das liegt an den deutlich größeren Schaltkreistiefen und Zwei-Qubit-Tiefen der zz_feature_map. Die folgende Abbildung zeigt die Wahrscheinlichkeitsverteilung für diese Berechnung.

Bad results from a zz feature map.

Es lohnt sich, mit einigen Datenpunkten aus derselben Kategorie herumzuexperimentieren, um herauszufinden, wie gering die Tiefe sein muss, um gute Ergebnisse zu erzielen. Der folgende Rat ist ein grober Richtwert, der sicherlich Ausnahmen hat. Im Allgemeinen sollte eine transpilierte Zwei-Qubit-Tiefe von 10 oder weniger kein Problem sein. Eine transpilierte Zwei-Qubit-Tiefe von 50–60 entspricht dem Stand der Technik und erfordert fortgeschrittene Fehlerminderung und weitere Werkzeuge. Dazwischen können deine Ergebnisse je nach Ähnlichkeit der Daten, Expressivität der Feature-Map, Schaltkreisbreite und anderen Faktoren variieren.

Normalerweise würde der Nachverarbeitungsschritt auch klassische Machine-Learning-Prozesse umfassen. Im nächsten Abschnitt werden wir diesen Prozess auf einen vollständigen Datensatz ausdehnen und den klassischen Machine-Learning-Arbeitsablauf zeigen.

Verständnisfragen

Lies die folgenden Fragen, denk über die Antworten nach und klicke dann auf die Dreiecke, um die Lösungen anzuzeigen.

Wie viele verschiedene Zustände können in einem 10-Qubit-Quantenschaltkreis generell gemessen werden?

Antwort:

2102^{10} oder 1024.

Angenommen, jemand, der neu im Bereich Quantencomputing ist, versucht einen Quantenschaltkreis mit sehr hoher Zwei-Qubit-Tiefe zu verwenden, ohne Fehlerminderung einzusetzen. Angenommen, dies führt zu einer Fehlerrate von 10 % pro Qubit. Wenn das wahre (fehlerfreie) Kernmatrixelement dieses Schaltkreises sehr groß ist, sagen wir 1,0 – wie groß wäre dann die Wahrscheinlichkeit, alle 10 Qubits im Zustand |0> zu messen?

Antwort:

Die Wahrscheinlichkeit, dass jedes Qubit korrekt im Zustand |0> gemessen wird, beträgt 0,90. Die Wahrscheinlichkeit, dass alle 10 Qubits im richtigen Zustand gemessen werden, beträgt 0,90100,90^{10}, also etwa 35 %.

Erkläre in eigenen Worten, warum es so wichtig ist, die Schaltkreistiefen zu überwachen. Das gilt allgemein, aber erkläre es im Kontext der Quantenkernel-Schätzung.

Antwort:

In diesem QKE-Arbeitsablauf basieren unsere Schätzungen auf den Messungen des Nullzustands, also dem Zustand, in dem jedes Qubit im Zustand 0|0\rangle gefunden wird. Sehr tiefe Schaltkreise führen zu hohen Fehlerraten. Wenn sich diese Fehlerrate über viele Qubits potenziert, wird die Wahrscheinlichkeit, den Nullzustand zu messen, erheblich reduziert.

Vollständige Kernmatrix

In diesem Abschnitt erweitern wir den obigen Prozess auf die binäre Klassifikation eines vollständigen Datensatzes. Das bringt zwei wichtige Komponenten mit sich: (1) Wir können nun klassisches Machine Learning in der Nachverarbeitung implementieren, und (2) wir können Genauigkeitswerte für unser Training erhalten.

Schritt 1: Klassische Eingaben auf ein Quantenproblem abbilden

Jetzt importieren wir einen bestehenden Datensatz für unsere Klassifikation. Dieser Datensatz besteht aus 128 Zeilen (Datenpunkten) und 14 Features pro Punkt. Es gibt ein 15. Element, das die binäre Kategorie jedes Punktes angibt (±1\pm 1). Der Datensatz wird unten importiert; du kannst den Datensatz auch einsehen und seine Struktur hier betrachten.

Wir verwenden die ersten 90 Datenpunkte für das Training und die nächsten 30 Punkte für das 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]
--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 die Speicherung mehrerer Ausgaben vor, indem wir eine Kernmatrix und eine Testmatrix in 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)

Jetzt erstellen wir eine Feature-Map zum Enkodieren und Abbilden unserer klassischen Daten in einem Quantenschaltkreis. Wir können unsere eigene Feature-Map konstruieren oder eine vorgefertigte verwenden. Du kannst die Feature-Map unten gerne ändern oder zur ZFeatureMap zurückwechseln. Behalte aber immer die Schaltkreistiefe im Blick. Erinnere dich: Im vorherigen 6-Qubit-Beispiel war die transpilierte Schaltkreistiefe beim Einsatz der zz_feature_map nicht handhabbar. Mit zunehmender Größe und Komplexität des Schaltkreises kann die Tiefe so schnell ansteigen, dass Rauschen unsere Ergebnisse überwältigt. Wenn du etwas über die Struktur deiner Daten weißt, das darauf hindeutet, welche Feature-Map-Struktur am nützlichsten wäre, empfiehlt es sich, eine 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 Primitives ausführen

Wir erstellen einen Overlap-Schaltkreis, und wenn wir in diesem Beispiel auf einem realen Quantencomputer ausführen würden, optimieren wir ihn wie zuvor für die Ausführung. In diesem Fall beabsichtigen wir jedoch, alle Datenpunkte zu durchlaufen und die vollständige Kernmatrix zu berechnen. Für jedes Paar von Datenvektoren xi\vec{x}_i und xj\vec{x}_j erstellen wir einen anderen Overlap-Schaltkreis. Daher müssen wir unseren Schaltkreis für jedes Datenpunktpaar optimieren. Schritte 2 und 3 würden also gemeinsam in den mehrfachen Iterationen durchgeführt.

Die folgende Code-Zelle führt genau denselben Prozess wie zuvor für ein einzelnes Datenpunktpaar durch. Diesmal wird er einfach innerhalb von zwei for-Schleifen ausgeführt, und es gibt die zusätzliche Zeile am Ende kernel_matrix[x_1,x_2] = ..., um die Ergebnisse jeder Berechnung zu speichern. Beachte, dass wir die Symmetrie einer Kernmatrix genutzt haben, um die Anzahl der Berechnungen um 1/2 zu reduzieren. Wir haben auch einfach die Diagonalelemente auf 1 gesetzt, da sie in Abwesenheit von Rauschen so sein sollten. Je nach deiner Implementierung und der erforderlichen Präzision könntest du die Diagonalelemente auch verwenden, um Rauschen zu schätzen oder Informationen für die Fehlerminderung zu gewinnen.

Sobald die Kernmatrix vollständig befüllt ist, wiederholen wir den Prozess für die Testdaten und befüllen die test_matrix. Das ist eigentlich auch eine Kernmatrix; 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")
training done
test matrix done

Schritt 4: Nachverarbeitung, Ergebnis im klassischen Format zurückgeben

Da wir jetzt eine Kernmatrix und eine entsprechend formatierte test_matrix aus Quantenkernel-Methoden haben, können wir klassische Machine-Learning-Algorithmen anwenden, um Vorhersagen über unsere Testdaten zu treffen und deren Genauigkeit zu prüfen. Wir beginnen mit dem Import von Scikit-Learns sklearn.svc, einem Support-Vector-Classifier (SVC). Wir müssen angeben, dass der SVC unseren vorberechneten Kern verwenden soll: kernel = precomputed.

# import a support vector classifier from a classical ML package.
from sklearn.svm import SVC

# Specify that you want to use a pre-computed kernel matrix
qml_svc = SVC(kernel="precomputed")

Mit SVC.fit können wir nun die Kernmatrix und die Trainingslabels eingeben, um einen Fit zu erhalten. SVC.score bewertet dann unsere Testdaten anhand dieses Fits mithilfe 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}")
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. Das ist jedoch weit entfernt von einem Quantenvorteil. Klassische Kerne hätten dieses Klassifikationsproblem wahrscheinlich ebenfalls mit 100 % Genauigkeit lösen können. Es gibt noch viel Arbeit bei der Charakterisierung verschiedener Datentypen und Datenbeziehungen, um herauszufinden, wo Quantenkerne im aktuellen Utility-Zeitalter am nützlichsten sein werden.

Wir überlassen es dir, Teile dieses Arbeitsablaufs zu modifizieren und die Effektivität verschiedener Quanten-Feature-Maps zu untersuchen. Hier sind einige Dinge zu bedenken:

  • Wie robust ist die Genauigkeit? Gilt sie für breite Datentypen oder nur für diese spezifischen Trainingsdaten?
  • Welche Struktur in deinen Daten lässt dich vermuten, dass eine Quanten-Feature-Map nützlich ist?
  • Wie wird die Genauigkeit durch Erhöhen/Verringern der Trainingsdatenmenge beeinflusst?
  • Welche Feature-Maps kannst du verwenden, und wie variieren die Ergebnisse mit den Feature-Maps?
  • Wie werden Genauigkeit und Laufzeit durch Erhöhen der Anzahl der Features beeinflusst?
  • Welche Trends, falls vorhanden, erwartest du auf realen Quantencomputern zu sehen?

Skalierung auf mehr Features und Qubits

In diesem Abschnitt wiederholen wir die Berechnung eines einzelnen Matrixelements, jedoch für eine deutlich größere Anzahl von Features und zeigen damit den Weg zur Skalierung in Richtung Utility. Die Beschränkung auf ein einzelnes Matrixelement erfolgt, damit der Prozess gezeigt werden kann, ohne zu viel deiner zugeteilten Zeit auf Quantencomputern zu verbrauchen.

Schritt 1: Klassische Eingaben auf ein Quantenproblem abbilden

Wir gehen von einem Datensatz aus, bei dem jeder Datenpunkt 42 Features hat. Wie im ersten Beispiel berechnen wir ein einzelnes Kernmatrixelement, wofür wir zwei Datenpunkte benötigen. Die beiden folgenden Punkte haben 42 Features und eine einzelne Kategorievariable (±1\pm 1).

# Two mock data points, including category labels, as in training

large_data = [
[
-0.028,
-1.49,
-1.698,
0.107,
-1.536,
-1.538,
-1.356,
-1.514,
-0.109,
-1.8,
-0.122,
-1.651,
-1.955,
-0.123,
-1.732,
0.091,
-0.048,
-0.128,
-0.026,
0.082,
-1.263,
0.065,
0.004,
-0.055,
-0.08,
-0.173,
-1.734,
-0.39,
-1.451,
0.078,
-1.578,
-0.025,
-0.184,
-0.119,
-1.336,
0.055,
-0.204,
-1.578,
0.132,
-0.121,
-1.599,
-0.187,
-1,
],
[
-1.414,
-1.439,
-1.606,
0.246,
-1.673,
0.002,
-1.317,
-1.262,
-0.178,
-1.814,
0.013,
-1.619,
-1.86,
-0.25,
-0.212,
-0.214,
-0.033,
0.071,
-0.11,
-1.607,
0.441,
-0.143,
-0.009,
-1.655,
-1.579,
0.381,
-1.86,
-0.079,
-0.088,
-0.058,
-1.481,
-0.064,
-0.065,
-1.507,
0.177,
-0.131,
-0.153,
0.07,
-1.627,
0.593,
-1.547,
-0.16,
-1,
],
]
train_data = [large_data[0][:-1], large_data[1][:-1]]

Erinnere dich: Die zz_feature_map erzeugte bei relativ wenigen Features (14 Features) bereits recht tiefe Schaltkreise. Mit zunehmender Anzahl von Features müssen wir die Schaltkreistiefe genau beobachten. Um das zu veranschaulichen, versuchen wir zunächst die zz_feature_map und prüfen die Tiefe des resultierenden Schaltkreises.

from qiskit.circuit.library import zz_feature_map

fm = zz_feature_map(
feature_dimension=np.shape(train_data)[1], entanglement="linear", reps=1
)

unitary1 = fm.assign_parameters(train_data[0])
unitary2 = fm.assign_parameters(train_data[1])
from qiskit.circuit.library import unitary_overlap

overlap_circ = unitary_overlap(unitary1, unitary2)
overlap_circ.measure_all()

print("circuit depth = ", overlap_circ.decompose(reps=2).depth())
print(
"two-qubit depth",
overlap_circ.decompose().depth(lambda instr: len(instr.qubits) > 1),
)
# overlap_circ.draw("mpl", scale=0.6, style="iqp")
circuit depth =  251
two-qubit depth 165

Wie bereits beschrieben, ist die genaue Bestimmung, ab wann ein Schaltkreis zu tief ist, eine nuancierte Frage. Aber eine Zwei-Qubit-Tiefe von mehr als 100, noch vor der Transpilation, ist ein K.O.-Kriterium. Deshalb wurden in dieser Lektion durchgehend benutzerdefinierte Feature-Maps betont. Wenn du etwas über die Struktur deines gesamten Datensatzes weißt, solltest du eine Entanglement-Map entwerfen, die diese Struktur berücksichtigt. Da wir hier nur das innere Produkt zwischen zwei solchen Datenpunkten berechnen, haben wir einer geringen Schaltkreistiefe Vorrang vor einer detaillierten Berücksichtigung der Datenstruktur 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 überprüfen die Tiefen vorerst nicht, da es wirklich auf die transpilierte Zwei-Qubit-Tiefe ankommt.

Schritt 2: Problem für die Quantenausführung optimieren

Wir beginnen mit der Auswahl des am wenigsten ausgelasteten Backends und optimieren dann unseren Schaltkreis 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)
<IBMBackend('ibm_brisbane')>

Bei kleinen Jobs gibt ein voreingestellter Pass-Manager oft zuverlässig denselben Schaltkreis mit gleicher Tiefe zurück. Bei sehr großen, komplexen Schaltkreisen kann der Pass-Manager jedoch bei jedem Durchlauf unterschiedliche transpilierte Schaltkreise zurückgeben. Das liegt daran, dass er Heuristiken verwendet und sehr große Schaltkreise eine komplizierte Landschaft möglicher Optimierungen aufweisen. Es ist oft sinnvoll, mehrmals zu transpilieren und den flachsten Schaltkreis zu nehmen. Das führt nur zu klassischem Overhead und kann die Ergebnisse vom Quantencomputer erheblich verbessern.

Hier transpilieren wir den Unitary-Overlap-Schaltkreis 20 Mal und schauen uns die Tiefen der erhaltenen Schaltkreise an.

# Apply level 3 optimization to our overlap circuit
transpiled_qcs = []
transpiled_depths = []
transpiled_twoqubit_depths = []
for i in range(1, 20):
pm = generate_preset_pass_manager(optimization_level=3, backend=backend)
overlap_ibm = pm.run(overlap_circ)
transpiled_qcs.append(overlap_ibm)
transpiled_depths.append(overlap_ibm.decompose().depth())
transpiled_twoqubit_depths.append(
overlap_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)
)

print("circuit depth = ", overlap_ibm.decompose().depth())
circuit depth =  61
print(transpiled_depths)
print(transpiled_twoqubit_depths)
[61, 60, 60, 69, 60, 60, 60, 65, 60, 60, 69, 61, 77, 77, 65, 60, 60, 77, 61]
[13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13]

Hier sieht man, dass es eine gewisse Variation in der Gesamtgate-Tiefe bei verschiedenen Transpilationsdurchläufen gibt. Unser Schaltkreis ist noch nicht tief/breit genug, um Variation in den transpilierten Zwei-Qubit-Tiefen zu sehen. Wir verwenden transpiled_qcs[1], das eine Tiefe von 60 hat – etwas geringer als die Tiefe des tiefsten erhaltenen Schaltkreises, die 77 betrug.

overlap_ibm = transpiled_qcs[1]

Schritt 3: Ausführung mit Qiskit Runtime Primitives

Wenn wir uns der Utility-Skala nähern, werden Simulatoren nicht mehr nützlich sein. Hier wird nur die Syntax für reale Quantencomputer gezeigt.

# Run on ibm_osaka, 7-12-24, required 22 sec.

# Import our runtime primitive
from qiskit_ibm_runtime import SamplerV2 as Sampler

# Open a Runtime session:
session = Session(backend=backend)
num_shots = 10000
# Use sampler and get the counts

sampler = Sampler(mode=session)
options = sampler.options
options.dynamical_decoupling.enable = True
options.twirling.enable_gates = True
counts = (
sampler.run([overlap_ibm], shots=num_shots).result()[0].data.meas.get_int_counts()
)

# Close session after done
session.close()

Schritt 4: Nachverarbeitung, Ergebnis im klassischen Format zurückgeben

Wie in der Einführung beschrieben, ist die nützlichste Messung hier die Wahrscheinlichkeit, den Nullzustand 00000|00000\rangle zu messen.

counts.get(0, 0.0) / num_shots
0.0138

Dieser Prozess für das einzelne Kernmatrixelement könnte zwischen anderen Datenpaarungen in deinem Datensatz wiederholt werden, um die vollständige Kernmatrix zu erhalten. Die Dimension der Kernmatrix wird durch die Anzahl der Punkte in deinen Trainingsdaten bestimmt, nicht durch die Anzahl der Features. Daher skaliert der Rechenaufwand für die Umwandlung der Kernmatrix in ein prädiktives Modell nicht wie die Anzahl der Features oder Qubits. Selbst bei relativ kleinen Datensätzen mit großen Feature-Zahlen müssten die Daten noch mit einer Feature-Map abgeglichen werden, die eine effektive Klassifikation ermöglicht.

Skalierung und zukünftige Arbeit

Die Kernel-Methode erfordert, dass wir den 0|0\rangle-Zustand so genau wie möglich messen. Gate-Fehler und Auslesefehler bedeuten jedoch, dass es eine von null verschiedene Wahrscheinlichkeit pp gibt, dass ein gegebenes Qubit fälschlicherweise im Zustand 1|1\rangle gemessen wird. Selbst mit der vereinfachenden Annahme, dass die Wahrscheinlichkeit für 0|0\rangle 100 % betragen sollte, wird bei vielen Features, die auf NN Bits kodiert sind, die Wahrscheinlichkeit, alle Bits korrekt als 0|0\rangle zu messen, auf (1p)N(1-p)^N reduziert. Mit zunehmendem NN wird diese Methode immer unzuverlässiger. Die Überwindung dieser Schwierigkeit und die Skalierung der Kernschätzung auf immer mehr Features ist ein aktives Forschungsgebiet. Mehr über dieses Problem erfahrst du in dieser Arbeit von Thanasilp, Wang, Cerezo und Holmes. Wir empfehlen dir, zu erkunden, was mit aktuellen Quantencomputern möglich ist, und auch vorausblickend zu betrachten, was im Zeitalter der Fehlerkorrektur möglich sein wird.

Zusammenfassung

Die Berechnung eines Quantenkerns umfasst:

  • die Berechnung von Kernmatrixeinträgen anhand von Paaren aus Trainingsdatenpunkten
  • das Enkodieren der Daten und ihre Abbildung über eine Feature-Map
  • die Optimierung deines Schaltkreises für die Ausführung auf realen Quantencomputern / Backends

Der Quantenkern kann dann in klassischen Machine-Learning-Algorithmen verwendet werden, wie in dieser Lektion gezeigt.

Einige wichtige Dinge, die du beim Einsatz von Quantenkernen beachten solltest:

  • Profitiert der Datensatz wahrscheinlich von Quantenkernel-Methoden?
  • Probiere verschiedene Feature-Maps und Entanglement-Schemata aus.
  • Ist die Schaltkreistiefe akzeptabel?
  • Führe den Pass-Manager mehrmals aus und verwende den Schaltkreis mit der geringsten Tiefe, den du erhalten kannst.

Quantenkernel-Methoden sind potenziell leistungsstarke Werkzeuge, wenn eine gute Übereinstimmung zwischen Datensätzen mit quantengeeigneten Features und einer passenden Quanten-Feature-Map besteht. Um besser zu verstehen, wo Quantenkerne voraussichtlich nützlich sein werden, empfehlen wir die Lektüre von Liu, Arunachalam & Temme (2021).