Przejdź do głównej treści

Jądra kwantowe

Wprowadzenie do jąder kwantowych

"Kwantowa metoda jądrowa" odnosi się do dowolnej metody, która wykorzystuje komputery kwantowe do estymacji jądra. W tym kontekście "jądro" odnosi się do macierzy jądra lub pojedynczych jej elementów. Przypomnijmy, że odwzorowanie cech Φ(x)\Phi(\vec{x}) jest odwzorowaniem z xRd\vec{x}\in \mathbb{R}^d na Φ(x)Rd,\Phi(\vec{x})\in \mathbb{R}^{d'}, gdzie zwykle d>dd'>d i gdzie celem tego odwzorowania jest uczynienie kategorii danych rozdzielnymi przez hiperpłaszczyznę. Funkcja jądra przyjmuje jako argumenty wektory w przestrzeni cech i zwraca ich iloczyn skalarny, to znaczy K:Rd×RdRK:\mathbb{R}^d\times\mathbb{R}^d\rightarrow \mathbb{R} z K(x,y)=Φ(x)Φ(y)K(x,y) = \langle \Phi(x)|\Phi(y)\rangle. Klasycznie interesują nas odwzorowania cech, dla których funkcja jądra jest łatwa do obliczenia. Często oznacza to znalezienie funkcji jądra, dla której iloczyn skalarny w przestrzeni cech może być zapisany w kategoriach oryginalnych wektorów danych, bez konieczności konstruowania Φ(x)\Phi(x) i Φ(y)\Phi(y). W metodzie jąder kwantowych odwzorowanie cech jest wykonywane przez obwód kwantowy, a jądro jest estymowane przy użyciu pomiarów na tym obwodzie oraz względnych prawdopodobieństw pomiarów.

W tej lekcji zbadamy głębokości wstępnie zakodowanych obwodów kodujących, które wykorzystują znaczne splątanie, i porównamy je z głębokościami obwodów kodowanych przez nas ręcznie. Nie jest to zachęta do stosowania jednej metody zamiast drugiej. Może się okazać, że wstępnie zakodowane obwody są zbyt głębokie, a splątanie w obwodzie zbudowanym na zamówienie jest niewystarczające, by było użyteczne. Ponownie, pokazujemy to tylko po to, aby umożliwić Twoją eksplorację.

Zanim szczegółowo przejdziemy przez estymację macierzy jądra, zarysujmy przepływ pracy przy użyciu języka wzorców Qiskit.

Krok 1: Odwzoruj klasyczne dane wejściowe na problem kwantowy

  • Wejście: Zbiór danych treningowych
  • Wyjście: Abstrakcyjny obwód do obliczania elementu macierzy jądra

Mając zbiór danych, punktem wyjścia jest zakodowanie danych w obwód kwantowy. Innymi słowy, musimy odwzorować nasze dane na przestrzeń Hilberta stanów naszego komputera kwantowego. Robimy to, konstruując obwód zależny od danych. Istnieje wiele sposobów, aby to zrobić, a poprzednia lekcja przedstawiła szereg opcji. Możesz skonstruować własny obwód do zakodowania swoich danych lub użyć gotowego odwzorowania cech, takiego jak zz_feature_map. W tej lekcji zrobimy jedno i drugie.

Zauważ, że aby obliczyć pojedynczy element macierzy jądra, będziemy chcieli zakodować dwa różne punkty, abyśmy mogli oszacować ich iloczyn skalarny. Pełny przepływ pracy jądra kwantowego będzie oczywiście obejmował wiele takich iloczynów skalarnych między odwzorowanymi wektorami danych, a także klasyczne metody uczenia maszynowego. Ale podstawowym iterowanym krokiem jest estymacja pojedynczego elementu macierzy jądra. W tym celu wybieramy obwód kwantowy zależny od danych i odwzorowujemy dwa wektory danych na przestrzeń cech.

Obwód jądra – przegląd klasyczny – tło

Do zadania generowania macierzy jądra szczególnie interesuje nas prawdopodobieństwo zmierzenia stanu 0N|0\rangle^{\otimes N}, w którym wszystkie NN kubitów znajduje się w stanie 0|0\rangle. Aby to zobaczyć, rozważmy, że obwód odpowiedzialny za kodowanie i odwzorowanie jednego wektora danych xi\vec{x}_i może być zapisany jako Φ(xi)\Phi(\vec{x}_i), a ten odpowiedzialny za kodowanie i odwzorowanie xj\vec{x}_j to Φ(xj)\Phi(\vec{x}_j), i oznaczmy odwzorowane stany

ψ(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}.

Te stany odwzorowaniem danych na wyższe wymiary, więc naszym pożądanym elementem jądra jest iloczyn skalarny

ψ(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}.

Jeśli działamy na domyślnym stanie początkowym 0N|0\rangle^{\otimes N} obydwoma obwodami Φ(xj)\Phi^\dagger(\vec{x}_j) i Φ(xi)\Phi(\vec{x}_i), to prawdopodobieństwo zmierzenia następnie stanu 0N|0\rangle^{\otimes N} wynosi

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

Jest to dokładnie wartość, której chcemy (z dokładnością do 2||^2). Warstwa pomiarowa naszego obwodu zwróci prawdopodobieństwa pomiarów (lub tak zwane "kwazi-prawdopodobieństwa", jeśli stosowane są pewne metody mitygacji błędów). Interesującym nas prawdopodobieństwem jest prawdopodobieństwo stanu zerowego, 0N|0\rangle^{\otimes N}.

Krok 2: Zoptymalizuj problem dla wykonania kwantowego

  • Wejście: Abstrakcyjny obwód, niezoptymalizowany dla konkretnego backendu
  • Wyjście: Docelowy obwód i obserwabla, zoptymalizowane dla wybranego QPU

W tym kroku użyjemy funkcji generate_preset_pass_manager z Qiskit, aby określić procedurę optymalizacji dla naszego obwodu w odniesieniu do rzeczywistego komputera kwantowego, na którym planujemy przeprowadzić eksperyment. Ustawiamy optimization_level=3, co oznacza, że użyjemy zdefiniowanego menedżera przebiegów, który zapewnia najwyższy poziom optymalizacji. W tym kontekście "optymalizacja" odnosi się do optymalizacji implementacji obwodu na rzeczywistym komputerze kwantowym. Obejmuje to takie aspekty, jak wybór fizycznych kubitów odpowiadających kubitom w abstrakcyjnym obwodzie kwantowym, co zminimalizuje głębokość bramek, lub wybór fizycznych kubitów o najniższych dostępnych współczynnikach błędu. Nie jest to bezpośrednio związane z optymalizacją problemu uczenia maszynowego (jak w klasycznych optymalizatorach, takich jak COBYLA).

W zależności od tego, jak zaimplementujesz krok 2, być może będziesz musiał zoptymalizować obwód więcej niż raz, ponieważ każda para punktów zaangażowana w element macierzy daje inny obwód do pomiaru.

Krok 3: Wykonanie z wykorzystaniem prymitywów Qiskit Runtime

  • Wejście: Docelowy obwód
  • Wyjście: Rozkład prawdopodobieństwa

Użyj prymitywu Sampler z Qiskit Runtime, aby zrekonstruować rozkład prawdopodobieństwa stanów uzyskanych z próbkowania obwodu. Zauważ, że możesz spotkać się z określeniem "kwazi-rozkład prawdopodobieństwa", terminem stosowanym tam, gdzie szum jest problemem i gdy wprowadzane są dodatkowe kroki, takie jak mitygacja błędów. W takich przypadkach suma wszystkich prawdopodobieństw może nie być dokładnie równa 1; stąd "kwazi-prawdopodobieństwo".

Krok 4: Przetwarzanie końcowe, zwróć wynik w klasycznym formacie

  • Wejście: Rozkład prawdopodobieństwa
  • Wyjście: Pojedynczy element macierzy jądra lub macierz jądra w przypadku powtarzania

Oblicz prawdopodobieństwo zmierzenia 0N|0\rangle^{\otimes N} na obwodzie kwantowym i wypełnij macierz jądra w pozycji odpowiadającej dwóm użytym wektorom danych. Aby wypełnić całą macierz jądra, musimy uruchomić eksperyment kwantowy dla każdego elementu. Gdy już mamy macierz jądra, możemy jej użyć w wielu klasycznych algorytmach uczenia maszynowego, które akceptują pre-calculated kernels. Na przykład: qml_svc = SVC(kernel="precomputed"). Następnie możemy użyć klasycznych przepływów pracy, aby zastosować nasz model na naszych danych testowych i uzyskać wynik dokładności. W zależności od naszej satysfakcji z wyniku dokładności, być może będziemy musieli ponownie przejrzeć aspekty naszego obliczenia, takie jak nasze odwzorowanie cech.

Zarys lekcji

W tej lekcji przeprowadzimy te kroki na kilka sposobów, aby optymalnie wykorzystać Twój czas na rzeczywistych komputerach kwantowych. Zastosujemy kwantową metodę jądrową do

  • Pojedynczego elementu macierzy jądra dla danych o stosunkowo niewielkiej liczbie cech, przy użyciu rzeczywistego backendu, abyśmy mogli łatwo śledzić, co dzieje się na każdym kroku.
  • Całego zbioru danych o stosunkowo niewielkiej liczbie cech, przy użyciu symulowanego backendu, abyśmy mogli zobaczyć, jak kwantowy przepływ pracy łączy się z klasycznymi metodami uczenia maszynowego
  • Pojedynczego elementu macierzy jądra dla danych z wieloma cechami, przy użyciu rzeczywistego komputera kwantowego. Nie będziemy estymować całej macierzy jądra dla dużego zbioru danych, aby oszczędzić czas na komputerach kwantowych IBM®.
# 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

Pojedynczy element macierzy jądra

Krok 1: Odwzoruj klasyczne dane wejściowe na problem kwantowy

Rozważmy najpierw zbiór danych z zaledwie kilkoma cechami, powiedzmy 10. Zbiór danych może być dowolnie duży, ponieważ obliczamy elementy macierzy jądra pojedynczo. Potrzebujemy co najmniej dwóch punktów, więc od tego zaczniemy (w następnym przykładzie zaimportujemy pełny zbiór danych). Zaimportujmy kilka potrzebnych pakietów:

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]]

Możemy spróbować użyć z_feature_map.

# 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])

Te dwie unitarne transformacje powyżej dokładnie odpowiadają U1U_1 i U2U_2 opisanym we wprowadzeniu. Możemy je połączyć za pomocą unitary_overlap. Jak zawsze, chcemy mieć oko na głębokość naszego obwodu.

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

Wynik poprzedniej komórki kodu

Krok 2: Zoptymalizuj problem dla wykonania kwantowego

Zaczynamy od wybrania najmniej obciążonego backendu, a następnie optymalizujemy nasz obwód do uruchomienia na tym backendzie.

# 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)

W przypadku skomplikowanych obwodów ten krok znacząco zwiększy głębokość obwodu, ponieważ dokonuje on mapowania na natywne bramki dla rzeczywistych komputerów kwantowych, a informacje mogą wymagać przenoszenia z kubitu na kubit. W tym prostym przypadku głębokość prawie nie ulega zmianie.

print("circuit depth = ", overlap_ibm.decompose().depth())
overlap_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)
circuit depth =  10
1

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

Składnia uruchamiania na symulatorze jest zakomentowana poniżej. Dla tego zbioru danych, z małą liczbą cech, uruchomienie na symulatorze jest nadal opcją. Dla obliczeń na skalę użytkową symulacja zazwyczaj nie jest wykonalna. Symulatorów należy używać wyłącznie do debugowania kodu w zmniejszonej skali.

# 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()

Krok 4: Przetwarzanie końcowe, zwrócenie wyniku w formacie klasycznym

Jak opisano we wprowadzeniu, najbardziej użytecznym pomiarem jest tutaj prawdopodobieństwo zmierzenia stanu zerowego 00000|00000\rangle.

counts.get(0, 0.0) / num_shots
0.6525

To jest wynik, którego oczekiwaliśmy: oszacowanie iloczynu skalarnego (z dokładnością do modułu kwadratu) wektorów odpowiadających dwóm punktom danych. Jeśli chcemy przyjrzeć się pełnemu rozkładowi prawdopodobieństw pomiarów (lub kwaziprawdopodobieństw), możemy to zrobić za pomocą funkcji plot_distribution, jak pokazano poniżej. Widać, że dla dużej liczby kubitów takie obrazy szybko stają się nieprzejrzyste.

from qiskit.visualization import plot_distribution

plot_distribution(counts_bit)

Wynik poprzedniej komórki kodu

Alternatywnie można zdefiniować wizualizację taką jak poniższa, aby przyjrzeć się tylko 10 najbardziej prawdopodobnym pomiarom. Może to być ważne przy rozwiązywaniu problemów lub próbie zdobycia większej intuicji dotyczącej danych. Ale prawdopodobieństwo pomiaru stanu zerowego jest naszym elementem macierzy jądra.

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)

Wynik poprzedniej komórki kodu

Na podstawie tej informacji o tylko jednym iloczynie skalarnym między dwoma punktami danych w przestrzeni cech o wyższym wymiarze możemy jedynie stwierdzić, że ich nakładanie się jest dość duże w porównaniu z maksymalnym nakładaniem (które wynosiłoby 1,0). Może to być wskaźnikiem, że te dwa punkty danych są w jakiś sposób podobne pod względem natury i zostaną zaklasyfikowane do tej samej klasy. Albo może to być wskaźnikiem, że nasze mapowanie cech nie jest skuteczne w mapowaniu do przestrzeni, w której podobne dane mają silne nakładanie, a niepodobne dane mają małe nakładanie. Aby dowiedzieć się, co jest prawdą, musimy zastosować nasze mapowanie cech do całego zbioru danych i sprawdzić, czy wynikowa macierz jądra może być manipulowana w sposób umożliwiający skuteczne oddzielenie klas z wysoką dokładnością.

Warto zauważyć, że użyliśmy z_feature_map, co zaowocowało niską dwukubitową głębokością po transpilacji (w rzeczywistości głębokością 1). Jeśli wasze obwody staną się zbyt głębokie, z pewnością spowoduje to duży szum, co sprawi, że prawdopodobieństwo pomiaru stanu zerowego będzie bardzo niskie, nawet jeśli wasze mapowanie cech jest dobrze dopasowane do waszych danych. Na przykład powtórzenie powyższego procesu z użyciem zz_feature_map oraz , entanglement='linear', reps=1 dało dist.get(0,0.0) = 0.0015 dla tych samych punktów danych. Wynika to ze znacznie większych głębokości obwodu i głębokości dwukubitowych w zz_feature_map. Poniższy rysunek pokazuje rozkład prawdopodobieństwa dla tego obliczenia.

Złe wyniki z mapowania cech zz.

Warto poeksperymentować z kilkoma punktami danych z tej samej kategorii, aby zobaczyć, jak niska musi być wasza głębokość, aby uzyskać dobre wyniki. Poniższe porady są ogólne i z pewnością mają wyjątki. Generalnie dwukubitowa głębokość po transpilacji wynosząca 10 lub mniej nie powinna stanowić problemu. Dwukubitowa głębokość po transpilacji wynosząca 50-60 jest na poziomie najnowszych osiągnięć i będzie wymagać zaawansowanego ograniczania błędów i innych narzędzi. Pomiędzy tymi wartościami wasze wyniki mogą się różnić w zależności od podobieństwa danych, ekspresyjności mapowania cech, szerokości obwodu i innych czynników. Zwykle krok przetwarzania końcowego obejmowałby również klasyczne procesy uczenia maszynowego. W następnej sekcji rozszerzymy ten proces na cały zbiór danych i pokażemy klasyczny przepływ pracy uczenia maszynowego.

Sprawdź swoje zrozumienie

Przeczytaj poniższe pytania, zastanów się nad swoimi odpowiedziami, a następnie kliknij trójkąty, aby ujawnić rozwiązania.

W 10-kubitowym obwodzie kwantowym, ogólnie rzecz biorąc, ile różnych stanów można potencjalnie zmierzyć?

Odpowiedź:

2102^{10} lub 1024.

Załóżmy, że ktoś nowy w obliczeniach kwantowych próbuje użyć obwodu kwantowego, który ma bardzo dużą głębokość dwukubitową, i nie stosuje ograniczania błędów. Załóżmy dalej, że skutkuje to współczynnikiem błędu 10% na każdym kubicie. Jeśli prawdziwy (wolny od błędów) element macierzy jądra odpowiadający temu obwodowi jest bardzo duży, powiedzmy 1,0, jakie byłoby prawdopodobieństwo zmierzenia wszystkich 10 kubitów w stanie, w którym każdy kubit jest w stanie |0>?

Odpowiedź:

Prawdopodobieństwo poprawnego znalezienia każdego kubitu w stanie |0> wynosi 0,90. Prawdopodobieństwo znalezienia wszystkich 10 kubitów w poprawnym stanie wynosi 0.90100.90^{10} lub około 35%.

Wyjaśnij własnymi słowami, dlaczego tak ważne jest monitorowanie głębokości obwodów. Jest to prawdą ogólnie, ale wyjaśnij to w kontekście estymacji jądra kwantowego.

Odpowiedź:

W tym przepływie pracy QKE nasze szacunki opierają się na pomiarach stanu zerowego, czyli stanu, w którym każdy kubit znajduje się w stanie 0|0\rangle. Bardzo głębokie obwody wprowadzą wysokie współczynniki błędów. Gdy ten współczynnik błędu kumuluje się na wielu kubitach, znacznie zmniejszy to prawdopodobieństwo zmierzenia stanu zerowego.

Pełna macierz jądra

W tej sekcji rozszerzymy powyższy proces na klasyfikację binarną pełnego zbioru danych. Wprowadzi to dwa ważne elementy: (1) możemy teraz zaimplementować klasyczne uczenie maszynowe w przetwarzaniu końcowym oraz (2) możemy uzyskać oceny dokładności dla naszego treningu.

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

Teraz zaimportujemy istniejący zbiór danych do naszej klasyfikacji. Ten zbiór danych składa się ze 128 wierszy (punktów danych) i 14 cech na każdym punkcie. Istnieje 15. element, który wskazuje binarną kategorię każdego punktu (±1\pm 1). Zbiór danych jest importowany poniżej lub możesz uzyskać dostęp do zbioru danych i wyświetlić jego strukturę tutaj.

Użyjemy pierwszych 90 punktów danych do treningu, a kolejnych 30 punktów do testowania.

!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]

Przygotujemy już miejsce na przechowywanie wielu wyników, konstruując macierz jądra oraz macierz testową o odpowiednich wymiarach.

# 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)

Teraz tworzymy odwzorowanie cech (feature map) służące do zakodowania i odwzorowania naszych klasycznych danych w obwodzie kwantowym. Możemy skonstruować własne odwzorowanie cech lub skorzystać z gotowego. Zachęcamy do modyfikowania poniższego odwzorowania cech lub powrotu do ZFeatureMap. Zawsze jednak należy zwracać uwagę na głębokość obwodu. Przypomnijmy, że w poprzednim przykładzie z 6 kubitami głębokość obwodu po transpilacji była nieakceptowalnie duża przy użyciu zz_feature_map. Wraz ze wzrostem skali i złożoności obwodu jego głębokość może szybko narastać do poziomu, przy którym szum przytłacza nasze wyniki. Zawsze, gdy wiesz coś o strukturze swoich danych, co może podpowiedzieć, jaka struktura odwzorowania cech byłaby najbardziej użyteczna, warto utworzyć własne, dostosowane odwzorowanie cech, które wykorzystuje tę wiedzę.

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)

Kroki 2 i 3: Optymalizacja problemu i wykonanie przy użyciu prymitywów

Skonstruujemy obwód nakładania (overlap circuit), a gdybyśmy w tym przykładzie uruchamiali go na prawdziwym komputerze kwantowym, zoptymalizowalibyśmy go pod kątem wykonania tak jak poprzednio. Jednak w tym przypadku zamierzamy przejść przez wszystkie punkty danych i obliczyć pełną macierz jądra. Dla każdej pary wektorów danych xi\vec{x}_i i xj\vec{x}_j tworzymy inny obwód nakładania. Musimy więc optymalizować nasz obwód dla każdej pary punktów danych. Zatem kroki 2 i 3 wykonywane byłyby razem w ramach wielu iteracji.

Poniższa komórka kodu wykonuje dokładnie ten sam proces, co wcześniej, dla pojedynczej pary punktów danych. Tym razem jest on po prostu wykonywany wewnątrz dwóch pętli for, a na końcu dodano linię kernel_matrix[x_1,x_2] = ..., aby zapisać wyniki każdego obliczenia. Zauważ, że wykorzystaliśmy symetrię macierzy jądra, aby zmniejszyć liczbę obliczeń o połowę. Ustawiliśmy też po prostu elementy diagonalne na 1, takie jakie powinny być przy braku szumu. W zależności od implementacji i wymaganej precyzji można również wykorzystać elementy diagonalne do oszacowania szumu lub poznania go w celach mitygacji błędów.

Gdy macierz jądra zostanie w pełni wypełniona, powtarzamy proces dla danych testowych i wypełniamy test_matrix. W rzeczywistości jest to także macierz jądra; po prostu nadajemy jej inną nazwę, aby odróżnić te dwie.

# 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

Krok 4: Przetwarzanie końcowe, zwrócenie wyniku w formacie klasycznym

Teraz, gdy dysponujemy macierzą jądra i sformatowaną analogicznie macierzą test_matrix uzyskanymi metodami kwantowych jąder, możemy zastosować klasyczne algorytmy uczenia maszynowego, aby prognozować nasze dane testowe i sprawdzić ich dokładność. Zaczniemy od zaimportowania sklearn.svc ze Scikit-Learn, czyli klasyfikatora wektorów nośnych (support vector classifier, SVC). Musimy wskazać, że chcemy, aby SVC użył naszego wstępnie obliczonego jądra, ustawiając 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")

Korzystając z SVC.fit, możemy teraz podać macierz jądra i etykiety treningowe, aby uzyskać dopasowanie. Następnie SVC.score oceni nasze dane testowe względem tego dopasowania, używając naszej test_matrix, i zwróci dokładność.

# 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

Widzimy, że dokładność naszego wytrenowanego modelu wyniosła 100%. To świetnie i pokazuje, że QKE może działać. Ale to bardzo różne od przewagi kwantowej. Klasyczne jądra prawdopodobnie również byłyby w stanie rozwiązać ten problem klasyfikacji ze 100% dokładnością. Pozostaje wiele pracy nad scharakteryzowaniem różnych typów danych i relacji między nimi, aby zobaczyć, gdzie kwantowe jądra będą najbardziej przydatne w obecnej erze użyteczności. Pozostawiamy uczącemu się możliwość modyfikacji części tego przepływu pracy oraz zbadania skuteczności różnych kwantowych odwzorowań cech. Oto kilka rzeczy do rozważenia:

  • Jak odporna jest dokładność? Czy utrzymuje się dla szerokich typów danych, czy tylko dla tych konkretnych danych treningowych?
  • Jaka struktura w Twoich danych sprawia, że podejrzewasz, iż kwantowa mapa cech będzie przydatna?
  • Jak dokładność zmienia się w zależności od zwiększania/zmniejszania ilości danych treningowych?
  • Jakie mapy cech możesz zastosować i jak wyniki różnią się w zależności od mapy cech?
  • Jak dokładność i czas wykonania zmieniają się wraz ze wzrostem liczby cech?
  • Których trendów, jeśli w ogóle, oczekujesz, że utrzymają się na prawdziwych komputerach kwantowych?

Skalowanie do większej liczby cech i kubitów

W tej sekcji powtórzymy obliczenie pojedynczego elementu macierzy, ale dla znacznie większej liczby cech, szkicując drogę do skalowania w kierunku użyteczności. Ograniczenie do pojedynczego elementu macierzy zostało dokonane, aby proces mógł zostać pokazany bez zużywania zbyt dużej części przydzielonego Ci czasu na komputerach kwantowych.

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

Założymy jako punkt wyjścia zbiór danych, w którym każdy punkt danych ma 42 cechy. Podobnie jak w pierwszym przykładzie, obliczymy pojedynczy element macierzy jądra, co wymaga dwóch punktów danych. Dwa punkty poniżej mają 42 cechy i jedną zmienną kategoryczną (±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]]

Przypomnij sobie, że zz_feature_map generowała dość głębokie obwody w przypadku stosunkowo niewielkiej liczby cech (14 cech). W miarę jak zwiększamy liczbę cech, musimy uważnie monitorować głębokość obwodu. Aby to zilustrować, najpierw spróbujemy użyć zz_feature_map i sprawdzimy głębokość powstałego obwodu.

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

Jak opisano wcześniej, określenie, jak duża głębokość jest zbyt duża, jest kwestią zniuansowaną. Jednak dwukubitowa głębokość większa niż 100, nawet przed transpilacją, dyskwalifikuje obwód. Właśnie dlatego w tej lekcji kładziono nacisk na niestandardowe mapy cech. Jeśli wiesz coś o strukturze całego swojego zbioru danych, powinieneś zaprojektować mapę splątania z tą strukturą na uwadze. Tutaj, ponieważ obliczamy jedynie iloczyn skalarny między dwoma takimi punktami danych, przedłożyliśmy małą głębokość obwodu nad szczegółowe rozważania dotyczące struktury danych.

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()

Nie będziemy jeszcze sprawdzać głębokości, ponieważ tak naprawdę liczy się głębokość dwukubitowa po transpilacji.

Krok 2: Zoptymalizuj problem pod kątem wykonania kwantowego

Zaczynamy od wybrania najmniej obciążonego backendu, a następnie optymalizujemy nasz obwód pod kątem uruchomienia na tym backendzie.

# 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')>

W przypadku zadań o niewielkiej skali preset pass manager często niezawodnie zwraca ten sam obwód o tej samej głębokości. Jednak w bardzo dużych, złożonych obwodach pass manager może za każdym razem zwracać inne obwody po transpilacji. Dzieje się tak, ponieważ korzysta on z heurystyk oraz dlatego, że bardzo duże obwody mają skomplikowaną przestrzeń możliwych optymalizacji. Często warto przeprowadzić transpilację kilka razy i wybrać najpłytszy obwód. Wprowadza to jedynie dodatkowe obciążenie klasyczne i może znacząco poprawić wyniki z komputera kwantowego.

Tutaj transpilujemy obwód unitary overlap 20 razy i przyglądamy się uzyskanym głębokościom obwodów.

# 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]

Tutaj widać, że całkowita głębokość bramek różni się nieco w zależności od przebiegu transpilacji. Nasz obwód nie jest jeszcze na tyle głęboki/szeroki, aby było widać zróżnicowanie w głębokościach dwukubitowych po transpilacji. Użyjemy transpiled_qcs[1], który ma głębokość 60, tylko nieznacznie mniejszą niż głębokość najgłębszego uzyskanego obwodu, wynosząca 77.

overlap_ibm = transpiled_qcs[1]

Krok 3: Wykonaj za pomocą prymitywów Qiskit Runtime

W miarę jak zbliżamy się skalą do użyteczności, symulatory przestaną być przydatne. Tutaj pokazano jedynie składnię dla prawdziwych komputerów kwantowych.

# 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()

Krok 4: Przetwarzanie końcowe, zwrócenie wyniku w formacie klasycznym

Jak opisano we wstępie, najbardziej użytecznym pomiarem jest tutaj prawdopodobieństwo pomiaru stanu zerowego 00000|00000\rangle.

counts.get(0, 0.0) / num_shots
0.0138

Ten proces dla pojedynczego elementu macierzy jądra można powtórzyć między innymi parami danych z zestawu, aby uzyskać pełną macierz jądra. Wymiar macierzy jądra jest określony przez liczbę punktów w danych treningowych, a nie przez liczbę cech. Zatem koszt obliczeniowy przekształcenia macierzy jądra w model predykcyjny nie skaluje się tak jak liczba cech czy kubitów. Nawet dla stosunkowo małych zestawów danych o dużej liczbie cech dane nadal musiałyby być dopasowane do odwzorowania cech, które zapewni skuteczną klasyfikację.

Skalowanie i przyszłe prace

Metoda jądrowa wymaga, abyśmy mierzyli 0|0\rangle tak dokładnie, jak to możliwe. Jednak błędy bramek i błędy odczytu oznaczają, że istnieje pewne niezerowe prawdopodobieństwo pp, że dany kubit zostanie błędnie zmierzony jako będący w stanie 1|1\rangle. Nawet przy nadmiernym uproszczeniu, że prawdopodobieństwo 0|0\rangle powinno wynosić 100%100\%, dla wielu cech zakodowanych na, powiedzmy, NN bitach, prawdopodobieństwo poprawnego zmierzenia wszystkich bitów jako 0|0\rangle zmniejsza się do (1p)N(1-p)^N. Gdy NN staje się duże, metoda ta staje się coraz mniej niezawodna. Pokonanie tej trudności i skalowanie estymacji jądra do coraz większej liczby cech jest obecnie obszarem badań. Aby dowiedzieć się więcej na ten temat, zobacz tę pracę autorstwa Thanasilpa, Wanga, Cerezo i Holmes. Zachęcamy do eksploracji możliwości obecnych komputerów kwantowych, a także do wyczekiwania tego, co będzie możliwe w erze korekcji błędów.

Podsumowanie

Obliczanie jądra kwantowego obejmuje

  • obliczanie wpisów macierzy jądra, przy użyciu par punktów danych treningowych
  • kodowanie danych i ich odwzorowanie poprzez mapowanie cech
  • optymalizację obwodu pod kątem uruchamiania na rzeczywistych komputerach kwantowych / backendach

Jądro kwantowe może być następnie użyte w klasycznych algorytmach uczenia maszynowego, tak jak w tej lekcji.

Oto kilka kluczowych kwestii, o których należy pamiętać podczas stosowania jąder kwantowych:

  • Czy zbiór danych prawdopodobnie skorzysta z metod jąder kwantowych?
  • Wypróbuj różne mapowania cech i schematy splątania.
  • Czy głębokość obwodu jest akceptowalna?
  • Spróbuj uruchomić pass manager wiele razy i użyj obwodu o możliwie najmniejszej głębokości.

Metody jąder kwantowych są potencjalnie potężnymi narzędziami, pod warunkiem odpowiedniego dopasowania między zbiorami danych o cechach sprzyjających obliczeniom kwantowym a odpowiednim mapowaniem cech kwantowych. Aby lepiej zrozumieć, gdzie jądra kwantowe mogą być użyteczne, polecamy lekturę Liu, Arunachalam i Temme (2021).