Skip to main content
IBM Quantum Platform

Núcleos Quânticos


Introdução aos kernels quânticos

O "método de kernel quântico" refere-se a qualquer método que use computadores quânticos para estimar um kernel. Nesse contexto, "kernel" se refere à matriz do kernel ou às entradas individuais nela contidas. Lembre-se de que um mapeamento de recursos Φ(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 onde o objetivo desse mapeamento é tornar as categorias de dados separáveis por um hiperplano. A função kernel usa vetores no espaço mapeado de recursos como argumentos e retorna seu produto interno, ou seja, 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 mapas de recursos para os quais a função de kernel é fácil de avaliar. Isso geralmente significa encontrar uma função de kernel para a qual o produto interno no espaço mapeado de recursos possa ser escrito em termos dos vetores de dados originais, sem precisar construir Φ(x)\Phi(x) e Φ(y)\Phi(y). No método de kernels quânticos, o mapeamento de recursos é feito por um circuito quântico e o kernel é estimado usando medições nesse circuito e as probabilidades de medição relativas.

Nesta lição, examinaremos as profundidades dos circuitos de codificação pré-codificados que usam emaranhamento substancial e as compararemos com as profundidades dos circuitos que codificamos manualmente. Não se trata de defender um método em detrimento de outro. Você pode descobrir que os circuitos pré-codificados são muito profundos e que o emaranhamento no circuito personalizado é insuficiente para ser útil. Novamente, eles são mostrados apenas para permitir sua exploração.

Antes de analisar detalhadamente uma estimativa de matriz de kernel, vamos delinear 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 uma entrada da matriz do 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 no espaço de Hilbert dos estados do nosso computador quântico. Fazemos isso construindo um circuito dependente de dados. Há muitas maneiras de fazer isso, e a lição anterior descreveu várias opções. Você pode construir seu próprio circuito para codificar seus dados ou pode usar um mapa de recursos pré-criado, como o zz_feature_map. Nesta lição, faremos as duas coisas.

Observe que, para calcular um único elemento da matriz de kernel, queremos codificar dois pontos diferentes, para que possamos estimar seu produto interno. Um fluxo de trabalho completo de kernel quântico envolverá, é claro, muitos desses produtos internos entre vetores de dados mapeados, bem como métodos clássicos de aprendizado de máquina. Mas a etapa principal que está sendo iterada é a estimativa de um único elemento da matriz do kernel. Para isso, selecionamos um circuito quântico dependente de dados e mapeamos dois vetores de dados no espaço de recursos.

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 qubits NN estão no estado 0|0\rangle. Para ver 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 xj\vec{x}_j é Φ(xj)\Phi(\vec{x}_j), e denote os estados mapeados

ψ(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, de modo que nossa 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 no 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 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 (até 2||^2 ). A camada de medição do nosso circuito retornará as probabilidades de medição (ou as chamadas "quase-probabilidades", se determinados métodos de atenuação de erros forem usados). A probabilidade de interesse é a do estado zero, 0N|0\rangle^{\otimes N}.

Etapa 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, otimizado para a QPU selecionada

Nesta etapa, usaremos a função generate_preset_pass_manager do Qiskit para especificar uma rotina de otimização para o 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 passagem predefinido que fornece o nível mais alto de otimização. Nesse contexto, "otimização" refere-se à otimização da implementação do circuito em um computador quântico real. Isso inclui considerações como a seleção de qubits físicos para corresponder aos qubits no circuito quântico abstrato que minimizará a profundidade da porta ou a seleção de 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 nos otimizadores clássicos, como o COBYLA).

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

Etapa 3: Executar usando primitivas de Qiskit Runtime

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

Use a primitiva Sampler de Qiskit Runtime para reconstruir uma distribuição de probabilidade de estados gerados pela amostragem do circuito. Observe que isso pode ser chamado de "distribuição de quase-probabilidade", um termo que se aplica quando o ruído é um problema e quando etapas extras são introduzidas, como na atenuação de erros. Nesses casos, a soma de todas as probabilidades pode não ser exatamente igual a 1; daí o termo "quase-probabilidade".

Etapa 4: Pós-processamento, retornar o resultado no formato clássico

  • Entrada: Distribuição de probabilidade
  • Saída: Um único elemento da matriz de kernel, ou uma matriz de kernel se estiver repetindo

Calcule a probabilidade de medir 0N|0\rangle^{\otimes N} no circuito quântico e preencha a matriz do kernel na posição correspondente aos dois vetores de dados usados. Para preencher toda a matriz do kernel, precisamos executar um experimento quântico para cada entrada. Quando tivermos uma matriz de kernel, poderemos usá-la em muitos algoritmos clássicos de aprendizado de máquina que aceitam pre-calculated kernels. Por exemplo: qml_svc = SVC(kernel="precomputed"). Em seguida, podemos usar fluxos de trabalho clássicos para aplicar nosso modelo em nossos dados de teste e obter uma pontuação de precisão. Dependendo da nossa satisfação com a pontuação de precisão, talvez seja necessário revisar aspectos do nosso cálculo, como o mapa de recursos.

Esboço da lição

Nesta lição, executaremos essas etapas de várias maneiras para otimizar o uso do seu tempo em computadores quânticos reais. Aplicaremos um método de kernel quântico para

  • Uma única entrada de matriz de kernel para dados com relativamente poucos recursos, usando um backend real, para que possamos acompanhar facilmente o que está acontecendo em cada etapa.
  • Um conjunto de dados inteiro com relativamente poucos recursos, usando um backend simulado, para que possamos ver como o fluxo de trabalho quântico se conecta com os métodos clássicos de aprendizado de máquina
  • Uma única entrada de matriz de kernel para dados com muitos recursos, usando um computador quântico real. Não estimaremos uma matriz de kernel inteira para um grande conjunto de dados, a fim de respeitar o tempo em IBM® computadores quânticos.
# If you have not already, install scikit learn
#!pip install scikit-learn

Entrada de matriz de kernel único

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

Vamos primeiro considerar um conjunto de dados com apenas alguns recursos, digamos 10. O conjunto de dados pode ser tão grande quanto você quiser, pois estamos calculando os elementos da matriz do kernel um de cada vez. Precisamos de pelo menos dois pontos, portanto, começaremos com isso (no próximo exemplo, importaremos 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 site 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 de 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")

Output:

circuit depth =  9
Output of the previous code cell

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

Output:

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

No caso de circuitos complicados, essa etapa aumentará substancialmente a profundidade do circuito, pois ele é mapeado para portas nativas de computadores quânticos reais, e as informações podem precisar ser movidas de qubit para qubit. Nesse caso simples, a profundidade quase não é afetada.

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

Output:

circuit depth =  10
1

Etapa 3: Executar usando primitivas de Qiskit Runtime

A sintaxe para execução em um simulador está comentada abaixo. Para esse conjunto de dados, com um pequeno número de recursos, a execução em um simulador ainda é uma opção. Para cálculos em escala de serviços públicos, a simulação normalmente não é viável. Os simuladores só devem ser usados para depurar códigos reduzidos.

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

Etapa 4: Pós-processamento, retornar o resultado no formato clássico

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

counts.get(0, 0.0) / num_shots

Output:

0.6525

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

from qiskit.visualization import plot_distribution

plot_distribution(counts_bit)

Output:

Output of the previous code cell

Como alternativa, pode-se definir uma visualização como a que está abaixo para examinar apenas as 10 medições mais prováveis. Isso pode ser importante para a solução de problemas ou para tentar obter mais intuição para os dados. Mas a probabilidade de medição do estado zero é o nosso elemento da matriz do núcleo.

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:

Output of the previous code cell

Com base nessas informações sobre apenas um produto interno entre dois pontos de dados no espaço de recursos de dimensão mais alta, tudo o que podemos dizer é que sua sobreposição é 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 em sua natureza e serão categorizados nas mesmas classes. Ou pode ser um indicador de que nosso mapa de recursos não é eficaz no mapeamento em um espaço em que os dados semelhantes têm uma forte sobreposição e os dados diferentes têm uma pequena sobreposição. Para saber qual é a verdadeira, devemos aplicar nosso mapa de recursos a todo o conjunto de dados e verificar se a matriz de kernel resultante pode ser manipulada para separar efetivamente as classes com alta precisão.

Vale a pena observar que usamos o z_feature_map que resultou em uma baixa profundidade de transpilação de dois qubits (profundidade 1, na verdade). Se seus circuitos se tornarem muito profundos, isso certamente resultará em muito ruído, o que tornará a probabilidade de medir o estado zero muito baixa, mesmo que seu mapa de recursos seja bem compatível com seus dados. Por exemplo, uma repetição do processo acima usando zz_feature_map e , entanglement='linear', reps=1 produziu dist.get(0,0.0) = 0.0015 usando os mesmos pontos de dados. Isso se deve ao fato de que as profundidades dos circuitos e dos dois qubits são muito maiores em zz_feature_map. A figura abaixo mostra a distribuição de probabilidade para esse cálculo.

Resultados ruins de um mapa de recursos zz.

Vale a pena brincar com alguns pontos de dados da mesma categoria para ver quão baixa deve ser sua profundidade para obter bons resultados. A seguir, um conselho grosseiro que certamente terá exceções. Em geral, uma profundidade transpilada de dois qubits de 10 ou menos não deve ser um problema. Uma profundidade transpilada de dois qubits de 50 a 60 é o estado da arte e exigirá mitigação avançada de erros, entre outras ferramentas. No meio disso, seus resultados podem variar de acordo com a similaridade dos dados, a expressividade do mapa de recursos, a largura do circuito e outros fatores.

Normalmente, a etapa de pós-processamento também incluiria processos clássicos de aprendizado de máquina. Na próxima seção, estenderemos esse processo a um conjunto de dados inteiro e mostraremos o fluxo de trabalho clássico de aprendizado de máquina.

Verifique sua compreensão

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

  • 2102^{10} ou 1024.

Suponhamos que alguém sem experiência em computação quântica tente usar um circuito quântico com profundidade de dois qubits muito elevada e não utilize mitigação de erros. Suponhamos ainda que isso resulte em uma taxa de erro de 10% em cada qubit. Se o elemento da matriz do kernel verdadeiro (sem erros) correspondente a este circuito for muito grande, digamos, de ordem 1.0, qual seria a probabilidade de medir todos os 10 qubits no estado em que cada qubit está em 0|0\rangle?

  • A probabilidade de cada qubit ser encontrado corretamente no estado |0> é 0.90. A probabilidade de que todos os 10 qubits sejam 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 isso no contexto da estimativa do kernel quântico.

  • Nesse fluxo de trabalho QKE, nossas estimativas são baseadas nas medições do estado zero, ou seja, o estado no qual cada qubit é encontrado no estado 0|0\rangle. Circuitos muito profundos introduzirão altas taxas de erro. Quando essa taxa de erro é composta por muitos qubits, isso reduzirá substancialmente a probabilidade de medir o estado zero.


Matriz kernel completa

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

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

Agora, importaremos um conjunto de dados existente para nossa classificação. Esse conjunto de dados consiste em 128 linhas (pontos de dados) e 14 recursos 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 acessar o conjunto de dados e visualizar sua estrutura aqui.

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

Output:

--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á estaremos nos preparando para armazenar várias saídas, construindo uma matriz de kernel e uma matriz de teste de dimensões apropriadas.

# 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 recursos para codificar e mapear nossos dados clássicos em um circuito quântico. Podemos construir nosso próprio mapa de características ou usar um mapa pré-fabricado. Sinta-se à vontade para modificar o mapa de recursos abaixo ou voltar para ZFeatureMap. Mas sempre preste atenção à profundidade do circuito. Lembre-se de que, no exemplo anterior de 6 qubits, a profundidade do circuito transpilado era intratavelmente alta ao usar zz_feature_map. À medida que a escala e a complexidade do circuito aumentam, a profundidade pode aumentar rapidamente até chegar a um ponto em que o ruído sobrecarregue nossos resultados. Sempre que você souber algo sobre sua estrutura de dados que possa informar qual estrutura de mapa de recursos seria mais útil, é recomendável criar seu próprio mapa de recursos 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)

Etapas 2 e 3: Otimize o problema e execute usando primitivas

Construiremos um circuito de sobreposição e, se estivéssemos executando em um computador quântico real neste exemplo, nós o otimizaríamos para execução como antes. Mas, neste caso, pretendemos percorrer todos os pontos de dados e calcular a matriz de 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, devemos otimizar nosso circuito para cada par de pontos de dados. Portanto, as etapas 2 e 3 seriam realizadas em conjunto nas várias iterações.

A célula de código abaixo faz exatamente o mesmo processo que antes para um único par de pontos de dados. Desta vez, ele é simplesmente executado dentro de dois loops for , e há uma linha adicional no final kernel_matrix[x_1,x_2] = ... para armazenar os resultados de cada cálculo. Observe que aproveitamos a simetria de uma matriz de kernel para reduzir o número de cálculos em 1/2. Também definimos simplesmente os elementos diagonais como 1, como deveriam ser na ausência de ruído. Dependendo da sua implementação e da precisão necessária, você também pode usar os elementos diagonais para estimar o ruído ou aprender sobre ele para fins de atenuação de erros.

Depois que a matriz do kernel tiver sido totalmente preenchida, repetimos o processo para os dados de teste e preenchemos a matriz test_matrix. Na verdade, essa também é uma matriz de kernel; simplesmente lhe demos 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")

Output:

training done
test matrix done

Etapa 4: Pós-processamento, retornar o resultado no formato clássico

Agora que temos uma matriz de kernel e uma matriz test_matrix com formato semelhante dos métodos de kernel quântico, podemos aplicar algoritmos clássicos de aprendizado de máquina para fazer previsões sobre nossos dados de teste e verificar sua precisão. Começaremos importando o sklearn.svc do Scikit-Learn, um classificador de vetor de suporte (SVC). Devemos especificar que queremos que o SVC use nosso kernel pré-computado usando 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 do kernel e os rótulos de treinamento para obter um ajuste. SVC.score pontuará nossos dados de teste em relação a esse ajuste usando nossa matriz test_matrix e retornará nossa precisão.

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

Output:

Precomputed kernel classification test score: 1.0

Vemos que a precisão do nosso modelo treinado foi de 100%. Isso é ótimo e mostra que o QKE pode funcionar. Mas isso é muito diferente da vantagem quântica. Os núcleos clássicos provavelmente também teriam sido capazes de resolver esse problema de classificação com 100% de precisão. Há muito trabalho a ser feito para caracterizar diferentes tipos de dados e relacionamentos de dados para ver onde os kernels quânticos serão mais úteis na atual era dos utilitários. Deixamos a cargo do aluno modificar partes desse fluxo de trabalho e estudar a eficácia de vários mapas de recursos quânticos. Aqui estão alguns aspectos a serem considerados:

  • Qual é a robustez da precisão? Isso se aplica a tipos amplos de dados ou apenas a esses dados de treinamento específicos?
  • Que estrutura em seus dados o faz suspeitar que um mapa de recursos quânticos seja útil?
  • Como a precisão é afetada pelo aumento/diminuição da quantidade de dados de treinamento?
  • Quais mapas de recursos você pode usar e como os resultados variam de acordo com os mapas de recursos?
  • Como a precisão e o tempo de execução são afetados pelo aumento do número de recursos?
  • Quais tendências, se houver, você espera que se mantenham nos computadores quânticos reais?

Ampliação para mais recursos e qubits

Nesta seção, repetiremos o cálculo de um único elemento da matriz, mas para um número muito maior de recursos, 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 mostrado sem consumir muito de seu tempo alocado em computadores quânticos.

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

Assumiremos como ponto de partida um conjunto de dados em que cada ponto de dados tem 42 recursos. Como no primeiro exemplo, calcularemos um único elemento da matriz de kernel, exigindo dois pontos de dados. Os dois pontos abaixo têm 42 recursos 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 site zz_feature_map produziu circuitos bastante profundos no caso de relativamente poucos recursos (14 recursos). À medida que aumentamos o número de recursos, precisamos monitorar de perto a profundidade do circuito. Para ilustrar isso, primeiro tentaremos usar o site zz_feature_map e verificaremos 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")

Output:

circuit depth =  251
two-qubit depth 165

Conforme descrito anteriormente, a determinação exata da profundidade excessiva é diferenciada. Mas uma profundidade de dois qubits superior a 100, mesmo antes da transpilação, não é viável. É por isso que os mapas de recursos personalizados foram enfatizados ao longo desta lição. Se você sabe algo sobre a estrutura de todo o seu conjunto de dados, deve projetar um mapa de emaranhamento 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 relação a qualquer consideração detalhada da estrutura de 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 nos preocuparemos em verificar as profundidades ainda, pois o que realmente importa é a profundidade transpilada de dois qubits.

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

Output:

<IBMBackend('ibm_brisbane')>

Em trabalhos de pequena escala, um gerenciador de passagem predefinido geralmente retornará o mesmo circuito com a mesma profundidade, de forma confiável. Porém, em circuitos muito grandes e complexos, o gerenciador de passagens pode retornar circuitos transpilados diferentes a cada vez que for executado. Isso ocorre porque ele está usando heurística e porque circuitos muito grandes terão um cenário complicado de possíveis otimizações. Muitas vezes é útil transpilar algumas vezes e fazer o circuito mais raso. Isso só introduz uma sobrecarga clássica e pode melhorar substancialmente os resultados do computador quântico.

Aqui, transpilamos o circuito de sobreposição unitário 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())

Output:

circuit depth =  61
print(transpiled_depths)
print(transpiled_twoqubit_depths)

Output:

[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 da porta com diferentes passagens de transpilação. Nosso circuito ainda não é profundo/amplo o suficiente para ver a variação nas profundidades transpiladas de dois qubits. Usaremos o transpiled_qcs[1], que tem uma profundidade de 60, apenas um pouco menor do que a profundidade do circuito mais profundo obtido, que foi de 77.

overlap_ibm = transpiled_qcs[1]

Etapa 3: Executar usando primitivas de Qiskit Runtime

À medida que nos aproximamos da utilidade, os simuladores não serão ú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()

Etapa 4: Pós-processamento, retornar o resultado no formato clássico

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

counts.get(0, 0.0) / num_shots

Output:

0.0138

Esse processo para o elemento único da matriz de kernel pode ser repetido entre outros pares de dados em seu conjunto para obter a matriz de kernel completa. A dimensão da matriz do kernel é ditada pelo número de pontos em seus dados de treinamento, não pelo número de recursos. Portanto, o custo de computação da manipulação da matriz do kernel em um modelo preditivo não é dimensionado como o número de recursos ou qubits. Mesmo para conjuntos de dados relativamente pequenos com grande número de recursos, os dados ainda precisariam ser combinados com um mapa de recursos que produzisse uma classificação eficaz.

Escalabilidade e trabalhos futuros

O método kernel exige que meçamos o 0|0\rangle com a maior precisão possível. Mas os erros de porta e de leitura significam que há alguma probabilidade diferente de zero pp de que um determinado qubit seja erroneamente medido como estando no estado 1|1\rangle. Mesmo com a simplificação excessiva de que a probabilidade de 0|0\rangle deve ser 100%100\%, para muitos recursos codificados em, digamos, NN bits, a probabilidade de medir corretamente todos os bits para ser 0|0\rangle é reduzida para (1p)N(1-p)^N. À medida que NN se torna grande, esse método se torna cada vez menos confiável. Superar essa dificuldade e dimensionar a estimativa do kernel para cada vez mais recursos é uma área de pesquisa atual. Para saber mais sobre esse problema, consulte 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 que espere o que será possível na era da correção de erros.

Revisar

O cálculo de um kernel quântico envolve

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

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

Alguns aspectos importantes a serem considerados ao usar kernels quânticos incluem:

  • É provável que o conjunto de dados se beneficie dos métodos de kernel quântico?
  • Experimente diferentes mapas de recursos e esquemas de emaranhamento.
  • A profundidade do circuito é aceitável?
  • Tente executar um gerenciador de passes várias vezes e use o circuito com a menor profundidade possível.

Os métodos de kernel quântico são ferramentas potencialmente poderosas, desde que haja uma correspondência adequada entre os conjuntos de dados com recursos quânticos-amenizáveis e um mapa de recursos quânticos adequado. Para entender melhor onde os núcleos quânticos podem ser úteis, recomendamos a leitura de Liu, Arunachalam & Temme (2021).

Esta página foi útil?
Relate um bug, erro de digitação ou solicite conteúdo no GitHub.