{"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":[{"sourceId":94147,"databundleVersionId":11390004,"sourceType":"competition"}],"dockerImageVersionId":31041,"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-05-14T18:46:40.753926Z","iopub.execute_input":"2025-05-14T18:46:40.754241Z","iopub.status.idle":"2025-05-14T18:46:40.761108Z","shell.execute_reply.started":"2025-05-14T18:46:40.754210Z","shell.execute_reply":"2025-05-14T18:46:40.760247Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"pip install torch_geometric","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-14T18:46:40.762417Z","iopub.execute_input":"2025-05-14T18:46:40.762644Z","iopub.status.idle":"2025-05-14T18:46:43.826321Z","shell.execute_reply.started":"2025-05-14T18:46:40.762628Z","shell.execute_reply":"2025-05-14T18:46:43.825458Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"############################################################\n# 0. Imports, env-var, paths\n############################################################\nimport os, warnings, gc\nos.environ[\"TORCH_ALLOW_DUPLICATE_LIBRARY_REGISTRATIONS\"] = \"1\"\n\nimport h5py, numpy as np, pandas as pd\nfrom tqdm import tqdm\n\nimport torch\nfrom torch import nn\nfrom torch.optim import AdamW\nfrom torch.optim.lr_scheduler import CosineAnnealingLR\nfrom torch.utils.data import Dataset, DataLoader\nimport torchvision.transforms as T\nimport torchvision.models as models\n\n# PyG imports\nfrom sklearn.neighbors import NearestNeighbors\nfrom torch_geometric.data import Data as GeoData, DataLoader as GeoDataLoader\nfrom torch_geometric.nn import SAGEConv\n\nfrom scipy.stats import spearmanr\nwarnings.filterwarnings(\"ignore\")\n\n# configuration\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nH5_PATH = \"/kaggle/input/el-hackathon-2025/elucidata_ai_challenge_data.h5\"\nepochs, batch_size, patch_size = 10, 16, 224\nk_neighbors = 8\nlr = 1e-4\n\n############################################################\n# 1. Spatial patch dataset\n############################################################\nclass SpatialDataset(Dataset):\n    def __init__(self, h5_path, split, slide_ids=None, patch_size=224, transform=None):\n        self.r = patch_size // 2\n        self.transform = transform\n        with h5py.File(h5_path, 'r') as f:\n            img_grp = f[f\"images/{split}\"]\n            keys = slide_ids or list(img_grp.keys())\n            self.images = {sid: img_grp[sid][:] for sid in keys}\n            spot_grp = f[f\"spots/{split}\"]\n            self.spots  = {sid: spot_grp[sid][:] for sid in keys}\n        sample = next(iter(self.spots.values()))\n        self.has_label = 'C1' in sample.dtype.names\n        self.index = [(sid, i)\n                      for sid, arr in self.spots.items()\n                      for i in range(arr.shape[0])]\n\n    def __len__(self):\n        return len(self.index)\n\n    def __getitem__(self, idx):\n        sid, i = self.index[idx]\n        img = self.images[sid]\n        rec = self.spots[sid]\n        x, y = int(rec['x'][i]), int(rec['y'][i])\n        r, H, W = self.r, *img.shape[:2]\n        patch = img[max(0, y-r):min(H, y+r), max(0, x-r):min(W, x+r), :]\n        pad = [(0,0),(0,0),(0,0)]\n        if patch.shape[0] < 2*r:\n            d = 2*r - patch.shape[0]\n            pad[0] = (d//2, d-d//2)\n        if patch.shape[1] < 2*r:\n            d = 2*r - patch.shape[1]\n            pad[1] = (d//2, d-d//2)\n        if any(p != (0,0) for p in pad[:2]):\n            patch = np.pad(patch, pad, mode='reflect')\n        patch = torch.from_numpy(patch.transpose(2,0,1)).float().div(255)\n        if self.transform:\n            patch = self.transform(patch)\n        label = torch.zeros(35, dtype=torch.float32)\n        if self.has_label:\n            label = torch.tensor([rec[f'C{k}'][i] for k in range(1,36)], dtype=torch.float32)\n        coord = torch.tensor([x, y], dtype=torch.float32)\n        return patch, label, coord\n\n# transforms\ntrain_tf = T.Compose([\n    T.RandomHorizontalFlip(),\n    T.RandomVerticalFlip(),\n    T.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])\n])\nval_tf = T.Compose([\n    T.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])\n])\n\n############################################################\n# 2. CNN feature extractor using ResNet101\n############################################################\nclass CNNBackbone(nn.Module):\n    def __init__(self):\n        super().__init__()\n        base = models.resnet101(weights=models.ResNet101_Weights.IMAGENET1K_V1)\n        self.encoder = nn.Sequential(*list(base.children())[:-1])\n\n    def forward(self, x):\n        x = self.encoder(x)\n        return x.view(x.size(0), -1)  # outputs (B,2048)\n\n############################################################\n# 3. GraphDataset builder\n############################################################\nclass GraphDataset(Dataset):\n    def __init__(self, h5_path, split, slide_ids, transform):\n        self.h5_path = h5_path\n        self.split   = split\n        self.slide_ids = slide_ids\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.slide_ids)\n\n    def __getitem__(self, idx):\n        sid = self.slide_ids[idx]\n        ds = SpatialDataset(self.h5_path, self.split, [sid], patch_size, self.transform)\n        loader = DataLoader(ds, batch_size=batch_size, shuffle=False)\n        feats, labels, coords = [], [], []\n        for xb, yb, coord in loader:\n            xb = xb.to(device)\n            with torch.no_grad():\n                feats.append(cnn(xb).cpu())\n            labels.append(yb)\n            coords.append(coord)\n        x = torch.cat(feats, dim=0)\n        y = torch.cat(labels, dim=0)\n        coord_np = torch.cat(coords, dim=0).numpy()\n        nbrs = NearestNeighbors(n_neighbors=k_neighbors+1).fit(coord_np)\n        _, idxs = nbrs.kneighbors(coord_np)\n        edges = []\n        for src, nbr in enumerate(idxs):\n            for dst in nbr[1:]:\n                edges += [[src, dst], [dst, src]]\n        edge_index = torch.tensor(edges, dtype=torch.long).t().contiguous()\n        return GeoData(x=x, edge_index=edge_index, y=y)\n\n############################################################\n# 4. GraphSAGE-based GNN\n############################################################\nclass GraphCellFracNet(nn.Module):\n    def __init__(self, in_dim=2048, hidden=256, out_dim=35, num_layers=3, dropout=0.2):\n        super().__init__()\n        self.convs = nn.ModuleList()\n        self.bns   = nn.ModuleList()\n        # first layer\n        self.convs.append(SAGEConv(in_dim, hidden))\n        self.bns.append(nn.BatchNorm1d(hidden))\n        # intermediate layers\n        for _ in range(num_layers-2):\n            self.convs.append(SAGEConv(hidden, hidden))\n            self.bns.append(nn.BatchNorm1d(hidden))\n        # final\n        self.convs.append(SAGEConv(hidden, out_dim))\n        self.dropout = nn.Dropout(dropout)\n\n    def forward(self, data):\n        x, edge_index = data.x.to(device), data.edge_index.to(device)\n        for conv, bn in zip(self.convs[:-1], self.bns):\n            x = conv(x, edge_index)\n            x = bn(x)\n            x = torch.relu(x)\n            x = self.dropout(x)\n        x = self.convs[-1](x, edge_index)\n        return x  # logits\n\n############################################################\n# 5. Instantiate models, optimizer, scheduler, loss\n############################################################\ncnn       = CNNBackbone().to(device)\ngnn       = GraphCellFracNet().to(device)\noptimizer = AdamW(list(cnn.parameters())+list(gnn.parameters()), lr=lr, weight_decay=1e-5)\nscheduler = CosineAnnealingLR(optimizer, T_max=epochs)\nloss_fn   = nn.MSELoss()\n\n############################################################\n# 6. Prepare data loaders\n############################################################\ntrain_ids = [f\"S_{i}\" for i in range(1,6)]\nval_ids   = [\"S_6\"]\ntest_ids  = [\"S_7\"]\ntrain_ds  = GraphDataset(H5_PATH, 'Train', train_ids, train_tf)\nval_ds    = GraphDataset(H5_PATH, 'Train', val_ids,   val_tf)\ntest_ds   = GraphDataset(H5_PATH, 'Test',  test_ids,  val_tf)\ntrain_loader = GeoDataLoader(train_ds, batch_size=1, shuffle=True)\nval_loader   = GeoDataLoader(val_ds,   batch_size=1, shuffle=False)\n############################################################\n# 7. Training and validation loop\n############################################################\nbest_sp = -1.0\nfor ep in range(1, epochs+1):\n    cnn.train(); gnn.train()\n    total_loss = 0.0\n    for data in tqdm(train_loader, desc=f\"Epoch {ep} train\"):\n        optimizer.zero_grad()\n        logits = gnn(data)\n        loss = loss_fn(torch.softmax(logits, dim=1), data.y.to(device))\n        loss.backward(); optimizer.step()\n        total_loss += loss.item()\n    scheduler.step()\n\n    cnn.eval(); gnn.eval()\n    sp_list = []\n    with torch.no_grad():\n        for data in val_loader:\n            logits = gnn(data)\n            preds  = torch.softmax(logits, dim=1).cpu().numpy()\n            true   = data.y.numpy()\n            for i in range(preds.shape[0]):\n                sp_list.append(spearmanr(preds[i], true[i]).correlation)\n    val_sp = np.nanmean(sp_list)\n    print(f\"Epoch {ep}/{epochs} - Loss: {total_loss/len(train_loader):.4f} | Val SP: {val_sp:.4f} (best {best_sp:.4f})\")\n    if val_sp > best_sp:\n        best_sp = val_sp\n        torch.save({'cnn': cnn.state_dict(), 'gnn': gnn.state_dict()}, 'best_resnet101.pth')\n\n############################################################\n# 8. Inference and submission\n############################################################\nckpt = torch.load('best_resnet101.pth', map_location=device)\ncnn.load_state_dict(ckpt['cnn']); gnn.load_state_dict(ckpt['gnn'])\ncnn.eval(); gnn.eval()\nall_preds = []\nwith torch.no_grad():\n    for data in GeoDataLoader(test_ds, batch_size=1, shuffle=False):\n        logits = gnn(data)\n        all_preds.append(torch.softmax(logits, dim=1).cpu().numpy())\n\ntest_preds = np.vstack(all_preds)\nwith h5py.File(H5_PATH,'r') as f:\n    spots = np.array(f['spots/Test']['S_7'])\nidx_df = pd.DataFrame(spots).reset_index()\nsub = pd.DataFrame(test_preds, columns=[f\"C{i+1}\" for i in range(35)])\nsub.insert(0, 'ID', idx_df['index'])\nsub.to_csv('submission_resnet101.csv', index=False)\nprint('✅ submission_resnet101.csv created', sub.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-14T18:59:50.256423Z","iopub.execute_input":"2025-05-14T18:59:50.256701Z"}},"outputs":[],"execution_count":null}]}