{"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":12209765,"sourceType":"datasetVersion","datasetId":7691619},{"sourceId":12209836,"sourceType":"datasetVersion","datasetId":7691670},{"sourceId":12209924,"sourceType":"datasetVersion","datasetId":7691730},{"sourceId":12249891,"sourceType":"datasetVersion","datasetId":7718540},{"sourceId":12250412,"sourceType":"datasetVersion","datasetId":7718851}],"dockerImageVersionId":31041,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"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 split_data_5fold(condition):\n    \"\"\"\n    Reads /kaggle/input/csv-files/<condition>.csv, drops NaN scores,\n    performs a 5-fold stratified split on 'score', and writes out:\n      - ./<condition>/<condition>_5folds.csv\n      - ./<condition>/fold_0/<condition>_train.csv, <condition>_val.csv\n      - ...\n      - ./<condition>/fold_4/<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 5-fold split\n    skf = StratifiedKFold(n_splits=5, 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 5-folds CSV\n    condition_dir = Path(f'./{condition}')\n    condition_dir.mkdir(exist_ok=True)\n    df.to_csv(condition_dir / f'{condition}_5folds.csv', index=False)\n    print(f\"Saved 5-folds CSV to: {condition_dir / f'{condition}_5folds.csv'}\")\n\n    # 4) Write out per-fold train/val splits\n    for fold in range(5):\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    split_data_5fold(cond)\n","metadata":{"trusted":true,"scrolled":true,"execution":{"iopub.status.busy":"2025-07-01T08:16:56.821798Z","iopub.execute_input":"2025-07-01T08:16:56.822304Z","iopub.status.idle":"2025-07-01T08:16:58.839792Z","shell.execute_reply.started":"2025-07-01T08:16:56.822278Z","shell.execute_reply":"2025-07-01T08:16:58.838998Z"}},"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    target = counts[maj_cls]  # oversample each minority up to majority\n\n    minors = [cls for cls, c in counts.items() if c < counts[maj_cls]]\n\n    def sample_and_label(min_cls):\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    # oversample all minority classes equally\n    aug_parts = [sample_and_label(m) for m in minors]\n\n    df['augmentation'] = None\n    return pd.concat([df, *aug_parts], ignore_index=True)\n\n# Five-fold augmentation for each condition and fold\nconditions = ['Neural_Foraminal_Narrowing',\n              'Spinal_Canal_Stenosis',\n              'Subarticular_Stenosis']\nfolds = list(range(5))  # 0,1,2,3,4\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-07-01T08:17:30.077136Z","iopub.execute_input":"2025-07-01T08:17:30.077845Z","iopub.status.idle":"2025-07-01T08:17:33.760164Z","shell.execute_reply.started":"2025-07-01T08:17:30.077821Z","shell.execute_reply":"2025-07-01T08:17:33.759504Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ─────────────────────────────────────────────────────────────────────────────\n# Cell: Image Data Preparation for 5-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 = 5,                    # ◀── default to 5 folds\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 with 5 folds:\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=5     # ◀── now processing folds 0 through 4\n    )\n","metadata":{"trusted":true,"scrolled":true,"execution":{"iopub.status.busy":"2025-07-01T08:18:16.659127Z","iopub.execute_input":"2025-07-01T08:18:16.659401Z","iopub.status.idle":"2025-07-01T09:55:01.767630Z","shell.execute_reply.started":"2025-07-01T08:18:16.659380Z","shell.execute_reply":"2025-07-01T09:55:01.766558Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ─────────────────────────────────────────────────────────────────────────────\n# Cell: Classification Label Preparation for 5-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      = list(range(5))  # 0, 1, 2, 3, 4 (five 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-07-01T09:55:39.856082Z","iopub.execute_input":"2025-07-01T09:55:39.856385Z","iopub.status.idle":"2025-07-01T09:55:55.820376Z","shell.execute_reply.started":"2025-07-01T09:55:39.856363Z","shell.execute_reply":"2025-07-01T09:55:55.819730Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\nfrom pathlib import Path\nimport tensorflow as tf\nfrom sklearn.metrics import classification_report\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 ImageDataGenerator\nfrom tensorflow.keras.optimizers import Adam\nfrom tensorflow.keras.optimizers.schedules import ExponentialDecay\n\n# ─────────────────────────────────────────────────────────────────────────────\n# Configuration\n# ─────────────────────────────────────────────────────────────────────────────\nCONDITION       = 'Neural_Foraminal_Narrowing'\nFOLDS           = list(range(5))                # folds 0,1,2,3,4\nIMAGE_ROOT_TMPL = '/kaggle/working/Neural_Foraminal_Narrowing/fold_{fold}'\nLABEL_ROOT_TMPL = '/kaggle/working/Neural_Foraminal_Narrowing_label/fold_{fold}'\nBATCH_SIZE      = 16\nIMG_SIZE        = (224, 224)\nEPOCHS          = 10\nPATIENCE        = 5\n\n# Learning-rate schedule: start at 0.01, decay by 0.5 every 1000 steps\nINITIAL_LR   = 0.01\nDECAY_STEPS  = 1000\nDECAY_RATE   = 0.5\nlr_schedule  = ExponentialDecay(\n    initial_learning_rate=INITIAL_LR,\n    decay_steps=DECAY_STEPS,\n    decay_rate=DECAY_RATE,\n    staircase=False\n)\n\n# How many ResNet50 layers to freeze\nFREEZE_LAYERS = 50\n\n# Focal loss factory\ndef focal_loss(gamma=2.0, alpha=0.5):\n    def loss_fn(y_true, y_pred):\n        eps = tf.keras.backend.epsilon()\n        y_pred = tf.clip_by_value(y_pred, eps, 1. - eps)\n        ce = -y_true * tf.math.log(y_pred)\n        return tf.reduce_sum(alpha * tf.pow(1 - y_pred, gamma) * ce, axis=1)\n    return loss_fn\n\n# ─────────────────────────────────────────────────────────────────────────────\n# 5-Fold Training Loop\n# ─────────────────────────────────────────────────────────────────────────────\nfor fold in FOLDS:\n    print(f\"\\n▶ Training {CONDITION}, fold {fold}\")\n    IMG_DIR = IMAGE_ROOT_TMPL.format(fold=fold)\n    LAB_DIR = LABEL_ROOT_TMPL.format(fold=fold)\n\n    # Load labels\n    train_df = pd.read_csv(os.path.join(LAB_DIR, f'{CONDITION}_augmented_labels.csv'))\n    val_df   = pd.read_csv(os.path.join(LAB_DIR, f'{CONDITION}_val_labels.csv'))\n\n    # Clean up filenames & labels\n    train_df['subject'] = train_df['subject'].str.replace('_augmented', '', regex=False)\n    val_df  ['subject'] = val_df  ['subject'].str.replace('_augmented', '', regex=False)\n    train_df['label']   = (train_df['label'].astype(int) - 1).astype(str)\n    val_df  ['label']   = (val_df  ['label'].astype(int) - 1).astype(str)\n\n    # Generators\n    datagen = ImageDataGenerator(rescale=1./255)\n    train_gen = datagen.flow_from_dataframe(\n        train_df, directory=os.path.join(IMG_DIR, 'train'),\n        x_col='subject', y_col='label',\n        target_size=IMG_SIZE, batch_size=BATCH_SIZE,\n        class_mode='categorical', shuffle=True\n    )\n    val_gen = datagen.flow_from_dataframe(\n        val_df, directory=os.path.join(IMG_DIR, 'val'),\n        x_col='subject', y_col='label',\n        target_size=IMG_SIZE, batch_size=BATCH_SIZE,\n        class_mode='categorical', shuffle=False\n    )\n\n    # Build model\n    base = ResNet50(include_top=False, weights='imagenet', input_shape=IMG_SIZE + (3,))\n    # Freeze initial layers\n    for layer in base.layers[:FREEZE_LAYERS]:\n        layer.trainable = False\n    for layer in base.layers[FREEZE_LAYERS:]:\n        layer.trainable = True\n\n    x = GlobalAveragePooling2D()(base.output)\n    x = Dense(256, activation='relu')(x)\n    x = Dropout(0.5)(x)\n    outputs = Dense(3, activation='softmax')(x)\n    model = Model(inputs=base.input, outputs=outputs)\n\n    # Compile with exponential-decay LR\n    model.compile(\n        optimizer=Adam(learning_rate=lr_schedule),\n        loss=focal_loss(),\n        metrics=['accuracy']\n    )\n\n    # Callbacks\n    out_dir = Path(f'./results/{CONDITION}/fold_{fold}')\n    out_dir.mkdir(parents=True, exist_ok=True)\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    # Train\n    model.fit(\n        train_gen,\n        validation_data=val_gen,\n        epochs=EPOCHS,\n        callbacks=callbacks,\n        verbose=2\n    )\n\n    # Evaluate\n    val_gen.reset()\n    y_pred = np.argmax(model.predict(val_gen), axis=1)\n    y_true = val_gen.classes\n    print(\"\\n\" + classification_report(y_true, y_pred, target_names=['Mild','Moderate','Severe']))\n\n    # Cleanup\n    tf.keras.backend.clear_session()\n    import gc; gc.collect()\n","metadata":{"trusted":true,"scrolled":true,"execution":{"iopub.status.busy":"2025-07-01T10:39:18.705712Z","iopub.execute_input":"2025-07-01T10:39:18.706261Z","iopub.status.idle":"2025-07-01T12:32:38.971472Z","shell.execute_reply.started":"2025-07-01T10:39:18.706239Z","shell.execute_reply":"2025-07-01T12:32:38.970850Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import 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, classification_report\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 ImageDataGenerator\nfrom tensorflow.keras.optimizers import Adam\nfrom tensorflow.keras.optimizers.schedules import ExponentialDecay\n\n# ─────────────────────────────────────────────────────────────────────────────\n# Configuration\n# ─────────────────────────────────────────────────────────────────────────────\nCONDITION       = 'Spinal_Canal_Stenosis'\nFOLDS           = list(range(5))                # folds 0–4\nIMAGE_ROOT_TMPL = '/kaggle/working/Spinal_Canal_Stenosis/fold_{fold}'\nLABEL_ROOT_TMPL = '/kaggle/working/Spinal_Canal_Stenosis_label/fold_{fold}'\nBATCH_SIZE      = 16\nIMG_SIZE        = (224, 224)\nEPOCHS          = 10\nPATIENCE        = 5\n\n# Exponential decay LR: start at 0.01, decay by 0.5 every 1000 steps\nINITIAL_LR   = 0.01\nDECAY_STEPS  = 1000\nDECAY_RATE   = 0.5\nlr_schedule  = ExponentialDecay(\n    initial_learning_rate=INITIAL_LR,\n    decay_steps=DECAY_STEPS,\n    decay_rate=DECAY_RATE,\n    staircase=False\n)\n\n# Freeze the first N layers of ResNet50\nFREEZE_LAYERS = 50\n\n# Focal loss factory\ndef focal_loss(gamma=2.0, alpha=0.5):\n    def loss_fn(y_true, y_pred):\n        eps = tf.keras.backend.epsilon()\n        y_pred = tf.clip_by_value(y_pred, eps, 1. - eps)\n        ce = -y_true * tf.math.log(y_pred)\n        return tf.reduce_sum(alpha * tf.pow(1 - y_pred, gamma) * ce, axis=1)\n    return loss_fn\n\n# ─────────────────────────────────────────────────────────────────────────────\n# 5-Fold Training Loop\n# ─────────────────────────────────────────────────────────────────────────────\nfor fold in FOLDS:\n    print(f\"\\n▶ Training {CONDITION}, fold {fold}\")\n    IMG_DIR = IMAGE_ROOT_TMPL.format(fold=fold)\n    LAB_DIR = LABEL_ROOT_TMPL.format(fold=fold)\n\n    # Load labels\n    train_df = pd.read_csv(os.path.join(LAB_DIR, f'{CONDITION}_augmented_labels.csv'))\n    val_df   = pd.read_csv(os.path.join(LAB_DIR, f'{CONDITION}_val_labels.csv'))\n\n    # Cleanup filenames & labels\n    train_df['subject'] = train_df['subject'].str.replace('_augmented', '', regex=False)\n    val_df['subject']   = val_df['subject'].str.replace('_augmented', '', regex=False)\n    train_df['label']   = (train_df['label'].astype(int) - 1).astype(str)\n    val_df['label']     = (val_df['label'].astype(int) - 1).astype(str)\n\n    # Generators\n    datagen = ImageDataGenerator(rescale=1./255)\n    train_gen = datagen.flow_from_dataframe(\n        train_df, directory=os.path.join(IMG_DIR, 'train'),\n        x_col='subject', y_col='label',\n        target_size=IMG_SIZE, batch_size=BATCH_SIZE,\n        class_mode='categorical', shuffle=True\n    )\n    val_gen = datagen.flow_from_dataframe(\n        val_df, directory=os.path.join(IMG_DIR, 'val'),\n        x_col='subject', y_col='label',\n        target_size=IMG_SIZE, batch_size=BATCH_SIZE,\n        class_mode='categorical', shuffle=False\n    )\n\n    # Build & freeze base model\n    base = ResNet50(include_top=False, weights='imagenet', input_shape=IMG_SIZE + (3,))\n    for layer in base.layers[:FREEZE_LAYERS]:\n        layer.trainable = False\n    for layer in base.layers[FREEZE_LAYERS:]:\n        layer.trainable = True\n\n    x = GlobalAveragePooling2D()(base.output)\n    x = Dense(256, activation='relu')(x)\n    x = Dropout(0.5)(x)\n    outputs = Dense(3, activation='softmax')(x)\n    model = Model(inputs=base.input, outputs=outputs)\n\n    # Compile with exponential-decay LR\n    model.compile(\n        optimizer=Adam(learning_rate=lr_schedule),\n        loss=focal_loss(),\n        metrics=['accuracy']\n    )\n\n    # Callbacks\n    out_dir = Path(f'./results/{CONDITION}/fold_{fold}')\n    out_dir.mkdir(parents=True, exist_ok=True)\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    # Train\n    model.fit(\n        train_gen,\n        validation_data=val_gen,\n        epochs=EPOCHS,\n        callbacks=callbacks,\n        verbose=2\n    )\n\n    # Evaluate\n    val_gen.reset()\n    y_pred = np.argmax(model.predict(val_gen), axis=1)\n    y_true = val_gen.classes\n    print(\"\\n\" + classification_report(y_true, y_pred, target_names=['Mild','Moderate','Severe']))\n\n    # Cleanup\n    tf.keras.backend.clear_session()\n    import gc; gc.collect()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-01T12:57:07.388560Z","iopub.execute_input":"2025-07-01T12:57:07.388835Z","iopub.status.idle":"2025-07-01T14:13:18.652304Z","shell.execute_reply.started":"2025-07-01T12:57:07.388815Z","shell.execute_reply":"2025-07-01T14:13:18.651573Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import 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, classification_report\nfrom tensorflow.keras.applications import ResNet50\nfrom tensorflow.keras.models import Model\nfrom tensorflow.keras.layers import GlobalAveragePooling2D, Dense, Dropout\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint\nfrom tensorflow.keras.optimizers import Adam\nfrom tensorflow.keras.optimizers.schedules import ExponentialDecay\n\n# ─────────────────────────────────────────────────────────────────────────────\n# Configuration\n# ─────────────────────────────────────────────────────────────────────────────\nCONDITION       = 'Subarticular_Stenosis'\nFOLDS           = list(range(5))                # folds 0–4\nIMAGE_ROOT_TMPL = '/kaggle/working/Subarticular_Stenosis/fold_{fold}'\nLABEL_ROOT_TMPL = '/kaggle/working/Subarticular_Stenosis_label/fold_{fold}'\nBATCH_SIZE      = 16\nIMG_SIZE        = (224, 224)\nEPOCHS          = 10\nPATIENCE        = 5\n\n# Exponential-decay LR: start at 0.01, decay by 0.5 every 1000 steps\nINITIAL_LR   = 0.01\nDECAY_STEPS  = 1000\nDECAY_RATE   = 0.5\nlr_schedule  = ExponentialDecay(\n    initial_learning_rate=INITIAL_LR,\n    decay_steps=DECAY_STEPS,\n    decay_rate=DECAY_RATE,\n    staircase=False\n)\n\n# Freeze the first N layers of ResNet50\nFREEZE_LAYERS = 50\n\n# Focal loss factory\ndef focal_loss(gamma=2.0, alpha=0.5):\n    def loss_fn(y_true, y_pred):\n        eps = tf.keras.backend.epsilon()\n        y_pred = tf.clip_by_value(y_pred, eps, 1. - eps)\n        ce = -y_true * tf.math.log(y_pred)\n        return tf.reduce_sum(alpha * tf.pow(1 - y_pred, gamma) * ce, axis=1)\n    return loss_fn\n\n# ─────────────────────────────────────────────────────────────────────────────\n# 5-Fold Training Loop\n# ─────────────────────────────────────────────────────────────────────────────\nfor fold in FOLDS:\n    print(f\"\\n▶ Training {CONDITION}, fold {fold}\")\n    IMG_DIR = IMAGE_ROOT_TMPL.format(fold=fold)\n    LAB_DIR = LABEL_ROOT_TMPL.format(fold=fold)\n\n    # Load labels\n    train_df = pd.read_csv(os.path.join(LAB_DIR, f'{CONDITION}_augmented_labels.csv'))\n    val_df   = pd.read_csv(os.path.join(LAB_DIR, f'{CONDITION}_val_labels.csv'))\n\n    # Clean up filenames & labels\n    train_df['subject'] = train_df['subject'].str.replace('_augmented', '', regex=False)\n    val_df['subject']   = val_df['subject'].str.replace('_augmented', '', regex=False)\n    train_df['label']   = (train_df['label'].astype(int) - 1).astype(str)\n    val_df['label']     = (val_df['label'].astype(int) - 1).astype(str)\n\n    # Generators\n    datagen = ImageDataGenerator(rescale=1./255)\n    train_gen = datagen.flow_from_dataframe(\n        train_df, directory=os.path.join(IMG_DIR, 'train'),\n        x_col='subject', y_col='label',\n        target_size=IMG_SIZE, batch_size=BATCH_SIZE,\n        class_mode='categorical', shuffle=True\n    )\n    val_gen = datagen.flow_from_dataframe(\n        val_df, directory=os.path.join(IMG_DIR, 'val'),\n        x_col='subject', y_col='label',\n        target_size=IMG_SIZE, batch_size=BATCH_SIZE,\n        class_mode='categorical', shuffle=False\n    )\n\n    # Build & freeze base model\n    base = ResNet50(include_top=False, weights='imagenet', input_shape=IMG_SIZE + (3,))\n    for layer in base.layers[:FREEZE_LAYERS]:\n        layer.trainable = False\n    for layer in base.layers[FREEZE_LAYERS:]:\n        layer.trainable = True\n\n    x = GlobalAveragePooling2D()(base.output)\n    x = Dense(256, activation='relu')(x)\n    x = Dropout(0.5)(x)\n    outputs = Dense(3, activation='softmax')(x)\n    model = Model(inputs=base.input, outputs=outputs)\n\n    # Compile with exponential-decay LR\n    model.compile(\n        optimizer=Adam(learning_rate=lr_schedule),\n        loss=focal_loss(),\n        metrics=['accuracy']\n    )\n\n    # Callbacks\n    out_dir = Path(f'./results/{CONDITION}/fold_{fold}')\n    out_dir.mkdir(parents=True, exist_ok=True)\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    # Train\n    model.fit(\n        train_gen,\n        validation_data=val_gen,\n        epochs=EPOCHS,\n        callbacks=callbacks,\n        verbose=2\n    )\n\n    # Evaluate\n    val_gen.reset()\n    y_pred = np.argmax(model.predict(val_gen), axis=1)\n    y_true = val_gen.classes\n    print(\"\\n\" + classification_report(y_true, y_pred, target_names=['Mild','Moderate','Severe']))\n\n    # Cleanup\n    tf.keras.backend.clear_session()\n    import gc; gc.collect()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-01T14:13:26.581997Z","iopub.execute_input":"2025-07-01T14:13:26.582260Z","iopub.status.idle":"2025-07-01T15:53:58.001248Z","shell.execute_reply.started":"2025-07-01T14:13:26.582244Z","shell.execute_reply":"2025-07-01T15:53:58.000490Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport tensorflow as tf\n\nfrom pathlib import Path\nfrom sklearn.metrics import (\n    confusion_matrix, ConfusionMatrixDisplay,\n    roc_curve, auc\n)\nfrom sklearn.preprocessing import label_binarize\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\n\n# --- must match your training config ---\nCONDITIONS = [\n    'Neural_Foraminal_Narrowing',\n    'Spinal_Canal_Stenosis',\n    'Subarticular_Stenosis'\n]\nFOLDS = list(range(5))   # ← now 5 folds: [0,1,2,3,4]\nIMAGE_ROOT_TMPL = '/kaggle/working/{condition}/fold_{fold}'\nLABEL_ROOT_TMPL = '/kaggle/working/{condition}_label/fold_{fold}'\nBATCH_SIZE = 16\nIMG_SIZE = (224, 224)\n\n# focal loss factory (needed to load)\ndef focal_loss(gamma=2., alpha=0.25):\n    def loss_fn(y_true, y_pred):\n        eps = tf.keras.backend.epsilon()\n        y_pred = tf.clip_by_value(y_pred, eps, 1.-eps)\n        ce = -y_true * tf.math.log(y_pred)\n        return tf.reduce_sum(alpha * (1.-y_pred)**gamma * ce, axis=1)\n    return loss_fn\n\nfor condition in CONDITIONS:\n    # accumulate across folds\n    y_true_all = []\n    y_score_all = []\n    class_indices = None\n\n    for fold in FOLDS:\n        print(f\"→ Loading {condition} / fold {fold}\")\n        # 1) reload val labels\n        label_root = LABEL_ROOT_TMPL.format(condition=condition, fold=fold)\n        val_df = pd.read_csv(os.path.join(label_root, f'{condition}_val_labels.csv'))\n        val_df['subject'] = val_df['subject'].str.replace('_augmented','',regex=False)\n        val_df['label']   = (val_df['label'].astype(int)-1).astype(str)\n\n        # 2) rebuild val generator\n        datagen = ImageDataGenerator(rescale=1./255)\n        img_dir = os.path.join(IMAGE_ROOT_TMPL.format(condition=condition, fold=fold), 'val')\n        val_gen = datagen.flow_from_dataframe(\n            val_df, directory=img_dir,\n            x_col='subject', y_col='label',\n            target_size=IMG_SIZE, batch_size=BATCH_SIZE,\n            class_mode='categorical', shuffle=False\n        )\n\n        if class_indices is None:\n            class_indices = val_gen.class_indices  # save mapping str→int\n\n        # 3) load model\n        model_path = Path(f'./results/{condition}/fold_{fold}/best_model.keras')\n        model = tf.keras.models.load_model(\n            model_path,\n            custom_objects={'loss_fn': focal_loss(2.0,0.5),\n                            'focal_loss_fixed': focal_loss(2.0,0.5)}\n        )\n\n        # 4) predict\n        val_gen.reset()\n        prob = model.predict(val_gen, verbose=0)\n        y_score_all.append(prob)\n        y_true_all.append(val_gen.classes)\n\n        tf.keras.backend.clear_session()\n\n    # concat across folds\n    y_true = np.concatenate(y_true_all)\n    y_score = np.concatenate(y_score_all)\n    y_pred  = np.argmax(y_score, axis=1)\n\n    # invert class_indices to get labels in order\n    inv_map = {v:k for k,v in class_indices.items()}\n    labels = [inv_map[i] for i in range(len(inv_map))]\n\n    # ----- confusion matrix -----\n    cm = confusion_matrix(y_true, y_pred)\n    disp = ConfusionMatrixDisplay(cm, display_labels=labels)\n    fig, ax = plt.subplots(figsize=(6,6))\n    disp.plot(ax=ax, cmap='Blues', colorbar=False)\n    ax.set_title(f'{condition} — Confusion Matrix\\n(all folds)')\n    plt.show()\n\n    # ----- ROC curves -----\n    n_classes = len(inv_map)\n    y_true_bin = label_binarize(y_true, classes=list(range(n_classes)))\n\n    fpr = dict(); tpr = dict(); roc_auc = dict()\n    for i in range(n_classes):\n        fpr[i], tpr[i], _ = roc_curve(y_true_bin[:,i], y_score[:,i])\n        roc_auc[i] = auc(fpr[i], tpr[i])\n\n    # macro-average\n    fpr[\"macro\"], tpr[\"macro\"], _ = roc_curve(y_true_bin.ravel(), y_score.ravel())\n    roc_auc[\"macro\"] = auc(fpr[\"macro\"], tpr[\"macro\"])\n\n    plt.figure(figsize=(8,6))\n    for i in range(n_classes):\n        plt.plot(fpr[i], tpr[i],\n                 label=f'Class {labels[i]} (AUC={roc_auc[i]:.2f})')\n    plt.plot(fpr[\"macro\"], tpr[\"macro\"], linestyle='--',\n             label=f'Macro-avg (AUC={roc_auc[\"macro\"]:.2f})')\n    plt.plot([0,1],[0,1],'k--', alpha=0.5)\n    plt.xlabel('False Positive Rate'); plt.ylabel('True Positive Rate')\n    plt.title(f'{condition} — ROC Curves (all folds)')\n    plt.legend(loc='lower right')\n    plt.tight_layout()\n    plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-01T16:01:26.895115Z","iopub.execute_input":"2025-07-01T16:01:26.895474Z","iopub.status.idle":"2025-07-01T16:06:48.619155Z","shell.execute_reply.started":"2025-07-01T16:01:26.895450Z","shell.execute_reply":"2025-07-01T16:06:48.618449Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport tensorflow as tf\n\nfrom pathlib import Path\nfrom sklearn.metrics import (\n    accuracy_score, precision_score, recall_score, f1_score,\n    confusion_matrix, ConfusionMatrixDisplay,\n    roc_curve, auc\n)\nfrom sklearn.preprocessing import label_binarize\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\n\n# --- must match your training config ---\nCONDITIONS = [\n    'Neural_Foraminal_Narrowing',\n    'Spinal_Canal_Stenosis',\n    'Subarticular_Stenosis'\n]\nFOLDS = list(range(5))   # ← now 5 folds: 0,1,2,3,4\nIMAGE_ROOT_TMPL = '/kaggle/working/{condition}/fold_{fold}'\nLABEL_ROOT_TMPL = '/kaggle/working/{condition}_label/fold_{fold}'\nRESULTS_DIR    = './results/{condition}/fold_{fold}/best_model.keras'\nBATCH_SIZE     = 16\nIMG_SIZE       = (224, 224)\n\n# focal loss factory (for loading .keras)\ndef focal_loss(gamma=2., alpha=0.25):\n    def loss_fn(y_true, y_pred):\n        eps = tf.keras.backend.epsilon()\n        y_pred = tf.clip_by_value(y_pred, eps, 1.-eps)\n        ce = -y_true * tf.math.log(y_pred)\n        return tf.reduce_sum(alpha * (1.-y_pred)**gamma * ce, axis=1)\n    return loss_fn\n\nfor condition in CONDITIONS:\n    for fold in FOLDS:\n        print(f\"\\n=== {condition} | fold {fold} ===\")\n\n        # 1) load & prep train labels + generator\n        label_root = LABEL_ROOT_TMPL.format(condition=condition, fold=fold)\n        train_df = pd.read_csv(os.path.join(label_root, f'{condition}_augmented_labels.csv'))\n        train_df['subject'] = train_df['subject'].str.replace('_augmented','',regex=False)\n        train_df['label']   = (train_df['label'].astype(int)-1).astype(str)\n\n        datagen = ImageDataGenerator(rescale=1./255)\n        train_gen = datagen.flow_from_dataframe(\n            dataframe=train_df,\n            directory=os.path.join(IMAGE_ROOT_TMPL.format(condition=condition, fold=fold),'train'),\n            x_col='subject', y_col='label',\n            target_size=IMG_SIZE, batch_size=BATCH_SIZE,\n            class_mode='categorical', shuffle=False\n        )\n\n        # 2) load & prep val labels + generator\n        val_df = pd.read_csv(os.path.join(label_root, f'{condition}_val_labels.csv'))\n        val_df['subject'] = val_df['subject'].str.replace('_augmented','',regex=False)\n        val_df['label']   = (val_df['label'].astype(int)-1).astype(str)\n\n        val_gen = datagen.flow_from_dataframe(\n            dataframe=val_df,\n            directory=os.path.join(IMAGE_ROOT_TMPL.format(condition=condition, fold=fold),'val'),\n            x_col='subject', y_col='label',\n            target_size=IMG_SIZE, batch_size=BATCH_SIZE,\n            class_mode='categorical', shuffle=False\n        )\n\n        # invert class_indices → sorted label names\n        inv_map = {v:k for k,v in train_gen.class_indices.items()}\n        labels = [inv_map[i] for i in range(len(inv_map))]\n\n        # 3) load model once\n        model = tf.keras.models.load_model(\n            Path(RESULTS_DIR.format(condition=condition, fold=fold)),\n            custom_objects={'loss_fn':         focal_loss(2.0,0.5),\n                            'focal_loss_fixed': focal_loss(2.0,0.5)}\n        )\n\n        # 4) eval & plot for both splits\n        for split, gen in [('train', train_gen), ('val', val_gen)]:\n            print(f\"\\n→ {split.upper()} results:\")\n            gen.reset()\n            y_score = model.predict(gen, verbose=0)\n            y_true  = gen.classes\n            y_pred  = np.argmax(y_score, axis=1)\n\n            # metrics\n            acc  = accuracy_score(y_true, y_pred)\n            prec = precision_score(y_true, y_pred, average='weighted', zero_division=0)\n            rec  = recall_score(y_true, y_pred, average='weighted', zero_division=0)\n            f1   = f1_score(y_true, y_pred, average='weighted', zero_division=0)\n            print(f\"  Acc: {acc:.3f}  Prec: {prec:.3f}  Rec: {rec:.3f}  F1: {f1:.3f}\")\n\n            # confusion matrix\n            cm = confusion_matrix(y_true, y_pred)\n            fig, ax = plt.subplots(figsize=(5,5))\n            disp = ConfusionMatrixDisplay(cm, display_labels=labels)\n            disp.plot(ax=ax, cmap='Blues', colorbar=False)\n            ax.set_title(f'{condition} | fold {fold} | {split} Confusion Matrix')\n            plt.show()\n\n            # ROC\n            n_classes = len(labels)\n            y_true_bin = label_binarize(y_true, classes=list(range(n_classes)))\n\n            fpr, tpr, roc_auc = {}, {}, {}\n            for i in range(n_classes):\n                fpr[i], tpr[i], _ = roc_curve(y_true_bin[:,i], y_score[:,i])\n                roc_auc[i] = auc(fpr[i], tpr[i])\n            fpr[\"macro\"], tpr[\"macro\"], _ = roc_curve(y_true_bin.ravel(), y_score.ravel())\n            roc_auc[\"macro\"] = auc(fpr[\"macro\"], tpr[\"macro\"])\n\n            plt.figure(figsize=(6,5))\n            for i in range(n_classes):\n                plt.plot(fpr[i], tpr[i],\n                         label=f'{labels[i]} (AUC={roc_auc[i]:.2f})')\n            plt.plot(fpr[\"macro\"], tpr[\"macro\"], linestyle='--',\n                     label=f'Macro (AUC={roc_auc[\"macro\"]:.2f})')\n            plt.plot([0,1],[0,1],'k--', alpha=0.3)\n            plt.xlabel('False Positive Rate')\n            plt.ylabel('True Positive Rate')\n            plt.title(f'{condition} | fold {fold} | {split} ROC Curves')\n            plt.legend(loc='lower right')\n            plt.tight_layout()\n            plt.show()\n\n        tf.keras.backend.clear_session()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-01T16:34:24.091565Z","iopub.execute_input":"2025-07-01T16:34:24.092122Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}