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

מעגלים וריאציוניים קוונטיים ורשתות עצביות קוונטיות

בשיעור זה נממש מספר מעגלים קוונטיים וריאציוניים למשימת סיווג נתונים, הנקראים מסווגים קוונטיים וריאציוניים (VQCs). בשלב מסוים היה נהוג לכנות תת-קבוצה של VQCs כרשתות עצביות קוונטיות (QNNs), בהקבלה לרשתות עצביות קלאסיות. אכן, ישנם מקרים בהם מבנים שאולים מרשתות עצביות קלאסיות, כמו שכבות קונבולוציה, ממלאים תפקיד חשוב ב-VQCs. במקרים כאלה שבהם ההקבלה חזקה, QNNs עשויים להיות תיאור שימושי. אולם מעגלים קוונטיים מפרמטרים אינם חייבים לעקוב אחרי המבנה הכללי של רשת עצבית; למשל, לא כל הנתונים צריכים להיטען בשכבה הראשונה (כניסה); אפשר לטעון חלק מהנתונים בשכבה הראשונה, להפעיל כמה Gate-ים ואז לטעון נתונים נוספים (תהליך הנקרא "reuploading" של נתונים). לכן עלינו לחשוב על QNNs כתת-קבוצה של מעגלים קוונטיים מפרמטרים, ולא להגביל את עצמנו בחיפוש אחרי מעגלים קוונטיים שימושיים בגלל ההקבלה לרשתות עצביות קלאסיות.

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

בסיום שיעור זה תוכל/י:

  • לטעון נתונים מתמונה לתוך Circuit קוונטי
  • לבנות ansatz עבור VQC (או QNN) ולהתאים אותו לבעיה שלך
  • לאמן את ה-VQC/QNN שלך ולהשתמש בו לביצוע תחזיות מדויקות על נתוני בדיקה
  • להרחיב את הסקאלה של הבעיה ולזהות מגבלות של מחשבים קוונטיים עכשוויים

יצירת נתונים

נתחיל בבניית הנתונים. מערכי נתונים לרוב אינם נוצרים במפורש כחלק ממסגרת Qiskit patterns. אבל סוג הנתונים והכנתם הם קריטיים להצלחת יישום המחשוב הקוונטי ללמידת מכונה. הקוד שלהלן מגדיר מערך נתונים של תמונות עם ממדי פיקסל קבועים. שורה אחת מלאה או עמודה אחת מלאה בתמונה מקבלת את הערך π/2\pi/2, והפיקסלים הנותרים מקבלים ערכים אקראיים בתחום (0,π/4)(0,\pi/4). הערכים האקראיים הם רעש בנתונים שלנו. עיין/י בקוד כדי לוודא שאתה/ת מבין/ה כיצד התמונות נוצרות. בהמשך נגדיל את הסקאלה של התמונות.

# 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 תמונות לסט אימון וסט בדיקה (יחד עם התוויות המתאימות שלהם). כאן אנו משתמשים ב-7070% ממערך הנתונים לאימון, כאשר ה-3030% הנותרים נשמרים לבדיקה.

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)

Output of the previous code cell

כל אחת מהתמונות האלה עדיין מזווגת עם התווית שלה ב-train_labels בצורת רשימה פשוטה:

print(train_labels[:8])
[1, 1, 1, 1, -1, 1, 1, 1]

מסווג קוונטי וריאציוני: ניסיון ראשון

שלב 1 בדפוסי Qiskit: מיפוי הבעיה למעגל קוונטי

המטרה היא למצוא פונקציה ff עם פרמטרים θ\theta שממפה וקטור נתונים / תמונה x\vec{x} לקטגוריה הנכונה: fθ(x)±1f_\theta(\vec{x}) \rightarrow \pm1. זה יושג באמצעות VQC עם כמה שכבות שניתן לזהות לפי מטרותיהן הייחודיות:

fθ(x)=0U(x)W(θ)OW(θ)U(x)0f_\theta(\vec{x}) = \langle 0|U^{\dagger}(\vec{x})W^\dagger(\theta)OW(\theta)U(\vec{x})|0\rangle

כאן, U(x)U(\vec{x}) הוא ה-Circuit של הקידוד, שלגביו יש לנו אפשרויות רבות כפי שנראה בשיעורים קודמים. W(θ)W(\theta) הוא בלוק Circuit וריאציוני, או ניתן לאימון, ו-θ\theta הוא אוסף הפרמטרים שיש לאמן. פרמטרים אלה ישתנו על ידי אלגוריתמי אופטימיזציה קלאסיים כדי למצוא את קבוצת הפרמטרים שמניבה את הסיווג הטוב ביותר של תמונות על ידי ה-Circuit הקוונטי. ה-Circuit הוריאציוני הזה נקרא לפעמים "ansatz". לבסוף, OO הוא אובייקטיב כלשהו שיוערך באמצעות ה-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. תיאור מלא חורג מהיקף מבוא זה; כאן אנו רק מצביעים על כמה קטגוריות של שיקולים.

  1. חומרה: כל המחשבים הקוונטיים המודרניים נוטים יותר לשגיאות ורגישים יותר לרעש מאשר עמיתיהם הקלאסיים. שימוש ב-ansatz עמוק מדי (בעיקר מבחינת עומק Transpiled של שני Qubit-ים) לא ייתן תוצאות טובות. בעיה קשורה היא שלמחשבים קוונטיים יש פריסה מסוימת של Qubit-ים, כלומר חלק מה-Qubit-ים הפיזיים הם שכנים במחשב הקוונטי, ואחרים עשויים להיות רחוקים מאוד זה מזה. שזירת Qubit-ים סמוכים לא מגדילה את העומק יותר מדי, אבל שזירת Qubit-ים רחוקים מאוד יכולה להגדיל את העומק באופן משמעותי, מכיוון שעלינו להכניס Gate-י swap כדי להעביר מידע ל-Qubit-ים שסמוכים זה לזה כדי שיוכלו להשתזר.
  2. הבעיה: בכל פעם שיש לך מידע על הבעיה שלך שיכול להנחות את ה-ansatz שלך, השתמש/י בו. למשל, הנתונים בשיעור זה מורכבים מתמונות של קווים אופקיים ואנכיים. אפשר לשקול איזה מתאם בין צבעים/ערכים סמוכים מזהה תמונה של קו אופקי או אנכי. אילו תכונות של ansatz יתאימו למתאם הזה בין פיקסלים סמוכים? נחזור לנקודה זו מבחינה יותר טכנית בהמשך השיעור. אבל לעת עתה, נאמר פשוט שהכללת שזירה ו-Gate-י CNOT בין Qubit-ים המתאימים לפיקסלים סמוכים נראית כרעיון טוב. בתמונה הגדולה, שקול/י האם הבעיה פתורה טוב יותר באמצעות Circuit קוונטי, או האם קיימים אלגוריתמים קלאסיים שיכולים לעשות עבודה טובה באותה מידה.
  3. מספר הפרמטרים: כל 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 הקוונטי שלנו דומים למדי לאלה שברשתות עצביות, כאשר U(x)U(\vec{x}) דומה ביותר לשכבה שטוענת ערכי קלט מהתמונה, ו-W(θ)W(\theta) דומה לשכבה של "משקולות" משתנות. מאחר שהקבלה זו נכונה במקרה זה, אנו מאמצים "qnn" בחלק ממוסכמות השמות שלנו; אבל הקבלה זו לא צריכה להגביל את חקירתך של VQCs.

QML_CR_background_QNN_circuit-2.png

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

Output of the previous code cell

כעת עלינו להגדיר אובייקטיב, כדי שנוכל להשתמש בו בפונקציית העלות שלנו. נקבל ערך ציפייה לאובייקטיב זה באמצעות Estimator. אם בחרנו ansatz טוב שמונחה על ידי הבעיה, אז כל Qubit יכיל מידע רלוונטי לסיווג. אפשר להוסיף שכבות לשילוב המידע על Qubit-ים מעטים יותר (הנקראת שכבה קונבולוציונית), כך שמדידות נדרשות רק על תת-קבוצה של ה-Qubit-ים ב-Circuit (כמו ברשתות עצביות קונבולוציוניות). או שאפשר למדוד מאפיין כלשהו מכל Qubit. כאן נבחר באפשרות השנייה, ולכן נכלול אופרטור Z לכל Qubit. אין שום ייחוד בבחירת ZZ, אבל היא מוצדקת היטב:

  • זו משימת סיווג בינארי, ומדידה של ZZ יכולה להניב שני תוצאות אפשריות.
  • ערכי העצמי של ZZ (±1\pm 1) מופרדים באופן סביר, ומביאים לתוצאת 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

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

בדיקה ודיוק

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

import copy
from sklearn.metrics import accuracy_score
from qiskit.primitives import StatevectorEstimator as Estimator # simulator
# from qiskit_ibm_runtime import EstimatorV2 as Estimator # real quantum computer

estimator = Estimator()
# estimator = Estimator(backend=backend)

pred_train = forward(circuit, np.array(train_images), res["x"], estimator, observable)
# pred_train = forward(circuit_ibm, np.array(train_images), res['x'], estimator, observable_ibm)

print(pred_train)

pred_train_labels = copy.deepcopy(pred_train)
pred_train_labels[pred_train_labels >= 0] = 1
pred_train_labels[pred_train_labels < 0] = -1
print(pred_train_labels)
print(train_labels)

accuracy = accuracy_score(train_labels, pred_train_labels)
print(f"Train accuracy: {accuracy * 100}%")
[-2.27688499e-02 -1.46227204e-02 -1.73927452e-02  9.93331786e-02
-4.85553548e-01 1.43558565e-01 8.34567054e-02 -1.40133992e-02
1.52169596e-01 -1.95082515e-01 8.24373578e-03 -9.90696638e-02
-3.54268344e-02 -4.77017954e-01 1.38713848e-02 -2.99706215e-01
-5.78378029e-02 3.25528779e-02 -4.11354239e-02 -1.06483708e-01
1.53095800e-01 2.90110884e-02 1.25745450e-02 6.46323079e-02
-1.53538943e-01 -1.57694952e-02 -1.67800067e-02 -1.99820822e-01
1.70360075e-01 7.86148038e-03 -2.33373818e-02 6.64233020e-02
-1.14895445e-01 -1.11296215e-01 1.15120303e-01 -2.94096140e-01
-1.00531392e-03 -1.69209726e-01 -1.26120885e-01 3.26298176e-02
-1.33517383e-02 -5.86983444e-02 -4.32341361e-01 -4.36509551e-01
-4.17940102e-02 1.76935235e-03 8.14479984e-03 1.86985655e-01
-2.75525019e-01 -1.63229907e-03 -1.08571055e-01 -7.37452387e-04
-6.44440657e-02 6.72812834e-04 2.16785530e-03 1.41381850e-01
-9.82570410e-02 4.35973325e-01 -7.62261965e-02 -1.86193980e-01
-1.56971183e-02 -4.02757541e-01 -1.53869367e-01 2.29262129e-02
-7.02788246e-03 3.65719683e-02 4.68232163e-01 2.36434668e-02
-2.59520939e-02 3.70550137e-01 -1.19630110e-01 -5.79555318e-02
2.09554455e-01 5.04689780e-02 7.39494314e-02 -1.77647326e-02
-1.45407207e-01 -9.54908878e-02 7.56029640e-02 -2.74049696e-02
3.34885873e-01 1.58546171e-03 1.09339091e-01 -8.84693274e-02
-2.36450457e-02 1.41892239e-01 -2.34453218e-01 -7.50717757e-02
-1.13281310e-01 -1.66649414e-01 -3.17224197e-01 -6.38220597e-02
3.28916563e-02 3.04739203e-02 2.67720196e-02 -1.16485785e-01
-3.08115732e-02 -2.95372010e-02 -7.54669023e-02 6.20013872e-02
-3.85258710e-01 -1.16456443e-01 -7.38548075e-02 -3.20558243e-02
-4.22284741e-02 1.01285659e-01 -1.76949246e-01 -2.02767491e-01
-1.12407344e-01 -3.81408267e-02 -4.33345231e-01 -9.24507501e-02
-4.21765393e-02 -6.06533771e-02 -2.22257783e-01 -1.17312535e-01
-6.74132262e-02 -2.76206274e-01 -9.13971800e-02 -2.27653991e-01
1.66358563e-01 2.17230774e-04 5.76426304e-02 -2.82079169e-02
-1.15482051e-01 -3.46716009e-01 -3.21448755e-01 -5.20041405e-02
-2.16833625e-01 -1.06154654e-02 -7.74854811e-02 -3.28257935e-01
-7.83242410e-02 1.65547682e-01 -2.55294862e-01 -8.89085025e-02
4.47581491e-01 1.92351832e-02 2.74083885e-02 -3.61304571e-01]
[-1. -1. -1. 1. -1. 1. 1. -1. 1. -1. 1. -1. -1. -1. 1. -1. -1. 1.
-1. -1. 1. 1. 1. 1. -1. -1. -1. -1. 1. 1. -1. 1. -1. -1. 1. -1.
-1. -1. -1. 1. -1. -1. -1. -1. -1. 1. 1. 1. -1. -1. -1. -1. -1. 1.
1. 1. -1. 1. -1. -1. -1. -1. -1. 1. -1. 1. 1. 1. -1. 1. -1. -1.
1. 1. 1. -1. -1. -1. 1. -1. 1. 1. 1. -1. -1. 1. -1. -1. -1. -1.
-1. -1. 1. 1. 1. -1. -1. -1. -1. 1. -1. -1. -1. -1. -1. 1. -1. -1.
-1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. -1. 1. 1. 1. -1. -1. -1.
-1. -1. -1. -1. -1. -1. -1. 1. -1. -1. 1. 1. 1. -1.]
[1, 1, 1, 1, -1, 1, 1, 1, -1, 1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1, 1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1, 1, -1, 1, 1, -1, 1, 1, -1, -1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, -1, 1, 1, -1, 1, 1, 1, 1, -1, -1, -1, -1, 1, -1, 1, 1, 1, -1, -1, 1, 1, -1, 1, 1, -1, -1, 1, -1, 1, 1, 1, 1, 1, -1, -1, 1, -1, -1, -1, -1, -1, 1, 1, -1, -1, 1, -1, -1, 1, 1, -1, 1, -1, 1, 1, 1, 1, 1, -1, -1, -1, 1, -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, 1, 1, -1, -1, -1, 1, 1, 1, -1, -1, -1, 1, -1, 1, 1, -1, -1, -1]
Train accuracy: 60.0%

דיוק האימון הוא רק 6060%, מה שבהחלט לא טוב. קשה לדמיין שביצועי המודל על מערך הבדיקה יהיו טובים יותר. בואו נבדוק זאת.

pred_test = forward(circuit, np.array(test_images), res["x"], estimator, observable)
# pred_test = forward(circuit_ibm, np.array(test_images), res['x'], estimator, observable_ibm)

print(pred_test)

pred_test_labels = copy.deepcopy(pred_test)
pred_test_labels[pred_test_labels >= 0] = 1
pred_test_labels[pred_test_labels < 0] = -1
print(pred_test_labels)
print(test_labels)

accuracy = accuracy_score(test_labels, pred_test_labels)
print(f"Test accuracy: {accuracy * 100}%")
[-2.77978120e-01 -2.62194862e-01  4.59636095e-02 -8.09344165e-02
-2.97362966e-01 9.22947242e-02 2.06693174e-01 3.31629460e-02
1.10971762e-03 -2.14602152e-01 -1.62671993e-01 -6.07179155e-04
-1.59948633e-01 -8.55722523e-02 -1.13057027e-01 -3.00187433e-01
-2.92832827e-01 7.38580629e-02 -6.03706270e-02 -8.57643552e-02
-1.52402062e-02 -3.57505447e-01 -3.54890597e-02 1.36534749e-01
-1.54688180e-01 -2.93714726e-01 1.89548513e-02 -6.15715564e-02
1.11042670e-01 -2.22861100e-02 -3.84230105e-02 1.67351034e-01
-8.38766333e-02 2.56348613e-01 -1.10653111e-01 -1.18989476e-01
-6.75723266e-05 -6.88580547e-02 1.02431393e-02 -2.42125353e-01
-1.09142367e-01 -1.22540757e-01 -1.63735850e-01 3.93334838e-01
2.36705685e-01 -2.34259814e-02 -3.91877756e-02 -1.95106746e-01
1.86707523e-01 4.74775215e-02 -4.24907432e-02 -2.06453265e-01
4.09184710e-02 -3.54762080e-02 -9.47513112e-02 2.97270112e-01
-2.99708696e-02 9.93941064e-03 -1.26760302e-01 -1.36183355e-01]
[-1. -1. 1. -1. -1. 1. 1. 1. 1. -1. -1. -1. -1. -1. -1. -1. -1. 1.
-1. -1. -1. -1. -1. 1. -1. -1. 1. -1. 1. -1. -1. 1. -1. 1. -1. -1.
-1. -1. 1. -1. -1. -1. -1. 1. 1. -1. -1. -1. 1. 1. -1. -1. 1. -1.
-1. 1. -1. 1. -1. -1.]
[-1, -1, 1, 1, -1, -1, 1, -1, 1, -1, 1, 1, 1, 1, -1, 1, -1, 1, 1, 1, 1, -1, -1, 1, 1, -1, 1, 1, 1, -1, 1, -1, -1, 1, 1, 1, -1, 1, 1, -1, -1, -1, 1, 1, 1, -1, 1, 1, 1, 1, -1, -1, 1, 1, 1, 1, 1, 1, -1, -1]
Test accuracy: 60.0%

המודל לא מסווג את הנתונים האלה טוב. כדאי לשאול למה, ובפרט לבדוק:

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

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

obj_func_vals_first = objective_func_vals
# import matplotlib.pyplot as plt

plt.figure(figsize=(12, 6))
plt.plot(obj_func_vals_first, label="first ansatz")
plt.xlabel("iteration")
plt.ylabel("loss")
plt.legend()
plt.show()

Output of the previous code cell

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

missed = []
for i in range(len(test_labels)):
if pred_test_labels[i] != test_labels[i]:
missed.append(test_images[i])
print(len(missed))
24
fig, ax = plt.subplots(12, 2, figsize=(6, 6), subplot_kw={"xticks": [], "yticks": []})
for i in range(len(missed)):
ax[i // 2, i % 2].imshow(
missed[i].reshape(vert_size, hor_size),
aspect="equal",
)
plt.subplots_adjust(wspace=0.02, hspace=0.025)

Output of the previous code cell

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

שיפור המודל

שלב 1 — סקירה מחדש

כאשר מיפינו את הבעיה שלנו למעגל קוונטי, היה עלינו לחשוב במפורש על האופן שבו המידע שבפיקסלים סמוכים קובע את הקטגוריה. כדי לזהות קווים אופקיים, אנחנו רוצים לדעת "אם פיקסל ii הוא צהוב, האם פיקסל i+1i+1 גם צהוב" — וזאת עבור כל הפיקסלים לאורך כל שורה. אנחנו גם רוצים לדעת לגבי קווים אנכיים. אבל מכיוון שהסיווג הוא בינארי, אפשר לדמיין פשוט לומר: אם לא מזוהה קו אופקי כזה, אז מדובר בקו אנכי. המעגל הוריאציוני הקודם שלנו כלל שערי CNOT בין Qubitים (ולכן פיקסלים) 0 ו-1, 1 ו-2, ו-2 ו-3. זה מכסה קווים אופקיים לאורך שורת הפיקסלים העליונה בתמונה, אך אינו מזהה ישירות קווים אנכיים, ואינו מזהה באופן מלא קווים אופקיים, שכן הוא מתעלם מהשורה התחתונה. כדי לזהות את כל הקווים האופקיים במלואם, נרצה סט דומה של שערי CNOT בין Qubitים (פיקסלים) 4 ו-5, 5 ו-6, ו-6 ו-7. כדאי לזכור שהוספת שערי CNOT בין Qubitים המתאימים לקווים אנכיים (כמו 0 ו-4, או 2 ו-6) עשויה להיות שימושית גם כן. אבל נבדוק קודם אם מספיק לזהות שיש או אין קו אופקי.

# 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 an extended list of qubit pairs between which we want CNOT gates. This now covers all pixels connected by horizontal lines.
qnn_cnot_list = [[0, 1], [1, 2], [2, 3], [4, 5], [5, 6], [6, 7]]

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)}"
)

# Combine the feature map and variational circuit
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)
5
2+ qubit depth: 3

Output of the previous code cell

לא הגדלנו את עומק המעגל. בואו נראה אם הגדלנו את יכולתו למדל את התמונות שלנו.

שלב 2 — סקירה מחדש

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

שלב 3 — סקירה מחדש

עכשיו נחיל את המודל המעודכן על נתוני האימון שלנו.

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.0049762969140237
Iter: 50, loss: 0.8274276543780351

שלב 4 — סקירה מחדש

נתחיל בבדיקה אם האופטימייזר התכנס במלואו.

obj_func_vals_revised = objective_func_vals
# import matplotlib.pyplot as plt

plt.figure(figsize=(12, 6))
plt.plot(obj_func_vals_revised, label="revised ansatz")
plt.xlabel("iteration")
plt.ylabel("loss")
plt.legend()
plt.show()

Output of the previous code cell

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

from sklearn.metrics import accuracy_score
from qiskit.primitives import StatevectorEstimator as Estimator # simulator
# from qiskit_ibm_runtime import EstimatorV2 as Estimator # real quantum computer

estimator = Estimator()
# estimator = Estimator(backend=backend)

pred_train = forward(circuit, np.array(train_images), res["x"], estimator, observable)
# pred_train = forward(circuit_ibm, np.array(train_images), res['x'], estimator, observable_ibm)

print(pred_train)

pred_train_labels = copy.deepcopy(pred_train)
pred_train_labels[pred_train_labels >= 0] = 1
pred_train_labels[pred_train_labels < 0] = -1
print(pred_train_labels)
print(train_labels)

accuracy = accuracy_score(train_labels, pred_train_labels)
print(f"Train accuracy: {accuracy * 100}%")
[ 0.46144755  0.42579688  0.35255977  0.55207273 -0.48578418  0.50805845
0.44892649 0.6173847 -0.62428139 0.40405121 0.46862421 0.29503395
-0.5740469 -0.71794562 -0.45022095 -0.45330418 -0.19795258 -0.46821777
-0.5622049 -0.32114059 0.54947838 -0.4889812 0.28327445 0.58149728
-0.27026749 0.41328304 0.21119412 0.60108606 0.39204178 -0.24974605
0.38496469 0.39867586 -0.38946996 0.62616766 0.61212525 -0.49719567
0.30860002 0.68443904 -0.27505907 -0.41508947 -0.49666422 0.67716994
-0.54696613 -0.70058779 0.42711815 -0.5285338 0.37678572 0.43888249
-0.30844464 0.42347715 -0.4250844 0.67324132 0.59914067 -0.45184567
0.13604098 0.65336342 0.26099853 0.60316559 -0.38743183 -0.54784284
-0.29549031 -0.45592302 0.41613453 -0.38781528 0.56903087 0.54955451
0.55532336 -0.3931852 -0.57599675 0.61246236 0.42014135 -0.38171749
0.56760389 0.45383135 -0.50473943 -0.47551181 0.54221517 -0.64987023
0.28845851 0.54403865 0.53841148 0.64477078 0.71912049 -0.63178323
-0.50764757 0.50304637 -0.38099972 -0.27707127 -0.24353841 -0.52045267
-0.61500665 0.65443173 0.31902266 -0.64969037 -0.4814051 0.47980608
-0.649786 -0.43048551 0.34562588 0.308998 -0.32454238 0.29558168
-0.45410187 0.54600712 0.33204827 0.22627804 0.4283921 0.56191874
-0.25400294 -0.6493613 -0.47445293 0.42272138 -0.35472546 -0.52240474
-0.45207595 0.40292125 -0.3361856 -0.46620886 0.60202719 -0.56505744
0.47169796 -0.43577622 0.40689437 0.48869108 -0.39701189 -0.57698634
-0.39236332 0.31294648 0.41797597 0.63004836 -0.52884541 -0.43805812
-0.3193499 0.36860211 -0.49190995 0.65000193 0.50260077 -0.56737168
-0.29693083 -0.40956432]
[ 1. 1. 1. 1. -1. 1. 1. 1. -1. 1. 1. 1. -1. -1. -1. -1. -1. -1.
-1. -1. 1. -1. 1. 1. -1. 1. 1. 1. 1. -1. 1. 1. -1. 1. 1. -1.
1. 1. -1. -1. -1. 1. -1. -1. 1. -1. 1. 1. -1. 1. -1. 1. 1. -1.
1. 1. 1. 1. -1. -1. -1. -1. 1. -1. 1. 1. 1. -1. -1. 1. 1. -1.
1. 1. -1. -1. 1. -1. 1. 1. 1. 1. 1. -1. -1. 1. -1. -1. -1. -1.
-1. 1. 1. -1. -1. 1. -1. -1. 1. 1. -1. 1. -1. 1. 1. 1. 1. 1.
-1. -1. -1. 1. -1. -1. -1. 1. -1. -1. 1. -1. 1. -1. 1. 1. -1. -1.
-1. 1. 1. 1. -1. -1. -1. 1. -1. 1. 1. -1. -1. -1.]
[1, 1, 1, 1, -1, 1, 1, 1, -1, 1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1, 1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1, 1, -1, 1, 1, -1, 1, 1, -1, -1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, -1, 1, 1, -1, 1, 1, 1, 1, -1, -1, -1, -1, 1, -1, 1, 1, 1, -1, -1, 1, 1, -1, 1, 1, -1, -1, 1, -1, 1, 1, 1, 1, 1, -1, -1, 1, -1, -1, -1, -1, -1, 1, 1, -1, -1, 1, -1, -1, 1, 1, -1, 1, -1, 1, 1, 1, 1, 1, -1, -1, -1, 1, -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, 1, 1, -1, -1, -1, 1, 1, 1, -1, -1, -1, 1, -1, 1, 1, -1, -1, -1]
Train accuracy: 100.0%
pred_test = forward(circuit, np.array(test_images), res["x"], estimator, observable)
# pred_test = forward(circuit_ibm, np.array(test_images), res['x'], estimator, observable_ibm)

print(pred_test)

pred_test_labels = copy.deepcopy(pred_test)
pred_test_labels[pred_test_labels >= 0] = 1
pred_test_labels[pred_test_labels < 0] = -1
print(pred_test_labels)
print(test_labels)

accuracy = accuracy_score(test_labels, pred_test_labels)
print(f"Test accuracy: {accuracy * 100}%")
[-0.48396136 -0.57123828  0.28373249  0.38983869 -0.45799092 -0.63643031
0.69164877 -0.47749808 0.16965244 -0.39669469 0.39366915 0.44206948
0.69733951 0.40445979 -0.33663432 0.54511581 -0.49397081 0.55934553
0.69269512 0.38875983 0.39724004 -0.49635863 -0.19131387 0.38813936
0.39537369 -0.46262489 0.5307315 0.21783317 0.31949453 -0.49772087
0.56409526 -0.66254365 -0.57507262 0.37363552 0.35154205 0.69295687
-0.31205475 0.37787066 0.67903997 -0.29984861 -0.46435535 -0.32610974
0.4327188 0.64626537 0.37592731 -0.14328906 0.59694745 0.71880638
0.32414334 0.42119333 -0.60745236 -0.42520033 0.28334222 0.21699081
0.34837252 0.31538989 0.30754545 0.5995197 -0.34678026 -0.46587602]
[-1. -1. 1. 1. -1. -1. 1. -1. 1. -1. 1. 1. 1. 1. -1. 1. -1. 1.
1. 1. 1. -1. -1. 1. 1. -1. 1. 1. 1. -1. 1. -1. -1. 1. 1. 1.
-1. 1. 1. -1. -1. -1. 1. 1. 1. -1. 1. 1. 1. 1. -1. -1. 1. 1.
1. 1. 1. 1. -1. -1.]
[-1, -1, 1, 1, -1, -1, 1, -1, 1, -1, 1, 1, 1, 1, -1, 1, -1, 1, 1, 1, 1, -1, -1, 1, 1, -1, 1, 1, 1, -1, 1, -1, -1, 1, 1, 1, -1, 1, 1, -1, -1, -1, 1, 1, 1, -1, 1, 1, 1, 1, -1, -1, 1, 1, 1, 1, 1, 1, -1, -1]
Test accuracy: 100.0%
```bash

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

## סקאלביליות והרצה על מחשבים קוונטיים אמיתיים \{#scaling-and-running-on-real-quantum-computers}

### נתונים \{#data}

בואו נתחיל בהגדלת גודל התמונות שלנו. אין שום דבר מיוחד בבחירת רשת 6x6, פרט לכך שהיא עולה על מספר ה-Qubitים (32) שאפשר לסמלץ עבור מעגלים עם שערים שאינם Clifford.

```python
# This code defines the images to be classified:

import numpy as np

# Total number of "pixels"/qubits
size = 36
# One dimension of the image (called vertical, but it doesn't matter). Must be a divisor of `size`
vert_size = 6
# 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 = 6

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]))
# Randomly select one of the several rows you made above.
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 one of the several rows you made above.

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

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

from sklearn.model_selection import train_test_split

np.random.seed(42)
# Here we specify a very small data set. Increase for realism, but monitor use of quantum computing time.
images, labels = generate_dataset(10)

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

# Generate a figure with nested images using subplots.

fig, ax = plt.subplots(2, 2, figsize=(10, 6), subplot_kw={"xticks": [], "yticks": []})
for i in range(4):
ax[i // 2, i % 2].imshow(
train_images[i].reshape(vert_size, hor_size),
aspect="equal",
)
plt.subplots_adjust(wspace=0.1, hspace=0.025)

Output of the previous code cell

שלב 1: מיפוי הבעיה למעגל קוונטי

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")
# This creates a circuit with the cxs in the compressed order.

from qiskit import QuantumCircuit
from qiskit.circuit import ParameterVector

qnn_circuit = QuantumCircuit(size)
params = ParameterVector("θ", length=2 * size)
for i in range(size):
qnn_circuit.ry(params[i], i)

# CNOT gates between horizontally adjacent qubits.
for i in range(vert_size):
for j in range(hor_size):
if j < hor_size - 1:
qnn_circuit.cx((i * hor_size) + j, (i * hor_size) + j + 1)

# CNOT gates between vertically adjacent qubits, likely not necessary based on our preliminary simulation.
# if i<vert_size-1:
# qnn_circuit.cx((i*hor_size)+j,(i*hor_size)+j+hor_size)
for i in range(size):
qnn_circuit.rx(params[size + i], i)
qnn_circuit_large = qnn_circuit

print(qnn_circuit_large.decompose().depth())
print(
f"2+ qubit depth: {qnn_circuit_large.decompose().depth(lambda instr: len(instr.qubits) > 1)}"
)
# qnn_circuit_large.draw()
7
2+ qubit depth: 5

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

# Combine the feature map and variational circuit
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)

# Check the depth of the full circuit
print(full_circuit.decompose().depth())
print(
f"2+ qubit depth: {full_circuit.decompose().depth(lambda instr: len(instr.qubits) > 1)}"
)
11
2+ qubit depth: 5

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

full_circuit.decompose().draw("mpl", style="clifford", idle_wires=False, fold=-1)

Output of the previous code cell

אפשר לשים לב שאם מזעור עומק ה-Qubit הכפול היה בעדיפות עליונה, ניתן היה להפחית אותו מעט על ידי שינוי סדר שערי ה-CNOT. לדוגמה, שערי ה-CNOT על q35q_{35} ו-q34q_{34} ניתן היה להזיז שמאלה בתרשים המעגל לעיל, ולמקם אותם ישירות מתחת לשערי ה-CNOT על q30q_{30} ו-q31q_{31}, לדוגמה. לעומק דו-Qubit של 5, לא ברור שזה יעשה הבדל לאחר הטרנספילציה, אבל כדאי לזכור זאת. אם סדר שערי ה-CNOT חשוב לצורך התאמה לוגית לבעיה הנידונה, העומק כאן בסדר. אם סדר ה-CNOT אינו קריטי למידול מבנה הנתונים בתמונות שלנו, נוכל לכתוב סקריפט שיסדר מחדש את שערי ה-CNOT האלה כדי למזער את העומק.

אנחנו גם צריכים להגדיר מחדש את ה-observable שלנו עבור התמונות הגדולות יותר:

from qiskit.quantum_info import SparsePauliOp

observable = SparsePauliOp.from_list([("Z" * (num_qubits), 1)])

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

נתחיל בבחירת Backend להרצה. במקרה זה נשתמש ב-Backend הפנוי ביותר.

from qiskit_ibm_runtime import QiskitRuntimeService

# To run on hardware, select the least busy quantum computer or specify a particular one.
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)
# backend = service.backend("ibm_brisbaneane")

print(backend.name)
ibm_brisbane

שוב, אנחנו מגדירים pass manager עם רמת אופטימיזציה 3.

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

# Try pass manager several times, since heuristics can return various transpilations on large circuits, and we want the shallowest.

transpiled_qcs = []
transpiled_depths = []
transpiled_2q_depths = []
for i in range(1, 10):
circuit_ibm = pm.run(full_circuit)
transpiled_qcs.append(circuit_ibm)
transpiled_depths.append(circuit_ibm.decompose().depth())
transpiled_2q_depths.append(
circuit_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)
)
# print(i)

print(transpiled_depths)
print(transpiled_2q_depths)

# Use the shallowest

minpos = transpiled_2q_depths.index(min(transpiled_2q_depths))
[85, 85, 81, 89, 81, 81, 89, 85, 85]
[10, 10, 10, 10, 10, 10, 10, 10, 10]

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

circuit_ibm = transpiled_qcs[2]
observable_ibm = observable.apply_layout(circuit_ibm.layout)
print(circuit_ibm.decompose().depth())
print(
f"2+ qubit depth: {circuit_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)}"
)
81
2+ qubit depth: 10

שלב 3 בתבניות Qiskit: הרצה באמצעות Qiskit Primitives

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

# This was run on an Eagle r3 processor on 10-4-24, and took 7 min.

from qiskit_ibm_runtime import EstimatorV2 as Estimator, Session

batch_size = 7
num_epochs = 1
num_samples = len(train_images)

# Globals
circuit = circuit_ibm
observable = observable_ibm
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
# Or re-load weights from a previous calculation
weight_params = np.array(
[
3.35330497,
5.97351416,
4.59925358,
3.76148219,
0.98029403,
0.98014248,
0.3649501,
6.44234523,
3.77691701,
4.44895122,
0.12933619,
6.09412333,
5.23039137,
1.33416598,
1.14243996,
1.15236452,
1.91161039,
3.2971419,
3.71399059,
1.82984665,
3.84438512,
0.87646578,
1.83559896,
2.30191935,
2.86557222,
4.93340606,
1.25458737,
3.23103027,
3.72225051,
0.29185655,
3.81731689,
1.07143467,
0.40873121,
5.96202367,
6.067245,
5.07931034,
1.91394476,
0.61369199,
4.2991629,
2.76555968,
0.76678884,
3.11128829,
0.21606945,
5.71342859,
1.62596258,
4.16275028,
1.95853845,
3.26768375,
3.43508199,
1.1614748,
6.09207989,
4.87030317,
5.90304595,
5.62236606,
3.75671636,
5.79230665,
0.55601479,
1.23139664,
0.28417144,
2.04411075,
2.44213144,
1.70493625,
5.20711134,
2.24154726,
1.76516358,
3.40986006,
0.88545302,
5.04035228,
0.46841551,
6.2007935,
4.85215699,
1.24856745,
]
)

# Running in a session avoids repeated queuing. This is available to Premium Plan, Flex Plan, and On-Prem (IBM Quantum Platform API) Plan users.

with Session(backend=backend) as session:
estimator = Estimator(mode=session, options={"resilience_level": 1})

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
# We can increase maxiter to do a full optimization.
res = minimize(
mse_loss_weights,
weight_params,
method="COBYLA",
options={"maxiter": 20},
)
weight_params = res["x"]
session.close()

# Open users can carry out the same calculation using a batch, but repeated queuing is possible.
# from qiskit_ibm_runtime import Batch

# with Batch(backend=backend) as batch:
# estimator = Estimator(
# mode=batch, options={"resilience_level": 1}
# )
#
# 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
# # We can increase maxiter to do a full optimization.
# res = minimize(
# mse_loss_weights,
# weight_params,
# method="COBYLA",
# options={"maxiter": 20},
# )
# weight_params = res["x"]
# batch.close()

מומלץ לשמור את פרמטרי המשקל שהוחזרו מחישוב זה, למקרה שתרצו להמשיך לאטר.

weight_params
array([3.35330497, 6.97351416, 5.59925358, 3.76148219, 0.98029403,
0.98014248, 0.3649501 , 6.44234523, 3.77691701, 4.44895122,
1.12933619, 7.09412333, 5.23039137, 1.33416598, 1.14243996,
1.15236452, 1.91161039, 3.2971419 , 3.71399059, 1.82984665,
3.84438512, 0.87646578, 1.83559896, 2.30191935, 2.86557222,
4.93340606, 1.25458737, 3.23103027, 3.72225051, 0.29185655,
3.81731689, 1.07143467, 0.40873121, 5.96202367, 6.067245 ,
5.07931034, 1.91394476, 0.61369199, 4.2991629 , 2.76555968,
0.76678884, 3.11128829, 0.21606945, 5.71342859, 1.62596258,
4.16275028, 1.95853845, 3.26768375, 3.43508199, 1.1614748 ,
6.09207989, 4.87030317, 5.90304595, 5.62236606, 3.75671636,
5.79230665, 0.55601479, 1.23139664, 0.28417144, 2.04411075,
2.44213144, 1.70493625, 5.20711134, 2.24154726, 1.76516358,
3.40986006, 0.88545302, 5.04035228, 0.46841551, 6.2007935 ,
4.85215699, 1.24856745])

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

obj_func_vals_qc = objective_func_vals
# import matplotlib.pyplot as plt

plt.figure(figsize=(12, 6))
plt.plot(obj_func_vals_qc, label="revised ansatz")
plt.xlabel("iteration")
plt.ylabel("loss")
plt.legend()
plt.show()

Output of the previous code cell

סיכום

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

שלב 1: מיפוי הבעיה למעגל קוונטי

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

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

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

שלב 3: הרצה באמצעות Qiskit (Runtime) Primitives

  • ביצוע ניסויים ראשוניים על סימולטורים לניפוי באגים ואופטימיזציה של ה-ansatz.
  • הרצה על מחשב קוונטי של IBM®.

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

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

מקורות