Skip to main content
IBM Quantum Platform

Treinamento de kernel quantum

Estimativa de tempo de execução: menos de um minuto em um processador Heron r3 (NOTA: Trata-se apenas de uma estimativa.) (O tempo de execução pode variar.)


Resultados do aprendizado

Ao concluir este tutorial, você deverá compreender as seguintes informações:

  • Métodos do kernel e suas aplicações
  • Núcleos quânticos e como eles podem proporcionar espaços de características aprimorados
  • Construção de circuitos de kernel quântico
  • Como treinar um kernel quântico usando um padrão do Qiskit : mapear, otimizar, executar e pós-processar

Pré-requisitos

Recomenda-se que você se familiarize com os kernels quânticos, com a importância deles e com a forma como são utilizados na prática.

Também é útil ter um conhecimento básico de teoria dos grupos.


Segundo plano

Os métodos de kernel são comuns em aplicações de aprendizado de máquina. Nesse contexto, "kernel" refere-se à matriz do kernel ou às entradas individuais nela contidas. Em geral, um kernel é uma medida de similaridade entre dados codificados em um espaço de características de alta dimensão e pode ser utilizado, por exemplo, em tarefas de classificação com máquinas de vetores de suporte.

Os métodos de kernel quântico são aqueles que utilizam computadores quânticos para estimar um kernel. Sabe-se que os computadores quânticos podem codificar dados em espaços de características aprimorados quânticamente, substituindo efetivamente seus análogos clássicos. Para xR\vec{x} \in \mathbb{R} e Ψ(x)Rd\Psi(\vec{x}) \in \mathbb{R}^{d'}, normalmente com d>dd' >d, Ψ(x)\Psi(\vec{x}) é um mapa de características, xΨ(x)\vec{x} \mapsto \Psi(\vec{x}). O objetivo de Ψ(x)\Psi(\vec{x}) é fazer com que as categorias de dados sejam separadas por um hiperplano. Tomando os vetores no espaço mapeado por características como argumentos, a função kernel K(x,y)=Ψ(x)Ψ(y)K(\vec{x}, \vec{y}) = \langle{\Psi(\vec{x}) | \Psi(\vec{y}) \rangle{}} retorna seu produto escalar: K:RdK: \mathbb{R}^d \rightarrow Rd\mathbb{R}^d. Classicamente, os mapas de características de interesse são aqueles nos quais a função kernel pode ser facilmente avaliada; ou seja, quando o produto escalar no espaço mapeado por características pode ser escrito em termos dos vetores de dados originais e não é necessário construir Ψ(x)\Psi(\vec{x}) e Ψ(y)\Psi(\vec{y}). No caso dos kernels quânticos, o mapeamento de características é realizado por um circuito quântico, e o kernel é estimado a partir das probabilidades de medição obtidas a partir do circuito.

Este tutorial mostra como criar um padrão no Qiskit para avaliar entradas em uma matriz de kernel quântico utilizada para classificação binária.


Requisitos

Antes de iniciar este tutorial, verifique se você tem os seguintes itens instalados:

  • Qiskit SDK v2.3.1 ou posterior, com suporte à visualização
  • Qiskit Runtime v0.44.0 ou posterior (pip install qiskit-ibm-runtime)

Instalação

# General Imports and helper functions
import urllib.request

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


from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit
from qiskit.circuit.library import unitary_overlap
from qiskit.primitives import StatevectorSampler
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

from qiskit_ibm_runtime import QiskitRuntimeService, Sampler

# Download the dataset (portable across platforms)
urllib.request.urlretrieve(
    "https://raw.githubusercontent.com/qiskit-community/prototype-quantum-kernel-training/main/data/dataset_graph7.csv",
    "dataset_graph7.csv",
)


def visualize_counts(res_counts, num_qubits, num_shots):
    """Visualize the outputs from the Qiskit Sampler primitive."""
    zero_prob = res_counts.get(0, 0.0)
    top_10 = dict(
        sorted(res_counts.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]))
    x_vals, y_vals = list(zip(*by_key.items()))
    x_vals = [bin(x_val)[2:].zfill(num_qubits) for x_val in x_vals]
    y_vals_prob = []
    for t in range(len(y_vals)):
        y_vals_prob.append(y_vals[t] / num_shots)
    y_vals = y_vals_prob
    plt.bar(x_vals, y_vals)
    plt.xticks(rotation=75)
    plt.title("Results of sampling")
    plt.xlabel("Measured bitstring")
    plt.ylabel("Probability")
    plt.show()


def get_training_data():
    """Read the training data."""
    df = pd.read_csv("dataset_graph7.csv", sep=",", header=None)
    training_data = df.values[:20, :]
    ind = np.argsort(training_data[:, -1])
    X_train = training_data[ind][:, :-1]

    return X_train

Exemplo de simulador em pequena escala

Nesta seção, apresentamos as quatro etapas do padrão Qiskit em uma instância de sete qubits do problema de rotulagem de classes com erros e avaliamos uma única entrada da matriz do kernel utilizando a StatevectorSampler primitiva do Qiskit. Um simulador de vetor de estado é exato (com exceção do ruído de disparo) e nos mostra o método de ponta a ponta sem consumir tempo da QPU. Em seguida, repetimos o mesmo procedimento em hardware real na seção de exemplos de hardware.

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.

O problema de classificação binária que pretendemos resolver aqui é conhecido como " rotulagem de classes com erro " O conjunto de dados de treinamento de entrada contém uma estrutura de grupo, composta por dois classes formadas por um grupo e um subgrupo. Considera-se que o grupo é o grupo de G=SU(2)nG = SU(2)^{\otimes n} es para qubits, que é o grupo unitário especial das matrizes 2×22 \times 2 es e tem ampla aplicabilidade na natureza; por exemplo, no Modelo Padrão da física de partículas. Consideramos o subgrupo (estabilizador do grafo) Sgraph<GS_\text{graph} < G, com Sgraph={Xik:(k,i)EZk}iV}S_\text{graph} = \langle \{ X_i \otimes _{k:(k,i) \in \mathcal{E}} Z_k\} _{i \in \mathcal{V}} \} \rangle, para um grafo com arestas E\mathcal{E} e vértices V\mathcal{V}. Observe que os estabilizadores fixam um estado do estabilizador tal que Dsψ=ψ, sSgraphD_s | \psi \rangle = | \psi \rangle,~ \forall s \in S_\text{graph}. Por fim, definimos dois cosets à esquerda C±=c±SgraphC_\pm = c_\pm S_\text{graph}, escolhendo aleatoriamente dois c±Gc_\pm \in G.

Para obter mais detalhes sobre o conjunto de dados e como ele é gerado, consulte este notebook do Quantum Kernel Training Toolkit.

Criamos o circuito quântico utilizado para calcular um elemento da matriz do kernel. Os dados de entrada são utilizados para determinar os ângulos de rotação das portas parametrizadas do circuito. Para simplificar, utilizaremos as amostras x1=14 de dados e x2=19.

Observação: o conjunto de dados usado neste tutorial pode ser baixado aqui.

# Prepare training data
X_train = get_training_data()

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

# Prepare feature map for computing overlap
num_features = np.shape(X_train)[1]
num_qubits = int(num_features / 2)
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)

# Assign tunable parameter to known optimal value and set the data params for
# first two samples
x1 = 14
x2 = 19
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()
overlap_circ.draw("mpl", scale=0.6, style="iqp")

Output:

Output of the previous code cell

Etapa 2: Otimizar o problema para execução em hardware quântico

  • Entrada: Circuito abstrato, não otimizado para um backend específico.
  • Resultado: Circuito de destino, otimizado para a QPU selecionada.

Para o caminho do simulador de vetor de estado utilizado nesta seção, não é necessária nenhuma otimização específica do backend: o circuito abstrato pode ser amostrado diretamente. Aplicamos essa etapa no exemplo de hardware abaixo, no qual o circuito é compilado para uma QPU real usando generate_preset_pass_manager com optimization_level=3.

Passo 3: Execute usando Qiskit primitives

  • Entrada: Circuito abstrato.
  • Resultado: Distribuição de quase-probabilidade.

Use a StatevectorSampler primitiva do Qiskit para reconstruir uma distribuição de quase-probabilidade dos estados obtidos a partir da amostragem do circuito. Para a tarefa de gerar uma matriz do kernel, estamos particularmente interessados na probabilidade de medir o estado |0>.

sampler = StatevectorSampler()

# Execute and get counts
num_shots = 10_000
results = sampler.run([overlap_circ], shots=num_shots).result()
counts = results[0].data.meas.get_int_counts()

# Plot counts
visualize_counts(counts, num_qubits, num_shots)

Output:

Output of the previous code cell

Etapa 4: Pós-processamento e retorno do resultado no formato clássico desejado

  • Entrada: Distribuição de probabilidade.
  • Resultado: Um único elemento da matriz do kernel.

Calcule a probabilidade de medir um valor de 0|0 \rangle no circuito de sobreposição e preencha a matriz do kernel na posição correspondente às amostras representadas por esse circuito de sobreposição específico (linha 15, coluna 20).

kernel_matrix[x1, x2] = counts.get(0, 0.0) / num_shots
print(f"Fidelity (simulator): {kernel_matrix[x1, x2]}")

Output:

Fidelity (simulator): 0.8261

Exemplo de hardware

Uma matriz de kernel quântico possui O(N2)\mathcal{O}(N^2) entradas para NN amostras de treinamento, e cada entrada requer a execução de um circuito de sobreposição cuja profundidade de portas de dois qubits aumenta com o tamanho do mapa de características. Consequentemente, a adaptação deste tutorial a um problema de maior escala acarreta dois custos cumulativos: o tempo de processamento da QPU por matriz do kernel cresce quadraticamente com o NN o, e a profundidade de unitary_overlap (que compõe o mapa de características com seu adjunto) prejudica a fidelidade, dadas as dimensões do sistema e a conectividade do hardware atual. Para manter a demonstração breve e fazer uma comparação clara, executamos, portanto, a mesma instância de sete qubits do exemplo em pequena escala em uma QPU real e comparamos a fidelidade de uma única entrada da matriz do kernel com o valor do simulador calculado acima.

# ------------------------------ Step 1 ------------------------------
# Prepare training data
X_train = get_training_data()

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

# Prepare feature map for computing overlap
num_features = np.shape(X_train)[1]
num_qubits = int(num_features / 2)
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)

# Assign tunable parameter to known optimal value and
# set the data params for first two samples
x1 = 14
x2 = 19
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()

# ------------------------------ Step 2 ------------------------------
service = QiskitRuntimeService()
# backend = service.least_busy(
#    operational=True, simulator=False, min_num_qubits=overlap_circ.num_qubits
# )
backend = service.backend("ibm_pittsburgh")
print(f"Using backend: {backend.name}")
pm = generate_preset_pass_manager(optimization_level=3, backend=backend)
overlap_ibm = pm.run(overlap_circ)

# ------------------------------ Step 3 ------------------------------
sampler = Sampler(mode=backend)
sampler.options.environment.job_tags = ["TUT_QKT"]

num_shots = 10_000
results = sampler.run([overlap_ibm], shots=num_shots).result()
counts = results[0].data.meas.get_int_counts()
visualize_counts(counts, num_qubits, num_shots)

# ------------------------------ Step 4 ------------------------------
kernel_matrix[x1, x2] = counts.get(0, 0.0) / num_shots
print(f"Fidelity (hardware): {kernel_matrix[x1, x2]}")

Output:

Using backend: ibm_pittsburgh
Output of the previous code cell
Fidelity (hardware): 0.7517

Para preencher toda a matriz do kernel, teríamos que realizar um experimento quântico para cada uma de suas N(N+1)/2N(N+1)/2 entradas únicas. A figura abaixo mostra a matriz resultante para este conjunto de dados; o vermelho mais escuro indica fidelidades mais próximas de 1.0.

kernel_matrix.png

Próximas etapas

Recomendações

Se você achou este trabalho interessante, talvez se interesse pelo seguinte material:

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