{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"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":"nvidiaTeslaT4","dataSources":[{"databundleVersionId":875431,"isSourceIdPinned":false,"mountSlug":"competitions/aptos2019-blindness-detection","sourceId":14774,"sourceType":"competition"}],"dockerImageVersionId":31401,"isGpuEnabled":true,"isInternetEnabled":true,"language":"python","sourceType":"notebook"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"1dbac06c-05fa-4d18-b1df-247f1d0eeb2a","cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n\nimport numpy as np  # linear algebra\nimport pandas as pd  # data processing, CSV file I/O (e.g. pd.read_csv)\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\nimport kagglehub\n# kagglehub.dataset_download('<owner>/<dataset-slug>')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-05T10:09:37.945938Z","iopub.execute_input":"2026-06-05T10:09:37.946349Z","iopub.status.idle":"2026-06-05T10:09:40.011627Z","shell.execute_reply.started":"2026-06-05T10:09:37.946322Z","shell.execute_reply":"2026-06-05T10:09:40.010890Z"}},"outputs":[],"execution_count":null},{"id":"f0ea2ac4-b30f-4740-bee2-e45010841ad1","cell_type":"code","source":"# ============================================================\n# Cell 1: Imports\n# CHANGE: Added TensorFlow/Keras imports for MobileNetV2.\n# PyTorch is still imported for transforms & DataLoader so\n# the CLAHE dataset pipeline remains completely identical.\n# ============================================================\n\nimport os\nimport random\nimport warnings\nwarnings.filterwarnings('ignore')\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport cv2\nfrom PIL import Image\nfrom tqdm import tqdm\n\n# PyTorch — kept for CLAHE dataset & DataLoader (unchanged)\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nimport torchvision.transforms as T\nimport torchvision.models as models\nfrom torch.optim.lr_scheduler import OneCycleLR\n\n# CHANGE: TensorFlow/Keras imports for MobileNetV2 feature extractor\nimport tensorflow as tf\nfrom tensorflow.keras.applications import MobileNetV2\nfrom tensorflow.keras.applications.mobilenet_v2 import preprocess_input as mobilenet_preprocess\nfrom tensorflow.keras.models import Model\n\nfrom sklearn.metrics import (\n    accuracy_score, precision_score, recall_score,\n    f1_score, confusion_matrix, classification_report,\n    cohen_kappa_score\n)\nfrom sklearn.model_selection import train_test_split, GridSearchCV\nfrom sklearn.svm import SVC\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.utils.class_weight import compute_class_weight\n\nprint(\"All libraries imported!\")\nprint(f\"TensorFlow version: {tf.__version__}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-05T10:09:40.013215Z","iopub.execute_input":"2026-06-05T10:09:40.013440Z","iopub.status.idle":"2026-06-05T10:09:40.021359Z","shell.execute_reply.started":"2026-06-05T10:09:40.013418Z","shell.execute_reply":"2026-06-05T10:09:40.020404Z"}},"outputs":[],"execution_count":null},{"id":"6db6f021-a2e1-402e-a31f-3a9305769581","cell_type":"code","source":"# ============================================================\n# Cell 2: Configuration\n# CHANGE: Updated MODEL_NAME string to reflect MobileNetV2.\n# All numeric hyper-parameters are identical to the original.\n# ============================================================\n\nSEED = 42\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\ntorch.cuda.manual_seed_all(SEED)\ntf.random.set_seed(SEED)\n\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f'Device: {DEVICE}')\n\nIMG_SIZE   = 224\nBATCH_SIZE = 64\nEPOCHS     = 30\nBACKBONE_LR    = 2e-4\nHEAD_LR        = 1e-3\nWEIGHT_DECAY   = 1e-4\nPATIENCE       = 8\nMIXUP_PROB     = 0.2\nLABEL_SMOOTHING = 0.1\n\nBASE        = '/kaggle/input/competitions/aptos2019-blindness-detection'\nCLASS_NAMES = ['No DR', 'Mild', 'Moderate', 'Severe', 'Proliferative']\nNUM_CLASSES = 5\n\n# ImageNet mean/std – used for PyTorch DataLoader (CLAHE pipeline, unchanged)\nMEAN = [0.485, 0.456, 0.406]\nSTD  = [0.229, 0.224, 0.225]\n\n# CHANGE: Model identifier updated for reporting/saving files\nMODEL_NAME = 'MobileNetV2'  # was 'EfficientNetB0'\n\nprint(\"Configuration loaded!\")\nprint(f\"Epochs: {EPOCHS}, Batch Size: {BATCH_SIZE}, Image Size: {IMG_SIZE}\")\nprint(f\"Backbone: {MODEL_NAME}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-05T10:09:40.022377Z","iopub.execute_input":"2026-06-05T10:09:40.022718Z","iopub.status.idle":"2026-06-05T10:09:40.040638Z","shell.execute_reply.started":"2026-06-05T10:09:40.022695Z","shell.execute_reply":"2026-06-05T10:09:40.039740Z"}},"outputs":[],"execution_count":null},{"id":"b870316f-52a8-4b46-b1e8-077402bd5d6e","cell_type":"code","source":"# ============================================================\n# Cell 3: CLAHE helpers  — UNCHANGED\n# These functions are identical to the EfficientNetB0 notebook.\n# ============================================================\n\ndef apply_clahe_fast(image_path, img_size=IMG_SIZE, clip_limit=2.0):\n    \"\"\"Fast CLAHE - no sharpening overhead (unchanged)\"\"\"\n    try:\n        img = cv2.imread(image_path)\n        if img is None:\n            raise ValueError('Cannot read')\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        img = cv2.resize(img, (img_size, img_size))\n\n        lab   = cv2.cvtColor(img, cv2.COLOR_RGB2LAB)\n        l, a, b = cv2.split(lab)\n        clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=(8, 8))\n        l_c   = clahe.apply(l)\n        result = cv2.cvtColor(cv2.merge([l_c, a, b]), cv2.COLOR_LAB2RGB)\n\n        return Image.fromarray(result)\n    except Exception:\n        return Image.open(image_path).convert('RGB').resize((img_size, img_size))\n\n\ndef find_image(id_code, folders):\n    for folder in folders:\n        for ext in ['.png', '.jpeg', '.jpg']:\n            p = os.path.join(BASE, folder, str(id_code) + ext)\n            if os.path.exists(p):\n                return p\n    return None\n\n\nclass FundusDataset(Dataset):\n    def __init__(self, df, transform=None):\n        self.df = df.reset_index(drop=True)\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img = apply_clahe_fast(row['filepath'], IMG_SIZE)\n        if self.transform:\n            img = self.transform(img)\n        return img, int(row['label'])\n\n\ndef mixup_batch(imgs, labels, alpha=0.2):\n    lam  = np.random.beta(alpha, alpha)\n    bs   = imgs.size(0)\n    idx  = torch.randperm(bs, device=imgs.device)\n    mixed = lam * imgs + (1 - lam) * imgs[idx]\n    return mixed, labels, labels[idx], lam\n\n\nprint(\"Fast helper functions ready!\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-05T10:09:40.041741Z","iopub.execute_input":"2026-06-05T10:09:40.042040Z","iopub.status.idle":"2026-06-05T10:09:40.055256Z","shell.execute_reply.started":"2026-06-05T10:09:40.042019Z","shell.execute_reply":"2026-06-05T10:09:40.054518Z"}},"outputs":[],"execution_count":null},{"id":"f13a8c07-d597-478d-b307-29368756c3fd","cell_type":"code","source":"# ============================================================\n# Cell 4: MobileNetV2 Feature Extractor\n# CHANGE: Replaced EfficientNetB0 (PyTorch) with MobileNetV2\n#         (TensorFlow/Keras).\n#\n# Key differences from the original EfficientNetB0_DR class:\n#   • Framework : PyTorch → TensorFlow/Keras\n#   • Backbone  : EfficientNetB0 → MobileNetV2\n#   • Weights   : ImageNet (same intent – pretrained)\n#   • include_top=False, pooling='avg'  → 1280-dim feature vector\n#     (EfficientNetB0 also produced 1280-dim; vector size unchanged)\n#   • The CNN is NOT fine-tuned end-to-end; it is used solely as a\n#     frozen feature extractor, identical to the original approach.\n# ============================================================\n\n# CHANGE: Build MobileNetV2 as a Keras model (weights=imagenet,\n#         include_top=False, pooling='avg' → global-average-pooled\n#         feature vector of shape (batch, 1280))\nmobilenet_base = MobileNetV2(\n    weights='imagenet',      # pretrained on ImageNet\n    include_top=False,       # remove classification head\n    pooling='avg',           # global-average-pool → flat 1280-d vector\n    input_shape=(IMG_SIZE, IMG_SIZE, 3)\n)\n\n# CHANGE: Freeze all layers — feature extractor only, no end-to-end training\nmobilenet_base.trainable = False\n\n# CHANGE: Expose as a named Keras Model for clarity\nfeature_extractor = Model(\n    inputs=mobilenet_base.input,\n    outputs=mobilenet_base.output,\n    name='MobileNetV2_feature_extractor'\n)\n\ntotal_params = mobilenet_base.count_params()\nprint(f'MobileNetV2 parameters: {total_params:,}')\nprint(f'Feature vector dimension: {mobilenet_base.output_shape[-1]}')\nprint(f'All layers frozen: {not mobilenet_base.trainable}')\nprint(\"MobileNetV2 feature extractor ready!\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-05T10:09:40.057251Z","iopub.execute_input":"2026-06-05T10:09:40.057555Z","iopub.status.idle":"2026-06-05T10:09:42.924659Z","shell.execute_reply.started":"2026-06-05T10:09:40.057529Z","shell.execute_reply":"2026-06-05T10:09:42.923936Z"}},"outputs":[],"execution_count":null},{"id":"7c3f9336-488f-495e-b446-02eaa2188868","cell_type":"code","source":"# ============================================================\n# Cell 5: Transforms — UNCHANGED\n# Same augmentation / normalisation as the original notebook.\n# ============================================================\n\ntrain_tfm = T.Compose([\n    T.RandomHorizontalFlip(p=0.5),\n    T.RandomVerticalFlip(p=0.3),\n    T.RandomRotation(30),\n    T.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.15, hue=0.02),\n    T.RandomAffine(degrees=0, translate=(0.1, 0.1), scale=(0.9, 1.1)),\n    T.ToTensor(),\n    T.Normalize(MEAN, STD),\n])\n\nval_tfm = T.Compose([\n    T.ToTensor(),\n    T.Normalize(MEAN, STD),\n])\n\nprint(\"Efficient transforms defined!\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-05T10:09:42.925527Z","iopub.execute_input":"2026-06-05T10:09:42.925944Z","iopub.status.idle":"2026-06-05T10:09:42.931889Z","shell.execute_reply.started":"2026-06-05T10:09:42.925920Z","shell.execute_reply":"2026-06-05T10:09:42.930993Z"}},"outputs":[],"execution_count":null},{"id":"05496030-5da9-4da0-b9c0-092c70dda92f","cell_type":"code","source":"# ============================================================\n# Cell 6: Load Data (80-20 Split)\n# ============================================================\n\nTRAIN_FOLDER = ['train_images']\n\ndf_raw = pd.read_csv(f'{BASE}/train.csv')\ndf_raw['label'] = df_raw['diagnosis']\ndf_raw['filepath'] = df_raw['id_code'].apply(\n    lambda x: find_image(x, TRAIN_FOLDER)\n)\n\ndf_raw = df_raw[df_raw['filepath'].notna()].reset_index(drop=True)\n\ndf_tr, df_te = train_test_split(\n    df_raw,\n    test_size=0.20,\n    stratify=df_raw['label'],\n    random_state=SEED\n)\n\nprint(f'Train: {len(df_tr)} | Test: {len(df_te)}')\n\nprint('\\nClass distribution (train):')\nfor i, name in enumerate(CLASS_NAMES):\n    c = (df_tr['label'] == i).sum()\n    print(f'  {name:15s}: {c:4d}')\n\ntrain_dl = DataLoader(\n    FundusDataset(df_tr, train_tfm),\n    batch_size=BATCH_SIZE,\n    shuffle=True,\n    num_workers=2,\n    pin_memory=True\n)\n\ntest_dl = DataLoader(\n    FundusDataset(df_te, val_tfm),\n    batch_size=BATCH_SIZE,\n    shuffle=False,\n    num_workers=2,\n    pin_memory=True\n)\n\ncw = compute_class_weight(\n    class_weight='balanced',\n    classes=np.arange(NUM_CLASSES),\n    y=df_tr['label'].values\n)\n\nprint('\\nClass weights:')\nprint({n: f'{w:.3f}' for n, w in zip(CLASS_NAMES, cw)})\n\nprint('✅ Data ready!')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-05T10:11:02.393636Z","iopub.execute_input":"2026-06-05T10:11:02.394108Z","iopub.status.idle":"2026-06-05T10:11:04.639814Z","shell.execute_reply.started":"2026-06-05T10:11:02.394080Z","shell.execute_reply":"2026-06-05T10:11:04.638916Z"}},"outputs":[],"execution_count":null},{"id":"d99cb1e9-cf21-47b3-8081-bf33ef2823a4","cell_type":"code","source":"# ============================================================\n# CLAHE Visualisation — UNCHANGED\n# ============================================================\n\nsample_path = df_tr.iloc[0][\"filepath\"]\n\noriginal = cv2.imread(sample_path)\noriginal = cv2.cvtColor(original, cv2.COLOR_BGR2RGB)\n\nenhanced = apply_clahe_fast(sample_path)\n\nplt.figure(figsize=(12, 5))\n\nplt.subplot(1, 2, 1)\nplt.imshow(original)\nplt.title(\"Original Fundus Image\")\nplt.axis(\"off\")\n\nplt.subplot(1, 2, 2)\nplt.imshow(enhanced)\nplt.title(\"CLAHE Enhanced Image\")\nplt.axis(\"off\")\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-05T10:11:12.459072Z","iopub.execute_input":"2026-06-05T10:11:12.459532Z","iopub.status.idle":"2026-06-05T10:11:13.140542Z","shell.execute_reply.started":"2026-06-05T10:11:12.459502Z","shell.execute_reply":"2026-06-05T10:11:13.139629Z"}},"outputs":[],"execution_count":null},{"id":"07026810-b48f-4f8a-b013-c94c8785dffd","cell_type":"code","source":"# ============================================================\n# Cell 8: MobileNetV2 Feature Extraction\n# ============================================================\n\n# Helper to denormalise PyTorch tensors back to uint8\n# so they can be fed to Keras preprocess_input.\ndef denormalize_batch(tensor_batch):\n    \"\"\"\n    Reverse PyTorch ImageNet normalisation and convert to\n    numpy uint8 array of shape (N, H, W, 3) ready for Keras.\n    \"\"\"\n    mean = torch.tensor(MEAN).view(1, 3, 1, 1)\n    std = torch.tensor(STD).view(1, 3, 1, 1)\n\n    imgs = tensor_batch.cpu() * std + mean\n    imgs = (imgs.clamp(0, 1).numpy() * 255).astype(np.uint8)\n    imgs = imgs.transpose(0, 2, 3, 1)  # NCHW → NHWC\n\n    return imgs\n\n\n# Feature extraction function using MobileNetV2 (Keras)\ndef extract_features_mobilenet(dataloader):\n    \"\"\"\n    Extract 1280-dim feature vectors from MobileNetV2\n    for every image in the dataloader.\n    \"\"\"\n\n    all_features = []\n    all_labels = []\n\n    for images, labels in tqdm(dataloader):\n\n        # 1. Convert PyTorch tensors → uint8 numpy (NHWC)\n        imgs_np = denormalize_batch(images)\n\n        # 2. MobileNetV2 preprocessing\n        imgs_pre = mobilenet_preprocess(\n            imgs_np.astype(np.float32)\n        )\n\n        # 3. Forward pass through frozen MobileNetV2\n        features = feature_extractor.predict(\n            imgs_pre,\n            verbose=0\n        )\n\n        all_features.append(features)\n        all_labels.append(labels.numpy())\n\n    return np.vstack(all_features), np.concatenate(all_labels)\n\n\nprint(\"\\nExtracting training features with MobileNetV2...\")\ntrain_features, train_labels = extract_features_mobilenet(train_dl)\n\nprint(\"Extracting test features with MobileNetV2...\")\ntest_features, test_labels = extract_features_mobilenet(test_dl)\n\nprint(\n    f\"\\n✅ Done!\"\n    f\"\\nTrain Features Shape : {train_features.shape}\"\n    f\"\\nTest Features Shape  : {test_features.shape}\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-05T10:12:50.233645Z","iopub.execute_input":"2026-06-05T10:12:50.234077Z","iopub.status.idle":"2026-06-05T10:16:52.137157Z","shell.execute_reply.started":"2026-06-05T10:12:50.234049Z","shell.execute_reply":"2026-06-05T10:16:52.136313Z"}},"outputs":[],"execution_count":null},{"id":"0104a9c8-877e-445f-be7b-39ccc0324dd2","cell_type":"code","source":"# ============================================================\n# Cell 9: SVM Training\n# Same kernel, same hyper-parameter grid, same CV strategy.\n# ============================================================\n\nprint(\"\\nTraining SVM...\")\n\nscaler = StandardScaler()\n\ntrain_scaled = scaler.fit_transform(train_features)\ntest_scaled = scaler.transform(test_features)\n\n# Small grid search\nparam_grid = {\n    'C': [1, 5, 10, 50],\n    'gamma': ['scale', 0.01],\n    'kernel': ['rbf'],\n    'class_weight': ['balanced']\n}\n\ngrid_search = GridSearchCV(\n    SVC(random_state=SEED, probability=True),\n    param_grid,\n    cv=3,\n    scoring='accuracy',\n    n_jobs=-1,\n    verbose=1\n)\n\ngrid_search.fit(train_scaled, train_labels)\n\nprint(f\"\\n✅ Best parameters: {grid_search.best_params_}\")\nprint(f\"Best CV accuracy: {grid_search.best_score_:.4f}\")\n\nbest_svm = SVC(\n    kernel='rbf',\n    C=grid_search.best_params_['C'],\n    gamma=grid_search.best_params_['gamma'],\n    class_weight='balanced',\n    random_state=SEED\n)\n\nbest_svm.fit(train_scaled, train_labels)\n\ntest_predictions = best_svm.predict(test_scaled)\n\nprint(\"\\n✅ Final SVM training completed!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-05T10:18:38.501304Z","iopub.execute_input":"2026-06-05T10:18:38.501987Z","iopub.status.idle":"2026-06-05T10:23:13.228543Z","shell.execute_reply.started":"2026-06-05T10:18:38.501956Z","shell.execute_reply":"2026-06-05T10:23:13.227731Z"}},"outputs":[],"execution_count":null},{"id":"7972ced2-163b-4877-bdbf-eec29241e1df","cell_type":"code","source":"# ============================================================\n# Cell 10: MobileNetV2 + SVM Results\n# ============================================================\n\nacc = accuracy_score(test_labels, test_predictions)\nprec = precision_score(\n    test_labels,\n    test_predictions,\n    average='weighted'\n)\nrec = recall_score(\n    test_labels,\n    test_predictions,\n    average='weighted'\n)\nf1 = f1_score(\n    test_labels,\n    test_predictions,\n    average='weighted'\n)\nkappa = cohen_kappa_score(\n    test_labels,\n    test_predictions,\n    weights='quadratic'\n)\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"MOBILENETV2 + SVM — 5-Class Diabetic Retinopathy\")\nprint(\"=\" * 60)\nprint(f\"Test Accuracy      : {acc * 100:.2f}%\")\nprint(f\"Weighted Precision : {prec:.4f}\")\nprint(f\"Weighted Recall    : {rec:.4f}\")\nprint(f\"Weighted F1-Score  : {f1:.4f}\")\nprint(f\"Quadratic Kappa    : {kappa:.4f}\")\nprint(\"=\" * 60)\n\nprint(\"\\n📊 Classification Report:\\n\")\n\nprint(\n    classification_report(\n        test_labels,\n        test_predictions,\n        target_names=CLASS_NAMES\n    )\n)\n\n# ============================================================\n# Confusion Matrix\n# ============================================================\n\ncm = confusion_matrix(test_labels, test_predictions)\n\nplt.figure(figsize=(10, 8))\n\nsns.heatmap(\n    cm,\n    annot=True,\n    fmt='d',\n    cmap='Blues',\n    xticklabels=CLASS_NAMES,\n    yticklabels=CLASS_NAMES\n)\n\nplt.title(\n    f\"MobileNetV2 + SVM | \"\n    f\"Accuracy: {acc * 100:.2f}% | \"\n    f\"Kappa: {kappa:.4f}\"\n)\n\nplt.xlabel(\"Predicted Label\")\nplt.ylabel(\"True Label\")\nplt.xticks(rotation=45)\nplt.yticks(rotation=0)\n\nplt.tight_layout()\n\nplt.savefig(\n    \"/kaggle/working/confusion_matrix_mobilenetv2.png\",\n    dpi=150,\n    bbox_inches=\"tight\"\n)\n\nplt.show()\n\nprint(\"\\n\" + \"=\" * 60)\nprint(f\"🎯 FINAL TEST ACCURACY: {acc * 100:.2f}%\")\nprint(f\"📈 WEIGHTED F1-SCORE : {f1:.4f}\")\nprint(f\"📊 QUADRATIC KAPPA   : {kappa:.4f}\")\nprint(\"=\" * 60)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-05T10:23:23.518107Z","iopub.execute_input":"2026-06-05T10:23:23.518952Z","iopub.status.idle":"2026-06-05T10:23:24.026027Z","shell.execute_reply.started":"2026-06-05T10:23:23.518911Z","shell.execute_reply":"2026-06-05T10:23:24.025176Z"}},"outputs":[],"execution_count":null},{"id":"8f35ae39-8167-4ce1-8997-8d34105ff5b3","cell_type":"code","source":"# ============================================================\n# Grad-CAM for MobileNetV2 Feature Extractor\n# ============================================================\n\nimport tensorflow as tf\nimport numpy as np\nimport cv2\nimport matplotlib.pyplot as plt\n\n# ------------------------------------------------------------\n# Find last Conv2D layer automatically\n# ------------------------------------------------------------\n\nlast_conv_layer_name = None\n\nfor layer in reversed(mobilenet_base.layers):\n    if isinstance(layer, tf.keras.layers.Conv2D):\n        last_conv_layer_name = layer.name\n        break\n\nprint(\"Last Conv Layer:\", last_conv_layer_name)\n\n# ------------------------------------------------------------\n# Build Grad-CAM model\n# ------------------------------------------------------------\n\ngrad_model = tf.keras.models.Model(\n    inputs=mobilenet_base.input,\n    outputs=[\n        mobilenet_base.get_layer(last_conv_layer_name).output,\n        mobilenet_base.output\n    ]\n)\n\n# ------------------------------------------------------------\n# Load one image from test set\n# ------------------------------------------------------------\n\nsample_img, sample_label = next(iter(test_dl))\n\nimg_tensor = sample_img[0]\n\nmean = np.array(MEAN).reshape(3,1,1)\nstd = np.array(STD).reshape(3,1,1)\n\nimg_np = img_tensor.numpy()\nimg_np = img_np * std + mean\nimg_np = np.clip(img_np, 0, 1)\n\nimg_rgb = np.transpose(img_np, (1,2,0))\n\nkeras_input = (img_rgb * 255).astype(np.float32)\nkeras_input = mobilenet_preprocess(keras_input)\nkeras_input = np.expand_dims(keras_input, axis=0)\n\n# ------------------------------------------------------------\n# Generate Grad-CAM\n# ------------------------------------------------------------\n\nwith tf.GradientTape() as tape:\n\n    conv_outputs, features = grad_model(keras_input)\n\n    loss = tf.reduce_mean(features)\n\ngrads = tape.gradient(loss, conv_outputs)\n\npooled_grads = tf.reduce_mean(\n    grads,\n    axis=(0, 1, 2)\n)\n\nconv_outputs = conv_outputs[0]\n\nheatmap = conv_outputs @ pooled_grads[..., tf.newaxis]\nheatmap = tf.squeeze(heatmap)\n\nheatmap = np.maximum(heatmap, 0)\nheatmap = heatmap / (np.max(heatmap) + 1e-8)\n\n# ------------------------------------------------------------\n# Overlay heatmap\n# ------------------------------------------------------------\n\nheatmap = cv2.resize(\n    heatmap,\n    (img_rgb.shape[1], img_rgb.shape[0])\n)\n\nheatmap_uint8 = np.uint8(255 * heatmap)\n\nheatmap_color = cv2.applyColorMap(\n    heatmap_uint8,\n    cv2.COLORMAP_JET\n)\n\nheatmap_color = cv2.cvtColor(\n    heatmap_color,\n    cv2.COLOR_BGR2RGB\n)\n\noverlay = (\n    0.6 * img_rgb +\n    0.4 * heatmap_color / 255.0\n)\n\noverlay = np.clip(overlay, 0, 1)\n\n# ------------------------------------------------------------\n# Display\n# ------------------------------------------------------------\n\nplt.figure(figsize=(15,5))\n\nplt.subplot(1,3,1)\nplt.imshow(img_rgb)\nplt.title(\"Original\")\nplt.axis(\"off\")\n\nplt.subplot(1,3,2)\nplt.imshow(heatmap, cmap=\"jet\")\nplt.title(\"Grad-CAM\")\nplt.axis(\"off\")\n\nplt.subplot(1,3,3)\nplt.imshow(overlay)\nplt.title(\"Overlay\")\nplt.axis(\"off\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-05T10:27:15.410213Z","iopub.execute_input":"2026-06-05T10:27:15.411076Z","iopub.status.idle":"2026-06-05T10:27:28.320751Z","shell.execute_reply.started":"2026-06-05T10:27:15.411044Z","shell.execute_reply":"2026-06-05T10:27:28.319910Z"}},"outputs":[],"execution_count":null},{"id":"964e8060-ddfa-4bf6-ad31-19410b5982fe","cell_type":"code","source":"# ============================================================\n# Save Models for Deployment\n# CHANGE: Filenames updated from effb0_* to mobilenetv2_*.\n#         Format changed from .pth (PyTorch) to .keras (Keras).\n# ============================================================\n\nimport joblib\n\n# CHANGE: Save MobileNetV2 feature extractor in Keras format\nfeature_extractor.save('/kaggle/working/mobilenetv2_feature_extractor.keras')\n\njoblib.dump(best_svm,  '/kaggle/working/svm_model_mobilenetv2.pkl')\njoblib.dump(scaler,    '/kaggle/working/scaler_mobilenetv2.pkl')\n\nprint(\"Saved Successfully\")\nprint(\"mobilenetv2_feature_extractor.keras\")\nprint(\"svm_model_mobilenetv2.pkl\")\nprint(\"scaler_mobilenetv2.pkl\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-05T10:28:04.563721Z","iopub.execute_input":"2026-06-05T10:28:04.564082Z","iopub.status.idle":"2026-06-05T10:28:05.006126Z","shell.execute_reply.started":"2026-06-05T10:28:04.564049Z","shell.execute_reply":"2026-06-05T10:28:05.005223Z"}},"outputs":[],"execution_count":null},{"id":"112a937b-3d6e-4f4f-9f19-8b9a520656c9","cell_type":"code","source":"# ============================================================\n# Save Paper Figures — UNCHANGED logic\n# CHANGE: File names updated to include mobilenetv2 suffix.\n# ============================================================\n\n# Confusion matrix was already saved in Cell 10.\n# Grad-CAM was already saved in Cell 11.\n# Comparison table was already saved in Cell 13.\n\nprint(\"All paper figures saved:\")\nprint(\"  /kaggle/working/confusion_matrix_mobilenetv2.png\")\nprint(\"  /kaggle/working/gradcam_mobilenetv2.png\")\nprint(\"  /kaggle/working/model_comparison_table.png\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-05T10:28:33.542921Z","iopub.execute_input":"2026-06-05T10:28:33.543471Z","iopub.status.idle":"2026-06-05T10:28:33.548166Z","shell.execute_reply.started":"2026-06-05T10:28:33.543441Z","shell.execute_reply":"2026-06-05T10:28:33.547347Z"}},"outputs":[],"execution_count":null}]}