{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport torch\nfrom torch.utils.data import random_split, DataLoader, Dataset\nimport torchvision.models as models\nimport torch.nn as nn\nimport torch.optim as optim\n\n\nimport torchvision\nfrom torchvision import transforms\nfrom PIL import Image\n\nimport numpy as np \nimport pandas as pd\nimport matplotlib.pyplot as plt\n\nimport os\nimport kagglehub\n\nimport random\n\ndef set_seed(seed=42):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n\nset_seed(42)\nloader_generator = torch.Generator()\nloader_generator.manual_seed(42)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-07-24T18:59:18.198981Z","iopub.execute_input":"2026-07-24T18:59:18.199239Z","iopub.status.idle":"2026-07-24T18:59:18.208029Z","shell.execute_reply.started":"2026-07-24T18:59:18.199217Z","shell.execute_reply":"2026-07-24T18:59:18.207267Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class AstroUniversalDataset(Dataset):\n    def __init__(self, csv_path, galaxy_zoo_img_dir, planet_dir, other_dirs, transform=None, sample_limit=150):\n        self.transform = transform\n        self.image_paths = []\n        self.labels = []\n        \n        self.class_to_idx = {\n            \"Spiral\": 0, \"Elliptical\": 1, \"Nebula\": 2, \"Planetary\": 3, \"Star Cluster\": 4\n        }\n        \n        df = pd.read_csv(csv_path)\n        elliptical_df = df[(df['Class1.1'] >= 0.80) & ((df['Class7.1'] + df['Class7.2']) >= 0.70)]\n        elliptical_ids = elliptical_df['GalaxyID'].tolist()\n        spiral_df = df[(df['Class1.2'] >= 0.80) & (df['Class4.1'] >= 0.75)]\n        spiral_ids = spiral_df['GalaxyID'].tolist()\n        \n        self._extract_galaxy_zoo(spiral_ids, galaxy_zoo_img_dir, self.class_to_idx[\"Spiral\"], sample_limit)\n        self._extract_galaxy_zoo(elliptical_ids, galaxy_zoo_img_dir, self.class_to_idx[\"Elliptical\"], sample_limit)\n\n        print(\"Indexing Planetary subfolders...\")\n        planet_label = self.class_to_idx[\"Planetary\"]\n\n        total_planet_count = 0  \n        \n        for folder_name in sorted(os.listdir(planet_dir)):\n            folder_path = os.path.join(planet_dir, folder_name)\n            if os.path.isdir(folder_path):\n                \n                current_planet_count = 0 \n                for filename in os.listdir(folder_path):\n                    if filename.lower().endswith(('.jpg', '.jpeg', '.png')):\n                        self.image_paths.append(os.path.join(folder_path, filename))\n                        self.labels.append(planet_label)\n                        \n                        current_planet_count += 1\n                        total_planet_count += 1\n                        \n                        if current_planet_count >= 18:    \n                            break\n                            \n        print(f\"Added {total_planet_count} total planet images (18 per subfolder).\")\n\n        print(\"Indexing static image directories...\")\n        for class_name, folder_path in other_dirs.items():\n            if class_name not in self.class_to_idx:\n                continue\n            \n            idx = self.class_to_idx[class_name]\n            count = 0\n            for filename in os.listdir(folder_path):\n                if filename.lower().endswith(('.jpg', '.jpeg', '.png')):\n                    self.image_paths.append(os.path.join(folder_path, filename))\n                    self.labels.append(idx)\n                    count += 1\n                    if count >= sample_limit:\n                        break\n                        \n        print(f\"Dataset initialization complete. Total samples: {len(self.image_paths)}\")\n\n    \n    def _extract_galaxy_zoo(self, galaxy_ids, img_dir, label_idx, sample_limit):\n        count = 0\n        for g_id in galaxy_ids:\n            img_path = os.path.join(img_dir, f\"{g_id}.jpg\")\n            if os.path.exists(img_path):\n                self.image_paths.append(img_path)\n                self.labels.append(label_idx)\n                count += 1\n                if count >= sample_limit:\n                    break\n\n    def __len__(self):\n        return len(self.image_paths)\n    \n    def __getitem__(self, idx):\n        img_path = self.image_paths[idx]\n        image = Image.open(img_path).convert(\"RGB\")\n        label = self.labels[idx]\n        \n        if self.transform:\n            image = self.transform(image)\n            \n        return image, torch.tensor(label, dtype=torch.long)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T18:59:18.209145Z","iopub.execute_input":"2026-07-24T18:59:18.209409Z","iopub.status.idle":"2026-07-24T18:59:18.223929Z","shell.execute_reply.started":"2026-07-24T18:59:18.209383Z","shell.execute_reply":"2026-07-24T18:59:18.223257Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import zipfile\nimport os\n\nos.makedirs(\"/kaggle/working/galaxy_zoo/images\", exist_ok=True)\n\nwith zipfile.ZipFile(\"/kaggle/input/competitions/galaxy-zoo-the-galaxy-challenge/training_solutions_rev1.zip\", 'r') as zip_ref:\n    zip_ref.extractall(\"/kaggle/working/galaxy_zoo/\")\n\nwith zipfile.ZipFile(\"/kaggle/input/competitions/galaxy-zoo-the-galaxy-challenge/images_training_rev1.zip\", 'r') as zip_ref:\n    zip_ref.extractall(\"/kaggle/working/galaxy_zoo/images\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T18:59:18.225373Z","iopub.execute_input":"2026-07-24T18:59:18.225577Z","iopub.status.idle":"2026-07-24T18:59:31.582813Z","shell.execute_reply.started":"2026-07-24T18:59:18.225558Z","shell.execute_reply":"2026-07-24T18:59:31.581887Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"CSV_PATH = \"/kaggle/working/galaxy_zoo/training_solutions_rev1.csv\"\nGALAXY_ZOO_DIR = \"/kaggle/working/galaxy_zoo/images/images_training_rev1\"\nPLANETS = \"/kaggle/input/private-dataset/Planets_Moons_Data/Planets and Moons\"\n\nOTHER_DIRECTORIES = {\n    \"Nebula\": \"/kaggle/input/datasets/arpitmahajan09/nebulae-dataset\",\n    \"Star Cluster\": \"/kaggle/input/datasets/arpitmahajan09/star-clusters-dataset/star_clusters_dataset\"\n}\n\nfrom torchvision import transforms\nastroTransfrom = transforms.Compose([\n    transforms.Resize((224,224)),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n\ndataset = AstroUniversalDataset(\n    csv_path=CSV_PATH,\n    galaxy_zoo_img_dir=GALAXY_ZOO_DIR,\n    planet_dir = PLANETS, \n    other_dirs=OTHER_DIRECTORIES,\n    transform=astroTransfrom,\n    sample_limit=150 \n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T18:59:31.583554Z","iopub.execute_input":"2026-07-24T18:59:31.583768Z","iopub.status.idle":"2026-07-24T18:59:31.781959Z","shell.execute_reply.started":"2026-07-24T18:59:31.583751Z","shell.execute_reply":"2026-07-24T18:59:31.781181Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"totalSize = len(dataset)\ntrainSize = int(len(dataset)*0.8)\ntestSize = totalSize-trainSize\n\ngenerator = torch.Generator().manual_seed(42)\ntrainDataset, testDataset = random_split(dataset, [trainSize, testSize], generator=generator)\n\ntrainTransform = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.RandomHorizontalFlip(p=0.5),\n    transforms.RandomVerticalFlip(p=0.5),\n    transforms.RandomRotation(180),\n    transforms.ColorJitter(brightness=0.2, contrast=0.2),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n\ntestTransform = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n\nimport copy\ntestDataset.dataset = copy.copy(dataset)\n\ntrainDataset.dataset.transform = trainTransform\ntestDataset.dataset.transform = testTransform\n\n\nprint(f\"Train Size ={trainSize}, Test Size = {testSize}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T18:59:31.783364Z","iopub.execute_input":"2026-07-24T18:59:31.783572Z","iopub.status.idle":"2026-07-24T18:59:31.792245Z","shell.execute_reply.started":"2026-07-24T18:59:31.783554Z","shell.execute_reply":"2026-07-24T18:59:31.791624Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"BATCH_SIZE = 32\ntrainLoader = DataLoader(trainDataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=2, generator=loader_generator)\nevalLoader = DataLoader(testDataset, batch_size=32, shuffle=False, num_workers=2)\n\nmodel = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)\n# Using resnet18 pretrained model here\n\nfor param in model.parameters():\n    param.requires_grad = False\n    \nfor param in model.layer4.parameters():\n    param.requires_grad = True\n\nnum_ftrs = model.fc.in_features\nmodel.fc = nn.Sequential(\n    nn.Dropout(p=0.5),\n    nn.Linear(num_ftrs, 5)\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T18:59:31.793152Z","iopub.execute_input":"2026-07-24T18:59:31.793391Z","iopub.status.idle":"2026-07-24T18:59:31.956324Z","shell.execute_reply.started":"2026-07-24T18:59:31.793364Z","shell.execute_reply":"2026-07-24T18:59:31.955663Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# model.load_state_dict(torch.load('/kaggle/working/astro_resnet18_best.pth'))\n# model.eval() \n# print(\"Pre-trained model loaded\")\n\n# Works only after a kaggle model is trained in the current session  \n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T18:59:31.957213Z","iopub.execute_input":"2026-07-24T18:59:31.957403Z","iopub.status.idle":"2026-07-24T18:59:31.96075Z","shell.execute_reply.started":"2026-07-24T18:59:31.957385Z","shell.execute_reply":"2026-07-24T18:59:31.960021Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from collections import Counter\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nmodel = model.to(device)\nprint(f\"model loaded and moved to: {device}\")\n\nidx_to_class = {v: k for k, v in dataset.class_to_idx.items()}\n\ntrain_labels = [dataset.labels[i] for i in trainDataset.indices]\nclass_counts = Counter(train_labels)\nnum_classes = len(dataset.class_to_idx)\n\nweights = torch.tensor(\n    [1.0 / class_counts[i] for i in range(num_classes)],\n    dtype=torch.float\n)\nweights = weights / weights.sum() * num_classes\nweights = weights.to(device)\n\nprint(\"Class weights:\", {idx_to_class[i]: round(w.item(), 3) for i, w in enumerate(weights)})\n\noptimizer = optim.Adam([\n    {\"params\": model.layer4.parameters(), \"lr\": 1e-5},\n    {\"params\": model.fc.parameters(), \"lr\": 1e-3},\n], weight_decay=1e-4)\n\ncriterion = nn.CrossEntropyLoss(weight=weights)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T18:59:31.962035Z","iopub.execute_input":"2026-07-24T18:59:31.962717Z","iopub.status.idle":"2026-07-24T18:59:32.0125Z","shell.execute_reply.started":"2026-07-24T18:59:31.96261Z","shell.execute_reply":"2026-07-24T18:59:32.011721Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Testing Dataset","metadata":{}},{"cell_type":"code","source":"test_loader = DataLoader(trainDataset, batch_size=8, shuffle=True)\n\nimages, labels = next(iter(test_loader))\nidx_to_class = {v: k for k, v in dataset.class_to_idx.items()}\n\ndef imshow(inp, title=None):\n    inp = inp.numpy().transpose((1, 2, 0))\n    \n    mean = np.array([0.485, 0.456, 0.406])\n    std = np.array([0.229, 0.224, 0.225])\n    inp = std * inp + mean\n    inp = np.clip(inp, 0, 1) \n    \n    plt.figure(figsize=(16, 4))\n    plt.imshow(inp)\n    if title is not None:\n        plt.title(title, fontsize=12)\n    plt.axis('off')\n    plt.show()\n\n\nout = torchvision.utils.make_grid(images, nrow=8)\nclass_names = [idx_to_class[label.item()] for label in labels]\n\nprint(\"Visualizing a random batch from the training set:\")\nimshow(out, title=\" | \".join(class_names))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T18:59:32.013444Z","iopub.execute_input":"2026-07-24T18:59:32.013698Z","iopub.status.idle":"2026-07-24T18:59:32.312922Z","shell.execute_reply.started":"2026-07-24T18:59:32.013677Z","shell.execute_reply":"2026-07-24T18:59:32.312091Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Continued Code","metadata":{}},{"cell_type":"code","source":"EPOCHS = 25\nprint(f\"Starting training on device: {device}...\")\n\nscheduler = optim.lr_scheduler.ReduceLROnPlateau(\n    optimizer, mode='max', factor=0.5, patience=2\n)\n\nBEST_MODEL_PATH = '/kaggle/working/astro_resnet18_best.pth'\nbest_test_acc = 0.0\n\ntrain_losses, test_losses = [], []\ntrain_accuracies, test_accuracies = [], []\n\nfor epoch in range(EPOCHS):\n    model.train()\n    running_loss, correct_train, total_train = 0.0, 0, 0\n    \n    for images, labels in trainLoader:\n        images, labels = images.to(device), labels.to(device)\n        \n        optimizer.zero_grad()      \n        outputs = model(images)\n        loss = criterion(outputs, labels) \n        loss.backward()    \n        optimizer.step()      \n        \n        running_loss += loss.item() * images.size(0)\n        _, predicted = torch.max(outputs, 1)\n        total_train += labels.size(0)\n        correct_train += (predicted == labels).sum().item()\n        \n    epoch_train_loss = running_loss / total_train\n    epoch_train_acc = correct_train / total_train\n    train_losses.append(epoch_train_loss)\n    train_accuracies.append(epoch_train_acc)\n\n    \n    model.eval()\n    test_loss, correct_test, total_test = 0.0, 0, 0\n    \n    with torch.no_grad():\n        for images, labels in evalLoader:\n            images, labels = images.to(device), labels.to(device)\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n            \n            test_loss += loss.item() * images.size(0)\n            _, predicted = torch.max(outputs, 1)\n            total_test += labels.size(0)\n            correct_test += (predicted == labels).sum().item()\n            \n    epoch_test_loss = test_loss / total_test\n    epoch_test_acc = correct_test / total_test\n    test_losses.append(epoch_test_loss)\n    test_accuracies.append(epoch_test_acc)\n\n    prev_lrs = [g['lr'] for g in optimizer.param_groups]\n    scheduler.step(epoch_test_acc)\n    new_lrs = [g['lr'] for g in optimizer.param_groups]\n    if new_lrs != prev_lrs:\n        print(f\"  -> LR reduced: {prev_lrs} -> {new_lrs}\")\n\n    \n    if epoch_test_acc > best_test_acc:\n        best_test_acc = epoch_test_acc\n        torch.save(model.state_dict(), BEST_MODEL_PATH)\n        marker = \" (new best, saved)\"\n    else:\n        marker = \"\"\n\n    \n    print(f\"Epoch [{epoch+1}/{EPOCHS}] -> \"\n          f\"Train Loss: {epoch_train_loss:.4f}, Train Acc: {epoch_train_acc*100:.2f}% | \"\n          f\"Test Loss: {epoch_test_loss:.4f}, Test Acc: {epoch_test_acc*100:.2f}%\")\n\nprint(f\"\\nTraining Complete: {best_test_acc*100:.2f}%\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T18:59:32.315414Z","iopub.execute_input":"2026-07-24T18:59:32.315602Z","iopub.status.idle":"2026-07-24T19:14:29.347956Z","shell.execute_reply.started":"2026-07-24T18:59:32.315584Z","shell.execute_reply":"2026-07-24T19:14:29.346762Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\nplt.figure(figsize=(14, 5))\n\nplt.subplot(1, 2, 1)\nplt.plot(train_losses, label='Train Loss', color='blue', marker='o')\nplt.plot(test_losses, label='Test Loss', color='red', marker='o')\nplt.title('Training and Testing Loss over Epochs')\nplt.xlabel('Epochs')\nplt.ylabel('Loss')\nplt.legend()\nplt.grid(True)\n\nplt.subplot(1, 2, 2)\nplt.plot(train_accuracies, label='Train Accuracy', color='blue', marker='o')\nplt.plot(test_accuracies, label='Test Accuracy', color='red', marker='o')\nplt.title('Training and Testing Accuracy over Epochs')\nplt.xlabel('Epochs')\nplt.ylabel('Accuracy')\nplt.legend()\nplt.grid(True)\n\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T19:14:29.349319Z","iopub.execute_input":"2026-07-24T19:14:29.349551Z","iopub.status.idle":"2026-07-24T19:14:29.571188Z","shell.execute_reply.started":"2026-07-24T19:14:29.349527Z","shell.execute_reply":"2026-07-24T19:14:29.570525Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(f\"Best model saved to {BEST_MODEL_PATH} with test accuracy {best_test_acc*100:.2f}%\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T19:14:29.572052Z","iopub.execute_input":"2026-07-24T19:14:29.572293Z","iopub.status.idle":"2026-07-24T19:14:29.576069Z","shell.execute_reply.started":"2026-07-24T19:14:29.572269Z","shell.execute_reply":"2026-07-24T19:14:29.575499Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Grad-CAM","metadata":{}},{"cell_type":"code","source":"!pip install grad-cam","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T19:14:29.57696Z","iopub.execute_input":"2026-07-24T19:14:29.577242Z","iopub.status.idle":"2026-07-24T19:14:40.488345Z","shell.execute_reply.started":"2026-07-24T19:14:29.577215Z","shell.execute_reply":"2026-07-24T19:14:40.48762Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cv2\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom pytorch_grad_cam import GradCAM\nfrom pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget\nfrom pytorch_grad_cam.utils.image import show_cam_on_image","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T19:14:40.489358Z","iopub.execute_input":"2026-07-24T19:14:40.489658Z","iopub.status.idle":"2026-07-24T19:14:41.730418Z","shell.execute_reply.started":"2026-07-24T19:14:40.489624Z","shell.execute_reply":"2026-07-24T19:14:41.729811Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.eval()\ntarget_layers = [model.layer4[-1]]\n\ncam = GradCAM(model=model, target_layers=target_layers)\n\nimages, labels = next(iter(evalLoader))\ninput_tensor = images[0].unsqueeze(0).to(device) # Add batch dimension\ninput_tensor.requires_grad = True\ntrue_label = labels[0].item()\ntargets = [ClassifierOutputTarget(true_label)]\ngrayscale_cam = cam(input_tensor=input_tensor, targets=targets)[0, :]\n\n\nmean = np.array([0.485, 0.456, 0.406])\nstd = np.array([0.229, 0.224, 0.225])\nimg = images[0].numpy().transpose((1, 2, 0))\nimg = std * img + mean\nimg = np.clip(img, 0, 1)\n\n\nvisualization = show_cam_on_image(img, grayscale_cam, use_rgb=True)\n\n\nidx_to_class = {v: k for k, v in dataset.class_to_idx.items()}\nclass_name = idx_to_class[true_label]\n\nfig, axs = plt.subplots(1, 2, figsize=(10, 5))\naxs[0].imshow(img)\naxs[0].set_title(f\"Original Image: {class_name}\")\naxs[0].axis('off')\n\naxs[1].imshow(visualization)\naxs[1].set_title(\"Grad-CAM Explanation\")\naxs[1].axis('off')\n\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T19:14:41.731162Z","iopub.execute_input":"2026-07-24T19:14:41.731584Z","iopub.status.idle":"2026-07-24T19:14:42.816581Z","shell.execute_reply.started":"2026-07-24T19:14:41.731565Z","shell.execute_reply":"2026-07-24T19:14:42.815824Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Gradio","metadata":{}},{"cell_type":"code","source":"import gradio as gr\nimport torch\nimport torch.nn as nn\nfrom torchvision import models, transforms\nfrom PIL import Image\nimport numpy as np\nfrom pytorch_grad_cam import GradCAM\nfrom pytorch_grad_cam.utils.image import show_cam_on_image\nfrom pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T19:14:42.817609Z","iopub.execute_input":"2026-07-24T19:14:42.817839Z","iopub.status.idle":"2026-07-24T19:14:51.084205Z","shell.execute_reply.started":"2026-07-24T19:14:42.817809Z","shell.execute_reply":"2026-07-24T19:14:51.08354Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nmodel = models.resnet18(weights=None) \n\n\nnum_ftrs = model.fc.in_features\nmodel.fc = nn.Sequential(\n    nn.Dropout(p=0.5), \n    nn.Linear(num_ftrs, 5)\n)\nmodel.load_state_dict(torch.load('/kaggle/working/astro_resnet18_best.pth', map_location=device))\nmodel = model.to(device)\nmodel.eval()\n\n\nval_transforms = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n\ntarget_layers = [model.layer4[-1]]\ncam = GradCAM(model=model, target_layers=target_layers)\n\n\nclass_names = [\"Spiral Galaxy\", \"Elliptical Galaxy\", \"Nebula\", \"Planetary Object\", \"Star Cluster\"]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T19:14:51.08502Z","iopub.execute_input":"2026-07-24T19:14:51.085184Z","iopub.status.idle":"2026-07-24T19:14:51.229236Z","shell.execute_reply.started":"2026-07-24T19:14:51.085167Z","shell.execute_reply":"2026-07-24T19:14:51.228532Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict_and_explain(input_image):\n    if input_image is None:\n        return None, None\n    \n    input_tensor = val_transforms(input_image).unsqueeze(0).to(device)\n    \n    input_tensor.requires_grad = True\n    outputs = model(input_tensor)\n    probabilities = torch.nn.functional.softmax(outputs[0], dim=0).detach().cpu().numpy()\n    \n    confidences = {class_names[i]: float(probabilities[i]) for i in range(5)}\n    predicted_class_idx = np.argmax(probabilities)\n    \n    targets = [ClassifierOutputTarget(predicted_class_idx)]\n    grayscale_cam = cam(input_tensor=input_tensor, targets=targets)[0, :]\n\n    img_resized = input_image.resize((224, 224))\n    img_array = np.array(img_resized) / 255.0\n    visualization = show_cam_on_image(img_array, grayscale_cam, use_rgb=True)\n    \n    return confidences, visualization","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T19:14:51.230145Z","iopub.execute_input":"2026-07-24T19:14:51.230377Z","iopub.status.idle":"2026-07-24T19:14:51.235926Z","shell.execute_reply.started":"2026-07-24T19:14:51.230353Z","shell.execute_reply":"2026-07-24T19:14:51.235213Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"with gr.Blocks(theme=gr.themes.Soft()) as demo:\n    gr.Markdown(\"# Astronomy Image Classifier\")\n    gr.Markdown(\"Upload an astronomical image. The model will classify it and generate an Attention Map to explain its decision.\")\n    \n    with gr.Row():\n        with gr.Column():\n            image_input = gr.Image(type=\"pil\", label=\"Upload Astronomical Image\")\n            submit_btn = gr.Button(\"Analyze Image\", variant=\"primary\")\n            \n        with gr.Column():\n            label_output = gr.Label(num_top_classes=5, label=\"Model Predictions\")\n            cam_output = gr.Image(label=\"Grad-CAM Attention Map\")\n            \n    submit_btn.click(fn=predict_and_explain, inputs=image_input, outputs=[label_output, cam_output])\n\ndemo.launch(share=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T19:14:51.236754Z","iopub.execute_input":"2026-07-24T19:14:51.236996Z","iopub.status.idle":"2026-07-24T19:14:54.009704Z","shell.execute_reply.started":"2026-07-24T19:14:51.236975Z","shell.execute_reply":"2026-07-24T19:14:54.009059Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Confusion Matrix and Inference Time  ","metadata":{}},{"cell_type":"code","source":"from sklearn.metrics import confusion_matrix, classification_report, accuracy_score\nimport time\nimport seaborn as sns\nimport matplotlib.pyplot as plt\nimport torch\nimport numpy as np\n\nmodel.eval()\nall_preds = []\nall_labels = []\nstart_time = time.time()\n\nwith torch.no_grad():\n    for images, labels in evalLoader:\n        images, labels = images.to(device), labels.to(device)\n        \n        outputs = model(images)\n        _, preds = torch.max(outputs, 1)\n        all_preds.extend(preds.cpu().numpy())\n        all_labels.extend(labels.cpu().numpy())\n\nend_time = time.time()\ntotal_time = end_time - start_time\n\nnum_images = len(testDataset)\ninference_time_per_image = (total_time / num_images) * 1000 \n\nprint(f\"Total Test Set Evaluation Time: {total_time:.2f} seconds\")\nprint(f\"Average Inference Time per Image: {inference_time_per_image:.2f} ms\")\n\naccuracy = accuracy_score(all_labels, all_preds)\nprint(f\"\\nFinal Test Accuracy: {accuracy * 100:.2f}%\")\n\nclass_names = [\"Spiral\", \"Elliptical\", \"Nebula\", \"Planetary\", \"Star Cluster\"]\nreport = classification_report(all_labels, all_preds, target_names=class_names)\nprint(report)\n\ncm = confusion_matrix(all_labels, all_preds)\n\nplt.figure(figsize=(10, 8))\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=class_names, yticklabels=class_names)\n\nplt.xlabel('Predicted Label', fontsize=12)\nplt.ylabel('True Label', fontsize=12)\nplt.title('Confusion Matrix', fontsize=12)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T19:14:54.010571Z","iopub.execute_input":"2026-07-24T19:14:54.010832Z","iopub.status.idle":"2026-07-24T19:15:00.04206Z","shell.execute_reply.started":"2026-07-24T19:14:54.010752Z","shell.execute_reply":"2026-07-24T19:15:00.041057Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import time\n\ndef benchmark_inference(model, device, image_size=224, num_warmup=10, num_runs=100):\n    model.eval()\n    dummy_input = torch.randn(1, 3, image_size, image_size).to(device)\n\n    with torch.no_grad():\n        for _ in range(num_warmup):\n            _ = model(dummy_input)\n\n    if device.type == 'cuda':\n        torch.cuda.synchronize()\n\n    timings = []\n    with torch.no_grad():\n        for _ in range(num_runs):\n            start = time.perf_counter()\n            _ = model(dummy_input)\n            if device.type == 'cuda':\n                torch.cuda.synchronize()\n            end = time.perf_counter()\n            timings.append((end - start) * 1000)  # ms\n\n    timings = np.array(timings)\n    print(f\"Device: {device}\")\n    print(f\"Mean inference time:   {timings.mean():.2f} ms\")\n    print(f\"Median inference time: {np.median(timings):.2f} ms\")\n    print(f\"Std dev:               {timings.std():.2f} ms\")\n    print(f\"Min / Max:             {timings.min():.2f} / {timings.max():.2f} ms\")\n    return timings\n\n_ = benchmark_inference(model, device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T19:15:00.04338Z","iopub.execute_input":"2026-07-24T19:15:00.043612Z","iopub.status.idle":"2026-07-24T19:15:03.651468Z","shell.execute_reply.started":"2026-07-24T19:15:00.043585Z","shell.execute_reply":"2026-07-24T19:15:03.65072Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torch.utils.data import Dataset\n\nclass HoldoutDataset(Dataset):\n    def __init__(self, class_dirs, class_to_idx, transform):\n        self.image_paths, self.labels = [], []\n        for class_name, folder in class_dirs.items():\n            idx = class_to_idx[class_name]\n            for fn in os.listdir(folder):\n                if fn.lower().endswith(('.jpg', '.jpeg', '.png')):\n                    self.image_paths.append(os.path.join(folder, fn))\n                    self.labels.append(idx)\n\n    def __len__(self):\n        return len(self.image_paths)\n\n    def __getitem__(self, i):\n        img = Image.open(self.image_paths[i]).convert(\"RGB\")\n        return testTransform(img), torch.tensor(self.labels[i], dtype=torch.long)\n\nholdout_dirs = {\n    \"Nebula\": \"holdout_nebula\",\n    \"Star Cluster\": \"holdout_star_cluster\",\n}\n\nholdoutDataset = HoldoutDataset(holdout_dirs, dataset.class_to_idx, testTransform)\nholdoutLoader = DataLoader(holdoutDataset, batch_size=16, shuffle=False)\n\nmodel.eval()\nholdout_preds, holdout_labels = [], []\nwith torch.no_grad():\n    for images, labels in holdoutLoader:\n        images = images.to(device)\n        outputs = model(images)\n        _, preds = torch.max(outputs, 1)\n        holdout_preds.extend(preds.cpu().numpy())\n        holdout_labels.extend(labels.numpy())\n\nholdout_acc = accuracy_score(holdout_labels, holdout_preds)\nprint(f\"Held-out (different-page) accuracy: {holdout_acc*100:.2f}%\")\nprint(classification_report(\n    holdout_labels, holdout_preds,\n    target_names=[\"Nebula\", \"Star Cluster\"],\n    labels=[dataset.class_to_idx[\"Nebula\"], dataset.class_to_idx[\"Star Cluster\"]]\n))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-24T19:15:45.824133Z","iopub.execute_input":"2026-07-24T19:15:45.824428Z","iopub.status.idle":"2026-07-24T19:15:48.283428Z","shell.execute_reply.started":"2026-07-24T19:15:45.824408Z","shell.execute_reply":"2026-07-24T19:15:48.282826Z"}},"outputs":[],"execution_count":null}]}