{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":6799,"databundleVersionId":4225553,"isSourceIdPinned":false},{"sourceType":"datasetVersion","sourceId":15488846,"datasetId":9909047,"databundleVersionId":16413434}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Q2: Grad-CAM on ResNet18 (No External Grad-CAM Libraries)\nGrad-CAM with forward/backward hooks, confidence tracking, Gaussian noise experiment, and overlay visualizations.","metadata":{}},{"cell_type":"code","source":"import glob\nimport os\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport torch\nimport torch.nn.functional as F\nfrom PIL import Image\nfrom torchvision import models\nfrom tqdm import tqdm\n\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f'Using device: {device}')\n\nweights = models.ResNet18_Weights.DEFAULT\nmodel = models.resnet18(weights=weights).to(device)\nmodel.eval()\nclass_names = weights.meta['categories']\n\npreprocess = weights.transforms()\nmean = torch.tensor([0.485, 0.456, 0.406])\nstd = torch.tensor([0.229, 0.224, 0.225])\nnorm_min = ((0.0 - mean) / std).view(1, 3, 1, 1)\nnorm_max = ((1.0 - mean) / std).view(1, 3, 1, 1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-02T10:07:50.087685Z","iopub.execute_input":"2026-04-02T10:07:50.088367Z","iopub.status.idle":"2026-04-02T10:07:50.313879Z","shell.execute_reply.started":"2026-04-02T10:07:50.088307Z","shell.execute_reply":"2026-04-02T10:07:50.31331Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def generate_candidate_views(image):\n    w, h = image.size\n    crops = []\n    scales = [1.0, 0.9, 0.8, 0.7]\n    for s in scales:\n        cw = int(w * s)\n        ch = int(h * s)\n        left = max((w - cw) // 2, 0)\n        top = max((h - ch) // 2, 0)\n        crop = image.crop((left, top, left + cw, top + ch))\n        crops.append(crop)\n    return crops\n\n@torch.no_grad()\ndef predict_top1(image):\n    x = preprocess(image).unsqueeze(0).to(device)\n    logits = model(x)\n    probs = torch.softmax(logits, dim=1)\n    conf, pred = probs.max(dim=1)\n    return int(pred.item()), float(conf.item()), x\n\ndef find_imagenet_cat_image_strong():\n    imagenet_roots = [\n        '/kaggle/input/datasets/rahulk2197/cat-images'\n    ]\n\n    best = None\n\n    for root in imagenet_roots:\n        if not os.path.isdir(root):\n            continue\n\n        image_paths = []\n        for ext in ['*.JPEG', '*.jpg', '*.png']:\n            image_paths.extend(glob.glob(os.path.join(root, ext)))\n\n        for image_path in image_paths[:200]:\n            try:\n                image = Image.open(image_path).convert('RGB')\n            except Exception:\n                continue\n\n            views = generate_candidate_views(image)\n\n            for view in views:\n                pred_idx, conf, x = predict_top1(view)\n\n                if pred_idx in [281, 282, 283, 284, 285] and conf > 0.8:\n                    print('Found valid ImageNet cat')\n                    print(image_path, conf)\n                    return view, x, pred_idx, conf, image_path\n\n                if best is None or conf > best['conf']:\n                    best = {\n                        'image': view,\n                        'tensor': x,\n                        'pred_idx': pred_idx,\n                        'conf': conf,\n                        'path': image_path\n                    }\n\n    if best is None:\n        raise RuntimeError('No valid image found in ImageNet cat folders.')\n\n    print('No image >80%, returning best found')\n    return best['image'], best['tensor'], best['pred_idx'], best['conf'], best['path']\n\nselected_image, selected_tensor, selected_pred_idx, selected_conf, image_source = find_imagenet_cat_image_strong()\n\nprint(f'Image source: {image_source}')\nprint(f\"Selected class: {class_names[selected_pred_idx]} | confidence: {selected_conf * 100:.2f}%\")\nif selected_conf < 0.8:\n    print('Warning: confidence below 80% on selected image.')\n\nplt.figure(figsize=(4, 4))\nplt.imshow(selected_image)\nplt.title(f\"Input Image: {class_names[selected_pred_idx]} ({selected_conf * 100:.1f}%)\")\nplt.axis('off')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-02T10:08:05.135674Z","iopub.execute_input":"2026-04-02T10:08:05.135951Z","iopub.status.idle":"2026-04-02T10:08:05.282049Z","shell.execute_reply.started":"2026-04-02T10:08:05.135928Z","shell.execute_reply":"2026-04-02T10:08:05.281289Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class GradCAM:\n    def __init__(self, model, target_layer):\n        self.model = model\n        self.target_layer = target_layer\n        self.activations = None\n        self.gradients = None\n        self.forward_handle = target_layer.register_forward_hook(self._forward_hook)\n        self.backward_handle = target_layer.register_full_backward_hook(self._backward_hook)\n\n    def _forward_hook(self, module, input_tensor, output):\n        self.activations = output\n\n    def _backward_hook(self, module, grad_input, grad_output):\n        self.gradients = grad_output[0]\n\n    def generate(self, input_tensor, target_class=None):\n        logits = self.model(input_tensor)\n        probs = torch.softmax(logits, dim=1)\n        conf, pred = probs.max(dim=1)\n\n        if target_class is None:\n            target_class = int(pred.item())\n\n        self.model.zero_grad(set_to_none=True)\n        score = logits[0, target_class]\n        score.backward(retain_graph=False)\n\n        grads = self.gradients\n        acts = self.activations\n\n        weights = grads.mean(dim=(2, 3), keepdim=True)\n        cam = (weights * acts).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\n        cam = cam[0, 0].detach().cpu().numpy()\n        cam = cam - cam.min()\n        cam = cam / (cam.max() + 1e-8)\n\n        return {\n            'pred_idx': int(pred.item()),\n            'confidence': float(conf.item()),\n            'cam': cam\n        }\n\n    def remove_hooks(self):\n        self.forward_handle.remove()\n        self.backward_handle.remove()\n\ndef denormalize_tensor(norm_tensor):\n    mean_t = mean.view(1, 3, 1, 1).to(norm_tensor.device)\n    std_t = std.view(1, 3, 1, 1).to(norm_tensor.device)\n    img = norm_tensor * std_t + mean_t\n    return torch.clamp(img, 0.0, 1.0)\n\ndef overlay_heatmap(image_tensor, cam, alpha=0.45):\n    rgb = denormalize_tensor(image_tensor).squeeze(0).permute(1, 2, 0).detach().cpu().numpy()\n    cmap = plt.cm.jet(cam)[..., :3]\n    overlay = (1 - alpha) * rgb + alpha * cmap\n    return np.clip(overlay, 0, 1), rgb","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-02T10:08:07.836194Z","iopub.execute_input":"2026-04-02T10:08:07.836555Z","iopub.status.idle":"2026-04-02T10:08:07.847072Z","shell.execute_reply.started":"2026-04-02T10:08:07.836519Z","shell.execute_reply":"2026-04-02T10:08:07.846405Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sigmas = np.linspace(0.05, 0.25, 5)\nimage_tensors = [selected_tensor.clone()]\nimage_names = ['Original']\n\nfor sigma in sigmas:\n    noise = torch.randn_like(selected_tensor) * float(sigma)\n    noisy = selected_tensor + noise\n    noisy = torch.max(torch.min(noisy, norm_max.to(noisy.device)), norm_min.to(noisy.device))\n    image_tensors.append(noisy)\n    image_names.append(f'Noise sigma={sigma:.2f}')\n\ngradcam = GradCAM(model, model.layer4[-1].conv2)\nresults = []\n\nfor i in tqdm(range(len(image_tensors)), desc='Generating Grad-CAM'):\n    tensor = image_tensors[i].to(device)\n    out = gradcam.generate(tensor)\n    overlay, rgb = overlay_heatmap(tensor, out['cam'])\n    results.append({\n        'name': image_names[i],\n        'pred_idx': out['pred_idx'],\n        'confidence': out['confidence'],\n        'heatmap_overlay': overlay,\n        'rgb': rgb\n    })\n\ngradcam.remove_hooks()\n\nfig, axes = plt.subplots(2, 6, figsize=(24, 8))\nfor col, item in enumerate(results):\n    title = f\"{item['name']}\\n{class_names[item['pred_idx']]} ({item['confidence'] * 100:.1f}%)\"\n    axes[0, col].imshow(item['rgb'])\n    axes[0, col].set_title(title, fontsize=10)\n    axes[0, col].axis('off')\n\n    axes[1, col].imshow(item['heatmap_overlay'])\n    axes[1, col].set_title('Grad-CAM Overlay', fontsize=10)\n    axes[1, col].axis('off')\n\nplt.tight_layout()\nplt.show()\n\nfor item in results:\n    print(f\"{item['name']}: {class_names[item['pred_idx']]} | {item['confidence'] * 100:.2f}%\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-02T10:08:09.892473Z","iopub.execute_input":"2026-04-02T10:08:09.893138Z","iopub.status.idle":"2026-04-02T10:08:12.703579Z","shell.execute_reply.started":"2026-04-02T10:08:09.893109Z","shell.execute_reply":"2026-04-02T10:08:12.702576Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}