{"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":"nvidiaTeslaT4","dataSources":[{"sourceId":21755,"databundleVersionId":1475600,"sourceType":"competition"}],"dockerImageVersionId":31153,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Import Python Libraries","metadata":{}},{"cell_type":"code","source":"import numpy as np \nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport cv2\nimport os\nimport math\nfrom PIL import Image\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport torch.nn.init as init\nfrom torch.utils.data import Dataset, random_split, DataLoader\n\nimport torchvision.models as models\nimport torchvision.transforms as transforms\n\nfrom tqdm.notebook import tqdm\nimport itertools\nimport time\nimport shutil","metadata":{"execution":{"iopub.status.busy":"2025-10-16T05:02:25.796765Z","iopub.execute_input":"2025-10-16T05:02:25.796987Z","iopub.status.idle":"2025-10-16T05:02:29.529725Z","shell.execute_reply.started":"2025-10-16T05:02:25.796966Z","shell.execute_reply":"2025-10-16T05:02:29.528991Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# EDA and Data Preprocessing","metadata":{}},{"cell_type":"code","source":"# access folders\npath_monet = \"../input/gan-getting-started/monet_jpg/\"\npath_photo = \"../input/gan-getting-started/photo_jpg/\"\n\nprint(\"Number of monet paintings: {}\".format(len(os.listdir(path_monet))))\nprint(\"Number of photos: {}\".format(len(os.listdir(path_photo))))","metadata":{"execution":{"iopub.status.busy":"2025-10-16T05:02:40.336312Z","iopub.execute_input":"2025-10-16T05:02:40.336647Z","iopub.status.idle":"2025-10-16T05:02:40.451812Z","shell.execute_reply.started":"2025-10-16T05:02:40.336622Z","shell.execute_reply":"2025-10-16T05:02:40.450950Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Now, let's display some example images, both monet and real photos.","metadata":{}},{"cell_type":"code","source":"# function to display sample images in a grid format\ndef display_sample_images(path, num_samples=16):\n    sample_images = os.listdir(path)[:num_samples]\n    \n    w = int(num_samples ** .5)\n    h = math.ceil(num_samples / w)\n    \n    for ind, image_name in enumerate(sample_images):\n        img = cv2.imread(os.path.join(path, image_name))\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) \n        plt.subplot(h, w, ind + 1)\n        plt.imshow(img)\n        plt.axis(\"off\")\n    \n    plt.show()","metadata":{"execution":{"iopub.status.busy":"2025-10-16T05:02:45.286169Z","iopub.execute_input":"2025-10-16T05:02:45.286525Z","iopub.status.idle":"2025-10-16T05:02:45.292238Z","shell.execute_reply.started":"2025-10-16T05:02:45.286473Z","shell.execute_reply":"2025-10-16T05:02:45.291150Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"display_sample_images(path_monet)","metadata":{"execution":{"iopub.status.busy":"2025-10-16T05:02:49.101213Z","iopub.execute_input":"2025-10-16T05:02:49.101546Z","iopub.status.idle":"2025-10-16T05:02:49.717388Z","shell.execute_reply.started":"2025-10-16T05:02:49.101507Z","shell.execute_reply":"2025-10-16T05:02:49.716551Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"display_sample_images(path_photo)","metadata":{"execution":{"iopub.status.busy":"2025-10-16T05:02:50.205333Z","iopub.execute_input":"2025-10-16T05:02:50.206113Z","iopub.status.idle":"2025-10-16T05:02:51.027172Z","shell.execute_reply.started":"2025-10-16T05:02:50.206084Z","shell.execute_reply":"2025-10-16T05:02:51.026367Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"As we can see monet paintings have that distinctive style that many of us instantly relate to the famous painter called Monet. The photos on the other hand seem to be taken with a normal camera. Both types of images are colored ones, not just black and white.\n\nWe can verify the stated image sizes from the kaggle description by inspecting the width and height of both the monet paintings as well as the real photos.","metadata":{}},{"cell_type":"code","source":"# function to analyze and show the image sizes in height and width \ndef analyze_image_sizes(image_dir):\n    image_sizes = []\n    \n    # determine image height and width\n    for image_filename in os.listdir(image_dir):\n        img = Image.open(os.path.join(image_dir, image_filename))\n        width, height = img.size\n        image_sizes.append((width, height))\n    \n    widths, heights = zip(*image_sizes)\n    \n    # plot the image widths\n    plt.figure(figsize=(10, 4))\n    plt.subplot(1, 2, 1)\n    plt.hist(widths, bins=30, color='blue', alpha=0.7)\n    plt.xlabel('Image Width')\n    plt.ylabel('Frequency')\n    plt.title('Distribution of Image Widths')\n    \n    # plot the image heights\n    plt.subplot(1, 2, 2)\n    plt.hist(heights, bins=30, color='green', alpha=0.7)\n    plt.xlabel('Image Height')\n    plt.ylabel('Frequency')\n    plt.title('Distribution of Image Heights')\n    \n    plt.tight_layout()\n    plt.show()","metadata":{"execution":{"iopub.status.busy":"2025-10-16T05:02:55.494479Z","iopub.execute_input":"2025-10-16T05:02:55.494837Z","iopub.status.idle":"2025-10-16T05:02:55.501245Z","shell.execute_reply.started":"2025-10-16T05:02:55.494812Z","shell.execute_reply":"2025-10-16T05:02:55.500204Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"analyze_image_sizes(path_monet)","metadata":{"execution":{"iopub.status.busy":"2025-10-16T05:03:00.750146Z","iopub.execute_input":"2025-10-16T05:03:00.750945Z","iopub.status.idle":"2025-10-16T05:03:03.423375Z","shell.execute_reply.started":"2025-10-16T05:03:00.750916Z","shell.execute_reply":"2025-10-16T05:03:03.422562Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"analyze_image_sizes(path_photo)","metadata":{"execution":{"iopub.status.busy":"2025-10-16T05:03:03.425026Z","iopub.execute_input":"2025-10-16T05:03:03.425684Z","iopub.status.idle":"2025-10-16T05:03:59.681401Z","shell.execute_reply.started":"2025-10-16T05:03:03.425641Z","shell.execute_reply":"2025-10-16T05:03:59.680533Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"As we can see in the histograms all images are indeed of size 256x256 pixels. Next, we will have a look at how the different color channels are distributed in the images. ","metadata":{}},{"cell_type":"code","source":"# function to display the present color channels in an image alongside the image itself\ndef display_color_channels(path):\n    plt.figure(figsize=(16, 4))\n    \n    img = cv2.imread(path)\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) \n    plt.subplot(1, 4, 1)\n    plt.imshow(img)\n    plt.axis(\"off\")\n    \n    colors = [\"red\", \"green\", \"blue\"]\n    for i in range(len(colors)):\n        plt.subplot(1, 4, i + 2)\n        plt.hist(\n            img[:, :, i].reshape(-1),\n            bins=50,\n            color=colors[i],\n            density=True\n        )\n        plt.xlim(0, 255)\n        plt.xticks([])\n        plt.yticks([])\n     \n    plt.show()","metadata":{"execution":{"iopub.status.busy":"2025-10-16T05:04:16.574865Z","iopub.execute_input":"2025-10-16T05:04:16.575552Z","iopub.status.idle":"2025-10-16T05:04:16.581275Z","shell.execute_reply.started":"2025-10-16T05:04:16.575524Z","shell.execute_reply":"2025-10-16T05:04:16.580364Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# show monet example color channels\ndisplay_color_channels(path_monet + os.listdir(path_monet)[0])\ndisplay_color_channels(path_monet + os.listdir(path_monet)[1])\ndisplay_color_channels(path_monet + os.listdir(path_monet)[2])","metadata":{"execution":{"iopub.status.busy":"2025-10-16T05:04:25.358274Z","iopub.execute_input":"2025-10-16T05:04:25.359170Z","iopub.status.idle":"2025-10-16T05:04:26.443873Z","shell.execute_reply.started":"2025-10-16T05:04:25.359139Z","shell.execute_reply":"2025-10-16T05:04:26.443082Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# show photo example color channels\ndisplay_color_channels(path_photo + os.listdir(path_photo)[0])\ndisplay_color_channels(path_photo + os.listdir(path_photo)[1])\ndisplay_color_channels(path_photo + os.listdir(path_photo)[2])","metadata":{"execution":{"iopub.status.busy":"2025-10-16T05:04:26.445131Z","iopub.execute_input":"2025-10-16T05:04:26.445415Z","iopub.status.idle":"2025-10-16T05:04:27.593775Z","shell.execute_reply.started":"2025-10-16T05:04:26.445390Z","shell.execute_reply":"2025-10-16T05:04:27.593006Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The above plots show the color channels for three monet paintings respectively photos. We could now ask the question if a certain color channel is more present in the monet paintings than in the photos.","metadata":{}},{"cell_type":"code","source":"# function to calculate the average channel values (Red, Green, Blue)\ndef calculate_average_channel_values(image_dir):\n    \n    # init each channel (R, G, B)\n    channel_sum = np.zeros(3) \n    total_images = 0\n\n    for image_filename in os.listdir(image_dir):\n        img = Image.open(os.path.join(image_dir, image_filename))\n        img_array = np.array(img)\n        \n        # make sure it is a color image\n        if img_array.shape == (256, 256, 3):\n            channel_sum += np.sum(img_array, axis=(0, 1))\n            total_images += 1\n\n    average_channel_values = channel_sum / total_images\n    return average_channel_values\n\n# calculate average channel values\naverage_monet_values = calculate_average_channel_values(path_monet)\naverage_photos_values = calculate_average_channel_values(path_photo)\n\n# ceate bar charts to compare average channel values\nchannels = ['Red', 'Green', 'Blue']\nx = np.arange(len(channels))\nwidth = 0.35\n\nplt.bar(x - width/2, average_monet_values, width, label='Monet Paintings', color='b', alpha=0.7)\nplt.bar(x + width/2, average_photos_values, width, label='Normal Photos', color='g', alpha=0.7)\n\nplt.xlabel('Color Channel')\nplt.ylabel('Average Value')\nplt.title('Average Channel Values: Monet Paintings vs. Normal Photos')\nplt.xticks(x, channels)\nplt.legend()\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2025-10-16T05:05:14.799304Z","iopub.execute_input":"2025-10-16T05:05:14.800128Z","iopub.status.idle":"2025-10-16T05:05:34.178146Z","shell.execute_reply.started":"2025-10-16T05:05:14.800097Z","shell.execute_reply":"2025-10-16T05:05:34.177036Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The bar plot shows us that all color channels (Red, Green, Blue) have higher average values in Monet's paintings compared to normal photos. This makes sense since Monet is known for his impressionist style which often uses intense colors to capture the play of light and atmosphere. They tend to have a richer and more saturated color palette. This characteristic aligns with Monet's artistic style, where he emphasized the use of color to show emotions and capture the essence of a scene rather than adhering to strict color realism. Now that we are fimiliar with the given data we can begin with model building.","metadata":{}},{"cell_type":"markdown","source":"# Model Building","metadata":{}},{"cell_type":"code","source":"device = 'cuda' if torch.cuda.is_available() else 'cpu'\nprint(\"Device used for modeling: \", device)","metadata":{"execution":{"iopub.status.busy":"2025-10-16T05:05:34.180078Z","iopub.execute_input":"2025-10-16T05:05:34.181031Z","iopub.status.idle":"2025-10-16T05:05:34.220751Z","shell.execute_reply.started":"2025-10-16T05:05:34.180990Z","shell.execute_reply":"2025-10-16T05:05:34.219796Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Dataset and Dataloader\n\nFirst we create a dataset for training our generative model. It takes the two directories where our monet paintings and photos can be found. The class loads and preprocesses the images, applying resizing and optionally normalization to create a consistent input for our neural network. With the __getitem__ method, we can randomly select a Monet-style image and the corresponding photo, processes them, and returns them as tensors. The __len__ method returns the length of the smaller of the two sets of images, ensuring that the dataset is limited by the number of available pairs.","metadata":{}},{"cell_type":"code","source":"class ImageDataset(Dataset):\n    def __init__(self, path_monet, path_photo, size=(256, 256), normalize=True):\n        super().__init__()\n        self.monet_dir = path_monet\n        self.photo_dir = path_photo\n        self.monet_idx = dict()\n        self.photo_idx = dict()\n        \n        # normalize the images for consistency\n        if normalize:\n            self.transform = transforms.Compose([\n                transforms.Resize(size),\n                transforms.ToTensor(),\n                transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))                                \n            ])\n        else:\n            self.transform = transforms.Compose([\n                transforms.Resize(size),\n                transforms.ToTensor()])\n            \n        for i, fl in enumerate(os.listdir(self.monet_dir)):\n            self.monet_idx[i] = fl\n        for i, fl in enumerate(os.listdir(self.photo_dir)):\n            self.photo_idx[i] = fl\n\n    # randomnly select a pair of monet painting and photo\n    def __getitem__(self, idx):\n        rand_idx = int(np.random.uniform(0, len(self.monet_idx.keys())))\n        photo_path = os.path.join(self.photo_dir, self.photo_idx[rand_idx])\n        monet_path = os.path.join(self.monet_dir, self.monet_idx[idx])\n        photo_img = Image.open(photo_path)\n        photo_img = self.transform(photo_img)\n        monet_img = Image.open(monet_path)\n        monet_img = self.transform(monet_img)\n        return photo_img, monet_img\n\n    # ensure same amount of available photos\n    def __len__(self):\n        return min(len(self.monet_idx.keys()), len(self.photo_idx.keys()))","metadata":{"execution":{"iopub.status.busy":"2025-10-16T05:05:37.165290Z","iopub.execute_input":"2025-10-16T05:05:37.166007Z","iopub.status.idle":"2025-10-16T05:05:37.173975Z","shell.execute_reply.started":"2025-10-16T05:05:37.165977Z","shell.execute_reply":"2025-10-16T05:05:37.173161Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"img_ds = ImageDataset(path_monet, path_photo)\nimg_dl = DataLoader(img_ds, batch_size=1, pin_memory=True)","metadata":{"execution":{"iopub.status.busy":"2025-10-16T05:05:41.625370Z","iopub.execute_input":"2025-10-16T05:05:41.626207Z","iopub.status.idle":"2025-10-16T05:05:41.635463Z","shell.execute_reply.started":"2025-10-16T05:05:41.626179Z","shell.execute_reply":"2025-10-16T05:05:41.634622Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The following is a helper method that performs the reverse operation of normalizing an image. Since our model only works with normalized versions of the images, this is needed for plotting the original version of the images after modeling them.","metadata":{}},{"cell_type":"code","source":"# reverse the normalization process\ndef reverse_normalize(img, mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]):\n    for image, mu, std in zip(img, mean, std):\n        image.mul_(std).add_(std)\n        \n    return img","metadata":{"execution":{"iopub.status.busy":"2025-10-16T05:05:43.181279Z","iopub.execute_input":"2025-10-16T05:05:43.181622Z","iopub.status.idle":"2025-10-16T05:05:43.186437Z","shell.execute_reply.started":"2025-10-16T05:05:43.181596Z","shell.execute_reply":"2025-10-16T05:05:43.185558Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Model building blocks\n\nThe upsampling part is important for increasing the spatial resolution of an image. It is typically used in the generator network to gradually upsample the feature maps, which helps in generating higher-resolution output images from lower-resolution input images.\n\nThe purpose of the following method is to define the logic for upsampling within a neural network. The choice of whether to use dropout or not is controlled by the use_dropout argument, providing more flexibility in designing the generator architecture.","metadata":{}},{"cell_type":"code","source":"# method for upsampling feature maps\ndef Upsample(in_ch, out_ch, use_dropout=True, dropout_ratio=0.5):\n    if use_dropout:\n        return nn.Sequential(\n            nn.ConvTranspose2d(in_ch, out_ch, 3, stride=2, padding=1, output_padding=1),\n            nn.InstanceNorm2d(out_ch),\n            nn.Dropout(dropout_ratio),\n            nn.GELU())\n    else:\n        return nn.Sequential(\n            nn.ConvTranspose2d(in_ch, out_ch, 3, stride=2, padding=1, output_padding=1),\n            nn.InstanceNorm2d(out_ch),\n            nn.GELU())","metadata":{"execution":{"iopub.status.busy":"2025-10-16T05:05:46.613232Z","iopub.execute_input":"2025-10-16T05:05:46.613574Z","iopub.status.idle":"2025-10-16T05:05:46.618634Z","shell.execute_reply.started":"2025-10-16T05:05:46.613548Z","shell.execute_reply":"2025-10-16T05:05:46.617828Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Next, we need the convolutional layer which is responsible for extracting features from the input images. These layers apply convolution operations to process the image data and capture important patterns and details. They are used both in the generator and discriminator networks.\n\nThe below method returns a PyTorch Sequential object that can be used to create the architecture of both the generator and discriminator in a CycleGAN model. The flexibility provided by the function's arguments allows for different architectural choices.","metadata":{}},{"cell_type":"code","source":"# define a convolutional layer with options for padding and more\ndef Convlayer(in_ch, out_ch, kernel_size=3, stride=2, use_leaky=True, use_inst_norm=True, use_pad=True):\n    if use_pad:\n        conv = nn.Conv2d(in_ch, out_ch, kernel_size, stride, 1, bias=True)\n    else:\n        conv = nn.Conv2d(in_ch, out_ch, kernel_size, stride, 0, bias=True)\n\n    if use_leaky:\n        actv = nn.LeakyReLU(negative_slope=0.2, inplace=True)\n    else:\n        actv = nn.GELU()\n\n    if use_inst_norm:\n        norm = nn.InstanceNorm2d(out_ch)\n    else:\n        norm = nn.BatchNorm2d(out_ch)\n\n    return nn.Sequential(conv, norm, actv)","metadata":{"execution":{"iopub.status.busy":"2025-10-16T05:05:50.620902Z","iopub.execute_input":"2025-10-16T05:05:50.621644Z","iopub.status.idle":"2025-10-16T05:05:50.627067Z","shell.execute_reply.started":"2025-10-16T05:05:50.621616Z","shell.execute_reply":"2025-10-16T05:05:50.626139Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The Residual Block is part of the generator network. Its purpose is to make the network learn residual functions, which help preserve important details and prevent vanishing gradients during training. This is particularly useful for maintaining image quality and consistency during the translation process.","metadata":{}},{"cell_type":"code","source":"# defines the residual block for the generator part\nclass Resblock(nn.Module):\n    def __init__(self, in_features, use_dropout=True, dropout_ratio=0.5):\n        super().__init__()\n        layers = list()\n        layers.append(nn.ReflectionPad2d(1))\n        layers.append(Convlayer(in_features, in_features, 3, 1, False, use_pad=False))\n        layers.append(nn.Dropout(dropout_ratio))\n        layers.append(nn.ReflectionPad2d(1))\n        layers.append(nn.Conv2d(in_features, in_features, 3, 1, padding=0, bias=True))\n        layers.append(nn.InstanceNorm2d(in_features))\n        self.res = nn.Sequential(*layers)\n\n    def forward(self, x):\n        return x + self.res(x)","metadata":{"execution":{"iopub.status.busy":"2025-10-16T05:05:56.301275Z","iopub.execute_input":"2025-10-16T05:05:56.301624Z","iopub.status.idle":"2025-10-16T05:05:56.307354Z","shell.execute_reply.started":"2025-10-16T05:05:56.301599Z","shell.execute_reply":"2025-10-16T05:05:56.306459Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The generator is the component responsible for translating images from one domain to another. It takes an input image (e.g., a photo) and transforms it into an image that mimics the style or characteristics of another domain (e.g., a Monet painting). The purpose of the generator is to learn a mapping between these domains and generate high-quality images in the target domain.\n\nThe class below represents this generator and defines the architecture and operations needed to perform the image translation task. It makes use of the Upsampling, Convolutional Layers, and ResBlock which we defined above.","metadata":{}},{"cell_type":"code","source":"# defines the generator class built upon convolutional layers\nclass Generator(nn.Module):\n    def __init__(self, in_ch, out_ch, num_res_blocks=6):\n        super().__init__()\n        model = list()\n        model.append(nn.ReflectionPad2d(3))\n        model.append(Convlayer(in_ch, 64, 7, 1, False, True, False))\n        model.append(Convlayer(64, 128, 3, 2, False))\n        model.append(Convlayer(128, 256, 3, 2, False))\n        for _ in range(num_res_blocks):\n            model.append(Resblock(256))\n        model.append(Upsample(256, 128))\n        model.append(Upsample(128, 64))\n        model.append(nn.ReflectionPad2d(3))\n        model.append(nn.Conv2d(64, out_ch, kernel_size=7, padding=0))\n        model.append(nn.Tanh())\n\n        self.gen = nn.Sequential(*model)\n\n    def forward(self, x):\n        return self.gen(x)","metadata":{"execution":{"iopub.status.busy":"2025-10-16T05:06:03.566646Z","iopub.execute_input":"2025-10-16T05:06:03.567427Z","iopub.status.idle":"2025-10-16T05:06:03.573663Z","shell.execute_reply.started":"2025-10-16T05:06:03.567392Z","shell.execute_reply":"2025-10-16T05:06:03.572683Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The discriminator is the component responsible for distinguishing between real and generated images. Its purpose is to provide feedback to the generator by assessing the realism of the generated images and helping the generator improve over time. The discriminator is trained to classify input images as either real (belonging to the target domain) or fake (generated by the generator).","metadata":{}},{"cell_type":"code","source":"# defines the discriminator component\nclass Discriminator(nn.Module):\n    def __init__(self, in_ch, num_layers=4):\n        super().__init__()\n        model = list()\n        model.append(nn.Conv2d(in_ch, 64, 4, stride=2, padding=1))\n        model.append(nn.LeakyReLU(0.2, inplace=True))\n        for i in range(1, num_layers):\n            in_chs = 64 * 2**(i-1)\n            out_chs = in_chs * 2\n            if i == num_layers -1:\n                model.append(Convlayer(in_chs, out_chs, 4, 1))\n            else:\n                model.append(Convlayer(in_chs, out_chs, 4, 2))\n        model.append(nn.Conv2d(512, 1, kernel_size=4, stride=1, padding=1))\n        self.disc = nn.Sequential(*model)\n\n    def forward(self, x):\n        return self.disc(x)","metadata":{"execution":{"iopub.status.busy":"2025-10-16T05:06:06.478373Z","iopub.execute_input":"2025-10-16T05:06:06.478745Z","iopub.status.idle":"2025-10-16T05:06:06.485352Z","shell.execute_reply.started":"2025-10-16T05:06:06.478720Z","shell.execute_reply":"2025-10-16T05:06:06.484473Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"As with deep learning in general we also need a weight initialization in CycleGAN to set the initial values of the model's weights in a way that helps improve the training process and convergence speed. This is really important since a proper weight initialization can prevent issues like vanishing gradients and allow the model to learn more effectively.","metadata":{}},{"cell_type":"code","source":"# initalize with normally distributed weights around mean 0 and standard deviation of 0.02\ndef init_weights(net, init_type='normal', std=0.02):\n    def init_func(m):\n        classname = m.__class__.__name__\n        if hasattr(m, 'weight') and (classname.find('Conv') != -1 or classname.find('Linear') != -1):\n            init.normal_(m.weight.data, 0.0, std)\n            if hasattr(m, 'bias') and m.bias is not None:\n                init.constant_(m.bias.data, 0.0)\n        elif classname.find('BatchNorm2d') != -1:\n            init.normal_(m.weight.data, 1.0, std)\n            init.constant_(m.bias.data, 0.0)\n    net.apply(init_func)","metadata":{"execution":{"iopub.status.busy":"2025-10-16T05:06:10.157330Z","iopub.execute_input":"2025-10-16T05:06:10.157969Z","iopub.status.idle":"2025-10-16T05:06:10.163696Z","shell.execute_reply.started":"2025-10-16T05:06:10.157939Z","shell.execute_reply":"2025-10-16T05:06:10.162857Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Cycle GAN\n\nFirst, we will need some additional helper methods that are explained and defined below.\n\nThe purpose of the first function is to provide a convenient way to control whether the parameters of a list of models should be trainable (require gradients) or frozen (not require gradients) during training. This can be useful in scenarios where you want to fine-tune certain parts of a pre-trained model while keeping others fixed, or when you want to selectively train or freeze specific model components during different training phases.","metadata":{}},{"cell_type":"code","source":"# decide whether certain model parameters should be trainable or not\ndef update_req_grad(models, requires_grad=True):\n    for model in models:\n        for param in model.parameters():\n            param.requires_grad = requires_grad","metadata":{"execution":{"iopub.status.busy":"2025-10-16T05:06:14.469885Z","iopub.execute_input":"2025-10-16T05:06:14.470707Z","iopub.status.idle":"2025-10-16T05:06:14.474689Z","shell.execute_reply.started":"2025-10-16T05:06:14.470676Z","shell.execute_reply":"2025-10-16T05:06:14.473810Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The purpose of the \"sample fake\" mechanism in CycleGAN is to help stabilize training by sampling a subset of generated fake images and using them as inputs to the discriminator. This approach is used to reduce the variability in the inputs to the discriminator, which can help avoid large oscillations in the discriminator's outputs from one training iteration to the next. By doing so, it can make the training process more stable and lead to better convergence.\n\nFor this reason we provide a method seen below to control the number of fake images used for each training iteration and introduce a level of randomness in the selection of fake images to stabilize training in CycleGAN.","metadata":{}},{"cell_type":"code","source":"# class to save 50 generated fake images and sample through them to feed discriminator\nclass sample_fake(object):\n    def __init__(self, max_imgs=50):\n        self.max_imgs = max_imgs\n        self.cur_img = 0\n        self.imgs = list()\n\n    def __call__(self, imgs):\n        ret = list()\n        for img in imgs:\n            if self.cur_img < self.max_imgs:\n                self.imgs.append(img)\n                ret.append(img)\n                self.cur_img += 1\n            else:\n                if np.random.ranf() > 0.5:\n                    idx = np.random.randint(0, self.max_imgs)\n                    ret.append(self.imgs[idx])\n                    self.imgs[idx] = img\n                else:\n                    ret.append(img)\n        return ret","metadata":{"execution":{"iopub.status.busy":"2025-10-16T05:06:19.893353Z","iopub.execute_input":"2025-10-16T05:06:19.894055Z","iopub.status.idle":"2025-10-16T05:06:19.899856Z","shell.execute_reply.started":"2025-10-16T05:06:19.894025Z","shell.execute_reply":"2025-10-16T05:06:19.898623Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The purpose of learning rate scheduling is to dynamically adjust the learning rate during training to optimize the convergence of the neural network. Learning rate scheduling typically involves reducing the learning rate over time to help the model converge more smoothly and reach a better minimum of the loss function.\n\nBy using this linear decay strategy, the learning rate gradually decreases as training progresses beyond **self.decay_epochs**. It's a simple yet effective way to improve training stability.","metadata":{}},{"cell_type":"code","source":"# class to schedule the learning rate during training\nclass lr_sched():\n    def __init__(self, decay_epochs=100, total_epochs=200):\n        self.decay_epochs = decay_epochs\n        self.total_epochs = total_epochs\n\n    def step(self, epoch_num):\n        if epoch_num <= self.decay_epochs:\n            return 1.0\n        else:\n            fract = (epoch_num - self.decay_epochs)  / (self.total_epochs - self.decay_epochs)\n            return 1.0 - fract","metadata":{"execution":{"iopub.status.busy":"2025-10-16T05:06:25.518450Z","iopub.execute_input":"2025-10-16T05:06:25.519274Z","iopub.status.idle":"2025-10-16T05:06:25.524054Z","shell.execute_reply.started":"2025-10-16T05:06:25.519247Z","shell.execute_reply":"2025-10-16T05:06:25.523235Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Finally, we define a class that is able to collect and track important statistics, like loss values and iteration numbers, over the course of training. These statistics can be used for various purposes, such as monitoring the training progress, visualizing loss curves, or making decisions about model training.","metadata":{}},{"cell_type":"code","source":"# class for saving some training metrics\nclass AvgStats(object):\n    def __init__(self):\n        self.reset()\n        \n    def reset(self):\n        self.losses =[]\n        self.its = []\n        \n    def append(self, loss, it):\n        self.losses.append(loss)\n        self.its.append(it)","metadata":{"execution":{"iopub.status.busy":"2025-10-16T05:06:26.061401Z","iopub.execute_input":"2025-10-16T05:06:26.061752Z","iopub.status.idle":"2025-10-16T05:06:26.066713Z","shell.execute_reply.started":"2025-10-16T05:06:26.061728Z","shell.execute_reply":"2025-10-16T05:06:26.065667Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The following class defines the Cycle GAN itself together with the required training loop. It trains the model to translate images between the two domains (photos and Monet paintings). It includes the training logic for both generators and discriminators, learning rate scheduling, and tracking of training statistics. The model's losses are backpropagated, and the parameters are updated accordingly during training.","metadata":{}},{"cell_type":"code","source":"# defines the Cycle GAN and its training loop\nclass CycleGAN(object):\n    def __init__(self, in_ch, out_ch, epochs, device, start_lr=2e-4, lmbda=10, idt_coef=0.5, decay_epoch=0):\n        self.epochs = epochs\n        self.decay_epoch = decay_epoch if decay_epoch > 0 else int(self.epochs/2)\n        self.lmbda = lmbda\n        self.idt_coef = idt_coef\n        self.device = device\n        self.gen_mtp = Generator(in_ch, out_ch)\n        self.gen_ptm = Generator(in_ch, out_ch)\n        self.desc_m = Discriminator(in_ch)\n        self.desc_p = Discriminator(in_ch)\n        self.init_models()\n        self.mse_loss = nn.MSELoss()\n        self.l1_loss = nn.L1Loss()\n        self.adam_gen = torch.optim.Adam(itertools.chain(self.gen_mtp.parameters(), self.gen_ptm.parameters()),\n                                         lr = start_lr, betas=(0.5, 0.999))\n        self.adam_desc = torch.optim.Adam(itertools.chain(self.desc_m.parameters(), self.desc_p.parameters()),\n                                          lr=start_lr, betas=(0.5, 0.999))\n        self.sample_monet = sample_fake()\n        self.sample_photo = sample_fake()\n        gen_lr = lr_sched(self.decay_epoch, self.epochs)\n        desc_lr = lr_sched(self.decay_epoch, self.epochs)\n        self.gen_lr_sched = torch.optim.lr_scheduler.LambdaLR(self.adam_gen, gen_lr.step)\n        self.desc_lr_sched = torch.optim.lr_scheduler.LambdaLR(self.adam_desc, desc_lr.step)\n        self.gen_stats = AvgStats()\n        self.desc_stats = AvgStats()\n        \n    def init_models(self):\n        init_weights(self.gen_mtp)\n        init_weights(self.gen_ptm)\n        init_weights(self.desc_m)\n        init_weights(self.desc_p)\n        self.gen_mtp = self.gen_mtp.to(self.device)\n        self.gen_ptm = self.gen_ptm.to(self.device)\n        self.desc_m = self.desc_m.to(self.device)\n        self.desc_p = self.desc_p.to(self.device)\n        \n    def train(self, photo_dl):\n        for epoch in range(self.epochs):\n            start_time = time.time()\n            avg_gen_loss = 0.0\n            avg_desc_loss = 0.0\n            t = tqdm(photo_dl, leave=False, total=photo_dl.__len__())\n            for i, (photo_real, monet_real) in enumerate(t):\n                photo_img, monet_img = photo_real.to(device), monet_real.to(device)\n                update_req_grad([self.desc_m, self.desc_p], False)\n                self.adam_gen.zero_grad()\n\n                # forward pass through generator\n                fake_photo = self.gen_mtp(monet_img)\n                fake_monet = self.gen_ptm(photo_img)\n\n                cycl_monet = self.gen_ptm(fake_photo)\n                cycl_photo = self.gen_mtp(fake_monet)\n\n                id_monet = self.gen_ptm(monet_img)\n                id_photo = self.gen_mtp(photo_img)\n\n                # generator losses\n                idt_loss_monet = self.l1_loss(id_monet, monet_img) * self.lmbda * self.idt_coef\n                idt_loss_photo = self.l1_loss(id_photo, photo_img) * self.lmbda * self.idt_coef\n\n                cycle_loss_monet = self.l1_loss(cycl_monet, monet_img) * self.lmbda\n                cycle_loss_photo = self.l1_loss(cycl_photo, photo_img) * self.lmbda\n\n                monet_desc = self.desc_m(fake_monet)\n                photo_desc = self.desc_p(fake_photo)\n\n                real = torch.ones(monet_desc.size()).to(self.device)\n\n                adv_loss_monet = self.mse_loss(monet_desc, real)\n                adv_loss_photo = self.mse_loss(photo_desc, real)\n\n                # total generator loss\n                total_gen_loss = cycle_loss_monet + adv_loss_monet\\\n                              + cycle_loss_photo + adv_loss_photo\\\n                              + idt_loss_monet + idt_loss_photo\n                \n                avg_gen_loss += total_gen_loss.item()\n\n                # backward pass\n                total_gen_loss.backward()\n                self.adam_gen.step()\n\n                # forward pass through Descriminator\n                update_req_grad([self.desc_m, self.desc_p], True)\n                self.adam_desc.zero_grad()\n\n                fake_monet = self.sample_monet([fake_monet.cpu().data.numpy()])[0]\n                fake_photo = self.sample_photo([fake_photo.cpu().data.numpy()])[0]\n                fake_monet = torch.tensor(fake_monet).to(self.device)\n                fake_photo = torch.tensor(fake_photo).to(self.device)\n\n                monet_desc_real = self.desc_m(monet_img)\n                monet_desc_fake = self.desc_m(fake_monet)\n                photo_desc_real = self.desc_p(photo_img)\n                photo_desc_fake = self.desc_p(fake_photo)\n\n                real = torch.ones(monet_desc_real.size()).to(self.device)\n                fake = torch.zeros(monet_desc_fake.size()).to(self.device)\n\n                # descriminator losses\n                monet_desc_real_loss = self.mse_loss(monet_desc_real, real)\n                monet_desc_fake_loss = self.mse_loss(monet_desc_fake, fake)\n                photo_desc_real_loss = self.mse_loss(photo_desc_real, real)\n                photo_desc_fake_loss = self.mse_loss(photo_desc_fake, fake)\n\n                monet_desc_loss = (monet_desc_real_loss + monet_desc_fake_loss) / 2\n                photo_desc_loss = (photo_desc_real_loss + photo_desc_fake_loss) / 2\n                total_desc_loss = monet_desc_loss + photo_desc_loss\n                avg_desc_loss += total_desc_loss.item()\n\n                # backward\n                monet_desc_loss.backward()\n                photo_desc_loss.backward()\n                self.adam_desc.step()\n                \n                t.set_postfix(gen_loss=total_gen_loss.item(), desc_loss=total_desc_loss.item())\n            \n            avg_gen_loss /= photo_dl.__len__()\n            avg_desc_loss /= photo_dl.__len__()\n            time_req = time.time() - start_time\n            \n            self.gen_stats.append(avg_gen_loss, time_req)\n            self.desc_stats.append(avg_desc_loss, time_req)\n            \n            print(\"Epoch %d  -  Generator Loss:%f  -  Discriminator Loss:%f\" % (epoch+1, avg_gen_loss, avg_desc_loss))\n      \n            self.gen_lr_sched.step()\n            self.desc_lr_sched.step()","metadata":{"execution":{"iopub.status.busy":"2025-10-16T05:06:33.759301Z","iopub.execute_input":"2025-10-16T05:06:33.759638Z","iopub.status.idle":"2025-10-16T05:06:33.779611Z","shell.execute_reply.started":"2025-10-16T05:06:33.759611Z","shell.execute_reply":"2025-10-16T05:06:33.778738Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Training process","metadata":{}},{"cell_type":"code","source":"gan = CycleGAN(3, 3, 100, device)\ngan.train(img_dl)","metadata":{"execution":{"iopub.status.busy":"2025-10-16T05:06:41.382052Z","iopub.execute_input":"2025-10-16T05:06:41.382682Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# plot losses\ndef plot_gan_loss():\n    plt.xlabel(\"Epochs\")\n    plt.ylabel(\"Losses\")\n    plt.plot(gan.gen_stats.losses, 'r', label='Generator Loss')\n    plt.plot(gan.desc_stats.losses, 'b', label='Descriminator Loss')\n    plt.legend()\n    plt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plot_gan_loss()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"We can see that both loss functions gradually decrease over time which is what we would expect from a working model. In order to see the actual results we can plot same random photos and the corresponding Monet-esque Photos created by our model.","metadata":{}},{"cell_type":"code","source":"# plot realistic photo and created monet-style image \n_, ax = plt.subplots(3, 2, figsize=(12, 12))\nfor i in range(3):\n    photo_img, _ = next(iter(img_dl))\n    pred_monet = gan.gen_ptm(photo_img.to(device)).cpu().detach()\n    photo_img = reverse_normalize(photo_img)\n    pred_monet = reverse_normalize(pred_monet)\n    \n    ax[i, 0].imshow(photo_img[0].permute(1, 2, 0))\n    ax[i, 1].imshow(pred_monet[0].permute(1, 2, 0))\n    ax[i, 0].set_title(\"Input Photo\")\n    ax[i, 1].set_title(\"Monet-esque Photo\")\n    ax[i, 0].axis(\"off\")\n    ax[i, 1].axis(\"off\")\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Hyperparameter Tuning","metadata":{}},{"cell_type":"markdown","source":"# Results and Analysis","metadata":{}},{"cell_type":"code","source":"# class to store and access the photos (similar to the paintings done before)\nclass PhotoDataset(Dataset):\n    def __init__(self, photo_dir, size=(256, 256), normalize=True):\n        super().__init__()\n        self.photo_dir = photo_dir\n        self.photo_idx = dict()\n        \n        # normalize images if needed\n        if normalize:\n            self.transform = transforms.Compose([\n                transforms.Resize(size),\n                transforms.ToTensor(),\n                transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))                                \n            ])\n        else:\n            self.transform = transforms.Compose([\n                transforms.Resize(size),\n                transforms.ToTensor()                               \n            ])\n        for i, fl in enumerate(os.listdir(self.photo_dir)):\n            self.photo_idx[i] = fl\n\n    # retrieve an image\n    def __getitem__(self, idx):\n        photo_path = os.path.join(self.photo_dir, self.photo_idx[idx])\n        photo_img = Image.open(photo_path)\n        photo_img = self.transform(photo_img)\n        return photo_img\n\n    def __len__(self):\n        return len(self.photo_idx.keys())","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# make a dataloader and the required directory for storing the images to be created\nph_ds = PhotoDataset(path_photo)\nph_dl = DataLoader(ph_ds, batch_size=1, pin_memory=True)\n!mkdir ../images\ntrans = transforms.ToPILImage()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# use model to create a monet style image\nt = tqdm(ph_dl, leave=False, total=ph_dl.__len__())\nfor i, photo in enumerate(t):\n    with torch.no_grad():\n        pred_monet = gan.gen_ptm(photo.to(device)).cpu().detach()\n    \n    # revert the normalization process to obtain the original image style\n    pred_monet = reverse_normalize(pred_monet)\n    img = trans(pred_monet[0]).convert(\"RGB\")\n    \n    # store the image\n    img.save(\"../images/\" + str(i+1) + \".jpg\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# create a zip file from all created images\nshutil.make_archive(\"/kaggle/working/images\", 'zip', \"/kaggle/images\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"display_sample_images(\"../images/\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# display best hyperparameters of model\nlearning_rate = [2e-4]\nbeta_1 = [0.5]\nbeta_2 = [0.999]  \ndf_results = pd.DataFrame({'Learning Rate': learning_rate, 'Beta 1': beta_1, 'Beta 2': beta_2})\nprint(df_results)","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}