{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# =============================================================================\n# Brain Tumor Classification — Paper Architecture Replication\n# Based on: \"An interpretable CNN framework for multi-class brain tumor\n# classification with visual explanation in MRI scans\" (AlShehri et al., 2026)\n#\n# HOW TO USE ON KAGGLE:\n# 1. Create a new Kaggle Notebook.\n# 2. Add your dataset (paper's original dataset first, then new ones later)\n#    via \"+ Add Data\" — note the folder path it gives you.\n# 3. Paste this whole file into a cell (or split by the \"# %%\" markers into\n#    separate cells) and set DATA_DIR below to match your dataset path.\n# 4. Turn on GPU (Settings > Accelerator > GPU T4 x2 or similar).\n# 5. Run all. Send me: final test accuracy, confusion matrix, classification\n#    report, and a few misclassified example images if possible.\n# =============================================================================\n\n# %% [CELL 1] Imports\nimport os\nimport numpy as np\nimport tensorflow as tf\nfrom tensorflow.keras import layers, models, optimizers\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom sklearn.metrics import (\n    classification_report,\n    confusion_matrix,\n    ConfusionMatrixDisplay,\n)\nimport matplotlib.pyplot as plt\n\nprint(\"TF version:\", tf.__version__)\nprint(\"GPUs available:\", tf.config.list_physical_devices(\"GPU\"))\n\n# %% [CELL 2] Config — EDIT THIS PER DATASET\n# -----------------------------------------------------------------------------\n# Expected folder structure (standard Kaggle brain-tumor layout):\n#   DATA_DIR/\n#     Training/  (or train/)\n#       glioma/\n#       meningioma/\n#       pituitary/\n#       notumor/\n#     Testing/   (or test/)\n#       glioma/\n#       meningioma/\n#       pituitary/\n#       notumor/\n#\n# If your new dataset has different class names or only one folder (no\n# train/test split), tell me and I'll adjust the loading code — don't just\n# rename folders yourself, since class-name mismatches are exactly the kind\n# of thing we want to track carefully.\n# -----------------------------------------------------------------------------\n\nDATA_DIR = \"/kaggle/input/datasets/masoudnickparvar/brain-tumor-mri-dataset\"  # <-- CHANGE per dataset\nTRAIN_SUBDIR = \"Training\"\nTEST_SUBDIR = \"Testing\"\n\nIMG_SIZE = (125, 125)      # paper uses 125x125\nBATCH_SIZE = 8             # paper uses batch size 8\nEPOCHS = 50                # paper uses 50 epochs\nNUM_CLASSES = 4\nCLASS_NAMES = [\"glioma\", \"meningioma\", \"notumor\", \"pituitary\"]  # alphabetical\n# (Keras flow_from_directory sorts classes alphabetically — verify this\n# matches train_gen.class_indices after building the generator below.)\n\nMODEL_SAVE_PATH = \"/kaggle/working/brain_tumor_cnn.keras\"\n\n# %% [CELL 3] Data generators (exact augmentation params from the paper)\ntrain_datagen = ImageDataGenerator(\n    rescale=1.0 / 255,\n    rotation_range=10,\n    width_shift_range=0.05,\n    height_shift_range=0.05,\n    horizontal_flip=True,\n    validation_split=0.2,   # paper: 20% of training set held out for validation\n)\n\ntest_datagen = ImageDataGenerator(rescale=1.0 / 255)  # no augmentation on test\n\ntrain_gen = train_datagen.flow_from_directory(\n    os.path.join(DATA_DIR, TRAIN_SUBDIR),\n    target_size=IMG_SIZE,\n    color_mode=\"grayscale\",\n    batch_size=BATCH_SIZE,\n    class_mode=\"categorical\",\n    subset=\"training\",\n    shuffle=True,\n    seed=42,\n)\n\nval_gen = train_datagen.flow_from_directory(\n    os.path.join(DATA_DIR, TRAIN_SUBDIR),\n    target_size=IMG_SIZE,\n    color_mode=\"grayscale\",\n    batch_size=BATCH_SIZE,\n    class_mode=\"categorical\",\n    subset=\"validation\",\n    shuffle=False,\n)\n\ntest_gen = test_datagen.flow_from_directory(\n    os.path.join(DATA_DIR, TEST_SUBDIR),\n    target_size=IMG_SIZE,\n    color_mode=\"grayscale\",\n    batch_size=BATCH_SIZE,\n    class_mode=\"categorical\",\n    shuffle=False,\n)\n\nprint(\"Class indices (verify this matches CLASS_NAMES order above):\")\nprint(train_gen.class_indices)\n\n# %% [CELL 4] Model architecture — exact replica of the paper's Fig. 4\ndef build_model(input_shape=(125, 125, 1), num_classes=4):\n    inputs = layers.Input(shape=input_shape)\n\n    # Block 1: 2x Conv2D(64), BN+ReLU each, MaxPool, Dropout 0.25\n    x = layers.Conv2D(64, 3, padding=\"same\")(inputs)\n    x = layers.BatchNormalization()(x)\n    x = layers.ReLU()(x)\n    x = layers.Conv2D(64, 3, padding=\"same\")(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.ReLU()(x)\n    x = layers.MaxPooling2D()(x)\n    x = layers.Dropout(0.25)(x)\n\n    # Block 2: 2x Conv2D(128), MaxPool, Dropout 0.25\n    x = layers.Conv2D(128, 3, padding=\"same\")(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.ReLU()(x)\n    x = layers.Conv2D(128, 3, padding=\"same\")(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.ReLU()(x)\n    x = layers.MaxPooling2D()(x)\n    x = layers.Dropout(0.25)(x)\n\n    # Block 3: 3x Conv2D(256), MaxPool, Dropout 0.3\n    for _ in range(3):\n        x = layers.Conv2D(256, 3, padding=\"same\")(x)\n        x = layers.BatchNormalization()(x)\n        x = layers.ReLU()(x)\n    x = layers.MaxPooling2D()(x)\n    x = layers.Dropout(0.3)(x)\n\n    # Block 4: 3x Conv2D(256), MaxPool, Dropout 0.3\n    for _ in range(3):\n        x = layers.Conv2D(256, 3, padding=\"same\")(x)\n        x = layers.BatchNormalization()(x)\n        x = layers.ReLU()(x)\n    x = layers.MaxPooling2D()(x)\n    x = layers.Dropout(0.3)(x)\n\n    # Block 5: 3x Conv2D(256), MaxPool, Dropout 0.3\n    for _ in range(3):\n        x = layers.Conv2D(256, 3, padding=\"same\")(x)\n        x = layers.BatchNormalization()(x)\n        x = layers.ReLU()(x)\n    x = layers.MaxPooling2D()(x)\n    x = layers.Dropout(0.3)(x)\n\n    # Global pooling + classifier head\n    x = layers.GlobalAveragePooling2D()(x)\n    x = layers.Dense(256, activation=\"relu\")(x)\n    x = layers.Dropout(0.4)(x)\n    x = layers.Dense(64, activation=\"relu\")(x)\n    x = layers.Dropout(0.3)(x)\n    outputs = layers.Dense(num_classes, activation=\"softmax\")(x)\n\n    return models.Model(inputs, outputs, name=\"paper_lightweight_cnn\")\n\n\nmodel = build_model(input_shape=(*IMG_SIZE, 1), num_classes=NUM_CLASSES)\nmodel.summary()\n\n# %% [CELL 5] Compile — paper's exact optimizer/loss\nmodel.compile(\n    optimizer=optimizers.Adamax(learning_rate=0.001),\n    loss=\"categorical_crossentropy\",\n    metrics=[\"accuracy\"],\n)\n\n# %% [CELL 6] Train\n# Note: paper reports NOT using early stopping, just checkpointing the best\n# validation epoch. Replicating that here.\ncheckpoint_cb = tf.keras.callbacks.ModelCheckpoint(\n    MODEL_SAVE_PATH,\n    monitor=\"val_accuracy\",\n    save_best_only=True,\n    mode=\"max\",\n    verbose=1,\n)\n\nhistory = model.fit(\n    train_gen,\n    validation_data=val_gen,\n    epochs=EPOCHS,\n    callbacks=[checkpoint_cb],\n)\n\n# %% [CELL 7] Plot training curves\nfig, axes = plt.subplots(1, 2, figsize=(12, 4))\naxes[0].plot(history.history[\"accuracy\"], label=\"train\")\naxes[0].plot(history.history[\"val_accuracy\"], label=\"val\")\naxes[0].set_title(\"Accuracy\")\naxes[0].legend()\n\naxes[1].plot(history.history[\"loss\"], label=\"train\")\naxes[1].plot(history.history[\"val_loss\"], label=\"val\")\naxes[1].set_title(\"Loss\")\naxes[1].legend()\nplt.tight_layout()\nplt.savefig(\"/kaggle/working/training_curves.png\")\nplt.show()\n\n# %% [CELL 8] Load best checkpoint and evaluate on TEST set\nbest_model = tf.keras.models.load_model(MODEL_SAVE_PATH)\n\ntest_loss, test_acc = best_model.evaluate(test_gen)\nprint(f\"\\nTEST ACCURACY: {test_acc*100:.2f}%\")\nprint(f\"TEST LOSS: {test_loss:.4f}\")\n\n# %% [CELL 9] Confusion matrix + per-class precision/recall/F1\ny_true = test_gen.classes\ny_pred_probs = best_model.predict(test_gen)\ny_pred = np.argmax(y_pred_probs, axis=1)\n\nlabel_map = {v: k for k, v in test_gen.class_indices.items()}\ntarget_names = [label_map[i] for i in range(NUM_CLASSES)]\n\nprint(\"\\nClassification report:\")\nprint(classification_report(y_true, y_pred, target_names=target_names, digits=4))\n\ncm = confusion_matrix(y_true, y_pred)\ndisp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=target_names)\nfig, ax = plt.subplots(figsize=(6, 6))\ndisp.plot(ax=ax, cmap=\"Blues\", values_format=\"d\")\nplt.title(\"Confusion Matrix — Test Set\")\nplt.savefig(\"/kaggle/working/confusion_matrix.png\")\nplt.show()\n\n# %% [CELL 10] Save a table of misclassified examples for inspection\n# This is the important part for the \"where does it fail\" analysis.\nmisclassified_idx = np.where(y_true != y_pred)[0]\nprint(f\"\\nTotal misclassified: {len(misclassified_idx)} / {len(y_true)}\")\n\nfilepaths = np.array(test_gen.filepaths)\nmis_records = []\nfor idx in misclassified_idx:\n    mis_records.append({\n        \"filepath\": filepaths[idx],\n        \"true_label\": label_map[y_true[idx]],\n        \"pred_label\": label_map[y_pred[idx]],\n        \"confidence\": float(np.max(y_pred_probs[idx])),\n    })\n\nimport pandas as pd\nmis_df = pd.DataFrame(mis_records)\nmis_df.to_csv(\"/kaggle/working/misclassified_samples.csv\", index=False)\nprint(mis_df.head(20))\n\n# %% [CELL 11] Visualize a handful of misclassified images\nn_show = min(8, len(misclassified_idx))\nif n_show > 0:\n    fig, axes = plt.subplots(2, 4, figsize=(16, 8))\n    axes = axes.flatten()\n    for i in range(n_show):\n        idx = misclassified_idx[i]\n        img = plt.imread(filepaths[idx])\n        axes[i].imshow(img, cmap=\"gray\")\n        axes[i].set_title(\n            f\"True: {label_map[y_true[idx]]}\\nPred: {label_map[y_pred[idx]]} \"\n            f\"({y_pred_probs[idx].max()*100:.1f}%)\",\n            fontsize=9,\n        )\n        axes[i].axis(\"off\")\n    plt.tight_layout()\n    plt.savefig(\"/kaggle/working/misclassified_examples.png\")\n    plt.show()\n\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-09-14T16:59:46.381093Z","iopub.execute_input":"2026-09-14T16:59:46.381663Z","iopub.status.idle":"2026-09-14T17:28:26.609808Z","shell.execute_reply.started":"2026-09-14T16:59:46.381633Z","shell.execute_reply":"2026-09-14T17:28:26.608714Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}