{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"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":3175,"databundleVersionId":44352},{"sourceType":"modelInstanceVersion","sourceId":821361,"databundleVersionId":16497234,"modelInstanceId":624165,"modelId":636005},{"sourceType":"kernelVersion","sourceId":308888038}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## Configuration and Loading the Model","metadata":{}},{"cell_type":"code","source":"# Is the model EfficientNet?\neff_net = True\nmodel_path = '/kaggle/input/models/tatumsoward/galaxymorphology-efficientnetv1/pytorch/default/1/efficientnet_b0_v1.pth'\n\nimport torch\nimport torchvision.models as models\n\n# 2. Initialize the architecture (ensure it matches the version used for training)\nif eff_net:\n    model = models.efficientnet_b0()\n    model.classifier[1] = torch.nn.Linear(in_features=1280, out_features=3)\n\nelse:\n    model = model.resnet18()\n    model.fc = torch.nn.Linear(in_features=model.fc.in_features, out_features=3)\n\n# 3. Load the state_dict (weights) from the file\n# Use map_location='cpu' if you are not using a GPU\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"Using device: {device}\")\n\nstate_dict = torch.load(model_path, map_location=device)\n\n# 4. Apply the weights to your model architecture\nmodel.load_state_dict(state_dict)\n\n# 5. Now you can set it to evaluation mode\nmodel.eval()\n\nprint(\"Model loaded successfully!\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-07T10:03:04.238491Z","iopub.execute_input":"2026-04-07T10:03:04.239207Z","iopub.status.idle":"2026-04-07T10:03:13.151075Z","shell.execute_reply.started":"2026-04-07T10:03:04.239173Z","shell.execute_reply":"2026-04-07T10:03:13.150306Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Loading the Test Set","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport os\n\n# 1. Get the Labels from your Preprocessing output\n# (Just unzip the SMALL output.zip for the CSVs)\n!unzip -q /kaggle/input/notebooks/tatumsoward/aig210-galaxyzoopreprocessing/_output_.zip -d /kaggle/temp/\n\n# 2. Path to your pre-split CSV\nTRAIN_CSV = '/kaggle/temp/train_split.csv'\n\n# 3. Path to the ORIGINAL images (Mount the dataset for this!)\n# Change this path to the exact location of the dataset ZIP\nIMG_ZIP = '/kaggle/input/competitions/galaxy-zoo-the-galaxy-challenge/images_training_rev1.zip'\n\n# 4. Unzip the images ONLY to the fast temp directory\n!unzip -q {IMG_ZIP} -d /kaggle/temp/images/\n\nBASE_PATH = '/kaggle/temp/'\nIMG_DIR = os.path.join(BASE_PATH, 'images_training_rev1', 'images_training_rev1')\n\nprint(\"CSVs and Images are now linked and ready in /kaggle/temp/\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-07T10:03:13.152482Z","iopub.execute_input":"2026-04-07T10:03:13.152809Z","iopub.status.idle":"2026-04-07T10:04:13.190113Z","shell.execute_reply.started":"2026-04-07T10:03:13.152785Z","shell.execute_reply":"2026-04-07T10:04:13.189085Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import json\n\n# 1. Load the stats from the same temp directory as your CSVs\nstats_path = '/kaggle/temp/norm_stats.json'\n\nwith open(stats_path, 'r') as f:\n    norm_dict = json.load(f)\n\nGALAXY_MEAN = norm_dict['mean']\nGALAXY_STD = norm_dict['std']\n\nprint(f\"✅ Loaded Normalization Stats:\")\nprint(f\"Mean: {GALAXY_MEAN}\")\nprint(f\"Std:  {GALAXY_STD}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-07T10:04:13.191697Z","iopub.execute_input":"2026-04-07T10:04:13.192756Z","iopub.status.idle":"2026-04-07T10:04:13.200387Z","shell.execute_reply.started":"2026-04-07T10:04:13.192724Z","shell.execute_reply":"2026-04-07T10:04:13.199090Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torchvision import transforms\n\ntrain_transforms = transforms.Compose([\n    transforms.RandomResizedCrop(224, scale=(0.8, 1.0), ratio=(0.9, 1.1)), #for EfficiNet\n    #transforms.CenterCrop(CFG['img_size']), # for ResNet\n    transforms.RandomRotation(180), # Galaxies can be at any angle!\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomVerticalFlip(), #for EfficiNet\n    transforms.ToTensor(),\n    transforms.Normalize(mean=GALAXY_MEAN, std=GALAXY_STD)\n])\n\nval_transforms = transforms.Compose([\n    transforms.CenterCrop(224),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=GALAXY_MEAN, std=GALAXY_STD)\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-07T10:04:13.202986Z","iopub.execute_input":"2026-04-07T10:04:13.203448Z","iopub.status.idle":"2026-04-07T10:04:13.234068Z","shell.execute_reply.started":"2026-04-07T10:04:13.203404Z","shell.execute_reply":"2026-04-07T10:04:13.233255Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nfrom torch.utils.data import Dataset\nfrom PIL import Image\n\n# Update your paths to point to the TEMP directory\nBASE_PATH = '/kaggle/temp/'\nTRAIN_CSV = os.path.join(BASE_PATH, 'train_split.csv')\n# Note: Unzipping 'images_training_rev1.zip' usually creates a subfolder with the same name\nIMG_DIR = os.path.join(BASE_PATH, 'images_training_rev1', 'images_training_rev1')\n\nclass GalaxyDataset(Dataset):\n    def __init__(self, csv_file, img_dir, transform=None):\n        # Read and force a clean integer index\n        self.df = pd.read_csv(csv_file).reset_index(drop=True)\n        self.img_dir = img_dir\n        self.transform = transform\n        # Ensure keys match exactly what is in your CSV\n        self.label_map = {'Elliptical': 0, 'Spiral': 1, 'Irregular': 2}\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        # FORCE idx to be a Python int. \n        # Sometimes Samplers pass a numpy.int64 which can cause indexing bugs.\n        if hasattr(idx, 'item'):\n            idx = idx.item()\n        idx = int(idx)\n\n        row = self.df.iloc[idx]\n        \n        # 1. Handle Label (The most likely source of 'string index' errors)\n        label_name = str(row['label']) \n        label = self.label_map[label_name]\n        \n        # 2. Handle Image ID\n        img_id = str(int(row['GalaxyID']))\n        img_path = os.path.join(self.img_dir, f\"{img_id}.jpg\")\n        \n        image = Image.open(img_path).convert('RGB')\n        \n        if self.transform:\n            image = self.transform(image)\n            \n        return image, label","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-07T10:04:13.235367Z","iopub.execute_input":"2026-04-07T10:04:13.235633Z","iopub.status.idle":"2026-04-07T10:04:13.247833Z","shell.execute_reply.started":"2026-04-07T10:04:13.235611Z","shell.execute_reply":"2026-04-07T10:04:13.247078Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torch.utils.data import DataLoader\n\n# 1. Create the Test Dataset Instance\n# Use the same transforms as your validation set (no augmentation)\ntest_dataset = GalaxyDataset(\n    csv_file=os.path.join(BASE_PATH, 'test_split.csv'), \n    img_dir=IMG_DIR, \n    transform=val_transforms\n)\n\n# 2. Create the Test Loader\ntest_loader = DataLoader(\n    test_dataset, \n    batch_size=8, \n    shuffle=False, # Crucial: Keep order for the confusion matrix\n    num_workers=0,\n    pin_memory=False\n)\n\nprint(f\"✅ Test Loader Ready with {len(test_dataset)} images.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-07T10:04:13.248919Z","iopub.execute_input":"2026-04-07T10:04:13.249211Z","iopub.status.idle":"2026-04-07T10:04:13.284435Z","shell.execute_reply.started":"2026-04-07T10:04:13.249178Z","shell.execute_reply":"2026-04-07T10:04:13.283682Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Grad-CAM","metadata":{}},{"cell_type":"code","source":"import numpy as np\nimport cv2\nimport torch.nn.functional as F\nimport matplotlib.pyplot as plt\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nmodel.to(device)\n\nclass GradCAM:\n    def __init__(self, model, target_layer):\n        self.model = model\n        self.target_layer = target_layer\n        self.gradients = None\n        self.activations = None\n        \n        # Register hooks\n        self.hooks = [\n            target_layer.register_forward_hook(self.save_activation),\n            target_layer.register_full_backward_hook(self.save_gradient)\n        ]\n\n    def save_activation(self, module, input, output):\n        self.activations = output\n\n    def save_gradient(self, module, grad_input, grad_output):\n        self.gradients = grad_output[0]\n\n    def generate_heatmap(self, input_tensor, class_idx=None):\n        self.model.zero_grad()\n        output = self.model(input_tensor)\n        \n        if class_idx is None:\n            class_idx = torch.argmax(output, dim=1).item()\n        \n        # Backward pass for the specific class\n        loss = output[0, class_idx]\n        loss.backward()\n\n        # Weight the filters by the gradients (Global Average Pooling)\n        weights = torch.mean(self.gradients, dim=(2, 3), keepdim=True)\n        \n        # Weighted sum of feature maps\n        cam = torch.sum(weights * self.activations, dim=1).squeeze()\n        \n        # ReLU to keep only features that have a positive influence on the class\n        cam = np.maximum(cam.detach().cpu().numpy(), 0)\n        \n        # Normalize and Resize\n        cam = cv2.resize(cam, (input_tensor.shape[2], input_tensor.shape[3]))\n        cam = (cam - cam.min()) / (cam.max() - cam.min() + 1e-8)\n        return cam, class_idx\n\n    def __del__(self):\n        for hook in self.hooks:\n            hook.remove()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-07T10:04:13.285330Z","iopub.execute_input":"2026-04-07T10:04:13.285778Z","iopub.status.idle":"2026-04-07T10:04:13.658622Z","shell.execute_reply.started":"2026-04-07T10:04:13.285748Z","shell.execute_reply":"2026-04-07T10:04:13.658054Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def apply_colormap_on_image(org_img, heatmap):\n    # Convert heatmap to RGB colormap\n    heatmap_img = cv2.applyColorMap(np.uint8(255 * heatmap), cv2.COLORMAP_JET)\n    heatmap_img = cv2.cvtColor(heatmap_img, cv2.COLOR_BGR2RGB)\n    \n    # Superimpose\n    result = heatmap_img * 0.4 + org_img * 0.6\n    return np.uint8(result)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-07T10:04:13.659463Z","iopub.execute_input":"2026-04-07T10:04:13.659719Z","iopub.status.idle":"2026-04-07T10:04:13.664207Z","shell.execute_reply.started":"2026-04-07T10:04:13.659696Z","shell.execute_reply":"2026-04-07T10:04:13.663311Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# In torchvision EfficientNet, features are in a Sequential block\nif eff_net:\n    target_layer = model.features[-1] \n    cam_extractor = GradCAM(model, target_layer)\n\n# Assuming 'model' is your ResNet18\nelse:\n    target_layer = model.layer4[-1] \n    cam_extractor = GradCAM(model, target_layer)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-07T10:04:13.665153Z","iopub.execute_input":"2026-04-07T10:04:13.665479Z","iopub.status.idle":"2026-04-07T10:04:13.677215Z","shell.execute_reply.started":"2026-04-07T10:04:13.665452Z","shell.execute_reply":"2026-04-07T10:04:13.676425Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.eval()\n# 1. Get the total number of images in the dataset\ntotal_test_images = len(test_dataset)\n\n# 2. Get 3 random indices from the entire dataset range\nrandom_indices = np.random.randint(0, total_test_images, size=3)\n\nfor i in random_indices:\n    # 3. Pull the specific image and label from the DATASET\n    # This ensures we aren't limited to just one batch\n    raw_image, label_idx = test_dataset[i] \n    \n    # Add batch dimension and move to device\n    input_tensor = raw_image.unsqueeze(0).to(device)\n    \n    # 4. Generate Heatmap\n    heatmap, pred_idx = cam_extractor.generate_heatmap(input_tensor)\n    \n    # 5. Prepare original image for display (Undo normalization)\n    img_np = raw_image.permute(1, 2, 0).cpu().numpy()\n    \n    # Use your specific GALAXY_MEAN and GALAXY_STD from earlier in the notebook\n    img_np = (img_np * GALAXY_STD) + GALAXY_MEAN\n    img_np = np.clip(img_np, 0, 1)\n    \n    # 6. Visualize\n    # Ensure apply_colormap_on_image expects 0-255 or 0-1\n    final_viz = apply_colormap_on_image(img_np * 255, heatmap)\n    \n    plt.figure(figsize=(6,6))\n    plt.imshow(final_viz)\n    # Map index back to string label for a better title\n    inv_label_map = {v: k for k, v in test_dataset.label_map.items()}\n    plt.title(f\"Pred: {inv_label_map[pred_idx]} | Actual: {inv_label_map[label_idx]}\")\n    plt.axis('off')\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-07T10:09:46.503704Z","iopub.execute_input":"2026-04-07T10:09:46.503995Z","iopub.status.idle":"2026-04-07T10:09:47.018906Z","shell.execute_reply.started":"2026-04-07T10:09:46.503972Z","shell.execute_reply":"2026-04-07T10:09:47.018309Z"}},"outputs":[],"execution_count":null}]}