{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Quantum Autoencoder\n", "\n", " Copyright (c) 2021 Institute for Quantum Computing, Baidu Inc. All Rights Reserved. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Overview\n", "\n", "This tutorial will show how to train a quantum autoencoder to compress and reconstruct a given quantum state (mixed state) [1].\n", "\n", "### Theory\n", "\n", "The form of the quantum autoencoder is very similar to the classical autoencoder, which is composed of an encoder $E$ and a decoder $D$. For the input quantum state $\\rho_{in}$ of the $N$ qubit system (here we use the density operator representation of quantum mechanics to describe the mixed state), first use the encoder $E = U(\\theta)$ to encode information into some of the qubits in the system. This part of qubits is denoted by **system $A$**. After measuring and discarding the remaining qubits (this part is denoted by **system $B$**), we get the compressed quantum state $\\rho_{encode}$! The dimension of the compressed quantum state is the same as the dimension of the quantum system $A$. Suppose we need $N_A$ qubits to describe the system $A$, then the dimension of the encoded quantum state $\\rho_{encode}$ is $2^{N_A}\\times 2^{N_A}$. Note that the mathematical operation corresponding to the measure-and-discard operation in this step is partial trace. The reader can intuitively treat it as the inverse operation of the tensor product $\\otimes$.\n", "\n", "Let us look at a specific example. Given a quantum state $\\rho_A$ of $N_A$ qubits and another quantum state $\\rho_B$ of $N_B$ qubits, the quantum state of the entire quantum system composed of subsystems $A$ and $B$ is $\\rho_{AB} = \\rho_A \\otimes \\rho_B$, which is a state of $N = N_A + N_B$ qubits. Now we let the entire quantum system evolve under the action of the unitary matrix $U$ for some time to get a new quantum state $\\tilde{\\rho_{AB}} = U\\rho_{AB}U^\\dagger$. So if we only want to get the new quantum state $\\tilde{\\rho_A}$ of quantum subsystem A at this time, what should we do? We simply measure the quantum subsystem $B$ and then discard it. This step of the operation is completed by partial trace $\\tilde{\\rho_A} = \\text{Tr}_B (\\tilde{\\rho_{AB}})$. With Paddle Quantum, we can call the built-in function `partial_trace(rho_AB, 2**N_A, 2**N_B, 2)` to complete this operation. **Note:** The last parameter is 2, which means that we want to discard quantum system $B$.\n", "\n", "![QA-fig-encoder_pipeline](./figures/QA-fig-encoder_pipeline.png)\n", "\n", "After discussing the encoding process, let us take a look at how decoding is done. To decode the quantum state $\\rho_{encode}$, we need to introduce an ancillary system $C$ with the same dimension as the system $B$ and take its initial state as the $|0\\dots0\\rangle$ state. Then use the decoder $D = U^\\dagger(\\theta)$ to act on the entire quantum system $A+C$ to decode the compressed information in system A. We hope that the final quantum state $\\rho_{out}$ and $\\rho_{in}$ are as similar as possible and use Uhlmann-Josza fidelity $F$ to measure the similarity between them.\n", "\n", "$$\n", "F(\\rho_{in}, \\rho_{out}) = \\left(\\operatorname{tr} \\sqrt{\\sqrt{\\rho_{in}} \\rho_{out} \\sqrt{\\rho_{in}}} \\right)^{2}.\n", "\\tag{1}\n", "$$\n", "\n", "Finally, by optimizing the encoder's parameters, we can improve the fidelity of $\\rho_{in}$ and $\\rho_{out}$ as much as possible." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Paddle Quantum Implementation\n", "\n", "Next, we will use a simple example to show the workflow of the quantum autoencoder. Here we first import the necessary packages." ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "ExecuteTime": { "end_time": "2021-04-30T08:57:25.375761Z", "start_time": "2021-04-30T08:57:25.355799Z" } }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "from IPython.core.display import HTML\n", "display(HTML(\"\"))" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "ExecuteTime": { "end_time": "2021-04-30T08:57:29.266507Z", "start_time": "2021-04-30T08:57:25.894969Z" } }, "outputs": [], "source": [ "import numpy as np\n", "from numpy import diag\n", "import scipy\n", "import scipy.stats\n", "import paddle\n", "from paddle import matmul, trace, kron, real\n", "from paddle_quantum.circuit import UAnsatz\n", "from paddle_quantum.utils import dagger, state_fidelity, partial_trace" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Generating the initial state\n", "\n", "Let us consider the quantum state $\\rho_{in}$ of $N = 3$ qubits. We first encode the information into the two qubits below (system $A$) through the encoder then measure and discard the first qubit (system $B$). Secondly, we introduce another qubit (the new reference system $C$) in state $|0\\rangle$ to replace the discarded qubit $B$. Finally, through the decoder, the compressed information in A is restored to $\\rho_{out}$. Here, we assume that the initial state is a mixed state and the spectrum of $\\rho_{in}$ is $\\lambda_i \\in \\{0.4, 0.2, 0.2, 0.1, 0.1, 0, 0, 0\\}$, and then generate the initial state $\\rho_{in}$ by applying a random unitary transformation.\n", "\n" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "ExecuteTime": { "end_time": "2021-04-30T08:57:29.283394Z", "start_time": "2021-04-30T08:57:29.270542Z" } }, "outputs": [], "source": [ "N_A = 2 # Number of qubits in system A\n", "N_B = 1 # Number of qubits in system B\n", "N = N_A + N_B # Total number of qubits\n", "\n", "scipy.random.seed(1) # Fixed random seed\n", "V = scipy.stats.unitary_group.rvs(2**N) # Generate a random unitary matrix\n", "D = diag([0.4, 0.2, 0.2, 0.1, 0.1, 0, 0, 0]) # Enter the spectrum of the target state rho\n", "V_H = V.conj().T # Apply Hermitian transpose\n", "rho_in = (V @ D @ V_H).astype('complex128') # Generate rho_in\n", "\n", "# Initialize the quantum system C\n", "rho_C = np.diag([1,0]).astype('complex128')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Building a quantum neural network\n", "\n", "Here, we use quantum neural networks (QNN) as encoders and decoders. Suppose system A has $N_A$ qubits, both system $B$ and $C$ have $N_B$ qubits, and the depth of the QNN is $D$. Encoder $E$ acts on the total system composed of systems A and B, and decoder $D$ acts on the total system composed of $A$ and $C$. In this example, $N_{A} = 2$ and $N_{B} = 1$." ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "ExecuteTime": { "end_time": "2021-04-30T08:57:29.301622Z", "start_time": "2021-04-30T08:57:29.288113Z" } }, "outputs": [], "source": [ "# Set circuit parameters\n", "cir_depth = 6 # Circuit depth\n", "block_len = 2 # The length of each block\n", "theta_size = N*block_len*cir_depth # The size of the circuit parameter theta\n", "\n", "\n", "# Build the encoder E\n", "def Encoder(theta):\n", "\n", " # Initialize the network with UAnsatz\n", " cir = UAnsatz(N)\n", " \n", " # Build the network by layers\n", " for layer_num in range(cir_depth):\n", " \n", " for which_qubit in range(N):\n", " cir.ry(theta[block_len*layer_num*N + which_qubit], which_qubit)\n", " cir.rz(theta[(block_len*layer_num + 1)*N+ which_qubit], which_qubit)\n", "\n", " for which_qubit in range(N-1):\n", " cir.cnot([which_qubit, which_qubit + 1])\n", " cir.cnot([N-1, 0])\n", "\n", " return cir" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Configuring the training model: loss function\n", "\n", "Here, we define the loss function to be\n", "\n", "$$\n", "Loss = 1-\\langle 0...0|\\rho_{trash}|0...0\\rangle,\n", "\\tag{2}\n", "$$\n", "\n", "where $\\rho_{trash}$ is the quantum state of the system $B$ discarded after encoding. Then we train the QNN through PaddlePaddle to minimize the loss function. If the loss function reaches 0, the input state and output state will be exactly the same state. This means that we have achieved compression and decompression perfectly, in which case the fidelity of the initial and final states is $F(\\rho_{in}, \\rho_{out}) = 1$." ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "ExecuteTime": { "end_time": "2021-04-30T08:57:52.962725Z", "start_time": "2021-04-30T08:57:42.476996Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "iter: 10 loss: 0.1683 fid: 0.8211\n", "iter: 20 loss: 0.1231 fid: 0.8720\n", "iter: 30 loss: 0.1122 fid: 0.8810\n", "iter: 40 loss: 0.1058 fid: 0.8864\n", "iter: 50 loss: 0.1025 fid: 0.8901\n", "iter: 60 loss: 0.1019 fid: 0.8907\n", "iter: 70 loss: 0.1013 fid: 0.8914\n", "iter: 80 loss: 0.1012 fid: 0.8917\n", "iter: 90 loss: 0.1010 fid: 0.8921\n", "iter: 100 loss: 0.1008 fid: 0.8924\n", "\n", "The trained circuit:\n", "--Ry(3.935)----Rz(2.876)----*---------X----Ry(2.678)----Rz(6.372)----*---------X----Ry(5.516)----Rz(4.082)----*---------X----Ry(1.199)----Rz(1.584)----*---------X----Ry(4.512)----Rz(0.847)----*---------X----Ry(5.038)----Rz(0.564)----*---------X--\n", " | | | | | | | | | | | | \n", "--Ry(2.045)----Rz(4.282)----X----*----|----Ry(6.116)----Rz(6.203)----X----*----|----Ry(5.135)----Rz(4.828)----X----*----|----Ry(3.532)----Rz(3.827)----X----*----|----Ry(0.497)----Rz(1.693)----X----*----|----Ry(5.243)----Rz(5.329)----X----*----|--\n", " | | | | | | | | | | | | \n", "--Ry(2.706)----Rz(4.168)---------X----*----Ry(2.141)----Rz(2.014)---------X----*----Ry(5.364)----Rz(-0.34)---------X----*----Ry(4.014)----Rz(2.668)---------X----*----Ry(3.419)----Rz(1.952)---------X----*----Ry(4.255)----Rz(1.856)---------X----*--\n", " \n" ] } ], "source": [ "# Set hyper-parameters\n", "N_A = 2 # Number of qubits in system A\n", "N_B = 1 # Number of qubits in system B\n", "N = N_A + N_B # Total number of qubits\n", "LR = 0.2 # Set the learning rate\n", "ITR = 100 # Set the number of iterations\n", "SEED = 15 # Fixed random number seed for initializing parameters\n", "\n", "class NET(paddle.nn.Layer):\n", " def __init__(self, shape, dtype='float64'):\n", " super(NET, self).__init__()\n", " \n", " # Convert Numpy array to Tensor supported in PaddlePaddle\n", " self.rho_in = paddle.to_tensor(rho_in)\n", " self.rho_C = paddle.to_tensor(rho_C)\n", " self.theta = self.create_parameter(shape=shape,\n", " default_initializer=paddle.nn.initializer.Uniform(low=0.0, high=2 * np.pi),\n", " dtype=dtype, is_bias=False)\n", " \n", " # Define loss function and forward propagation mechanism\n", " def forward(self):\n", " \n", " # Generate initial encoder E and decoder D\n", " cir = Encoder(self.theta)\n", " E = cir.U\n", " E_dagger = dagger(E)\n", " D = E_dagger\n", " D_dagger = E\n", "\n", " # Encode the quantum state rho_in\n", " rho_BA = matmul(matmul(E, self.rho_in), E_dagger)\n", " \n", " # Take partial_trace() to get rho_encode and rho_trash\n", " rho_encode = partial_trace(rho_BA, 2 ** N_B, 2 ** N_A, 1)\n", " rho_trash = partial_trace(rho_BA, 2 ** N_B, 2 ** N_A, 2)\n", "\n", " # Decode the quantum state rho_out\n", " rho_CA = kron(self.rho_C, rho_encode)\n", " rho_out = matmul(matmul(D, rho_CA), D_dagger)\n", " \n", " # Calculate the loss function with rho_trash\n", " zero_Hamiltonian = paddle.to_tensor(np.diag([1,0]).astype('complex128'))\n", " loss = 1 - real(trace(matmul(zero_Hamiltonian, rho_trash)))\n", "\n", " return loss, self.rho_in, rho_out, cir\n", "\n", "\n", "paddle.seed(SEED)\n", "# Generate network\n", "net = NET([theta_size])\n", "# Generally speaking, we use Adam optimizer to get relatively good convergence\n", "# Of course, it can be changed to SGD or RMS prop.\n", "opt = paddle.optimizer.Adam(learning_rate=LR, parameters=net.parameters())\n", "\n", "# Optimization loops\n", "for itr in range(1, ITR + 1):\n", " # Forward propagation for calculating loss function\n", " loss, rho_in, rho_out, cir = net()\n", " # Use back propagation to minimize the loss function\n", " loss.backward()\n", " opt.minimize(loss)\n", " opt.clear_grad()\n", " # Calculate and print fidelity\n", " fid = state_fidelity(rho_in.numpy(), rho_out.numpy())\n", " if itr% 10 == 0:\n", " print('iter:', itr,'loss:','%.4f'% loss,'fid:','%.4f'% np.square(fid))\n", " if itr == ITR:\n", " print(\"\\nThe trained circuit:\")\n", " print(cir)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If the dimension of system A is denoted by $d_A$, it is easy to prove that the maximum fidelity can be achieved by quantum autoencoder is the sum of $d_A$ largest eigenvalues ​​of $\\rho_{in}$. In our case $d_A = 4$ and the maximum fidelity is\n", "\n", "$$\n", "F_{\\text{max}}(\\rho_{in}, \\rho_{out}) = \\sum_{j=1}^{d_A} \\lambda_j(\\rho_{in})= 0.4 + 0.2 + 0.2 + 0.1 = 0.9.\n", "\\tag{3}\n", "$$\n", "\n", "After 100 iterations, the fidelity achieved by the quantum autoencoder we trained reaches above 0.89, which is very close to the optimal value." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "_______\n", "\n", "## References\n", "\n", "[1] Romero, J., Olson, J. P. & Aspuru-Guzik, A. Quantum autoencoders for efficient compression of quantum data. [Quantum Sci. Technol. 2, 045001 (2017).](https://iopscience.iop.org/article/10.1088/2058-9565/aa8072)\n", "\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.10" }, "toc": { "base_numbering": 1, "nav_menu": {}, "number_sections": true, "sideBar": true, "skip_h1_title": false, "title_cell": "Table of Contents", "title_sidebar": "Contents", "toc_cell": false, "toc_position": {}, "toc_section_display": true, "toc_window_display": true } }, "nbformat": 4, "nbformat_minor": 4 }