May 03, 2024 | 6 min read

\( \newcommand{\bra}[1]{\langle #1|} \) \( \newcommand{\ket}[1]{|#1\rangle} \) \( \newcommand{\braket}[2]{\langle #1|#2\rangle} \) \( \newcommand{\ketbra}[2]{| #1\rangle \langle #2|} \) \( \newcommand{\i}{{\color{blue} i}} \) \( \newcommand{\Hil}{{\mathbb H}} \) \( \newcommand{\boldn}{{\bf n}} \) \( \newcommand{\tr}{{\rm tr}}\) \( \newcommand{\bn}{{\bf n}} \)

Consulta la notación que se ha utilizado durante todo el documento en el siguiente enlace.

2. Feature encoding#

Esta sección se centra en el proceso de codificación de caracteristicas o feature encoding, se encarga de codificar datos clásicos en estados cuánticos. Es un parte muy delicada en procesamiento de la información cuántica debido a que te dará el mapa que proyectará tus datos clásicos en el espacio de hilbert.

Nota

Dependiendo de cómo esté proyectado este conjunto de datos en el espacio de Hilbert, el tratamiento que se le tendrá que hacer para extraer información será muy simple o, si no se hace adecuadamente, imposible.

Se empieza con un conjunto de datos \(\mathbf{x}\) y se quiere codificar en un estado \(\ket{\psi_x}\) con el que trabajar, para ello se construye un circuito parametrizado que generará una unitaria \(\mathcal{U}(\mathbf{x})\) tal que \(\ket{\psi_x}=\mathcal{U}(\mathbf{x})\ket{0}\). Dependiendo del tipo de feature encoding que se utilice, la unitaria \(\mathcal{U}(x)\) tendrá diferentes características.

Se define un conjunto de datos (dataset) de entrada \(D\) de dimensión \(M\) x \(N\), es decir, se cuenta con un dataset con M instancias y N variables. Este dataset se puede expresar como \(D = {\mathbf{x}_{(1)}, ..., \mathbf{x}_{(m)}, ..., \mathbf{x}_{(M)}}\) donde cada \(\mathbf{x}_{(m)}\) es un vector de dimensión N.

Hay multiples técnicas de feature encoding, a continuación se explicarán las más básicas y se dará un ejemplo de cómo implementar cada una. Para más detalles consultar[24],[36].

2.1. Basis encoding#

La técnica Basis encoding, también conocida como Basis Embedding, asocia cada elemento del dataset a un estado de la base computacional de un sistema de qubits. Es por eso que los datos clásicos deben estar representados por una cadena de bits de longitud equivalente al número de qubits del circuito. Esto da una correspondencia uno a uno entre elementos del dataset y vectores de la base computacional del espacio de Hilbert.

Se considera el conjunto de datos \(D\), definido anteriormente. Asumimos por simplicidad que podemos codificar las \(N\) características con un número binario de longitud \(N\), tal que \(\mathbf{x}_m = (b_1,...,b_N)\) con \(b_i\in { 0,1}\) para \(i=1,...,N\). De esta manera, cada elemento del conjunto de datos \(\mathbf{x}_{m}\) se puede representar como \(\ket{\mathbf{x}_{m}}\). Extendiendo la expresión anterior a todo el dataset, obtenemos la representación del conjunto de datos al completo tal que:

\[\ket{D} = \frac{1}{\sqrt M} \sum^M_{m=1} \ket{\mathbf{x}_{m}}\]

Por ejemplo, si consideramos que el dataset \( D=\{1,3,5,7\} \) los datos en binario corresponden a \( D=\{001,011,101,111\} \). En este caso, el estado correspondiente sería:

\[\ket{D} = \frac{1}{\sqrt{4}}(\ket{001}+\ket{011}+\ket{101}+\ket{111})\]

Que como vector en la base computacional corresponde a \(\vec{x}=\frac{1}{2}(0,1,0,1,0,1,0,1)\).

import numpy as np
import qibo
import matplotlib.pyplot as plt
from qibo import callbacks, gates, hamiltonians, models
from qibo.symbols import Y, Z, I
from qibo.models import Circuit
# Número de qubits de nuestro ejemplo
num_q=3 

Nota

El circuito que codifica el dataset entero no es trivial y se debe hacer a mano. Aun así para muchas aplicaciones no es necesario codificar el dataset entero y se puede pasar los elemenotos del mismo de uno a uno. La función Basis_encoding codifica un elemento del dataset en el circuito.

# Funcion que codifica 1 vector binario x ens el estado |x>

def Basis_encoding(x, nqubits= 2): 
    c= Circuit(nqubits=nqubits)
    
    for i in range(nqubits):
        if x[i]==1:
            c.add(gates.X(q=i))
    return c
x = 0.5*np.array([0,1,0,1,0,1,0,1])
c_b=Circuit(num_q)
c_b.add(gates.H(q=0))
c_b.add(gates.H(q=1))
c_b.add(gates.X(q=2))


print("x               : ", x)
print("amplitude vector: ", np.array(c_b.execute().state()))
print(c_b.draw())
x               :  [0.  0.5 0.  0.5 0.  0.5 0.  0.5]
[Qibo 0.1.12.dev0|INFO|2024-06-11 12:05:59]: Using tensorflow backend on /device:CPU:0
amplitude vector:  [0. +0.j 0.5+0.j 0. +0.j 0.5+0.j 0. +0.j 0.5+0.j 0. +0.j 0.5+0.j]
q0: ─H─
q1: ─H─
q2: ─X─

2.2. Amplitude encoding#

La técnica Amplitude encoding o amplitude embedding, codifica los datos clásicos como las amplitudes del vector de un estado cuántico. En este caso, un elemento \(\mathbf{x_m}=(b_0,...b_N)\) del conjunto de datos \(D\) con \(N\) características se codifica en un sistema de \(2^n=N\) qubits tal que:

\[ \ket{\psi_x} = \sum^N_{i=1} b_i\ket{i}\]

Donde \(b_i\) es el i-ésimo elemento del vector \(\mathbf{x_m}\) y \(\ket{i}\) es el i-ésimo estado de la base computacional. A diferencia de la técnica basis encoding, \(x_i\) puede tomar valores de distintos tipos, como integer (valores enteros) o float (valores de coma flotante).

Como ejemplo, si tratamos de codificarel vector \(\mathbf{x}=(1,3,5,7)\) utilizando esta técnica obtendremos:

\[\ket{\psi_x} = \frac{1}{2\sqrt{21}} [1\ket{00}+3\ket{01}+5\ket{10}+7\ket{11}]\]

Nota

Hay que tener en cuenta que todos los vectores cuánticos deben estar siempre normalizados, \(\bra{\psi_x}\ket{\psi_x} = 1\)

num_q=2
def Normalize(x):
    N=np.linalg.norm(x)
    return 1/N*x
x = np.array([1,3,5,7])

# Normalizamos x
x_norm=Normalize(x)

# No hay una funcion para ejecutar el circuito que codifica el estado x en la amplitud pero podemos inizializar con un estado arbitrario el circuito en qibo.
c_a = Circuit(num_q)

print("x               : ", x)
print("amplitude vector: ", np.array(c_a(x_norm,nshots=10000).state()))
print(c_a.draw())
x               :  [1 3 5 7]
amplitude vector:  [0.10910895+0.j 0.32732684+0.j 0.54554473+0.j 0.76376262+0.j]
q0: ─
q1: ─

Para codificar un vector arbitrario en las amplitudes de un estado cuántico con el mínimo número de puertas cuánticas hay que utilizar los resultados de [27]. Una implementación de este método en qibo todavía no está desarrollada. Por esa razon de momento se pasa el vector cuántico explicitamente.

2.3. Angle encoding#

Angle encoding es la tecnica de codificación más fácil de implementar debido a que utiliza las puertas parametrizadas más basicas para ello. Las N características de los elementos del dataset se codifican como ángulos de rotación de N cúbits. Esta metodología codifica N características en un ángulos de rotación de N qubits. Dichas rotaciones pueden llevarse a cabo en cualquier eje, tanto en el \(X\), como en el \(Y\) o en el \(Z\). Mas explicitamente si queremos codificar el vector del conjuto de datos \(\mathbf{x_m}=(x_1,...,x_N)\) con rotaciones en el eje \(X\) el estado cuántico resultante sería:

\[ \ket {\mathbf{x}} = \bigotimes^N_{i=1} \cos(x_i)\ket 0 + \sin(x_i)\ket 1 \]

Por ejemplo, si se trata de codificar el vector \(\mathbf{x}=(1,3,5,7)\) con este tipo de encoding, se necesitarán cuatro cúbits y quedará como sigue:

\[ \ket{\mathbf{x}} = (\cos(1)\ket{0} + \sin(1)\ket{1} ) \otimes (\cos(3)\ket{0} + \sin(3)\ket{1}) \otimes (\cos(5)\ket{0} + \sin(5)\ket{1}) \otimes (\cos(7)\ket{0} + \sin(7)\ket{1} ) \]
num_q=4
def Angle(x,nqubits):
    c= Circuit(nqubits=nqubits)
    
    for i in range(nqubits):
        c.add(gates.RX(q=i,theta=x[i]))
    return c
x = np.array([1,3,5,7])

c_an=Angle(x,num_q)

print("x               : ", x)
print("amplitude vector: ", np.array(c_an.execute().state()))
print(c_an.draw())
x               :  [1 3 5 7]
amplitude vector:  [ 0.04657297+0.j          0.        -0.01744557j  0.        +0.03479105j
  0.01303223+0.j          0.        -0.656745j   -0.24600725+0.j
  0.49060316+0.j          0.        -0.1837729j   0.        -0.02544293j
 -0.00953056+0.j          0.01900644+0.j          0.        -0.00711954j
 -0.35878143+0.j          0.        +0.13439437j  0.        -0.26801773j
 -0.10039559+0.j        ]
q0: ─RX─
q1: ─RX─
q2: ─RX─
q3: ─RX─

2.2.4. Dense angle encoding#

Esta técnica, tal como su nombre indica, es una versión más sofisticada de la codificación anterior. Es capaz de codificar dos características por cada qubit, haciendo uso de fases relativas. En este caso, la instancia \(\mathbf{x}=(x_1,...,x_N)\) se codifica como sigue:

\[ \ket{x} = \bigotimes^{N/2}_{i=1} \cos(x_{2i-1})\ket{0} + e^{ix_{2i}}\sin(x_{2i-1})\ket{1} \]

Nota

Angle encoding necesita \(num \, qubits = \text{dim}(\mathbf{x})\), mientras que dense angle requiere \(num \, qubits = \text{dim}(\mathbf{x})/2\). Aun así, hay aplicaciones donde la simplicidad de angle encoding es preferible.

Por ejemplo, si se trata de codificar el vector \(\mathbf{x}=(1,3,5,7)\) con este tipo de encoding, se necesitarán dos cúbits y quedará como sigue:

\[ \ket{\mathbf{x}} = (\cos(1)\ket{0} + e^{i3}\sin(1)\ket{1} ) \otimes (\cos(5)\ket{0} + e^{i7}\sin(5)\ket{1}) \]
num_q=2
def denseAngle(x,nqubits):
    c= Circuit(nqubits=nqubits)
    
    for i in range(nqubits):
        c.add(gates.RX(q=i,theta=x[i]))
        c.add(gates.RZ(q=i,theta=x[i+1]))
    return c
x = np.array([0,1,2,3])

c_dan=denseAngle(x,num_q)

print("x               : ", x)
print("amplitude vector: ", np.array(c_dan.execute().state()))
print(c_dan.draw())
x               :  [0 1 2 3]
amplitude vector:  [0.06207773-0.87538421j 0.22984885-0.42073549j 0.        +0.j
 0.        +0.j        ]
q0: ─RX─RZ─
q1: ─RX─RZ─

En este apartado se muestran algunas de las técnicas de codificación, no obstante existen otras como Displacement Embedding, IQP Embedding, QAOA Embedding…

2.3. Cómo escoger Feature Encoding#

Cuando se trata con circuitos variacionales, la decisión de qué feature encoding utilizar es crucial. Los diferentes feature encoding presentan diferentes ventajas e inconvenientes dependiendo del problema que queramos resolver. Las técnicas mencionadas en este notebook se pueden separar en dos tipos:

  • Basis encoding donde se trabaja con los elementos de la base computacional como inputs (entradas).

  • El resto de codificadores que trabajan con las amplitudes del vector cuántico.

La técnica de basis encoding presenta la capacidad de calcular operaciones no lineales de forma natural, a cambio es el codificador que presenta mayor número de problemas:

  • Es el más susceptible a errores.

  • Escala muy mal con el número de cúbits, ya que de esto depende la precisión de los feature que se quiere codificar.

  • Es el método que presenta mayor dificultad a la hora de entrenar la red neuronal y tiene tendencia a presentar barren plateaus (gradientes que tienden a cero).

El resto de métodos basados en codificar en las amplitudes del vector cuántico no presentan los inconvenientes enumerados anteriormente y son los métodos preferidos para desarrollar algoritmos en la era del NISQ (Noisy Intermediate-Scale Quantum). Aún así, presentan el inconveniente de que hay que escoger sabiamente cómo introducir la no linealidad necesaria para cada problema. La forma más genérica es utilizar Amplitude encoding y aplicar una función \(f(x)\) a tus datos para obtener correlaciones entre ellos y así poder resolver problemas no lineales. Este método, que es a primera vista sencillo, presenta el problema de que la amplitud del circuito puede ser muy grande dependiendo de \(f(x)\) y además, escoger \(f(x)\) puede ser un proceso muy arbitrario. Por otra parte, otros métodos de codificación más sencillos pueden proporcionar directamente los elementos de no linealidad necesarios para resolver el problema.

Para finalizar, existen otras técnicas más sofisticadas para introducir datos clásicos en un circuito variacional, una de ellas es conocida como data re-uploading [30] el cual se cubrira en el siguiente notebook.

ANEXO NOTACIÓN

Para que la comprensión de los notebooks sea mejor se ha unificado la notación utilizada en los mismos. Para diferenciar un vector de un valor único se hará uso de la negrita. De manera que \(\mathbf{x}\) corresponde a un vector y \(z\) será una variable de una única componente.

Si se quiere hacer referencia a dos vectores distintos pero que pertenecen al mismo dataset se utilizará un subíndice, es decir, \(\mathbf{x_i}\) hará referencia al i-ésimo vector del dataset. Si se quiere referenciar una característica concreta del vector \(\mathbf{x_i}\) se añadirá un nuevo subíndice, de manera que \(\mathbf{x_{i_j}}\) hará referencia a la j-ésima variable del i-ésimo vector.


Autores:

Carmen Calvo (SCAYLE), Antoni Alou (PIC), Carlos Hernani (UV), Nahia Iriarte (NASERTIC) y Carlos Luque (IAC)

../../_images/LOGO-SCAILE.png ../../_images/Logo_pic.png ../../_images/Logo_UV.jpg ../../_images/Logo_Nasertic.png ../../_images/Logo_IAC.jpg
https://quantumspain-project.es/wp-content/uploads/2022/11/Logo_QS_EspanaDigital.png
Licencia Creative Commons

License: Licencia Creative Commons Atribución-CompartirIgual 4.0 Internacional.

This work has been financially supported by the Ministry for Digital Transformation and of Civil Service of the Spanish Government through the QUANTUM ENIA project call - Quantum Spain project, and by the European Union through the Recovery, Transformation and Resilience Plan - NextGenerationEU within the framework of the Digital Spain 2026 Agenda.