{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":71549,"databundleVersionId":8561470,"sourceType":"competition"},{"sourceId":12203343,"sourceType":"datasetVersion","datasetId":7687130},{"sourceId":12203493,"sourceType":"datasetVersion","datasetId":7687241},{"sourceId":12204854,"sourceType":"datasetVersion","datasetId":7688229},{"sourceId":12249891,"sourceType":"datasetVersion","datasetId":7718540},{"sourceId":12250412,"sourceType":"datasetVersion","datasetId":7718851}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"**RUN IT FROM HERE**","metadata":{}},{"cell_type":"code","source":"# Cell 1: Install all dependencies once\n# ⛔ Uninstall existing potentially problematic versions\n#%pip uninstall -y torch torchvision torchaudio\n\n# ✅ Install clean versions that avoid all UnpicklingErrors (torch < 2.6)\n%pip install torch<2.6.0 torchvision<0.20.0 torchaudio --upgrade\n\n# ✅ Install compatible ultralytics + common YOLO training libraries\n%pip install ultralytics==8.0.111 pandas numpy pydicom albumentations scikit-learn tqdm\n","metadata":{"trusted":true,"scrolled":true,"execution":{"iopub.status.busy":"2025-06-22T12:23:06.559451Z","iopub.execute_input":"2025-06-22T12:23:06.560306Z","iopub.status.idle":"2025-06-22T12:23:10.909057Z","shell.execute_reply.started":"2025-06-22T12:23:06.560269Z","shell.execute_reply":"2025-06-22T12:23:10.908116Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Uninstall ray to avoid YOLOv8 ray tune callback error\n!pip uninstall -y ray\n","metadata":{"trusted":true,"scrolled":true,"execution":{"iopub.status.busy":"2025-06-22T12:23:16.688822Z","iopub.execute_input":"2025-06-22T12:23:16.689491Z","iopub.status.idle":"2025-06-22T12:23:18.092031Z","shell.execute_reply.started":"2025-06-22T12:23:16.689459Z","shell.execute_reply":"2025-06-22T12:23:18.091084Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport dill\nimport os\nfrom pathlib import Path\nimport yaml\nfrom ultralytics import YOLO\n\n# Ultralytics components\nfrom ultralytics.nn.modules.block import Bottleneck, C2f, DFL, SPPF\nfrom ultralytics.nn.modules.conv import Conv, Concat\nfrom ultralytics.nn.modules.head import Detect\nfrom ultralytics.nn.tasks import DetectionModel\nfrom ultralytics.yolo.utils.tal import TaskAlignedAssigner\nfrom ultralytics.yolo.utils import IterableSimpleNamespace\nfrom ultralytics.yolo.utils.loss import BboxLoss, v8DetectionLoss\n\nimport numpy as np\n\n# Torch components\nfrom torch.nn import (\n    Conv2d, BatchNorm2d, MaxPool2d, Sequential,\n    ModuleList, SiLU, Upsample, BCEWithLogitsLoss\n)\n\ntorch.serialization.add_safe_globals([\n    # PyTorch\n    Conv2d, BatchNorm2d, MaxPool2d, Sequential,\n    ModuleList, SiLU, Upsample, BCEWithLogitsLoss, \n    BboxLoss,TaskAlignedAssigner,\n\n    np.dtype, np.float64, np.dtypes.Float64DType, np.core.multiarray.scalar,\n\n\n    # Ultralytics\n    DetectionModel,\n    Bottleneck, C2f, DFL, SPPF,\n    Conv, Concat, Detect,\n    IterableSimpleNamespace,\n    v8DetectionLoss,\n\n    # Dill (used for pickled config objects)\n    dill._dill._load_type\n])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-22T12:23:49.418098Z","iopub.execute_input":"2025-06-22T12:23:49.418430Z","iopub.status.idle":"2025-06-22T12:23:57.587988Z","shell.execute_reply.started":"2025-06-22T12:23:49.418400Z","shell.execute_reply":"2025-06-22T12:23:57.587150Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n\n# ✅ Disable WandB\nos.environ['WANDB_MODE'] = 'disabled'\nos.environ['WANDB_PROJECT'] = 'yolo_training'\n\n# ✅ Disable Ray Tune callback\nfrom ultralytics.yolo.utils import callbacks\ncallbacks.default_callbacks.pop('on_fit_epoch_end', None)\n\n# ✅ Custom global Ultralytics settings\nultralytics_settings_path = Path(\"/kaggle/working/ultralytics_custom_settings.yaml\")\nultralytics_settings_path.parent.mkdir(parents=True, exist_ok=True)\n\nif not ultralytics_settings_path.exists():\n    default_settings = {\n        \"datasets_dir\": \"/kaggle/working/yolo_cache\",  # Custom cache location\n        \"runs_dir\": \"/kaggle/working/yolo_results\"     # Output folder\n    }\n    with open(ultralytics_settings_path, 'w') as f:\n        yaml.dump(default_settings, f)\n\n# ✅ Point Ultralytics to the settings file\nos.environ[\"ULTRALYTICS_SETTINGS\"] = str(ultralytics_settings_path)\n\n# -----------------------------\n# CONFIGURATION\n# -----------------------------\nDATA_ROOT = \"/kaggle/input\"\nRESULTS_ROOT = \"/kaggle/working/yolo_results\"\nCONDITIONS = [\"spinal-stenosis\"]\nFOLDS = [0, 1]\nEPOCHS = 20\nPATIENCE = 5\nBATCH_SIZE = 8\n\n# -----------------------------\n# TRAINING LOOP\n# -----------------------------\nfor condition in CONDITIONS:\n    for fold in FOLDS:\n        data_yaml = f\"{DATA_ROOT}/{condition}/fold_{fold}/datasets/yolo_config.yaml\"\n        save_dir = Path(RESULTS_ROOT) / condition / f\"fold_{fold}\"\n        save_dir.mkdir(parents=True, exist_ok=True)\n\n        print(f\"\\n▶ Training {condition}, fold {fold}\")\n        \n        # ✅ From config — no pretrained weights\n        model = YOLO(\"yolov8n.yaml\")\n        \n        # ✅ Training\n        model.train(\n            data=data_yaml,\n            project=\"yolo_results\",\n            name=f\"{condition.replace('-', '_')}_fold_{fold}\",\n            epochs=EPOCHS,\n            patience=PATIENCE,\n            batch=BATCH_SIZE,\n            amp=False,\n            save=True,\n            exist_ok=True  # avoids errors if dir exists\n        )\n        \n        print(f\"✅ Done {condition}, fold {fold}\")","metadata":{"trusted":true,"scrolled":true,"execution":{"iopub.status.busy":"2025-06-22T12:24:01.322007Z","iopub.execute_input":"2025-06-22T12:24:01.322917Z","iopub.status.idle":"2025-06-22T14:33:17.844205Z","shell.execute_reply.started":"2025-06-22T12:24:01.322886Z","shell.execute_reply":"2025-06-22T14:33:17.840531Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ✅ Disable WandB\nos.environ['WANDB_MODE'] = 'disabled'\nos.environ['WANDB_PROJECT'] = 'yolo_training'\n\n# ✅ Disable Ray Tune callback\nfrom ultralytics.yolo.utils import callbacks\ncallbacks.default_callbacks.pop('on_fit_epoch_end', None)\n\n# ✅ Custom global Ultralytics settings\nultralytics_settings_path = Path(\"/kaggle/working/ultralytics_custom_settings.yaml\")\nultralytics_settings_path.parent.mkdir(parents=True, exist_ok=True)\n\nif not ultralytics_settings_path.exists():\n    default_settings = {\n        \"datasets_dir\": \"/kaggle/working/yolo_cache\",  # Custom cache location\n        \"runs_dir\": \"/kaggle/working/yolo_results\"     # Output folder\n    }\n    with open(ultralytics_settings_path, 'w') as f:\n        yaml.dump(default_settings, f)\n\n# ✅ Point Ultralytics to the settings file\nos.environ[\"ULTRALYTICS_SETTINGS\"] = str(ultralytics_settings_path)\n\n# -----------------------------\n# CONFIGURATION\n# -----------------------------\nDATA_ROOT = \"/kaggle/input\"\nRESULTS_ROOT = \"/kaggle/working/yolo_results\"\nCONDITIONS = [\"subarticular-stenosis\"]\nFOLDS = [0, 1]\nEPOCHS = 20\nPATIENCE = 5\nBATCH_SIZE = 8\n\n# -----------------------------\n# TRAINING LOOP\n# -----------------------------\nfor condition in CONDITIONS:\n    for fold in FOLDS:\n        data_yaml = f\"{DATA_ROOT}/{condition}/fold_{fold}/datasets/yolo_config.yaml\"\n        save_dir = Path(RESULTS_ROOT) / condition / f\"fold_{fold}\"\n        save_dir.mkdir(parents=True, exist_ok=True)\n\n        print(f\"\\n▶ Training {condition}, fold {fold}\")\n        \n        # ✅ From config — no pretrained weights\n        model = YOLO(\"yolov8n.yaml\")\n        \n        # ✅ Training\n        model.train(\n            data=data_yaml,\n            project=\"yolo_results\",\n            name=f\"{condition.replace('-', '_')}_fold_{fold}\",\n            epochs=EPOCHS,\n            patience=PATIENCE,\n            batch=BATCH_SIZE,\n            amp=False,\n            save=True,\n            exist_ok=True  # avoids errors if dir exists\n        )\n        \n        print(f\"✅ Done {condition}, fold {fold}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-22T14:41:35.721850Z","iopub.execute_input":"2025-06-22T14:41:35.722631Z","iopub.status.idle":"2025-06-22T17:13:31.894446Z","shell.execute_reply.started":"2025-06-22T14:41:35.722599Z","shell.execute_reply":"2025-06-22T17:13:31.893189Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ✅ Disable WandB\nos.environ['WANDB_MODE'] = 'disabled'\nos.environ['WANDB_PROJECT'] = 'yolo_training'\n\n# ✅ Disable Ray Tune callback\nfrom ultralytics.yolo.utils import callbacks\ncallbacks.default_callbacks.pop('on_fit_epoch_end', None)\n\n# ✅ Custom global Ultralytics settings\nultralytics_settings_path = Path(\"/kaggle/working/ultralytics_custom_settings.yaml\")\nultralytics_settings_path.parent.mkdir(parents=True, exist_ok=True)\n\nif not ultralytics_settings_path.exists():\n    default_settings = {\n        \"datasets_dir\": \"/kaggle/working/yolo_cache\",  # Custom cache location\n        \"runs_dir\": \"/kaggle/working/yolo_results\"     # Output folder\n    }\n    with open(ultralytics_settings_path, 'w') as f:\n        yaml.dump(default_settings, f)\n\n# ✅ Point Ultralytics to the settings file\nos.environ[\"ULTRALYTICS_SETTINGS\"] = str(ultralytics_settings_path)\n\n# -----------------------------\n# CONFIGURATION\n# -----------------------------\nDATA_ROOT = \"/kaggle/input\"\nRESULTS_ROOT = \"/kaggle/working/yolo_results\"\nCONDITIONS = [\"neural-foraminal-narrowing\"]\nFOLDS = [0, 1]\nEPOCHS = 20\nPATIENCE = 5\nBATCH_SIZE = 8\n\n# -----------------------------\n# TRAINING LOOP\n# -----------------------------\nfor condition in CONDITIONS:\n    for fold in FOLDS:\n        data_yaml = f\"{DATA_ROOT}/{condition}/fold_{fold}/datasets/yolo_config.yaml\"\n        save_dir = Path(RESULTS_ROOT) / condition / f\"fold_{fold}\"\n        save_dir.mkdir(parents=True, exist_ok=True)\n\n        print(f\"\\n▶ Training {condition}, fold {fold}\")\n        \n        # ✅ From config — no pretrained weights\n        model = YOLO(\"yolov8n.yaml\")\n        \n        # ✅ Training\n        model.train(\n            data=data_yaml,\n            project=\"yolo_results\",\n            name=f\"{condition.replace('-', '_')}_fold_{fold}\",\n            epochs=EPOCHS,\n            patience=PATIENCE,\n            batch=BATCH_SIZE,\n            amp=False,\n            save=True,\n            exist_ok=True  # avoids errors if dir exists\n        )\n        \n        print(f\"✅ Done {condition}, fold {fold}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-22T17:16:07.878215Z","iopub.execute_input":"2025-06-22T17:16:07.878803Z","iopub.status.idle":"2025-06-22T19:15:29.129328Z","shell.execute_reply.started":"2025-06-22T17:16:07.878748Z","shell.execute_reply":"2025-06-22T19:15:29.127811Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!zip -r yolo_results.zip /kaggle/working/yolo_results\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-22T19:25:21.098701Z","iopub.execute_input":"2025-06-22T19:25:21.099066Z","iopub.status.idle":"2025-06-22T19:25:27.918451Z","shell.execute_reply.started":"2025-06-22T19:25:21.098999Z","shell.execute_reply":"2025-06-22T19:25:27.917746Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"rm -rf /kaggle/working/scs_yolo_fold0.zip","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-22T19:28:35.635240Z","iopub.execute_input":"2025-06-22T19:28:35.636295Z","iopub.status.idle":"2025-06-22T19:28:36.057216Z","shell.execute_reply.started":"2025-06-22T19:28:35.636261Z","shell.execute_reply":"2025-06-22T19:28:36.056258Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**CLASSIFER**","metadata":{}},{"cell_type":"code","source":"import os\nimport pandas as pd\nfrom sklearn.model_selection import StratifiedKFold\nfrom pathlib import Path\n\ndef splitted_data_2fold(condition):\n    \"\"\"\n    Reads ./condition_csv/<condition>.csv, drops NaN scores,\n    performs a 2-fold stratified split on 'score', and writes out:\n      - ./<condition>/<condition>_2folds.csv\n      - ./<condition>/fold_0/<condition>_train.csv, <condition>_val.csv\n      - ./<condition>/fold_1/<condition>_train.csv, <condition>_val.csv\n    \"\"\"\n    # 1) Load and clean\n    df = pd.read_csv(f'/kaggle/input/csv-files/{condition}.csv')\n    df = df.dropna(subset=['score']).reset_index(drop=True)\n\n    # 2) Stratified 2-fold split\n    skf = StratifiedKFold(n_splits=2, shuffle=True, random_state=42)\n    df['fold'] = -1\n    for fold, (_, val_idx) in enumerate(skf.split(df, df['score'])):\n        df.loc[val_idx, 'fold'] = fold\n\n    # 3) Save the combined 2-folds CSV\n    condition_dir = Path(f'./{condition}')\n    condition_dir.mkdir(exist_ok=True)\n    df.to_csv(condition_dir / f'{condition}_2folds.csv', index=False)\n    print(f\"Saved 2-folds CSV to: {condition_dir / f'{condition}_2folds.csv'}\")\n\n    # 4) Write out per-fold train/val splits\n    for fold in range(2):\n        fold_dir = condition_dir / f'fold_{fold}'\n        fold_dir.mkdir(exist_ok=True)\n        train_df = df[df['fold'] != fold]\n        val_df   = df[df['fold'] == fold]\n        train_df.to_csv(fold_dir / f'{condition}_train.csv', index=False)\n        val_df.to_csv(  fold_dir / f'{condition}_val.csv',   index=False)\n        print(f\"  Fold {fold}: {len(train_df)} train rows → {fold_dir / f'{condition}_train.csv'}\")\n        print(f\"  Fold {fold}: {len(val_df)}   val rows → {fold_dir / f'{condition}_val.csv'}\")\n\n# Run for each condition\nfor cond in ['Neural_Foraminal_Narrowing', 'Spinal_Canal_Stenosis', 'Subarticular_Stenosis']:\n    splitted_data_2fold(cond)\n","metadata":{"trusted":true,"scrolled":true,"execution":{"iopub.status.busy":"2025-06-22T19:36:28.741971Z","iopub.execute_input":"2025-06-22T19:36:28.742742Z","iopub.status.idle":"2025-06-22T19:36:29.965768Z","shell.execute_reply.started":"2025-06-22T19:36:28.742698Z","shell.execute_reply":"2025-06-22T19:36:29.965004Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\nimport random\nimport shutil\n\n# Define augmentation methods\naugmentations = ['rotate', 'horizontal_flip', 'vertical_flip', 'gaussian_noise']\n\n# Function to balance minority classes\ndef augment_data(df: pd.DataFrame, augmentations: list) -> pd.DataFrame:\n    \"\"\"\n    Oversample minority \"score\" classes to balance against the majority class.\n    Adds an 'augmentation' column to each augmented sample.\n    \"\"\"\n    counts = df['score'].value_counts()\n    maj_cls = counts.idxmax()\n    target1 = counts[maj_cls] // 3\n    target2 = counts[maj_cls] // 2\n\n    minors = [cls for cls, c in counts.items() if c < counts[maj_cls]]\n    if len(minors) < 2:\n        minors = minors * 2\n\n    def sample_and_label(min_cls, target):\n        existing = df[df['score'] == min_cls]\n        needed = max(0, target - len(existing))\n        if needed <= 0:\n            return pd.DataFrame(columns=list(df.columns) + ['augmentation'])\n        sampled = existing.sample(needed, replace=True).copy()\n        sampled['augmentation'] = [random.choice(augmentations) for _ in range(needed)]\n        return sampled\n\n    aug1 = sample_and_label(minors[0], target1)\n    aug2 = sample_and_label(minors[1], target2)\n\n    df['augmentation'] = None\n    return pd.concat([df, aug1, aug2], ignore_index=True)\n\n# Two-fold augmentation for each condition and fold\nconditions = ['Neural_Foraminal_Narrowing', 'Spinal_Canal_Stenosis', 'Subarticular_Stenosis']\nfolds = [0, 1]\n\nfor cond in conditions:\n    for fold in folds:\n        train_csv = f'./{cond}/fold_{fold}/{cond}_train.csv'\n        val_csv   = f'./{cond}/fold_{fold}/{cond}_val.csv'\n\n        df_train = pd.read_csv(train_csv)\n        df_aug   = augment_data(df_train, augmentations)\n\n        out_dir = os.path.join('augmented_output', cond, f'fold_{fold}')\n        os.makedirs(out_dir, exist_ok=True)\n\n        aug_train_path = os.path.join(out_dir, f'{cond}_augmented_train.csv')\n        df_aug.to_csv(aug_train_path, index=False)\n        print(f'Augmented train data saved to: {aug_train_path}')\n\n        # Copy validation set unchanged\n        aug_val_path = os.path.join(out_dir, f'{cond}_val.csv')\n        shutil.copy(val_csv, aug_val_path)\n        print(f'Validation data copied to: {aug_val_path}')\n","metadata":{"trusted":true,"scrolled":true,"execution":{"iopub.status.busy":"2025-06-22T19:39:42.212385Z","iopub.execute_input":"2025-06-22T19:39:42.212924Z","iopub.status.idle":"2025-06-22T19:39:42.907968Z","shell.execute_reply.started":"2025-06-22T19:39:42.212898Z","shell.execute_reply":"2025-06-22T19:39:42.907173Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ─────────────────────────────────────────────────────────────────────────────\n# Cell: Image Data Preparation for 2-Fold Splits\n# ─────────────────────────────────────────────────────────────────────────────\n\nimport os\nimport pydicom\nimport pandas as pd\nimport numpy as np\nfrom PIL import Image, ImageOps\nfrom pathlib import Path\n\nclass DataPreparationImage:\n    def __init__(\n        self,\n        dataset_directory: str,\n        condition: str,\n        csv_directory: str,\n        num_folds: int = 2,\n        augmentation_list=None,\n    ):\n        self.dataset_directory = dataset_directory\n        self.condition = condition\n        self.csv_directory = csv_directory\n        self.num_folds = num_folds\n        self.augmentation_list = augmentation_list or ['rotate','horizontal_flip','vertical_flip','gaussian_noise']\n\n        print(f\"Starting image data prep for {self.condition} ({self.num_folds} folds)\")\n        self._create_folders()\n        self._process_all_folds()\n\n    def _create_folders(self):\n        base = Path(f\"./{self.condition}\")\n        base.mkdir(exist_ok=True)\n        for fold in range(self.num_folds):\n            (base / f\"fold_{fold}\" / \"train\").mkdir(parents=True, exist_ok=True)\n            (base / f\"fold_{fold}\" / \"val\").mkdir(parents=True, exist_ok=True)\n\n    def _read_csv(self, fold: int, split: str) -> pd.DataFrame:\n        filename = f\"{self.condition}_{'augmented_train' if split=='train' else 'val'}.csv\"\n        path = Path(self.csv_directory) / self.condition / f\"fold_{fold}\" / filename\n        return pd.read_csv(path)\n\n    def _read_dicom(self, path: str) -> np.ndarray:\n        ds = pydicom.dcmread(path)\n        img = ds.pixel_array.astype(float)\n        img = (img - img.min())/(img.max()-img.min()+1e-6)*255.0\n        return np.stack([img]*3,axis=-1).astype('uint8')\n\n    def _crop(self, image: np.ndarray, x: float, y: float, box: int = 16) -> Image.Image:\n        img = Image.fromarray(image)\n        left, top = int(x-box), int(y-box)\n        right, bottom = int(x+box), int(y+box)\n        return img.crop((left, top, right, bottom))\n\n    def _apply_augmentation(self, img: Image.Image, aug: str) -> Image.Image:\n        if aug=='rotate':\n            return img.rotate(np.random.uniform(-20,20), expand=True)\n        if aug=='horizontal_flip':\n            return ImageOps.mirror(img)\n        if aug=='vertical_flip':\n            return ImageOps.flip(img)\n        if aug=='gaussian_noise':\n            arr = np.array(img)\n            noise = np.random.normal(0,25,arr.shape)\n            return Image.fromarray(np.clip(arr+noise,0,255).astype('uint8'))\n        return img\n\n    def _process_all_folds(self):\n        for fold in range(self.num_folds):\n            # TRAIN SPLIT\n            df_train = self._read_csv(fold, 'train')\n            for _, row in df_train.iterrows():\n                sid, seid, inst = row['study_id'], row['series_id'], row['instance_number']\n                x, y = row['x'], row['y']\n                aug = row.get('augmentation')\n                dcm_path = os.path.join(self.dataset_directory, str(sid), str(seid), f\"{inst}.dcm\")\n                img = self._read_dicom(dcm_path)\n                cropped = self._crop(img, x, y)\n                if aug in self.augmentation_list:\n                    out_img = self._apply_augmentation(cropped, aug)\n                    suffix = f\"_{aug}\"\n                else:\n                    out_img = cropped\n                    suffix = \"\"\n                fname = f\"{sid}_{seid}_{inst}_{int(x)}_{int(y)}{suffix}.png\"\n                out_path = Path(self.condition) / f\"fold_{fold}\" / \"train\" / fname\n                out_img.save(out_path)\n\n            # VAL SPLIT (no augmentation)\n            df_val = self._read_csv(fold, 'val')\n            for _, row in df_val.iterrows():\n                sid, seid, inst = row['study_id'], row['series_id'], row['instance_number']\n                x, y = row['x'], row['y']\n                dcm_path = os.path.join(self.dataset_directory, str(sid), str(seid), f\"{inst}.dcm\")\n                img = self._read_dicom(dcm_path)\n                cropped = self._crop(img, x, y)\n                fname = f\"{sid}_{seid}_{inst}_{int(x)}_{int(y)}.png\"\n                out_path = Path(self.condition) / f\"fold_{fold}\" / \"val\" / fname\n                cropped.save(out_path)\n\n# ─────────────────────────────────────────────────────────────────────────────\n# Run for your three conditions:\n# ─────────────────────────────────────────────────────────────────────────────\n\nDATASET_DIR = \"/kaggle/input/rsna-2024-lumbar-spine-degenerative-classification/train_images\"\nCSV_DIR     = \"/kaggle/working/augmented_output\"\n\nfor cond in [\"Subarticular_Stenosis\", \"Spinal_Canal_Stenosis\", \"Neural_Foraminal_Narrowing\"]:\n    DataPreparationImage(\n        dataset_directory=DATASET_DIR,\n        condition=cond,\n        csv_directory=CSV_DIR,\n        num_folds=2\n    )\n","metadata":{"trusted":true,"scrolled":true,"execution":{"iopub.status.busy":"2025-06-22T19:46:57.120223Z","iopub.execute_input":"2025-06-22T19:46:57.120516Z","iopub.status.idle":"2025-06-22T20:13:07.556147Z","shell.execute_reply.started":"2025-06-22T19:46:57.120493Z","shell.execute_reply":"2025-06-22T20:13:07.553789Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ─────────────────────────────────────────────────────────────────────────────\n# Cell: Classification Label Preparation for 2-Fold Splits\n# ─────────────────────────────────────────────────────────────────────────────\n\nimport os\nimport pandas as pd\nfrom pathlib import Path\n\n# 1) Parameters\nCSV_DIR    = '/kaggle/working/augmented_output'   # where your augmented CSVs live\nCONDITIONS = ['Spinal_Canal_Stenosis',\n              'Neural_Foraminal_Narrowing',\n              'Subarticular_Stenosis']\nFOLDS      = [0, 1]  # only two folds\nOUT_ROOT   = '/kaggle/working/'    # base for label output folders\n\n# 2) Helper: build 'subject' and 'label' columns\ndef working_on_csv(csv_path: str, split: str) -> pd.DataFrame:\n    df = pd.read_csv(csv_path)\n    # subject PNG filename\n    def make_subject(r):\n        name = f\"{r.study_id}_{r.series_id}_{r.instance_number}_{int(r.x)}_{int(r.y)}.png\"\n        if split=='train' and pd.notna(r.augmentation) and r.augmentation:\n            return name.replace('.png', '_augmented.png')\n        return name\n    # numeric label mapping\n    def make_label(score):\n        return {'Normal/Mild':1, 'Moderate':2, 'Severe':3}.get(score, None)\n\n    df['subject'] = df.apply(make_subject, axis=1)\n    df['label']   = df['score'].apply(make_label)\n    return df[['subject','label']]\n\n# 3) Create output folder structure\nfor cond in CONDITIONS:\n    base = Path(OUT_ROOT) / f\"{cond}_label\"\n    for fold in FOLDS:\n        (base / f\"fold_{fold}\").mkdir(parents=True, exist_ok=True)\n\n# 4) Process each condition & fold\nfor cond in CONDITIONS:\n    for fold in FOLDS:\n        # ←— **NOTE**: train file ends in `_augmented_train.csv`\n        train_csv = f\"{CSV_DIR}/{cond}/fold_{fold}/{cond}_augmented_train.csv\"\n        val_csv   = f\"{CSV_DIR}/{cond}/fold_{fold}/{cond}_val.csv\"\n\n        # build label DataFrames\n        train_labels = working_on_csv(train_csv, 'train')\n        val_labels   = working_on_csv(val_csv,   'val')\n\n        # write out\n        out_base = Path(OUT_ROOT) / f\"{cond}_label\" / f\"fold_{fold}\"\n        train_labels.to_csv(out_base / f\"{cond}_augmented_labels.csv\", index=False)\n        val_labels.to_csv(  out_base / f\"{cond}_val_labels.csv\",       index=False)\n\n        print(f\"→ {cond} fold {fold}:\")\n        print(f\"   • train labels → {out_base / f'{cond}_augmented_labels.csv'} ({len(train_labels)} rows)\")\n        print(f\"   • val   labels → {out_base / f'{cond}_val_labels.csv'} ({len(val_labels)} rows)\")\n","metadata":{"trusted":true,"scrolled":true,"execution":{"iopub.status.busy":"2025-06-22T20:17:12.750943Z","iopub.execute_input":"2025-06-22T20:17:12.751263Z","iopub.status.idle":"2025-06-22T20:17:16.348664Z","shell.execute_reply.started":"2025-06-22T20:17:12.751238Z","shell.execute_reply":"2025-06-22T20:17:16.347776Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ─────────────────────────────────────────────────────────────────────────────\n# Cell: Neural_Foraminal_Narrowing Fast Severity Classification Training on 2 Folds\n# ─────────────────────────────────────────────────────────────────────────────\n\nimport os\nimport pandas as pd\nimport numpy as np\nfrom pathlib import Path\nimport tensorflow as tf\nfrom sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score\nfrom tensorflow.keras.applications import ResNet50\nfrom tensorflow.keras.models import Model\nfrom tensorflow.keras.layers import Dense, Dropout, GlobalAveragePooling2D\nfrom tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint\nfrom tensorflow.keras.preprocessing.image import load_img, img_to_array\n\n# Configuration\nCONDITION = 'Neural_Foraminal_Narrowing'\nFOLDS = [0, 1]\nIMAGE_ROOT_TMPL = '/kaggle/working/Neural_Foraminal_Narrowing/fold_{fold}'\nLABEL_ROOT_TMPL = '/kaggle/working/Neural_Foraminal_Narrowing_label/fold_{fold}'\nBATCH_SIZE = 16\nPATIENCE = 5\nLEARNING_RATE = 1e-4\nIMG_SIZE = (224, 224)\n\n# Focal loss implementation\ndef focal_loss(gamma=2., alpha=0.25):\n    def focal_loss_fixed(y_true, y_pred):\n        epsilon = tf.keras.backend.epsilon()\n        y_pred = tf.clip_by_value(y_pred, epsilon, 1. - epsilon)\n        cross_entropy = -y_true * tf.math.log(y_pred)\n        loss = alpha * tf.math.pow(1 - y_pred, gamma) * cross_entropy\n        return tf.reduce_sum(loss, axis=1)\n    return focal_loss_fixed\n\ndef load_data(df, image_root, split):\n    X, y = [], []\n    for _, row in df.iterrows():\n        img_path = os.path.join(image_root, split, row['subject'])\n        try:\n            img = load_img(img_path, target_size=IMG_SIZE)\n            arr = img_to_array(img) / 255.0\n            X.append(arr)\n            y.append(int(row['label']) - 1)\n        except:\n            print(f\"Failed to load {img_path}\")\n    return np.array(X), tf.keras.utils.to_categorical(y, num_classes=3)\n\nfor fold in FOLDS:\n    print(f\"\\n▶ Training {CONDITION}, fold {fold}\")\n    IMAGE_ROOT = IMAGE_ROOT_TMPL.format(cond=CONDITION, fold=fold)\n    LABEL_ROOT = LABEL_ROOT_TMPL.format(cond=CONDITION, fold=fold)\n\n    # Load labels\n    train_df = pd.read_csv(os.path.join(LABEL_ROOT, f'{CONDITION}_augmented_labels.csv'))\n    val_df = pd.read_csv(os.path.join(LABEL_ROOT, f'{CONDITION}_val_labels.csv'))\n\n    X_train, y_train = load_data(train_df, IMAGE_ROOT, 'train')\n    X_val, y_val = load_data(val_df, IMAGE_ROOT, 'val')\n\n    # Load ResNet50 base\n    base_model = ResNet50(include_top=False, weights='imagenet', input_shape=IMG_SIZE + (3,))\n    x = base_model.output\n    x = GlobalAveragePooling2D()(x)\n    x = Dense(256, activation='relu')(x)\n    x = Dropout(0.5)(x)\n    predictions = Dense(3, activation='softmax')(x)\n    model = Model(inputs=base_model.input, outputs=predictions)\n\n    model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=LEARNING_RATE),\n                  loss=focal_loss(gamma=2.0, alpha=0.5),\n                  metrics=['accuracy'])\n\n    out_dir = Path(f'./results/{CONDITION}/fold_{fold}')\n    out_dir.mkdir(parents=True, exist_ok=True)\n\n    callbacks = [\n        EarlyStopping(monitor='val_loss', patience=PATIENCE, restore_best_weights=True, verbose=1),\n        ModelCheckpoint(filepath=out_dir / 'best_model.keras', monitor='val_loss', save_best_only=True, verbose=1)\n    ]\n\n    model.fit(X_train, y_train,\n              validation_data=(X_val, y_val),\n              epochs=EPOCHS,\n              batch_size=BATCH_SIZE,\n              callbacks=callbacks,\n              verbose=2)\n\n    # Evaluation\n    y_pred = np.argmax(model.predict(X_val), axis=1)\n    y_true = np.argmax(y_val, axis=1)\n    acc = accuracy_score(y_true, y_pred)\n    prec = precision_score(y_true, y_pred, average='weighted')\n    rec = recall_score(y_true, y_pred, average='weighted')\n    f1 = f1_score(y_true, y_pred, average='weighted')\n\n    with open(out_dir / 'metrics.txt', 'w') as f:\n        f.write(f\"Accuracy: {acc}\\n\")\n        f.write(f\"Precision: {prec}\\n\")\n        f.write(f\"Recall: {rec}\\n\")\n        f.write(f\"F1 Score: {f1}\\n\")\n\n    print(f\"✅ Saved model and metrics for fold {fold} to: {out_dir}\")\n","metadata":{"trusted":true,"scrolled":true,"execution":{"iopub.status.busy":"2025-06-22T20:28:39.816823Z","iopub.execute_input":"2025-06-22T20:28:39.817344Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ─────────────────────────────────────────────────────────────────────────────\n# Cell: Spinal Canal Stenosis Fast Severity Classification Training on 2 Folds\n# ─────────────────────────────────────────────────────────────────────────────\n\n# ─────────────────────────────────────────────────────────────────────────────\n# Cell: Fast Severity Classification Training on 2 Folds with ResNet & Confusion Matrix\n# ─────────────────────────────────────────────────────────────────────────────\n\nimport os\nimport pandas as pd\nimport numpy as np\nfrom pathlib import Path\nimport tensorflow as tf\nfrom sklearn.metrics import (\n    accuracy_score, precision_score, recall_score, f1_score,\n    classification_report, confusion_matrix\n)\nfrom tensorflow.keras.applications import ResNet50\nfrom tensorflow.keras.models import Model\nfrom tensorflow.keras.layers import Dense, Dropout, GlobalAveragePooling2D\nfrom tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint\nfrom tensorflow.keras.preprocessing.image import load_img, img_to_array\n\n# Configuration\nCONDITION = 'Spinal_Canal_Stenosis'\nFOLDS = [0, 1]\nIMAGE_ROOT_TMPL = '/kaggle/working/Spinal_Canal_Stenosis/fold_{fold}'\nLABEL_ROOT_TMPL = '/kaggle/working/Spinal_Canal_Stenosis_label/fold_{fold}'\nBATCH_SIZE = 16\nPATIENCE = 5\nLEARNING_RATE = 1e-4\nEPOCHS = 10\nIMG_SIZE = (224, 224)\nCLASS_NAMES = ['Normal/Mild', 'Moderate', 'Severe']\n\n# Focal loss implementation\ndef focal_loss(gamma=2., alpha=0.25):\n    def focal_loss_fixed(y_true, y_pred):\n        epsilon = tf.keras.backend.epsilon()\n        y_pred = tf.clip_by_value(y_pred, epsilon, 1. - epsilon)\n        cross_entropy = -y_true * tf.math.log(y_pred)\n        loss = alpha * tf.math.pow(1 - y_pred, gamma) * cross_entropy\n        return tf.reduce_sum(loss, axis=1)\n    return focal_loss_fixed\n\ndef load_data(df, image_root, split):\n    X, y = [], []\n    for _, row in df.iterrows():\n        img_path = os.path.join(image_root, split, row['subject'])\n        try:\n            img = load_img(img_path, target_size=IMG_SIZE)\n            arr = img_to_array(img) / 255.0\n            X.append(arr)\n            y.append(int(row['label']) - 1)\n        except:\n            print(f\"Failed to load {img_path}\")\n    return np.array(X), tf.keras.utils.to_categorical(y, num_classes=3)\n\nfor fold in FOLDS:\n    print(f\"\\n▶ Training {CONDITION}, fold {fold}\")\n    IMAGE_ROOT = IMAGE_ROOT_TMPL.format(cond=CONDITION, fold=fold)\n    LABEL_ROOT = LABEL_ROOT_TMPL.format(cond=CONDITION, fold=fold)\n\n    train_df = pd.read_csv(os.path.join(LABEL_ROOT, f'{CONDITION}_augmented_labels.csv'))\n    val_df = pd.read_csv(os.path.join(LABEL_ROOT, f'{CONDITION}_val_labels.csv'))\n\n    X_train, y_train = load_data(train_df, IMAGE_ROOT, 'train')\n    X_val, y_val = load_data(val_df, IMAGE_ROOT, 'val')\n\n    base_model = ResNet50(include_top=False, weights='imagenet', input_shape=IMG_SIZE + (3,))\n    x = base_model.output\n    x = GlobalAveragePooling2D()(x)\n    x = Dense(256, activation='relu')(x)\n    x = Dropout(0.5)(x)\n    predictions = Dense(3, activation='softmax')(x)\n    model = Model(inputs=base_model.input, outputs=predictions)\n\n    model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=LEARNING_RATE),\n                  loss=focal_loss(gamma=2.0, alpha=0.5),\n                  metrics=['accuracy'])\n\n    out_dir = Path(f'./results/{CONDITION}/fold_{fold}')\n    out_dir.mkdir(parents=True, exist_ok=True)\n\n    callbacks = [\n        EarlyStopping(monitor='val_loss', patience=PATIENCE, restore_best_weights=True, verbose=1),\n        ModelCheckpoint(filepath=out_dir / 'best_model.keras', monitor='val_loss', save_best_only=True, verbose=1)\n    ]\n\n    model.fit(X_train, y_train,\n              validation_data=(X_val, y_val),\n              epochs=EPOCHS,\n              batch_size=BATCH_SIZE,\n              callbacks=callbacks,\n              verbose=2)\n\n    # Evaluation\n    y_pred = np.argmax(model.predict(X_val), axis=1)\n    y_true = np.argmax(y_val, axis=1)\n\n    acc = accuracy_score(y_true, y_pred)\n    prec = precision_score(y_true, y_pred, average='weighted')\n    rec = recall_score(y_true, y_pred, average='weighted')\n    f1 = f1_score(y_true, y_pred, average='weighted')\n    cm = confusion_matrix(y_true, y_pred)\n\n    with open(out_dir / 'metrics.txt', 'w') as f:\n        f.write(f\"Accuracy: {acc}\\n\")\n        f.write(f\"Precision: {prec}\\n\")\n        f.write(f\"Recall: {rec}\\n\")\n        f.write(f\"F1 Score: {f1}\\n\")\n\n    pd.DataFrame(cm, index=CLASS_NAMES, columns=CLASS_NAMES).to_csv(out_dir / 'confusion_matrix.csv')\n    print(f\"✅ Saved model and metrics for fold {fold} to: {out_dir}\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ─────────────────────────────────────────────────────────────────────────────\n# Cell: Fast Severity Classification Training on 2 Folds with ResNet & Confusion Matrix\n# ─────────────────────────────────────────────────────────────────────────────\n\nimport os\nimport pandas as pd\nimport numpy as np\nfrom pathlib import Path\nimport tensorflow as tf\nfrom sklearn.metrics import (\n    accuracy_score, precision_score, recall_score, f1_score,\n    classification_report, confusion_matrix\n)\nfrom tensorflow.keras.applications import ResNet50\nfrom tensorflow.keras.models import Model\nfrom tensorflow.keras.layers import Dense, Dropout, GlobalAveragePooling2D\nfrom tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint\nfrom tensorflow.keras.preprocessing.image import load_img, img_to_array\n\n# Configuration\nCONDITION = 'Subarticular_Stenosis'\nFOLDS = [0, 1]\nIMAGE_ROOT_TMPL = '/kaggle/working/Subarticular_Stenosis/fold_{fold}'\nLABEL_ROOT_TMPL = '/kaggle/working/Subarticular_Stenosis_label/fold_{fold}'\nBATCH_SIZE = 16\nPATIENCE = 5\nLEARNING_RATE = 1e-4\nEPOCHS = 10\nIMG_SIZE = (224, 224)\nCLASS_NAMES = ['Normal/Mild', 'Moderate', 'Severe']\n\n# Focal loss implementation\ndef focal_loss(gamma=2., alpha=0.25):\n    def focal_loss_fixed(y_true, y_pred):\n        epsilon = tf.keras.backend.epsilon()\n        y_pred = tf.clip_by_value(y_pred, epsilon, 1. - epsilon)\n        cross_entropy = -y_true * tf.math.log(y_pred)\n        loss = alpha * tf.math.pow(1 - y_pred, gamma) * cross_entropy\n        return tf.reduce_sum(loss, axis=1)\n    return focal_loss_fixed\n\ndef load_data(df, image_root, split):\n    X, y = [], []\n    for _, row in df.iterrows():\n        img_path = os.path.join(image_root, split, row['subject'])\n        try:\n            img = load_img(img_path, target_size=IMG_SIZE)\n            arr = img_to_array(img) / 255.0\n            X.append(arr)\n            y.append(int(row['label']) - 1)\n        except:\n            print(f\"Failed to load {img_path}\")\n    return np.array(X), tf.keras.utils.to_categorical(y, num_classes=3)\n\nfor fold in FOLDS:\n    print(f\"\\n▶ Training {CONDITION}, fold {fold}\")\n    IMAGE_ROOT = IMAGE_ROOT_TMPL.format(cond=CONDITION, fold=fold)\n    LABEL_ROOT = LABEL_ROOT_TMPL.format(cond=CONDITION, fold=fold)\n\n    train_df = pd.read_csv(os.path.join(LABEL_ROOT, f'{CONDITION}_augmented_labels.csv'))\n    val_df = pd.read_csv(os.path.join(LABEL_ROOT, f'{CONDITION}_val_labels.csv'))\n\n    X_train, y_train = load_data(train_df, IMAGE_ROOT, 'train')\n    X_val, y_val = load_data(val_df, IMAGE_ROOT, 'val')\n\n    base_model = ResNet50(include_top=False, weights='imagenet', input_shape=IMG_SIZE + (3,))\n    x = base_model.output\n    x = GlobalAveragePooling2D()(x)\n    x = Dense(256, activation='relu')(x)\n    x = Dropout(0.5)(x)\n    predictions = Dense(3, activation='softmax')(x)\n    model = Model(inputs=base_model.input, outputs=predictions)\n\n    model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=LEARNING_RATE),\n                  loss=focal_loss(gamma=2.0, alpha=0.5),\n                  metrics=['accuracy'])\n\n    out_dir = Path(f'./results/{CONDITION}/fold_{fold}')\n    out_dir.mkdir(parents=True, exist_ok=True)\n\n    callbacks = [\n        EarlyStopping(monitor='val_loss', patience=PATIENCE, restore_best_weights=True, verbose=1),\n        ModelCheckpoint(filepath=out_dir / 'best_model.keras', monitor='val_loss', save_best_only=True, verbose=1)\n    ]\n\n    model.fit(X_train, y_train,\n              validation_data=(X_val, y_val),\n              epochs=EPOCHS,\n              batch_size=BATCH_SIZE,\n              callbacks=callbacks,\n              verbose=2)\n\n    # Evaluation\n    y_pred = np.argmax(model.predict(X_val), axis=1)\n    y_true = np.argmax(y_val, axis=1)\n\n    acc = accuracy_score(y_true, y_pred)\n    prec = precision_score(y_true, y_pred, average='weighted')\n    rec = recall_score(y_true, y_pred, average='weighted')\n    f1 = f1_score(y_true, y_pred, average='weighted')\n    cm = confusion_matrix(y_true, y_pred)\n\n    with open(out_dir / 'metrics.txt', 'w') as f:\n        f.write(f\"Accuracy: {acc}\\n\")\n        f.write(f\"Precision: {prec}\\n\")\n        f.write(f\"Recall: {rec}\\n\")\n        f.write(f\"F1 Score: {f1}\\n\")\n\n    pd.DataFrame(cm, index=CLASS_NAMES, columns=CLASS_NAMES).to_csv(out_dir / 'confusion_matrix.csv')\n    print(f\"✅ Saved model and metrics for fold {fold} to: {out_dir}\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ─────────────────────────────────────────────────────────────────────────────\n# Cell: Training + Evaluation with Confusion Matrix for 2 Folds\n# ─────────────────────────────────────────────────────────────────────────────\n\nimport os\nimport pandas as pd\nimport numpy as np\nimport tensorflow as tf\nfrom tensorflow.keras.models import Sequential\nfrom tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout\nfrom tensorflow.keras.optimizers import Adam\nfrom tensorflow.keras.callbacks import EarlyStopping\nfrom sklearn.metrics import (\n    accuracy_score, precision_score, recall_score, f1_score,\n    classification_report, confusion_matrix\n)\nfrom pathlib import Path\n\n# CONFIGURATION\nCONDITION       = 'Subarticular_Stenosis'\nFOLDS           = [0, 1]\nIMAGE_ROOT_TMPL = './{cond}/fold_{fold}'\nLABEL_ROOT_TMPL = './{cond}_label/fold_{fold}'\nEPOCHS          = 10\nBATCH_SIZE      = 16\nPATIENCE        = 3\nLEARNING_RATE   = 1e-3\nCLASS_NAMES     = ['Normal/Mild', 'Moderate', 'Severe']\n\ndef load_data(df, image_root, split):\n    X, y = [], []\n    for _, row in df.iterrows():\n        img_path = os.path.join(image_root, split, row['subject'])\n        img = tf.keras.preprocessing.image.load_img(img_path, target_size=(32,32))\n        arr = tf.keras.preprocessing.image.img_to_array(img) / 255.0\n        X.append(arr)\n        y.append(int(row['label']) - 1)\n    return np.array(X), np.array(y)\n\nfor fold in FOLDS:\n    print(f\"\\n▶ Training {CONDITION}, fold {fold}\")\n    IMAGE_ROOT = IMAGE_ROOT_TMPL.format(cond=CONDITION, fold=fold)\n    LABEL_ROOT = LABEL_ROOT_TMPL.format(cond=CONDITION, fold=fold)\n\n    # Load labels\n    train_df = pd.read_csv(os.path.join(LABEL_ROOT, f'{CONDITION}_augmented_labels.csv'))\n    val_df   = pd.read_csv(os.path.join(LABEL_ROOT, f'{CONDITION}_val_labels.csv'))\n\n    # Load data arrays\n    X_train, y_train = load_data(train_df, IMAGE_ROOT, 'train')\n    X_val,   y_val   = load_data(val_df,   IMAGE_ROOT, 'val')\n\n    # Build model\n    model = Sequential([\n        Conv2D(16, 3, activation='relu', input_shape=(32,32,3)),\n        MaxPooling2D(),\n        Conv2D(32, 3, activation='relu'),\n        MaxPooling2D(),\n        Conv2D(64, 3, activation='relu'),\n        MaxPooling2D(),\n        Flatten(),\n        Dense(64, activation='relu'),\n        Dropout(0.3),\n        Dense(3, activation='softmax'),\n    ])\n    model.compile(\n        optimizer=Adam(learning_rate=LEARNING_RATE),\n        loss='sparse_categorical_crossentropy',\n        metrics=['accuracy']\n    )\n\n    # Early stopping\n    es = EarlyStopping(monitor='val_loss', patience=PATIENCE, restore_best_weights=True, verbose=1)\n\n    # Train\n    model.fit(X_train, y_train, validation_data=(X_val, y_val),\n              epochs=EPOCHS, batch_size=BATCH_SIZE, callbacks=[es], verbose=2)\n\n    # Predict on validation set\n    y_pred_probs = model.predict(X_val, batch_size=BATCH_SIZE)\n    y_pred       = np.argmax(y_pred_probs, axis=1)\n\n    # Compute metrics\n    acc   = accuracy_score(y_val, y_pred)\n    prec  = precision_score(y_val, y_pred, average='weighted', zero_division=0)\n    rec   = recall_score(y_val, y_pred, average='weighted', zero_division=0)\n    f1    = f1_score(y_val, y_pred, average='weighted', zero_division=0)\n    cm    = confusion_matrix(y_val, y_pred)\n\n    # Display results\n    print(f\"\\nFold {fold} Validation Metrics:\")\n    print(f\"  Accuracy : {acc:.4f}\")\n    print(f\"  Precision: {prec:.4f}\")\n    print(f\"  Recall   : {rec:.4f}\")\n    print(f\"  F1 score : {f1:.4f}\\n\")\n    print(\"Classification Report:\")\n    print(classification_report(y_val, y_pred, target_names=CLASS_NAMES, zero_division=0))\n    print(\"Confusion Matrix:\")\n    print(pd.DataFrame(cm, index=CLASS_NAMES, columns=CLASS_NAMES))\n\n    # Save model and metrics\n    out_dir = Path(f'./results/{CONDITION}/fold_{fold}')\n    out_dir.mkdir(parents=True, exist_ok=True)\n    model.save(out_dir / 'quick_model.h5')\n\n    # Save metrics + confusion matrix to CSV\n    metrics_df = pd.DataFrame({\n        'metric':      ['accuracy','precision','recall','f1_score'],\n        'value':       [acc,prec,rec,f1]\n    })\n    metrics_df.to_csv(out_dir / 'validation_metrics.csv', index=False)\n    cm_df = pd.DataFrame(cm, index=CLASS_NAMES, columns=CLASS_NAMES)\n    cm_df.to_csv(out_dir / 'confusion_matrix.csv')\n    print(f\"✅ Saved metrics and confusion matrix to {out_dir}\\n\")\n","metadata":{"trusted":true,"scrolled":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}