{ "nbformat": 4, "nbformat_minor": 0, "metadata": { "colab": { "provenance": [], "gpuType": "T4", "authorship_tag": "ABX9TyM7c25lrmEww8m7ZUcLm9mv", "include_colab_link": true }, "kernelspec": { "name": "python3", "display_name": "Python 3" }, "language_info": { "name": "python" }, "accelerator": "GPU" }, "cells": [ { "cell_type": "markdown", "metadata": { "id": "view-in-github", "colab_type": "text" }, "source": [ "\"Open" ] }, { "cell_type": "markdown", "source": [ "# **Model Training and Fine-Tuning**\n", "\n", "**Objective:** Ingest the Kaggle Chest X-Ray dataset, apply strategic data augmentation, handle severe class imbalance via loss weighting, and fine-tune a pre-trained DenseNet121 architecture to detect pneumonia.\n", "\n", "## **Environment Setup**\n", "First, we import the necessary PyTorch libraries and configure our hardware accelerator. Because image processing is computationally expensive, we must ensure PyTorch detects and utilizes the Colab GPU (CUDA)." ], "metadata": { "id": "24IiOoFPN2oN" } }, { "cell_type": "code", "source": [ "%%capture install_output\n", "!pip install optuna" ], "metadata": { "id": "OwBert5NpQwF" }, "execution_count": null, "outputs": [] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "IGGkAlCRNlro", "outputId": "69681eec-4dca-43fc-f0c3-489d8d514d71" }, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Compute Device: cuda\n", "GPU Model: Tesla T4\n" ] } ], "source": [ "import torch\n", "import torch.nn as nn\n", "import torch.optim as optim\n", "from torchvision import datasets, transforms, models\n", "from torch.utils.data import DataLoader\n", "import os\n", "import optuna\n", "import time\n", "import copy\n", "import numpy as np\n", "import torch.nn.functional as F\n", "from sklearn.metrics import classification_report, confusion_matrix\n", "import matplotlib.pyplot as plt\n", "import seaborn as sns\n", "\n", "# Configure the device\n", "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n", "print(f'Compute Device: {device}')\n", "if device.type == 'cuda':\n", " print(f'GPU Model: {torch.cuda.get_device_name(0)}')" ] }, { "cell_type": "markdown", "source": [ "## **Data Preprocessing and Augmentation Pipeline**\n", "As determined in our EDA, we must standardize all input resolutions to 224x224.\n", "\n", "To prevent overfitting without introducing artificial artifacts, we apply light data augmentation **only to the training set** (rotation and color jitter). The validation and test sets remain strictly unaltered to provide an honest evaluation metric." ], "metadata": { "id": "2SGyO1PxOmDX" } }, { "cell_type": "code", "source": [ "# Define the dataset paths\n", "data_dir = '../dataset/chest_xray'\n", "train_dir = os.path.join(data_dir, 'train')\n", "val_dir = os.path.join(data_dir, 'val')\n", "test_dir = os.path.join(data_dir, 'test')\n", "\n", "# 1. Training Transforms (with Augmentation)\n", "train_transforms = transforms.Compose([\n", " transforms.Resize((224, 224)),\n", " transforms.RandomRotation(10), # Slight rotation to simulate patient misalignment\n", " transforms.ColorJitter(brightness=0.2, contrast=0.2), # Account for different X-ray exposures\n", " transforms.ToTensor(),\n", " transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) # Standard ImageNet normalization\n", "])\n", "\n", "# 2. Validation/Testing Transforms (Strictly unaltered)\n", "eval_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", "\n", "print(\"Transforms configured successfully.\")" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "ikceEotXOU_1", "outputId": "077395cc-6cf0-42f5-e0b3-65a1e322b5a8" }, "execution_count": null, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Transforms configured successfully.\n" ] } ] }, { "cell_type": "markdown", "source": [ "## **Initializing Datasets and DataLoaders**\n", "\n", "With our transforms defined, we will use PyTorch's `ImageFolder` to automatically read our directory structure and assign the correct labels (0 for Normal, 1 for Pneumonia).\n", "\n", "We will then wrap these datasets in `DataLoaders`. DataLoaders handle the heavy lifting of batching the images, shuffling them, and pushing them to the GPU. We are setting the `batch_size` to 64 to fully utilize the Google Colab GPU memory." ], "metadata": { "id": "odyLrQARmmn-" } }, { "cell_type": "code", "source": [ "# 1. Create the Dataset objects\n", "train_dataset = datasets.ImageFolder(train_dir, transform=train_transforms)\n", "val_dataset = datasets.ImageFolder(val_dir, transform=eval_transforms)\n", "test_dataset = datasets.ImageFolder(test_dir, transform=eval_transforms)\n", "\n", "print(f\"Training images: {len(train_dataset)}\")\n", "print(f\"Validation images: {len(val_dataset)}\")\n", "print(f\"Testing images: {len(test_dataset)}\")\n", "print(f\"Detected Classes: {train_dataset.classes}\")\n", "\n", "# 2. Create the DataLoaders\n", "BATCH_SIZE = 64\n", "\n", "# We shuffle the training data so the model doesn't learn sequence patterns\n", "train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=2, pin_memory=True)\n", "\n", "# We do not need to shuffle validation/testing data\n", "val_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)\n", "test_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)\n", "\n", "print(\"DataLoaders are ready!\")" ], "metadata": { "id": "I6obJOa1QYMy", "colab": { "base_uri": "https://localhost:8080/" }, "outputId": "3a3b35dc-01e5-4980-bdfc-0958467a5f24" }, "execution_count": null, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Training images: 5216\n", "Validation images: 16\n", "Testing images: 624\n", "Detected Classes: ['NORMAL', 'PNEUMONIA']\n", "DataLoaders are ready!\n" ] } ] }, { "cell_type": "markdown", "source": [ "## **Model Architecture and Class Weighting**\n", "\n", "Instead of training a model from scratch, we use **Transfer Learning**. We will download DenseNet121 (which already knows how to detect edges, textures, and shapes from millions of standard images) and replace its final classification layer with a custom one designed specifically for our two classes (Normal vs. Pneumonia).\n", "\n", "Simultaneously, we will calculate the inverse frequency of our classes to generate **Class Weights**, injecting them directly into the PyTorch loss function to penalize the model more heavily if it misclassifies a minority \"Normal\" scan." ], "metadata": { "id": "MPPAHhh2nlVz" } }, { "cell_type": "code", "source": [ "# --- 1. Handling the Imbalance (Class Weights) ---\n", "# ImageFolder automatically stores the label for every image in the .targets attribute\n", "labels = train_dataset.targets\n", "class_counts = np.bincount(labels)\n", "total_samples = len(labels)\n", "num_classes = len(train_dataset.classes)\n", "\n", "# Inverse frequency formula: Weight = Total / (NumClasses * ClassCount)\n", "weights = total_samples / (num_classes * class_counts)\n", "class_weights = torch.FloatTensor(weights).to(device)\n", "\n", "print(f\"Class Counts: NORMAL={class_counts[0]}, PNEUMONIA={class_counts[1]}\")\n", "print(f\"Applied Weights: NORMAL={weights[0]:.4f}, PNEUMONIA={weights[1]:.4f}\\n\")\n", "\n", "# --- 2. Model Architecture (DenseNet121) ---\n", "# Download the pre-trained neural network\n", "model = models.densenet121(weights=models.DenseNet121_Weights.IMAGENET1K_V1)\n", "\n", "# Freeze the core convolutional layers so we don't destroy the pre-trained visual features\n", "for param in model.parameters():\n", " param.requires_grad = False\n", "\n", "# Replace the final classification layer (originally 1000 classes) with our custom 2-class head\n", "num_ftrs = model.classifier.in_features\n", "\n", "# Push the entire model architecture to the GPU\n", "model = model.to(device)\n", "\n", "# --- 3. Loss Function and Optimizer ---\n", "# Inject our calculated class weights into the loss function\n", "criterion = nn.CrossEntropyLoss(weight=class_weights)\n", "\n", "# We only want the optimizer to update our newly added classifier layers\n", "optimizer = optim.Adam(model.classifier.parameters(), lr=0.001)\n", "\n", "print(\"Model architecture, optimizer, and weighted loss function configured successfully!\")" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "3cTqFqcvnVG2", "outputId": "19533754-f990-4485-b848-d9c2be3f782b" }, "execution_count": null, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Class Counts: NORMAL=1341, PNEUMONIA=3875\n", "Applied Weights: NORMAL=1.9448, PNEUMONIA=0.6730\n", "\n", "Model architecture, optimizer, and weighted loss function configured successfully!\n" ] } ] }, { "cell_type": "markdown", "source": [ "## **Hyperparameter Tuning with Optuna and Model Training**\n", "\n", "Instead of manually guessing the best hyperparameters, we use **Optuna** to perform an automated Bayesian search.\n", "\n", "We will wrap our standard PyTorch training loop inside an Optuna `objective` function. Optuna will test different combinations of:\n", "1. **Learning Rates** (Logarithmic scale)\n", "2. **Dropout Rates** (To find the sweet spot for preventing overfitting)\n", "3. **Optimizers** (Adam vs. SGD)\n", "\n", "*Note: To manage Google Colab compute limits, we restrict the search to a small number of epochs and trials. Optuna's built-in \"Pruning\" will automatically kill unpromising trials early to save GPU time.*" ], "metadata": { "id": "31zYMNWrqGYj" } }, { "cell_type": "code", "source": [ "# We will store the best model weights globally so we can save them after the study\n", "best_model_wts = copy.deepcopy(model.state_dict())\n", "highest_accuracy = 0.0\n", "\n", "def objective(trial):\n", " global best_model_wts, highest_accuracy\n", "\n", " # 1. Let Optuna suggest hyperparameters for this specific trial\n", " dropout_rate = trial.suggest_float(\"dropout_rate\", 0.2, 0.6)\n", " lr = trial.suggest_float(\"lr\", 1e-5, 1e-2, log=True)\n", " optimizer_name = trial.suggest_categorical(\"optimizer\", [\"Adam\", \"SGD\"])\n", "\n", " # 2. Re-build the custom classifier with the suggested dropout rate\n", " model.classifier = nn.Sequential(\n", " nn.Linear(num_ftrs, 256),\n", " nn.ReLU(),\n", " nn.Dropout(dropout_rate),\n", " nn.Linear(256, 2)\n", " ).to(device)\n", "\n", " # 3. Assign the suggested optimizer\n", " if optimizer_name == \"Adam\":\n", " optimizer = optim.Adam(model.classifier.parameters(), lr=lr)\n", " else:\n", " optimizer = optim.SGD(model.classifier.parameters(), lr=lr, momentum=0.9)\n", "\n", " EPOCHS = 3 # Keep epochs low per trial for time constraints\n", "\n", " for epoch in range(EPOCHS):\n", " # --- TRAINING PHASE ---\n", " model.train()\n", " running_loss = 0.0\n", "\n", " for inputs, labels in train_loader:\n", " inputs, labels = inputs.to(device), labels.to(device)\n", "\n", " optimizer.zero_grad()\n", " outputs = model(inputs)\n", " loss = criterion(outputs, labels) # Uses the class_weights we defined earlier!\n", " loss.backward()\n", " optimizer.step()\n", "\n", " running_loss += loss.item() * inputs.size(0)\n", "\n", " # --- VALIDATION PHASE ---\n", " model.eval()\n", " correct = 0\n", " total = 0\n", "\n", " with torch.no_grad():\n", " for inputs, labels in val_loader:\n", " inputs, labels = inputs.to(device), labels.to(device)\n", " outputs = model(inputs)\n", " _, predicted = torch.max(outputs, 1)\n", " total += labels.size(0)\n", " correct += (predicted == labels).sum().item()\n", "\n", " val_acc = correct / total\n", "\n", " # --- OPTUNA PRUNING & SAVING ---\n", " # Report accuracy to Optuna. If it's performing terribly, Optuna kills the trial.\n", " trial.report(val_acc, epoch)\n", " if trial.should_prune():\n", " raise optuna.exceptions.TrialPruned()\n", "\n", " # Save the best weights if this trial beats our global high score\n", " if val_acc > highest_accuracy:\n", " highest_accuracy = val_acc\n", " best_model_wts = copy.deepcopy(model.state_dict())\n", "\n", " return val_acc # Optuna uses this final returned value to judge the trial\n", "\n", "# --- RUN THE OPTUNA STUDY ---\n", "print(\"Starting Optuna Hyperparameter Search...\")\n", "start_time = time.time()\n", "\n", "study = optuna.create_study(direction=\"maximize\")\n", "# We run 5 trials. (Increase this if you have Colab Pro and more time)\n", "study.optimize(objective, n_trials=5)\n", "\n", "time_elapsed = time.time() - start_time\n", "print(f\"\\nOptimization complete in {time_elapsed // 60:.0f}m {time_elapsed % 60:.0f}s\")\n", "print(f\"Best Trial Accuracy: {study.best_trial.value:.4f}\")\n", "print(\"Best Hyperparameters:\", study.best_trial.params)\n", "\n", "# Load the absolute best weights back into the model for final export\n", "model.load_state_dict(best_model_wts)\n", "print(\"Best model weights restored and ready for testing.\")" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "HrV6nUsjodCM", "outputId": "ff77d837-8183-4f16-ea06-4979fd6e540a" }, "execution_count": null, "outputs": [ { "output_type": "stream", "name": "stderr", "text": [ "[I 2026-05-28 22:18:23,269] A new study created in memory with name: no-name-045ce5dc-ed4f-4a58-8341-754b15b1d97b\n" ] }, { "output_type": "stream", "name": "stdout", "text": [ "Starting Optuna Hyperparameter Search...\n" ] }, { "output_type": "stream", "name": "stderr", "text": [ "[I 2026-05-28 22:22:44,758] Trial 0 finished with value: 0.625 and parameters: {'dropout_rate': 0.3761532058720902, 'lr': 1.417924247896451e-05, 'optimizer': 'SGD'}. Best is trial 0 with value: 0.625.\n", "[I 2026-05-28 22:27:02,814] Trial 1 finished with value: 0.9375 and parameters: {'dropout_rate': 0.4806277944716644, 'lr': 0.007419660171950955, 'optimizer': 'SGD'}. Best is trial 1 with value: 0.9375.\n", "[I 2026-05-28 22:31:19,752] Trial 2 finished with value: 0.875 and parameters: {'dropout_rate': 0.4749246311849121, 'lr': 0.002914222867247111, 'optimizer': 'SGD'}. Best is trial 1 with value: 0.9375.\n", "[I 2026-05-28 22:35:36,219] Trial 3 finished with value: 0.9375 and parameters: {'dropout_rate': 0.5587573900041348, 'lr': 0.000938365213167857, 'optimizer': 'Adam'}. Best is trial 1 with value: 0.9375.\n", "[I 2026-05-28 22:39:51,671] Trial 4 finished with value: 0.8125 and parameters: {'dropout_rate': 0.5357695028670346, 'lr': 8.813243604021709e-05, 'optimizer': 'SGD'}. Best is trial 1 with value: 0.9375.\n" ] }, { "output_type": "stream", "name": "stdout", "text": [ "\n", "Optimization complete in 21m 28s\n", "Best Trial Accuracy: 0.9375\n", "Best Hyperparameters: {'dropout_rate': 0.4806277944716644, 'lr': 0.007419660171950955, 'optimizer': 'SGD'}\n", "Best model weights restored and ready for testing.\n" ] } ] }, { "cell_type": "markdown", "source": [ "## **Final Evaluation and Model Export**\n", "\n", "With our Optuna-optimized weights loaded, we must evaluate the model against the **Test Dataset**. This dataset contains images the model has never seen during training or hyperparameter tuning, providing an honest assessment of how it will perform in the real world.\n", "\n", "Finally, we export the tuned architecture weights to a `.pth` file. This is the \"brain\" that our FastAPI backend will load to generate real-time Grad-CAM heatmaps." ], "metadata": { "id": "HgGk_69gybfq" } }, { "cell_type": "code", "source": [ "# --- 1. Evaluate on the Test Set ---\n", "model.eval()\n", "test_loss = 0.0\n", "correct = 0\n", "total = 0\n", "all_preds = []\n", "all_labels = []\n", "\n", "print(\"Evaluating on Test Dataset...\")\n", "\n", "with torch.no_grad():\n", " for inputs, labels in test_loader:\n", " inputs, labels = inputs.to(device), labels.to(device)\n", "\n", " outputs = model(inputs)\n", " loss = criterion(outputs, labels)\n", " test_loss += loss.item() * inputs.size(0)\n", "\n", " # Get predictions\n", " probabilities = F.softmax(outputs, dim=1)\n", " _, predicted = torch.max(probabilities, 1)\n", "\n", " total += labels.size(0)\n", " correct += (predicted == labels).sum().item()\n", "\n", " # Store for metrics\n", " all_preds.extend(predicted.cpu().numpy())\n", " all_labels.extend(labels.cpu().numpy())\n", "\n", "test_accuracy = correct / total\n", "test_loss = test_loss / total\n", "\n", "print(f\"Test Accuracy: {test_accuracy * 100:.2f}%\")\n", "print(f\"Test Loss: {test_loss:.4f}\\n\")\n", "\n", "# --- 2. Classification Report & Confusion Matrix ---\n", "target_names = train_dataset.classes\n", "print(\"Classification Report:\")\n", "print(classification_report(all_labels, all_preds, target_names=target_names))\n", "\n", "# Plotting the Confusion Matrix\n", "cm = confusion_matrix(all_labels, all_preds)\n", "plt.figure(figsize=(6, 5))\n", "sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=target_names, yticklabels=target_names)\n", "plt.title('Test Dataset Confusion Matrix')\n", "plt.ylabel('Actual Diagnosis')\n", "plt.xlabel('Predicted Diagnosis')\n", "plt.show()\n", "\n", "# --- 3. Export the Model Weights ---\n", "export_path = \"densenet_pneumonia_finetuned.pth\"\n", "torch.save(model.state_dict(), export_path)\n", "print(f\"\\n✅ Model weights successfully exported to: {export_path}\")" ], "metadata": { "id": "Wr-7CrqRqs4w", "outputId": "486eaf48-3dd2-452c-a486-da7f2b8317ef", "colab": { "base_uri": "https://localhost:8080/", "height": 773 } }, "execution_count": null, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Evaluating on Test Dataset...\n", "Test Accuracy: 86.38%\n", "Test Loss: 0.3476\n", "\n", "Classification Report:\n", " precision recall f1-score support\n", "\n", " NORMAL 0.89 0.73 0.80 234\n", " PNEUMONIA 0.85 0.95 0.90 390\n", "\n", " accuracy 0.86 624\n", " macro avg 0.87 0.84 0.85 624\n", "weighted avg 0.87 0.86 0.86 624\n", "\n" ] }, { "output_type": "display_data", "data": { "text/plain": [ "
" ], "image/png": "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\n" }, "metadata": {} }, { "output_type": "stream", "name": "stdout", "text": [ "\n", "✅ Model weights successfully exported to: densenet_pneumonia_finetuned.pth\n" ] } ] }, { "cell_type": "markdown", "source": [ "## **Clinical Assessment and Conclusion**\n", "\n", "Our fine-tuned DenseNet121 architecture has been evaluated on the completely unseen Test Dataset. To understand its true clinical viability, we must look past raw accuracy and analyze the Confusion Matrix.\n", "\n", "### **Clinical Metrics Breakdown**\n", "* **True Negatives (170):** Healthy lungs correctly identified.\n", "* **True Positives (369):** Pneumonia cases successfully detected.\n", "* **False Positives (64):** Healthy lungs flagged as potential pneumonia (Type I Error).\n", "* **False Negatives (21):** Actual pneumonia cases missed by the model (Type II Error).\n", "\n", "### **The \"Recall\" Victory**\n", "In precision diagnostics, minimizing False Negatives is the absolute highest priority. Sending a sick patient home without treatment is a severe failure.\n", "\n", "Our model achieved a **Pneumonia Recall (Sensitivity) of 94.6%** ($369 / 390$). Out of 390 sick patients, the model only missed 21. The higher number of False Positives (64) is an acceptable clinical trade-off, acting as a cautious safety net that simply prompts a human physician for a secondary review. This proves that our strategy of applying **Class Weights** during training successfully forced the model to prioritize pathological detection.\n", "\n", "---\n", "**Next Phase: Building the Inference Engine**\n", "With the `densenet_pneumonia_finetuned.pth` weights successfully exported, the core machine learning phase is complete. We will now transition out of the Jupyter environment and begin building the production MLOps pipeline. The next step is to construct a **FastAPI backend** that will load this \"brain,\" process incoming X-ray uploads in real-time, and generate **Grad-CAM explainability heatmaps** to show the doctors exactly *where* the model is looking." ], "metadata": { "id": "DWEkHU0AzaZJ" } }, { "cell_type": "markdown", "source": [ "## **Hugging Face**" ], "metadata": { "id": "fjado_xqZDQJ" } }, { "cell_type": "code", "source": [ "# 1. Retrieve the token (Checks Colab Secrets first, then Environment variables)\n", "try:\n", " hf_token = userdata.get('HF_TOKEN')\n", "except userdata.SecretNotFoundError:\n", " hf_token = os.environ.get(\"HF_TOKEN\")\n", "\n", "if not hf_token:\n", " raise ValueError(\"HF_TOKEN not found! Please set it in Colab Secrets or export it.\")\n", "\n", "# 2. Define your repository setup\n", "hf_username = \"\" # e.g., mobadara\n", "repo_name = \"pneumonia-densenet121\"\n", "repo_id = f\"{hf_username}/{repo_name}\"\n", "model_path = \"densenet_pneumonia_finetuned.pth\"\n", "\n", "# 3. Create the repository\n", "api = HfApi(token=hf_token)\n", "print(f\"Creating/Checking repository: {repo_id}...\")\n", "create_repo(repo_id=repo_id, token=hf_token, exist_ok=True)\n", "\n", "# 4. Generate the Model Card with YAML Metadata\n", "model_card_content = \"\"\"---\n", "language: en\n", "license: mit\n", "tags:\n", "- image-classification\n", "- vision\n", "- pytorch\n", "- densenet\n", "- medical\n", "- healthcare\n", "datasets:\n", "- paultimothymooney/chest-xray-pneumonia\n", "metrics:\n", "- accuracy\n", "- recall\n", "---\n", "# DenseNet121 - Pediatric Pneumonia Detection\n", "\n", "## Model Description\n", "This is a fine-tuned DenseNet121 model trained to classify chest X-rays into two categories: **NORMAL** and **PNEUMONIA**. It was developed as part of a precision diagnostics AI pipeline to assist clinical workflows.\n", "\n", "To handle the severe class imbalance inherent in the dataset, dynamic class weights were applied to the PyTorch `CrossEntropyLoss` function, heavily penalizing false negatives to prioritize patient safety.\n", "\n", "## Intended Uses & Limitations\n", "* **Intended Use:** As an auxiliary tool for clinical triage and radiologist workflow acceleration. It is designed to be paired with explainability tools (like Grad-CAM) to provide visual context.\n", "* **Limitations:** This model is a research prototype and is not FDA-approved. It should not be used as a standalone diagnostic tool.\n", "\n", "## Training Data\n", "Trained on the Kaggle Chest X-Ray Images (Pneumonia) dataset. All images were normalized and resized to 224x224. Light data augmentation (rotation and color jittering) was applied to the training set to prevent overfitting.\n", "\n", "## Evaluation Results\n", "The model was evaluated on a completely unseen test dataset consisting of 624 images.\n", "* **Overall Accuracy:** 86.4%\n", "* **Pneumonia Recall (Sensitivity):** 94.6% (Successfully identified 369 out of 390 sick patients)\n", "\n", "The high recall indicates the model heavily prioritizes catching pathology, treating false positives as an acceptable clinical trade-off requiring secondary physician review.\n", "\"\"\"\n", "\n", "# Save the card locally\n", "with open(\"README.md\", \"w\") as f:\n", " f.write(model_card_content)\n", "\n", "# 5. Upload the files\n", "print(\"Uploading Model Weights...\")\n", "api.upload_file(\n", " path_or_fileobj=model_path,\n", " path_in_repo=model_path,\n", " repo_id=repo_id,\n", " commit_message=\"Initial push: Fine-tuned DenseNet weights\"\n", ")\n", "\n", "print(\"Uploading Model Card...\")\n", "api.upload_file(\n", " path_or_fileobj=\"README.md\",\n", " path_in_repo=\"README.md\",\n", " repo_id=repo_id,\n", " commit_message=\"Add professional model card with YAML metadata\"\n", ")\n", "\n", "print(f\"✅ Successfully deployed to Hugging Face! View it here: https://huggingface.co/{repo_id}\")" ], "metadata": { "id": "Dwm-2sodZCUk" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "## **GitHub Release**" ], "metadata": { "id": "Javrpd_gZZiK" } }, { "cell_type": "code", "source": [ "# Safely extract the token and assign it to an environment variable\n", "os.environ[\"GITHUB_TOKEN\"] = userdata.get(\"GITHUB_TOKEN\")\n", "print(\"✅ Token securely loaded into environment!\")" ], "metadata": { "id": "Uq2QnjXJZr_g" }, "execution_count": null, "outputs": [] }, { "cell_type": "code", "source": [ "%%bash\n", "%%bash\n", "# 1. Variables (Notice GITHUB_TOKEN is missing here, we read it directly from the environment!)\n", "REPO_OWNER=\"mobadara\"\n", "REPO_NAME=\"precision-diagnostics-xai\"\n", "MODEL_FILE=\"densenet_pneumonia_finetuned.pth\"\n", "TAG=\"v1.0.0\"\n", "\n", "# 2. Install the GitHub CLI into Colab\n", "curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \\\n", "&& sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \\\n", "&& echo \"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main\" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \\\n", "&& sudo apt update -q && sudo apt install gh -y -q\n", "\n", "# 3. Authenticate GitHub CLI securely using the environment variable\n", "echo \"$GITHUB_TOKEN\" | gh auth login --with-token\n", "\n", "# 4. Create the Release and upload the file simultaneously\n", "echo \"Creating GitHub Release $TAG...\"\n", "gh release create $TAG $MODEL_FILE \\\n", " --repo \"$REPO_OWNER/$REPO_NAME\" \\\n", " --title \"v1.0.0 - Baseline DenseNet121 Model\" \\\n", " --notes \"Initial release of the fine-tuned DenseNet121 weights for Pneumonia detection. Achieved 94.6% recall on the test set. File attached for backend deployment.\"\n", "\n", "echo \"✅ Successfully created GitHub Release and uploaded weights!\"" ], "metadata": { "id": "o4_40DdsZhIb" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "___\n", "Muyiwa J. Obadara" ], "metadata": { "id": "-6LkhH6WzlIF" } }, { "cell_type": "code", "source": [], "metadata": { "id": "D5I3Ujm6yyOh" }, "execution_count": null, "outputs": [] } ] }