{"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# BrainGNN + Knowledge Graph (RSNA Intracranial Hemorrhage)\n# ===============================================================\nimport os, random, time\nimport numpy as np\nimport pandas as pd\nfrom tqdm import tqdm\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import models, transforms\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score, cohen_kappa_score\nfrom sklearn.metrics.pairwise import cosine_similarity\nfrom sklearn.decomposition import PCA\nimport pydicom, cv2\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_PATH = \"/kaggle/input/rsna-intracranial-hemorrhage-detection/rsna-intracranial-hemorrhage-detection\"\nCSV_PATH = os.path.join(BASE_PATH, \"stage_2_train.csv\")\nIMG_DIR = os.path.join(BASE_PATH, \"stage_2_train\")\n\nSAMPLES_PER_CLASS = 2000\nIMG_SIZE = 160\nBATCH = 64\nFT_EPOCHS = 2\nEPOCHS = 20\nLR_BACKBONE = 2e-4\nLR_GNN = 1e-3\nK_NEIGH = 16\nPCA_DIM = 256\nKG_EMB_DIM = 16\nTEMP = 0.2\nALPHA_SUP = 1.0\nALPHA_CON = 1.0\n\nSUBTYPE_COLS = ['any','epidural','intraparenchymal','intraventricular','subarachnoid','subdural']\n\n# -------------------- HELPERS --------------------\ndef set_seed(s=SEED):\n    random.seed(s); np.random.seed(s); torch.manual_seed(s)\nset_seed()\n\ndef apply_brain_window(img, level=40, width=80):\n    low = level - width/2.0\n    high = level + width/2.0\n    img_w = np.clip(img, low, high)\n    img_w = (img_w - low) / (high - low + 1e-6)\n    return img_w.astype(np.float32)\n\ndef load_dicom(path):\n    d = pydicom.dcmread(path)\n    arr = d.pixel_array.astype(np.float32)\n    arr = apply_brain_window(arr)\n    return arr\n\ndef make_3ch(path):\n    arr = load_dicom(path)\n    arr = cv2.resize(arr,(IMG_SIZE,IMG_SIZE))\n    return np.stack([arr,arr,arr], axis=-1)\n\n# -------------------- LOAD & BALANCE LABELS --------------------\ndf = pd.read_csv(CSV_PATH)\ndf[\"Image\"] = df[\"ID\"].apply(lambda x: x.split(\"_\")[1])\ndf[\"Subtype\"] = df[\"ID\"].apply(lambda x: x.split(\"_\")[2])\ndf2 = df.groupby([\"Image\",\"Subtype\"], as_index=False)[\"Label\"].max()\ndf_pivot = df2.pivot(index=\"Image\", columns=\"Subtype\", values=\"Label\").reset_index().fillna(0)\ndf_pivot[\"Label_binary\"] = df_pivot.iloc[:,1:].max(axis=1).astype(int)\n\n# Balanced sampling\npos = df_pivot[df_pivot[\"Label_binary\"]==1].sample(n=SAMPLES_PER_CLASS, random_state=SEED)\nneg = df_pivot[df_pivot[\"Label_binary\"]==0].sample(n=SAMPLES_PER_CLASS, random_state=SEED)\ndf_bal = pd.concat([pos,neg]).sample(frac=1, random_state=SEED).reset_index(drop=True)\n\n# -------------------- DATASET --------------------\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        self.aug_tf = transforms.Compose([\n            transforms.ToPILImage(),\n            transforms.Resize((IMG_SIZE,IMG_SIZE)),\n            transforms.RandomResizedCrop(IMG_SIZE, scale=(0.85,1.0)),\n            transforms.RandomHorizontalFlip(),\n            transforms.RandomRotation(10),\n            transforms.ToTensor()\n        ])\n        self.eval_tf = transforms.Compose([\n            transforms.ToPILImage(),\n            transforms.Resize((IMG_SIZE,IMG_SIZE)),\n            transforms.ToTensor()\n        ])\n    def __len__(self):\n        return len(self.df)\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_path = os.path.join(self.img_dir, f\"ID_{row.Image}.dcm\")\n        img = make_3ch(img_path)\n        tf = self.aug_tf if self.augment else self.eval_tf\n        img_t = tf(img)\n        label = int(row.Label_binary)\n        return img_t.float(), label, row.Image\n\n# -------------------- SPLIT --------------------\ntrain_df, temp_df = train_test_split(df_bal,test_size=0.3,stratify=df_bal[\"Label_binary\"],random_state=SEED)\nval_df, test_df = train_test_split(temp_df,test_size=0.5,stratify=temp_df[\"Label_binary\"],random_state=SEED)\n\ntrain_ds = RSNADataset(train_df, IMG_DIR, augment=True)\nval_ds = RSNADataset(val_df, IMG_DIR, augment=False)\ntest_ds = RSNADataset(test_df, IMG_DIR, augment=False)\n\ntrain_loader = DataLoader(train_ds, batch_size=BATCH, shuffle=True)\nval_loader = DataLoader(val_ds, batch_size=BATCH, shuffle=False)\ntest_loader = DataLoader(test_ds, batch_size=BATCH, shuffle=False)\n\n# -------------------- BACKBONE --------------------\nresnet = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1)\nembedding_dim = resnet.fc.in_features\nresnet.fc = nn.Identity()\nresnet = resnet.to(DEVICE)\n\n# -------------------- EXTRACT EMBEDDINGS --------------------\ndef extract_embeddings(loader):\n    resnet.eval()\n    embs=[]; labs=[]; ids=[]\n    with torch.no_grad():\n        for imgs, lab, idlist in tqdm(loader):\n            imgs = imgs.to(DEVICE)\n            feat = resnet(imgs)\n            embs.append(feat.cpu().numpy())\n            labs.extend(lab.numpy())\n            ids.extend(idlist)\n    return np.vstack(embs), np.array(labs), ids\n\nX_tr, y_tr, ids_tr = extract_embeddings(train_loader)\nX_val, y_val, ids_val = extract_embeddings(val_loader)\nX_te, y_te, ids_te = extract_embeddings(test_loader)\n\n# PCA dim reduction\nif PCA_DIM is not None and PCA_DIM < X_tr.shape[1]:\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\n# -------------------- CONSTRUCT GRAPH --------------------\nclass ConstructGraph:\n    def __init__(self, df_indexed, subtype_cols, k_neigh=K_NEIGH, kg_dim=KG_EMB_DIM):\n        self.df_indexed = df_indexed\n        self.subtype_cols = subtype_cols\n        self.k_neigh = k_neigh\n        self.kg_dim = kg_dim\n        self.concept_feats = np.random.normal(0,0.01,(len(subtype_cols), kg_dim))\n\n    def build_knn_adj(self,X,k=None):\n        if k is None: k=self.k_neigh\n        sim = cosine_similarity(X); np.fill_diagonal(sim,0)\n        N=sim.shape[0]; A=np.zeros_like(sim)\n        for i in range(N):\n            idx = np.argsort(sim[i])[-k:]\n            A[i, idx] = sim[i, idx]\n        A = np.maximum(A, A.T) + np.eye(N)*1e-6\n        deg = A.sum(1); deg_inv_sqrt=1.0/np.sqrt(deg)\n        return (deg_inv_sqrt[:,None]*A)*deg_inv_sqrt[None,:]\n\n    def build_graph(self,X_img,img_ids):\n        N = X_img.shape[0]; C = self.concept_feats.shape[0]\n        A_ii = self.build_knn_adj(X_img)\n        A_ic = np.zeros((N,C))\n        for i,iid in enumerate(img_ids):\n            if iid in self.df_indexed.index:\n                A_ic[i,:] = self.df_indexed.loc[iid,self.subtype_cols].values\n        A_ci = A_ic.T\n        simc = cosine_similarity(self.concept_feats); np.fill_diagonal(simc,0)\n        A_cc = simc\n        top = np.concatenate([A_ii,A_ic],1)\n        bottom = np.concatenate([A_ci,A_cc],1)\n        A = np.concatenate([top,bottom],0) + np.eye(N+C)*1e-6\n        deg = A.sum(1); deg_inv_sqrt=1.0/np.sqrt(deg)\n        A_norm = (deg_inv_sqrt[:,None]*A)*deg_inv_sqrt[None,:]\n        D_img = X_img.shape[1]; D_con = self.concept_feats.shape[1]\n        if D_con<D_img:\n            con_padded = np.concatenate([self.concept_feats,np.zeros((C,D_img-D_con))],1)\n        else:\n            con_padded = self.concept_feats[:,:D_img]\n        X_all = np.vstack([X_img,con_padded])\n        labels_img = np.array([self.df_indexed.loc[iid,\"Label_binary\"] if iid in self.df_indexed.index else 0 for iid in img_ids])\n        return X_all, A_norm, labels_img\n\ndf_indexed = df_pivot.set_index(\"Image\")\ngraph_builder = ConstructGraph(df_indexed, SUBTYPE_COLS)\nX_all_tr, A_tr, labels_tr = graph_builder.build_graph(X_tr, ids_tr)\nX_all_val, A_val, labels_val = graph_builder.build_graph(X_val, ids_val)\nX_all_te,  A_te,  labels_te  = graph_builder.build_graph(X_te, ids_te)\n\n# Convert to torch\nX_tr_t = torch.tensor(X_all_tr, dtype=torch.float32, device=DEVICE)\nA_tr_t = torch.tensor(A_tr, dtype=torch.float32, device=DEVICE)\ny_tr_img = torch.tensor(labels_tr, dtype=torch.long, device=DEVICE)\nX_val_t = torch.tensor(X_all_val, dtype=torch.float32, device=DEVICE)\nA_val_t = torch.tensor(A_val, dtype=torch.float32, device=DEVICE)\ny_val_img = torch.tensor(labels_val, dtype=torch.long, device=DEVICE)\nX_te_t = torch.tensor(X_all_te, dtype=torch.float32, device=DEVICE)\nA_te_t = torch.tensor(A_te, dtype=torch.float32, device=DEVICE)\nN_img_tr = X_tr.shape[0]; N_img_val = X_val.shape[0]; N_img_te = X_te.shape[0]\n\n# -------------------- BrainGNN (Simple GNN block) --------------------\nclass SimpleGCNBlock(nn.Module):\n    def __init__(self, in_dim, hid_dim):\n        super().__init__()\n        self.lin1 = nn.Linear(in_dim,hid_dim)\n        self.lin2 = nn.Linear(hid_dim,hid_dim)\n        self.dropout = nn.Dropout(0.4)\n        self.bn = nn.LayerNorm(hid_dim)\n    def forward(self,X,A):\n        h = F.relu(self.bn(self.lin1(X)))\n        h = A@h\n        h = self.dropout(h)\n        h = F.relu(self.lin2(h))\n        h = A@h\n        return h\n\nclass BrainGNN(nn.Module):\n    def __init__(self, feat_dim,hid_dim=256,n_classes=2):\n        super().__init__()\n        self.encoder = nn.Linear(feat_dim,hid_dim)\n        self.gcn = SimpleGCNBlock(hid_dim,hid_dim)\n        self.classifier = nn.Linear(hid_dim,n_classes)\n    def forward(self,X,A):\n        H = F.relu(self.encoder(X))\n        H_g = self.gcn(H,A)\n        logits = self.classifier(H_g)\n        return logits\n\n# -------------------- TRAIN --------------------\nmodel = BrainGNN(X_all_tr.shape[1], hid_dim=256).to(DEVICE)\nopt = torch.optim.AdamW(model.parameters(), lr=LR_GNN)\ncrit = nn.CrossEntropyLoss()\n\nfor ep in range(1,EPOCHS+1):\n    model.train()\n    opt.zero_grad()\n    logits_all = model(X_tr_t, A_tr_t)\n    logits_img = logits_all[:N_img_tr]\n    loss = crit(logits_img, y_tr_img)\n    loss.backward(); opt.step()\n    if ep%5==0:\n        print(f\"Epoch {ep}/{EPOCHS} Loss:{loss.item():.4f}\")\n\n# -------------------- FINAL METRICS --------------------\nmodel.eval()\nwith torch.no_grad():\n    logits_te_all = model(X_te_t, A_te_t)\n    preds = torch.argmax(logits_te_all[:N_img_te],dim=1).cpu().numpy()\n    probs = F.softmax(logits_te_all[:N_img_te],dim=1)[:,1].cpu().numpy()\n    acc = accuracy_score(labels_te, preds)\n    prec = precision_score(labels_te, preds, zero_division=0)\n    rec = recall_score(labels_te, preds, zero_division=0)\n    f1s = f1_score(labels_te, preds, zero_division=0)\n    auc = roc_auc_score(labels_te, probs)\n    kappa = cohen_kappa_score(labels_te, preds)\nprint(\"\\n=== FINAL METRICS ===\")\nprint(f\"Accuracy : {acc:.4f}\")\nprint(f\"ROC-AUC  : {auc:.4f}\")\nprint(f\"Precision: {prec:.4f}\")\nprint(f\"Recall   : {rec:.4f}\")\nprint(f\"F1 Score : {f1s:.4f}\")\nprint(f\"Cohen Kappa : {kappa:.4f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-19T17:50:02.217046Z","iopub.execute_input":"2025-11-19T17:50:02.217355Z","iopub.status.idle":"2025-11-19T17:52:48.303786Z","shell.execute_reply.started":"2025-11-19T17:50:02.217332Z","shell.execute_reply":"2025-11-19T17:52:48.302709Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ===============================================================\n# BrainGNN + Knowledge Graph (RSNA Intracranial Hemorrhage)\n# ===============================================================\nimport os, random, time\nimport numpy as np\nimport pandas as pd\nfrom tqdm import tqdm\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import models, transforms\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score, cohen_kappa_score\nfrom sklearn.metrics.pairwise import cosine_similarity\nfrom sklearn.decomposition import PCA\nimport pydicom, cv2\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_PATH = \"/kaggle/input/rsna-intracranial-hemorrhage-detection/rsna-intracranial-hemorrhage-detection\"\nCSV_PATH = os.path.join(BASE_PATH, \"stage_2_train.csv\")\nIMG_DIR = os.path.join(BASE_PATH, \"stage_2_train\")\n\nSAMPLES_PER_CLASS = 2000\nIMG_SIZE = 160\nBATCH = 64\nFT_EPOCHS = 2\nEPOCHS = 50\nLR_BACKBONE = 2e-4\nLR_GNN = 1e-3\nK_NEIGH = 16\nPCA_DIM = 256\nKG_EMB_DIM = 16\nTEMP = 0.2\nALPHA_SUP = 1.0\nALPHA_CON = 1.0\n\nSUBTYPE_COLS = ['any','epidural','intraparenchymal','intraventricular','subarachnoid','subdural']\n\n# -------------------- HELPERS --------------------\ndef set_seed(s=SEED):\n    random.seed(s); np.random.seed(s); torch.manual_seed(s)\nset_seed()\n\ndef apply_brain_window(img, level=40, width=80):\n    low = level - width/2.0\n    high = level + width/2.0\n    img_w = np.clip(img, low, high)\n    img_w = (img_w - low) / (high - low + 1e-6)\n    return img_w.astype(np.float32)\n\ndef load_dicom(path):\n    d = pydicom.dcmread(path)\n    arr = d.pixel_array.astype(np.float32)\n    arr = apply_brain_window(arr)\n    return arr\n\ndef make_3ch(path):\n    arr = load_dicom(path)\n    arr = cv2.resize(arr,(IMG_SIZE,IMG_SIZE))\n    return np.stack([arr,arr,arr], axis=-1)\n\n# -------------------- LOAD & BALANCE LABELS --------------------\ndf = pd.read_csv(CSV_PATH)\ndf[\"Image\"] = df[\"ID\"].apply(lambda x: x.split(\"_\")[1])\ndf[\"Subtype\"] = df[\"ID\"].apply(lambda x: x.split(\"_\")[2])\ndf2 = df.groupby([\"Image\",\"Subtype\"], as_index=False)[\"Label\"].max()\ndf_pivot = df2.pivot(index=\"Image\", columns=\"Subtype\", values=\"Label\").reset_index().fillna(0)\ndf_pivot[\"Label_binary\"] = df_pivot.iloc[:,1:].max(axis=1).astype(int)\n\n# Balanced sampling\npos = df_pivot[df_pivot[\"Label_binary\"]==1].sample(n=SAMPLES_PER_CLASS, random_state=SEED)\nneg = df_pivot[df_pivot[\"Label_binary\"]==0].sample(n=SAMPLES_PER_CLASS, random_state=SEED)\ndf_bal = pd.concat([pos,neg]).sample(frac=1, random_state=SEED).reset_index(drop=True)\n\n# -------------------- DATASET --------------------\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        self.aug_tf = transforms.Compose([\n            transforms.ToPILImage(),\n            transforms.Resize((IMG_SIZE,IMG_SIZE)),\n            transforms.RandomResizedCrop(IMG_SIZE, scale=(0.85,1.0)),\n            transforms.RandomHorizontalFlip(),\n            transforms.RandomRotation(10),\n            transforms.ToTensor()\n        ])\n        self.eval_tf = transforms.Compose([\n            transforms.ToPILImage(),\n            transforms.Resize((IMG_SIZE,IMG_SIZE)),\n            transforms.ToTensor()\n        ])\n    def __len__(self):\n        return len(self.df)\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_path = os.path.join(self.img_dir, f\"ID_{row.Image}.dcm\")\n        img = make_3ch(img_path)\n        tf = self.aug_tf if self.augment else self.eval_tf\n        img_t = tf(img)\n        label = int(row.Label_binary)\n        return img_t.float(), label, row.Image\n\n# -------------------- SPLIT --------------------\ntrain_df, temp_df = train_test_split(df_bal,test_size=0.3,stratify=df_bal[\"Label_binary\"],random_state=SEED)\nval_df, test_df = train_test_split(temp_df,test_size=0.5,stratify=temp_df[\"Label_binary\"],random_state=SEED)\n\ntrain_ds = RSNADataset(train_df, IMG_DIR, augment=True)\nval_ds = RSNADataset(val_df, IMG_DIR, augment=False)\ntest_ds = RSNADataset(test_df, IMG_DIR, augment=False)\n\ntrain_loader = DataLoader(train_ds, batch_size=BATCH, shuffle=True)\nval_loader = DataLoader(val_ds, batch_size=BATCH, shuffle=False)\ntest_loader = DataLoader(test_ds, batch_size=BATCH, shuffle=False)\n\n# -------------------- BACKBONE --------------------\nresnet = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1)\nembedding_dim = resnet.fc.in_features\nresnet.fc = nn.Identity()\nresnet = resnet.to(DEVICE)\n\n# -------------------- EXTRACT EMBEDDINGS --------------------\ndef extract_embeddings(loader):\n    resnet.eval()\n    embs=[]; labs=[]; ids=[]\n    with torch.no_grad():\n        for imgs, lab, idlist in tqdm(loader):\n            imgs = imgs.to(DEVICE)\n            feat = resnet(imgs)\n            embs.append(feat.cpu().numpy())\n            labs.extend(lab.numpy())\n            ids.extend(idlist)\n    return np.vstack(embs), np.array(labs), ids\n\nX_tr, y_tr, ids_tr = extract_embeddings(train_loader)\nX_val, y_val, ids_val = extract_embeddings(val_loader)\nX_te, y_te, ids_te = extract_embeddings(test_loader)\n\n# PCA dim reduction\nif PCA_DIM is not None and PCA_DIM < X_tr.shape[1]:\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\n# -------------------- CONSTRUCT GRAPH --------------------\nclass ConstructGraph:\n    def __init__(self, df_indexed, subtype_cols, k_neigh=K_NEIGH, kg_dim=KG_EMB_DIM):\n        self.df_indexed = df_indexed\n        self.subtype_cols = subtype_cols\n        self.k_neigh = k_neigh\n        self.kg_dim = kg_dim\n        self.concept_feats = np.random.normal(0,0.01,(len(subtype_cols), kg_dim))\n\n    def build_knn_adj(self,X,k=None):\n        if k is None: k=self.k_neigh\n        sim = cosine_similarity(X); np.fill_diagonal(sim,0)\n        N=sim.shape[0]; A=np.zeros_like(sim)\n        for i in range(N):\n            idx = np.argsort(sim[i])[-k:]\n            A[i, idx] = sim[i, idx]\n        A = np.maximum(A, A.T) + np.eye(N)*1e-6\n        deg = A.sum(1); deg_inv_sqrt=1.0/np.sqrt(deg)\n        return (deg_inv_sqrt[:,None]*A)*deg_inv_sqrt[None,:]\n\n    def build_graph(self,X_img,img_ids):\n        N = X_img.shape[0]; C = self.concept_feats.shape[0]\n        A_ii = self.build_knn_adj(X_img)\n        A_ic = np.zeros((N,C))\n        for i,iid in enumerate(img_ids):\n            if iid in self.df_indexed.index:\n                A_ic[i,:] = self.df_indexed.loc[iid,self.subtype_cols].values\n        A_ci = A_ic.T\n        simc = cosine_similarity(self.concept_feats); np.fill_diagonal(simc,0)\n        A_cc = simc\n        top = np.concatenate([A_ii,A_ic],1)\n        bottom = np.concatenate([A_ci,A_cc],1)\n        A = np.concatenate([top,bottom],0) + np.eye(N+C)*1e-6\n        deg = A.sum(1); deg_inv_sqrt=1.0/np.sqrt(deg)\n        A_norm = (deg_inv_sqrt[:,None]*A)*deg_inv_sqrt[None,:]\n        D_img = X_img.shape[1]; D_con = self.concept_feats.shape[1]\n        if D_con<D_img:\n            con_padded = np.concatenate([self.concept_feats,np.zeros((C,D_img-D_con))],1)\n        else:\n            con_padded = self.concept_feats[:,:D_img]\n        X_all = np.vstack([X_img,con_padded])\n        labels_img = np.array([self.df_indexed.loc[iid,\"Label_binary\"] if iid in self.df_indexed.index else 0 for iid in img_ids])\n        return X_all, A_norm, labels_img\n\ndf_indexed = df_pivot.set_index(\"Image\")\ngraph_builder = ConstructGraph(df_indexed, SUBTYPE_COLS)\nX_all_tr, A_tr, labels_tr = graph_builder.build_graph(X_tr, ids_tr)\nX_all_val, A_val, labels_val = graph_builder.build_graph(X_val, ids_val)\nX_all_te,  A_te,  labels_te  = graph_builder.build_graph(X_te, ids_te)\n\n# Convert to torch\nX_tr_t = torch.tensor(X_all_tr, dtype=torch.float32, device=DEVICE)\nA_tr_t = torch.tensor(A_tr, dtype=torch.float32, device=DEVICE)\ny_tr_img = torch.tensor(labels_tr, dtype=torch.long, device=DEVICE)\nX_val_t = torch.tensor(X_all_val, dtype=torch.float32, device=DEVICE)\nA_val_t = torch.tensor(A_val, dtype=torch.float32, device=DEVICE)\ny_val_img = torch.tensor(labels_val, dtype=torch.long, device=DEVICE)\nX_te_t = torch.tensor(X_all_te, dtype=torch.float32, device=DEVICE)\nA_te_t = torch.tensor(A_te, dtype=torch.float32, device=DEVICE)\nN_img_tr = X_tr.shape[0]; N_img_val = X_val.shape[0]; N_img_te = X_te.shape[0]\n\n# -------------------- BrainGNN (Simple GNN block) --------------------\nclass SimpleGCNBlock(nn.Module):\n    def __init__(self, in_dim, hid_dim):\n        super().__init__()\n        self.lin1 = nn.Linear(in_dim,hid_dim)\n        self.lin2 = nn.Linear(hid_dim,hid_dim)\n        self.dropout = nn.Dropout(0.4)\n        self.bn = nn.LayerNorm(hid_dim)\n    def forward(self,X,A):\n        h = F.relu(self.bn(self.lin1(X)))\n        h = A@h\n        h = self.dropout(h)\n        h = F.relu(self.lin2(h))\n        h = A@h\n        return h\n\nclass BrainGNN(nn.Module):\n    def __init__(self, feat_dim,hid_dim=256,n_classes=2):\n        super().__init__()\n        self.encoder = nn.Linear(feat_dim,hid_dim)\n        self.gcn = SimpleGCNBlock(hid_dim,hid_dim)\n        self.classifier = nn.Linear(hid_dim,n_classes)\n    def forward(self,X,A):\n        H = F.relu(self.encoder(X))\n        H_g = self.gcn(H,A)\n        logits = self.classifier(H_g)\n        return logits\n\n# -------------------- TRAIN --------------------\nmodel = BrainGNN(X_all_tr.shape[1], hid_dim=256).to(DEVICE)\nopt = torch.optim.AdamW(model.parameters(), lr=LR_GNN)\ncrit = nn.CrossEntropyLoss()\n\nfor ep in range(1,EPOCHS+1):\n    model.train()\n    opt.zero_grad()\n    logits_all = model(X_tr_t, A_tr_t)\n    logits_img = logits_all[:N_img_tr]\n    loss = crit(logits_img, y_tr_img)\n    loss.backward(); opt.step()\n    if ep%5==0:\n        print(f\"Epoch {ep}/{EPOCHS} Loss:{loss.item():.4f}\")\n\n# -------------------- FINAL METRICS --------------------\nmodel.eval()\nwith torch.no_grad():\n    logits_te_all = model(X_te_t, A_te_t)\n    preds = torch.argmax(logits_te_all[:N_img_te],dim=1).cpu().numpy()\n    probs = F.softmax(logits_te_all[:N_img_te],dim=1)[:,1].cpu().numpy()\n    acc = accuracy_score(labels_te, preds)\n    prec = precision_score(labels_te, preds, zero_division=0)\n    rec = recall_score(labels_te, preds, zero_division=0)\n    f1s = f1_score(labels_te, preds, zero_division=0)\n    auc = roc_auc_score(labels_te, probs)\n    kappa = cohen_kappa_score(labels_te, preds)\nprint(\"\\n=== FINAL METRICS ===\")\nprint(f\"Accuracy : {acc:.4f}\")\nprint(f\"ROC-AUC  : {auc:.4f}\")\nprint(f\"Precision: {prec:.4f}\")\nprint(f\"Recall   : {rec:.4f}\")\nprint(f\"F1 Score : {f1s:.4f}\")\nprint(f\"Cohen Kappa : {kappa:.4f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-24T23:55:22.693814Z","iopub.execute_input":"2025-11-24T23:55:22.694174Z","iopub.status.idle":"2025-11-24T23:57:55.954811Z","shell.execute_reply.started":"2025-11-24T23:55:22.69415Z","shell.execute_reply":"2025-11-24T23:57:55.953793Z"}},"outputs":[],"execution_count":null}]}