{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "52f1271d",
      "metadata": {
        "id": "52f1271d"
      },
      "source": [
        "\n",
        "  <h1 style=\"margin:0 0 8px 0; font-size:1.8em; letter-spacing:1px;\">\n",
        "     ACCA7025 – Application of Machine Learning\n",
        "  </h1>\n",
        "  \"<h2 style=\"margin:0 0 8px 0; font-size:1.8em; letter-spacing:1px;\">\n",
        "     Student: MD Mahadi Hasan\n",
        "  </h2>\n",
        "  <h3 style=\"margin:0 0 16px 0; font-weight:400; opacity:0.85;\">\n",
        "    ACCA7025 – Application of Machine Learning\n",
        "  </h3>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "042ec0de",
      "metadata": {
        "id": "042ec0de"
      },
      "source": [
        "\n",
        "## 1. Imports & Reproducibility\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "a0b8ac34",
      "metadata": {
        "id": "a0b8ac34"
      },
      "outputs": [],
      "source": [
        "import numpy as np\n",
        "import matplotlib.pyplot as plt\n",
        "import matplotlib.gridspec as gridspec\n",
        "import seaborn as sns\n",
        "import os\n",
        "\n",
        "import tensorflow as tf\n",
        "from tensorflow.keras import layers, models, regularizers\n",
        "from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau\n",
        "from tensorflow.keras.optimizers import Adam, SGD\n",
        "\n",
        "from sklearn.metrics import (\n",
        "    classification_report,\n",
        "    confusion_matrix,\n",
        "    precision_score,\n",
        "    recall_score,\n",
        "    f1_score,\n",
        "    accuracy_score\n",
        ")\n",
        "\n",
        "# Reproducibility\n",
        "\n",
        "SEED = 42\n",
        "np.random.seed(SEED)\n",
        "tf.random.set_seed(SEED)\n",
        "\n",
        "os.makedirs(\"outputs\", exist_ok=True)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "108d186f",
      "metadata": {
        "id": "108d186f"
      },
      "source": [
        "## 2.Dataset Loading & Preprocessing\n",
        "\n",
        "**CIFAR-10** is loaded directly from Keras. Two preprocessing steps are applied:\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "f7269695",
      "metadata": {
        "id": "f7269695"
      },
      "outputs": [],
      "source": [
        "# Dataset Loading & Preprocessing\n",
        "\n",
        "CLASS_NAMES = [\n",
        "    \"airplane\", \"automobile\", \"bird\", \"cat\", \"deer\",\n",
        "    \"dog\", \"frog\", \"horse\", \"ship\", \"truck\"\n",
        "]\n",
        "\n",
        "print(\"=\" * 60)\n",
        "print(\"  CIFAR-10 Lightweight CNN Optimisation\")\n",
        "print(\"=\" * 60)\n",
        "\n",
        "(x_train, y_train), (x_test, y_test) = tf.keras.datasets.cifar10.load_data()\n",
        "\n",
        "# Normalise pixel values to [0, 1]\n",
        "x_train = x_train.astype(\"float32\") / 255.0\n",
        "x_test  = x_test.astype(\"float32\")  / 255.0\n",
        "\n",
        "# Per-channel mean subtraction (zero-centring)\n",
        "channel_mean = x_train.mean(axis=(0, 1, 2), keepdims=True)\n",
        "x_train -= channel_mean\n",
        "x_test  -= channel_mean\n",
        "\n",
        "# Flatten labels\n",
        "y_train_flat = y_train.flatten()\n",
        "y_test_flat  = y_test.flatten()\n",
        "\n",
        "print(f\"\\nTraining samples : {x_train.shape[0]}\")\n",
        "print(f\"Test samples     : {x_test.shape[0]}\")\n",
        "print(f\"Image shape      : {x_train.shape[1:]}\")\n",
        "print(f\"Classes          : {CLASS_NAMES}\\n\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "db2d091d",
      "metadata": {
        "id": "db2d091d"
      },
      "source": [
        "## 3. Data Augmentation\n",
        "\n",
        "| Transform | Purpose |\n",
        "|:---|:---|\n",
        "| `RandomFlip(\"horizontal\")` | Mirror images left-right; preserves class semantics |\n",
        "| `RandomTranslation(±10%)` | Simulate object position variation |\n",
        "| `RandomRotation(±10%)` | Handle slight camera angle differences |\n",
        "| `RandomZoom(±10%)` | Simulate scale variation |\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "0c488832",
      "metadata": {
        "id": "0c488832"
      },
      "outputs": [],
      "source": [
        "# Data Augmentation (applied during training only)\n",
        "\n",
        "data_augmentation = tf.keras.Sequential([\n",
        "    layers.RandomFlip(\"horizontal\"),\n",
        "    layers.RandomTranslation(height_factor=0.1, width_factor=0.1),\n",
        "    layers.RandomRotation(0.1),\n",
        "    layers.RandomZoom(0.1),\n",
        "], name=\"data_augmentation\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "061d275e",
      "metadata": {
        "id": "061d275e"
      },
      "source": [
        "## 4.Lightweight CNN Architecture\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "65196884",
      "metadata": {
        "id": "65196884"
      },
      "outputs": [],
      "source": [
        "\n",
        "# Lightweight CNN Architecture Builder\n",
        "def build_cnn(\n",
        "    dropout_rate=0.4,\n",
        "    optimizer=\"adam\",\n",
        "    learning_rate=0.001,\n",
        "    filter_size=(3, 3),\n",
        "    use_augmentation=True\n",
        "):\n",
        "\n",
        "    inputs = layers.Input(shape=(32, 32, 3))\n",
        "    x = inputs\n",
        "\n",
        "    if use_augmentation:\n",
        "        x = data_augmentation(x)\n",
        "\n",
        "    # Block 1\n",
        "    x = layers.Conv2D(32, filter_size, padding=\"same\", activation=\"relu\",\n",
        "                      kernel_initializer=\"he_normal\")(x)\n",
        "    x = layers.BatchNormalization()(x)\n",
        "    x = layers.Conv2D(32, filter_size, padding=\"same\", activation=\"relu\",\n",
        "                      kernel_initializer=\"he_normal\")(x)\n",
        "    x = layers.BatchNormalization()(x)\n",
        "    x = layers.MaxPooling2D(pool_size=(2, 2))(x)\n",
        "    x = layers.Dropout(dropout_rate * 0.5)(x)   # lighter dropout after pooling\n",
        "\n",
        "    # Block 2\n",
        "    x = layers.Conv2D(64, filter_size, padding=\"same\", activation=\"relu\",\n",
        "                      kernel_initializer=\"he_normal\")(x)\n",
        "    x = layers.BatchNormalization()(x)\n",
        "    x = layers.Conv2D(64, filter_size, padding=\"same\", activation=\"relu\",\n",
        "                      kernel_initializer=\"he_normal\")(x)\n",
        "    x = layers.BatchNormalization()(x)\n",
        "    x = layers.MaxPooling2D(pool_size=(2, 2))(x)\n",
        "    x = layers.Dropout(dropout_rate * 0.5)(x)\n",
        "\n",
        "    # Block 3\n",
        "    x = layers.Conv2D(128, filter_size, padding=\"same\", activation=\"relu\",\n",
        "                      kernel_initializer=\"he_normal\")(x)\n",
        "    x = layers.BatchNormalization()(x)\n",
        "    x = layers.MaxPooling2D(pool_size=(2, 2))(x)\n",
        "    x = layers.Dropout(dropout_rate)(x)\n",
        "\n",
        "    # Classification Head\n",
        "    x = layers.Flatten()(x)\n",
        "    x = layers.Dense(256, activation=\"relu\", kernel_initializer=\"he_normal\")(x)\n",
        "    x = layers.BatchNormalization()(x)\n",
        "    x = layers.Dropout(dropout_rate)(x)\n",
        "    outputs = layers.Dense(10, activation=\"softmax\")(x)\n",
        "\n",
        "    model = models.Model(inputs, outputs)\n",
        "\n",
        "    # Optimizer selection\n",
        "    if optimizer == \"adam\":\n",
        "        opt = Adam(learning_rate=learning_rate)\n",
        "    else:\n",
        "        opt = SGD(learning_rate=learning_rate, momentum=0.9, nesterov=True)\n",
        "\n",
        "    model.compile(\n",
        "        optimizer=opt,\n",
        "        loss=\"sparse_categorical_crossentropy\",\n",
        "        metrics=[\"accuracy\"]\n",
        "    )\n",
        "    return model"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "4bcc8794",
      "metadata": {
        "id": "4bcc8794"
      },
      "source": [
        "## 5. Training & Evaluation Helpers\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "9fde2135",
      "metadata": {
        "id": "9fde2135"
      },
      "outputs": [],
      "source": [
        "# Training Helper\n",
        "\n",
        "def train_model(model, epochs=20, batch_size=64, label=\"model\"):\n",
        "    print(f\"\\n{'─'*10}\")\n",
        "    print(f\"  Training: {label}\")\n",
        "    print(f\"{'─'*10}\")\n",
        "\n",
        "    callbacks = [\n",
        "        EarlyStopping(monitor=\"val_loss\", patience=10, restore_best_weights=True),\n",
        "        ReduceLROnPlateau(monitor=\"val_loss\", factor=0.5, patience=5, min_lr=1e-6, verbose=1)\n",
        "    ]\n",
        "\n",
        "    history = model.fit(\n",
        "        x_train, y_train_flat,\n",
        "        epochs=epochs,\n",
        "        batch_size=batch_size,\n",
        "        validation_split=0.1,\n",
        "        callbacks=callbacks,\n",
        "        verbose=1\n",
        "    )\n",
        "    return history\n",
        "\n",
        "# Evaluation Helper\n",
        "\n",
        "def evaluate_model(model, label=\"Model\"):\n",
        "    y_pred_probs = model.predict(x_test, verbose=0)\n",
        "    y_pred       = np.argmax(y_pred_probs, axis=1)\n",
        "\n",
        "    acc  = accuracy_score(y_test_flat, y_pred)\n",
        "    prec = precision_score(y_test_flat, y_pred, average=\"weighted\")\n",
        "    rec  = recall_score(y_test_flat, y_pred, average=\"weighted\")\n",
        "    f1   = f1_score(y_test_flat, y_pred, average=\"weighted\")\n",
        "\n",
        "    print(f\"\\n{'═'*20}\")\n",
        "    print(f\"  Results: {label}\")\n",
        "    print(f\"{'═'*20}\")\n",
        "    print(f\"  Accuracy  : {acc:.4f}\")\n",
        "    print(f\"  Precision : {prec:.4f}\")\n",
        "    print(f\"  Recall    : {rec:.4f}\")\n",
        "    print(f\"  F1-Score  : {f1:.4f}\")\n",
        "    print(f\"\\n  Per-Class Report:\")\n",
        "    print(classification_report(y_test_flat, y_pred, target_names=CLASS_NAMES))\n",
        "\n",
        "    return {\n",
        "        \"label\": label,\n",
        "        \"accuracy\": acc,\n",
        "        \"precision\": prec,\n",
        "        \"recall\": rec,\n",
        "        \"f1\": f1,\n",
        "        \"y_pred\": y_pred,\n",
        "        \"y_pred_probs\": y_pred_probs\n",
        "    }"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a8216b07",
      "metadata": {
        "id": "a8216b07"
      },
      "source": [
        "## 6. Experiment A - Baseline\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "8b5e8af4",
      "metadata": {
        "id": "8b5e8af4"
      },
      "outputs": [],
      "source": [
        "# EXPERIMENT A – Baseline (Adam, dropout=0.4, 3×3 filters)\n",
        "model_A = build_cnn(dropout_rate=0.4, optimizer=\"adam\", filter_size=(3, 3))\n",
        "model_A.summary()\n",
        "hist_A = train_model(model_A, epochs=20, label=\"Exp-A: Adam | Dropout=0.4 | 3×3\")\n",
        "res_A  = evaluate_model(model_A, label=\"Exp-A: Adam | Dropout=0.4 | 3×3\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "cf7c5e04",
      "metadata": {
        "id": "cf7c5e04"
      },
      "source": [
        "## 7. Experiment B - SGD Optimiser\n",
        "**Configuration:** SGD (lr=0.01, momentum=0.9, Nesterov=True) | Dropout = 0.4 | Filter = 3×3\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "23af86cc",
      "metadata": {
        "id": "23af86cc"
      },
      "outputs": [],
      "source": [
        "# 7. EXPERIMENT B – SGD optimizer\n",
        "model_B = build_cnn(dropout_rate=0.4, optimizer=\"sgd\",\n",
        "                    learning_rate=0.01, filter_size=(3, 3))\n",
        "hist_B = train_model(model_B, epochs=20, label=\"Exp-B: SGD | Dropout=0.4 | 3×3\")\n",
        "res_B  = evaluate_model(model_B, label=\"Exp-B: SGD | Dropout=0.4 | 3×3\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "0c203335",
      "metadata": {
        "id": "0c203335"
      },
      "source": [
        "## 8. Experiment C - Higher Dropout Rate\n",
        "**Configuration:** Adam (lr=0.001) | Dropout = **0.5** | Filter = 3×3\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "69554a53",
      "metadata": {
        "id": "69554a53"
      },
      "outputs": [],
      "source": [
        "# 8. EXPERIMENT C – Higher dropout (0.5)\n",
        "\n",
        "model_C = build_cnn(dropout_rate=0.5, optimizer=\"adam\", filter_size=(3, 3))\n",
        "hist_C = train_model(model_C, epochs=20, label=\"Exp-C: Adam | Dropout=0.5 | 3×3\")\n",
        "res_C  = evaluate_model(model_C, label=\"Exp-C: Adam | Dropout=0.5 | 3×3\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "3b64618e",
      "metadata": {
        "id": "3b64618e"
      },
      "source": [
        "\n",
        "## 9.  Experiment D - Larger Filter Size (5×5)\n",
        "**Configuration:** Adam (lr=0.001) | Dropout = 0.4 | Filter = **5×5**\n",
        "\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "0213d8d7",
      "metadata": {
        "id": "0213d8d7"
      },
      "outputs": [],
      "source": [
        "# 9. EXPERIMENT D – Larger filter size (5×5)\n",
        "model_D = build_cnn(dropout_rate=0.4, optimizer=\"adam\", filter_size=(5, 5))\n",
        "hist_D = train_model(model_D, epochs=20, label=\"Exp-D: Adam | Dropout=0.4 | 5×5\")\n",
        "res_D  = evaluate_model(model_D, label=\"Exp-D: Adam | Dropout=0.4 | 5×5\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "1f1a273e",
      "metadata": {
        "id": "1f1a273e"
      },
      "source": [
        "## 10. Training & Validation Curves\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "25f66111",
      "metadata": {
        "id": "25f66111"
      },
      "outputs": [],
      "source": [
        "# PLOT: Training & Validation Accuracy / Loss per Experiment\n",
        "def plot_history(hist, title, filename):\n",
        "    fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n",
        "    fig.suptitle(title, fontsize=14, fontweight=\"bold\")\n",
        "\n",
        "    # Accuracy\n",
        "    axes[0].plot(hist.history[\"accuracy\"],     label=\"Train Accuracy\",      color=\"#2196F3\")\n",
        "    axes[0].plot(hist.history[\"val_accuracy\"], label=\"Validation Accuracy\", color=\"#FF5722\", linestyle=\"--\")\n",
        "    axes[0].set_title(\"Accuracy\")\n",
        "    axes[0].set_xlabel(\"Epoch\")\n",
        "    axes[0].set_ylabel(\"Accuracy\")\n",
        "    axes[0].legend()\n",
        "    axes[0].grid(True, alpha=0.3)\n",
        "\n",
        "    # Loss\n",
        "    axes[1].plot(hist.history[\"loss\"],     label=\"Train Loss\",      color=\"#2196F3\")\n",
        "    axes[1].plot(hist.history[\"val_loss\"], label=\"Validation Loss\", color=\"#FF5722\", linestyle=\"--\")\n",
        "    axes[1].set_title(\"Loss\")\n",
        "    axes[1].set_xlabel(\"Epoch\")\n",
        "    axes[1].set_ylabel(\"Loss\")\n",
        "    axes[1].legend()\n",
        "    axes[1].grid(True, alpha=0.3)\n",
        "\n",
        "    plt.tight_layout()\n",
        "    plt.savefig(f\"outputs/{filename}\", dpi=150, bbox_inches=\"tight\")\n",
        "    plt.show()\n",
        "    print(f\"  -> Saved: outputs/{filename}\")\n",
        "\n",
        "plot_history(hist_A, \"Exp-A: Adam | Dropout=0.4 | 3x3\", \"expA_history.png\")\n",
        "plot_history(hist_B, \"Exp-B: SGD  | Dropout=0.4 | 3x3\", \"expB_history.png\")\n",
        "plot_history(hist_C, \"Exp-C: Adam | Dropout=0.5 | 3x3\", \"expC_history.png\")\n",
        "plot_history(hist_D, \"Exp-D: Adam | Dropout=0.4 | 5x5\", \"expD_history.png\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "5d1395c8",
      "metadata": {
        "id": "5d1395c8"
      },
      "source": [
        "## 11.Confusion Matrix\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "ebf25469",
      "metadata": {
        "id": "ebf25469"
      },
      "outputs": [],
      "source": [
        "# 11. PLOT: Confusion Matrix for Best Model (Exp-A assumed)\n",
        "def plot_confusion_matrix(res, filename):\n",
        "    cm = confusion_matrix(y_test_flat, res[\"y_pred\"])\n",
        "    cm_norm = cm.astype(float) / cm.sum(axis=1, keepdims=True)\n",
        "\n",
        "    fig, axes = plt.subplots(1, 2, figsize=(18, 7))\n",
        "    fig.suptitle(f\"Confusion Matrix — {res['label']}\", fontsize=13, fontweight=\"bold\")\n",
        "\n",
        "    # Raw counts\n",
        "    sns.heatmap(cm, annot=True, fmt=\"d\", cmap=\"Blues\",\n",
        "                xticklabels=CLASS_NAMES, yticklabels=CLASS_NAMES, ax=axes[0])\n",
        "    axes[0].set_title(\"Raw Counts\")\n",
        "    axes[0].set_xlabel(\"Predicted Label\")\n",
        "    axes[0].set_ylabel(\"True Label\")\n",
        "    axes[0].tick_params(axis=\"x\", rotation=45)\n",
        "\n",
        "    # Normalised\n",
        "    sns.heatmap(cm_norm, annot=True, fmt=\".2f\", cmap=\"Greens\",\n",
        "                xticklabels=CLASS_NAMES, yticklabels=CLASS_NAMES, ax=axes[1])\n",
        "    axes[1].set_title(\"Normalised (Row %)\")\n",
        "    axes[1].set_xlabel(\"Predicted Label\")\n",
        "    axes[1].set_ylabel(\"True Label\")\n",
        "    axes[1].tick_params(axis=\"x\", rotation=45)\n",
        "\n",
        "    plt.tight_layout()\n",
        "    plt.savefig(f\"outputs/{filename}\", dpi=150, bbox_inches=\"tight\")\n",
        "    plt.show()\n",
        "    print(f\"  -> Saved: outputs/{filename}\")\n",
        "\n",
        "plot_confusion_matrix(res_A, \"confmat_expA.png\")\n",
        "plot_confusion_matrix(res_B, \"confmat_expB.png\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "5143e26b",
      "metadata": {
        "id": "5143e26b"
      },
      "source": [
        "## 12.  Comparative Performance  All Experiments\n",
        "\n",
        "A grouped bar chart compares **Accuracy, Precision, Recall, and weighted F1-score** side-by-side across all four experiments. This visualisation makes it straightforward to identify the best-performing configuration across all metrics simultaneously and to observe whether any experiment trades recall for precision or vice versa.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "c2a9d67d",
      "metadata": {
        "id": "c2a9d67d"
      },
      "outputs": [],
      "source": [
        "# 12. PLOT: Comparative Performance Table (Bar Chart)\n",
        "\n",
        "results_list = [res_A, res_B, res_C, res_D]\n",
        "labels   = [r[\"label\"].split(\":\")[1].strip() for r in results_list]\n",
        "metrics  = [\"accuracy\", \"precision\", \"recall\", \"f1\"]\n",
        "colors   = [\"#2196F3\", \"#4CAF50\", \"#FF9800\", \"#E91E63\"]\n",
        "\n",
        "x = np.arange(len(labels))\n",
        "width = 0.2\n",
        "\n",
        "fig, ax = plt.subplots(figsize=(14, 6))\n",
        "for i, (metric, color) in enumerate(zip(metrics, colors)):\n",
        "    vals = [r[metric] for r in results_list]\n",
        "    bars = ax.bar(x + i * width, vals, width, label=metric.capitalize(), color=color, alpha=0.85)\n",
        "    for bar, val in zip(bars, vals):\n",
        "        ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.002,\n",
        "                f\"{val:.3f}\", ha=\"center\", va=\"bottom\", fontsize=8)\n",
        "\n",
        "ax.set_xticks(x + width * 1.5)\n",
        "ax.set_xticklabels(labels, rotation=20, ha=\"right\", fontsize=9)\n",
        "ax.set_ylabel(\"Score\")\n",
        "ax.set_ylim(0, 1.05)\n",
        "ax.set_title(\"Comparative Performance Across All Experiments\", fontsize=13, fontweight=\"bold\")\n",
        "ax.legend(loc=\"lower right\")\n",
        "ax.grid(axis=\"y\", alpha=0.3)\n",
        "plt.tight_layout()\n",
        "plt.savefig(\"outputs/comparative_bar.png\", dpi=150, bbox_inches=\"tight\")\n",
        "plt.show()\n",
        "print(\"  -> Saved: outputs/comparative_bar.png\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "5ad034af",
      "metadata": {
        "id": "5ad034af"
      },
      "source": [
        "## 13. Adam vs SGD - Validation Overlay"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "5a8206e5",
      "metadata": {
        "id": "5a8206e5"
      },
      "outputs": [],
      "source": [
        "# 13. PLOT: Overlay – Adam vs SGD Validation Accuracy\n",
        "\n",
        "fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n",
        "fig.suptitle(\"Adam vs SGD: Validation Accuracy & Loss Overlay\", fontsize=13, fontweight=\"bold\")\n",
        "\n",
        "# Accuracy overlay\n",
        "axes[0].plot(hist_A.history[\"val_accuracy\"], label=\"Adam (val_acc)\",\n",
        "             color=\"#2196F3\", linewidth=2)\n",
        "axes[0].plot(hist_B.history[\"val_accuracy\"], label=\"SGD  (val_acc)\",\n",
        "             color=\"#FF5722\", linewidth=2, linestyle=\"--\")\n",
        "axes[0].set_title(\"Validation Accuracy\")\n",
        "axes[0].set_xlabel(\"Epoch\")\n",
        "axes[0].set_ylabel(\"Accuracy\")\n",
        "axes[0].legend()\n",
        "axes[0].grid(True, alpha=0.3)\n",
        "\n",
        "# Loss overlay\n",
        "axes[1].plot(hist_A.history[\"val_loss\"], label=\"Adam (val_loss)\",\n",
        "             color=\"#2196F3\", linewidth=2)\n",
        "axes[1].plot(hist_B.history[\"val_loss\"], label=\"SGD  (val_loss)\",\n",
        "             color=\"#FF5722\", linewidth=2, linestyle=\"--\")\n",
        "axes[1].set_title(\"Validation Loss\")\n",
        "axes[1].set_xlabel(\"Epoch\")\n",
        "axes[1].set_ylabel(\"Loss\")\n",
        "axes[1].legend()\n",
        "axes[1].grid(True, alpha=0.3)\n",
        "\n",
        "plt.tight_layout()\n",
        "plt.savefig(\"outputs/adam_vs_sgd_overlay.png\", dpi=150, bbox_inches=\"tight\")\n",
        "plt.show()\n",
        "print(\"  -> Saved: outputs/adam_vs_sgd_overlay.png\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "e3b27850",
      "metadata": {
        "id": "e3b27850"
      },
      "source": [
        "## 14. Sample Predictions  Best Model"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "25b7411d",
      "metadata": {
        "id": "25b7411d"
      },
      "outputs": [],
      "source": [
        "# 14. PLOT: Sample Predictions (Best Model)\n",
        "\n",
        "# Reload original (non-normalised) for display only\n",
        "(_, _), (x_test_display, _) = tf.keras.datasets.cifar10.load_data()\n",
        "x_test_display = x_test_display[:25]\n",
        "\n",
        "best_model = model_A\n",
        "y_pred_sample = np.argmax(best_model.predict(x_test[:25], verbose=0), axis=1)\n",
        "\n",
        "fig, axes = plt.subplots(5, 5, figsize=(12, 12))\n",
        "fig.suptitle(\"Sample Predictions — Exp-A (Best Model)\", fontsize=13, fontweight=\"bold\")\n",
        "for i, ax in enumerate(axes.flat):\n",
        "    ax.imshow(x_test_display[i])\n",
        "    true_label = CLASS_NAMES[y_test_flat[i]]\n",
        "    pred_label = CLASS_NAMES[y_pred_sample[i]]\n",
        "    color = \"green\" if true_label == pred_label else \"red\"\n",
        "    ax.set_title(f\"T:{true_label}\\nP:{pred_label}\", fontsize=7, color=color)\n",
        "    ax.axis(\"off\")\n",
        "plt.tight_layout()\n",
        "plt.savefig(\"outputs/sample_predictions.png\", dpi=150, bbox_inches=\"tight\")\n",
        "plt.show()\n",
        "print(\"  -> Saved: outputs/sample_predictions.png\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "cc0ed547",
      "metadata": {
        "id": "cc0ed547"
      },
      "source": [
        "## 15. Results Summary Table"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "bb8d4ee0",
      "metadata": {
        "id": "bb8d4ee0"
      },
      "outputs": [],
      "source": [
        "# 15. Summary Table\n",
        "\n",
        "print(\"\\n\" + \"=\" * 70)\n",
        "print(f\"  {'Experiment':<35} {'Acc':>7} {'Prec':>7} {'Rec':>7} {'F1':>7}\")\n",
        "print(\"=\" * 70)\n",
        "for r in results_list:\n",
        "    print(f\"  {r['label']:<35} {r['accuracy']:>7.4f} {r['precision']:>7.4f} \"\n",
        "          f\"{r['recall']:>7.4f} {r['f1']:>7.4f}\")\n",
        "print(\"=\" * 70)\n",
        "\n",
        "print(\"\\n All experiments complete. Outputs saved to /outputs/\")"
      ]
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python [conda env:base] *",
      "language": "python",
      "name": "conda-base-py"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3.13.9"
    },
    "colab": {
      "provenance": []
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5
}