דלג לתוכן הראשי

ליבות קוונטיות

מבוא לליבות קוונטיות

"שיטת הליבה הקוונטית" מתייחסת לכל שיטה שמשתמשת במחשבים קוונטיים להערכת ליבה. בהקשר זה, "ליבה" תתייחס למטריצת הליבה או לאיבריה הבודדים. נזכיר כי מיפוי מאפיינים Φ(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) בפועל. בשיטת הליבות הקוונטיות, מיפוי המאפיינים מתבצע על ידי Circuit קוונטי, והליבה מוערכת באמצעות מדידות על אותו Circuit והסתברויות המדידה היחסיות.

בשיעור זה נבחן את העומק של Circuits קידוד מוכנים מראש שמשתמשים בשזירה ניכרת ונשווה אותם לעומק של Circuits שנבנה בעצמנו. אין בכך המלצה לשיטה מסוימת. ייתכן שתגלו כי Circuits מוכנים מראש עמוקים מדי, ושרמת השזירה ב-Circuit המותאם אישית אינה מספקת. שוב, אלו מוצגים אך ורק כדי לאפשר לכם לחקור.

לפני שנעבור על הערכת מטריצת ליבה בפירוט, נסכם את זרימת העבודה תוך שימוש בשפה של תבניות Qiskit.

שלב 1: מיפוי קלטים קלאסיים לבעיה קוונטית

  • קלט: מערך נתוני אימון
  • פלט: Circuit מופשט לחישוב איבר של מטריצת ליבה

בהינתן מערך הנתונים, נקודת ההתחלה היא קידוד הנתונים ל-Circuit קוונטי. במילים אחרות, עלינו למפות את הנתונים שלנו למרחב הילברט של מצבי המחשב הקוונטי. אנו עושים זאת על ידי בניית Circuit תלוי-נתונים. יש דרכים רבות לעשות זאת, והשיעור הקודם תיאר מספר אפשרויות. אפשר לבנות Circuit משלכם לקידוד הנתונים, או להשתמש במיפוי מאפיינים מוכן כמו zz_feature_map. בשיעור זה נעשה את שניהם.

שימו לב שכדי לחשב איבר בודד של מטריצת הליבה, נרצה לקודד שתי נקודות שונות, כדי שנוכל להעריך את המכפלה הפנימית שלהן. זרימת עבודה של ליבה קוונטית מלאה תכלול כמובן מכפלות פנימיות רבות בין וקטורי נתונים ממופים, כמו גם שיטות למידת מכונה קלאסיות. אך השלב הבסיסי שחוזר על עצמו הוא הערכת איבר בודד של מטריצת הליבה. לשם כך בוחרים Circuit קוונטי תלוי-נתונים וממפים שני וקטורי נתונים למרחב המאפיינים.

Classical_Review_background_kernel_circuit

לצורך יצירת מטריצת ליבה, אנו מתעניינים בעיקר בהסתברות למדוד את המצב 0N|0\rangle^{\otimes N}, שבו כל NN ה-Qubits נמצאים במצב 0|0\rangle. כדי לראות מדוע, נשים לב שה-Circuit האחראי לקידוד ומיפוי של וקטור נתונים אחד 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} את שני ה-Circuits Φ(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). שכבת המדידה של ה-Circuit שלנו תחזיר הסתברויות מדידה (או "מעין-הסתברויות", אם משתמשים בשיטות מסוימות להפחתת שגיאות). ההסתברות שמעניינת אותנו היא זו של מצב האפס, 0N|0\rangle^{\otimes N}.

שלב 2: אופטימיזציה של הבעיה לביצוע קוונטי

  • קלט: Circuit מופשט, לא מותאם ל-Backend ספציפי
  • פלט: Circuit יעד ואובייקט Observable, מותאמים ל-QPU הנבחר

בשלב זה נשתמש בפונקציה generate_preset_pass_manager מ-Qiskit כדי לציין שגרת אופטימיזציה ל-Circuit שלנו ביחס למחשב הקוונטי האמיתי שעליו מתכננים להריץ את הניסוי. מגדירים optimization_level=3, כלומר נשתמש במנהל ה-Pass המוגדר מראש שמספק את רמת האופטימיזציה הגבוהה ביותר. בהקשר זה, "אופטימיזציה" מתייחסת לאופטימיזציה של יישום ה-Circuit על מחשב קוונטי אמיתי. זה כולל שיקולים כמו בחירת Qubits פיזיים שיתאימו ל-Qubits ב-Circuit הקוונטי המופשט כך שימזערו את עומק ה-Gate, או בחירת Qubits פיזיים עם שיעורי שגיאות נמוכים ככל האפשר. אין לכך קשר ישיר לאופטימיזציה של בעיית למידת המכונה (כמו במייעלים קלאסיים דוגמת COBYLA).

בהתאם לאופן שבו תממשו את שלב 2, ייתכן שתצטרכו לבצע אופטימיזציה של ה-Circuit יותר מפעם אחת, שכן כל זוג נקודות המעורבות באיבר מטריצה מניב Circuit שונה שיש למדוד.

שלב 3: ביצוע באמצעות Primitives של Qiskit Runtime

  • קלט: Circuit יעד
  • פלט: התפלגות הסתברויות

השתמשו ב-Primitive מסוג Sampler מ-Qiskit Runtime כדי לשחזר התפלגות הסתברות של מצבים שהתקבלו מדגימת ה-Circuit. שימו לב שאפשר לראות זאת מכונה "התפלגות מעין-הסתברות", מונח רלוונטי כשרעש הוא גורם בעיה וכאשר מוסיפים שלבים נוספים, כמו בהפחתת שגיאות. במקרים כאלה, סכום כל ההסתברויות עשוי שלא להיות שווה בדיוק ל-1; ומכאן "מעין-הסתברות".

שלב 4: עיבוד לאחר-מדידה, החזרת התוצאה בפורמט קלאסי

  • קלט: התפלגות הסתברויות
  • פלט: איבר בודד של מטריצת ליבה, או מטריצת ליבה שלמה אם חוזרים על התהליך

מחשבים את ההסתברות למדוד 0N|0\rangle^{\otimes N} על ה-Circuit הקוונטי וממלאים את מטריצת הליבה במיקום המתאים לשני וקטורי הנתונים שנעשה בהם שימוש. כדי למלא את מטריצת הליבה כולה, צריך להריץ ניסוי קוונטי עבור כל איבר. ברגע שיש בידינו מטריצת ליבה, נוכל להשתמש בה באלגוריתמים רבים של למידת מכונה קלאסית שמקבלים pre-calculated kernels. לדוגמה: qml_svc = SVC(kernel="precomputed"). לאחר מכן נוכל להשתמש ב-workstreams קלאסיים כדי להחיל את המודל על נתוני הבדיקה שלנו ולקבל ציון דיוק. בהתאם לשביעות רצוננו מציון הדיוק, ייתכן שנצטרך לחזור ולעיין בהיבטים של החישוב שלנו, כגון מיפוי המאפיינים.

סיכום השיעור

בשיעור זה נבצע שלבים אלה בכמה דרכים כדי למקסם את השימוש היעיל בזמן על מחשבים קוונטיים אמיתיים. נחיל שיטת ליבה קוונטית על:

  • איבר בודד של מטריצת ליבה עבור נתונים עם מעט מאפיינים יחסית, תוך שימוש ב-Backend אמיתי, כדי שנוכל לעקוב בקלות אחר מה שקורה בכל שלב.
  • מערך נתונים שלם עם מעט מאפיינים יחסית, תוך שימוש ב-Backend מדומה, כדי שנוכל לראות כיצד workstream הקוונטי מתחבר לשיטות למידת מכונה קלאסיות
  • איבר בודד של מטריצת ליבה עבור נתונים עם מאפיינים רבים, תוך שימוש במחשב קוונטי אמיתי. לא נעריך מטריצת ליבה שלמה עבור מערך נתונים גדול, מתוך כבוד לזמן על מחשבי 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. כתמיד, כדאי לשים עין על עומק ה-Circuit שלנו.

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: אופטימיזציה של הבעיה לביצוע קוונטי

נתחיל בבחירת ה-Backend הפחות עמוס, ולאחר מכן נבצע אופטימיזציה של ה-Circuit שלנו להרצה על אותו Backend.

# 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)

עבור Circuits מורכבים, שלב זה יגדיל משמעותית את עומק ה-Circuit כשהוא ממפה ל-Gates מקוריים עבור מחשבים קוונטיים אמיתיים, ומידע עשוי להצטרך להיות מועבר מ-Qubit ל-Qubit. במקרה פשוט זה, העומק כמעט ואינו מושפע.

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

שלב 3: ביצוע באמצעות Primitives של Qiskit Runtime

התחביר להרצה על סימולטור מוזנח להלן. עבור מערך נתונים זה, עם מספר קטן של מאפיינים, הרצה על סימולטור היא עדיין אפשרות. לחישובים בסדר גודל utility-scale, סימולציה אינה ישימה בדרך כלל. סימולטורים צריכים לשמש אך ורק לאיתור באגים בקוד מוקטן.

# 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 כפי שמוצג להלן. ניתן לראות שעבור מספר גדול של Qubits, תמונות כאלה הופכות מהר מאוד לבלתי ניתנות לניתוח.

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 שהביא לעומק קטן של שני Qubits לאחר Transpilation (עומק 1, למעשה). אם ה-Circuits שלכם הופכים עמוקים מדי, בוודאות ייווצר רעש רב, ודבר זה יביא לכך שהסתברות מדידת מצב האפס תהיה נמוכה מאוד, גם אם מיפוי המאפיינים שלכם מתאים לנתונים. לדוגמה, חזרה על התהליך לעיל תוך שימוש ב-zz_feature_map ו-, entanglement='linear', reps=1 הניבה dist.get(0,0.0) = 0.0015 עם אותן נקודות נתונים. הסיבה לכך היא עומקי Circuit ועומקי שני-Qubit גדולים בהרבה מ-zz_feature_map. התרשים להלן מציג את התפלגות ההסתברות עבור חישוב זה.

Bad results from a zz feature map.

שווה לשחק עם כמה נקודות נתונים מאותה קטגוריה כדי לראות עד כמה נמוך צריך להיות העומק שלכם כדי לקבל תוצאות טובות. מה שלהלן הוא עצה גסה שבוודאות תהיינה לה יוצאות דופן. בדרך כלל, עומק Transpiled של שני-Qubit של 10 או פחות לא אמור להיות בעיה. עומק Transpiled של שני-Qubit של 50-60 הוא state-of-the-art ויצריך הפחתת שגיאות מתקדמת ועוד כלים. בין לבין, תוצאותיכם עשויות להשתנות בהתאם לדמיון הנתונים, אקספרסיביות מיפוי המאפיינים, רוחב ה-Circuit וגורמים אחרים. בדרך כלל שלב העיבוד לאחר-מדידה יכלול גם תהליכי למידת מכונה קלאסיים. בחלק הבא נרחיב את התהליך הזה למערך נתונים שלם ונציג את זרימת העבודה של למידת מכונה קלאסית.

בדקו את הבנתכם

קראו את השאלות שלהלן, חשבו על תשובותיכם, ולאחר מכן לחצו על המשולשים כדי לגלות את הפתרונות.

ב-Circuit קוונטי של 10 Qubits, בדרך כלל, כמה מצבים שונים יכולים להיות מדודים?

תשובה:

2102^{10} או 1024.

נניח שמישהו חדש בחישוב קוונטי מנסה להשתמש ב-Circuit קוונטי עם עומק שני-Qubit גבוה מאוד, ואינו משתמש בהפחתת שגיאות. נניח עוד שהדבר מביא לשיעור שגיאות של 10% על כל Qubit. אם איבר מטריצת הליבה האמיתי (ללא שגיאות) המתאים ל-Circuit זה גדול מאוד, נניח 1.0, מה תהיה ההסתברות למדוד את כל 10 ה-Qubits במצב שבו כל Qubit נמצא ב-|0>?

תשובה:

ההסתברות שכל Qubit יימצא נכונה במצב |0> היא 0.90. ההסתברות שכל 10 ה-Qubits יימצאו במצב הנכון היא 0.90100.90^{10} או בערך 35%.

הסבירו במילים שלכם מדוע כל-כך חשוב לפקח על עומקי ה-Circuit. זה נכון באופן כללי, אבל הסבירו זאת בהקשר של הערכת ליבה קוונטית.

תשובה:

בזרימת עבודה של QKE, ההערכות שלנו מבוססות על מדידות מצב האפס, כלומר המצב שבו כל Qubit נמצא במצב 0|0\rangle. Circuits עמוקים מאוד יכניסו שיעורי שגיאות גבוהים. כאשר שיעור השגיאה הזה מצטבר על פני Qubits רבים, ירד משמעותית ההסתברות למדוד את מצב האפס.

מטריצת ליבה מלאה

בחלק זה נרחיב את התהליך שלמעלה לסיווג בינארי של מערך נתונים שלם. הדבר יציג שני רכיבים חשובים: (1) כעת נוכל לממש למידת מכונה קלאסית בעיבוד לאחר מכן, ו-(2) נוכל לקבל ציוני דיוק עבור האימון שלנו.

שלב 1: מיפוי קלטים קלאסיים לבעיה קוונטית

כעת נייבא מערך נתונים קיים לסיווג שלנו. מערך נתונים זה מורכב מ-128 שורות (נקודות נתונים) ו-14 מאפיינים בכל נקודה. יש איבר ה-15 שמציין את הקטגוריה הבינארית של כל נקודה (±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)

כעת ניצור מפת מאפיינים לקידוד ומיפוי הנתונים הקלאסיים שלנו ב-Circuit קוונטי. אנחנו חופשיים לבנות מפת מאפיינים משלנו או להשתמש באחת מוכנה מראש. אל תהססו לשנות את מפת המאפיינים למטה, או לעבור בחזרה ל-ZFeatureMap. אבל שימו תמיד לב לעומק ה-Circuit. זכרו שבדוגמת 6 ה-Qubit הקודמת עומק ה-Circuit המתומלל היה גבוה מדי כשהשתמשנו ב-zz_feature_map. ככל שהסקאלה והמורכבות של ה-Circuit גדלות, העומק עלול לגדול במהירות לנקודה שבה הרעש מכריע את התוצאות. כאשר אתם יודעים משהו על מבנה הנתונים שעשוי להצביע על איזו מבנה של מפת מאפיינים יהיה המועיל ביותר, כדאי ליצור מפת מאפיינים מותאמת אישית שמנצלת את הידע הזה.

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: אופטימיזציה של הבעיה והרצה באמצעות primitives

נבנה Circuit חפיפה, ואם היינו מריצים על מחשב קוונטי אמיתי בדוגמה זו, היינו מייעלים אותו להרצה כמקודם. אבל במקרה זה, בכוונתנו לעבור על כל נקודות הנתונים ולחשב את מטריצת הגרעין המלאה. עבור כל זוג וקטורי נתונים xi\vec{x}_i ו-xj\vec{x}_j, אנחנו יוצרים Circuit חפיפה שונה. לכן עלינו לייעל את ה-Circuit עבור כל זוג נקודות נתונים. לפיכך שלבים 2 ו-3 יתבצעו יחד במהלך האיטרציות המרובות.

תא הקוד למטה מבצע בדיוק את אותו תהליך כמקודם עבור זוג נקודות נתונים יחיד. הפעם הוא פשוט מורץ בתוך שתי לולאות for, ויש שורה נוספת בסוף kernel_matrix[x_1,x_2] = ... לאחסון תוצאות כל חישוב. שימו לב שניצלנו את הסימטריה של מטריצת גרעין כדי לצמצם את מספר החישובים ב-1/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%. יש עוד הרבה עבודה לאפיון סוגי נתונים שונים וקשרי נתונים כדי לראות היכן גרעינים קוונטיים יהיו המועילים ביותר בעידן השימוש הנוכחי. אנחנו מותירים ללומד לשנות חלקים בתהליך עבודה זה ולחקור את האפקטיביות של מפות מאפיינים קוונטיות שונות. הנה כמה דברים שכדאי לשקול:

  • עד כמה הדיוק חזק? האם הוא מחזיק לסוגים רחבים של נתונים או רק לנתוני אימון ספציפיים אלו?
  • איזה מבנה בנתונים שלכם גורם לכם לחשוד שמפת מאפיינים קוונטית שימושית?
  • כיצד הדיוק מושפע מהגדלה/הקטנה של כמות נתוני האימון?
  • אילו מפות מאפיינים תוכלו להשתמש וכיצד התוצאות משתנות עם מפות מאפיינים?
  • כיצד הדיוק וזמן הריצה מושפעים מהגדלת מספר המאפיינים?
  • אילו מגמות, אם בכלל, אתם מצפים שיתקיימו על מחשבים קוונטיים אמיתיים?

סקאלה לעוד מאפיינים ו-Qubit

בחלק זה נחזור על חישוב של איבר מטריצה יחיד, אבל עבור מספר גדול בהרבה של מאפיינים, ונתווה את הדרך לסקאלה לכיוון שימושיות. ההגבלה לאיבר מטריצה יחיד נעשית כך שניתן יהיה להראות את התהליך מבלי לבזבז יותר מדי מהזמן המוקצב שלכם על מחשבים קוונטיים.

שלב 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

כפי שתואר קודם, קביעת העומק שהוא "עמוק מדי" היא עניין מורכב. אבל עומק של שני Qubit-ים מעל 100, אפילו לפני ה-Transpilation, הוא פשוט לא ישים. זו הסיבה שהדגשנו לאורך כל השיעור את חשיבות מפות התכונות המותאמות אישית. אם יש לך ידע על מבנה מערך הנתונים שלך, כדאי לתכנן את מפת ה-entanglement בהתאם למבנה הזה. כאן, מכיוון שאנחנו מחשבים רק את המכפלה הפנימית בין שתי נקודות מידע, העדפנו עומק מעגל נמוך על פני התחשבות מפורטת במבנה הנתונים.

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()

לא נטרח לבדוק את העומקים עכשיו, כי מה שחשוב באמת הוא עומק שני ה-Qubit-ים לאחר ה-Transpilation.

שלב 2: אופטימיזציה של הבעיה להרצה קוונטית

נתחיל בבחירת ה-Backend הפחות עמוס, ואז נבצע אופטימיזציה של המעגל להרצה על אותו Backend.

# 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')>

במשימות קטנות, מנהל הפאסים המוגדר מראש לרוב מחזיר את אותו מעגל עם אותו עומק, בצורה עקבית. אבל במעגלים גדולים ומורכבים מאוד, מנהל הפאסים עשוי להחזיר מעגלים שונים בכל הרצה. הסיבה לכך היא שהוא משתמש בהיוריסטיקות, ומכיוון שמעגלים גדולים מאוד יוצרים מרחב מורכב של אפשרויות אופטימיזציה. לעיתים קרובות כדאי לבצע Transpilation כמה פעמים ולבחור את המעגל הרדוד ביותר. זה מוסיף רק תקורה קלאסית אבל עשוי לשפר משמעותית את התוצאות מהמחשב הקוונטי.

כאן נבצע Transpilation למעגל ה-overlap 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]

כאן אפשר לראות שיש שונות מסוימת בעומק השערים הכולל בין פאסי Transpilation שונים. המעגל שלנו עדיין לא עמוק או רחב מספיק כדי לראות שונות בעומקי שני ה-Qubit-ים לאחר ה-Transpilation. נשתמש ב-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

התהליך הזה עבור איבר בודד במטריצת הגרעין יכול להיות חוזר על זוגות נתונים אחרים במערך כדי לקבל את מטריצת הגרעין המלאה. מימד מטריצת הגרעין נקבע לפי מספר הנקודות בנתוני האימון שלך, לא לפי מספר התכונות. לכן עלות החישוב של עיבוד מטריצת הגרעין לדגם חיזוי אינה גדלה עם מספר התכונות או ה-Qubit-ים. אפילו עבור מערכי נתונים קטנים יחסית עם מספר גדול של תכונות, הנתונים עדיין צריכים להיות ממופים למפת תכונות שמניבה סיווג אפקטיבי.

סקאלביליות ועבודה עתידית

שיטת הגרעין מחייבת שנמדוד את 0|0\rangle בדיוק הגבוה ביותר האפשרי. אבל שגיאות שערים ושגיאות קריאה אומרות שיש הסתברות לא-אפסית pp שכל Qubit יימדד בטעות במצב 1|1\rangle. גם עם ההפשטה הפשטנית שהסתברות ל-0|0\rangle אמורה להיות 100%100\%, עבור תכונות רבות המקודדות על, נניח, NN ביטים, ההסתברות למדידה נכונה של כל הביטים כ-0|0\rangle יורדת ל-(1p)N(1-p)^N. ככל ש-NN גדל, השיטה הזו נהיית פחות ופחות אמינה. התגברות על הקושי הזה והרחבת אומדן הגרעין לתכונות רבות יותר ויותר הם תחום מחקר פעיל. לקריאה נוספת על הנושא, ראו את העבודה של Thanasilp, Wang, Cerezo, and Holmes. אנחנו ממליצים לחקור מה אפשר לעשות עם מחשבים קוונטיים קיימים, ולצפות גם למה שיהיה אפשרי בעידן תיקון השגיאות.

סיכום

חישוב גרעין קוונטי כולל:

  • חישוב איברי מטריצת הגרעין, תוך שימוש בזוגות של נקודות נתוני אימון
  • קידוד הנתונים ומיפוים דרך מיפוי תכונות
  • אופטימיזציה של המעגל להרצה על מחשבים קוונטיים אמיתיים / Backends

הגרעין הקוונטי יכול לאחר מכן לשמש באלגוריתמים קלאסיים של למידת מכונה, כפי שהוצג בשיעור זה.

כמה דברים חשובים לזכור בעת שימוש בגרעינים קוונטיים:

  • האם מערך הנתונים צפוי להפיק תועלת משיטות גרעין קוונטי?
  • נסו מפות תכונות ותכניות entanglement שונות.
  • האם עומק המעגל מקובל?
  • נסו להריץ מנהל פאסים מספר פעמים ולהשתמש במעגל בעל העומק הנמוך ביותר שתקבלו.

שיטות גרעין קוונטי הן כלים עוצמתיים פוטנציאליים כאשר יש התאמה מתאימה בין מערכי נתונים עם תכונות מתאימות לקוונטים, ומפת תכונות קוונטית מתאימה. כדי להבין טוב יותר היכן גרעינים קוונטיים עשויים להיות שימושיים, אנחנו ממליצים לקרוא את Liu, Arunachalam & Temme (2021).