{"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":"gpu","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 StratifiedKFold\nfrom sklearn.metrics import precision_score, recall_score, f1_score, roc_curve, auc, confusion_matrix\nfrom PIL import Image\nimport matplotlib.pyplot as plt\nimport pydicom\nfrom tqdm import tqdm\nimport pydicom\nfrom PIL import Image\n!pip install torchcam\nfrom torchcam.methods import SmoothGradCAMpp\nfrom torchcam.utils import overlay_mask","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-27T17:12:05.80389Z","iopub.execute_input":"2025-04-27T17:12:05.80413Z","execution_failed":"2025-04-27T17:12:45.478Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Paths\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_task4'\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\n# Create directories\nfor d in [PNG_ROOT, OUTPUT_DIR, ROC_DIR, CAM_DIR, METRICS_DIR]:\n    os.makedirs(d, exist_ok=True)\n","metadata":{"trusted":true,"execution":{"execution_failed":"2025-04-27T17:12:45.479Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Hyperparameters\nBATCH_SIZE    = 8\nNUM_EPOCHS    = 100\nLEARNING_RATE = 1e-4\nWEIGHT_DECAY  = 1e-6\nPATIENCE      = 10\nNUM_FOLDS     = 5\nDEVICE        = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nCLASS_NAMES   = ['epidural', 'subdural', 'subarachnoid', 'intraparenchymal', 'intraventricular']","metadata":{"trusted":true,"execution":{"execution_failed":"2025-04-27T17:12:45.479Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def generate_gradcams(\n    model, df, probs, targets, classes,\n    png_root, transform, out_dir, fold,\n    n_tp=5, n_fp=5, n_fn=5, device=DEVICE\n):\n\n    os.makedirs(out_dir, exist_ok=True)\n    cam_extractor = SmoothGradCAMpp(model)\n    preds = (probs >= 0.5).astype(int)\n\n    for i, cls in enumerate(classes):\n        # 选索引\n        tp_idxs = np.where((targets[:,i]==1)&(preds[:,i]==1))[0][:n_tp]\n        fp_idxs = np.where((targets[:,i]==0)&(preds[:,i]==1))[0][:n_fp]\n        fn_idxs = np.where((targets[:,i]==1)&(preds[:,i]==0))[0][:n_fn]\n\n        for kind, idxs in [('TP',tp_idxs), ('FP',fp_idxs), ('FN',fn_idxs)]:\n            for j, idx in enumerate(idxs):\n                img_id = df.loc[idx, 'image']\n                pil = Image.open(os.path.join(png_root, img_id+'.png')).convert('RGB')\n                inp = transform(pil).unsqueeze(0).to(device)\n                out = model(inp)\n                activation_map = cam_extractor(class_idx=i, scores=out)[0].cpu().numpy()\n                heatmap = overlay_mask(pil, activation_map, alpha=0.5)\n\n                fname = f'gradcam_{cls}_{kind}_fold{fold}_{j}.png'\n                heatmap.save(os.path.join(out_dir, fname))\n    print(f\"[Fold {fold}] Saved Grad-CAM to {out_dir}\")","metadata":{"trusted":true,"execution":{"execution_failed":"2025-04-27T17:12:45.479Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"BALANCE_N   = 500   # 每類正/負例上限\n\n# 3. 讀 CSV，拆出 image & subtype\ndf = pd.read_csv(CSV_PATH)\ndf[['image','subtype']] = df['ID'].str.rsplit('_', n=1, expand=True)\n\n# 4. Pivot 成 multi-hot 格式\ndf_ml = df.pivot_table(\n    index='image',\n    columns='subtype',\n    values='Label',\n    aggfunc='max',\n    fill_value=0\n).reset_index()\ndf_ml['any'] = df_ml[CLASS_NAMES].max(axis=1)\n\n# 5. Balance 子集：對每個 class 抽正/負例\nframes = []\nfor cls in CLASS_NAMES:\n    pos = df_ml[df_ml[cls]==1]\n    neg = df_ml[df_ml[cls]==0]\n    # 如果正例超過上限，就隨機抽 BALANCE_N；否則全要\n    pos = pos.sample(BALANCE_N, random_state=42) if len(pos)>BALANCE_N else pos\n    # 同理抽負例\n    neg = neg.sample(BALANCE_N, random_state=42) if len(neg)>BALANCE_N else neg\n    frames.append(pos)\n    frames.append(neg)\n\n# 合併並去重，確保同一張圖只出現一次\nbalanced_df = pd.concat(frames).drop_duplicates(subset='image').reset_index(drop=True)\nprint(f\"Balanced subset size: {len(balanced_df)} images\")\n\n","metadata":{"trusted":true,"execution":{"execution_failed":"2025-04-27T17:12:45.479Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 6. 存成 CSV\nbalanced_csv = os.path.join(METRICS_DIR, 'subset_multilabel_balanced.csv')\nbalanced_df.to_csv(balanced_csv, index=False)\n\n# 7. 只針對子集進行 DICOM → PNG 轉檔\nprint(\"Converting balanced subset DICOM to PNG...\")\nfor img_id in tqdm(balanced_df['image'].unique(), desc=\"DICOM→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 not os.path.exists(dcm_path):\n        print(f\"Missing file: {dcm_path}\")\n        continue\n    ds = pydicom.dcmread(dcm_path)\n    arr = ds.pixel_array.astype(np.float32)\n    arr = (arr - arr.min()) / (arr.max() - arr.min()) * 255.0\n    arr = arr.astype(np.uint8)\n    Image.fromarray(arr).save(png_path)\n\nprint(\"Done! Balanced subset PNGs are in:\", PNG_ROOT)\n#///","metadata":{"trusted":true,"execution":{"execution_failed":"2025-04-27T17:12:45.479Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class MultiHemoDataset(Dataset):\n    def __init__(self, df, root_dir, transform=None):\n        self.df = df\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        # 1) 先读影像\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        # 2) 明确把 label 列转成 float32 数组\n        #    这样就不会是 object dtype 了\n        label_arr = rec[CLASS_NAMES].astype(np.float32).to_numpy()\n        labels = torch.from_numpy(label_arr)          # dtype=torch.float32\n        return img, labels","metadata":{"trusted":true,"execution":{"execution_failed":"2025-04-27T17:12:45.479Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 3. Transforms\ntrain_transform = transforms.Compose([\n    transforms.Resize(256),\n    transforms.RandomResizedCrop(224),\n    transforms.RandomHorizontalFlip(),\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(256),\n    transforms.CenterCrop(224),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485,0.456,0.406], [0.229,0.224,0.225])\n])","metadata":{"trusted":true,"execution":{"execution_failed":"2025-04-27T17:12:45.479Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 4. Cross-validation setup\nkf = StratifiedKFold(n_splits=NUM_FOLDS, shuffle=True, random_state=42)\nX = balanced_df['image']\ny = balanced_df['any']\n\nall_metrics   = []\nall_roc_data  = {cls: [] for cls in CLASS_NAMES}\nfold          = 0\n\nfor train_idx, val_idx in kf.split(X, y):\n    fold += 1\n    print(f\"\\n=== Fold {fold}/{NUM_FOLDS} ===\")\n    train_df = balanced_df.iloc[train_idx].reset_index(drop=True)\n    val_df   = balanced_df.iloc[val_idx].reset_index(drop=True)\n\n    # Save splits\n    train_df.to_csv(os.path.join(METRICS_DIR, f'train_fold_{fold}.csv'), index=False)\n    val_df.to_csv(  os.path.join(METRICS_DIR, f'val_fold_{fold}.csv'),   index=False)\n\n    train_loader = DataLoader(\n        MultiHemoDataset(train_df, PNG_ROOT, train_transform),\n        batch_size=BATCH_SIZE, shuffle=True,  num_workers=0)\n    val_loader   = DataLoader(\n        MultiHemoDataset(val_df,   PNG_ROOT, val_transform),\n        batch_size=BATCH_SIZE, shuffle=False, num_workers=0)\n\n    # Model / Loss / Optimizer\n    model     = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V1)\n    model.fc  = nn.Linear(model.fc.in_features, len(CLASS_NAMES))\n    model     = model.to(DEVICE)\n    criterion = nn.BCEWithLogitsLoss()\n    optimizer = torch.optim.Adam(model.parameters(),\n                                 lr=LEARNING_RATE,\n                                 weight_decay=WEIGHT_DECAY)\n\n    best_val_loss    = np.inf\n    # Prepare lists for logging\n    train_losses, train_accs = [], []\n    val_losses,   val_accs   = [], []\n\n    # --- Epoch Loop ---\n    for epoch in range(1, NUM_EPOCHS+1):\n        # — Training —\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            outputs = model(imgs)\n            loss    = criterion(outputs, labels)\n            loss.backward()\n            optimizer.step()\n\n            running_loss  += loss.item() * imgs.size(0)\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        # — Validation —\n        model.eval()\n        val_running_loss = val_running_corr = val_running_total = 0\n        all_preds, all_labels = [], []\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                loss    = criterion(outputs, labels)\n\n                val_running_loss  += loss.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                all_preds.extend(preds.cpu().numpy())\n                all_labels.extend(labels.cpu().numpy())\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        # Print epoch results\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            # Early Stopping\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(), os.path.join(OUTPUT_DIR, f'best_fold{fold}.pth'))\n        else:\n            no_improve += 1\n            if no_improve >= PATIENCE:\n                print(f\"Early stopping at epoch {epoch}\")\n                break\n\n        \n            # --- Plot & save Loss/Accuracy curves ---\n    epochs = range(1, len(train_losses)+1)\n    # Loss\n    plt.figure()\n    plt.plot(epochs, train_losses, label='Train Loss')\n    plt.plot(epochs, val_losses,   label='Val Loss')\n    plt.xlabel('Epoch'); plt.ylabel('Loss')\n    plt.title(f'Fold {fold} Loss Curve')\n    plt.legend()\n    plt.savefig(os.path.join(OUTPUT_DIR, f'loss_fold_{fold}.png'))\n    plt.close()\n    # Accuracy\n    plt.figure()\n    plt.plot(epochs, train_accs, label='Train Acc')\n    plt.plot(epochs, val_accs,   label='Val Acc')\n    plt.xlabel('Epoch'); plt.ylabel('Accuracy')\n    plt.title(f'Fold {fold} Accuracy Curve')\n    plt.legend()\n    plt.savefig(os.path.join(OUTPUT_DIR, f'acc_fold_{fold}.png'))\n    plt.close()\n\n\n    # Compute metrics\n    metrics = {}\n    for i, cls in enumerate(CLASS_NAMES):\n        preds = (val_probs[:, i] >= 0.5).astype(int)\n        p = precision_score(val_targets[:, i], preds, zero_division=0)\n        r = recall_score(   val_targets[:, i], preds, zero_division=0)\n        f = f1_score(       val_targets[:, i], preds, zero_division=0)\n        metrics[cls] = {'precision': p, 'recall': r, 'f1': f}\n        # ROC per fold\n        fpr, tpr, _ = roc_curve(val_targets[:, i], val_probs[:, i])\n        roc_auc = auc(fpr, tpr)\n        all_roc_data[cls].append((fpr, tpr, roc_auc))\n        # Save ROC per fold\n        plt.figure(); plt.plot(fpr, tpr, label=f\"AUC={roc_auc:.2f}\"); plt.plot([0,1],[0,1],'--')\n        plt.title(f\"ROC {cls} Fold{fold}\"); plt.xlabel('FPR'); plt.ylabel('TPR'); plt.legend()\n        plt.savefig(os.path.join(ROC_DIR, f'roc_{cls}_fold{fold}.png')); plt.close()\n\n    # Micro/Macro\n    preds_all = (val_probs>=0.5).astype(int)\n    metrics['micro'] = {} ; metrics['macro'] = {}\n    for avg in ['micro','macro']:\n        p,r,f,_ = precision_score(val_targets, preds_all, average=avg, zero_division=0), \\\n                  recall_score(  val_targets, preds_all, average=avg, zero_division=0), \\\n                  f1_score(      val_targets, preds_all, average=avg, zero_division=0), None\n        metrics[avg] = {'precision':p, 'recall':r, 'f1':f}\n\n    # Save metrics per fold\n    df_metrics = []\n    for key,val in metrics.items():\n        df_metrics.append({'fold':fold, 'class':key, **val})\n    pd.DataFrame(df_metrics).to_csv(\n        os.path.join(METRICS_DIR, f'metrics_fold_{fold}.csv'), index=False)\n    all_metrics.append(metrics)\n\n    # Standardized Grad-CAM analysis: TP/FP/FN samples\n    generate_gradcams(\n        model=model,\n        df=val_df,\n        probs=val_probs,\n        targets=val_targets,\n        classes=CLASS_NAMES,\n        png_root=PNG_ROOT,\n        transform=val_transform,\n        out_dir=os.path.join(CAM_DIR, f'fold_{fold}'),\n        fold=fold,\n        n_tp=5, n_fp=5, n_fn=5\n    )\n","metadata":{"trusted":true,"execution":{"execution_failed":"2025-04-27T17:12:45.479Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 5. Average metrics across folds\navg_metrics = {}\nfor key in all_metrics[0].keys():\n    vals = [fold_m[key] for fold_m in all_metrics]\n    avg_metrics[key] = {m: np.mean([v[m] for v in vals]) for m in ['precision','recall','f1']}\npd.DataFrame([{'class':k, **v} for k,v in avg_metrics.items()])\\\n    .to_csv(os.path.join(METRICS_DIR, 'metrics_average.csv'), index=False)","metadata":{"trusted":true,"execution":{"execution_failed":"2025-04-27T17:12:45.479Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 6. Aggregate ROC curves per class\ndef plot_avg_roc(cls):\n    mean_fpr = np.linspace(0,1,100)\n    tprs = []\n    aucs = []\n    for fpr,tpr,roc_auc in all_roc_data[cls]:\n        tprs.append(np.interp(mean_fpr, fpr, tpr))\n        aucs.append(roc_auc)\n    mean_tpr = np.mean(tprs, axis=0)\n    mean_auc = np.mean(aucs)\n    plt.plot(mean_fpr, mean_tpr, label=f\"{cls} (AUC={mean_auc:.2f})\")\n\nplt.figure()\nfor cls in CLASS_NAMES:\n    plot_avg_roc(cls)\nplt.plot([0,1],[0,1],'--', color='gray')\nplt.xlabel('FPR'); plt.ylabel('TPR'); plt.title('Average ROC')\nplt.legend(); plt.savefig(os.path.join(ROC_DIR, 'roc_average.png'))\nplt.close()","metadata":{"trusted":true,"execution":{"execution_failed":"2025-04-27T17:12:45.479Z"}},"outputs":[],"execution_count":null}]}