{"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":[{"sourceId":21755,"databundleVersionId":1475600,"sourceType":"competition"},{"sourceId":251162420,"sourceType":"kernelVersion"}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Introduction\n\nThis notebook implements Generative Adversial Networks (GANs) to generate images that do not original exist.   GANs work by employing two models, a Generator that creates images, and a Discriminator that marks an image as real or fake. \n\nThis notebook trains a CycleGAN to transform real photos into Monet-style paintings","metadata":{}},{"cell_type":"markdown","source":"# Setup\nImport tensorflow and set accelerator to GPU to reduce the run time. ","metadata":{}},{"cell_type":"code","source":"!pip install imageio\n!pip install git+https://github.com/tensorflow/docs","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-20T03:51:10.614443Z","iopub.execute_input":"2025-07-20T03:51:10.614718Z","iopub.status.idle":"2025-07-20T03:51:23.364867Z","shell.execute_reply.started":"2025-07-20T03:51:10.614692Z","shell.execute_reply":"2025-07-20T03:51:23.364030Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 📦 Step 1: Setup\n# ---------------\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\nimport pandas as pd\nimport numpy as np\nimport os, re\nimport glob\nimport imageio\nimport PIL\nfrom tensorflow.keras import layers\nimport time\nimport seaborn as sns\nfrom IPython import display\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-20T03:51:23.366981Z","iopub.execute_input":"2025-07-20T03:51:23.367211Z","iopub.status.idle":"2025-07-20T03:51:37.253761Z","shell.execute_reply.started":"2025-07-20T03:51:23.367185Z","shell.execute_reply":"2025-07-20T03:51:37.253129Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"MONET_TFREC_PATH = '/kaggle/input/gan-getting-started/monet_tfrec/*.tfrec'\nmonet_tfrec_files = glob.glob(MONET_TFREC_PATH)\n\nprint(f\"Number of TFRecord files: {len(monet_tfrec_files)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-20T03:51:37.254399Z","iopub.execute_input":"2025-07-20T03:51:37.254887Z","iopub.status.idle":"2025-07-20T03:51:37.269822Z","shell.execute_reply.started":"2025-07-20T03:51:37.254868Z","shell.execute_reply":"2025-07-20T03:51:37.269117Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Load and prepare the dataset\n\nYou will use the Monet dataset to train the generator and the discriminator. It will load the images in .tfrecord format which is a condensed version of images and specifically designed for Tensorflow. The generator will generate real photos that resemble Monet paintings. ","metadata":{}},{"cell_type":"code","source":"IMAGE_SIZE = [256, 256]\nBATCH_SIZE = 1 \nAUTOTUNE = tf.data.AUTOTUNE\n\nMONET_TFREC_PATH = '/kaggle/input/gan-getting-started/monet_tfrec/*.tfrec'\nPHOTO_TFREC_PATH = '/kaggle/input/gan-getting-started/photo_tfrec/*.tfrec'\n\nmonet_files = tf.data.Dataset.list_files(MONET_TFREC_PATH)\nphoto_files = tf.data.Dataset.list_files(PHOTO_TFREC_PATH)\n\nprint(f\"Number of Monet TFRecord files: {len(monet_tfrec_files)}\")\nprint(f\"Number of Real Photo TFRecord files: {len(photo_files)}\")","metadata":{"_uuid":"41e7dab4-e19e-40e6-90cd-e47754c5b5f8","_cell_guid":"4cbabec0-1ec3-4925-b865-77671e3b3b9b","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-07-20T03:51:37.270592Z","iopub.execute_input":"2025-07-20T03:51:37.270827Z","iopub.status.idle":"2025-07-20T03:51:39.140881Z","shell.execute_reply.started":"2025-07-20T03:51:37.270809Z","shell.execute_reply":"2025-07-20T03:51:39.139866Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 🖼️ Step 3: Create Datasets and shuffle them\n# ----------------------------------------\ndef decode_image(example_proto):\n    feature_description = {\n        'image': tf.io.FixedLenFeature([], tf.string),\n    }\n    example = tf.io.parse_single_example(example_proto, feature_description)\n    image = tf.image.decode_jpeg(example['image'], channels=3)\n    image = tf.image.resize(image, IMAGE_SIZE)\n    image = (tf.cast(image, tf.float32) / 127.5) - 1  # Normalize to [-1, 1]\n    return image\n\n\ndef load_dataset(filenames):\n    return tf.data.TFRecordDataset(filenames, num_parallel_reads=tf.data.AUTOTUNE)\\\n             .map(decode_image, num_parallel_calls=tf.data.AUTOTUNE)\n\nmonet_ds = load_dataset(monet_files)\nphoto_ds = load_dataset(photo_files)\n\nmonet_ds = monet_ds.shuffle(300).batch(BATCH_SIZE).prefetch(AUTOTUNE)\nphoto_ds = photo_ds.shuffle(7028).batch(BATCH_SIZE).prefetch(AUTOTUNE)\n\n\n\n\n\n\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-20T03:51:39.141786Z","iopub.execute_input":"2025-07-20T03:51:39.142181Z","iopub.status.idle":"2025-07-20T03:51:39.266050Z","shell.execute_reply.started":"2025-07-20T03:51:39.142146Z","shell.execute_reply":"2025-07-20T03:51:39.265300Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Exploratory Data Analysis\nI will inspect some of the images to see how they compare and see if there are visually apparent features for the GAN Model to learn from. ","metadata":{}},{"cell_type":"code","source":"def show_samples(dataset, title, num_images=5):\n    \"\"\"\n    Display a 2-row grid of sample images and return the list of sampled images.\n    \"\"\"\n    sampled_images = []\n    plt.figure(figsize=(num_images * 2, 8))\n    images_shown = 0\n\n    for batch in dataset:\n        for image in batch:\n            sampled_images.append(image)\n            plt.subplot(2, num_images, images_shown + 1)\n            plt.imshow((image + 1) / 2.0)\n            plt.axis('off')\n            images_shown += 1\n            if images_shown == 2 * num_images:\n                break\n        if images_shown == 2 * num_images:\n            break\n\n    plt.suptitle(title, fontsize=16)\n    plt.tight_layout()\n    plt.subplots_adjust(top=0.90)\n    plt.show()\n    return sampled_images","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-20T03:51:39.268350Z","iopub.execute_input":"2025-07-20T03:51:39.268886Z","iopub.status.idle":"2025-07-20T03:51:39.275830Z","shell.execute_reply.started":"2025-07-20T03:51:39.268859Z","shell.execute_reply":"2025-07-20T03:51:39.275147Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"monet_samples = show_samples(monet_ds, \"Monet Sample Images\")\nphoto_samples = show_samples(photo_ds, \"Photo Sample Images\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-20T03:51:39.276589Z","iopub.execute_input":"2025-07-20T03:51:39.276852Z","iopub.status.idle":"2025-07-20T03:51:46.669623Z","shell.execute_reply.started":"2025-07-20T03:51:39.276830Z","shell.execute_reply":"2025-07-20T03:51:46.668850Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Next I will inspect the color pixel density and distribution to see if there are differences in between the monet images and the photos.","metadata":{}},{"cell_type":"code","source":"def get_rgb_from_images(image_list):\n    r_vals, g_vals, b_vals = [], [], []\n    for image in image_list:\n        image = ((image + 1) / 2.0).numpy()\n        r_vals.extend(image[:, :, 0].flatten())\n        g_vals.extend(image[:, :, 1].flatten())\n        b_vals.extend(image[:, :, 2].flatten())\n    return np.array(r_vals), np.array(g_vals), np.array(b_vals)\n\ndef plot_rgb_histograms(r1, g1, b1, r2, g2, b2, label1='Dataset 1', label2='Dataset 2'):\n    \"\"\"Plot RGB histograms for two datasets with clear legends.\"\"\"\n    colors = ['red', 'green', 'blue']\n    datasets = [(r1, g1, b1, label1, 0.6), (r2, g2, b2, label2, 0.35)]\n\n    plt.figure(figsize=(15, 4))\n    for i, color in enumerate(colors):\n        plt.subplot(1, 3, i + 1)\n        for r, g, b, label, alpha in datasets:\n            channel = [r, g, b][i]\n            plt.hist(channel, bins=50, alpha=alpha, label=label,\n                     color=color, density=True, histtype='stepfilled',\n                     edgecolor='black', linewidth=0.5)\n        plt.title(f'{color.upper()} channel')\n        plt.xlabel('Pixel Intensity')\n        plt.ylabel('Density')\n        plt.legend(facecolor='white', edgecolor='black')\n    plt.tight_layout()\n    plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-20T03:51:46.670432Z","iopub.execute_input":"2025-07-20T03:51:46.670688Z","iopub.status.idle":"2025-07-20T03:51:46.678138Z","shell.execute_reply.started":"2025-07-20T03:51:46.670668Z","shell.execute_reply":"2025-07-20T03:51:46.677356Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"r_monet, g_monet, b_monet = get_rgb_from_images(monet_samples)\nr_photo, g_photo, b_photo = get_rgb_from_images(photo_samples)\n\nplot_rgb_histograms(r_monet, g_monet, b_monet, r_photo, g_photo, b_photo, \n                    label1='Monet', label2='Photo')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-20T03:51:46.678798Z","iopub.execute_input":"2025-07-20T03:51:46.678961Z","iopub.status.idle":"2025-07-20T03:51:47.838975Z","shell.execute_reply.started":"2025-07-20T03:51:46.678947Z","shell.execute_reply":"2025-07-20T03:51:47.838309Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import warnings\nwarnings.filterwarnings(\"ignore\", category=FutureWarning)\n\ndef plot_rgb_kde(r1, g1, b1, r2, g2, b2, label1='Monet', label2='Photo'):\n    \"\"\"Plot KDEs of RGB pixel values for two datasets.\"\"\"\n    # Create DataFrame for seaborn\n    def build_df(r, g, b, label):\n        return pd.DataFrame({\n            'Red': r,\n            'Green': g,\n            'Blue': b,\n            'Label': label\n        })\n\n    df1 = build_df(r1, g1, b1, label1)\n    df2 = build_df(r2, g2, b2, label2)\n    df = pd.concat([df1, df2])\n    # Plot\n    plt.figure(figsize=(15, 4))\n    for i, channel in enumerate(['Red', 'Green', 'Blue']):\n        plt.subplot(1, 3, i + 1)\n        sns.kdeplot(data=df, x=channel, hue='Label', fill=True, common_norm=False, alpha=0.5)\n        plt.title(f'{channel} Channel KDE')\n        plt.xlabel('Pixel Intensity')\n        plt.ylabel('Density')\n\n    plt.tight_layout()\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-20T03:51:47.839736Z","iopub.execute_input":"2025-07-20T03:51:47.839941Z","iopub.status.idle":"2025-07-20T03:51:47.846163Z","shell.execute_reply.started":"2025-07-20T03:51:47.839925Z","shell.execute_reply":"2025-07-20T03:51:47.845512Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plot_rgb_kde(r_monet, g_monet, b_monet, r_photo, g_photo, b_photo)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-20T03:51:47.846919Z","iopub.execute_input":"2025-07-20T03:51:47.847106Z","iopub.status.idle":"2025-07-20T03:52:04.519249Z","shell.execute_reply.started":"2025-07-20T03:51:47.847091Z","shell.execute_reply":"2025-07-20T03:52:04.518533Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def show_edges_from_list(image_list, title, num_images=5, image_size=256):\n    \"\"\"\n    Visualize Sobel edge magnitudes for a given list of image tensors.\n\n    Args:\n        image_list (list): List of image tensors scaled to [-1, 1].\n        title (str): Title for the entire plot.\n        num_images (int): Number of images to show.\n        image_size (int): Size to resize images to for edge detection.\n    \"\"\"\n    plt.figure(figsize=(num_images * 3, 3))\n\n    for i, img in enumerate(image_list[:num_images]):\n        img = (img + 1) / 2.0  # Rescale to [0, 1]\n        img_gray = tf.image.rgb_to_grayscale(img)\n        img_gray = tf.image.resize(img_gray, [image_size, image_size])\n\n        # Apply Sobel edge detection\n        sobel = tf.image.sobel_edges(tf.expand_dims(img_gray, axis=0))  # shape: (1, H, W, 1, 2)\n        edges = sobel[0]  # Remove batch dimension\n        grad_magnitude = tf.sqrt(tf.reduce_sum(tf.square(edges), axis=-1))[..., 0]  # shape: (H, W)\n\n        # Plot result\n        plt.subplot(1, num_images, i + 1)\n        plt.imshow(grad_magnitude.numpy(), cmap='gray')\n        plt.axis('off')\n\n    plt.suptitle(title, fontsize=16)\n    plt.tight_layout()\n    plt.subplots_adjust(top=0.85)\n    plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-20T03:52:04.520114Z","iopub.execute_input":"2025-07-20T03:52:04.520524Z","iopub.status.idle":"2025-07-20T03:52:04.526727Z","shell.execute_reply.started":"2025-07-20T03:52:04.520479Z","shell.execute_reply":"2025-07-20T03:52:04.525974Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"show_edges_from_list(photo_samples, \"Photo Edges\")\nshow_edges_from_list(monet_samples, \"Monet Edges\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-20T03:52:04.527417Z","iopub.execute_input":"2025-07-20T03:52:04.527657Z","iopub.status.idle":"2025-07-20T03:52:05.869232Z","shell.execute_reply.started":"2025-07-20T03:52:04.527633Z","shell.execute_reply":"2025-07-20T03:52:05.868550Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"While there is some variation between the monet images, and the photos, it seems to change as resampling occurs.  However there does seem to a general intensity in the colors for monet vs photos.   Examing the edges of the pictures, there are distinct outlines that I will try to have the models capture. ","metadata":{}},{"cell_type":"markdown","source":"# Model Architecture\n\n## Original Generator\nInitially the generator used tf.keras.layers.Conv2DTranspose (upsampling) layers to produce an image from seed.  It started  with a Dense layer that takes this seed as input, and unsample a few times until it reaches the desired image size.   I will use LeakyReLu activation for each layer, except for the final output layer which will use tanh. However it resulted in 7 hour compile time for 35 epochs.  I reduced the number of epoch to make the 6 hour cutoff, but the MiFID score was high (295)\n\n## Updated Generator via pix2pix\nThis creates a U-Net style model — an encoder-decoder network with skip connections — which is highly effective for image-to-image translation tasks like turning photos into Monet-style paintings.  It does run into issues with the GPU usage, but after reducing the number of saved images for tracking, it was able to complete 50 epochs in approximately 1 hour.  \n\nHow pix2pix.unet_generator works:\n\n* Encoder (Downsampling): Repeated Conv2D layers with stride 2 reduce the image size while increasing the feature depth, capturing context.\n* Decoder (Upsampling): Conv2DTranspose layers increase spatial resolution and reduce depth, gradually reconstructing the image.\n* Skip connections: At each level, activations from the encoder are concatenated with those in the decoder to help preserve spatial detail (as in U-Net).\n* The final activation is tanh, producing pixel values between -1 and 1.\n\nI sete norm_type='instancenorm' to look at 1 image at a time, which is more resource intensive but results in a better model. \n\n","metadata":{}},{"cell_type":"code","source":"!pip install git+https://github.com/tensorflow/examples.git\nfrom tensorflow_examples.models.pix2pix import pix2pix\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-20T04:18:54.958900Z","iopub.execute_input":"2025-07-20T04:18:54.959200Z","iopub.status.idle":"2025-07-20T04:19:01.665249Z","shell.execute_reply.started":"2025-07-20T04:18:54.959174Z","shell.execute_reply":"2025-07-20T04:19:01.664210Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Define the loss and optimizers\nDefine loss functions and optimizers for both models.\n\nSince we will classify the images as generated or real , we use BinaryCrossentropy as loss function. \n### Discriminator Loss:\nEncourages the discriminator to output 1 for real images and 0 for generated images.\nWe average the loss from real and fake predictions to balance the update scale.\n\n### Generator Adversarial Loss:\nEncourages the generator to fool the discriminator into thinking the generated image is real.\n\n### Cycle Consistency Loss:\nEnsures that if we translate a photo to Monet and back again, we recover the original photo.\nPrevents the generator from just producing arbitrary outputs.\n\n### Identity Loss:\nEncourages the generator to preserve the image if it's already in the target domain.\nHelps with color preservation and style consistency.\n\n","metadata":{}},{"cell_type":"code","source":"# Binary cross entropy loss for GAN\nloss_obj = tf.keras.losses.BinaryCrossentropy(from_logits=True)\n\n# Discriminator loss: real images should be 1, fake should be 0\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\n# Generator adversarial loss: want to fool discriminator\ndef generator_loss(generated):\n    return loss_obj(tf.ones_like(generated), generated)\n\n# L1 cycle-consistency loss (real → fake → real)\nLAMBDA = 10\n\ndef calc_cycle_loss(real_image, cycled_image):\n    return tf.reduce_mean(tf.abs(real_image - cycled_image)) * LAMBDA\n\n# Identity loss (real Monet → Monet), to preserve color/style\ndef identity_loss(real_image, same_image):\n    return tf.reduce_mean(tf.abs(real_image - same_image)) * LAMBDA * 0.5","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-20T04:19:01.667531Z","iopub.execute_input":"2025-07-20T04:19:01.668234Z","iopub.status.idle":"2025-07-20T04:19:01.674247Z","shell.execute_reply.started":"2025-07-20T04:19:01.668204Z","shell.execute_reply":"2025-07-20T04:19:01.673418Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CycleGAN(tf.keras.Model):\n    def __init__(self, generator_G, generator_F, discriminator_X, discriminator_Y):\n        super(CycleGAN, self).__init__()\n        self.gen_G = generator_G  # Photo → Monet\n        self.gen_F = generator_F  # Monet → Photo\n        self.disc_X = discriminator_X  # Monet discriminator\n        self.disc_Y = discriminator_Y  # Photo discriminator\n\n    def compile(self, gen_G_optimizer, gen_F_optimizer, disc_X_optimizer, disc_Y_optimizer,\n                gen_loss_fn, disc_loss_fn, cycle_loss_fn, identity_loss_fn):\n        super(CycleGAN, self).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.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    def train_step(self, batch_data):\n        real_x, real_y = batch_data  # x = photo, y = monet\n\n        with tf.GradientTape(persistent=True) as tape:\n            # Generate fakes\n            fake_y = self.gen_G(real_x, training=True)  # photo → fake Monet\n            fake_x = self.gen_F(real_y, training=True)  # Monet → fake photo\n\n            # Cycle back\n            cycled_x = self.gen_F(fake_y, training=True)\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 predictions\n            disc_real_x = self.disc_X(real_y, training=True)\n            disc_real_y = self.disc_Y(real_x, training=True)\n\n            disc_fake_x = self.disc_X(fake_y, training=True)\n            disc_fake_y = self.disc_Y(fake_x, training=True)\n\n            # Losses\n            gen_G_loss = self.gen_loss_fn(disc_fake_x)\n            gen_F_loss = self.gen_loss_fn(disc_fake_y)\n\n            total_cycle_loss = self.cycle_loss_fn(real_x, cycled_x) + self.cycle_loss_fn(real_y, cycled_y)\n\n            id_loss_G = self.identity_loss_fn(real_y, same_y)\n            id_loss_F = self.identity_loss_fn(real_x, same_x)\n\n            total_gen_G_loss = gen_G_loss + total_cycle_loss + id_loss_G\n            total_gen_F_loss = gen_F_loss + total_cycle_loss + id_loss_F\n\n            disc_X_loss = self.disc_loss_fn(disc_real_x, disc_fake_x)\n            disc_Y_loss = self.disc_loss_fn(disc_real_y, disc_fake_y)\n\n        # Apply gradients\n        grads_G = tape.gradient(total_gen_G_loss, self.gen_G.trainable_variables)\n        grads_F = tape.gradient(total_gen_F_loss, self.gen_F.trainable_variables)\n\n        grads_disc_X = tape.gradient(disc_X_loss, self.disc_X.trainable_variables)\n        grads_disc_Y = tape.gradient(disc_Y_loss, self.disc_Y.trainable_variables)\n\n        self.gen_G_optimizer.apply_gradients(zip(grads_G, self.gen_G.trainable_variables))\n        self.gen_F_optimizer.apply_gradients(zip(grads_F, self.gen_F.trainable_variables))\n\n        self.disc_X_optimizer.apply_gradients(zip(grads_disc_X, self.disc_X.trainable_variables))\n        self.disc_Y_optimizer.apply_gradients(zip(grads_disc_Y, self.disc_Y.trainable_variables))\n\n        return {\n            \"G_loss\": total_gen_G_loss,\n            \"F_loss\": total_gen_F_loss,\n            \"D_X_loss\": disc_X_loss,\n            \"D_Y_loss\": disc_Y_loss,\n        }","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-20T04:19:01.675222Z","iopub.execute_input":"2025-07-20T04:19:01.675521Z","iopub.status.idle":"2025-07-20T04:19:01.734307Z","shell.execute_reply.started":"2025-07-20T04:19:01.675480Z","shell.execute_reply":"2025-07-20T04:19:01.733622Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_discriminator():\n    initializer = tf.random_normal_initializer(0., 0.02)\n\n    inp = tf.keras.layers.Input(shape=[256, 256, 3], name='input_image')\n\n    x = tf.keras.layers.Conv2D(64, 4, strides=2, padding='same',\n                               kernel_initializer=initializer)(inp)\n    x = tf.keras.layers.LeakyReLU()(x)\n\n    x = tf.keras.layers.Conv2D(128, 4, strides=2, padding='same',\n                               kernel_initializer=initializer)(x)\n    x = tf.keras.layers.BatchNormalization()(x)\n    x = tf.keras.layers.LeakyReLU()(x)\n\n    x = tf.keras.layers.Conv2D(256, 4, strides=2, padding='same',\n                               kernel_initializer=initializer)(x)\n    x = tf.keras.layers.BatchNormalization()(x)\n    x = tf.keras.layers.LeakyReLU()(x)\n\n    x = tf.keras.layers.Conv2D(512, 4, strides=1, padding='same',\n                               kernel_initializer=initializer)(x)\n    x = tf.keras.layers.BatchNormalization()(x)\n    x = tf.keras.layers.LeakyReLU()(x)\n\n    x = tf.keras.layers.Conv2D(1, 4, strides=1, padding='same',\n                               kernel_initializer=initializer)(x)\n\n    return tf.keras.Model(inputs=inp, outputs=x)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-20T04:19:01.735782Z","iopub.execute_input":"2025-07-20T04:19:01.736030Z","iopub.status.idle":"2025-07-20T04:19:01.757281Z","shell.execute_reply.started":"2025-07-20T04:19:01.736012Z","shell.execute_reply":"2025-07-20T04:19:01.756481Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Photo → Monet generator\ngenerator_G = pix2pix.unet_generator(output_channels=3, norm_type='instancenorm')  # G: Photo → Monet\n\n# Monet → Photo generator\ngenerator_F = pix2pix.unet_generator(output_channels=3, norm_type='instancenorm')  # F: Monet → Photo\n\ndiscriminator_X = build_discriminator()  # For Monet\ndiscriminator_Y = build_discriminator()  # For Photo","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-20T04:19:01.758157Z","iopub.execute_input":"2025-07-20T04:19:01.758427Z","iopub.status.idle":"2025-07-20T04:19:02.504375Z","shell.execute_reply.started":"2025-07-20T04:19:01.758403Z","shell.execute_reply":"2025-07-20T04:19:02.503779Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"generator_G.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-20T04:19:21.449735Z","iopub.execute_input":"2025-07-20T04:19:21.450055Z","iopub.status.idle":"2025-07-20T04:19:21.480510Z","shell.execute_reply.started":"2025-07-20T04:19:21.450036Z","shell.execute_reply":"2025-07-20T04:19:21.479922Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"generator_F.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-20T04:20:40.415062Z","iopub.execute_input":"2025-07-20T04:20:40.415347Z","iopub.status.idle":"2025-07-20T04:20:40.442810Z","shell.execute_reply.started":"2025-07-20T04:20:40.415325Z","shell.execute_reply":"2025-07-20T04:20:40.442196Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"discriminator_X.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-20T04:21:17.419666Z","iopub.execute_input":"2025-07-20T04:21:17.419941Z","iopub.status.idle":"2025-07-20T04:21:17.440206Z","shell.execute_reply.started":"2025-07-20T04:21:17.419921Z","shell.execute_reply":"2025-07-20T04:21:17.439533Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"discriminator_Y.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-20T04:21:33.287568Z","iopub.execute_input":"2025-07-20T04:21:33.287840Z","iopub.status.idle":"2025-07-20T04:21:33.306648Z","shell.execute_reply.started":"2025-07-20T04:21:33.287821Z","shell.execute_reply":"2025-07-20T04:21:33.305955Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Inspection of the Generator without training\nDemonstrate an example of a photo going under one iteration of the monetization process","metadata":{}},{"cell_type":"code","source":"G = generator_G\n\n# Take one real photo (e.g., from your photo_samples list)\ninput_image = photo_samples[0]  # shape: (256, 256, 3)\ninput_image = tf.expand_dims(input_image, 0)  # Add batch dimension\n\n# Generate output (untrained)\ngenerated_image = G(input_image, training=False)\n\n# Convert from [-1, 1] to [0, 1] for display\noutput_image = (generated_image[0] + 1) / 2.0\n\n# Plot original and generated\n\nplt.figure(figsize=(8, 4))\nplt.subplot(1, 2, 1)\nplt.imshow((input_image[0] + 1) / 2.0)\nplt.title(\"Input Image\")\nplt.axis(\"off\")\n\nplt.subplot(1, 2, 2)\nplt.imshow(output_image.numpy())\nplt.title(\"Untrained Generator Output\")\nplt.axis(\"off\")\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-20T04:22:23.500828Z","iopub.execute_input":"2025-07-20T04:22:23.501065Z","iopub.status.idle":"2025-07-20T04:22:23.841442Z","shell.execute_reply.started":"2025-07-20T04:22:23.501049Z","shell.execute_reply":"2025-07-20T04:22:23.840867Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Helper functions to help track loss and see how the images change over epochs","metadata":{}},{"cell_type":"code","source":"from IPython.display import Image, display\nfrom PIL import Image\nimport gc\n\n# ---- 1. Track loss over epochs ----\nclass LossHistory(tf.keras.callbacks.Callback):\n    def on_train_begin(self, logs=None):\n        self.history = {\n            \"G_loss\": [],\n            \"F_loss\": [],\n            \"D_X_loss\": [],\n            \"D_Y_loss\": [],\n        }\n\n    def on_epoch_end(self, epoch, logs=None):\n        for key in self.history:\n            self.history[key].append(logs.get(key))\n\n\n# ---- 2. Save generated images from fixed photo samples ----\nsample_photos = next(iter(photo_ds.take(1)))[0:5]  # 5 fixed input photos\n\ndef generate_and_save_images(model, epoch, save_dir=\"generated_images\"):\n    os.makedirs(save_dir, exist_ok=True)\n    for i, photo in enumerate(sample_photos):\n        prediction = model.gen_G(tf.expand_dims(photo, axis=0), training=False)[0]\n        prediction = ((prediction + 1.0) * 127.5).numpy().astype('uint8')\n        imageio.imwrite(f\"{save_dir}/img_{i}_epoch_{epoch:03d}.png\", prediction)\n\nclass ImageSaverCallback(tf.keras.callbacks.Callback):\n    def __init__(self, sample_images, save_dir=\"monetified_samples\", freq=5):\n        super().__init__()\n        self.sample_images = sample_images  # Tensor of shape (N, H, W, C)\n        self.save_dir = save_dir\n        self.freq = freq\n        os.makedirs(self.save_dir, exist_ok=True)\n\n    def on_epoch_end(self, epoch, logs=None):\n        if (epoch + 1) % self.freq != 0:\n            return  # Skip saving unless it's the right frequency\n\n        generated = self.model.gen_G(self.sample_images, training=False)\n        generated = tf.cast((generated + 1.0) * 127.5, tf.uint8).numpy()\n        originals = tf.cast((self.sample_images + 1.0) * 127.5, tf.uint8).numpy()\n\n        for i, (real, fake) in enumerate(zip(originals, generated)):\n            side_by_side = np.concatenate([real, fake], axis=1)\n            img = Image.fromarray(side_by_side)\n            img.save(os.path.join(self.save_dir, f\"epoch_{epoch+1}_img_{i}.png\"))\n\n        # Clear tensors from memory\n        del generated\n        del originals\n        gc.collect()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-20T04:24:55.742862Z","iopub.execute_input":"2025-07-20T04:24:55.743587Z","iopub.status.idle":"2025-07-20T04:24:59.270107Z","shell.execute_reply.started":"2025-07-20T04:24:55.743563Z","shell.execute_reply":"2025-07-20T04:24:59.269531Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Training Loop\nAfter testing multiple models, I found that a learning rate of 2e-4 worked best. More Epochs seem to improve the model but there were diminishing returns of improvement,  ","metadata":{}},{"cell_type":"code","source":"cyclegan_model = CycleGAN(generator_G, generator_F, discriminator_X, discriminator_Y)\n\ncyclegan_model.compile(\n    gen_G_optimizer=tf.keras.optimizers.Adam(2e-4, beta_1=0.5),\n    gen_F_optimizer=tf.keras.optimizers.Adam(2e-4, beta_1=0.5),\n    disc_X_optimizer=tf.keras.optimizers.Adam(2e-4, beta_1=0.5),\n    disc_Y_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)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-20T04:26:24.691813Z","iopub.execute_input":"2025-07-20T04:26:24.692072Z","iopub.status.idle":"2025-07-20T04:26:24.717167Z","shell.execute_reply.started":"2025-07-20T04:26:24.692057Z","shell.execute_reply":"2025-07-20T04:26:24.716480Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"loss_tracker = LossHistory()\nimage_saver = ImageSaverCallback(sample_images = sample_photos)\n\ntrain_dataset = tf.data.Dataset.zip((photo_ds, monet_ds)).repeat()\ncyclegan_model.fit(\n    train_dataset,\n    epochs=50,\n    steps_per_epoch=300,\n    callbacks=[loss_tracker, image_saver]\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-20T04:26:27.828898Z","iopub.execute_input":"2025-07-20T04:26:27.829154Z","iopub.status.idle":"2025-07-20T05:06:02.231451Z","shell.execute_reply.started":"2025-07-20T04:26:27.829135Z","shell.execute_reply":"2025-07-20T05:06:02.230721Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Results and Analysis\n\nOver the course of 50 epochs, the training losses for the CycleGAN model demonstrate reasonable convergence behavior:\n\n* G_loss and F_loss (generator losses) fluctuate around a relatively stable mean between epochs 10–50, indicating that the generators are learning useful mappings between the photo and Monet domains.\n* D_X_loss and D_Y_loss (discriminator losses) remain lower and more stable, suggesting the discriminators are effectively distinguishing between real and generated images without overpowering the generators.\n* The slight fluctuations across epochs are expected due to the adversarial nature of GAN training, where generator and discriminator losses tend to oscillate.\n\n\nOverall, the loss curves suggest that the model is learning a meaningful translation between domains without collapsing. Additional qualitative inspection of generated images (e.g., side-by-side comparisons) would further validate training success.","metadata":{}},{"cell_type":"code","source":"for key, values in loss_tracker.history.items():\n    plt.plot(values, label=key)\nplt.legend()\nplt.title(\"CycleGAN Loss Over Epochs\")\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Loss\")\nplt.grid()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-19T08:21:36.757505Z","iopub.execute_input":"2025-07-19T08:21:36.757858Z","iopub.status.idle":"2025-07-19T08:21:36.935488Z","shell.execute_reply.started":"2025-07-19T08:21:36.757840Z","shell.execute_reply":"2025-07-19T08:21:36.934752Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Visualize image results after training","metadata":{}},{"cell_type":"code","source":"def create_gif(image_dir, output_path, epochs=50, freq=5):\n    images = []\n    for epoch in range(freq, epochs + 1, freq):\n        path = os.path.join(image_dir, f\"epoch_{epoch}_img_0.png\")\n        if os.path.exists(path):\n            images.append(imageio.imread(path))\n        else:\n            print(f\"❌ Missing: {path}\")\n    \n    if images:\n        print(f\"✅ Saving GIF with {len(images)} frames to {output_path}\")\n        imageio.mimsave(output_path, images, fps=5)\n    else:\n        print(f\"⚠️ No valid frames found. GIF not saved.\")\n\n# Run it for the one tracked image (img_0)\ncreate_gif(\"monetified_samples\", \"evolution_img_0.gif\", epochs=50, freq=5)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-20T05:42:53.819386Z","iopub.execute_input":"2025-07-20T05:42:53.820018Z","iopub.status.idle":"2025-07-20T05:42:55.399099Z","shell.execute_reply.started":"2025-07-20T05:42:53.819991Z","shell.execute_reply":"2025-07-20T05:42:55.398521Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from IPython.display import Image, display\ndisplay(Image(filename=\"evolution_img_0.gif\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-20T05:44:10.244898Z","iopub.execute_input":"2025-07-20T05:44:10.245638Z","iopub.status.idle":"2025-07-20T05:44:10.271242Z","shell.execute_reply.started":"2025-07-20T05:44:10.245604Z","shell.execute_reply":"2025-07-20T05:44:10.270462Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Visually we can see that increase in pixel intensity as the model monetifies the photo.  ","metadata":{}},{"cell_type":"markdown","source":"# Conclusion \nThe CycleGAN model successfully learns to translate photos into Monet-style paintings using unpaired image-to-image translation. The animated GIF demonstrates progressive improvements over epochs, with generated outputs becoming increasingly stylized and visually aligned with Monet’s aesthetic. The discriminator and generator losses also show stable convergence, indicating that the training dynamics are functioning as intended. Switching generators from a custom layer to pix2pix vastly improved the efficiency and score of the model.  Overall, this experiment highlights the effectiveness of CycleGAN with U-Net generators in artistic style transfer tasks.\n\n## References \nhttps://www.tensorflow.org/tutorials/generative/dcgan\nhttps://www.tensorflow.org/guide/autodiff\nAmy Jang, Ana Sofia Uzsoy, and Phil Culliton. I’m Something of a Painter Myself. https://kaggle.com/competitions/gan-getting-started, 2020. Kaggle.","metadata":{}},{"cell_type":"markdown","source":"# Submission to Kaggle","metadata":{}},{"cell_type":"code","source":"import zipfile\nfrom tqdm import tqdm\n\n# Directory to save generated Monet-style images\noutput_dir = \"monet_submission\"\nos.makedirs(output_dir, exist_ok=True)\n\ncount = 0\nMAX_IMAGES = 10000\n\n# Generate Monet-style images from photo dataset\nfor photo_batch in photo_ds:\n    monetified = cyclegan_model.gen_G(photo_batch, training=False)  # Use generator G (photo → Monet)\n    for img in monetified:\n        img = tf.image.resize(img, (256, 256))  # Resize to 256x256\n        img = tf.cast((img + 1.0) * 127.5, tf.uint8)  # Denormalize from [-1, 1] to [0, 255]\n        img_path = os.path.join(output_dir, f\"{count:05}.jpg\")\n        tf.keras.utils.save_img(img_path, img.numpy())\n        count += 1\n        if count >= MAX_IMAGES:\n            break\n    if count >= MAX_IMAGES:\n        break\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-20T05:48:39.037396Z","iopub.execute_input":"2025-07-20T05:48:39.038029Z","iopub.status.idle":"2025-07-20T05:54:09.817914Z","shell.execute_reply.started":"2025-07-20T05:48:39.038004Z","shell.execute_reply":"2025-07-20T05:54:09.817300Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Create a zip file\nwith zipfile.ZipFile(\"images.zip\", \"w\", zipfile.ZIP_DEFLATED) as zipf:\n    for root, _, files in os.walk(output_dir):\n        for file in tqdm(files):\n            filepath = os.path.join(root, file)\n            zipf.write(filepath, arcname=os.path.basename(filepath))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-20T05:55:37.127951Z","iopub.execute_input":"2025-07-20T05:55:37.128228Z","iopub.status.idle":"2025-07-20T05:55:40.562186Z","shell.execute_reply.started":"2025-07-20T05:55:37.128208Z","shell.execute_reply":"2025-07-20T05:55:40.561402Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}