{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","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"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"d156baee-11bf-40cb-97e4-df80e3486973","cell_type":"markdown","source":"\n***Public LB: 0.515***\n\n***This first baseline is intentionally simple. The low leaderboard score and widening train/validation gap show that the current setup generalizes poorly. That gives us a clear starting point for the next iteration.***\n\n","metadata":{}},{"id":"1fd8074d","cell_type":"markdown","source":"In this notebook:\n\n**data -> image -> tensor -> model -> logits -> loss -> backpropagation -> validation -> probabilities -> submission**\n\nFor this first baseline we use:\n\n- one MRI series per study\n- the middle slice of that series\n- a pretrained **ResNet18**\n- 12 output logits, converted to probabilities with sigmoid\n- only explicitly available target labels\n- a masked binary cross-entropy loss so missing labels are never treated as negatives\n\nThis is a clean baseline that can be improveed later.","metadata":{}},{"id":"5f156bba-9549-4466-b50c-558092ecb202","cell_type":"markdown","source":"# Before running this notebook\n> Make sure a **GPU accelerator is enabled** in Kaggle:\n\n> Go to **Settings → Accelerator → GPU**, then save/apply the setting before running the notebook.\n\nYou can verify that the GPU is available with:\n\n```python\nprint(torch.cuda.is_available())\nprint(torch.cuda.get_device_name(0) if torch.cuda.is_available() else \"CPU\")\n```\nIf a GPU is enabled correctly, the first line should print `True`.","metadata":{}},{"id":"d396ebd8-9b86-4c4c-841a-a8767096ab4e","cell_type":"code","source":"import torch\nprint(torch.cuda.is_available())\nprint(torch.cuda.get_device_name(0) if torch.cuda.is_available() else \"CPU\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-29T10:26:41.10539Z","iopub.execute_input":"2026-08-29T10:26:41.106011Z","iopub.status.idle":"2026-08-29T10:26:43.030901Z","shell.execute_reply.started":"2026-08-29T10:26:41.105975Z","shell.execute_reply":"2026-08-29T10:26:43.030124Z"}},"outputs":[],"execution_count":null},{"id":"3d2b5281","cell_type":"markdown","source":"### Previous notebooks\n\n0. **[Origin: Compet Roadmap & Keywords](https://www.kaggle.com/code/h17ann/0-rsna-knee-origin-compet-roadmap-keywords)**\n1. **[Simple Starting Point (EDA)](https://www.kaggle.com/code/h17ann/1-rsna-knee-simple-starting-point-eda)**\n2. **[DICOM Metadata Explorer](https://www.kaggle.com/code/h17ann/2-rsna-knee-dicom-metadata-explorer)**\n3. **[See the MRI: Slices & Studies](https://www.kaggle.com/code/h17ann/3-rsna-knee-see-the-mri-ax-cor-sag)**\n4. **[Know the 12 Targets](https://www.kaggle.com/code/h17ann/4-rsna-knee-know-the-12-targets)**\n5. **[Read the Radiology Reports](https://www.kaggle.com/code/h17ann/5-rsna-knee-read-the-radiological-reports)**","metadata":{}},{"id":"f7e1188b","cell_type":"markdown","source":"## 1. Imports","metadata":{}},{"id":"a7e646eb","cell_type":"code","source":"import kagglehub\nfrom pathlib import Path\nimport random\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport pydicom\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\n\nfrom torchvision.models import resnet18, ResNet18_Weights\nfrom torchvision.transforms import v2\n\nfrom sklearn.model_selection import train_test_split","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-29T10:26:43.032339Z","iopub.execute_input":"2026-08-29T10:26:43.032654Z","iopub.status.idle":"2026-08-29T10:26:45.753107Z","shell.execute_reply.started":"2026-08-29T10:26:43.032632Z","shell.execute_reply":"2026-08-29T10:26:45.752431Z"}},"outputs":[],"execution_count":null},{"id":"08ec6015","cell_type":"markdown","source":"## 2. Reproducibility","metadata":{}},{"id":"0f75876b","cell_type":"code","source":"SEED = 42\n\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\n\nif torch.cuda.is_available():\n    torch.cuda.manual_seed_all(SEED)\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nprint(\"Device:\", device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-29T10:26:45.754Z","iopub.execute_input":"2026-08-29T10:26:45.754442Z","iopub.status.idle":"2026-08-29T10:26:45.76154Z","shell.execute_reply.started":"2026-08-29T10:26:45.754417Z","shell.execute_reply":"2026-08-29T10:26:45.7609Z"}},"outputs":[],"execution_count":null},{"id":"d4db326a","cell_type":"markdown","source":"## 3. Dataset paths","metadata":{}},{"id":"0c587729","cell_type":"code","source":"DATA_DIR = Path(\"/kaggle/input/competitions/rsna-knee-abnormality-detection\")\n\nif not DATA_DIR.exists():\n    DATA_DIR = Path(\n        kagglehub.competition_download(\n            \"rsna-knee-abnormality-detection\"\n        )\n    )\n    \nTRAIN_DIR = DATA_DIR / \"train_series\"\nTEST_DIR  = DATA_DIR / \"test_series\"\n\nassert DATA_DIR.exists(), (\n    \"Competition data not found. \"\n    \"Attach 'RSNA Knee Abnormality Detection' to this notebook first.\"\n)\n\nprint(DATA_DIR)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-29T10:26:45.763018Z","iopub.execute_input":"2026-08-29T10:26:45.76321Z","iopub.status.idle":"2026-08-29T10:26:45.777655Z","shell.execute_reply.started":"2026-08-29T10:26:45.763192Z","shell.execute_reply":"2026-08-29T10:26:45.776804Z"}},"outputs":[],"execution_count":null},{"id":"c25fd548","cell_type":"markdown","source":"## 4. Load the tables","metadata":{}},{"id":"f8c703ea","cell_type":"code","source":"train = pd.read_csv(DATA_DIR / \"train.csv\")\ntrain_series = pd.read_csv(DATA_DIR / \"train_series.csv\")\n\ntest = pd.read_csv(DATA_DIR / \"test.csv\")\ntest_series = pd.read_csv(DATA_DIR / \"test_series.csv\")\n\nsample_submission = pd.read_csv(DATA_DIR / \"sample_submission.csv\")\n\nprint(\"Train studies :\", len(train))\nprint(\"Train series  :\", len(train_series))\nprint(\"Test studies  :\", len(test))\nprint(\"Test series   :\", len(test_series))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-29T10:26:45.778639Z","iopub.execute_input":"2026-08-29T10:26:45.778944Z","iopub.status.idle":"2026-08-29T10:26:45.940323Z","shell.execute_reply.started":"2026-08-29T10:26:45.778913Z","shell.execute_reply":"2026-08-29T10:26:45.939493Z"}},"outputs":[],"execution_count":null},{"id":"2889f573","cell_type":"markdown","source":"## 5. The 12 targets","metadata":{}},{"id":"95c2f556","cell_type":"code","source":"TARGETS = [\n    \"ACL\",\n    \"MCL\",\n    \"Medial Meniscus\",\n    \"Lateral Meniscus\",\n    \"Medial OA\",\n    \"Lateral OA\",\n    \"PF OA\",\n    \"Effusion\",\n    \"Synovitis\",\n    \"Baker's\",\n    \"Contusion\",\n    \"Fracture\",\n]\n\nprint(\"Number of targets:\", len(TARGETS))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-29T10:26:45.941255Z","iopub.execute_input":"2026-08-29T10:26:45.941552Z","iopub.status.idle":"2026-08-29T10:26:45.946348Z","shell.execute_reply.started":"2026-08-29T10:26:45.941519Z","shell.execute_reply":"2026-08-29T10:26:45.945571Z"}},"outputs":[],"execution_count":null},{"id":"9ee2c482","cell_type":"markdown","source":"## 6. Use only explicit labels\n\nA very important detail:\n\n**missing target values are not negatives.**\n\nSo we first keep studies that contain at least one explicit target value.\n\nLater, the loss will also use a mask so that each individual missing target is ignored.","metadata":{}},{"id":"9e93f86b","cell_type":"code","source":"has_labels = train[TARGETS].notna().any(axis=1)\n\nlabeled = train.loc[has_labels].copy()\n\nprint(\"Studies with at least one expliicit label:\", len(labeled))\n\ndisplay(\n    labeled[[\"StudyInstanceUID\"] + TARGETS].head()\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-29T10:26:45.947424Z","iopub.execute_input":"2026-08-29T10:26:45.947712Z","iopub.status.idle":"2026-08-29T10:26:45.981356Z","shell.execute_reply.started":"2026-08-29T10:26:45.947679Z","shell.execute_reply":"2026-08-29T10:26:45.9808Z"}},"outputs":[],"execution_count":null},{"id":"141bed1a","cell_type":"markdown","source":"## 7. Pick one series per study\n\nTo keep the first baseline intentionally simple, we take the **first listed series** for each study.\n\nThis is not a claim that it is the best series.\n\nIt is simply a deterministic rule that lets us build the full pipeline without adding further strategies yet.","metadata":{}},{"id":"b3b161e0","cell_type":"code","source":"first_train_series = (\n    train_series\n    .drop_duplicates(\"StudyInstanceUID\")\n    [[\"StudyInstanceUID\", \"SeriesInstanceUID\"]]\n)\n\nlabeled = labeled.merge(\n    first_train_series,\n    on=\"StudyInstanceUID\",\n    how=\"inner\"\n)\n\nprint(\"Labeled studies with a selected series:\", len(labeled))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-29T10:26:45.982117Z","iopub.execute_input":"2026-08-29T10:26:45.982491Z","iopub.status.idle":"2026-08-29T10:26:45.993563Z","shell.execute_reply.started":"2026-08-29T10:26:45.982465Z","shell.execute_reply":"2026-08-29T10:26:45.992806Z"}},"outputs":[],"execution_count":null},{"id":"3ebc51d6","cell_type":"markdown","source":"## 8. Train / validation split\n\nFor simplicity, we will make a simple random split at the **study level**.\n\nWith such a small explicitly labeled subset, this validation score should be interpreted carefully.","metadata":{}},{"id":"72a79c8b","cell_type":"code","source":"train_df, val_df = train_test_split(\n    labeled,\n    test_size=0.20,\n    random_state=SEED\n)\n\ntrain_df = train_df.reset_index(drop=True)\nval_df = val_df.reset_index(drop=True)\n\nprint(\"Training studies  :\", len(train_df))\nprint(\"Validation studies:\", len(val_df))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-29T10:26:45.994584Z","iopub.execute_input":"2026-08-29T10:26:45.994934Z","iopub.status.idle":"2026-08-29T10:26:46.006845Z","shell.execute_reply.started":"2026-08-29T10:26:45.994912Z","shell.execute_reply":"2026-08-29T10:26:46.006216Z"}},"outputs":[],"execution_count":null},{"id":"5fe7980a","cell_type":"markdown","source":"## 9. From DICOM series to one image\n\nFor each selected series:\n\n1. find all DICOM files\n2. sort them using `InstanceNumber`\n3. take the middle slice\n4. clip extreme intensities\n5. scale the image to `[0, 1]`\n6. resize to `224 x 224`\n7. copy the grayscale channel three times because the pretrained ResNet expects 3 channels\n8. apply the ImageNet normalization used by the pretrained network","metadata":{}},{"id":"f25e93db","cell_type":"code","source":"image_transform = v2.Compose([\n    v2.Resize((224, 224), antialias=True),\n    v2.Normalize(\n        mean=[0.485, 0.456, 0.406],\n        std=[0.229, 0.224, 0.225]\n    )\n])\n\n\ndef load_middle_slice(series_dir):\n    files = list(series_dir.glob(\"*.dcm\"))\n\n    slices = []\n\n    for path in files:\n        ds = pydicom.dcmread(path)\n        instance = int(ds.get(\"InstanceNumber\", 0))\n        slices.append((instance, ds.pixel_array))\n\n    slices.sort(key=lambda x: x[0])\n\n    image = slices[len(slices) // 2][1].astype(np.float32)\n\n    lo, hi = np.percentile(image, [1, 99])\n\n    image = np.clip(image, lo, hi)\n    image = (image - lo) / (hi - lo + 1e-8)\n\n    return image","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-29T10:26:46.009488Z","iopub.execute_input":"2026-08-29T10:26:46.010118Z","iopub.status.idle":"2026-08-29T10:26:46.023576Z","shell.execute_reply.started":"2026-08-29T10:26:46.010092Z","shell.execute_reply":"2026-08-29T10:26:46.022954Z"}},"outputs":[],"execution_count":null},{"id":"31c2a638","cell_type":"markdown","source":"## 10. PyTorch Dataset\n\nThe Dataset defines what we return for one study: the image, its 12 targets, and the label mask.\n\nhere, for every study we return:\n\n- `x`: the MRI image tensor\n- `y`: the 12 target values\n- `mask`: which target values are actually known","metadata":{}},{"id":"43c44343","cell_type":"code","source":"class KneeDataset(Dataset):\n    def __init__(self, dataframe, root_dir, targets=None):\n        self.df = dataframe.reset_index(drop=True)\n        self.root_dir = root_dir\n        self.targets = targets\n\n    def __len__(self):\n        return len(self.df)\n\n    \n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n\n        series_dir = (\n            self.root_dir\n            / str(row[\"StudyInstanceUID\"])\n            / str(row[\"SeriesInstanceUID\"])\n        )\n\n        image = load_middle_slice(series_dir)\n        x = torch.from_numpy(image).float().unsqueeze(0)\n        x = x.repeat(3, 1, 1)\n        x = image_transform(x)\n\n        if self.targets is None:\n            return x, row[\"StudyInstanceUID\"]\n\n        y_np = row[self.targets].to_numpy(dtype=np.float32)\n        mask_np = ~np.isnan(y_np)\n        y_np = np.nan_to_num(y_np, nan=0.0)\n\n        y = torch.tensor(y_np, dtype=torch.float32)\n        mask = torch.tensor(mask_np, dtype=torch.bool)\n\n        return x, y, mask","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-29T10:26:46.024522Z","iopub.execute_input":"2026-08-29T10:26:46.024855Z","iopub.status.idle":"2026-08-29T10:26:46.040032Z","shell.execute_reply.started":"2026-08-29T10:26:46.024831Z","shell.execute_reply":"2026-08-29T10:26:46.039144Z"}},"outputs":[],"execution_count":null},{"id":"3f8d3d81","cell_type":"markdown","source":"## 11. DataLoaders","metadata":{}},{"id":"7ff46e23","cell_type":"code","source":"BATCH_SIZE = 8\n\ntrain_ds = KneeDataset(\n    train_df,\n    TRAIN_DIR,\n    targets=TARGETS\n)\n\nval_ds = KneeDataset(\n    val_df,\n    TRAIN_DIR,\n    targets=TARGETS\n)\n\ntrain_loader = DataLoader(\n    train_ds,\n    batch_size=BATCH_SIZE,\n    shuffle=True,\n    num_workers=2,\n    pin_memory=True\n)\n\nval_loader = DataLoader(\n    val_ds,\n    batch_size=BATCH_SIZE,\n    shuffle=False,\n    num_workers=2,\n    pin_memory=True\n)\n\nx_batch, y_batch, mask_batch = next(iter(train_loader))\n\nprint(\"Images :\", x_batch.shape)\nprint(\"Targets:\", y_batch.shape)\nprint(\"Mask   :\", mask_batch.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-29T10:26:46.040891Z","iopub.execute_input":"2026-08-29T10:26:46.041274Z","iopub.status.idle":"2026-08-29T10:26:47.394231Z","shell.execute_reply.started":"2026-08-29T10:26:46.041248Z","shell.execute_reply":"2026-08-29T10:26:47.393268Z"}},"outputs":[],"execution_count":null},{"id":"e831caae","cell_type":"markdown","source":"For example, with batch size 8:\n\n`Images: [8, 3, 224, 224]`\n\nmeans:\n\n**batch x channels x height x width**\n\nand:\n\n`Targets: [8, 12]`\n\nmeans 12 outputs for every study.","metadata":{}},{"id":"a25df2b6","cell_type":"markdown","source":"## 12. Visual sanity check","metadata":{}},{"id":"9bd40807","cell_type":"code","source":"x, y, mask = train_ds[0]\n\nmean = torch.tensor([0.485, 0.456, 0.406])[:, None, None]\nstd = torch.tensor([0.229, 0.224, 0.225])[:, None, None]\n\ndisplay_img = (x.cpu()*std + mean).clamp(0, 1)\n\nplt.figure(figsize=(5, 5))\nplt.imshow(display_img[0], cmap=\"gray\")\nplt.axis(\"off\")\nplt.title(\"Image entering the network\")\nplt.show()\n\nprint(\"Known labels:\", int(mask.sum()), \"/\", len(TARGETS))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-29T10:26:47.395526Z","iopub.execute_input":"2026-08-29T10:26:47.39592Z","iopub.status.idle":"2026-08-29T10:26:47.612538Z","shell.execute_reply.started":"2026-08-29T10:26:47.395891Z","shell.execute_reply":"2026-08-29T10:26:47.611813Z"}},"outputs":[],"execution_count":null},{"id":"7542389c","cell_type":"markdown","source":"## 13. Model: pretrained ResNet18\n\nSo, a pretrained CNN already knows useful low-level visual features such as edges, textures and shapes.\n\nWe will keep the ResNet18 backbone and replace its final classification layer.\n\nOriginal output: `ResNet18 -> 1000 ImageNet classes`\n\nOur output: `ResNet18 -> 12 knee targets`","metadata":{}},{"id":"69060a55","cell_type":"code","source":"# weights = ResNet18_Weights.DEFAULT\n\n# model = resnet18(weights=weights)\n\n# torch.save(\n#     model.state_dict(),\n#     \"/kaggle/working/resnet18_imagenet.pth\"\n# )\n\n# in_features = model.fc.in_features\n\n# model.fc = nn.Linear(\n#     in_features,\n#     len(TARGETS)\n# )\n\n# model = model.to(device)\n\n# print(model.fc)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-29T10:26:47.613573Z","iopub.execute_input":"2026-08-29T10:26:47.613972Z","iopub.status.idle":"2026-08-29T10:26:47.617844Z","shell.execute_reply.started":"2026-08-29T10:26:47.613924Z","shell.execute_reply":"2026-08-29T10:26:47.617219Z"}},"outputs":[],"execution_count":null},{"id":"1a86381f-be71-41f8-a704-9331db46bc6a","cell_type":"code","source":"model = resnet18(weights=None)\n\nstate_dict = torch.load(\n    \"/kaggle/input/datasets/h17ann/resnet18-imagenet-weights/resnet18_imagenet.pth\",\n    map_location=\"cpu\",\n    weights_only=True\n)\n\nmodel.load_state_dict(state_dict)\n\n# Now replace the ImageNet classifier\nin_features = model.fc.in_features\nmodel.fc = nn.Linear(in_features, len(TARGETS))\n\nmodel = model.to(device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-29T10:26:47.619037Z","iopub.execute_input":"2026-08-29T10:26:47.619799Z","iopub.status.idle":"2026-08-29T10:26:48.15795Z","shell.execute_reply.started":"2026-08-29T10:26:47.619769Z","shell.execute_reply":"2026-08-29T10:26:48.157132Z"}},"outputs":[],"execution_count":null},{"id":"3058d918","cell_type":"markdown","source":"### Freeze the backbone first\n\nWe will freeze the pretrained feature extractor and train only the new final layer.","metadata":{}},{"id":"bcdcc229","cell_type":"code","source":"for parameter in model.parameters():\n    parameter.requires_grad = False\n\nfor parameter in model.fc.parameters():\n    parameter.requires_grad = True\n\ntrainable = sum(\n    p.numel()\n    for p in model.parameters()\n    if p.requires_grad\n        )\n\nprint(\"Trainable parameters:\", trainable)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-29T10:26:48.159104Z","iopub.execute_input":"2026-08-29T10:26:48.159481Z","iopub.status.idle":"2026-08-29T10:26:48.164963Z","shell.execute_reply.started":"2026-08-29T10:26:48.159439Z","shell.execute_reply":"2026-08-29T10:26:48.164147Z"}},"outputs":[],"execution_count":null},{"id":"a24f1b8c","cell_type":"markdown","source":"## 14. Logits, sigmoid and probabilities\n\nThe model first gives raw scores called **logits**.\n\nDuring training, we use `binary_cross_entropy_with_logits`, which already handles the sigmoid internally.\n\nDuring prediction, we convert logits into probabilities with `probability = sigmoid(logit)`","metadata":{}},{"id":"6543bee8","cell_type":"code","source":"x_example, _, _ = train_ds[0]\n\nmodel.eval()\nwith torch.no_grad():\n    example_logits = model(\n        x_example.unsqueeze(0).to(device)\n    )\n\nexample_probs = torch.sigmoid(example_logits)\n\nprint(\"Logits shape       :\", example_logits.shape)\nprint(\"Probabilities shape:\", example_probs.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-29T10:26:48.165956Z","iopub.execute_input":"2026-08-29T10:26:48.166435Z","iopub.status.idle":"2026-08-29T10:26:48.537354Z","shell.execute_reply.started":"2026-08-29T10:26:48.166411Z","shell.execute_reply":"2026-08-29T10:26:48.536597Z"}},"outputs":[],"execution_count":null},{"id":"d62c5040","cell_type":"markdown","source":"## 15. Masked binary cross-entropy\n\nThe full training table contains many missing target values. After selecting the explicitly labeled studies, the available labels are complete in this dataset. We keep the mask in the pipeline as a safe way to ignore missing labels if they occur.","metadata":{}},{"id":"848c1991","cell_type":"code","source":"def masked_bce_loss(logits, targets, mask):\n    loss = F.binary_cross_entropy_with_logits(\n        logits,\n        targets,\n        reduction=\"none\"\n    )\n\n    loss = loss * mask.float()\n\n    return loss.sum() / mask.float().sum().clamp_min(1.0)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-29T10:26:48.53835Z","iopub.execute_input":"2026-08-29T10:26:48.539143Z","iopub.status.idle":"2026-08-29T10:26:48.543111Z","shell.execute_reply.started":"2026-08-29T10:26:48.539108Z","shell.execute_reply":"2026-08-29T10:26:48.542452Z"}},"outputs":[],"execution_count":null},{"id":"d7d0496b","cell_type":"markdown","source":"## 16. Optimizer","metadata":{}},{"id":"0a98c368","cell_type":"code","source":"optimizer = torch.optim.AdamW(\n    model.fc.parameters(),\n    lr=1e-3,\n    weight_decay=1e-4\n)\n\nEPOCHS = 17","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-29T10:26:48.544067Z","iopub.execute_input":"2026-08-29T10:26:48.544385Z","iopub.status.idle":"2026-08-29T10:26:48.560081Z","shell.execute_reply.started":"2026-08-29T10:26:48.54436Z","shell.execute_reply":"2026-08-29T10:26:48.559488Z"}},"outputs":[],"execution_count":null},{"id":"e7243a70","cell_type":"markdown","source":"## 17. One training epoch\n\nThe core deep-learning loop is:\n\n1. forward pass\n2. compute loss\n3. clear old gradients\n4. backpropagation\n5. optimizer step","metadata":{}},{"id":"1a89264e","cell_type":"code","source":"def train_one_epoch(model, loader, optimizer):\n    model.eval()\n    model.fc.train()\n\n    total_loss = 0.0\n\n    for images, targets, mask in loader:\n        images = images.to(device)\n        targets = targets.to(device)\n        mask = mask.to(device)\n\n        optimizer.zero_grad()\n\n        logits = model(images)\n\n        loss = masked_bce_loss(\n            logits,\n            targets,\n            mask\n        )\n\n        loss.backward()\n        optimizer.step()\n\n        total_loss += loss.item()\n\n    return total_loss / len(loader)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-29T10:26:48.561057Z","iopub.execute_input":"2026-08-29T10:26:48.561334Z","iopub.status.idle":"2026-08-29T10:26:48.576386Z","shell.execute_reply.started":"2026-08-29T10:26:48.561301Z","shell.execute_reply":"2026-08-29T10:26:48.575804Z"}},"outputs":[],"execution_count":null},{"id":"b1cc8a2a","cell_type":"markdown","source":"## 18. Validation","metadata":{}},{"id":"35a5f0ed","cell_type":"code","source":"@torch.no_grad()\ndef validate(model, loader):\n    model.eval()\n\n    total_loss = 0.0\n\n    for images, targets, mask in loader:\n        images = images.to(device)\n        targets = targets.to(device)\n        mask = mask.to(device)\n\n        logits = model(images)\n\n        loss = masked_bce_loss(\n            logits,\n            targets,\n            mask\n        )\n\n        total_loss += loss.item()\n\n    return total_loss / len(loader)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-29T10:26:48.577372Z","iopub.execute_input":"2026-08-29T10:26:48.577662Z","iopub.status.idle":"2026-08-29T10:26:48.595917Z","shell.execute_reply.started":"2026-08-29T10:26:48.577632Z","shell.execute_reply":"2026-08-29T10:26:48.595143Z"}},"outputs":[],"execution_count":null},{"id":"3d144f5f","cell_type":"markdown","source":"## 19. Train the baseline","metadata":{}},{"id":"4384374a","cell_type":"code","source":"best_val_loss = float(\"inf\")\nhistory = []\n\nfor epoch in range(EPOCHS):\n    train_loss = train_one_epoch(\n        model,\n        train_loader,\n        optimizer\n    )\n\n    val_loss = validate(\n        model,\n        val_loader\n    )\n\n    history.append({\n        \"epoch\": epoch + 1,\n        \"train_loss\": train_loss,\n        \"val_loss\": val_loss\n    })\n\n    print(\n        f\"Epoch {epoch + 1:02d}/{EPOCHS} | \"\n        f\"train loss: {train_loss:.4f} | \"\n        f\"val loss: {val_loss:.4f}\"\n    )\n\n    if val_loss < best_val_loss:\n        best_val_loss = val_loss\n\n        torch.save(\n            model.state_dict(),\n            \"/kaggle/working/best_resnet18_baseline.pt\"\n        )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-29T10:26:48.596842Z","iopub.execute_input":"2026-08-29T10:26:48.59727Z","iopub.status.idle":"2026-08-29T10:27:31.456439Z","shell.execute_reply.started":"2026-08-29T10:26:48.597244Z","shell.execute_reply":"2026-08-29T10:27:31.455604Z"}},"outputs":[],"execution_count":null},{"id":"d0f8d54c","cell_type":"markdown","source":"## 20. Learning curves","metadata":{}},{"id":"e497fe25","cell_type":"code","source":"history_df = pd.DataFrame(history)\n\nplt.figure(figsize=(8, 5))\n\nplt.plot(\n    history_df[\"epoch\"],\n    history_df[\"train_loss\"],\n    marker=\"o\",\n    label=\"Train\"\n)\n\nplt.plot(\n    history_df[\"epoch\"],\n    history_df[\"val_loss\"],\n    marker=\"o\",\n    label=\"Validation\"\n)\n\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Masked BCE loss\")\nplt.title(\"Baseline learning curve\")\nplt.legend()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-29T10:27:31.457728Z","iopub.execute_input":"2026-08-29T10:27:31.458146Z","iopub.status.idle":"2026-08-29T10:27:31.619799Z","shell.execute_reply.started":"2026-08-29T10:27:31.458115Z","shell.execute_reply":"2026-08-29T10:27:31.619054Z"}},"outputs":[],"execution_count":null},{"id":"8c42769b","cell_type":"markdown","source":"## 21. Load the best checkpoint","metadata":{}},{"id":"f9169654","cell_type":"code","source":"model.load_state_dict(\n    torch.load(\n        \"/kaggle/working/best_resnet18_baseline.pt\",\n        map_location=device\n    )\n)\n\nmodel.eval()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-29T10:27:31.620846Z","iopub.execute_input":"2026-08-29T10:27:31.621137Z","iopub.status.idle":"2026-08-29T10:27:31.696289Z","shell.execute_reply.started":"2026-08-29T10:27:31.621113Z","shell.execute_reply":"2026-08-29T10:27:31.695632Z"}},"outputs":[],"execution_count":null},{"id":"5791be6a","cell_type":"markdown","source":"## 22. Prepare the test studies\n\nWe use exactly the same simple rule as during training:\n\n**first listed series -> middle slice**","metadata":{}},{"id":"56a094b8","cell_type":"code","source":"first_test_series = (\n    test_series\n    .drop_duplicates(\"StudyInstanceUID\")\n    [[\"StudyInstanceUID\", \"SeriesInstanceUID\"]]\n)\n\ntest_df = test.merge(\n    first_test_series,\n    on=\"StudyInstanceUID\",\n    how=\"left\"\n)\n\ndisplay(test_df.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-29T10:27:31.697163Z","iopub.execute_input":"2026-08-29T10:27:31.697506Z","iopub.status.idle":"2026-08-29T10:27:31.709111Z","shell.execute_reply.started":"2026-08-29T10:27:31.697469Z","shell.execute_reply":"2026-08-29T10:27:31.708504Z"}},"outputs":[],"execution_count":null},{"id":"e6fcfa99","cell_type":"markdown","source":"## 23. Test DataLoader","metadata":{}},{"id":"55bf3712","cell_type":"code","source":"test_ds = KneeDataset(\n    test_df,\n    TEST_DIR,\n    targets=None\n)\n\ntest_loader = DataLoader(\n    test_ds,\n    batch_size=BATCH_SIZE,\n    shuffle=False,\n    num_workers=2,\n    pin_memory=True\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-29T10:27:31.710089Z","iopub.execute_input":"2026-08-29T10:27:31.710478Z","iopub.status.idle":"2026-08-29T10:27:31.724464Z","shell.execute_reply.started":"2026-08-29T10:27:31.710456Z","shell.execute_reply":"2026-08-29T10:27:31.723896Z"}},"outputs":[],"execution_count":null},{"id":"32abdb3a","cell_type":"markdown","source":"## 24. Inference\n\nTraining uses logits.\n\nSubmission uses probabilities:\n\n`sigmoid(logits) -> values between 0 and 1`","metadata":{}},{"id":"6afea478","cell_type":"code","source":"all_ids = []\nall_probs = []\n\nmodel.eval()\n\nwith torch.no_grad():\n    for images, study_ids in test_loader:\n        images = images.to(device)\n\n        logits = model(images)\n        probs = torch.sigmoid(logits)\n\n        all_probs.append(\n            probs.cpu().numpy()\n        )\n\n        all_ids.extend(study_ids)\n\nall_probs = np.concatenate(\n    all_probs,\n    axis=0\n)\n\nprint(all_probs.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-29T10:27:31.725404Z","iopub.execute_input":"2026-08-29T10:27:31.725793Z","iopub.status.idle":"2026-08-29T10:27:32.121284Z","shell.execute_reply.started":"2026-08-29T10:27:31.72576Z","shell.execute_reply":"2026-08-29T10:27:32.12025Z"}},"outputs":[],"execution_count":null},{"id":"34a857c4","cell_type":"markdown","source":"## 25. Create `submission.csv`","metadata":{}},{"id":"07931d3a","cell_type":"code","source":"predictions = pd.DataFrame(\n    all_probs,\n    columns=TARGETS\n)\n\npredictions.insert(\n    0,\n    \"StudyInstanceUID\",\n    all_ids\n)\n\nsubmission = (\n    sample_submission[[\"StudyInstanceUID\"]]\n    .merge(\n        predictions,\n        on=\"StudyInstanceUID\",\n        how=\"left\"\n    )\n)\n\nsubmission.to_csv(\n    \"/kaggle/working/submission.csv\",\n    index=False\n)\n\ndisplay(submission.head())\n\nprint(\"Saved to /kaggle/working/submission.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-29T10:27:32.125057Z","iopub.execute_input":"2026-08-29T10:27:32.125406Z","iopub.status.idle":"2026-08-29T10:27:32.148178Z","shell.execute_reply.started":"2026-08-29T10:27:32.125376Z","shell.execute_reply":"2026-08-29T10:27:32.147579Z"}},"outputs":[],"execution_count":null},{"id":"93b1e22b","cell_type":"markdown","source":"# Quick Recapp\n\nThis notebook went through this full pipeline:\n\n### 1. Data\n`DICOM -> NumPy array`\n\n### 2. Preprocessing\n`intensity clipping -> normalization -> resize`\n\n### 3. Tensor\n`H x W -> 3 x 224 x 224`\n\n### 4. Dataset\none study returned at a time\n\n### 5. DataLoader\nseveral studies grouped into batches\n\n### 6. Model\n`ResNet18 -> 12 logits`\n\n### 7. Loss\nmasked binary cross-entropy\n\n### 8. Backpropagation\n`loss.backward()`\n\n### 9. Optimization\n`optimizer.step()`\n\n### 10. Validation\nevaluate without changing weights\n\n### 11. Inference\n`logits -> sigmoid -> probabilities`\n\n### 12. Submission\none row per study and 12 predicted probabilities\n\nThat is the basic deep-learning loop.","metadata":{}},{"id":"cefd021a","cell_type":"markdown","source":"## Next\n\nNow that the pipeline is clear, the next notebook can improve **one component at a time** instead of changing everything at once. \n\n# Good Luck !","metadata":{}}]}