Pular para o conteúdo principal

Codificação de dados

Introdução e notação

Para usar um algoritmo quântico, os dados clássicos precisam ser inseridos de alguma forma num circuito quântico. Isso costuma ser chamado de codificação de dados, mas também é denominado carregamento de dados. Relembrando as lições anteriores, temos a noção de mapeamento de características (feature mapping): um mapeamento das características dos dados de um espaço para outro. Simplesmente transferir dados clássicos para um computador quântico é uma espécie de mapeamento e poderia ser chamado de mapeamento de características. Na prática, os mapeamentos de características embutidos no Qiskit (como ZFeatureMap e ZZFeatureMap) costumam incluir camadas de rotação e camadas de entrelaçamento que estendem o estado a muitas dimensões no espaço de Hilbert. Esse processo de codificação é uma parte crítica dos algoritmos de aprendizado de máquina quântico e afeta diretamente suas capacidades computacionais.

Algumas das técnicas de codificação apresentadas abaixo podem ser simuladas de forma eficiente de modo clássico; isso é particularmente fácil de perceber em métodos de codificação que geram estados produto (ou seja, que não entrelaçam qubits). Lembre também que a vantagem quântica é mais provável de aparecer quando a complexidade quântica do conjunto de dados é bem compatível com o método de codificação. Portanto, é muito provável que você acabe escrevendo seus próprios circuitos de codificação. Aqui apresentamos uma grande variedade de estratégias de codificação possíveis simplesmente para que você possa compará-las e ver o que é viável. Existem algumas afirmações bem gerais que podem ser feitas sobre a utilidade das técnicas de codificação. Por exemplo, efficient_su2 (veja abaixo) com um esquema de entrelaçamento completo tem muito mais chance de capturar características quânticas dos dados do que métodos que geram estados produto (como z_feature_map). Mas isso não significa que efficient_su2 seja suficiente, ou suficientemente bem ajustado ao seu conjunto de dados, para produzir uma aceleração quântica. Isso requer uma análise cuidadosa da estrutura dos dados sendo modelados ou classificados. Há também um equilíbrio delicado com a profundidade do circuito, já que muitos mapas de características que entrelaçam completamente os qubits num circuito produzem circuitos muito profundos — profundos demais para obter resultados utilizáveis nos computadores quânticos atuais.

Notação

Um conjunto de dados é um conjunto de MM vetores de dados: X={x(j)j[M]}\text{X} = \{\vec{x}^{(j)}\,|\,j\in [M]\}, onde cada vetor é NN-dimensional, isto é, x(j)=(x1(j),,xN(j))RN\vec{x}^{(j)}=(\vec{x}^{(j)}_1,\ldots,\vec{x}^{(j)}_N)\in\mathbb{R}^N. Isso pode ser estendido para características de dados complexas. Nesta lição, podemos ocasionalmente usar essas notações para o conjunto completo (X)(\text{X}) e seus elementos específicos como x(j)\vec{x}^{(j)}. Mas nos referiremos principalmente ao carregamento de um único vetor do nosso conjunto de dados por vez, e frequentemente nos referiremos simplesmente a um único vetor de NN características como x\vec{x}.

Além disso, é comum usar o símbolo Φ(x)\Phi(\vec{x}) para se referir ao mapeamento de características Φ\Phi do vetor de dados x\vec{x}. Em computação quântica especificamente, é comum se referir a mapeamentos usando U(x),U(\vec{x}), uma notação que reforça a natureza unitária dessas operações. Poder-se-ia usar corretamente o mesmo símbolo para ambos; ambos são mapeamentos de características. Ao longo deste curso, tendemos a usar:

  • Φ(x)\Phi(\vec{x}) quando discutimos mapeamentos de características em aprendizado de máquina em geral, e
  • U(x)U(\vec{x}) quando discutimos implementações em circuitos de mapeamentos de características.

Normalização e perda de informação

No aprendizado de máquina clássico, as características dos dados de treinamento são frequentemente "normalizadas" ou reescalonadas, o que geralmente melhora o desempenho do modelo. Uma forma comum de fazer isso é usando a normalização min-max ou a padronização. Na normalização min-max, as colunas de características da matriz de dados X\text{X} (digamos, a característica kk) são normalizadas:

xk(i)=xk(i)min{xk(j)x(j)[X]}max{xk(j)x(j)[X]}min{xk(j)x(j)[X]}x^{'(i)}_k = \frac{x^{(i)}_k - \text{min}\{x^{(j)}_k\,|\,\vec{x}^{(j)}\in [\text{X}]\}}{\text{max}\{x^{(j)}_k\,|\,\vec{x}^{(j)}\in [\text{X}]\}-\text{min}\{x^{(j)}_k\,|\,\vec{x}^{(j)}\in [\text{X}]\}}

onde min e max se referem ao mínimo e ao máximo da característica kk sobre os MM vetores de dados no conjunto X\text{X}. Todos os valores de características ficam então no intervalo unitário: xk(i)[0,1]x^{'(i)}_k \in [0,1] para todo i[M]i\in [M], k[N]k\in[N].

A normalização também é um conceito fundamental em mecânica quântica e computação quântica, mas é ligeiramente diferente da normalização min-max. A normalização em mecânica quântica exige que o comprimento (no contexto de computação quântica, a 2-norma) de um vetor de estado ψ|\psi\rangle seja igual à unidade: ψ=ψψ=1\|\psi\|=\sqrt{\langle\psi|\psi\rangle} = 1, garantindo que as probabilidades de medição somem 1. O estado é normalizado dividindo-se pela 2-norma; isto é, reescalonando

ψψ1ψ|\psi\rangle\rightarrow\|\psi\|^{-1}|\psi\rangle

Em computação quântica e mecânica quântica, isso não é uma normalização imposta pelas pessoas sobre os dados, mas uma propriedade fundamental dos estados quânticos. Dependendo do seu esquema de codificação, essa restrição pode afetar como seus dados são reescalonados. Por exemplo, na codificação por amplitude (veja abaixo), o vetor de dados é normalizado x(j)=1\vert\vec{x}^{(j)}\vert = 1 conforme exigido pela mecânica quântica, e isso afeta o escalonamento dos dados sendo codificados. Na codificação por fase, recomenda-se reescalonar os valores das características como xi(j)(0,2π]\vec{x}^{(j)}_i \in (0,2\pi] para que não haja perda de informação devido ao efeito módulo-2π2\pi da codificação para um ângulo de fase de qubit[1,2].

Métodos de codificação

Nas próximas seções, nos referiremos a um pequeno conjunto de dados clássico de exemplo Xex\text{X}_\text{ex} composto por M=5M=5 vetores de dados, cada um com N=3N=3 características:

Xex={(4,8,5),(9,8,6),(2,9,2),(5,7,0),(3,7,5)}\text{X}_{\text{ex}}=\{(4,8,5),(9,8,6),(2,9,2),(5,7,0),(3,7,5)\}

Na notação introduzida acima, poderíamos dizer que a 1a1^\text{a} característica do 4o4^\text{o} vetor de dados no nosso conjunto Xex\text{X}_{\text{ex}} é x1(4)=5,\vec{x}^{(4)}_1 = 5, por exemplo.

Codificação por base

A codificação por base codifica uma string clássica de PP bits em um estado de base computacional de um sistema de PP qubits. Tome por exemplo x3(1)=5=0(23)+1(22)+0(21)+1(20).\vec{x}^{(1)}_3 = 5 = 0(2^3)+1(2^2)+0(2^1)+1(2^0). Isso pode ser representado como uma string de 44 bits como (0101)(0101), e por um sistema de 44 qubits como o estado quântico 0101|0101\rangle. De forma mais geral, para uma string de PP bits: xk(j)=(b1,b2,...,bP)\vec{x}^{(j)}_k = (b_1, b_2, ... , b_P), o estado de PP qubits correspondente é xk(j)=b1,b2,...,bP|x^{(j)}_k\rangle = | b_1, b_2, ... , b_P \rangle com bn{0,1}b_n \in \{0,1\} para n=1,,Pn = 1 , \dots , P. Observe que isso é apenas para uma única característica.

A codificação por base em computação quântica representa cada bit clássico como um qubit separado, mapeando a representação binária dos dados diretamente nos estados quânticos da base computacional. Quando múltiplas características precisam ser codificadas, cada característica é primeiro convertida para sua forma binária e depois atribuída a um grupo distinto de qubits — um grupo por característica — onde cada qubit reflete um bit na representação binária daquela característica.

Como exemplo, vamos codificar o vetor (5, 7, 0).

Suponha que todas as características sejam armazenadas em quatro bits (mais do que precisamos, mas suficiente para representar qualquer inteiro de um único dígito na base 10):

5 → binário 0101

7 → binário 0111

0 → binário 0000

Essas strings de bits são atribuídas a três conjuntos de quatro qubits, portanto o estado de base geral de 12 qubits é:

010101110000∣0101 0111 0000⟩

Aqui, os primeiros quatro qubits representam a primeira característica, os próximos quatro qubits a segunda característica, e os últimos quatro qubits a terceira característica. O código abaixo converte o vetor de dados (5,7,0) em um estado quântico, e é generalizado para fazer o mesmo com outras características de um único dígito.

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit
from qiskit import QuantumCircuit

# Data point to encode
x = 5 # binary: 0101
y = 7 # binary: 0111
z = 0 # binary: 0000

# Convert each to 4-bit binary list
x_bits = [int(b) for b in format(x, "04b")] # [0,1,0,1]
y_bits = [int(b) for b in format(y, "04b")] # [0,1,1,1]
z_bits = [int(b) for b in format(z, "04b")] # [0,0,0,0]

# Combine all bits
all_bits = x_bits + y_bits + z_bits # [0,1,0,1,0,1,1,1,0,0,0,0]

# Initialize a 12-qubit quantum circuit
qc = QuantumCircuit(12)

# Apply x-gates where the bit is 1
for idx, bit in enumerate(all_bits):
if bit == 1:
qc.x(idx)

qc.draw("mpl")

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

Verifique seu entendimento

Leia a questão abaixo, pense na sua resposta e clique no triângulo para revelar a solução.

Escreva código para codificar o primeiro vetor do nosso conjunto de dados de exemplo Xex\text{X}_{\text{ex}}:

x(1)=(4,8,5)\vec{x}^{(1)}=(4,8,5)

usando codificação por base.

Resposta:

import math
from qiskit import QuantumCircuit

# Data point to encode
x = 4 # binary: 0100
y = 8 # binary: 1000
z = 5 # binary: 0101

# Convert each to 4-bit binary list
x_bits = [int(b) for b in format(x, '04b')] # [0,1,0,0]
y_bits = [int(b) for b in format(y, '04b')] # [1,0,0,0]
z_bits = [int(b) for b in format(z, '04b')] # [0,1,0,1]

# Combine all bits
all_bits = x_bits + y_bits + z_bits # [0,1,0,0,1,0,0,0,0,1,0,1]

# Initialize a 12-qubit quantum circuit
qc = QuantumCircuit(12)

# Apply x-gates where the bit is 1
for idx, bit in enumerate(all_bits):
if bit == 1:
qc.x(idx)

qc.draw('mpl')

Codificação por amplitude

A codificação por amplitude codifica dados nas amplitudes de um estado quântico. Ela representa um vetor de dados clássico normalizado de NN dimensões, x(j)\vec{x}^{(j)}, como as amplitudes de um estado quântico de nn qubits, ψx|\psi_x\rangle:

ψx(j)=1αi=1Nxi(j)i|\psi^{(j)}_x\rangle = \frac{1}{\alpha}\sum_{i=1}^N x^{(j)}_i |i\rangle

onde NN é a mesma dimensão dos vetores de dados de antes, xi(j)\vec{x}^{(j)}_i é o ioi^\text{o} elemento de x(j)\vec{x}^{(j)} e i|i\rangle é o ioi^\text{o} estado de base computacional. Aqui, α\alpha é uma constante de normalização a ser determinada a partir dos dados sendo codificados. Essa é a condição de normalização imposta pela mecânica quântica:

i=1Nxi(j)2=α2.\sum_{i=1}^N \left|x^{(j)}_i\right|^2 = \left|\alpha\right|^2.

Em geral, essa é uma condição diferente da normalização min/max usada para cada característica em todos os vetores de dados. Como isso será tratado dependerá do seu problema. Mas não há como contornar a condição de normalização da mecânica quântica acima.

Na codificação por amplitude, cada característica em um vetor de dados é armazenada como a amplitude de um estado quântico diferente. Como um sistema de nn qubits fornece 2n2^n amplitudes, a codificação por amplitude de NN características requer nlog2(N)n \ge \mathrm{log}_2(N) qubits.

Como exemplo, vamos codificar o primeiro vetor do nosso conjunto de dados de exemplo Xex\text{X}_\text{ex}, x(1)=(4,8,5)\vec{x}^{(1)} = (4,8,5) usando codificação por amplitude. Normalizando o vetor resultante, obtemos:

i=1Nxi(1)2=42+82+52=105=α2α=105\sum_{i=1}^N \left|x^{(1)}_i\right|^2 = 4^2+8^2+5^2 = 105 = \left|\alpha\right|^2 \rightarrow \alpha = \sqrt{105}

e o estado quântico de 2 qubits resultante seria:

ψ(x(1))=1105(400+801+510+011)|\psi(\vec{x}^{(1)})\rangle = \frac{1}{\sqrt{105}}(4|00\rangle+8|01\rangle+5|10\rangle+0|11\rangle)

No exemplo acima, o número de características no vetor N=3N=3 não é uma potência de 2. Quando NN não é uma potência de 2, simplesmente escolhemos um valor para o número de qubits nn tal que 2nN2^n\geq N e preenchemos o vetor de amplitudes com constantes não informativas (aqui, um zero).

Assim como na codificação por base, uma vez que calculamos qual estado codificará nosso conjunto de dados, no Qiskit podemos usar a função initialize para prepará-lo:

import math

desired_state = [
1 / math.sqrt(105) * 4,
1 / math.sqrt(105) * 8,
1 / math.sqrt(105) * 5,
1 / math.sqrt(105) * 0,
]

qc = QuantumCircuit(2)
qc.initialize(desired_state, [0, 1])

qc.decompose(reps=5).draw(output="mpl")

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

Uma vantagem da codificação por amplitude é o já mencionado requisito de apenas log2(N)\mathrm{log}_2(N) qubits para codificação. No entanto, algoritmos subsequentes precisam operar sobre as amplitudes de um estado quântico, e os métodos para preparar e medir os estados quânticos tendem a não ser eficientes.

Verifique seu entendimento

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

Escreva o estado normalizado para codificar o seguinte vetor (formado por dois vetores do nosso conjunto de dados de exemplo):

x=(9,8,6,2,9,2)\vec{x}=(9,8,6,2,9,2)

usando codificação por amplitude.

Resposta:

Para codificar 6 números, precisaremos ter pelo menos 6 estados disponíveis em cujas amplitudes possamos codificar. Isso vai requerer 3 qubits. Usando um fator de normalização desconhecido α\alpha, podemos escrever isso como:

ψ=α(9000+8001+6010+2011+9100+2101+0110+0111)|\psi\rangle = \alpha(9|000\rangle+8|001\rangle+6|010\rangle+2|011\rangle+9|100\rangle+2|101\rangle+0|110\rangle+0|111\rangle)

Observe que

ψψ=α2×(92+82+62+22+92+22+02+02)=α2×(270)=1α=1270\langle \psi|\psi\rangle = |\alpha|^2\times(9^2+8^2+6^2+2^2+9^2+2^2+0^2+0^2) = |\alpha|^2\times(270)=1 \rightarrow \alpha = \frac{1}{\sqrt{270}}

Portanto, finalmente,

ψ=1270(9000+8001+6010+2011+9100+2101+0110+0111)|\psi\rangle = \frac{1}{\sqrt{270}}(9|000\rangle+8|001\rangle+6|010\rangle+2|011\rangle+9|100\rangle+2|101\rangle+0|110\rangle+0|111\rangle)

Para o mesmo vetor de dados x=(9,8,6,2,9,2),\vec{x}=(9,8,6,2,9,2), escreva código para criar um circuito que carregue essas características de dados usando codificação por amplitude.

Resposta:

desired_state = [
9 / math.sqrt(270),
8 / math.sqrt(270),
6 / math.sqrt(270),
2 / math.sqrt(270),
9 / math.sqrt(270),
2 / math.sqrt(270),
0,
0,
]

print(desired_state)

qc = QuantumCircuit(3)
qc.initialize(desired_state, [0, 1, 2])
qc.decompose(reps=8).draw(output="mpl")

[0.5477225575051662, 0.48686449556014766, 0.36514837167011077, 0.12171612389003691, 0.5477225575051662, 0.12171612389003691, 0, 0]

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

Pode ser necessário lidar com vetores de dados muito grandes. Considere o vetor

x=(4,8,5,9,8,6,2,9,2,5,7,0,3,7,5).\vec{x}=(4,8,5,9,8,6,2,9,2,5,7,0,3,7,5).

Escreva código para automatizar a normalização e gere um circuito quântico para codificação por amplitude.

Resposta:

Existem muitas respostas possíveis. Aqui está um código que imprime alguns passos intermediários:

import numpy as np
from math import sqrt

init_list = [4, 8, 5, 9, 8, 6, 2, 9, 2, 5, 7, 0, 3, 7, 5]
qubits = round(np.log(len(init_list)) / np.log(2) + 0.4999999999)
need_length = 2**qubits
pad = need_length - len(init_list)
for i in range(0, pad):
init_list.append(0)

init_array = np.array(init_list) # Unnormalized data vector
length = sqrt(
sum(init_array[i] ** 2 for i in range(0, len(init_array)))
) # Vector length
norm_array = init_array / length # Normalized array
print("Normalized array:")
print(norm_array)
print()

qubit_numbers = []
for i in range(0, qubits):
qubit_numbers.append(i)
print(qubit_numbers)

qc = QuantumCircuit(qubits)
qc.initialize(norm_array, qubit_numbers)
qc.decompose(reps=7).draw(output="mpl")

Normalized array: [0.17342199 0.34684399 0.21677749 0.39019949 0.34684399 0.26013299 0.086711 0.39019949 0.086711 0.21677749 0.30348849 0. 0.1300665 0.30348849 0.21677749 0. ]

[0, 1, 2, 3]

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

Você consegue ver vantagens da codificação por amplitude em relação à codificação por base? Se sim, explique.

Resposta:

Pode haver várias respostas. Uma delas é que, dada a ordenação fixa dos estados de base, essa codificação por amplitude preserva a ordem dos números codificados. Frequentemente também será codificada de forma mais densa.

Um benefício da codificação por amplitude é que apenas log2(N)\log_2(N) qubits são necessários para um vetor de dados NN-dimensional (NN características) xx\vec{x}\rightarrow|\vec{x}\rangle. No entanto, a codificação por amplitude é geralmente um procedimento ineficiente que requer preparação de estado arbitrária, que é exponencial no número de portas CNOT. Dito de outra forma, a preparação do estado tem uma complexidade de tempo polinomial de O(N)\mathcal O(N) no número de dimensões, onde N=2nN = 2^n, e nn é o número de qubits. A codificação por amplitude "proporciona uma economia exponencial em espaço ao custo de um aumento exponencial em tempo"[3]; no entanto, aumentos de tempo de execução para O(logN)\mathcal O(\log N) são alcançáveis em certos casos[4]. Para uma aceleração quântica de ponta a ponta, a complexidade de tempo de carregamento dos dados precisa ser considerada.

Codificação por ângulo

A codificação por ângulo é de interesse em muitos modelos de QML que usam mapas de características de Pauli, como máquinas de suporte vetorial quânticas (QSVMs) e circuitos quânticos variacionais (VQCs), entre outros. A codificação por ângulo está intimamente relacionada à codificação por fase e à codificação por ângulo densa, apresentadas abaixo. Aqui usaremos "codificação por ângulo" para nos referir a uma rotação em θ\theta, ou seja, uma rotação para longe do eixo zz realizada, por exemplo, por uma porta RXR_X ou uma porta RYR_Y[1,3]. Na prática, pode-se codificar dados em qualquer rotação ou combinação de rotações. Mas RYR_Y é comum na literatura, então enfatizamos isso aqui.

Quando aplicada a um único qubit, a codificação por ângulo impõe uma rotação em torno do eixo Y proporcional ao valor do dado. Considere a codificação de uma única característica (kak^\text{a}) do joj^\text{o} vetor de dados de um conjunto, xk(j)\vec{x}^{(j)}_k:

xk(j)=RY(θ=xk(j))0=cos(xk(j)2)0+sin(xk(j)2)1.|\vec{x}^{(j)}_k\rangle = R_Y(\theta=\vec{x}^{(j)}_k)|0\rangle = \textstyle\cos\left(\frac{\vec{x}^{(j)}_k}{2}\right)|0\rangle + \sin\left(\frac{\vec{x}^{(j)}_k}{2}\right)|1\rangle.

Alternativamente, a codificação por ângulo pode ser realizada usando portas RX(θ)R_X(\theta), embora o estado codificado tenha uma fase relativa complexa em comparação com RY(θ)R_Y(\theta).

A codificação por ângulo é diferente dos dois métodos anteriores discutidos em vários aspectos. Na codificação por ângulo:

  • Cada valor de característica é mapeado para um qubit correspondente, xk(j)Qk\vec{x}^{(j)}_k \rightarrow Q_k, deixando os qubits em um estado produto.
  • Um valor numérico é codificado por vez, em vez de um conjunto inteiro de características de um ponto de dado.
  • nn qubits são necessários para NN características de dados, onde nNn\leq N. Com frequência, a igualdade se aplica aqui. Veremos como n<Nn<N é possível nas próximas seções.
  • O circuito resultante tem profundidade constante (tipicamente a profundidade é 1 antes da transpilação).

O circuito quântico de profundidade constante o torna particularmente adequado para o hardware quântico atual. Uma característica adicional de codificar nossos dados usando θ\theta (e especificamente, nossa escolha de usar a codificação por ângulo no eixo Y) é que ela cria estados quânticos de valor real que podem ser úteis para certas aplicações. Para a rotação no eixo Y, os dados são mapeados com uma porta de rotação no eixo Y RY(θ)R_Y(\theta) por um ângulo de valor real θ(0,2π]\theta \in (0, 2\pi] (Qiskit RYGate). Assim como na codificação por fase (veja abaixo), recomendamos que você reescalone os dados de forma que xk(j)(0,2π]\vec{x}^{(j)}_k \in (0,2\pi], prevenindo perda de informação e outros efeitos indesejados.

O código Qiskit a seguir rotaciona um único qubit de um estado inicial 0|0\rangle para codificar um valor de dado xk(j)=12π\vec{x}^{(j)}_k=\frac{1}{2}\pi.

from qiskit.quantum_info import Statevector
from math import pi

qc = QuantumCircuit(1)
state1 = Statevector.from_instruction(qc)
qc.ry(pi / 2, 0) # Phase gate rotates by an angle pi/2
state2 = Statevector.from_instruction(qc)
states = state1, state2

Vamos definir uma função para visualizar a ação sobre o vetor de estado. Os detalhes da definição da função não são importantes, mas a capacidade de visualizar os vetores de estado e suas mudanças é importante.

import numpy as np
from qiskit.visualization.bloch import Bloch
from qiskit.visualization.state_visualization import _bloch_multivector_data

def plot_Nstates(states, axis, plot_trace_points=True):
"""This function plots N states to 1 Bloch sphere"""
bloch_vecs = [_bloch_multivector_data(s)[0] for s in states]

if axis is None:
bloch_plot = Bloch()
else:
bloch_plot = Bloch(axes=axis)

bloch_plot.add_vectors(bloch_vecs)

if len(states) > 1:

def rgba_map(x, num):
g = (0.95 - 0.05) / (num - 1)
i = 0.95 - g * num
y = g * x + i
return (0.0, y, 0.0, 0.7)

num = len(states)
bloch_plot.vector_color = [rgba_map(x, num) for x in range(1, num + 1)]

bloch_plot.vector_width = 3
bloch_plot.vector_style = "simple"

if plot_trace_points:

def trace_points(bloch_vec1, bloch_vec2):
# bloch_vec = (x,y,z)
n_points = 15
thetas = np.arccos([bloch_vec1[2], bloch_vec2[2]])
phis = np.arctan2(
[bloch_vec1[1], bloch_vec2[1]], [bloch_vec1[0], bloch_vec2[0]]
)
if phis[1] < 0:
phis[1] = phis[1] + 2 * pi
angles0 = np.linspace(phis[0], phis[1], n_points)
angles1 = np.linspace(thetas[0], thetas[1], n_points)

xp = np.cos(angles0) * np.sin(angles1)
yp = np.sin(angles0) * np.sin(angles1)
zp = np.cos(angles1)
pnts = [xp, yp, zp]
bloch_plot.add_points(pnts)
bloch_plot.point_color = "k"
bloch_plot.point_size = [4] * len(bloch_plot.points)
bloch_plot.point_marker = ["o"]

for i in range(len(bloch_vecs) - 1):
trace_points(bloch_vecs[i], bloch_vecs[i + 1])

bloch_plot.sphere_alpha = 0.05
bloch_plot.frame_alpha = 0.15
bloch_plot.figsize = [4, 4]

bloch_plot.render()

plot_Nstates(states, axis=None, plot_trace_points=True)

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

Isso foi apenas uma única característica de um único vetor de dados. Ao codificar NN características nos ângulos de rotação de nn qubits, digamos para o joj^\text{o} vetor de dados x(j)=(x1,...,xN),\vec{x}^{(j)} = (x_1,...,x_N), o estado produto codificado ficará assim:

x(j)=k=1Ncos(xk(j))0+sin(xk(j))1|\vec{x}^{(j)}\rangle = \bigotimes^N_{k=1} \cos(\vec{x}^{(j)}_k)|0\rangle + \sin(\vec{x}^{(j)}_k)|1\rangle

Observamos que isso é equivalente a

x(j)=k=1NRY(2xk(j))0.|\vec{x}^{(j)}\rangle = \bigotimes^N_{k=1} R_Y(2\vec{x}^{(j)}_k)|0\rangle.

Verifique seu entendimento

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

Codifique o vetor de dados x=(0,π/4,π/2)\vec{x} = (0, \pi/4, \pi/2) usando codificação por ângulo, como descrito acima.

Resposta:

qc = QuantumCircuit(3)
qc.ry(0, 0)
qc.ry(2 * math.pi / 4, 1)
qc.ry(2 * math.pi / 2, 2)
qc.draw(output="mpl")

&quot;Saída da célula de código anterior&quot;

Usando a codificação por ângulo como descrita acima, quantos qubits são necessários para codificar 5 características?

Resposta: 5

Codificação por fase

A codificação por fase é muito semelhante à codificação por ângulo descrita acima. O ângulo de fase de um qubit é um ângulo de valor real ϕ\phi em torno do eixo zz a partir do eixo +x+x. Os dados são mapeados com uma rotação de fase, P(ϕ)=eiϕ/2RZ(ϕ)P(\phi) = e^{i\phi/2}R_Z(\phi), onde ϕ(0,2π]\phi \in (0,2\pi] (veja Qiskit PhaseGate para mais informações). Recomenda-se reescalonar os dados de forma que xk(j)(0,2π]\vec{x}^{(j)}_k \in (0,2\pi]. Isso previne a perda de informação e outros efeitos potencialmente indesejados[1,2].

Um qubit geralmente é inicializado no estado 0|0\rangle, que é um autoestado do operador de rotação de fase, o que significa que o estado do qubit precisa primeiro ser rotacionado para que a codificação por fase seja implementada. Portanto, faz sentido inicializar o estado com uma porta Hadamard: H0=+=12(0+1)H|0\rangle = |+\rangle = \textstyle\frac{1}{\sqrt{2}}(|0\rangle + |1\rangle). A codificação por fase em um único qubit significa imprimir uma fase relativa proporcional ao valor do dado:

xk(j)=P(ϕ=xk(j))+=12(0+eixk(j)1).|\vec{x}^{(j)}_k\rangle = P(\phi=\vec{x}^{(j)}_k)|+\rangle = \textstyle\frac{1}{\sqrt{2}}\big(|0\rangle + e^{i\vec{x}^{(j)}_k}|1\rangle\big).

O procedimento de codificação por fase mapeia cada valor de característica para a fase de um qubit correspondente, xk(j)Qk\vec{x}^{(j)}_k \rightarrow Q_k. No total, a codificação por fase tem uma profundidade de circuito de 2, incluindo a camada Hadamard, o que a torna um esquema de codificação eficiente. O estado de múltiplos qubits codificado por fase (nn qubits para N=nN=n características) é um estado produto:

x(j)=k=1NPk(ϕ=xk(j))+N=12Nk=1N(0+eixk(j)1).|\vec{x}^{(j)}\rangle = \bigotimes_{k=1}^{N} P_k(\phi = \vec{x}^{(j)}_k)|+\rangle^{\otimes N} = {\textstyle\frac{1}{\sqrt{2^N}}} \bigotimes_{k=1}^{N}\big(|0\rangle + e^{i\vec{x}^{(j)}_k}|1\rangle\big).

O código Qiskit a seguir primeiro prepara o estado inicial de um único qubit rotacionando-o com uma porta Hadamard, e depois o rotaciona novamente usando uma porta de fase para codificar uma característica de dado xk(j)=12π\vec{x}^{(j)}_k=\frac{1}{2}\pi.

qc = QuantumCircuit(1)
qc.h(0) # Hadamard gate rotates state down to Bloch equator
state1 = Statevector.from_instruction(qc)

qc.p(pi / 2, 0) # Phase gate rotates by an angle pi/2
state2 = Statevector.from_instruction(qc)

states = state1, state2

qc.draw("mpl", scale=1)

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

Podemos visualizar a rotação em ϕ\phi usando a função plot_Nstates que definimos.

plot_Nstates(states, axis=None, plot_trace_points=True)

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

O gráfico da esfera de Bloch mostra a rotação no eixo Z +P(12π)+|+\rangle \rightarrow P(\frac{1}{2}\pi)|+\rangle onde xk(j)=12π\vec{x}^{(j)}_k=\frac{1}{2}\pi. A seta verde claro mostra o estado final.

A codificação por fase é usada em muitos mapas de características quânticos, particularmente nos mapas de características ZZ e ZZZZ, e em mapas de características de Pauli em geral, entre outros.

Verifique seu entendimento

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

Quantos qubits são necessários para usar a codificação por fase como descrita acima para armazenar 8 características?

Resposta: 8

Escreva código para o vetor x(1)=(4,8,5,9,8,6,2,9,2,5,7,0)\vec{x}^{(1)}=(4,8,5,9,8,6,2,9,2,5,7,0) usando codificação por fase.

Resposta:

Pode haver muitas respostas. Aqui está um exemplo:

phase_data = [4, 8, 5, 9, 8, 6, 2, 9, 2, 5, 7, 0]
qc = QuantumCircuit(len(phase_data))
for i in range(0, len(phase_data)):
qc.h(i)
qc.rz(phase_data[i] * 2 * math.pi / float(max(phase_data)), i)
qc.draw(output="mpl")

&quot;Saída da célula de código anterior&quot;

Codificação por ângulo densa

A codificação por ângulo densa (DAE, do inglês dense angle encoding) é uma combinação de codificação por ângulo e codificação por fase. A DAE permite que dois valores de características sejam codificados em um único qubit: um ângulo com uma rotação no eixo Y, e o outro com uma rotação no eixo zz: xk(j),\vec{x}^{(j)}_k, x(j)θ,ϕ\vec{x}^{(j)}_\ell \rightarrow \theta, \phi. Ela codifica duas características da seguinte forma:

xk(j),x(j)=RZ(ϕ=x(j))RY(θ=xk(j))0=cos(xk(j)2)0+eix(j)sin(xk(j)2)1.|\vec{x}^{(j)}_k,\vec{x}^{(j)}_\ell\rangle = R_Z(\phi=\vec{x}^{(j)}_\ell) R_Y(\theta=\vec{x}^{(j)}_k)|0\rangle = \cos\left(\frac{\vec{x}^{(j)}_k}{2}\right)|0\rangle + e^{i\vec{x}^{(j)}_\ell} \sin\left(\frac{\vec{x}^{(j)}_k}{2}\right)|1\rangle.

Codificar dois dados em um qubit resulta numa redução de 2×2\times no número de qubits necessários para a codificação. Estendendo isso para mais características, o vetor de dados x=(x1,...,xN)\vec{x} = (x_1,...,x_N) pode ser codificado como:

x=k=1N/2cos(x2k1)0+eix2ksin(x2k1)1|\vec{x}\rangle = \bigotimes_{k=1}^{N/2} \cos(x_{2k-1})|0\rangle + e^{i x_{2k}}\sin(x_{2k-1})|1\rangle

A DAE pode ser generalizada para funções arbitrárias das duas características em vez das funções senoidais usadas aqui. Isso é chamado de codificação geral de qubit (general qubit encoding)[7].

Como exemplo de DAE, o código abaixo codifica e visualiza a codificação das características x1=θ=3π/8x_1=\theta = 3\pi/8 e x2=ϕ=7π/4x_2=\phi = 7\pi/4.

qc = QuantumCircuit(1)
state1 = Statevector.from_instruction(qc)
qc.ry(3 * pi / 8, 0)
state2 = Statevector.from_instruction(qc)
qc.rz(7 * pi / 4, 0)
state3 = Statevector.from_instruction(qc)
states = state1, state2, state3

plot_Nstates(states, axis=None, plot_trace_points=True)

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

Verifique seu entendimento

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

Dado o tratamento acima, quantos qubits são necessários para codificar 6 características usando a codificação densa?

Resposta: 3

Escreva código para carregar o vetor x(1)=(4,8,5,9,8,6,2,9,2,5,7,0,3,7,5)\vec{x}^{(1)}=(4,8,5,9,8,6,2,9,2,5,7,0,3,7,5) usando codificação por ângulo densa.

Resposta:

Observe que preenchemos a lista com um "0" para evitar o problema de haver um único parâmetro não utilizado no nosso esquema de codificação.

dense_data = [4, 8, 5, 9, 8, 6, 2, 9, 2, 5, 7, 0, 3, 7, 5, 0]
qc = QuantumCircuit(int(len(dense_data) / 2))
entry = 0
for i in range(0, int(len(dense_data) / 2)):
qc.ry(dense_data[entry] * 2 * math.pi / float(max(dense_data)), i)
entry = entry + 1
qc.rz(dense_data[entry] * 2 * math.pi / float(max(dense_data)), i)
entry = entry + 1
qc.draw(output="mpl")

&quot;Saída da célula de código anterior&quot;

Codificação com feature maps integrados

Codificando em pontos arbitrários

A codificação por ângulo, codificação por fase e codificação densa preparavam estados produto com uma feature codificada em cada qubit (ou duas features por qubit). Isso é diferente da codificação por base e da codificação por amplitude, pois esses métodos fazem uso de estados entrelaçados. Não há uma correspondência 1:1 entre a feature do dado e o qubit. Na codificação por amplitude, por exemplo, uma feature pode ser a amplitude do estado 01|01\rangle e outra feature pode ser a amplitude do estado 10|10\rangle. Em geral, métodos que codificam em estados produto produzem circuitos mais rasos e podem armazenar 1 ou 2 features em cada qubit. Métodos que usam entrelaçamento e associam uma feature a um estado em vez de a um qubit resultam em circuitos mais profundos e podem armazenar mais features por qubit em média.

Mas a codificação não precisa ser inteiramente em estados produto ou inteiramente em estados entrelaçados como na codificação por amplitude. Na verdade, muitos esquemas de codificação integrados ao Qiskit permitem codificar tanto antes quanto depois de uma camada de entrelaçamento, em vez de apenas no início. Isso é conhecido como "data reuploading". Para trabalhos relacionados, veja as referências [5] e [6].

Nesta seção, vamos usar e visualizar alguns dos esquemas de codificação integrados. Todos os métodos desta seção codificam NN features como rotações em NN portas parametrizadas em nn qubits, onde nNn \leq N. Note que maximizar o carregamento de dados para um determinado número de qubits não é a única consideração. Em muitos casos, a profundidade do circuito pode ser uma consideração ainda mais importante do que a contagem de qubits.

Efficient SU2

Um exemplo comum e útil de codificação com entrelaçamento é o circuito efficient_su2 do Qiskit. De forma impressionante, esse circuito pode, por exemplo, codificar 8 features em apenas 2 qubits. Vamos ver isso e depois tentar entender como é possível.

from qiskit.circuit.library import efficient_su2

circuit = efficient_su2(num_qubits=2, reps=1, insert_barriers=True)
circuit.decompose().draw(output="mpl")

Output of the previous code cell

Ao escrever nosso estado, usaremos a convenção do Qiskit de que os qubits menos significativos são ordenados à direita, como em q2,q1,q0|q_2,q_1,q_0\rangle ou q2q1q0.|q_2\rangle\otimes|q_1\rangle\otimes|q_0\rangle. Esses estados podem se tornar muito complicados muito rapidamente, e esse raro exemplo pode ajudar a explicar por que tais estados raramente são escritos explicitamente.

Nosso sistema começa no estado 00.|00\rangle. Até a primeira barreira (um ponto que rotulamos b1b1), nossos estados são:

ψb1=(cos(θ12)0+sin(θ12)eiθ31)(cos(θ02)0+sin(θ02)eiθ21)|\psi\rangle_{b1} = \left(\cos\left(\frac{\theta_1}{2}\right)|0\rangle+\sin\left(\frac{\theta_1}{2}\right)e^{i\theta_3}|1\rangle\right)\otimes\left(\cos\left(\frac{\theta_0}{2}\right)|0\rangle+\sin\left(\frac{\theta_0}{2}\right)e^{i\theta_2}|1\rangle\right)

Isso é apenas codificação densa, que já vimos antes. Agora, após a porta CNOT, na segunda barreira (b2b2), nosso estado é

ψb2=cos(θ12)cos(θ02)00+cos(θ12)sin(θ02)eiθ211+sin(θ12)cos(θ02)eiθ310+sin(θ12)sin(θ02)eiθ2eiθ301\begin{aligned} |\psi\rangle_{b2} = & \cos\left(\frac{\theta_1}{2}\right)\cos\left(\frac{\theta_0}{2}\right)|00\rangle+\cos\left(\frac{\theta_1}{2}\right)\sin\left(\frac{\theta_0}{2}\right)e^{i\theta_2}|11\rangle\\ + & \sin\left(\frac{\theta_1}{2}\right)\cos\left(\frac{\theta_0}{2}\right)e^{i\theta_3}|10\rangle+\sin\left(\frac{\theta_1}{2}\right)\sin\left(\frac{\theta_0}{2}\right)e^{i\theta_2}e^{i\theta_3}|01\rangle \end{aligned}

Agora aplicamos o último conjunto de rotações de qubit único e agrupamos os estados semelhantes para obter:

ψfinal=[cos(θ02)(cos(θ12)cos(θ52)sin(θ12)sin(θ52)eiθ3)cos(θ42)+sin(θ02)(cos(θ12)sin(θ52)sin(θ12)cos(θ52)eiθ3)sin(θ42)eiθ2]00+[cos(θ02)(cos(θ12)cos(θ52)sin(θ12)sin(θ52)eiθ3)sin(θ42)+sin(θ02)(cos(θ12)sin(θ52)+sin(θ12)cos(θ52)eiθ3)cos(θ42)eiθ2]eiθ601+[cos(θ02)(cos(θ12)sin(θ52)+sin(θ12)cos(θ52)eiθ3)cos(θ42)sin(θ02)(cos(θ12)cos(θ52)+sin(θ12)sin(θ52)eiθ3)sin(θ42)eiθ2]eiθ710+[cos(θ02)(cos(θ12)sin(θ52)+sin(θ12)cos(θ52)eiθ3)sin(θ42)+sin(θ02)(cos(θ12)cos(θ52)+sin(θ12)sin(θ52)eiθ3)cos(θ42)eiθ2]eiθ6eiθ711\begin{align*} |\psi\rangle_{\text{final}} = & \left[\cos\left(\frac{\theta_0}{2}\right)\left(\cos\left(\frac{\theta_1}{2}\right)\cos\left(\frac{\theta_5}{2}\right)-\sin\left(\frac{\theta_1}{2}\right)\sin\left(\frac{\theta_5}{2}\right)e^{i\theta_3}\right)\cos\left(\frac{\theta_4}{2}\right)\right.\\ + & \left.\sin\left(\frac{\theta_0}{2}\right)\left(\cos\left(\frac{\theta_1}{2}\right)\sin\left(\frac{\theta_5}{2}\right)-\sin\left(\frac{\theta_1}{2}\right)\cos\left(\frac{\theta_5}{2}\right)e^{i\theta_3}\right)\sin\left(\frac{\theta_4}{2}\right)e^{i\theta_2}\right] |00\rangle\\ + & \left[\cos\left(\frac{\theta_0}{2}\right)\left(\cos\left(\frac{\theta_1}{2}\right)\cos\left(\frac{\theta_5}{2}\right)-\sin\left(\frac{\theta_1}{2}\right)\sin\left(\frac{\theta_5}{2}\right)e^{i\theta_3}\right)\sin\left(\frac{\theta_4}{2}\right)\right.\\ + & \left.\sin\left(\frac{\theta_0}{2}\right)\left(-\cos\left(\frac{\theta_1}{2}\right)\sin\left(\frac{\theta_5}{2}\right)+\sin\left(\frac{\theta_1}{2}\right)\cos\left(\frac{\theta_5}{2}\right)e^{i\theta_3}\right)\cos\left(\frac{\theta_4}{2}\right)e^{i\theta_2}\right] e^{i\theta_6}|01\rangle\\ + & \left[\cos\left(\frac{\theta_0}{2}\right)\left(\cos\left(\frac{\theta_1}{2}\right)\sin\left(\frac{\theta_5}{2}\right)+\sin\left(\frac{\theta_1}{2}\right)\cos\left(\frac{\theta_5}{2}\right)e^{i\theta_3}\right)\cos\left(\frac{\theta_4}{2}\right)\right.\\ - & \left.\sin\left(\frac{\theta_0}{2}\right)\left(\cos\left(\frac{\theta_1}{2}\right)\cos\left(\frac{\theta_5}{2}\right)+\sin\left(\frac{\theta_1}{2}\right)\sin\left(\frac{\theta_5}{2}\right)e^{i\theta_3}\right)\sin\left(\frac{\theta_4}{2}\right)e^{i\theta_2}\right] e^{i\theta_7}|10\rangle\\ + & \left[\cos\left(\frac{\theta_0}{2}\right)\left(\cos\left(\frac{\theta_1}{2}\right)\sin\left(\frac{\theta_5}{2}\right)+\sin\left(\frac{\theta_1}{2}\right)\cos\left(\frac{\theta_5}{2}\right)e^{i\theta_3}\right)\sin\left(\frac{\theta_4}{2}\right)\right.\\ + & \left.\sin\left(\frac{\theta_0}{2}\right)\left(\cos\left(\frac{\theta_1}{2}\right)\cos\left(\frac{\theta_5}{2}\right)+\sin\left(\frac{\theta_1}{2}\right)\sin\left(\frac{\theta_5}{2}\right)e^{i\theta_3}\right)\cos\left(\frac{\theta_4}{2}\right)e^{i\theta_2}\right] e^{i\theta_6}e^{i\theta_7}|11\rangle \end{align*}

Isso provavelmente é complicado demais para interpretar. Em vez disso, dê um passo atrás e pense em quantos parâmetros carregamos no estado: oito. Mas temos apenas quatro estados da base computacional. À primeira vista, pode parecer que carregamos mais parâmetros do que faz sentido, já que o estado final pode ser escrito como ψfinal=c000+c101+c210+c311\psi_\text{final} = c_0|00\rangle+c_1|01\rangle+c_2|10\rangle+c_3|11\rangle. Note, porém, que cada prefator é complexo! Escrito assim:

ψfinal=(a0+ib0)00+(a1+ib1)01+(a2+ib2)10+(a3+ib3)11\psi_\text{final} = (a_0+ib_0)|00\rangle+(a_1+ib_1)|01\rangle+(a_2+ib_2)|10\rangle+(a_3+ib_3)|11\rangle

Pode-se ver que temos, de fato, oito parâmetros no estado nos quais codificar nossas oito features.

Ao aumentar o número de qubits e o número de repetições das camadas de entrelaçamento e rotação, é possível codificar muito mais dados. Escrever as funções de onda rapidamente se torna inviável. Mas ainda podemos ver a codificação em ação. Aqui codificamos o vetor de dados x\vec{x} com 12 features, em um circuito efficient_su2 de 3 qubits, usando cada uma das portas parametrizadas para codificar uma feature diferente.

x=(0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1.0,1.1,1.2)\vec{x} = (0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1.0,1.1,1.2)

Neste vetor de dados, as features são mostradas em uma ordem específica. De forma isolada, não importa se elas são codificadas nessa ordem ou na ordem inversa. O que é importante é manter o controle disso e ser consistente. Note no diagrama do circuito que efficient_su2 assume uma certa ordem de codificação, especificamente preenchendo a primeira camada de portas parametrizadas do qubit 0 ao qubit 2, e depois passando para a próxima camada. Isso não é nem consistente nem inconsistente com a notação little-endian, pois aqui as features dos dados não podem ser ordenadas por qubit a priori, antes que um circuito de codificação tenha sido especificado.

x = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2]
circuit = efficient_su2(num_qubits=3, reps=1, insert_barriers=True)
encode = circuit.assign_parameters(x)
encode.decompose().draw(output="mpl")

Output of the previous code cell

Em vez de aumentar o número de qubits, você pode optar por aumentar o número de repetições das camadas de entrelaçamento e rotação. Mas há limites para quantas repetições são úteis. Como dito anteriormente, há uma troca: circuitos com mais qubits ou mais repetições de camadas de entrelaçamento e rotação podem armazenar mais parâmetros, mas o fazem com maior profundidade de circuito. Voltaremos às profundidades de alguns feature maps integrados mais adiante. Os próximos métodos de codificação integrados ao Qiskit têm "feature map" como parte de seus nomes. Vale reiterar que codificar dados em um circuito quântico é um mapeamento de features, no sentido de que leva os dados a um novo espaço: o espaço de Hilbert dos qubits envolvidos. A relação entre a dimensionalidade do espaço de features original e o do espaço de Hilbert dependerá do circuito que você usar para a codificação.

Feature map ZZ

O feature map ZZ (ZFM) pode ser interpretado como uma extensão natural da codificação por fase. O ZFM consiste em camadas alternadas de portas de qubit único: camadas de porta Hadamard e camadas de porta de fase. Seja o vetor de dados x\vec{x} com NN features. O circuito quântico que realiza o mapeamento de features é representado como um operador unitário que age sobre o estado inicial:

UZFM(x)0N=ϕ(x)\mathscr{U}_{\text{ZFM}}(\vec{x})|0\rangle^{\otimes N}=|\phi(\vec{x})\rangle

onde 0N|0\rangle^{\otimes N} é o estado fundamental de NN qubits. Essa notação é usada para consistência com a referência [4] de Havlicek et al. As features de dados xix_i são mapeadas um-a-um com os qubits correspondentes. Por exemplo, se você tem 8 features em um vetor de dados, usaria 8 qubits. O circuito ZFM é composto por rr repetições de um subcircuito formado por camadas de porta Hadamard e camadas de porta de fase. Uma camada Hadamard é formada por uma porta Hadamard agindo em cada qubit de um registrador de nn qubits, HHH=HnH \otimes H \otimes \dots \otimes H = H^{\otimes n}, no mesmo estágio do algoritmo. Essa descrição também se aplica a uma camada de porta de fase em que o ioi^\text{o} qubit é acionado por P(xi)P(\vec{x}_i). Cada porta PP tem uma feature como argumento, mas a camada de porta de fase (P(x1)P(xk)P(xN)P(\vec{x}_1)\otimes\ldots P(\vec{x}_k)\otimes\ldots P(\vec{x}_N) é uma função do vetor de dados. O unitário completo do circuito ZFM com uma única repetição é:

UZFM=(P(x1)P(xk)P(xN)HN)=(k=1NP(xk))HN\mathscr{U}_{\text{ZFM}}=\big(P(\vec{x}_1)\otimes\ldots P(\vec{x}_k)\otimes\ldots P(\vec{x}_N)H^{\otimes N}\big)=\left(\bigotimes_{k = 1}^N P(\vec{x}_k)\right)H^{\otimes N}

Então rr repetições desse unitário seriam

UZFM(r)(x)=s=1r[(k=1NP(xk))HN]\mathscr{U}^{(r)}_{\text{ZFM}}\left(\vec{x}\right)=\prod_{s=1}^{r}\left[\left(\bigotimes_{k = 1}^N P(\vec{x}_k)\right)H^{\otimes N}\right]

As features de dados, xkx_k, são mapeadas para as portas de fase da mesma forma em todas as rr repetições. O estado do feature map ZFM é um estado produto e é eficiente para simulação clássica[4].

Para começar com um exemplo pequeno, um circuito ZFM de dois qubits é codificado usando o Qiskit e desenhado para exibir a estrutura simples do circuito. No exemplo, uma única repetição, r=1r=1, é implementada com o vetor de dados x=(12π,13π)\vec{x} = \left(\textstyle\frac{1}{2}\pi, \textstyle\frac{1}{3}\pi\right). Note que isso é escrito na ordem padrão de um vetor em Python, o que significa que o elemento de índice 00 é 12π.\textstyle\frac{1}{2}\pi. Podemos codificar essa feature de índice 00 no nosso qubit 00, ou no qubit N.N. Novamente, nem sempre pode haver um mapeamento único 1:1 da ordem das features para a ordem dos qubits, pois diferentes feature maps codificam diferentes números de features em cada qubit. O que importa, novamente, é que estejamos cientes de onde cada feature está sendo codificada. Ao fornecer uma lista de parâmetros para o feature map ZZ, ele codificará a feature 0 da lista no qubit menos significativo com uma porta parametrizada, como no qubit 0. Então seguiremos essa convenção ao fazer isso manualmente. Codificaremos 12π\textstyle\frac{1}{2}\pi no qubit 00 e 13π\textstyle\frac{1}{3}\pi no qubit 11.

O operador unitário do circuito ZFM age sobre o estado inicial da seguinte forma:

UZFM(xˉ)00=P(xˉ)2H200=(P(13π)H0)(P(12π)H0).\mathscr{U}_{\text{ZFM}}(\bar{x})|00\rangle = P(\bar{x})^{\otimes 2} H^{\otimes 2}|00\rangle = \left( P\left(\textstyle\frac{1}{3}\pi\right)H|0\rangle \right) \otimes \left(P\left(\textstyle\frac{1}{2}\pi\right)H|0\rangle\right).

A fórmula foi reorganizada em torno do produto tensorial para enfatizar as operações em cada qubit. O código Qiskit a seguir usa portas Hadamard e de fase explicitamente para mostrar a estrutura do ZFM:

qc0 = QuantumCircuit(1)
qc1 = QuantumCircuit(1)

qc0.h(0)
qc0.p(pi / 2, 0)

qc1.h(0)
qc1.p(pi / 3, 0)

# Combine circuits qc0 and qc1 into 1 circuit
qc = QuantumCircuit(2)
qc.compose(qc0, [0], inplace=True)
qc.compose(qc1, [1], inplace=True)

qc.draw("mpl", scale=1)

Output of the previous code cell

Agora codificamos o mesmo vetor de dados x=(12π,13π)\vec{x} = \left(\textstyle\frac{1}{2}\pi, \textstyle\frac{1}{3}\pi\right) em um circuito ZFM com três repetições, r=3r=3, usando a classe z_feature_map do Qiskit, o que no total nos dá o feature map quântico UZFM(x)\mathscr{U}_{\text{ZFM}}(\vec{x}). Por padrão na classe z_feature_map, os parâmetros β\beta são multiplicados por 2 antes de serem mapeados para a porta de fase βP(θ=2β)\beta \rightarrow P(\theta = 2\beta). Para reproduzir as mesmas codificações acima, dividimos por 2.

from qiskit.circuit.library import z_feature_map

zfeature_map = z_feature_map(feature_dimension=2, reps=3)
zfeature_map = zfeature_map.assign_parameters([(1 / 2) * pi / 2, (1 / 2) * pi / 3])
zfeature_map.decompose().draw("mpl")

Output of the previous code cell

Claramente, esse é um mapeamento diferente do feito manualmente acima, mas note a consistência na ordenação dos parâmetros: 12π\textstyle\frac{1}{2}\pi foi novamente codificado no qubit 00.

Você pode usar o ZFM por meio da classe ZFM do Qiskit; também pode usar essa estrutura como inspiração para construir seu próprio mapeamento de features.

Feature map ZZZZ

O feature map ZZZZ (ZZFM) estende o ZFM com a inclusão de portas de entrelaçamento de dois qubits, especificamente a porta de rotação ZZZZ, RZZ(θ)R_{ZZ}(\theta). Conjectura-se que o ZZFM seja geralmente caro para computar em um computador clássico, ao contrário do ZFM.

RZZ(θ)R_{ZZ}(\theta) implementa uma interação ZZZZ e é maximalmente entrelaçante para θ=12π\theta = \textstyle{\frac{1}{2}}\pi. RZZ(θ)R_{ZZ}(\theta) pode ser decomposto em uma série de portas em dois qubits, como mostrado no código Qiskit a seguir usando a porta RZZ e o método decompose da classe QuantumCircuit. Codificamos uma única feature do vetor de dados x\vec{x}: xk=π.\vec{x}_k=\pi.

qc = QuantumCircuit(2)
qc.rzz(pi, 0, 1)
qc.draw("mpl", scale=1)

Output of the previous code cell

Como costuma ser o caso, vemos isso representado como uma única unidade semelhante a uma porta, até usarmos .decompose() para ver todas as portas constituintes.

qc.decompose().draw("mpl", scale=1)

Output of the previous code cell

Os dados são mapeados com uma rotação de fase P(θ)=eiθ/2RZ(θ)P(\theta) = e^{i\theta/2}R_Z(\theta) no segundo qubit. A porta RZZ(θ)R_{ZZ}(\theta) entrelaça os dois qubits nos quais opera por um grau de entrelaçamento determinado pelo valor da feature codificada.

O circuito ZZFM completo consiste em uma porta Hadamard e uma porta de fase, como no ZFM, seguidas do entrelaçamento descrito acima. Uma única repetição do circuito ZZFM é:

UZZFM(x)=UZZ(x)(P(x1)P(xk)P(xN)HN)=UZZ(x)(k=1NP(xk))HN,\mathscr{U}_{\text{ZZFM}}(\vec{x}) = U_{ZZ}(\vec{x})\big(P(\vec{x}_1)\otimes\ldots P(\vec{x}_k)\otimes\ldots P(\vec{x}_N)H^{\otimes N}\big)=U_{ZZ}(\vec{x})\left(\bigotimes_{k = 1}^N P(\vec{x}_k)\right)H^{\otimes N},

onde UZZ(x)U_{ZZ}(\vec{x}) contém uma camada de portas ZZ estruturada por um esquema de entrelaçamento. Vários esquemas de entrelaçamento são mostrados nos blocos de código abaixo. A estrutura de UZZ(x)U_{ZZ}(\vec{x}) também inclui uma função que combina as features de dados dos qubits sendo entrelaçados da seguinte forma. Digamos que a porta RZZR_{ZZ} será aplicada aos qubits pp e qq. Na camada de fase, esses qubits têm portas de fase que codificam xp\vec{x}_p e xq\vec{x}_q neles, respectivamente. O argumento θq,p\theta_{q,p} do RZZ,q,p(θq,p)R_{ZZ,q,p}(\theta_{q,p}) não será simplesmente uma dessas features ou a outra, mas uma função frequentemente denotada por ϕ\phi (não confundir com o ângulo azimutal):

θq,pϕ(xq,xp)=2(πxq)(πxp).\theta_{q,p} \rightarrow \phi(\vec{x}_q, \vec{x}_p) = 2(\pi-\vec{x}_q)(\pi-\vec{x}_p).

Veremos isso em vários exemplos abaixo. A extensão para múltiplas repetições é a mesma que no caso do z_feature_map:

UZZFM(r)(x)=s=1r[UZZ(x)(k=1NP(xk))HN].\mathscr{U}^{(r)}_{\text{ZZFM}}\left(\vec{x}\right)=\prod_{s=1}^{r}\left[U_{ZZ}(\vec{x})\left(\bigotimes_{k = 1}^N P(\vec{x}_k)\right)H^{\otimes N}\right].

Como os operadores aumentaram em complexidade, vamos primeiro codificar um vetor de dados x=(x0,x1)\vec{x} = (x_0, x_1) com um ZZFM de dois qubits e uma repetição usando o código a seguir:

from qiskit.circuit.library import zz_feature_map

feature_dim = 2
zzfeature_map = zz_feature_map(
feature_dimension=feature_dim, entanglement="linear", reps=1
)
zzfeature_map.decompose(reps=1).draw("mpl", scale=1)

Output of the previous code cell

Por padrão no Qiskit, as features (x1,x2)(\vec{x}_1, \vec{x}_2) são mapeadas juntas para RZZ(θ)R_{ZZ}(\theta) por essa função de mapeamento θ1,2=ϕ(x1,x2)=2(πx1)(πx2)\theta_{1,2} = \phi(\vec{x}_1, \vec{x}_2) = 2(\pi-\vec{x}_1)(\pi-\vec{x}_2). O Qiskit permite ao usuário personalizar a função ϕ\phi (ou ϕS\phi_S onde SS é o conjunto de pares de qubits acoplados por portas RZZR_{ZZ}) como etapa de pré-processamento.

Passando para um vetor de dados de quatro dimensões x=(x1,x2,x3,x4)\vec{x} = (\vec{x}_1, \vec{x}_2, \vec{x}_3, \vec{x}_4) e mapeando para um ZZFM de quatro qubits com uma repetição, podemos começar a ver o mapeamento ϕ\phi para vários pares de qubits. Também podemos ver o significado de entrelaçamento "linear":

feature_dim = 4
zzfeature_map = zz_feature_map(
feature_dimension=feature_dim, entanglement="linear", reps=1
)
zzfeature_map.decompose().draw("mpl", scale=1)

Output of the previous code cell

No esquema de entrelaçamento linear, pares vizinhos (numerados) de qubits neste circuito são entrelaçados. Há outros esquemas de entrelaçamento integrados no Qiskit, incluindo circular e full.

Feature map de Pauli

O feature map de Pauli (PFM) é a generalização do ZFM e do ZZFM para usar portas de Pauli arbitrárias. O feature map de Pauli tem uma forma muito semelhante aos dois feature maps anteriores. Para rr repetições da codificação das NN features do vetor x,\vec{x},

UPFM(x)=s=1rU(x)Hn.\mathscr{U}_{\text{PFM}}(\vec{x}) = \prod_{s=1}^{r} U(\vec{x}) H^{\otimes n}.

Para o PFM, U(x)U(\vec{x}) é generalizado para um operador unitário de expansão de Pauli. Aqui apresentamos uma forma mais generalizada dos feature maps considerados até agora:

U(x)=exp(iSIϕS(x)iSσi),U(\vec{x}) = \exp\left(i \sum_{S \in\mathcal{I}} \phi_S(\vec{x}) \prod_{i \in S} \sigma_i \right),

onde σi\sigma_i é um operador de Pauli, σiI,X,Y,Z\sigma_i \in {I,X,Y,Z}. Aqui I\mathcal{I} é o conjunto de todas as conectividades de qubits conforme determinado pelo feature map, incluindo o conjunto de qubits acionados por portas de qubit único. Ou seja, para um feature map no qual o qubit 0 é acionado por uma porta de fase, e os qubits 2 e 3 são acionados por uma porta RZZR_{ZZ}, o conjunto I\mathcal{I} incluiria {{0},{2,3}}\{\{0\},\{2,3\}\}. SS percorre todos os elementos desse conjunto. Nos feature maps anteriores, a função ϕS(x)\phi_S(\vec{x}) estava envolvida exclusivamente com portas de qubit único ou exclusivamente com portas de dois qubits. Aqui, a definimos de forma geral:

ϕS(x)={xiif S={i} (single-qubit)jS(πxj)if S2 (multi-qubit)\phi_S(\vec{x})= \begin{cases} x_i & \text{if } S= \{i\} \text{ (single-qubit)}\\ \prod_{j\in{S}}(\pi-x_j) & \text{if } |S|\ge2 \text{ (multi-qubit)}\\ \end{cases}

Para a documentação, veja a documentação da classe Pauli feature map do Qiskit). No ZZFM, o operador σi\sigma_i é restrito a ZiZ_i.

Uma forma de entender o unitário acima é por analogia com o propagador em um sistema físico. O unitário acima é um operador de evolução unitária, exp(itH)\exp(it\mathcal{H}), para um Hamiltoniano H\mathcal{H}, similar ao modelo de Ising, onde o parâmetro de tempo tt é substituído por valores de dados para conduzir a evolução. A expansão desse operador unitário fornece o circuito PFM. As conectividades de entrelaçamento em SS podem ser interpretadas como acoplamentos de Ising em uma rede de spins. Vamos considerar um exemplo de operadores de Pauli YY e XXXX representando essas interações do tipo Ising. O Qiskit fornece uma classe pauli_feature_map para instanciar um PFM com uma escolha de portas de qubit único e nn qubits, que neste exemplo serão passadas como strings de Pauli 'Y' e 'XX'. Tipicamente, nn é 1 ou 2 para interações de um e dois qubits, respectivamente. O esquema de entrelaçamento é "linear", o que significa que apenas qubits vizinhos no circuito quântico são acoplados. Note que isso não corresponde a qubits vizinhos no computador quântico em si, pois este circuito quântico é uma camada de abstração.

from qiskit.circuit.library import pauli_feature_map

feature_dim = 3
pfmap = pauli_feature_map(
feature_dimension=feature_dim, entanglement="linear", reps=1, paulis=["Y", "XX"]
)

pfmap.decompose().draw("mpl", scale=1.5)

Output of the previous code cell

O Qiskit fornece um parâmetro α\alpha nos feature maps de Pauli para controlar o escalonamento das rotações de Pauli.

U(xˉ)=exp(iαS[n]ϕS(xˉ)iSσi)U(\bar{x}) = \exp\left(i \alpha \sum_{S\subseteq[n]} \phi_S(\bar{x}) \prod_{i \in S} \sigma_i \right)

O valor padrão de α\alpha é 22. Ao otimizar seu valor no intervalo, por exemplo, [0,4],[0,4], pode-se alinhar melhor um kernel quântico aos dados.

Aqui visualizamos vários feature maps de Pauli para circuitos de dois qubits, para ter uma ideia melhor da gama de possibilidades.

from qiskit.visualization import circuit_drawer
import matplotlib.pyplot as plt

feature_dim = 2
fig, axs = plt.subplots(9, 2)
i_plot = 0
for paulis in [
["I"],
["X"],
["Y"],
["Z"],
["XX"],
["XY"],
["XZ"],
["YY"],
["YZ"],
["ZZ"],
["X", "ZZ"],
["Y", "ZZ"],
["Z", "ZZ"],
["X", "YZ"],
["Y", "YZ"],
["Z", "YZ"],
["YY", "ZZ"],
["XY", "ZZ"],
]:
pfmap = pauli_feature_map(feature_dimension=feature_dim, paulis=paulis, reps=1)
circuit_drawer(
pfmap.decompose(),
output="mpl",
style={"backgroundcolor": "#EEEEEE"},
ax=axs[int((i_plot - i_plot % 2) / 2), i_plot % 2],
)
axs[int((i_plot - i_plot % 2) / 2), i_plot % 2].title.set_text(paulis)
i_plot += 1

fig.set_figheight(16)
fig.set_figwidth(16)

Output of the previous code cell

O acima pode, é claro, ser estendido para incluir outras permutações e repetições de matrizes de Pauli. Encorajamos os estudantes a experimentar com essas opções.

Revisão dos feature maps integrados

Você viu vários esquemas para codificar dados em um circuito quântico:

  • Codificação por base
  • Codificação por amplitude
  • Codificação por ângulo
  • Codificação por fase
  • Codificação densa

Você viu como construir seus próprios feature maps usando esses esquemas de codificação, e viu quatro feature maps integrados que aproveitam a codificação por ângulo e por fase:

  • Efficient SU2
  • Feature map Z
  • Feature map ZZ
  • Feature map de Pauli

Esses feature maps integrados se diferenciaram uns dos outros de várias formas:

  • A profundidade para um dado número de features codificadas
  • O número de qubits necessários para um dado número de features
  • O grau de entrelaçamento (obviamente relacionado às outras diferenças)

O código abaixo aplica esses quatro feature maps integrados à codificação de um conjunto de features e plota a profundidade de dois qubits do circuito resultante. Como as taxas de erro de dois qubits são muito mais altas do que as de portões de qubit único, pode-se razoavelmente ter maior interesse na profundidade das portas de dois qubits. No código abaixo, obtemos contagens de todas as portas em um circuito primeiro decompondo o circuito e depois usando count_ops(), como mostrado abaixo. Aqui as portas de dois qubits que nos interessam são as portas 'cx':

# Initializing parameters and empty lists for depths
x = [0.1, 0.2]
n_data = []
zz2gates = []
su22gates = []
z2gates = []
p2gates = []

# Generating feature maps
for n in range(3, 10):
x.append(n / 10)
zzcircuit = zz_feature_map(n, reps=1, insert_barriers=True)
zcircuit = z_feature_map(n, reps=1, insert_barriers=True)
su2circuit = efficient_su2(n, reps=1, insert_barriers=True)
pcircuit = pauli_feature_map(n, reps=1, paulis=["XX"], insert_barriers=True)
# Getting the cx depths
zzcx = zzcircuit.decompose().count_ops().get("cx")
zcx = zcircuit.decompose().count_ops().get("cx")
su2cx = su2circuit.decompose().count_ops().get("cx")
pcx = pcircuit.decompose().count_ops().get("cx")

# Appending the cx gate counts to the lists. We shift the zz and pauli data points, because they overlap.
n_data.append(n)
zz2gates.append(zzcx - 0.5)
z2gates.append(0)
su22gates.append(su2cx)
p2gates.append(pcx + 0.5)

# Plot the output
plt.plot(n_data, p2gates, "bo")
plt.plot(n_data, zz2gates, "ro")
plt.plot(n_data, su22gates, "yo")
plt.plot(n_data, z2gates, "go")
plt.ylabel("CX Gates")
plt.xlabel("Data elements")
plt.legend(["Pauli", "ZZ", "SU2", "Z"])
# plt.suptitle('zz_feature_map(n)')
plt.show()

Em geral, os feature maps Pauli e ZZ resultarão em maior profundidade de circuito e maior número de portas de 2 qubits do que o efficient_su2 e o feature map Z.

Como os feature maps integrados ao Qiskit são amplamente aplicáveis, muitas vezes não precisaremos projetar os nossos próprios, especialmente na fase de aprendizado. No entanto, especialistas em aprendizado de máquina quântico provavelmente voltarão ao tema de projetar seu próprio mapeamento de features, ao enfrentarem dois desafios complexos:

  1. Hardware moderno: a presença de ruído e o grande overhead de código de correção de erros significa que as aplicações atuais precisarão considerar coisas como eficiência de hardware e minimização da profundidade de portas de dois qubits.

  2. Mapeamentos que se adequam ao problema em questão: Uma coisa é dizer que o zz_feature_map, por exemplo, é difícil de simular classicamente, e portanto interessante. Outra coisa bem diferente é o zz_feature_map ser ideal para sua tarefa de aprendizado de máquina ou conjunto de dados. O desempenho de diferentes circuitos quânticos parametrizados em diferentes tipos de dados é uma área ativa de investigação.

Encerramos com uma observação sobre eficiência de hardware.

Mapeamento de features eficiente em hardware

Um mapeamento de features eficiente em hardware é aquele que leva em conta as restrições de computadores quânticos reais, com o objetivo de reduzir ruído e erros na computação. Ao rodar circuitos quânticos em computadores quânticos de curto prazo, há muitas estratégias para mitigar o ruído inerente ao hardware. Uma estratégia principal para eficiência de hardware é a minimização da profundidade do circuito quântico, para que o ruído e a decoerência tenham menos tempo para corromper a computação. A profundidade de um circuito quântico é o número de etapas de portas alinhadas temporalmente necessárias para completar toda a computação (após a otimização do circuito)[5]. Lembre-se de que a profundidade do circuito lógico abstrato pode ser muito menor do que a profundidade após o circuito ser transpilado para um computador quântico real.

A transpilação é o processo de converter o circuito quântico de uma abstração de alto nível para um que esteja pronto para rodar em um computador quântico real, levando em conta as restrições do hardware. Um computador quântico tem um conjunto nativo de portas de qubit único e de dois qubits. Isso significa que todas as portas no código Qiskit precisam ser transpiladas para o conjunto de portas de hardware nativas. Por exemplo, no ibm_torino, um QPU com um processador Heron r1 concluído em 2023, as portas nativas ou de base são \{CZ, ID, RZ, SX, X\}. São elas a porta de dois qubits controlled-Z, e portas de qubit único chamadas identidade, rotação-ZZ, raiz quadrada de NOT, e NOT, respectivamente, fornecendo um conjunto universal. Ao implementar portas de múltiplos qubits como um subcircuito equivalente, são necessárias portas físicas de dois qubits CZCZ, juntamente com outras portas de qubit único disponíveis no hardware. Além disso, para realizar uma porta de dois qubits em um par de qubits que não estão fisicamente acoplados, portas SWAP são adicionadas para mover os estados de qubits entre qubits para permitir o acoplamento, o que leva a uma extensão inevitável do circuito. Usando o argumento optimization que pode ser definido de 0 até um nível máximo de 3. Para maior controle e personalização, o pipeline do transpilador pode ser gerenciado com o Qiskit Pass Manager. Consulte a documentação do Transpilador Qiskit para mais informações sobre transpilação.

Em Havlicek et al. 2019 [2], uma forma como os autores alcançam eficiência de hardware é usando o feature map ZZZZ porque ele é uma expansão de segunda ordem (veja a seção "feature map ZZZZ" acima). Uma expansão de ordem NN tem portas de NN qubits. Os computadores quânticos IBM® não possuem portas nativas de NN qubits, onde N>2N>2, portanto implementá-las exigiria decomposição em portas CNOT de dois qubits disponíveis no hardware. Uma segunda forma pela qual os autores minimizam a profundidade é escolhendo uma topologia de acoplamento ZZZZ que mapeia diretamente para os acoplamentos da arquitetura. Uma otimização adicional que eles realizam é selecionar um subcircuito de hardware de alto desempenho e adequadamente conectado. Outras coisas a considerar são minimizar o número de repetições do feature map e escolher um esquema de entrelaçamento personalizado de baixa profundidade ou "linear" em vez do esquema "full" que entrelaça todos os qubits.

Data encoding image

O gráfico acima mostra uma rede de nós e arestas que representam qubits físicos e acoplamentos de hardware, respectivamente. O mapa de acoplamento e o desempenho do ibm_torino são mostrados com todas as possíveis portas de acoplamento CZ de dois qubits. Os qubits são codificados por cores em uma escala baseada no tempo de relaxação T1 em microssegundos (μs), onde tempos T1 mais longos são melhores e em um tom mais claro. As arestas de acoplamento são codificadas por cores pelo erro CZ, onde tons mais escuros são melhores. Informações sobre a especificação do hardware podem ser acessadas no esquema de configuração do backend de hardware IBMQBackend.configuration().

Referências

  1. Maria Schuld and Francesco Petruccione, Supervised Learning with Quantum Computers, Springer 2018, doi:10.1007/978-3-319-96424-9.
  2. Vojtech Havlicek et al., "Supervised Learning with Quantum Enhanced Feature Spaces." Nature, vol. 567 (2019): 209–212. https://arxiv.org/abs/1804.11326.
  3. Ryan LaRose and Brian Coyle, "Robust data encodings for quantum classifiers", Physical Review A 102, 032420 (2020), doi:10.1103/PhysRevA.102.032420, arXiv:2003.01695.
  4. Lou Grover and Terry Rudolph. "Creating Superpositions That Correspond to Efficiently Integrable Probability Distributions." arXiv:quant-ph/0208112, August 15, 2002, https://arxiv.org/abs/quant-ph/0208112.
  5. Adrián Pérez-Salinas, Alba Cervera-Lierta, Elies Gil-Fuster, José I. Latorre, "Data re-uploading for a universal quantum classifier", Quantum 4, 226 (2020), ArXiv.org/abs/1907.02085.
  6. Maria Schuld, Ryan Sweke, Johannes Jakob Meyer, "The effect of data encoding on the expressive power of variational quantum machine learning models", Phys. Rev. A 103, 032430 (2021), arxiv.org/abs/2008.08605
import qiskit

qiskit.version.get_version_info()