{"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":"none","dataSources":[{"sourceId":13451,"databundleVersionId":1188070,"sourceType":"competition"}],"dockerImageVersionId":31192,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"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},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================================================================\n# RSNA ICH - EfficientNet Embeddings + GCN + Knowledge Graph\n# Single-cell full pipeline (uses your CSV + ID_<Image>.dcm files)\n# ================================================================\nimport os, time, random\nimport numpy as np\nimport pandas as pd\nfrom tqdm import tqdm\nimport cv2, pydicom\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import (\n    accuracy_score, precision_score, recall_score,\n    f1_score, cohen_kappa_score, roc_auc_score\n)\nfrom sklearn.metrics.pairwise import cosine_similarity\nfrom sklearn.decomposition import PCA\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms, models\n\n# ---------------- CONFIG ----------------\nSEED = 42\nrandom.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Device:\", DEVICE)\n\nBASE = \"/kaggle/input/rsna-intracranial-hemorrhage-detection/rsna-intracranial-hemorrhage-detection\"\nCSV_PATH = os.path.join(BASE, \"stage_2_train.csv\")\nTRAIN_DIR = os.path.join(BASE, \"stage_2_train\")\n\nIMG_SIZE = 256\nSAMPLES_PER_CLASS = 600   # total ~1200 images → faster on CPU\nBATCH = 16\nEMB_BATCH = 16\n\nPCA_DIM = 256\nK_NEIGH = 10\n\nEPOCHS_EFF = 2            # small to stay fast\nEPOCHS_GCN = 25\nLR_EFF = 3e-4\nLR_GCN = 3e-4\n\nLABEL_SMOOTH = 0.05\n\n# ================================================================\n#   1. TRIPLE-WINDOW CT → 3-CHANNEL IMAGE\n# ================================================================\ndef window_image(img, wl, ww):\n    low = wl - ww / 2.0\n    high = wl + ww / 2.0\n    out = (img - low) / (high - low + 1e-9)\n    return np.clip(out, 0.0, 1.0)\n\ndef make_3ch_dcm(path):\n    dcm = pydicom.dcmread(path)\n    img = dcm.pixel_array.astype(np.float32)\n\n    # common RSNA windows\n    ch1 = window_image(img, 40, 80)       # brain\n    ch2 = window_image(img, 80, 200)      # subdural-ish\n    ch3 = window_image(img, 600, 2800)    # bone\n\n    ch1 = cv2.resize(ch1, (IMG_SIZE, IMG_SIZE))\n    ch2 = cv2.resize(ch2, (IMG_SIZE, IMG_SIZE))\n    ch3 = cv2.resize(ch3, (IMG_SIZE, IMG_SIZE))\n\n    return np.stack([ch1, ch2, ch3], axis=0)   # (3,H,W)\n\n# ================================================================\n#   2. LOAD CSV + BUILD BINARY + BALANCED SUBSET\n# ================================================================\ndf = pd.read_csv(CSV_PATH)\n\n# Your CSV example already has Image, Subtype, but we make sure:\nif \"Image\" not in df.columns or \"Subtype\" not in df.columns:\n    df[\"Image\"] = df[\"ID\"].apply(lambda x: x.split(\"_\")[1])\n    df[\"Subtype\"] = df[\"ID\"].apply(lambda x: x.split(\"_\")[2])\n\n# Aggregate to per-image multi-label\ndf_group = df.groupby([\"Image\",\"Subtype\"], as_index=False)[\"Label\"].max()\ndf_pivot = df_group.pivot(index=\"Image\", columns=\"Subtype\", values=\"Label\").fillna(0)\n\n# Binary: any hemorrhage\ndf_pivot[\"Label_binary\"] = df_pivot.max(axis=1).astype(int)\n\nsubtype_cols = list(df_pivot.columns[:-1])  # all subtypes\nC = len(subtype_cols)\nprint(\"Subtypes:\", subtype_cols)\n\n# Reset index so \"Image\" is a column\ndf_pivot = df_pivot.reset_index()   # columns: Image, <subtypes...>, Label_binary\n\n# Balanced subset\nsamples = []\nfor lbl in [0,1]:\n    sub = df_pivot[df_pivot[\"Label_binary\"] == lbl]\n    n = min(SAMPLES_PER_CLASS, len(sub))\n    samples.append(sub.sample(n, random_state=SEED))\ndf_bal = pd.concat(samples).reset_index(drop=True)\nprint(\"Balanced counts:\\n\", df_bal[\"Label_binary\"].value_counts())\n\n# Keep an index by Image for later graph / labels\ndf_indexed = df_bal.set_index(\"Image\")   # index: Image, has subtypes + Label_binary\n\n# ================================================================\n#   3. DATASET FOR EFFICIENTNET\n# ================================================================\nclass RSNADataset(Dataset):\n    def __init__(self, df, img_dir, augment=False):\n        self.df = df.reset_index(drop=True)\n        self.img_dir = img_dir\n        self.augment = augment\n\n        self.tf_aug = transforms.Compose([\n            transforms.ToPILImage(),\n            transforms.RandomResizedCrop(IMG_SIZE, scale=(0.85,1.0)),\n            transforms.RandomHorizontalFlip(),\n            transforms.RandomRotation(10),\n            transforms.ToTensor(),\n            transforms.Normalize([0.485,0.456,0.406], [0.229,0.224,0.225])\n        ])\n        self.tf_eval = transforms.Compose([\n            transforms.ToPILImage(),\n            transforms.Resize((IMG_SIZE, IMG_SIZE)),\n            transforms.ToTensor(),\n            transforms.Normalize([0.485,0.456,0.406], [0.229,0.224,0.225])\n        ])\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_id = row[\"Image\"]                # <-- IMPORTANT: correct image ID\n        label = int(row[\"Label_binary\"])\n\n        dcm_path = os.path.join(self.img_dir, f\"ID_{img_id}.dcm\")\n        img3 = make_3ch_dcm(dcm_path)        # (3,H,W), float [0..1]\n        img3 = (img3 * 255).astype(np.uint8) # to uint8\n        img3 = np.transpose(img3, (1,2,0))   # (H,W,3) for ToPILImage\n\n        tf = self.tf_aug if self.augment else self.tf_eval\n        img_t = tf(img3)\n        return img_t, label, img_id\n\n# ================================================================\n#   4. SPLIT TO TRAIN / VAL / TEST\n# ================================================================\ntrain_df, temp_df = train_test_split(\n    df_bal,\n    test_size=0.3,\n    stratify=df_bal[\"Label_binary\"],\n    random_state=SEED\n)\nval_df, test_df = train_test_split(\n    temp_df,\n    test_size=0.5,\n    stratify=temp_df[\"Label_binary\"],\n    random_state=SEED\n)\n\nprint(\"Split sizes: Train =\", len(train_df), \"Val =\", len(val_df), \"Test =\", len(test_df))\n\ntrain_ds = RSNADataset(train_df, TRAIN_DIR, augment=True)\nval_ds   = RSNADataset(val_df, TRAIN_DIR, augment=False)\ntest_ds  = RSNADataset(test_df, TRAIN_DIR, augment=False)\n\ntrain_loader = DataLoader(train_ds, batch_size=BATCH, shuffle=True, num_workers=2, pin_memory=True)\nval_loader   = DataLoader(val_ds,   batch_size=BATCH, shuffle=False, num_workers=2, pin_memory=True)\ntest_loader  = DataLoader(test_ds,  batch_size=BATCH, shuffle=False, num_workers=2, pin_memory=True)\n\n# ================================================================\n#   5. EFFICIENTNET-B0 FINE-TUNE (BINARY)\n# ================================================================\ndef make_efficientnet_model(num_classes=2):\n    effnet = models.efficientnet_b0(pretrained=True)\n    in_features = effnet.classifier[1].in_features\n    effnet.classifier[1] = nn.Linear(in_features, num_classes)\n    return effnet\n\neffnet = make_efficientnet_model(2).to(DEVICE)\n\nfor p in effnet.parameters():\n    p.requires_grad = True\n\ncrit_eff = nn.CrossEntropyLoss(label_smoothing=LABEL_SMOOTH)\nopt_eff  = torch.optim.AdamW(effnet.parameters(), lr=LR_EFF)\n\ndef eval_eff_acc(model, loader):\n    model.eval()\n    all_preds, all_labs = [], []\n    with torch.no_grad():\n        for imgs, labels, _ in loader:\n            imgs = imgs.to(DEVICE); labels = labels.to(DEVICE)\n            logits = model(imgs)\n            preds = torch.argmax(logits, dim=1).cpu().numpy()\n            labs  = labels.cpu().numpy()\n            all_preds.extend(preds); all_labs.extend(labs)\n    return accuracy_score(all_labs, all_preds)\n\nprint(\"\\n=== Stage 1: EfficientNet fine-tuning (short) ===\")\nfor ep in range(1, EPOCHS_EFF+1):\n    effnet.train()\n    running_loss = 0.0\n    t0 = time.time()\n    for imgs, labels, _ in train_loader:\n        imgs = imgs.to(DEVICE)\n        labels = labels.to(DEVICE)\n\n        opt_eff.zero_grad()\n        logits = effnet(imgs)\n        loss = crit_eff(logits, labels)\n        loss.backward()\n        torch.nn.utils.clip_grad_norm_(effnet.parameters(), max_norm=5.0)\n        opt_eff.step()\n\n        running_loss += loss.item() * imgs.size(0)\n\n    train_loss = running_loss / len(train_ds)\n    val_acc = eval_eff_acc(effnet, val_loader)\n    print(f\"Eff Ep {ep}/{EPOCHS_EFF} | Loss:{train_loss:.4f} | ValAcc:{val_acc:.4f} | time:{time.time()-t0:.1f}s\")\n\n# ================================================================\n#   6. EMBEDDING EXTRACTOR\n# ================================================================\nclass EffnetEmbedder(nn.Module):\n    def __init__(self, eff):\n        super().__init__()\n        self.features = eff.features\n        self.avgpool  = eff.avgpool\n        self.dropout  = eff.classifier[0]  # dropout\n        self.feat_dim = eff.classifier[1].in_features\n\n    def forward(self, x):\n        x = self.features(x)\n        x = self.avgpool(x)\n        x = torch.flatten(x, 1)\n        x = self.dropout(x)\n        return x   # (N, feat_dim)\n\nembedder = EffnetEmbedder(effnet).to(DEVICE)\nembedder.eval()\n\ndef extract_embeddings_for_df(df_subset):\n    ds = RSNADataset(df_subset, TRAIN_DIR, augment=False)\n    dl = DataLoader(ds, batch_size=EMB_BATCH, shuffle=False, num_workers=2, pin_memory=True)\n\n    embs, ids = [], []\n    with torch.no_grad():\n        for imgs, _, id_batch in tqdm(dl, desc=\"Extracting embeddings\"):\n            imgs = imgs.to(DEVICE)\n            feat = embedder(imgs)          # (B, feat_dim)\n            embs.append(feat.cpu().numpy())\n            ids.extend(id_batch)\n    X = np.vstack(embs)\n    return X, ids\n\nprint(\"\\n=== Stage 2: Extracting embeddings ===\")\nX_tr, ids_tr = extract_embeddings_for_df(train_df)\nX_val, ids_val = extract_embeddings_for_df(val_df)\nX_te, ids_te = extract_embeddings_for_df(test_df)\n\nprint(\"Embedding dims:\", X_tr.shape, X_val.shape, X_te.shape)\n\n# ================================================================\n#   7. PCA REDUCTION\n# ================================================================\nif PCA_DIM is not None and PCA_DIM < X_tr.shape[1]:\n    print(\"Fitting PCA to\", PCA_DIM, \"dims...\")\n    pca = PCA(n_components=PCA_DIM, random_state=SEED)\n    X_tr = pca.fit_transform(X_tr)\n    X_val = pca.transform(X_val)\n    X_te = pca.transform(X_te)\n    print(\"New shapes:\", X_tr.shape, X_val.shape, X_te.shape)\nelse:\n    PCA_DIM = X_tr.shape[1]\n\n# For PMI later\nB_full = df_bal[subtype_cols].values.astype(float)\n\n# ================================================================\n#   8. GRAPH CONSTRUCTION (IMAGE + KG)\n# ================================================================\ndef construct_combined_graph(X_images, ids_images, k_neighbors=K_NEIGH):\n    \"\"\"\n    Combined graph:\n      - image-image: mutual kNN cosine\n      - image-concept: subtype incidence\n      - concept-concept: PMI\n    \"\"\"\n    N = X_images.shape[0]\n\n    # image-image similarity\n    sim = cosine_similarity(X_images)\n    np.fill_diagonal(sim, 0.0)\n    sim = np.power(sim, 3)\n\n    A_ii = np.zeros_like(sim)\n    for i in range(N):\n        k = min(k_neighbors, N-1)\n        if k <= 0:\n            continue\n        nbrs = np.argsort(sim[i])[-k:]\n        A_ii[i, nbrs] = sim[i, nbrs]\n    A_ii = np.minimum(A_ii, A_ii.T)\n\n    # image-concept incidence\n    B_img = df_indexed.loc[ids_images, subtype_cols].values.astype(float)  # (N, C)\n    row_sum = B_img.sum(axis=1, keepdims=True)\n    B_norm = B_img / (row_sum + 1e-6)\n    B_norm[row_sum.squeeze(-1) == 0] = 0.0\n    A_ic = B_norm\n    A_ci = A_ic.T.copy()\n\n    # concept-concept PMI (based on full balanced set)\n    p = B_full.mean(axis=0) + 1e-9\n    co = B_full.T @ B_full\n    pmi = np.log((co + 1e-6) / (p[:, None] * p[None, :]))\n    pmi[pmi < 0] = 0.0\n    A_cc = pmi\n\n    # block concat\n    top = np.concatenate([A_ii, A_ic], axis=1)\n    bottom = np.concatenate([A_ci, A_cc], axis=1)\n    A = np.concatenate([top, bottom], axis=0)\n\n    # symmetric normalization\n    deg = A.sum(axis=1)\n    inv_sqrt = 1.0 / (np.sqrt(deg) + 1e-9)\n    A_norm = (inv_sqrt[:, None] * A) * inv_sqrt[None, :]\n\n    # node features: images have X_images; concepts start as zeros\n    X_all = np.vstack([X_images, np.zeros((C, X_images.shape[1]), dtype=float)])\n\n    X_all_t  = torch.tensor(X_all,  dtype=torch.float32, device=DEVICE)\n    A_norm_t = torch.tensor(A_norm, dtype=torch.float32, device=DEVICE)\n\n    img_idx     = np.arange(N)\n    concept_idx = np.arange(N, N + C)\n\n    labels_img = np.array([df_indexed.loc[iid, \"Label_binary\"] for iid in ids_images], dtype=int)\n    return X_all_t, A_norm_t, img_idx, concept_idx, labels_img\n\nprint(\"\\n=== Stage 3: Building graphs (train/val/test) ===\")\nX_all_tr, A_tr, img_idx_tr, concept_idx_tr, labels_tr = construct_combined_graph(X_tr, ids_tr)\nX_all_val, A_val, img_idx_val, concept_idx_val, labels_val = construct_combined_graph(X_val, ids_val)\nX_all_te,  A_te,  img_idx_te,  concept_idx_te, labels_te  = construct_combined_graph(X_te, ids_te)\n\nprint(\"Train graph:\", X_all_tr.shape, A_tr.shape)\n\n# ================================================================\n#   9. GCN + KNOWLEDGE GRAPH MODEL\n# ================================================================\nclass GCNLayer(nn.Module):\n    def __init__(self, in_dim, out_dim):\n        super().__init__()\n        self.lin = nn.Linear(in_dim, out_dim)\n\n    def forward(self, X, A):\n        X = self.lin(X)\n        X = A @ X\n        return F.relu(X)\n\nclass GCN_KG(nn.Module):\n    \"\"\"\n    GCN over combined Image+KG graph:\n      - concept nodes have learnable embeddings\n      - 2-layer GCN\n      - classifier on image-node embeddings\n    \"\"\"\n    def __init__(self, in_dim, hidden=256, out_dim=2,\n                 num_concepts=C, concept_emb_dim=256):\n        super().__init__()\n        self.num_concepts = num_concepts\n\n        self.concept_embed = nn.Embedding(num_concepts, concept_emb_dim)\n        nn.init.xavier_uniform_(self.concept_embed.weight)\n        self.concept_proj = nn.Linear(concept_emb_dim, in_dim)\n\n        self.gcn1 = GCNLayer(in_dim, hidden)\n        self.gcn2 = GCNLayer(hidden, hidden)\n        self.dropout = nn.Dropout(0.4)\n        self.classifier = nn.Linear(hidden, out_dim)\n\n    def inject_concepts(self, X_all, concept_idx):\n        if concept_idx is None or len(concept_idx) == 0:\n            return X_all\n        concept_idx_t = torch.tensor(concept_idx, dtype=torch.long, device=X_all.device)\n        concept_ids   = torch.arange(self.num_concepts, dtype=torch.long, device=X_all.device)\n        c_emb = self.concept_proj(self.concept_embed(concept_ids))  # (C,in_dim)\n        X_new = X_all.clone()\n        X_new[concept_idx_t, :] = c_emb\n        return X_new\n\n    def forward(self, X_all, A_norm, img_idx, concept_idx):\n        X_in = self.inject_concepts(X_all, concept_idx)\n        h = self.gcn1(X_in, A_norm)\n        h = self.dropout(h)\n        h = self.gcn2(h, A_norm)\n        h = self.dropout(h)\n\n        img_idx_t = torch.tensor(img_idx, dtype=torch.long, device=h.device)\n        h_img = h[img_idx_t]\n        logits = self.classifier(h_img)\n        return logits, h_img\n\ndef eval_gcn_kg(model, X_all, A_norm, img_idx, concept_idx, labels_img):\n    model.eval()\n    with torch.no_grad():\n        logits, h_img = model(X_all, A_norm, img_idx, concept_idx)\n        preds = torch.argmax(logits, dim=1).cpu().numpy()\n        probs = F.softmax(logits, dim=1)[:,1].cpu().numpy()\n\n    labels = labels_img\n    acc   = accuracy_score(labels, preds)\n    prec  = precision_score(labels, preds, zero_division=0)\n    rec   = recall_score(labels, preds, zero_division=0)\n    f1    = f1_score(labels, preds, zero_division=0)\n    kappa = cohen_kappa_score(labels, preds)\n    try:\n        auc = roc_auc_score(labels, probs)\n    except Exception:\n        auc = float(\"nan\")\n    return acc, prec, rec, f1, auc, kappa\n\nmodel = GCN_KG(in_dim=X_all_tr.shape[1], hidden=256, out_dim=2, num_concepts=C, concept_emb_dim=256).to(DEVICE)\nopt_gcn = torch.optim.AdamW(model.parameters(), lr=LR_GCN, weight_decay=1e-5)\ncrit_gcn = nn.CrossEntropyLoss(label_smoothing=LABEL_SMOOTH)\n\nlabels_tr_t = torch.tensor(labels_tr, dtype=torch.long, device=DEVICE)\n\nprint(\"\\n=== Stage 4: Training GCN + KG ===\")\nbest_val_acc = -1.0\nbest_state = None\npatience = 7\npat = 0\n\nfor ep in range(1, EPOCHS_GCN+1):\n    t0 = time.time()\n    model.train()\n    opt_gcn.zero_grad()\n\n    logits, _ = model(X_all_tr, A_tr, img_idx_tr, concept_idx_tr)\n    loss = crit_gcn(logits, labels_tr_t)\n    loss.backward()\n    torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0)\n    opt_gcn.step()\n\n    # metrics\n    tr_acc, tr_prec, tr_rec, tr_f1, tr_auc, tr_kappa = eval_gcn_kg(\n        model, X_all_tr, A_tr, img_idx_tr, concept_idx_tr, labels_tr\n    )\n    val_acc, val_prec, val_rec, val_f1, val_auc, val_kappa = eval_gcn_kg(\n        model, X_all_val, A_val, img_idx_val, concept_idx_val, labels_val\n    )\n\n    print(f\"Ep {ep}/{EPOCHS_GCN} | Loss:{loss.item():.4f} | \"\n          f\"TrAcc:{tr_acc:.4f} ValAcc:{val_acc:.4f} | \"\n          f\"ValF1:{val_f1:.4f} ValAUC:{val_auc:.4f} | time:{time.time()-t0:.1f}s\")\n\n    # early stopping\n    if val_acc > best_val_acc:\n        best_val_acc = val_acc\n        best_state = model.state_dict()\n        pat = 0\n    else:\n        pat += 1\n        if pat >= patience:\n            print(\"Early stopping GCN+KG.\")\n            break\n\nif best_state is not None:\n    model.load_state_dict(best_state)\n    print(f\"Loaded best GCN+KG model (ValAcc={best_val_acc:.4f})\")\n\n# ================================================================\n#   10. FINAL METRICS\n# ================================================================\ntrain_metrics = eval_gcn_kg(model, X_all_tr, A_tr, img_idx_tr, concept_idx_tr, labels_tr)\nval_metrics   = eval_gcn_kg(model, X_all_val, A_val, img_idx_val, concept_idx_val, labels_val)\ntest_metrics  = eval_gcn_kg(model, X_all_te, A_te, img_idx_te, concept_idx_te, labels_te)\n\nprint(\"\\n=== FINAL METRICS (GCN + KG) ===\")\nprint(\"Train Acc, Prec, Rec, F1, AUC, Kappa:\", train_metrics)\nprint(\"Val   Acc, Prec, Rec, F1, AUC, Kappa:\", val_metrics)\nprint(\"Test  Acc, Prec, Rec, F1, AUC, Kappa:\", test_metrics)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-11T21:39:57.221042Z","iopub.execute_input":"2025-12-11T21:39:57.221401Z","iopub.status.idle":"2025-12-11T21:46:36.580499Z","shell.execute_reply.started":"2025-12-11T21:39:57.22138Z","shell.execute_reply":"2025-12-11T21:46:36.579333Z"}},"outputs":[],"execution_count":null}]}