دوال التكلفة
خلال هذا الدرس، سنتعلم كيفية تقييم دالة التكلفة:
- أولاً، سنتعرف على بدائيات Qiskit Runtime
- تعريف دالة التكلفة . وهي دالة خاصة بالمشكلة تحدد هدف المشكلة ليقوم المُحسِّن بتصغيره (أو تكبيره)
- تحديد استراتيجية القياس باستخدام بدائيات Qiskit Runtime لتحقيق التوازن بين السرعة والدقة
البدائيات
جميع الأنظمة الفيزيائية، سواء كانت كلاسيكية أو كمية، يمكن أن توجد في حالات مختلفة. على سبيل المثال، يمكن أن يكون للسيارة على الطريق كتلة معينة، وموضع، وسرعة، أو تسارع يميز حالتها. وبالمثل، يمكن أن تكون للأنظمة الكمية أيضاً تكوينات أو حالات مختلفة، لكنها تختلف عن الأنظمة الكلاسيكية في كيفية تعاملنا مع القياسات وتطور الحالة. وهذا يؤدي إلى خصائص فريدة مثل التراكب والتشابك التي هي حصرية لميكانيكا الكم. تماماً كما يمكننا وصف حالة السيارة باس تخدام خصائص فيزيائية مثل السرعة أو التسارع، يمكننا أيضاً وصف حالة نظام كمي باستخدام المرصودات، وهي كائنات رياضية.
في ميكانيكا الكم، تُمثَّل الحالات بمتجهات عمودية مركبة مُعيَّرة، أو كيتات ()، والمرصودات هي مؤثرات خطية هرمتية () تعمل على الكيتات. المتجه الذاتي () لمرصود يُعرف بـالحالة الذاتية. قياس مرصود لإحدى حالاته الذاتية () سيعطينا القيمة الذاتية المقابلة () كقراءة.
إذا كنت تتساءل عن كيفية قياس نظام كمي وما يمكنك قياسه، يقدم Qiskit بدائيتين يمكنهما المساعدة:
Sampler: بالنظر إلى حالة كمية ، تحصل هذه البدائية على احتمال كل حالة أساسية حسابية ممكنة.Estimator: بالنظر إلى مرصود كمي وحالة ، تحسب هذه البدائية القيمة المتوقعة لـ .
بدائية Sampler
تحسب بدائية Sampler احتمال الحصول على كل حالة ممكنة من الأساس الحسابي، بالنظر إلى دائرة كمية تحضّر الحالة . تحسب
حيث هو عدد الكيوبتات، و هو التمثيل الصحيح لأي سلسلة ثنائية ناتجة ممكنة (أي، أعداد صحيحة في الأساس ).
يقوم Sampler في Qiskit Runtime بتشغيل الدائرة عدة مرات على جهاز كمي، مع إجراء قياسات في كل تشغيل، وإعادة بناء التوزيع الاحتمالي من سلاسل البتات المسترجعة. كلما زاد عدد التشغيلات (أو اللقطات)، كانت النتائج أكثر دقة، لكن هذا يتطلب مزيداً من الوقت والموارد الكمية.
ومع ذلك، بما أن عدد المخرجات الممكنة ينمو أسياً مع عدد الكيوبتات (أي )، فإن عدد اللقطات سيحتاج إلى النمو أسياً أيضاً من أجل التقاط توزيع احتمالي كثيف. لذلك، Sampler فعّال فقط للتوزيعات الاحتمالية المتناثرة؛ حيث يجب أن تكون الحالة المستهدفة قابلة للتعبير عنها كتركيبة خطية لحالات الأساس الحسابي، مع نمو عدد الحدود على الأكثر بشكل متعدد الحدود مع عدد الكيوبتات:
يمكن أيضاً تكوين Sampler لاسترجاع الاحتمالات من جزء من الدائرة، يمثل مجموعة فرعية من إجمالي الحالات الممكنة.
بدائية Estimator
تحسب بدائية Estimator القيمة المتوقعة لمرصود لحالة كمية ؛ حيث يمكن التعبير عن احتمالات المرصود كـ ، حيث هي الحالات الذاتية للمرصود . تُعرَّف القيمة المتوقعة بأنها متوسط جميع النتائج الممكنة (أي القيم الذاتية للمرصود) لقياس الحالة ، مرجحة بالاحتمالات المقابلة:
ومع ذلك، حساب القيمة المتوقعة لمرصود ليس ممكناً دائماً، لأننا غالباً لا نعرف أساسه الذاتي. يستخدم Estimator في Qiskit Runtime عملية جبرية معقدة لتقدير القيمة المتوقعة على جهاز كمي حقيقي عن طريق تفكيك المرصود إلى تركيبة من مرصودات أخرى نعرف أساسها الذاتي.
بعبارات أبسط، يفكك Estimator أي مرصود لا يعرف كيفية قياسه إلى مرصودات أبسط قابلة للقياس تسمى مؤثرات باولي.
يمكن التعبير عن أي مؤثر كتركيبة من مؤثر باولي.
بحيث
حيث هو عدد الكيوبتات، لـ (أي أعداد صحيحة في الأساس )، و .
بعد إجراء هذا التفكيك، يشتق Estimator دائرة جديدة لكل مرصود (من الدائرة الأصلية)، لـتقطير مرصود باولي بشكل فعال في الأساس الحسابي وقياسه. يمكننا قياس مرصودات باولي بسهولة لأننا نعرف مسبقاً، وهو ما لا ينطبق عموماً على المرصودات الأخرى.
لكل ، يقوم Estimator بتشغيل الدائرة المقابلة على جهاز كمي عدة مرات، ويقيس حالة الخرج في الأساس الحسابي، ويحسب الاحتمال للحصول على كل خرج ممكن . ثم يبحث عن القيمة الذاتية لـ المقابلة لكل خرج ، ويضرب في ، ويجمع كل النتائج معاً للحصول على القيمة المتوقعة للمرصود للحالة المعطاة .
بما أن حساب القيمة المتوقعة لـ من مؤثرات باولي غير عملي (أي ينمو أسياً)، لا يمكن أن يكون Estimator فعالاً إلا عندما يكون عدد كبير من صفراً (أي تفكيك باولي متناثر بدلاً من كثيف). نقول رسمياً أنه لكي يكون هذا الحساب قابلاً للحل بكفاءة، يجب أن ينمو عدد الحدود غير الصفرية على الأكثر بشكل متعدد الحدود مع عدد الكيوبتات :
قد يلاحظ القارئ الافتراض الضمني بأن أخذ العينات الاحتمالي يحتاج أيضاً إلى أن يكون فعالاً كما شُرح لـ Sampler، مما يعني
مثال موجّه لحساب القيم المتوقعة
لنفترض حالة الكيوبت الواحد ، والمرصود
مع القيمة المتوقعة النظرية التالية
بما أننا لا نعرف كيفية قياس هذا المرصود، لا يمكننا حساب قيمته المتوقعة مباشرة، ونحتاج إلى إعادة التعبير عنه كـ . يمكن إثبات أن هذا يعطي نفس النتيجة بملاحظة أن ، و .
دعنا نرى كيفية حساب و مباشرة. بما أن و لا يتبادلان (أي لا يشتركان في نفس الأساس الذاتي)، لا يمكن قياسهما في وقت واحد، لذلك نحتاج إلى الدوائر المساعدة:
# 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")
يمكننا الآن إجراء الحساب يدوياً باستخدام Sampler والتحقق من النتائج على 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
الدقة الرياضية (اختياري)
بالتعبير عن بالنسبة لأساس الحالات الذاتية لـ ، ، يترتب على ذلك:
بما أننا لا نعرف القيم الذاتية أو الحالات الذاتية للمرصود المستهدف ، نحتاج أولاً إلى النظر في تقطيره. بالنظر إلى أن هرمتي، يوجد تحويل وحدوي بحيث حيث هي المصفوفة القطرية للقيم الذاتية، بحيث إذا ، و .
هذا يعني أن القيمة المتوقعة يمكن إعادة كتابتها كالتالي:
بالنظر إلى أنه إذا كان النظام في الحالة فإن احتمال قياس هو ، يمكن التعبير عن القيمة المتوقعة أعلاه كالتالي:
من المهم جداً ملاحظة أن الاحتمالات مأخوذة من الحالة بدلاً من . هذا هو السبب في أن المصفوفة ضرورية تماماً. قد تتساءل عن كيفية الحصول على المصفوفة والقيم الذاتية . إذا كنت تملك القيم الذاتية بالفعل، فلن تكون هناك حاجة لاستخدام حاسوب كمي لأن هدف الخوارزميات التباينية هو إيجاد هذه القيم الذاتية لـ .
لحسن الحظ، هناك طريقة للتغلب على ذلك: يمكن كتابة أي مصفوفة كتركيبة خطية من جداء موتري لـ من مصفوفات باولي ومصفوفات الوحدة، وجميعها هرمتية ووحدوية مع و معروفين. هذا ما يفعله Estimator في Runtime داخلياً عن طريق تفكيك أي كائن Operator إلى SparsePauliOp.
إليك المؤثرات التي يمكن استخدامها:
لذا دعنا نعيد كتابة بالنسبة لمؤثرات باولي ومصفوفات الوحدة:
حيث لـ (أي في الأساس )، و :
حيث و ، بحيث:
دوال التكلفة
بشكل عام، تُستخدم دوال التكلفة لوصف هدف المشكلة ومدى جودة أداء حالة التجربة بالنسبة لذلك الهدف. يمكن تطبيق هذا التعريف على أمثلة متنوعة في الكيمياء، والتعلم الآلي، والتمويل، والتحسين، وما إلى ذلك.
لنأخذ مثالاً بسيطاً لإيجاد الحالة الأرضية لنظام ما. هدفنا هو تصغير القيمة المتوقعة للمرصود الذي يمثل الطاقة (الهاملتوني ):
يمكننا استخدام Estimator لتقييم القيمة المتوقعة وتمرير هذه القيمة إلى مُحسِّن لتصغيرها. إذا نجح التحسين، سيعيد مجموعة من قيم المعلمات المثلى ، والتي سنتمكن من خلالها من بناء حالة الحل المقترحة وحساب القيمة المتوقعة المرصودة كـ .
لاحظ أننا سنكون قادرين فقط على تصغير دالة التكلفة لمجموعة محدودة من الحالات التي نأخذها بعين الاعتبار. هذا يقودنا إلى احتمالين منفصلين:
- الأنساتز الخاص بنا لا يحدد حالة الحل عبر فضاء البحث: إذا كان هذا هو الحال، فلن يجد المُحسِّن الحل أبداً، ونحتاج إلى تجربة أنساتزات أخرى قد تكون قادرة على تمثيل فضاء البحث بدقة أكبر.
- المُحسِّن غير قادر على إيجاد هذا الحل الصالح: يمكن تعريف التحسين عالمياً ومحلياً. سنستكشف ما يعنيه هذا في القسم اللاحق.
بشكل عام، سنقوم بتنفيذ حلقة تحسين كلاسيكية لكن مع الاعتماد على تقييم دالة التكلفة بواسطة حاسوب كمي. من هذا المنظور، يمكن للمرء أن يفكر في التحسين كمسعى كلاسيكي بحت حيث نستدعي أوراكل كمي صندوق أسود في كل مرة يحتاج فيها المُحسِّن إلى تقييم دالة التكلفة.
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")
سنقوم أولاً بتنفيذ هذا باستخدام محاكي: StatevectorEstimator. عادة ما يُنصح بهذا لأغراض تصحيح الأخطاء، لكننا سنتبع تشغيل التصحيح فوراً بحساب على عتاد كمي حقيقي. بشكل متزايد، المشكلات ذات الاهتمام لم تعد قابلة للمحاكاة كلاسيكياً بدون مرافق حوسبة فائقة متطورة.
estimator = StatevectorEstimator()
cost = cost_func_vqe(theta_list, ansatz, observable, estimator)
print(cost)
[-0.58744589]
سننتقل الآن إلى التشغيل على حاسوب كمي حقيقي. لاحظ تغييرات الصياغة. سيتم مناقشة الخطوات المتعلقة بـ pass_manager بمزيد من التفصيل في المثال التالي. خطوة ذات أهمية خاصة في الخوارزميات التباينية هي استخدام جلسة Qiskit Runtime. بدء جلسة يسمح لك بتشغيل تكرارات متعددة لخوارزمية تباينية دون الانتظار في طابور جديد في كل مرة يتم فيها تحديث المعلمات. هذا مهم إذا كانت أوقات الطابور طويلة و/أو كانت هناك حاجة إلى العديد من التكرارات. فقط الشركاء في شبكة IBM Quantum® يمكنهم استخدام جلسات Runtime. إذا لم يكن لديك وصول إلى الجلسات، يمكنك تقليل عدد التكرارات التي ترسلها في وقت معين، وحفظ أحدث المعلمات لاستخدامها في عمليات التشغيل المستقبلية. إذا أرسلت عدداً كبيراً جداً من التكرارات أو واجهت أوقات طابور طويلة جداً، فقد تواجه رمز الخطأ 1217، الذي يشير إلى تأخيرات طويلة بين إرسالات المهام.
# 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)
لاحظ أن القيم المحصلة من الحسابين أعلاه متشابهة جداً. سيتم مناقشة تقنيات تحسين النتائج بمزيد من التفصيل أدناه.
مثال على التعيين لأنظمة غير فيزيائية
مشكلة القطع الأقصى (Max-Cut) هي مشكلة تحسين توافقية تتضمن تقسيم رؤوس رسم بياني إلى مجموعتين منفصلتين بحيث يكون عدد الحواف بين المجموعتين أقصى ما يمكن. بشكل أكثر رسمية، بالنظر إلى رسم بياني غير موجه ، حيث هي مجموعة الرؤوس و هي مجموعة الحواف، تطلب مشكلة القطع الأقصى تقسيم الرؤوس إلى مجموعتين فرعيتين منفصلتين، و ، بحيث يكون عدد الحواف التي لها نقطة نهاية واحدة في والأخرى في أقصى ما يمكن.
يمكننا تطبيق القطع الأقصى لحل مشاكل متنوعة بما في ذلك: التجميع، وتصميم الشبكات، والتحولات الطورية، وما إلى ذلك. سنبدأ بإنشاء رسم بياني للمشكلة:
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"
)
يمكن التعبير عن هذه المشكلة كمشكلة تحسين ثنائية. لكل عقدة ، حيث هو عدد عقد الرسم البياني (في هذه الحالة )، سنأخذ بعين الاعتبار المتغير الثنائي . سيكون لهذا المتغير القيمة إذا كانت العقدة في إحدى المجموعات التي سنسميها و إذا كانت في المجموعة الأخرى التي سنسميها . سنرمز أيضاً بـ (العنصر من مصفوفة التجاور ) لوزن الحافة التي تربط العقدة بالعقدة . لأن الرسم البياني غير موجه، . يمكننا حينئذ صياغة مشكلتنا كتكبير دالة التكلفة التالية:
لحل هذه المشكلة بحاسوب كمي، سنعبّر عن دالة التكلفة كقيمة متوقعة لمرصود. ومع ذلك، فإن المرصودات التي يقبلها Qiskit أصلاً تتكون من مؤثرات باولي، التي لها قيم ذاتية و بدلاً من و . لهذا السبب سنجري تغيير المتغير التالي:
حيث . يمكننا استخدام مصفوفة التجاور للوصول بسهولة إلى أوزان جميع الحواف. سيُستخدم هذا للحصول على دالة التكلفة:
هذا يعني أن:
إذن دالة التكلفة الجديدة التي نريد تكبيرها هي:
علاوة على ذلك، الميل الطبيعي للحاسوب الكمي هو إيجاد الحدود الدنيا (عادة أقل طاقة) بدلاً من القصوى، لذا بدلاً من تكبير سنقوم بتصغير:
الآن بما أن لدينا دالة تكلفة لتصغيرها ومتغيراتها يمكن أن تأخذ القيم و ، يمكننا إجراء التناظر التالي مع مؤثر باولي :
بعبارة أخرى، المتغير سيكون مكافئاً لبوابة تعمل على الكيوبت . علاوة على ذلك:
إذن المرصود الذي سنأخذه بعين الاعتبار هو:
والذي سنضيف إليه الحد المستقل بعد ذلك:
المؤثر هو تركيبة خطية لحدود تحتوي على مؤثرات Z على العقد المتصلة بحافة (تذكر أن الكيوبت رقم 0 هو الأبعد يميناً): . بمجرد بناء المؤثر، يمكن بناء الأنساتز لخوارزمية QAOA بسهولة باستخدام دائرة QAOAAnsatz من مكتبة دوائر 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
مع أخذ Runtime Estimator مباشرة للهاملتوني والأنساتز المعلمي، وإرجاع الطاقة اللازمة، فإن دالة التكلفة لمثال QAOA بسيطة للغاية:
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
سنعود إلى هذا المثال في التطبيقات لاستكشاف كيفية الاستفادة من مُحسِّن للتكرار عبر فضاء البحث. بشكل عام، يتضمن هذا:
- الاستفادة من مُحسِّن لإيجاد المعلمات المثلى
- ربط المعلمات المثلى بالأنساتز لإيجاد القيم الذاتية
- ترجمة القيم الذاتية إلى تعريف مشكلتنا
استراتيجية القياس: السرعة مقابل الدقة
كما ذُكر، نحن نستخدم حاسوباً كمياً مشوشاً كـأوراكل صندوق أسود، حيث يمكن أن تجعل الضوضاء القيم المسترجعة غير حتمية، مما يؤدي إلى تقلبات عشوائية والتي بدورها ستضر — أو حتى تمنع تماماً — تقارب بعض المُحسِّنات نحو حل مقترح. هذه مشكلة عامة يجب أن نعالجها بينما نستكشف تدريجياً الفائدة الكمية ونتقدم نحو التفوق الكمي:
يمكننا استخدام خيارات قمع الأخطاء وتخفيف الأخطاء في بدائيات Qiskit Runtime لمعالجة الضوضاء وتعظيم فائدة الحواسيب الكمية الحالية.
قمع الأخطاء
يشير قمع الأخطاء إلى التقنيات المستخدمة لتحسين وتحويل الدائرة أثناء التجميع من أجل تقليل الأخطاء. هذه تقنية أساسية لمعالجة الأخطاء تؤدي عادة إلى بعض التكلفة الإضافية الكلاسيكية في المعالجة المسبقة لوقت التشغيل الكلي. تتضمن التكلفة الإضافية تحويل الدوائر للتشغيل على العتاد الكمي عن طريق:
- التعبير عن الدائرة باستخدام البوابات الأصلية المتاحة على النظام الكمي
- تعيين الكيوبتات الافتراضية على الكيوبتات الفيزيائية
- إضافة عمليات SWAP بناءً على متطلبات الاتصال
- تحسين بوابات 1Q و 2Q
- إضافة الفصل الديناميكي للكيوبتات الخاملة لمنع تأثيرات فقدان الترابط.
تسمح البدائيات باستخدام تقنيات قمع الأخطاء من خلال ضبط خيار optimization_level واختيار خيارات التحويل المتقدمة. في دورة لاحقة، سنتعمق في طرق بناء الدوائر المختلفة لتحسين النتائج، لكن في معظم الحالات، نوصي بضبط optimization_level=3.
سنقوم بتوضيح قيمة زيادة التحسين في عملية التحويل من خلال النظر إلى دائرة مثال ذات سلوك مثالي بسيط.
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")
يمكن للدائرة أعلاه أن تنتج قيم متوقعة جيبية للمرصود المعطى، شريطة أن ندخل أطواراً تمتد على فترة مناسبة، مثل .
## 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]
يمكننا استخدام محاكي لإظهار فائدة التحويل المحسّن. سنعود أدناه لاستخدام العتاد الحقيقي لتوضيح فائدة تخفيف الأخطاء. سنستخدم QiskitRuntimeService للحصول على واجهة خلفية حقيقية (في هذه الحالة، ibm_brisbane)، ونستخدم AerSimulator لمحاكاة تلك الواجهة الخلفية، بما في ذلك سلوك الضوضاء.
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)
يمكننا الآن استخدام مدير التمريرات لتحويل الدائرة إلى "بنية مجموعة التعليمات" أو ISA للواجهة الخلفية. هذا متطلب جديد في Qiskit Runtime: يجب أن تتوافق جميع الدوائر المقدمة إلى واجهة خلفية مع قيود هدف الواجهة الخلفية، مما يعني أنها يجب أن تكون مكتوبة بمصطلحات ISA للواجهة الخلفية — أي مجموعة التعليمات التي يمكن للجهاز فهمها وتنفيذها. يتم تحديد قيود الهدف هذه بع وامل مثل بوابات الأساس الأصلية للجهاز، واتصال الكيوبتات، و — عند الاقتضاء — مواصفات النبض والتوقيت الأخرى للتعليمات.
لاحظ أنه في الحالة الحالية، سنقوم بهذا مرتين: مرة مع optimization_level = 0، ومرة مع ضبطه على 3. في كل مرة سنستخدم بدائية Estimator لتقدير القيم المتوقعة للمرصود عند قيم مختلفة للطور.
# 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]
أخيراً، يمكننا رسم النتائج، ونرى أن دقة الحساب كانت جيدة إلى حد ما حتى بدون تحسين، لكنها تحسنت بالتأكيد بزيادة التحسين إلى المستوى 3. لاحظ أنه في الدوائر الأعمق والأكثر تعقيداً، من المرجح أن يكون الفرق بين مستويات التحسين 0 و 3 أكثر أهمية. هذه دائرة بسيطة جداً تُستخدم كنموذج تجريبي.
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()
تخفيف الأخطاء
يشير تخفيف الأخطاء إلى التقنيات التي تسمح للمستخدمين بتقليل أخطاء الدائرة عن طريق نمذجة ضوضاء الجهاز في وقت التنفيذ. عادة ما يؤدي هذا إلى تكلفة إضافية في المعالجة المسبقة الكمية المتعلقة بتدريب النموذج وتكلفة إضافية في المعالجة اللاحقة الكلاسيكية لتخفيف الأخطاء في النتائج الخام باستخدام النموذج المُنشأ.
يحدد خيار resilience_level لبدائيات Qiskit Runtime مقدار المرونة المبنية ضد الأخطاء. تولّد المستويات الأعلى نتائج أكثر دقة على حساب أوقات معالجة أطول بسبب تكلفة أخذ العينات الكمية الإضافية. يمكن استخدام مستويات المرونة لتكوين المقايضة بين التكلفة والدقة عند تطبيق تخفيف الأخطاء على استعلام البدائية.
عند تنفيذ أي تقنية لتخفيف الأخطاء، نتوقع أن يتم تقليل التحيز في نتائجنا بالنسبة للتحيز السابق غير المخفف. في بعض الحالات، قد يختفي التحيز حتى. ومع ذلك، يأتي هذا بتكلفة. عندما نقلل التحيز في كمياتنا المقدرة، سيزداد التباين الإحصائي (أي التباين)، والذي يمكننا تعويضه بزيادة عدد اللقطات لكل دائرة في عملية أخذ العينات. سيقدم هذا تكلفة إضافية تتجاوز تلك اللازمة لتقليل التحيز، لذا لا يتم بشكل افتراضي. يمكننا بسهولة الاشتراك في هذا السلوك من خلال ضبط عدد اللقطات لكل دائرة في options.executions.shots، كما هو موضح في المثال أدناه.
في هذه الدورة، سنستكشف نماذج تخفيف الأخطاء هذه على مستوى عالٍ لتوضيح تخفيف الأخطاء الذي يمكن أن تنفذه بدائيات Qiskit Runtime دون الحاجة إلى تفاصيل التنفيذ الكاملة.
إخماد خطأ القراءة الملتوي (T-REx)
يستخدم إخماد خطأ القراءة الملتوي (T-REx) تقنية تعرف بتلوية باولي لتقليل الضوضاء المدخلة أثناء عملية القياس الكمي. تفترض هذه التقنية عدم وجود شكل محدد للضوضاء، مما يجعلها عامة جداً وفعالة.
سير العمل العام:
- الحصول على بيانات للحالة الصفرية م ع قلبات بتات عشوائية (باولي X قبل القياس)
- الحصول على بيانات للحالة المطلوبة (المشوشة) مع قلبات بتات عشوائية (باولي X قبل القياس)
- حساب الدالة الخاصة لكل مجموعة بيانات، والقسمة.
يمكننا ضبط هذا مع options.resilience_level = 1، كما هو موضح في المثال أدناه.
استقراء الضوضاء الصفرية
يعمل استقراء الضوضاء الصفرية (ZNE) عن طريق تضخيم الضوضاء أولاً في الدائرة التي تحضّر الحالة الكمية المطلوبة، والحصول على قياسات لعدة مستويات مختلفة من الضوضاء، واستخدام تلك القياسات لاستنتاج النتيجة الخالية من الضوضاء.
سير العمل العام:
- تضخيم ضوضاء الدائرة لعدة عوامل ضوضاء
- تشغيل كل دائرة مضخمة الضوضاء
- الاستقراء إلى حد الضوضاء الصفرية
يمكننا ضبط هذا مع options.resilience_level = 2. يمكننا تحسين هذا أكثر من خلال استكشاف مجموعة متنوعة من noise_factors و noise_amplifiers و extrapolators، لكن هذا خارج نطاق هذه الدورة. نشجعك على تجربة هذه الخيارات كما هو موصوف هنا.
كل طريقة تأتي مع تكلفتها الإضافية الخاصة: مقايضة بين عدد الحسابات الكمية اللازمة (الوقت) ودقة نتائجنا: