{"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,"isSourceIdPinned":false,"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 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-24T08:50:54.214377Z","iopub.execute_input":"2025-06-24T08:50:54.214641Z","iopub.status.idle":"2025-06-24T08:50:56.252153Z","shell.execute_reply.started":"2025-06-24T08:50:54.214622Z","shell.execute_reply":"2025-06-24T08:50:56.251381Z"}},"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-24T08:50:56.253364Z","iopub.execute_input":"2025-06-24T08:50:56.253617Z","iopub.status.idle":"2025-06-24T08:50:56.873048Z","shell.execute_reply.started":"2025-06-24T08:50:56.253599Z","shell.execute_reply":"2025-06-24T08:50:56.872509Z"}},"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-24T08:50:56.873765Z","iopub.execute_input":"2025-06-24T08:50:56.874029Z","iopub.status.idle":"2025-06-24T09:15:13.731248Z","shell.execute_reply.started":"2025-06-24T08:50:56.874011Z","shell.execute_reply":"2025-06-24T09:15:13.730625Z"}},"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-24T09:15:13.732705Z","iopub.execute_input":"2025-06-24T09:15:13.732912Z","iopub.status.idle":"2025-06-24T09:15:17.309267Z","shell.execute_reply.started":"2025-06-24T09:15:13.732896Z","shell.execute_reply":"2025-06-24T09:15:17.308542Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#neural foraminal training cell\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 ImageDataGenerator\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\nEPOCHS = 10\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\n# For each fold\nfor fold in FOLDS:\n    print(f\"\\n▶ Training {CONDITION}, fold {fold}\")\n    IMAGE_ROOT = IMAGE_ROOT_TMPL.format(fold=fold)\n    LABEL_ROOT = LABEL_ROOT_TMPL.format(fold=fold)\n\n    # Load and process CSVs\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    # Remove \"_augmented\" from filenames\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\n    # Adjust label indexing (ensure string classes: \"0\", \"1\", \"2\")\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    # Data generators\n    datagen = ImageDataGenerator(rescale=1./255)\n\n    train_gen = datagen.flow_from_dataframe(\n        dataframe=train_df,\n        directory=os.path.join(IMAGE_ROOT, 'train'),\n        x_col='subject',\n        y_col='label',\n        target_size=IMG_SIZE,\n        batch_size=BATCH_SIZE,\n        class_mode='categorical',\n        shuffle=True\n    )\n\n    val_gen = datagen.flow_from_dataframe(\n        dataframe=val_df,\n        directory=os.path.join(IMAGE_ROOT, 'val'),\n        x_col='subject',\n        y_col='label',\n        target_size=IMG_SIZE,\n        batch_size=BATCH_SIZE,\n        class_mode='categorical',\n        shuffle=False\n    )\n\n    # Model\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(\n        optimizer=tf.keras.optimizers.Adam(learning_rate=LEARNING_RATE),\n        loss=focal_loss(gamma=2.0, alpha=0.5),\n        metrics=['accuracy']\n    )\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(\n        train_gen,\n        validation_data=val_gen,\n        epochs=EPOCHS,\n        callbacks=callbacks,\n        verbose=2\n    )\n\n    # Evaluation\n    val_gen.reset()\n    y_pred = np.argmax(model.predict(val_gen), axis=1)\n    y_true = val_gen.classes\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\n    # Clean up memory\n    tf.keras.backend.clear_session()\n    import gc; gc.collect()\n","metadata":{"trusted":true,"scrolled":true,"execution":{"iopub.status.busy":"2025-06-24T09:15:17.310169Z","iopub.execute_input":"2025-06-24T09:15:17.310508Z","iopub.status.idle":"2025-06-24T09:48:06.348780Z","shell.execute_reply.started":"2025-06-24T09:15:17.310475Z","shell.execute_reply":"2025-06-24T09:48:06.348019Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#spinal canal steno\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 ImageDataGenerator\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\nEPOCHS = 10\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\n# For each fold\nfor fold in FOLDS:\n    print(f\"\\n▶ Training {CONDITION}, fold {fold}\")\n    IMAGE_ROOT = IMAGE_ROOT_TMPL.format(fold=fold)\n    LABEL_ROOT = LABEL_ROOT_TMPL.format(fold=fold)\n\n    # Load and process CSVs\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    # Remove \"_augmented\" from filenames\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\n    # Adjust label indexing (ensure string classes: \"0\", \"1\", \"2\")\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    # Data generators\n    datagen = ImageDataGenerator(rescale=1./255)\n\n    train_gen = datagen.flow_from_dataframe(\n        dataframe=train_df,\n        directory=os.path.join(IMAGE_ROOT, 'train'),\n        x_col='subject',\n        y_col='label',\n        target_size=IMG_SIZE,\n        batch_size=BATCH_SIZE,\n        class_mode='categorical',\n        shuffle=True\n    )\n\n    val_gen = datagen.flow_from_dataframe(\n        dataframe=val_df,\n        directory=os.path.join(IMAGE_ROOT, 'val'),\n        x_col='subject',\n        y_col='label',\n        target_size=IMG_SIZE,\n        batch_size=BATCH_SIZE,\n        class_mode='categorical',\n        shuffle=False\n    )\n\n    # Model\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(\n        optimizer=tf.keras.optimizers.Adam(learning_rate=LEARNING_RATE),\n        loss=focal_loss(gamma=2.0, alpha=0.5),\n        metrics=['accuracy']\n    )\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(\n        train_gen,\n        validation_data=val_gen,\n        epochs=EPOCHS,\n        callbacks=callbacks,\n        verbose=2\n    )\n\n    # Evaluation\n    val_gen.reset()\n    y_pred = np.argmax(model.predict(val_gen), axis=1)\n    y_true = val_gen.classes\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\n    # Clean up memory\n    tf.keras.backend.clear_session()\n    import gc; gc.collect()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-24T09:48:06.364513Z","iopub.execute_input":"2025-06-24T09:48:06.364739Z","iopub.status.idle":"2025-06-24T10:08:28.077051Z","shell.execute_reply.started":"2025-06-24T09:48:06.364721Z","shell.execute_reply":"2025-06-24T10:08:28.076389Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#spinal canal stenosis training cell\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 ImageDataGenerator\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\nEPOCHS = 10\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\n# For each fold\nfor fold in FOLDS:\n    print(f\"\\n▶ Training {CONDITION}, fold {fold}\")\n    IMAGE_ROOT = IMAGE_ROOT_TMPL.format(fold=fold)\n    LABEL_ROOT = LABEL_ROOT_TMPL.format(fold=fold)\n\n    # Load and process CSVs\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    # Remove \"_augmented\" from filenames\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\n    # Adjust label indexing (ensure string classes: \"0\", \"1\", \"2\")\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    # Data generators\n    datagen = ImageDataGenerator(rescale=1./255)\n\n    train_gen = datagen.flow_from_dataframe(\n        dataframe=train_df,\n        directory=os.path.join(IMAGE_ROOT, 'train'),\n        x_col='subject',\n        y_col='label',\n        target_size=IMG_SIZE,\n        batch_size=BATCH_SIZE,\n        class_mode='categorical',\n        shuffle=True\n    )\n\n    val_gen = datagen.flow_from_dataframe(\n        dataframe=val_df,\n        directory=os.path.join(IMAGE_ROOT, 'val'),\n        x_col='subject',\n        y_col='label',\n        target_size=IMG_SIZE,\n        batch_size=BATCH_SIZE,\n        class_mode='categorical',\n        shuffle=False\n    )\n\n    # Model\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(\n        optimizer=tf.keras.optimizers.Adam(learning_rate=LEARNING_RATE),\n        loss=focal_loss(gamma=2.0, alpha=0.5),\n        metrics=['accuracy']\n    )\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(\n        train_gen,\n        validation_data=val_gen,\n        epochs=EPOCHS,\n        callbacks=callbacks,\n        verbose=2\n    )\n\n    # Evaluation\n    val_gen.reset()\n    y_pred = np.argmax(model.predict(val_gen), axis=1)\n    y_true = val_gen.classes\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\n    # Clean up memory\n    tf.keras.backend.clear_session()\n    import gc; gc.collect()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-24T10:08:28.098443Z","iopub.execute_input":"2025-06-24T10:08:28.098691Z","iopub.status.idle":"2025-06-24T10:34:56.055108Z","shell.execute_reply.started":"2025-06-24T10:08:28.098673Z","shell.execute_reply":"2025-06-24T10:34:56.054533Z"}},"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 = [0, 1]\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-06-24T10:34:56.072413Z","iopub.execute_input":"2025-06-24T10:34:56.072644Z","iopub.status.idle":"2025-06-24T10:37:38.313210Z","shell.execute_reply.started":"2025-06-24T10:34:56.072625Z","shell.execute_reply":"2025-06-24T10:37:38.312550Z"}},"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 = [0, 1]\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-06-24T10:51:15.402591Z","iopub.execute_input":"2025-06-24T10:51:15.403236Z","iopub.status.idle":"2025-06-24T10:56:07.736208Z","shell.execute_reply.started":"2025-06-24T10:51:15.403215Z","shell.execute_reply":"2025-06-24T10:56:07.735625Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}