From 2070c37353881d03492c7c85aa7eb18ac624b929 Mon Sep 17 00:00:00 2001 From: quantummind Date: Wed, 23 Jun 2021 13:39:18 -0700 Subject: [PATCH] eq-gan tutorials (#597) --- eq_gan/noise_suppression.ipynb | 1308 ++++++++++++++++++++++++++++++++ eq_gan/variational_qram.ipynb | 1123 +++++++++++++++++++++++++++ 2 files changed, 2431 insertions(+) create mode 100644 eq_gan/noise_suppression.ipynb create mode 100644 eq_gan/variational_qram.ipynb diff --git a/eq_gan/noise_suppression.ipynb b/eq_gan/noise_suppression.ipynb new file mode 100644 index 000000000..e8ed77208 --- /dev/null +++ b/eq_gan/noise_suppression.ipynb @@ -0,0 +1,1308 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "name": "noise_suppression.ipynb", + "provenance": [], + "collapsed_sections": [] + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python" + } + }, + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "f0_gMI4Blbe8" + }, + "source": [ + "##### Copyright 2021 The TensorFlow Quantum Authors." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "bogCr-sSkXM1" + }, + "source": [ + "#@title Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ], + "execution_count": 1, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "j65ELcA9itDu" + }, + "source": [ + "# Learning to supress noise with an entangling quantum generative adversarial network (EQ-GAN)\n", + "\n", + "Author : Alexander Zlokapa\n", + "\n", + "Created : 2021-Jun-23\n", + "\n", + "Last updated : 2021-Jun-23" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HW0VUaXkR_8H" + }, + "source": [ + "In this tutorial, we describe the [EQ-GAN architecture](https://arxiv.org/abs/2105.00080) and demonstrate its ability to mitigate common errors on near-term quantum hardware. Given an unknown state, the quantum neural network aims to learn a variational circuit that reproduces the state in the presence of an unknown noise model. While a perfect swap test is shown to converge to the incorrect state, the EQ-GAN successfully generates a state with lower error." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HMKFQ_xYjc07" + }, + "source": [ + "## Setup\n", + "\n", + "Install TensorFlow and TensorFlow Quantum." + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "Nnjo2W473QKV", + "outputId": "d7f4926e-ed52-4fdb-da80-07e8aee02284" + }, + "source": [ + "!pip install -q tensorflow==2.4.1\n", + "!pip install -q tensorflow-quantum" + ], + "execution_count": null, + "outputs": [ + { + "output_type": "stream", + "text": [ + "\u001b[K |████████████████████████████████| 394.3MB 36kB/s \n", + "\u001b[K |████████████████████████████████| 471kB 39.3MB/s \n", + "\u001b[K |████████████████████████████████| 3.8MB 27.5MB/s \n", + "\u001b[K |████████████████████████████████| 2.9MB 26.1MB/s \n", + "\u001b[K |████████████████████████████████| 7.8MB 3.0MB/s \n", + "\u001b[K |████████████████████████████████| 1.3MB 41.2MB/s \n", + "\u001b[K |████████████████████████████████| 92kB 7.6MB/s \n", + "\u001b[K |████████████████████████████████| 5.6MB 18.9MB/s \n", + "\u001b[K |████████████████████████████████| 92kB 9.2MB/s \n", + "\u001b[K |████████████████████████████████| 102kB 11.5MB/s \n", + "\u001b[K |████████████████████████████████| 1.5MB 41.8MB/s \n", + "\u001b[K |████████████████████████████████| 389kB 39.2MB/s \n", + "\u001b[?25h" + ], + "name": "stdout" + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aMUbFUv6jiXz" + }, + "source": [ + "Import TensorFlow, TensorFlowQuantum, and Cirq to perform both machine learning and quantum computing." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "c5CydyvE2jCi" + }, + "source": [ + "import tensorflow as tf\n", + "import tensorflow_quantum as tfq\n", + "\n", + "import cirq\n", + "import sympy\n", + "import numpy as np\n", + "\n", + "# visualization tools\n", + "%matplotlib inline\n", + "import matplotlib.pyplot as plt\n", + "from cirq.contrib.svg import SVGCircuit" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "obAL1Alf4KYk" + }, + "source": [ + "## 1. Create the quantum circuits.\n", + "### 1.1 Define the quantum generator (and the quantum data)\n", + "\n", + "We prepare a parameterized quantum circuit $G(\\theta)$ that will serve as both our true data for particular parameters $\\theta = \\theta_0$ and as a generator for arbitrary $\\theta$. For this example, we'll use a circuit that prepares a superposition of $|0\\rangle$ and $|1\\rangle$ states with some rotation between them. It can also be extended to multiple qubits using $CZ$ entangling gates, since Google's quantum hardware uses $CZ$ as a native gate." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "_j_JDkSk4Jrj" + }, + "source": [ + "def generator_circuit(qubits, rotations):\n", + " \"\"\"Make a GHZ-like state with arbitrary phase using CZ gates.\n", + " For the purposes of the noise experiment, we don't apply Z phase\n", + " corrections, since the point is to match the generator and data\n", + " gate parameters to know that there's high state overlap.\n", + "\n", + " Args:\n", + " qubits: Python `lst` of `cirq.GridQubit`s\n", + " rotations: Python `lst` indicating the X half rotations, Y half\n", + " rotations and Z half rotations.\n", + " \"\"\"\n", + " if len(rotations) != 3:\n", + " raise ValueError(\"Number of needed rotations is 3.\")\n", + " \n", + " u = [cirq.Z(qubits[0])**rotations[0],\n", + " cirq.X(qubits[0])**rotations[1],\n", + " cirq.Z(qubits[0])**rotations[2]]\n", + " for q0, q1 in zip(qubits, qubits[1:]):\n", + " u.extend([cirq.Y(q1)**0.5, cirq.X(q1), cirq.CZ(q0, q1),\n", + " cirq.Y(q1)**0.5, cirq.X(q1)])\n", + " return cirq.Circuit(u)" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 107 + }, + "id": "jYM5TGrmpqvz", + "outputId": "f6187ed3-bf14-4257-d2f1-2d29262715c6" + }, + "source": [ + "print('One-qubit example generator:')\n", + "SVGCircuit(generator_circuit(cirq.GridQubit.rect(1, 1),\n", + " [sympy.Symbol('a'), sympy.Symbol('b'), sympy.Symbol('c')]))" + ], + "execution_count": null, + "outputs": [ + { + "output_type": "stream", + "text": [ + "findfont: Font family ['Arial'] not found. Falling back to DejaVu Sans.\n" + ], + "name": "stderr" + }, + { + "output_type": "stream", + "text": [ + "One-qubit example generator:\n" + ], + "name": "stdout" + }, + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "" + ], + "image/svg+xml": "(0, 0): Z^aX^bZ^c" + }, + "metadata": { + "tags": [] + }, + "execution_count": 4 + } + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 139 + }, + "id": "0af85yvE_Cfw", + "outputId": "9f571795-04ef-4952-ca21-699848512681" + }, + "source": [ + "print('Two-qubit example generator:')\n", + "SVGCircuit(generator_circuit(cirq.GridQubit.rect(1, 2),\n", + " [sympy.Symbol('a'), sympy.Symbol('b'), sympy.Symbol('c')]))" + ], + "execution_count": null, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Two-qubit example generator:\n" + ], + "name": "stdout" + }, + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "" + ], + "image/svg+xml": "(0, 0): (0, 1): Z^aY^0.5X^bXZ^cY^0.5X" + }, + "metadata": { + "tags": [] + }, + "execution_count": 5 + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "K4R-xl4YqD3S" + }, + "source": [ + "### 1.2 Define the discriminator circuit\n", + "\n", + "For the discriminator circuit, we'll decompose a swap test circuit to use $CZ$ two-qubit gates. Since the predominant source of error on a $CZ$ gate is single-qubit $Z$ phase, we'll include $RZ$ rotations directly after the $CZ$ gate. A successful discriminator would learn how to correct the phase error by applying $RZ$ with an appropriate angle. In the case of \"perfect\" swap test (which is exactly a swap test in the absence of noise), the $RZ$ angles are fixed at zero." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "rglKfMgwppGQ" + }, + "source": [ + "def discriminator_circuit(qubits_a, qubits_b, rotations):\n", + " \"\"\"Make a variational swap test circuit with CZ as the two-qubit gate.\n", + "\n", + " Args:\n", + " qubits_a: Python `lst` of `cirq.GridQubit`s indicating subsystem A's\n", + " qubits.\n", + " qubits_b: Python `lst` of `cirq.GridQubit`s indicating subsystem B's\n", + " qubits.\n", + " rotations: Python `lst` of shape [n_qubits, 2] containing Z rotation\n", + " parameters for the swap test.\n", + " \"\"\"\n", + " if len(rotations) != len(qubits_a) or any(len(x) != 2 for x in rotations):\n", + " raise ValueError(\"rotations must be shape [len(qubits_a), 2]\")\n", + "\n", + " if len(qubits_a) != len(qubits_b):\n", + " raise ValueError(\"unequal system sizes.\")\n", + " \n", + " u = []\n", + " for i in range(len(qubits_a)):\n", + " q0 = qubits_a[i]\n", + " q1 = qubits_b[i]\n", + " u.extend([cirq.Y(q1)**0.5, cirq.X(q1), cirq.CZ(q0, q1), cirq.Z(q0)**rotations[i][0], cirq.Z(q1)**rotations[i][1], cirq.Y(q1)**0.5, cirq.X(q1)])\n", + " \n", + " # expanded Hadamard: H = X Y^(1/2)\n", + " for i, q in enumerate(qubits_a):\n", + " u.append(cirq.Y(q)**0.5)\n", + " u.append(cirq.X(q)**1.0)\n", + "\n", + " return cirq.Circuit(u)" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DYMxwNqtrS7n" + }, + "source": [ + "Here's how the discriminator circuit looks when comparing two registers with one-qubit data." + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 120 + }, + "id": "7CW7JBsNq0eG", + "outputId": "62f38b65-7abf-49d8-e091-ca84b66b169d" + }, + "source": [ + "SVGCircuit(discriminator_circuit([cirq.GridQubit(0, 0)], [cirq.GridQubit(0, 1)],\n", + " [[sympy.Symbol('a'), sympy.Symbol('b')]]))" + ], + "execution_count": null, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "" + ], + "image/svg+xml": "(0, 0): (0, 1): Y^0.5XZ^aZ^bY^0.5Y^0.5XX" + }, + "metadata": { + "tags": [] + }, + "execution_count": 7 + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7mwetFRm0fhq" + }, + "source": [ + "To perform the ancilla-free swap test, we need some classical post-processing. This readout operation is described in the destructive swap test construction of the appendix." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "4eZcPjNI0ge-" + }, + "source": [ + "def swap_readout_op(qubits_a, qubits_b):\n", + " \"\"\"Readout operation for variational swap test.\n", + "\n", + " Computes the bitwise and of matched qubits from qubits_a and qubits_b.\n", + "\n", + " When the states have perfect overlap the expectation of this op will be -1\n", + " when these states are orthogonal the expectation of this op will be 1.\n", + "\n", + " Args:\n", + " qubits_a: Python `lst` of `cirq.GridQubit`s. The qubits system A act on\n", + " qubits_b: Python `lst` of `cirq.GridQubit`s. The qubits system B act on\n", + " \"\"\"\n", + "\n", + " def _countSetBits(n):\n", + " count = 0\n", + " while n:\n", + " count += n & 1\n", + " n >>= 1\n", + " return count\n", + "\n", + " def _one_proj(a):\n", + " return 0.5 * (1 - cirq.Z(a))\n", + "\n", + " if len(qubits_a) != len(qubits_b):\n", + " raise ValueError(\"unequal system sizes.\")\n", + "\n", + " ret_op = 0\n", + " for i in range(1 << len(qubits_a)):\n", + " if _countSetBits(i) % 2 == 0:\n", + " tmp_op = 1\n", + " for j, ch in enumerate(bin(i)[2:].zfill(len(qubits_a))):\n", + " intermediate = _one_proj(qubits_a[j]) * _one_proj(qubits_b[j])\n", + " if ch == '0':\n", + " intermediate = 1 - intermediate\n", + " tmp_op *= intermediate\n", + " ret_op += tmp_op\n", + "\n", + " return 1.0 - (ret_op * 2 - 1)" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f877slvl4nZl" + }, + "source": [ + "### 1.3 Create the noise model\n", + "\n", + "We use Cirq to implement the noise model described above: each $CZ$ gate is followed by single-qubit $Z$ rotations that insert a phase error according to a Gaussian distribution. We'll also extend the noise model to include two-qubit phase errors, although our experiment will only include a single-qubit phase error." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "RvpMZMeX4jln" + }, + "source": [ + "# add controlled phase and Z phase errors after each CZ gate\n", + "# CZ phase error is fully random\n", + "# Z phase error is always the same for a given qubit index\n", + "class CZNoiseModel(cirq.NoiseModel):\n", + " def __init__(self, qubits, mean, stdev, seed=0):\n", + " self.mean = mean\n", + " self.stdev = stdev\n", + " \n", + " np.random.seed(seed)\n", + " single_errors = {}\n", + " for q in qubits:\n", + " single_errors[q] = np.random.normal(self.mean[1], self.stdev[1])\n", + " self.single_errors = single_errors\n", + " \n", + " def noisy_operation(self, op):\n", + " if isinstance(op.gate, cirq.ops.CZPowGate):\n", + " return [op, cirq.ops.CZPowGate(exponent=np.random.normal(self.mean[0], self.stdev[0]))(*op.qubits), cirq.ops.ZPowGate(exponent=self.single_errors[op.qubits[0]])(op.qubits[0]), cirq.ops.ZPowGate(exponent=self.single_errors[op.qubits[1]])(op.qubits[1])]\n", + " \n", + " return op" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Qs-4ylTJsHeo" + }, + "source": [ + "## 2. Create the quantum neural networks\n", + "\n", + "We can now prepare TensorFlow Quantum objects from the Cirq circuits defined above. These will include the adversarial loss function, completing the definition of the EQ-GAN." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3PtTNiToz4gF" + }, + "source": [ + "### 2.1 Utility functions\n", + "We prepare a series of functions to accesss the quantum data, generator, discriminator, and the number of parameters associated with each of those." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "SKHP9xRu3IbP" + }, + "source": [ + "def get_data_maker():\n", + " \"\"\"Get appropriate dataset maker for a given circuit type.\"\"\"\n", + " return generator_circuit\n", + "\n", + "def get_circuit_maker():\n", + " \"\"\"Get appropriate circuit maker for a given circuit type.\"\"\"\n", + " return generator_circuit\n", + "\n", + "def num_data_parameters(n_qubits):\n", + " \"\"\"Get number of true data circuit parameters for a circuit type.\"\"\"\n", + " return num_gen_parameters(n_qubits)\n", + "\n", + "def num_gen_parameters(n_qubits):\n", + " \"\"\"Get number of generator model parameters for a circuit type.\"\"\"\n", + " return 3\n", + "\n", + "def num_disc_parameters(n_qubits):\n", + " \"\"\"Get number of discriminator model parameters for a circuit type.\"\"\"\n", + " return 2*n_qubits\n", + "\n", + "def get_rand_state(n_qubits, data_noise):\n", + " \"\"\"Get number of data preparation circuit parameters for a circuit type.\"\"\"\n", + " return np.random.uniform(-data_noise, data_noise, \n", + " num_data_parameters(n_qubits))" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pHphmhG35gbB" + }, + "source": [ + "When loading data in the quantum computer, it may be beneficial to augment the training set by adding noise (similarly to data augmentation in classical machine learning). Hence, our `generate_data` function takes a `data_noise` parameter that can create noisy copies of the original dataset. In our experiment below, however, we will set this to zero for simplicity." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "9KWQdxSm4p-1" + }, + "source": [ + "def generate_data(data_qubits, generator_qubits, target_quantum_data,\n", + " data_noise, noise_model, n_points):\n", + " \"\"\"Generate n_points data on data_qubits with generator_qubits linked for\n", + " later copying.\"\"\"\n", + " data_maker = get_data_maker()\n", + "\n", + " target_circuits = []\n", + " target_real_data_circuit = []\n", + "\n", + " rand_states = []\n", + " for i in range(n_points):\n", + " rand_states.append(get_rand_state(len(data_qubits), data_noise))\n", + " for rand_state in rand_states:\n", + " rand_circuit = data_maker(data_qubits, rand_state + target_quantum_data)\n", + " rand_circuit_true_data_on_generator_qubit = data_maker(\n", + " generator_qubits, rand_state + target_quantum_data)\n", + " \n", + " c_data = rand_circuit.with_noise(noise_model)\n", + " c_gen = rand_circuit_true_data_on_generator_qubit.with_noise(noise_model)\n", + "\n", + " target_circuits.append(c_data)\n", + " target_real_data_circuit.append(c_gen)\n", + " target_circuits = tfq.convert_to_tensor(target_circuits)\n", + " target_real_data_circuit = tfq.convert_to_tensor(target_real_data_circuit)\n", + "\n", + " return target_circuits, target_real_data_circuit" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bRQryMgM1vnY" + }, + "source": [ + "Finally, we require a custom `keras` layer to share variables between the discriminator and generator, since they represent two different quantum circuits." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "7VqO7T3m7Dg9" + }, + "source": [ + "class SharedVar(tf.keras.layers.Layer):\n", + " \"\"\"A custom tf.keras.layers.Layer used for sharing variables.\"\"\"\n", + " def __init__(self, symbol_names, operators, init_vals, backend,\n", + " use_sampled):\n", + " \"\"\"Custom keras layer used to share tf.Variables between several\n", + " tfq.layers.Expectation.\"\"\"\n", + " super(SharedVar, self).__init__()\n", + " self.init_vals = init_vals\n", + " self.symbol_names = symbol_names\n", + " self.operators = operators\n", + " self.use_sampled = use_sampled\n", + " self.backend = backend\n", + "\n", + " def build(self, input_shape):\n", + " # Build a tf.Variable that is the shape of the number of symbols.\n", + " self.w = self.add_weight(shape=(len(self.symbol_names),),\n", + " initializer=tf.constant_initializer(\n", + " self.init_vals))\n", + "\n", + " def call(self, inputs):\n", + " # inputs[0] = circuit tensor\n", + " # inputs[1] = circuit tensor\n", + " # Their expectations are evaluated with shared variables between them\n", + " n_datapoints = tf.gather(tf.shape(inputs[0]), 0)\n", + " values = tf.tile(tf.expand_dims(self.w, 0), [n_datapoints, 1])\n", + " if not self.use_sampled:\n", + " return tfq.layers.Expectation(backend=self.backend)(\n", + " inputs[0],\n", + " symbol_names=self.symbol_names,\n", + " operators=self.operators,\n", + " symbol_values=values), tfq.layers.Expectation(\n", + " backend=self.backend)(inputs[1],\n", + " symbol_names=self.symbol_names,\n", + " operators=self.operators,\n", + " symbol_values=values)\n", + " else:\n", + " return tfq.layers.SampledExpectation(backend=self.backend)(\n", + " inputs[0],\n", + " symbol_names=self.symbol_names,\n", + " operators=self.operators,\n", + " symbol_values=values,\n", + " repetitions=10000), tfq.layers.SampledExpectation(\n", + " backend=self.backend)(inputs[1],\n", + " symbol_names=self.symbol_names,\n", + " operators=self.operators,\n", + " symbol_values=values,\n", + " repetitions=10000)" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aVFXREU914mG" + }, + "source": [ + "### 2.2 Build the generator\n", + "\n", + "The generator model includes both the quantum data and the quantum generator circuit, so that the data and model can later be *entangled* together by the discriminator. To compute the generator loss, we append the generator circuit to the quantum data and then apply the entangling variational swap test discriminator. The expectation of the output can either be computed exactly (with `tfq.layers.Expectation`) or sampled (with `tfq.layers.SampledExpectation`) before being provided to the loss function\n", + "\n", + "$$\n", + "\\min_{\\theta_g} \\max_{\\theta_d} [1 - D(\\theta_d, G(\\theta_g))],\n", + "$$\n", + "\n", + "which is optimized with `keras`. By default, we choose Adam to optimize the generator circuit parameters." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "2bi6E8pv5zjk" + }, + "source": [ + "def build_generator(generator_qubits,\n", + " data_qubits,\n", + " generator_symbols,\n", + " lr,\n", + " generator_initialization,\n", + " noise_model,\n", + " backend=None,\n", + " use_sampled=False,\n", + " regularization=0.000001,\n", + " optimizer=None):\n", + " \"\"\"Build a generator tf.keras.Model using standard circuits.\n", + "\n", + " Args:\n", + " generator_qubits: Python `lst` of `cirq.GridQubit`s indicating the\n", + " qubits that the generator should use.\n", + " data_qubits: Python `lst` of `cirq.GridQubit`s indicating the qubits\n", + " that the data will arrive on.\n", + " generator_symbols: Python `lst` of numbers or `sympy.Symbol`s\n", + " to use in the ansatze used for the generator.\n", + " lr: Python `float` the learning rate of the model.\n", + " backend: Python object for the backend type to use when running quantum\n", + " circuits.\n", + " generator_initialization: `np.ndarray` of initial values to place\n", + " inside of the generator symbols in the tensorflow managed\n", + " variables.\n", + " noise_model: `cirq.NoiseModel` to apply to circuits.\n", + " use_sampled: Python `bool` indicating whether or not to use analytical\n", + " expectation or sample based expectation calculation.\n", + " regularization: Python `float` added as margin to an orthogonal swap test.\n", + " optimizer: `tf.keras.optimizers` optimizer for training the circuit. Default\n", + " is tf.keras.optimizers.Adam.\n", + " \"\"\"\n", + " if optimizer is None:\n", + " optimizer = tf.keras.optimizers.Adam\n", + " \n", + " # Input for the circuits that generate the quantum data from the source.\n", + " signal_input = tf.keras.layers.Input(shape=(), dtype=tf.dtypes.string)\n", + "\n", + " # Input for the swaptest circuits. These will have the variables from the\n", + " # discriminator resolved into them.\n", + " swap_test_input = tf.keras.layers.Input(shape=(), dtype=tf.dtypes.string)\n", + "\n", + " data_and_generated = tfq.layers.AddCircuit()(signal_input,\n", + " append=generator_circuit(\n", + " generator_qubits,\n", + " generator_symbols).\n", + " with_noise(noise_model))\n", + "\n", + " # Append the variational swap test on to the data on data_qubits\n", + " # and the \"generated\" data on generator_qubits.\n", + " full_swaptest = tfq.layers.AddCircuit()(data_and_generated,\n", + " append=swap_test_input)\n", + "\n", + " expectation_output = None\n", + " if not use_sampled:\n", + " expectation_output = tfq.layers.Expectation(backend=backend)(\n", + " full_swaptest,\n", + " symbol_names=generator_symbols,\n", + " operators=swap_readout_op(generator_qubits, data_qubits),\n", + " initializer=tf.constant_initializer(generator_initialization))\n", + "\n", + " else:\n", + " expectation_output = tfq.layers.SampledExpectation(backend=backend)(\n", + " full_swaptest,\n", + " symbol_names=generator_symbols,\n", + " operators=swap_readout_op(generator_qubits, data_qubits),\n", + " initializer=tf.constant_initializer(generator_initialization),\n", + " repetitions=10000)\n", + "\n", + " expectation_output = tf.add(expectation_output, tf.constant(regularization))\n", + " log_output = tf.math.log(expectation_output)\n", + "\n", + " # Input is true data on data qubits, and swap_test_input for both qubits.\n", + " qgan_g_model = tf.keras.Model(inputs=[signal_input, swap_test_input],\n", + " outputs=[expectation_output, log_output])\n", + "\n", + " optimizerg = optimizer(learning_rate=lr)\n", + " lossg = lambda x, y: tf.reduce_mean(y)\n", + " qgan_g_model.compile(optimizer=optimizerg, loss=lossg, loss_weights=[0,1])\n", + "\n", + " return qgan_g_model" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "eMTArL8R3fwP" + }, + "source": [ + "### 2.3 Build the discriminator\n", + "\n", + "The discriminator is similar to the generator, except the loss function is multiplied by a minus sign to perform adversarial learning. To propagate the loss through the generator, we use the `SharedVar` defined above." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "IWPwgEqQ6PmU" + }, + "source": [ + "def build_discriminator(generator_qubits,\n", + " data_qubits,\n", + " discriminator_symbols,\n", + " lr,\n", + " discriminator_initialization,\n", + " noise_model,\n", + " backend=None,\n", + " use_sampled=False,\n", + " regularization=0.000001,\n", + " optimizer=None):\n", + " \"\"\"Build a discriminator model.\n", + "\n", + " Args:\n", + " generator_qubits: Python `lst` of `cirq.GridQubit`s indicating the\n", + " qubits that the generator should use.\n", + " data_qubits: Python `lst` of `cirq.GridQubit`s indicating the qubits\n", + " that the data will arrive on.\n", + " discriminator_symbols: Python `lst` of numbers or `sympy.Symbol`s\n", + " to use in the ansatze used for the discriminator.\n", + " lr: Python `float` the learning rate of the model.\n", + " discriminator_initialization: `np.ndarray` of symbols to place\n", + " inside of the discriminator symbols in the tensorflow managed\n", + " variables.\n", + " backend: Python object for the backend type to use when running quantum\n", + " circuits.\n", + " use_sampled: Python `bool` indicating whether or not to use analytical\n", + " expectation or sample based expectation calculation.\n", + " regularization: Python `float` added as margin to an orthogonal swap test.\n", + " optimizer: `tf.keras.optimizers` optimizer for training the circuit. Default\n", + " is tf.keras.optimizers.Adam.\n", + " \"\"\"\n", + " if optimizer is None:\n", + " optimizer = tf.keras.optimizers.Adam\n", + "\n", + " # True data on data_qubits.\n", + " signal_input_d = tf.keras.layers.Input(shape=(), dtype=tf.dtypes.string)\n", + "\n", + " # Generator data on generator_qubits.\n", + " load_generator_data_d = tf.keras.layers.Input(shape=(),\n", + " dtype=tf.dtypes.string)\n", + "\n", + " # True data on generator_qubits.\n", + " load_true_data_d = tf.keras.layers.Input(shape=(), dtype=tf.dtypes.string)\n", + "\n", + " # Swap circuit with input.\n", + " swap_test_input_d = tfq.layers.AddCircuit()(\n", + " signal_input_d,\n", + " append=discriminator_circuit(data_qubits, generator_qubits,\n", + " np.array(discriminator_symbols).reshape(-1, 2)).\n", + " with_noise(noise_model))\n", + " \n", + "\n", + " # Swap test between the true data and generator.\n", + " swaptest_d = tfq.layers.AddCircuit()(load_generator_data_d,\n", + " append=swap_test_input_d)\n", + "\n", + " # Swap test between the true data and itself. Useful for how close to the\n", + " # \"true\" swap test we are over time as we train.\n", + " swapontruedata = tfq.layers.AddCircuit()(load_true_data_d,\n", + " append=swap_test_input_d)\n", + "\n", + " tmp = SharedVar(discriminator_symbols,\n", + " swap_readout_op(generator_qubits, data_qubits),\n", + " discriminator_initialization, backend, use_sampled)\n", + " expectation_output_d, expectation_output2 = tmp(\n", + " [swaptest_d, swapontruedata])\n", + "\n", + " expectation_output_d = tf.add(expectation_output_d, tf.constant(regularization))\n", + " log_discrim_dist = tf.math.log(tf.keras.backend.flatten(expectation_output_d))\n", + " log_true_dist = tf.math.log(tf.keras.backend.flatten(expectation_output2))\n", + "\n", + "\n", + " final_output = -log_discrim_dist\n", + "\n", + " qgan_d_model = tf.keras.Model(\n", + " inputs=[signal_input_d, load_generator_data_d, load_true_data_d],\n", + " outputs=[expectation_output_d, expectation_output2, final_output])\n", + "\n", + " optimizerd = optimizer(learning_rate=lr)\n", + "\n", + " # Difference between \"generator vs true data\" and \"true vs true (given\n", + " # we many not be doing a perfect swap test yet)\"\n", + " lossd = lambda x, y: -tf.reduce_mean(y)\n", + " qgan_d_model.compile(optimizer=optimizerd, loss=lossd, loss_weights=[0,0,1])\n", + "\n", + " return qgan_d_model" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "98JaTG4v39Go" + }, + "source": [ + "## 3 Benchmark the EQ-GAN\n", + "\n", + "We'll train the EQ-GAN and compare it to a \"perfect\" swap test, which has the same generator but a frozen discriminator. The perfect swap test is *imperfect* in the presence of noise, since it assumes fully calibrated quantum hardware. Since noise on hardware oscillates on the order of 10 minutes, it becomes difficult to perfectly calibrate $CZ$ gates everywhere; consequently, the incorrectly calibrated perfect swap test will likely converge to an incorrect state. An approach robust to incorrect noise models is helpful to learn variational circuits. Since the EQ-GAN has a Nash equilibrium at the location of a calibrated swap test, we expect it to properly converge.\n", + "\n", + "### 3.1 Training the EQ-GAN and perfect swap test models\n", + "\n", + "To track performance, we need to compare the generated state with the true data state. This exact state fidelity metric is computed in the absence of noise." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "xUKhWTiZ1R2X" + }, + "source": [ + "def quantum_data_overlap(qubits, params_a, params_b):\n", + " \"\"\"Compute overlap of quantum data circuits with params_a and params_b.\"\"\"\n", + " sim = cirq.Simulator()\n", + " circuit_maker = get_circuit_maker()\n", + " data_maker = get_data_maker()\n", + " circuit_a = circuit_maker(qubits, params_a)\n", + " circuit_b = data_maker(qubits, params_b)\n", + " res_a = sim.simulate(circuit_a)\n", + " res_b = sim.simulate(circuit_b)\n", + " overlap = np.abs(np.vdot(res_a.final_state_vector, res_b.final_state_vector))\n", + " return overlap" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JjF_JcTB5YFh" + }, + "source": [ + "Running an experiment consists of the following steps:\n", + "* *Initialize the generator quantum circuit.* We set all rotation angles to zero in the beginning.\n", + "* *Initialize the discriminator quantum circuit.* The perfect swap test has a discriminator frozen with the expected gate parameters (which does not correspond to a perfect swap test due to noise). The adversarial swap test has a discriminator also initialized to the circuit expected to be a true swap test; although the assumption is false due to noise, it provides a good guess to start training.\n", + "* *Train model(s).* To enhance training stability, adversarial training will be performed in two phases. In the first half, the discriminator is frozen to be equivalent to perfect swap test training. In the second half, the discriminator is trained adversarially, allowing the EQ-GAN to converge to a true swap test and correct the noise. Training the perfect swap test is equivalent to performing the first phase (frozen discriminator) for the duration of both adversarial phases.\n", + "* *Record training history.* We output the loss functions and state overlap with the true data, as well as the parameter history of the generator and discriminator.\n", + "\n", + "This is implemented in `run_experiment` below." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "meDv569f4t3m" + }, + "source": [ + "def run_experiment(d_learn, g_learn, d_epoch, g_epoch, batchsize, n_episodes,\n", + " n_qubits, target_quantum_data, use_perfect_swap,\n", + " gate_error_mean, gate_error_stdev, n_data=1, data_noise=0,\n", + " use_sampled=False, log_interval=10, backend=None, seed=0):\n", + " \"\"\"Run a QGAN experiment.\n", + "\n", + " Args:\n", + " d_learn: Python `float` discriminator learning rate.\n", + " g_learn: Python `float` generator learning rate.\n", + " d_epoch: Python `int` number of discriminator iterations per episode.\n", + " g_epoch: Python `int` number of generator iterations per episode.\n", + " batchsize: Python `int` number of entries to use in a batch.\n", + " n_episodes: Python `int` number of total QGAN training episodes.\n", + " n_qubits: Python `int` number of qubits to use for each susbsystem.\n", + " target_quantum_data: Python object. True target state.\n", + " use_perfect_swap: `bool` whether or not to train discriminator.\n", + " gate_error_mean: mean angle error on 2-qubit gates (`None` if no noise).\n", + " gate_error_stdev: standard deviation of angle error on 2-qubit gates.\n", + " n_data: Python `int` number of total datapoints to generate.\n", + " data_noise: Python `float` bounds on noise in real data preparation.\n", + " use_sampled: Python `bool` whether or not analytical or sampled exp.\n", + " backend: None or `cirq.SimulatesFinalState` or `cirq.Sampler`.\n", + " log_interval: Python `int` log every log_interval episodes.\n", + " seed: seed of run for noise model and training.\n", + " \"\"\"\n", + "\n", + " circuit_maker = get_circuit_maker()\n", + " generator_initialization = np.zeros(num_gen_parameters(n_qubits))\n", + " discriminator_initialization = np.array([[0.0, 0.0]] * n_qubits)\n", + " \n", + " # Create data and generator qubits\n", + " data_qubits = [cirq.GridQubit(1, k + 4) for k in range(n_qubits)]\n", + " generator_qubits = [cirq.GridQubit(2, k + 4) for k in range(n_qubits)]\n", + " ancilla = cirq.GridQubit(1, 5) # potentially unused.\n", + " all_qubits = data_qubits + generator_qubits\n", + " \n", + " # Noise on single-qubit gates\n", + " if (gate_error_mean is None) or (gate_error_stdev is None):\n", + " noise_model = None\n", + " else:\n", + " noise_model = CZNoiseModel(all_qubits, gate_error_mean, gate_error_stdev, seed=seed)\n", + "\n", + " # Generator and Discriminator symbols\n", + " discriminator_parameters = []\n", + " generator_parameters = []\n", + " for j in range(num_disc_parameters(n_qubits)):\n", + " discriminator_parameters.append(sympy.Symbol('Discrimx{!r}'.format(j)))\n", + " for j in range(num_gen_parameters(n_qubits)):\n", + " generator_parameters.append(sympy.Symbol('Genx{!r}'.format(j)))\n", + " target_circuits, target_real_data_circuit = generate_data(data_qubits,\n", + " generator_qubits, target_quantum_data, data_noise, noise_model, n_data)\n", + "\n", + " # Generator and Discriminator models\n", + " qgan_d_model = build_discriminator(\n", + " generator_qubits, data_qubits, discriminator_parameters, d_learn,\n", + " discriminator_initialization, noise_model, backend, use_sampled)\n", + " qgan_g_model = build_generator(\n", + " generator_qubits, data_qubits, generator_parameters, g_learn,\n", + " generator_initialization, noise_model, backend, use_sampled)\n", + "\n", + " # Tracking info\n", + " d_loss = []\n", + " g_loss = []\n", + " overlap_record = []\n", + " param_history = []\n", + " \n", + " repeats = 1\n", + " if not use_perfect_swap: # introduce adversarial second phase\n", + " repeats = 2\n", + " n_episodes = n_episodes // 2\n", + "\n", + " for r in range(repeats):\n", + " if r == 0: # use perfect swap for first half\n", + " use_perfect_swap = True\n", + " elif r == 1: # use adversarial learning for second half\n", + " use_perfect_swap = False\n", + " # begin training\n", + " for k in range(1, n_episodes + 1):\n", + " if k != 0:\n", + " generator_initialization = qgan_g_model.trainable_variables[\n", + " 0].numpy()\n", + "\n", + " overlap_record.append(\n", + " quantum_data_overlap(data_qubits, generator_initialization,\n", + " target_quantum_data))\n", + " param_history.append([qgan_g_model.trainable_variables[0].numpy(), \n", + " qgan_d_model.trainable_variables[0].numpy()])\n", + "\n", + " if not use_perfect_swap:\n", + " # prepare discriminator network input\n", + " gen_circuit = circuit_maker(generator_qubits, generator_initialization)\n", + " gen_circuit = gen_circuit.with_noise(noise_model)\n", + " load_generator_circuit = tf.tile(\n", + " tfq.convert_to_tensor(\n", + " [gen_circuit]),\n", + " tf.constant([n_data]))\n", + "\n", + " historyd = qgan_d_model.fit(x=[\n", + " target_circuits, load_generator_circuit, target_real_data_circuit\n", + " ],\n", + " y=[\n", + " tf.zeros_like(target_circuits,\n", + " dtype=tf.float32),\n", + " tf.zeros_like(target_circuits,\n", + " dtype=tf.float32),\n", + " tf.zeros_like(target_circuits,\n", + " dtype=tf.float32)\n", + " ],\n", + " epochs=d_epoch,\n", + " batch_size=batchsize,\n", + " verbose=0)\n", + "\n", + " d_loss.append(historyd.history['loss'])\n", + "\n", + " # prepare generator network input\n", + " discriminator_initialization = qgan_d_model.trainable_variables[\n", + " 0].numpy().reshape((-1, 2))\n", + "\n", + " # evaluate noisy swap test\n", + " swap_test_circuit = discriminator_circuit(\n", + " data_qubits, generator_qubits, discriminator_initialization)\n", + "\n", + " swap_test_circuit = swap_test_circuit.with_noise(noise_model)\n", + " swap_test_circuit = tf.tile(tfq.convert_to_tensor([swap_test_circuit]),\n", + " tf.constant([n_data]))\n", + "\n", + " # record history\n", + " history = qgan_g_model.fit(x=[target_circuits, swap_test_circuit],\n", + " y=[tf.zeros_like(target_circuits,\n", + " dtype=tf.float32),tf.zeros_like(target_circuits,\n", + " dtype=tf.float32)],\n", + " epochs=g_epoch,\n", + " batch_size=batchsize,\n", + " verbose=0)\n", + "\n", + " g_loss.append(history.history['loss'])\n", + "\n", + " if k % log_interval == 0:\n", + " print(f'Step = {k}. Overlap={overlap_record[-1]}')\n", + " print(f'Step = {k}. g_loss={g_loss[-1]}')\n", + " if not use_perfect_swap:\n", + " print(f'Step = {k}. d_loss={d_loss[-1]}')\n", + " print(f'Step = {k}. gen_params={qgan_g_model.trainable_variables[0].numpy()}')\n", + " print(f'Step = {k}. discrim_params={qgan_d_model.trainable_variables[0].numpy()}')\n", + " \n", + " print('-'*50)\n", + "\n", + " return np.array(g_loss), np.array(d_loss), np.array(overlap_record), np.array(param_history)" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "T-5mlPlp7RyM" + }, + "source": [ + "Finally, we evaluate the performance of the EQ-GAN and perfect swap test models in learning the quantum state $\\frac{1}{\\sqrt{2}(|0\\rangle + |1\\rangle)$. Realistic single-qubit phase noise is estimated from Fig. S2 of Google's [paper](https://arxiv.org/abs/2010.07965) on Fermi-Hubbard dynamics, and we set the two-qubit controlled phase error to zero. Experimentally, the two-qubit controlled phase error is much smaller than the single-qubit phase error, so this assumption is realistic." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "RPYafTby7dhr" + }, + "source": [ + "d_epoch = 1\n", + "g_epoch = 1\n", + "batchsize = 4\n", + "\n", + "target_quantum_data = [0.0, 0.5, 0.5]\n", + "\n", + "n_qubits = 1\n", + "d_learn = 0.01\n", + "g_learn = 0.01\n", + "n_episodes = 80\n", + "\n", + "# format (radians): [controlled phase error, single-qubit Z phase error]\n", + "gate_error_mean = [0.0, 0.06]\n", + "gate_error_stdev = [0.005, 0.02]" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "USNqzIXl9dk4", + "outputId": "952b2c2b-fefc-4515-d60b-9094eae5083f" + }, + "source": [ + "# we run with a \"perfect\" swap test that is imperfect due to noise\n", + "use_perfect_swap = True\n", + "print('TRAINING PERFECT SWAP TEST')\n", + "g_loss_perf, d_loss_perf, overlap_perf, params_perf = run_experiment(\n", + " d_learn, g_learn, d_epoch, g_epoch, batchsize,\n", + " n_episodes, n_qubits, target_quantum_data,\n", + " use_perfect_swap, gate_error_mean, gate_error_stdev)\n", + "print()\n", + "\n", + "# we run with adversarial training to see noise get suppressed\n", + "use_perfect_swap = False\n", + "print('TRAINING ADVERSARIAL SWAP TEST')\n", + "g_loss_adv, d_loss_adv, overlap_adv, params_adv = run_experiment(\n", + " d_learn, g_learn, d_epoch, g_epoch, batchsize,\n", + " n_episodes, n_qubits, target_quantum_data,\n", + " use_perfect_swap, gate_error_mean, gate_error_stdev)" + ], + "execution_count": null, + "outputs": [ + { + "output_type": "stream", + "text": [ + "TRAINING PERFECT SWAP TEST\n", + "Step = 10. Overlap=0.7287916541099548\n", + "Step = 10. g_loss=[-0.7754115462303162]\n", + "Step = 10. gen_params=[-0.00283764 -0.10153843 -0.08077508]\n", + "Step = 10. discrim_params=[0. 0.]\n", + "--------------------------------------------------\n", + "Step = 20. Overlap=0.8095174431800842\n", + "Step = 20. g_loss=[-1.1870241165161133]\n", + "Step = 20. gen_params=[-0.00320345 -0.2109291 -0.18916626]\n", + "Step = 20. discrim_params=[0. 0.]\n", + "--------------------------------------------------\n", + "Step = 30. Overlap=0.9168037176132202\n", + "Step = 30. g_loss=[-2.1242780685424805]\n", + "Step = 30. gen_params=[ 0.01510484 -0.33290875 -0.31152907]\n", + "Step = 30. discrim_params=[0. 0.]\n", + "--------------------------------------------------\n", + "Step = 40. Overlap=0.9908996820449829\n", + "Step = 40. g_loss=[-4.5349602699279785]\n", + "Step = 40. gen_params=[ 0.04663827 -0.47101778 -0.430013 ]\n", + "Step = 40. discrim_params=[0. 0.]\n", + "--------------------------------------------------\n", + "Step = 50. Overlap=0.9881062507629395\n", + "Step = 50. g_loss=[-10.211024284362793]\n", + "Step = 50. gen_params=[ 0.0176267 -0.51823 -0.41223136]\n", + "Step = 50. discrim_params=[0. 0.]\n", + "--------------------------------------------------\n", + "Step = 60. Overlap=0.9888364672660828\n", + "Step = 60. g_loss=[-13.145798683166504]\n", + "Step = 60. gen_params=[ 0.03992724 -0.512491 -0.4081674 ]\n", + "Step = 60. discrim_params=[0. 0.]\n", + "--------------------------------------------------\n", + "Step = 70. Overlap=0.9899049401283264\n", + "Step = 70. g_loss=[-6.551358699798584]\n", + "Step = 70. gen_params=[ 0.10705256 -0.4968453 -0.40928578]\n", + "Step = 70. discrim_params=[0. 0.]\n", + "--------------------------------------------------\n", + "Step = 80. Overlap=0.98850417137146\n", + "Step = 80. g_loss=[-9.988781929016113]\n", + "Step = 80. gen_params=[ 0.14890672 -0.5256788 -0.40720248]\n", + "Step = 80. discrim_params=[0. 0.]\n", + "--------------------------------------------------\n", + "\n", + "TRAINING ADVERSARIAL SWAP TEST\n" + ], + "name": "stdout" + }, + { + "output_type": "stream", + "text": [ + "/usr/local/lib/python3.7/dist-packages/ipykernel_launcher.py:148: VisibleDeprecationWarning: Creating an ndarray from ragged nested sequences (which is a list-or-tuple of lists-or-tuples-or ndarrays with different lengths or shapes) is deprecated. If you meant to do this, you must specify 'dtype=object' when creating the ndarray\n" + ], + "name": "stderr" + }, + { + "output_type": "stream", + "text": [ + "Step = 10. Overlap=0.7287916541099548\n", + "Step = 10. g_loss=[-0.7754115462303162]\n", + "Step = 10. gen_params=[-0.00283764 -0.10153843 -0.08077508]\n", + "Step = 10. discrim_params=[0. 0.]\n", + "--------------------------------------------------\n", + "Step = 20. Overlap=0.8095174431800842\n", + "Step = 20. g_loss=[-1.1870241165161133]\n", + "Step = 20. gen_params=[-0.00320345 -0.2109291 -0.18916626]\n", + "Step = 20. discrim_params=[0. 0.]\n", + "--------------------------------------------------\n", + "Step = 30. Overlap=0.9168037176132202\n", + "Step = 30. g_loss=[-2.1242780685424805]\n", + "Step = 30. gen_params=[ 0.01510484 -0.33290875 -0.31152907]\n", + "Step = 30. discrim_params=[0. 0.]\n", + "--------------------------------------------------\n", + "Step = 40. Overlap=0.9908996820449829\n", + "Step = 40. g_loss=[-4.5349602699279785]\n", + "Step = 40. gen_params=[ 0.04663827 -0.47101778 -0.430013 ]\n", + "Step = 40. discrim_params=[0. 0.]\n", + "--------------------------------------------------\n", + "Step = 10. Overlap=0.9954313635826111\n", + "Step = 10. g_loss=[-10.230671882629395]\n", + "Step = 10. d_loss=[-9.696494102478027]\n", + "Step = 10. gen_params=[ 0.06719901 -0.5061495 -0.4461166 ]\n", + "Step = 10. discrim_params=[-0.03542047 -0.0467722 ]\n", + "--------------------------------------------------\n", + "Step = 20. Overlap=0.9998957514762878\n", + "Step = 20. g_loss=[-8.789029121398926]\n", + "Step = 20. d_loss=[-11.646615982055664]\n", + "Step = 20. gen_params=[ 0.09604842 -0.501386 -0.5149285 ]\n", + "Step = 20. discrim_params=[-0.11223976 -0.07033748]\n", + "--------------------------------------------------\n", + "Step = 30. Overlap=0.9956529140472412\n", + "Step = 30. g_loss=[-9.234436988830566]\n", + "Step = 30. d_loss=[-10.168179512023926]\n", + "Step = 30. gen_params=[ 0.14366579 -0.51015866 -0.5637149 ]\n", + "Step = 30. discrim_params=[-0.16043268 -0.10197245]\n", + "--------------------------------------------------\n", + "Step = 40. Overlap=0.981902003288269\n", + "Step = 40. g_loss=[-10.121150016784668]\n", + "Step = 40. d_loss=[-8.474078178405762]\n", + "Step = 40. gen_params=[ 0.17561905 -0.53878194 -0.62337 ]\n", + "Step = 40. discrim_params=[-0.21422769 -0.16146933]\n", + "--------------------------------------------------\n" + ], + "name": "stdout" + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Em6DmSYM8EWv" + }, + "source": [ + "### 4.1 Comparison\n", + "\n", + "To analyze performance, we plot the overlap between the generated state $\\psi = G(\\theta)|0\\rangle$ and the true data state $\\psi = G(\\theta_0)|0\\rangle$ over time. Since the models are initialized in the $|0\\rangle$ state, the initial fidelity with the data $\\frac{1}{\\sqrt{2}}(|0\\rangle + |1\\rangle)$ is $1/2$.\n", + "\n", + "In the first half of training, the EQ-GAN and perfect swap test experience identical training, since both are being optimized with frozen discriminators. In the second half of training, the EQ-GAN is seen to generate a state closer to the true data state, while the perfect swap test stabilizes at the incorrect state. As with classical GANs, the quantum GAN naturally drifts away from the converged state after it has been obtained due to a vanishing gradient. To identify where the converged state is, we examine the loss function and select the lowest discriminator loss (dashed line)." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "k9l5trOpiBjv" + }, + "source": [ + "def stopping_ind(d_loss, smoothing_period=5):\n", + " \"\"\"Get overlap and parameters at minimum generator loss.\"\"\"\n", + " # simple moving average\n", + " flattened_loss = np.array(d_loss).flatten()\n", + " if smoothing_period > 1:\n", + " smoothed = np.convolve(flattened_loss, np.ones(smoothing_period), 'valid') / smoothing_period\n", + " else:\n", + " smoothed = flattened_loss\n", + " \n", + " # find when the discriminator loss is lowest in the second half of training\n", + " # this corresponds to when the GAN is most fooled by the fake data\n", + " n_episodes = len(d_loss)*2\n", + " best_ind = n_episodes//2 + np.argmin(smoothed)\n", + " best_ind += smoothing_period // 2\n", + " if best_ind >= n_episodes:\n", + " best_ind = n_episodes - 1\n", + " return best_ind" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 290 + }, + "id": "tA1o4KU7BKMH", + "outputId": "5c214e2f-067e-4528-a0fd-8255d156334c" + }, + "source": [ + "fidelity_perfect_swap = overlap_perf**2\n", + "fidelity_adversarial = overlap_adv**2\n", + "adv_best_ind = stopping_ind(d_loss_adv, smoothing_period=5)\n", + "\n", + "plt.figure(figsize=(5, 3.9))\n", + "plt.plot(fidelity_perfect_swap, 'C1', label='Perfect SWAP')\n", + "plt.plot(fidelity_adversarial, 'C2', label='EQ-GAN')\n", + "plt.axvline(x=adv_best_ind, c='C2', linestyle='--')\n", + "plt.legend(fontsize=12)\n", + "plt.xlabel('Iteration', fontsize=14)\n", + "plt.ylabel('Fidelity $|\\\\mathrm{data} | \\\\mathrm{generated} \\\\rangle|^2$', fontsize=14)\n", + "plt.tight_layout()\n", + "plt.show()" + ], + "execution_count": null, + "outputs": [ + { + "output_type": "display_data", + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAWAAAAERCAYAAABFH30oAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjIsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+WH4yJAAAgAElEQVR4nO3dd3wUdf7H8dcnjZJAQi9JkCpNERUQ6RYU7IoloKd4lvPUE/U8xY5Y8cR25+lhoag0G6ICnvKTKqhUKQICoYTeQxop+/n9MZsQQsBksskkm8/z8dhHdmdmZ95JNp/Mfvc736+oKsYYY8peiNcBjDGmsrICbIwxHrECbIwxHrECbIwxHrECbIwxHrECbIwxHgnzOkBpq1u3rjZt2tTrGMYEhU3JmwBoWrOppzkqmsWLF+9V1XoFlwd9AW7atCmLFi3yOoYxQWHW1lkA9Inv42mOikZENhe2POgLsDEmcKzwBpa1ARtjiizxUCKJhxK9jhE07AzYGFNkwxcMB2B0v9EeJwkOdgZsjDEeKTcFWEQ+EJHdIrLyBOtFRN4UkfUi8quInFXWGY0xJpDKTQEGxgD9TrK+P9DKf7sTeLsMMhljTKkpNwVYVecA+0+yyZXAOHUsBGJEpFHZpDPGmMCrSB/CxQJb8z1O8i/b4U0cY8q3lMwUNiVvYlvKNnam7mRX2i72pO3h0JFDHM48zOGsw6Rnp5OZk0lmTiY+9SEiAIRICGEhYYSHhBMWEkbV0KpUC6tGli+LqqFVGTp3KDUjahJTJYZ61etRv1p96levzyk1T6F6eHWPv/OKoyIV4CITkTtxmilo0qSJx2mMKT3Zvmx+P/A7a/avYVvKNnak7mB7ynY2J29mT/qeY7atFlaN+tXrEx0RTXTVaOJrxFM1rCoRoRFEhEYQKqGoKoriUx/ZvmyyNZvMnEyO5BwhPTud9Ox0UjJTWLZ7GcmZyRzOPHxcptioWFrGtKRZdDNio2LzbnWq1aFGRA1CpNy88fZcRSrA24D4fI/j/MuOo6qjgFEAnTp1sik/TFDI8mWx8eBG1h1Yx9r9a1m5byWr960mPTsdcM5a61WrR+OoxnRr3I2m0U1pVrMZcTXiaBjZkJoRNfPOcN1as38NAG1qt8nLtC99H7vTdrMzdSeJhxLZcHADvx/8nQXbF5Dpyzzm+WESRkzVGGpE1KB6WHUiwyOJrhJN3Wp1qVetHvWq18sr2A2qNyA0JLREecu7ilSApwL3ishE4BzgkKpa84MJWgcyDrBs9zKW7VnGst3LWLl3ZV5BiwiJoHXt1gxoNYAO9TrQvk57GkU1IjwkvFQzjfh5BHC0H3B4SDgNIxvSMLIhHep1OGZbn/rYm76XbSnb2J6ynf0Z+zmQcYD9Gfs5nHmY1OxU0rLS2HBwAwt3LDzubDpMwoirEUfz6OY0i25Gy1otaVOrDU2jmxIWUpFK14mVm+9CRCYAfYC6IpIEPA2EA6jqO8A04BJgPZAG3OpNUmMKl+PLYfPhzaw7sI51+9eReCiROtXq0CKmBS2iW1C7am1yNIcczSHLl0VKZgqHsw6TmplKpi/Tecvvy2Zz8maW7l7KxkMbAQgLCaNdnXYktEmgXZ12tKndhlNqnlLui1CIhFC/utM2fGb9M/9w+4zsDPak7SEpJYltKdtIOpzEpuRNbDy0kTlJc8jWbACqhFahda3WdKzfkbManMVZ9c+iVtVapf3tlIpy8xtU1YF/sF6Be8oojjF5jmQcYvGqCSTuW0Ni8iaSjuynWngUdWrEUqd2Sw7hY/X+31izf01ec0CohBJXI46fdvzE4azj20lPpkZ4DTrW78jlLS7nzPpn0r5Oe6qGVS2Nb61cqRpWlfia8cTXjD9uXZYvi8RDiazdv5bf9v/Gqr2rmLhmIuNWjwOgY72OXNr8Ui5uenGFKsYS7LMid+rUSW00NOPGkYxDfPrDo3ywYw67Q5220xo+H01y4IjmsDc0hIOhoVRTaB0ZS7v4nrSt257WtVrTPKY5VUKroKrsSd/D+oPrOZx5mFAJJURCCA8JJyoiiqhw5xYRGpHX66BqWNVy+0HVrTOcN57l4VLkzJxMVu1bxc87fmbGphmsP7ieMAmjU8NOnNXgLM6sfyYd6nYoF70yRGSxqnYquLzcnAEbU55MmfkIb27+hj2hwtkhVXjq1EG0b3oBdeq1R8LCITMVdq0ia9siQpZ+TOimBbB1K3QfAs0ug1DnT0tE8t6GB0Tafti3HvZtgIxD+A8CoeEQWR+iGkCNBlAzDkLKZxEPlIjQCM6sfyZn1j+TOzvcyboD6/h649fM3z6ft5e9jaIIQqPIRjSNbkrTmk3pUK8D5zQ6h7rV6nodH7AzYGOO4cvJ5rUvrmNM6no6ajj3nXE3nTve5hS5E1GF9d/D3Fdhy4/QsANc8S9o3DEwoY4chkUfwMJ34PD2oj0nvDo0aA8NToP4c6DVRRBZx32GzDTYv5Flm3+AtH10lKqQfgDSD0JWGmSlO7ecTPBlQU42+LJBfaA5zldwflb5iQDifJWQArfcZaHO15BQ535I7i0s3y0UQsKd+6HhJAv86ktjRc5hNuWksSknlU1ZyaRpFgCtqtanR622XNKwK61jTkWqREHVaKgW4/zsSthbpKATnQFbATbGLyP9AI99egXf+Q6SUDWeoQOmEBoWUbydrJ4K0/4Bqbvh3Hug18NQtaa7QGn74Zf3YOF/nGLX/DxoeSHUaQl1WkB1f0FVhZwjkLLbuR3eDrt/g50rYdcK50xZQiC+K5x6EcSeDY3OcApOYVQheTskzoHE2bBpPhzacvx2VaKhWjSER0J4NecWGuGcjYeEO2fguQUzt8iCc985kP+Lz1+YFXw5zlf131ff0VtuQffl+O/7v+YWe1+2v/jnv2VCdgag5ABrIiJYUK0qC6tVZXHVKmSL0Cwzi0tTU7nmcAr1cnxO9uq1nZ9v9TpQpUa+gh8OFz4N0XHF+lVaATbmJFJTdvLXTy9lqWTyUN2u3Nz/v4jbt/DpB+H7p2HxGKdIdf4znPNXp2mgKLYvg5/fhZWfOsWj9SXQ8yGIO7v4WVRhxzJYOx3WTHMKcq5azSCyrr94VnfOZJO3Q/IOyEp1tqlWG5r1hAanQ53mLAvJgcj6dIzrkdfMUu6pOsU5OwOyjzhn6tkZHEjZyXc75jNt5wIWJ28kjBD6RjVlUNUmnJENkr7f+Sd45HC+Ap8NN33m/AMshhIVYBGpBtRW1W0FlrdX1VXFSlLGrACbP5KdlcGQCecx33eYES1u4OKeTwZmx9uXwrzX4bepzpnTqRdBbCfnDLReG0CdM7TMNKdIblkAmxfAnt+cs8ozboDOd0CDdoHJA5C61znWjuWwc4XzzyI7w2nTDqsKNRtDzViIaQJNu0P99se0JZenD+ECaUvyFiasmcCU9VNIyUqhQ90ODD5tMOfHnx+Qi0FcF2ARuRZ4HdiLM3jPHar6k3/dElUt18NCWgE2f+TFyZcxPn0zTzY8n+svfiPwB9i3ARa85bQTHyx0ajBHlZoQ19lpr+048MRNBB4K1gKcKy0rjakbpjJu9Ti2Ht5KkxpNuP3027mq5VUluoqwJL0gngDOVtVdInI2MFZEXlDV8RxtzDGmQvp4+t2MT9/MzdWbl07xBeft6mWvOvdT98H2JU5RDgl12kzDqjgfmNVv528vNV6pHl6dhDYJXHfqdczcMpPRK0fz1I9P8c3Gb3i629PE1zi+j3JJFKUAh6vqLgBVXSwivYAvRKQlea3oxlQ883/5Ny/vmsN5IdE8ePUnZXPQyDrQqq9zM+VWaEgoFzW9iL6n9OWz3z/jlUWvMGDqAO478z4GthkYsDEqivIpw24RybvIW1X3A32BtkCHEz7LmHJs7941PLbyHVpoKC9d+2XxezuYSkFEuPbUa5ly5RQ6NejEiF9GsGLvij9+YlH3X4Q24DggW1V3FrKuu6rOD1iaUmBtwKYg9fm456Nu/JyTwsRer9KyxUVeR6owCo6GVpmoKsv3LKdj/eL373bdBqyqSSdZV66LrzGFGf/tvczVVB5r1MeKbzFVxsKbS0RcFd+TCe5rFY0pYN366by6aw69JIqEi970Ok6Fs2D7AhZsX+B1jKBR5J7UIpKIuw/dXldVe6Ubz2VlpfHY3KHUUBh+2Vj3F1pUYqN+HQXAuY3P9ThJcCjOpSyDXR5jk8vnGRNQH864l7UhPl5veRN16p7qdRxjil6AVXV2aQYxpjQlJS3k7b0/c15oNBd0H+p1HGMAawM2lYD6fDw3cwghwGN9/+11HGPylLgAi0iciFgnSlNuzZg7nPmkcV/DnjRs9MdT4xhTVlwNZyQiZwJX+W+nAaki8i3wJfC1qh4MXERj3Es+tJURGz7lNAknoW8pXWpciTx17lNeRwgqRT4DFpG2IvKmiGwGZgKtgBeAWkAPYDkwBNglIjNF5G+lEdiY4hj1v7+xPwSePPdpu9otAJpFN6NZdDOvYwSN4pwBd8EZfOc2YJaqf4pSx6/+23MiEgtcCVwB/CtQQY0prs2b5/Jx6nqurtKIdm2u8jpOUJi1dRYAfeL7eJojWBSnF8RYYGwRttsG/Md/M8YzI2cPJULhbxdaN/RAGbvKKQFWgAPDekGYoLRw8X/5QZO5o87Z1K3X1us4xhSqOFfCfVDUbVX1z+7iGFNyOdmZvPzrf2is8Ce73NiUY8VpA65X4HEvwAfkjs12Gs4Z9ZwA5DLGtS9+GMrvIT5eaX49VcrhrBLG5CpOG/DlufdF5FEgHbhVVVP9yyKB9zlakI0pcxnpB3h76//oIBFc1P1xr+MYc1JupzW9D7ggt/gCqGqqiDyL00Xt+UCEM6a4Js78B7tDhZc63GOD7ZSCF3u+6HWEoOK2AEcBjYHVBZY3AqqXKJExLh1O3sZ7exbSLaQ6nc+8zes4QalhZEOvIwQVt6cInwGjRSRBRJr6bwk4TRCfBy6eMUU3dubfORQi3HeODbZTWmYkzmBG4gyvYwQNt2fAfwVGAmOACJxxgrNxCvBDAUlmTDHs27uOcQdX0jesFu3bXON1nKA1ae0kAPo16+dxkuDgqgCrajpwt4j8A2jhX7whf5uwMWXpvR8e4ojAvT2e8TqKMUXm+lMKEekPTAImAgf8H8LdLiIXBCydMUWwe9dKJqdu5IqIhjRvdr7XcYwpMlcFWERuBCYDvwPNgHD/qlDgYZf77Ccia0VkvYgc14gnIqf4B/n5VURm+WdrNobRsx8lB7iz57NeRzGmWNyeAT8M3KGqD+C0/eZaCBR72lARCQXeAvoD7YCBItKuwGavAONUtQMwHLD+MIY9u1fxSWoil0U0JD7e5ikzFYvbD+FaAYVNjZoC1HSxvy7AelXdCCAiE3FGVMvfza0d8KD//g/AFBfHMUFm9OzHyBa4s8cwr6NUCq/2edXrCEHF7RnwdqCwWQ17ARtc7C8W2JrvcZJ/WX7LgdyPt68GaohIHRfHMkFi7941fJKygUsjGtCkSQ+v41QKtarWolbVWl7HCBpuC/Ao4E0R6e5/HC8itwAvA28HJNnxHgJ6i8hSoDewDcgpbEMRuVNEFonIoj179pRSHOO1MT8MJVPgzu7DvI5SaUxZP4Up6+3NZ6C47Yb2sohEA98BVXGaBI4Ar6jqWy52uQ2Iz/c4zr8s/zG34z8DFpEoYMCJpj5S1VE4/yTo1KmTushjyrn9+9czOWU9l0bU55RTenodp9L4cv2XAFzV0ga4DwS3bcCo6uMi8jxO22wIsFpVU1zu7heglYg0wym8CcCg/BuISF1gv6r6gEeBIg+PaYLPR7MeI0Pg9m5PeB3FGNfcdkNrIiKiqmmqukhVf84tviLSpLj7809vdC/wLfAbMFlVV4nIcBG5wr9ZH2CtiKwDGmAD/lRaKYd3MPHQai4MjbF+v6ZCc3sGnIgz8M7u/Av9H4ol4vQHLhZVnQZMK7DsqXz3PwU+dRPWBJfJsx7ncIhwW+cH/3hjY8oxtx/CCc74DwVFARnu4xhzchnpBxi352fOpZqN+WAqvGKdAYtI7vwuCrwoImn5Vofi9OddFqBsxhznyzlPsy9UuL3DnV5HqZT+c6HNtRtIxW2CON3/VYC2QGa+dZnAEpwr1owJuOysDEZv+4EOEkbnM2zaQS9UC6vmdYSgUqwCrKrnAYjIaGCIqiaXSipjCvHt/BfYFgoPtxpos114ZOKaiQAktEnwOElwcNsP+FYRCRORbkATnDGB868fF4hwxuRSn4/RiV/STIU+59iHb175dtO3gBXgQHFVgEWkDfAVzkhognNFWhiQhXNBhhVgE1ALl45ibYiPZ2IvJiTUdfd1Y8oVt+/jXgcWA9FAGk57cCecD+AGBCaaMUeNWfkBdXOUy3o+7XUUYwLGbQHuDDznnwHDB4Sp6hKcYSpHBiqcMQBrf/+GH0nnxrpnE1GlhtdxjAmYkvQDzu2CtoejI5clAS1LGsqY/Mb+PJJqPuW63jbgugkubhvTVgJnABuBn4FHRCQHuANYH6BsxrBz5zKmZ+0mIbIZ0dHFvsrdBNjofqO9jhBU3Bbg54FI//0ngG9wRkTbC1wfgFzGAPDxvGdQ4KbuT3odxZiAc9sN7dt89zcCbUWkNs7knDb8owmI1JSdfHr4d/qG1yY2tovXcQwwZuUYAAafNtjTHMGi2G3AIhIuIj+JSOv8y1V1vxVfE0hfzB1GSohw85l/8zqK8ZudNJvZSbO9jhE0il2AVTULp/+vFVtTanKyM/lox3zO1AhOb3+d13GMKRVue0GMxfnAzZhS8cPCkWwLhT+1tG7lJni5/RAuErhRRPriXJCRmn+lqt5X0mCmcvtw/afE+uD8rg95HcWYUuO2ALfFGfkMoHmBddY0YUpk5epPWCKZ/KPhuYSGRfzxE0yZqRJWxesIQcVtL4jzAh3EmFzjlvybSJ9yTc9nvI5iCnjnwne8jhBUbEw/U67s3LmM77L3cU1UC6JqNPI6jjGlynUBFpH+IvKNiPwmIvH+ZbeLyAWBi2cqm0nzn8MHDDr3Ua+jmEK8s/wd3lluZ8GB4nZW5BuBycA6oCkQ7l8VijMgjzHFlpF+gE+T19AnNJq4uK5exzGF+GnHT/y04yevYwQNt2fADwN3qOoDQHa+5QuBjiVOZSqlb+Y9x8EQ4abTb/M6ijFlwm0viFbAgkKWpwA13ccxlZX6fHyU9D2tCaFTh8FexzGmTLg9A94OnFrI8l7ABvdxTGX187L3WB/i48YmF9t8b6bScHsGPAp4U0Ru9z+OF5GewMvAsEAEM5XLRyvHUNunXNL9ca+jmJOIqRLjdYSg4rYf8MsiEg18B1TFGYryCPCKqr4VwHymEti6dT6zfcncEdOeKlWjvY5jTuK1817zOkJQcT27oao+LiLPA+1wmjJWq2pKwJKZSmPCwpcJBW7oZmP+msqlRNPLqmoasChAWUwllJaymy9SNtA3vA71G5zmdRzzB15f/DoA9599v8dJgoPrAiwiNwAXAPUp8GGeql5Rwlymkpg671lSQoQbO/7V6yimCJbvWe51hKDiqgCLyD+B+3HafrdjA/AYF9TnY/z22ZwmYXRoZzNZmcrH7RnwzcBAVf00kGFM5bJgyTskhiovNLnEup6ZSsntqz4EWBbIIKbyGb/6Q2r7lIu7DfU6ijGecFuARwE3BTIIgIj0E5G1IrJeRI77qxSRJiLyg4gsFZFfReSSQGcwZWPLlnnM8R3mupjTiKhSw+s4pogaRDagQWQDr2MEDbdNEDHAIP+MGL8CWflXupkRQ0RCgbeAvkAS8IuITFXV1fk2ewKYrKpvi0g7YBrOYECmgpnwk9P17PpuT3gdxRTDSz1f8jpCUHFbgNtxtAmiTYF1bj+Q6wKs909zj4hMBK4E8hdg5ehYE9E4HwCaCiYtZTdTUjZa1zNT6ZWnGTFiga35HicB5xTYZhjwPxH5G868dBcWtiMRuRO4E6BJkyYBD2pKJrfr2aAz7vQ6iimmET+PAOCRLo94nCQ4lHRA9q9FZHUZDsg+EBijqnHAJcCHInLc96Cqo1S1k6p2qlevXinGMcWlPh8Tts+mrS+UM9oP9DqOKaY1+9ewZv8ar2MEjZIOyP470IzADMi+DYjP9zjOvyy/2/zHRVUX4IxDUdfl8YwHFi4dxcZQZZCNemZMuRqQ/ReglYg0E5EIIAGYWmCbLThX3yEibXEK8B6XxzMeGL9qHLV8Sv/uj3kdxRjPuS3AAR+QXVWzgXuBb4HfcHo7rBKR4SKSe2nz34E7RGQ5MAEYrKp2FV4FkZS0kNm+ZAbUbGOjnhmD+14QuQOyby6wvEQDsqvqNJyuZfmXPZXv/mqgu9v9G29NWjiCEOCGbjbmb0V1Ss1TvI4QVGxAdlMm0tP28/nh3zk/LIaGjc70Oo5xaVi3YV5HCCo2ILspE9/Mf47kEGHg6X/2Ooox5YYNyG5Knfp8jE+ayak24WaFN+zHYc5XOxMOCBuQ3ZS6Rb+O4fcQH8MaX2Rdzyq4zckFP/YxJeF2POAPTrBKgQxgPTBJVe1SYcOEFR9Q0ybcNOY4bs+A6wE9AR+w0r/sNECAxcA1wHAR6amqNmxlJbZj+2L+L+cgN9c8lWrVa3sdx5hyxe37wfnAdCBOVXupai+cK9emAf8DTgG+AUYGJKWpsCYteBEFEs61Cy+MKcjtGfAQ4Hx/GzDgtAf7P5Sb6e8lMQL4PhAhTcWUkX6Az5LX0CcsmsaNO3kdxwRAm9oFBz80JeG2AEcBjXCuWMuvoX8dQHIJ9m+CwPQfX+RgiHDjabd6HcUEiI2CFlhuC+QXwPsi8jDOGA4AnXEuxPjc/7gLsK5k8UxFpT4fE7b8j5YqdD7D+v4aUxi3Bfgu4FXgo3z7yAY+AB7yP/4NuKNE6UyFtXTFR/wWksNTjS60rmdBZOhcZ6YwmxkjMNxeCZcG3CUifwda+BdvUNXUfNtY74dK7OMV71LTp1zaw7qeBZNdqbu8jhBUSnohRirOnHDG5Nm5Yykzsw9wc41WVK9uwzUbcyL23tAE3KQfn0eBG8591OsoxpRrRT4DFpFE3E24+bqqvunieaYCykg/wKfJazgvLJrY2C5exzGmXCtOE8Rgl8fY5PJ5pgKaPv8FDoYIg6zrWVA6o94ZXkcIKkUuwKo6uzSDmIpPfT4+3vo/WhJiXc+C1P1n3+91hKDi+kM4EQnD6evbBIjIv05Vx5Uwl6mAFv06hrU26pkxReZ2NLQ2wFc4MyILkOPfVxbOwOxWgCuhj1a8T4xPubTHE15HMaXkgR8eAOC1817zOElwcHua8jrOqGfRQBrQFugELAMGBCaaqUiSkhbyQ84hrotuS9VqtbyOY0rJwSMHOXjkoNcxgobbJojOQG9VTRURHxCmqkv8lyb/C+gQsISmQpiw4EVCgRu6P+l1FGMqDLdnwIJz5guwB4j1308CWpY0lKlYUlN28nnKBvqG1aFBA/vfa0xRuT0DXgmcAWwEfgYeEZEcnLEf1gcom6kgvpz3LCkhwk1n3u11FGMqFLcF+Hkg0n//CZzB138A9gLXByCXqSB8OdmM3z6HDhJGh/Y3eB3HlLJzGp3jdYSg4nYwnm/z3d8ItBWR2sABVXVztZypoOYt+hebQ2FE0yu8jmLKwF1n3OV1hKDiqg1YRJqIiORfpqr7VVVFpElgopmKYNya8TTIUfp2s4G6jSkutx/CJeJMzHkMEanjX2cqgbXrvuYnMhhUrwvh4dW9jmPKwF3f38Vd39tZcKC4bQMWCh+YJwpnWnpTCYxb9BrVfMqAXsO8jmLKyJHsI15HCCrFKsAikjuqmQIvikhavtWhOJcm20DslcCe3auYlrmLa6s3ITraWp2McaO4Z8Cn+78KztVvmfnWZQJLgFcCkMuUcxPnDycHuKmrjflrjFvFKsCqeh6AiIwGhqhqcqmkMuVaRvoBJh9cRe/QmpxySk+v4xhTYbnthnariISJSDdsNLRK56u5wzkYItx8+m1eRzFlrHdcb68jBJVyMxqaiPQD3sBpS35PVV8qsP414Dz/w+pAfVWNcZPfuOfLyWZc0kzaSiidzrBB1yubwacN9jpCUCkXo6GJSCjwFtAfaAcMFJF2+bdR1QdUtaOqdsQZ8Odzl9lNCcz55Q02hSqDm19pY/4aU0Ju/4I6A8/5Z0XOGw0NeBgY6WJ/XYD1qrpRVTOBicCVJ9l+IDDBxXFMCY1ZM4FGduFFpXXrjFu5dYa98wmU8jIaWiywNd/jpHz7PPbAIqfgNH383wnDidwpIotEZNGePXtcxDGFWbHqExbLEW5q2M0uvDAmANwW4NzR0ODoaGi9gWco/dHQEoBPVTXnRBuo6ihV7aSqnerVO+6CPePSmKVvUsOnDOg13OsoxgQFtwX4eZyzYHBGQ2uCMxraRcB9Lva3DYjP9zjOv6wwCVjzQ5nbunUB32cf4LqarYmMauh1HGOCQnkZDe0XoJWINMMpvAnAoIIb+Xtf1AIWuMlt3PtowfOEAIN6PO11FGOChutZkQtS1f0leG62iNwLfIvTDe0DVV0lIsOBRao61b9pAjDRhrwsWwf2b+CL1E1cUqWBzXhRyV3c9GKvIwSVIhdgEfmgqNuq6p+LG0RVpwHTCix7qsDjYcXdrym5CbOfID1E+PM5Q72OYjyW0CbB6whBpThnwAU/zeqF0wVthf/xaThtynMCkMuUE2lpexl/cAV9QmvSokVfr+MYj6VnpwNQLayax0mCQ5ELsKpenntfRB4F0oFb/X2BEZFI4H2OFmQTBD6f/QSHQoTbzrrX6yimHLj7e2fev9H9RnucJDi47QVxHzAst/gC+O8/C/wtEMGM97Ky0hi7Yx5naQQdTzvuM1FjTAm5LcBRQONCljfCGafBBIHpc59jZ6hwW9ubvY5iTFBy2wviM2C0iPwDWOhf1hUYgY3REBR8Odl8sOlrWkkIPTvbmxpjSoPbAvxXnDEfxgDh/mXZOG3AD5U8lvHarJ9eZUOo8kKTK2zQHWNKidsLMdKBu/1nwC38izfkbxM2FZf6fIxaO544hf49nvA6jilHrmx5sjGyTHGV6EIMf8H9NUBZTDnx4+L/sCokh2GNLyIsvKrXcUw5clXLq7yOEJfdae0AAB7dSURBVFQCdiWcCQ7q8/HfVaNp4FOusEF3TAEHMg4AUKtqLY+TBIfiXAmXSOFT0f+R11X1zT/ezJQHi34dw1LJ5NHGvQmvEul1HFPOPDjrQcD6AQdKcc6AB7s8xiaXzzMeGLX8HerkKNf0fs7rKMYEveJcCTe7NIMY7/26ahILSefv9btStZq9xTSmtFn/IpPn7UWvEe1Tru/zgtdRjKkUrAAbAJavnMg8UhlctxPVo+p7HceYSsF6QRgA3l78GrV8yqDzXvY6iinHbmh9g9cRgooVYMOyleOZTxoP1O1iZ7/mpPo16+d1hKBiTRCGtxe/Tm2fknCBnf2ak9uZupOdqTu9jhE0XBVgEZkiIpeJiBXwCm7Zio/5kXRurdeV6tXreh3HlHOPzn2UR+c+6nWMoOG2gKYCk4AkEXlBRFoFMJMpQ/9e4pz9Xn/+S15HMabScVWAVfVGnLF/nwUuBNaKyBwRuVlEbK6SCuKnJaP4iQxua9DNzn6N8YDrJgRVTVbVt1W1C3A6sBj4L7BDRP4rIm0DFdIEnvp8vLH8bRrmKDec/0+v4xhTKZW4DVdEGgNXApfhjAn8GRAP/CoiNjZwOfV/C15mRUg2dzfpT5Wq0V7HMaZSctUNTUTCcYrun4G+wFLgZWCCqqb4t7kCGAe8EpioJlBysjP519rxNEO4vPezXscxFcgt7W/xOkJQcdsPeAcgwHhgqKoWNibwHOCA22Cm9Hw95yk2hCojm99g4/2aYukT38frCEHFbQF+Axipqmn5F4qIAPGqukVVDwLNShrQBFbmkcP8Z9M3tJNQ+nZ/zOs4poJJPJQIQLNo+9MOBLdtwMNwZkYuqDaQ6DqNKXUTvv8720NhyGl32FxvptiGLxjO8AU2UH+guP0LlBMsjwIyXO7TlLJDBzfx390/0p3qdOt8j9dxjKn0itUEISK5M1so8IKI5G+CCAW6AMsClM0E2Dvf3UeqwIPdh3kdxRhD8duAT/d/FaAtkJlvXSawBOv1UC5t2TKPiakbubpKY05t2d/rOMYYilmAVfU8ABEZDQxR1eRSSWUC7vXZQwlXuOeC17yOYozxc9ULQlVvDXQQU3qW/voR3/kOcXetM6hXv73XcUwFdmeHO72OEFSKMyvyVOAmVU323z8hVb2iuEFEpB9O97ZQ4D1VPW50GBG5HqcHhgLLVXVQcY9T2eRkZ/LS4leor8otF73hdRxTwZ3b+FyvIwSV4pwB7+PotPT7AhlCREKBt3CuqksCfhGRqaq6Ot82rYBHge6qekBEbOTwIvjih6GsDsnhpVOusQF3TImt2b8GgDa123icJDgUZ1bkWwu7HyBdgPWquhFARCbiXOq8Ot82dwBvqeoBf4bdAc4QdA4d2sKbSf/jLKnCJb2GeR3HBIERP48AYHS/0R4nCQ7lpSd+LLA13+Mk/7L8TgVOFZH5IrLQ32RhTuKtGX/lkMBj3Z6xiy6MKYeK2wZcJG7agIsgDGgF9AHigDkicrr/kudjiMidwJ0ATZo0KYUo5d/a379hUvpmrqvWhNanXuZ1HGNMIYrbBlxatuEMYZkrzr8svyTgJ1XNAhJFZB1OQf6l4M5UdRQwCqBTp05acH2wU5+PF+c/TU2Fv138H6/jGGNOwFUbcCn4BWglIs1wCm8CULCHwxRgIDBaROriNElsLMVMFdaU/3uExXKEp2MvIjqmqddxjDEnUC6mpVfVbBG5F/gWpxvaB6q6SkSGA4tUdap/3UUishrIAf6hqqV5Vl4h7du7jle2TucsqcI15wd2lmOfz0dSUhKpqakB3a8pXyIjI4mLiyOkkM8Nhpw1xINEwUtU3b1DF5H+wL04Q05erKpbReR2IFFVZwYwY4l06tRJFy1a5HWMMvPIx334X9ZePuv9Bs2bXRDQfe/evZsjR44QGxtb6B+nqfh8Ph/btm2jSpUq1K9vPT0DRUQWq2qngsvdTkt/IzAZWIdTgMP9q0KBh92GNCUz/5d/My17H7dHnx7w4gtw8OBBGjRoYMU3iIWEhNCgQQMOHTpU6Pplu5exbLeNtxUobv+SHgbuUNUHcOaBy7UQ6FjiVKbY0tP28+yK/9I0B27v/06pHCMnJ4fw8PA/3tBUaOHh4WRnZxe67o0lb/DGEruiMlDctgG3AhYUsjwFqOk+jnHrja9vYVsofNDh/lKdZNOZ9MQEM/sdlx23Z8DbcXohFNQL2OA+jnHj56Xv8XH6JgZVO4XOZ97mdRxjTBG5LcCjgDdFpLv/cbyI3IIzM/LbAUlmiiQ1ZSdPLX2DJjkw5LIxXscJCrt27aJXr17UqFGDv//9717HMUHMVQFW1ZeBz4HvgEjgB+Ad4B1VfStw8cwfeeXrW9gRojzfeWilHmynadOmVKtWjaioKBo0aMDgwYNJSUlxta9Ro0ZRt25dkpOTGTlypOtMgwcP5oknnjjpNl9++SUdO3akZs2a1K1bl/PPP5/ExER27NiBiLBr1668bZ9//vlCl/Xrd/Sq/MTEREJCQvjrX/963LFEhMjISKKiooiNjeXBBx8kJyfH9fdnSs71x9mq+jhQF2cgna5APVV9MlDBzB+b9/O/+PTIdm6JakXH02/0Oo7nvvrqK1JSUliyZAmLFi3iueeeK9bzVRWfz8fmzZtp165dqbeFrl+/nptvvpmRI0dy6NAhEhMTueeeewgNDaVRo0a0bNmSOXPm5G0/Z84c2rRpc9yyXr165T0eN24ctWrVYtKkSRw5cuS4Yy5fvpyUlBRmzpzJ+PHjeffdd4uV+ZEuj/BIl0dcfLemMCXqT6Sqaaq6SFV/VlV3pxvGlb17fuPxVf+lZY5wjzU9HCM2Npb+/fuzcuVKABYuXEi3bt2IiYnhjDPOYNasWXnb9unTh8cff5zu3btTvXp1br75ZsaOHcvLL79MVFQU33//PT6fj5deeokWLVpQp04drr/+evbv35+3j3nz5uXtPz4+njFjxjBq1Cg+/vjjvP1cfvnlx+VctmwZzZo144ILLkBEqFGjBgMGDMgbv6RXr155xTYnJ4clS5YwZMiQY5YtWLAgrwCrKuPGjeO5554jPDycr7766oQ/ozZt2tCzZ8+8n1FRtandxoaiDKDiDMbzQVG3VdU/u4tjisKXk82j024hDXi/9yul2uvhpKYPhZ0rSvcYDU+H/seNzX9SW7duZdq0aVxzzTVs27aNSy+9lA8//JB+/foxc+ZMBgwYwJo1a6hXrx4AH374IdOnT6d169aoKmFhYcTFxeWdQb/xxhtMmTKF2bNnU69ePe677z7uueceJkyYwObNm+nfvz+jRo3i2muvJTk5ma1bt9KxY0d+/PHHY/ZT0FlnncWaNWt44IEHuOKKK+jcuTNRUVF563v16sWrr74KwNKlS2nbti0XXHABb7/9dt6yrKwsunTpAjj/CJKSkkhISGD16tWMHTuWa6+9ttBjr169mrlz5/L8888X62e7YLvT+ckGZg+M4pwB1ytwGwBcDbT0364CrsFpljCl6INvbmMh6QyNu5iWLS7yOk65cdVVVxETE0OPHj3o3bs3jz32GB999BGXXHIJl1xyCSEhIfTt25dOnToxbdq0vOcNHjyY9u3bExYWVmg/53feeYfnn3+euLg4qlSpwrBhw/j000/Jzs5m/PjxXHjhhQwcOJDw8HDq1KlDx45F6wrfvHlzZs2axbZt27j++uupW7fuMW3XvXv3ZuXKlRw8eJC5c+fSs2dPWrVqxZ49e/KWde3alYiICADGjh1L//79qVWrFoMGDWLGjBns3n3ssNlnnXUWtWrV4vLLL+f222/n1luLN8TLqF9HMerXUcV6jjmx4gzGk/ceSkQeBdKBW1U11b8sEngfKOVTospt2YqP+ff+xfQLq801F/zT2zDFPDMtbVOmTOHCCy88ZtnmzZv55JNPjnk7npWVxXnnnZf3OD4+npPZvHkzV1999TFXAIaGhrJr1y62bt1KixYtXGfu2rUrkydPBuCXX37hhhtu4Pnnn+fFF1+kadOmxMbGMnfuXObMmcNf/vIXALp165a3LLf5IT09nU8++YT33nsPgHPPPZcmTZowfvx47r///rzjLVmyhJYtW7rOawLLbRvwfcCw3OIL4L//LPC3QAQzx9u3dx3/+OVFGvqEp66YYIOsF0F8fDx/+tOfOHjwYN4tNTWVoUOH5m3zRx+2xcfHM3369GP2kZGRQWxsLPHx8WzYUHjX9+J+iNe5c2euueaaY9plc9uBFyxYQLdu3QDo2bMnc+bMYd68eXkF+IsvviA5OZm7776bhg0b0rBhQ7Zt28bYsWOLlcGULbd/wVFA40KWNwKqu49jTiQrK42Hvh7EAYGR3Z6hRs2CE4aYwtx000189dVXfPvtt+Tk5JCRkcGsWbNISkoq8j7uuusuHn/8cTZv3gzAnj17+PLLLwG48cYb+f7775k8eTLZ2dns27ePZcucsRIaNGjAxo0nHjF13rx5vPvuu3nNBGvWrGHq1Kl07do1b5tevXoxbtw4GjduTM2azkWmPXr0YNy4cRw6dIhzz3XaYseOHcuf//xnVqxYwbJly1i2bBnz589n+fLlrFhhb0rLK7cF+DOccXkTRKSp/5aA0wTxeeDimVyvfHE9i+QITze9ivZtrvE6ToURHx/Pl19+yQsvvEC9evWIj4/nn//8Jz6fr8j7GDJkCFdccQUXXXQRNWrUoGvXrvz000+AM+PKtGnTGDlyJLVr16Zjx44sX74cgNtuu43Vq1cTExPDVVddddx+Y2JimDp1KqeffjpRUVH069ePq6++mocfPjqeVe/evdm9ezc9evTIW9axY0fS09M5++yzqV69Otu2bWPmzJncf//9eWe/DRs25Oyzz6Zfv352FlyOuRqOUkSqASOBP+OMhCZAFk4BfkhV0wIZsiSCYTjKKTMf4cmkadxcvTn/uO5Lz3L89ttvtG3b1rPjm7Jzot914qFEAJpFNyvrSBXaiYajdDUYj6qmA3eLyD+A3E8gNuRvEzaBsfTXj3h26zecI9V44KpJXscxlZwV3sAq7qScN6lq8okm6Mz90KGUJuWsdDYmzuTexS/RSIVXrp5MWHhVryOZSm7W1lkA9Inv42mOYFHcSTk1331TinbvWsldPwwhHHi77yhiatmZh/He2FVOe7IV4MBwNSlnKU/QWemlHN7B3dNu5KDA6HOHEx9vVx0ZE4yK1QtCRDqIiHU+LUVpKbu557PL2CA5vNr+L9bjwZggVtxiupR8lxqLyDci0iiwkSqvtLS93P3pJSznCC82v44eXeyaFmOCWXF7QRS8tKcXUC1AWSq1tLS93Du5H0vJYETz6+jX62mvIxljSpnbOeFMACUf2sqQL65mCRm82GyAFV9Tbr3Y80WvIwSV4hZg5WhPiPzLjEs7dyzlrzMGs0lyeKnZtfTvPczrSMacUMPIhl5HCCrFbQMW4CMRmervC1wVeDf3cb7lpgjWrZ/OjdP/xE5yePv0v1nxLaH80xLl3u69914AkpKSuPHGG6lTpw6RkZF06dLlmCEpT+a7777jvPPOo0aNGnnDTY4YMYKMjIxjthszZgwiwqRJx14wM2vWLESEu++++5jlPXr0YMyYMe6/YQ/MSJzBjMQZXscIGsUtwGNxZkTe5799BGzN9zj3Zv7ArIUjuWXuPwAY02MEXc/+i8eJgkPutES5t3//+9/s37+fHj16EBERwapVq9i7dy8PPPAACQkJTJky5aT7++STT7j22msZNGgQmzdvZt++fUyaNImkpCS2bt16zLZjx46ldu3ajBs37rj9REZG8uGHH7Jp06ZAfrtlbtLaSUxaa1dkBkqxmiCs/2/JZWdl8K+pN/JByjraEsYb/d6nUeOzvY4V1F577TWioqJ4//3388b0HThwIFu2bOHBBx/kyiuvLHToSFXlwQcf5KmnnuKOO+7IW966dWv+9a9/HbPt5s2bmT17Np988gk33HADO3fupGHDo2/XY2JiuPrqq3nmmWcYPXp0KX2npqKxD+HK0O5dK3lk+mAWyRGuqxLLI1dN8m46oQAY8fMI1uxfU6rHaFO7TYkngfzuu+8YMGDAMQOqA1x//fUMHTqU9evX06pVq+Oet3btWpKSkhgwYMAfHmPcuHF06tSJAQMG0LZtWz7++OPjprR//PHHOfXUUxk6dCitW7cu0fdkgoNdVFEG1Ofjs+/+zlXTElilGbzQ5AqeSphRoYtveZU7LVHu7d1332Xv3r00anR8d/XcZXv27Cl0X3v37gU45kw2ISGBmJgYqlevzocffpi3fNy4cQwaNAiAQYMGFdoM0bBhQ+666y6eeuop99+gCSp2BlzKtm6dzzP/dz8/kUEnqcqw817jlFN6eh0rIMrj9OSFTUv0/vvvs2PHjuO2zV1Wt25dtmzZQrt27fLWpaSkUKdOnbztmjVzxuKYOHEi4HyAlpOTA8D8+fNJTEwkISEBcArw448/zrJly46bH+6RRx6hRYsWeWMGm8rNCnAp2bt3De/+30NMTttEVYWnYvsy4IJ/EhJqP/KyduGFF/L555/z9NNPH9MMMXnyZOLi4mjZsiUhISF5k2Hmat26NbGxsXz++efHNSfkN3bsWFT1uGI7duzY45bVqVOH+++/nyeffDIA31nZe7XPq15HCCpWDQJs7941jJ/zFB8dWk2mwFVVGnP3+a9Sv8FpXkertB544AHGjRvHbbfdxosvvkhMTAxffPEFzz77LG+++eZxbcO5QkJCGDlyJHfccQc1a9bk2muvJSYmhvXr17Nr1y4AMjIymDx5MqNGjeLSSy/Ne+5nn33G8OHD+ec/j5849cEHH6R58+a4mQzBa7Wq1vI6QnBR1XJxA/oBa4H1wNBC1g8G9gDL/Lfbi7Lfs88+W0ubLydHf1k6Wh/6sKd2HN1eTxtzmv79w56amDir1I9dllavXu11hJM65ZRTtGrVqhoZGZl3u+qqq1RVdfPmzZqQkKC1atXS0NBQDQsL0zFjxhRpv9OnT9devXppZGSk1q5dWzt27Kgvv/yypqSk6IQJE7Rhw4aamZl5zHPS0tK0du3a+tVXX+kPP/ygsbGxx6wfMWKEAjp69OiAfO+BdqLf9Re/f6Ff/P5FGaep+IBFWkh9cjUlUaCJSCiwDugLJAG/AANVdXW+bQYDnVT13uLsu7SmJDqScYhffh3HrMTpzEndwo5QoYZPuTKqOdd3fpBmTfsE/JheC5YpiZKTk+nevTtXX301w4cP9zpOuXSi3/WtM5yeqKP7WVe64gjolESloAuwXlU3AojIROBKYPVJn1UKNm+eS44vC5EQBCEzO40DyVvZf3g7u1O2s+7QBtYe2csGySFbhGo+pWtYNPfE9eSirg9TrXrtso5siqlmzZpMmzaNDz744Lj+usaUpfJSgGNxrqjLlQScU8h2A0SkF87Z8gOqurWQbUpk8My/sjf0+E75uermKK1DI+kRFc9ZcT0554zB1p2sAoqPj+fpp23QI+Ot8lKAi+IrYIKqHhGRv+BcFn1+YRuKyJ3AneBMG14cT7a5mYysVKeNBiU8NILaUY2pXbMJdWq3tKmBjDEBU14K8DYgPt/jOP+yPKqaf4yJ94CXT7QzVR0FjAKnDbg4Qc7v9nBxNq90VLXQy3ZN8CgPnwtVFuWlAP8CtBKRZjiFNwEYlH8DEWmkqrm96a8AfivbiCY0NJSsrCwiIiK8jmJKUVZWFmFhhZeG/1z4nzJOE9zKRQFW1WwRuRf4FggFPlDVVSIyHKf7xlTgPhG5AsgG9uN0SzNlKCYmhl27dhEbG3vCvrOmYvP5fOzatYvo6MI/16gWZhPgBFK56IZWmkqrG1pl5PP5SEpKIjU11esophRFRkYSFxdX6D/ZiWucS7ET2iSUdawKrbx3QzMVQEhISLE/1DTB5dtN3wJWgAPF3kcaY4xHrAAbY4xHrAAbY4xHrAAbY4xHgr4XhIjsATYX82l1gb2lEKeiZYDykcMyHFUecliGo4qa4xRVrVdwYdAXYDdEZFFhXUYqW4byksMylK8cliFwOawJwhhjPGIF2BhjPGIFuHCjvA5A+cgA5SOHZTiqPOSwDEeVKIe1ARtjjEfsDNgYYzxiBTgfEeknImtFZL2IDC3D434gIrtFZGW+ZbVF5DsR+d3/tVSnoxWReBH5QURWi8gqERlS1jlEpKqI/Cwiy/0ZnvEvbyYiP/l/L5NEpNTHwxSRUBFZKiJfe5hhk4isEJFlIrLIv6ysXxcxIvKpiKwRkd9E5FwPMrT2/wxyb8kicr8HOR7wvy5XisgE/+u1RK8LK8B+/olB3wL6A+2AgSLSrowOPwZnVuj8hgIzVbUVMNP/uDRlA39X1XZAV+Ae//dfljmOAOer6hlAR6CfiHQFRgCvqWpL4ABwWylmyDWEY8ec9iIDwHmq2jFfV6eyfl28AcxQ1TbAGTg/kzLNoKpr/T+DjsDZQBrwRVnmEJFY4D6ciYFPwxk2N4GSvi4Kmyq5Mt6Ac4Fv8z1+FHi0DI/fFFiZ7/FaoJH/fiNgbRn/PL7EmaXakxxAdWAJztyAe4Gwwn5PpXTsOJw/6POBrwEp6wz+42wC6hZYVma/DyAaSMT/WZEXGQrJdBEw34OfRe68lbVxRpH8Gri4pK8LOwM+qrCJQWM9ygLQQI/OALITaFBWBxaRpsCZwE9lncP/1n8ZsBv4DtgAHFTVbP8mZfF7eR14GPD5H9fxIAOAAv8TkcX+eQ6hbH8fzYA9wGh/c8x7IhJZxhkKSgAm+O+XWQ5V3Qa8AmwBdgCHgMWU8HVhBbgCUOffa5l0VxGRKOAz4H5VTS7rHKqao85bzTigC9CmNI9XkIhcBuxW1cVledwT6KGqZ+E0i93jnxE8Txn8PsKAs4C3VfVMIJUCb/PL+LUZgTMd2ScF15V2Dn/78pU4/5QaA5Ec32xYbFaAj/rDiUHL2C4RaQTOfHg4Z4SlSkTCcYrvx6r6uVc5AFT1IPADztu6GBHJnTygtH8v3YErRGQTMBGnGeKNMs4A5J11oaq7cdo8u1C2v48kIElVf/I//hSnIHvymsD5R7REVXf5H5dljguBRFXdo6pZwOc4r5USvS6sAB+VNzGo/z9tAjDVwzxTgVv892/BaZMtNSIiwPvAb6r6qhc5RKSeiMT471fDaYP+DacQX1sWGVT1UVWNU9WmOK+B/1PVG8syA4CIRIpIjdz7OG2fKynD34eq7gS2ikhr/6ILgNVlmaGAgRxtfqCMc2wBuopIdf/fSu7PomSvi7JqPK8IN+ASYB1Ou+PjZXjcCTjtSlk4Zx234bQ7zgR+B74Hapdyhh44b+F+BZb5b5eUZQ6gA7DUn2El8JR/eXPgZ2A9ztvPKmX0e+kDfO1FBv/xlvtvq3Jfjx68LjoCi/y/kylArbLO4M8RCewDovMtK+ufxTPAGv9r80OgSklfF3YlnDHGeMSaIIwxxiNWgI0xxiNWgI0xxiNWgI0xxiNWgI0xxiNWgI0pIREZln8kO2OKyrqhmQpFRMbgDFBzWf77ZXTspjiD03RW1UX5lkfh9P/cVxY5TPAI++NNjAlu/ktJc9Tl2YiqpgApgU1lKgNrgjAVkogMw7n081IRUf+tj39drIhMFJED/ts3ItIq/3P9g2oPFpENOOMQR4ozIP9c/3P2i8i3ItI232ET/V9/8R9vVv795dt/iIg8KSJbReSIf1D1K/Otb+p//gD/QOJp4gyE37eUflymnLICbCqqV4DJOJegNvLffhSR6jjX52cAvXEG89kBfO9fl6sZMAi4Dmeg8Qycy11fxxn0pg/OkINf5ZvloIv/az//8a45QbYhwD+AR4DTcQbS+VxEOhbY7nngTf/xfwEm+pszTCVhTRCmQlLVFBFJB46oM2gMACJyE84A6rfmNimIyF9wRsq6DKdoA0QAf9KjI2uBMxIc+fZ1K5CMU3jn4YyNC7Av/zEL8RDwiqqO9z9+yj+U5EPATfm2e01Vv/If6zHgZpyxF+YV4UdggoCdAZtgczbO2e1hEUkRkRScM9laQIt82yUVKL6ISAsRGS8iG0QkGdiF8zfSpKgHF5GaOOPFzi+wah7OVFf5/Zrv/nb/1/pFPZap+OwM2ASbEJyR3BIKWbc/3/3UQtZ/jTMa3V9wxnXNxhlyMFATcBb8kC8rb4WqOqMc2klRZWIF2FRkmTiTI+a3BGfc2L3qDOpeJCJSB2f2jbtV9Qf/srM49m8k0/+14DHzqGqyiGzHGax7Zr5VPXCKuTF57L+tqcg2AaeJM215Xf+MHh/jNB18KSK9/QPs9xKRkfl7QhTiAM4Ei3eISEsR6Q28g3MWnGs3kA5cLCINRCT6BPv6J/CQiAwUkVNFZDjQE+eDQ2PyWAE2Fdm7ODNmLML5gKy7qqYBvYCNOANkrwHG4rQBHzjRjlTVB9yAMyj8SuAt4EmcLmq522TjTE1+O06b7YlmP3gTpwi/7N/X1cAAVV3u8vs0QcquhDPGGI/YGbAxxnjECrAxxnjECrAxxnjECrAxxnjECrAxxnjECrAxxnjECrAxxnjECrAxxnjECrAxxnjk/wExALKYfGMe7AAAAABJRU5ErkJggg==\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "tags": [], + "needs_background": "light" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/eq_gan/variational_qram.ipynb b/eq_gan/variational_qram.ipynb new file mode 100644 index 000000000..970866314 --- /dev/null +++ b/eq_gan/variational_qram.ipynb @@ -0,0 +1,1123 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "name": "variational_qram.ipynb", + "provenance": [], + "collapsed_sections": [] + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python" + } + }, + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "f0_gMI4Blbe8" + }, + "source": [ + "##### Copyright 2021 The TensorFlow Quantum Authors." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "bogCr-sSkXM1" + }, + "source": [ + "#@title Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fG6bs-jI_Ssm" + }, + "source": [ + "# Variational quantum memory for training quantum neural networks\n", + "\n", + "Author : Alexander Zlokapa\n", + "\n", + "Created : 2021-Jun-23\n", + "\n", + "Last updated : 2021-Jun-23" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vwkA6M5wSfK1" + }, + "source": [ + "In this tutorial, we use a shallow circuit prepared by an entangling quantum generative adversarial network ([EQ-GAN](https://arxiv.org/abs/2105.00080)) that loads an entire classical dataset in superposition. Although the dataset representation is *approximate* and does not correspond to an exact superposition, it can be used effectively in quantum machine leanring. In particular, we show how the variational quantum random access memory (QRAM) is a useful primitive to speed up training of a quantum neural network ([QNN](https://arxiv.org/pdf/1802.06002.pdf)) on a classification task." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yKEXl1uVBXkv" + }, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "FC3jVL4EBZcH" + }, + "source": [ + "Install TensorFlow and TensorFlow Quantum." + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "juImyZKkkCX4", + "outputId": "c3059c97-22d7-44e0-88bd-8394cee15726" + }, + "source": [ + "!pip install -q tensorflow==2.4.1\n", + "!pip install -q tensorflow-quantum" + ], + "execution_count": null, + "outputs": [ + { + "output_type": "stream", + "text": [ + "\u001b[K |████████████████████████████████| 394.3MB 35kB/s \n", + "\u001b[K |████████████████████████████████| 3.8MB 20.6MB/s \n", + "\u001b[K |████████████████████████████████| 471kB 41.0MB/s \n", + "\u001b[K |████████████████████████████████| 2.9MB 18.7MB/s \n", + "\u001b[K |████████████████████████████████| 7.8MB 5.4MB/s \n", + "\u001b[K |████████████████████████████████| 1.3MB 30.4MB/s \n", + "\u001b[K |████████████████████████████████| 5.6MB 18.8MB/s \n", + "\u001b[K |████████████████████████████████| 102kB 10.4MB/s \n", + "\u001b[K |████████████████████████████████| 92kB 9.8MB/s \n", + "\u001b[K |████████████████████████████████| 92kB 9.5MB/s \n", + "\u001b[K |████████████████████████████████| 389kB 50.0MB/s \n", + "\u001b[K |████████████████████████████████| 1.5MB 35.9MB/s \n", + "\u001b[?25h" + ], + "name": "stdout" + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pCJCjT0tBeL5" + }, + "source": [ + "Import the modules required for preparing the data, quantum neural network, and visualization." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "cm1bEEIHkIai" + }, + "source": [ + "import tensorflow as tf\n", + "import tensorflow_quantum as tfq\n", + "\n", + "import cirq\n", + "import sympy\n", + "import numpy as np\n", + "\n", + "# visualization tools\n", + "%matplotlib inline\n", + "import matplotlib.pyplot as plt\n", + "from cirq.contrib.svg import SVGCircuit\n", + "\n", + "import collections\n", + "import itertools" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MmzxciZHCAKz" + }, + "source": [ + "Since TensorFlow Quantum is compatible with a hardware backend, we show how a physical device on Quantum Engine can replace the simulated backend." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "DKRSR2KPkZwW" + }, + "source": [ + "hardware_backend = False\n", + "\n", + "if hardware_backend:\n", + " project_id = 'PROJECT_ID'\n", + " engine = cirq.google.Engine(project_id=project_id)\n", + " testsamplerxmon_rainbow = engine.sampler(processor_id=['PROCESSOR_ID'], gate_set=cirq.google.XMON)\n", + " backend = testsamplerxmon_rainbow\n", + "else:\n", + " backend = None" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nZgfZSN_B60e" + }, + "source": [ + "## 1 Load variational QRAM\n", + "\n", + "### 1.1 Prepare classical dataset\n", + "We'll prepare a classification dataset with two Gaussian-distributed peaks to demonstrate the variational QRAM. First, the classical dataset is generated." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "hu_aLCkMkS3V" + }, + "source": [ + "# create 2-peak dataset\n", + "def create_data(seed, n, dataset_size=100):\n", + " np.random.seed(seed)\n", + " # sample data from Gaussian\n", + " data0_raw = np.random.normal(2**(n-1), scale=2, size=dataset_size)\n", + " bins = np.arange(2**n + 1).astype(np.float64)\n", + " bins[-1] = np.inf\n", + " counts0, _ = np.histogram(data0_raw, bins=bins)\n", + " data0 = np.clip(np.floor(data0_raw), 0, 2**n - 1)\n", + "\n", + " data1_raw = np.random.normal(2**(n-2), scale=1, size=dataset_size)\n", + " counts1, _ = np.histogram(data1_raw, bins=bins)\n", + " data1 = np.clip(np.floor(data1_raw), 0, 2**n - 1)\n", + " \n", + " return data0, data1" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yCTAO73dFf7s" + }, + "source": [ + "Let's prepare a dataset of 120 examples over the numbers $\\{0, 1, \\dots, 15\\}$ and visualize the two Gaussian distributions." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "66nm1nmHkk0b" + }, + "source": [ + "# create circuits from dataset (for sampling)\n", + "size = 120\n", + "n = 4 # number of qubits\n", + "data0, data1 = create_data(0, n, dataset_size=size)" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 230 + }, + "id": "4TR1qnyUmHF0", + "outputId": "b6fdb5ed-dcec-4a7e-c343-1930ec18a07e" + }, + "source": [ + "bins = np.arange(2**n + 1).astype(np.float64)\n", + "bins[-1] = np.inf\n", + "probs0, _ = np.histogram(data0, bins=bins)\n", + "probs1, _ = np.histogram(data1, bins=bins)\n", + "\n", + "print('Classical dataset probabilities')\n", + "plt.figure(figsize=(3.2, 2.8))\n", + "plt.scatter(bins[:-1], probs0, label='Class 0')\n", + "plt.scatter(bins[:-1], probs1, label='Class 1')\n", + "plt.legend()\n", + "plt.ylabel('Count')\n", + "plt.tight_layout()\n", + "plt.show()" + ], + "execution_count": null, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Classical dataset probabilities\n" + ], + "name": "stdout" + }, + { + "output_type": "display_data", + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAN8AAADCCAYAAADJsRdpAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjIsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+WH4yJAAATwElEQVR4nO3df3BW1ZnA8e9DCE3GYkL5JSRhg+CwlgAhhHZbK0NhFVdXGlvFH+0uTEXsuFprdzPC6rCs4w502JXWnZ0qrRXs1GpkFbHtyiJqcdvdNj+AQFUU3TBJRAJpk+5uA4T47B/3BhJ4k9yb5L7nve/7fGac933Pe+6955L38Z5z7jn3iKpijEm+Ea4LYEymsuAzxhELPmMcseAzxhELPmMcseAzxpGRrgsQxLhx47S4uNh1MYwJrba29oSqjk/0XSyCr7i4mJqaGtfFMCY0ETnS13dW7TTGEQs+Yxyx4EuG+irYVALr8r3X+irXJTIpIBZtvlirr4KXvg6dHd7n9kbvM8DsZe7KNQidnZ00NTVx8uRJ10VJOTk5ORQWFpKdnR14Gwu+qO1+6Fzgdevs8NJjFnxNTU2MHj2a4uJiRMR1cVKGqtLa2kpTUxNTp04NvJ1VO6PW3hQuPYWdPHmSsWPHWuCdR0QYO3Zs6BqBBd9QBGnL5RUm3rav9BRngZfYYP5dLPgGq7st194I6Lm23PkBuHgtZOf2TsvO9dJNaB9++CG33HIL06ZNY968eVx77bW88847NDQ0UFJSEskxT506xc0338z06dP59Kc/TUNDw7Ds14JvsPpry/U0exlc/yjkFQHivV7/aOzae6lAVbnhhhtYuHAh7733HrW1taxfv55jx45FetwnnniCMWPGcPjwYe677z7uv//+YdmvBd9ghWnLzV4G9x2EdW3ea4YE3va9zVyx4VWmrv4pV2x4le17m4e0v9dee43s7Gy+9rWvnU2bM2cOV155Za98DQ0NXHnllZSVlVFWVsYvf/lLAI4ePcqCBQsoLS2lpKSEN954g66uLlasWEFJSQmzZs1i06ZNFxz3xRdfZPny5QDceOON7N69m+F4AoT1dg5WXqFf5UyQbti+t5k1zx+go7MLgOa2DtY8fwCAirkFg9rnwYMHmTdv3oD5JkyYwK5du8jJyeHdd9/l1ltvpaamhqeffpolS5bwwAMP0NXVxR/+8Af27dtHc3MzBw8eBKCtre2C/TU3N1NUVATAyJEjycvLo7W1lXHjxg3qPLrZlW+wrC3Xr407D50NvG4dnV1s3Hko8mN3dnZyxx13MGvWLG666SbefPNNAObPn8+TTz7JunXrOHDgAKNHj+bSSy/l/fff55577uHll1/m4osvjrx83SIPPhHJEpG9IvIT//NUEfmViBwWkWdFZFTUZYiEteX69UFbR6j0IGbOnEltbe2A+TZt2sTEiRPZv38/NTU1nD59GoAFCxawZ88eCgoKWLFiBU899RRjxoxh//79LFy4kMcee4yVK1desL+CggIaG71azpkzZ2hvb2fs2LGDPo9uybjy3Qu81ePzt4BNqjod+B1wexLKEI0MbcsFMTk/N1R6EIsWLeLUqVNs3rz5bFp9fT1vvPFGr3zt7e1MmjSJESNG8MMf/pCuLu8KfOTIESZOnMgdd9zBypUrqaur48SJE3z00Ud86Utf4uGHH6auru6C4y5dupStW7cCsG3bNhYtWjQst1wiDT4RKQSuA77vfxZgEbDNz7IVqIiyDMaNyiUzyM3O6pWWm51F5ZIZg96niPDCCy/wyiuvMG3aNGbOnMmaNWu45JJLeuW766672Lp1K3PmzOHtt9/moosuAuD1119nzpw5zJ07l2effZZ7772X5uZmFi5cSGlpKV/5yldYv379Bce9/fbbaW1tZfr06TzyyCNs2LBh0OfQ63yifG6niGwD1gOjgb8BVgD/5V/1EJEi4N9Utd8bNOXl5Wrz+dx76623uPzyywPn3763mY07D/FBWweT83OpXDJj0J0tcZDo30dEalW1PFH+yHo7ReTPgRZVrRWRhYPYfhWwCmDKlCnDXDqTDBVzC9I62IYqymrnFcBSEWkAnsGrbn4HyBeR7qAvBBLe/FHVzaparqrl48cnnIVvTKxFFnyqukZVC1W1GLgFeFVVvwy8BtzoZ1sOvBhVGYxJZS7u890PfFNEDgNjgScclMEY55IywkVVXwde99+/D3wqGcc1JpXZCBdjHLHgM7HiYkrRnj17KCsrY+TIkWzbtm3gDQKy4DOx4WpK0ZQpU9iyZQu33XbbsO7Xgs9EZ5if2uZqSlFxcTGzZ89mxIjhDRebUmSiEcFT21xNKYqKBZ+JhsOntnV2dnL33Xezb98+srKyeOeddwBvStFXv/pVOjs7qaiooLS0tNeUouuuu46rr7460rL1ZNVOE40IntrmakpRVCz4TDQieGqbqylFUbHgM9GIYKa/qylF1dXVFBYW8txzz3HnnXcyc+bMQZ9Dr/OJckrRcLEpRakh7JQi6qu8Nl57k3fFW7w2rSccp8yUImOYvSytg22orNppjCMWfMY4YsFnQolDH4ELg/l3seAzgeXk5NDa2moBeJ7uJcJycnJCbWcdLiawwsJCmpqaOH78uOuipJzuxTHDsOAzgWVnZ4da/NH0z6qdxjhiwWeMIxZ8xjhiwWeMIxZ8xjhiwWeMIxZ8xjhiwWeMI5EFn4jkiMivRWS/iPxGRP7eT0+PlWmNGaIor3yngEWqOgcoBa4RkT8hnVamNWYIolylSFX1f/2P2f5/iq1MawwQ/bLQWSKyD2gBdgHvAW2qesbP0gTY6okmI0UafKrapaqleItgfgr446DbisgqEakRkRobRW/SUVJ6O1W1DW9RzM9gK9MaA0Tb2zleRPL997nAVcBb2Mq0xgDRzuebBGwVkSy8IK9S1Z+IyJvAMyLyMLAXW5nWZKjIgk9V64G5CdJtZVpjsBEuxjhjwWeMI/YMF9PL9r3NbNx5iA/aOpicn0vlkhlUzLVbsVGw4DNnbd/bzJrnD9DR6a3q09zWwZrnDwBYAEbAqp3mrI07D50NvG4dnV1s3HnIUYnSmwWfOeuDto5Q6WZoLPjMWZPzc0Olm6Gx4DNnVS6ZQW52Vq+03OwsKpfMcFSi9GYdLuas7k4V6+1MDgs+00vF3AILtiSxaqcxjgQKPhG5IkiaMSa4oFe+fw6YZowJqN82n4h8BvgsMF5Evtnjq4uBrMRbGWOCGKjDZRTwcT/f6B7pv+fchFhjzCD0G3yq+nPg5yKyRVWPJKlMxmSEoLcaPiYim4Hintuo6qIoCmVMJggafM8BjwHfB7oGyGuMCSBo8J1R1e9GWhJjMkzQWw0vichdIjJJRD7R/V+kJTMmzQW98i33Xyt7pClw6fAWx5jMESj4VHVq1AUxJtMECj4R+ctE6ar61PAWx5jMEbTaOb/H+xxgMVAHWPAZM0hBq5339PzsPwb+mUhKZEyGGOyUov8DrB1ozBAEbfO9hNe7Cd6A6suBqgG2KcKrlk70t92sqt/xb1E8izdapgFYpqq/G0zhjYmzoG2+f+zx/gxwRFWbBtjmDPDXqlonIqOBWhHZBawAdqvqBhFZDawG7g9ZbmNiL1C10x9g/TbezIYxwOkA2xxV1Tr//f/gLQ9WAHwBbzlosGWhTQYLOpN9GfBr4CZgGfArEQk8pUhEivFWLPoVMFFVj/pffYhXLU20ja1Ma9Ja0GrnA8B8VW0Bb+FL4BVg20AbisjHgX8FvqGqvxeRs9+pqoqIJtpOVTcDmwHKy8sT5olEfRXsfgjamyCvEBavhdnLknb4KNj6C6kpaPCN6A48XysBrpoiko0XeD9S1ef95GMiMklVj4rIJKCl7z0kWX0VvPR16PSf0Nze6H2G2Aagrb+QuoLeanhZRHaKyAoRWQH8FPhZfxuId4l7AnhLVR/p8dUOzo0VTa1loXc/dC7wunV2eOkxZesvpK6BnuEyHa+NVikiXwQ+53/1n8CPBtj3FcBfAAdEZJ+f9rfABqBKRG4HjuC1IVNDex8duH2lx4Ctv5C6Bqp2fhtYA+BXG58HEJFZ/nfX97Whqv4HIH18vTh0SZMhr9CraiZKj6nJ+bk0Jwg0W3/BvYGqnRNV9cD5iX5acSQlcmnxWsg+70eZneulx5Stv5C6Brry5ffzXfr9r7O7UyWNejtt/YXUJap99+KLyI+BV1X1e+elrwSuUtWbIy4f4N1qqKmpScahjBlWIlKrquWJvhvoyvcN4AUR+TJQ66eV4z3P84bhK6IxmWeg53YeAz4rIp8HSvzkn6rqq5GXzJg0F3Q+32vAaxGXxYTgetSK6+OnA1ufL4Zcj1pxffx0YevzxZDrUSuuj58uLPhiyPWoFdfHTxcWfDHU1+iUZI1acX38dGHBF0OuR624Pn66sA6XGHI9asX18dNFvyNcUoWNcDFx1d8IF6t2GuOIBZ8xjljwGeOIBZ8xjljwGeOIBZ8xjljwGeOIBZ8xjljwGeOIDS9LMdU7HqeobiMT9DgtMp7GskrmL73TdbGGJMzE20yapGvBl0KqdzxOSe2D5MppELiE4+TVPkg1xDYAw0y8zbRJulbtTCFFdRu9wOshV05TVLfRUYmGLszE20ybpBtZ8InID0SkRUQO9kj7hIjsEpF3/dcxUR0/jiZo4qXQJuiJJJdk+ISZeJtpk3SjvPJtAa45L2013qq0lwG7/c/G1yLj+0gfl+SSDJ8wE28zbZJuZMGnqnuA356XbKvS9qOxrJIOHdUrrUNH0VhW6ahEQxdm4m2mTdJNdodLoFVpM9X8pXdSDX5v5wlaZByN8+Ld2xlm4m2mTdKNdDKtvxz0T1S1xP/cpqr5Pb7/naombPeJyCpgFcCUKVPmHTlyJLJyGhOVVJpMe8xfjZaBVqVV1c2qWq6q5ePHJ24LGRNnyQ6+1F2V1pgki/JWw4/xVrCdISJN/kq0G4CrRORd4E/9z8ZkpMg6XFT11j6+Ss1VaY1JMhvhYowjFnzGOGLBZ4wjFnzGOGLBZ4wjFnzGOGKTaZMgk2Znm+As+CKWabOzTXBW7YxYps3ONsFZ8EUs02Znm+As+CKWabOzTXAWfBHLtNnZJjjrcIlYps3ONsFZ8CVBxdwCCzZzAat2GuOIBZ8xjljwxVV9FWwqgXX53mt9lesSmZCszRdH9VXw0teh079X2N7ofQaYvcxduUwoduWLo90PnQu8bp0dXrqJjcy48tVXeT/M9ibIK4TFa+N9hWhvCpeehtJhsHr6B186VtHyCr3zSJSeAdJlsHr6VzvTsYq2eC1knzc8LTvXS88A6TJYPf2DLx2raLOXwfWPQl4RIN7r9Y/G90oeUroMVk//amfIKlps2hKzl2VMsJ1vcn4uzQkCLW6D1dP/yheiitbdlmhu60A515bYvrc5OWU1gaTLYPX0D74QVbR0aUuku4q5Baz/4iwK8nMRoCA/l/VfnJWaNZR+OKl2isg1wHeALOD7qjqoNRuqdzzur2V3nBYZT2NZ4rXstnddwcZTj/LByQ4m5+RS2TUj4aqcYdsSsamihrnVEjRvFPsMkbci6xdUfOwhyGmCjxVC1log8T6D/k6C5gubty9JDz4RyQL+BbgKaAKqRWSHqr4ZZj/VOx6npPZBcuU0CFzCcfJqH6Qaev0jhOmWDtOWiE13d5hbLUHzRrHPiI4f9HcSNF/YvP1xUe38FHBYVd9X1dPAM3jLRYdSVLfRO/kecuU0RXUbe6WFqUqGaUvEpooa5lZL0LxR7DOi4wf9nQTNFzZvf1wEXwHQs/uxyU/rRURWiUiNiNQcP378gp1M0AvTvPQTvT6HqUqGaUvEprs7zK2WoHmj2GdExw/6OwmaL2ze/qRsh8tAK9O2SOLValtkXK/PYZ+hUjG3gF+sXsR/b7iOX6xe1GcVMjbPZulr1Eui9KB5o9hnRMcP+jsJmi9s3v64CL5moKjH50I/LZTGsko6dFSvtA4dRWNZZa+0qLqlY9PdHWY0TNC8UewzouMH/Z0EzRc2b39c9HZWA5eJyFS8oLsFuC3sTuYvvZNq8HucTtAi42icd2GPU1TPUInNs1m6OyCC9DYGzRvFPiM6ftDfSdB8YfP2R1Q11AbDQUSuBb6Nd6vhB6r6D/3lLy8v15qamqSUzZjhJCK1qlqe6Dsn9/lU9WfAz1wc25hUkbIdLsakOws+Yxxx0uYLS0SOA0f6yTIOCHeTJfXZOcXDQOf0R6qa8N5ELIJvICJS01ejNq7snOJhKOdk1U5jHLHgM8aRdAm+za4LEAE7p3gY9DmlRZvPmDhKlyufMbET6+ATkWtE5JCIHBaR1a7LMxxEpEFEDojIPhGJ7Zg6EfmBiLSIyMEeaZ8QkV0i8q7/OsZlGcPq45zWiUiz//fa5w+dDCS2wddjRvyfAZ8EbhWRT7ot1bD5vKqWxrxbfgtwzXlpq4HdqnoZsNv/HCdbuPCcADb5f69Sf+hkILENPoZpRryJhqruAX57XvIXgK3++62Q8FE6KauPcxq0OAdfoBnxMaTAv4tIrYiscl2YYTZRVY/67z8EJroszDC6W0Tq/Wpp4Kp0nIMvXX1OVcvwqtN/JSILXBcoCup1s6dDV/t3gWlAKXAU+KegG8Y5+IZlRnyqUdVm/7UFeAGvep0ujonIJAD/tcVxeYZMVY+papeqfgR8jxB/rzgH39kZ8SIyCm9G/A7HZRoSEblIREZ3vweuBg72v1Ws7ACW+++XAy86LMuw6P6fie8GQvy9YrtWg6qeEZG7gZ2cmxH/G8fFGqqJwAsiAt7f5mlVfdltkQZHRH4MLATGiUgT8HfABqBKRG7Hm6USq8Um+jinhSJSileFbgACP0vCRrgY40icq53GxJoFnzGOWPAZ44gFnzGOWPAZ44gFnzGOWPAZ44gFnzGO/D9pCY7nYCoTqwAAAABJRU5ErkJggg==\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "tags": [], + "needs_background": "light" + } + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7qEWAzZzDQOM" + }, + "source": [ + "### 1.2 Prepare approximate QRAM of classical dataset\n", + "\n", + "To prepare two quantum states in QRAM — one that represents Class 0 in superposition, and another that represents Class 1 in superposition — we design a shallow variational circuit that can approximate the Gaussian peaks. Qubits are selected for the circuit based on the Rainbow chip hardware. Here, we hard-code the choice of $n=4$ qubits as an example of how to run the generator on physical hardware." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "8pJJsTn5kfwX" + }, + "source": [ + "# get qubits for a rainbow chip\n", + "def get_exp_qubits(n, class_type=-1):\n", + " # we hard-wire choice of qubits for n = 4 on quantum device\n", + " if class_type == 0:\n", + " return [cirq.GridQubit(2, 4), cirq.GridQubit(1, 4), cirq.GridQubit(2, 3), cirq.GridQubit(2, 5), cirq.GridQubit(3, 4)]\n", + " elif class_type == 1:\n", + " return [cirq.GridQubit(1, 4), cirq.GridQubit(2, 4), cirq.GridQubit(2, 3), cirq.GridQubit(2, 5), cirq.GridQubit(3, 4)]\n", + " else:\n", + " return [cirq.GridQubit(3, 4), cirq.GridQubit(1, 4), cirq.GridQubit(2, 3), cirq.GridQubit(2, 5), cirq.GridQubit(2, 4)]" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fC-kKtiKEWqD" + }, + "source": [ + "As a variational ansatz for generating the two-peak dataset, we use the concatenated double-exponential peaks proposed by [Klco and Savage](https://journals.aps.org/pra/abstract/10.1103/PhysRevA.102.012612). Although the resulting distribution represented by the quantum circuit is not Gaussian, the shallow depth of the circuit ensures fewer errors due to noise. In the era of noisy intermediate-scale quantum (NISQ) devices, the error of representing the approximate distribution with a shallow circuit is likely smaller than the error introduced by representing the exact distribution with a deep circuit. Hence, the EQ-GAN can generate a variational QRAM useful for near-term quantum machine learning applications." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "GMYrxM9dkWew" + }, + "source": [ + "# EQ-GAN generator for double exponential peaks\n", + "def build_qnn(qubits, model_type):\n", + " n = len(qubits)\n", + " u = []\n", + " angles = []\n", + " if model_type == 0:\n", + " center = 0\n", + " j = 0\n", + " for i in range(n):\n", + " if i == center:\n", + " u.extend([cirq.Y(qubits[i])**0.5, cirq.X(qubits[i])])\n", + " else:\n", + " theta = sympy.Symbol('t' + str(i))\n", + " angles.append(theta)\n", + " u.append(cirq.ry(2*theta).on(qubits[i]))\n", + " j += 1\n", + " for i in range(n):\n", + " if i != center:\n", + " u.extend([cirq.Y(qubits[i])**0.5, cirq.X(qubits[i]),\n", + " cirq.CZ(qubits[center], qubits[i]),\n", + " cirq.Y(qubits[i])**0.5, cirq.X(qubits[i])])\n", + " circuit = cirq.Circuit(u)\n", + " elif model_type == 1:\n", + " j = 0\n", + " center = 1\n", + " u.append(cirq.I.on(qubits[0]))\n", + " for i in range(1, n):\n", + " if i == center:\n", + " u.extend([cirq.Y(qubits[i])**0.5, cirq.X(qubits[i])])\n", + " else:\n", + " theta = sympy.Symbol('t' + str(i))\n", + " angles.append(theta)\n", + " u.append(cirq.ry(2*theta).on(qubits[i]))\n", + " j += 1\n", + " for i in range(1, n):\n", + " if i != center:\n", + " u.extend([cirq.Y(qubits[i])**0.5, cirq.X(qubits[i]),\n", + " cirq.CZ(qubits[center], qubits[i]),\n", + " cirq.Y(qubits[i])**0.5, cirq.X(qubits[i])])\n", + " circuit = cirq.Circuit(u)\n", + " return circuit, angles" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fJLA7_JQCX4a" + }, + "source": [ + "For simplicity, we will use the parameters learned by a previously trained EQ-GAN. For a tutorial on how to train the EQ-GAN, see \"Learning to supress noise with an entangling quantum generative adversarial network (EQ-GAN).\" To keep everything hardware-friendly, the swap test is also decomposed into the native gate basis." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "Vqk7Z209kjEG" + }, + "source": [ + "# do a swap gate with CZ between q0 and q1\n", + "def compiled_swap(q0, q1):\n", + " u = []\n", + " u.extend([cirq.X(q0)**0.5])\n", + " u.extend([cirq.Z(q1)**-0.5, cirq.X(q1)**0.5, cirq.Z(q1)**0.5])\n", + " u.append(cirq.CZ(q0, q1))\n", + " u.extend([cirq.Z(q0)**-1, cirq.X(q0)**0.5, cirq.Z(q0)**1])\n", + " u.extend([cirq.Z(q1)**-1.5, cirq.X(q1)**0.5, cirq.Z(q1)**1.5])\n", + " u.append(cirq.CZ(q0, q1))\n", + " u.extend([cirq.X(q0)**0.5])\n", + " u.extend([cirq.Z(q1)**-0.5, cirq.X(q1)**0.5, cirq.Z(q1)**0.5])\n", + " u.append(cirq.CZ(q0, q1))\n", + " u.extend([cirq.Z(q0)**-0.5])\n", + " u.extend([cirq.Z(q1)**0.5])\n", + " return cirq.Circuit(u)\n", + "\n", + "# get a learned circuit for a given dataset\n", + "def get_model(n, class_type):\n", + " # pre-trained weights from EQ-GAN on exactly the same training set\n", + " # QRAM is trained from 60 examples (half of the size = 120)\n", + " all_weights = [[1.3459893, 1.0012823, 0.94282967], [4.7395287, 0.96802247]]\n", + "\n", + " qubits = get_exp_qubits(n, class_type)\n", + " qnn, symbols = build_qnn(qubits[:-1], class_type)\n", + " resolver = {}\n", + " for i in range(len(symbols)):\n", + " resolver[symbols[i]] = all_weights[class_type][i]\n", + " resolved_qnn = cirq.resolve_parameters(qnn, resolver)\n", + "\n", + " all_qubits = get_exp_qubits(n)\n", + " resolved_qnn += compiled_swap(all_qubits[0], all_qubits[-1])\n", + " return resolved_qnn" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Z-W1-eELFvjh" + }, + "source": [ + "We can view the variational circuits that represent a superposition over data within a given peak (Class 0 or Class 1). These are much shallower than the exponential-length circuits required to store an exact superposition of the dataset's bitstrings! Similarly, the quantum circuit required to prepare a Gaussian distribution (instead of the double-peak ansatz of Klco and Savage) requires an exponential number of $CZ$ gates when decomposed onto a planar architecture in the native gate basis. Hence, these shallow circuit representations provide very efficient representations of the dataset." + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 309 + }, + "id": "KwnQH8Qcmx6D", + "outputId": "a0d644b6-c9fa-4b12-e9aa-c75a8cdd2ce2" + }, + "source": [ + "SVGCircuit(get_model(n, 0))" + ], + "execution_count": null, + "outputs": [ + { + "output_type": "stream", + "text": [ + "findfont: Font family ['Arial'] not found. Falling back to DejaVu Sans.\n" + ], + "name": "stderr" + }, + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "" + ], + "image/svg+xml": "(1, 4): (2, 3): (2, 4): (2, 5): (3, 4): Y^0.5Ry(0.857π)Ry(0.637π)Ry(0.6π)XY^0.5Y^0.5Y^0.5XXXY^0.5XY^0.5XY^0.5XX^0.5S^-1X^0.5SZSX^0.5X^0.5ZS^-1X^0.5S^-1X^0.5SS^-1S" + }, + "metadata": { + "tags": [] + }, + "execution_count": 10 + } + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 290 + }, + "id": "hfjRhV9XnNjV", + "outputId": "d9bab111-d4a6-4da4-c14c-e39268145ba8" + }, + "source": [ + "SVGCircuit(get_model(n, 1))" + ], + "execution_count": null, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "" + ], + "image/svg+xml": "(1, 4): (2, 3): (2, 4): (2, 5): (3, 4): IY^0.5Ry(-0.983π)Ry(0.616π)XY^0.5Y^0.5XXY^0.5XY^0.5XX^0.5S^-1X^0.5SZSX^0.5X^0.5ZS^-1X^0.5S^-1X^0.5SS^-1S" + }, + "metadata": { + "tags": [] + }, + "execution_count": 11 + } + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 230 + }, + "id": "MNjURGY4nV2k", + "outputId": "f9ed07d7-7f4f-451c-fffd-993ea4b8cfe4" + }, + "source": [ + "simulator = cirq.Simulator()\n", + "qubit_order_0 = [cirq.GridQubit(2, 4), cirq.GridQubit(3, 4), cirq.GridQubit(1, 4), cirq.GridQubit(2, 3), cirq.GridQubit(2, 5)]\n", + "result = simulator.simulate(get_model(n, 0), qubit_order=qubit_order_0).final_state_vector\n", + "probs_class_0 = np.abs(result)**2\n", + "\n", + "qubit_order_1 = [cirq.GridQubit(2, 4), cirq.GridQubit(1, 4), cirq.GridQubit(3, 4), cirq.GridQubit(2, 3), cirq.GridQubit(2, 5)]\n", + "result = simulator.simulate(get_model(n, 1), qubit_order=qubit_order_1).final_state_vector\n", + "probs_class_1 = np.abs(result)**2\n", + "\n", + "print('Variational QRAM')\n", + "plt.figure(figsize=(3.2, 2.8))\n", + "plt.scatter(np.arange(2**n), probs_class_0[:2**n], label='Class 0')\n", + "plt.scatter(np.arange(2**n), probs_class_1[:2**n], label='Class 1')\n", + "plt.ylabel('PDF')\n", + "plt.legend()\n", + "plt.tight_layout()\n", + "plt.show()" + ], + "execution_count": null, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Variational QRAM\n" + ], + "name": "stdout" + }, + { + "output_type": "display_data", + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAOUAAADDCAYAAAB9PNY9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjIsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+WH4yJAAAWDElEQVR4nO3dfZAU9Z3H8feXZXHXB54JCgsuT6HkcRfX849EjsOTRS0BlVOgtCCKyKFRkzpKKa/UIkmht7nD8i6e4aIlmOIAiSBezBFCVFArFxZYFqLhMRh3VBQQ/MMFluV7f3TPOrs7u9OzOz3d0/N9VW3NzG9+3f3rmfnu9PR0f0ZUFWNMeHQJegDGmOasKI0JGStKY0LGitKYkLGiNCZkrCiNCRlfi1JEporIfhE5JCKPJbl/oYjsFZEaEXlXREa57aUiUu+214jIC36O05gwEb++pxSRAuAAcANQB+wAZqvqBwl9uqvqV+71acAiVZ0qIqXA/6jqGK/L69u3r5aWlmZuBYzx0c6dO4+rar9k93X1cbl/AxxS1SMAIrIGmA40FWW8IF2XAB3+D1FaWkp1dXVHJzcmq0Tko7bu83PzdSDwccLtOretGRF5QEQOA/8CPJRw1xAR2S0i74jIdckWICILRKRaRKq/+OKLTI7dmMAEvqNHVX+mqsOAR4F/dps/BQarajnwQ2C1iHRPMu0KVa1Q1Yp+/ZJuCRiTc/wsyhgwKOF2idvWljXADABVPauqJ9zrO4HDwLd9Gqe/atfB8jHwVE/nsnZd5/qZyPPzM+UOYISIDMEpxlnAnMQOIjJCVQ+6N28GDrrt/YCTqtooIkOBEcARH8fqj9p18MZD0FDv3D79sXMbYNwd6fcLqYaGBurq6jhz5kzQQwmdoqIiSkpKKCws9DyNb0WpqudF5EFgM1AAvKSqfxKRpUC1qm4CHhSRvwcagC+Bue7kE4GlItIAXAAWqupJv8bqm61Lvym0uIZ6pz2x2Lz2C6m6ujouu+wySktLEZGghxMaqsqJEyeoq6tjyJAhnqfz850SVX0TeLNF2xMJ1x9uY7pfAb/yc2xZcbrOW7vXfiF15swZK8gkRIQ+ffqQ7k7IwHf0RFqPEm/tXvuFmBVkch15XKwo/XT9E1BY3LytsNhp70g/kxesKP007g645TnoMQgQ5/KW51p/TvTaz7Tps88+Y9asWQwbNoyrr76am266iQMHDnD06FHGjPF8YFhazp49y5133snw4cO59tprOXr0aEbm6+tnSoNTWF6Ky2u/CNi4O0bV5v18cqqeAT2LWVw5khnlrY4r8UxVufXWW5k7dy5r1qwBYM+ePRw7doxBgwalmLrjXnzxRXr16sWhQ4dYs2YNjz76KGvXru30fO2d0mTVxt0xlry2l9ipehSInapnyWt72bi7va+w2/fWW29RWFjIwoULm9rGjx/Pddc1PxDs6NGjXHfddUyYMIEJEybw/vvvA/Dpp58yceJEysrKGDNmDNu3b6exsZF58+YxZswYxo4dy/Lly1st9/XXX2fuXOcLg5kzZ7J161YycSy5vVOarKravJ/6hsZmbfUNjVRt3t/hd8t9+/Zx9dVXp+z3rW99iy1btlBUVMTBgweZPXs21dXVrF69msrKSh5//HEaGxv5+uuvqampIRaLsW/fPgBOnTrVan6xWKzpnbhr16706NGDEydO0Ldv3w6tR5wVpcmqT07Vp9WeSQ0NDTz44IPU1NRQUFDAgQMHALjmmmu45557aGhoYMaMGZSVlTF06FCOHDnC97//fW6++WamTJni+/jibPPVZNWAnsVptXsxevRodu7cmbLf8uXL6d+/P3v27KG6uppz584BMHHiRLZt28bAgQOZN28eq1atolevXuzZs4dJkybxwgsvMH/+/FbzGzhwIB9/7Jxzcf78eU6fPk2fPn06vB5xVpQmqxZXjqS4sKBZW3FhAYsrR3Z4npMnT+bs2bOsWLGiqa22tpbt27c363f69GmuuOIKunTpwiuvvEJjo7MZ/dFHH9G/f3/uu+8+5s+fz65duzh+/DgXLlzg9ttv58c//jG7du1qtdxp06axcuVKANavX8/kyZMz8n2tbb6arIp/bszk3lcRYcOGDTzyyCM888wzFBUVUVpayrPPPtus36JFi7j99ttZtWoVU6dO5ZJLLgHg7bffpqqqisLCQi699FJWrVpFLBbje9/7HhcuXABg2bJlrZZ77733cvfddzN8+HB69+7dtOe3s3xLHsi2iooKtZOcg/Hhhx9y1VVXBT2M0Er2+IjITlWtSNbfNl+NCRkrSmNCJpRpdu59S9zp9otIpZ/jNCZMfCtKN83uZ8CNwChgdmLRuVar6lhVLcPJ6Pk3d9pROCdFjwam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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "tags": [], + "needs_background": "light" + } + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4s7Gi2gnD6iI" + }, + "source": [ + "### 1.3 Individual quantum data states\n", + "\n", + "Later on, we'll benchmark a QNN trained over the dataset in superposition vs. a QNN trained with each data example individually (as in the original QNN paper by Farhi et al.). To encode individual bitstrings as quantum states, we use a binary encoding of $X$ gates." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "wrWcqnG6lKvJ" + }, + "source": [ + "def convert_to_circuit(data, n):\n", + " values = np.ndarray.flatten(data)\n", + " qubits = get_exp_qubits(n)\n", + " circuit = cirq.Circuit()\n", + " for i, value in enumerate(values):\n", + " circuit.append(cirq.X(qubits[i])**value)\n", + " return circuit" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fJ5CP0ysH0e9" + }, + "source": [ + "We can now translate our classical dataset into an array of quantum states, completing the dataset needed for the QNN benchmark." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "LNQQ3MKiH1pR" + }, + "source": [ + "# helper function to replace np.unpackbits with a custom bitstring length\n", + "def unpackbits(x, num_bits):\n", + " xshape = list(x.shape)\n", + " x = x.reshape([-1, 1])\n", + " mask = 2**np.arange(num_bits).reshape([1, num_bits])\n", + " return np.flip((x & mask).astype(bool).astype(int).reshape(xshape + [num_bits]), axis=1)\n", + "\n", + "all_data = np.array([unpackbits(data0.astype(np.int64), n), unpackbits(data1.astype(np.int64), n)])\n", + "\n", + "x_circ = [convert_to_circuit(x, n) for x in all_data[0]] + [convert_to_circuit(x, n) for x in all_data[1]]\n", + "y = np.array([0]*len(all_data[0]) + [1]*len(all_data[1]))" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6haSW3j3IEpN" + }, + "source": [ + "## 2 Defining the QNN classifier\n", + "\n", + "To evaluate the effectiveness of the variational QRAM, we train the QNN in two ways:\n", + "* Read each class of the dataset in superposition, allowing the QNN to classify *all* data examples of that class at once. Compute the loss over the entire class.\n", + "* Read each data example as individual quantum states, requiring the QNN to classify each example one by one. This corresponds to the original QNN proposed by Farhi et al.\n", + "\n", + "Each QNN will be trained with an equal number of evaluations on the quantum device, and then we will compare the performance. First, a variational circuit is defined for the QNN to classify between two peaks. A hardware-efficient ansatz is used given the native gate basis." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "EVmSUOKllSmT" + }, + "source": [ + "# define the QNN classifier\n", + "class ClassifierCircuitLayerBuilder():\n", + " def __init__(self, data_qubits, readouts):\n", + " self.data_qubits = data_qubits\n", + " self.readouts = readouts\n", + " \n", + " def add_layer(self, circuit, prefix):\n", + " for j, readout in enumerate(self.readouts):\n", + " for i, qubit in enumerate(self.data_qubits):\n", + " symbol = sympy.Symbol(prefix + '-' + str(j) + '-' + str(i))\n", + " u = []\n", + " u.extend([cirq.Z(qubit)**-0.5, cirq.X(qubit)**0.5, cirq.Z(qubit)**0.5])\n", + " u.append(cirq.CZ(qubit, readout))\n", + " u.extend([cirq.Z(qubit)**-1, cirq.X(qubit)**symbol, cirq.Z(qubit)**1])\n", + " u.append(cirq.CZ(qubit, readout))\n", + " u.extend([cirq.Z(qubit)**0.5, cirq.X(qubit)**0.5, cirq.Z(qubit)**-0.5])\n", + " circuit += cirq.Circuit(u)" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "moSzqbShI7LS" + }, + "source": [ + "The QNN consists of one or more layers of parameterized gates as well as a readout qubit, which is measured to determine the predicted class of input data. In general, we can allow for more than one readout qubit." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "_CS6h3LlI6M0" + }, + "source": [ + "def build_quantum_classifier(n_readouts=1):\n", + " \"\"\"Create a QNN model circuit and readout operation to go along with it.\"\"\"\n", + " readouts = []\n", + " qubits = get_exp_qubits(n)\n", + " for i in range(n_readouts):\n", + " readouts.append(qubits[-1])\n", + " circuit = cirq.Circuit()\n", + " data_qubits = qubits[:-1]\n", + " \n", + " # prepare the readout qubit\n", + " circuit.append(cirq.X.on_each(readouts))\n", + " circuit.append((cirq.Y**0.5).on_each(readouts))\n", + " circuit.append(cirq.X.on_each(readouts))\n", + " \n", + " builder = ClassifierCircuitLayerBuilder(data_qubits, readouts)\n", + "\n", + " # add layer(s)\n", + " builder.add_layer(circuit, \"layer1\")\n", + "\n", + " # prepare the readout qubit\n", + " circuit.append((cirq.Y**0.5).on_each(readouts))\n", + " circuit.append(cirq.X.on_each(readouts))\n", + "\n", + " total = cirq.Z(readouts[0])\n", + " for readout in readouts[1:]:\n", + " total += cirq.Z(readout)\n", + " return circuit, total/len(readouts)" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": { + "id": "U9vgfBsQleGI" + }, + "source": [ + "# create the QNN classifier\n", + "model_circuit, model_readout = build_quantum_classifier(1)" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 290 + }, + "id": "uBc4WaqwJjd-", + "outputId": "eabe638d-1b34-4d6e-8d61-33108761f837" + }, + "source": [ + "SVGCircuit(model_circuit)" + ], + "execution_count": null, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "" + ], + "image/svg+xml": "(1, 4): (2, 3): (2, 4): (2, 5): (3, 4): XY^0.5XS^-1X^0.5SZX^(layer1-0-0)ZSX^0.5S^-1S^-1X^0.5SZX^(layer1-0-1)ZSX^0.5S^-1S^-1X^0.5SZX^(layer1-0-2)ZSX^0.5S^-1S^-1X^0.5SZX^(layer1-0-3)ZSY^0.5X^0.5XS^-1" + }, + "metadata": { + "tags": [] + }, + "execution_count": 18 + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "V-RkOzB7JlfL" + }, + "source": [ + "## 3 Training the QNN\n", + "\n", + "### 3.1 Preparing for training\n", + "\n", + "Since the expected readout qubit is in the range `[-1, 1]`, we choose a hinge loss to train the QNN." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "B-S34J3ilfkD" + }, + "source": [ + "def hinge_accuracy(y_true, y_pred):\n", + " y_true = tf.squeeze(y_true) > 0.0\n", + " y_pred = tf.squeeze(y_pred) > 0.0\n", + " result = tf.cast(y_true == y_pred, tf.float32)\n", + "\n", + " return tf.reduce_mean(result)" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "G1DfEsAcKRbF" + }, + "source": [ + "To prepare the data, we convert our circuits into TensorFlow Quantum tensors, dividing the dataset into half for training and test data. Additionally, the class labels must be mapped to be either `-1` or `1` for the hinge loss." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "xYhQcnH_lPvw" + }, + "source": [ + "x_train_tfcirc = tfq.convert_to_tensor(x_circ[:size//2])\n", + "x_test_tfcirc = tfq.convert_to_tensor(x_circ[size//2:])\n", + "y_train = y[:size//2]\n", + "y_test = y[size//2:]\n", + "\n", + "y_train_hinge = 2.0*y_train-1.0\n", + "y_test_hinge = 2.0*y_test-1.0" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cKGVL_dHKWiA" + }, + "source": [ + "Finally, we can define a parameterized quantum circuit (`tfq.layers.PQC`) and minimize hinge loss in `tf.keras` using Adam. For the QNN that learns from individual examples in the dataset, the loss is computed over each example." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "1RlLzr_ulhgY" + }, + "source": [ + "epochs = 1\n", + "\n", + "# train non-superposition QNN classifier\n", + "def train_sample_qnn(n, averages=5, learning_rate=0.001):\n", + " sample_acc_data = []\n", + " for i in range(averages):\n", + " model = tf.keras.Sequential([\n", + " # The input is the data-circuit, encoded as a tf.string\n", + " tf.keras.layers.Input(shape=(), dtype=tf.string),\n", + " # The PQC layer returns the expected value of the readout gate, range [-1,1].\n", + " tfq.layers.PQC(model_circuit, model_readout, repetitions=10000, backend=backend),\n", + " ])\n", + " model.compile(\n", + " loss=tf.keras.losses.Hinge(),\n", + " optimizer=tf.keras.optimizers.Adam(learning_rate=learning_rate),\n", + " metrics=[hinge_accuracy])\n", + " \n", + " qnn_history_sample = model.fit(\n", + " x_train_tfcirc, y_train_hinge,\n", + " batch_size=1,\n", + " epochs=epochs,\n", + " verbose=0,\n", + " validation_data=(x_test_tfcirc, y_test_hinge))\n", + " \n", + " qnn_results_sample = model.evaluate(x_test_tfcirc, y_test)\n", + " sample_weights = model.get_weights()[0]\n", + " sample_acc_data.append(qnn_results_sample[1])\n", + " print('Trained model', i+1)\n", + " \n", + " return sample_acc_data" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fYfASSTiLNcq" + }, + "source": [ + "For the QNN that learns from the QRAM superposition, the total number of quantum hardware evaluations is kept constant, causing an adjustment to the total number of epochs. Additionally, since the QNN outputs the average label of an entire class due to the linear superposition of data examples, the loss is computed over the entire class." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "zBs3DakLlrV7" + }, + "source": [ + "# train superposition QNN classifier\n", + "def train_superpos_qnn(n, averages=5, learning_rate=0.001):\n", + " gen_circuit_class_0 = get_model(n, 0)\n", + " gen_circuit_class_1 = get_model(n, 1)\n", + " superposition_acc_data = []\n", + " for i in range(averages):\n", + " model = tf.keras.Sequential([\n", + " # The input is the data-circuit, encoded as a tf.string\n", + " tf.keras.layers.Input(shape=(), dtype=tf.string),\n", + " # The PQC layer returns the expected value of the readout gate, range [-1,1].\n", + " tfq.layers.PQC(model_circuit, model_readout, repetitions=10000, backend=backend),\n", + " ])\n", + " model.compile(\n", + " loss=tf.keras.losses.Hinge(),\n", + " optimizer=tf.keras.optimizers.Adam(learning_rate=learning_rate),\n", + " metrics=[hinge_accuracy])\n", + "\n", + " x_superposition = tfq.convert_to_tensor([gen_circuit_class_0, gen_circuit_class_1])\n", + " y_superposition = np.array([-1, 1])\n", + "\n", + " qnn_history_superposition = model.fit(\n", + " x_superposition, y_superposition,\n", + " batch_size=1,\n", + " epochs=epochs*len(x_train_tfcirc)//2,\n", + " verbose=0,\n", + " validation_data=(x_superposition, y_superposition))\n", + "\n", + " qnn_results_superposition = model.evaluate(x_test_tfcirc, y_test)\n", + " superposition_weights = model.get_weights()[0]\n", + " superposition_acc_data.append(qnn_results_superposition[1])\n", + " print('Trained model', i+1)\n", + " \n", + " return superposition_acc_data" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MQj47iWOLshv" + }, + "source": [ + "### 3.2 Evaluating the models\n", + "\n", + "For simplicity, we take pre-computer hyperparameter parameter tunes of the learning rate previously obtained from Bayesian optimization. The two QNNs — trained by a superposition over the entire dataset or sampling individual examples — are then trained many times to determine the accuracy's mean and variance." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "kEwQHcgYB43g" + }, + "source": [ + "# pre-optimized parameter tunes\n", + "lr_tunes = {'superposition': 10**-1.83, 'sample': 10**-3.93}" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "WNaV7UPGly04", + "outputId": "636de78d-f558-42a3-ed25-2056679b9d9c" + }, + "source": [ + "averages = 20\n", + "superpos_qnn_data = train_superpos_qnn(n, averages=averages, learning_rate=lr_tunes['superposition'])\n", + "superpos_mean = np.mean(superpos_qnn_data)\n", + "superpos_std = np.std(superpos_qnn_data)/np.sqrt(averages)\n", + "print('QNN superposition accuracy (mean):', superpos_mean)\n", + "print('QNN superposition accuracy (stdev):', superpos_std)" + ], + "execution_count": null, + "outputs": [ + { + "output_type": "stream", + "text": [ + "6/6 [==============================] - 1s 60ms/step - loss: 0.6369 - hinge_accuracy: 0.6750\n", + "Trained model 1\n", + "6/6 [==============================] - 1s 66ms/step - loss: 0.4712 - hinge_accuracy: 0.8719\n", + "Trained model 2\n", + "6/6 [==============================] - 1s 70ms/step - loss: 0.6392 - hinge_accuracy: 0.6750\n", + "Trained model 3\n", + "6/6 [==============================] - 1s 67ms/step - loss: 0.7865 - hinge_accuracy: 0.6698\n", + "Trained model 4\n", + "6/6 [==============================] - 1s 70ms/step - loss: 0.5772 - hinge_accuracy: 0.6750\n", + "Trained model 5\n", + "6/6 [==============================] - 1s 73ms/step - loss: 0.9590 - hinge_accuracy: 0.4792\n", + "Trained model 6\n", + "6/6 [==============================] - 1s 71ms/step - loss: 0.6818 - hinge_accuracy: 0.7500\n", + "Trained model 7\n", + "6/6 [==============================] - 1s 81ms/step - loss: 0.7041 - hinge_accuracy: 0.6615\n", + "Trained model 8\n", + "6/6 [==============================] - 1s 79ms/step - loss: 0.6333 - hinge_accuracy: 0.6750\n", + "Trained model 9\n", + "6/6 [==============================] - 1s 77ms/step - loss: 0.4822 - hinge_accuracy: 0.8583\n", + "Trained model 10\n", + "6/6 [==============================] - 1s 76ms/step - loss: 0.6434 - hinge_accuracy: 0.6750\n", + "Trained model 11\n", + "6/6 [==============================] - 1s 70ms/step - loss: 0.3498 - hinge_accuracy: 0.8562\n", + "Trained model 12\n", + "6/6 [==============================] - 1s 67ms/step - loss: 0.6904 - hinge_accuracy: 0.5573\n", + "Trained model 13\n", + "6/6 [==============================] - 1s 73ms/step - loss: 1.0139 - hinge_accuracy: 0.4792\n", + "Trained model 14\n", + "6/6 [==============================] - 1s 74ms/step - loss: 0.9846 - hinge_accuracy: 0.4792\n", + "Trained model 15\n", + "6/6 [==============================] - 1s 72ms/step - loss: 0.9768 - hinge_accuracy: 0.4792\n", + "Trained model 16\n", + "6/6 [==============================] - 1s 77ms/step - loss: 0.7744 - hinge_accuracy: 0.6854\n", + "Trained model 17\n", + "6/6 [==============================] - 1s 78ms/step - loss: 0.9123 - hinge_accuracy: 0.5729\n", + "Trained model 18\n", + "6/6 [==============================] - 1s 78ms/step - loss: 0.6574 - hinge_accuracy: 0.6750\n", + "Trained model 19\n", + "6/6 [==============================] - 1s 78ms/step - loss: 0.5895 - hinge_accuracy: 0.6750\n", + "Trained model 20\n", + "QNN superposition accuracy (mean): 0.6562500059604645\n", + "QNN superposition accuracy (stdev): 0.026543089735993636\n" + ], + "name": "stdout" + } + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "ZW4UIKVclwks", + "outputId": "5503b8ab-05a0-4e01-fe45-529f0ef3709b" + }, + "source": [ + "sample_qnn_data = train_sample_qnn(n, averages=averages, learning_rate=lr_tunes['sample'])\n", + "sample_mean = np.mean(sample_qnn_data)\n", + "sample_std = np.std(sample_qnn_data)/np.sqrt(averages)\n", + "print('QNN sample accuracy (mean):', sample_mean)\n", + "print('QNN sample accuracy (stdev):', sample_std)" + ], + "execution_count": null, + "outputs": [ + { + "output_type": "stream", + "text": [ + "6/6 [==============================] - 1s 74ms/step - loss: 1.3611 - hinge_accuracy: 0.2458\n", + "Trained model 1\n", + "6/6 [==============================] - 1s 70ms/step - loss: 0.8586 - hinge_accuracy: 0.6719\n", + "Trained model 2\n", + "6/6 [==============================] - 1s 78ms/step - loss: 1.0331 - hinge_accuracy: 0.4448\n", + "Trained model 3\n", + "6/6 [==============================] - 1s 76ms/step - loss: 0.8689 - hinge_accuracy: 0.5250\n", + "Trained model 4\n", + "6/6 [==============================] - 1s 80ms/step - loss: 0.9726 - hinge_accuracy: 0.4729\n", + "Trained model 5\n", + "6/6 [==============================] - 1s 78ms/step - loss: 1.0333 - hinge_accuracy: 0.4167\n", + "Trained model 6\n", + "6/6 [==============================] - 1s 80ms/step - loss: 0.9244 - hinge_accuracy: 0.4188\n", + "Trained model 7\n", + "6/6 [==============================] - 1s 80ms/step - loss: 1.2292 - hinge_accuracy: 0.4208\n", + "Trained model 8\n", + "6/6 [==============================] - 1s 81ms/step - loss: 1.1388 - hinge_accuracy: 0.3406\n", + "Trained model 9\n", + "6/6 [==============================] - 1s 80ms/step - loss: 0.8012 - hinge_accuracy: 0.5135\n", + "Trained model 10\n", + "6/6 [==============================] - 1s 71ms/step - loss: 1.3918 - hinge_accuracy: 0.2521\n", + "Trained model 11\n", + "6/6 [==============================] - 1s 79ms/step - loss: 0.7282 - hinge_accuracy: 0.6875\n", + "Trained model 12\n", + "6/6 [==============================] - 1s 79ms/step - loss: 1.1379 - hinge_accuracy: 0.5583\n", + "Trained model 13\n", + "6/6 [==============================] - 1s 83ms/step - loss: 1.1785 - hinge_accuracy: 0.3240\n", + "Trained model 14\n", + "6/6 [==============================] - 1s 85ms/step - loss: 1.0180 - hinge_accuracy: 0.4729\n", + "Trained model 15\n", + "6/6 [==============================] - 1s 81ms/step - loss: 0.5691 - hinge_accuracy: 0.7521\n", + "Trained model 16\n", + "6/6 [==============================] - 1s 82ms/step - loss: 1.2714 - hinge_accuracy: 0.2344\n", + "Trained model 17\n", + "6/6 [==============================] - 1s 83ms/step - loss: 1.2833 - hinge_accuracy: 0.1302\n", + "Trained model 18\n", + "6/6 [==============================] - 1s 81ms/step - loss: 1.2765 - hinge_accuracy: 0.2458\n", + "Trained model 19\n", + "6/6 [==============================] - 1s 72ms/step - loss: 0.5838 - hinge_accuracy: 0.7917\n", + "Trained model 20\n", + "QNN sample accuracy (mean): 0.4459895886480808\n", + "QNN sample accuracy (stdev): 0.039933298914609255\n" + ], + "name": "stdout" + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Ua2Ia5akMFBn" + }, + "source": [ + "As we can see, training the QNN with an approximate QRAM helped reduce the number of calls to quantum hardware required for the QNN to converge. Error bars show two standard deviations, and the dashed lines indicate random chance (50% accuracy) and full accuracy (100%). Note that 100% accuracy is impossible to achieve on this dataset due to the overlapped region between the two peaks." + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "8zLeKAtknlt7", + "outputId": "5d52749c-2d8a-44c4-c942-19add67eb5e9" + }, + "source": [ + "fig, ax = plt.subplots(figsize=(4.8, 3.5))\n", + "ax.bar(0, superpos_mean, alpha=0.5)\n", + "ax.errorbar(0, superpos_mean, yerr=2*superpos_std, fmt='none')\n", + "ax.bar(1, sample_mean, alpha=0.5)\n", + "ax.errorbar(1, sample_mean, yerr=2*sample_std, fmt='none')\n", + "ax.set_xticks([0, 1])\n", + "ax.set_xticklabels(['Superposition', 'Sample'])\n", + "ax.set_ylabel('Classification accuracy')\n", + "ax.set_ylim(-0.1, 1.1)\n", + "ax.plot([-0.5, 1.5], [0, 0], color='k')\n", + "ax.plot([-0.5, 1.5], [0.5, 0.5], 'r--')\n", + "ax.plot([-0.5, 1.5], [1, 1], 'r--')\n", + "plt.tight_layout()\n", + "plt.show()" + ], + "execution_count": null, + "outputs": [ + { + "output_type": "display_data", + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "tags": [], + "needs_background": "light" + } + } + ] + } + ] +} \ No newline at end of file