{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":21755,"databundleVersionId":1475600,"sourceType":"competition"}],"dockerImageVersionId":31089,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Introducción // Introduction\n\nEl objetivo de este proyecto es aprender sobre la utilización de las redes GAN y el potencial que tienen en la creación de imagenes, hasta el punto de convertirnos en el futuro falsificador de Monet.\n\nPara ello desarrollare un notebook completo acerca de como se desarrolla todo el proyecto, desde como preparar los datos hasta el entrenamiento y prediccion del modelo.\n\n---------------------------------------------------------------------------------------------\n\nThe goal of this project is to learn about the use of GANs and their potential for image creation — up to the point of becoming, in the future, a forger of Monet.\n\nTo that end, I will develop a complete notebook that covers the entire project: from data preparation to model training and prediction.\n\nWould you like this phrasing to be more formal (technical paper style) or more casual/playful for a portfolio/Kaggle notebook?","metadata":{}},{"cell_type":"markdown","source":"# Primeros Pasos y preparación del entorno // First Steps and Environment Setup\n\nEl objetivo del proyecto es conseguir aplicar el estilo de Monet a diferentes fotos mediante la utilización de una arquitectura basada en redes GAN. Para ello, emplearemos el dataset \"monet_tfrec\".\n\n---------------------------------------------------------------------------------------------\n\nThe goal of this project is to apply Monet’s style to different photos using a GAN-based architecture. For this purpose, we will use the monet_tfrec dataset.","metadata":{}},{"cell_type":"code","source":"import tensorflow as tf\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport os\nimport glob\nfrom kaggle_datasets import KaggleDatasets\n\n\nMONET_TFREC_PATH = '/kaggle/input/gan-getting-started/monet_tfrec/*.tfrec'\nPHOTO_TFREC_PATH = '/kaggle/input/gan-getting-started/photo_tfrec/*.tfrec'\n\n#Valor especial que TensorFlow entiende como “ajústalo automáticamente”\nAUTOTUNE = tf.data.experimental.AUTOTUNE\n#En el caso de: map(..., num_parallel_calls=AUTOTUNE)\n#TensorFlow decide cuántos hilos/cores usar en paralelo para aplicar la \n#transformación (map) de forma óptima.","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-24T10:35:28.849903Z","iopub.execute_input":"2025-09-24T10:35:28.850526Z","iopub.status.idle":"2025-09-24T10:35:43.822829Z","shell.execute_reply.started":"2025-09-24T10:35:28.850504Z","shell.execute_reply":"2025-09-24T10:35:43.822248Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"A la hora de cargar los datos para el entrenamiento del modelo, nos interesa mantener separados las imagenes reales de monet de nuestras imagenes, de esta manera no habrá confusiónes a la hora de mandar los datos al modelo.\n\nComo podemos observar dentro de los datos que se nos proporcionan por parte de la competición, podemos diferenciar entre 2 tipos de formato de datos: JPEG y TFREC.\n* JPEG: son las imagenes en crudo, tienen la ventaja de que se trata de un formato estandar que es facil de abrir en cualquier dispositivo. El inconveniente es que cuando se tiene muchas imagenes con las que trabajar, leerlas una por una desde el disco es más lento y genera mucho I/O.\n* TFRed: permite agrupar muchas imagenes y etiquetas dentro de un solo archivo. Permite la lectura en paralelo más rápida, ocupan menos espacio, son mas efecientes en TPU/GPU y al tratarse de un formato optimizado para TensorFlow (tecnología con la que desarrollaremos el proyecto) permite usarlas directamente.\n\n---------------------------------------------------------------------------------------------\n\nWhen loading the data for training the model, it’s important to keep Monet’s real paintings separate from our own images. This way, there’s no confusion when passing the data to the model.\n\nAs we can see from the competition’s provided dataset, we can distinguish between two data formats: JPEG and TFRecord.\n* JPEG: these are the raw images. The advantage is that it’s a standard format, easy to open on any device. The downside is that when working with a large number of images, reading them one by one from disk is slower and generates a lot of I/O.\n* TFRecord: this format allows grouping many images and labels into a single file. It enables faster parallel reading, takes up less space, is more efficient on TPUs/GPUs, and since it’s optimized for TensorFlow (the framework we’ll use to develop this project), it can be used directly.","metadata":{}},{"cell_type":"code","source":"#Obtiene la ruta en Google Cloud Storage (GCS) donde Kaggle guarda el dataset\n# Las TPUs no leen desde el disco local, solo desde GCS\nGCS_PATH = KaggleDatasets().get_gcs_path()\n\n#Busca todos los archivos .tfrec dentro de las carpetas \nMONET_FILENAMES = tf.io.gfile.glob(str(GCS_PATH + '/monet_tfrec/*.tfrec'))\n#Lista de TFRecords que contienen cuadros de Monet.\nprint('Monet TFRecord Files:', len(MONET_FILENAMES))\n\nPHOTO_FILENAMES = tf.io.gfile.glob(str(GCS_PATH + '/photo_tfrec/*.tfrec'))\n#Lista de TFRecords que contienen fotos reales.\nprint('Photo TFRecord Files:', len(PHOTO_FILENAMES))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-24T10:35:43.824135Z","iopub.execute_input":"2025-09-24T10:35:43.824601Z","iopub.status.idle":"2025-09-24T10:35:46.538328Z","shell.execute_reply.started":"2025-09-24T10:35:43.824581Z","shell.execute_reply":"2025-09-24T10:35:46.537604Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Entendiendo las imagenes // Understanding the Images\n\nAl trabajar con imágenes es necesario comprender ciertas características de las mismas para poder entender la forma en la que trabajamos.\n\nEn principio todas las imagenes han sido recogidas en forma 256x256 RGB, esto quiere decir que, cada imagen tendra un ancho y alto de 256px y 3 canales que harán referencia a los colores primarios que formaran la imagen (rojo, verde y azul).\n\nAdemás, es necesario escalar los valores de cada pixel en la imagen para que se encuentren dentro del rango [-1, 1]. Esta estrategia es muy común a la hora de trabajar con imágenes porque:\n* Si trabajamos con valores entre 0-255 los gradientes se vuelven más grandes/desvalanceados y el optimizador necesita tasas de aprendizaje muy bajos para no colapsar.\n* Si normalizamos los valores entre [-1,1], los valores se centran entorno a 0 ayudando a la convergencia y ciertas activaciones (ejemplo: Tahn) espera inputs en este rango.\n\nPor lo tanto el objetivo de estas dos funciones es crear dos funciones para:\n* Función decode_image(image): recibe los bytes de una imagen y devuelve una imagen lista para el entrenamiento de dimensiones 256x256x3 y valores entre [-1, 1].\n* Funcion read_tfrecord(example): lee una entrada del archivo tfrec y se estructura en el formato \"tfrecord_format\", le pasamos esa entrada en el nuevo formato a la función anterior y se retorna unicamente el valor de la imagen decodificada.\n\n---------------------------------------------------------------------------------------------\n\nWhen working with images, it’s important to understand certain characteristics in order to know how to properly process them.\n\nAll images in this dataset have been collected in 256x256 RGB format. This means each image has a width and height of 256px, and 3 channels corresponding to the primary colors that make up the image (red, green, and blue).\n\nIn addition, it’s necessary to scale the pixel values of each image into the range [-1, 1]. This strategy is very common when working with images because:\n* If we work with values in the 0–255 range, the gradients become larger/imbalanced and the optimizer requires very low learning rates to avoid collapsing.\n* If we normalize the values to [-1, 1], they are centered around 0, which helps convergence, and certain activations (e.g., tanh) expect inputs in this range.\n\nTherefore, the purpose here is to create two functions:\n* decode_image(image): receives the bytes of an image and returns a ready-to-train image of dimensions 256x256x3 with values scaled to [-1, 1].\n* read_tfrecord(example): reads an entry from the TFRecord file and structures it according to the tfrecord_format; the formatted entry is passed to the previous function, and only the decoded image is returned.","metadata":{}},{"cell_type":"code","source":"IMAGE_SIZE = [256, 256]\n\ndef decode_image(image):\n    #Convierte los bytes JPEG en un tensor de forma [alto, ancho, 3]\n    image = tf.image.decode_jpeg(image, channels=3)\n    #Convierte a float32 y normaliza al rango [-1, 1]\n    image = (tf.cast(image, tf.float32) / 127.5) - 1\n    #Fuerza la forma a [256, 256, 3] para evitar problemas\n    image = tf.reshape(image, [*IMAGE_SIZE, 3])\n    return image\n\ndef read_tfrecord(example):\n    #Se define un nuevo formato:\n    tfrecord_format = {\n        #Cadena de texto que define el nombre de la imagen\n        \"image_name\": tf.io.FixedLenFeature([], tf.string),\n        #Valores en bytes que forman la imagen en formato JPEG comprimido\n        \"image\": tf.io.FixedLenFeature([], tf.string),\n        \"target\": tf.io.FixedLenFeature([], tf.string)\n    }\n    #Lee una entrada del archivo y lo convierte en el formato \"tfrecord_format\"\n    example = tf.io.parse_single_example(example, tfrecord_format)\n    #Coge los bytes JPEG del campo \"image\" y los pasa a la función anterior\n    image = decode_image(example['image'])\n    return image","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-24T10:35:46.539028Z","iopub.execute_input":"2025-09-24T10:35:46.539281Z","iopub.status.idle":"2025-09-24T10:35:46.545537Z","shell.execute_reply.started":"2025-09-24T10:35:46.539254Z","shell.execute_reply":"2025-09-24T10:35:46.544707Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Creamos una función que aplica a todos los archivos del dataset la función read_tfrecord, encargada de decodificar cada ejemplo. Además, si se desea, se pueden mezclar (shuffle) las imágenes para evitar que el modelo aprenda siempre en el mismo orden y así mejorar su capacidad de generalización.\n\nPosteriormente, las imágenes se agrupan en lotes (batches). De este modo, cada elemento que se pasa al modelo está formado por tantas imágenes como hayamos definido en el parámetro batch_size.\nPor ejemplo, si batch_size = 5, la entrada al modelo tendrá dimensiones: (5, 256, 256, 3).\n\n---------------------------------------------------------------------------------------------\n\nWe create a function that applies the read_tfrecord function to all the files in the dataset, which is responsible for decoding each example. In addition, if desired, the images can be shuffled to prevent the model from always learning in the same order, thereby improving its generalization ability.\n\nAfter that, the images are grouped into batches. In this way, each element passed to the model consists of as many images as defined by the batch_size parameter.\nFor example, if batch_size = 5, the input to the model will have the following dimensions: (5, 256, 256, 3).","metadata":{}},{"cell_type":"code","source":"def load_dataset(filenames, labeled=True, shuffle=False, batch_size=1, buffer_size=2048):\n    dataset = tf.data.TFRecordDataset(filenames)\n    dataset = dataset.map(read_tfrecord, num_parallel_calls=AUTOTUNE)\n\n    if shuffle:\n        dataset = dataset.shuffle(buffer_size)\n\n    dataset = dataset.batch(batch_size)\n    return dataset","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-24T10:35:46.546420Z","iopub.execute_input":"2025-09-24T10:35:46.546694Z","iopub.status.idle":"2025-09-24T10:35:46.562095Z","shell.execute_reply.started":"2025-09-24T10:35:46.546672Z","shell.execute_reply":"2025-09-24T10:35:46.561398Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Procedemos a cargar todos los archivos, diferenciando entre las imagenes de monet y el resto de imagenes.\n\nAdemás, también hacemos una prueba para comprobar si las imagenes se han cargado correctamente.\n\n---------------------------------------------------------------------------------------------\n\nWe proceed to load all the files, differentiating between Monet’s images and the rest of the images.\n\nIn addition, we run a quick test to check whether the images have been loaded correctly.","metadata":{}},{"cell_type":"code","source":"monet_filenames = tf.io.gfile.glob(MONET_TFREC_PATH)\nphoto_filenames = tf.io.gfile.glob(PHOTO_TFREC_PATH)\n\nmonet_dataset_eda = load_dataset(monet_filenames, batch_size=5, shuffle=False)\nphoto_dataset_eda = load_dataset(photo_filenames, batch_size=5, shuffle=False)\n\nprint(f'Monet TFRecords: {len(monet_filenames)}')\nprint(f'Photo TFRecords: {len(photo_filenames)}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-24T10:35:46.563707Z","iopub.execute_input":"2025-09-24T10:35:46.564146Z","iopub.status.idle":"2025-09-24T10:35:47.593063Z","shell.execute_reply.started":"2025-09-24T10:35:46.564129Z","shell.execute_reply":"2025-09-24T10:35:47.592227Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def show_samples(dataset, title, num_images=5):\n    plt.figure(figsize=(15, 3))\n    for i, image_batch in enumerate(dataset.take(1)):\n        for j in range(num_images):\n            image = image_batch[j]\n            image = (image + 1) / 2  # Rescale to [0, 1] for display\n            plt.subplot(1, num_images, j + 1)\n            plt.imshow(image)\n            plt.axis('off')\n    plt.suptitle(title)\n    plt.show()\n\nshow_samples(monet_dataset_eda, \"Sample Monet Images\")\nshow_samples(photo_dataset_eda, \"Sample Photo Images\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-24T10:35:47.593883Z","iopub.execute_input":"2025-09-24T10:35:47.594139Z","iopub.status.idle":"2025-09-24T10:35:48.368613Z","shell.execute_reply.started":"2025-09-24T10:35:47.594120Z","shell.execute_reply":"2025-09-24T10:35:48.367793Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Creación de la red CycleGAN y la diferencia con una GAN // Building the CycleGAN and Its Difference from a GAN\n\nUna Generative Adversarial Network (GAN) es un tipo de red neuronal compuesta por dos modelos que compiten entre sí:\n\n1. Generador (G): recibe como entrada ruido aleatorio, su objetivo es crear imágenes falsas que parezcan reales.\n2. Discriminador (D): recibe como entrada imágenes (reales o generadas), su objetivo es distinguir si la imagen es real o falsa.\n\nDurante el entrenamiento: el generador aprende a engañar al discriminador y el discriminador aprende a detectar falsificaciones. Este “juego” competitivo hace que, con el tiempo, el generador produzca imágenes cada vez más realistas.\n\nUna CycleGAN es un tipo especial de GAN diseñada para traducir imágenes entre dos dominios distintos sin necesidad de pares de imágenes correspondientes. Está compuesta por dos generadores y dos discriminadores que trabajan de forma conjunta:\n\n1. Generadores (G₁ y G₂):\n    * G₁ (Photo → Monet): transforma imágenes de un dominio (fotos) al otro (Monet).\n    * G₂ (Monet → Photo): transforma imágenes de Monet al dominio de fotos.\n    * Cada generador aprende a engañar al discriminador del dominio de destino, produciendo imágenes que parezcan auténticas en ese estilo.\n2.\tDiscriminadores (D₁ y D₂):\n    * D₁: distingue si una foto es real o generada por G₂.\n    * D₂: distingue si un Monet es real o generado por G₁.\n    * Cada discriminador se especializa en las características de su propio dominio.\n3. Pérdidas adicionales:\n\t* Cycle-consistency loss: asegura que una imagen traducida ida y vuelta (foto → Monet → foto) conserve el contenido original.\n    * Identity loss: asegura que las imágenes que ya están en el estilo correcto no sean alteradas.\n\nDurante el entrenamiento, los generadores y discriminadores compiten y cooperan: los generadores aprenden a producir imágenes que engañen a los discriminadores, mientras que los discriminadores aprenden a detectar falsificaciones. Gracias a las pérdidas de ciclo e identidad, el modelo puede aprender transformaciones bidireccionales realistas entre dos dominios distintos sin necesidad de pares de entrenamiento.\n\n---------------------------------------------------------------------------------------------\n\nA Generative Adversarial Network (GAN) is a type of neural network composed of two models that compete with each other:\n1. Generator (G): takes random noise as input, aiming to create fake images that look real.\n2. Discriminator (D): takes images (real or generated) as input, aiming to distinguish whether the image is real or fake.\n\nDuring training: the generator learns to fool the discriminator, and the discriminator learns to detect fakes. This competitive “game” gradually pushes the generator to produce increasingly realistic images.\n\nA CycleGAN is a special type of GAN designed to translate images between two different domains without the need for paired training data. It consists of two generators and two discriminators working together:\n1. Generators (G₁ and G₂):\n\t* G₁ (Photo → Monet): transforms images from the photo domain into the Monet domain.\n\t* G₂ (Monet → Photo): transforms Monet images into the photo domain.\n\t* Each generator learns to fool the discriminator of the target domain, producing images that appear authentic in that style.\n2. Discriminators (D₁ and D₂):\n\t* D₁: distinguishes whether a photo is real or generated by G₂.\n\t* D₂: distinguishes whether a Monet painting is real or generated by G₁.\n\t* Each discriminator specializes in the features of its respective domain.\n3. Additional Losses:\n\t* Cycle-consistency loss: ensures that an image translated back and forth (photo → Monet → photo) retains its original content.\n\t* Identity loss: ensures that images already in the correct style remain unchanged.\n\nDuring training, the generators and discriminators both compete and cooperate: the generators learn to produce images that fool the discriminators, while the discriminators learn to detect fakes. Thanks to cycle and identity losses, the model can learn realistic bidirectional transformations between two domains without requiring paired training examples.","metadata":{}},{"cell_type":"markdown","source":"Para la toma de contacto con el modelo, lo primero es cargar de nuevo los datos con un tamaño de batch igual a 1. De esta manera, las imágenes se procesan una a una, lo que facilita observar cómo se comporta el modelo en cada caso.\n\nAdemás, activamos la opción shuffle=True para que el modelo no aprenda en función del orden de las imágenes, sino que reciba ejemplos en un orden aleatorio. Esto es importante porque evita sesgos durante el entrenamiento y ayuda a que el modelo generalice mejor en lugar de memorizar patrones basados únicamente en la secuencia de los datos.\n\n---------------------------------------------------------------------------------------------\n\nFor an initial run with the model, the first step is to reload the data with a batch size of 1. This way, the images are processed one by one, making it easier to observe how the model behaves in each case.\n\nIn addition, we enable the option shuffle=True so that the model does not learn based on the order of the images, but instead receives examples in a random order. This is important because it prevents bias during training and helps the model generalize better rather than memorizing patterns solely based on the sequence of the data.","metadata":{}},{"cell_type":"code","source":"BATCH_SIZE = 1\nmonet_dataset_train = load_dataset(monet_filenames, batch_size=BATCH_SIZE, shuffle=True)\nphoto_dataset_train = load_dataset(photo_filenames, batch_size=BATCH_SIZE, shuffle=True)\n\nfor monet_batch in monet_dataset_train.take(1):\n    print(\"Monet batch shape:\", monet_batch.shape)\n\nfor photo_batch in photo_dataset_train.take(1):\n    print(\"Photo batch shape:\", photo_batch.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-24T10:35:48.369339Z","iopub.execute_input":"2025-09-24T10:35:48.369533Z","iopub.status.idle":"2025-09-24T10:35:50.669482Z","shell.execute_reply.started":"2025-09-24T10:35:48.369518Z","shell.execute_reply":"2025-09-24T10:35:50.668726Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Generador // Generator\n\nPara la creación del generador se sigue la siguiente estrategia:\n\n1. Generador Downsampling (ENCODER):\n    Este punto pretende reducir la imagen para quedarnos con la información importante, para ello utiliza tres capas Conv2D con stride=2 que se desplazan a lo largo de la matriz de píxeles que conforman la imagen, reduciendo su tamaño pero aumento su profundidad de canales (los filtros).\n   Con esto el modelo pretende extraer características como bordes, texturas, sombras, ... y se queda con una representación comprimida de la imagen.\n   \n2. Bloques residuales (Residual Blocks)\n    A traves de esta parte se pretende modificar la información con bloques especíales que cambien los detalles (colores, texturas, estilo, ...). El objetivo de estos bloques es no borrar la información original, sino ajustarla hasta conseguir que las imagenes se parezcan a un cuadro de monet.\n\n3. Generador Upsampling (DECODER)\n    Llegando al final del proceso está la reconstrucción de la imagen al tamaño original, pero manteniendo el nuevo estilo que se ha aprendido con los bloques residuales.\n\n4. Salida con activación tanh\n    Se trata de resturar la imagen hasta la forma original, con 3 canales de color (RGB) y con los valores de la imagen normalizados al rango de colores de [-1, 1] (esto lo lleva a cabo el uso de la función Tanh).\n\n---------------------------------------------------------------------------------------------\n\n1. Downsampling Generator (Encoder)\nThis step reduces the image while retaining the most important information. It uses three Conv2D layers with stride=2, which move across the pixel grid of the image, reducing its spatial size while increasing its channel depth (filters).\nWith this, the model extracts key features such as edges, textures, and shadows, keeping a compressed representation of the image.\n\n2. Residual Blocks\nAt this stage, the information is modified using special blocks that adjust details like colors, textures, and style.\nThe purpose of these blocks is not to erase the original information, but rather to refine it until the images resemble a Monet painting.\n\n3. Upsampling Generator (Decoder)\nFinally, the image is reconstructed back to its original size, but now preserving the new style learned through the residual blocks.\n\n4. Output with Tanh Activation\nThe last layer restores the image to its original form, with 3 color channels (RGB) and values normalized to the range [-1, 1]. This normalization is handled by the Tanh activation function.\n","metadata":{}},{"cell_type":"code","source":"from tensorflow.keras import layers\n\n#Creación de los bloques residuales\ndef resnet_block(x, filters):\n    y = layers.Conv2D(filters, (3, 3), padding='same')(x)\n    y = layers.GroupNormalization(groups=1, axis=-1)(y)\n    y = layers.Activation('relu')(y)\n    y = layers.Conv2D(filters, (3, 3), padding='same')(y)\n    y = layers.GroupNormalization(groups=1, axis=-1)(y)\n    # El motivo por el que se suma la capa X e Y es porque permite que el bloque aprenda de una \n    # transformación suave en lugar de r\n    return layers.add([x, y])\n\ndef build_generator(image_shape=(256, 256, 3), n_res_blocks=9):\n    inputs = layers.Input(shape=image_shape)\n\n    # Encoder\n    x = layers.Conv2D(64, (7, 7), padding='same')(inputs)\n    # Empleo de \"GroupNormalization\" en lugar de \"BatchNormalization\" porque es más \n    # estable en entrenamientos pequeños\n    x = layers.GroupNormalization(groups=1, axis=-1)(x)\n    # Función de activación\n    x = layers.Activation('relu')(x)\n\n    x = layers.Conv2D(128, (3, 3), strides=2, padding='same')(x)\n    x = layers.GroupNormalization(groups=1, axis=-1)(x)\n    x = layers.Activation('relu')(x)\n\n    x = layers.Conv2D(256, (3, 3), strides=2, padding='same')(x)\n    x = layers.GroupNormalization(groups=1, axis=-1)(x)\n    x = layers.Activation('relu')(x)\n\n    # ResNet Blocks\n    for _ in range(n_res_blocks):\n        x = resnet_block(x, 256)\n\n    # Decoder\n    x = layers.Conv2DTranspose(128, (3, 3), strides=2, padding='same')(x)\n    x = layers.GroupNormalization(groups=1, axis=-1)(x)\n    x = layers.Activation('relu')(x)\n\n    x = layers.Conv2DTranspose(64, (3, 3), strides=2, padding='same')(x)\n    x = layers.GroupNormalization(groups=1, axis=-1)(x)\n    x = layers.Activation('relu')(x)\n\n    x = layers.Conv2D(3, (7, 7), padding='same', activation='tanh')(x)\n\n    return tf.keras.Model(inputs, x)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-24T10:35:50.670396Z","iopub.execute_input":"2025-09-24T10:35:50.670631Z","iopub.status.idle":"2025-09-24T10:35:50.733069Z","shell.execute_reply.started":"2025-09-24T10:35:50.670613Z","shell.execute_reply":"2025-09-24T10:35:50.732478Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Discriminador // Discriminator\n\nSu trabajo es analizar una imagen y decir si parece auténtica o inventada.\n\nPara eso se usa un diseño llamado PatchGAN:\n* En lugar de decidir si la imagen completa es real o falsa, la divide en pequeños parches (como cuadraditos).\n* Así se fija en detalles locales, como trazos de pincel, texturas o colores, que son más difíciles de falsificar.\n\nCabe destacar que el final, el discriminador retornara una rejilla de valores en lugar de un único valor que represente si la imagen es o no verdadera. En esta rejilla esta compuesto por valores que representan si ese \"Parche\" parece o no un caso real de Monet.\n\n--------------------------------------------------------------------------------------------\n\nIts job is to analyze an image and decide whether it looks authentic or generated.\n\nTo achieve this, we use a design called PatchGAN:\n* Instead of deciding if the whole image is real or fake, it splits the image into small patches (like little squares).\n* This allows the model to focus on local details such as brush strokes, textures, or color variations, which are much harder to fake.\n\nIt’s important to note that, at the end, the discriminator does not return a single value representing whether the image is real.\nInstead, it outputs a grid of values, where each value corresponds to a patch and indicates whether that patch looks like a real Monet painting or not.","metadata":{}},{"cell_type":"code","source":"def build_discriminator(image_shape=(256, 256, 3)):\n    inputs = layers.Input(shape=image_shape)\n\n    #Aplica filtros para encontrar bordes, formas simples y colores\n    x = layers.Conv2D(64, (4, 4), strides=2, padding='same')(inputs)\n    #LeakyReLU ayuda a que la red no se quede “bloqueada” con valores negativos\n    x = layers.LeakyReLU(0.2)(x)\n\n    #La red ahora ve patrones más complejos (como líneas o texturas)\n    x = layers.Conv2D(128, (4, 4), strides=2, padding='same')(x)\n    #GroupNormalization mantiene los valores de la red estables, para que aprenda mejor\n    x = layers.GroupNormalization(groups=1, axis=-1)(x)\n    x = layers.LeakyReLU(0.2)(x)\n\n    x = layers.Conv2D(256, (4, 4), strides=2, padding='same')(x)\n    x = layers.GroupNormalization(groups=1, axis=-1)(x)\n    x = layers.LeakyReLU(0.2)(x)\n\n    x = layers.Conv2D(512, (4, 4), strides=1, padding='same')(x)\n    x = layers.GroupNormalization(groups=1, axis=-1)(x)\n    x = layers.LeakyReLU(0.2)(x)\n\n    #En lugar de devolver un número que represente si la imagen es cierta (1) o falsa (0), se \n    #devuelve una rejilla de valores (parches), donde cada número indicara si ese parche parece\n    #real o falso. Finalmente se convinan para dar el veredicto final.\n    x = layers.Conv2D(1, (4, 4), strides=1, padding='same')(x)\n\n    return tf.keras.Model(inputs, x)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-24T10:35:50.733682Z","iopub.execute_input":"2025-09-24T10:35:50.733940Z","iopub.status.idle":"2025-09-24T10:35:50.739740Z","shell.execute_reply.started":"2025-09-24T10:35:50.733923Z","shell.execute_reply":"2025-09-24T10:35:50.739118Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Construimos el modelo para que vaya pasando por los generadores y los discriminadores.\n\n---------------------------------------------------------------------------------------------\n\nWe build the model so that the data passes sequentially through the generators and discriminators.","metadata":{}},{"cell_type":"code","source":"generator_pm = build_generator()  # Photo → Monet\ngenerator_mp = build_generator()  # Monet → Photo\n\ndiscriminator_p = build_discriminator()  # Discriminates Photos\ndiscriminator_m = build_discriminator()  # Discriminates Monet","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-24T10:35:50.740717Z","iopub.execute_input":"2025-09-24T10:35:50.741057Z","iopub.status.idle":"2025-09-24T10:35:52.430499Z","shell.execute_reply.started":"2025-09-24T10:35:50.741022Z","shell.execute_reply":"2025-09-24T10:35:52.429652Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Funciones de perdida // Loss Functions\n\nEn un modelo CycleGAN, no basta con usar la pérdida adversarial clásica de una GAN. Para conseguir que las imágenes mantengan su contenido y solo cambien de estilo, se introducen varias funciones de pérdida.\n\n1.\tAdversarial Loss (pérdida adversarial)\n    * El generador intenta crear imágenes falsas que el discriminador clasifique como reales.\n    * El discriminador, en cambio, aprende a distinguir entre imágenes reales y falsas.\n2.\tCycle-consistency Loss (pérdida de ciclo)\n\t* Si convertimos una foto en un Monet falso y luego lo transformamos de nuevo a foto, el resultado debería parecerse mucho a la foto original.\n\t* Esto obliga al modelo a mantener la estructura y contenido de la imagen al cambiar de estilo.\n3.\tIdentity Loss (pérdida de identidad)\n\t* Si al generador de “foto → Monet” le damos ya un Monet real, debería devolver prácticamente el mismo Monet.\n\t* Así se evita que el modelo distorsione imágenes que ya están en el estilo correcto.\n\nEstas pérdidas se combinan con pesos de importancia (LAMBDA_CYCLE, LAMBDA_ID) para equilibrar cuánto influye cada una en el entrenamiento.\n\nPara entrenar los generadores y discriminadores, se utiliza el optimizador Adam con parámetros estándar para GANs:\n* learning rate = 2e-4\n* beta_1 = 0.5 (esto ayuda a estabilizar el entrenamiento evitando oscilaciones bruscas).\n\nSe definen optimizadores independientes para:\n* Generador foto → Monet\n* Generador Monet → foto\n* Discriminador de fotos\n* Discriminador de Monet\n\nDe esta manera, cada parte de la red puede aprender a su propio ritmo, pero bajo las mismas reglas de optimización.\n\n---------------------------------------------------------------------------------------------\n\nIn a CycleGAN model, the classic adversarial loss of a GAN is not enough. To ensure that images retain their content while only changing style, several loss functions are introduced:\n1. Adversarial Loss\n\t* The generator tries to create fake images that the discriminator classifies as real.\n\t* The discriminator, in turn, learns to distinguish between real and fake images.\n2. Cycle-consistency Loss\n\t* If we convert a photo into a fake Monet and then transform it back into a photo, the result should closely resemble the original photo.\n\t* This forces the model to preserve the image’s structure and content while changing style.\n3. Identity Loss\n\t* If we give a real Monet to the “photo → Monet” generator, it should return almost the same Monet.\n\t* This prevents the model from unnecessarily altering images that are already in the correct style.\n\nThese losses are combined with weighting factors (LAMBDA_CYCLE, LAMBDA_ID) to balance their influence during training.\n\nTo train the generators and discriminators, the Adam optimizer is used with standard GAN parameters:\n* learning rate: 2e-4\n* beta_1: 0.5 (helps stabilize training and avoid large oscillations)\n\nIndependent optimizers are defined for:\n* Photo → Monet generator\n* Monet → Photo generator\n* Photo discriminator\n* Monet discriminator\n\nThis allows each part of the network to learn at its own pace while following the same optimization rules.","metadata":{}},{"cell_type":"code","source":"# ✅ Constants for weighting\nLAMBDA_CYCLE = 10.0\nLAMBDA_ID = 5.0\n\n# Binary Crossentropy for real vs. fake\nloss_obj = tf.keras.losses.BinaryCrossentropy(from_logits=True)\n\n# Adversarial Loss (Generator wants Discriminator to say 'real')\ndef generator_adversarial_loss(fake_output):\n    return loss_obj(tf.ones_like(fake_output), fake_output)\n\n# Discriminator Loss (real should be 1, fake should be 0)\ndef discriminator_loss(real_output, fake_output):\n    real_loss = loss_obj(tf.ones_like(real_output), real_output)\n    fake_loss = loss_obj(tf.zeros_like(fake_output), fake_output)\n    return (real_loss + fake_loss) * 0.5\n\n# Cycle-consistency Loss\ndef calc_cycle_loss(real_image, cycled_image):\n    loss = tf.reduce_mean(tf.abs(real_image - cycled_image))\n    return LAMBDA_CYCLE * loss\n\n# Identity Loss\ndef identity_loss(real_image, same_image):\n    loss = tf.reduce_mean(tf.abs(real_image - same_image))\n    return LAMBDA_ID * loss\n\n# Optimizers (standard Adam settings for CycleGAN)\ngenerator_pm_optimizer = tf.keras.optimizers.Adam(2e-4, beta_1=0.5)\ngenerator_mp_optimizer = tf.keras.optimizers.Adam(2e-4, beta_1=0.5)\ndiscriminator_p_optimizer = tf.keras.optimizers.Adam(2e-4, beta_1=0.5)\ndiscriminator_m_optimizer = tf.keras.optimizers.Adam(2e-4, beta_1=0.5)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-24T10:35:52.431492Z","iopub.execute_input":"2025-09-24T10:35:52.431757Z","iopub.status.idle":"2025-09-24T10:35:52.448940Z","shell.execute_reply.started":"2025-09-24T10:35:52.431734Z","shell.execute_reply":"2025-09-24T10:35:52.448165Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Creación del primer modelo // Creating the First Model\n\nEl train_step es la unidad básica de entrenamiento: define qué pasa en una sola iteración (batch) y cómo se actualizan los pesos de generadores y discriminadores.\nEn CycleGAN tenemos:\n* 2 generadores: generator_pm (Photo → Monet) y generator_mp (Monet → Photo).\n* 2 discriminadores: discriminator_p (fotos) y discriminator_m (Monet).\n\nCada paso de entrenamiento:\n1. Genera imágenes falsas.\n2.\tReconstruye imágenes originales (cycle).\n3.\tCalcula pérdidas (adversarial, ciclo e identidad).\n4.\tCalcula gradientes y actualiza pesos de las redes.\n\nEs importante indicar en este último paso es importante recalcar que: un gradiente es, básicamente, la derivada de la función de pérdida respecto a los parámetros del modelo (los pesos y sesgos de la red neuronal).\n\n--------------------------------------------------------------------------------------------\n\nThe train_step is the basic training unit: it defines what happens in a single iteration (batch) and how the weights of the generators and discriminators are updated.\n\nIn a CycleGAN, we have:\n* 2 generators: generator_pm (Photo → Monet) and generator_mp (Monet → Photo).\n* 2 discriminators: discriminator_p (photos) and discriminator_m (Monet).\n\nEach training step:\n1. Generates fake images.\n2. Reconstructs the original images (cycle).\n3. Calculates the losses (adversarial, cycle, and identity).\n4. Computes gradients and updates the network weights.\n\nIt is important to note that in the last step, a gradient is essentially the derivative of the loss function with respect to the model parameters (the weights and biases of the neural network).","metadata":{}},{"cell_type":"code","source":"@tf.function\ndef train_step(real_p, real_m):\n    with tf.GradientTape(persistent=True) as tape:\n        # --------------------------------\n        # Generator_PM: Photo -> Monet\n        # --------------------------------\n        # Se almacena el resultado de convertir una imagen real a monet\n        fake_m = generator_pm(real_p, training=True)\n        # Se almacena el resultado de devolver la imagen monet generada a su forma original\n        cycled_p = generator_mp(fake_m, training=True)\n        # Con estas dos pruebas sobre el generador se pretende gestionar tanto la calidad del\n        # generador para crear imágenes de 0 como la calidad de recuperar la forma original \n        # de las imagenes generadas\n\n        # --------------------------------\n        # Generator_MP: Monet -> Photo\n        # --------------------------------\n        # Se almacena el resultado de convertir una imagen monet a real\n        fake_p = generator_mp(real_m, training=True)\n        # Se almacena el resultado de devolver la imagen real generada a su forma original\n        cycled_m = generator_pm(fake_p, training=True)\n\n        # --------------------------------\n        # Identity Mapping\n        # --------------------------------\n        # Resultado de aplicar a las imagenes reales los generadores inversos \n        same_p = generator_mp(real_p, training=True)\n        same_m = generator_pm(real_m, training=True)\n        # Permite medir la capacidad de estos generadores para no alterar imagenes que ya\n        # se encuentran correctamente generadas en su propia naturaleza\n\n        # --------------------------------\n        # Discriminator Outputs\n        # --------------------------------\n        # Aplicación de los discriminadores en su tarea de detectar tanto las imágenes que\n        # se le han pasado en su forma original y correcta como aquellas que han sido \n        # generadas con la intención de engañarlos\n        disc_real_p = discriminator_p(real_p, training=True)\n        disc_real_m = discriminator_m(real_m, training=True)\n        disc_fake_p = discriminator_p(fake_p, training=True)\n        disc_fake_m = discriminator_m(fake_m, training=True)\n\n        # --------------------------------\n        # Generator Losses\n        # --------------------------------\n        # Evaluación de las perdidas al intentar engañar al discriminador con las imagenes falsas\n        gen_pm_loss = generator_adversarial_loss(disc_fake_m)\n        gen_mp_loss = generator_adversarial_loss(disc_fake_p)\n\n        # Evaluación de las diferencias entre las imagenes reales y las generadas tras \n        # recibor como entrada los generadores las imagenes falsas\n        total_cycle_loss = calc_cycle_loss(real_p, cycled_p) + calc_cycle_loss(real_m, cycled_m)\n\n        # Evaluación de las diferencias entre las imagenes reales y las generadas tras \n        # recibor como entrada los generadores las imagenes verdaderas\n        total_gen_pm_loss = gen_pm_loss + total_cycle_loss + identity_loss(real_m, same_m)\n        total_gen_mp_loss = gen_mp_loss + total_cycle_loss + identity_loss(real_p, same_p)\n\n        # --------------------------------\n        # Discriminator Losses\n        # --------------------------------\n        # Evaluación de la precision de los discriminadores\n        disc_p_loss = discriminator_loss(disc_real_p, disc_fake_p)\n        disc_m_loss = discriminator_loss(disc_real_m, disc_fake_m)\n\n    # --------------------------------\n    # Calculate and Apply Gradients\n    # --------------------------------\n    # Se calcula cuanto cambia la pérdida si ajustamos un poquito cada peso.\n    generator_pm_gradients = tape.gradient(total_gen_pm_loss, generator_pm.trainable_variables)\n    generator_mp_gradients = tape.gradient(total_gen_mp_loss, generator_mp.trainable_variables)\n\n    discriminator_p_gradients = tape.gradient(disc_p_loss, discriminator_p.trainable_variables)\n    discriminator_m_gradients = tape.gradient(disc_m_loss, discriminator_m.trainable_variables)\n\n    #Aplica los gradientes indicados\n    generator_pm_optimizer.apply_gradients(zip(generator_pm_gradients, generator_pm.trainable_variables))\n    generator_mp_optimizer.apply_gradients(zip(generator_mp_gradients, generator_mp.trainable_variables))\n\n    discriminator_p_optimizer.apply_gradients(zip(discriminator_p_gradients, discriminator_p.trainable_variables))\n    discriminator_m_optimizer.apply_gradients(zip(discriminator_m_gradients, discriminator_m.trainable_variables))\n\n    return {\n        'gen_pm_loss': total_gen_pm_loss,\n        'gen_mp_loss': total_gen_mp_loss,\n        'disc_p_loss': disc_p_loss,\n        'disc_m_loss': disc_m_loss\n    }","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-24T10:35:52.449817Z","iopub.execute_input":"2025-09-24T10:35:52.450045Z","iopub.status.idle":"2025-09-24T10:35:52.458610Z","shell.execute_reply.started":"2025-09-24T10:35:52.450029Z","shell.execute_reply":"2025-09-24T10:35:52.458009Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Con la intención de ir analizando el prograso del modelo en cada iteración del entrenamiento, se calcula las pérdidas de cada fase de entrenamiento.\n\n--------------------------------------------------------------------------------------------\n\nTo monitor the model’s progress during each training iteration, the losses for each phase of training are calculated.","metadata":{}},{"cell_type":"markdown","source":"","metadata":{}},{"cell_type":"code","source":"import time\n\ndef train(dataset_P, dataset_M, epochs=1):\n    history = {\n        'gen_pm_loss': [],\n        'gen_mp_loss': [],\n        'disc_p_loss': [],\n        'disc_m_loss': []\n    }\n    \n    for epoch in range(epochs):\n        start = time.time()\n        print(f\"Epoch {epoch+1}/{epochs}\")\n        \n        gen_pm_losses = []\n        gen_mp_losses = []\n        disc_p_losses = []\n        disc_m_losses = []\n        \n        for real_p, real_m in tf.data.Dataset.zip((dataset_P, dataset_M)):\n            losses = train_step(real_p, real_m)\n            gen_pm_losses.append(losses['gen_pm_loss'].numpy())\n            gen_mp_losses.append(losses['gen_mp_loss'].numpy())\n            disc_p_losses.append(losses['disc_p_loss'].numpy())\n            disc_m_losses.append(losses['disc_m_loss'].numpy())\n        \n        # Average for the epoch\n        history['gen_pm_loss'].append(np.mean(gen_pm_losses))\n        history['gen_mp_loss'].append(np.mean(gen_mp_losses))\n        history['disc_p_loss'].append(np.mean(disc_p_losses))\n        history['disc_m_loss'].append(np.mean(disc_m_losses))\n        \n        print(f\"Time for epoch {epoch+1}: {time.time() - start:.2f} sec\")\n        print(f\"Gen_PM Loss: {history['gen_pm_loss'][-1]:.4f} | Gen_MP Loss: {history['gen_mp_loss'][-1]:.4f}\")\n        print(f\"Disc_P Loss: {history['disc_p_loss'][-1]:.4f} | Disc_M Loss: {history['disc_m_loss'][-1]:.4f}\\n\")\n    \n    return history","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-24T10:35:52.459318Z","iopub.execute_input":"2025-09-24T10:35:52.459629Z","iopub.status.idle":"2025-09-24T10:35:52.474648Z","shell.execute_reply.started":"2025-09-24T10:35:52.459613Z","shell.execute_reply":"2025-09-24T10:35:52.473972Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"history = train(photo_dataset_train, monet_dataset_train, epochs=10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-24T10:35:52.476686Z","iopub.execute_input":"2025-09-24T10:35:52.476901Z","iopub.status.idle":"2025-09-24T10:59:19.976120Z","shell.execute_reply.started":"2025-09-24T10:35:52.476885Z","shell.execute_reply":"2025-09-24T10:59:19.975308Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Veamos de manera gráfica la evolución de la función de pérdida (loss).\n\nInterpretación:\n\n1. Generadores (azul y naranja):\n* Empiezan con un error alto.\n* Van bajando de forma clara hasta estabilizarse en torno a 5.\n* Esto significa que, conforme avanzan las épocas, los generadores van mejorando en producir imágenes más realistas, aunque todavía les queda margen (loss > 0).\n2.\tDiscriminadores (verde y rojo):\n* Mantienen un valor mucho más bajo.\n* Esto indica que los discriminadores aprenden rápido a distinguir, pero su pérdida se estabiliza porque hay un “equilibrio” con los generadores (ninguno gana claramente).\n3.\tBalance general:\n* En un entrenamiento GAN, es buena señal que los generadores y discriminadores se estabilicen y no colapsen.\n* Aquí parece que hay un equilibrio sano:\n  * Los generadores aún están mejorando pero de forma estable.\n  * Los discriminadores mantienen su capacidad de distinguir sin volverse dominantes (si la pérdida fuera casi 0, significaría que los generadores no pueden engañarlos en absoluto).\n\nEl gráfico muestra que el CycleGAN está entrenando correctamente, con generadores que mejoran progresivamente y discriminadores que se mantienen estables, lo cual es un buen indicador de convergencia.\n\n---------------------------------------------------------------------------------------------\n\nLet’s visualize the evolution of the loss function graphically.\n\nInterpretation:\n1. Generators (blue and orange):\n* Start with high error.\n* Gradually decrease and stabilize around 5.\n* This indicates that, as epochs progress, the generators improve at producing more realistic images, though there’s still room for improvement (loss > 0).\n2. Discriminators (green and red):\n* Maintain a much lower value.\n* This shows that discriminators learn quickly to distinguish, but their loss stabilizes due to the “balance” with the generators (neither side wins clearly).\n3. Overall Balance:\n* In GAN training, it’s a good sign when generators and discriminators stabilize without collapsing.\n* Here, there seems to be a healthy balance:\n\t* Generators are still improving, but steadily.\n\t* Discriminators maintain their distinguishing ability without becoming dominant (if loss were near 0, it would mean generators cannot fool them at all).\n\nThe graph shows that the CycleGAN is training correctly, with progressively improving generators and stable discriminators — a good indicator of convergence.","metadata":{}},{"cell_type":"code","source":"plt.figure(figsize=(12, 6))\nplt.plot(history['gen_pm_loss'], label='Generator Photo→Monet Loss')\nplt.plot(history['gen_mp_loss'], label='Generator Monet→Photo Loss')\nplt.plot(history['disc_p_loss'], label='Discriminator Photo Loss')\nplt.plot(history['disc_m_loss'], label='Discriminator Monet Loss')\nplt.title('CycleGAN Training Loss')\nplt.xlabel('Epoch')\nplt.ylabel('Loss')\nplt.legend()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-24T10:59:19.976978Z","iopub.execute_input":"2025-09-24T10:59:19.977241Z","iopub.status.idle":"2025-09-24T10:59:20.177681Z","shell.execute_reply.started":"2025-09-24T10:59:19.977224Z","shell.execute_reply":"2025-09-24T10:59:20.177064Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"El analisis de estos datos nos ayudará en caso de querer llevar a cabo mejoras sobre el modelo, pero es fundamental analizar el resultado de las imágenes. Para ello, creamos una función encargada de convertir la imagen que devuelve el generador a los valores de pixeles y las dimensiones estandar.\n\nFinalmente mostramos una comparación entre la imagen real y la generada.\n\n---------------------------------------------------------------------------------------------\n\nAnalyzing these data will help if we want to make improvements to the model, but it is crucial to examine the output images.\nTo do this, we create a function that converts the image returned by the generator into standard pixel values and dimensions.\n\nFinally, we display a comparison between the real image and the generated one.","metadata":{}},{"cell_type":"code","source":"import random\n\n# Define denormalization helper\ndef denormalize_image(image):\n    image = (image + 1) * 127.5\n    image = tf.clip_by_value(image, 0, 255)\n    return tf.cast(image, tf.uint8)\n\n# Take 5 random photos from training dataset\nphoto_batches = list(photo_dataset_train)\nrandom.shuffle(photo_batches)\nphoto_samples = [batch[0] for batch in photo_batches[:5]]\n\n# Monetify\nmonetified_samples = []\nfor photo in photo_samples:\n    monetified = generator_pm(tf.expand_dims(photo, 0), training=False)[0]\n    monetified_samples.append(denormalize_image(monetified).numpy())\n\n# Plot\nplt.figure(figsize=(15, 6))\nfor i in range(5):\n    plt.subplot(2, 5, i + 1)\n    plt.imshow(denormalize_image(photo_samples[i]).numpy())\n    plt.title('Photo')\n    plt.axis('off')\n    \n    plt.subplot(2, 5, i + 6)\n    plt.imshow(monetified_samples[i])\n    plt.title('Monetified')\n    plt.axis('off')\n\nplt.suptitle('Random Photo Samples Monetified', fontsize=16)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-24T11:00:37.113552Z","iopub.execute_input":"2025-09-24T11:00:37.113949Z","iopub.status.idle":"2025-09-24T11:00:45.491605Z","shell.execute_reply.started":"2025-09-24T11:00:37.113922Z","shell.execute_reply":"2025-09-24T11:00:45.490498Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Finalmente subimos los resultados a la competición de Kaggle\n\n--------------------------------------------------------------------------------------------\n\nFinally, we upload the results to the Kaggle competition.","metadata":{}},{"cell_type":"code","source":"import os\nfrom PIL import Image\nimport numpy as np\n\noutput_dir = '/kaggle/working/generated_images_model'\nos.makedirs(output_dir, exist_ok=True)\n\nindex = 0\n\nfor photo_batch in photo_dataset_train:\n    generated_batch = generator_pm(photo_batch, training=False)\n    \n    # Process each image in the batch (in case batch_size > 1)\n    for i in range(generated_batch.shape[0]):\n        generated = denormalize_image(generated_batch[i]).numpy()\n        img = Image.fromarray(generated)\n        img.convert('RGB').save(os.path.join(output_dir, f'{index}.jpg'), 'JPEG')\n        index += 1\n\nprint(f\"✅ Saved {index} generated Monet-style images to: {output_dir}\")\n\nimport zipfile\n\nsubmission_zip = '/kaggle/working/images.zip'\n\nwith zipfile.ZipFile(submission_zip, 'w') as zipf:\n    for i in range(index):\n        zipf.write(os.path.join(output_dir, f'{i}.jpg'), arcname=f'{i}.jpg')\n\nprint(f\"✅ Created images zip: {submission_zip}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-24T11:00:52.205874Z","iopub.execute_input":"2025-09-24T11:00:52.206167Z","iopub.status.idle":"2025-09-24T11:13:52.583387Z","shell.execute_reply.started":"2025-09-24T11:00:52.206147Z","shell.execute_reply":"2025-09-24T11:13:52.582666Z"}},"outputs":[],"execution_count":null}]}