{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":21755,"databundleVersionId":1475600,"sourceType":"competition"}],"dockerImageVersionId":31011,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Monet-Style Image Generation using CycleGAN","metadata":{"_uuid":"2c9ecfe9-4064-4827-ac31-63ce9d462c88","_cell_guid":"23b8c4f7-1faf-4ba7-9049-0c6322dceacf","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"markdown","source":"##  Introduction\nThe objective is to build and train a GAN model capable of generating images in the artistic style of Claude Monet. We can either transform existing photographs into Monet's style or generate new images from scratch that mimic his characteristic brush strokes, color palettes, and overall aesthetic.\n\n**The Challenge: CycleGAN**\nWe will implement a CycleGAN, a type of GAN particularly suited for image-to-image translation tasks where paired training data (e.g., a photo and its exact Monet-style equivalent) is not available. A CycleGAN learns mappings between two distinct image domains (in our case, landscape photographs and Monet paintings) using two generators and two discriminators.\n* **Generator G:** Learns to translate photos into Monet-style paintings ($Photo \\rightarrow Monet$).\n* **Generator F:** Learns to translate Monet paintings back into photos ($Monet \\rightarrow Photo$).\n* **Discriminator D_Y:** Tries to distinguish between real Monet paintings and generated (fake) Monet paintings.\n* **Discriminator D_X:** Tries to distinguish between real photos and generated (fake) photos.\n\nThe key idea is **cycle consistency loss**: if we translate a photo to Monet style ($G(Photo)$) and then translate it back to a photo ($F(G(Photo))$), the result should look very similar to the original photo. The same applies in the reverse direction ($G(F(Monet)) \\approx Monet$). This ensures the generators learn meaningful translations rather than just outputting random images in the target style.\n\n**Dataset**\nThe competition provides two main sets of images:\n* **Monet Paintings:** ~300 images (`monet_jpg`, `monet_tfrec`). Used to train the model on Monet's style.\n* **Photos:** ~7000 landscape photos (`photo_jpg`, `photo_tfrec`). These are the images we need to transform into Monet style for the submission.\n\n**Evaluation Metric: MiFID**\nSubmissions are evaluated using the **Memorization-informed Fréchet Inception Distance (MiFID)** score.","metadata":{"_uuid":"f30f74c2-39b8-4fb2-ac2e-7936080e481f","_cell_guid":"8e802a92-23e8-4ea4-854c-bb389996b5f0","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"markdown","source":"## Imports & Configurations\n* Define constants, hyperparameters, and paths to the dataset.","metadata":{}},{"cell_type":"code","source":"# --- Imports ---\nimport os\nimport numpy as np\nimport tensorflow as tf\nfrom tensorflow import keras\nfrom tensorflow.keras import layers\nimport matplotlib.pyplot as plt\nfrom PIL import Image\nimport random\nimport time\nimport zipfile\nfrom tqdm.notebook import tqdm\n\n\n# --- Configuration ---\n# Data Paths (Kaggle)\nBASE_PATH = '/kaggle/input/gan-getting-started/'\n\nMONET_TFREC_PATH = os.path.join(BASE_PATH, 'monet_tfrec')\nPHOTO_TFREC_PATH = os.path.join(BASE_PATH, 'photo_tfrec')\nMONET_JPG_PATH = os.path.join(BASE_PATH, 'monet_jpg')\nPHOTO_JPG_PATH = os.path.join(BASE_PATH, 'photo_jpg')\n\n# Model & Training Params\nIMG_WIDTH = 256\nIMG_HEIGHT = 256\nIMG_CHANNELS = 3\nBUFFER_SIZE = 200 # For shuffling dataset\nBATCH_SIZE = 1\nEPOCHS = 25\nAUTOTUNE = tf.data.AUTOTUNE\n\n# Loss weights\nLAMBDA_CYCLE = 10.0\nLAMBDA_IDENTITY = 0.5\n\n# Output directory for generated images\nOUTPUT_DIR = \"/kaggle/working/images/\"\nZIP_FILENAME = \"/kaggle/working/images.zip\"","metadata":{"_uuid":"33bfb3c1-9354-40de-878b-52097884ef31","_cell_guid":"d3f76a49-c932-44f1-8d87-1fd8f28ee2b2","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-04-29T08:16:25.053214Z","iopub.execute_input":"2025-04-29T08:16:25.053601Z","iopub.status.idle":"2025-04-29T08:16:28.486576Z","execution_failed":"2025-04-29T08:40:19.958Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Exploratory Data Analysis (EDA)\n\n- Inspect the dataset structure, check file counts, verify image dimensions, and visualize some samples.","metadata":{"_uuid":"81b6389e-9ed9-46d0-be07-b8919b72a5c9","_cell_guid":"d321b473-5f98-48c8-a6db-e2a07aa89fb5","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"# --- Inspection ---\n\n# List files and count\nmonet_jpg_files = os.listdir(MONET_JPG_PATH)\nphoto_jpg_files = os.listdir(PHOTO_JPG_PATH)\nprint(f\"Number of Monet JPG files found: {len(monet_jpg_files)}\")\nprint(f\"Number of Photo JPG files found: {len(photo_jpg_files)}\")\n\n# List TFRecord files\nmonet_tfrec_files = tf.io.gfile.glob(os.path.join(MONET_TFREC_PATH, '*.tfrec'))\nphoto_tfrec_files = tf.io.gfile.glob(os.path.join(PHOTO_TFREC_PATH, '*.tfrec'))\nprint(f\"Number of Monet TFRecord files found: {len(monet_tfrec_files)}\")\nprint(f\"Number of Photo TFRecord files found: {len(photo_tfrec_files)}\")\n\n\n# --- Visualization ---\n \n# Function to display sample images\ndef display_samples(image_paths, num_samples=5, title=\"Sample Images\"):\n\n    plt.figure(figsize=(15, 4))\n    plt.suptitle(title, fontsize=16)\n    random_indices = random.sample(range(len(image_paths)), min(num_samples, len(image_paths)))\n\n    for i, idx in enumerate(random_indices):\n        img_path = image_paths[idx]\n        plt.subplot(1, num_samples, i + 1)\n        img_raw = tf.io.read_file(img_path)\n        img = tf.image.decode_jpeg(img_raw, channels=3)\n        plt.imshow(img)\n        plt.title(os.path.basename(img_path)[:15]+\"...\") # Show start of filename\n        plt.axis('off')\n\n    plt.tight_layout(rect=[0, 0.03, 1, 0.95])\n    plt.show()\n\n# Get full paths for visualization\nmonet_jpg_fullpaths = [os.path.join(MONET_JPG_PATH, f) for f in monet_jpg_files if f.lower().endswith('.jpg')]\nphoto_jpg_fullpaths = [os.path.join(PHOTO_JPG_PATH, f) for f in photo_jpg_files if f.lower().endswith('.jpg')]\n\n# Display samples if paths exist\ndisplay_samples(monet_jpg_fullpaths, num_samples=5, title=\"Sample Monet Paintings (JPG)\")\ndisplay_samples(photo_jpg_fullpaths, num_samples=5, title=\"Sample Photos (JPG)\")","metadata":{"_uuid":"63aa22c8-9618-4e22-9566-ced5d4cf784c","_cell_guid":"8d104b75-877f-4d88-a0fc-0d7523f82be9","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-04-29T08:16:28.496335Z","iopub.execute_input":"2025-04-29T08:16:28.496538Z","iopub.status.idle":"2025-04-29T08:16:30.287267Z","execution_failed":"2025-04-29T08:40:19.958Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## EDA Summary and Preprocessing Discussion\n\n* **File Counts:** The number of JPG files matches the competition description (300 Monet, ~7k Photos).\n* **Image Dimensions:** Sample images confirmed the expected 256x256x3 shape.\n* **Visual Inspection:** The sample images clearly show the two distinct domains: Monet's impressionist paintings and standard landscape photographs.\n* **Format Choice:** We will use the TFRecord files (`monet_tfrec`, `photo_tfrec`) for training as they are generally more efficient for TensorFlow data pipelines, especially with accelerators (GPU/TPU).\n* **Preprocessing Steps:**\n    1.  **Decoding:** TFRecord files need to be parsed to extract the image data.\n    2.  **Normalization:** Image pixel values (currently `uint8` [0, 255]) **must** be normalized to the range `[-1, 1]` (common practice for GANs using `tanh` activation in the generator's output layer). This will be done by casting to `float32` and applying `(image / 127.5) - 1.0`.\n    3.  **Data Type:** Ensure images are `tf.float32`.\n    4.  **Shuffling & Batching:** The `tf.data.Dataset` pipeline will handle shuffling and batching.\n    5.  **Data Augmentation:** For CycleGAN, light augmentation like random horizontal flips *can* be applied to both datasets during loading to potentially improve generalization. We might add random jitter (slight resizing and cropping) as well, common in CycleGAN implementations.","metadata":{"_uuid":"3817985e-76c4-497d-8cb0-e9603de4d47c","_cell_guid":"a7d17366-ee7d-4960-bcb9-c986dbf0f83a","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"markdown","source":"## Data Loading and Preprocessing Pipeline\n\n- Create `tf.data.Dataset` pipelines to efficiently load, decode, and preprocess images from TFRecord files.","metadata":{"_uuid":"b13f4ddb-c65e-488b-9b43-6c7d5afb4274","_cell_guid":"19a1c45e-64b6-4e6e-8d80-0f56062d4b25","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"# --- TFRecord Parsing and Preprocessing Functions ---\n\ndef decode_tfrecord(record_bytes):\n    \"\"\"Parses a single TFRecord and decodes the JPEG image.\"\"\"\n    features = tf.io.parse_single_example(record_bytes, {\n        'image': tf.io.FixedLenFeature([], tf.string)\n    })\n    image = tf.io.decode_jpeg(features['image'], channels=3)\n    image = tf.reshape(image, [IMG_HEIGHT, IMG_WIDTH, IMG_CHANNELS])\n    # Cast to float32 for normalization\n    image = tf.cast(image, tf.float32)\n    return image\n\ndef normalize_image(image):\n    \"\"\"Normalizes image pixel values to the range [-1, 1].\"\"\"\n    image = (image / 127.5) - 1.0\n    return image\n\ndef random_jitter(image):\n    \"\"\"Applies random jitter: resizing and random cropping.\"\"\"\n    # Resize to 286 x 286 x 3\n    image = tf.image.resize(image, [286, 286],\n                            method=tf.image.ResizeMethod.NEAREST_NEIGHBOR)\n    # Randomly crop to 256 x 256 x 3\n    image = tf.image.random_crop(image, size=[IMG_HEIGHT, IMG_WIDTH, IMG_CHANNELS])\n    # Random mirroring\n    image = tf.image.random_flip_left_right(image)\n    return image\n\ndef preprocess_image_train(image):\n    \"\"\"Applies jitter and normalization for training.\"\"\"\n    image = random_jitter(image)\n    image = normalize_image(image)\n    return image\n\ndef preprocess_image_test(image):\n    \"\"\"Applies only normalization for testing/generation.\"\"\"\n    image = normalize_image(image)\n    return image\n\n\n# --- Dataset Creation Function ---\n\ndef load_dataset(tfrecord_files, train=True, shuffle=True):\n    \"\"\"Creates a tf.data.Dataset from TFRecord files.\"\"\"\n    dataset = tf.data.TFRecordDataset(tfrecord_files)\n    # Map decoding and preprocessing\n    if train:\n        dataset = dataset.map(decode_tfrecord, num_parallel_calls=AUTOTUNE)\n        dataset = dataset.map(preprocess_image_train, num_parallel_calls=AUTOTUNE)\n    else: # For generation/testing, don't apply jitter\n        dataset = dataset.map(decode_tfrecord, num_parallel_calls=AUTOTUNE)\n        dataset = dataset.map(preprocess_image_test, num_parallel_calls=AUTOTUNE)\n\n    if shuffle:\n        dataset = dataset.shuffle(BUFFER_SIZE)\n\n    dataset = dataset.batch(BATCH_SIZE)\n    dataset = dataset.prefetch(buffer_size=AUTOTUNE)\n    return dataset\n\n# --- Load the Datasets ---\nprint(\"\\n--- Loading Datasets ---\")\nmonet_ds = load_dataset(monet_tfrec_files, train=True, shuffle=True)\nphoto_ds = load_dataset(photo_tfrec_files, train=True, shuffle=True)\n\n# Combine datasets for paired iteration during training\nfull_dataset = tf.data.Dataset.zip((monet_ds.repeat(), photo_ds.repeat()))\n\ndef count_data_items(filenames):\n    \"\"\"Counts the total number of records across TFRecord files.\"\"\"\n    n = [int(tf.data.TFRecordDataset(fn).reduce(np.int64(0), lambda x, _: x + 1)) for fn in filenames]\n    return np.sum(n)\n\nn_monet_samples = count_data_items(monet_tfrec_files)\nn_photo_samples = count_data_items(photo_tfrec_files)\nprint(f\"Number of Monet samples (from TFRecords): {n_monet_samples}\")\nprint(f\"Number of Photo samples (from TFRecords): {n_photo_samples}\")\n\n# Determine steps per epoch based on the larger dataset size for CycleGAN\nsteps_per_epoch = 150 # max(n_monet_samples, n_photo_samples) // BATCH_SIZE\nprint(f\"Calculated steps per epoch: {steps_per_epoch}\")\n\n\nprint(f\"\\nMonet dataset element spec: {monet_ds.element_spec}\")\nprint(f\"Photo dataset element spec: {photo_ds.element_spec}\")\nprint(f\"Combined training dataset element spec: {full_dataset.element_spec}\")\n\n# Visualize one preprocessed sample from each dataset\nsample_monet_batch = next(iter(monet_ds))\nsample_photo_batch = next(iter(photo_ds))\n\nplt.figure(figsize=(10, 5))\nplt.subplot(1, 2, 1)\nplt.imshow((sample_monet_batch[0].numpy() + 1.0) * 0.5)\nplt.title(\"Sample Preprocessed Monet\")\nplt.axis('off')\n\nplt.subplot(1, 2, 2)\nplt.imshow((sample_photo_batch[0].numpy() + 1.0) * 0.5)\nplt.title(\"Sample Preprocessed Photo\")\nplt.axis('off')\nplt.show()","metadata":{"_uuid":"a8298541-12e6-4467-8fb3-0e8c685ad73a","_cell_guid":"0f356aed-d1cd-444d-b622-397e48611b02","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-04-29T08:16:30.292896Z","iopub.execute_input":"2025-04-29T08:16:30.293101Z","iopub.status.idle":"2025-04-29T08:16:33.990196Z","execution_failed":"2025-04-29T08:40:19.958Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Model Architecture (CycleGAN)\n\nDefining the building blocks and the main Generator and Discriminator models.\n* **Generator:** Uses a U-Net like architecture with downsampling (encoder) and upsampling (decoder) blocks connected by skip connections. This helps preserve details during translation. **Layer Normalization** is used as a fallback for Instance Normalization.\n* **Discriminator:** Uses a PatchGAN architecture. Instead of classifying the entire image as real or fake, it classifies overlapping patches (e.g., 70x70) of the image. This encourages the generator to produce realistic details across the whole image. It outputs a feature map where each value represents the \"realness\" of a patch. **Layer Normalization** is used here as well.","metadata":{"_uuid":"35d530b0-2f46-48a0-9ad2-a00d7e8b90a1","_cell_guid":"c157ecd8-fdfc-44de-8f8b-8f6eea88c09f","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"# --- Model Architecture Components ---\n\n# Fallback to LayerNormalization\nNormalizationLayer = lambda: layers.LayerNormalization(axis=[1, 2])\n\n\n# --- Building Blocks ---\ndef downsample(filters, size, apply_norm=True, name=None):\n    \"\"\"Downsampling block: Conv2D -> Norm -> LeakyReLU\"\"\"\n    initializer = tf.random_normal_initializer(0., 0.02)\n    result = keras.Sequential(name=name)\n    result.add(layers.Conv2D(filters, size, strides=2, padding='same',\n                             kernel_initializer=initializer, use_bias=False))\n    if apply_norm:\n        result.add(NormalizationLayer())\n    result.add(layers.LeakyReLU())\n    return result\n\ndef upsample(filters, size, apply_dropout=False, name=None):\n    \"\"\"Upsampling block: Conv2DTranspose -> Norm -> Dropout -> ReLU\"\"\"\n    initializer = tf.random_normal_initializer(0., 0.02)\n    result = keras.Sequential(name=name)\n    result.add(layers.Conv2DTranspose(filters, size, strides=2, padding='same',\n                                       kernel_initializer=initializer, use_bias=False))\n    result.add(NormalizationLayer())\n    if apply_dropout:\n        result.add(layers.Dropout(0.5))\n    result.add(layers.ReLU())\n    return result\n\n# --- Generator Model (U-Net based) ---\ndef build_generator(name=\"generator\"):\n    \"\"\"Builds the Generator model (U-Net architecture).\"\"\"\n    inputs = layers.Input(shape=[IMG_HEIGHT, IMG_WIDTH, IMG_CHANNELS])\n\n    # Downsampling path (Encoder)\n    down_stack = [\n        downsample(64, 4, apply_norm=False, name=\"down_1\"), \n        downsample(128, 4, name=\"down_2\"),                 \n        downsample(256, 4, name=\"down_3\"),                 \n        downsample(512, 4, name=\"down_4\"),                 \n        downsample(512, 4, name=\"down_5\"),                 \n        downsample(512, 4, name=\"down_6\"),                 \n        downsample(512, 4, name=\"down_7\"),                 \n        downsample(512, 4, name=\"down_8\"),\n    ]\n\n    # Upsampling path (Decoder)\n    up_stack = [\n        upsample(512, 4, apply_dropout=True, name=\"up_1\"),\n        upsample(512, 4, apply_dropout=True, name=\"up_2\"),\n        upsample(512, 4, apply_dropout=True, name=\"up_3\"),\n        upsample(512, 4, name=\"up_4\"),                    \n        upsample(256, 4, name=\"up_5\"),                    \n        upsample(128, 4, name=\"up_6\"),                    \n        upsample(64, 4, name=\"up_7\"),                     \n    ]\n\n    initializer = tf.random_normal_initializer(0., 0.02)\n    # Final output layer\n    last = layers.Conv2DTranspose(IMG_CHANNELS, 4,\n                                  strides=2,\n                                  padding='same',\n                                  kernel_initializer=initializer,\n                                  activation='tanh', name=\"output_image\")\n\n    x = inputs\n    skips = []\n    # Connect encoder to decoder with skip connections\n    for down in down_stack:\n        x = down(x)\n        skips.append(x)\n\n    skips = reversed(skips[:-1])\n\n    # Build decoder path with skip connections\n    for up, skip in zip(up_stack, skips):\n        x = up(x)\n        x = layers.Concatenate()([x, skip])\n\n    x = last(x)\n\n    return keras.Model(inputs=inputs, outputs=x, name=name)\n\n# --- Discriminator Model (PatchGAN) ---\ndef build_discriminator(name=\"discriminator\"):\n    \"\"\"Builds the Discriminator model (PatchGAN architecture).\"\"\"\n    initializer = tf.random_normal_initializer(0., 0.02)\n\n    inp = layers.Input(shape=[IMG_HEIGHT, IMG_WIDTH, IMG_CHANNELS], name='input_image')\n\n    # PatchGAN Structure\n    x = downsample(64, 4, False, name=\"down_1\")(inp)\n    x = downsample(128, 4, name=\"down_2\")(x)\n    x = downsample(256, 4, name=\"down_3\")(x)\n\n    # Add padding before the next convolution\n    x = layers.ZeroPadding2D(name=\"pad_1\")(x)\n    x = layers.Conv2D(512, 4, strides=1,\n                      kernel_initializer=initializer,\n                      use_bias=False, name=\"conv_1\")(x)\n\n    # Apply normalization after convolution\n    x = NormalizationLayer()(x)\n    x = layers.LeakyReLU(name=\"leaky_relu\")(x)\n\n    # Add padding before the final convolution\n    x = layers.ZeroPadding2D(name=\"pad_2\")(x)\n\n    last = layers.Conv2D(1, 4, strides=1,\n                         kernel_initializer=initializer, name=\"output_patch\")(x)\n\n    return tf.keras.Model(inputs=inp, outputs=last, name=name)\n\n\n# --- Instantiate Models ---\n# Generator G: Photo -> Monet\n# Generator F: Monet -> Photo\n# Discriminator D_Y: Discriminates real/fake Monet\n# Discriminator D_X: Discriminates real/fake Photo\n\nmonet_generator = build_generator(name=\"monet_generator\")       # G\nphoto_generator = build_generator(name=\"photo_generator\")       # F\n\nmonet_discriminator = build_discriminator(name=\"monet_discriminator\") # D_Y\nphoto_discriminator = build_discriminator(name=\"photo_discriminator\") # D_X","metadata":{"_uuid":"05e6be83-ed1b-478b-b75b-f78b7a78ace2","_cell_guid":"c795fc2d-e01b-467e-9c17-8393b5581e01","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-04-29T08:16:33.995306Z","iopub.execute_input":"2025-04-29T08:16:33.995509Z","iopub.status.idle":"2025-04-29T08:16:34.463466Z","execution_failed":"2025-04-29T08:40:19.958Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Loss Functions and Optimizers\n\nDefining the loss functions required for CycleGAN training:\n* **Adversarial Loss:** Measures how well the generators fool the discriminators and how well discriminators distinguish real from fake. Uses Binary Cross-Entropy with logits (`from_logits=True`) because the PatchGAN discriminators don't have a final sigmoid activation.\n* **Cycle Consistency Loss:** Enforces the idea that translating an image to the other domain and back should yield the original image. Uses L1 loss (Mean Absolute Error) as it often produces less blurry results than L2. Weighted by `LAMBDA_CYCLE`.\n* **Identity Loss:** Encourages the generator to preserve the color profile and characteristics of the input image when it's already in the target domain (e.g., feeding a Monet painting to the Monet generator should ideally result in the same painting). Also uses L1 loss. Weighted by `LAMBDA_CYCLE * LAMBDA_IDENTITY`. This loss is optional but often helps stabilize training.\n\nWe use the Adam optimizer for all four models.","metadata":{"_uuid":"2f149871-f6eb-418a-b6b9-50db51833d71","_cell_guid":"560a6fdd-3130-42f8-a2fc-b7945bbaa0cb","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"# --- Loss Functions ---\nloss_obj = tf.keras.losses.BinaryCrossentropy(from_logits=True)\n\ndef discriminator_loss(real, generated):\n    \"\"\"Calculates discriminator loss.\"\"\"\n    real_loss = loss_obj(tf.ones_like(real), real)\n    generated_loss = loss_obj(tf.zeros_like(generated), generated)\n    total_disc_loss = real_loss + generated_loss\n    return total_disc_loss * 0.5\n\ndef generator_loss(generated_output_from_discriminator):\n    \"\"\"Calculates generator adversarial loss.\"\"\"\n    return loss_obj(tf.ones_like(generated_output_from_discriminator), generated_output_from_discriminator)\n\ndef calc_cycle_loss(real_image, cycled_image):\n    \"\"\"Calculates cycle consistency loss (L1).\"\"\"\n    loss1 = tf.reduce_mean(tf.abs(real_image - cycled_image))\n    return LAMBDA_CYCLE * loss1\n\ndef calc_identity_loss(real_image, same_image):\n    \"\"\"Calculates identity loss (L1).\"\"\"\n    loss = tf.reduce_mean(tf.abs(real_image - same_image))\n    return LAMBDA_CYCLE * LAMBDA_IDENTITY * loss\n\n\n# --- Optimizers ---\ngenerator_g_optimizer = tf.keras.optimizers.Adam(2e-4, beta_1=0.5) # Monet Generator (G)\ngenerator_f_optimizer = tf.keras.optimizers.Adam(2e-4, beta_1=0.5) # Photo Generator (F)\n\ndiscriminator_y_optimizer = tf.keras.optimizers.Adam(2e-4, beta_1=0.5) # Monet Discriminator (D_Y)\ndiscriminator_x_optimizer = tf.keras.optimizers.Adam(2e-4, beta_1=0.5) # Photo Discriminator (D_X)","metadata":{"_uuid":"38c5bfab-14f5-461b-8291-2ba459be9183","_cell_guid":"61922292-e96c-46bc-ad4d-48235d68b2b8","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-04-29T08:16:34.467183Z","iopub.execute_input":"2025-04-29T08:16:34.467390Z","iopub.status.idle":"2025-04-29T08:16:34.483286Z","execution_failed":"2025-04-29T08:40:19.958Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Training\n\n**Checkpoints:** Set up TensorFlow checkpoints to save model weights periodically during training. This allows resuming training if interrupted and loading the best weights later.","metadata":{"_uuid":"8fb16684-527f-4e57-9969-88d295d3cb9a","_cell_guid":"8595dc95-7582-46c1-8a14-546efcef596c","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"# --- Checkpoints ---\ncheckpoint_path = \"./checkpoints/train\"\nos.makedirs(checkpoint_path, exist_ok=True)\n\nckpt = tf.train.Checkpoint(monet_generator=monet_generator,\n                           photo_generator=photo_generator,\n                           monet_discriminator=monet_discriminator,\n                           photo_discriminator=photo_discriminator,\n                           generator_g_optimizer=generator_g_optimizer,\n                           generator_f_optimizer=generator_f_optimizer,\n                           discriminator_y_optimizer=discriminator_y_optimizer,\n                           discriminator_x_optimizer=discriminator_x_optimizer)\n\nckpt_manager = tf.train.CheckpointManager(ckpt, checkpoint_path, max_to_keep=5)\n\n# Restore the latest checkpoint if it exists\nif ckpt_manager.latest_checkpoint:\n    ckpt.restore(ckpt_manager.latest_checkpoint).expect_partial()\n    print (f'Latest checkpoint restored from {ckpt_manager.latest_checkpoint}')\nelse:\n    print('Initializing from scratch.')","metadata":{"_uuid":"a45991d1-a47e-4f8f-99e1-661034754e9e","_cell_guid":"359f5c1b-90a1-482f-87bd-197dafa767c5","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-04-29T08:16:34.488023Z","iopub.execute_input":"2025-04-29T08:16:34.488245Z","iopub.status.idle":"2025-04-29T08:16:35.328317Z","execution_failed":"2025-04-29T08:40:19.958Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Training Step\nDefine the core `train_step` function, decorated with `@tf.function` for performance optimization (compiles into a TensorFlow graph). This function performs one step of training for the CycleGAN:\n1.  Takes one batch of real photos (`real_x`) and real Monet paintings (`real_y`).\n2.  Performs forward passes:\n    * Generate fake Monet: `fake_y = G(real_x)`\n    * Cycle back to photo: `cycled_x = F(fake_y)`\n    * Generate fake photo: `fake_x = F(real_y)`\n    * Cycle back to Monet: `cycled_y = G(fake_x)`\n    * Generate identity mappings: `same_y = G(real_y)`, `same_x = F(real_x)`\n3.  Gets discriminator outputs for real and fake images.\n4.  Calculates all loss components (adversarial, cycle, identity) for both generators (G and F) and both discriminators (D_X and D_Y).\n5.  Computes gradients for all four models with respect to their losses using `tf.GradientTape`.\n6.  Applies the gradients using the respective optimizers.\n7.  Returns a dictionary of calculated losses for monitoring.","metadata":{"_uuid":"a4bae165-837e-4b6d-adaa-61705d24d898","_cell_guid":"67db789d-08f4-4e5c-a3e8-42361a468eb6","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"# --- Training Step Definition ---\n@tf.function\ndef train_step(real_x, real_y):\n    \"\"\"Performs a single training step for the CycleGAN.\"\"\"\n    # real_x: real photo, real_y: real monet\n    with tf.GradientTape(persistent=True) as tape:\n        # --- Forward Pass ---\n        # Generator G translates X (Photo) -> Y (Monet)\n        # Generator F translates Y (Monet) -> X (Photo)\n\n        fake_y = monet_generator(real_x, training=True)    # G(X) -> Fake Monet\n        cycled_x = photo_generator(fake_y, training=True)  # F(G(X)) -> Cycled Photo\n\n        fake_x = photo_generator(real_y, training=True)    # F(Y) -> Fake Photo\n        cycled_y = monet_generator(fake_x, training=True)  # G(F(Y)) -> Cycled Monet\n\n        # Identity mapping (input should ideally remain unchanged)\n        same_y = monet_generator(real_y, training=True)    # G(Y) should be ~ Y\n        same_x = photo_generator(real_x, training=True)    # F(X) should be ~ X\n\n        # --- Discriminator Outputs ---\n        disc_real_y = monet_discriminator(real_y, training=True) # D_Y(Y)\n        disc_fake_y = monet_discriminator(fake_y, training=True) # D_Y(G(X))\n\n        disc_real_x = photo_discriminator(real_x, training=True) # D_X(X)\n        disc_fake_x = photo_discriminator(fake_x, training=True) # D_X(F(Y))\n\n        # --- Calculate Losses ---\n        # Generator Adversarial Loss\n        gen_g_adv_loss = generator_loss(disc_fake_y) # G wants D_Y to think fake_y is real\n        gen_f_adv_loss = generator_loss(disc_fake_x) # F wants D_X to think fake_x is real\n\n        # Cycle Consistency Loss (Forward and Backward)\n        total_cycle_loss = calc_cycle_loss(real_x, cycled_x) + calc_cycle_loss(real_y, cycled_y)\n\n        # Identity Loss\n        total_identity_loss = calc_identity_loss(real_y, same_y) + calc_identity_loss(real_x, same_x)\n\n        # Total Generator Loss\n        total_gen_g_loss = gen_g_adv_loss + total_cycle_loss + total_identity_loss\n        total_gen_f_loss = gen_f_adv_loss + total_cycle_loss + total_identity_loss\n\n        # Discriminator Loss\n        disc_y_loss = discriminator_loss(disc_real_y, disc_fake_y) # D_Y loss\n        disc_x_loss = discriminator_loss(disc_real_x, disc_fake_x) # D_X loss\n\n    # --- Calculate Gradients ---\n    # Generator gradients\n    generator_g_gradients = tape.gradient(total_gen_g_loss, monet_generator.trainable_variables)\n    generator_f_gradients = tape.gradient(total_gen_f_loss, photo_generator.trainable_variables)\n\n    # Discriminator gradients\n    discriminator_y_gradients = tape.gradient(disc_y_loss, monet_discriminator.trainable_variables)\n    discriminator_x_gradients = tape.gradient(disc_x_loss, photo_discriminator.trainable_variables)\n\n    # --- Apply Gradients ---\n    generator_g_optimizer.apply_gradients(zip(generator_g_gradients, monet_generator.trainable_variables))\n    generator_f_optimizer.apply_gradients(zip(generator_f_gradients, photo_generator.trainable_variables))\n\n    discriminator_y_optimizer.apply_gradients(zip(discriminator_y_gradients, monet_discriminator.trainable_variables))\n    discriminator_x_optimizer.apply_gradients(zip(discriminator_x_gradients, photo_discriminator.trainable_variables))\n\n    # Return losses for monitoring\n    return {\n        \"gen_G_loss\": total_gen_g_loss, \"gen_F_loss\": total_gen_f_loss,\n        \"disc_Y_loss\": disc_y_loss, \"disc_X_loss\": disc_x_loss,\n        \"cycle_loss\": total_cycle_loss, \"identity_loss\": total_identity_loss,\n        \"gen_G_adv_loss\": gen_g_adv_loss, \"gen_F_adv_loss\": gen_f_adv_loss\n    }","metadata":{"_uuid":"cd2cddbe-6abb-43e4-bdc7-c41a9dd5dd70","_cell_guid":"26e86983-ae58-4eb4-944d-0f879c96fe88","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-04-29T08:16:35.329722Z","iopub.execute_input":"2025-04-29T08:16:35.329998Z","iopub.status.idle":"2025-04-29T08:16:35.338191Z","execution_failed":"2025-04-29T08:40:19.958Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Training Loop\n- Implement the main loop to iterate through epochs and steps, calling `train_step` and monitoring progress. Also includes a function to generate and display sample transformed images during training.","metadata":{"_uuid":"26ea93ff-f169-46c7-9856-344bc7f0e74a","_cell_guid":"1366d752-6da6-4d86-946f-40600fad84bd","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"# --- Function to Generate and Display Images During Training ---\nsample_photo_batch_vis = next(iter(load_dataset(photo_tfrec_files, train=False, shuffle=False).take(1)))\n\ndef generate_images(model, test_input, epoch, step):\n    \"\"\"Generates and displays a sample transformed image.\"\"\"\n    prediction = model(test_input, training=False)[0].numpy()\n\n    plt.figure(figsize=(10, 5))\n\n    display_input = (test_input[0].numpy() + 1.0) * 0.5\n    display_prediction = (prediction + 1.0) * 0.5\n\n    title = ['Input Photo', 'Generated Monet Style']\n\n    plt.subplot(1, 2, 1)\n    plt.title(title[0])\n    plt.imshow(np.clip(display_input, 0, 1))\n    plt.axis('off')\n\n    plt.subplot(1, 2, 2)\n    plt.title(title[1])\n    plt.imshow(np.clip(display_prediction, 0, 1))\n    plt.axis('off')\n\n    plt.suptitle(f\"Epoch: {epoch+1}, Step: {step}\", fontsize=14)\n    plt.show()\n\n\n# --- Training Loop ---\nprint(\"\\n--- Starting Training ---\")\nprint(f\"Total Epochs: {EPOCHS}\")\nprint(f\"Steps per Epoch: {steps_per_epoch}\")\n\n# Frequency for printing losses and generating sample images\nprint_freq = 100\nsample_freq = 500\n\nfor epoch in range(EPOCHS):\n    start_time = time.time()\n    print(f\"\\nEpoch {epoch + 1}/{EPOCHS}\")\n\n    step_pbar = tqdm(total=steps_per_epoch, desc=f\"Epoch {epoch+1} Steps\")\n\n    # Initialize epoch losses dictionary using tf.keras.metrics.Mean\n    epoch_losses = {k: tf.keras.metrics.Mean() for k in [\n        \"gen_G_loss\", \"gen_F_loss\", \"disc_Y_loss\", \"disc_X_loss\",\n        \"cycle_loss\", \"identity_loss\", \"gen_G_adv_loss\", \"gen_F_adv_loss\"\n    ]}\n\n    # Iterate through the zipped dataset for the calculated number of steps\n    for step, (real_monet, real_photo) in enumerate(full_dataset.take(steps_per_epoch)):\n        if step >= steps_per_epoch: break\n\n        # Perform one training step\n        losses = train_step(real_photo, real_monet)\n\n        # Update epoch average losses\n        for k, v in losses.items():\n            if k in epoch_losses:\n                epoch_losses[k].update_state(v)\n            else:\n                print(f\"Warning: Loss key '{k}' not found in epoch_losses dictionary.\")\n\n\n        # Print progress periodically\n        if (step + 1) % print_freq == 0:\n            # Format loss string using the result() method of the metrics\n            loss_str = \", \".join([f\"{k}={v.result():.4f}\" for k, v in epoch_losses.items()])\n            print(f'> Step {step+1}/{steps_per_epoch}: {loss_str}')\n\n        # Generate and show a sample image periodically\n        if (step + 1) % sample_freq == 0:\n             print(f\"\\nGenerating sample image at step {step+1}...\")\n             generate_images(monet_generator, sample_photo_batch_vis, epoch, step+1)\n\n        step_pbar.update(1)\n\n    step_pbar.close()\n\n    # Print average losses for the completed epoch\n    print(f\"Epoch {epoch+1} Average Losses: \", end=\"\")\n    avg_loss_str = \", \".join([f\"{k}={v.result():.4f}\" for k, v in epoch_losses.items()])\n    print(avg_loss_str)\n\n    # Save checkpoint\n    if (epoch + 1) % 5 == 0:\n        ckpt_save_path = ckpt_manager.save()\n        print(f'Saving checkpoint for epoch {epoch+1} at {ckpt_save_path}')\n    # Save the last epoch as well\n    elif epoch == EPOCHS - 1:\n         ckpt_save_path = ckpt_manager.save()\n         print(f'Saving final checkpoint for epoch {epoch+1} at {ckpt_save_path}')\n\n\n    end_time = time.time()\n    print(f'Time taken for epoch {epoch + 1}: {end_time - start_time:.2f} sec')\n\n    # Reset metrics at the end of each epoch\n    for k in epoch_losses:\n        epoch_losses[k].reset_state()\n","metadata":{"_uuid":"7186c0cc-b546-40f0-bab0-aeb65b33dd60","_cell_guid":"b1b18655-1bf9-4d23-9fee-26dfd6b4a46d","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-04-29T08:21:25.988451Z","iopub.execute_input":"2025-04-29T08:21:25.988791Z","iopub.status.idle":"2025-04-29T08:25:05.249701Z","shell.execute_reply.started":"2025-04-29T08:21:25.988769Z","shell.execute_reply":"2025-04-29T08:25:05.248616Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Image Generation and Submission File Creation\n\nUse the trained Monet generator (`monet_generator`) to transform the photos in the `photo_tfrec` dataset into Monet style. Save these generated images as JPG files and package them into a `images.zip` file for submission.\n\n**Important:** Kaggle Kernels have an output file limit (often 500). To submit 7,000+ images, we must write them directly into a zip archive or save them to a temporary directory within `/kaggle/working/` and then zip that directory. We will save them to `/kaggle/working/images/` and then zip this folder.","metadata":{"_uuid":"889a9eac-40b1-4f01-bb4a-be7a862d5591","_cell_guid":"08691f87-345d-4880-bc70-aa7891324289","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"# --- Generate Monet-Style Images for Submission ---\nprint(\"\\n--- Generating Submission Images ---\")\n\n# Ensure the output directory exists\nos.makedirs(OUTPUT_DIR, exist_ok=True)\n\n# Create a dataset for the photos to be transformed (use non-augmented preprocessing)\nsubmission_photo_ds = load_dataset(photo_tfrec_files, train=False, shuffle=False)\n\n# Counter for generated images\nimg_count = 0\nmax_images = 10000 # Competition requires 7k-10k images\n\n# Use tqdm for progress bar\n# Estimate total number of images to generate (can be slightly off if batch size > 1)\ntotal_gen_images = n_photo_samples * BATCH_SIZE\ngeneration_pbar = tqdm(total=min(total_gen_images, max_images), desc=\"Generating Images\")\n\n# Iterate through the photo dataset batches\nfor photo_batch in submission_photo_ds:\n    # Generate Monet-style images using the trained generator\n    generated_batch = monet_generator(photo_batch, training=False)\n\n    # Process each image in the batch (batch size is likely 1 here)\n    for i in range(generated_batch.shape[0]):\n        if img_count >= max_images:\n            break\n\n        # Get single generated image\n        generated_img = generated_batch[i].numpy()\n\n        # Denormalize: Convert from [-1, 1] back to [0, 255] uint8\n        generated_img = (generated_img * 127.5 + 127.5).astype(np.uint8)\n\n        # Create PIL Image object\n        img_pil = Image.fromarray(generated_img)\n\n        # Save the image as JPG in the output directory\n        output_filepath = os.path.join(OUTPUT_DIR, f\"image_{img_count+1}.jpg\")\n        try:\n            img_pil.save(output_filepath, format='JPEG')\n            img_count += 1\n            generation_pbar.update(1) # Update progress for each image SAVED\n        except Exception as e:\n            print(f\"Error saving image {img_count+1}: {e}\")\n\n\n    if img_count >= max_images:\n        print(f\"\\nReached maximum image limit ({max_images}). Stopping generation.\")\n        break\n\ngeneration_pbar.close()\nprint(f\"\\nGenerated {img_count} images in {OUTPUT_DIR}\")\n\n# --- Create Submission Zip File ---\nif img_count >= 7000: # Only create zip if enough images were generated\n    print(f\"\\n--- Creating Submission Zip File: {ZIP_FILENAME} ---\")\n    try:\n        with zipfile.ZipFile(ZIP_FILENAME, 'w', zipfile.ZIP_DEFLATED) as zf:\n            # Walk through the output directory and add files to the zip\n            for root, _, files in os.walk(OUTPUT_DIR):\n                for file in tqdm(files, desc=\"Zipping Images\"):\n                    if file.lower().endswith('.jpg'):\n                        file_path = os.path.join(root, file)\n                        # ** FIX: Use OUTPUT_DIR as the base for relpath **\n                        arcname = os.path.relpath(file_path, OUTPUT_DIR)\n                        zf.write(file_path, arcname=arcname)\n        print(f\"Successfully created {ZIP_FILENAME}\")\n\n        # Optional: Verify zip file contents (list first few files)\n        # with zipfile.ZipFile(ZIP_FILENAME, 'r') as zf:\n        #     print(\"\\nSample files in zip:\")\n        #     for i, name in enumerate(zf.namelist()):\n        #         if i >= 5: break\n        #         print(f\"- {name}\")\n\n    except Exception as e:\n        print(f\"Error creating zip file: {e}\")\nelse:\n    print(f\"\\nSkipping zip file creation. Generated {img_count} images (minimum 7000 required).\")\n","metadata":{"_uuid":"5a095ab6-a780-4483-8e19-3b2e485817b2","_cell_guid":"bf15b37f-7671-4491-bfbf-5dfeaf7f4fce","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-04-29T08:31:09.961547Z","iopub.execute_input":"2025-04-29T08:31:09.961873Z","iopub.status.idle":"2025-04-29T08:37:59.952850Z","shell.execute_reply.started":"2025-04-29T08:31:09.961849Z","shell.execute_reply":"2025-04-29T08:37:59.952077Z"}},"outputs":[],"execution_count":null}]}