الدوائر الكمومية التغايرية والشبكات العصبية الكمومية
في هذا الدرس، سنُنفّذ عدة دوائر كمومية تغايرية لمهمة تصنيف البيانات، وتُعرف بالمصنّفات الكمومية التغايرية (VQCs). كان من الشائع في وقت ما أن يُشار إلى مجموعة فرعية من VQCs باسم الشبكات العصبية الكمومية (QNNs) قياساً على الشبكات العصبية الكلاسيكية. وفعلاً، ثمة حالات تؤدي فيها البنى المستعارة من الشبكات العصبية الكلاسيكية، كطبقات الالتفاف (Convolution)، دوراً مهماً في VQCs. في مثل هذه الحالات التي يكون فيها التشابه قوياً، قد يكون وصف QNNs مفيداً. لكن الدوائر الكمومية ذات المعاملات لا تحتاج بالضرورة إلى اتباع البنية العامة للشبكة العصبية؛ فمثلاً، لا يلزم تحميل جميع البيانات في الطبقة الأولى (طبقة الإدخال)؛ إذ يمكن تحميل بعض البيانات في الطبقة الأولى، ثم تطبيق بعض البوابات، ثم تحميل بيانات إضافية (وهي عملية تُعرف بـ"إعادة التحميل"). لذا ينبغي لنا أن ننظر إلى QNNs باعتبارها مجموعة فرعية من الدوائر الكمومية ذات المعاملات، وألا تقيّدنا المقارنة بالشبكات العصبية الكلاسيكية في استكشاف الدوائر الكمومية المفيدة.
مجموعة البيانات التي نتناولها في هذا الدرس تتألف من صور تحتوي على خطوط أفقية وعمودية، وهدفنا هو تصنيف الصور غير المرئية إلى إحدى الفئتين بحسب اتجاه الخط. وسنُحقق ذلك باستخدام VQC. وأثناء تقدّمنا، سنتناول طرقاً لتحسين الحساب وتوسيع نطاقه. مجموعة البيانات هذه سهلة للغاية في التصنيف الكلاسيكي، وقد اخترناها لبساطتها حتى نتمكن من التركيز على الجانب الكمومي من هذه المسألة، والنظر في كيفية ترجمة خاصية في مجموعة البيانات إلى جزء من دائرة كمومية. ولا يُعقل توقع تسريع كمومي لمثل هذه الحالات البسيطة التي تتمتع فيها الخوارزميات الكلاسيكية بكفاءة عالية.
بنهاية هذا الدرس، ستكون قادراً على:
- تحميل البيانات من صورة إلى دائرة كمومية
- بناء ansatz لـ VQC (أو QNN)، وضبطه ليتناسب مع مسألتك
- تدريب VQC/QNN الخاص بك واستخدامه لإجراء تنبؤات دقيقة على بيانات الاختبار
- توسيع نطاق المسألة، والتعرف على حدود أجهزة الحوسبة الكمومية الحالية
توليد البيانات
سنبدأ ببناء البيانات. في الغالب لا تُولَّد مجموعات البيانات بشكل صريح ضمن إطار Qiskit patterns. لكن ن وع البيانات وإعدادها أمر بالغ الأهمية لتطبيق الحوسبة الكمومية بنجاح في تعلم الآلة. يُعرّف الكود أدناه مجموعة بيانات من الصور بأبعاد بكسل محددة. يُعيَّن لصف أو عمود كامل من الصورة القيمة ، في حين تُعيَّن للبكسلات المتبقية قيم عشوائية في الفترة . القيم العشوائية تمثل الضوضاء في بياناتنا. راجع الكود للتأكد من فهمك لطريقة توليد الصور. لاحقاً سنوسّع نطاق الصور.
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-ibm-runtime scipy scikit-learn
# This code defines the images to be classified:
import numpy as np
# Total number of "pixels"/qubits
size = 8
# One dimension of the image (called vertical, but it doesn't matter). Must be a divisor of `size`
vert_size = 2
# The length of the line to be detected (yellow). Must be less than or equal to the smallest dimension of the image (`<=min(vert_size,size/vert_size)`
line_size = 2
def generate_dataset(num_images):
images = []
labels = []
hor_array = np.zeros((size - (line_size - 1) * vert_size, size))
ver_array = np.zeros((round(size / vert_size) * (vert_size - line_size + 1), size))
j = 0
for i in range(0, size - 1):
if i % (size / vert_size) <= (size / vert_size) - line_size:
for p in range(0, line_size):
hor_array[j][i + p] = np.pi / 2
j += 1
# Make two adjacent entries pi/2, then move down to the next row. Careful to avoid the "pixels" at size/vert_size - linesize, because we want to fold this list into a grid.
j = 0
for i in range(0, round(size / vert_size) * (vert_size - line_size + 1)):
for p in range(0, line_size):
ver_array[j][i + p * round(size / vert_size)] = np.pi / 2
j += 1
# Make entries pi/2, spaced by the length/rows, so that when folded, the entries appear on top of each other.
for n in range(num_images):
rng = np.random.randint(0, 2)
if rng == 0:
labels.append(-1)
random_image = np.random.randint(0, len(hor_array))
images.append(np.array(hor_array[random_image]))
elif rng == 1:
labels.append(1)
random_image = np.random.randint(0, len(ver_array))
images.append(np.array(ver_array[random_image]))
# Randomly select 0 or 1 for a horizontal or vertical array, assign the corresponding label.
# Create noise
for i in range(size):
if images[-1][i] == 0:
images[-1][i] = np.random.rand() * np.pi / 4
return images, labels
hor_size = round(size / vert_size)
لاحظ أن الكود أعلاه قد أنشأ أيضاً تسميات تشير إلى ما إذا ك انت الصور تحتوي على خط عمودي (+1) أم أفقي (-1). سنستخدم الآن sklearn لتقسيم مجموعة بيانات مؤلفة من 100 صورة إلى مجموعة تدريب ومجموعة اختبار (مع تسمياتها المقابلة). هنا نستخدم من مجموعة البيانات للتدريب، بينما نحتفظ بالـ المتبقية للاختبار.
from sklearn.model_selection import train_test_split
np.random.seed(42)
images, labels = generate_dataset(200)
train_images, test_images, train_labels, test_labels = train_test_split(
images, labels, test_size=0.3, random_state=246
)
لنرسم بعض عناصر مجموعة بياناتنا لنرى كيف تبدو هذه الخطوط:
import matplotlib.pyplot as plt
# Make subplot titles so we can identify categories
titles = []
for i in range(8):
title = "category: " + str(train_labels[i])
titles.append(title)
# Generate a figure with nested images using subplots.
fig, ax = plt.subplots(4, 2, figsize=(10, 6), subplot_kw={"xticks": [], "yticks": []})
for i in range(8):
ax[i // 2, i % 2].imshow(
train_images[i].reshape(vert_size, hor_size),
aspect="equal",
)
ax[i // 2, i % 2].set_title(titles[i])
plt.subplots_adjust(wspace=0.1, hspace=0.3)
كل صورة من هذه الصور لا تزال مقترنة بتسميتها في train_labels في شكل قائمة بسيطة:
print(train_labels[:8])
[1, 1, 1, 1, -1, 1, 1, 1]
المصنّف الكمومي التغايري: محاولة أولى
خطوة Qiskit patterns الأولى: تعيين المسألة إلى دائرة كمومية
الهدف هو إيجاد دالة بمعاملات تُعيّن متجه بيانات / صورة إلى الفئة الصحيحة: . سيتحقق ذلك باستخدام VQC بعدد قليل من الطبقات يمكن تمييزها بأغراضها المختلفة:
هنا، هي دائرة الترميز، ولدينا خيارات عديدة لها كما رأينا في الدروس السابقة. هي كتلة الدائرة التغايرية أو القابلة للتدريب، و هي مجموعة المعاملات المراد تدريبها. ستتغير هذه المعاملات بواسطة خوارزميات التحسين الكلاسيكية للعثور على مجموعة المعا ملات التي تُعطي أفضل تصنيف للصور بواسطة الدائرة الكمومية. تُسمى هذه الدائرة التغايرية أحياناً بـ"الـansatz". وأخيراً، هو مقياس 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. الوصف الكامل يتجاوز نطاق هذه المقدمة؛ هنا نشير فقط إلى بعض فئات الاعتبارات.
- الأجهزة: جميع أجهزة الحوسبة الكمومية الحديثة أكثر عرضة للأخطاء وأكثر تأثراً بالضوضاء مقارنةً بنظيراتها الكلاسيكية. استخدام ansatz عميق بشكل مفرط (خاصة في عمق بوابات الكيوبت الثنائي بعد التحويل) لن يُعطي نتائج جيدة. مسألة ذات صلة هي أن أجهزة الحوسبة الكمومية لها تخطيط معين للكيوبتات، بمعنى أن بعض الكيوبتات الفيزيائية متجاورة على الجهاز الكمومي، وقد تكون أخرى بعيدة جداً عن بعضها. تشابك الكيوبتات المتجاورة لا يزيد العمق كثيراً، لكن تشابك الكيوبتات البعيدة جداً قد يزيد العمق بشكل كبير، إذ يجب إدراج بوابات المبادلة (swap) لنقل المعلومات إلى كيوبتات متجاورة حتى يمكن تشابكها.
- المسألة: كلما توفرت لديك معلومات عن مسألتك يمكنها توجيه اختيار الـansatz، استخدمها. على سبيل المثال، البيانات في هذا الدرس مكوّنة من صور لخطوط أفقية وعمودية. يمكن للمرء أن يفكر في الارتباط بين الألوان/القيم المتجاورة الذي يُميّز صورة تحتوي على خط أفقي عن صورة تحتوي على خط عمودي. ما صفات الـansatz التي تتوافق مع هذا الارتباط بين البكسلات المتجاورة؟ سنعود إلى هذه النقطة بشكل أكثر تقنية لاحقاً في هذا الدرس. لكن في الوقت الحالي، لنقل فقط أن تضمين التشابك وبوابات CNOT بين الكيوبتات المقابلة للبكسلات المتجاورة يبدو فكرة جيدة. في الصورة الأشمل، فكر في ما إذا كانت المسألة تُحلّ فعلاً بشكل أفضل باستخدام دائرة كمومية، أو ما إذا كانت ثمة خوارزميات كلاسيكية قادرة على أداء نفس الدور.
- عدد المعاملات: كل بوابة كمومية ذات معامل مستقل في الدائرة تزيد من الفضاء المراد تحسينه كل اسيكياً، مما يُبطئ التقارب. لكن مع توسّع المسائل، قد يُصادَف ما يُعرف بـالمستويات القاحلة (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 الكامل. في هذه الحالة، مكونات دائرتنا الكمومية مشابهة تماماً لمكونات الشبكات العصبية، إذ يُشبه إلى حد بعيد الطبقة التي تُحمّل قيم الإدخال من الصورة، ويُشبه طبقة "الأوزان" المتغيرة. نظراً لقوة هذا التشابه في هذه الحالة، سنعتمد "qnn" في بعض اصطلاحات التسمية؛ لكن لا ينبغي لهذا التشابه أن يُقيّد استكشافك لـVQCs.

# QNN ansatz
ansatz = qnn_circuit
# Combine the feature map with the ansatz
full_circuit = QuantumCircuit(num_qubits)
full_circuit.compose(feature_map, range(num_qubits), inplace=True)
full_circuit.compose(ansatz, range(num_qubits), inplace=True)
# Display the circuit
full_circuit.decompose().draw("mpl", style="clifford", fold=-1)
يجب الآن تعريف observable حتى نتمكن من استخدامه في دالة التكلفة. سنحصل على قيمة التوقع لهذا الـobservable باستخدام Estimator. إذا اخترنا ansatz جيداً مُدفوعاً بطبيعة المسألة، فسيحمل كل كيوبت معلومات ذات صلة بالتصنيف. يمكن إضافة طبقات لدمج المعلومات على عدد أقل من الكيوبتات (تُسمى طبقة الالتفاف)، بحيث تكون القياسات مطلوبة على مجموعة فرعية فقط من الكيوبتات في الدائرة (كما في الشبكات العصبية الالتفافية). أو يمكن قياس خاصية ما من كل كيوبت. هنا سنختار الخيار الأخير، لذا نُدرج عامل Z لكل كيوبت. لا يوجد ما يميّز اختيار عن غيره، لكنه مُبرر بشكل جيد:
- هذه مهمة تصنيف ثنائية، وقياس يمكن أن يُعطي نتيجتين محتملتين.
- القيم الذاتية لـ () متباعدة بشكل معقول، وتُعطي نتيجة للمقدِّر في الفترة [-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)