{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"tpuV5e8","dataSources":[{"sourceId":21755,"databundleVersionId":1475600,"sourceType":"competition"}],"dockerImageVersionId":31192,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"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        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":"2025-11-11T22:13:23.703756Z","iopub.execute_input":"2025-11-11T22:13:23.703938Z","iopub.status.idle":"2025-11-11T22:13:39.994979Z","shell.execute_reply.started":"2025-11-11T22:13:23.703909Z","shell.execute_reply":"2025-11-11T22:13:39.994103Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Import Required Libraries","metadata":{}},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport seaborn as sns\nimport matplotlib.pyplot as plt\nfrom collections import Counter\n\nfrom PIL import Image\nimport os\nimport shutil\nfrom pathlib import Path \n\nimport keras\nimport tensorflow as tf\nimport tensorflow_datasets as tfds\nfrom keras import layers\nfrom keras import ops\nimport matplotlib.pyplot as plt\nimport os\n\nfrom zipfile import ZipFile\nimport keras\nfrom keras.models import Sequential\nfrom keras.layers import Dense, Conv2D, Flatten\nfrom tensorflow.keras import regularizers\nimport random\nimport PIL\nfrom PIL import Image\n\n\nfrom tensorflow.keras.utils import load_img, img_to_array\nimport warnings\nwarnings.filterwarnings('ignore')\n\ntfds.disable_progress_bar()\nautotune = tf.data.AUTOTUNE\n\nos.environ[\"KERAS_BACKEND\"] = \"tensorflow\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-16T23:39:29.751384Z","iopub.execute_input":"2025-11-16T23:39:29.751517Z","iopub.status.idle":"2025-11-16T23:40:05.367818Z","shell.execute_reply.started":"2025-11-16T23:39:29.751501Z","shell.execute_reply":"2025-11-16T23:40:05.367059Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Problem Description","metadata":{}},{"cell_type":"markdown","source":"The objective is to implement a GAN architecture to synthesize a dataset of 7,000 to 10,000 images consistent with Monet's artistic style. The problem requires to build two components: a neural network generator model, responsible for image creation, and a discriminator model, which evaluates authenticity. The training dynamic is adversarial: the generator aims to produce outputs indistinguishable from real Monet paintings, while the discriminator strives to correctly identify generated images, fostering continuous improvement in the generator's output quality. \n\n Task is to build a GAN that generates 7,000 to 10,000 Monet-style images.","metadata":{}},{"cell_type":"markdown","source":"## Exploratory Data Analysis","metadata":{}},{"cell_type":"code","source":"path = '/kaggle/input/gan-getting-started/'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-11T19:50:08.394514Z","iopub.execute_input":"2025-11-11T19:50:08.395150Z","iopub.status.idle":"2025-11-11T19:50:08.399591Z","shell.execute_reply.started":"2025-11-11T19:50:08.395123Z","shell.execute_reply":"2025-11-11T19:50:08.398665Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Data Description","metadata":{}},{"cell_type":"markdown","source":"The dataset contains four directories: monet_tfrec, photo_tfrec, monet_jpg, and photo_jpg. The monet_tfrec and monet_jpg directories contain the same painting images, and the photo_tfrec and photo_jpg directories contain the same photos.\n\nThe monet directories contain Monet paintings. Used to train model.The photo directories contain photos. \n\n**Files**\n\nmonet_jpg - 300 Monet paintings sized 256x256 in JPG format\n\nmonet_tfrec - 300 Monet paintings sized 256x256 in TFRecord format\n\nphoto_jpg - 7028 photos sized 256x256 in JPG format\n\nphoto_tfrec - 7028 photos sized 256x256 in TFRecord format","metadata":{}},{"cell_type":"markdown","source":"### Train Image Files","metadata":{}},{"cell_type":"markdown","source":"Image files that are in the monet directory are the train data. 300 Monet paintings in monet_jpeg are 256 x 256 JPEG format. These files are in the directory '/kaggle/input/gan-getting-started/monet_jpg'\n\n300 Image files in monet_tfrec of size 256 * 256 in TFRecord format. These files are in the directory '/kaggle/input/gan-getting-started/monet_tfrec/'. The monet_tfrec and monet_jpg directories contain the same painting images.","metadata":{}},{"cell_type":"code","source":"image_directory = '/kaggle/input/gan-getting-started/monet_tfrec/'\n\ntr_image_files = [os.path.join(image_directory, f) for f in os.listdir(image_directory) \n               if f.endswith(('.tfrec'))]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-14T20:34:25.655113Z","iopub.execute_input":"2025-11-14T20:34:25.656008Z","iopub.status.idle":"2025-11-14T20:34:25.668362Z","shell.execute_reply.started":"2025-11-14T20:34:25.655977Z","shell.execute_reply":"2025-11-14T20:34:25.667697Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Number of .tfrec files","metadata":{}},{"cell_type":"code","source":"a = len(tr_image_files)\nprint(\"Number of .tfrec files :\", a)  # 5","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-14T20:34:29.150348Z","iopub.execute_input":"2025-11-14T20:34:29.150654Z","iopub.status.idle":"2025-11-14T20:34:29.155005Z","shell.execute_reply.started":"2025-11-14T20:34:29.150631Z","shell.execute_reply":"2025-11-14T20:34:29.154277Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Number of JPG Files","metadata":{}},{"cell_type":"code","source":"# Number of files in dir /kaggle/input/gan-getting-started/monet_jpg/\njpg_img_dir = '/kaggle/input/gan-getting-started/monet_jpg/'\n\njpg_img_files = [os.path.join(jpg_img_dir, f) for f in os.listdir(jpg_img_dir) \n               if f.endswith(('.jpg'))]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-11T22:14:47.778850Z","iopub.execute_input":"2025-11-11T22:14:47.779126Z","iopub.status.idle":"2025-11-11T22:14:47.783994Z","shell.execute_reply.started":"2025-11-11T22:14:47.779106Z","shell.execute_reply":"2025-11-11T22:14:47.783284Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"b = len(jpg_img_files)\nprint(\"Number of .jpg files :\", b) # 300","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-11T14:04:27.110799Z","iopub.execute_input":"2025-11-11T14:04:27.111139Z","iopub.status.idle":"2025-11-11T14:04:27.116015Z","shell.execute_reply.started":"2025-11-11T14:04:27.111113Z","shell.execute_reply":"2025-11-11T14:04:27.115052Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Check for Duplicates in Train Images","metadata":{}},{"cell_type":"code","source":"def find_duplicates_with_counter(input_list):\n    counts = Counter(input_list)\n    duplicates = [item for item, count in counts.items() if count > 1]\n    return duplicates\n\nduplicate_values = find_duplicates_with_counter(tr_image_files)\nprint(f\"Duplicate images in monet_tfrec folder: {duplicate_values}\")\n\nduplicate_values = find_duplicates_with_counter(jpg_img_files)\nprint(f\"Duplicate images in monet_jpg folder: {duplicate_values}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T19:53:44.944077Z","iopub.execute_input":"2025-11-10T19:53:44.944483Z","iopub.status.idle":"2025-11-10T19:53:44.952191Z","shell.execute_reply.started":"2025-11-10T19:53:44.944458Z","shell.execute_reply":"2025-11-10T19:53:44.951149Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Test Images","metadata":{}},{"cell_type":"code","source":"image_directory_test = '/kaggle/input/gan-getting-started/photo_jpg/'\n    \n# list of image file paths\nimage_files_test = [os.path.join(image_directory_test, f) \n                    for f in os.listdir(image_directory_test) if f.endswith(('.jpg', '.png', '.jpeg', '.tif'))]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-11T19:50:52.637275Z","iopub.execute_input":"2025-11-11T19:50:52.637709Z","iopub.status.idle":"2025-11-11T19:50:52.652724Z","shell.execute_reply.started":"2025-11-11T19:50:52.637674Z","shell.execute_reply":"2025-11-11T19:50:52.651973Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Number of Test Images","metadata":{}},{"cell_type":"code","source":"c = len(image_files_test)\nprint(\"Number of jpg test files :\", c)  # 7038","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-11T19:51:00.133033Z","iopub.execute_input":"2025-11-11T19:51:00.133374Z","iopub.status.idle":"2025-11-11T19:51:00.138680Z","shell.execute_reply.started":"2025-11-11T19:51:00.133349Z","shell.execute_reply":"2025-11-11T19:51:00.137735Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Image Visualizations","metadata":{}},{"cell_type":"markdown","source":"### Plots of Train Images","metadata":{}},{"cell_type":"code","source":"# Define a function to decode the images from the TFRecord\n\ndef decode_image_from_tfrec(record_bytes):\n    \n    feature_description = {\n        'image': tf.io.FixedLenFeature([], tf.string),\n       \n    }\n    example = tf.io.parse_single_example(record_bytes, feature_description)\n    image = tf.image.decode_jpeg(example['image'], channels=3)\n    # Normalize \n    image = tf.cast(image, tf.float32) / 255.0 \n    return image\n\n# List of paths to your .tfrec files\nimage_paths = [\n    '/kaggle/input/gan-getting-started/monet_tfrec/monet12-60.tfrec',\n    '/kaggle/input/gan-getting-started/monet_tfrec/monet16-60.tfrec',\n    '/kaggle/input/gan-getting-started/monet_tfrec/monet08-60.tfrec',\n    '/kaggle/input/gan-getting-started/monet_tfrec/monet04-60.tfrec',\n    '/kaggle/input/gan-getting-started/monet_tfrec/monet00-60.tfrec',\n    '/kaggle/input/gan-getting-started/monet_tfrec/monet12-60.tfrec',\n]\n\nfig, axs = plt.subplots(2, 3, figsize=(7, 5))\naxs = axs.flatten()\n\n# Iterate through the first few TFRecord files and display the first image from each\nfor i, img_path in enumerate(image_paths):\n    # Create a TFRecordDataset for the single file\n    dataset = tf.data.TFRecordDataset(img_path)\n    \n    # Map the decoding function to the dataset and get the first image\n    # We use as_numpy_iterator() to access the data in a Python loop\n    first_image = next(iter(dataset.map(decode_image_from_tfrec).batch(1)))\n    \n    # Convert the tensor to a numpy array for matplotlib\n    img_array = first_image.numpy().squeeze() \n\n    axs[i].imshow(img_array)\n    axs[i].axis('off')\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-11T14:05:08.857858Z","iopub.execute_input":"2025-11-11T14:05:08.858168Z","iopub.status.idle":"2025-11-11T14:05:09.770610Z","shell.execute_reply.started":"2025-11-11T14:05:08.858145Z","shell.execute_reply":"2025-11-11T14:05:09.769620Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The first image is repeated as there are 2 rows with 3 images each. The function decode_image_from_tfrec gets the image path as argument, decodes into a jpeg image and normalize it for better viewing. ","metadata":{}},{"cell_type":"markdown","source":"## Monet JPG Sample Images","metadata":{}},{"cell_type":"code","source":"fig, axs = plt.subplots(2, 3, figsize=(7, 5))\n\n# Flatten the `axs` array for easier iteration\naxs = axs.flatten()\n\nimages_to_display = random.sample(jpg_img_files, min(len(jpg_img_files), 6))\n\n# Display  the first 6 images in their own subplot\nfor i, img_path in enumerate(images_to_display):\n    #img_path = jpg_img_files[i]\n    img = Image.open(img_path)\n    axs[i].imshow(img)\n    axs[i].axis('off')\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-11T14:05:19.334900Z","iopub.execute_input":"2025-11-11T14:05:19.335195Z","iopub.status.idle":"2025-11-11T14:05:19.758005Z","shell.execute_reply.started":"2025-11-11T14:05:19.335176Z","shell.execute_reply":"2025-11-11T14:05:19.757094Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Photo JPG Sample Images","metadata":{}},{"cell_type":"code","source":"#/kaggle/input/gan-getting-started/photo_jpg/\n\nfig, axs = plt.subplots(2, 3, figsize=(7, 5))\n\n# Flatten the `axs` array for easier iteration\naxs = axs.flatten()\n\nimages_to_display = random.sample(image_files_test, min(len(image_files_test), 6))\n\n# Display  the first 6 images in their own subplot\nfor i, img_path in enumerate(images_to_display):\n    #img_path = jpg_img_files[i]\n    img = Image.open(img_path)\n    axs[i].imshow(img)\n    axs[i].axis('off')\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-11T14:07:50.474538Z","iopub.execute_input":"2025-11-11T14:07:50.474951Z","iopub.status.idle":"2025-11-11T14:07:50.950154Z","shell.execute_reply.started":"2025-11-11T14:07:50.474925Z","shell.execute_reply":"2025-11-11T14:07:50.949216Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Properties of Images","metadata":{}},{"cell_type":"markdown","source":"#### Size of Image","metadata":{}},{"cell_type":"code","source":"image_path = '/kaggle/input/gan-getting-started/monet_jpg/d14c1abdd4.jpg'\n\nwith Image.open(image_path) as img:\n    width, height = img.size\n    print(f\"Image Format: {img.format}\")\n    print(f\"Image Mode: {img.mode}\")\n    print(f\"Width: {width} pixels\")\n    print(f\"Height: {height} pixels\")\n    print(f\"Dimensions (WxH): {img.size}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-12T15:04:30.160094Z","iopub.execute_input":"2025-11-12T15:04:30.160851Z","iopub.status.idle":"2025-11-12T15:04:30.170406Z","shell.execute_reply.started":"2025-11-12T15:04:30.160825Z","shell.execute_reply":"2025-11-12T15:04:30.169623Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Image and Grayscale","metadata":{}},{"cell_type":"code","source":"\n    # Open the image file\nwith Image.open(image_path) as color_image:\n        # Convert the image to grayscale\n        grayscale_image = color_image.convert('L') # 'L' mode is for grayscale\n\n        # Use matplotlib to display both images side-by-side\n        fig, axs = plt.subplots(1, 2, figsize=(10, 5))\n\n        # Display the original color image\n        axs[0].imshow(color_image)\n        axs[0].set_title('Original Image')\n        axs[0].axis('off')  # Hide the axes\n\n        # Display the grayscale image\n        # The cmap='gray' argument is crucial for matplotlib to render it correctly\n        axs[1].imshow(grayscale_image, cmap='gray')\n        axs[1].set_title('Grayscale Image')\n        axs[1].axis('off')  # Hide the axes\n\n    plt.tight_layout()\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T21:22:14.160620Z","iopub.execute_input":"2025-11-10T21:22:14.160986Z","iopub.status.idle":"2025-11-10T21:22:14.602728Z","shell.execute_reply.started":"2025-11-10T21:22:14.160960Z","shell.execute_reply":"2025-11-10T21:22:14.601743Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Rotated and Resized Images","metadata":{}},{"cell_type":"code","source":"with Image.open(image_path) as img:\n    resized_img = img.resize((128, 128))\n    rotated_img = resized_img.rotate(90)\n\n    fig, axs = plt.subplots(1, 3, figsize=(15, 5))\n    titles = ['original (256x256)', 'Resized (128x128)', 'Rotated 90deg anticlockwise']\n\n        # Display the Original image\n    axs[0].imshow(img)\n    axs[0].set_title(titles[0])\n    axs[0].axis('off')\n\n    # Display the Resized image\n    axs[1].imshow(resized_img)\n    axs[1].set_title(titles[1])\n    axs[1].axis('off')\n\n    # Display the Rotated image\n    axs[2].imshow(rotated_img)\n    axs[2].set_title(titles[2])\n    axs[2].axis('off')\n\n    plt.tight_layout()\n    plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-12T15:05:22.334959Z","iopub.execute_input":"2025-11-12T15:05:22.335541Z","iopub.status.idle":"2025-11-12T15:05:22.696583Z","shell.execute_reply.started":"2025-11-12T15:05:22.335518Z","shell.execute_reply":"2025-11-12T15:05:22.695638Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Pixel Distribution of Image","metadata":{}},{"cell_type":"code","source":"# getting a sample image\n\nsample_image = '/kaggle/input/gan-getting-started/monet_jpg/79224da51f.jpg'\nwith Image.open(sample_image) as img:\n    img_array = np.asarray(img)\n    colors = ('red', 'green', 'blue')\n    for i, color in enumerate(colors):\n        plt.hist(img_array[:, :, i].ravel(), bins=256, color=color, alpha=0.5)\n    plt.title('Pixel Distribution (RGB Histograms)')\n    plt.xlabel('Pixel Intensity')\n    plt.ylabel('Frequency')\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T21:35:45.627125Z","iopub.execute_input":"2025-11-10T21:35:45.627948Z","iopub.status.idle":"2025-11-10T21:35:46.601030Z","shell.execute_reply.started":"2025-11-10T21:35:45.627904Z","shell.execute_reply":"2025-11-10T21:35:46.600125Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Boxplot of Pixel Distribution","metadata":{}},{"cell_type":"code","source":"with Image.open(sample_image) as img:\n    img_array = np.asarray(img)\n    if img_array.ndim == 3 :\n        red_channel = img_array[:, :, 0].flatten()\n        green_channel = img_array[:, :, 1].flatten()\n        blue_channel = img_array[:, :, 2].flatten()\n    \n    data = [red_channel, green_channel, blue_channel]\n    labels = ['Red', 'Green', 'Blue']\n    title = 'Pixel Distribution by Color Channel'\n\nplt.figure(figsize=(10, 6))\nplt.boxplot(data, vert=True, patch_artist=True, labels=labels)\nplt.title(title)\nplt.ylabel('Pixel Value')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T21:38:29.744623Z","iopub.execute_input":"2025-11-10T21:38:29.745019Z","iopub.status.idle":"2025-11-10T21:38:29.925672Z","shell.execute_reply.started":"2025-11-10T21:38:29.744994Z","shell.execute_reply":"2025-11-10T21:38:29.924786Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Image Statistics","metadata":{}},{"cell_type":"code","source":"from PIL import Image, ImageStat\n\nwith Image.open(sample_image) as img:\n    #img_array = np.asarray(img)\n    stat = ImageStat.Stat(img)\n\n    print(f\"Mean pixel value: {stat.mean[0]:.2f}\")\n    print(f\"Median pixel value: {stat.median[0]}\")\n    print(f\"Min/Max pixel values: {stat.extrema[0]}\")\n    print(f\"Standard deviation: {stat.stddev[0]:.2f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T21:44:00.657821Z","iopub.execute_input":"2025-11-10T21:44:00.658601Z","iopub.status.idle":"2025-11-10T21:44:00.667803Z","shell.execute_reply.started":"2025-11-10T21:44:00.658572Z","shell.execute_reply":"2025-11-10T21:44:00.666872Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Project Execution Plan","metadata":{}},{"cell_type":"markdown","source":"1.**Preprocess data**: Decode and convert both monet and photo jpg images to train_monet, test_monet and train_photo test_photo dataset in the split of (80, 20) with appropriate labels.  Images in train_monet, test_monet are labelled 1 and images in train_photo test_photo are labelled 0.\n\n2.**CycleGAN Layers Implementation**: define the generator and discriminator models using Keras layers.\n\n3.**Define loss functions**\n\n4.**Train and test the model**\n\n5.Hyperparameter Tuning and finding the best model\n\n6.Generate sample test Monet style transfer images\n\n7.Computation of FID\n\n8. Generate files for submission","metadata":{}},{"cell_type":"markdown","source":"## Preprocessing Images","metadata":{}},{"cell_type":"markdown","source":"#### Define Constants and Initialize - Image Size, Kernels, Bias, Buffer size and Batch Size","metadata":{}},{"cell_type":"code","source":"# Define the standard image size.\norig_img_size = (286, 286)\n# Size of the random crops to be used during training.\ninput_img_size = (256, 256, 3) # Model got stuck in first epoch trying 128*128\n#input_img_size = (128, 128, 3)\n# Weights initializer for the layers.\nkernel_init = keras.initializers.RandomNormal(mean=0.0, stddev=0.02)\n# Gamma initializer for instance normalization.\ngamma_init = keras.initializers.RandomNormal(mean=0.0, stddev=0.02)\n\nbuffer_size = 256\nbatch_size = 1\n# Second Iteration\n#batch_size = 4","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-16T23:40:30.222924Z","iopub.execute_input":"2025-11-16T23:40:30.223218Z","iopub.status.idle":"2025-11-16T23:40:30.226700Z","shell.execute_reply.started":"2025-11-16T23:40:30.223199Z","shell.execute_reply":"2025-11-16T23:40:30.226012Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### Convert JPG Files to Dataset and Dataset to Tensor Batches","metadata":{}},{"cell_type":"code","source":"import tensorflow as tf\nimport math \n\n# --- Configuration Variables ---\nAUTOTUNE = tf.data.AUTOTUNE\nBUFFER_SIZE = 1000\nBATCH_SIZE = 1 # Set  batch size  (1 is common for CycleGAN)\n#BATCH_SIZE = 4\n\n# Define the paths to monet and photo image directories\nmonet_dir = \"/kaggle/input/gan-getting-started/monet_jpg/\"\nphoto_dir = \"/kaggle/input/gan-getting-started/photo_jpg/\"\n\ndef decode_image(image_path):\n    image = tf.io.read_file(image_path)\n    image = tf.image.decode_jpeg(image, channels=3)\n    image = tf.image.resize(image, [256, 256])\n    # Normalize images to [-1, 1] as commonly done in GANs\n    image = (image / 127.5) - 1.0\n    return image\n    \n# Create datasets of file paths\nmonet_files = tf.data.Dataset.list_files(monet_dir + \"*.jpg\", shuffle=True) \nphoto_files = tf.data.Dataset.list_files(photo_dir + \"*.jpg\", shuffle=True)\n\n# Map the decoding function to the file paths dataset\nmonet_dataset = monet_files.map(decode_image, num_parallel_calls=AUTOTUNE)\nphoto_dataset = photo_files.map(decode_image, num_parallel_calls=AUTOTUNE)\n\n# Determine the size efficiently using glob and len()\nmonet_size = len(tf.io.gfile.glob(monet_dir + \"*.jpg\"))\nphoto_size = len(tf.io.gfile.glob(photo_dir + \"*.jpg\"))\n\n# 80:20 split\ntrain_monet_size = int(0.8 * monet_size)\ntrain_photo_size = int(0.8 * photo_size)\n\n# The datasets are already shuffled from the list_files step\n\ntrain_monet_raw = monet_dataset.take(train_monet_size)\ntest_monet_raw = monet_dataset.skip(train_monet_size)\n\ntrain_photo_raw = photo_dataset.take(train_photo_size)\ntest_photo_raw = photo_dataset.skip(train_photo_size)\n\n# The add_monet_label and add_photo_label functions are removed or ignored.\n\n# --- FIX: Apply final processing (cache, shuffle, batch, prefetch) ---\n\n# train_monet now yields batches of shape (BATCH_SIZE, 256, 256, 3)\ntrain_monet = (\n    train_monet_raw\n    .cache()\n    .shuffle(BUFFER_SIZE)\n    .batch(BATCH_SIZE, drop_remainder=True) # drop_remainder=True ensures fixed batch size\n    .prefetch(AUTOTUNE)\n)\n\n# train_photo now yields batches of shape (BATCH_SIZE, 256, 256, 3)\ntrain_photo = (\n    train_photo_raw\n    .cache()\n    .shuffle(BUFFER_SIZE)\n    .batch(BATCH_SIZE, drop_remainder=True)\n    .prefetch(AUTOTUNE)\n)\n\n# test_monet now yields batches of shape (BATCH_SIZE, 256, 256, 3)\ntest_monet = (\n    test_monet_raw\n    .cache()\n    .batch(BATCH_SIZE, drop_remainder=True)\n    .prefetch(AUTOTUNE)\n)\n\n# test_photo now yields batches of shape (BATCH_SIZE, 256, 256, 3)\ntest_photo = (\n    test_photo_raw\n    .cache()\n    .batch(BATCH_SIZE, drop_remainder=True)\n    .prefetch(AUTOTUNE)\n)\n\n# --- Verification Step (Optional) ---\nprint(\"Verification of Dataset Shapes:\")\nfor element in train_monet.take(1):\n    # This should print a shape like (1, 128, 128, 3) if BATCH_SIZE=1\n    print(f\"train_monet batch shape: {element.shape}\") \n\nfor element in train_photo.take(1):\n    # This should print a shape like (1, 128, 128, 3) if BATCH_SIZE=1\n    print(f\"train_photo batch shape: {element.shape}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-16T23:42:46.005979Z","iopub.execute_input":"2025-11-16T23:42:46.007120Z","iopub.status.idle":"2025-11-16T23:42:47.265009Z","shell.execute_reply.started":"2025-11-16T23:42:46.007101Z","shell.execute_reply":"2025-11-16T23:42:47.263903Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## CycleGAN Model Architecture Flow","metadata":{}},{"cell_type":"markdown","source":"1.\t**Input Layer**: Takes an image (256x256x3).\n2.\t**Encoder** (Downsampling): Reduces spatial size, increases feature depth.\n3.\t**Transformer** (Residual Blocks): Processes features at a deep level.\n4.\t**Decoder** (Upsampling): Restores spatial size, reduces feature depth.\n5.\t**Output Layer**: Generates the final translated image.","metadata":{}},{"cell_type":"markdown","source":"#### Implementation of Residual Network Block","metadata":{}},{"cell_type":"code","source":"class ReflectionPadding2D(layers.Layer):\n    \"\"\"Implements Reflection Padding as a layer.\n\n    Args:\n        padding(tuple): Amount of padding for the\n        spatial dimensions.\n\n    Returns:\n        A padded tensor with the same type as the input tensor.\n    \"\"\"\n\n    def __init__(self, padding=(1, 1), **kwargs):\n        self.padding = tuple(padding)\n        super().__init__(**kwargs)\n\n    def call(self, input_tensor, mask=None):\n        padding_width, padding_height = self.padding\n        padding_tensor = [\n            [0, 0],\n            [padding_height, padding_height],\n            [padding_width, padding_width],\n            [0, 0],\n        ]\n        return ops.pad(input_tensor, padding_tensor, mode=\"REFLECT\")\n\n\ndef residual_block(\n    x,\n    activation,\n    kernel_initializer=kernel_init,\n    kernel_size=(3, 3),\n    strides=(1, 1),\n    padding=\"valid\",\n    gamma_initializer=gamma_init,\n    use_bias=False,\n):\n    dim = x.shape[-1]\n    input_tensor = x\n\n    x = ReflectionPadding2D()(input_tensor)\n    x = layers.Conv2D(\n        dim,\n        kernel_size,\n        strides=strides,\n        kernel_initializer=kernel_initializer,\n        padding=padding,\n        use_bias=use_bias,)(x)\n    x = keras.layers.GroupNormalization(groups=1, gamma_initializer=gamma_initializer)(x)\n    x = activation(x)\n\n    x = ReflectionPadding2D()(x)\n    \n    x = layers.Conv2D(\n        dim,\n        kernel_size,\n        strides=strides,\n        kernel_initializer=kernel_initializer,\n        padding=padding,\n        use_bias=use_bias,)(x)\n    x = keras.layers.GroupNormalization(groups=1, gamma_initializer=gamma_initializer)(x)\n    x = layers.add([input_tensor, x])\n    \n    return x\n\n\ndef downsample(\n    x,\n    filters,\n    activation,\n    kernel_initializer=kernel_init,\n    kernel_size=(3, 3),\n    strides=(2, 2),\n    padding=\"same\",\n    gamma_initializer=gamma_init,\n    use_bias=False,\n):\n    x = layers.Conv2D(\n        filters,\n        kernel_size,\n        strides=strides,\n        kernel_initializer=kernel_initializer,\n        padding=padding,\n        use_bias=use_bias,\n    )(x)\n    x = keras.layers.GroupNormalization(groups=1, gamma_initializer=gamma_initializer)(\n        x\n    )\n    if activation:\n        x = activation(x)\n    return x\n\n\ndef upsample(\n    x,\n    filters,\n    activation,\n    kernel_size=(3, 3),\n    strides=(2, 2),\n    padding=\"same\",\n    kernel_initializer=kernel_init,\n    gamma_initializer=gamma_init,\n    use_bias=False,\n):\n    x = layers.Conv2DTranspose(\n        filters,\n        kernel_size,\n        strides=strides,\n        padding=padding,\n        kernel_initializer=kernel_initializer,\n        use_bias=use_bias,\n    )(x)\n    x = keras.layers.GroupNormalization(groups=1, gamma_initializer=gamma_initializer)(\n        x\n    )\n    if activation:\n        x = activation(x)\n    return x\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-16T23:43:21.160979Z","iopub.execute_input":"2025-11-16T23:43:21.161214Z","iopub.status.idle":"2025-11-16T23:43:21.168525Z","shell.execute_reply.started":"2025-11-16T23:43:21.161198Z","shell.execute_reply":"2025-11-16T23:43:21.167770Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Build Generators","metadata":{}},{"cell_type":"markdown","source":"The generator consists of downsampling blocks: nine residual blocks\nand upsampling blocks. The structure of the generator is the following:\n\n```\nc7s1-64 ==> Conv block with `relu` activation, filter size of 7\nd128 ====|\n         |-> 2 downsampling blocks\nd256 ====|\nR256 ====|\nR256     |\nR256     |\nR256     |\nR256     |-> 9 residual blocks\nR256     |\nR256     |\nR256     |\nR256 ====|\nu128 ====|\n         |-> 2 upsampling blocks\nu64  ====|\nc7s1-3 => Last conv block with `tanh` activation, filter size of 7.\n```","metadata":{}},{"cell_type":"markdown","source":"## Build Residual Network","metadata":{}},{"cell_type":"code","source":"def get_resnet_generator(\n    filters=64,\n    num_downsampling_blocks=2,\n    num_residual_blocks=9,\n    num_upsample_blocks=2,\n    gamma_initializer=gamma_init,\n    name=None,\n):\n    # Input layer specifies the exact shape (256, 256, 3)\n    img_input = layers.Input(shape=input_img_size, name=name + \"_img_input\")\n\n    # Initial Convolution Block\n    \n    x = ReflectionPadding2D(padding=(3, 3))(img_input)\n    x = layers.Conv2D(filters, (7, 7), kernel_initializer=kernel_init, use_bias=False)(x)\n    x = keras.layers.GroupNormalization(groups=1, gamma_initializer=gamma_initializer)(x)\n    x = layers.Activation(\"relu\")(x)\n\n    # Downsampling to 64x64, then 32x32 for 2 blocks\n    for _ in range(num_downsampling_blocks):\n        filters *= 2\n        x = downsample(x, filters=filters, activation=layers.Activation(\"relu\"))\n\n    # Residual blocks maintains current spatial resolution, e.g., 32x32\n    for _ in range(num_residual_blocks):\n        x = residual_block(x, activation=layers.Activation(\"relu\"))\n\n    # Upsampling back to 64x64, then 128x128 for 2 blocks\n    for _ in range(num_upsample_blocks):\n        filters //= 2\n        x = upsample(x, filters, activation=layers.Activation(\"relu\"))\n\n    # Final Convolution block\n    x = ReflectionPadding2D(padding=(3, 3))(x)\n    x = layers.Conv2D(3, (7, 7), padding=\"valid\")(x)\n    #  output into the [-1, 1] range\n    x = layers.Activation(\"tanh\")(x)\n\n    model = keras.models.Model(img_input, x, name=name)\n    return model\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-16T23:43:48.832517Z","iopub.execute_input":"2025-11-16T23:43:48.832930Z","iopub.status.idle":"2025-11-16T23:43:48.838284Z","shell.execute_reply.started":"2025-11-16T23:43:48.832899Z","shell.execute_reply":"2025-11-16T23:43:48.837486Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Building Discriminator ","metadata":{}},{"cell_type":"markdown","source":"The discriminators implement the following architecture: C64->C128->C256->C512","metadata":{}},{"cell_type":"code","source":"# Discriminators in CycleGAN use bias and no group norm\n\ndef get_discriminator(\n    filters=64, kernel_initializer=kernel_init, num_downsampling=3, name=None\n):\n    img_input = layers.Input(shape=input_img_size, name=name + \"_img_input\")\n    x = layers.Conv2D(\n        filters,\n        (4, 4),\n        strides=(2, 2),\n        padding=\"same\",\n        kernel_initializer=kernel_initializer,\n    )(img_input)\n    x = layers.LeakyReLU(0.2)(x)\n\n    num_filters = filters\n    for num_downsample_block in range(3):\n        num_filters *= 2\n        if num_downsample_block < 2:\n            x = downsample(\n                x,\n                filters=num_filters,\n                activation=layers.LeakyReLU(0.2),\n                kernel_size=(4, 4),\n                strides=(2, 2),\n            )\n        else:\n            x = downsample(\n                x,\n                filters=num_filters,\n                activation=layers.LeakyReLU(0.2),\n                kernel_size=(4, 4),\n                strides=(1, 1),\n            )\n\n    # Final Convolution layer\n    x = layers.Conv2D(\n        1, (4, 4), strides=(1, 1), padding=\"same\", kernel_initializer=kernel_initializer)(x)\n\n    model = keras.models.Model(inputs=img_input, outputs=x, name=name)\n    return model\n\n\n# Get the generators\ngen_G = get_resnet_generator(name=\"generator_G\")\ngen_F = get_resnet_generator(name=\"generator_F\")\n\n# Get the discriminators\ndisc_X = get_discriminator(name=\"discriminator_X\")\ndisc_Y = get_discriminator(name=\"discriminator_Y\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-16T23:44:03.092065Z","iopub.execute_input":"2025-11-16T23:44:03.092308Z","iopub.status.idle":"2025-11-16T23:44:03.664509Z","shell.execute_reply.started":"2025-11-16T23:44:03.092291Z","shell.execute_reply":"2025-11-16T23:44:03.663482Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## CycleGAN Model Architecture","metadata":{}},{"cell_type":"markdown","source":"The CycleGAN architecture consists of two generators and two discriminators. The standard implementation typically uses a ResNet-based generator and a PatchGAN discriminator. \n\n**Key Features** : Instance Normalization, Reflection Padding, tanh Activation function\n\nThe typical layers and dimensions for a CycleGAN designed for **256x256x3** size images:\n\n**Input Layer** accepts RGB image of size (256 * 256 * 3)\n\n**First Convolution Layer** extracts initial features from image and implements reflection padding with 64 filters, instance normalization, and output layer eith ReLU activation function Size of the image will become ( 256 * 256 * 64)\n\n**Encoder (Downsampling)**  halves spatial size and doubles filters with ReLU activation function. It consists of 2 downsample blocks (convolution layers with stride (2, 2))\n\n**Transformer (Residual Network Block)** Processes deep features and preserves shape. This consists of 9 residual blocks.\n\n**Decoder (Upsampling)** Doubles spatial size, halves filters with ReLU. This layer consists of 2 upsample blocks and Conv2DTranspose with stride (2, 2)\n\n**Output Layer** Final convolution maps features back to 3 color channels and tanh activation function scales output to [-1, 1]. It consists of reflection padding layer, Conv2D layer and tanh activation function. Now the size of the translated monet style image will be (256 * 256 * 3)\n\n\n","metadata":{}},{"cell_type":"markdown","source":"## Build the CycleGAN model","metadata":{}},{"cell_type":"code","source":"class CycleGan(keras.Model):\n    def __init__(\n        self,\n        generator_G,\n        generator_F,\n        discriminator_X,\n        discriminator_Y,\n        lambda_cycle=10.0,#lambda_identity=5.0 # As hyperparameter tuning\n        lambda_identity=0.5,\n        #lambda_identity=0.25\n    ):\n        super().__init__()\n        self.gen_G = generator_G\n        self.gen_F = generator_F\n        self.disc_X = discriminator_X\n        self.disc_Y = discriminator_Y\n        self.lambda_cycle = lambda_cycle\n        self.lambda_identity = lambda_identity\n\n    def call(self, inputs):\n        return (\n            self.disc_X(inputs),\n            self.disc_Y(inputs),\n            self.gen_G(inputs),\n            self.gen_F(inputs),\n        )\n\n    def compile(\n        self,\n        gen_G_optimizer,\n        gen_F_optimizer,\n        disc_X_optimizer,\n        disc_Y_optimizer,\n        gen_loss_fn,\n        disc_loss_fn,\n    ):\n        super().compile()\n        self.gen_G_optimizer = gen_G_optimizer\n        self.gen_F_optimizer = gen_F_optimizer\n        self.disc_X_optimizer = disc_X_optimizer\n        self.disc_Y_optimizer = disc_Y_optimizer\n        self.generator_loss_fn = gen_loss_fn\n        self.discriminator_loss_fn = disc_loss_fn\n        self.cycle_loss_fn = keras.losses.MeanAbsoluteError()\n        self.identity_loss_fn = keras.losses.MeanAbsoluteError()\n\n    def train_step(self, batch_data):\n        # x is Horse and y is zebra\n        real_x, real_y = batch_data\n\n        # For CycleGAN, we need to calculate different\n        # kinds of losses for the generators and discriminators.\n        # We will perform the following steps here:\n        #\n        # 1. Pass real images through the generators and get the generated images\n        # 2. Pass the generated images back to the generators to check if we\n        #    can predict the original image from the generated image.\n        # 3. Do an identity mapping of the real images using the generators.\n        # 4. Pass the generated images in 1) to the corresponding discriminators.\n        # 5. Calculate the generators total loss (adversarial + cycle + identity)\n        # 6. Calculate the discriminators loss\n        # 7. Update the weights of the generators\n        # 8. Update the weights of the discriminators\n        # 9. Return the losses in a dictionary\n\n        with tf.GradientTape(persistent=True) as tape:\n            # Horse to fake zebra\n            fake_y = self.gen_G(real_x, training=True)\n            # Zebra to fake horse -> y2x\n            fake_x = self.gen_F(real_y, training=True)\n\n            # Cycle (Horse to fake zebra to fake horse): x -> y -> x\n            cycled_x = self.gen_F(fake_y, training=True)\n            # Cycle (Zebra to fake horse to fake zebra) y -> x -> y\n            cycled_y = self.gen_G(fake_x, training=True)\n\n            # Identity mapping\n            same_x = self.gen_F(real_x, training=True)\n            same_y = self.gen_G(real_y, training=True)\n\n            # Discriminator output\n            disc_real_x = self.disc_X(real_x, training=True)\n            disc_fake_x = self.disc_X(fake_x, training=True)\n\n            disc_real_y = self.disc_Y(real_y, training=True)\n            disc_fake_y = self.disc_Y(fake_y, training=True)\n\n            # Generator adversarial loss\n            gen_G_loss = self.generator_loss_fn(disc_fake_y)\n            gen_F_loss = self.generator_loss_fn(disc_fake_x)\n\n            # Generator cycle loss\n            cycle_loss_G = self.cycle_loss_fn(real_y, cycled_y) * self.lambda_cycle\n            cycle_loss_F = self.cycle_loss_fn(real_x, cycled_x) * self.lambda_cycle\n\n            # Generator identity loss\n            id_loss_G = (\n                self.identity_loss_fn(real_y, same_y)\n                * self.lambda_cycle\n                * self.lambda_identity\n            )\n            id_loss_F = (\n                self.identity_loss_fn(real_x, same_x)\n                * self.lambda_cycle\n                * self.lambda_identity\n            )\n\n            # Total generator loss\n            total_loss_G = gen_G_loss + cycle_loss_G + id_loss_G\n            total_loss_F = gen_F_loss + cycle_loss_F + id_loss_F\n\n            # Discriminator loss\n            disc_X_loss = self.discriminator_loss_fn(disc_real_x, disc_fake_x)\n            disc_Y_loss = self.discriminator_loss_fn(disc_real_y, disc_fake_y)\n\n        # Get the gradients for the generators\n        grads_G = tape.gradient(total_loss_G, self.gen_G.trainable_variables)\n        grads_F = tape.gradient(total_loss_F, self.gen_F.trainable_variables)\n\n        # Get the gradients for the discriminators\n        disc_X_grads = tape.gradient(disc_X_loss, self.disc_X.trainable_variables)\n        disc_Y_grads = tape.gradient(disc_Y_loss, self.disc_Y.trainable_variables)\n\n        # Update the weights of the generators\n        self.gen_G_optimizer.apply_gradients(\n            zip(grads_G, self.gen_G.trainable_variables)\n        )\n        self.gen_F_optimizer.apply_gradients(\n            zip(grads_F, self.gen_F.trainable_variables)\n        )\n\n        # Update the weights of the discriminators\n        self.disc_X_optimizer.apply_gradients(\n            zip(disc_X_grads, self.disc_X.trainable_variables)\n        )\n        self.disc_Y_optimizer.apply_gradients(\n            zip(disc_Y_grads, self.disc_Y.trainable_variables)\n        )\n\n        return {\n            \"G_loss\": total_loss_G,\n            \"F_loss\": total_loss_F,\n            \"D_X_loss\": disc_X_loss,\n            \"D_Y_loss\": disc_Y_loss,\n        }\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-16T23:44:27.241439Z","iopub.execute_input":"2025-11-16T23:44:27.241675Z","iopub.status.idle":"2025-11-16T23:44:27.250471Z","shell.execute_reply.started":"2025-11-16T23:44:27.241657Z","shell.execute_reply":"2025-11-16T23:44:27.249678Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Create a callback that periodically saves generated images","metadata":{}},{"cell_type":"code","source":"class GANMonitor(keras.callbacks.Callback):\n    \"\"\"A callback to generate and save images after each epoch\"\"\"\n\n    def __init__(self, num_img=4):\n        self.num_img = num_img\n\n    def on_epoch_end(self, epoch, logs=None):\n        _, ax = plt.subplots(4, 2, figsize=(12, 12))\n        for i, img in enumerate(test_photo.take(self.num_img)):\n            prediction = self.model.gen_G(img)[0].numpy()\n            prediction = (prediction * 127.5 + 127.5).astype(np.uint8)\n            img = (img[0] * 127.5 + 127.5).numpy().astype(np.uint8)\n\n            ax[i, 0].imshow(img)\n            ax[i, 1].imshow(prediction)\n            ax[i, 0].set_title(\"Input image\")\n            ax[i, 1].set_title(\"Translated image\")\n            ax[i, 0].axis(\"off\")\n            ax[i, 1].axis(\"off\")\n\n            prediction = keras.utils.array_to_img(prediction)\n            prediction.save(\n                \"generated_img_{i}_{epoch}.png\".format(i=i, epoch=epoch + 1)\n            )\n        plt.show()\n        plt.close()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-16T23:44:51.059771Z","iopub.execute_input":"2025-11-16T23:44:51.060030Z","iopub.status.idle":"2025-11-16T23:44:51.064803Z","shell.execute_reply.started":"2025-11-16T23:44:51.060012Z","shell.execute_reply":"2025-11-16T23:44:51.064075Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Train the end-to-end model","metadata":{}},{"cell_type":"markdown","source":"### Define Loss Functions","metadata":{}},{"cell_type":"markdown","source":"CycleGAN model's loss is the summation of\n\n* adversarial losses,\n* cycle consistency loss,\n* identity loss\n\n**Loss** = Generator G loss + Generator F loss + (lambda * cycle consistency loss) + (gamma * Identity loss)\n\nwhere G and F are generators, lambda and gamma are hyperparameters to control the relative importance of the losses.\n\n**Adversarial loss** : The adversarial loss is applied to both generators and discriminators to ensure that the generated images look realistic and belong to the target domain. Generators G and F produce images that are realistic that their discriminators DX and DY classify as 'real'. MSE is used to compute this loss. The discriminators are trained to distinguish between real images (from the dataset) and fake (generated) images. The goal is to maximize their ability to correctly classify images.The objective is to reduce this error. adv_loss_fn is used to evaluate adversarial loss.\n\n**Cycle consistency loss** is when an image is transferred from X to Y domain and back to X domain it should be nearly identical to the original input which is the main idea of unpaired image to image translation. It works in both directions, forward and backward. The loss is L1 or L2 distance between the original and translated image.\n\n**Identity Loss** It encourages the generator to preserve the color and content of an image if the input image is already in the target domain. For example, if a photo image is input into the photo-to-monet generator, the output should be the same photo image. It is calculated as the L1 distance between a real image from the target domain and the image produced by the generator for that domain","metadata":{}},{"cell_type":"code","source":"# Loss function for evaluating adversarial loss\nadv_loss_fn = keras.losses.MeanSquaredError()\n\n# Define the loss function for the generators\n\ndef generator_loss_fn(fake):\n    fake_loss = adv_loss_fn(ops.ones_like(fake), fake)\n    return fake_loss\n\n# Define the loss function for the discriminators\ndef discriminator_loss_fn(real, fake):\n    real_loss = adv_loss_fn(ops.ones_like(real), real)\n    fake_loss = adv_loss_fn(ops.zeros_like(fake), fake)\n    return (real_loss + fake_loss) * 0.5\n\n# Create cycle gan model\ncycle_gan_model = CycleGan(\n#cycle_gan_model1 = CycleGan(\n    generator_G=gen_G, generator_F=gen_F, discriminator_X=disc_X, discriminator_Y=disc_Y\n)\n\n# EXPLICITLY BUILD THE MODEL \n# Assuming  input images are 128x128 with 3 channels (RGB)\n# The input shape is just the image shape, Keras handles the batch dimension internally.\ncycle_gan_model.build(input_shape=(None, 256, 256, 3)) \n#cycle_gan_model1.build(input_shape=(None, 256, 256, 3)) \n\n\n# Compile the model\ncycle_gan_model.compile(\n#cycle_gan_model1.compile(\n    gen_G_optimizer=keras.optimizers.Adam(learning_rate=2e-4, beta_1=0.5),\n    gen_F_optimizer=keras.optimizers.Adam(learning_rate=2e-4, beta_1=0.5),\n    disc_X_optimizer=keras.optimizers.Adam(learning_rate=2e-4, beta_1=0.5),\n    disc_Y_optimizer=keras.optimizers.Adam(learning_rate=2e-4, beta_1=0.5),\n    gen_loss_fn=generator_loss_fn,\n    disc_loss_fn=discriminator_loss_fn,\n    #tf.keras.optimizers.Adam(2e-4, beta_1=0.5)\n)\n# Callbacks\nplotter = GANMonitor()\ncheckpoint_filepath = \"/kaggle/working/model_checkpoints/cyclegan_checkpoints.weights.h5\"\nmodel_checkpoint_callback = keras.callbacks.ModelCheckpoint(\n    filepath=checkpoint_filepath, save_weights_only=True\n)\n\n# Here we will train the model for just one epoch as each epoch takes around\n# 7 minutes on a single P100 backed machine.\ncycle_gan_model.fit(\n#cycle_gan_model1.fit(\n    tf.data.Dataset.zip((train_monet, train_photo)),\n    epochs=10,\n    callbacks=[plotter, model_checkpoint_callback],\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-16T23:45:28.176188Z","iopub.execute_input":"2025-11-16T23:45:28.176474Z","execution_failed":"2025-11-17T01:40:25.992Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Number of Epochs","metadata":{}},{"cell_type":"markdown","source":"Initially I tried with 150 epochs, it took 7 to 8 minutes for first couple of epochs and then froze, as there are 240 JPG images to train in each iteration. I tried 75 then 50 even for 20 it froze. So I chose 10 and sometimes 15 to be number of epochs. This will impact the performance heavily. But still I continue with my project. What values I get for performance metrics (low), registering here.","metadata":{}},{"cell_type":"markdown","source":"## Hyperparameter Tuning","metadata":{}},{"cell_type":"markdown","source":"Trying increasing **batch size to 4**, same lambda cycle = 10.0 and **lambda_identity=5.0** and ran the model again. Code been reused by commenting and adding code. Results are stored in model1. \n\nThe last epoch shows \n60/60 ━━━━━━━━━━━━━━━━━━━━ 54s 884ms/step - D_X_loss: 0.1573 - D_Y_loss: 0.0293 - F_loss: 9.0353 - G_loss: 11.5244\n\nAs compared with previous model the generator F and G losses were higher to this model. By viewing the images previous model perform better.","metadata":{}},{"cell_type":"markdown","source":"## Test the performance of the model","metadata":{}},{"cell_type":"markdown","source":"**CycleGAN Performance Metrics**\n\n**Frechet Inception Distance (FID)**\n\nSimilarity measure between the real and generated image distributions. Widely used in evaluation of GAN models. Lower the FID score better the performance.\n\n**Learned Perceptual Image Patch Similarity (LPIPS)**\n\nMeasure that captures perceptual similarity using deep features\n\n**Structural Similarity Index (SSIM)**\n\nCompares luminance, contrast and structure between real and generated image.\n\n**Peak Signal to Noise Ratio (PSNR)**\n\nHigher scores of PSNR means more realistic generated images.\n\n**Mean Absolute Error (MAE) & Mean Squared Error (MSE)**\n\nQuantitative differences in pixel level measure.\n\n**Fully Convolutional Network Score (FCNS)**\n\nMeasure of semantic label preserved generated images.\n","metadata":{}},{"cell_type":"code","source":"# Once the weights are loaded, we will take a few samples from the test data and check the model's performance.\n\n\n# Load the checkpoints\ncycle_gan_model.load_weights(checkpoint_filepath)\n#cycle_gan_model1.load_weights(checkpoint_filepath)\nprint(\"Weights loaded successfully\")\n\n_, ax = plt.subplots(4, 2, figsize=(10, 15))\nfor i, img in enumerate(test_photo.take(4)):\n    prediction = cycle_gan_model.gen_G(img, training=False)[0].numpy()\n    #prediction = cycle_gan_model1.gen_G(img, training=False)[0].numpy()\n    prediction = (prediction * 127.5 + 127.5).astype(np.uint8)\n    img = (img[0] * 127.5 + 127.5).numpy().astype(np.uint8)\n\n    ax[i, 0].imshow(img)\n    ax[i, 1].imshow(prediction)\n    ax[i, 0].set_title(\"Input image\")\n    ax[i, 0].set_title(\"Input image\")\n    ax[i, 1].set_title(\"Translated image\")\n    ax[i, 0].axis(\"off\")\n    ax[i, 1].axis(\"off\")\n\n    prediction = keras.utils.array_to_img(prediction)\n    prediction.save(\"predicted_img_{i}.png\".format(i=i))\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-15T21:04:34.530754Z","iopub.status.idle":"2025-11-15T21:04:34.531101Z","shell.execute_reply.started":"2025-11-15T21:04:34.530858Z","shell.execute_reply":"2025-11-15T21:04:34.530865Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Computaion of MiFID","metadata":{}},{"cell_type":"markdown","source":"The memorization distance is defined as the minimum cosine distance of all training samples in the feature space, averaged across all user generated image samples. This distance is thresholded, and it's assigned to 1.0 if the distance exceeds a pre-defined epsilon.","metadata":{}},{"cell_type":"markdown","source":"For calculating MiFID using torch-fidelity need to install the library and its dependencies (torch, torch-fidelity). I tried but it throws ValueError in InceptionV3 code. So Im trying FID score.","metadata":{}},{"cell_type":"markdown","source":"### Save 300 Sample Test and Translated Photo Images","metadata":{}},{"cell_type":"code","source":"import os\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport keras.utils\n\n# Ensure the target directory exists\ninput_dir  = '/kaggle/working/test_gen_images/input_test_photo'\nif not os.path.exists(input_dir):\n    os.makedirs(input_dir)\n    print(f\"Created directory: {input_dir}\")\n    \noutput_dir = '/kaggle/working/test_gen_images/predicted'\n#output_dir = '/kaggle/working/images'      #test_gen_images/predicted'\nif not os.path.exists(output_dir):\n    os.makedirs(output_dir)\n    print(f\"Created directory: {output_dir}\")\n\n# Load the checkpoints (assuming cycle_gan_model and checkpoint_filepath are defined earlier)\ncycle_gan_model.load_weights(checkpoint_filepath)\nprint(\"Weights loaded successfully (placeholder)\") # Replace with your actual loading logic\n\n_, ax = plt.subplots(300, 2, figsize=(10, 15))\n# Iterate through the test photos and corresponding axes\nfor i, axes_row in enumerate(ax):\n    # Get one image batch (which contains a single image because of batch size 1)\n    img_tensor = next(iter(test_photo.take(1))) # Use next(iter(...)) to get a single batch\n\n    # Generate the prediction (model output will also have a batch dimension)\n    prediction_tensor = cycle_gan_model.gen_G(img_tensor, training=False).numpy()\n    #prediction_tensor = cycle_gan_model1.gen_G(img_tensor, training=False).numpy()\n\n    # Convert tensors back to uint8 range (0-255) and REMOVE THE BATCH DIMENSION\n    prediction_img_data = (prediction_tensor[0] * 127.5 + 127.5).astype(np.uint8)\n    input_img_data = (img_tensor[0] * 127.5 + 127.5).numpy().astype(np.uint8)\n\n    # --- Saving Images ---\n    # Convert numpy arrays to PIL images for saving\n    prediction_pil_img = keras.utils.array_to_img(prediction_img_data)\n    input_pil_img = keras.utils.array_to_img(input_img_data)\n\n    # Define the save paths\n    input_save_path = os.path.join(input_dir, f\"input_img_{i:02d}.png\")\n    prediction_save_path = os.path.join(output_dir, f\"predicted_img_{i:02d}.png\")\n\n    # Save the images\n    input_pil_img.save(input_save_path)\n    prediction_pil_img.save(prediction_save_path)\n    \n\nprint(\"Images Saved to /kaggle/working/images/\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-15T21:04:35.039608Z","iopub.execute_input":"2025-11-15T21:04:35.039957Z","iopub.status.idle":"2025-11-15T21:08:21.540400Z","shell.execute_reply.started":"2025-11-15T21:04:35.039935Z","shell.execute_reply":"2025-11-15T21:08:21.539102Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"['/kaggle/working/test_gen_images/input_test_photo',\n '/kaggle/working/test_gen_images/predicted']","metadata":{}},{"cell_type":"code","source":"#work_directory_test = '/kaggle/working/test_gen_images/predicted/' input_test_photo\nwork_directory_test = '/kaggle/working/test_gen_images/input_test_photo/'\n    \n# list of image file paths\nimage_files = [os.path.join(work_directory_test, f) \n                    for f in os.listdir(work_directory_test)] #if f.endswith(('.jpg', '.png', '.jpeg', '.tif'))]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-14T23:00:11.164236Z","iopub.execute_input":"2025-11-14T23:00:11.164916Z","iopub.status.idle":"2025-11-14T23:00:11.169406Z","shell.execute_reply.started":"2025-11-14T23:00:11.164893Z","shell.execute_reply":"2025-11-14T23:00:11.168600Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"len(image_files)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-14T23:00:13.560170Z","iopub.execute_input":"2025-11-14T23:00:13.560886Z","iopub.status.idle":"2025-11-14T23:00:13.565437Z","shell.execute_reply.started":"2025-11-14T23:00:13.560863Z","shell.execute_reply":"2025-11-14T23:00:13.564841Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Define image dimensions and batch size\nIMAGE_SIZE = (299, 299)\nBATCH_SIZE = 32\n\nreal_images_dir = '/kaggle/working/test_gen_images/input_test_photo'\ngenerated_images_dir = '/kaggle/working/test_gen_images/predicted'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-14T23:00:16.628422Z","iopub.execute_input":"2025-11-14T23:00:16.629108Z","iopub.status.idle":"2025-11-14T23:00:16.632649Z","shell.execute_reply.started":"2025-11-14T23:00:16.629085Z","shell.execute_reply":"2025-11-14T23:00:16.631908Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Helper Function for Creating Image Datasets","metadata":{}},{"cell_type":"code","source":"def preprocess_image(image_path):\n    \"\"\"Loads, decodes, and resizes an image.\"\"\"\n    image_bytes = tf.io.read_file(image_path)\n    image = tf.image.decode_png(image_bytes, channels=3)  # Use decode_jpeg or decode_png\n    image = tf.image.resize(image, IMAGE_SIZE)\n    image = tf.cast(image, tf.uint8)  # MiFID expects uint8 with values 0-255\n    return image","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-14T23:00:24.924487Z","iopub.execute_input":"2025-11-14T23:00:24.924784Z","iopub.status.idle":"2025-11-14T23:00:24.929106Z","shell.execute_reply.started":"2025-11-14T23:00:24.924767Z","shell.execute_reply":"2025-11-14T23:00:24.928406Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def create_image_dataset(directory_path):\n    # Creates a TensorFlow Dataset from a directory of images.\n    \n    file_paths = [os.path.join(directory_path, fname) for fname in os.listdir(directory_path) if fname.endswith('.png')]\n    dataset = tf.data.Dataset.from_tensor_slices(file_paths)\n    dataset = dataset.shuffle(buffer_size=len(file_paths))\n    dataset = dataset.map(preprocess_image, num_parallel_calls=tf.data.AUTOTUNE)\n    dataset = dataset.batch(BATCH_SIZE)\n    return dataset\n\n# Create datasets for both real and generated images\nreal_dataset = create_image_dataset(real_images_dir)\ngenerated_dataset = create_image_dataset(generated_images_dir)\n\nprint(f\"Real dataset created with {len(os.listdir(real_images_dir))} images.\")\nprint(f\"Generated dataset created with {len(os.listdir(generated_images_dir))} images.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-14T23:00:26.897305Z","iopub.execute_input":"2025-11-14T23:00:26.897595Z","iopub.status.idle":"2025-11-14T23:00:26.990914Z","shell.execute_reply.started":"2025-11-14T23:00:26.897577Z","shell.execute_reply":"2025-11-14T23:00:26.990244Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from tensorflow.keras.applications.inception_v3 import InceptionV3\n# load inception v3 model\nmodelf = InceptionV3()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-14T23:01:27.609073Z","iopub.execute_input":"2025-11-14T23:01:27.609786Z","iopub.status.idle":"2025-11-14T23:01:30.176375Z","shell.execute_reply.started":"2025-11-14T23:01:27.609753Z","shell.execute_reply":"2025-11-14T23:01:30.175573Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# prepare the inception v3 model\nmodel = InceptionV3(include_top=False, pooling='avg', input_shape=(299,299,3))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-14T23:01:33.278433Z","iopub.execute_input":"2025-11-14T23:01:33.279024Z","iopub.status.idle":"2025-11-14T23:01:35.499960Z","shell.execute_reply.started":"2025-11-14T23:01:33.278999Z","shell.execute_reply":"2025-11-14T23:01:35.499351Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def scale_images(images, new_shape):\n\timages_list = list()\n\tfor image in images:\n\t\t# resize with nearest neighbor interpolation\n\t\tnew_image = resize(image, new_shape, 0)\n\t\t# store\n\t\timages_list.append(new_image)\n\treturn asarray(images_list)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T19:32:04.312962Z","iopub.execute_input":"2025-11-13T19:32:04.313746Z","iopub.status.idle":"2025-11-13T19:32:04.317772Z","shell.execute_reply.started":"2025-11-13T19:32:04.313720Z","shell.execute_reply":"2025-11-13T19:32:04.317011Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Prepare the Datasets","metadata":{}},{"cell_type":"code","source":"import numpy as np\nfrom PIL import Image\nimport os\nimport glob\n\ndef load_images_to_numpy(directory_path, target_size=(299, 299)):\n    \"\"\"Loads images from a directory into a NumPy array and resizes them.\"\"\"\n    images = []\n    filepaths = glob.glob(os.path.join(directory_path, '*.png'))\n    # You might need to check for .jpg too\n    if not filepaths:\n         filepaths = glob.glob(os.path.join(directory_path, '*.jpg'))\n\n    for path in filepaths:\n        try:\n            img = Image.open(path).convert('RGB')\n            img = img.resize(target_size, Image.Resampling.BICUBIC)\n            img_array = np.asarray(img)\n            # Normalize to the range expected by standard Inception models (-1 to 1 or 0 to 1)\n            # Standard Keras InceptionV3 expects values from 0-255 initially\n            images.append(img_array)\n        except Exception as e:\n            print(f\"Error loading image {path}: {e}\")\n            continue\n\n    return np.array(images)\n\n# Load data\nreal_images_np = load_images_to_numpy('/kaggle/working/test_gen_images/input_test_photo')\ngenerated_images_np = load_images_to_numpy('/kaggle/working/test_gen_images/predicted')\n\nprint(f\"Loaded real images shape: {real_images_np.shape}\")\nprint(f\"Loaded generated images shape: {generated_images_np.shape}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-14T23:01:48.341175Z","iopub.execute_input":"2025-11-14T23:01:48.341941Z","iopub.status.idle":"2025-11-14T23:01:50.975227Z","shell.execute_reply.started":"2025-11-14T23:01:48.341917Z","shell.execute_reply":"2025-11-14T23:01:50.974572Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Compute FID Score","metadata":{}},{"cell_type":"markdown","source":"FID (Frechet Inception Distance) is a measures the statistical distance between the feature distributions of real images and generated images. A lower score is better.","metadata":{}},{"cell_type":"code","source":"import numpy\nfrom numpy import cov\nfrom numpy import trace\nfrom numpy import iscomplexobj\nfrom scipy.linalg import sqrtm\nfrom tensorflow.keras.applications.inception_v3 import InceptionV3\n# You might need to import preprocess_input if your function uses normalization internally\n\n# Function definition you provided\ndef calculate_fid(model, images1, images2):\n    # images1 and images2 MUST be NumPy arrays here\n    # calculate activations\n    act1 = model.predict(images1)\n    act2 = model.predict(images2)\n    # calculate mean and covariance statistics\n    mu1, sigma1 = act1.mean(axis=0), cov(act1, rowvar=False)\n    mu2, sigma2 = act2.mean(axis=0), cov(act2, rowvar=False)\n    # calculate sum squared difference between means\n    ssdiff = numpy.sum((mu1 - mu2)**2.0)\n    # calculate sqrt of product between cov\n    covmean = sqrtm(sigma1.dot(sigma2))\n    # check and correct imaginary numbers from sqrt\n    if iscomplexobj(covmean):\n        covmean = covmean.real\n    # calculate score\n    fid = ssdiff + trace(sigma1 + sigma2 - 2.0 * covmean)\n    return fid\n\n# Load the pre-trained InceptionV3 model (without the top layer)\nmodel = InceptionV3( pooling='avg', \n                    include_top=False, \n                    weights='imagenet', \n                    input_shape=(299, 299, 3))\n\n# Calculate the FID score using the NumPy data\nfid = calculate_fid(model, real_images_np, generated_images_np)\nprint(\"FID:\", fid)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-14T23:01:57.477785Z","iopub.execute_input":"2025-11-14T23:01:57.478393Z","iopub.status.idle":"2025-11-14T23:02:25.329825Z","shell.execute_reply.started":"2025-11-14T23:01:57.478368Z","shell.execute_reply":"2025-11-14T23:02:25.328994Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"First model :\n\nA FID Score of 53896 suggests there is almost no statistical overlap between the feature space of your real images and your generated images. The images you generated are likely noisy.\n\nSecond model :\n\nA FID Score of 115945.26 suggests there is almost no statistical overlap between the feature space of your real images and your generated images. The images you generated are likely noisy and even higher than first model.","metadata":{}},{"cell_type":"markdown","source":"## Results and Analysis","metadata":{}},{"cell_type":"markdown","source":"Model with hyperparameters \n\nNumber of Epochs = 10 , **BATCH_SIZE = 1**, \noptimizers = Adam, Learning Rate = 0.0002, \nbeta_1=0.5 (moving average of the gradients),\nlambda_cycle=10.0: (weight applied to the cycle-consistency loss),\n**lambda_identity=0.5** (weight applied to the identity loss)\nThe FID score is 53896 which is very high suggesting the totally different distributions of real and generated images (as expected because of number of epochs and ssample size). \n\n240/240 ━━━━━━━━━━━━━━━━━━━━ 69s 287ms/step - D_X_loss: 0.0980 - D_Y_loss: 0.1253 - F_loss: 3.4117 - G_loss: 3.6359\n\nModel with hyperparameters \n\nNumber of Epochs = 10 , **BATCH_SIZE = 4**, \noptimizers = Adam, Learning Rate = 0.0002, \nbeta_1=0.5 (moving average of the gradients),\nlambda_cycle=10.0: (weight applied to the cycle-consistency loss),\n**lambda_identity=5.0** (weight applied to the identity loss)\n\nTrying increasing batch size to 4, same lambda cycle = 10.0 and lambda_identity=5.0 and ran the model again. Code been reused by commenting and adding code. Results are stored in model1.\n\nThe last epoch shows 60/60 ━━━━━━━━━━━━━━━━━━━━ 54s 884ms/step - D_X_loss: 0.1573 - D_Y_loss: 0.0293 - F_loss: 9.0353 - G_loss: 11.5244\n\nAs compared with previous model the generator F and generator G losses were higher to this model. Discriminator losses D_X and D_Y are also high compared to the previous model. By viewing the images previous model perform better.\n\nThe FID score is FID: 115945.26 which is very high suggesting the totally different distributions of real and generated images (as expected because of lower number of epochs and sample size). Besides on viewing the model just transform that into a black and white picture like a grayscale image.\n\nObviously first model with low value of lambda_identity perform better than higher value of lambda_identity.","metadata":{}},{"cell_type":"markdown","source":"## Conclusion","metadata":{}},{"cell_type":"markdown","source":"CycleGAN models perform unpaired image to image translation that enables transformations from one domain to other without aligned datasets.These models preserve semantic content and stabilize training thereby producing realistic generated images. Higher performing models prove promising in medical imaging, Photo restoration and enhancement.\n\nFuture Study: Hybrid models such as cycleGAN combined with variational autoencoders (VAE) can be build, extending to wide range of domains like text, audio and multimodal inputs can be constructed.","metadata":{}},{"cell_type":"markdown","source":"## Reference","metadata":{}},{"cell_type":"markdown","source":"**Layers Implementation** : https://keras.io/examples/generative/cyclegan/\n\n**Comutation of FID Score** : https://machinelearningmastery.com/how-to-implement-the-frechet-inception-distance-fid-from-scratch/\n","metadata":{}},{"cell_type":"markdown","source":"## Thank You!","metadata":{}}]}