{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":21755,"databundleVersionId":1475600,"sourceType":"competition"}],"dockerImageVersionId":31194,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## Deep Learning mini project Week 5\n###  Problem Description & Generative Models\n\nThe challenge is to **generate images in the unique style of Claude Monet** using **Generative Adversarial Networks (GANs)**.\n\n* **GAN Structure:** Consists of a **Generator** (creates Monet-style images) and a **Discriminator** (distinguishes real Monet paintings from generated images). They are trained adversarially.\n* **Output Goal:** Build a GAN to generate **7,000 to 10,000** Monet-style images.\n* **Evaluation Metric:** **MiFID** (Memorization-informed Fréchet Inception Distance). Lower MiFID indicates better quality generation.\n\n---\n\n###  Data Description: Size, Dimension, & Structure\n\nThe dataset contains two domains: Monet paintings and general photos, available in JPEG and TFRecord formats.\n\n* **Monet Paintings (Training Data):**\n    * **Size:** 300 images.\n    * **Dimension/Structure:** $256 \\times 256$ pixels (RGB color).\n    * **Purpose:** Train the model on the target style.\n\n* **Photos (Source/Transformation Data):**\n    * **Size:** 7,028 images.\n    * **Dimension/Structure:** $256 \\times 256$ pixels (RGB color).\n    * **Purpose:** Photos to be transformed into Monet-style images (or used for scratch generation).\n\n* **File Formats:** **.jpg** and **.tfrec** (TFRecord format is recommended).\n* **Image Dimension:** All images are $256 \\times 256 \\times 3$ (Height x Width x Color Channels).","metadata":{}},{"cell_type":"markdown","source":"### EDA and Preprocessing Plan\n\nThe primary objective of EDA and Cleaning for this GAN challenge is to confirm data integrity, understand the source and target domains, and prepare the image tensors for the adversarial network training (Generator and Discriminator).\n\n---\n\n### 1. **EDA Plan: Understanding the Domains**\n\n| Step | Description | Necessary Plot/Output | Purpose for Next Steps |\n| :--- | :--- | :--- | :--- |\n| **Data Integrity Check** | Verify file counts across the provided folders (`monet_tfrec`: 300, `photo_tfrec`: 7028). Check for corrupted images (e.g., failed to decode TFRecord/JPEG). | Print file counts and any error logs. | Ensure the model consumes the full, clean dataset. |\n| **Domain Visualization** | **Randomly sample 5x5 images** from both the **Monet** and **Photo** domains.  | **Grid Plot** of sample images (Monet vs. Photo) with normalized pixel values. | Visually assess the \"style gap\" and confirm image dimensions ($256 \\times 256$). |\n| **Color Distribution** | Calculate and plot **RGB channel histograms** for both domains. | **Histogram Plots** (Monet RGB vs. Photo RGB). | Determine differences in color palette and intensity, informing model design and loss functions (e.g., L1 loss). |\n| **Pixel Range Check** | Check min/max pixel values before normalization. | Simple printout (e.g., Min: 0, Max: 255). | Confirm standard 8-bit image range. |\n\n---\n\n### 2. **Data Cleaning and Preprocessing**\n\nFor GANs, data cleaning is primarily tensor preprocessing:\n\n1.  **Tensor Decoding & Resizing:**\n    * Utilize **TFRecords** for faster loading, decoding the raw bytes into a $256 \\times 256 \\times 3$ tensor.\n    * (No resizing is generally needed as all images are already $256 \\times 256$.)\n\n2.  **Normalization (Crucial for GANs):**\n    * Rescale all pixel values from the initial $[0, 255]$ range to the standard GAN input range of **$[-1, 1]$**. This is necessary for stable training, especially with the use of activation functions like `tanh` in the Generator's output layer.","metadata":{}},{"cell_type":"code","source":"## 🚀 Code Implementation for EDA and Data Preprocessing Plan (Simplified)\n\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\nimport numpy as np\n\n# --- Configuration ---\n# Data path as requested for the competition\nDATASET_PATH = \"/kaggle/input/gan-getting-started\"\nMONET_TFREC = f\"{DATASET_PATH}/monet_tfrec\"\nPHOTO_TFREC = f\"{DATASET_PATH}/photo_tfrec\"\nIMAGE_SIZE = [256, 256]\n\n# --- 1. Utility Functions ---\n\ndef decode_image(image):\n    \"\"\"Decodes the image from TFRecord, standardizes, and converts to float32.\"\"\"\n    image = tf.image.decode_jpeg(image, channels=3)\n    # Convert to float32\n    image = tf.cast(image, tf.float32)\n    # Scale to [-1, 1] (Normalization for GANs)\n    image = (image / 127.5) - 1\n    return image\n\ndef read_tfrecord(example):\n    \"\"\"Reads a single TFRecord example.\"\"\"\n    tfrecord_format = {\n        \"image_name\": tf.io.FixedLenFeature([], tf.string),\n        \"image\": tf.io.FixedLenFeature([], tf.string),\n        \"target\": tf.io.FixedLenFeature([], tf.string)\n    }\n    example = tf.io.parse_single_example(example, tfrecord_format)\n    image = decode_image(example['image'])\n    return image\n\ndef load_dataset(filenames):\n    \"\"\"Loads and processes the dataset from a list of TFRecord files.\"\"\"\n    dataset = tf.data.TFRecordDataset(filenames)\n    dataset = dataset.map(read_tfrecord, num_parallel_calls=tf.data.AUTOTUNE)\n    return dataset\n\n# --- 2. Data Loading and Integrity Check ---\n\nprint(\"--- 1. Data Integrity and Loading ---\")\n\n# Get the list of all TFRecord files\nmonet_filenames = tf.io.gfile.glob(f\"{MONET_TFREC}/*.tfrec\")\nphoto_filenames = tf.io.gfile.glob(f\"{PHOTO_TFREC}/*.tfrec\")\n\n# Load data\n# We use batch(1) only for easier visualization, not for general loading\nmonet_ds = load_dataset(monet_filenames).batch(1)\nphoto_ds = load_dataset(photo_filenames).batch(1)\n\n# Print file counts (Data Integrity Check)\nprint(f\"Total Monet TFRecords: {len(monet_filenames)}\") # Expected: ~300\nprint(f\"Total Photo TFRecords: {len(photo_filenames)}\") # Expected: ~7028\n\n# --- 3. EDA: Domain Visualization ---\n\nprint(\"\\n--- 2. EDA: Domain Visualization ---\")\n\ndef display_samples(dataset, title):\n    \"\"\"Displays 5x5 image samples.\"\"\"\n    plt.figure(figsize=(10, 10))\n    plt.suptitle(title, fontsize=16)\n    \n    # Take the first 25 samples\n    for i, image in enumerate(dataset.unbatch().take(25)):\n        plt.subplot(5, 5, i + 1)\n        # Convert from [-1, 1] back to [0, 1] for display\n        display_img = (image.numpy() * 0.5) + 0.5 \n        plt.imshow(display_img)\n        plt.axis('off')\n    \n    plt.tight_layout(rect=[0, 0, 1, 0.96])\n    plt.show()\n\n# Display Monet image samples\ndisplay_samples(monet_ds, \"Sample Monet Paintings (Target Domain)\")\n\n# Display Photo image samples\ndisplay_samples(photo_ds, \"Sample Photos (Source Domain)\")\n\n\n# --- 4. Preprocessing/Normalization Check ---\n\nprint(\"\\n--- 3. Preprocessing Check ---\")\n\n# Check one image sample after normalization\nfor monet_sample in monet_ds.take(1):\n    monet_tensor = monet_sample.numpy()\n    \n    print(f\"Monet Sample Shape: {monet_tensor.shape}\")\n    \n    # Check the value range after normalization\n    min_val = np.min(monet_tensor)\n    max_val = np.max(monet_tensor)\n    \n    print(f\"Min Pixel Value (Normalized): {min_val:.4f}\") # Expected: near -1\n    print(f\"Max Pixel Value (Normalized): {max_val:.4f}\") # Expected: near 1\n    \n    if min_val >= -1.0 and max_val <= 1.0:\n        print(\"Normalization success: Pixel range is within [-1, 1].\")\n    else:\n        print(\"Normalization warning: Pixel range is outside [-1, 1].\")\n\nprint(\"\\n✅ Setup Complete: Data is loaded, normalized to [-1, 1], and ready for model construction.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-23T13:07:07.203807Z","iopub.execute_input":"2025-11-23T13:07:07.204411Z","iopub.status.idle":"2025-11-23T13:07:27.370035Z","shell.execute_reply.started":"2025-11-23T13:07:07.204387Z","shell.execute_reply":"2025-11-23T13:07:27.369216Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Model Architecture & Training Strategy\n\nThe next phase focuses on implementing the **CycleGAN** architecture, optimizing it for fast, stable training on the available TPU hardware.\n\n### 1. **Architecture Rationale (CycleGAN)**\n\n| Rationale | Detail |\n| :--- | :--- |\n| **Unpaired Data** | The core necessity is handling **unpaired data** (Monet vs. Photo), which CycleGAN is designed for. |\n| **Cycle Consistency Loss** | Enforces structural integrity during style transfer (Photo $\\to$ Monet $\\to$ Photo), preventing mode collapse and maintaining image content. |\n| **Generator Structure** | **ResNet-based Generator** (e.g., U-Net like structure with 9 residual blocks) for effective high-level feature extraction and transformation. |\n| **Discriminator Structure** | **PatchGAN Discriminator** (outputs a matrix of decisions rather than a single scalar) to focus on local consistency (style details). |\n\n### 2. **Training & Hyperparameter Strategy**\n\n| Feature | Strategy | Fine-Tuning Goal (Reduce Wait Time) |\n| :--- | :--- | :--- |\n| **Batch Size** | **$BATCH\\_SIZE = 1$ (Standard for CycleGAN)**. Due to the high memory usage of $256 \\times 256$ images and the Cycle-Loss, a batch size of 1 is typical. | N/A (Fixed by CycleGAN nature). |\n| **Learning Rate** | Start with $LR=2e-4$. Use a **Linear Decay** schedule: Keep $LR$ constant for the first $100$ epochs, then linearly decay to zero over the next $100$ epochs. | Gradual decay ensures fine-tuning stability and prevents abrupt weight changes in later stages. |\n| **Epochs** | Target $200$ epochs total. | Initial $20-50$ epochs for rapid convergence test before full training run. |\n| **Loss Weights** | $\\lambda_{cycle} = 10.0$ (High weight for cycle consistency). $\\lambda_{identity} = 0.5 \\times \\lambda_{cycle}$. | Prioritize cycle consistency early to ensure structure is maintained, which speeds up visual quality improvement. |\n| **Image Buffer** | Use an **Image Pool/History Buffer** of size 50 for Discriminator training. | Feeds older generated images to the Discriminator, stabilizing its training and preventing it from oscillating based solely on the current batch's generated images. |\n\n### 3. **Analysis & Plotting Plan**\n\n* **Mid-Training Plot:** Generate and plot 5 transformed images (Photo $\\to$ Monet) **every 5 epochs** to visually track style acquisition and structural preservation.\n* **Final MiFID Evaluation:** Use the final generated $7,000-10,000$ images for evaluation. Analyze the final MiFID score against the expected benchmark.","metadata":{}},{"cell_type":"markdown","source":"Training setup","metadata":{}},{"cell_type":"markdown","source":"Augmentation function","metadata":{}},{"cell_type":"code","source":"import tensorflow as tf\nfrom tensorflow.keras import layers, models, optimizers, losses, callbacks\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport os\nimport zipfile\nimport io\n\n# --- GPU CONFIGURATION ---\nphysical_devices = tf.config.list_physical_devices('GPU')\nif len(physical_devices) > 0:\n    try:\n        # Attempt to enable memory growth to avoid hogging all VRAM immediately\n        tf.config.experimental.set_memory_growth(physical_devices[0], True)\n        print(f\"✅ GPU found and Memory Growth enabled: {physical_devices[0]}\")\n    except RuntimeError:\n        # If GPU was initialized previously, ignore the error\n        print(f\"✅ GPU found (Runtime already initialized): {physical_devices[0]}\")\nelse:\n    print(\"⚠️ No GPU found, code will run very slowly on CPU!\")\n\n# ==============================================================================\n# PART 0: DATA PREPARATION (DATA LOADING & AUGMENTATION)\n# ==============================================================================\n\n# Path configuration (Kaggle Environment)\nDATASET_PATH = \"/kaggle/input/gan-getting-started\"\nMONET_TFREC = os.path.join(DATASET_PATH, \"monet_tfrec\")\nPHOTO_TFREC = os.path.join(DATASET_PATH, \"photo_tfrec\")\nBATCH_SIZE = 1 # GPU usually handles batch=1 well with 256x256 images\n\ndef decode_image(image):\n    \"\"\"Decodes image from TFRecord and normalizes to [-1, 1]\"\"\"\n    image = tf.image.decode_jpeg(image, channels=3)\n    image = tf.cast(image, tf.float32)\n    image = (image / 127.5) - 1 # Normalize [-1, 1]\n    image = tf.reshape(image, [256, 256, 3])\n    return image\n\ndef read_tfrecord(example):\n    tfrecord_format = {\n        \"image\": tf.io.FixedLenFeature([], tf.string),\n    }\n    example = tf.io.parse_single_example(example, tfrecord_format)\n    image = decode_image(example['image'])\n    return image\n\ndef data_augment(image):\n    \"\"\"\n    Data Augmentation (Crucial for the small Monet dataset):\n    - Random flip left/right\n    - (Optional slight rotation if needed)\n    \"\"\"\n    image = tf.image.random_flip_left_right(image)\n    return image\n\ndef get_gan_dataset(filenames, augment=False, repeat=True, shuffle=True, batch_size=1):\n    dataset = tf.data.TFRecordDataset(filenames)\n    \n    # SỬA 1: Thay tf.data.AUTOTUNE bằng 2 (hoặc 4) để tránh kẹt luồng\n    dataset = dataset.map(read_tfrecord, num_parallel_calls=2) \n    \n    if augment:\n        dataset = dataset.map(data_augment, num_parallel_calls=2)\n        \n    if repeat:\n        dataset = dataset.repeat()\n        \n    if shuffle:\n        dataset = dataset.shuffle(2048)\n        \n    dataset = dataset.batch(batch_size, drop_remainder=True)\n    \n    # SỬA 2: Xóa hoặc comment dòng cache() đi để tránh tràn RAM\n    # dataset = dataset.cache()  <-- COMMENT DÒNG NÀY LẠI\n    \n    dataset = dataset.prefetch(2) # Prefetch ít thôi cho nhẹ\n    return dataset\n\n# Load actual data\nmonet_filenames = tf.io.gfile.glob(str(MONET_TFREC + \"/*.tfrec\"))\nphoto_filenames = tf.io.gfile.glob(str(PHOTO_TFREC + \"/*.tfrec\"))\n\n# Monet: Needs Augmentation due to small data size (300 images)\nmonet_ds = get_gan_dataset(monet_filenames, augment=True, batch_size=BATCH_SIZE)\n# Photo: Less Augmentation needed due to large data size (7000 images)\nphoto_ds = get_gan_dataset(photo_filenames, augment=False, batch_size=BATCH_SIZE)\n\nprint(f\"✅ Data loaded: {len(monet_filenames)} Monet files, {len(photo_filenames)} Photo files\")\n\n# ==============================================================================\n# PART 1: MODEL BUILDING BLOCKS\n# ==============================================================================\n\nclass InstanceNormalization(layers.Layer):\n    def __init__(self, epsilon=1e-5):\n        super(InstanceNormalization, self).__init__()\n        self.epsilon = epsilon\n\n    def build(self, input_shape):\n        self.scale = self.add_weight(\n            name='scale',\n            shape=input_shape[-1:],\n            initializer=tf.random_normal_initializer(1., 0.02),\n            trainable=True)\n        self.offset = self.add_weight(\n            name='offset',\n            shape=input_shape[-1:],\n            initializer='zeros',\n            trainable=True)\n\n    def call(self, x):\n        mean, variance = tf.nn.moments(x, axes=[1, 2], keepdims=True)\n        inv = tf.math.rsqrt(variance + self.epsilon)\n        normalized = (x - mean) * inv\n        return self.scale * normalized + self.offset\n\ndef downsample(filters, size, apply_instancenorm=True):\n    initializer = tf.random_normal_initializer(0., 0.02)\n    result = tf.keras.Sequential()\n    result.add(layers.Conv2D(filters, size, strides=2, padding='same',\n                             kernel_initializer=initializer, use_bias=False))\n    if apply_instancenorm:\n        result.add(InstanceNormalization())\n    result.add(layers.LeakyReLU())\n    return result\n\ndef upsample(filters, size, apply_dropout=False):\n    initializer = tf.random_normal_initializer(0., 0.02)\n    result = tf.keras.Sequential()\n    result.add(layers.Conv2DTranspose(filters, size, strides=2, padding='same',\n                                      kernel_initializer=initializer, use_bias=False))\n    result.add(InstanceNormalization())\n    if apply_dropout:\n        result.add(layers.Dropout(0.5))\n    result.add(layers.ReLU())\n    return result\n\ndef residual_block(input_layer, filters):\n    initializer = tf.random_normal_initializer(0., 0.02)\n    \n    x = layers.Conv2D(filters, 3, strides=1, padding='same', \n                      kernel_initializer=initializer, use_bias=False)(input_layer)\n    x = InstanceNormalization()(x)\n    x = layers.ReLU()(x)\n    \n    x = layers.Conv2D(filters, 3, strides=1, padding='same', \n                      kernel_initializer=initializer, use_bias=False)(x)\n    x = InstanceNormalization()(x)\n    \n    return layers.Add()([input_layer, x])\n\n# ==============================================================================\n# PART 2: NETWORK ARCHITECTURE\n# ==============================================================================\n\ndef Generator():\n    inputs = layers.Input(shape=[256, 256, 3])\n    initializer = tf.random_normal_initializer(0., 0.02)\n\n    # 1. Encoding (Downsampling)\n    x = layers.Conv2D(64, 7, strides=1, padding='same', \n                      kernel_initializer=initializer, use_bias=False)(inputs)\n    x = InstanceNormalization()(x)\n    x = layers.ReLU()(x)\n    \n    x = downsample(128, 3)(x) \n    x = downsample(256, 3)(x) \n    \n    # 2. Transformation (9 Residual Blocks)\n    for _ in range(9):\n        x = residual_block(x, 256)\n        \n    # 3. Decoding (Upsampling)\n    x = upsample(128, 3)(x) \n    x = upsample(64, 3)(x)  \n    \n    # 4. Output Layer (Tanh activation -> [-1, 1])\n    # --- FIX: Use layers.Activation instead of direct tf.tanh ---\n    x = layers.Conv2D(3, 7, strides=1, padding='same', kernel_initializer=initializer)(x)\n    outputs = layers.Activation('tanh')(x)\n    \n    return models.Model(inputs=inputs, outputs=outputs)\n\ndef Discriminator():\n    initializer = tf.random_normal_initializer(0., 0.02)\n    inputs = layers.Input(shape=[256, 256, 3], name='input_image')\n\n    x = inputs\n    x = downsample(64, 4, apply_instancenorm=False)(x) \n    x = downsample(128, 4)(x) \n    x = downsample(256, 4)(x) \n\n    x = layers.ZeroPadding2D()(x)\n    x = layers.Conv2D(512, 4, strides=1, kernel_initializer=initializer, use_bias=False)(x)\n    x = InstanceNormalization()(x)\n    x = layers.LeakyReLU()(x)\n\n    x = layers.ZeroPadding2D()(x)\n    last = layers.Conv2D(1, 4, strides=1, kernel_initializer=initializer)(x) \n\n    return models.Model(inputs=inputs, outputs=last)\n\n# Initialize models\nwith tf.device('/device:GPU:0'):\n    monet_generator = Generator()\n    photo_generator = Generator()\n    monet_discriminator = Discriminator()\n    photo_discriminator = Discriminator()\n\nprint(\"✅ Architecture initialized\")\n\n# ==============================================================================\n# PART 3: CYCLEGAN CLASS & TRAINING LOGIC\n# ==============================================================================\n\nclass CycleGan(tf.keras.Model):\n    def __init__(self, monet_generator, photo_generator, discriminator_monet, discriminator_photo):\n        super(CycleGan, self).__init__()\n        self.m_gen = monet_generator\n        self.p_gen = photo_generator\n        self.d_m = discriminator_monet\n        self.d_p = discriminator_photo\n        \n    def compile(self, m_gen_optimizer, p_gen_optimizer, d_m_optimizer, d_p_optimizer, gen_loss_fn, disc_loss_fn, cycle_loss_fn, identity_loss_fn):\n        super(CycleGan, self).compile()\n        self.m_gen_optimizer = m_gen_optimizer\n        self.p_gen_optimizer = p_gen_optimizer\n        self.d_m_optimizer = d_m_optimizer\n        self.d_p_optimizer = d_p_optimizer\n        self.gen_loss_fn = gen_loss_fn\n        self.disc_loss_fn = disc_loss_fn\n        self.cycle_loss_fn = cycle_loss_fn\n        self.identity_loss_fn = identity_loss_fn\n        \n    def train_step(self, batch_data):\n        real_monet, real_photo = batch_data\n        \n        with tf.GradientTape(persistent=True) as tape:\n            # --- Forward Pass ---\n            fake_monet = self.m_gen(real_photo, training=True)\n            cycled_photo = self.p_gen(fake_monet, training=True)\n\n            fake_photo = self.p_gen(real_monet, training=True)\n            cycled_monet = self.m_gen(fake_photo, training=True)\n\n            same_monet = self.m_gen(real_monet, training=True)\n            same_photo = self.p_gen(real_photo, training=True)\n\n            disc_real_monet = self.d_m(real_monet, training=True)\n            disc_fake_monet = self.d_m(fake_monet, training=True)\n            disc_real_photo = self.d_p(real_photo, training=True)\n            disc_fake_photo = self.d_p(fake_photo, training=True)\n\n            # --- Calculate Losses ---\n            monet_gen_loss = self.gen_loss_fn(disc_fake_monet)\n            photo_gen_loss = self.gen_loss_fn(disc_fake_photo)\n            \n            total_cycle_loss = self.cycle_loss_fn(real_monet, cycled_monet) + self.cycle_loss_fn(real_photo, cycled_photo)\n            \n            total_identity_loss = self.identity_loss_fn(real_monet, same_monet) + self.identity_loss_fn(real_photo, same_photo)\n            \n            total_monet_gen_loss = monet_gen_loss + (total_cycle_loss * 10) + (total_identity_loss * 5)\n            total_photo_gen_loss = photo_gen_loss + (total_cycle_loss * 10) + (total_identity_loss * 5)\n\n            monet_disc_loss = self.disc_loss_fn(disc_real_monet, disc_fake_monet)\n            photo_disc_loss = self.disc_loss_fn(disc_real_photo, disc_fake_photo)\n\n        # --- Calculate Gradients ---\n        monet_gen_gradients = tape.gradient(total_monet_gen_loss, self.m_gen.trainable_variables)\n        photo_gen_gradients = tape.gradient(total_photo_gen_loss, self.p_gen.trainable_variables)\n        monet_disc_gradients = tape.gradient(monet_disc_loss, self.d_m.trainable_variables)\n        photo_disc_gradients = tape.gradient(photo_disc_loss, self.d_p.trainable_variables)\n\n        # --- Apply Gradients ---\n        self.m_gen_optimizer.apply_gradients(zip(monet_gen_gradients, self.m_gen.trainable_variables))\n        self.p_gen_optimizer.apply_gradients(zip(photo_gen_gradients, self.p_gen.trainable_variables))\n        self.d_m_optimizer.apply_gradients(zip(monet_disc_gradients, self.d_m.trainable_variables))\n        self.d_p_optimizer.apply_gradients(zip(photo_disc_gradients, self.d_p.trainable_variables))\n        \n        return {\n            \"m_gen_loss\": total_monet_gen_loss,\n            \"p_gen_loss\": total_photo_gen_loss,\n            \"m_disc_loss\": monet_disc_loss,\n            \"p_disc_loss\": photo_disc_loss\n        }\n\n# ==============================================================================\n# PART 4: LOSS FUNCTIONS & COMPILATION\n# ==============================================================================\n\nwith tf.device('/device:GPU:0'):\n    adv_loss_fn = losses.MeanSquaredError() \n\n    def generator_loss(generated):\n        return adv_loss_fn(tf.ones_like(generated), generated)\n\n    def discriminator_loss(real, generated):\n        real_loss = adv_loss_fn(tf.ones_like(real), real)\n        generated_loss = adv_loss_fn(tf.zeros_like(generated), generated)\n        return (real_loss + generated_loss) * 0.5\n\n    def calc_cycle_loss(real_image, cycled_image):\n        return tf.reduce_mean(tf.abs(real_image - cycled_image))\n\n    def identity_loss(real_image, same_image):\n        return tf.reduce_mean(tf.abs(real_image - same_image))\n\n    # --- FIX: Use tf.cond instead of python if/else ---\n    class LinearDecay(tf.keras.optimizers.schedules.LearningRateSchedule):\n        def __init__(self, initial_learning_rate, total_steps, warmup_steps):\n            self.initial_learning_rate = initial_learning_rate\n            self.total_steps = total_steps\n            self.warmup_steps = warmup_steps\n\n        def __call__(self, step):\n            step = tf.cast(step, tf.float32)\n            warmup_steps = tf.cast(self.warmup_steps, tf.float32)\n            total_steps = tf.cast(self.total_steps, tf.float32)\n            initial_learning_rate = tf.cast(self.initial_learning_rate, tf.float32)\n\n            return tf.cond(\n                step < warmup_steps,\n                lambda: initial_learning_rate,\n                lambda: initial_learning_rate * (1 - (step - warmup_steps) / (total_steps - warmup_steps))\n            )\n    \n    # --- TRAINING IMAGE COUNT CONFIGURATION (IMPORTANT) ---\n    TRAIN_SIZE = 1000  # <--- ADJUST THIS (e.g., 1500 for quick test, 7028 for full run)\n    EPOCHS = 20        # Number of epochs\n    \n    steps_per_epoch = TRAIN_SIZE // BATCH_SIZE\n    total_steps = EPOCHS * steps_per_epoch\n    warmup_steps = (EPOCHS // 2) * steps_per_epoch\n    \n    print(f\"⚙️ Config: Training with {TRAIN_SIZE} images/epoch for {EPOCHS} epochs.\")\n    \n    lr_schedule = LinearDecay(2e-4, total_steps, warmup_steps)\n\n    # Optimizers\n    m_gen_opt = optimizers.Adam(lr_schedule, beta_1=0.5)\n    p_gen_opt = optimizers.Adam(lr_schedule, beta_1=0.5)\n    d_m_opt = optimizers.Adam(lr_schedule, beta_1=0.5)\n    d_p_opt = optimizers.Adam(lr_schedule, beta_1=0.5)\n\n    cycle_gan_model = CycleGan(monet_generator, photo_generator, monet_discriminator, photo_discriminator)\n\n    cycle_gan_model.compile(\n        m_gen_optimizer = m_gen_opt,\n        p_gen_optimizer = p_gen_opt,\n        d_m_optimizer = d_m_opt,\n        d_p_optimizer = d_p_opt,\n        gen_loss_fn = generator_loss,\n        disc_loss_fn = discriminator_loss,\n        cycle_loss_fn = calc_cycle_loss,\n        identity_loss_fn = identity_loss\n    )\n\nprint(\"✅ Model compiled successfully.\")\n\n# ==============================================================================\n# PART 5: CALLBACK MONITORING\n# ==============================================================================\n\nclass GANMonitor(callbacks.Callback):\n    def __init__(self, num_img=4):\n        self.num_img = num_img\n\n    def on_epoch_end(self, epoch, logs=None):\n        if (epoch + 1) % 5 == 0 or epoch == 0: \n            print(f\"\\n--- Epoch {epoch+1}: Prediction Results ---\")\n            _, ax = plt.subplots(self.num_img, 2, figsize=(10, 10))\n            for i, img in enumerate(photo_ds.take(self.num_img)):\n                prediction = self.model.m_gen(img)[0].numpy()\n                prediction = (prediction * 127.5 + 127.5).astype(np.uint8)\n                img = (img[0] * 127.5 + 127.5).numpy().astype(np.uint8)\n\n                ax[i, 0].imshow(img)\n                ax[i, 0].set_title(\"Input Photo\")\n                ax[i, 0].axis(\"off\")\n                ax[i, 1].imshow(prediction)\n                ax[i, 1].set_title(\"Monet Style\")\n                ax[i, 1].axis(\"off\")\n            plt.show()\n            plt.close()\n\n# ==============================================================================\n# PART 6: TRAINING EXECUTION\n# ==============================================================================\n\nprint(\"🚀 Starting Training...\")\nfull_dataset = tf.data.Dataset.zip((monet_ds, photo_ds))\n\nhistory = cycle_gan_model.fit(\n    full_dataset,\n    epochs=EPOCHS,\n    steps_per_epoch=steps_per_epoch,\n    callbacks=[GANMonitor()],\n    verbose=2\n)\n\n# ==============================================================================\n# PART 7: PLOTTING METRICS & SAVING SUBMISSION (FIXED FOR 500 FILE LIMIT)\n# ==============================================================================\n\ndef plot_history(history):\n    plt.figure(figsize=(15, 6))\n    \n    plt.subplot(1, 2, 1)\n    plt.plot(history.history['m_gen_loss'], label='Monet Gen Loss', color='blue')\n    plt.plot(history.history['m_disc_loss'], label='Monet Disc Loss', color='orange')\n    plt.title('Monet Generator vs Discriminator')\n    plt.legend()\n    \n    plt.subplot(1, 2, 2)\n    plt.plot(history.history['p_gen_loss'], label='Photo Gen Loss', color='green')\n    plt.plot(history.history['p_disc_loss'], label='Photo Disc Loss', color='red')\n    plt.title('Photo Generator vs Discriminator')\n    plt.legend()\n    plt.show()\n\nprint(\"📊 Plotting Loss Charts...\")\nplot_history(history)\n\n# --- Save Submission (Optimized way, avoiding writing individual files to disk) ---\nprint(\"💾 Generating submission file images.zip (Full 7028 images)...\")\n\n# Use ZipFile to write directly from RAM to avoid Kaggle file limit\nwith zipfile.ZipFile('images.zip', 'w') as zf:\n    # Loop through all 7028 images\n    for i, img in enumerate(photo_ds.take(7028)): \n        # 1. Predict\n        prediction = monet_generator(img, training=False)[0].numpy()\n        prediction = (prediction * 127.5 + 127.5).astype(np.uint8)\n        \n        # 2. Convert to image format (PIL)\n        im = tf.keras.utils.array_to_img(prediction)\n        \n        # 3. Save to buffer (RAM) instead of disk\n        img_byte_arr = io.BytesIO()\n        im.save(img_byte_arr, format='JPEG')\n        img_byte_arr = img_byte_arr.getvalue()\n        \n        # 4. Write directly to zip file\n        # Filename inside zip will be 1.jpg, 2.jpg...\n        zf.writestr(f\"{i+1}.jpg\", img_byte_arr)\n        \n        if (i + 1) % 1000 == 0:\n            print(f\"   Compressed {i+1} images...\")\n\nprint(f\"✅ Done! File images.zip is ready for submission.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-23T13:07:27.371526Z","iopub.execute_input":"2025-11-23T13:07:27.371736Z"}},"outputs":[],"execution_count":null}]}