{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":13451,"datasetId":654585,"databundleVersionId":1188070}],"dockerImageVersionId":31011,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms, models\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import (\n    precision_score, recall_score, f1_score, roc_curve, auc, \n    confusion_matrix, classification_report\n)\nfrom PIL import Image\nimport matplotlib.pyplot as plt\nimport pydicom\nfrom tqdm import tqdm\nimport itertools\n\nDATA_DIR    = '/kaggle/input/rsna-intracranial-hemorrhage-detection/rsna-intracranial-hemorrhage-detection'\nCSV_PATH    = os.path.join(DATA_DIR, 'stage_2_train.csv')\nDCM_DIR     = os.path.join(DATA_DIR, 'stage_2_train')\nPNG_ROOT    = '/kaggle/working/png_data'\nOUTPUT_DIR  = '/kaggle/working/output_single_run'\nROC_DIR     = os.path.join(OUTPUT_DIR, 'roc_curves')\nCAM_DIR     = os.path.join(OUTPUT_DIR, 'gradcam')\nMETRICS_DIR = os.path.join(OUTPUT_DIR, 'metrics')\n\nfor d in [PNG_ROOT, OUTPUT_DIR, ROC_DIR, CAM_DIR, METRICS_DIR]:\n    os.makedirs(d, exist_ok=True)\n\nBATCH_SIZE    = 16  \nNUM_EPOCHS    = 20  \nLEARNING_RATE = 5e-5 \nWEIGHT_DECAY  = 1e-5  \nPATIENCE      = 5 \nDEVICE        = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nCLASS_NAMES   = ['epidural', 'subdural', 'subarachnoid', 'intraparenchymal', 'intraventricular']\nBALANCE_N     = 1000  \nVAL_SPLIT_SIZE= 0.2\n\ndef display_sample_images_per_class(df, root_dir, classes, num_samples=5):\n    n_cols = num_samples\n    n_rows = len(classes)\n    \n    fig, axes = plt.subplots(n_rows, n_cols, figsize=(4 * n_cols, 4 * n_rows))\n    fig.suptitle('Sample Images Per Hemorrhage Subtype (Positive Examples)', fontsize=16, y=1.02)\n    \n    for i, cls in enumerate(classes):\n        positive_samples = df[df[cls] == 1]\n        samples = positive_samples.head(n_cols)\n        image_ids = samples['image'].tolist()\n\n        for j in range(n_cols):\n            ax = axes[i, j]\n            \n            if j < len(image_ids):\n                img_id = image_ids[j]\n                png_path = os.path.join(root_dir, img_id + '.png')\n                \n                try:\n                    img = Image.open(png_path).convert('RGB')\n                    ax.imshow(img)\n                    ax.set_title(f\"ID: {img_id}\", fontsize=10)\n                except FileNotFoundError:\n                    ax.text(0.5, 0.5, \"PNG Not Found\", ha='center', va='center', transform=ax.transAxes)\n                    ax.set_title(f\"Missing: {img_id}\", fontsize=10)\n            else:\n                ax.axis('off')\n\n            ax.set_xticks([])\n            ax.set_yticks([])\n\n        axes[i, 0].set_ylabel(cls.upper(), fontsize=12, rotation=90, labelpad=20)\n\n    plt.tight_layout()\n    plt.savefig(os.path.join(OUTPUT_DIR, 'sample_images_per_class.png'))\n    plt.close(fig)\n    print(f\"Saved sample image grid to {os.path.join(OUTPUT_DIR, 'sample_images_per_class.png')}\")\n\ndf = pd.read_csv(CSV_PATH)\ndf = df.drop_duplicates()  \n\ndf[['image','subtype']] = df['ID'].str.rsplit('_', n=1, expand=True)\ndf = df[df['subtype'] != 'any']\ndf = df.pivot(index='image', columns='subtype', values='Label').reset_index()\n\ndf[CLASS_NAMES] = df[CLASS_NAMES].fillna(0).astype(int)\n\ndef convert_dcm_to_png(dcm_path, png_path):\n    dcm = pydicom.dcmread(dcm_path)\n    img = dcm.pixel_array\n    \n    def apply_window(image, center, width):\n        img = (image - (center - width / 2)) / width\n        img = np.clip(img, 0, 1)\n        return img\n    \n    brain_img = apply_window(img, 40, 80)  \n    subdural_img = apply_window(img, 80, 200)\n    bone_img = apply_window(img, 600, 2800)\n    \n    combined = np.stack([brain_img, subdural_img, bone_img], axis=-1)\n    combined = (combined * 255).astype(np.uint8)\n    \n    Image.fromarray(combined).save(png_path)\n\ndef prepare_png_data(df, dcm_dir, png_root, limit_per_class=500):\n    unique_images = df['image'].unique()\n    \n    class_counts = {cls: df[df[cls] == 1]['image'].nunique() for cls in CLASS_NAMES}\n    \n    positive_samples = []\n    for cls in CLASS_NAMES:\n        cls_pos = df[df[cls] == 1]['image'].unique()\n        if cls == 'epidural':\n            positive_samples.extend(np.repeat(cls_pos, 8)[:min(limit_per_class * 8, len(cls_pos) * 8)])\n        else:\n            positive_samples.extend(cls_pos[:min(limit_per_class, len(cls_pos))])\n    \n    positive_samples = list(positive_samples)  \n    \n    negative_images = list(set(unique_images) - set(df[df[CLASS_NAMES].sum(axis=1) > 0]['image'].unique()))\n    negative_samples = np.random.choice(negative_images, min(len(negative_images), len(set(positive_samples)) * 2), replace=False)\n    \n    selected_images = list(set(positive_samples + list(negative_samples)))\n    \n    for img_id in tqdm(selected_images, desc=\"Converting DCM to PNG\"):\n        dcm_path = os.path.join(dcm_dir, img_id + '.dcm')\n        png_path = os.path.join(png_root, img_id + '.png')\n        if os.path.exists(dcm_path) and not os.path.exists(png_path):\n            convert_dcm_to_png(dcm_path, png_path)\n    \n    balanced_df = df[df['image'].isin(selected_images)]\n    oversampled_df = pd.DataFrame()\n    for cls in CLASS_NAMES:\n        cls_df = balanced_df[balanced_df[cls] == 1]\n        if cls == 'epidural':\n            oversampled_df = pd.concat([oversampled_df, pd.concat([cls_df] * 8)])\n        else:\n            oversampled_df = pd.concat([oversampled_df, cls_df])\n    neg_df = balanced_df[balanced_df[CLASS_NAMES].sum(axis=1) == 0]\n    balanced_df = pd.concat([oversampled_df, neg_df]).drop_duplicates(subset='image')  \n    \n    return balanced_df\n\nprint(\"\\n=== Preparing PNG Data (Multi-Window RGB) ===\")\nbalanced_df = prepare_png_data(df, DCM_DIR, PNG_ROOT, limit_per_class=BALANCE_N)\n\nprint(f\"Balanced dataset shape: {balanced_df.shape}\")\nbalanced_df.to_csv(os.path.join(OUTPUT_DIR, 'balanced_dataset.csv'), index=False)\n\ndisplay_sample_images_per_class(balanced_df, PNG_ROOT, CLASS_NAMES)\n\nclass MultiHemoDataset(Dataset):\n    def __init__(self, df, root_dir, transform=None):\n        self.df = df.copy().reset_index(drop=True) \n        self.root = root_dir\n        self.tf = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        rec = self.df.iloc[idx]\n        \n        path = os.path.join(self.root, rec['image'] + '.png')\n        img = Image.open(path).convert('RGB')\n        if self.tf:\n            img = self.tf(img)\n            \n        label_arr = rec[CLASS_NAMES].astype(np.float32).to_numpy()\n        labels = torch.from_numpy(label_arr)\n        \n        return img, labels\n\ntrain_transform = transforms.Compose([\n    transforms.Resize(340), \n    transforms.RandomResizedCrop(299, scale=(0.8, 1.0)),  \n    transforms.RandomHorizontalFlip(),\n    transforms.RandomRotation(15),  \n    transforms.ColorJitter(brightness=0.2, contrast=0.2),  \n    transforms.ToTensor(),\n    transforms.Normalize([0.485,0.456,0.406], [0.229,0.224,0.225])\n])\nval_transform = transforms.Compose([\n    transforms.Resize(299), \n    transforms.CenterCrop(299),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485,0.456,0.406], [0.229,0.224,0.225])\n])\n\ntrain_df, val_df = train_test_split(\n    balanced_df, \n    test_size=VAL_SPLIT_SIZE, \n    random_state=42, \n    stratify=balanced_df[CLASS_NAMES].apply(lambda x: ''.join(map(str, x)), axis=1)  \n)\n\nclass_counts = train_df[CLASS_NAMES].sum()\ntotal_samples = len(train_df)\n\nweights = total_samples / (len(CLASS_NAMES) * class_counts + 1e-6) \nclass_weights = torch.tensor(weights.values, dtype=torch.float).to(DEVICE)\n\ntrain_loader = DataLoader(\n    MultiHemoDataset(train_df, PNG_ROOT, train_transform),\n    batch_size=BATCH_SIZE, shuffle=True,  num_workers=4)  \nval_loader   = DataLoader(\n    MultiHemoDataset(val_df,   PNG_ROOT, val_transform),\n    batch_size=BATCH_SIZE, shuffle=False, num_workers=4)\n\nprint(f\"Training images: {len(train_df)}, Validation images: {len(val_df)}\")\n\nmodel = models.inception_v3(weights=models.Inception_V3_Weights.IMAGENET1K_V1, aux_logits=True, transform_input=True)\n\nmodel.AuxLogits = None\nmodel.aux_logits = False\n\nnum_ftrs = model.fc.in_features\n\nmodel.fc = nn.Linear(num_ftrs, len(CLASS_NAMES))\n\nmodel = model.to(DEVICE)\n\ncriterion = nn.BCEWithLogitsLoss(pos_weight=class_weights)  \n\noptimizer = torch.optim.AdamW(model.parameters(),  \n                             lr=LEARNING_RATE,\n                             weight_decay=WEIGHT_DECAY)\n\nscheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=2)  \n\nbest_val_loss    = np.inf\nno_improve       = 0\ntrain_losses, train_accs = [], []\nval_losses,   val_accs   = [], []\nbest_model_path  = os.path.join(OUTPUT_DIR, 'best_model.pth')\n\nprint(f\"\\n=== Starting Training (Max {NUM_EPOCHS} Epochs) ===\")\nfor epoch in range(1, NUM_EPOCHS+1):\n    \n    model.train()\n    running_loss = running_corr = running_total = 0\n    for imgs, labels in train_loader:\n        imgs, labels = imgs.to(DEVICE), labels.to(DEVICE)\n        optimizer.zero_grad()\n        \n        outputs = model(imgs) \n        \n        loss = criterion(outputs, labels)\n\n        loss.backward()\n        optimizer.step()\n\n        running_loss  += loss.item() * imgs.size(0)\n       \n        preds         = (torch.sigmoid(outputs) >= 0.5).long()\n        running_corr  += (preds == labels).all(dim=1).sum().item()\n        running_total += imgs.size(0)\n\n    epoch_train_loss = running_loss / running_total\n    epoch_train_acc  = running_corr  / running_total\n    train_losses.append(epoch_train_loss)\n    train_accs.append(epoch_train_acc)\n\n    model.eval()\n    val_running_loss = val_running_corr = val_running_total = 0\n    with torch.no_grad():\n        for imgs, labels in val_loader:\n            imgs, labels = imgs.to(DEVICE), labels.to(DEVICE)\n            outputs = model(imgs)\n\n            unweighted_criterion = nn.BCEWithLogitsLoss() \n            val_running_loss  += unweighted_criterion(outputs, labels).item() * imgs.size(0)\n            preds              = (torch.sigmoid(outputs) >= 0.5).long()\n            val_running_corr  += (preds == labels).all(dim=1).sum().item()\n            val_running_total += imgs.size(0)\n\n    epoch_val_loss = val_running_loss / val_running_total\n    epoch_val_acc  = val_running_corr  / val_running_total\n    val_losses.append(epoch_val_loss)\n    val_accs.append(epoch_val_acc)\n\n    scheduler.step(epoch_val_loss) \n\n    print(f\"Epoch {epoch}/{NUM_EPOCHS} — \"\n          f\"Train loss: {epoch_train_loss:.4f}, acc: {epoch_train_acc:.4f} | \"\n          f\" Val loss: {epoch_val_loss:.4f}, acc: {epoch_val_acc:.4f}\")\n\n    if epoch_val_loss < best_val_loss:\n        best_val_loss = epoch_val_loss\n        no_improve    = 0\n        torch.save(model.state_dict(), best_model_path)\n    else:\n        no_improve += 1\n        if no_improve >= PATIENCE:\n            print(f\"Early stopping at epoch {epoch}\")\n            break\n\nprint(\"\\n=== Final Evaluation and Metrics ===\")\n\nmodel.load_state_dict(torch.load(best_model_path))\nmodel.eval()\n\nval_probs_list, val_targets_list = [], []\nwith torch.no_grad():\n    for imgs, labels in tqdm(val_loader, desc=\"Final Evaluation\"):\n        imgs, labels = imgs.to(DEVICE), labels.to(DEVICE)\n        outputs = model(imgs)\n        val_probs_list.extend(torch.sigmoid(outputs).cpu().numpy())\n        val_targets_list.extend(labels.cpu().numpy())\n        \nval_probs   = np.array(val_probs_list)\nval_targets = np.array(val_targets_list)\nval_preds   = (val_probs >= 0.5).astype(int)\n\nprint(\"\\n--- Multi-Label Classification Report (Threshold=0.5) ---\")\n\nreport = classification_report(\n    val_targets, val_preds, \n    target_names=CLASS_NAMES, \n    zero_division=0, \n    output_dict=True\n)\nreport_df = pd.DataFrame(report).transpose().round(4)\nprint(report_df.to_markdown())\nreport_df.to_csv(os.path.join(METRICS_DIR, 'classification_report.csv'))\n\ndef plot_confusion_matrix(cm, classes, title, cmap=plt.cm.Blues):\n    plt.imshow(cm, interpolation='nearest', cmap=cmap)\n    plt.title(title)\n    plt.colorbar()\n    tick_marks = np.arange(len(classes))\n    plt.xticks(tick_marks, classes, rotation=45)\n    plt.yticks(tick_marks, classes)\n\n    fmt = 'd'\n    thresh = cm.max() / 2.\n    for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):\n        plt.text(j, i, format(cm[i, j], fmt),\n                 horizontalalignment=\"center\",\n                 color=\"white\" if cm[i, j] > thresh else \"black\")\n\n    plt.ylabel('True label')\n    plt.xlabel('Predicted label')\n    plt.tight_layout()\n    plt.savefig(os.path.join(METRICS_DIR, f'cm_{title.replace(\" \", \"_\")}.png'))\n    plt.close()\n\nfor i, cls in enumerate(CLASS_NAMES):\n    cm = confusion_matrix(val_targets[:, i], val_preds[:, i])\n    plot_confusion_matrix(\n        cm, \n        classes=['Negative', 'Positive'], \n        title=f'Confusion Matrix: {cls}'\n    )\n    \n\nprint(f\"\\nSaved {len(CLASS_NAMES)} Confusion Matrix plots to: {METRICS_DIR}\")\n\nplt.figure(figsize=(8,6))\nmetrics_list = []\nfor i, cls in enumerate(CLASS_NAMES):\n    fpr, tpr, _ = roc_curve(val_targets[:, i], val_probs[:, i])\n    roc_auc = auc(fpr, tpr)\n    \n    plt.plot(fpr, tpr, label=f\"{cls} (AUC = {roc_auc:.2f})\")\n    metrics_list.append({\n        'class': cls,\n        'AUC': roc_auc,\n        'Precision': report_df.loc[cls, 'precision'],\n        'Recall': report_df.loc[cls, 'recall'],\n        'F1-Score': report_df.loc[cls, 'f1-score']\n    })\n\nplt.plot([0,1],[0,1],'--', color='gray', label='Random Guess')\nplt.xlabel('False Positive Rate (FPR)'); plt.ylabel('True Positive Rate (TPR)')\nplt.title('Receiver Operating Characteristic (ROC) Curve')\nplt.legend()\nplt.savefig(os.path.join(ROC_DIR, 'roc_all_classes.png'))\nplt.close()\n\npd.DataFrame(metrics_list).to_csv(os.path.join(METRICS_DIR, 'final_metrics_summary.csv'), index=False)\nprint(f\"Saved ROC curve plot to: {ROC_DIR}\")\n\nprint(f\"\\nAll results (metrics, CM, ROC) saved to: {OUTPUT_DIR}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T08:48:56.059554Z","iopub.execute_input":"2025-12-17T08:48:56.060071Z","iopub.status.idle":"2025-12-17T09:44:28.123114Z","shell.execute_reply.started":"2025-12-17T08:48:56.060044Z","shell.execute_reply":"2025-12-17T09:44:28.122408Z"}},"outputs":[],"execution_count":null}]}