Pular para o conteúdo principal

Circuitos Quânticos Variacionais e Redes Neurais Quânticas

Nesta aula, implementamos vários circuitos quânticos variacionais para uma tarefa de classificação de dados — os chamados classificadores quânticos variacionais (VQCs). Em determinado momento, era comum chamar um subconjunto de VQCs de redes neurais quânticas (QNNs), por analogia com as redes neurais clássicas. De fato, há casos em que estruturas emprestadas das redes neurais clássicas, como camadas de convolução, desempenham um papel importante nos VQCs. Nesses casos em que a analogia é forte, QNNs pode ser uma descrição útil. Mas circuitos quânticos parametrizados não precisam seguir a estrutura geral de uma rede neural; por exemplo, nem todos os dados precisam ser carregados na primeira camada (de entrada); podemos carregar alguns dados na primeira camada, aplicar algumas portas e depois carregar dados adicionais (um processo chamado reuploading de dados). Devemos, portanto, pensar nas QNNs como um subconjunto de circuitos quânticos parametrizados, e não devemos nos deixar limitar, na exploração de circuitos quânticos úteis, pela analogia com redes neurais clássicas.

O conjunto de dados abordado nesta aula consiste em imagens contendo listras horizontais e verticais, e nosso objetivo é classificar imagens desconhecidas em uma das duas categorias dependendo da orientação da linha. Vamos fazer isso com um VQC. Ao longo do caminho, vamos discutir formas de melhorar e escalar o cálculo. O conjunto de dados aqui é excecionalmente fácil de classificar classicamente. Ele foi escolhido pela sua simplicidade para que possamos nos concentrar na parte quântica do problema e ver como um atributo do conjunto de dados pode se traduzir em uma parte de um circuito quântico. Não é razoável esperar uma aceleração quântica em casos tão simples, onde os algoritmos clássicos são tão eficientes.

Ao final desta aula você deverá ser capaz de:

  • Carregar dados de uma imagem em um circuito quântico
  • Construir um ansatz para um VQC (ou QNN) e ajustá-lo ao seu problema
  • Treinar seu VQC/QNN e usá-lo para fazer previsões precisas em dados de teste
  • Escalar o problema e reconhecer os limites dos computadores quânticos atuais

Geração de dados

Vamos começar construindo os dados. Os conjuntos de dados geralmente não são gerados explicitamente como parte do framework de padrões Qiskit. Mas o tipo e a preparação dos dados são fundamentais para aplicar computação quântica com sucesso ao aprendizado de máquina. O código abaixo define um conjunto de dados de imagens com dimensões de pixels fixas. Uma linha ou coluna inteira da imagem recebe o valor π/2\pi/2, e os demais pixels recebem valores aleatórios no intervalo (0,π/4)(0,\pi/4). Os valores aleatórios são ruído nos nossos dados. Dê uma olhada no código para ter certeza de que você entende como as imagens são geradas. Mais adiante vamos ampliar o tamanho das imagens.

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

Note que o código acima também gerou rótulos indicando se as imagens contêm uma linha vertical (+1) ou horizontal (-1). Agora vamos usar o sklearn para dividir um conjunto de dados de 100 imagens em um conjunto de treino e de teste (junto com seus rótulos correspondentes). Aqui usamos 70%70\% do conjunto de dados para treino, com os 30%30\% restantes reservados para teste.

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
)

Vamos plotar alguns elementos do nosso conjunto de dados para ver como essas linhas ficam:

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)

Saída da célula de código anterior

Cada uma dessas imagens ainda está pareada com seu rótulo em train_labels na forma de uma lista simples:

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

Classificador quântico variacional: uma primeira tentativa

Passo 1 dos padrões Qiskit: Mapear o problema para um circuito quântico

O objetivo é encontrar uma função ff com parâmetros θ\theta que mapeie um vetor de dados / imagem x\vec{x} para a categoria correta: fθ(x)±1f_\theta(\vec{x}) \rightarrow \pm1. Isso será feito usando um VQC com poucas camadas que podem ser identificadas por seus propósitos distintos:

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

Aqui, U(x)U(\vec{x}) é o circuito de codificação, para o qual temos muitas opções como visto nas aulas anteriores. W(θ)W(\theta) é um bloco de circuito variacional, ou treinável, e θ\theta é o conjunto de parâmetros a serem treinados. Esses parâmetros serão variados por algoritmos de otimização clássicos para encontrar o conjunto de parâmetros que produz a melhor classificação das imagens pelo circuito quântico. Esse circuito variacional às vezes é chamado de "ansatz". Por fim, OO é algum observável que será estimado usando a primitiva Estimator. Não há restrição que force as camadas a virem nessa ordem, ou mesmo a serem totalmente separadas. Podem existir múltiplas camadas variacionais e/ou de codificação em qualquer ordem que seja tecnicamente motivada.

Começamos escolhendo um mapa de características para codificar nossos dados. Vamos usar o z_feature_map, pois ele mantém as profundidades dos circuitos baixas em comparação com alguns outros mapeamentos de características.

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

Agora devemos decidir um ansatz a ser treinado. Há muitas considerações na hora de selecionar um ansatz. Uma descrição completa está além do escopo desta introdução; aqui simplesmente destacamos algumas categorias de considerações.

  1. Hardware: Todos os computadores quânticos modernos são mais propensos a erros e mais suscetíveis a ruído do que seus equivalentes clássicos. Usar um ansatz excessivamente profundo (especialmente em profundidade de duas qubits transpilado) não produzirá bons resultados. Um problema relacionado é que computadores quânticos têm algum layout de qubits, o que significa que alguns qubits físicos são adjacentes no computador quântico e outros podem estar muito distantes uns dos outros. Entrelaçar qubits adjacentes não aumenta muito a profundidade, mas entrelaçar qubits muito distantes pode aumentar a profundidade substancialmente, pois devemos inserir portas de swap para mover informações para qubits adjacentes a fim de que possam ser entrelaçados.
  2. O problema: Sempre que você tiver alguma informação sobre seu problema que possa guiar seu ansatz, use-a. Por exemplo, os dados desta aula são compostos por imagens de linhas horizontais e verticais. Pode-se considerar qual correlação entre cores/valores adjacentes identifica uma imagem de linha horizontal ou vertical. Quais atributos de um ansatz corresponderiam a essa correlação entre pixels adjacentes? Vamos revisitar esse ponto de forma mais técnica mais adiante nesta aula. Mas por ora, digamos simplesmente que incluir entrelaçamento e portas CNOT entre qubits correspondentes a pixels adjacentes parece uma boa ideia. No quadro geral, considere se o problema é de fato melhor resolvido com um circuito quântico, ou se podem existir algoritmos clássicos que façam um trabalho tão bom.
  3. Número de parâmetros: Cada porta quântica parametrizada independentemente no circuito aumenta o espaço a ser otimizado classicamente, e isso resulta em convergência mais lenta. Mas à medida que os problemas escalam, pode-se encontrar platôs áridos (barren plateaus). Esse termo se refere a um fenômeno em que a paisagem de otimização de um algoritmo quântico variacional se torna exponencialmente plana e sem características à medida que o tamanho do problema aumenta. Isso causa gradientes que desaparecem, tornando difícil treinar o algoritmo de forma eficaz[1]. Os platôs áridos são relevantes para algoritmos quânticos variacionais como VQCs/QNNs. É importante notar que o número crescente de parâmetros não é a única consideração para evitar platôs áridos; outras considerações incluem funções de custo globais e inicialização aleatória de parâmetros.

Nesta aula veremos alguns exemplos simples de boas práticas na construção de ansatz. Vamos primeiro tentar o ansatz abaixo. Voltaremos para revisá-lo mais tarde.

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

Com a codificação de dados e o circuito variacional preparados, podemos combiná-los para formar nosso ansatz completo. Nesse caso, os componentes do nosso circuito quântico são bastante análogos aos de redes neurais, sendo U(x)U(\vec{x}) mais similar à camada que carrega valores de entrada da imagem, e W(θ)W(\theta) semelhante à camada de "pesos" variáveis. Como essa analogia se sustenta nesse caso, estamos adotando "qnn" em algumas das nossas convenções de nomenclatura; mas essa analogia não deve ser limitante na sua exploração de VQCs.

QML_CR_background_QNN_circuit-2.png

# QNN ansatz
ansatz = qnn_circuit

# Combine the feature map with the ansatz
full_circuit = QuantumCircuit(num_qubits)
full_circuit.compose(feature_map, range(num_qubits), inplace=True)
full_circuit.compose(ansatz, range(num_qubits), inplace=True)

# Display the circuit
full_circuit.decompose().draw("mpl", style="clifford", fold=-1)

Saída da célula de código anterior

Agora devemos definir um observável para poder usá-lo na nossa função de custo. Vamos obter um valor esperado para esse observável usando o Estimator. Se selecionamos um ansatz bom e motivado pelo problema, então cada qubit conterá informações relevantes para a classificação. Pode-se adicionar camadas para combinar informações em menos qubits (chamada de camada convolucional), de forma que as medições sejam necessárias apenas em um subconjunto dos qubits do circuito (como nas redes neurais convolucionais). Ou pode-se medir algum atributo de cada qubit. Aqui vamos optar pela segunda abordagem, então incluímos um operador Z para cada qubit. Não há nada de especial em escolher ZZ, mas a escolha é bem motivada:

  • Esta é uma tarefa de classificação binária, e uma medição de ZZ pode produzir dois resultados possíveis.
  • Os autovalores de ZZ (±1\pm 1) são razoavelmente bem separados e resultam em um resultado do estimador no intervalo [-1, +1], onde 0 pode simplesmente ser usado como valor de corte.
  • É simples medir na base de Pauli Z sem overhead adicional de portas.

Então, Z é uma escolha muito natural.

from qiskit.quantum_info import SparsePauliOp

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

Temos nosso circuito quântico e o observável que queremos estimar. Agora precisamos de algumas coisas para rodar e otimizar esse circuito. Primeiro, precisamos de uma função para executar um passo forward. Note que a função abaixo recebe os input_params e weight_params separadamente. O primeiro é o conjunto de parâmetros estáticos que descrevem os dados em uma imagem, e o segundo é o conjunto de parâmetros variáveis a serem otimizados.

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

Função de perda

Em seguida, precisamos de uma função de perda para calcular a diferença entre os valores previstos e calculados dos rótulos. A função receberá os rótulos previstos pelo algoritmo e os rótulos corretos e retornará a diferença quadrática média. Existem muitas funções de perda diferentes. Aqui, o MSE é um exemplo que escolhemos.

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

Vamos também definir uma função de perda ligeiramente diferente que é função dos parâmetros variáveis (pesos), para uso pelo otimizador clássico. Essa função recebe apenas os parâmetros do ansatz como entrada; outras variáveis para o passo forward e a perda são definidas como parâmetros globais. O otimizador treinará o modelo amostrando diferentes pesos e tentando diminuir a saída da função de custo/perda.

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

Acima nos referimos ao uso de um otimizador clássico. Quando chegarmos à busca por pesos para minimizar a função de custo, vamos usar o otimizador COBYLA:

from scipy.optimize import minimize

Vamos definir algumas variáveis globais iniciais para a função de custo.

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

Passo 2 do Qiskit Patterns: Otimizar o problema para execução quântica

Começamos selecionando um backend para a execução. Neste caso, vamos usar o backend menos ocupado.

from qiskit_ibm_runtime import QiskitRuntimeService

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

Aqui otimizamos o circuito para rodar em um backend real, especificando o optimization_level e adicionando desacoplamento dinâmico. O código abaixo gera um pass manager usando pass managers predefinidos do 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,
),
]
)

Agora usamos o pass manager no circuito. As mudanças de layout resultantes também precisam ser aplicadas ao observável. Para circuitos muito grandes, as heurísticas usadas na otimização de circuitos nem sempre produzem o circuito mais eficiente e raso. Nesses casos, faz sentido executar esses pass managers várias vezes e usar o melhor circuito. Veremos isso mais adiante, quando escalarmos nosso cálculo.

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

Passo 3 do Qiskit Patterns: Executar usando Primitivos do Qiskit

Iterar sobre o dataset em lotes e épocas

Primeiro implementamos o algoritmo completo usando um simulador para depuração rápida e para estimar os erros. Podemos agora percorrer o dataset inteiro em lotes, pelo número desejado de épocas, para treinar nossa rede neural quântica.

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

Passo 4 do Qiskit Patterns: Pós-processar e retornar o resultado em formato clássico

Teste e acurácia

Agora interpretamos os resultados do treinamento. Primeiro testamos a acurácia no conjunto de treinamento.

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%

A acurácia de treinamento é apenas 60%60\%, o que definitivamente não é bom. É difícil imaginar que o desempenho do modelo no conjunto de teste possa ser melhor. Vamos verificar.

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%

O modelo não está classificando esses dados bem. Devemos nos perguntar por que isso acontece e, em particular, verificar:

  • Paramos o treinamento cedo demais? Seriam necessários mais passos de otimização?
  • Construímos um ansatz ruim? Isso pode significar muitas coisas. Quando trabalhamos em computadores quânticos reais, a profundidade do circuito é uma consideração fundamental. O número de parâmetros também pode ser importante, assim como o entrelaçamento entre qubits.
  • Combinando os dois pontos acima, construímos um ansatz com parâmetros demais para ser treinável?

Podemos começar verificando a convergência na otimização:

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

Saída da célula de código anterior

Poderíamos tentar aumentar o número de passos de otimização para garantir que o otimizador não ficou preso em um mínimo local no espaço de parâmetros. Mas parece que já convergiu razoavelmente bem. Vamos dar uma olhada mais detalhada nas imagens que não foram classificadas corretamente e tentar entender o que está acontecendo.

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)

Saída da célula de código anterior

Aqui podemos ver que a grande maioria das imagens classificadas incorretamente possui uma linha vertical. Algo no nosso modelo está falhando em capturar informações sobre elas. Você pode ter previsto isso ao analisar o primeiro circuito variacional. Vamos examiná-lo com mais atenção.

Melhorando o modelo

Passo 1 revisitado

Ao mapear o nosso problema para um circuito quântico, deveríamos ter pensado explicitamente em como a informação dos pixels adjacentes determina a classe. Para identificar linhas horizontais, queremos saber "se o pixel ii é amarelo, o pixel i+1i+1 também é amarelo" para todos os pixels ao longo de cada linha. Também queremos saber sobre linhas verticais. Mas como a classificação é binária, dá pra imaginar simplesmente que, se uma linha horizontal não for detectada, então é uma linha vertical. Nosso circuito variacional anterior continha portas CNOT entre os qubits (e portanto os pixels) 0 e 1, 1 e 2, e 2 e 3. Isso cobre qualquer linha horizontal na parte superior da imagem, mas não detecta diretamente linhas verticais, nem detecta completamente as linhas horizontais, já que ignora a linha inferior. Para detectar completamente todas as linhas horizontais, precisaríamos de um conjunto semelhante de portas CNOT entre os qubits (pixels) 4 e 5, 5 e 6, e 6 e 7. Podemos ter em mente que adicionar portas CNOT entre qubits correspondentes a linhas verticais (como 0 e 4, ou 2 e 6) também pode ser útil. Mas primeiro vamos verificar se é suficiente detectar que existe ou não existe uma linha horizontal.

# 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

Saída da célula de código anterior

Não aumentamos a profundidade do circuito. Vamos ver se aumentamos a capacidade de modelar nossas imagens.

Passo 2 revisitado

Precisaremos transpilar este novo circuito para execução em um backend quântico real. Vamos pular essa etapa por enquanto para ver se a nossa revisão do circuito variacional teve o efeito desejado nos simuladores. Vamos nos aprofundar em transpilação na próxima subseção.

Passo 3 revisitado

Agora aplicamos o modelo atualizado aos nossos dados de treinamento.

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

Passo 4 revisitado

Vamos começar verificando se o nosso otimizador convergiu completamente.

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

Saída da célula de código anterior

Aparentemente não convergiu completamente, já que a função de perda não se manteve aproximadamente estável por um número substancial de passos. Mas a função de perda já está ~60% menor do que com o circuito variacional anterior. Se este fosse um projeto de pesquisa, gostaríamos de garantir a convergência completa. Mas para fins de exploração, isso é suficiente. Vamos verificar a acurácia nos dados de treinamento e de teste.

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\%$ de acurácia em ambos os conjuntos! Nossa suspeita de que a detecção precisa de linhas horizontais seria suficiente estava correta! Além disso, o nosso mapeamento da informação necessária dos pixels para as portas CNOT no circuito quântico foi eficaz. Vamos agora ver como esse processo escala para execução em computadores quânticos reais.

## Escalando e executando em computadores quânticos reais \{#scaling-and-running-on-real-quantum-computers}

### Dados \{#data}

Vamos começar aumentando o tamanho das nossas imagens. Não há nada de especial na escolha de uma grade 6x6, exceto que ela excede o número de qubits (32) que conseguimos simular para circuitos com portas não-Clifford.

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

import numpy as np

# Total number of "pixels"/qubits
size = 36
# One dimension of the image (called vertical, but it doesn't matter). Must be a divisor of `size`
vert_size = 6
# The length of the line to be detected (yellow). Must be less than or equal to the smallest dimension of the image (`<=min(vert_size,size/vert_size)`
line_size = 6

def generate_dataset(num_images):
images = []
labels = []
hor_array = np.zeros((size - (line_size - 1) * vert_size, size))
ver_array = np.zeros((round(size / vert_size) * (vert_size - line_size + 1), size))

j = 0
for i in range(0, size - 1):
if i % (size / vert_size) <= (size / vert_size) - line_size:
for p in range(0, line_size):
hor_array[j][i + p] = np.pi / 2
j += 1

# Make two adjacent entries pi/2, then move down to the next row. Careful to avoid the "pixels" at size/vert_size - linesize, because we want to fold this list into a grid.

j = 0
for i in range(0, round(size / vert_size) * (vert_size - line_size + 1)):
for p in range(0, line_size):
ver_array[j][i + p * round(size / vert_size)] = np.pi / 2
j += 1

# Make entries pi/2, spaced by the length/rows, so that when folded, the entries appear on top of each other.

for n in range(num_images):
rng = np.random.randint(0, 2)
if rng == 0:
labels.append(-1)
random_image = np.random.randint(0, len(hor_array))
images.append(np.array(hor_array[random_image]))
# Randomly select one of the several rows you made above.
elif rng == 1:
labels.append(1)
random_image = np.random.randint(0, len(ver_array))
images.append(np.array(ver_array[random_image]))
# Randomly select one of the several rows you made above.

# Create noise
for i in range(size):
if images[-1][i] == 0:
images[-1][i] = np.random.rand() * np.pi / 4
return images, labels

hor_size = round(size / vert_size)

Como o tempo de computação quântica é um recurso valioso, vamos usar um conjunto de treinamento muito pequeno e pouquíssimas etapas de otimização. Isso será suficiente para demonstrar o fluxo de trabalho.

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)

Saída da célula de código anterior

Passo 1: Mapear o problema para um circuito quântico

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

Esta é uma profundidade de dois qubits razoável. Devemos conseguir obter resultados de alta qualidade em um computador quântico real.

# 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

Como estamos usando o z_feature_map, que não tem portas CNOT, adicionar a camada de codificação não aumenta a nossa profundidade de dois qubits. Podemos visualizar o circuito completo aqui.

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

Saída da célula de código anterior

Talvez você note que, se minimizar a profundidade de dois qubits fosse de suma importância, poderíamos realmente reduzi-la um pouco mudando a ordem das CNOTs. Por exemplo, as CNOTs em q35q_{35} e q34q_{34} poderiam ser movidas para a esquerda no diagrama do circuito acima, e poderiam ser colocadas diretamente abaixo das CNOTs em q30q_{30} e q31q_{31}, por exemplo. Para uma profundidade de porta de dois qubits igual a 5, não é óbvio que isso faça diferença após a transpilação, mas é algo a ter em mente. Se a ordem das portas CNOT for importante para corresponder logicamente ao problema em questão, a profundidade aqui está ótima. Se a ordem das CNOTs não for crítica para modelar a estrutura dos dados nas nossas imagens, então poderíamos escrever um script para reordenar essas portas CNOT a fim de minimizar a profundidade.

Também precisamos redefinir o nosso observável para as imagens maiores:

from qiskit.quantum_info import SparsePauliOp

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

Passo 2 do Qiskit Patterns: Otimizar o problema para execução quântica

Começamos selecionando um backend para execução. Neste caso, vamos usar o backend menos ocupado.

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

Mais uma vez, estamos definindo um pass manager, com o nível de otimização configurado para 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,
),
]
)

Agora vamos aplicar o pass manager várias vezes. Para circuitos muito largos ou muito profundos, pode haver grande variabilidade nas profundidades de dois qubits após a transpilação. Para esses circuitos, é importante tentar o pass manager várias vezes e usar o melhor resultado (o mais raso).

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

Vemos que, neste caso, a profundidade de dois qubits transpilada foi sempre 10. Houve uma variação pequena na profundidade de qubit único, e vamos usar o resultado mais raso. Mas nesse circuito de 36 qubits, essa não é uma melhoria crítica. Podemos visualizar esse circuito transpilado, embora nessa escala fique cada vez mais difícil de interpretar visualmente.

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

Passo 3 do Qiskit Patterns: Executar usando os Primitivos do Qiskit

Para limitar o tempo usado em computadores quânticos reais, vamos realizar apenas alguns passos de otimização aqui, e estamos fazendo isso com um conjunto de treinamento bem pequeno. Mas a escalabilidade disso para mais passos de otimização e conjuntos de dados de teste maiores deve ficar clara a partir das instruções ao longo da lição.

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

É recomendado que você salve os parâmetros de peso retornados por esse cálculo, caso queira iterar mais.

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

Podemos plotar esses primeiros passos de otimização, embora não esperemos nenhuma convergência após apenas alguns passos. Essas curvas foram relativamente planas nos primeiros passos, mesmo usando simuladores. Vale notar, no entanto, que a otimização atualmente tem 72 parâmetros livres. Isso pode ser reduzido por um fator de pelo menos 2-3 sem comprometer os resultados, por exemplo, parametrizando qubits com dados correspondentes a um subconjunto de linhas e colunas completas. De fato, o espaço de parâmetros deve ser reduzido antes de gastar mais tempo de computação quântica minimizando a função de perda.

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

Saída da célula de código anterior

Conclusão

Em resumo, nesta lição aprendemos o fluxo de trabalho para classificação binária de imagens usando uma rede neural quântica. Algumas considerações importantes em cada passo do Qiskit Patterns foram:

Passo 1: Mapear o problema para um circuito quântico

  • Carregar os dados de treinamento. Isso pode ser feito "na mão" ou usando um feature map pré-construído como z_feature_map.
  • Construir um ansatz contendo camadas de rotação e entrelaçamento adequadas para o seu problema.
  • Monitorar a profundidade do circuito para garantir resultados de qualidade em computadores quânticos.

Passo 2: Otimizar o problema para execução quântica

  • Selecionar um backend, geralmente o menos ocupado.
  • Usar um pass manager para transpilar tanto o circuito quanto os observáveis para a arquitetura do backend escolhido.
  • Para circuitos muito profundos ou largos, transpilar várias vezes e selecionar o circuito mais raso.

Passo 3: Executar usando os Primitivos do Qiskit (Runtime)

  • Realizar testes preliminares em simuladores para depurar e otimizar o seu ansatz.
  • Executar em um computador quântico da IBM®.

Passo 4: Pós-processar e retornar o resultado em formato clássico

  • Calcular a acurácia do modelo nos dados de treinamento e nos dados de teste.
  • Monitorar a convergência da otimização clássica.

Referências

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