{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.12.13"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"databundleVersionId":1475600,"mountSlug":"competitions/gan-getting-started","sourceId":21755,"sourceType":"competition"}],"dockerImageVersionId":31401,"isGpuEnabled":true,"isInternetEnabled":true,"language":"python","sourceType":"notebook"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# ITE 153 - Machine Learning Project: Monet Paintings (CycleGAN)\n\n## 1. Introduction and Problem Definition\nIn this project, we address the Kaggle challenge **\"I'm Something of a Painter Myself\"**. The objective is to perform unpaired image-to-image translation. Specifically, we want to build a model that can take any landscape photograph and translate it into the artistic style of the famous painter Claude Monet, while preserving the structural content of the original photo.\n\nBecause the dataset is **unpaired** (i.e., we don't have pairs of \"photo of a place\" and \"Monet painting of that exact same place\"), standard supervised training is not possible. Instead, we implement a **CycleGAN (Cycle-Consistent Generative Adversarial Network)**, which learns to translate between the two domains (Photos and Monet paintings) using a combination of adversarial losses, cycle-consistency losses, and identity losses.\n\nDataset source: [Kaggle Monet GAN Competition](https://www.kaggle.com/competitions/gan-getting-started)","metadata":{}},{"cell_type":"code","source":"import os\nimport glob\nimport random\nimport shutil\nimport zipfile\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom PIL import Image\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nimport torchvision.transforms as transforms\nfrom torch.utils.data import Dataset, DataLoader\n\nprint(f\"PyTorch version: {torch.__version__ if hasattr(torch, '__version__') else 'unknown'}\")\nprint(f\"CUDA available: {torch.cuda.is_available() if hasattr(torch, 'cuda') else False}\")","metadata":{"execution":{"iopub.status.busy":"2026-06-02T15:21:17.726229Z","iopub.execute_input":"2026-06-02T15:21:17.726947Z","iopub.status.idle":"2026-06-02T15:21:26.035989Z","shell.execute_reply.started":"2026-06-02T15:21:17.726916Z","shell.execute_reply":"2026-06-02T15:21:26.035108Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Load the Dataset\nWe setup directories. On Kaggle, the input dataset path is `/kaggle/input/gan-getting-started/`. We write our data loader to work with standard JPG image files.","metadata":{}},{"cell_type":"code","source":"KAGGLE_PATH = \"/kaggle/input/competitions/gan-getting-started/\"\nLOCAL_PATH = \"./monet_dummy_data/\"\n\nif os.path.exists(KAGGLE_PATH):\n    DATA_DIR = KAGGLE_PATH\nelse:\n    DATA_DIR = LOCAL_PATH\n    # Generate some mock directories locally to prevent loader errors\n    os.makedirs(os.path.join(DATA_DIR, \"monet_jpg\"), exist_ok=True)\n    os.makedirs(os.path.join(DATA_DIR, \"photo_jpg\"), exist_ok=True)\n    # Create a dummy image if directories are empty\n    if not os.listdir(os.path.join(DATA_DIR, \"monet_jpg\")):\n        dummy_img = Image.fromarray((np.random.rand(256, 256, 3) * 255).astype('uint8'))\n        dummy_img.save(os.path.join(DATA_DIR, \"monet_jpg\", \"dummy_monet.jpg\"))\n        dummy_img.save(os.path.join(DATA_DIR, \"photo_jpg\", \"dummy_photo.jpg\"))\n\nmonet_files = glob.glob(os.path.join(DATA_DIR, \"monet_jpg/*.jpg\"))\nphoto_files = glob.glob(os.path.join(DATA_DIR, \"photo_jpg/*.jpg\"))\n\nprint(f\"Total Monet paintings: {len(monet_files)}\")\nprint(f\"Total landscape photos: {len(photo_files)}\")","metadata":{"execution":{"iopub.status.busy":"2026-06-02T15:21:26.037360Z","iopub.execute_input":"2026-06-02T15:21:26.037747Z","iopub.status.idle":"2026-06-02T15:21:26.151392Z","shell.execute_reply.started":"2026-06-02T15:21:26.037722Z","shell.execute_reply":"2026-06-02T15:21:26.150517Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(f\"KAGGLE_PATH exists: {os.path.exists(KAGGLE_PATH)}\")\nprint(f\"DATA_DIR: {DATA_DIR}\")\n\n# List all files/folders in DATA_DIR\nif os.path.exists(DATA_DIR):\n    print(f\"\\nContents of {DATA_DIR}:\")\n    for item in os.listdir(DATA_DIR):\n        path = os.path.join(DATA_DIR, item)\n        if os.path.isdir(path):\n            file_count = len(os.listdir(path))\n            print(f\"  {item}/ - {file_count} items\")\n        else:\n            print(f\"  {item}\")\n\n# Check file patterns\nprint(f\"\\nFiles matching *.jpg: {len(glob.glob(os.path.join(DATA_DIR, 'monet_jpg/*.jpg')))}\")\nprint(f\"Files matching *.JPG: {len(glob.glob(os.path.join(DATA_DIR, 'monet_jpg/*.JPG')))}\")\nprint(f\"Files matching *: {len(glob.glob(os.path.join(DATA_DIR, 'monet_jpg/*')))}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-02T15:21:26.152491Z","iopub.execute_input":"2026-06-02T15:21:26.152821Z","iopub.status.idle":"2026-06-02T15:21:26.177498Z","shell.execute_reply.started":"2026-06-02T15:21:26.152787Z","shell.execute_reply":"2026-06-02T15:21:26.176613Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Custom PyTorch Dataset\nWe create a dataset class that returns a random pairing of a photo and a Monet painting.","metadata":{}},{"cell_type":"code","source":"class CycleGANDataset(Dataset):\n    def __init__(self, monet_paths, photo_paths, transform=None):\n        self.monet_paths = monet_paths\n        self.photo_paths = photo_paths\n        self.transform = transform\n        \n    def __len__(self):\n        return max(len(self.monet_paths), len(self.photo_paths))\n        \n    def __getitem__(self, idx):\n        # Loop back if index exceeds domain size\n        monet_path = self.monet_paths[idx % len(self.monet_paths)]\n        photo_path = self.photo_paths[idx % len(self.photo_paths)]\n        \n        monet_img = Image.open(monet_path).convert(\"RGB\")\n        photo_img = Image.open(photo_path).convert(\"RGB\")\n        \n        if self.transform:\n            monet_img = self.transform(monet_img)\n            photo_img = self.transform(photo_img)\n            \n        return monet_img, photo_img\n\n# Image transformations: standard sizing is 256x256\ntransform = transforms.Compose([\n    transforms.Resize((256, 256)),\n    transforms.RandomHorizontalFlip(),\n    transforms.ToTensor(),\n    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))  # Map to [-1, 1]\n])\n\ndataset = CycleGANDataset(monet_files, photo_files, transform=transform)\ndataloader = DataLoader(dataset, batch_size=1, shuffle=True, num_workers=0)\n\nprint(\"Dataset and DataLoader initialized.\")","metadata":{"execution":{"iopub.status.busy":"2026-06-02T15:21:26.179113Z","iopub.execute_input":"2026-06-02T15:21:26.179443Z","iopub.status.idle":"2026-06-02T15:21:26.187391Z","shell.execute_reply.started":"2026-06-02T15:21:26.179419Z","shell.execute_reply":"2026-06-02T15:21:26.186423Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. CycleGAN Architecture\nWe define the two key modules: the Generator and the Discriminator.","metadata":{}},{"cell_type":"markdown","source":"### Generator Network (ResNet-based)\nThe Generator uses a ResNet architecture consisting of downsampling convolutional layers, 6/9 residual blocks (we use 6 blocks for efficiency), and upsampling layers.","metadata":{}},{"cell_type":"code","source":"class ResidualBlock(nn.Module):\n    def __init__(self, in_features):\n        super(ResidualBlock, self).__init__()\n        self.block = nn.Sequential(\n            nn.ReflectionPad2d(1),\n            nn.Conv2d(in_features, in_features, kernel_size=3),\n            nn.InstanceNorm2d(in_features),\n            nn.ReLU(inplace=True),\n            nn.ReflectionPad2d(1),\n            nn.Conv2d(in_features, in_features, kernel_size=3),\n            nn.InstanceNorm2d(in_features)\n        )\n        \n    def forward(self, x):\n        return x + self.block(x)\n\nclass Generator(nn.Module):\n    def __init__(self, input_nc=3, output_nc=3, num_resnet_blocks=6):\n        super(Generator, self).__init__()\n        \n        # Initial convolution layer\n        model = [\n            nn.ReflectionPad2d(3),\n            nn.Conv2d(input_nc, 64, kernel_size=7),\n            nn.InstanceNorm2d(64),\n            nn.ReLU(inplace=True)\n        ]\n        \n        # Downsampling\n        in_features = 64\n        out_features = in_features * 2\n        for _ in range(2):\n            model += [\n                nn.Conv2d(in_features, out_features, kernel_size=3, stride=2, padding=1),\n                nn.InstanceNorm2d(out_features),\n                nn.ReLU(inplace=True)\n            ]\n            in_features = out_features\n            out_features = in_features * 2\n            \n        # Residual blocks\n        for _ in range(num_resnet_blocks):\n            model += [ResidualBlock(in_features)]\n            \n        # Upsampling\n        out_features = in_features // 2\n        for _ in range(2):\n            model += [\n                nn.ConvTranspose2d(in_features, out_features, kernel_size=3, stride=2, padding=1, output_padding=1),\n                nn.InstanceNorm2d(out_features),\n                nn.ReLU(inplace=True)\n            ]\n            in_features = out_features\n            out_features = in_features // 2\n            \n        # Output layer\n        model += [\n            nn.ReflectionPad2d(3),\n            nn.Conv2d(64, output_nc, kernel_size=7),\n            nn.Tanh()\n        ]\n        \n        self.model = nn.Sequential(*model)\n        \n    def forward(self, x):\n        return self.model(x)","metadata":{"execution":{"iopub.status.busy":"2026-06-02T15:21:26.188315Z","iopub.execute_input":"2026-06-02T15:21:26.188721Z","iopub.status.idle":"2026-06-02T15:21:26.204798Z","shell.execute_reply.started":"2026-06-02T15:21:26.188685Z","shell.execute_reply":"2026-06-02T15:21:26.203958Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Discriminator Network (PatchGAN)\nThe Discriminator is a PatchGAN classifier that determines if local $70 \\times 70$ patches of the image are real or fake, promoting fine-grained style transfer.","metadata":{}},{"cell_type":"code","source":"class Discriminator(nn.Module):\n    def __init__(self, input_nc=3):\n        super(Discriminator, self).__init__()\n        \n        model = [\n            nn.Conv2d(input_nc, 64, kernel_size=4, stride=2, padding=1),\n            nn.LeakyReLU(0.2, inplace=True)\n        ]\n        \n        in_features = 64\n        out_features = in_features * 2\n        for i in range(3):\n            stride = 1 if i == 2 else 2\n            padding = 1\n            model += [\n                nn.Conv2d(in_features, out_features, kernel_size=4, stride=stride, padding=padding),\n                nn.InstanceNorm2d(out_features),\n                nn.LeakyReLU(0.2, inplace=True)\n            ]\n            in_features = out_features\n            out_features = in_features * 2\n            \n        # Output patch map\n        model += [nn.Conv2d(in_features, 1, kernel_size=4, stride=1, padding=1)]\n        \n        self.model = nn.Sequential(*model)\n        \n    def forward(self, x):\n        return self.model(x)","metadata":{"execution":{"iopub.status.busy":"2026-06-02T15:21:26.205889Z","iopub.execute_input":"2026-06-02T15:21:26.206156Z","iopub.status.idle":"2026-06-02T15:21:26.220456Z","shell.execute_reply.started":"2026-06-02T15:21:26.206126Z","shell.execute_reply":"2026-06-02T15:21:26.219676Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. Full CycleGAN System & Training Logic\nWe group the models and define loss parameters, optimizers, and the update steps.","metadata":{}},{"cell_type":"code","source":"class CycleGAN:\n    def __init__(self, device, lr=2e-4, lambda_cycle=10.0, lambda_identity=5.0):\n        self.device = device\n        \n        # Generators\n        self.G_A2B = Generator().to(device)  # Photo to Monet\n        self.G_B2A = Generator().to(device)  # Monet to Photo\n        \n        # Discriminators\n        self.D_A = Discriminator().to(device)  # Monet classifier\n        self.D_B = Discriminator().to(device)  # Photo classifier\n        \n        # Loss weights\n        self.lambda_cycle = lambda_cycle\n        self.lambda_identity = lambda_identity\n        \n        # Losses\n        self.criterion_GAN = nn.MSELoss()  # LSGAN\n        self.criterion_cycle = nn.L1Loss()\n        self.criterion_identity = nn.L1Loss()\n        \n        # Optimizers\n        self.optimizer_G = optim.Adam(\n            list(self.G_A2B.parameters()) + list(self.G_B2A.parameters()), \n            lr=lr, betas=(0.5, 0.999)\n        )\n        self.optimizer_D_A = optim.Adam(self.D_A.parameters(), lr=lr, betas=(0.5, 0.999))\n        self.optimizer_D_B = optim.Adam(self.D_B.parameters(), lr=lr, betas=(0.5, 0.999))\n        \n    def train_step(self, real_A, real_B):\n        # real_A: Monet, real_B: Photo\n        real_A, real_B = real_A.to(self.device), real_B.to(self.device)\n        \n        # -------------------\n        # Train Generators\n        # -------------------\n        self.optimizer_G.zero_grad()\n        \n        # Identity loss\n        # G_A2B should output target domain when fed target domain\n        loss_id_A = self.criterion_identity(self.G_B2A(real_A), real_A) * self.lambda_identity\n        loss_id_B = self.criterion_identity(self.G_A2B(real_B), real_B) * self.lambda_identity\n        \n        # GAN loss (Adversarial)\n        fake_B = self.G_A2B(real_B)  # Gen Monet from Photo\n        pred_fake_B = self.D_A(fake_B)\n        loss_GAN_A2B = self.criterion_GAN(pred_fake_B, torch.ones_like(pred_fake_B))\n        \n        fake_A = self.G_B2A(real_A)  # Gen Photo from Monet\n        pred_fake_A = self.D_B(fake_A)\n        loss_GAN_B2A = self.criterion_GAN(pred_fake_A, torch.ones_like(pred_fake_A))\n        \n        # Cycle consistency loss\n        rec_A = self.G_B2A(fake_B)  # Photo -> Monet -> Photo\n        loss_cycle_A = self.criterion_cycle(rec_A, real_B) * self.lambda_cycle\n        \n        rec_B = self.G_A2B(fake_A)  # Monet -> Photo -> Monet\n        loss_cycle_B = self.criterion_cycle(rec_B, real_A) * self.lambda_cycle\n        \n        # Total Generator Loss\n        loss_G = loss_id_A + loss_id_B + loss_GAN_A2B + loss_GAN_B2A + loss_cycle_A + loss_cycle_B\n        loss_G.backward()\n        self.optimizer_G.step()\n        \n        # -------------------\n        # Train Discriminator A (Monet)\n        # -------------------\n        self.optimizer_D_A.zero_grad()\n        pred_real_A = self.D_A(real_A)\n        loss_D_real_A = self.criterion_GAN(pred_real_A, torch.ones_like(pred_real_A))\n        \n        pred_fake_B = self.D_A(fake_B.detach())\n        loss_D_fake_B = self.criterion_GAN(pred_fake_B, torch.zeros_like(pred_fake_B))\n        \n        loss_D_A = (loss_D_real_A + loss_D_fake_B) * 0.5\n        loss_D_A.backward()\n        self.optimizer_D_A.step()\n        \n        # -------------------\n        # Train Discriminator B (Photo)\n        # -------------------\n        self.optimizer_D_B.zero_grad()\n        pred_real_B = self.D_B(real_B)\n        loss_D_real_B = self.criterion_GAN(pred_real_B, torch.ones_like(pred_real_B))\n        \n        pred_fake_A = self.D_B(fake_A.detach())\n        loss_D_fake_A = self.criterion_GAN(pred_fake_A, torch.zeros_like(pred_fake_A))\n        \n        loss_D_B = (loss_D_real_B + loss_D_fake_A) * 0.5\n        loss_D_B.backward()\n        self.optimizer_D_B.step()\n        \n        return loss_G.item(), loss_D_A.item(), loss_D_B.item()","metadata":{"execution":{"iopub.status.busy":"2026-06-02T15:21:26.221386Z","iopub.execute_input":"2026-06-02T15:21:26.222025Z","iopub.status.idle":"2026-06-02T15:21:26.238255Z","shell.execute_reply.started":"2026-06-02T15:21:26.222001Z","shell.execute_reply":"2026-06-02T15:21:26.237412Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5. Model Training\nWe select the target device (GPU or CPU) and run training. **Note**: If running on Kaggle, recommend setting `EPOCHS=20` or higher to get pleasant paint translations. Locally we configure a small fallback of 1 epoch.","metadata":{}},{"cell_type":"code","source":"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"Using device: {device}\")\n\nmodel_system = CycleGAN(device=device)\n\n# Set epochs (adjust higher on Kaggle with GPU)\nEPOCHS = 12 if torch.cuda.is_available() else 1\nprint(f\"Starting training for {EPOCHS} epochs...\")\n\ng_losses = []\nda_losses = []\ndb_losses = []\n\nfor epoch in range(EPOCHS):\n    epoch_g = 0\n    epoch_da = 0\n    epoch_db = 0\n    batches = 0\n    \n    for i, (real_monet, real_photo) in enumerate(dataloader):\n        loss_g, loss_da, loss_db = model_system.train_step(real_monet, real_photo)\n        epoch_g += loss_g\n        epoch_da += loss_da\n        epoch_db += loss_db\n        batches += 1\n        \n        if i % 100 == 0:\n            print(f\"Epoch [{epoch+1}/{EPOCHS}] Batch [{i}/{len(dataloader)}] - Loss G: {loss_g:.4f}, Loss DA: {loss_da:.4f}, Loss DB: {loss_db:.4f}\")\n            \n    g_losses.append(epoch_g / batches)\n    da_losses.append(epoch_da / batches)\n    db_losses.append(epoch_db / batches)\n    print(f\"=== Epoch {epoch+1} Completed === Avg G: {g_losses[-1]:.4f}, Avg DA: {da_losses[-1]:.4f}, Avg DB: {db_losses[-1]:.4f}\")","metadata":{"execution":{"iopub.status.busy":"2026-06-02T15:21:26.239202Z","iopub.execute_input":"2026-06-02T15:21:26.239505Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Plot Training Losses","metadata":{}},{"cell_type":"code","source":"plt.figure(figsize=(10, 5))\nplt.plot(g_losses, label='Generator Loss', color='green')\nplt.plot(da_losses, label='Discriminator Monet Loss', color='blue')\nplt.plot(db_losses, label='Discriminator Photo Loss', color='red')\nplt.title('CycleGAN Training Loss Curves')\nplt.xlabel('Epochs')\nplt.ylabel('Loss')\nplt.legend()\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6. Visualize Style Translations\nLet's write code to show style-transferred results.","metadata":{}},{"cell_type":"code","source":"# Load a sample photo and show translation\nmodel_system.G_A2B.eval()\nwith torch.no_grad():\n    # Get one item from dataloader\n    _, real_photo_batch = next(iter(dataloader))\n    real_photo = real_photo_batch.to(device)\n    \n    # Translate photo to Monet style\n    fake_monet = model_system.G_A2B(real_photo)\n    \n    # Denormalize images to [0, 1] for visualization\n    photo_vis = real_photo.cpu().squeeze().permute(1, 2, 0).numpy() * 0.5 + 0.5\n    monet_vis = fake_monet.cpu().squeeze().permute(1, 2, 0).numpy() * 0.5 + 0.5\n    \n    fig, ax = plt.subplots(1, 2, figsize=(10, 5))\n    ax[0].imshow(np.clip(photo_vis, 0, 1))\n    ax[0].set_title('Original Landscape Photo')\n    ax[0].axis('off')\n    \n    ax[1].imshow(np.clip(monet_vis, 0, 1))\n    ax[1].set_title('Generated Monet Style')\n    ax[1].axis('off')\n    \n    plt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 7. Generate Submission Package\nKaggle requires submitting a ZIP folder `images.zip` containing generated images for all 7,000+ photos in `photo_jpg` folder in $256 \\times 256$ dimensions.","metadata":{}},{"cell_type":"code","source":"print(\"Generating predictions for submission...\")\nsubmission_dir = \"../images\"\nos.makedirs(submission_dir, exist_ok=True)\n\nmodel_system.G_A2B.eval()\n\neval_transform = transforms.Compose([\n    transforms.Resize((256, 256)),\n    transforms.ToTensor(),\n    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))\n])\n\n# Create dataset specifically for photos\nclass PhotoDataset(Dataset):\n    def __init__(self, photo_paths, transform=None):\n        self.photo_paths = photo_paths\n        self.transform = transform\n    def __len__(self):\n        return len(self.photo_paths)\n    def __getitem__(self, idx):\n        path = self.photo_paths[idx]\n        img = Image.open(path).convert(\"RGB\")\n        if self.transform:\n            img = self.transform(img)\n        return img, os.path.basename(path)\n\nphoto_dataset = PhotoDataset(photo_files, transform=eval_transform)\nphoto_loader = DataLoader(photo_dataset, batch_size=1, shuffle=False)\n\nwith torch.no_grad():\n    for i, (img, name) in enumerate(photo_loader):\n        img = img.to(device)\n        fake_monet = model_system.G_A2B(img)\n        \n        # Convert back to PIL Image\n        fake_monet = fake_monet.squeeze().cpu().permute(1, 2, 0).numpy() * 0.5 + 0.5\n        fake_monet = np.clip(fake_monet * 255, 0, 255).astype(np.uint8)\n        res_img = Image.fromarray(fake_monet)\n        \n        # Save image\n        res_img.save(os.path.join(submission_dir, name[0]))\n        \n        if i % 1000 == 0:\n            print(f\"Processed {i}/{len(photo_loader)} photos.\")\n\n# Zip files\nshutil.make_archive(\"images\", \"zip\", submission_dir)\nprint(\"Submission package images.zip generated successfully!\")\n# Clean up temp folder\nshutil.rmtree(submission_dir)","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}