Účelové funkce
Během této lekce se naučíme, jak vyhodnotit účelovou funkci:
- Nejprve se seznámíme s primitivy Qiskit Runtime
- Definujeme účelovou funkci . Jde o funkci specifickou pro daný problém, která definuje cíl problému, jenž má optimalizátor minimalizovat (nebo maximalizovat)
- Definování strategie měření pomocí primitiv Qiskit Runtime pro optimalizaci rychlosti vůči přesnosti
Primitiva
Všechny fyzikální systémy, ať už klasické nebo kvantové, mohou existovat v různých stavech. Například auto na silnici může mít určitou hmotnost, polohu, rychlost nebo zrychlení, které charakterizují jeho stav. Podobně i kvantové systémy mohou mít různé konfigurace nebo stavy, ale od klasických systémů se liší tím, jak zacházíme s měřeními a s vývojem stavů. To vede k jedinečným vlastnostem, jako je superpozice a provázanost, které jsou výhradně záležitostí kvantové mechaniky. Stejně jako můžeme popsat stav auta pomocí fyzikálních veličin, jako je rychlost nebo zrychlení, můžeme popsat i stav kvantového systému pomocí pozorovatelných veličin, což jsou matematické objekty.
V kvantové mechanice jsou stavy reprezentovány normalizovanými komplexními sloupcovými vektory neboli kety () a pozorovatelné veličiny jsou hermitovské lineární operátory (), které na tyto kety působí. Vlastní vektor () pozorovatelné veličiny se nazývá vlastní stav. Měřením pozorovatelné veličiny na některém z jejích vlastních stavů () získáme jako výstup odpovídající vlastní hodnotu ().
Pokud se ptáš, jak změřit kvantový systém a co vlastně můžeš měřit, Qiskit ti nabízí dvě primitiva, která mohou pomoci:
Sampler: Pro daný kvantový stav toto primitivum získává pravděpodobnost každého možného stavu výpočetní báze.Estimator: Pro danou kvantovou pozorovatelnou veličinu a stav toto primitivum počítá střední hodnotu .
Primitivum Sampler
Primitivum Sampler počítá pravděpodobnost získání každého možného stavu z výpočetní báze pro daný kvantový circuit, který připravuje stav . Počítá
kde je počet qubitů a celočíselná reprezentace libovolného možného výstupního binárního řetězce (tedy celá čísla v základu ).
Qiskit Runtime Sampler spouští circuit na kvantovém zařízení opakovaně, při každém běhu provádí měření a z získaných bitových řetězců rekonstruuje rozdělení pravděpodobností. Čím více běhů (neboli shotů) provede, tím přesnější budou výsledky, to ale vyžaduje více času a kvantových prostředků.
Protože však počet možných výstupů roste exponenciálně s počtem qubitů (tedy ), bude muset i počet shotů růst exponenciálně, aby zachytil husté rozdělení pravděpodobnosti. Proto je Sampler efektivní pouze pro řídká rozdělení pravděpodobnosti; kde cílový stav musí být vyjádřitelný jako lineární kombinace stavů výpočetní báze, přičemž počet členů roste nejvýše polynomiálně s počtem qubitů:
Sampler lze také nakonfigurovat tak, aby získával pravděpodobnosti z podčásti circuitu, reprezentující podmnožinu všech možných stavů.
Primitivum Estimator
Primitivum Estimator počítá střední hodnotu pozorovatelné veličiny pro kvantový stav ; kde pravděpodobnosti pozorovatelné veličiny lze vyjádřit jako , přičemž jsou vlastní stavy pozorovatelné veličiny . Střední hodnota je pak definována jako průměr všech možných výsledků (tedy vlastních hodnot pozorovatelné veličiny) měření stavu , vážený odpovídajícími pravděpodobnostmi:
Výpočet střední hodnoty pozorovatelné veličiny však není vždy možný, protože často neznáme její vlastní bázi. Qiskit Runtime Estimator využívá složitý algebraický postup k odhadu střední hodnoty na reálném kvantovém zařízení tak, že pozorovatelnou veličinu rozloží na kombinaci jiných pozorovatelných, jejichž vlastní bázi známe.
Zjednodušeně řečeno, Estimator rozkládá libovolnou pozorovatelnou veličinu, kterou neumí změřit, na jednodušší, měřitelné pozorovatelné zvané Pauliho operátory.
Libovolný operátor lze vyjádřit jako kombinaci Pauliho operátorů.
tak, že
kde je počet qubitů, pro (tedy celá čísla v základu ) a .
Po provedení tohoto rozkladu Estimator odvodí nový circuit pro každou pozorovatelnou veličinu (z původního circuitu), aby efektivně diagonalizoval Pauliho pozorovatelnou ve výpočetní bázi a změřil ji. Pauliho pozorovatelné můžeme snadno změřit, protože známe předem, což obecně pro jiné pozorovatelné neplatí.
Pro každé Estimator spustí odpovídající circuit na kvantovém zařízení několikrát, změří výstupní stav ve výpočetní bázi a vypočítá pravděpodobnost získání každého možného výstupu . Poté najde vlastní hodnotu operátoru odpovídající každému výstupu , vynásobí ji a všechny výsledky sečte, čímž získá střední hodnotu pozorovatelné veličiny pro daný stav .
Protože výpočet střední hodnoty Pauliho operátorů je nepraktický (tedy roste exponenciálně), Estimator může být efektivní pouze tehdy, když je velké množství nulových (tedy řídký Pauliho rozklad namísto hustého). Formálně říkáme, že aby tento výpočet byl efektivně řešitelný, musí počet nenulových členů růst nejvýše polynomiálně s počtem qubitů :
Čtenář si může povšimnout implicitního předpokladu, že pravděpodobnostní vzorkování musí být také efektivní, jak bylo vysvětleno pro Sampler, což znamená
Vedený příklad výpočtu středních hodnot
Předpokládejme jednoqubitový stav a pozorovatelnou
s následující teoretickou střední hodnotou
Protože neumíme tuto pozorovatelnou změřit přímo, nemůžeme její střední hodnotu spočítat přímo a musíme ji přepsat jako . Lze ukázat, že se vyhodnotí na stejný výsledek, vezmeme-li v úvahu, že a .
Podívejme se, jak spočítat a přímo. Protože a nekomutují (tj. nesdílejí stejnou vlastní bázi), nemohou být měřeny současně, a proto potřebujeme pomocné obvody:
# 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")
Nyní můžeme výpočet provést ručně pomocí Sampleru a zkontrolovat výsledky pomocí Estimatoru:
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
Matematická přísnost (volitelné)
Vyjádříme-li v bázi vlastních stavů , , dostaneme:
Protože neznáme vlastní hodnoty ani vlastní stavy cílové pozorovatelné , musíme nejprve uvažovat její diagonalizaci. Vzhledem k tomu, že je hermitovská, existuje unitární transformace taková, že kde je diagonální matice vlastních hodnot, tedy pro a .
Z toho plyne, že střední hodnotu lze přepsat jako:
Pokud je systém ve stavu , je pravděpodobnost naměření rovna , a výše uvedenou střední hodnotu lze tedy vyjádřit jako:
Je velmi důležité poznamenat, že pravděpodobnosti jsou brány ze stavu , nikoliv ze stavu . Proto je matice naprosto nezbytná. Možná tě napadá, jak matici a vlastní hodnoty získat. Kdybys vlastní hodnoty již měl/a k dispozici, nebylo by potřeba používat kvantový počítač, protože cílem variačních algoritmů je právě tyto vlastní hodnoty nalézt.
Naštěstí existuje způsob, jak tuto překážku obejít: každou matici lze zapsat jako lineární kombinaci tenzorových součinů Pauliho matic a jednotkových matic, přičemž všechny jsou hermitovské i unitární se známými a . To je právě to, co Runtime Estimator dělá interně – rozkládá libovolný objekt Operator na SparsePauliOp.
Zde jsou Operátory, které lze použít:
Přepišme tedy pomocí Pauliho matic a jednotkových matic:
kde pro (tj. v základu ) a :
kde a , takže:
Účelové funkce
Účelové funkce se obecně používají k popisu cíle problému a k tomu, jak dobře si zkušební stav vede vzhledem k tomuto cíli. Tuto definici lze aplikovat na různé příklady v chemii, strojovém učení, financích, optimalizaci a tak dále.
Uvažujme jednoduchý příklad hledání základního stavu systému. Naším cílem je minimalizovat střední hodnotu pozorovatelné veličiny představující energii (Hamiltonián ):
Pomocí Estimator můžeme vyhodnotit střední hodnotu a předat tuto hodnotu optimalizátoru k minimalizaci. Pokud je optimalizace úspěšná, vrátí sadu optimálních hodnot parametrů , ze kterých budeme moci sestavit navrhovaný stavový vektor a vypočítat pozorovanou střední hodnotu jako .
Všimni si, že účelovou funkci budeme moci minimalizovat pouze pro omezený soubor stavů, které zvažujeme. To nás vede ke dvěma různým možnostem:
- Náš ansatz nedefinuje řešení v celém prohledávaném prostoru: V takovém případě optimalizátor řešení nikdy nenajde a musíme experimentovat s jinými ansatzy, které by mohly lépe reprezentovat náš prohledávaný prostor.
- Náš optimalizátor není schopen toto platné řešení nalézt: Optimalizace může být definována globálně i lokálně. Co to znamená, prozkoumáme v pozdější části.
Celkově vzato budeme provádět klasickou optimalizační smyčku, přičemž se budeme spoléhat na vyhodnocení účelové funkce kvantovým počítačem. Z tohoto pohledu by bylo možné na optimalizaci nahlížet jako na čistě klasické úsilí, při němž pokaždé, když optimalizátor potřebuje vyhodnotit účelovou funkci, voláme jakýsi kvantový orákulum černé skříňky.
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")
Nejprve to provedeme pomocí simulátoru: StatevectorEstimator. To se obvykle doporučuje pro ladění, ale ihned po ladicím spuštění provedeme výpočet na skutečném kvantovém hardware. Problémy, které nás zajímají, jsou stále méně klasicky simulovatelné bez nejmodernějších superpočítačových zařízení.
estimator = StatevectorEstimator()
cost = cost_func_vqe(theta_list, ansatz, observable, estimator)
print(cost)
[-0.58744589]
Nyní přistoupíme ke spuštění na skutečném kvantovém počítači. Všimni si změn v syntaxi. Kroky zahrnující pass_manager budou podrobněji probrány v dalším příkladu. Jedním obzvláště důležitým krokem ve variačních algoritmech je použití Session Qiskit Runtime. Zahájení session ti umožňuje spouštět více iterací variačního algoritmu, aniž bys pokaždé čekal/a v nové frontě, když se aktualizují parametry. To je důležité, pokud jsou časy front dlouhé nebo je potřeba mnoho iterací. Runtime sessions mohou používat pouze partneři v síti IBM Quantum® Network. Pokud k sessions nemáš přístup, můžeš snížit počet iterací, které odešleš najednou, a uložit nejnovější parametry pro použití v budoucích spuštěních. Pokud odešleš příliš mnoho iterací nebo narazíš na příliš dlouhé časy front, může se objevit chybový kód 1217, který označuje dlouhé prodlevy mezi odesláním úloh.
# 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)
Všimni si, že hodnoty získané z obou výpočtů výše jsou velmi podobné. Techniky pro zlepšení výsledků budou dále probrány níže.
Příklad mapování na nefyzikální systémy
Problém maximálního řezu (Max-Cut) je kombinatorický optimalizační problém, který spočívá v rozdělení vrcholů grafu do dvou disjunktních množin tak, aby byl maximalizován počet hran mezi těmito dvěma množinami. Formálněji: pro neorientovaný graf , kde je množina vrcholů a je množina hran, se problém Max-Cut ptá, jak rozdělit vrcholy do dvou disjunktních podmnožin a tak, aby byl maximalizován počet hran, jejichž jeden koncový bod leží v a druhý v .
Max-Cut lze použít k řešení různých problémů, včetně: shlukování, návrhu sítí, fázových přechodů a dalších. Začneme vytvořením grafu problému:
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"
)
Tento problém lze vyjádřit jako binární optimalizační problém. Pro každý uzel , kde je počet uzlů grafu (v tomto případě ), budeme uvažovat binární proměnnou . Tato proměnná bude mít hodnotu , pokud uzel patří do skupiny, kterou označíme jako , a hodnotu , pokud patří do druhé skupiny, kterou označíme jako . Budeme také označovat jako (prvek matice sousednosti ) váhu hrany vedoucí z uzlu do uzlu . Protože je graf neorientovaný, platí . Náš problém pak lze formulovat jako maximalizaci následující účelové funkce:
Abychom mohli tento problém řešit na kvantovém počítači, vyjádříme účelovou funkci jako střední hodnotu pozorovatelné veličiny. Pozorovatelné veličiny, které Qiskit nativně podporuje, se skládají z Pauliho operátorů s vlastními hodnotami a místo a . Proto provedeme následující změnu proměnných:
Kde . Matici sousednosti lze pohodlně využít pro přístup k váhám všech hran. Ta bude použita k získání naší účelové funkce:
Z toho plyne:
Nová účelová funkce, kterou chceme maximalizovat, je tedy:
Kvantový počítač navíc přirozeně hledá minima (zpravidla nejnižší energii) namísto maxim, takže místo maximalizace budeme minimalizovat:
Nyní, když máme účelovou funkci k minimalizaci, jejíž proměnné mohou nabývat hodnot a , můžeme provést následující analogii s Pauliho operátorem :
Jinými slovy, proměnná bude ekvivalentní hradlu působícímu na qubit . Navíc:
Pozorovatelná veličina, kterou budeme uvažovat, je tedy:
ke které bude třeba následně přičíst konstantní člen:
Operátor je lineární kombinací členů s operátory Z na uzlech spojených hranou (připomeň, že 0. qubit je nejdále vpravo): . Jakmile je operátor sestaven, ansatz pro algoritmus QAOA lze snadno vytvořit pomocí obvodu QAOAAnsatz z knihovny Qiskit circuit library.
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
Protože Estimator z Runtime přímo přijímá hamiltonián a parametrizovaný ansatz a vrací potřebnou energii, je účelová funkce pro instanci QAOA poměrně jednoduchá:
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
K tomuto příkladu se vrátíme v části Aplikace, kde prozkoumáme, jak využít optimalizátor k procházení prohledávaného prostoru. Obecně to zahrnuje:
- Využití optimalizátoru k nalezení optimálních parametrů
- Navázání optimálních parametrů na ansatz za účelem nalezení vlastních hodnot
- Převod vlastních hodnot na definici našeho problému
Strategie měření: rychlost versus přesnost
Jak bylo zmíněno, používáme zašuměný kvantový počítač jako black-box orákulum, kde šum může způsobit, že získané hodnoty budou nedeterministické, což vede k náhodným výkyvům, jež zase mohou uškodit — nebo dokonce zcela zabránit — konvergenci určitých optimalizátorů k navrhovanému řešení. Jde o obecný problém, který musíme řešit, jak postupně zkoumáme kvantovou užitečnost a pokračujeme směrem ke kvantové výhodě:
Ke zvládnutí šumu a maximalizaci užitečnosti dnešních kvantových počítačů můžeme využít možnosti potlačení chyb a zmírnění chyb primitiv Qiskit Runtime.
Potlačení chyb
Potlačení chyb označuje techniky používané k optimalizaci a transformaci obvodu během kompilace s cílem minimalizovat chyby. Jde o základní techniku zpracování chyb, která obvykle přináší určitou režii klasického předzpracování do celkové doby běhu. Tato režie zahrnuje transpilaci obvodů pro spuštění na kvantovém hardwaru prostřednictvím:
- Vyjádření obvodu pomocí nativních hradel dostupných na kvantovém systému
- Mapování virtuálních qubitů na fyzické qubity
- Přidání SWAP operací na základě požadavků na konektivitu
- Optimalizace 1Q a 2Q hradel
- Přidání dynamického oddělování (dynamical decoupling) k nečinným qubitům, aby se předešlo efektům dekoherence.
Primitiva umožňují využívat techniky potlačení chyb nastavením volby optimization_level a výběrem pokročilých možností transpilace. V pozdějším kurzu se budeme podrobněji věnovat různým metodám konstrukce obvodů pro zlepšení výsledků, ale pro většinu případů doporučujeme nastavit optimization_level=3.
Hodnotu zvyšující se optimalizace v procesu transpilace vizualizujeme na příkladu obvodu s jednoduchou ideální chování.
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")
Výše uvedený obvod může poskytovat sinusoidální střední hodnoty dané pozorovatelné veličiny za předpokladu, že vložíme fáze pokrývající vhodný interval, například .
## 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]
Pomocí simulátoru můžeme ukázat užitečnost optimalizované transpilace. Níže se vrátíme k používání skutečného hardwaru, abychom demonstrovali užitečnost zmírnění chyb. Pomocí QiskitRuntimeService získáme skutečný backend (v tomto případě ibm_brisbane) a použijeme AerSimulator k simulaci tohoto backendu včetně jeho šumového chování.
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)
Nyní můžeme použít pass manager k transpilaci obvodu do „instrukční sady architektury" (ISA) backendu. Toto je nový požadavek v Qiskit Runtime: všechny obvody odeslané do backendu musí odpovídat omezením cíle backendu, to znamená, že musí být zapsány ve smyslu ISA backendu — tedy sady instrukcí, které zařízení dokáže pochopit a vykonat. Tato cílová omezení jsou definována faktory jako nativní základní hradla zařízení, konektivita jeho qubitů a — pokud je to relevantní — specifikace pulzů a dalších instrukčních časování.
Všimni si, že v tomto případě to provedeme dvakrát: jednou s optimization_level = 0 a jednou s hodnotou nastavenou na 3. Pokaždé použijeme primitiv Estimator k odhadnutí středních hodnot pozorovatelné veličiny při různých hodnotách fáze.
# 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]
Nakonec můžeme zobrazit výsledky a vidíme, že přesnost výpočtu byla poměrně dobrá i bez optimalizace, ale rozhodně se zlepšila zvýšením optimalizace na úroveň 3. Všimni si, že u hlubších a složitějších obvodů bude rozdíl mezi úrovněmi optimalizace 0 a 3 pravděpodobně výraznější. Toto je velmi jednoduchý obvod použitý jako hračkový 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()
Zmírnění chyb
Zmírnění chyb označuje techniky, které umožňují uživatelům snižovat chyby v obvodech tím, že modelují šum zařízení v době provádění. Typicky to vede k overhead při kvantovém předzpracování spojeném s trénováním modelu a k overhead při klasickém postprocesingu, který slouží ke zmírnění chyb v surových výsledcích pomocí vygenerovaného modelu.
Možnost resilience_level u Qiskit Runtime primitivů určuje míru odolnosti vůči chybám. Vyšší úrovně generují přesnější výsledky za cenu delší doby zpracování kvůli overhead při kvantovém vzorkování. Úrovně odolnosti lze použít ke konfiguraci kompromisu mezi náklady a přesností při aplikaci zmírnění chyb na dotaz na primitiva.
Při implementaci jakékoli techniky zmírnění chyb očekáváme, že bias v našich výsledcích bude snížen oproti předchozímu, nemitigovanému biasu. V některých případech může bias dokonce zmizet. To však přichází za cenu. Jak snižujeme bias v odhadovaných veličinách, statistická variabilita se zvýší (tedy rozptyl), což můžeme kompenzovat dalším zvýšením počtu shotů na obvod v našem procesu vzorkování. To způsobí overhead nad rámec toho, který je potřeba ke snížení biasu, takže se to ve výchozím nastavení neprovádí. Toto chování můžeme snadno zapnout úpravou počtu shotů na obvod v options.executions.shots, jak je ukázáno v níže uvedeném příkladu.
V tomto kurzu budeme zkoumat tyto modely zmírnění chyb na vysoké úrovni, abychom ilustrovali zmírnění chyb, které mohou Qiskit Runtime primitivy provádět, aniž by bylo nutné znát veškeré detaily implementace.
Twirled readout error extinction (T-REx)
Twirled readout error extinction (T-REx) využívá techniku zvanou Pauli twirling ke snížení šumu zavedeného při procesu kvantového měření. Tato technika nepředpokládá žádnou konkrétní formu šumu, což ji činí velmi obecnou a účinnou.
Celkový postup:
- Získej data pro nulový stav s náhodným překlápěním bitů (Pauli X před měřením)
- Získej data pro požadovaný (zašuměný) stav s náhodným překlápěním bitů (Pauli X před měřením)
- Vypočítej speciální funkci pro každou datovou sadu a vyděl.
Toto nastavení lze provést pomocí options.resilience_level = 1, jak je ukázáno v níže uvedeném příkladu.
Zero noise extrapolation
Zero noise extrapolation (ZNE) funguje tak, že nejprve zesiluje šum v obvodu, který připravuje požadovaný kvantový stav, získává měření pro několik různých úrovní šumu a pomocí těchto měření odvozuje výsledek bez šumu.
Celkový postup:
- Zesil šum obvodu pro několik šumových faktorů
- Spusť každý obvod se zesíleným šumem
- Extrapoluj zpět na limit nulového šumu
Toto nastavení lze provést pomocí options.resilience_level = 2. Můžeme to dále optimalizovat průzkumem různých noise_factors, noise_amplifiers a extrapolators, ale to je mimo rozsah tohoto kurzu. Doporučujeme ti experimentovat s těmito možnostmi, jak jsou popsány zde.
Každá metoda přináší vlastní overhead: kompromis mezi počtem potřebných kvantových výpočtů (čas) a přesností našich výsledků:
Použití možností zmírnění a potlačení chyb v Qiskit Runtime
Zde je ukázka, jak vypočítat střední hodnotu při použití zmírnění a potlačení chyb v Qiskit Runtime. Můžeme použít přesně stejný obvod a pozorovatelnou jako dříve, tentokrát však s pevně nastavenou úrovní optimalizace na úrovni 2 a s laděním odolnosti neboli použité techniky (technik) zmírnění chyb. Tento proces zmírnění chyb probíhá vícekrát v průběhu optimalizační smyčky.
Tuto část provádíme na reálném hardwaru, protože zmírnění chyb není k dispozici na simulátorech.
# 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]
Stejně jako dříve můžeme vykreslit výsledné střední hodnoty jako funkci fázového úhlu pro tři použité úrovně zmírnění chyb. S velkým úsilím lze vidět, že zmírnění chyb výsledky mírně zlepšuje. Tento efekt je opět mnohem výraznější u hlubších a složitějších obvodů.
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()
Shrnutí
V této lekci ses naučil(a), jak vytvořit účelovou funkci:
- Vytvoření účelové funkce
- Jak využít primitiva Qiskit Runtime ke zmírnění a potlačení šumu
- Jak definovat strategii měření pro optimalizaci rychlosti versus přesnosti
Zde je náš přehled variačního pracovního postupu:
Naše účelová funkce se spouští při každé iteraci optimalizační smyčky. Následující lekce prozkoumá, jak klasický optimalizátor využívá vyhodnocení naší účelové funkce k výběru nových parametrů.
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