{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"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.12"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":21755,"databundleVersionId":1475600}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# I’m Something of a Painter Myself\nUse GANs to create art - will you be the next Monet?","metadata":{}},{"cell_type":"markdown","source":"### Dataset Setup\nThis cell defines the training dataset class and prepares file paths for Monet and photo images.","metadata":{}},{"cell_type":"code","source":"# Dataset\nimport os\nif os.path.exists(\"/kaggle/input/competitions/gan-getting-started\"):\n    ROOT = \"/kaggle/input/competitions/gan-getting-started\"\nelse:\n    ROOT = os.getcwd()\n\nfrom PIL import Image\nfrom torch.utils.data import Dataset, DataLoader\n\nclass MonetPhotoDataset(Dataset):\n    def __init__(self, monet_dir: str, photo_dir: str, transform=None):\n        super().__init__()\n        self.monet_dir = monet_dir\n        self.photo_dir = photo_dir\n        self.transform = transform\n\n        self.monet_images = sorted(os.listdir(monet_dir))\n        self.photo_images = sorted(os.listdir(photo_dir))\n\n        self.monet_len = len(self.monet_images)\n        self.photo_len = len(self.photo_images)\n\n        self.length_dataset = max(self.monet_len, self.photo_len)\n\n    def __len__(self):\n        return self.length_dataset\n    \n    def __getitem__(self, idx : int) -> tuple:\n        monet_img = self.monet_images[idx % self.monet_len]\n        photo_img = self.photo_images[idx % self.photo_len]\n\n        monet_path = os.path.join(self.monet_dir, monet_img)\n        photo_path = os.path.join(self.photo_dir, photo_img)\n\n        monet_image = Image.open(monet_path).convert(\"RGB\")\n        photo_image = Image.open(photo_path).convert(\"RGB\")\n\n        if self.transform:\n            monet_image = self.transform(monet_image)\n            photo_image = self.transform(photo_image)\n\n        return monet_image, photo_image","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-23T04:42:34.765995Z","iopub.execute_input":"2026-03-23T04:42:34.766282Z","iopub.status.idle":"2026-03-23T04:42:41.950712Z","shell.execute_reply.started":"2026-03-23T04:42:34.766248Z","shell.execute_reply":"2026-03-23T04:42:41.949897Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Data Preprocessing\nThis cell configures image transforms used for training and testing, including normalization.","metadata":{}},{"cell_type":"code","source":"# Preprocessing\nimport torchvision.transforms as transforms\n\n# Train transform\ntrain_transform = transforms.Compose([\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])\n])\n\n# Aditional transforms for data augmentation\n# train_transform = transforms.Compose([\n#     transforms.Resize(286),\n#     transforms.RandomCrop(256),\n#     transforms.RandomHorizontalFlip(p=0.5),\n#     transforms.RandomRotation(10),\n#     transforms.ToTensor(),\n#     transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])\n# ])\n\n# Test transform\ntest_transform = transforms.Compose([\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-23T04:42:41.952654Z","iopub.execute_input":"2026-03-23T04:42:41.953021Z","iopub.status.idle":"2026-03-23T04:42:46.205776Z","shell.execute_reply.started":"2026-03-23T04:42:41.952995Z","shell.execute_reply":"2026-03-23T04:42:46.205028Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Dataset Verification\nThis cell performs a quick sanity check by loading a batch and visualizing sample images.","metadata":{}},{"cell_type":"code","source":"# Verify the dataset\nfrom matplotlib import pyplot as plt\n\nmonet_dir = os.path.join(ROOT, \"monet_jpg\")\nphoto_dir = os.path.join(ROOT, \"photo_jpg\")\ndataset = MonetPhotoDataset(monet_dir, photo_dir, transform=train_transform)\ndataloader = DataLoader(dataset, batch_size=6, shuffle=True, num_workers=4, pin_memory=True, persistent_workers=True, prefetch_factor=2, drop_last=True)\n\nfor monet_images, photo_images in dataloader:\n    print(monet_images.shape)  # Should be [6, 3, 256, 256]\n    print(photo_images.shape)  # Should be [6, 3, 256, 256]\n    break\n\nX, y = dataset[0]\nprint(X.shape, y.shape)  # Should be [3, 256, 256] [3, 256, 256]\nplt.subplot(1, 2, 1)\nplt.imshow((X.permute(1, 2, 0) * 0.5 + 0.5).numpy())  # Denormalize for visualization\nplt.title(\"Monet Image\")\nplt.axis(\"off\")\nplt.subplot(1, 2, 2)\nplt.imshow((y.permute(1, 2, 0) * 0.5 + 0.5).numpy())  # Denormalize for visualization\nplt.title(\"Photo Image\")\nplt.axis(\"off\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-23T04:42:46.206714Z","iopub.execute_input":"2026-03-23T04:42:46.207014Z","iopub.status.idle":"2026-03-23T04:42:47.573384Z","shell.execute_reply.started":"2026-03-23T04:42:46.206982Z","shell.execute_reply":"2026-03-23T04:42:47.572546Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Generator Architecture\nThis cell defines core neural network components, including the residual block and generator model.","metadata":{}},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\n\nclass ResidualBlock(nn.Module):\n    def __init__(self, in_channels : int) -> None:\n        super().__init__()\n        self.block = nn.Sequential(\n            nn.ReflectionPad2d(1),\n            nn.Conv2d(in_channels, in_channels, kernel_size=3, stride=1, padding=0),\n            nn.InstanceNorm2d(in_channels),\n            nn.ReLU(inplace=True),\n\n            nn.ReflectionPad2d(1),\n            nn.Conv2d(in_channels, in_channels, kernel_size=3, stride=1, padding=0),\n            nn.InstanceNorm2d(in_channels)\n        )\n        return None\n\n    def forward(self, x : torch.Tensor) -> torch.Tensor:\n        return x + self.block(x)\n    \nclass Generator(nn.Module):\n    def __init__(self, input_shape: tuple, num_residual_blocks: int = 9, *args, **kwargs) -> None:\n        super().__init__(*args, **kwargs)\n        channels, height, width = input_shape\n\n        # Initial Convolution and Downsampling\n        out_features = 64\n        self.initial = nn.Sequential(\n            nn.ReflectionPad2d(3),\n            nn.Conv2d(channels, out_features, kernel_size=7, stride=1, padding=0),\n            nn.InstanceNorm2d(out_features),\n            nn.ReLU(inplace=True)\n        )\n\n        in_features = out_features\n        for _ in range(2):\n            out_features *= 2\n            self.initial.add_module(\n                f\"downsample_{out_features}\",\n                nn.Sequential(\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            )\n            in_features = out_features\n        \n        # Residual Blocks\n        self.residual_blocks = nn.Sequential(\n            *[ResidualBlock(out_features) for _ in range(num_residual_blocks)]\n        )\n\n        # Upsampling\n        for _ in range(2):\n            out_features //= 2\n            self.residual_blocks.add_module(\n                f\"upsample_{out_features}\",\n                nn.Sequential(\n                    nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True),\n                    nn.ReflectionPad2d(1),\n                    nn.Conv2d(in_features, out_features, kernel_size=3, stride=1, padding=0),\n                    nn.InstanceNorm2d(out_features),\n                    nn.ReLU(inplace=True)\n                )\n            )\n            in_features = out_features\n        \n        # Final Convolution\n        self.final = nn.Sequential(\n            nn.ReflectionPad2d(3),\n            nn.Conv2d(out_features, channels, kernel_size=7, stride=1, padding=0),\n            nn.Tanh()\n        )\n\n        self.model = nn.Sequential(\n            self.initial,\n            self.residual_blocks,\n            self.final\n        )\n\n    def forward(self, x : torch.Tensor) -> torch.Tensor:\n        return self.model(x)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-23T04:42:47.575036Z","iopub.execute_input":"2026-03-23T04:42:47.575320Z","iopub.status.idle":"2026-03-23T04:42:47.585573Z","shell.execute_reply.started":"2026-03-23T04:42:47.575282Z","shell.execute_reply":"2026-03-23T04:42:47.585028Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Generator Forward Test\nThis cell runs a quick forward pass to validate generator output tensor shape.","metadata":{}},{"cell_type":"code","source":"# Test the generator\nG = Generator(input_shape=(3, 256, 256))\ntest_result = G(torch.randn(6, 3, 256, 256))  # Add batch dimension\nprint(\"Input shape:\", (6, 3, 256, 256))\nprint(\"Output shape:\", test_result.shape)  # Should be [6, 3, 256, 256]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-23T04:42:47.586391Z","iopub.execute_input":"2026-03-23T04:42:47.586690Z","iopub.status.idle":"2026-03-23T04:42:57.054389Z","shell.execute_reply.started":"2026-03-23T04:42:47.586659Z","shell.execute_reply":"2026-03-23T04:42:57.053802Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Discriminator Architecture\nThis cell defines the PatchGAN-style discriminator used for adversarial training.","metadata":{}},{"cell_type":"code","source":"# Discriminator\nclass Discriminator(nn.Module):\n    def __init__(self, input_shape: tuple, *args, **kwargs) -> None:\n        super().__init__(*args, **kwargs)\n        channels, height, width = input_shape\n\n        def discriminator_block(in_filters : int, out_filters : int, kernel_size : int = 4, stride : int = 2, padding : int = 1, normalize : bool = True) -> nn.Sequential:\n            block = nn.Sequential(\n                nn.Conv2d(in_filters, out_filters, kernel_size, stride, padding),\n                nn.InstanceNorm2d(out_filters) if normalize else nn.Identity(),\n                nn.LeakyReLU(0.2, inplace=True)\n            )\n            return block\n            \n        self.model = nn.Sequential(\n            discriminator_block(channels, 64, stride=2, normalize=False),\n            discriminator_block(64, 128, stride=2),\n            discriminator_block(128, 256, stride=2),\n            discriminator_block(256, 512, stride=2),\n            nn.ZeroPad2d((1, 0, 1, 0)),\n            nn.Conv2d(512, 1, kernel_size=4, padding=1)\n        )\n\n    def forward(self, x : torch.Tensor) -> torch.Tensor:\n        return self.model(x)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-23T04:42:57.055273Z","iopub.execute_input":"2026-03-23T04:42:57.055575Z","iopub.status.idle":"2026-03-23T04:42:57.061877Z","shell.execute_reply.started":"2026-03-23T04:42:57.055548Z","shell.execute_reply":"2026-03-23T04:42:57.061222Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Discriminator Forward Test\nThis cell verifies the discriminator output shape on a random input batch.","metadata":{}},{"cell_type":"code","source":"# Test the discriminator\nD = Discriminator(input_shape=(3, 256, 256))\ntest_result = D(torch.randn(6, 3, 256, 256))  # Add batch dimension\nprint(\"Input shape:\", (6, 3, 256, 256))\nprint(\"Output shape:\", test_result.shape)  # Should be [6, 1, 16, 16]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-23T04:42:57.064732Z","iopub.execute_input":"2026-03-23T04:42:57.065014Z","iopub.status.idle":"2026-03-23T04:42:57.466645Z","shell.execute_reply.started":"2026-03-23T04:42:57.064991Z","shell.execute_reply":"2026-03-23T04:42:57.466001Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Training Utilities\nThis cell defines loss functions, mixed-precision training helpers, and visualization routines.","metadata":{}},{"cell_type":"code","source":"# Loss functions\nimport itertools\nfrom torch.optim import Adam\nfrom tqdm import tqdm\nfrom torch.amp import autocast, GradScaler\nimport torchvision.utils as vutils\n\ncriterion_GAN = nn.MSELoss()\ncriterion_cycle = nn.L1Loss()\ncriterion_identity = nn.L1Loss()\n\n# loss weight hyperparameters\nlambda_cycle = 10.0\nlambda_identity = 5.0\n# Formula: L_G = L_GAN + lambda_cycle * L_cycle + lambda_identity * L_identity\n\nscaler_G = GradScaler()\nscaler_D_M = GradScaler()\nscaler_D_P = GradScaler()\n\ndef train_one_epoch(\n    dataloader : DataLoader,\n    generator_M2P : Generator,\n    generator_P2M : Generator,\n    discriminator_M : Discriminator,\n    discriminator_P : Discriminator,\n    optimizer_G : Adam,\n    optimizer_D_M : Adam,\n    optimizer_D_P : Adam,\n    scaler_G : GradScaler,\n    scaler_D_M : GradScaler,\n    scaler_D_P : GradScaler,\n    criterion_GAN : nn.Module,\n    criterion_cycle : nn.Module,\n    criterion_identity : nn.Module,\n    device : torch.device,\n    lambda_cycle : float,\n    lambda_identity : float,\n) -> tuple[float, float]:\n    generator_M2P.train()\n    generator_P2M.train()\n    discriminator_M.train()\n    discriminator_P.train()\n\n    total_loss_G = 0.0\n    total_loss_D = 0.0\n\n    pbar = tqdm(dataloader, desc=\"Training Epoch\")\n\n    for i, batch in enumerate(pbar):\n        real_monet, real_photo = batch\n        real_monet = real_monet.to(device)\n        real_photo = real_photo.to(device)\n\n        pred_real_M = discriminator_M(real_monet)\n        valid_label = torch.ones_like(pred_real_M, device=device)\n        fake_label = torch.zeros_like(pred_real_M, device=device)\n\n        optimizer_G.zero_grad(set_to_none=True)\n        with autocast('cuda'):\n            identity_monet = generator_P2M(real_monet)\n            loss_identity_monet = criterion_identity(identity_monet, real_monet) * lambda_identity\n\n            identity_photo = generator_M2P(real_photo)\n            loss_identity_photo = criterion_identity(identity_photo, real_photo) * lambda_identity\n\n            fake_monet = generator_P2M(real_photo)\n            loss_GAN_P2M = criterion_GAN(discriminator_M(fake_monet), valid_label)\n\n            fake_photo = generator_M2P(real_monet)\n            loss_GAN_M2P = criterion_GAN(discriminator_P(fake_photo), valid_label)\n\n            recov_monet = generator_P2M(fake_photo)\n            loss_cycle_monet = criterion_cycle(recov_monet, real_monet) * lambda_cycle\n\n            recov_photo = generator_M2P(fake_monet)\n            loss_cycle_photo = criterion_cycle(recov_photo, real_photo) * lambda_cycle\n\n            loss_G = loss_identity_monet + loss_identity_photo + loss_GAN_P2M + loss_GAN_M2P + loss_cycle_monet + loss_cycle_photo\n\n        scaler_G.scale(loss_G).backward()\n        scaler_G.step(optimizer_G)\n        scaler_G.update()\n\n        optimizer_D_M.zero_grad(set_to_none=True)\n        with autocast('cuda'):\n            loss_real_M = criterion_GAN(discriminator_M(real_monet), valid_label)\n            loss_fake_M = criterion_GAN(discriminator_M(fake_monet.detach()), fake_label)\n            loss_D_M = (loss_real_M + loss_fake_M) * 0.5\n        scaler_D_M.scale(loss_D_M).backward()\n        scaler_D_M.step(optimizer_D_M)\n        scaler_D_M.update()\n\n        optimizer_D_P.zero_grad(set_to_none=True)\n        with autocast('cuda'):\n            loss_real_P = criterion_GAN(discriminator_P(real_photo), valid_label)\n            loss_fake_P = criterion_GAN(discriminator_P(fake_photo.detach()), fake_label)\n            loss_D_P = (loss_real_P + loss_fake_P) * 0.5\n        scaler_D_P.scale(loss_D_P).backward()\n        scaler_D_P.step(optimizer_D_P)\n        scaler_D_P.update()\n\n        total_loss_G += loss_G.item()\n        total_loss_D += loss_D_M.item() + loss_D_P.item()\n\n        pbar.set_postfix({\n            \"loss_G\": f\"{loss_G.item():.4f}\",\n            \"loss_D\": f\"{(loss_D_M.item() + loss_D_P.item()):.4f}\"\n        })\n\n    return total_loss_G / len(dataloader), total_loss_D / len(dataloader)\n\ndef evaluate_visually(\n    generator_M2P : Generator,\n    generator_P2M : Generator,\n    dataloader : DataLoader,\n    device : torch.device,\n    num_images : int = 4\n) -> None:\n    generator_M2P.eval()\n    generator_P2M.eval()\n\n    with torch.no_grad():\n        for i, batch in enumerate(dataloader):\n            real_monet, real_photo = batch\n            real_monet = real_monet.to(device)\n            real_photo = real_photo.to(device)\n\n            fake_monet = generator_P2M(real_photo)\n            fake_photo = generator_M2P(real_monet)\n\n            img_grid = vutils.make_grid(\n                torch.cat((real_monet[:num_images], fake_photo[:num_images], real_photo[:num_images], fake_monet[:num_images]), dim=0),\n                nrow=num_images,\n                normalize=True,\n                scale_each=False,\n                value_range=(-1, 1)\n            )\n\n            plt.figure(figsize=(12, 6))\n            plt.imshow(img_grid.permute(1, 2, 0).cpu().numpy())\n            plt.title(\n                \"Row 1: Real Monet  |  Row 2: Fake Photo (Generated)\\n\"\n                \"Row 3: Real Photo  |  Row 4: Fake Monet (Generated)\",\n                fontsize=14, pad=20\n            )\n            plt.axis(\"off\")\n            plt.show()\n            break\n\n    generator_M2P.train()\n    generator_P2M.train()\n    return None","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-23T04:42:57.467479Z","iopub.execute_input":"2026-03-23T04:42:57.467737Z","iopub.status.idle":"2026-03-23T04:42:57.483856Z","shell.execute_reply.started":"2026-03-23T04:42:57.467714Z","shell.execute_reply":"2026-03-23T04:42:57.483318Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Model Training Loop\nThis cell initializes models/optimizers and runs the epoch-based CycleGAN training process.","metadata":{}},{"cell_type":"code","source":"# Train the model\nimport torch.backends.cudnn as cudnn\ncudnn.benchmark = True  # Enable cuDNN auto-tuner for better performance\n\n# Initialize models, optimizers, and loss functions\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\ngpu_count = torch.cuda.device_count()\nprint(f\"Current computing device in use: {device}\")\nif device.type == 'cuda':\n    print(f\"Number of available GPUs detected: {gpu_count}\")\n\ngenerator_M2P = Generator(input_shape=(3, 256, 256)).to(device)\ngenerator_P2M = Generator(input_shape=(3, 256, 256)).to(device)\ndiscriminator_M = Discriminator(input_shape=(3, 256, 256)).to(device)\ndiscriminator_P = Discriminator(input_shape=(3, 256, 256)).to(device)\n\noptimizer_G = Adam(itertools.chain(generator_M2P.parameters(), generator_P2M.parameters()), lr=0.0002, betas=(0.5, 0.999))\noptimizer_D_M = Adam(discriminator_M.parameters(), lr=0.0002, betas=(0.5, 0.999))\noptimizer_D_P = Adam(discriminator_P.parameters(), lr=0.0002, betas=(0.5, 0.999))\n\ndef save_model(model, filepath):\n    torch.save(model.state_dict(), filepath)\n\nnum_epochs = 0\nfor epoch in range(num_epochs):\n    avg_loss_G, avg_loss_D = train_one_epoch(\n        dataloader,\n        generator_M2P,\n        generator_P2M,\n        discriminator_M,\n        discriminator_P,\n        optimizer_G,\n        optimizer_D_M,\n        optimizer_D_P,\n        scaler_G,\n        scaler_D_M,\n        scaler_D_P,\n        criterion_GAN,\n        criterion_cycle,\n        criterion_identity,\n        device,\n        lambda_cycle,\n        lambda_identity\n    )\n    print(f\"Epoch [{epoch+1}/{num_epochs}] - Average Loss G: {avg_loss_G:.4f}, Average Loss D: {avg_loss_D:.4f}\")\n\n    # Evaluate visually every 5 epochs\n    if (epoch + 1) % 5 == 0:\n        evaluate_visually(generator_M2P, generator_P2M, dataloader, device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-23T04:42:57.484629Z","iopub.execute_input":"2026-03-23T04:42:57.484819Z","iopub.status.idle":"2026-03-23T04:42:57.700409Z","shell.execute_reply.started":"2026-03-23T04:42:57.484800Z","shell.execute_reply":"2026-03-23T04:42:57.699906Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Test Data Pipeline\nThis cell prepares the test dataset class and dataloader for inference.","metadata":{}},{"cell_type":"code","source":"# Test Dataset and Dataloader\nclass TestMonetPhotoDataset(Dataset):\n    def __init__(self, photo_dir: str, transform=None) -> None:\n        super().__init__()\n        self.photo_dir = photo_dir\n        self.transform = transform\n\n        self.photo_images = sorted(os.listdir(photo_dir))\n\n    def __len__(self) -> int:\n        return len(self.photo_images)\n    \n    def __getitem__(self, idx : int) -> torch.Tensor:\n        photo_name = self.photo_images[idx]\n        photo_path = os.path.join(self.photo_dir, photo_name)\n        photo_image = Image.open(photo_path).convert(\"RGB\")\n\n        if self.transform:\n            photo_image = self.transform(photo_image)\n        else:\n            photo_image = transforms.ToTensor()(photo_image)\n\n        return photo_image","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-23T04:42:57.701250Z","iopub.execute_input":"2026-03-23T04:42:57.701754Z","iopub.status.idle":"2026-03-23T04:42:57.707088Z","shell.execute_reply.started":"2026-03-23T04:42:57.701729Z","shell.execute_reply":"2026-03-23T04:42:57.706295Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Inference and Export\nThis cell translates photos to Monet-style outputs and packs generated images into a zip file.","metadata":{}},{"cell_type":"code","source":"# Inference function\ndef translate_image(\n    generator_P2M : Generator,\n    photo_dataloader : DataLoader,\n    device : torch.device,\n    output_dir : str = \"./generated_images\",\n    max_images : int = 7038\n) -> None:\n    os.makedirs(output_dir, exist_ok=True)\n\n    generator_P2M.eval()\n    img_count = 0\n\n    with torch.no_grad():\n        for batch in tqdm(photo_dataloader, desc=\"Translating to Monet Style\"):\n            real_photos = batch\n            real_photos = real_photos.to(device)\n\n            fake_monets = generator_P2M(real_photos)\n\n            fake_monets = (fake_monets * 0.5) + 0.5\n            fake_monets = (fake_monets * 255).clamp(0, 255).to(torch.uint8)\n            fake_monets = fake_monets.cpu().numpy()\n\n            for i in range(fake_monets.shape[0]):\n                img_array = fake_monets[i].transpose(1, 2, 0)\n                img = Image.fromarray(img_array)\n                img.save(os.path.join(output_dir, f\"generated_{img_count:04d}.jpg\"), format=\"JPEG\")\n\n                img_count += 1\n                if img_count >= max_images:\n                    break\n            if img_count >= max_images:\n                break\n\n# create zip file of generated images\nimport zipfile\n\ndef create_zip_from_directory(directory : str, zip_name : str) -> None:\n    with zipfile.ZipFile(zip_name, 'w') as zipf:\n        for foldername, subfolders, filenames in os.walk(directory):\n            for filename in filenames:\n                file_path = os.path.join(foldername, filename)\n                zipf.write(file_path, os.path.relpath(file_path, directory))\n\n    return None","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-23T04:42:57.708027Z","iopub.execute_input":"2026-03-23T04:42:57.708308Z","iopub.status.idle":"2026-03-23T04:42:57.723896Z","shell.execute_reply.started":"2026-03-23T04:42:57.708276Z","shell.execute_reply":"2026-03-23T04:42:57.723361Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Final Generation Step\nThis final cell runs inference end-to-end and writes the submission archive file.","metadata":{}},{"cell_type":"code","source":"!wget https://raw.githubusercontent.com//Richard-Qin-001/kaggle-learning/main/gan-getting-started/gan-getting-started.py","metadata":{"trusted":true,"_kg_hide-input":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2026-03-23T04:42:57.724688Z","iopub.execute_input":"2026-03-23T04:42:57.724926Z","iopub.status.idle":"2026-03-23T04:42:58.300937Z","shell.execute_reply.started":"2026-03-23T04:42:57.724895Z","shell.execute_reply":"2026-03-23T04:42:58.300238Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# We do not run this cell directly as the notebook environment does not support Multi-GPU parallel training, instead, we run a python file.\n# generate_img_dir = \"./generated_images\"\n# test_dataset = TestMonetPhotoDataset(photo_dir=photo_dir, transform=test_transform)\n# test_dataloader = DataLoader(test_dataset, batch_size=16, shuffle=False, num_workers=8, pin_memory=True, persistent_workers=True, prefetch_factor=2)\n# translate_image(generator_P2M, test_dataloader, device=device, output_dir=generate_img_dir, max_images=7038)\n# create_zip_from_directory(generate_img_dir, \"images.zip\")\n\nimport torch\nimport gc\n\n# Free all GPU memory from notebook before launching torchrun\n# Delete GPU models and optimizers\ndel generator_M2P, generator_P2M, discriminator_M, discriminator_P\ndel optimizer_G, optimizer_D_M, optimizer_D_P\ndel scaler_G, scaler_D_M, scaler_D_P\n# Delete CPU test variables from earlier cells\ndel G, D, test_result\n# Delete dataloader (terminates persistent workers) and dataset\ndel dataloader, dataset\ngc.collect()\ntorch.cuda.empty_cache()\n\n!torchrun --nproc_per_node=2 gan-getting-started.py","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-23T04:42:58.302156Z","iopub.execute_input":"2026-03-23T04:42:58.302387Z","execution_failed":"2026-03-23T04:46:58.667Z"}},"outputs":[],"execution_count":null}]}