انتقل إلى المحتوى الرئيسي

النوى الكمومية

مقدمة إلى النوى الكمومية

يُشير مصطلح "طريقة النواة الكمومية" إلى أي طريقة تستخدم الحواسيب الكمومية لتقدير نواة ما. في هذا السياق، تُشير كلمة "نواة" إلى مصفوفة النواة أو العناصر المفردة فيها. تذكّر أن تعيين الميزة Φ(x)\Phi(\vec{x}) هو تعيين من xRd\vec{x}\in \mathbb{R}^d إلى Φ(x)Rd\Phi(\vec{x})\in \mathbb{R}^{d'}، حيث يكون عادةً d>dd'>d، والهدف من هذا التعيين هو جعل فئات البيانات قابلة للفصل بواسطة مستوى فائق. تأخذ دالة النواة متجهات في الفضاء المُعيَّن بالميزات كوسيطات وتُعيد حاصل ضربها الداخلي، أي K:Rd×RdRK:\mathbb{R}^d\times\mathbb{R}^d\rightarrow \mathbb{R} مع K(x,y)=Φ(x)Φ(y)K(x,y) = \langle \Phi(x)|\Phi(y)\rangle. من الناحية الكلاسيكية، نهتم بتعيينات الميزات التي تكون دالة النواة فيها سهلة الحساب. وكثيرًا ما يعني ذلك إيجاد دالة نواة يمكن فيها كتابة حاصل الضرب الداخلي في الفضاء المُعيَّن بالميزات بدلالة متجهات البيانات الأصلية، دون الحاجة إلى بناء Φ(x)\Phi(x) وΦ(y)\Phi(y) صراحةً. في طريقة النوى الكمومية، يتم تعيين الميزة بواسطة دائرة كمومية، وتُقدَّر النواة باستخدام القياسات على تلك الدائرة واحتمالات القياس النسبية.

في هذا الدرس سنفحص أعماق دوائر الترميز المُبرمجة مسبقًا التي تستخدم تشابكًا كبيرًا ونقارنها بأعماق الدوائر التي نبرمجها بأنفسنا. لا يعني ذلك التفضيل لإحدى الطريقتين على الأخرى. قد تجد أن الدوائر المُبرمجة مسبقًا عميقة جدًا، وأن التشابك في الدائرة المُصمَّمة يدويًا غير كافٍ ليكون مفيدًا. مرة أخرى، تُعرض هذه الأمثلة فقط لتمكينك من الاستكشاف.

قبل الخوض في تفاصيل تقدير مصفوفة النواة، لنستعرض سير العمل باستخدام لغة أنماط Qiskit.

الخطوة 1: تعيين المدخلات الكلاسيكية إلى مسألة كمومية

  • المدخل: مجموعة بيانات التدريب
  • المخرج: دائرة مجردة لحساب عنصر مصفوفة النواة

بالنظر إلى مجموعة البيانات، نقطة البداية هي ترميز البيانات في دائرة كمومية. بمعنى آخر، نحتاج إلى تعيين بياناتنا إلى فضاء هيلبرت لحالات حاسوبنا الكمومي. نفعل ذلك ببناء دائرة تعتمد على البيانات. هناك طرق عديدة لتحقيق ذلك، والدرس السابق استعرض عددًا من الخيارات. يمكنك بناء دائرتك الخاصة لترميز بياناتك، أو استخدام خريطة ميزات جاهزة مثل zz_feature_map. في هذا الدرس، سنفعل الأمرين معًا.

لاحظ أنه لحساب عنصر واحد من مصفوفة النواة، سنحتاج إلى ترميز نقطتين مختلفتين حتى نتمكن من تقدير حاصل ضربهما الداخلي. بالطبع، سيتضمن سير عمل النواة الكمومية الكامل العديد من هذه الحواصل الداخلية بين متجهات البيانات المُعيَّنة، إلى جانب طرق التعلم الآلي الكلاسيكية. لكن الخطوة الأساسية المُكرَّرة هي تقدير عنصر واحد من مصفوفة النواة. لهذا نختار دائرة كمومية تعتمد على البيانات ونعيّن متجهَي البيانات إلى فضاء الميزات.

Classical_Review_background_kernel_circuit

لمهمة توليد مصفوفة النواة، نهتم بشكل خاص باحتمال قياس الحالة 0N|0\rangle^{\otimes N}، التي تكون فيها جميع الكيوبتات الـ NN في حالة 0|0\rangle. لفهم ذلك، لنأخذ في الاعتبار أن الدائرة المسؤولة عن ترميز وتعيين متجه بيانات واحد xi\vec{x}_i يمكن كتابتها كـ Φ(xi)\Phi(\vec{x}_i)، وتلك المسؤولة عن ترميز وتعيين xj\vec{x}_j هي Φ(xj)\Phi(\vec{x}_j)، ونرمز للحالات المُعيَّنة بـ

ψ(xi)=Φ(xi)0N|\psi(\vec{x}_i)\rangle = \Phi(\vec{x}_i)|0\rangle^{\otimes N} ψ(xj)=Φ(xj)0N.|\psi(\vec{x}_j)\rangle = \Phi(\vec{x}_j)|0\rangle^{\otimes N}.

هذه الحالات هي تعيين البيانات إلى أبعاد أعلى، لذا فإن عنصر النواة المطلوب هو حاصل الضرب الداخلي

ψ(xj)ψ(xi)=0NΦ(xj)Φ(xi)0N.\langle\psi(\vec{x}_j)|\psi(\vec{x}_i)\rangle = \langle 0 |^{\otimes N}\Phi^\dagger(\vec{x}_j)\Phi(\vec{x}_i)|0\rangle^{\otimes N}.

إذا طبّقنا على الحالة الابتدائية الافتراضية 0N|0\rangle^{\otimes N} كلتا الدائرتين Φ(xj)\Phi^\dagger(\vec{x}_j) وΦ(xi)\Phi(\vec{x}_i)، فإن احتمال قياس الحالة 0N|0\rangle^{\otimes N} هو

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

هذه بالضبط القيمة التي نريدها (حتى 2||^2). ستُعيد طبقة القياس في دائرتنا احتمالات القياس (أو ما يُسمى "شبه الاحتمالات" عند استخدام بعض طرق تخفيف الأخطاء). الاحتمال المعني هو احتمال الحالة الصفرية 0N|0\rangle^{\otimes N}.

الخطوة 2: تحسين المسألة للتنفيذ الكمومي

  • المدخل: دائرة مجردة، غير مُحسَّنة لواجهة خلفية معينة
  • المخرج: الدائرة المستهدفة والمشاهَد، مُحسَّنَين لوحدة معالجة الكم (QPU) المختارة

في هذه الخطوة، سنستخدم الدالة generate_preset_pass_manager من Qiskit لتحديد روتين تحسين لدائرتنا بالنسبة إلى الحاسوب الكمومي الحقيقي الذي نخطط لتشغيل التجربة عليه. نضبط optimization_level=3، مما يعني استخدام مدير المرور المُعدَّ مسبقًا الذي يوفر أعلى مستوى من التحسين. في هذا السياق، يُشير "التحسين" إلى تحسين تنفيذ الدائرة على الحاسوب الكمومي الحقيقي. يشمل ذلك اعتبارات مثل اختيار الكيوبتات الفيزيائية المقابلة للكيوبتات في الدائرة الكمومية المجردة بما يقلل عمق البوابات، أو اختيار الكيوبتات الفيزيائية ذات أدنى معدلات خطأ متاحة. لا يرتبط هذا ارتباطًا مباشرًا بتحسين مسألة التعلم الآلي (كما في المحسِّنات الكلاسيكية مثل COBYLA).

بحسب كيفية تنفيذك للخطوة 2، قد تضطر إلى تحسين الدائرة أكثر من مرة، إذ ينتج عن كل زوج من النقاط المشاركة في عنصر مصفوفة دائرةً مختلفة يجب قياسها.

الخطوة 3: التنفيذ باستخدام Qiskit Runtime Primitives

  • المدخل: الدائرة المستهدفة
  • المخرج: توزيع الاحتمال

استخدم الأداة الأولية Sampler من Qiskit Runtime لإعادة بناء توزيع احتمال الحالات الناتجة عن أخذ عينات من الدائرة. لاحظ أنك قد ترى هذا مشارًا إليه بـ "توزيع شبه الاحتمال"، وهو مصطلح ينطبق عندما يكون الضجيج مشكلة وعند إدخال خطوات إضافية، كما في تخفيف الأخطاء. في مثل هذه الحالات، قد لا يساوي مجموع جميع الاحتمالات 1 بالضبط؛ ومن هنا جاء مصطلح "شبه الاحتمال".

الخطوة 4: المعالجة اللاحقة، وإعادة النتيجة بصيغة كلاسيكية

  • المدخل: توزيع الاحتمال
  • المخرج: عنصر واحد من مصفوفة النواة، أو مصفوفة النواة كاملة عند التكرار

احسب احتمال قياس 0N|0\rangle^{\otimes N} على الدائرة الكمومية، وأدرج القيمة في مصفوفة النواة في الموضع المقابل لمتجهَي البيانات المستخدمَين. لملء مصفوفة النواة بالكامل، نحتاج إلى إجراء تجربة كمومية لكل عنصر. بمجرد الحصول على مصفوفة النواة، يمكننا استخدامها في كثير من خوارزميات التعلم الآلي الكلاسيكية التي تقبل pre-calculated kernels. مثلًا: qml_svc = SVC(kernel="precomputed"). يمكننا بعدها استخدام مسارات العمل الكلاسيكية لتطبيق نموذجنا على بيانات الاختبار والحصول على درجة دقة. بحسب مدى رضانا عن درجة الدقة، قد نحتاج إلى مراجعة جوانب من حساباتنا، مثل خريطة الميزات.

مخطط الدرس

في هذا الدرس سنُطبّق هذه الخطوات بعدة طرق للاستفادة المثلى من وقتك على الحواسيب الكمومية الحقيقية. سنُطبّق طريقة النواة الكمومية على:

  • عنصر واحد من مصفوفة النواة لبيانات ذات عدد قليل نسبيًا من الميزات، باستخدام واجهة خلفية حقيقية، حتى نتمكن من متابعة ما يحدث في كل خطوة بسهولة.
  • مجموعة بيانات كاملة ذات عدد قليل نسبيًا من الميزات، باستخدام واجهة خلفية محاكاة، حتى نرى كيف يتصل مسار العمل الكمومي بطرق التعلم الآلي الكلاسيكية.
  • عنصر واحد من مصفوفة النواة لبيانات ذات ميزات كثيرة، باستخدام حاسوب كمومي حقيقي. لن نُقدِّر مصفوفة نواة كاملة لمجموعة بيانات كبيرة، احترامًا للوقت على حواسيب IBM® الكمومية.
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy pandas qiskit qiskit-ibm-runtime scikit-learn
# If you have not already, install scikit learn
#!pip install scikit-learn

عنصر واحد من مصفوفة النواة

الخطوة 1: تعيين المدخلات الكلاسيكية إلى مسألة كمومية

لنبدأ بالنظر في مجموعة بيانات تحتوي على عدد قليل من الميزات، لنقل 10. يمكن أن تكون مجموعة البيانات بأي حجم تريد، إذ نحسب عناصر مصفوفة النواة واحدًا تلو الآخر. نحتاج على الأقل إلى نقطتين، لذا سنبدأ بذلك (في المثال التالي، سنستورد مجموعة بيانات كاملة). لنستورد بعض الحزم اللازمة:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# Two mock data points, including category labels, as in training
small_data = [
[-0.194, 0.114, -0.006, 0.301, -0.359, -0.088, -0.156, 0.342, -0.016, 0.143, 1],
[-0.1, 0.002, 0.244, 0.127, -0.064, -0.086, 0.072, 0.043, -0.053, 0.02, -1],
]

# Data points with labels removed, for inner product
train_data = [small_data[0][:-1], small_data[1][:-1]]

يمكننا تجربة استخدام z_feature_map.

# from qiskit.circuit.library import zz_feature_map
# fm = zz_feature_map(feature_dimension=np.shape(train_data)[1], entanglement='linear', reps=1)

from qiskit.circuit.library import z_feature_map

fm = z_feature_map(feature_dimension=np.shape(train_data)[1])

unitary1 = fm.assign_parameters(train_data[0])
unitary2 = fm.assign_parameters(train_data[1])

الوحدتان الأحاديتان أعلاه تقابلان بالضبط U1U_1 و U2U_2 اللتين وصفناهما في المقدمة. يمكننا دمجهما باستخدام unitary_overlap. وكما هو الحال دائماً، علينا أن نراقب عمق الدائرة.

from qiskit.circuit.library import unitary_overlap

overlap_circ = unitary_overlap(unitary1, unitary2)
overlap_circ.measure_all()

print("circuit depth = ", overlap_circ.decompose().depth())
overlap_circ.decompose().draw("mpl", scale=0.6, style="iqp")
circuit depth =  9

Output of the previous code cell

الخطوة 2: تحسين المسألة للتنفيذ الكمي

نبدأ باختيار الخلفية الأقل ازدحاماً، ثم نُحسِّن دائرتنا لتشغيلها على تلك الخلفية.

# Import needed packages
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService

# Get the least busy backend
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=fm.num_qubits
)
print(backend)
<IBMBackend('ibm_brisbane')>
# Apply level 3 optimization to our overlap circuit
pm = generate_preset_pass_manager(optimization_level=3, backend=backend)
overlap_ibm = pm.run(overlap_circ)

بالنسبة للدوائر المعقدة، ستزيد هذه الخطوة من عمق الدائرة بشكل ملحوظ أثناء تعيينها إلى البوابات الأصلية لأجهزة الحوسبة الكمية الحقيقية، وقد تحتاج المعلومات إلى الانتقال من كيوبت إلى آخر. في هذه الحالة البسيطة، لا يتأثر العمق تقريباً.

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

الخطوة 3: التنفيذ باستخدام Qiskit Runtime Primitives

الصيغة الخاصة بالتشغيل على محاكي مُعلَّقة في الكود أدناه. بالنسبة لهذه المجموعة من البيانات التي تحتوي على عدد صغير من الميزات، لا يزال التشغيل على محاكي خياراً متاحاً. أما للحسابات على نطاق الاستخدام الفعلي، فالمحاكاة لا تكون ممكنة في الغالب. ينبغي استخدام المحاكيات فقط لتصحيح أخطاء الكود المصغَّر.

# Run this for a simulator
# from qiskit.primitives import StatevectorSampler

# from qiskit_ibm_runtime import Options, Session, Sampler

# num_shots = 10000

# Evaluate the problem using state vector-based primitives from Qiskit
# sampler = StatevectorSampler()
# results = sampler.run([overlap_circ], shots=num_shots).result()
# .get_counts() returns counts associated with a state labeled by bit results such as |001101...01>.
# counts_bit = results[0].data.meas.get_counts()
# .get_int_counts returns the same counts, but labeled by integer equivalent of the above bit string.
# counts = results[0].data.meas.get_int_counts()
# Benchmarked on an Eagle processor, 7-11-24, took 4 sec.

# Import our runtime primitive
from qiskit_ibm_runtime import Session, SamplerV2 as Sampler

num_shots = 10000

# Use sampler and get the counts

sampler = Sampler(mode=backend)
results = sampler.run([overlap_ibm], shots=num_shots).result()
# .get_counts() returns counts associated with a state labeled by bit results such as |001101...01>.
counts_bit = results[0].data.meas.get_counts()
# .get_int_counts returns the same counts, but labeled by integer equivalent of the above bit string.
counts = results[0].data.meas.get_int_counts()

الخطوة 4: المعالجة اللاحقة وإرجاع النتيجة بالصيغة الكلاسيكية

كما وصفنا في المقدمة، أكثر قياس مفيد هنا هو احتمالية قياس الحالة الصفرية 00000|00000\rangle.

counts.get(0, 0.0) / num_shots
0.6525

هذه هي النتيجة التي أردناها: تقدير للجداء الداخلي (حتى المربع) للمتجهات المقابلة لنقطتي بيانات. إن أردنا الاطلاع على التوزيع الكامل لاحتمالات القياس (أو شبه الاحتمالات)، يمكننا فعل ذلك باستخدام الدالة plot_distribution كما هو موضح أدناه. ويتضح أنه مع زيادة عدد الكيوبتات، تصبح مثل هذه الصور سريعاً غير قابلة للمعالجة.

from qiskit.visualization import plot_distribution

plot_distribution(counts_bit)

Output of the previous code cell

كذلك، قد يرغب المرء في تعريف تصوير مثل الذي يظهر أدناه للنظر فقط في أعلى 10 قياسات من حيث الاحتمالية. وهذا قد يكون مهماً لاستكشاف الأخطاء أو لمحاولة الحصول على فهم أعمق للبيانات. لكن احتمالية قياس الحالة الصفرية هي عنصر مصفوفة النواة الخاصة بنا.

def visualize_counts(probs, num_qubits):
"""Visualize the outputs from the Qiskit Sampler primitive."""
zero_prob = probs.get(0, 0.0)
top_10 = dict(sorted(probs.items(), key=lambda item: item[1], reverse=True)[:10])
top_10.update({0: zero_prob})
by_key = dict(sorted(top_10.items(), key=lambda item: item[0]))
xvals, yvals = list(zip(*by_key.items()))
xvals = [bin(xval)[2:].zfill(num_qubits) for xval in xvals]
plt.bar(xvals, yvals)
plt.xticks(rotation=75)
plt.title("Results of sampling")
plt.xlabel("Measured bitstring")
plt.ylabel("Counts")
plt.show()

visualize_counts(counts, overlap_circ.num_qubits)

Output of the previous code cell

من خلال هذه المعلومات التي تخصّ حاصل الضرب الداخلي لنقطتَي بيانات واحدة فقط في فضاء الميزات عالي الأبعاد، كل ما يمكننا قوله هو أن التداخل بينهما كبير نسبيًا مقارنةً بالتداخل الأقصى (الذي سيكون 1.0). قد يشير هذا إلى أن نقطتَي البيانات هاتين متشابهتان بطريقة ما وستُصنَّفان في نفس الفئة. أو قد يشير إلى أن خريطة الميزات لدينا غير فعّالة في التعيين إلى فضاء يكون فيه تداخل البيانات المتشابهة كبيرًا وتداخل البيانات المختلفة صغيرًا. لمعرفة أيّ الحالتَين صحيح، يجب تطبيق خريطة الميزات على مجموعة البيانات بأكملها والتحقق من إمكانية معالجة مصفوفة النواة الناتجة لفصل الفئات بدقة عالية.

تجدر الإشارة إلى أننا استخدمنا z_feature_map التي أفضت إلى عمق ترانسبايل منخفض للبوابات ثنائية الكيوبت (عمق 1 بالفعل). إذا أصبحت دوائرك عميقة جدًا، فهذا سيؤدي حتمًا إلى قدر كبير من الضوضاء، مما سيجعل احتمال قياس الحالة الصفرية منخفضًا جدًا حتى لو كانت خريطة الميزات لديك متوافقة مع بياناتك. على سبيل المثال، أعطى تكرار العملية أعلاه باستخدام zz_feature_map و, entanglement='linear', reps=1 نتيجة dist.get(0,0.0) = 0.0015 باستخدام نفس نقاط البيانات. ويعود ذلك إلى الأعماق الأكبر بكثير للدائرة والبوابات ثنائية الكيوبت في zz_feature_map. يوضح الشكل أدناه توزيع الاحتمالية لتلك العملية الحسابية.

Bad results from a zz feature map.

من المفيد تجربة بعض نقاط البيانات من نفس الفئة لمعرفة مدى انخفاض العمق المطلوب للحصول على نتائج جيدة. فيما يلي نصائح تقريبية مع وجود استثناءات بالتأكيد. بشكل عام، عمق الترانسبايل للبوابات ثنائية الكيوبت بمقدار 10 أو أقل لا يمثل مشكلة. أما العمق بين 50 و60 فهو من أحدث التقنيات ويتطلب تخفيف متقدم للأخطاء إلى جانب أدوات أخرى. وفيما بين ذلك، قد تتفاوت النتائج بحسب تشابه البيانات، وقدرة خريطة الميزات التعبيرية، وعرض الدائرة، وعوامل أخرى. عادةً ما تتضمن خطوة المعالجة اللاحقة أيضًا عمليات تعلم آلة كلاسيكية. في القسم التالي سنوسّع هذه العملية لتشمل مجموعة بيانات كاملة، ونعرض سير عمل التعلم الآلي الكلاسيكي.

تحقق من فهمك

اقرأ الأسئلة أدناه، فكّر في إجاباتك، ثم انقر على المثلثات لعرض الحلول.

في دائرة كمية مكوّنة من 10 كيوبتات، كم عدد الحالات المختلفة التي يمكن قياسها بشكل عام؟

الجواب:

2102^{10} أو 1024.

لنفترض أن شخصًا مبتدئًا في الحوسبة الكمية يحاول استخدام دائرة كمية ذات عمق كيوبت ثنائي مرتفع جدًا دون استخدام تخفيف للأخطاء. ولنفترض كذلك أن هذا أفضى إلى معدل خطأ 10% على كل كيوبت. إذا كان عنصر مصفوفة النواة الحقيقي (الخالي من الأخطاء) المقابل لهذه الدائرة كبيرًا جدًا، أي 1.0، فما هو احتمال قياس جميع الـ 10 كيوبتات في الحالة التي يكون فيها كل كيوبت |0>؟

الجواب:

احتمال العثور على كل كيوبت بشكل صحيح في الحالة |0> هو 0.90. احتمال العثور على جميع الـ 10 كيوبتات في الحالة الصحيحة هو 0.90100.90^{10} أو ما يقارب 35%.

اشرح بكلماتك الخاصة لماذا من المهم جدًا مراقبة أعماق الدائرة. هذا صحيح بشكل عام، لكن اشرحه في سياق تقدير النواة الكمية.

الجواب:

في سير عمل QKE هذا، تعتمد تقديراتنا على قياسات الحالة الصفرية، أي الحالة التي يُوجد فيها كل كيوبت في الحالة 0|0\rangle. الدوائر العميقة جدًا ستُدخل معدلات خطأ عالية. وعندما يتراكم معدل الخطأ هذا عبر كيوبتات كثيرة، فسيؤدي ذلك إلى تقليل احتمال قياس الحالة الصفرية بشكل كبير.

مصفوفة النواة الكاملة

في هذا القسم، سنوسّع العملية السابقة لتشمل التصنيف الثنائي لمجموعة بيانات كاملة. سيُضيف هذا مكوّنَين مهمَّين: (1) يمكننا الآن تطبيق التعلم الآلي الكلاسيكي في المعالجة اللاحقة، و(2) يمكننا الحصول على درجات دقة للتدريب.

الخطوة 1: تعيين المدخلات الكلاسيكية إلى مسألة كمية

سنستورد الآن مجموعة بيانات موجودة للتصنيف. تتكون مجموعة البيانات هذه من 128 صفًا (نقطة بيانات) و14 ميزة لكل نقطة. يوجد عنصر خامس عشر يشير إلى الفئة الثنائية لكل نقطة (±1\pm 1). يتم استيراد مجموعة البيانات أدناه، أو يمكنك الوصول إليها وعرض هيكلها هنا.

سنستخدم أول 90 نقطة بيانات للتدريب، والـ 30 نقطة التالية للاختبار.

!wget https://raw.githubusercontent.com/qiskit-community/prototype-quantum-kernel-training/main/data/dataset_graph7.csv

df = pd.read_csv("dataset_graph7.csv", sep=",", header=None)

# Prepare training data

train_size = 90
X_train = df.values[0:train_size, :-1]
train_labels = df.values[0:train_size, -1]

# Prepare testing data
test_size = 30
X_test = df.values[train_size : train_size + test_size, :-1]
test_labels = df.values[train_size : train_size + test_size, -1]
--2024-07-11 23:05:22--  https://raw.githubusercontent.com/qiskit-community/prototype-quantum-kernel-training/main/data/dataset_graph7.csv
Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.110.133, 185.199.111.133, 185.199.109.133, ...
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.110.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 49405 (48K) [text/plain]
Saving to: ‘dataset_graph7.csv.15’

dataset_graph7.csv. 100%[===================>] 48.25K --.-KB/s in 0.02s

2024-07-11 23:05:23 (2.11 MB/s) - ‘dataset_graph7.csv.15’ saved [49405/49405]

سنجهّز مسبقًا تخزين مخرجات متعددة من خلال بناء مصفوفة نواة ومصفوفة اختبار بالأبعاد المناسبة.

# Empty kernel matrix
num_samples = np.shape(X_train)[0]
kernel_matrix = np.full((num_samples, num_samples), np.nan)
test_matrix = np.full((test_size, num_samples), np.nan)

الآن سنُنشئ خريطة ميزات للترميز وتعيين بياناتنا الكلاسيكية في دائرة كمية. يمكننا بناء خريطة الميزات الخاصة بنا أو استخدام واحدة جاهزة. لا تتردد في تعديل خريطة الميزات أدناه، أو التبديل إلى ZFeatureMap. لكن انتبه دائمًا لعمق الدائرة. تذكّر أنه في مثال الكيوبتات الست السابق، كان عمق الدائرة المُترجمة مرتفعًا بشكل يتعذّر التعامل معه عند استخدام zz_feature_map. كلما زاد حجم الدائرة وتعقيدها، قد يرتفع العمق بسرعة إلى حد تطغى فيه الضوضاء على نتائجنا. كلما عرفت شيئًا عن هيكل بياناتك قد يُرشدك إلى بنية خريطة الميزات الأكثر فائدة، فمن المستحسن إنشاء خريطة ميزات مخصصة تستفيد من تلك المعرفة.

from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit

# Prepare feature map for computing overlap
num_features = np.shape(X_train)[1]
num_qubits = int(num_features / 2)

# To use a custom feature map use the lines below.
entangler_map = [[0, 2], [3, 4], [2, 5], [1, 4], [2, 3], [4, 6]]

fm = QuantumCircuit(num_qubits)
training_param = Parameter("θ")
feature_params = ParameterVector("x", num_qubits * 2)
fm.ry(training_param, fm.qubits)
for cz in entangler_map:
fm.cz(cz[0], cz[1])
for i in range(num_qubits):
fm.rz(-2 * feature_params[2 * i + 1], i)
fm.rx(-2 * feature_params[2 * i], i)

الخطوتان 2 و3: تحسين المسألة وتنفيذها باستخدام البدائيات

سنقوم ببناء دائرة التداخل، وفي حال تشغيل هذا المثال على حاسوب كمي حقيقي، كنا سنحسّنها للتنفيذ كما فعلنا سابقًا. لكن في هذه الحالة، نريد المرور على جميع نقاط البيانات وحساب مصفوفة النواة الكاملة. لكل زوج من متجهات البيانات xi\vec{x}_i وxj\vec{x}_j، نُنشئ دائرة تداخل مختلفة. لذا يجب علينا تحسين الدائرة لكل زوج من نقاط البيانات. وبالتالي، ستُنفَّذ الخطوتان 2 و3 معًا في تكرارات متعددة.

خلية الكود أدناه تُنفّذ نفس العملية السابقة لزوج واحد من نقاط البيانات، لكنها هذه المرة داخل حلقتَي for، مع إضافة سطر في النهاية kernel_matrix[x_1,x_2] = ... لتخزين نتائج كل حساب. لاحظ أننا استثمرنا تماثل مصفوفة النواة لتقليل عدد الحسابات إلى النصف، كما قمنا بتعيين عناصر القطر الرئيسي إلى 1 كما ينبغي في غياب الضوضاء. وحسب التطبيق والدقة المطلوبة، يمكنك أيضًا استخدام عناصر القطر لتقدير الضوضاء أو دراستها لأغراض تخفيف الأخطاء.

بمجرد ملء مصفوفة النواة بالكامل، نكرر العملية لبيانات الاختبار ونملأ test_matrix. وهي في حقيقتها مصفوفة نواة أيضًا؛ نُطلق عليها اسمًا مختلفًا فقط للتمييز بين الاثنتين.

# To use a simulator
from qiskit.primitives import StatevectorSampler

# Remember to insert your token in the QiskitRuntimeService constructor to use real quantum computers
# service = QiskitRuntimeService()
# backend = service.least_busy(
# operational=True, simulator=False, min_num_qubits=fm.num_qubits
# )

num_shots = 10000

# Evaluate the problem using state vector-based primitives from Qiskit.
sampler = StatevectorSampler()

for x1 in range(0, train_size):
for x2 in range(x1 + 1, train_size):
unitary1 = fm.assign_parameters(list(X_train[x1]) + [np.pi / 2])
unitary2 = fm.assign_parameters(list(X_train[x2]) + [np.pi / 2])

# Create the overlap circuit
overlap_circ = unitary_overlap(unitary1, unitary2)
overlap_circ.measure_all()

# These lines run the qiskit sampler primitive.
counts = (
sampler.run([overlap_circ], shots=num_shots)
.result()[0]
.data.meas.get_int_counts()
)

# Assign the probability of the 0 state to the kernel matrix, and the transposed element (since this is an inner product)
kernel_matrix[x1, x2] = counts.get(0, 0.0) / num_shots
kernel_matrix[x2, x1] = counts.get(0, 0.0) / num_shots
# Fill in on-diagonal elements with 1, again, since this is an inner-product corresponding to probability (or alter the code to check these entries and verify they yield 1)
kernel_matrix[x1, x1] = 1

print("training done")

# Similar process to above, but for testing data.
for x1 in range(0, test_size):
for x2 in range(0, train_size):
unitary1 = fm.assign_parameters(list(X_test[x1]) + [np.pi / 2])
unitary2 = fm.assign_parameters(list(X_train[x2]) + [np.pi / 2])

# Create the overlap circuit
overlap_circ = unitary_overlap(unitary1, unitary2)
overlap_circ.measure_all()

counts = (
sampler.run([overlap_circ], shots=num_shots)
.result()[0]
.data.meas.get_int_counts()
)

test_matrix[x1, x2] = counts.get(0, 0.0) / num_shots

print("test matrix done")
training done
test matrix done

الخطوة 4: المعالجة اللاحقة وإرجاع النتيجة بصيغة كلاسيكية

الآن بعد أن حصلنا على مصفوفة نواة وtest_matrix بنفس التنسيق من خلال طرق النواة الكمية، يمكننا تطبيق خوارزميات التعلم الآلي الكلاسيكية للتنبؤ ببيانات الاختبار والتحقق من دقتها. سنبدأ باستيراد sklearn.svc من Scikit-Learn، وهو مصنّف متجه الدعم (SVC). يجب أن نحدد أننا نريد من SVC استخدام النواة المحسوبة مسبقًا باستخدام kernel = precomputed.

# import a support vector classifier from a classical ML package.
from sklearn.svm import SVC

# Specify that you want to use a pre-computed kernel matrix
qml_svc = SVC(kernel="precomputed")

باستخدام SVC.fit، يمكننا الآن تغذية مصفوفة النواة وتسميات التدريب للحصول على نموذج مُدرَّب. ثم يقوم SVC.score بتقييم بيانات الاختبار مقابل هذا النموذج باستخدام test_matrix ويُعيد الدقة.

# Feed in the pre-computed matrix and the labels of the training data. The classical algorithm gives you a fit.
qml_svc.fit(kernel_matrix, train_labels)

# Now use the .score to test your data, using the matrix of test data, and test labels as your inputs.
qml_score_precomputed_kernel = qml_svc.score(test_matrix, test_labels)
print(f"Precomputed kernel classification test score: {qml_score_precomputed_kernel}")
Precomputed kernel classification test score: 1.0

نرى أن دقة النموذج المُدرَّب بلغت 100%. هذا رائع ويُثبت أن QKE يعمل بشكل جيد. لكن هذا يختلف كثيرًا عن التفوق الكمي. فمن المرجح أن النوى الكلاسيكية كانت ستحل هذه المسألة التصنيفية بدقة 100% أيضًا. لا يزال هناك الكثير من العمل لتوصيف أنواع البيانات والعلاقات المختلفة بينها لمعرفة أين ستكون النوى الكمية الأكثر فائدة في عصر الاستخدام الفعلي الحالي.

نترك للمتعلم تعديل أجزاء من هذا المسار ودراسة فاعلية خرائط الميزات الكمية المختلفة. إليك بعض الأمور التي يمكن التفكير فيها:

  • ما مدى متانة الدقة؟ هل تنطبق على أنواع واسعة من البيانات أم على بيانات التدريب المحددة هذه فقط؟
  • ما البنية الموجودة في بياناتك التي تجعلك تشك في أن خريطة الميزات الكمية مفيدة؟
  • كيف تتأثر الدقة بزيادة أو تقليل حجم بيانات التدريب؟
  • ما خرائط الميزات التي يمكنك استخدامها وكيف تتباين النتائج بينها؟
  • كيف تتأثر الدقة ووقت التشغيل بزيادة عدد الميزات؟
  • أي الاتجاهات، إن وُجدت، تتوقع أن تنطبق على الحواسيب الكمية الحقيقية؟

التوسع نحو المزيد من الميزات والكيوبتات

في هذا القسم، سنكرر حساب عنصر واحد من المصفوفة، لكن لعدد أكبر بكثير من الميزات، مما يرسم مسار التوسع نحو الاستخدام الفعلي. الاقتصار على عنصر مصفوفة واحد يُتيح عرض العملية دون استنزاف الوقت المخصص لك على الحواسيب الكمية.

الخطوة 1: تحويل المدخلات الكلاسيكية إلى مسألة كمية

سنفترض نقطة انطلاق تتمثل في مجموعة بيانات تحتوي كل نقطة فيها على 42 ميزة. كما في المثال الأول، سنحسب عنصرًا واحدًا من مصفوفة النواة، مما يتطلب نقطتَي بيانات. النقطتان أدناه لهما 42 ميزة ومتغير فئة واحد (±1\pm 1).

# Two mock data points, including category labels, as in training

large_data = [
[
-0.028,
-1.49,
-1.698,
0.107,
-1.536,
-1.538,
-1.356,
-1.514,
-0.109,
-1.8,
-0.122,
-1.651,
-1.955,
-0.123,
-1.732,
0.091,
-0.048,
-0.128,
-0.026,
0.082,
-1.263,
0.065,
0.004,
-0.055,
-0.08,
-0.173,
-1.734,
-0.39,
-1.451,
0.078,
-1.578,
-0.025,
-0.184,
-0.119,
-1.336,
0.055,
-0.204,
-1.578,
0.132,
-0.121,
-1.599,
-0.187,
-1,
],
[
-1.414,
-1.439,
-1.606,
0.246,
-1.673,
0.002,
-1.317,
-1.262,
-0.178,
-1.814,
0.013,
-1.619,
-1.86,
-0.25,
-0.212,
-0.214,
-0.033,
0.071,
-0.11,
-1.607,
0.441,
-0.143,
-0.009,
-1.655,
-1.579,
0.381,
-1.86,
-0.079,
-0.088,
-0.058,
-1.481,
-0.064,
-0.065,
-1.507,
0.177,
-0.131,
-0.153,
0.07,
-1.627,
0.593,
-1.547,
-0.16,
-1,
],
]
train_data = [large_data[0][:-1], large_data[1][:-1]]

تذكّر أن zz_feature_map أنتج دوائر عميقة جداً حتى مع عدد قليل نسبياً من الميزات (14 ميزة). ومع زيادة عدد الميزات، نحتاج إلى مراقبة عمق الدائرة عن كثب. لتوضيح ذلك، سنجرّب أولاً استخدام zz_feature_map ونتحقق من عمق الدائرة الناتجة.

from qiskit.circuit.library import zz_feature_map

fm = zz_feature_map(
feature_dimension=np.shape(train_data)[1], entanglement="linear", reps=1
)

unitary1 = fm.assign_parameters(train_data[0])
unitary2 = fm.assign_parameters(train_data[1])
from qiskit.circuit.library import unitary_overlap

overlap_circ = unitary_overlap(unitary1, unitary2)
overlap_circ.measure_all()

print("circuit depth = ", overlap_circ.decompose(reps=2).depth())
print(
"two-qubit depth",
overlap_circ.decompose().depth(lambda instr: len(instr.qubits) > 1),
)
# overlap_circ.draw("mpl", scale=0.6, style="iqp")
circuit depth =  251
two-qubit depth 165

كما أشرنا سابقاً، تحديد الحد الذي يُعدّ فيه العمق كبيراً جداً أمرٌ دقيق ومعقّد. لكن عمق ثنائي الكيوبت يزيد على 100، حتى قبل عملية التحويل (transpilation)، يجعل الأمر غير قابل للتطبيق من البداية. ولهذا السبب تم التأكيد على خرائط الميزات المخصصة طوال هذا الدرس. إن كنت تعرف شيئاً عن بنية مجموعة بياناتك الكاملة، فعليك تصميم خريطة التشابك (entanglement map) مع مراعاة تلك البنية. هنا، بما أننا نحسب فقط الضرب الداخلي بين نقطتي بيانات، فقد أولينا الأولوية لتقليل عمق الدائرة على حساب أي اعتبارات تفصيلية لبنية البيانات.

from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit

# Prepare feature map for computing overlap

entangler_map = [
[3, 4],
[2, 5],
[1, 4],
[2, 3],
[4, 6],
[7, 9],
[10, 11],
[9, 12],
[8, 11],
[9, 10],
[11, 13],
[14, 16],
[17, 18],
[16, 19],
[15, 18],
[16, 17],
[18, 20],
]
# Use the entangler map above to build a feature map

num_features = np.shape(train_data)[1]
num_qubits = int(num_features / 2)

fm = QuantumCircuit(num_qubits)
training_param = Parameter("θ")
feature_params = ParameterVector("x", num_qubits * 2)
fm.ry(training_param, fm.qubits)
for cz in entangler_map:
fm.cz(cz[0], cz[1])
for i in range(num_qubits):
fm.rz(-2 * feature_params[2 * i + 1], i)
fm.rx(-2 * feature_params[2 * i], i)
from qiskit.circuit.library import unitary_overlap

# Assign features of each data point to a unitary, an instance of the general feature map.

unitary1 = fm.assign_parameters(list(train_data[0]) + [np.pi / 2])
unitary2 = fm.assign_parameters(list(train_data[1]) + [np.pi / 2])

# Create the overlap circuit

overlap_circ = unitary_overlap(unitary1, unitary2)
overlap_circ.measure_all()

لن نهتم بفحص الأعماق حتى الآن، لأن ما يهم فعلاً هو عمق ثنائي الكيوبت بعد التحويل.

الخطوة 2: تحسين المسألة للتنفيذ الكمي

نبدأ باختيار أقل الأنظمة الخلفية انشغالاً، ثم نحسّن الدائرة للتشغيل على ذلك النظام.

# Import needed packages
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService

# Get the least busy backend
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=fm.num_qubits
)
print(backend)
<IBMBackend('ibm_brisbane')>

في المهام صغيرة النطاق، غالباً ما يُعيد مدير التمرير المُعدّ مسبقاً نفس الدائرة بنفس العمق بشكل موثوق. لكن في الدوائر الكبيرة والمعقدة جداً، قد يُعيد مدير التمرير دوائر محوّلة مختلفة في كل مرة يُشغَّل فيها. يعود ذلك إلى استخدامه الإرشادات (heuristics)، ولأن الدوائر الكبيرة جداً ستملك مشهداً معقداً من التحسينات الممكنة. من المفيد غالباً إجراء التحويل عدة مرات واختيار الدائرة الأقل عمقاً. هذا لا يُدخل سوى عبء حسابي كلاسيكي وقد يحسّن النتائج من الحاسوب الكمي بشكل ملحوظ.

هنا نحوّل دائرة التداخل الوحداني 20 مرة، وننظر إلى أعماق الدوائر المحصَّلة.

# Apply level 3 optimization to our overlap circuit
transpiled_qcs = []
transpiled_depths = []
transpiled_twoqubit_depths = []
for i in range(1, 20):
pm = generate_preset_pass_manager(optimization_level=3, backend=backend)
overlap_ibm = pm.run(overlap_circ)
transpiled_qcs.append(overlap_ibm)
transpiled_depths.append(overlap_ibm.decompose().depth())
transpiled_twoqubit_depths.append(
overlap_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)
)

print("circuit depth = ", overlap_ibm.decompose().depth())
circuit depth =  61
print(transpiled_depths)
print(transpiled_twoqubit_depths)
[61, 60, 60, 69, 60, 60, 60, 65, 60, 60, 69, 61, 77, 77, 65, 60, 60, 77, 61]
[13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13]

هنا يمكنك ملاحظة بعض التباين في العمق الكلي للبوابات مع تمريرات تحويل مختلفة. دائرتنا ليست عميقة/واسعة بما يكفي بعد لرؤية تباين في أعماق ثنائي الكيوبت المحوّلة. سنستخدم transpiled_qcs[1]، التي لها عمق 60، أقل بقليل من عمق أعمق دائرة تم الحصول عليها وهي 77.

overlap_ibm = transpiled_qcs[1]

الخطوة 3: التنفيذ باستخدام Qiskit Runtime Primitives

مع اقترابنا من النطاق العملي، لن تكون المحاكيات مفيدة. تُعرض هنا فقط الصياغة الخاصة بالحواسيب الكمية الحقيقية.

# Run on ibm_osaka, 7-12-24, required 22 sec.

# Import our runtime primitive
from qiskit_ibm_runtime import SamplerV2 as Sampler

# Open a Runtime session:
session = Session(backend=backend)
num_shots = 10000
# Use sampler and get the counts

sampler = Sampler(mode=session)
options = sampler.options
options.dynamical_decoupling.enable = True
options.twirling.enable_gates = True
counts = (
sampler.run([overlap_ibm], shots=num_shots).result()[0].data.meas.get_int_counts()
)

# Close session after done
session.close()

الخطوة 4: المعالجة اللاحقة وإرجاع النتيجة بالصيغة الكلاسيكية

كما وُصف في المقدمة، القياس الأكثر فائدة هنا هو احتمال قياس الحالة الصفرية 00000|00000\rangle.

counts.get(0, 0.0) / num_shots
0.0138

يمكن تكرار هذه العملية لعنصر مصفوفة النواة الفردي بين أزواج بيانات أخرى في مجموعتك للحصول على مصفوفة النواة الكاملة. أبعاد مصفوفة النواة تحددها عدد النقاط في بيانات التدريب، وليس عدد الميزات. لذا فإن تكلفة حساب مصفوفة النواة وتحويلها إلى نموذج تنبؤي لا تتناسب مع عدد الميزات أو الكيوبتات. حتى في مجموعات البيانات الصغيرة نسبياً التي تحتوي على أعداد كبيرة من الميزات، لا يزال يجب مطابقة البيانات مع خريطة ميزات تُنتج تصنيفاً فعّالاً.

التوسع والعمل المستقبلي

تستلزم طريقة النواة قياس الحالة 0|0\rangle بأكبر قدر ممكن من الدقة. لكن أخطاء البوابات وأخطاء القراءة تعني أن هناك احتمالًا غير صفري pp لقياس أي كيوبت بشكل خاطئ في الحالة 1|1\rangle. حتى مع التبسيط المفرط بأن احتمالية 0|0\rangle يجب أن تكون 100%100\%، فإنه مع الميزات المشفّرة على، لنقل، NN بت، تنخفض احتمالية القياس الصحيح لجميع البتات على أنها 0|0\rangle إلى (1p)N(1-p)^N. كلما أصبح NN كبيرًا، أصبحت هذه الطريقة أقل وأقل موثوقية. التغلب على هذه الصعوبة وتوسيع تقدير النواة ليشمل عددًا أكبر من الميزات هو مجال بحث حالي. لمعرفة المزيد حول هذه المسألة، راجع هذا العمل بقلم Thanasilp, Wang, Cerezo, and Holmes. نوصيك باستكشاف ما يمكن تحقيقه مع الحواسيب الكمية الحالية، والتطلع أيضًا إلى ما سيكون ممكنًا في عصر تصحيح الأخطاء.

مراجعة

حساب نواة كمية يتضمن:

  • حساب عناصر مصفوفة النواة، باستخدام أزواج من نقاط بيانات التدريب
  • ترميز البيانات وتعيينها عبر تعيين الميزات
  • تحسين الدائرة للتشغيل على حواسيب/باكيندات كمية حقيقية

يمكن بعد ذلك استخدام النواة الكمية في خوارزميات التعلم الآلي الكلاسيكية، كما في هذا الدرس.

بعض الأشياء المهمة التي يجب مراعاتها عند استخدام النوى الكمية:

  • هل من المرجح أن تستفيد مجموعة البيانات من أساليب النواة الكمية؟
  • جرّب خرائط ميزات ومخططات تشابك مختلفة.
  • هل عمق الدائرة مقبول؟
  • جرّب تشغيل مدير التمرير عدة مرات واستخدم الدائرة ذات أصغر عمق يمكنك الحصول عليه.

أساليب النواة الكمية هي أدوات قوية محتملة إذا تحقق توافق مناسب بين مجموعات البيانات ذات الميزات الملائمة للكم، وخريطة ميزات كمية مناسبة. لفهم أفضل لأماكن انتفاع النوى الكمية على الأرجح، نوصيك بقراءة Liu, Arunachalam & Temme (2021).