{"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":31012,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        pass\n        # print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"_kg_hide-input":true,"execution":{"iopub.status.busy":"2025-04-18T18:09:32.496600Z","iopub.execute_input":"2025-04-18T18:09:32.496764Z","iopub.status.idle":"2025-04-18T18:09:40.840200Z","shell.execute_reply.started":"2025-04-18T18:09:32.496749Z","shell.execute_reply":"2025-04-18T18:09:40.839646Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import tensorflow as tf\nimport matplotlib.pyplot as plt\nfrom tensorflow.keras.models import Sequential, Model\nfrom tensorflow.keras.layers import Conv2D, Input, BatchNormalization, LeakyReLU, Conv2DTranspose, Dropout, ReLU, Concatenate, ZeroPadding2D\nimport os\nfrom glob import glob\nfrom PIL import Image\nimport shutil\n\nfrom kaggle_datasets import KaggleDatasets","metadata":{"trusted":true,"_kg_hide-input":true,"execution":{"iopub.status.busy":"2025-04-18T18:09:40.841960Z","iopub.execute_input":"2025-04-18T18:09:40.842284Z","iopub.status.idle":"2025-04-18T18:09:53.589533Z","shell.execute_reply.started":"2025-04-18T18:09:40.842267Z","shell.execute_reply":"2025-04-18T18:09:53.588969Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"try:\n    tpu = tf.distribute.cluster_resolver.TPUClusterResolver()  # Detect TPU\n    tf.config.experimental_connect_to_cluster(tpu)\n    tf.tpu.experimental.initialize_tpu_system(tpu)\n    strategy = tf.distribute.TPUStrategy(tpu)\n    print(\"Running on TPU!\")\nexcept:\n    strategy = tf.distribute.get_strategy()  # Default to GPU/CPU\n    print(\"Running on CPU/GPU\")\n\nAUTOTUNE = tf.data.experimental.AUTOTUNE","metadata":{"trusted":true,"_kg_hide-input":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2025-04-18T18:09:53.590122Z","iopub.execute_input":"2025-04-18T18:09:53.590537Z","iopub.status.idle":"2025-04-18T18:09:53.596124Z","shell.execute_reply.started":"2025-04-18T18:09:53.590518Z","shell.execute_reply":"2025-04-18T18:09:53.595393Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class Config():\n    seed = 42\n    monet_path = '/monet_tfrec/*.tfrec'\n    photo_path = '/photo_tfrec/*.tfrec'\n    image_size = [256, 256]\n    monet_filenames = []\n    photo_filenames = []\n    identity_weight = 2.0\n    batch_size = 1\n    epochs = 40","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-18T18:09:53.596837Z","iopub.execute_input":"2025-04-18T18:09:53.597166Z","iopub.status.idle":"2025-04-18T18:09:53.628924Z","shell.execute_reply.started":"2025-04-18T18:09:53.597138Z","shell.execute_reply":"2025-04-18T18:09:53.628289Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"tf.random.set_seed(Config.seed)\nnp.random.seed(Config.seed)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-18T18:09:53.629717Z","iopub.execute_input":"2025-04-18T18:09:53.629917Z","iopub.status.idle":"2025-04-18T18:09:53.643771Z","shell.execute_reply.started":"2025-04-18T18:09:53.629895Z","shell.execute_reply":"2025-04-18T18:09:53.643159Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style=\"background-color: #2C3E50; color: white; padding: 10px; border-radius: 10px; text-align: center; font-size: 24px; font-weight: bold;\">\n    Use GANs to create art\n</div>","metadata":{}},{"cell_type":"markdown","source":"<div style=\"background-color: #9B59B6; color: white; padding: 10px; border-radius: 10px; text-align: center; font-size: 24px; font-weight: bold;\">\n    Problem Statement\n</div>","metadata":{}},{"cell_type":"markdown","source":"A GAN consists of at least two neural networks: a generator model and a discriminator model. The generator is a neural network that creates the images. For our competition, you should generate images in the style of Monet. This generator is trained using a discriminator.\n\nThe two models will work against each other, with the generator trying to trick the discriminator, and the discriminator trying to accurately classify the real vs. generated images.\n\nThe goal is to build a GAN that generates 7,000 to 10,000 Monet-style images.","metadata":{}},{"cell_type":"markdown","source":"## Dataset\n\n* monet_jpg - 300 Monet paintings sized 256x256 in JPEG format\n* monet_tfrec - 300 Monet paintings sized 256x256 in TFRecord format\n* photo_jpg - 7028 photos sized 256x256 in JPEG format\n* photo_tfrec - 7028 photos sized 256x256 in TFRecord format","metadata":{}},{"cell_type":"markdown","source":"## Generative Deep Learning Models","metadata":{}},{"cell_type":"markdown","source":"Generative deep learning models are a class of neural networks designed to learn data distributions and generate new, realistic samples — such as images, text, audio, or video — that resemble the original data.","metadata":{}},{"cell_type":"markdown","source":"Types of Generative Models\n1. Variational Autoencoders (VAEs)\n2. Generative Adversarial Networks (GANs)\n3. Autoregressive Models\n4. Diffusion Models\n\nIn this project we will use GANs. They are especially powerful for image generation tasks due to their ability to produce sharp, high-resolution, and realistic outputs — often outperforming other generative models in visual quality.","metadata":{}},{"cell_type":"markdown","source":"<div style=\"background-color: #9B59B6; color: white; padding: 10px; border-radius: 10px; text-align: center; font-size: 24px; font-weight: bold;\">\n    EDA\n</div>","metadata":{}},{"cell_type":"markdown","source":"Let's use [Amy Jang's notebook](https://www.kaggle.com/code/amyjang/monet-cyclegan-tutorial) that goes over the basics of loading data from TFRecords, using TPUs, and building a CycleGAN.","metadata":{}},{"cell_type":"code","source":"gcs_path = KaggleDatasets().get_gcs_path()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-18T18:09:53.644393Z","iopub.execute_input":"2025-04-18T18:09:53.644580Z","iopub.status.idle":"2025-04-18T18:09:54.113127Z","shell.execute_reply.started":"2025-04-18T18:09:53.644566Z","shell.execute_reply":"2025-04-18T18:09:54.112594Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"Config.monet_filenames = tf.io.gfile.glob(str(gcs_path + Config.monet_path))\nprint('Monet TFRecord Files:', len(Config.monet_filenames))\n\nConfig.photo_filenames = tf.io.gfile.glob(str(gcs_path + Config.photo_path))\nprint('Photo TFRecord Files:', len(Config.photo_filenames))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-18T18:09:54.115260Z","iopub.execute_input":"2025-04-18T18:09:54.115460Z","iopub.status.idle":"2025-04-18T18:09:56.330953Z","shell.execute_reply.started":"2025-04-18T18:09:54.115444Z","shell.execute_reply":"2025-04-18T18:09:56.330358Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"All the images for the competition are already sized to 256x256. As these images are RGB images, set the channel to 3. Additionally, we need to scale the images to a [-1, 1] scale. Because 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":"code","source":"def decode_image(image):\n    image = tf.image.decode_jpeg(image, channels=3)\n    image = (tf.cast(image, tf.float32) / 127.5) - 1\n    image = tf.reshape(image, [*Config.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":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-18T18:09:56.331638Z","iopub.execute_input":"2025-04-18T18:09:56.331906Z","iopub.status.idle":"2025-04-18T18:09:56.336675Z","shell.execute_reply.started":"2025-04-18T18:09:56.331887Z","shell.execute_reply":"2025-04-18T18:09:56.336013Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Define the 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)\n    dataset = dataset.map(read_tfrecord, num_parallel_calls=AUTOTUNE)\n    return dataset","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-18T18:09:56.337425Z","iopub.execute_input":"2025-04-18T18:09:56.337665Z","iopub.status.idle":"2025-04-18T18:09:56.352331Z","shell.execute_reply.started":"2025-04-18T18:09:56.337642Z","shell.execute_reply":"2025-04-18T18:09:56.351774Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Let's load in our datasets.","metadata":{}},{"cell_type":"code","source":"monet_ds = load_dataset(Config.monet_filenames, labeled=True).batch(1)\nphoto_ds = load_dataset(Config.photo_filenames, labeled=True).batch(1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-18T18:09:56.353063Z","iopub.execute_input":"2025-04-18T18:09:56.353278Z","iopub.status.idle":"2025-04-18T18:09:57.680715Z","shell.execute_reply.started":"2025-04-18T18:09:56.353255Z","shell.execute_reply":"2025-04-18T18:09:57.680030Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"example_monet = next(iter(monet_ds))\nexample_photo = next(iter(photo_ds))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-18T18:09:57.681370Z","iopub.execute_input":"2025-04-18T18:09:57.681581Z","iopub.status.idle":"2025-04-18T18:10:03.087166Z","shell.execute_reply.started":"2025-04-18T18:09:57.681566Z","shell.execute_reply":"2025-04-18T18:10:03.086607Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Photo shape:\", example_photo[0].shape)\nprint(\"Monet shape:\", example_monet[0].shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-18T18:10:03.087946Z","iopub.execute_input":"2025-04-18T18:10:03.088150Z","iopub.status.idle":"2025-04-18T18:10:03.096454Z","shell.execute_reply.started":"2025-04-18T18:10:03.088135Z","shell.execute_reply":"2025-04-18T18:10:03.095956Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Plot 1 image from each set\nplt.figure(figsize=(10, 5))\n\n# Monet image\nax = plt.subplot(1, 2, 1)\nplt.imshow((example_monet[0].numpy() * 0.5 + 0.5))  # Unnormalize if in [-1, 1]\nplt.title(\"Monet\")\nplt.axis(\"off\")\n\n# Photo image\nax = plt.subplot(1, 2, 2)\nplt.imshow((example_photo[0].numpy() * 0.5 + 0.5))  # Unnormalize if in [-1, 1]\nplt.title(\"Photo\")\nplt.axis(\"off\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-18T18:10:03.097111Z","iopub.execute_input":"2025-04-18T18:10:03.097325Z","iopub.status.idle":"2025-04-18T18:10:03.609207Z","shell.execute_reply.started":"2025-04-18T18:10:03.097310Z","shell.execute_reply":"2025-04-18T18:10:03.608521Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Histogram of Pixel Intensities","metadata":{}},{"cell_type":"code","source":"def plot_pixel_histograms(img_list, labels, bins=50):\n    plt.figure(figsize=(12, 4))\n    for i, (img, label) in enumerate(zip(img_list, labels)):\n        img = (img + 1) / 2  # if normalized to [-1, 1]\n        img = img.numpy() if hasattr(img, 'numpy') else img\n        gray = np.mean(img, axis=-1).flatten()\n\n        plt.subplot(1, len(img_list), i+1)\n        plt.hist(gray, bins=bins, color='gray')\n        plt.title(f\"{label} Histogram\")\n        plt.xlabel(\"Pixel intensity\")\n        plt.ylabel(\"Count\")\n    plt.tight_layout()\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-18T18:10:03.610001Z","iopub.execute_input":"2025-04-18T18:10:03.610207Z","iopub.status.idle":"2025-04-18T18:10:03.615112Z","shell.execute_reply.started":"2025-04-18T18:10:03.610191Z","shell.execute_reply":"2025-04-18T18:10:03.614525Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plot_pixel_histograms(\n    [example_photo[0], example_monet[0]],\n    labels=[\"Photo\", \"Monet\"]\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-18T18:10:03.615644Z","iopub.execute_input":"2025-04-18T18:10:03.615854Z","iopub.status.idle":"2025-04-18T18:10:03.995856Z","shell.execute_reply.started":"2025-04-18T18:10:03.615837Z","shell.execute_reply":"2025-04-18T18:10:03.995247Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"* The Monet images are significantly brighter overall, with more pixels in the high-intensity range.\n* This suggests that a style transfer model like CycleGAN must learn not only texture changes, but also a nonlinear shift in overall brightness and tone.","metadata":{}},{"cell_type":"markdown","source":"<div style=\"background-color: #9B59B6; color: white; padding: 10px; border-radius: 10px; text-align: center; font-size: 24px; font-weight: bold;\">\n    CycleGAN Model\n</div>","metadata":{}},{"cell_type":"markdown","source":"The goal of this CycleGAN model is to perform unpaired image-to-image translation between two domains:\n\n* Photos (real-world images)\n* Monet paintings (art-style domain)\n \nThe model learns to translate photos to Monet-style images without needing 1-to-1 paired examples.","metadata":{}},{"cell_type":"markdown","source":"## Why CycleGAN?\n\n* No need for paired images\n* Well-suited for style transfer tasks like\n* Proven success in artistic domain generation tasks","metadata":{}},{"cell_type":"markdown","source":"## How It Works\n\nCycleGAN uses:\n\nTwo generators:\n\n* G: Photo → Monet\n* F: Monet → Photo\n\nTwo discriminators:\n\n* D_Y: distinguishes real Monet paintings from generated ones\n* D_X: distinguishes real photos from generated ones","metadata":{}},{"cell_type":"markdown","source":"## Model Architecture\n**Generator (G & F)**\n\n* Based on U-Net\n\n* Consists of:\n1. Downsampling (Conv + BatchNorm + LeakyReLU)\n2. Residual blocks\n3. Upsampling (Transposed Conv)\n\n\n**Discriminator (D_X & D_Y)**\n\n* PatchGAN architecture\n* Judges authenticity over image patches, not the full image\n* Helps the model focus on local texture and style","metadata":{}},{"cell_type":"code","source":"def downsample(filters, size, apply_batchnorm=True):\n    initializer = tf.random_normal_initializer(0., 0.02)\n    result = Sequential([\n        Conv2D(filters, size, strides=2, padding='same',\n               kernel_initializer=initializer, use_bias=False),\n    ])\n    if apply_batchnorm:\n        result.add(BatchNormalization())\n        \n    result.add(LeakyReLU())\n    return result\n\ndef upsample(filters, size, apply_dropout=False):\n    initializer = tf.random_normal_initializer(0., 0.02)\n    result = Sequential([\n        Conv2DTranspose(filters, size, strides=2, padding='same',\n                                      kernel_initializer=initializer, use_bias=False),\n        BatchNormalization()\n    ])\n    if apply_dropout:\n        result.add(Dropout(0.5))\n    result.add(ReLU())\n    return result","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-18T18:10:03.996547Z","iopub.execute_input":"2025-04-18T18:10:03.996804Z","iopub.status.idle":"2025-04-18T18:10:04.002408Z","shell.execute_reply.started":"2025-04-18T18:10:03.996782Z","shell.execute_reply":"2025-04-18T18:10:04.001734Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def Generator():\n    inputs = Input(shape=[*Config.image_size, 3])\n    down_stack = [\n        downsample(64, 4, apply_batchnorm=False),  # (bs, 128, 128, 64)\n        downsample(128, 4),  # (bs, 64, 64, 128)\n        downsample(256, 4),  # (bs, 32, 32, 256)\n    ]\n    up_stack = [\n        upsample(128, 4),  # (bs, 64, 64, 128)\n        upsample(64, 4),   # (bs, 128, 128, 64)\n    ]\n    last = Conv2DTranspose(3, 4, strides=2, padding='same',\n                            kernel_initializer=tf.random_normal_initializer(0., 0.02),\n                            activation='tanh')  # (bs, 256, 256, 3)\n\n    x = inputs\n    skips = []\n    for down in down_stack:\n        x = down(x)\n        skips.append(x)\n    skips = reversed(skips[:-1])\n\n    for up, skip in zip(up_stack, skips):\n        x = up(x)\n        x = Concatenate()([x, skip])\n\n    x = last(x)\n    return Model(inputs=inputs, outputs=x)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-18T18:10:04.003183Z","iopub.execute_input":"2025-04-18T18:10:04.003749Z","iopub.status.idle":"2025-04-18T18:10:04.017739Z","shell.execute_reply.started":"2025-04-18T18:10:04.003728Z","shell.execute_reply":"2025-04-18T18:10:04.017101Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def Discriminator():\n    initializer = tf.random_normal_initializer(0., 0.02)\n    inp = Input(shape=[*Config.image_size, 3], name='input_image')\n    x = downsample(64, 4, False)(inp)\n    x = downsample(128, 4)(x)\n    x = downsample(256, 4)(x)\n    x = ZeroPadding2D()(x)\n    x = Conv2D(512, 4, strides=1, kernel_initializer=initializer, use_bias=False)(x)\n    x = BatchNormalization()(x)\n    x = LeakyReLU()(x)\n    x = ZeroPadding2D()(x)\n    x = Conv2D(1, 4, strides=1, kernel_initializer=initializer)(x)\n    return Model(inputs=inp, outputs=x)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-18T18:10:04.018463Z","iopub.execute_input":"2025-04-18T18:10:04.018735Z","iopub.status.idle":"2025-04-18T18:10:04.041512Z","shell.execute_reply.started":"2025-04-18T18:10:04.018714Z","shell.execute_reply":"2025-04-18T18:10:04.040776Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"generator_g = Generator()  # Monet → Photo\ngenerator_f = Generator()  # Photo → Monet\n\ndiscriminator_x = Discriminator()  # Discriminator for Monet\ndiscriminator_y = Discriminator()  # Discriminator for Photo","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-18T18:10:04.042235Z","iopub.execute_input":"2025-04-18T18:10:04.042418Z","iopub.status.idle":"2025-04-18T18:10:04.323211Z","shell.execute_reply.started":"2025-04-18T18:10:04.042403Z","shell.execute_reply":"2025-04-18T18:10:04.322709Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"generator_f.summary()\ndiscriminator_x.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-18T18:10:04.323877Z","iopub.execute_input":"2025-04-18T18:10:04.324123Z","iopub.status.idle":"2025-04-18T18:10:04.354822Z","shell.execute_reply.started":"2025-04-18T18:10:04.324103Z","shell.execute_reply":"2025-04-18T18:10:04.354298Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Let's create loss functions for CycleGAN. The model uses a combination of adversarial, cycle consistency, and identity losses to guide both the generators and discriminators.","metadata":{}},{"cell_type":"markdown","source":"* discriminator_loss - train discriminator to separate real/fake\n* generator_loss - train generator to fool discriminator\n* cycle_loss - ensure round-trip image consistency\n* identity_loss - preserve input when already in target domain","metadata":{}},{"cell_type":"code","source":"loss_obj = tf.keras.losses.BinaryCrossentropy(from_logits=True)\n\ndef discriminator_loss(real, generated):\n    real_loss = loss_obj(tf.ones_like(real), real)\n    generated_loss = loss_obj(tf.zeros_like(generated), generated)\n    return (real_loss + generated_loss) * 0.5\n\ndef generator_loss(generated):\n    return loss_obj(tf.ones_like(generated), generated)\n\ndef cycle_loss(real_image, cycled_image):\n    return tf.reduce_mean(tf.abs(real_image - cycled_image)) * 10.0\n\ndef identity_loss(real_image, same_image):\n    return tf.reduce_mean(tf.abs(real_image - same_image)) * 5.0","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-18T18:10:04.355415Z","iopub.execute_input":"2025-04-18T18:10:04.355705Z","iopub.status.idle":"2025-04-18T18:10:04.360382Z","shell.execute_reply.started":"2025-04-18T18:10:04.355681Z","shell.execute_reply":"2025-04-18T18:10:04.359803Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"This function defines a single training step for the CycleGAN model. It performs forward and backward passes for both generators and discriminators, and updates their weights using the computed gradients.\n\nThe function is wrapped in @tf.function to compile it into a TensorFlow graph for optimized execution.","metadata":{}},{"cell_type":"code","source":"@tf.function\ndef train_step(real_x, real_y):\n    with tf.GradientTape(persistent=True) as tape:\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 = 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        gen_g_loss = generator_loss(disc_fake_y)\n        gen_f_loss = generator_loss(disc_fake_x)\n\n        total_cycle_loss = cycle_loss(real_x, cycled_x) + cycle_loss(real_y, cycled_y)\n        total_gen_g_loss = gen_g_loss + total_cycle_loss + identity_loss(real_y, same_y) * Config.identity_weight\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    # Apply gradients\n    generator_g_grad = tape.gradient(total_gen_g_loss, generator_g.trainable_variables)\n    generator_f_grad = tape.gradient(total_gen_f_loss, generator_f.trainable_variables)\n\n    discriminator_x_grad = tape.gradient(disc_x_loss, discriminator_x.trainable_variables)\n    discriminator_y_grad = tape.gradient(disc_y_loss, discriminator_y.trainable_variables)\n\n    generator_g_optimizer.apply_gradients(zip(generator_g_grad, generator_g.trainable_variables))\n    generator_f_optimizer.apply_gradients(zip(generator_f_grad, generator_f.trainable_variables))\n    discriminator_x_optimizer.apply_gradients(zip(discriminator_x_grad, discriminator_x.trainable_variables))\n    discriminator_y_optimizer.apply_gradients(zip(discriminator_y_grad, discriminator_y.trainable_variables))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-18T18:10:04.363228Z","iopub.execute_input":"2025-04-18T18:10:04.363727Z","iopub.status.idle":"2025-04-18T18:10:04.382496Z","shell.execute_reply.started":"2025-04-18T18:10:04.363712Z","shell.execute_reply":"2025-04-18T18:10:04.381933Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Let's train our model.","metadata":{}},{"cell_type":"code","source":"def visualize_epoch_result(epoch):\n    # Visualize intermediate epochs\n    if (epoch + 1) % 5 == 0:\n        os.makedirs(\"cycle_outputs\", exist_ok=True)\n        \n        sample_photo = next(iter(photo_ds))\n        prediction = generator_g(sample_photo)\n    \n        plt.figure(figsize=(8, 4))\n        plt.subplot(1, 2, 1)\n        plt.imshow((sample_photo[0] * 0.5 + 0.5).numpy())\n        plt.title(\"Input\")\n        plt.axis('off')\n    \n        plt.subplot(1, 2, 2)\n        plt.imshow((prediction[0] * 0.5 + 0.5).numpy())\n        plt.title(f\"Epoch {epoch+1}\")\n        plt.axis('off')\n    \n        plt.tight_layout()\n        plt.savefig(f\"cycle_outputs/epoch_{epoch+1}.png\")\n        plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-18T18:10:04.383089Z","iopub.execute_input":"2025-04-18T18:10:04.383325Z","iopub.status.idle":"2025-04-18T18:10:04.401091Z","shell.execute_reply.started":"2025-04-18T18:10:04.383300Z","shell.execute_reply":"2025-04-18T18:10:04.400495Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def recalc_losses():\n    sample_photo = next(iter(photo_ds))\n    sample_monet = next(iter(monet_ds))\n\n    fake_y = generator_g(sample_photo, training=False)\n    cycled_x = generator_f(fake_y, training=False)\n    fake_x = generator_f(sample_monet, training=False)\n    cycled_y = generator_g(fake_x, training=False)\n    same_x = generator_f(sample_photo, training=False)\n    same_y = generator_g(sample_monet, training=False)\n\n    disc_real_x = discriminator_x(sample_photo, training=False)\n    disc_fake_x = discriminator_x(fake_x, training=False)\n    disc_real_y = discriminator_y(sample_monet, training=False)\n    disc_fake_y = discriminator_y(fake_y, training=False)\n\n    # Calculate losses for visualization\n    g_loss = generator_loss(disc_fake_y)\n    f_loss = generator_loss(disc_fake_x)\n    cyc_loss = cycle_loss(sample_photo, cycled_x) + cycle_loss(sample_monet, cycled_y)\n    id_loss = identity_loss(sample_monet, same_y) + identity_loss(sample_photo, same_x)\n    dx_loss = discriminator_loss(disc_real_x, disc_fake_x)\n    dy_loss = discriminator_loss(disc_real_y, disc_fake_y)\n\n    # Append losses\n    gen_g_losses.append(g_loss.numpy())\n    gen_f_losses.append(f_loss.numpy())\n    disc_x_losses.append(dx_loss.numpy())\n    disc_y_losses.append(dy_loss.numpy())\n    cycle_losses.append(cyc_loss.numpy())\n    identity_losses.append(id_loss.numpy())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-18T18:10:04.401710Z","iopub.execute_input":"2025-04-18T18:10:04.401896Z","iopub.status.idle":"2025-04-18T18:10:04.419672Z","shell.execute_reply.started":"2025-04-18T18:10:04.401883Z","shell.execute_reply":"2025-04-18T18:10:04.419099Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"initial_lr = 2e-4\nlr_schedule = tf.keras.optimizers.schedules.PiecewiseConstantDecay(\n    boundaries=[20],  # after 20 epochs\n    values=[initial_lr, initial_lr * 0.5]  # decay to half\n)\n\n# apply clipnorm=1.0 to make backpropagation more stable\ngenerator_g_optimizer = tf.keras.optimizers.Adam(lr_schedule, beta_1=0.5, clipnorm=1.0)\ngenerator_f_optimizer = tf.keras.optimizers.Adam(lr_schedule, beta_1=0.5)\ndiscriminator_x_optimizer = tf.keras.optimizers.Adam(lr_schedule, beta_1=0.5)\ndiscriminator_y_optimizer = tf.keras.optimizers.Adam(lr_schedule, beta_1=0.5)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-18T18:10:04.420303Z","iopub.execute_input":"2025-04-18T18:10:04.420533Z","iopub.status.idle":"2025-04-18T18:10:04.443322Z","shell.execute_reply.started":"2025-04-18T18:10:04.420510Z","shell.execute_reply":"2025-04-18T18:10:04.442819Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"gen_g_losses = []\ngen_f_losses = []\ndisc_x_losses = []\ndisc_y_losses = []\ncycle_losses = []\nidentity_losses = []\n\nfor epoch in range(Config.epochs):\n    for image_x, image_y in tf.data.Dataset.zip((photo_ds, monet_ds)):\n        train_step(image_x, image_y)\n        \n    recalc_losses()\n    visualize_epoch_result(epoch)\n    \n    print(f\"Epoch {epoch+1}/{Config.epochs} done.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-18T18:10:04.443985Z","iopub.execute_input":"2025-04-18T18:10:04.444590Z","iopub.status.idle":"2025-04-18T18:33:43.441886Z","shell.execute_reply.started":"2025-04-18T18:10:04.444573Z","shell.execute_reply":"2025-04-18T18:33:43.441153Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Plot loss curves","metadata":{}},{"cell_type":"code","source":"epochs = range(1, len(gen_g_losses) + 1)\n\nplt.figure(figsize=(12, 6))\n\nplt.plot(epochs, gen_g_losses, label='Generator G Loss (Photo→Monet)')\nplt.plot(epochs, gen_f_losses, label='Generator F Loss (Monet→Photo)')\nplt.plot(epochs, disc_x_losses, label='Discriminator X Loss (Photo)')\nplt.plot(epochs, disc_y_losses, label='Discriminator Y Loss (Monet)')\nplt.plot(epochs, cycle_losses, label='Cycle Consistency Loss')\nplt.plot(epochs, identity_losses, label='Identity Loss')\n\nplt.title(\"CycleGAN Training Losses Over Epochs\")\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Loss\")\nplt.legend()\nplt.grid(True)\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-18T18:33:43.442772Z","iopub.execute_input":"2025-04-18T18:33:43.443056Z","iopub.status.idle":"2025-04-18T18:33:43.701701Z","shell.execute_reply.started":"2025-04-18T18:33:43.443030Z","shell.execute_reply":"2025-04-18T18:33:43.700998Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Cycle Consistency Loss:\n\n* Starts high and steadily decreases, indicating that the model is learning to preserve the structure of the input images when translating and cycling back.\n* A smooth decline here is a good sign — the generators are becoming more consistent.\n\nGenerator Losses:\n\n* G (Photo → Monet) (Blue): Fairly stable and relatively low, suggesting the Monet generator is learning steadily.\n* F (Monet → Photo) (Orange): Slightly noisier and higher, with a spike around epoch 5 and some recovery after — this may suggest a temporary imbalance, or that the Monet → Photo translation is more difficult.\n\nDiscriminator Losses:\n\n* Discriminator X (Photo) and Y (Monet) are moderate and stable, indicating that the discriminators are not overpowering the generators (no collapse).\n* Slight fluctuations are normal as generators and discriminators compete.\n\nIdentity Loss:\n\n* Gradually decreasing and staying under control, suggesting the model is learning to preserve features when translating images already in the target domain.\n* No sharp spikes — a good regularization signal.","metadata":{}},{"cell_type":"markdown","source":"Visualize some image examples","metadata":{}},{"cell_type":"code","source":"def generate_images(model, test_input):\n    prediction = model(test_input, training=False)\n    plt.figure(figsize=(12, 6))\n\n    display_list = [test_input[0], prediction[0]]\n    title = ['Input Image', 'Translated Image']\n\n    for i in range(2):\n        plt.subplot(1, 2, i+1)\n        plt.title(title[i])\n        plt.imshow((display_list[i] * 0.5 + 0.5).numpy())\n        plt.axis('off')\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-18T18:33:43.702464Z","iopub.execute_input":"2025-04-18T18:33:43.702712Z","iopub.status.idle":"2025-04-18T18:33:43.708110Z","shell.execute_reply.started":"2025-04-18T18:33:43.702694Z","shell.execute_reply":"2025-04-18T18:33:43.707448Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for i, batch in enumerate(photo_ds):\n    generate_images(generator_g, batch)\n    if i == 4:\n        break","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-18T18:33:43.708820Z","iopub.execute_input":"2025-04-18T18:33:43.709031Z","iopub.status.idle":"2025-04-18T18:33:48.371181Z","shell.execute_reply.started":"2025-04-18T18:33:43.709007Z","shell.execute_reply":"2025-04-18T18:33:48.370545Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<div style=\"background-color: #9B59B6; color: white; padding: 10px; border-radius: 10px; text-align: center; font-size: 24px; font-weight: bold;\">\n    Conclusion\n</div>","metadata":{}},{"cell_type":"markdown","source":"In this project was implemented and trained a CycleGAN model to translate real-world photos into Monet-style paintings using unpaired image data.\n\nBy leveraging a U-Net–based generator and a PatchGAN discriminator, the model was able to learn the underlying texture and brushstroke patterns of Monet's style while preserving the content and structure of the original photographs.\n\nKey Takeaways:\n* Cycle Consistency was essential for training without paired datasets, ensuring that images translated back and forth retained their identity.\n* Adversarial Losses helped the generator learn to produce realistic images that could fool the discriminator.\n* Identity Loss acted as a regularizer, improving color preservation and reducing over-stylization.\n* Visualizing training losses and intermediate outputs provided valuable insight into model progress and stability.\n\nThe final model demonstrates a convincing ability to translate modern photos into impressionist artwork, with stylistic features resembling Monet’s paintings — including soft colors, blended textures, and painterly strokes.\n\nFuture Improvements:\n* Switch to a ResNet-based generator for more stylized results.\n* Apply style-guided augmentation or content masks for fine-grained control.\n* Use perceptual loss for better high-level feature alignment.","metadata":{}},{"cell_type":"markdown","source":"<div style=\"background-color: #9B59B6; color: white; padding: 10px; border-radius: 10px; text-align: center; font-size: 24px; font-weight: bold;\">\n    Submission\n</div>","metadata":{}},{"cell_type":"code","source":"! mkdir ../images","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-18T18:33:48.372128Z","iopub.execute_input":"2025-04-18T18:33:48.372730Z","iopub.status.idle":"2025-04-18T18:33:48.528992Z","shell.execute_reply.started":"2025-04-18T18:33:48.372708Z","shell.execute_reply":"2025-04-18T18:33:48.528097Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"i = 1\nfor img in photo_ds:\n    prediction = generator_g(img, training=False)[0].numpy()\n    prediction = (prediction * 127.5 + 127.5).astype(np.uint8)\n    im = Image.fromarray(prediction)\n    im.save(\"../images/\" + str(i) + \".jpg\")\n    i += 1","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-18T18:33:48.530102Z","iopub.execute_input":"2025-04-18T18:33:48.530388Z","iopub.status.idle":"2025-04-18T18:36:27.637279Z","shell.execute_reply.started":"2025-04-18T18:33:48.530355Z","shell.execute_reply":"2025-04-18T18:36:27.636517Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"shutil.make_archive(\"/kaggle/working/images\", 'zip', \"/kaggle/images\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-18T18:36:27.638226Z","iopub.execute_input":"2025-04-18T18:36:27.638460Z","iopub.status.idle":"2025-04-18T18:36:30.587927Z","shell.execute_reply.started":"2025-04-18T18:36:27.638437Z","shell.execute_reply":"2025-04-18T18:36:30.587331Z"}},"outputs":[],"execution_count":null}]}