{"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":"5195e10f-bd70-499d-8d60-89d993b791bc","cell_type":"markdown","source":"# RSNA Knee -- gold-label baseline (v1)\n\nFirst end-to-end pass, run entirely on Kaggle's own compute against the mounted\ncompetition data -- no local downloads. Deliberately simple:\n\n- **Training data**: only the 58 studies in `train.csv` that already have all 12\n  finding columns filled in by a radiologist (no report-derived pseudo-labels --\n  that's a later iteration, this is just \"does the pipeline work end to end\").\n- **One slice per study**: the true anatomical middle slice of one series per\n  study, series picked via `train_series.csv`'s `Fluid_Sensitive`/`Fat_Suppression`\n  flags (falls back to the first listed series if no fluid-sensitive/fat-sat series\n  exists for that study).\n- **Model**: ImageNet-pretrained ResNet-18, fine-tuned, sigmoid 12-output head.\n\nEvery major section below is timed independently (start/elapsed printed as it runs,\nplus a summary table at the end) so the total is broken down by phase -- this is\nthe reference point for how long training/inference actually take on Kaggle's\ncompute vs. downloading everything locally first.","metadata":{}},{"id":"3e109cd6-3318-48c4-ab59-a0f96ec02cb7","cell_type":"code","source":"import time\n\nT0 = time.time()\n\ndef log(msg):\n    print(f\"[{time.time() - T0:7.1f}s] {msg}\", flush=True)\n\nSECTION_TIMES = {}\n\nclass timed_section:\n    \"\"\"Context manager: prints start/end + elapsed for one notebook section.\"\"\"\n    def __init__(self, name):\n        self.name = name\n    def __enter__(self):\n        self.t0 = time.time()\n        log(f\">>> START {self.name}\")\n        return self\n    def __exit__(self, exc_type, exc, tb):\n        elapsed = time.time() - self.t0\n        SECTION_TIMES[self.name] = elapsed\n        log(f\"<<< END {self.name}  ({elapsed:.1f}s / {elapsed/60:.2f} min)\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-13T07:04:14.135295Z","iopub.execute_input":"2026-08-13T07:04:14.135546Z","iopub.status.idle":"2026-08-13T07:04:14.145672Z","shell.execute_reply.started":"2026-08-13T07:04:14.13552Z","shell.execute_reply":"2026-08-13T07:04:14.144757Z"}},"outputs":[],"execution_count":null},{"id":"f3273a3b-6513-4e80-b271-d8b1effb7987","cell_type":"code","source":"with timed_section(\"00_setup_and_imports\"):\n    import sys\n    import subprocess\n    from pathlib import Path\n\n    import numpy as np\n    import pandas as pd\n    from PIL import Image\n\n    try:\n        import pydicom\n    except ImportError:\n        subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"pydicom\"], check=True)\n        import pydicom\n\n    import torch\n    import torch.nn as nn\n    import torchvision\n    from torch.utils.data import TensorDataset, DataLoader\n    from sklearn.model_selection import train_test_split\n    from sklearn.metrics import roc_auc_score\n\n    LABEL_COLS = [\"ACL\", \"MCL\", \"Medial Meniscus\", \"Lateral Meniscus\", \"Medial OA\",\n                  \"Lateral OA\", \"PF OA\", \"Effusion\", \"Synovitis\", \"Baker's\",\n                  \"Contusion\", \"Fracture\"]\n    IMG_SIZE = 224\n    IMAGENET_MEAN = [0.485, 0.456, 0.406]\n    IMAGENET_STD = [0.229, 0.224, 0.225]\n\n    CANDIDATE_ROOTS = [\n        Path(\"/kaggle/input/rsna-knee-abnormality-detection\"),\n        Path(\"/kaggle/input/competitions/rsna-knee-abnormality-detection\"),\n    ]\n\n    def find_root():\n        for c in CANDIDATE_ROOTS:\n            if (c / \"train.csv\").is_file():\n                return c\n        base = Path(\"/kaggle/input\")\n        if base.exists():\n            for p in base.rglob(\"train.csv\"):\n                return p.parent\n        raise FileNotFoundError(\"could not locate competition data root under /kaggle/input\")\n\n    def pick_series_for_study(study_uid, series_df):\n        rows = series_df[series_df.StudyInstanceUID == study_uid]\n        if rows.empty:\n            return None\n        preferred = rows[(rows.Fluid_Sensitive == 1) & (rows.Fat_Suppression == 1)]\n        chosen = preferred.iloc[0] if len(preferred) else rows.iloc[0]\n        return chosen.SeriesInstanceUID\n\n    def load_middle_slice_array(series_dir, size=IMG_SIZE):\n        \"\"\"True middle slice by InstanceNumber, percentile-normalized, resized to size x size in [0,1].\"\"\"\n        files = sorted(series_dir.glob(\"*.dcm\")) if series_dir.exists() else []\n        if not files:\n            return None\n        tagged = []\n        for p in files:\n            try:\n                ds = pydicom.dcmread(p, stop_before_pixels=False)\n                order_key = float(getattr(ds, \"InstanceNumber\", None) or 0)\n                tagged.append((order_key, ds))\n            except Exception:\n                continue\n        if not tagged:\n            return None\n        tagged.sort(key=lambda t: t[0])\n        mid_ds = tagged[len(tagged) // 2][1]\n        arr = mid_ds.pixel_array.astype(np.float32)\n        lo, hi = np.percentile(arr, 0.5), np.percentile(arr, 99.5)\n        arr = np.clip((arr - lo) / max(hi - lo, 1e-6), 0, 1)\n        img = Image.fromarray((arr * 255).astype(np.uint8)).convert(\"L\")\n        img = img.resize((size, size), Image.BILINEAR)\n        return np.asarray(img, dtype=np.float32) / 255.0\n\n    def to_model_input(x_np):\n        \"\"\"(N,H,W) in [0,1] -> (N,3,H,W) ImageNet-normalized tensor.\"\"\"\n        t = torch.from_numpy(x_np).float().unsqueeze(1).repeat(1, 3, 1, 1)\n        mean_t = torch.tensor(IMAGENET_MEAN).view(1, 3, 1, 1)\n        std_t = torch.tensor(IMAGENET_STD).view(1, 3, 1, 1)\n        return (t - mean_t) / std_t\n\n    device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    log(f\"torch {torch.__version__}, torchvision {torchvision.__version__}, device={device}\")\n    if device.type == \"cuda\":\n        log(f\"GPU: {torch.cuda.get_device_name(0)}\")\n\n    ROOT = find_root()\n    log(f\"data root: {ROOT}\")\n    log(f\"train_series/ exists: {(ROOT / 'train_series').exists()}, \"\n        f\"test_series/ exists: {(ROOT / 'test_series').exists()}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-13T07:04:14.146701Z","iopub.execute_input":"2026-08-13T07:04:14.147018Z","iopub.status.idle":"2026-08-13T07:04:26.827102Z","shell.execute_reply.started":"2026-08-13T07:04:14.146989Z","shell.execute_reply":"2026-08-13T07:04:26.826408Z"}},"outputs":[],"execution_count":null},{"id":"6e78188f-0920-450b-9201-c6e350d59c75","cell_type":"code","source":"with timed_section(\"01_load_gold_labels\"):\n    train_df = pd.read_csv(ROOT / \"train.csv\")\n    train_series_df = pd.read_csv(ROOT / \"train_series.csv\")\n\n    # has_all_labels = train_df[LABEL_COLS].apply(lambda col: col.astype(str).str.strip() != \"\").all(axis=1)\n    has_all_labels = train_df[LABEL_COLS].notna().all(axis=1)\n    \n    gold_df = train_df[has_all_labels].reset_index(drop=True)\n\n    log(f\"train.csv: {len(train_df)} studies total\")\n    log(f\"gold studies (all 12 label columns filled): {len(gold_df)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-13T07:04:26.828778Z","iopub.execute_input":"2026-08-13T07:04:26.829194Z","iopub.status.idle":"2026-08-13T07:04:27.095377Z","shell.execute_reply.started":"2026-08-13T07:04:26.829166Z","shell.execute_reply":"2026-08-13T07:04:27.09473Z"}},"outputs":[],"execution_count":null},{"id":"df00b0f2-9096-4291-bbd7-57f341607b75","cell_type":"code","source":"with timed_section(\"02_build_train_dataset\"):\n    X_list, y_list, kept_ids = [], [], []\n    for i, row in gold_df.iterrows():\n        study = row.StudyInstanceUID\n        series_uid = pick_series_for_study(study, train_series_df)\n        arr = None\n        if series_uid is not None:\n            series_dir = ROOT / \"train_series\" / study / series_uid\n            arr = load_middle_slice_array(series_dir)\n        if arr is None:\n            log(f\"  [{i + 1}/{len(gold_df)}] {study[-8:]}: no usable series/slice, skipping\")\n            continue\n        X_list.append(arr)\n        y_list.append([float(row[c]) for c in LABEL_COLS])\n        kept_ids.append(study)\n        log(f\"  [{i + 1}/{len(gold_df)}] {study[-8:]}: loaded\")\n\n    X = np.stack(X_list)\n    y = np.array(y_list, dtype=np.float32)\n    log(f\"dataset built: {X.shape[0]} studies, image shape {X.shape[1:]}, label shape {y.shape}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-13T07:04:27.096324Z","iopub.execute_input":"2026-08-13T07:04:27.096623Z","iopub.status.idle":"2026-08-13T07:04:46.500065Z","shell.execute_reply.started":"2026-08-13T07:04:27.096598Z","shell.execute_reply":"2026-08-13T07:04:46.49932Z"}},"outputs":[],"execution_count":null},{"id":"2a7f3f80-3165-4a3f-ba4f-e71ed9525eb7","cell_type":"code","source":"with timed_section(\"03_train_val_split\"):\n    idx = np.arange(len(X))\n    train_idx, val_idx = train_test_split(idx, test_size=0.2, random_state=42)\n    X_train, y_train = X[train_idx], y[train_idx]\n    X_val, y_val = X[val_idx], y[val_idx]\n    log(f\"train: {len(train_idx)} studies, val: {len(val_idx)} studies\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-13T07:04:46.501169Z","iopub.execute_input":"2026-08-13T07:04:46.501475Z","iopub.status.idle":"2026-08-13T07:04:46.515541Z","shell.execute_reply.started":"2026-08-13T07:04:46.501449Z","shell.execute_reply":"2026-08-13T07:04:46.514724Z"}},"outputs":[],"execution_count":null},{"id":"d2f54ebd-2cdb-499a-b675-4312f7800a87","cell_type":"code","source":"with timed_section(\"04_build_model\"):\n    from torchvision.models import resnet18\n\n    model = resnet18(weights=None)\n    local_weights = sorted(Path(\"/kaggle/input\").rglob(\"resnet18*.pth\"))\n    if local_weights:\n        state_dict = torch.load(local_weights[0], map_location=device)\n        model.load_state_dict(state_dict)\n        log(f\"loaded pretrained weights from local resource: {local_weights[0]}\")\n    else:\n        try:\n            from torchvision.models import ResNet18_Weights\n            model = resnet18(weights=ResNet18_Weights.DEFAULT)\n            log(\"no local weights found; downloaded pretrained weights over the internet\")\n        except Exception as e:\n            log(f\"no local weights and download failed ({e}); using random init\")\n\n    model.fc = nn.Linear(model.fc.in_features, len(LABEL_COLS))\n    model.to(device)\n\n    loss_fn = nn.BCEWithLogitsLoss()\n    optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)\n    log(f\"model: resnet18, {sum(p.numel() for p in model.parameters()):,} params\")\n\n\n# with timed_section(\"04_build_model\"):\n#     try:\n#         from torchvision.models import resnet18, ResNet18_Weights\n#         model = resnet18(weights=ResNet18_Weights.DEFAULT)\n#     except ImportError:\n#         from torchvision.models import resnet18\n#         model = resnet18(pretrained=True)\n#     model.fc = nn.Linear(model.fc.in_features, len(LABEL_COLS))\n#     model.to(device)\n\n#     loss_fn = nn.BCEWithLogitsLoss()\n#     optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)\n#     log(f\"model: resnet18, {sum(p.numel() for p in model.parameters()):,} params\")\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-13T07:04:46.516769Z","iopub.execute_input":"2026-08-13T07:04:46.517288Z","iopub.status.idle":"2026-08-13T07:09:22.384443Z","shell.execute_reply.started":"2026-08-13T07:04:46.517249Z","shell.execute_reply":"2026-08-13T07:09:22.383524Z"}},"outputs":[],"execution_count":null},{"id":"be47be91-317d-4269-b6ae-5bbe0f2efef1","cell_type":"code","source":"# --- TRAINING -----------------------------------------------------------------\nEPOCHS = 15\nBATCH_SIZE = 8\n\nwith timed_section(\"05_TRAINING\"):\n    train_ds = TensorDataset(to_model_input(X_train), torch.from_numpy(y_train))\n    train_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True)\n\n    for epoch in range(1, EPOCHS + 1):\n        epoch_t0 = time.time()\n        model.train()\n        running_loss = 0.0\n        for xb, yb in train_loader:\n            xb, yb = xb.to(device), yb.to(device)\n            optimizer.zero_grad()\n            out = model(xb)\n            loss = loss_fn(out, yb)\n            loss.backward()\n            optimizer.step()\n            running_loss += loss.item() * xb.size(0)\n        epoch_loss = running_loss / len(train_ds)\n        epoch_elapsed = time.time() - epoch_t0\n        log(f\"  epoch {epoch:2d}/{EPOCHS}  loss={epoch_loss:.4f}  ({epoch_elapsed:.1f}s)\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-13T07:09:22.385452Z","iopub.execute_input":"2026-08-13T07:09:22.385758Z","iopub.status.idle":"2026-08-13T07:10:40.750102Z","shell.execute_reply.started":"2026-08-13T07:09:22.385732Z","shell.execute_reply":"2026-08-13T07:10:40.749376Z"}},"outputs":[],"execution_count":null},{"id":"a71dd6bf-0228-4100-9804-4e5b9e116bf3","cell_type":"code","source":"# --- VALIDATION -----------------------------------------------------------------\nwith timed_section(\"06_VALIDATION\"):\n    model.eval()\n    with torch.no_grad():\n        val_logits = model(to_model_input(X_val).to(device)).cpu().numpy()\n    val_probs = 1 / (1 + np.exp(-val_logits))\n\n    aucs = {}\n    for i, col in enumerate(LABEL_COLS):\n        yt = y_val[:, i]\n        if len(set(yt)) < 2:\n            log(f\"  {col}: skipped (only one class present in {len(yt)}-study val split)\")\n            continue\n        aucs[col] = roc_auc_score(yt, val_probs[:, i])\n        log(f\"  {col}: AUC={aucs[col]:.3f}\")\n\n    if aucs:\n        log(f\"mean AUC over {len(aucs)}/{len(LABEL_COLS)} computable labels: \"\n            f\"{np.mean(list(aucs.values())):.3f}\")\n    else:\n        log(\"no label had both classes present in the validation split -- \"\n            \"can't compute AUC (expected with only ~12 val studies)\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-13T07:10:40.751216Z","iopub.execute_input":"2026-08-13T07:10:40.751566Z","iopub.status.idle":"2026-08-13T07:10:41.215012Z","shell.execute_reply.started":"2026-08-13T07:10:40.751529Z","shell.execute_reply":"2026-08-13T07:10:41.214181Z"}},"outputs":[],"execution_count":null},{"id":"e2991261-370b-407d-910b-eb746c104bbe","cell_type":"code","source":"with timed_section(\"07_build_test_dataset\"):\n    test_df = pd.read_csv(ROOT / \"test.csv\")\n    test_series_df = pd.read_csv(ROOT / \"test_series.csv\")\n\n    X_test_list, test_ids = [], []\n    for i, row in test_df.iterrows():\n        study = row.StudyInstanceUID\n        series_uid = pick_series_for_study(study, test_series_df)\n        arr = None\n        if series_uid is not None:\n            series_dir = ROOT / \"test_series\" / study / series_uid\n            arr = load_middle_slice_array(series_dir)\n        if arr is None:\n            log(f\"  [{i + 1}/{len(test_df)}] {study[-8:]}: no usable series/slice, using blank image\")\n            arr = np.zeros((IMG_SIZE, IMG_SIZE), dtype=np.float32)\n        X_test_list.append(arr)\n        test_ids.append(study)\n        log(f\"  [{i + 1}/{len(test_df)}] {study[-8:]}: loaded\")\n\n    X_test = np.stack(X_test_list)\n    log(f\"test dataset built: {X_test.shape[0]} studies\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-13T07:10:41.217196Z","iopub.execute_input":"2026-08-13T07:10:41.217519Z","iopub.status.idle":"2026-08-13T07:10:42.898512Z","shell.execute_reply.started":"2026-08-13T07:10:41.217493Z","shell.execute_reply":"2026-08-13T07:10:42.897851Z"}},"outputs":[],"execution_count":null},{"id":"c6862ef8-e529-4740-bc87-322e09717e93","cell_type":"code","source":"# --- TEST INFERENCE -----------------------------------------------------------------\nwith timed_section(\"08_TEST_INFERENCE\"):\n    model.eval()\n    with torch.no_grad():\n        test_logits = model(to_model_input(X_test).to(device)).cpu().numpy()\n    test_probs = 1 / (1 + np.exp(-test_logits))\n    log(f\"inference done on {len(test_ids)} test studies\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-13T07:10:42.899449Z","iopub.execute_input":"2026-08-13T07:10:42.899765Z","iopub.status.idle":"2026-08-13T07:10:43.03792Z","shell.execute_reply.started":"2026-08-13T07:10:42.899733Z","shell.execute_reply":"2026-08-13T07:10:43.037057Z"}},"outputs":[],"execution_count":null},{"id":"c409054f-874e-4ef6-9deb-26c0cedd4563","cell_type":"code","source":"with timed_section(\"09_write_submission\"):\n    sub = pd.DataFrame({\"StudyInstanceUID\": test_ids})\n    for i, col in enumerate(LABEL_COLS):\n        sub[col] = test_probs[:, i]\n    sub = sub[[\"StudyInstanceUID\"] + LABEL_COLS]\n\n    out_dir = Path(\"/kaggle/working\") if Path(\"/kaggle/working\").exists() else Path(\".\")\n    out_path = out_dir / \"submission.csv\"\n    sub.to_csv(out_path, index=False)\n    log(f\"submission written: {out_path}  ({len(sub)} rows)\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-13T07:10:43.039026Z","iopub.execute_input":"2026-08-13T07:10:43.039317Z","iopub.status.idle":"2026-08-13T07:10:43.059327Z","shell.execute_reply.started":"2026-08-13T07:10:43.039293Z","shell.execute_reply":"2026-08-13T07:10:43.058368Z"}},"outputs":[],"execution_count":null},{"id":"53544c4e-04d8-4f3d-9cac-14326692c5d9","cell_type":"code","source":"log(\"=== TIMING SUMMARY ===\")\nfor name, secs in SECTION_TIMES.items():\n    log(f\"  {name:35s} {secs:8.1f}s  ({secs/60:5.2f} min)\")\nlog(f\"TOTAL notebook time: {time.time() - T0:.1f}s ({(time.time() - T0)/60:.2f} min)\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-13T07:10:43.060351Z","iopub.execute_input":"2026-08-13T07:10:43.060783Z","iopub.status.idle":"2026-08-13T07:10:43.075908Z","shell.execute_reply.started":"2026-08-13T07:10:43.060755Z","shell.execute_reply":"2026-08-13T07:10:43.075082Z"}},"outputs":[],"execution_count":null},{"id":"5117e17c-844f-4f4f-9dc9-142b183e45f8","cell_type":"code","source":"from datetime import datetime\n\nprint(datetime.now())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-13T07:10:43.076942Z","iopub.execute_input":"2026-08-13T07:10:43.077256Z","iopub.status.idle":"2026-08-13T07:10:43.082692Z","shell.execute_reply.started":"2026-08-13T07:10:43.077221Z","shell.execute_reply":"2026-08-13T07:10:43.081833Z"}},"outputs":[],"execution_count":null},{"id":"2d0bb625-4027-40a3-9885-75eff5f15334","cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}