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

الدوائر الكمومية التغايرية والشبكات العصبية الكمومية

في هذا الدرس، سنُنفّذ عدة دوائر كمومية تغايرية لمهمة تصنيف البيانات، وتُعرف بالمصنّفات الكمومية التغايرية (VQCs). كان من الشائع في وقت ما أن يُشار إلى مجموعة فرعية من VQCs باسم الشبكات العصبية الكمومية (QNNs) قياساً على الشبكات العصبية الكلاسيكية. وفعلاً، ثمة حالات تؤدي فيها البنى المستعارة من الشبكات العصبية الكلاسيكية، كطبقات الالتفاف (Convolution)، دوراً مهماً في VQCs. في مثل هذه الحالات التي يكون فيها التشابه قوياً، قد يكون وصف QNNs مفيداً. لكن الدوائر الكمومية ذات المعاملات لا تحتاج بالضرورة إلى اتباع البنية العامة للشبكة العصبية؛ فمثلاً، لا يلزم تحميل جميع البيانات في الطبقة الأولى (طبقة الإدخال)؛ إذ يمكن تحميل بعض البيانات في الطبقة الأولى، ثم تطبيق بعض البوابات، ثم تحميل بيانات إضافية (وهي عملية تُعرف بـ"إعادة التحميل"). لذا ينبغي لنا أن ننظر إلى QNNs باعتبارها مجموعة فرعية من الدوائر الكمومية ذات المعاملات، وألا تقيّدنا المقارنة بالشبكات العصبية الكلاسيكية في استكشاف الدوائر الكمومية المفيدة.

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

بنهاية هذا الدرس، ستكون قادراً على:

  • تحميل البيانات من صورة إلى دائرة كمومية
  • بناء 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 صورة إلى مجموعة تدريب ومجموعة اختبار (مع تسمياتها المقابلة). هنا نستخدم 70%70\% من مجموعة البيانات للتدريب، بينما نحتفظ بالـ 30%30\% المتبقية للاختبار.

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]

المصنّف الكمومي التغايري: محاولة أولى

خطوة Qiskit patterns الأولى: تعيين المسألة إلى دائرة كمومية

الهدف هو إيجاد دالة 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}) هي دائرة الترميز، ولدينا خيارات عديدة لها كما رأينا في الدروس السابقة. W(θ)W(\theta) هي كتلة الدائرة التغايرية أو القابلة للتدريب، وθ\theta هي مجموعة المعاملات المراد تدريبها. ستتغير هذه المعاملات بواسطة خوارزميات التحسين الكلاسيكية للعثور على مجموعة المعاملات التي تُعطي أفضل تصنيف للصور بواسطة الدائرة الكمومية. تُسمى هذه الدائرة التغايرية أحياناً بـ"الـansatz". وأخيراً، OO هو مقياس observable ما سيُقدَّر باستخدام primitive المقدِّر (Estimator). لا يوجد قيد يُلزم الطبقات بأن تأتي بهذا الترتيب، أو حتى أن تكون منفصلة تماماً. يمكن أن تتضمن الدائرة طبقات تغايرية و/أو طبقات ترميز متعددة بأي ترتيب مُبرر تقنياً.

نبدأ باختيار خريطة ميزات (feature map) لترميز بياناتنا. سنستخدم z_feature_map، إذ يحافظ على عمق الدوائر منخفضاً مقارنةً ببعض خرائط الميزات الأخرى.

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 عميق بشكل مفرط (خاصة في عمق بوابات الكيوبت الثنائي بعد التحويل) لن يُعطي نتائج جيدة. مسألة ذات صلة هي أن أجهزة الحوسبة الكمومية لها تخطيط معين للكيوبتات، بمعنى أن بعض الكيوبتات الفيزيائية متجاورة على الجهاز الكمومي، وقد تكون أخرى بعيدة جداً عن بعضها. تشابك الكيوبتات المتجاورة لا يزيد العمق كثيراً، لكن تشابك الكيوبتات البعيدة جداً قد يزيد العمق بشكل كبير، إذ يجب إدراج بوابات المبادلة (swap) لنقل المعلومات إلى كيوبتات متجاورة حتى يمكن تشابكها.
  2. المسألة: كلما توفرت لديك معلومات عن مسألتك يمكنها توجيه اختيار الـansatz، استخدمها. على سبيل المثال، البيانات في هذا الدرس مكوّنة من صور لخطوط أفقية وعمودية. يمكن للمرء أن يفكر في الارتباط بين الألوان/القيم المتجاورة الذي يُميّز صورة تحتوي على خط أفقي عن صورة تحتوي على خط عمودي. ما صفات الـansatz التي تتوافق مع هذا الارتباط بين البكسلات المتجاورة؟ سنعود إلى هذه النقطة بشكل أكثر تقنية لاحقاً في هذا الدرس. لكن في الوقت الحالي، لنقل فقط أن تضمين التشابك وبوابات CNOT بين الكيوبتات المقابلة للبكسلات المتجاورة يبدو فكرة جيدة. في الصورة الأشمل، فكر في ما إذا كانت المسألة تُحلّ فعلاً بشكل أفضل باستخدام دائرة كمومية، أو ما إذا كانت ثمة خوارزميات كلاسيكية قادرة على أداء نفس الدور.
  3. عدد المعاملات: كل بوابة كمومية ذات معامل مستقل في الدائرة تزيد من الفضاء المراد تحسينه كلاسيكياً، مما يُبطئ التقارب. لكن مع توسّع المسائل، قد يُصادَف ما يُعرف بـالمستويات القاحلة (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]) ├─────────────────────────────────────
└──────────┘└───────────┘

بعد إعداد دائرة ترميز البيانات والدائرة التغايرية، يمكننا دمجهما لتشكيل الـansatz الكامل. في هذه الحالة، مكونات دائرتنا الكمومية مشابهة تماماً لمكونات الشبكات العصبية، إذ يُشبه 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)

مخرجات خلية الكود السابقة

يجب الآن تعريف observable حتى نتمكن من استخدامه في دالة التكلفة. سنحصل على قيمة التوقع لهذا الـobservable باستخدام Estimator. إذا اخترنا ansatz جيداً مُدفوعاً بطبيعة المسألة، فسيحمل كل كيوبت معلومات ذات صلة بالتصنيف. يمكن إضافة طبقات لدمج المعلومات على عدد أقل من الكيوبتات (تُسمى طبقة الالتفاف)، بحيث تكون القياسات مطلوبة على مجموعة فرعية فقط من الكيوبتات في الدائرة (كما في الشبكات العصبية الالتفافية). أو يمكن قياس خاصية ما من كل كيوبت. هنا سنختار الخيار الأخير، لذا نُدرج عامل Z لكل كيوبت. لا يوجد ما يميّز اختيار ZZ عن غيره، لكنه مُبرر بشكل جيد:

  • هذه مهمة تصنيف ثنائية، وقياس ZZ يمكن أن يُعطي نتيجتين محتملتين.
  • القيم الذاتية لـZZ (±1\pm 1) متباعدة بشكل معقول، وتُعطي نتيجة للمقدِّر في الفترة [-1, +1]، حيث يمكن استخدام 0 ببساطة كقيمة حدية.
  • القياس في أساس Pauli Z مباشر دون الحاجة لأي بوابات إضافية.

إذن، Z خيار طبيعي جداً.

from qiskit.quantum_info import SparsePauliOp

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

لدينا الآن دائرتنا الكمومية والـobservable الذي نريد تقديره. نحتاج الآن إلى بعض الأمور لتشغيل هذه الدائرة وتحسينها. أولاً، نحتاج إلى دالة تُشغّل تمريرة أمامية (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 كمدخل؛ أما المتغيرات الأخرى للتمريرة الأمامية والخسارة فتُضبط كمعاملات عالمية. سيُدرّب المحسِّن النموذج بأخذ أوزان مختلفة ومحاولة تخفيض ناتج دالة التكلفة/الخسارة.

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

هنا نُحسِّن الدائرة للتشغيل على backend حقيقي عبر تحديد optimization_level وإضافة الفصل الديناميكي (dynamical 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 على الدائرة. يجب تطبيق تغييرات التخطيط الناتجة على الـ observable أيضاً. بالنسبة للدوائر الكبيرة جداً، قد لا تُنتج الـ heuristics المستخدمة في تحسين الدوائر أفضل دائرة وأقلها عمقاً في كل مرة. في تلك الحالات، يكون من المنطقي تشغيل هذه الـ pass managers عدة مرات واختيار أفضل دائرة. سنرى هذا لاحقاً عند توسيع نطاق حساباتنا.

circuit_ibm = pm.run(full_circuit)
observable_ibm = observable.apply_layout(circuit_ibm.layout)

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

التكرار على مجموعة البيانات في دفعات وحِقَب

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

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%

دقة التدريب هي 60%60\% فقط، وهذا بالتأكيد ليس جيداً. يصعب تصور أن أداء النموذج على مجموعة الاختبار سيكون أفضل من ذلك. دعنا نتحقق.

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 سيئاً؟ قد يعني هذا أشياء كثيرة. عند العمل على حواسيب كمية حقيقية، سيكون عمق الدائرة اعتباراً رئيسياً. كما أن عدد المعاملات مهم أيضاً، وكذلك التشابك بين الكيوبتات.
  • بدمج النقطتين أعلاه، هل بنينا 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

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

تحسين النموذج

الخطوة الأولى مراجعةً

حين نربط مسألتنا بدائرة كمومية، كان ينبغي لنا أن نفكّر بجدية في كيفية تحديد الفئة من خلال المعلومات الموجودة في البكسلات المتجاورة. لاكتشاف الخطوط الأفقية، نريد أن نعرف "إذا كان البكسل ii أصفر، هل البكسل i+1i+1 أصفر أيضًا؟" لجميع البكسلات عبر كل صف. كما نريد معرفة ما يخص الخطوط الرأسية. لكن بما أن التصنيف ثنائي، يمكن تبسيط الأمر بالقول: إذا لم يُكتَشَف خط أفقي، فهو إذن خط رأسي. احتوت الدائرة التباينية السابقة على بوابات CNOT بين الكيوبتات (وبالتالي البكسلات) 0 و1، و1 و2، و2 و3. هذا يغطي أي خطوط أفقية في الجزء العلوي من الصورة، لكنه لا يكتشف الخطوط الرأسية مباشرةً، ولا يكتشف الخطوط الأفقية بالكامل، إذ يتجاهل الصف السفلي. لاكتشاف جميع الخطوط الأفقية بشكل كامل، نحتاج إلى مجموعة مماثلة من بوابات CNOT بين الكيوبتات (البكسلات) 4 و5، و5 و6، و6 و7. يمكن الأخذ بعين الاعتبار أن إضافة بوابات CNOT بين الكيوبتات المقابلة للخطوط الرأسية (مثل 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

لم نزِد عمق الدائرة. لنتحقق إذا كنا قد حسّنّا قدرتها على نمذجة صورنا.

الخطوة الثانية مراجعةً

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

الخطوة الثالثة مراجعةً

نُطبّق الآن النموذج المحدَّث على بيانات التدريب.

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

الخطوة الرابعة مراجعةً

لنبدأ بالتحقق مما إذا كان المُحسِّن قد تقارَب بالكامل.

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

لا يبدو أن التقارب اكتمل، إذ لم تبقَ دالة الخسارة مستقرة تقريبًا لعدد كافٍ من الخطوات. غير أن دالة الخسارة باتت أقل بنحو 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}

لنبدأ بزيادة حجم صورنا. لا يوجد ما يميز اختيار شبكة 6×6 تحديدًا، سوى أنها تتجاوز عدد الكيوبتات (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

الخطوة الأولى: ربط المسألة بدائرة كمومية

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

هذا عمق معقول للبوابات ثنائية الكيوبت. ينبغي أن نحصل على نتائج عالية الجودة من حاسوب كمومي حقيقي.

# 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، فإن إضافة طبقة الترميز لا تزيد من عمق البوابات ثنائية الكيوبت. يمكننا تمثيل الدائرة الكاملة هنا.

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

Output of the previous code cell

قد تلاحظ أنه إذا كان تقليل عمق البوابات ثنائية الكيوبت أمرًا بالغ الأهمية، يمكننا فعلًا تقليله قليلًا بتغيير ترتيب بوابات CNOT. فمثلًا، يمكن نقل بوابات CNOT على q35q_{35} وq34q_{34} إلى اليسار في مخطط الدائرة أعلاه، ووضعها مباشرةً أسفل بوابات CNOT على q30q_{30} وq31q_{31}. لعمق بوابة ثنائية الكيوبت يبلغ 5، ليس من الواضح أن هذا سيُحدث فرقًا بعد التحويل، لكن من المفيد أخذه بعين الاعتبار. إذا كان ترتيب بوابات CNOT مهمًا لمطابقة المسألة منطقيًا، فالعمق المذكور هنا جيد. أما إذا لم يكن ترتيب بوابات CNOT حاسمًا لنمذجة بنية البيانات في صورنا، فيمكننا كتابة سكريبت لإعادة ترتيب هذه البوابات لتقليل العمق.

نحتاج أيضًا إلى إعادة تعريف المتغيَّر القابل للرصد مع صورنا الأكبر حجمًا:

from qiskit.quantum_info import SparsePauliOp

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

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

نبدأ باختيار نظام خلفي للتنفيذ. في هذه الحالة، سنستخدم النظام الخلفي الأقل انشغالاً.

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

مرةً أخرى، نُعرّف مدير التمريرات مع ضبط مستوى التحسين على 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,
),
]
)

سنُطبّق مدير التمريرات عدة مرات الآن. بالنسبة للدوائر الواسعة جداً أو العميقة جداً، قد يكون هناك تباين كبير في أعماق البتين الكموميتين بعد النقل. لهذا النوع من الدوائر، من المهم تجربة مدير التمريرات مرات عديدة واختيار أفضل نتيجة (أي الأضحل منها).

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

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

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

خطوة أنماط 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 على الأقل دون المساس بالنتائج، وذلك مثلاً بترميز البتات الكمومية ببيانات تقابل مجموعة فرعية من الصفوف والأعمدة الكاملة. ومن ثَمّ، ينبغي تقليص فضاء المعاملات قبل إنفاق مزيد من وقت الحوسبة الكمومية في تدنية دالة الخسارة.

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:

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

  • تحميل بيانات التدريب. يمكن القيام بذلك "يدوياً" أو باستخدام خريطة ميزات جاهزة مثل z_feature_map.
  • بناء ansatz يحتوي على طبقات دوران وتشابك مناسبة لمسألتك.
  • مراقبة عمق الدائرة لضمان جودة النتائج على أجهزة الكمبيوتر الكمومية.

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

  • اختيار نظام خلفي، وغالباً ما يكون الأقل انشغالاً.
  • استخدام مدير التمريرات لنقل كلٍّ من الدائرة والمراقِبات إلى بنية النظام الخلفي المختار.
  • بالنسبة للدوائر العميقة أو الواسعة جداً، أجرِ النقل عدة مرات واختر الدائرة الأضحل.

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

  • إجراء تجارب أولية على المحاكيات لتصحيح الأخطاء وتحسين الـ ansatz.
  • التنفيذ على جهاز كمبيوتر كمومي من IBM®.

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

  • حساب دقة النموذج على بيانات التدريب وبيانات الاختبار.
  • مراقبة تقارب عملية التحسين الكلاسيكية.

المراجع