Lumaktaw sa pangunahing nilalaman

Mga Quantum Variational Circuit at Quantum Neural Network

Sa araling ito, nagpapatupad tayo ng ilang variational quantum circuit para sa isang gawain ng pag-uuri ng data — ang tinatawag na variational quantum classifiers (VQCs). Noon, karaniwan nang tawaging quantum neural networks (QNNs) ang isang subset ng mga VQC, bilang pagkakatulad sa mga klasikal na neural network. Sa katunayan, may mga pagkakataon kung saan ang mga estrukturang hiniram mula sa mga klasikal na neural network — tulad ng mga convolutional layer — ay may mahalagang papel sa mga VQC. Sa mga ganitong kaso kung saan malakas ang pagkakatulad, maaaring kapaki-pakinabang ang pagtawag na QNN. Ngunit hindi kailangang sundin ng mga parameterized quantum circuit ang pangkalahatang estruktura ng isang neural network; halimbawa, hindi lahat ng data ay kailangang i-load sa unang (input) layer — maaari tayong mag-load ng ilang data sa unang layer, mag-apply ng ilang gate, at pagkatapos ay mag-load ng karagdagang data (prosesong tinatawag na data "reuploading"). Kaya naman, dapat nating isipin ang mga QNN bilang isang subset ng mga parameterized quantum circuit, at hindi dapat tayong limitahan sa ating pag-explore ng mga kapaki-pakinabang na quantum circuit dahil lang sa pagkakatulad nito sa mga klasikal na neural network.

Ang dataset na tinutugunan sa araling ito ay binubuo ng mga imahe na naglalaman ng mga pahalang at patayong guhit, at ang ating layunin ay i-label ang mga bagong imahe sa isa sa dalawang kategorya batay sa oryentasyon ng linya nito. Gagawin natin ito gamit ang isang VQC. Habang tumatagal, tatalakayin natin ang mga paraan upang mapabuti at mapalawig ang kalkulasyon. Napakasimple ng dataset na ito para sa klasikal na pag-uuri — pinili ito dahil sa kasimplihan nito upang makapag-focus tayo sa quantum na bahagi ng problema, at tingnan kung paano maaaring isahiligan ng isang katangian ng dataset sa isang bahagi ng quantum circuit. Hindi makatuwiran na asahan ang quantum speed-up para sa mga ganitong simpleng kaso kung saan napaka-episyente ng mga klasikal na algorithm.

Sa pagtatapos ng araling ito, dapat mong magawa ang mga sumusunod:

  • Mag-load ng data mula sa isang imahe papasok sa isang quantum circuit
  • Bumuo ng ansatz para sa isang VQC (o QNN), at i-adjust ito para sa iyong problema
  • Sanayin ang iyong VQC/QNN at gamitin ito para gumawa ng tumpak na mga hula sa test data
  • Palakihin ang problema, at makilala ang mga limitasyon ng kasalukuyang mga quantum computer

Pagbuo ng data

Magsisimula tayo sa pagbuo ng data. Ang mga dataset ay kadalasang hindi tahasang nililikha bilang bahagi ng Qiskit patterns framework. Ngunit ang uri at paghahanda ng data ay kritikal sa matagumpay na paglalapat ng quantum computing sa machine learning. Ang code sa ibaba ay nagtatakda ng isang dataset ng mga imahe na may nakatakdang dimensyon ng pixel. Ang isang buong hilera o kolumna ng imahe ay itinalaga ang halagang π/2\pi/2, at ang natitirang mga pixel ay itinalaga ang mga random na halaga sa agwat na (0,π/4)(0,\pi/4). Ang mga random na halagang ito ay ingay sa ating data. Tingnan ang code upang matiyak na naiintindihan mo kung paano nalilikha ang mga imahe. Sa bandang huli, palalawakin natin ang mga imahe.

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

Pansinin na ang code sa itaas ay naglabas din ng mga label na nagpapahiwatig kung ang mga imahe ay naglalaman ng patayong (+1) o pahalang (-1) na linya. Gagamitin natin ngayon ang sklearn upang hatiin ang isang dataset ng 100 imahe sa isang training set at testing set (kasama ang kanilang mga kaukulang label). Dito, gumagamit tayo ng 70%70\% ng dataset para sa training, at ang natitirang 30%30\% ay itinago para sa testing.

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
)

I-plot natin ang ilang elemento ng ating dataset upang makita kung ano ang hitsura ng mga linya:

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

Ang bawat isa sa mga imahe na ito ay nananatiling naka-pair sa kanyang label sa train_labels sa simpleng listahan:

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

Variational quantum classifier: unang pagtatangka

Hakbang 1 ng Qiskit patterns: I-map ang problema sa isang quantum circuit

Ang layunin ay makahanap ng function na ff na may mga parameter na θ\theta na nag-map ng isang data vector / imahe na x\vec{x} sa tamang kategorya: fθ(x)±1f_\theta(\vec{x}) \rightarrow \pm1. Magagawa ito gamit ang isang VQC na may ilang layer na maaaring matukoy sa pamamagitan ng kanilang natatanging layunin:

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

Dito, ang U(x)U(\vec{x}) ay ang encoding circuit, na may maraming opsyon tulad ng nakita sa mga nakaraang aralin. Ang W(θ)W(\theta) ay isang variational, o maaaring sanayin na circuit block, at ang θ\theta ay ang hanay ng mga parameter na sasanayin. Ang mga parameter na ito ay bababaguhin ng mga klasikal na optimization algorithm upang mahanap ang hanay ng mga parameter na nagbibigay ng pinakamahusay na pag-uuri ng mga imahe ng quantum circuit. Ang variational circuit na ito ay tinatawag minsan na "ansatz". Sa wakas, ang OO ay ilang observable na itatantya gamit ang Estimator primitive. Walang hadlang na puwersahin ang mga layer na dumating sa pagkakasunod-sunod na ito, o kahit na maging ganap na hiwalay. Maaaring magkaroon ng maraming variational at/o encoding layer sa anumang pagkakasunod-sunod na may teknikal na motibasyon.

Magsisimula tayo sa pagpili ng feature map upang i-encode ang ating data. Gagamitin natin ang z_feature_map, dahil pinapanatili nito ang mababang circuit depth kumpara sa ilang ibang feature mapping.

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

Kailangan nating pumili ngayon ng ansatz na sasanayin. Maraming dapat isaalang-alang sa pagpili ng ansatz. Lampas na sa saklaw ng introduksiyon na ito ang kumpletong paglalarawan; dito ay ituturo lang natin ang ilang kategorya ng mga konsiderasyon.

  1. Hardware: Lahat ng modernong quantum computer ay mas madaling magkaroon ng error at mas madaling maapektuhan ng ingay kaysa sa kanilang mga klasikal na katapat. Ang paggamit ng ansatz na masyadong malalim (lalo na sa transpiled, two-qubit depth) ay hindi magbibigay ng magandang resulta. Kaugnay na isyu ay ang layout ng qubit sa mga quantum computer — ang ibig sabihin, ang ilang physical qubit ay magkakatabi sa quantum computer, at ang iba ay maaaring napakalayo sa isa't isa. Ang pag-entangle ng magkakatabi na qubit ay hindi masyadong nagpapataas ng depth, ngunit ang pag-entangle ng napakalayong qubit ay maaaring malaki ang epekto sa depth, dahil kailangan nating magpasok ng mga swap gate upang ilipat ang impormasyon sa mga qubit na magkakatabi bago sila ma-entangle.
  2. Ang problema: Kapag mayroon kang impormasyon tungkol sa iyong problema na makakatulong sa iyong ansatz, gamitin ito. Halimbawa, ang data sa araling ito ay binubuo ng mga imahe ng mga pahalang at patayong linya. Maaaring isaalang-alang kung anong ugnayan sa pagitan ng mga magkakatabi na kulay/halaga ang nagpapakilala ng imahe ng pahalang o patayong linya. Anong katangian ng isang ansatz ang tumutugma sa ugnayan na ito sa pagitan ng magkakatabi na pixel? Babalikan natin ang puntong ito nang mas teknikal sa ibang bahagi ng araling ito. Ngunit sa ngayon, sabihin na lang nating mukhang magandang ideya ang pagsasama ng entanglement at mga CNOT gate sa pagitan ng mga qubit na tumutugma sa magkakatabi na pixel. Sa mas malawak na larawan, isaalang-alang kung ang problema ay talagang pinakamainam na nalulutas gamit ang isang quantum circuit, o kung may mga klasikal na algorithm na maaaring makagawa ng parehong trabaho.
  3. Bilang ng mga parameter: Ang bawat independyenteng parameterized quantum gate sa circuit ay nagpapataas ng espasyong klasikal na ino-optimize, at nagreresulta ito sa mas mabagal na convergence. Ngunit habang lumalaki ang mga problema, maaaring maranasan ang barren plateaus. Ang terminong ito ay tumutukoy sa isang penomenon kung saan ang optimization landscape ng isang variational quantum algorithm ay nagiging exponentially patag at walang tampok habang lumalaki ang sukat ng problema. Nagreresulta ito sa mga nawawalang gradient, na nagpapahirap sa epektibong pagsasanay ng algorithm[1]. Ang mga barren plateau ay may kaugnayan sa mga variational quantum algorithm tulad ng mga VQC/QNN. Dapat tandaan na ang pagtaas ng bilang ng mga parameter ay hindi lamang ang dapat isaalang-alang sa pag-iwas sa mga barren plateau; kasama rin dito ang mga global cost function at random na pag-initialize ng parameter.

Sa araling ito, makikita natin ang ilang simpleng halimbawa ng magandang gawi sa pagbuo ng ansatz. Subukan muna natin ang ansatz sa ibaba. Babalikan natin ito upang baguhin sa ibang pagkakataon.

# 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]) ├─────────────────────────────────────
└──────────┘└───────────┘

Sa paghahanda ng data encoding at variational circuit, maaari na nating pagsamahin ang mga ito upang mabuo ang ating kumpletong ansatz. Sa kasong ito, ang mga bahagi ng ating quantum circuit ay halos katulad ng mga nasa neural network, kung saan ang U(x)U(\vec{x}) ay katulad sa layer na naglo-load ng mga input value mula sa imahe, at ang W(θ)W(\theta) ay katulad sa layer ng mga variable na "weights". Dahil malapit ang pagkakatulad sa kasong ito, gumagamit tayo ng "qnn" sa ilang pangalan ng ating convention; ngunit ang pagkakatulad na ito ay hindi dapat maging hadlang sa iyong pag-explore ng mga VQC.

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

Kailangan na nating tukuyin ang isang observable upang magamit natin ito sa ating cost function. Kukuha tayo ng expectation value para sa observable na ito gamit ang Estimator. Kung nakapili tayo ng magandang, problem-motivated na ansatz, ang bawat qubit ay maglalaman ng impormasyon na may kaugnayan sa pag-uuri. Maaaring magdagdag ng mga layer upang pagsama-samahin ang impormasyon sa mas kaunting qubit (tinatawag na convolutional layer), upang ang mga sukat ay kailangan lamang sa isang subset ng mga qubit sa circuit (tulad ng sa mga convolutional neural network). O maaari ring sukatin ang ilang katangian mula sa bawat qubit. Dito pipiliin natin ang huli, kaya isasama natin ang isang operator na Z para sa bawat qubit. Walang natatangi sa pagpili ng ZZ, ngunit mayroon itong magandang motibasyon:

  • Ito ay isang binary classification task, at ang pagsukat ng ZZ ay maaaring magbigay ng dalawang posibleng resulta.
  • Ang mga eigenvalue ng ZZ (±1\pm 1) ay sapat na malayo sa isa't isa, at nagbubunga ng estimator outcome sa agwat [-1, +1], kung saan ang 0 ay maaaring gamitin bilang cutoff value.
  • Madaling sukatin sa Pauli Z basis nang walang karagdagang gate overhead.

Kaya, napaka-natural na pagpipilian ang Z.

from qiskit.quantum_info import SparsePauliOp

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

Mayroon na tayo ng ating quantum circuit at ang observable na gusto nating tantiyahin. Kailangan pa natin ng ilang bagay upang mapatakbo at ma-optimize ang circuit na ito. Una, kailangan natin ng function upang patakbuhin ang isang forward pass. Pansinin na ang function sa ibaba ay tumatanggap ng input_params at weight_params nang hiwalay. Ang una ay ang hanay ng mga static na parameter na naglalarawan ng data sa isang imahe, at ang huli ay ang hanay ng mga variable na parameter na ino-optimize.

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

Loss function

Susunod, kailangan natin ng loss function upang kalkulahin ang pagkakaiba sa pagitan ng mga hinulaang halaga at ng mga tunay na label. Ang function ay tatanggap ng mga label na hinula ng algorithm at ng mga tamang label, at ibabalik ang mean squared difference. Maraming iba't ibang loss function. Dito, ang MSE ay isang halimbawa na aming pinili.

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

Tukuyin din natin ang bahagyang naiibang loss function na isang function ng mga variable na parameter (weights), para gamitin ng klasikal na optimizer. Ang function na ito ay tumatanggap lamang ng mga ansatz parameter bilang input; ang iba pang mga variable para sa forward pass at ang loss ay itinakda bilang mga global na parameter. Susubukin ng optimizer na sanayin ang modelo sa pamamagitan ng pag-sample ng iba't ibang weight at pagsisikap na babaan ang output ng cost/loss function.

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

Sa itaas ay binanggit natin ang paggamit ng klasikal na optimizer. Kapag naghanap na tayo ng mga weight upang mabawasan ang cost function, gagamitin natin ang optimizer na COBYLA:

from scipy.optimize import minimize

Magtatakda tayo ng ilang panimulang global na variable para sa cost function.

# Globals
circuit = full_circuit
observables = observable
# input_params = train_images_batch
# target = train_labels_batch
objective_func_vals = []
iter = 0

Hakbang 2 ng Qiskit Patterns: I-optimize ang problema para sa quantum execution

Magsisimula tayo sa pagpili ng backend para sa execution. Sa kasong ito, gagamitin natin ang pinaka-hindi abala na backend.

from qiskit_ibm_runtime import QiskitRuntimeService

service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)
print(backend.name)
ibm_brisbane

Dito ino-optimize natin ang circuit para patakbuhin sa isang tunay na backend sa pamamagitan ng pagtukoy sa optimization_level at pagdaragdag ng dynamical decoupling. Ang code sa ibaba ay nagbubuo ng pass manager gamit ang mga preset pass manager mula sa 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,
),
]
)

Gagamitin na natin ang pass manager sa circuit. Ang mga pagbabago sa layout na magreresulta ay dapat ding ilapat sa observable. Para sa napakalaking circuit, ang mga heuristic na ginagamit sa pag-optimize ng circuit ay hindi palaging nagbibigay ng pinakamainam at pinakamahinang circuit. Sa mga ganitong kaso, makatuwiran na patakbuhin ang mga ganitong pass manager nang maraming beses at gamitin ang pinakamainam na circuit. Makikita natin ito sa ibang pagkakataon kapag pinalaki natin ang ating kalkulasyon.

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

Hakbang 3 ng Qiskit Patterns: Mag-execute gamit ang Qiskit Primitives

Mag-loop sa dataset sa pamamagitan ng mga batch at epoch

Una, ipapatupad natin ang buong algorithm gamit ang isang simulator para sa mabilis na pag-debug at para sa mga tantiya ng error. Maaari na nating tahakin ang buong dataset sa mga batch sa nais na bilang ng mga epoch upang sanayin ang ating quantum neural network.

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

Qiskit Patterns Hakbang 4: Post-process, ibalik ang resulta sa klasikal na format

Pagsubok at katumpakan

Ngayon ay bibigyang-kahulugan natin ang mga resulta mula sa training. Una, susubukan natin ang katumpakan ng training sa buong training set.

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% lang ang training accuracy — hindi talaga maganda. Mahirap isipin na mas magiging maayos pa ang performance ng modelo sa test set. Patunayan natin ito.

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%

Hindi magaling ang modelo sa pag-classify ng mga datos na ito. Dapat nating tanungin kung bakit, at sa partikular, dapat nating suriin ang mga sumusunod:

  • Masyadong maagang natapos ang training? Kailangan ba ng mas maraming optimization steps?
  • Gumawa ba tayo ng masamang ansatz? Maraming posibleng dahilan dito. Kapag nagtatrabaho sa tunay na quantum computers, ang lalim ng Circuit ay magiging malaking konsiderasyon. Ang bilang ng mga parameter ay posibleng mahalaga rin, pati na rin ang entanglement sa pagitan ng mga Qubit.
  • Pinagsama ang dalawa sa itaas — gumawa ba tayo ng ansatz na may napakaraming parameter para maging trainable?

Maaari tayong magsimula sa pamamagitan ng pagsusuri ng convergence sa optimization:

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

Maaari nating subukang palawigin ang optimization steps para matiyak na hindi lang natigil ang optimizer sa isang lokal na minimum sa parameter space. Ngunit mukhang medyo converged na ito. Tingnan nating mas mabuti ang mga larawan na hindi natamang na-classify, at subukan nating unawain kung ano ang nangyayari.

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

Makikita natin dito na ang napakaraming mali sa pag-classify ay may patayong linya. May bahagi ng ating modelo na hindi nakakakuha ng impormasyon tungkol sa mga iyon. Maaaring nahulaan mo na ito batay sa unang variational circuit. Tingnan nating ito nang mas malapitan.

Pagpapabuti ng modelo

Muling pagtingin sa Hakbang 1

Sa pag-map ng ating problema sa isang quantum circuit, dapat tayong nag-isip nang malinaw kung paano tinutukoy ng mga katabing pixel ang klase. Para matukoy ang mga pahalang na linya, gusto nating malaman ang "kung dilaw ang pixel ii, dilaw din ba ang pixel i+1i+1" para sa lahat ng pixel sa bawat hilera. Gusto rin nating malaman tungkol sa mga patayong linya. Pero dahil binary ang classification, maiisip ng isa na kung walang natukoy na pahalang na linya, patayo na ito ang linya. Ang ating nakaraang variational circuit ay naglalaman ng mga CNOT gate sa pagitan ng mga Qubit (at samakatuwid ay mga pixel) 0 at 1, 1 at 2, at 2 at 3. Sinasaklaw nito ang mga pahalang na linya sa itaas ng larawan, ngunit hindi nito direktang natutukoy ang mga patayong linya, at hindi rin ganap nitong natutukoy ang mga pahalang na linya dahil binabalewala nito ang ibabang hilera. Para ganap na matukoy ang lahat ng pahalang na linya, gusto nating magkaroon ng katulad na hanay ng mga CNOT gate sa pagitan ng mga Qubit (pixel) 4 at 5, 5 at 6, at 6 at 7. Maaari tayong isaalang-alang na ang pagdaragdag ng mga CNOT gate sa pagitan ng mga Qubit na tumutugma sa mga patayong linya (tulad ng 0 at 4, o 2 at 6) ay maaari ring maging kapaki-pakinabang. Ngunit unang susuriin natin kung sapat na ang matukoy kung mayroon o walang pahalang na linya.

# 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

Hindi natin nadagdagan ang lalim ng Circuit. Tingnan natin kung nadagdagan ang kakayahan nitong mag-model ng ating mga larawan.

Muling pagtingin sa Hakbang 2

Kailangan nating i-transpile ang bagong Circuit na ito para mapatakbo sa isang tunay na quantum backend. Laktawan muna natin ang hakbang na ito para makita kung ang ating pagbabago sa variational circuit ay nagkaroon ng nais na epekto sa mga simulator. Mas malalim tayong tatalakayin ang transpilation sa susunod na subseksyon.

Muling pagtingin sa Hakbang 3

Ngayon ay ilalapat natin ang updated na modelo sa ating training data.

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

Muling pagtingin sa Hakbang 4

Simulan natin sa pagsusuri kung ganap na nag-converge ang ating optimizer.

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

Mukhang hindi pa ganap na nag-converge ito, dahil hindi pa nananatiling halos pantay ang loss function sa mahabang panahon. Ngunit ang loss function ay ~60% na mas mababa kaysa nang gamitin ang nakaraang variational circuit. Kung ito ay isang research project, gusto nating tiyakin ang ganap na convergence. Ngunit para sa layunin ng pagsisiyasat, ito ay sapat na. Suriin natin ang katumpakan sa ating training at testing data.

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\%$ na katumpakan sa magkabilang set! Tama ang ating hinala na ang tamang pagtukoy ng mga pahalang na linya ay sapat na! Higit pa rito, epektibo ang ating pag-map mula sa kinakailangang impormasyon tungkol sa mga pixel patungo sa mga CNOT gate sa quantum circuit. Tingnan na natin kung paano nag-scale ang prosesong ito para sa pagpapatakbo sa mga tunay na quantum computer.

## Pag-scale at pagpapatakbo sa tunay na mga quantum computer \{#scaling-and-running-on-real-quantum-computers}

### Data \{#data}

Simulan natin sa pamamagitan ng pagpapalaki ng ating mga imahe. Walang espesyal na dahilan para pumili ng 6x6 na grid, maliban sa lumampas ito sa bilang ng mga qubit (32) na kaya nating i-simulate para sa mga circuit na gumagamit ng mga non-Clifford gate.

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

Dahil mahalaga ang quantum computing time, gagamit tayo ng napakaliit na training set at iilan lang na optimization steps. Sapat na ito para maipakita ang workflow.

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

Hakbang 1: I-map ang problema sa isang quantum 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")
# 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

Ito ay isang makatwirang two-qubit depth. Dapat makakuha tayo ng mataas na kalidad na mga resulta mula sa isang tunay na quantum computer.

# 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

Dahil gumagamit tayo ng z_feature_map, na walang CNOT gate, hindi nadaragdagan ang ating two-qubit depth kahit idagdag ang encoding layer. Maaari nating i-visualize ang buong circuit dito.

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

Output of the previous code cell

Maaaring mapansin mo na kung kailangan talagang bawasan ang two-qubit depth, maaari nating gawin ito nang kaunti sa pamamagitan ng pagbabago ng pagkakasunud-sunod ng mga CNOT. Halimbawa, ang mga CNOT sa q35q_{35} at q34q_{34} ay maaaring ilipat sa kaliwa sa circuit diagram sa itaas, at maaaring ilagay nang direkta sa ilalim ng mga CNOT sa q30q_{30} at q31q_{31}, halimbawa. Para sa isang two-qubit gate depth na 5, hindi malinaw na magdudulot ito ng pagbabago pagkatapos ng transpilation, ngunit ito ay isang bagay na dapat isaalang-alang. Kung mahalaga ang pagkakasunud-sunod ng mga CNOT gate para sa lohikal na pagtutugma ng problemang hinaharap, maayos ang depth dito. Kung hindi naman kritikal ang pagkakasunud-sunod ng mga CNOT sa pag-modelo ng istruktura ng data sa ating mga imahe, maaari tayong sumulat ng script para muling ayusin ang mga CNOT gate na ito upang mabawasan ang depth.

Kailangan din nating muling i-define ang ating observable para sa ating mas malalaking imahe:

from qiskit.quantum_info import SparsePauliOp

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

Qiskit Patterns Hakbang 2: I-optimize ang problema para sa quantum execution

Magsisimula tayo sa pamamagitan ng pagpili ng backend para sa execution. Sa kasong ito, gagamitin natin ang pinaka-hindi abala na 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

Muli, nagde-define tayo ng pass manager, na may optimization level na nakatakda sa 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,
),
]
)

Ngayon ay ilang beses nating ia-apply ang pass manager. Para sa napaka-malawak o napaka-malalim na mga circuit, maaaring malaki ang pagkakaiba-iba ng transpiled two-qubit depth. Para sa ganitong mga circuit, mahalagang subukan ang pass manager nang maraming beses at gamitin ang pinakamainam (pinakamababaw) na resulta.

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

Makikita natin na sa kasong ito, palaging 10 ang transpiled two-qubit depth. May kaunting pagkakaiba-iba sa single-qubit depth, at gagamitin natin ang pinakamababaw. Ngunit sa 36-qubit circuit na ito, hindi ito isang kritikal na pagpapabuti. Maaari nating i-visualize ang transpiled circuit na ito, kahit sa ganitong sukat ay nagiging mas mahirap nang pag-aralan nang biswal.

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 Patterns Hakbang 3: Mag-execute gamit ang Qiskit Primitives

Upang limitahan ang oras na gagamitin sa tunay na mga quantum computer, iilan lang na optimization steps ang isasagawa natin dito, at ginagawa natin ito sa napakaliit na training set. Ngunit malinaw ang scaling nito sa mas maraming optimization steps at mas malalaking testing data set mula sa mga tagubilin sa buong aralin.

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

Inirerekomenda na i-save mo ang mga weight parameter na ibinabalik mula sa kompuwasyong ito, sakaling magpasya kang magpatuloy pa.

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

Maaari nating i-plot ang unang ilang optimization steps na ito, kahit hindi natin inaasahang makakita ng convergence pagkatapos ng ilang optimization steps lamang. Medyo patag ang mga curve na ito sa unang ilang hakbang, kahit gamit ang mga simulator. Dapat nating tandaan, gayunpaman, na kasalukuyang may 72 free parameter ang optimization. Maaaring bawasan ito ng hindi bababa sa salik na 2–3 nang hindi nino-kompromiso ang mga resulta sa pamamagitan ng, halimbawa, pag-parametrize ng mga qubit na may data na tumutugma sa isang subset ng buong mga row at column. Sa katunayan, dapat bawasan ang parameter space bago gumastos ng mas maraming quantum computing time sa pagliit ng loss function.

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

Pagtatapos

Sa buod, sa araling ito natutunan natin ang workflow para sa binary classification ng mga imahe gamit ang isang quantum neural network. Ilang mahahalagang konsiderasyon sa bawat hakbang ng Qiskit patterns:

Hakbang 1: I-map ang problema sa isang quantum circuit

  • I-load ang training data. Magagawa ito "nang mano-mano" o gamit ang isang pre-built na feature map tulad ng z_feature_map.
  • Bumuo ng ansatz na naglalaman ng mga rotation at entanglement layer na angkop sa iyong problema.
  • Subaybayan ang circuit depth upang matiyak ang kalidad na mga resulta sa mga quantum computer.

Hakbang 2: I-optimize ang problema para sa quantum execution

  • Pumili ng backend, madalas ang pinaka-hindi abala.
  • Gamitin ang pass manager para i-transpile ang parehong circuit at mga observable sa arkitektura ng piniling backend.
  • Para sa napaka-malalim o napaka-malawak na mga circuit, mag-transpile nang maraming beses, at piliin ang pinakamababaw na circuit.

Hakbang 3: Mag-execute gamit ang Qiskit (Runtime) Primitives

  • Magsagawa ng mga paunang pagsubok sa mga simulator para i-debug at i-optimize ang iyong ansatz.
  • Mag-execute sa isang IBM® quantum computer.

Hakbang 4: Post-process, ibalik ang resulta sa classical format

  • Kalkulahin ang katumpakan ng modelo sa training data, at sa testing data.
  • Subaybayan ang convergence ng classical optimization.

Mga Sanggunian

[1] https://arxiv.org/abs/2405.00781