{"cells":[{"metadata":{},"cell_type":"markdown","source":"## Acknowledgements\n\n* Used [Monet CycleGAN Tutorial](https://www.kaggle.com/amyjang/monet-cyclegan-tutorial) kernel by [Amy Jang](https://www.kaggle.com/amyjang) to build the data pipeline. \n* The [official TensorFlow Cycle GAN tutorial](https://www.tensorflow.org/tutorials/generative/cyclegan) is used to build the baseline model based on Pix2Pix model.\n* Used [CycleGAN to generate Monet-style images](https://www.kaggle.com/swepat/cyclegan-to-generate-monet-style-images) kernel by [Swetha](https://www.kaggle.com/swepat) to build the submission pipeline.\n\n\n## Updates\n\n* `v1`: Using generator and discriminator used in Pix2Pix model as the baseline. Using Random Jitter as the augmentation policy while training. "},{"metadata":{},"cell_type":"markdown","source":"# 🧰 Setups and Imports\n\n* We will be using `tensorflow_examples` package to import generator and discriminator used in Pix2Pix. \n* [Weights and Biases](https://wandb.ai/site) is used for experiment tracking. "},{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"cell_type":"code","source":"!pip install -q git+https://github.com/tensorflow/examples.git\n!pip install wandb -q","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","trusted":true},"cell_type":"code","source":"import tensorflow as tf\n\nfrom tensorflow.keras.layers import * \nfrom tensorflow.keras.models import *\nfrom tensorflow.keras.initializers import *\nfrom tensorflow.keras.optimizers import *\nfrom tensorflow_addons.layers import *\n\nimport tensorflow_datasets as tfds\nfrom tensorflow_examples.models.pix2pix import pix2pix\n\nimport os\nimport random\nimport time\nimport PIL\nfrom tqdm import tqdm\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom IPython.display import clear_output\n\nfrom kaggle_datasets import KaggleDatasets","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"import wandb\nfrom kaggle_secrets import UserSecretsClient\n\nuser_secrets = UserSecretsClient()\nwandb_api = user_secrets.get_secret(\"wandb_api\")\n\nwandb.login(key=wandb_api)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Set the random seeds\nos.environ['TF_CUDNN_DETERMINISTIC'] = '1' \nrandom.seed(hash(\"setting random seeds\") % 2**32 - 1)\nnp.random.seed(hash(\"improves reproducibility\") % 2**32 - 1)\ntf.random.set_seed(hash(\"by removing stochasticity\") % 2**32 - 1)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# 📀 Load Data and Prepare\n\nWe will have two separate `tf.data` dataset:\n* One for Monet which will be our style images. \n* Other for the content images. \n\nThe resulting `monet_ds` and `photo_ds` data loaders will be used to train the CycleGAN. We will train with some augmentation policy so that the model doesn't overfit on the training data since the same data is used for generating styled images. \n\n`photo_ds_eval` will be used to generate the styled images. Note that only pixel normalization is applied in the evaluation dataset."},{"metadata":{"trusted":true},"cell_type":"code","source":"GCS_PATH = KaggleDatasets().get_gcs_path()\nprint(GCS_PATH)\n\nMONET_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))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"IMG_HEIGHT = 256\nIMG_WIDTH = 256\nBUFFER_SIZE = 1024\nAUTOTUNE = tf.data.experimental.AUTOTUNE\n\n# Function to read the TFRecord files.\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    # decode image\n    image = tf.image.decode_jpeg(example['image'], channels=3)\n    \n    return image\n\n# Crop the image randomly \ndef random_crop(image):\n  cropped_image = tf.image.random_crop(\n      image, size=[IMG_HEIGHT, IMG_WIDTH, 3])\n\n  return cropped_image\n\n# Bring image pixels in the range of [-1, 1]\ndef normalize(image):\n  image = tf.cast(image, tf.float32)\n  image = (image / 127.5) - 1\n  return image\n\n# Apply random jitter augmentation\ndef random_jitter(image):\n  # resizing to 286 x 286 x 3\n  image = tf.image.resize(image, [286, 286],\n                          method=tf.image.ResizeMethod.NEAREST_NEIGHBOR)\n\n  # randomly cropping to 256 x 256 x 3\n  image = random_crop(image)\n\n  # random mirroring\n  image = tf.image.random_flip_left_right(image)\n\n  return image\n\n# Preprocessing pipeline for training\ndef preprocess_image_train(image):\n  image = random_jitter(image)\n  image = normalize(image)\n  return image\n\n# Preprocessing pipeline for evaluation/submission\ndef preprocess_image_eval(image):\n  image = normalize(image)\n  return image","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def load_dataset(filenames, labeled=True, ordered=False):\n    dataset = tf.data.TFRecordDataset(filenames)\n    dataset = dataset.map(read_tfrecord, num_parallel_calls=AUTOTUNE)\n    return dataset","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"monet_ds = load_dataset(MONET_FILENAMES, labeled=True).map(\n    preprocess_image_train, num_parallel_calls=AUTOTUNE).cache().shuffle(BUFFER_SIZE).batch(1)\n\nphoto_ds = load_dataset(PHOTO_FILENAMES, labeled=True).map(\n    preprocess_image_train, num_parallel_calls=AUTOTUNE).cache().shuffle(BUFFER_SIZE).batch(1)\n\nphoto_ds_eval = load_dataset(PHOTO_FILENAMES, labeled=True).map(\n    preprocess_image_eval, num_parallel_calls=AUTOTUNE).shuffle(BUFFER_SIZE).batch(1)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Visualize a pair of style and content image. \n\n**Note**: There is no one-to-one mapping between the style and the content images. \n\n* First column - Style(Monet) Images\n* Second column - Content(Photo) Images"},{"metadata":{"trusted":true},"cell_type":"code","source":"def show_image_pairs(monet_images, photo_images):\n    fig, axs = plt.subplots(nrows=5, ncols=2, figsize=(8,12))\n    for i, (monet_image, photo_image) in enumerate(zip(monet_images, photo_images)):\n        axs[i][0].imshow(np.squeeze(monet_image, 0) * 0.5 + 0.5); \n        axs[i][1].imshow(np.squeeze(photo_image, 0) * 0.5 + 0.5);\n        \nmonet_images = []\nphoto_images = []\n\nfor i in range(5):\n    monet_images.append(next(iter(monet_ds)))\n    photo_images.append(next(iter(photo_ds)))\n\nshow_image_pairs(monet_images, photo_images)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# 💎 CycleGAN Model\n\nThe model architecture is similar to that of Pix2Pix. Some of the differences are:\n\n* Instance normalization is used instead of batch normalization.\n* UNET based generator is used.\n\nThere are 2 generators (G and F) and 2 discriminators (X and Y) being trained here.\n\n* Generator G learns to transform image X to image Y. \n* Generator F learns to transform image Y to image X.\n* Discriminator D_X learns to differentiate between image X and generated image X (F(Y)).\n* Discriminator D_Y learns to differentiate between image Y and generated image Y (G(X))."},{"metadata":{"trusted":true},"cell_type":"code","source":"OUTPUT_CHANNELS = 3\n\ntf.keras.backend.clear_session()\n\ngenerator_g = pix2pix.unet_generator(OUTPUT_CHANNELS, norm_type='instancenorm')\ngenerator_f = pix2pix.unet_generator(OUTPUT_CHANNELS, norm_type='instancenorm')\n\ndiscriminator_x = pix2pix.discriminator(norm_type='instancenorm', target=False)\ndiscriminator_y = pix2pix.discriminator(norm_type='instancenorm', target=False)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def define_discriminator(image_shape=(IMG_HEIGHT, IMG_HEIGHT, 3)):\n\t# weight initialization\n\tinit = RandomNormal(stddev=0.02)\n\t# source image input\n\tin_image = Input(shape=image_shape)\n\t# C64\n\td = Conv2D(64, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(in_image)\n\td = LeakyReLU(alpha=0.2)(d)\n\t# C128\n\td = Conv2D(128, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(d)\n\td = InstanceNormalization(axis=-1)(d)\n\td = LeakyReLU(alpha=0.2)(d)\n\t# C256\n\td = Conv2D(256, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(d)\n\td = InstanceNormalization(axis=-1)(d)\n\td = LeakyReLU(alpha=0.2)(d)\n\t# C512\n\td = Conv2D(512, (4,4), strides=(2,2), padding='same', kernel_initializer=init)(d)\n\td = InstanceNormalization(axis=-1)(d)\n\td = LeakyReLU(alpha=0.2)(d)\n\t# second last output layer\n\td = Conv2D(512, (4,4), padding='same', kernel_initializer=init)(d)\n\td = InstanceNormalization(axis=-1)(d)\n\td = LeakyReLU(alpha=0.2)(d)\n\t# patch output\n\tpatch_out = Conv2D(1, (4,4), padding='same', kernel_initializer=init)(d)\n\t# define model\n\tmodel = Model(in_image, patch_out)\n\t# compile model\n# \tmodel.compile(loss='mse', optimizer=Adam(lr=0.0002, beta_1=0.5), loss_weights=[0.5])\n\treturn model","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# generator a resnet block\ndef resnet_block(n_filters, input_layer):\n\t# weight initialization\n\tinit = RandomNormal(stddev=0.02)\n\t# first layer convolutional layer\n\tg = Conv2D(n_filters, (3,3), padding='same', kernel_initializer=init)(input_layer)\n\tg = InstanceNormalization(axis=-1)(g)\n\tg = Activation('relu')(g)\n\t# second convolutional layer\n\tg = Conv2D(n_filters, (3,3), padding='same', kernel_initializer=init)(g)\n\tg = InstanceNormalization(axis=-1)(g)\n\t# concatenate merge channel-wise with input layer\n\tg = Concatenate()([g, input_layer])\n\treturn g","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# define the standalone generator model\ndef define_generator(image_shape=(IMG_HEIGHT,IMG_WIDTH,3), n_resnet=9):\n\t# weight initialization\n\tinit = RandomNormal(stddev=0.02)\n\t# image input\n\tin_image = Input(shape=image_shape)\n\t# c7s1-64\n\tg = Conv2D(64, (7,7), padding='same', kernel_initializer=init)(in_image)\n\tg = InstanceNormalization(axis=-1)(g)\n\tg = Activation('relu')(g)\n\t# d128\n\tg = Conv2D(128, (3,3), strides=(2,2), padding='same', kernel_initializer=init)(g)\n\tg = InstanceNormalization(axis=-1)(g)\n\tg = Activation('relu')(g)\n\t# d256\n\tg = Conv2D(256, (3,3), strides=(2,2), padding='same', kernel_initializer=init)(g)\n\tg = InstanceNormalization(axis=-1)(g)\n\tg = Activation('relu')(g)\n\t# R256\n\tfor _ in range(n_resnet):\n\t\tg = resnet_block(256, g)\n\t# u128\n\tg = Conv2DTranspose(128, (3,3), strides=(2,2), padding='same', kernel_initializer=init)(g)\n\tg = InstanceNormalization(axis=-1)(g)\n\tg = Activation('relu')(g)\n\t# u64\n\tg = Conv2DTranspose(64, (3,3), strides=(2,2), padding='same', kernel_initializer=init)(g)\n\tg = InstanceNormalization(axis=-1)(g)\n\tg = Activation('relu')(g)\n\t# c7s1-3\n\tg = Conv2D(3, (7,7), padding='same', kernel_initializer=init)(g)\n\tg = InstanceNormalization(axis=-1)(g)\n\tout_image = Activation('tanh')(g)\n\t# define model\n\tmodel = Model(in_image, out_image)\n\treturn model","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"tf.keras.backend.clear_session()\n\ngenerator_g = define_generator()\ngenerator_f = define_generator()\n\ndiscriminator_x = define_discriminator()\ndiscriminator_y = define_discriminator()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"generator_g.summary()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"discriminator_x.summary()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# 🌸 Loss\n\nIn CycleGAN, there is no paired data to train on, hence there is no guarantee that the input x and the target y pair are meaningful during training. Thus in order to enforce that the network learns the correct mapping, the authors propose the cycle consistency loss."},{"metadata":{"trusted":true},"cell_type":"code","source":"LAMBDA = 10\n\nloss_obj = tf.keras.losses.BinaryCrossentropy(from_logits=True)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def discriminator_loss(real, generated):\n  real_loss = loss_obj(tf.ones_like(real), real)\n\n  generated_loss = loss_obj(tf.zeros_like(generated), generated)\n\n  total_disc_loss = real_loss + generated_loss\n\n  return total_disc_loss * 0.5","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def generator_loss(generated):\n    return loss_obj(tf.ones_like(generated), generated)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Cycle consistency loss is used. It ensures that the the styled image/result is close to the original input. \n\nThis is an inportant objective function since there ain't any one-to-one mapping between the style and the content images.\n\n"},{"metadata":{"trusted":true},"cell_type":"code","source":"def calc_cycle_loss(real_image, cycled_image):\n    loss1 = tf.reduce_mean(tf.abs(real_image - cycled_image))\n\n    return LAMBDA * loss1","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Itenstity loss ensures that if you fed image Y to generator G, it should yield the real image Y or something similar. For example, if we run the photo(X)-to-monet(Y) model on a monet(Y) then the generated image should be similar to Y."},{"metadata":{"trusted":true},"cell_type":"code","source":"def identity_loss(real_image, same_image):\n    loss = tf.reduce_mean(tf.abs(real_image - same_image))\n    return LAMBDA * 0.5 * loss","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# 🍄 Optimizer\n\nInitialize the optimizers for all the generators and the discriminators."},{"metadata":{"trusted":true},"cell_type":"code","source":"generator_g_optimizer = tf.keras.optimizers.Adam(2e-4, beta_1=0.5)\ngenerator_f_optimizer = tf.keras.optimizers.Adam(2e-4, beta_1=0.5)\n\ndiscriminator_x_optimizer = tf.keras.optimizers.Adam(2e-4, beta_1=0.5)\ndiscriminator_y_optimizer = tf.keras.optimizers.Adam(2e-4, beta_1=0.5)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# 🌀 Train Loop\n\nThe training step consists of four basic steps:\n\n* Get the predictions.\n* Calculate the loss.\n* Calculate the gradients using backpropagation.\n* Apply the gradients to the optimizer.\n"},{"metadata":{"trusted":true},"cell_type":"code","source":"@tf.function\ndef train_step(real_x, real_y):\n  # persistent is set to True because the tape is used more than\n  # once to calculate the gradients.\n  with tf.GradientTape(persistent=True) as tape:\n    # Generator G translates X -> Y\n    # Generator F translates Y -> X.\n    \n    fake_y = generator_g(real_x, training=True)\n    cycled_x = generator_f(fake_y, training=True)\n\n    fake_x = generator_f(real_y, training=True)\n    cycled_y = generator_g(fake_x, training=True)\n\n    # same_x and same_y are used for identity loss.\n    same_x = generator_f(real_x, training=True)\n    same_y = generator_g(real_y, training=True)\n\n    disc_real_x = discriminator_x(real_x, training=True)\n    disc_real_y = discriminator_y(real_y, training=True)\n\n    disc_fake_x = discriminator_x(fake_x, training=True)\n    disc_fake_y = discriminator_y(fake_y, training=True)\n\n    # calculate the loss\n    gen_g_loss = generator_loss(disc_fake_y)\n    gen_f_loss = generator_loss(disc_fake_x)\n    \n    total_cycle_loss = calc_cycle_loss(real_x, cycled_x) + calc_cycle_loss(real_y, cycled_y)\n    \n    # Total generator loss = adversarial loss + cycle loss\n    total_gen_g_loss = gen_g_loss + total_cycle_loss + identity_loss(real_y, same_y)\n    total_gen_f_loss = gen_f_loss + total_cycle_loss + identity_loss(real_x, same_x)\n\n    disc_x_loss = discriminator_loss(disc_real_x, disc_fake_x)\n    disc_y_loss = discriminator_loss(disc_real_y, disc_fake_y)\n    \n  \n  # Calculate the gradients for generator and discriminator\n  generator_g_gradients = tape.gradient(total_gen_g_loss, \n                                        generator_g.trainable_variables)\n  generator_f_gradients = tape.gradient(total_gen_f_loss, \n                                        generator_f.trainable_variables)\n  \n  discriminator_x_gradients = tape.gradient(disc_x_loss, \n                                            discriminator_x.trainable_variables)\n  discriminator_y_gradients = tape.gradient(disc_y_loss, \n                                            discriminator_y.trainable_variables)\n  \n  # Apply the gradients to the optimizer\n  generator_g_optimizer.apply_gradients(zip(generator_g_gradients, \n                                            generator_g.trainable_variables))\n\n  generator_f_optimizer.apply_gradients(zip(generator_f_gradients, \n                                            generator_f.trainable_variables))\n  \n  discriminator_x_optimizer.apply_gradients(zip(discriminator_x_gradients,\n                                                discriminator_x.trainable_variables))\n  \n  discriminator_y_optimizer.apply_gradients(zip(discriminator_y_gradients,\n                                                discriminator_y.trainable_variables))\n\n  return total_gen_g_loss, total_gen_f_loss, disc_x_loss, disc_y_loss","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# 📲 Callbacks\n\nWe have two callbacks:\n\n* `log_generated_images` will log the styled images to W&B dashboard. You can click on the link generated below while training and visualize how the model learns to create better styled images.\n* `save_model` will save the trained weights of the generator and discriminator at the given epoch. These weights will later be logged as W&B artifacts to be used in another kernel to generate submissions."},{"metadata":{"trusted":true},"cell_type":"code","source":"# samples of content images.\nphoto_images = []\nfor i in range(5):\n    photo_images.append(next(iter(photo_ds_eval)))\n\n    \n# Log the styled images to W&B dashboard.\ndef log_generated_images(model, test_inputs, epoch):\n    predictions = []\n    for test_input in test_inputs:\n        prediction = model(test_input)\n        predictions.append(prediction)\n    \n    if epoch==0:\n        wandb.log({'Test Content Images': [wandb.Image(test_input[0]*0.5+0.5) for test_input in test_inputs]})\n        \n    wandb.log({'Styled Images': [wandb.Image(pred[0]*0.5+0.5) for pred in predictions]})\n    \n    \n# Save the trained weights of the generator at the given epoch.\ndef save_model_weights(gen_g, gen_f, epoch):\n    gen_g.save(SAVE_PATH + f'generator_g_checkpoint_{epoch}')\n    gen_f.save(SAVE_PATH + f'generator_f_checkpoint_{epoch}')","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# 🚅 Train with W&B"},{"metadata":{"trusted":true},"cell_type":"code","source":"# Number of epochs to train\nEPOCHS = 30\n\n# create dir to save trained weights\nSAVE_PATH = '../models/'\nos.makedirs(SAVE_PATH, exist_ok=True)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"run = wandb.init(project='Kaggle CycleGAN')\n\nfor epoch in range(EPOCHS):\n    total_gen_g_loss = []\n    total_gen_f_loss = []\n    total_disc_x_loss = []\n    total_disc_y_loss = []\n    \n    start = time.time()\n    \n    n = 0\n    for image_x, image_y in tf.data.Dataset.zip((photo_ds, monet_ds)):\n        gen_g_loss, gen_f_loss, disc_x_loss, disc_y_loss = train_step(image_x, image_y)\n        total_gen_g_loss.append(gen_g_loss)\n        total_gen_f_loss.append(gen_f_loss)\n        total_disc_x_loss.append(disc_x_loss)\n        total_disc_y_loss.append(disc_y_loss)\n        \n        if n % 10 == 0:\n            print ('.', end='')\n        n+=1\n    \n    # Log the styled images to W&B\n    log_generated_images(generator_g, photo_images, epoch)\n    \n    # Save the trained weights after every 5 epochs.\n    if (epoch+1) % 10 == 0:\n        save_model_weights(generator_g,\n                           generator_f,\n                           epoch+1)\n\n    print(f'\\nTime taken for epoch {epoch + 1} is {time.time()-start} sec\\n total gen g loss: {np.mean(total_gen_g_loss)},\\\n    total gen f loss: {np.mean(total_gen_f_loss)}, total disc x loss: {np.mean(total_disc_x_loss)},\\\n    total disc y loss: {np.mean(total_disc_y_loss)}')\n\n    wandb.log({'total_gen_g_loss': np.mean(total_gen_g_loss), \n               'total_gen_f_loss': np.mean(total_gen_f_loss),\n               'total_disc_x _loss': np.mean(total_disc_x_loss),\n               'total_disc_y_loss': np.mean(total_disc_y_loss)})\n    \nrun.join()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# 💾 Save the Weights as W&B Artifacts"},{"metadata":{"trusted":true},"cell_type":"code","source":"run = wandb.init(project='Kaggle CycleGAN', job_type='producer')\n\nartifact = wandb.Artifact('cyclegan_models', type='model')\n\nartifact.add_dir(SAVE_PATH)\n\nrun.log_artifact(artifact)\nrun.join()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# 🎈 Generate Submission"},{"metadata":{"trusted":true},"cell_type":"code","source":"def predict_and_save(input_ds, generator_model, output_path):\n    i = 1\n    for img in tqdm(input_ds):\n        prediction = generator_model(img, training=False)[0].numpy() # make predition\n        prediction = (prediction * 127.5 + 127.5).astype(np.uint8)   # re-scale\n        im = PIL.Image.fromarray(prediction)\n        im.save(f'{output_path}{str(i)}.jpg')\n        i += 1","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"os.makedirs('../images/') # Create folder to save generated images\n\npredict_and_save(photo_ds_eval, generator_g, '../images/')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"import shutil\nshutil.make_archive('/kaggle/working/images/', 'zip', '../images')\n\nprint(f\"Number of generated samples: {len([name for name in os.listdir('../images/') if os.path.isfile(os.path.join('../images/', name))])}\")","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"","execution_count":null,"outputs":[]}],"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":4,"nbformat_minor":4}