Fungsi biaya
Dalam pelajaran ini, kita akan belajar cara mengevaluasi sebuah fungsi biaya:
- Pertama, kita akan belajar tentang primitif Qiskit Runtime
- Mendefinisikan sebuah fungsi biaya . Ini adalah fungsi yang spesifik untuk masalah tertentu yang mendefinisikan tujuan masalah bagi optimizer untuk diminimalkan (atau dimaksimalkan)
- Mendefinisikan strategi pengukuran dengan primitif Qiskit Runtime untuk mengoptimalkan kecepatan versus akurasi
Primitif
Semua sistem fisik, baik klasik maupun kuantum, bisa ada dalam keadaan yang berbeda-beda. Misalnya, sebuah mobil di jalan bisa memiliki massa, posisi, kecepatan, atau percepatan tertentu yang mencirikan keadaannya. Demikian pula, sistem kuantum juga bisa memiliki konfigurasi atau keadaan yang berbeda, namun berbeda dari sistem klasik dalam cara kita menangani pengukuran dan evolusi keadaan. Hal ini mengarah pada sifat-sifat unik seperti superposisi dan keterikatan yang eksklusif untuk mekanika kuantum. Sama seperti kita bisa menggambarkan keadaan mobil menggunakan sifat fisik seperti kecepatan atau percepatan, kita juga bisa menggambarkan keadaan sistem kuantum menggunakan observabel, yang merupakan objek matematis.
Dalam mekanika kuantum, keadaan direpresentasikan oleh vektor kolom kompleks yang dinormalisasi, atau ket (), dan observabel adalah operator linear Hermitian () yang bekerja pada ket. Vektor eigen () dari sebuah observabel dikenal sebagai eigenstate. Mengukur observabel untuk salah satu eigenstate-nya () akan memberikan nilai eigen yang sesuai () sebagai hasil baca.
Jika kamu bertanya-tanya bagaimana cara mengukur sistem kuantum dan apa yang bisa kamu ukur, Qiskit menawarkan dua primitif yang bisa membantu:
Sampler: Diberikan keadaan kuantum , primitif ini mendapatkan probabilitas dari setiap kemungkinan keadaan basis komputasional.Estimator: Diberikan observabel kuantum dan keadaan , primitif ini menghitung nilai ekspektasi dari .
Primitif Sampler
Primitif Sampler menghitung probabilitas mendapatkan setiap kemungkinan keadaan dari basis komputasional, diberikan sebuah sirkuit kuantum yang mempersiapkan keadaan . Ia menghitung
Di mana adalah jumlah qubit, dan representasi integer dari semua kemungkinan string biner output (yaitu, bilangan bulat basis ).
Qiskit Runtime Sampler menjalankan Circuit beberapa kali pada perangkat kuantum, melakukan pengukuran pada setiap jalannya, dan merekonstruksi distribusi probabilitas dari bitstring yang didapatkan. Semakin banyak jalannya (atau shot), semakin akurat hasilnya, namun ini membutuhkan lebih banyak waktu dan sumber daya kuantum.
Namun, karena jumlah kemungkinan output tumbuh secara eksponensial seiring dengan jumlah qubit (yaitu, ), jumlah shot juga perlu tumbuh secara eksponensial agar bisa menangkap distribusi probabilitas yang padat. Oleh karena itu, Sampler hanya efisien untuk distribusi probabilitas yang jarang; di mana keadaan target harus bisa diekspresikan sebagai kombinasi linear dari keadaan basis komputasional, dengan jumlah suku yang tumbuh paling banyak secara polinomial seiring dengan jumlah qubit:
Sampler juga bisa dikonfigurasi untuk mengambil probabilitas dari sebagian Circuit, yang merepresentasikan subset dari total kemungkinan keadaan.
Primitif Estimator
Primitif Estimator menghitung nilai ekspektasi dari observabel untuk keadaan kuantum ; di mana probabilitas observabel bisa diekspresikan sebagai , dengan adalah eigenstate dari observabel . Nilai ekspektasi kemudian didefinisikan sebagai rata-rata dari semua kemungkinan hasil (yaitu, nilai eigen dari observabel) dari sebuah pengukuran keadaan , berbobot oleh probabilitas yang sesuai:
Namun, menghitung nilai ekspektasi dari sebuah observabel tidak selalu bisa dilakukan, karena kita sering tidak mengetahui eigenbasis-nya. Qiskit Runtime Estimator menggunakan proses aljabar yang kompleks untuk memperkirakan nilai ekspektasi pada perangkat kuantum nyata dengan menguraikan observabel menjadi kombinasi dari observabel lain yang eigenbasis-nya sudah kita ketahui.
Dalam istilah yang lebih sederhana, Estimator menguraikan setiap observabel yang tidak diketahui cara mengukurnya menjadi observabel yang lebih sederhana dan bisa diukur yang disebut operator Pauli.
Setiap operator bisa diekspresikan sebagai kombinasi dari operator Pauli.
sehingga
di mana adalah jumlah qubit, untuk (yaitu, bilangan bulat basis ), dan .
Setelah melakukan dekomposisi ini, Estimator menghasilkan Circuit baru untuk setiap observabel (dari Circuit asli), untuk secara efektif mendiagonalisasi observabel Pauli dalam basis komputasional dan mengukurnya. Kita bisa dengan mudah mengukur observabel Pauli karena kita mengetahui sebelumnya, yang tidak demikian halnya untuk observabel lain pada umumnya.
Untuk setiap , Estimator menjalankan Circuit yang sesuai pada perangkat kuantum beberapa kali, mengukur keadaan output dalam basis komputasional, dan menghitung probabilitas untuk mendapatkan setiap kemungkinan output . Kemudian ia mencari nilai eigen dari yang sesuai dengan setiap output , mengalikannya dengan , dan menjumlahkan semua hasilnya untuk mendapatkan nilai ekspektasi dari observabel untuk keadaan yang diberikan.
Karena menghitung nilai ekspektasi dari Pauli tidak praktis (yaitu, tumbuh secara eksponensial), Estimator hanya bisa efisien ketika sejumlah besar bernilai nol (yaitu, dekomposisi Pauli yang jarang bukan yang padat). Secara formal kita katakan bahwa, agar komputasi ini bisa diselesaikan secara efisien, jumlah suku yang tidak nol harus tumbuh paling banyak secara polinomial seiring dengan jumlah qubit :
Pembaca mungkin memperhatikan asumsi implisit bahwa pengambilan sampel probabilitas juga perlu efisien seperti yang dijelaskan untuk Sampler, yang berarti
Contoh terpandu untuk menghitung nilai ekspektasi
Mari kita asumsikan keadaan qubit tunggal , dan observabel
dengan nilai ekspektasi teoritis berikut
Karena kita tidak tahu cara mengukur observabel ini, kita tidak bisa menghitung nilai ekspektasinya secara langsung, dan kita perlu mengekspresikannya kembali sebagai . Yang bisa ditunjukkan menghasilkan hasil yang sama dengan memperhatikan bahwa , dan .
Mari kita lihat cara menghitung dan secara langsung. Karena dan tidak komutatif (yaitu, mereka tidak berbagi eigenbasis yang sama), mereka tidak bisa diukur secara bersamaan, sehingga kita memerlukan Circuit bantu:
# 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")
Sekarang kita bisa melakukan komputasi secara manual menggunakan Sampler dan memeriksa hasilnya di 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
Ketelitian matematis (opsional)
Mengekspresikan terhadap basis eigenstate dari , , maka:
Karena kita tidak mengetahui nilai eigen atau eigenstate dari observabel target , pertama-tama kita perlu mempertimbangkan diagonalisasinya. Mengingat bahwa bersifat Hermitian, terdapat transformasi uniter sedemikian sehingga di mana adalah matriks nilai eigen diagonal, sehingga jika , dan .
Ini berarti nilai ekspektasi bisa ditulis ulang sebagai:
Mengingat bahwa jika sebuah sistem berada dalam keadaan probabilitas mengukur adalah , nilai ekspektasi di atas bisa diekspresikan sebagai:
Sangat penting untuk diperhatikan bahwa probabilitas diambil dari keadaan bukan dari . Inilah mengapa matriks sangat diperlukan. Kamu mungkin bertanya-tanya bagaimana cara mendapatkan matriks dan nilai eigen . Jika kamu sudah memiliki nilai eigen, maka tidak perlu menggunakan komputer kuantum karena tujuan algoritma variasional adalah menemukan nilai eigen dari ini.
Untungnya, ada cara untuk mengatasinya: setiap matriks bisa ditulis sebagai kombinasi linear dari hasil kali tensor dari matriks Pauli dan identitas, yang semuanya bersifat hermitian dan uniter dengan dan yang sudah diketahui. Inilah yang dilakukan Estimator Runtime secara internal dengan menguraikan setiap objek Operator menjadi SparsePauliOp.
Berikut adalah Operator yang bisa digunakan:
Jadi mari kita tulis ulang terhadap Pauli dan identitas:
di mana untuk (yaitu, basis ), dan :
di mana dan , sehingga:
Fungsi biaya
Secara umum, fungsi biaya digunakan untuk menggambarkan tujuan dari suatu masalah dan seberapa baik suatu trial state memenuhi tujuan tersebut. Definisi ini bisa diterapkan ke berbagai contoh dalam kimia, machine learning, keuangan, optimasi, dan lain-lain.
Mari kita lihat contoh sederhana yaitu mencari ground state dari sebuah sistem. Tujuan kita adalah meminimalkan nilai ekspektasi dari observable yang merepresentasikan energi (Hamiltonian ):
Kita bisa menggunakan Estimator untuk mengevaluasi nilai ekspektasi dan meneruskan nilai ini ke optimizer untuk diminimalkan. Jika optimasi berhasil, akan dikembalikan sekumpulan nilai parameter optimal , dari mana kita bisa membangun solution state yang diusulkan dan menghitung nilai ekspektasi yang diamati sebagai .
Perhatikan bahwa kita hanya bisa meminimalkan fungsi biaya untuk sekumpulan state terbatas yang sedang kita pertimbangkan. Ini membawa kita pada dua kemungkinan terpisah:
- Ansatz kita tidak mendefinisikan solution state di seluruh search space: Jika ini kasusnya, optimizer kita tidak akan pernah menemukan solusinya, dan kita perlu bereksperimen dengan ansatz lain yang mungkin bisa merepresentasikan search space kita dengan lebih akurat.
- Optimizer kita tidak bisa menemukan solusi yang valid ini: Optimasi bisa didefinisikan secara global dan secara lokal. Kita akan mengeksplorasi apa artinya ini di bagian selanjutnya.
Pada akhirnya, kita akan menjalankan loop optimasi klasik tapi bergantung pada evaluasi fungsi biaya ke komputer kuantum. Dari perspektif ini, seseorang bisa menganggap optimasi sebagai usaha yang murni klasik di mana kita memanggil beberapa black-box quantum oracle setiap kali optimizer perlu mengevaluasi fungsi biaya.
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")
Pertama kita akan melakukan ini menggunakan simulator: StatevectorEstimator. Ini biasanya disarankan untuk debugging, tapi kita akan segera melanjutkan debugging run dengan perhitungan pada hardware kuantum nyata. Semakin lama, masalah-masalah yang menarik sudah tidak bisa lagi disimulasikan secara klasik tanpa fasilitas superkomputer mutakhir.
estimator = StatevectorEstimator()
cost = cost_func_vqe(theta_list, ansatz, observable, estimator)
print(cost)
[-0.58744589]
Sekarang kita akan melanjutkan dengan menjalankan pada komputer kuantum nyata. Perhatikan perubahan sintaksis. Langkah-langkah yang melibatkan pass_manager akan dibahas lebih lanjut di contoh berikutnya. Salah satu langkah yang sangat penting dalam algoritma variasional adalah penggunaan Qiskit Runtime Session. Memulai sebuah Session memungkinkan kamu menjalankan beberapa iterasi dari algoritma variasional tanpa harus menunggu di antrian baru setiap kali parameter diperbarui. Ini penting jika waktu antrian panjang dan/atau banyak iterasi diperlukan. Hanya mitra di IBM Quantum® Network yang bisa menggunakan Runtime Session. Jika kamu tidak punya akses ke Session, kamu bisa mengurangi jumlah iterasi yang kamu submit pada suatu waktu, dan menyimpan parameter terbaru untuk digunakan di run mendatang. Jika kamu menyubmit terlalu banyak iterasi atau menghadapi waktu antrian yang terlalu panjang, kamu mungkin akan mendapat kode error 1217, yang merujuk pada penundaan lama antara pengiriman job.
# 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)
Perhatikan bahwa nilai yang diperoleh dari dua perhitungan di atas sangat mirip. Teknik untuk meningkatkan hasil akan dibahas lebih lanjut di bawah.
Contoh pemetaan ke sistem non-fisik
Masalah maximum cut (Max-Cut) adalah masalah optimasi kombinatorial yang melibatkan pembagian simpul-simpul sebuah graf menjadi dua himpunan disjoint sedemikian sehingga jumlah sisi antara dua himpunan dimaksimalkan. Lebih formal, diberikan graf tak berarah , di mana adalah himpunan simpul dan adalah himpunan sisi, masalah Max-Cut meminta untuk mempartisi simpul menjadi dua himpunan bagian disjoint, dan , sedemikian sehingga jumlah sisi dengan satu titik ujung di dan titik ujung lainnya di dimaksimalkan.
Kita bisa menerapkan Max-Cut untuk memecahkan berbagai masalah termasuk: clustering, desain jaringan, transisi fase, dan lain-lain. Kita akan mulai dengan membuat graf masalah:
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"
)
Masalah ini bisa dinyatakan sebagai masalah optimasi biner. Untuk setiap simpul , di mana adalah jumlah simpul pada graf (dalam hal ini ), kita akan mempertimbangkan variabel biner . Variabel ini akan bernilai jika simpul berada di salah satu kelompok yang kita beri label dan jika berada di kelompok lain, yang kita beri label . Kita juga akan menandai (elemen dari matriks ketetanggaan ) sebagai bobot sisi yang menghubungkan simpul ke simpul . Karena grafnya tak berarah, . Maka kita bisa merumuskan masalah kita sebagai memaksimalkan fungsi biaya berikut:
Untuk memecahkan masalah ini dengan komputer kuantum, kita akan menyatakan fungsi biaya sebagai nilai ekspektasi dari sebuah observable. Namun, observable yang diterima Qiskit secara native terdiri dari operator Pauli, yang memiliki nilai eigen dan bukannya dan . Itulah mengapa kita akan melakukan perubahan variabel berikut:
Di mana . Kita bisa menggunakan matriks ketetanggaan untuk mengakses bobot semua sisi dengan nyaman. Ini akan digunakan untuk mendapatkan fungsi biaya kita:
Ini berarti:
Jadi fungsi biaya baru yang ingin kita maksimalkan adalah:
Selain itu, kecenderungan alami komputer kuantum adalah mencari minima (biasanya energi terendah) bukannya maksima, sehingga alih-alih memaksimalkan kita akan meminimalkan:
Sekarang kita punya fungsi biaya untuk diminimalkan yang variabelnya bisa bernilai dan , kita bisa membuat analogi berikut dengan Pauli :
Dengan kata lain, variabel akan setara dengan Gate yang bekerja pada qubit . Selain itu:
Maka observable yang akan kita pertimbangkan adalah:
yang harus kita tambahkan suku konstanta setelahnya:
Operatornya adalah kombinasi linear dari suku-suku dengan operator Z pada simpul yang terhubung oleh sisi (ingat bahwa qubit ke-0 paling kanan): . Setelah operator terbentuk, ansatz untuk algoritma QAOA bisa dengan mudah dibangun menggunakan Circuit QAOAAnsatz dari pustaka Circuit 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
Dengan Estimator Runtime yang langsung menerima Hamiltonian dan ansatz berparameter, serta mengembalikan energi yang diperlukan, fungsi biaya untuk instance QAOA cukup sederhana:
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
Kita akan kembali ke contoh ini di bagian Aplikasi untuk mengeksplorasi cara memanfaatkan optimizer untuk beriterasi melalui search space. Secara umum, ini meliputi:
- Memanfaatkan optimizer untuk menemukan parameter optimal
- Mengikat parameter optimal ke ansatz untuk menemukan nilai eigen
- Menerjemahkan nilai eigen ke definisi masalah kita
Strategi pengukuran: kecepatan versus akurasi
Seperti yang disebutkan, kita menggunakan komputer kuantum yang bising sebagai black-box oracle, di mana noise bisa membuat nilai yang diambil menjadi non-deterministik, menyebabkan fluktuasi acak yang pada gilirannya akan menghambat — atau bahkan sepenuhnya mencegah — konvergensi optimizer tertentu ke solusi yang diusulkan. Ini adalah masalah umum yang harus kita tangani saat kita secara bertahap mengeksplorasi quantum utility dan berkembang menuju quantum advantage:
Kita bisa menggunakan opsi error suppression dan error mitigation dari Qiskit Runtime Primitive untuk mengatasi noise dan memaksimalkan utilitas komputer kuantum saat ini.
Error Suppression
Error suppression mengacu pada teknik yang digunakan untuk mengoptimalkan dan mentransformasi Circuit selama kompilasi agar kesalahan diminimalkan. Ini adalah teknik penanganan error dasar yang biasanya menghasilkan beberapa overhead pra-pemrosesan klasik pada keseluruhan runtime. Overhead tersebut mencakup transpilasi Circuit untuk dijalankan pada hardware kuantum dengan cara:
- Mengekspresikan Circuit menggunakan gate native yang tersedia pada sistem kuantum
- Memetakan virtual qubit ke qubit fisik
- Menambahkan SWAP berdasarkan persyaratan konektivitas
- Mengoptimalkan gate 1Q dan 2Q
- Menambahkan dynamical decoupling pada qubit yang idle untuk mencegah efek dekoherensi.
Primitives memungkinkan penggunaan teknik error suppression dengan mengatur opsi optimization_level dan memilih opsi transpilasi lanjutan. Di kursus berikutnya, kita akan mendalami berbagai metode konstruksi Circuit untuk meningkatkan hasil, tapi untuk kebanyakan kasus, kami menyarankan mengatur optimization_level=3.
Kita akan memvisualisasikan nilai dari peningkatan optimasi dalam proses transpilasi dengan melihat contoh Circuit dengan perilaku ideal yang sederhana.
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")
Circuit di atas bisa menghasilkan nilai ekspektasi sinusoidal dari observable yang diberikan, asalkan kita memasukkan fase yang mencakup interval yang sesuai, seperti .
## 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]
Kita bisa menggunakan simulator untuk menunjukkan kegunaan transpilasi yang dioptimalkan. Kita akan kembali di bawah ini menggunakan hardware nyata untuk mendemonstrasikan kegunaan error mitigation. Kita akan menggunakan QiskitRuntimeService untuk mendapatkan Backend nyata (dalam hal ini, ibm_brisbane), dan menggunakan AerSimulator untuk menyimulasikan Backend tersebut, termasuk perilaku noise-nya.
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)
Kita sekarang bisa menggunakan pass manager untuk mentranspilasi Circuit ke dalam "instruction set architecture" atau ISA dari Backend. Ini adalah persyaratan baru di Qiskit Runtime: semua Circuit yang disubmit ke Backend harus sesuai dengan batasan target Backend, artinya harus ditulis dalam hal ISA Backend — yaitu, kumpulan instruksi yang bisa dipahami dan dieksekusi oleh perangkat. Batasan target ini ditentukan oleh faktor-faktor seperti gate basis native perangkat, konektivitas qubit-nya, dan — bila relevan — spesifikasi timing pulse dan instruksi lainnya.
Perhatikan bahwa dalam kasus ini, kita akan melakukannya dua kali: sekali dengan optimization_level = 0, dan sekali dengan nilainya diatur ke 3. Setiap kali kita akan menggunakan primitif Estimator untuk memperkirakan nilai ekspektasi dari observable pada berbagai nilai fase.
# 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]
Akhirnya, kita bisa memplot hasilnya, dan kita lihat bahwa presisi perhitungan cukup baik bahkan tanpa optimasi, tapi jelas meningkat dengan meningkatkan optimasi ke level 3. Perhatikan bahwa pada Circuit yang lebih dalam dan lebih kompleks, perbedaan antara level optimasi 0 dan 3 kemungkinan akan lebih signifikan. Ini adalah Circuit yang sangat sederhana yang digunakan sebagai model mainan.
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()
Error Mitigation
Error mitigation mengacu pada teknik yang memungkinkan pengguna untuk mengurangi error Circuit dengan memodelkan noise perangkat pada saat eksekusi. Biasanya, ini menghasilkan overhead pra-pemrosesan kuantum yang berkaitan dengan pelatihan model dan overhead pasca-pemrosesan klasik untuk memitigasi error dalam hasil mentah menggunakan model yang dihasilkan.
Opsi resilience_level dari primitif Qiskit Runtime menentukan jumlah ketahanan yang dibangun terhadap error. Level yang lebih tinggi menghasilkan hasil yang lebih akurat dengan biaya waktu pemrosesan yang lebih lama akibat overhead sampling kuantum. Level resilience bisa digunakan untuk mengonfigurasi trade-off antara biaya dan akurasi saat menerapkan error mitigation pada query primitif kamu.
Saat mengimplementasikan teknik error mitigation apa pun, kita harapkan bias dalam hasil kita berkurang dibandingkan dengan bias sebelumnya yang tidak dimitigasi. Dalam beberapa kasus, bias bahkan bisa hilang. Namun, ini ada biayanya. Saat kita mengurangi bias dalam kuantitas yang kita estimasi, variabilitas statistik akan meningkat (yaitu, varians), yang bisa kita kompensasi dengan lebih meningkatkan jumlah shot per Circuit dalam proses sampling kita. Ini akan memperkenalkan overhead di luar yang diperlukan untuk mengurangi bias, jadi tidak dilakukan secara default. Kita bisa dengan mudah memilih perilaku ini dengan menyesuaikan jumlah shot per Circuit di options.executions.shots, seperti yang ditunjukkan dalam contoh di bawah ini.
Untuk kursus ini, kita akan mengeksplorasi model error mitigation ini pada level tinggi untuk mengilustrasikan error mitigation yang bisa dilakukan oleh primitif Qiskit Runtime tanpa memerlukan detail implementasi penuh.
Twirled readout error extinction (T-REx)
Twirled readout error extinction (T-REx) menggunakan teknik yang dikenal sebagai Pauli twirling untuk mengurangi noise yang diperkenalkan selama proses pengukuran kuantum. Teknik ini tidak mengasumsikan bentuk noise tertentu, yang membuatnya sangat umum dan efektif.
Alur kerja keseluruhan:
- Ambil data untuk zero state dengan bit flip yang diacak (Pauli X sebelum pengukuran)
- Ambil data untuk state yang diinginkan (bising) dengan bit flip yang diacak (Pauli X sebelum pengukuran)
- Hitung fungsi khusus untuk setiap kumpulan data, dan bagi.
Kita bisa mengaturnya dengan options.resilience_level = 1, yang didemonstrasikan dalam contoh di bawah ini.
Zero noise extrapolation
Zero noise extrapolation (ZNE) bekerja dengan pertama-tama mengamplifikasi noise dalam Circuit yang menyiapkan quantum state yang diinginkan, mendapatkan pengukuran untuk beberapa level noise yang berbeda, dan menggunakan pengukuran tersebut untuk menyimpulkan hasil tanpa noise.
Alur kerja keseluruhan:
- Amplifikasi noise Circuit untuk beberapa faktor noise
- Jalankan setiap sirkuit yang diamplifikasi noise-nya
- Ekstrapolasi kembali ke batas zero noise
Kita bisa mengaturnya dengan options.resilience_level = 2. Kita bisa mengoptimalkannya lebih lanjut dengan mengeksplorasi berbagai noise_factors, noise_amplifiers, dan extrapolators, tapi ini di luar cakupan kursus ini. Kami mendorong kamu untuk bereksperimen dengan opsi-opsi ini seperti yang dijelaskan di sini.
Setiap metode memiliki overhead terkait masing-masing: trade-off antara jumlah komputasi kuantum yang diperlukan (waktu) dan akurasi hasil kita:
Menggunakan opsi mitigasi dan supresi Qiskit Runtime
Berikut cara menghitung nilai ekspektasi sambil menggunakan error mitigation dan suppression di Qiskit Runtime. Kita bisa menggunakan Circuit dan observable yang persis sama seperti sebelumnya, tapi kali ini menjaga optimization level tetap di level 2, dan sekarang menyetel resilience atau teknik error mitigation yang digunakan. Proses error mitigation ini terjadi beberapa kali sepanjang loop optimasi.
Kita melakukan bagian ini pada hardware nyata, karena error mitigation tidak tersedia pada simulator.
# 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]
Seperti sebelumnya, kita bisa memplot nilai ekspektasi yang dihasilkan sebagai fungsi sudut fase untuk tiga level error mitigation yang digunakan. Dengan susah payah, seseorang bisa melihat bahwa error mitigation sedikit meningkatkan hasilnya. Sekali lagi, efek ini jauh lebih terlihat pada Circuit yang lebih dalam dan lebih kompleks.
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()
Ringkasan
Dengan pelajaran ini, kamu belajar cara membuat fungsi biaya:
- Membuat fungsi biaya
- Cara memanfaatkan primitif Qiskit Runtime untuk memitigasi dan menekan noise
- Cara mendefinisikan strategi pengukuran untuk mengoptimalkan kecepatan vs akurasi
Berikut workload variasional kita secara high-level:
Fungsi biaya kita berjalan selama setiap iterasi dari loop optimasi. Pelajaran berikutnya akan mengeksplorasi bagaimana optimizer klasik menggunakan evaluasi fungsi biaya kita untuk memilih parameter baru.
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