{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.12","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":30919,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# Install dependencies\n!pip install monai\n\nimport h5py\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nimport torchvision.transforms as transforms\nfrom torch.utils.data import Dataset, DataLoader\nfrom monai.networks.nets import UNet\nfrom scipy.stats import spearmanr\nimport random\n\n# Set Seed\ndef set_seed(seed=42):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n\nset_seed(42)\n\n# Load Data\nwith h5py.File('/kaggle/input/el-hackathon-2025/elucidata_ai_challenge_data.h5', 'r') as f:\n    train_spots = {k: pd.DataFrame(np.array(f[f'spots/Train/{k}'])) for k in f['spots/Train'].keys()}\n    train_images = {k: np.array(f[f'images/Train/{k}']) for k in f['images/Train'].keys()}\n    test_spots = {k: pd.DataFrame(np.array(f[f'spots/Test/{k}'])) for k in f['spots/Test'].keys()}\n    test_images = {k: np.array(f[f'images/Test/{k}']) for k in f['images/Test'].keys()}\n\n# Preprocessing\ntransform = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.Normalize(mean=[0.5], std=[0.5])\n])\n\n# Dataset Class\nclass TissueDataset(Dataset):\n    def __init__(self, spots, images, transform=None, patch_size=55):\n        self.spots = spots\n        self.images = images\n        self.transform = transform\n        self.patch_size = patch_size\n    \n    def __len__(self):\n        return sum(len(v) for v in self.spots.values())\n    \n    def __getitem__(self, idx):\n        slide_ids = list(self.spots.keys())\n        slide_idx = idx // len(self.spots[slide_ids[0]])\n        spot_idx = idx % len(self.spots[slide_ids[slide_idx]])\n\n        slide_id = slide_ids[slide_idx]\n        spot = self.spots[slide_id].iloc[spot_idx]\n        \n        x, y = int(spot['x']), int(spot['y'])\n        label = torch.tensor(spot.iloc[2:].values, dtype=torch.float32)\n\n        # Extract the patch around the spot\n        half_patch = self.patch_size // 2\n        img_h, img_w, _ = self.images[slide_id].shape  # Image dimensions\n\n        # Adjust patch bounds while ensuring valid patch size\n        x1, x2 = max(0, x - half_patch), min(img_w, x + half_patch + 1)\n        y1, y2 = max(0, y - half_patch), min(img_h, y + half_patch + 1)\n\n        # Ensure patch has minimum valid dimensions\n        if x2 - x1 < 1:\n            x2 = min(img_w, x1 + 1)  # Ensure at least 1 pixel width\n        if y2 - y1 < 1:\n            y2 = min(img_h, y1 + 1)  # Ensure at least 1 pixel height\n\n        image = self.images[slide_id][y1:y2, x1:x2, :]\n\n        # Convert to tensor and permute dimensions\n        image = torch.tensor(image, dtype=torch.float32).permute(2, 0, 1)\n\n        if self.transform:\n            image = self.transform(image)\n\n        return image, label\n\n\n\n# 2D U-Net Model\nclass UNet2DPredictor(nn.Module):\n    def __init__(self, num_classes=35):\n        super(UNet2DPredictor, self).__init__()\n        self.unet2d = UNet(\n            spatial_dims=2,\n            in_channels=3,\n            out_channels=num_classes,\n            channels=(16, 32, 64, 128, 256),\n            strides=(2, 2, 2, 2),\n            num_res_units=2,\n        )\n    \n    def forward(self, x):\n        x = self.unet2d(x)\n        return x.mean(dim=(2, 3))  # Global average pooling\n\n# Loss Function\nclass HybridLoss(nn.Module):\n    def __init__(self, alpha=0.5):\n        super(HybridLoss, self).__init__()\n        self.mse = nn.MSELoss()\n        self.alpha = alpha\n    \n    def forward(self, preds, targets):\n        mse_loss = self.mse(preds, targets)\n        rank_diffs = preds.unsqueeze(2) - preds.unsqueeze(1)\n        target_diffs = targets.unsqueeze(2) - targets.unsqueeze(1)\n        pl_loss = torch.log(1 + torch.exp(-rank_diffs * target_diffs)).mean()\n        return self.alpha * mse_loss + (1 - self.alpha) * pl_loss\n\n# Training Setup\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\ncriterion = HybridLoss(alpha=0)\n\n# Train & Evaluate Function\ndef train_epoch(model, dataloader, optimizer, criterion):\n    model.train()\n    for images, labels in dataloader:\n        images, labels = images.to(device), labels.to(device)\n        optimizer.zero_grad()\n        outputs = model(images)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n\ndef evaluate(model, dataloader):\n    model.eval()\n    predictions, truths = [], []\n    with torch.no_grad():\n        for images, labels in dataloader:\n            images = images.to(device)\n            preds = model(images).cpu().numpy()\n            predictions.extend(preds)\n            truths.extend(labels.numpy())\n    return np.mean([spearmanr(p, t).correlation for p, t in zip(predictions, truths)])\n\n# Load data for cross-validation\nlast_slide = list(train_spots.keys())[-1]\ntrain_spots_cv = {k: v for k, v in train_spots.items() if k != last_slide}\nval_spots_cv = {last_slide: train_spots[last_slide]}\ntrain_images_cv = {k: v for k, v in train_images.items() if k != last_slide}\nval_images_cv = {last_slide: train_images[last_slide]}\n\ntrain_dataset = TissueDataset(train_spots_cv, train_images_cv, transform, patch_size=1)  # Starting with 1 pixel\nval_dataset = TissueDataset(val_spots_cv, val_images_cv, transform, patch_size=1)  \ntrain_loader = DataLoader(train_dataset, batch_size=8, shuffle=True)\nval_loader = DataLoader(val_dataset, batch_size=8, shuffle=False)\n\n# Model Training with Patch Size Variations\npatch_sizes = [45]\n\nfor patch_size in patch_sizes:\n    print(f\"Training model with patch size {patch_size} pixels.\")\n    \n    # Create a new model for each patch size\n    model = UNet2DPredictor().to(device)\n    optimizer = optim.Adam(model.parameters(), lr=1e-4)\n    \n    # Create new dataset with the updated patch size\n    train_dataset = TissueDataset(train_spots_cv, train_images_cv, transform, patch_size=patch_size)\n    val_dataset = TissueDataset(val_spots_cv, val_images_cv, transform, patch_size=patch_size)\n    train_loader = DataLoader(train_dataset, batch_size=8, shuffle=True)\n    val_loader = DataLoader(val_dataset, batch_size=8, shuffle=False)\n    \n    # Train the model\n    best_score = -np.inf\n    patience, patience_counter = 10, 0\n    for epoch in range(100):\n        train_epoch(model, train_loader, optimizer, criterion)\n        score = evaluate(model, val_loader)\n        print(f\"Epoch {epoch+1}, Validation Spearman: {score:.4f}\")\n        \n        if score > best_score:\n            best_score = score\n            patience_counter = 0\n            torch.save(model.state_dict(), f\"best_model_patch_{patch_size}.pth\")\n        else:\n            patience_counter += 1\n        \n        if patience_counter >= patience:\n            print(\"Early stopping triggered.\")\n            break\n    \n    # Load Best Model for Inference\n    model.load_state_dict(torch.load(f\"best_model_patch_{patch_size}.pth\"))\n    \n    # Inference and Save Submission\n    model.eval()\n    test_dataset = TissueDataset(test_spots, test_images, transform, patch_size=patch_size)\n    test_loader = DataLoader(test_dataset, batch_size=8, shuffle=False)\n    predictions = []\n    \n    with torch.no_grad():\n        for images, _ in test_loader:\n            images = images.to(device)\n            preds = model(images).cpu().numpy()\n            predictions.extend(preds)\n    \n    # Save Submission File\n    submission = pd.DataFrame(predictions, columns=[f'C{i+1}' for i in range(35)])\n    submission.insert(0, 'ID', range(len(submission)))\n    submission.to_csv(f'submission_patch_{patch_size}.csv', index=False)\n    print(f\"Submission for patch size {patch_size} saved as submission_patch_{patch_size}.csv\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-01T17:20:06.81323Z","iopub.execute_input":"2025-04-01T17:20:06.813561Z","iopub.status.idle":"2025-04-01T17:21:26.414073Z","shell.execute_reply.started":"2025-04-01T17:20:06.813535Z","shell.execute_reply":"2025-04-01T17:21:26.411682Z"}},"outputs":[],"execution_count":null}]}