{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceType":"competition","sourceId":21755,"databundleVersionId":1475600}],"dockerImageVersionId":31090,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## Project Description\n\n**Objective:**\nThis project uses the Kaggle “GAN Getting Started” competition to practice building and training generative deep learning models, specifically Generative Adversarial Networks (GANs). The goal is to generate new images in the style of Monet paintings. The quality of generated images is evaluated using the MiFID (Memorization-informed Fréchet Inception Distance) score, which measures similarity between generated and real images while accounting for memorization.\n\n**Dataset Overview:**\n\n* The dataset contains four directories of images:\n\n* monet_jpg: 300 Monet paintings in JPEG format (256×256).\n\n* monet_tfrec: 300 Monet paintings in TFRecord format (256×256).\n\n* photo_jpg: 7,028 photos in JPEG format (256×256).\n\n* photo_tfrec: 7,028 photos in TFRecord format (256×256).\n\nTFRecords are recommended for efficient data loading, but JPEG images are also provided. The Monet directories contain the training images for style generation, while the photo directories provide target images to be transformed into Monet-style outputs.\n\n**Challenge Description:**\n\nA GAN consists of two neural networks:\n\n* Generator: Creates images attempting to replicate Monet’s style.\n\n* Discriminator: Distinguishes between real Monet paintings and generated images.\n\nDuring training, the generator and discriminator compete: the generator improves to “fool” the discriminator, while the discriminator improves to correctly classify real vs. generated images.\n\n**Project Task:**\n\n* Build a GAN capable of generating 7,000–10,000 Monet-style images.\n\n* Submit generated images as a zip file (maximum 10,000 images).\n\n* Optionally, experiment with other GAN architectures such as DCGAN or explore datasets like CycleGAN for artistic style transfer.","metadata":{}},{"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)\nimport matplotlib.pyplot as plt\nfrom IPython.display import clear_output\n%matplotlib inline \nimport random\nimport time\nfrom PIL import Image\nimport tensorflow as tf \nfrom keras import layers\nfrom tensorflow.keras import optimizers\nimport keras\nfrom tensorflow.keras import backend as K\nimport zipfile\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\nimport warnings\nwarnings.filterwarnings('ignore', category=FutureWarning)\nwarnings.filterwarnings('ignore', category=UserWarning)\nimport os\nimport shutil\n\n# for dirname, _, filenames in os.walk('/kaggle/input'):\n#     for filename in filenames:\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,"execution":{"iopub.status.busy":"2026-04-01T15:05:47.862455Z","iopub.execute_input":"2026-04-01T15:05:47.863009Z","iopub.status.idle":"2026-04-01T15:06:01.564036Z","shell.execute_reply.started":"2026-04-01T15:05:47.862982Z","shell.execute_reply":"2026-04-01T15:06:01.563400Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Set Paths\nmonet_path = '/kaggle/input/gan-getting-started/monet_jpg'\nphoto_path = '/kaggle/input/gan-getting-started/photo_jpg'\n\n# List of image files\nmonet_images = [os.path.join(monet_path, f) for f in os.listdir(monet_path) if f.endswith('.jpg')]\nphoto_images = [os.path.join(photo_path,f) for f in os.listdir(photo_path) if f.endswith('.jpg')]\nprint(f'Monet images: {len(monet_images)}')\nprint(f'Photo images: {len(photo_images)}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-01T15:06:01.565487Z","iopub.execute_input":"2026-04-01T15:06:01.566201Z","iopub.status.idle":"2026-04-01T15:06:01.687559Z","shell.execute_reply.started":"2026-04-01T15:06:01.566179Z","shell.execute_reply":"2026-04-01T15:06:01.686796Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Check for Corrupt Images\ndef check_images(image_list):\n    good =[]\n    for img_path in image_list:\n        try:\n            img =Image.open(img_path)\n            img.verify() # Check if image is corrupted\n            good.append(img_path)\n        except:\n            print(f'Corrupt image: {img_path},Error: {e}')\n    return good\nmonet_images = check_images(monet_images)\nphoto_images = check_images(photo_images)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-01T15:06:01.688424Z","iopub.execute_input":"2026-04-01T15:06:01.688736Z","iopub.status.idle":"2026-04-01T15:06:47.011842Z","shell.execute_reply.started":"2026-04-01T15:06:01.688716Z","shell.execute_reply":"2026-04-01T15:06:47.011261Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(f'Monet images: {len(monet_images)}')\nprint(f'Photo images: {len(photo_images)}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-01T15:06:47.012605Z","iopub.execute_input":"2026-04-01T15:06:47.012872Z","iopub.status.idle":"2026-04-01T15:06:47.016976Z","shell.execute_reply.started":"2026-04-01T15:06:47.012848Z","shell.execute_reply":"2026-04-01T15:06:47.016115Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## EDA","metadata":{}},{"cell_type":"code","source":"def display_images(image_list, n):\n    plt.figure(figsize=(15,3))\n    for i in range(n):\n        img_path = random.choice(image_list)\n        img = Image.open(img_path)\n        plt.subplot(1,n,i+1)\n        plt.imshow(img)\n        plt.axis('off')\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-01T15:06:47.019261Z","iopub.execute_input":"2026-04-01T15:06:47.019468Z","iopub.status.idle":"2026-04-01T15:06:47.036764Z","shell.execute_reply.started":"2026-04-01T15:06:47.019452Z","shell.execute_reply":"2026-04-01T15:06:47.036191Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"display_images(monet_images, 5)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-01T15:06:47.038278Z","iopub.execute_input":"2026-04-01T15:06:47.038543Z","iopub.status.idle":"2026-04-01T15:06:47.352637Z","shell.execute_reply.started":"2026-04-01T15:06:47.038518Z","shell.execute_reply":"2026-04-01T15:06:47.351811Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"display_images(photo_images, 5)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-01T15:06:47.353557Z","iopub.execute_input":"2026-04-01T15:06:47.353799Z","iopub.status.idle":"2026-04-01T15:06:47.605841Z","shell.execute_reply.started":"2026-04-01T15:06:47.353780Z","shell.execute_reply":"2026-04-01T15:06:47.605118Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## DCGAN Model","metadata":{}},{"cell_type":"code","source":"# Load images from your list of paths\ndef load_image(path):\n    img = tf.io.read_file(path)\n    img = tf.image.decode_jpeg(img, channels=3)\n    img = tf.image.resize(img, [256, 256])\n    # Normalize to [-1, 1] to match Generator's Tanh output\n    img = (img - 127.5) / 127.5 \n    return img\n\nBUFFER_SIZE = 300\nBATCH_SIZE = 16 # Keep batch small for 256x256 resolution\n\ntrain_dataset = tf.data.Dataset.from_tensor_slices(monet_images)\ntrain_dataset = train_dataset.map(load_image).shuffle(BUFFER_SIZE).batch(BATCH_SIZE)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-01T15:06:47.606833Z","iopub.execute_input":"2026-04-01T15:06:47.607391Z","iopub.status.idle":"2026-04-01T15:06:48.336960Z","shell.execute_reply.started":"2026-04-01T15:06:47.607363Z","shell.execute_reply":"2026-04-01T15:06:48.336246Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Generator and Discriminator \ndef build_generator():\n    model = tf.keras.Sequential([\n        layers.Dense(4 * 4 * 1024, input_shape=(100,)),\n        layers.Reshape((4, 4, 1024)),\n        \n        layers.Conv2DTranspose(512, 5, strides=2, padding='same', use_bias=False),\n        layers.BatchNormalization(), layers.ReLU(),\n        \n        layers.Conv2DTranspose(256, 5, strides=2, padding='same', use_bias=False),\n        layers.BatchNormalization(), layers.ReLU(),\n        \n        layers.Conv2DTranspose(128, 5, strides=2, padding='same', use_bias=False),\n        layers.BatchNormalization(), layers.ReLU(),\n        \n        layers.Conv2DTranspose(64, 5, strides=2, padding='same', use_bias=False),\n        layers.BatchNormalization(), layers.ReLU(),\n        \n        layers.Conv2DTranspose(32, 5, strides=2, padding='same', use_bias=False),\n        layers.BatchNormalization(), layers.ReLU(),\n\n        layers.Conv2DTranspose(3, 5, strides=2, padding='same', activation='tanh')\n    ])\n    return model\n\ndef build_discriminator():\n    model = tf.keras.Sequential([\n        layers.Conv2D(64, 5, strides=2, padding='same', input_shape=[256, 256, 3]),\n        layers.LeakyReLU(0.2),\n        layers.Dropout(0.3),\n\n        layers.Conv2D(128, 5, strides=2, padding='same'),\n        layers.LeakyReLU(0.2),\n        layers.Dropout(0.3),\n\n        layers.Conv2D(256, 5, strides=2, padding='same'),\n        layers.LeakyReLU(0.2),\n        layers.Dropout(0.3),\n\n        layers.Flatten(),\n        layers.Dense(1) \n    ])\n    return model","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-01T15:06:48.337618Z","iopub.execute_input":"2026-04-01T15:06:48.337803Z","iopub.status.idle":"2026-04-01T15:06:48.345094Z","shell.execute_reply.started":"2026-04-01T15:06:48.337789Z","shell.execute_reply":"2026-04-01T15:06:48.344501Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# custom GAN class\nclass GAN(tf.keras.Model):\n    def __init__(self, discriminator, generator, latent_dim=100):\n        super(GAN, self).__init__()\n        self.discriminator = discriminator\n        self.generator = generator\n        self.latent_dim = latent_dim\n\n    def compile(self, d_optimizer, g_optimizer, loss_fn):\n        super(GAN, self).compile()\n        self.d_optimizer = d_optimizer\n        self.g_optimizer = g_optimizer\n        self.loss_fn = loss_fn\n\n    def train_step(self, real_images):\n        batch_size = tf.shape(real_images)[0]\n        random_latent_vectors = tf.random.normal(shape=(batch_size, self.latent_dim))\n\n        # Train the Discriminator\n        with tf.GradientTape() as tape:\n            fake_images = self.generator(random_latent_vectors, training=True)\n            combined_images = tf.concat([fake_images, real_images], axis=0)\n            labels = tf.concat([tf.ones((batch_size, 1)), tf.zeros((batch_size, 1))], axis=0)\n            # Add noise to labels (trick to stabilize training)\n            labels += 0.05 * tf.random.uniform(tf.shape(labels))\n\n            predictions = self.discriminator(combined_images, training=True)\n            d_loss = self.loss_fn(labels, predictions)\n            \n        grads = tape.gradient(d_loss, self.discriminator.trainable_weights)\n        self.d_optimizer.apply_gradients(zip(grads, self.discriminator.trainable_weights))\n\n        # Train the Generator\n        random_latent_vectors = tf.random.normal(shape=(batch_size, self.latent_dim))\n        misleading_labels = tf.zeros((batch_size, 1))\n\n        with tf.GradientTape() as tape:\n            predictions = self.discriminator(self.generator(random_latent_vectors, training=True), training=True)\n            g_loss = self.loss_fn(misleading_labels, predictions)\n            \n        grads = tape.gradient(g_loss, self.generator.trainable_weights)\n        self.g_optimizer.apply_gradients(zip(grads, self.generator.trainable_weights))\n        \n        return {\"d_loss\": d_loss, \"g_loss\": g_loss}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-01T15:06:48.345884Z","iopub.execute_input":"2026-04-01T15:06:48.346316Z","iopub.status.idle":"2026-04-01T15:06:48.360443Z","shell.execute_reply.started":"2026-04-01T15:06:48.346292Z","shell.execute_reply":"2026-04-01T15:06:48.359767Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Training \ngan = GAN(discriminator=build_discriminator(), generator=build_generator())\ngan.compile(\n    d_optimizer=tf.keras.optimizers.Adam(learning_rate=0.0002, beta_1=0.5),\n    g_optimizer=tf.keras.optimizers.Adam(learning_rate=0.0002, beta_1=0.5),\n    loss_fn=tf.keras.losses.BinaryCrossentropy(from_logits=True)\n)\n\ngan.fit(train_dataset, epochs=100)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-01T15:06:48.361306Z","iopub.execute_input":"2026-04-01T15:06:48.361567Z","iopub.status.idle":"2026-04-01T15:11:46.152761Z","shell.execute_reply.started":"2026-04-01T15:06:48.361544Z","shell.execute_reply":"2026-04-01T15:11:46.152194Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def show_generated_images(generator, epoch=None, num_img=8, latent_dim=100):\n    # Generate random noise\n    random_latent_vectors = tf.random.normal(shape=(num_img, latent_dim))\n    # Generate images from noise\n    generated_images = generator(random_latent_vectors, training=False)\n    \n    # Denormalize from [-1, 1] to [0, 1]\n    generated_images = (generated_images * 0.5) + 0.5\n    generated_images = generated_images.numpy()\n\n    # Plot in a grid\n    plt.figure(figsize=(10, 10))\n    for i in range(num_img):\n        plt.subplot(4, 4, i+1)\n        plt.imshow(generated_images[i])\n        plt.axis('off')\n    \n    if epoch is not None:\n        plt.suptitle(f\"Generated Images at Epoch {epoch}\")\n    plt.show()\n\nshow_generated_images(gan.generator)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-01T15:11:46.153526Z","iopub.execute_input":"2026-04-01T15:11:46.153754Z","iopub.status.idle":"2026-04-01T15:11:46.783346Z","shell.execute_reply.started":"2026-04-01T15:11:46.153729Z","shell.execute_reply":"2026-04-01T15:11:46.782634Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Conclusion\n\nDCGAN successfully demonstrated that a dual-network architecture can capture Monet’s signature color palette and painterly textures. However, several technical hurdles were identified during this phase:\n1. The \"Checkerboard\" Artifact Challenge: The grid-like patterns seen are a phenomenon known as checkerboard artifacts.\nThese occur when the Conv2DTranspose (deconvolution) layers have kernels that do not perfectly overlap, creating an uneven distribution of pixel values during the upsampling process. While the colors may be accurate to Monet's style, the structural quality is degraded by these mathematical residues, which would negatively impact a MiFID score.\nReplacing Conv2DTranspose with Bilinear UpSampling2D followed by a standard Conv2D layer effectively eliminates these artifacts by smoothing the image before refining it.\n\n2. Random Noise vs. Content Translation: The fundamental limitation of using DCGAN for this specific competition is the relationship between input and output.\n\nGenerative vs. Transformative: DCGAN is designed to map a latent noise vector ($z$) to an image. This results in \"original\" hallucinations of Monet-like art that do not correspond to any existing photograph. And because the input is random noise, there is no mechanism to ensure the generated image preserves the structure (trees, houses, horizons) of the specific photos provided in the dataset.\n\nWhile DCGAN is excellent for learning a general style distribution, it cannot fulfill the project requirement of transforming 7,000 specific photos into Monet paintings. It lacks the \"Cycle Consistency\" needed to link the photo domain to the painting domain while keeping the content intact.","metadata":{}},{"cell_type":"markdown","source":"## CycleGAN U_Net Architecture","metadata":{}},{"cell_type":"code","source":"def decode_and_normalize(path):\n    img = tf.io.read_file(path)\n    img = tf.image.decode_jpeg(img, channels=3)\n    img = tf.image.resize(img, [256, 256])\n    # Normalize to [-1, 1] for the Generator\n    img = (tf.cast(img, tf.float32) / 127.5) - 1\n    return img\n\n# Create the Photo dataset\nphoto_ds = tf.data.Dataset.from_tensor_slices(photo_images)\nphoto_ds = photo_ds.map(decode_and_normalize).shuffle(1000).batch(1)\n\n# Create the Monet dataset\nmonet_ds = tf.data.Dataset.from_tensor_slices(monet_images)\nmonet_ds = monet_ds.map(decode_and_normalize).shuffle(1000).batch(1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-01T15:11:46.784027Z","iopub.execute_input":"2026-04-01T15:11:46.784227Z","iopub.status.idle":"2026-04-01T15:11:46.866708Z","shell.execute_reply.started":"2026-04-01T15:11:46.784212Z","shell.execute_reply":"2026-04-01T15:11:46.866123Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class InstanceNormalization(tf.keras.layers.Layer):\n    \"\"\"Custom Instance Normalization Layer (Keras)\"\"\"\n    def __init__(self, epsilon=1e-5):\n        super(InstanceNormalization, self).__init__()\n        self.epsilon = epsilon\n\n    def build(self, input_shape):\n        self.scale = self.add_weight(\n            name='scale',\n            shape=input_shape[-1:],\n            initializer=tf.random_normal_initializer(1., 0.02),\n            trainable=True)\n        self.offset = self.add_weight(\n            name='offset',\n            shape=input_shape[-1:],\n            initializer='zeros',\n            trainable=True)\n\n    def call(self, x):\n        # Calculate mean and variance per instance (axes 1 and 2 are height and width)\n        mean, variance = tf.nn.moments(x, axes=[1, 2], keepdims=True)\n        inv = tf.math.rsqrt(variance + self.epsilon)\n        normalized = (x - mean) * inv\n        return self.scale * normalized + self.offset","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-01T15:11:46.873795Z","iopub.execute_input":"2026-04-01T15:11:46.874004Z","iopub.status.idle":"2026-04-01T15:11:46.879697Z","shell.execute_reply.started":"2026-04-01T15:11:46.873989Z","shell.execute_reply":"2026-04-01T15:11:46.879082Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def upsample(filters, size, apply_dropout=False):\n    initializer = tf.random_normal_initializer(0., 0.02)\n    result = tf.keras.Sequential()\n    \n    # Use Resize + Conv instead of Transposed Conv to stop checkerboarding\n    result.add(layers.UpSampling2D(size=(2, 2))) \n    result.add(layers.Conv2D(filters, size, strides=1, padding='same',\n                             kernel_initializer=initializer, use_bias=False))\n    \n    result.add(InstanceNormalization())\n    \n    if apply_dropout:\n        result.add(layers.Dropout(0.5))\n    result.add(layers.ReLU())\n    return result\n\ndef downsample(filters, size, apply_instancenorm=True):\n    initializer = tf.random_normal_initializer(0., 0.02)\n    result = tf.keras.Sequential()\n    result.add(layers.Conv2D(filters, size, strides=2, padding='same', \n                             kernel_initializer=initializer, use_bias=False))\n    \n    # Using Instance Normalization for better style transfer\n    if apply_instancenorm:\n        result.add(InstanceNormalization())\n        \n    result.add(layers.LeakyReLU())\n    return result","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-01T15:11:46.880382Z","iopub.execute_input":"2026-04-01T15:11:46.880727Z","iopub.status.idle":"2026-04-01T15:11:46.893902Z","shell.execute_reply.started":"2026-04-01T15:11:46.880709Z","shell.execute_reply":"2026-04-01T15:11:46.893217Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"OUTPUT_CHANNELS = 3\n\ndef Generator():\n    inputs = layers.Input(shape=[256, 256, 3])\n\n    # Downsampling (Encoder)\n    down_stack = [\n        downsample(64, 4, apply_instancenorm=False), # (bs, 128, 128, 64)\n        downsample(128, 4),                       # (bs, 64, 64, 128)\n        downsample(256, 4),                       # (bs, 32, 32, 256)\n        downsample(512, 4),                       # (bs, 16, 16, 512)\n        downsample(512, 4),                       # (bs, 8, 8, 512)\n        downsample(512, 4),                       # (bs, 4, 4, 512)\n        downsample(512, 4),                       # (bs, 2, 2, 512)\n        downsample(512, 4),                       # (bs, 1, 1, 512)\n    ]\n\n    # Upsampling (Decoder)\n    up_stack = [\n        upsample(512, 4, apply_dropout=True),     # (bs, 2, 2, 1024)\n        upsample(512, 4, apply_dropout=True),     # (bs, 4, 4, 1024)\n        upsample(512, 4, apply_dropout=True),     # (bs, 8, 8, 1024)\n        upsample(512, 4),                        # (bs, 16, 16, 1024)\n        upsample(256, 4),                        # (bs, 32, 32, 512)\n        upsample(128, 4),                        # (bs, 64, 64, 256)\n        upsample(64, 4),                         # (bs, 128, 128, 128)\n    ]\n\n    initializer = tf.random_normal_initializer(0., 0.02)\n    last = layers.Conv2DTranspose(OUTPUT_CHANNELS, 4, strides=2,\n                                  padding='same', kernel_initializer=initializer,\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\n    skips = reversed(skips[:-1])\n\n    for up, skip in zip(up_stack, skips):\n        x = up(x)\n        x = layers.Concatenate()([x, skip])\n\n    x = last(x)\n    return tf.keras.Model(inputs=inputs, outputs=x)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-01T15:11:46.894640Z","iopub.execute_input":"2026-04-01T15:11:46.895635Z","iopub.status.idle":"2026-04-01T15:11:46.904581Z","shell.execute_reply.started":"2026-04-01T15:11:46.895617Z","shell.execute_reply":"2026-04-01T15:11:46.903945Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def Discriminator():\n    initializer = tf.random_normal_initializer(0., 0.02)\n    inputs = layers.Input(shape=[256, 256, 3], name='input_image')\n\n    # Note: First layer usually doesn't use normalization\n    x = downsample(64, 4, apply_instancenorm=False)(inputs) \n    x = downsample(128, 4, apply_instancenorm=True)(x)        \n    x = downsample(256, 4, apply_instancenorm=True)(x)        \n\n    x = layers.ZeroPadding2D()(x) \n    x = layers.Conv2D(512, 4, strides=1, kernel_initializer=initializer,\n                      use_bias=False)(x) \n\n    x = InstanceNormalization()(x) # Use our custom class here\n    x = layers.LeakyReLU()(x)\n    x = layers.ZeroPadding2D()(x) \n\n    last = layers.Conv2D(1, 4, strides=1, \n                         kernel_initializer=initializer)(x) \n\n    return tf.keras.Model(inputs=inputs, outputs=last)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-01T15:11:46.905295Z","iopub.execute_input":"2026-04-01T15:11:46.905576Z","iopub.status.idle":"2026-04-01T15:11:46.917810Z","shell.execute_reply.started":"2026-04-01T15:11:46.905558Z","shell.execute_reply":"2026-04-01T15:11:46.917207Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Training \nmonet_generator = Generator() # transforms photos to Monet-style\nphoto_generator = Generator() # transforms Monets to photo-style\n\nmonet_discriminator = Discriminator() # differentiates real Monet from generated Monet\nphoto_discriminator = Discriminator() # differentiates real photo from generated photo\n\n\n# Standard Binary Cross Entropy for GAN loss\nloss_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 calc_cycle_loss(real_image, cycled_image, LAMBDA):\n    loss1 = tf.reduce_mean(tf.abs(real_image - cycled_image))\n    return LAMBDA * loss1\n\ndef identity_loss(real_image, same_image, LAMBDA):\n    loss = tf.reduce_mean(tf.abs(real_image - same_image))\n    return LAMBDA * 0.5 * loss","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-01T15:11:46.918468Z","iopub.execute_input":"2026-04-01T15:11:46.918635Z","iopub.status.idle":"2026-04-01T15:11:48.117496Z","shell.execute_reply.started":"2026-04-01T15:11:46.918622Z","shell.execute_reply":"2026-04-01T15:11:48.116886Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CycleGan(tf.keras.Model):\n    def __init__(self, monet_generator, photo_generator, \n                 monet_discriminator, photo_discriminator, lambda_cycle=10):\n        super(CycleGan, self).__init__()\n        self.m_gen = monet_generator\n        self.p_gen = photo_generator\n        self.m_disc = monet_discriminator\n        self.p_disc = photo_discriminator\n        self.lambda_cycle = lambda_cycle\n        \n    def compile(self, m_gen_optimizer, p_gen_optimizer, m_disc_optimizer, p_disc_optimizer,\n                gen_loss_fn, disc_loss_fn, cycle_loss_fn, identity_loss_fn):\n        super(CycleGan, self).compile()\n        self.m_gen_optimizer = m_gen_optimizer\n        self.p_gen_optimizer = p_gen_optimizer\n        self.m_disc_optimizer = m_disc_optimizer\n        self.p_disc_optimizer = p_disc_optimizer\n        self.gen_loss_fn = gen_loss_fn\n        self.disc_loss_fn = disc_loss_fn\n        self.cycle_loss_fn = cycle_loss_fn\n        self.identity_loss_fn = identity_loss_fn\n\n    @tf.function\n    def train_step(self, batch_data):\n        real_monet, real_photo = batch_data\n        \n        with tf.GradientTape(persistent=True) as tape:\n            # 1. Generate Fake Images\n            fake_monet = self.m_gen(real_photo, training=True)\n            cycled_photo = self.p_gen(fake_monet, training=True)\n\n            fake_photo = self.p_gen(real_monet, training=True)\n            cycled_monet = self.m_gen(fake_photo, training=True)\n\n            # 2. Identity Mapping (G(y) -> y and F(x) -> x)\n            same_monet = self.m_gen(real_monet, training=True)\n            same_photo = self.p_gen(real_photo, training=True)\n\n            # 3. Discriminator Outputs\n            disc_real_monet = self.m_disc(real_monet, training=True)\n            disc_fake_monet = self.m_disc(fake_monet, training=True)\n\n            disc_real_photo = self.p_disc(real_photo, training=True)\n            disc_fake_photo = self.p_disc(fake_photo, training=True)\n\n            # 4. Generator Losses\n            monet_gen_loss = self.gen_loss_fn(disc_fake_monet)\n            photo_gen_loss = self.gen_loss_fn(disc_fake_photo)\n\n            # 5. Cycle & Identity Losses\n            total_cycle_loss = self.cycle_loss_fn(real_monet, cycled_monet, self.lambda_cycle) + \\\n                               self.cycle_loss_fn(real_photo, cycled_photo, self.lambda_cycle)\n\n            total_monet_gen_loss = monet_gen_loss + total_cycle_loss + \\\n                                   self.identity_loss_fn(real_monet, same_monet, self.lambda_cycle)\n            total_photo_gen_loss = photo_gen_loss + total_cycle_loss + \\\n                                   self.identity_loss_fn(real_photo, same_photo, self.lambda_cycle)\n\n            # 6. Discriminator Losses\n            monet_disc_loss = self.disc_loss_fn(disc_real_monet, disc_fake_monet)\n            photo_disc_loss = self.disc_loss_fn(disc_real_photo, disc_fake_photo)\n\n        # Calculate and Apply Gradients\n        m_gen_grads = tape.gradient(total_monet_gen_loss, self.m_gen.trainable_variables)\n        p_gen_grads = tape.gradient(total_photo_gen_loss, self.p_gen.trainable_variables)\n        self.m_gen_optimizer.apply_gradients(zip(m_gen_grads, self.m_gen.trainable_variables))\n        self.p_gen_optimizer.apply_gradients(zip(p_gen_grads, self.p_gen.trainable_variables))\n\n        m_disc_grads = tape.gradient(monet_disc_loss, self.m_disc.trainable_variables)\n        p_disc_grads = tape.gradient(photo_disc_loss, self.p_disc.trainable_variables)\n        self.m_disc_optimizer.apply_gradients(zip(m_disc_grads, self.m_disc.trainable_variables))\n        self.p_disc_optimizer.apply_gradients(zip(p_disc_grads, self.p_disc.trainable_variables))\n        \n        return {\n            \"monet_gen_loss\": total_monet_gen_loss,\n            \"photo_gen_loss\": total_photo_gen_loss,\n            \"monet_disc_loss\": monet_disc_loss,\n            \"photo_disc_loss\": photo_disc_loss\n        }","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-01T15:11:48.118223Z","iopub.execute_input":"2026-04-01T15:11:48.118442Z","iopub.status.idle":"2026-04-01T15:11:48.130833Z","shell.execute_reply.started":"2026-04-01T15:11:48.118425Z","shell.execute_reply":"2026-04-01T15:11:48.130101Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Initialize models\ncycle_gan_model = CycleGan(Generator(), Generator(), Discriminator(), Discriminator())\n\n# Compile\ncycle_gan_model.compile(\n    m_gen_optimizer=tf.keras.optimizers.Adam(2e-4, beta_1=0.5),\n    p_gen_optimizer=tf.keras.optimizers.Adam(2e-4, beta_1=0.5),\n    m_disc_optimizer=tf.keras.optimizers.Adam(2e-4, beta_1=0.5),\n    p_disc_optimizer=tf.keras.optimizers.Adam(2e-4, beta_1=0.5),\n    gen_loss_fn=generator_loss,\n    disc_loss_fn=discriminator_loss,\n    cycle_loss_fn=calc_cycle_loss,\n    identity_loss_fn=identity_loss\n)\n\n# Zip the photo and monet datasets for training\nfull_dataset = tf.data.Dataset.zip((monet_ds, photo_ds))\n\n# Train (25-50 epochs is typical for decent results)\ncycle_gan_model.fit(full_dataset, epochs=25)\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-01T15:11:48.131521Z","iopub.execute_input":"2026-04-01T15:11:48.131767Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def generate_images(model, test_input):\n    # Generate the Monet-style images for the whole batch\n    prediction = model(test_input, training=False)\n\n    plt.figure(figsize=(8, 8))\n\n    for i in range(4):\n        # Original Photo\n        plt.subplot(4, 2, 2*i + 1)\n        plt.title(\"Original Photo\")\n        plt.imshow(test_input[i] * 0.5 + 0.5)\n        plt.axis('off')\n\n        # Generated Monet\n        plt.subplot(4, 2, 2*i + 2)\n        plt.title(\"Generated Monet\")\n        plt.imshow(prediction[i] * 0.5 + 0.5)\n        plt.axis('off')\n    \n    plt.tight_layout()\n    plt.show()\n\nsample_batch = next(iter(photo_ds.unbatch().batch(4)))\ngenerate_images(cycle_gan_model.m_gen, sample_batch)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Final Submission \nos.makedirs('../images', exist_ok=True)\ni = 1\nfor img in photo_ds:\n    prediction = cycle_gan_model.m_gen(img, training=False)[0].numpy()\n    prediction = (prediction * 127.5 + 127.5).astype(np.uint8)\n    im = Image.fromarray(prediction)\n    im.save(f\"../images/{i}.jpg\")\n    i += 1\n    if i > 7000: # Submission requirement\n        break\n\nshutil.make_archive('/kaggle/working/images', 'zip', '../images')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Conclusion \n1. Architectural Upgrades: Unlike the DCGAN, the CycleGAN uses a more complex system of four neural networks (two Generators and two Discriminators) and specific architectural improvements:\n   * U-Net Generator: By using a U-Net with Skip Connections, the model \"remembers\" the fine structural details (like trees or horizons) of the original photo, ensuring the \"what\" of the image isn't lost during the translation.\n   * PatchGAN Discriminator: Instead of evaluating the whole image, this discriminator looks at $70 \\times 70$ patches, forcing the Generator to focus on high-frequency artistic details like specific Monet brushstrokes.\n   * Instance Normalization: Replacing Batch Normalization with Instance Normalization was critical; it normalizes each image independently, which prevents style \"wash-out\" and helps capture artistic textures more effectively.\n     \n2. Solving the Artifact Issue\nThe CycleGAN progress photos showed a significant reduction in the checkerboard artifacts seen in the DCGAN. This was achieved by replacing Conv2DTranspose with a combination of UpSampling2D (Bilinear) and standard Conv2D, the model eliminated the mathematical pixel overlaps that created the grid patterns. resulting in smoother color gradients and a more \"painterly\" feel in the sky and water regions of your generated images.\n\n3. The Dual-Loss Advantage: CycleGAN's success in this competition relies on two specialized loss functions that DCGAN lacks:\n\n    * Cycle Consistency Loss: This ensures that if a photo is turned into a Monet and then back into a photo, the result is identical to the original. This is the \"anchor\" that prevents the model from hallucinating random shapes.\n\n* Identity Loss: This encourages the Generator to preserve the original color palette of the input when it already matches the target style, helping maintain the natural lighting of your photos.\n\nThe final model preserves the structural \"content\" of the photos while applying the \"style\" of Monet. ","metadata":{}},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}