{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":8089,"databundleVersionId":44321,"sourceType":"competition"}],"dockerImageVersionId":31154,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\n# List all input datasets mounted by Kaggle\nprint(\"Available Kaggle input folders:\\n\")\nfor folder in os.listdir(\"/kaggle/input\"):\n    print(folder)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-06T21:30:30.861292Z","iopub.execute_input":"2025-11-06T21:30:30.861863Z","iopub.status.idle":"2025-11-06T21:30:30.866861Z","shell.execute_reply.started":"2025-11-06T21:30:30.861832Z","shell.execute_reply":"2025-11-06T21:30:30.866088Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!mkdir -p ./data\n!unzip -q /kaggle/input/data-science-bowl-2018/stage1_train.zip -d ./data/\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-06T21:30:48.977998Z","iopub.execute_input":"2025-11-06T21:30:48.978236Z","iopub.status.idle":"2025-11-06T21:30:51.7773Z","shell.execute_reply.started":"2025-11-06T21:30:48.97822Z","shell.execute_reply":"2025-11-06T21:30:51.776213Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nfrom PIL import Image\nimport numpy as np\nfrom tqdm import tqdm\n\nbase_path = \"./data\"\nimage_ids = os.listdir(base_path)\n\nheights, widths = [], []\n\nfor img_id in tqdm(image_ids[:200]):  # only 200 to be quick\n    img_dir = os.path.join(base_path, img_id, \"images\")\n    img_path = os.path.join(img_dir, os.listdir(img_dir)[0])\n    img = np.array(Image.open(img_path))\n    heights.append(img.shape[0])\n    widths.append(img.shape[1])\n\nprint(f\"✅ Total images: {len(image_ids)}\")\nprint(f\"📏 Average size: {np.mean(heights):.1f} × {np.mean(widths):.1f}\")\nprint(f\"📈 Min/Max size: {min(heights)}–{max(heights)} × {min(widths)}–{max(widths)}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-06T21:33:42.966606Z","iopub.execute_input":"2025-11-06T21:33:42.966885Z","iopub.status.idle":"2025-11-06T21:33:43.980904Z","shell.execute_reply.started":"2025-11-06T21:33:42.966867Z","shell.execute_reply":"2025-11-06T21:33:43.980118Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport random\n\ndef load_image_and_mask(image_id):\n    img_dir = os.path.join(base_path, image_id, \"images\")\n    mask_dir = os.path.join(base_path, image_id, \"masks\")\n\n    img_path = os.path.join(img_dir, os.listdir(img_dir)[0])\n    image = np.array(Image.open(img_path).convert(\"RGB\"))\n    mask = np.zeros((image.shape[0], image.shape[1]), dtype=np.uint8)\n    \n    for mask_file in os.listdir(mask_dir):\n        m = np.array(Image.open(os.path.join(mask_dir, mask_file)))\n        mask = np.maximum(mask, m)\n    return image, mask\n\n# Plot 6 random examples\nplt.figure(figsize=(12, 10))\nfor i, img_id in enumerate(random.sample(image_ids, 6)):\n    img, mask = load_image_and_mask(img_id)\n    plt.subplot(3, 4, 2*i+1)\n    plt.imshow(img)\n    plt.title(f\"Original: {img_id[:6]}\")\n    plt.axis(\"off\")\n\n    plt.subplot(3, 4, 2*i+2)\n    plt.imshow(img)\n    plt.imshow(mask, cmap=\"Reds\", alpha=0.4)\n    plt.title(\"Overlay Mask\")\n    plt.axis(\"off\")\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-06T21:33:53.889315Z","iopub.execute_input":"2025-11-06T21:33:53.889936Z","iopub.status.idle":"2025-11-06T21:33:55.15178Z","shell.execute_reply.started":"2025-11-06T21:33:53.88991Z","shell.execute_reply":"2025-11-06T21:33:55.150984Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import random\nfrom sklearn.model_selection import train_test_split\n\n# Reproducibility\nrandom.seed(42)\n\ntrain_ids, val_ids = train_test_split(image_ids, test_size=0.15, random_state=42)\n\nprint(f\"🧩 Training samples: {len(train_ids)}\")\nprint(f\"🧪 Validation samples: {len(val_ids)}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-06T21:34:19.599698Z","iopub.execute_input":"2025-11-06T21:34:19.599968Z","iopub.status.idle":"2025-11-06T21:34:20.21263Z","shell.execute_reply.started":"2025-11-06T21:34:19.59995Z","shell.execute_reply":"2025-11-06T21:34:20.211817Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport matplotlib.pyplot as plt\n\nnuclei_counts = []\n\nfor img_id in random.sample(train_ids, 200):  # sample 200 for speed\n    mask_dir = os.path.join(base_path, img_id, \"masks\")\n    count = len(os.listdir(mask_dir))\n    nuclei_counts.append(count)\n\nplt.figure(figsize=(8,5))\nplt.hist(nuclei_counts, bins=30, color=\"#1f77b4\", edgecolor=\"black\")\nplt.title(\"Distribution of Nuclei Count per Image\")\nplt.xlabel(\"Number of Nuclei\")\nplt.ylabel(\"Frequency\")\nplt.show()\n\nprint(f\"🧫 Average nuclei per image: {np.mean(nuclei_counts):.1f}\")\nprint(f\"📈 Range: {min(nuclei_counts)} – {max(nuclei_counts)}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-06T21:34:51.469121Z","iopub.execute_input":"2025-11-06T21:34:51.469836Z","iopub.status.idle":"2025-11-06T21:34:51.649813Z","shell.execute_reply.started":"2025-11-06T21:34:51.469811Z","shell.execute_reply":"2025-11-06T21:34:51.6491Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# 🧱 Step 5 — Create Dataset + DataLoader (Fixed Version)\n# ============================================================\n\nimport os\nimport numpy as np\nfrom PIL import Image\nimport torch\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\n\n# ============================================================\n# Custom Dataset for Nuclei Segmentation\n# ============================================================\n\nclass NucleiDataset(torch.utils.data.Dataset):\n    def __init__(self, base_dir, ids, image_size=128):\n        \"\"\"\n        Args:\n            base_dir: path to the dataset folder\n            ids: list of image IDs (folder names)\n            image_size: size to which images and masks will be resized\n        \"\"\"\n        self.base_dir = base_dir\n        self.ids = ids\n        self.image_size = image_size\n\n        # Transform for images (bilinear) and masks (nearest neighbor)\n        self.img_transform = transforms.Compose([\n            transforms.Resize((image_size, image_size)),\n            transforms.ToTensor(),\n        ])\n        self.mask_transform = transforms.Compose([\n            transforms.Resize((image_size, image_size), interpolation=Image.NEAREST),\n            transforms.ToTensor(),\n        ])\n\n    def __len__(self):\n        return len(self.ids)\n\n    def __getitem__(self, idx):\n        image_id = self.ids[idx]\n        image_dir = os.path.join(self.base_dir, image_id, \"images\")\n        mask_dir = os.path.join(self.base_dir, image_id, \"masks\")\n\n        # Load image\n        image_path = os.path.join(image_dir, os.listdir(image_dir)[0])\n        image = np.array(Image.open(image_path).convert(\"RGB\"))\n\n        # Merge all instance masks into one binary mask\n        mask = np.zeros((image.shape[0], image.shape[1]), dtype=np.uint8)\n        for m in os.listdir(mask_dir):\n            mask_path = os.path.join(mask_dir, m)\n            m_img = np.array(Image.open(mask_path))\n            mask = np.maximum(mask, m_img)\n\n        # Convert to PIL for transforms\n        image = Image.fromarray(image)\n        mask = Image.fromarray(mask)\n\n        # Apply transforms\n        image = self.img_transform(image)\n        mask = self.mask_transform(mask)\n\n        return image, mask","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-06T22:02:11.011723Z","iopub.execute_input":"2025-11-06T22:02:11.012489Z","iopub.status.idle":"2025-11-06T22:02:11.020961Z","shell.execute_reply.started":"2025-11-06T22:02:11.012461Z","shell.execute_reply":"2025-11-06T22:02:11.02017Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"image_size = 128  # You can increase to 256 later if GPU allows\n\ntrain_dataset = NucleiDataset(base_path, train_ids, image_size=image_size)\nval_dataset   = NucleiDataset(base_path, val_ids, image_size=image_size)\n\ntrain_loader = DataLoader(train_dataset, batch_size=8, shuffle=True)\nval_loader   = DataLoader(val_dataset, batch_size=8, shuffle=False)\n\nprint(f\"✅ Train batches: {len(train_loader)} | Val batches: {len(val_loader)}\")\n\n# ============================================================\n# Visualize a sample batch to confirm correctness\n# ============================================================\n\nimport matplotlib.pyplot as plt\n\nimages, masks = next(iter(train_loader))\nprint(\"Batch shapes:\", images.shape, masks.shape)\n\nplt.figure(figsize=(12, 6))\nfor i in range(4):\n    img = images[i].permute(1, 2, 0)  # Convert from CHW to HWC for plotting\n    mask = masks[i][0]                # Take the single channel\n    plt.subplot(2, 4, i + 1)\n    plt.imshow(img)\n    plt.title(\"Original Image\")\n    plt.axis(\"off\")\n    plt.subplot(2, 4, i + 5)\n    plt.imshow(img)\n    plt.imshow(mask, cmap='Reds', alpha=0.4)\n    plt.title(\"Overlay Mask\")\n    plt.axis(\"off\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-06T22:02:11.283597Z","iopub.execute_input":"2025-11-06T22:02:11.28386Z","iopub.status.idle":"2025-11-06T22:02:12.088843Z","shell.execute_reply.started":"2025-11-06T22:02:11.28384Z","shell.execute_reply":"2025-11-06T22:02:12.088098Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# 🧩 Step 6 — Define and Train a U-Net Model\n# ============================================================\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport matplotlib.pyplot as plt\n\n# ------------------------------------------------------------\n# Define U-Net building blocks\n# ------------------------------------------------------------\nclass DoubleConv(nn.Module):\n    \"\"\"(Conv → BN → ReLU) × 2\"\"\"\n    def __init__(self, in_ch, out_ch):\n        super().__init__()\n        self.double_conv = nn.Sequential(\n            nn.Conv2d(in_ch, out_ch, 3, padding=1),\n            nn.BatchNorm2d(out_ch),\n            nn.ReLU(inplace=True),\n            nn.Conv2d(out_ch, out_ch, 3, padding=1),\n            nn.BatchNorm2d(out_ch),\n            nn.ReLU(inplace=True)\n        )\n\n    def forward(self, x):\n        return self.double_conv(x)\n\n\nclass UNet(nn.Module):\n    def __init__(self, n_channels=3, n_classes=1):\n        super().__init__()\n        self.inc   = DoubleConv(n_channels, 64)\n        self.down1 = nn.Sequential(nn.MaxPool2d(2), DoubleConv(64, 128))\n        self.down2 = nn.Sequential(nn.MaxPool2d(2), DoubleConv(128, 256))\n        self.down3 = nn.Sequential(nn.MaxPool2d(2), DoubleConv(256, 512))\n        self.up1   = nn.ConvTranspose2d(512, 256, 2, stride=2)\n        self.conv1 = DoubleConv(512, 256)\n        self.up2   = nn.ConvTranspose2d(256, 128, 2, stride=2)\n        self.conv2 = DoubleConv(256, 128)\n        self.up3   = nn.ConvTranspose2d(128, 64, 2, stride=2)\n        self.conv3 = DoubleConv(128, 64)\n        self.outc  = nn.Conv2d(64, n_classes, 1)\n\n    def forward(self, x):\n        x1 = self.inc(x)\n        x2 = self.down1(x1)\n        x3 = self.down2(x2)\n        x4 = self.down3(x3)\n        x  = self.up1(x4)\n        x  = self.conv1(torch.cat([x, x3], dim=1))\n        x  = self.up2(x)\n        x  = self.conv2(torch.cat([x, x2], dim=1))\n        x  = self.up3(x)\n        x  = self.conv3(torch.cat([x, x1], dim=1))\n        return torch.sigmoid(self.outc(x))\n\n# ------------------------------------------------------------\n# Initialize model, loss, optimizer\n# ------------------------------------------------------------\ndevice = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nmodel = UNet().to(device)\n\ncriterion = nn.BCELoss()\noptimizer = torch.optim.Adam(model.parameters(), lr=1e-3)\n\n# ------------------------------------------------------------\n# Training and validation loops\n# ------------------------------------------------------------\ndef train_one_epoch(model, loader, optimizer, criterion):\n    model.train()\n    running_loss = 0\n    for imgs, masks in loader:\n        imgs, masks = imgs.to(device), masks.to(device)\n        optimizer.zero_grad()\n        preds = model(imgs)\n        loss = criterion(preds, masks)\n        loss.backward()\n        optimizer.step()\n        running_loss += loss.item()\n    return running_loss / len(loader)\n\ndef evaluate(model, loader, criterion):\n    model.eval()\n    val_loss = 0\n    with torch.no_grad():\n        for imgs, masks in loader:\n            imgs, masks = imgs.to(device), masks.to(device)\n            preds = model(imgs)\n            val_loss += criterion(preds, masks).item()\n    return val_loss / len(loader)\n\n# ------------------------------------------------------------\n# 🚀 Train for a few epochs\n# ------------------------------------------------------------\nnum_epochs = 10\ntrain_losses, val_losses = [], []\n\nfor epoch in range(num_epochs):\n    train_loss = train_one_epoch(model, train_loader, optimizer, criterion)\n    val_loss = evaluate(model, val_loader, criterion)\n    train_losses.append(train_loss)\n    val_losses.append(val_loss)\n    print(f\"Epoch [{epoch+1}/{num_epochs}] | Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f}\")\n\n# ------------------------------------------------------------\n# 📈 Plot learning curve\n# ------------------------------------------------------------\nplt.figure(figsize=(6,4))\nplt.plot(train_losses, label='Train')\nplt.plot(val_losses, label='Validation')\nplt.title(\"U-Net Training Curve\")\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"BCE Loss\")\nplt.legend()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-06T21:43:08.859496Z","iopub.execute_input":"2025-11-06T21:43:08.860045Z","iopub.status.idle":"2025-11-06T21:48:22.077897Z","shell.execute_reply.started":"2025-11-06T21:43:08.860021Z","shell.execute_reply":"2025-11-06T21:48:22.077245Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# 🧠 Step 7 — Visualize Predictions + Compute Dice Coefficient\n# ============================================================\n\nimport torch\nimport matplotlib.pyplot as plt\nimport numpy as np\n\n# ------------------------------------------------------------\n# Dice coefficient function (for segmentation quality)\n# ------------------------------------------------------------\ndef dice_coeff(pred, target, smooth=1e-6):\n    pred = (pred > 0.5).float()\n    intersection = (pred * target).sum(dim=(1,2,3))\n    union = pred.sum(dim=(1,2,3)) + target.sum(dim=(1,2,3))\n    dice = (2. * intersection + smooth) / (union + smooth)\n    return dice.mean().item()\n\n# ------------------------------------------------------------\n# Evaluate and visualize a few examples\n# ------------------------------------------------------------\nmodel.eval()\ndice_scores = []\n\nval_iter = iter(val_loader)\nimages, masks = next(val_iter)\nimages, masks = images.to(device), masks.to(device)\n\nwith torch.no_grad():\n    preds = model(images)\n    dice = dice_coeff(preds, masks)\n    dice_scores.append(dice)\n\nprint(f\"Average Dice Coefficient: {np.mean(dice_scores):.4f}\")\n\n# ------------------------------------------------------------\n# Show qualitative results\n# ------------------------------------------------------------\nplt.figure(figsize=(14, 10))\nfor i in range(4):\n    img = images[i].permute(1,2,0).cpu().numpy()\n    true_mask = masks[i][0].cpu().numpy()\n    pred_mask = preds[i][0].cpu().numpy()\n    \n    plt.subplot(4,3,3*i+1)\n    plt.imshow(img)\n    plt.title(\"Original Image\")\n    plt.axis('off')\n    \n    plt.subplot(4,3,3*i+2)\n    plt.imshow(true_mask, cmap='Reds')\n    plt.title(\"Ground Truth Mask\")\n    plt.axis('off')\n    \n    plt.subplot(4,3,3*i+3)\n    plt.imshow(pred_mask, cmap='Reds')\n    plt.title(\"Predicted Mask\")\n    plt.axis('off')\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-06T21:48:51.329969Z","iopub.execute_input":"2025-11-06T21:48:51.330537Z","iopub.status.idle":"2025-11-06T21:48:52.330485Z","shell.execute_reply.started":"2025-11-06T21:48:51.330509Z","shell.execute_reply":"2025-11-06T21:48:52.329808Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# 🔥 Step — Grad-CAM for U-Net Segmentation\n# ============================================================\nimport torch\nimport torch.nn.functional as F\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom torchvision import transforms\n\n# ------------------------------------------------------------\n# Hook functions to capture activations and gradients\n# ------------------------------------------------------------\nactivations = {}\ngradients = {}\n\ndef forward_hook(module, input, output):\n    activations['value'] = output.detach()\n\ndef backward_hook(module, grad_in, grad_out):\n    gradients['value'] = grad_out[0].detach()\n\n# ------------------------------------------------------------\n# Choose a layer for Grad-CAM (last encoder or decoder block)\n# For ResNet-based U-Nets use: model.encoder[-1]\n# For basic U-Net use: model.down4 or model.conv5\n# ------------------------------------------------------------\ntarget_layer = list(model.children())[-1]   # works for most plain U-Nets\ntarget_layer.register_forward_hook(forward_hook)\ntarget_layer.register_backward_hook(backward_hook)\n\n# ------------------------------------------------------------\n# Select one test image and run forward + backward\n# ------------------------------------------------------------\ntest_img = test_images[0].unsqueeze(0).to(device)  # pick first test sample\noutput = model(test_img)                           # forward pass\ntarget = output.mean()                             # scalar for backprop\nmodel.zero_grad()\ntarget.backward()                                  # compute gradients\n\n# ------------------------------------------------------------\n# Compute Grad-CAM heatmap\n# ------------------------------------------------------------\ngrad = gradients['value']      # shape: [B, C, H, W]\nact = activations['value']     # shape: [B, C, H, W]\nweights = grad.mean(dim=(2, 3), keepdim=True)  # average over spatial dims\ncam = (weights * act).sum(dim=1, keepdim=True)\ncam = F.relu(cam)\n\n# Normalize heatmap to [0,1]\ncam = F.interpolate(cam, size=test_img.shape[2:], mode='bilinear', align_corners=False)\ncam = cam.squeeze().cpu().numpy()\ncam = (cam - cam.min()) / (cam.max() - cam.min() + 1e-8)\n\n# ------------------------------------------------------------\n# Overlay Grad-CAM heatmap on original image\n# ------------------------------------------------------------\norig = test_img.squeeze().permute(1,2,0).cpu().numpy()\norig = (orig - orig.min()) / (orig.max() - orig.min())\n\nplt.figure(figsize=(10,4))\nplt.subplot(1,2,1)\nplt.imshow(orig)\nplt.title(\"Original Image\")\nplt.axis(\"off\")\n\nplt.subplot(1,2,2)\nplt.imshow(orig)\nplt.imshow(cam, cmap='jet', alpha=0.5)\nplt.title(\"Grad-CAM Heatmap\")\nplt.axis(\"off\")\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-06T21:58:44.124515Z","iopub.execute_input":"2025-11-06T21:58:44.125379Z","iopub.status.idle":"2025-11-06T21:58:44.408688Z","shell.execute_reply.started":"2025-11-06T21:58:44.125354Z","shell.execute_reply":"2025-11-06T21:58:44.407945Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# 🔥 Step — Grad-CAM++ for U-Net (Multiple Images)\n# ============================================================\nimport torch\nimport torch.nn.functional as F\nimport matplotlib.pyplot as plt\nimport numpy as np\n\n# ------------------------------------------------------------\n# Hook functions to capture activations and gradients\n# ------------------------------------------------------------\nactivations = {}\ngradients = {}\n\ndef forward_hook(module, input, output):\n    activations['value'] = output.detach()\n\ndef backward_hook(module, grad_in, grad_out):\n    gradients['value'] = grad_out[0].detach()\n\n# ------------------------------------------------------------\n# Select target convolutional layer (usually the last decoder block)\n# ------------------------------------------------------------\ntarget_layer = list(model.children())[-1]\ntarget_layer.register_forward_hook(forward_hook)\ntarget_layer.register_backward_hook(backward_hook)\n\n# ------------------------------------------------------------\n# Helper: Compute Grad-CAM++ heatmap for one image\n# ------------------------------------------------------------\ndef generate_gradcam_pp(model, input_tensor):\n    model.zero_grad()\n    output = model(input_tensor)\n    target = output.mean()       # scalar for gradient\n    target.backward(retain_graph=True)\n\n    grad = gradients['value']    # [B, C, H, W]\n    act = activations['value']   # [B, C, H, W]\n    b, c, h, w = grad.size()\n\n    # Compute Grad-CAM++ weights (improved version)\n    grad2 = grad ** 2\n    grad3 = grad ** 3\n    eps = 1e-8\n\n    numerator = grad2\n    denominator = 2 * grad2 + (act * grad3).sum(dim=(2,3), keepdim=True)\n    alpha = numerator / (denominator + eps)\n    positive_grad = F.relu(target.expand_as(grad))\n    weights = (alpha * F.relu(grad)).sum(dim=(2,3), keepdim=True)\n\n    cam = (weights * act).sum(dim=1, keepdim=True)\n    cam = F.relu(cam)\n    cam = F.interpolate(cam, size=input_tensor.shape[2:], mode='bilinear', align_corners=False)\n    cam = cam.squeeze().cpu().numpy()\n    cam = (cam - cam.min()) / (cam.max() - cam.min() + 1e-8)\n    return cam\n\n# ------------------------------------------------------------\n# Visualize multiple test images\n# ------------------------------------------------------------\nnum_to_show = 4\nplt.figure(figsize=(12, 6))\n\nfor i in range(num_to_show):\n    img = test_images[i].unsqueeze(0).to(device)\n    cam = generate_gradcam_pp(model, img)\n\n    orig = img.squeeze().permute(1,2,0).cpu().numpy()\n    orig = (orig - orig.min()) / (orig.max() - orig.min())\n\n    plt.subplot(2, num_to_show, i+1)\n    plt.imshow(orig)\n    plt.title(f\"Original #{i}\")\n    plt.axis(\"off\")\n\n    plt.subplot(2, num_to_show, i+1+num_to_show)\n    plt.imshow(orig)\n    plt.imshow(cam, cmap=\"jet\", alpha=0.5)\n    plt.title(\"Grad-CAM++ Heatmap\")\n    plt.axis(\"off\")\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-06T22:00:14.420057Z","iopub.execute_input":"2025-11-06T22:00:14.420847Z","iopub.status.idle":"2025-11-06T22:00:15.169525Z","shell.execute_reply.started":"2025-11-06T22:00:14.420819Z","shell.execute_reply":"2025-11-06T22:00:15.168817Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}