{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.11.13"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":71549,"databundleVersionId":8561470,"sourceType":"competition"}],"dockerImageVersionId":31154,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":2289.61858,"end_time":"2025-10-14T18:03:40.534785","environment_variables":{},"exception":true,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2025-10-14T17:25:30.916205","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import pandas as pd\nimport os\n\n# Path where Kaggle mounts the competition dataset\ndata_path = \"/kaggle/input/rsna-2024-lumbar-spine-degenerative-classification\"\n\n# List what’s inside\nprint(os.listdir(data_path))\n\n# Load train.csv\ntrain_df = pd.read_csv(os.path.join(data_path, \"train.csv\"))\n\ntrain_df.shape\ntrain_df.head()","metadata":{"_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","execution":{"iopub.status.busy":"2025-11-10T14:41:19.422248Z","iopub.execute_input":"2025-11-10T14:41:19.42246Z","iopub.status.idle":"2025-11-10T14:41:19.947559Z","shell.execute_reply.started":"2025-11-10T14:41:19.422438Z","shell.execute_reply":"2025-11-10T14:41:19.946735Z"},"papermill":{"duration":1.530033,"end_time":"2025-10-14T17:25:36.836251","exception":false,"start_time":"2025-10-14T17:25:35.306218","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_label_coordinates = pd.read_csv(os.path.join(data_path, \"train_label_coordinates.csv\"))","metadata":{"execution":{"iopub.status.busy":"2025-11-10T14:41:40.187328Z","iopub.execute_input":"2025-11-10T14:41:40.187985Z","iopub.status.idle":"2025-11-10T14:41:40.291985Z","shell.execute_reply.started":"2025-11-10T14:41:40.187953Z","shell.execute_reply":"2025-11-10T14:41:40.291442Z"},"papermill":{"duration":0.098913,"end_time":"2025-10-14T17:25:36.941014","exception":false,"start_time":"2025-10-14T17:25:36.842101","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_label_coordinates","metadata":{"execution":{"iopub.status.busy":"2025-11-10T14:41:41.896765Z","iopub.execute_input":"2025-11-10T14:41:41.896999Z","iopub.status.idle":"2025-11-10T14:41:41.910373Z","shell.execute_reply.started":"2025-11-10T14:41:41.896982Z","shell.execute_reply":"2025-11-10T14:41:41.909603Z"},"papermill":{"duration":0.018557,"end_time":"2025-10-14T17:25:36.964543","exception":false,"start_time":"2025-10-14T17:25:36.945986","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\ndata_path = \"/kaggle/input/rsna-2024-lumbar-spine-degenerative-classification\"\n\n# list a few study folders\nprint(os.listdir(os.path.join(data_path, \"train_images\"))[:5])\n","metadata":{"execution":{"iopub.status.busy":"2025-11-10T14:41:43.306137Z","iopub.execute_input":"2025-11-10T14:41:43.306798Z","iopub.status.idle":"2025-11-10T14:41:43.339379Z","shell.execute_reply.started":"2025-11-10T14:41:43.306774Z","shell.execute_reply":"2025-11-10T14:41:43.338804Z"},"papermill":{"duration":0.036212,"end_time":"2025-10-14T17:25:37.005614","exception":false,"start_time":"2025-10-14T17:25:36.969402","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"study_id = \"4003253\"\nstudy_path = os.path.join(data_path, \"train_images\", study_id)\n\nprint(\"Series inside study:\", os.listdir(study_path)[:5])\n","metadata":{"execution":{"iopub.status.busy":"2025-11-10T14:41:44.339253Z","iopub.execute_input":"2025-11-10T14:41:44.339804Z","iopub.status.idle":"2025-11-10T14:41:44.351489Z","shell.execute_reply.started":"2025-11-10T14:41:44.339779Z","shell.execute_reply":"2025-11-10T14:41:44.350943Z"},"papermill":{"duration":0.012998,"end_time":"2025-10-14T17:25:37.023267","exception":false,"start_time":"2025-10-14T17:25:37.010269","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"series_id = os.listdir(study_path)[0]  # take first series\nseries_path = os.path.join(study_path, series_id)\n\nprint(\"DICOM slices inside series:\", os.listdir(series_path)[:5])\n","metadata":{"execution":{"iopub.status.busy":"2025-11-10T14:41:45.378349Z","iopub.execute_input":"2025-11-10T14:41:45.378782Z","iopub.status.idle":"2025-11-10T14:41:45.393875Z","shell.execute_reply.started":"2025-11-10T14:41:45.37876Z","shell.execute_reply":"2025-11-10T14:41:45.393349Z"},"papermill":{"duration":0.019023,"end_time":"2025-10-14T17:25:37.046916","exception":false,"start_time":"2025-10-14T17:25:37.027893","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pydicom\nimport matplotlib.pyplot as plt\n\ndcm_file = os.path.join(series_path, os.listdir(series_path)[0])  # first slice\ndcm = pydicom.dcmread(dcm_file)\n\nprint(dcm)  # metadata\nplt.imshow(dcm.pixel_array, cmap='gray')\nplt.axis('off')\nplt.show()\n","metadata":{"execution":{"iopub.status.busy":"2025-11-10T14:41:46.524423Z","iopub.execute_input":"2025-11-10T14:41:46.524961Z","iopub.status.idle":"2025-11-10T14:41:47.266562Z","shell.execute_reply.started":"2025-11-10T14:41:46.524935Z","shell.execute_reply":"2025-11-10T14:41:47.265781Z"},"papermill":{"duration":0.816957,"end_time":"2025-10-14T17:25:37.868624","exception":false,"start_time":"2025-10-14T17:25:37.051667","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pydicom\nimport matplotlib.pyplot as plt\nimport os\n\n# Pick one study and series (from your earlier code)\nstudy_id = \"4003253\"\nseries_id = os.listdir(os.path.join(data_path, \"train_images\", study_id))[0]\nseries_path = os.path.join(data_path, \"train_images\", study_id, series_id)\n\n# Get all slices in this series and sort by filename (important!)\ndcm_files = sorted(os.listdir(series_path))\n\n# Pick 5 slices spread across the series\nsample_slices = [dcm_files[i] for i in [0, len(dcm_files)//4, len(dcm_files)//2, 3*len(dcm_files)//4, -1]]\n\n# Plot them\nfig, axes = plt.subplots(1, 5, figsize=(20, 5))\n\nfor ax, fname in zip(axes, sample_slices):\n    dcm_path = os.path.join(series_path, fname)\n    dcm = pydicom.dcmread(dcm_path)\n    ax.imshow(dcm.pixel_array, cmap='gray')\n    ax.set_title(fname)\n    ax.axis('off')\n\nplt.show()\n","metadata":{"execution":{"iopub.status.busy":"2025-11-10T14:41:49.714415Z","iopub.execute_input":"2025-11-10T14:41:49.715055Z","iopub.status.idle":"2025-11-10T14:41:50.22505Z","shell.execute_reply.started":"2025-11-10T14:41:49.715026Z","shell.execute_reply":"2025-11-10T14:41:50.224358Z"},"papermill":{"duration":0.47316,"end_time":"2025-10-14T17:25:38.35037","exception":false,"start_time":"2025-10-14T17:25:37.87721","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport os\n\ncoords_path = os.path.join(data_path, \"train_label_coordinates.csv\")\ncoords_df = pd.read_csv(coords_path)\n\nprint(coords_df.shape)\nprint(coords_df.head())\n","metadata":{"execution":{"iopub.status.busy":"2025-11-10T14:41:51.980584Z","iopub.execute_input":"2025-11-10T14:41:51.981147Z","iopub.status.idle":"2025-11-10T14:41:52.046323Z","shell.execute_reply.started":"2025-11-10T14:41:51.981122Z","shell.execute_reply":"2025-11-10T14:41:52.045684Z"},"papermill":{"duration":0.077871,"end_time":"2025-10-14T17:25:38.443109","exception":false,"start_time":"2025-10-14T17:25:38.365238","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pydicom\nimport matplotlib.pyplot as plt\nimport os\n\n# Pick one row from coords_df\nrow = coords_df.iloc[0]\n\nstudy_id = str(row.study_id)\nseries_id = str(row.series_id)\ninstance_number = int(row.instance_number)\n\n# Build path to the series folder\nseries_path = os.path.join(data_path, \"train_images\", study_id, series_id)\n\n# Get all slices (files) in this series\ndcm_files = sorted(os.listdir(series_path))\n\n# instance_number in metadata starts from 1, so adjust index\ndcm_file = os.path.join(series_path, dcm_files[instance_number - 1])\n\n# Load the DICOM slice\ndcm = pydicom.dcmread(dcm_file)\nimg = dcm.pixel_array\n\n# Plot image with coordinate marked\nplt.figure(figsize=(6,6))\nplt.imshow(img, cmap='gray')\nplt.scatter(row.x, row.y, c='red', s=40, label=f\"{row.condition} {row.level}\")\nplt.legend()\nplt.axis('off')\nplt.show()\n","metadata":{"execution":{"iopub.status.busy":"2025-11-10T14:41:53.983962Z","iopub.execute_input":"2025-11-10T14:41:53.984604Z","iopub.status.idle":"2025-11-10T14:41:54.190265Z","shell.execute_reply.started":"2025-11-10T14:41:53.984581Z","shell.execute_reply":"2025-11-10T14:41:54.189535Z"},"papermill":{"duration":0.206345,"end_time":"2025-10-14T17:25:38.664232","exception":false,"start_time":"2025-10-14T17:25:38.457887","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pydicom\nimport matplotlib.pyplot as plt\n\n# Pick a study to visualize\nstudy_id = \"4003253\"\n\n# Filter coords for this study\nstudy_coords = coords_df[coords_df.study_id == int(study_id)]\n\n# Pick one series from this study\nseries_id = str(study_coords.series_id.iloc[0])\nseries_path = os.path.join(data_path, \"train_images\", study_id, series_id)\n\n# Get all slices in this series\ndcm_files = sorted(os.listdir(series_path))\n\n# Choose a slice number that has multiple labels (for demonstration)\nslice_num = study_coords.instance_number.iloc[0]\ndcm_file = os.path.join(series_path, dcm_files[slice_num - 1])\n\n# Load the slice\ndcm = pydicom.dcmread(dcm_file)\nimg = dcm.pixel_array\n\n# Plot slice with all coordinates from this slice\nplt.figure(figsize=(7,7))\nplt.imshow(img, cmap='gray')\n\nfor _, row in study_coords[study_coords.instance_number == slice_num].iterrows():\n    plt.scatter(row.x, row.y, c='red', s=40)\n    plt.text(row.x+5, row.y+5, f\"{row.level}\", color='yellow', fontsize=9)\n\nplt.title(f\"Study {study_id} - Slice {slice_num}\")\nplt.axis('off')\nplt.show()\n","metadata":{"execution":{"iopub.status.busy":"2025-11-10T14:41:56.460662Z","iopub.execute_input":"2025-11-10T14:41:56.460897Z","iopub.status.idle":"2025-11-10T14:41:56.719514Z","shell.execute_reply.started":"2025-11-10T14:41:56.460881Z","shell.execute_reply":"2025-11-10T14:41:56.718717Z"},"papermill":{"duration":0.261099,"end_time":"2025-10-14T17:25:38.943901","exception":false,"start_time":"2025-10-14T17:25:38.682802","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_study_ids = set(train_df.study_id.astype(str))\nimage_study_ids = set(os.listdir(os.path.join(data_path, \"train_images\")))\n\nmissing_studies = train_study_ids - image_study_ids\nextra_studies = image_study_ids - train_study_ids\n\nprint(\"Missing studies in images:\", len(missing_studies))\nprint(\"Extra studies not in train.csv:\", len(extra_studies))\n","metadata":{"execution":{"iopub.status.busy":"2025-11-10T14:41:59.431898Z","iopub.execute_input":"2025-11-10T14:41:59.432454Z","iopub.status.idle":"2025-11-10T14:41:59.439394Z","shell.execute_reply.started":"2025-11-10T14:41:59.432431Z","shell.execute_reply":"2025-11-10T14:41:59.438619Z"},"papermill":{"duration":0.032721,"end_time":"2025-10-14T17:25:38.999788","exception":false,"start_time":"2025-10-14T17:25:38.967067","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport pydicom\nfrom glob import glob\nfrom tqdm import tqdm\n\nBASE_PATH = data_path\nLABEL_CSV = os.path.join(BASE_PATH, \"train_label_coordinates.csv\")\nTRAIN_DIR = os.path.join(BASE_PATH, \"train_images\")\n\n# Load labels\ndf = pd.read_csv(LABEL_CSV)\n\n# Use study_id + series_id from folder structure\nseries_index = {}\n\nprint(\"Indexing DICOM files...\")\ndicom_files = glob(os.path.join(TRAIN_DIR, \"**\", \"*.dcm\"), recursive=True)\n\nfor fp in tqdm(dicom_files):\n    try:\n        ds = pydicom.dcmread(fp, stop_before_pixels=True)\n        inst_no = int(ds.InstanceNumber)\n\n        # Extract study_id and series_id from folder path\n        parts = fp.split(os.sep)\n        study_id = parts[-3]\n        series_id = parts[-2]\n        key = (study_id, series_id)\n\n        if key not in series_index:\n            series_index[key] = set()\n        series_index[key].add(inst_no)\n    except Exception:\n        continue\n\nprint(f\"Indexed {len(series_index)} (study_id, series_id) pairs from {len(dicom_files)} DICOMs.\")\n\n# Now check labels against index\nmissing_series = []\nmissing_instances = []\n\nprint(\"Checking label references...\")\nfor idx, row in tqdm(df.iterrows(), total=len(df)):\n    study_id = str(row[\"study_id\"])\n    series_id = str(row[\"series_id\"])\n    inst_no = int(row[\"instance_number\"])\n    key = (study_id, series_id)\n\n    if key not in series_index:\n        missing_series.append((idx, study_id, series_id))\n    elif inst_no not in series_index[key]:\n        missing_instances.append((idx, study_id, series_id, inst_no))\n\nprint(f\"Total rows checked: {len(df)}\")\nprint(f\"Missing series: {len(missing_series)}\")\nprint(f\"Missing instances: {len(missing_instances)}\")\n\n# Save reports\npd.DataFrame(missing_series, columns=[\"row_index\",\"study_id\",\"series_id\"]).to_csv(\"/kaggle/working/missing_series.csv\", index=False)\npd.DataFrame(missing_instances, columns=[\"row_index\",\"study_id\",\"series_id\",\"instance_number\"]).to_csv(\"/kaggle/working/missing_instances.csv\", index=False)\n\nprint(\"Reports saved in /kaggle/working/: missing_series.csv, missing_instances.csv\")\n","metadata":{"execution":{"iopub.execute_input":"2025-10-14T17:25:39.046192Z","iopub.status.busy":"2025-10-14T17:25:39.045994Z","iopub.status.idle":"2025-10-14T17:55:56.2723Z","shell.execute_reply":"2025-10-14T17:55:56.271602Z"},"papermill":{"duration":1817.250719,"end_time":"2025-10-14T17:55:56.273605","exception":false,"start_time":"2025-10-14T17:25:39.022886","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Normalise and Cropping","metadata":{"papermill":{"duration":0.553,"end_time":"2025-10-14T17:55:57.474616","exception":false,"start_time":"2025-10-14T17:55:56.921616","status":"completed"},"tags":[]}},{"cell_type":"code","source":"import os\nimport pydicom\nimport numpy as np\nimport cv2\nfrom tqdm import tqdm\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\n# Base path (same as before)\ndata_path = \"/kaggle/input/rsna-2024-lumbar-spine-degenerative-classification\"\n\n# Parameters\nPATCH_SIZE = 128\nOUTPUT_DIR = \"/kaggle/working/patches_windowed\"\n\nos.makedirs(OUTPUT_DIR, exist_ok=True)\n\n# Load label coordinates\ncoords_df = pd.read_csv(os.path.join(data_path, \"train_label_coordinates.csv\"))\n\n# ============== Helper Functions ==============\n\ndef window_image(img, center, width):\n    \"\"\"Apply DICOM windowing to convert 16-bit -> 8-bit visible range.\"\"\"\n    img_min = center - width / 2\n    img_max = center + width / 2\n    img = np.clip(img, img_min, img_max)\n    img = (img - img_min) / (img_max - img_min)\n    img = (img * 255).astype(np.uint8)\n    return img\n\ndef safe_crop(img, center_x, center_y, size=PATCH_SIZE):\n    \"\"\"Crop square patch centered at (x,y) safely even near borders.\"\"\"\n    h, w = img.shape\n    half = size // 2\n    x1, x2 = center_x - half, center_x + half\n    y1, y2 = center_y - half, center_y + half\n    patch = np.zeros((size, size), dtype=img.dtype)\n    x1_img, x2_img = max(0, x1), min(w, x2)\n    y1_img, y2_img = max(0, y1), min(h, y2)\n    x1_patch, y1_patch = x1_img - x1, y1_img - y1\n    x2_patch, y2_patch = x1_patch + (x2_img - x1_img), y1_patch + (y2_img - y1_img)\n    patch[y1_patch:y2_patch, x1_patch:x2_patch] = img[y1_img:y2_img, x1_img:x2_img]\n    return patch\n\n# ============== Cropping Loop ==============\n\nsaved, skipped = 0, 0\nsample_patches = []  # to visualize\n\nfor idx, row in tqdm(coords_df.iterrows(), total=len(coords_df)):\n    study_id = str(row[\"study_id\"])\n    series_id = str(row[\"series_id\"])\n    inst_no = int(row[\"instance_number\"])\n    x, y = int(row[\"x\"]), int(row[\"y\"])\n\n    condition = str(row['condition']).replace(\" \", \"_\")\n    level = str(row['level']).replace(\"/\", \"-\")\n\n    series_path = os.path.join(data_path, \"train_images\", study_id, series_id)\n\n    try:\n        dcm_file = os.path.join(series_path, f\"{inst_no}.dcm\")\n        dcm = pydicom.dcmread(dcm_file)\n        img = dcm.pixel_array.astype(np.float32)\n\n        # Apply DICOM windowing\n        wc = float(getattr(dcm, \"WindowCenter\", 300))\n        ww = float(getattr(dcm, \"WindowWidth\", 600))\n        img = window_image(img, wc, ww)\n\n        patch = safe_crop(img, x, y, PATCH_SIZE)\n\n        # Skip if patch is nearly blank (to avoid pure black)\n        if np.mean(patch) < 5:\n            skipped += 1\n            continue\n\n        out_path = os.path.join(OUTPUT_DIR, f\"{study_id}_{series_id}_{inst_no}_{condition}_{level}.png\")\n        ok = cv2.imwrite(out_path, patch)\n\n        if ok:\n            saved += 1\n            # collect a few random samples for preview\n            if len(sample_patches) < 5 and np.random.rand() < 0.001:\n                sample_patches.append((patch, f\"{condition}_{level}\"))\n        else:\n            skipped += 1\n    except Exception as e:\n        skipped += 1\n        continue\n\nprint(f\"✅ Done. Saved: {saved} patches | Skipped: {skipped}\")\nprint(f\"✅ Output dir: {OUTPUT_DIR}\")\n\n# ============== Quick Visualization ==============\n\nif sample_patches:\n    plt.figure(figsize=(15,3))\n    for i, (patch, title) in enumerate(sample_patches):\n        plt.subplot(1, len(sample_patches), i+1)\n        plt.imshow(patch, cmap='gray')\n        plt.title(title)\n        plt.axis('off')\n    plt.tight_layout()\n    plt.show()\nelse:\n    print(\"⚠️ No sample patches collected for preview. (Try increasing probability)\")\n","metadata":{"execution":{"iopub.status.busy":"2025-11-10T14:42:08.149203Z","iopub.execute_input":"2025-11-10T14:42:08.149963Z","iopub.status.idle":"2025-11-10T14:52:46.098402Z","shell.execute_reply.started":"2025-11-10T14:42:08.14994Z","shell.execute_reply":"2025-11-10T14:52:46.097519Z"},"papermill":{"duration":437.705987,"end_time":"2025-10-14T18:03:15.792745","exception":false,"start_time":"2025-10-14T17:55:58.086758","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport random\nimport cv2\nimport numpy as np\nimport matplotlib.pyplot as plt\n\nPATCH_DIR = \"/kaggle/working/patches_windowed\"\n\n# List all patches\nall_patches = os.listdir(PATCH_DIR)\nprint(f\"Total patches found: {len(all_patches)}\")\n\n# Randomly sample up to 12 patches for visualization\nsample_files = random.sample(all_patches, min(12, len(all_patches)))\n\nplt.figure(figsize=(12, 8))\nfor i, fname in enumerate(sample_files):\n    img_path = os.path.join(PATCH_DIR, fname)\n    img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)\n    plt.subplot(3, 4, i + 1)\n    plt.imshow(img, cmap='gray')\n    plt.title(fname.split(\"_\")[3])  # condition\n    plt.axis('off')\nplt.tight_layout()\nplt.show()\n\n# ---------- Black Pixel Check ----------\ndef is_black_patch(img, threshold=10, black_ratio=0.9):\n    \"\"\"\n    Returns True if more than 90% of pixels are below intensity 10.\n    \"\"\"\n    return np.mean(img < threshold) > black_ratio\n\nblack_count = 0\ncheck_limit = min(1000, len(all_patches))  # check up to 1000 randomly\n\nfor fname in random.sample(all_patches, check_limit):\n    img_path = os.path.join(PATCH_DIR, fname)\n    img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)\n    if img is not None and is_black_patch(img):\n        black_count += 1\n\nblack_ratio = black_count / check_limit * 100\nprint(f\"\\n🧩 Quality check summary:\")\nprint(f\"Checked {check_limit} random patches\")\nprint(f\"Black/empty patches: {black_count} ({black_ratio:.2f}%)\")\n\nif black_ratio > 20:\n    print(\"⚠️ Warning: Too many black patches! You may need to adjust crop or window settings.\")\nelse:\n    print(\"✅ Looks good! Most patches contain visible anatomical content.\")\n","metadata":{"execution":{"iopub.status.busy":"2025-11-10T14:54:26.725342Z","iopub.execute_input":"2025-11-10T14:54:26.726024Z","iopub.status.idle":"2025-11-10T14:54:27.929352Z","shell.execute_reply.started":"2025-11-10T14:54:26.725999Z","shell.execute_reply":"2025-11-10T14:54:27.928545Z"},"papermill":{"duration":2.022663,"end_time":"2025-10-14T18:03:18.604233","exception":false,"start_time":"2025-10-14T18:03:16.58157","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# train data preparation","metadata":{"papermill":{"duration":0.781554,"end_time":"2025-10-14T18:03:20.105959","exception":false,"start_time":"2025-10-14T18:03:19.324405","status":"completed"},"tags":[]}},{"cell_type":"code","source":"import pandas as pd\nimport os\n\n# Paths\ndata_path = \"/kaggle/input/rsna-2024-lumbar-spine-degenerative-classification\"\n\n# Load CSVs\ncoords_df = pd.read_csv(os.path.join(data_path, \"train_label_coordinates.csv\"))\ntrain_df = pd.read_csv(os.path.join(data_path, \"train.csv\"))\n\n# Define mapping from \"condition\" + \"level\" → column name in train.csv\ndef get_column_name(row):\n    cond = row[\"condition\"]\n    lvl = row[\"level\"].replace(\"/\", \"_\")\n    \n    if \"Spinal Canal\" in cond:\n        return f\"spinal_canal_stenosis_{lvl.lower()}\"\n    elif \"Left Neural\" in cond:\n        return f\"left_neural_foraminal_narrowing_{lvl.lower()}\"\n    elif \"Right Neural\" in cond:\n        return f\"right_neural_foraminal_narrowing_{lvl.lower()}\"\n    elif \"Left Subarticular\" in cond:\n        return f\"left_subarticular_stenosis_{lvl.lower()}\"\n    elif \"Right Subarticular\" in cond:\n        return f\"right_subarticular_stenosis_{lvl.lower()}\"\n    else:\n        return None\n\ncoords_df[\"target_col\"] = coords_df.apply(get_column_name, axis=1)\n\n# Merge severity from train.csv\nmerged_df = coords_df.merge(\n    train_df,\n    on=\"study_id\",\n    how=\"left\"\n)\n\n# Add actual severity labels from the right column\nmerged_df[\"severity\"] = merged_df.apply(lambda r: r[r[\"target_col\"]] if pd.notnull(r[\"target_col\"]) else None, axis=1)\n\nmerged_df = merged_df[[\"study_id\", \"series_id\", \"instance_number\", \"condition\", \"level\", \"x\", \"y\", \"severity\"]]\nmerged_df.head()\n","metadata":{"execution":{"iopub.status.busy":"2025-11-10T14:54:31.628168Z","iopub.execute_input":"2025-11-10T14:54:31.628498Z","iopub.status.idle":"2025-11-10T14:54:32.487741Z","shell.execute_reply.started":"2025-11-10T14:54:31.628474Z","shell.execute_reply":"2025-11-10T14:54:32.486944Z"},"papermill":{"duration":1.649488,"end_time":"2025-10-14T18:03:22.469967","exception":false,"start_time":"2025-10-14T18:03:20.820479","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport os\nimport pandas as pd\n\nPATCH_DIR = \"/kaggle/working/patches_windowed\"  # ✅ use your real patch folder\n\n# 1. Get filenames\npatch_files = os.listdir(PATCH_DIR)\npatch_df = pd.DataFrame({\"filename\": patch_files})\n\n# 2. Parse study_id, series_id, instance_number, condition, level\ndef parse_filename(fname):\n    parts = fname.replace(\".png\", \"\").split(\"_\")\n    study_id, series_id, instance_number = parts[:3]\n    condition = \"_\".join(parts[3:-1])  # includes 'Stenosis' or 'Narrowing'\n    level = parts[-1]                  # e.g., 'L4-L5'\n    return pd.Series([study_id, series_id, int(instance_number), condition, level])\n\npatch_df[[\"study_id\", \"series_id\", \"instance_number\", \"condition\", \"level\"]] = \\\n    patch_df[\"filename\"].apply(parse_filename)\n\n\ndef normalize_text(s):\n    return (\n        s.strip()\n         .replace(\" \", \"_\")\n         .replace(\"/\", \"_\")\n         .replace(\"-\", \"_\")\n         .replace(\"__\", \"_\")\n         .lower()\n    )\n\n# Normalize text fields\nmerged_df[\"condition_norm\"] = merged_df[\"condition\"].apply(normalize_text)\nmerged_df[\"level_norm\"] = merged_df[\"level\"].apply(normalize_text)\npatch_df[\"condition_norm\"] = patch_df[\"condition\"].apply(normalize_text)\npatch_df[\"level_norm\"] = patch_df[\"level\"].apply(normalize_text)\n\n# Ensure matching datatypes\nfor col in [\"study_id\", \"series_id\", \"instance_number\"]:\n    merged_df[col] = merged_df[col].astype(str)\n    patch_df[col] = patch_df[col].astype(str)\n\n# Merge labels\ntrain_ready = patch_df.merge(\n    merged_df[\n        [\"study_id\", \"series_id\", \"instance_number\", \"condition_norm\", \"level_norm\", \"severity\"]\n    ],\n    on=[\"study_id\", \"series_id\", \"instance_number\", \"condition_norm\", \"level_norm\"],\n    how=\"left\"\n)\n\n# Drop unmatched\ntrain_ready = train_ready.dropna(subset=[\"severity\"]).reset_index(drop=True)\n\nprint(\"✅ Patches matched correctly:\", len(train_ready))\nprint(\"🔸 Unique severity labels:\", train_ready[\"severity\"].unique())\ntrain_ready.head()","metadata":{"execution":{"iopub.status.busy":"2025-11-10T14:54:34.354655Z","iopub.execute_input":"2025-11-10T14:54:34.354922Z","iopub.status.idle":"2025-11-10T14:54:38.367169Z","shell.execute_reply.started":"2025-11-10T14:54:34.354903Z","shell.execute_reply":"2025-11-10T14:54:38.366487Z"},"papermill":{"duration":0.960627,"end_time":"2025-10-14T18:03:24.245411","exception":false,"start_time":"2025-10-14T18:03:23.284784","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom PIL import Image\n\n# Define custom dataset\nclass SpinePatchDataset(Dataset):\n    def __init__(self, dataframe, image_dir, transform=None):\n        self.dataframe = dataframe\n        self.image_dir = image_dir\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.dataframe)\n\n    def __getitem__(self, idx):\n        row = self.dataframe.iloc[idx]\n        img_path = os.path.join(self.image_dir, row[\"filename\"])\n        image = Image.open(img_path).convert(\"L\")  # Convert to grayscale\n        label = row[\"severity_encoded\"]\n\n        if self.transform:\n            image = self.transform(image)\n\n        return image, label\n\n# Define image transformations\ntransform = transforms.Compose([\n    transforms.Resize((128, 128)),           # Ensure consistent size\n    transforms.ToTensor(),                   # Convert to tensor\n    transforms.Normalize([0.5], [0.5])       # Normalize grayscale to [-1, 1]\n])\n\n# Split into train and validation\nfrom sklearn.model_selection import train_test_split\ntrain_df, val_df = train_test_split(train_ready, test_size=0.15, stratify=train_ready[\"severity_encoded\"], random_state=42)\n\n# Dataset instances\ntrain_dataset = SpinePatchDataset(train_df, PATCH_DIR, transform=transform)\nval_dataset = SpinePatchDataset(val_df, PATCH_DIR, transform=transform)\n\n# Dataloaders\ntrain_loader = DataLoader(train_dataset, batch_size=64, shuffle=True, num_workers=2)\nval_loader = DataLoader(val_dataset, batch_size=64, shuffle=False, num_workers=2)\n","metadata":{"execution":{"iopub.status.busy":"2025-11-10T14:54:41.938537Z","iopub.execute_input":"2025-11-10T14:54:41.938803Z","iopub.status.idle":"2025-11-10T14:54:49.960659Z","shell.execute_reply.started":"2025-11-10T14:54:41.938783Z","shell.execute_reply":"2025-11-10T14:54:49.959809Z"},"papermill":{"duration":12.405666,"end_time":"2025-10-14T18:03:37.432485","exception":true,"start_time":"2025-10-14T18:03:25.026819","status":"failed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Encode string labels → numeric\nseverity_map = {\n    \"Normal/Mild\": 0,\n    \"Moderate\": 1,\n    \"Severe\": 2\n}\ntrain_ready[\"severity_encoded\"] = train_ready[\"severity\"].map(severity_map)\n\nprint(train_ready[\"severity_encoded\"].value_counts())\n","metadata":{"execution":{"iopub.status.busy":"2025-11-10T14:54:38.36826Z","iopub.execute_input":"2025-11-10T14:54:38.368508Z","iopub.status.idle":"2025-11-10T14:54:38.382318Z","shell.execute_reply.started":"2025-11-10T14:54:38.368489Z","shell.execute_reply":"2025-11-10T14:54:38.38163Z"},"papermill":{"duration":null,"end_time":null,"exception":null,"start_time":null,"status":"pending"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# MobileNetV2 demo overfit wala\n","metadata":{"papermill":{"duration":null,"end_time":null,"exception":null,"start_time":null,"status":"pending"},"tags":[]}},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torchvision import models\nfrom tqdm import tqdm\nfrom sklearn.utils.class_weight import compute_class_weight\nfrom sklearn.metrics import (\n    confusion_matrix, ConfusionMatrixDisplay,\n    precision_recall_fscore_support, roc_auc_score, roc_curve, auc\n)\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom itertools import cycle\n\n# ======================================\n# 1️⃣ Device setup\n# ======================================\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Using device:\", device)\n\n# ======================================\n# 2️⃣ Load pretrained MobileNetV2\n# ======================================\nmodel = models.mobilenet_v2(pretrained=True)\n\n# Modify first conv layer for grayscale input (1 channel)\nmodel.features[0][0] = nn.Conv2d(1, 32, kernel_size=3, stride=2, padding=1, bias=False)\n\n# Replace final classification layer\nnum_features = model.classifier[1].in_features\nmodel.classifier[1] = nn.Linear(num_features, 3)  # 3 classes\n\nmodel = model.to(device)\n\n# ======================================\n# 3️⃣ Compute Class Weights\n# ======================================\nclasses = np.unique(train_ready[\"severity_encoded\"])\nclass_weights = compute_class_weight(\n    class_weight='balanced',\n    classes=classes,\n    y=train_ready[\"severity_encoded\"]\n)\nclass_weights = torch.tensor(class_weights, dtype=torch.float32).to(device)\nprint(\"Class Weights:\", class_weights)\n\n# ======================================\n# 4️⃣ Define Loss, Optimizer & Scheduler\n# ======================================\ncriterion = nn.CrossEntropyLoss(weight=class_weights)\noptimizer = optim.Adam(model.parameters(), lr=1e-4)\nscheduler = optim.lr_scheduler.ReduceLROnPlateau(\n    optimizer, mode='min', patience=2, factor=0.5, verbose=True\n)\n\n# ======================================\n# 5️⃣ Training Setup\n# ======================================\nnum_epochs = 9\ntrain_losses, val_losses, train_accs, val_accs = [], [], [], []\n\n# ======================================\n# 6️⃣ Training Loop\n# ======================================\nfor epoch in range(num_epochs):\n    # ---- Training ----\n    model.train()\n    train_loss, correct, total = 0, 0, 0\n    for images, labels in tqdm(train_loader, desc=f\"Epoch {epoch+1}/{num_epochs} - Training\"):\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        train_loss += loss.item()\n        _, predicted = outputs.max(1)\n        total += labels.size(0)\n        correct += predicted.eq(labels).sum().item()\n\n    train_acc = 100. * correct / total\n    avg_train_loss = train_loss / len(train_loader)\n\n    # ---- Validation ----\n    model.eval()\n    val_loss, val_correct, val_total = 0, 0, 0\n    all_preds, all_labels, all_probs = [], [], []\n    with torch.no_grad():\n        for images, labels in val_loader:\n            images, labels = images.to(device), labels.to(device)\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n\n            val_loss += loss.item()\n            probs = torch.softmax(outputs, dim=1)\n            _, predicted = torch.max(probs, 1)\n\n            val_total += labels.size(0)\n            val_correct += predicted.eq(labels).sum().item()\n\n            all_preds.extend(predicted.cpu().numpy())\n            all_labels.extend(labels.cpu().numpy())\n            all_probs.extend(probs.cpu().numpy())\n\n    val_acc = 100. * val_correct / val_total\n    avg_val_loss = val_loss / len(val_loader)\n\n    # ---- Save Metrics ----\n    train_losses.append(avg_train_loss)\n    val_losses.append(avg_val_loss)\n    train_accs.append(train_acc)\n    val_accs.append(val_acc)\n\n    print(f\"Epoch [{epoch+1}/{num_epochs}] \"\n          f\"Train Loss: {avg_train_loss:.4f} | \"\n          f\"Train Acc: {train_acc:.2f}% | \"\n          f\"Val Loss: {avg_val_loss:.4f} | \"\n          f\"Val Acc: {val_acc:.2f}%\")\n\n    scheduler.step(avg_val_loss)\n\n# ======================================\n# 7️⃣ Plot Final Graphs\n# ======================================\nplt.figure(figsize=(12,5))\n\n# Loss Plot\nplt.subplot(1,2,1)\nplt.plot(train_losses, label='Train Loss', marker='o')\nplt.plot(val_losses, label='Val Loss', marker='o')\nplt.title('Loss over Epochs')\nplt.xlabel('Epoch')\nplt.ylabel('Loss')\nplt.legend()\nplt.grid(True)\n\n# Accuracy Plot\nplt.subplot(1,2,2)\nplt.plot(train_accs, label='Train Acc', marker='o')\nplt.plot(val_accs, label='Val Acc', marker='o')\nplt.title('Accuracy over Epochs')\nplt.xlabel('Epoch')\nplt.ylabel('Accuracy (%)')\nplt.legend()\nplt.grid(True)\n\nplt.tight_layout()\nplt.show()\n\n# ======================================\n# 8️⃣ Confusion Matrix + Metrics\n# ======================================\ncm = confusion_matrix(all_labels, all_preds, labels=classes)\ndisp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=classes)\ndisp.plot(cmap=plt.cm.Blues)\nplt.title(\"Confusion Matrix - Validation Set\")\nplt.show()\n\n# ---- Precision, Recall, F1 ----\nprec, rec, f1, _ = precision_recall_fscore_support(\n    all_labels, all_preds, labels=classes, average=None\n)\nmacro_prec, macro_rec, macro_f1, _ = precision_recall_fscore_support(\n    all_labels, all_preds, average='macro'\n)\n\nprint(\"\\n📊 Classification Metrics per Class:\")\nfor i, c in enumerate(classes):\n    print(f\"Class {c}: Precision={prec[i]:.3f}, Recall={rec[i]:.3f}, F1={f1[i]:.3f}\")\n\nprint(f\"\\n🔥 Macro Avg: Precision={macro_prec:.3f}, Recall={macro_rec:.3f}, F1={macro_f1:.3f}\")\n\n# ======================================\n# 9️⃣ ROC–AUC Curve (Multi-class)\n# ======================================\nall_labels_np = np.array(all_labels)\nall_probs_np = np.array(all_probs)\n\n# One-hot encode labels\nn_classes = len(classes)\ny_true = np.eye(n_classes)[all_labels_np]\n\n# Compute ROC curve and ROC area for each class\nfpr, tpr, roc_auc = {}, {}, {}\nfor i in range(n_classes):\n    fpr[i], tpr[i], _ = roc_curve(y_true[:, i], all_probs_np[:, i])\n    roc_auc[i] = auc(fpr[i], tpr[i])\n\n# Compute micro-average ROC curve and ROC area\nfpr[\"micro\"], tpr[\"micro\"], _ = roc_curve(y_true.ravel(), all_probs_np.ravel())\nroc_auc[\"micro\"] = auc(fpr[\"micro\"], tpr[\"micro\"])\n\n# Plot all ROC curves\nplt.figure(figsize=(8,6))\ncolors = cycle(['blue', 'green', 'red'])\nfor i, color in zip(range(n_classes), colors):\n    plt.plot(fpr[i], tpr[i], color=color, lw=2,\n             label=f'Class {classes[i]} (AUC = {roc_auc[i]:.2f})')\n\nplt.plot([0, 1], [0, 1], 'k--', lw=1)\nplt.xlabel('False Positive Rate')\nplt.ylabel('True Positive Rate')\nplt.title('ROC–AUC Curve (Validation Set)')\nplt.legend(loc='lower right')\nplt.grid(True)\nplt.show()\n\n# ---- Print Macro AUC ----\nmacro_auc = roc_auc_score(y_true, all_probs_np, average=\"macro\", multi_class=\"ovr\")\nprint(f\"🏆 Macro ROC–AUC: {macro_auc:.3f}\")\n\n# ======================================\n# 🔟 Save Model\n# ======================================\ntorch.save(model.state_dict(), \"/kaggle/working/mobilenetv2_spine_balanced.pth\")\nprint(\"✅ Model saved at /kaggle/working/mobilenetv2_spine_balanced.pth\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-26T12:15:41.412535Z","iopub.execute_input":"2025-10-26T12:15:41.413252Z","iopub.status.idle":"2025-10-26T12:24:03.573078Z","shell.execute_reply.started":"2025-10-26T12:15:41.413229Z","shell.execute_reply":"2025-10-26T12:24:03.572217Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Densenet121","metadata":{}},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torchvision import models\nfrom tqdm import tqdm\nfrom sklearn.metrics import (\n    confusion_matrix, ConfusionMatrixDisplay,\n    precision_recall_fscore_support, roc_curve, auc, roc_auc_score\n)\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom itertools import cycle\n\n# ======================================\n# 1️⃣ Device setup\n# ======================================\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Using device:\", device)\n\n# ======================================\n# 2️⃣ Load pretrained DenseNet121\n# ======================================\nmodel = models.densenet121(pretrained=True)\n\n# Modify first conv layer for grayscale (1 channel)\nmodel.features.conv0 = nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3, bias=False)\n\n# Replace classifier for 3 classes\nnum_features = model.classifier.in_features\nmodel.classifier = nn.Linear(num_features, 3)\nmodel = model.to(device)\n\n# ======================================\n# 🔧 Fine-Tuning Control\n# ======================================\nfine_tune_stage = 2  # 1 = freeze base, 2 = unfreeze top, 3 = full fine-tune\n\nif fine_tune_stage == 1:\n    print(\"🧊 Phase 1: Freezing all feature layers (training only classifier head)\")\n    for param in model.features.parameters():\n        param.requires_grad = False\n\nelif fine_tune_stage == 2:\n    print(\"🔥 Phase 2: Unfreezing last Dense Block and Norm layer\")\n    for name, param in model.features.named_parameters():\n        if 'denseblock4' in name or 'norm5' in name:\n            param.requires_grad = True\n        else:\n            param.requires_grad = False\n\nelif fine_tune_stage == 3:\n    print(\"🚀 Phase 3: Full Fine-Tuning (all layers trainable)\")\n    for param in model.parameters():\n        param.requires_grad = True\n\n# Verify which layers are trainable\ntrainable_params = [name for name, p in model.named_parameters() if p.requires_grad]\nprint(f\"Trainable layers: {len(trainable_params)}\")\n\n# ======================================\n# 3️⃣ Define Class Weights (Manual)\n# ======================================\nclass_weights = torch.tensor([0.5, 1.5, 3.5], dtype=torch.float32).to(device)\nprint(\"Using manual class weights:\", class_weights)\n\n# ======================================\n# 4️⃣ Define Loss, Optimizer & Scheduler\n# ======================================\ncriterion = nn.CrossEntropyLoss(weight=class_weights)\noptimizer = optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr=1e-4, weight_decay=1e-4)\nscheduler = optim.lr_scheduler.ReduceLROnPlateau(\n    optimizer, mode='min', patience=2, factor=0.5, verbose=True\n)\n\n# ======================================\n# 5️⃣ Training Setup\n# ======================================\nnum_epochs = 15\ntrain_losses, val_losses, train_accs, val_accs = [], [], [], []\n\nbest_val_loss = float('inf')\npatience = 3\ntrigger_times = 0\nbest_model_wts = None\n\n# ======================================\n# 6️⃣ Training Loop\n# ======================================\nfor epoch in range(num_epochs):\n    model.train()\n    train_loss, correct, total = 0, 0, 0\n    for images, labels in tqdm(train_loader, desc=f\"Epoch {epoch+1}/{num_epochs} - Training\"):\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        train_loss += loss.item()\n        _, predicted = outputs.max(1)\n        total += labels.size(0)\n        correct += predicted.eq(labels).sum().item()\n\n    train_acc = 100. * correct / total\n    avg_train_loss = train_loss / len(train_loader)\n\n    # ---- Validation ----\n    model.eval()\n    val_loss, val_correct, val_total = 0, 0, 0\n    all_preds, all_labels, all_probs = [], [], []\n    with torch.no_grad():\n        for images, labels in val_loader:\n            images, labels = images.to(device), labels.to(device)\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n            val_loss += loss.item()\n\n            probs = torch.softmax(outputs, dim=1)\n            _, predicted = probs.max(1)\n\n            val_total += labels.size(0)\n            val_correct += predicted.eq(labels).sum().item()\n\n            all_preds.extend(predicted.cpu().numpy())\n            all_labels.extend(labels.cpu().numpy())\n            all_probs.extend(probs.cpu().numpy())\n\n    val_acc = 100. * val_correct / val_total\n    avg_val_loss = val_loss / len(val_loader)\n\n    train_losses.append(avg_train_loss)\n    val_losses.append(avg_val_loss)\n    train_accs.append(train_acc)\n    val_accs.append(val_acc)\n\n    print(f\"Epoch [{epoch+1}/{num_epochs}] \"\n          f\"Train Loss: {avg_train_loss:.4f} | Train Acc: {train_acc:.2f}% | \"\n          f\"Val Loss: {avg_val_loss:.4f} | Val Acc: {val_acc:.2f}%\")\n\n    scheduler.step(avg_val_loss)\n\n    # ---- Early Stopping ----\n    if avg_val_loss < best_val_loss:\n        best_val_loss = avg_val_loss\n        best_model_wts = model.state_dict().copy()\n        trigger_times = 0\n        print(\"✅ Validation loss improved, saving best model weights.\")\n    else:\n        trigger_times += 1\n        print(f\"⚠️ No improvement for {trigger_times} epoch(s).\")\n        if trigger_times >= patience:\n            print(\"⏹️ Early stopping triggered.\")\n            break\n\n# ======================================\n# 7️⃣ Restore Best Model\n# ======================================\nif best_model_wts:\n    model.load_state_dict(best_model_wts)\n    print(\"🔁 Loaded best model weights (lowest validation loss).\")\n\n# ======================================\n# 8️⃣ Plot Final Graphs\n# ======================================\nplt.figure(figsize=(12,5))\nplt.subplot(1,2,1)\nplt.plot(train_losses, label='Train Loss', marker='o')\nplt.plot(val_losses, label='Val Loss', marker='o')\nplt.title('Loss over Epochs (DenseNet121)')\nplt.xlabel('Epoch')\nplt.ylabel('Loss')\nplt.legend()\nplt.grid(True)\n\nplt.subplot(1,2,2)\nplt.plot(train_accs, label='Train Acc', marker='o')\nplt.plot(val_accs, label='Val Acc', marker='o')\nplt.title('Accuracy over Epochs (DenseNet121)')\nplt.xlabel('Epoch')\nplt.ylabel('Accuracy (%)')\nplt.legend()\nplt.grid(True)\nplt.tight_layout()\nplt.show()\n\n# ======================================\n# 9️⃣ Confusion Matrix + Metrics\n# ======================================\nclasses = np.array([0, 1, 2])\ncm = confusion_matrix(all_labels, all_preds, labels=classes)\ndisp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=classes)\ndisp.plot(cmap=plt.cm.Blues)\nplt.title(\"Confusion Matrix - DenseNet121 Validation Set\")\nplt.show()\n\nprec, rec, f1, _ = precision_recall_fscore_support(all_labels, all_preds, labels=classes, average=None)\nmacro_prec, macro_rec, macro_f1, _ = precision_recall_fscore_support(all_labels, all_preds, average='macro')\n\nprint(\"\\n📊 Classification Metrics per Class:\")\nfor i, c in enumerate(classes):\n    print(f\"Class {c}: Precision={prec[i]:.3f}, Recall={rec[i]:.3f}, F1={f1[i]:.3f}\")\n\nprint(f\"\\n🔥 Macro Avg: Precision={macro_prec:.3f}, Recall={macro_rec:.3f}, F1={macro_f1:.3f}\")\n\n# ======================================\n# 🔟 ROC–AUC Curve\n# ======================================\nall_labels_np = np.array(all_labels)\nall_probs_np = np.array(all_probs)\nn_classes = len(classes)\ny_true = np.eye(n_classes)[all_labels_np]\n\nfpr, tpr, roc_auc = {}, {}, {}\nfor i in range(n_classes):\n    fpr[i], tpr[i], _ = roc_curve(y_true[:, i], all_probs_np[:, i])\n    roc_auc[i] = auc(fpr[i], tpr[i])\n\nplt.figure(figsize=(8,6))\ncolors = cycle(['blue', 'green', 'red'])\nfor i, color in zip(range(n_classes), colors):\n    plt.plot(fpr[i], tpr[i], color=color, lw=2,\n             label=f'Class {classes[i]} (AUC = {roc_auc[i]:.2f})')\n\nplt.plot([0, 1], [0, 1], 'k--', lw=1)\nplt.xlabel('False Positive Rate')\nplt.ylabel('True Positive Rate')\nplt.title('ROC–AUC Curve (DenseNet121 Validation Set)')\nplt.legend(loc='lower right')\nplt.grid(True)\nplt.show()\n\nmacro_auc = roc_auc_score(y_true, all_probs_np, average=\"macro\", multi_class=\"ovr\")\nprint(f\"🏆 Macro ROC–AUC: {macro_auc:.3f}\")\n\n# ======================================\n# 11️⃣ Save Model\n# ======================================\ntorch.save(model.state_dict(), \"/kaggle/working/densenet121_spine_balanced.pth\")\nprint(\"✅ Model saved at /kaggle/working/densenet121_spine_balanced.pth\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-26T12:36:55.741808Z","iopub.execute_input":"2025-10-26T12:36:55.742468Z","iopub.status.idle":"2025-10-26T12:42:42.502491Z","shell.execute_reply.started":"2025-10-26T12:36:55.742443Z","shell.execute_reply":"2025-10-26T12:42:42.501763Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# mobilenetv2 good one ","metadata":{}},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torchvision import models\nfrom tqdm import tqdm\nfrom sklearn.utils.class_weight import compute_class_weight\nfrom sklearn.metrics import (\n    confusion_matrix, ConfusionMatrixDisplay,\n    precision_recall_fscore_support, roc_auc_score, roc_curve, auc\n)\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom itertools import cycle\n\n# ======================================\n# 1️⃣ Device setup\n# ======================================\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Using device:\", device)\n\n# ======================================\n# 2️⃣ Load pretrained MobileNetV2\n# ======================================\nmodel = models.mobilenet_v2(pretrained=True)\n\n# Modify first conv layer for grayscale input (1 channel)\nmodel.features[0][0] = nn.Conv2d(1, 32, kernel_size=3, stride=2, padding=1, bias=False)\n\n# 🔹 Replace final classification layer with Dropout + Linear\nnum_features = model.classifier[1].in_features\nmodel.classifier = nn.Sequential(\n    nn.Dropout(0.3),\n    nn.Linear(num_features, 256),\n    nn.ReLU(),\n    nn.Dropout(0.3),\n    nn.Linear(256, 3)\n)\n\nmodel = model.to(device)\n\n# ======================================\n# 3️⃣ Compute Class Weights (with smoothing)\n# ======================================\nclasses = np.unique(train_ready[\"severity_encoded\"])\nraw_class_weights = compute_class_weight(\n    class_weight='balanced',\n    classes=classes,\n    y=train_ready[\"severity_encoded\"]\n)\n# 🔹 Slight smoothing to avoid over-penalizing rare classes\nclass_weights = np.clip(raw_class_weights, a_min=None, a_max=np.mean(raw_class_weights) * 3)\nclass_weights = torch.tensor(class_weights, dtype=torch.float32).to(device)\nprint(\"Class Weights:\", class_weights)\n\n# ======================================\n# 4️⃣ Define Loss, Optimizer & Scheduler\n# ======================================\ncriterion = nn.CrossEntropyLoss(weight=class_weights)\noptimizer = optim.Adam(model.parameters(), lr=1e-4, weight_decay=1e-5)  # 🔹 Added weight decay\nscheduler = optim.lr_scheduler.ReduceLROnPlateau(\n    optimizer, mode='min', patience=2, factor=0.5, verbose=True\n)\n\n# ======================================\n# 5️⃣ Training Setup\n# ======================================\nnum_epochs = 15\npatience = 3  # 🔹 Early stopping patience\nbest_val_loss = float('inf')\npatience_counter = 0\n\ntrain_losses, val_losses, train_accs, val_accs = [], [], [], []\n\n# ======================================\n# 6️⃣ Training Loop\n# ======================================\nfor epoch in range(num_epochs):\n    # ---- Training ----\n    model.train()\n    train_loss, correct, total = 0, 0, 0\n    for images, labels in tqdm(train_loader, desc=f\"Epoch {epoch+1}/{num_epochs} - Training\"):\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        train_loss += loss.item()\n        _, predicted = outputs.max(1)\n        total += labels.size(0)\n        correct += predicted.eq(labels).sum().item()\n\n    train_acc = 100. * correct / total\n    avg_train_loss = train_loss / len(train_loader)\n\n    # ---- Validation ----\n    model.eval()\n    val_loss, val_correct, val_total = 0, 0, 0\n    all_preds, all_labels, all_probs = [], [], []\n    with torch.no_grad():\n        for images, labels in val_loader:\n            images, labels = images.to(device), labels.to(device)\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n\n            val_loss += loss.item()\n            probs = torch.softmax(outputs, dim=1)\n            _, predicted = torch.max(probs, 1)\n\n            val_total += labels.size(0)\n            val_correct += predicted.eq(labels).sum().item()\n\n            all_preds.extend(predicted.cpu().numpy())\n            all_labels.extend(labels.cpu().numpy())\n            all_probs.extend(probs.cpu().numpy())\n\n    val_acc = 100. * val_correct / val_total\n    avg_val_loss = val_loss / len(val_loader)\n\n    # ---- Save Metrics ----\n    train_losses.append(avg_train_loss)\n    val_losses.append(avg_val_loss)\n    train_accs.append(train_acc)\n    val_accs.append(val_acc)\n\n    print(f\"Epoch [{epoch+1}/{num_epochs}] \"\n          f\"Train Loss: {avg_train_loss:.4f} | \"\n          f\"Train Acc: {train_acc:.2f}% | \"\n          f\"Val Loss: {avg_val_loss:.4f} | \"\n          f\"Val Acc: {val_acc:.2f}%\")\n\n    scheduler.step(avg_val_loss)\n\n    # ---- Early Stopping ----\n    if avg_val_loss < best_val_loss:\n        best_val_loss = avg_val_loss\n        patience_counter = 0\n        torch.save(model.state_dict(), \"best_mobilenetv2.pth\")\n    else:\n        patience_counter += 1\n        if patience_counter >= patience:\n            print(\"⏹️ Early stopping triggered!\")\n            break\n\n# ======================================\n# 7️⃣ Plot Final Graphs\n# ======================================\nplt.figure(figsize=(12,5))\n\nplt.subplot(1,2,1)\nplt.plot(train_losses, label='Train Loss', marker='o')\nplt.plot(val_losses, label='Val Loss', marker='o')\nplt.title('Loss over Epochs')\nplt.xlabel('Epoch')\nplt.ylabel('Loss')\nplt.legend()\nplt.grid(True)\n\nplt.subplot(1,2,2)\nplt.plot(train_accs, label='Train Acc', marker='o')\nplt.plot(val_accs, label='Val Acc', marker='o')\nplt.title('Accuracy over Epochs')\nplt.xlabel('Epoch')\nplt.ylabel('Accuracy (%)')\nplt.legend()\nplt.grid(True)\n\nplt.tight_layout()\nplt.show()\n\n# ======================================\n# 8️⃣ Confusion Matrix + Metrics\n# ======================================\ncm = confusion_matrix(all_labels, all_preds, labels=classes)\ndisp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=classes)\ndisp.plot(cmap=plt.cm.Blues)\nplt.title(\"Confusion Matrix - Validation Set\")\nplt.show()\n\nprec, rec, f1, _ = precision_recall_fscore_support(all_labels, all_preds, labels=classes, average=None)\nmacro_prec, macro_rec, macro_f1, _ = precision_recall_fscore_support(all_labels, all_preds, average='macro')\n\nprint(\"\\n📊 Classification Metrics per Class:\")\nfor i, c in enumerate(classes):\n    print(f\"Class {c}: Precision={prec[i]:.3f}, Recall={rec[i]:.3f}, F1={f1[i]:.3f}\")\n\nprint(f\"\\n🔥 Macro Avg: Precision={macro_prec:.3f}, Recall={macro_rec:.3f}, F1={macro_f1:.3f}\")\n\n# ======================================\n# 9️⃣ ROC–AUC Curve (Multi-class)\n# ======================================\nall_labels_np = np.array(all_labels)\nall_probs_np = np.array(all_probs)\n\nn_classes = len(classes)\ny_true = np.eye(n_classes)[all_labels_np]\n\nfpr, tpr, roc_auc = {}, {}, {}\nfor i in range(n_classes):\n    fpr[i], tpr[i], _ = roc_curve(y_true[:, i], all_probs_np[:, i])\n    roc_auc[i] = auc(fpr[i], tpr[i])\n\nfpr[\"micro\"], tpr[\"micro\"], _ = roc_curve(y_true.ravel(), all_probs_np.ravel())\nroc_auc[\"micro\"] = auc(fpr[\"micro\"], tpr[\"micro\"])\n\nplt.figure(figsize=(8,6))\ncolors = cycle(['blue', 'green', 'red'])\nfor i, color in zip(range(n_classes), colors):\n    plt.plot(fpr[i], tpr[i], color=color, lw=2,\n             label=f'Class {classes[i]} (AUC = {roc_auc[i]:.2f})')\n\nplt.plot([0, 1], [0, 1], 'k--', lw=1)\nplt.xlabel('False Positive Rate')\nplt.ylabel('True Positive Rate')\nplt.title('ROC–AUC Curve (Validation Set)')\nplt.legend(loc='lower right')\nplt.grid(True)\nplt.show()\n\nmacro_auc = roc_auc_score(y_true, all_probs_np, average=\"macro\", multi_class=\"ovr\")\nprint(f\"🏆 Macro ROC–AUC: {macro_auc:.3f}\")\n\n# ======================================\n# 🔟 Save Best Model\n# ======================================\nprint(\"✅ Best model saved as 'best_mobilenetv2.pth'\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T14:54:59.762275Z","iopub.execute_input":"2025-11-10T14:54:59.762737Z","iopub.status.idle":"2025-11-10T14:59:40.772598Z","shell.execute_reply.started":"2025-11-10T14:54:59.762714Z","shell.execute_reply":"2025-11-10T14:59:40.771922Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torchvision import models\nfrom tqdm import tqdm\nfrom sklearn.utils.class_weight import compute_class_weight\nfrom sklearn.metrics import (\n    confusion_matrix, ConfusionMatrixDisplay,\n    precision_recall_fscore_support, roc_auc_score, roc_curve, auc\n)\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom itertools import cycle\n\n# ======================================\n# 1️⃣ Device setup\n# ======================================\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Using device:\", device)\n\n# ======================================\n# 2️⃣ Load pretrained DenseNet169\n# ======================================\nmodel = models.densenet169(pretrained=True)\n\n# Modify first conv layer for grayscale input (1 channel)\nmodel.features.conv0 = nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3, bias=False)\n\n# 🔹 Replace classifier with Dropout + Linear\nnum_features = model.classifier.in_features\nmodel.classifier = nn.Sequential(\n    nn.Dropout(0.3),\n    nn.Linear(num_features, 256),\n    nn.ReLU(),\n    nn.Dropout(0.3),\n    nn.Linear(256, 3)  # 3 classes\n)\n\nmodel = model.to(device)\n\n# ======================================\n# 3️⃣ Compute Class Weights (with smoothing)\n# ======================================\nclasses = np.unique(train_ready[\"severity_encoded\"])\nraw_class_weights = compute_class_weight(\n    class_weight='balanced',\n    classes=classes,\n    y=train_ready[\"severity_encoded\"]\n)\nclass_weights = np.clip(raw_class_weights, a_min=None, a_max=np.mean(raw_class_weights) * 3)\nclass_weights = torch.tensor(class_weights, dtype=torch.float32).to(device)\nprint(\"Class Weights:\", class_weights)\n\n# ======================================\n# 4️⃣ Define Loss, Optimizer & Scheduler\n# ======================================\ncriterion = nn.CrossEntropyLoss(weight=class_weights)\noptimizer = optim.Adam(model.parameters(), lr=1e-4, weight_decay=1e-5)  # weight decay for regularization\nscheduler = optim.lr_scheduler.ReduceLROnPlateau(\n    optimizer, mode='min', patience=2, factor=0.5, verbose=True\n)\n\n# ======================================\n# 5️⃣ Training Setup\n# ======================================\nnum_epochs = 15\npatience = 3  # Early stopping patience\nbest_val_loss = float('inf')\npatience_counter = 0\n\ntrain_losses, val_losses, train_accs, val_accs = [], [], [], []\n\n# ======================================\n# 6️⃣ Training Loop\n# ======================================\nfor epoch in range(num_epochs):\n    # ---- Training ----\n    model.train()\n    train_loss, correct, total = 0, 0, 0\n    for images, labels in tqdm(train_loader, desc=f\"Epoch {epoch+1}/{num_epochs} - Training\"):\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        train_loss += loss.item()\n        _, predicted = outputs.max(1)\n        total += labels.size(0)\n        correct += predicted.eq(labels).sum().item()\n\n    train_acc = 100. * correct / total\n    avg_train_loss = train_loss / len(train_loader)\n\n    # ---- Validation ----\n    model.eval()\n    val_loss, val_correct, val_total = 0, 0, 0\n    all_preds, all_labels, all_probs = [], [], []\n    with torch.no_grad():\n        for images, labels in val_loader:\n            images, labels = images.to(device), labels.to(device)\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n\n            val_loss += loss.item()\n            probs = torch.softmax(outputs, dim=1)\n            _, predicted = torch.max(probs, 1)\n\n            val_total += labels.size(0)\n            val_correct += predicted.eq(labels).sum().item()\n\n            all_preds.extend(predicted.cpu().numpy())\n            all_labels.extend(labels.cpu().numpy())\n            all_probs.extend(probs.cpu().numpy())\n\n    val_acc = 100. * val_correct / val_total\n    avg_val_loss = val_loss / len(val_loader)\n\n    # ---- Save Metrics ----\n    train_losses.append(avg_train_loss)\n    val_losses.append(avg_val_loss)\n    train_accs.append(train_acc)\n    val_accs.append(val_acc)\n\n    print(f\"Epoch [{epoch+1}/{num_epochs}] \"\n          f\"Train Loss: {avg_train_loss:.4f} | \"\n          f\"Train Acc: {train_acc:.2f}% | \"\n          f\"Val Loss: {avg_val_loss:.4f} | \"\n          f\"Val Acc: {val_acc:.2f}%\")\n\n    scheduler.step(avg_val_loss)\n\n    # ---- Early Stopping ----\n    if avg_val_loss < best_val_loss:\n        best_val_loss = avg_val_loss\n        patience_counter = 0\n        torch.save(model.state_dict(), \"best_densenet169.pth\")\n    else:\n        patience_counter += 1\n        if patience_counter >= patience:\n            print(\"⏹️ Early stopping triggered!\")\n            break\n\n# ======================================\n# 7️⃣ Plot Final Graphs\n# ======================================\nplt.figure(figsize=(12,5))\n\nplt.subplot(1,2,1)\nplt.plot(train_losses, label='Train Loss', marker='o')\nplt.plot(val_losses, label='Val Loss', marker='o')\nplt.title('Loss over Epochs')\nplt.xlabel('Epoch')\nplt.ylabel('Loss')\nplt.legend()\nplt.grid(True)\n\nplt.subplot(1,2,2)\nplt.plot(train_accs, label='Train Acc', marker='o')\nplt.plot(val_accs, label='Val Acc', marker='o')\nplt.title('Accuracy over Epochs')\nplt.xlabel('Epoch')\nplt.ylabel('Accuracy (%)')\nplt.legend()\nplt.grid(True)\n\nplt.tight_layout()\nplt.show()\n\n# ======================================\n# 8️⃣ Confusion Matrix + Metrics\n# ======================================\ncm = confusion_matrix(all_labels, all_preds, labels=classes)\ndisp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=classes)\ndisp.plot(cmap=plt.cm.Blues)\nplt.title(\"Confusion Matrix - Validation Set\")\nplt.show()\n\nprec, rec, f1, _ = precision_recall_fscore_support(all_labels, all_preds, labels=classes, average=None)\nmacro_prec, macro_rec, macro_f1, _ = precision_recall_fscore_support(all_labels, all_preds, average='macro')\n\nprint(\"\\n📊 Classification Metrics per Class:\")\nfor i, c in enumerate(classes):\n    print(f\"Class {c}: Precision={prec[i]:.3f}, Recall={rec[i]:.3f}, F1={f1[i]:.3f}\")\n\nprint(f\"\\n🔥 Macro Avg: Precision={macro_prec:.3f}, Recall={macro_rec:.3f}, F1={macro_f1:.3f}\")\n\n# ======================================\n# 9️⃣ ROC–AUC Curve (Multi-class)\n# ======================================\nall_labels_np = np.array(all_labels)\nall_probs_np = np.array(all_probs)\n\nn_classes = len(classes)\ny_true = np.eye(n_classes)[all_labels_np]\n\nfpr, tpr, roc_auc = {}, {}, {}\nfor i in range(n_classes):\n    fpr[i], tpr[i], _ = roc_curve(y_true[:, i], all_probs_np[:, i])\n    roc_auc[i] = auc(fpr[i], tpr[i])\n\nfpr[\"micro\"], tpr[\"micro\"], _ = roc_curve(y_true.ravel(), all_probs_np.ravel())\nroc_auc[\"micro\"] = auc(fpr[\"micro\"], tpr[\"micro\"])\n\nplt.figure(figsize=(8,6))\ncolors = cycle(['blue', 'green', 'red'])\nfor i, color in zip(range(n_classes), colors):\n    plt.plot(fpr[i], tpr[i], color=color, lw=2,\n             label=f'Class {classes[i]} (AUC = {roc_auc[i]:.2f})')\n\nplt.plot([0, 1], [0, 1], 'k--', lw=1)\nplt.xlabel('False Positive Rate')\nplt.ylabel('True Positive Rate')\nplt.title('ROC–AUC Curve (Validation Set)')\nplt.legend(loc='lower right')\nplt.grid(True)\nplt.show()\n\nmacro_auc = roc_auc_score(y_true, all_probs_np, average=\"macro\", multi_class=\"ovr\")\nprint(f\"🏆 Macro ROC–AUC: {macro_auc:.3f}\")\n\n# ======================================\n# 🔟 Save Best Model\n# ======================================\nprint(\"✅ Best model saved as 'best_densenet169.pth'\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-26T12:54:26.207469Z","iopub.execute_input":"2025-10-26T12:54:26.20781Z","iopub.status.idle":"2025-10-26T13:13:32.464977Z","shell.execute_reply.started":"2025-10-26T12:54:26.207784Z","shell.execute_reply":"2025-10-26T13:13:32.464237Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader, random_split\nfrom torchvision import transforms, models\nfrom PIL import Image\nimport pandas as pd\nfrom sklearn.preprocessing import LabelEncoder\nfrom sklearn.metrics import confusion_matrix, classification_report, roc_curve, auc\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport numpy as np\n\n# --------------------------\n# 1. Dataset\n# --------------------------\nclass SpineDataset(Dataset):\n    def __init__(self, df, patch_dir, transform=None):\n        self.df = df.reset_index(drop=True)\n        self.patch_dir = patch_dir\n        self.transform = transform\n        self.le = LabelEncoder()\n        self.df['severity_enc'] = self.le.fit_transform(self.df['severity'])\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_path = os.path.join(self.patch_dir, row['filename'])\n        img = Image.open(img_path).convert(\"RGB\")\n        if self.transform:\n            img = self.transform(img)\n        label = row['severity_enc']\n        return img, label\n\n# --------------------------\n# 2. Settings\n# --------------------------\nPATCH_DIR = \"/kaggle/working/patches_windowed\"\nBATCH_SIZE = 32\nNUM_EPOCHS = 20\nLR = 1e-4\nPATIENCE = 5  # early stopping\n\n# Transforms\ntrain_transform = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.RandomHorizontalFlip(),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485,0.456,0.406], [0.229,0.224,0.225])\n])\n\nval_transform = transforms.Compose([\n    transforms.Resize((224,224)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485,0.456,0.406], [0.229,0.224,0.225])\n])\n\n# Dataset\ndataset = SpineDataset(train_ready, PATCH_DIR, transform=train_transform)\n\n# Train/Val Split\nval_size = int(0.2 * len(dataset))\ntrain_size = len(dataset) - val_size\ntrain_ds, val_ds = random_split(dataset, [train_size, val_size])\n\ntrain_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True)\nval_loader = DataLoader(val_ds, batch_size=BATCH_SIZE, shuffle=False)\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# --------------------------\n# 3. Model\n# --------------------------\nnum_classes = len(dataset.df['severity'].unique())\nmodel = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2)\nmodel.fc = nn.Linear(model.fc.in_features, num_classes)\nmodel = model.to(device)\n\n# --------------------------\n# 4. Loss with class weights\n# --------------------------\ncounts = dataset.df['severity_enc'].value_counts().sort_index().values\nclass_weights = 1.0 / counts\nclass_weights = class_weights / class_weights.sum() * num_classes\nclass_weights = torch.tensor(class_weights, dtype=torch.float32).to(device)\ncriterion = nn.CrossEntropyLoss(weight=class_weights)\n\noptimizer = optim.Adam(model.parameters(), lr=LR)\n\n# --------------------------\n# 5. Training loop with early stopping\n# --------------------------\nbest_val_loss = np.inf\npatience_counter = 0\ntrain_losses, val_losses = [], []\ntrain_accs, val_accs = [], []\n\nfor epoch in range(NUM_EPOCHS):\n    # ---- Train ----\n    model.train()\n    running_loss = 0.0\n    correct = 0\n    total = 0\n    for batch_idx, (images, labels) in enumerate(train_loader):\n        images, labels = images.to(device), labels.to(device)\n        optimizer.zero_grad()\n        outputs = model(images)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n\n        running_loss += loss.item() * images.size(0)\n        _, preds = torch.max(outputs, 1)\n        correct += (preds == labels).sum().item()\n        total += labels.size(0)\n\n        if batch_idx % 50 == 0:\n            print(f\"Epoch [{epoch+1}/{NUM_EPOCHS}] Batch [{batch_idx}/{len(train_loader)}] Loss: {loss.item():.4f}\")\n\n    epoch_loss = running_loss / total\n    epoch_acc = correct / total\n    train_losses.append(epoch_loss)\n    train_accs.append(epoch_acc)\n\n    # ---- Validation ----\n    model.eval()\n    val_loss = 0.0\n    correct_val = 0\n    total_val = 0\n    all_preds = []\n    all_labels = []\n    with torch.no_grad():\n        for images, labels in val_loader:\n            images, labels = images.to(device), labels.to(device)\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n            val_loss += loss.item() * images.size(0)\n            _, preds = torch.max(outputs, 1)\n            correct_val += (preds == labels).sum().item()\n            total_val += labels.size(0)\n            all_preds.extend(preds.cpu().numpy())\n            all_labels.extend(labels.cpu().numpy())\n\n    val_epoch_loss = val_loss / total_val\n    val_epoch_acc = correct_val / total_val\n    val_losses.append(val_epoch_loss)\n    val_accs.append(val_epoch_acc)\n\n    print(f\"Epoch [{epoch+1}/{NUM_EPOCHS}] Train Loss: {epoch_loss:.4f} Train Acc: {epoch_acc:.4f} | Val Loss: {val_epoch_loss:.4f} Val Acc: {val_epoch_acc:.4f}\")\n\n    # Early stopping\n    if val_epoch_loss < best_val_loss:\n        best_val_loss = val_epoch_loss\n        torch.save(model.state_dict(), \"best_model.pth\")\n        patience_counter = 0\n    else:\n        patience_counter += 1\n        if patience_counter >= PATIENCE:\n            print(\"Early stopping triggered!\")\n            break\n\n# --------------------------\n# 6. Load best model\n# --------------------------\nmodel.load_state_dict(torch.load(\"best_model.pth\"))\n\n# --------------------------\n# 7. Metrics & Plots\n# --------------------------\nfrom sklearn.metrics import roc_auc_score, roc_curve\n\n# Confusion Matrix\ncm = confusion_matrix(all_labels, all_preds)\nplt.figure(figsize=(6,5))\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=dataset.le.classes_, yticklabels=dataset.le.classes_)\nplt.xlabel(\"Predicted\")\nplt.ylabel(\"Actual\")\nplt.title(\"Confusion Matrix\")\nplt.show()\n\n# Classification report\nprint(classification_report(all_labels, all_preds, target_names=dataset.le.classes_))\n\n# Train vs Val Loss/Acc\nplt.figure(figsize=(12,5))\nplt.subplot(1,2,1)\nplt.plot(train_losses, label='Train Loss')\nplt.plot(val_losses, label='Val Loss')\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Loss\")\nplt.legend()\nplt.title(\"Loss per Epoch\")\n\nplt.subplot(1,2,2)\nplt.plot(train_accs, label='Train Acc')\nplt.plot(val_accs, label='Val Acc')\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Accuracy\")\nplt.legend()\nplt.title(\"Accuracy per Epoch\")\nplt.show()\n\n# ROC Curve (one-vs-rest)\nfrom sklearn.preprocessing import label_binarize\nall_labels_bin = label_binarize(all_labels, classes=range(num_classes))\nall_preds_prob = []\n\nmodel.eval()\nwith torch.no_grad():\n    for images, labels in val_loader:\n        images = images.to(device)\n        outputs = model(images)\n        all_preds_prob.append(torch.softmax(outputs, 1).cpu().numpy())\nall_preds_prob = np.vstack(all_preds_prob)\n\nplt.figure(figsize=(7,6))\nfor i in range(num_classes):\n    fpr, tpr, _ = roc_curve(all_labels_bin[:,i], all_preds_prob[:,i])\n    plt.plot(fpr, tpr, label=f\"{dataset.le.classes_[i]} (AUC={auc(fpr,tpr):.2f})\")\nplt.plot([0,1],[0,1],'k--')\nplt.xlabel(\"False Positive Rate\")\nplt.ylabel(\"True Positive Rate\")\nplt.title(\"ROC Curve\")\nplt.legend()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-26T13:17:05.698961Z","iopub.execute_input":"2025-10-26T13:17:05.69925Z","iopub.status.idle":"2025-10-26T14:24:26.20478Z","shell.execute_reply.started":"2025-10-26T13:17:05.699231Z","shell.execute_reply":"2025-10-26T14:24:26.203983Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import confusion_matrix, classification_report, roc_curve, auc\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom PIL import Image\n\nimport timm\nfrom tqdm import tqdm\nfrom sklearn.preprocessing import label_binarize\n\n\n# -------------------------\n# Defaults / Hyperparams\n# -------------------------\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nLR = 1e-4\nNUM_EPOCHS = 10\nPATIENCE = 3\nBATCH_SIZE = 32\nMODEL_SAVE_PATH = \"/kaggle/working/best_model_effb0.pth2\"\n\n# -------------------------\n# 1️⃣ Dataset & Loader\n# -------------------------\nclass SpineDataset(Dataset):\n    def __init__(self, df, image_dir, transform=None, label_map=None):\n        self.df = df.reset_index(drop=True)\n        self.image_dir = image_dir\n        if label_map is None:\n            # infer label map from unique values sorted for stable ordering\n            labels = sorted(self.df['severity'].unique().tolist())\n            self.label_map = {lab: i for i, lab in enumerate(labels)}\n            self.labels_list = labels\n        else:\n            self.label_map = label_map\n            # labels_list ordered by label_map keys sorted by value\n            self.labels_list = [k for k,v in sorted(self.label_map.items(), key=lambda x: x[1])]\n        self.transform = transform\n        \n    def __len__(self):\n        return len(self.df)\n    \n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_path = os.path.join(self.image_dir, row['filename'])\n        image = Image.open(img_path).convert('RGB')\n        label = self.label_map[row['severity']]\n        if self.transform:\n            image = self.transform(image)\n        return image, label\n\n# -------------------------\n# 2️⃣ Transforms\n# -------------------------\ntrain_transform = transforms.Compose([\n    transforms.Resize((224,224)),\n    transforms.RandomHorizontalFlip(),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])\n])\n\nval_transform = transforms.Compose([\n    transforms.Resize((224,224)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])\n])\n\n# -------------------------\n# 3️⃣ Train-Validation Split\n# -------------------------\ntrain_df, val_df = train_test_split(train_ready, test_size=0.2, stratify=train_ready['severity'], random_state=42)\n\n# Make label map from train_df so validation uses same encoding\nlabel_names = sorted(train_df['severity'].unique().tolist())\nlabel_map = {lab: i for i, lab in enumerate(label_names)}\nnum_classes = len(label_names)\n\ntrain_dataset = SpineDataset(train_df, PATCH_DIR, transform=train_transform, label_map=label_map)\nval_dataset   = SpineDataset(val_df, PATCH_DIR, transform=val_transform, label_map=label_map)\n\ntrain_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=2, pin_memory=True)\nval_loader   = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)\n\n# --------------------------\n# Prepare class weights (from training set)\n# --------------------------\n# compute counts in the same order as label_map\ncounts = np.array([ (train_df['severity'] == lab).sum() for lab in label_names ], dtype=np.float32)\n# avoid division by zero\ncounts[counts==0] = 1.0\nclass_weights = 1.0 / counts\n# normalize to sum=num_classes (optional)\nclass_weights = class_weights / class_weights.sum() * num_classes\nclass_weights = torch.tensor(class_weights, dtype=torch.float32).to(device)\n\n# --------------------------\n# 1. Model\n# --------------------------\nmodel = timm.create_model('efficientnet_b0', pretrained=True, num_classes=num_classes)\nmodel = model.to(device)\n\n# --------------------------\n# 2. Loss with class weights\n# --------------------------\ncriterion = nn.CrossEntropyLoss(weight=class_weights)\noptimizer = optim.Adam(model.parameters(), lr=LR)\n\n# --------------------------\n# 3. Training loop with tqdm\n# --------------------------\nbest_val_loss = np.inf\npatience_counter = 0\ntrain_losses, val_losses = [], []\ntrain_accs, val_accs = [], []\n\nfor epoch in range(NUM_EPOCHS):\n    # ---- Train ----\n    model.train()\n    running_loss, correct, total = 0.0, 0, 0\n    train_bar = tqdm(train_loader, desc=f\"Epoch {epoch+1}/{NUM_EPOCHS} [Train]\")\n    for images, labels in train_bar:\n        images, labels = images.to(device), labels.to(device)\n        optimizer.zero_grad()\n        outputs = model(images)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n\n        running_loss += loss.item() * images.size(0)\n        _, preds = torch.max(outputs, 1)\n        correct += (preds == labels).sum().item()\n        total += labels.size(0)\n\n        train_bar.set_postfix(loss=running_loss/total, acc=correct/total)\n\n    epoch_loss = running_loss / total\n    epoch_acc = correct / total\n    train_losses.append(epoch_loss)\n    train_accs.append(epoch_acc)\n\n    # ---- Validation ----\n    model.eval()\n    val_loss, correct_val, total_val = 0.0, 0, 0\n\n    val_bar = tqdm(val_loader, desc=f\"Epoch {epoch+1}/{NUM_EPOCHS} [Val]  \")\n    with torch.no_grad():\n        for images, labels in val_bar:\n            images, labels = images.to(device), labels.to(device)\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n\n            val_loss += loss.item() * images.size(0)\n            _, preds = torch.max(outputs, 1)\n            correct_val += (preds == labels).sum().item()\n            total_val += labels.size(0)\n\n            val_bar.set_postfix(loss=val_loss/total_val, acc=correct_val/total_val)\n\n    val_epoch_loss = val_loss / total_val\n    val_epoch_acc = correct_val / total_val\n    val_losses.append(val_epoch_loss)\n    val_accs.append(val_epoch_acc)\n\n    print(f\"\\nEpoch [{epoch+1}/{NUM_EPOCHS}] Train Loss: {epoch_loss:.4f} | Train Acc: {epoch_acc:.4f} | \"\n          f\"Val Loss: {val_epoch_loss:.4f} | Val Acc: {val_epoch_acc:.4f}\")\n\n    # Early stopping\n    if val_epoch_loss < best_val_loss:\n        best_val_loss = val_epoch_loss\n        torch.save(model.state_dict(), MODEL_SAVE_PATH)\n        patience_counter = 0\n    else:\n        patience_counter += 1\n        if patience_counter >= PATIENCE:\n            print(\"Early stopping triggered!\")\n            break\n\n# --------------------------\n# 6. Load best model (if saved)\n# --------------------------\nif os.path.exists(MODEL_SAVE_PATH):\n    model.load_state_dict(torch.load(MODEL_SAVE_PATH, map_location=device))\n    print(f\"Loaded best model from {MODEL_SAVE_PATH}\")\nelse:\n    print(\"No saved model found; using current weights.\")\n\n# --------------------------\n# 7. Final inference on val set to get predictions/probs for metrics & plots\n# --------------------------\nmodel.eval()\nall_labels = []\nall_preds = []\nall_probs = []\n\nwith torch.no_grad():\n    for images, labels in tqdm(val_loader, desc=\"Final Inference on Val\"):\n        images = images.to(device)\n        outputs = model(images)\n        probs = torch.softmax(outputs, dim=1).cpu().numpy()\n        preds = np.argmax(probs, axis=1)\n        all_probs.append(probs)\n        all_preds.extend(preds.tolist())\n        all_labels.extend(labels.numpy().tolist())\n\nall_probs = np.vstack(all_probs)\nall_labels = np.array(all_labels)\nall_preds = np.array(all_preds)\n\n# Confusion Matrix\ncm = confusion_matrix(all_labels, all_preds, labels=list(range(num_classes)))\nplt.figure(figsize=(6,5))\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues',\n            xticklabels=label_names, yticklabels=label_names)\nplt.xlabel(\"Predicted\")\nplt.ylabel(\"Actual\")\nplt.title(\"Confusion Matrix\")\nplt.show()\n\n# Classification report\nprint(classification_report(all_labels, all_preds, target_names=label_names))\n\n# Train vs Val Loss/Acc\nplt.figure(figsize=(12,5))\nplt.subplot(1,2,1)\nplt.plot(train_losses, label='Train Loss')\nplt.plot(val_losses, label='Val Loss')\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Loss\")\nplt.legend()\nplt.title(\"Loss per Epoch\")\n\nplt.subplot(1,2,2)\nplt.plot(train_accs, label='Train Acc')\nplt.plot(val_accs, label='Val Acc')\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Accuracy\")\nplt.legend()\nplt.title(\"Accuracy per Epoch\")\nplt.show()\n\n# ROC Curve (one-vs-rest)\nall_labels_bin = label_binarize(all_labels, classes=list(range(num_classes)))\n\nplt.figure(figsize=(7,6))\nfor i in range(num_classes):\n    fpr, tpr, _ = roc_curve(all_labels_bin[:,i], all_probs[:,i])\n    roc_auc = auc(fpr, tpr)\n    plt.plot(fpr, tpr, label=f\"{label_names[i]} (AUC={roc_auc:.2f})\")\nplt.plot([0,1],[0,1],'k--')\nplt.xlabel(\"False Positive Rate\")\nplt.ylabel(\"True Positive Rate\")\nplt.title(\"ROC Curve (One-vs-Rest)\")\nplt.legend()\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}