Pular para o conteúdo principal

Kernels Quânticos

Introdução aos kernels quânticos

O "método de kernel quântico" se refere a qualquer método que use computadores quânticos para estimar um kernel. Nesse contexto, "kernel" se refere à matriz de kernel ou a entradas individuais dela. Lembre-se de que um mapeamento de características Φ(x)\Phi(\vec{x}) é um mapeamento de xRd\vec{x}\in \mathbb{R}^d para Φ(x)Rd,\Phi(\vec{x})\in \mathbb{R}^{d'}, onde geralmente d>dd'>d e o objetivo desse mapeamento é tornar as categorias dos dados separáveis por um hiperplano. A função kernel recebe vetores no espaço de características mapeadas como argumentos e retorna seu produto interno, isto é, K:Rd×RdRK:\mathbb{R}^d\times\mathbb{R}^d\rightarrow \mathbb{R} com K(x,y)=Φ(x)Φ(y)K(x,y) = \langle \Phi(x)|\Phi(y)\rangle. Classicamente, estamos interessados em mapeamentos de características para os quais a função kernel seja fácil de avaliar. Isso geralmente significa encontrar uma função kernel cujo produto interno no espaço de características mapeadas possa ser escrito em termos dos vetores de dados originais, sem precisar construir Φ(x)\Phi(x) e Φ(y)\Phi(y) explicitamente. No método de kernels quânticos, o mapeamento de características é feito por um circuito quântico, e o kernel é estimado usando medições nesse circuito e as probabilidades relativas das medições.

Nesta aula vamos examinar as profundidades de circuitos de codificação pré-programados que usam entrelaçamento substancial e compará-las às profundidades de circuitos que codificamos manualmente. Isso não é uma defesa de um método em detrimento do outro. Você pode descobrir que os circuitos pré-programados são muito profundos e que o entrelaçamento no circuito construído à mão é insuficiente para ser útil. Mais uma vez, eles são mostrados apenas para permitir a sua exploração.

Antes de percorrer detalhadamente uma estimativa de matriz de kernel, vamos descrever o fluxo de trabalho usando a linguagem dos padrões do Qiskit.

Passo 1: Mapear entradas clássicas para um problema quântico

  • Entrada: Conjunto de dados de treinamento
  • Saída: Circuito abstrato para calcular um elemento da matriz de kernel

Dado o conjunto de dados, o ponto de partida é codificar os dados em um circuito quântico. Em outras palavras, precisamos mapear nossos dados para o espaço de Hilbert de estados do nosso computador quântico. Fazemos isso construindo um circuito dependente dos dados. Existem muitas formas de fazer isso, e a aula anterior descreveu várias opções. Você pode construir seu próprio circuito para codificar os dados, ou usar um mapeamento de características pré-pronto como o zz_feature_map. Nesta aula, faremos as duas coisas.

Note que, para calcular um único elemento da matriz de kernel, vamos querer codificar dois pontos diferentes, para que possamos estimar seu produto interno. Um fluxo de trabalho completo de kernel quântico vai envolver, é claro, muitos desses produtos internos entre vetores de dados mapeados, além de métodos clássicos de aprendizado de máquina. Mas o passo central que é iterado é a estimativa de um único elemento da matriz de kernel. Para isso, selecionamos um circuito quântico dependente dos dados e mapeamos dois vetores de dados para o espaço de características.

Classical_Review_background_kernel_circuit

Para a tarefa de gerar uma matriz de kernel, estamos particularmente interessados na probabilidade de medir o estado 0N|0\rangle^{\otimes N}, no qual todos os NN qubits estão no estado 0|0\rangle. Para entender isso, considere que o circuito responsável pela codificação e mapeamento de um vetor de dados xi\vec{x}_i pode ser escrito como Φ(xi)\Phi(\vec{x}_i), e o responsável pela codificação e mapeamento de xj\vec{x}_j é Φ(xj)\Phi(\vec{x}_j). Denote os estados mapeados como

ψ(xi)=Φ(xi)0N|\psi(\vec{x}_i)\rangle = \Phi(\vec{x}_i)|0\rangle^{\otimes N} ψ(xj)=Φ(xj)0N.|\psi(\vec{x}_j)\rangle = \Phi(\vec{x}_j)|0\rangle^{\otimes N}.

Esses estados são o mapeamento dos dados para dimensões mais altas, então a entrada de kernel desejada é o produto interno

ψ(xj)ψ(xi)=0NΦ(xj)Φ(xi)0N.\langle\psi(\vec{x}_j)|\psi(\vec{x}_i)\rangle = \langle 0 |^{\otimes N}\Phi^\dagger(\vec{x}_j)\Phi(\vec{x}_i)|0\rangle^{\otimes N}.

Se operarmos sobre o estado inicial padrão 0N|0\rangle^{\otimes N} com ambos os circuitos Φ(xj)\Phi^\dagger(\vec{x}_j) e Φ(xi)\Phi(\vec{x}_i), a probabilidade de então medir o estado 0N|0\rangle^{\otimes N} é

P0=0NΦ(xj)Φ(xi)0N2.P_0 = |\langle0|^{\otimes N}\Phi^\dagger(\vec{x}_j)\Phi(\vec{x}_i)|0\rangle^{\otimes N}|^2.

Esse é exatamente o valor que queremos (a menos de 2||^2). A camada de medição do nosso circuito retornará probabilidades de medição (ou as chamadas "quasi-probabilidades", se certos métodos de mitigação de erros forem usados). A probabilidade de interesse é a do estado zero, 0N|0\rangle^{\otimes N}.

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

  • Entrada: Circuito abstrato, não otimizado para um backend específico
  • Saída: Circuito alvo e observável, otimizados para a QPU selecionada

Neste passo, vamos usar a função generate_preset_pass_manager do Qiskit para especificar uma rotina de otimização para nosso circuito em relação ao computador quântico real no qual planejamos executar o experimento. Definimos optimization_level=3, o que significa que usaremos o gerenciador de passagens predefinido que oferece o maior nível de otimização. Nesse contexto, "otimização" se refere a otimizar a implementação do circuito em um computador quântico real. Isso inclui considerações como selecionar qubits físicos que correspondam aos qubits do circuito quântico abstrato de forma a minimizar a profundidade de portas, ou selecionar qubits físicos com as menores taxas de erro disponíveis. Isso não está diretamente relacionado à otimização do problema de aprendizado de máquina (como em otimizadores clássicos como o COBYLA).

Dependendo de como você implementa o passo 2, pode ser necessário otimizar o circuito mais de uma vez, já que cada par de pontos envolvido em um elemento de matriz produz um circuito diferente a ser medido.

Passo 3: Executar usando os Primitivos do Qiskit Runtime

  • Entrada: Circuito alvo
  • Saída: Distribuição de probabilidade

Use o primitivo Sampler do Qiskit Runtime para reconstruir uma distribuição de probabilidade de estados obtida por amostragem do circuito. Note que você pode encontrar esse processo referido como "distribuição de quasi-probabilidade", um termo aplicável quando o ruído é um problema e etapas extras são introduzidas, como na mitigação de erros. Nesses casos, a soma de todas as probabilidades pode não ser exatamente igual a 1; daí o termo "quasi-probabilidade".

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

  • Entrada: Distribuição de probabilidade
  • Saída: Um único elemento da matriz de kernel, ou a matriz de kernel completa se repetido

Calcule a probabilidade de medir 0N|0\rangle^{\otimes N} no circuito quântico e preencha a matriz de kernel na posição correspondente aos dois vetores de dados usados. Para preencher toda a matriz de kernel, precisamos executar um experimento quântico para cada entrada. Depois de ter uma matriz de kernel, podemos usá-la em muitos algoritmos clássicos de aprendizado de máquina que aceitam kernels pré-calculados. Por exemplo: qml_svc = SVC(kernel="precomputed"). Em seguida, podemos usar fluxos de trabalho clássicos para aplicar nosso modelo aos dados de teste e obter uma pontuação de acurácia. Dependendo da nossa satisfação com essa pontuação, pode ser necessário revisitar aspectos do nosso cálculo, como o mapeamento de características.

Resumo da aula

Nesta aula, vamos executar esses passos de várias formas para fazer o melhor uso possível do seu tempo em computadores quânticos reais. Vamos aplicar um método de kernel quântico a:

  • Um único elemento da matriz de kernel para dados com relativamente poucas características, usando um backend real, para que possamos acompanhar facilmente o que acontece em cada passo.
  • Um conjunto de dados completo com relativamente poucas características, usando um backend simulado, para que possamos ver como o fluxo de trabalho quântico se conecta com métodos clássicos de aprendizado de máquina.
  • Um único elemento da matriz de kernel para dados com muitas características, usando um computador quântico real. Não vamos estimar uma matriz de kernel inteira para um conjunto de dados grande, a fim de respeitar o tempo nos computadores quânticos da IBM®.
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy pandas qiskit qiskit-ibm-runtime scikit-learn
# If you have not already, install scikit learn
#!pip install scikit-learn

Elemento único da matriz de kernel

Passo 1: Mapear entradas clássicas para um problema quântico

Vamos considerar primeiro um conjunto de dados com apenas algumas características, digamos 10. O conjunto de dados pode ser tão grande quanto você quiser, já que estamos calculando os elementos da matriz de kernel um de cada vez. Precisamos de pelo menos dois pontos, então começaremos com isso (no próximo exemplo, vamos importar um conjunto de dados completo). Vamos importar alguns pacotes necessários:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# Two mock data points, including category labels, as in training
small_data = [
[-0.194, 0.114, -0.006, 0.301, -0.359, -0.088, -0.156, 0.342, -0.016, 0.143, 1],
[-0.1, 0.002, 0.244, 0.127, -0.064, -0.086, 0.072, 0.043, -0.053, 0.02, -1],
]

# Data points with labels removed, for inner product
train_data = [small_data[0][:-1], small_data[1][:-1]]

Podemos tentar usar o z_feature_map.

# from qiskit.circuit.library import zz_feature_map
# fm = zz_feature_map(feature_dimension=np.shape(train_data)[1], entanglement='linear', reps=1)

from qiskit.circuit.library import z_feature_map

fm = z_feature_map(feature_dimension=np.shape(train_data)[1])

unitary1 = fm.assign_parameters(train_data[0])
unitary2 = fm.assign_parameters(train_data[1])

Os dois unitários acima correspondem exatamente a U1U_1 e U2U_2 descritos na introdução. Podemos combiná-los usando unitary_overlap. Como sempre, queremos ficar de olho na profundidade do nosso circuito.

from qiskit.circuit.library import unitary_overlap

overlap_circ = unitary_overlap(unitary1, unitary2)
overlap_circ.measure_all()

print("circuit depth = ", overlap_circ.decompose().depth())
overlap_circ.decompose().draw("mpl", scale=0.6, style="iqp")
circuit depth =  9

Output of the previous code cell

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

Começamos selecionando o backend menos ocupado e, em seguida, otimizamos nosso circuito para execução nesse backend.

# Import needed packages
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService

# Get the least busy backend
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=fm.num_qubits
)
print(backend)
<IBMBackend('ibm_brisbane')>
# Apply level 3 optimization to our overlap circuit
pm = generate_preset_pass_manager(optimization_level=3, backend=backend)
overlap_ibm = pm.run(overlap_circ)

Para circuitos complicados, esse passo vai aumentar substancialmente a profundidade do circuito ao mapear para portas nativas de computadores quânticos reais, e as informações podem precisar ser movidas de qubit em qubit. Nesse caso simples, a profundidade praticamente não é afetada.

print("circuit depth = ", overlap_ibm.decompose().depth())
overlap_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)
circuit depth =  10
1

Passo 3: Executar usando os Primitivos do Qiskit Runtime

A sintaxe para execução em um simulador está comentada abaixo. Para este conjunto de dados, com um número pequeno de características, a execução em um simulador ainda é uma opção. Para cálculos em escala de utilidade, a simulação geralmente não é viável. Os simuladores devem ser usados apenas para depurar código em escala reduzida.

# Run this for a simulator
# from qiskit.primitives import StatevectorSampler

# from qiskit_ibm_runtime import Options, Session, Sampler

# num_shots = 10000

# Evaluate the problem using state vector-based primitives from Qiskit
# sampler = StatevectorSampler()
# results = sampler.run([overlap_circ], shots=num_shots).result()
# .get_counts() returns counts associated with a state labeled by bit results such as |001101...01>.
# counts_bit = results[0].data.meas.get_counts()
# .get_int_counts returns the same counts, but labeled by integer equivalent of the above bit string.
# counts = results[0].data.meas.get_int_counts()
# Benchmarked on an Eagle processor, 7-11-24, took 4 sec.

# Import our runtime primitive
from qiskit_ibm_runtime import Session, SamplerV2 as Sampler

num_shots = 10000

# Use sampler and get the counts

sampler = Sampler(mode=backend)
results = sampler.run([overlap_ibm], shots=num_shots).result()
# .get_counts() returns counts associated with a state labeled by bit results such as |001101...01>.
counts_bit = results[0].data.meas.get_counts()
# .get_int_counts returns the same counts, but labeled by integer equivalent of the above bit string.
counts = results[0].data.meas.get_int_counts()

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

Como descrito na introdução, a medição mais útil aqui é a probabilidade de medir o estado zero 00000|00000\rangle.

counts.get(0, 0.0) / num_shots
0.6525

Esse é o resultado que queríamos: uma estimativa do produto interno (a menos do módulo ao quadrado) dos vetores correspondentes a dois pontos de dados. Se quisermos ver a distribuição completa das probabilidades de medição (ou quasi-probabilidades), podemos fazer isso usando a função plot_distribution como mostrado abaixo. Observa-se que, para um grande número de qubits, imagens como essa rapidamente se tornam intratáveis.

from qiskit.visualization import plot_distribution

plot_distribution(counts_bit)

Output of the previous code cell

Alternativamente, pode-se definir uma visualização como a abaixo para olhar apenas para as 10 medições mais prováveis. Isso pode ser importante para solucionar problemas ou tentar obter mais intuição sobre os dados. Mas a probabilidade de medição do estado zero é o nosso elemento da matriz de kernel.

def visualize_counts(probs, num_qubits):
"""Visualize the outputs from the Qiskit Sampler primitive."""
zero_prob = probs.get(0, 0.0)
top_10 = dict(sorted(probs.items(), key=lambda item: item[1], reverse=True)[:10])
top_10.update({0: zero_prob})
by_key = dict(sorted(top_10.items(), key=lambda item: item[0]))
xvals, yvals = list(zip(*by_key.items()))
xvals = [bin(xval)[2:].zfill(num_qubits) for xval in xvals]
plt.bar(xvals, yvals)
plt.xticks(rotation=75)
plt.title("Results of sampling")
plt.xlabel("Measured bitstring")
plt.ylabel("Counts")
plt.show()

visualize_counts(counts, overlap_circ.num_qubits)

Output of the previous code cell

A partir dessas informações sobre apenas um produto interno entre dois pontos de dados no espaço de características de dimensão superior, tudo o que podemos dizer é que a sobreposição deles é bastante grande em comparação com a sobreposição máxima (que seria 1,0). Isso pode ser um indicador de que esses dois pontos de dados são de alguma forma semelhantes por natureza e serão categorizados na mesma classe. Ou pode ser um indicador de que nosso mapeamento de características não é eficaz para mapear para um espaço onde dados semelhantes têm grande sobreposição e dados diferentes têm pequena sobreposição. Para saber qual das opções é verdadeira, precisamos aplicar nosso mapeamento de características a todo o conjunto de dados e ver se a matriz de kernel resultante pode ser manipulada para separar as classes com alta acurácia.

Vale notar que usamos o z_feature_map, que resultou em profundidade transpilada de dois qubits baixa (profundidade 1, de fato). Se seus circuitos se tornarem muito profundos, isso certamente resultará em muito ruído, e isso vai fazer com que a probabilidade de medir o estado zero seja muito baixa, mesmo que seu mapeamento de características esteja bem combinado com seus dados. Por exemplo, uma repetição do processo acima usando zz_feature_map e , entanglement='linear', reps=1 gerou dist.get(0,0.0) = 0.0015 usando os mesmos pontos de dados. Isso se deve às profundidades de circuito e de dois qubits muito maiores do zz_feature_map. A figura abaixo mostra a distribuição de probabilidade para esse cálculo.

Bad results from a zz feature map.

Vale a pena experimentar alguns pontos de dados da mesma categoria para ver quão baixa deve ser sua profundidade para obter bons resultados. O que se segue é um conselho geral que certamente terá exceções. Em geral, uma profundidade transpilada de dois qubits de 10 ou menos não deve ser problema. Uma profundidade transpilada de dois qubits de 50 a 60 é estado da arte e vai exigir mitigação avançada de erros, entre outras ferramentas. No intervalo entre esses valores, seus resultados podem variar conforme a similaridade dos dados, a expressividade do mapeamento de características, a largura do circuito e outros fatores. Normalmente, o passo de pós-processamento também incluiria processos clássicos de aprendizado de máquina. Na próxima seção, vamos estender esse processo a um conjunto de dados completo e mostrar o fluxo de trabalho clássico de aprendizado de máquina.

Verifique seu entendimento

Leia as perguntas abaixo, pense nas suas respostas e clique nos triângulos para ver as soluções.

Em um circuito quântico de 10 qubits, em geral, quantos estados diferentes podem ser medidos?

Resposta:

2102^{10} ou 1024.

Suponha que alguém iniciante em computação quântica tente usar um circuito quântico com profundidade de dois qubits muito alta, sem usar mitigação de erros. Suponha ainda que isso resulte em uma taxa de erro de 10% em cada qubit. Se o elemento verdadeiro (sem erros) da matriz de kernel correspondente a esse circuito for muito grande, digamos 1,0, qual seria a probabilidade de medir todos os 10 qubits no estado com cada qubit |0>?

Resposta:

A probabilidade de cada qubit ser corretamente encontrado no estado |0> é 0,90. A probabilidade de todos os 10 qubits serem encontrados no estado correto é 0,90100,90^{10}, ou cerca de 35%.

Explique com suas próprias palavras por que é tão importante monitorar as profundidades dos circuitos. Isso é verdade em geral, mas explique no contexto da estimativa de kernel quântico.

Resposta:

Neste fluxo de trabalho de estimativa de kernel quântico (QKE), nossas estimativas se baseiam nas medições do estado zero, ou seja, o estado no qual cada qubit é encontrado no estado 0|0\rangle. Circuitos muito profundos introduzem altas taxas de erro. Quando essa taxa de erro é acumulada sobre muitos qubits, isso reduz substancialmente a probabilidade de medir o estado zero.

Matriz kernel completa

Nesta seção, vamos estender o processo acima para a classificação binária de um conjunto de dados completo. Isso introduz dois componentes importantes: (1) agora podemos aplicar aprendizado de máquina clássico no pós-processamento, e (2) podemos obter pontuações de acurácia para o nosso treinamento.

Passo 1: Mapear entradas clássicas para um problema quântico

Agora vamos importar um conjunto de dados existente para a nossa classificação. Esse conjunto de dados consiste em 128 linhas (pontos de dados) e 14 características em cada ponto. Há um 15º elemento que indica a categoria binária de cada ponto (±1\pm 1). O conjunto de dados é importado abaixo, ou você pode acessá-lo e visualizar sua estrutura aqui.

Vamos usar os primeiros 90 pontos de dados para treinamento e os próximos 30 pontos para teste.

!wget https://raw.githubusercontent.com/qiskit-community/prototype-quantum-kernel-training/main/data/dataset_graph7.csv

df = pd.read_csv("dataset_graph7.csv", sep=",", header=None)

# Prepare training data

train_size = 90
X_train = df.values[0:train_size, :-1]
train_labels = df.values[0:train_size, -1]

# Prepare testing data
test_size = 30
X_test = df.values[train_size : train_size + test_size, :-1]
test_labels = df.values[train_size : train_size + test_size, -1]
--2024-07-11 23:05:22--  https://raw.githubusercontent.com/qiskit-community/prototype-quantum-kernel-training/main/data/dataset_graph7.csv
Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.110.133, 185.199.111.133, 185.199.109.133, ...
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.110.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 49405 (48K) [text/plain]
Saving to: ‘dataset_graph7.csv.15’

dataset_graph7.csv. 100%[===================>] 48.25K --.-KB/s in 0.02s

2024-07-11 23:05:23 (2.11 MB/s) - ‘dataset_graph7.csv.15’ saved [49405/49405]

Já vamos nos preparar para armazenar múltiplas saídas construindo uma matriz kernel e uma matriz de teste com as dimensões adequadas.

# Empty kernel matrix
num_samples = np.shape(X_train)[0]
kernel_matrix = np.full((num_samples, num_samples), np.nan)
test_matrix = np.full((test_size, num_samples), np.nan)

Agora criamos um mapa de características para codificar e mapear nossos dados clássicos em um circuito quântico. Somos livres para construir nosso próprio mapa de características ou usar um pré-fabricado. Fique à vontade para modificar o mapa de características abaixo, ou voltar ao ZFeatureMap. Mas preste sempre atenção à profundidade do circuito. Lembre-se de que no exemplo anterior de 6 qubits a profundidade do circuito transpilado era intratável ao usar o zz_feature_map. À medida que a escala e a complexidade do circuito aumentam, a profundidade pode crescer rapidamente a um ponto em que o ruído compromete os resultados. Sempre que você souber algo sobre a estrutura dos seus dados que possa indicar qual estrutura de mapa de características seria mais útil, é recomendável criar seu próprio mapa de características personalizado que aproveite esse conhecimento.

from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit

# Prepare feature map for computing overlap
num_features = np.shape(X_train)[1]
num_qubits = int(num_features / 2)

# To use a custom feature map use the lines below.
entangler_map = [[0, 2], [3, 4], [2, 5], [1, 4], [2, 3], [4, 6]]

fm = QuantumCircuit(num_qubits)
training_param = Parameter("θ")
feature_params = ParameterVector("x", num_qubits * 2)
fm.ry(training_param, fm.qubits)
for cz in entangler_map:
fm.cz(cz[0], cz[1])
for i in range(num_qubits):
fm.rz(-2 * feature_params[2 * i + 1], i)
fm.rx(-2 * feature_params[2 * i], i)

Passos 2 e 3: Otimizar o problema e executar usando primitivas

Vamos construir um circuito de sobreposição e, se estivéssemos rodando em um computador quântico real neste exemplo, o otimizaríamos para execução como antes. Mas neste caso, pretendemos percorrer todos os pontos de dados e calcular a matriz kernel completa. Para cada par de vetores de dados xi\vec{x}_i e xj\vec{x}_j, criamos um circuito de sobreposição diferente. Portanto, precisamos otimizar nosso circuito para cada par de pontos de dados. Assim, os passos 2 e 3 seriam executados juntos nas múltiplas iterações.

O bloco de código abaixo executa exatamente o mesmo processo de antes para um único par de pontos de dados. Desta vez, ele é simplesmente executado dentro de dois laços for, e há a linha adicional no final kernel_matrix[x_1,x_2] = ... para armazenar os resultados de cada cálculo. Note que aproveitamos a simetria de uma matriz kernel para reduzir o número de cálculos pela metade. Também simplesmente definimos os elementos da diagonal como 1, já que é o esperado na ausência de ruído. Dependendo da sua implementação e da precisão necessária, você também poderia usar os elementos diagonais para estimar o ruído ou aprender sobre ele para fins de mitigação de erros.

Uma vez que a matriz kernel esteja completamente preenchida, repetimos o processo para os dados de teste e preenchemos a test_matrix. Isso também é uma matriz kernel; simplesmente damos a ela um nome diferente para distinguir as duas.

# To use a simulator
from qiskit.primitives import StatevectorSampler

# Remember to insert your token in the QiskitRuntimeService constructor to use real quantum computers
# service = QiskitRuntimeService()
# backend = service.least_busy(
# operational=True, simulator=False, min_num_qubits=fm.num_qubits
# )

num_shots = 10000

# Evaluate the problem using state vector-based primitives from Qiskit.
sampler = StatevectorSampler()

for x1 in range(0, train_size):
for x2 in range(x1 + 1, train_size):
unitary1 = fm.assign_parameters(list(X_train[x1]) + [np.pi / 2])
unitary2 = fm.assign_parameters(list(X_train[x2]) + [np.pi / 2])

# Create the overlap circuit
overlap_circ = unitary_overlap(unitary1, unitary2)
overlap_circ.measure_all()

# These lines run the qiskit sampler primitive.
counts = (
sampler.run([overlap_circ], shots=num_shots)
.result()[0]
.data.meas.get_int_counts()
)

# Assign the probability of the 0 state to the kernel matrix, and the transposed element (since this is an inner product)
kernel_matrix[x1, x2] = counts.get(0, 0.0) / num_shots
kernel_matrix[x2, x1] = counts.get(0, 0.0) / num_shots
# Fill in on-diagonal elements with 1, again, since this is an inner-product corresponding to probability (or alter the code to check these entries and verify they yield 1)
kernel_matrix[x1, x1] = 1

print("training done")

# Similar process to above, but for testing data.
for x1 in range(0, test_size):
for x2 in range(0, train_size):
unitary1 = fm.assign_parameters(list(X_test[x1]) + [np.pi / 2])
unitary2 = fm.assign_parameters(list(X_train[x2]) + [np.pi / 2])

# Create the overlap circuit
overlap_circ = unitary_overlap(unitary1, unitary2)
overlap_circ.measure_all()

counts = (
sampler.run([overlap_circ], shots=num_shots)
.result()[0]
.data.meas.get_int_counts()
)

test_matrix[x1, x2] = counts.get(0, 0.0) / num_shots

print("test matrix done")
training done
test matrix done

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

Agora que temos uma matriz kernel e uma test_matrix de formato similar, obtidas por métodos de kernel quântico, podemos aplicar algoritmos clássicos de aprendizado de máquina para fazer previsões sobre os dados de teste e verificar a acurácia. Vamos começar importando o sklearn.svc do Scikit-Learn, um classificador de vetores de suporte (SVC). Precisamos especificar que queremos que o SVC use nosso kernel pré-computado com kernel = precomputed.

# import a support vector classifier from a classical ML package.
from sklearn.svm import SVC

# Specify that you want to use a pre-computed kernel matrix
qml_svc = SVC(kernel="precomputed")

Usando SVC.fit, podemos agora alimentar a matriz kernel e os rótulos de treinamento para obter um ajuste. SVC.score irá então avaliar os dados de teste com base nesse ajuste usando nossa test_matrix, e retornar a acurácia.

# Feed in the pre-computed matrix and the labels of the training data. The classical algorithm gives you a fit.
qml_svc.fit(kernel_matrix, train_labels)

# Now use the .score to test your data, using the matrix of test data, and test labels as your inputs.
qml_score_precomputed_kernel = qml_svc.score(test_matrix, test_labels)
print(f"Precomputed kernel classification test score: {qml_score_precomputed_kernel}")
Precomputed kernel classification test score: 1.0

Vemos que a acurácia do nosso modelo treinado foi de 100%. Isso é ótimo e mostra que o QKE pode funcionar. Mas isso é muito diferente de vantagem quântica. Kernels clássicos provavelmente também teriam conseguido resolver esse problema de classificação com 100% de acurácia. Há muito trabalho a ser feito para caracterizar diferentes tipos de dados e relações entre dados, a fim de identificar onde os kernels quânticos serão mais úteis na era de utilidade atual. Deixamos para o aprendiz modificar partes deste fluxo de trabalho e estudar a eficácia de vários mapas de características quânticos. Aqui estão alguns pontos a considerar:

  • Quão robusta é a acurácia? Ela se mantém para tipos amplos de dados ou apenas para esses dados de treinamento específicos?
  • Que estrutura nos seus dados faz você suspeitar que um mapa de características quântico é útil?
  • Como a acurácia é afetada pelo aumento/redução da quantidade de dados de treinamento?
  • Quais mapas de características você pode usar e como os resultados variam com eles?
  • Como a acurácia e o tempo de execução são afetados pelo aumento do número de características?
  • Quais tendências, se houver, você espera que se mantenham em computadores quânticos reais?

Escalando para mais características e qubits

Nesta seção, vamos repetir o cálculo de um único elemento de matriz, mas para um número muito maior de características, esboçando o caminho para escalar em direção à utilidade. A restrição a um único elemento de matriz é feita para que o processo possa ser demonstrado sem consumir muito do seu tempo alocado em computadores quânticos.

Passo 1: Mapear entradas clássicas para um problema quântico

Vamos assumir como ponto de partida um conjunto de dados em que cada ponto tem 42 características. Como no primeiro exemplo, vamos calcular um único elemento da matriz kernel, o que requer dois pontos de dados. Os dois pontos abaixo têm 42 características e uma única variável de categoria (±1\pm 1).

# Two mock data points, including category labels, as in training

large_data = [
[
-0.028,
-1.49,
-1.698,
0.107,
-1.536,
-1.538,
-1.356,
-1.514,
-0.109,
-1.8,
-0.122,
-1.651,
-1.955,
-0.123,
-1.732,
0.091,
-0.048,
-0.128,
-0.026,
0.082,
-1.263,
0.065,
0.004,
-0.055,
-0.08,
-0.173,
-1.734,
-0.39,
-1.451,
0.078,
-1.578,
-0.025,
-0.184,
-0.119,
-1.336,
0.055,
-0.204,
-1.578,
0.132,
-0.121,
-1.599,
-0.187,
-1,
],
[
-1.414,
-1.439,
-1.606,
0.246,
-1.673,
0.002,
-1.317,
-1.262,
-0.178,
-1.814,
0.013,
-1.619,
-1.86,
-0.25,
-0.212,
-0.214,
-0.033,
0.071,
-0.11,
-1.607,
0.441,
-0.143,
-0.009,
-1.655,
-1.579,
0.381,
-1.86,
-0.079,
-0.088,
-0.058,
-1.481,
-0.064,
-0.065,
-1.507,
0.177,
-0.131,
-0.153,
0.07,
-1.627,
0.593,
-1.547,
-0.16,
-1,
],
]
train_data = [large_data[0][:-1], large_data[1][:-1]]

Lembre-se de que o zz_feature_map produziu circuitos bastante profundos no caso de relativamente poucas características (14 características). À medida que aumentamos o número de características, precisamos monitorar de perto a profundidade do circuito. Para ilustrar isso, vamos primeiro tentar usar o zz_feature_map e verificar a profundidade do circuito resultante.

from qiskit.circuit.library import zz_feature_map

fm = zz_feature_map(
feature_dimension=np.shape(train_data)[1], entanglement="linear", reps=1
)

unitary1 = fm.assign_parameters(train_data[0])
unitary2 = fm.assign_parameters(train_data[1])
from qiskit.circuit.library import unitary_overlap

overlap_circ = unitary_overlap(unitary1, unitary2)
overlap_circ.measure_all()

print("circuit depth = ", overlap_circ.decompose(reps=2).depth())
print(
"two-qubit depth",
overlap_circ.decompose().depth(lambda instr: len(instr.qubits) > 1),
)
# overlap_circ.draw("mpl", scale=0.6, style="iqp")
circuit depth =  251
two-qubit depth 165

Como descrito antes, determinar exatamente quão profundo é profundo demais é uma questão sutil. Mas uma profundidade de duas qubits acima de 100, mesmo antes da transpilação, é inviável. É por isso que mapas de características personalizados foram enfatizados ao longo desta lição. Se você conhece algo sobre a estrutura do seu conjunto de dados completo, deve projetar um mapa de entrelaçamento com essa estrutura em mente. Aqui, como estamos calculando apenas o produto interno entre dois desses pontos de dados, priorizamos a baixa profundidade do circuito em detrimento de qualquer consideração detalhada sobre a estrutura dos dados.

from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit

# Prepare feature map for computing overlap

entangler_map = [
[3, 4],
[2, 5],
[1, 4],
[2, 3],
[4, 6],
[7, 9],
[10, 11],
[9, 12],
[8, 11],
[9, 10],
[11, 13],
[14, 16],
[17, 18],
[16, 19],
[15, 18],
[16, 17],
[18, 20],
]
# Use the entangler map above to build a feature map

num_features = np.shape(train_data)[1]
num_qubits = int(num_features / 2)

fm = QuantumCircuit(num_qubits)
training_param = Parameter("θ")
feature_params = ParameterVector("x", num_qubits * 2)
fm.ry(training_param, fm.qubits)
for cz in entangler_map:
fm.cz(cz[0], cz[1])
for i in range(num_qubits):
fm.rz(-2 * feature_params[2 * i + 1], i)
fm.rx(-2 * feature_params[2 * i], i)
from qiskit.circuit.library import unitary_overlap

# Assign features of each data point to a unitary, an instance of the general feature map.

unitary1 = fm.assign_parameters(list(train_data[0]) + [np.pi / 2])
unitary2 = fm.assign_parameters(list(train_data[1]) + [np.pi / 2])

# Create the overlap circuit

overlap_circ = unitary_overlap(unitary1, unitary2)
overlap_circ.measure_all()

Não vamos nos preocupar em verificar as profundidades ainda, já que o que realmente importa é a profundidade de duas qubits após a transpilação.

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

Começamos selecionando o backend menos ocupado e, em seguida, otimizamos nosso circuito para rodar nesse backend.

# Import needed packages
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService

# Get the least busy backend
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=fm.num_qubits
)
print(backend)
<IBMBackend('ibm_brisbane')>

Em jobs de pequena escala, um gerenciador de passes predefinido frequentemente retorna o mesmo circuito com a mesma profundidade, de forma confiável. Mas em circuitos muito grandes e complexos, o gerenciador de passes pode retornar circuitos transpilados diferentes a cada execução. Isso ocorre porque ele usa heurísticas e porque circuitos muito grandes têm um cenário complexo de possíveis otimizações. É frequentemente útil transpilar algumas vezes e usar o circuito mais raso. Isso introduz apenas sobrecarga clássica e pode melhorar substancialmente os resultados do computador quântico.

Aqui, transpilamos o circuito de sobreposição unitária 20 vezes e observamos as profundidades dos circuitos obtidos.

# Apply level 3 optimization to our overlap circuit
transpiled_qcs = []
transpiled_depths = []
transpiled_twoqubit_depths = []
for i in range(1, 20):
pm = generate_preset_pass_manager(optimization_level=3, backend=backend)
overlap_ibm = pm.run(overlap_circ)
transpiled_qcs.append(overlap_ibm)
transpiled_depths.append(overlap_ibm.decompose().depth())
transpiled_twoqubit_depths.append(
overlap_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)
)

print("circuit depth = ", overlap_ibm.decompose().depth())
circuit depth =  61
print(transpiled_depths)
print(transpiled_twoqubit_depths)
[61, 60, 60, 69, 60, 60, 60, 65, 60, 60, 69, 61, 77, 77, 65, 60, 60, 77, 61]
[13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13]

Aqui você pode ver que há alguma variação na profundidade total de portas com diferentes passes de transpilação. Nosso circuito ainda não é profundo/largo o suficiente para apresentar variação nas profundidades de duas qubits transpiladas. Vamos usar o transpiled_qcs[1], que tem uma profundidade de 60, ligeiramente inferior à profundidade do circuito mais profundo obtido, que foi 77.

overlap_ibm = transpiled_qcs[1]

Passo 3: Executar usando as Primitivas do Qiskit Runtime

À medida que escalamos em direção à utilidade, os simuladores deixam de ser úteis. Apenas a sintaxe para computadores quânticos reais é mostrada aqui.

# Run on ibm_osaka, 7-12-24, required 22 sec.

# Import our runtime primitive
from qiskit_ibm_runtime import SamplerV2 as Sampler

# Open a Runtime session:
session = Session(backend=backend)
num_shots = 10000
# Use sampler and get the counts

sampler = Sampler(mode=session)
options = sampler.options
options.dynamical_decoupling.enable = True
options.twirling.enable_gates = True
counts = (
sampler.run([overlap_ibm], shots=num_shots).result()[0].data.meas.get_int_counts()
)

# Close session after done
session.close()

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

Como descrito na introdução, a medição mais útil aqui é a probabilidade de medir o estado zero 00000|00000\rangle.

counts.get(0, 0.0) / num_shots
0.0138

Esse processo para um único elemento da matriz kernel poderia ser repetido entre outros pares de dados do seu conjunto para obter a matriz kernel completa. A dimensão da matriz kernel é determinada pelo número de pontos no seu conjunto de treinamento, não pelo número de características. Portanto, o custo computacional de manipular a matriz kernel em um modelo preditivo não escala com o número de características ou qubits. Mesmo para conjuntos de dados relativamente pequenos com grande número de características, os dados ainda precisariam ser associados a um mapa de características que produza uma classificação eficaz.

Escalabilidade e trabalhos futuros

O método kernel exige que medimos o 0|0\rangle com a maior precisão possível. Mas erros de portas e erros de leitura significam que há uma probabilidade não nula pp de que qualquer qubit seja erroneamente medido no estado 1|1\rangle. Mesmo com a simplificação excessiva de que a probabilidade de 0|0\rangle deveria ser 100%100\%, para muitas características codificadas em, digamos, NN bits, a probabilidade de medir corretamente todos os bits como 0|0\rangle se reduz a (1p)N(1-p)^N. À medida que NN aumenta, esse método se torna cada vez menos confiável. Superar essa dificuldade e escalar a estimativa de kernel para cada vez mais características é uma área de pesquisa atual. Para saber mais sobre esse problema, veja este trabalho de Thanasilp, Wang, Cerezo e Holmes. Recomendamos que você explore o que pode ser feito com os computadores quânticos atuais e também aguarde com expectativa o que será possível na era da correção de erros.

Revisão

Calcular um kernel quântico envolve:

  • calcular entradas da matriz kernel, usando pares de pontos de dados de treinamento
  • codificar os dados e mapeá-los por meio de um mapeamento de características
  • otimizar seu circuito para execução em computadores quânticos reais / backends

O kernel quântico pode então ser usado em algoritmos clássicos de aprendizado de máquina, como nesta lição.

Algumas coisas importantes a ter em mente ao usar kernels quânticos incluem:

  • O conjunto de dados tem probabilidade de se beneficiar dos métodos de kernel quântico?
  • Experimente diferentes mapas de características e esquemas de entrelaçamento.
  • A profundidade do circuito é aceitável?
  • Tente executar o gerenciador de passes várias vezes e use o circuito de menor profundidade que você conseguir.

Os métodos de kernel quântico são ferramentas potencialmente poderosas quando há uma correspondência adequada entre conjuntos de dados com características adequadas para quantum e um mapa de características quânticas apropriado. Para entender melhor onde os kernels quânticos provavelmente serão úteis, recomendamos a leitura de Liu, Arunachalam & Temme (2021).