{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":21755,"databundleVersionId":1475600,"sourceType":"competition"}],"dockerImageVersionId":31042,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# I’m Something of a Painter Myself - Creating art with GANs\n\nThis notebook explores the application of GANs using the dataset from Jang, Uzsoy, and Culliton (2020). It closely follows the tutorial by Jang (2020) to train a generative adversarial network for transforming photos into Monet-ized paintings. The goal of this project is to learn about GANs and how to apply them practically.\n\n# Exploratory Data Analysis (EDA)\n\nThe first step is to import TensorFlow and set the path to the images. We will use the TFRecords files to read the TFRecords (used by Tensorflow).","metadata":{}},{"cell_type":"code","source":"import tensorflow as tf\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport os\nimport glob\n\nMONET_TFREC_PATH = '/kaggle/input/gan-getting-started/monet_tfrec/*.tfrec'\nPHOTO_TFREC_PATH = '/kaggle/input/gan-getting-started/photo_tfrec/*.tfrec'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-11T05:01:22.846473Z","iopub.execute_input":"2025-07-11T05:01:22.847032Z","iopub.status.idle":"2025-07-11T05:01:22.850580Z","shell.execute_reply.started":"2025-07-11T05:01:22.847011Z","shell.execute_reply":"2025-07-11T05:01:22.849801Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Here, we will set the size to 256x256 (as guided by the data tab from Jang et al.). We also define two methods, the first of which is to parse the TFRecord file. This decodes the image into a JPEG, ensures it is the right size, and converts it to a number between -1 and 1. Finally, if augment=True (which is used for training only), we will apply random flips to the image to aid in training.\n\nThe second method loads the entire dataset based on the path. It then iterates over the files and creates a TFRecord dataset using the method above.","metadata":{}},{"cell_type":"code","source":"IMAGE_SIZE = [256, 256]\n\ndef parse_tfrecord(example, image_size=(256, 256), augment=False):\n    features = {'image': tf.io.FixedLenFeature([], tf.string)}\n    example = tf.io.parse_single_example(example, features)\n    image = tf.io.decode_jpeg(example['image'], channels=3)\n    image = tf.image.resize(image, image_size)\n    image = (tf.cast(image, tf.float32) / 127.5) - 1\n\n    if augment:\n        image = tf.image.random_flip_left_right(image)\n    \n    return image\n\ndef load_tfrecord_dataset(filenames, batch_size=1, shuffle=False, buffer_size=2048, image_size=(256, 256), augment=False):\n    dataset = tf.data.TFRecordDataset(filenames, num_parallel_reads=tf.data.AUTOTUNE)\n    dataset = dataset.map(lambda x: parse_tfrecord(x, image_size=image_size, augment=augment),\n                          num_parallel_calls=tf.data.AUTOTUNE)\n    if shuffle:\n        dataset = dataset.shuffle(buffer_size)\n    dataset = dataset.batch(batch_size)\n    dataset = dataset.prefetch(tf.data.AUTOTUNE)\n    return dataset\n\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-07-11T05:01:25.453676Z","iopub.execute_input":"2025-07-11T05:01:25.453950Z","iopub.status.idle":"2025-07-11T05:01:25.460562Z","shell.execute_reply.started":"2025-07-11T05:01:25.453931Z","shell.execute_reply":"2025-07-11T05:01:25.459893Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Glob finds a list of files give the path expression. We then load the money dataset and the photo dataset and finally we just print the number of records (to ensure they match as expected).","metadata":{}},{"cell_type":"code","source":"monet_filenames = tf.io.gfile.glob(MONET_TFREC_PATH)\nphoto_filenames = tf.io.gfile.glob(PHOTO_TFREC_PATH)\n\nmonet_dataset_eda = load_tfrecord_dataset(monet_filenames, batch_size=5, shuffle=False)\nphoto_dataset_eda = load_tfrecord_dataset(photo_filenames, batch_size=5, shuffle=False)\n\nprint(f'Monet TFRecords: {len(monet_filenames)}')\nprint(f'Photo TFRecords: {len(photo_filenames)}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-11T05:01:36.497576Z","iopub.execute_input":"2025-07-11T05:01:36.497831Z","iopub.status.idle":"2025-07-11T05:01:37.784148Z","shell.execute_reply.started":"2025-07-11T05:01:36.497813Z","shell.execute_reply":"2025-07-11T05:01:37.783324Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"We display some samples of images (both Monet and photos). This is just a sanitary check for our own records.","metadata":{}},{"cell_type":"code","source":"def show_samples(dataset, title, num_images=5):\n    plt.figure(figsize=(15, 3))\n    for i, image_batch in enumerate(dataset.take(1)):\n        for j in range(num_images):\n            image = image_batch[j]\n            image = (image + 1) / 2  # Rescale to [0, 1] for display\n            plt.subplot(1, num_images, j + 1)\n            plt.imshow(image)\n            plt.axis('off')\n    plt.suptitle(title)\n    plt.show()\n\nshow_samples(monet_dataset_eda, \"Sample Monet Images\")\nshow_samples(photo_dataset_eda, \"Sample Photo Images\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-11T05:01:46.898550Z","iopub.execute_input":"2025-07-11T05:01:46.899316Z","iopub.status.idle":"2025-07-11T05:01:47.683882Z","shell.execute_reply.started":"2025-07-11T05:01:46.899282Z","shell.execute_reply":"2025-07-11T05:01:47.683244Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Ok, we have some sample images, but let's see how they differ side-by-side.","metadata":{}},{"cell_type":"code","source":"def show_side_by_side(photo_ds, monet_ds, n=5):\n    photo_iter = iter(photo_ds.unbatch().shuffle(1000).batch(1))\n    monet_iter = iter(monet_ds.unbatch().shuffle(1000).batch(1))\n\n    plt.figure(figsize=(12, 4 * n))\n    for i in range(n):\n        photo = next(photo_iter)[0]\n        monet = next(monet_iter)[0]\n        photo = (photo + 1) / 2\n        monet = (monet + 1) / 2\n\n        plt.subplot(n, 2, 2*i + 1)\n        plt.imshow(photo)\n        plt.title('Photo')\n        plt.axis('off')\n\n        plt.subplot(n, 2, 2*i + 2)\n        plt.imshow(monet)\n        plt.title('Monet')\n        plt.axis('off')\n    plt.show()\n\nshow_side_by_side(photo_dataset_eda, monet_dataset_eda)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-11T05:01:51.045026Z","iopub.execute_input":"2025-07-11T05:01:51.045276Z","iopub.status.idle":"2025-07-11T05:01:53.249167Z","shell.execute_reply.started":"2025-07-11T05:01:51.045258Z","shell.execute_reply":"2025-07-11T05:01:53.248332Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"We can see that the Monets are very different, but they also share some similar patterns. For example, the Monets have coastland, rivers, and trees. The photos also have some rivers, grass, and trees.\n\nWe want to inspect the shape of the dataset image to ensure it is as expected (i.e., batch size, 256x256, three channels).","metadata":{}},{"cell_type":"code","source":"for image_batch in monet_dataset_eda.take(1):\n    print(\"Monet batch shape:\", image_batch.shape)\n\nfor image_batch in photo_dataset_eda.take(1):\n    print(\"Photo batch shape:\", image_batch.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-11T05:01:55.748816Z","iopub.execute_input":"2025-07-11T05:01:55.749107Z","iopub.status.idle":"2025-07-11T05:01:55.833187Z","shell.execute_reply.started":"2025-07-11T05:01:55.749087Z","shell.execute_reply":"2025-07-11T05:01:55.832376Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"What we want to visualize here is the difference between the monet and photo.","metadata":{}},{"cell_type":"code","source":"def plot_pixel_histogram(dataset, title):\n    all_pixels = []\n    for image_batch in dataset.take(5):\n        images = (image_batch + 1) / 2  # Back to [0,1]\n        all_pixels.extend(images.numpy().flatten())\n\n    plt.figure(figsize=(6,4))\n    plt.hist(all_pixels, bins=50, color='blue', alpha=0.7)\n    plt.title(title)\n    plt.xlabel('Pixel value')\n    plt.ylabel('Frequency')\n    plt.show()\n\nplot_pixel_histogram(monet_dataset_eda, 'Monet Pixel Distribution')\nplot_pixel_histogram(photo_dataset_eda, 'Photo Pixel Distribution')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-11T05:02:00.788209Z","iopub.execute_input":"2025-07-11T05:02:00.788743Z","iopub.status.idle":"2025-07-11T05:02:20.622300Z","shell.execute_reply.started":"2025-07-11T05:02:00.788712Z","shell.execute_reply":"2025-07-11T05:02:20.621367Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"We can see that the photo distribution is heavily skewed towards 0, whereas the monet distribution seems to be more normal.\n\nLet's break that down into the three channels.","metadata":{}},{"cell_type":"code","source":"def plot_color_histogram(images, title):\n    r, g, b = [], [], []\n    for img in images.unbatch().take(100):\n        img = (img + 1) / 2  # back to [0,1]\n        r.extend(img[:,:,0].numpy().flatten())\n        g.extend(img[:,:,1].numpy().flatten())\n        b.extend(img[:,:,2].numpy().flatten())\n\n    plt.figure(figsize=(8,4))\n    plt.hist(r, bins=50, color='red', alpha=0.5, label='R')\n    plt.hist(g, bins=50, color='green', alpha=0.5, label='G')\n    plt.hist(b, bins=50, color='blue', alpha=0.5, label='B')\n    plt.title(f'{title} Color Channel Histogram')\n    plt.legend()\n    plt.show()\n\nplot_color_histogram(photo_dataset_eda, 'Photo')\nplot_color_histogram(monet_dataset_eda, 'Monet')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-11T05:03:39.231173Z","iopub.execute_input":"2025-07-11T05:03:39.231811Z","iopub.status.idle":"2025-07-11T05:04:56.363499Z","shell.execute_reply.started":"2025-07-11T05:03:39.231792Z","shell.execute_reply":"2025-07-11T05:04:56.362656Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"We can see the palette shift from photo to monet. This means a GAN should be able to learn it.\n\nWhat we lastly want to check for is edge detection. This will help us see how complex/detailed they are. We'll take a small sample to check the photos versus the monet.","metadata":{}},{"cell_type":"code","source":"def show_edges(dataset, title):\n    plt.figure(figsize=(15, 3))\n    for i, img_batch in enumerate(dataset.take(1)):\n        for j in range(5):\n            img = img_batch[j]\n            img = (img + 1) / 2\n            img_gray = tf.image.rgb_to_grayscale(img)\n            img_gray = tf.image.resize(img_gray, [256, 256])\n            edges = tf.image.sobel_edges(tf.expand_dims(img_gray, axis=0))\n            edges = edges[0]\n            edge_magnitude = tf.sqrt(tf.reduce_sum(tf.square(edges), axis=-1))[...,0]\n\n            plt.subplot(1, 5, j+1)\n            plt.imshow(edge_magnitude, cmap='gray')\n            plt.axis('off')\n    plt.suptitle(title)\n    plt.show()\n\nshow_edges(photo_dataset_eda, 'Photo Edges')\nshow_edges(monet_dataset_eda, 'Monet Edges')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-11T05:04:56.364702Z","iopub.execute_input":"2025-07-11T05:04:56.364944Z","iopub.status.idle":"2025-07-11T05:04:58.066130Z","shell.execute_reply.started":"2025-07-11T05:04:56.364928Z","shell.execute_reply":"2025-07-11T05:04:58.065399Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Examining the edges, it appears that there shouldn't be too much of a difference, which suggests that this is something our GAN can learn.\n\n# Plan of Analysis\n\nBased on the samples taken above, we can see there is a visual difference between photos and monet paintings. We can also see that the edges are reasonably similar, which means it shouldn't be too complex for a GAN to learn. Lastly, we can see there is a color palette difference, which is also something a GAN can learn.\n\nBecause we want to transform from one image to another without the paired data, we'll need to use Cycle GANs, which employ the generator-discriminator pattern to learn from each other.\n\n# Preprocessing\n\nWe will load the entire dataset for training and apply augmentation (random image flipping).","metadata":{}},{"cell_type":"code","source":"# Imports\nimport tensorflow as tf\nimport glob\n\n\nBATCH_SIZE = 1\nmonet_dataset_train = load_tfrecord_dataset(monet_filenames, batch_size=BATCH_SIZE, shuffle=True, augment=True)\nphoto_dataset_train = load_tfrecord_dataset(photo_filenames, batch_size=BATCH_SIZE, shuffle=True, augment=True)\n\nfor monet_batch in monet_dataset_train.take(1):\n    print(\"Monet batch shape:\", monet_batch.shape)\n\nfor photo_batch in photo_dataset_train.take(1):\n    print(\"Photo batch shape:\", photo_batch.shape)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-11T05:05:10.245237Z","iopub.execute_input":"2025-07-11T05:05:10.245509Z","iopub.status.idle":"2025-07-11T05:05:12.676967Z","shell.execute_reply.started":"2025-07-11T05:05:10.245489Z","shell.execute_reply":"2025-07-11T05:05:12.676287Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Model Architecture\n\nWe are following Jang's (2020) tutorial, and she is creating a cycle GAN. It has been slightly modified to make it easier to follow.\n\n## Generator\n\nWe're building the generator using downsampling and the encoder to extract features. We then use Residual Blocks to learn the residual functions (transforming the features) and finally upsampling with the decoder. Since our images are in the range of -1 to 1, we can use the tanh function as the final output function for the pixel.\n\nWe will create a generator that can transform a photo into a Monet-style image and vice versa.","metadata":{}},{"cell_type":"code","source":"from tensorflow.keras import layers\n\ndef resnet_block(x, filters):\n    y = layers.Conv2D(filters, (3, 3), padding='same')(x)\n    y = layers.GroupNormalization(groups=1, axis=-1)(y)\n    y = layers.Activation('relu')(y)\n    y = layers.Conv2D(filters, (3, 3), padding='same')(y)\n    y = layers.GroupNormalization(groups=1, axis=-1)(y)\n    return layers.add([x, y])\n\ndef build_generator(image_shape=(256, 256, 3), n_res_blocks=9):\n    inputs = layers.Input(shape=image_shape)\n\n    # Encoder\n    x = layers.Conv2D(64, (7, 7), padding='same')(inputs)\n    x = layers.GroupNormalization(groups=1, axis=-1)(x)\n    x = layers.Activation('relu')(x)\n\n    x = layers.Conv2D(128, (3, 3), strides=2, padding='same')(x)\n    x = layers.GroupNormalization(groups=1, axis=-1)(x)\n    x = layers.Activation('relu')(x)\n\n    x = layers.Conv2D(256, (3, 3), strides=2, padding='same')(x)\n    x = layers.GroupNormalization(groups=1, axis=-1)(x)\n    x = layers.Activation('relu')(x)\n\n    # ResNet Blocks\n    for _ in range(n_res_blocks):\n        x = resnet_block(x, 256)\n\n    # Decoder\n    x = layers.Conv2DTranspose(128, (3, 3), strides=2, padding='same')(x)\n    x = layers.GroupNormalization(groups=1, axis=-1)(x)\n    x = layers.Activation('relu')(x)\n\n    x = layers.Conv2DTranspose(64, (3, 3), strides=2, padding='same')(x)\n    x = layers.GroupNormalization(groups=1, axis=-1)(x)\n    x = layers.Activation('relu')(x)\n\n    x = layers.Conv2D(3, (7, 7), padding='same', activation='tanh')(x)\n\n    return tf.keras.Model(inputs, x)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-11T05:05:16.323216Z","iopub.execute_input":"2025-07-11T05:05:16.323491Z","iopub.status.idle":"2025-07-11T05:05:16.388747Z","shell.execute_reply.started":"2025-07-11T05:05:16.323472Z","shell.execute_reply":"2025-07-11T05:05:16.388214Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Discriminator\n\nThe discriminator has to classify whether the image is real or fake. In this case, we are using a PatchGAN design, a common approach for CycleGANs. This will help the discriminator focus on the features of the image, such as brushstrokes. ","metadata":{}},{"cell_type":"code","source":"def build_discriminator(image_shape=(256, 256, 3)):\n    inputs = layers.Input(shape=image_shape)\n\n    x = layers.Conv2D(64, (4, 4), strides=2, padding='same')(inputs)\n    x = layers.LeakyReLU(0.2)(x)\n\n    x = layers.Conv2D(128, (4, 4), strides=2, padding='same')(x)\n    x = layers.GroupNormalization(groups=1, axis=-1)(x)\n    x = layers.LeakyReLU(0.2)(x)\n\n    x = layers.Conv2D(256, (4, 4), strides=2, padding='same')(x)\n    x = layers.GroupNormalization(groups=1, axis=-1)(x)\n    x = layers.LeakyReLU(0.2)(x)\n\n    x = layers.Conv2D(512, (4, 4), strides=1, padding='same')(x)\n    x = layers.GroupNormalization(groups=1, axis=-1)(x)\n    x = layers.LeakyReLU(0.2)(x)\n\n    x = layers.Conv2D(1, (4, 4), strides=1, padding='same')(x)\n\n    return tf.keras.Model(inputs, x)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-11T05:05:19.276636Z","iopub.execute_input":"2025-07-11T05:05:19.277248Z","iopub.status.idle":"2025-07-11T05:05:19.285366Z","shell.execute_reply.started":"2025-07-11T05:05:19.277221Z","shell.execute_reply":"2025-07-11T05:05:19.284338Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Now is the time to build the model. With CycleGAN, we need to cycle through the generators and discriminators.","metadata":{}},{"cell_type":"code","source":"generator_pm = build_generator()  # Photo → Monet\ngenerator_mp = build_generator()  # Monet → Photo\n\ndiscriminator_p = build_discriminator()  # Discriminates Photos\ndiscriminator_m = build_discriminator()  # Discriminates Monet","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-11T05:05:22.158101Z","iopub.execute_input":"2025-07-11T05:05:22.158668Z","iopub.status.idle":"2025-07-11T05:05:23.473318Z","shell.execute_reply.started":"2025-07-11T05:05:22.158648Z","shell.execute_reply":"2025-07-11T05:05:23.472772Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Loss Functions\n\nNow, we will define the loss functions and constants.\n\n* Adversarial loss: Loss function for trying to make fake look authentic.\n* Discriminator loss: Loss function for trying to determine what is fake vs. real.\n* Cycle-consistency loss: Ensuring that the round-trip looks like the original.\n* Identity loss: Don't change images already in the target style.\n\nWe also use the suggested Adam optimizers for CycleGAN.","metadata":{}},{"cell_type":"code","source":"import tensorflow as tf\n\n# ✅ Constants for weighting\nLAMBDA_CYCLE = 10.0\nLAMBDA_ID = 5.0\n\n# ✅ Binary Crossentropy for real vs. fake\nloss_obj = tf.keras.losses.BinaryCrossentropy(from_logits=True)\n\n# ✅ Adversarial Loss (Generator wants Discriminator to say 'real')\ndef generator_adversarial_loss(fake_output):\n    return loss_obj(tf.ones_like(fake_output), fake_output)\n\n# ✅ Discriminator Loss (real should be 1, fake should be 0)\ndef discriminator_loss(real_output, fake_output):\n    real_loss = loss_obj(tf.ones_like(real_output), real_output)\n    fake_loss = loss_obj(tf.zeros_like(fake_output), fake_output)\n    return (real_loss + fake_loss) * 0.5\n\n# ✅ Cycle-consistency Loss\ndef calc_cycle_loss(real_image, cycled_image):\n    loss = tf.reduce_mean(tf.abs(real_image - cycled_image))\n    return LAMBDA_CYCLE * loss\n\n# ✅ Identity Loss\ndef identity_loss(real_image, same_image):\n    loss = tf.reduce_mean(tf.abs(real_image - same_image))\n    return LAMBDA_ID * loss\n\n# ✅ Optimizers (standard Adam settings for CycleGAN)\ngenerator_pm_optimizer = tf.keras.optimizers.Adam(2e-4, beta_1=0.5)\ngenerator_mp_optimizer = tf.keras.optimizers.Adam(2e-4, beta_1=0.5)\ndiscriminator_p_optimizer = tf.keras.optimizers.Adam(2e-4, beta_1=0.5)\ndiscriminator_m_optimizer = tf.keras.optimizers.Adam(2e-4, beta_1=0.5)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-11T05:05:27.190608Z","iopub.execute_input":"2025-07-11T05:05:27.191156Z","iopub.status.idle":"2025-07-11T05:05:27.208315Z","shell.execute_reply.started":"2025-07-11T05:05:27.191138Z","shell.execute_reply":"2025-07-11T05:05:27.207619Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"We want to add some checkpointing, so that we can restore the weights later if we interrupt the training and need to come back.","metadata":{}},{"cell_type":"code","source":"# 1. Define Checkpoint\nckpt = tf.train.Checkpoint(generator_pm=generator_pm,\n                           generator_mp=generator_mp,\n                           discriminator_p=discriminator_p,\n                           discriminator_m=discriminator_m,\n                           generator_pm_optimizer=generator_pm_optimizer,\n                           generator_mp_optimizer=generator_mp_optimizer,\n                           discriminator_p_optimizer=discriminator_p_optimizer,\n                           discriminator_m_optimizer=discriminator_m_optimizer)\n\n# 2. Define Manager\nckpt_manager = tf.train.CheckpointManager(ckpt, './checkpoints', max_to_keep=5)\n\n# 3. Restore if available\nif ckpt_manager.latest_checkpoint:\n    ckpt.restore(ckpt_manager.latest_checkpoint)\n    print(f\"Restored from {ckpt_manager.latest_checkpoint}\")\nelse:\n    print(\"Starting training from scratch.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-11T05:05:30.354269Z","iopub.execute_input":"2025-07-11T05:05:30.355042Z","iopub.status.idle":"2025-07-11T05:05:30.361068Z","shell.execute_reply.started":"2025-07-11T05:05:30.354989Z","shell.execute_reply":"2025-07-11T05:05:30.360317Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Results and Analysis\n\nIn this section, we will run the models and begin optimizing against them. For this project, only one model type is available (Cycle GAN), as explained above.\n\n## V1. Starting Model\n\nNow that we have the model architecture in place, let's begin training.","metadata":{}},{"cell_type":"code","source":"@tf.function\ndef train_step(real_p, real_m):\n    with tf.GradientTape(persistent=True) as tape:\n        # --------------------------------\n        # Generator_PM: Photo -> Monet\n        # --------------------------------\n        fake_m = generator_pm(real_p, training=True)\n        cycled_p = generator_mp(fake_m, training=True)\n\n        # --------------------------------\n        # Generator_MP: Monet -> Photo\n        # --------------------------------\n        fake_p = generator_mp(real_m, training=True)\n        cycled_m = generator_pm(fake_p, training=True)\n\n        # --------------------------------\n        # Identity Mapping\n        # --------------------------------\n        same_p = generator_mp(real_p, training=True)\n        same_m = generator_pm(real_m, training=True)\n\n        # --------------------------------\n        # Discriminator Outputs\n        # --------------------------------\n        disc_real_p = discriminator_p(real_p, training=True)\n        disc_real_m = discriminator_m(real_m, training=True)\n        disc_fake_p = discriminator_p(fake_p, training=True)\n        disc_fake_m = discriminator_m(fake_m, training=True)\n\n        # --------------------------------\n        # Generator Losses\n        # --------------------------------\n        gen_pm_loss = generator_adversarial_loss(disc_fake_m)\n        gen_mp_loss = generator_adversarial_loss(disc_fake_p)\n\n        total_cycle_loss = calc_cycle_loss(real_p, cycled_p) + calc_cycle_loss(real_m, cycled_m)\n\n        total_gen_pm_loss = gen_pm_loss + total_cycle_loss + identity_loss(real_m, same_m)\n        total_gen_mp_loss = gen_mp_loss + total_cycle_loss + identity_loss(real_p, same_p)\n\n        # --------------------------------\n        # Discriminator Losses\n        # --------------------------------\n        disc_p_loss = discriminator_loss(disc_real_p, disc_fake_p)\n        disc_m_loss = discriminator_loss(disc_real_m, disc_fake_m)\n\n    # --------------------------------\n    # Calculate and Apply Gradients\n    # --------------------------------\n    generator_pm_gradients = tape.gradient(total_gen_pm_loss, generator_pm.trainable_variables)\n    generator_mp_gradients = tape.gradient(total_gen_mp_loss, generator_mp.trainable_variables)\n\n    discriminator_p_gradients = tape.gradient(disc_p_loss, discriminator_p.trainable_variables)\n    discriminator_m_gradients = tape.gradient(disc_m_loss, discriminator_m.trainable_variables)\n\n    generator_pm_optimizer.apply_gradients(zip(generator_pm_gradients, generator_pm.trainable_variables))\n    generator_mp_optimizer.apply_gradients(zip(generator_mp_gradients, generator_mp.trainable_variables))\n\n    discriminator_p_optimizer.apply_gradients(zip(discriminator_p_gradients, discriminator_p.trainable_variables))\n    discriminator_m_optimizer.apply_gradients(zip(discriminator_m_gradients, discriminator_m.trainable_variables))\n\n    return {\n        'gen_pm_loss': total_gen_pm_loss,\n        'gen_mp_loss': total_gen_mp_loss,\n        'disc_p_loss': disc_p_loss,\n        'disc_m_loss': disc_m_loss\n    }","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-11T05:05:34.390614Z","iopub.execute_input":"2025-07-11T05:05:34.391430Z","iopub.status.idle":"2025-07-11T05:05:34.398727Z","shell.execute_reply.started":"2025-07-11T05:05:34.391406Z","shell.execute_reply":"2025-07-11T05:05:34.398045Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import time\n\ndef train(dataset_P, dataset_M, epochs=1):\n    history = {\n        'gen_pm_loss': [],\n        'gen_mp_loss': [],\n        'disc_p_loss': [],\n        'disc_m_loss': []\n    }\n    \n    for epoch in range(epochs):\n        start = time.time()\n        print(f\"Epoch {epoch+1}/{epochs}\")\n        \n        gen_pm_losses = []\n        gen_mp_losses = []\n        disc_p_losses = []\n        disc_m_losses = []\n        \n        for real_p, real_m in tf.data.Dataset.zip((dataset_P, dataset_M)):\n            losses = train_step(real_p, real_m)\n            gen_pm_losses.append(losses['gen_pm_loss'].numpy())\n            gen_mp_losses.append(losses['gen_mp_loss'].numpy())\n            disc_p_losses.append(losses['disc_p_loss'].numpy())\n            disc_m_losses.append(losses['disc_m_loss'].numpy())\n        \n        # Average for the epoch\n        history['gen_pm_loss'].append(np.mean(gen_pm_losses))\n        history['gen_mp_loss'].append(np.mean(gen_mp_losses))\n        history['disc_p_loss'].append(np.mean(disc_p_losses))\n        history['disc_m_loss'].append(np.mean(disc_m_losses))\n        \n        print(f\"Time for epoch {epoch+1}: {time.time() - start:.2f} sec\")\n        print(f\"Gen_PM Loss: {history['gen_pm_loss'][-1]:.4f} | Gen_MP Loss: {history['gen_mp_loss'][-1]:.4f}\")\n        print(f\"Disc_P Loss: {history['disc_p_loss'][-1]:.4f} | Disc_M Loss: {history['disc_m_loss'][-1]:.4f}\\n\")\n        \n        ckpt_save_path = ckpt_manager.save()\n        print(f\"✅ Checkpoint saved at {ckpt_save_path}\\n\")\n    \n    return history","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-11T05:05:49.761373Z","iopub.execute_input":"2025-07-11T05:05:49.761646Z","iopub.status.idle":"2025-07-11T05:05:49.768364Z","shell.execute_reply.started":"2025-07-11T05:05:49.761626Z","shell.execute_reply":"2025-07-11T05:05:49.767705Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"history = train(photo_dataset_train, monet_dataset_train, epochs=10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-11T05:05:53.496593Z","iopub.execute_input":"2025-07-11T05:05:53.497191Z","iopub.status.idle":"2025-07-11T05:29:41.083060Z","shell.execute_reply.started":"2025-07-11T05:05:53.497170Z","shell.execute_reply":"2025-07-11T05:29:41.081881Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\nplt.figure(figsize=(12, 6))\nplt.plot(history['gen_pm_loss'], label='Generator Photo→Monet Loss')\nplt.plot(history['gen_mp_loss'], label='Generator Monet→Photo Loss')\nplt.plot(history['disc_p_loss'], label='Discriminator Photo Loss')\nplt.plot(history['disc_m_loss'], label='Discriminator Monet Loss')\nplt.title('CycleGAN Training Loss')\nplt.xlabel('Epoch')\nplt.ylabel('Loss')\nplt.legend()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-11T05:42:56.286333Z","iopub.execute_input":"2025-07-11T05:42:56.287022Z","iopub.status.idle":"2025-07-11T05:42:56.480456Z","shell.execute_reply.started":"2025-07-11T05:42:56.286983Z","shell.execute_reply":"2025-07-11T05:42:56.479776Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Now let's run the image generation.","metadata":{}},{"cell_type":"code","source":"import random\nimport matplotlib.pyplot as plt\n\n# Define denormalization helper\ndef denormalize_image(image):\n    image = (image + 1) * 127.5\n    image = tf.clip_by_value(image, 0, 255)\n    return tf.cast(image, tf.uint8)\n\n# Take 5 random photos from training dataset\nphoto_batches = list(photo_dataset_train)\nrandom.shuffle(photo_batches)\nphoto_samples = [batch[0] for batch in photo_batches[:5]]\n\n# Monetify\nmonetified_samples = []\nfor photo in photo_samples:\n    monetified = generator_pm(tf.expand_dims(photo, 0), training=False)[0]\n    monetified_samples.append(denormalize_image(monetified).numpy())\n\n# Plot\nplt.figure(figsize=(15, 6))\nfor i in range(5):\n    plt.subplot(2, 5, i + 1)\n    plt.imshow(denormalize_image(photo_samples[i]).numpy())\n    plt.title('Photo')\n    plt.axis('off')\n    \n    plt.subplot(2, 5, i + 6)\n    plt.imshow(monetified_samples[i])\n    plt.title('Monetified')\n    plt.axis('off')\n\nplt.suptitle('Random Photo Samples Monetified', fontsize=16)\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-11T05:43:00.003227Z","iopub.execute_input":"2025-07-11T05:43:00.003464Z","iopub.status.idle":"2025-07-11T05:43:09.857020Z","shell.execute_reply.started":"2025-07-11T05:43:00.003449Z","shell.execute_reply":"2025-07-11T05:43:09.856341Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import os\n# from PIL import Image\n# import numpy as np\n\n# output_dir = '/kaggle/working/generated_images_model_v1'\n# os.makedirs(output_dir, exist_ok=True)\n\n# index = 0\n\n# for photo_batch in photo_dataset_train:\n#     generated_batch = generator_pm(photo_batch, training=False)\n    \n#     # Process each image in the batch (in case batch_size > 1)\n#     for i in range(generated_batch.shape[0]):\n#         generated = denormalize_image(generated_batch[i]).numpy()\n#         img = Image.fromarray(generated)\n#         img.convert('RGB').save(os.path.join(output_dir, f'{index}.jpg'), 'JPEG')\n#         index += 1\n\n# print(f\"✅ Saved {index} generated Monet-style images to: {output_dir}\")\n\n# import zipfile\n\n# submission_zip = '/kaggle/working/images.zip'\n\n# with zipfile.ZipFile(submission_zip, 'w') as zipf:\n#     for i in range(index):\n#         zipf.write(os.path.join(output_dir, f'{i}.jpg'), arcname=f'{i}.jpg')\n\n# print(f\"✅ Created images zip: {submission_zip}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-09T04:50:39.755809Z","iopub.execute_input":"2025-07-09T04:50:39.756092Z","iopub.status.idle":"2025-07-09T04:50:39.766549Z","shell.execute_reply.started":"2025-07-09T04:50:39.756071Z","shell.execute_reply":"2025-07-09T04:50:39.765687Z"},"_kg_hide-output":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The first version of the model got a score of 87.63909. What we need to do now is optimize the model to see how we can improve it.\n\n## V2. Use More Data Augmentation\n\nCurrently, the only thing we do is flip the images. What I will add is changing the brightness and the contrast.","metadata":{}},{"cell_type":"code","source":"# 1. Define Checkpoint\nckpt = tf.train.Checkpoint(generator_pm=generator_pm,\n                           generator_mp=generator_mp,\n                           discriminator_p=discriminator_p,\n                           discriminator_m=discriminator_m,\n                           generator_pm_optimizer=generator_pm_optimizer,\n                           generator_mp_optimizer=generator_mp_optimizer,\n                           discriminator_p_optimizer=discriminator_p_optimizer,\n                           discriminator_m_optimizer=discriminator_m_optimizer)\n\n# 2. Define Manager\nckpt_manager = tf.train.CheckpointManager(ckpt, './checkpoints_v2', max_to_keep=5)\n\n# 3. Restore if available\nif ckpt_manager.latest_checkpoint:\n    ckpt.restore(ckpt_manager.latest_checkpoint)\n    print(f\"Restored from {ckpt_manager.latest_checkpoint}\")\nelse:\n    print(\"Starting training from scratch.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-11T05:43:28.059727Z","iopub.execute_input":"2025-07-11T05:43:28.059969Z","iopub.status.idle":"2025-07-11T05:43:28.068186Z","shell.execute_reply.started":"2025-07-11T05:43:28.059952Z","shell.execute_reply":"2025-07-11T05:43:28.067449Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def parse_tfrecord_v2(example, image_size=(256, 256), augment=False):\n    features = {'image': tf.io.FixedLenFeature([], tf.string)}\n    example = tf.io.parse_single_example(example, features)\n    image = tf.io.decode_jpeg(example['image'], channels=3)\n    image = tf.image.resize(image, image_size)\n    image = (tf.cast(image, tf.float32) / 127.5) - 1\n\n    if augment:\n        image = tf.image.random_flip_left_right(image)\n        image = tf.image.random_brightness(image, 0.1)\n        image = tf.image.random_contrast(image, 0.9, 1.1)\n    \n    return image\n\ndef load_tfrecord_dataset_v2(filenames, batch_size=1, shuffle=False, buffer_size=2048, image_size=(256, 256), augment=False):\n    dataset = tf.data.TFRecordDataset(filenames, num_parallel_reads=tf.data.AUTOTUNE)\n    dataset = dataset.map(lambda x: parse_tfrecord_v2(x, image_size=image_size, augment=augment),\n                          num_parallel_calls=tf.data.AUTOTUNE)\n    if shuffle:\n        dataset = dataset.shuffle(buffer_size)\n    dataset = dataset.batch(batch_size)\n    dataset = dataset.prefetch(tf.data.AUTOTUNE)\n    return dataset","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-11T05:43:35.112225Z","iopub.execute_input":"2025-07-11T05:43:35.112703Z","iopub.status.idle":"2025-07-11T05:43:35.118930Z","shell.execute_reply.started":"2025-07-11T05:43:35.112683Z","shell.execute_reply":"2025-07-11T05:43:35.118286Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Imports\nimport tensorflow as tf\nimport glob\n\n\nBATCH_SIZE = 1\nmonet_dataset_train_v2 = load_tfrecord_dataset_v2(monet_filenames, batch_size=BATCH_SIZE, shuffle=True, augment=True)\nphoto_dataset_train_v2 = load_tfrecord_dataset_v2(photo_filenames, batch_size=BATCH_SIZE, shuffle=True, augment=True)\n\nhistory_v2 = train(photo_dataset_train_v2, monet_dataset_train_v2, epochs=10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-11T05:43:43.528678Z","iopub.execute_input":"2025-07-11T05:43:43.529237Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\nplt.figure(figsize=(12, 6))\nplt.plot(history_v2['gen_pm_loss'], label='Generator Photo→Monet Loss')\nplt.plot(history_v2['gen_mp_loss'], label='Generator Monet→Photo Loss')\nplt.plot(history_v2['disc_p_loss'], label='Discriminator Photo Loss')\nplt.plot(history_v2['disc_m_loss'], label='Discriminator Monet Loss')\nplt.title('CycleGAN Training Loss')\nplt.xlabel('Epoch')\nplt.ylabel('Loss')\nplt.legend()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-11T06:22:36.263629Z","iopub.execute_input":"2025-07-11T06:22:36.264307Z","iopub.status.idle":"2025-07-11T06:22:36.447406Z","shell.execute_reply.started":"2025-07-11T06:22:36.264284Z","shell.execute_reply":"2025-07-11T06:22:36.446529Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"We can see from the history above that the loss does decrease over time; however, how will it perform when we generate images and obtain the MFID score?","metadata":{}},{"cell_type":"code","source":"import random\nimport matplotlib.pyplot as plt\n\n# Define denormalization helper\ndef denormalize_image(image):\n    image = (image + 1) * 127.5\n    image = tf.clip_by_value(image, 0, 255)\n    return tf.cast(image, tf.uint8)\n\n# Take 5 random photos from training dataset\nphoto_batches = list(photo_dataset_train_v2)\nrandom.shuffle(photo_batches)\nphoto_samples = [batch[0] for batch in photo_batches[:5]]\n\n# Monetify\nmonetified_samples = []\nfor photo in photo_samples:\n    monetified = generator_pm(tf.expand_dims(photo, 0), training=False)[0]\n    monetified_samples.append(denormalize_image(monetified).numpy())\n\n# Plot\nplt.figure(figsize=(15, 6))\nfor i in range(5):\n    plt.subplot(2, 5, i + 1)\n    plt.imshow(denormalize_image(photo_samples[i]).numpy())\n    plt.title('Photo')\n    plt.axis('off')\n    \n    plt.subplot(2, 5, i + 6)\n    plt.imshow(monetified_samples[i])\n    plt.title('Monetified')\n    plt.axis('off')\n\nplt.suptitle('Random Photo Samples Monetified', fontsize=16)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-11T06:22:56.803188Z","iopub.execute_input":"2025-07-11T06:22:56.803457Z","iopub.status.idle":"2025-07-11T06:23:06.275065Z","shell.execute_reply.started":"2025-07-11T06:22:56.803437Z","shell.execute_reply":"2025-07-11T06:23:06.273990Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import os\n# from PIL import Image\n# import numpy as np\n\n# output_dir = '/kaggle/working/generated_images_model_v2'\n# os.makedirs(output_dir, exist_ok=True)\n\n# index = 0\n\n# for photo_batch in photo_dataset_train:\n#     generated_batch = generator_pm(photo_batch, training=False)\n    \n#     # Process each image in the batch (in case batch_size > 1)\n#     for i in range(generated_batch.shape[0]):\n#         generated = denormalize_image(generated_batch[i]).numpy()\n#         img = Image.fromarray(generated)\n#         img.convert('RGB').save(os.path.join(output_dir, f'{index}.jpg'), 'JPEG')\n#         index += 1\n\n# print(f\"✅ Saved {index} generated Monet-style images to: {output_dir}\")\n\n# import zipfile\n\n# submission_zip = '/kaggle/working/images.zip'\n\n# with zipfile.ZipFile(submission_zip, 'w') as zipf:\n#     for i in range(index):\n#         zipf.write(os.path.join(output_dir, f'{i}.jpg'), arcname=f'{i}.jpg')\n\n# print(f\"✅ Created images zip: {submission_zip}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Score: 99.56088. This performed worse than my initial version. ","metadata":{}},{"cell_type":"markdown","source":"## V3. Increase Epochs and Change Lambda Loss Weights\n\nI want to perform one last optimization, which involves adjusting the LAMBDA_CYCLE and LAMBDA_ID weights. These are hyperparameters to the CycleGAN loss function. I will also increase the number of epochs from 10 to 20.","metadata":{}},{"cell_type":"code","source":"# 1. Define Checkpoint\nckpt = tf.train.Checkpoint(generator_pm=generator_pm,\n                           generator_mp=generator_mp,\n                           discriminator_p=discriminator_p,\n                           discriminator_m=discriminator_m,\n                           generator_pm_optimizer=generator_pm_optimizer,\n                           generator_mp_optimizer=generator_mp_optimizer,\n                           discriminator_p_optimizer=discriminator_p_optimizer,\n                           discriminator_m_optimizer=discriminator_m_optimizer)\n\n# 2. Define Manager\nckpt_manager = tf.train.CheckpointManager(ckpt, './checkpoints_v3', max_to_keep=5)\n\n# 3. Restore if available\nif ckpt_manager.latest_checkpoint:\n    ckpt.restore(ckpt_manager.latest_checkpoint)\n    print(f\"Restored from {ckpt_manager.latest_checkpoint}\")\nelse:\n    print(\"Starting training from scratch.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-11T06:23:21.592826Z","iopub.execute_input":"2025-07-11T06:23:21.593116Z","iopub.status.idle":"2025-07-11T06:23:21.602146Z","shell.execute_reply.started":"2025-07-11T06:23:21.593095Z","shell.execute_reply":"2025-07-11T06:23:21.601538Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Imports\nimport tensorflow as tf\nimport glob\n\n\nBATCH_SIZE = 1\nLAMBDA_CYCLE = 15.0\nLAMBDA_ID = 8.0\n\nhistory_v3 = train(photo_dataset_train, monet_dataset_train, epochs=20)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-11T06:23:26.601905Z","iopub.execute_input":"2025-07-11T06:23:26.602551Z","iopub.status.idle":"2025-07-11T07:08:07.693489Z","shell.execute_reply.started":"2025-07-11T06:23:26.602525Z","shell.execute_reply":"2025-07-11T07:08:07.692692Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\nplt.figure(figsize=(12, 6))\nplt.plot(history_v3['gen_pm_loss'], label='Generator Photo→Monet Loss')\nplt.plot(history_v3['gen_mp_loss'], label='Generator Monet→Photo Loss')\nplt.plot(history_v3['disc_p_loss'], label='Discriminator Photo Loss')\nplt.plot(history_v3['disc_m_loss'], label='Discriminator Monet Loss')\nplt.title('CycleGAN Training Loss')\nplt.xlabel('Epoch')\nplt.ylabel('Loss')\nplt.legend()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-11T07:08:07.694525Z","iopub.execute_input":"2025-07-11T07:08:07.694735Z","iopub.status.idle":"2025-07-11T07:08:07.953297Z","shell.execute_reply.started":"2025-07-11T07:08:07.694719Z","shell.execute_reply":"2025-07-11T07:08:07.952703Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"We can see from the history graph above that the generator loss for both the photo-to-monet and monet-to-photo decreases over time. So, to improve, we would likely need to continue with longer epochs.","metadata":{}},{"cell_type":"code","source":"import random\nimport matplotlib.pyplot as plt\n\n# Define denormalization helper\ndef denormalize_image(image):\n    image = (image + 1) * 127.5\n    image = tf.clip_by_value(image, 0, 255)\n    return tf.cast(image, tf.uint8)\n\n# Take 5 random photos from training dataset\nphoto_batches = list(photo_dataset_train)\nrandom.shuffle(photo_batches)\nphoto_samples = [batch[0] for batch in photo_batches[:5]]\n\n# Monetify\nmonetified_samples = []\nfor photo in photo_samples:\n    monetified = generator_pm(tf.expand_dims(photo, 0), training=False)[0]\n    monetified_samples.append(denormalize_image(monetified).numpy())\n\n# Plot\nplt.figure(figsize=(15, 6))\nfor i in range(5):\n    plt.subplot(2, 5, i + 1)\n    plt.imshow(denormalize_image(photo_samples[i]).numpy())\n    plt.title('Photo')\n    plt.axis('off')\n    \n    plt.subplot(2, 5, i + 6)\n    plt.imshow(monetified_samples[i])\n    plt.title('Monetified')\n    plt.axis('off')\n\nplt.suptitle('Random Photo Samples Monetified', fontsize=16)\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-11T07:08:07.954055Z","iopub.execute_input":"2025-07-11T07:08:07.954294Z","iopub.status.idle":"2025-07-11T07:08:14.896804Z","shell.execute_reply.started":"2025-07-11T07:08:07.954267Z","shell.execute_reply":"2025-07-11T07:08:14.895569Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nfrom PIL import Image\nimport numpy as np\n\noutput_dir = '/kaggle/working/generated_images_model_v3'\nos.makedirs(output_dir, exist_ok=True)\n\nindex = 0\n\nfor photo_batch in photo_dataset_train:\n    generated_batch = generator_pm(photo_batch, training=False)\n    \n    # Process each image in the batch (in case batch_size > 1)\n    for i in range(generated_batch.shape[0]):\n        generated = denormalize_image(generated_batch[i]).numpy()\n        img = Image.fromarray(generated)\n        img.convert('RGB').save(os.path.join(output_dir, f'{index}.jpg'), 'JPEG')\n        index += 1\n\nprint(f\"✅ Saved {index} generated Monet-style images to: {output_dir}\")\n\nimport zipfile\n\nsubmission_zip = '/kaggle/working/images.zip'\n\nwith zipfile.ZipFile(submission_zip, 'w') as zipf:\n    for i in range(index):\n        zipf.write(os.path.join(output_dir, f'{i}.jpg'), arcname=f'{i}.jpg')\n\nprint(f\"✅ Created images zip: {submission_zip}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Score: 84.07554. This performed better than V1 (lower score).","metadata":{}},{"cell_type":"markdown","source":"# Conclusion and Learning","metadata":{}},{"cell_type":"code","source":"import pandas as pd\n\nresults = {\n    'Model': ['Cycle GAN', 'Cycle GAN with More Augmentation', 'Cycle GAN with More Epochs and Lamda Loss Change'],\n    'MFID': [87.63909, 99.56088, 84.07554],\n}\n\ndf_result = pd.DataFrame(results).sort_values(by=['MFID'], ascending=True)\ndf_result","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-11T01:12:00.210765Z","iopub.execute_input":"2025-07-11T01:12:00.211055Z","iopub.status.idle":"2025-07-11T01:12:00.533377Z","shell.execute_reply.started":"2025-07-11T01:12:00.211033Z","shell.execute_reply":"2025-07-11T01:12:00.532611Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"From the table above, we can see that CycleGAN with increased Epochs has resulted in the best score. The original CycleGAN performed in the middle, and the CycleGAN with additional augmentation performed the worst.\n\nWe observed, after running the actual generation and obtaining the MFID score, that the additional augmentation did not provide any benefit and performed significantly worse. My assumption here is that adding the additional randomness made it harder for the GAN to match the target distribution. It had trouble matching domains.\n\nWhat I've learned is that for CycleGAN, the number of Epochs is significant. To improve the algorithm further, we can:\n\n* Add additional Epochs.\n* Try adding dropout to the discriminator.\n* Use a learning rate scheduler.\n\n## Conclusion\n\nOverall, the Kaggle ranking is pretty good. However, it would be better to be in the lower 30s. That means that this model still has a long way to go. What we observed was that CycleGAN with additional augmentation performed the worst, likely because the two distributions would have been more complex to match within the given number of Epochs. Version three, with increased Epochs, performed the best.","metadata":{}},{"cell_type":"markdown","source":"Note: This is a helper function for deleting checkpoints. I used this to ensure we restart the entire Kaggle working space.","metadata":{}},{"cell_type":"code","source":"#import shutil\n\n#folders_to_delete = ['./checkpoints', './checkpoints_v2']\n\n#for folder in folders_to_delete:\n#    shutil.rmtree(folder, ignore_errors=True)\n#    print(f\"✅ Deleted checkpoint folder: {folder}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-10T08:21:38.233744Z","iopub.execute_input":"2025-07-10T08:21:38.234357Z","iopub.status.idle":"2025-07-10T08:21:38.238777Z","shell.execute_reply.started":"2025-07-10T08:21:38.234334Z","shell.execute_reply":"2025-07-10T08:21:38.237876Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# References\n\nJang, A., Uzsoy, A., S., & Culliton, P. (2020). I’m Something of a Painter Myself. Retrieved 05/13/2025 from https://kaggle.com/competitions/gan-getting-started\n\nJang, A. (2020). Monet CycleGAN Tutorial.  Retrieved 05/13/2025 from https://www.kaggle.com/code/amyjang/monet-cyclegan-tutorial","metadata":{}}]}