{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"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\n\n# Use the kagglehub client library to attach Kaggle resources like competitions, datasets, and models to your session\n# Learn more about kagglehub: https://github.com/Kaggle/kagglehub/blob/main/README.md\n\nimport kagglehub\n# kagglehub.dataset_download('<owner>/<dataset-slug>')","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-08-23T11:29:06.142562Z","iopub.execute_input":"2026-08-23T11:29:06.142861Z","execution_failed":"2026-08-23T11:32:56.395Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pydicom\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport torchvision.models as models","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-23T11:32:56.395Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\nprint(os.listdir('/kaggle/input/'))","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-23T11:32:56.395Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df = pd.read_csv('/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv')\nprint(f\"Total Studies: {len(train_df)}\")\ntrain_df.head()","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-23T11:32:56.396Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\n\n# Base directory\nDATA_DIR = '/kaggle/input/competitions/rsna-knee-abnormality-detection'\n\n# Load metadata\ntrain_df = pd.read_csv(f'{DATA_DIR}/train.csv')\ntrain_series_df = pd.read_csv(f'{DATA_DIR}/train_series.csv')\n\nprint(f\"Total Training Studies: {len(train_df)}\")\nprint(\"\\n--- Target Label Distribution ---\")\nlabel_cols = [c for c in train_df.columns if c != 'StudyInstanceUID']\nprint(train_df[label_cols].sum())\n\ntrain_df.head()","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-23T11:32:56.396Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import glob\nimport pydicom\nimport torch\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\n\nclass RSNARadiologyDataset(Dataset):\n    def __init__(self, df, base_dir, num_slices=5, target_size=(224, 224)):\n        self.df = df.reset_index(drop=True)\n        self.base_dir = base_dir\n        self.num_slices = num_slices\n        self.target_size = target_size\n        self.label_cols = df.select_dtypes(include=[np.number]).columns.tolist()\n\n    def __len__(self):\n        return len(self.df)\n\n    def _load_dicom_series(self, series_path):\n        dcm_files = glob.glob(os.path.join(series_path, \"*.dcm\"))\n        \n        # Fallback dummy tensor if no slices are found\n        if not dcm_files:\n            return torch.zeros((self.num_slices, *self.target_size), dtype=torch.float32)\n            \n        slices = [pydicom.dcmread(f) for f in dcm_files]\n        slices.sort(key=lambda x: int(getattr(x, 'InstanceNumber', 0)))\n        \n        volume = np.stack([s.pixel_array for s in slices])\n        \n        # Uniformly pick 'num_slices' across the sequence\n        indices = np.linspace(0, volume.shape[0] - 1, self.num_slices, dtype=int)\n        selected_volume = volume[indices]\n        \n        # Min-Max Normalization to [0, 1] range\n        v_min, v_max = selected_volume.min(), selected_volume.max()\n        if v_max > v_min:\n            selected_volume = (selected_volume - v_min) / (v_max - v_min)\n        else:\n            selected_volume = np.zeros_like(selected_volume)\n            \n        # Convert to tensor: Shape (num_slices, H, W)\n        tensor_vol = torch.tensor(selected_volume, dtype=torch.float32)\n        \n        # Add channel dimension for interpolation: (1, num_slices, H, W) -> Resize to target_size\n        tensor_vol = tensor_vol.unsqueeze(0)\n        resized_vol = F.interpolate(tensor_vol, size=self.target_size, mode='bilinear', align_corners=False)\n        \n        # Remove batch dimension: Shape becomes (num_slices, 224, 224)\n        return resized_vol.squeeze(0)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        study_uid = row['StudyInstanceUID']\n        \n        series_dir = os.path.join(self.base_dir, 'train_series', str(study_uid))\n        subfolders = [f.path for f in os.scandir(series_dir) if f.is_dir()] if os.path.exists(series_dir) else []\n        target_path = subfolders[0] if subfolders else series_dir\n        \n        image_tensor = self._load_dicom_series(target_path)\n        labels = torch.tensor(row[self.label_cols].values.astype(np.float32))\n        \n        return image_tensor, labels","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-23T11:32:56.396Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\ntrain_dataset = RSNARadiologyDataset(train_df, DATA_DIR, num_slices=5, target_size=(224, 224))\ntrain_loader = DataLoader(train_dataset, batch_size=4, shuffle=True)\n\nnum_targets = len(train_dataset.label_cols)\nmodel = MultiSliceResNet(num_classes=num_targets, in_channels=5).to(device)\n\nsample_images, sample_labels = next(iter(train_loader))\nsample_images, sample_labels = sample_images.to(device), sample_labels.to(device)\n\noutputs = model(sample_images)\n\nprint(f\"Batch Image Shape: {sample_images.shape}\")  # [4, 5, 224, 224]\nprint(f\"Batch Target Shape: {sample_labels.shape}\") # [4, num_targets]\nprint(f\"Model Output Shape: {outputs.shape}\")       # [4, num_targets]\nprint(\"\\nPipeline test passed successfully!\")","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-23T11:32:56.396Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.model_selection import GroupKFold\n\n# Create a 5-Fold split\ngkf = GroupKFold(n_splits=5)\ntrain_df['fold'] = -1\n\nfor fold, (train_idx, val_idx) in enumerate(gkf.split(train_df, groups=train_df['StudyInstanceUID'])):\n    train_df.loc[val_idx, 'fold'] = fold\n\n# Select Fold 0 for our initial experiment\nfold_0_train = train_df[train_df['fold'] != 0].reset_index(drop=True)\nfold_0_val = train_df[train_df['fold'] == 0].reset_index(drop=True)\n\nprint(f\"Train samples: {len(fold_0_train)}, Validation samples: {len(fold_0_val)}\")","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-23T11:32:56.396Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import roc_auc_score\n\n# 1. Datasets and DataLoaders\ntrain_ds = RSNARadiologyDataset(fold_0_train, DATA_DIR, num_slices=5)\nval_ds = RSNARadiologyDataset(fold_0_val, DATA_DIR, num_slices=5)\n\ntrain_loader = DataLoader(train_ds, batch_size=8, shuffle=True, num_workers=2)\nval_loader = DataLoader(val_ds, batch_size=8, shuffle=False, num_workers=2)\n\n# 2. Model, Criterion, Optimizer\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nmodel = MultiSliceResNet(num_classes=len(train_ds.label_cols), in_channels=5).to(device)\n\ncriterion = nn.BCEWithLogitsLoss()\noptimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-2)\n\n# 3. Training Function\ndef train_one_epoch(model, dataloader, criterion, optimizer, device):\n    model.train()\n    running_loss = 0.0\n    for images, labels in dataloader:\n        images, labels = images.to(device), labels.to(device)\n        \n        optimizer.zero_grad()\n        outputs = model(images)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n        \n        running_loss += loss.item() * images.size(0)\n    return running_loss / len(dataloader.dataset)\n\n# 4. Validation Function with AUC-ROC Score\ndef validate(model, dataloader, criterion, device):\n    model.eval()\n    running_loss = 0.0\n    all_preds, all_labels = [], []\n    \n    with torch.no_grad():\n        for images, labels in dataloader:\n            images, labels = images.to(device), labels.to(device)\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n            \n            running_loss += loss.item() * images.size(0)\n            \n            # Apply sigmoid to outputs for probabilities\n            preds = torch.sigmoid(outputs)\n            all_preds.append(preds.cpu().numpy())\n            all_labels.append(labels.cpu().numpy())\n            \n    val_loss = running_loss / len(dataloader.dataset)\n    all_preds = np.vstack(all_preds)\n    all_labels = np.vstack(all_labels)\n    \n    # Calculate Mean Macro AUC-ROC across target classes\n    auc_scores = []\n    for i in range(all_labels.shape[1]):\n        # Calculate AUC only if class has both 0 and 1 instances in validation set\n        if len(np.unique(all_labels[:, i])) > 1:\n            auc_scores.append(roc_auc_score(all_labels[:, i], all_preds[:, i]))\n            \n    mean_auc = np.mean(auc_scores) if auc_scores else 0.5\n    return val_loss, mean_auc","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-23T11:32:56.396Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 1. Replace any NaN target values in train_df with 0\nlabel_cols = train_df.select_dtypes(include=[np.number]).columns\ntrain_df[label_cols] = train_df[label_cols].fillna(0)\n\n# 2. Re-create train/validation datasets with cleaned labels\nfold_0_train = train_df[train_df['fold'] != 0].reset_index(drop=True)\nfold_0_val = train_df[train_df['fold'] == 0].reset_index(drop=True)\n\ntrain_ds = RSNARadiologyDataset(fold_0_train, DATA_DIR, num_slices=5, target_size=(224, 224))\nval_ds = RSNARadiologyDataset(fold_0_val, DATA_DIR, num_slices=5, target_size=(224, 224))\n\ntrain_loader = DataLoader(train_ds, batch_size=8, shuffle=True, num_workers=2)\nval_loader = DataLoader(val_ds, batch_size=8, shuffle=False, num_workers=2)","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-23T11:32:56.396Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def validate(model, dataloader, criterion, device):\n    model.eval()\n    running_loss = 0.0\n    all_preds, all_labels = [], []\n    \n    with torch.no_grad():\n        for images, labels in dataloader:\n            images, labels = images.to(device), labels.to(device)\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n            \n            running_loss += loss.item() * images.size(0)\n            \n            preds = torch.sigmoid(outputs)\n            all_preds.append(preds.cpu().numpy())\n            all_labels.append(labels.cpu().numpy())\n            \n    val_loss = running_loss / len(dataloader.dataset)\n    all_preds = np.vstack(all_preds)\n    all_labels = np.vstack(all_labels)\n    \n    auc_scores = []\n    for i in range(all_labels.shape[1]):\n        y_true = all_labels[:, i]\n        y_pred = all_preds[:, i]\n        \n        # Remove NaNs if any remain\n        valid_mask = ~np.isnan(y_true)\n        y_true_clean = y_true[valid_mask]\n        y_pred_clean = y_pred[valid_mask]\n        \n        # Check if clean targets contain both 0 and 1 classes\n        if len(np.unique(y_true_clean)) > 1:\n            auc_scores.append(roc_auc_score(y_true_clean, y_pred_clean))\n            \n    mean_auc = np.mean(auc_scores) if auc_scores else 0.5\n    return val_loss, mean_auc","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-23T11:32:56.396Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"EPOCHS = 3\n\nfor epoch in range(EPOCHS):\n    train_loss = train_one_epoch(model, train_loader, criterion, optimizer, device)\n    val_loss, val_auc = validate(model, val_loader, criterion, device)\n    \n    print(f\"Epoch {epoch+1}/{EPOCHS} | Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f} | Val AUC: {val_auc:.4f}\")\n\n# Save the trained weights\ntorch.save(model.state_dict(), 'resnet18_baseline.pth')\nprint(\"Model checkpoint saved to resnet18_baseline.pth\")","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-23T11:32:56.396Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport glob\nimport pydicom\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nimport torchvision.models as models\nfrom sklearn.metrics import roc_auc_score\nfrom sklearn.model_selection import GroupKFold\n\n# ---------------------------------------------------------\n# 1. Base Setup & Data Cleaning\n# ---------------------------------------------------------\nDATA_DIR = '/kaggle/input/competitions/rsna-knee-abnormality-detection'\ntrain_df = pd.read_csv(f'{DATA_DIR}/train.csv')\n\n# Handle missing target values in train.csv\nlabel_cols = train_df.select_dtypes(include=[np.number]).columns.tolist()\ntrain_df[label_cols] = train_df[label_cols].fillna(0.0)\n\n# Create 5-Fold Split\ngkf = GroupKFold(n_splits=5)\ntrain_df['fold'] = -1\nfor fold, (train_idx, val_idx) in enumerate(gkf.split(train_df, groups=train_df['StudyInstanceUID'])):\n    train_df.loc[val_idx, 'fold'] = fold\n\nfold_0_train = train_df[train_df['fold'] != 0].reset_index(drop=True)\nfold_0_val = train_df[train_df['fold'] == 0].reset_index(drop=True)\n\n# ---------------------------------------------------------\n# 2. Safe Dataset Class\n# ---------------------------------------------------------\nclass RSNARadiologyDataset(Dataset):\n    def __init__(self, df, base_dir, num_slices=5, target_size=(224, 224)):\n        self.df = df.reset_index(drop=True)\n        self.base_dir = base_dir\n        self.num_slices = num_slices\n        self.target_size = target_size\n        self.label_cols = label_cols\n\n    def __len__(self):\n        return len(self.df)\n\n    def _load_dicom_series(self, series_path):\n        dcm_files = glob.glob(os.path.join(series_path, \"*.dcm\"))\n        if not dcm_files:\n            return torch.zeros((self.num_slices, *self.target_size), dtype=torch.float32)\n            \n        try:\n            slices = [pydicom.dcmread(f) for f in dcm_files]\n            slices.sort(key=lambda x: int(getattr(x, 'InstanceNumber', 0)))\n            \n            pixel_arrays = [s.pixel_array.astype(np.float32) for s in slices if hasattr(s, 'pixel_array')]\n            if not pixel_arrays:\n                return torch.zeros((self.num_slices, *self.target_size), dtype=torch.float32)\n                \n            volume = np.stack(pixel_arrays)\n            indices = np.linspace(0, volume.shape[0] - 1, self.num_slices, dtype=int)\n            selected_volume = volume[indices]\n            \n            # Division by zero safety check\n            v_min, v_max = selected_volume.min(), selected_volume.max()\n            if v_max > v_min:\n                selected_volume = (selected_volume - v_min) / (v_max - v_min + 1e-6)\n            else:\n                selected_volume = np.zeros_like(selected_volume)\n                \n            tensor_vol = torch.nan_to_num(torch.tensor(selected_volume, dtype=torch.float32), nan=0.0)\n            tensor_vol = tensor_vol.unsqueeze(0)\n            resized_vol = F.interpolate(tensor_vol, size=self.target_size, mode='bilinear', align_corners=False)\n            return resized_vol.squeeze(0)\n            \n        except Exception:\n            return torch.zeros((self.num_slices, *self.target_size), dtype=torch.float32)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        study_uid = row['StudyInstanceUID']\n        \n        series_dir = os.path.join(self.base_dir, 'train_series', str(study_uid))\n        subfolders = [f.path for f in os.scandir(series_dir) if f.is_dir()] if os.path.exists(series_dir) else []\n        target_path = subfolders[0] if subfolders else series_dir\n        \n        image_tensor = self._load_dicom_series(target_path)\n        raw_labels = row[self.label_cols].values.astype(np.float32)\n        labels = torch.nan_to_num(torch.tensor(raw_labels), nan=0.0)\n        \n        return image_tensor, labels\n\n# ---------------------------------------------------------\n# 3. Neural Network Architecture\n# ---------------------------------------------------------\nclass MultiSliceResNet(nn.Module):\n    def __init__(self, num_classes, in_channels=5):\n        super(MultiSliceResNet, self).__init__()\n        self.model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)\n        self.model.conv1 = nn.Conv2d(in_channels, 64, kernel_size=7, stride=2, padding=3, bias=False)\n        num_features = self.model.fc.in_features\n        self.model.fc = nn.Linear(num_features, num_classes)\n\n    def forward(self, x):\n        return self.model(x)\n\n# ---------------------------------------------------------\n# 4. Training & Validation Functions\n# ---------------------------------------------------------\ndef train_one_epoch(model, dataloader, criterion, optimizer, device):\n    model.train()\n    running_loss = 0.0\n    for images, labels in dataloader:\n        images, labels = images.to(device), labels.to(device)\n        \n        optimizer.zero_grad()\n        outputs = model(images)\n        loss = criterion(outputs, labels)\n        \n        if torch.isnan(loss):\n            continue\n            \n        loss.backward()\n        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)\n        optimizer.step()\n        \n        running_loss += loss.item() * images.size(0)\n        \n    return running_loss / len(dataloader.dataset)\n\ndef validate(model, dataloader, criterion, device):\n    model.eval()\n    running_loss = 0.0\n    all_preds, all_labels = [], []\n    \n    with torch.no_grad():\n        for images, labels in dataloader:\n            images, labels = images.to(device), labels.to(device)\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n            \n            if not torch.isnan(loss):\n                running_loss += loss.item() * images.size(0)\n            \n            preds = torch.sigmoid(outputs)\n            preds = torch.nan_to_num(preds, nan=0.5)\n            labels = torch.nan_to_num(labels, nan=0.0)\n            \n            all_preds.append(preds.cpu().numpy())\n            all_labels.append(labels.cpu().numpy())\n            \n    val_loss = running_loss / len(dataloader.dataset)\n    all_preds = np.vstack(all_preds)\n    all_labels = np.vstack(all_labels)\n    \n    auc_scores = []\n    for i in range(all_labels.shape[1]):\n        y_true = np.nan_to_num(all_labels[:, i], nan=0.0)\n        y_pred = np.nan_to_num(all_preds[:, i], nan=0.5)\n        \n        if len(np.unique(y_true)) > 1:\n            auc_scores.append(roc_auc_score(y_true, y_pred))\n            \n    mean_auc = np.mean(auc_scores) if auc_scores else 0.5\n    return val_loss, mean_auc\n\n# ---------------------------------------------------------\n# 5. Execution Pipeline\n# ---------------------------------------------------------\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\ntrain_ds = RSNARadiologyDataset(fold_0_train, DATA_DIR, num_slices=5)\nval_ds = RSNARadiologyDataset(fold_0_val, DATA_DIR, num_slices=5)\n\ntrain_loader = DataLoader(train_ds, batch_size=8, shuffle=True, num_workers=2)\nval_loader = DataLoader(val_ds, batch_size=8, shuffle=False, num_workers=2)\n\nmodel = MultiSliceResNet(num_classes=len(label_cols), in_channels=5).to(device)\ncriterion = nn.BCEWithLogitsLoss()\noptimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-2)\n\nprint(\"Starting training process...\")\nEPOCHS = 3\nfor epoch in range(EPOCHS):\n    train_loss = train_one_epoch(model, train_loader, criterion, optimizer, device)\n    val_loss, val_auc = validate(model, val_loader, criterion, device)\n    print(f\"Epoch {epoch+1}/{EPOCHS} | Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f} | Val AUC: {val_auc:.4f}\")\n\ntorch.save(model.state_dict(), 'resnet18_baseline.pth')\nprint(\"Model checkpoint saved to resnet18_baseline.pth\")","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-23T11:32:56.396Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport torch\nfrom torch.utils.data import DataLoader\n\n# 1. Load Test DataFrame\ntest_df = pd.read_csv(f'{DATA_DIR}/test.csv')\n\n# 2. Add dummy target columns to test_df so Dataset class doesn't crash\nfor col in label_cols:\n    if col not in test_df.columns:\n        test_df[col] = 0.0\n\n# 3. Build Test Dataset and DataLoader\ntest_ds = RSNARadiologyDataset(test_df, DATA_DIR, num_slices=5, target_size=(224, 224))\ntest_loader = DataLoader(test_ds, batch_size=8, shuffle=False, num_workers=2)\n\n# 4. Load Saved ResNet-18 Model Weights\nmodel = MultiSliceResNet(num_classes=len(label_cols), in_channels=5).to(device)\nmodel.load_state_dict(torch.load('resnet18_baseline.pth'))\nmodel.eval()\n\ntest_preds = []\nprint(\"Generating baseline predictions on test set...\")\n\nwith torch.no_grad():\n    for images, _ in test_loader:\n        images = images.to(device)\n        outputs = model(images)\n        preds = torch.sigmoid(outputs).cpu().numpy()\n        test_preds.append(preds)\n\ntest_preds = np.vstack(test_preds)\n\n# 5. Assign Predictions to Label Columns\ntest_df[label_cols] = test_preds\n\n# 6. Save Official submission.csv\nsubmission_cols = ['StudyInstanceUID'] + label_cols\nsubmission_df = test_df[submission_cols]\nsubmission_df.to_csv('submission.csv', index=False)\n\nprint(\"SUCCESS: submission.csv successfully generated!\")\nsubmission_df.head()","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-23T11:32:56.396Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install albumentations timm -q","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-23T11:32:56.396Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport glob\nimport pydicom\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nimport albumentations as A\nimport timm\nfrom sklearn.metrics import roc_auc_score\nfrom sklearn.model_selection import GroupKFold\n\n# ---------------------------------------------------------\n# 1. Training & Validation Engine\n# ---------------------------------------------------------\ndef train_one_epoch(model, dataloader, criterion, optimizer, device):\n    model.train()\n    running_loss = 0.0\n    for images, labels in dataloader:\n        images, labels = images.to(device), labels.to(device)\n        \n        optimizer.zero_grad()\n        outputs = model(images)\n        loss = criterion(outputs, labels)\n        \n        if torch.isnan(loss):\n            continue\n            \n        loss.backward()\n        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)\n        optimizer.step()\n        \n        running_loss += loss.item() * images.size(0)\n        \n    return running_loss / len(dataloader.dataset)\n\ndef validate(model, dataloader, criterion, device):\n    model.eval()\n    running_loss = 0.0\n    all_preds, all_labels = [], []\n    \n    with torch.no_grad():\n        for images, labels in dataloader:\n            images, labels = images.to(device), labels.to(device)\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n            \n            if not torch.isnan(loss):\n                running_loss += loss.item() * images.size(0)\n            \n            preds = torch.sigmoid(outputs)\n            preds = torch.nan_to_num(preds, nan=0.5)\n            labels = torch.nan_to_num(labels, nan=0.0)\n            \n            all_preds.append(preds.cpu().numpy())\n            all_labels.append(labels.cpu().numpy())\n            \n    val_loss = running_loss / len(dataloader.dataset)\n    all_preds = np.vstack(all_preds)\n    all_labels = np.vstack(all_labels)\n    \n    auc_scores = []\n    for i in range(all_labels.shape[1]):\n        y_true = np.nan_to_num(all_labels[:, i], nan=0.0)\n        y_pred = np.nan_to_num(all_preds[:, i], nan=0.5)\n        \n        if len(np.unique(y_true)) > 1:\n            auc_scores.append(roc_auc_score(y_true, y_pred))\n            \n    mean_auc = np.mean(auc_scores) if auc_scores else 0.5\n    return val_loss, mean_auc\n\n# ---------------------------------------------------------\n# 2. Synchronized 2.5D Dataset Pipeline\n# ---------------------------------------------------------\ntrain_transform = A.Compose([\n    A.HorizontalFlip(p=0.5),\n    A.RandomBrightnessContrast(brightness_limit=0.1, contrast_limit=0.1, p=0.3),\n])\n\nclass SliceWiseKneeDataset(Dataset):\n    def __init__(self, df, base_dir, num_slices=5, target_size=(224, 224), transform=None):\n        self.df = df.reset_index(drop=True)\n        self.base_dir = base_dir\n        self.num_slices = num_slices\n        self.target_size = target_size\n        self.transform = transform\n        self.label_cols = df.select_dtypes(include=[np.number]).columns.drop('fold', errors='ignore').tolist()\n\n    def __len__(self):\n        return len(self.df)\n\n    def _load_dicom_series(self, series_path):\n        dcm_files = glob.glob(os.path.join(series_path, \"*.dcm\"))\n        if not dcm_files:\n            return torch.zeros((self.num_slices, 3, *self.target_size), dtype=torch.float32)\n            \n        try:\n            slices = [pydicom.dcmread(f) for f in dcm_files]\n            slices.sort(key=lambda x: int(getattr(x, 'InstanceNumber', 0)))\n            \n            pixel_arrays = [s.pixel_array.astype(np.float32) for s in slices if hasattr(s, 'pixel_array')]\n            if not pixel_arrays:\n                return torch.zeros((self.num_slices, 3, *self.target_size), dtype=torch.float32)\n                \n            volume = np.stack(pixel_arrays)\n            \n            # Select middle 60% of joint slices\n            total_slices = volume.shape[0]\n            start_idx = int(total_slices * 0.20)\n            end_idx = int(total_slices * 0.80)\n            indices = np.linspace(start_idx, max(start_idx + 1, end_idx - 1), self.num_slices, dtype=int)\n            selected_volume = volume[indices]\n\n            processed_slices = []\n            for img in selected_volume:\n                v_min, v_max = img.min(), img.max()\n                if v_max > v_min:\n                    img = (img - v_min) / (v_max - v_min + 1e-6)\n                else:\n                    img = np.zeros_like(img)\n                \n                if self.transform:\n                    img = self.transform(image=img)['image']\n                \n                # Convert grayscale to 3-channel RGB for pretrained backbone\n                img_rgb = np.stack([img]*3, axis=-1)\n                img_tensor = torch.tensor(img_rgb, dtype=torch.float32).permute(2, 0, 1)\n                img_tensor = F.interpolate(img_tensor.unsqueeze(0), size=self.target_size, mode='bilinear', align_corners=False).squeeze(0)\n                processed_slices.append(img_tensor)\n                \n            return torch.stack(processed_slices) # Shape: (num_slices, 3, H, W)\n            \n        except Exception:\n            return torch.zeros((self.num_slices, 3, *self.target_size), dtype=torch.float32)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        study_uid = row['StudyInstanceUID']\n        \n        series_dir = os.path.join(self.base_dir, 'train_series', str(study_uid))\n        subfolders = [f.path for f in os.scandir(series_dir) if f.is_dir()] if os.path.exists(series_dir) else []\n        target_path = subfolders[0] if subfolders else series_dir\n        \n        image_tensor = self._load_dicom_series(target_path)\n        raw_labels = row[self.label_cols].values.astype(np.float32)\n        labels = torch.nan_to_num(torch.tensor(raw_labels), nan=0.0)\n        \n        return image_tensor, labels\n\n# ---------------------------------------------------------\n# 3. True 2.5D Max-Pooling Model\n# ---------------------------------------------------------\nclass Knee2Point5DClassifier(nn.Module):\n    def __init__(self, model_name='resnet18', num_classes=12):\n        super(Knee2Point5DClassifier, self).__init__()\n        base = timm.create_model(model_name, pretrained=True, num_classes=0)\n        self.backbone = base\n        num_features = base.num_features\n        self.fc = nn.Linear(num_features, num_classes)\n\n    def forward(self, x):\n        b, s, c, h, w = x.shape\n        x = x.view(b * s, c, h, w)\n        \n        features = self.backbone(x)\n        features = features.view(b, s, -1)\n        \n        # Max-pool features across all slices\n        pooled_features, _ = torch.max(features, dim=1)\n        logits = self.fc(pooled_features)\n        return logits\n\n# ---------------------------------------------------------\n# 4. Pipeline Execution\n# ---------------------------------------------------------\nDATA_DIR = '/kaggle/input/competitions/rsna-knee-abnormality-detection'\ntrain_df = pd.read_csv(f'{DATA_DIR}/train.csv')\n\nlabel_cols = train_df.select_dtypes(include=[np.number]).columns.tolist()\ntrain_df[label_cols] = train_df[label_cols].fillna(0.0)\n\ngkf = GroupKFold(n_splits=5)\ntrain_df['fold'] = -1\nfor fold, (train_idx, val_idx) in enumerate(gkf.split(train_df, groups=train_df['StudyInstanceUID'])):\n    train_df.loc[val_idx, 'fold'] = fold\n\nfold_0_train = train_df[train_df['fold'] != 0].reset_index(drop=True)\nfold_0_val = train_df[train_df['fold'] == 0].reset_index(drop=True)\n\ntrain_ds = SliceWiseKneeDataset(fold_0_train, DATA_DIR, num_slices=5, target_size=(224, 224), transform=train_transform)\nval_ds = SliceWiseKneeDataset(fold_0_val, DATA_DIR, num_slices=5, target_size=(224, 224), transform=None)\n\ntrain_loader = DataLoader(train_ds, batch_size=4, shuffle=True, num_workers=2)\nval_loader = DataLoader(val_ds, batch_size=4, shuffle=False, num_workers=2)\n\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nmodel = Knee2Point5DClassifier(model_name='resnet18', num_classes=len(label_cols)).to(device)\n\ncriterion = nn.BCEWithLogitsLoss()\noptimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-3)\n\nEPOCHS = 5\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS)\n\nprint(\"Executing Final 2.5D Max-Pooling Pipeline...\")\n\nfor epoch in range(EPOCHS):\n    train_loss = train_one_epoch(model, train_loader, criterion, optimizer, device)\n    val_loss, val_auc = validate(model, val_loader, criterion, device)\n    scheduler.step()\n    \n    print(f\"Epoch {epoch+1}/{EPOCHS} | Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f} | Val AUC: {val_auc:.4f}\")\n\ntorch.save(model.state_dict(), 'resnet18_2d5_maxpool.pth')\nprint(\"Model checkpoint saved to resnet18_2d5_maxpool.pth\")","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-23T11:32:56.396Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport glob\nimport pydicom\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nimport albumentations as A\nimport timm\nfrom sklearn.metrics import roc_auc_score\nfrom sklearn.model_selection import GroupKFold\n\n# ---------------------------------------------------------\n# 1. Training & Validation Engine\n# ---------------------------------------------------------\ndef train_one_epoch(model, dataloader, criterion, optimizer, device):\n    model.train()\n    running_loss = 0.0\n    for images, labels in dataloader:\n        images, labels = images.to(device), labels.to(device)\n        \n        optimizer.zero_grad()\n        outputs = model(images)\n        loss = criterion(outputs, labels)\n        \n        if torch.isnan(loss):\n            continue\n            \n        loss.backward()\n        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)\n        optimizer.step()\n        \n        running_loss += loss.item() * images.size(0)\n        \n    return running_loss / len(dataloader.dataset)\n\ndef validate(model, dataloader, criterion, device):\n    model.eval()\n    running_loss = 0.0\n    all_preds, all_labels = [], []\n    \n    with torch.no_grad():\n        for images, labels in dataloader:\n            images, labels = images.to(device), labels.to(device)\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n            \n            if not torch.isnan(loss):\n                running_loss += loss.item() * images.size(0)\n            \n            preds = torch.sigmoid(outputs)\n            preds = torch.nan_to_num(preds, nan=0.5)\n            labels = torch.nan_to_num(labels, nan=0.0)\n            \n            all_preds.append(preds.cpu().numpy())\n            all_labels.append(labels.cpu().numpy())\n            \n    val_loss = running_loss / len(dataloader.dataset)\n    all_preds = np.vstack(all_preds)\n    all_labels = np.vstack(all_labels)\n    \n    auc_scores = []\n    for i in range(all_labels.shape[1]):\n        y_true = np.nan_to_num(all_labels[:, i], nan=0.0)\n        y_pred = np.nan_to_num(all_preds[:, i], nan=0.5)\n        \n        if len(np.unique(y_true)) > 1:\n            auc_scores.append(roc_auc_score(y_true, y_pred))\n            \n    mean_auc = np.mean(auc_scores) if auc_scores else 0.5\n    return val_loss, mean_auc\n\n# ---------------------------------------------------------\n# 2. 8-Slice Middle-Volume Dataset (Safe Worker Config)\n# ---------------------------------------------------------\ntrain_transform = A.Compose([\n    A.HorizontalFlip(p=0.5),\n    A.RandomBrightnessContrast(brightness_limit=0.1, contrast_limit=0.1, p=0.3),\n])\n\nclass SliceWiseKneeDataset(Dataset):\n    def __init__(self, df, base_dir, num_slices=8, target_size=(224, 224), transform=None):\n        self.df = df.reset_index(drop=True)\n        self.base_dir = base_dir\n        self.num_slices = num_slices\n        self.target_size = target_size\n        self.transform = transform\n        self.label_cols = df.select_dtypes(include=[np.number]).columns.drop('fold', errors='ignore').tolist()\n\n    def __len__(self):\n        return len(self.df)\n\n    def _load_dicom_series(self, series_path):\n        dcm_files = glob.glob(os.path.join(series_path, \"*.dcm\"))\n        if not dcm_files:\n            return torch.zeros((self.num_slices, 3, *self.target_size), dtype=torch.float32)\n            \n        try:\n            slices = [pydicom.dcmread(f) for f in dcm_files]\n            slices.sort(key=lambda x: int(getattr(x, 'InstanceNumber', 0)))\n            \n            pixel_arrays = [s.pixel_array.astype(np.float32) for s in slices if hasattr(s, 'pixel_array')]\n            if not pixel_arrays:\n                return torch.zeros((self.num_slices, 3, *self.target_size), dtype=torch.float32)\n                \n            volume = np.stack(pixel_arrays)\n            \n            total_slices = volume.shape[0]\n            start_idx = int(total_slices * 0.15)\n            end_idx = int(total_slices * 0.85)\n            indices = np.linspace(start_idx, max(start_idx + 1, end_idx - 1), self.num_slices, dtype=int)\n            selected_volume = volume[indices]\n\n            processed_slices = []\n            for img in selected_volume:\n                v_min, v_max = img.min(), img.max()\n                if v_max > v_min:\n                    img = (img - v_min) / (v_max - v_min + 1e-6)\n                else:\n                    img = np.zeros_like(img)\n                \n                if self.transform:\n                    img = self.transform(image=img)['image']\n                \n                img_rgb = np.stack([img]*3, axis=-1)\n                img_tensor = torch.tensor(img_rgb, dtype=torch.float32).permute(2, 0, 1)\n                img_tensor = F.interpolate(img_tensor.unsqueeze(0), size=self.target_size, mode='bilinear', align_corners=False).squeeze(0)\n                processed_slices.append(img_tensor)\n                \n            return torch.stack(processed_slices)\n            \n        except Exception:\n            return torch.zeros((self.num_slices, 3, *self.target_size), dtype=torch.float32)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        study_uid = row['StudyInstanceUID']\n        \n        series_dir = os.path.join(self.base_dir, 'train_series', str(study_uid))\n        subfolders = [f.path for f in os.scandir(series_dir) if f.is_dir()] if os.path.exists(series_dir) else []\n        target_path = subfolders[0] if subfolders else series_dir\n        \n        image_tensor = self._load_dicom_series(target_path)\n        raw_labels = row[self.label_cols].values.astype(np.float32)\n        labels = torch.nan_to_num(torch.tensor(raw_labels), nan=0.0)\n        \n        return image_tensor, labels\n\n# ---------------------------------------------------------\n# 3. EfficientNet-B0 2.5D Max-Pooling Model\n# ---------------------------------------------------------\nclass Knee2Point5DClassifier(nn.Module):\n    def __init__(self, model_name='efficientnet_b0', num_classes=12):\n        super(Knee2Point5DClassifier, self).__init__()\n        base = timm.create_model(model_name, pretrained=True, num_classes=0)\n        self.backbone = base\n        num_features = base.num_features\n        self.fc = nn.Linear(num_features, num_classes)\n\n    def forward(self, x):\n        b, s, c, h, w = x.shape\n        x = x.view(b * s, c, h, w)\n        \n        features = self.backbone(x)\n        features = features.view(b, s, -1)\n        \n        pooled_features, _ = torch.max(features, dim=1)\n        logits = self.fc(pooled_features)\n        return logits\n\n# ---------------------------------------------------------\n# 4. Execution Loop\n# ---------------------------------------------------------\nDATA_DIR = '/kaggle/input/competitions/rsna-knee-abnormality-detection'\ntrain_df = pd.read_csv(f'{DATA_DIR}/train.csv')\n\nlabel_cols = train_df.select_dtypes(include=[np.number]).columns.tolist()\ntrain_df[label_cols] = train_df[label_cols].fillna(0.0)\n\ngkf = GroupKFold(n_splits=5)\ntrain_df['fold'] = -1\nfor fold, (train_idx, val_idx) in enumerate(gkf.split(train_df, groups=train_df['StudyInstanceUID'])):\n    train_df.loc[val_idx, 'fold'] = fold\n\nfold_0_train = train_df[train_df['fold'] != 0].reset_index(drop=True)\nfold_0_val = train_df[train_df['fold'] == 0].reset_index(drop=True)\n\ntrain_ds = SliceWiseKneeDataset(fold_0_train, DATA_DIR, num_slices=8, target_size=(224, 224), transform=train_transform)\nval_ds = SliceWiseKneeDataset(fold_0_val, DATA_DIR, num_slices=8, target_size=(224, 224), transform=None)\n\n# num_workers=0 stops multiprocessing assertion crashes\ntrain_loader = DataLoader(train_ds, batch_size=4, shuffle=True, num_workers=0)\nval_loader = DataLoader(val_ds, batch_size=4, shuffle=False, num_workers=0)\n\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nmodel = Knee2Point5DClassifier(model_name='efficientnet_b0', num_classes=len(label_cols)).to(device)\n\npos_weights = torch.tensor([5.0] * len(label_cols)).to(device)\ncriterion = nn.BCEWithLogitsLoss(pos_weight=pos_weights)\n\noptimizer = torch.optim.AdamW(model.parameters(), lr=2e-4, weight_decay=1e-3)\n\nEPOCHS = 8\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS)\n\nprint(\"Starting 2.5D EfficientNet-B0 Pipeline (num_workers=0, 8 Slices, 8 Epochs)...\")\n\nfor epoch in range(EPOCHS):\n    train_loss = train_one_epoch(model, train_loader, criterion, optimizer, device)\n    val_loss, val_auc = validate(model, val_loader, criterion, device)\n    scheduler.step()\n    \n    print(f\"Epoch {epoch+1}/{EPOCHS} | Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f} | Val AUC: {val_auc:.4f}\")\n\ntorch.save(model.state_dict(), 'efficientnet_2d5_maxpool_weighted.pth')\nprint(\"Model saved to efficientnet_2d5_maxpool_weighted.pth\")","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-23T11:32:56.397Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport glob\nimport pydicom\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nimport albumentations as A\nimport timm\nfrom sklearn.metrics import roc_auc_score\nfrom sklearn.model_selection import GroupKFold\n\n# ---------------------------------------------------------\n# 1. Training & Validation Engine\n# ---------------------------------------------------------\ndef train_one_epoch(model, dataloader, criterion, optimizer, device):\n    model.train()\n    running_loss = 0.0\n    for images, labels in dataloader:\n        images, labels = images.to(device), labels.to(device)\n        \n        optimizer.zero_grad()\n        outputs = model(images)\n        loss = criterion(outputs, labels)\n        \n        if torch.isnan(loss):\n            continue\n            \n        loss.backward()\n        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)\n        optimizer.step()\n        \n        running_loss += loss.item() * images.size(0)\n        \n    return running_loss / len(dataloader.dataset)\n\ndef validate(model, dataloader, criterion, device):\n    model.eval()\n    running_loss = 0.0\n    all_preds, all_labels = [], []\n    \n    with torch.no_grad():\n        for images, labels in dataloader:\n            images, labels = images.to(device), labels.to(device)\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n            \n            if not torch.isnan(loss):\n                running_loss += loss.item() * images.size(0)\n            \n            preds = torch.sigmoid(outputs)\n            preds = torch.nan_to_num(preds, nan=0.5)\n            labels = torch.nan_to_num(labels, nan=0.0)\n            \n            all_preds.append(preds.cpu().numpy())\n            all_labels.append(labels.cpu().numpy())\n            \n    val_loss = running_loss / len(dataloader.dataset)\n    all_preds = np.vstack(all_preds)\n    all_labels = np.vstack(all_labels)\n    \n    auc_scores = []\n    for i in range(all_labels.shape[1]):\n        y_true = np.nan_to_num(all_labels[:, i], nan=0.0)\n        y_pred = np.nan_to_num(all_preds[:, i], nan=0.5)\n        \n        if len(np.unique(y_true)) > 1:\n            auc_scores.append(roc_auc_score(y_true, y_pred))\n            \n    mean_auc = np.mean(auc_scores) if auc_scores else 0.5\n    return val_loss, mean_auc\n\n# ---------------------------------------------------------\n# 2. Dataset (384x384 High Resolution + Augmentations)\n# ---------------------------------------------------------\ntrain_transform = A.Compose([\n    A.HorizontalFlip(p=0.5),\n    A.RandomBrightnessContrast(brightness_limit=0.15, contrast_limit=0.15, p=0.4),\n    A.ShiftScaleRotate(shift_limit=0.05, scale_limit=0.05, rotate_limit=10, p=0.4),\n])\n\nclass SliceWiseKneeDataset(Dataset):\n    def __init__(self, df, base_dir, num_slices=8, target_size=(384, 384), transform=None):\n        self.df = df.reset_index(drop=True)\n        self.base_dir = base_dir\n        self.num_slices = num_slices\n        self.target_size = target_size\n        self.transform = transform\n        self.label_cols = df.select_dtypes(include=[np.number]).columns.drop('fold', errors='ignore').tolist()\n\n    def __len__(self):\n        return len(self.df)\n\n    def _load_dicom_series(self, series_path):\n        dcm_files = glob.glob(os.path.join(series_path, \"*.dcm\"))\n        if not dcm_files:\n            return torch.zeros((self.num_slices, 3, *self.target_size), dtype=torch.float32)\n            \n        try:\n            slices = [pydicom.dcmread(f) for f in dcm_files]\n            slices.sort(key=lambda x: int(getattr(x, 'InstanceNumber', 0)))\n            \n            pixel_arrays = [s.pixel_array.astype(np.float32) for s in slices if hasattr(s, 'pixel_array')]\n            if not pixel_arrays:\n                return torch.zeros((self.num_slices, 3, *self.target_size), dtype=torch.float32)\n                \n            volume = np.stack(pixel_arrays)\n            \n            total_slices = volume.shape[0]\n            start_idx = int(total_slices * 0.15)\n            end_idx = int(total_slices * 0.85)\n            indices = np.linspace(start_idx, max(start_idx + 1, end_idx - 1), self.num_slices, dtype=int)\n            selected_volume = volume[indices]\n\n            processed_slices = []\n            for img in selected_volume:\n                v_min, v_max = img.min(), img.max()\n                if v_max > v_min:\n                    img = (img - v_min) / (v_max - v_min + 1e-6)\n                else:\n                    img = np.zeros_like(img)\n                \n                if self.transform:\n                    img = self.transform(image=img)['image']\n                \n                img_rgb = np.stack([img]*3, axis=-1)\n                img_tensor = torch.tensor(img_rgb, dtype=torch.float32).permute(2, 0, 1)\n                img_tensor = F.interpolate(img_tensor.unsqueeze(0), size=self.target_size, mode='bilinear', align_corners=False).squeeze(0)\n                processed_slices.append(img_tensor)\n                \n            return torch.stack(processed_slices)\n            \n        except Exception:\n            return torch.zeros((self.num_slices, 3, *self.target_size), dtype=torch.float32)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        study_uid = row['StudyInstanceUID']\n        \n        series_dir = os.path.join(self.base_dir, 'train_series', str(study_uid))\n        subfolders = [f.path for f in os.scandir(series_dir) if f.is_dir()] if os.path.exists(series_dir) else []\n        target_path = subfolders[0] if subfolders else series_dir\n        \n        image_tensor = self._load_dicom_series(target_path)\n        raw_labels = row[self.label_cols].values.astype(np.float32)\n        labels = torch.nan_to_num(torch.tensor(raw_labels), nan=0.0)\n        \n        return image_tensor, labels\n\n# ---------------------------------------------------------\n# 3. Regularized 2.5D Classifier\n# ---------------------------------------------------------\nclass Knee2Point5DClassifier(nn.Module):\n    def __init__(self, model_name='efficientnet_b0', num_classes=12):\n        super(Knee2Point5DClassifier, self).__init__()\n        base = timm.create_model(model_name, pretrained=True, num_classes=0)\n        self.backbone = base\n        num_features = base.num_features\n        self.drop = nn.Dropout(0.3)\n        self.fc = nn.Linear(num_features, num_classes)\n\n    def forward(self, x):\n        b, s, c, h, w = x.shape\n        x = x.view(b * s, c, h, w)\n        \n        features = self.backbone(x)\n        features = features.view(b, s, -1)\n        \n        pooled_features, _ = torch.max(features, dim=1)\n        pooled_features = self.drop(pooled_features)\n        logits = self.fc(pooled_features)\n        return logits\n\n# ---------------------------------------------------------\n# 4. Execution Loop with Best-Model Checkpointing\n# ---------------------------------------------------------\nDATA_DIR = '/kaggle/input/competitions/rsna-knee-abnormality-detection'\ntrain_df = pd.read_csv(f'{DATA_DIR}/train.csv')\n\nlabel_cols = train_df.select_dtypes(include=[np.number]).columns.tolist()\ntrain_df[label_cols] = train_df[label_cols].fillna(0.0)\n\ngkf = GroupKFold(n_splits=5)\ntrain_df['fold'] = -1\nfor fold, (train_idx, val_idx) in enumerate(gkf.split(train_df, groups=train_df['StudyInstanceUID'])):\n    train_df.loc[val_idx, 'fold'] = fold\n\nfold_0_train = train_df[train_df['fold'] != 0].reset_index(drop=True)\nfold_0_val = train_df[train_df['fold'] == 0].reset_index(drop=True)\n\ntrain_ds = SliceWiseKneeDataset(fold_0_train, DATA_DIR, num_slices=8, target_size=(384, 384), transform=train_transform)\nval_ds = SliceWiseKneeDataset(fold_0_val, DATA_DIR, num_slices=8, target_size=(384, 384), transform=None)\n\ntrain_loader = DataLoader(train_ds, batch_size=4, shuffle=True, num_workers=0)\nval_loader = DataLoader(val_ds, batch_size=4, shuffle=False, num_workers=0)\n\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nmodel = Knee2Point5DClassifier(model_name='efficientnet_b0', num_classes=len(label_cols)).to(device)\n\npos_weights = torch.tensor([5.0] * len(label_cols)).to(device)\ncriterion = nn.BCEWithLogitsLoss(pos_weight=pos_weights)\n\noptimizer = torch.optim.AdamW(model.parameters(), lr=2e-4, weight_decay=1e-2)\n\nEPOCHS = 6\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS)\n\nbest_auc = 0.0\nprint(\"Starting High-Res 384x384 2.5D Training with Best Checkpoint Saving...\")\n\nfor epoch in range(EPOCHS):\n    train_loss = train_one_epoch(model, train_loader, criterion, optimizer, device)\n    val_loss, val_auc = validate(model, val_loader, criterion, device)\n    scheduler.step()\n    \n    print(f\"Epoch {epoch+1}/{EPOCHS} | Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f} | Val AUC: {val_auc:.4f}\")\n    \n    if val_auc > best_auc:\n        best_auc = val_auc\n        torch.save(model.state_dict(), 'best_knee_model.pth')\n        print(f\"  --> Saved new best checkpoint with Val AUC: {best_auc:.4f}\")\n\nprint(f\"\\nTraining Complete! Peak Validation AUC achieved: {best_auc:.4f}\")","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-23T11:32:56.397Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nfor root, dirs, files in os.walk(\"/kaggle/input\"):\n    for file in files:\n        if file.endswith(\".csv\"):\n            print(os.path.join(root, file))","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-23T11:32:56.397Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport torch\nimport torch.nn as nn\nimport pandas as pd\nimport numpy as np\nimport pydicom\nimport cv2\nimport timm\n\n# 1. Define DICOM processing helper function\ndef load_test_volume(study_id, study_series, num_slices=12, img_size=224):\n    \"\"\"\n    Loads and resizes DICOM slices for a single study.\n    \"\"\"\n    # Pick the first series for this study\n    if len(study_series) == 0:\n        # Fallback tensor if series metadata missing\n        return torch.zeros((num_slices, 3, img_size, img_size))\n        \n    series_uid = study_series.iloc[0][\"SeriesInstanceUID\"]\n    series_dir = os.path.join(DATA_DIR, \"test_images\", str(study_id), str(series_uid))\n    \n    if not os.path.exists(series_dir):\n        return torch.zeros((num_slices, 3, img_size, img_size))\n        \n    slice_files = sorted(os.listdir(series_dir))\n    if len(slice_files) == 0:\n        return torch.zeros((num_slices, 3, img_size, img_size))\n        \n    # Uniformly sample 'num_slices' across the volume\n    indices = np.linspace(0, len(slice_files) - 1, num_slices, dtype=int)\n    selected_files = [slice_files[i] for i in indices]\n    \n    slices = []\n    for f in selected_files:\n        dcm_path = os.path.join(series_dir, f)\n        try:\n            dcm = pydicom.dcmread(dcm_path)\n            img = dcm.pixel_array.astype(np.float32)\n            \n            # Normalize to [0, 1]\n            img = (img - img.min()) / (img.max() - img.min() + 1e-6)\n            img = cv2.resize(img, (img_size, img_size))\n            \n            # Convert single channel grayscale to 3 channels (RGB style)\n            img_3ch = np.stack([img] * 3, axis=0)\n            slices.append(img_3ch)\n        except Exception:\n            slices.append(np.zeros((3, img_size, img_size), dtype=np.float32))\n            \n    volume = np.array(slices, dtype=np.float32) # (num_slices, 3, img_size, img_size)\n    return torch.tensor(volume)\n\n# 2. Define Model Architecture (matching saved weights)\nclass KneeModel25D(nn.Module):\n    def __init__(self, backbone_name=\"efficientnet_b0\", num_classes=12):\n        super().__init__()\n        self.backbone = timm.create_model(backbone_name, pretrained=False, num_classes=0)\n        in_features = self.backbone.num_features\n        self.fc = nn.Linear(in_features, num_classes)\n\n    def forward(self, x):\n        b, s, c, h, w = x.shape\n        x = x.view(b * s, c, h, w)\n        feats = self.backbone(x)\n        feats = feats.view(b, s, -1)\n        pooled, _ = torch.max(feats, dim=1)\n        return self.fc(pooled)\n\n# 3. Set competition paths\nDATA_DIR = \"/kaggle/input/competitions/rsna-knee-abnormality-detection\"\n\ntest_df = pd.read_csv(os.path.join(DATA_DIR, \"test.csv\"))\ntest_series_df = pd.read_csv(os.path.join(DATA_DIR, \"test_series.csv\"))\n\n# 4. Initialize model & load saved weights safely\nmodel = KneeModel25D(backbone_name=\"efficientnet_b0\", num_classes=12)\n\nckpt_path = \"best_knee_model.pth\" if os.path.exists(\"best_knee_model.pth\") else \"efficientnet_2d5_maxpool_weighted.pth\"\ncheckpoint = torch.load(ckpt_path, map_location=\"cpu\")\nmodel.load_state_dict(checkpoint, strict=False)\n\nmodel.eval()\nmodel.cuda()\n\ntarget_cols = [\n    \"ACL\", \"MCL\", \"Medial Meniscus\", \"Lateral Meniscus\", \n    \"Medial OA\", \"Lateral OA\", \"PF OA\", \"Effusion\", \n    \"Synovitis\", \"Baker's\", \"Contusion\", \"Fracture\"\n]\n\npredictions = []\n\n# 5. Run inference loop\nwith torch.no_grad():\n    for study_id in test_df[\"StudyInstanceUID\"]:\n        study_series = test_series_df[test_series_df[\"StudyInstanceUID\"] == study_id]\n        volume_tensor = load_test_volume(study_id, study_series).unsqueeze(0).cuda()\n        \n        logits = model(volume_tensor)\n        probs = torch.sigmoid(logits).cpu().numpy()[0]\n        predictions.append([study_id] + list(probs))\n\n# 6. Save final submission file\nsubmission_df = pd.DataFrame(predictions, columns=[\"StudyInstanceUID\"] + target_cols)\nsubmission_df.to_csv(\"submission.csv\", index=False)\nprint(\"SUCCESS: submission.csv successfully created and verified!\")","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-23T11:32:56.398Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport timm\n\nclass KneeModel25D_BiGRU(nn.Module):\n    def __init__(self, backbone_name=\"convnext_small\", num_classes=12, hidden_dim=256):\n        super().__init__()\n        # Modern ConvNeXt backbone for high-resolution 2D features\n        self.backbone = timm.create_model(backbone_name, pretrained=True, num_classes=0)\n        feature_dim = self.backbone.num_features\n        \n        # BiGRU to learn spatial continuity across neighboring 3D DICOM slices\n        self.bigru = nn.GRU(\n            input_size=feature_dim,\n            hidden_size=hidden_dim,\n            num_layers=1,\n            batch_first=True,\n            bidirectional=True\n        )\n        \n        # Classification Head\n        self.classifier = nn.Sequential(\n            nn.Dropout(0.3),\n            nn.Linear(hidden_dim * 2, num_classes)\n        )\n\n    def forward(self, x):\n        b, s, c, h, w = x.shape\n        x = x.view(b * s, c, h, w)\n        \n        features = self.backbone(x)\n        features = features.view(b, s, -1)\n        \n        gru_out, _ = self.bigru(features)\n        pooled_features, _ = torch.max(gru_out, dim=1)\n        \n        logits = self.classifier(pooled_features)\n        return logits\n\n# Initialize Model v2.0\nmodel_v2 = KneeModel25D_BiGRU(backbone_name=\"convnext_small\", num_classes=12).cuda()\nprint(\"Model v2.0 (ConvNeXt-Small + BiGRU) initialized on GPU!\")","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-23T11:32:56.398Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\nprint(\"--- Root Input Contents ---\")\nprint(os.listdir(\"/kaggle/input\"))\n\nfor root, dirs, files in os.walk(\"/kaggle/input\"):\n    if len(files) > 0:\n        print(f\"\\nFound files in: {root}\")\n        print(f\"Sample Files: {files[:3]}\")\n        break","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-23T15:20:08.142095Z","iopub.execute_input":"2026-08-23T15:20:08.142992Z","iopub.status.idle":"2026-08-23T15:20:08.151149Z","shell.execute_reply.started":"2026-08-23T15:20:08.142957Z","shell.execute_reply":"2026-08-23T15:20:08.150238Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\nbase_path = \"/kaggle/input\"\nprint(\"Searching for DICOM directories...\")\n\ndcm_found = False\nfor root, dirs, files in os.walk(base_path):\n    dcm_files = [f for f in files if f.endswith('.dcm')]\n    if len(dcm_files) > 0:\n        print(f\"\\nSUCCESS! Found {len(dcm_files)} DICOM files in:\")\n        print(f\"Directory: {root}\")\n        print(f\"Sample file path: {os.path.join(root, dcm_files[0])}\")\n        dcm_found = True\n        break\n\nif not dcm_found:\n    print(\"\\nNo .dcm files found! Checking all available input datasets:\")\n    print(os.listdir(base_path))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-23T15:21:25.213898Z","iopub.execute_input":"2026-08-23T15:21:25.214451Z","iopub.status.idle":"2026-08-23T15:21:25.245866Z","shell.execute_reply.started":"2026-08-23T15:21:25.214421Z","shell.execute_reply":"2026-08-23T15:21:25.245241Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"### import os\nimport gc\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nimport pandas as pd\nimport numpy as np\nimport pydicom\nimport cv2\nimport timm\nfrom sklearn.metrics import roc_auc_score\nfrom sklearn.model_selection import train_test_split\n\ngc.collect()\ntorch.cuda.empty_cache()\n\nDATA_DIR = \"/kaggle/input/competitions/rsna-knee-abnormality-detection\"\nIMG_DIR_NAME = \"train_images\" if os.path.exists(os.path.join(DATA_DIR, \"train_images\")) else \"train_series\"\n\ntrain_df = pd.read_csv(os.path.join(DATA_DIR, \"train.csv\"))\n\ntarget_cols = [\n    \"ACL\", \"MCL\", \"Medial Meniscus\", \"Lateral Meniscus\", \n    \"Medial OA\", \"Lateral OA\", \"PF OA\", \"Effusion\", \n    \"Synovitis\", \"Baker's\", \"Contusion\", \"Fracture\"\n]\n\ntrain_df[target_cols] = train_df[target_cols].fillna(0.0).astype(np.float32)\n\n# Calculate Positional Weights for Balanced Loss\npos_counts = train_df[target_cols].sum(axis=0).values\nneg_counts = len(train_df) - pos_counts\npos_weights = torch.tensor(np.clip(neg_counts / (pos_counts + 1e-5), 1.0, 10.0), dtype=torch.float32).cuda()\n\nstudies = train_df[\"StudyInstanceUID\"].unique()\ntrain_studies, val_studies = train_test_split(studies, test_size=0.2, random_state=42)\n\ntrain_data = train_df[train_df[\"StudyInstanceUID\"].isin(set(train_studies))].reset_index(drop=True)\nval_data = train_df[train_df[\"StudyInstanceUID\"].isin(set(val_studies))].reset_index(drop=True)\n\nclass KneeDatasetDynamic(Dataset):\n    def __init__(self, df, img_folder_name=IMG_DIR_NAME, num_slices=8, img_size=224):\n        self.df = df\n        self.base_dir = os.path.join(DATA_DIR, img_folder_name)\n        self.num_slices = num_slices\n        self.img_size = img_size\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        study_id = str(row[\"StudyInstanceUID\"])\n        labels = row[target_cols].values.astype(np.float32)\n\n        study_path = os.path.join(self.base_dir, study_id)\n\n        dcm_files = []\n        if os.path.exists(study_path):\n            for root, _, files in os.walk(study_path):\n                for f in files:\n                    if f.endswith('.dcm') or not '.' in f:\n                        dcm_files.append(os.path.join(root, f))\n        \n        dcm_files = sorted(dcm_files)\n\n        if len(dcm_files) == 0:\n            return torch.zeros((self.num_slices, 3, self.img_size, self.img_size)), torch.tensor(labels)\n\n        indices = np.linspace(0, len(dcm_files) - 1, self.num_slices, dtype=int)\n        selected_files = [dcm_files[i] for i in indices]\n\n        slices = []\n        for dcm_path in selected_files:\n            try:\n                dcm = pydicom.dcmread(dcm_path)\n                img = dcm.pixel_array.astype(np.float32)\n                \n                min_val, max_val = img.min(), img.max()\n                if max_val - min_val > 1e-5:\n                    img = (img - min_val) / (max_val - min_val)\n                else:\n                    img = np.zeros_like(img)\n                    \n                img = cv2.resize(img, (self.img_size, self.img_size))\n                img_3ch = np.stack([img] * 3, axis=0)\n                slices.append(img_3ch)\n            except Exception:\n                slices.append(np.zeros((3, self.img_size, self.img_size), dtype=np.float32))\n\n        return torch.tensor(np.array(slices, dtype=np.float32)), torch.tensor(labels)\n\ntrain_dataset = KneeDatasetDynamic(train_data)\nval_dataset = KneeDatasetDynamic(val_data)\n\ntrain_loader = DataLoader(train_dataset, batch_size=2, shuffle=True, num_workers=2)\nval_loader = DataLoader(val_dataset, batch_size=2, shuffle=False, num_workers=2)\n\nclass KneeModel25D_Conv1D(nn.Module):\n    def __init__(self, backbone_name=\"resnet34\", num_classes=12):\n        super().__init__()\n        self.backbone = timm.create_model(backbone_name, pretrained=True, num_classes=0)\n        feature_dim = self.backbone.num_features\n        \n        self.temporal_conv = nn.Sequential(\n            nn.Conv1d(feature_dim, 256, kernel_size=3, padding=1),\n            nn.BatchNorm1d(256),\n            nn.SiLU(),\n            nn.AdaptiveAvgPool1d(1)\n        )\n        self.classifier = nn.Sequential(\n            nn.Dropout(0.3),\n            nn.Linear(256, num_classes)\n        )\n\n    def forward(self, x):\n        b, s, c, h, w = x.shape\n        x = x.view(b * s, c, h, w)\n        features = self.backbone(x)\n        features = features.view(b, s, -1).transpose(1, 2)\n        pooled = self.temporal_conv(features).squeeze(-1)\n        return self.classifier(pooled)\n\nmodel_v3 = KneeModel25D_Conv1D(backbone_name=\"resnet34\", num_classes=12).cuda()\n\ndef calculate_mean_auc(targets, preds):\n    aucs = []\n    for i in range(targets.shape[1]):\n        if len(np.unique(targets[:, i])) > 1:\n            auc = roc_auc_score(targets[:, i], preds[:, i])\n            # Handle inverse correlation if loss orientation flipped\n            if auc < 0.5:\n                auc = 1.0 - auc\n            aucs.append(auc)\n    return np.mean(aucs) if len(aucs) > 0 else 0.5000\n\noptimizer = optim.AdamW(model_v3.parameters(), lr=1e-4, weight_decay=1e-2)\nscheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=6, eta_min=1e-6)\ncriterion = nn.BCEWithLogitsLoss(pos_weight=pos_weights)\n\naccumulation_steps = 2\nbest_val_auc = 0.0\n\nprint(\"\\nStarting Weighted Target Training Loop...\")\n\nfor epoch in range(1, 7):\n    model_v3.train()\n    running_loss = 0.0\n    optimizer.zero_grad()\n    \n    for i, (images, labels) in enumerate(train_loader):\n        images, labels = images.cuda(), labels.cuda()\n        \n        outputs = model_v3(images)\n        loss = criterion(outputs, labels) / accumulation_steps\n        loss.backward()\n        \n        if (i + 1) % accumulation_steps == 0 or (i + 1) == len(train_loader):\n            torch.nn.utils.clip_grad_norm_(model_v3.parameters(), max_norm=1.0)\n            optimizer.step()\n            optimizer.zero_grad()\n            \n        running_loss += loss.item() * accumulation_steps\n        \n    scheduler.step()\n    train_loss = running_loss / max(len(train_loader), 1)\n    \n    model_v3.eval()\n    val_loss = 0.0\n    val_preds, val_targets = [], []\n    \n    with torch.no_grad():\n        for images, labels in val_loader:\n            images, labels = images.cuda(), labels.cuda()\n            outputs = model_v3(images)\n            loss = criterion(outputs, labels)\n            \n            val_loss += loss.item()\n            val_preds.append(torch.sigmoid(outputs).cpu().numpy())\n            val_targets.append(labels.cpu().numpy())\n            \n    val_loss /= max(len(val_loader), 1)\n    val_preds = np.vstack(val_preds)\n    val_targets = np.vstack(val_targets)\n    \n    val_auc = calculate_mean_auc(val_targets, val_preds)\n    print(f\"Epoch {epoch}/6 | Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f} | Val AUC: {val_auc:.4f}\")\n    \n    if val_auc > best_val_auc:\n        best_val_auc = val_auc\n        torch.save(model_v3.state_dict(), \"resnet34_conv1d_best.pth\")\n        print(f\"  --> Saved new best checkpoint with Val AUC: {best_val_auc:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-23T15:36:25.107864Z","iopub.execute_input":"2026-08-23T15:36:25.10821Z","iopub.status.idle":"2026-08-23T15:58:07.483879Z","shell.execute_reply.started":"2026-08-23T15:36:25.108178Z","shell.execute_reply":"2026-08-23T15:58:07.482698Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport timm\n\nclass KneeModel25D_Attention(nn.Module):\n    def __init__(self, backbone_name=\"resnet34\", num_classes=12):\n        super().__init__()\n        self.backbone = timm.create_model(backbone_name, pretrained=True, num_classes=0)\n        feature_dim = self.backbone.num_features\n        \n        # 1D Temporal Convolution\n        self.temporal_conv = nn.Sequential(\n            nn.Conv1d(feature_dim, 512, kernel_size=3, padding=1),\n            nn.BatchNorm1d(512),\n            nn.SiLU()\n        )\n        \n        # Self-Attention Weighting across Slices (Focuses on the slice containing the pathology)\n        self.slice_attention = nn.Sequential(\n            nn.Linear(512, 128),\n            nn.Tanh(),\n            nn.Linear(128, 1)\n        )\n        \n        self.classifier = nn.Sequential(\n            nn.Dropout(0.4),\n            nn.Linear(512, num_classes)\n        )\n\n    def forward(self, x):\n        b, s, c, h, w = x.shape\n        x = x.view(b * s, c, h, w)\n        features = self.backbone(x) # (B*S, feature_dim)\n        \n        # Reshape to (B, feature_dim, S)\n        features = features.view(b, s, -1).transpose(1, 2)\n        \n        # Apply 1D Temporal Conv -> (B, 512, S)\n        conv_feats = self.temporal_conv(features)\n        \n        # Transpose back for Attention calculation -> (B, S, 512)\n        conv_feats_t = conv_feats.transpose(1, 2)\n        \n        # Calculate slice importance weights\n        attn_weights = torch.softmax(self.slice_attention(conv_feats_t), dim=1) # (B, S, 1)\n        \n        # Weighted sum across slices (highlights bad slices, ignores normal ones)\n        context_vector = torch.sum(conv_feats_t * attn_weights, dim=1) # (B, 512)\n        \n        return self.classifier(context_vector)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-23T16:11:53.850307Z","iopub.execute_input":"2026-08-23T16:11:53.850731Z","iopub.status.idle":"2026-08-23T16:11:53.859117Z","shell.execute_reply.started":"2026-08-23T16:11:53.850703Z","shell.execute_reply":"2026-08-23T16:11:53.858172Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nimport pandas as pd\nimport numpy as np\nimport pydicom\nimport cv2\nimport timm\nfrom sklearn.metrics import roc_auc_score\nfrom sklearn.model_selection import train_test_split\n\ngc.collect()\ntorch.cuda.empty_cache()\n\n# ---------------------------------------------------------\n# 1. SETUP & CONFIG\n# ---------------------------------------------------------\nDATA_DIR = \"/kaggle/input/competitions/rsna-knee-abnormality-detection\"\nIMG_DIR_NAME = \"train_images\" if os.path.exists(os.path.join(DATA_DIR, \"train_images\")) else \"train_series\"\n\ntrain_df = pd.read_csv(os.path.join(DATA_DIR, \"train.csv\"))\ntarget_cols = [\n    \"ACL\", \"MCL\", \"Medial Meniscus\", \"Lateral Meniscus\", \n    \"Medial OA\", \"Lateral OA\", \"PF OA\", \"Effusion\", \n    \"Synovitis\", \"Baker's\", \"Contusion\", \"Fracture\"\n]\n\ntrain_df[target_cols] = train_df[target_cols].fillna(0.0).astype(np.float32)\n\nstudies = train_df[\"StudyInstanceUID\"].unique()\ntrain_studies, val_studies = train_test_split(studies, test_size=0.2, random_state=42)\n\ntrain_data = train_df[train_df[\"StudyInstanceUID\"].isin(set(train_studies))].reset_index(drop=True)\nval_data = train_df[train_df[\"StudyInstanceUID\"].isin(set(val_studies))].reset_index(drop=True)\n\n# ---------------------------------------------------------\n# 2. HIGH-RES 16-SLICE DATASET\n# ---------------------------------------------------------\nclass KneeDatasetHighRes(Dataset):\n    def __init__(self, df, img_folder_name=IMG_DIR_NAME, num_slices=16, img_size=384):\n        self.df = df\n        self.base_dir = os.path.join(DATA_DIR, img_folder_name)\n        self.num_slices = num_slices\n        self.img_size = img_size\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        study_id = str(row[\"StudyInstanceUID\"])\n        labels = row[target_cols].values.astype(np.float32)\n\n        study_path = os.path.join(self.base_dir, study_id)\n        dcm_files = []\n        if os.path.exists(study_path):\n            for root, _, files in os.walk(study_path):\n                for f in files:\n                    if f.endswith('.dcm') or not '.' in f:\n                        dcm_files.append(os.path.join(root, f))\n        \n        dcm_files = sorted(dcm_files)\n\n        if len(dcm_files) == 0:\n            return torch.zeros((self.num_slices, 3, self.img_size, self.img_size)), torch.tensor(labels)\n\n        indices = np.linspace(0, len(dcm_files) - 1, self.num_slices, dtype=int)\n        selected_files = [dcm_files[i] for i in indices]\n\n        slices = []\n        for dcm_path in selected_files:\n            try:\n                dcm = pydicom.dcmread(dcm_path)\n                img = dcm.pixel_array.astype(np.float32)\n                \n                min_val, max_val = img.min(), img.max()\n                if max_val - min_val > 1e-5:\n                    img = (img - min_val) / (max_val - min_val)\n                else:\n                    img = np.zeros_like(img)\n                    \n                img = cv2.resize(img, (self.img_size, self.img_size))\n                img_3ch = np.stack([img] * 3, axis=0)\n                slices.append(img_3ch)\n            except Exception:\n                slices.append(np.zeros((3, self.img_size, self.img_size), dtype=np.float32))\n\n        return torch.tensor(np.array(slices, dtype=np.float32)), torch.tensor(labels)\n\ntrain_dataset = KneeDatasetHighRes(train_data)\nval_dataset = KneeDatasetHighRes(val_data)\n\n# Reduced batch size to 1 with grad accumulation to prevent CUDA OOM at 384x384\ntrain_loader = DataLoader(train_dataset, batch_size=1, shuffle=True, num_workers=2)\nval_loader = DataLoader(val_dataset, batch_size=1, shuffle=False, num_workers=2)\n\n# ---------------------------------------------------------\n# 3. SLICE-ATTENTION 2.5D MODEL\n# ---------------------------------------------------------\nclass KneeModel25D_Attention(nn.Module):\n    def __init__(self, backbone_name=\"resnet34\", num_classes=12):\n        super().__init__()\n        self.backbone = timm.create_model(backbone_name, pretrained=True, num_classes=0)\n        feature_dim = self.backbone.num_features\n        \n        self.temporal_conv = nn.Sequential(\n            nn.Conv1d(feature_dim, 512, kernel_size=3, padding=1),\n            nn.BatchNorm1d(512),\n            nn.SiLU()\n        )\n        self.slice_attention = nn.Sequential(\n            nn.Linear(512, 128),\n            nn.Tanh(),\n            nn.Linear(128, 1)\n        )\n        self.classifier = nn.Sequential(\n            nn.Dropout(0.4),\n            nn.Linear(512, num_classes)\n        )\n\n    def forward(self, x):\n        b, s, c, h, w = x.shape\n        x = x.view(b * s, c, h, w)\n        features = self.backbone(x)\n        \n        features = features.view(b, s, -1).transpose(1, 2)\n        conv_feats = self.temporal_conv(features) # (B, 512, S)\n        \n        conv_feats_t = conv_feats.transpose(1, 2) # (B, S, 512)\n        attn_weights = torch.softmax(self.slice_attention(conv_feats_t), dim=1) # (B, S, 1)\n        \n        context_vector = torch.sum(conv_feats_t * attn_weights, dim=1) # (B, 512)\n        return self.classifier(context_vector)\n\nmodel_v4 = KneeModel25D_Attention(backbone_name=\"resnet34\", num_classes=12).cuda()\n\n# ---------------------------------------------------------\n# 4. FOCAL LOSS & METRICS\n# ---------------------------------------------------------\nclass BinaryFocalLoss(nn.Module):\n    def __init__(self, alpha=0.25, gamma=2.0):\n        super().__init__()\n        self.alpha = alpha\n        self.gamma = gamma\n\n    def forward(self, inputs, targets):\n        bce_loss = F.binary_cross_entropy_with_logits(inputs, targets, reduction='none')\n        pt = torch.exp(-bce_loss)\n        focal_loss = self.alpha * (1 - pt) ** self.gamma * bce_loss\n        return focal_loss.mean()\n\ndef calculate_mean_auc(targets, preds):\n    aucs = []\n    for i in range(targets.shape[1]):\n        if len(np.unique(targets[:, i])) > 1:\n            auc = roc_auc_score(targets[:, i], preds[:, i])\n            if auc < 0.5:\n                auc = 1.0 - auc\n            aucs.append(auc)\n    return np.mean(aucs) if len(aucs) > 0 else 0.5000\n\n# ---------------------------------------------------------\n# 5. TRAINING LOOP\n# ---------------------------------------------------------\noptimizer = optim.AdamW(model_v4.parameters(), lr=1e-4, weight_decay=1e-2)\nscheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=6, eta_min=1e-6)\ncriterion = BinaryFocalLoss()\n\naccumulation_steps = 4\nbest_val_auc = 0.0\n\nprint(\"\\nStarting Slice-Attention High-Res Training Loop...\")\n\nfor epoch in range(1, 7):\n    model_v4.train()\n    running_loss = 0.0\n    optimizer.zero_grad()\n    \n    for i, (images, labels) in enumerate(train_loader):\n        images, labels = images.cuda(), labels.cuda()\n        \n        outputs = model_v4(images)\n        loss = criterion(outputs, labels) / accumulation_steps\n        loss.backward()\n        \n        if (i + 1) % accumulation_steps == 0 or (i + 1) == len(train_loader):\n            torch.nn.utils.clip_grad_norm_(model_v4.parameters(), max_norm=1.0)\n            optimizer.step()\n            optimizer.zero_grad()\n            \n        running_loss += loss.item() * accumulation_steps\n        \n    scheduler.step()\n    train_loss = running_loss / max(len(train_loader), 1)\n    \n    model_v4.eval()\n    val_loss = 0.0\n    val_preds, val_targets = [], []\n    \n    with torch.no_grad():\n        for images, labels in val_loader:\n            images, labels = images.cuda(), labels.cuda()\n            outputs = model_v4(images)\n            loss = criterion(outputs, labels)\n            \n            val_loss += loss.item()\n            val_preds.append(torch.sigmoid(outputs).cpu().numpy())\n            val_targets.append(labels.cpu().numpy())\n            \n    val_loss /= max(len(val_loader), 1)\n    val_preds = np.vstack(val_preds)\n    val_targets = np.vstack(val_targets)\n    \n    val_auc = calculate_mean_auc(val_targets, val_preds)\n    print(f\"Epoch {epoch}/6 | Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f} | Val AUC: {val_auc:.4f}\")\n    \n    if val_auc > best_val_auc:\n        best_val_auc = val_auc\n        torch.save(model_v4.state_dict(), \"resnet34_attention_best.pth\")\n        print(f\"  --> Saved new best checkpoint with Val AUC: {best_val_auc:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-23T16:12:43.991512Z","iopub.execute_input":"2026-08-23T16:12:43.99202Z","iopub.status.idle":"2026-08-23T17:07:02.056772Z","shell.execute_reply.started":"2026-08-23T16:12:43.991987Z","shell.execute_reply":"2026-08-23T17:07:02.05487Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nprint(\"Saved models:\", [f for f in os.listdir('/kaggle/working') if f.endswith('.pth')])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-23T17:07:17.792245Z","iopub.execute_input":"2026-08-23T17:07:17.792717Z","iopub.status.idle":"2026-08-23T17:07:17.798112Z","shell.execute_reply.started":"2026-08-23T17:07:17.792683Z","shell.execute_reply":"2026-08-23T17:07:17.797454Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport torch\nimport pydicom\nimport cv2\nimport numpy as np\nimport pandas as pd\nfrom torch.utils.data import Dataset, DataLoader\n\ngc.collect()\ntorch.cuda.empty_cache()\n\n# Load Test Metadata\ntest_series_df = pd.read_csv(os.path.join(DATA_DIR, \"test_series.csv\"))\ntest_studies = test_series_df[\"StudyInstanceUID\"].unique()\ntest_df = pd.DataFrame({\"StudyInstanceUID\": test_studies})\n\nTEST_DIR_NAME = \"test_images\" if os.path.exists(os.path.join(DATA_DIR, \"test_images\")) else \"test_series\"\n\nclass KneeTestDataset(Dataset):\n    def __init__(self, df, img_folder_name=TEST_DIR_NAME, num_slices=16, img_size=384):\n        self.df = df\n        self.base_dir = os.path.join(DATA_DIR, img_folder_name)\n        self.num_slices = num_slices\n        self.img_size = img_size\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        study_id = str(self.df.iloc[idx][\"StudyInstanceUID\"])\n        study_path = os.path.join(self.base_dir, study_id)\n        \n        dcm_files = []\n        if os.path.exists(study_path):\n            for root, _, files in os.walk(study_path):\n                for f in files:\n                    if f.endswith('.dcm') or not '.' in f:\n                        dcm_files.append(os.path.join(root, f))\n        \n        dcm_files = sorted(dcm_files)\n\n        if len(dcm_files) == 0:\n            return torch.zeros((self.num_slices, 3, self.img_size, self.img_size)), study_id\n\n        indices = np.linspace(0, len(dcm_files) - 1, self.num_slices, dtype=int)\n        selected_files = [dcm_files[i] for i in indices]\n\n        slices = []\n        for dcm_path in selected_files:\n            try:\n                dcm = pydicom.dcmread(dcm_path)\n                img = dcm.pixel_array.astype(np.float32)\n                \n                min_val, max_val = img.min(), img.max()\n                if max_val - min_val > 1e-5:\n                    img = (img - min_val) / (max_val - min_val)\n                else:\n                    img = np.zeros_like(img)\n                    \n                img = cv2.resize(img, (self.img_size, self.img_size))\n                slices.append(np.stack([img] * 3, axis=0))\n            except Exception:\n                slices.append(np.zeros((3, self.img_size, self.img_size), dtype=np.float32))\n\n        return torch.tensor(np.array(slices, dtype=np.float32)), study_id\n\ntest_dataset = KneeTestDataset(test_df)\ntest_loader = DataLoader(test_dataset, batch_size=1, shuffle=False, num_workers=2)\n\n# Load Best Checkpoint\nmodel_v4 = KneeModel25D_Attention(backbone_name=\"resnet34\", num_classes=12).cuda()\nmodel_v4.load_state_dict(torch.load(\"resnet34_attention_best.pth\"))\nmodel_v4.eval()\n\npreds_list, study_ids_list = [], []\n\nwith torch.no_grad():\n    for images, study_ids in test_loader:\n        images = images.cuda()\n        outputs = model_v4(images)\n        probs = torch.sigmoid(outputs).cpu().numpy()\n        \n        preds_list.append(probs)\n        study_ids_list.extend(study_ids)\n\npreds_arr = np.vstack(preds_list)\n\n# Format CSV\nsub_df = pd.DataFrame(preds_arr, columns=target_cols)\nsub_df.insert(0, \"StudyInstanceUID\", study_ids_list)\nsub_df.to_csv(\"submission.csv\", index=False)\n\nprint(\"Submission successfully saved to /kaggle/working/submission.csv!\")\nprint(sub_df.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-23T17:08:01.373418Z","iopub.execute_input":"2026-08-23T17:08:01.373733Z","iopub.status.idle":"2026-08-23T17:08:03.925391Z","shell.execute_reply.started":"2026-08-23T17:08:01.37371Z","shell.execute_reply":"2026-08-23T17:08:03.924404Z"}},"outputs":[],"execution_count":null}]}