Przejdź do głównej treści

Trening kwantowego jądra (Quantum kernel training)

Szacowany czas użycia: poniżej jednej minuty na procesorze Heron r3 (UWAGA: to tylko szacunek. Twój czas wykonania może się różnić.)

Cele uczenia

Po ukończeniu tego samouczka możesz spodziewać się zrozumienia następujących zagadnień:

  • Metody jądrowe i ich zastosowania
  • Kwantowe jądra i to, jak mogą zapewniać rozszerzone przestrzenie cech
  • Budowa kwantowych obwodów jądra
  • Jak trenować kwantowe jądro przy użyciu wzorca Qiskit: odwzorowanie, optymalizacja, wykonanie i post-processing

Wymagania wstępne

Zaleca się zapoznanie z kwantowymi jądrami, ich znaczeniem i praktycznym zastosowaniem.

Przydatna jest również podstawowa znajomość teorii grup.

Tło

Metody jądrowe są powszechnie stosowane w zastosowaniach uczenia maszynowego. W tym kontekście „jądro" odnosi się do macierzy jądra lub jej poszczególnych wpisów. Ogólnie rzecz biorąc, jądro jest miarą podobieństwa między danymi zakodowanymi w wielowymiarowej przestrzeni cech i może być wykorzystywane, na przykład, w zadaniach klasyfikacji z maszynami wektorów nośnych.

Kwantowe metody jądrowe to te, które używają komputerów kwantowych do szacowania jądra. Wiadomo, że komputery kwantowe mogą kodować dane w kwantowo-rozszerzonych przestrzeniach cech, skutecznie zastępując klasyczne odpowiedniki. Dla xR\vec{x} \in \mathbb{R} i Ψ(x)Rd\Psi(\vec{x}) \in \mathbb{R}^{d'}, zazwyczaj z d>dd' >d, Ψ(x)\Psi(\vec{x}) jest odwzorowaniem cech, xΨ(x)\vec{x} \mapsto \Psi(\vec{x}). Celem Ψ(x)\Psi(\vec{x}) jest uczynienie kategorii danych rozdzielonymi przez hiperpłaszczyznę. Biorąc wektory w przestrzeni z odwzorowanymi cechami jako argumenty, funkcja jądra K(x,y)=Ψ(x)Ψ(y)K(\vec{x}, \vec{y}) = \langle{\Psi(\vec{x}) | \Psi(\vec{y}) \rangle{}} zwraca ich iloczyn skalarny: K:RdK: \mathbb{R}^d \rightarrow Rd\mathbb{R}^d. Klasycznie interesujące odwzorowania cech to te, w których funkcja jądra może być łatwo obliczona; tzn. gdy iloczyn skalarny w przestrzeni z odwzorowanymi cechami można wyrazić w kategoriach oryginalnych wektorów danych, a Ψ(x)\Psi(\vec{x}) i Ψ(y)\Psi(\vec{y}) nie muszą być konstruowane. W przypadku kwantowych jąder odwzorowanie cech jest wykonywane przez obwód kwantowy, a jądro jest szacowane przy użyciu prawdopodobieństw pomiarów próbkowanych z obwodu.

Ten samouczek pokazuje, jak zbudować wzorzec Qiskit do obliczania wpisów macierzy kwantowego jądra używanej w klasyfikacji binarnej.

Wymagania

Przed rozpoczęciem tego samouczka upewnij się, że masz zainstalowane następujące elementy:

  • Qiskit SDK v2.3.1 lub nowszy, z obsługą wizualizacji
  • Qiskit Runtime v0.44.0 lub nowszy (pip install qiskit-ibm-runtime)

Konfiguracja

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy pandas qiskit qiskit-ibm-runtime
# General Imports and helper functions
import urllib.request

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit
from qiskit.circuit.library import unitary_overlap
from qiskit.primitives import StatevectorSampler
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

from qiskit_ibm_runtime import QiskitRuntimeService, Sampler

# Download the dataset (portable across platforms)
urllib.request.urlretrieve(
"https://raw.githubusercontent.com/qiskit-community/prototype-quantum-kernel-training/main/data/dataset_graph7.csv",
"dataset_graph7.csv",
)

def visualize_counts(res_counts, num_qubits, num_shots):
"""Visualize the outputs from the Qiskit Sampler primitive."""
zero_prob = res_counts.get(0, 0.0)
top_10 = dict(
sorted(res_counts.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]))
x_vals, y_vals = list(zip(*by_key.items()))
x_vals = [bin(x_val)[2:].zfill(num_qubits) for x_val in x_vals]
y_vals_prob = []
for t in range(len(y_vals)):
y_vals_prob.append(y_vals[t] / num_shots)
y_vals = y_vals_prob
plt.bar(x_vals, y_vals)
plt.xticks(rotation=75)
plt.title("Results of sampling")
plt.xlabel("Measured bitstring")
plt.ylabel("Probability")
plt.show()

def get_training_data():
"""Read the training data."""
df = pd.read_csv("dataset_graph7.csv", sep=",", header=None)
training_data = df.values[:20, :]
ind = np.argsort(training_data[:, -1])
X_train = training_data[ind][:, :-1]

return X_train

Przykład z symulatorem na małą skalę

W tej sekcji przechodzimy przez cztery kroki wzorca Qiskit na siedmio-qubitowej instancji problemu etykietowania klas bocznych z błędem i obliczamy pojedynczy wpis macierzy jądra przy użyciu prymitywu StatevectorSampler z Qiskit. Symulator wektora stanu jest dokładny (z wyjątkiem szumu strzałów) i pokazuje nam metodę od początku do końca bez zużywania czasu QPU. Następnie powtarzamy tę samą instancję na rzeczywistym sprzęcie w sekcji przykładu sprzętowego.

Krok 1: Odwzorowanie klasycznych danych wejściowych na problem kwantowy

  • Dane wejściowe: Zbiór danych treningowych.
  • Dane wyjściowe: Abstrakcyjny Circuit do obliczania jednego wpisu macierzy jądra.

Problem klasyfikacji binarnej, który tutaj rozwiązujemy, jest określany jako „etykietowanie klas bocznych z błędem". Wejściowy zbiór danych treningowych zawiera strukturę grupową, składającą się z dwóch klas bocznych utworzonych przez grupę i podgrupę. Grupę przyjmuje się jako G=SU(2)nG = SU(2)^{\otimes n} dla qubitów, czyli specjalną grupę unitarną macierzy 2×22 \times 2, mającą szerokie zastosowanie w przyrodzie; np. w Modelu Standardowym fizyki cząstek. Przyjmujemy podgrupę (stabilizatora grafu) Sgraph<GS_\text{graph} < G z Sgraph={Xik:(k,i)EZk}iV}S_\text{graph} = \langle \{ X_i \otimes _{k:(k,i) \in \mathcal{E}} Z_k\} _{i \in \mathcal{V}} \} \rangle dla grafu z krawędziami E\mathcal{E} i wierzchołkami V\mathcal{V}. Należy zauważyć, że stabilizatory ustalają stan stabilizatora taki, że Dsψ=ψ, sSgraphD_s | \psi \rangle = | \psi \rangle,~ \forall s \in S_\text{graph}. Na koniec definiujemy dwie lewe klasy boczne C±=c±SgraphC_\pm = c_\pm S_\text{graph}, losując dwa c±Gc_\pm \in G.

Więcej szczegółów na temat zbioru danych i sposobu jego generowania znajdziesz w tym notebooku z Quantum Kernel Training Toolkit.

Tworzymy obwód kwantowy służący do obliczenia jednego wpisu macierzy jądra. Dane wejściowe są używane do określenia kątów obrotu dla sparametryzowanych bramek obwodu. Dla uproszczenia użyjemy próbek danych x1=14 i x2=19.

Uwaga: Zbiór danych użyty w tym samouczku można pobrać tutaj.

# Prepare training data
X_train = get_training_data()

# Empty kernel matrix
num_samples = np.shape(X_train)[0]
kernel_matrix = np.full((num_samples, num_samples), np.nan)

# Prepare feature map for computing overlap
num_features = np.shape(X_train)[1]
num_qubits = int(num_features / 2)
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)

# Assign tunable parameter to known optimal value and set the data params for
# first two samples
x1 = 14
x2 = 19
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()
overlap_circ.draw("mpl", scale=0.6, style="iqp")

Output of the previous code cell

Krok 2: Optymalizacja problemu pod kątem sprzętu kwantowego

  • Dane wejściowe: Abstrakcyjny Circuit, nieoptymalizowany pod kątem konkretnego Backend.
  • Dane wyjściowe: Docelowy Circuit, zoptymalizowany pod wybrany QPU.

W przypadku ścieżki z symulatorem wektora stanu używanej w tej sekcji nie jest wymagana optymalizacja specyficzna dla Backend: abstrakcyjny obwód może być próbkowany bezpośrednio. Ten krok realizujemy w przykładzie sprzętowym poniżej, gdzie obwód jest transpilowany na rzeczywisty QPU przy użyciu generate_preset_pass_manager z optimization_level=3.

Krok 3: Wykonanie przy użyciu prymitywów Qiskit

  • Dane wejściowe: Abstrakcyjny Circuit.
  • Dane wyjściowe: Rozkład quasi-prawdopodobieństwa.

Użyj prymitywu StatevectorSampler z Qiskit, aby zrekonstruować rozkład quasi-prawdopodobieństwa stanów uzyskanych przez próbkowanie obwodu. W zadaniu generowania macierzy jądra szczególnie interesuje nas prawdopodobieństwo zmierzenia stanu |0>.

sampler = StatevectorSampler()

# Execute and get counts
num_shots = 10_000
results = sampler.run([overlap_circ], shots=num_shots).result()
counts = results[0].data.meas.get_int_counts()

# Plot counts
visualize_counts(counts, num_qubits, num_shots)

Output of the previous code cell

Krok 4: Post-processing i zwrócenie wyniku w żądanym formacie klasycznym

  • Dane wejściowe: Rozkład prawdopodobieństwa.
  • Dane wyjściowe: Pojedynczy element macierzy jądra.

Oblicz prawdopodobieństwo zmierzenia 0|0 \rangle na obwodzie nakładkowym i wypełnij macierz jądra na pozycji odpowiadającej próbkom reprezentowanym przez ten konkretny obwód (wiersz 15, kolumna 20).

kernel_matrix[x1, x2] = counts.get(0, 0.0) / num_shots
print(f"Fidelity (simulator): {kernel_matrix[x1, x2]}")
Fidelity (simulator): 0.8261

Przykład sprzętowy

Macierz kwantowego jądra ma O(N2)\mathcal{O}(N^2) wpisów dla NN próbek treningowych, a każdy wpis wymaga uruchomienia obwodu nakładkowego, którego głębokość bramek dwu-qubitowych rośnie wraz z rozmiarem odwzorowania cech. W rezultacie skalowanie tego samouczka do większego problemu ma dwa nakładające się koszty: czas QPU na macierz jądra rośnie kwadratowo z NN, a głębokość unitary_overlap (która składa odwzorowanie cech z jego sprzężeniem hermitowskim) obniża wierność przy rozmiarze i połączeniach obecnego sprzętu. Aby demo było krótkie i umożliwiało czystą porównanie, uruchamiamy tę samą siedmio-qubitową instancję z przykładu na małą skalę na rzeczywistym QPU i porównujemy wierność pojedynczego wpisu macierzy jądra z wartością symulatora obliczoną powyżej.

# ------------------------------ Step 1 ------------------------------
# Prepare training data
X_train = get_training_data()

# Empty kernel matrix
num_samples = np.shape(X_train)[0]
kernel_matrix = np.full((num_samples, num_samples), np.nan)

# Prepare feature map for computing overlap
num_features = np.shape(X_train)[1]
num_qubits = int(num_features / 2)
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)

# Assign tunable parameter to known optimal value and
# set the data params for first two samples
x1 = 14
x2 = 19
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()

# ------------------------------ Step 2 ------------------------------
service = QiskitRuntimeService()
# backend = service.least_busy(
# operational=True, simulator=False, min_num_qubits=overlap_circ.num_qubits
# )
backend = service.backend("ibm_pittsburgh")
print(f"Using backend: {backend.name}")
pm = generate_preset_pass_manager(optimization_level=3, backend=backend)
overlap_ibm = pm.run(overlap_circ)

# ------------------------------ Step 3 ------------------------------
sampler = Sampler(mode=backend)
sampler.options.environment.job_tags = ["TUT_QKT"]

num_shots = 10_000
results = sampler.run([overlap_ibm], shots=num_shots).result()
counts = results[0].data.meas.get_int_counts()
visualize_counts(counts, num_qubits, num_shots)

# ------------------------------ Step 4 ------------------------------
kernel_matrix[x1, x2] = counts.get(0, 0.0) / num_shots
print(f"Fidelity (hardware): {kernel_matrix[x1, x2]}")
Using backend: ibm_pittsburgh

Output of the previous code cell

Fidelity (hardware): 0.7517

Aby wypełnić całą macierz jądra, uruchomilibyśmy eksperyment kwantowy dla każdego z jej N(N+1)/2N(N+1)/2 unikalnych wpisów. Poniższy rysunek pokazuje wynikową macierz dla tego zbioru danych; ciemniejszy czerwony oznacza wierności bliższe 1,0.

kernel_matrix.png

Następne kroki

Recommendations

Jeśli ta praca jest dla ciebie interesująca, możesz zainteresować się następującymi materiałami: