Pular para o conteúdo principal

Treinamento de kernel quântico

Estimativa de uso: menos de um minuto em um processador Heron r3 (NOTA: Esta é apenas uma estimativa. Seu tempo de execução pode variar.)

Resultados de aprendizagem

Após concluir este tutorial, você poderá compreender as seguintes informações:

  • Métodos de kernel e seus usos
  • Kernels quânticos e como podem fornecer espaços de características aprimorados
  • Construção de circuitos de kernel quântico
  • Como treinar um kernel quântico usando um padrão Qiskit: mapear, otimizar, executar e pós-processar

Pré-requisitos

É recomendado que você se familiarize com kernels quânticos, por que são importantes e como são usados na prática.

Também é útil ter uma compreensão básica de teoria de grupos.

Contexto

Os métodos de kernel são comuns em aplicações de aprendizado de máquina. Neste contexto, "kernel" refere-se à matriz de kernel ou às entradas individuais desta. 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. É sabido que computadores quânticos podem codificar dados em espaços de características aprimorados quanticamente, substituindo efetivamente os análogos clássicos. Para xR\vec{x} \in \mathbb{R} e Ψ(x)Rd\Psi(\vec{x}) \in \mathbb{R}^{d'}, tipicamente 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 interno: 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 interno no espaço mapeado por características pode ser escrito em termos dos vetores de dados originais e Ψ(x)\Psi(\vec{x}) e Ψ(y)\Psi(\vec{y}) não precisam ser construídos. No caso de kernels quânticos, o mapeamento de características é realizado por um circuito quântico, e o kernel é estimado usando as probabilidades de medição amostradas do circuito.

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

Requisitos

Antes de iniciar este tutorial, certifique-se de ter o seguinte instalado:

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

Configuração

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy pandas qiskit qiskit-ibm-runtime
# 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 com simulador de pequena escala

Nesta seção, percorremos as quatro etapas do padrão Qiskit em uma instância de sete qubits do problema de rotulagem de cosets com erro e avaliamos uma única entrada da matriz de kernel usando a primitiva StatevectorSampler do Qiskit. Um simulador de vetor de estado é exato (exceto pelo ruído de shots) e nos mostra o método de ponta a ponta sem consumir tempo de QPU. Em seguida, repetimos a mesma instância em hardware real na seção de exemplo de hardware.

Etapa 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 de kernel.

O problema de classificação binária que buscamos resolver aqui é denominado "rotulagem de cosets com erro." O conjunto de dados de treinamento de entrada contém uma estrutura de grupo, consistindo em dois cosets formados por um grupo e subgrupo. O grupo é tomado como G=SU(2)nG = SU(2)^{\otimes n} para qubits, que é o grupo unitário especial de 2×22 \times 2 matrizes e tem ampla aplicabilidade na natureza; por exemplo, o Modelo Padrão da física de partículas. Tomamos o subgrupo (estabilizador de 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 estabilizador tal que Dsψ=ψ, sSgraphD_s | \psi \rangle = | \psi \rangle,~ \forall s \in S_\text{graph}. Por fim, definimos dois cosets esquerdos C±=c±SgraphC_\pm = c_\pm S_\text{graph} sorteando dois c±Gc_\pm \in G aleatoriamente.

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

Criamos o circuito quântico usado para avaliar uma entrada na matriz de kernel. Os dados de entrada são usados para determinar os ângulos de rotação para as portas parametrizadas do circuito. Para simplificar, usaremos as amostras de dados x1=14 e x2=19.

Nota: 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 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.
  • Saída: Circuito alvo, otimizado para a QPU selecionada.

Para o caminho do simulador de vetor de estado usado nesta seção, nenhuma otimização específica para o backend é necessária: o circuito abstrato pode ser amostrado diretamente. Exercemos esta etapa no exemplo de hardware abaixo, onde o circuito é transpilado contra uma QPU real usando generate_preset_pass_manager com optimization_level=3.

Etapa 3: Executar usando primitivas Qiskit

  • Entrada: Circuito abstrato.
  • Saída: Distribuição de quasi-probabilidade.

Use a primitiva StatevectorSampler do Qiskit para reconstruir uma distribuição de quasi-probabilidade dos estados produzidos pela amostragem do circuito. Para a tarefa de gerar uma matriz de 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 of the previous code cell

Etapa 4: Pós-processar e retornar o resultado no formato clássico desejado

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

Calcule a probabilidade de medir 0|0 \rangle no circuito de sobreposição e preencha a matriz de kernel na posição correspondente às amostras representadas por este 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]}")
Fidelity (simulator): 0.8261

Exemplo de hardware

Uma matriz de kernel quântico tem 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 cresce com o tamanho do mapa de características. Como resultado, escalar este tutorial para um problema maior tem dois custos compostos: o tempo de QPU por matriz de kernel cresce quadraticamente com NN, e a profundidade de unitary_overlap (que compõe o mapa de características com seu adjunto) corrói a fidelidade no tamanho e conectividade do sistema do hardware atual. Para manter a demonstração curta e fazer uma comparação limpa, executamos portanto a mesma instância de sete qubits do exemplo de pequena escala em uma QPU real e comparamos a fidelidade de uma única entrada da matriz de 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]}")
Using backend: ibm_pittsburgh

Output of the previous code cell

Fidelity (hardware): 0.7517

Para preencher toda a matriz de kernel, executaríamos 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; vermelho mais escuro indica fidelidades mais próximas de 1.0.

kernel_matrix.png

Próximos passos

Recommendations

Se você achou este trabalho interessante, pode se interessar pelo seguinte material: