{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceType":"competition","sourceId":21755,"databundleVersionId":1475600},{"sourceType":"kernelVersion","sourceId":226855511}],"dockerImageVersionId":30919,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Introduction\n\nThis notebook is a continuation of the [Part One: Image Translation using CycleGAN](https://www.kaggle.com/code/dorisqzchen/part-one-image-translation-using-cyclegan?scriptVersionId=226855511). I will continue training the model with a decayed learning rate and early stopping setup, then submit the newly generated images to Kaggle to see if the results improve.\n\nSince this is a separate notebook, I will load the data and build the model as I did in [Part One: Image Translation using CycleGAN](https://www.kaggle.com/code/dorisqzchen/part-one-image-translation-using-cyclegan?scriptVersionId=226855511)., and then load the weights from the first model.","metadata":{}},{"cell_type":"code","source":"# Import packages \nfrom kaggle_datasets import KaggleDatasets\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport os\nimport tensorflow as tf\nfrom tensorflow import keras\nfrom keras.models import Model\nfrom keras.layers import Layer, Input, Conv2D, Conv2DTranspose, GroupNormalization \nfrom keras.layers import Activation, ReLU, LeakyReLU, Add\nfrom keras.optimizers import Adam\nfrom keras.ops import pad, ones_like, zeros_like\nfrom keras.utils import register_keras_serializable\nfrom keras.losses import MeanSquaredError, MeanAbsoluteError\nfrom keras.callbacks import ModelCheckpoint, EarlyStopping\nfrom keras.optimizers.schedules import PolynomialDecay","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-05T22:30:36.901937Z","iopub.execute_input":"2025-03-05T22:30:36.902223Z","iopub.status.idle":"2025-03-05T22:30:49.567601Z","shell.execute_reply.started":"2025-03-05T22:30:36.902192Z","shell.execute_reply":"2025-03-05T22:30:49.566914Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Load the data","metadata":{}},{"cell_type":"code","source":"# Define a function to decode images from TFRecord files\ndef decode_img(img):\n    image = tf.io.decode_jpeg(img, channels = 3)\n    image = (tf.cast(image, tf.float32) / 127.5) - 1\n    image = tf.reshape(image, [256, 256, 3])\n    return image\n\n# Define a function to read images from TFRecord files\ndef read_tfrecord(tfdata):\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    tfdata = tf.io.parse_single_example(tfdata, tfrecord_format)\n    image = decode_img(tfdata['image'])\n    return image\n\n# Define a function to load images from the TFRecord files\ndef load_dataset(filenames):\n    # disable order, increase speed\n    ignore_order = tf.data.Options()\n    ignore_order.experimental_deterministic = False   \n    \n    # read images from multiple files if available\n    dataset = tf.data.TFRecordDataset(filenames)\n    dataset = dataset.with_options(ignore_order)\n    dataset = dataset.map(read_tfrecord)\n    return dataset","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-05T22:30:49.568605Z","iopub.execute_input":"2025-03-05T22:30:49.569053Z","iopub.status.idle":"2025-03-05T22:30:49.574760Z","shell.execute_reply.started":"2025-03-05T22:30:49.569033Z","shell.execute_reply":"2025-03-05T22:30:49.573821Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Get the file names from monet_tfrec and photo_tfrec\nmonet_file = tf.io.gfile.glob('/kaggle/input/gan-getting-started/monet_tfrec/*.tfrec')\nphoto_file = tf.io.gfile.glob('/kaggle/input/gan-getting-started/photo_tfrec/*.tfrec')\n\n# Load the datasets\nmonet_ds = load_dataset(monet_file).batch(1)\nphoto_ds = load_dataset(photo_file).batch(1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-05T22:30:49.576986Z","iopub.execute_input":"2025-03-05T22:30:49.577329Z","iopub.status.idle":"2025-03-05T22:30:50.785037Z","shell.execute_reply.started":"2025-03-05T22:30:49.577296Z","shell.execute_reply":"2025-03-05T22:30:50.784328Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Define a function to prepare the data \ndef CycleGan_dataset(monet_dataset, photo_dataset):\n    monet_ds = monet_dataset.repeat()\n    photo_ds = photo_dataset.repeat()\n   \n    monet_ds = monet_ds.shuffle(2048)\n    photo_ds = photo_ds.shuffle(2048)\n    \n    gan_ds = tf.data.Dataset.zip((monet_ds, photo_ds))\n    \n    return gan_ds\n\n# Get the training set \nCycleGan_ds = CycleGan_dataset(monet_ds, photo_ds)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-05T22:30:50.786244Z","iopub.execute_input":"2025-03-05T22:30:50.786486Z","iopub.status.idle":"2025-03-05T22:30:50.800923Z","shell.execute_reply.started":"2025-03-05T22:30:50.786460Z","shell.execute_reply":"2025-03-05T22:30:50.800185Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Build CycleGAN model","metadata":{}},{"cell_type":"code","source":"# Weights initializer for the layers\nkernel_init = keras.initializers.RandomNormal(mean = 0.0, stddev = 0.02)\n\n# Weights initializer for the instance normalization\ngamma_init = keras.initializers.RandomNormal(mean = 0.0, stddev = 0.02)\n\n# Define a function for downsampling block\ndef downsample(x, filters, kernel_size, strides, activation = 'relu'):\n    x = Conv2D(filters, kernel_size, strides = strides, padding = 'same',\n                     kernel_initializer = kernel_init, use_bias = False)(x)\n    x = GroupNormalization(groups = filters, gamma_initializer = gamma_init)(x)    # Instance Normalization\n    x = Activation(activation)(x)\n    return x\n\n# Define a function for upsampling block\ndef upsample(x, filters, kernel_size, strides, activation = 'relu'):\n    x = Conv2DTranspose(filters, kernel_size, strides = strides, padding = 'same',\n                     kernel_initializer = kernel_init, use_bias = False)(x)\n    x = GroupNormalization(groups = filters, gamma_initializer = gamma_init)(x)  \n    x = Activation(activation)(x)\n    return x\n\n# Define a layer of reflection padding via subclassing\n@register_keras_serializable()\nclass reflection_padding(Layer):\n    def __init__(self, padding = (1, 1), **kwargs):\n        self.padding = tuple(padding)\n        super().__init__(**kwargs)\n        \n    def call(self, input_tensor):\n        pad_x, pad_y = self.padding\n        pad_width = [\n            [0, 0],              # no padding on batch axis\n            [pad_y, pad_y],      # padding on image height axis\n            [pad_x, pad_x],      # padding on image width axis\n            [0, 0]               # no padding on channel axis\n        ]\n        return pad(input_tensor, pad_width, mode = \"reflect\")\n        \n    def get_config(self):\n        config = super.get_config()\n        config.update({\"padding\": self.padding})\n        return config\n\n# Define a layer of residule block via subclassing\n@register_keras_serializable()\nclass residual_block(Layer):\n    def __init__(self, **kwargs):\n        super().__init__(**kwargs)\n        self.padding = reflection_padding()\n\n    def build(self, input_shape):\n        filters = input_shape[-1]\n        self.conv1 = Conv2D(filters, kernel_size = (3, 3), \n                            kernel_initializer = kernel_init, use_bias = False)\n        self.conv2 = Conv2D(filters, kernel_size = (3, 3), \n                            kernel_initializer = kernel_init, use_bias = False)\n        self.instance_norm1 = GroupNormalization(groups = filters, gamma_initializer = gamma_init)\n        self.instance_norm2 = GroupNormalization(groups = filters, gamma_initializer = gamma_init)\n\n    def call(self, inputs):\n        input_tensor = inputs\n        # first conv block\n        x = self.padding(inputs)\n        x = self.conv1(x)\n        x = self.instance_norm1(x)\n        x = Activation('relu')(x)\n        \n        # second conv block\n        x = self.padding(x)\n        x = self.conv2(x)\n        x = self.instance_norm2(x)\n        x = Activation('relu')(x)\n\n        # output\n        x = Add()([input_tensor, x])\n        return x\n\n    def get_config(self):\n        return super.get_config()   ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-05T22:30:50.801678Z","iopub.execute_input":"2025-03-05T22:30:50.801919Z","iopub.status.idle":"2025-03-05T22:30:50.872345Z","shell.execute_reply.started":"2025-03-05T22:30:50.801899Z","shell.execute_reply":"2025-03-05T22:30:50.871659Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Build the generator\ndef generator(filters = 64, num_downsample = 2, num_residual = 9, num_upsample = 2, name = None):\n    inputs = Input(shape = [256, 256, 3])\n    # First convolutional block \n    x = reflection_padding(padding = (3, 3))(inputs)\n    x = Conv2D(filters, kernel_size = (7, 7), kernel_initializer = kernel_init, use_bias = False)(x)\n    x = GroupNormalization(groups = filters, gamma_initializer = gamma_init)(x)\n    x = Activation('relu')(x)\n\n    # Downsampling block\n    for _ in range(num_downsample):\n        filters *= 2\n        x = downsample(x, filters, (3, 3), (2, 2), activation = 'relu')\n\n    # Residual block\n    for _ in range(num_residual):\n        x = residual_block()(x)\n\n    # Upsampling block\n    for _ in range(num_upsample):\n        filters //= 2\n        x = upsample(x, filters, (3, 3), (2, 2), activation = 'relu')\n\n    # Final block\n    x = reflection_padding(padding = (3, 3))(x)\n    x = Conv2D(3, kernel_size = (7, 7), kernel_initializer = kernel_init, use_bias = False)(x)\n    x = GroupNormalization(groups = 3, gamma_initializer = gamma_init)(x)\n    x = Activation('tanh')(x)\n\n    return Model(inputs = inputs, outputs = x)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-05T22:30:50.873135Z","iopub.execute_input":"2025-03-05T22:30:50.873432Z","iopub.status.idle":"2025-03-05T22:30:50.880048Z","shell.execute_reply.started":"2025-03-05T22:30:50.873402Z","shell.execute_reply":"2025-03-05T22:30:50.879388Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Build the discriminator\ndef discriminator(filters = 64, num_downsample = 3, name = None):\n    inputs = Input(shape = [256, 256, 3])\n    # First convolutional block\n    x = Conv2D(filters, kernel_size = (4, 4), strides = (2, 2), padding = 'same',\n               kernel_initializer = kernel_init, use_bias = False)(inputs)\n    x = Activation('leaky_relu')(x)\n\n    # Downsampling block\n    for _ in range(num_downsample):\n        filters *= 2\n        x = downsample(x, filters, (4, 4), (2, 2), activation = 'leaky_relu')   \n\n    # Final block\n    x = Conv2D(1, kernel_size = (4, 4), padding = 'same',\n               kernel_initializer = kernel_init, use_bias = False)(x)\n\n    return Model(inputs = inputs, outputs = x)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-05T22:30:50.880931Z","iopub.execute_input":"2025-03-05T22:30:50.881278Z","iopub.status.idle":"2025-03-05T22:30:50.900732Z","shell.execute_reply.started":"2025-03-05T22:30:50.881251Z","shell.execute_reply":"2025-03-05T22:30:50.899865Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Define the functions of adversarial loss\ndef generator_loss(dis_fake):\n    loss_fn = MeanSquaredError()\n    return loss_fn(ones_like(dis_fake), dis_fake)\n\ndef discriminator_loss(dis_real, dis_fake):\n    loss_fn = MeanSquaredError()\n    real_loss = loss_fn(ones_like(dis_real), dis_real)\n    fake_loss = loss_fn(zeros_like(dis_fake), dis_fake)\n    return (real_loss + fake_loss) * 0.5\n\n# Define the function of cycle consistency loss\ndef cycle_loss(img, cycled_img, Lambda):\n    loss_fn = MeanAbsoluteError()\n    return Lambda * loss_fn(img, cycled_img)\n\n# Define the function of identity loss\ndef identity_loss(img, same_img, Lambda):\n    loss_fn = MeanAbsoluteError()\n    return 0.5 * Lambda *  loss_fn(img, same_img)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-05T22:30:50.901558Z","iopub.execute_input":"2025-03-05T22:30:50.901777Z","iopub.status.idle":"2025-03-05T22:30:50.916015Z","shell.execute_reply.started":"2025-03-05T22:30:50.901759Z","shell.execute_reply":"2025-03-05T22:30:50.915091Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Build the CycleGAN model via subclassing\nclass CycleGAN(Model):\n    def __init__(self, generator_monet, generator_photo, discriminator_monet, \n                 discriminator_photo, lambda_cycle = 10):\n        super().__init__()\n        self.GenM = generator_monet\n        self.GenP = generator_photo\n        self.DisM = discriminator_monet\n        self.DisP = discriminator_photo\n        self.Lamd = lambda_cycle\n        \n    def compile(self, \n                genM_optimizer, genP_optimizer, disM_optimizer, disP_optimizer,\n                gen_loss, dis_loss, cycle_loss, identity_loss):\n        super().compile()\n        self.GenM_Opt = genM_optimizer\n        self.GenP_Opt = genP_optimizer\n        self.DisM_Opt = disM_optimizer\n        self.DisP_Opt = disP_optimizer\n        self.gen_loss = gen_loss\n        self.dis_loss = dis_loss\n        self.cycle_loss = cycle_loss\n        self.identity_loss = identity_loss\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            # photo → monet → photo\n            fake_monet = self.GenM(real_photo, training = True)\n            cycled_photo = self.GenP(fake_monet, training = True)\n\n            # monet → photo → monet\n            fake_photo = self.GenP(real_monet, training = True)\n            cycled_monet = self.GenM(fake_photo, training = True)\n\n            # identity mapping\n            same_monet = self.GenM(real_monet, training = True)\n            same_photo = self.GenP(real_photo, training = True)\n            \n            # discriminator output\n            dis_real_monet = self.DisM(real_monet, training = True)\n            dis_fake_monet = self.DisM(fake_monet, training = True)\n            dis_real_photo = self.DisP(real_photo, training = True)\n            dis_fake_photo = self.DisP(fake_photo, training = True)\n\n            # generator adversarial loss\n            genM_adver = self.gen_loss(dis_fake_monet)\n            genP_adver = self.gen_loss(dis_fake_photo)\n\n            # cycle loss\n            cycle_photo = self.cycle_loss(real_photo, cycled_photo, self.Lamd)\n            cycle_monet = self.cycle_loss(real_monet, cycled_monet, self.Lamd)\n            total_cycle = cycle_photo + cycle_monet\n\n            # identity loss\n            genM_identity = self.identity_loss(real_monet, same_monet, self.Lamd)\n            genP_identity = self.identity_loss(real_photo, same_photo, self.Lamd)\n\n            # total generator loss\n            genM_loss = genM_adver + total_cycle + genM_identity\n            genP_loss = genP_adver + total_cycle + genP_identity\n\n            # discriminator loss\n            disM_loss = self.dis_loss(dis_real_monet, dis_fake_monet)\n            disP_loss = self.dis_loss(dis_real_photo, dis_fake_photo)\n\n        # calculate gradients for generators\n        grads_GenM = tape.gradient(genM_loss, self.GenM.trainable_variables)\n        grads_GenP = tape.gradient(genP_loss, self.GenP.trainable_variables)\n            \n        # calculate gradients for discriminators\n        grads_DisM = tape.gradient(disM_loss, self.DisM.trainable_variables)\n        grads_DisP = tape.gradient(disP_loss, self.DisP.trainable_variables)\n\n        # update weights of generators and discriminators\n        self.GenM_Opt.apply_gradients(zip(grads_GenM, self.GenM.trainable_variables))\n        self.GenP_Opt.apply_gradients(zip(grads_GenP, self.GenP.trainable_variables))\n        self.DisM_Opt.apply_gradients(zip(grads_DisM, self.DisM.trainable_variables))\n        self.DisP_Opt.apply_gradients(zip(grads_DisP, self.DisP.trainable_variables))\n\n        return {\n            \"genM_loss\": genM_loss,\n            \"genP_loss\": genP_loss,\n            \"disM_loss\": disM_loss,\n            \"disP_loss\": disP_loss\n        }","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-05T22:30:50.918667Z","iopub.execute_input":"2025-03-05T22:30:50.918912Z","iopub.status.idle":"2025-03-05T22:30:50.939862Z","shell.execute_reply.started":"2025-03-05T22:30:50.918892Z","shell.execute_reply":"2025-03-05T22:30:50.938913Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Continue training\n\nIn the original paper, the model was trained for 200 epochs, but I only trained my first model for 50 epochs. In this session, I will load the weights from the first model, and continue training for another 150 epochs with the following setup: \n\n* Use a learning rate schedule: keep the learning rate at 0.0002 for the first 100 epochs, then linearly decay the rate to zero over the next 100 epochs.\n* Add an early stopping: stop training if the loss of `generator_monet` doesn't decrease for 5 consecutive epochs.","metadata":{}},{"cell_type":"code","source":"# Define model check_point\ncheck_point = ModelCheckpoint(\n    filepath = '/kaggle/working/cyclegan2.weights.h5',\n    save_weights_only = True\n)\n\n# Define Early Stopping\nearly_stop = EarlyStopping(\n    monitor = 'genM_loss',\n    patience = 5,\n    verbose = 1,\n    mode = 'min',\n    restore_best_weights = True\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-05T22:30:50.941018Z","iopub.execute_input":"2025-03-05T22:30:50.941359Z","iopub.status.idle":"2025-03-05T22:30:50.959679Z","shell.execute_reply.started":"2025-03-05T22:30:50.941324Z","shell.execute_reply":"2025-03-05T22:30:50.958784Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Define the learning rate schedule\ninitial_learning_rate = 0.0002\ndecay_steps = 50 * 300   # decay over 100 epochs (from 100 to 200)\nend_learning_rate = 0.0\n\n# Create the linear decay schedule\nlr_schedule = PolynomialDecay(\n    initial_learning_rate = initial_learning_rate,\n    decay_steps = decay_steps,\n    end_learning_rate = end_learning_rate,\n    power = 1.0\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-05T22:30:50.960530Z","iopub.execute_input":"2025-03-05T22:30:50.960771Z","iopub.status.idle":"2025-03-05T22:30:50.977638Z","shell.execute_reply.started":"2025-03-05T22:30:50.960752Z","shell.execute_reply":"2025-03-05T22:30:50.976694Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Train the model\nCycleGan2 = CycleGAN(generator_monet = generator(name = 'gen_monet'), \n                    generator_photo = generator(name = 'gen_photo'), \n                    discriminator_monet = discriminator(name = 'dis_monet'), \n                    discriminator_photo = discriminator(name = 'dis_photo'))\n    \nCycleGan2.compile(genM_optimizer = Adam(learning_rate=lr_schedule), \n                 genP_optimizer = Adam(learning_rate=lr_schedule), \n                 disM_optimizer = Adam(learning_rate=lr_schedule), \n                 disP_optimizer = Adam(learning_rate=lr_schedule),\n                 gen_loss = generator_loss, \n                 dis_loss = discriminator_loss, \n                 cycle_loss = cycle_loss, \n                 identity_loss = identity_loss)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-05T22:30:50.978446Z","iopub.execute_input":"2025-03-05T22:30:50.978685Z","iopub.status.idle":"2025-03-05T22:30:54.173970Z","shell.execute_reply.started":"2025-03-05T22:30:50.978666Z","shell.execute_reply":"2025-03-05T22:30:54.173245Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Load the weights from the first CycleGan model\nCycleGan2.load_weights('/kaggle/input/image-translation-using-cyclegan/cyclegan1.weights.h5')\n\nhistory = CycleGan2.fit(CycleGan_ds, \n                       steps_per_epoch = 300,\n                       epochs = 150,\n                       callbacks = [check_point])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-05T22:31:54.357712Z","iopub.execute_input":"2025-03-05T22:31:54.358057Z","iopub.status.idle":"2025-03-06T01:59:25.642922Z","shell.execute_reply.started":"2025-03-05T22:31:54.358031Z","shell.execute_reply":"2025-03-06T01:59:25.642124Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Extract generator_monet\ngenerator_monet =  CycleGan2.GenM\n\n# Visualize the Photos vs. Monet-style photos \nfig, ax = plt.subplots(nrows = 2, ncols = 3, figsize = (15, 10))\nfig.suptitle('Photo vs. Monet-style', fontsize = 16)\nfor i, img in enumerate(photo_ds.take(3)):\n    generated_img = generator_monet(img, training = False)[0].numpy()\n    generated_img = (generated_img * 127.5 + 127.5).astype(np.uint8)\n    img = tf.cast(img * 127.5 + 127.5, tf.uint8).numpy()\n    img = np.squeeze(img, axis=0)\n    ax[0, i].imshow(img)\n    ax[0, i].set_title('Photo')\n    ax[0, i].axis('off')\n    ax[1, i].imshow(generated_img)\n    ax[1, i].set_title('Monet-style')\n    ax[1, i].axis('off')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-06T01:59:28.110745Z","iopub.execute_input":"2025-03-06T01:59:28.110995Z","iopub.status.idle":"2025-03-06T01:59:29.371702Z","shell.execute_reply.started":"2025-03-06T01:59:28.110973Z","shell.execute_reply":"2025-03-06T01:59:29.370473Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Submit the Generated images","metadata":{}},{"cell_type":"code","source":"import PIL\nimport shutil\n\n# Create the folder to save generated images\nos.makedirs('../images/')\n\n# Generate monet-style images\ni = 1\nfor img in photo_ds:\n    generated_img = generator_monet(img, training = False)[0].numpy()\n    generated_img = (generated_img * 127.5 + 127.5).astype(np.uint8)\n    im = PIL.Image.fromarray(generated_img)\n    im.save(\"../images/\" + str(i) + '.jpg')\n    i += 1\n\nprint(f\"Generated images: {len([name for name in os.listdir('../images') if os.path.isfile(os.path.join('../images', name))])}\")\n\n# archive the image folder\nshutil.make_archive('/kaggle/working/images', 'zip', '../images')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-06T01:59:29.372761Z","iopub.execute_input":"2025-03-06T01:59:29.373017Z","iopub.status.idle":"2025-03-06T02:14:19.930916Z","shell.execute_reply.started":"2025-03-06T01:59:29.372995Z","shell.execute_reply":"2025-03-06T02:14:19.930095Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Conclusion\n\nIn this project, I built a CycleGAN model to translate regular photos into Monet-style paintings. By carefully following the details in the original paper, learning from the CycleGAN example in Keras, and studying notebooks from other Kagglers, I successfully built and trained the model on the Kaggle dataset.\n\nThe Monet-style images generated by the second model (trained for 200 epochs in total) appear more realistic than those from the first one (trained for 50 epochs, see [Part One: Image Translation using CycleGAN](https://www.kaggle.com/code/dorisqzchen/part-one-image-translation-using-cyclegan?scriptVersionId=226855511).), indicating that additional training epochs led to better results in this case.\n\nAs I am completely new to GANs, this project was more of a learning process for me on how to build a CycleGAN model using the Keras API. In future projects, I may explore further improvements by fine-tuning more parameters or adopting other deep neural network techniques.  \n\n### Reference¶\nJun-Yan Zhu, Taesung Park, Phillip Isola, Alexei A. Efros. Unpaired Image-to-Image Translation using Cycle-Consistent Adversarial Networks. 2020. arXiv:1703.10593 [cs.CV]\n\nAmy Jang. Monet CycleGAN Tutorial. Kaggle Notebook. https://www.kaggle.com/code/amyjang/monet-cyclegan-tutorial/notebook","metadata":{}}]}