From 4b9776ae8d55f57f418c72c7f2916e5a3cde1a14 Mon Sep 17 00:00:00 2001 From: Jae Yoo Date: Sat, 24 Jul 2021 10:23:28 +0900 Subject: [PATCH 1/2] Add Quantum Generative Adversarial Networks tutorial --- docs/_book.yaml | 2 + docs/tutorials/noise_suppression.ipynb | 1308 ++++++++++++++++++++++++ 2 files changed, 1310 insertions(+) create mode 100644 docs/tutorials/noise_suppression.ipynb diff --git a/docs/_book.yaml b/docs/_book.yaml index 47daeba1b..4454fded7 100644 --- a/docs/_book.yaml +++ b/docs/_book.yaml @@ -54,6 +54,8 @@ upper_tabs: path: /quantum/tutorials/research_tools - title: "Quantum reinforcement learning" path: /quantum/tutorials/quantum_reinforcement_learning + - title: "Quantum Generative Adversarial Networks" + path: /quantum/tutorials/noise_suppression - name: API skip_translation: true diff --git a/docs/tutorials/noise_suppression.ipynb b/docs/tutorials/noise_suppression.ipynb new file mode 100644 index 000000000..e8ed77208 --- /dev/null +++ b/docs/tutorials/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": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "tags": [], + "needs_background": "light" + } + } + ] + } + ] +} \ No newline at end of file From e4192c7ffcece3560ace120e313aa78fe13acb04 Mon Sep 17 00:00:00 2001 From: "Jae H. Yoo" <40815393+jaeyoo@users.noreply.github.com> Date: Fri, 23 Jul 2021 19:36:11 -0700 Subject: [PATCH 2/2] Update noise_suppression.ipynb --- docs/tutorials/noise_suppression.ipynb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/tutorials/noise_suppression.ipynb b/docs/tutorials/noise_suppression.ipynb index e8ed77208..fcc020d7c 100644 --- a/docs/tutorials/noise_suppression.ipynb +++ b/docs/tutorials/noise_suppression.ipynb @@ -946,9 +946,9 @@ " 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", + " discriminator_parameters.append(sympy.Symbol('Discrimx{}'.format(j)))\n", " for j in range(num_gen_parameters(n_qubits)):\n", - " generator_parameters.append(sympy.Symbol('Genx{!r}'.format(j)))\n", + " generator_parameters.append(sympy.Symbol('Genx{}'.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", @@ -1305,4 +1305,4 @@ ] } ] -} \ No newline at end of file +}