מעגלים וריאציוניים קוונטיים ורשתות עצביות קוונטיות
בשיעור זה נממש מספר מעגלים קוונטיים וריאציוניים למשימת סיווג נתונים, הנקראים מסווגים קוונטיים וריאציוניים (VQCs). בשלב מסוים היה נהוג לכנות תת-קבוצה של VQCs כרשתות עצביות קוונטיות (QNNs), בהקבלה לרשתות עצביות קלאסיות. אכן, ישנם מקרים בהם מבנים שאולים מרשתות עצביות קלאסיות, כמו שכבות קונבולוציה, ממלאים תפקיד חשוב ב-VQCs. במקרים כאלה שבהם ההקבלה חזקה, QNNs עשויים להיות תיאור שימושי. אולם מעגלים קוונטיים מפרמטרים אינם חייבים לעקוב אחרי המבנה הכללי של רשת עצבית; למשל, לא כל הנתונים צריכים להיטען בשכבה הראשונה (כניסה); אפשר לטעון חלק מהנתונים בשכבה הראשונה, להפעיל כמה Gate-ים ואז לטעון נתונים נוספים (תהליך הנקרא "reuploading" של נתונים). לכן עלינו לחשוב על QNNs כתת-קבוצה של מעגלים קוונטיים מפרמטרים, ולא להגביל את עצמנו בחיפוש אחרי מעגלים קוונטיים שימושיים בגלל ההקבלה לרשתות עצביות קלאסיות.
מערך הנתונים שמטופל בשיעור זה מורכב מתמונות המכילות פסים אופקיים ואנכיים, ומטרתנו היא לסווג תמונות חדשות לאחת משתי הקטגוריות בהתאם לכיוון הקו שבהן. נשיג זאת באמצעות VQC. תוך כדי עבודה נבחן דרכים לשיפור ולהרחבת הסקאלה של החישוב. מערך הנתונים כאן פשוט מאוד לסיווג קלאסי. הוא נבחר בשל פשטותו כדי שנוכל להתמקד בחלק הקוונטי של הבעיה, ולבחון כיצד מאפיין של מערך נתונים עשוי להתורגם לחלק של מעגל קוונטי. אין טעם לצפות להאצה קוונטית במקרים פשוטים כאלה שבהם אלגוריתמים קלאסיים כל-כך יעילים.
בסיום שיעור זה תוכל/י:
- לטעון נתונים מתמונה לתוך Circuit קוונטי
- לבנות ansatz עבור VQC (או QNN) ולהתאים אותו לבעיה שלך
- לאמן את ה-VQC/QNN שלך ולהשתמש בו לביצוע תחזיות מדויקות על נתוני בדיקה
- להרחיב את הסקאלה של הבעיה ולזהות מגבלות של מחשבים קוונטיים עכשוויים
יצירת נתונים
נתחיל בבניית הנתונים. מערכי נתונים לרוב אינם נוצרים במפורש כחלק ממסגרת Qiskit patterns. אבל סוג הנתונים והכנתם הם קריטיים להצלחת יישום המחשוב הקוונטי ללמידת מכונה. הקוד שלהלן מגדיר מערך נתונים של תמונות עם ממדי פיקסל קבועים. שורה אחת מלאה או עמודה אחת מלאה בתמונה מקבלת את הערך , והפיקסלים הנותרים מקבלים ערכים אקראיים בתחום . הערכים האקראיים הם רעש בנתונים שלנו. עיין/י בקוד כדי לוודא שאתה/ת מבין/ה כיצד התמונות נוצרות. בהמשך נגדיל את הסקאלה של התמונות.
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-ibm-runtime scipy scikit-learn
# This code defines the images to be classified:
import numpy as np
# Total number of "pixels"/qubits
size = 8
# One dimension of the image (called vertical, but it doesn't matter). Must be a divisor of `size`
vert_size = 2
# The length of the line to be detected (yellow). Must be less than or equal to the smallest dimension of the image (`<=min(vert_size,size/vert_size)`
line_size = 2
def generate_dataset(num_images):
images = []
labels = []
hor_array = np.zeros((size - (line_size - 1) * vert_size, size))
ver_array = np.zeros((round(size / vert_size) * (vert_size - line_size + 1), size))
j = 0
for i in range(0, size - 1):
if i % (size / vert_size) <= (size / vert_size) - line_size:
for p in range(0, line_size):
hor_array[j][i + p] = np.pi / 2
j += 1
# Make two adjacent entries pi/2, then move down to the next row. Careful to avoid the "pixels" at size/vert_size - linesize, because we want to fold this list into a grid.
j = 0
for i in range(0, round(size / vert_size) * (vert_size - line_size + 1)):
for p in range(0, line_size):
ver_array[j][i + p * round(size / vert_size)] = np.pi / 2
j += 1
# Make entries pi/2, spaced by the length/rows, so that when folded, the entries appear on top of each other.
for n in range(num_images):
rng = np.random.randint(0, 2)
if rng == 0:
labels.append(-1)
random_image = np.random.randint(0, len(hor_array))
images.append(np.array(hor_array[random_image]))
elif rng == 1:
labels.append(1)
random_image = np.random.randint(0, len(ver_array))
images.append(np.array(ver_array[random_image]))
# Randomly select 0 or 1 for a horizontal or vertical array, assign the corresponding label.
# Create noise
for i in range(size):
if images[-1][i] == 0:
images[-1][i] = np.random.rand() * np.pi / 4
return images, labels
hor_size = round(size / vert_size)
שים/י לב שהקוד שלמעלה גם יצר תוויות המציינות האם התמונות מכילות קו אנכי (+1) או אופקי (-1). כעת נשתמש ב-sklearn לפיצול מערך נתונים של 100 תמונות לסט אימון וסט בדיקה (יחד עם התווי ות המתאימות שלהם). כאן אנו משתמשים ב- ממערך הנתונים לאימון, כאשר ה- הנותרים נשמרים לבדיקה.
from sklearn.model_selection import train_test_split
np.random.seed(42)
images, labels = generate_dataset(200)
train_images, test_images, train_labels, test_labels = train_test_split(
images, labels, test_size=0.3, random_state=246
)
בואו נצייר כמה אלמנטים ממערך הנתונים שלנו כדי לראות איך הקווים האלה נראים:
import matplotlib.pyplot as plt
# Make subplot titles so we can identify categories
titles = []
for i in range(8):
title = "category: " + str(train_labels[i])
titles.append(title)
# Generate a figure with nested images using subplots.
fig, ax = plt.subplots(4, 2, figsize=(10, 6), subplot_kw={"xticks": [], "yticks": []})
for i in range(8):
ax[i // 2, i % 2].imshow(
train_images[i].reshape(vert_size, hor_size),
aspect="equal",
)
ax[i // 2, i % 2].set_title(titles[i])
plt.subplots_adjust(wspace=0.1, hspace=0.3)
כל אחת מהתמונות האלה עדיין מזווגת עם התווית שלה ב-train_labels בצורת רשימה פשוטה:
print(train_labels[:8])
[1, 1, 1, 1, -1, 1, 1, 1]
מסווג קוונטי וריאציוני: ניסיון ראשון
שלב 1 בדפוסי Qiskit: מיפוי הבעיה למעגל קוונטי
המטרה היא למצוא פונקציה עם פרמטרים שממפה וקטור נתונים / תמונה לקטגוריה הנכונה: . זה יושג באמצעות VQC עם כמה שכבות שניתן לזהות לפי מטרותיהן הייחודיות:
כאן, הוא ה-Circuit של הקידוד, שלגביו יש לנו אפשרויות רבות כפי שנראה בשיעורים קודמים. הוא בלוק Circuit וריאציוני, או ניתן לאימון, ו- הוא אוסף הפרמטרים שיש לאמן. פרמטרים אלה ישתנו על ידי אלגוריתמי אופטימיזציה קלאסיים כדי למצוא את קבוצת הפרמטרים שמניבה את הסיווג הטוב ביותר של תמונות על ידי ה-Circuit הקוונטי. ה-Circuit הוריאציוני הזה נקרא לפעמים "ansatz". לבסוף, הוא אובייקטיב כלשהו שיוערך באמצעות ה-Estimator primitive. אין שום אילוץ שמכריח את השכבות לבוא בסדר הזה, או אפילו להיות נפרדות לחלוטין. אפשר לקבל שכבות וריאציוניות ו/או שכבות קידוד מרובות בכל סדר שמוצדק מבחינה טכנית.
נתחיל בבחירת feature map לקידוד הנתונים שלנו. נשתמש ב-z_feature_map, מכיוון שהוא שומר על עומקי Circuit נמוכים בהשוואה לחלק ממיפויי הפיצ'רים האחרים.
from qiskit.circuit.library import z_feature_map
# One qubit per data feature
num_qubits = len(train_images[0])
# Data encoding
# Note that qiskit orders parameters alphabetically. We assign the parameter prefix "a" to ensure our data encoding goes to the first part of the circuit, the feature mapping.
feature_map = z_feature_map(num_qubits, parameter_prefix="a")
כעת עלינו להחליט על ansatz שיאומן. ישנם שיקולים רבים בבחירת ansatz. תיאור מלא חורג מהיקף מבוא זה; כאן אנו רק מצביעים על כמה קטגוריות של שיקולים.
- חומרה: כל המחשבים הקוונטיים המודרניים נוטים יותר לשגיאות ורגישים יותר לרעש מאשר עמיתיהם הקלאסיים. שימוש ב-ansatz עמוק מדי (בעיקר מבחינת עומק Transpiled של שני Qubit-ים) לא ייתן תוצאות טובות. בעיה קשורה היא שלמחשבים קוונטיים יש פריסה מסוימת של Qubit-ים, כלומר חלק מה-Qubit-ים הפיזיים הם שכנים במחשב הקוונטי, ואחרים עשויים להיות רחוקים מאוד זה מזה. שזירת Qubit-ים סמוכים לא מגדילה את העומק יותר מדי, אבל שזירת Qubit-ים רחוקים מאוד יכולה להגדיל את העומק באופן משמעותי, מכיוון שעלינו להכניס Gate-י swap כדי להעביר מידע ל-Qubit-ים שסמוכים זה לזה כדי שיוכלו להשתזר.
- הבעיה: בכל פעם שיש לך מידע על הבעיה שלך שיכול להנחות את ה-ansatz שלך, השתמש/י בו. למשל, הנתונים בשיעור זה מורכבים מתמונות של קווים אופקיים ואנכיים. אפשר לשקול איזה מתאם בין צבעים/ערכים סמוכים מזהה תמונה של קו אופקי או אנכי. אילו תכונות של ansatz יתאימו למתאם הזה בין פיקסלים סמוכים? נחזור לנקודה זו מבחינה יותר טכנית בהמשך השיעור. אבל לעת עתה, נאמר פשוט שהכללת שזירה ו-Gate-י CNOT בין Qubit-ים המתאימים לפיקסלים סמוכים נראית כרעיון טוב. בתמונה הגדולה, שקול/י האם הבעיה פתורה טוב יותר באמצעות Circuit קוונטי, או האם קיימים אלגוריתמים קלאסיים שיכולים לעשות עבודה טובה באותה מידה.
- מספר הפרמטרים: כל Gate קוונטי בעל פרמטר עצמאי ב-Circuit מגדיל את המרחב שיש לבצע עליו אופטימיזציה קלאסית, מה שמביא להתכנסות איטית יותר. אבל כאשר הבעיות מתרחבות בסקאלה, עלולים להיתקל ב-ישורות שוממות (barren plateaus). מונח זה מתייחס לתופעה שבה נוף האופטימיזציה של אלגוריתם קוונטי וריאציוני הופך שטוח ואנמי באופן אקספוננציאלי ככל שגודל הבעיה גדל. זה גורם לגרדיינטים דועכים, מה שמקשה על אימון יעיל של האלגוריתם[1]. ישורות שוממות רלוונטיות לאלגוריתמים קוונטיים וריאציוניים כמו VQCs/QNNs. יש לציין שמספר הפרמטרים הגדל הוא לא השיקול היחיד להימנעות מישורות שוממות; שיקולים אחרים כוללים פונקציות עלות גלובליות ואתחול פרמטרים אקראי.
בשיעור זה נראה כמה דוגמאות פשוטות לשיטות עבודה טובות בבניית ansatz. בואו ננסה תחילה את ה-ansatz שלהלן. נחזור לשנות אותו בהמשך.
# Import the necessary packages
from qiskit import QuantumCircuit
from qiskit.circuit import ParameterVector
# Initialize the circuit using the same number of qubits as the image has pixels
qnn_circuit = QuantumCircuit(size)
# We choose to have two variational parameters for each qubit.
params = ParameterVector("θ", length=2 * size)
# A first variational layer:
for i in range(size):
qnn_circuit.ry(params[i], i)
# Here is a list of qubit pairs between which we want CNOT gates. The choice of these is not yet obvious.
qnn_cnot_list = [[0, 1], [1, 2], [2, 3]]
for i in range(len(qnn_cnot_list)):
qnn_circuit.cx(qnn_cnot_list[i][0], qnn_cnot_list[i][1])
# The second variational layer:
for i in range(size):
qnn_circuit.rx(params[size + i], i)
# Check the circuit depth, and the two-qubit gate depth
print(qnn_circuit.decompose().depth())
print(
f"2+ qubit depth: {qnn_circuit.decompose().depth(lambda instr: len(instr.qubits) > 1)}"
)
# Draw the circuit
qnn_circuit.draw("mpl")
5
2+ qubit depth: 3
┌──────────┐ ┌──────────┐
q_0: ┤ Ry(θ[0]) ├──────■──────┤ Rx(θ[8]) ├─────────────────────────
├──────────┤ ┌─┴─┐ └──────────┘┌──────────┐
q_1: ┤ Ry(θ[1]) ├────┤ X ├─────────■──────┤ Rx(θ[9]) ├─────────────
├──────────┤ └───┘ ┌─┴─┐ └──────────┘┌───────────┐
q_2: ┤ Ry(θ[2]) ├────────────────┤ X ├─────────■──────┤ Rx(θ[10]) ├
├──────────┤ └───┘ ┌─┴─┐ ├───────────┤
q_3: ┤ Ry(θ[3]) ├────────────────────────────┤ X ├────┤ Rx(θ[11]) ├
├──────────┤┌───────────┐ └───┘ └───────────┘
q_4: ┤ Ry(θ[4]) ├┤ Rx(θ[12]) ├───────────────────────── ────────────
├──────────┤├───────────┤
q_5: ┤ Ry(θ[5]) ├┤ Rx(θ[13]) ├─────────────────────────────────────
├──────────┤├───────────┤
q_6: ┤ Ry(θ[6]) ├┤ Rx(θ[14]) ├─────────────────────────────────────
├──────────┤├───────────┤
q_7: ┤ Ry(θ[7]) ├┤ Rx(θ[15]) ├─────────────────────────────────────
└──────────┘└───────────┘
לאחר שהכנו את קידוד הנתונים ואת ה-Circuit הוריאציוני, נוכל לשלב אותם ליצירת ה-ansatz המלא שלנו. במקרה זה, הרכיבים של ה-Circuit הקוונטי שלנו דומים למדי לאלה שברשתות עצביות, כאשר דומה ביותר לשכבה שטוענת ערכי קלט מהתמונה, ו- דומה לשכבה של "משקולות" משתנות. מאחר שהקבלה זו נכונה במקרה זה, אנו מאמצים "qnn" בחלק ממוסכמות השמות שלנו; אבל הקבלה זו לא צריכה להגביל את חקירתך של VQCs.

# QNN ansatz
ansatz = qnn_circuit
# Combine the feature map with the ansatz
full_circuit = QuantumCircuit(num_qubits)
full_circuit.compose(feature_map, range(num_qubits), inplace=True)
full_circuit.compose(ansatz, range(num_qubits), inplace=True)
# Display the circuit
full_circuit.decompose().draw("mpl", style="clifford", fold=-1)
כעת עלינו להגדיר אובייקטיב, כדי שנוכל להשתמש בו בפונקציית העלות שלנו. נקבל ערך ציפייה לאובייקטיב זה באמצעות Estimator. אם בחרנו ansatz טוב שמונחה על ידי הבעיה, אז כל Qubit יכיל מידע רלוונטי לסיווג. אפשר להוסיף שכבות לשילוב המידע על Qubit-ים מעטים יותר (הנקראת שכבה קונבולוציונית), כך שמדידות נדרשות רק על תת-קבוצה של ה-Qubit-ים ב-Circuit (כמו ברשתות עצביות קונבולוציוניות). או שאפשר למדוד מאפיין כלשהו מכל Qubit. כאן נבחר באפשרות השנייה, ולכן נכלול אופרטור Z לכל Qubit. אין שום ייחוד בבחירת , אבל היא מוצדקת היטב:
- זו משימת סיווג בינארי, ומדידה של יכולה להניב שני תוצאות אפשריות.
- ערכי העצמי של () מופרדים באופן סביר, ומביאים לתוצאת Estimator בתחום [-1, +1], כאשר 0 יכול פשוט לשמש כערך סף.
- קל למדוד בבסיס Pauli Z ללא תקורה נוספת של Gate-ים.
לכן, Z הוא בחירה מאוד טבעית.
from qiskit.quantum_info import SparsePauliOp
observable = SparsePauliOp.from_list([("Z" * (num_qubits), 1)])
יש לנו את ה-Circuit הקוונטי שלנו ואת האובייקטיב שאנו רוצים להעריך. כעת אנו זקוקים לכמה דברים כדי להריץ ולאופטמז את ה-Circuit הזה. ראשית, אנו זקוקים לפונקציה להרצת forward pass. שים/י לב שהפונקציה שלהלן מקבלת את input_params ואת weight_params בנפרד. הראשון הוא קבוצת הפרמטרים הסטטיים המתארים את הנתונים בתמונה, והשני הוא קבוצת הפרמטרים המשתנים שיש לאופטמז.
from qiskit.primitives import BaseEstimatorV2
from qiskit.quantum_info.operators.base_operator import BaseOperator
def forward(
circuit: QuantumCircuit,
input_params: np.ndarray,
weight_params: np.ndarray,
estimator: BaseEstimatorV2,
observable: BaseOperator,
) -> np.ndarray:
"""
Forward pass of the neural network.
Args:
circuit: circuit consisting of data loader gates and the neural network ansatz.
input_params: data encoding parameters.
weight_params: neural network ansatz parameters.
estimator: EstimatorV2 primitive.
observable: a single observable to compute the expectation over.
Returns:
expectation_values: an array (for one observable) or a matrix (for a sequence of observables) of expectation values.
Rows correspond to observables and columns to data samples.
"""
num_samples = input_params.shape[0]
weights = np.broadcast_to(weight_params, (num_samples, len(weight_params)))
params = np.concatenate((input_params, weights), axis=1)
pub = (circuit, observable, params)
job = estimator.run([pub])
result = job.result()[0]
expectation_values = result.data.evs
return expectation_values
פונקציית אובדן
לאחר מכן, אנו זקוקים לפונקציית אובדן לחישוב ההפרש בין הערכים החזויים לערכי התוויות המחושבים. הפונקציה תקבל את התוויות שהאלגוריתם חזה ואת התוויות הנכונות ותחזיר את ממוצע ההפרש הריבועי. ישנן פונקציות אובדן רבות. כאן, MSE הוא דוגמה שבחרנו.
def mse_loss(predict: np.ndarray, target: np.ndarray) -> np.ndarray:
"""
Mean squared error (MSE).
prediction: predictions from the forward pass of neural network.
target: true labels.
output: MSE loss.
"""
if len(predict.shape) <= 1:
return ((predict - target) ** 2).mean()
else:
raise AssertionError("input should be 1d-array")
בואו גם נגדיר פונקציית אובדן שונה מעט שהיא פונקציה של הפרמטרים המשתנים (המשקולות), לשימוש על ידי האופטימייזר הקלאסי. פונקציה זו מקבלת רק את פרמטרי ה-ansatz כקלט; משתנים אחרים עבור ה-forward pass והאובדן מוגדרים כפרמטרים גלובליים. האופטימייזר יאמן את המודל על ידי דגימת משקולות שונות וניסיון להוריד את הפלט של פונקציית העלות/אובדן.
def mse_loss_weights(weight_params: np.ndarray) -> np.ndarray:
"""
Cost function for the optimizer to update the ansatz parameters.
weight_params: ansatz parameters to be updated by the optimizer.
output: MSE loss.
"""
predictions = forward(
circuit=circuit,
input_params=input_params,
weight_params=weight_params,
estimator=estimator,
observable=observable,
)
cost = mse_loss(predict=predictions, target=target)
objective_func_vals.append(cost)
global iter
if iter % 50 == 0:
print(f"Iter: {iter}, loss: {cost}")
iter += 1
return cost
לעיל התייחסנו לשימוש באופטימייזר קלאסי. כאשר נגיע לחיפוש בין משקולות כדי למזער את פונקציית העלות, נשתמש באופטימייזר COBYLA:
from scipy.optimize import minimize
נגדיר כמה משתנים גלובליים ראשוניים עבור פונקציית העלות.
# Globals
circuit = full_circuit
observables = observable
# input_params = train_images_batch
# target = train_labels_batch
objective_func_vals = []
iter = 0
שלב 2 בדפוסי Qiskit: אופטימיזציה של הבעיה לביצוע קוונטי
אנחנו מתחילים בבחירת Backend להרצה. במקרה הזה נשתמש ב-Backend הפחות עמוס.
from qiskit_ibm_runtime import QiskitRuntimeService
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)
print(backend.name)
ibm_brisbane
כאן אנחנו מייעלים את ה-Circuit להרצה על Backend אמיתי על ידי ציון רמת האופטימיזציה והוספת decoupling דינמי. הקוד הבא מייצר pass manager באמצעות preset pass managers מ-qiskit.transpiler.
from qiskit.circuit.library import XGate
from qiskit.transpiler import PassManager
from qiskit.transpiler.passes import (
ALAPScheduleAnalysis,
ConstrainedReschedule,
PadDynamicalDecoupling,
)
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
target = backend.target
pm = generate_preset_pass_manager(target=target, optimization_level=3)
pm.scheduling = PassManager(
[
ALAPScheduleAnalysis(target=target),
ConstrainedReschedule(
acquire_alignment=target.acquire_alignment,
pulse_alignment=target.pulse_alignment,
target=target,
),
PadDynamicalDecoupling(
target=target,
dd_sequence=[XGate(), XGate()],
pulse_alignment=target.pulse_alignment,
),
]
)
עכשיו אנחנו מריצים את ה-pass manager על ה-Circuit. שינויי הפריסה שנוצרים חייבים להיות מוחלים גם על ה-observable. עבור Circuits גדולים מאוד, היוריסטיקות שבשימוש באופטימיזציית Circuit לא תמיד מניבות את ה-Circuit הטוב והרדוד ביותר. במקרים כאלה הגיוני להריץ pass managers כאלה מספר פעמים ולהשתמש ב-Circuit הטוב ביותר. נראה זאת מאוחר יותר כשנגדיל את החישוב שלנו.
circuit_ibm = pm.run(full_circuit)
observable_ibm = observable.apply_layout(circuit_ibm.layout)
שלב 3 בתבניות Qiskit: הרצה באמצעות Qiskit Primitives
מעבר על מערך הנתונים בקבוצות ועידנים
קודם כל אנחנו מממשים את האלגוריתם המלא ב אמצעות סימולטור לצורך דיבוג ראשוני ולהערכת שגיאות. עכשיו אפשר לעבור על כל מערך הנתונים בקבוצות במספר עידנים רצוי כדי לאמן את הרשת הנוירונית הקוונטית שלנו.
from qiskit.primitives import StatevectorEstimator as Estimator
batch_size = 140
num_epochs = 1
num_samples = len(train_images)
# Globals
circuit = full_circuit
estimator = Estimator() # simulator for debugging
observables = observable
objective_func_vals = []
iter = 0
# Random initial weights for the ansatz
np.random.seed(42)
weight_params = np.random.rand(len(ansatz.parameters)) * 2 * np.pi
for epoch in range(num_epochs):
for i in range((num_samples - 1) // batch_size + 1):
print(f"Epoch: {epoch}, batch: {i}")
start_i = i * batch_size
end_i = start_i + batch_size
train_images_batch = np.array(train_images[start_i:end_i])
train_labels_batch = np.array(train_labels[start_i:end_i])
input_params = train_images_batch
target = train_labels_batch
iter = 0
res = minimize(
mse_loss_weights, weight_params, method="COBYLA", options={"maxiter": 100}
)
weight_params = res["x"]
Epoch: 0, batch: 0
Iter: 0, loss: 1.0002309063537163
Iter: 50, loss: 0.9434121445008878