Funkcje kosztu
Podczas tej lekcji nauczymy się, jak obliczać funkcję kosztu:
- Najpierw poznamy prymitywy Qiskit Runtime
- Zdefiniujemy funkcję kosztu . Jest to funkcja specyficzna dla problemu, która określa cel problemu do minimalizacji (lub maksymalizacji) przez optymalizator
- Zdefiniujemy strategię pomiaru z użyciem prymitywów Qiskit Runtime, aby zoptymalizować szybkość względem dokładności
Prymitywy
Wszystkie układy fizyczne, zarówno klasyczne, jak i kwantowe, mogą istnieć w różnych stanach. Na przykład samochód na drodze może mieć określoną masę, położenie, prędkość lub przyspieszenie, które charakteryzują jego stan. Podobnie układy kwantowe również mogą mieć różne konfiguracje lub stany, ale różnią się one od układów klasycznych tym, jak traktujemy pomiary i ewolucję stanu. Prowadzi to do unikalnych właściwości, takich jak superpozycja i splątanie, które są wyłączne dla mechaniki kwantowej. Podobnie jak możemy opisać stan samochodu przy użyciu właściwości fizycznych, takich jak prędkość czy przyspieszenie, tak samo możemy opisać stan układu kwantowego za pomocą obserwabli, które są obiektami matematycznymi.
W mechanice kwantowej stany są reprezentowane przez znormalizowane zespolone wektory kolumnowe, czyli kety (), a obserwable są hermitowskimi operatorami liniowymi (), które działają na ketach. Wektor własny () obserwabli jest nazywany stanem własnym. Pomiar obserwabli dla jednego z jej stanów własnych () da nam odpowiadającą wartość własną () jako odczyt.
Jeśli zastanawiasz się, jak mierzyć układ kwantowy i co można mierzyć, Qiskit oferuje dwa prymitywy, które mogą pomóc:
Sampler: Mając dany stan kwantowy , ten prymityw uzyskuje prawdopodobieństwo każdego możliwego stanu bazy obliczeniowej.Estimator: Mając daną obserwablę kwantową i stan , ten prymityw oblicza wartość oczekiwaną .
Prymityw Sampler
Prymityw Sampler oblicza prawdopodobieństwo uzyskania każdego możliwego stanu z bazy obliczeniowej, mając dany obwód kwantowy, który przygotowuje stan . Oblicza on
Gdzie to liczba kubitów, a to całkowita reprezentacja dowolnego możliwego wyjściowego ciągu binarnego (czyli liczby całkowite w bazie ).
Qiskit Runtime Sampler wykonuje obwód wielokrotnie na urządzeniu kwantowym, przeprowadzając pomiary przy każdym uruchomieniu, i rekonstruuje rozkład prawdopodobieństwa z odzyskanych ciągów bitów. Im więcej uruchomień (lub strzałów) wykonuje, tym dokładniejsze będą wyniki, ale wymaga to więcej czasu i zasobów kwantowych.
Jednakże, ponieważ liczba możliwych wyników rośnie wykładniczo wraz z liczbą kubitów (czyli ), liczba strzałów również będzie musiała rosnąć wykładniczo, aby uchwycić gęsty rozkład prawdopodobieństwa. Dlatego Sampler jest wydajny tylko dla rzadkich rozkładów prawdopodobieństwa; gdzie stan docelowy musi być wyrażalny jako kombinacja liniowa stanów bazy obliczeniowej, przy czym liczba składników rośnie co najwyżej wielomianowo wraz z liczbą kubitów:
Sampler można również skonfigurować tak, aby pobierał prawdopodobieństwa z podsekcji obwodu, reprezentując podzbiór wszystkich możliwych stanów.
Prymityw Estimator
Prymityw Estimator oblicza wartość oczekiwaną obserwabli dla stanu kwantowego ; gdzie prawdopodobieństwa obserwabli można wyrazić jako , przy czym to stany własne obserwabli . Wartość oczekiwana jest zatem zdefiniowana jako średnia wszystkich możliwych wyników (czyli wartości własnych obserwabli) pomiaru stanu , ważona odpowiednimi prawdopodobieństwami:
Jednak obliczenie wartości oczekiwanej obserwabli nie zawsze jest możliwe, ponieważ często nie znamy jej bazy własnej. Qiskit Runtime Estimator wykorzystuje złożony proces algebraiczny do oszacowania wartości oczekiwanej na rzeczywistym urządzeniu kwantowym, rozkładając obserwablę na kombinację innych obserwabli, których bazę własną znamy.
Mówiąc prościej, Estimator rozkłada każdą obserwablę, której nie wie, jak zmierzyć, na prostsze, mierzalne obserwable zwane operatorami Pauli.
Każdy operator można wyrazić jako kombinację operatorów Pauli.
tak, że
gdzie to liczba kubitów, dla (czyli liczby całkowite w bazie ) oraz .
Po wykonaniu tej dekompozycji, Estimator wyprowadza nowy obwód dla każdej obserwabli (z oryginalnego obwodu), aby skutecznie zdiagonalizować obserwablę Pauli w bazie obliczeniowej i zmierzyć ją. Możemy łatwo mierzyć obserwable Pauli, ponieważ znamy z góry, co nie jest generalnie prawdziwe dla innych obserwabli.
Dla każdego , Estimator uruchamia odpowiedni obwód na urządzeniu kwantowym wielokrotnie, mierzy stan wyjściowy w bazie obliczeniowej i oblicza prawdopodobieństwo uzyskania każdego możliwego wyjścia . Następnie szuka wartości własnej odpowiadającej każdemu wyjściu , mnoży przez i sumuje wszystkie wyniki, aby otrzymać wartość oczekiwaną obserwabli dla danego stanu .
Ponieważ obliczanie wartości oczekiwanej operatorów Pauli jest niepraktyczne (czyli rośnie wykładniczo), Estimator może być wydajny tylko wtedy, gdy duża liczba wynosi zero (czyli rzadka dekompozycja Pauli zamiast gęstej). Formalnie mówimy, że aby to obliczenie było efektywnie rozwiązywalne, liczba niezerowych składników musi rosnąć co najwyżej wielomianowo wraz z liczbą kubitów :
Czytelnik może zauważyć niejawne założenie, że próbkowanie prawdopodobieństwa również musi być wydajne, jak wyjaśniono dla Sampler, co oznacza
Przewodnik na przykładzie obliczania wartości oczekiwanych
Załóżmy stan jednokubitowy oraz obserwablę
o następującej teoretycznej wartości oczekiwanej
Ponieważ nie wiemy, jak zmierzyć tę obserwablę, nie możemy obliczyć jej wartości oczekiwanej bezpośrednio i musimy wyrazić ją ponownie jako . Można pokazać, że daje ona ten sam wynik, zauważając, że oraz .
Zobaczmy, jak bezpośrednio obliczyć oraz . Ponieważ i nie komutują (czyli nie dzielą tej samej bazy własnej), nie można ich mierzyć jednocześnie, dlatego potrzebujemy obwodów pomocniczych:
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-aer qiskit-ibm-runtime rustworkx
from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
# The following code will work for any other initial single-qubit state and observable
original_circuit = QuantumCircuit(1)
original_circuit.h(0)
H = SparsePauliOp(["X", "Z"], [2, -1])
aux_circuits = []
for pauli in H.paulis:
aux_circ = original_circuit.copy()
aux_circ.barrier()
if str(pauli) == "X":
aux_circ.h(0)
elif str(pauli) == "Y":
aux_circ.sdg(0)
aux_circ.h(0)
else:
aux_circ.id(0)
aux_circ.measure_all()
aux_circuits.append(aux_circ)
original_circuit.draw("mpl")
# Auxiliary circuit for X
aux_circuits[0].draw("mpl")
# Auxiliary circuit for Z
aux_circuits[1].draw("mpl")
Możemy teraz ręcznie przeprowadzić obliczenia za pomocą Sampler i sprawdzić wyniki na Estimator:
from qiskit.primitives import StatevectorSampler, StatevectorEstimator
from qiskit.result import QuasiDistribution
import numpy as np
## SAMPLER
shots = 10000
sampler = StatevectorSampler()
job = sampler.run(aux_circuits, shots=shots)
# Run the sampler job and step through results
expvals = []
for index, pauli in enumerate(H.paulis):
data_pub = job.result()[index].data
bitstrings = data_pub.meas.get_bitstrings()
counts = data_pub.meas.get_counts()
quasi_dist = QuasiDistribution(
{outcome: freq / shots for outcome, freq in counts.items()}
)
# Use the probabilities and known eigenvalues of Pauli operators to estimate the expectation value.
val = 0
if str(pauli) == "X":
val += -1 * quasi_dist.get(1, 0)
val += 1 * quasi_dist.get(0, 0)
if str(pauli) == "Y":
val += -1 * quasi_dist.get(1, 0)
val += 1 * quasi_dist.get(0, 0)
if str(pauli) == "Z":
val += 1 * quasi_dist.get(0, 0)
val += -1 * quasi_dist.get(1, 0)
expvals.append(val)
# Print expectation values
print("Sampler results:")
for pauli, expval in zip(H.paulis, expvals):
print(f" >> Expected value of {str(pauli)}: {expval:.5f}")
total_expval = np.sum(H.coeffs * expvals).real
print(f" >> Total expected value: {total_expval:.5f}")
# Use estimator for comparison
observables = [
*H.paulis,
H,
] # Note: run for individual Paulis as well as full observable H
estimator = StatevectorEstimator()
job = estimator.run([(original_circuit, observables)])
estimator_expvals = job.result()[0].data.evs
# Print results
print("Estimator results:")
for obs, expval in zip(observables, estimator_expvals):
if obs is not H:
print(f" >> Expected value of {str(obs)}: {expval:.5f}")
else:
print(f" >> Total expected value: {expval:.5f}")
Sampler results:
>> Expected value of X: 1.00000
>> Expected value of Z: 0.00420
>> Total expected value: 1.99580
Estimator results:
>> Expected value of X: 1.00000
>> Expected value of Z: 0.00000
>> Total expected value: 2.00000
Rygor matematyczny (opcjonalnie)
Wyrażając względem bazy stanów własnych operatora , , otrzymujemy:
Ponieważ nie znamy wartości własnych ani stanów własnych docelowej obserwabli , musimy najpierw rozważyć jej diagonalizację. Biorąc pod uwagę, że jest hermitowski, istnieje przekształcenie unitarne takie, że gdzie jest diagonalną macierzą wartości własnych, zatem jeśli oraz .
Oznacza to, że wartość oczekiwaną można przepisać jako:
Biorąc pod uwagę, że jeśli układ znajduje się w stanie , to prawdopodobieństwo pomiaru wynosi , powyższą wartość oczekiwaną można wyrazić jako:
Bardzo ważne jest, aby zauważyć, że prawdopodobieństwa są brane ze stanu , a nie . Dlatego macierz jest absolutnie niezbędna. Być może zastanawiasz się, jak uzyskać macierz i wartości własne . Gdybyś już miał wartości własne, nie byłoby potrzeby używania komputera kwantowego, ponieważ celem algorytmów wariacyjnych jest znalezienie tych wartości własnych operatora .
Na szczęście istnieje sposób na obejście tego: dowolna macierz może być zapisana jako kombinacja liniowa iloczynów tensorowych macierzy Pauliego i identyczności, z których wszystkie są zarówno hermitowskie, jak i unitarne, ze znanymi i . Tak właśnie działa wewnętrznie Estimator Runtime, dekomponując dowolny obiekt Operator na SparsePauliOp.
Oto operatory, których można użyć:
Przepiszmy zatem względem macierzy Pauliego i identyczności:
gdzie dla (czyli w bazie ), a :
gdzie oraz , takie że:
Funkcje kosztu
Ogólnie rzecz biorąc, funkcje kosztu służą do opisania celu problemu oraz tego, jak dobrze stan próbny spełnia ten cel. Tę definicję można stosować w różnych przykładach z chemii, uczenia maszynowego, finansów, optymalizacji itd.
Rozważmy prosty przykład znajdowania stanu podstawowego układu. Naszym celem jest zminimalizowanie wartości oczekiwanej obserwabli reprezentującej energię (Hamiltonian ):
Możemy użyć Estimator do wyznaczenia wartości oczekiwanej i przekazać tę wartość do optymalizatora w celu minimalizacji. Jeśli optymalizacja zakończy się sukcesem, zwróci ona zestaw optymalnych wartości parametrów , z których będziemy w stanie skonstruować proponowany stan rozwiązania i obliczyć obserwowaną wartość oczekiwaną jako .
Zauważ, że będziemy w stanie minimalizować funkcję kosztu jedynie dla ograniczonego zbioru stanów, które rozważamy. Prowadzi to do dwóch odrębnych możliwości:
- Nasz ansatz nie definiuje stanu rozwiązania w całej przestrzeni poszukiwań: Jeśli tak jest, nasz optymalizator nigdy nie znajdzie rozwiązania i musimy eksperymentować z innymi ansatzami, które mogą dokładniej reprezentować naszą przestrzeń poszukiwań.
- Nasz optymalizator nie jest w stanie znaleźć tego prawidłowego rozwiązania: Optymalizacja może być zdefiniowana globalnie i lokalnie. Zgłębimy znaczenie tych pojęć w dalszej części.
Podsumowując, będziemy wykonywać klasyczną pętlę optymalizacyjną, ale polegając na ewaluacji funkcji kosztu na komputerze kwantowym. Z tej perspektywy można myśleć o optymalizacji jako o czysto klasycznym przedsięwzięciu, w którym wywołujemy pewną wyrocznię kwantową typu czarna skrzynka za każdym razem, gdy optymalizator musi obliczyć funkcję kosztu.
def cost_func_vqe(params, circuit, hamiltonian, estimator):
"""Return estimate of energy from estimator
Parameters:
params (ndarray): Array of ansatz parameters
ansatz (QuantumCircuit): Parameterized ansatz circuit
hamiltonian (SparsePauliOp): Operator representation of Hamiltonian
estimator (Estimator): Estimator primitive instance
Returns:
float: Energy estimate
"""
pub = (circuit, hamiltonian, params)
cost = estimator.run([pub]).result()[0].data.evs
return cost
from qiskit.circuit.library import TwoLocal
observable = SparsePauliOp.from_list([("XX", 1), ("YY", -3)])
reference_circuit = QuantumCircuit(2)
reference_circuit.x(0)
variational_form = TwoLocal(
2,
rotation_blocks=["rz", "ry"],
entanglement_blocks="cx",
entanglement="linear",
reps=1,
)
ansatz = reference_circuit.compose(variational_form)
theta_list = (2 * np.pi * np.random.rand(1, 8)).tolist()
ansatz.decompose().draw("mpl")
Najpierw wykonamy to za pomocą symulatora: StatevectorEstimator. Jest to zazwyczaj zalecane do debugowania, ale zaraz po przebiegu debugowania przeprowadzimy obliczenia na prawdziwym sprzęcie kwantowym. Coraz częściej interesujące problemy nie są już klasycznie symulowalne bez najnowocześniejszych obiektów superkomputerowych.
estimator = StatevectorEstimator()
cost = cost_func_vqe(theta_list, ansatz, observable, estimator)
print(cost)
[-0.58744589]
Teraz przejdziemy do uruchomienia na prawdziwym komputerze kwantowym. Zwróć uwagę na zmiany w składni. Kroki dotyczące pass_manager zostaną omówione dalej w następnym przykładzie. Jednym ze szczególnie ważnych kroków w algorytmach wariacyjnych jest użycie sesji Qiskit Runtime. Rozpoczęcie sesji umożliwia uruchamianie wielu iteracji algorytmu wariacyjnego bez oczekiwania w nowej kolejce za każdym razem, gdy parametry są aktualizowane. Jest to istotne, jeśli czasy oczekiwania w kolejce są długie i/lub potrzeba wielu iteracji. Tylko partnerzy sieci IBM Quantum® Network mogą korzystać z sesji Runtime. Jeśli nie masz dostępu do sesji, możesz zmniejszyć liczbę iteracji wysyłanych w danym momencie i zapisać najnowsze parametry do wykorzystania w przyszłych przebiegach. Jeśli wyślesz zbyt wiele iteracji lub napotkasz zbyt długie czasy oczekiwania w kolejce, możesz napotkać kod błędu 1217, który odnosi się do długich opóźnień między wysyłaniem zadań.
# Estimated usage: < 1 min. Benchmarked at 7 seconds on an Eagle processor
# Load necessary packages:
from qiskit_ibm_runtime import (
QiskitRuntimeService,
Session,
EstimatorOptions,
EstimatorV2 as Estimator,
)
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
# Select the least busy backend:
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, min_num_qubits=ansatz.num_qubits, simulator=False
)
# Or get a specific backend:
# backend = service.backend("ibm_brisbane")
# Use a pass manager to transpile the circuit and observable for the specific backend being used:
pm = generate_preset_pass_manager(backend=backend, optimization_level=1)
isa_ansatz = pm.run(ansatz)
isa_observable = observable.apply_layout(layout=isa_ansatz.layout)
# Set estimator options
estimator_options = EstimatorOptions(resilience_level=1, default_shots=10_000)
# Open a Runtime session:
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
cost = cost_func_vqe(theta_list, isa_ansatz, isa_observable, estimator)
session.close()
print(cost)
Zauważ, że wartości uzyskane z dwóch powyższych obliczeń są bardzo podobne. Techniki ulepszania wyników zostaną omówione w dalszej części.
Przykład odwzorowania na układy niefizyczne
Problem maksymalnego cięcia (Max-Cut) to problem optymalizacji kombinatorycznej, który polega na podziale wierzchołków grafu na dwa rozłączne zbiory w taki sposób, aby liczba krawędzi między tymi dwoma zbiorami była maksymalna. Bardziej formalnie, mając nieskierowany graf , gdzie jest zbiorem wierzchołków, a jest zbiorem krawędzi, problem Max-Cut polega na podziale wierzchołków na dwa rozłączne podzbiory i w taki sposób, aby liczba krawędzi, których jeden koniec znajduje się w , a drugi w , była maksymalna.
Możemy zastosować Max-Cut do rozwiązania różnych problemów, w tym: klastrowania, projektowania sieci, przejść fazowych itp. Zaczniemy od utworzenia grafu problemu:
import rustworkx as rx
from rustworkx.visualization import mpl_draw
n = 4
G = rx.PyGraph()
G.add_nodes_from(range(n))
# The edge syntax is (start, end, weight)
edges = [(0, 1, 1.0), (0, 2, 1.0), (0, 3, 1.0), (1, 2, 1.0), (2, 3, 1.0)]
G.add_edges_from(edges)
mpl_draw(
G, pos=rx.shell_layout(G), with_labels=True, edge_labels=str, node_color="#1192E8"
)
Problem ten można wyrazić jako problem optymalizacji binarnej. Dla każdego węzła , gdzie jest liczbą węzłów grafu (w tym przypadku ), rozważymy zmienną binarną . Zmienna ta będzie miała wartość , jeśli węzeł należy do jednej z grup, którą oznaczymy , oraz , jeśli należy do drugiej grupy, którą oznaczymy jako . Oznaczymy również jako (element macierzy sąsiedztwa ) wagę krawędzi biegnącej od węzła do węzła . Ponieważ graf jest nieskierowany, . Wówczas nasz problem możemy sformułować jako maksymalizację następującej funkcji kosztu:
Aby rozwiązać ten problem za pomocą komputera kwantowego, wyrazimy funkcję kosztu jako wartość oczekiwaną pewnej obserwabli. Jednakże obserwable, które Qiskit natywnie obsługuje, składają się z operatorów Pauli, które mają wartości własne i zamiast i . Dlatego dokonamy następującej zamiany zmiennych:
Gdzie . Możemy wykorzystać macierz sąsiedztwa , aby wygodnie uzyskać dostęp do wag wszystkich krawędzi. Zostanie to użyte do uzyskania naszej funkcji kosztu:
Oznacza to, że:
Zatem nowa funkcja kosztu, którą chcemy zmaksymalizować, to:
Co więcej, naturalną tendencją komputera kwantowego jest znajdowanie minimów (zwykle najniższej energii) zamiast maksimów, więc zamiast maksymalizować , będziemy minimalizować:
Teraz, gdy mamy funkcję kosztu do minimalizacji, której zmienne mogą przyjmować wartości i , możemy wykonać następującą analogię z Pauli :
Innymi słowy, zmienna będzie odpowiadać bramce działającej na kubit . Ponadto:
Wtedy obserwablą, którą rozważymy, jest:
do której będziemy musieli później dodać niezależny człon:
Operator jest liniową kombinacją członów z operatorami Z na węzłach połączonych krawędzią (przypomnijmy, że kubit 0 znajduje się najbardziej na prawo): . Po skonstruowaniu operatora ansatz dla algorytmu QAOA można łatwo zbudować, używając obwodu QAOAAnsatz z biblioteki obwodów Qiskit.
from qiskit.circuit.library import QAOAAnsatz
from qiskit.quantum_info import SparsePauliOp
hamiltonian = SparsePauliOp.from_list(
[("IIZZ", 1), ("IZIZ", 1), ("IZZI", 1), ("ZIIZ", 1), ("ZZII", 1)]
)
ansatz = QAOAAnsatz(hamiltonian, reps=2)
# Draw
ansatz.decompose(reps=3).draw("mpl")
# Sum the weights, and divide by 2
offset = -sum(edge[2] for edge in edges) / 2
print(f"""Offset: {offset}""")
Offset: -2.5
Ponieważ Runtime Estimator bezpośrednio przyjmuje Hamiltonian i sparametryzowany ansatz oraz zwraca niezbędną energię, funkcja kosztu dla instancji QAOA jest dość prosta:
def cost_func(params, ansatz, hamiltonian, estimator):
"""Return estimate of energy from estimator
Parameters:
params (ndarray): Array of ansatz parameters
ansatz (QuantumCircuit): Parameterized ansatz circuit
hamiltonian (SparsePauliOp): Operator representation of Hamiltonian
estimator (Estimator): Estimator primitive instance
Returns:
float: Energy estimate
"""
pub = (ansatz, hamiltonian, params)
cost = estimator.run([pub]).result()[0].data.evs
# cost = estimator.run(ansatz, hamiltonian, parameter_values=params).result().values[0]
return cost
import numpy as np
x0 = 2 * np.pi * np.random.rand(ansatz.num_parameters)
estimator = StatevectorEstimator()
cost = cost_func_vqe(x0, ansatz, hamiltonian, estimator)
print(cost)
1.473098768180865
# Estimated usage: < 1 min, benchmarked at 6 seconds on ibm_osaka, 5-23-24
# Load some necessary packages:
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import Session, EstimatorV2 as Estimator
# Select the least busy backend:
backend = service.least_busy(
operational=True, min_num_qubits=ansatz.num_qubits, simulator=False
)
# Or get a specific backend:
# backend = service.backend("ibm_brisbane")
# Use a pass manager to transpile the circuit and observable for the specific backend being used:
pm = generate_preset_pass_manager(backend=backend, optimization_level=1)
isa_ansatz = pm.run(ansatz)
isa_hamiltonian = hamiltonian.apply_layout(layout=isa_ansatz.layout)
# Set estimator options
estimator_options = EstimatorOptions(resilience_level=1, default_shots=10_000)
# Open a Runtime session:
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
cost = cost_func_vqe(x0, isa_ansatz, isa_hamiltonian, estimator)
# Close session after done
session.close()
print(cost)
1.1120776913677988
Wrócimy do tego przykładu w sekcji Zastosowania, aby zbadać, jak wykorzystać optymalizator do iterowania po przestrzeni przeszukiwania. Ogólnie rzecz biorąc, obejmuje to:
- Wykorzystanie optymalizatora do znalezienia optymalnych parametrów
- Powiązanie optymalnych parametrów z ansatzem w celu znalezienia wartości własnych
- Przetłumaczenie wartości własnych na naszą definicję problemu
Strategia pomiaru: szybkość kontra dokładność
Jak wspomniano, używamy zaszumionego komputera kwantowego jako czarnej skrzynki wyroczni, gdzie szum może sprawić, że uzyskiwane wartości będą niedeterministyczne, prowadząc do losowych fluktuacji, które z kolei zaszkodzą — lub nawet całkowicie uniemożliwią — zbieżność pewnych optymalizatorów do proponowanego rozwiązania. Jest to ogólny problem, który musimy rozwiązywać, stopniowo badając użyteczność kwantową i zmierzając w kierunku przewagi kwantowej:
Możemy wykorzystać opcje tłumienia błędów i łagodzenia błędów prymitywów Qiskit Runtime, aby poradzić sobie z szumem i zmaksymalizować użyteczność dzisiejszych komputerów kwantowych.
Tłumienie błędów
Tłumienie błędów odnosi się do technik stosowanych do optymalizacji i transformacji obwodu podczas kompilacji w celu zminimalizowania błędów. Jest to podstawowa technika obsługi błędów, która zwykle skutkuje pewnym klasycznym narzutem przetwarzania wstępnego do całkowitego czasu wykonania. Narzut ten obejmuje transpilację obwodów do uruchomienia na sprzęcie kwantowym poprzez:
- Wyrażenie obwodu za pomocą natywnych bramek dostępnych w systemie kwantowym
- Odwzorowanie wirtualnych kubitów na fizyczne kubity
- Dodawanie operacji SWAP na podstawie wymagań łączności
- Optymalizację bramek 1Q i 2Q
- Dodawanie dynamicznego rozprzęgania do bezczynnych kubitów, aby zapobiec efektom dekoherencji.
Prymitywy umożliwiają korzystanie z technik tłumienia błędów poprzez ustawienie opcji optimization_level i wybór zaawansowanych opcji transpilacji. W późniejszym kursie zagłębimy się w różne metody konstrukcji obwodów w celu poprawy wyników, ale w większości przypadków zalecamy ustawienie optimization_level=3.
Zwizualizujemy wartość zwiększenia optymalizacji w procesie transpilacji, patrząc na przykładowy obwód z prostym idealnym zachowaniem.
from qiskit.circuit import Parameter, QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
theta = Parameter("theta")
qc = QuantumCircuit(2)
qc.x(1)
qc.h(0)
qc.cp(theta, 0, 1)
qc.h(0)
observables = SparsePauliOp.from_list([("ZZ", 1)])
qc.draw("mpl")
Powyższy obwód może dawać sinusoidalne wartości oczekiwane danej obserwabli, pod warunkiem że wstawimy fazy obejmujące odpowiedni przedział, taki jak .
## Setup phases
import numpy as np
phases = np.linspace(0, 2 * np.pi, 50)
# phases need to be expressed as a list of lists in order to work
individual_phases = [[phase] for phase in phases]
Możemy użyć symulatora, aby pokazać użyteczność zoptymalizowanej transpilacji. Poniżej powrócimy do korzystania z prawdziwego sprzętu, aby zademonstrować użyteczność łagodzenia błędów. Użyjemy QiskitRuntimeService, aby uzyskać prawdziwy backend (w tym przypadku ibm_brisbane), oraz AerSimulator, aby symulować ten backend, w tym jego zachowanie związane z szumem.
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_aer import AerSimulator
# get a real backend from the runtime service
service = QiskitRuntimeService()
backend = service.backend("ibm_brisbane")
# generate a simulator that mimics the real quantum system with the latest calibration results
backend_sim = AerSimulator.from_backend(backend)
Możemy teraz użyć menedżera przebiegów, aby transpilować obwód do "architektury zestawu instrukcji" lub ISA backendu. Jest to nowe wymaganie w Qiskit Runtime: wszystkie obwody przesyłane do backendu muszą być zgodne z ograniczeniami celu backendu, co oznacza, że muszą być napisane w kategoriach ISA backendu — czyli zestawu instrukcji, które urządzenie może zrozumieć i wykonać. Te ograniczenia celu są zdefiniowane przez takie czynniki, jak natywne bramki bazowe urządzenia, jego łączność kubitów oraz — gdy ma to znaczenie — specyfikacje impulsów i innych czasów instrukcji.
Zauważ, że w obecnym przypadku zrobimy to dwukrotnie: raz z optimization_level = 0, a raz ustawione na 3. Za każdym razem użyjemy prymitywu Estimator do oszacowania wartości oczekiwanych obserwabli przy różnych wartościach fazy.
# Import estimator and specify that we are using the simulated backend:
from qiskit_ibm_runtime import EstimatorV2 as Estimator
estimator = Estimator(mode=backend_sim)
circuit = qc
# Use a pass manager to transpile the circuit and observable for the backend being simulated.
# Start with no optimization:
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
pm = generate_preset_pass_manager(backend=backend_sim, optimization_level=0)
isa_circuit = pm.run(circuit)
isa_observables = observables.apply_layout(layout=isa_circuit.layout)
noisy_exp_values = []
pub = (isa_circuit, isa_observables, [individual_phases])
cost = estimator.run([pub]).result()[0].data.evs
noisy_exp_values = cost[0]
# Repeat above steps, but now with optimization = 3:
exp_values_with_opt_es = []
pm = generate_preset_pass_manager(backend=backend_sim, optimization_level=3)
isa_circuit = pm.run(circuit)
isa_observables = observables.apply_layout(layout=isa_circuit.layout)
pub = (isa_circuit, isa_observables, [individual_phases])
cost = estimator.run([pub]).result()[0].data.evs
exp_values_with_opt_es = cost[0]
Na koniec możemy wykreślić wyniki i widzimy, że precyzja obliczeń była dość dobra nawet bez optymalizacji, ale zdecydowanie poprawiła się po zwiększeniu optymalizacji do poziomu 3. Zauważ, że w głębszych, bardziej skomplikowanych obwodach różnica między poziomami optymalizacji 0 i 3 będzie prawdopodobnie bardziej znacząca. Jest to bardzo prosty obwód użyty jako zabawkowy model.
import matplotlib.pyplot as plt
plt.plot(phases, noisy_exp_values, "o", label="opt=0")
plt.plot(phases, exp_values_with_opt_es, "o", label="opt=3")
plt.plot(phases, 2 * np.sin(phases / 2) ** 2 - 1, label="ideal")
plt.ylabel("Expectation")
plt.legend()
plt.show()
Łagodzenie błędów
Łagodzenie błędów odnosi się do technik, które pozwalają użytkownikom redukować błędy obwodu poprzez modelowanie szumu urządzenia w czasie wykonania. Zazwyczaj skutkuje to kwantowym narzutem przetwarzania wstępnego związanym z trenowaniem modelu oraz klasycznym narzutem przetwarzania końcowego w celu złagodzenia błędów w surowych wynikach za pomocą wygenerowanego modelu.
Opcja resilience_level prymitywu Qiskit Runtime określa poziom odporności budowanej przeciwko błędom. Wyższe poziomy generują dokładniejsze wyniki kosztem dłuższych czasów przetwarzania z powodu narzutu próbkowania kwantowego. Poziomy odporności mogą być używane do konfiguracji kompromisu między kosztem a dokładnością podczas stosowania łagodzenia błędów w zapytaniach prymitywów.
Wdrażając jakąkolwiek technikę łagodzenia błędów, oczekujemy, że obciążenie w naszych wynikach zostanie zmniejszone w stosunku do poprzedniego, niezłagodzonego obciążenia. W niektórych przypadkach obciążenie może nawet zniknąć. Wiąże się to jednak z kosztem. Gdy zmniejszamy obciążenie w naszych szacowanych wielkościach, zmienność statystyczna wzrasta (czyli wariancja), co możemy uwzględnić, dalej zwiększając liczbę pomiarów na obwód w naszym procesie próbkowania. Wprowadzi to narzut poza tym potrzebnym do zmniejszenia obciążenia, więc nie jest to robione domyślnie. Możemy łatwo włączyć to zachowanie, dostosowując liczbę pomiarów na obwód w options.executions.shots, jak pokazano w przykładzie poniżej.
W tym kursie zbadamy te modele łagodzenia błędów na wysokim poziomie, aby zilustrować łagodzenie błędów, które prymitywy Qiskit Runtime mogą wykonać bez konieczności znajomości pełnych szczegółów implementacji.
Twirled readout error extinction (T-REx)
Twirled readout error extinction (T-REx) wykorzystuje technikę znaną jako twirling Pauliego w celu zmniejszenia szumu wprowadzanego podczas procesu pomiaru kwantowego. Technika ta nie zakłada konkretnej postaci szumu, co czyni ją bardzo ogólną i skuteczną.
Ogólny przepływ pracy:
- Pozyskaj dane dla stanu zerowego z losowymi odwróceniami bitów (Pauli X przed pomiarem)
- Pozyskaj dane dla pożądanego (zaszumionego) stanu z losowymi odwróceniami bitów (Pauli X przed pomiarem)
- Oblicz specjalną funkcję dla każdego zestawu danych i podziel.
Możemy ustawić to za pomocą options.resilience_level = 1, co pokazano w przykładzie poniżej.
Ekstrapolacja do zerowego szumu
Ekstrapolacja do zerowego szumu (ZNE, zero noise extrapolation) działa poprzez wstępne wzmocnienie szumu w obwodzie przygotowującym żądany stan kwantowy, uzyskanie pomiarów dla kilku różnych poziomów szumu, a następnie wykorzystanie tych pomiarów do wnioskowania o wyniku pozbawionym szumu.
Ogólny przebieg pracy:
- Wzmocnij szum obwodu dla kilku współczynników szumu
- Uruchom każdy obwód ze wzmocnionym szumem
- Ekstrapoluj z powrotem do granicy zerowego szumu
Możemy to ustawić za pomocą options.resilience_level = 2. Możemy dodatkowo zoptymalizować to, eksplorując różne noise_factors, noise_amplifiers i extrapolators, ale wykracza to poza zakres tego kursu. Zachęcamy do eksperymentowania z tymi opcjami opisanymi tutaj.
Każda metoda wiąże się z własnym narzutem: kompromisem między liczbą wymaganych obliczeń kwantowych (czas) a dokładnością naszych wyników:
Korzystanie z opcji mitigacji i tłumienia w Qiskit Runtime
Oto jak obliczyć wartość oczekiwaną przy jednoczesnym użyciu mitigacji i tłumienia błędów w Qiskit Runtime. Możemy wykorzystać dokładnie ten sam obwód i obserwablę co poprzednio, ale tym razem utrzymując poziom optymalizacji na poziomie 2, a teraz dostrajając odporność lub stosowane techniki mitigacji błędów. Ten proces mitigacji błędów odbywa się wielokrotnie w całej pętli optymalizacji.
Tę część wykonujemy na rzeczywistym sprzęcie, ponieważ mitigacja błędów nie jest dostępna w symulatorach.
# Estimated usage: 8 minutes, benchmarked on an Eagle processor, 5-23-24
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import (
Session,
EstimatorOptions,
EstimatorV2 as Estimator,
)
# We select the least busy backend
# Select the least busy backend
# backend = service.least_busy(
# operational=True, min_num_qubits=ansatz.num_qubits, simulator=False
# )
# Or use a specific backend
backend = service.backend("ibm_brisbane")
# Initialize some variables to save the results from different runs:
exp_values_with_em0_es = []
exp_values_with_em1_es = []
exp_values_with_em2_es = []
# Use a pass manager to optimize the circuit and observables for the backend chosen:
pm = generate_preset_pass_manager(backend=backend, optimization_level=2)
isa_circuit = pm.run(circuit)
isa_observables = observables.apply_layout(layout=isa_circuit.layout)
# Open a session and run with no error mitigation:
estimator_options = EstimatorOptions(resilience_level=0, default_shots=10_000)
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
pub = (isa_circuit, isa_observables, [individual_phases])
cost = estimator.run([pub]).result()[0].data.evs
session.close()
exp_values_with_em0_es = cost[0]
# Open a session and run with resilience = 1:
estimator_options = EstimatorOptions(resilience_level=1, default_shots=10_000)
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
pub = (isa_circuit, isa_observables, [individual_phases])
cost = estimator.run([pub]).result()[0].data.evs
session.close()
exp_values_with_em1_es = cost[0]
# Open a session and run with resilience = 2:
estimator_options = EstimatorOptions(resilience_level=2, default_shots=10_000)
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
pub = (isa_circuit, isa_observables, [individual_phases])
cost = estimator.run([pub]).result()[0].data.evs
session.close()
exp_values_with_em2_es = cost[0]
Tak jak poprzednio, możemy wykreślić otrzymane wartości oczekiwane jako funkcję kąta fazowego dla trzech zastosowanych poziomów mitigacji błędów. Z dużym trudem można dostrzec, że mitigacja błędów nieznacznie poprawia wyniki. Ponownie, efekt ten jest znacznie bardziej wyraźny w głębszych, bardziej skomplikowanych obwodach.
import matplotlib.pyplot as plt
plt.plot(phases, exp_values_with_em0_es, "o", label="unmitigated")
plt.plot(phases, exp_values_with_em1_es, "o", label="resil = 1")
plt.plot(phases, exp_values_with_em2_es, "o", label="resil = 2")
plt.plot(phases, 2 * np.sin(phases / 2) ** 2 - 1, label="ideal")
plt.ylabel("Expectation")
plt.legend()
plt.show()
Podsumowanie
Dzięki tej lekcji nauczyłeś się, jak stworzyć funkcję kosztu:
- Tworzenie funkcji kosztu
- Jak wykorzystać prymitywy Qiskit Runtime do mitigacji i tłumienia szumu
- Jak zdefiniować strategię pomiaru w celu optymalizacji kompromisu między szybkością a dokładnością
Oto nasze wysokopoziomowe obciążenie wariacyjne:
Nasza funkcja kosztu jest uruchamiana podczas każdej iteracji pętli optymalizacyjnej. Następna lekcja będzie poświęcona temu, jak klasyczny optymalizator wykorzystuje naszą ewaluację funkcji kosztu do wybierania nowych parametrów.
import qiskit
import qiskit_ibm_runtime
print(qiskit.version.get_version_info())
print(qiskit_ibm_runtime.version.get_version_info())
1.1.0
0.23.0