Přeskočit na hlavní obsah

Kvantové variační obvody a kvantové neuronové sítě

V této lekci implementujeme několik variačních kvantových obvodů pro úlohu klasifikace dat – takzvaných variačních kvantových klasifikátorů (VQC). V určitém období bylo běžné označovat podmnožinu VQC jako kvantové neuronové sítě (QNN) podle analogie s klasickými neuronovými sítěmi. Existují skutečně případy, kdy struktury převzaté z klasických neuronových sítí, jako jsou konvoluční vrstvy, hrají ve VQC důležitou roli. V takových případech, kde je analogie silná, může být označení QNN užitečné. Parametrizované kvantové obvody však nemusí sledovat obecnou strukturu neuronové sítě – například ne všechna data musí být načtena v první (vstupní) vrstvě; můžeme načíst část dat v první vrstvě, aplikovat některé Gate a poté načíst další data (proces nazývaný „reuploading" dat). QNN bychom proto měli považovat za podmnožinu parametrizovaných kvantových obvodů a při hledání užitečných kvantových obvodů bychom se neměli omezovat analogií s klasickými neuronovými sítěmi.

Datová sada, se kterou v této lekci pracujeme, se skládá z obrazů obsahujících vodorovné a svislé pruhy. Naším cílem je zařadit dosud neviděné obrazy do jedné ze dvou kategorií podle orientace jejich čáry. Dosáhneme toho pomocí VQC. Postupně se budeme věnovat způsobům, jak výpočet zlepšit a škálovat. Tato datová sada je klasicky mimořádně snadno klasifikovatelná. Byla zvolena pro svou jednoduchost, abychom se mohli soustředit na kvantovou část problému a podívat se na to, jak se vlastnost datové sady může promítnout do části kvantového obvodu. U tak jednoduchých případů, kde jsou klasické algoritmy tak efektivní, nelze realisticky očekávat kvantové zrychlení.

Po skončení této lekce bys měl/a umět:

  • Načíst data z obrazu do kvantového Circuit
  • Sestavit Ansatz pro VQC (nebo QNN) a upravit ho podle svého problému
  • Natrénovat svůj VQC/QNN a použít ho k přesným predikcím na testovacích datech
  • Škálovat problém a rozpoznat limity současných kvantových počítačů

Generování dat

Začneme sestavením dat. Datové sady nejsou v rámci frameworku Qiskit patterns většinou explicitně generovány. Typ a příprava dat jsou však pro úspěšné použití kvantového výpočtu ve strojovém učení klíčové. Kód níže definuje datovou sadu obrazů s pevnými rozměry v pixelech. Celý jeden řádek nebo sloupec obrazu dostane hodnotu π/2\pi/2 a zbývající pixely dostanou náhodné hodnoty na intervalu (0,π/4)(0,\pi/4). Náhodné hodnoty představují šum v našich datech. Prohlédni si kód a ujisti se, že rozumíš způsobu generování obrazů. Později obrazy zvětšíme.

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

Všimni si, že výše uvedený kód také vygeneroval štítky označující, zda obrazy obsahují svislou (+1) nebo vodorovnou (-1) čáru. Nyní použijeme sklearn k rozdělení datové sady 100 obrazů na trénovací a testovací sadu (spolu s odpovídajícími štítky). Zde používáme 70%70\% datové sady k trénování, přičemž zbývajících 30%30\% je vyhrazeno pro testování.

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
)

Vykresleme několik prvků naší datové sady, abychom viděli, jak tyto čáry vypadají:

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)

Výstup předchozí buňky kódu

Každý z těchto obrazů je stále spárován se svým štítkem v train_labels v jednoduché podobě seznamu:

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

Variační kvantový klasifikátor: první pokus

Qiskit patterns krok 1: Mapování problému na kvantový Circuit

Cílem je najít funkci ff s parametry θ\theta, která mapuje datový vektor / obrázek x\vec{x} do správné kategorie: fθ(x)±1f_\theta(\vec{x}) \rightarrow \pm1. Toho dosáhneme pomocí VQC s několika vrstvami, jež lze identifikovat podle jejich odlišných účelů:

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

Zde U(x)U(\vec{x}) je kódovací Circuit, pro který máme mnoho možností, jak jsme viděli v předchozích lekcích. W(θ)W(\theta) je variační, neboli trénovatelný blok Circuitu, a θ\theta je sada parametrů, které se mají trénovat. Tyto parametry budou měněny klasickými optimalizačními algoritmy, aby se našla sada parametrů, která přinese nejlepší klasifikaci obrázků kvantovým Circuitem. Tento variační Circuit se někdy nazývá „Ansatz". Nakonec OO je nějaká pozorovatelná veličina, která bude odhadována pomocí primitivu Estimator. Neexistuje žádné omezení, které by nutilo vrstvy být v tomto pořadí, ani aby byly zcela oddělené. Lze mít více variačních nebo kódovacích vrstev v libovolném pořadí, které je technicky motivováno.

Začneme výběrem feature map pro kódování našich dat. Použijeme z_feature_map, protože oproti jiným feature mappingům udržuje hloubku Circuitu nízkou.

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

Nyní musíme zvolit Ansatz, který se bude trénovat. Při výběru Ansatzu je třeba zvážit mnoho faktorů. Úplný popis přesahuje rámec tohoto úvodu; zde jen stručně zmíníme několik kategorií úvah.

  1. Hardware: Všechny moderní kvantové počítače jsou náchylnější k chybám a citlivější na šum než jejich klasické protějšky. Použití příliš hlubokého Ansatzu (zejména z hlediska transpilované hloubky dvou-Qubitových operací) nepřinese dobré výsledky. Souvisejícím problémem je to, že kvantové počítače mají určité rozložení Qubitů, což znamená, že některé fyzické Qubity jsou na kvantovém počítači sousední a jiné mohou být od sebe velmi vzdálené. Provázání sousedních Qubitů příliš nezvyšuje hloubku, ale provázání velmi vzdálených Qubitů může hloubku výrazně zvýšit, protože musíme vkládat swap Gate pro přesun informací na sousední Qubity, aby mohly být provázány.
  2. Problém: Kdykoli máš o svém problému informace, které by mohly vést výběr Ansatzu, využij je. Například data v této lekci se skládají z obrázků horizontálních a vertikálních čar. Lze uvažovat, jaká korelace mezi sousedními barvami/hodnotami identifikuje obrázek horizontální nebo vertikální čáry. Jaké vlastnosti Ansatzu by odpovídaly této korelaci mezi sousedními pixely? K tomuto bodu se vrátíme technicky podrobněji později v této lekci. Ale prozatím jen říkejme, že zahrnutí provázání a CNOT Gate mezi Qubity odpovídajícími sousedním pixelům se zdá být dobrým nápadem. V širším kontextu zvažuj, zda je problém skutečně nejlépe řešitelný pomocí kvantového Circuitu, nebo zda existují klasické algoritmy, které mohou odvést stejně dobrou práci.
  3. Počet parametrů: Každý nezávisle parametrizovaný kvantový Gate v Circuitu zvyšuje prostor pro klasickou optimalizaci, což vede k pomalejší konvergenci. Ale s rostoucím rozsahem problémů se lze setkat s barren plateaus (pustými planinami). Tento termín označuje jev, kdy se optimalizační krajina variačního kvantového algoritmu stává exponenciálně plochou a beztvárnou s růstem velikosti problému. To způsobuje mizení gradientů, což ztěžuje efektivní trénování algoritmu[1]. Barren plateaus jsou relevantní pro variační kvantové algoritmy jako VQC/QNN. Je třeba poznamenat, že rostoucí počet parametrů není jediným faktorem při vyhýbání se barren plateaus; mezi další faktory patří globální účelové funkce a náhodná inicializace parametrů.

V této lekci uvidíme několik jednoduchých příkladů dobré praxe při konstrukci Ansatzu. Nejprve vyzkoušejme Ansatz níže. Vrátíme se k jeho úpravě později.

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

Nyní, když máme připraveno kódování dat a variační Circuit, můžeme je zkombinovat a vytvořit náš úplný Ansatz. V tomto případě jsou komponenty našeho kvantového Circuitu dosti analogické těm v neuronových sítích: U(x)U(\vec{x}) je nejpodobnější vrstvě, která načítá vstupní hodnoty z obrázku, a W(θ)W(\theta) je jako vrstva proměnných „vah". Protože tato analogie v tomto případě platí, používáme „qnn" v některých našich jmenovacích konvencích; tato analogie by však neměla omezovat tvé zkoumání 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

Nyní musíme definovat pozorovatelnou veličinu, abychom ji mohli použít v naší účelové funkci. Očekávanou hodnotu této pozorovatelné veličiny získáme pomocí Estimatoru. Pokud jsme zvolili dobrý, problémem motivovaný Ansatz, bude každý Qubit obsahovat informace relevantní pro klasifikaci. Lze přidat vrstvy pro kombinování informací na méně Qubitů (nazývané konvoluční vrstva), takže měření jsou potřeba jen na podmnožině Qubitů v Circuitu (jako v konvolučních neuronových sítích). Nebo lze měřit nějaký atribut z každého Qubitu. Zde zvolíme druhou možnost, takže zahrneme operátor Z pro každý Qubit. Na výběru ZZ není nic jedinečného, ale je dobře motivovaný:

  • Jde o binární klasifikační úlohu a měření ZZ může přinést dva možné výsledky.
  • Vlastní hodnoty ZZ (±1\pm 1) jsou přiměřeně dobře odděleny a vedou k výsledku Estimatoru v intervalu [-1, +1], kde 0 lze jednoduše použít jako mezní hodnotu.
  • Měření v Pauli Z bázi je přímočaré bez dalších Gate overhead.

ZZ je tedy velmi přirozená volba.

from qiskit.quantum_info import SparsePauliOp

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

Máme náš kvantový Circuit a pozorovatelnou veličinu, kterou chceme odhadnout. Nyní potřebujeme několik věcí, abychom mohli tento Circuit spustit a optimalizovat. Nejprve potřebujeme funkci pro provedení dopředného průchodu. Všimni si, že funkce níže přijímá input_params a weight_params odděleně. První je sada statických parametrů popisujících data v obrázku a druhá je sada proměnných parametrů, které mají být optimalizovány.

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

Ztrátová funkce

Dále potřebujeme ztrátovou funkci, která vypočítá rozdíl mezi předpovězenými a skutečnými hodnotami štítků. Funkce přijme štítky předpovězené algoritmem a správné štítky a vrátí průměrnou kvadratickou odchylku. Existuje mnoho různých ztrátových funkcí. Zde jako příklad použijeme 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")

Definujme také mírně odlišnou ztrátovou funkci, která je funkcí proměnných parametrů (vah), určenou pro klasický optimalizátor. Tato funkce přijímá jako vstup pouze parametry Ansatzu; ostatní proměnné pro dopředný průchod a ztrátu jsou nastaveny jako globální parametry. Optimalizátor natrénuje model vzorkováním různých vah a snahou o snížení výstupu funkce nákladů/ztráty.

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

Výše jsme zmínili použití klasického optimalizátoru. Až přejdeme k prohledávání vah za účelem minimalizace funkce nákladů, použijeme optimalizátor COBYLA:

from scipy.optimize import minimize

Nastavíme některé počáteční globální proměnné pro funkci nákladů.

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

Krok 2 Qiskit Patterns: Optimalizace problému pro kvantové spuštění

Začneme výběrem Backend pro spuštění. V tomto případě použijeme nejméně vytížený Backend.

from qiskit_ibm_runtime import QiskitRuntimeService

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

Zde optimalizujeme Circuit pro spuštění na reálném Backend zadáním optimization_level a přidáním dynamického oddělování. Níže uvedený kód generuje správce průchodů (pass manager) pomocí přednastavených správců průchodů z 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,
),
]
)

Nyní použijeme správce průchodů na Circuit. Výsledné změny rozložení je nutné aplikovat také na observable. U velmi velkých Circuit nemusí heuristiky použité při optimalizaci Circuit vždy přinést nejlepší a nejplošší Circuit. V takových případech má smysl spustit tyto správce průchodů několikrát a použít nejlepší Circuit. Uvidíme to později, až budeme škálovat naše výpočty.

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

Krok 3 Qiskit Patterns: Spuštění pomocí Qiskit Primitives

Procházení datasetu v dávkách a epochách

Nejprve implementujeme celý algoritmus pomocí simulátoru pro rychlé ladění a odhady chyb. Nyní můžeme procházet celý dataset v dávkách v požadovaném počtu epoch a trénovat naši kvantovou neuronovou síť.

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

Krok 4 Qiskit Patterns: Následné zpracování, vrácení výsledku v klasickém formátu

Testování a přesnost

Nyní interpretujeme výsledky tréninku. Nejprve otestujeme přesnost na trénovací množině.

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%

Přesnost na trénovací množině je pouze 6060%, což rozhodně není dobré. Těžko si představit, že by výkon modelu na testovací množině byl o něco lepší. Pojďme to ověřit.

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%

Model tato data neklasifikuje dobře. Měli bychom se zamyslet nad tím, proč tomu tak je, a zejména bychom měli prověřit:

  • Zastavili jsme trénink příliš brzy? Bylo by potřeba více kroků optimalizace?
  • Sestavili jsme špatný Ansatz? To může znamenat mnoho věcí. Při práci na skutečných kvantových počítačích bude hloubka Circuit zásadním faktorem. Důležitý může být také počet parametrů, stejně jako provázání (entanglement) mezi Qubity.
  • V kombinaci obou výše uvedených bodů: sestavili jsme Ansatz s příliš mnoha parametry, než aby byl trénovatelný?

Můžeme začít tím, že zkontrolujeme konvergenci optimalizace:

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

Mohli bychom zkusit prodloužit počet kroků optimalizace, abychom se ujistili, že optimalizátor neuvízl jen v lokálním minimu v prostoru parametrů. Zdá se ale, že konvergence je poměrně dobrá. Podívejme se blíže na obrázky, které nebyly správně klasifikovány, a zkusme pochopit, co se děje.

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

Zde vidíme, že drtivá většina chybně klasifikovaných obrázků zobrazuje svislou čáru. Něco v našem modelu neumí zachytit informace o svislých čárách. Možná jsi to tušil/a už na základě prvního variačního Circuit. Pojďme se na něj podívat blíže.

Vylepšení modelu

Krok 1 znovu

Při mapování našeho problému na kvantový Circuit jsme měli výslovně přemýšlet o tom, jak informace v sousedních pixelech určuje třídu. Abychom identifikovali vodorovné čáry, chceme vědět „pokud je pixel ii žlutý, je pixel i+1i+1 žlutý" pro všechny pixely napříč každým řádkem. Chceme také vědět o svislých čárách. Protože je ale klasifikace binární, lze si jednoduše představit, že pokud taková vodorovná čára není detekována, jde o čáru svislou. Náš předchozí variační Circuit obsahoval CNOT Gate mezi Qubity (a tedy pixely) 0 a 1, 1 a 2 a 2 a 3. To pokrývá vodorovné čáry v horní části obrázku, ale přímo nedetekuje svislé čáry a ani úplně nedetekuje vodorovné čáry, protože ignoruje spodní řádek. Pro úplnou detekci všech vodorovných čar bychom chtěli mít podobnou sadu CNOT Gate mezi Qubity (pixely) 4 a 5, 5 a 6 a 6 a 7. Mohli bychom mít na paměti, že přidání CNOT Gate mezi Qubity odpovídajícími svislým čarám (například 0 a 4 nebo 2 a 6) může být také užitečné. Nejprve ale zkontrolujeme, zda postačí detekovat, zda vodorovná čára je, nebo není přítomna.

# 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

Výstup předchozí buňky kódu

Hloubku Circuit jsme nezvýšili. Podívejme se, zda jsme zvýšili jeho schopnost modelovat naše obrázky.

Krok 2 znovu

Tento nový Circuit budeme muset transpilovat pro spuštění na reálném kvantovém Backend. Tento krok prozatím přeskočíme, abychom zjistili, zda naše revize variačního Circuit měla požadovaný efekt na simulátorech. Transpilaci se podrobněji věnujeme v další podsekci.

Krok 3 znovu

Nyní aplikujeme aktualizovaný model na naše trénovací 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

Krok 4 znovu

Začněme tím, že ověříme, zda náš optimalizátor plně konvergoval.

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

Výstup předchozí buňky kódu

Zdá se, že optimalizátor plně nekonvergoval, protože ztrátová funkce se po dostatečně mnoho krocích neustálila přibližně na stejné hodnotě. Ztrátová funkce je však již přibližně o 60 % nižší než při použití předchozího variačního obvodu. Kdyby šlo o výzkumný projekt, chtěli bychom zajistit úplnou konvergenci. Pro účely průzkumu to ale postačí. Pojďme zkontrolovat přesnost na trénovacích a testovacích datech.

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\%$ přesnost na obou sadách! Naše domněnka, že pro správnou detekci stačí přesně rozpoznávat vodorovné čáry, se potvrdila! Navíc naše mapování potřebných informací o pixelech na CNOT Gate v kvantovém Circuit bylo efektivní. Podíváme se teď na to, jak tento proces škáluje při spuštění na skutečných kvantových počítačích.
## Škálování a spuštění na skutečných kvantových počítačích \{#scaling-and-running-on-real-quantum-computers}

### Data \{#data}

Začněme zvětšením velikosti našich obrázků. Na volbě mřížky 6×6 není nic zvláštního, jen přesahuje počet qubitů (32), které lze simulovat pro Circuit s non-Cliffordovými hradly.

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

Protože čas na kvantových počítačích je vzácná komodita, použijeme velmi malou trénovací sadu a velmi málo optimalizačních kroků. To bude dostačující k předvedení pracovního postupu.

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

Krok 1: Mapování problému na kvantový 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

Toto je rozumná hloubka pro dvouqubitová hradla. Měli bychom být schopni získat vysoce kvalitní výsledky z reálného kvantového počítače.

# 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

Protože používáme z_feature_map, který neobsahuje žádná CNOT hradla, přidání kódovací vrstvy nezvyšuje naši hloubku pro dvouqubitová hradla. Celý Circuit si zde můžeme vizualizovat.

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

Output of the previous code cell

Možná si všimneš, že kdybychom chtěli minimalizovat hloubku pro dvouqubitová hradla nade vše ostatní, mohli bychom ji trochu snížit změnou pořadí CNOT hradel. Například CNOT hradla na q35q_{35} a q34q_{34} by šlo posunout doleva v diagramu Circuit výše a umístit je přímo pod CNOT hradla na q30q_{30} a q31q_{31}. Při hloubce dvouqubitových hradel 5 není zřejmé, zda to po transpilaci udělá rozdíl, ale je to něco, co je dobré mít na paměti. Pokud je pořadí CNOT hradel důležité pro logické odpovídání řešeného problému, je hloubka zde v pořádku. Pokud pořadí CNOT hradel není klíčové pro modelování struktury dat v našich obrázcích, mohli bychom napsat skript pro přeuspořádání těchto CNOT hradel s cílem minimalizovat hloubku.

Také musíme znovu definovat naši pozorovatelnou veličinu pro naše větší obrázky:

from qiskit.quantum_info import SparsePauliOp

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

Qiskit Patterns krok 2: Optimalizace problému pro kvantové spuštění

Začínáme výběrem backendu pro spuštění. V tomto případě použijeme nejméně vytížený 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

Opět definujeme pass manager s úrovní optimalizace nastavenou na 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,
),
]
)

Nyní použijeme pass manager několikrát. U velmi širokých nebo velmi hlubokých Circuit může docházet k velké variabilitě v transpilované hloubce dvou-Qubit operací. U takových Circuit je důležité vyzkoušet pass manager mnohokrát a použít nejlepší (nejplošší) výsledek.

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

Vidíme, že v tomto případě byla transpilovaná hloubka dvou-Qubit operací vždy 10. V hloubce jednoho Qubitu došlo k menší variaci a použijeme nejplošší výsledek. Na tomto 36-Qubitovém Circuit to však není zásadní zlepšení. Tento transpilovaný Circuit si můžeme vizualizovat, ačkoli v tomto měřítku je stále obtížnější ho vizuálně analyzovat.

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 krok 3: Spuštění pomocí Qiskit Primitives

Abychom omezili čas strávený na skutečných kvantových počítačích, provedeme zde pouze několik optimalizačních kroků, a to na velmi malé tréninkové sadě. Škálování na více optimalizačních kroků a větší testovací datové sady by mělo být z pokynů v celé lekci zřejmé.

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

Doporučujeme uložit si váhové parametry vrácené z tohoto výpočtu pro případ, že se rozhodneš pokračovat v iteracích.

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

Můžeme si vykreslit těchto prvních několik optimalizačních kroků, i když po tak malém počtu kroků nelze očekávat žádnou konvergenci. Tyto křivky byly relativně ploché v průběhu prvních několika kroků, a to i při použití simulátorů. Je třeba poznamenat, že optimalizace v současné době pracuje s 72 volnými parametry. Tento počet lze snížit alespoň o faktor 2–3, aniž by se zhoršily výsledky — například parametrizací Qubitů daty odpovídajícími podmnožině celých řádků a sloupců. Prostor parametrů by měl být zredukován ještě před tím, než se věnuje více kvantového výpočetního času minimalizaci ztrátové funkce.

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

Výstup předchozí buňky s kódem

Závěr

Shrňme si, co jsme se v této lekci naučili: prošli jsme si pracovním postupem pro binární klasifikaci obrázků pomocí kvantové neuronové sítě. Klíčové aspekty každého kroku vzoru Qiskit byly:

Krok 1: Namapuj problém na kvantový obvod

  • Načti trénovací data. To lze udělat „ručně" nebo pomocí předpřipraveného příznakovéhu mapování, jako je z_feature_map.
  • Sestav ansatz obsahující rotační a provazovací vrstvy vhodné pro tvůj problém.
  • Sleduj hloubku obvodu, abys zajistil kvalitní výsledky na kvantových počítačích.

Krok 2: Optimalizuj problém pro kvantové spuštění

  • Vyber backend, nejčastěji ten nejméně vytížený.
  • Použij správce průchodů (pass manager) k transpilaci obvodu i observovatelných veličin na architekturu zvoleného backendu.
  • U velmi hlubokých nebo širokých obvodů transpiluj vícekrát a vyber nejplošší obvod.

Krok 3: Spusť pomocí primitiv Qiskit (Runtime)

  • Proveď předběžné testy na simulátorech, abys odladil a optimalizoval svůj ansatz.
  • Spusť výpočet na kvantovém počítači IBM®.

Krok 4: Post-procesuj výsledky a vrať je v klasickém formátu

  • Vypočítej přesnost modelu na trénovacích datech i na testovacích datech.
  • Sleduj konvergenci klasické optimalizace.

Reference

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