{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":113002,"databundleVersionId":13471427,"sourceType":"competition"},{"sourceId":257537682,"sourceType":"kernelVersion"},{"sourceId":260249782,"sourceType":"kernelVersion"}],"dockerImageVersionId":31089,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-09-05T12:38:53.424771Z","iopub.execute_input":"2025-09-05T12:38:53.425021Z","iopub.status.idle":"2025-09-05T12:41:53.943498Z","shell.execute_reply.started":"2025-09-05T12:38:53.424994Z","shell.execute_reply":"2025-09-05T12:41:53.942599Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 📊 Exploratory Data Analysis (EDA)\n\nIn this section, we perform initial exploration of the dataset to understand its structure, completeness, and label distribution.  \n\n---\n\n### 1. Dataset Overview\nWe start by loading the training CSV file and previewing the first 10 rows.\n\n- **Total Images** → Total number of X-ray images available.  \n- **Total Patients** → Unique patient identifiers present in the dataset.  \n- **Total Studies** → Number of unique studies conducted.  \n\nThis gives a quick sense of dataset size and coverage.\n\n---\n\n### 2. Handling Missing Values\n- **Age** → Missing ages are imputed with the **median** age.  \n- **Sex** → Missing sex values are filled with `\"Unknown\"`.  \n\nThis ensures no missing values remain in critical demographic columns.\n\n---\n\n### 3. Label Columns\nWe define 14 condition labels for classification:  \n\n- **No Finding**, **Lung Opacity**, **Support Devices**, **Atelectasis**,  \n- **Cardiomegaly**, **Pleural Effusion**, **Enlarged Cardiomediastinum**,  \n- **Edema**, **Consolidation**, **Pneumonia**, **Fracture**,  \n- **Lung Lesion**, **Pneumothorax**, **Pleural Other**.  \n\nFor each condition, we calculate:  \n- **Count** → Number of images with the condition.  \n- **Percent (%)** → Prevalence as a percentage of the dataset.  \n\nThis helps us understand class imbalance and disease prevalence.\n\n---\n\n### 4. Data Quality Checks\nTo ensure data integrity, we check for the following:\n\n- **Duplicate Images** → Verifies if any images are repeated.  \n- **Duplicate Patients** → Expected, since a patient can have multiple images.  \n- **Invalid Age Values** → Counts number of negative ages (should be zero).  \n\n---\n\n### 5. Outputs\n- **Summary metrics** (images, patients, studies).  \n- **Prevalence table** showing condition counts and percentages.  \n- **Data quality reports** on duplicates and invalid values.  \n\nThis forms the foundation of our dataset understanding before moving into deeper analysis and modeling.\n","metadata":{}},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\ntrain_df = pd.read_csv('/kaggle/input/grand-xray-slam-division-b/train2.csv')\ntrain_df.head(10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-06T11:45:28.927901Z","iopub.execute_input":"2025-09-06T11:45:28.928651Z","iopub.status.idle":"2025-09-06T11:45:29.155765Z","shell.execute_reply.started":"2025-09-06T11:45:28.928624Z","shell.execute_reply":"2025-09-06T11:45:29.155107Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df.info()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-06T11:45:29.156958Z","iopub.execute_input":"2025-09-06T11:45:29.15765Z","iopub.status.idle":"2025-09-06T11:45:29.191867Z","shell.execute_reply.started":"2025-09-06T11:45:29.157631Z","shell.execute_reply":"2025-09-06T11:45:29.191321Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Summarize key metrics\ntotal_images = len(train_df)\ntotal_patients = train_df['Patient_ID'].nunique()\ntotal_studies = train_df['Study'].nunique()\nprint(f\"Total Images: {total_images}\")\nprint(f\"Total Patients: {total_patients}\")\nprint(f\"Total Studies: {total_studies}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-06T11:45:29.280633Z","iopub.execute_input":"2025-09-06T11:45:29.281045Z","iopub.status.idle":"2025-09-06T11:45:29.287808Z","shell.execute_reply.started":"2025-09-06T11:45:29.281026Z","shell.execute_reply":"2025-09-06T11:45:29.287067Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df.isnull().sum()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-06T11:45:31.731299Z","iopub.execute_input":"2025-09-06T11:45:31.731764Z","iopub.status.idle":"2025-09-06T11:45:31.759349Z","shell.execute_reply.started":"2025-09-06T11:45:31.731741Z","shell.execute_reply":"2025-09-06T11:45:31.758773Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df['Age'] = train_df['Age'].fillna(train_df['Age'].median())\ntrain_df['Sex'] = train_df['Sex'].fillna('Unknown')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-06T11:45:31.882662Z","iopub.execute_input":"2025-09-06T11:45:31.882934Z","iopub.status.idle":"2025-09-06T11:45:31.895583Z","shell.execute_reply.started":"2025-09-06T11:45:31.882913Z","shell.execute_reply":"2025-09-06T11:45:31.894812Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df.isnull().sum()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-06T11:45:32.050477Z","iopub.execute_input":"2025-09-06T11:45:32.050676Z","iopub.status.idle":"2025-09-06T11:45:32.077637Z","shell.execute_reply.started":"2025-09-06T11:45:32.050661Z","shell.execute_reply":"2025-09-06T11:45:32.077154Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Define the 14 condition columns\nlabel_columns = ['No Finding', 'Lung Opacity', 'Support Devices', 'Atelectasis',\n                 'Cardiomegaly', 'Pleural Effusion', 'Enlarged Cardiomediastinum',\n                 'Edema', 'Consolidation', 'Pneumonia', 'Fracture', 'Lung Lesion',\n                 'Pneumothorax', 'Pleural Other']\n\n# Calculate counts and percentages for each condition\nlabel_counts = train_df[label_columns].sum()\nlabel_percentages = (label_counts / total_images * 100).round(2)\nprevalence_df = pd.DataFrame({\n    'Condition': label_counts.index,\n    'Count': label_counts.values,\n    'Percent (%)': label_percentages.values\n})\n\n# Display prevalence table\nprint(\"Label Prevalence:\")\nprint(prevalence_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-06T11:45:34.003255Z","iopub.execute_input":"2025-09-06T11:45:34.003527Z","iopub.status.idle":"2025-09-06T11:45:34.01502Z","shell.execute_reply.started":"2025-09-06T11:45:34.003506Z","shell.execute_reply":"2025-09-06T11:45:34.01422Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Check for duplicate Image_Names\nduplicate_images = train_df['Image_name'].duplicated().sum()\nprint(f\"Duplicated Image_Name entries: {duplicate_images}\")\n\n# Check for duplicate Patient_IDs (expected due to multiple images per patient)\nduplicate_patients = total_images - total_patients\nprint(f\"Duplicated Patient_ID entries: {duplicate_patients}\")\n\n# Check for invalid Age values\ninvalid_ages = train_df['Age'].dropna()\ninvalid_ages = invalid_ages[invalid_ages < 0].count()\nprint(f\"Invalid Age values (<0): {invalid_ages}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-06T11:45:34.190862Z","iopub.execute_input":"2025-09-06T11:45:34.191084Z","iopub.status.idle":"2025-09-06T11:45:34.205139Z","shell.execute_reply.started":"2025-09-06T11:45:34.191067Z","shell.execute_reply":"2025-09-06T11:45:34.204535Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🧩 Train–Validation Split\n\nTo ensure robust evaluation, we carefully split the dataset into **training** and **validation** subsets.  \n\n---\n\n### 1. Why Grouped Splitting?\nA key challenge in medical imaging datasets is **patient-level data leakage**.  \nIf images from the same patient appear in both training and validation sets, the model may learn patient-specific features instead of generalizable disease patterns.  \n\n✅ To prevent this, we use **GroupShuffleSplit** with `Patient_ID` as the grouping variable.  \nThis guarantees that all images from a single patient are restricted to **either training or validation**, never both.  \n\n---\n\n### 2. Split Details\n- **Split ratio** → 80% training, 20% validation.  \n- **Random state** fixed at `42` for reproducibility.  \n- Splitting performed only once (`n_splits=1`).  \n\n---\n\n### 3. Dataset Sizes\nAfter the split:  \n- **Train set size** → Number of images used for model training.  \n- **Validation set size** → Number of images held out for unbiased performance evaluation.  \n\nThis ensures that the model’s validation accuracy reflects real-world generalization ability rather than memorization.\n\n---\n","metadata":{}},{"cell_type":"code","source":"import os, random\nfrom collections import Counter\nimport numpy as np\nimport torch\nfrom torch.utils.data import DataLoader, WeightedRandomSampler\nfrom torchvision import transforms, datasets\nimport matplotlib.pyplot as plt\n\nfrom sklearn.model_selection import GroupShuffleSplit\n\n# Important: split by Patient_ID so same patient never leaks into train+val\ngss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)\ntrain_idx, val_idx = next(gss.split(train_df, groups=train_df[\"Patient_ID\"]))\n\ntrain_df_split = train_df.iloc[train_idx].reset_index(drop=True)\nval_df_split   = train_df.iloc[val_idx].reset_index(drop=True)\n\nprint(f\"Train size: {len(train_df_split)} | Val size: {len(val_df_split)}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-06T11:45:36.144163Z","iopub.execute_input":"2025-09-06T11:45:36.144428Z","iopub.status.idle":"2025-09-06T11:45:36.186562Z","shell.execute_reply.started":"2025-09-06T11:45:36.144409Z","shell.execute_reply":"2025-09-06T11:45:36.185783Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🗂️ Custom Dataset Class: `ChestXrayDataset`\n\nTo train deep learning models on medical images, we define a **custom PyTorch Dataset** that handles loading images, applying transformations, and returning labels in a structured format.\n\n---\n\n### 1. Purpose\nThe dataset class:\n- Loads **X-ray images** directly from the dataset directory.  \n- Ensures consistent preprocessing (e.g., grayscale conversion, resizing, normalization).  \n- Returns both **image tensors** and their associated **multi-label targets**.  \n\nThis provides a clean pipeline for training and validation.\n\n---\n\n### 2. Key Components\n\n#### **Initialization (`__init__`)**\n- Accepts a dataframe (`dataframe`) containing metadata and image names.  \n- Stores image directory (`img_dir`) for locating files.  \n- Applies optional transformations (`transform`) for augmentation and normalization.  \n- Extracts label columns (`label_columns`) and stores them as a NumPy float32 array.  \n\n#### **Dataset Length (`__len__`)**\n- Returns the number of samples (rows in the dataframe).  \n\n#### **Fetching a Sample (`__getitem__`)**\n- Retrieves an image name and constructs its full path.  \n- Loads the image with **PIL** and converts it to **grayscale** (`\"L\"`), since chest X-rays are single-channel.  \n- Applies transformations if provided.  \n- Returns:\n  - `(image, labels)` → when labels exist (train/validation).  \n  - `(image, img_name)` → for test set (no labels).  \n\n---\n\n### 3. Advantages\n- 🔄 **Reusability** → Works for train, validation, and test splits.  \n- 🩻 **Medical image ready** → Handles grayscale conversion properly.  \n- ⚡ **Integration** → Compatible with PyTorch `DataLoader` for efficient batching and shuffling.  \n\nThis dataset class forms the backbone of our training pipeline, ensuring images and labels are consistently processed.\n","metadata":{}},{"cell_type":"code","source":"from torch.utils.data import Dataset\nfrom PIL import Image, UnidentifiedImageError\nimport torch\nimport os\n\nclass ChestXrayDataset(Dataset):\n    def __init__(self, dataframe, img_dir, transform=None, label_columns=None):\n        self.df = dataframe.reset_index(drop=True)\n        self.img_dir = img_dir\n        self.transform = transform\n        self.label_columns = label_columns\n\n        # Pre-store labels if provided\n        if self.label_columns is not None:\n            self.targets = self.df[self.label_columns].values.astype(\"float32\")\n        else:\n            self.targets = None\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        img_name = self.df.loc[idx, \"Image_name\"]\n        img_path = os.path.join(self.img_dir, img_name)\n\n        # ---- Load image safely ----\n        try:\n            image = Image.open(img_path).convert(\"RGB\")\n        except (UnidentifiedImageError, OSError) as e:\n            print(f\"⚠️ Corrupted image skipped: {img_path} ({e})\")\n            # Return next sample instead of crashing\n            return self.__getitem__((idx + 1) % len(self.df))\n\n        if self.transform:\n            image = self.transform(image)\n\n        # ---- Labels ----\n        if self.targets is not None:\n            labels = torch.tensor(self.targets[idx], dtype=torch.float32)\n            return image, labels\n        else:\n            return image, img_name\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-06T11:45:38.359417Z","iopub.execute_input":"2025-09-06T11:45:38.360186Z","iopub.status.idle":"2025-09-06T11:45:38.367009Z","shell.execute_reply.started":"2025-09-06T11:45:38.360159Z","shell.execute_reply":"2025-09-06T11:45:38.366422Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## ⚙️ Data Pipeline Setup\n\nNow that we have our custom dataset class, we define the **data preprocessing pipeline** and prepare dataloaders for training and validation.\n\n---\n\n### 1. Image Transformations\nWe use **torchvision transforms** to preprocess and augment images before feeding them into the model.\n\n- **Train Transformations**\n  - `Resize(224, 224)` → standardize input size for CNNs.  \n  - `RandomHorizontalFlip()` → simulates left/right orientation changes.  \n  - `RandomRotation(8°)` → introduces slight rotational variation.  \n  - `ColorJitter()` → adds minor brightness/contrast shifts.  \n  - `Grayscale(num_output_channels=3)` → converts single-channel X-ray to **3-channel grayscale** (so pretrained CNNs like ResNet can process them).  \n  - `ToTensor()` → converts to PyTorch tensor.  \n  - `Normalize(mean, std)` → applies ImageNet normalization.  \n\n- **Validation Transformations**\n  - Only resizing, grayscale conversion, tensor conversion, and normalization.  \n  - ❌ No random augmentations → ensures consistent validation results.\n\n---\n\n### 2. Dataset Creation\nWe wrap the preprocessed data into our custom `ChestXrayDataset`:\n\n- **`train_ds`** → Training split with augmentations.  \n- **`val_ds`** → Validation split with minimal preprocessing.  \n\nThis makes the dataset ready for PyTorch `DataLoader`.\n\n---\n\n### 3. Dataloaders\nWe create efficient data pipelines:\n\n- **`train_loader`**\n  - `batch_size = BATCH_SIZE`  \n  - `shuffle=True` → ensures batches are randomized each epoch.  \n  - `num_workers=4` & `pin_memory=True` → speed up data loading.  \n\n- **`val_loader`**\n  - `shuffle=False` → validation set order is fixed.  \n  - Same parallelization optimizations as training.\n\n---\n\n### 4. Handling Class Imbalance\nChest X-ray datasets are **highly imbalanced** (e.g., \"No Finding\" dominates).  \nTo address this imbalance in **multi-label classification**:\n\n- Count positives (`pos_counts`) and negatives (`neg_counts`) for each condition.  \n- Compute **`pos_weight = neg_counts / pos_counts`**.  \n- This weight is passed to **`BCEWithLogitsLoss`**, giving rare diseases higher importance during training.  \n\n---\n\n### 5. Device Setup\n- Automatically detects **GPU (`cuda`)** if available, otherwise falls back to CPU.  \n- Prints device information (including GPU name when applicable).  \n\n✅ This ensures efficient training and fair handling of imbalanced medical labels.\n","metadata":{}},{"cell_type":"code","source":"from torch.utils.data import DataLoader\nfrom torchvision import transforms\n\n# ---- Transforms ----\nmean = [0.485, 0.456, 0.406]\nstd  = [0.229, 0.224, 0.225]\n\ntrain_transform = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomRotation(8),\n    transforms.ColorJitter(brightness=0.08, contrast=0.08),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=mean, std=std)\n])\n\nval_transform = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=mean, std=std)\n])\n\n# ---- Datasets ----\ntrain_ds = ChestXrayDataset(train_df_split,\n                            \"/kaggle/input/grand-xray-slam-division-b/train2\",\n                            transform=train_transform,\n                            label_columns=label_columns)\n\nval_ds   = ChestXrayDataset(val_df_split,\n                            \"/kaggle/input/grand-xray-slam-division-b/train2\",\n                            transform=val_transform,\n                            label_columns=label_columns)\n\n# ---- Dataloaders ----\ntrain_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4, pin_memory=True)\nval_loader   = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=4, pin_memory=True)\n\n# ---- Class imbalance for BCEWithLogitsLoss ----\npos_counts = train_df_split[label_columns].sum().values\nneg_counts = len(train_df_split) - pos_counts\npos_weight = torch.tensor(neg_counts / pos_counts, dtype=torch.float32)\n\n# ---- Device ----\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Device:\", DEVICE)\nif DEVICE.type == \"cuda\":\n    print(\"CUDA device:\", torch.cuda.get_device_name(0))\n\n# Move pos_weight to same device\npos_weight = pos_weight.to(DEVICE)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-06T11:45:43.171602Z","iopub.execute_input":"2025-09-06T11:45:43.171902Z","iopub.status.idle":"2025-09-06T11:45:43.195466Z","shell.execute_reply.started":"2025-09-06T11:45:43.17188Z","shell.execute_reply":"2025-09-06T11:45:43.194887Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🏋️ Model Training Pipeline\n\nWith the dataset and dataloaders ready, we now move to **model definition, training, and fine-tuning**.\n\n---\n\n### 1. Model Architecture\nWe use a **ResNet-50** backbone pretrained on ImageNet and replace its classification head with a custom multi-label head:\n\n- **Dropout** → reduces overfitting.  \n- **Hidden layer + ReLU** → adds non-linearity.  \n- **Final Linear layer** → outputs predictions for **14 conditions**.  \n\nThis allows transfer learning while adapting the network to chest X-ray classification.\n\n---\n\n### 2. Loss Function\nWe use **`BCEWithLogitsLoss`** (binary cross-entropy with sigmoid), suitable for **multi-label classification**.  \n- Weighted with **`pos_weight`** to handle strong class imbalance.  \n- Ensures rare conditions contribute fairly to the gradient updates.\n\n---\n\n### 3. Training Strategy\nWe adopt a **two-stage training** process:\n\n#### **Stage 1 — Train Head Only**\n- Backbone (ResNet-50 convolutional layers) **frozen**.  \n- Only the new classification head is trained.  \n- Optimizer → `Adam`, learning rate = **1e-3**.  \n- LR Scheduler → `ReduceLROnPlateau` for adaptive learning rate adjustment.  \n\n#### **Stage 2 — Fine-Tune Backbone**\n- All layers **unfrozen** for full network training.  \n- Optimizer → `AdamW`, learning rate = **1e-4** (smaller to avoid catastrophic forgetting).  \n- LR Scheduler → same as Stage 1.  \n\nThis strategy stabilizes training and avoids destroying pretrained weights.\n\n---\n\n### 4. Training Loop\nWe define a reusable function **`run_epoch`**:\n\n- **Mode switching** → `train()` for training, `eval()` for validation.  \n- **Mixed precision option (`use_amp`)** for faster training on GPUs.  \n- Tracks:\n  - **Loss** → mean BCE loss across batches.  \n  - **AUC (ROC-AUC score)** → primary evaluation metric for multi-label tasks.  \n\n---\n\n### 5. Monitoring & Logging\nFor each epoch, we log:  \n- Training and validation **loss**.  \n- Training and validation **AUC**.  \n- **Time per epoch** for efficiency tracking.  \n\nThis allows us to monitor model convergence and early stop if necessary.\n\n---\n\n✅ With this setup, the model benefits from **transfer learning**, balanced training for rare conditions, and careful fine-tuning of the backbone for maximum performance.\n","metadata":{}},{"cell_type":"code","source":"import os\nimport time\nimport copy\nimport numpy as np\nfrom tqdm import tqdm\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom sklearn.metrics import roc_auc_score\nfrom torchvision import models\nfrom torchvision.models import resnet50, ResNet50_Weights\n\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\n# ---- Hyperparameters ----\nHEAD_EPOCHS   = 5          # train classifier head first\nFT_EPOCHS     = 8         # then fine-tune backbone\nBATCH_SIZE    = 32         # don't leave None, set explicitly\nLR_HEAD       = 1e-3       # higher LR for head\nLR_FT         = 1e-4       # lower LR for fine-tuning\nWEIGHT_DECAY  = 1e-4       # regularization\nPATIENCE      = 3          # early stopping patience\nMIN_LR        = 1e-7       # minimum learning rate for scheduler\n\ndef build_model(num_classes=14, dropout=0.3):\n    model = resnet50(weights=ResNet50_Weights.DEFAULT)\n    in_features = model.fc.in_features\n    \n    # Better head: hidden layer + ReLU + Dropout\n    model.fc = nn.Sequential(\n        nn.Dropout(dropout),\n        nn.Linear(in_features, in_features // 2),\n        nn.ReLU(),\n        nn.Dropout(dropout/2),\n        nn.Linear(in_features // 2, num_classes)\n    )\n    return model\n# Build model\nmodel = build_model(num_classes=14, dropout=0.3).to(DEVICE)\n# Loss with pos_weight for multi-label\ncriterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight.to(DEVICE))  # pos_weight must match labels shape### LOSS\n\n# Stage 1: freeze backbone except final fc\nfor name, param in model.named_parameters():\n    param.requires_grad = False\nfor name, param in model.fc.named_parameters():\n    param.requires_grad = True\n\nopt_head = optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=LR_HEAD, weight_decay=WEIGHT_DECAY)\nscheduler = optim.lr_scheduler.ReduceLROnPlateau(\n    opt_head, mode='min', factor=0.5, patience=PATIENCE, min_lr=MIN_LR\n)\n\ndef run_epoch(model, loader, optimizer=None, train=False, device=DEVICE, use_amp=False):\n    if train:\n        model.train()\n    else:\n        model.eval()\n    losses = []\n    all_labels = []\n    all_probs = []\n    scaler = torch.cuda.amp.GradScaler() if use_amp and device.type == 'cuda' else None\n    loop = tqdm(loader, desc='Train' if train else 'EVAL', leave= False)\n    for imgs, labels in loop:\n        imgs = imgs.to(DEVICE)\n        labels = labels.to(DEVICE)\n        with torch.set_grad_enabled(train):\n            if scaler:\n                with torch.cuda.amp.autocast():\n                    logits = model(imgs)\n                    loss = criterion(logits, labels)\n\n            else:\n                logits = model(imgs)\n                loss = criterion(logits, labels)\n            probs = torch.sigmoid(logits).detach().cpu().numpy()\n            all_probs.append(probs)\n            all_labels.append(labels.detach().cpu().numpy())\n\n            if train:\n                if scaler:\n                    scaler.scale(loss).backward()\n                    scaler.step(optimizer)\n                    scaler.update()\n                    optimizer.zero_grad()\n\n                else:\n                    loss.backward()\n                    optimizer.step()\n                    optimizer.zero_grad()\n\n        losses.append(loss.item())\n        loop.set_postfix(loss=np.mean(losses))\n    all_probs = np.concatenate(all_probs)\n    all_labels = np.concatenate(all_labels)\n    try:\n        auc = roc_auc_score(all_labels, all_probs)\n    except ValueError:\n        auc = float('nan')\n\n    return np.mean(losses), auc\n\n### STAGE - 1 HEAD TRAINING\nfor epoch in range(1,HEAD_EPOCHS+1):\n    t0 = time.time()\n    train_loss, train_auc = run_epoch(model, train_loader, optimizer=opt_head, train=True, use_amp=False)\n    val_loss, val_auc = run_epoch(model, val_loader, train=False, use_amp=False)\n    scheduler.step(val_loss)\n    print(f\"Epoch {epoch}/{HEAD_EPOCHS}  train_loss={train_loss:.4f} train_auc={train_auc:.4f}  val_loss={val_loss:.4f} val_auc={val_auc:.4f}  time={(time.time()-t0):.1f}s\")\n\n## UNFREEZE BACKBONE AND FINE TUNEEEEEE\n\nfor param in model.parameters():\n    param.requires_grad = True\n\nopt_ft = optim.AdamW(model.parameters(), lr=LR_FT, weight_decay=WEIGHT_DECAY)\nscheduler_ft = optim.lr_scheduler.ReduceLROnPlateau(\n    opt_ft, mode='min', factor=0.5, patience=PATIENCE, min_lr=MIN_LR\n)\n\nfor epoch in range(1,FT_EPOCHS+1):\n    t0 = time.time()\n    train_loss, train_auc = run_epoch(model, train_loader, optimizer=opt_ft, train=True, use_amp=False)\n    val_loss, val_auc = run_epoch(model, val_loader, train=False, use_amp=False)\n    scheduler_ft.step(val_loss)\n    print(f\"FT Epoch {epoch}/{FT_EPOCHS}  train_loss={train_loss:.4f} train_auc={train_auc:.4f}  val_loss={val_loss:.4f} val_auc={val_auc:.4f}  time={(time.time()-t0):.1f}s\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-06T11:46:05.979516Z","iopub.execute_input":"2025-09-06T11:46:05.980201Z","iopub.status.idle":"2025-09-06T17:34:34.466864Z","shell.execute_reply.started":"2025-09-06T11:46:05.980176Z","shell.execute_reply":"2025-09-06T17:34:34.465222Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🔗 Saving & Uploading Model to Hugging Face Hub\n\nSince Kaggle imposes strict runtime limits (9–12 hours depending on resources), there is always a risk of training being **interrupted before we can save our model locally**.  \nTo make sure our progress is never lost and to easily reuse the model later (for ensembling, inference, or sharing), we directly **push the trained ResNet50 model to Hugging Face Hub**.\n\n### Why Hugging Face Hub?\n- ✅ **Persistent storage** → even if the Kaggle session resets, the model stays safe.  \n- ✅ **Reusability** → we can load the model later with just a few lines of code.  \n- ✅ **Collaboration** → makes it easy to share checkpoints with teammates or the community.  \n- ✅ **Ensembling** → multiple saved models can be combined later for stronger performance.  \n\n### What gets saved?\n1. **`pytorch_model.bin`** → model weights.  \n2. **`config.json`** → metadata (architecture, num_classes, dropout, etc.) so the model can be reloaded consistently.  \n\nThis way, after fine-tuning finishes, our model is automatically versioned and available at:\n\n👉 [Hugging Face Repo: `TanmayTomar/chest-xray-resnet50`](https://huggingface.co/TanmayTomar/chest-xray-resnet50)\n\nLater, we can reload it in just 2 lines:\n```python\nfrom huggingface_hub import hf_hub_download\nweights_path = hf_hub_download(\"TanmayTomar/chest-xray-resnet50\", \"pytorch_model.bin\")\n","metadata":{}},{"cell_type":"code","source":"import os, json, torch\nfrom kaggle_secrets import UserSecretsClient\nfrom huggingface_hub import HfApi, HfFolder, upload_folder\n\n# ---- Load HF Token from Kaggle Secrets ----\nuser_secrets = UserSecretsClient()\nHF_TOKEN = user_secrets.get_secret(\"HF_TOKEN\")\n\nHF_REPO_ID = \"TanmayTomar/chest-xray-resnet50\"\n\n# Save token so huggingface_hub can pick it up\nHfFolder.save_token(HF_TOKEN)\n\n# Init API with token\napi = HfApi(token=HF_TOKEN)\n\n# Create repo if not exists\ntry:\n    api.create_repo(repo_id=HF_REPO_ID, private=False)\nexcept Exception as e:\n    print(\"Repo already exists:\", e)\n\n# ---- After fine-tuning ----\nSAVE_DIR = \"./resnet50_chest_xray\"\nos.makedirs(SAVE_DIR, exist_ok=True)\n\n# save model weights\ntorch.save(model.state_dict(), f\"{SAVE_DIR}/pytorch_model.bin\")\n\n# save config\nconfig = {\"architecture\": \"resnet50\", \"num_classes\": 14, \"dropout\": 0.3}\nwith open(f\"{SAVE_DIR}/config.json\", \"w\") as f:\n    json.dump(config, f)\n\n# push to HF\nupload_folder(\n    repo_id=HF_REPO_ID,\n    folder_path=SAVE_DIR,\n    commit_message=\"Upload fine-tuned ResNet50 after FT training\",\n    token=HF_TOKEN,\n)\n\nprint(f\"Model pushed to https://huggingface.co/{HF_REPO_ID}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-06T17:39:25.292204Z","iopub.execute_input":"2025-09-06T17:39:25.293146Z","iopub.status.idle":"2025-09-06T17:39:30.951267Z","shell.execute_reply.started":"2025-09-06T17:39:25.29312Z","shell.execute_reply":"2025-09-06T17:39:30.950353Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import DataLoader\nfrom huggingface_hub import hf_hub_download\nfrom torchvision import models\nimport json\nfrom tqdm import tqdm\n\n# ---- Load config from HF ----\nconfig_path = hf_hub_download(\n    repo_id=\"TanmayTomar/chest-xray-resnet50\",\n    filename=\"config.json\"\n)\nwith open(config_path, \"r\") as f:\n    config = json.load(f)\n\nNUM_CLASSES = config[\"num_classes\"]\nDROPOUT = config[\"dropout\"]\n\n# ---- Load model weights ----\nweights_path = hf_hub_download(\n    repo_id=\"TanmayTomar/chest-xray-resnet50\",\n    filename=\"pytorch_model.bin\"\n)\n\n# Build the model EXACTLY like in training\nmodel = models.resnet50(weights=None)\nin_features = model.fc.in_features\nmodel.fc = nn.Sequential(\n    nn.Dropout(DROPOUT),\n    nn.Linear(in_features, in_features // 2),\n    nn.ReLU(),\n    nn.Dropout(DROPOUT / 2),\n    nn.Linear(in_features // 2, NUM_CLASSES)\n)\n\n# Load weights\nstate_dict = torch.load(weights_path, map_location=\"cpu\")\nmodel.load_state_dict(state_dict)\nmodel = model.to(DEVICE)\nmodel.eval()\n\n# ---- Load sample submission ----\nsub_df = pd.read_csv(\"/kaggle/input/grand-xray-slam-division-b/sample_submission_2.csv\")\n\n# Ensure label columns (skip first col if it's ID)\nlabel_columns = sub_df.columns.tolist()[1:]\n\n# ---- Test dataset ----\ntest_transform = val_transform  # reuse validation transform\n\ntest_ds = ChestXrayDataset(\n    dataframe=sub_df,\n    img_dir=\"/kaggle/input/grand-xray-slam-division-b/test2\",\n    transform=test_transform,\n    label_columns=None\n)\n\ntest_loader = DataLoader(\n    test_ds, batch_size=32, shuffle=False, num_workers=4, pin_memory=True\n)\n\n# ---- Inference ----\nall_preds = []\nwith torch.no_grad():\n    for imgs, img_names in tqdm(test_loader, desc=\"Predicting\"):\n        imgs = imgs.to(DEVICE)\n        logits = model(imgs)\n        probs = torch.sigmoid(logits).cpu().numpy()\n        all_preds.append(probs)\n\nall_preds = np.vstack(all_preds)\n\n# ---- Fill & Save submission ----\nsub_df[label_columns] = all_preds\noutput_path = \"submission.csv\"\nsub_df.to_csv(output_path, index=False)\n\nprint(f\"Submission file saved at {output_path}\")\ndisplay(sub_df.head())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-06T17:46:05.537432Z","iopub.execute_input":"2025-09-06T17:46:05.538177Z","iopub.status.idle":"2025-09-06T18:05:22.573973Z","shell.execute_reply.started":"2025-09-06T17:46:05.538143Z","shell.execute_reply":"2025-09-06T18:05:22.573112Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}