{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":21755,"databundleVersionId":1475600,"sourceType":"competition"}],"dockerImageVersionId":30918,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## Week 5 Deep Learning: *I Am Something of a Painter Myself* with GANs\n\n### Overview\n\nFor this week's project challenge, we’ve been assigned a task via Kaggle's *\"I Am Something of a Painter Myself\"* competition: to generate a set of painting-style images from regular photographs — images convincing enough to potentially fool a museum into believing they were painted by Claude Monet.\n\nFor training, we are provided with two datasets:\n- A collection of Claude Monet paintings\n- A collection of real landscape photographs\n\nThese form the foundation of this week’s deep learning project.\n\n\n\n#### What Unique Challenges Exist for This Deep Learning Project Compared to Prior Weeks?\n\nUnlike earlier projects, this week’s task involves Generative Adversarial Networks (GANs). GANs consist of two models:\n- A Generator: attempts to create realistic Monet-style images\n- A Discriminator: tries to distinguish between real Monet paintings and the generator's output\n\nThe two models are trained in opposition — the generator improves by \"fooling\" the discriminator, while the discriminator gets better at spotting fakes. Their interplay is governed by loss functions, which in turn are optimized using gradient-based methods (something we’ve become familiar with in prior weeks).\n\n\n\n#### How Do the Generator and Discriminator Work Together?\n\nWe can think of the generator as initially producing outputs from random noise. Over time, it learns to generate Monet-style images by:\n- Deconstructing input photos\n- Encoding important features\n- Reconstructing images that resemble Monet’s style\n\nMeanwhile, the discriminator evaluates how close the generator's output is to the real Monet paintings. Loss functions measure the difference between generated images and authentic paintings. These losses guide the training of both models through backpropagation and optimization.\n\n\n\n#### Why Use CycleGAN?\n\nIn this project, I’ll focus on a specific type of GAN known as a CycleGAN, particularly suited for artistic style transfer. While I reference Amy Jang’s CycleGAN tutorial on Kaggle ([link](https://www.kaggle.com/code/amyjang/monet-cyclegan-tutorial)), I also introduce an architectural enhancement using Residual Networks (ResNet).\n\nThis distinguishes our implementation from Amy Jang’s, by incorporating residual blocks in the generator network.\n\n\n\n#### What Are Residual Blocks, and Why Are They Useful?\n\nUnlike simple encoder-decoder models (which compress and then reconstruct features), Residual Blocks allow the network to preserve original input features through skip connections.\n\nBenefits of residual blocks include:\n- Keeping spatial and texture information intact  \n- Learning only delta-style edits (i.e., what to change, not recreate)\n- Strong performance on unpaired image translation (like photo ↔ Monet)\n- More stable training compared to vanilla GANs\n\nThey form the backbone of our generator and are a key reason ResNet-based CycleGANs outperform simpler architectures.\n\n\n#### Why Use PatchGAN as the Discriminator?\n\nInstead of evaluating an entire image globally, PatchGAN looks at local patches — for example, 70x70 regions of the image — and determines whether each patch is \"real\" or \"fake.\"\n\nThis is particularly effective for style transfer tasks like ours. That’s because:\n-  Monet’s style is defined more by local brushstroke texture than overall structure\n-  Classic Monet traits (short, quick strokes, translucent layers, complex color blending) appear at the patch level\n-  Patch-level discrimination is more efficient and less computationally intensive than full-image evaluation\n\n\n\n#### Wrapping Up This Introduction\n\nWhile I’ll provide more details on the CycleGAN architecture and training strategy later in the notebook, this overview should give a sense of the deep learning techniques powering this project.\n\nNow, let's dive into the implementation.\n","metadata":{}},{"cell_type":"code","source":"import tensorflow as tf\n\ngpus = tf.config.list_physical_devices('GPU')\nif gpus:\n    try:\n        for gpu in gpus:\n            tf.config.experimental.set_memory_growth(gpu, True)\n        print(\"Memory growth enabled\")\n    except RuntimeError as e:\n        print(e)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T20:29:03.483819Z","iopub.execute_input":"2025-04-07T20:29:03.484248Z","iopub.status.idle":"2025-04-07T20:29:06.444300Z","shell.execute_reply.started":"2025-04-07T20:29:03.484210Z","shell.execute_reply":"2025-04-07T20:29:06.443525Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 📦 Imports\nimport os\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom PIL import Image\nfrom tqdm.notebook import tqdm","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T20:29:08.917136Z","iopub.execute_input":"2025-04-07T20:29:08.917699Z","iopub.status.idle":"2025-04-07T20:29:09.272384Z","shell.execute_reply.started":"2025-04-07T20:29:08.917671Z","shell.execute_reply":"2025-04-07T20:29:09.271453Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"data_dir = './kaggle'  # or wherever you unzipped it\n\nfor root, dirs, files in os.walk(data_dir):\n    print(f\"Directory: {root}\")\n    print(f\"Subdirectories: {dirs}\")\n    print(f\"Files: {len(files)} files\")\n    break  # just top level","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T20:29:11.816864Z","iopub.execute_input":"2025-04-07T20:29:11.817458Z","iopub.status.idle":"2025-04-07T20:29:11.821678Z","shell.execute_reply.started":"2025-04-07T20:29:11.817427Z","shell.execute_reply":"2025-04-07T20:29:11.820798Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"monet_dir = \"/kaggle/input/gan-getting-started/monet_jpg\"\nphoto_dir = \"/kaggle/input/gan-getting-started/photo_jpg\"\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T20:29:13.074182Z","iopub.execute_input":"2025-04-07T20:29:13.074480Z","iopub.status.idle":"2025-04-07T20:29:13.078103Z","shell.execute_reply.started":"2025-04-07T20:29:13.074458Z","shell.execute_reply":"2025-04-07T20:29:13.077140Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport glob\n\ndef count_images(folder_path, ext=\"jpg\"):\n    return len(glob.glob(os.path.join(folder_path, f\"*.{ext}\")))\n\nmonet_count = count_images(monet_dir)\nphoto_count = count_images(photo_dir)\n\nprint(f\"Monet images: {monet_count}\")\nprint(f\"Photo images: {photo_count}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T20:29:14.171713Z","iopub.execute_input":"2025-04-07T20:29:14.172027Z","iopub.status.idle":"2025-04-07T20:29:14.596027Z","shell.execute_reply.started":"2025-04-07T20:29:14.172003Z","shell.execute_reply":"2025-04-07T20:29:14.595287Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Here are some sample photos from the Monet folder of images.  Probably one of the more complex feature of the Monet \n\npainting that could be arguable difficult to replicate is Monet's specular vibrance at times.  Specularity refers to \n\nthe bright, shiny reflections you see on a surface when it's directly illuminated by a light source.  You can see this light\n\nvibrance below in some of the Monet images that are generally either full sun or partially cloudy days.  Compare to other weather \n\nconditions that are dreary.  Monet's paintings look more muted!  Specularity in Monet's painting, we would note add finer contrasting \n\nlight details!  It might be worth adding a loss function for specularity?  As it turns out we may focus on both directional edges and \n\nnon-directional edges (using sobel functions) to preserve specular highlights.","metadata":{}},{"cell_type":"code","source":"import random\n\nimage_folder = monet_dir  \n\nsample_images = random.sample(os.listdir(image_folder), 9)\n\nplt.figure(figsize=(10, 10))\nfor i, img_name in enumerate(sample_images):\n    img_path = os.path.join(image_folder, img_name)\n    img = Image.open(img_path)\n    plt.subplot(3, 3, i + 1)\n    plt.imshow(img)\n    plt.title(img_name)\n    plt.axis('off')\nplt.suptitle(\"Sample Images from Dataset\", fontsize=16)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T20:29:17.299803Z","iopub.execute_input":"2025-04-07T20:29:17.300118Z","iopub.status.idle":"2025-04-07T20:29:18.256891Z","shell.execute_reply.started":"2025-04-07T20:29:17.300092Z","shell.execute_reply":"2025-04-07T20:29:18.256038Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Water landscape images tend to have cooler tonality (i.e., blue spectrum predominance).  While Monet, has water scenes above that demonstrate similar tonality to some of the images below.  We might notice the Monet image of the ships at see above have quite a bit of blue contrast (i.e., between light and dark ranges of blue).  Compare the shoreline images that have more surface water reflection (i.e., there is less water turbulence).  Blue contrast in the photographic images below have a shallower range in one photo, and a deeper range in another, but notice the warm reflection of autumn trees.  The autumn water landscape picture (shown upper left) has deeper light contrast in the lower left regions of the image.  The center shoreline image seems like it could already have some impressionistic attributes with long exposure capture and deep light contrasts.  Although when we see the Monet images above, it appears that Monet may not have been as interested purely in Golden Hour lighting conditions when thinking of his compositions.","metadata":{}},{"cell_type":"code","source":"import random\n\nimage_folder = photo_dir\n\nsample_images = random.sample(os.listdir(image_folder), 9)\n\nplt.figure(figsize=(10, 10))\nfor i, img_name in enumerate(sample_images):\n    img_path = os.path.join(image_folder, img_name)\n    img = Image.open(img_path)\n    plt.subplot(3, 3, i + 1)\n    plt.imshow(img)\n    plt.title(img_name)\n    plt.axis('off')\nplt.suptitle(\"Sample Images from Dataset\", fontsize=16)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T20:29:21.846467Z","iopub.execute_input":"2025-04-07T20:29:21.846790Z","iopub.status.idle":"2025-04-07T20:29:22.714555Z","shell.execute_reply.started":"2025-04-07T20:29:21.846764Z","shell.execute_reply":"2025-04-07T20:29:22.713386Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"A reading of the histograms below indicate that with blue means more spread out, while green is less variable on the correlation say with red tells us that we have warmer color image tones.  Warm tones are typically more green-red or (yellow in tone).  Cool toned photography is more blue predominant.  Typically landscape photography are in warmer tonal ranges.  Because this is in our photo folder (not the monet folder), the photo dataset should translate better stylistically for Monet's warmer tonal range.","metadata":{}},{"cell_type":"code","source":"def get_color_histogram(img_path):\n    img = Image.open(img_path).resize((64, 64)).convert(\"RGB\")\n    r, g, b = img.split()\n    return {\n        'r_mean': np.mean(r),\n        'g_mean': np.mean(g),\n        'b_mean': np.mean(b)\n    }\n\ncolor_data = []\nfor img_name in tqdm(os.listdir(photo_dir)[:500]):\n    img_path = os.path.join(image_folder, img_name)\n    color_data.append(get_color_histogram(img_path))\n\ncolor_df = pd.DataFrame(color_data)\nsns.pairplot(color_df)\nplt.suptitle(\"RGB Mean Distributions\", y=1.02)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T20:29:26.320353Z","iopub.execute_input":"2025-04-07T20:29:26.320662Z","iopub.status.idle":"2025-04-07T20:29:34.184873Z","shell.execute_reply.started":"2025-04-07T20:29:26.320636Z","shell.execute_reply":"2025-04-07T20:29:34.184010Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"A low entropy < 14 tell us that images are not so complex and easier to train. High entropy +15 > tell us that the image\n\nis more difficult to train.","metadata":{}},{"cell_type":"code","source":"from skimage.measure import shannon_entropy\nfrom skimage.io import imread\n\nentropy_scores = []\nfor img_name in tqdm(os.listdir(photo_dir)[:500]):\n    img_path = os.path.join(image_folder, img_name)\n    img = imread(img_path, as_gray=True)\n    entropy_scores.append(shannon_entropy(img))\n\nsns.histplot(entropy_scores, bins=30)\nplt.title(\"Image Complexity (Entropy)\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T20:29:34.186151Z","iopub.execute_input":"2025-04-07T20:29:34.186482Z","iopub.status.idle":"2025-04-07T20:29:37.102278Z","shell.execute_reply.started":"2025-04-07T20:29:34.186448Z","shell.execute_reply":"2025-04-07T20:29:37.101267Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### What does t-SNE analysis tell us below?  \n\nThe domain space between Monet and photograph images are not easily separable (as visually seen in the graph below).  That is, there isn't any distinct grouping of Monet images relative the domain of non-Monet images.  This has other implications as well.  Namely, we aren't likely to find GAN architecture that is domain generalized across the image very useful for learning well Monet's stylistic features.  This is important since this fits well into patchGAN discriminator architecture.  We are looking for localized stylistic subtelties in the Monet stylistic transfer training case.  Again, we are not interested in global content for a given Monet training image. ","metadata":{}},{"cell_type":"code","source":"import os\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom tqdm import tqdm\n\nfrom tensorflow.keras.applications.vgg16 import VGG16, preprocess_input\nfrom tensorflow.keras.preprocessing.image import img_to_array, load_img\nfrom sklearn.manifold import TSNE\n\n# Load VGG16 model (no top, feature extractor)\nmodel = VGG16(weights=\"imagenet\", include_top=False, input_shape=(64, 64, 3))\n\nfeatures = []\nlabels = []\ndata_dir='./data'\n# Folder 1: Real photos\nphoto_folder = photo_dir\nfor img_name in tqdm(os.listdir(photo_folder)[:200]):\n    img_path = os.path.join(photo_folder, img_name)\n    img = load_img(img_path, target_size=(64, 64))\n    img_arr = preprocess_input(np.expand_dims(img_to_array(img), axis=0))\n    feat = model.predict(img_arr)\n    features.append(feat.flatten())\n    labels.append(0)  # Label: 0 = photo\n\n# Folder 2: Monet paintings\nmonet_folder = monet_dir\nfor img_name in tqdm(os.listdir(monet_folder)[:200]):\n    img_path = os.path.join(monet_folder, img_name)\n    img = load_img(img_path, target_size=(64, 64))\n    img_arr = preprocess_input(np.expand_dims(img_to_array(img), axis=0))\n    feat = model.predict(img_arr)\n    features.append(feat.flatten())\n    labels.append(1)  # Label: 1 = Monet\n\n# Convert to numpy arrays\nfeatures = np.array(features)\nlabels = np.array(labels)\n\n# Apply t-SNE\ntsne = TSNE(n_components=2, perplexity=30, random_state=42)\ntsne_features = tsne.fit_transform(features)\n\n# Plot with color-coded labels\nplt.figure(figsize=(8, 6))\nplt.scatter(\n    tsne_features[:, 0],\n    tsne_features[:, 1],\n    c=labels,\n    cmap=\"coolwarm\",\n    s=10,\n    alpha=0.7\n)\nplt.title(\"t-SNE of VGG16 Features\\n(0 = Photo, 1 = Monet)\")\nplt.colorbar(label=\"Image Domain\")\nplt.axis(\"off\")\nplt.tight_layout()\nplt.show()\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T20:29:39.815680Z","iopub.execute_input":"2025-04-07T20:29:39.816040Z","iopub.status.idle":"2025-04-07T20:30:10.611843Z","shell.execute_reply.started":"2025-04-07T20:29:39.816011Z","shell.execute_reply":"2025-04-07T20:30:10.610920Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Option: use perceptual hash (pHash) to find near-duplicates\nfrom PIL import Image\nimport imagehash\n\nhashes = {}\nduplicates = []\n\nfor img_name in tqdm(os.listdir(photo_dir)):\n    img_path = os.path.join(photo_dir, img_name)\n    img = Image.open(img_path)\n    h = imagehash.phash(img)\n    if h in hashes:\n        duplicates.append((img_name, hashes[h]))\n    else:\n        hashes[h] = img_name\n\nprint(f\"Found {len(duplicates)} potential duplicates.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T20:30:10.612984Z","iopub.execute_input":"2025-04-07T20:30:10.613239Z","iopub.status.idle":"2025-04-07T20:32:06.426607Z","shell.execute_reply.started":"2025-04-07T20:30:10.613204Z","shell.execute_reply":"2025-04-07T20:32:06.425596Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Option: use perceptual hash (pHash) to find near-duplicates\nfrom PIL import Image\nimport imagehash\n\nhashes = {}\nduplicates = []\n\nfor img_name in tqdm(os.listdir(monet_dir)):\n    img_path = os.path.join(monet_dir, img_name)\n    img = Image.open(img_path)\n    h = imagehash.phash(img)\n    if h in hashes:\n        duplicates.append((img_name, hashes[h]))\n    else:\n        hashes[h] = img_name\n\nprint(f\"Found {len(duplicates)} potential duplicates.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T20:32:06.427891Z","iopub.execute_input":"2025-04-07T20:32:06.428651Z","iopub.status.idle":"2025-04-07T20:32:07.944300Z","shell.execute_reply.started":"2025-04-07T20:32:06.428624Z","shell.execute_reply":"2025-04-07T20:32:07.943350Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport tensorflow as tf\nfrom tensorflow import keras\nfrom tensorflow.keras import layers\nfrom glob import glob\nfrom PIL import Image\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T20:32:07.945358Z","iopub.execute_input":"2025-04-07T20:32:07.945677Z","iopub.status.idle":"2025-04-07T20:32:07.949522Z","shell.execute_reply.started":"2025-04-07T20:32:07.945642Z","shell.execute_reply":"2025-04-07T20:32:07.948814Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"IMG_SIZE = 256\nBATCH_SIZE = 10\nAUTOTUNE = tf.data.AUTOTUNE","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T20:39:22.863685Z","iopub.execute_input":"2025-04-07T20:39:22.864079Z","iopub.status.idle":"2025-04-07T20:39:22.868144Z","shell.execute_reply.started":"2025-04-07T20:39:22.864048Z","shell.execute_reply":"2025-04-07T20:39:22.867085Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def save_image(img_tensor, save_path):\n    img = (img_tensor + 1.0) * 127.5  # Denormalize to [0, 255]\n    img = tf.cast(img, tf.uint8)\n    img = tf.image.encode_jpeg(img)\n    tf.io.write_file(save_path, img)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T20:39:24.373995Z","iopub.execute_input":"2025-04-07T20:39:24.374336Z","iopub.status.idle":"2025-04-07T20:39:24.378538Z","shell.execute_reply.started":"2025-04-07T20:39:24.374304Z","shell.execute_reply":"2025-04-07T20:39:24.377668Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def load_image(file_path):\n    img = tf.io.read_file(file_path)\n    img = tf.image.decode_jpeg(img, channels=3)\n    img = tf.image.resize(img, [IMG_SIZE, IMG_SIZE])\n    img = (img / 127.5) - 1.0\n    return img\n\ndef create_dataset(path, batch_size=1):\n    files = tf.data.Dataset.list_files(path + '/*.jpg')\n    dataset = files.map(load_image, num_parallel_calls=tf.data.AUTOTUNE)\n    dataset = dataset.shuffle(1000)\n    dataset = dataset.batch(batch_size)\n    dataset = dataset.repeat()  # 🔥 Add this\n    dataset = dataset.prefetch(tf.data.AUTOTUNE)\n    return dataset\n\n\nphoto_ds = create_dataset(photo_dir, batch_size=BATCH_SIZE)\nmonet_ds = create_dataset(monet_dir, batch_size=BATCH_SIZE)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T20:39:24.939017Z","iopub.execute_input":"2025-04-07T20:39:24.939292Z","iopub.status.idle":"2025-04-07T20:39:25.944846Z","shell.execute_reply.started":"2025-04-07T20:39:24.939269Z","shell.execute_reply":"2025-04-07T20:39:25.943921Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Generator Architecture (ResNet-based) \n\nThe generator in this model is designed to transform input images into Monet-style paintings using a ResNet-based architecture. It takes a 256×256 RGB image as input and outputs a stylized image of the same dimensions. The architecture is composed of several key parts: initial feature extraction, downsampling, residual blocks, upsampling, and output reconstruction.\n\nThe process begins with a 7×7 convolutional layer, which captures broad visual patterns in the input image. This is followed by two downsampling layers using 3×3 convolutions with a stride of 2, reducing the image size to 64×64 while increasing the number of feature channels. This allows the network to learn more abstract features at a smaller scale.\n\nNext, the model applies a series of residual blocks (typically 9), which help preserve the original content and structure of the input image while learning how to apply stylistic changes. Each residual block includes two convolutional layers and a skip connection that adds the input of the block to its output, enabling the model to focus on learning small differences rather than reconstructing the entire image from scratch.\n\nAfter the residual blocks, two upsampling layers are used to restore the original image size. These layers use transposed convolutions to increase the spatial dimensions back to 128×128 and then to 256×256, gradually reconstructing the final image in the new artistic style.\n\nFinally, the output layer is a 7×7 convolution with a `tanh` activation function, which maps pixel values to the range [-1, 1], matching the normalization typically used for GAN training. The result is a stylized image that retains the structure of the input but has been transformed to match the texture and color palette of Monet's paintings.\n","metadata":{}},{"cell_type":"code","source":"from tensorflow.keras import layers\n\ndef residual_block(x, filters):\n    res = x\n    x = layers.Conv2D(filters, kernel_size=3, padding=\"same\")(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.ReLU()(x)\n\n    x = layers.Conv2D(filters, kernel_size=3, padding=\"same\")(x)\n    x = layers.BatchNormalization()(x)\n\n    return layers.add([x, res])\n\ndef build_generator(input_shape=(256, 256, 3), num_res_blocks=9):\n    inputs = layers.Input(shape=input_shape)\n\n    # Initial convolution\n    x = layers.Conv2D(64, kernel_size=7, padding=\"same\")(inputs)\n    x = layers.BatchNormalization()(x)\n    x = layers.ReLU()(x)\n\n    # Downsampling\n    x = layers.Conv2D(128, kernel_size=3, strides=2, padding=\"same\")(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.ReLU()(x)\n\n    x = layers.Conv2D(256, kernel_size=3, strides=2, padding=\"same\")(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.ReLU()(x)\n\n    # Residual blocks\n    for _ in range(num_res_blocks):\n        x = residual_block(x, 256)\n\n    # Upsampling\n    x = layers.Conv2DTranspose(128, kernel_size=3, strides=2, padding=\"same\")(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.ReLU()(x)\n\n    x = layers.Conv2DTranspose(64, kernel_size=3, strides=2, padding=\"same\")(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.ReLU()(x)\n\n    # Final output layer\n    x = layers.Conv2D(3, kernel_size=7, padding=\"same\", activation=\"tanh\")(x)\n\n    return tf.keras.Model(inputs, x, name=\"generator\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T20:32:21.233489Z","iopub.execute_input":"2025-04-07T20:32:21.233799Z","iopub.status.idle":"2025-04-07T20:32:21.241677Z","shell.execute_reply.started":"2025-04-07T20:32:21.233774Z","shell.execute_reply":"2025-04-07T20:32:21.240692Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## PatchGAN Discriminator\n\nThe discriminator in this model is based on a PatchGAN architecture, which differs from traditional discriminators by focusing on local image patches instead of the entire image. Rather than outputting a single scalar prediction (real or fake), PatchGAN outputs a grid of predictions, where each value corresponds to a small patch in the input image. This allows the model to assess the realism of local textures and patterns, which is particularly useful for style transfer tasks like generating Monet-style paintings.\n\nThe architecture starts with a 4×4 convolutional layer with a stride of 2, progressively reducing the spatial dimensions while increasing the depth of the feature maps. This is repeated across four convolutional blocks, each followed by LeakyReLU activation, and batch normalization in all but the first layer. The final convolutional layer outputs a single-channel feature map representing the authenticity of each image patch.\n\nBy evaluating overlapping 70×70 patches (determined by the kernel and stride configuration), the discriminator becomes highly sensitive to fine details, textures, and brushstrokes — which are essential for distinguishing real Monet paintings from generated ones. The result is a more localized and texture-aware evaluation, encouraging the generator to produce more realistic and stylistically consistent outputs.","metadata":{}},{"cell_type":"code","source":"def build_discriminator(input_shape=(256, 256, 3)):\n    inputs = layers.Input(shape=input_shape)\n\n    x = layers.Conv2D(64, kernel_size=4, strides=2, padding=\"same\")(inputs)\n    x = layers.LeakyReLU(0.2)(x)\n\n    x = layers.Conv2D(128, kernel_size=4, strides=2, padding=\"same\")(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.LeakyReLU(0.2)(x)\n\n    x = layers.Conv2D(256, kernel_size=4, strides=2, padding=\"same\")(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.LeakyReLU(0.2)(x)\n\n    x = layers.Conv2D(512, kernel_size=4, strides=1, padding=\"same\")(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.LeakyReLU(0.2)(x)\n\n    # Output patch\n    x = layers.Conv2D(1, kernel_size=4, strides=1, padding=\"same\")(x)\n\n    return tf.keras.Model(inputs, x, name=\"discriminator\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T20:32:25.088048Z","iopub.execute_input":"2025-04-07T20:32:25.088357Z","iopub.status.idle":"2025-04-07T20:32:25.094314Z","shell.execute_reply.started":"2025-04-07T20:32:25.088333Z","shell.execute_reply":"2025-04-07T20:32:25.093464Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Perceptual Loss Model\n\nHere is a perceptual loss model to implement a perceptual loss function.\n\nWe will use this function to improve image realism, better preserve,\n\ntexture, style, and helping to reduce bluriness.  Immediate empirical results\n\nfrom the loss function, incidentally range at around $10^2$ scaling.  We want \n\nthis around $10^1$ ranges, so we can set our associated lambda parameter as needed\n\nlater.","metadata":{}},{"cell_type":"code","source":"from tensorflow.keras.applications import VGG19\nfrom tensorflow.keras.applications.vgg19 import preprocess_input\nfrom tensorflow.keras.models import Model\n\n# Create a VGG model for perceptual loss (use relu2_2 = 'block2_conv2')\nvgg = VGG19(include_top=False, weights='imagenet')\nvgg.trainable = False\nperceptual_model = Model(inputs=vgg.input, outputs=vgg.get_layer('block2_conv2').output)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T20:32:28.121577Z","iopub.execute_input":"2025-04-07T20:32:28.121898Z","iopub.status.idle":"2025-04-07T20:32:31.111800Z","shell.execute_reply.started":"2025-04-07T20:32:28.121870Z","shell.execute_reply":"2025-04-07T20:32:31.111144Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Building the models","metadata":{}},{"cell_type":"code","source":"G_photo_to_monet = build_generator(input_shape=(IMG_SIZE, IMG_SIZE, 3),num_res_blocks=6)\nG_monet_to_photo = build_generator(input_shape=(IMG_SIZE, IMG_SIZE, 3),num_res_blocks=6)\nD_photo = build_discriminator(input_shape=(IMG_SIZE, IMG_SIZE, 3))\nD_monet = build_discriminator(input_shape=(IMG_SIZE, IMG_SIZE, 3))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T20:32:36.044905Z","iopub.execute_input":"2025-04-07T20:32:36.045240Z","iopub.status.idle":"2025-04-07T20:32:36.921092Z","shell.execute_reply.started":"2025-04-07T20:32:36.045216Z","shell.execute_reply":"2025-04-07T20:32:36.920169Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Defining Loss Functions\n\nWe will use loss functions with both buidling generator and discriminators in the cycleGAN implementation.  To do this,\n\nwe want loss function that promote realism, consistency, and finer detail in the generated images.  \n\n\n- **Binary Cross Entropy** is designed to penalize heavily confident but incorrect prediction. It is used for binary tasks.\n\nIt is used in training the generator instance to fool the discriminator, and vice versa, train the discriminator to \n\nidentify fakes.\n\n\n- **GAN Loss function** is, as described in class, the minmax function where the Generator tries to minimize loss, whereas the \n\ndiscriminator tries to maximize loss.  GAN loss drives competition in the adversarial network in better training both\n\nthe generator and discriminator.\n\n\n- **Cycle Consistency Loss** is aimed at improving the quality of the original image after the transformation from Photo to Monet, \n\nand Monet to Photo function models.  Ideally, a good Cycle consistency should return an image as close to the original under\n\na set of model of transformations.  Note:  Unlike Amy Jang's cycleGAN implementation, we are working bidirectionally \n\nand not unidirectionally.  This helps improve the quality of the unidirectional Photo to Monet transformation for such \n\nloss function.\n\n- **Identity Loss** is used to help preserve content and color when such is in the target domain.  In otherwords,\n\nIf you feed a Monet painting into a generator that turns photos → Monets, it should ideally return the same painting without changing it.\n\n\n- **Perceptual Loss** can be thought of a perception humanizing feature.  The idea is that model and loss function is looking at localities \n\nfor texture and structure, but not at individual pixels.  Pixel based losses, for example, produce bluriness in the image, so Perceptual \n\nloss works to minimize bluriness of an image, for example. Increasing Perceptual loss parameter weighting to the upper ranges will \n\nremove banding and artifcats cleanly, but too much weighting means that perceptual loss predominates in the total loss function.\n\nIts an empirical experimenting balancing act getting this lambda parametric setting right.  As mentioned before, loss in our context \n\nmay scale range $10^2$.\n\n-  **Edge Loss** in our instance is creates basic Sobel tensors and preserves both magnitude and directionality of image edges.  This\n\nis to preserve more the image and reduce blurring tendancies, for example.\n\n- **Edge loss preservation** is slightly mistitled as it is designed with light masking features.  This also extracts raw image sobel tensors\n\nfrom both generator and original images, and works to preserve 'highlight' frequency related brightness.  If you examine, for example,\n\nMonet's use of highlights in painting, you will find this creates a definition enhancing quality to his artistic renderings.  In essence,\n\nwe are trying to ensure we capture and more likely retain this stylistic feature which, at times, in painting produces slight glow and \n\nlight shimmering effects.\n","metadata":{}},{"cell_type":"code","source":"# Binary cross-entropy for adversarial loss\nloss_obj = tf.keras.losses.BinaryCrossentropy(from_logits=True)\n\n# GAN loss for generator (wants discriminator to predict \"real\" for generated images)\ndef generator_loss(fake_output):\n    return loss_obj(tf.ones_like(fake_output), fake_output)\n\ndef discriminator_loss(real_output, fake_output):\n    real_labels = tf.ones_like(real_output) * tf.random.uniform([], 0.8, 1.0)  # smoothed labels\n    fake_labels = tf.zeros_like(fake_output)\n    \n    real_loss = loss_obj(real_labels, real_output)\n    fake_loss = loss_obj(fake_labels, fake_output)\n    return (real_loss + fake_loss) * 0.5\n\n\n# Cycle-consistency loss (L1 loss)\nLAMBDA_CYCLE = 10.0\n\ndef cycle_consistency_loss(real_image, cycled_image):\n    return tf.reduce_mean(tf.abs(real_image - cycled_image)) * LAMBDA_CYCLE\n\n# Identity loss (optional)\nLAMBDA_ID = 5.0\n\ndef identity_loss(real_image, same_image):\n    return tf.reduce_mean(tf.abs(real_image - same_image)) * LAMBDA_ID\n\ndef perceptual_loss(real, generated):\n    # Scale to VGG input range\n    real = preprocess_input((real + 1.0) * 127.5)\n    generated = preprocess_input((generated + 1.0) * 127.5)\n\n    real_features = perceptual_model(real)\n    generated_features = perceptual_model(generated)\n\n    return tf.reduce_mean(tf.abs(real_features - generated_features))\n\ndef edge_loss(real, generated):\n    real_edges = tf.image.sobel_edges(real)\n    gen_edges = tf.image.sobel_edges(generated)\n    return tf.reduce_mean(tf.abs(real_edges - gen_edges))\n\ndef edge_preservation_loss(real, generated, brightness_threshold=0.6, epsilon=1e-6):\n\n    \"\"\"\n    Weighted edge preservation loss focusing on bright areas in the image.\n    \"\"\"\n    # Convert to [0, 1] to ensure Sobel edges are meaningful\n    real_norm = tf.clip_by_value((real + 1.0) / 2.0, 0.0, 1.0)\n    gen_norm = tf.clip_by_value((generated + 1.0) / 2.0, 0.0, 1.0)\n\n    # Compute Sobel edges\n    real_edges = tf.image.sobel_edges(real_norm)       # shape: [B, H, W, C, 2]\n    gen_edges = tf.image.sobel_edges(gen_norm)\n\n    # Compute L1 difference\n    edge_diff = tf.abs(real_edges - gen_edges)         # shape: [B, H, W, C, 2]\n\n    # Brightness (mean of channels) — shape: [B, H, W, 1]\n    brightness = tf.reduce_mean(real_norm, axis=-1, keepdims=True)\n\n    # Soft brightness weighting (optional: can use binary if preferred)\n    brightness_weight = tf.clip_by_value((brightness - brightness_threshold), 0.0, 1.0) / (1.0 - brightness_threshold + epsilon)\n    \n    # Expand to match edge shape: [B, H, W, 1, 1]\n    brightness_weight = tf.expand_dims(brightness_weight, axis=-1)\n\n    # Apply weighting\n    weighted_diff = edge_diff * brightness_weight\n\n    return tf.reduce_mean(weighted_diff)\n\n\n\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T20:37:13.203094Z","iopub.execute_input":"2025-04-07T20:37:13.203410Z","iopub.status.idle":"2025-04-07T20:37:13.213020Z","shell.execute_reply.started":"2025-04-07T20:37:13.203386Z","shell.execute_reply":"2025-04-07T20:37:13.212042Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### Combined Loss for each Generator\n\nThis demonstrate an example of how loss might be computed.  \n\nWe will actually see this in the training step below, however.\n\nIt isn't necessary to run the cell below.","metadata":{}},{"cell_type":"code","source":"\n#total_loss_G_photo_to_monet = (\n#    generator_loss(fake_monet_output)\n#    + cycle_consistency_loss(real_photo, cycled_photo)\n#    + cycle_consistency_loss(real_monet, cycled_monet)\n#    + identity_loss(real_monet, same_monet)\n#)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T23:10:34.122552Z","iopub.execute_input":"2025-04-07T23:10:34.122875Z","iopub.status.idle":"2025-04-07T23:10:34.126363Z","shell.execute_reply.started":"2025-04-07T23:10:34.122850Z","shell.execute_reply":"2025-04-07T23:10:34.125422Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Full Train Step Function\n\nThe train_step function defines a single training iteration for the CycleGAN model. It begins by taking a batch \n\nof real images from two domains — typically, photos (real_photo) and artworks or styled images (real_monet). Using \n\nthese, the function applies both generators: G_photo_to_monet converts real photos to Monet-style images \n\n(fake_monet), while G_monet_to_photo performs the reverse, generating photo-style images from Monet paintings \n\n(fake_photo). These generated images are then passed back through the opposite generators to produce cycled_photo \n\nand cycled_monet, which represent reconstructions of the original inputs. This enables the use of cycle consistency \n\nloss, which encourages the model to preserve the content and structure of the original images after a round-trip \n\ntranslation.\n\nTo further stabilize the mapping and maintain identity when translating an image already in the target domain, the \n\nmodel also computes identity mappings (same_photo, same_monet) and applies an identity loss. This helps preserve color \n\nand content consistency, preventing unnecessary alterations.\n\nThe function then uses the discriminators to evaluate both real and fake images in each domain. These evaluations produce \n\npredictions that are used to compute adversarial losses: the generator tries to fool the discriminator into thinking the\n\nfake images are real, while the discriminator tries to correctly distinguish between real and generated samples. To \n\nimprove stability and regularize the discriminator, label smoothing is applied to the real labels.\n\nIn addition to adversarial, cycle, and identity losses, the function can optionally incorporate more advanced losses like \n\nperceptual loss, which compares high-level feature representations from a pretrained VGG model, and edge-based losses \n\n(like Sobel or brightness-weighted edge loss), which enforce sharpness and structural fidelity in regions of visual importance.\n\nAfter all losses are calculated, gradients are computed for both generators and discriminators using TensorFlow’s GradientTape. \n\nThese gradients are then applied via their respective optimizers to update the model weights. The function returns a dictionary \n\nof the various loss components for monitoring and visualization during training.","metadata":{}},{"cell_type":"code","source":"# Create optimizers\ngenerator_G_optimizer = tf.keras.optimizers.Adam(2e-4, beta_1=0.5)\ngenerator_F_optimizer = tf.keras.optimizers.Adam(2e-4, beta_1=0.5)\ndiscriminator_X_optimizer = tf.keras.optimizers.Adam(2e-4, beta_1=0.5)\ndiscriminator_Y_optimizer = tf.keras.optimizers.Adam(2e-4, beta_1=0.5)\n\n@tf.function\ndef train_step(real_photo, real_monet):\n    with tf.GradientTape(persistent=True) as tape:\n        # Generate fakes\n        fake_monet = G_photo_to_monet(real_photo, training=True)\n        fake_photo = G_monet_to_photo(real_monet, training=True)\n\n        # Cycled images\n        cycled_photo = G_monet_to_photo(fake_monet, training=True)\n        cycled_monet = G_photo_to_monet(fake_photo, training=True)\n\n        # Identity (optional but helpful)\n        same_photo = G_monet_to_photo(real_photo, training=True)\n        same_monet = G_photo_to_monet(real_monet, training=True)\n\n        # Discriminator outputs\n        disc_real_photo = D_photo(real_photo, training=True)\n        disc_real_monet = D_monet(real_monet, training=True)\n        disc_fake_photo = D_photo(fake_photo, training=True)\n        disc_fake_monet = D_monet(fake_monet, training=True)\n\n        # Generator losses\n        gen_G_loss = generator_loss(disc_fake_monet)\n        gen_F_loss = generator_loss(disc_fake_photo)\n\n        # Cycle losses\n        cycle_loss_total = cycle_consistency_loss(real_photo, cycled_photo) + \\\n                           cycle_consistency_loss(real_monet, cycled_monet)\n\n        # Identity losses\n        id_loss_G = identity_loss(real_monet, same_monet)\n        id_loss_F = identity_loss(real_photo, same_photo)\n\n        # Perceptual loss (real vs fake)\n        perc_loss_G = perceptual_loss(real_monet, fake_monet)\n        perc_loss_F = perceptual_loss(real_photo, fake_photo)\n\n        lambda_perc = 0.05  # You can adjust this for more or less emphasis\n\n        # Edge loss (real vs fake)\n        edge_loss_G = edge_loss(real_monet, fake_monet)\n        edge_loss_F = edge_loss(real_photo, fake_photo)\n\n        lambda_edge = 10.0\n\n        # Edge preservation loss (real vs fake)\n        edge_pres_loss_G = edge_preservation_loss(real_monet, fake_monet)\n        edge_pres_loss_F = edge_preservation_loss(real_photo, fake_photo)\n\n        lambda_edge_pres = 100.0\n\n        # Total generator losses\n        total_loss_G = gen_G_loss + cycle_loss_total + id_loss_G + lambda_perc * perc_loss_G + lambda_edge * edge_loss_G + lambda_edge_pres * edge_pres_loss_G\n        total_loss_F = gen_F_loss + cycle_loss_total + id_loss_F + lambda_perc * perc_loss_F + lambda_edge * edge_loss_F + lambda_edge_pres * edge_pres_loss_F\n\n        # Discriminator losses\n        disc_loss_photo = discriminator_loss(disc_real_photo, disc_fake_photo)\n        disc_loss_monet = discriminator_loss(disc_real_monet, disc_fake_monet)\n  \n    \n    # Calculate gradients and apply\n    generator_G_grads = tape.gradient(total_loss_G, G_photo_to_monet.trainable_variables)\n    generator_F_grads = tape.gradient(total_loss_F, G_monet_to_photo.trainable_variables)\n\n    discriminator_photo_grads = tape.gradient(disc_loss_photo, D_photo.trainable_variables)\n    discriminator_monet_grads = tape.gradient(disc_loss_monet, D_monet.trainable_variables)\n\n    generator_G_optimizer.apply_gradients(zip(generator_G_grads, G_photo_to_monet.trainable_variables))\n    generator_F_optimizer.apply_gradients(zip(generator_F_grads, G_monet_to_photo.trainable_variables))\n\n    discriminator_X_optimizer.apply_gradients(zip(discriminator_photo_grads, D_photo.trainable_variables))\n    discriminator_Y_optimizer.apply_gradients(zip(discriminator_monet_grads, D_monet.trainable_variables))\n\n    return {\n        \"gen_G\": total_loss_G,\n        \"gen_F\": total_loss_F,\n        \"disc_photo\": disc_loss_photo,\n        \"disc_monet\": disc_loss_monet,\n        \"cycle\": cycle_loss_total,\n        \"id_G\": id_loss_G,\n        \"id_F\": id_loss_F,\n        \"perc_G\": perc_loss_G,\n        \"perc_F\": perc_loss_F,\n        \"edge_G\": edge_loss_G,\n        \"edge_F\": edge_loss_F,\n        \"edge_pres_G\": edge_pres_loss_G,\n        \"edge_pres_F\": edge_pres_loss_F\n\n    }\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T20:39:40.744576Z","iopub.execute_input":"2025-04-07T20:39:40.744939Z","iopub.status.idle":"2025-04-07T20:39:40.764112Z","shell.execute_reply.started":"2025-04-07T20:39:40.744907Z","shell.execute_reply":"2025-04-07T20:39:40.763334Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The helper functions below will be useful in monitoring epoch training progress,\n\nso that we can see a side-by-side comparison of input originals relative a fake monet\n\ngenerated image.","metadata":{}},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport tensorflow as tf\nimport os\n\ndef denormalize(img):\n    img = (img + 1.0) * 127.5\n    return tf.cast(img, tf.uint8)\n\ndef generate_and_save_images(model, test_input, epoch, name='', save_dir='outputs'):\n    prediction = model(test_input, training=False)\n\n    if not os.path.exists(save_dir):\n        os.makedirs(save_dir)\n\n    plt.figure(figsize=(6, 3))\n\n    # Original photo\n    plt.subplot(1, 2, 1)\n    plt.imshow(denormalize(test_input[0]).numpy())\n    plt.title(\"Input Photo\")\n    plt.axis('off')\n\n    # Generated Monet-style painting\n    plt.subplot(1, 2, 2)\n    plt.imshow(denormalize(prediction[0]).numpy())\n    plt.title(\"Generated Monet\")\n    plt.axis('off')\n\n    plt.tight_layout()\n    plt.savefig(f\"{save_dir}/epoch_{epoch:03d}{name}.png\")\n    plt.close()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T20:39:45.792595Z","iopub.execute_input":"2025-04-07T20:39:45.792895Z","iopub.status.idle":"2025-04-07T20:39:45.799371Z","shell.execute_reply.started":"2025-04-07T20:39:45.792871Z","shell.execute_reply":"2025-04-07T20:39:45.798528Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\nphoto_paths = tf.io.gfile.glob(\"/kaggle/input/gan-getting-started/photo_jpg/*.jpg\")\nmonet_paths = tf.io.gfile.glob(\"/kaggle/input/gan-getting-started/monet_jpg/*.jpg\")\n\nsteps_per_epoch = min(len(photo_paths), len(monet_paths)) // BATCH_SIZE\nprint(steps_per_epoch)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T20:39:47.205743Z","iopub.execute_input":"2025-04-07T20:39:47.206138Z","iopub.status.idle":"2025-04-07T20:39:47.243502Z","shell.execute_reply.started":"2025-04-07T20:39:47.206104Z","shell.execute_reply":"2025-04-07T20:39:47.242738Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Train Images\n\nThis is a lengthier train on a modest lower level GPU that is CUDA enabled.\n\nPlan for around a couple hours minimum (in my experience).\n\nWe are using 100 epochs, and combine the photo_ds (datset) and monet_ds to form \n\nthe train_datset.  In monitoring performance we can watch for all of the loss \n\nfunction outputs.  Noting:  these loss computations are averaged per step per epoch.\n\nWe have set prediction image outputs for an original image every 5 epochs of completion.","metadata":{}},{"cell_type":"code","source":"EPOCHS = 100\n\ntrain_dataset = tf.data.Dataset.zip((photo_ds, monet_ds))\n\n\nimport time\n\nfor epoch in range(EPOCHS):\n    print(f\"\\n🌀 Epoch {epoch+1}/{EPOCHS}\")\n    start = time.time()\n\n    losses = {\n        \"gen_G\": 0,\n        \"gen_F\": 0,\n        \"disc_photo\": 0,\n        \"disc_monet\": 0,\n        \"cycle\": 0,\n        \"id_G\": 0,\n        \"id_F\": 0,\n        \"perc_G\": 0,\n        \"perc_F\": 0,\n        \"edge_G\": 0,\n        \"edge_F\": 0,\n        \"edge_pres_G\": 0,\n        \"edge_pres_F\": 0\n    }\n\n    for step, (photo_batch, monet_batch) in enumerate(train_dataset):\n        if step > steps_per_epoch:\n            break\n\n        loss_vals = train_step(photo_batch, monet_batch)\n\n        for k in losses:\n            losses[k] += loss_vals[k]\n\n        if step % 100 == 0:\n            print(f\"  Step {step}/{steps_per_epoch}\")\n\n    # Average loss values\n    for k in losses:\n        losses[k] = losses[k] / steps_per_epoch\n    sample_photo = next(iter(photo_ds.take(1)))\n    if epoch % 5 == 0:\n        generate_and_save_images(G_photo_to_monet, sample_photo, epoch)\n    print(f\"✅ Epoch {epoch+1} completed in {time.time() - start:.2f}s\")\n    print(f\"  Generator G loss:      {losses['gen_G']:.4f}\")\n    print(f\"  Generator F loss:      {losses['gen_F']:.4f}\")\n    print(f\"  Discriminator Photo:   {losses['disc_photo']:.4f}\")\n    print(f\"  Discriminator Monet:   {losses['disc_monet']:.4f}\")\n    print(f\"  Cycle Consistency:     {losses['cycle']:.4f}\")\n    print(f\"  Identity G/F:          {losses['id_G']:.4f} / {losses['id_F']:.4f}\")\n    print(f\"  Perceptual Loss G:     {losses['perc_G']:.4f}\")\n    print(f\"  Perceptual Loss F:     {losses['perc_F']:.4f}\")\n    print(f\"  Edge Loss G:           {losses['edge_G']:.4f}\")\n    print(f\"  Edge Loss F:           {losses['edge_F']:.4f}\")\n    print(f\"  Edge Pres Loss G:      {losses['edge_pres_G']:.4f}\")\n    print(f\"  Edge Pres Loss F:      {losses['edge_pres_F']:.4f}\")\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T20:39:50.593604Z","iopub.execute_input":"2025-04-07T20:39:50.593908Z","iopub.status.idle":"2025-04-07T22:43:58.114868Z","shell.execute_reply.started":"2025-04-07T20:39:50.593883Z","shell.execute_reply":"2025-04-07T22:43:58.113887Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Extract 10 unbatched individual images from the dataset\nsample_photos = [image for image in photo_ds.unbatch().take(10)]\nfor i, sample_photo in enumerate(sample_photos):\n    generate_and_save_images(G_photo_to_monet, tf.expand_dims(sample_photo, 0), 100, name=str(i), save_dir='outputs')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T22:44:16.185098Z","iopub.execute_input":"2025-04-07T22:44:16.185397Z","iopub.status.idle":"2025-04-07T22:44:19.558244Z","shell.execute_reply.started":"2025-04-07T22:44:16.185374Z","shell.execute_reply":"2025-04-07T22:44:19.557518Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def load_images_from_directory(directory, prefix=\"epoch100\", count=10):\n    images = []\n    for i in range(count):\n        path = os.path.join(directory, f\"{prefix}{i}.png\")\n        img = tf.io.read_file(path)\n        img = tf.image.decode_png(img, channels=3)\n        images.append(img)\n    return images","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T22:44:27.872146Z","iopub.execute_input":"2025-04-07T22:44:27.872482Z","iopub.status.idle":"2025-04-07T22:44:27.877037Z","shell.execute_reply.started":"2025-04-07T22:44:27.872458Z","shell.execute_reply":"2025-04-07T22:44:27.876071Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def display_image_grid(images, rows=2, cols=5, title=\"Generated Monet Images\"):\n    plt.figure(figsize=(cols * 3, rows * 3))\n    for i, img in enumerate(images):\n        plt.subplot(rows, cols, i + 1)\n        plt.imshow(img.numpy())\n        plt.axis('off')\n    plt.suptitle(title, fontsize=16)\n    plt.tight_layout()\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T22:44:31.815677Z","iopub.execute_input":"2025-04-07T22:44:31.815988Z","iopub.status.idle":"2025-04-07T22:44:31.820875Z","shell.execute_reply.started":"2025-04-07T22:44:31.815943Z","shell.execute_reply":"2025-04-07T22:44:31.819872Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Results - Qualitative Observation analysis of Sample Images\n\nWe notice in the produced images below, that either images doesn't appear excessively blurred in the case of the Monet fake images.\n\nIf we were to remove the Edge loss functions, we would more stongly see this feature in produced images. However, Monet's expressive\n\nstylizations are quite muted between image sets. The overall appearance of these images appears as though a semi-translucent light mask\n\nwere applied giving a Matte like layered addition to each image. There isn't a degradation in the resolution of features (no obvious strong\n\nblurring signal), so we can reason that the edge loss functions have done their jobs! Also the perceptual loss functions appear in these sets\n\nof images to have prevented artefacts and banding-like textural noise. These usually show up like repeating textural bricks across an image\n\nif present which can be evident for the basic cycleGAN implementations that I have seen.","metadata":{}},{"cell_type":"code","source":"output_images = load_images_from_directory('./outputs', prefix=\"epoch_100\", count=10)\ndisplay_image_grid(output_images)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T22:44:35.866631Z","iopub.execute_input":"2025-04-07T22:44:35.866925Z","iopub.status.idle":"2025-04-07T22:44:36.530772Z","shell.execute_reply.started":"2025-04-07T22:44:35.866902Z","shell.execute_reply":"2025-04-07T22:44:36.529932Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\noutput_dir = \"monet_jpg_outputs\"\nos.makedirs(output_dir, exist_ok=True)\noutput_dir = \"saved_models\"\nos.makedirs(output_dir, exist_ok=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T22:48:10.969847Z","iopub.execute_input":"2025-04-07T22:48:10.970227Z","iopub.status.idle":"2025-04-07T22:48:10.974668Z","shell.execute_reply.started":"2025-04-07T22:48:10.970198Z","shell.execute_reply":"2025-04-07T22:48:10.973731Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"G_photo_to_monet.save(\"saved_models/G_photo_to_monet.keras\")\nG_monet_to_photo.save(\"saved_models/G_monet_to_photo.keras\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T22:48:13.616055Z","iopub.execute_input":"2025-04-07T22:48:13.616395Z","iopub.status.idle":"2025-04-07T22:48:13.982566Z","shell.execute_reply.started":"2025-04-07T22:48:13.616366Z","shell.execute_reply":"2025-04-07T22:48:13.981607Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Let's batch predict several thousand images for the Kaggle challenge","metadata":{}},{"cell_type":"code","source":"import tensorflow as tf\nfrom PIL import Image\n\ndef generate_and_save_images(model, dataset, total_count=7000, save_dir=\"monet_jpg_outputs\"):\n    count = 0\n    for batch in dataset.unbatch().batch(1):  # Ensure individual images\n        if count >= total_count:\n            break\n\n        # Generate Monet-style image\n        generated = model(batch, training=False)[0]\n        image = tf.clip_by_value((generated + 1.0) * 127.5, 0, 255)\n        image = tf.cast(image, tf.uint8).numpy()\n\n        # Save as .jpg\n        img = Image.fromarray(image)\n        img.save(os.path.join(save_dir, f\"monet_{count:05d}.jpg\"), format=\"JPEG\", quality=95)\n        count += 1\n\n        if count % 500 == 0:\n            print(f\"Saved {count} images...\")\n\n    print(f\"✅ Done! {count} Monet images saved to {save_dir}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T22:49:43.654153Z","iopub.execute_input":"2025-04-07T22:49:43.654555Z","iopub.status.idle":"2025-04-07T22:49:43.661127Z","shell.execute_reply.started":"2025-04-07T22:49:43.654523Z","shell.execute_reply":"2025-04-07T22:49:43.660094Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"generate_and_save_images(G_photo_to_monet, photo_ds, total_count=7000)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T22:50:06.867694Z","iopub.execute_input":"2025-04-07T22:50:06.868041Z","iopub.status.idle":"2025-04-07T22:55:50.714754Z","shell.execute_reply.started":"2025-04-07T22:50:06.868013Z","shell.execute_reply":"2025-04-07T22:55:50.713687Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import zipfile\n\ndef zip_images_folder(folder=\"monet_jpg_outputs\", output_zip=\"images.zip\"):\n    with zipfile.ZipFile(output_zip, 'w', zipfile.ZIP_DEFLATED) as zipf:\n        for root, _, files in os.walk(folder):\n            for file in files:\n                file_path = os.path.join(root, file)\n                arcname = os.path.relpath(file_path, start=folder)\n                zipf.write(file_path, arcname)\n    print(f\"📦 Zipped into {output_zip}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T22:56:07.574684Z","iopub.execute_input":"2025-04-07T22:56:07.575004Z","iopub.status.idle":"2025-04-07T22:56:07.579859Z","shell.execute_reply.started":"2025-04-07T22:56:07.574979Z","shell.execute_reply":"2025-04-07T22:56:07.579047Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"zip_images_folder(\"monet_jpg_outputs\", \"images.zip\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T22:56:21.120280Z","iopub.execute_input":"2025-04-07T22:56:21.120583Z","iopub.status.idle":"2025-04-07T22:56:28.522095Z","shell.execute_reply.started":"2025-04-07T22:56:21.120553Z","shell.execute_reply":"2025-04-07T22:56:28.521356Z"}},"outputs":[],"execution_count":null}]}