{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":10338,"databundleVersionId":862042,"sourceType":"competition"}],"dockerImageVersionId":30918,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# The bulk of the code has been copied but then adapted from the pneumonia detection challenge code by Koorosh Aslansefat:\n#https://www.kaggle.com/code/kooaslansefat/pneumonia-detection-using-resnet","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<a id=\"khs\"></a>\n# <div style=\"padding:20px;color:white;margin:0;font-size:20px;font-family:Georgia;text-align:left;display:fill;border-radius:5px;background-color:#254E58;overflow:hidden\"><b>Checking Kaggle Hardware Specs</b></div> ","metadata":{}},{"cell_type":"code","source":"!nvidia-smi\n!lscpu | head -n 15","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<a id=\"lib\"></a>\n# <div style=\"padding:20px;color:white;margin:0;font-size:20px;font-family:Georgia;text-align:left;display:fill;border-radius:5px;background-color:#254E58;overflow:hidden\"><b>Importing Required Libraries</b></div> ","metadata":{}},{"cell_type":"code","source":"!pip install grad-cam torchcam","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, classification_report, confusion_matrix # SD \n\nfrom pydicom import dcmread\nfrom PIL import Image\nfrom tqdm import tqdm\n\nimport torch\nimport torch.nn as nn\nfrom torchvision import models, datasets\nimport torchvision.transforms as transforms\nfrom torch.utils import data\n\nimport torch.nn.functional as F\nfrom torchvision.utils import make_grid, save_image\n\nfrom matplotlib import rcParams\nimport matplotlib.patches as patches\nimport matplotlib.pyplot as plt\nimport seaborn as sns #SD \nfrom math import ceil\n\nfrom pytorch_grad_cam import GradCAM\nfrom pytorch_grad_cam.utils.image import show_cam_on_image, preprocess_image\nfrom pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget\n\n# PyTorch XLA imports for TPU usage\n# import torch_xla\n# import torch_xla.core.xla_model as xm\n# import torch_xla.distributed.parallel_loader as pl\n# import torch_xla.utils.utils as xu\n# import torch_xla.distributed.xla_multiprocessing as xmp\n\n## Specify device object\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n# Set device to TPU\n#device = xm.xla_device()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<a id=\"labels\"></a>\n# <div style=\"padding:20px;color:white;margin:0;font-size:20px;font-family:Georgia;text-align:left;display:fill;border-radius:5px;background-color:#254E58;overflow:hidden\"><b>Loading Labels</b></div> ","metadata":{}},{"cell_type":"code","source":"label_data = pd.read_csv('../input/rsna-pneumonia-detection-challenge/stage_2_train_labels.csv')\ncolumns = ['patientId', 'Target']\nall_data = label_data\n\nlabel_data = label_data.filter(columns)\nlabel_data.head(5)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<a id=\"split\"></a>\n# <div style=\"padding:20px;color:white;margin:0;font-size:20px;font-family:Georgia;text-align:left;display:fill;border-radius:5px;background-color:#254E58;overflow:hidden\"><b>Spliting Train and Validation Sets</b></div> ","metadata":{"execution":{"iopub.status.busy":"2025-04-07T03:31:29.530457Z","iopub.execute_input":"2025-04-07T03:31:29.530877Z","iopub.status.idle":"2025-04-07T03:31:29.537103Z","shell.execute_reply.started":"2025-04-07T03:31:29.530838Z","shell.execute_reply":"2025-04-07T03:31:29.53577Z"}}},{"cell_type":"code","source":"train_labels, val_labels = train_test_split(label_data.values, test_size=0.1)\nprint(train_labels.shape)\nprint(val_labels.shape)\n\nprint(f'patientId: {train_labels[0][0]}, Target: {train_labels[0][1]}')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_f = '../input/rsna-pneumonia-detection-challenge/stage_2_train_images'\ntest_f = '../input/rsna-pneumonia-detection-challenge/stage_2_test_images'\n\ntrain_paths = [os.path.join(train_f, image[0]) for image in train_labels]\nval_paths = [os.path.join(train_f, image[0]) for image in val_labels]\n\nprint(len(train_paths))\nprint(len(val_paths))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<a id=\"sample\"></a>\n# <div style=\"padding:20px;color:white;margin:0;font-size:20px;font-family:Georgia;text-align:left;display:fill;border-radius:5px;background-color:#254E58;overflow:hidden\"><b>Checking Some Samples from Dataset</b></div> ","metadata":{}},{"cell_type":"code","source":"def imshow(num_to_show=9):\n    \n    plt.figure(figsize=(10,10))\n    \n    for i in range(num_to_show):\n        plt.subplot(3, 3, i+1)\n        plt.grid(False)\n        plt.xticks([])\n        plt.yticks([])\n        \n        img_dcm = dcmread(f'{train_paths[i+20]}.dcm')\n        img_np = img_dcm.pixel_array\n        plt.imshow(img_np, cmap=plt.cm.binary)\n        plt.xlabel(train_labels[i+20][1])\n\nimshow()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<a id=\"trans\"></a>\n# <div style=\"padding:20px;color:white;margin:0;font-size:20px;font-family:Georgia;text-align:left;display:fill;border-radius:5px;background-color:#254E58;overflow:hidden\"><b>Composing Transformations</b></div>  ","metadata":{}},{"cell_type":"code","source":"#Create two pytorch is a composition objects. One for training data and one for validation.\n# The compositio object in pytorcht bundles together multiple image transformations to be applied sequentially. \n# Apply more transformations to the training dataset and keep the validation one relatively unadjusted.\n\n#Training composition: Updated augemntation values and added Rotation and Jitter\n#  RandomHorizontalFlip: Flip it randomly (to generalize across left/right lung variations)\n#  RandomRotation: Rotate it (to make the model robust to misalignment)\n#  ColorJitter: Adjust brightness/contrast (to simulate differences in X-ray machine exposure)\n#  ToTensor: Convert to tensor. This is essential because it converts images to PyTorch-friendly tensors, \n#            reorders the dimensions, scales the values, and prepares the data for the model pipeline.\n#  Normalize: Normalize pixel values (centering around mean/standard deviation from ImageNet)\n\n#Without these the raw image data will be fed to the model (not ideal) and the model may struggle with overfitting\n\n#Data augmentation and normalization for training and validation\ntransform_train = transforms.Compose([\n    transforms.RandomHorizontalFlip(), \n    transforms.RandomRotation(10),# Randomly rotate image by +/-10 degrees\n    transforms.Resize(224),\n    transforms.ColorJitter(brightness=0.2, contrast=0.2),# Slightly vary brightness and contrast\n    transforms.ToTensor(),\n    transforms.Normalize([0.485], [0.229]) # Normalize using ImageNet mean and std\n])\n\n#Validation composition:\ntransform_val = transforms.Compose([\n    transforms.ToTensor(),\n    transforms.Normalize([0.485], [0.229])\n])\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<a id=\"data\"></a>\n# <div style=\"padding:20px;color:white;margin:0;font-size:20px;font-family:Georgia;text-align:left;display:fill;border-radius:5px;background-color:#254E58;overflow:hidden\"><b>Writing a Custom Dataset Function</b></div>   ","metadata":{}},{"cell_type":"code","source":"class Dataset(data.Dataset):\n    \n    def __init__(self, paths, labels, transform=None):\n        self.paths = paths\n        self.labels = labels\n        self.transform = transform\n    \n    def __getitem__(self, index):\n        #Reads the DICOM file — a common format used for medical imaging (e.g., chest X-rays, MRIs, CT scans).\n        image = dcmread(f'{self.paths[index]}.dcm') \n\n        # Extract the actual pixel data from the DICOM file as a NumPy array. \n        #DICOM images often contain metadata, and pixel_array gives you just the image content.\n        image = image.pixel_array \n        \n        # Normalizes pixel values from [0, 255] to [0.0, 1.0]. This is common in image preprocessing for neural networks — helps with numerical stability during training.\n        #Note: DICOM images can sometimes be in 12-bit (0–4095), so normalizing by 255 only makes sense if the pixel range is already in 8-bit.\n        image = image / 255.0 \n\n        #Rescale the normalized image back to the 0–255 range, just in case downstream functions (like PIL) expect 8-bit values.\n        #.clip(0, 255) ensures values stay within valid image bounds.\n        #.astype(np.uint8) converts it to 8-bit unsigned integers (standard for images).\n        image = (255*image).clip(0, 255).astype(np.uint8)\n        \n        ##Converts the NumPy array to a PIL Image.\n        # .convert('RGB') ensures the image has 3 channels, even if the original was grayscale — useful if your model (like ResNet) expects RGB inputs.\n        image = Image.fromarray(image).convert('RGB')\n\n        label = self.labels[index][1]\n\n        #Apply the transformation pipeline to the current image.\n        # This could include:\n        #  Data augmentation (flip, rotate, etc.)\n        #  Normalization\n        #  Tensor conversion (ToTensor)\n        # Ensures the image is in the correct format and distribution before feeding it into the model.\n        if self.transform is not None:\n            image = self.transform(image)\n        \n        \n        name = self.paths[index].split(\"/\")[-1]\n        GH = all_data['patientId']==name\n        FIL = all_data[GH]\n        #print(\"From the datset loader, name\", name)\n        box = [FIL['x'].values[0], FIL['y'].values[0], FIL['width'].values[0], FIL['height'].values[0]]\n            \n        return image, label, box\n    \n    def __len__(self):\n        \n        return len(self.paths)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Prepare training and validation dataloader","metadata":{}},{"cell_type":"code","source":"train_dataset = Dataset(train_paths, train_labels, transform=transform_train)\nval_dataset = Dataset(val_paths, val_labels, transform=transform_val)\ntrain_loader = data.DataLoader(dataset=train_dataset, batch_size=128, shuffle=True)\nval_loader = data.DataLoader(dataset=val_dataset, batch_size=128, shuffle=False)\n\ndef img_check(loader, output_text):\n    print(output_text)\n    batch = iter(loader)\n    images, labels, _ = next(batch)\n    \n    image_grid = make_grid(images[:4]) #torchvision.utils.\n    image_np = image_grid.numpy()\n    img = np.transpose(image_np, (1, 2, 0))\n    plt.imshow(img)\n    img.shape","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Check dataloader for training\nimg_check(train_loader, \"Loader (training):\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Check dataloader for validation\nimg_check(val_loader, \"Loader (val):\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<a id=\"model\"></a>\n# <div style=\"padding:20px;color:white;margin:0;font-size:20px;font-family:Georgia;text-align:left;display:fill;border-radius:5px;background-color:#254E58;overflow:hidden\"><b>Loading a Pre-trained ResNet Model and its Fine-tuning</b></div>    ","metadata":{}},{"cell_type":"code","source":"# Define model training and evaluation for multiple ResNet variants\nresnet_variants = {\n    'resnet18': models.resnet18,\n    'resnet34': models.resnet34,\n    'resnet50': models.resnet50\n}\n\nresults = {}","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Loop through each specified ResNet variant (e.g., resnet18, resnet34, resnet50)\n# Each iteration trains and evaluates a different model architecture\nfor model_name, model_fn in resnet_variants.items():\n    print(f\"\\nTraining {model_name.upper()}...\")\n\n    # Load the pre-trained model (weights from ImageNet)\n    # Load the pre-trained model using the updated 'weights' argument to avoid deprecation warnings\n    from torchvision.models import ResNet18_Weights, ResNet34_Weights, ResNet50_Weights\n    if model_name == 'resnet18':\n        model = model_fn(weights=ResNet18_Weights.IMAGENET1K_V1)\n    elif model_name == 'resnet34':\n        model = model_fn(weights=ResNet34_Weights.IMAGENET1K_V1)\n    elif model_name == 'resnet50':\n        model = model_fn(weights=ResNet50_Weights.IMAGENET1K_V1)\n        # Get the number of input features to the final fully connected (fc) layer\n    num_ftrs = model.fc.in_features\n        # Replace the fc layer with a new classifier suitable for binary classification\n    model.fc = nn.Sequential(\n        nn.Linear(num_ftrs, 128),\n        nn.ReLU(),\n        nn.Dropout(0.3),\n        nn.Linear(128, 2)\n    )\n        # Move model to the appropriate device (GPU if available)\n    model.to(device)\n\n        # Define the loss function for classification\n    criterion = nn.CrossEntropyLoss()\n        # Use the Adam optimizer with a small learning rate\n    optimizer = torch.optim.Adam(model.parameters(), lr=0.0001)\n        # Learning rate scheduler that decays the learning rate every 7 epochs\n    scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=7, gamma=0.1)\n\n        # Set the number of training epochs\n    num_epochs = 10\n        # Track the best accuracy to save the best-performing model\n    best_accuracy = 0\n\n    for epoch in range(num_epochs):\n        model.train()\n        running_loss = 0\n\n        for images, labels, _ in tqdm(train_loader):\n            images, labels = images.to(device), labels.to(device)\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n\n            optimizer.zero_grad()\n            loss.backward()\n            optimizer.step()\n\n            running_loss += loss.item()\n\n        scheduler.step()\n\n        model.eval()\n        all_preds, all_labels = [], []\n        with torch.no_grad():\n            for images, labels, _ in val_loader:\n                images, labels = images.to(device), labels.to(device)\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\n        acc = accuracy_score(all_labels, all_preds)\n        prec = precision_score(all_labels, all_preds)\n        rec = recall_score(all_labels, all_preds)\n        f1 = f1_score(all_labels, all_preds)\n\n        print(f\"Epoch [{epoch+1}/{num_epochs}] - Loss: {running_loss:.4f} | Acc: {acc:.4f} | Precision: {prec:.4f} | Recall: {rec:.4f} | F1: {f1:.4f}\")\n\n        if acc > best_accuracy:\n            best_accuracy = acc\n            torch.save(model.state_dict(), f'{model_name}_best_model.pth')\n\n    results[model_name] = {\n        'accuracy': acc,\n        'precision': prec,\n        'recall': rec,\n        'f1_score': f1,\n        'labels': all_labels,\n        'predictions': all_preds\n    }\n\n    print(f\"\\nConfusion Matrix for {model_name.upper()}:\")\n    conf_matrix = confusion_matrix(all_labels, all_preds)\n    sns.heatmap(conf_matrix, annot=True, fmt='d', cmap='Blues')\n    plt.xlabel('Predicted')\n    plt.ylabel('True')\n    plt.title(f'Confusion Matrix - {model_name.upper()}')\n    plt.show()\n\n    print(f\"\\nGrad-CAM Visualizations for {model_name.upper()}:\")\n    dataiter = iter(val_loader)\n    images, labels, _ = next(dataiter)\n    images, labels = images.to(device), labels.to(device)\n    outputs = model(images)\n    _, preds = torch.max(outputs, 1)\n\n    target_layers = [model.layer4[-1]]\n    cam = GradCAM(model=model, target_layers=target_layers, use_cuda=torch.cuda.is_available())\n\n    rgb_img = images[0].cpu().numpy().transpose(1, 2, 0)\n    rgb_img = np.clip(rgb_img * 0.229 + 0.485, 0, 1)\n    input_tensor = images[0].unsqueeze(0)\n    targets = [ClassifierOutputTarget(preds[0].item())]\n    grayscale_cam = cam(input_tensor=input_tensor, targets=targets)[0, :]\n    visualization = show_cam_on_image(rgb_img, grayscale_cam, use_rgb=True)\n\n    plt.imshow(visualization)\n    plt.title(f\"Grad-CAM - {model_name.upper()} - Predicted: {preds[0].item()}\")\n    plt.axis('off')\n    plt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"\\nAll training complete. Summary:\")\nfor name, metrics in results.items():\n    print(f\"\\n{name.upper()} - Accuracy: {metrics['accuracy']:.4f} | Precision: {metrics['precision']:.4f} | Recall: {metrics['recall']:.4f} | F1: {metrics['f1_score']:.4f}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}