{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Introduction and Setup\n\nWe recognize the works of artists through their unique style, such as color choices or brush strokes. The “je ne sais quoi” of artists like Claude Monet can now be imitated with algorithms thanks to generative adversarial networks (GANs). \n\nIn this project, we will bring that style to photos and recreate the style from scratch! Thus, we will utilize a CycleGAN architecture to add Monet-style to photos using the TFRecord dataset. \n\nFor more information, check out [TensorFlow](https://www.tensorflow.org/tutorials/generative/cyclegan) and [Keras](https://keras.io/examples/generative/cyclegan/) CycleGAN documentation pages.\n\nReference taken from:\n[Notebook1](http://www.kaggle.com/dapy15/monet-using-gan)\n[Notebook2](http://www.kaggle.com/drzhuzhe/monet-cyclegan-tutorial)\n[Notebook3](http://www.kaggle.com/sanjitschouhan/gan-getting-started)","metadata":{}},{"cell_type":"markdown","source":"Import relevant packages and change the accelerator to **TPU**.","metadata":{}},{"cell_type":"code","source":"import tensorflow as tf\nfrom tensorflow import keras\nfrom tensorflow.keras import layers\nimport tensorflow_addons as tfa\n\nfrom kaggle_datasets import KaggleDatasets\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport cv2\nimport os\nimport math\nimport random\nimport re\n\ntry:\n    tpu = tf.distribute.cluster_resolver.TPUClusterResolver() \n    print('Device:', tpu.master()) \n    tf.config.experimental_connect_to_cluster(tpu) \n    tf.tpu.experimental.initialize_tpu_system(tpu) \n    strategy = tf.distribute.experimental.TPUStrategy(tpu) \nexcept:\n    strategy = tf.distribute.get_strategy() \nprint('Number of replicas:', strategy.num_replicas_in_sync) \n\nAUTOTUNE = tf.data.experimental.AUTOTUNE \n    \nprint(tf.__version__)","metadata":{"_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","execution":{"iopub.status.busy":"2021-07-22T08:01:02.829559Z","iopub.execute_input":"2021-07-22T08:01:02.830167Z","iopub.status.idle":"2021-07-22T08:01:14.428501Z","shell.execute_reply.started":"2021-07-22T08:01:02.830053Z","shell.execute_reply":"2021-07-22T08:01:14.427803Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Load in the data\n\nFirst, we will load in the filenames of the TFRecords for photo dataset and Monet dataset separately. ","metadata":{}},{"cell_type":"code","source":"GCS_PATH = KaggleDatasets().get_gcs_path() #gcs = google cloud storage","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:01:29.682421Z","iopub.execute_input":"2021-07-22T08:01:29.682758Z","iopub.status.idle":"2021-07-22T08:01:30.019946Z","shell.execute_reply.started":"2021-07-22T08:01:29.682729Z","shell.execute_reply":"2021-07-22T08:01:30.019101Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"MONET_FILENAMES = tf.io.gfile.glob(str(GCS_PATH + '/monet_tfrec/*.tfrec')) \nprint('Monet TFRecord Files:', len(MONET_FILENAMES))\n\nPHOTO_FILENAMES = tf.io.gfile.glob(str(GCS_PATH + '/photo_tfrec/*.tfrec'))\nprint('Photo TFRecord Files:', len(PHOTO_FILENAMES))","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:01:31.712287Z","iopub.execute_input":"2021-07-22T08:01:31.712794Z","iopub.status.idle":"2021-07-22T08:01:31.868970Z","shell.execute_reply.started":"2021-07-22T08:01:31.712745Z","shell.execute_reply":"2021-07-22T08:01:31.868009Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"We can obtain the number of Monet images and photo images in the TFRecord files.","metadata":{}},{"cell_type":"code","source":"def count_data_items(filenames):\n    n = [int(re.compile(r\"-([0-9]*)\\.\").search(filename).group(1)) \n         for filename in filenames]\n    return np.sum(n)\n\nprint(f\"Monet images: {count_data_items(MONET_FILENAMES)}\")\n# n = [60,60,60,60,60] so total is 300\nprint(f\"Photo images: {count_data_items(PHOTO_FILENAMES)}\")\n# n = [352, 352, 352, 352, 352, 352, 352, 352, 352, 352, 352, 352, 352, 352, 352, 352, 352, 352, 352, 350] so total is 7038","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:01:36.573661Z","iopub.execute_input":"2021-07-22T08:01:36.574047Z","iopub.status.idle":"2021-07-22T08:01:36.579864Z","shell.execute_reply.started":"2021-07-22T08:01:36.574013Z","shell.execute_reply":"2021-07-22T08:01:36.579150Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"All the images for the competition are already sized to 256x256. As these images are RGB images, we will set the channel to 3. Additionally, we need to scale the images to a [-1, 1] scale. Since we are building a generative model, we don't need the labels or the image id so we'll only return the image from the TFRecord.","metadata":{}},{"cell_type":"markdown","source":"Decoding is the process of converting an encoded format back into the original sequence of characters. Hence, the JPEG-encoded input images will be decoded to unit8 tensors and scaled to a [-1,1] scale using the decode_image() function.\n\nTo decode a serialized example, we need to give Tensorflow a description of what kind of data to expect using the tfrecord_format.","metadata":{}},{"cell_type":"code","source":"IMAGE_SIZE = [256, 256]\n\ndef decode_image(image):\n    \n    image = tf.image.decode_jpeg(image, channels=3) #channel 3 output an RGB image\n    image = (tf.cast(image, tf.float32) / 127.5) - 1 \n    image = tf.reshape(image, [*IMAGE_SIZE, 3])\n    return image\n\ndef read_tfrecord(example):\n    tfrecord_format = {\n        \"image_name\": tf.io.FixedLenFeature([], tf.string),\n        \"image\": tf.io.FixedLenFeature([], tf.string),\n        \"target\": tf.io.FixedLenFeature([], tf.string)\n    }\n    example = tf.io.parse_single_example(example, tfrecord_format)\n    image = decode_image(example['image']) \n    return image","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:01:48.994406Z","iopub.execute_input":"2021-07-22T08:01:48.994811Z","iopub.status.idle":"2021-07-22T08:01:49.002364Z","shell.execute_reply.started":"2021-07-22T08:01:48.994775Z","shell.execute_reply":"2021-07-22T08:01:49.001256Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"We define the load_dataset() function to extract the image from the files.","metadata":{}},{"cell_type":"code","source":"def load_dataset(filenames, labeled=True, ordered=False):\n    dataset = tf.data.TFRecordDataset(filenames) #read a TFRecord file back to bytes\n    dataset = dataset.map(read_tfrecord, num_parallel_calls=AUTOTUNE) \n    return dataset","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:01:59.359007Z","iopub.execute_input":"2021-07-22T08:01:59.359357Z","iopub.status.idle":"2021-07-22T08:01:59.364202Z","shell.execute_reply.started":"2021-07-22T08:01:59.359329Z","shell.execute_reply":"2021-07-22T08:01:59.362900Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Let's load in our datasets.","metadata":{}},{"cell_type":"code","source":"monet_ds = load_dataset(MONET_FILENAMES, labeled=True).batch(1) \nphoto_ds = load_dataset(PHOTO_FILENAMES, labeled=True).batch(1)","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:02:01.718567Z","iopub.execute_input":"2021-07-22T08:02:01.719142Z","iopub.status.idle":"2021-07-22T08:02:01.892866Z","shell.execute_reply.started":"2021-07-22T08:02:01.719094Z","shell.execute_reply":"2021-07-22T08:02:01.891909Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"example_monet = next(iter(monet_ds)) #next() function returns the next item in an iterator.\nexample_photo = next(iter(photo_ds))","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:02:03.629576Z","iopub.execute_input":"2021-07-22T08:02:03.630139Z","iopub.status.idle":"2021-07-22T08:02:04.940105Z","shell.execute_reply.started":"2021-07-22T08:02:03.630101Z","shell.execute_reply":"2021-07-22T08:02:04.939237Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Visualize the data\nLet's  visualize a photo example and a Monet example.","metadata":{}},{"cell_type":"code","source":"plt.subplot(121)\nplt.title('Photo')\nplt.imshow(example_photo[0] * 0.5 + 0.5)\n\nplt.subplot(122)\nplt.title('Monet')\nplt.imshow(example_monet[0] * 0.5 + 0.5)","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:02:07.419323Z","iopub.execute_input":"2021-07-22T08:02:07.419811Z","iopub.status.idle":"2021-07-22T08:02:07.758428Z","shell.execute_reply.started":"2021-07-22T08:02:07.419780Z","shell.execute_reply":"2021-07-22T08:02:07.757729Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"To view more examples, we can change the batch size.","metadata":{}},{"cell_type":"code","source":"monet_ds_2 = load_dataset(MONET_FILENAMES, labeled=True).batch(5) \nphoto_ds_2 = load_dataset(PHOTO_FILENAMES, labeled=True).batch(5)\nexample_monet_2 = next(iter(monet_ds_2)) \nexample_photo_2 = next(iter(photo_ds_2))\n\n_, ax = plt.subplots(5, 2, figsize=(16, 16))\nfor i in range(5):\n    ax[i,0].imshow(example_photo_2[i] * 0.5 + 0.5)\n    ax[0,0].set_title('Photo')\n    ax[i,0].axis(\"off\")\n    \n    ax[i,1].imshow(example_monet_2[i] * 0.5 + 0.5)\n    ax[0,1].set_title('Monet')\n    ax[i,1].axis(\"off\")\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:02:35.668734Z","iopub.execute_input":"2021-07-22T08:02:35.669302Z","iopub.status.idle":"2021-07-22T08:02:36.619081Z","shell.execute_reply.started":"2021-07-22T08:02:35.669272Z","shell.execute_reply":"2021-07-22T08:02:36.618168Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"We can also visualize our input images in jpeg files using the batch_visualization() function.","metadata":{}},{"cell_type":"code","source":"def batch_visualization(path, n_images, is_random=True, figsize=(16, 16)):\n    plt.figure(figsize=figsize)\n    \n    w = int(n_images ** .5)\n    h = math.ceil(n_images / w)\n    \n    all_names = os.listdir(path)\n    \n    image_names = all_names[:n_images]\n    if is_random:\n        image_names = random.sample(all_names, n_images)\n    \n    for ind, image_name in enumerate(image_names):\n        img = cv2.imread(os.path.join(path, image_name)) \n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) \n        plt.subplot(h, w, ind + 1)\n        plt.imshow(img)\n        plt.axis('off')\n    \n    plt.show()","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:08:20.533276Z","iopub.execute_input":"2021-07-22T08:08:20.533638Z","iopub.status.idle":"2021-07-22T08:08:20.540934Z","shell.execute_reply.started":"2021-07-22T08:08:20.533604Z","shell.execute_reply":"2021-07-22T08:08:20.540243Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"base_path = '../input/monet-gan-getting-started/'\nmonet_path = os.path.join(base_path, 'monet_jpg')\nphoto_path = os.path.join(base_path, 'photo_jpg')","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:08:22.865086Z","iopub.execute_input":"2021-07-22T08:08:22.865579Z","iopub.status.idle":"2021-07-22T08:08:22.869931Z","shell.execute_reply.started":"2021-07-22T08:08:22.865533Z","shell.execute_reply":"2021-07-22T08:08:22.868999Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"batch_visualization(monet_path,6)","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:08:25.870021Z","iopub.execute_input":"2021-07-22T08:08:25.870382Z","iopub.status.idle":"2021-07-22T08:08:26.600735Z","shell.execute_reply.started":"2021-07-22T08:08:25.870349Z","shell.execute_reply":"2021-07-22T08:08:26.599849Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"batch_visualization(photo_path,6)","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:08:38.241240Z","iopub.execute_input":"2021-07-22T08:08:38.241575Z","iopub.status.idle":"2021-07-22T08:08:39.162497Z","shell.execute_reply.started":"2021-07-22T08:08:38.241547Z","shell.execute_reply":"2021-07-22T08:08:39.161470Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Let's visualize the differences between photos with and without Monet-Style using these functions:\n1. color_hist_visualization() gives the RGB colour histogram of photos.\n2. channels_visualization() gives the RGB individual channel graph of photos.\n3. grayscale_visualization() converts the RGB channel to grayscale.\n4. color_graph() gives the 'flattened' RGB colour histogram of photos.","metadata":{}},{"cell_type":"code","source":"rand_monet = r\"../input/monet-gan-getting-started/monet_jpg/000c1e3bff.jpg\"\nrand_photo = r\"../input/monet-gan-getting-started/photo_jpg/00068bc07f.jpg\"","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:22:55.468634Z","iopub.execute_input":"2021-07-22T08:22:55.469018Z","iopub.status.idle":"2021-07-22T08:22:55.473834Z","shell.execute_reply.started":"2021-07-22T08:22:55.468975Z","shell.execute_reply":"2021-07-22T08:22:55.472650Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def color_hist_visualization(image_path, figsize=(16, 4)):\n    plt.figure(figsize=figsize)\n    layers = ['red', 'green', 'blue']\n    \n    img = cv2.imread(image_path)\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) \n    plt.subplot(1, 4, 1)\n    plt.imshow(img)\n    plt.axis('off')\n    \n    for i in range(len(layers)):\n        plt.subplot(1, 4, i + 2)\n        plt.hist(\n            img[:, :, i].reshape(-1),\n            bins=25,\n            alpha=0.5,\n            color=layers[i],\n            density=True\n        )\n        plt.title(layers[i])\n        plt.xlim(0, 255)\n        plt.xticks([])\n        plt.yticks([])\n    plt.show()","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:22:58.425190Z","iopub.execute_input":"2021-07-22T08:22:58.425524Z","iopub.status.idle":"2021-07-22T08:22:58.433252Z","shell.execute_reply.started":"2021-07-22T08:22:58.425494Z","shell.execute_reply":"2021-07-22T08:22:58.432223Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"print(\"Monet: \")\ncolor_hist_visualization(rand_monet)\nprint(\"\\nPhoto: \")\ncolor_hist_visualization(rand_photo)","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:23:01.384481Z","iopub.execute_input":"2021-07-22T08:23:01.384840Z","iopub.status.idle":"2021-07-22T08:23:02.124130Z","shell.execute_reply.started":"2021-07-22T08:23:01.384810Z","shell.execute_reply":"2021-07-22T08:23:02.123367Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def channels_visualization(image_path, figsize=(16, 4)):\n    plt.figure(figsize=figsize)\n    \n    img = cv2.imread(image_path)\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) \n    plt.subplot(1, 4, 1)\n    plt.imshow(img)\n    plt.axis('off')\n    \n    for i in range(3):\n        plt.subplot(1, 4, i + 2)\n        tmp_img = np.full_like(img, 0)\n        tmp_img[:, :, i] = img[:, :, i]\n        plt.imshow(tmp_img)\n        plt.xlim(0, 255)\n        plt.xticks([])\n        plt.yticks([])\n    plt.show()","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:23:49.074559Z","iopub.execute_input":"2021-07-22T08:23:49.075084Z","iopub.status.idle":"2021-07-22T08:23:49.082048Z","shell.execute_reply.started":"2021-07-22T08:23:49.075035Z","shell.execute_reply":"2021-07-22T08:23:49.081327Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"print(\"Monet: \")\nchannels_visualization(rand_monet)\nprint(\"\\nPhoto: \")\nchannels_visualization(rand_photo)","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:23:51.919320Z","iopub.execute_input":"2021-07-22T08:23:51.919855Z","iopub.status.idle":"2021-07-22T08:23:52.469643Z","shell.execute_reply.started":"2021-07-22T08:23:51.919791Z","shell.execute_reply":"2021-07-22T08:23:52.468921Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def grayscale_visualization(image_path, figsize=(16, 4)):\n    plt.figure(figsize=figsize)\n    \n    img = cv2.imread(image_path)\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) \n    plt.subplot(1, 3, 1)\n    plt.imshow(img)\n    plt.axis('off')\n    \n    plt.subplot(1, 3, 2)\n    tmp_img = np.full_like(img, 0)\n    for i in range(3):\n        tmp_img[:, :, i] = img.mean(axis=-1)\n    plt.imshow(tmp_img)\n    plt.axis('off')\n    \n    img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)\n    plt.subplot(1,3,3)\n    plt.imshow(img)\n    plt.axis(\"off\")\n    \n    plt.show()","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:23:59.864102Z","iopub.execute_input":"2021-07-22T08:23:59.864617Z","iopub.status.idle":"2021-07-22T08:23:59.871617Z","shell.execute_reply.started":"2021-07-22T08:23:59.864586Z","shell.execute_reply":"2021-07-22T08:23:59.870915Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"print(\"Monet: \")\ngrayscale_visualization(rand_monet)\nprint(\"\\nPhoto: \")\ngrayscale_visualization(rand_photo)","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:24:02.663604Z","iopub.execute_input":"2021-07-22T08:24:02.664151Z","iopub.status.idle":"2021-07-22T08:24:03.215989Z","shell.execute_reply.started":"2021-07-22T08:24:02.664120Z","shell.execute_reply":"2021-07-22T08:24:03.215289Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def color_graph(image_path, figsize=(16, 4)):\n    plt.figure(figsize=figsize)\n    \n    img = cv2.imread(image_path)\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) \n    plt.subplot(1, 2, 1)\n    plt.imshow(img)\n    plt.axis('off')\n    \n    chans = cv2.split(img)\n    colors = (\"b\", \"g\", \"r\")\n    plt.subplot(1, 2, 2)\n    plt.title(\"'Flattened' Color Histogram\")\n    plt.xlabel(\"Bins\")\n    plt.ylabel(\"# of Pixels\")\n    features = []\n    # loop over the image channels\n    for (chan, color) in zip(chans, colors):\n        # create a histogram for the current channel and\n        # concatenate the resulting histograms for each\n        # channel\n        hist = cv2.calcHist([chan], [0], None, [256], [0, 256])\n        features.extend(hist)\n        # plot the histogram\n        plt.plot(hist, color = color)\n        plt.xlim([0, 256])\n    \n    plt.show()","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:24:06.863236Z","iopub.execute_input":"2021-07-22T08:24:06.863751Z","iopub.status.idle":"2021-07-22T08:24:06.871170Z","shell.execute_reply.started":"2021-07-22T08:24:06.863711Z","shell.execute_reply":"2021-07-22T08:24:06.870499Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"print(\"Monet: \")\ncolor_graph(rand_monet)\nprint(\"\\nPhoto: \")\ncolor_graph(rand_photo)","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:24:10.414353Z","iopub.execute_input":"2021-07-22T08:24:10.415005Z","iopub.status.idle":"2021-07-22T08:24:10.869060Z","shell.execute_reply.started":"2021-07-22T08:24:10.414951Z","shell.execute_reply":"2021-07-22T08:24:10.868108Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Build the generator\n\nWe'll be using a UNET architecture for our CycleGAN. To build our generator, let's first define our `downsample` and `upsample` methods.\n\nThe `downsample`, as the name suggests, reduces the 2D dimensions, the width and height, of the image by the stride. The stride is the length of the step the filter takes. Since the stride is 2, the filter is applied to every other pixel, hence reducing the weight and height by 2.\n\nNormalization is a technique used to standardize the inputs to a network. There are a few versions of normalization available. Batch version normalizes all images across the batch and spatial locations while instance version normalizes each element of the batch independently (across spatial locations only).\n\nHere we'll be using an **instance normalization** instead of batch normalization. As the instance normalization is not standard in the TensorFlow API, we'll use the layer from TensorFlow Add-ons.","metadata":{}},{"cell_type":"code","source":"OUTPUT_CHANNELS = 3\n\ndef downsample(filters, size, apply_instancenorm=True):\n    initializer = tf.random_normal_initializer(0., 0.02)\n    gamma_init = keras.initializers.RandomNormal(mean=0.0, stddev=0.02)\n\n    result = keras.Sequential()\n    result.add(layers.Conv2D(filters, size, strides=2, padding='same',\n                             kernel_initializer=initializer, use_bias=False))\n\n    if apply_instancenorm:\n        result.add(tfa.layers.InstanceNormalization(gamma_initializer=gamma_init)) # initializer for the gamma weight\n\n    result.add(layers.LeakyReLU()) # The Leaky ReLU modifies the function to allow small negative values when the input is less than zero\n\n    return result","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:27:48.920092Z","iopub.execute_input":"2021-07-22T08:27:48.920462Z","iopub.status.idle":"2021-07-22T08:27:48.927153Z","shell.execute_reply.started":"2021-07-22T08:27:48.920428Z","shell.execute_reply":"2021-07-22T08:27:48.926399Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"`Upsample` does the opposite of downsample and increases the dimensions of the of the image. `Conv2DTranspose` does basically the opposite of a `Conv2D` layer.","metadata":{}},{"cell_type":"code","source":"def upsample(filters, size, apply_dropout=False):\n    initializer = tf.random_normal_initializer(0., 0.02)\n    gamma_init = keras.initializers.RandomNormal(mean=0.0, stddev=0.02)\n\n    result = keras.Sequential()\n    result.add(layers.Conv2DTranspose(filters, size, strides=2,\n                                      padding='same',\n                                      kernel_initializer=initializer,\n                                      use_bias=False))\n\n    result.add(tfa.layers.InstanceNormalization(gamma_initializer=gamma_init))\n\n    if apply_dropout:\n        result.add(layers.Dropout(0.5))\n\n    result.add(layers.ReLU())\n\n    return result","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:37:37.553403Z","iopub.execute_input":"2021-07-22T08:37:37.553749Z","iopub.status.idle":"2021-07-22T08:37:37.560879Z","shell.execute_reply.started":"2021-07-22T08:37:37.553718Z","shell.execute_reply":"2021-07-22T08:37:37.560020Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Let's build our generator!\n\nThe generator first downsamples the input image and then upsample while establishing long skip connections. Skip connections are a way to help bypass the vanishing gradient problem by concatenating the output of a layer to multiple layers instead of only one. \n\nBy using a skip connection, we provide an alternative path for the gradient. These additional paths are often beneficial for our model convergence. Here we concatenate the output of the downsample layer to the upsample layer in a symmetrical fashion.","metadata":{}},{"cell_type":"code","source":"def Generator():\n    inputs = layers.Input(shape=[256,256,3])\n\n    # bs = batch size\n    down_stack = [\n        downsample(64, 4, apply_instancenorm=False), # (bs, 128, 128, 64)\n        downsample(128, 4), # (bs, 64, 64, 128)\n        downsample(256, 4), # (bs, 32, 32, 256)\n        downsample(512, 4), # (bs, 16, 16, 512)\n        downsample(512, 4), # (bs, 8, 8, 512)\n        downsample(512, 4), # (bs, 4, 4, 512)\n        downsample(512, 4), # (bs, 2, 2, 512)\n        downsample(512, 4), # (bs, 1, 1, 512)\n    ]\n\n    up_stack = [\n        upsample(512, 4, apply_dropout=True), # (bs, 2, 2, 1024)\n        upsample(512, 4, apply_dropout=True), # (bs, 4, 4, 1024)\n        upsample(512, 4, apply_dropout=True), # (bs, 8, 8, 1024)\n        upsample(512, 4), # (bs, 16, 16, 1024)\n        upsample(256, 4), # (bs, 32, 32, 512)\n        upsample(128, 4), # (bs, 64, 64, 256)\n        upsample(64, 4), # (bs, 128, 128, 128)\n    ]\n\n    initializer = tf.random_normal_initializer(0., 0.02)\n    last = layers.Conv2DTranspose(OUTPUT_CHANNELS, 4,\n                                  strides=2,\n                                  padding='same',\n                                  kernel_initializer=initializer,\n                                  activation='tanh') # (bs, 256, 256, 3)\n\n    x = inputs\n\n    # Downsampling through the model\n    skips = []\n    for down in down_stack:\n        x = down(x)\n        skips.append(x)\n\n    skips = reversed(skips[:-1])\n\n    # Upsampling and establishing the skip connections\n    for up, skip in zip(up_stack, skips):\n        x = up(x)\n        x = layers.Concatenate()([x, skip])\n\n    x = last(x)\n\n    return keras.Model(inputs=inputs, outputs=x)","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:37:40.341561Z","iopub.execute_input":"2021-07-22T08:37:40.342158Z","iopub.status.idle":"2021-07-22T08:37:40.352556Z","shell.execute_reply.started":"2021-07-22T08:37:40.342127Z","shell.execute_reply":"2021-07-22T08:37:40.351764Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Build the discriminator\n\nThe discriminator takes in the input image and classifies it as real or fake (generated). Instead of outputing a single node, the discriminator outputs a smaller 2D image with higher pixel values indicating a real classification and lower values indicating a fake classification.","metadata":{}},{"cell_type":"code","source":"def Discriminator():\n    initializer = tf.random_normal_initializer(0., 0.02)\n    gamma_init = keras.initializers.RandomNormal(mean=0.0, stddev=0.02)\n\n    inp = layers.Input(shape=[256, 256, 3], name='input_image')\n\n    x = inp\n\n    down1 = downsample(64, 4, False)(x) # (bs, 128, 128, 64)\n    down2 = downsample(128, 4)(down1) # (bs, 64, 64, 128)\n    down3 = downsample(256, 4)(down2) # (bs, 32, 32, 256)\n\n    zero_pad1 = layers.ZeroPadding2D()(down3) # (bs, 34, 34, 256) #Zero padding allows us to preserve the original input size. \n    conv = layers.Conv2D(512, 4, strides=1,\n                         kernel_initializer=initializer,\n                         use_bias=False)(zero_pad1) # (bs, 31, 31, 512)\n\n    norm1 = tfa.layers.InstanceNormalization(gamma_initializer=gamma_init)(conv)\n\n    leaky_relu = layers.LeakyReLU()(norm1)\n\n    zero_pad2 = layers.ZeroPadding2D()(leaky_relu) # (bs, 33, 33, 512)\n\n    last = layers.Conv2D(1, 4, strides=1,\n                         kernel_initializer=initializer)(zero_pad2) # (bs, 30, 30, 1)\n\n    return tf.keras.Model(inputs=inp, outputs=last)","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:38:30.402367Z","iopub.execute_input":"2021-07-22T08:38:30.402768Z","iopub.status.idle":"2021-07-22T08:38:30.410953Z","shell.execute_reply.started":"2021-07-22T08:38:30.402737Z","shell.execute_reply":"2021-07-22T08:38:30.410090Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"with strategy.scope():\n    monet_generator = Generator() # transforms photos to Monet-esque paintings\n    photo_generator = Generator() # transforms Monet paintings to be more like photos\n\n    monet_discriminator = Discriminator() # differentiates real Monet paintings and generated Monet paintings\n    photo_discriminator = Discriminator() # differentiates real photos and generated photos","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:38:33.090566Z","iopub.execute_input":"2021-07-22T08:38:33.090979Z","iopub.status.idle":"2021-07-22T08:38:43.183645Z","shell.execute_reply.started":"2021-07-22T08:38:33.090947Z","shell.execute_reply":"2021-07-22T08:38:43.182693Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Let's visualize how a generator and a discriminator work.","metadata":{}},{"cell_type":"markdown","source":"**Downsample**","metadata":{}},{"cell_type":"code","source":"tf.keras.utils.plot_model(monet_generator.layers[1], show_layer_names=False, dpi=64)","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:40:11.602780Z","iopub.execute_input":"2021-07-22T08:40:11.603115Z","iopub.status.idle":"2021-07-22T08:40:11.995117Z","shell.execute_reply.started":"2021-07-22T08:40:11.603088Z","shell.execute_reply":"2021-07-22T08:40:11.994101Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"**Upsample**","metadata":{}},{"cell_type":"code","source":"tf.keras.utils.plot_model(monet_generator.layers[-3], show_layer_names=False, dpi=64)","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:40:29.317356Z","iopub.execute_input":"2021-07-22T08:40:29.317731Z","iopub.status.idle":"2021-07-22T08:40:29.416085Z","shell.execute_reply.started":"2021-07-22T08:40:29.317697Z","shell.execute_reply":"2021-07-22T08:40:29.415110Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"**Generator**","metadata":{}},{"cell_type":"code","source":"tf.keras.utils.plot_model(monet_generator, show_shapes=True, dpi=64)","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:40:32.347004Z","iopub.execute_input":"2021-07-22T08:40:32.347586Z","iopub.status.idle":"2021-07-22T08:40:32.606558Z","shell.execute_reply.started":"2021-07-22T08:40:32.347531Z","shell.execute_reply":"2021-07-22T08:40:32.605585Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"**Discriminator**","metadata":{}},{"cell_type":"code","source":"tf.keras.utils.plot_model(monet_discriminator, show_shapes=True, dpi=64)","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:40:42.434986Z","iopub.execute_input":"2021-07-22T08:40:42.435316Z","iopub.status.idle":"2021-07-22T08:40:42.572502Z","shell.execute_reply.started":"2021-07-22T08:40:42.435286Z","shell.execute_reply":"2021-07-22T08:40:42.571478Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Since our generators are not trained yet, the generated Monet-esque photo does not show what is expected at this point.","metadata":{}},{"cell_type":"code","source":"to_monet = monet_generator(example_photo)\n\nplt.subplot(1, 2, 1)\nplt.title(\"Original Photo\")\nplt.imshow(example_photo[0] * 0.5 + 0.5)\n\nplt.subplot(1, 2, 2)\nplt.title(\"Monet-esque Photo\")\nplt.imshow(to_monet[0] * 0.5 + 0.5)\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:40:55.756552Z","iopub.execute_input":"2021-07-22T08:40:55.756963Z","iopub.status.idle":"2021-07-22T08:40:56.377816Z","shell.execute_reply.started":"2021-07-22T08:40:55.756925Z","shell.execute_reply":"2021-07-22T08:40:56.376775Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Build the CycleGAN model\n\nWe will subclass a `tf.keras.Model` so that we can run `fit()` later to train our model. During the training step, the model transforms a photo to a Monet painting and then back to a photo. The difference between the original photo and the twice-transformed photo is the **cycle-consistency loss**. \n\nSince there is no paired data to train on in cycleGAN, we have no guarantee that the input x and the target y pair are meaningful during training. Hence, in order to enforce that our network learns the correct mapping, we need to compute the cycle consistency loss to ensure that the original photo and the twice-transformed photo are similar to one another.\n\nThe losses are defined in the next section.","metadata":{}},{"cell_type":"code","source":"class CycleGan(keras.Model):\n    def __init__(\n        self,\n        monet_generator,\n        photo_generator,\n        monet_discriminator,\n        photo_discriminator,\n        lambda_cycle=10,\n    ):\n        super(CycleGan, self).__init__()\n        self.m_gen = monet_generator\n        self.p_gen = photo_generator\n        self.m_disc = monet_discriminator\n        self.p_disc = photo_discriminator\n        self.lambda_cycle = lambda_cycle\n        \n    def compile(\n        self,\n        m_gen_optimizer,\n        p_gen_optimizer,\n        m_disc_optimizer,\n        p_disc_optimizer,\n        gen_loss_fn,\n        disc_loss_fn,\n        cycle_loss_fn,\n        identity_loss_fn\n    ):\n        super(CycleGan, self).compile()\n        self.m_gen_optimizer = m_gen_optimizer\n        self.p_gen_optimizer = p_gen_optimizer\n        self.m_disc_optimizer = m_disc_optimizer\n        self.p_disc_optimizer = p_disc_optimizer\n        self.gen_loss_fn = gen_loss_fn\n        self.disc_loss_fn = disc_loss_fn\n        self.cycle_loss_fn = cycle_loss_fn\n        self.identity_loss_fn = identity_loss_fn\n        \n    def train_step(self, batch_data):\n        real_monet, real_photo = batch_data\n        \n        with tf.GradientTape(persistent=True) as tape:\n            # photo to monet back to photo\n            fake_monet = self.m_gen(real_photo, training=True)\n            cycled_photo = self.p_gen(fake_monet, training=True)\n\n            # monet to photo back to monet\n            fake_photo = self.p_gen(real_monet, training=True)\n            cycled_monet = self.m_gen(fake_photo, training=True)\n\n            # generating itself\n            same_monet = self.m_gen(real_monet, training=True)\n            same_photo = self.p_gen(real_photo, training=True)\n\n            # discriminator used to check, inputing real images\n            disc_real_monet = self.m_disc(real_monet, training=True)\n            disc_real_photo = self.p_disc(real_photo, training=True)\n\n            # discriminator used to check, inputing fake images\n            disc_fake_monet = self.m_disc(fake_monet, training=True)\n            disc_fake_photo = self.p_disc(fake_photo, training=True)\n\n            # evaluates generator loss\n            monet_gen_loss = self.gen_loss_fn(disc_fake_monet)\n            photo_gen_loss = self.gen_loss_fn(disc_fake_photo)\n\n            # evaluates total cycle consistency loss\n            total_cycle_loss = self.cycle_loss_fn(real_monet, cycled_monet, self.lambda_cycle) + self.cycle_loss_fn(real_photo, cycled_photo, self.lambda_cycle)\n\n            # evaluates total generator loss\n            total_monet_gen_loss = monet_gen_loss + total_cycle_loss + self.identity_loss_fn(real_monet, same_monet, self.lambda_cycle)\n            total_photo_gen_loss = photo_gen_loss + total_cycle_loss + self.identity_loss_fn(real_photo, same_photo, self.lambda_cycle)\n\n            # evaluates discriminator loss\n            monet_disc_loss = self.disc_loss_fn(disc_real_monet, disc_fake_monet)\n            photo_disc_loss = self.disc_loss_fn(disc_real_photo, disc_fake_photo)\n\n        # Calculate the gradients for generator and discriminator\n        monet_generator_gradients = tape.gradient(total_monet_gen_loss,\n                                                  self.m_gen.trainable_variables)\n        photo_generator_gradients = tape.gradient(total_photo_gen_loss,\n                                                  self.p_gen.trainable_variables)\n\n        monet_discriminator_gradients = tape.gradient(monet_disc_loss,\n                                                      self.m_disc.trainable_variables)\n        photo_discriminator_gradients = tape.gradient(photo_disc_loss,\n                                                      self.p_disc.trainable_variables)\n\n        # Apply the gradients to the optimizer\n        self.m_gen_optimizer.apply_gradients(zip(monet_generator_gradients,\n                                                 self.m_gen.trainable_variables))\n\n        self.p_gen_optimizer.apply_gradients(zip(photo_generator_gradients,\n                                                 self.p_gen.trainable_variables))\n\n        self.m_disc_optimizer.apply_gradients(zip(monet_discriminator_gradients,\n                                                  self.m_disc.trainable_variables))\n\n        self.p_disc_optimizer.apply_gradients(zip(photo_discriminator_gradients,\n                                                  self.p_disc.trainable_variables))\n        \n        return {\n            \"monet_gen_loss\": total_monet_gen_loss,\n            \"photo_gen_loss\": total_photo_gen_loss,\n            \"monet_disc_loss\": monet_disc_loss,\n            \"photo_disc_loss\": photo_disc_loss\n        }","metadata":{"execution":{"iopub.status.busy":"2021-07-22T08:59:02.689202Z","iopub.execute_input":"2021-07-22T08:59:02.689810Z","iopub.status.idle":"2021-07-22T08:59:02.713284Z","shell.execute_reply.started":"2021-07-22T08:59:02.689760Z","shell.execute_reply":"2021-07-22T08:59:02.712237Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Define loss functions\n\nWe will define four loss functions here. They are discriminator loss, generator loss, cycle consistency loss and identity loss. ","metadata":{}},{"cell_type":"markdown","source":"The discriminator loss function below compares real images to a matrix of 1s and fake images to a matrix of 0s. The perfect discriminator will output all 1s for real images and all 0s for fake images. The discriminator loss outputs the average of the real and generated loss.","metadata":{}},{"cell_type":"code","source":"with strategy.scope():\n    def discriminator_loss(real, generated):\n        real_loss = tf.keras.losses.BinaryCrossentropy(from_logits=True, reduction=tf.keras.losses.Reduction.NONE)(tf.ones_like(real), real)\n        \n        generated_loss = tf.keras.losses.BinaryCrossentropy(from_logits=True, reduction=tf.keras.losses.Reduction.NONE)(tf.zeros_like(generated), generated)\n\n        total_disc_loss = real_loss + generated_loss\n\n        return total_disc_loss * 0.5","metadata":{"execution":{"iopub.status.busy":"2021-07-22T09:00:39.834435Z","iopub.execute_input":"2021-07-22T09:00:39.834924Z","iopub.status.idle":"2021-07-22T09:00:39.843852Z","shell.execute_reply.started":"2021-07-22T09:00:39.834888Z","shell.execute_reply":"2021-07-22T09:00:39.842103Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"The generator wants to fool the discriminator into thinking the generated image is real. The perfect generator will have the discriminator output only 1s. Thus, it compares the generated image to a matrix of 1s to find the loss.","metadata":{}},{"cell_type":"code","source":"with strategy.scope():\n    def generator_loss(generated):\n        return tf.keras.losses.BinaryCrossentropy(from_logits=True, reduction=tf.keras.losses.Reduction.NONE)(tf.ones_like(generated), generated)","metadata":{"execution":{"iopub.status.busy":"2021-07-22T09:00:42.485499Z","iopub.execute_input":"2021-07-22T09:00:42.485944Z","iopub.status.idle":"2021-07-22T09:00:42.493708Z","shell.execute_reply.started":"2021-07-22T09:00:42.485906Z","shell.execute_reply":"2021-07-22T09:00:42.492872Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"We want our original photo and the twice transformed photo to be similar to one another. Thus, we can calculate the cycle consistency loss be finding the average of their difference.","metadata":{}},{"cell_type":"code","source":"with strategy.scope():\n    def calc_cycle_loss(real_image, cycled_image, LAMBDA):\n        loss1 = tf.reduce_mean(tf.abs(real_image - cycled_image))\n\n        return LAMBDA * loss1","metadata":{"execution":{"iopub.status.busy":"2021-07-22T09:00:44.338397Z","iopub.execute_input":"2021-07-22T09:00:44.339063Z","iopub.status.idle":"2021-07-22T09:00:44.344067Z","shell.execute_reply.started":"2021-07-22T09:00:44.339018Z","shell.execute_reply":"2021-07-22T09:00:44.343166Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"The identity loss compares the image with its generator (i.e. photo with photo generator). If given a photo as input, we want it to generate the same image as the image was originally a photo. The identity loss compares the input with the output of the generator.","metadata":{}},{"cell_type":"code","source":"with strategy.scope():\n    def identity_loss(real_image, same_image, LAMBDA):\n        loss = tf.reduce_mean(tf.abs(real_image - same_image))\n        return LAMBDA * 0.5 * loss","metadata":{"execution":{"iopub.status.busy":"2021-07-22T09:00:46.757986Z","iopub.execute_input":"2021-07-22T09:00:46.758534Z","iopub.status.idle":"2021-07-22T09:00:46.763207Z","shell.execute_reply.started":"2021-07-22T09:00:46.758496Z","shell.execute_reply":"2021-07-22T09:00:46.762387Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Train the CycleGAN\n\nLet's compile our model. Since we used `tf.keras.Model` to build our CycleGAN, we can just use the `fit` function to train our model.","metadata":{}},{"cell_type":"code","source":"with strategy.scope():\n    monet_generator_optimizer = tf.keras.optimizers.Adam(2e-4, beta_1=0.5) #beta_1 is the exponential decay rate for the 1st moment estimates.\n    photo_generator_optimizer = tf.keras.optimizers.Adam(2e-4, beta_1=0.5)\n\n    monet_discriminator_optimizer = tf.keras.optimizers.Adam(2e-4, beta_1=0.5)\n    photo_discriminator_optimizer = tf.keras.optimizers.Adam(2e-4, beta_1=0.5)","metadata":{"execution":{"iopub.status.busy":"2021-07-22T09:00:49.178140Z","iopub.execute_input":"2021-07-22T09:00:49.178554Z","iopub.status.idle":"2021-07-22T09:00:49.186359Z","shell.execute_reply.started":"2021-07-22T09:00:49.178518Z","shell.execute_reply":"2021-07-22T09:00:49.184666Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"with strategy.scope():\n    cycle_gan_model = CycleGan(\n        monet_generator, photo_generator, monet_discriminator, photo_discriminator\n    )\n\n    cycle_gan_model.compile(\n        m_gen_optimizer = monet_generator_optimizer,\n        p_gen_optimizer = photo_generator_optimizer,\n        m_disc_optimizer = monet_discriminator_optimizer,\n        p_disc_optimizer = photo_discriminator_optimizer,\n        gen_loss_fn = generator_loss,\n        disc_loss_fn = discriminator_loss,\n        cycle_loss_fn = calc_cycle_loss,\n        identity_loss_fn = identity_loss\n    )","metadata":{"execution":{"iopub.status.busy":"2021-07-22T09:00:52.131251Z","iopub.execute_input":"2021-07-22T09:00:52.131633Z","iopub.status.idle":"2021-07-22T09:00:52.189126Z","shell.execute_reply.started":"2021-07-22T09:00:52.131600Z","shell.execute_reply":"2021-07-22T09:00:52.188000Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"cycle_gan_model.fit(\n    tf.data.Dataset.zip((monet_ds, photo_ds)),\n    epochs=50 #we can compare our models using different number of epochs\n)","metadata":{"execution":{"iopub.status.busy":"2021-07-22T05:23:02.683039Z","iopub.execute_input":"2021-07-22T05:23:02.683337Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Visualize our Monet-esque photos","metadata":{}},{"cell_type":"code","source":"_, ax = plt.subplots(3, 2, figsize=(12, 12))\nfor i, img in enumerate(photo_ds.take(3)):\n    prediction = monet_generator(img, training=False)[0].numpy()\n    prediction = (prediction * 127.5 + 127.5).astype(np.uint8)\n    img = (img[0] * 127.5 + 127.5).numpy().astype(np.uint8) #unit8: Unsigned integer (0 to 255)\n\n    ax[i, 0].imshow(img)\n    ax[i, 1].imshow(prediction)\n    ax[i, 0].set_title(\"Input Photo\")\n    ax[i, 1].set_title(\"Monet-esque\")\n    ax[i, 0].axis(\"off\")\n    ax[i, 1].axis(\"off\")\nplt.show()","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Create submission file","metadata":{}},{"cell_type":"code","source":"import PIL\n! mkdir ../images","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"i = 1\nfor img in photo_ds:\n    prediction = monet_generator(img, training=False)[0].numpy()\n    prediction = (prediction * 127.5 + 127.5).astype(np.uint8)\n    im = PIL.Image.fromarray(prediction)\n    im.save(\"../images/\" + str(i) + \".jpg\")\n    i += 1","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"import shutil\nshutil.make_archive(\"/kaggle/working/images\", 'zip', \"/kaggle/images\")","metadata":{"trusted":true},"execution_count":null,"outputs":[]}]}